Introduction

Pepe runs AI agents on your own machine. Describe who they are, connect any OpenAI-compatible model, and let them do real work with tools. No database server, no vendor lock-in.

What Pepe is

Pepe runs AI agents on your own machine or server. You describe an agent once (a name, its instructions, the tools it may use, and which model it thinks with) and Pepe does the rest: when a request comes in, the agent works in steps, using its tools, until it has a real answer.

Agents are long-lived: conversations, channels, background jobs, not one-off requests. Pepe is built in Elixir/OTP, a technology made for exactly that shape of work, so one modest server keeps a whole team of agents running side by side without much memory or CPU.

That inner loop is the whole point. A plain chat call returns text. An agent can actually do things: read a file, run a command, search the web, call your API, and then reason about what it found and keep going. Pepe gives you that loop as a finished runtime instead of something you wire up by hand for every project.

pepe run "read package.json and tell me which dependencies are outdated"

You define the behavior once, and the same agent is reachable four ways: from the terminal, over an OpenAI-compatible HTTP API, over a streaming WebSocket, and from messaging channels like Telegram and WhatsApp. There is also a dashboard for browsing and chatting from the browser. Meet each use case where it already lives, without creating a separate agent for each channel.

The tool-calling loop

Here is the cycle Pepe runs for every turn:

  1. Send the conversation, plus the agent’s tool definitions, to the model.
  2. If the model returns tool calls, run each tool and collect its output.
  3. Append the assistant message and the tool results to the conversation.
  4. Go back to step 1. Stop when the model returns a plain answer, or when the agent hits its max_iterations safety limit.

Along the way Pepe announces each step, so any surface can show progress in real time: the answer as it streams in (assistant_delta), each tool call and its result (tool_call, tool_result), the final answer (done), and errors (error).

Risky tools (anything that runs a command or writes a file) can be made to ask you first. If you refuse, the tool never runs: the model just gets a short “denied” note (and a tool_denied event fires), so an agent never silently acts on your machine without consent.

Built-in tools. Every agent can be given tools like bash, read_file, write_file, edit_file, list_dir, fetch_url, and web_search. You choose which ones each agent gets when you create it, so a support bot and a coding agent can have very different powers.

The five surfaces

You build an agent once. Pepe then exposes it through whichever surface fits the job. Setup and management themselves happen three ways: the pepe CLI, the web dashboard, and by chat (talking in plain language to an agent that holds the matching management tool).

CLI

The pepe command is how you set things up and how you run agents from a terminal. One-shot runs stream their answer straight to stdout, and pepe chat opens an interactive session that remembers the conversation.

pepe run assistant "summarize the git log from the last week"
pepe chat assistant

Web dashboard

Run the server and open the dashboard in a browser to chat with an agent, browse past sessions, and manage agents, model connections, channels, scheduled tasks, usage, and traces from a point-and-click UI. On localhost it is open by default; you can gate it behind an operator password when you expose it.

pepe serve --port 4000
# then open http://localhost:4000

OpenAI-compatible HTTP API

Start the server and Pepe speaks the OpenAI Chat Completions protocol, so any OpenAI SDK, LangChain, or a plain curl can talk to it with no adapter. It serves POST /v1/chat/completions and GET /v1/models.

curl http://localhost:4000/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
    "model": "assistant",
    "messages": [{"role": "user", "content": "what files are in this project?"}]
  }'

Point an existing OpenAI client at http://localhost:4000/v1 and the model name becomes your agent name. See the HTTP API page for streaming, tool events, and authentication.

WebSocket

For live, token-by-token conversations in a web or mobile app, connect over a WebSocket and subscribe to the topic for your agent (agent:<name>). You receive assistant text as it streams, plus events for each tool call and result. Details and a client example are on the API page.

Messaging channels

Put the same agent in front of real users on the platforms they already use. Pepe ships gateways for Telegram, WhatsApp, Slack, Discord, Microsoft Teams, and Google Chat, plus a generic inbound webhook for anything else. Each channel binds to an agent and keeps its own conversation memory per user. See the channels page.

