A2A extension

The command extension

https://agentd.dev/a2a/ext/command · schema.json

URIhttps://agentd.dev/a2a/ext/command
Schemahttps://agentd.dev/a2a/ext/command/schema.json (JSON Schema draft 2020-12)
KindA2A profile extension: no new method, no change to a core structure
Requirednever
Applies toSendMessage, SendStreamingMessage

This page is the normative specification of the URI above. It is served at that URI.

The URI carries no version. A2A 1.0.1 §4.6.3 makes a version in an extension URI a SHOULD, not a MUST, and a version in a name agentd owns would be a second name for the same thing. An incompatible change takes a new URI with a new name, never a /vN suffix: §4.6.3 and §5.8 say a URI's meaning MUST NOT change under a peer that already speaks it.

A2A has no tool-call primitive. The protocol's own answer is a message that carries structured input, so every operation agentd offers over A2A is an ordinary SendMessage whose message carries one DataPart under the agentd key. A client that never activates this extension can still converse, list tasks and subscribe to them. The extension adds reach, and is never a precondition.

Params

The declaration on the agent card carries these params. The public card carries the vocabulary agentd can answer, identical on every instance. The extended card (GetExtendedAgentCard) narrows it to what this instance serves and this caller may run.

keytypecardmeaning
dataPartKeystringpublicalways "agentd": the key under a DataPart's data that holds the command
schemastringpublicthe URL of this extension's schema bundle
ops[{op, reply}]publicthe ops, each with its reply kind (message or task). On the public card, every op any build serves; on the extended card, the ops this instance serves and the caller may run
commands[{op, workflow, schema?}]extendedthe commands loaded workflows declare that the caller may fire, each with the workflow it starts and the schema its arguments are held to
settablestring[]extendedthe paths admin.set accepts, on an extended card whose caller may run it

Activation

A command is a command only when the request says so, twice:

  1. the A2A-Extensions request header names this URI, and
  2. the message lists this URI in message.extensions.

The response's A2A-Extensions header echoes the URI when it was activated. A DataPart under agentd sent without both is refused (see Errors) and never run: a client cannot send a command by accident.

The envelope

The message carries exactly one part whose data is the command:

{
  "jsonrpc": "2.0", "id": 1, "method": "SendMessage",
  "params": {
    "message": {
      "role": "ROLE_USER",
      "messageId": "m-1",
      "extensions": ["https://agentd.dev/a2a/ext/command"],
      "parts": [{"data": {"agentd": {"op": "workflow.run", "workflow": "triage", "inputs": {"ticket": 42}}}}]
    }
  }
}

The object under agentd is the envelope: op, and the op's arguments beside it. Each op's envelope is $defs/envelopes/$defs/<op> in the schema bundle, which is the schema the listener validates the envelope against. An argument the schema does not name is refused, never ignored.

A command message:

  • carries no taskId: a command starts its own task, or none;
  • may name a contextId, and runs in that conversation of the caller's;
  • if it lists configuration.acceptedOutputModes, includes application/json among them: every command answers with JSON.

Replies

An op answers in one of two ways, given by its reply column below and by params.ops[].reply on the card.

  • message, a read. The answer is a Message (ROLE_AGENT) with one DataPart, media type application/json, whose data is the op's result document. Its extensions lists this URI. No task is created, so a client that polls a read leaves nothing behind in any task list.
  • task, work. The answer is a Task. When it completes with a result, the task carries one artifact, <taskId>.result, whose DataPart holds the result and whose extensions lists this URI. An op whose result schema is false completes with no result artifact.

Each op's result schema is $defs/ops/<op>/result in the bundle. For a message op it is the document in the reply. For a task op it is the data of the result artifact.

Ops

Every op agentd serves, in the order the card lists them, followed by the reserved names. The reserved names are never listed in params.ops, and no workflow may declare them as a command.

  • who may call is the op's floor. operator means the operator role alone, and no grant reaches it. any named caller means every authenticated principal. The remaining rows are open to the roles named and to any principal whose grants name the op.
  • served is the switch that serves the op. An op behind a closed switch is answered as an unknown op, except the introspection ops, which say that introspection is off.
