rig-compose 0.3.0

Composable agent kernel: stateless skills, transport-agnostic tools, registry-driven agents, signal-routing coordinator. Companion crate for rig.
Documentation

rig-compose

Composable agent kernel: stateless skills, transport-agnostic tools, registry-driven agents, signal-routing coordinator. Companion crate for rig.

crates.io docs.rs license

Overview

rig-compose is a domain-neutral composition layer for Rig-shaped agent systems. It provides the kernel traits and data structures for stateless skills, side-effectful tools, shared registries, generic agents, deterministic signal routing, workflows, in-process agent delegation, and optional YAML manifest loading.

The crate does not call an LLM itself and does not depend on rig-core; instead it defines the small tool and skill surfaces that companion crates such as rig-mcp and rig-resources plug into.

Why it exists

Rig supplies provider-agnostic model, embedding, vector-store, and tool traits. rig-compose fills a different gap: it organizes domain skills and tools into repeatable agent workflows without forcing those workflows to know whether a tool is local, delegated to another in-process agent, or surfaced through MCP.

This keeps downstream systems from reimplementing the same coordination pieces: Skill, Tool, ToolRegistry, SkillRegistry, GenericAgent, CoordinatorAgent, DelegateTool, and the budget guards used to meter finite work.

Status

  • Crate version: 0.2.0.
  • Rust edition: 2024.
  • MSRV: 1.88.
  • Runtime stance: runtime-agnostic library; tokio is used only as a dev-dependency for tests and examples.
  • Current Unreleased work adds the budget module, drop-safe TokenReservation refunds, KernelError::ToolNotApplicable for soft tool failures, provider-neutral tool-call normalization/dispatch helpers, dispatch hooks, provider-neutral context packing primitives, and bounded tool-result envelopes.

The crate-local maturity plan lives in ROADMAP.md. Cross-crate coordination lives in rig-contributions/docs/roadmap.md.

Feature flags

Feature Default Enables Checked by just check
none yes Core agent, skill, tool, registry, workflow, delegate, coordinator, context, instruction, and budget APIs. cargo clippy --all-targets, cargo test --all-targets
manifest no YAML-backed portable agent manifest types and materialization helpers from src/manifest.rs. Pulls serde_yaml. cargo clippy --all-targets --features manifest, cargo test --all-targets --features manifest, docs and examples with all features

Key Types

  • src/skill.rs: Skill, SkillId, and SkillOutcome. Skills are stateless and decide whether they apply to an InvestigationContext.
  • src/tool.rs: Tool, ToolSchema, ToolName, LocalTool, ToolResultEnvelope, and ToolResultEnvelopeConfig. Tools are the side-effectful async boundary; envelopes provide deterministic bounding metadata for large tool results.
  • src/registry.rs: ToolRegistry, SkillRegistry, and KernelError. Registries hold shared tools and skills; ToolRegistry::scoped produces per-agent tool views.
  • src/agent.rs: Agent, AgentId, AgentStepResult, GenericAgent, and GenericAgentBuilder. Generic agents run registered skill chains over mutable investigation context.
  • src/context.rs: InvestigationContext, Signal, Evidence, and NextAction for skill-chain state, plus ContextItem, ContextPack, ContextPackConfig, and ContextSourceKind for provider-neutral context-window planning.
  • src/delegate.rs: DelegateExecutor, DelegateRegistry, DelegateTool, DelegateName, and InProcessAgentDelegate. This is the model-driven agent-to-agent delegation path.
  • src/coordinator.rs: CoordinatorAgent, CoordinatorBuilder, and RoutingRule. This is deterministic first-match routing for fixed topologies.
  • src/budget.rs: BudgetGuard, TokenBudget, AtomicBudget, AtomicTokenBudget, TokenReservation, TokenRefund, and BudgetError. These meter rows, dispatch slots, and prompt-token reservations.
  • src/normalizer.rs: ToolCallNormalizer, LfmNormalizer, StructuredToolCallNormalizer, ToolInvocation, and dispatch_tool_invocations. These normalize LFM/MLX text markers, OpenAI Responses function_call output, and OpenAI Chat Completions tool_calls into the same dispatchable shape.
  • src/workflow.rs: Workflow, the async workflow composition trait.
  • src/instructions.rs: Instructions, a serializable instruction bundle with examples, response schema, and metadata.
  • src/manifest.rs: AgentManifest, ModelSpec, ToolSpec, DelegateSpec, and materialization helpers, gated behind manifest.

