Skip to content

Call other agents over A2A

Give an agent the skills of other agents as tools. The calling agent can serve any facade — WebSocket, REST or A2A. The agent it calls must serve an a2a facade, or be an external A2A agent with a URL.

  • A target AgentRuntime in the same namespace with a type: a2a facade whose a2a.agentCard.skills lists what it offers. Each skill becomes one tool.
  • A credential the target accepts, when the target sets spec.externalAuth. For a client key, see Configure authentication.

Put the target’s token in a Secret, under the key token:

Terminal window
kubectl create secret generic billing-agent-key \
--context my-cluster -n my-namespace \
--from-literal=token="<token minted for the caller>"

Add the target to the caller’s spec.a2aClients:

apiVersion: omnia.altairalabs.ai/v1alpha1
kind: AgentRuntime
metadata:
name: support
spec:
facades:
- type: websocket
a2aClients:
- name: billing
agentRuntimeRef:
name: billing-agent
exposeAsTools: true
authentication:
secretRef:
name: billing-agent-key
timeout: 2m
# promptPackRef, providers, ...

For an agent outside the cluster, set url to its base URL in place of agentRuntimeRef. Set timeout above the longest turn the target takes: a call that runs longer fails here while the target keeps working.

With exposeAsTools: true, the runtime fetches the target’s agent card when a conversation opens and offers each skill as a2a__<agent>__<skill>. The pack does not need to list these tools.

On an Enterprise agent, the policy broker decides each call under the registry a2a. Write a ToolPolicy with selector.registry: a2a that permits the calls; see ToolPolicy.

Update the Secret’s token. The runtime reads the token file on every call, so the next call after the kubelet syncs the Secret uses the new value — within about a minute, with no restart. Keep the old token valid on the target until then.

The first call a conversation makes to a target starts a remote conversation. Every later call from that conversation to that target sends the target’s contextId, so the target sees one session with its history.

Calls from one conversation to one target run one at a time, even when the model asks for several in one turn, so two turns never run at once on one remote conversation. Calls to other targets, and calls from other conversations, run in parallel.

The runtime keeps these ids in memory, per pod. It drops a conversation’s ids when the conversation ends, after 50 minutes without a call, and when a call fails. A runtime restart, or a conversation’s next message landing on another replica, drops them too. In every case the next call starts a new remote conversation. When the target refuses a remembered id as expired, the runtime sends the call again without it, and the model sees one successful call.

Two clients that resolve to one endpoint are refused: the second is reported not ready.

Headers a ToolPolicy injects are added to each request.

A call to an agentRuntimeRef target with exposeAsTools carries a delegation token on the X-Omnia-Delegation header, as tool calls to sandboxes and MCP servers do. The runtime asks the operator for one on every call; the operator’s client caches it for up to about five minutes. It names:

  • sub: the end user of the calling conversation;
  • act.sub: the calling agent (agent:<namespace>/<name>);
  • aud: the target’s facade, agent-facade:<namespace>/<target>;
  • origin: how the caller established the user. Only oidc means the operator verified them;
  • del: how many more agent hops the delegation allows.

The operator mints one only for a target the calling agent lists as an agentRuntimeRef client in its own namespace. A call to a url target never carries a token. When the operator has no delegation mint configured, calls carry none. When the mint refuses, the call fails.

The target verifies the token against the operator’s JWKS: the signature, its own audience, the expiry, a calling agent and a hop left. It also checks the credential the call arrived with:

  • The delegation is accepted only on a client key or the shared token, the credentials an agent calls with. On an OIDC, edge-asserted or dashboard credential it is refused.
  • A client key whose scopes set a2a_caller to an agent (agent:<namespace>/<name>, or <namespace>/<name>) accepts only tokens whose act.sub is that agent. The binding is opt-in, and recommended: set it on every key you issue to a calling agent. Without it the target can only check that the credential is an agent’s kind, not that it is that agent’s.

