Skip to content

Agent Interop

Agent Interop lets your Fontana agents work with agents on other platforms in both directions. Outbound, a Fontana agent delegates a task to an external agent you register in Admin → Agent Interop. Inbound, an external agent sends a task to one of your Fontana agents and receives the answer. Fontana speaks four open protocols: A2A, ACP, ANP, and OFP. Every call is authenticated, every outbound endpoint must be on your origin allowlist, and every delegation is recorded with audit metadata.

SubAgent Delegation

Use SubAgent Delegation to isolate deep work in child threads while the parent keeps context lean.

Agent Handoff

Use Agent Handoff to transfer the root thread to a specialist agent when the task surface changes.

Agent Interop

Use Agent Interop to send tasks to external agents and answer theirs over A2A, ACP, ANP, and OFP.

Pattern Status What it does Configuration
SubAgent delegation Available Spawn a child thread for delegated work, then return results to the parent. Isolates deep work while the parent keeps context lean. subagents on the agent definition in Admin → Agents
Agent handoff Available Transfer the current conversation to another in-app agent on the same thread. Use when the task surface changes. handoffAgents on the agent definition in Admin → Agents

All four protocols are available inbound and outbound. Each endpoint follows the exact specification version below.

Protocol Status Version What Fontana implements Specification
A2A (Agent2Agent) Available 1.0 (JSON-RPC binding; 0.3 method names accepted) Agent Cards, SendMessage, GetTask, CancelTask, ListTasks, conversation continuity through contextId A2A specification
ACP (Agent Communication Protocol) Available (legacy) 0.2.0 (archived OpenAPI) Agent manifests, synchronous and asynchronous runs, run status and cancel, sessions through session_id ACP repository
ANP (Agent Network Protocol) Available 1.2 (did:wba, JSON-RPC core binding anp.core.binding.v1) DID document, agent descriptions, discovery collection, signed did:wba handshake, direct.send, direct.status, anp.get_capabilities ANP repository
OFP (Open Floor Protocol) Available 1.1.0 inter-agent message, 1.0.1 assistant manifest, 1.0.2 dialog event Conversation envelopes with utterance, getManifests, invite, uninvite, and bye events OFP introduction

Fontana publishes interop endpoints on your tenant origin:

  • Discovery documents that a protocol expects at a well-known location live at the origin root: https://<tenant>.<baseDomain>/.well-known/….
  • Every other endpoint lives under https://<tenant>.<baseDomain>/_convex-site/api/…. The discovery documents advertise these URLs, so a peer that starts from discovery finds them on its own.

For https://acme.example.com, the A2A Agent Card is https://acme.example.com/.well-known/agent-card.json and the A2A JSON-RPC endpoint is https://acme.example.com/_convex-site/api/a2a/json-rpc.

Agent ids in a URL are percent-encoded. The agent system/agents/research appears as system%2Fagents%2Fresearch.

Discovery documents are public and need no token. They list the agents that are active, available for chat, and open to everyone in your organisation; an agent restricted to named users is never listed.

Every inbound task, run, or message call carries a personal access token:

  1. Open Admin → Personal access tokens (/admin/access-tokens) on your tenant origin.
  2. Create a token and bind it to the agent that should answer the external agent.
  3. Give the full bearer (fnt_pat_…) to the external platform. It sends it on every call:
Authorization: Bearer fnt_pat_…

The external agent then acts as you, through the agent the token is bound to, inside that agent’s permissions. Each task runs as one turn of that agent in a dedicated conversation that belongs to you.

Agent routing. Without a query parameter, the token’s bound agent answers. Add ?agent=<percent-encoded agent id> to the endpoint URL to address another agent you can chat with; the per-agent discovery documents already carry this parameter. ACP selects the agent by the run’s agent_name instead.

Ownership. A task, run, or status read answers only for tasks created with a token of the same person. Another person’s task id reads as not found.

Request limits. Each token has a per-token rate limit (HTTP 429 when exceeded), and a request body may be at most 1 MiB (HTTP 413 above that). A missing or unknown token answers HTTP 401 with WWW-Authenticate: Bearer.

ANP peers can also authenticate with a signed did:wba handshake instead of a token you hand over. See ANP.

