Rig v0.41.0 released! #2225
gold-silver-copper
announced in
Announcements
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
43 PRs merged since 0.40. THANK YOU to everyone who contributed, tested, reported issues, and provided feedback on the API changes 🫡
The highlights of 0.41 are as follows: Rig's monolithic core is now split into a portable contracts crate and a runtime crate behind the
rigfacade. Truly local inference viarig-candle. Tool execution and hooks collapsed from several parallel APIs into one.AgentRunnerbecame the single execution path. Browser WASM is a supported target with a documented matrix, and sensitive telemetry content is opt-in rather than absent.This is a big release for architecture, so We wrote a full migration guide for it —
MIGRATING.md— covering 0.38 through 0.41. We will start providing a full migration guide for each release going forward.The Crate Split
The most important change of 0.41 is the split of the monolithic core into two crates presented behind the
rigfacade (#2197).rig-core— portable, runtime-independent contracts: provider and model clients, canonical messages and completion values, streaming values, portable tool contracts, memory and vector-store traits, telemetry, and WASM support.rig-agent— the classic agent runtime: the builder, run state machine, typed hooks, contextual tools, memory orchestration, extraction, and the blocking and streaming drivers.rig— the facade you depend on, re-exporting both at their familiarrig::…paths.If you depend on the
rigfacade, you need essentially no source changes.rig::…paths,rig::prelude::*, andrig::tool::{Tool, ToolContext}all keep working. The split matters if you depended onrig-coredirectly: it is portable-only now, so agent construction moved torig-agent.The portable, context-free tool contract is now named
PortableTool, freeingrig::tool::Toolto unambiguously mean the classic contextual trait. And provider clients no longer carry inherentagent()/extractor()methods (#2205): there is one canonicalCompletionClientinrig-coreprovidingcompletion_model, with the classic constructors on a newAgentClientExt.use rig::prelude::*;brings both into scope, so the pre-split client surface is still one import. Portable items are reached through the explicitrig_agent::corenamespace.Local Inference with
rig-candlerig-candleis aCandle-backed local inference runtime supporting Llama, SmolLM2, and Qwen3, shipping with a browser WASM chat example (#2155, #2214). This is a very exciting addition, because it unlocks truly local inference for Rig. Other local model providers such as Ollama require the user to run a local server which... serves... the model. This is acceptable and even preferred for many use cases, but less than ideal or even untenable for others.Candleallows users to embed model weights directly into their application, no server required, and it even works in your browser through WASM!The current state of
rig-candleis experimental, but we are very excited about the possibilities that it unlocks, and look forward to developing it further.Candlesupports a number of interesting model types beyond LLMs and Image Generation models, and we hope to support them too!Browser WASM
The WASM support matrix is now explicit and true (#2213):
wasm32-unknown-unknown(browser) is supported, with no feature flags at all. Every wasm feature flag in the workspace is gone — building for the target is the entire opt-in. Dropfeatures = ["wasm"]from your dependency lines; nothing replaces it.Telemetry
Sensitive span content is opt-in rather than simply absent (#2151). 0.40 stopped recording GenAI input and output messages to avoid accidental content disclosure; 0.41 gives that an explicit switch for the people who need it.
The completion-parent contract also has a single declarative source (#2115, #2208). This fixes a genuinely nasty failure mode:
tracingbakes a span's field set into static metadata andSpan::recordsilently no-ops on undeclared fields, so a hand-mirrored field list that dropped one field lost that telemetry with no error — and the contract had been duplicated in six places. The newcompletion_parent_span!macro declares the marker and every requiredgen_ai.*field in one place, exact-set tests pin it so the lists cannot drift, and a parent carrying the marker but missing a field now emits awarn!naming what is missing instead of degrading silently.One Tool Path, One Hook Vocabulary
0.40 introduced structured tool results. 0.41 removes the parallel APIs that had grown around them (#2132).
Typed tools now implement exactly one method:
Tool::call(&mut ToolContext, Args) -> Result<Output, Error>. Author-facing errors stay typed for ordinary?propagation until private runtime erasure normalizes them intoToolExecutionError.classify_error,call_with_extensions, andcall_structuredare gone.ToolCallExtensionsandToolResultExtensionscollapse into a singleToolContextthat carries inbound values and host-only result metadata, snapshotted once per dispatch.Dispatch consolidated too:
ToolSet::executeandToolServerHandle::executereplace the three-waycall/call_with_extensions/call_structuredsplit on each.ToolDynleft the public API in favor ofDynamicTool, andToolSetis now the single ordered registry — itsget_tool_definitionsanddocumentsare synchronous and infallible, and registration no longer returns an artificialResult.Serializable outputs convert once into canonical
ToolOutputcontent blocks: strings stay literal text, explicitserde_json::Valuestays JSON, and multimodal tools return typedToolResultContent. MCP responses preserve ordered text and image content, retain unsupported and future blocks as typed JSON, and attach the rawCallToolResultandstructuredContenttoToolContext.Hooks moved in the same direction.
AgentHook::on_event,StepEvent, and the singleFlowenum are replaced by event-specific methods with their own action types —CompletionCallAction,ToolCallAction,ToolResultAction,InvalidToolCallAction,ObservationAction— which makes invalid event/action combinations unrepresentable rather than a runtime failure.Hooks are also provider-independent now (#2176).
AgentHookandHookStackno longer carry a completion-model type parameter, andCompletionResponseEvent/StreamResponseFinishexpose canonical Rig content, usage, prompt, and message IDs instead of typed provider responses:Passive RAG now runs on that same lifecycle.
AgentBuilder::dynamic_contextandExtractorBuilder::dynamic_contextare unchanged at the call site, but they are backed by a hook rather than a separate retrieval pipeline inside request construction (#2219). Query selection, sample forwarding, document formatting, ordering, and failure-before-I/O all behave as before. Two consequences follow from it being an ordinary hook: register a stop policy beforedynamic_contextif it should be able to suppress retrieval, and multiple registrations run sequentially throughHookStackrather than concurrently. Anything beyond the built-in behavior — filtering, reranking, caching, per-turn policy — is your ownAgentHook.New in 0.41: response retry hooks (#2182), so retry policy is expressible in the same vocabulary as everything else.
AgentRunnerIs the Only Execution PathConfigured agents now execute through one path (#2161). The raw
CompletionandStreamingCompletiontraits and theirAgentimplementations are removed, agent execution state is private, andExtractorroutes through the full hook lifecycle like everything else.The reason is that the old raw path silently bypassed hooks, so whether your guardrails ran depended on which method you happened to call. Hook-free transport is still available — deliberately and explicitly — by starting from
model.completion_request(prompt)on the providerCompletionModel.Derive Macros
#[rig_tool]required-ness is now derived from the parameter types, and the advertised schema always agrees with the deserializer (#2207). Previously a parameter left out of an explicitrequired(...)was advertised as optional while the generated deserializer still demanded it — failing at runtime whenever the model legitimately omitted it.Now, without an explicit
required(...), non-Optionparameters are required andOption<T>parameters are optional. Several silent failures became compile errors: listing anOption<T>inrequired(...), naming a parameter that does not exist, malformed or duplicate attribute entries, and a field carrying twoembed_withattributes where the first previously won silently.Dependency hygiene improved as well. Macro-generated code resolves
serde,serde_json, andschemarsthroughrig-core's re-exports, so crates using#[rig_tool]or#[derive(Embed)]no longer need directserde/serde_jsondependencies.Embedemits fully qualified impls and no longer needs the trait imported at the call site. Generatedparameters()builds its schema once viaLazyLockand contains noexpect, so downstream crates denyingclippy::expect_usedare unaffected.Providers
Doubleword joins as a new provider with cassette coverage from day one (#2163).
OpenAI gained support for GPT-5.6 models and reasoning controls (#2106) and now exposes complete Responses reasoning metadata (#2112).
0.40 consolidated the OpenAI-compatible completion models onto one shared engine. 0.41 does the same for embeddings (#2157). Together, OpenRouter, and Mistral embeddings now run on a shared generic transport behind an explicit
OpenAIEmbeddingsCompatiblecapability that carries each provider's real policy — endpoint selection, supported fields, usage requirements, and dimension spelling — instead of every provider hand-mutating aserde_json::Value. That policy is specific where the APIs are: Together rejects inheritedencoding_format/userfields, OpenRouter serves/api/v1/embeddings, and Mistral emitsoutput_dimensiononly for the Codestral embed models, capped at the documented 3072. Base64 response encoding is now rejected consistently before the HTTP call across OpenAI, OpenRouter, Together, Mistral, and Llamafile, since the shared response parser reads numeric vectors.Provider correctness fixes worth calling out:
max_tokensasoptions.num_predictin native requests (#2185). The native/api/chatAPI has no top-levelmax_tokens, so the value was being discarded — if you set it previously and saw no effect, it starts applying now.filenamefor URL-backed PDFs in Responses requests (#2166), which the API had been rejecting outright with a 400.encrypted_contentas an empty string on completed reasoning items; Rig treated any present value as encrypted reasoning and emitted a second, empty event alongside the visible one. Empty encrypted reasoning is now filtered on both the streaming and non-streaming paths (#2209), and non-empty encrypted payloads are untouched.strict: nullin Responses tool definitions and maps it tofalse(#2178); deserializing it directly asboolhad been rejecting otherwise-valid responses.ProviderResponsewith the provider's status and body intact (#2147). Untagged deserialization had been accepting an error object as an empty success response, so what you got back was a generic missing-image error with the real cause discarded.On the security side, the AWS Bedrock and S3 Vectors integrations no longer enable the AWS SDK's legacy Rustls connector (#2152), removing vulnerable
rustls-webpki0.101 from their active dependency graphs while keeping the modern default HTTPS client.rmcpwas also bumped to latest (#2103).Three Fixes That Change Behavior
These are worth reading even if nothing in your code stops compiling.
Extractor usage accounting was undercounting spend (#2109).
extract_with_usagedocuments that usage accumulates across retry attempts, but noExtractionErrorvariant carried usage — so a completion that billed tokens and then failed extraction (the model never calledsubmit, or the arguments failed to deserialize) silently dropped its usage. Withretries(3)and two failed attempts before success, anyone metering spend was undercounting by two full completions. Usage now survives extraction failure and accumulates on every attempt. Attempts whose completion call itself errors contribute zero, and when every attempt fails the error carries no usage; both caveats are now stated in the docs rather than implied. Single-attempt extractions — the common case — report identical numbers to before.Two smaller notes from the same work, which followed collapsing the extractor's four near-identical retry loops into one helper (#2107): the retry loop no longer deep-copies the entire chat history on every attempt, which matters with long multimodal histories; and one log string changed —
"Multiple submit calls detected, using the last one"is now"using the first one", because the code has always deserialized the first. If you match that string in log tooling, update it.Structured-output tools could shadow real tools (#2146). Tool output mode pins its synthetic tool name for the lifetime of a run. If a mutable tool server, retrieval, or an MCP refresh later made a real tool effective under that reserved name, Rig detected the collision but only logged a warning — then advertised both definitions and intercepted any matching call as final structured output. A call intended for the real tool could therefore end the run. Request preparation now fails closed with an actionable
CompletionError::RequestErrorbefore any provider I/O, naming the reserved tool. A colliding tool that is filtered out byactive_toolsis not a false positive; the run fails only once it actually becomes effective.Multipart tool results reach OpenAI intact (#2217). Conversion was concatenating multipart tool results before provider-specific wire normalization, destroying the original block boundaries — so
tool_result_array_contentemitted a single already-flattened item. Ordered text, JSON, and image blocks are now preserved in the Responses API, and Chat Completions keeps separate text parts when array-form results are enabled. Single text or JSON results keep their compact string form, and the newline-flattened behavior remains for providers that require string content. If you tuned prompts against the flattened shape, they are worth re-checking.API Cleanup
Rig is still pre-1.0, so we continue removing stale APIs rather than carrying permanent aliases.
PromptError,StructuredOutputError, andVectorStoreErrorare now#[non_exhaustive](#2114), so downstreammatchexpressions need a wildcard arm. Conversation memory load failures surface as the typedPromptError::MemoryErrorrather than being flattened intoCompletionError::RequestError.Internally, the
HttpClientExtimplementations forreqwest::ClientandClientWithMiddlewareare now generated from one macro with shared response conversion, removing 139 net lines while preserving request, error, header, body, and streaming behavior (#2113).One note for contributors:
CONTRIBUTING.mdno longer asks PR authors to add AI-assistance disclosure boilerplate (#2159). The same review, quality, and testing standards apply to every contribution regardless of how it was written.Upgrading
Six breaking changes ship in 0.41: the crate split, the client trait consolidation, the tool and hook API rework,
AgentRunneras the only execution path, non-exhaustive core errors, and the wasm feature-flag removal.Please read the new
MIGRATING.md. It covers 0.38 through 0.41 with per-release sections, before-and-after code, and a symbol reference table for anything you cannot find by name anymore.Huge thank you to everyone who made 0.41 happen:
All reactions