Defining an agent

An agent is just a name, a system prompt, a tool list, and a model. Create one from the CLI:

pepe agent add assistant \
  --prompt "You are Pepe, a helpful coding agent." \
  --tools bash,read_file,write_file,edit_file,list_dir,fetch_url,web_search \
  --default

You can also do this in the dashboard on the Agents page, which includes a form for the persona, model, and tool selection.

Do it by chat

An agent that holds the manage_agent tool can create and shape other agents straight from a conversation. Send it a plain message:

You: Create a new agent called “researcher” whose job is to dig through docs and summarize findings, and give it web_search and fetch_url.

The agent uses manage_agent to create the new agent, set its persona, and add each tool. manage_agent is deliberately guarded: the agent may only touch the agents it was explicitly allowed to manage, it is instructed to confirm the changes with you first, and because it is a risky tool each call still asks for your approval before anything is written. You see the proposed change and approve it before it takes effect.

Connecting a model

Pepe never ships a model or a key. You point it at any OpenAI-compatible provider with a model connection:

pepe model add openrouter \
  --api-key '${OPENROUTER_API_KEY}' \
  --model openai/gpt-5-chat \
  --default

The Models page in the dashboard does the same thing with a form, and can test a connection before you save it. Notice the ${OPENROUTER_API_KEY}: secrets are stored as environment-variable references and expanded only when read, so your keys are never written back to disk in plain text.

Adding a channel

Bind an agent to a messaging channel so people can talk to it where they already are. From the dashboard, the Channels page walks you through connecting a bot and choosing which agent it talks to. The channel then keeps a separate conversation memory per user.

Do it by chat

An agent that holds the manage_channel tool can stand up a Telegram bot from a conversation:

You: Add a Telegram bot named “support-bot” that talks to the support agent. The token is in the env var SUPPORT_BOT_TOKEN.

The agent uses manage_channel to add the bot and bind it to the named agent. This capability is deliberately guarded: it only touches named bots (never the protected default), it is instructed to confirm the details with you first, and it is a risky tool, so the call goes through the permission gate. Crucially, you give the name of an environment variable that holds the token, never the token itself, so the secret never passes through the chat or the model. After the change the running bot starts live, with no restart.

Design choices that keep it simple

Self-hosted, your keys, your data

Pepe never ships a model or an API key. You run it on your own machine or server, and you point it at whatever provider you want. Nothing about a conversation leaves your infrastructure except the calls you configure to your chosen model endpoint.

Model-agnostic

Because every provider is reached over the same OpenAI Chat Completions protocol, switching models is a config change, not a code change. OpenAI, OpenRouter, Together, Groq, DeepSeek, Mistral, and local servers like Ollama, LM Studio, and vLLM all work the same way. A model connection can even list fallback models, so a transient failure (a rate limit, a server error, a network blip) on one provider quietly rolls over to the next, while a bad key or a malformed request fails fast instead of retrying pointlessly.

No database server

All configuration (model connections, agents, channels, schedules) lives in a single JSON file at ~/.pepe/config.json, easy to read, edit, and back up. There is nothing to install alongside Pepe and nothing to migrate. Secrets are written as ${ENV_VAR} references and expanded only when read, so your keys are never written back to disk in plain text.

{
  "models": {
    "openrouter": {
      "base_url": "https://openrouter.ai/api/v1",
      "api_key": "${OPENROUTER_API_KEY}",
      "model": "openai/gpt-5-chat"
    }
  }
}

Isolated conversations

Each conversation runs separately from all the others. If one goes wrong, the rest never notice: a single bad turn cannot take down your other agents or conversations.

Multi-tenant when you need it

Work can be scoped to a project, isolating agents, channels, models, and usage per tenant. If you never opt in, everything lives in the default project, which every command falls back to, and you can ignore projects entirely.

Where to go next