The crate-level architecture rule is simple: Skill is pure decision logic, Tool is the side-effect boundary, registries own lookup, and agents compose registered pieces without hard-coding concrete implementations.

Integration With Rig

rig-compose does not pin rig-core in Cargo.toml. It is intentionally Rig-shaped rather than Rig-bound: other crates can adapt Rig tools, MCP tools, local closures, or in-process delegates into the same Tool and Skill flow.

The main companion integration points are:

  • rig-mcp implements remote MCP access by adapting MCP endpoints into rig_compose::Tool values.
  • rig-resources provides reusable skills and tools that implement rig-compose traits.
  • Downstream systems can use BudgetGuard and TokenBudget to enforce compute budgets around agent dispatch and LLM token use.

Usage

The minimal runnable example is examples/basic_agent.rs. It registers one stateless skill, builds a GenericAgent, and runs that agent against an InvestigationContext.

use std::sync::Arc;

use async_trait::async_trait;
use rig_compose::{
    Agent, GenericAgent, InvestigationContext, KernelError, Skill, SkillOutcome, SkillRegistry,
    ToolRegistry,
};

struct KeywordSkill;

#[async_trait]
impl Skill for KeywordSkill {
    fn id(&self) -> &str {
        "example.keyword"
    }

    fn applies(&self, context: &InvestigationContext) -> bool {
        context.has_signal("keyword.match")
    }

    async fn execute(
        &self,
        _context: &mut InvestigationContext,
        _tools: &ToolRegistry,
    ) -> Result<SkillOutcome, KernelError> {
        Ok(SkillOutcome::default().with_delta(0.25))
    }
}

# async fn run() -> Result<(), KernelError> {
let skills = SkillRegistry::new();
skills.register(Arc::new(KeywordSkill));

let tools = ToolRegistry::new();
let agent = GenericAgent::builder("triage")
    .with_skills(["example.keyword"])
    .build(&skills, &tools)?;

let mut context = InvestigationContext::new("entity-1", "default")
    .with_signal("keyword.match");
let result = agent.step(&mut context).await?;

assert_eq!(result.skills_run, vec!["example.keyword".to_string()]);
# Ok(()) }

The budget behavior is covered by the unit tests in src/budget.rs, including reservation reconciliation and drop-time refunds.

Tool-Call Normalization

Local OpenAI-compatible servers sometimes emit tool intent as text instead of provider-native structured tool calls. rig-compose normalizes both paths into one kernel shape:

raw model output or provider JSON
    -> ToolInvocation { name, args }
    -> ToolRegistry
    -> tool result
    -> next model turn / final answer

Supported normalizers today:

  • LfmNormalizer: parses LiquidAI LFM markers such as <|tool_call_start|>[get_weather(city='Berlin')]<|tool_call_end|>.
  • StructuredToolCallNormalizer::normalize_openai_responses: parses OpenAI Responses function_call output items or full response objects.
  • StructuredToolCallNormalizer::normalize_openai_chat_completions: parses OpenAI Chat Completions tool_calls or full chat completion responses.

All of these produce ToolInvocation values that can be dispatched through dispatch_tool_invocations(&ToolRegistry, &[ToolInvocation]).

The intended agent loop is: normalize the first model response, dispatch any invocations through the registry, pass the tool results back to the model, then let the model produce the final answer. rig-compose owns the provider-neutral normalization and dispatch pieces; callers decide how to encode tool results for their provider or local server.

Use dispatch_tool_invocations_with_hooks when callers need policy, accounting, or tracing around dispatch. ToolDispatchHook can continue an invocation, skip it with a synthetic result, or terminate the loop before the tool runs. Hooks receive only kernel shapes (ToolInvocation and ToolInvocationResult), keeping provider, MCP, memory, approval, and telemetry implementations downstream.

DispatchBudgetHook is the first concrete hook. It gates each normalized tool invocation on a BudgetGuard, terminates dispatch when the budget denies the reservation, and releases the reservation after success, skip, or dispatch error.

Context Planning

