feat(rig-core): ToolCallExtensions — per-call tool context through the agent loop, MCP & sub-agents (supersedes #1537, #1953) - #1954
Merged
Conversation
… of #1537) Faithful replay of redroy44's PR #1537 onto current main, with no behavioral changes. Adds `ToolCallContext` (a type-erased, cloneable type-map) and the `call_with_context` dispatch methods threaded through ToolServerHandle -> ToolSet -> ToolType -> ToolDyn -> Tool. Subsequent commits harden the type and wire the context into the agent run loop (which #1537 did not do). Co-authored-by: Piotr Bandurski <redroy44@gmail.com>
- Collapse the duplicated wasm/non-wasm cfg blocks into one AnyClone trait using the existing WasmCompatSend/WasmCompatSync markers. - `insert` now returns the displaced value (`Option<T>`), matching `http::Extensions`/`HashMap` instead of silently dropping it. - Add `require::<T>()` + `MissingContextValue` so tools that depend on a context value get an actionable error instead of a silent `None`. - Make read-accessor bounds symmetric with `insert` (Send + Sync on native). - Add a compile-time `Send + Sync` assertion (the property the agent loop relies on) and tests for deep-clone independence, insert-returns-previous, and `require`. - Remove the now-dead `ToolType::call` (was `#[allow(dead_code)]`); route the empty-context delegations through the shared `EMPTY` constant. - Consolidate to a single public path `rig::tool::ToolCallContext` (drop the crate-root re-export; make the `context` module private). - Fix the doctest crate path (`rig::` -> `rig_core::`) so it compiles. - Document the `call`/`call_with_context` override contract.
This is the integration #1537 was missing: the new context was plumbed through ToolServer but never reached tools executed by an Agent, because the run loop called the context-less `call_tool`. - Add a `tool_context: ToolCallContext` to `AgentRunner` (empty by default) and a `tool_context(..)` builder, mirrored on `PromptRequest`, `TypedPromptRequest`, and `StreamingPromptRequest`. - `run_single_tool` now takes the context and dispatches via `call_tool_with_context`, so both the blocking and streaming drivers thread it identically. - Add a shared `MockContextProbeTool`/`SessionId` test fixture and end-to-end tests proving a context set on `agent.prompt(..)` / `agent.stream_prompt(..)` reaches the executed tool, and that the empty-context default still works. Callers now do: `agent.prompt(p).tool_context(ctx).await`.
McpTool implements ToolDyn directly, so the default call_with_context dropped the context for MCP tools — the exact A2A/auth path the feature targets. Override it: an `rmcp::model::Meta` placed in the ToolCallContext is now attached as the MCP request's `_meta` (SEP-1319), the idiomatic channel for per-call metadata (auth tokens, session ids, A2A context_id/task_id). The shared `execute` helper backs both `call` (no meta) and `call_with_context`, so behavior is unchanged when no Meta is supplied. Adds a mock-server test asserting the `_meta` reaches the server (and is absent when the context carries none).
`Agent<M>` implements `Tool` (the agent-as-tool / sub-agent pattern) but only implemented `call`, so a context set on the outer run was dropped at the sub-agent boundary and the inner agent's tools never saw it. Override `call_with_context` to forward the context into the inner `prompt(..).tool_context(ctx)`, enabling context-carrying sub-agent delegation / A2A chains. Adds a nested-agent test.
…ent MCP _meta - Add a streaming empty-context test mirroring the blocking one (driver symmetry). - Add a direct `call` test pinning MockContextProbeTool's "call-no-context" sentinel, and tighten its docstring to explain the sentinel's purpose. - Document the McpTool `_meta` forwarding in the rmcp module docs with an example, and re-export `rig::tool::rmcp::Meta` for convenience.
5 tasks
…p_used) CI runs clippy --all-features --all-targets, enabling the test-utils feature so the fixture compiles as library code where the workspace's deny(clippy::unwrap_used) applies. Recover poisoned locks via into_inner(), matching the existing test_utils convention.
…s parity Adopts the stronger type-map decisions from the parallel #1953 so the surviving design carries both the better name and the full integration. - Rename the public surface to the "Extensions" vocabulary the maintainers prefer, resolving the name collision with rmcp's own `ToolCallContext` and rig's `InvalidToolCallContext`/`Tool::Context`: ToolCallContext -> ToolCallExtensions MissingContextValue -> MissingExtension Tool/ToolDyn/ToolType/ToolSet::call_with_context -> call_with_extensions ToolServerHandle::call_tool_with_context -> call_tool_with_extensions PromptRequest/TypedPromptRequest/StreamingPromptRequest/AgentRunner ::tool_context -> with_tool_extensions (module tool/context.rs -> tool/extensions.rs) - Port the no-op `IdHasher` over `TypeId` keys (http::Extensions parity; avoids re-hashing TypeIds with SipHash on the dispatch path) with a non-panicking byte-fold `write` fallback, plus a round-trip stress test. - Add `len()`/`is_empty()` for full http::Extensions API parity. Keeps this PR's deeper integration: MCP `_meta` forwarding, sub-agent propagation, the TypedPromptRequest entry point, require()/MissingExtension, deduped wasm cfg, the assert_send_sync guard, and the broader e2e tests.
… entry Addresses review feedback on #1954: - Drop the `with_` prefix from the builder to match the convention the CHANGELOG documents for this same unreleased cycle (runner/request builders dropped `with_history`->`history`, `with_tool_concurrency`->`tool_concurrency`): `with_tool_extensions(..)` -> `tool_extensions(..)` on PromptRequest, TypedPromptRequest, StreamingPromptRequest, and AgentRunner. - Add the missing `### Added` CHANGELOG entry for the whole ToolCallExtensions surface (the project keeps a strict per-change changelog). - Doc/nit polish: name the trait-default param `_extensions` (not `_ctx`), and tighten the `call_with_extensions` override contract to say *dynamic* dispatch (a direct `Tool::call` still runs its own body).
…e test Review follow-ups on #1954: - Finish the ToolCallExtensions rename in user-facing text: the struct/new()/ require docs and — most visibly — the MissingExtension runtime error string ("required tool-call context value of type ..." -> "required tool-call extension of type ..."), plus the internal comments/test names. These render in published rustdoc and logs under a type named ToolCallExtensions. - Add an end-to-end test that extensions persist across MULTIPLE tool-call rounds in one run (the headline value prop): MockExtensionsProbeTool now records every call (observations()), and the model scripts two probe rounds; both observe the same injected SessionId. - Document that get_mut/remove are owner-side (tools get a shared ref). - Disambiguate the AgentRunner.tool_extensions field doc link to the builder method (field and method share the name).
Final review nit: the de-context rename updated the type docs but left 'context' in the prose of four public method docs (AgentRunner::tool_extensions, ToolServerHandle/ToolSet/ToolDyn ::*_with_extensions) plus two doc comments. Now uniformly 'extensions'. Docs-only; no API or behavior change.
Merged
This was referenced Jul 7, 2026
Closed
Closed
Closed
Merged
1 task
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Per-call runtime extensions for tools — a type-erased type-map that lets callers attach values (auth tokens, session IDs, A2A
context_id/task_id, conversation state) to a tool call and lets tools read them by type, threaded all the way through the agent run loop, MCP tools, and sub-agents. The model never sees them.This supersedes #1537 (rebased onto current
main) and now also incorporates the strengths of the parallel #1953, so the surviving design has both the better naming/type and the full integration. Credit to @redroy44 for the original #1537 design (first commit replays it).Closes #1536. Supersedes #1537 and #1953.
A tool reads them by overriding
call_with_extensions:What's here
The type (
tool/extensions.rs) — a port ofhttp::Extensions'AnyClonepattern: type-erased, cloneable, zero-alloc when empty (Option<Box<HashMap>>), no-opIdHasheroverTypeIdkeys (no SipHash; hardened with a non-panickingwritefallback),insertreturns the displaced value,len/is_empty, plus arequire::<T>() -> Result<&T, MissingExtension>accessor and anassert_send_synccompile guard. The wasm/non-wasm split is deduped into oneAnyClonedefinition viaWasmCompat{Send,Sync}.The dispatch chain gains
call_with_extensionsonTool → ToolDyn → ToolType → ToolSet → ToolServerHandle, each defaulting to the existing extension-freecall(fully backward compatible; the now-deadToolType::callis removed, not#[allow]'d).The agent loop — the piece #1537 was missing: a
tool_extensionsfield onAgentRunneris threaded into the singlerun_single_tooldispatch site (borrowed, not cloned) for both the blocking and streaming drivers, and surfaced viatool_extensions(..)onPromptRequest,TypedPromptRequest, andStreamingPromptRequest.Reaches the real destinations (what makes this end-to-end usable for the motivating A2A/MCP case):
McpTool::call_with_extensionsforwards anrmcp::model::Metafrom the extensions as the request_meta(SEP-1319): per-call auth/session to MCP servers. Re-exported asrig::tool::rmcp::Meta.Agent-as-tool propagates the extensions into the inner run, so delegated agents' tools observe them.Relationship to #1953
#1953 (
ToolCallExtensions) independently rebuilt #1537 with a better type (theIdHasher,len/is_empty, and the collision-free name). This PR adopts all of those (last commit) on top of its own deeper integration — MCP_meta, sub-agent propagation, theTypedPromptRequestentry point,require(), deduped wasm, and broader tests — so #1954 is now a strict superset and #1953 can be closed in its favor.Test plan
cargo test -p rig-core --lib— 1019 pass (type +IdHasherround-trip +len/is_empty+ deep-clone +require; blocking & streaming end-to-end through the real agent loop; sub-agent propagation; sentinel/default paths).cargo test -p rig-core --lib --features rmcp— 1029 pass, incl. a mock-server test asserting_metareaches the server.cargo clippy -p rig-core --all-features --all-targetsclean;cargo fmtclean;cargo check --features wasm --target wasm32-unknown-unknownclean; doctests pass.Generated with AI assistance (Claude).