How to decompose tasks recursively within a single capsule
When a task contains multiple sequential steps that depend on each other's output, a single capsule can decompose it on the fly: the first turn acts as a router that identifies the steps and dispatches each as a sub-task back to its own queue; every subsequent turn executes one sub-task independently. All sub-tasks run in the same capsule session and share the same workdir filesystem, so output from one step is immediately available as input to the next. This eliminates the need for a separate orchestrator capsule when the work is sequential and state flows through files.
What is A2A?
A2A (Agent-to-Agent) is a JSON-RPC 2.0 protocol for structured communication between autonomous agents. One agent sends a message to another over HTTP and receives a task ID in return. In this guide, the capsule sends messages to itself — its own A2A endpoint — to queue sub-tasks without spawning a second capsule.
The relevant manifest options are:
| Option | Controls |
|---|---|
| lifecycle.task_acceptance | Whether and how many A2A tasks the capsule accepts |
| lifecycle.after_task | What the capsule does after a task completes |
| lifecycle.queue_depth | How many tasks can be buffered while one is running |
| network.internal_port | Pins the capsule to a fixed port so self-dispatched sub-tasks always find it |
| capabilities.network.allow | Host/URL patterns the capsule may connect to — must include localhost:52222 for self-dispatch |
| capabilities.shell.allow | Shell binaries the capsule may call — must include curl for self-dispatch |
Step 1 — Create the manifest
Six manifest fields work together to make the recursive pattern possible. Create murmur.yaml:
name: poem-agent
version: "0.1.0"
context:
max_tokens: 1000000
artifacts:
- name: murmur-driver-anthropic
version: "1.0.0"
runtime: driver
- name: murmur-tool-editor
version: "1.0.0"
runtime: tool
capabilities:
network:
allow:
- https://api.anthropic.com
- localhost:52222
filesystem:
allow:
- .
shell:
allow:
- curl
network:
internal_port: 52222
inference:
max_turns: 100
transport: http
endpoint: https://api.anthropic.com
model: claude-sonnet-5
api_key: ${ANTHROPIC_API_KEY}
driver:
artifact: murmur-driver-anthropic
system_prompt_file: instructions.md
lifecycle:
task_acceptance: queue
after_task: sleep
queue_depth: 20
name: poem-agent
version: "0.1.0"
context:
max_tokens: 200000
artifacts:
- name: murmur-driver-openai
version: "1.0.0"
runtime: driver
- name: murmur-tool-editor
version: "1.0.0"
runtime: tool
capabilities:
network:
allow:
- https://api.openai.com
- localhost:52222
filesystem:
allow:
- .
shell:
allow:
- curl
network:
internal_port: 52222
inference:
max_turns: 100
transport: http
endpoint: https://api.openai.com
model: o3-mini-high
api_key: ${OPENAI_API_KEY}
driver:
artifact: murmur-driver-openai
system_prompt_file: instructions.md
lifecycle:
task_acceptance: queue
after_task: sleep
queue_depth: 20
name: poem-agent
version: "0.1.0"
context:
max_tokens: 1000000
artifacts:
- name: murmur-driver-deepseek
version: "1.0.0"
runtime: driver
- name: murmur-tool-editor
version: "1.0.0"
runtime: tool
capabilities:
network:
allow:
- https://api.deepseek.com
- localhost:52222
filesystem:
allow:
- .
shell:
allow:
- curl
network:
internal_port: 52222
inference:
max_turns: 100
transport: http
endpoint: https://api.deepseek.com
model: deepseek-r1
api_key: ${DEEPSEEK_API_KEY}
driver:
artifact: murmur-driver-deepseek
system_prompt_file: instructions.md
lifecycle:
task_acceptance: queue
after_task: sleep
queue_depth: 20
Self-targeting. network.internal_port: 52222 locks the capsule to a predictable port. Capsules normally let the OS assign a free port at startup — but the router must know its own URL in advance to embed it in curl dispatches. Fixing the port solves this. capabilities.network.allow must include that same address: the runtime blocks any outbound connection not on the allow list, including connections back to the capsule itself.
Task creation. capabilities.shell.allow: curl exposes curl as a named tool so the router can POST new tasks to the A2A endpoint. Those tasks land in the capsule's own queue, which is enabled by lifecycle.task_acceptance: queue. lifecycle.after_task: sleep keeps the capsule alive to drain it rather than exiting after the first task. lifecycle.queue_depth: 20 gives the router room to dispatch all sub-tasks in parallel without any being rejected.
Iteration headroom. inference.max_turns: 100 raises the per-task turn limit from the default of 10. The iteration sub-task may rewrite the poem and both reviews several times before both scores reach 10/10; without extra headroom it would be cut off mid-iteration.
The key ingredient. inference.system_prompt_file: instructions.md is what makes the capsule recursive. It points to the file that tells the model to decompose incoming tasks into [EXECUTE]:-prefixed sub-tasks rather than executing everything in one context. Without it the capsule is just a standard single-task agent. As a best practice, the prompt also instructs the model to issue all curl dispatches as parallel tool calls in a single turn, avoiding unnecessary round trips.
Step 2 — Install dependencies
mur install
Different ways to install artifacts
mur install needs to know where to fetch artifacts from. You have two options:
Option A — configure a registry source in ~/.murmur/config.yaml:
registry:
default: official
sources:
- name: official
type: github
repo: <owner>/<repo>
token: "${GITHUB_TOKEN}"
Then install by artifact name and version:
mur install <artifact-name@version>
Option B — pass a full GitHub reference and skip configuration entirely:
mur install github:<username>/<repo>@<tag>
See Installing artifacts to learn more.
Step 3 — Write the system prompt
The system prompt makes a single capsule serve as both router and executor. It uses an [EXECUTE]: prefix to distinguish the two roles: any incoming message without that prefix triggers routing; any message with it triggers direct execution.
Create instructions.md:
If this task starts with [EXECUTE]: strip the prefix, execute it directly,
save all output using operation write_file to the named file, exit.
Otherwise you are a router. The task is already in this message — do not read any files.
Identify the ordered sequential steps in the goal, including any iteration or
quality-gating as an explicit final step. For each step, craft a self-contained sub-task
message that names every file to read and every file to write explicitly.
Every sub-task must save its output to a named file.
Dispatch all sub-tasks as parallel tool calls in turn 0 — call curl once per sub-task,
all in a single response. Prefix each sub-task text with [EXECUTE].
Router rules:
- Call only curl. Do not call any other tool under any circumstances.
- The output files do not exist during dispatch — sub-tasks run after your session ends.
Attempting to read them will produce errors. This is expected; do not react to it.
- After all dispatches return {"state":"submitted"}, write "Done." and stop immediately.
- If a dispatch returns an error, report it in text and stop. Do not retry.
Dispatch shape:
-s -X POST http://localhost:52222 -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","method":"message/send","params":{"message":{"messageId":"task-N","role":"user","parts":[{"text":"[EXECUTE] ..."}]}},"id":N}'
The router completes in two turns: one to fire all dispatches in parallel, one to confirm submission and stop. It never calls any tool other than curl and never reads files — the task text arrives in the incoming message itself.
Four rules make this pattern reliable:
- Every sub-task is self-contained. The router names every input and output file explicitly in the sub-task message. An executor never has to infer what to read.
- Sequential ordering is implicit. The queue processes sub-tasks one at a time. A later sub-task can safely read files written by an earlier one.
- Iteration is itself a sub-task. Quality gating (e.g. "redo if not 10/10") is dispatched as a dedicated sub-task at the end of the queue. It reads the review files and rewrites the main output until the threshold is met — the router does not need to know how many iterations are required.
- Parallel dispatch is faster and safer. Firing all curl calls in a single turn eliminates the intermediate turns where the model could drift off-script between dispatches.
Step 4 — Write the task
Create task.md with the goal the router will decompose:
Write a short poem about Colombia (8–12 lines). Save it to poem.md. Then write a
review of the artistic nature, and separately an assessment of the accuracy of the poem's
content. Iterate all three if either the review or assessment is not 10/10.
Final version remains in poem.md.
Step 5 — Run the capsule
mur run
mur run
murmur: url localhost:52222
session: ses_019f01a940ce7761854e768ecbe3d399
The capsule reads task.md as its first task. Because the message does not start with [EXECUTE]:, the model enters router mode. In turn 0 it fires all four curl dispatches in parallel — write, review, assess, iterate — each prefixed with [EXECUTE]:. In turn 1 it confirms all submissions and stops. The router uses two turns total and calls no tool other than curl.
The capsule then processes each queued sub-task in order:
- Writes
poem.md - Reads
poem.md, writesreview_artistic.md - Reads
poem.md, writesassessment_accuracy.md - Reads all three files, iterates revisions until both scores reach 10/10, then saves the final poem to
poem.md
When the queue drains and the last sub-task completes, the capsule exits.
The capsule is its own queue
Because lifecycle.task_acceptance: queue and after_task: sleep are set, the capsule's A2A endpoint at localhost:52222 acts as the queue. The router dispatches to the same process, the same session, the same workdir. There is no second capsule.
Step 6 — Inspect the trace
mur trace show
Different ways to identify a session
mur trace show locates a session three ways:
No argument — most recent session:
mur trace show
Finds the lexicographically largest ses_* entry in workdir/ — always the most recently created session, because session IDs are UUID v7 (time-ordered).
Short suffix — type the last few characters:
mur trace show 3e4b
Matches any session whose ID ends with the given string. Use 4 or more characters to avoid ambiguity. Matching is case-insensitive. If the suffix matches more than one session, mur lists the candidates and asks you to be more specific.
Full session ID:
mur trace show ses_6801f81dd28b4a9daf434e8324c4793e
Resolves the session directly without scanning workdir/.
Legacy file path:
mur trace show path/to/trace.jsonl
Any argument containing / or ending in .jsonl is treated as a literal file path. Kept for backward compatibility.
Use --workdir <path> if your session directory is not ./workdir.
Other trace exploration commands
mur trace has four subcommands for exploring session output:
mur trace show — print the full trace for a session to the terminal. Accepts a session ID, suffix, or no argument (most recent session).
mur trace steps — show a turn-by-turn summary of what the agent did in a session. Pass --verbose to include a truncated summary of each tool's input.
mur trace diff — compare the traces of two sessions side by side. Useful for spotting behavioural regressions between runs.
mur trace report — generate a structured summary report from a session's trace. Covers token usage, tool calls, latency, and other session-level metrics.
The Tool calls section shows the parallel dispatch in turn 0, and the Tasks section shows each sub-task's turn count, token usage, and duration:
── Session ──────────────────────────────────────
session: ses_019f01a940ce7761854e768ecbe3d399
capsule: poem-agent v0.1.0
model: deepseek-v4-flash
status: ok
duration: 94.6s
── Turns ────────────────────────────────────────
count: 15 (max: 100)
── Tokens ───────────────────────────────────────
input: 32,116 (avg 2141/turn)
output: 7,128 (avg 475/turn)
total: 39,244
── Tool calls ───────────────────────────────────
count: 16 (16 ok, 0 error) success 100.0%
latency: avg 24ms
turn 0 curl 16ms ✓ curl 6ms ✓ curl 6ms ✓ curl 5ms ✓ ...
...
turn 4 end_turn
── Tasks ───────────────────────────────────────
task 1 tsk_019f01a9 turns: 2 in: 5,156 out: 2,250 ok 25.0s
task 2 tsk_019f01a9 turns: 2 in: 2,114 out: 512 ok 8.2s
task 3 tsk_019f01a9 turns: 3 in: 4,025 out: 818 ok 13.3s
task 4 tsk_019f01a9 turns: 3 in: 4,555 out: 1,267 ok 16.5s
task 5 tsk_019f01a9 turns: 5 in: 16,266 out: 2,281 ok 31.4s
Turn 0 opens with four curl calls in parallel — the router dispatching all sub-tasks at once. Task 1 (the router) completes in 2 turns: one to dispatch, one to confirm and stop. Tasks 2–5 are the executor sub-tasks, with task 5 spending the most turns iterating the poem and both review files until both scores reached 10/10.
Summary
| Feature / setting | How it works |
|---|---|
| Single capsule as router and executor | The system prompt routes untagged tasks by dispatching [EXECUTE]:-prefixed sub-tasks to its own A2A queue |
| Sub-tasks share the workdir | All sub-tasks run in the same session with the same working directory — files written by one step are readable by the next |
| Capsule stays alive between sub-tasks | lifecycle.after_task: sleep keeps the capsule running until the queue is fully drained |
| Queue capacity | lifecycle.queue_depth: 20 buffers all router-dispatched sub-tasks without rejection |
| Self-dispatch | capabilities.shell.allow: [curl] and capabilities.network.allow: [localhost:52222] let the router POST to its own A2A endpoint |
| Quality iteration | The router dispatches a dedicated iteration sub-task at the end of the queue that loops on the review files until the threshold is met — no external orchestration required |