--- description: "A2A (Agent2Agent) integration with Conductor — call remote agents as durable workflow tasks, and expose Conductor workflows as A2A agents. Crash-safe, resumable, observable." --- # A2A integration [A2A (Agent2Agent)](https://a2a-protocol.org/) is an open protocol for agents to talk to one another over HTTP/JSON-RPC. Conductor integrates A2A in **both directions**: - **Client** — call a remote A2A agent from a workflow as a durable system task (`AGENT`, `GET_AGENT_CARD`, `CANCEL_AGENT`). - **Server** — expose any Conductor workflow as an A2A agent that other A2A clients (Google ADK, CrewAI, LangGraph, another Conductor) can discover and invoke. The integration is **durable**: a remote agent call survives a server crash, restart, or redeploy, and resumes from where it left off — the call's state lives in the workflow execution, not in a thread. ## What is A2A A2A standardizes three things: an **Agent Card** (a `/.well-known/agent-card.json` document describing an agent's skills), a **JSON-RPC** surface (`message/send`, `tasks/get`, `tasks/cancel`, …), and a **task lifecycle** (`submitted → working → input-required → completed/failed/canceled`). An agent runs a long task; the client polls (or is pushed) until it reaches a terminal state. Conductor maps this lifecycle onto its own durable task model, so a remote agent task behaves like any other Conductor task — retried, timed out, observed, and resumed by the engine. Conductor speaks A2A in **both directions**: a workflow can *call* remote agents (client), and a workflow can *be* an agent that external A2A clients call (server). ```mermaid flowchart LR ExtClient["External A2A client
(Google ADK · CrewAI · LangGraph · another Conductor)"] Remote["Remote A2A agent"] subgraph C["Conductor"] WF["Workflow execution
(durable · resumable · observable)"] end ExtClient -->|"server: message/send starts the workflow"| WF WF -->|"client: AGENT task sends message/send"| Remote ``` ## Call a remote agent from a workflow (client) *Direction A — Conductor is the A2A client.* These tasks require the AI integration to be enabled: ```properties conductor.integrations.ai.enabled=true ``` Each task takes an **`agentType`** input that selects the agent runtime. It defaults to `"a2a"` (Agent2Agent — the only runtime in OSS today); native runtimes such as LangGraph and OpenAI are planned, and the field is the extension point for them. An unrecognized `agentType` fails the task with a clear error. ### AGENT — send a message to an agent Sends an A2A `message/send` and works the resulting agent task to a terminal state. Non-blocking: a fast reply completes immediately; long-running work moves to `IN_PROGRESS` and is polled at a cadence (no worker thread is held). ```mermaid sequenceDiagram autonumber participant WF as Conductor workflow participant T as AGENT task participant R as Remote A2A agent WF->>T: schedule { agentUrl, message } T->>R: message/send (idempotencyKey = deterministic messageId) R-->>T: Task { state: working } alt poll (default) / push backstop loop until terminal or input-required T->>R: tasks/get R-->>T: Task { working → completed } end else streaming R-->>T: SSE status-update / artifact-update … end T-->>WF: artifacts + state as task output ``` ```json { "name": "call_currency_agent", "taskReferenceName": "agent", "type": "AGENT", "inputParameters": { "agentType": "a2a", "agentUrl": "https://currency-agent.example.com", "text": "convert 100 USD to EUR", "pollIntervalSeconds": 5, "headers": { "Authorization": "Bearer ${workflow.input.agentToken}" } } } ``` **Key inputs** (see `A2ACallRequest`): | Field | Description | |---|---| | `agentType` | Agent runtime to use — defaults to `"a2a"`. Reserved for native runtimes (e.g. `langgraph`, `openai`) coming later; any other value is rejected today. | | `agentUrl` | Base URL of the remote agent (required). | | `text` / `prompt` | Convenience for a single text part. | | `parts` / `message` | A full A2A message (multi-part / data parts) instead of `text`. | | `contextId`, `taskId` | Continue an existing conversation / resume an agent task (multi-turn). | | `headers` | Per-call HTTP headers (e.g. auth). Reference credentials via workflow inputs/secrets rather than hardcoding them. | | `pollIntervalSeconds` | Poll cadence in poll mode (default 5). | | `streaming` | `true` → consume `message/stream` (SSE) and aggregate to completion. | | `pushNotification` | `true` → the agent calls back our webhook on completion (see below). | | `maxDurationSeconds` | Absolute deadline (default 86400). | | `maxPollFailures` | Consecutive transient poll failures tolerated before failing (default 30). | **Output** (`agent.output`): `state` (the A2A task state), `taskId` and `contextId` (for resumption), `artifacts`, `text` (extracted text), `agentMessage`, and the full `task` object. For a completed call it looks like: ```json { "state": "completed", "taskId": "task-7f3a", "contextId": "ctx-7f3a", "text": "100 USD = 92.40 EUR", "artifacts": [ { "artifactId": "result", "parts": [ { "kind": "text", "text": "100 USD = 92.40 EUR" } ] } ] } ``` Downstream tasks read these with `${agent.output.text}`, `${agent.output.taskId}`, etc. #### Three execution modes - **Poll** (default) — the task is `IN_PROGRESS` and polled via `tasks/get` at `pollIntervalSeconds`. No thread is held between polls; the call survives restarts. - **Streaming** (`streaming: true`) — consumes the agent's SSE stream and aggregates events. Requires `capabilities.streaming=true` on the agent card. Holds a thread for the stream's duration. - **Push** (`pushNotification: true`) — the agent calls Conductor's webhook when the task finishes, so nothing polls in the meantime. Requires `conductor.a2a.callback.url`. A slow **backstop poll** still runs (`pushBackstopPollSeconds`, default 300) so a lost webhook can't hang the task. #### Push notifications — end to end **1. Configure the externally-reachable callback base URL** (where the agent can reach Conductor): ```properties conductor.integrations.ai.enabled=true conductor.a2a.callback.url=https://conductor.example.com ``` **2. Ask for push on the task:** ```json { "name": "call_research_agent", "taskReferenceName": "agent", "type": "AGENT", "inputParameters": { "agentUrl": "https://research-agent.example.com", "text": "research durable agent protocols", "pushNotification": true, "pushBackstopPollSeconds": 300 } } ``` **3. What Conductor sends** — the `message/send` carries a `pushNotificationConfig` pointing at a per-task webhook, with a single-use bearer token (a `{uuid}:{expiryEpochMillis}` value, 24h TTL): ```json { "method": "message/send", "params": { "message": { "role": "user", "messageId": "a2a-...", "parts": [ { "kind": "text", "text": "research durable agent protocols" } ] }, "configuration": { "pushNotificationConfig": { "url": "https://conductor.example.com/api/a2a/callback/", "token": "3f9c…:1750300000000", "authentication": { "schemes": ["Bearer"], "credentials": "3f9c…:1750300000000" } } } } } ``` The `AGENT` task then **waits** (holds no worker thread) until the webhook arrives; the backstop poll runs only as a safety net. **4. The agent calls back** when the task reaches a terminal/interrupted state — Conductor verifies the token (constant-time + expiry), fetches the final task via `tasks/get`, and completes the workflow task: ```bash curl -X POST https://conductor.example.com/api/a2a/callback/ \ -H 'Authorization: Bearer 3f9c…:1750300000000' \ -H 'Content-Type: application/json' \ -d '{ "taskId": "", "status": { "state": "completed" } }' # → 200 OK; the AGENT task is now COMPLETED with the agent's output. ``` Agents that don't support the `authentication` field fall back to a `?token=` query parameter on the callback URL, which the endpoint still accepts (with a deprecation warning, since tokens in URLs land in access logs). ### GET_AGENT_CARD — discover an agent ```json { "name": "discover_agent", "taskReferenceName": "discover", "type": "GET_AGENT_CARD", "inputParameters": { "agentUrl": "https://currency-agent.example.com" } } ``` Resolves the agent card from `/.well-known/agent-card.json` (falling back to the legacy `/.well-known/agent.json`) and returns the parsed skills/capabilities — feed it to an LLM so it can pick a skill at runtime. ### CANCEL_AGENT — cancel a running agent task ```json { "name": "cancel_agent_task", "taskReferenceName": "cancel", "type": "CANCEL_AGENT", "inputParameters": { "agentUrl": "https://currency-agent.example.com", "taskId": "${agent.output.taskId}" } } ``` ### Multi-turn (input-required) When a remote task reaches `input-required` (or `auth-required`), `AGENT` **completes** and surfaces the agent's question plus the `taskId`/`contextId` in its output. The workflow branches on that state and issues another `AGENT` task with the **same `taskId` and `contextId`** carrying the answer — resuming the same remote task rather than starting a new conversation: ```json { "name": "branch_on_state", "taskReferenceName": "branch", "type": "SWITCH", "evaluatorType": "value-param", "expression": "state", "inputParameters": { "state": "${ask.output.state}" }, "decisionCases": { "input-required": [ { "name": "answer_agent", "taskReferenceName": "answer", "type": "AGENT", "inputParameters": { "agentUrl": "${workflow.input.agentUrl}", "text": "${workflow.input.answer}", "contextId": "${ask.output.contextId}", "taskId": "${ask.output.taskId}" } } ] }, "defaultCase": [] } ``` Full example: `ai/examples/29-a2a-client-multi-turn.json`. ### Orchestrating multiple agents Because each `AGENT` is an ordinary durable task, you compose agents with the usual Conductor operators — e.g. **`FORK_JOIN`** to call several agents in parallel, **`JOIN`** to gather results. Every branch is independently crash-safe: if Conductor restarts mid-flight, each in-flight agent call resumes from persisted state (`ai/examples/27-a2a-multi-agent.json`). To let an LLM pick which skill to use, chain `GET_AGENT_CARD → LLM_CHAT_COMPLETE → AGENT` (`ai/examples/28-a2a-llm-pick-skill.json`). ```mermaid flowchart LR Start([Workflow]) --> Fork{{FORK_JOIN}} Fork --> A1[AGENT → agent A] Fork --> A2[AGENT → agent B] Fork --> A3[AGENT → agent C] A1 --> Join{{JOIN}} A2 --> Join A3 --> Join Join --> Next([aggregate results]) ``` ### Error handling & retries `AGENT` maps remote outcomes onto Conductor task statuses, so the engine's normal retry/timeout machinery applies. Retryable failures become `FAILED` (the engine retries per the task def's `retryCount`); permanent failures become `FAILED_WITH_TERMINAL_ERROR` (no retry): | Condition | Task status | Retried? | |---|---|---| | HTTP 408/429/5xx, connect/read timeout, dropped/empty stream | `FAILED` | yes | | JSON-RPC transient error (e.g. `-32603` internal) | `FAILED` | yes | | Remote agent task ends `failed` / `rejected` | `FAILED` | yes | | HTTP 4xx (except 408/429) | `FAILED_WITH_TERMINAL_ERROR` | no | | JSON-RPC terminal codes (`-32700/-32600/-32601/-32602/-3200{1..5,7}`) | `FAILED_WITH_TERMINAL_ERROR` | no | | Missing `agentUrl` / empty message / **SSRF-blocked** URL | `FAILED_WITH_TERMINAL_ERROR` | no | | Exceeds `maxDurationSeconds`, or `maxPollFailures` consecutive poll failures | `FAILED_WITH_TERMINAL_ERROR` | no | Retries reuse the deterministic `messageId`, so agents that dedupe on it get effectively-once delivery. The failure reason is on `task.reasonForIncompletion`. **Troubleshooting** | Symptom | Cause / fix | |---|---| | `… SSRF blocked` | `agentUrl` resolves to a private/loopback/metadata address. Use a public URL, or set `conductor.a2a.client.allow-private-network=true` for trusted/dev (cloud-metadata stays blocked). | | `streaming: true` behaves like poll | The agent card has `capabilities.streaming=false`; the client only streams when the agent advertises it. | | Fails after N poll failures | The agent is unreachable — raise `maxPollFailures` or check connectivity. | | Hangs, then fails at the deadline | The agent never reached a terminal state within `maxDurationSeconds`. | ## Expose a workflow as an A2A agent (server) *Direction B — Conductor is the A2A server.* Any Conductor workflow can be published as an A2A agent that other A2A clients (Google ADK, CrewAI, LangGraph, another Conductor) discover and invoke. The workflow execution **is** the durable, resumable A2A task — that's the native fit. ```mermaid sequenceDiagram autonumber participant Client as External A2A client participant S as A2AServerResource participant A as A2AWorkflowAgent participant E as Conductor engine Client->>S: GET …/.well-known/agent-card.json S-->>Client: Agent Card (one skill = the workflow) Client->>S: POST message/send S->>A: sendMessage A->>E: startWorkflow (idempotencyKey = A2A messageId) E-->>A: workflowId A-->>Client: Task { id = workflowId, state: working } loop tasks/get until terminal Client->>S: tasks/get S->>E: getExecutionStatus E-->>S: RUNNING → COMPLETED S-->>Client: Task { state, artifacts } end note over Client,E: blocked on HUMAN/WAIT → input-required;
a follow-up message/send resumes the same execution ``` Enable the server and opt the workflow in: ```properties conductor.a2a.server.enabled=true # Expose by name… conductor.a2a.server.exposed-workflows=order_pizza,book_appointment ``` …or per-workflow via `WorkflowDef.metadata`: ```json { "name": "order_pizza", "version": 1, "metadata": { "a2a.enabled": true, "a2a.tags": ["ordering"] }, "tasks": [ ... ] } ``` **Routing: one agent per workflow.** Each exposed workflow is its own focused agent at `{basePath}/{workflow}` (default basePath `/a2a`): | Method & path | Purpose | |---|---| | `GET /a2a/{workflow}/.well-known/agent-card.json` | Agent Card (also `/agent.json`). | | `POST /a2a/{workflow}` | JSON-RPC: `message/send`, `message/stream` (SSE), `tasks/get`, `tasks/cancel`. | | `GET /a2a` | Convenience listing of exposed agents (non-spec). | Exposed agents advertise `capabilities.streaming=true`. ### Discover and call ```bash # 1. Discover curl http://localhost:8080/a2a/order_pizza/.well-known/agent-card.json # 2. Start a task (message/send → starts the workflow) curl -X POST http://localhost:8080/a2a/order_pizza \ -H 'Content-Type: application/json' \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "message/send", "params": { "message": { "role": "user", "messageId": "m-1", "parts": [ { "kind": "text", "text": "one large pepperoni" } ] } } }' # → result is an A2A Task: { "id": "", "contextId": ..., "status": { "state": "working" } } # 3. Poll curl -X POST http://localhost:8080/a2a/order_pizza \ -H 'Content-Type: application/json' \ -d '{ "jsonrpc": "2.0", "id": 2, "method": "tasks/get", "params": { "id": "" } }' ``` The inbound A2A message is injected into the workflow input as `_a2a_text`, `_a2a_message_id`, `_a2a_context_id` (plus any data parts), and `contextId` becomes the workflow `correlationId`. ### Streaming (message/stream) Use `message/stream` instead of `message/send` for a Server-Sent Events stream: the initial `Task`, then `status-update` events as the workflow's A2A state changes and `artifact-update` events as output is produced, ending with a `final` status-update at a terminal / input-required state. ```bash curl -N -X POST http://localhost:8080/a2a/order_pizza \ -H 'Content-Type: application/json' \ -d '{ "jsonrpc":"2.0", "id":1, "method":"message/stream", "params": { "message": { "role":"user", "messageId":"m-1", "parts":[ {"kind":"text","text":"one large pepperoni"} ] } } }' ``` ```text data: {"jsonrpc":"2.0","id":1,"result":{"kind":"task","id":"wf-7f3a","status":{"state":"working"}}} data: {"jsonrpc":"2.0","id":1,"result":{"kind":"artifact-update","taskId":"wf-7f3a","artifact":{"artifactId":"workflow-output","parts":[{"kind":"data","data":{"orderId":"ORD-42"}}]}}} data: {"jsonrpc":"2.0","id":1,"result":{"kind":"status-update","taskId":"wf-7f3a","status":{"state":"completed"},"final":true}} ``` The stream is a live view of the durable execution — if the connection drops, resume tracking with `tasks/get`. Tuning: `conductor.a2a.server.stream-poll-interval-millis` (default 500) and `conductor.a2a.server.stream-max-duration-seconds` (default 300). ### Durable, idempotent start `message/send` starts the workflow with `idempotencyKey = {workflow}:{messageId}` and `RETURN_EXISTING`, so a client's **retried** `message/send` returns the **existing** execution rather than starting a duplicate — server-side effectively-once. The execution's durability (crash-safe, resumable) is inherited from the engine. ### Status mapping | Conductor workflow | A2A task state | |---|---| | RUNNING, blocked on a `HUMAN`/`WAIT` task | `input-required` | | RUNNING (not blocked) / PAUSED | `working` | | COMPLETED | `completed` (output → an artifact) | | FAILED / TIMED_OUT | `failed` | | TERMINATED | `canceled` | ### Multi-turn resume — worked example If the workflow blocks on a `HUMAN`/`WAIT` task, the agent reports `input-required`. A follow-up `message/send` carrying that task's `id` (the workflow id) **resumes** the paused execution — the message content completes the pending task and the workflow continues. No duplicate workflow is started; if the workflow is already terminal or not awaiting input, its current state is returned unchanged. Take this exposed workflow (`ai/examples/25-a2a-server-multi-turn.json`) — it asks a question, then confirms: ```json { "name": "book_appointment", "version": 1, "metadata": { "a2a.enabled": true }, "tasks": [ { "name": "ask_preferred_time", "taskReferenceName": "ask", "type": "HUMAN" }, { "name": "confirm_appointment", "taskReferenceName": "confirm", "type": "INLINE", "inputParameters": { "evaluatorType": "graaljs", "expression": "({ status: 'confirmed', when: $.when })", "when": "${ask.output._a2a_text}" } } ] } ``` **Turn 1 — start.** The workflow reaches the `HUMAN` task and parks at `input-required`: ```bash curl -X POST http://localhost:8080/a2a/book_appointment \ -H 'Content-Type: application/json' \ -d '{ "jsonrpc":"2.0", "id":1, "method":"message/send", "params": { "message": { "role":"user", "messageId":"m-1", "parts":[ {"kind":"text","text":"Book me a dentist appointment"} ] } } }' ``` ```json { "jsonrpc": "2.0", "id": 1, "result": { "kind": "task", "id": "wf-7f3a91", "contextId": "wf-7f3a91", "status": { "state": "input-required", "message": { "role": "agent", "parts": [ { "kind": "text", "text": "Workflow is awaiting input. Send another message/send carrying this task's id to provide the input and resume the execution." } ] } } } } ``` **Turn 2 — resume.** Send the answer with the **same `taskId`** (`= result.id`); the `HUMAN` task completes with the message as its input and the workflow finishes: ```bash curl -X POST http://localhost:8080/a2a/book_appointment \ -H 'Content-Type: application/json' \ -d '{ "jsonrpc":"2.0", "id":2, "method":"message/send", "params": { "message": { "role":"user", "messageId":"m-2", "taskId":"wf-7f3a91", "parts":[ {"kind":"text","text":"Tuesday at 3pm"} ] } } }' ``` ```json { "jsonrpc": "2.0", "id": 2, "result": { "kind": "task", "id": "wf-7f3a91", "contextId": "wf-7f3a91", "status": { "state": "completed" }, "artifacts": [ { "artifactId": "workflow-output", "name": "output", "parts": [ { "kind": "data", "data": { "status": "confirmed", "when": "Tuesday at 3pm" } } ] } ] } } ``` The answer (`Tuesday at 3pm`) arrives at the workflow as `${ask.output._a2a_text}`, exactly as if the `HUMAN` task had been completed through the Conductor API. ## Durability The "durable A2A" claim rests on a few concrete mechanisms: - **Deterministic message id.** `AGENT` derives the A2A `messageId` from `workflowInstanceId + referenceTaskName + iteration` — stable across task retries and server restarts, distinct per `DO_WHILE` iteration. Agents that dedupe on `messageId` get effectively-once delivery despite at-least-once retries. - **State in the execution, not the thread.** Poll mode holds no thread; the remote `taskId`, deadline, and poll-failure count live in the persisted task output, so a restart resumes the poll loop. - **Liveness guards.** An absolute deadline (`maxDurationSeconds`) and a consecutive-poll-failure bound (`maxPollFailures`) ensure a dead or stuck agent can't hang a task forever. - **Push backstop.** Push mode still backstop-polls, so a lost webhook degrades to polling rather than hanging. ## Security - **SSRF guard.** Outbound `agentUrl`s that resolve to loopback, private (RFC-1918), link-local, IPv6 unique-local (`fc00::/7`), or cloud-metadata addresses are rejected. Cloud-metadata addresses are blocked **always**. To allow private-network agents (e.g. localhost in dev): ```properties conductor.a2a.client.allow-private-network=true ``` Cloud metadata stays blocked even with this on. For production, prefer a network-layer egress firewall. - **Server auth.** Like OSS Conductor REST, the A2A server is **open by default**. Front it with a gateway/firewall (or mTLS) to control access. Inbound authentication (API keys, OAuth/OIDC, mTLS, per-skill scopes, signed Agent Cards) is provided by the **enterprise** build. - **Push tokens.** Push callbacks carry a single-use bearer token with an embedded 24h expiry, validated constant-time by the callback endpoint (`POST /api/a2a/callback/{taskId}`). ## Observability A2A code paths emit metrics through the shared Conductor metrics registry and set MDC keys for log correlation. **Metrics:** `a2a_client_calls{result}`, `a2a_client_poll_failures`, `a2a_rpc_errors{method,terminal}`, `a2a_ssrf_blocked`, `a2a_server_requests{method}`, `a2a_server_resumes`. **MDC keys** (greppable in logs): `a2aWorkflowId`, `a2aTaskId`, `a2aRef`, `a2aRemoteTaskId`, `a2aContextId`, `a2aMessageId`, `a2aAgent`, `a2aMethod`. ## Configuration reference | Property | Default | Purpose | |---|---|---| | `conductor.integrations.ai.enabled` | `false` | Enables the client tasks (`AGENT`, …). | | `conductor.a2a.callback.url` | — | Externally-reachable base URL for push callbacks. | | `conductor.a2a.client.allow-private-network` | `false` | Allow agent URLs on private/loopback networks (metadata still blocked). | | `conductor.a2a.server.enabled` | `false` | Enables the A2A server endpoints. | | `conductor.a2a.server.basePath` | `/a2a` | Base path for exposed agents. | | `conductor.a2a.server.exposed-workflows` | — | Comma-separated workflow names to expose. | | `conductor.a2a.server.public-url` | request-derived | Base URL advertised in the agent card. | | `conductor.a2a.server.provider-organization` | `Conductor` | `provider.organization` on the card. | ## Examples ### A complete workflow: discover then call This workflow discovers a remote agent's card, then calls it — passing the agent URL and prompt as workflow inputs so the same definition works against any A2A agent: ```json { "name": "a2a_interop_echo", "version": 1, "schemaVersion": 2, "description": "Discover a remote A2A agent, then call it.", "ownerEmail": "a2a@example.com", "tasks": [ { "name": "discover_agent", "taskReferenceName": "discover", "type": "GET_AGENT_CARD", "inputParameters": { "agentUrl": "${workflow.input.agentUrl}" } }, { "name": "call_agent", "taskReferenceName": "call", "type": "AGENT", "inputParameters": { "agentUrl": "${workflow.input.agentUrl}", "text": "${workflow.input.prompt}", "pollIntervalSeconds": 2 } } ] } ``` Register and run it (the AI integration must be enabled — `conductor.integrations.ai.enabled=true`; for a localhost agent in dev also set `conductor.a2a.client.allow-private-network=true`): ```bash # register curl -X POST localhost:8080/api/metadata/workflow \ -H 'Content-Type: application/json' -d @a2a_interop_echo.json # run against a reachable A2A agent curl -X POST localhost:8080/api/workflow/a2a_interop_echo \ -H 'Content-Type: application/json' \ -d '{"agentUrl":"http://localhost:9999","prompt":"convert 100 USD to EUR"}' ``` ### Run it end to end (showcase demos) Two self-contained demos under `ai/src/test/resources/a2a/` boot a real agent + Conductor and run a workflow against it — no API keys: ```bash # Interop: Conductor calls the official a2a-sdk reference agent (a real, non-Conductor A2A server) ai/src/test/resources/a2a/interop-demo/run-interop-demo.sh # Durability: kill the Conductor server mid-call; the workflow resumes and completes after restart ai/src/test/resources/a2a/durable-demo/run-durable-demo.sh ``` ### Example library Runnable workflow definitions live in [`ai/examples/`](https://github.com/conductor-oss/conductor/tree/main/ai/examples): | File | Shows | |---|---| | `10-a2a-call-agent.json` | Call a remote agent (poll mode) | | `11-a2a-get-agent-card.json` | Discover an agent's skills | | `12-a2a-server-workflow.json` | Expose a workflow as an A2A agent | | `23-a2a-streaming.json` | Streaming (SSE) call | | `24-a2a-push.json` | Push-notification mode | | `25-a2a-server-multi-turn.json` | Multi-turn server agent (HUMAN task → resume) | | `26-a2a-cancel.json` | Start then cancel a remote agent task | | `27-a2a-multi-agent.json` | Call multiple agents in parallel (FORK_JOIN → JOIN) | | `28-a2a-llm-pick-skill.json` | Discover → LLM picks the prompt → call | | `29-a2a-client-multi-turn.json` | Client multi-turn (branch on input-required, re-call) |