From b917caf30db9f859f337a4acc0ded7ea84ee9b1b Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Wed, 23 Sep 2026 12:04:15 +0300 Subject: [PATCH 01/12] ci: restructure workflow into parallel jobs and optimise caching The CI workflow is split into separate lint, test, and coverage jobs so that each feature set builds concurrently rather than serially with intervening clean steps. Environment variables are added to disable incremental compilation and debug info, reducing build times and cache sizes. The push trigger is restricted to the main branch to avoid duplicate runs on pull request commits, and concurrency grouping is adjusted to use head_ref for pull requests so superseded commits cancel each other. Auto-committed-on: dragonfly Co-authored-by: Medulla --- .github/workflows/ci.yml | 152 ++++++++++++++++++++++++++++----------- 1 file changed, 110 insertions(+), 42 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 41b9fb58..468a9cff 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,18 +2,39 @@ name: CI on: push: + # Pull requests are covered by the `pull_request` trigger below. Building + # every branch push as well duplicated the entire suite for each PR commit + # (two identical ~23 minute runs), so pushes only build the mainline, which + # is also what keeps a shared, warm cache for PR branches to restore from. + branches: [main] pull_request: concurrency: - group: ${{ github.workflow }}-${{ github.ref }} + # `github.head_ref` is set for pull requests and empty for pushes, so a PR's + # runs coalesce under one group and superseded commits cancel each other. + group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} cancel-in-progress: true permissions: contents: read +env: + CARGO_TERM_COLOR: always + # Incremental compilation only pays off across edits on one machine; in CI it + # costs codegen time and inflates the cached target directory. + CARGO_INCREMENTAL: 0 + # Debug info dominates link time and target-directory size. Dropping it + # roughly halves both, and nothing in CI reads line numbers out of a + # backtrace. + CARGO_PROFILE_DEV_DEBUG: 0 + CARGO_PROFILE_TEST_DEBUG: 0 + RUST_BACKTRACE: 1 + jobs: - rust-sdk: - name: Rust SDK + # Static analysis only: `cargo clippy` and `cargo doc` stop at metadata, so + # this job never links a binary and finishes long before the test matrix. + lint: + name: Lint runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 @@ -44,45 +65,6 @@ jobs: - name: Clippy all features run: cargo clippy --workspace --all-targets --all-features -- -D warnings - - name: Build - run: cargo build --workspace --all-targets - - - name: Build all features - run: cargo build --workspace --all-targets --all-features - - - name: Test - run: cargo test --workspace - - - name: Test all features - run: cargo test --workspace --all-features - - - name: Test optional features independently - run: | - # Each feature set links a distinct workspace graph. Clearing the - # preceding graph keeps the runner's linker working set bounded and - # makes these genuinely independent builds. - cargo clean - cargo test --workspace --no-default-features --features sqlite - cargo clean - cargo test --workspace --no-default-features --features tools - cargo clean - cargo test --workspace --no-default-features --features multimodal - - - name: Free build artifacts before coverage - run: cargo clean - - - name: Coverage gate - uses: taiki-e/install-action@cargo-llvm-cov - - - name: Verify line coverage - env: - CARGO_PROFILE_DEV_DEBUG: 0 - CARGO_PROFILE_TEST_DEBUG: 0 - run: >- - cargo llvm-cov --all-features --workspace - --ignore-filename-regex '(^|/)(tests?|examples)/|/test(_.*)?\.rs$' - --fail-under-lines 80 - # TODO: drop `continue-on-error` once the ~160 existing broken # intra-doc link warnings across the workspace are fixed and this can # block merges instead of only reporting. @@ -98,3 +80,89 @@ jobs: - name: Unused dependencies uses: bnjbvr/cargo-machete@main continue-on-error: true + + # Every feature selection links a distinct workspace graph, so they cannot + # share a target directory. Previously they ran back to back with a + # `cargo clean` between each, which serialised five cold builds into one job + # and left the cache holding whichever graph happened to be built last. As + # separate jobs they build concurrently and each keeps its own warm cache. + test: + name: Test (${{ matrix.name }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - name: default + features: "" + - name: all features + features: "--all-features" + - name: sqlite + features: "--no-default-features --features sqlite" + - name: tools + features: "--no-default-features --features tools" + - name: multimodal + features: "--no-default-features --features multimodal" + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + submodules: recursive + + - uses: dtolnay/rust-toolchain@stable + + - uses: Swatinem/rust-cache@v2 + with: + workspaces: . + # Without a distinct key every matrix leg would race to overwrite one + # cache entry with its own feature graph. + key: ${{ matrix.name }} + + - name: Build + run: cargo build --workspace --all-targets ${{ matrix.features }} + + - name: Test + run: cargo test --workspace ${{ matrix.features }} + + coverage: + name: Coverage + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + submodules: recursive + + - uses: dtolnay/rust-toolchain@stable + with: + components: llvm-tools-preview + + - uses: Swatinem/rust-cache@v2 + with: + workspaces: . + # Instrumented objects are not interchangeable with the test matrix's, + # so coverage keeps its own cache rather than invalidating theirs. + key: coverage + + - uses: taiki-e/install-action@cargo-llvm-cov + + - name: Verify line coverage + run: >- + cargo llvm-cov --all-features --workspace + --ignore-filename-regex '(^|/)(tests?|examples)/|/test(_.*)?\.rs$' + --fail-under-lines 80 + + # Single required status check: branch protection can depend on this one job + # instead of being updated every time the matrix gains or loses a leg. + ci: + name: CI + runs-on: ubuntu-latest + if: always() + needs: [lint, test, coverage] + steps: + - name: Check results + run: | + echo "lint: ${{ needs.lint.result }}" + echo "test: ${{ needs.test.result }}" + echo "coverage: ${{ needs.coverage.result }}" + [ "${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}" = "false" ] From 14e2f95611653296fdd2000c63a7ddb2aaae44ed Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Wed, 23 Sep 2026 12:04:32 +0300 Subject: [PATCH 02/12] fix(ci): use hyphen in CI matrix job name The job name for the "all features" matrix entry contained a space, which caused issues with certain CI tooling that expects hyphenated identifiers. The name is changed to "all-features" to ensure compatibility. Auto-committed-on: dragonfly Co-authored-by: Medulla --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 468a9cff..8217c032 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -95,7 +95,7 @@ jobs: include: - name: default features: "" - - name: all features + - name: all-features features: "--all-features" - name: sqlite features: "--no-default-features --features sqlite" From 766afc5442318bf08dda6a2440fa7b442665bfb8 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Wed, 23 Sep 2026 12:19:21 +0300 Subject: [PATCH 03/12] chore(docs): fix intra-doc link paths in graph crate Update several rustdoc links to use fully-qualified paths or plain code spans where the original link targets were not in scope, preventing broken documentation links and keeping the docs accurate. Auto-committed-on: dragonfly Co-authored-by: Medulla --- crates/tinyagents-graph/src/agent_loop/mod.rs | 4 ++-- crates/tinyagents-graph/src/channel/registry.rs | 2 +- crates/tinyagents-graph/src/channel/types.rs | 2 +- crates/tinyagents-graph/src/checkpoint/types.rs | 2 +- crates/tinyagents-graph/src/compiled/mod.rs | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/tinyagents-graph/src/agent_loop/mod.rs b/crates/tinyagents-graph/src/agent_loop/mod.rs index 9f1f8966..107f8550 100644 --- a/crates/tinyagents-graph/src/agent_loop/mod.rs +++ b/crates/tinyagents-graph/src/agent_loop/mod.rs @@ -48,7 +48,7 @@ //! `structured_strategy_override`) or `EndStrategy::Early`/`Exhaustive` (A6); //! `resolve_structured_plan` also resolves the profile-driven `Auto` choice //! against the *default* model binding rather than the turn's actually- -//! resolved model (see that function's docs). [`RunPolicy::execution`] +//! resolved model (see that function's docs). [`tinyagents_harness::runtime::RunPolicy::execution`] //! defaults to [`tinyagents_harness::runtime::LoopExecution::Direct`], so //! every existing caller is unaffected unless it opts in. //! @@ -93,7 +93,7 @@ //! make installing a driver on a not-yet-`Arc`'d harness impossible — a //! real usability regression) or unsafely extending the borrow's lifetime //! (which this workspace denies via `unsafe_code = "deny"`). So -//! [`GraphLoopDriver::drive`] instead calls the exact same node bodies +//! [`GraphLoopDriver`]'s `drive` instead calls the exact same node bodies //! directly, in a hand-rolled loop, against the real borrowed `&mut` //! state — no `Arc`, no `Mutex`, no `CompiledGraph` involved — which is //! sound with zero unsafe code precisely because a borrowed async call diff --git a/crates/tinyagents-graph/src/channel/registry.rs b/crates/tinyagents-graph/src/channel/registry.rs index de9f0dcb..49127c0f 100644 --- a/crates/tinyagents-graph/src/channel/registry.rs +++ b/crates/tinyagents-graph/src/channel/registry.rs @@ -150,7 +150,7 @@ impl ReducerRegistry { /// The `{"reducer": name}` config payload for a named `BinaryAggregate` /// channel, or `{"reducer": null}` for an unnamed one (which - /// [`crate::channel::channel_from_config`] then rejects on decode). + /// `channel_from_config` then rejects on decode). pub(crate) fn config_for(name: Option<&str>) -> Value { json!({ "reducer": name }) } diff --git a/crates/tinyagents-graph/src/channel/types.rs b/crates/tinyagents-graph/src/channel/types.rs index e35605cc..3ef887fc 100644 --- a/crates/tinyagents-graph/src/channel/types.rs +++ b/crates/tinyagents-graph/src/channel/types.rs @@ -59,7 +59,7 @@ pub trait Channel: Send + Sync { /// The channel's construction config, serialized so [`ChannelSet`] can /// round-trip `{ kind, config, value }` through a durable checkpointer /// without knowing the concrete channel type. Paired with - /// [`channel_from_config`] on decode. Channels with no configuration + /// `channel_from_config` on decode. Channels with no configuration /// (the default) serialize `Value::Null`; [`Barrier`]/[`NamedBarrier`] /// carry their `expected` set, and [`BinaryAggregate`] carries the /// registered reducer name (see [`BinaryAggregate::named`] and diff --git a/crates/tinyagents-graph/src/checkpoint/types.rs b/crates/tinyagents-graph/src/checkpoint/types.rs index d3850377..76fe7be9 100644 --- a/crates/tinyagents-graph/src/checkpoint/types.rs +++ b/crates/tinyagents-graph/src/checkpoint/types.rs @@ -607,7 +607,7 @@ impl Checkpoint { /// re-serializes as clean v2), and stamps [`Checkpoint::version`] to /// [`CHECKPOINT_FORMAT_VERSION`]. /// - /// A no-op on an already-v2 record. Every bundled [`Checkpointer`] + /// A no-op on an already-v2 record. Every bundled [`crate::checkpoint::Checkpointer`] /// backend calls this on every decode path (`get`/`get_scoped`/`list`/ /// `state_history`/`get_thread`), so callers outside this module never /// observe a v1 record — see `docs/modules/graph/checkpointing.md`. diff --git a/crates/tinyagents-graph/src/compiled/mod.rs b/crates/tinyagents-graph/src/compiled/mod.rs index 66d475a4..40a860db 100644 --- a/crates/tinyagents-graph/src/compiled/mod.rs +++ b/crates/tinyagents-graph/src/compiled/mod.rs @@ -47,7 +47,7 @@ //! forward across however many times this step interrupts/fails and gets //! resumed/retried; only once every branch of the step has completed does //! the executor route them all together, in one call, against one -//! committed state — see [`boundary::CompiledGraph::advance`]'s +//! committed state — see `boundary::CompiledGraph::advance`'s //! `carried_completed` handling. One caveat: a deferred branch's routing //! is re-resolved via static/conditional edges only (an explicit //! `Command::goto` it returned is not itself persisted across the From 9bc54aba578cc907f1dc31a0bc70ee500d49d9f5 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Wed, 23 Sep 2026 12:19:30 +0300 Subject: [PATCH 04/12] fix(types): clarify LoopState.finished doc comment Reword the doc comment on the `finished` field to explain that a run ending in an error reports the error through the driver's `Result` rather than through this struct, making the terminal outcome semantics clearer. Auto-committed-on: dragonfly Co-authored-by: Medulla --- crates/tinyagents-graph/src/agent_loop/types.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/tinyagents-graph/src/agent_loop/types.rs b/crates/tinyagents-graph/src/agent_loop/types.rs index 890388ee..4d3f944d 100644 --- a/crates/tinyagents-graph/src/agent_loop/types.rs +++ b/crates/tinyagents-graph/src/agent_loop/types.rs @@ -43,8 +43,9 @@ pub struct LoopState { pub tool_calls: usize, /// Names of calls that reached a tool executor, in execution order. pub executed_tools: Vec, - /// Set once the loop has produced a terminal outcome (finished, not - /// necessarily successfully — see [`Self::final_error`]). + /// Set once the loop has produced a terminal outcome — finished, not + /// necessarily successfully. A run that ended in an error reports it + /// through the driver's `Result`, not through this struct. pub finished: bool, /// The final assistant text, once [`Self::finished`] is set by a normal /// completion, [`MiddlewareControl::StopWithFinal`][mc], or From 84a3e7c7b58aacb5c91ef8b2dc8306a9883d2f42 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Wed, 23 Sep 2026 12:19:46 +0300 Subject: [PATCH 05/12] chore: update intra-doc links to use fully-qualified paths Update documentation links across multiple modules to use fully-qualified paths instead of relative crate paths, ensuring they resolve correctly in generated documentation and preventing broken links when items are re-exported or restructured. Auto-committed-on: dragonfly Co-authored-by: Medulla --- crates/tinyagents-harness/src/agent_loop/tools.rs | 2 +- crates/tinyagents-harness/src/context/mod.rs | 2 +- crates/tinyagents-harness/src/context/types.rs | 2 +- crates/tinyagents-harness/src/error.rs | 2 +- crates/tinyagents-harness/src/host/mod.rs | 2 +- crates/tinyagents-harness/src/limits/types.rs | 2 +- crates/tinyagents-harness/src/runtime/types.rs | 2 +- crates/tinyagents-harness/src/summarization/types.rs | 4 ++-- crates/tinyagents-harness/src/tool/deferred/types.rs | 6 +++--- 9 files changed, 12 insertions(+), 12 deletions(-) diff --git a/crates/tinyagents-harness/src/agent_loop/tools.rs b/crates/tinyagents-harness/src/agent_loop/tools.rs index cd543952..c78be5c6 100644 --- a/crates/tinyagents-harness/src/agent_loop/tools.rs +++ b/crates/tinyagents-harness/src/agent_loop/tools.rs @@ -1844,7 +1844,7 @@ impl AgentHarness { /// re-attempts the call. /// /// A call whose tool is no longer registered on this harness (renamed, - /// removed since the interrupted run) is treated as [`ToolReplay::Never`] + /// removed since the interrupted run) is treated as [`tinytools::ToolReplay::Never`] /// — fail closed rather than blindly re-run an unknown effect. /// /// Only ledger rows still in [`crate::tool::ToolEffectStatus::Started`] diff --git a/crates/tinyagents-harness/src/context/mod.rs b/crates/tinyagents-harness/src/context/mod.rs index c501c272..5a3bee6e 100644 --- a/crates/tinyagents-harness/src/context/mod.rs +++ b/crates/tinyagents-harness/src/context/mod.rs @@ -365,7 +365,7 @@ impl RunContext { /// The counter is per-context, not process-global: a freshly constructed /// context (including a child context, which never inherits its parent's /// counter) always starts at `0`. Callers that spawn deterministically - /// named children — [`crate::subagent::SubAgent`], for one — use this + /// named children — `tinyagents_orchestration::SubAgent`, for one — use this /// instead of a process-wide sequence so two processes calling the same /// parent context's child spawner in the same order derive identical /// ordinals, and therefore identical child run ids (M-2). diff --git a/crates/tinyagents-harness/src/context/types.rs b/crates/tinyagents-harness/src/context/types.rs index 4ac48fe3..42092d71 100644 --- a/crates/tinyagents-harness/src/context/types.rs +++ b/crates/tinyagents-harness/src/context/types.rs @@ -489,7 +489,7 @@ pub struct RunContext { pub(crate) child_ordinal: std::sync::Arc, /// Durable tool-effect ledger for this run, when a host wants crash-safe /// bookkeeping of tool-call side effects (B5). `None` (the default) means - /// no ledger writes happen and [`crate::tool::ToolPolicy`]'s + /// no ledger writes happen and [`tinytools::ToolPolicy`]'s /// `runtime.replay` declaration has nothing to guard resume against — the /// agent loop behaves exactly as it did before this existed. Attach one /// with [`RunContext::with_tool_effect_ledger`]; a child context inherits diff --git a/crates/tinyagents-harness/src/error.rs b/crates/tinyagents-harness/src/error.rs index 72ac88c5..86eeda64 100644 --- a/crates/tinyagents-harness/src/error.rs +++ b/crates/tinyagents-harness/src/error.rs @@ -151,7 +151,7 @@ pub enum TinyAgentsError { /// the counterpart to [`Self::ModelRetry`]. Folded into a /// [`tinytools::ToolResult::failed`] result (still recoverable at the /// transcript level — the model sees the message — but - /// [`crate::retry::RetryMiddleware`] and any other retry policy treat it + /// [`crate::middleware::library::RetryMiddleware`] and any other retry policy treat it /// as non-retryable rather than re-attempting the call). #[error("permanent tool failure: {0}")] ToolFailed(String), diff --git a/crates/tinyagents-harness/src/host/mod.rs b/crates/tinyagents-harness/src/host/mod.rs index 6a13fa25..74aa8e95 100644 --- a/crates/tinyagents-harness/src/host/mod.rs +++ b/crates/tinyagents-harness/src/host/mod.rs @@ -103,7 +103,7 @@ pub struct HostCapabilities { /// Procedural memory of how this agent has performed before. `None` means /// no experience is recorded or recalled. pub experience: Option>, - /// Whether a resolved [`AgentDefinition`] that declares no tools (an + /// Whether a resolved [`tinyagents_definition::AgentDefinition`] that declares no tools (an /// empty or absent `tools` list) denies every tool, instead of granting /// the whole registered catalogue. /// diff --git a/crates/tinyagents-harness/src/limits/types.rs b/crates/tinyagents-harness/src/limits/types.rs index 6b0d68b7..f3e973d9 100644 --- a/crates/tinyagents-harness/src/limits/types.rs +++ b/crates/tinyagents-harness/src/limits/types.rs @@ -63,7 +63,7 @@ pub struct RunLimits { /// Maximum sub-agent / recursion depth allowed for the run tree rooted at /// this run. A top-level run is depth `0`; each nested child run increments /// the depth. A sub-agent invocation whose child depth would exceed this cap - /// fails fast (see [`crate::subagent`]). Defaults to + /// fails fast (see the `tinyagents-orchestration` sub-agent invoker). Defaults to /// [`RunLimits::DEFAULT_MAX_DEPTH`]. pub max_depth: usize, /// What the run should do when a call cap is reached. Defaults to diff --git a/crates/tinyagents-harness/src/runtime/types.rs b/crates/tinyagents-harness/src/runtime/types.rs index d994696b..9df15053 100644 --- a/crates/tinyagents-harness/src/runtime/types.rs +++ b/crates/tinyagents-harness/src/runtime/types.rs @@ -352,7 +352,7 @@ pub struct RunPolicy { /// [`OutputRetryPolicy`]. pub output_retry: OutputRetryPolicy, /// What the loop does when one turn's tool calls include both a - /// structured-output "schema" call ([`StructuredStrategy::ToolCall`]'s + /// structured-output "schema" call ([`crate::structured::StructuredStrategy::ToolCall`]'s /// synthetic tool) and one or more genuine function-tool calls (A6). /// Defaults to [`EndStrategy::Graceful`]. pub end_strategy: EndStrategy, diff --git a/crates/tinyagents-harness/src/summarization/types.rs b/crates/tinyagents-harness/src/summarization/types.rs index 05788b5b..9ee76664 100644 --- a/crates/tinyagents-harness/src/summarization/types.rs +++ b/crates/tinyagents-harness/src/summarization/types.rs @@ -523,8 +523,8 @@ pub struct CompactionRecord { pub summary: String, /// Index, into the non-system message slice compaction operated over, of /// the first message that survives verbatim (everything before it was - /// folded into [`Self::summary`]). Matches [`CutPoint::index`] when the - /// record was produced from a [`CutPoint`]. + /// folded into [`Self::summary`]). Matches [`crate::summarization::CutPoint::index`] when the + /// record was produced from a [`crate::summarization::CutPoint`]. pub first_kept_index: usize, /// Estimated total tokens of the transcript immediately before /// compaction. diff --git a/crates/tinyagents-harness/src/tool/deferred/types.rs b/crates/tinyagents-harness/src/tool/deferred/types.rs index f61a8784..09f6ce78 100644 --- a/crates/tinyagents-harness/src/tool/deferred/types.rs +++ b/crates/tinyagents-harness/src/tool/deferred/types.rs @@ -5,13 +5,13 @@ //! (`ToolPolicy.access.approval_required`), the tool (or a `before_tool` //! middleware) raised [`TinyAgentsError::ApprovalRequired`] / //! [`TinyAgentsError::CallDeferred`], or the tool was registered schema-only -//! through [`ToolRegistry::register_external`]. The loop then finishes the +//! through [`crate::ToolRegistry::register_external`]. The loop then finishes the //! rest of the batch and exits with [`DeferredToolRequests`], which the host //! resolves into [`DeferredToolResults`] and hands back to resume. //! //! [`TinyAgentsError::ApprovalRequired`]: crate::error::TinyAgentsError::ApprovalRequired //! [`TinyAgentsError::CallDeferred`]: crate::error::TinyAgentsError::CallDeferred -//! [`ToolRegistry::register_external`]: crate::tool::ToolRegistry::register_external +//! [`crate::ToolRegistry::register_external`]: crate::tool::ToolRegistry::register_external use std::collections::BTreeMap; @@ -109,7 +109,7 @@ pub trait DeferredToolHandler: Send + Sync { } /// A schema-only tool the host executes out of band; see -/// [`ToolRegistry::register_external`]. +/// [`crate::ToolRegistry::register_external`]. /// /// Admission recognises it through [`is_external_tool`] and defers the call /// before anything runs. `execute` still exists (a host calling the From d990da146f32b4ec53c88b8906b257473d6a69f4 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Wed, 23 Sep 2026 12:20:07 +0300 Subject: [PATCH 06/12] fix(harness): remove memory module references from doc comment The module-level documentation in the harness crate no longer references the `memory` module, which has been moved to the separate `tinyagents-session` crate. The comment now correctly describes that transcript persistence lives in that external crate rather than being part of the harness itself. Auto-committed-on: dragonfly Co-authored-by: Medulla --- crates/tinyagents-harness/src/lib.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/tinyagents-harness/src/lib.rs b/crates/tinyagents-harness/src/lib.rs index d1f1c918..b7adebe8 100644 --- a/crates/tinyagents-harness/src/lib.rs +++ b/crates/tinyagents-harness/src/lib.rs @@ -29,10 +29,9 @@ //! //! # Host utilities not on the agent loop path (M-10) //! -//! [`handoff`] and the [`memory`] module's -//! [`memory::ChatHistory`]/[`memory::ShortTermMemory`] are exported for a -//! host to build on, but [`agent_loop`] does not call into any of them on its -//! own — they are opt-in plumbing, not implicit loop behavior. (A +//! [`handoff`] is exported for a host to build on, but [`agent_loop`] does +//! not call into it on its own — it is opt-in plumbing, not implicit loop +//! behavior. (A //! [`run_queue::RunQueue`] *is* drained by the loop once attached via //! [`context::RunContext::with_run_queue`]; see that module's docs.) //! @@ -40,9 +39,10 @@ //! results; a host calls [`handoff::apply_handoff`] itself before //! appending a tool result to history, and registers an extraction tool //! that reads the same [`handoff::ResultHandoffCache`]. -//! - [`memory::ChatHistory`]/[`memory::ShortTermMemory`] persist a thread's -//! transcript across runs; a host reads history into a run's `input` and -//! appends the run's messages back afterward. +//! +//! Persisting a thread's transcript across runs lives in the separate +//! `tinyagents-session` crate: a host reads history into a run's `input` and +//! appends the run's messages back afterward. //! //! Wiring any of these directly into the loop is deliberately future work //! rather than default behavior, so a host that does not need one pays From 79e0a9a40199d657de49b06cfc7d225d95771e92 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Wed, 23 Sep 2026 12:20:39 +0300 Subject: [PATCH 07/12] fix: use absolute paths in doc comments for CombinedToolSet and PrefixedToolSet The doc comments for `CombinedToolSet` and `PrefixedToolSet` used relative `super::` paths to reference each other, which would break when the documentation is rendered outside the module hierarchy. Changed both references to use the full `crate::` path so the links resolve correctly in generated documentation. Auto-committed-on: dragonfly Co-authored-by: Medulla --- crates/tinyagents-harness/src/tool/toolset/combined/types.rs | 2 +- crates/tinyagents-harness/src/tool/toolset/prefixed/types.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/tinyagents-harness/src/tool/toolset/combined/types.rs b/crates/tinyagents-harness/src/tool/toolset/combined/types.rs index 7d6df73b..34a368ee 100644 --- a/crates/tinyagents-harness/src/tool/toolset/combined/types.rs +++ b/crates/tinyagents-harness/src/tool/toolset/combined/types.rs @@ -12,7 +12,7 @@ use crate::tool::toolset::ToolSet; /// dispatches to the **first** member (in registration order) that /// currently exposes the requested name. Name collisions across members are /// the caller's responsibility to avoid — wrap a member in -/// [`super::PrefixedToolSet`] first when its names might clash with +/// [`crate::tool::toolset::PrefixedToolSet`] first when its names might clash with /// another's. pub struct CombinedToolSet { pub(crate) members: Vec>>, diff --git a/crates/tinyagents-harness/src/tool/toolset/prefixed/types.rs b/crates/tinyagents-harness/src/tool/toolset/prefixed/types.rs index 4c801345..5a8572c7 100644 --- a/crates/tinyagents-harness/src/tool/toolset/prefixed/types.rs +++ b/crates/tinyagents-harness/src/tool/toolset/prefixed/types.rs @@ -9,7 +9,7 @@ use crate::tool::toolset::ToolSet; /// /// Mirrors Pydantic AI's `.prefixed('weather')` (`docs/runtime-comparison/ /// pydantic-ai.md` §3.4): the primary use is collision avoidance when -/// [`super::CombinedToolSet`] merges toolsets whose member names might +/// [`crate::tool::toolset::CombinedToolSet`] merges toolsets whose member names might /// otherwise clash (two MCP servers each exposing a `search` tool, say). pub struct PrefixedToolSet { pub(crate) inner: Arc>, From a894a252d3f416287e7031486cf09f35dc48bc98 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Wed, 23 Sep 2026 12:21:01 +0300 Subject: [PATCH 08/12] ci(workflows): enable all features for doc lints and scope unused dependency check The doc lint step now passes `--all-features` to `cargo doc` so that intra-doc links into feature-gated items resolve correctly, and the `continue-on-error` flag has been removed since the previous workaround for broken links is no longer needed. The unused dependency check is now scoped to the `crates` directory to avoid scanning vendored submodules, and its `continue-on-error` flag has been dropped as the workspace findings have been cleaned up. Auto-committed-on: dragonfly Co-authored-by: Medulla --- .github/workflows/ci.yml | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8217c032..e798d971 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -65,21 +65,20 @@ jobs: - name: Clippy all features run: cargo clippy --workspace --all-targets --all-features -- -D warnings - # TODO: drop `continue-on-error` once the ~160 existing broken - # intra-doc link warnings across the workspace are fixed and this can - # block merges instead of only reporting. + # `--all-features` so links into feature-gated items resolve; without + # it rustdoc cannot see the very modules the docs point at. - name: Doc lints env: RUSTDOCFLAGS: -D rustdoc::broken_intra_doc_links - run: cargo doc --workspace --no-deps - continue-on-error: true + run: cargo doc --workspace --no-deps --all-features - # TODO: drop `continue-on-error` once the existing unused-dependency - # findings across the workspace are cleaned up and this can block - # merges instead of only reporting. + # Scoped to `crates`, this repo's own workspace: an unscoped run also + # walks `vendor/`, where it reports findings against the vendored + # submodules that have to be fixed in their own repositories. - name: Unused dependencies uses: bnjbvr/cargo-machete@main - continue-on-error: true + with: + args: crates # Every feature selection links a distinct workspace graph, so they cannot # share a target directory. Previously they ran back to back with a From 3ff4bcefdf9e4c1e0af584710b9a26436164d24d Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Wed, 23 Sep 2026 12:24:46 +0300 Subject: [PATCH 09/12] docs: update doc comments to use full path for EntryKind::Message Updated three doc comments in the legacy module to reference `crate::entry_tree::EntryKind::Message` instead of the previous `Entry::Message` path, ensuring the documentation links resolve correctly after a refactor of the entry tree types. Auto-committed-on: dragonfly Co-authored-by: Medulla --- crates/tinyagents-session/src/entry_tree/legacy.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/tinyagents-session/src/entry_tree/legacy.rs b/crates/tinyagents-session/src/entry_tree/legacy.rs index 7743d9e5..2592b769 100644 --- a/crates/tinyagents-session/src/entry_tree/legacy.rs +++ b/crates/tinyagents-session/src/entry_tree/legacy.rs @@ -17,13 +17,13 @@ use crate::types::SessionMessage; use super::types::{Entry, EntryId, EntryKind}; -/// Derives an append-only linear chain of [`Entry::Message`] nodes from a +/// Derives an append-only linear chain of [`crate::entry_tree::EntryKind::Message`] nodes from a /// parsed JSONL transcript's message array, in file order. pub fn from_transcript(session_id: &str, transcript: &SessionTranscript) -> Vec { from_messages(session_id, &transcript.messages) } -/// Derives an append-only linear chain of [`Entry::Message`] nodes from a +/// Derives an append-only linear chain of [`crate::entry_tree::EntryKind::Message`] nodes from a /// bare message slice, in the given order. pub fn from_messages(session_id: &str, messages: &[TranscriptMessage]) -> Vec { let mut entries = Vec::with_capacity(messages.len()); @@ -49,7 +49,7 @@ pub fn from_messages(session_id: &str, messages: &[TranscriptMessage]) -> Vec Date: Wed, 23 Sep 2026 13:28:44 +0300 Subject: [PATCH 10/12] chore(ci): pin GitHub Actions to commit SHAs Pin all third-party GitHub Actions to specific commit SHAs instead of version tags or branch names, adding the original tag as a comment for readability. This follows supply-chain security best practices by preventing a compromised tag from being silently updated in future workflow runs. Auto-committed-on: dragonfly Co-authored-by: Medulla --- .github/workflows/ci.yml | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e798d971..d1087db7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,7 +37,7 @@ jobs: name: Lint runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: # This job executes repo code (cargo build/test); don't persist the # token in git config. @@ -48,11 +48,12 @@ jobs: # vendor/tinytools/crates/tinytools/Cargo.toml". submodules: recursive - - uses: dtolnay/rust-toolchain@stable + - uses: dtolnay/rust-toolchain@6bed0761d98439e5a578e2877258200ad565ba87 # stable with: + toolchain: stable components: rustfmt, clippy - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: workspaces: . @@ -76,7 +77,7 @@ jobs: # walks `vendor/`, where it reports findings against the vendored # submodules that have to be fixed in their own repositories. - name: Unused dependencies - uses: bnjbvr/cargo-machete@main + uses: bnjbvr/cargo-machete@ac30a525c0a8d163a92d727b3ff079ee3f6ecb08 # v0.9.2 with: args: crates @@ -103,14 +104,16 @@ jobs: - name: multimodal features: "--no-default-features --features multimodal" steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false submodules: recursive - - uses: dtolnay/rust-toolchain@stable + - uses: dtolnay/rust-toolchain@6bed0761d98439e5a578e2877258200ad565ba87 # stable + with: + toolchain: stable - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: workspaces: . # Without a distinct key every matrix leg would race to overwrite one @@ -127,23 +130,26 @@ jobs: name: Coverage runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false submodules: recursive - - uses: dtolnay/rust-toolchain@stable + - uses: dtolnay/rust-toolchain@6bed0761d98439e5a578e2877258200ad565ba87 # stable with: + toolchain: stable components: llvm-tools-preview - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: workspaces: . # Instrumented objects are not interchangeable with the test matrix's, # so coverage keeps its own cache rather than invalidating theirs. key: coverage - - uses: taiki-e/install-action@cargo-llvm-cov + - uses: taiki-e/install-action@7623a79cdfecb99d681017af368ca353d9f49bb5 # v2.87.19 + with: + tool: cargo-llvm-cov - name: Verify line coverage run: >- From 1a331588bf78f012b8cc1b6fea48024379b6e857 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Wed, 23 Sep 2026 13:33:07 +0300 Subject: [PATCH 11/12] chore: files changed crates/tinyagents-session/src/transcript/test.rs Auto-committed-on: dragonfly Co-authored-by: Medulla --- crates/tinyagents-session/src/transcript/test.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/crates/tinyagents-session/src/transcript/test.rs b/crates/tinyagents-session/src/transcript/test.rs index 0a0f0d76..33f98e18 100644 --- a/crates/tinyagents-session/src/transcript/test.rs +++ b/crates/tinyagents-session/src/transcript/test.rs @@ -444,6 +444,15 @@ fn opening_a_generation_that_already_exists_is_refused() { /// `path_lock` both handles' first `append_turn_with_partial` would /// otherwise race on the writer's create-fresh branch and whichever `fs::write` /// lands last would silently discard the other's retained set. +/// +/// The threads rendezvous twice, and the second rendezvous is load-bearing. +/// `begin_generation` refuses to open a successor that already exists, so with +/// only the pre-`begin_generation` barrier a thread that got all the way +/// through its append before the other called `begin_generation` would make +/// that call fail on the existence check — a race in the test itself rather +/// than the contention it means to exercise. Holding both threads until each +/// owns its handle puts the contention where this test is aiming it: on the +/// two first appends. #[test] fn concurrent_begin_generation_handles_for_one_session_never_lose_either_append() { let dir = tempdir().unwrap(); @@ -466,6 +475,8 @@ fn concurrent_begin_generation_handles_for_one_session_never_lose_either_append( let left = std::thread::spawn(move || { left_barrier.wait(); let (_, handle) = left_locator.begin_generation(&left_root, meta()).unwrap(); + // Both handles exist before either append starts. + left_barrier.wait(); handle .append(TranscriptMessage::new("user", "from left")) .unwrap(); @@ -477,6 +488,8 @@ fn concurrent_begin_generation_handles_for_one_session_never_lose_either_append( let right = std::thread::spawn(move || { right_barrier.wait(); let (_, handle) = right_locator.begin_generation(&right_root, meta()).unwrap(); + // Both handles exist before either append starts. + right_barrier.wait(); handle .append(TranscriptMessage::new("user", "from right")) .unwrap(); From 43f19ca6984061fd7625c73e691ba7b76fbb5ccf Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Wed, 23 Sep 2026 13:37:48 +0300 Subject: [PATCH 12/12] fix(ci): use pull request number for concurrency group The concurrency group key was changed from `github.head_ref` to `github.event.pull_request.number` to prevent pull requests from different forks that share the same head branch name from being placed in the same concurrency group, which could cause one PR's required checks to be incorrectly cancelled by another. Auto-committed-on: dragonfly Co-authored-by: Medulla --- .github/workflows/ci.yml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d1087db7..1c179a1c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,9 +10,13 @@ on: pull_request: concurrency: - # `github.head_ref` is set for pull requests and empty for pushes, so a PR's - # runs coalesce under one group and superseded commits cancel each other. - group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} + # Keyed by pull-request *identity*, not by branch name: two PRs from + # different forks can share a head branch name (`main`, `feature`), and + # keying on `github.head_ref` would put them in one group where + # `cancel-in-progress` lets either one cancel the other's required checks. + # `github.event.pull_request.number` is unset for pushes, which fall back to + # the ref. + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true permissions: