rig Documentation

️🦀 Build modular and scalable LLM Applications in Rust

RAW Doc

Docker & Self-Hosted Deployment Verified OCI

Deploy rig on any server, homelab, or cloud container platform.

1-Click Terminal Command
bash (docker run)
docker run -d --name rig --restart always 0xplaygrounds/rig:latest

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

bash
cargo install rig
rig

In 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
    in plugins.toml, so any of them can be removed, replaced or added to.
  • It rebuilds itself. /reload rebuilds 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 --json and rig --json drive it over pipes;
    rig help-agents is 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

bash
cargo add rig
rust
use 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

toml
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:

sh
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 writes CHANGELOG.md and MIGRATING.md on the release PR, from the output of release-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/.rs with its modules under
    crates/rig-cassette/tests/providers//cassette/ (and /live/
    for the ignored live cells those suites still carry). The shared drivers they
    include live in crates/rig-cassette/tests/common/.
  • Providers with no recorded corpus keep their live-only suites in the facade:
    tests/.rs with tests/providers//. These are ignored
    tests that require a real service.
  • tests/core.rs contains provider-agnostic core behavior tests and the guards
    that scan the source tree, which need the repository root.
  • test-support/service-tests/integrations.rs runs the vector-store suites from tests/integrations/ as the unpublished rig-service-tests package. tests/integrations.rs retains 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 unpublished
rig-cassette-minimal runner at
crates/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.

sh
RIG_PROVIDER_TEST_MODE=replay cargo nextest run --locked -p rig-cassette-minimal --all-features --retries 0

Keep 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 the
rig-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.

bash
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 warnings

The 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:

bash
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 features

Fallible 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:

bash
cargo check --locked -p rig-core --no-default-features --lib

When 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:

bash
cargo test -p rig-cassette --all-features --test <provider> <provider>::cassette -- --nocapture --test-threads=1

Record mode requires the relevant provider credentials in the environment and
overwrites existing cassette files:

bash
RIG_PROVIDER_TEST_MODE=record \
cargo test -p rig-cassette --all-features --test <provider> <provider>::cassette -- --nocapture --test-threads=1

Prefer re-recording by fixture, from the repository root:

bash
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, *_TOKEN and
    *_SECRET value literally.
  • cassette goldens [--test TARGET]... regenerates effect goldens from
    replay: each selected target runs with RIG_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 with CassetteSpec::expects_account_failure or, 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. Send store: false unless the cell is a chain that deletes its
    own state.

Fixtures older than the stored-state rule are listed in
crates/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 default
cassette-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 through
assert_matches_recorded_document, or one field through
assert_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:

rust
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:

bash
find target/debug/deps -maxdepth 1 -name '*.rcgu.o' -delete

Bedrock 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, and finish panics 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:

json
{
  "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 messages and the items
    of a Responses input stay in sequence;
  • the values of model, role, name (tool names, also inside content)
    and stream (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 until
cargo 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().

bash
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 difference

A rewrite deletes every snapshot first, then replays with
RIG_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.

bash
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
    --check lists, 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:

bash
cargo test -p rig-cassette --all-features --test gemini \
  streaming_tools_smoke \
  -- --nocapture --test-threads=1

Cassette 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 in
test-support/rig-test-support/src/history_survival.rs, modeled per dialect,
and is applied twice:

  • crates/rig-cassette/tests/cassette_history_survival.rs sweeps 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 by previous_response_id), every native chat_history must
    pair too (this includes the requests after cancellations, invalid arguments
    and provider faults), and a census proves each provider and content kind
    was examined. Legacy REDACTED_<n> placeholders cannot prove a slot and
    are counted, not judged. Exemptions need a cited provider behavior and are
    reported when stale.
  • history_survival_matrix cells 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_matrix cells 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. Set
RIG_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 its
call 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 in
    test-support/rig-test-support/src/cache_prefix.rs and is shared with the per-scenario harness so the two cannot
    drift. It compares cached content: cache_control markers 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 same CompletionRequest eight 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 a serde_json::Value round-trip normalizes key order too, so a HashMap in 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 in NO_CACHE_SUITE with 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's
input_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:

bash
RIG_PROVIDER_TEST_MODE=record \
cargo test -p rig-cassette --all-features --test openai openai::cassette::prompt_caching \
  -- --exact --nocapture --test-threads=1

Layer 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) under
fixtures/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 helpers chat and
    chat_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's Usage as the wire reported it (input, cached reads, cache
    writes, output and total), the conversation the call continues, the OpenAI
    prompt_cache_key, and for Gemini the cache the call reads and every cachedContents
    resource created. Prompt tokens are input_tokens on every wire, as Usage'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
    through rig_core::completion::CacheCost at the provider's rates, and the same for the first
    30 turns alone, to set beside the baseline.
  • The checks check runs on every run: rig's Usage matches 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 same prompt_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:

