Agents Agent-to-agent (A2A)

Agent-to-agent (A2A)

A2A lets one governed agent (the caller) open a task on another (the callee), using the A2A protocol shape (SendMessage, GetTask, CancelTask). Tasks pass through the same gateway that governs a model call. Forgebench never runs 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). Either way, it writes the whole exchange to the ledger.

It applies the governance Forgebench already uses for model calls to "one agent asking another to do something". A binding replaces the model allowlist and a task replaces the completion. The audit row, the budget-adjacent guardrails and the principle that nothing crosses this edge ungoverned stay the same.

How it works

  1. Authorization check. Forgebench 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.
  2. 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.
  3. Delivery. If the callee has an endpoint_url set, Forgebench POSTs the task there (push). If not, the callee's own process claims the task by long-polling GET /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 keeps a multi-agent workflow auditable per agent. 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:

billing-orchestrator's Calls tab: an approved outbound binding to invoice-classifier, and the empty inbound side.
billing-orchestrator's Calls tab: an approved outbound binding to invoice-classifier, and the empty inbound side.
# caller's owner or an admin, granting caller -> callee(s)
import os
import httpx

r = httpx.post(
  f"https://api.forgebench.ai/v1/agents/{os.environ['CALLER_ID']}/agents",
  headers={"Authorization": f"Bearer {os.environ['ADMIN_KEY']}"},
  json={"callees": ["invoice-classifier", "refund-approver"]},
)
r.raise_for_status()
bindings = r.json()  # one entry per callee

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_at is set immediately and the binding is live right away — that's the row in the screenshot.
  • Otherwise approved_at is null: 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
import os
import httpx

r = httpx.post(
  f"https://api.forgebench.ai/v1/agents/{os.environ['CALLEE_ID']}/agents/{os.environ['BINDING_ID']}/approve",
  headers={"Authorization": f"Bearer {os.environ['ADMIN_KEY']}"},
)
r.raise_for_status()
binding = r.json()  # approved_at is now set

Two-sided approval means a caller can't grant itself access to a callee it doesn't own. It's the same "both owners sign off" shape any cross-team integration needs. 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, not deleted, so a later security review can see that an edge existed and was cut. Revocation takes effect on the caller's next task.

Next

Cookie settings

Our Cookie Policy lists every cookie and what it does.