-
Notifications
You must be signed in to change notification settings - Fork 15
Examples
Runnable examples live in
crates/tinyagents-integration-tests/examples/.
They belong to the tinyagents-integration-tests package, so run them with
-p, not a bare cargo run --example:
cargo run -p tinyagents-integration-tests --example <name>Run every command from the repository root.
Examples whose name starts with openai_ (plus orchestrator_subagents) call
the real OpenAI API and need OPENAI_API_KEY:
export OPENAI_API_KEY=... # or copy .env.example to .env
cargo run -p tinyagents-integration-tests --example openai_chatEverything else — basic_graph, durable_graph, resilient_graph,
complex_graph, agent_loop_tools, rag_blueprint, goals_and_todos, and
subconscious_loop — runs offline with no key and no feature flag.
A minimal durable graph: a whole-state agent/tool loop. A GraphBuilder over
Update == State (the overwrite reducer, so each node returns the full next
state) wires an agent node and a tool node. Conditional edges route
agent -> tool while needs_tool is set, otherwise agent -> END; tool
clears the flag and loops back to agent.
Partial updates, a reducer, and checkpointing. State is a Counter; each node
returns a small i64 partial update, and a ClosureStateReducer folds
updates into the running counter while appending a log line. The graph runs
on a thread backed by an InMemoryCheckpointer, and the example lists the
checkpoints written at each superstep boundary.
Two resilience primitives on one graph. CompiledGraph::with_node_retry
re-runs a node from its start when it fails with a retryable error — here a
fetch node fails its first two attempts and succeeds on the third. A
commit node then fails past the retry budget entirely; on a checkpointed
thread the run persists a resumable failure-boundary checkpoint instead of
losing progress, and CompiledGraph::retry restarts the run from that
checkpoint once the simulated outage clears.
A subgraph embedded inside a parallel fan-out/join. dispatch fans out via
Command::goto into two branches that run concurrently. One branch
(branch_sub) is a subgraph embedded with adapter_subgraph_node; its child
itself embeds a further subgraph via shared_subgraph_node, so a value flows
up through two levels of nesting (0 -> +5 -> +1 = 6). The reducer
deterministically joins the branches before join finalizes the run.
The harness agent loop end-to-end with a real tool and no network. A
ScriptedModel is scripted to first request a tool call, then, after seeing
the tool result, produce a final answer. A small CalculatorTool (add) is
registered so the loop has something to run — this is the model-tool-model
loop the harness runs under every example that talks to a real provider.
The simplest hosted call: register an OpenAiModel in an AgentHarness, set
it as the default model, run one question through the default agent loop,
and print the answer plus token usage.
The full tool-calling loop against a real provider. A local get_weather
tool is registered alongside an OpenAiModel; the question triggers the
tool, the harness runs it locally, feeds the result back, and the model
produces the final answer.
Schema-constrained output. RunPolicy::default_response_format is set to a
ResponseFormat::json_schema describing a {sentiment, score} object; the
harness attaches it to every request and extracts the parsed JSON into the
run's structured field.
A durable graph whose node drives a real OpenAI-backed harness. An
AgentHarness is wrapped in an Arc and captured in a graph-node closure;
the graph runs START -> agent -> END, where the node calls the harness
(which talks to OpenAI), stores the answer in graph state, and ends.
Runs a matrix of scenarios — plain text, tool calls, forced/required tool
choice, parallel tool calls, streaming, JSON object/schema output, and
<think>-tag leakage — directly against an OpenAiModel pointed at a local
server, and prints a PASS/FAIL line per scenario:
OPENAI_BASE_URL=http://localhost:1234/v1 \
OPENAI_MODEL=qwen/qwen3-4b \
OPENAI_API_KEY=local \
cargo run -p tinyagents-integration-tests --example local_model_probeUseful for checking a local runtime (Ollama, LM Studio, llama.cpp server) against the same wire-format assumptions the OpenAI provider makes.
An orchestrator that decides at runtime which sub-agents to call, resolving
them by name from a CapabilityRegistry. Three specialist sub-agents
(researcher, coder, summarizer) — each an OpenAiModel with a distinct
system prompt — are wrapped as SubAgentTools and registered by name. The
flow:
- Register the named capabilities.
- Discover the available names and descriptions back out of the registry — nothing is hard-coded into the planner.
- Design — an orchestrator agent, given the task plus the discovered menu, decides via structured output which sub-agents to invoke.
-
Bind at runtime — each chosen name is resolved with
CapabilityRegistry::tool, the sub-agents run in parallel (join_all), and their results are composed into a final answer.
The orchestrator never holds a direct handle to a sub-agent; it only knows names and looks them up in the registry when it decides to use one.
Compiles a .rag blueprint (a small support-ticket workflow) and binds its
capabilities. The example parses the source into a Program, compiles it
into a Blueprint, prints the node/edge/route structure, then calls
bind_capabilities to resolve the blueprint's referenced model and tools
against a CapabilityResolver allowlist.
OpenAI is asked to emit .rag source for a small agent graph (given the
grammar plus a worked example in the system prompt). The .rag text is
extracted from the reply and run through the same pipeline as a
human-authored blueprint: parse_str -> compile -> print the Blueprint
-> bind_capabilities against a CapabilityResolver allowlist -> build the
graph with a NodeFactory -> run to END. The model only ever produces
declarative source; it does not execute code, and the capability allowlist
is what keeps an unexpected model or tool name from being usable. A parse or
compile failure prints the diagnostic and the offending source instead of
panicking.
A multi-file example under examples/subconscious_loop/ (see its own
README)
that models an autonomous closed loop with three stages on the graph
runtime: a quick layer (frontend_agent) that turns channel input into
instructions and later compiles the final response, a reasoning layer
(agent_execution) that does mock memory retrieval, simulates sub-agent
work, and emits a state diff, and a subconscious layer
(subconscious_eval) that consumes gated summaries and emits a steering
directive for the next cycle. A summarization_gate compresses diffs and a
context_manager_hook evicts history into a mock vector store once
context_utilization crosses a threshold. Everything is deterministic, so
it runs under cargo test as well as as a binary. Matching integration
coverage is in tests/e2e_subconscious_loop_example.rs.
Wires a durable goal (tinyagents_graph::goals) and a kanban task board
(tinyagents_graph::todos) together on one thread, backed by a shared
InMemoryStore, and lets the goal drive the board. A ThreadGoal ("ship the
v2 release") is the completion contract with a token budget; a TaskBoard
holds three cards; a goal_gate_node loops, advancing the board by one
kanban transition (Todo -> InProgress -> Done) per iteration, accounting
token usage against the goal's budget, and completing the goal once every
card is done. See Goals and Todos for the full design.
Small, copy-pasteable snippets for less obvious harness/graph/registry behavior. Each cites the test it mirrors.
Resolution normally skips models whose profile reports ModelStatus::Retired;
set allow_retired to opt one back in. (See
crates/tinyagents-harness/src/model_registry/mod.rs,
ModelRegistry::resolve.)
use tinyagents_harness::{ModelRegistry, ModelSelection};
// Without allow_retired this returns None; with it the retired model resolves.
let binding = registry.resolve(ModelSelection {
requested: Some("gpt-legacy".into()),
allow_retired: true,
..ModelSelection::default()
});Turn a model's call to an unregistered tool into a recoverable tool-error
message instead of aborting the run. (See
crates/tinyagents-integration-tests/tests/e2e_unknown_tool_policy.rs,
return_tool_error_preserves_original_arguments.)
use tinyagents_harness::runtime::{RunPolicy, UnknownToolPolicy};
harness.with_policy(RunPolicy {
unknown_tool: UnknownToolPolicy::ReturnToolError,
..RunPolicy::default()
});
// The next run injects an "unknown tool `..`" message and keeps going.This emits AgentEvent::UnknownToolCall { requested_name, arguments, recovery, .. }
with recovery == "tool_error".
Prepare an isolated workspace, enforce that every tool path stays inside it,
then clean up. (See crates/tinyagents-harness/src/workspace/mod.rs.)
use std::path::Path;
use tinyagents_harness::context::{RunConfig, RunContext};
use tinyagents_harness::events::EventSink;
use tinyagents_harness::workspace::{
SharedRootWorkspace, cleanup_workspace, prepare_workspace,
};
let events = EventSink::new();
let provider = SharedRootWorkspace::new("/work");
let ws = prepare_workspace(&provider, &events, "run-7", Some("worker")).await?;
ws.enforce(Path::new("/work/out.txt"), &events)?; // Err for paths outside the root
let ctx = RunContext::new(RunConfig::new("run-7"), ()).with_workspace(ws.clone());
cleanup_workspace(&provider, &events, &ws).await?;A blocked path emits workspace.violation and fails closed; setup/teardown
emit workspace.prepared and workspace.cleanup.
Gate tool exposure and execution: sandboxed tools require a sandboxed
workspace, approval-gated tools require an explicit grant, and oversized
results are truncated. (See
crates/tinyagents-harness/src/middleware/library/test.rs,
tool_policy_requires_sandbox_for_sandboxed_tool,
tool_policy_truncates_oversized_results.)
use tinyagents_harness::middleware::ToolPolicyMiddleware;
let mw = ToolPolicyMiddleware::new(policies) // HashMap<String, ToolPolicy>
.require_sandbox(true)
.require_approval(["deploy"])
.enforce_result_bytes(true);A blocked call surfaces as TinyAgentsError::Validation; an oversized result
is truncated in place with a note attached.
inheriting composes a parent allow/deny list with a child's so a sub-agent
can only narrow, never widen, the tools exposed to it. (See
crates/tinyagents-harness/src/middleware/library/test.rs,
contextual_selection_inheriting_narrows_never_widens.)
use tinyagents_harness::middleware::ContextualToolSelectionMiddleware;
// parent allow ∩ child allow, then union of both deny lists.
let mw = ContextualToolSelectionMiddleware::inheriting(
Some(["a", "b", "c"]), // parent allow
["c"], // parent deny
Some(["b", "c", "d"]), // child allow
Vec::<String>::new(), // child deny
);Emits AgentEvent::ToolsFiltered { excluded, remaining, .. } before the
model call.
Request a control outcome from middleware; the loop honors it at the
checkpoint after the model call, keeping the highest-precedence request.
(See crates/tinyagents-harness/src/context/test.rs,
request_control_keeps_highest_precedence.)
use tinyagents_harness::context::MiddlewareControl;
// From inside a middleware's after_model, on `ctx: &mut RunContext`:
ctx.request_control(MiddlewareControl::StopWithFinal("stopped".into()));
// A stronger Interrupt is never downgraded by a later StopWithFinal.StopWithFinal ends the run and still emits run.completed plus
control.applied; Interrupt surfaces as TinyAgentsError::Interrupted.
Preflight reserves against estimated input tokens (blocking oversized calls)
and reconciles against actual usage; a separate cap bounds cache-read
tokens. (See crates/tinyagents-harness/src/middleware/library/test.rs,
budget_preflight_reserves_and_reconciles,
budget_enforces_cached_input_token_limit.)
use tinyagents_harness::middleware::{BudgetLimits, BudgetMiddleware};
let mw = BudgetMiddleware::new(BudgetLimits {
max_input_tokens: Some(5),
max_cached_input_tokens: Some(10),
..BudgetLimits::default()
});Emits AgentEvent::BudgetReserved on preflight and BudgetReconciled after
the call; exhaustion emits BudgetExceeded { blocked: true, .. } and errors
with TinyAgentsError::LimitExceeded.
Seed a sink with a stream id so (stream_id, offset) re-mints identical
event ids across restarts or replays. (See
crates/tinyagents-harness/src/events/test.rs,
stream_id_prefix_makes_event_ids_stable_and_collision_free.)
use tinyagents_harness::events::{AgentEvent, EventSink};
let sink = EventSink::with_stream_id("run-42");
let record = sink.emit(AgentEvent::StateUpdate);
assert_eq!(record.id.as_str(), "run-42-evt-0"); // stable across restartIds are "<stream_id>-evt-<offset>"; default sinks get process-unique
prefixes instead.
Run a fallible async closure over items with bounded concurrency, per-item
and total timeouts, and a cancellation token; results stay in input order.
(See crates/tinyagents-graph/src/parallel/.)
use std::time::Duration;
use tinyagents_graph::{ParallelOptions, map_reduce};
use tinyagents_harness::{CancellationToken, TinyAgentsError};
let token = CancellationToken::new();
let out = map_reduce(
vec![1u64, 2, 3],
ParallelOptions::default()
.with_item_timeout(Duration::from_millis(50))
.with_total_timeout(Duration::from_secs(5))
.with_cancellation(token),
|_index, n| async move { Ok::<_, TinyAgentsError>(n * 10) },
)
.await?;
let values: Vec<u64> = out.into_successes();A total timeout yields TinyAgentsError::Timeout; a cancelled token yields
TinyAgentsError::Cancelled.
The orchestrate_list tool filters spawned tasks by kind and a
creation-time window. (See crates/tinyagents-graph/src/orchestration/test.rs.)
use serde_json::json;
use tinyagents_graph::{OrchestrationTool, OrchestrationToolKind};
use tinyagents_harness::tool::Tool;
use tinyinference::tool::ToolCall;
let list = OrchestrationTool::new(OrchestrationToolKind::List, store);
let result = list
.call(&(), ToolCall::new("l1", "orchestrate_list",
json!({ "kind": "sub_agent", "created_after_ms": 0 })))
.await?;
// result.raw is a JSON array of matching task snapshots.Snapshot the registry (components plus aliases) for audit or UI display, and
run integrity diagnostics. (See
crates/tinyagents-registry/src/capability/test.rs,
snapshot_enumerates_aliases, diagnostics_flag_name_reused_across_kinds.)
use tinyagents_registry::ComponentKind;
let snapshot = registry.snapshot();
for a in &snapshot.aliases {
println!("{:?} {} -> {}", a.kind, a.alias, a.canonical);
}
let components = snapshot.by_kind(ComponentKind::Model);
let diagnostics = registry.diagnostics(); // e.g. a name reused across kindsShared contract suites prove every TaskStore/Checkpointer backend behaves
interchangeably, including concurrent access and replay after a restart.
(See crates/tinyagents-integration-tests/tests/conformance.rs.)
use tinyagents_graph::{InMemoryTaskStore, JsonlTaskStore};
use tinyagents_graph::testkit::conformance::{
taskstore_concurrent_contract, taskstore_replay_contract,
};
taskstore_concurrent_contract(std::sync::Arc::new(InMemoryTaskStore::new()));
taskstore_replay_contract(|| JsonlTaskStore::open(&path).unwrap());Each helper panics on a contract violation, so a passing call is the assertion.
- Harness — the agent loop, sub-agents, steering, and the surfaces these examples drive.
- Graph Runtime — subgraphs, reducers, and checkpointing.
-
Providers — configuring
OpenAiModeland compatible hosts.
Provider-neutral agent harness and durable state-graph runtime for Rust.
Getting started
Concepts
Modules
Providers
Contributing