| name | a2a-protocol |
| description | How agents call other agents via the A2A (agent-to-agent) JSON-RPC protocol. Use when enabling inter-agent communication, exposing agent skills to other agents, or calling external agents from scripts. |
| scope | dev |
| metadata | {"internal":true} |
A2A Protocol (Agent-to-Agent)
Rule
Agents can call other agents using the A2A protocol. This is a JSON-RPC-based protocol for agent discovery and communication. Use it when work should go to a different agent entirely — not the local agent chat.
Why
Agent-native apps don't exist in isolation. A mail agent might need analytics data. A calendar agent might need to search issues. A2A lets agents discover each other, send messages, and receive structured results — all over HTTP with bearer token auth.
How to Enable A2A (Server Side)
Add mountA2A() to a server plugin:
import { mountA2A } from "@agent-native/core/a2a";
export default defineNitroPlugin((nitro) => {
const app = nitro.h3App;
mountA2A(app, {
name: "Analytics Agent",
description: "Queries analytics data across providers",
version: "1.0.0",
skills: [
{
id: "query-data",
name: "Query Data",
description: "Run analytics queries across connected data sources",
tags: ["analytics", "data"],
examples: ["What were last week's signups?", "Show conversion rates"],
},
],
apiKeyEnv: "A2A_API_KEY",
streaming: true,
});
});
This mounts the agent-native A2A endpoints:
GET /.well-known/agent-card.json — public agent discovery (no auth required)
POST /_agent-native/a2a — primary JSON-RPC endpoint (bearer token auth required)
The client may fall back to POST /a2a for external or legacy peers that only
expose that simple path. New agent-native apps should document and call the
/_agent-native/a2a endpoint.
The Config Object
interface A2AConfig {
name: string;
description: string;
version?: string;
skills: AgentSkill[];
handler?: A2AHandler;
apiKeyEnv?: string;
streaming?: boolean;
}
interface AgentSkill {
id: string;
name: string;
description: string;
tags?: string[];
examples?: string[];
}
Agent Card
The agent card is auto-generated at GET /.well-known/agent-card.json. Other agents fetch this to discover what skills are available:
{
"name": "Analytics Agent",
"description": "Queries analytics data across providers",
"url": "https://analytics.example.com",
"version": "1.0.0",
"protocolVersion": "0.3",
"capabilities": { "streaming": true },
"skills": [
{
"id": "query-data",
"name": "Query Data",
"description": "Run analytics queries across connected data sources"
}
]
}
Calling Another Agent
Simple: callAgent() (text in, text out)
import { callAgent } from "@agent-native/core/a2a";
const answer = await callAgent(
"https://analytics.example.com",
"What were last week's signups?",
{ apiKey: process.env.ANALYTICS_A2A_KEY },
);
Fast bounded read: invokeAgentAction()
When the caller knows the exact receiver-owned read action and arguments, skip
the receiver's model loop:
import { invokeAgentAction } from "@agent-native/core/a2a";
const { result } = await invokeAgentAction({
target: "analytics",
action: "gong-calls",
input: { company: "Acme", days: 90, includeTranscripts: true },
userEmail,
orgDomain,
orgSecret,
});
The receiver still owns schema validation, credentials, access scoping, audit
attribution, and exposure policy. Direct invocation is available only for
cataloged, authenticated, explicitly exposed read-only actions that do not
require approval. Its JWT is audience-bound to the receiving app. Use normal
message delegation for planning, synthesis, multi-step work, or mutations.
Inside an agent loop, call-agent exposes the same path with action + input;
omit message and taskId in that mode.
Retry safety and trace linkage
call-agent automatically derives an owner-scoped idempotency key from the
originating turn, target, and exact message. If a retry reaches the receiver
after the caller timed out, asynchronous message/send returns the existing
active or completed task instead of starting a duplicate agent run. Failed and
canceled tasks release the key for an intentional retry; synchronous calls are
never deduplicated. Lower-level clients may pass an
idempotencyKey explicitly; keep it stable for the same logical submission and
change it when the work changes. Dedupe is scoped to the JWT-authenticated
owner and verified org, and keys are limited to 128 characters.
The caller also forwards bounded correlation metadata (callerApp,
callerThreadId, parentRunId, parentTurnId, and direct-read
invocationId). These fields
are telemetry hints only. Receivers must continue to derive identity,
ownership, org scope, access, and approval from the verified request context.
Delegated model loops emit $ai_generation with A2A/MCP lineage, while direct
reads emit the content-free $a2a_read_invoke event; neither event includes
action arguments or results.
Advanced: A2AClient (full control)
import { A2AClient } from "@agent-native/core/a2a";
const client = new A2AClient(
"https://analytics.example.com",
process.env.ANALYTICS_A2A_KEY,
);
const card = await client.getAgentCard();
const task = await client.send({
role: "user",
parts: [{ type: "text", text: "What were last week's signups?" }],
});
for await (const update of client.stream({
role: "user",
parts: [{ type: "text", text: "Detailed breakdown by day" }],
})) {
console.log(update.status.state, update.status.message);
}
Carrying explicit chat authorization
When the authenticated caller has an exact consequential action that the user
explicitly authorized in the originating chat, pass the tool name and complete
input as approvedActions. The receiver accepts these grants only from a
JWT-verified user identity, converts each one to the same content-addressed key
as its local approval gate, and consumes it once:
await client.send(message, {
async: true,
approvedActions: [
{
tool: "send-email",
input: { to, subject, body, attachments },
},
],
});
Never infer authorization from request prose or broaden the input. A changed
recipient, body, attachment, or tool produces a different key and follows the
receiver's normal approval-required path. Static API keys and unsigned callers
cannot carry these grants.
JSON-RPC Methods
| Method | Purpose | Auth required |
|---|
message/send | Send a message, get a task back | Yes |
message/stream | Send a message, stream responses | Yes |
actions/invoke | Invoke one exposed read action | Yes, JWT |
tasks/get | Get task status by ID | Yes |
tasks/cancel | Cancel a running task | Yes |
Task Lifecycle
Tasks go through these states:
submitted → working → completed
→ failed
→ canceled
→ input-required
- submitted — message received, not yet processing
- working — agent is processing the request
- completed — agent finished, result in
status.message
- failed — agent encountered an error
- canceled — task was canceled via
tasks/cancel
- input-required — agent needs more information from the caller
Security
A2A uses bearer token auth. The server reads the token from the environment variable specified by apiKeyEnv:
- Set
A2A_API_KEY=<A2A_API_KEY_VALUE> in the server's deployment environment
- Callers pass it as
Authorization: Bearer <A2A_API_KEY_VALUE>
- The agent card endpoint (
/.well-known/agent-card.json) is public — no auth needed for discovery
Never hardcode the bearer token in source, docs, prompts, app state, action
descriptions, client bundles, or examples. A2A tokens are deploy-level secrets
unless a specific app designs a scoped credential flow; read them from secure
runtime configuration and never log or return them.
Message Parts
Messages contain typed parts:
| Part type | Fields | Use for |
|---|
text | { type: "text", text: "..." } | Natural language messages |
file | { type: "file", file: { ... } } | Files (bytes or URI) |
data | { type: "data", data: { ... } } | Structured JSON data |
Example: Cross-Agent Workflow
A mail agent calls an analytics agent to include data in an email draft:
import { callAgent } from "@agent-native/core/a2a";
import { writeAppState } from "@agent-native/core/application-state";
export default async function (args: string[]) {
const stats = await callAgent(
process.env.ANALYTICS_AGENT_URL!,
"Summarize last week's key metrics in 3 bullet points",
{ apiKey: process.env.ANALYTICS_A2A_KEY },
);
await writeAppState("compose-analytics-report", {
id: "analytics-report",
to: "team@example.com",
subject: "Weekly Analytics Summary",
body: `Hi team,\n\nHere are last week's numbers:\n\n${stats}\n\nBest`,
mode: "compose",
});
}
All Types
All types are exported from @agent-native/core/a2a:
import type {
A2AConfig,
A2AHandler,
A2AHandlerContext,
A2AHandlerResult,
AgentCard,
AgentSkill,
AgentCapabilities,
Task,
TaskState,
TaskStatus,
Message,
Part,
TextPart,
FilePart,
DataPart,
Artifact,
JsonRpcRequest,
JsonRpcResponse,
} from "@agent-native/core/a2a";
Related Skills
- delegate-to-agent — For work the local agent handles. Use A2A when the work goes to a different agent.
- actions — A2A calls typically happen inside actions
- storing-data — Results from A2A calls are stored in SQL like any other data