Free lesson · GenAI Solutions Architecture

Implement A2A task delegation with streaming artifact exchange

You will build an A2ATaskDelegator that routes tasks to capable agents using the A2A protocol with support for streaming status updates and large artifact exchange. Define a DelegationRequest Pydantic model with fields task_id: str, source_agent_id: str, target_agent_id: str, skill_id: str, input_payload: dict, priority: TaskPriority (enum: low, normal, high, critical), timeout_seconds: int, artifacts: list[ArtifactReference], streaming: bool, and callback_url: str | None. The ArtifactReference model includes artifact_id: str, content_type: str, size_bytes: int, storage_url: str, checksum_sha256: str, and encryption: str | None. Implement delegate_task(request: DelegationRequest) -> TaskHandle that resolves the target agent from the registry via A2AAgentCardRegistry.discover_agents(), constructs an A2A tasks/send JSON-RPC request with the input payload and artifact references, sends it to the target agent's endpoint using httpx.AsyncClient, handles HTTP 429 responses with Retry-After backoff, and returns a TaskHandle with task_id: str, status: TaskStatus, status_stream_url: str, estimated_completion_seconds: float. Build stream_task_status(task_id: str) -> AsyncIterator[TaskStatusUpdate] that connects to the target agent's tasks/sendSubscribe SSE endpoint using httpx_sse, yielding TaskStatusUpdate events with fields status: str (submitted, working, input-required, completed, failed), progress_percentage: float, message: str, partial_result: dict | None, and updated_at: datetime. Handle SSE reconnection on network interruption with exponential backoff. Implement MultiStepOrchestrator using LangGraph that chains multiple A2A delegations: define a StateGraph where each node wraps a delegate_task() call to a different agent, edges carry artifacts from one agent's output to the next agent's input via ArtifactReference, conditional edges handle branching based on intermediate results, and the orchestrator manages the overall task lifecycle including timeout and cancellation propagation. Store delegation history in PostgreSQL a2a_delegations table with columns delegation_id, source_agent, target_agent, skill_id, status, started_at, completed_at, artifacts_transferred, error_message, retry_count. Emit Prometheus metrics a2a_delegations_total{source,target,status}, a2a_delegation_duration_seconds{skill}, a2a_artifacts_transferred_bytes{direction}, a2a_streaming_events_total{event_type}, and a2a_delegation_retries_total{reason}. Build a FastAPI endpoint POST /api/v1/a2a/delegate that accepts delegation requests and returns streaming SSE responses for real-time status tracking, and GET /api/v1/a2a/delegations/{task_id} returning the current task status, artifact list, and delegation history chain for debugging multi-step orchestrations.

Course: GenAI Architecture & Design Patterns · Chapter 12 · A2A Agent Network

Free to read — no subscription required.

Introduction

When you delegate a task from one agent to another in an A2A network, the interaction is nothing like a simple RPC call—it involves capability negotiation, streaming progress updates, large artifact transfers, and multi-step orchestration where intermediate results feed subsequent delegation chains. Get this wrong and agents block indefinitely, artifacts arrive truncated, or transient failures get silently swallowed. By the end of this lesson, you'll be able to build a DelegationRequest model that captures timeout, retry, and artifact-format constraints, route a task to a capable target agent, and consume streaming status updates that reassemble chunked artifacts—all while preserving protocol-level guarantees around delivery, ordering, and failure recovery. This is the machinery that turns isolated agents into a coordinated workforce.

Key Terminology

Concepts

Handling Large Artifact Exchange

A2A artifacts are not limited to small text payloads. Agents may exchange large datasets, images, or compiled binaries. The ArtifactSpec.max_size_bytes field in the delegation request signals size limits, but the real challenge is streaming large artifacts without exhausting memory. The A2A protocol supports chunked artifact delivery through multiple SSE events, each carrying a portion of the artifact with append semantics. Your delegator must reassemble these chunks, which means maintaining a buffer keyed by artifact index and flushing to persistent storage once the artifact is complete.

For artifacts exceeding practical SSE payload sizes (typically above 5 MB), the recommended pattern is artifact-by-reference: the target agent uploads the artifact to a shared object store and sends a reference URI in the artifact's uri field instead of inline data. The delegator then fetches the artifact asynchronously. This keeps the SSE stream lightweight and prevents timeout issues on slow connections. The ArtifactSpec.streaming_preferred flag in your DelegationRequest tells the target agent which mode to use—set it to False for large binary artifacts where reference-based exchange is more reliable.

Retry and Dead Letter Semantics in Delegation

Not every delegation succeeds on the first attempt. Network partitions, agent overload, and transient bugs all cause failures. The max_retries field in DelegationRequest controls how many times the delegator should retry before giving up. However, retry logic for streaming delegations is nuanced: you cannot simply re-send the same request, because the target agent may have partially completed the work. Idempotent retries require sending the same task_id, which the A2A protocol uses to deduplicate—if the target agent recognizes the task_id, it resumes from where it left off rather than starting over.

When retries are exhausted, the failed delegation enters a dead letter queue (DLQ). The DLQ serves two purposes: it preserves the full delegation context (request, partial artifacts, error messages) for debugging, and it provides a retry surface for operators who can manually resubmit tasks after resolving the underlying issue. In production A2A networks, the DLQ is typically backed by a durable message queue like Redis Streams or Google Cloud Pub/Sub, ensuring that failed delegations survive agent restarts.

  • Idempotency key: Always reuse the original task_id when retrying. The A2A protocol mandates that agents treat duplicate task_id submissions as resume operations, not new tasks.
  • Backoff strategy: Use exponential backoff with jitter (base 2 seconds, max 60 seconds) between retries. Fixed-interval retries cause thundering herd problems when multiple delegators retry simultaneously against a recovering agent.
  • Partial artifact preservation: Before retrying, persist any artifacts received from the working state updates. Even if the retry succeeds, the earlier partial artifacts may contain useful diagnostic data.
  • Chain-aware DLQ: Store the full delegation_chain with each DLQ entry. When debugging cascading failures across a multi-hop orchestration, the chain reveals exactly which hop failed and which agents were involved upstream.
