Agents

Describe an agent once, its instructions, its model, and its tools, and Pepe does the rest, calling the model and running tools until it has a real answer.

What an agent is

An agent is a short description you write once: its name, its instructions (the system prompt that gives it a persona), which model it thinks with, and the list of tools it is allowed to call. A few extra settings (an iteration limit, a temperature, who it may talk to, who it may administer) round it out. That is the whole thing. The agent holds no logic of its own. Pepe does the work: it calls the model, runs any tools the model asks for, feeds the results back, and repeats until there is a final answer.

Every agent lives as one entry in a single JSON file at ~/.pepe/config.json. There is no database. You can create and edit agents three ways, and they all write to the same file:

  1. The pepe command-line tool.
  2. The dashboard.
  3. Plain conversation, by talking to an agent that has the relevant management tool.

Here is a complete agent as it appears on disk:

{
  "agents": {
    "assistant": {
      "description": "General-purpose helper",
      "model": "openrouter",
      "system_prompt": "You are a concise, helpful assistant.",
      "tools": ["bash", "read_file", "write_file", "web_search"],
      "auto_approve": [],
      "can_message": [],
      "can_manage": null,
      "hooks": [],
      "max_iterations": 12,
      "temperature": null
    }
  }
}

Your first agent

An agent needs a model connection before it can think. If you have not created one yet, the guided setup walks you through picking a provider, signing in, and choosing a model:

pepe setup

Then define an agent with a prompt and some tools:

pepe agent add assistant \
  --model openrouter \
  --prompt "You are a concise, helpful assistant." \
  --tools bash,read_file,write_file,web_search

Run a one-shot prompt against it. The reply streams to your terminal as it is produced:

pepe run assistant "What files are in the current directory?"

That single command triggers the full loop. The agent decides it needs to look at the filesystem, calls the list_dir or bash tool, reads the result, and answers you in plain language.

From the dashboard. The Agents section of the web dashboard does the same thing with a form: name, persona, model, a checklist of tools, and the admin scope. It writes the identical entry to ~/.pepe/config.json, so you can mix and match the CLI, the dashboard, and hand-editing freely.

Do it by chat

Any agent that has the manage_agent tool can create and configure other agents through conversation. This is how the very first agent (see “The owner agent” below) lets you build out the rest of your fleet without touching the CLI. A message like:

Create a new agent called researcher. Give it a persona focused on careful
web research, point it at the openrouter model, and turn on web_search and
fetch_url.

The agent calls manage_agent with action: "create", then set_persona, set_model, and add_tool for each capability. manage_agent is a risky tool: it passes through the permission gate, so on a surface that can ask (the console, a chat channel) the runtime asks you to authorize the change before it is written, and the tool itself is instructed to confirm the plan with you first. An agent may only manage the agents inside its can_manage scope (covered under Administering agents below); asking it to touch one outside that scope is politely refused.

The fields, one by one

Field What it does Default
name The agent’s addressable label. In a project it becomes a handle like acme/assistant (see below). The agent also carries a stable internal id, so this name can be changed without breaking any binding. required
description A short human note. Never sent to the model. none
model The name of a model connection. Leave it unset to use the project’s default model. project default
system_prompt The persona and instructions the agent runs with. You are Pepe, a helpful AI agent. (a seed prompt)
langfuse_prompt Fetch the persona from this prompt’s name in Langfuse instead of system_prompt. null (off)
tools The list of tool names this agent may call. Only these are offered to the model. all tools: a new agent starts with everything enabled; remove what you don’t want
auto_approve Tools this agent may run without asking for permission. ["*"] means every tool. []
can_message Other agents this one may send messages to (a directed route). []
can_manage Which agents this one may administer. See Administering agents. null (itself only)
hooks Message-flow transforms to apply, such as PII redaction. []
max_iterations The hard cap on how many model-plus-tool rounds one turn may take. 12
temperature Sampling temperature passed to the model. Unset uses the provider’s own default. provider default
triage_model A model connection, judging complexity before a session’s first turn. See Complexity-based model routing. none (off)
simple_model The model connection to downgrade to when triage_model judges a chat simple. none

How the tool-calling loop runs

When you send a message to an agent, Pepe does this:

  1. It calls the model with the conversation so far and a description of every tool the agent is allowed to call.
  2. If the model replies with a final answer, that answer is returned and the loop ends.
  3. If the model instead asks to call one or more tools, Pepe runs each tool, appends the results to the conversation, and goes back to step 1.
  4. This repeats until the model produces a final answer or the loop reaches max_iterations. If the cap is hit, the turn ends with the note (stopped: max iterations reached).

