A2A Protocol & Client SDK
Implement the A2A protocol with governance extension
Agent Communication Protocol Guide
How users, frontends, agents, and tools communicate on the platform — using AG-UI, A2A, A2UI, and MCP.
Protocol Architecture
The platform uses four protocols. They are not layers on top of each other — they serve different communication directions:
Scroll to zoom · Drag to pan · 100%
Protocol Roles
| Protocol | Spec | Direction | Purpose |
|---|---|---|---|
| AG-UI | AG-UI Protocol | App ↔ Agent | How the frontend and agent talk to each other (transport, events, streaming, sessions) |
| A2UI | A2UI v0.8 | Agent → App | What UI to render — declarative, structured surfaces (forms, selectors, cards) |
| A2A | Google A2A v0.3+ | Agent ↔ Agent | How agents talk to each other (discovery, task delegation, JSON-RPC) |
| MCP | Model Context Protocol | Agent ↔ Tools | How agents access tools, APIs, and data sources |
Key distinctions:
- AG-UI = how app and agent communicate (event-based streaming protocol)
- A2UI = what gets rendered (declarative generative-UI language, travels inside AG-UI
CUSTOMevents) - A2A = how agents talk to other agents (JSON-RPC task management)
- MCP = how agents use tools and data (tool registry, credential resolution)
How it maps to our platform
Scroll to zoom · Drag to pan · 100%
| Endpoint | Protocol | Client | Purpose |
|---|---|---|---|
POST /api/chat/:agentId/message | AG-UI | Portal, UI clients | User ↔ Agent interaction with streaming |
POST /api/a2a/v1 | A2A | Other agents, programmatic clients | Agent ↔ Agent task delegation |
POST {external-agent}/ag-ui | AG-UI | Platform (as client) | Platform ↔ External agent execution |
GET /.well-known/agents/:id.json | A2A | Discovery | Agent Card for capabilities & auth |
A2A Extension Declarations
Both A2UI and Governed Execution register as A2A extensions in the Agent Card, so other agents (and clients) know what capabilities are available:
{
"extensions": [
{ "uri": "urn:agentic-platform:governed-execution:v1", "required": true },
{ "uri": "https://a2ui.org/a2a-extension/a2ui/v0.8" }
]
}
1. Authentication & Access Control
Every request to the platform — A2A, AG-UI, or AG-UI/A2A turn-based — passes through the same auth middleware pipeline before reaching any route handler.
Auth Middleware Pipeline
Scroll to zoom · Drag to pan · 100%
Authentication Methods
The platform supports four authentication methods, evaluated in priority order:
| Method | Token Format | Use Case | Validation |
|---|---|---|---|
| API Key | Bearer ak_* | Programmatic clients, CI/CD, external systems | SHA-256 hash lookup in api_keys table |
| A2A Invocation Secret | Bearer a2a_inv_* | Agent-to-agent calls | SHA-256 hash lookup in agent config.a2aConfig.invocationSecretHash |
| JWT (Keycloak/OIDC) | Bearer eyJ... (RS256) | Production — SSO via corporate IdP | JWKS public key from KEYCLOAK_JWKS_URL |
| JWT (Platform) | Bearer eyJ... (HS256) | Portal/Studio/Admin frontends | Signed with NEXTAUTH_SECRET |
In development mode, mock headers are also accepted (X-User-Id, X-User-Role, X-Tenant-Id, X-User-Groups).
How to Authenticate an A2A Request
Option A: API Key (recommended for programmatic clients)
curl -X POST https://platform.example.com/api/a2a/v1 \
-H "Authorization: Bearer ak_your-api-key-here" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": "req-1",
"method": "agent/sendMessage",
"params": {
"agentId": "agent-research-001",
"message": { "role": "user", "parts": [{ "type": "text", "text": "Hello" }] }
}
}'
The platform resolves the API key to a user context:
- Tenant-wide keys (
ownerType='tenant'): grantsadminrole within the tenant - User-bound keys (
ownerType='user'): inherits the user's role and groups - Service account keys (
ownerType='service-account'): inherits the SA's role and groups
Option B: A2A Invocation Secret (agent-to-agent)
When one agent invokes another via A2A, it authenticates with the target agent's invocation secret:
curl -X POST https://platform.example.com/api/a2a/v1 \
-H "Authorization: Bearer a2a_inv_target-agent-secret" \
-H "Content-Type: application/json" \
-d '{ "jsonrpc": "2.0", "id": "1", "method": "agent/sendMessage", ... }'
This resolves to a synthetic identity: userId = "agent:{agentId}", role = "consumer", idpSource = "a2a-secret".
Option C: JWT (Portal frontend / SSO)
The Portal frontend authenticates via NextAuth.js, which sets an access-token httpOnly cookie or provides a Bearer token:
curl -X POST https://platform.example.com/api/a2a/v1 \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..." \
-H "Content-Type: application/json" \
-d '{ "jsonrpc": "2.0", "id": "1", "method": "agent/sendMessage", ... }'
JWT claims used: sub (userId), email, name, role, tenant_id, groups, idp_source.
Access Control (4 Dimensions)
After authentication, every agent interaction is checked against four access control dimensions:
Scroll to zoom · Drag to pan · 100%
D1 (User → Agent) is checked at request time in the route handler. D2-D4 are checked during workflow execution by the MCP Gateway and LLM Gateway.
Agent Card Security Schemes
Agents declare their authentication requirements in the A2A Agent Card:
{
"name": "Research Agent",
"url": "https://platform.example.com/api/a2a/v1",
"securitySchemes": {
"spiffe": {
"type": "spiffe",
"description": "SPIFFE workload identity for agent-to-agent authentication"
},
"bearer": {
"type": "http",
"scheme": "bearer",
"description": "Bearer token (API key, JWT, or A2A invocation secret)"
}
},
"security": [{ "bearer": [] }],
"extensions": [
{
"uri": "urn:agentic-platform:governed-execution:v1",
"required": true
}
]
}
Token Revocation
JWT tokens can be revoked via JTI-based blocklist stored in Redis. Revoked tokens are rejected by the auth middleware even if the signature is valid.
Governance Metadata in A2A Requests
The metadata field in SendMessageParams carries identity context for governance checks:
{
"params": {
"agentId": "agent-research-001",
"message": { ... },
"metadata": {
"userId": "user-456", // D1 checks
"tenantId": "tenant-1", // Multi-tenancy scoping
"userGroups": ["analysts"], // Entitlement filtering
"userRole": "consumer", // Role-based checks
"delegationChain": ["agent-coordinator-001"], // D3 delegation tracking
"conversationId": "conv-abc" // Context grouping
}
}
}
Note: When authenticating via API key or JWT, the platform resolves
userId,tenantId,userRole, anduserGroupsfrom the token — the client does not need to provide them inmetadata. Themetadatafields are primarily for agent-to-agent delegation context (D3) and explicit conversation binding.
2. A2A Protocol — JSON-RPC Methods (Agent-to-Agent)
A2A is used for programmatic and agent-to-agent communication. UI clients typically use the AG-UI endpoint instead (see Section 4).
agent/sendMessage (Non-Streaming)
// Request
{
"jsonrpc": "2.0",
"id": "req-1",
"method": "agent/sendMessage",
"params": {
"agentId": "agent-research-001",
"message": {
"role": "user",
"parts": [{ "type": "text", "text": "Research quantum computing" }]
},
"contextId": "conv-abc",
"metadata": {
"userId": "user-456",
"tenantId": "tenant-1"
}
}
}
// Response — A2A Task object
{
"jsonrpc": "2.0",
"id": "req-1",
"result": {
"id": "task-0001",
"contextId": "conv-abc",
"status": { "state": "working" },
"history": [
{ "role": "user", "parts": [{ "type": "text", "text": "Research quantum computing" }] }
],
"metadata": {
"agentId": "agent-research-001",
"tenantId": "tenant-1",
"protocol": "a2a"
}
}
}
agent/sendStreamingMessage (Streaming)
Same request format. Returns an SSE stream with A2A task status events:
data: {"type":"status","taskId":"task-0001","status":{"state":"working"},"final":false}
data: {"type":"status","taskId":"task-0001","status":{"state":"working","message":{"role":"agent","parts":[{"type":"text","text":"Searching..."}]}},"final":false}
data: {"type":"artifact","taskId":"task-0001","artifact":{"id":"art-1","name":"summary.md","parts":[{"type":"text","text":"# Summary\n..."}]}}
data: {"type":"status","taskId":"task-0001","status":{"state":"completed","message":{"role":"agent","parts":[{"type":"text","text":"Done."}]}},"final":true}
Note: A2A streaming uses coarse-grained task status events. For token-level streaming, tool call lifecycle, and interactive UI surfaces, use the AG-UI endpoint (Section 4).
agent/getTask
Poll for task status (when streaming is not used):
{ "jsonrpc": "2.0", "id": "2", "method": "agent/getTask", "params": { "taskId": "task-0001" } }
agent/cancelTask
Cancel a running task. Sends a cancelWorkflowSignal to Temporal:
{ "jsonrpc": "2.0", "id": "3", "method": "agent/cancelTask", "params": { "taskId": "task-0001" } }
agent/listTasks
Query tasks with filtering:
{
"jsonrpc": "2.0", "id": "4",
"method": "agent/listTasks",
"params": { "contextId": "conv-abc", "states": ["completed"], "limit": 20 }
}
3. Task State Machine
The A2A task state machine applies to all agent executions. AG-UI clients see the same state transitions reflected as RUN_STARTED → RUN_FINISHED / RUN_ERROR lifecycle events.
Scroll to zoom · Drag to pan · 100%
4. AG-UI — User-Facing Interaction Protocol
AG-UI is the primary protocol for user-facing clients (Portal, mobile apps, custom frontends). It is a separate protocol from A2A — not a layer on top of it.
- A2A = agent-to-agent (JSON-RPC, task-oriented, coarse-grained status updates)
- AG-UI = app-to-agent (HTTP POST + SSE, event-based, token-level streaming)
AG-UI Endpoint
POST /api/chat/:agentId/message
Content-Type: application/json
Accept: text/event-stream
Body: RunAgentInput
Response: SSE stream of AG-UI events
// RunAgentInput — what the client sends
interface RunAgentInput {
threadId: string; // Conversation ID
runId: string; // Unique run identifier
messages: AGUIMessage[]; // Full message history
tools?: AGUIToolDef[]; // Available tools (optional)
state?: Record<string, unknown>; // Shared state from previous runs
}
How it works internally
Scroll to zoom · Drag to pan · 100%
The Temporal workflow emits internal platform events to Redis. The SSE bridge translates them to AG-UI events:
| Platform Event | AG-UI Event |
|---|---|
workflow_started | RUN_STARTED |
token | TEXT_MESSAGE_CONTENT (per-token delta) |
tool_call | TOOL_CALL_START + TOOL_CALL_ARGS |
tool_result | TOOL_CALL_END |
workflow_step | STEP_STARTED / STEP_FINISHED |
workflow_paused | CUSTOM hitl_paused + CUSTOM a2ui |
hitl_response_received | CUSTOM hitl_resumed + CUSTOM a2ui (deleteSurface) |
done | CUSTOM token_usage + RUN_FINISHED |
workflow_failed | RUN_ERROR |
Comparison: AG-UI vs. A2A for the same execution
| AG-UI (Portal) | A2A (Agent-to-Agent) | |
|---|---|---|
| Endpoint | POST /api/chat/:agentId/message | POST /api/a2a/v1 |
| Request | RunAgentInput (threadId, messages, tools, state) | JSON-RPC agent/sendStreamingMessage (agentId, message, contextId) |
| Text streaming | TEXT_MESSAGE_CONTENT per token | Accumulated in status.message |
| Tool calls | TOOL_CALL_START → TOOL_CALL_ARGS → TOOL_CALL_END | Embedded in status.message |
| HITL | CUSTOM hitl_paused + A2UI surfaces | status.state = "input-required" (no UI) |
| Steps | STEP_STARTED / STEP_FINISHED | Not visible |
| Completion | RUN_FINISHED with token usage | status.state = "completed" (final: true) |
AG-UI Event Flow Example
SSE Stream (protocol=ag-ui):
───────────────────────────────────────────────────
1. RUN_STARTED { runId, threadId }
2. TEXT_MESSAGE_CONTENT { delta: "Let me " }
3. TEXT_MESSAGE_CONTENT { delta: "search for that..." }
4. TOOL_CALL_START { toolCallId: "tc-1", toolCallName: "web_search" }
5. TOOL_CALL_ARGS { toolCallId: "tc-1", delta: '{"query":"quantum"}' }
6. TOOL_CALL_END { toolCallId: "tc-1" }
← Platform executes tool via MCP Gateway →
7. TEXT_MESSAGE_CONTENT { delta: "I found 5 results. " }
8. TEXT_MESSAGE_CONTENT { delta: "Here's a summary..." }
9. CUSTOM { name: "urn:...governance:v1:token_usage", value: { input: 800, output: 350 } }
10. RUN_FINISHED { result: { tokenUsage: {...} } }
CUSTOM Event Namespaces
AG-UI's CUSTOM event is the extension point for all platform-specific behavior:
| Name | Source | Purpose |
|---|---|---|
a2ui | A2UI extension | Interactive UI surfaces (see Section 6) |
urn:agentic-platform:governance:v1:hitl_paused | Governance extension | Workflow paused for human input |
urn:agentic-platform:governance:v1:hitl_resumed | Governance extension | Workflow resumed after response |
urn:agentic-platform:governance:v1:token_usage | Governance extension | Token usage and cost attribution |
thinking_start / thinking_complete | Platform | LLM reasoning (collapsible in UI) |
memory_retrieved / memory_stored | Platform | Agent memory context |
knowledge_retrieved | Platform | RAG/knowledge base retrieval |
guardrail | Platform | Guardrail check result |
skills_loaded | Platform | Skill bundles loaded for agent |
5. Governed Execution Extension
A2A Extension URI: urn:agentic-platform:governed-execution:v1
This is the platform's core A2A extension. It extends A2A's Part system with two new part types that express the "agent declares, platform executes" model. Agents declare these in their Agent Card:
{
"extensions": [
{
"uri": "urn:agentic-platform:governed-execution:v1",
"name": "Governed Execution",
"description": "Agent declares tool calls; platform executes through governance pipeline",
"required": true
}
]
}
Extended A2A Part Types
// Standard A2A parts:
type A2AStandardPart = TextPart | DataPart | FilePart;
// Platform extension parts (via governed-execution extension):
type A2APart = A2AStandardPart | GovernedToolCallPart | GovernedToolResultPart;
GovernedToolCallPart (Agent → Platform)
Agent declares a tool call inside an A2A message:
{
"type": "governed-tool-call",
"toolCallId": "tc-12345",
"toolName": "web_search",
"arguments": { "query": "quantum computing basics", "limit": 5 }
}
GovernedToolResultPart (Platform → Agent)
Platform returns the result after governance pipeline execution:
{
"type": "governed-tool-result",
"toolCallId": "tc-12345",
"status": "success",
"output": { "results": [...], "count": 5 },
"durationMs": 1250,
"approvalId": "appr-999"
}
Tool Call Flow
Scroll to zoom · Drag to pan · 100%
6. HITL (Human-in-the-Loop) — Complete Flow
HITL maps to A2A's input-required task state. When the workflow needs human input, the A2A task transitions to input-required. AG-UI clients additionally receive CUSTOM events with HITL details and A2UI surfaces for interactive input.
HITL Types
| Type | A2UI Component | User Action |
|---|---|---|
plan_selection | Multi-option selector (Checkboxes in Cards) | Select one or more options |
parameter_edit | Editable form (TextFields, NumberInputs) | Modify tool parameters, approve/reject |
user_input | Text input (TextField) | Provide free-form text |
approval | Accept/reject card (Buttons) | Binary approve or reject |
End-to-End Sequence Diagram
Scroll to zoom · Drag to pan · 100%
A2A Client — Same Flow, Different Granularity
An A2A client (another agent, a programmatic client) sees the same execution as coarser-grained task status updates — no token deltas, no A2UI surfaces:
Scroll to zoom · Drag to pan · 100%
HITL API — Responding to Input Requests
AG-UI Client (Portal)
AG-UI clients POST the canonical v0.9 action envelope to the surfaces
resolve route. The envelope carries the surface's own submit context
verbatim — the platform does not restructure or rename fields, and
agents read their own submit context via
hitl_response.action.context.<field>. See
docs/SDK_GUIDE.md §"HITL Response Contract".
POST /api/surfaces/{surfaceRequestId}/resolve
Content-Type: application/json
# picker surface submit (ChoicePicker):
{
"action": {
"kind": "event",
"protocolVersion": "v0.9",
"name": "urn:a2ui:submit",
"surfaceId": "hitl-hitl-wf-001-1",
"context": { "requestId": "hitl-wf-001-1", "selected": ["plan_b"] }
},
"dataModel": null
}
# form surface submit (TextField/NumberField):
{
"action": {
"kind": "event",
"name": "urn:a2ui:submit",
"surfaceId": "hitl-hitl-wf-001-2",
"context": { "requestId": "hitl-wf-001-2", "parameters": { "query": "adjusted query", "limit": 10 } }
}
}
# input surface submit:
{
"action": {
"kind": "event",
"name": "urn:a2ui:submit",
"surfaceId": "hitl-hitl-wf-001-3",
"context": { "requestId": "hitl-wf-001-3", "value": "Focus on renewable energy" }
}
}
# approval response:
{
"type": "hitl_approval_response",
"requestId": "hitl-wf-001-4",
"approved": true,
"respondedBy": "user-456"
}
Raw A2A Client
A raw A2A client responds by sending a follow-up agent/sendMessage on the same contextId/taskId. The platform routes it as the HITL response:
{
"jsonrpc": "2.0",
"id": "req-2",
"method": "agent/sendMessage",
"params": {
"agentId": "agent-research-001",
"message": {
"role": "user",
"parts": [{ "type": "text", "text": "Focus on topics 1 and 3" }]
},
"contextId": "conv-abc"
}
}
7. A2UI — Interactive Surfaces (A2A Extension)
A2A Extension URI: https://a2ui.org/a2a-extension/a2ui/v0.8
A2UI is a separate protocol that registers as an A2A extension and travels inside AG-UI CUSTOM events:
// A2UI messages are wrapped in AG-UI CUSTOM events:
{
type: "CUSTOM",
name: "a2ui", // ← AG-UI custom event name
value: <A2UIMessage> // ← A2UI protocol message
}
A2UI provides platform-agnostic UI definitions that agents can generate. Clients render them using native widgets. The protocol uses an adjacency-list model where components reference children by ID.
Surface Lifecycle
Scroll to zoom · Drag to pan · 100%
A2UI Message Types (Server → Client)
| Message | Purpose |
|---|---|
surfaceUpdate | Create or update components in a surface (adjacency list) |
dataModelUpdate | Update data model values (bindings for dynamic content) |
beginRendering | Signal that surface is ready to render from root component |
deleteSurface | Remove a surface (after HITL resolved) |
A2UI Message Types (Client → Server)
| Message | Purpose |
|---|---|
userAction | User interacted with a component (button click, form submit) |
clientError | Client-side rendering error |
Component Types (Standard Catalog v0.8)
Layout: Column · Row · Card · Tabs · Modal · List
Display: Text · Image · Icon · Divider · ProgressBar
Input: TextField · CheckBox · MultipleChoice · Slider
Interaction: Button (with ActionDef → triggers userAction)
Example: Plan Selector Surface
{
"surfaceUpdate": {
"surfaceId": "plan-sel-1",
"components": [
{
"id": "root",
"component": {
"Column": {
"children": { "explicitList": ["header", "option_1", "option_2", "submit"] }
}
}
},
{
"id": "header",
"component": {
"Text": {
"text": { "literalString": "Select a research direction:" },
"usageHint": "heading"
}
}
},
{
"id": "option_1",
"component": {
"Card": { "child": "option_1_content" }
}
},
{
"id": "option_1_content",
"component": {
"CheckBox": {
"label": { "literalString": "Quantum Computing Basics" },
"value": { "path": "/selections/0" }
}
}
},
{
"id": "option_2",
"component": {
"Card": { "child": "option_2_content" }
}
},
{
"id": "option_2_content",
"component": {
"CheckBox": {
"label": { "literalString": "Quantum Error Correction" },
"value": { "path": "/selections/1" }
}
}
},
{
"id": "submit",
"component": {
"Button": {
"label": { "literalString": "Confirm Selection" },
"action": { "name": "submit_selection", "context": [
{ "key": "selections", "value": { "path": "/selections" } }
] }
}
}
}
]
}
}
8. Multi-Turn HITL Flow (Research Agent Example)
A real-world example showing all protocol layers working together. The agent finds 5 research topics, asks the user to select via A2UI, executes tools via Governed Execution, then asks a follow-up question.
Scroll to zoom · Drag to pan · 100%
9. Agent State Management Across Turns
External agents are stateless. The platform manages all state and sends the full context on every turn.
What the agent receives on each turn
interface AgentTurnRequest {
threadId: string; // Conversation ID
turnNumber: number; // 0-indexed turn counter
messages: AgentMessage[]; // FULL message history (grows each turn)
toolResults?: GovernedToolResultPart[]; // Results from previous turn's tool calls
context: Record<string, unknown>; // Agent's own statePatch from previous turn
availableTools?: AvailableTool[]; // Only on turn 0 (tools don't change)
}
How state flows
Scroll to zoom · Drag to pan · 100%
The agent never needs a database. The context field acts as the agent's "memory" — the platform persists it and sends it back every turn. The full messages history is also included so the agent can always reconstruct what happened.
10. Error Handling
JSON-RPC Errors (A2A Layer)
| Code | Name | Description |
|---|---|---|
-32700 | Parse Error | Invalid JSON |
-32600 | Invalid Request | Missing jsonrpc/id/method |
-32601 | Method Not Found | Unknown method |
-32602 | Invalid Params | Missing required params |
-32603 | Internal Error | Server error |
-32001 | Task Not Found | Unknown taskId |
-32002 | Task Not Cancelable | Task in terminal state |
-32003 | Agent Not Found | Unknown agentId |
-32004 | Unauthorized | Authentication failed |
-32005 | Policy Violation | D2 access denied |
AG-UI Error Events
// Workflow failure
{ "type": "RUN_ERROR", "runId": "run-abc", "message": "Tool execution failed", "code": "TOOL_ERROR" }
Governed Execution Rejection
When a tool call is rejected by the governance pipeline:
{
"type": "governed-tool-result",
"toolCallId": "tc-1",
"status": "rejected",
"rejectionReason": "Agent does not have D2 grant for tool 'database_query'"
}
11. Client Implementation Checklist
Minimal A2A Client (Agent-to-Agent)
- Send messages via
agent/sendMessageoragent/sendStreamingMessage - Parse SSE stream — handle
data:lines, matchtype: "status"andtype: "artifact" - Handle
input-required— detect when task needs input, send follow-upsendMessage - Handle errors — JSON-RPC error responses and
status.state = "failed" - Support cancellation —
agent/cancelTaskfor abort - Poll if needed —
agent/getTaskwhen SSE is unavailable
Full AG-UI Client (Portal / UI)
Everything above, plus:
- Negotiate AG-UI — append
?protocol=ag-uito SSE endpoint - Accumulate text deltas — build full message from
TEXT_MESSAGE_CONTENTevents - Track tool calls — match
TOOL_CALL_START→TOOL_CALL_ARGS→TOOL_CALL_ENDbytoolCallId - Detect HITL pauses — listen for
CUSTOMevents withhitl_pausedname - Render A2UI surfaces — parse
surfaceUpdate,beginRendering,deleteSurfacefromCUSTOM "a2ui"events - Submit HITL responses —
POST /api/surfaces/:surfaceRequestId/resolvewith the canonical v0.9 action envelope ({action: {kind, name, surfaceId, context}, dataModel?}) - Handle HITL resume — remove A2UI surface, create new message placeholder
- Display thinking blocks — collapsible reasoning from
thinking_start/thinking_complete - Show token usage — from
token_usageCUSTOM events orRUN_FINISHEDresult - Handle governance events — guardrail results, memory retrieval, skill loading