opreplywho may callserveddescription
statusmessageany named calleralwaysLiveness and a snapshot of this instance, as the caller may see it: its runs, conversations and activity; an operator sees everything
configmessageoperatoralwaysThe effective configuration, credentials redacted
workflow.runtaskuser, agent or a grantalwaysStart a workflow; the reply is the task it runs under
workflow.statusmessageuser, agent or a grantalwaysThe status of one run, or of every run the caller started
workflow.canceltaskuser or a grantalwaysCancel one run by id
workflow.signaltaska grantalwaysDeliver a signal a workflow is waiting on
subagent.sendtaskuser or a grantalwaysSend a message to a warm subagent
subagent.killtaska grantalwaysStop a subagent
subagent.statusmessageuser or a grantalwaysThe status of one subagent, and its result once it has one
plan.getmessageuser or a grantalwaysOne conversation's current plan and its progress
conversation.getmessageuser or a granta2a.introspection.enabledOne conversation's transcript, message bodies included
run.getmessageuser or a granta2a.introspection.enabledOne run with per-step status, timings, errors and output
subagent.getmessageuser or a granta2a.introspection.enabledOne subagent's instruction, attempts, result and error
debug.eventsmessageoperatora2a.introspection.enabledA cursor read of the live log ring, across every principal
admin.draintaskoperatoralwaysBegin a graceful drain, then exit 0
admin.pausetaskoperatoralwaysHold the instance, or one run, at a safe boundary
admin.resumetaskoperatoralwaysClear a prior pause
admin.canceltaskoperatoralwaysCancel one run by id, whoever started it
admin.settaskoperatoralwaysSet a runtime-settable path (agent.approval, a2a.introspection.enabled) until the next reload
auth.device.pendingmessageoperatora2a.device_grant.enabledDevice sign-ins waiting for an operator's decision
auth.device.approvetaskoperatora2a.device_grant.enabledApprove a device sign-in {user_code, as, scope?}; every session approved as one name is one principal
auth.device.denytaskoperatora2a.device_grant.enabledRefuse a pending device sign-in {user_code}, or all of them {all: true}
auth.sessionsmessageoperatora TCP listenerThe signed-in sessions: kind, name, principal and expiry
auth.sessions.revoketaskoperatora TCP listenerEnd one session {sid}, every session of a name {name}, or all {all: true}
_instance.resulttaskoperatoralwaysreserved: a mode: sync child's first result, recorded on the parent's handle; operator only
_instance.emittaskoperatoralwaysreserved: one event of a child's mirror_streams stream, appended to the parent's stream of the same name; operator only
ask_humantaskoperatorneverreserved: the model's tool for asking a person; not served over A2A

The two _instance.* ops are how a child instance reports to the parent that started it. Their arguments and results are published under $defs/reserved.

Workflow-declared commands

A workflow whose start step is kind: a2a with a command: declares one more op: sending that command starts the workflow. Its arguments are held to the start step's schema, when it declares one, instead of a row above. The extended card lists the commands the caller may fire in params.commands: those whose op the caller's grants admit (an operator's always do) and whose start's roles: admit the caller's role. A send meets the same two checks. The schema bundle does not describe these commands. It lets through an op it does not know, because only the instance knows what its workflows declare.

Errors

Refusals are JSON-RPC errors whose data carries a google.rpc.ErrorInfo with the reason below, and a google.rpc.BadRequest naming the field for a malformed command. Nothing refused is run.

codereasonwhen
-32602EXTENSION_NOT_ACTIVATEDa DataPart under agentd, and the A2A-Extensions header does not name this URI
-32602EXTENSION_NOT_MARKEDa command, and message.extensions does not list this URI
-32602COMMAND_ENVELOPE_AMBIGUOUSmore than one part carries a command
-32602COMMAND_TASK_IDa command message names a taskId
-32005CONTENT_TYPE_NOT_SUPPORTEDacceptedOutputModes is given and excludes application/json
-32602UNKNOWN_OPthe op is empty, or nothing serves it: no row, and no loaded workflow declares it
-32602INVALID_COMMAND_ARGSthe envelope does not match the op's schema; BadRequest names each miss
-31403PERMISSION_DENIEDthe caller's role and grants do not reach the op (HTTP 403)
-32004INTROSPECTION_DISABLEDan introspection op while a2a.introspection.enabled is off

Security

  • The floor is the table's. A user holding grants: ["*"] still reaches no operator op. An op that drains the instance, relaxes its approval policy or reads every principal's log lines is an operator's, whatever a grant says.
  • Ownership is enforced at the object. An op that names a run, a subagent or a conversation acts only on one the caller started. An operator acts on any. A caller who may not run a workflow is told it may not, never that it does not exist.
  • The public card promises no reach. It lists the vocabulary, not what this instance has switched on or what this caller may run. The extended card answers those questions, per caller.
  • The reserved ops are the operator's. _instance.* from any other caller is refused on the request itself, before a task exists.

Schema and examples

The bundle at schema.json validates the data of a command DataPart at its root:

  • $defs/envelopes/$defs/<op> holds each op's envelope;
  • $defs/ops/<op> holds each op's reply, description, args and result;
  • $defs/reserved/<op> holds the same for the _instance.* ops.

agentd --extension-schema command prints the bundle. It is generated from the values the listener validates against, and CI fails when the published copy differs from them. Golden examples, each valid against the bundle, are published beside it under examples/.

  • events.md: the observation feed, which a display client bootstraps with the status read.
  • task-annotations.md: the command annotation names the op that started a task.
  • ../a2a.md: the listener, its pipeline and its core methods.