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.
In-app orchestration patterns
Section titled “In-app orchestration patterns”Use SubAgent Delegation to isolate deep work in child threads while the parent keeps context lean.
Use Agent Handoff to transfer the root thread to a specialist agent when the task surface changes.
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 |
External protocols
Section titled “External protocols”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 |
Public URLs
Section titled “Public URLs”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.
Inbound authentication
Section titled “Inbound authentication”Every inbound task, run, or message call carries a personal access token:
- Open Admin → Personal access tokens (
/admin/access-tokens) on your tenant origin. - Create a token and bind it to the agent that should answer the external agent.
- 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.
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.
SendMessagealways returns a task. By default Fontana waits up to two minutes: a finished task carries the answer as an artifact and asstatus.message; an unfinished one returns inTASK_STATE_WORKINGand you poll it withGetTask. Setconfiguration.returnImmediately: trueto get the task back at once inTASK_STATE_SUBMITTED. - Task states.
TASK_STATE_SUBMITTED,TASK_STATE_WORKING,TASK_STATE_COMPLETED,TASK_STATE_FAILED, andTASK_STATE_CANCELED. A failed task carries the reason instatus.message. - Conversations. The returned
contextIdnames the conversation. Send the next message with the samecontextIdand 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.
syncwaits up to two minutes and returns the run as it stands (in-progresswhen the agent is still working; pollGET /runs/<run_id>).asyncreturns202at once with the run increated.streamanswers400. - Run statuses.
created,in-progress,completed,failed, andcancelled. A completed run’soutputis one agent message with atext/plainpart. - Sessions. Reuse
session_idto continue a conversation. A session with a run in progress answers409. - Content. Inline
text/*parts are read in order. A part sent bycontent_urlor in base64 encoding answers400. - Errors.
404 not_foundfor an unknown agent or another person’s run,409for a run that already finished when you cancel it,400 invalid_inputfor a malformed run.
Fontana implements ANP 1.2 with one did:wba identity for your deployment: did:wba:<tenant>.<baseDomain>.
Generate the ANP identity
Section titled “Generate the ANP identity”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.
DID handshake and trust boundary
Section titled “DID handshake and trust boundary”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:
- The peer signs the request with RFC 9421 HTTP Message Signatures over
@method,@target-uri,@authority, andcontent-digest(RFC 9530 over the exact body bytes), withcreated,expires, and anonce. Thekeyidis a DID URL of the peer’s DID. - 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
createdwithin five minutes, and accepts each nonce once. - The response carries
Authentication-Info: access_token="…", token_type="Bearer", expires_in=3600. The peer sends that bearer on later calls, and signs again afterexpires_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.
Methods
Section titled “Methods”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.sendwithbody.textwaits up to two minutes and returnstask_id,status,session_id, and the answer intext(or the reason inerror). Status issubmitted,working,completed,failed, orcanceled.direct.statuswithbody.task_idreturns the same shape; poll it until the task finishes.anp.get_capabilitiesreturns 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: [].
Outbound connections
Section titled “Outbound connections”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.
Origin allowlist
Section titled “Origin allowlist”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
Section titled “Test connection”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 |
Connection file
Section titled “Connection file”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.
Admin → Agent Interop
Section titled “Admin → Agent Interop”- 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.
Limits
Section titled “Limits”| 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 |
Related documentation
Section titled “Related documentation”- 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