mentionable.dev

Multi-Agent Composition

Single-line summary: Symmetric @x@domain addressing means any agent can call any other agent over A2A; AgentChain bounds depth, recipient_capabilities tells each hop how to dispatch siblings, and on_behalf_of carries upstream user authority.

See also: normalized-message-guide, identity-auth-guide, transport-module-guide, policy-part-guide, glossary

Type sources: packages/core/src/{message,recipient-capabilities,identity}.ts.

Symmetric Addressing

sender.address and recipient use the same @x@domain shape. Humans (@alice@example.com over email), agents (@scheduler@bots.example.com over A2A), and intermediate services share the address space.

When agent A sends to agent B over A2A, A is the sender and B is the recipient — exactly as if A were a human caller. B’s runtime applies the same trust pipeline:

  1. Verify the wire credential (A2A JWT or OAuth).
  2. Verify any forwarded IdentityEvidence from A’s metadata extension.
  3. Apply IdentityPolicy and per-agent ACL.
  4. Invoke B.handle(message, ctx).

This symmetry is what makes Mentionable composable. There is no separate “agent-to-agent” trust path — it is the same path with the addresser being an agent.

AgentChain

When an agent dispatches to another agent on behalf of a chained turn, it sets recipient_capabilities.agent_chain:

type AgentChain = {
  hop: number // 1-indexed; 1 = first chained turn
  max_hops: number // operator-supplied ceiling
  is_final: boolean // hop === max_hops
}

Carried over A2A as metadata.mentionable.agent_chain. The receiving agent’s LLM Harness reads this and adjusts: when is_final: true, the agent MUST synthesize a conclusion and MUST NOT invite another agent into the chain.

The default renderAgentChain helper produces system-prompt cues:

Agents that ignore the cue still respect the wire-level max_hops enforcement: the dispatching agent (or the runtime) checks hop < max_hops before issuing the next call.

recipient_capabilities Forwarded Across Hops

When a Slack-style client dispatches an agent call over A2A, the dispatcher overrides A2A’s default mention_relay: 'none':

// Slack connector calling agent B over A2A
{
  recipient_capabilities: {
    mention_relay: { kind: 'inline' },  // override of A2A default
    agent_chain: { hop: 1, max_hops: 4, is_final: false },
  },
}

This tells agent B: “the user is on Slack — if you want to bring in agent C, type @c@domain and the platform will route.” The dispatcher (Slack Connector) carries recipient_capabilities over A2A via the same metadata.mentionable.* namespace.

Without the override, agent B would default to A2A’s none and issue an outbound A2A call to agent C — wasteful when Slack will relay an inline mention naturally.

Fan-Out Pattern

When the user (or upstream agent) asks something requiring multiple sibling agents, the orchestrator pattern is:

user "@scheduler @docs find a free Tuesday and link the runbook"


   orchestrator agent

        ├──► A2A call to @scheduler@bots.example.com  (parallel)
        ├──► A2A call to @docs@bots.example.com        (parallel)

   collect responses → synthesize one cohesive reply


   single reply back to user

Implementation rules:

Payment in Chained Calls (on_behalf_of)

When agent A pays agent B on behalf of user U, the upstream user authority travels in IdentityEvidence.on_behalf_of:

{
  subject: '@agent-a@example.com',
  on_behalf_of: ['mailto:u@example.com'],   // immediate caller first
  proof: { type: 'signed-attestation', ... },
}

The chain is ordered immediate-caller-first. Agent B authorizes against:

Delegation chain for payment is F-1 future work — the v0.1 baseline supports the shape but advanced policy (delegated spending caps, per-hop attenuation) is not normative yet. See identity-auth-guide#chained-agent-calls---on_behalf_of.

Multi-Hop Cap Enforcement

When agent_chain.is_final: true, the receiving agent MUST NOT invite another agent. Implementation:

The runtime’s outbound A2A helper computes this automatically.

Combining With PolicyPart

A chained call may surface a PolicyPart from any hop:

user → orchestrator → @paid-search@example.com
                       returns payment_required (state: S)


        orchestrator surfaces payment_required to user

   user pays → orchestrator receives policy_resolution { in_reply_to_state: S }


        orchestrator calls @paid-search again with policy_resolution forwarded


                    final answer to user

The orchestrator MUST verify in_reply_to_state against @paid-search’s issuance store via the resumption pattern — typically by re-dispatching the original call with policy_resolution set, letting the receiving agent’s adapter look up the state and authorize. See policy-part-guide#resumption-patterns.

Composition Rules

Common Mistakes