Endpoint Method URL Auth
Workspace Agent Card GET https://<tenant>.<baseDomain>/.well-known/agent-card.json Public
Per-agent Agent Card GET https://<tenant>.<baseDomain>/_convex-site/api/a2a/agent-card?agent=<agent id> Public
JSON-RPC (token’s agent) POST https://<tenant>.<baseDomain>/_convex-site/api/a2a/json-rpc Bearer token
JSON-RPC (a specific agent) POST https://<tenant>.<baseDomain>/_convex-site/api/a2a/json-rpc?agent=<agent id> Bearer token

The workspace card describes your deployment; each per-agent card names one agent and advertises the JSON-RPC URL with that agent’s ?agent= parameter. Cards declare HTTP bearer security, streaming: false, and pushNotifications: false.

Methods. SendMessage, GetTask, CancelTask, and ListTasks. The A2A 0.3 names message/send, tasks/get, tasks/cancel, and tasks/list are accepted as aliases, and 0.3 configuration.blocking is read as the inverse of returnImmediately.

Terminal window
curl https://<tenant>.<baseDomain>/_convex-site/api/a2a/json-rpc \
-H "Authorization: Bearer fnt_pat_…" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"SendMessage","params":{"message":{"role":"ROLE_USER","messageId":"m-1","parts":[{"text":"Summarise the open risks in the Q3 register."}]}}}'
  • Replies. SendMessage always returns a task. By default Fontana waits up to two minutes: a finished task carries the answer as an artifact and as status.message; an unfinished one returns in TASK_STATE_WORKING and you poll it with GetTask. Set configuration.returnImmediately: true to get the task back at once in TASK_STATE_SUBMITTED.
  • Task states. TASK_STATE_SUBMITTED, TASK_STATE_WORKING, TASK_STATE_COMPLETED, TASK_STATE_FAILED, and TASK_STATE_CANCELED. A failed task carries the reason in status.message.
  • Conversations. The returned contextId names the conversation. Send the next message with the same contextId and the agent continues with the earlier messages in context. One task runs per conversation at a time.
  • Content. Text parts only; text parts of one message are joined with line breaks.
JSON-RPC error When
-32001 TaskNotFound The task id is unknown or belongs to another person
-32002 TaskNotCancelable The task already finished, or the conversation still has a task running (conversation busy)
-32004 UnsupportedOperation Streaming, task subscription, push notification configs, or follow-up input on an existing taskId
-32005 ContentTypeNotSupported The message carries file, URL, raw, or data parts
-32602 InvalidParams A malformed message, or the agent is not found or not available to you
-32007 ExtendedAgentCardNotConfigured Extended Agent Card requests (cards declare no extended card)

Errors after a request is read travel as JSON-RPC errors on HTTP 200.

ACP is a legacy protocol: it merged into A2A under the Linux Foundation in August 2025. Fontana serves the archived OpenAPI 0.2.0 under one base URL, https://<tenant>.<baseDomain>/_convex-site/api/acp.

Endpoint Method Path under the base URL Auth
Ping GET /ping Public
List agents GET /agents (limit, offset) Public
Agent manifest GET /agents/<agent id> Public
Create run POST /runs Bearer token
Read run GET /runs/<run_id> Bearer token
Cancel run POST /runs/<run_id>/cancel Bearer token
Run events GET /runs/<run_id>/events Answers 501

A manifest’s name is the Fontana agent id; send it back as agent_name.

