feat!: split rig-core and rig-agent behind the rig facade - #2197
Merged
Conversation
gold-silver-copper
force-pushed
the
split/core-agent
branch
5 times, most recently
from
July 22, 2026 15:04
22423f0 to
94b07b2
Compare
Separate the monolithic core into a portable contracts crate (rig-core) and the
classic agent runtime crate (rig-agent), presented behind the rig facade.
- rig-core owns provider/model client traits, canonical messages and completion
values, streaming values, portable tool contracts (PortableTool and friends),
memory/vector-store traits, telemetry, and WASM support.
- rig-agent owns the classic runtime: builder, AgentRun state machine, typed
hooks, contextual tools, memory orchestration, extraction, and the
blocking/streaming driver. It never leaks rig-core at its crate root; portable
contracts are reached through rig_agent::core.
- The rig facade re-exports both at their familiar paths. Facade users need
essentially no source changes: rig::tool::{Tool, ToolContext},
rig::client::CompletionClient (completion_model + agent + extractor from one
import), and rig::prelude::* all keep working.
Breaking changes (see MIGRATING.md): the portable tool trait is PortableTool
(rig_core::tool::Tool removed); provider clients no longer carry inherent
agent()/extractor(); direct rig-core dependents that constructed agents must
depend on rig-agent.
gold-silver-copper
force-pushed
the
split/core-agent
branch
from
July 22, 2026 17:03
94b07b2 to
d1c0d5d
Compare
Three fixes surfaced while reviewing the rig-core/rig-agent split:
- rig-derive: `is_tool_context_type` regressed detection of the
fully-qualified facade path `rig::tool::ToolContext`, breaking the
documented `use rig::tool::{Tool, ToolContext};` compatibility. Widen
the 3-segment arm to accept `rig` alongside `rig_agent`. Add a
`#[rig_tool]` regression case to the tool_facade fixture.
- rig-agent: `StreamingResult`/`DriveStream` were gated on bare
`target_arch = "wasm32"`, diverging from `WasmCompatSend`'s
`all(feature = "wasm", target_arch = "wasm32")` gating. Realign both
so the alias Send-ness matches the trait bound in every config.
- rig-derive: restore the loud `.expect(...)` on tool parameter schema
serialization instead of silently substituting an untyped
`{"type":"object"}` schema.
Serializing an already-parsed serde_json::Value is infallible and its Display impl produces exactly this compact JSON, so the fallible match with its unreachable Err arm was dead code. Collapse to value.to_string(), matching the sibling value_to_json_string helper.
Agent unit tests moved from rig-core to rig-agent, so the `-p rig-core -E 'test(agent::)'` step now selects zero tests and fails under current nextest (exit 4, 'no tests to run'), aborting the job before the full suite runs. Retarget it to rig-agent, which selects the relocated agent:: tests. Add wasm check lanes for rig-agent and the rig facade (matrix), covering the wasm-sensitive code the split introduced (target-gated MCP cancellation dispatch, facade feature forwarding). Both need `--features wasm` for the relaxed async bounds, mirroring the rig-core lane. The existing rig-core wasm job is left untouched so its required status-check name is preserved.
streaming.rs relaxed StreamingResult/DriveStream's `Send` bound only under `all(feature = "wasm", target_arch = "wasm32")`, but rig-agent's `[target.'cfg(wasm32)']` deps enable `rig-core/wasm` without turning on rig-agent's own `wasm` feature. So a bare `cargo check --target wasm32` — the standalone portability lane the Cargo.toml promises — failed with ~25 Send-bound errors; only `--features wasm` passed. Gate these bounds on `target_arch = "wasm32"` alone (matching the rmcp `spawn_local` dispatch), so the standalone wasm build compiles as documented. Native behavior is unchanged. Switch the rig-agent/rig wasm CI lanes to the bare (default-feature) check so the promise stays tested.
… parent CompletionSpanBuilder::build adopted any current span declaring the `rig.completion_parent` marker, then recorded the gen_ai.* completion fields onto it. But tracing::Span::record silently no-ops for fields absent from a span's static metadata, so a span carrying only the marker (as the public contract's docs implied was sufficient) was adopted and then dropped every completion field — losing provider, model, response, usage, and content telemetry with no error, and suppressing the canonical child span too. Require the span to also declare every COMPLETION_PARENT_REQUIRED_FIELDS entry (the canonical new_completion_span! set) before adopting; otherwise fall through to a fresh rig::completions child span so telemetry is never silently lost. Document the contract and add a regression test for a marker-only span. rig's own runtimes already declare the full set.
Merged
This was referenced Jul 27, 2026
gold-silver-copper
added a commit
that referenced
this pull request
Jul 28, 2026
…split MIGRATING.md was scoped to the runtime split (#2197/#2205/#2207) and covered only unreleased work. It said nothing about 0.40, which carried 31 breaking changes -- so a user on 0.39 had a flat CHANGELOG section and nothing else. 0.39 added 2 more and the unreleased section adds 13. Rewrite it as a full guide: per-release sections newest-first, the old runtime-split content absorbed into the unreleased section, and a symbol-reference appendix. The lead section is "Silent behavior changes" -- the seven changes that leave code compiling and behaving differently, which no compiler upgrade will point at. `max_turns` is the headline: it now bounds the exact total number of model calls including the initial call and retries, so an existing `max_turns(n)` means something different than it did. Also there: `max_tokens` now actually forwarded by deepseek, together, hyperbolic, and azure after being silently dropped; three providers switching from empty-text to an error; and the `chat_streaming` -> `chat` span rename. Two breaking changes were missing from the CHANGELOG and are documented here, both found by diffing the public API rather than reading PRs: - #1944 is absent entirely. Its commit body is marked [breaking], and the tree confirms it: RerankError is #[non_exhaustive] and gained ProviderResponse(ProviderResponseError). - #1950 has only its sibling #1951 listed. Output::Unknown is now Output::Unknown(Value), so hosted-tool payloads that were previously discarded at the typed-decode boundary now survive. Symbol names are verified against the tree; the before/after snippets are transcribed from CHANGELOG prose and PR descriptions and are not compiled. See the PR body for the method and its limits.
pull Bot
pushed a commit
to appelgriebsch/rig
that referenced
this pull request
Jul 28, 2026
…split (0xPlaygrounds#2216) * docs(migrating): cover 0.38 through unreleased, not just the runtime split MIGRATING.md was scoped to the runtime split (0xPlaygrounds#2197/0xPlaygrounds#2205/0xPlaygrounds#2207) and covered only unreleased work. It said nothing about 0.40, which carried 31 breaking changes -- so a user on 0.39 had a flat CHANGELOG section and nothing else. 0.39 added 2 more and the unreleased section adds 13. Rewrite it as a full guide: per-release sections newest-first, the old runtime-split content absorbed into the unreleased section, and a symbol-reference appendix. The lead section is "Silent behavior changes" -- the seven changes that leave code compiling and behaving differently, which no compiler upgrade will point at. `max_turns` is the headline: it now bounds the exact total number of model calls including the initial call and retries, so an existing `max_turns(n)` means something different than it did. Also there: `max_tokens` now actually forwarded by deepseek, together, hyperbolic, and azure after being silently dropped; three providers switching from empty-text to an error; and the `chat_streaming` -> `chat` span rename. Two breaking changes were missing from the CHANGELOG and are documented here, both found by diffing the public API rather than reading PRs: - 0xPlaygrounds#1944 is absent entirely. Its commit body is marked [breaking], and the tree confirms it: RerankError is #[non_exhaustive] and gained ProviderResponse(ProviderResponseError). - 0xPlaygrounds#1950 has only its sibling 0xPlaygrounds#1951 listed. Output::Unknown is now Output::Unknown(Value), so hosted-tool payloads that were previously discarded at the typed-decode boundary now survive. Symbol names are verified against the tree; the before/after snippets are transcribed from CHANGELOG prose and PR descriptions and are not compiled. See the PR body for the method and its limits. * docs(migrating): account for PRs merged since the guide was written Rebased onto main and reworked for the five commits that landed after d612ba7. The guide said `dynamic_context` was removed with no replacement. 0xPlaygrounds#2174 removed it and 0xPlaygrounds#2219 restored it, so that section was actively wrong -- it told readers to rewrite working code. Rewritten: the call is unchanged, but it is now a thin wrapper over a private `AgentHook` on the ordinary completion-call lifecycle rather than a separate retrieval pipeline. Two consequences are called out, since neither is visible from the signature: registration order now decides whether an application hook can suppress retrieval, and multiple registrations run sequentially through `HookStack` instead of concurrently. `DynamicContextStore` really is gone, so it moves to the symbol table as removed-with-no-replacement rather than disappearing from the guide alongside the helper that came back. Two new silent behavior changes: - Ollama now honors `max_tokens` (0xPlaygrounds#2185). Anyone who set it, saw no effect and left it there gets truncated responses at a budget they may have chosen long ago. - Multipart tool results reach OpenAI intact (0xPlaygrounds#2217). Mixed text/JSON/rich output now presents as distinct blocks rather than one merged blob, so prompts tuned against the flattened shape are worth re-checking. 0xPlaygrounds#2166 (URL-backed PDF filename) and 0xPlaygrounds#2218 (test-only) are deliberately omitted: the first turned a hard 400 into a working request, so there is nothing to migrate, and the second changes no public surface. Re-verified every symbol the guide names against the current tree, not the tree it was written against: `FinalResponse` and `DynamicContextStore` absent, `dynamic_context` present again, and the rest unchanged. All 9 internal links resolve against 37 headings. * docs(migrating): name the upcoming release 0.41 instead of "unreleased" This is the last PR before 0.41, so the section that has been called "unreleased" is just 0.41. Retitle it and settle the version label everywhere it appeared: the intro range, the navigation table and its anchor (#40--unreleased -> #40--041), the three silent-change subsection tags, the two cross-references from the older sections, and the Version column of the symbol appendix. Rewrapped the three prose paragraphs the substitution left ragged. Two links now wrap inside their link *text*, which renders normally; no link target is split. All 9 internal links still resolve against 37 headings, and no occurrence of "unreleased" remains.
This was referenced Jul 28, 2026
Closed
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
Splits Rig's monolithic core into a portable contracts crate (
rig-core) andthe classic agent runtime crate (
rig-agent), presented behind therigfacade. This is the base of a two-PR stack — the experimental ECS runtime
lands separately in #2198 (do not merge that until this one lands).
rig-core— portable, runtime-independent contracts: provider/modelclients, canonical messages/completion values, streaming values, the portable
tool contracts (
PortableTooland friends), memory/vector-store traits,telemetry, and WASM support.
rig-agent— the classic runtime: builder,AgentRunstate machine,typed hooks, contextual tools, memory orchestration, extraction, and the
blocking/streaming driver. Its crate root never re-exports
rig-core;portable contracts are reached through
rig_agent::core.rig— the facade. Re-exports both at their familiar paths.Compatibility (facade users need ~no source changes)
use rig::tool::{Tool, ToolContext};—rig::tool::Toolis still the classiccontextual trait; the portable trait is explicit as
rig::tool::PortableTool.use rig::client::CompletionClient;— one import givescompletion_model,agent, andextractor.use rig::prelude::*;— still carries the classicTool/ToolContext.Breaking changes
See
MIGRATING.md. In short:rig_core::tool::Tool→PortableTool;provider clients no longer have inherent
.agent()/.extractor(); directrig-coredependents that constructed agents must depend onrig-agent.Guards
CI enforces the dependency direction (
rig-corenever depends onrig-agent)and the
rig-agentroot export surface, with wasm lanes for core, agent, andthe facade.
Reviewer notes
rig-ecsruntime on top of this branch. Review/merge this first.git diff -M25%/ GitHub's"hide whitespace" + rename view). git does not auto-detect the
rig-core → rig-agentfile moves, so much of the +lines are moved, not new.rig.completion_parentspan marker (via
core::telemetry::system_instructions_json) so the runtimesenrich a single shared model span instead of creating a duplicate. Intentional,
covered by a new
CompletionTelemetryModeltest — flagged here so it isn't asurprise during review/bisect.