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:
- The
pepecommand-line tool. - The dashboard.
- 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.
~/.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:
- It calls the model with the conversation so far and a description of every tool the agent is allowed to call.
- If the model replies with a final answer, that answer is returned and the loop ends.
- 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.
- 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.
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:
- Allow once. Ask again next time.
- Allow for the rest of this run. Only offered while the run has taken in content from outside (see Security and sandbox): the one kind of pre-approval that actually keeps working during that window.
- Allow for the rest of this session. Kept in memory, forgotten on restart.
- Allow always. Persisted on the agent by adding the tool to its
auto_approvelist. - Deny. Never remembered, so it is asked again.
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:
triage_model: a model connection that classifies the incoming message with a fixed, built-in prompt (not a persona you write); Pepe just looks for the word “SIMPLE” in its reply.simple_model: the model connection to downgrade to (and keep, for the rest of the session) once the triage verdict is simple.
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:
- Unset (
null): the agent may manage only itself. - Empty (
[], set with--can-manage none): it may manage nobody, not even itself. A locked child, for example a client-facing agent that must not alter itself. - A list of names: exactly those agents, and no others. Include its own name to let it manage itself too.
["*"](set with--can-manage "*"): every agent. An explicit super-admin.
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.