diff --git a/AGENTS.md b/AGENTS.md
index 670cf7653..2573ca737 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -4,11 +4,10 @@
TinyAgents is a Rust 2024 virtual workspace rooted at `Cargo.toml`. It has no
compatibility facade crate: consumers depend directly on the focused package
-that owns an API. The public packages are `crates/tinyagents-graph/` (durable
-typed state graphs), `crates/tinyagents-harness/` (provider-neutral model
+that owns an API. The public packages are `crates/tinyagents-harness/` (provider-neutral model
calls, tools, middleware, and streaming), `crates/tinyagents-registry/` (the named
capability catalog), `crates/tinyagents-session/` (durable session data),
-`crates/tinyagents-tasks/` (graph-independent detached-task registry,
+`crates/tinyagents-tasks/` (detached-task registry,
task stores and orchestration controls), and
`crates/tinyagents-orchestration/` (direct and durable subagent invocation,
tools, sessions, lifecycle logic, and focused tests).
@@ -29,24 +28,22 @@ smallest useful API.
Cargo features are package-local. `tinyagents-harness` exposes `sqlite`,
`multimodal`, `png-optimize`, `media`, `claude-code`, `langfuse`,
`storage-drivers`, and `tracing`; `tinyagents-harness-tools` exposes default
-`todos`, `goals`, and `skills`, plus opt-in `subagents`; `tinyagents-graph`
-exposes `sqlite`, `storage-drivers`, and `tracing`; session exposes
+`todos`, `goals`, and `skills`, plus opt-in `subagents`; session exposes
`storage-drivers` and `tracing`; registry exposes `tracing`. The
`storage-drivers` features put each store on `tinystoragedrivers` ports. The harness's own
`tracing` instrumentation is always compiled in; the `tracing` features forward
to `tinytools-agent/tracing`, which enables that crate's `debug!`/`warn!`
diagnostics (native-call parsing, malformed arguments).
-Integration tests are in `crates/tinyagents-integration-tests/tests/`, covering serialization, graph routing,
+Integration tests are in `crates/tinyagents-integration-tests/tests/`, covering serialization,
registry binding, streaming,
and provider contracts (including live, network-gated tests such as
`tests/live_*.rs`). Subagent-specific E2E and live tests live with their owner
in `crates/tinyagents-orchestration/tests/`. Runnable usage examples are in
-`crates/tinyagents-integration-tests/examples/`, especially `basic_graph.rs`.
+`crates/tinyagents-integration-tests/examples/`, especially `openai_chat.rs`.
Design notes and module-level specifications live
in `docs/`, with `docs/spec/README.md` as the top-level architecture
-reference and `docs/modules/` holding per-surface design docs (`graph/`,
-`harness/`, `registry/`). A `wiki/`
+reference and `docs/modules/` holding per-surface design docs (`harness/` `registry/`). A `wiki/`
git submodule holds the published GitHub wiki pages; do not edit it as part
of unrelated work, and commit its pointer update separately when it does
change.
@@ -59,7 +56,7 @@ change.
tests, and examples, treating warnings as failures.
- `cargo build --workspace --all-targets`: compile all crate targets.
- `cargo test --workspace`: run the full test suite.
-- `cargo run -p tinyagents-integration-tests --example basic_graph`: run the bundled graph execution example.
+- `cargo run -p tinyagents-integration-tests --example openai_chat`: run the bundled graph execution example.
Run commands from the repository root unless a future workspace layout changes
the crate location.
@@ -77,7 +74,7 @@ have a predictable surface.
Place integration tests in `crates/tinyagents-integration-tests/tests/` and use descriptive test names such as
`serializes_chat_messages`. Add focused tests when changing serialization,
-graph routing, tool invocation, or public model request/response shapes. For
+tool invocation, or public model request/response shapes. For
async behavior, use the existing `tokio` dev dependency rather than introducing
another runtime.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 969e94347..c601df038 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,18 @@
# Changelog
+## 3.0.0 — 2026-10-22
+
+**Breaking.** This release completes the TinyAgents slim-down: generic tool
+types and tool-call parsing live in TinyTools, while host-specific execution
+policy and adapters remain with their hosts. Public crates and modules removed
+during the 2.x migration are no longer part of the TinyAgents workspace.
+
+- Bump the surviving TinyAgents workspace crates to 3.0.0.
+- Update the nested TinyTools pin to the 0.6-compatible epic revision.
+
+The unreleased entries below describe the changes accumulated before this
+major release.
+
## Unreleased: `AgentProgress::ContextCompacting`
**Breaking.** Added `AgentProgress::ContextCompacting` to the public
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 2d26d3586..fb463eb4c 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -17,12 +17,12 @@ cargo test --workspace
The bundled example should also run:
```sh
-cargo run -p tinyagents-integration-tests --example basic_graph
+cargo run -p tinyagents-integration-tests --example openai_chat
```
Cargo features are package-local. `tinyagents-harness` exposes `sqlite`,
-`tools`, `multimodal`, and `tracing`; `tinyagents-graph` exposes `sqlite` and
-`tracing`; `tinyagents-registry` and `tinyagents-session` expose `tracing`.
+`tools`, `multimodal`, and `tracing`; `tinyagents-registry` and
+`tinyagents-session` expose `tracing`.
To build with a feature enabled, pass it on the relevant package:
```sh
@@ -89,7 +89,7 @@ Use concise imperative commit subjects, for example:
```text
Add graph route validation tests
-Document graph runtime safety boundary
+Document harness safety boundary
```
Avoid mixing formatting, refactors, and behavior changes unless they are
diff --git a/Cargo.lock b/Cargo.lock
index b1aad213a..3313c5266 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -156,7 +156,7 @@ dependencies = [
"maybe-owned",
"rustix",
"rustix-linux-procfs",
- "windows-sys 0.61.2",
+ "windows-sys 0.60.2",
"winx",
]
@@ -351,7 +351,7 @@ dependencies = [
"libc",
"option-ext",
"redox_users",
- "windows-sys 0.61.2",
+ "windows-sys 0.59.0",
]
[[package]]
@@ -384,7 +384,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
- "windows-sys 0.61.2",
+ "windows-sys 0.59.0",
]
[[package]]
@@ -1327,7 +1327,7 @@ dependencies = [
"once_cell",
"socket2",
"tracing",
- "windows-sys 0.61.2",
+ "windows-sys 0.59.0",
]
[[package]]
@@ -1567,7 +1567,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys",
- "windows-sys 0.61.2",
+ "windows-sys 0.59.0",
]
[[package]]
@@ -1796,7 +1796,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
dependencies = [
"libc",
- "windows-sys 0.61.2",
+ "windows-sys 0.60.2",
]
[[package]]
@@ -1892,7 +1892,7 @@ dependencies = [
"getrandom 0.4.3",
"once_cell",
"rustix",
- "windows-sys 0.61.2",
+ "windows-sys 0.59.0",
]
[[package]]
@@ -1917,7 +1917,7 @@ dependencies = [
[[package]]
name = "tinyagents-definition"
-version = "2.11.0"
+version = "3.0.0"
dependencies = [
"anyhow",
"async-trait",
@@ -1930,27 +1930,9 @@ dependencies = [
"tracing",
]
-[[package]]
-name = "tinyagents-graph"
-version = "2.11.0"
-dependencies = [
- "async-trait",
- "futures",
- "serde",
- "serde_json",
- "tinyagents-harness",
- "tinyagents-storage-kit",
- "tinyagents-tasks",
- "tinyinference-llm",
- "tinystoragedrivers-testkit",
- "tinytools",
- "tokio",
- "tracing",
-]
-
[[package]]
name = "tinyagents-harness"
-version = "2.11.0"
+version = "3.0.0"
dependencies = [
"anyhow",
"async-trait",
@@ -1985,8 +1967,8 @@ dependencies = [
"tinystoragedrivers-types",
"tinytools",
"tinytools-agent",
- "tinytools-std",
"tokio",
+ "toml 0.8.23",
"tracing",
"uuid",
"wait-timeout",
@@ -1995,14 +1977,25 @@ dependencies = [
[[package]]
name = "tinyagents-harness-tools"
-version = "2.11.0"
+version = "3.0.0"
dependencies = [
"anyhow",
"async-trait",
+ "base64 0.23.1",
"chrono",
"chrono-tz",
+ "fs2",
+ "futures-util",
+ "glob",
+ "libc",
+ "log",
+ "parking_lot",
+ "regex",
+ "reqwest",
+ "rustix",
"serde",
"serde_json",
+ "sha2 0.11.1",
"tempfile",
"tinyagents-harness",
"tinyagents-orchestration",
@@ -2011,11 +2004,13 @@ dependencies = [
"tinytools",
"tokio",
"tracing",
+ "uuid",
+ "walkdir",
]
[[package]]
name = "tinyagents-hooks"
-version = "2.11.0"
+version = "3.0.0"
dependencies = [
"anyhow",
"async-trait",
@@ -2034,7 +2029,7 @@ dependencies = [
[[package]]
name = "tinyagents-integration-tests"
-version = "2.11.0"
+version = "3.0.0"
dependencies = [
"anyhow",
"async-trait",
@@ -2045,7 +2040,6 @@ dependencies = [
"serde_json",
"tempfile",
"tinyagents-definition",
- "tinyagents-graph",
"tinyagents-harness",
"tinyagents-harness-tools",
"tinyagents-orchestration",
@@ -2066,7 +2060,7 @@ dependencies = [
[[package]]
name = "tinyagents-live"
-version = "2.11.0"
+version = "3.0.0"
dependencies = [
"anyhow",
"async-trait",
@@ -2079,9 +2073,8 @@ dependencies = [
[[package]]
name = "tinyagents-orchestration"
-version = "2.11.0"
+version = "3.0.0"
dependencies = [
- "anyhow",
"async-trait",
"chrono",
"dotenvy",
@@ -2093,13 +2086,15 @@ dependencies = [
"tempfile",
"thiserror",
"tinyagents-definition",
- "tinyagents-graph",
"tinyagents-harness",
"tinyagents-registry",
+ "tinyagents-runtime",
"tinyagents-session",
"tinyagents-storage-kit",
"tinyagents-tasks",
"tinyinference-llm",
+ "tinystoragedrivers-connect",
+ "tinystoragedrivers-document",
"tinystoragedrivers-testkit",
"tinytools",
"tokio",
@@ -2109,7 +2104,7 @@ dependencies = [
[[package]]
name = "tinyagents-registry"
-version = "2.11.0"
+version = "3.0.0"
dependencies = [
"anyhow",
"async-trait",
@@ -2126,12 +2121,13 @@ dependencies = [
[[package]]
name = "tinyagents-runtime"
-version = "2.11.0"
+version = "3.0.0"
dependencies = [
"anyhow",
"async-trait",
"chrono",
"serde_json",
+ "sha2 0.11.1",
"tempfile",
"thiserror",
"tinyagents-harness",
@@ -2140,7 +2136,10 @@ dependencies = [
"tinyagents-session",
"tinyagents-storage-kit",
"tinyinference-llm",
+ "tinystoragedrivers",
"tinystoragedrivers-api",
+ "tinystoragedrivers-connect",
+ "tinystoragedrivers-document",
"tinystoragedrivers-testkit",
"tinystoragedrivers-types",
"tinytools",
@@ -2150,7 +2149,7 @@ dependencies = [
[[package]]
name = "tinyagents-session"
-version = "2.11.0"
+version = "3.0.0"
dependencies = [
"anyhow",
"async-trait",
@@ -2166,6 +2165,8 @@ dependencies = [
"tinyagents-storage-kit",
"tinyagents-tasks",
"tinystoragedrivers-api",
+ "tinystoragedrivers-connect",
+ "tinystoragedrivers-document",
"tinystoragedrivers-testkit",
"tinystoragedrivers-types",
"tinytools-agent",
@@ -2176,7 +2177,7 @@ dependencies = [
[[package]]
name = "tinyagents-storage-kit"
-version = "2.11.0"
+version = "3.0.0"
dependencies = [
"data-encoding",
"serde",
@@ -2192,7 +2193,7 @@ dependencies = [
[[package]]
name = "tinyagents-tasks"
-version = "2.11.0"
+version = "3.0.0"
dependencies = [
"serde",
"serde_json",
@@ -2205,6 +2206,7 @@ dependencies = [
"tinystoragedrivers-testkit",
"tokio",
"tracing",
+ "uuid",
]
[[package]]
@@ -2342,6 +2344,21 @@ dependencies = [
"url",
]
+[[package]]
+name = "tinystoragedrivers"
+version = "2.1.0"
+source = "git+https://github.com/tinyhumansai/tinystoragedrivers?rev=05eaada962decbab33bafa4763d2bd1a67f6b456#05eaada962decbab33bafa4763d2bd1a67f6b456"
+dependencies = [
+ "tinystoragedrivers-api",
+ "tinystoragedrivers-blob",
+ "tinystoragedrivers-connect",
+ "tinystoragedrivers-document",
+ "tinystoragedrivers-kv",
+ "tinystoragedrivers-queue",
+ "tinystoragedrivers-secrets",
+ "tinystoragedrivers-types",
+]
+
[[package]]
name = "tinystoragedrivers-api"
version = "2.1.0"
@@ -2366,7 +2383,9 @@ dependencies = [
"futures-core",
"serde",
"serde_json",
+ "sha2 0.10.9",
"tinystoragedrivers-api",
+ "tinystoragedrivers-connect",
"tinystoragedrivers-types",
]
@@ -2439,6 +2458,7 @@ dependencies = [
"serde",
"serde_json",
"tinystoragedrivers-api",
+ "tinystoragedrivers-connect",
"tinystoragedrivers-types",
]
@@ -2531,30 +2551,6 @@ dependencies = [
"tracing",
]
-[[package]]
-name = "tinytools-std"
-version = "0.6.0"
-dependencies = [
- "anyhow",
- "async-trait",
- "base64 0.23.1",
- "fs2",
- "futures-util",
- "glob",
- "libc",
- "log",
- "parking_lot",
- "regex",
- "reqwest",
- "rustix",
- "serde_json",
- "sha2 0.11.1",
- "tinytools",
- "tokio",
- "tracing",
- "walkdir",
-]
-
[[package]]
name = "tinyvec"
version = "1.13.3"
@@ -3096,7 +3092,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
- "windows-sys 0.61.2",
+ "windows-sys 0.59.0",
]
[[package]]
diff --git a/Cargo.toml b/Cargo.toml
index cbd165d83..7e0045104 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -3,7 +3,6 @@ members = ["crates/*"]
default-members = [
"crates/tinyagents-harness",
"crates/tinyagents-harness-tools",
- "crates/tinyagents-graph",
"crates/tinyagents-tasks",
"crates/tinyagents-registry",
"crates/tinyagents-session",
@@ -16,7 +15,7 @@ exclude = ["vendor", "worktrees", "integration-proofs/run-ledger-mongodb"]
resolver = "3"
[workspace.package]
-version = "2.11.0"
+version = "3.0.0"
edition = "2024"
license = "GPL-3.0-only"
repository = "https://github.com/tinyhumansai/tinyagents"
@@ -41,6 +40,7 @@ tracing = "0.1"
tinystoragedrivers-types = { version = "2.1.0", git = "https://github.com/tinyhumansai/tinystoragedrivers", rev = "05eaada962decbab33bafa4763d2bd1a67f6b456" }
tinystoragedrivers-api = { version = "2.1.0", git = "https://github.com/tinyhumansai/tinystoragedrivers", rev = "05eaada962decbab33bafa4763d2bd1a67f6b456" }
tinystoragedrivers-testkit = { version = "2.1.0", git = "https://github.com/tinyhumansai/tinystoragedrivers", rev = "05eaada962decbab33bafa4763d2bd1a67f6b456" }
+tinystoragedrivers = { version = "2.1.0", git = "https://github.com/tinyhumansai/tinystoragedrivers", rev = "05eaada962decbab33bafa4763d2bd1a67f6b456", default-features = false, features = ["sqlite"] }
tinystoragedrivers-document = { version = "2.1.0", git = "https://github.com/tinyhumansai/tinystoragedrivers", rev = "05eaada962decbab33bafa4763d2bd1a67f6b456", features = ["sqlite", "file"] }
tinystoragedrivers-connect = { version = "2.1.0", git = "https://github.com/tinyhumansai/tinystoragedrivers", rev = "05eaada962decbab33bafa4763d2bd1a67f6b456", features = ["sqlite"] }
tinyagents-storage-kit = { path = "crates/tinyagents-storage-kit" }
diff --git a/README.md b/README.md
index 751c12b45..331e9d244 100644
--- a/README.md
+++ b/README.md
@@ -9,13 +9,10 @@
-TinyAgents is a small, provider-neutral agent harness for Rust, plus a durable
-typed state-graph runtime. It takes its shape from
-[LangChain](https://www.langchain.com/) (models, tools, middleware, structured
-output, streaming, usage/cost) and
-[LangGraph](https://www.langchain.com/langgraph) (`START`/`END`, nodes,
-conditional edges, channels/reducers, checkpoints, interrupts, subgraphs, time
-travel) — rebuilt as ordinary, typed Rust with no hidden magic.
+TinyAgents is a small, provider-neutral agent harness for Rust. It takes its
+shape from [LangChain](https://www.langchain.com/) (models, tools, middleware,
+structured output, streaming, usage/cost) — rebuilt as ordinary, typed Rust
+with no hidden magic.
It is for Rust services that need to call models and tools in a loop, want
that loop to be resumable and inspectable, and would rather not carry a
@@ -30,24 +27,19 @@ TinyAgents is a Cargo workspace, not one crate. Depend on the pieces you need:
caching, and a Claude Code CLI model adapter with stream-json,
session, authentication, and MCP endpoint support. Features: `sqlite`,
`storage-drivers`, `multimodal`, `png-optimize` (opt-in PNG re-compression), `media`,
- `claude-code` and `langfuse` (both default), `tracing` (forwards to `tinytools-agent/tracing` for its parsing diagnostics; the harness's own instrumentation is always compiled in).
+ `claude-code` and `langfuse` (both default), `json-schema` (opt-in `JsonSchema` for context config types), and `tracing` (forwards to `tinytools-agent/tracing` for its parsing diagnostics; the harness's own instrumentation is always compiled in).
- **`tinyagents-harness-tools`** — reusable implementations for the harness:
goals and budgets, per-thread todos, skill tool packs, subagent control
tools, time and clarification tools, plan exit, per-call binding, and
per-thread locks. Features: `todos`, `goals`, `skills` (all default), and
`subagents` (opt-in). See the [crate guide](crates/tinyagents-harness-tools/README.md).
-- **`tinyagents-graph`** — a LangGraph-style durable, typed state graph:
- `START`/`END`, nodes, conditional edges, `Send` fanout, reducers/channels,
- checkpoints, interrupts, subgraphs, and time travel. Features: `sqlite`,
- `tracing`.
-- **`tinyagents-tasks`** — graph-independent detached-task machinery: task
+- **`tinyagents-tasks`** — detached-task machinery: task
model, `TaskStore` (in-memory and JSONL), `DetachedTaskRegistry`, restart
reconciliation, the durable completion router (push a finished child to its
parent, redelivered after a restart), and the `spawn`/`await`/`cancel`/`steer`
orchestration tools.
- Re-exported from `tinyagents_graph::orchestration` for compatibility.
- **`tinyagents-registry`** — a named capability catalog (models, tools,
- agents, graphs, and routers), plus an offline model price/capability catalog.
+ agents, and routers), plus an offline model price/capability catalog.
- **`tinyagents-session`** — a SQLite-backed store for session history,
messages, tool calls, cost, and run lineage, plus JSONL transcripts and the
`threads` chat thread/message store. Transcript spend separates charged and
@@ -79,7 +71,6 @@ None of the crates are published to crates.io (`publish = false` in every
```toml
[dependencies]
tinyagents-harness = { git = "https://github.com/tinyhumansai/tinyagents", package = "tinyagents-harness" }
-tinyagents-graph = { git = "https://github.com/tinyhumansai/tinyagents", package = "tinyagents-graph" }
tinyagents-registry = { git = "https://github.com/tinyhumansai/tinyagents", package = "tinyagents-registry" }
# The code samples below build `Message` and provider types directly from
# TinyInference, the message/model crate TinyAgents is built on. It is a
@@ -87,49 +78,6 @@ tinyagents-registry = { git = "https://github.com/tinyhumansai/tinyagents", pack
tinyinference-llm = { git = "https://github.com/tinyhumansai/tinyinference", package = "tinyinference-llm" }
```
-A minimal typed graph — a whole-state agent/tool loop (trimmed from
-[`examples/basic_graph.rs`](crates/tinyagents-integration-tests/examples/basic_graph.rs)):
-
-```rust
-use tinyagents_graph::*;
-use tinyinference_llm::message::Message;
-
-#[derive(Clone, Debug)]
-struct AgentState {
- messages: Vec,
- needs_tool: bool,
-}
-
-let graph = GraphBuilder::::overwrite()
- .add_node("agent", |mut state: AgentState, _ctx: NodeContext| async move {
- state.messages.push(Message::assistant("checking the local tool"));
- Ok(NodeResult::Update(state))
- })
- .add_node("tool", |mut state: AgentState, _ctx: NodeContext| async move {
- state.messages.push(Message::tool("echo", "tool result"));
- state.needs_tool = false;
- Ok(NodeResult::Update(state))
- })
- .set_entry("agent")
- .add_conditional_edges(
- "agent",
- |state: &AgentState| if state.needs_tool { "tool".to_string() } else { "done".to_string() },
- [("tool", "tool"), ("done", END)],
- )
- .add_edge("tool", "agent")
- .compile()?;
-
-let run = graph.run(AgentState { messages: vec![], needs_tool: true }).await?;
-```
-
-Run it for real:
-
-```sh
-git clone git@github.com:tinyhumansai/tinyagents.git
-cd tinyagents
-cargo run -p tinyagents-integration-tests --example basic_graph
-```
-
A one-shot model call through the harness (`export OPENAI_API_KEY=...` then
`cargo run -p tinyagents-integration-tests --example openai_chat`):
@@ -141,7 +89,10 @@ use tinyinference_llm::providers::openai::OpenAiModel;
let model = OpenAiModel::from_env()?;
let mut harness: AgentHarness<()> = AgentHarness::new();
-harness.register_model("openai", Arc::new(model)).set_default_model("openai");
+harness.with_model_resolver(Arc::new(tinyagents_harness::host::FixedModelResolver::named(
+ "openai",
+ Arc::new(model),
+)));
let run = harness
.invoke_default(&(), vec![Message::user("What is a Rust trait?")])
@@ -149,16 +100,6 @@ let run = harness
println!("{}", run.text().unwrap_or_default());
```
-## Graph runtime
-
-`tinyagents-graph` is a durable, typed state graph modeled on LangGraph:
-`START`/`END` markers, nodes, static and conditional edges, `Command`-based
-routing, `Send` fanout, reducers over named channels, checkpointing (with an
-optional `sqlite` backend), interrupts, streaming events, topology export, and
-replay/time travel across superstep boundaries. A node can embed another
-compiled graph as a subgraph, so a whole workflow can appear as a single step
-inside a larger one.
-
## Harness
`tinyagents-harness` runs the model/tool agent loop: provider-neutral model
@@ -180,14 +121,12 @@ changing the outer transcript or treating a preview as a write.
`tinyagents_orchestration::subagent::SubAgentJobRegistry`. The `subagents`
feature in `tinyagents-harness-tools`
exposes `tinyagents_harness_tools::subagents`, which provides
-`DriverSubAgentTool`, the host-planned typed dispatcher: its `background` mode
-starts a child and returns a stable job id, while `inline` mode waits for the
-final result. That module also provides the `SubAgentJobsTool` and
-`SubAgentMessageTool` declarations over the shared registry. `SubAgentSession`
-covers retained post-completion
-conversations, while `SubagentDriver` coordinates durable lifecycle
-preparation, execution, pause, resume, and persistence. Teams and workflow DAGs
-are intentionally outside this focused crate.
+`DriverSubAgentTool`, the host-planned typed dispatcher. Every call starts an
+asynchronous job and returns its stable job id; `SubAgentJobsTool` provides
+status, wait, cancellation, and steering over the shared registry, while
+`SubAgentMessageTool` delivers messages. `SubagentDriver` coordinates durable
+lifecycle preparation, execution, pause, resume, and persistence. Teams and
+workflow DAGs are intentionally outside this focused crate.
## Session runtime
@@ -235,8 +174,8 @@ All live in
**`openai_graph_agent`** — provider-backed chat, tool calling, structured
output, and a graph-driven agent (all need `OPENAI_API_KEY`).
- **`subconscious_loop`** — an offline, testable autonomous closed-loop
- harness (see its own
- [README](crates/tinyagents-integration-tests/examples/subconscious_loop/README.md)).
+ harness (see the source and test in
+ `crates/tinyagents-integration-tests/examples/subconscious_loop/`).
## Documentation
diff --git a/ROADMAP.md b/ROADMAP.md
index 669675a9a..b14912539 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -8,15 +8,11 @@ build toward a production-grade Rust agent runtime.
- typed harness model calls, tools, middleware, structured output, streaming,
usage/cost tracking, retry/limits, cache, memory/embeddings, sub-agents,
and steering (`harness/`)
-- durable typed state graph runtime: `START`/`END`, nodes, conditional
- routing, `Command`s, fan-out, reducers/channels, checkpoints, interrupts,
- subgraphs, streaming, and topology export (`graph/`)
- per-thread `ThreadGoal` and `TodoList` productivity primitives, exposed as
harness tools
-- named capability registry (models, tools, agents, graphs, stores,
+- named capability registry (models, tools, agents, stores,
middleware, policy) bound by name (`registry/`)
-- an optional SQLite-backed checkpointer (`sqlite` feature)
-- an embedded Langfuse client and graph exporter for observability
+- an embedded Langfuse client for observability
## Near-Term Work
@@ -31,14 +27,11 @@ build toward a production-grade Rust agent runtime.
ranks the correctness fixes and feature gaps found by comparing TinyAgents
with LangGraph, Pydantic AI and pi
-## Parallel Agents And Sub-Agents
+## Sub-Agents
-Shipped: forked child contexts, shared caches with explicit isolation policy,
-child event namespaces, parent/child run ids, deterministic reducer-based
-merges, optional/blocking/race/quorum/fallback/compare policies, and
-resumable checkpoints across parallel branches. Ongoing work focuses on
-hardening edge cases surfaced by the `e2e_parallel_*` and `live_subagent_*`
-test suites.
+Shipped: child contexts, shared caches with explicit isolation policy,
+child event namespaces, parent/child run ids, policy-controlled detached
+work, and durable child task lifecycle through `tinyagents-orchestration`.
## Stability
diff --git a/crates/tinyagents-definition/src/execution_spec.rs b/crates/tinyagents-definition/src/execution_spec.rs
index 4ddd2ba65..0fb0a0327 100644
--- a/crates/tinyagents-definition/src/execution_spec.rs
+++ b/crates/tinyagents-definition/src/execution_spec.rs
@@ -74,12 +74,7 @@ pub enum SandboxMode {
/// The tools through which an agent spawns sub-agents of its own choosing.
/// A named scope that lists none of them delegates only to what its host
/// declared, never to an inherited roster.
-pub const SPAWN_TOOL_NAMES: &[&str] = &[
- "spawn_subagent",
- "spawn_async_subagent",
- "spawn_parallel_agents",
- "spawn_worker_thread",
-];
+pub const SPAWN_TOOL_NAMES: &[&str] = &["spawn_subagent"];
impl ToolScope {
/// Whether this is a named scope that lists one of [`SPAWN_TOOL_NAMES`].
diff --git a/crates/tinyagents-definition/src/execution_spec_tests.rs b/crates/tinyagents-definition/src/execution_spec_tests.rs
index 54a4c8ad8..0dae6e2ee 100644
--- a/crates/tinyagents-definition/src/execution_spec_tests.rs
+++ b/crates/tinyagents-definition/src/execution_spec_tests.rs
@@ -4,6 +4,15 @@ fn named(names: &[&str]) -> ToolScope {
ToolScope::Named(names.iter().map(|n| n.to_string()).collect())
}
+#[test]
+fn spawn_tool_roster_contains_only_the_async_subagent_tool() {
+ assert_eq!(SPAWN_TOOL_NAMES, &["spawn_subagent"]);
+ assert!(named(&["spawn_subagent"]).names_a_spawn_tool());
+ assert!(!named(&["spawn_async_subagent"]).names_a_spawn_tool());
+ assert!(!named(&["spawn_parallel_agents"]).names_a_spawn_tool());
+ assert!(!named(&["spawn_worker_thread"]).names_a_spawn_tool());
+}
+
#[test]
fn a_named_scope_listing_a_spawn_tool_is_detected() {
for spawn in SPAWN_TOOL_NAMES {
diff --git a/crates/tinyagents-definition/tests/fixtures/openhuman/orchestrator.toml b/crates/tinyagents-definition/tests/fixtures/openhuman/orchestrator.toml
index ca2f8dc0f..403fb345c 100644
--- a/crates/tinyagents-definition/tests/fixtures/openhuman/orchestrator.toml
+++ b/crates/tinyagents-definition/tests/fixtures/openhuman/orchestrator.toml
@@ -243,7 +243,7 @@ named = [
# listing it here (and teaching it in prompt.md) only trained the model
# to call a tool that does not exist.
# Sub-agent surface, deliberately three tools (#5701). A sub-agent is
- # always async: `spawn_async_subagent` returns a task id immediately and
+ # always async: `spawn_subagent` returns a task id immediately and
# the runner delivers the result back on an idle system turn via
# `orchestration::background_delivery`. Nothing has to block for it, so
# the collection surface that used to sit here — `wait`, `wait_loop`,
@@ -254,7 +254,7 @@ named = [
# were fleet-management affordances for a fleet the model no longer
# enumerates; a worker that needs different instructions is cheaper to
# respawn, and idle sessions are reaped against the registry cap anyway.
- "spawn_async_subagent",
+ "spawn_subagent",
# Fleet awareness. Re-enumerates the workers this parent has in flight
# (running / awaiting_user / completed-uncollected / failed) plus reusable
# durable sessions — the recovery move when the `[async_subagent_ref]`
diff --git a/crates/tinyagents-graph/Cargo.toml b/crates/tinyagents-graph/Cargo.toml
deleted file mode 100644
index 9a43cd133..000000000
--- a/crates/tinyagents-graph/Cargo.toml
+++ /dev/null
@@ -1,45 +0,0 @@
-[package]
-publish = false
-name = "tinyagents-graph"
-version.workspace = true
-edition.workspace = true
-license.workspace = true
-repository.workspace = true
-rust-version.workspace = true
-description = "Durable typed state graphs for TinyAgents."
-
-[dependencies]
-async-trait = { workspace = true }
-futures = { workspace = true }
-serde = { workspace = true }
-serde_json = { workspace = true }
-tinyagents-tasks = { path = "../tinyagents-tasks", version = "2.11.0", default-features = false }
-tinyagents-harness = { path = "../tinyagents-harness", version = "2.11.0", default-features = false, features = [
- "langfuse",
-] }
-tinyinference-llm = { path = "../../vendor/tinyinference/crates/tinyinference-llm", version = "0.5.0" }
-tinytools = { path = "../../vendor/tinytools/crates/tinytools", version = "0.6.0" }
-tokio = { workspace = true, features = ["sync", "time", "macros", "rt", "rt-multi-thread", "fs"] }
-tracing = { workspace = true }
-# Storage ports (no database client) for `DriverCheckpointer`.
-tinyagents-storage-kit = { workspace = true }
-
-[features]
-default = []
-# Retained as a no-op: SQLite persistence moved to the tinystoragedrivers
-# SQLite driver, which a host passes to `DriverCheckpointer` / `DriverTaskCache`.
-sqlite = []
-# `DriverCheckpointer`: checkpoints, pending writes and leases on any
-# tinystoragedrivers backend the host opened.
-storage-drivers = []
-# Tracing instrumentation is now always compiled in (via the `tracing` crate
-# dependency above). This feature is retained as a no-op so downstream
-# feature forwards keep compiling.
-tracing = ["tinyagents-harness/tracing"]
-
-[dev-dependencies]
-tinystoragedrivers-testkit = { workspace = true }
-tokio = { workspace = true, default-features = true, features = ["macros", "rt-multi-thread", "time", "test-util"] }
-
-[lints]
-workspace = true
diff --git a/crates/tinyagents-graph/src/README.md b/crates/tinyagents-graph/src/README.md
deleted file mode 100644
index 82106d49a..000000000
--- a/crates/tinyagents-graph/src/README.md
+++ /dev/null
@@ -1,81 +0,0 @@
-# graph
-
-TinyAgents' durable workflow runtime (LangGraph-style), and one of the
-load-bearing surfaces of the crate's recursive architecture.
-
-Because a node can embed another compiled graph (`subgraph`) or invoke a
-sub-agent, **graphs run graphs** and orchestration recurses while every step
-stays typed, checkpointed, and observable. Workflows driven from a host
-orchestrator use these same runtime types.
-
-Each submodule keeps type definitions in `types.rs`, behavior in `mod.rs`, and
-unit tests in a sibling `_tests.rs` (`mod_tests.rs` beside a `mod.rs`); complex submodules additionally
-carry their own `README.md` — see the module map below.
-
-## Module map
-
-| Module | Concern |
-| --- | --- |
-| `builder` | Authoring/compile contract: `GraphBuilder` accumulates nodes, edges, conditional routing, and a reducer; `.compile()` validates topology and freezes it into an immutable `CompiledGraph`. See [`builder/README.md`](builder/README.md). |
-| `channel` | Channel-per-field state model (additive) — per-field merge rules and concurrent-write conflict detection, running on the unmodified executor. See [`channel/README.md`](channel/README.md). |
-| `checkpoint` | The `Checkpointer` trait and backends (file, sqlite, in-memory) — durability that makes runs resumable and time-travelable. See [`checkpoint/README.md`](checkpoint/README.md). |
-| `command` | `Command`, `Interrupt`, `NodeResult`, `RouteTarget`, `Send` — the vocabulary a node handler returns to update state, route, interrupt, or fan out. See [`command/README.md`](command/README.md). |
-| `compiled` | The superstep executor, `CompiledGraph` — sequential/parallel steps, node retry, resumable failure, run/resume/state APIs. See [`compiled/README.md`](compiled/README.md). |
-| `dag` | Structural validation (duplicates, dangling edges, cycles) of a borrowed dependency-node view, independent of the graph runtime. See [`dag/README.md`](dag/README.md). |
-| `export` | Graph introspection/visualization: topology extraction, Mermaid/JSON export, validation reports. |
-| `goals` | A durable per-thread goal (single "completion contract"), continuation loop, and harness tools. See [`goals/README.md`](goals/README.md). |
-| `observability` | Durable graph observability: journals, status stores, the journaling sink, latency/health rollups, Langfuse export. See [`observability/README.md`](observability/README.md). |
-| `orchestration` | Detached-task runtime types backed by a `TaskStore`. The runtime lives in `tinyagents-tasks` ([README](../../tinyagents-tasks/src/README.md)); this module re-exports its current public surface for compatibility. Model-facing orchestration tool adapters were removed. |
-| `parallel` | `map_reduce` — ordered, bounded-concurrency parallel map/reduce with a configurable failure policy, independent of the graph executor. |
-| `recursion` | Recursion policy and depth tracking: `RecursionFrame`/`RecursionPolicy`/`RecursionStack`/`RunTree` bound and observe nested graph/subgraph/sub-agent recursion. See [`recursion/README.md`](recursion/README.md). |
-| `reducer` | `StateReducer`/`Reducer` implementations (overwrite, append, min/max, set-union, closures) that fold branch updates into committed state at a superstep boundary. See [`reducer/README.md`](reducer/README.md). |
-| `status` | `GraphRunStatus` — a compact run-status snapshot. See [`status/README.md`](status/README.md). |
-| `stream` | `GraphEvent`, `GraphEventSink`, and streaming modes — the live, in-process event surface `observability` makes durable. See [`stream/README.md`](stream/README.md). |
-| `subagent_node` | Embeds a harness agent as a graph node (the graph-level analogue of `subgraph`, but for agents instead of graphs). |
-| `subgraph` | Embeds a `CompiledGraph` as a node (shared-state or adapter mode) — graph-level recursion. See [`subgraph/README.md`](subgraph/README.md). |
-| `testkit` | Deterministic node doubles, event recorder, fluent run assertions, storage conformance suites. See [`testkit/README.md`](testkit/README.md). |
-| `todos` | A per-thread todo checklist and its harness tool. See [`todos/README.md`](todos/README.md). |
-
-## How the pieces fit together
-
-1. **Author**: `GraphBuilder` (in `builder`) accumulates nodes (`NodeHandler`),
- edges, conditional `Route`s, and a `StateReducer`, then `.compile()`
- validates the topology and produces an immutable `CompiledGraph` (in
- `compiled`).
-2. **Run**: `CompiledGraph::run`/`run_with_thread` drives supersteps. Each
- step's node handlers return `NodeResult`s built from `command` vocabulary
- (`Command::Update`, `Command::Goto`, `Interrupt`, `Send` for fanout); the
- step boundary folds results through the configured reducer (`reducer`, or
- `channel` for per-field semantics).
-3. **Persist**: when a `Checkpointer` (`checkpoint`) is configured, each
- boundary is checkpointed, making the run resumable, forkable, and
- time-travelable; `status::GraphRunStatus` gives a compact live snapshot.
-4. **Observe**: `stream::GraphEvent`s fan out live through a `GraphEventSink`;
- `observability` makes that history durable (journals, status stores,
- Langfuse export) and derives latency/health rollups.
-5. **Recurse**: a node can embed another compiled graph (`subgraph`) or a
- harness agent (`subagent_node`), or hand off named child work through
- `orchestration`'s tool surface; `recursion` tracks depth and the run tree
- across all three so nesting stays bounded and observable.
-6. **Test**: `testkit` provides node doubles, a recorder, fluent assertions,
- and storage conformance suites so both hand-written graphs and new storage
- backends can be verified against the same contracts.
-
-## Errors
-
-Graph-specific failures surface through the crate's shared `Result`/
-`TinyAgentsError` (see `crate::error`); the most graph-specific variants are
-`TinyAgentsError::RecursionLimit` (nesting/loop depth exceeded) and
-`TinyAgentsError::InvalidConcurrentUpdate` (a same-step concurrent write to a
-channel that does not allow it — see `channel`).
-
-## Operational constraints
-
-- Checkpoints are written **only at superstep boundaries**, never mid-node —
- a node handler must be safe to re-run from scratch after a crash.
-- Subgraph and sub-agent embedding always extends the child's checkpoint
- namespace with the embedding node id; inspecting a child's checkpoints
- directly requires accounting for that namespace suffix.
-- Parallel-step determinism depends on branches never mutating shared state
- outside their own snapshot clone; the reducer, not lock ordering, is what
- resolves conflicting concurrent writes.
diff --git a/crates/tinyagents-graph/src/agent_loop/compile.rs b/crates/tinyagents-graph/src/agent_loop/compile.rs
deleted file mode 100644
index 4c0669880..000000000
--- a/crates/tinyagents-graph/src/agent_loop/compile.rs
+++ /dev/null
@@ -1,172 +0,0 @@
-//! [`compile_loop`]: assembles the `plan -> model -> tools -> settle` graph
-//! over a [`LoopRuntime`].
-//!
-//! See the module doc on [`super`] for the loop's documented scope and the
-//! shape of [`LoopState`]/[`LoopUpdate`].
-
-use std::sync::Arc;
-
-use crate::builder::{GraphBuilder, NodeContext};
-use crate::compiled::CompiledGraph;
-use tinyagents_harness::error::Result;
-use tinyagents_harness::runtime::AgentHarness;
-
-use super::runtime::{self, LoopRuntime};
-use super::types::{LoopState, LoopUpdate, node};
-
-/// Compiles the `plan -> model -> tools -> settle` agent loop into a
-/// [`CompiledGraph`] bound to `rt`.
-///
-/// # Shape
-///
-/// [`LoopState`] doubles as its own [`LoopUpdate`] (`GraphBuilder::overwrite`,
-/// see that type's docs): every node returns the whole next state rather than
-/// a partial patch, so the reducer is a plain overwrite and there is no
-/// separate merge step to keep in sync with the state shape.
-///
-/// Every node routes with an explicit [`crate::Command::goto`]
-/// (`mark_command_routing`) rather than static/conditional edges — the
-/// `plan`/`model`/`tools` nodes can each jump to more than one destination
-/// depending on the turn's outcome (a tool-free response skips `tools`; a
-/// [`tinyagents_harness::context::MiddlewareControl::JumpTo`] can loop back
-/// to `plan` from `tools`, or straight to `settle` from `model`/`tools`), so a
-/// fixed edge table cannot express the routing — see `runtime::apply_control`
-/// for the full mapping from [`tinyagents_harness::context::MiddlewareControl`]
-/// to a `goto`. `plan`, `model`, and `tools` are also marked as interrupt
-/// points for the export (see this function's body, below): a steering
-/// pause (from `plan`) or a
-/// [`tinyagents_harness::context::MiddlewareControl::Interrupt`] (from any of
-/// the three) surfaces as a real [`crate::Interrupt`], checkpointed by the
-/// graph executor exactly like any other durable interrupt — this is how
-/// A5's "approvals surfacing as graph interrupts" requirement is met: an
-/// approval gate is just a middleware that requests
-/// `MiddlewareControl::Interrupt`, and the graph rendition pauses/resumes it
-/// through the same `CompiledGraph::resume` path a hand-authored
-/// human-in-the-loop node would use.
-///
-/// # Tool batch execution
-///
-/// The `tools` node calls
-/// [`tinyagents_harness::agent_loop::phases::execute_tool_batch`] once per
-/// activation, for the *whole* turn's tool calls in one node run rather than
-/// one graph node per tool call. This preserves the direct loop's ordering,
-/// concurrency-eligibility, and budget/limit semantics exactly (see that
-/// function's docs) without re-deriving them as a `Send`-fanout over
-/// per-call nodes, which would have to reimplement the direct loop's
-/// serial-admission / serial-or-concurrent-dispatch decision as graph
-/// topology instead of reusing it.
-///
-/// # Recursion / limits
-///
-/// [`tinyagents_harness::limits::RunLimits`] is enforced the same way the
-/// direct loop enforces it — via `ctx.record_model_call()`/tool budget
-/// checks inside the node bodies, fed from the same
-/// [`tinyagents_harness::context::RunContext::limits`] — so a caller who also
-/// wants the *graph's own* recursion guard (independent of the harness's
-/// limits) can additionally call
-/// [`crate::compiled::CompiledGraph::with_recursion_policy`]/
-/// [`crate::compiled::CompiledGraph::with_run_deadline`] on the returned
-/// graph, exactly as any other compiled graph.
-pub fn compile_loop(
- rt: Arc>,
-) -> Result>
-where
- State: Send + Sync + 'static,
- Ctx: Send + Sync + 'static,
-{
- let mut builder = GraphBuilder::::overwrite()
- .with_name("tinyagents.agent_loop")
- .add_node(node::PLAN, {
- let rt = rt.clone();
- move |loop_state: LoopState, _ctx: NodeContext| {
- let rt = rt.clone();
- async move {
- let harness: Arc> = rt.harness.clone();
- let mut ctx_guard = rt.ctx.lock().await;
- runtime::plan_node(&harness, &mut ctx_guard, loop_state).await
- }
- }
- })
- .add_node(node::MODEL, {
- let rt = rt.clone();
- move |loop_state: LoopState, _ctx: NodeContext| {
- let rt = rt.clone();
- async move {
- let harness = rt.harness.clone();
- let app_state = rt.app_state.clone();
- let mut ctx_guard = rt.ctx.lock().await;
- let mut run_guard = rt.run.lock().await;
- let mut status_guard = rt.status.lock().await;
- runtime::model_node(
- &harness,
- &app_state,
- &mut ctx_guard,
- &mut run_guard,
- &mut status_guard,
- loop_state,
- )
- .await
- }
- }
- })
- .add_node(node::TOOLS, {
- let rt = rt.clone();
- move |loop_state: LoopState, _ctx: NodeContext| {
- let rt = rt.clone();
- async move {
- let harness = rt.harness.clone();
- let app_state = rt.app_state.clone();
- let mut ctx_guard = rt.ctx.lock().await;
- let mut run_guard = rt.run.lock().await;
- let mut status_guard = rt.status.lock().await;
- runtime::tools_node(
- &harness,
- &app_state,
- &mut ctx_guard,
- &mut run_guard,
- &mut status_guard,
- loop_state,
- )
- .await
- }
- }
- })
- .add_node(node::SETTLE, {
- let rt = rt.clone();
- move |loop_state: LoopState, _ctx: NodeContext| {
- let rt = rt.clone();
- async move {
- let harness = rt.harness.clone();
- let mut ctx_guard = rt.ctx.lock().await;
- let mut run_guard = rt.run.lock().await;
- runtime::settle_node(&harness, &mut ctx_guard, &mut run_guard, loop_state).await
- }
- }
- })
- .set_entry(node::PLAN)
- .mark_command_routing(node::PLAN)
- .mark_command_routing(node::MODEL)
- .mark_command_routing(node::TOOLS)
- .mark_command_routing(node::SETTLE);
-
- // `plan`/`model`/`tools` each already return a real, node-emitted
- // `NodeResult::Interrupt` when they need to pause (a steering pause from
- // `plan`, a `MiddlewareControl::Interrupt` from any of the three — see
- // the module doc above); that alone is a genuine, checkpointed executor
- // pause, with no help from `GraphBuilder::mark_interrupt` needed.
- // `mark_interrupt` is *not* used here because — unlike when this graph
- // was first written — it is no longer a behavior-free export marker: it
- // now aliases `GraphBuilder::interrupt_before`, which would make the
- // executor pause *every* activation of these nodes on its own, before
- // the node (and its middleware) ever runs, double-pausing on top of the
- // node's own interrupt and desyncing resume's interrupt-acknowledgement
- // bookkeeping across turns. Setting the `NodeMeta` interrupt marker
- // directly (`pub(crate)`, reachable from this sibling module) restores
- // the original export-only annotation without opting into that runtime
- // pause.
- for node in [node::PLAN, node::MODEL, node::TOOLS] {
- builder.node_meta.entry(node.into()).or_default().interrupt = true;
- }
-
- builder.compile()
-}
diff --git a/crates/tinyagents-graph/src/agent_loop/driver.rs b/crates/tinyagents-graph/src/agent_loop/driver.rs
deleted file mode 100644
index ab31865ff..000000000
--- a/crates/tinyagents-graph/src/agent_loop/driver.rs
+++ /dev/null
@@ -1,343 +0,0 @@
-//! [`GraphLoopDriver`]: plugs the compiled loop's node bodies into
-//! [`AgentHarness::invoke`] (and friends) via
-//! [`tinyagents_harness::agent_loop::phases::LoopDriver`] +
-//! [`AgentHarness::with_loop_driver`], selected by
-//! [`tinyagents_harness::runtime::RunPolicy::execution`]`::Graph`.
-//!
-//! See the module doc on [`super`] ("`GraphLoopDriver` vs.
-//! `compile_loop`/`LoopIter`") for why this drives
-//! [`super::runtime::plan_node`]/`model_node`/`tools_node`/`settle_node`
-//! directly over borrowed `&mut` state in a hand-rolled loop instead of
-//! building a [`crate::CompiledGraph`].
-
-use async_trait::async_trait;
-
-use tinyagents_harness::agent_loop::phases::{self, LoopDriver};
-use tinyagents_harness::context::RunContext;
-use tinyagents_harness::error::{Result, TinyAgentsError};
-use tinyagents_harness::events::{AgentEvent, HarnessRunStatus};
-use tinyagents_harness::ids::HarnessPhase;
-use tinyagents_harness::middleware::AgentRun;
-use tinyagents_harness::runtime::AgentHarness;
-use tinyagents_harness::steering::PauseState;
-use tinyagents_harness::terminal::{TerminalOutcome, TerminalReason};
-use tinyinference_llm::message::Message;
-
-use crate::command::{NodeResult, RouteTarget};
-
-use super::runtime;
-use super::types::{LoopState, node};
-
-/// Drives [`AgentHarness::invoke`] through the same node bodies
-/// [`super::compile_loop`] wires into a [`crate::CompiledGraph`], without
-/// itself building one. Install with
-/// [`AgentHarness::with_loop_driver`]`(Arc::new(GraphLoopDriver::new()))` and
-/// [`tinyagents_harness::runtime::RunPolicy::execution`]`::Graph`.
-///
-/// Stateless — one instance can be shared (via `Arc`) across every harness
-/// that wants the graph engine.
-#[derive(Debug, Default, Clone, Copy)]
-pub struct GraphLoopDriver;
-
-impl GraphLoopDriver {
- /// Creates a driver. Stateless: nothing to configure.
- pub fn new() -> Self {
- Self
- }
-}
-
-#[async_trait]
-impl LoopDriver for GraphLoopDriver
-where
- State: Send + Sync,
- Ctx: Send + Sync,
-{
- async fn drive(
- &self,
- harness: &AgentHarness,
- state: &State,
- ctx: &mut RunContext,
- run: &mut AgentRun,
- status: &mut HarnessRunStatus,
- input: Vec,
- streaming: bool,
- ) -> Result<()> {
- // Mirrors `run_loop`'s own top-of-run bookkeeping (see that
- // function's docs on why the limit tracker restarts here rather
- // than at `RunContext::new`).
- ctx.limits.restart();
- runtime::reconcile_call_limits(ctx, harness.policy());
- ctx.streaming = streaming;
-
- let record = ctx.emit(AgentEvent::RunStarted {
- run_id: ctx.run_id().clone(),
- thread_id: ctx.thread_id().cloned(),
- });
- status.set_last_event(record.id);
- status.mark_running(HarnessPhase::Idle);
-
- harness.middleware().run_before_agent(ctx, state).await?;
-
- let mut loop_state = LoopState {
- messages: input,
- ..LoopState::default()
- };
- ctx.reset_turn_tracker(loop_state.messages.len());
- let mut current: &str = node::PLAN;
- let mut limit_stop = false;
- let mut limit_kind = None;
-
- let outcome = loop {
- // Keeps `run.messages` a running snapshot of the transcript as
- // of the start of each node call, so a node that errors or
- // interrupts mid-call (and so never hands `loop_state` back)
- // still leaves `run.messages` close to current — mirroring
- // `run_loop`'s "transcript survives every exit path" guarantee
- // as closely as this driver's by-value node contract allows.
- // The one gap: mutations a node makes to its *own* copy of
- // `loop_state.messages` before erroring/interrupting (for
- // example `plan_node`'s steering-injected message on a pause)
- // are not reflected until that node returns successfully.
- run.messages = loop_state.messages.clone();
- let result = match current {
- node::PLAN => runtime::plan_node(harness, ctx, loop_state).await,
- node::MODEL => {
- runtime::model_node(harness, state, ctx, run, status, loop_state).await
- }
- node::TOOLS => {
- runtime::tools_node(harness, state, ctx, run, status, loop_state).await
- }
- node::SETTLE => runtime::settle_node(harness, ctx, run, loop_state).await,
- other => {
- break Err(TinyAgentsError::Validation(format!(
- "GraphLoopDriver: unknown loop node `{other}`"
- )));
- }
- };
-
- match result {
- Ok(NodeResult::Interrupt(interrupt))
- if interrupt.id.ends_with("-steering-pause") =>
- {
- // No `CompiledGraph` is in play here, so there is no
- // checkpoint to pause against — mirror the direct loop's
- // steering pause instead: latch `run.paused` and finish
- // this call with `Ok(())`, exactly like
- // `run_loop`'s `LoopExit::Paused` handling. See the
- // module doc on `super` for why this is not a resumable
- // graph interrupt.
- break Ok(Some(interrupt));
- }
- Ok(NodeResult::Interrupt(interrupt)) => {
- // A `MiddlewareControl::Interrupt` (an approval gate, not
- // a steering pause). The direct loop surfaces this as
- // `TinyAgentsError::Interrupted` (see
- // `agent_loop::run_loop`'s `apply_pending_control`), not
- // a pause — matched here so `RunPolicy::execution ==
- // Graph` behaves identically to `Direct` for this
- // control. Only `compile_loop`/`LoopIter`'s real
- // `CompiledGraph` upgrades this into a resumable graph
- // interrupt (A5's "approvals surfacing as graph
- // interrupts").
- let message = interrupt
- .payload
- .get("message")
- .and_then(|value| value.as_str())
- .unwrap_or("interrupted")
- .to_string();
- break Err(TinyAgentsError::Interrupted {
- node: interrupt.node.to_string(),
- message,
- });
- }
- Ok(NodeResult::Update(updated)) => {
- // Every node body returns `Command`/`Interrupt` (see
- // `runtime`'s node docs); a bare `Update` would mean the
- // loop cannot determine where to go next.
- let _ = updated;
- break Err(TinyAgentsError::Validation(
- "GraphLoopDriver: loop node returned an un-routed update".to_string(),
- ));
- }
- Ok(NodeResult::Command(command)) => {
- loop_state = match command.update {
- Some(update) => update,
- None => {
- break Err(TinyAgentsError::Validation(
- "GraphLoopDriver: loop node's command carried no update"
- .to_string(),
- ));
- }
- };
- limit_stop |= loop_state.limit_stop;
- // Latch the first kind: a later command must not erase it.
- limit_kind = limit_kind.or(loop_state.limit_kind);
- let Some(target) = command.goto.first() else {
- break Err(TinyAgentsError::Validation(
- "GraphLoopDriver: loop node's command carried no route".to_string(),
- ));
- };
- let RouteTarget::Node(node_id) = target else {
- break Err(TinyAgentsError::Validation(
- "GraphLoopDriver: loop node routed via `Send`, which this driver \
- does not support"
- .to_string(),
- ));
- };
- if node_id.as_str() == crate::builder::END {
- break Ok(None);
- }
- current = match node_id.as_str() {
- node::PLAN => node::PLAN,
- node::MODEL => node::MODEL,
- node::TOOLS => node::TOOLS,
- node::SETTLE => node::SETTLE,
- other => {
- break Err(TinyAgentsError::Validation(format!(
- "GraphLoopDriver: unknown loop node `{other}`"
- )));
- }
- };
- continue;
- }
- Err(error) => break Err(error),
- }
- };
-
- let terminal = match &outcome {
- Ok(None) => {
- let reason = if limit_stop {
- TerminalOutcome::limit_reached(
- limit_kind,
- format!(
- "stopped with the partial run: {} limit reached",
- limit_kind.map_or("run", |kind| kind.as_str())
- ),
- )
- } else {
- TerminalOutcome::completed()
- };
- Some(reason.with_provider_started(ctx.provider_started()))
- }
- Ok(Some(interrupt)) => {
- let reason = interrupt
- .payload
- .get("reason")
- .or_else(|| interrupt.payload.get("message"))
- .and_then(|value| value.as_str())
- .map(str::to_string);
- let outcome = if let Some(summary) = ctx.take_halted_by_guard() {
- TerminalOutcome::halted(summary)
- } else {
- TerminalOutcome::new(
- TerminalReason::Paused,
- reason
- .clone()
- .unwrap_or_else(|| format!("paused at node `{}`", interrupt.node)),
- )
- };
- Some(outcome.with_provider_started(ctx.provider_started()))
- }
- Err(error) => {
- use tinyagents_harness::terminal::TimeoutPhase;
- let in_model_call = ctx.model_call_failed() || ctx.active_model_call.is_some();
- let site = if in_model_call && ctx.call_provider_started() {
- TimeoutPhase::Provider
- } else if in_model_call {
- TimeoutPhase::BeforeProvider
- } else if ctx.provider_started() {
- TimeoutPhase::AfterTurn
- } else {
- TimeoutPhase::BeforeProvider
- };
- // A `LimitExceeded` carries only text; the cap that tripped was
- // announced by a `LimitReached` event just before.
- let kind = matches!(error, TinyAgentsError::LimitExceeded(_))
- .then(|| ctx.peek_last_limit())
- .flatten();
- let mut outcome = TerminalOutcome::from_error(error, site).with_limit_kind(kind);
- // A failed summarizer already received a provider response; keep the
- // match for summarizers that report usage but never marked dispatch
- // (host-defined `Summarizer` impls cannot call `mark_dispatched`).
- outcome.provider_started = ctx.provider_started()
- || matches!(error, TinyAgentsError::SummarizationUsage { .. });
- Some(outcome)
- }
- };
- // Announce whatever the last turn appended and close it, on every exit
- // path, as the direct loop does before the transcript moves onto the
- // run (`run.messages` is the transcript as of the last node boundary).
- phases::lifecycle_close_turn(harness, ctx, &run.messages);
- run.terminal = terminal.clone();
- status.mark_running(HarnessPhase::Middleware);
- let after_agent = harness.middleware().run_after_agent(ctx, state, run).await;
- // `after_agent` may post-process `run.messages`; announce anything it
- // appended (even if it then failed), as the direct loop does.
- phases::lifecycle_flush(harness, ctx, &run.messages);
- if let Err(hook_error) = after_agent {
- if outcome.is_err() {
- // The originating node failure stays authoritative, as in the
- // direct loop; the hook still ran for its cleanup.
- tracing::warn!(
- target: "tinyagents::agent_loop",
- error = %hook_error,
- "[agent_loop] after_agent failed after a node error; keeping the node error"
- );
- } else {
- return Err(hook_error);
- }
- }
-
- // `status.mark_completed`/`mark_interrupted`/`mark_failed` and (on
- // error) `AgentEvent::RunFailed` are applied centrally by
- // `agent_loop::entry::drive_collecting` after this call returns,
- // identically for the direct loop and this driver — see that
- // function's doc comment. This driver only emits the terminal event
- // that (like the direct loop's `run_loop_body`) is its own
- // responsibility to raise: `RunCompleted` on a clean finish, or
- // latching `run.paused` (mirroring a steering pause) on an
- // interrupt.
- match outcome {
- Ok(None) => {
- // `after_agent` may have replaced the outcome; report the final one.
- let outcome = run
- .terminal
- .clone()
- .or(terminal)
- .expect("terminal set before after_agent");
- let record = ctx.emit(AgentEvent::RunCompleted {
- run_id: ctx.run_id().clone(),
- outcome: Some(outcome),
- });
- status.set_last_event(record.id);
- Ok(())
- }
- Ok(Some(interrupt)) => {
- let reason = interrupt
- .payload
- .get("reason")
- .or_else(|| interrupt.payload.get("message"))
- .and_then(|value| value.as_str())
- .map(str::to_string);
- let record = ctx.emit(AgentEvent::ControlApplied {
- control: "paused".to_string(),
- detail: reason
- .clone()
- .unwrap_or_else(|| format!("paused at node `{}`", interrupt.node)),
- });
- status.set_last_event(record.id);
- if !matches!(
- run.terminal.as_ref().map(|outcome| outcome.reason),
- Some(TerminalReason::Halted)
- ) {
- run.paused = Some(PauseState {
- reason,
- paused_at_checkpoint: 0,
- });
- }
- Ok(())
- }
- Err(error) => Err(error),
- }
- }
-}
diff --git a/crates/tinyagents-graph/src/agent_loop/iter.rs b/crates/tinyagents-graph/src/agent_loop/iter.rs
deleted file mode 100644
index 431aa1b04..000000000
--- a/crates/tinyagents-graph/src/agent_loop/iter.rs
+++ /dev/null
@@ -1,287 +0,0 @@
-//! [`AgentLoopGraphExt::iter`]/[`LoopIter`]: steps the compiled loop one node
-//! at a time.
-//!
-//! See the module doc on [`super`] for how this relates to [`super::compile_loop`]
-//! and [`super::GraphLoopDriver`].
-
-use std::sync::Arc;
-
-use tinyagents_harness::context::RunContext;
-use tinyagents_harness::error::{Result, TinyAgentsError};
-use tinyagents_harness::events::HarnessRunStatus;
-use tinyagents_harness::ids::ComponentId;
-use tinyagents_harness::middleware::AgentRun;
-use tinyagents_harness::runtime::AgentHarness;
-use tinyinference_llm::message::Message;
-
-use crate::command::NodeResult;
-
-use super::runtime::{self, LoopRuntime};
-use super::types::{LoopState, node};
-
-/// One completed activation reported by [`LoopIter::next`].
-#[derive(Clone, Debug)]
-pub struct LoopStep {
- /// The node that just ran.
- pub node: String,
- /// The node [`LoopIter::next`] will run next, unless
- /// [`LoopIter::override_next`] changes it first. `None` once the run has
- /// finished (reached `END`).
- pub next: Option,
- /// Whether this activation interrupted the run (a steering pause or a
- /// [`tinyagents_harness::context::MiddlewareControl::Interrupt`]) rather
- /// than completing normally. A caller that wants to resume calls
- /// [`LoopIter::next`] again — the interrupted node re-runs, exactly like
- /// [`crate::compiled::CompiledGraph::resume`] re-running an interrupted
- /// node — after applying whatever unblocks it (for example draining a
- /// [`tinyagents_harness::steering::SteeringHandle`]).
- pub interrupted: bool,
-}
-
-/// Extension trait adding [`Self::iter`] to `Arc<`[`AgentHarness`]`>`, mirroring pydantic-ai's `Agent.iter` (`docs/runtime-comparison/pydantic-ai.md`
-/// §4).
-///
-/// Implemented for `Arc>` rather than
-/// `AgentHarness` directly because the returned [`LoopIter`]
-/// keeps driving the loop across many `.await` points spanning its own
-/// lifetime (unlike a single [`AgentHarness::invoke`] call), so it needs to
-/// own a durable handle to the harness — see the module doc on [`super`]
-/// ("`GraphLoopDriver` vs. `compile_loop`/`LoopIter`") for why that rules out
-/// a borrowed `&AgentHarness` the way [`super::GraphLoopDriver`] gets one.
-pub trait AgentLoopGraphExt {
- /// Starts a steppable run: builds the compiled `plan -> model -> tools ->
- /// settle` graph (see [`super::compile_loop`]) and returns a
- /// [`LoopIter`] positioned at its entry (`plan`), seeded with `input` as
- /// the starting transcript. `app_state` is the harness's shared,
- /// read-only application state (the same value an
- /// [`AgentHarness::invoke`] caller would pass as `state`); `ctx` supplies
- /// the run identity and dependencies exactly as for `invoke`.
- fn iter(
- self,
- app_state: Arc,
- ctx: RunContext,
- input: Vec,
- ) -> Result>;
-}
-
-impl AgentLoopGraphExt for Arc>
-where
- State: Send + Sync + 'static,
- Ctx: Send + Sync + 'static,
-{
- fn iter(
- self,
- app_state: Arc,
- ctx: RunContext,
- input: Vec,
- ) -> Result> {
- LoopIter::new(self, app_state, ctx, input)
- }
-}
-
-/// Steps the compiled agent loop one node activation at a time.
-///
-/// Built via [`AgentLoopGraphExt::iter`]. Each [`Self::next`] call runs
-/// exactly one node body (the same [`super::runtime::plan_node`]/`model_node`/
-/// `tools_node`/`settle_node` [`super::compile_loop`] wires into a
-/// [`crate::CompiledGraph`]) against this iterator's own [`LoopRuntime`], so
-/// stepping through a `LoopIter` observes the identical transcript, usage,
-/// and routing decisions a full [`crate::CompiledGraph::run`] over the same
-/// graph would — just one activation at a time, with [`Self::override_next`]
-/// able to redirect the very next activation (for tests, debugging, or a
-/// host that wants to splice in extra bookkeeping between turns).
-pub struct LoopIter {
- rt: Arc>,
- state: LoopState,
- next: Option,
- overridden: Option,
- finished: bool,
-}
-
-impl LoopIter
-where
- State: Send + Sync + 'static,
- Ctx: Send + Sync + 'static,
-{
- pub(crate) fn new(
- harness: Arc>,
- app_state: Arc,
- ctx: RunContext,
- input: Vec,
- ) -> Result {
- let run_id = ctx.run_id().clone();
- let status = HarnessRunStatus::new(run_id, ComponentId::new("agent_loop.iter"));
- let rt = Arc::new(LoopRuntime::new(
- harness,
- app_state,
- ctx,
- AgentRun::default(),
- status,
- false,
- ));
- Ok(Self {
- rt,
- state: LoopState {
- messages: input,
- ..LoopState::default()
- },
- next: Some(node::PLAN.to_string()),
- overridden: None,
- finished: false,
- })
- }
-
- /// The committed [`LoopState`] as of the last completed [`Self::next`]
- /// call (or the seeded input, before the first call).
- pub fn state(&self) -> &LoopState {
- &self.state
- }
-
- /// The accumulated [`AgentRun`] mirrored alongside the loop state —
- /// useful for reading `usage`/`model_calls`/`tool_calls` without waiting
- /// for `settle` to populate [`LoopState::final_text`].
- pub async fn run(&self) -> AgentRun {
- self.rt.run.lock().await.clone()
- }
-
- /// The node [`Self::next`] will run on its next call, or `None` once the
- /// run has finished.
- pub fn next_node(&self) -> Option<&str> {
- self.next.as_deref()
- }
-
- /// Redirects the very next [`Self::next`] call to `node` instead of
- /// wherever the last activation routed to. Consumed by that one call —
- /// subsequent calls follow the graph's normal routing again unless
- /// overridden again. A no-op once the run has finished.
- pub fn override_next(&mut self, node: impl Into) {
- if !self.finished {
- self.overridden = Some(node.into());
- }
- }
-
- /// Runs exactly one node activation and reports what happened, or `Ok(None)`
- /// if the run had already finished.
- pub async fn next(&mut self) -> Result