Skip to content

Simplify tool execution and hook APIs#2132

Open
gold-silver-copper wants to merge 14 commits into
mainfrom
feat/issue-2126-tool-api
Open

Simplify tool execution and hook APIs#2132
gold-silver-copper wants to merge 14 commits into
mainfrom
feat/issue-2126-tool-api

Conversation

@gold-silver-copper

@gold-silver-copper gold-silver-copper commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Description

Closes #2126 by replacing Rig's parallel tool execution, metadata, result, dynamic-dispatch, and hook-control APIs with one canonical structured path:

Tool::call(&mut ToolContext, Args) -> Result<Output, ToolExecutionError> → private erased dispatch → ToolResult → hooks, telemetry, and provider presentation.

This is intentionally a one-step breaking replacement. No compatibility aliases, forwarding wrappers, or second dispatch hierarchy remain.

Root concerns addressed

  • One authoring and execution contract. Typed tools implement one call method. ToolSet::execute and ToolServerHandle::execute are the public dispatch surfaces, object-safe erasure is crate-private, and runtime-defined tools use DynamicTool.
  • One context path. ToolContext carries typed inbound values and host-only result metadata. Dispatch snapshots inbound context once, isolates tool-local mutation, clears stale result metadata, and publishes only metadata from the current execution.
  • One error concept. ToolExecutionError carries normalized kind, detailed diagnostics, retryability, code, HTTP status, refusal disposition, and a downcastable concrete source. Detailed diagnostics are model-visible by default so models can self-correct; sensitive tools can explicitly call redact_model_feedback, with_model_feedback, or with_model_output.
  • One output representation. ToolOutput is the canonical ordered container of ToolResultContent blocks. Ordinary Rust strings remain literal text, explicit serde_json::Value remains JSON even when it is a JSON string, and directly authored rich blocks retain their type and order. Rig never reparses ordinary strings to guess structured or multimodal content.
  • One runtime observation. ToolResult keeps success, execution failure, intentional refusal, and framework skip distinct through a private mutually exclusive disposition, without creating another author-facing outcome hierarchy.
  • Typed hook control. Event-specific hook methods return CompletionCallAction, ToolCallAction, ToolResultAction, InvalidToolCallAction, or ObservationAction, making unsupported event/action combinations unrepresentable. Completion patches merge, rewrites chain through nested stacks, and terminal actions short-circuit with consistent streaming/non-streaming behavior.
  • Raw-versus-presentation integrity. Result rewrites affect model presentation only. Raw results and execution metadata are recorded before a result hook can stop the run, preserving policy and telemetry invariants.
  • Turn-scoped registration. Each model turn snapshots the exact advertised definitions and executable registrations. Mid-turn registry mutation cannot make Rig execute a different tool than the one the model saw, and active_tools filters definitions and executable handles together.
  • Canonical JSON arguments. Providers normalize wire-format argument strings at their boundaries. Inside the runtime, ToolFunction.arguments is canonical JSON, including strings, scalars, and arrays, across normal dispatch, invalid-call repair, output-tool finalization, and streaming eviction/EOF paths.
  • MCP correctness. MCP calls accept absent/null arguments or JSON objects and reject arrays/scalars with actionable feedback. Structured content remains typed JSON, real rich blocks retain order, generated text fallbacks are not duplicated, and raw response metadata remains available through ToolContext.
  • MCP refresh safety. Initial discovery and refresh are bounded by timeout. Network fetches happen outside the registry lock, versioned commits prevent stale responses from rolling back newer state, ownership-aware reconciliation protects competing/local registrations, disconnected owners are retired before their generations can protect names, disappeared owners can be reclaimed, and server order is preserved.
  • Provider boundaries. Gemini Interactions preserves singleton JSON scalars and tagged mixed/rich ordering. Gemini Generate Content preserves provider function-call correlation IDs, carries them into matching function responses, and sends structured output and ordered media parts without unsupported synthetic references. OpenAI-compatible streaming finalization now treats valid object, array, scalar, and string arguments identically at eviction and EOF.
  • Explicit derive context. #[rig_tool] automatically recognizes fully qualified rig::tool::ToolContext and rig_core::tool::ToolContext. Imported aliases use #[rig(context)], while an unrelated user type merely named ToolContext remains an ordinary model argument. The one mutable runtime context is forwarded and excluded from generated arguments/schema.

Breaking migration