The operator mints a delegation token only for an agent it can identify from its ServiceAccount. Agents that share one ServiceAccount (a workspace-wide runtime ServiceAccount) cannot make delegated calls: the mint refuses them, and those calls fail.

A token that fails any check is a 401 with the body invalid delegation token; the reason is in the target’s debug log. The call is never served as the client credential instead. Without the header, the target sees only the client credential, as before.

origin: oidc (verified)Any other origin (asserted)
Session owner and identity.endUserThe userThe user, for a new conversation
identity.originoidca2a-asserted
Continue the user’s own sessions on the targetYesNo: refused as expired
Continue a conversation another calling agent opened for the userNoNo
Continue the conversation this caller openedYesYes, with a session archive
User-tier memoryYesNo

A verified user whose proof the target cannot obtain from the operator keeps the session but is presented to policy as a2a-asserted and gets no user-tier memory. A verified delegation from any agent that lists the target as a client can continue that user’s own sessions on the target, as long as those sessions are not bound to another calling agent.

The calling agent gains no authority on the target. The target’s own ToolPolicy applies to the user and the token’s wrole is not carried over. identity.origin describes the user, so to require a verified user test identity.origin == "oidc". The calling client’s credential is kept apart, so its role is never read as the user’s. The target records the calling agent on the session’s state (a2a.delegation.actor, a2a.delegation.origin) and passes these claims to its ToolPolicy:

ClaimValue
identity.claims.a2a_delegation_actorThe calling agent
identity.claims.a2a_delegation_originThe user’s origin at the caller
identity.claims.a2a_delegation_depthAgent hops left for an onward call
identity.claims.a2a_delegation_caller_originThe calling client’s credential origin (client-key, shared-token)
identity.claims["a2a_delegation_caller.<name>"]Each claim of the calling client’s credential, such as its role

A rule written for undelegated A2A calls that tests identity.origin == "client-key" or identity.claims.role does not match a delegated call. Test the a2a_delegation_caller* claims for those.

A credential on any surface that carries a claim named a2a_delegation_* has it dropped.

By default a user’s delegation allows one agent hop: agent A may call B for the user, and B may not call C for that user. The operator’s mint refuses the token for the hop past the limit, so B’s call to C fails. Raise the limit with the operator flag --delegation-max-a2a-depth.

The mint enforces the limit for a verified user. For an asserted user each calling agent passes the hops left on to the mint, which can only lower the limit, so the bound holds only while the calling agents’ pods are honest.

A call that carries a delegation token never follows an HTTP redirect.

A2A calls use the runtime’s HTTPS_PROXY, HTTP_PROXY and NO_PROXY, as MCP and HTTP tool calls do. When you set a proxy in spec.runtime.extraEnv, add .svc and .svc.cluster.local to NO_PROXY, or calls to in-cluster targets go through the proxy too.

Check that every client resolved:

Terminal window
kubectl get agentruntime support --context my-cluster -n my-namespace \
-o jsonpath='{.status.a2a.clients}'

Each entry has ready: true and a resolvedURL. The A2AClientsResolved condition is True.

status.a2a.clients[].error says why. target AgentRuntime has no a2a facade means the target needs a type: a2a facade. cross-namespace agentRuntimeRef is not supported means the ref names another namespace. An unresolved client is left out of the runtime.

The runtime logs a2a agent tools not registered when it cannot fetch the target’s card, and a2a agent exposes no tools when the card lists no skills. Check that exposeAsTools is true.

The runtime cannot read the token file. Check that the Secret exists and has a token key. The pod starts without the Secret; its file appears once the Secret exists and the kubelet next syncs the volume. A token key that is empty also fails each call.

The resource is refused as mutually exclusive

Section titled “The resource is refused as mutually exclusive”

The agent sets both spec.a2aClients and the deprecated spec.facades[].a2a.clients. Move every client to spec.a2aClients and remove clients from the facade.