A loop engineering framework. It owns the shape of one goal run: attempt something, evaluate what came back, route to the next turn, and stop when a verdict or a budget says to.
It is the middle of three layers, and that split is what keeps each of them small.
TinyFlows is the engine. It
executes one graph run and decides nothing. A loop's unit of work is a
WorkflowGraph it compiles once and runs per turn. Every effect (models, tools,
HTTP, code execution, persistence) goes through a capability trait, so a loop
picks its own vendors and the examples here run offline against in-memory mocks.
TinyLoops, this repository, is the run itself: attempt, evaluate, route, budget, plus the orchestrator, roles, tools, memory, workspace, and observability a run needs to do that honestly.
tinyflows-adaptive is what spans runs:
ledger rows, scored lessons, workflow selection and authoring, promotion. Its
own rule draws the line this repository works to. The engine may know about one
run; anything that spans runs lives there.
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β tinyflows-adaptive what spans runs: ledger rows, scored β
β lessons, workflow selection, promotion β
ββββββββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββ
β picks a graph, records how it went
ββββββββββββββββββββββββββββββββΌββββββββββββββββββββββββββββββββ
β tinyloops (here) one goal run: attempt, evaluate, β
β route, budget β
β β
β orchestrate/ plan, attempt, report: the run's own agent β
β loops/ emits the graph below, from state + policy β
β policy/ thresholds, verdicts, the routing ladder β
β harness/ memory/ tools/ workspace/ ledger/ the seams β
β observe/ one ordered event stream, and the Report β
ββββββββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββ
β compiles the graph once, runs it per turn
ββββββββββββββββββββββββββββββββΌββββββββββββββββββββββββββββββββ
β tinyflows one graph run, and no decisions. β
β every effect behind a capability trait β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
The graph loops/ emits is the whole run, not one turn of it, so a pause and a
resume are the engine's problem rather than a driver's:
trigger ββΆ plan ββΆ research ββΆ βββββββββββββββ ββ done ββΆ stand_down ββΆ report
(once) (once) β loop head β
β accumulator β
ββββββββ¬βββββββ
βΌ
attempt one report, written once
β
ββββββββββββββββββΌβββββββββββββββββ
βΌ βΌ βΌ
reflect judge ...more arms
β β β
ββββββββββββββββββΌβββββββββββββββββ
βΌ
merge folds every arm by delta
β
βΌ
route jq generated from Rust constants
β
βΌ
pass ββββΆ the only edge back to the head
Three things in that picture are load-bearing. The loop head is the only writer
of the accumulator, so a checkpoint and a resume carry the counters rather than
losing them with a stack frame. Every verdict, retry, and diversify leaves
through the single pass node, because the engine's node map is cumulative and
a fold that reads "the merge if it ran, otherwise the reflection" will read a
stale merge forever. And the arms read the attempt report, never the
accumulator, since the head folds at the top of a pass and mid-body the
accumulator is one pass behind.
The arms are concurrent and they converge on a real barrier. They also receive
the attempt as input rather than as their own prior turn, which is not a
stylistic choice: relabelling an identical wrong claim away from the assistant
role raises the correction rate by 23 to 93 points across most model and domain
pairs. docs/specs/prior-art.md carries that evidence and the rest of it.
crates/tinyloops-bus is the wire contract: member names, payload types, and
the contract version, with no transport and no behavior. crates/tinyloops is
the implementation. A host that only makes calls depends on the contract crate
alone and compiles neither the module nor tinybus itself.
tinyloops depends on the contract crate and re-exports all of it, so a payload
type named through the framework and the same type named through the contract
are one type rather than structural twins. A parallel set of payload types for
hosts would mean a conversion at every call site that nothing checks.
Two more dependencies are vendored rather than reimplemented.
TinyAgents is the optional durable
harness: once a loop has to pause, checkpoint, resume, or be watched, a while
statement stops being enough and the control flow moves into a harness graph. It
sits behind the tinyagents cargo feature, so a shipped module never resolves
it. TinyBus is how a loop is reached
from outside the process; the workspace builds as both an rlib and the
cdylib TinyBus loads.
Compile the graph once, run it per turn, feed each result back in, and stop on a
judge or a budget. That is the whole shape, and it is the complete
simple_loop example minus its
graph definition:
// Compile once, outside the loop: a compiled workflow is the reusable artifact.
let step = compile(&refine_step())?;
let capabilities = mock_capabilities();
let mut state = json!({ "score": 0 });
let mut turns = 0;
while turns < MAX_TURNS {
let outcome = run(&step, state.clone(), &capabilities).await?;
// The run state is keyed by node id; the loop carries the last node forward.
state = outcome.output["nodes"]["refine"]["items"][0]["json"].clone();
turns += 1;
if state["score"].as_i64().unwrap_or(0) >= TARGET_SCORE {
break;
}
}Swap refine_step() for a graph that calls a model, and score for a real
judge, and nothing around them changes. The turn budget belongs to the loop
rather than to a caller who remembers to add one. A loop without a budget is a
way to spend an afternoon discovering that a judge never says yes.
When the loop needs durability, those same two decisions become harness nodes
and the while statement disappears:
let loop_graph = GraphBuilder::<LoopState, LoopState>::overwrite()
.add_node("refine", move |state: LoopState, _ctx: NodeContext| {
refine(state, Arc::clone(&workflow), Arc::clone(&capabilities))
})
.set_entry("refine")
.add_conditional_edges(
"refine",
|state: &LoopState| judge(state), // "again" or "done"
[("again", "refine"), ("done", END)],
)
.compile()?;
let finished = loop_graph.run(LoopState::new()).await?;TinyFlows never learns that a harness is driving it, and TinyAgents never learns
what the workflow does. See
tinyagents_harness for the
running version.
cargo run -p tinyloops --example simple_loop # the loop, in plain Rust
cargo run -p tinyloops --example research_loop # the whole preset, end to end
cargo run -p tinyloops --features tinyagents --example tinyagents_harness # the loop, under a harness
cargo run -p tinyloops --example basic # ordinary library API usageresearch_loop is the one to read first. It assembles the shipped preset over
the reference seams, drives it to a terminal state, and prints every pass
boundary, every step, every arm, the merge, the verdict, and the route each pass
took with the counters it was taken on.
tuned_research_loop is the same loop with a third arm that may revise the
run's own configuration β its thresholds, its spend, and which arms it is still
paying for β within the room its preset declares. Every revision and every
refusal is an event and a line in the report; nothing here scores them, because
scoring a configuration against outcomes spans runs and lives in
tinyflows-adaptive.
Every example runs against TinyFlows' mock capabilities or the reference
implementations, so they are deterministic, offline, and need no provider
credentials. tinyagents is optional: the harness example declares
required-features, so a default build skips it instead of failing to compile.
| Area | What is configured |
|---|---|
| Layout | A cargo workspace under crates/, split into a dependency-light wire contract and the framework that implements it; one directory module per concern, a crate-wide error type, integration tests, and runnable examples |
| Lints | unsafe_code forbidden, missing_docs, clippy all plus pedantic, and no unwrap/expect/panic/todo in library code, all declared once in [workspace.lints] so every crate, local run, and CI run agree |
| CI | Format, clippy, build, test (default and all features), a run of each bundled example, an assertion that the contract crate stays transport-free, at least 90% line coverage in every source file, rustdoc with -D warnings, an MSRV build, and a cargo-deny supply-chain check |
| Release | Manual workflow_dispatch bump that validates, versions, tags, and creates installable native module packages for every supported platform |
| Community | Issue and pull request templates, Dependabot, contributing, security, support, and code of conduct docs |
| Agents | AGENTS.md as the single source of truth, symlinked as CLAUDE.md, plus a .claude/settings.json allowlist for the standard commands |
| Loop | The engine seam: a workflow compiled once and run per turn, a judge that decides whether to go again, and a budget the loop enforces rather than the caller |
| Seams | Harness, memory, tools, and workspace are traits the embedder implements, so nothing here picks a model vendor, a store, or a runtime for you |
| Vendor | TinyBus host types and module SDK, the TinyFlows workflow engine and its adaptive loop, and the TinyAgents harness, each pinned as a vendor/ build-time submodule |
Cargo.toml # virtual workspace: members, shared metadata, lints
crates/
βββ tinyloops-bus/ # the wire contract: what crosses the bus
β βββ README.md # why the contract is its own crate
β βββ src/
β βββ lib.rs # crate docs + the entire public re-export surface
β βββ names/ # interface, object path, one constant per member
β βββ <family>/ # payload types, one directory per family
β β βββ mod.rs
β β βββ types.rs
β β βββ test.rs
β βββ version/ # contract version and the host bind rule
βββ tinyloops/ # the framework: behavior, adapter, and the cdylib
βββ src/
β βββ lib.rs # crate docs + public surface, re-exporting the contract
β βββ error/ # crate-wide `Error` and `Result<T>`
β βββ state/ # what one goal run carries from turn to turn
β βββ policy/ # the decision a turn's outcome feeds: stop, retry, route
β βββ step/ # one unit of work, compiled once and run per turn
β βββ arm/ # the alternatives a route can choose between
β βββ loops/ # the graph that wires state, steps, arms, and policy
β βββ budget/ # turn, token, wall-clock, and cost limits
β βββ observe/ # the events a run emits, and who receives them
β βββ orchestrate/# what drives a goal run end to end
β βββ harness/ # the seam a durable driver plugs into
β βββ memory/ # the seam recall plugs into
β βββ tools/ # the seam a tool provider plugs into
β βββ workspace/ # the seam run artifacts are written through
β βββ ledger/ # the rows a run leaves behind for the next one
β βββ presets/ # assembled loops, ready to run
β βββ tinybus_module/ # bus interface, setup, and ABI v1 exports
βββ tests/
β βββ public_api.rs # integration tests against the public API only
βββ examples/
βββ simple_loop.rs # the loop, in plain Rust
βββ research_loop.rs # the shipped preset, driven end to end
βββ tinyagents_harness.rs # the same loop under a durable harness
βββ basic.rs # ordinary library API usage
βββ verify_module.rs # local dynamic-module verification
βββ verify_github_release.rs # tagged-release download and bus call
vendor/
βββ tinybus/ # pinned TinyBus git submodule: host types, module SDK
βββ tinyflows/ # pinned workflow engine and its adaptive loop
βββ tinyagents/ # pinned durable agent + graph harness
docs/
βββ README.md # documentation index and conventions
βββ specs/ # behavior and architecture specifications
βββ plans/ # implementation-ordered delivery plans
βββ adr/ # immutable architecture decision records
Directories under crates/tinyloops/src/ that do not exist yet are where that
work lands. ROADMAP.md says which is which.
Within each crate, feature areas use directory modules: implementation and
exports live in mod.rs, substantial types move to types.rs, and unit tests
live in test.rs. AGENTS.md holds the complete repository
guidance, and CLAUDE.md is a symlink to it so every coding agent reads one
source of truth.
Clone with submodules, or initialize them before building:
git submodule update --init --recursivecargo fmt --all -- --check
cargo clippy --all-targets --all-features -- -D warnings
cargo build --all-targets --all-features
cargo test --all-features
cargo run -p tinyloops --example simple_loop
cargo run -p tinyloops --example research_loop
cargo run -p tinyloops --features tinyagents --example tinyagents_harness
cargo build -p tinyloops --release --lib # produces the installable cdylibThe first four are exactly what CI runs. Optional extras:
cargo doc --no-deps --all-features # CI builds this with RUSTDOCFLAGS="-D warnings"
cargo deny check all # supply-chain check; see deny.toml
cargo install cargo-llvm-cov # once, before running the coverage gate
.github/scripts/check-file-coverage.sh 90 coverage.jsonRun the Release workflow from the Actions tab with a patch, minor, or
major bump. Use current only to resume an interrupted release whose version
commit and tag already exist. The workflow revalidates the workspace, versions
and tags it (one [workspace.package] version that every member inherits),
builds crates/tinyloops as a TinyBus cdylib, and creates a GitHub release.
Assets follow tinyloops-<version>-<platform>.<tar.gz|zip> and contain the
native module, its SHA-256 modules.toml, the license, and
MODULE.md. Every release also publishes checksum.toml, which
TinyBus uses to verify an archive before extraction. The workflow then loads the
published Ubuntu archive through TinyBus's GitHub release API and calls its
Greet method before declaring the release successful. TinyBus itself is not
shipped by this repository; the pinned submodule is the build-time SDK.
The stable native matrix covers Ubuntu 22.04 and 24.04 on x86_64 and ARM64;
Fedora 43 and 44 on x86_64 and ARM64; rolling Arch Linux on its officially
supported x86_64 architecture; macOS 15 and 26 on Intel and Apple Silicon;
Windows Server 2022 and 2025 on x86_64; and Windows 11 on ARM64. Preview,
deprecated, and unofficial architecture images are not release gates. Do not
hand-edit the version in the root Cargo.toml.
AGENTS.mdfor repository guidelines, human and agent alikeROADMAP.mdfor what is built, what is next, what is notCONTRIBUTING.mdfor how to propose a changedocs/specs/for behavior and architecture specsdocs/plans/for test-first implementation plansdocs/adr/for architecture decision recordsSECURITY.mdfor how to report a vulnerability
GPL-3.0-only. See LICENSE.