Because the results are fed back in, the model can chain steps. It can read a file, decide it needs another, read that too, then write a summary, all inside one turn. The iteration limit is the guardrail that keeps a confused agent from looping forever.

Two other gates sit in front of the model call. An agent whose model requires redaction refuses to run unless the agent has a redaction hook enabled, and a project that has hit its monthly spend cap (or its monthly customer-message cap, a separate limit) stops here with no new model calls or replies. Both fail the turn cleanly rather than silently proceeding; see Billing & limits for how those caps are set.

Streaming and events. As the loop runs it emits lifecycle events: a streamed text fragment (assistant_delta), a full assistant message (assistant), a tool call (tool_call), a refused tool (tool_denied), a tool result (tool_result), a model failover (failover), a token-usage record (usage), a final answer (done), or an error (error). The CLI, the WebSocket, and the messaging channels all render these live, which is why you see typing and tool activity as it happens rather than one blob at the end.

Long conversations: compaction

A conversation doesn’t grow forever inside the model’s context window (the amount of conversation a model can see at once). Once the estimated size crosses about 60% of it, Pepe replaces the middle of the history with a short summary the model writes of itself, keeping the system prompt and the most recent turns word for word. This is automatic and needs no configuration: the full transcript is still kept (see Traces), only what’s sent to the model gets condensed.

By default, that summarization happens once, from scratch, each time the threshold is crossed again. That is fine for most conversations, but a long-running one can hit it repeatedly, each time re-summarizing a middle that’s only gotten bigger. An agent can opt into micro_compaction instead: once the window fills, it folds exactly the oldest not-yet-covered exchange into a running summary every turn, a small, steady cost instead of a periodic stall. The tradeoff is real, which is why it’s off by default: once active, the running summary changes every turn, so the provider can no longer reuse its cached copy of the unchanged start of the prompt between calls. Worth it for a conversation long enough to hit the threshold often, not for a short one.

pepe agent add support --micro-compaction ...

Or toggle it on an existing agent from the dashboard’s agent editor, or by asking an agent with the manage_agent tool to set the micro_compaction flag on another agent.

Mentioning what else you can do

People who already use an agent for one thing often never learn it can also watch something for changes, run a recurring job, or work toward a goal until it’s actually done - nothing surfaces that beyond whatever the agent’s own persona happens to mention. capability_nudge, off by default, lets an agent add one short, natural sentence pointing at a related capability right after it successfully helps with something, when one genuinely fits - not a menu, not every turn. Discovery through use, not an onboarding blast.

pepe agent add support --capability-nudge ...

Leave it off for an agent meant to stay terse and transactional; turn it on from the dashboard’s agent editor, or by asking an agent with the manage_agent tool to set the capability_nudge flag on another agent.

Tools and the permission gate

A tool is a capability. An agent can only do what its tools list allows. Give an agent read_file but not write_file and it can look but not touch.

List every tool available in your install:

pepe tools

The built-in set covers the common ground:

Tool What it does
bash Run a shell command.
run_script Write and run a short program in Python, Node, Ruby, or Elixir.
read_file, write_file, edit_file, move_file, list_dir Work with files in the agent’s workspace.
fetch_url, web_search Read a web page or search the web.
send_file Deliver a file the agent produced on the current channel.
send_to_agent Message another agent (subject to can_message).
ask_user Ask you to pick one of a few options, as real tappable buttons/menu where the channel supports it.
schedule_task, watch Create recurring jobs and one-shot “notify me when X” watches.
manage_agent, rename_agent, enable_tool, set_route Manage agents, tools, and routing from chat.
manage_channel, end_session Connect and close messaging channels from chat.
manage_mcp, scan_skill, skill Add external tool servers and skills.
manage_plugin Install, scan, list, and remove community plugins (tools, channels) from chat.
config_get, config_set, doctor Inspect and change configuration under guardrails, run diagnostics.

Some tools only read things, so they run freely: read_file, list_dir, fetch_url, web_search, config_get, skill, docs, doctor, scan_skill, and send_to_agent (which is governed by the can_message routes instead). Everything else, including any plugin tool, is treated as risky and passes through a permission gate before it executes.

When a risky tool has not been pre-approved and the surface can ask a human (the console, a chat channel), Pepe asks you to authorize the call. You can answer:

Put a tool on auto_approve yourself to skip the prompt from the start. On surfaces with no human to ask (for example the HTTP API, a webhook, a cron job) a gated tool is refused rather than run unwatched: only what is already on auto_approve executes.

Asking you to choose

