Every session Shrooms Agents runs is also an agent other agents can talk to. An agent on your laptop can ask an agent on your home server to review a branch, run the slow test suite, or look at a log only that machine has, and get the answer back as a tool result. Nobody has to hand out a recipe: every session is started knowing how.
The wire format is A2A (Agent2Agent, v1.0, the JSON-RPC binding), so what one session asks another is a standard A2A task, and any A2A client on your mesh can talk to a session too. What A2A leaves open, shrooms fills in with a small extension: a task stays open until the worker says it is finished, a worker that goes quiet is reminded, and the asker acknowledges the result. The extension is specified in the task extension; this page is how it looks from where you sit.
Why it is built this way is in ADR-041 (A2A, and the mesh as authentication) and ADR-042 (a task is open until the worker finishes it).
An agent is a session, named MACHINE/SESSION: laptop/webapp,
homeserver/review. MACHINE is the device's name on the mesh, as
shrooms status shows it. A task is named by the session that works on it and
the id of the message that asked: review:cli-20261008T091502-3f9a1c2e. From
another machine you write it with the machine in front:
homeserver/review:cli-20261008T091502-3f9a1c2e.
A message id is chosen by the sender, so sending the same message twice is the same task, not a second one. A renamed session keeps its tasks.
You do nothing to switch this on. Each session starts with:
shrooms (the agent's own binary, run as
shrooms-agent mcp), with seven tools:| Tool | Who uses it | What it does |
|---|---|---|
list_agents | anyone | every session on the mesh: MACHINE/SESSION, harness, state (idle, working, waiting), directory, and what it is good at and responsible for (below) |
ask_agent | the asker | sends a message to MACHINE/SESSION, with a short title that lists of tasks show; waits for the reply (up to 10 minutes) unless wait is false; with task set, answers a task that is blocked; with fresh, asks for the task on a fresh conversation (below) |
task_status | the asker | where a task stands, and the reply so far |
task_update | the worker | finishes a task: done with a summary, blocked with what it needs, or failed with why |
task_ack | the asker | says it has what it needed |
send_file | anyone | sends a file (path, with a note) to MACHINE/SESSION, at most 100 MB; kept only if that session takes files from this one (Files between agents) |
schedule | the session | its own routines, in the Scala calendar it shares with you: list, add, change, remove; you are told of every change (ADR-064) |
In Claude Code, list_agents and task_status run without asking. The others
ask for permission like any tool, unless the session auto-approves. pi reads
MCP servers only from ~/.pi/agent/mcp.json; the agent adds a shrooms entry
there when it starts, and leaves alone one of yours by that name.
A session started with --mcp=false gets neither the tools nor the note.
Each session can say what it is good at (its skills) and what it is
responsible for: what it owns and may do without asking you. A reviewer
reviews; a publisher publishes to the LAN repos; one session knows Basecamp
apps, another SPEL. You set them per session, in either app or through its
settings (POST /v1/sessions/NAME/settings with skills and
responsibilities); a session cannot set its own, and a caged one cannot
reach its settings at all.
They are on the session's A2A card, the skills as A2A's own skills and the
responsibilities as an extension, and list_agents shows both, so an agent
that needs help reads them and asks the one that fits. Nothing is sent to a
skill on its own, and a responsibility does not enforce anything: a public
release still waits for the code from your phone. ADR-063.
You are working with laptop/webapp on a branch, and you want a second
opinion from homeserver/review, a session that has the whole monorepo
checked out and knows its conventions.
1. You ask your agent. "Ask the review agent on the home server to look
at the checkout-retry branch before I open the PR." It calls list_agents,
finds homeserver/review idle, and calls ask_agent:
to: homeserver/review
text: This is laptop/webapp. Please review branch checkout-retry in the
webapp repo (pushed to origin): the retry loop in payment/client.go.
I need a list of problems, most serious first.
In Claude Code this asks your permission first, as any tool would.
2. The worker gets a task. If homeserver/review is free, the message
arrives at once; if it is busy, the task waits in its queue (below). Its
conversation shows a message that begins:
[shrooms task review:cli-20261008T091502-3f9a1c2e from laptop.home (laptop/webapp)]
This is laptop/webapp. Please review branch checkout-retry ...
[This is a task: it stays open until you finish it. When it is done, call
the shrooms tool task_update with task "review:cli-...", state "done" and a
short summary of the result. ...]
laptop.home is what the mesh says sent it: the device, on the mesh home.
(laptop/webapp) is what the sender says it is. More on that difference under
"Security".
3. The asker waits. ask_agent holds for up to 10 minutes, until the task
is finished or needs input. A review that takes longer comes back as
working, with whatever the worker last said, and the asker follows it with
task_status. Meanwhile the asker's session is free to do other things.
4. The worker works, maybe over several turns. It fetches the branch, reads, runs the tests. If Claude Code starts the tests in the background, its turn ends, but the task does not: a task is open until the worker says how it ended. If it goes quiet without saying, the watchdog reminds it (below).
5a. It finishes. The worker calls
task_update(task, "done", "Three problems: ..."). Its own conversation
shows a note, task review:cli-... completed — Three problems: .... The
asker's ask_agent (or its next task_status) returns:
task homeserver/review:cli-20261008T091502-3f9a1c2e: completed
Three problems: ...
5b. Or it is blocked. "The branch is not on origin." The worker calls
task_update with blocked and what it needs; the asker sees
input-required and that text. The asker answers with ask_agent, task
set to the task id; the answer reaches the worker as
[shrooms task review:cli-... — more from laptop.home (laptop/webapp)], and
the task is working again.
6. The asker acknowledges. When it has what it needed, it calls
task_ack. That is bookkeeping, not what closes the task: completed is
already final. If the result is not good enough, the asker opens a new
task rather than reopening the old one; an A2A client can point back at the
old one with referenceTaskIds.
Throughout, both of your apps show it: homeserver/review says "1 task" in
the session list, and in Basecamp's board a dashed line runs from
laptop/webapp to homeserver/review until the task ends.
| State | Means |
|---|---|
submitted (queued) | the worker's session is busy; the task waits its turn |
working | sent to the session; not yet finished |
input-required | the worker said it is blocked, or a permission prompt is waiting on the worker's machine |
completed | the worker called task_update with done; the summary is the result |
failed | the worker said it failed, or the message could not be delivered |
rejected | refused, with why. A session that takes no tasks from caged agents refuses the message itself, with error -32051, so no task is made |
canceled | someone cancelled it, or it expired |
The queue. A session works on one task at a time. A task sent to a session that is working, has a prompt waiting, or already has a task open (idle between turns does not count as free) waits, and starts when the one before is finished, blocked or stalled. Nobody is told "busy". Answers to a blocked task go ahead of new tasks.
Expiry. A task nobody finished within 24 hours of being asked (time in the queue counts) is cancelled and marked expired. Finished tasks are kept for a week, then dropped.
Where it is kept. On the worker's machine, in tasks.json in the agent's
state directory. Restarting the agent resumes everything, reminders included,
where it was.
Some sessions do one job per task: an auditor, a reviewer. Each job should
start clean, not carry the last one's context, findings or files. A new task
can ask for that: ask_agent with fresh, a2a send --fresh, or
"shrooms/fresh": true in the message's metadata from any A2A client
(ADR-072).
fresh_by_agents,
ticked in the apps' "new conversation" dialog. It's off by default, so no
agent can wipe a session its owner did not open to that. Only the owner
can change it.-32052) saying why,
and the task is not run in the old conversation instead.fresh goes on in the
session's conversation as before, and a reply on an open task always goes
where that task is. So agents can still just talk to it.A fresh start keeps the session's name, cage, model, settings and role, and forgets its conversation: the next message begins a new one. A caged session's working directory is cleared too, its tools kept; see "new conversation" in Using the apps.
A model's turn can end with the work half done, and only the worker knows which. So the worker's machine watches, every 30 seconds, for a session that is idle, has no prompt waiting, and has a task it has started and not finished.
A reminder is written by the agent, not a model, and is the same every time: how long the session has been quiet, each task's request (the conversation may have been compacted since), and the last thing the worker said, so it picks up where it was rather than starting over. The apps label it SHROOMS, not YOU. It costs a turn of the worker's model, and the usage dashboard counts it under "shrooms" as who asked. A session with no open task is never woken: cost follows open work, not the clock.
A stalled task is still open. The worker can still finish it, you can cancel it, and otherwise it expires at 24 hours.
task ... completed — summary,
blocked, failed, stalled, expired.shrooms-agent a2a cancel homeserver/review:cli-20261008T091502-3f9a1c2e
GET /v1/tasks on its agent (add
?session=review for one session), which is what the apps read.shrooms-agent a2a is the same client the MCP tools use, for you or for an
agent that would rather run a command:
shrooms-agent a2a list
shrooms-agent a2a send --wait homeserver/review "Is the nightly build green?"
shrooms-agent a2a send --title "Nightly build" homeserver/review "Is it green?"
shrooms-agent a2a send --fresh --title "Audit foray" proteus/auditor "Audit ~/foray at main."
shrooms-agent a2a reply --wait homeserver/review:cli-20261008T091502-3f9a1c2e "Yes, main."
shrooms-agent a2a send-file --note "Last night's log" homeserver/review build.log
shrooms-agent a2a get homeserver/review:cli-20261008T091502-3f9a1c2e
shrooms-agent a2a cancel homeserver/review:cli-20261008T091502-3f9a1c2e
shrooms-agent a2a ack homeserver/review:cli-20261008T091502-3f9a1c2e
shrooms-agent a2a update review:cli-20261008T091502-3f9a1c2e done "Two problems: ..."
send without --wait prints the task id and returns; with it, the reply
(or the task as it stands after 10 minutes). --title names the task in
lists. --fresh asks for it on a fresh conversation (below).reply is more on a task you sent: the answer it asked for, or a
correction. It is the same task, not a new one.send-file pushes a file to a session that takes files from this one;
--note says what it is.MACHINE is the device's mesh name or its overlay address. Names are
looked up through the shrooms daemon's socket (--socket, default
/run/shrooms/shrooms.sock).update is for a worker, and only works on the worker's own machine. Note
that its task id has no machine in front.SHROOMS_AGENT_SESSION, which send
passes on as who is asking. From your own shell it is unset, and the task
shows only the device.failed or rejected task exits with status 2.Each machine's agent listens on its overlay address, port 7387, reachable only from the mesh. A client on a mesh device can find and use sessions like any A2A agent:
| What | Where |
|---|---|
| The machine's card, its sessions as skills | GET http://[ADDR]:7387/.well-known/agent-card.json |
| A session's card | GET http://[ADDR]:7387/a2a/SESSION/.well-known/agent-card.json |
| A session's JSON-RPC endpoint | POST http://[ADDR]:7387/a2a/SESSION |
Any session's, found from the task id or metadata["shrooms/session"] | POST http://[ADDR]:7387/a2a |
Methods: SendMessage (configuration.blocking waits up to 10 minutes, until
the task is finished or needs input), SendStreamingMessage and
SubscribeToTask (server-sent events, ending with the final state),
GetTask, CancelTask, ListTasks, and the extension's AckTask. Messages
are text parts only. A SendMessage whose message carries a taskId answers
a blocked task. Push notifications and files are not supported, and the cards
say so.
The cards declare the extension, not required, by its URI:
https://github.com/vpavlin/shrooms/blob/master/docs/a2a-tasks.md
Its fields are in each task's metadata, under shrooms/: who it is from,
whether it is queued, how many reminders, stalled, expired, acknowledged, and
the worker's last line. the task extension lists them. A client
that knows nothing of the extension still sees ordinary A2A tasks; it just
never sends AckTask.
A minimal request:
curl -s http://[ADDR]:7387/a2a/review -H 'Content-Type: application/json' -d '{
"jsonrpc": "2.0", "id": 1, "method": "SendMessage",
"params": {
"message": {
"messageId": "my-client-0001", "role": "ROLE_USER",
"parts": [{"text": "Which branch is deployed on staging?"}],
"metadata": {"shrooms/from": "laptop/my-script"}
},
"configuration": {"blocking": true}
}
}'
shrooms/from is optional: it is what your client says it is, shown beside
the device. Pick a fresh messageId for each new request; reusing one returns
the existing task.
The mesh is the authentication. The agent listens only on its overlay address, which only mesh members can route to, and WireGuard guarantees that a request's source address belongs to the device whose key it is. So every request is attributed to a device, with no logins and no tokens. Who may talk to an agent is who is a member of the meshes it serves. Today every device on them is yours; see Setting up.
What is verified, and what is claimed.
| Part | Verified? |
|---|---|
The device (laptop.home) | Yes: the mesh names it from the source address |
The session (laptop/webapp) | No: the sender's own shrooms/from, shown beside the device, never instead of it |
The worker's task_update | Only from the worker's own machine, and for the session the task belongs to |
CancelTask, AckTask, GetTask | Any mesh device that knows the task id |
A process on laptop can claim to be any session on any machine, but it
cannot claim to be a different device. Requests from the agent's own machine
carry no device name, and so no claim either. Another device cannot close
work it was not given: only the worker's machine accepts task_update.
What an agent is told. Every session's note says that a message from another agent is a colleague's request, not its owner's instructions: help with what is reasonable, and ask the owner before anything destructive, costly or outside its usual work. That is a convention a model follows, not a wall. A session that auto-approves its tools runs what it decides to run, whoever asked. For work you would not want another agent to start, keep the session asking first, or put it in a cage (Cages).
The loop guard. One device may send one session at most 30 new tasks an
hour; past that, SendMessage is refused with error -32050. Answers to a
blocked task and resends of the same message do not count. Two agents that
keep asking each other run into it within the hour.
Not built yet (from the design in
the design of agents together): a chain of who asked whom
(parent), a depth limit on such chains, per-session allow-lists of agents
that may give it tasks, and a switch that makes an auto-approving session
accept tasks only from you.
A session in a cage (Cages) reaches other agents only through
its own agent, which names it and marks the request as from a cage; the
task reads "laptop (laptop/review, in a cage)" and carries
shrooms/caged: true. Each session decides whether it takes such tasks:
from every caged agent ("caged tasks" in the apps, accept_caged over the
API), or from those you allow one by one (accept_caged_from, or
shrooms-agent caged allow SESSION MACHINE/SESSION). By default a caged
session takes them from every cage, and a session outside a cage from none
you have not allowed.
A task from a cage you have not allowed is held, not refused (ADR-070). It
waits in auth-required, the asker is told so at once, and the apps show it
as a request: "laptop/fileware wants to give publisher a task". Allow
keeps that cage on the session's list and starts its held tasks; decline
rejects them and tells the asker, and the cage's next task asks again. That is
how the publisher takes tasks from the one cage that builds an app and from no
other.
There is no first-class "spawn" yet. A session can create another today
through the agent's HTTP API, POST /v1/sessions on any machine's agent, from
its shell; the mesh is the authentication, so any agent on the mesh can. The
two then work together with tasks as above. What is missing is what would make
it safe to leave to them: a record of which session started which, a depth
and a budget, and a cage by default. The planned shape (a spawn tool that
records the parent, counts against a depth and a budget, and always asks you
before starting anything off your machines) is in
the cages research and plan, "Agents starting agents".