ZAgents Docs

A2A Protocol

Expose Multica agents to external A2A v1.0 (Agent2Agent) clients. Built on the official a2a-python SDK; one Bearer token per workspace; opt-in per agent.

A2A (Agent2Agent) Protocol v1.0 is an emerging standard for agent-to-agent communication: a JSON-RPC + REST + SSE surface that lets one agent system call another's agents using a uniform Bearer-authenticated API. Multica ships a sidecar that publishes any of your agents as A2A-addressable endpoints — no extra coding required.

Why we adopted A2A

Before A2AAfter A2A
Multica agents were reachable only via the web/desktop UI or multica CLI.Any A2A-aware client (LangChain, CrewAI, custom orchestrators, the official a2a-inspector) can talk to your agents.
Integrations meant writing one-off HTTP wrappers per consumer.A single standard protocol; an Agent Card declares everything a consumer needs to know.
No machine-to-machine auth — only user JWTs and PATs scoped to a person.Workspace-scoped Bearer tokens (mca_…) — independent of any user's session, revocable in the UI.
External traffic would have polluted the issue list.A2A traffic uses chat-style tasks (source='a2a', no issue) — cleanly separated from team workflows.
No per-source quotas; a runaway external client could starve your team.Per-token rate limit + per-workspace in-flight cap (50 concurrent A2A tasks).

Version compatibility — v1.0 and v0.3 in parallel

The gateway accepts both v1.0 and v0.3 on the same endpoints. v1.0 callers get v1.0-shaped responses; v0.3 callers get v0.3-shaped responses. The SDK negotiates per-request based on:

  • A2A-Version header (1.0 or 0.3)
  • Method name style: SendMessage (v1.0) vs message/send (v0.3)

Mismatched combinations (e.g. A2A-Version: 1.0 + method: "message/send") are rejected as self-contradicting. Either send both v1.0 or both v0.3.

Dual support exists because early A2A SDKs and browser clients (the JS ecosystem in particular) still ship the v0.3 method-name + kind-discriminator + lowercase-enum format. Cutting them off would break every existing browser integration on day one. For new code, prefer v1.0.

What's supported

A2A v1.0 covers three transports — Multica's sidecar serves the first two natively (gRPC requires the SDK's [grpc] extra and is not enabled by default):

TransportStatus
JSON-RPC 2.0✅
HTTP+JSON / REST✅
gRPC⏸️ off by default

All three communication patterns from the spec work:

  • Sync request/response — SendMessage blocks (default) or returns immediately (returnImmediately: true).
  • Streaming — SendStreamingMessage and SubscribeToTask deliver TaskStatusUpdateEvent / TaskArtifactUpdateEvent over SSE.
  • Push notifications — CreateTaskPushNotificationConfig / Get / List / Delete. The sidecar POSTs StreamResponse payloads to your webhook URL on every task update.

Other v1.0 specifics implemented:

  • TaskState uses TASK_STATE_* enum values (no lowercase legacy).
  • Part is unified — discriminate by member presence (text / url / raw / data), no kind field.
  • AgentCard uses supportedInterfaces[] with tenant per interface (set to your workspace id).
  • Errors use google.rpc.Status shape with domain: "a2a-protocol.org".
  • Multi-tenancy — tenant field flows through the request envelope and the card.
  • HTTP+JSON drops the /v1 prefix per spec (e.g. POST /message:send, not POST /v1/message:send).

What's intentionally not in v1:

  • Agent Card signing (JWS over JCS) — optional in the spec; we'll add it when federation/marketplace use-cases demand it.
  • gRPC — pluggable, ship later if asked for.
  • OAuth 2.0 device-code flow — Bearer suffices for our deployment.
  • Cross-process streaming bridge — the sidecar uses the SDK's in-memory task store, so SSE/push works within one sidecar process. A future enhancement will bridge Multica's realtime hub for multi-instance setups.

What flows from your agent into the published card

Every field you set on an agent in the LK has a defined home on the A2A AgentCard — or is intentionally omitted. The mapping is enforced by tests; if it ever silently drifts, CI catches it.

Multica LK fieldA2A AgentCard locationNotes
Agent namenameRequired, copied 1:1.
Agent descriptiondescriptionTruncated to 500 chars per spec. Falls back to instructions when blank.
Agent instructionsdescription (fallback only)Used as the description source when description is empty. Otherwise NOT exposed — it's an internal system prompt.
Avatar URLiconUrlPublic URL passes straight through.
Workspace nameprovider.organizationTells external clients who runs this agent.
A2A toggleis_a2a_exposed (gate)Required true for the card to be served at all. 404 otherwise.
Skills (id, name, description)skills[]Every attached skill gets one AgentSkill entry with id/name/description and the default inputModes/outputModes/tags: [].
Workspace IDsupportedInterfaces[].tenantA2A multi-tenancy field; routes external clients correctly.