Loading diagram...

Code Walkthrough

Building on the large-artifact exchange and retry semantics described above, this section turns those ideas into a working delegator. The foundation is a well-structured request model that captures not just what needs to be done, but how the delegator expects the work to proceed—timeout constraints, artifact-format preferences, and retry budget. The DelegationRequest below uses Pydantic for schema validation, and its embedded ArtifactSpec tells the target agent what output the delegator can consume.

Code snippetpython
1from pydantic import BaseModel, Field 2from enum import Enum 3import uuid 4 5class ArtifactSpec(BaseModel): 6 mime_types: list[str] = Field(default_factory=lambda: ["application/json"]) 7 max_size_bytes: int = Field(default=10_485_760) # 10 MB 8 streaming_preferred: bool = Field(default=True) 9 10class TaskPriority(str, Enum): 11 LOW = "low" 12 NORMAL = "normal" 13 HIGH = "high" 14 CRITICAL = "critical" 15 16class DelegationRequest(BaseModel): 17 task_id: str = Field(default_factory=lambda: str(uuid.uuid4())) 18 capability_required: str 19 input_message: str 20 artifact_spec: ArtifactSpec = Field(default_factory=ArtifactSpec) 21 priority: TaskPriority = TaskPriority.NORMAL 22 timeout_seconds: int = Field(default=300, ge=10, le=3600) 23 max_retries: int = Field(default=3, ge=0, le=10) 24 delegation_chain: list[str] = Field(default_factory=list)

Setting streaming_preferred=True requests chunked SSE delivery; the max_retries budget bounds how many times the delegator re-sends after a transient failure. When a task runs via tasks/sendSubscribe, the target streams status updates, each carrying a portion of an artifact. The delegator must reassemble these chunks—buffering by artifact index and honoring append semantics—and fall back to artifact-by-reference when the agent returns a uri instead of inline bytes.

Code snippetpython
1import httpx 2 3async def collect_artifacts(events, spec: ArtifactSpec) -> dict[int, bytes]: 4 buffers: dict[int, bytearray] = {} 5 for event in events: # each SSE status update 6 art = event["artifact"] 7 idx = art["index"] 8 if art.get("uri"): # artifact-by-reference (large payloads) 9 async with httpx.AsyncClient() as client: 10 resp = await client.get(art["uri"]) 11 buffers[idx] = bytearray(resp.content) 12 continue 13 buffers.setdefault(idx, bytearray()) 14 if art.get("append"): # chunked inline delivery 15 buffers[idx].extend(art["bytes"]) 16 else: 17 buffers[idx] = bytearray(art["bytes"]) 18 return {i: bytes(b) for i, b in buffers.items()}

Each inline chunk extends its index's buffer, while a uri artifact is fetched asynchronously and stored whole—keeping the SSE stream lightweight for payloads above a few megabytes. Verify by delegating a task with streaming_preferred=True and confirming collect_artifacts reassembles every chunk in index order; you'll know it works when a multi-chunk inline artifact matches its expected byte length and a reference-mode artifact is fetched from its uri.

Do's and Don'ts

Do's

  1. Do embed ArtifactSpec inside DelegationRequest with explicit mime_types and streaming_preferred=True — declaring the artifact contract upfront tells the target agent what output format and delivery mode you can consume; without it, the target may buffer the entire payload server-side and return bytes in a format collect_artifacts cannot reassemble, silently producing zero usable output.
  2. Do buffer SSE chunks by artifact["index"] and branch on the append flag before extending — calling buffers[idx].extend(art["bytes"]) only when append is true and assigning a fresh bytearray otherwise preserves chunk ordering across all status events; skipping the flag check overwrites every prior chunk with the latest one, producing a truncated artifact whose byte length will never match the expected total.
  3. Do handle uri artifacts with a dedicated httpx.AsyncClient fetch on a separate code path from the inline-extend branch — when the target returns a reference URI rather than inline bytes, the SSE frame is intentionally lightweight for payloads above a few megabytes; routing the uri case through the same buffers[idx].extend path blocks the event loop and conflates reference-mode and inline-mode delivery semantics.

Don'ts

  1. Don't omit your own agent identifier from delegation_chain before re-delegating a DelegationRequest to a downstream agent — the delegation_chain list is the only protocol-level record of which agents have touched a task; forwarding without appending your identity removes loop-detection coverage and lets a misconfigured network cycle the same task_id indefinitely with no stop condition.
  2. Don't treat each SSE status event from tasks/sendSubscribe as a complete, standalone artifact — each event delivers only a slice keyed by artifact["index"]; reading only the final event and discarding earlier ones drops all preceding chunks, producing output that is shorter than a correctly assembled artifact and fails any byte-length verification you'd run after collect_artifacts returns.
  3. Don't leave timeout_seconds unbounded or ignore the ge=10, le=3600 constraints on DelegationRequest — a target agent that stalls mid-stream holds the SSE connection open until the delegator's socket times out at the OS level; without an explicit ceiling enforced by the schema, the blocking delegator starves every downstream step in the multi-step orchestration chain with no recoverable failure signal.

This lesson is free to read. Its 3 hands-on labs — real code, in a cloud IDE — are part of the GenAI Solutions Architecture subscription.

From · cancel anytime

More free lessons in GenAI Architecture & Design Patterns

All free lessons in GenAI Solutions Architecture