bash
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:

bash
cargo test -p rig-cassette --all-features --test anthropic long_run_caching -- --nocapture --test-threads=1

Workloads 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 its
RunLog. 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 in messages and none is hoisted into system.
  • dynamic_tools_chat with the ToolSchedule hook: 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) and
long_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 one
prompt_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:

bash
cargo test -p rig-cassette --all-features --test openai live_cache_economics \
  -- --exact --ignored --nocapture --test-threads=1

Rules 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 by assert_warms.
  • Pad above the provider's documented minimum (min_cacheable_tokens in the descriptor) or
    the API silently declines to cache. Where a provider's rate limit is tighter than the default
    probe, CacheProbe::with_padding shrinks 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 on
gemini-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 small max_tokens yields 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 /), so
    scrub_resource_names handles 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 under
gemini/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.

bash
# 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=1

Use 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.

bash
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=1

The 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 from
rig_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).

bash
# 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=1

Shared 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 under
crates/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, and
case_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 by
cargo 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 as type and
    finish_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.txt and pinned.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 in pinned.txt with its reason, and the bank
    keeps that fixture's replies verbatim in pinned.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.

bash
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 runtime

The 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 in
rig_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 in
rig-core.

Direct users import construction and prompting explicitly:

rust,ignore
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-memory
AsyncStore: 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 with
PromptError::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. See
examples/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 with
AgentBuilder::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 and
check_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 — the
RunSpec 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, the
tool::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 twin
DynModel<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) or
set_model_label (a registered label), or change one run's default candidate
with using_model(label) or using_model_value(model). Implement
AgentHook::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 a
TypedRun, 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.

rust,ignore
#[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-free
runtime_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-core
conversation::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, while
ToolOutput 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. Explicit
ToolExecutionError constructors expose their detailed messages; use them or
with_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 its
WasmCompat* 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 a
cfg fix.

MCP (rig-rmcp) is native-only. rmcp's ClientHandler is declared
Sized + 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 nor
Sync on wasm. Enabling rmcp on a wasm target fails with a single explanatory
compile_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:

toml
[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:

shell
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, and
CandlePoseModel::pose() estimates human poses (see
Pose estimation). The crate receives byte buffers and
performs no filesystem or network access itself.

rust,no_run
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 and
Specific 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:

bash
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=1

The 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). Its
PoseEstimation 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.

rust,no_run
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 above
iou_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:

bash
./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 in
additional_params, which take precedence; unknown keys are errors. Omitting
top_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:

toml
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 and
tests/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, including
format, are rejected. An interrupted run resumes from its saved state with
EffectLog::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 of
new() when original stream item boundaries are required. After driving the
agent, import rig_cassette::agent::AgentReplayExt and call
agent.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 with
Handlers::record_to. Replay registration is
rig_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:

text
<fixture-root>/anthropic/completion.yaml
<fixture-root>/openai/nested/scenario.yaml

