Agent-to-agent (A2A)
A2A lets one governed agent (the caller) open a task on another governed
agent (the callee), in the A2A protocol shape (SendMessage, GetTask,
CancelTask), through the same door that governs a model call. Forgebench
never executes the callee itself — it relays the task to the callee's own
endpoint (push) or hands it to the callee's own polling process (pull),
and it writes the whole exchange to the ledger either way.
Think of it as the same governance Forgebench already applies to a model call, extended to "one agent asking another to do something": a binding instead of a model allowlist, a task instead of a completion, but the same audit row, the same budget-adjacent guardrails, the same idea that nothing crosses this edge ungoverned.
How it works
- Authorization check. The control plane checks that a binding exists from caller to callee, that it's approved and not revoked, and that the callee is in a callable state (not paused or retired). That's the only check on this edge — not the caller's tools, not the callee's tools, not either agent's budget. A binding answers exactly one question: may this caller open a task on this callee.
- Ledger write. A task row and an audit row are written before
delivery is attempted, tagged with the caller's lineage
(
X-Parent-Call). Whichever way delivery goes next — relayed, claimed, answered, failed, never picked up — the task already exists on the record. - Delivery. If the callee has an
endpoint_urlset, the control plane POSTs the task there (push). If not, the callee's own process claims the task by long-pollingGET /v1/agents/me/tasks/next(pull).
A binding only grants "caller may open a task on callee" — it says nothing about what the callee is allowed to do while working on it. The callee's own model and tool calls are still gated by the callee's own bindings, independent of who opened the task. That separation is what keeps a multi-agent workflow auditable per-agent instead of collapsing into one shared blast radius: agent A calling agent B never means A borrowed B's tools.
1. Grant a binding
A caller can only open a task on a callee it's bound to. Grant one from the caller's Calls tab, or the same operation via the API:

# caller's owner or an admin, granting caller -> callee(s)
curl -X POST https://api.forgebench.ai/v1/agents/$CALLER_ID/agents \
-H "Authorization: Bearer $ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{"callees": ["invoice-classifier", "refund-approver"]}'# caller's owner or an admin, granting caller -> callee(s)
curl -X POST https://api.forgebench.ai/v1/agents/$CALLER_ID/agents \
-H "Authorization: Bearer $ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{"callees": ["706608c0-4160-452a-86ac-ded1186326ac", "8b6a2e1f-2f3a-4b9a-9d0e-1c2d3e4f5a6b"]}'Each entry in the response has caller_agent_id, callee_agent_id,
source, revoked, and approved_at.
- If the granting principal can also speak for the callee (an admin, or a
person who owns both agents),
approved_atis set immediately and the binding is live right away — that's the row in the screenshot above. - Otherwise
approved_atisnull: the binding is pending until the callee's owner (or an admin) approves it from the callee's own Calls tab, or via the API:
# callee's owner or an admin — agent_id is the CALLEE, binding_id from the grant response
curl -X POST https://api.forgebench.ai/v1/agents/$CALLEE_ID/agents/$BINDING_ID/approve \
-H "Authorization: Bearer $ADMIN_KEY"This two-sided approval is deliberate: it's the same "both owners sign off" shape a cross-team integration needs anywhere else, so a caller can't grant itself access to a callee it doesn't own by asking nicely through the API. Same-owner or admin-granted pairs skip the second step because the same person already speaks for both sides.
Re-granting an already-revoked pair un-revokes the same row instead of creating a new one — the history of who was ever bound to whom stays intact.
Read both directions for one agent:
GET /v1/agents/{agent_id}/agents
-> {"outbound": [...], "inbound": [...]}Revoke a binding from either side, from the Calls tab's Revoke button or
DELETE /v1/agents/{agent_id}/agents/{binding_id}. The row is kept and
marked revoked: true rather than deleted — a security review six months
later should be able to see that an edge existed and was cut, not just that
it's gone. Revocation takes effect on the caller's next task.
Next
SendMessage, GetTask, CancelTask — the caller side, with a worked
example.
Run an agent as a callee: pull with serve(), or push over a webhook.
The A2A discovery document, and every refusal shape specific to this edge.
AgentsRegister the two agents you're wiring together.