{
"agent_name": "system/agents/research",
"mode": "sync",
"session_id": "quarterly-review",
"input": [
{
"role": "user",
"parts": [{ "content_type": "text/plain", "content": "Summarise the open risks in the Q3 register." }]
}
]
}
  • Modes. sync waits up to two minutes and returns the run as it stands (in-progress when the agent is still working; poll GET /runs/<run_id>). async returns 202 at once with the run in created. stream answers 400.
  • Run statuses. created, in-progress, completed, failed, and cancelled. A completed run’s output is one agent message with a text/plain part.
  • Sessions. Reuse session_id to continue a conversation. A session with a run in progress answers 409.
  • Content. Inline text/* parts are read in order. A part sent by content_url or in base64 encoding answers 400.
  • Errors. 404 not_found for an unknown agent or another person’s run, 409 for a run that already finished when you cancel it, 400 invalid_input for a malformed run.

Fontana implements ANP 1.2 with one did:wba identity for your deployment: did:wba:<tenant>.<baseDomain>.

In Admin → Agent Interop → Discovery, choose Generate ANP identity once. Fontana creates an Ed25519 key pair in your Vault and a signed DID document. Until you generate it, /.well-known/did.json answers 404 and outbound ANP calls fail. The identity is created only once; the action refuses when it already exists.

Endpoint Method URL Auth
DID document GET https://<tenant>.<baseDomain>/.well-known/did.json Public
Agent descriptions (ANP-08) GET https://<tenant>.<baseDomain>/.well-known/agent-descriptions Public
Agent description (ANP-07) GET https://<tenant>.<baseDomain>/_convex-site/api/anp/agents/<agent id>/ad.json Public
JSON-RPC POST https://<tenant>.<baseDomain>/_convex-site/api/anp/rpc?agent=<agent id> did:wba signature or Bearer

The discovery collection is a JSON-LD CollectionPage that links each agent description; each description names your DID and one natural-language JSON-RPC interface.

An ANP connection names the peer’s DID and an inbound agent. When you save it, Fontana creates a dedicated interop token for you, named ANP peer: <display name>, bound to that inbound agent, and keeps it in your Vault. Deleting the connection deletes the token.

A peer authenticates by signing its first request:

  1. The peer signs the request with RFC 9421 HTTP Message Signatures over @method, @target-uri, @authority, and content-digest (RFC 9530 over the exact body bytes), with created, expires, and a nonce. The keyid is a DID URL of the peer’s DID.
  2. Fontana accepts the signature only when that DID is the peer DID of an active ANP connection. It resolves the peer’s DID document over HTTPS from an origin on your allowlist, verifies the Ed25519 signature, requires created within five minutes, and accepts each nonce once.
  3. The response carries Authentication-Info: access_token="…", token_type="Bearer", expires_in=3600. The peer sends that bearer on later calls, and signs again after expires_in.

The token Fontana hands to an ANP peer works only on the Agent Interop endpoints; the Fontana MCP URL refuses it. It is bound to the inbound agent you chose: every request made with it reaches that agent, and a ?agent= parameter on the URL is ignored for this token. Calls with it run as you.

Requests use the JSON-RPC 2.0 core binding: a string id, and params with meta.profile set to anp.core.binding.v1, an optional meta.session_id, and a body.

{
"jsonrpc": "2.0",
"id": "req-1",
"method": "direct.send",
"params": {
"meta": { "profile": "anp.core.binding.v1", "session_id": "quarterly-review" },
"body": { "text": "Summarise the open risks in the Q3 register." }
}
}
  • direct.send with body.text waits up to two minutes and returns task_id, status, session_id, and the answer in text (or the reason in error). Status is submitted, working, completed, failed, or canceled.
  • direct.status with body.task_id returns the same shape; poll it until the task finishes.
  • anp.get_capabilities returns your DID, the supported profile, the 1 MiB body limit, the methods, and the agent’s description.

Errors carry an ANP code in data.anp_code, for example anp.unauthorized (401), anp.target_not_found (404), anp.delivery_rejected (409, conversation busy), anp.rate_limited (429), and anp.unsupported_profile (400).

Endpoint Method URL Auth
Conversation envelope POST https://<tenant>.<baseDomain>/_convex-site/api/ofp/envelope?agent=<agent id> Bearer token
Assistant manifest GET https://<tenant>.<baseDomain>/_convex-site/api/ofp/manifest?agent=<agent id> Public

OFP defines no well-known manifest URL. A peer asks for manifests with a getManifests event; the manifest URL above serves the same document for inspection.

Each Fontana agent has the speaker URI tag:<tenant>.<baseDomain>,2026:agents/<agent id> and the envelope URL above as its serviceUrl.

Incoming event Fontana answers
utterance Runs the agent and replies with an utterance carrying the answer in features.text.tokens
getManifests publishManifests with the agent’s assistant manifest
invite acceptInvite
uninvite, bye events: []; the conversation is not continued
Floor events events: [] (requestFloor, grantFloor, revokeFloor, yieldFloor)

conversation.id names the conversation, so the next envelope with the same id continues it. Fontana replies in the same HTTP response. When the agent needs more than two minutes, the reply is events: [].

A Fontana agent delegates to an external agent with its external-agent delegation tool. Register each external agent in Admin → Agent Interop first. Saving a connection needs agents:write.

Every connection has a display name, a slug, a protocol, the protocol version, an endpoint URL, an authentication kind, and an active switch. The protocol is fixed once the connection exists.

Protocol Endpoint URL Protocol-specific fields What Fontana sends
A2A The peer’s JSON-RPC URL None SendMessage, then GetTask while the task is unfinished
ACP The peer’s ACP base URL (Fontana appends /runs) Agent name: the peer’s agent_name POST /runs in sync mode, then GET /runs/<run_id> while the run is unfinished
ANP The peer’s JSON-RPC URL Peer DID (did:wba:…) and Inbound agent (answers the peer’s calls) direct.send signed with your deployment’s DID key, then direct.status with the bearer the peer returns
OFP The peer’s serviceUrl Peer speaker URI: the peer agent’s speakerUri One envelope with an utterance addressed to the peer; conversation.id is the delegating conversation’s id

Authentication. A2A, ACP, and OFP connections use no authentication, a bearer token read from a deployment environment variable whose name starts with FONTANA_A2A_BEARER_, or a stored bearer token you paste in the editor. A stored token is kept only in your Vault and is never shown again. ANP connections authenticate with your deployment’s DID signature.

Waiting for an answer. When a peer returns an unfinished task or run, Fontana polls it for up to eight minutes before the delegation fails with a timeout.

Audit. Every delegation, successful or failed, records on the tool call the protocol and version, the connection, the endpoint origin, the method, the latency, the HTTP status, the external task id, and a failure category.

Fontana calls only endpoints whose origin is on your deployment’s interop allowlist, FONTANA_A2A_ALLOWED_ENDPOINT_ORIGINS: a comma-separated list of HTTPS origins such as https://agents.example.com. Your platform team sets it for your deployment. The same list governs every outbound protocol and the hosts Fontana resolves peer DID documents from. Endpoints must use HTTPS, carry no embedded credentials, and resolve to a public host.

Test connection in the editor runs one handshake for the connection’s protocol and shows the result inline:

Protocol Handshake
A2A Fetches the peer’s Agent Card
ACP GET /ping on the peer’s base URL
ANP Resolves the peer’s DID document
OFP Sends getManifests and expects publishManifests

Each connection’s settings are a file in the system namespace of Fontana Files, config/a2a/<slug>.yaml: display name, protocol (a2a, acp, anp, or ofp), protocol version, endpoint URL, authentication kind, whether it is active, and the protocol’s own keys (agentName, peerDid and inboundAgentId, or peerSpeakerUri). Saving on Admin → Agent Interop writes that file. The slug names the file, so it cannot change once the connection exists; the slug anp-identity is reserved. The file never holds a token: bearer tokens and the ANP inbound token live only in your Vault. If a saved file cannot be read, the connection keeps its last good settings and the error shows in the editor.

  • Connections: one tile per protocol (A2A, ACP, ANP, OFP), the per-protocol fields above, and Test connection.
  • Discovery: live previews of every document Fontana publishes, fetched from the public URL and shown as raw JSON with a copyable URL: the A2A workspace and per-agent cards, the ACP agent manifests, the ANP DID document, agent descriptions, and discovery collection, and the OFP assistant manifests. Generate ANP identity lives here.
  • Tasks: your last 50 inbound tasks with protocol, agent, state, and timestamps.
Area Behaviour
Streaming A2A SendStreamingMessage and SubscribeToTask answer -32004; cards declare streaming: false
Push notifications A2A push notification configs answer -32004; cards declare pushNotifications: false
ACP events and stream mode GET /runs/<run_id>/events answers 501; mode: stream answers 400
ANP negotiation The meta-protocol negotiation draft (ANP-06) is out of scope; anp.core.binding.v1 is the one profile
OFP floor control Floor events are accepted and answered with events: []
Agent turn Each inbound task runs one agent turn, bounded by ten minutes
Blocking replies Return after two minutes with a task you can poll: A2A GetTask, ACP GET /runs/<run_id>, ANP direct.status
Message content Text only
Request body 1 MiB per request
Outbound wait Eight minutes of polling per delegation
  • Agents - in-app personas, subagents, and handoff configuration
  • External agents - personal access tokens bound to an agent, and the Fontana MCP URL
  • Tools and MCP - MCP integrations distinct from peer-agent federation
  • Immutable audit trail - delegation and tool-call audit evidence