Some questions are better answered with a tap than a typed reply. ask_user lets an agent present a genuine multiple-choice question and get the pick back as part of the same turn, instead of guessing or ending its turn and hoping the next message answers the right thing. Telegram renders it as real inline buttons; the console, as a numbered menu; the dashboard chat, as clickable options. It runs freely: asking a question carries no risk of its own, so it never needs approval. But it only works where there is an interactive person to ask: the HTTP API, a webhook, or an unattended cron/watch run refuses the call outright rather than hang waiting for a button nobody can press.

Do it by chat

An agent that has just installed a plugin, or that wants a capability it does not yet hold, can enable a tool on itself with enable_tool:

Enable the web_search tool for yourself.

The agent calls enable_tool with the tool name. The tool must already exist as a built-in or an installed plugin, and the change takes effect on the agent’s next message. enable_tool is itself gated, so you authorize the grant before it is written.

The model connection

model names a connection you defined with pepe model add. Leaving it unset means the agent uses its project’s default model, so you can point a whole set of agents at one provider and switch them all by changing one default.

A model connection can carry a fallback chain. When the agent’s primary model fails with a passing error (a rate limit, a timeout, a network blip, or a 5xx), Pepe moves down the chain and retries on the next model, emitting a failover event as it does. A hard error like a bad API key or a malformed request fails fast instead, since another endpoint would not fix it.

Pepe talks to providers over the OpenAI Chat Completions protocol, so any OpenAI-compatible endpoint works with no code change.

Do it by chat

An agent with the manage_agent tool can repoint a model it administers:

Point the researcher agent at the groq-fast model.

The agent calls manage_agent with action: "set_model". The target model must be a configured connection, and the change goes through the permission gate like any other config edit.

Complexity-based model routing

An agent’s own model is treated as the good default. Optionally, a quick and cheap first check can judge whether a chat is simple enough to downgrade to a cheaper model, before the real turn even starts. No extra agent to configure, just two fields:

pepe agent add assistant \
  --model strong-expensive-model \
  --triage-model cheap-fast-model \
  --simple-model everyday-model \
  --prompt "..." \
  --tools bash,read_file,web_search

Triage runs once, on a session’s first-ever turn, never again for that same session; once a chat is judged simple it stays on the cheaper model for the rest of the conversation (the same mechanism the /model command uses to switch a session’s model, just triggered automatically instead of by hand). A complex verdict changes nothing: the session runs on the agent’s own model exactly as it would with no triage_model set at all.

Triage is a best-effort optimization, never a dependency. If the triage model does not exist, is unreachable, or just takes too long (capped at a few seconds), the turn proceeds on the agent’s own model, silently; a triage outage never blocks or breaks a conversation. simple_model must also be set for triage to run at all; there would be nowhere to downgrade to otherwise.

Every verdict shows up as its own step on that turn’s Trace (the dashboard’s per-run replay), alongside any privacy hook that ran on the message, so you can see exactly why a session ended up on one model instead of the other.

The default agent

One agent per project can be the default. The default is what runs when you do not name an agent:

pepe run "summarize this repository"

The first agent you create in the default project automatically becomes the default. Change it at any time:

pepe agent default assistant

The owner agent

The very first agent created during setup is the owner’s own agent, and it is born fully capable. It gets every tool, it is a super-admin over all other agents (can_manage is ["*"]), and all of its tool calls are pre-approved (auto_approve is ["*"]) so it never stops to ask. This is what lets you do real work through chat from the first minute, including creating and configuring every later agent. Agents you add afterward are narrower by default: you choose their tools, they manage only themselves, and their risky calls go through the permission gate.

Letting agents talk to each other

can_message is a one-way list. If agent A lists agent B, then A may send B a message with the send_to_agent tool. The reverse is not implied. Add a route from the CLI:

pepe agent route triage assistant

Now triage can hand work to assistant. Remove the route with --remove. Routes never cross a project boundary; the CLI refuses A -> B when the two are in different projects.

Do it by chat

An agent with the set_route tool can change routing conversationally. from defaults to the calling agent:

Allow yourself to message the billing agent.

The agent calls set_route with action: "allow" and to: "billing". Routing is directed, so this does not let billing message back. Because it edits config, set_route goes through the permission gate and you authorize the change.

Administering agents

can_manage controls which agents an agent may administer (create, edit, reconfigure, train) through the manage_agent tool. It is closed by default and its meaning is precise:

Grant management authority directly:

pepe agent manage supervisor "*"

Do it by chat

An admin agent uses manage_agent to shape the agents in its scope. Its actions are list, get, create, set_persona, set_model, add_tool, remove_tool, and remember (append a durable fact to the target’s memory). For example:

Give the support agent the send_file tool and add a note to its memory that
refunds over 200 need a human.

The agent calls manage_agent with action: "add_tool" and then action: "remember". Every one of these actions is gated: the agent proposes the change, you authorize it, and only then is it applied. An agent can also rename itself with the separate rename_agent tool (“From now on, call yourself scout”), which moves its workspace directory and takes effect on the next message.

Multi-tenant agents with projects

Every agent lives in a project. On a fresh install that is the single default project, which every command falls back to when you omit --project, exactly as a single-tenant install always has. Add a second project to wall a tenant off: its agents, workspaces, shared space, model connections, and routing are isolated from every other project.

An agent’s real identity is its handle. In the default project the handle is just the bare name (assistant). In another project it is qualified as project/name (acme/assistant), so the same bare name can be reused across projects without collision.

Create a project, then add agents inside it with --project:

pepe project add acme --description "Acme Corp"

pepe agent add support \
  --project acme \
  --model openrouter \
  --prompt "You are Acme's support agent." \
  --tools read_file,web_search

Add --project acme to any agent command to act inside that scope. Bare peer names in --can-message and --can-manage resolve into the agent’s own project, so routes never accidentally cross a tenant boundary. Each project can pin its own default model and default agent, or share the operator’s global provider. An agent is never promoted to the global default just by being the first one created inside a non-default project.

Both projects and agents carry a stable internal id, and every binding (routing, permissions, defaults, crons, bots, tokens) is recorded against that id, not the name. Renaming a project or an agent relabels it and moves its directory; nothing that pointed at it dangles.

Managing agents from the CLI

# Create an agent. Omit --tools to grant all tools; pass --tools "" for none.
pepe agent add NAME \
  --model MODEL \
  --prompt "..." \
  --tools t1,t2 \
  [--description "..."] \
  [--can-message b,c] \
  [--can-manage x,y | "*" | none] \
  [--hooks pii_redact] \
  [--max-iterations 12] \
  [--temperature 0.7] \
  [--triage-model MODEL] \
  [--simple-model MODEL] \
  [--default] \
  [--project PROJECT]

# List agents in a project, or every agent everywhere.
pepe agent list [--project PROJECT | --all]

# Print the fully-assembled system prompt - not just the persona field, everything Pepe
# builds around it. See "Seeing exactly what the model sees" below.
pepe agent prompt NAME [--project PROJECT]

# Directed messaging: let FROM message TO.
pepe agent route FROM TO [--remove] [--project PROJECT]

# Management authority: let ADMIN administer TARGET (or "*" for all).
pepe agent manage ADMIN TARGET [--remove] [--project PROJECT]

# Rename an agent and move its workspace directory.
pepe agent rename OLD NEW

# Delete an agent.
pepe agent remove NAME [--project PROJECT]

# Set the default agent for a project.
pepe agent default NAME [--project PROJECT]

Running an agent

The same agent is reachable four ways.

One-shot from the CLI. No session, streams to stdout.

pepe run assistant "your prompt here"

Interactive console. Keeps the conversation, so context carries between turns. Resume or separate console sessions with --session KEY.

pepe chat assistant

Over HTTP and WebSocket. Start the server, then call the OpenAI-compatible API or open a streaming WebSocket. The model field of the request names the agent.

pepe serve --port 4000
POST /v1/chat/completions
Content-Type: application/json

{
  "model": "assistant",
  "messages": [{ "role": "user", "content": "your prompt here" }]
}

The WebSocket is served at ws://localhost:4000/socket/websocket, and the health check at GET /health.

Through a messaging channel. Bind an agent to a Telegram, WhatsApp, Slack, Discord, Microsoft Teams, or Google Chat connection, or to a generic inbound webhook, and it answers there with the same loop and the same tools.

Managing a persona from Langfuse

Set langfuse_prompt to a prompt name and this agent’s persona comes from Langfuse instead of its own system_prompt/SOUL.md - edit the prompt in Langfuse and the change reaches Pepe within a few minutes, no redeploy and no touching config.json.

pepe agent add support --langfuse-prompt support-persona

Opt-in per agent - one with no langfuse_prompt set is completely unaffected, and a fetch failure (unreachable, name doesn’t resolve) falls straight back to the local persona. Setup and credentials: Langfuse.

Seeing exactly what the model sees

The system_prompt field is only the seed. What actually goes to the model as the system message also includes the agent’s persona/identity/boot files if it has them, a short behavior contract, the current time, and an index of the docs and skills it knows about, none of which shows up if you only read the field on disk. To see the whole thing, assembled exactly the way a real conversation would send it:

pepe agent prompt NAME

The dashboard’s agent edit page has the same view, under Assembled prompt, collapsed by default, since it can run long.