Skip to content

refactor(client)!: single canonical CompletionClient + AgentClientExt - #2205

Merged
gold-silver-copper merged 3 commits into
mainfrom
refactor/single-completion-client
Jul 24, 2026
Merged

refactor(client)!: single canonical CompletionClient + AgentClientExt#2205
gold-silver-copper merged 3 commits into
mainfrom
refactor/single-completion-client

Conversation

@gold-silver-copper

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

Copy link
Copy Markdown
Contributor

TL;DR (net change): Collapse the duplicate rig-agent CompletionClient
into a supertrait-based AgentClientExt extension trait, leaving rig-core's
CompletionClient as the single canonical trait. rig::client::CompletionClient
still resolves and use rig::prelude::*; keeps working unchanged; no source
changes are required for facade users. No behavior change, no shadow, no
hand-maintained forward.


Follow-up to #2197. That PR, to preserve the pre-split client.agent(m)
ergonomics, introduced a second, same-named CompletionClient trait in
rig-agent that re-declared completion_model, hand-forwarded to the canonical
rig-core trait (with a "add a matching forward if core grows" maintainer
comment), and shadowed the core trait in the facade and preludes. This
removes that duplication while keeping the ergonomics.

What changed

  • One canonical CompletionClient (rig-core). The classic agent() /
    extractor() constructors now live on a new AgentClientExt extension trait
    whose supertrait is rig_core::client::completion::CompletionClient — so it
    builds on completion_model / CompletionModel via the supertrait bound and
    there is nothing to keep in sync if the portable trait grows a method. The
    hand-written forward and its maintainer comment are gone.
  • No trait shadowing. AgentClientExt and CompletionClient share no method
    names, so both resolve without ambiguity. The deliberate same-name shadow in
    the facade client module and both preludes is removed.
  • rig::client re-exports the full rig-core client surface, including the
    canonical CompletionClient (pub use rig_core::client::*;). AgentClientExt
    is a distinct name layered on top, so this is one canonical trait with no
    shadow — not a re-introduced duplicate.
  • Call sites standardized on the prelude. All sites that imported the client
    trait by bare path now use use rig::prelude::*; (or rig_agent::prelude::*
    for crates depending on rig-agent directly).

Migration

Facade users (most): no change required. use rig::prelude::*; gives the
full completion_model + agent + extractor surface, and
rig::client::CompletionClient still resolves. If you imported the client trait
explicitly, use rig::client::{CompletionClient, AgentClientExt}
CompletionClient provides completion_model, AgentClientExt provides
agent / extractor. AgentModelExt::into_agent_builder is unchanged.

Provider clients no longer carry inherent .agent() / .extractor() methods;
they come from AgentClientExt (a blanket impl over every completion client),
which the prelude brings into scope.

Verification

  • cargo check --workspace --all-features --all-targets — clean (0 warnings)
  • cargo clippy --workspace --all-features --all-targets -- -D warnings — exit 0
    (no ambiguous-glob-reexport)
  • cargo test --doc -p rig -p rig-agent -p rig-core --all-features — pass
  • cargo test -p rig-agent --features test-utils, cargo test -p rig-runtime-conformance — pass
  • cargo check -p rig --no-default-features --features {agent,derive,test-utils} — pass
  • cargo check -p rig{-agent,} --target wasm32-unknown-unknown — pass
  • tests/tool_facade_traits.rs covers both the single use rig::prelude::*;
    surface and the explicit rig::client::{CompletionClient, AgentClientExt} pair,
    each exercising completion_model + agent + extractor.
  • A pure-rig consumer (no rig-core dep) compiles the README, prelude, and
    explicit import forms.

Collapse the duplicated `rig-agent` `CompletionClient` trait — which
re-declared `completion_model` and hand-forwarded to core with a
"keep this in sync" maintainer comment — into `AgentClientExt`, an
extension trait that inherits `completion_model` / `CompletionModel`
from its supertrait `rig_core::client::completion::CompletionClient`.
There is now one canonical `CompletionClient` (in `rig-core`); the
classic `agent()` / `extractor()` constructors live on `AgentClientExt`,
and nothing needs forwarding if the portable trait grows.

Drop the `rig::client::CompletionClient` facade path entirely for
purity: the facade `client` module re-exports the portable provider
surface minus `CompletionClient`, which is reached via `rig::prelude::*`
or `rig_core::client::completion::CompletionClient`. This also removes
the deliberate trait shadowing in the facade `client` module and
prelude — `AgentClientExt` and `CompletionClient` share no method names,
so both resolve without ambiguity.

Migrate all call sites to `use rig::prelude::*;` (or
`rig_agent::prelude::*` for crates depending on `rig-agent` directly).
…docs

Review of the exclusion approach found it unnecessary and net-negative:
`pub use rig_core::client::*;` alongside `AgentClientExt` compiles with no
ambiguity (the names never collide), so the hand-curated re-export list only
added drift risk and broke real imports.

- Facade `rig::client` uses `pub use rig_core::client::*;` again, restoring the
  canonical `CompletionClient` path (still a single trait, no shadow).
- Fix root README example: it imported the removed `rig::client::CompletionClient`
  and failed to compile — now `use rig::prelude::*;`.
- Fix MIGRATING.md: the "explicit imports" note suggested
  `rig_core::client::completion::CompletionClient`, which a `rig`-only consumer
  cannot name; use facade paths (`rig::client::{CompletionClient, AgentClientExt}`)
  and drop the now-false "no facade path" wording.
- Fix CHANGELOG wording accordingly.
- Reword the `AgentClientExt` doc: it does not "inherit" `completion_model`;
  the supertrait bound makes it callable inside default bodies, and callers need
  both traits in scope (importing `AgentClientExt` alone does not bring
  `completion_model` into method-resolution scope).

Verified: clippy --all-features --all-targets -D warnings, doctests, and a
pure-`rig` consumer compiling the README/explicit/prelude import forms.
…tClientExt} import

Add a regression test alongside the prelude-surface test that imports the
documented explicit facade pair and exercises completion_model, agent, and
extractor. Guards the restored `rig::client::CompletionClient` path referenced
by README.md / MIGRATING.md, without depending on rig-core.
@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 4155d06 Jul 24, 2026
8 checks passed
@github-actions github-actions Bot mentioned this pull request Jul 24, 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