Deliberately NOT exposed (would leak internal data or has no spec home):

  • instructions (when description is set) — internal system prompt.
  • model, runtime_id, runtime_mode — backend orchestration detail.
  • custom_env, custom_args, mcp_config — agent-process secrets and CLI flags.
  • max_concurrent_tasks — internal throttle, already enforced server-side.
  • visibility — Multica's internal access control. A2A exposure is a separate flag.
  • Skill content, config — the script body itself stays internal; only the public name+description are advertised.

To preview exactly what an external A2A client will see, open Settings → A2A → Agent Card preview and pick the agent.

Quick start

1. Expose an agent

Open the agent (Workspace → Agents → click an agent), find the property row labeled A2A in the right inspector, and toggle it to Exposed. The agent is now publishable; nothing leaks until step 3.

Local-runtime agents are reachable too, but only when their daemon is online. For external clients you usually want a cloud runtime so the agent is always available.

2. Mint a workspace token

Settings → A2A → Mint token. Give it a memorable name (inspector-test, langchain-prod, …) and click Mint. A modal shows the raw secret (mca_…) once — copy it now, you won't see it again. The hash is what we store; the prefix (mca_xxxxxxxx…) stays in the table for identification.

CLI alternative:

zagents a2a token create <workspace-id> --name <label>
zagents a2a token list   <workspace-id>
zagents a2a token revoke <token-id> --workspace <workspace-id>

3. Point a client at your agent

The base URL for any external client is:

https://<your-host>/agents/<agent-id>

with Authorization: Bearer <mca_…> and A2A-Version: 1.0 headers. Self-hosted Mac mini deployment uses https://a2a.magomadov.dev as the public host.

a2a-inspector

The official inspector gives you a chat UI plus compliance checks. Run it locally and point at the sidecar:

git clone https://github.com/a2aproject/a2a-inspector
cd a2a-inspector && bash scripts/run.sh
# Open http://127.0.0.1:5001
# Base URL: https://a2a.magomadov.dev/agents/<agent-id>
# Headers: Authorization: Bearer mca_…, A2A-Version: 1.0

Programmatic — Python SDK

from a2a.client import A2AClient

client = A2AClient(
    base_url="https://a2a.magomadov.dev/agents/<agent-id>",
    headers={"Authorization": "Bearer mca_…"},
)
task = await client.send_message(text="Summarize the latest PRs.")
print(task.status.state)  # TASK_STATE_COMPLETED

Programmatic — curl

curl -s -X POST https://a2a.magomadov.dev/agents/<agent-id>/message:send \
  -H "Authorization: Bearer mca_…" \
  -H "A2A-Version: 1.0" \
  -H "Content-Type: application/json" \
  -d '{
    "message": {
      "messageId": "'$(uuidgen)'",
      "role": "ROLE_USER",
      "parts": [{"text": "ping"}]
    },
    "configuration": {"returnImmediately": true}
  }'

Preview, audit, revoke — all from the UI

Settings → A2A has three sections:

  • Tokens — list with prefix, last-used time, created date; mint button; revoke action with confirmation.
  • Agent Card preview — agent picker → formatted JSON of the card external clients see at /.well-known/agent-card.json. Useful for spot-checking your agent's published description.
  • Recent A2A activity — most recent in-flight + completed A2A tasks with agent name, status badge, timestamps. Reflects only source='a2a' traffic; UI-driven tasks don't appear.

Quotas and limits

LimitValueNotes
Per-token rate limit60 req/min, 300 burstIn-memory on the sidecar; per-token.
Per-workspace concurrent A2A tasks50Counted across all agents in the workspace.
Per-agent concurrent tasksMaxConcurrentTasks (default 6)Existing Multica throttle — applies to A2A traffic just like UI-driven traffic.
Per-(agent, contextId)strictly serialTwo A2A messages with the same contextId queue behind each other; different contextIds run in parallel.

Limit excess returns a google.rpc.Status with reason: "WORKSPACE_QUOTA_EXCEEDED" (HTTP 429) or RESOURCE_EXHAUSTED. Tasks that hit per-agent throttle wait in TASK_STATE_SUBMITTED — the client can subscribe via SSE / push or poll GetTask.

Integrating your own services

Want to call a Multica agent from LangChain, CrewAI, n8n, Claude Code, your own Python/Go service, or plain curl? See A2A Integrations — recipes for each.

Maintenance — testing every change

A2A is a published spec, not internal API — silent drift breaks every external client at once. Two non-negotiable rules:

  1. Every change that touches A2A code (sidecar, card builder, handler, executor, types, SDK upgrade) ends with a clean run of the official a2a-inspector validator returning validation_errors: NONE. The unit tests (Go + Python + Vitest) are necessary but not sufficient — only the inspector validates the full wire format against the spec's pydantic schema.
  2. Every upstream sync (sync-upstream.sh or manual git merge upstream/main) ends with the post-merge sanity script from UPSTREAM_SYNC.md ("Защита приватного A2A-слоя при merge upstream") to verify no A2A patches were dropped during the merge — and then again with the inspector compliance probe.

Deeper dive