CassetteSpec::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 use
ProviderCassette::try_start_at and try_finish, which return a
CassetteError 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 and
start_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 returns
CassetteError::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 under
attempt_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
    by reply_account_failure, including a failure delivered inside a 2xx
    stream. Cells declare one with CassetteSpec::expects_account_failure,
    ProviderCassette::expect_account_failure or bogus_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's
ledger.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 in
test-support/rig-test-support/src/cassettes.rs is a thin path binding that
supplies crates/rig-cassette/fixtures/cassettes; a downstream can supply
fixtures/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:

  1. 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 under fixtures/cassettes/. The golden call is a no-op in that
    mode.
  2. 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.
  3. 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 is
tests/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 on
rig-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 in
fixtures/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 in fixtures/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-agent unit tests: anything that needs crate-private types.

Running

sh
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-memory saves 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 a vendor/model reference 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::registry references 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's id() returns a catalog selection only when its dialect name is registered. Providers outside this catalog can use a Model and ModelAdapter directly.
  • 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) over Catalog::builtin() with with_models_dev (which keeps rig's hand-reviewed facts) and with_overrides, and connect through the result.
  • catalog::Connector: reach a catalog's models by vendor/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

bash
cargo add rig-core --features reqwest

The reqwest feature gives provider clients a default HTTP transport, so
OpenAI::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's
Crypto.getRandomValues implementation, as modern browsers, Web Workers, and
Node.js 19 or later do. WASI targets are not supported.

Simple example

rust
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:

toml
rig = { version = "0.36.0", features = ["lancedb", "fastembed"] }

The following providers are available as separate companion-crates:

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 with
Dialect 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 and
ProviderError::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's
preserve_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. Set
with_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 to
ProviderError::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 a
Witness sink. Pass it separately from request data through
Model::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 reuse
observe::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 entity
    TurnOf its agent, and its calls are entities CallOf the turn.
  • stage: every step of an agent's work is an effect entity. Each frame the
    AgentPass schedule 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 by
    Deliver).
  • 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-memory Store, 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. The
rig-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

