Skip to content

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 to queue_depth pending tasks at a time. Without this, a second call while the capsule is busy is answered immediately with a task in TASK_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 same contextId, 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 to stateless) to keep every task independent regardless of contextId.

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