skip to content
$worker

harness

v1.0.1

Thin durable turn loop that wires session-manager, context-manager, and llm-router into an agent loop; spawns sub-agents as child sessions.

iiiverified
381 installs87 in 7d3 today
install
$iii worker add harness@1.0.1
  • macOS: arm64 · x64
  • Linux: arm64 · armv7 · x64
  • Windows: arm64 · x64 · x86

exact versions are immutable; binary and bundle artifacts are digest-pinned.

README.md

harness

harness is the thin, durable turn loop that turns a model plus a few iii workers into an agent. It takes an incoming message, persists it, assembles a context, streams a completion, runs any function calls the model requests, and repeats until the turn stops — all as durable, resumable steps so a crash or restart picks up mid-turn. It wires session-manager (transcript), context-manager (token budgeting, soft dependency), and llm-router (generation); install those alongside it for the full loop.

Install

iii worker add harness

iii worker add fetches the binary, registers the harness configuration entry, and the engine starts the worker on the next iii start. Add the workers it wires so the loop has something to call:

iii worker add session-manager
iii worker add llm-router
iii worker add context-manager

The harness enqueues turn steps on the engine's built-in default queue, provided by the iii-queue worker (see engine.config.yaml).

Quickstart

Send a message and let the loop run; render the conversation by binding session-manager's triggers, and observe turn boundaries with harness::turn-completed.

use iii_sdk::{register_worker, InitOptions, TriggerRequest};
use serde_json::json;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let iii = register_worker("ws://localhost:49134", InitOptions::default());

    // Fire-and-return: persists the user message and kicks off a turn.
    let accepted = iii
        .trigger(TriggerRequest {
            function_id: "harness::send".into(),
            payload: json!({
                "message": "Summarise the repo README",
                "model": "claude-sonnet-4",
                "provider": "anthropic",
                "options": { "functions": { "allow": ["shell::*", "fs::*"] } }
            }),
            action: None,
            timeout_ms: Some(10_000),
        })
        .await?;
    println!("{accepted:#?}"); // { session_id, turn_id, accepted: true }
    Ok(())
}

Prefer to call an agent like a function and get a typed result back? Use harness::run (held open until the turn ends) with an output contract:

let result = iii
    .trigger(TriggerRequest {
        function_id: "harness::run".into(),
        payload: json!({
            "message": "Classify: 'Refund not received after 14 days'",
            "model": "claude-sonnet-4",
            "provider": "anthropic",
            "options": { "output": { "type": "json", "schema": {
                "type": "object",
                "properties": { "category": { "type": "string" }, "urgency": { "type": "string" } },
                "required": ["category"]
            } } }
        }),
        action: None,
        timeout_ms: Some(300_000),
    })
    .await?;
// { status: "completed", result: { "category": "billing", "urgency": "high" }, ... }

The agent-facing function surface is deny-by-default: with no functions.allow globs, every model-requested call is refused and the harness is a plain chat loop. Allow functions in per-send (options.functions.allow) and gate them with the optional approval-gate sibling.

The full function reference (every harness::* id and its request/response schema) lives in the code and iii worker info harness.

Building a consumer — a chat UI, a Telegram/WhatsApp bridge, a cron worker, or any event-driven loop on top of the harness? Start with the integration contract in architecture/integration.md: the functions to trigger, the triggers to bind, and the canonical consumer patterns.

Configuration

The harness configuration entry is owned by the configuration worker; every field hot-reloads (no restart). The fields a deployment is most likely to tune:

default_max_turns: 16            # per-turn generate-step cap when a send omits it
default_pending_timeout_ms: 1800000  # parked pending-call (sub-agent / hold) wait guard
max_depth: 3                     # sub-agent depth budget
max_children: 5                  # sub-agent fan-out budget
sweep_expression: "0 * * * * *"  # cron for the pending-call expiry sweep

Other keys (RPC timeouts, stream coalescing, idempotency TTL, validation retries) and their defaults live in src/config.rs.

System prompt

When options.system_prompt is omitted (or empty), the harness assembles the engine-grounded identity prompt at send time: four provider-specific variants (anthropic, openai → gpt, kimi, and a step-by-step default for local runtimes) selected from provider, plus an optional mode (plan | ask | agent) that prepends a short operating-mode paragraph. A non-empty system_prompt override wins verbatim. Prompt bodies live in prompts/ and are tested in src/prompt/tests.rs.

Custom trigger types

The harness emits two async orchestration trigger types siblings and consumers bind to, and registers five synchronous hook points operator-trusted siblings plug into in-path. Bind with the standard two-step pattern.

Trigger type Kind Fires / runs
harness::turn-started async event A turn began executing (first loop step).
harness::turn-completed async event A turn reached a terminal status (completed / cancelled / failed), carrying the result.
harness::hook::pre-turn sync hook First step of a turn, before any model spend. May veto.
harness::hook::pre-generate sync hook After context assembly, before generation. May extend the system prompt, append messages, or veto.
harness::hook::post-generate sync hook After the final assistant message. Observe only.
harness::hook::pre-trigger sync hook After the allow/deny policy passes, before the target runs. May deny, hold, or rewrite arguments.
harness::hook::post-trigger sync hook After the target returns, before the result is persisted. May rewrite the result.

Event configs accept { session_id?, parent_session_id? }; hook configs accept { functions?, priority?, timeout_ms?, on_error? }. See the spec at tech-specs/2026-06-agentic/harness.md for the hook contract and chain semantics.