toml
[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 the
fastembed, 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:

toml
[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:

shell
export GEMINI_API_KEY=your_api_key_here

Example

rust,no_run
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. HeadlessPlugins is
    Bevy's MinimalPlugins with the log, a clean exit on signals and a loop that
    sleeps until there is work. RigHarnessPlugins adds 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 generated main.rs ends with rig_harness::run.
  • Everything else is a plugin in a crate of the repository's
    plugins/ folder, listed in plugins.toml and built only
    on public API, exactly as a third-party plugin is: the compiler holds them
    to it. Most are in rig-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
    generated main.rs adds each with load, 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 or
rig 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 the
inspect 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

bash
git clone https://github.com/0xPlaygrounds/rig && cd rig
cargo install --path .
rig

It needs Rust 1.97.1 or newer and runs on Linux and macOS. The optional
rig-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):

bash
cargo add rig-helixdb

There's a few different ways you can run HelixDB:

  • Through HelixDB's cloud offering
  • Running it locally through their helix start command (requires the Helix CLI to be installed).
    • For local dev, you will likely want to use helix push dev for continuous iteration.

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:

bash
helix push dev

This will then deploy the queries/schema into your instance.

To run the example, add your OpenAI API key as an environment variable:

bash
export OPENAI_API_KEY=my_key

Finally, use the following command below to run the example:

bash
cargo run --example vector_search_helixdb --features rig/derive

Overview (`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 (feature websocket): the websocket connection contract the
    Responses WebSocket session is written against;
  • wasm_compat: the Send/Sync bounds and boxed futures that relax on
    browser wasm;
  • runtime (feature tokio, 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) and
rig-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 implements
HttpClientExt:

rust
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:

toml
[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's
AgentBuilder::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):

bash
cargo add rig-milvus

There's a couple of different ways you can run Milvus:

Before creating a collection that is compliant with rig-milvus, ensure you set up your Milvus related environment variables:

bash
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.

bash
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 embedding field for storing vectors
  • a document field (for storing metadata)
  • an embeddedText field (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:

bash
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:

bash
cargo run --example vector_search_milvus --features rig/derive

Overview (`crates/rig-mongodb/README.md`)

text
+

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:

toml
[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`)

text
+

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:

toml
[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.

Notes

  • The rig-neo4j::vector_index module 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.
cypher
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`)

text
+

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:

toml
[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:

sh
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/pgvector

Now you can configure Postgres, the recommended way is using sqlx and migrations (you can find an example inside integration tests folder).

Example sql:

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 embeddings

You 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:

sh
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:

rust
#[derive(Embed, Clone, Serialize, Deserialize, Debug)]
pub struct Product {
    name: String,
    category: String,
    #[embed]
    description: String,
    price: f32
}

Example usage

rust
// 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

toml
[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:

rust
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.

  • McpConnection owns 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 on close().
  • McpTool is one server tool, from McpConnection::tools or McpTool::new(definition, &connection). It keeps its connection open, so an agent that holds its tools keeps the server connected. Convert it into a rig_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_name offers 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 McpMeta in the runtime's rig_core::tool::ToolContext is forwarded as the request's _meta, and McpTool::with_call_meta adds entries derived from the call itself (the MCP name and the exact arguments sent), for signatures or idempotency keys. The raw CallToolResult (with the response's structuredContent and _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::resources and read_resource list 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's AgentBuilder::dynamic_context_handler to 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 with notifications/tools/list_changed. rig-agent's ToolServerHandle implements ManagedToolSink, so McpClientHandler::new(client_config, tool_server_handle.clone()).connect(transport) is the agent usage. The registry owns those tools, so hold the McpConnection that connect returns 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

toml
[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

rust
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`)

text
+

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:

toml
[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 and
cargo 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:

rust
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.

rust
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:

rust
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

toml
[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:

How to run the example

To run the example, add your OpenAI API key as an environment variable:

bash
export OPENAI_API_KEY=my_key

Finally, use the following command below to run the example:

bash
cargo run --example vector_search_surreal --features rig/derive

Overview (`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 a
Workspace: 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:

rust
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. An
edit 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 with
path: "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:

rust,ignore
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 feature
typesafeai 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:

rust
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:

rust,ignore
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 before
decode 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), ...]) returns
    ChoiceAnswer<T> on evaluation. Values implement Serialize,
    DeserializeOwned, and Ord; their Serde representation must be a string.
  • Score::new(instructions, [(level, description), ...]) returns
    ScoreAnswer<L> with enum-keyed probabilities. Array order determines
    numeric positions, independently of enum discriminants and Ord.
    Evaluation requires L: Ord + Clone and the WASM-compatible transport bounds.
  • DynamicScore::new(instructions, descriptions) returns DynamicScoreAnswer
    with numeric indices and a legend when rubric identities are runtime-defined.
  • Noul::new(instructions) returns NoulAnswer, 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:

sh
cargo run -p typesafeai_chat

Add -- --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 against
jev-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

toml
[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

bash
npx wrangler vectorize create rig-integration-test --dimensions=1536 --metric=cosine

2. Set environment variables and run tests

bash
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=1

Tests 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:

bash
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=string

Overview (`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:

toml
[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:

rust
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's
rawPredict 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 carries
anthropic_version: "vertex-2023-10-16". Use Vertex AI's model ids, such as
claude-sonnet-4-6 or claude-haiku-4-5@20251001. global and the us and
eu multi-regions have their own hosts; any other location is a region.

rust,no_run
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 unless
VertexAiBuilder::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:

shell
gcloud auth application-default login

Supplying your own SDK client

VertexAi::from_env() (and VertexAi::builder() without either explicit credentials
or a supplied service) resolves Application Default Credentials. That resolution builds
google-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:

rust,no_run
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 bounded
RunEvents 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.

sh
OPENAI_API_KEY=… cargo run -p agent_no_tokio

Tokio appears only transitively, inside rig-reqwest, which drives the HTTP
wire on a private runtime:

sh
cargo tree -p agent_no_tokio -e normal -i tokio

Overview (`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:

bash
./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:

bash
./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.jpg

The 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:

bash
./examples/candle_wasm_chat/build.sh
./examples/candle_wasm_chat/serve.sh

Then 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 and
include_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 official
HuggingFaceTB/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:

bash
node --test examples/candle_wasm_chat/www/worker-runtime.test.mjs

After build.sh, exercise the actual embedded model, idempotent initialization,
chat history, and Clear behavior with:

bash
node examples/candle_wasm_chat/smoke.mjs

Measured 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:

sh
uv tool install 'litellm[proxy]'

From the repository root, in a shell with OPENAI_API_KEY exported, run:

sh
litellm --config examples/litellm/config.yaml --host 127.0.0.1 --port 4000

The supplied configuration maps the public alias rig-chat to
openai/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:

sh
LITELLM_API_KEY=unused cargo run -p litellm

The 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:

sh
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:

sh
cargo run -p typesafeai_chat

Try: 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:

sh
cargo run -p typesafeai_chat -- --agent

This calls Jev first, then uses its assessment to choose instructions for
gpt-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:

sh
cargo run -p typesafeai_triage

This 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's AGENTS.md files in the system
    prompt (AgentsMdPlugin);
  • skills: the skills listed in the system prompt, and /skill:<name>
    (SkillsPlugin);
  • models: /model, /effort and the model picker (ModelsPlugin), and
    the model a new session starts on (defaults::DefaultsPlugin);
  • login: /login and /logout for the ChatGPT plan's models and the
    providers' API keys (LoginPlugin);
  • session_picker: the session and conversation lists of /resume,
    /fork and /agents (SessionPickerPlugin, which the plugins of those
    commands add, so it is not listed on its own);
  • sessions: /new, /resume and /name (SessionsPlugin), and /fork,
    /agents and 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: the read, edit, write and shell tools (ReadTool,
    EditTool, WriteTool, ShellTool), @path attachments
    (AttachPlugin), and HOST:PATH paths 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: /reload and the reload tool (ReloadPlugin), and
    /plugins (plugins::PluginsPlugin);
  • print: --print and --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: the helper tool, which runs rig -p --json children
    (HelpersPlugin);
  • subagents: the task, message and wait tools (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:

toml
[[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 and
DiagnosticsPlugin are listed disabled by default. A plugin that needs
another adds it when it is missing, as SessionsPlugin adds
SessionPickerPlugin 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, in
RIG_HOME/settings.toml:

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: the
inspect 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:

bash
rig plugin add rig_inspect::InspectPlugin --crate rig-inspect

Only 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 default
plugins.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 the
rig-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 the
rig-ecs crate, for building your own harness.

Install and run

bash
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 it

The 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 in
RIG_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 at
https://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. A
CHATGPT_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's
Retry-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 sets
timeout_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 in
RIG_HOME/settings.toml turns that off.

With [remote.hosts] in RIG_HOME/settings.toml, such as
bench = ["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's
task 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 optional
rig_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 in
plugins/, most from rig-basics, so
any of them can be removed or replaced
(crates/rig-harness lists them):

toml
[[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 + Default

rig 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 before
plugins.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 from
rig_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: a
run_steel tool whose Steel (Scheme)
program spawns agents, messages them and calls tools, in a sandbox. Enable it
with:

toml
[[plugin]]
crate = "rig-steel"
plugin = "rig_steel::SteelPlugin"

A fan-out that asks three agents at once and keeps the shortest answer:

scheme
(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:

bash
cd rig && cargo install --path . --root /some/dir
RIG_HOME=/some/dir/home RIG_SOURCE=$PWD /some/dir/bin/rig

--- METRICS ---