Skip to content

A2A

SecondBrain’s brain/a2a package now treats the local queue as a delivery layer for A2A-shaped contracts rather than as a standalone bespoke message format.

Why This Exists

Google’s ADK A2A documentation and the A2A protocol define a model built around:

  • agent cards for discovery
  • messages composed of typed parts
  • tasks with explicit lifecycle state
  • artifacts as the result surface

SecondBrain still uses a local SQLite queue for dispatch, retries, and DLQ replay, but the contracts in brain/a2a/models.py now project those queue records into the same concepts.

Local Mapping

Queue Message -> A2A Message + Task

A row in a2a_messages is still the durable local source of truth. The model layer projects it into:

  • A2AProtocolMessage
  • messageId: the local message_id
  • contextId: correlation_id when present, else a deterministic local context id
  • taskId: a deterministic task id derived from the queue message id
  • parts[0]: the human task text as text/plain
  • parts[1]: the JSON payload as application/json when present
  • A2ATask
  • status.state is derived from local queue status
  • history contains the projected request message
  • artifacts are synthesized from ack results when a result payload exists

Status Mapping

SecondBrain local queue state maps to A2A task state as follows:

  • pending -> TASK_STATE_SUBMITTED
  • in_progress -> TASK_STATE_WORKING
  • acked -> TASK_STATE_COMPLETED
  • failed -> TASK_STATE_FAILED
  • dlq -> TASK_STATE_FAILED
  • canceled -> TASK_STATE_CANCELED
  • input_required -> TASK_STATE_INPUT_REQUIRED
  • rejected -> TASK_STATE_REJECTED
  • auth_required -> TASK_STATE_AUTH_REQUIRED

Agent Cards

The local registry still keys cards by agent_id, but the stored card shape now carries the main A2A concepts:

  • supportedInterfaces
  • capabilities
  • skills
  • defaultInputModes
  • defaultOutputModes

For purely local queue targets, SecondBrain uses:

  • protocolBinding: SECOND_BRAIN_LOCAL
  • protocolVersion: local-queue
  • url: internal://a2a/<agent>

This keeps local routing explicit instead of pretending the queue is a public JSON-RPC endpoint.

Current Support

  • local publish / consume / ack / nack / DLQ replay
  • exact-message claiming for inline worker execution
  • typed A2A parts, messages, tasks, task status, and artifacts
  • typed agent cards with skills and supported interfaces
  • registry sync from local agent manifests into A2A-style cards
  • local registered-agent execution through brain/agent_builder/runtime.py
  • HTTP+JSON serve routes through sb serve for discovery and task operations
  • broker-backed SSE task-event transport through sb serve
  • JSON-RPC A2A route through sb serve
  • optional custom gRPC Struct route through sb serve --grpc-port <port>
  • signed and verifiable agent-card payloads through sb serve
  • orchestrator a2a_delegate steps can complete inline when the target resolves to a registered local agent
  • optional ADK relay payloads that now send A2A-shaped message/task data

HTTP Edge

sb serve now exposes the local binding through the A2A v1 protocol edge. Agent Cards advertise HTTP+JSON and JSON-RPC URLs; canonical protocol requests must send A2A-Version: 1.0.

  • GET /.well-known/agent-card.json
  • POST /a2a/message:send
  • POST /a2a/message:stream
  • GET /a2a/tasks
  • GET /a2a/tasks/{task_id}
  • POST /a2a/tasks/{task_id}:cancel
  • POST /a2a/tasks/{task_id}:subscribe
  • POST /a2a/tasks/{task_id}/pushNotificationConfigs
  • GET /a2a/tasks/{task_id}/pushNotificationConfigs
  • GET /a2a/tasks/{task_id}/pushNotificationConfigs/{config_id}
  • DELETE /a2a/tasks/{task_id}/pushNotificationConfigs/{config_id}
  • GET /a2a/extendedAgentCard
  • POST /a2a/jsonrpc
  • GET /a2a/agents/{agent_id}/extendedAgentCard
  • POST /a2a/agents/{agent_id}/message:send
  • POST /a2a/agents/{agent_id}/message:stream
  • GET /a2a/agents/{agent_id}/tasks
  • GET /a2a/agents/{agent_id}/tasks/{task_id}
  • POST /a2a/agents/{agent_id}/tasks/{task_id}:cancel
  • POST /a2a/agents/{agent_id}/tasks/{task_id}:subscribe
  • POST /a2a/agents/{agent_id}/tasks/{task_id}/pushNotificationConfigs
  • GET /a2a/agents/{agent_id}/tasks/{task_id}/pushNotificationConfigs
  • GET /a2a/agents/{agent_id}/tasks/{task_id}/pushNotificationConfigs/{config_id}
  • DELETE /a2a/agents/{agent_id}/tasks/{task_id}/pushNotificationConfigs/{config_id}
  • POST /a2a/agents/{agent_id}/jsonrpc

