Skip to content

Examples

Steven Enamakel edited this page Aug 31, 2026 · 9 revisions

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_chat

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


basic_graph (offline)

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.

durable_graph (offline)

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.

resilient_graph (offline)

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.

complex_graph (offline)

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.

agent_loop_tools (offline)

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.

openai_chat (needs OPENAI_API_KEY)

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.

openai_tools (needs OPENAI_API_KEY)

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.

openai_structured (needs OPENAI_API_KEY)

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.

openai_graph_agent (needs OPENAI_API_KEY)

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.

local_model_probe (needs a local OpenAI-compatible server)

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_probe

Useful for checking a local runtime (Ollama, LM Studio, llama.cpp server) against the same wire-format assumptions the OpenAI provider makes.

orchestrator_subagents (needs OPENAI_API_KEY)

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:

  1. Register the named capabilities.
  2. Discover the available names and descriptions back out of the registry — nothing is hard-coded into the planner.
  3. Design — an orchestrator agent, given the task plus the discovered menu, decides via structured output which sub-agents to invoke.
  4. 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.

rag_blueprint (offline)

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_self_blueprint (needs OPENAI_API_KEY)

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.

subconscious_loop (offline)

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.

goals_and_todos (offline)

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.


Selected API snippets

Small, copy-pasteable snippets for less obvious harness/graph/registry behavior. Each cites the test it mirrors.

Allow a retired model to resolve

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()
});

Recover from unknown tool calls

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

Workspace isolation and path enforcement

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.

Enforce tool policy

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.

Inherit and narrow tool exposure

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.

Middleware control precedence

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.

Budget with reservation and cached-input cap

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.

Stable event ids across restarts

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 restart

Ids are "<stream_id>-evt-<offset>"; default sinks get process-unique prefixes instead.

Parallel map/reduce with timeouts and cancellation

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.

List orchestration tasks by kind and creation window

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.

Registry introspection

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 kinds

Storage conformance helpers

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


Where to go next

  • Harness — the agent loop, sub-agents, steering, and the surfaces these examples drive.
  • Graph Runtime — subgraphs, reducers, and checkpointing.
  • Providers — configuring OpenAiModel and compatible hosts.

TinyAgents

Provider-neutral agent harness and durable state-graph runtime for Rust.

Getting started

Concepts

Modules

Providers

Contributing


Clone this wiki locally