Removed API Replacement
Multiple Tool::call* methods and associated type Error One Tool::call(&mut ToolContext, Args) -> Result<Output, ToolExecutionError>
ToolDyn and public erasure roles Private erasure plus public DynamicTool
ToolError, ToolFailure, and parallel failure types ToolExecutionError / ToolErrorKind
ToolReturn, ToolExecutionResult, and parallel outcome enums Ordinary tool Result plus hook-facing ToolResult
ToolCallExtensions / ToolResultExtensions ToolContext
ToolSet::call* / ToolServerHandle::call_tool* execute
Generic Flow / StepEvent control Lifecycle-specific hook methods and action types
AgentBuilder::tools(Vec<Box<dyn ToolDyn>>) Repeated .tool(...) calls for typed tools, or dynamic_tools(Vec<DynamicTool>) for runtime-defined callbacks
Retrieval-backed dynamic_tools retrieved_tools
Retrieval-backed ToolSetBuilder::dynamic_tool(ToolEmbeddingDyn) retrieved_tool(ToolEmbedding); runtime-defined callbacks use dynamic_tool(DynamicTool)

Behavioral invariants covered

  • Text that looks like JSON remains text; explicitly structured strings, scalars, arrays, and objects remain JSON.
  • Explicit multimodal content and block order are preserved or rejected when a provider cannot represent the shape.
  • Presentation rewrites never mutate raw results, structured classification, per-dispatch context, policy data, or telemetry.
  • Tool refusal remains distinct from ordinary permission failure and framework skip.
  • Result metadata is host-only and isolated per dispatch.
  • Streaming and non-streaming runs share the same driver, hook semantics, repair behavior, and scalar argument handling.
  • Tool definitions remain ordered and deduplicated across static, dynamic, retrieved, local, and MCP registrations.
  • Invalid tool calls may defer to later hooks; each explicit resolution is terminal.
  • The tool advertised in a turn is the exact registration executed in that turn.

Regression hardening

  • Added compile-pass/fail coverage for qualified, marked-alias, unrelated-name, immutable, owned, duplicate, and misplaced ToolContext parameters.
  • Added regressions for literal versus explicit JSON strings, ordered rich output, detailed/default-redacted failures, scalar arguments, invalid-call repair parity, scalar output tools, raw telemetry, nested hook chaining, and exact registration snapshots.
  • Added MCP regressions for strict arguments, structured/rich response conversion, timeout classification, initial discovery timeout, concurrent stale refreshes, competing owners, disconnected-owner reclamation, owner disappearance, local replacement, and server ordering.
  • Added Gemini regressions for scalar structured content, mixed rich ordering, unsupported media rejection, provider function-call correlation IDs, and unreferenced inline function-response media.
  • Re-recorded the two touched Bedrock cassettes and the Gemini hybrid tool-result cassette against the live provider APIs. Live recording exposed and fixed Bedrock object-only JSON tool results, Gemini function-call ID loss and unsupported synthetic media references, and binary AWS event-stream scrubbing; replay and cassette safety checks pass.

Verification

  • cargo fmt --all -- --check
  • git diff --check
  • cargo clippy --workspace --all-targets --all-features -- -D warnings
  • cargo test — passed across the default workspace suite and provider replay targets
  • cargo test -p rig-core --all-features --lib — 1,348 passed, 8 ignored
  • cargo test -p rig-derive --tests — passed, including ToolContext trybuild and runtime coverage
  • Targeted Bedrock, Vertex AI, Gemini gRPC, MCP timeout/cancellation, registry snapshot, and tool-result conversion regressions — passed
  • cargo check -p rig-core --lib --target wasm32-unknown-unknown --features wasm
  • cargo doc --workspace --no-deps
  • cargo nextest run --all-features --retries 2 — 1,121 passed, 220 skipped
  • Targeted live record and replay runs for both Bedrock cassettes and the Gemini hybrid tool-result cassette — passed
  • Cassette registration and secret-safety checks — passed
  • Removed-symbol search is empty outside historical changelogs
  • Fresh independent full-diff, runtime, and provider/derive audits are clean

Type of change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation update

@gold-silver-copper gold-silver-copper force-pushed the feat/issue-2126-tool-api branch from deb0d39 to 788e4ef Compare July 14, 2026 02:26
* preserve typed tool errors at dispatch

* fix streaming tool commit event ordering
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.

Simplify tool execution, result, metadata, and hook APIs

1 participant