Agent-To-Agent (A2A) Integration
The Inference Gateway supports Agent-To-Agent (A2A) integration, enabling Large Language Models (LLMs) to seamlessly coordinate with external specialized agents. This powerful feature allows LLMs to access and utilize a wide range of external tools and services through standardized agent interfaces.
What is Agent-To-Agent (A2A)?
Agent-To-Agent (A2A) is a protocol that enables LLMs to discover, communicate with, and coordinate multiple specialized agents simultaneously. Each agent can provide specific capabilities (called "skills") that the LLM can automatically discover and use to fulfill user requests.
Key Features
- Automatic Agent Discovery: The LLM automatically discovers available agents and their capabilities
- Multi-Agent Coordination: Coordinate multiple agents in a single conversation
- Specialized Skills: Each agent provides specialized tools for specific domains
- Distributed Architecture: Agents can run as separate services and scale independently
- Natural Language Integration: Users interact naturally while the LLM handles agent coordination
- Protocol Standardization: Based on standardized A2A protocol for interoperability
How A2A Works
When a user makes a request:
- Request Analysis: The LLM analyzes the user's request
- Agent Discovery: Available agents and their skills are discovered
- Task Decomposition: The request is broken down into tasks for specific agents
- Agent Coordination: Multiple agents are called simultaneously or sequentially
- Result Integration: Results from all agents are integrated into a coherent response
This coordination lives in the CLI, not in the gateway's /v1/chat/completions handler - the gateway injects no A2A tools into chat completions, only MCP tools.
The Gateway as an A2A Server
Separately from the CLI delegating to agents, the gateway can be an A2A server itself. With A2A_ENABLED=true it serves one merged agent card at GET /.well-known/agent-card.json and relays every JSON-RPC call on POST /a2a to the agent named in the request, resolved by alias from A2A_AGENTS. Any A2A client then talks to a single endpoint instead of to each agent directly, and the gateway's auth, telemetry, and guardrails apply to the relayed calls.
Requires gateway v0.58.0 or newer. See the A2A settings for the environment variables, the API reference for the routes and methods, and A2A Agents (spec.a2a) for configuring it declaratively on Kubernetes.
Using A2A with the Inference Gateway CLI
The best way to use A2A is through the Inference Gateway CLI. The CLI provides seamless integration with A2A agents, allowing you to delegate specialized tasks to external agents directly from your chat sessions.
Getting Started
First, install and initialize the CLI:
# Install the CLI
curl -fsSL https://raw.githubusercontent.com/inference-gateway/cli/main/install.sh | bash
# Seed the userspace baseline in ~/.infer/
infer init
# Start interactive chat
infer chatAdding A2A Agents
The CLI provides dedicated commands for managing A2A agents through the infer agents command.
Browse available agents in the A2A Registry, hosted at registry.inference-gateway.com.
# Initialize agents configuration
infer agents init
# Add a remote A2A agent
infer agents add <agent-name> <agent-url>
# Add a local agent with Docker support
infer agents add <agent-name> <agent-url> \
--oci <oci-image> \
--run
# Add any catalog agent by name only (URL and image derived from its catalog entry)
infer agents add grafana-agent
# Add a built-in agent by name only (browser-agent ships one tag per browser engine)
infer agents add browser-agent --tag lightpanda
# Pin a built-in agent to a released version
infer agents add browser-agent --tag chromium-0.8.0
# Add an agent with environment variables - comma-separated, or the flag repeated
infer agents add <agent-name> <agent-url> \
--environment KEY=value,OTHER_KEY=other-value
infer agents add <agent-name> <agent-url> \
--environment KEY=value \
--environment OTHER_KEY=other-value
# List configured agents
infer agents list
# Show details for a specific agent
infer agents show <agent-name>
# Remove an agent
infer agents remove <agent-name>--environment on agents add and agents update takes KEY=VALUE,KEY=VALUE on a single flag, and the repeated-flag form still works - the two can be mixed. An empty --environment "" is rejected.
Built-in agents vs other catalog agents
The bare-name form (infer agents add <agent-name>, with no URL) resolves any agent published in the A2A Registry catalog, not just the ones the CLI ships defaults for. Resolution happens in two steps:
- Built-in defaults win. Five agents have defaults compiled into the CLI -
browser-agent,mock-agent,google-calendar-agent,documentation-agent, andn8n-agent. If the name matches one of these, those defaults are used and no catalog lookup is needed. - Otherwise the catalog is consulted. The CLI fetches the published catalog and looks the name up there.
# Built-in agent, resolved from compiled-in defaults
infer agents add browser-agent --tag lightpanda
# Any other catalog agent, resolved by name from the catalog
infer agents add grafana-agentFor a catalog agent, the entry is derived from its published metadata the same way the registry site renders its "Add to CLI" command:
- URL comes from the entry's
spec.server- scheme and port, registered as<scheme>://localhost:<port>with the next free port at or above the declared one. - OCI image comes from the entry's
spec.deployment. When no image is declared there, the CLI falls back toghcr.io/inference-gateway/<agent-name>:<version>- but only for agents sourced from thegithub.com/inference-gatewayorg; entries from anywhere else without a declared image get none. --runis enabled only when an image can be derived. A catalog entry with no derivable image is registered as a remote agent, and you start it yourself.
Anything you pass explicitly still wins over the derived values, so you can point a catalog agent at a different URL, swap in a different image with --oci, or add environment variables:
infer agents add grafana-agent http://localhost:8080 \
--oci ghcr.io/inference-gateway/grafana-agent:0.3.9 \
--runOffline and unknown names: catalog resolution needs network access. If the catalog is unreachable, the five built-in agents keep working, because their defaults never leave the binary. A name that is in neither the built-in defaults nor the catalog still requires an explicit URL argument - the CLI cannot invent one.
Agents can be configured at two levels:
- Project-level:
.infer/agents.yaml- Agents specific to the current project - Userspace:
~/.infer/agents.yaml- Global agents available across all projects (use--userspaceflag)
Authenticating to A2A Agents
An agent that protects its /a2a endpoint needs credentials. Give its entry in agents.yaml an auth block and the CLI sends an Authorization: Bearer header on every request to that agent. There are no infer agents add flags for credentials, so edit agents.yaml directly.
The auth block never holds a secret. It names the environment variable, file or command the secret is read from, which keeps agents.yaml safe to commit - infer agents show prints the mode and that name, never the value. A file-backed agent shows bearer token from file <path> and a command-backed one shows bearer token from command <argv[0]>, with the command's arguments omitted.
Set exactly one of token_env, token_file, token_command and oidc per agent.
Static bearer token
# .infer/agents.yaml
agents:
- name: research
url: https://research.example.com
auth:
token_env: RESEARCH_AGENT_TOKENexport RESEARCH_AGENT_TOKEN=...auth.token_env(required for this mode): name of the environment variable holding the static bearer token.
Write the variable name, not ${RESEARCH_AGENT_TOKEN} - substitution would put the secret itself into the field.
Bearer token from a file
For a token a platform writes and rotates on disk: the projected service account token of a Kubernetes pod, a JWT-SVID written by spiffe-helper, or a token rendered by Vault Agent.
# .infer/agents.yaml
agents:
- name: research
url: https://research.example.com
auth:
token_file: /var/run/secrets/kubernetes.io/serviceaccount/tokenauth.token_file(required for this mode): path of a file holding the bearer token.
The file is re-read on every request, so a token rotated in place is picked up without restarting the CLI. Nothing is provisioned to the CLI itself - the platform attests the workload and hands it the token. Pair it with an agent whose issuer is the platform's own, for example the cluster's OIDC discovery endpoint or the SPIRE OIDC Discovery Provider.
Bearer token from a command
For a token minted by a cloud CLI or a secrets broker. The command's trimmed stdout is the bearer token.
# ~/.infer/agents.yaml
agents:
- name: billing
url: https://billing.example.com
auth:
token_command: [gcloud, auth, print-identity-token, --audiences=https://billing.example.com]auth.token_command(required for this mode): argv of the command to run. It is a list, not a shell line.
The command runs without a shell under a 10 second timeout, and its token is reused until it expires: the exp claim when the token is a JWT, otherwise five minutes. A failing command is reported by its name and exit status, never by its output.
| Provider | Command |
|---|---|
| Google Cloud | gcloud auth print-identity-token --audiences=<agent audience> (add --impersonate-service-account off GCP) |
| Microsoft Entra ID | az account get-access-token --resource api://<api app id> --query accessToken -o tsv |
| HashiCorp Vault | vault read -field=token identity/oidc/token/<role> |
token_command is honoured from ~/.infer/agents.yaml only. A project .infer/agents.yaml arrives with a clone and the agent card is fetched without approval, so a command there is refused:
Authentication failed for A2A agent "billing": token_command is only read from ~/.infer/agents.yamlUse token_file in a project file instead.
OIDC client credentials
# .infer/agents.yaml
agents:
- name: billing
url: https://billing.example.com
auth:
oidc:
client_id: infer
client_secret_env: BILLING_AGENT_CLIENT_SECRET
audience: billing-agent # optional
issuer_url: https://idp.example.com/realms/agents # optional
scopes: [api://billing-agent/.default] # optionalauth.oidc.client_id(required for this mode): client of the client-credentials grant.auth.oidc.client_secret_env(required for this mode): name of the environment variable holding that client's secret.auth.oidc.audience(optional): sent as theaudienceparameter of the token request.auth.oidc.issuer_url(optional): pins the issuer and acts as a fallback, see below.auth.oidc.scopes(optional): scopes requested on top of the ones the agent card lists. Microsoft Entra ID needsapi://<api app id>/.defaulthere, because it requires that scope on client credentials and the ADK card declares none.
The entry holds only the client. You do not configure where the token comes from: as the A2A protocol defines, the agent declares it in the securitySchemes of its agent card and the CLI reads it from there.
- An
openIdConnectscheme gives the OpenID Connect discovery URL, whose document names the token endpoint. - An
oauth2scheme with aclientCredentialsflow gives the token URL directly.
The scopes the card's securityRequirements list for that scheme are requested with the token, plus any in scopes. A token is fetched once per process, reused across requests, and refreshed shortly before it expires.
issuer_url does two things:
- Pin. The client secret goes to the token endpoint the card names. With
issuer_urlset, the CLI refuses a card pointing anywhere outside that issuer before the secret is sent, so a tampered card cannot collect it. - Fallback. An agent whose card declares no security scheme cannot say where its tokens come from. The CLI then discovers the token endpoint from
issuer_url. Without it, such an agent fails with an authentication error.
The a2a-auth example runs both modes against mock-agent, one agent behind a bearer token and one behind OIDC with Keycloak, with a mock model and no API key. The a2a-auth-gcp, a2a-auth-entraid and a2a-auth-aws examples put an agent behind Google, Microsoft Entra ID and Amazon Cognito and authenticate with each provider's own tokens.
See A2A Integration for which requests carry the credentials and the error a rejected request produces.
Extra headers
An agent that needs something other than an auth block - a plain shared secret, a trace header, a tenant id - gets a headers map instead. Every key is sent on every request to that agent, and the values are expanded from the environment when agents.yaml is loaded, with both $VAR and ${VAR} accepted:
# .infer/agents.yaml
agents:
- name: peer
url: http://127.0.0.1:8090
headers:
Authorization: Bearer ${PEER_TOKEN}
X-Custom-Trace: trace-plainA variable that is not set expands to the empty string, so the header is sent empty rather than failing. An auth block also sets Authorization, so with both configured the auth credential wins.
This is how one infer authenticates to another: point the entry at the peer's daemon-hosted A2A server and put its a2a.server.token in PEER_TOKEN.
Delegating Tasks to A2A Agents
Within the CLI, you can delegate specialized tasks to A2A agents. The CLI handles agent discovery, coordination, and result integration automatically.
infer chat
> "Schedule a team meeting for tomorrow at 2 PM using the calendar agent"
> "Check my calendar for conflicts this week"For autonomous task execution with A2A agents:
infer headless "Analyze my calendar and suggest optimal meeting times for the team"A2A Tools
The CLI provides four A2A tools that LLMs can use to interact with agents:
- A2A_QueryAgent: Query agent capabilities and metadata for discovery
- A2A_SubmitTask: Submit tasks to specialized agents for processing
- A2A_QueryTask: Check the status and results of submitted tasks
A2A_QueryAgent returns the agent card in its A2A v1.0.1 shape, so the endpoint comes from supportedInterfaces rather than a flat url field. The result message names the preferred endpoint - the URL of the first supportedInterfaces entry, falling back to the configured agent URL when the card declares none.
Protocol Version and Task States
The CLI's A2A client speaks A2A v1.0.1, matching agents built on ADK v0.30.0 or later. Task states in A2A_SubmitTask and A2A_QueryTask results therefore use the v1.0.1 spellings, so a canceled task reports TASK_STATE_CANCELED.
The pre-v1.0.1 spelling TASK_STATE_CANCELLED is still accepted on read, along with bare forms such as canceled and cancelled. Agents on either release render as the same terminal Canceled status in the /tasks view and in tool results - no agent upgrade is required.
Viewing Connected Agents
During a chat session, the /agents shortcut opens the Agents view - one list of every agent the chat can delegate to, where each row carries an a2a chip for remote agents or a local chip for Markdown subagent presets:
infer chat
> /agentsThe a2a rows show each configured agent's URL and connection state. Filter the list with / and type a2a to narrow it to remote agents.
A2A Liveness Probes
When A2A agents are configured, the CLI periodically re-probes them for the lifetime of the session instead of checking them only once at startup. This keeps the A2A: X/Y status bar indicator and the a2a rows of the /agents view live throughout the session:
- An agent that was down at startup turns green automatically when it becomes reachable.
- An agent that goes down mid-session turns the
A2A: X/Yindicator red and shows the failure detail in its row. - A recovered agent counts back up in the indicator and shows a "Recovered" status.
How probes work:
- External agents (URLs in
a2a.agents) are probed via their agent card (/.well-known/agent-card.json). - Local Docker agents are probed via
GET <url>/health. - Status updates are emitted only on state changes (no noisy per-probe output).
- Probes shut down cleanly when the session ends.
Configuration:
# .infer/config.yaml
a2a:
enabled: true
agents:
- http://localhost:8081
liveness_probe_enabled: true # default: true
liveness_probe_interval: 30 # seconds, default: 30| Setting | Default | Environment variable | Description |
|---|---|---|---|
liveness_probe_enabled | true | INFER_A2A_LIVENESS_PROBE_ENABLED | Enable recurring liveness probes. Set false for one-shot checks |
liveness_probe_interval | 30 | INFER_A2A_LIVENESS_PROBE_INTERVAL | Interval in seconds between probes |
Disabling probes: Set liveness_probe_enabled: false to restore the old one-shot startup check. Agents are checked once when the session starts and never re-probed.
Tuning the interval: Set liveness_probe_interval to a higher value (for example 60 for once per minute) to reduce network traffic, or lower it (for example 10) for faster failure detection on critical agents.
Why Use the CLI for A2A?
The Inference Gateway CLI acts as an A2A agent client, providing a seamless interface for interacting with A2A-compatible agents. The CLI provides several advantages for A2A integration:
- A2A Client Implementation: The CLI implements the A2A client protocol, handling all communication with agents
- Dedicated Agent Management: Add, list, show, and remove agents with simple commands
- Automatic Discovery: Agents are discovered and coordinated automatically
- Safety Controls: Built-in approval workflows for sensitive operations
- Interactive Experience: Rich TUI with real-time feedback on agent interactions
- Flexible Modes: Switch between Standard, Plan, and Auto-Accept modes for different workflows
- Docker Support: Run agents locally with OCI image support
Learn more about the CLI in our CLI Documentation.
Local Subagents vs. A2A Agents
A2A targets external, independently deployed agent servers discovered over the network - long-running, specialized services (calendar, documentation, and so on) that you add with infer agents add and reach through A2A_SubmitTask / A2A_QueryTask / A2A_QueryAgent.
For short-lived, local helpers there is a lighter-weight option: the CLI's built-in Agent tool. It spawns one or more local subagents - each just an infer headless subprocess with its own isolated session - that run in parallel and fold their results back into the main conversation, with no agent server to stand up.
| Use the Agent tool (local subagents) | Use the A2A tools (external agents) |
|---|---|
| Parallel fan-out for the task at hand (exploration, scoped edits) | Delegating to a specialized, independently deployed service |
| Cheap, isolated, session-persisted local subprocesses | Long-running, network-discovered, independently scaled server |
| No server to run | Requires a running A2A server (local OCI or remote URL) |
Local subagents can run headless (in the background) or interactive (each subagent in a live tmux pane you can watch). Interactive mode uses tmux, an optional runtime dependency: it is required only for interactive mode and must be run from inside tmux, while headless mode needs nothing extra. See Local Subagents (Agent tool) for the tool parameters, result modes, and the tools.agent.* configuration.
Available Agents
Google Calendar Agent
The Google Calendar Agent provides comprehensive calendar management and scheduling capabilities.
Skill:
The agent advertises a single high-level skill that the LLM discovers and delegates to:
schedule-meeting: Schedule a meeting, book a slot, or find a time that works. Resolves a conflict-free booking by finding open slots, validating that nothing overlaps, and creating the event.
Tools:
The schedule-meeting skill is backed by a set of calendar tools the agent calls internally:
list_calendar_events: List upcoming events for a time range, with optional free-text searchcreate_calendar_event: Create a new event, including attendees and locationupdate_calendar_event: Update an existing event by IDdelete_calendar_event: Delete an event by IDget_calendar_event: Retrieve the details of a specific event by IDfind_available_time: Find open time slots of a given duration within a date rangecheck_conflicts: Check for scheduling conflicts in a time rangeget_current_datetime: Return the current date/time and the user's IANA timezone, called first for any time-relative request so events land in the correct timezone
Features:
- Timezone-aware scheduling: anchors the current time and the user's IANA timezone before emitting RFC3339 timestamps
- Conflict detection and availability checking
- Attendee and location handling on event creation and updates
- Google Calendar API integration
Configuration:
These options are set as environment variables, derived from the agent's googleCalendar config:
mockMode/GOOGLE_CALENDAR_MOCK_MODE(defaultfalse): serve in-memory mock data instead of calling the Google Calendar API, useful for demos and testing without credentialstimezone/GOOGLE_CALENDAR_TIMEZONE(defaultUTC): default IANA timezone applied when a request does not specify one
Repository: github.com/inference-gateway/google-calendar-agent
Documentation Agent
The Documentation Agent provides Context7-style documentation retrieval, resolving a library name to a canonical ID and fetching version-scoped documentation so other agents can ground their code generation in up-to-date library docs.
Skill:
library-documentation-lookup: Fetch up-to-date documentation for a third-party library or framework before writing code against it. Resolves the library name to a Context7-compatible ID and retrieves focused, topic-scoped documentation.
Tools:
resolve_library_id: Resolve an official library name into a ranked list of Context7-compatible library IDsget_library_docs: Fetch up-to-date, topic-scoped documentation for a Context7-compatible library IDread: Read a file from disk (used to load skill bodies on demand)
Repository: github.com/inference-gateway/documentation-agent
For a full walkthrough of the Documentation Agent, its skill, tools, and configuration, see the Dedicated Documentation Agent page.
Creating Custom Agents
Tip: Use the ADL CLI to scaffold A2A agents from YAML definitions. It generates complete project structures with service injection, CI/CD pipelines, and deployment configurations.
Writing the server by hand in TypeScript? See the TypeScript ADK for the
@inference-gateway/adkpackage, which ships the HTTP server core and JSON-RPC handlers (starting withSendMessage).
To create your own A2A-compatible agent, implement these endpoints:
Required Endpoints
/.well-known/agent-card.json- Agent card: capabilities and metadata/a2a- Main A2A protocol endpoint/health- Health check endpoint
Agent Card
Your agent must publish an A2A v1.0.1 AgentCard at /.well-known/agent-card.json. The gateway reads it to discover what the agent does, where to reach it, and how to authenticate:
{
"name": "my-agent",
"version": "1.0.0",
"description": "What this agent does",
"supportedInterfaces": [
{
"url": "https://my-agent.example.com/a2a",
"protocolBinding": "JSONRPC",
"protocolVersion": "1.0"
}
],
"defaultInputModes": ["text"],
"defaultOutputModes": ["text"],
"capabilities": {
"streaming": true,
"pushNotifications": false
},
"skills": [
{
"id": "your-skill-id",
"name": "Your Skill Name",
"description": "Description of what your skill does",
"tags": []
}
]
}Card fields worth knowing:
supportedInterfacesis an ordered list of the protocol endpoints the agent serves, one entry per binding (JSONRPC,GRPC,HTTP+JSON, or another value). At least one entry is required and the first is the preferred one - this replaces the single top-levelurl/preferredTransport/protocolVersiontriple used before v1.0.1.capabilities.extendedAgentCard: trueadvertises that an authenticated caller can fetch a richer card viaGetExtendedAgentCard. Leave it out when the agent serves only the public card. The v1.0.1 card has nostateTransitionHistoryflag.securitySchemesnames the schemes the agent accepts, andsecurityRequirementslists which of them a caller must satisfy (several keys in one entry are ANDed, separate entries are ORed). Keep/.well-known/agent-card.jsonitself unauthenticated so clients can negotiate auth from the advertised schemes - see Authentication.
A2A Protocol Implementation
Implement the A2A protocol at the /a2a endpoint to handle:
SendMessage- Send a message and receive response. Per A2A spec section 3.2.2 the call blocks until the task settles in a terminal or interrupted state; the Go and Rust ADK servers both do this, and a client that wants the old immediate reply passesconfiguration.returnImmediately: trueSendStreamingMessage- Send a streaming messageGetTask- Get task status (optional)CancelTask- Cancel a running task (optional)
The v1.0.1 method names are bare PascalCase identifiers. The v0.x slash names (message/send, tasks/get, ...) are gone - an agent that still registers them answers -32601 Method not found to a v1.0.1 caller.
Inside params, field names follow the normative proto3 JSON mapping of A2A spec section 1.4: a server accepts both the lowerCamelCase form (pageSize, contextId) and the proto3 form (page_size, context_id), and ignores params it does not recognize rather than rejecting the request with -32602 Invalid params. Rust ADK servers behave this way as of the current release; the other ADKs are converging on it, so do not rely on a non-Rust agent tolerating snake_case or extra fields yet.
The same mapping governs values: an optional field carrying its proto3 default is equivalent to omitting it, so a ListTasks call with pageToken: "" asks for the first page, pageSize: 0 asks for the server's default page size, and status: "" / contextId: "" apply no filter. The Go and TypeScript ADK servers both accept these, which is what lets a client serialize every field unconditionally on its first call.
Usage Extension
The A2A specification has no field for token usage. Agents built with the ADKs report it through the usage extension, an optional extension the agent declares in its card:
{
"capabilities": {
"extensions": [
{
"uri": "https://github.com/inference-gateway/schemas/tree/main/a2a/extensions/usage/v1",
"required": false
}
]
}
}Like every A2A extension, it is inactive by default. A client activates it per request by listing the URI in the A2A-Extensions header, and the agent echoes the URI in the response header. Only then does a returned task carry the extension's two metadata keys, namespaced by the URI:
<URI>/usage-prompt_tokens,completion_tokensandtotal_tokens, summed over every LLM call of the task. It is absent when no call reported usage, which a client must treat as unknown rather than zero.<URI>/execution_stats-iterations,messages,tool_callsandfailed_tools.
The CLI activates the extension on every A2A request, so a finished A2A task shows the tokens the agent spent in the subagent list and the task view. A task from an agent that does not serve the extension shows its tool counts without a token figure. The Go, Rust and TypeScript ADKs serve it.
Best Practices
Agent Design
- Single Responsibility: Each agent should focus on a specific domain
- Stateless Operations: Design agents to be stateless for better scalability
- Error Handling: Implement robust error handling and fallback mechanisms
- Documentation: Clearly document agent capabilities and expected inputs
Security
- Authentication: Implement proper authentication between gateway and agents
- Input Validation: Validate all inputs to prevent injection attacks
- Rate Limiting: Implement rate limiting to prevent abuse
- Network Security: Use secure communication channels (HTTPS/TLS)
Performance
- Caching: Implement caching for frequently accessed data
- Timeouts: Set appropriate timeouts for agent communications
- Load Balancing: Use load balancing for high-availability deployments
- Monitoring: Monitor agent performance and availability
Related Resources
- A2A Debugger - Inspect, stream, and replay tasks on any A2A server from the CLI
- A2A Registry - Discover available A2A agents and their capabilities (registry.inference-gateway.com)
- Documentation Agent - A dedicated A2A agent for Context7-style documentation retrieval
- ADL CLI - Generate A2A agents from YAML definitions
- TypeScript ADK - Build A2A agents in TypeScript with
@inference-gateway/adk - Inference Gateway CLI - Manage and chat with A2A agents
- Awesome A2A
- A2A Protocol Specification
