name: rig-agent-scaffold description: > Scaffold a Rig agent implementation. Use when creating agents, chatbots, RAG pipelines, or tool-calling workflows with Rig's builder pattern. argument-hint: "[agent-description]" allowed-tools:
- •Read
- •Glob
- •Grep
- •Edit
- •Write
- •Bash
Rig Agent Scaffold
Create or refactor a Rig agent implementation.
Execution requirements:
- •Preserve trait-based abstractions and builder-pattern ergonomics.
- •Use
WasmCompatSend/WasmCompatSyncbounds where applicable. - •Handle all fallible paths without
.unwrap()/.expect()unless impossible. - •Add tests or compileable examples for user-facing functionality.
- •Run
cargo fmt,cargo clippy --all-targets --all-features, andcargo test.
rig-core-patterns
Rig is built on several core principles that all contributions must respect:
Trait-Based Abstractions
Rig uses traits to define provider-agnostic interfaces:
- •
CompletionModel- For text completion/chat models - •
EmbeddingModel- For embedding generation - •
VectorStoreIndex- For vector similarity search - •
Tool- For defining callable tools
New implementations should implement these traits rather than creating parallel abstractions.
Builder Pattern
Nearly every configurable type uses the builder pattern:
let agent = client
.agent(openai::GPT_5_2)
.preamble("System prompt")
.tool(my_tool)
.temperature(0.8)
.build();
Follow this pattern for any new configurable types.
Generic Client Architecture
The client system uses a generic architecture with provider extensions:
pub struct Client<Ext = Nothing, H = reqwest::Client> {
// ...
}
Where Ext is a provider extension defining capabilities and H is the HTTP backend.
Capability-Based Providers
Providers declare their capabilities explicitly:
impl<H> Capabilities<H> for MyProviderExt {
type Completion = Capable<CompletionModel<H>>; // Supported
type Embeddings = Nothing; // Not supported
// ...
}
Use Capable<T> for supported features and Nothing for unsupported ones.
rig-wasm-compat
Rig supports WebAssembly targets. Since WASM is single-threaded, Send/Sync bounds are unnecessary and can prevent compilation.
The Pattern
#[cfg(not(target_family = "wasm"))]
pub trait WasmCompatSend: Send {} // native
#[cfg(target_family = "wasm")]
pub trait WasmCompatSend {} // wasm
#[cfg(not(target_family = "wasm"))]
pub trait WasmCompatSync: Sync {} // native
#[cfg(target_family = "wasm")]
pub trait WasmCompatSync {} // wasm
Usage Rules
Always use WasmCompatSend and WasmCompatSync instead of raw Send and Sync in trait bounds.
// Correct
pub trait MyTrait: WasmCompatSend + WasmCompatSync {
fn do_thing(&self) -> impl Future<Output = ()> + WasmCompatSend;
}
// Incorrect - will break WASM builds
pub trait MyTrait: Send + Sync {
fn do_thing(&self) -> impl Future<Output = ()> + Send;
}
Boxed Futures
Use WasmBoxedFuture for boxed futures:
pub type WasmBoxedFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>; // native pub type WasmBoxedFuture<'a, T> = Pin<Box<dyn Future<Output = T> + 'a>>; // wasm
Conditional Error Types
Some error types need platform-specific bounds:
#[derive(Debug, thiserror::Error)]
pub enum MyError {
#[cfg(not(target_family = "wasm"))]
#[error("Error: {0}")]
Inner(#[from] Box<dyn std::error::Error + Send + Sync + 'static>),
#[cfg(target_family = "wasm")]
#[error("Error: {0}")]
Inner(#[from] Box<dyn std::error::Error + 'static>),
}
rig-prompt-hooks
Prompt hooks allow observing and controlling the agent's execution lifecycle.
The PromptHook Trait
PromptHook<M> provides callbacks for both streaming and non-streaming requests. All methods have default implementations — implement only what you need.
Hook Callbacks
- •
on_completion_call- Before sending prompt to model (returnsHookAction) - •
on_completion_response- After receiving response (returnsHookAction) - •
on_tool_call- Before invoking a tool (returnsToolCallHookAction) - •
on_tool_result- After tool returns result (returnsHookAction) - •
on_text_delta- During streaming, on each text chunk (returnsHookAction) - •
on_tool_call_delta- During streaming, on each tool call chunk (returnsHookAction)
Control Flow
- •
ToolCallHookAction::Continue(or.cont()) - Allow tool execution - •
ToolCallHookAction::Skip { reason }(or.skip("reason")) - Reject tool; reason becomes tool result - •
ToolCallHookAction::Terminate { reason }(or.terminate("reason")) - Terminate the agentic loop early - •
HookAction::Continue(or.cont()) - Continue normal execution - •
HookAction::Terminate { reason }(or.terminate("reason")) - Terminate the agentic loop early
Design Rationale
- •Default implementations - All methods have defaults; implement only what you need
- •Per-request hooks - Attached via
.with_hook(), not globally - •WASM compatible - Uses
WasmCompatSend/WasmCompatSyncbounds
rig-quality-gates
Trait Bounds
Use full where clause syntax for readability:
// Correct
impl<T> CompletionModel for MyModel<T>
where
T: HttpClientExt + Clone + WasmCompatSend + Debug + Default + 'static,
{
// ...
}
// Avoid inline bounds for complex signatures
impl<T: HttpClientExt + Clone + WasmCompatSend + Debug + Default + 'static> CompletionModel for MyModel<T> {
// ...
}
Error Handling
DO NOT use String as an error type. Define proper error enums:
// Correct
#[derive(Debug, thiserror::Error)]
pub enum MyError {
#[error("Failed to parse response: {0}")]
ParseError(#[from] serde_json::Error),
#[error("Provider returned error: {0}")]
ProviderError(String),
}
// Absolutely forbidden
fn do_thing() -> Result<(), String> // NO
DO NOT stub out error handling:
// Forbidden
let result = fallible_operation().unwrap(); // NO
let result = fallible_operation().expect("this should work"); // NO unless truly impossible
// Correct
let result = fallible_operation()?;
let result = fallible_operation().map_err(MyError::from)?;
Comments
Comments explain WHY, not WHAT.
If you need to explain what code is doing, the code itself is unclear. Rename variables, extract functions, or restructure.
// Bad - explains what
// Increment counter by one
counter += 1;
// Bad - explains what
// Check if user is admin
if user.role == Role::Admin {
// Good - explains why
// Rate limiting resets at midnight UTC, so we need the day boundary
let boundary = timestamp.truncate_to_day();
// Good - explains non-obvious business logic
// Providers may return tool calls split across multiple chunks,
// so we accumulate them by index until the stream ends
Documentation
- •Add doc comments (
///) to all public items - •Add module-level documentation (
//!) to modules - •Include usage examples in doc comments where helpful
TODO Items
TODO items are not allowed in submitted code.
If you need a TODO, the implementation is incomplete. Either:
- •Complete the implementation
- •Don't submit it yet
- •Create a separate issue tracking the future work
// Forbidden
fn process() {
// TODO: handle edge case
}
// Forbidden
fn process() {
unimplemented!("will add later")
}
Before Submitting
Required Checks
Run these commands and fix all issues before submitting:
cargo fmt cargo clippy --all-targets --all-features cargo test
If you cannot run these commands (e.g., environment limitations), explicitly ask the user to run them before the PR is submitted.
Architectural Changes
If your change involves:
- •New traits or significant trait modifications
- •Changes to the client/provider architecture
- •New abstractions or patterns
- •Breaking changes to public APIs
Discuss with the user first. Do not implement major architectural changes without explicit approval. Open an issue for discussion if needed.
Self-Review Checklist
Before considering code complete:
- • All error cases handled properly (no
.unwrap()or.expect()on fallible operations unless truly impossible) - • No
Stringerror types - • No TODO comments
- • No stubbed implementations
- • Comments explain "why", not "what"
- • Uses
WasmCompatSend/WasmCompatSyncinstead ofSend/Sync - • Follows existing code patterns in the codebase
- • Includes tests or examples that compile
- •
cargo fmtpasses - •
cargo clippy --all-targets --all-featurespasses - •
cargo testpasses
Commits
DO NOT MAKE COMMITS UNLESS THE USER HAS ASKED YOU TO DO SO. Users should be able to manually verify that what you have done has worked before proceeding with a commit, and may also want to write their own commit messages.
If the PR contains breaking changes, the PR message must list the public API items that have broken and the migration path for each.
What Will Be Rejected
PRs will be rejected if they contain:
- •Lazy workarounds -
Stringerror types,.unwrap()everywhere, incomplete handling - •TODO items - Submit complete work or don't submit
- •Stubbed error handling - Every error path must be properly handled
- •Provider API divergence - Don't add fields/features that don't exist in real provider APIs
- •Missing WASM compatibility - Using
Send/Syncinstead ofWasmCompat*variants - •Unclear code requiring explanatory comments - Refactor instead
- •Architectural changes without discussion - Major changes need approval first
Remember: The quality bar exists because Rig is used in production by many projects. Every contribution must maintain that standard.