harness
v1.0.3Thin durable turn loop that wires session-manager, context-manager, and llm-router into an agent loop; spawns sub-agents as child sessions.
- macOS: arm64 · x64
- Linux: arm64 · armv7 · x64
- Windows: arm64 · x64 · x86
exact versions are immutable; binary and bundle artifacts are digest-pinned.
readme
open as markdownharness
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 harnessiii 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-managerThe 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(())
}Want a typed result back? Add an output contract
(options.output: { type: "json", schema }) and read the result off the
harness::turn-completed event — bind it filtered to
your session and the result arrives when the turn finishes.
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 sweepOther 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 is combined with the built-in prompt per
options.system_prompt_strategy: override (default) uses it verbatim, while
enrich appends it to the built-in prompt. 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.