rig-agent 0.42.0

Rig's classic agent runtime.
Documentation

rig-agent

rig-agent contains Rig's classic agent runtime: builders, the serializable sans-I/O run state, blocking and streaming drivers, typed hooks, contextual tools, extraction, and runtime integrations.

Most applications should use the root rig facade, where this runtime remains enabled by default. Low-level provider and backend contracts live in rig-core.

Direct users import construction and prompting explicitly:

use rig_agent::prelude::*;
use rig_core::{client::ProviderClient, providers::openai};

let client = openai::Client::from_env()?;
let agent = client.agent(openai::GPT_5_2).build();
let answer = agent.prompt("Explain ownership briefly.").await?;

Runtime model routing

High-level agents are concrete values: the provider model is erased once into an opaque, cloneable ModelHandle, whose ProviderCapabilities snapshot is captured by value at erasure. Provider authors still implement the typed CompletionModel trait, and direct completion or stream calls (plus each provider's raw_* escape hatches) retain their provider-specific behavior.

Replace the default on one agent value with set_model or set_model_handle, or change one run's default candidate with using_model. Implement AgentHook::on_model_select to route before every model call. Model selection hooks chain in registration order, with the last selection winning and a stop terminating the run. Per model-call boundary, completion-call hooks resolve first and their merged RequestPatch is passed to the selection event (ModelSelection::request_patch); only then does request preparation run against the selected model's captured capabilities and issue the attempt. A selected handle cannot change while its future or stream is in flight. Retries and calls after tool execution are new boundaries and may select another handle.

Hooks attached through AgentBuilder::add_hook apply to every later runner from that agent; hooks appended to a prompt or runner apply only to that run. using_model changes the run's initial candidate but does not suppress routing hooks. To force a model, omit routing hooks or append a final hook that always selects it. Blocking and streaming prompts share this lifecycle: Stop cancels before request preparation and provider execution, and dropping an in-flight attempt still cancels it by dropping its retained future or stream.

Extractors support the same run-local choice through extractor.using_model(handle).extract(...) or using_model_value(model). That handle is the default candidate for each extraction retry, routing hooks may replace it, and the extractor's default is unchanged for later calls.

#[derive(Clone)]
struct RouteModels {
    fast: ModelHandle,
    strong: ModelHandle,
}

impl AgentHook for RouteModels {
    fn on_model_select(
        &self,
        context: &HookContext,
        _event: ModelSelection<'_>,
    ) -> ModelSelectionAction {
        if context.turn() == 1 {
            ModelSelectionAction::select(self.fast.clone())
        } else {
            ModelSelectionAction::select(self.strong.clone())
        }
    }
}

let fast = ModelHandle::named("fast", fast_model);
let strong = ModelHandle::named("strong", strong_model);
let agent = AgentBuilder::from_model_handle(fast.clone())
    .tool(search_tool)
    .build();

let answer = agent
    .prompt("Research, then synthesize")
    .max_turns(3)
    .add_hook(RouteModels {
        fast: fast.clone(),
        strong: strong.clone(),
    })
    .await?;

Handles and routing hooks contain live clients, callbacks, and policy state, so handles, hook stacks, and hook actions are deliberately not serializable; persist an application model identifier and resolve it to a handle at runtime. Clones share the retained model safely, while replacing an agent clone has ordinary value semantics. Concurrent runs keep independent default and hook-stack snapshots; explicitly synchronized state captured by a hook follows that hook's own clone semantics. See the credential-free runtime_model_routing example for a complete two-model tool round trip.

Portable tools implement rig_core::tool::PortableTool and work in both runtimes. Classic tools that need mutable per-call state implement rig_agent::tool::Tool and receive &mut ToolContext.

Target support

Tier Target Status
1 native (linux / macOS / windows, x86_64 and aarch64) Full support, all features including rmcp
2 wasm32-unknown-unknown (browser) Supported, with no feature flags to set; the rmcp feature is not available
wasm32-wasip1 / wasm32-wasip2 (WASI) Not supported
wasm32-unknown-emscripten Not supported

Building for wasm32-unknown-unknown is the entire opt-in — there are no wasm feature flags anywhere in the workspace. rig-core relaxes its WasmCompat* bounds from the target alone.

Wasm gates name a target_os (all(target_arch = "wasm32", target_os = "unknown")) rather than a bare target_arch = "wasm32", because the latter also matches WASI, which has no JS host. WASI itself does not build: rig-core depends unconditionally on reqwest, which pulls hyper/socket2 and a tokio feature set WASI rejects. Supporting it would mean making reqwest optional and adding a wasi:http client behind rig_core::http_client — a project, not a cfg fix.

rmcp is native-only. rmcp's ClientHandler is declared Sized + Send + Sync + 'static unconditionally — its local feature relaxes the future bounds but not the handler itself — while this crate's handler owns a tool registry whose Arc<dyn ErasedTool> is deliberately neither Send nor Sync on wasm. Enabling rmcp on a wasm target fails with a single explanatory compile_error! rather than a wall of trait errors.