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: "1.0.0"
    runtime: driver

capabilities:
  network:
    allow:
      - https://api.anthropic.com

inference:
  transport: http
  endpoint: https://api.anthropic.com
  model: claude-sonnet-5
  api_key: ${ANTHROPIC_API_KEY}
  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: "1.0.0"
    runtime: driver

capabilities:
  network:
    allow:
      - https://api.openai.com

inference:
  transport: http
  endpoint: https://api.openai.com
  model: o3-mini-high
  api_key: ${OPENAI_API_KEY}
  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: "1.0.0"
    runtime: driver

capabilities:
  network:
    allow:
      - https://api.deepseek.com

inference:
  transport: http
  endpoint: https://api.deepseek.com
  model: deepseek-r1
  api_key: ${DEEPSEEK_API_KEY}
  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 gets state: "rejected" immediately.
  • 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 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 — 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

In queue mode the capsule does not read a task.md at startup — all tasks arrive as A2A messages.


Step 4 — send tasks

The capsule URL is printed to stderr at startup. Note the port — you will use it in every request:

MURMUR_CAPSULE_URL: localhost:52322

Set a shell variable for convenience, replacing 52322 with the port you see:

PORT=52322

Send the first task:

curl -s -X POST http://localhost:$PORT \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "message/send",
    "params": {
      "message": {
        "messageId": "msg-001",
        "contextId": "ctx-001",
        "role": "user",
        "parts": [{"text": "Here are some scores: alice=92, bob=85, carol=78, dave=91. How many entries are there?"}]
      }
    }
  }'

Response:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "id": "tsk_019e23bd122f77e291a065f9481325cb",
    "contextId": "ctx-001",
    "status": { "state": "submitted" }
  }
}

Save the returned id — that is the task ID you will use to poll status.

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" \
  -d '{
    "jsonrpc": "2.0",
    "id": 2,
    "method": "message/send",
    "params": {
      "message": {
        "messageId": "msg-002",
        "contextId": "ctx-001",
        "role": "user",
        "parts": [{"text": "Who has the highest score?"}]
      }
    }
  }'

Because queue_depth: 2, this second task is accepted and queued. A third task sent at the same moment would receive 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:

workdir/out/result.txt                              ← always updated to the latest result
workdir/out/result_tsk_019e23bd122f77e291a065f9481325cb.txt   ← task 1 result
workdir/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 tasks/get with the task ID from step 3:

curl -s -X POST http://localhost:$PORT \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":3,"method":"tasks/get","params":{"id":"<your_task_id>"}}'

state progresses through: submittedworkingcompleted | failed. If you poll after the task has finished you will see completed or failed directly — working is only visible during active processing.

{
  "jsonrpc": "2.0",
  "id": 3,
  "result": {
    "id": "tsk_019e23bd122f77e291a065f9481325cb",
    "contextId": "ctx-001",
    "status": { "state": "working" }
  }
}

tasks/get can look up any task by ID from the capsule's TaskRegistry, including tasks that have already finished — you are not limited to the currently-running task.


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 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.

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: 10)

── 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:

a2a_task_received   ← task-1 arrived
task_start          ← task-1 begins
session_start       ← inference loop starts for task-1
inference           ← model calls for task-1
session_end         ← inference loop ends for task-1
task_end            ← task-1 metrics written

a2a_task_received   ← task-2 arrived (was queued)
task_start          ← task-2 begins
session_start       ← inference loop starts for task-2
...
task_end            ← task-2 metrics written

Each task gets independent session_start / session_end events, so per-task token and turn counts are in the task_end line.


Step 7 — shutdown

A queue+sleep capsule waits indefinitely after the last task completes. It does not self-terminate — shutdown is external, driven by the host (mur-roost closes the task channel) or by a SIGTERM when the process is stopped directly.

MURMUR_A2A_TIMEOUT_SECS does not apply in queue+sleep mode. It only takes effect when after_task: exit or task_acceptance: single, where self-termination after an idle period is the intended behavior.


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
tasks/get + task ID Look up any task — current or historical — from the TaskRegistry