How to configure a long-running capsule
By default a Murmur capsule exits as soon as its task finishes. This is default by design. This guide shows how to keep a capsule alive across multiple tasks.
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 |
| lifecycle.conversation | Whether tasks with the same contextId accumulate conversation history |
Step 1 — write the manifest
Start from a working agent manifest (inference block, driver artifact, network allow). Add a lifecycle: block:
name: my-worker
version: "0.1.0"
artifacts:
- name: murmur/driver-anthropic
version: "0.19.0"
runtime: driver
gateway:
endpoint: https://api.anthropic.com
api_key: ${ANTHROPIC_API_KEY}
inference:
transport: http
model: claude-sonnet-5
driver:
artifact: murmur/driver-anthropic
lifecycle:
task_acceptance: queue
after_task: sleep
queue_depth: 2
conversation: threaded
name: my-worker
version: "0.1.0"
artifacts:
- name: murmur/driver-openai
version: "0.19.0"
runtime: driver
gateway:
endpoint: https://api.openai.com/v1
api_key: ${OPENAI_API_KEY}
inference:
transport: http
model: o3-mini-high
driver:
artifact: murmur/driver-openai
lifecycle:
task_acceptance: queue
after_task: sleep
queue_depth: 2
conversation: threaded
name: my-worker
version: "0.1.0"
artifacts:
- name: murmur/driver-deepseek
version: "0.17.0"
runtime: driver
gateway:
endpoint: https://api.deepseek.com
api_key: ${DEEPSEEK_API_KEY}
inference:
transport: http
model: deepseek-r1
driver:
artifact: murmur/driver-deepseek
lifecycle:
task_acceptance: queue
after_task: sleep
queue_depth: 2
conversation: threaded
What each field does:
task_acceptance: queue— the capsule accepts incoming messages even while a task is running, up toqueue_depthpending tasks at a time. Without this, a second call while the capsule is busy is answered immediately with a task inTASK_STATE_REJECTED.after_task: sleep— instead of exiting when a task finishes, the capsule parks on the queue channel and waits for the next task. This is what keeps it alive between tasks.queue_depth: 2— sets the buffer to 2 pending tasks; you can use any positive integer. While one task is running, incoming messages queue up to this limit; any message that arrives when the buffer is full is rejected immediately.conversation: threaded— when two tasks share the samecontextId, the second task picks up the full conversation history left by the first, giving the model a continuous thread. Omit this field (or set it tostateless) to keep every task independent regardless ofcontextId.
Step 2 — install dependencies
mur install
Different ways to install artifacts
mur install fetches an artifact from the source of its scope. murmur/ artifacts come from
the built-in source, murmur-nexus/default-artifacts, with no configuration.
Option A — name the source of a scope in ~/.murmur/config.yaml:
registry:
sources:
- repo: <owner>/<repo>
token: "${GITHUB_TOKEN}"
Then install by full name, with or without a version:
mur install <owner>/<artifact-name>@<version>
Option B — pass a full GitHub reference and skip configuration entirely:
mur install github:<owner>/<repo>@<tag>
See Installing artifacts to learn more.
Step 3 — run
mur run
The capsule starts and waits for the first incoming task. You will see no immediate agent activity — this is expected.
No task.md needed
Tasks arrive at the capsule's door as A2A messages. A --task, or a task.md in the
workdir, is optional: when given, it is the first task the capsule runs, listed on the door
like any task a client sent.
Step 4 — send tasks
The capsule URL is printed at startup. Note the port — you will use it in every request:
mur run
murmur: url localhost:52222
session: ses_019ed2af53da75c2aefee84ee10c34af
Set a shell variable for convenience, replacing 52222 with the port you see:
PORT=52222
Send the first task:
curl -s -X POST http://localhost:$PORT \
-H "Content-Type: application/json" \
-H "A2A-Version: 1.0" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "SendMessage",
"params": {
"message": {
"messageId": "msg-001",
"contextId": "ctx-001",
"role": "ROLE_USER",
"parts": [{"text": "Here are some scores: alice=92, bob=85, carol=78, dave=91. How many entries are there?"}]
},
"configuration": {"returnImmediately": true}
}
}'
Response:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"task": {
"id": "tsk_019e23bd122f77e291a065f9481325cb",
"contextId": "ctx-001",
"status": { "state": "TASK_STATE_SUBMITTED", "timestamp": "2026-10-13T09:00:00.123Z" }
}
}
}
Save the returned result.task.id — that is the task ID you will use to poll status.
"returnImmediately": true is what has the door answer once it accepts the task. Without it the
door holds the request until the task settles and answers with the finished task — see
Agent Card: Answers.
Send a second task while the first is still running. Use the same contextId to continue the conversation thread:
curl -s -X POST http://localhost:$PORT \
-H "Content-Type: application/json" \
-H "A2A-Version: 1.0" \
-d '{
"jsonrpc": "2.0",
"id": 2,
"method": "SendMessage",
"params": {
"message": {
"messageId": "msg-002",
"contextId": "ctx-001",
"role": "ROLE_USER",
"parts": [{"text": "Who has the highest score?"}]
},
"configuration": {"returnImmediately": true}
}
}'
Because queue_depth: 2, this second task is accepted and queued. Up to two tasks wait while one runs; a message that arrives when two are already waiting is answered with a task in TASK_STATE_REJECTED.
When task 2 runs, the agent receives the full history from task 1 — the scores list and the answer about the count — so it can answer the follow-up without the data being repeated. Each completed task also writes a per-task result file alongside the shared out/result.txt, both in the session's records directory:
<records directory>/out/result.txt ← always updated to the latest result
<records directory>/out/result_tsk_019e23bd122f77e291a065f9481325cb.txt ← task 1 result
<records directory>/out/result_tsk_019e24bc7a3f6810b5c2d9e0a71438fd.txt ← task 2 result
To start a separate, independent thread, use a different contextId. Each contextId maintains its own history; threads do not share state with each other.
Step 5 — poll task status
Use GetTask with the task ID from step 4:
curl -s -X POST http://localhost:$PORT \
-H "Content-Type: application/json" \
-H "A2A-Version: 1.0" \
-d '{"jsonrpc":"2.0","id":3,"method":"GetTask","params":{"id":"<your_task_id>"}}'
status.state progresses through TASK_STATE_SUBMITTED → TASK_STATE_WORKING → TASK_STATE_COMPLETED | TASK_STATE_FAILED. If you poll after the task has finished you will see TASK_STATE_COMPLETED or TASK_STATE_FAILED directly — TASK_STATE_WORKING is only visible during active processing. Task states lists every state.
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"id": "tsk_019e23bd122f77e291a065f9481325cb",
"contextId": "ctx-001",
"status": { "state": "TASK_STATE_WORKING", "timestamp": "2026-10-13T09:00:01.276Z" }
}
}
GetTask can look up any task by ID from the capsule's TaskRegistry, including tasks that have already finished, not only the running one.
Step 6 — watch the trace
Use mur trace show to summarize the session. With no argument it picks the most recent session automatically:
mur trace show
Different ways to identify a session
mur trace show with no argument reads the most recent session:
mur trace show
To name another one, pass an ordinal counting back from the newest (@2), the last 4 or more
characters of its ID (3e4b), the full ID, or a path to its trace.jsonl:
mur trace show @2
mur trace show 3e4b
mur trace show ses_6801f81dd28b4a9daf434e8324c4793e
mur trace show path/to/trace.jsonl
Use --workdir <path> if your session directories are not under ./workdir. Every command
that names a session takes the same addresses — see
Session addresses.
For persistent capsules the output adds a Tasks section with a row per task:
── Session ──────────────────────────────────────
session: ses_6801f81dd28b4a9daf434e8324c4793e
capsule: my-worker v0.1.0
model: claude-sonnet-4-6
status: ok
duration: 198.5s
── Turns ────────────────────────────────────────
count: 2 (max: no cap)
── Tokens ───────────────────────────────────────
input: 403 (avg 202/turn)
output: 476 (avg 238/turn)
total: 879
── Tool calls ───────────────────────────────────
count: 0
── Skill calls ───────────────────────────────────
count: 0
── Shell calls ──────────────────────────────────
count: 0
── Compaction ───────────────────────────────────
fired: no
── Tasks ───────────────────────────────────────
task 1 tsk_19e23bd turns: 1 in: 213 out: 68 ok 1.4s
task 2 tsk_019e24bc turns: 1 in: 190 out: 408 ok 5.6s
The task count is the number of rows in the Tasks section. Each row shows the first 12 characters of the task ID, per-task turns and tokens, exit status, and duration.
The underlying trace.jsonl event sequence for a persistent session looks like:
session_start ← the capsule launched
a2a_task_received ← task-1 arrived
task_start ← task-1 begins
inference ← model calls for task-1
task_end ← task-1 metrics written
a2a_task_received ← task-2 arrived (was queued)
task_start ← task-2 begins
inference ← model calls for task-2
task_end ← task-2 metrics written
session_end ← the capsule shut down
One session_start / session_end pair frames the whole launch, however many tasks it handles. Per-task token and turn counts are in each task_end line; the session_end totals are the launch-wide sum.
Step 7 — shutdown
A queue+sleep capsule waits indefinitely after the last task completes. It does not self-terminate — shutdown is external. A capsule run directly stops on a SIGTERM. A capsule launched as a child of another capsule stops when its parent's runtime terminates it, which the parent does when it shuts the child down and when the parent itself ends. mur-roost never stops a capsule: it referees which capsules may be spawned and holds no process.
MURMUR_A2A_TIMEOUT_SECS does not apply in queue+sleep mode. It only takes effect when after_task: exit or task_acceptance: single, and only on a launch that has received no task: the capsule waits that long for one, then ends without calling the model.
Step 8 — override lifecycle from the CLI
You can switch a manifest to persistent mode without editing the file:
mur run --manifest murmur.yaml \
--lifecycle-task-acceptance queue \
--lifecycle-after-task sleep
This is useful for running the same capsule in both ephemeral mode (CI) and persistent mode (local server) without maintaining two manifests.
Summary
| Manifest setting | Effect |
|---|---|
task_acceptance: queue + after_task: sleep |
Capsule persists, processes tasks from an mpsc channel one at a time, loops back to wait after each task |
queue_depth: N |
Buffers up to N pending tasks; any positive integer — tasks beyond the limit are rejected immediately |
conversation: threaded |
Tasks sharing a contextId accumulate history — each task sees the full prior exchange for that thread |
conversation: stateless (default) |
Every task starts with a blank context regardless of contextId |
| Shutdown | External only — host closes the task channel or process receives SIGTERM; no idle self-exit |
GetTask + task ID |
Look up any task — current or historical — from the TaskRegistry |