harness-rs-loop 0.0.48

ReAct agent loop for harness-rs with measured guards: stuck detection, acceptance checks, a tool-result ceiling that spills oversized payloads to disk instead of truncating, per-call tool deadlines, subagent isolation, and session record/replay (JSONL).
Documentation

harness-rs-loop

crates.io

The ReAct agent loop for harness-rs: think → call tools → observe → self-correct, repeat until the model stops or the iteration budget runs out. Also home to subagent isolation and session record/replay.

What the loop does each iteration

  1. Applies Guides (system context) — once at the start.
  2. Sends the Context (with available tools) to the Model.
  3. Dispatches every returned tool call through the ToolRegistry.
  4. Runs Sensors after each action — auto-fix patches are applied to the World directly; blocking signals are fed back to the model to retry.
  5. Hooks wrap each step (PreToolUse / PostToolUse / TaskCompleted).
  6. When the model stops asking for tools, acceptance checks decide whether the work is actually done (a run that narrates a plan and stops looks exactly like one that did the job); a failed verdict goes back to the model and the loop continues. Otherwise stops at max_iters.

Guards (each one measured, see the workspace README's ablation)

  • Stuck detection (StuckPolicy) — repeated identical tool-call rounds get a "change your approach" nudge, then a clean Outcome::Stuck.
  • Result ceiling with spill (ToolResultPolicy) — a single tool result larger than max_bytes (default 24 KiB) is saved in full to .harness/spill/ inside the workspace; the context gets a 4 KiB preview plus the path, and the model retrieves slices with its own read_file/grep. Nothing is lost. spill: false falls back to destructive truncation.
  • Per-call tool deadline (with_tool_timeout, default 120s) — a hung call becomes an error result the model can route around, not a hung run.
  • Acceptance (with_acceptance) — cheap deterministic "is it done?" checks (e.g. FilesExist) consulted before Outcome::Done.

Usage

use harness_loop::{AgentLoop, Outcome};
use harness_models::OpenAiCompat;
use harness_context::default_world;
use harness_core::Task;
use harness_tools_fs::{ReadFile, ListDir};
use std::sync::Arc;

let model = OpenAiCompat::with_key("https://api.deepseek.com", "deepseek-chat", key);
let mut world = default_world(".");

let outcome = AgentLoop::new(model)
    .with_tool(Arc::new(ReadFile))
    .with_tool(Arc::new(ListDir))
    .run_with_max_iters(
        Task { description: "summarize the workspace".into(), source: None, deadline: None },
        &mut world, 12,
    )
    .await?;

match outcome {
    Outcome::Done { text, iters, tools_called, usage, .. } =>
        println!("done in {iters} iters, {tools_called} tools, {} tok", usage.input_tokens),
    Outcome::BudgetExhausted { last_text, .. } => println!("hit budget: {last_text:?}"),
}

Outcome is #[non_exhaustive] — always destructure with a trailing ...

Subagents

Run an isolated child agent with its own tools and budget, returning a single report to the parent — the basis for harness-rs-orchestrator and harness-rs-scheduler:

use harness_loop::{Subagent, SubagentSpec};

let spec = SubagentSpec::new("researcher", task).with_max_iters(8).with_tool(tool);
let report = Subagent::new(harness_core::DynModel(model), spec).run(&mut world).await?;
println!("{:?}", report.text);

Record & replay

Every run can emit a JSONL session log; read_session + SessionStats / format_event_* reconstruct it (the harness trace CLI command reads these).

License

MIT OR Apache-2.0.