Agent Card
Every capsule serves an A2A v1.0 AgentCard at
GET /.well-known/agent-card.json on its HTTP listener. The card parses as lf.a2a.v1.AgentCard,
as defined by a2a.proto at v1.0.1,
under a strict protobuf JSON parser. It names the session answering the address, states what the
capsule may do, lists what its listener answers, and names the frames its stream can send.
The card a capsule my-agent 0.1.0 serves on port 41873, with lifecycle.task_acceptance: single,
bash installed, shell and network granted, exports.files declared and no
network.authentication:
{
"name": "my-agent",
"description": "Murmur capsule my-agent 0.1.0",
"version": "0.1.0",
"supportedInterfaces": [
{ "url": "http://localhost:41873", "protocolBinding": "JSONRPC", "protocolVersion": "1.0" }
],
"capabilities": {
"streaming": true,
"pushNotifications": false,
"extendedAgentCard": false,
"extensions": [
{
"uri": "https://docs.murmur.nexus/reference/agent-card/#murmur-door-v1",
"description": "Every JSON-RPC method this door answers, A2A methods and murmur's own alike, the murmur ones named murmur.<Method>; whether it accepts tasks from peer capsules; and the forgetSession field a message's metadata may carry under this URI.",
"required": false,
"params": {
"methods": ["SendMessage", "SendStreamingMessage", "murmur.WatchSession", "GetTask", "ListTasks", "CancelTask", "SubscribeToTask", "murmur.StopSession"],
"peerTasks": false
}
},
{
"uri": "https://docs.murmur.nexus/reference/agent-card/#murmur-capsule-v1",
"description": "The session answering this address and what the capsule may do. Served only to authenticated callers once the door authenticates.",
"required": false,
"params": {
"sessionId": "ses_019f01a940ce7761854e768ecbe3d399",
"tools": ["bash"],
"shell": true,
"network": true,
"planes": ["files"]
}
},
{
"uri": "https://docs.murmur.nexus/reference/streaming-protocol/#murmur-stream-v1",
"description": "The murmur frames this capsule writes in addition to the A2A v1.0 events: sent on SendStreamingMessage and SubscribeToTask when the request's A2A-Extensions header names this URI, and always on murmur.WatchSession. Each is carried in a StreamResponse under metadata keyed by this URI.",
"required": false,
"params": {
"frames": ["text", "thinking", "tool-call-started", "tool-call-progress", "artifact", "gap", "lagged", "connection-ack", "capsule-closed"]
}
}
]
},
"securitySchemes": {},
"securityRequirements": [],
"defaultInputModes": ["text/plain", "application/json"],
"defaultOutputModes": ["text/plain", "application/json"],
"skills": [
{
"id": "task",
"name": "Run a task",
"description": "Runs one task given as a text message and reports its outcome.",
"tags": ["task"]
}
]
}
What murmur adds to the standard card sits in three extensions:
| Extension | Answers | Derived from |
|---|---|---|
| Door | What does this listener answer, and does it serve other capsules? | The methods POST / dispatches, and exports.peer_tasks |
| Capsule | Which session is this, and what may the capsule do? | The session, the installed artifacts, capabilities.* and the declared exports |
| Stream | Which frames can its stream send? | The streaming methods the door serves |
A capsule that declares network.authentication
serves a different public card, and moves the capsule extension to an extended card only an
authenticated caller can read — see Security.
Keys
Every key below is present on every card this runtime serves.
| Key | Type | Value |
|---|---|---|
name |
string | The capsule's name from murmur.yaml |
description |
string | Murmur capsule <name> <version> |
version |
string | The capsule's version from murmur.yaml |
supportedInterfaces |
array of objects | One entry — see supportedInterfaces |
capabilities |
object | See capabilities |
securitySchemes |
object | {}, or the bearer scheme on an authenticated door — see Security |
securityRequirements |
array | [], or one requirement on an authenticated door — see Security |
defaultInputModes |
array of strings | ["text/plain", "application/json"]. The door reads text parts, data parts and image parts — see Parts |
defaultOutputModes |
array of strings | ["text/plain", "application/json"]. The door writes text parts, and data parts in a cancel's residue artifact |
skills |
array of objects | See skills. Empty under lifecycle.task_acceptance: none |
The card carries no provider, documentationUrl, iconUrl or signatures.
supportedInterfaces
One interface: the JSON-RPC door at POST /.
| Key | Value |
|---|---|
url |
http://<host:port> — the address this capsule's listener answers on. The door speaks plain HTTP |
protocolBinding |
JSONRPC |
protocolVersion |
1.0 |
The door answers A2A v1.0 method names, and serves a JSON-RPC request only when it names version
1.0 in its A2A-Version header. Tasks, messages and parts have the A2A v1.0
ProtoJSON shape — see Tasks and messages.
capabilities
| Key | Type | Value |
|---|---|---|
capabilities.streaming |
boolean | true exactly when the door extension lists SendStreamingMessage |
capabilities.pushNotifications |
boolean | false — see Push notifications, and what is not supported |
capabilities.extendedAgentCard |
boolean | false on a public door, where every caller gets this card. true on an authenticated door |
capabilities.extensions |
array of objects | The door extension, the capsule extension, then the stream extension. An authenticated door's public card carries the door extension, then the stream extension |
A door that does not serve SendStreamingMessage —
lifecycle.task_acceptance: none — reports false and
answers it and SubscribeToTask -32004 UnsupportedOperationError.
Security
Who may call the door is set by network.authentication.
The security schemes the door does not offer are listed under A2A v1.0 conformance.
| Manifest | Door | securitySchemes |
securityRequirements |
Extended card |
|---|---|---|---|---|
No network.authentication |
Public: answers every caller that reaches the port, and ignores Authorization |
{} |
[] |
None |
network.authentication.scheme: bearer |
Authenticated: every request but the public card presents a token | bearer, an HTTP Bearer scheme |
Any valid token | The authenticated card with the capsule extension — see The extended card |
A public door's card is exactly the card at the top of this page.
Tokens
An authenticated capsule's runtime mints its tokens at launch, and mur token
prints them. A token is valid until the session ends; a restart or a --resume mints new ones.
| Credential | Scopes | Minted |
|---|---|---|
operator |
Every scope | Always |
Each name under network.authentication.credentials |
The scopes that credential lists | One per declared credential |
A scope is a door method's name, or resources/files for the
operator plane:
| Scope | Reaches |
|---|---|
SendMessage |
SendMessage |
SendStreamingMessage |
SendStreamingMessage |
murmur.WatchSession |
murmur.WatchSession |
GetTask |
GetTask |
ListTasks |
ListTasks |
CancelTask |
CancelTask |
SubscribeToTask |
SubscribeToTask |
murmur.StopSession |
murmur.StopSession |
resources/files |
The operator plane |
GetExtendedAgentCard needs no scope: every valid token may read the extended card. The four push
config methods need none either: every caller is answered that push is not
supported.
A scope decides which methods a token reaches. Which tasks those methods reach is decided by the credential — see Who sees a task.
Who sees a task
Every method that names or lists a task reads one rule:
| Caller | Sees |
|---|---|
| Any caller of a public door | Every task of the session |
The operator token |
Every task of the session |
| A declared credential's token | The tasks a request carrying that credential started |
A formation token, credential member:<caller> |
The tasks that member started |
The rule governs GetTask, ListTasks, CancelTask,
SubscribeToTask, a message's taskId and its
referenceTaskIds. A task the caller does not see is answered exactly as an
id the session never held: -32001 with metadata.taskId, and
not a task this capsule holds in a reference block. A task the runtime starts itself belongs to
the session:
| Task | On the door |
|---|---|
The launch's own task: mur run --task, or a task.md or input.txt in the workdir |
From the moment the door is announced, so the first ListTasks lists it. SubscribeToTask follows it and CancelTask reaches it like any task a client sent |
| A detached command's result, or a resumed launch's report of commands it lost | From the moment the command's result arrives, or the launch starts |
murmur.WatchSession and murmur.StopSession are the session's own methods. A credential holding either
reaches every task through it, whatever that credential started: murmur.WatchSession carries every
task's events, and murmur.StopSession cancels every live task.
The public card of an authenticated door
my-agent from the top of this page, declaring network.authentication:
{
"name": "my-agent",
"description": "Murmur capsule my-agent 0.1.0",
"version": "0.1.0",
"supportedInterfaces": [
{ "url": "http://localhost:41873", "protocolBinding": "JSONRPC", "protocolVersion": "1.0" }
],
"capabilities": {
"streaming": true,
"pushNotifications": false,
"extendedAgentCard": true,
"extensions": [
{
"uri": "https://docs.murmur.nexus/reference/agent-card/#murmur-door-v1",
"description": "Every JSON-RPC method this door answers, A2A methods and murmur's own alike, the murmur ones named murmur.<Method>; whether it accepts tasks from peer capsules; and the forgetSession field a message's metadata may carry under this URI.",
"required": false,
"params": {
"methods": ["SendMessage", "SendStreamingMessage", "murmur.WatchSession", "GetTask", "ListTasks", "CancelTask", "SubscribeToTask", "murmur.StopSession", "GetExtendedAgentCard"],
"peerTasks": false
}
},
{
"uri": "https://docs.murmur.nexus/reference/streaming-protocol/#murmur-stream-v1",
"description": "The murmur frames this capsule writes in addition to the A2A v1.0 events: sent on SendStreamingMessage and SubscribeToTask when the request's A2A-Extensions header names this URI, and always on murmur.WatchSession. Each is carried in a StreamResponse under metadata keyed by this URI.",
"required": false,
"params": {
"frames": ["text", "thinking", "tool-call-started", "tool-call-progress", "artifact", "gap", "lagged", "connection-ack", "capsule-closed"]
}
}
]
},
"securitySchemes": {
"bearer": {
"httpAuthSecurityScheme": {
"scheme": "Bearer",
"description": "A token this capsule's runtime mints at launch and accepts until the session ends."
}
}
},
"securityRequirements": [ { "schemes": { "bearer": { "list": [] } } } ],
"defaultInputModes": ["text/plain", "application/json"],
"defaultOutputModes": ["text/plain", "application/json"],
"skills": [
{
"id": "task",
"name": "Run a task",
"description": "Runs one task given as a text message and reports its outcome.",
"tags": ["task"],
"securityRequirements": [
{ "schemes": { "bearer": { "list": ["SendMessage"] } } },
{ "schemes": { "bearer": { "list": ["SendStreamingMessage"] } } }
]
}
]
}
It differs from a public door's card in these keys:
| Key | Value |
|---|---|
capabilities.extendedAgentCard |
true |
capabilities.extensions |
The door extension, then the stream extension. The capsule extension is on the extended card |
Door extension params.methods |
Gains GetExtendedAgentCard |
securitySchemes |
bearer: an httpAuthSecurityScheme with scheme Bearer |
securityRequirements |
[{"schemes": {"bearer": {"list": []}}}]: any valid token |
task skill securityRequirements |
One requirement per served task-starting method, SendMessage then SendStreamingMessage, each naming the scope it needs |
The extended card
GetExtendedAgentCard returns the extended card as its JSON-RPC result, to any valid token. It
takes no params:
curl -s -X POST http://localhost:41873/ \
-H 'Content-Type: application/json' \
-H 'A2A-Version: 1.0' \
-H "Authorization: Bearer $TOKEN" \
-d '{"jsonrpc":"2.0","id":1,"method":"GetExtendedAgentCard"}'
The result is an A2A v1.0 AgentCard: the
public card of the authenticated door with the
capsule extension added.
| Key | Value on the extended card |
|---|---|
capabilities.extensions |
The door extension, the capsule extension, then the stream extension |
| Every other key | As on the authenticated door's public card: the bearer scheme, securityRequirements, capabilities.extendedAgentCard: true, and supportedInterfaces declaring protocolVersion 1.0 |
The capsule extension's params.sessionId is at the same path on a public door's card and on the
extended card.
A public door has no extended card. It answers GetExtendedAgentCard with
-32004 UnsupportedOperationError, and its card does not list the method.
A2A v1.0 conformance
The door conforms to A2A v1.0 as
a2a.proto at v1.0.1
defines it. This section lists what it serves, what it answers as not supported, and the
extensions murmur adds.
What is served
The door has one interface:
| Interface | Value |
|---|---|
| Transport | JSON-RPC 2.0 over HTTP: one POST / per request |
| Address | The card's supportedInterfaces[0].url |
protocolBinding |
JSONRPC |
protocolVersion |
1.0 |
| Version negotiation | Every JSON-RPC request sends A2A-Version: 1.0, or is answered -32009 |
Every method of the proto's service A2AService, in its order, then murmur's own:
| Method | Answer | Where documented |
|---|---|---|
SendMessage |
Served under lifecycle.task_acceptance single or queue. -32601 under none |
Messages, Answers |
SendStreamingMessage |
Served under single or queue. -32004 under none |
Streaming Protocol |
GetTask |
Served under every task_acceptance |
GetTask |
ListTasks |
Served under every task_acceptance |
ListTasks |
CancelTask |
Served under every task_acceptance. -32002 on a task that has ended |
GetTask, Errors |
SubscribeToTask |
Served under single or queue. -32004 under none, and on a task that has ended |
SubscribeToTask |
CreateTaskPushNotificationConfig |
-32003 |
Push notifications |
GetTaskPushNotificationConfig |
-32003 |
Push notifications |
ListTaskPushNotificationConfigs |
-32003 |
Push notifications |
GetExtendedAgentCard |
Served on an authenticated door. -32004 on a public door |
The extended card |
DeleteTaskPushNotificationConfig |
-32003 |
Push notifications |
murmur.WatchSession |
Served under every task_acceptance |
murmur.WatchSession |
murmur.StopSession |
Served under every task_acceptance |
murmur.StopSession |
What is not supported
| Feature | How a request meets it | Why |
|---|---|---|
Push notification configs: the four methods, and configuration.taskPushNotificationConfig in a message |
-32003. The card says capabilities.pushNotifications: false |
A capsule's egress is closed by default, and a webhook to a client's URL would be a new egress path |
| The gRPC and HTTP+JSON bindings | Not listed in supportedInterfaces: the door's one interface is JSONRPC |
One binding is enough, and JSON-RPC is the one every A2A SDK speaks |
| The OAuth2, OpenID Connect, API key and mutual TLS security schemes | Not in securitySchemes: an authenticated door declares only bearer |
Murmur consumes credentials and never issues them. Door tokens are minted per launch and presented as bearer tokens |
| Card signatures | The card carries no signatures |
Signatures answer whether a provider issued a card, which matters when a card is discovered. A formation launches its own members, so the caller already knows where each card came from |
| Tenants | A request naming a tenant is answered -32602. The interface declares no tenant |
A capsule is one session. A tenant is refused rather than ignored |
Inside the methods it serves, the door refuses these requests:
| Request | Answer |
|---|---|
A message with a url part, or a raw part that is not an image |
-32005 |
A message whose image part breaks a limit, continues a task, or reaches a capsule on inference.transport: process |
-32602 |
SendStreamingMessage or SubscribeToTask to a door that starts no task |
-32004 |
SubscribeToTask naming a task that has ended |
-32004 |
A message whose taskId names a task that is not in TASK_STATE_INPUT_REQUIRED |
-32004 |
GetExtendedAgentCard to a public door |
-32004 |
Extensions
Every extension murmur declares has required: false, so a client that ignores all three is served.
| URI | Adds | required |
How a client activates it |
|---|---|---|---|
https://docs.murmur.nexus/reference/agent-card/#murmur-door-v1 |
The murmur. methods, the forgetSession message metadata field, and the A2A-Extensions echo of what a request used |
false |
By calling a murmur. method, or by setting forgetSession under the URI in message.metadata. A served answer names the URI in its A2A-Extensions header |
https://docs.murmur.nexus/reference/agent-card/#murmur-capsule-v1 |
Card params only: the session id and what the capsule may do |
false |
Nothing to activate. It is on a public door's card, and on an authenticated door's extended card |
https://docs.murmur.nexus/reference/streaming-protocol/#murmur-stream-v1 |
Murmur's frames on a stream, in each event's metadata under the URI |
false |
By sending A2A-Extensions: <URI> on SendStreamingMessage or SubscribeToTask; the response echoes it — see Activation. Always active on murmur.WatchSession |
The JSON-RPC door
Every JSON-RPC request is one POST / with content-type: application/json, a JSON-RPC 2.0 body,
and an A2A-Version: 1.0 header:
POST / HTTP/1.1
Content-Type: application/json
A2A-Version: 1.0
{"jsonrpc":"2.0","id":1,"method":"GetTask","params":{"id":"tsk_…"}}
The methods are listed in the door extension.
What the door answers
The door checks, in this order, on every request:
GET /.well-known/agent-card.json, the peer plane under/resources/peer/and the control surface, which takes its own token, are served without a door token.- The token, on a door declaring
network.authentication, before the path, the body or the method is read. A refusal here says nothing about whether a task, a file or a path exists. - Peer consent: a request carrying
x-murmur-task-origin: peeris refused unless the capsule declaresexports.peer_tasks.accept: true. Checked on every door, before the path, the body or the method is read. - The operator plane under
/resources/files: theresources/filesscope on a door declaringnetwork.authentication, then the plane. - Anything but a
POST /withcontent-type: application/jsonand a body is answered404. - A body that is not JSON:
-32700Invalid JSON payload, withid: null. - A body that is not a JSON-RPC 2.0 request:
-32600Request payload validation error. - The
A2A-Versionheader:-32009unless it names1.0. - A completion, a request carrying
x-murmur-task-origin: completion: addressed to another session,-31001; otherwise the door answers it — see Completion errors. - The method:
-32601Method not foundfor a method the door extension does not list. A public door answersGetExtendedAgentCardwith-32004instead, a door that starts no task answersSendStreamingMessageandSubscribeToTaskwith-32004, and every door answers the four push config methods with-32003. - The scope of the method, on a door declaring
network.authentication. paramsthat is present and is not an object:-32602.- A
tenantinparamsother than"":-32602. - The method itself, whose errors are listed under Errors. On
SendMessageandSendStreamingMessagethat starts with reading the message, itsforgetSessionfield included.
A refusal at steps 2, 3, 4 and 11 is an HTTP status and a JSON body, never a JSON-RPC envelope:
| Case | Status | www-authenticate |
Body error |
|---|---|---|---|
No Authorization header |
401 |
Bearer realm="<capsule name>" |
unauthenticated |
An Authorization header that is not Bearer <token>, a token this session did not mint, or more than one Authorization header |
401 |
Bearer realm="<capsule name>", error="invalid_token" |
invalid_token |
| A peer-origin request to a capsule that does not accept peer tasks | 403 |
None | peer_not_accepted |
| A valid token whose credential lacks the scope | 403 |
Bearer realm="<capsule name>", error="insufficient_scope", scope="<scope>" |
insufficient_scope |
The body is {"error": "<code>", "message": "<sentence>"}. The insufficient_scope message names
the credential and the scope it lacks: credential 'watcher' does not reach SendMessage. The
peer_not_accepted response is byte-identical for every request it refuses. The scheme name
Bearer matches in any case; the token matches exactly.
Every answer from step 6 on is a JSON-RPC response with HTTP status 200, except a scope refusal at
step 11.
The door speaks plain HTTP, so a token sent across a network travels in clear text. Put a TLS terminator in front of a door that is reached off the host.
JSON-RPC request
| Member | Required | Accepted |
|---|---|---|
jsonrpc |
yes | Exactly "2.0" |
method |
yes | A string, matched exactly against the door extension's method names |
id |
yes | A string or an integer. A request without one, a notification, is refused |
params |
no | An object. Omitted, it reads as {} |
The body is one object; a batch array is refused. Members beyond these four are ignored. A refusal
at step 7 echoes the request's id when the id itself is valid, and is null otherwise.
A2A-Version
Every JSON-RPC request names the A2A version it speaks in the A2A-Version HTTP header. The door
speaks 1.0.
A2A-Version header |
Door |
|---|---|
1.0, or 1.0.<digits> such as 1.0.1 |
Serves the request |
Absent or empty, which A2A reads as version 0.3 |
-32009 |
Any other value: 0.3, 1, 1.1, 2.0, 1.0x |
-32009 |
More than one A2A-Version header line, whatever their values |
-32009 |
The header name matches in any case, and surrounding whitespace in the value is ignored. A query parameter does not set the version.
Every method is negotiated: the A2A methods, murmur.WatchSession, murmur.StopSession and
completions alike. The refusal is one JSON body, also on SendStreamingMessage and
murmur.WatchSession, and nothing is
started or stopped:
{"jsonrpc": "2.0", "id": 1,
"error": {"code": -32009,
"message": "A2A version '' is not supported; this agent speaks 1.0",
"data": [{"@type": "type.googleapis.com/google.rpc.ErrorInfo",
"reason": "VERSION_NOT_SUPPORTED",
"domain": "a2a-protocol.org",
"metadata": {"requestedVersion": "", "supportedVersions": "1.0"}}]}}
GET /.well-known/agent-card.json, the resource plane, the peer plane and the
control surface read no A2A-Version.
Every request murmur itself sends to a door carries A2A-Version: 1.0: mur ps, mur watch,
mur cancel, mur stop, call-member,
delegate-task and its completions, a
plan's capsule step, and murmur:message/send.
Errors
The standard JSON-RPC errors carry no data:
| Code | Message | Answers |
|---|---|---|
-32700 |
Invalid JSON payload |
A body that is not JSON. id is null |
-32600 |
Request payload validation error |
A body that is not a JSON-RPC 2.0 request |
-32601 |
Method not found |
A method the door extension does not list |
-32602 |
Names the parameter | params that is not an object, a tenant, a message the door cannot read in full, a configuration.returnImmediately that is not a boolean, a message naming a waiting task in another contextId, a GetTask, CancelTask or SubscribeToTask with no id, a ListTasks parameter it cannot read, or a forgetSession that is malformed, names a taskId, or reaches a capsule not on inference.transport: process |
-32603 |
Names the failure | A request the door accepted and could not hand to the session |
Every A2A error carries error.data, an array holding one google.rpc.ErrorInfo:
| Key | Value |
|---|---|
@type |
type.googleapis.com/google.rpc.ErrorInfo |
reason |
The error's reason, below |
domain |
a2a-protocol.org |
metadata |
An object of string values, below. {} when the error has none |
| Code | Error | reason |
The door answers it | metadata |
|---|---|---|---|---|
-32001 |
TaskNotFoundError |
TASK_NOT_FOUND |
On GetTask, CancelTask, SubscribeToTask, naming an id this session never held or a task the caller does not see, and on a message whose taskId names one |
taskId |
-32002 |
TaskNotCancelableError |
TASK_NOT_CANCELABLE |
On CancelTask for a task that had already ended. The task is left unchanged |
taskId; state, the state it ended in, spelled as GetTask spells it: TASK_STATE_COMPLETED, TASK_STATE_FAILED, TASK_STATE_REJECTED or TASK_STATE_CANCELED |
-32003 |
PushNotificationNotSupportedError |
PUSH_NOTIFICATION_NOT_SUPPORTED |
On the four push config methods, and on a message asking for push — see Push notifications | {} |
-32004 |
UnsupportedOperationError |
UNSUPPORTED_OPERATION |
On GetExtendedAgentCard to a public door, and on SendStreamingMessage and SubscribeToTask to a door that starts no task, with {}. On a message whose taskId names a task that is not waiting for input — see Continuing a task. On SubscribeToTask naming a task that has ended |
taskId and state, on a message or a SubscribeToTask naming a task |
-32005 |
ContentTypeNotSupportedError |
CONTENT_TYPE_NOT_SUPPORTED |
On a message carrying a url part, or a raw part that is not an image — see Parts |
partIndex, the first refused part's 0-based index; mediaType, when that part declared one |
-32006 |
InvalidAgentResponseError |
INVALID_AGENT_RESPONSE |
Never | — |
-32007 |
ExtendedAgentCardNotConfiguredError |
EXTENDED_AGENT_CARD_NOT_CONFIGURED |
Never | — |
-32008 |
ExtensionSupportRequiredError |
EXTENSION_SUPPORT_REQUIRED |
Never | — |
-32009 |
VersionNotSupportedError |
VERSION_NOT_SUPPORTED |
On any request whose A2A-Version is not 1.0 |
requestedVersion, the header's value as sent: "" when it was absent, and every line's value joined with , when there was more than one; supportedVersions, 1.0 |
{"jsonrpc": "2.0", "id": 3,
"error": {"code": -32002,
"message": "Task cannot be canceled: it is already TASK_STATE_COMPLETED",
"data": [{"@type": "type.googleapis.com/google.rpc.ErrorInfo",
"reason": "TASK_NOT_CANCELABLE",
"domain": "a2a-protocol.org",
"metadata": {"taskId": "tsk_…", "state": "TASK_STATE_COMPLETED"}}]}}
{"jsonrpc": "2.0", "id": 4,
"error": {"code": -32005,
"message": "message.parts[1] is a url part; this agent reads text parts, data parts and image raw parts only",
"data": [{"@type": "type.googleapis.com/google.rpc.ErrorInfo",
"reason": "CONTENT_TYPE_NOT_SUPPORTED",
"domain": "a2a-protocol.org",
"metadata": {"partIndex": "1", "mediaType": "application/pdf"}}]}}
A completion's errors, -31001 and -31002, are murmur's own and are listed under
Completion errors.
Push notifications
The card says capabilities.pushNotifications: false. The door answers each request below
-32003 PushNotificationNotSupportedError, with the message
push notifications are not supported by this agent and metadata {}:
| Request | Answered |
|---|---|
CreateTaskPushNotificationConfig, GetTaskPushNotificationConfig, ListTaskPushNotificationConfigs, DeleteTaskPushNotificationConfig |
At step 10, to every caller the door lets in, before any scope or params check. The door extension does not list them |
SendMessage or SendStreamingMessage whose configuration.taskPushNotificationConfig is present and not null |
Once the message is read, before any task starts. SendStreamingMessage answers it as one JSON body, with no stream |
{"jsonrpc": "2.0", "id": 1,
"error": {"code": -32003,
"message": "push notifications are not supported by this agent",
"data": [{"@type": "type.googleapis.com/google.rpc.ErrorInfo",
"reason": "PUSH_NOTIFICATION_NOT_SUPPORTED",
"domain": "a2a-protocol.org",
"metadata": {}}]}}
Tenants
The card's interface names no tenant, and the door serves none. A request to any method the door
serves, murmur's own included, whose params.tenant is present and is anything but "" is answered
-32602 at step 13, before anything starts, stops or is cancelled:
Invalid params: tenant "acme" names a tenant; this agent serves no tenants, so omit tenant
The value is shown as JSON, so "tenant": 7 reads tenant 7. "", the unset value, is served as
if the member were absent. A method the door does not serve is still -32601, a push config method
still -32003, and a wrong A2A-Version still -32009.
Tasks and messages
Every task, message, part and artifact on the JSON-RPC door is A2A v1.0 ProtoJSON: camelCase field names and enum value names. The door reads field names and enum values in that spelling only.
Task states
status.state on the wire |
Meaning | Murmur's word |
|---|---|---|
TASK_STATE_SUBMITTED |
Accepted and queued | submitted |
TASK_STATE_WORKING |
Running | working |
TASK_STATE_INPUT_REQUIRED |
Waiting for a reply — see Continuing a task | input-required |
TASK_STATE_COMPLETED |
Ended with an answer | completed |
TASK_STATE_FAILED |
Ended without one | failed |
TASK_STATE_REJECTED |
Refused, busy or closing, and never run | rejected |
TASK_STATE_CANCELED |
Stopped by CancelTask or murmur.StopSession |
canceled |
The door never produces TASK_STATE_AUTH_REQUIRED: it authenticates a request before any task
exists, so no task waits for authentication.
Murmur's word is the spelling murmur uses off the wire:
murmur:message/send's task-result.state, mur output and the
trace. Every task status on the door and on its streams carries the
TASK_STATE_* name.
Status timestamps
Every status the door writes carries timestamp: when that status was recorded, in RFC 3339 UTC
with milliseconds and a Z, such as 2026-10-13T09:00:00.123Z.
| Status | timestamp |
|---|---|
A task read from the session's task record: GetTask, ListTasks, CancelTask, SendMessage's answer, and on a stream the opening task and the terminal status |
The task's last change: the same change that moves it to the front of ListTasks |
A refused TASK_STATE_REJECTED task the door never took |
When the door refused it |
A status a stream writes as it happens: a TASK_STATE_WORKING progress status, a TASK_STATE_INPUT_REQUIRED status, a carrier |
When it was written |
A timestamp is the exact instant the door compares statusTimestampAfter with,
so a cutoff copied from a task's status.timestamp includes that task.
Messages
SendMessage and SendStreamingMessage take params.message, an A2A Message. The door reads it
in full or refuses it; no part is skipped.
| Field | Required | Accepted |
|---|---|---|
messageId |
yes | A non-empty string |
role |
yes | ROLE_USER only. A message to this agent is from the user |
parts |
yes | A non-empty array of parts |
taskId |
no | A string naming the task this message continues — see Continuing a task. "" names none |
contextId |
no | A string. A new task without one is given a fresh ctx_… |
referenceTaskIds |
no | An array of non-empty strings naming at most 16 distinct tasks — see Referenced tasks |
metadata |
no | An object. The door reads one key, the door extension's URI, which holds forgetSession. Every other key, and a metadata that is not an object, is ignored |
extensions |
no | Ignored |
The checks run in this order, and the first failure answers:
params.messageis an object. A bare message asparamsis refused.messageId,role, thenparts, as in the table.- Every part sets exactly one of
text,raw,urlanddata. text,raw,url,mediaTypeandfilenameare strings where present.taskIdandcontextId, thenreferenceTaskIds, as in the table.- The door extension's key in
metadata, then itsforgetSession— seeforgetSession. - A
configuration.returnImmediatelythat is present, notnulland not a boolean — see Answers. - The first
urlpart, orrawpart that is not an image:-32005. - A
configuration.taskPushNotificationConfigthat is present and notnull:-32003. - A
forgetSession: trueto a capsule not oninference.transport: process:-32602. - The image parts, in order: see Image parts.
Steps 1 to 7, 10 and 11 answer -32602. Every refusal comes before the door looks up a task or starts
one: a refused message starts no task, writes no trace record, reaches no model and delivers nothing
to a waiting task. Unknown fields are ignored.
Parts
| Part | The door | The agent receives |
|---|---|---|
{"text": "…"} |
Reads it, whatever its mediaType |
The text |
{"data": <any JSON>} |
Reads it, null included, whatever its mediaType |
The value in a fenced block — see below |
{"raw": "…"} with mediaType image/png, image/jpeg, image/gif or image/webp |
Checks it — see Image parts | An image after the message's text |
Any other {"raw": "…"}, and every {"url": "…"} |
Refuses the message with -32005 |
Nothing |
| Part field | The agent receives |
|---|---|
mediaType, filename |
On a data part, in the fence's opening line. Not on a text part |
metadata |
Never |
The agent reads the parts in order, joined by a newline. A data part is its value pretty-printed in a
Markdown code fence labelled data, followed by media-type=<mediaType> and filename=<filename>
when the part has them. Whitespace, backticks and = in those values become _. The fence is one
backtick longer than the longest run of backticks in the value, and at least three, so nothing in the
value closes it:
summarise
````data media-type=application/json
{
"note": "a ``` b",
"rows": [
1,
2
]
}
````
A task whose trust class is untrusted has the whole text, fence included, wrapped in the untrusted fence.
Every text part the door writes has mediaType text/plain, and every data part
application/json.
Image parts
An image part is a raw part, its base64 bytes in raw, whose mediaType is image/png,
image/jpeg, image/gif or image/webp. The agent receives each image after the message's text,
in part order, stored and shown to the model as a tool's returned image
is. A message carrying only images starts a task.
The door refuses the whole message with -32602, naming message.parts[<i>], when an image part:
| Refusal | Limit or rule |
|---|---|
Its raw is not base64 |
— |
Its bytes are not an image of its mediaType |
The first bytes must be those of the declared type |
| It is larger than the limit | context.images.max_bytes |
| It is one image more than the limit | context.images.max_per_turn images per message |
Its message names a taskId |
Images go on the message that starts a task |
The capsule runs on inference.transport: process |
The harness takes no images from the door |
The task's task_start record lists each image under
attachments.
Continuing a task
SendMessage and SendStreamingMessage route a message by the task its taskId names:
| The message names | Answer |
|---|---|
No task, or "" |
A new task: TASK_STATE_SUBMITTED, or TASK_STATE_REJECTED when the door has no room or the session is closing |
| A task this session never held, or one the caller does not see | -32001 |
| A task that has ended | -32004, naming its state. Send a new message without taskId, in the same contextId, to start a new task |
A TASK_STATE_SUBMITTED or TASK_STATE_WORKING task |
-32004, naming its state. A task takes a message only while it waits in TASK_STATE_INPUT_REQUIRED |
A TASK_STATE_INPUT_REQUIRED task, with a contextId other than the task's |
-32602 |
A TASK_STATE_INPUT_REQUIRED task |
The message's text is the task's reply, and the task is answered in TASK_STATE_WORKING. The door's capacity is not consulted |
A message that names no task never reaches a waiting one. SendStreamingMessage answers each
refusal as one JSON body before any event, except a new task refused because the capsule is busy
or its session is closing, which is a stream of one TASK_STATE_REJECTED
task event. How a stream ends and how a reply continues
it is under What SendStreamingMessage writes.
Referenced tasks
referenceTaskIds names tasks the message refers to. Each id is kept once, in first-occurrence
order, recorded on the task's task_start, and named to the
agent in a block after the parts, as each task stood when the message arrived:
<the parts>
Referenced tasks:
- tsk_a: completed
```response
four
```
- tsk_b: failed: the driver failed
- tsk_c: working
- tsk_d: not a task this capsule holds
| Referenced task | Its line |
|---|---|
| Live | - <id>: <state> |
completed, with a response |
- <id>: completed, then the response in a fence labelled response |
| Ended otherwise, with a final status message | - <id>: <state>: <message>, the message on one line |
| Never held, or one the sender does not see | - <id>: not a task this capsule holds |
States are murmur's words. A message without referenceTaskIds gets no block.
Answers
SendMessage answers a SendMessageResponse: {"task": <Task>} for a new, rejected or continued
task, and {"message": <Message>} for a completion the door accepted.
When a task is answered depends on configuration.returnImmediately:
configuration.returnImmediately |
The door answers |
|---|---|
false, null or absent, or no configuration |
Once the task settles, with the task as GetTask reports it at that moment |
true |
Once the task is accepted: TASK_STATE_SUBMITTED for a new task, TASK_STATE_WORKING for a reply to a waiting task |
| Anything else | -32602 configuration.returnImmediately must be a boolean. Nothing starts |
A task settles when it ends in TASK_STATE_COMPLETED, TASK_STATE_FAILED, TASK_STATE_CANCELED
or TASK_STATE_REJECTED, or pauses in TASK_STATE_INPUT_REQUIRED. A completed task carries its
response artifact, and a paused one its prompt artifact. A reply to the paused task is answered
the same way.
These are answered at once, whatever returnImmediately says:
- every refusal, as a JSON-RPC error;
- a new task rejected because the door has no room or the session is closing, in
TASK_STATE_REJECTED; - a completion, with its
message.
A bare SendMessage whose task completes:
{"jsonrpc": "2.0", "id": 1,
"result": {"task": {"id": "tsk_…", "contextId": "ctx_…",
"status": {"state": "TASK_STATE_COMPLETED",
"message": {"messageId": "msg_tsk_…_status",
"contextId": "ctx_…", "taskId": "tsk_…",
"role": "ROLE_AGENT",
"parts": [{"text": "task completed",
"mediaType": "text/plain"}]},
"timestamp": "2026-10-13T09:00:04.512Z"},
"artifacts": [{"artifactId": "response", "name": "response",
"parts": [{"text": "four", "mediaType": "text/plain"}]}]}}}
The same message with "configuration": {"returnImmediately": true}:
{"jsonrpc": "2.0", "id": 1,
"result": {"task": {"id": "tsk_…", "contextId": "ctx_…",
"status": {"state": "TASK_STATE_SUBMITTED",
"timestamp": "2026-10-13T09:00:00.123Z"}}}}
A completion:
{"jsonrpc": "2.0", "id": 1,
"result": {"message": {"messageId": "msg_dlg_…_received", "contextId": "ctx_…",
"role": "ROLE_AGENT",
"parts": [{"data": {"delegation_id": "dlg_…", "received": true},
"mediaType": "application/json"}]}}}
The door sets no deadline on a held request, and writes nothing until it answers. A client whose
HTTP timeout is shorter than its task sends returnImmediately: true and polls GetTask, or reads
the task's events with SendStreamingMessage or SubscribeToTask. A client
that timed out never learned the task's id: ListTasks filtered by the contextId
it put on the message finds the task.
A held request ends one of three ways:
| What happens | The held request |
|---|---|
| The task settles | Answered with the task |
The door closes: the capsule exits, mur stop runs, or murmur.StopSession is called |
Answered with the task as GetTask reports it at that moment, whatever its state. Under after_task: exit the task has settled by then |
| The client disconnects | Nothing changes for the task: it runs on, and GetTask, ListTasks and SubscribeToTask see it. The door finds out the client left only when the answer fails to write. Nothing is cancelled and nothing is logged |
The door reads two members of configuration: returnImmediately, and
taskPushNotificationConfig, which it refuses. Every other member is
ignored. A tenant is refused.
A message from the agent — a task's status.message, a completion's acknowledgement — has role
ROLE_AGENT. A task's status.message also carries its contextId and taskId.
Every artifact carries an artifactId, which is its name: response, prompt or residue. Each
occurs at most once in a task, and its id is the same on every read.
skills
A door that serves SendMessage advertises one skill: running a task. Under
lifecycle.task_acceptance: none no task can be started,
and skills is [].
| Key | Value |
|---|---|
id |
task |
name |
Run a task |
description |
Runs one task given as a text message and reports its outcome. |
tags |
["task"] |
Installed tools are not skills: a caller cannot invoke a tool directly. They are listed on the capsule extension.
Extensions
All three extensions are A2A AgentExtension objects with required: false: a standard A2A client
can call the capsule without understanding any of them. How a client uses each is under
Extensions.
| Key | Value |
|---|---|
uri |
The extension's identifier, which is the address of its section in these docs |
description |
What the extension carries |
required |
false |
params |
The extension's content, below |
Door extension
URI: https://docs.murmur.nexus/reference/agent-card/#murmur-door-v1
| Key | Type | Value |
|---|---|---|
params.methods |
array of strings | The JSON-RPC methods POST / answers, A2A and murmur alike |
params.peerTasks |
boolean | Whether the door serves a request another capsule's runtime sends, stamped x-murmur-task-origin: peer: the capsule's exports.peer_tasks.accept, false when the block is absent. Always present, on the public and the extended card. When false, every such request is answered 403 peer_not_accepted |
The extension also carries forgetSession, a field a message's metadata may
hold under the extension's URI.
A method is listed exactly when POST / answers it with something other than -32601 Method not
found. Method names match exactly: case and surrounding whitespace are significant. Murmur's own
methods are the ones named murmur.<Method>; every other name is an A2A v1.0 method. Methods
appear in this order:
| Method | Answers | Listed when |
|---|---|---|
SendMessage |
Starts a task, or delivers input to a task waiting for it, and returns the task | lifecycle.task_acceptance is single or queue |
SendStreamingMessage |
Starts a task and streams its events as text/event-stream |
lifecycle.task_acceptance is single or queue |
murmur.WatchSession |
Streams every task of this session as an observer, without starting a task | Always |
GetTask |
Returns the task named by params.id |
Always |
ListTasks |
Returns one page of the tasks the caller sees | Always |
CancelTask |
Cancels the task named by params.id. A task that had already ended is left unchanged and answered -32002 |
Always |
SubscribeToTask |
Streams the rest of the live task named by params.id as text/event-stream — see SubscribeToTask |
lifecycle.task_acceptance is single or queue |
murmur.StopSession |
Cancels every live task and reports what the session leaves running | Always |
GetExtendedAgentCard |
Returns the extended card | The capsule declares network.authentication |
See lifecycle.task_acceptance. Under none, POST /
answers SendMessage with -32601, and SendStreamingMessage and SubscribeToTask with -32004,
so none of the three is listed, capabilities.streaming is false and skills is [].
| Door | params.methods |
|---|---|
| Public | SendMessage, SendStreamingMessage, murmur.WatchSession, GetTask, ListTasks, CancelTask, SubscribeToTask, murmur.StopSession |
| Authenticated | The public door's eight, then GetExtendedAgentCard |
Public, task_acceptance: none |
murmur.WatchSession, GetTask, ListTasks, CancelTask, murmur.StopSession |
Authenticated, task_acceptance: none |
murmur.WatchSession, GetTask, ListTasks, CancelTask, murmur.StopSession, GetExtendedAgentCard |
The card endpoint itself is not listed, and neither are the four push config methods, which the door answers but does not serve.
Calling a murmur method, or sending a message whose metadata holds the extension's URI, uses the
extension. A served answer names what it used in its A2A-Extensions response header; a refusal
carries none:
| Request | A2A-Extensions on the answer |
|---|---|
murmur.WatchSession |
The door URI, then the stream extension's URI |
murmur.StopSession |
The door URI |
SendMessage whose message.metadata holds the door URI |
The door URI |
SendStreamingMessage whose message.metadata holds the door URI |
The door URI, then the stream extension's URI when the request activated it |
A refusal: -32009, -32601, 403, -32602 and every other error |
None |
murmur.WatchSession
The observer stream of this session: every task's events, across turns and tasks, without sending
anything. SendStreamingMessage follows the one task its message starts or continues;
murmur.WatchSession follows them all, and stays open when a task ends. params is {}, and is
not read.
curl -N -X POST http://localhost:41873/ \
-H 'Content-Type: application/json' \
-H 'A2A-Version: 1.0' \
-H 'Last-Event-ID: 41' \
-d '{"jsonrpc":"2.0","id":1,"method":"murmur.WatchSession","params":{}}'
The answer is a text/event-stream with no content-length; the body ends when the connection
closes:
HTTP/1.1 200 OK
content-type: text/event-stream
cache-control: no-cache
connection: keep-alive
A2A-Extensions: https://docs.murmur.nexus/reference/agent-card/#murmur-door-v1, https://docs.murmur.nexus/reference/streaming-protocol/#murmur-stream-v1
It writes, in order:
- A
connection-ackframe in an unnumberedmessageevent. - The replay from
Last-Event-ID, or from the start of the replay buffer when the header is absent: agapframe when the buffer cannot supply every event after that id, then the buffered events. - Every task's live events, murmur frames included, until the capsule's stream ends or the client disconnects.
The stream extension is always active on this stream,
whatever the request's A2A-Extensions says. The first two events of a reconnect from id 41:
data: {"jsonrpc":"2.0","id":1,"result":{"message":{"messageId":"msg_01a131fc15437521b334719530864c4f","role":"ROLE_AGENT","parts":[{"mediaType":"text/plain","text":"connection-ack"}],"metadata":{"https://docs.murmur.nexus/reference/streaming-protocol/#murmur-stream-v1":{"frame":"connection-ack","role":"observer","conversationMode":"stateless"}}}}}
id: 42
data: {"jsonrpc":"2.0","id":1,"result":{"statusUpdate":{"taskId":"tsk_0199c4e2f1b7712a9d3e4f5061728394","contextId":"ctx_0199c4e2f1b7712a9d3e4f50617283a1","status":{"state":"TASK_STATE_WORKING","message":{"messageId":"msg_0199c4e2f1b87a01b2c3d4e5f6071829","contextId":"ctx_0199c4e2f1b7712a9d3e4f50617283a1","taskId":"tsk_0199c4e2f1b7712a9d3e4f5061728394","role":"ROLE_AGENT","parts":[{"mediaType":"text/plain","text":"inference turn 2"}]},"timestamp":"2026-10-13T09:00:00.123Z"}}}}
Every event and frame it can write, and the heartbeat, are under
What murmur.WatchSession writes.
mur watch is a client of this method.
murmur.StopSession
Cancels every task the session still holds, and reports what the session leaves running, in one
answer. params is {}, and is not read.
curl -s -X POST http://localhost:41873/ \
-H 'Content-Type: application/json' \
-H 'A2A-Version: 1.0' \
-d '{"jsonrpc":"2.0","id":1,"method":"murmur.StopSession","params":{}}'
HTTP/1.1 200 OK
content-type: application/json
content-length: 267
connection: close
A2A-Extensions: https://docs.murmur.nexus/reference/agent-card/#murmur-door-v1
{"jsonrpc":"2.0","id":1,"result":{"canceled":["tsk_019ed5211c827f63a8fe4be623277c55"],"residue":[{"binary":"bash","command":"sleep 30","kind":"detached_shell","started_at_ms":1757068800123,"work_id":"wrk_9f2a1c"}],"session_id":"ses_019f01a940ce7761854e768ecbe3d399"}}
| Key | Type | Value |
|---|---|---|
session_id |
string | The session this door answers for |
canceled |
array of strings | The tasks this call moved to TASK_STATE_CANCELED, sorted. [] when none was live, so a second call answers [] |
residue |
array of objects | One residue item per thing the session leaves running. [] when nothing is |
All three keys are always present. CancelTask omits its residue artifact when nothing is
running; murmur.StopSession answers "residue": [].
murmur.StopSession ends no process and does not shut the door: the capsule keeps running and
answering. mur stop calls it, then signals the process.
Residue items
The items a murmur.StopSession answer lists, and the data parts of a CancelTask answer's
residue artifact. mur cancel and mur stop print a
detached_shell item as running: and a delegation item as ended:.
kind |
Names | Keys |
|---|---|---|
detached_shell |
A demoted shell command, which runs until it finishes or its session ends, which kills it | work_id (wrk_…), binary, command, started_at_ms |
delegation |
A sub-capsule the cancelled task delegated to, which that task ends | delegation_id (dlg_…), capsule, version, child_session_id, child_workdir (the child's work folder, absolute) |
{"kind": "delegation", "delegation_id": "dlg_7b31de", "capsule": "my-worker", "version": "0.1.0",
"child_session_id": "ses_019ed2af76b57c42912b612f07dd4d51", "child_workdir": "/home/me/.murmur/records/home/me/proj/ses_019ed2a0c4b17e3a8f5d6c2b1e0f9a87/children/my-worker-7b31de0c4a9f1e25"}
forgetSession
A field of a SendMessage or SendStreamingMessage message that starts a task: it asks the
capsule to drop the harness session the message's context names, so the task starts a new
conversation under the same context id. It lives in the message's metadata, under the door
extension's URI:
| Path | Type | Value |
|---|---|---|
params.message.metadata["https://docs.murmur.nexus/reference/agent-card/#murmur-door-v1"].forgetSession |
boolean | true asks for the forget. false, an absent field and an absent URI key ask for nothing |
Other keys in the URI's object, and other metadata keys, are ignored. List the door URI in
message.extensions too; the door does not read extensions.
curl -s -X POST http://localhost:41873/ \
-H 'Content-Type: application/json' \
-H 'A2A-Version: 1.0' \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "SendMessage",
"params": {
"message": {
"messageId": "msg-1",
"role": "ROLE_USER",
"contextId": "ctx_0193f2a1c4d87e01b2c3d4e5f6a7b8c9",
"parts": [{"text": "carry on"}],
"extensions": ["https://docs.murmur.nexus/reference/agent-card/#murmur-door-v1"],
"metadata": {
"https://docs.murmur.nexus/reference/agent-card/#murmur-door-v1": {"forgetSession": true}
}
}
}
}'
HTTP/1.1 200 OK
content-type: application/json
content-length: 220
connection: close
A2A-Extensions: https://docs.murmur.nexus/reference/agent-card/#murmur-door-v1
{"jsonrpc":"2.0","id":1,"result":{"task":{"contextId":"ctx_0193f2a1c4d87e01b2c3d4e5f6a7b8c9","id":"tsk_019ed5211c827f63a8fe4be623277c55","status":{"state":"TASK_STATE_SUBMITTED","timestamp":"2026-10-13T09:00:00.123Z"}}}}
The door refuses the message with -32602, starting no task, in this order. The first three come
after the other message checks and before the -32005 file-part check:
| Refusal | error.message |
|---|---|
| The URI's value is not an object | Invalid params: message.metadata["https://docs.murmur.nexus/reference/agent-card/#murmur-door-v1"] must be an object |
forgetSession is not a boolean |
Invalid params: message.metadata["https://docs.murmur.nexus/reference/agent-card/#murmur-door-v1"].forgetSession must be a boolean |
forgetSession: true on a message that names a taskId |
Invalid params: forgetSession starts a new conversation, so it goes on a message that starts a task, not on one that continues task <id> |
forgetSession: true to a capsule not on inference.transport: process. Checked once the message is read, before it is routed |
forgetSession asks this capsule to forget the harness session this context names, and only a capsule on inference.transport: process has one |
On SendStreamingMessage each refusal is one JSON body, before any stream opens.
On a transport: process capsule, the forget:
- Drops the harness session the context names before the task's first attempt. The
harness_session_forgottentrace record names the context, the dropped id andrequested_by: "a2a"— see what removes an entry. - Starts the harness on a new session: the run's
harness_startsayssession_mode: "new". - Applies to that one task. The next message in the context resumes the new session.
A context that names no harness session has nothing to drop, and the task starts a new
conversation as it would have without the field. mur run --context <id> --forget-session asks for
the same forget from the CLI — see --forget-session. Both are the
answer to E-RUN-036.
Runtime headers
Murmur's runtimes send each other these headers, and a capsule's runtime stamps them itself. A client calling the door sends none of them:
| Header | Sent by | Read by | Effect |
|---|---|---|---|
x-murmur-task-origin |
A capsule's outbound A2A send, and a sub-capsule's completion | The door, before the body | The task's origin. On a door whose peerTasks is false, peer is refused 403 peer_not_accepted on every method |
x-murmur-task-trust |
A capsule's outbound A2A send, beside x-murmur-task-origin |
The door, before the body | The task's trust class |
x-murmur-delegation-id |
A sub-capsule's completion | The door's completion path | Names the delegation the completion answers. One no task here awaits is refused -31002 before its params is read |
x-murmur-completion-session |
A sub-capsule's completion | The door's completion path | Names the session the completion is for. A misaddressed one is refused -31001 before its method or params is read |
x-murmur-audience |
A capsule redeeming a peer handle | The peer plane at /resources/peer/<handle> |
Names the redeeming capsule, which the handle must have been issued to |
x-murmur-handle-id |
The peer plane, on its answer to a redeem | The redeeming client | Names the handle the bytes were served for |
Completion errors
A completion is a SendMessage carrying
x-murmur-task-origin: completion, which a sub-capsule posts to the session that delegated to it.
The door refuses one with a murmur error:
| Code | reason |
Answers a completion that | metadata |
|---|---|---|---|
-31001 |
COMPLETION_MISADDRESSED |
Names, in x-murmur-completion-session, a session other than the one running here |
addressedSession: the session the completion named, "" when it named none. Never the session running here |
-31002 |
COMPLETION_NOT_AWAITED |
Names no delegation in x-murmur-delegation-id, names one no task here waits for, or names one this session is ending |
delegationId, when the completion named one |
Each carries error.data with one google.rpc.ErrorInfo, shaped as an A2A error's, in
the domain murmur.nexus. The codes lie outside the range JSON-RPC 2.0 reserves, -32768 to
-32000; A2A calls such codes JSON-RPC custom errors. The child records the refusal's message
as its delivery_error.
{"jsonrpc": "2.0", "id": 1,
"error": {"code": -31002,
"message": "no task in this session is waiting for delegation dlg_…",
"data": [{"@type": "type.googleapis.com/google.rpc.ErrorInfo",
"reason": "COMPLETION_NOT_AWAITED",
"domain": "murmur.nexus",
"metadata": {"delegationId": "dlg_…"}}]}}
A completion sent with any method but SendMessage is answered -32601.
GetTask
GetTask answers the task params.id names, running or finished, as an A2A Task. params.id
is required: a GetTask without one, or with "", is answered -32602 GetTask requires an id.
historyLength is accepted and ignored; a task carries no history.
| Key | Present |
|---|---|
id, contextId, status.state |
Always |
status.message |
On a terminal task — TASK_STATE_COMPLETED, TASK_STATE_FAILED, TASK_STATE_CANCELED or TASK_STATE_REJECTED — whose final status said something: a ROLE_AGENT message with one text part, as the task's terminal status on a stream carries it |
artifacts: [{"artifactId": "response", …}] |
On a TASK_STATE_COMPLETED task that produced a response: its answer, as one text part |
artifacts: [{"artifactId": "prompt", …}] |
On a TASK_STATE_INPUT_REQUIRED task: the question it is waiting on |
metadata.murmur |
On a terminal task with something to say about answers it lacks or background commands it left running — see Task metadata |
{"id": "tsk_…", "contextId": "ctx_…",
"status": {"state": "TASK_STATE_COMPLETED",
"message": {"messageId": "msg_tsk_…_status", "contextId": "ctx_…", "taskId": "tsk_…",
"role": "ROLE_AGENT",
"parts": [{"text": "task completed", "mediaType": "text/plain"}]},
"timestamp": "2026-10-13T09:00:00.123Z"},
"artifacts": [{"artifactId": "response", "name": "response",
"parts": [{"text": "four", "mediaType": "text/plain"}]}]}
{"id": "tsk_…", "contextId": "ctx_…",
"status": {"state": "TASK_STATE_INPUT_REQUIRED", "timestamp": "2026-10-13T09:00:00.123Z"},
"artifacts": [{"artifactId": "prompt", "name": "prompt",
"parts": [{"text": "Which branch?", "mediaType": "text/plain"}]}]}
CancelTask on a live task answers the task in TASK_STATE_CANCELED, with a residue artifact
naming what the session still runs, one data part per residue item, when anything
is:
{"id": "tsk_…", "contextId": "ctx_…",
"status": {"state": "TASK_STATE_CANCELED", "timestamp": "2026-10-13T09:00:00.123Z"},
"artifacts": [{"artifactId": "residue", "name": "residue",
"parts": [{"data": {"kind": "detached_shell", "work_id": "wrk_…",
"binary": "sleep", "command": "sleep 30",
"started_at_ms": 1760000000000},
"mediaType": "application/json"}]}]}
GetTask and CancelTask answer only a task the caller sees. Any other id — another
credential's task, the operator's, or one the session never held — is answered
-32001 Task not found.
A SendMessage the door has no room for answers a TASK_STATE_REJECTED task whose
status.message says why: task rejected: capsule is busy, or
task rejected: the session is closing.
Task metadata
A terminal task says, in metadata.murmur, which answers it lacks and which background commands
it left running. metadata is absent when none of these keys is present.
| Key | Type | Present |
|---|---|---|
noAnswer |
true |
On a TASK_STATE_FAILED task a formation member ended with end-without-answer. Absent otherwise |
noAnswerBelow |
array of {"member": "<roster name>", "status": "<status>"} |
On a terminal task when a member it called gave it no answer: each such member, then each member those reported below them, each once, at most 8. status is a member_call status other than completed. Absent when empty |
runningCommands |
array of {"workId": "wrk_…", "binary": "<path>", "command": "<text>", "runningMs": <n>} |
On a terminal task that ended while shell commands moved to the background were still running, oldest first. command is the text the model wrote, cut to 200 characters followed by …; runningMs counts from the command's start to the task's end. Each workId is a background-command handle. Absent when empty |
{"id": "tsk_…", "contextId": "ctx_…",
"status": {"state": "TASK_STATE_FAILED",
"message": {"messageId": "msg_tsk_…_status", "contextId": "ctx_…", "taskId": "tsk_…",
"role": "ROLE_AGENT",
"parts": [{"text": "q gave no answer", "mediaType": "text/plain"}]},
"timestamp": "2026-10-13T09:00:00.123Z"},
"metadata": {"murmur": {"noAnswer": true,
"noAnswerBelow": [{"member": "q", "status": "timed_out"}]}}}
{"id": "tsk_…", "contextId": "ctx_…",
"status": {"state": "TASK_STATE_COMPLETED", "timestamp": "2026-10-13T09:00:00.123Z"},
"metadata": {"murmur": {"runningCommands": [
{"workId": "wrk_0199a3f1c2d47e8ab5c6d7e8f9a0b1c2", "binary": "/usr/bin/bash",
"command": "cargo test --workspace", "runningMs": 312004}]}}}
A completed task with noAnswerBelow or runningCommands keeps its response artifact. A caller
that does not read these keys can ignore metadata: the task reads as an ordinary
TASK_STATE_FAILED or TASK_STATE_COMPLETED one.
ListTasks
ListTasks answers one page of the tasks the caller sees, the most recently changed
first. Its params are a ListTasksRequest, read in this order:
| Param | Accepted | Refused |
|---|---|---|
contextId |
A string. "" or absent: every context |
Not a string: -32602 |
status |
A TASK_STATE_* name. TASK_STATE_UNSPECIFIED or absent: every state. TASK_STATE_AUTH_REQUIRED matches no task |
Anything else: -32602 |
pageSize |
An integer from 1. Absent: 50. Above 100: 100 |
Below 1, or not an integer: -32602 |
pageToken |
"" or absent for the first page, or a nextPageToken this door answered |
Anything else: -32602 |
statusTimestampAfter |
An RFC 3339 timestamp, in any offset: only the tasks whose status.timestamp is at or after it. null or absent: every task |
Anything else: -32602 statusTimestampAfter must be an RFC 3339 timestamp, such as 2026-10-13T09:00:00Z |
includeArtifacts |
A boolean. Absent: false |
Not a boolean: -32602 |
historyLength |
Any value, ignored: a task carries no history |
— |
Other members are ignored. The first parameter refused answers, naming it.
The answer is a ListTasksResponse, whose four members are always present:
| Member | Value |
|---|---|
tasks |
The page. Each task is exactly what GetTask answers for it, without artifacts unless includeArtifacts is true; then artifacts is the task's artifacts, or [] |
nextPageToken |
The pageToken of the next page. "" on the last page |
pageSize |
The page size used |
totalSize |
How many tasks the caller sees that match the filters, across every page |
{"jsonrpc": "2.0", "id": 1,
"result": {"tasks": [{"id": "tsk_…", "contextId": "ctx_…",
"status": {"state": "TASK_STATE_WORKING",
"timestamp": "2026-10-13T09:00:04.512Z"}},
{"id": "tsk_…", "contextId": "ctx_…",
"status": {"state": "TASK_STATE_COMPLETED",
"message": {"messageId": "msg_tsk_…_status", "contextId": "ctx_…",
"taskId": "tsk_…", "role": "ROLE_AGENT",
"parts": [{"text": "task completed",
"mediaType": "text/plain"}]},
"timestamp": "2026-10-13T09:00:00.123Z"}}],
"nextPageToken": "MTI6dHNrXzAxYTE…",
"pageSize": 2,
"totalSize": 5}}
A task moves to the front of the order each time its state changes or its ending is recorded. A
nextPageToken names a position in that order and nothing else: the filters and
who sees a task apply afresh to every page, so a token passed to another caller
shows that caller only its own tasks. A walk returns every matching task once while no matching task
changes state. A task that changes during a walk moves ahead of the cursor, and a fresh listing
shows it first.
SubscribeToTask
SubscribeToTask streams the rest of one task the caller sees as
text/event-stream, in the events
SendStreamingMessage writes. params.id names the task:
| Request | Answer |
|---|---|
No id, or "" |
-32602 SubscribeToTask requires an id |
| A task the session never held, or one the caller does not see | -32001, metadata.taskId |
| A task that has ended | -32004, metadata.taskId and state |
| A door that starts no task | -32004 |
| A live task | The stream |
Each refusal is one JSON body; no stream opens. The stream opens with the task as GetTask answers
it at that moment, then carries the task's later events, and ends after its terminal status. It
stays open through TASK_STATE_INPUT_REQUIRED: the reply arrives on a SendMessage naming the
task, and the stream carries the task's resumed TASK_STATE_WORKING. Last-Event-ID and
A2A-Extensions work as on SendStreamingMessage — see
Streaming Protocol.
curl -N -X POST http://localhost:41873/ \
-H 'Content-Type: application/json' \
-H 'A2A-Version: 1.0' \
-H "Authorization: Bearer $TOKEN" \
-d '{"jsonrpc":"2.0","id":"sub-1","method":"SubscribeToTask","params":{"id":"tsk_…"}}'
Capsule extension
URI: https://docs.murmur.nexus/reference/agent-card/#murmur-capsule-v1
This extension is extended-card material: the session id and what the capsule may do. Nothing of it appears anywhere else on the card.
| Door | Where the extension is |
|---|---|
| Public | The public card |
| Authenticated | The extended card only |
| Key | Type | Value |
|---|---|---|
params.sessionId |
string | The session answering this address. Compare it with the session you expect before sending a task |
params.tools |
array of strings | Names of the installed artifacts the model can call |
params.shell |
boolean | true when capabilities.shell.allow lists at least one command |
params.network |
boolean | true when capabilities.network.allow lists at least one destination |
params.planes |
array of strings | The HTTP planes that serve content — see planes |
Planes
A plane is listed only when the manifest declares it. An undeclared plane answers every request with a refusal. Planes appear in this order:
| Plane | Listed when | Serves |
|---|---|---|
files |
exports.files is declared |
The operator plane under /resources/files |
peer_files |
exports.peer_files is declared |
The peer plane under /resources/peer/<handle> |
Stream extension
URI: https://docs.murmur.nexus/reference/streaming-protocol/#murmur-stream-v1
params.frames lists the murmur frames a connection to the door can receive. Activation, the
frames and the events that carry them are under
The stream extension.
| Capsule | params.frames |
|---|---|
transport: http, as in the examples on this page |
text, thinking, tool-call-started, tool-call-progress, artifact, gap, lagged, connection-ack, capsule-closed |
transport: process |
The same, with quota and suggested-prompt after artifact: only a harness reports them |
A card with no supportedInterfaces
A card without supportedInterfaces comes from a runtime at v0.4.0 or earlier, which serves the
previous card. A capsule serving it answers the method names of its own version, and:
| Reader | Result |
|---|---|
mur ps, mur stop |
Lists the capsule as unreachable: its card names no session. The record is kept, never pruned |
| Peer handle minting for the capsule | Fails with peer_unreachable: its card has no JSONRPC interface |
Both hold until that capsule ends.
Migrating from the previous card
Where each key of the card served by v0.4.0 and earlier is on this card:
| Previous key | On this card |
|---|---|
name |
name |
version |
version |
url (localhost:41873) |
supportedInterfaces[0].url, with the scheme (http://localhost:41873) |
session_id |
Capsule extension params.sessionId |
capabilities.tools |
Capsule extension params.tools |
capabilities.shell |
Capsule extension params.shell |
capabilities.network |
Capsule extension params.network |
capabilities.streaming |
capabilities.streaming, derived the same way |
capabilities.cancellation |
Removed. It was true on every card; the door extension's params.methods lists CancelTask |
serves.methods |
Door extension params.methods, same values and order |
serves.planes |
Capsule extension params.planes, same values and order |
Find an extension by its uri in capabilities.extensions, not by its position.
Migrating from A2A 0.3
The door speaks A2A v1.0 only — see A2A v1.0 conformance. A client written for a
door that declared protocolVersion 0.3 changes these:
| A2A 0.3 door | A2A v1.0 door |
|---|---|
No A2A-Version header |
A2A-Version: 1.0 on every JSON-RPC request. A request without it is answered -32009 |
message/send |
SendMessage |
message/stream |
SendStreamingMessage |
tasks/get |
GetTask |
tasks/cancel |
CancelTask |
agent/getAuthenticatedExtendedCard, answering an A2A 0.3 AgentCard |
GetExtendedAgentCard, answering an A2A v1.0 AgentCard — see The extended card. A public door answers it -32004, where a 0.3 door answered -32007 |
supportedInterfaces[0].protocolVersion 0.3 |
1.0 |
tasks/cancel on a task that had already ended answers the task |
CancelTask answers -32002 and leaves the task unchanged |
A completion refused with -32004 |
-31001 or -31002. -32004 is UnsupportedOperationError only |
Errors without data |
Every A2A error carries an ErrorInfo |
Credential scopes message/send, message/stream, tasks/get, tasks/cancel |
SendMessage, SendStreamingMessage, GetTask, CancelTask. A manifest listing a 0.3 name is refused — see network.authentication.credentials |
Task states submitted, working, input-required, completed, failed, rejected, canceled |
TASK_STATE_SUBMITTED, TASK_STATE_WORKING, TASK_STATE_INPUT_REQUIRED, TASK_STATE_COMPLETED, TASK_STATE_FAILED, TASK_STATE_REJECTED, TASK_STATE_CANCELED |
role: "user", role: "agent" |
ROLE_USER, ROLE_AGENT. A message with role: "user" is refused |
message/send answering a bare task |
SendMessage answering {"task": …} |
| A reply reaching whichever task waited for input | A reply names its task in taskId |
A part with kind, and a file or data part skipped |
A part sets one of text, raw, url or data. A data part is read, and so is an image part; any other file part is refused with -32005 |
| Artifacts without an id | Every artifact carries an artifactId |
tasks/get without an id answering the active task |
GetTask requires an id |
stream/watch |
murmur.WatchSession |
session/stop |
murmur.StopSession |
Credential scopes stream/watch, session/stop |
murmur.WatchSession, murmur.StopSession. A manifest listing an old name is refused |
Header x-murmur-forget-session: true |
forgetSession: true in the message's metadata, under the door extension's URI. The header is not read |
Every old method name in the table is answered -32601 Method not found: the door serves no alias.
The other x-murmur-* headers are unchanged — see Runtime headers.