Gateway routes use the A2A tenant field or query parameter to select a target local agent. Agent-scoped routes bind the target in the URL.

Persisted push configs trigger best-effort outbound webhook delivery on task updates.

SendMessage is idempotent for matching target, context, and protocol messageId: retries return the existing task instead of queueing duplicate local work, and repeated task-continuation messages do not duplicate task history.

Cards served there are signed using the protocol Agent Card signature shape. Well-known card responses are publicly cacheable for a short window and include weak ETag validators; authenticated extended-card responses are private-cacheable and support the same conditional request flow.

HTTP+JSON request bodies accept application/a2a+json and application/json; JSON-RPC request bodies require application/json. Invalid bodies, query parameters, and media types are normalized into A2A status payloads or JSON-RPC errors with ErrorInfo and BadRequest detail objects rather than FastAPI's default validation shape.

When the daemon starts with sb serve --grpc-port <port>, the well-known card also advertises https://secondbrain.local/a2a/bindings/grpc-struct/v1 and exposes the secondbrain.a2a.A2AGateway service with:

  • GetAgentCard
  • SendMessage
  • SendStreamingMessage
  • GetTask
  • ListTasks
  • CancelTask
  • SubscribeToTask

Both SSE and custom gRPC streaming now use the same live in-process task-event broker for low-latency delivery and tail the durable SQLite task-event journal for replay and cross-instance catch-up.

Task-stream events are also journaled durably in SQLite with per-task sequence numbers, so clients can resume after reconnects or daemon restarts by replaying from the last seen sequence.

When serve auth is enabled, the well-known gateway card now advertises both:

  • bearerAuth
  • sbTokenAuth

Unauthorized HTTP responses are plain JSON 401 responses without a browser auth challenge, and unauthorized gRPC responses include matching challenge metadata plus an x-sb-token hint.

Remaining Limitations

The major protocol-edge gaps are closed for the local daemon. The remaining constraints are narrower:

  • true shared immediate real-time delivery across multiple daemon instances would still need an external broker; the current implementation catches up through the shared SQLite journal
  • auth is still the local bearer-token model rather than protocol-level federation/auth negotiation
  • standard A2A GRPC is not advertised until generated lf.a2a.v1.A2AService stubs are shipped; the current gRPC server remains a custom local binding

The local queue remains the runtime primitive; the A2A model layer is the compatibility and correctness layer around it.

Local Execution Path

SecondBrain now has a narrow but real local A2A worker path:

  • publish(...) still creates the durable queue row first
  • claim_message(message_id) claims that specific row for execution
  • AgentRuntime.execute_message(...) runs eligible registered agents locally
  • ack(...) persists the execution result, which is then exposed as an A2A task artifact

This is the first step toward using A2A as the durable internal delegation backbone rather than only as a queue/debug surface.

Operator Surface

CLI:

sb a2a publish planner researcher "Summarize latest release notes" \
  --payload-json '{"topic":"release-notes"}'
sb a2a execute <message_id>
sb a2a consume researcher --json
sb a2a ack <message_id> --result-json '{"status":"done"}'
sb a2a status --json
sb a2a dlq --json
sb a2a replay-dlq --message-id <message_id> --to-agent researcher_retry --json
sb a2a card-upsert agent.research.v1 "Research Agent" \
  --capabilities-json '["summarize","retrieve"]' \
  --endpoint internal://a2a/research

REPL slash commands mirror the same queue and card operations under /a2a ....

/agent run in sb chat now uses an A2A-style send-message request shape instead of a queue-only intent wrapper:

  • text input becomes Message.parts
  • JSON payload becomes a structured Part(data=...)
  • the chat session id becomes the default contextId
  • --task-id <task_id> continues an existing local A2A task in place
  • --wait maps to blocking local execution

Reference Sources