Skip to content

feat!: split rig-core and rig-agent behind the rig facade - #2197

Merged
gold-silver-copper merged 7 commits into
mainfrom
split/core-agent
Jul 24, 2026
Merged

feat!: split rig-core and rig-agent behind the rig facade#2197
gold-silver-copper merged 7 commits into
mainfrom
split/core-agent

Conversation

@gold-silver-copper

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

Copy link
Copy Markdown
Contributor

Summary

Splits Rig's monolithic core into a portable contracts crate (rig-core) and
the classic agent runtime crate (rig-agent), presented behind the rig
facade. 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/model
    clients, canonical messages/completion values, streaming values, the portable
    tool contracts (PortableTool and friends), memory/vector-store traits,
    telemetry, and WASM support.
  • rig-agent — the classic runtime: builder, AgentRun state 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::Tool is still the classic
    contextual trait; the portable trait is explicit as rig::tool::PortableTool.
  • use rig::client::CompletionClient; — one import gives completion_model,
    agent, and extractor.
  • use rig::prelude::*; — still carries the classic Tool/ToolContext.

Breaking changes

See MIGRATING.md. In short: rig_core::tool::ToolPortableTool;
provider clients no longer have inherent .agent()/.extractor(); direct
rig-core dependents that constructed agents must depend on rig-agent.

Guards

CI enforces the dependency direction (rig-core never depends on rig-agent)
and the rig-agent root export surface, with wasm lanes for core, agent, and
the facade.


Reviewer notes

  • Stacked PR. This is the base of a two-PR stack; feat: add the experimental native ECS runtime (rig-ecs) #2198 adds the experimental
    rig-ecs runtime on top of this branch. Review/merge this first.
  • Enable rename detection when reviewing (e.g. git diff -M25% / GitHub's
    "hide whitespace" + rename view). git does not auto-detect the
    rig-core → rig-agent file moves, so much of the +lines are moved, not new.
  • One non-mechanical change rides along: a telemetry rig.completion_parent
    span marker (via core::telemetry::system_instructions_json) so the runtimes
    enrich a single shared model span instead of creating a duplicate. Intentional,
    covered by a new CompletionTelemetryModel test — flagged here so it isn't a
    surprise during review/bisect.

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.
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.
@gold-silver-copper
gold-silver-copper added this pull request to the merge queue Jul 24, 2026
Merged via the queue into main with commit a3dfd74 Jul 24, 2026
8 checks passed
@github-actions github-actions Bot mentioned this pull request Jul 18, 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
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.

1 participant