Technical Documentation: 0xPlaygrounds/rig
ℹ️ Provenance: Multi-Tier Ingestion · CodeWiki Reference
Overview (`README.md`)
Rig is a Rust library for building LLM applications, and rig, a terminal
coding agent built on it that you extend with Bevy plugins.
The coding agent
cargo install rig
rigIn the agent, /login signs in with a ChatGPT plan or saves a provider's API
key, or export the key (such as OPENAI_API_KEY) before starting. /model picks
the model and /help lists the commands. rig -p "fix the failing test"
answers one prompt without the terminal view.
- Everything is a plugin. The agent is a Bevy app; its
tools, commands, sessions, sign-in and terminal view are each a plugin listed
inplugins.toml, so any of them can be removed, replaced or added to. - It rebuilds itself.
/reloadrebuilds the agent with your plugin
changes and restarts it in the same session, and keeps the old build running
if the new one does not compile. - Scriptable.
rig -p --jsonandrig --jsondrive it over pipes;rig help-agentsis the protocol. - More when you want it. Subagents and checkpoints of what
shell
changed are optional plugins, and the tools work on remote hosts over ssh
or any command.
The agent guide covers the keys, sessions, compaction,
plugins and files, and the plugin guide shows
how to write a tool, command or panel. The agent needs Rust 1.97.1 or newer
and runs on Linux and macOS.
The library
cargo add riguse rig::prelude::*;
use rig::providers::openai::{self, OpenAI};
#[tokio::main]
async fn main() -> Result<(), anyhow::Error> {
// The client reads `OPENAI_API_KEY` and builds the models it serves.
let model = OpenAI::from_env()?.completion(openai::GPT_6_1_SOL);
let comedian = AgentBuilder::new(model)
.preamble("You are a comedian here to entertain the user using humour and jokes.")
.build();
let response = comedian.prompt("Entertain me!").await?;
println!("{}", response.output());
Ok(())
}#[tokio::main] needs tokio's macros and rt-multi-thread features
(cargo add tokio --features macros,rt-multi-thread).
- 20+ model providers and 10+ vector stores, each under one interface
- Agents with tools, multi-turn streaming and typed hooks
- Completion, embedding, transcription, audio and image generation
- GenAI semantic conventions for OpenTelemetry
- Recording and replay of an agent's effects (
rig-cassette) - Browser WASM (
wasm32-unknown-unknown) for the core and the agent runtime (target support)
More in examples/, the docs and the
API reference.
Crates
The rig crate re-exports the core and the agent runtime, and each companion
crate behind a feature.
| Crate | What it is |
|---|---|
rig-core |
Provider-neutral messages, completion and embedding models, tool contracts, the conversation type, vector-store contracts, and the providers |
rig-agent |
The agent builder, prompting and streaming, hooks, the tool registry and the serializable AgentRun |
rig-memory |
Windows, compaction, and saving, resuming and forking conversations |
rig-cassette |
Effect logs: recording, replay and checkpoints |
rig-tools |
Coding tools any rig agent can use: read, write, edit, search, shell |
rig-ecs, rig-harness |
The agent kernel on Bevy's ECS, and the harness the coding agent's plugins build on |
rig-tui, rig-basics |
The terminal view, and the coding agent's default plugins |
Integrations
rig = { version = "0.44.0", features = ["lancedb", "fastembed"] }| Integration | Crate | Feature | Module path |
|---|---|---|---|
| AWS Bedrock | rig-bedrock |
bedrock |
rig::bedrock |
| AWS S3Vectors | rig-s3vectors |
s3vectors |
rig::s3vectors |
| Candle (local Llama/SmolLM2/Qwen3 tools, YOLOv8 pose) | rig-candle |
candle |
rig::candle |
| Cloudflare Vectorize | rig-vectorize |
vectorize |
rig::vectorize |
| FastEmbed | rig-fastembed |
fastembed |
rig::fastembed |
| Google Gemini gRPC | rig-gemini-grpc |
gemini-grpc |
rig::gemini_grpc |
| Google Vertex AI | rig-vertexai |
vertexai |
rig::vertexai |
| HelixDB | rig-helixdb |
helixdb |
rig::helixdb |
| LanceDB | rig-lancedb |
lancedb |
rig::lancedb |
| Conversations over time | rig-memory |
memory |
rig::memory |
| Milvus | rig-milvus |
milvus |
rig::milvus |
| MongoDB | rig-mongodb |
mongodb |
rig::mongodb |
| Neo4j | rig-neo4j |
neo4j |
rig::neo4j |
| PostgreSQL | rig-postgres |
postgres |
rig::postgres |
| Qdrant | rig-qdrant |
qdrant |
rig::qdrant |
| ScyllaDB | rig-scylladb |
scylladb |
rig::scylladb |
| SQLite | rig-sqlite |
sqlite |
rig::sqlite |
| SurrealDB | rig-surrealdb |
surrealdb |
rig::surrealdb |
| TypeSafe Jev (experimental judgments) | rig-typesafeai |
typesafeai |
rig::typesafeai |
Community
Projects built on Rig are listed in
awesome-rig; open a pull
request there to add yours. Questions and ideas are welcome on
Discord, and CONTRIBUTING.md
says how to contribute. Rig is MIT licensed.
[!WARNING]
Rig is pre-1.0. Releases contain breaking changes; each release's
changelog and migration notes say what
changed and how to move.
Overview (`examples/README.md`)
Rig examples
Each example is its own package. Run one with:
cargo run -p <example-name>Most examples expect provider API keys in the environment (e.g. OPENAI_API_KEY,ANTHROPIC_API_KEY, GEMINI_API_KEY, COHERE_API_KEY). See each example's source for specifics.
| Example | Description |
|---|---|
agent_autonomous |
Demonstrates an autonomous extractor loop that keeps feeding its own output back in. |
agent_evaluator_optimizer |
See source. |
agent_orchestrator |
See source. |
agent_parallelization |
See source. |
agent_prompt_chaining |
Demonstrates prompt chaining with two agents in sequence. |
agent_routing |
Demonstrates routing one prompt into different follow-up prompts. |
agent_run_stepping |
Drives the agent loop by hand with the sans-IO [AgentRun] state machine. |
agent_stream_chat |
Demonstrates a streamed run over prior conversation history (prompt(..).history(..).stream()). |
agent_tool_call_streaming |
Streams tool-call arguments as they arrive on a raw completion stream and an agent run: each call's start names its tool, fragments render through parse_partial_arguments, a hook can stop mid-call, and a summary checks the joined fragments against each call's end. Takes a provider argument. |
agent_with_agent_tool |
See source. |
agent_with_approval_policy |
Demonstrates a non-interactive, policy-based HITL gate: an AgentHook auto-approves an allow-list, denies the rest (fail-closed), and applies an arg-based rule (mirrors needs_approval/interrupt_on predicates). |
agent_with_context |
Demonstrates adding small context documents directly to an agent. |
agent_with_default_max_turns |
Demonstrates extending the default agent loop budget for tool-heavy prompts. |
agent_with_durable_approval |
Demonstrates durable HITL: the hand-driven AgentRun is serialized while tool calls are pending and resumed from JSON (as another process would), so approval can happen out-of-process / later. |
agent_with_echochambers |
See source. |
agent_with_human_in_the_loop |
Demonstrates human-in-the-loop tool-call approval: an AgentHook pauses on each tool call so a human can approve/deny/edit/abort, mapped onto the dispatch-boundary DispatchAction (Proceed/skip/rewrite_tool_args/stop). |
agent_with_loaders |
Demonstrates loading real example files into agent context. |
agent_with_memory |
Demonstrates Rig-managed conversation memory with an in-memory backend; -- --stream streams each prompt. |
agent_with_tools_otel |
Agent multi-turn with tools, but with a tracing subscriber that sends all logs/traces to an OTel collector; -- --stream streams the answer. |
agent_with_tools |
Demonstrates registering runtime-defined tools on an agent. |
agent |
Demonstrates the smallest useful agent setup with OpenAI. |
calculator_chatbot |
See source. |
chain |
Demonstrates a retrieval-augmented pipeline with parallel! and lookup. |
complex_agentic_loop_claude |
See source. |
custom_vector_store |
Example: Implementing a custom vector store backend |
debate |
See source. |
enum_dispatch |
See source. |
extractor |
Demonstrates typed extraction and extraction with usage metadata. |
force_tool_first_turn |
Demonstrates a per-turn RequestPatch footgun and its fix: forcing tool_choice = Required on every turn loops until max_turns, so an AgentHook gates the patch on ctx.turn() == 1 to force the tool only up front. |
gemini_deep_research |
Runs Deep Research or Deep Research Max in the background, then streams it with resume or polls it, and prints the report's sources. |
gemini_default_api_recovery |
Demonstrates recovering from Gemini emitting a legacy default_api tool name. |
gemini_extractor_with_rag |
See source. |
gemini_nanobanana_image_generation |
Generates a sized image with Nano Banana 2, optionally from a reference image, and writes every image and text part. |
gemini_stream_kill_token_count |
Live Gemini example: obtaining a token-count estimate when a streaming |
gemini_text_to_speech |
Speaks a line in one voice, then a two-host script as PCM joined into one WAV file. |
gemini_video_understanding |
Demonstrates Gemini video understanding with provider-specific request parameters. |
litellm |
Runs an agent through a LiteLLM proxy using its OpenAI-compatible Chat Completions API. Includes local proxy setup. |
manual_tool_calls |
Demonstrates manual tool-call handling with an explicit raw Model request. |
multi_agent |
See source. |
multi_extract |
Demonstrates fan-out structured extraction with try_parallel!. |
multi_turn_agent |
See source. |
openai_agent_completions_api_otel |
This example shows how you can use OpenAI's Completions API. |
openai_streaming_per_call_usage |
Shows how to inspect per-completion-call usage in an agent stream. |
pdf_agent |
See source. |
rag_dynamic_tools |
Retrieves the tools each prompt needs from a vector index, then answers a one-tool and a multi-turn prompt. |
rag |
Answers from documents found by vector search; -- ollama uses a local Ollama server. |
reasoning_loop |
See source. |
request_hook |
Demonstrates observing prompt/response/tool lifecycle events by stacking two AgentHooks with add_hook. |
reqwest_middleware |
Demonstrates supplying a custom reqwest client with retry middleware. |
rmcp_example |
Connects agents to MCP servers: a local server's tools and resources, and with --bin remote -- any hosted streamable HTTP server. |
sentiment_classifier |
Demonstrates the smallest typed extractor for classification. |
transcription |
See source. |
tool_result_outcomes |
Demonstrates structured disk (Other/EIO) and network (Network/ENETUNREACH) tool failures, a run-scoped scratchpad ledger, and ordered recorder/policy hooks that terminate fatal failures while returning recoverable feedback to the model. Run cargo run -p tool_result_outcomes -- --help for credential-free usage. |
vector_search_cohere |
Demonstrates vector search with separate Cohere document and query embeddings. |
vector_search |
Demonstrates embedding documents and querying an in-memory vector index with OpenAI, or with -- ollama a local Ollama server. |
Overview (`scripts/README.md`)
scripts
check-dependency-floors.py— downgrades every direct dependency to its declared floor and checks the workspace still builds.release-notes.sh [PREVIOUS_TAG] [OUT_DIR]— collects the commit log, merged PR notes and public API diffs since the previous tag into one directory for the release editorial pass.release-notes-prompt.md— the editorial pass that writesCHANGELOG.mdandMIGRATING.mdon the release PR, from the output ofrelease-notes.sh.
Overview (`tests/README.md`)
Test Suites
Run the smallest useful local check for changed behavior and leave comprehensive
execution to CI; see DEVELOPING.md for check selection,cargo xtask verify modes, review, and publication.
Provider test targets have two owners:
- Cassette-backed suites are targets of
rig-cassette, beside the corpus they
replay:crates/rig-cassette/tests/.rswith its modules undercrates/rig-cassette/tests/providers//cassette/(and/live/
for the ignored live cells those suites still carry). The shared drivers they
include live incrates/rig-cassette/tests/common/. - Providers with no recorded corpus keep their live-only suites in the facade:
tests/.rswithtests/providers//. These are ignored
tests that require a real service. tests/core.rscontains provider-agnostic core behavior tests and the guards
that scan the source tree, which need the repository root.test-support/service-tests/integrations.rsruns the vector-store suites fromtests/integrations/as the unpublishedrig-service-testspackage.tests/integrations.rsretains the root Bedrock integrations.
Cassette suites require a checkout of this repository: rig-cassette excludes
its fixtures and integration tests from the published package. The unpublishedrig-cassette-minimal runner atcrates/rig-cassette/tests/minimal/Cargo.toml executes the same verify
source, plus the shared effect-log/classic-replay regressions. It selects only
cassette's agent feature, without the native HTTP engine.
RIG_PROVIDER_TEST_MODE=replay cargo nextest run --locked -p rig-cassette-minimal --all-features --retries 0Keep RIG_REGENERATE_GOLDEN unset. This execution complements, rather than
replaces, the unified verification runs through rig-cassette.
The default/all-features cassette library still owns the unified library
regressions; core-all also retains the migrated classic replay cases.
Minimal verification runs with zero retries; unified verification and both
standalone/default-member effect-corpus configurations retain their existing
two. The standalone effect-corpus lane explicitly includes therig-test-support corpus regressions. Source ownership includes the library
tests shared by the minimal runner, not only its verification entrypoint.
Testing Doctrine
Recorded provider traffic is the default evidence; provider APIs are the
ultimate judge of whether the code is correct. Every genuine pre-existing
streaming bug found during the #2258 refactor was found by live recording,
not by the synthetic corpus — a corpus written alongside an abstraction
encodes the team's model of the wire and structurally cannot falsify it.
- Cassette-first. New provider behavior gets a cassette-backed test.
A unit test still earns its place — for internal behavior that is
definitory rather than observed — but each unit test of provider-facing
behavior should say (in its doc comment) why it isn't, or can't be, a
cassette test. Recording needs API keys and isn't trivial; writing the
cassette test a contributor couldn't is core maintainer work. - Record first, derive the assertion, review the derivation. Prefer
assertions generated from a replay of real traffic and then reviewed over
assertions hand-authored from documentation — hand-authored expectations
encode the same model of the wire the code under test does. - Assert on the request boundary too. A frozen cassette replays the
provider's responses; it cannot by itself catch outbound drift in the
requests the live code builds. The cassette harness matches each request
body against the recorded one, so a request-shape regression fails as a
404 mock miss — treat that as a first-class assertion, and when a change
intentionally alters a request, update the recorded body deliberately and
say so. (This is exactly what caught fabricated ids reaching request
serializers in #2258.) Body matching compares key-sorted canonical JSON,
so map reordering is invisible to it (Layer 0's determinism check below
exists for that), and a cassette cannot notice the provider changing its
behavior after record time (Layer 3 exists for that). - Never weaken a cassette to make it pass. Update assertions and
recordings to the new intended behavior, or re-record; a scrubbed value
must redact real data, never invent data that was not recorded.
Core Tests
Start with the owning package and a test-name filter. Confirm that the filter
actually ran the intended tests; a successful command running zero tests is not
verification.
cargo nextest run --locked --profile local -p rig-core --lib <test-name-filter>
cargo test --locked -p rig-core --lib <test-name-filter>
cargo test --locked -p rig --test <provider> <test-name-filter>
cargo clippy --locked -p rig-core --all-features --tests -- -D warningsThe Clippy command is useful for core test changes involving lint-sensitive
patterns, not a mandatory check for every edit. Use the owning package and
relevant features for other changes. Broader facade test commands:
cargo test -p rig --test core # provider-agnostic core tests
cargo test -p rig # all default non-ignored root-crate tests
cargo test -p rig --all-features # same, with all root crate featuresFallible test assertions
Workspace Clippy denies panic_in_result_fn: assert! and assert_eq! inside
a test returning Result fail that check even when the test itself passes.
Use anyhow::ensure!(actual == expected, "value changed") in such tests,
alongside ? for fallible setup. Preserve each assertion's condition; do not
add lint suppressions or weaken the test. Unit-returning tests can keep their
existing assertion style.
Core feature isolation
The core crate's dev-dependency on itself enables test-utils, websocket,
and its default features. Consequently, a core unit-test run with--no-default-features does not prove those features are absent. For an actual
feature-disabled library check, select core alone without test targets:
cargo check --locked -p rig-core --no-default-features --libWhen the change needs WASM coverage and the target is installed, add--target wasm32-unknown-unknown. Keep runtime test coverage and compilation
coverage distinct in the report.
Cassette Provider Tests
Cassette tests replay committed HTTP interactions by default and do not require
provider API keys. Fixtures live under crates/rig-cassette/fixtures/cassettes//....
Replay one migrated provider suite (anthropic, bedrock, chatgpt, cohere,copilot, deepseek, doubleword, gemini, groq, llamacpp, mistral,mistralrs, ollama, openai, openrouter, perplexity, venice, xai)
with:
cargo test -p rig-cassette --all-features --test <provider> <provider>::cassette -- --nocapture --test-threads=1Record mode requires the relevant provider credentials in the environment and
overwrites existing cassette files:
RIG_PROVIDER_TEST_MODE=record \
cargo test -p rig-cassette --all-features --test <provider> <provider>::cassette -- --nocapture --test-threads=1Prefer re-recording by fixture, from the repository root:
cargo xtask cassette record [--cap N] [--pause SECS] [--dry-run] [--no-cleanup] <provider/scenario.yaml>...It runs each fixture's owning test once in record mode and logs every run in
an attempt ledger, before it starts and when it ends. A test stops at six
attempts, or at --cap when that is lower. After a failed run it copies every
fixture change aside and restores the provider's fixtures as they were just
before the run. It ends with the cleanup pass below, whatever happened, and
exits non-zero when any test did not record, including one already at its cap.
The other commands:
cassette owner <fixture>...prints every test that records a fixture.cassette spend [--cap-per-wire USD] [--cap-total USD]prices the attempt
ledger conservatively. A failed or interrupted attempt costs at least $0.05.cassette scan [--base REF] [<fixture>...]checks changed fixtures for
credentials, account ids, emails and home paths. It matches case-sensitively
on whole tokens, and searches for every exported*_API_KEY,*_TOKENand*_SECRETvalue literally.cassette goldens [--test TARGET]...regenerates effect goldens from
replay: each selected target runs withRIG_REGENERATE_GOLDEN=1, so each
golden holds what its producer logs over its replayed cassette.cassette cleanup [ledger.jsonl]runs the cleanup pass on its own.
The recorder refuses to write a fixture, and panics, in two cases:
- A reply is an account failure the cell did not declare as its subject: a
refused credential, a spent quota, a rate limit or an empty balance. This
includes one delivered inside a successful stream. Declare an expected
failure withCassetteSpec::expects_account_failureor, in a wrapper that
presents a rejected key,ProviderCassette::expect_account_failure
(bogus_api_key()declares it too). - An OpenAI or xAI Responses request stored a response the same session never
deleted. Sendstore: falseunless the cell is a chain that deletes its
own state.
Fixtures older than the stored-state rule are listed incrates/rig-cassette/tests/common/stored_state_grandfathered.txt. When you
re-record one with store: false, delete its line; the safety tests fail
until you do.
A failed or refused recording is written under the attempt root, never over
the fixture. The attempt root is RIG_CASSETTE_ATTEMPT_DIR, or by defaultcassette-attempts in the target directory. As replies arrive, before the
test can panic, the recorder appends every stored response, file, cache,
interaction, conversation or vector store it sees created to ledger.jsonl
there. cargo xtask cassette cleanup deletes whatever the ledger still holds,
using each provider's API key. A 404, or Gemini's 403 "not found", counts as
already gone.
The recorder normalizes volatile timestamps (created, created_at, …) at
any depth, so a recording pass sees live values its fixture never holds.
Compare a reply document with its fixture throughassert_matches_recorded_document, or one field throughassert_wire_value_matches. Both compare such keys by type when recording and
exactly in replay. The cassette-safety tests reject an exact comparison of a
volatile key outside replay.
A handful of committed cassettes hold bytes no provider will return: they are
hand-derived from a live capture, or deliberately corrupted to pin a parser
regression. The test that owns such a fixture marks itself, so the sweep above
abandons it instead of healing it:
if crate::cassettes::skip_when_recording(
"cell 3 is hand-derived from cell 2: the tool input carries a control byte",
) {
return;
}Hand-editing a committed cassette means adding that line, with a reason naming
why the bytes are not obtainable live. Scripted fault families need no marker:
they borrow frames from another scenario's fixture and never open a recording
session of their own.
Stale objects slow macOS test processes
On macOS, incremental builds leave each relinked test binary's object files
beside it in target/debug/deps, and nothing deletes the old ones. Every
process that asks CoreFoundation for its main bundle lists that directory:
reqwest does when it reads the system proxy settings, and rustls-native-certs
does when it loads the platform trust store. With hundreds of thousands of
stale objects the listing takes seconds per test. A fully parallel-p rig-cassette run then fails the tests that start an httpmock server,
which loads the trust store (no native root CA certificates found). Delete
the stale objects. A binary built before then loses only its backtrace line
numbers until it is relinked:
find target/debug/deps -maxdepth 1 -name '*.rcgu.o' -deleteBedrock replay gives the SDK a plain HTTP client for the loopback replay
server, so it never loads the trust store.
Time in cassette tests
Code whose requests depend on time (a cache that expires, a TTL chosen from
the gaps between calls) sends different bodies when time differs, and the
request snapshots pin bodies byte for byte. Give such code the session's clock,ProviderCassette::clock(), instead of the system clock:
- Recording reads wall time and saves every reading, in order, beside the
fixture as<fixture>.clock.json. - Replay returns the same readings in the same order. It panics when the
code asks for more readings than were recorded, andfinishpanics when it
asks for fewer: either way the code reads time differently from its
recording, so re-record.
A test that needs a real pause calls ProviderCassette::pause(duration). It
sleeps only while recording, returns at once on replay, and refuses anything
over MAX_PAUSE (60 s): no cassette test waits longer, and no replayed test
sleeps at all.
Request snapshots
Every replay in this workspace also checks each request body against the
request snapshot beside its fixture, <fixture>.requests.json, and fails
with the differing JSON pointers when they disagree. .cargo/config.toml
sets RIG_CASSETTE_SNAPSHOTS=check and cargo xtask verify forces it; the
published engine leaves snapshots off.
A snapshot holds only how the body Rig sends differs from the recorded one,
one entry per differing interaction:
{
"interactions": [
{
"index": 0,
"changes": [
{ "path": "/messages/0/content", "recorded": [{ "text": "hi", "type": "text" }], "sent": "hi" },
{ "path": "/messages", "splice": 2, "recorded": [], "sent": [{ "role": "user" }] }
]
}
]
}path is a JSON pointer into the scrubbed, key-sorted recorded body.recorded alone removes a key, sent alone adds one, and both replace a
value. With splice, recorded and sent are the array items removed and
inserted at that index. A multipart body is compared as its parts: headers,
and the body as text up to 4 KiB, else its length and FNV-1a hash. A fixture
whose requests all equal their recordings has no snapshot. Full copies of
every request body would add about 156 MB beside 222 MB of cassettes; the
recording already pins those bytes, so only the difference is stored.
Shape-matched replay
Replay in this workspace matches a request to a recording by its coarse
shape, not its bytes: .cargo/config.toml sets RIG_CASSETTE_MATCHING=shape
and cargo xtask verify forces it; the published engine matches exactly.
The method, path, query and recorded headers still match exactly. The body
key keeps:
- the field structure, with every value and its type erased;
- every array of objects in order, so the roles of
messagesand the items
of a Responsesinputstay in sequence; - the values of
model,role,name(tool names, also inside content)
andstream(the reply mode, with the path and query);
and reduces a content value (content, parts, system, instructions,output, prompt, response, result) to the tool names in it, so a
string and a one-part text array agree. Tool schemas and tool arguments
(parameters, input_schema, arguments, args, ...) are erased, an array
of scalars collapses, and a multipart body is keyed by its field names.
Unordered replay serves, among the unplayed interactions with the key, the
one whose expected body (the recording with its snapshot applied) the
request is closest to, counted in snapshot changes.
So a change to how Rig writes a request that keeps its coarse shape needs no
re-record: the request still replays, the snapshot check fails untilcargo xtask cassette snapshots records the difference, and the reviewer
reads that diff. A change of shape (a new field, another role sequence)
misses its recording and needs one. A cell whose subject is the exact matcher
opts out with CassetteSpec::exact_matched().
cargo xtask cassette snapshots # rewrite every snapshot from replay
cargo xtask cassette snapshots --test openai # rewrite only what one target replays
cargo xtask cassette snapshots --check # replay and fail on any differenceA rewrite deletes every snapshot first, then replays withRIG_CASSETTE_SNAPSHOTS=write; with --test it deletes nothing. Run it after
a change to what Rig sends or after re-recording a fixture, and review the
snapshot diff with the change.
Acceptance index
crates/rig-cassette/fixtures/acceptance.toml maps every request fact Rig
sends, per provider and encoder, to one interaction recorded live whose
request holds it. A fact is one object of the body: its path with array
indices collapsed, its sorted keys, and each key's scalar type or container
kind, keeping the values of discriminators such as role and type. A
JSON Schema's property names (properties, $defs, ...) share one path, and
a tool call's arguments or a tool's result is not read. A body that is not
JSON is one fact: empty, binary, text, or its multipart field names. Facts
do not capture combinations of fields: a request that only combines objects
already recorded needs no recording of its own. The coarse key and replay
still exercise every whole request. What Rig sends is read offline from each
recording with its snapshot applied, so the check reads files only. A
recording whose request body the proxy recorder dropped is pinned to the
facts its snapshot showed when the fixture was first indexed, until the
fixture changes.
cargo xtask cassette acceptance # rewrite the index
cargo xtask cassette acceptance --check # CI's `verify --check acceptance`--check fails when a fact Rig sends has no live recording, naming the
provider, the encoder, the path, the fact and the smallest cassette that
sends it, and when the index is stale. The rules for a change to what Rig
sends:
- it keeps every request's coarse shape: refresh the snapshots, rewrite the
index, and record nothing; - it adds a fact: re-record one cassette per provider and fact that
--checklists, and only those; - never re-record a cassette whose shape did not change.
ChatGPT record mode additionally needs CHATGPT_ACCESS_TOKEN=... CHATGPT_ACCOUNT_ID=....
Bedrock cassette replay does not require AWS credentials. Bedrock record mode uses the AWS
SDK credential provider chain and a direct SigV4-aware recorder, so it requires AWS credentials
with Bedrock model access in us-east-1. The Bedrock recorder buffers streaming/event-stream
responses and stores non-UTF-8 cassette bodies as base64; those opaque bodies are intended for
replay fidelity, and safety checks also scan their decoded bytes for credential-shaped material.
Venice's text-to-speech scenario records through the direct recorder rather than
the httpmock proxy: the proxy exports bodies as strings, so a binary response
(raw audio) is exported with no body at all and replays as zero bytes. Its
transcription scenario stays on the proxy path, where the same limitation drops
the request's multipart body. A cassette that recorded no body still matches
a multipart request; its request snapshot pins the parts Rig sends.
Run one cassette test by passing a test-name substring after the test target;
the filter is a substring match, so use the full module path only when the
shorter name is ambiguous:
cargo test -p rig-cassette --all-features --test gemini \
streaming_tools_smoke \
-- --nocapture --test-threads=1Cassette Safety
Record mode scrubs and safety-checks cassette contents before writing fixtures,
and the committed cassette safety tests enforce the same scrubbed form during
normal test runs. Scrubbing removes credentials (API keys, bearer tokens,
cookies, OAuth tokens, SigV4 material), AWS account numbers in ARNs and
home-directory paths, drops response headers outside a small allowlist, and
normalizes volatile timestamps. Everything else the provider sent is recorded
verbatim:
thinking signatures, encrypted and redacted reasoning, tool-call and response
ids, cache keys, cursors, generated images. Those values are provider-issued
and carry no secret of ours, and keeping them lets a recording seed a live
call and lets a replay decode the provider's own bytes. Request matching
compares the recorded bytes as they are, so a fixture must hold exactly what
the test sends: a fixture recorded before this policy replays only while its
placeholders are values a response delivered and the test echoes back, and a
fixture whose placeholder stands for a value the test itself mints must be
re-recorded. Still review every cassette diff for:
- no API keys, bearer tokens, cookies, or provider account identifiers;
- expected request paths, methods, and bodies;
- expected provider responses for the scenario;
- no unrelated cassette churn.
History survival and portability
A provider hands Rig opaque fields only it can interpret (thinking signatures,
encrypted reasoning, redacted reasoning, reasoning item ids, tool-call ids)
and expects them back on the next turn, in the slot that carries them: the
signature on its own reasoning block, the ciphertext on its reasoning item,
the call id on both the call and its result. A Responses message's phase
is held to the same rule: it must return on the assistant item with the
message's id. The rule lives intest-support/rig-test-support/src/history_survival.rs, modeled per dialect,
and is applied twice:
crates/rig-cassette/tests/cassette_history_survival.rssweeps every
committed cassette and effect golden at zero provider cost: delivered fields
must reach every continuation request in their slot, every recorded request
must pair its tool calls with results (a stored Responses chain answers
calls held byprevious_response_id), every nativechat_historymust
pair too (this includes the requests after cancellations, invalid arguments
and provider faults), and a census proves each provider and content kind
was examined. LegacyREDACTED_<n>placeholders cannot prove a slot and
are counted, not judged. Exemptions need a cited provider behavior and are
reported when stale.history_survival_matrixcells per provider run three prompts over
deterministic lookup/verify tools with one transient tool failure, assert
the same delivered content on unary and streaming transports, then apply
the rule to the recording they just made.portability_matrixcells decode
another wire's committed reply through Rig's real decoder, continue it on
the target wire, and assert no foreign reasoning state reached it.
An assistant turn records its origin: the wire format, provider and model
that produced it. A request to that same model replays the turn's provider
items verbatim, and any other request replays only its canonical fields.
Further cell families exercise the same round trip: stateful_chain_matrix
(OpenAI previous_response_id and file ids, Gemini cachedContents and
Interactions), adversarial_matrix (reused call ids, reordered parallel results, long
ciphertext, signed empty reasoning, a history ported across three providers
and back), image_input_matrix and request_identity_matrix. Chains create
and delete their server-side resources in the recorded session, so a
committed chain replays but its handles cannot seed a live call.
Record a cell with the ordinary record mode and an exact test name. SetRIG_LONG_TASK_ATTEMPT_DIR to keep the exchanges of a failed attempt outside
the tree, and RIG_HISTORY_SURVIVAL_CENSUS or RIG_PORTABILITY_REPORT to
write the census and the forwarded-field report to a file.
Goldens checked in the test
A producer listed in crates/rig-cassette/fixtures/in-test-goldens.txt checks
its log in the test instead of against a committed golden: the log replays
record by record through its own replayers by effect id. Such a golden is
never written, RIG_REGENERATE_GOLDEN included.
The parity snapshots (fixtures/parity/.json) are kept the same
way: each pins one reply per reply shape and mode, the smallest, then the
first in path order, and every other reply is checked in the test by itscall and stream().finish() agreeing. RIG_REGENERATE_PARITY=1 rewrites
the pinned entries.
Prompt Cache Testing
Provider prompt caching is a prefix match: the cache key is derived from the exact request
bytes up to each breakpoint, so any change to an earlier block invalidates everything after it.
The failure that costs real money is therefore not "caching is off" — it is "caching silently
degraded", where a reordered map, a rewritten earlier turn, or a re-advertised tool set moves the
prefix and every request quietly misses while the counters stay non-zero.
Four layers guard this. Each catches something the others structurally cannot.
Layer 0 — free, key-free, whole-corpus (`tests/cassette_cache_prefix.rs`)
Checks over cassettes that already exist, costing no provider traffic:
recorded_conversations_do_not_move_their_cache_prefix— for consecutive same-endpoint requests
in one cassette, turn N-1's canonical blocks must be a prefix of turn N's. The rule lives intest-support/rig-test-support/src/cache_prefix.rsand is shared with the per-scenario harness so the two cannot
drift. It compares cached content:cache_controlmarkers are stripped first, because
Anthropic's documented incremental-caching pattern moves the breakpoint forward every turn and
that is correct behavior, not a prefix move.every_provider_is_covered_by_the_prefix_check— a per-provider census that makes the check
fail closed. Every recorded request is classified modeled / non-conversational / unmodeled,
and an unmodeled conversational endpoint is a finding rather than a silent skip.*_request_serialization_is_deterministic— serializes the sameCompletionRequesteight times
through each provider's real client and requires byte-identical output. This is the only check
that can see unstable map or tool ordering: cassette replay compares key-sorted canonical JSON,
and aserde_json::Valueround-trip normalizes key order too, so aHashMapin a request body
busts every real cache while all recorded evidence looks identical.every_cassette_provider_has_a_cache_suite— a provider with cassettes and no cache scenario
fails unless it is inNO_CACHE_SUITEwith a reason.
Layer 1 — the shared harness (`test-support/rig-test-support/src/cache_conformance.rs`)
One deterministic three-turn probe — warm, byte-identical repeat, then append and repeat —
asserted against a per-provider CacheSupport descriptor, so adding a provider is a descriptor
rather than another copy of the assertion logic.
The assertion that carries the weight is assert_hit_ratio. cached_input_tokens > 0 passes
just as happily when 200 of 40,000 prefix tokens are cached as when 39,800 are; the ratio is
taken against turn 1's billed prompt. Getting the denominator wrong makes the assertion
vacuous, and it is one rule on every provider: CacheSupport::input_tokens, turn 1'sinput_tokens, which counts cache reads and writes as Usage's rustdoc
(rig_core::completion::Usage) states. tests/cassette_usage_census.rs enforces that
contract on every recorded call of every provider.
Growth is asserted as a ratio, not a monotonic token count: providers cache in coarse blocks, so
the absolute figure drifts a few tokens as block boundaries re-align (Gemini was measured going
3,765 -> 3,760 across a 21-token append) while a genuine prefix move collapses it to zero.
Layer 2 — per-provider cassettes (`crates/rig-cassette/fixtures/cassettes//prompt_caching/`)
Recorded live, replayed key-free. Replay is not a tautology: the harness matches request bodies,
so a rig change that perturbs the outbound prefix fails as a replay miss in CI with no API key.
Record one scenario at a time:
RIG_PROVIDER_TEST_MODE=record \
cargo test -p rig-cassette --all-features --test openai openai::cassette::prompt_caching \
-- --exact --nocapture --test-threads=1Layer 2b: long runs (`test-support/rig-test-support/src/cache_longrun.rs`)
A probe proves a hit on turn 2 or 3. Caching can still degrade over a long run: a marker that
stops moving forward, a prefix that drifts after turn 40, a cache key that changes, or cache
writes that cost more than the reads save. Each provider with caching records the same 100-turn
support chat (the handbook preamble, one lookup_order call per turn, about 200 calls) underfixtures/cassettes//long_run_caching/ (Gemini: auto_caching/), plus a 30-turn run
with caching off as the baseline.
cache_longrun holds everything the runs share:
- The workload (
SUPPORT_PREAMBLE,LookupOrder,question) and the turn helperschatandchat_streamed, which retry a 5xx or 429 up to three times with record-only pauses of 2, 5
and 10 s and count the retries. - One reader per wire,
CacheWire::{Gemini, Anthropic, OpenAiChat, OpenAiResponses}. For each
recorded call it gives rig'sUsageas the wire reported it (input, cached reads, cache
writes, output and total), the conversation the call continues, the OpenAIprompt_cache_key, and for Gemini the cache the call reads and everycachedContents
resource created. Prompt tokens areinput_tokenson every wire, asUsage's rustdoc
states. - The figures, printed as one
CACHE_LONGRUN /<scenario> {json}line per run so a PR
table is generated rather than transcribed: calls, fixture bytes, retries, prompt tokens,
cached reads, cache writes, cached share, hit rate (calls after the first that read anything),
the smallest per-call share from the first read on, cache accuracy (reads over cacheable
tokens, where a call's cacheable tokens are its conversation's previous prompt), where the
reads fell short of that, writes over prompt tokens, input dollars with and without caching
throughrig_core::completion::CacheCostat the provider's rates, and the same for the first
30 turns alone, to set beside the baseline. - The checks
checkruns on every run: rig'sUsagematches the wire on every call (input,
cached reads, cache writes, output and total); the input saving, counting every write, read
and stored token-hour, is at least the run's floor; from each conversation's first read on,
every call is cached above the run's floor and none reads nothing (the prefix-move
signature); cache writes stay under the run's cap.
"Not faked" means something different per provider, and each run asserts its own form:
- Anthropic bills a write above the input price, so a marker that stops moving forward
shows as the same prefix written again and again: writes stay under a share of prompt tokens. - OpenAI caches automatically and bills writes on GPT-5.6 and later: every request carries
the sameprompt_cache_key, and writes stay under a share of prompt tokens. - Gemini caches are resources: a request that reads one carries none of what it holds,
cache plus request tail is the conversation byte for byte, every cache is deleted, every
replaced cache was read at least three times, and creation stays at or below 15% of prompt
tokens.
To add a provider, give CacheWire a variant with its reader (the call path, the usage fields,
the first user message), add a with__long_run_cassette wrapper and register it
with the cassette-safety scan, and record one 100-turn fixture and its baseline.
Rates come from the provider's price page, quoted with the date. Thresholds are placeholders
until a first recording confirms them; never loosen one to make a recording pass. Record with:
cargo xtask cassette record --cap 3 <provider>/long_run_caching/<fixture>.yaml ...A 100-turn run takes six to eleven minutes to record. Replay prints the figures:
cargo test -p rig-cassette --all-features --test anthropic long_run_caching -- --nocapture --test-threads=1Workloads beyond the support chat (`cache_longrun::workloads`)
The support chat measures one shape of traffic. cache_longrun::workloads holds the others,
each a deterministic function that drives one conversation and returns itsRunLog. Each names its conversation with a marker in the first user message, so one fixture
can hold several conversations and LongRun::conversation can pick one out.
document_session: a store policy of about eight thousand tokens attached on the first
turn, then a question about it every turn. On Anthropic the document enables citations from
the first request, because enabling them later changes the rendered system prompt.mid_system_chat: a mid-conversation system message every few turns, alternately where
Anthropic takes one and where rig moves it after the next user turn. The run asserts every
instruction stays inmessagesand none is hoisted intosystem.dynamic_tools_chatwith theToolSchedulehook: the advertised tools change every few
turns through a request patch. A turn that fails ends the run and is returned, so a run can
assert a refusal.
The runs live in /cassette/long_run_workloads.rs (workloads on their own) andlong_run_features.rs (features that can move a prefix), with fixtures under/long_run_caching/:
| run | model | measures |
|---|---|---|
document_60 |
claude-opus-5-5 | a large fixed input with citations, read back every turn |
mid_system_every_10_60 |
claude-opus-5-5 | mid-conversation system messages keep the prefix |
dynamic_tools_30 |
claude-opus-5-5 | a changed tool list: the thinking-block binding 400 on turn 11 |
document_60_responses |
gpt-6-sol | as document_60, without citations |
The OpenAI run is stateless (store: false), reasons at low effort and sends oneprompt_cache_key. It runs on Responses only: the GPT-6 models take function tools on Chat
Completions only at reasoning_effort: "none", and gpt-6-astra and gpt-6.1-sol not at all.
To add a workload, write it in cache_longrun::workloads as a deterministic function taking
the agent, the clock, its size and a conversation marker; keep generated inputs fixed text
(no nonce or timestamp), so a re-recording sends the bytes its cassette holds. Add a run for
it in long_run_workloads.rs with its own Limits, rehearse it once under a scratch scenario
name, and record it with cargo xtask cassette record.
Layer 3 — live economics (`live_cache_economics`, `#[ignore]`d)
A cassette pins what a provider did at record time; only a live run catches the provider changing
its cache semantics under us. Each cell prints a LIVE-CACHE-ECONOMICS row, so the economics
table can be regenerated rather than trusted as a transcription:
cargo test -p rig-cassette --all-features --test openai live_cache_economics \
-- --exact --ignored --nocapture --test-threads=1Rules for recording cache fixtures
- Never commit a cache cassette whose recorded turn 2 shows zero reads. The Layer-1 assertions
run identically in record mode, so a bad session fails instead of committing a fixture that pins
a miss. That is the intended outcome — a recorded miss is worse than a failed recording. - Some providers need a warm-up pass. Gemini's implicit cache only serves a prefix once an
entry for that exact prefix exists, so on a cold run the grown turn-3 prefix reads zero. Run
every implicit-cache scenario twice and keep the second recording. Mistral's cache is
intermittent for a different reason (routing without cache affinity) and may need several attempts. - Padding must be deterministic and committed — no nonce, no timestamp. A nonce guarantees a
turn-1 miss, churns the cassette on every re-record, and breaks body matching. The org-pre-warm
risk it would avoid is already tolerated byassert_warms. - Pad above the provider's documented minimum (
min_cacheable_tokensin the descriptor) or
the API silently declines to cache. Where a provider's rate limit is tighter than the default
probe,CacheProbe::with_paddingshrinks it — Groq's 8,000 TPM tier needs this. - All three turns must record back-to-back in one test body, because cache TTLs are minutes.
The shared probe does this by construction. - Never mark a cache scenario
.unordered(). Ordered replay is what lets two byte-identical
requests replay two different recorded responses, which is the only reason a miss-then-hit pair
is replayable at all.
Gemini: two caching features, and they behave differently
Gemini is the one provider in the matrix with two caching features, and the
suite covers both because they are not interchangeable.
Implicit caching is automatic and best-effort. Measured ongemini-2.5-flash over an 18,497-token corpus: five consecutive turns reusing
that corpus read zero cached tokens (~92k tokens billed at full price), and
only a sixth request read 99.6%. It keys on a prefix the provider has already
seen, so a fresh conversation starts cold and there is no way to pre-warm it.
Explicit caching (cachedContents) uploads once and hands back a handle.
Same corpus, same day: 100.0% on turn one, and 100.0% again from an
unrelated conversation. It bills storage per token-hour, so it pays when one
large fixed payload is reused enough to beat that — and it pays immediately
rather than after a warm-up.
Practical consequences for anyone touching these fixtures:
- Disable thinking (
generationConfig.thinkingConfig.thinkingBudget: 0) on
every cell that is not specifically about thinking. Gemini 2.5 spends its
output budget on thoughts first, so a smallmax_tokensyields a response
with no message at all. - Explicit-cache cells create billed server-side resources. Delete what you
create, including on the failure path. - Cache handles are account-scoped and server-generated. They ride in
request bodies, request paths and responses, and the generated-token
scrubber cannot reach them (it stops a token at/), soscrub_resource_nameshandles them. Never assert a literal handle. - Pad past the documented 1,024-token minimum. Every probe's padding
assumes it; a prompt below it is not cached.
Automatic caching (gemini::caching, fixtures undergemini/auto_caching/) is recorded as long runs: 100-turn support chats
(about 200 calls each), a 60-call tool loop, four sub-agents and a lifecycle
run. Their cache book reads the session's recorded clock, so each fixture has
a .clock.json beside it. They run on the shared long-run checks (Layer 2b),
which for a run with a book include Gemini's rules: no request re-sends what
its cache holds, cache plus request tail is the conversation byte for byte,
every created cache is deleted, every replaced cache was read at least three
times, and cache creation stays under 15% of prompt tokens.
Live Provider Tests
Live provider tests use real provider APIs, local model servers, or account credentials. They are
ignored by default unless a test file says otherwise.
# all ignored tests for one provider target
cargo test -p rig-cassette --all-features --test openrouter -- --ignored --nocapture --test-threads=1
# one ignored provider test
cargo test -p rig-cassette --all-features --test openai \
responses_document_file_id_roundtrip_live \
-- --ignored --nocapture --test-threads=1Use the provider-specific environment variables named in the ignored test reason or provider
module, such as OPENROUTER_API_KEY, MISTRAL_API_KEY, GROQ_API_KEY, XAI_API_KEY,HUGGINGFACE_API_KEY, or local services such as Ollama and llama.cpp (llama-server, which also serves a .llamafile).
Local Artifact Model Tests
rig-candle has an ignored native model-contract suite. It is not an HTTP
cassette: it loads one pinned Qwen3 GGUF artifact and runs provider-neutral
completion, buffered/raw-streaming parity, parallel and sequential tools,
zero-argument and complex typed arguments, call/result history correlation,
result serialization, invalid-call recovery, hook rewrite chaining, turn-local
request patches, cancellation/max-turn diagnostics, extraction with usage,
tool-choice, protocol hygiene, and synthetic structured-output scenarios
through Rig's agent driver.
export RIG_CANDLE_TEST_MODEL_DIR="$PWD/crates/rig-candle/test-models/qwen3-4b-q4-k-m"
./crates/rig-candle/tests/download_qwen3.sh
cargo test --release -p rig-candle --test live_conformance \
-- --ignored --nocapture --test-threads=1The 2.33-GiB model is checksum-verified, cached in an ignored directory, and
loaded once per test binary. The measured ARM64 release run completed in 164.41
seconds; allow at least fifteen minutes for slower CPU hosts and more than twice
the checkpoint size during loading. Use serial execution to bound CPU and memory use.
See crates/rig-candle/README.md for revisions, hashes, measured performance,
and the boundary between model-contract and provider-transport tests.
Reusable scenarios and typed validators are exported fromrig_core::test_utils. A provider suite should call a complete model-driving
scenario when its cassette records the same prompt and tool definitions. When
wire-specific prompts, schemas, request parameters, or metadata must remain
local, the provider test should retain that transport setup and call the shared
validator on its public Rig result. Do not move authentication, HTTP body, SSE,
hosted-tool, remote-file, or provider-session assertions into the portable
module.
Universal scenarios require only the public completion/agent contract.
Optional capability scenarios—parallel model emission, structured reasoning,
provider-assigned IDs, native constrained decoding, and hosted tools—must be
selected explicitly. A provider that does not expose one optional capability
must not weaken the universal assertions or silently mark the scenario passed.
Integration Tests
External-service integration tests are collected under the integrations target and are gated by
feature flags. Some start Docker containers through testcontainers, so Docker must be running;
others are ignored because they need external credentials or pre-provisioned services. Check each
integration module for required environment variables (Vectorize requires VECTORIZE_INDEX_NAME;
Bedrock needs AWS credentials plus access to the configured Bedrock models).
# all enabled non-ignored integration tests
cargo test -p rig-service-tests --all-features --test integrations
# one feature-gated group
cargo test -p rig-service-tests --features qdrant --test integrations qdrant -- --nocapture
cargo test -p rig-service-tests --features mongodb --test integrations mongodb -- --nocapture
cargo test -p rig-service-tests --features sqlite --test integrations sqlite -- --nocapture
# ignored groups
cargo test -p rig --features bedrock --test integrations bedrock -- --ignored --nocapture --test-threads=1
cargo test -p rig-service-tests --features vectorize --test integrations vectorize -- --ignored --nocapture --test-threads=1Shared test implementation
test-support/rig-test-support compiles neutral tools, cassette paths, cache and
stream assertions and golden comparison once. Provider binaries import these
modules; their cassette-safety tests remain registered in each binary. The
generic corpus matrix drivers live undercrates/rig-cassette/tests/common/corpus_matrix: a cell edit then rebuilds its
provider binaries without invalidating unrelated providers. Their long-loop
regression tests stay with those modules. Other shared helper regressions run
in the support crate; its corpus_ regressions also retain the standalone
effect-corpus dependency configuration.
Matrix declarations keep literal scenario names and explicit per-cell parameters.golden_matrix! runs a common agent cell against its named oracle, andcase_matrix! selects a family body.
A case_matrix! declaration without a wrapper contains only scripted rows;
it registers tests without claiming cassette scenarios.
Their parsers reject malformed rows and exclude ignored rows from the recording
inventory. Keep a compiled listing when changing declarations: source discovery
alone does not establish that a configuration registers or executes a test.
Regression scenarios
Runtime scenarios and the reply bank
The agent loop, tool lifecycle, turn endings, resume, memory and the effect
bus behave the same on every provider. Their scenarios run
once, in the runtime target of rig-cassette
(crates/rig-cassette/tests/runtime.rs and runtime/), over replies from
the reply bank instead of a provider's cassette. A bank transport serves the
replies in order and ignores what was sent, so each reply still passes
through its provider's real decoder while request encoding stays pinned by
the request snapshots and the acceptance index. Streamed replies arrive one
server-sent event per chunk, so a consumer that stops at a delta drops the
stream before its end, as it does live.
The bank lives in crates/rig-cassette/fixtures/bank/ and is written bycargo xtask cassette bank from the corpus, offline:
.yaml: one real recorded reply per provider, completion
encoder, reply shape and called tools, the smallest the corpus holds, with
the interaction it came from. A reply shape is the status, the framing, and
the skeleton of the whole body or of each stream event: JSON keys and types
kept, values erased except discriminators such astypeandfinish_reason. A fixture whose test marks it hand-derived gives the bank
nothing.scripts.tsv: every fixture's replies as bank keys. A runtime scenario
names the scenario whose reply shapes it runs against
(rig_test_support::bank::script).pinned.txtandpinned.yaml: a scenario whose assertions read what a
reply says (an answer marker, tool arguments, reasoning that must be
non-empty) is listed by hand inpinned.txtwith its reason, and the bank
keeps that fixture's replies verbatim inpinned.yaml
(rig_test_support::bank::recorded).
An entry or script whose fixture is deleted stays in the bank, so pruning a
cassette does not take its replies from the runtime scenarios. pinned.txt
is the only file of the bank written by hand; never edit a reply, re-record
the cassette and rewrite the bank.
cargo xtask cassette bank # rewrite the bank after a cassette changed
cargo xtask cassette bank --check # CI's `verify --check bank`
cargo nextest run --locked --profile local -p rig-cassette --test runtimeThe runtime target holds the corpus matrix's cells on rig-agent (cells),
its focused families (families, faults), the turn-termination cells
(termination, run over every bank reply that decodes to the cell's
ending), the agent tool sessions (sessions), the lifecycle matrix
(lifecycle), and decode, which decodes every bank reply through its
provider's decoder and checks it against the tools and ending the bank read
off its bytes. Which families run here and which stay with their providers
is in crates/rig-cassette/tests/runtime/runtime-families.md.
Stream-fault cells
tests/providers/{gemini,openai}/cassette/stream_faults.rs drive the runner
through the real adapter into a stream that ends badly: scripted faults
served by rig::test_utils's sequenced transport, a recording cut before its
terminal (test-support/rig-test-support/src/stream_faults.rs), a Gemini
refusal, an in-band error after content. Every scripted frame is a
labelled constant with its provenance beside the cell; no cassette is
hand-edited and no new fixture is committed. A scripted cell pins what the
runtime does with the fault (the failure kind, the record, the committed
history, the tools that never ran); the request shape it would have sent is
pinned by the recording's owning test, not by the cell. Error classification
follows the one funnel inrig_core::provider_response (see AGENTS.md, Error Handling).
Consumed cassettes and goldens remain in Git; historical execution logs, proof
snapshots, and archive infrastructure are intentionally not maintained.
Overview (`crates/rig-agent/README.md`)
rig-agent
rig-agent contains Rig's classic agent runtime: builders, the serializable
sans-I/O run state, blocking and streaming drivers, typed hooks, contextual
tools, extraction, and runtime integrations.
Most applications should use the root rig facade, where this runtime is
enabled by default. Low-level provider and backend contracts live inrig-core.
Direct users import construction and prompting explicitly:
use rig_agent::prelude::*;
use rig_core::{Model, providers::openai::{self, OpenAI}};
let model = OpenAI::from_env()?.completion(openai::GPT_5_2);
let agent = AgentBuilder::new(model).build();
let answer = agent.prompt("Explain ownership briefly.").await?;Conversation memory
AgentBuilder::memory keeps an agent's conversation in a rig-memoryAsyncStore: a local MemoryStore or DirStore, or a store behind a network
or a JavaScript promise. A run loads the conversation (conversation(log)
names it) before its first model call and saves its messages with one append
once it finished. A failed load or save fails the run withPromptError::Memory; a run that fails or is cancelled saves nothing. A save
appends only if the log is as the save read it, the store checking and
appending in one step: of two runs that loaded the same log, the second to
save fails with SessionError::Changed and adds nothing to the log, even when
the saves overlap, in one process or several sharing the store. A save reads
only its own log, so one store can hold many conversations, a log each. Seeexamples/persistent_chat.rs.
Recording and replay
The runtime accepts rig_core::serve::Recorder, not a concrete log type.
Enable the separate rig-cassette crate's agent feature for logs and replay
adapters; neither crate configuration adds the native HTTP engine.
Retain a rig_cassette::effect_log::EffectLogRecorder and attach a clone withAgentBuilder::record_to. Choose keeping_stream_events() when the recording
must preserve stream items. Import rig_cassette::agent::AgentReplayExt and
call agent.stamp(recorder.take()) after the run, or stamp recorder.log() for
a snapshot. The extension trait also owns run_spec_hash andcheck_replayable. A host that owns the handler table attaches its recorder
through Handlers::record_to.
Register recorded handlers through rig_cassette::agent::replay::register_all
or register_all_checking, then run the agent as usual. The runtime has no
normal cassette dependency, even with all runtime features enabled.
See the cassette README for feature isolation,
checkpoint formats and migration from implicit recording/log getters.
Handlers and dispatch
An agent dispatches every effect through a Handlers table: its own, or a
host's given to AgentBuilder::over_handlers. A dispatch looks its handler up
and runs it inside the caller's future or stream, so no executor or driver is
involved, and dropping the future or stream cancels the dispatch and everything
it dispatched. Missing handlers report HandlerUnavailable.
Registration refuses a family change under a live key. Typed keys carry a family
proof; binding either a typed or untyped key checks the current descriptor's
family. A handler dispatches nested work through Handlers::serving(&dispatch),
whose dispatches are recorded as children of the one it serves.
Handler layers intercept requests and outcomes, but recording observes the
innermost handler. A denial produces no handler record; an outcome replacement
leaves the original handler answer in the log. Suspended approval retains the
in-flight dispatch. Recorder-provided adapter contexts propagate
to completion adapters unless the request supplies one explicitly; contexts
must distinguish concurrent logical calls, and adapter facts are not effect-log
entries.
The run protocol
rig_agent::run::AgentRun is a steppable, serializable state machine: it owns
every decision the agent loop makes — turn budget, tool-call validation and
recovery, history threading, structured-output policy, usage accounting, final
response — and performs no IO. A driver calls next_step() and acts on the
returned AgentRunStep (CallModel, CallTools, Done), feeding results
back with model_response / answer. This crate's futures loop is the
driver; the machine itself never awaits (a source-level guard keeps it so) and
is Serialize + Deserialize, so a run can be suspended between steps and
resumed in another process. Resume with the same Rig version; the envelope format
is checked but cross-version state compatibility is not guaranteed. Serialized
state includes conversation content and every recorded provider response body,
so its sensitivity and size grow with the run. Hosts must apply their persistence
and redaction policy before storing it. Constructing a run directly does not
execute an agent's configured hooks, tools, or retrieval.
Everything an agent loop is sits beside it in rig_agent::run — theRunSpec it is configured by, prepare_request (the pure (RunSpec, capabilities, history, tools, patch) → PreparedRequest step, so every driver
sends the same bytes), the output policy, the per-turn RequestPatch, the
run's response and error types, the invalid-call decisions, the streamed-turn
assembler and the loop-side transcript helpers — all sans-IO and serializable.
rig-core keeps only the message-model invariants (conversation::pairing, thetool::feedback constructors). A host that drives runs itself (an ECS schedule, a
job system) depends on this crate with default features off: that graph
carries no async runtime, transport or MCP client (a dependency guard pins it),
and tests/fixtures/agent_run_stepper is that host in miniature.
Runtime model routing
High-level agents are concrete values: models are registered in the agent's
handler table under a ModelRef label, and each model's ProviderCapabilities snapshot
is captured by value at registration. AgentBuilder::new registers its model
as default; AgentBuilder::named_model and model_route register models
under labels of your choice. The typed Model<W, T> and its erased twinDynModel<Completion> stay available for direct call and stream, and a
backend is a wire plus a transport rather than a trait implementation.
Replace the default on one agent value with set_model (a model value) orset_model_label (a registered label), or change one run's default candidate
with using_model(label) or using_model_value(model). ImplementAgentHook::on_model_select to route before every model call. Model
selection hooks chain in registration order, with the last selection winning
and a stop terminating the run. Per model-call boundary, completion-call hooks
resolve first and their merged RequestPatch is passed to the selection event
(ModelSelection::request_patch); only then does request preparation run
against the selected model's captured capabilities and issue the attempt. A
selected model cannot change while its future or stream is in flight. Retries
and calls after tool execution are new boundaries and may select another
model.
Hooks attached through AgentBuilder::add_hook apply to every later runner
from that agent; hooks appended to a prompt or runner apply only to that run.using_model changes the run's initial candidate but does not suppress routing
hooks. To force a model, omit routing hooks or append a final hook that always
selects it. Blocking and streaming prompts share this lifecycle: Stop
cancels before request preparation and provider execution, and dropping an
in-flight attempt still cancels it by dropping its retained future or stream.
Extractors support the same run-local choice: extract(...) returns aTypedRun, so extractor.extract(text).using_model(label) or.using_model_value(model) sets that run's default candidate. That model is
the default candidate for each extraction retry, routing hooks may replace it,
and the extractor's default is unchanged for later calls.
#[derive(Clone)]
struct RouteModels;
impl AgentHook for RouteModels {
fn on_model_select(
&self,
context: &HookContext,
_event: ModelSelection<'_>,
) -> ModelSelectionAction {
if context.turn() == 1 {
ModelSelectionAction::select("fast")
} else {
ModelSelectionAction::select("strong")
}
}
}
let agent = AgentBuilder::named_model("fast", fast_model)
.model_route("strong", strong_model)
.tool(search_tool)
.build();
let answer = agent
.prompt("Research, then synthesize")
.max_turns(3)
.add_hook(RouteModels)
.await?;Registered models and routing hooks contain live clients, callbacks, and policy
state, so hook stacks and hook actions are deliberately not serializable;
persist the model's label and register the model again at runtime.
Clones share the retained model safely, while replacing an agent clone has
ordinary value semantics. Concurrent runs keep independent default and
hook-stack snapshots; explicitly synchronized state captured by a hook follows
that hook's own clone semantics. See the credential-freeruntime_model_routing example for a complete two-model tool round trip.
Hook stacks preserve registration order, including nested stacks. Request patches
merge; dispatch patches and outcome replacements feed later hooks. Denials,
stops, and model-turn retries short-circuit later hooks, so observers that must
see an event belong before steering hooks. Dispatch interest is a gate: internal
families require opting in through observes. Replaced tool output reaches both
the model and result-content telemetry; handler recording retains the original
answer. Streaming deltas are provisional, and ModelTurnRetried tells consumers
to discard a rejected turn.
Keep hook-private state in the hook, keyed by run ID when shared across runs.
Use Scratchpad for transient cross-hook state and run entries (rig-coreconversation::Asides) for state that must serialize or follow forks. Full snapshots read through last_entry avoid
custom replay folds; event-shaped state can instead fold entries. Entries
appended during settlement do not enter the finished run. Context-bound handles
use delegation rather than Deref so hooks cannot clone an owned dispatch
capability out of their borrowed view.
Retries of tool-free turns consume the ordinary model-call budget. Hooks may
impose narrower limits in run-local state. ModelTurnFinished exposes the
attempt's terminal reason and effective output cap for portable truncation
policies; the credential-free retry_on_truncation example demonstrates cap
growth on both surfaces.
Tools implement rig_core::tool::Tool, work in both runtimes, and receive
each call's &mut ToolContext.
Tool output remains typed through dispatch; providers and telemetry render it
at their boundaries. Ordinary serializable results become model output, whileToolOutput and ToolResultContent preserve explicit structured or multimodal
content. Tool::map_error preserves arbitrary source errors for operators and
exposes safe kind-level feedback to the model by default. ExplicitToolExecutionError constructors expose their detailed messages; use them orwith_model_output only for deliberately model-visible feedback. Inbound
context and host-only result metadata belong in ToolContext, not model output.
Tool registries publish changes to every attached handler table. Generated tool
keys use <owner>/tool:<name>#<generation>, and a request catalog runs the
registration it advertised even after the registry replaced it. Explicit keys
are preserved verbatim: reusing one replaces its binding, even across tool
names. Standalone registry execution runs inline rather than through an agent's
handlers.
Target support
| Tier | Target | Status |
|---|---|---|
| 1 | native (linux / macOS / windows, x86_64 and aarch64) |
Full support, all features; MCP tools via the companion rig-rmcp crate |
| 2 | wasm32-unknown-unknown (browser) |
Supported, with no feature flags to set; rig-rmcp (MCP tools) is not available |
| — | wasm32-wasip1 / wasm32-wasip2 (WASI) |
Not supported |
| — | wasm32-unknown-emscripten |
Not supported |
Building for wasm32-unknown-unknown is the entire opt-in — there are no
wasm feature flags anywhere in the workspace. rig-core relaxes itsWasmCompat* bounds from the target alone.
Wasm gates name a target_os (all(target_arch = "wasm32", target_os = "unknown")) rather than a bare target_arch = "wasm32", because the latter
also matches WASI, which has no JS host. WASI itself does not build: rig-core
depends unconditionally on reqwest, which pulls hyper/socket2 and a tokio
feature set WASI rejects. Supporting it would mean making reqwest optional and
adding a wasi:http client behind rig_core::http_client — a project, not acfg fix.
MCP (rig-rmcp) is native-only. rmcp's ClientHandler is declaredSized + Send + Sync + 'static unconditionally — its local feature relaxes
the future bounds but not the handler itself — while this crate's handler owns a
tool registry whose erased handlers are deliberately neither Send norSync on wasm. Enabling rmcp on a wasm target fails with a single explanatorycompile_error! rather than a wall of trait errors.
Overview (`crates/rig-bedrock/README.md`)
Rig-Bedrock
This companion crate integrates AWS Bedrock as model provider with Rig.
Usage
Add the companion crate to your Cargo.toml, along with the rig-core crate:
[dependencies]
rig-bedrock = "0.4.5"
rig-core = "0.36.0"You can also run cargo add rig-bedrock rig-core to add the most recent versions of the dependencies to your project.
See the /examples folder for usage examples.
Make sure to have AWS credentials env vars loaded before starting client such as:
export AWS_DEFAULT_REGION=us-east-1
export AWS_SECRET_ACCESS_KEY=.......
export AWS_ACCESS_KEY_ID=......Overview (`crates/rig-candle/README.md`)
rig-candle
rig-candle runs validated local checkpoints through Rig's model and agent
APIs: CandleModel::completion() is the model a loaded checkpoint serves, andCandlePoseModel::pose() estimates human poses (see
Pose estimation). The crate receives byte buffers and
performs no filesystem or network access itself.
use rig_agent::agent::AgentBuilder;
use rig_candle::{CandleModel, ModelData};
#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let model = CandleModel::from_safetensors_async(ModelData {
config: std::fs::read("./model/config.json")?,
tokenizer: std::fs::read("./model/tokenizer.json")?,
weights: std::fs::read("./model/model.safetensors")?,
})
.await?;
let agent = AgentBuilder::new(model.completion())
.preamble("You are a concise assistant.")
.build();
println!("{}", agent.prompt("Explain ownership briefly.").await?.output());
Ok(())
}Validated model profiles
- Llama 3 instruct: one unsharded safetensors checkpoint, explicit Llama 3
conversation formatting, text conversations. - SmolLM2-360M-Instruct: Q4_K_M GGUF using Candle's quantized Llama backend,
explicit ChatML formatting. This remains the small native/WASM demo model. - Qwen3-4B: the official
Qwen3-4B-Q4_K_M.gguf, using Candle's
quantized Qwen3 backend and the explicit Qwen Hermes tool protocol. This is
the native-only agent-conformance profile; it is rejected on wasm32 because
its runtime memory cannot fit reliably in wasm32 linear memory.
ModelArtifacts selects safetensors versus GGUF explicitly. Native async
constructors and build_async perform validation and model construction on
Tokio's blocking pool; synchronous constructors remain available. A load that
has entered the blocking pool runs to completion even if its awaiting future is
dropped. builder_from_gguf_bytes gives borrowed GGUF buffers the same
generation/concurrency settings as owned artifacts without copying them,
including static buffers such as include_bytes!.
Arbitrary Qwen, Qwen2, Qwen3 MoE/vision, other sizes, shards, and unvalidated
quantizations are rejected rather than treated as Llama.
The loader validates architecture metadata before tensor allocation, exact
profile dimensions and special-token agreement, the complete GGUF vocabulary,
required tensor names/shapes, and the allowed Q4_K_M tensor mix. The effective
GGUF context limit is currently 4096 tokens because that is Candle 0.11's
quantized cache capacity.
Qwen3 tools and output behavior
Qwen3 supports function name, description, and arbitrary object-shaped JSON
Schema parameters, including optional properties, enums, nested objects, and
arrays. Auto, None, Required, and Specific tool choices are represented
explicitly. Specific exposes only the named functions; Required andSpecific add a mandatory-call instruction and reject a model turn with no
call. Malformed/non-object arguments, duplicate IDs, truncated envelopes, and
unmatched results are typed Candle completion errors. Syntactically valid
unknown or disallowed names reach Rig's agent dispatcher, where fail, repair,
retry, or skip handling produces the corresponding typed PromptError and
never executes the rejected call.
Argument validation is intentionally split: rig-candle validates the JSON
envelope and requires arguments to be an object, while Rig's registered tool
performs typed deserialization/schema validation before execution. Tool
execution always remains in Rig's agent driver. Assistant tool calls and
correlated text or JSON results round-trip through history. Multimodal tool
results and provider-hosted tools are rejected. Caller-controlled text, tool
schemas, arguments, and results containing reserved chat-template delimiters
are rejected before rendering so they cannot create structural prompt content.
Qwen tool syntax can cross token boundaries, so streaming buffers one model
turn, parses it, then emits ordered text/reasoning items and whole tool calls
(a Start/End pair per call on the stream) followed by the terminal
record. This keeps model XML out of user-visible text
while preserving cancellation, bounded backpressure, and the same parsed result
as buffered completion.
The explicit prompt requests Qwen's no-thinking mode. If a checkpoint emits a
leading `` block anyway, it is represented as Rig reasoning content and
omitted from later rendered history; control syntax is never exposed as normal
text.
Direct CompletionRequest::output_schema returns a typed unsupported-feature
error: decoding is not grammar constrained. Rig's OutputMode::Tool works and
is the supported path for a real tool followed by a synthetic structured final
result. That mode is best-effort and Rig validates the returned JSON; it is not
native grammar enforcement.
Pinned live model
Download the official artifacts with the reproducible helper:
export RIG_CANDLE_TEST_MODEL_DIR="$PWD/crates/rig-candle/test-models/qwen3-4b-q4-k-m"
./crates/rig-candle/tests/download_qwen3.sh
cargo test --release -p rig-candle --test live_conformance \
-- --ignored --nocapture --test-threads=1The script uses immutable revisions, retries resumable temporary downloads,
checks size and SHA-256, then atomically installs each file. Verified files are
reused and test-models/ is ignored.
| artifact | revision | bytes | SHA-256 |
|---|---|---|---|
Qwen3-4B-Q4_K_M.gguf |
bc640142c66e1fdd12af0bd68f40445458f3869b |
2,497,280,256 (2.33 GiB) | 7485fe6f11af29433bc51cab58009521f205840f5b4ae3a32fa7f92e8534fdf5 |
tokenizer.json |
1cfa9a7208912126459214e8b04321603b3df60c |
11,422,654 (10.89 MiB) | aeb13307a71acd8fe81861d94ad54ab689df773318809eed3cbe794b4492dae4 |
config.json |
1cfa9a7208912126459214e8b04321603b3df60c |
726 | 8ba006f74fecfaaeb392872a60f4a480e7ec9860153d2e1b769ec81f9a147f8a |
The live test loads the model once, reuses cheap CandleModel clones with a
fresh KV cache per request, applies greedy decoding and a fixed seed, enforces
timeouts, and prints tokens, tool counts, history size, timings, throughput, and
safe output. On the ARM64 development host used for the release-mode
verification, it passed a direct completion plus sixteen portable reports:
buffered/raw-streaming text
parity, optional arguments, parallel calls, serial host execution of a parallel
batch, a zero-argument tool, string/JSON result serialization, complex nested
Unicode and escaped arguments, invalid-call fail/repair/retry/skip recovery,
hook rewrite chaining with turn-local request patches, cancellation and
max-turn diagnostics, structured extraction with usage, sequential tools,
streamed tool execution, buffered and streamed synthetic structured output,
and all tool-choice modes. Actual resident memory and speed depend on the target
and concurrent system load; the loaded quantized tensors are based on a
2.33-GiB GGUF, KV cache grows with context, and loading temporarily also holds
the 2.33-GiB input byte buffer alongside constructed tensors. Plan for more than
twice the checkpoint size during loading rather than treating file size as a
hard memory bound.
Conformance boundary
Portable model-contract helpers live under rig_core::test_utils. The
artifact-backed Candle suite calls the model-driving scenarios directly.
Provider cassette suites reuse the same runners where their recorded request
already represents the scenario, and reuse the typed validators when transport
fixtures need provider-specific prompts or schemas. Current adoption covers
Gemini parallel/zero-argument/result-serialization, recovery, cancellation,
hook, extraction, and structured-output cases; OpenAI extraction and structured
output; Anthropic hook rewrites, extraction, and structured output; and Ollama
optional and sequential tools.
The universal model contract covers semantic text, typed arguments, canonical
call/result history, streaming finalization and usage, hook-visible outcomes,
extraction, and structured output. Parallel model emission, structured
reasoning, provider-assigned IDs, native constrained decoding, and hosted tools
remain optional capabilities and are selected independently rather than hidden
behind a passing umbrella test. Provider suites retain HTTP serialization,
authentication, SSE framing, hosted tools, remote files, and provider-specific
reasoning/session assertions; those are transport conformance, not local model
quality.
Pose estimation
rig_candle::pose runs YOLOv8 pose checkpoints in the safetensors layout of
Candle's YOLOv8 example (lmz/candle-yolo-v8, sizes n, s, m, l and x). ItsPoseEstimation operation takes RGB frames and yields one FramePoses event
per frame, in order: every person's box, confidence and the 17 COCO
keypoints, in the frame's pixels. A call folds the events into a PoseTrack.
The model is a Local wire on the loaded checkpoint, so it
streams, erases to DynModel and swaps transports like any
other Rig model.
use futures::StreamExt;
use rig_candle::pose::{BodyPart, CandlePoseModel, ImageFrame, PoseRequest};
use rig_core::streaming::Item;
# async fn run(weights: Vec<u8>, video: Vec<(u32, u32, Vec<u8>)>) -> Result<(), Box<dyn std::error::Error>> {
let model = CandlePoseModel::builder(weights)
.confidence_threshold(0.5)
.build_async()
.await?
.pose();
let frames = video
.into_iter()
.map(|(width, height, rgb)| ImageFrame::from_rgb8(width, height, rgb))
.collect::<Result<PoseRequest, _>>()?;
let mut poses = model.stream(frames)?;
while let Some(item) = poses.next().await {
if let Item::Event(frame) = item? {
for person in frame.people {
println!("{:?}", person.keypoint(BodyPart::LeftWrist));
}
}
}
# Ok(())
# }The loader reads the size from the checkpoint and refuses a detection or
non-COCO checkpoint before allocating tensors. Frames are resized so their
longest side is the builder's input_size (640 by default, a multiple of 32)
and the other keeps the aspect ratio; overlapping boxes aboveiou_threshold are suppressed. Frames run one at a time on a blocking worker,
and dropping the stream stops it before the next frame. A reply that ends
before every frame was answered fails the call.
The weights are Ultralytics' YOLOv8 pose weights and are licensed under
AGPL-3.0; rig-candle neither ships nor downloads them. Check that license
before you ship them. examples/candle_pose runs the model on JPEG or PNG
files, and the opt-in live test checks the nano checkpoint on a photo:
./crates/rig-candle/tests/download_yolov8_pose.sh
cargo test --release -p rig-candle --test live_pose -- --ignored --nocapture| artifact | revision | bytes | SHA-256 |
|---|---|---|---|
yolov8n-pose.safetensors (lmz/candle-yolo-v8) |
be388c6fab95ae3035a039070e1b883b9c5a1325 |
6,650,412 | 08c76047b41744c027c8150e81d1dbbd94f996656cf5be266491367ae7fad072 |
bike.jpg (huggingface/candle) |
7a62aad24a5d8b1d8cb351e5ef77a7b3457a74b8 |
182,991 | 317e4a9d2d2be7859ba0ab8726a526f5ece9d77daf92857f3e93fb7b367824c1 |
Runtime behavior and limits
Builder defaults can be overridden by request max_tokens and temperature,
by the GenerationOptions top_p and seed, and by the CandleOptions
provider options top_k, repeat_penalty and repeat_last_n
(rig_candle::extension). The same five keys are accepted inadditional_params, which take precedence; unknown keys are errors. Omittingtop_k or top_p inherits its builder default, while an explicit JSON null
in additional_params disables it.
Native inference runs in spawn_blocking; model loading and each complete
inference operation run inside Candle's CPU context so its private Rayon pool
stays active. max_concurrent_requests defaults to one. Every request owns its
cache and sampler. Cancellation is cooperative between Candle forwards, and the
bounded stream channel has capacity eight.
WASM inference is synchronous and should run in an application-owned Web Worker.
The maintained browser example embeds SmolLM2 at compile time, rejects modified
artifacts during its build, bounds input/history growth, and includes both a
model-independent worker test and an opt-in real-artifact smoke test. CPU only;
no CUDA/Metal selection, batching, multimodal prompts, or checkpoint shards.
Overview (`crates/rig-cassette/README.md`)
rig-cassette
Rig's record/replay home: effect logs and runtime replay adapters, the native
HTTP cassette engine, the committed fixture corpora, and their verification suites.
| part | path | published |
|---|---|---|
| effect logs | src/effect_log/ |
yes, always |
| classic-agent replay adapter | src/agent/ |
yes, agent feature |
| native HTTP engine | src/http/ |
yes, http feature |
| provider cassettes | fixtures/cassettes//...yaml |
no (exclude) |
| agent effect-log goldens | fixtures/effects/<name>.effects.json |
no (exclude) |
| cassette provider suites | tests/.rs, tests/providers/, tests/common/ |
no (exclude) |
| effect-bus verification | tests/verify/ |
no (exclude) |
| minimal verification runner | tests/minimal/Cargo.toml (shared test sources) |
no (publish = false, exclude) |
Features and dependency direction
Defaults are empty. An effect-log-only consumer uses:
rig-cassette = { version = "0.42.0", default-features = false }| features | public modules | additional normal dependencies |
|---|---|---|
| none | effect_log |
core contracts, futures, serialization and base64 only |
jsonl |
effect_log, effect_log::jsonl (JSON-lines files, native, or a file of a session's store such as SESSION_EFFECT_LOG; a recording for replay that no restore reads) |
rig-memory (its Files trait) |
agent |
effect_log, agent |
rig-agent, without its default features |
http |
effect_log, http |
the native HTTP server/client engine, Tokio, ordered/round-trip JSON |
bedrock |
effect_log, http |
http plus Smithy event-stream decoding |
Effect logs alone acquire neither the agent runtime, HTTP clients/servers,
Tokio nor AWS dependencies. The agent integration is selectable on its own and
does not enable HTTP. These guarantees concern the selected normal dependency
graph; Cargo can still unify features requested by other dependencies in the
same build.
The dependency direction is cassette → runtime → core. The runtime does not
depend on cassette, including through optional features. The rig facade re-exports
this crate as rig::cassette behind its opt-in cassette feature; with the
facade's agent feature it also enables the cassette agent adapter. Direct
minimal consumers should depend on rig-cassette, rather than the facade's
default transport configuration.
The facade and provider helpers needed by this package's tests remain
version-less path dev-dependencies, omitted from its published manifest.
The facade dev-dependency explicitly enables its capability features so provider
targets retain the same test inventory without relying on workspace defaults.
Independent downstream graph guards live in src/http/paths.rs andtests/core/dependency_graph.rs; they distinguish native-engine isolation from
the minimal library and the agent adapter.
Effect logs and runtime integration
rig_cassette::effect_log owns EffectLog, EffectLogRecorder,EffectLogReplayer, RequestCheck, header validation and stable hashing. The
log wire format and fingerprint inputs are unchanged.
The recorder implements rig_core::serve::Recorder; the replayer implements
the ordinary core handler interface.
Effect records require tool_output: null means no publication and an object
means published values, including an empty map. Omitting it cannot establish
whether values were lost, so decoding rejects omission. Replayers publish these
values before resolving outcomes, including errors. Handlers must publish before
resolving and must exclude inbound context, secrets, and live capabilities.
Custom outcomes keep their JSON value in an explicit payload field. Nested tool
identities retain origin tags; bare-string identities are rejected. Logs validate
their data rather than a global format number; unknown header fields, includingformat, are rejected. An interrupted run resumes from its saved state withEffectLog::tail replayed.
Replay preserves recorded semantic families, model identities, and capabilities.
Conflicting declarations are rejected. Callers reapply executable middleware.
Inferred descriptors for logs without declarations cannot establish verified
program compatibility.
The shared replayer supplies exchanges and event sequences. Without retained
stream items, replay cannot prove exact partial state. Stream error positions
preserve ordering even around a final event; a folded outcome cannot
reconstruct that order. Empty metadata is omitted, and logs without error
positions cannot prove the original error-item sequence.
For a classic agent, enable agent, retain an EffectLogRecorder, and pass a
clone to AgentBuilder::record_to. Use keeping_stream_events() instead ofnew() when original stream item boundaries are required. After driving the
agent, import rig_cassette::agent::AgentReplayExt and callagent.stamp(recorder.take()) (or stamp recorder.log() for a snapshot).
The extension trait also supplies run_spec_hash and check_replayable.
For a host-owned handler table, attach the recorder withHandlers::record_to. Replay registration isrig_cassette::agent::replay::register_all (or register_all_checking).
The engine
Enable http and import engine APIs from rig_cassette::http. This feature is
native-only and intentionally opts into both JSON features listed above.
Pass a fixture root containing provider directories to ProviderCassette::start,start_via(RecordVia::Direct, ..), cassette_path and every recorded_* reader:
<fixture-root>/anthropic/completion.yaml
<fixture-root>/openai/nested/scenario.yamlCassetteSpec::new preserves strict interaction order; .unordered() permits
matching any unused interaction. finish checks complete consumption and shuts
down the replay server. A replay session dropped without finish panics when
it still holds an unplayed interaction or a refused request, so a test that
returns early cannot pass on a recording it never played to the end. The guard
stays silent while the thread is already panicking, for a fully played session,
and after finish_after_test_result returns a test's own error. Invalid
fixtures and failed assertions panic, preserving the original test-support
behavior. Applications that record or replay outside a test useProviderCassette::try_start_at and try_finish, which return aCassetteError for each of those failures instead. try_finish shuts the
session down whatever its outcome, so dropping it afterwards never panics.
RIG_PROVIDER_TEST_MODE defaults to replay. record contacts the configured
upstream and overwrites the selected fixture after scrubbing; start andstart_via read the mode from the environment, and a recording reaches the
provider through the proxy (start) or directly (start_via(RecordVia::Direct, ..)).
Consumers that stage candidates use ProviderCassette::start_at with an explicit
mode and exact path: a live capture records into a candidate path, and a
verification pass replays with CassetteMode::Replay even when the environment
asks for recording, so a candidate is never implicitly promoted to a fixture.
While a live run is in progress, checkpoint_recording writes the completed,
scrubbed exchanges to a partial path without finalizing the recording.
RIG_CASSETTE_SNAPSHOTS is off unless set. With check, a replay session
also compares each request body with the fixture's request snapshot,<fixture>.requests.json (request_snapshot), and finish returnsCassetteError::SnapshotMismatch naming each difference by JSON pointer.
With write, the session rewrites the snapshot from the requests it received.
A snapshot holds only how a sent body differs from its recording, compared
after scrubbing as canonical JSON or as multipart parts, so a fixture whose
requests equal their recordings has none. This repository sets check for
its own cassette tests.
Replay compares request bodies exactly unless RIG_CASSETTE_MATCHING=shape
is set or the spec says .shape_matched(); .exact_matched() keeps exact
matching whatever the variable says. Shape matching still compares the
method, path, query and recorded headers exactly, but compares bodies by a
coarse key: the field structure, the order of arrays of objects, and the
values of model, role, name and stream, with every other value, the
types of content, tool schemas and tool arguments erased. A change to how a
request is written then replays without a new recording, and with snapshots
on in check mode the session fails until the snapshot holds the change.
Unordered shape matching serves, among the interactions with the request's
key, the one whose body is closest to the request's. This
repository sets shape for its own cassette tests.
A recording only becomes the fixture when the test passed and the recording is
clean. A failed test's exchanges, and a recording finish refuses, go underattempt_root() (RIG_CASSETTE_ATTEMPT_DIR, default cassette-attempts in the
target directory). finish refuses two kinds of recording:
- one holding an account failure that the cell did not declare: a refused
credential, a spent quota, a rate limit or an empty balance, as classified
byreply_account_failure, including a failure delivered inside a 2xx
stream. Cells declare one withCassetteSpec::expects_account_failure,ProviderCassette::expect_account_failureorbogus_api_key. - one in which an OpenAI or xAI Responses request stored a response that the
session never deleted (cassette_stored_state).
A relay in front of the proxy, and the direct recorder for unary and
multipart requests, append every created stored response, file, cache,
interaction, conversation and vector store to the ledger module'sledger.jsonl before the reply reaches the test. ledger::clean_up deletes
what the ledger still holds. The cassette_tool example exposes both checks
and the cleanup pass.
DirectRecorder, its request/response types and DirectRecordingHttpClient
preserve binary bodies that a text proxy cannot record. When the proxy omits a
non-UTF-8 multipart request body, replay accepts multipart input without comparing
its missing bytes; provider unit tests must cover the multipart shape. Other
requests with an absent recorded body must be empty. SSE and ordinary binary
responses require only http. Enable bedrock for Smithy event-stream decoding
and scrubbing; its Smithy dependencies are absent otherwise.
The engine retains credential scrubbing, strict request matching, and safety
validation; scrubbing also redacts every key shape rig_core::redact knows,
in text, bodies and headers. Provider-issued values (ids, signatures, encrypted reasoning,
cache keys, cursors, generated images) are recorded verbatim, and request
matching compares recorded bytes as they are. Repository-specific source
scans and fixture censuses remain with each caller. Rig's adapter intest-support/rig-test-support/src/cassettes.rs is a thin path binding that
supplies crates/rig-cassette/fixtures/cassettes; a downstream can supplyfixtures/cassettes of its own instead. Retry-After response headers retain
canonical seconds or HTTP dates for replay diagnostics; malformed values are
discarded instead of persisting arbitrary server text. Home-directory paths are
scrubbed separately because credential scans cannot identify operator names or
cache layouts. Only paths at the beginning of a string value are eligible,
preserving embedded public URLs and the model basename. Spaces remain part of
the path to avoid leaking account names.
The corpora
fixtures/cassettes// holds the recorded HTTP interactions the
provider suites in this package replay. fixtures/effects/*.effects.json holds
agent-produced effect logs. All fixtures are data: they are
regenerated by their producer, never edited by hand, and never regenerated to
make a check pass. .gitattributes exempts the cassettes from the
blank-at-eof whitespace check because SSE bodies legitimately end in a blank
line.
No corpus is embedded with include_*!; the binaries read them at runtime
from CARGO_MANIFEST_DIR, which is why exclude can keep them out of the
published tarball (the crates.io 10 MiB upload limit is enforced server-side
and never surfaces in cargo publish --dry-run).
The recording loop
Background, not an instruction to record: recording contacts a real provider
and is a deliberate, separately authorized act.
A golden is produced from the recorded HTTP in three steps:
- Record the HTTP. The producer test runs against the real provider under
RIG_PROVIDER_TEST_MODE=record, on its own exact test filter, and writes a
cassette underfixtures/cassettes/. The golden call is a no-op in that
mode. - Produce the agent golden. The same producer runs again in replay mode under
RIG_REGENERATE_GOLDEN=1, so the golden is generated from the replayed
cassette and holds exactly the bytes replay serves. - Replay the goldens. The suite here replays them with no provider behind any
key. A change in what the program asks (a kind), what it was answered (an
outcome) or how a stream was delivered (its events) fails the replay naming
the record and the JSON pointer of the difference. Fix forward, and
re-record live when the change is intended, never by hand-editing a golden.
Hooks are program (the header names them; a different stack is refused before
the first dispatch); tools are record (a replayer answers them); nothing the
engine mints is random, so the same program produces the same log twice.
Agent producers live in this package (tests/providers/*/cassette/corpus_*.rs)
and the root package (tests/core/golden_*.rs); the shared matrix driver istests/common/corpus_matrix.rs with its corpus_matrix/ modules.tests/core/golden_pairing.rs enforces one producer per golden.
The verification suite
tests/verify/main.rs is the single entrypoint: every matrix below is a module
of that target, not a target of its own, so the shared corpus implementation
(tests/corpus/mod.rs, which holds the program table and the dimension table of
an effect trace as a whole) is compiled once.
The unpublished rig-cassette-minimal package in tests/minimal/Cargo.toml
points at the same verify entrypoint and reuses the effect-log and classic
replay unit-test sources through its effect_log target. It depends onrig-cassette with only agent, without the native HTTP engine, facade or
provider helpers. Testing the main cassette package does not prove this
isolation: its dev-dependencies enable the native engine.
CI runs the minimal targets separately with zero retries, and the shared
verification target through the default-member graph with two. The all-features
workspace run excludes the nested package to avoid repeating the same sources
with unified features. core-all separately retains the migrated classic replay
unit tests; the default/all-features cassette library executes all migrated
library regressions. The planner tracks both the shared test entrypoints and
the shared library regression sources.
| matrix | module | subject |
|---|---|---|
| — | golden_replay.rs |
the original corpus: ten goldens, both agent interpreters |
| A | corpus_retrieval.rs |
retrieval effects |
| B | corpus_hooks.rs |
the hook surface |
| C | corpus_serving.rs |
serving policy, routing and bus ownership |
| D | corpus_outcome.rs |
continuation, cancellation and failure outcomes |
| E | corpus_request_shape.rs |
request-shape axes that change the spec hash |
| F | corpus_endings.rs |
hook-ended runs |
| G | corpus_invalid.rs |
invalid tool calls, streamed and ignored |
| H | corpus_output.rs |
output modes |
| I | corpus_host.rs |
a host's own families over the host's bus |
| J | corpus_memory.rs |
memory operations |
| K | corpus_delta.rs |
the delta wire |
| L | corpus_resume.rs |
resumption under everything |
| M | corpus_shaping.rs |
per-turn shaping |
| N | corpus_breadth.rs |
provider breadth for the pass-2 shapes |
| O | corpus_oracle.rs |
the oracle and the header |
| P | corpus_layers.rs |
layers |
| Q | corpus_causal.rs |
causal dispatch |
| S | corpus_header.rs |
the header's new types |
| T | corpus_leftovers.rs |
the leftovers of the #2443 review, and Denied |
Each module carries its own dimension table, its cells and what it found; that
header is the census, not this list. Beside the matrices, golden_refusal.rs
proves a stale golden refuses rather than passing on a different trace,record_replay.rs and durable_execution.rs pin recording and interrupted
runs, interpreters_agree.rs states interpreter agreement as a proptest
property, and log_header.rs pins the header.
The two agent interpreters (the classic runner and the direct AgentRun
driver, both in tests/corpus/mod.rs) replay the goldens their matrices
enumerate, including cancelled streams: the replayer answers the record as the
cancel it was, after the events it kept. A producer listed infixtures/in-test-goldens.txt replays its log record by record through its own
replayers by effect id instead of comparing it with a committed file.
What lives where
- here: effect logs, concrete replay adapters, the cassette engine,
fixtures/cassettes/, agent goldens infixtures/effects/, every
cassette-backed provider suite with its producers, and the behaviour of the
classic agent's handlers (record and replay, durable execution, the two
interpreters agreeing); - the root package's
tests/core: guards that scan the source tree and the
fixture runners (they need the repository root), plus the one-producer-per-
golden pairing guard; - the root package's
tests/providers: the live-only provider suites of
providers with no recorded corpus; rig-core/rig-agentunit tests: anything that needs crate-private types.
Running
RIG_PROVIDER_TEST_MODE=replay cargo nextest run --locked -p rig-cassette-minimal --test effect_log --retries 0
RIG_PROVIDER_TEST_MODE=replay cargo test --locked -p rig-cassette --lib --all-features
RIG_PROVIDER_TEST_MODE=replay cargo nextest run --locked -p rig-cassette --all-features -E 'binary(verify)'
RIG_PROVIDER_TEST_MODE=replay cargo nextest run --locked -p rig-cassette-minimal --all-features --retries 0
RIG_PROVIDER_TEST_MODE=replay cargo nextest run --locked -p rig-cassette --all-features --test <provider>RIG_REGENERATE_GOLDEN must be unset for all of them. The lane owners of these
executions, including their retry policies and the default-member graph twins,
are in xtask/src/verify/checks.rs (default-tests, effect-corpus,bus-verification).
Overview (`crates/rig-core/README.md`)
Rig
Rig is a Rust library for building LLM-powered applications that focuses on ergonomics and modularity.
More information about this crate can be found in the crate documentation.
Table of contents
Features
- Portable contracts for agent runtimes, including completions, messages, conversations, and tools
conversation: one conversation type with stable message ids, authors, asides, branches and a change log a store replays;rig-memorysaves it- Provider facts a host acts on: calls a provider cut off, prefill support, foreign reasoning
Model::check: a dry run that lists the options a model would drop- Full GenAI Semantic Convention compatibility
- 20+ model providers, all under one singular unified interface
- Built-in providers selectable as data:
Catalog::connect("anthropic/claude-opus-5-5")builds a model from avendor/modelreference with the key from the environment, in the protocol family the model catalog names, and the model encodes and prices with that catalog's facts.providers::registryreferences name a model or carry a whole typed configuration, and both round-trip through serde without a credential. Model references discard embedded credentials and reject empty identifiers; a configuration'sid()returns a catalog selection only when its dialect name is registered. Providers outside this catalog can use aModelandModelAdapterdirectly. - A model catalog an application can correct: lay a fetched copy of models.dev (
Catalog::from_models_dev, which skips what it cannot read) and the user's override file (Catalog::from_overrides, which rejects a misspelt vendor, field or model id with a "did you mean" hint) overCatalog::builtin()withwith_models_dev(which keeps rig's hand-reviewed facts) andwith_overrides, and connect through the result. catalog::Connector: reach a catalog's models byvendor/model, with keys or a sign-in- 10+ vector store integrations, all under one singular unified interface
- Full support for LLM completion and embedding workflows
- Support for transcription, audio generation and image generation model capabilities
- Integrate LLMs in your app with minimal boilerplate
- Full WASM compatibility (core library only)
Installation
cargo add rig-core --features reqwestThe reqwest feature gives provider clients a default HTTP transport, soOpenAI::from_env() needs no client of yours. Without it, pass a transport
with OpenAIConfig::from_env()?.connect(http).
WASM target support
rig-core supports the browser-oriented wasm32-unknown-unknown target. When
the pdf feature is enabled, the host must provide the Web Crypto API'sCrypto.getRandomValues implementation, as modern browsers, Web Workers, and
Node.js 19 or later do. WASI targets are not supported.
Simple example
use rig_core::{
completion::CompletionRequest,
providers::openai::{self, OpenAI},
};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// The client reads `OPENAI_API_KEY` and sends through the bundled reqwest
// client (rig-core's `reqwest` feature). A config's `connect(http)` uses another.
let model = OpenAI::from_env()?.completion(openai::GPT_5_2);
let response = model
.call(CompletionRequest::new("Who are you?").temperature(0.2))
.await?;
println!("{}", response.text());
Ok(())
}Note using #[tokio::main] requires you enable tokio's macros and rt-multi-thread features
or just full to enable all features (cargo add tokio --features macros,rt-multi-thread).
You can find more examples in the repository-level examples/ directory. Many provider-specific examples now also live as ignored live integration tests under the repository-level tests/providers directory, organized by provider. When running those provider-backed tests, prefer provider-specific targets such as cargo test -p rig --test openai -- --ignored --test-threads=1 to avoid rate-limiting. More detailed walkthroughs are regularly published on our Dev.to blog and added to Rig's official documentation at docs.rig.rs.
Integrations
Rig supports the following LLM providers out of the box:
- Anthropic
- Azure OpenAI
- ChatGPT and GitHub Copilot auth-backed clients
- Cohere
- DeepSeek
- Gemini
- Groq
- Hugging Face
- Hyperbolic
- llama.cpp (
llama-server, and llamafile) - MiniMax
- Mira
- Mistral
- Moonshot
- Ollama
- OpenAI
- OpenRouter
- Perplexity
- Together
- Venice
- Voyage AI
- xAI
- Xiaomi MiMo
- Z.ai
The built-in rig_core::vector_store::in_memory_store::InMemoryVectorStore
stores serializable documents without requiring Eq or Default. Its custom
ID callbacks accept closures that capture and mutate application state.
Vector stores are available as separate companion-crates and as feature-gated modules on the root rig facade:
rig = { version = "0.36.0", features = ["lancedb", "fastembed"] }- MongoDB:
rig-mongodb - LanceDB:
rig-lancedb - Neo4j:
rig-neo4j - Qdrant:
rig-qdrant - SQLite:
rig-sqlite - SurrealDB:
rig-surrealdb - Milvus:
rig-milvus - ScyllaDB:
rig-scylladb - AWS S3Vectors:
rig-s3vectors - HelixDB:
rig-helixdb - Cloudflare Vectorize:
rig-vectorize
The following providers are available as separate companion-crates:
- AWS Bedrock:
rig-bedrock - Fastembed:
rig-fastembed - Google Gemini gRPC:
rig-gemini-grpc - Google Vertex:
rig-vertexai
Who is using Rig?
Companies, applications, libraries and articles built on Rig are listed in awesome-rig, a community-maintained list. Using Rig? Open a pull request there to add your project.
Provider selection persistence
Registry references distinguish registered presets from explicit configurations.
Qualified vendor/format:model strings remain unambiguous when a vendor gains
another protocol family, but do not freeze preset defaults, endpoint availability,
or model names. Explicit configurations retain host, route, and typed options and
serialize as objects rather than lossy labels. References remove credentials;
standalone configurations may hold them at runtime. Deserialization requires a
self-describing format such as JSON.
Copilot presets accept already-exchanged session tokens and derive their endpoint
from those tokens. Explicit configurations keep their host and instruction
placement. Token exchange and asynchronous SDK setup belong to the host. The
catalog is not exhaustive: other models can use ModelAdapter directly.
Registry-qualified names do not replace the provider names used in telemetry.
Provider implementation
A provider has a serializable configuration (OpenAIConfig) and a client
(OpenAI), the configuration on a transport. The client builds one Model
per capability: the capability's wire, which says what to send and how to
read the reply, on the client's transport. One driver runs every model.
Chat-compatible dialects use the shared Chat wire and decoder withDialect data and BodyRewrite hooks rather than duplicating request conversion.
This keeps normalization, retry classification, and telemetry consistent.
An operation (wire::Operation) names its request, event and response types,
which event ends a reply, and the fold one reply's events pass through; it is
written once, and every provider of it is interchangeable behind a DynModel.
A crate outside Rig adds an operation and serves it with its own wire and
decoder over HTTP, or with driver::Local and a transport of its own for an
in-process runtime.
Each wire encodes requests without transport access and creates a fresh decoder
for each reply. Buffered and streaming replies use the same classifier and event
mapping. A buffered response with a distinct shape is another classified event,
not a separate normalization path. The driver owns framing and asynchronous I/O;
decoders can be exercised directly with frames without opening a connection.
Credentials stored in configuration use Secret, whose serialized redaction
must be replaced with a credential when the host reloads the configuration.
Construct normalized completion responses through their builders so finish
reasons reconcile with tool output. Preserve unknown terminal reasons in Other,
use the selected descriptor's provider name, and retain the decoded provider
payload in raw for typed inspection without a second request. Preserve error
bodies through ProviderError::from_http_response for failed HTTP responses andProviderError::from_provider_body for error envelopes on successful HTTP
responses.
Credentials require redacted debug output. Provider changes need coverage for
supported streaming, usage, tool and multimodal content, with examples and facade
exposure matching the configured capabilities.
Request serialization must use stable map ordering. Randomized HashMap
iteration can change request bytes and serialized schemas embedded in prompts,
reducing prefix-cache reuse. Compare raw serialized bytes when testing this;
canonicalized cassette JSON can hide ordering differences. The JSON helpers sort
map keys and recursively sort rendered values regardless of serde_json'spreserve_order feature.
Anthropic prompt caching
Manual caching marks the system prompt, final tool definition, and final message
block. Automatic caching delegates the moving conversation breakpoint to the
provider. Combined mode retains static-prefix markers within the four-marker
budget, including any markers already supplied on provider-specific tools.
A longer TTL can keep shared tools and instructions cached across conversations
without paying the longer storage lifetime for every conversation tail. Setwith_static_prefix_cache_ttl(CacheTtl::OneHour) with automatic five-minute
caching for this arrangement. One-hour markers must precede five-minute markers;
request encoding rejects the inverse ordering. Cache-write prices differ by TTL,
so choose a lifetime based on actual reuse.
Caching is skipped when the prefix through a marker is below the model's minimum:
| Model | Minimum tokens |
|---|---|
claude-opus-4-7, claude-opus-4-6, claude-opus-4-5 |
4096 |
claude-sonnet-4-6 |
2048 |
claude-sonnet-4-5, claude-opus-4-1, claude-opus-4, claude-sonnet-4 |
1024 |
claude-haiku-4-5 |
4096 |
Gemini explicit caching
Explicit caching uploads reusable content and returns a cachedContents handle.
Requests can reuse it immediately, including across conversations. Storage is
billed per token-hour until deletion or expiry, in addition to cached-input
charges. Implicit prefix caching is automatic and best-effort; it does not
provide a handle or guarantee a warm first request.
A request using an explicit cache cannot also send systemInstruction, tools,
or toolConfig. Rig rejects these conflicts before sending. Agents therefore
need no preamble, advertised tools, or configured tool choice on those turns.
An empty RequestPatch::active_tools allow-list suppresses tool advertisements,
but does not make cached function declarations executable by the agent. Agent
advertisement and dispatch share a registry snapshot. To execute functions
specified in a cache, drive GenerateContent directly and append matching
function responses yourself. Provider-hosted tools such as codeExecution
need no caller-side dispatch.
Native structured output and context documents can accompany a cache because
neither adds those conflicting fields. OutputMode::Tool adds a synthetic tool
and extends the preamble; OutputMode::Prompted adds schema instructions to the
preamble. Neither can accompany a cache. Extractors select tool output mode.
Cache resource paths accept a bare id or a cachedContents/ handle. Validation
rejects path separators, query delimiters, fragments, and traversal segments
rather than escaping them, so lookup, expiry updates, and deletion cannot be
retargeted by an invalid handle. A 403 or 404 on an existing handle maps toProviderError::CacheExpired, preserving the provider's reply because 403 can
also indicate credential or quota problems. Cache creation does not apply this
mapping, so authorization failures do not become recreation loops.
Provider observations
observe::AdapterContext carries a caller-owned operation identity and aWitness sink. Pass it separately from request data throughModel::call_observed(request, context) or Model::stream_observed(request, context); Model::call and Model::stream record nothing. Request builders
and request literals contain only provider request data.
Bus-backed ModelHandle::call_observed and ModelHandle::stream_observed
take the same context. ModelAdapter forwards Dispatch::adapter_context;
explicit caller context takes precedence over Recorder/Observe context for
that invocation, including through handler layers. It is never serialized into
provider data or effect records and is not inherited by child calls.
Clone the context for attempts of the same operation;
use a distinct non-sensitive identity for another logical call.for_host_attempt(subject, ordinal) rebinds a host retry to its current
dispatch subject while sharing the logical operation and HTTP send counter.
Facts carry the optional host ordinal separately: multiple HTTP sends within
one dispatch share its host ordinal. Already-running attempts keep their
original subjects and ordinals. A changed logical request needs a new context.
The Gemini unary and SSE paths emit Action::Adapter facts for send, HTTP
response status, provider metadata, error envelopes and closure through their
shared drivers. Unary closure
reports decoding, error or drop. SSE closure distinguishes terminal, EOF,
partial frame, error and drop; recoverable corrupt frames are separate facts.
Usage facts retain provider-reported optional counts even when decoding or
normalization rejects a response. Each fact is a cumulative snapshot for its
attempt: replace earlier snapshots instead of summing them, keep absent counts
unknown, and do not add overlapping token categories to invent a total.
Provider verdicts retain reported finish/block reasons and model versions;
error-envelope fields remain separate from HTTP status and Rig retryability.
Sparse verdict fields update only when present. Response IDs and allowlisted
headers are bounded, scrubbed analysis data, excluded from semantic comparison.
An ID-only payload attaches its latest ID to the next verdict or attempt
closure without creating an empty semantic event. This also preserves the ID
on error and consumer drop. Known request credentials are redacted before
persistence; arbitrary response bodies and headers are not copied wholesale.
Credential extraction includes URL userinfo and known query keys, including
origin-form request URIs. Diagnostic comparisons account for percent escapes
and control removal without rewriting safe diagnostic text. Hosts can reuseobserve::diagnostic_url_secrets for endpoint configuration; its returned
secrets must stay in memory and must never be persisted or logged.
The normalized Gemini unary API closes after normalization, so an empty
decoded response closes as an error; the raw API retains its decode boundary.
Error closures also preserve the original typed error boundary before report
conversion erases HTTP subtypes: request construction, provider response,
response decoding, typed transport termination, or unknown. An opaque client
error stays unknown; its message is never used to infer a boundary. This field
does not change the error returned to the caller or its retryability.ObservationLog::with_clock optionally stamps individual facts with host elapsed
time. Rig does not calculate run, handler or transport intervals. These stamps
describe the executing environment, including replay, and do not establish live
provider performance.
Partial-frame facts count raw bytes after the last blank SSE delimiter without
retaining their contents. Complete-frame counts include only frames passed to
the adapter driver, excluding provider-recognized analysis-only frames (Gemini
frames containing only a valid string responseId). Unknown, malformed and
otherwise-empty frames still count. Corrupt-frame ordinals start at one using
the same counting rule. TransportEof
records body completeness independently: a provider terminal followed by
an incomplete trailing frame retains both its terminal closure and partial
EOF evidence. The context does not promise cross-execution identity. Response bodies and
credentials are not copied into these boundary facts.
Overview (`crates/rig-ecs/README.md`)
rig-ecs
The kernel of the rig coding agent, on Bevy and
Rig's rig-core, for building your own
agent harness. It holds only what every agent needs, whatever its interface.
Subagents, compaction, usage and the effect log are plugins on its public API.
agent: an agent is an entity whose components hold its conversation, model,
reasoning setting, system prompt and tool access. A running turn is an entityTurnOfits agent, and its calls are entitiesCallOfthe turn.stage: every step of an agent's work is an effect entity. Each frame theAgentPassschedule runs its stages (Arrive, Review, Commit, Admit, Prepare,
Gate, Start) until nothing moves, and a plugin steps in with an ordinary
system in a stage.turn: the turn loop: model requests, tool calls, retries, stops, and how a
turn ends (TurnOutcome).tools,prompt: tools and system-prompt sections are entities plugins
register.inbox: a message to an agent is an effect too (Incoming, sent byDeliver).model: the agent's catalog model and how it is connected.tasks,effects: work off the main thread on Bevy's task pools, and the
one path every effect is dispatched and recorded through.session_log,restore: the session log in any rig-memoryStore, the
restore after a restart, and state kept with the session (Saved).
The rules a plugin follows, with an example per extension point, are in
rig-harness's plugin guide.
AgentPlugin adds Bevy's TimePlugin when the app has none. The crate has no
features and builds for wasm32-unknown-unknown. Therig-harness terminal coding agent is built on it.
This crate replaces an older, unrelated rig-ecs (0.43–0.44) on crates.io.
Overview (`crates/rig-fastembed/README.md`)
Fastembed integration with Rig
This crate allows you to use fastembed-rs with Rig.
Unlike the providers found in the core crate, fastembed does not compile to the wasm32-unknown-unknown target.
Installation
[dependencies]
rig-fastembed = "0.4.0"
rig-core = "0.36.0"The default features enable Hugging Face model downloads and ONNX Runtime binary
downloads through fastembed. The root rig facade exposes this crate with thefastembed, fastembed-hf-hub, and fastembed-ort-download-binaries features.
See examples/vector_search_fastembed.rs
and examples/vector_search_fastembed_local.rs
for end-to-end vector search examples.
Overview (`crates/rig-gemini-grpc/README.md`)
Rig-Gemini-gRPC
This companion crate sends the Google Gemini API's GenerateContent and EmbedContent calls over gRPC instead of REST. The request is the Gemini REST provider's request transcoded to protobuf, and each reply is transcoded once to its REST JSON and read by the same decoder, so the history rules, typed options and replies match rig_core::providers::gemini.
Usage
Add the companion crate to your Cargo.toml, along with the rig-core crate:
[dependencies]
rig-gemini-grpc = "0.44.0"
rig-core = "0.44.0"You can also run cargo add rig-gemini-grpc rig-core to add the most recent versions of the dependencies to your project, or enable the rig facade's gemini-grpc feature and use rig::gemini_grpc.
See the /examples folder for usage examples.
Setup
Set your Gemini API key as an environment variable:
export GEMINI_API_KEY=your_api_key_hereExample
use rig_core::completion::CompletionRequest;
use rig_core::providers::gemini::GeminiConfig;
use rig_gemini_grpc::GeminiGrpc;
# async fn example() -> anyhow::Result<()> {
let transport = GeminiGrpc::connect(&GeminiConfig::from_env()?).await?;
let reply = transport
.completion("gemini-2.5-flash")
.call(CompletionRequest::new("Hello!"))
.await?;
# let _ = reply;
# Ok(())
# }GeminiGrpc::connect reads the same GeminiConfig as the REST client and
connects to its base URL. GeminiGrpc::with_channel sends through a tonic
channel the host built instead.
Features
- Completion, unary and streamed
- Embedding generation
- Tool calling
- Reasoning and thought signatures
- Image input
- Typed options and reply extras (
extension::GeminiGrpcExt)
Overview (`crates/rig-harness-protocol/README.md`)
rig-harness-protocol
The protocol between the rig launcher and the rig-harness agent it builds and
runs: the RIG_HOME layout, session ids, the environment the launcher passes
to the agent, and the exit code with which the agent asks for a restart. It
uses only std, so the launcher stays dependency-free.
Overview (`crates/rig-harness/README.md`)
rig-harness
The harness of the rig coding agent: a terminal coding agent that is a
Bevy app made of plugins, on the rig-ecs
kernel and Rig's rig-core. The rig
launcher generates a small Cargo project for it from RIG_HOME/plugins.toml,
builds it and runs it; /reload rebuilds it with changed plugins and restarts
in the same session. The agent guide
describes using it.
A kernel, a harness and plugins
- The kernel is rig-ecs: agents, turns and calls as entities, message
delivery, tools as entities, the session log and its restore. - The harness is this crate: what the binary needs to run and be
relaunched, and the services every plugin shares.HeadlessPluginsis
Bevy'sMinimalPluginswith the log, a clean exit on signals and a loop that
sleeps until there is work.RigHarnessPluginsadds the session and its
store (StoreOpener, which refuses, unchanged, a session of another
format), the kernel, the launcher protocol
(Invoked,AppStart,Secrets,Exit), slash commands, fronts, and the
shared services every agent uses: settings, the workspace and child
processes, file watches, plugin data, questions and pickers, and transcript
notes. The generatedmain.rsends withrig_harness::run. - Everything else is a plugin in a crate of the repository's
plugins/folder, listed inplugins.tomland built only
on public API, exactly as a third-party plugin is: the compiler holds them
to it. Most are inrig-basics, one module
per group, with the services only some plugins use: approvals, the preview
and the checkpoint of tool calls, and the session picker. The launcher's
generatedmain.rsadds each withload, which records it as an entity
(PluginSource) and marks what it adds (ProvidedBy). Any entry can be
removed.
The default plugins.toml, in order:
| Crate | Plugin | What it adds |
|---|---|---|
rig-basics |
prompt::DefaultPromptPlugin |
rig's own system prompt for an agent without one: one fixed text, the same on every request |
rig-basics |
agents_md::AgentsMdPlugin |
the AGENTS.md files from the working directory up to the repository root, and RIG_HOME/AGENTS.md, in the system prompt |
rig-basics |
skills::SkillsPlugin |
the skills (SKILL.md) of the project, RIG_HOME/skills and ~/.agents/skills, listed in the system prompt; /skill:<name> |
rig-basics |
models::ModelsPlugin |
/model, /effort and the model picker |
rig-basics |
login::LoginPlugin |
/login and /logout: the ChatGPT plan, and providers' API keys |
rig-basics |
models::defaults::DefaultsPlugin |
a new session starts on the last model and reasoning chosen |
rig-basics |
sessions::SessionsPlugin |
/new, /resume, /name |
rig-basics |
sessions::agents::AgentsPlugin |
/fork, /agents and rewind (Alt+Up to an earlier message, then Enter): the agents of this session |
rig-basics |
compaction::CompactionPlugin |
summarizing a conversation near the context window; /compact |
rig-basics |
telemetry::usage::UsagePlugin |
what model calls cost; /usage |
rig-basics |
activity::ActivityPlugin |
what each agent is doing, for views |
rig-basics |
coding_tools::ReadTool, coding_tools::EditTool, coding_tools::WriteTool, coding_tools::ShellTool |
the coding tools of rig-tools, one plugin each, acting in the app's workspace; the library's search, list, delete and move are left to shell |
rig-basics |
coding_tools::AttachPlugin |
the files the user names as @path, from the agent's directory, go with the message |
rig-basics |
coding_tools::remote::RemoteHostsPlugin |
a path written HOST:PATH is that host's for every tool, through the transport [remote.hosts] in settings.toml gives it (such as ssh or koh exec); nothing without hosts |
rig-basics |
reload::ReloadPlugin |
/reload and the reload tool, with which the agent rebuilds itself, and, when the launcher started the agent, the system prompt's fixed self_extension section on how it writes plugins |
rig-basics |
reload::plugins::PluginsPlugin |
/plugins: every entry of plugins.toml, enabled and disabled, in a picker that turns them on and off and rebuilds at once |
rig-basics |
BasicCommandsPlugin |
/help, /retry, /quit |
rig-basics |
print::PrintPlugin |
--print and --json: the fronts of a run with -p, with stdin piped in, or driven by JSON lines (rig help-agents) |
rig-tui |
TuiPlugin |
the terminal view: the front of a run on an interactive terminal, with the status line and pickers other plugins add to |
Listed disabled by default, each a commented-out entry that /plugins orrig plugin enable <type> enables:
| Crate | Plugin | What it adds |
|---|---|---|
rig-basics |
approval::ApprovalPlugin |
one question for every effect a plugin wants approved; nothing asks by default, and SubagentsPlugin adds it |
rig-basics |
preview::PreviewPlugin |
what a tool call would change on disk, before it runs, for gates that ask |
rig-basics |
checkpoint::CheckpointPlugin |
what each tool call changed on disk, from snapshots before and after |
rig-basics |
telemetry::effect_log::EffectLogPlugin |
every model and tool call in the session's effects.jsonl |
rig-basics |
helpers::HelpersPlugin |
the helper tool: tasks for rig -p --json child processes, one, side by side or in a chain |
rig-basics |
subagents::SubagentsPlugin |
the task, message and wait tools, and the agents at work in the status line |
rig-basics |
telemetry::diagnostics::DiagnosticsPlugin |
the process's warnings and errors, Bevy's included, as the Diagnostics resource |
rig-inspect |
InspectPlugin |
the inspect tool, with which the agent reads its own running world |
rig-steel |
SteelPlugin |
the run_steel tool, whose Steel program orchestrates agents and tools |
An entry names its crate, crate = "rig-tui", and no source: the agent
builds rig's own crates from crates.io at the launcher's version, or from
the rig checkout it is built from (RIG_SOURCE). A plugin crate that adds to the terminal view (status
line items, pickers, tool renderers) does so behind a default tui cargo
feature, so it also builds without rig-tui.
The optional rig-inspect plugin adds theinspect tool, with which the agent reads its own Bevy world, read-only.rig-steel adds code mode.
PLUGINS.md shows how to write a plugin: an index from task to
section, then one example per extension point.keep_last_compaction replaces
a default plugin, the compaction, on public API alone. A plugin's tests run a
small app frame by frame, and stage_conflicts checks that its stage systems
are ordered against those of the plugins it is used with
(testing a plugin).
Install
git clone https://github.com/0xPlaygrounds/rig && cd rig
cargo install --path .
rigIt needs Rust 1.97.1 or newer and runs on Linux and macOS. The optionalrig-inspect plugin needs more to build on Linux; its README says what.
Overview (`crates/rig-helixdb/README.md`)
Rig HelixDB integration
This crate integrates HelixDB into Rig, allowing you to easily use RAG with this database.
Installation
To install this crate, run the following command in a Rust project directory which will add rig-helixdb as a dependency (requires rig-core added for intended usage):
cargo add rig-helixdbThere's a few different ways you can run HelixDB:
- Through HelixDB's cloud offering
- Running it locally through their
helix startcommand (requires the Helix CLI to be installed).- For local dev, you will likely want to use
helix push devfor continuous iteration.
- For local dev, you will likely want to use
How to run the example
Before running the example, you will need to ensure that you are running an instance of HelixDB which you can do with helix dockerdev run.
Once done, you will then need to deploy your queries/schema. The queries/schema in the examples/helixdb-cfg folder are a required minimum to be use this integration. rig-helixdb also additionally provides a way to get a manual handle on the client yourself so that you can invoke your own queries should you need to.
Assuming rig-helixdb is your current working directory, to deploy a minimum viable configuration for HelixDB (with rig-helixdb) you will need to cd into the helixdb-cfg folder and then run the following:
helix push devThis will then deploy the queries/schema into your instance.
To run the example, add your OpenAI API key as an environment variable:
export OPENAI_API_KEY=my_keyFinally, use the following command below to run the example:
cargo run --example vector_search_helixdb --features rig/deriveOverview (`crates/rig-http/README.md`)
rig-http
The transport contracts every Rig
provider sends through:
http_client:HttpClientExt, the trait a transport implements;DynHttpClient, an erased
client with middleware (HttpMiddleware); request framing, multipart forms and the error type;ws_client(featurewebsocket): the websocket connection contract the
Responses WebSocket session is written against;wasm_compat: theSend/Syncbounds and boxed futures that relax on
browser wasm;runtime(featuretokio, native): the tokio runtime the bundled
transports poll their I/O in, from any executor.
This crate has no transport of its own. rig-reqwest (HTTP) andrig-tungstenite (websocket) are the bundled transports, and depend only on
this crate. rig-core re-exports these modules at rig_core::http_client,rig_core::ws_client and rig_core::wasm_compat, so provider code and
users reach them there.
A transport author depends on rig-http alone and implementsHttpClientExt:
use rig_http::http_client::{HttpClientExt, DynHttpClient};
fn erase(client: impl HttpClientExt + Clone + 'static) -> DynHttpClient {
DynHttpClient::new(client)
}The test-utils feature adds HTTP client doubles for tests.
Overview (`crates/rig-lancedb/README.md`)
Rig-Lancedb
This companion crate implements a Rig vector store based on Lancedb.
Pre-requisites
If you are using rig-lancedb locally, you must ensure you have protoc (the Protobuf Compiler) installed.
Usage
Add the companion crate to your Cargo.toml, along with the rig-core crate:
[dependencies]
rig-lancedb = "0.4.5"
rig-core = "0.36.0"You can also run cargo add rig-lancedb rig-core to add the most recent versions of the dependencies to your project.
See the /examples folder for usage examples.
Overview (`crates/rig-memory/README.md`)
rig-memory
Saved conversations for Rig: recent messages,
compaction, and sessions saved as JSON-lines logs (rig-session/4) that can be resumed,
rewritten, and an agent's log forked up to a turn boundary. With default features it is pure data and builds for wasm32;
feature fs adds DirStore and DirFiles. load_async and save_async keep a conversation in
any AsyncStore, such as a database or IndexedDB, and every local store is one; rig-agent'sAgentBuilder::memory keeps an agent's conversation there. A store appends to a log only if the
log is still as long as the writer read it, checking and appending in one step, so of two saves
that read one log the second is refused. See the crate docs for the API.
Overview (`crates/rig-milvus/README.md`)
Rig Milvus integration
This crate integrates Milvus into Rig, allowing you to easily use RAG with this database.
Installation
To install this crate, run the following command in a Rust project directory which will add rig-milvus as a dependency (requires rig-core added for intended usage):
cargo add rig-milvusThere's a couple of different ways you can run Milvus:
- Through a Docker container, either locally or on Docker-compatible architecture
- Their Docker container requires using an install script to use which is listed on the page and can be found directly on the Milvus repo.
- They also support Kubernetes, although you will probably need to already know how to use Kubernetes to do this.
- Using the Zilliz cloud offering
- Using the cloud offering you can manage your Milvus instance through their web UI.
Before creating a collection that is compliant with rig-milvus, ensure you set up your Milvus related environment variables:
export MILVUS_DATABASE_NAME=
export MILVUS_COLLECTION_NAME=
export MILVUS_USERNAME=
export MILVUS_PASSWORD=
export MILVUS_BASE_URL=To create a collection, you will need the bash script below.
export TOKEN="${MILVUS_USERNAME}:${MILVUS_PASSWORD}"
curl --request POST \
--url "${MILVUS_BASE_URL}/v2/vectordb/collections/create" \
--header "Authorization: Bearer ${TOKEN}" \
--header "Content-Type: application/json" \
-d '{
"collectionName": "${MILVUS_COLLECTION_NAME}",
"dbName": "${MILVUS_DATABASE_NAME}",
"schema": {
"autoId": true,
"enabledDynamicField": false,
"fields": [
{
"fieldName": "embedding",
"dataType": "FloatVector",
"elementTypeParams": {
"dim": "1536"
}
},
{
"fieldName": "document",
"dataType": "JSON",
},
{
"fieldName": "embeddedText",
"dataType": "VarChar"
}
]
}
}'This will create a collection with the following fields:
- an ID field (auto-generated as i64 but converted to String when using
top_n()) - an
embeddingfield for storing vectors - a
documentfield (for storing metadata) - an
embeddedTextfield (for the actual item that was embedded)
Once done, you'll now be ready to use!
How to run the example
To run the example, add your OpenAI API key as an environment variable. Don't forget your Milvus username, password, database & collection name and endpoint URL if you're executing this from a new terminal tab/window:
export OPENAI_API_KEY=my_key
export MILVUS_DATABASE_NAME=
export MILVUS_COLLECTION_NAME=
export MILVUS_USERNAME=
export MILVUS_PASSWORD=
export MILVUS_BASE_URL=Finally, use the following command below to run the example:
cargo run --example vector_search_milvus --features rig/deriveOverview (`crates/rig-mongodb/README.md`)
+Rig-MongoDB
This companion crate implements a Rig vector store based on MongoDB.
Usage
Add the companion crate to your Cargo.toml, along with the rig-core crate:
[dependencies]
rig-mongodb = "0.4.5"
rig-core = "0.36.0"You can also run cargo add rig-mongodb rig-core to add the most recent versions of the dependencies to your project.
See the /examples folder for usage examples.
Overview (`crates/rig-neo4j/README.md`)
+This companion crate implements a Rig vector store based on Neo4j Graph database. It uses the neo4rs crate to interact with Neo4j. Note that the neo4rs crate is a work in progress and does not yet support all Neo4j features. Further documentation on Neo4j & vector search integration can be found on the neo4rs docs.
Prerequisites
The GenAI plugin is enabled by default in Neo4j Aura.
The plugin needs to be installed on self-managed instances. This is done by moving the neo4j-genai.jar file from /products to /plugins in the Neo4j home directory, or, if you are using Docker, by starting the Docker container with the extra parameter --env NEO4J_PLUGINS='["genai"]'. For more information, see Operations Manual → Configure plugins.
Usage
Add the companion crate to your Cargo.toml, along with the rig-core crate:
[dependencies]
rig-neo4j = "0.5.5"
rig-core = "0.36.0"You can also run cargo add rig-neo4j rig-core to add the most recent versions of the dependencies to your project.
See the examples folder for usage examples.
- examples/vector_search_simple.rs shows how to create an index on simple data.
- examples/vector_search_movies_consume.rs shows how to query an existing index.
- examples/vector_search_movies_create.rs shows how to create embeddings & index on a large DB and query it in one go.
Notes
- The
rig-neo4j::vector_indexmodule offers utility functions to create and query a Neo4j vector index. You can also create indexes using the Neo4j browser or directly call cypther queries with the Neo4rs crate. See the Neo4j documentation for more information. Example examples/vector_search_simple.rs shows how to create an index on existing data.
CREATE VECTOR INDEX moviePlots
FOR (m:Movie)
ON m.embedding
OPTIONS {indexConfig: {
`vector.dimensions`: 1536,
`vector.similarity_function`: 'cosine'
}}Roadmap
- Add support for creating the vector index through RIG.
- Add support for adding embeddings to an existing database
- Add support for uploading documents to an existing database
Overview (`crates/rig-postgres/README.md`)
+Rig-postgres
This companion crate implements a Rig vector store based on PostgreSQL.
Usage
Add the companion crate to your Cargo.toml, along with the rig-core crate:
[dependencies]
rig-core = "0.36.0"
rig-postgres = "0.2.5"You can also run cargo add rig-core rig-postgres to add the most recent versions of the dependencies to your project.
PostgreSQL setup
The crate utilizes pgvector extension, which is available for PostgreSQL version 13 and later. Use any of the official or alternative methods to install psql.
You can install Postgres using Docker:
docker pull pgvector/pgvector:pg17
docker run -e POSTGRES_USER=myuser \
-e POSTGRES_PASSWORD=mypassword \
-e POSTGRES_DB=mydatabase \
--name my_postgres \
-p 5432:5432 \
-d ankane/pgvectorNow you can configure Postgres, the recommended way is using sqlx and migrations (you can find an example inside integration tests folder).
Example sql:
-- ensure PgVector extension is installed
CREATE EXTENSION IF NOT EXISTS vector;
-- create table with embeddings using 1536 dimensions (based on OpenAI model text-embedding-3-small)
CREATE TABLE documents (
id uuid DEFAULT gen_random_uuid(), -- we can have repeated entries
document jsonb NOT NULL,
embedded_text text NOT NULL,
embedding vector(1536)
);
-- create index on embeddings
CREATE INDEX IF NOT EXISTS document_embeddings_idx ON documents
USING hnsw(embedding vector_cosine_ops); -- recommended for text embeddingsYou can change the table name and the number of dimensions but keep the same fields schema.
You can use different indexes depending the type of distance method you want to use, check PgVector documentation.
Usage
Declare the database URL:
export DATABASE_URL="postgres://myuser:mypassword@localhost:5432/mydatabase"Define the document you want to index, it has to implement Embed, Serialize and Deserialize.
Note: you can index different type of documents in the same table.
Example:
#[derive(Embed, Clone, Serialize, Deserialize, Debug)]
pub struct Product {
name: String,
category: String,
#[embed]
description: String,
price: f32
}Example usage
// OpenAI's embedding model, erased once: the builder and the store each
// take a clone of the handle.
let model = rig::providers::openai::OpenAI::from_env()?
.embedding(rig::providers::openai::TEXT_EMBEDDING_3_SMALL, None)
.erase();
// connect to Postgres
let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL not set");
let pool = PgPoolOptions::new() .connect(&database_url) .await?;
// run migrations (optional but recommended)
sqlx::migrate!("./migrations").run(&pool).await?;
// init documents
let products: Vec<Product> = ...;
let documents = EmbeddingsBuilder::new(model.clone())
.documents(products)
.unwrap()
.build()
.await?;
// Create your index
let vector_store = PostgresVectorStore::with_defaults(model, pool);
// store documents
vector_store.insert_documents(documents).await?;
// retrieve embeddings
let req = VectorSearchRequest::builder()
.query("Which phones have more than 16Gb and support 5G")
.samples(50)
.build();
let results = vector_store.top_n::<Product>(req).await?;
...Overview (`crates/rig-qdrant/README.md`)
Rig-Qdrant
Vector store index integration for Qdrant. This integration supports dense vector retrieval using Rig's embedding providers. It is also extensible to allow all hybrid queries supported by Qdrant.
Installation
[dependencies]
rig-qdrant = "0.2.5"
rig-core = "0.36.0"The root rig facade also exposes this crate behind the qdrant feature.
Examples
See examples/qdrant_vector_search.rs
for an end-to-end example using a Qdrant collection with a Rig embedding model.
Filtered searches use the crate-level QdrantFilter type:
use rig_core::vector_store::request::{SearchFilter, VectorSearchRequest};
use rig_qdrant::QdrantFilter;
let req = VectorSearchRequest::<QdrantFilter>::builder()
.query("What is a linglingdong?")
.samples(1)
.filter(QdrantFilter::eq(
"id",
serde_json::json!("f9e17d59-32e5-440c-be02-b2759a654824"),
))
.build();Overview (`crates/rig-reqwest/README.md`)
rig-reqwest
The bundled reqwest HTTP transport for Rig: ReqwestClient and ReqwestMiddlewareClient implement HttpClientExt. It depends only on rig-http, which holds that contract. ReqwestClient::default() is the process-wide client, built once on first use and shared by every clone; ReqwestClient::from(client) wraps a host-built reqwest::Client. With rig-core's reqwest feature (on in the rig facade), a provider client built with no transport named, such as OpenAI::from_env()?, sends through ReqwestClient::default(); any other client goes in with the configuration's .connect(client). Construction never fails: when the reqwest client cannot be built, every send reports the build failure in-band.
rig-core without its reqwest feature has no reqwest/Tokio dependency; this crate is where both live. On native targets, requests and lazy response bodies capture the caller's Tokio context or, outside Tokio (Bevy task pools, smol, futures::executor), a lazily started fallback reactor. Each poll enters that context; no detached per-request or stream-forwarding worker owns the operation. Dropping a request, response body or stream releases its local operation without waiting for another upstream chunk. It cannot undo a remotely accepted request.
A host runtime must enable I/O and timers and remain driven. Context detection does not prove this; a runtime without its reactor can panic during I/O. A handle alone does not keep a runtime alive. Stop admitting work, cancel/drain owned operations, release shared clients, then shut down the runtime. Supplied endpoint, proxy, redirect, TLS and timeout policy remains with the host-built client. WASM uses its native browser transport path, not the native Tokio fallback.
Overview (`crates/rig-rmcp/README.md`)
rig-rmcp
MCP (Model Context Protocol) client support for Rig, built on the rmcp SDK. Depends on rig-core only.
McpConnectionowns a connected rmcp client (McpConnection::new(client_config.serve(transport).await?)). Clones share it, and it closes when the last clone, or every tool holding one, is dropped, or onclose().McpToolis one server tool, fromMcpConnection::toolsorMcpTool::new(definition, &connection). It keeps its connection open, so an agent that holds its tools keeps the server connected. Convert it into arig_core::tool::DynamicTool(TryFrom, failing on an empty tool name) and register it wherever dynamic tools go. The dynamic tool carries a liveness probe bound to the transport, so registries retire it on disconnect.McpTool::with_nameoffers the tool to the model under another name, while calls still send the server the tool's MCP name: namespace one server's tools, or tell apart two servers' tools of the same name.- Per call, an
McpMetain the runtime'srig_core::tool::ToolContextis forwarded as the request's_meta, andMcpTool::with_call_metaadds entries derived from the call itself (the MCP name and the exact arguments sent), for signatures or idempotency keys. The rawCallToolResult(with the response'sstructuredContentand_meta) is published to the context's result map for hooks (McpCallToolResult). A tool that declares an output schema answers the model with its structured content alone. McpConnection::resourcesandread_resourcelist and read the server's resources.resource_context(uris)is a retrieval handler that reads them again for every model call: pass it to rig-agent'sAgentBuilder::dynamic_context_handlerto give an agent live resources as context.McpClientHandler<S: rig_core::tool::ManagedToolSink>is an rmcp client handler that keeps a tool registry in sync withnotifications/tools/list_changed. rig-agent'sToolServerHandleimplementsManagedToolSink, soMcpClientHandler::new(client_config, tool_server_handle.clone()).connect(transport)is the agent usage. The registry owns those tools, so hold theMcpConnectionthatconnectreturns for as long as they should work.
The rig facade re-exports this crate as rig::tool::rmcp behind its rmcp feature. The rmcp_example package shows a local server's tools and resources and a hosted server over streamable HTTP.
Native-only: rmcp's ClientHandler requires Send + Sync unconditionally, which rig's wasm tool registry cannot satisfy; building for wasm32 fails with a single explanatory compile_error!.
Overview (`crates/rig-s3vectors/README.md`)
Rig AWS S3Vectors integration
This crate integrates AWS S3Vectors into Rig, allowing you to easily use RAG with AWS' newest offering.
Installation
[dependencies]
rig-s3vectors = "0.2.5"
rig-core = "0.36.0"You can also run cargo add rig-s3vectors rig-core to add the latest published
versions. The root rig facade exposes this crate behind the s3vectors feature.
This integration will require that you have an AWS account, although the region does not matter.
See examples/s3vectors_vector_search.rs
for a vector search example.
Overview (`crates/rig-scylladb/README.md`)
Rig-ScyllaDB
Vector store implementation for ScyllaDB. This integration provides vector storage and similarity search using ScyllaDB as the backend.
Usage
use rig::{
Embed,
providers::openai,
vector_store::{VectorStoreIndex, request::VectorSearchRequest},
};
use rig_scylladb::{ScyllaDbVectorStore, create_session};
#[derive(Embed, serde::Deserialize, serde::Serialize, Debug)]
struct Document {
id: String,
#[embed]
text: String,
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create ScyllaDB session
let session = create_session("127.0.0.1:9042").await?;
// OpenAI's embedding model
let model = openai::OpenAI::from_env()?.embedding(openai::TEXT_EMBEDDING_ADA_002, None);
// Create vector store
let vector_store = ScyllaDbVectorStore::new(
model,
session,
"vector_db", // keyspace
"documents", // table
1536, // embedding dimensions
).await?;
// Query the store
let req = VectorSearchRequest::builder()
.query("search query")
.samples(5)
.build();
let results = vector_store.top_n::<Document>(req).await?;
for result in results {
println!(
"Score: {}, ID: {}, Document: {:?}",
result.score, result.id, result.document
);
}
Ok(())
}See the /examples folder for usage examples.
Notes
- Uses application-level cosine similarity search (similar to SQLite and SurrealDB implementations)
- Suitable for small to medium datasets (< 100k vectors)
- Provides ScyllaDB's operational benefits: high availability, horizontal scaling, low latency
- Future-ready for ScyllaDB's native vector search capabilities
Overview (`crates/rig-sqlite/README.md`)
+Rig-SQLite
This companion crate implements a Rig vector store based on SQLite.
Usage
Add the companion crate to your Cargo.toml, along with the rig-core crate:
[dependencies]
rig-sqlite = "0.2.6"
rig-core = "0.37.0"
serde = { version = "1", features = ["derive"] }
serde_json = "1"You can also run cargo add rig-sqlite rig-core serde_json andcargo add serde --features derive to add the most recent versions of the
dependencies to your project.
See the /examples folder for usage examples.
Important Note
Before using the SQLite vector store, you must initialize the SQLite vector extension. Add this code before creating your connection:
use rusqlite::ffi::sqlite3_auto_extension;
use sqlite_vec::sqlite3_vec_init;
unsafe {
sqlite3_auto_extension(Some(std::mem::transmute(sqlite3_vec_init as *const ())));
}Storing JSON Metadata
Declare JSON metadata columns with Column::new("metadata", "JSON") and store
the value as serde_json::Value. Rig writes the value as JSON text and parses
it back as structured JSON when documents are returned from vector searches.
use rig_core::Embed;
use rig_sqlite::{Column, ColumnValue, SqliteVectorStoreTable};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Deserialize, Embed, Serialize)]
struct Document {
id: String,
#[embed]
text: String,
metadata: serde_json::Value,
}
impl SqliteVectorStoreTable for Document {
fn name() -> &'static str {
"documents"
}
fn schema() -> Vec<Column> {
vec![
Column::new("id", "TEXT PRIMARY KEY"),
Column::new("text", "TEXT"),
Column::new("metadata", "JSON"),
]
}
fn id(&self) -> String {
self.id.clone()
}
fn column_values(&self) -> Vec<(&'static str, Box<dyn ColumnValue>)> {
vec![
("id", Box::new(self.id.clone())),
("text", Box::new(self.text.clone())),
("metadata", Box::new(self.metadata.clone())),
]
}
}Filtering JSON Metadata
SQLite filters can target document-table columns that store JSON text. Use
SQLite's JSON extraction operators in the filter key:
use rig_core::vector_store::request::{SearchFilter, VectorSearchRequest};
use rig_sqlite::SqliteSearchFilter;
let req = VectorSearchRequest::builder()
.query("release notes")
.samples(5)
.filter(SqliteSearchFilter::eq(
"metadata->>'$.source'",
serde_json::json!("docs"),
))
.build();Use ->> when you want SQLite to compare a JSON value as a SQL scalar, such as
text, number, or boolean. Use -> when you want to compare against JSON text;
Rig serializes the right-hand side as JSON for that form.
JSON metadata filters are applied after sqlite-vec candidate search because
they reference the document table, not sqlite-vec metadata columns. This keeps
results correct, including with samples(1), but requires exhaustive candidate
retrieval. For frequently-used scalar filters, prefer storing the value in a
regular column marked with Column::indexed() so sqlite-vec can apply it during
candidate search.
Overview (`crates/rig-surrealdb/README.md`)
Rig SurrealDB integration
This crate integrates SurrealDB into Rig, allowing you to easily use RAG with this database.
Installation
[dependencies]
rig-surrealdb = "0.2.5"
rig-core = "0.36.0"You can also run cargo add rig-surrealdb rig-core to add the latest published
versions. The root rig facade exposes this crate behind the surrealdb feature.
There's a few different ways you can run SurrealDB:
- Install it locally and run it
- Through a Docker container, either locally or on Docker-compatible architecture
docker run --rm --pull always -p 8000:8000 surrealdb/surrealdb:latest start --username root --password rootstarts up a SurrealDB instance at port 8000 with the username and password as "root".
- Using SurrealDB's cloud offering
- Using the cloud offering you can manage your SurrealDB instance through their web UI.
How to run the example
To run the example, add your OpenAI API key as an environment variable:
export OPENAI_API_KEY=my_keyFinally, use the following command below to run the example:
cargo run --example vector_search_surreal --features rig/deriveOverview (`crates/rig-tools/README.md`)
rig-tools
Coding tools for Rig agents:
| Tool | What it does |
|---|---|
read |
A text file's lines, with an offset and limit |
write |
A whole file, written atomically |
edit |
Several exact (or whitespace- and quote-tolerant) replacements, applied in order, all or nothing, answered with the changed lines' numbers and those lines, with a few around each, as they now are |
search |
A regex over the tree as git ignores it, hidden files too when asked or when the glob names one, or the names of the files with a match (every file for an empty pattern); find returns the hits as data, render shows them |
list |
A directory's files as git ignores them, down to a depth, optionally matching a glob, hidden files as for search |
shell |
sh -c in its own process group, as a Launcher starts it, run until it ends unless the call sets a timeout, stdout and stderr interleaved line by line without terminal escape codes such as colours, and an optional OutputLimit that keeps both ends of a long output and the first and last errors from between |
delete, move |
A file removed, or moved or renamed |
Tools publish facts beside their text (such as FilesChanged), and every tool acts on aWorkspace: this machine (Local) or another. The pieces the tools are built from, such as
checkpoints, the overlay workspace and process groups, are listed in the crate docs.
remote reaches other machines through a transport: a command that runs its last argument,
a shell line, there with the remote user's shell, keeps stdout and stderr apart and ends with
255 when it failed itself, as ssh HOST and koh exec HOST --sh do. Remote is such a
machine's workspace, and Hosts wraps one so that a path written HOST:PATH, as scp
reads it, is that host's and every other path stays the inner workspace's:
use std::sync::Arc;
use rig_tools::process::Launcher;
use rig_tools::remote::{Hosts, Remote};
use rig_tools::workspace::Local;
use rig_tools::Read;
let koh = ["koh", "exec", "bench", "--sh"];
let ssh = ["ssh", "-o", "BatchMode=yes", "dev@box"];
let hosts = Hosts::new(
Arc::new(Local),
[
Remote::new("bench", koh, Launcher::default())?,
Remote::new("box", ssh, Launcher::default())?,
],
);
// `read` of `bench:~/strata/src/lib.rs` reads that file on bench.
let read = Read { workspace: Arc::new(hosts), ..Read::default() };Each file operation there is one short sh script through the transport, at most two at
once per host; a read whose transport failed (exit 255) or was killed is sent once more, a write never: when a write's transport ends some other way, such as a
link dropped mid-write, the file is hashed there again and the error says whether the write
happened, and edit forgets the text it kept of that file. A read of some lines of a
file over 50 KB is sent only those lines; a smaller file comes whole. Anedit of a file read (any part of it, when it is that small), or edited before, is one call that writes only if the file still
holds the text it was made from. A shell command there runs under a short sh script that
stops the command's process group when the transport's stdin closes. This side holds that
stdin open until the call ends, so a timeout, an interrupt, rig quitting or a transport killed
on this machine stops the command there too, with any transport that forwards stdin and
closes it when it goes (ssh does). Without setsid there, only the command's own process
is stopped.
Every tool then takes such paths: read, edit and write a file there, search withpath: "bench:~/strata", shell with cwd: "bench:~/strata", and the paths they print and
publish keep the HOST: form.
The tools run on the file system and processes, so the crate is native only.
The rig-harness coding agent registers them.
Overview (`crates/rig-tungstenite/README.md`)
rig-tungstenite
The bundled tokio-tungstenite websocket backend for Rig: TungsteniteClient, a WebSocketClientExt implementation. It depends only on rig-http, which holds the websocket contract.
rig-core owns the websocket protocol: the OpenAI Responses session, its event envelopes and its turn state machine all live in rig_core::providers::openai::responses_api::websocket, written against the transport-agnostic rig_http::ws_client contract (re-exported at rig_core::ws_client). This crate owns only the socket, exactly as rig-reqwest owns only the HTTP transport. With rig-core's tungstenite feature, a session opens over this backend with no backend named:
let session = model.responses_websocket().connect().await?;Without it, pass any backend: model.responses_websocket().connect_with(&TungsteniteClient::new()).
It also works when the caller has no tokio runtime (Bevy task pools, smol, futures::executor): the caller still owns and polls the socket, and each operation enters the context of rig_http::runtime, the lazily started fallback runtime rig-reqwest uses too. A cancelled receive leaves unread frames in the socket, and dropping the connection closes it. Reading on demand is the backpressure for slow consumers.
Overview (`crates/rig-typesafeai/README.md`)
rig-typesafeai
Experimental TypeSafe Jev judgments for Rig. Enable the root rig featuretypesafeai to use rig::typesafeai, or depend on this crate and pair it with a
Rig HTTP transport explicitly. It uses Rig's Operation, Wire, Model, and
shared driver.
There are no Jev derive macros or schema-generation traits.
One structure for questions and answers
Implement Query on a generic named struct. Its associated Output replaces
question fields with their answer types while preserving their names and layout:
use rig_core::error::ProviderError;
use rig_typesafeai::{Noul, NoulAnswer, Query};
use serde::{Serialize, Deserialize};
// The fields are declared once, for both questions and answers.
#[derive(Serialize, Deserialize)]
struct Assessment<R, V> {
ready: R,
needs_review: V,
}
impl<R: Query, V: Query> Query for Assessment<R, V> {
type Response = Assessment<R::Response, V::Response>;
type Output = Assessment<R::Output, V::Output>;
fn decode(&self, response: Self::Response) -> Result<Self::Output, ProviderError> {
Ok(Assessment {
ready: self.ready.decode(response.ready)?,
needs_review: self.needs_review.decode(response.needs_review)?,
})
}
}
let query = Assessment {
ready: Noul::new("Is this ready to ship?")?,
needs_review: Noul::new("Does this need human review?")?,
};
# Ok::<(), ProviderError>(())With a Jev model (Jev::from_env()?.evaluation()), evaluate it directly:
let answers: Assessment<NoulAnswer, NoulAnswer> =
jev.evaluate(&state, &query).await?.answers;The query is reusable by reference. .await? handles construction-independent
transport and response errors once; the result contains ordinary typed fields.
Serde field names and rename attributes supply question IDs. The request is
serialized directly, and Serde reconstructs the matching response struct beforedecode validates each answer against its question. No manual map construction
or lookup is needed. Use #[serde(flatten)] to compose named groups.
See triage for a complete example
using Choice, Score, and Noul in the same generic struct. Its query and answer
forms are concrete instantiations of one type, with no type erasure.
Question builders
Choice::new(instructions, [(variant, description), ...])returnsChoiceAnswer<T>on evaluation. Values implementSerialize,DeserializeOwned, andOrd; their Serde representation must be a string.Score::new(instructions, [(level, description), ...])returnsScoreAnswer<L>with enum-keyed probabilities. Array order determines
numeric positions, independently of enum discriminants andOrd.
Evaluation requiresL: Ord + Cloneand the WASM-compatible transport bounds.DynamicScore::new(instructions, descriptions)returnsDynamicScoreAnswer
with numeric indices and a legend when rubric identities are runtime-defined.Noul::new(instructions)returnsNoulAnswer, the probability of yes..criteria(yes, no)?accepts structured descriptions for both outcomes.
Fixed arrays check choice counts (2–255) and score counts (2–10) at compile time.try_from_iter constructors check runtime collections and reject duplicate
labels or levels. Builders accept serializable descriptions, including structs,
JSON objects, arrays, strings, and null. State accepts strings, objects, or arrays.
For runtime IDs, maps of questions implement Query, and .named("ready")?
gives a standalone question an ID. .join(other) flattens two query objects;.map(...) projects validated answers. Empty or duplicate IDs and missing or
unexpected response IDs are rejected at the evaluation boundary.
For a fully runtime-defined question set, use DynamicQuery::new(definitions)?.
Its output is explicitly BTreeMap<String, types::Answer>; construction validates
the definitions and evaluation validates the provider's answers.
Jev::from_env() reads JEV_TOKEN without loading a secrets file. Its default
model is jev-latest; JevConfig::model(...) selects another provider model
identifier. Configure timeouts and retry middleware on the Rig transport and
pass it with JevConfig::connect.
Questions share state but cannot consume each other's answers in one request.
Confidence measures concentration, not correctness. Keep distributions and put
threshold policy in application code. Validation preserves the provider's values,
including bounded hundredth-rounding error; it does not silently renormalize them.
Examples and verification
Run the interactive console from the repository root with JEV_TOKEN already
exported:
cargo run -p typesafeai_chatAdd -- --agent for gpt-5.6-sol replies using OPENAI_API_KEY. The default
mode uses Jev alone and displays decisions plus deterministic next actions.
Both modes support /reset, /quit, and conversational history.
- Interactive chat: named generic query structs and
batched judgments over conversation state. - Mixed triage: all three primitives in one call.
Run cargo test -p rig-typesafeai to replay the synthetic, live-recorded
plain, structured, and
rounded matrix fixtures
offline, check local validation invariants, and run compile-fail doctests.
The fixtures contain no authentication headers. They were recorded againstjev-1.13.0; replay checks request JSON and decoded answers, not future model
accuracy or calibration. Mutation tests exercise invalid responses separately.
Protocol sources: API,
Choice,
Score,
Noul.
Overview (`crates/rig-vectorize/README.md`)
rig-vectorize
Vector store integration for Cloudflare Vectorize. This integration supports vector similarity search and document insertion using Rig's embedding providers.
Installation
[dependencies]
rig-vectorize = "0.2.5"
rig-core = "0.36.0"The root rig facade also exposes this crate behind the vectorize feature.
You can find end-to-end examples here.
For Vectorize-specific questions, ask in the Cloudflare Developers Discord.
Running Integration Tests
Integration tests require a real Cloudflare Vectorize index.
1. Create a Vectorize index
npx wrangler vectorize create rig-integration-test --dimensions=1536 --metric=cosine2. Set environment variables and run tests
export CLOUDFLARE_ACCOUNT_ID="your-account-id"
export CLOUDFLARE_API_TOKEN="your-api-token"
export VECTORIZE_INDEX_NAME="rig-integration-test"
cargo test --package rig-vectorize --test integration_tests -- --test-threads=1Tests run sequentially (--test-threads=1) to avoid conflicts since they clear the index before each test.
Note: Vectorize has eventual consistency. Tests wait 5 seconds after inserting documents before querying (configured via EVENTUAL_CONSISTENCY_DELAY constant).
3. (Optional) Enable filter tests
Filter tests require metadata indexes. Without them, filter tests will be skipped:
npx wrangler vectorize create-metadata-index rig-integration-test --property-name=category --type=string
npx wrangler vectorize create-metadata-index rig-integration-test --property-name=id --type=stringOverview (`crates/rig-vertexai/README.md`)
Rig-VertexAI
This companion crate integrates Google Cloud Vertex AI as a model provider with Rig: Gemini through GenerateContent, and Anthropic's Claude through the anthropic publisher's rawPredict endpoint.
Usage
Add the companion crate to your Cargo.toml, along with the rig-core crate:
[dependencies]
rig-vertexai = "0.42.0"
rig-core = "0.42.0"You can also run cargo add rig-vertexai rig-core to add the most recent versions of the dependencies to your project.
See the /examples folder for usage examples.
Raw responses
A response's raw is the reply's REST JSON: the SDK's GenerateContentResponse
serialized, with enum values spelled by name. It can be recovered from a
normalized response without another RPC:
use google_cloud_aiplatform_v1::model::GenerateContentResponse;
use rig_core::{completion::CompletionResponse, serde_json};
fn recover(response: CompletionResponse) -> Result<GenerateContentResponse, serde_json::Error> {
serde_json::from_value(response.raw)
}Claude
VertexAi::anthropic(model) sends Anthropic's Messages API to Vertex AI'srawPredict endpoint (streamRawPredict when streamed) with the client's
Google credentials. Requests, replies, Anthropic typed options and history
rules are Anthropic's; the model is named in the URL and the body carriesanthropic_version: "vertex-2023-10-16". Use Vertex AI's model ids, such asclaude-sonnet-4-6 or claude-haiku-4-5@20251001. global and the us andeu multi-regions have their own hosts; any other location is a region.
use rig_core::completion::CompletionRequest;
use rig_vertexai::VertexAi;
# async fn example() -> anyhow::Result<()> {
let claude = VertexAi::from_env()?.anthropic("claude-sonnet-4-6");
let reply = claude.call(CompletionRequest::new("Hello, Claude").max_tokens(256)).await?;
# let _ = reply;
# Ok(())
# }Claude requests travel over HTTP, through the shared reqwest client unlessVertexAiBuilder::with_http_client supplies another. They need the
credentials the client resolves, so a client built on a supplied prediction
service cannot send them.
Setup
Make sure to have Google Cloud credentials configured. You can use Application Default Credentials (ADC) by running:
gcloud auth application-default loginSupplying your own SDK client
VertexAi::from_env() (and VertexAi::builder() without either explicit credentials
or a supplied service) resolves Application Default Credentials. That resolution buildsgoogle-cloud-auth's token cache, which spawns a refresh task on the current
Tokio runtime while it is being constructed: call it inside a runtime
context, and keep that runtime alive and driven for as long as the client is
used. The refresh task belongs to the runtime, not to any one completion.
A host that would rather own the connection and its credential lifetime can
build the Google SDK client itself and hand it over. Rig then takes the
endpoint, credentials, transport, retry policy and universe domain as given,
and never rebuilds the client or reads ADC:
use google_cloud_aiplatform_v1::client::PredictionService;
# async fn example() -> anyhow::Result<()> {
let service = PredictionService::builder()
.with_endpoint("https://us-central1-aiplatform.googleapis.com")
.build()
.await?;
let client = rig_vertexai::VertexAi::builder()
.with_project("my-project")
.with_location("us-central1")
.with_prediction_service(service)
.build()?;
# let _ = client;
# Ok(())
# }project and location are still configured on the Rig client: they name the
model resource in the request, not the connection. Supplying both a
prediction service and with_credentials(...) is rejected at build time —
the supplied client's credentials are already fixed.
Overview (`examples/agent_no_tokio/README.md`)
agent_no_tokio
Runs a rig agent on Bevy's AsyncComputeTaskPool with no tokio dependency in
this crate's manifest.
AgentRunner::run_channel splits a run into a runtime-agnostic future and a boundedRunEvents feed. The future is spawned on the pool; the main thread acts as a
frame loop, draining events with the non-blocking RunEvents::try_next and
checking the task with bevy_tasks::futures::check_ready — the shape a Bevy
system or any other tick-driven host would use.
OPENAI_API_KEY=… cargo run -p agent_no_tokioTokio appears only transitively, inside rig-reqwest, which drives the HTTP
wire on a private runtime:
cargo tree -p agent_no_tokio -e normal -i tokioOverview (`examples/candle_local/README.md`)
Local rig-candle example
This workspace example downloads three pinned Hugging Face files and passes their owned
bytes through the root rig facade to rig-candle. All filesystem and network
access stays in the example application; rig-candle only receives ModelData.
From the repository root:
./examples/candle_local/download_model.sh
cargo run -p candle_local -- "Explain ownership in one sentence."The default model is SmolLM2-360M-Instruct in Q4_K_M GGUF form. Its
270,590,880-byte checkpoint produces useful instruction responses while staying
small enough for the browser example.
The example prints generated fragments as they arrive, then reports final usage,
finish reason, context limits, prefill time, time to first token, total duration,
and throughput.
Set MODEL_DIR for both commands to store and load the three files elsewhere.
The downloader verifies immutable revisions and SHA-256 checksums and preserves
only already complete files.
Overview (`examples/candle_pose/README.md`)
Local rig-candle pose example
This workspace example estimates human poses with a YOLOv8 pose checkpoint
through the root rig facade. It reads the checkpoint and decodes the images
itself; rig-candle only receives the checkpoint bytes and RGB frames.
From the repository root:
./examples/candle_pose/download_model.sh
cargo run --release -p candle_pose
cargo run --release -p candle_pose -- frame-001.jpg frame-002.jpg frame-003.jpgThe downloader fetches Candle's safetensors copy of yolov8n-pose and a photo
of a cycling race, both pinned to immutable revisions and checked by SHA-256.
Every image named on the command line is one frame of a video; each frame's
people are printed as soon as it is estimated.
The YOLOv8 pose weights are Ultralytics' and are licensed under AGPL-3.0.
Check that license before you ship them.
Set MODEL_DIR for both commands to store and load the files elsewhere.
Overview (`examples/candle_wasm_chat/README.md`)
Local rig-candle WASM chat
This example compiles SmolLM2-360M-Instruct Q4_K_M into the WebAssembly module and
runs Rig and Candle entirely inside a browser Web Worker. Chat messages never
leave the page.
From the repository root:
./examples/candle_wasm_chat/build.sh
./examples/candle_wasm_chat/serve.shThen open http://127.0.0.1:8080. Set PORT when running serve.sh to use a
different port.
build.sh first runs download_model.sh, which downloads config.json,tokenizer.json, and model.gguf into the ignored model/ directory.
The Rust build script copies those bytes into Cargo's build output andinclude_bytes! embeds them in the final WASM module. wasm-pack writes the
browser package to the ignored www/pkg/ directory. Direct builds verify the
pinned size and SHA-256 of every artifact before copying hundreds of megabytes;
a partial or modified model directory fails early with a recovery instruction.
Set MODEL_DIR to choose the artifact directory for both downloading and
embedding. The downloader intentionally pins immutable model revisions and
checksums; it does not accept model overrides.
The model configuration and tokenizer are pinned to the officialHuggingFaceTB/SmolLM2-360M-Instruct
repository. The 270,590,880-byte checkpoint is Bartowski's Q4_K_M GGUF. Q4_K_M
is used because Candle supports its GGUF tensor kernels on native and WASM and it
is substantially smaller than full-precision weights while retaining useful
instruction output. The three embedded inputs total 272,696,282 bytes.
The Web Worker prevents synchronous WASM inference from freezing the page. The
Rust side retains a rolling window of recent Rig chat history until the
Clear button is pressed. User messages are limited to 1,024 UTF-8 bytes and
the retained window is bounded by both message count and serialized size, so a
long-running demo does not inevitably become stuck beyond the model's 4,096-token
context. Reinitialization is idempotent, avoiding a transient second model
allocation in wasm32's constrained linear memory.
Generation is currently returned after completion because synchronous Candle
inference cannot yield incremental fragments to JavaScript on the same worker.
After the page, worker, and WASM package load, inference makes no network
requests.
The worker controller has a model-independent Node regression test:
node --test examples/candle_wasm_chat/www/worker-runtime.test.mjsAfter build.sh, exercise the actual embedded model, idempotent initialization,
chat history, and Clear behavior with:
node examples/candle_wasm_chat/smoke.mjsMeasured reference build
On an Apple Silicon development machine, the release build produced a
277,885,159-byte WASM file. A direct JavaScript/WASM smoke test loaded the package
from the included local server and initialized the 272,696,282 embedded artifact
bytes in 496 ms. The required conversation produced Hello!, Paris, Green,
and then recalled Green; turn times were 14.5, 20.2, 26.2, and 31.6 seconds as
the full history grew. After Clear, the model no longer recalled green.
The borrowed embedded-artifact loader avoids a transient 270,590,880-byte GGUF
copy. Node observed about 1.76 GB RSS immediately after initialization and about
1.23 GB after the full conversation, with roughly 760 MB classified as
external/WASM memory. Browser engines, hardware, and optimization settings will
vary, so treat these numbers as a practical reference rather than a guarantee.
The equivalent unoptimized native run generated the same answer in 17,536 ms
(0.46 generated tokens/s), including a 13,914 ms prefill. Release builds are
strongly recommended for actual use.
Overview (`examples/litellm/README.md`)
Rig with LiteLLM
Run a Rig agent through the LiteLLM Proxy.
Rig sends OpenAI-compatible requests to the proxy, which routes them to the
configured provider. No LiteLLM Rust dependency is needed.
Start a local proxy
Install the proxy with uv:
uv tool install 'litellm[proxy]'From the repository root, in a shell with OPENAI_API_KEY exported, run:
litellm --config examples/litellm/config.yaml --host 127.0.0.1 --port 4000The supplied configuration maps the public alias rig-chat toopenai/gpt-4o-mini. The provider key stays in the proxy's environment.
To use another provider, change litellm_params in config.yaml and supply
that provider's credentials to the proxy.
Run the agent
In another shell, from the repository root:
LITELLM_API_KEY=unused cargo run -p litellmThe local configuration has no proxy authentication, so unused is a dummy
key. The example sends one prompt and prints the response.
For an existing authenticated proxy, export its key as LITELLM_API_KEY and
set the URL and model alias to match your deployment:
export LITELLM_BASE_URL=https://your-litellm-host/v1
export LITELLM_MODEL=your-model-alias
cargo run -p litellm| Variable | Meaning | Default |
|---|---|---|
LITELLM_API_KEY |
Proxy key, or unused for the local unauthenticated configuration |
Required |
LITELLM_BASE_URL |
Proxy API base URL, including /v1 |
http://localhost:4000/v1 |
LITELLM_MODEL |
A model_name alias from the proxy configuration |
rig-chat |
The Rust process needs the proxy key, not the upstream provider key.client.chat(model) explicitly selects /chat/completions, producing/v1/chat/completions with the default base URL. Rig's OpenAI client otherwise
defaults to the Responses API. The model value is the proxy alias, not the
provider-qualified litellm_params.model value.
Overview (`examples/typesafeai_chat/README.md`)
Interactive Jev + Rig chat
Run from the repository root in a shell that already exports JEV_TOKEN:
cargo run -p typesafeai_chatTry: I upgraded yesterday, but exports still say plan limit exceeded.
Then add evidence in the next message. Jev sees the current input and the last
8 completed turns. Each call batches a typed route, an urgency rubric, and the
probability that clarification is needed. A generic Assessment struct implements Query, retaining the same fields for
questions and answers. AssessmentQuery::new() builds it once for reuse.
The default mode prints decisions and
a deterministic suggested action; Jev does not generate conversational prose.
For replies from an ordinary Rig agent, also export OPENAI_API_KEY, then run:
cargo run -p typesafeai_chat -- --agentThis calls Jev first, then uses its assessment to choose instructions forgpt-5.6-sol. Your OpenAI endpoint must support that model and have available
credits. Failed requests print an error and leave conversation history unchanged.
Requests time out after 90 seconds. No secrets files are loaded by the example.
Use /reset to clear history, /quit or EOF to exit, and --help for usage
(which requires no credentials). The thresholds are illustrative application
policy; confidence measures distribution concentration, not correctness.
Overview (`examples/typesafeai_triage/README.md`)
Typed Jev triage
Requires JEV_TOKEN in the environment. From the workspace root:
cargo run -p typesafeai_triageThis sends a synthetic support ticket as a Serde struct and evaluates Choice,
Score, and Noul together. The selected route is a Rust enum. The example prints
distributions and confidence instead of hiding uncertainty behind that selection.
Score is an expected zero-based rubric index; Noul is a probability of yes.
The questions are independent: they share the ticket, not each other's answers.
See the Jev primitives documentation.
Assessment<R, U, D> declares its fields once. Its Query implementation maps
each field type to its associated output type. AssessmentQuery::new() supplies
explicit choice and score variants; no Jev derives are involved.
client.evaluate(&ticket, AssessmentQuery::new()?).await?.answers returns the
same named structure populated with typed answers.
Overview (`plugins/rig-basics/README.md`)
rig-basics
The default plugins of the rig coding agent, and
the services only some plugins add. One crate, one module per group, all on
rig-harness's public API. Each plugin is its own entry in plugins.toml,
so each can be turned off alone.
- the root:
BasicCommandsPlugin(/help,/retry,/quit); prompt: rig's fixed system prompt for an agent without one
(DefaultPromptPlugin,DEFAULT_SYSTEM_PROMPT);agents_md: the project's and the user'sAGENTS.mdfiles in the system
prompt (AgentsMdPlugin);skills: the skills listed in the system prompt, and/skill:<name>
(SkillsPlugin);models:/model,/effortand the model picker (ModelsPlugin), and
the model a new session starts on (defaults::DefaultsPlugin);login:/loginand/logoutfor the ChatGPT plan's models and the
providers' API keys (LoginPlugin);session_picker: the session and conversation lists of/resume,/forkand/agents(SessionPickerPlugin, which the plugins of those
commands add, so it is not listed on its own);sessions:/new,/resumeand/name(SessionsPlugin), and/fork,/agentsand rewind (agents::AgentsPlugin);compaction: summarizing a conversation near the model's window, and/compact(CompactionPlugin);telemetry: what model calls cost and/usage(usage::UsagePlugin),
the effect log (effect_log::EffectLogPlugin), and warnings and errors
as data (diagnostics::DiagnosticsPlugin);activity: what each agent is doing, for views (ActivityPlugin);coding_tools: theread,edit,writeandshelltools (ReadTool,EditTool,WriteTool,ShellTool),@pathattachments
(AttachPlugin), andHOST:PATHpaths on other machines
(RemoteHostsPlugin, below);approval: one question for every effect a plugin wants approved
(ApprovalPlugin);preview: what a tool call would change on disk, before it runs
(PreviewPlugin);checkpoint: what each tool call changed on disk (CheckpointPlugin);reload:/reloadand thereloadtool (ReloadPlugin), and/plugins(plugins::PluginsPlugin);print:--printand--json, the fronts of a run with-p, piped
stdin or JSON lines (PrintPlugin), and the loop of any run nobody sits
at (AppHeadlessExt);helpers: thehelpertool, which runsrig -p --jsonchildren
(HelpersPlugin);subagents: thetask,messageandwaittools (SubagentsPlugin).
An entry names the crate and the plugin's type by the module that defines
it, the one path the type has, which /plugins and inspect show too:
[[plugin]]
crate = "rig-basics"
plugin = "rig_basics::compaction::CompactionPlugin"rig plugin disable rig_basics::compaction::CompactionPlugin comments an
entry out, and rig plugin enable <type> puts it back; /plugins does the
same from inside the agent. ApprovalPlugin, PreviewPlugin,CheckpointPlugin, HelpersPlugin, SubagentsPlugin, EffectLogPlugin andDiagnosticsPlugin are listed disabled by default. A plugin that needs
another adds it when it is missing, as SessionsPlugin addsSessionPickerPlugin and SubagentsPlugin adds ApprovalPlugin.
With the default tui feature, each plugin also draws its part in the
terminal view; default-features = false leaves that out.
coding_tools::remote::RemoteHostsPlugin makes a path written HOST:PATH another
machine's for every tool: read bench:~/app/src/lib.rs, edit and write
there, and shell with cwd: "bench:~/app", which runs the command there.
Other paths stay on this machine. Each host is a transport, a command that
runs its last argument (a shell line) on the host, inRIG_HOME/settings.toml:
[remote.hosts]
# ssh's options go in the command.
box = ["ssh", "-o", "BatchMode=yes", "dev@box"]
# koh exec runs the line given to `--sh` with the remote shell.
bench = ["koh", "exec", "bench", "--sh"]With hosts configured, the system prompt names them and the path form, and
the tools' path and cwd arguments say they take it. Exit status 255 from
a transport means the transport failed: a file operation fails saying so,
and a shell call ends with that status. A transport must forward its stdin
and close it there when it ends: a shell command on the host stops when
that stdin closes, so a call stopped at its timeout, interrupted, or cut off
by a transport killed on this machine leaves nothing running there. ssh
does this; koh exec does on a stop or when its stdin closes. A project's.rig/settings.toml cannot add hosts.
Overview (`plugins/rig-inspect/README.md`)
rig-inspect
An optional plugin of the rig-harness coding agent: theinspect tool, with which the agent reads its own running Bevy world through
Bevy Remote, answered in process (no transport, no port) and read-only. The
agent asks it which plugins are loaded and what each added, its agents, the
state saved with the session, the warnings logged (the Diagnostics
resource), schedules and every reflected type, before reading rig's source.
It is not in the default plugins.toml, because Bevy Remote makes the agent's
build heavier and, on Linux, needs the ALSA development headers
(libasound2-dev on Debian and Ubuntu, alsa-lib-devel on Fedora,alsa-lib on Arch). Enable it by uncommenting its entry in$RIG_HOME/plugins.toml, or with:
rig plugin add rig_inspect::InspectPlugin --crate rig-inspectOnly read-only methods are sent; a plugin
that adds its own Bevy Remote method marks it with ReadOnlyMethod (see the
crate docs). An answer over 16 KiB is cut and kept whole in the session's
spill directory for the agent to read.
Overview (`plugins/rig-steel/README.md`)
rig-steel
Code mode (SteelPlugin), an optional plugin of the
rig coding agent, not in the defaultplugins.toml: the run_steel tool, whose
Steel (Scheme) program spawns agents,
sends them requests, waits for their replies and calls the model's own tools,
in Steel's sandboxed engine. It builds on rig-ecs alone, so any rig-ecs app can
add it. The agent guide shows how to
enable it and an example program.
Overview (`plugins/rig-tui/README.md`)
rig-tui
The terminal view of the rig coding agent, and the
front of a run on an interactive terminal. TuiPlugin is TuiViewPlugin
(terminal, keys, frame) plus DefaultScreenPlugin (transcript, status line,
input), and the default screen is built from the same public widgets a plugin
places in panels of its own.
The view tells the terminal when a turn of an agent nothing spawned runs, as a
shell does a command: OSC 133 marks (C as it starts, D;0 or D;1 as it
ends, then A and B), a state report (133;S;activity=busy or reading)
for a multiplexer such as fux, and the title (rig · working, rig · idle).
Plugins add panels, transcript looks, status line items, key bindings and
completion without touching the view. The plugin guide shows each
(PLUGINS.md),
and the crate docs list every type.
A crate that also builds without the view puts its rig-tui code behind a
default tui feature. rig_tui::ratatui and rig_tui::crossterm are
re-exported. Without rig-tui the agent answers -p or piped stdin only. Tests
run the view without a terminal by adding the plugin and then add_view, put
keys in PendingInput, and read a frame from a TuiTestScreen.
Agent (`docs/agent.md`)
The rig coding agent
rig is a terminal coding agent. It is a Bevy app built on therig-harness crate, and every part of it but the
harness is a plugin, listed in plugins.toml and built from a plugin crate.
The rig command is its launcher: it generates a small Cargo project for the
agent with those plugins, builds it, and runs it.
The agent's coding tools come from the rig-tools
crate, usable by any rig agent, and its kernel (agents, turns and tool calls
as Bevy entities, one recorded effect path, session logs) is therig-ecs crate, for building your own harness.
- Install and run
- Models and sign-in
- The terminal view
- Sessions
- Context, compaction and retries
- Tools
- Subagents
- Without the terminal view
- Reload and builds
- Plugins
- Files
- Running from a checkout
Install and run
cargo install rig
export OPENAI_API_KEY=... # or any other provider key in the model catalog
rig # builds the agent (later starts rebuild what changed), then opens itThe agent builds from rig's crates on crates.io, at the launcher's version,
or from a rig checkout (Running from a checkout).
It needs Rust 1.97.1 or newer and runs on Linux and macOS; Windows is not
supported yet.
Models and sign-in
/model picks a model (providers that need no key, such as a local Ollama,
are listed last), and /effort its reasoning setting. -m vendor/model picks
one on the command line; otherwise a session starts with the last model and
reasoning chosen, else the first model with a key.
/login without arguments lets you pick the ChatGPT plan or a provider that
takes an API key. /login , such as /login anthropic, asks for
the provider's key, hidden as you type or paste it. The key is saved inRIG_HOME/auth/keys/.json, readable by you alone, and is used
before the provider's environment variable (ANTHROPIC_API_KEY). With a saved
key the provider's other variables, such as a base URL, are not read. If the
agent has no model yet, the model picker opens on the provider's models./logout lets you pick what to remove of what /login stored, or takes its
name (/logout anthropic); environment variables are never touched.
A ChatGPT subscription can pay for the model calls instead of an API key./login chatgpt opens your browser on the ChatGPT sign-in page and shows its
URL in case the browser does not open; the page returns to a one-shot
listener on 127.0.0.1 port 1455 (1457 when 1455 is taken). Without a
graphical session (no DISPLAY or WAYLAND_DISPLAY on Linux and the BSDs),
over SSH, or when both ports are taken, it shows a code to enter athttps://auth.openai.com/codex/device instead, as /login chatgpt --device
does. Print mode prints the URL or code, and rig -p "/login chatgpt" signs in
without the terminal view. Esc or a second /login cancels a sign-in that waits.
Once signed in, /model lists the plan's models as chatgpt/..., marked
"(ChatGPT plan)", and a successful sign-in switches the agent to the plan's
latest frontier model. The credential is kept in RIG_HOME/auth/chatgpt.json,
readable by you alone, and refreshed before a request when it has expired, so
a long session keeps working (rig processes running at once take turns
refreshing it). /logout chatgpt asks OpenAI to revoke it, then deletes it.
It is rig's own sign-in, separate from the Codex CLI's. ACHATGPT_ACCESS_TOKEN in the environment (with CHATGPT_ACCOUNT_ID) is used
instead when set.
The terminal view
The input takes several lines. Enter sends; Shift+Enter, Ctrl+J or \ before
Enter starts a new line; Up and Down move between lines and through the
prompts sent before (kept in RIG_HOME/data/tui/history.jsonl); the usual
emacs keys edit (Ctrl+A/E/K/U/W, Alt+B/F/D); Ctrl+C clears the input, and
Ctrl+D pressed twice on an empty input quits. A
leading / completes command names and @ completes the project's paths
(skipping what .gitignore leaves out); Tab or Enter takes the selected one.
A command that is unknown or refused comes back in the input with the reason,
and Enter then sends it to the model as it is. /help lists the commands.
While nothing is typed, the input's top row shows the keys and its bottom row
the status line: the model, the reasoning setting, what the agent is doing,
the session's tokens (uncached input, output, cache reads and writes), its
cost (the provider's figure, else the catalog's list price; + when some
calls had no price) and the context in use against the model's window./usage breaks them down.
Typing while the agent works is fine. Enter steers the running turn (the
message goes to the model with its next call, after the tool results it waits
for) and Tab queues a follow-up that is sent when the turn would end. Both
wait under the transcript. Esc stops the turn and puts them back in the input
(a plugin may keep them for the next turn instead); what the reply had
streamed stays in the transcript, saved as stopped, and is not sent to the
model again. Messages still waiting when the agent exits come back when the
session is resumed.
@path to a PNG, JPEG, GIF or WebP file attaches the image when the model
reads images. Ctrl+V pastes the clipboard's image (through wl-paste, xclip
or pngpaste) as such a path, and a dropped image file becomes one too.
Answers are drawn as markdown, edits as diffs, and each built-in tool's call
in its own way, always whole: every call's arguments and result, every diff
and every note, nothing cut or folded. A call's row is tinted by where it
stands (waiting, running, done, failed). Code is highlighted in the terminal's
own palette, so it follows the terminal's theme. PageUp, PageDown and
Shift+Up/Down scroll the transcript. With NO_COLOR set, nothing is drawn in
color. A plugin can draw its own tools' calls with app.add_tool_renderer
(rig_tui::AppToolRenderersExt).
Sessions
Each rig starts a new session. /quit ends it, /new starts a new one and/name names it. /resume lists the earlier sessions (name or first
message, cost, directory, age) and resumes the one picked, in its own
directory; rig --resume (or /resume ) resumes a
given one, such as after a crash.
The session is written as it happens. Each agent, subagents included, has a
log, sessions//logs/<agent-id>.jsonl, of its messages, settings, usage
and compactions, appended to as it runs and rewritten only through
rig-memory (a plugin's redaction, say, keeps each record's place and time).
Images are stored once in files/blobs/, and files/meta.json caches what/resume lists. An older session format is resumed and written on in its
own; one from before rig-session/2 is refused, unchanged.
A tool call that may change something starts only once the reply that asked
for it, and a mark that it started, are on disk. After a crash, a closed
terminal or a /quit mid-turn, resuming the session reads the logs back and
settles what was left half done: a tool call that may change something and
had started is answered as interrupted by the restart (or with its result, if
it finished before the crash), and a plugin may answer it first; any other
unfinished call, one still waiting for your approval included, goes through
the permission checks again and runs; and a turn that waited on the model
carries on.
Context, compaction and retries
The system prompt is the same on every request of a session, so the
provider's prompt cache keeps the whole conversation behind it: no date or
tool rules in it, and each tool says in its own description when to pick it.
Models whose catalog entry lists a prompt cache get it, with the agent's id
as the cache key where the provider takes one.
Two default plugins add to it when the agent starts. AgentsMdPlugin adds
the AGENTS.md files of the working directory and of each directory above
it up to the repository root (the nearest holding .git), nearest last,
after your own RIG_HOME/AGENTS.md. CLAUDE.md is not read; a repository
that has only one can link AGENTS.md to it. SkillsPlugin lists your
skills: each directory with a SKILL.md (the Agent Skills format, a name
and a description in its frontmatter) under .rig/skills or.agents/skills of those same directories, RIG_HOME/skills or~/.agents/skills. The model reads a skill's file when a task matches its
description, and /skill:<name> [text] sends one with your message. A skill
with disable-model-invocation: true is left out of the list and only sent
by hand. Both are read once, so an edit takes effect on /reload or /new.
When the conversation comes within 16k tokens of the model's window, older
tool outputs are cleared and, if that is not enough, the older messages are
summarized by the model into a structured checkpoint (goal, progress,
decisions, next steps, files read and changed) that requests send in their
place; the newest 20k tokens stay as they are. When the conversation outgrows
the window anyway, older tool outputs are cleared and the call is sent again./compact compacts now, and /compact <focus> says what the summary should
keep. The summarized messages stay in the transcript, under a line that shows
the summary; a resumed session starts from the messages the summary kept.
A model call that fails on a rate limit, an overloaded provider or a dropped
connection is retried up to four times, waiting as long as the provider'sRetry-After asks (or 2, 4, 8, 16 seconds). If it still fails, your message
stays in the conversation and /retry sends it again.
Tools
The default tools are read, edit, write and shell, each its own
plugin. A shell call runs until its command ends, unless the model setstimeout_secs or you interrupt it; either stops the command, on a remote
host too. The whole output
of a shell call that was cut is kept in RIG_HOME/spill/, and the model
reads it by the path the cut output names.
With rig_basics::checkpoint::CheckpointPlugin enabled (it is listed
disabled by default), every shell call, and any other call that may write
anywhere, reports the files it changed, from snapshots of the project before
and after it kept in the session's scratch directory, never in the
repository's .git. [checkpoints] enabled = false inRIG_HOME/settings.toml turns that off.
With [remote.hosts] in RIG_HOME/settings.toml, such asbench = ["koh", "exec", "bench", "--sh"] or box = ["ssh", "dev@box"], a
path written HOST:PATH (bench:~/app/src/lib.rs) is that host's for every
tool: read, edit, write and search act on the file there, and shell
with cwd: "box:~/app" runs the command there, through the host's command,
while other paths stay on this machine
(rig-basics).
Subagents
With rig_basics::subagents::SubagentsPlugin enabled (it is listed disabled
by default; /plugins or rig plugin enable rig_basics::subagents::SubagentsPlugin enables it), the model can hand work
to subagents with the task tool. Each is a new agent in the same process,
with its own conversation and, if the call asks, another model, reasoning
setting or a subset of the tools. task returns the subagent's id at once,
and message sends one of the model's own subagents a follow-up, which it
reads with its conversation kept.
Each task or message call is a request, and exactly one report for it
(done, failed or stopped) arrives later as a message to the agent that asked,
which starts a turn of an idle agent or follows the running one (a stopped
subagent's report is a note that starts no turn); the reports of one reply'stask calls reach it together. Nothing waits for them, so you can keep
talking to the main agent, or steer it, meanwhile. An agent that needs a
message before it goes on calls wait, which keeps its turn open until that
agent sends it one, and a subagent whose turn ends while a request it made is
open is waiting, not done: it reports the answer it gives once that request
is answered.
Esc stops the shown agent's turn and the turns of every subagent below it,
even when the shown agent is idle: stopping follows who started whom./agents (rig_basics::sessions::agents::AgentsPlugin) lists every agent
with its model and state, and shows the one picked: its transcript, and what
you type then goes to it. A subagent can start subagents of its own, one
level deep. Subagents started with peers can also send each other requests
with message and wait for each other; a request or wait that would make
agents wait on each other is refused. In the effect log, a subagent's model
calls name the call that gave it its work as their parent.
Without the terminal view
rig -p "fix the failing test" answers one prompt and exits: the answer goes
to stdout, failures to stderr, and the exit code is 0 when the turn ended with
an answer, 2 for a refused slash command. Text piped in follows the prompt
(git diff | rig -p "review this"), and piped in without -p it is the
prompt: rig runs the terminal view only on an interactive terminal.
-r resumes a given session instead of starting a new one; it
goes wherever an option goes, but not among a command's words or after --,
and rig -r <command> runs a plugin's command in that session.rig -r -p with no prompt finishes the turn the session left
unfinished, such as one a crash cut short, and prints its answer; with nothing
to finish it exits 2. A plugin may keep such a run going, sending the agent
work over time (KeepRunning); each answer is then printed as it comes.
To drive rig from a script or another agent, over pipes and nothing else,rig -p --json "task" writes the run as JSON lines, ending with an exit
line that carries the final answer, and rig --json stays up, one request
per line on stdin and one event per line on stdout, until stdin closes.rig help-agents is the whole protocol, with a Python driver. The optionalrig_basics::helpers::HelpersPlugin gives the model the helper tool, which
hands tasks to such one-shot rig children (one, up to eight side by side, or a
chain) and returns their final answers.
Reload and builds
/reload rebuilds the agent and restarts it on the same session. Typed while
a turn runs, it waits until no turn runs (/reload cancel drops it), and a
turn started during the build delays the restart until it ends. It shows
cargo's latest line, keeps the current build running if the new one does not
compile (its error notice names the build log), and rolls back to it if the
new one crashes during startup.
The agent can ask for a reload itself with its reload tool, which waits the
same way and shows a notice. A reload the model asked for reports back: one
message that starts a turn says the new build started, carries the build's
errors, or says the new build was rolled back, so the model carries on without
a second prompt. It may ask for at most 3 reloads per message of yours, and
Esc or /reload cancel drops the report. A -p run whose model asks for a
reload waits for it and carries on on the new build too.
Every build, the one before each start and /reload's, writes its whole
output (the launcher's and cargo's) to RIG_HOME/build.log, and a failure
message names its reason and first compiler error. The agent sees build
failures too: the reason, the first errors and the paths of build.log,plugins.toml and the generated project go into its conversation as a message
from the build plugin, which starts no turn when the agent is idle (unless
the model asked for that reload) and is saved with the session. Ask it why the
build failed and it can read the log, fix the cause, and reload.
rig build regenerates and builds the agent without starting it.CARGO_BUILD_JOBS sets cargo's -j for these builds, as for any cargo build.
Plugins
Plugins are Bevy plugins, listed in $RIG_HOME/plugins.toml. Everything but
the harness (the session and the launcher protocol) is an entry in that list:
the project context, models, sign-in, sessions, compaction, usage, activity,
each tool, @path attachments, /reload, /plugins, the commands,--print and the terminal view, each from a plugin crate inplugins/, most from rig-basics, so
any of them can be removed or replaced
(crates/rig-harness lists them):
[[plugin]]
crate = "rig-basics" # one of rig's own plugin crates: no source
plugin = "rig_basics::coding_tools::ReadTool"
[[plugin]]
crate = "rig-tui"
plugin = "rig_tui::TuiPlugin"
[[plugin]]
crate = "hello" # the package name
path = "plugins/hello" # relative to plugins.toml; or git = "..." (branch, rev), or version = "..."
plugin = "hello::HelloPlugin" # implements Plugin + Defaultrig plugin new hello makes that crate in $RIG_HOME/plugins/hello, outside
any workspace, and adds its entry; rig plugin add <type> --path <dir> (or--git, --version) adds an entry for an existing crate, and rig plugin remove <type> takes one out (its crate stays), each checked beforeplugins.toml is written. A commented-out entry is a disabled plugin: the
template lists every plugin rig ships, the ones not on by default commented
out. rig plugin list shows each entry, on or off, with its crate and the
comment above it, and rig plugin enable <type> and disable <type>
uncomment and comment out an entry, keeping every other line. In the agent,/plugins is a picker of the same list: tick and untick plugins, press Enter,
and the agent rebuilds and restarts at once (without a terminal it prints the
list).
rig plugin check checks the list. With --build it also builds the agent
with its plugins in $RIG_HOME/target without staging it, and names the
plugins whose systems in one agent-loop stage use the same data with no order
between them; a debug build of the agent warns of those at start.
A plugin crate depends on rig-harness, and on another plugin crate whose
types it uses (such as rig-tui for a terminal panel), and registers tools,
slash commands, tool renderers, terminal panels or a window the way the
built-in ones do, never by editing rig's crates.crates/rig-harness/PLUGINS.md is the
plugin guide, with a copy-ready example of each kind, its names fromrig_harness::prelude and the plugin crates, and the src/lib.rs that rig plugin new writes is a working, commented slash command. An agent started by
the launcher knows its plugins, commands and tools, and how to write and add
its own plugins, from its system prompt.
The optional rig-inspect plugin adds the inspect
tool, with which the agent reads its own Bevy world, read-only.
Code mode is the optional rig-steel plugin: arun_steel tool whose Steel (Scheme)
program spawns agents, messages them and calls tools, in a sandbox. Enable it
with:
[[plugin]]
crate = "rig-steel"
plugin = "rig_steel::SteelPlugin"A fan-out that asks three agents at once and keeps the shortest answer:
(define agents (map (lambda (i) (spawn-agent (string-append "solver-" (number->string i)))) (range 0 3)))
(define requests (map (lambda (agent) (send agent "How does src/lib.rs load plugins? Three sentences.")) agents))
(define answers (map reply requests))
(foldl (lambda (answer best) (if (< (string-length answer) (string-length best)) answer best))
(car answers) (cdr answers))Files
Every file lives under RIG_HOME (default ~/.rig), and several rig
processes can share one:
| Path | What it holds |
|---|---|
plugins.toml |
The plugin list. |
settings.toml |
Settings, such as [shell], [checkpoints] and [remote.hosts]. |
auth/ |
/login's credentials: chatgpt.json and the API keys in keys/. |
project/, target/, bin/ |
The generated agent project, cargo's build of it (a few gigabytes), and the builds. |
build.log |
The last build's whole output. |
defaults.json |
The last chosen model and reasoning, which a new session starts with. |
data/ |
Plugins' data across sessions, such as the prompt history data/tui/history.jsonl. |
projects/ |
Plugins' data per project. |
sessions// |
A session: the agent logs in logs/, the name /name gave it in name, and files/ (meta.json, the images in blobs/, the effect log effects.jsonl with EffectLogPlugin, plugins' files). |
spill/ |
The whole output of a shell call that was cut. |
agent.log |
The agent's log. |
Set RIG_HOME to put everything elsewhere, for example under a cache
directory.
Running from a checkout
To run the agent from a rig checkout instead of crates.io, install the
launcher from it or point RIG_SOURCE at it:
cd rig && cargo install --path . --root /some/dir
RIG_HOME=/some/dir/home RIG_SOURCE=$PWD /some/dir/bin/rig--- METRICS ---
- Files Extracted: 46
- Estimated Token Budget: ~57416 tokens
- Canonical Reference: https://codewiki.google/github.com/0xPlaygrounds/rig