Agent2Agent · AI agents · Distributed systems

Build Two Agents That Collaborate over A2A 1.0

Connect a coordinator to an independently operated report agent using an Agent Card, SendMessage, task state, typed artifacts, and strict authorization.

Published
Updated
Code examples
Illustrative
Reading time
5 minutes

Two agents should use A2A when one independently operated agent delegates an outcome to another and needs a discoverable skill, stateful Task, and durable Artifact—not access to the remote agent's internal tools. This guide designs a coordinator that asks a report agent for a cited incident report using A2A protocol version 1.0.

The examples use the JSON-RPC binding at the protocol level so the boundary is visible. They are implementation sketches, not a claim that a particular SDK release was run. Consult the official Python quickstart for an SDK walkthrough, and pin the SDK version before publishing executable code.

Split ownership before writing code

Coordinator agent                      Report agent
owns the user's larger workflow        owns incident-report production
        │                                      │
        ├── discovers Agent Card ─────────────►│
        ├── SendMessage(report request) ──────►│
        │◄── Task: WORKING ────────────────────┤
        ├── GetTask / stream ─────────────────►│
        │◄── Artifact: cited report ───────────┤
        └── validates artifact                 │

The coordinator must not direct the report agent's model steps or internal tools. It sends the objective, bounded inputs, required output modes, and acceptance criteria. The report agent remains a black box across the A2A boundary, which is a core property of A2A's remote-agent model (A2A core actors).

If both components are functions in one application and have no independent lifecycle or security boundary, use a typed function or queue. If the coordinator only needs a database or ticket operation, use MCP rather than A2A.

Publish an A2A 1.0 Agent Card

The report agent serves a public card at:

https://reports.example.com/.well-known/agent-card.json

An illustrative A2A 1.0 card is:

{
  "name": "Incident Report Agent",
  "description": "Produces evidence-linked incident reports from authorized incident records.",
  "supportedInterfaces": [
    {
      "url": "https://reports.example.com/a2a/jsonrpc",
      "protocolBinding": "JSONRPC",
      "protocolVersion": "1.0"
    }
  ],
  "version": "2.1.0",
  "capabilities": {
    "streaming": true,
    "pushNotifications": false
  },
  "securitySchemes": {
    "company_oidc": {
      "openIdConnectSecurityScheme": {
        "openIdConnectUrl": "https://login.example.com/.well-known/openid-configuration"
      }
    }
  },
  "securityRequirements": [
    { "company_oidc": ["reports:create", "reports:read"] }
  ],
  "defaultInputModes": ["application/json", "text/plain"],
  "defaultOutputModes": ["application/json", "text/markdown"],
  "skills": [
    {
      "id": "produce-incident-report",
      "name": "Produce incident report",
      "description": "Creates a cited report for one authorized incident ID.",
      "tags": ["incidents", "reports", "citations"],
      "examples": ["Produce a report for incident INC-2048"],
      "inputModes": ["application/json"],
      "outputModes": ["application/json", "text/markdown"]
    }
  ]
}

The OpenID Connect URL and scopes above are illustrative. Replace them with metadata from your authorization system, validate that metadata before use, and never put static credentials in the card.

In A2A 1.0, each supportedInterfaces entry declares its own binding, URL, and protocol version. The list is ordered by preference. This replaces older card shapes that placed a single protocol version or preferred transport elsewhere (A2A 1.0 Agent Card changes).

Serve caching headers and an ETag, but revalidate on expiry. For sensitive skills or internal endpoints, advertise only a safe public subset and use an authenticated extended card. Treat card content as untrusted discovery data until its domain, policy, and optional signature are verified.

Send a typed report request

The coordinator selects a supported interface, acquires credentials for the report agent, and sends A2A 1.0 service parameters in HTTP headers. For the JSON-RPC binding:

POST /a2a/jsonrpc HTTP/1.1
Host: reports.example.com
Authorization: Bearer <audience-bound-token>
A2A-Version: 1.0
Content-Type: application/json
{
  "jsonrpc": "2.0",
  "id": "rpc_7e5a",
  "method": "SendMessage",
  "params": {
    "message": {
      "messageId": "msg_812f",
      "role": "ROLE_USER",
      "parts": [
        {
          "data": {
            "skill": "produce-incident-report",
            "incidentId": "INC-2048",
            "sections": ["summary", "timeline", "impact", "root_cause", "actions"],
            "citationPolicy": "Every factual claim must identify an evidence record."
          },
          "mediaType": "application/json"
        }
      ]
    },
    "configuration": {
      "acceptedOutputModes": ["application/json", "text/markdown"],
      "historyLength": 0
    }
  }
}

