Capability: a2a-server
The harness is an A2A 1.0 server on the official SDK, every verb over JSON-RPC and HTTP+JSON, streaming from the workflow's durable event log;
SendMessageis the inbox.
What it is
The inbound half of A2A and the only API. A request never runs the core loop: execute update-with-starts the task's workflow and streams its event log back; the inbox is the workflow's mailbox. Lives in tiny_harness/service/a2a/ and service/inbox.py.
Current behaviour
- The server SHALL serve A2A 1.0 with
a2a-sdk's routes over the JSON-RPC and HTTP+JSON bindings:SendMessage,SendStreamingMessage,GetTask,ListTasks,CancelTask,SubscribeToTask, the four push-notification config operations andGetExtendedAgentCard. No gRPC. - The agent card at
/.well-known/agent-card.jsonSHALL be built from the agent's description: the name, description, version and skills set in[card], or the built-in defaults where unset (issue-34), so every extension (task, channel, status, commands with the registered commands in itsparams, A2UI with its catalog ids) reachescapabilities.extensions; the command registry is frozen and published before the card is built; it declares no security scheme (decision-003) and is pinned by a contract snapshot. - WHEN
A2A-Extensionsnames an extension the card does not advertise THEN the request SHALL be rejected with the unsupported-operation error and no task created. - WHEN
executeruns THEN it SHALL redact the message, update-with-start the task'sTaskWorkflowwith the message as theinboxupdate, and stream the workflow's event log from the receipt's cursor into the SDK's queue: theTaskfirst, then status and artifact updates. A first message names no task; the server assigns the id. returnImmediately: trueSHALL return once the task and the inbox item exist. A message SHALL be persisted (the inbox audit row) before the request is acknowledged.- WHEN
cancelruns THEN the workflow SHALL be signalled and the task emitted asCANCELED. AwatcherSHALL be refused with the unsupported-operation error. - A message, completion or cancellation for a task in a terminal state SHALL be refused with an error naming the state (
the task is in a terminal state (TASK_STATE_COMPLETED)), and the task left unchanged. - WHEN a message's one data part is the
controlenvelope{"kind": "control", "action": "complete"}THEN the server SHALL complete the task (extensions): the inbox update's validator refuses a sender who is missing, not on the task or only awatcherbefore anything is interrupted; the control then bypasses the SDK's per-task request queue so it can cut a running turn, and the stream endsCOMPLETED, orINPUT_REQUIRED · awaiting-next-messagewhen an unresolved sub-task refuses it. - WHEN a message is exactly
/<name> <arguments>for a registered command THEN the workflow's dispatcher SHALL run it through therun_commandactivity before (or, mid turn, between) model calls, never invoking the model for it; the reply arrives as the message of a re-emitted status update with metadata{"command": "<name>"}(and"task_ref"for a created sub-task), and the state is unchanged. Commands and controls skiprequest.received;command.invokedis their policy point. An unregistered/<name>is an ordinary message. A command or refused message as a task's first message parks the task instead of re-emittingSUBMITTED. SubscribeToTaskSHALL replay the event log from sequence 1, across server restarts, then stay attached for live events, ending only at a terminal state (A2A 1.0), so a subscriber to a parked (INPUT_REQUIRED) conversation sees what happens next. ASendMessagestream still ends at the newest final event,INPUT_REQUIREDincluded.- A
taskevent envelope on an existing task SHALL update its name, description and acceptance criteria for any participant and its participants only for an admin (refused otherwise); astatus_updateorartifact_updateenvelope SHALL be recorded into the task's history as a framed event with an audit row. Routing such updates to a remote sub-task is not built in this work item. - Partial progress SHALL be streamed: every status update, the A2UI artifact updates and channel message events, not one terminal event per turn.
GetTaskSHALL answer from the workflow'staskquery, falling back to the store once the workflow is gone; every task operation SHALL apply one access policy: withX-Participant-Idset, a task the participant is not on is not found.- Push-notification configs SHALL be stored with their token encrypted and every status update delivered to them from the
emit_eventactivity with theX-A2A-Notification-Tokenheader. POST /a2a/push/{task_id}SHALL receive a remote task's push for its local stand-in (issue-27). A body over 64 KiB is413and an unparseable one400. A missing or wrongX-A2A-Notification-Token, an id that names no stand-in or an ended one, or a body naming another remote task is401, all alike. Only a push whose token digest matches, checked in constant time by theremote_pushupdate's validator, wakes the stand-in (204). The SDK's DEBUG log of JSON-RPC request bodies is dropped, since a body can carry a push token.- Every route SHALL sit behind the limits middleware: a body over
max_request_bytesis 413 and a peer overrate_limit_per_minuteis 429, before anything reaches Temporal. - A
Harness(tiny_harness/service/harness.py) SHALL serve the agent without opening a port:asgi_app()is the app a host mounts and exists from construction. WHILE the harness is not started, every request SHALL be503and reach no route. WHEN the app is mounted somewhere other than its prefix THEN every request SHALL be500naming both paths. As the top-level app it SHALL start its harness from thelifespanscope.running_harnessis aHarnessbehind its own uvicorn, withroot_pathset to the prefix. - The routes SHALL be built before start over a
DeferredRequestHandlerand the card throughcard_modifier, so they can be mounted early and listed in OpenAPI.tiny_harness.fastapi.harness_router(harness)SHALL return anAPIRouterwith the harness's lifespan and prefix, its routes taggedA2A: <agent id>. The 503 gate, the prefix check and the limits are router dependencies there, and CORS and/uiare not served (a warning names each). - The card and the push callback SHALL carry
public_url:base_urlwith the mount prefix. A prefix that disagrees withbase_url's path is aConfigError.RouteOptionsSHALL turn off the limits, CORS,/_monitor, the push route (the agent then follows remote tasks) and/uione by one. - WHEN a participant resolver is given THEN the participant SHALL come from it only, the
X-Participant-Idheader SHALL be ignored, and a message with no resolved participant SHALL be refused even when its metadata names one. A resolver that raises fails the request. - A task SHALL belong to the agent whose task queue runs its workflow.
TemporalTaskStoredescribes the workflow first (cached per id), and a foreign queue is answered as an unknown id.ListTasksfilters onTaskQueue, and thecontext_idin that query is quoted.PushReceiveranswers401for a stand-in on another queue. GET /_monitorSHALL return the last heartbeat snapshot; withui_dirset the built web renderer is served under/ui;cors_originsenables CORS for a dev renderer.- WHEN a new task's first message carries a
taskenvelope THEN the server SHALL build the task from it, add the sender as reporter when it is not on the task, and remove the envelope from the message so intake does not apply it again.lifecycle,modelandreasoning_effortSHALL be accepted only from a trusted delegator — this agent itself, or an id in[server] delegators— and refused whole from anyone else; an existing task keeps refusing them from everyone (issue-34 R7, abuse cases 5–7). - WHEN the brief names an assignee other than this agent THEN the task SHALL run in participant mode (no model call; it waits for its assignee). WHEN the first message is the brief alone THEN the task SHALL start with no message for its model: a conversation parks awaiting its first message (issue-34 R7.2).
Design
design.md § A2A server, design.md § Inbox and events, decision-002, decision-003, A2A extensions.
History
| Work item | What changed | Links |
|---|---|---|
| issue-3 | Executor, handler, card, bridge, task store, push, limits, inbox intake (Layer 5); A2UI params on the card (Layer 7); /ui and CORS (Layer 8) | spec, PR #11, PR #13, PR #14 |
| issue-25 | Status and commands extensions on the card; the control completion envelope; terminal and watcher refusals naming the state; slash commands dispatched in the workflow with the reply on a re-emitted status update | spec, PR #26, decision-007 |
| issue-27 | POST /a2a/push/{task_id} for remote tasks' pushes; the SDK's request-body DEBUG log dropped | spec, PR #28 |
| issue-34 | The card's name, description, version and skills come from [card] | spec, PR #35 |
| issue-34 (R7) | A new task built from its creator's brief: trusted delegators ([server] delegators, this agent always), participant mode for an assignee other than this agent, a brief-only start | spec, PR #35 |
| issue-36 | Harness and asgi_app() mounted on a host app (503 before start, mount check, lifespan); routes built before start; tiny_harness.fastapi.harness_router; public_url in the card and push URL; RouteOptions; the participant resolver with strict mode; task and push ownership by queue | spec, PR #37, decision-009 |