Consuming Other Agents
A Beach application reaches a point at which it needs another agent's capabilities — a partner's booking agent, an internal billing agent, a third-party research peer. This guide is how to call them.
The agent registry
Beach maintains a local agent registry: a small JSON file listing the peer agents the application knows about, by id, with their Agent Card URLs.
agents.json (committed alongside the application's code, or generated from a database):
{
"agents": [
{
"id": "partner-booking",
"cardUrl": "https://partner-booking.example.com/.well-known/agent-card.json"
},
{
"id": "billing-service",
"cardUrl": "https://billing.example.com/.well-known/agent-card.json"
},
{
"id": "research-peer",
"cardUrl": "https://research.example.com/.well-known/agent-card.json"
}
]
}
Load the registry at startup:
import { AgentRegistry } from '@cool-ai/beach-transport';
import agentsJson from './agents.json' with { type: 'json' };
const agents = new AgentRegistry();
await agents.loadFromConfig(agentsJson);
The registry fetches each agent's card on startup, caches it, and refreshes periodically (the default refresh is hourly). If a peer's card is unreachable at startup, the registry logs a warning and continues; calls to that peer will fail at call time rather than at boot.
Calling an A2A peer
import { callAgent } from '@cool-ai/beach-transport';
const reply = await callAgent('partner-booking', {
parts: [{ text: 'Looking for a quiet boutique in Rome for next Friday' }],
});
console.log(reply.parts);
// [{ partType: 'response', text: 'Three options under £400/night...' }, ...]
What happens beneath that single call:
callAgentlooks uppartner-bookingin the registry.- It reads the cached Agent Card and finds the A2A transport endpoint.
- It POSTs JSON-RPC
message/sendto the peer with theMessage. - It awaits the JSON-RPC response.
- It returns the peer's reply as a Beach
Message.
If the peer's Agent Card declares Beach extensions (extensions.beach.envelope.consumes includes llm-context), Beach automatically translates the reply into analytical prose for the peer's LLM. If the peer is not a Beach application, the translation step is skipped and only standard A2A parts are sent.
Calling from inside an actor's tool loop
Most of the time, a peer is called as a tool the orchestrator may decide to invoke:
import { ToolRegistry } from '@cool-ai/beach-llm';
import { callAgent } from '@cool-ai/beach-transport';
const tools = new ToolRegistry();
tools.register({
name: 'ask-partner-booking',
description: 'Ask the partner booking agent for hotel recommendations.',
scope: 'router', // observable; the peer call goes through the router
inputSchema: {
type: 'object',
properties: {
brief: { type: 'string', description: 'A free-text brief of what the user wants' },
},
required: ['brief'],
},
handler: async (args) => {
const reply = await callAgent('partner-booking', {
parts: [{ text: args.brief }],
});
return reply.parts;
},
});
// Orchestrator config
const concierge = {
id: 'concierge',
model: 'claude-sonnet-4-6',
systemPrompt: '… you can ask-partner-booking for hotel recommendations …',
tools: ['ask-partner-booking'],
};
The orchestrator decides when to call the peer based on the user's intent. The peer's reply is fed back into the actor's loop as a tool result; the actor reasons about it and decides what to say to the user.
scope: 'router' means the peer call flows through routeEvent(), so filtering rules can fan the call event out to audit, archive, and the rest.
Calling an MCP server
If the peer exposes its work as MCP tools rather than as A2A:
import { MCPOutboundAdapter } from '@cool-ai/beach-transport';
const peerMcp = new MCPOutboundAdapter({
serverUrl: 'https://partner.example.com/mcp',
// Authentication header if required
headers: { authorization: `Bearer ${process.env.PARTNER_TOKEN}` },
});
await peerMcp.start();
// Direct call
const result = await peerMcp.callTool('partner.lookup-pricing', {
destinationCode: 'ROM',
dates: { from: '2026-09-12', to: '2026-09-15' },
});
To expose the MCP tool to the orchestrator, wrap it as a Beach tool:
tools.register({
name: 'partner-lookup-pricing',
description: 'Look up pricing from the partner agent.',
scope: 'specialist', // private to the orchestrator's loop
inputSchema: {
type: 'object',
properties: {
destinationCode: { type: 'string' },
dates: { type: 'object' },
},
required: ['destinationCode', 'dates'],
},
handler: async (args) => {
return await peerMcp.callTool('partner.lookup-pricing', args);
},
});
A2A and MCP — choosing per peer
Some peers expose both A2A and MCP. The choice per call is straightforward:
- A conversation, possibly multi-turn, possibly with partial results? Pick A2A.
- A single function call with typed input and output? Pick MCP.
The peer's Agent Card declares which transports it supports:
const card = await agents.fetchCard('peer-id');
console.log(card.transports);
// [{ protocol: 'a2a', endpoint: '...' }, { protocol: 'mcp', endpoint: '...' }]
A peer that advertises both invites the calling code to choose by the shape of the work. A peer that advertises only one limits the choice to that one. Beach does not require an application to pick once and stick to it; different tools registered against the same peer may use different transports.
Handling peer suspended turns
If the peer is a Beach application and its Agent Card advertises turnStates: ['suspended'], a peer call may return with turnState: 'suspended' — the peer is awaiting a human approval, or its own peer's approval, before completing.
The calling application has three options:
Wait for the peer. Hold the calling turn open ('awaiting') and either poll the peer or accept a callback when the peer's suspension resolves.
Forward the suspension. If the user of the calling application is the right person to approve, forward the peer's approval-request part to the calling channel. The user's answer routes back to the peer.
Cancel. Tell the peer the calling application cannot wait. The peer is then expected to drop the in-flight work.
Suspended-turn forwarding is a real operational concern; design it explicitly rather than hoping it does not come up.
Caching peer cards
The registry caches Agent Cards by URL, honouring the HTTP Cache-Control header the peer set. The default, when no header is present, is one hour.
For peers whose capabilities change often, shorten the cache:
const agents = new AgentRegistry({ cacheTtlMs: 5 * 60_000 });
For peers that change only when the calling application is being deployed alongside, lengthen:
const agents = new AgentRegistry({ cacheTtlMs: 24 * 60 * 60_000 });
A peer that breaks its Agent Card with a bad deploy will block the calling application until the cache expires. Tune accordingly.
What not to do
Do not hardcode peer URLs in application code. Use the agent registry. URLs change; agent ids do not.
Do not call peers from inside a deterministic handler that the orchestrator treats as a tool. The handler is supposed to be deterministic, and peer calls are not. Wrap peer calls as tools the orchestrator chooses to invoke, and let the orchestrator handle the "peer did not respond" reasoning.
Do not assume the peer will reply in the same shape every time. The peer's Agent Card declares its outputs; the peer is supposed to honour the contract, but bugs happen. Validate the reply before reading.
Do not ignore the peer's turnStates. If the peer advertises 'suspended', the calling code must handle suspension. Code that assumes every reply is 'complete' will hang in production.
Do not chain peer-of-peer-of-peer calls without timeouts. A → B → C → D, and a slow agent or a network blip somewhere in the chain stalls the whole flow. Set timeouts at every hop. The Delivery Manifest pattern is what handles this gracefully — open one with timeoutMs and accept a partial reply if the chain has not settled.
Do not treat a non-Beach peer as if it speaks Beach. Check the Agent Card's extensions.beach; if the block is absent, the call is plain A2A. Beach-specific extension methods will fail.
Related
- Your first agent card — declaring what the application offers.
- Being consumed by other applications — the other side: peers calling in.
- MCP vs A2A — choosing the right protocol per peer.
- Reference: agent-card — full Agent Card schema.
- Reference: tool-registry — wrapping peer calls as orchestrator tools.