rpi-agent
The provider-agnostic agent runtime: the agent loop, the AgentTool trait,
events, hooks, queues and cancellation. This is the layer you embed when you
want to build your own agent rather than use the rpi CLI.
Install
[]
= "0.3"
= "0.3"
= { = "1", = ["rt-multi-thread", "macros"] }
For built-in file and shell tools, add
rpi-tools. For durable sessions and
compaction, add rpi-harness.
Design
The only LLM boundary is StreamFn, which returns an
AssistantMessageEventStream synchronously. Everything else in the loop speaks
AgentMessage and never touches a provider wire format, so a provider, a test
double and a recorded transcript are interchangeable.
The invariants that the loop guarantees:
- Tools may finish in any order, but tool results are emitted in the order the calls appeared, so the transcript stays valid for the next request.
- A
MessageEndis emitted exactly once per assistant message, including when a late streaming update arrives after the run already settled. - Truncated or failed tool calls never leave a dangling call without a result.
Optional native message identity
Public AgentEvent::{MessageStart, MessageUpdate, MessageEnd} now carry
message_id: Option<String>. The direct agent loop emits None; a native
session/harness can attach its reserved entry ID before publishing the event
and commit that same ID to history. Provider response IDs and tool-call IDs are
not substitutes. Tool-execution and unrelated lifecycle events do not acquire
this field. Consumers constructing these Rust enum variants must supply
message_id (use None without a session owner); pattern matches that do not
need identity can use ... Optional wire fields do not make adding a Rust enum
field source-compatible with exhaustive old constructors/patterns.
AgentEmitter::emit_injected(event) marks an actual queued message delivery
separately from ordinary generation, so the session owner can consume the
correct durable reservation even for identical-content messages. Its default
implementation calls emit(event), preserving existing custom emitter behavior
without requiring an override. It does not generate an ID or change provider
message content. See the RPC identity contract
for native advertisement and adapter scope.
Quick start
Deterministic, offline, no API key — the faux provider scripts one text reply:
use Arc;
use AgentBuilder;
use ;
use Provider;
async
A runnable version of this lives in
examples/minimal
(cargo run -p minimal), and a version that drives the real filesystem through
the built-in tools lives in
examples/tools.
Defining a tool
Tools implement AgentTool. A tool owns the provider-facing schema, a display
label, and the execution function; parameters are validated against that schema
before execute is called:
use Arc;
use async_trait;
use ;
use Tool;
use CancellationToken;
execute receives a CancellationToken for cancellation and an on_update
callback for partial results. Calls to on_update after the run has settled are
dropped by the loop, so a tool never has to track settlement itself. Returning
Err(AgentError) turns into an error tool result, not a failed run.
For real implementations, wrap the JSON Schema derivation with schemars and
start from rpi-tools, which contains
complete examples of a filesystem tool and a streaming shell tool.
Observing a run
agent.subscribe() returns a broadcast receiver of AgentEvent covering the
run (AgentStart/AgentEnd), each assistant message
(MessageStart/MessageUpdate/MessageEnd), and each tool
(ToolStart/ToolUpdate/ToolEnd). Cancellation goes through AbortHandle.
Related crates
| Crate | Role |
|---|---|
rpi-ai |
Provider-agnostic message and streaming types |
rpi-tools |
read/write/edit/bash and friends |
rpi-harness |
Sessions, JSONL persistence, compaction |
rpi-cli |
The rpi terminal agent built on all of the above |
Documentation
- Crate docs: https://docs.rs/rpi-agent
- Architecture: docs/architecture/overview.md
- Project structure guide: docs/agent/project-structure.md