Effect bus: the crate split (rig-bus, rig-effect-log, registry to rig-agent) - #2445
Merged
Merged
Conversation
`Key<F>` is a `HandlerKey` plus a type-level proof of the family under
it; it holds nothing of the runtime. It moves from `bus` to `effect`
beside `HandlerKey`, with `model_key`/`tool_key`. The two doc sentences
that linked `Dispatcher::bind`/`handle` are prose now — the vocabulary
cannot link downstream once the runtime is its own crate, which is what
this move prepares (S1 of effect-bus-crate-split-pr.md).
No behaviour change; paths only. Public break: `rig_core::bus::{Key,
model_key, tool_key}` are `rig_core::effect::{Key, model_key, tool_key}`.
`RegisteredTool`, `ToolSet`, `ToolDispatch`, `dispatch_tool`,
`ToolCatalog` and `ToolLease` move from `rig_core::tool` to
`rig_agent::tool` (`registry`, `catalog`), next to `ToolServer`, their
only driver. rig-core's tool module keeps tool authoring: `Tool`,
`ToolEmbedding`, `ErasedTool`, `DynamicTool`, the portable module,
`ToolContext`, the output and result types. The seam a registry needs
goes public: `LivenessFn`, `DynamicTool::into_parts`,
`PortableDynamicTool::into_parts`. The registry tests move with the
registry, paths only. The facade exposes the registry under `agent`.
Migration: `rig_core::tool::{RegisteredTool, ToolSet, ToolDispatch,
ToolCatalog, ToolLease, dispatch_tool}` → `rig_agent::tool::…` (the
`ToolSet`/`dispatch_tool` paths already resolved there).
Also fixes the two `bus/mod.rs` intra-doc links to `HandlerKey` that
S1 left unqualified (CI's doc job caught them).
The effect bus runtime — Bus, Dispatcher, Registrar, BusDriver, BusConfig,
Pending, EffectStream, Handle and the typed views, Typed, wrap_stream, the
sync shim, the loom models — moves from rig_core::bus to a new crate,
rig-bus, depending on rig-core and nothing else rig-shaped (futures,
tracing, serde). rig-core keeps the vocabulary and, under a new `serve`
module, what a handler author implements: Serve, OutcomeSink, the one
erasure ErasedHandler, serve_inline, the adapters, and StreamWriter
(an inherent method on OutcomeSink cannot live downstream, so the writer
stays with the sink; the prompt had it moving).
Two seams are now explicit. The sink's driver-facing constructors
(OutcomeSink::unary/stream/with_tap, ErasedHandler::handle, StreamTap,
finish_unary, stream_truncated) are public in rig_core::serve: what a bus
driver builds to hand a handler its sink — rig-bus is one such driver, an
ECS schedule is another. The driver records through a `Recorder` trait
(handlers, begin, keep_events, event, resolve), held as closures like the
sink's taps, so the driver names no recorder type; EffectLogRecorder and
EffectLogReplayer ride in rig-bus for this commit (the replayer needs
BusDriver) and move to rig-effect-log next.
Every guard follows the code: one-erasure scans rig-bus, its erasure file
is serve/handler.rs, the no-unsafe scan covers rig-bus and rig-core's
serve module; stream_ids allows serve/{handler,writer}.rs; the Bevy
fixture's rig side is rig-core and rig-bus; the dependency-graph guard
asserts rig-bus is runtime-free and rig-core does not depend on it;
provider_layout exempts the new package. CI: the loom job runs rig-bus
and rig-agent; the wasm matrix checks rig-bus. The facade exposes
rig::bus as a named module like rig::run.
Bench, release: unary dispatch median 625ns (p90 708ns). Publish
dry-run of rig-bus fails on the registry's rig-core (no `serve`, no
`effect` published yet), as rig-agent's does on rig-reqwest.
Migration: rig_core::bus::{Bus, Dispatcher, Registrar, BusDriver,
BusConfig, Pending, EffectStream, Handle, *Handle, Typed, wrap_stream,
EffectLogRecorder, EffectLogReplayer} → rig_bus::…;
rig_core::bus::{Serve, OutcomeSink, SinkClosed, ErasedHandler,
serve_inline, events_from_response, StreamWriter, adapters} →
rig_core::serve::…
EffectLog, LogHeader, EFFECT_LOG_FORMAT and stable_hash leave
rig_core::effect; EffectLogRecorder (a rig_bus::Recorder) and
EffectLogReplayer leave rig-bus. The new crate depends on rig-core and
rig-bus and is the persistence story over both: what a host that saves
and restores in-flight effects depends on. EffectRecord stays vocabulary
in rig-core. rig-agent's record_effects/take_effect_log/run_spec_hash/
check_replayable keep their signatures over the new paths.
The record/replay tests over a live bus move from rig-bus's tests to the
log crate (a dev-dependency back onto rig-bus builds the crate twice and
the trait impls no longer line up, so the tests live where the types
do); the helpers they use are copies. The log round-trip test moves with
EffectLog. The guards scan four crates; the dependency-graph guard
asserts rig-effect-log is runtime-free and sits above rig-bus, and
neither rig-core nor rig-bus depends on it; provider_layout exempts the
package; the wasm matrix checks it; the facade exposes rig::effect_log.
The trybuild diagnostic re-pinned (the impl list no longer names the
replayer). Publish dry-run fails on the registry's missing rig-bus, as
expected until rig-bus publishes.
Migration: rig_core::effect::{EffectLog, LogHeader, EFFECT_LOG_FORMAT,
stable_hash} and rig_bus::{EffectLogRecorder, EffectLogReplayer} →
rig_effect_log::…
4 tasks
gold-silver-copper
marked this pull request as ready for review
September 3, 2026 08:36
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.
Effect bus: the crate split (PR #2445)
Stacked on #2443 (
feat/effect-bus); base branchfeat/effect-bus, lands by fast-forward push (the repository is squash-only), retarget tomainwhen #2443 merges. Executing prompt:effect-bus/effect-bus-crate-split-pr.md(audited 2026-09-03 against5a8958620). Toolchain1.94.0. Four code commits and this description.What the PR does
Draws the crate line where the dependency edges already were. rig-core keeps the vocabulary and what a handler author implements; a new
rig-busis the runtime; a newrig-effect-logis record and replay; the tool registry moves to rig-agent, the only crate that drives it. Nothing in rig-core names a type from the two new crates, and a guard says so. A Bevy runtime (rig-bevy, next) importsrig-coreandrig-busand nothing agent-shaped.Crate layout as built
rig-coreeffect(the vocabulary:EffectKind,Outcome,FamilyDescriptor,Family,Key<F>,EffectRecord,model_key/tool_key) andserve(Serve,OutcomeSink, the one erasureErasedHandler,serve_inline,StreamWriter, the adapters, the driver seam)rig-busBus,Dispatcher,Registrar,BusDriver,BusConfig,Recorder,Pending,EffectStream,Handleand the typed views,Typed,wrap_stream; the sync shim and the loom modelsrig-effect-logEffectLog,LogHeader,EFFECT_LOG_FORMAT,stable_hash,EffectLogRecorder,EffectLogReplayerrig-agentRegisteredTool,ToolSet,ToolDispatch,dispatch_tool,ToolCatalog,ToolLeasebesideToolServerWhy the line is here. The vocabulary must sit with the domain types (orphan rule on
Familyimpls and theFrom<XError> for ErrorReportconversions). The adapters implementServeover rig-core's six traits, so they stay withServe. The runtime knows only the vocabulary andErrorReport. The registry was the one type that straddled the line: it holds anErasedHandlerand mints aKey, and its only driver isToolServer.Two seams made explicit.
rig_core::serve):OutcomeSink::unary,OutcomeSink::stream,OutcomeSink::with_tap,OnOutcome/OnEvent,ErasedHandler::handle,HandlerFuture,StreamTap,finish_unary,stream_truncated. What a bus driver builds to hand a handler its sink and observe what it answers. rig-bus is one such driver; an ECS schedule is another. Public, documented, not hidden.rig_bus::Recorder):handlers,begin,keep_events,event,resolve.BusDriver::record_totakes anyRecorder + Clone + Send + Syncand holds it as closures, the way the sink holds its taps, so the driver names no recorder type.EffectLogRecorderimplements it.Key<F>is vocabulary. A typed key is aHandlerKeyplus a type-level proof of the family; it holds nothing of the runtime, so it lives inrig_core::effect. This is what lets the registry compile without the runtime and is why the split has no cycle.Guards, as they follow the code
one_erasurerig-core/src/serve/handler.rs; the typed-view impl scan reads rig-bus'shandle.rs; the no-unsafescan covers all of rig-bus and rig-core'sserve/stream_idsserve/handler.rsandserve/writer.rsbevy_bus_host["rig-bus", "rig-core"]dependency_graphprovider_layoutExemptrows for both new packages-p rig-bus -p rig-agent; the wasm matrix checks rig-bus and rig-effect-logMigration (public breaks)
rig_core::bus::{Key, model_key, tool_key}rig_core::effect::…rig_core::tool::{RegisteredTool, ToolSet, ToolDispatch, dispatch_tool, ToolCatalog, ToolLease}rig_agent::tool::…(theToolSet/dispatch_toolpaths already resolved there)rig_core::bus::{Bus, BusConfig, BusDriver, Dispatcher, Registrar, Pending, EffectStream, Handle, ModelHandle, ToolHandle, MemoryHandle, IndexHandle, EmbedHandle, RerankHandle, Typed, Completion, ToolCall, Retrieval, wrap_stream}rig_bus::…rig_core::bus::{Serve, OutcomeSink, SinkClosed, ErasedHandler, serve_inline, events_from_response, StreamWriter}andrig_core::bus::adaptersrig_core::serve::…rig_core::effect::{EffectLog, LogHeader, EFFECT_LOG_FORMAT, stable_hash}rig_effect_log::…rig_core::bus::{EffectLogRecorder, EffectLogReplayer}rig_effect_log::…BusDriver::record_to(EffectLogRecorder)record_to(impl Recorder + Clone + Send + Sync)rig::bus,rig::effect_log(named modules likerig::run);rig::tool::{RegisteredTool, …}under theagentfeatureAdditive:
rig_core::tool::LivenessFn,DynamicTool::into_parts,PortableDynamicTool::into_parts(the seam a registry needs);rig_core::serve::StreamTapand the sink constructors above.Bench
Unary dispatch, release: median 625 ns, p90 708 ns (the 50 µs threshold test moved with the runtime; a crate boundary is not a call boundary).
Risks, as they stand
OutcomeSink::for_driver(reply, tap); recorded as a follow-up, not done.cargo publish --dry-runof rig-bus and rig-effect-log fails against the registry's rig-core (noserve, noeffectpublished yet), as rig-agent's does on rig-reqwest today. Publishing order is rig-core, rig-bus, rig-effect-log, rig-agent.Audit trail
Commit map
e29c116c8refactor(core): a typed key is vocabulary[HandlerKey]links inbus/mod.rsthat the removed import had been resolving; fixed in S2 (the local gate had run per-crate docs; the fix-forward rule applied)fdd43d264feat(agent)!: the tool registry is the agent'sRegisteredTool::from_dynamicreached intoDynamicTool's private fields —into_partsis the seam; rig-agent already re-exportedToolSet/dispatch_tool, so the break is smaller than drafted; the facade gates the registry underagent6ef12d19efeat!: rig-bus, the runtime as its own crateOutcomeSink::writeris an inherent method, soStreamWriterstays in rig-core (the prompt had it moving); the sink's tap held the driver'sStreamTap— moved into the handler module (audit ruling 1a);Recorderheld as closures; the trybuild diagnostic now spellsrig_core::serve::handler::Serve742f12c2efeat!: rig-effect-log, record and replay as their own crateEffectLogRecorder: Recorderno longer unifies — the seven record/replay tests moved to the log crate with copied helpersWhat the split did not find
No hidden coupling beyond the two the audit named (the sink's tap, the writer's inherent method). Every test count held: rig-core + rig-bus + rig-effect-log + rig-agent + rig-verify + rig-derive at 2461, the root guards at 207 (one new dependency-graph test), all six loom models green.
Gate
Fast local gate per commit: fmt, clippy on the workspace, rustdoc
-D warningson the workspace (the CI command, after S1's miss), nextest on the six crates, the roottests/coreguards on every commit, the wasm check on rig-core/rig-bus/rig-effect-log/rig-agent, the four fixtures under-D warningswith regenerated lockfiles, loom and the bench on S3 and S4, the publish dry-runs recorded. CI is the gate of record.