ContextItem is the provider-neutral shape for one piece of context that may enter a model window. It records the source kind, stable source id, rank, score, prompt-ready text, character estimate, provenance, and metadata without tying the kernel to memory, MCP, files, resource stores, or provider SDKs.

ContextPack takes ranked context items and a ContextPackConfig, selects what fits in a bounded character window, and records omitted items with explicit ContextOmissionReason values. This is the kernel-level generalization of the memory candidate/context-pack work in rig-memvid: memory cards, tool results, resource lookups, files, and reasoning workspace notes can all project into the same packable shape.

use rig_compose::{ContextItem, ContextPack, ContextPackConfig, ContextSourceKind};

let memory = ContextItem::new(
    ContextSourceKind::Memory,
    "memory/card/alice-location",
    "fact alice lives in Berlin",
)
.with_rank(0)
.with_score(9.5);

let tool_result = ContextItem::new(
    ContextSourceKind::ToolResult,
    "tool/weather/call-1",
    "weather Berlin is clear and cool",
)
.with_rank(1);

let pack = ContextPack::pack(
    vec![tool_result, memory],
    ContextPackConfig::new(1_000).with_max_items(8),
);

assert!(pack.render_text().contains("Berlin"));

Harness Prototype

examples/tool_loop_harness.rs shows the first deterministic harness shape around that loop. It records the task, first model output, normalized invocations, dispatched tool results, final answer, and passed assertions. This is intentionally an example rather than a new crate: it lets the platform prove the run-record vocabulary before extracting a dedicated rig-harness layer.

Validation

Canonical validation is just check.

That recipe runs formatter checks, clippy and tests for default features and manifest, rustdoc with -D warnings -D rustdoc::broken_intra_doc_links, and cargo build --examples --all-features.

Gotchas

  • DelegateTool and CoordinatorAgent solve different routing problems. Use DelegateTool when the model should decide whether to call another agent; use CoordinatorAgent for fixed host-driven signal routing.
  • KernelError::ToolNotApplicable is a soft failure for tools that cannot apply to the current context. Callers may treat it as a no-op, as rig-resources does for missing graph entities.
  • TokenReservation refunds its estimate on drop unless AtomicTokenBudget::record_usage disarms it. Keep the handle alive until actual usage is recorded.
  • The library is runtime-agnostic. Do not add runtime dependencies to [dependencies]; tests and examples use tokio from [dev-dependencies].

Ecosystem

These companion crates are maintained as separate repositories. Together they form a small stack around the upstream Rig project: rig-compose provides the kernel surface, rig-resources contributes reusable skills and tools, rig-mcp moves tools across MCP, and rig-memvid connects Rig agents to persistent .mv2 memory.

flowchart TD
    rig["rig / rig-core"]
    compose["rig-compose 0.2.x"]
    resources["rig-resources 0.1.x"]
    mcp["rig-mcp 0.1.x"]
    memvid["rig-memvid 0.1.x"]

    compose -. "Rig-shaped kernel; no direct rig-core dep" .-> rig
    resources -- "rig-compose = 0.2; features: security, graph, full" --> compose
    mcp -- "rig-compose = 0.2; rmcp stdio bridge" --> compose
    memvid -- "rig-core = 0.36.0; features: lex, vec, api_embed, temporal, encryption" --> rig

Pinned Rig-facing dependencies from the current manifests:

Crate Direct Rig-facing dependency Notes
rig-compose none Defines a Rig-shaped kernel surface without depending on rig-core.
rig-resources rig-compose = 0.2 Provides reusable skills, resource tools, and security helpers.
rig-mcp rig-compose = 0.2 Bridges rig-compose tools over MCP stdio and loopback transports.
rig-memvid rig-core = 0.36.0 Implements Rig vector-store and prompt-hook flows over Memvid.

The concrete multi-crate workflow tested today is the MCP loopback path: a rig_compose::ToolRegistry is exposed through rig_mcp::LoopbackTransport, remote schemas are wrapped as rig_mcp::McpTool, and the wrapped tools are registered back into another ToolRegistry. That proves a local rig-compose tool and an MCP-adapted tool are indistinguishable to callers. The backing test is mcp_tool_indistinguishable_from_local in rig-mcp/src/transport.rs.

License

Licensed under either Apache-2.0 or MIT, at your option.