The identifiers and incident are illustrative. The request should contain the minimum context necessary. Do not forward the coordinator's full transcript, hidden instructions, or unrelated credentials.

The remote agent may return a direct Message for an immediate response or a Task for stateful work. A report that takes time should become a server-generated Task.

Persist the Task as the remote agent's state

The report agent validates the request, authorizes the incident, creates an opaque Task ID and context ID, persists state, and returns:

{
  "jsonrpc": "2.0",
  "id": "rpc_7e5a",
  "result": {
    "task": {
      "id": "task_23ab",
      "contextId": "ctx_891c",
      "status": {
        "state": "TASK_STATE_WORKING",
        "timestamp": "2026-07-18T18:30:00.000Z"
      }
    }
  }
}

A2A 1.0 defines submitted, working, completed, failed, canceled, input-required, rejected, and auth-required states, plus an unspecified value. Completed, failed, canceled, and rejected are terminal (A2A Task data model).

Store the authenticated caller and tenant with the Task. Every GetTask, ListTasks, CancelTask, subscription, and artifact read must enforce that ownership. The A2A 1.0 change notes explicitly require task visibility to be scoped to the authenticated caller.

The coordinator can poll through GetTask:

{
  "jsonrpc": "2.0",
  "id": "rpc_a190",
  "method": "GetTask",
  "params": {
    "id": "task_23ab",
    "historyLength": 0
  }
}

Or, if the card advertised streaming, use SendStreamingMessage and consume SSE StreamResponse values. The coordinator must still recover after a dropped stream by fetching Task state; transient status messages are not guaranteed durable delivery.

Return results as Artifacts, not chat status

When complete, the report agent attaches deliverables to the Task:

{
  "id": "task_23ab",
  "contextId": "ctx_891c",
  "status": {
    "state": "TASK_STATE_COMPLETED",
    "timestamp": "2026-07-18T18:31:42.000Z"
  },
  "artifacts": [
    {
      "artifactId": "artifact_report_31",
      "name": "Incident INC-2048 report",
      "parts": [
        {
          "data": {
            "incidentId": "INC-2048",
            "reportRevision": 1,
            "claims": [
              {
                "text": "The alert fired before customer impact was confirmed.",
                "evidenceIds": ["event_392", "ticket_812"]
              }
            ]
          },
          "mediaType": "application/json"
        }
      ]
    }
  ]
}

Messages communicate instructions, clarification, and status. Artifacts are the task's tangible outputs. The A2A specification says task outputs should use Artifacts rather than Messages so clients can retrieve and process them reliably (messages and artifacts).

The coordinator validates the artifact's MIME type, JSON shape, incident ID, citation references, size, and authorization before using it. A completed state does not prove the report meets local acceptance criteria.

Handle clarification without inventing a new Task

If the report agent lacks a required reporting window, it returns TASK_STATE_INPUT_REQUIRED with a Message describing the missing input. The coordinator sends another SendMessage with the same taskId; if it also supplies contextId, it must match the Task.

Keep each Message's messageId unique and make duplicate delivery safe. If a repeated message ID arrives with different content, reject it and alert rather than treating it as a fresh instruction.

Use TASK_STATE_AUTH_REQUIRED for an interrupted task that needs authentication and follow the advertised security flow. Do not put bearer tokens in Message Parts.

Secure the agent-to-agent boundary

  • Authenticate every operation using headers, separate from A2A message content.
  • Validate token issuer, audience, lifetime, scopes, principal, and tenant.
  • Treat Agent Cards, Messages, Parts, URLs, and Artifacts as untrusted input.
  • Allowlist remote endpoints and constrain redirects to prevent SSRF.
  • Bound Part counts, inline byte sizes, referenced downloads, history, and Task duration.
  • Scan referenced files and do not fetch private network URLs.
  • Use idempotency and deduplication for messages and external effects.
  • Require human approval before either agent performs a high-impact action.
  • Propagate only the minimum delegated authority; never share the coordinator's entire credential set.

Agent Card signatures in A2A 1.0 use JWS over RFC 8785-canonicalized JSON. Verification proves integrity and signer possession, not that the agent is trustworthy; bind accepted signing keys to a registry or organizational trust policy.

Interoperability test checklist

  • Fetch and schema-validate the public Agent Card.
  • Select an interface that explicitly advertises protocol 1.0.
  • Reject unsupported versions and required unknown extensions.
  • Exercise direct Message and stateful Task responses.
  • Poll, stream, reconnect, and cancel.
  • Resume an input-required Task with matching identifiers.
  • Return structured Artifacts and validate them at the coordinator.
  • Test duplicate messages, wrong-tenant Tasks, expired tokens, and malicious URLs.
  • Verify an A2A 0.3-shaped client fails clearly rather than being silently misparsed.