Skip to content

feat(a2a): add rig-a2a client crate for consuming A2A agents as tools - #2204

Open
mccormickt wants to merge 3 commits into
0xPlaygrounds:mainfrom
mccormickt:feat/rig-a2a
Open

feat(a2a): add rig-a2a client crate for consuming A2A agents as tools#2204
mccormickt wants to merge 3 commits into
0xPlaygrounds:mainfrom
mccormickt:feat/rig-a2a

Conversation

@mccormickt

Copy link
Copy Markdown
Contributor

An initial implementation of an integration with the Agent2Agent (A2A) protocol that allows consuming A2A agents as Rig tools:

  • A2AClient discovers the well-known AgentCard with a 1 MiB limit, selects the first supported JSON-RPC or HTTP+JSON interface in card order, validates that selected interface against the card origin, and forwards its optional tenant.
  • A2ATool projects each declared skill as a provider-safe Rig tool. Threading follows the A2A echo model: server-generated contextId and taskId values are surfaced in tool output markers and echoed back via tool arguments. Task ids are omitted for terminal tasks; direct Message responses surface every server-returned identifier.
  • A2AAgentBuilderExt::a2a_tools appends every remote skill to a Rig agent at build time, mirroring the rmcp integration.
  • A2AClient::message(..) supports direct requests that echo server-issued context and task ids.
  • Loopback integration tests exercise both HTTP transports against the upstream a2a-server crate, which remains a dev-dependency only.

The integration is native-only because the upstream A2A networking stack does not support wasm32-unknown-unknown.

Updates #391

@mccormickt
mccormickt force-pushed the feat/rig-a2a branch 5 times, most recently from ba142db to 5052ebf Compare July 26, 2026 18:16
`HookContext` reported the run id, turn, streaming surface and agent name,
but not which conversation the run continued. Only the runner knew that, and
it used the id solely to load and append `ConversationMemory`.

A hook is where per-conversation state belongs. A provider that keeps its own
session on the far side of the wire — an A2A `contextId`, a hosted assistant
thread — needs to know which conversation a run continues in order to attach
its own identifiers, and no hook event carried that.

Report it on `HookContext` alongside the rest of the run's identity. The id is
what the caller named, independent of whether a memory backend consumed it: an
explicit `chat_history` bypasses load and save while leaving the conversation
named, and only `without_memory` clears it.
… hooks

`CompletionResponse` and `StreamResponseFinish` carried the message id but not
the response id, so the two response-scoped surfaces disagreed with the rest
of Rig: `CompletionResponse::response_id` is populated by every provider and
reported as `gen_ai.response.id`, yet no hook could read it.

It is deliberately not promoted into history — `response_scoped_id_is_not_promoted_into_history` pins that — which leaves a hook as the only place a
turn can be correlated with a provider's own logs, or with state a provider
keeps per response.

Report it on both events. The streaming side reads it from the terminal
record, so a stream that ended early reports `None` rather than inventing one.
Consume remote Agent2Agent (A2A) protocol agents from Rig two ways:

- A2AClient::tool projects a whole remote agent as one Rig tool taking a
  single prompt. A2A carries no skill selector, so a card's skills are
  documentation in the tool description, not separate tools.
- A2AModel implements CompletionModel over message/send and
  message/stream, so A2AClient::agent backs a Rig Agent with a remote
  one. Request fields A2A cannot express — tools, output_schema, a
  demanding tool_choice — fail loudly rather than being dropped.

Threading is host-side: ConversationId keys a bounded store of the
server-issued contextId and taskId, so a model can neither forge an
identifier nor lose one; task ids are reused only for tasks paused in
input-required. Each call publishes an A2AThreadInfo for hooks. Remote
states the caller cannot act on become typed tool errors carrying the
remote's status text.

A2AClient discovers the well-known AgentCard with a 1 MiB limit, selects
the first supported JSON-RPC or HTTP+JSON interface in card order,
validates it against the card origin, and forwards its optional tenant.

Loopback tests cover both HTTP transports against the upstream a2a-server
crate, a dev-dependency; the a2a_tool and a2a_agent examples demo each
surface against the same stub, with no credentials.

The integration is native-only because the upstream A2A networking stack
does not support wasm32-unknown-unknown.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant