Agent-to-agent routing
Let one agent hand work to another with the send_to_agent tool. You decide exactly who may call whom, one direction at a time.
Agents can message each other through the send_to_agent tool. Who may call whom is
governed by a directed allowlist: each agent’s can_message lists the agents that
it is allowed to message. A route from triage to billing does not imply a route
from billing back to triage.
When an agent routes a message, the called agent answers in a fresh run, and its reply comes back to the caller as the tool result. A hop limit and a cycle check stop chains of routed calls from looping forever.
send_to_agent never changes who’s actually talking to the user: it’s a one-off
consult, and the caller stays the agent answering the conversation. Handing the whole
conversation to another agent from now on is switch_agent, a different tool covered
below.
Adding a route
# triage hands work to billing; billing can escalate to refunds
pepe agent route triage billing
pepe agent route triage refunds
pepe agent route billing refunds
# revoke a route
pepe agent route triage billing --remove
# or set it when creating the agent
pepe agent add triage --model mock --can-message billing,refunds
Routes are stored in ~/.pepe/config.json as each agent’s can_message list:
"agents": {
"triage": { "can_message": ["billing", "refunds"] },
"billing": { "can_message": ["refunds"] },
"refunds": { "can_message": [] }
}
refunds has an empty can_message, so it answers when it is called but it cannot
call anyone back. Because the allowlist is directed, granting the route from billing
to refunds grants nothing in the reverse direction.
An agent also needs send_to_agent in its tools list before it can route at all. The
allowlist decides who it may call, and the tool is what lets it place the call.
--can-message resolve inside the agent's own
project, and the CLI refuses a route between two agents that live in different
projects.Handing off the whole conversation, for now (switch_agent)
send_to_agent is a one-off consult; switch_agent is the other thing: the agent
answering right now hands the rest of the conversation to a different agent -
reachable from a plain request (“connect me with billing”, “let me talk to support
directly”) instead of a command.
Let me talk to the billing agent directly.
The agent calls switch_agent with target: "billing". Its own reply to this turn
still goes out from the agent that’s already answering (“sure, connecting you now”);
the switch only takes effect starting with the next message. The new agent starts from
a fresh context; it doesn’t inherit this conversation’s history.
This is deliberately temporary: it lasts only until /new, which returns the
conversation to whichever agent it actually started with. It is not the same thing
as /agent NAME (or asking an agent to “connect this channel to billing from now on,
permanently”) - that’s a lasting change to the channel’s own configuration, covered in
Telegram and Webhooks, and it’s the one /new does not
undo. Use switch_agent for “put me through to billing right now”; use the permanent
kind for “this channel is always billing’s from now on.”
It uses the exact same can_message allowlist as send_to_agent: if an agent can
message a peer, it can also hand the conversation to it, no separate route to
configure. Like send_to_agent, switch_agent does not go through the human
permission gate: the route is the authorization. A channel that must never change agent
sets agent_switch_locked, which refuses it outright.
Routing and the permission gate
The route allowlist is the authorization for the send_to_agent call. An operator
already decided, in configuration, that this agent may message that agent, so the call
itself is not put through the human permission gate. It simply runs.
That is exactly why the allowlist is directed and closed by default instead of symmetric and open. The grant is narrow and explicit, one direction at a time, which is what makes an ungated call safe to allow. A symmetric allowlist would silently hand the callee a route back to its caller that nobody ever asked for.
The callee’s own risky tools are a separate question, and they are still gated. When
billing runs bash or write_file, that call goes through the permission gate just
as it would if you had talked to billing yourself. Routing lets one agent reach
another, but it never launders that agent’s permissions.
Changing routes from chat
Give an agent the manage_agent tool and it can add or remove routes
conversationally, guided by the built-in manage-routing skill, for any agent it has
can_manage authority over (unset defaults to itself only). The allow_route/
deny_route actions take {target, value}, where target is the sender and value
the recipient.
Allow yourself to message the billing agent.
The agent calls manage_agent with action: "allow_route", target itself, and
value: "billing". Since this edits configuration, it does pass through the
permission prompt: you authorize the new route before it is written to disk. Routing
is still directed, so allowing this one does not let billing message back.