harness
v0.5.4Meta-worker that composes the modular Node workers backing the iii chat surface.
full markdown
/workers/harness.md?version=0.5.4. paste it into an llm prompt or pipe it through curl from a worker.install
dependencies
readme
The iii harness
The harness is not a layer on top of your backend. On iii, it is the backend.
Many setups keep the agent loop in one process and everything else (queues, HTTP, state, traces) in another. Tool calls cross that boundary; retries and traces rarely line up.
On iii, agents are workers. Tools are functions. Handoffs use the same triggers and queues as the rest of the system.
This package is the production harness for that model: turn orchestration, approvals, sessions, providers, context compaction, and budgets, all as iii workers next to shell, storage, database, and whatever you add.
Read The Harness Is the Backend by Mike Piccolo for the full argument.
What you get
One trace. Each hop is a trigger() on the bus. Trace IDs propagate across workers, languages, and queue steps. You debug one runtime, not separate logs aligned by timestamp.
Live discovery. Workers register functions on connect; the engine keeps a catalog. Agents and the console see what the system can do today, including workers added without redeploying the orchestrator. Providers self-register; the model catalog fills from discovery, not a hardcoded seed.
Composition, not frameworks. Thin vs thick harnesses map to how many functions you register and how you wire triggers. Fewer functions for a lean loop; approval rules and extra workers for more structure.
New capability, new worker. When the harness needs something else (shell, database, coder, another provider), you add a worker, not a fork of the orchestrator. Published workers install from the iii worker registry with iii worker add ; they register on the iii engine and show up in the live catalog.
Turns, approvals, budgets. Seven-state durable turn FSM with queue-backed steps. Approval gate with YAML permissions, parallel tool batches, pending state across reload, fail-closed when policy is unreachable. Workspace and agent budget caps. Five provider workers behind one registry.
Context compaction. Long sessions exceed model windows. The context-compaction worker compacts history as turns accumulate and backs the console /compact command.
What ships here
Fifteen workers in one TypeScript package, one folder per worker, one feature per file:
| Concern | Workers |
|---|---|
| Orchestration | turn-orchestrator (durable turn FSM), hook-fanout |
| Governance | harness (permissions, provider registry, UI fanout), approval-gate |
| Sessions | session (branching session tree + inbox queues) |
| Context | context-compaction (keeps long sessions inside the model window) |
| Models | models-catalog, provider-anthropic, provider-openai, provider-kimi, provider-lmstudio, provider-llamacpp |
| Cost | llm-budget |
Rust workers (shell, iii-directory) and engine builtins (state::*, stream::*, iii::durable::*) stay on the same bus; this package does not reimplement them.
Quickstart
- Install iii:
curl -fsSL https://install.iii.dev/iii/main/install.sh | sh - Verify the install:
iii --version - Add the harness and console workers:
iii worker add harness console - Start the engine:
iii --config config.yaml - Open the console at
http://127.0.0.1:3113
Chat, approve/deny, model picker, and trace explorer ship in one binary (console).
Tools, orchestration, governance, and observability use the same worker, trigger, and function model as the rest of iii.
Further reading
api reference (json)
{
"functions": [
{
"description": "Append a function id to the per-session always-allow list (idempotent).",
"metadata": {},
"name": "approval::add_always_allow",
"request_schema": {},
"response_schema": {}
},
{
"description": "Remember an \"approve always\" decision for this session — the function id stops prompting in every mode. Idempotent.",
"metadata": {},
"name": "approval::approve_always",
"request_schema": {},
"response_schema": {}
},
{
"description": "Clear the per-session approval settings on conversation deletion.",
"metadata": {},
"name": "approval::clear_settings",
"request_schema": {},
"response_schema": {}
},
{
"description": "Read the per-session approval settings; returns defaults when none persisted.",
"metadata": {},
"name": "approval::get_settings",
"request_schema": {},
"response_schema": {}
},
{
"description": "",
"metadata": {},
"name": "approval::on_harness_config",
"request_schema": {},
"response_schema": {}
},
{
"description": "Remove a function id from the per-session always-allow list.",
"metadata": {},
"name": "approval::remove_always_allow",
"request_schema": {},
"response_schema": {}
},
{
"description": "Flip an approval to allow or deny. Persists the decision to the approvals scope to wake the parked turn.",
"metadata": {},
"name": "approval::resolve",
"request_schema": {},
"response_schema": {}
},
{
"description": "Set the permission mode (manual | auto | full) for a session.",
"metadata": {},
"name": "approval::set_mode",
"request_schema": {},
"response_schema": {}
},
{
"description": "Add an alert to a budget.",
"metadata": {},
"name": "budget::alert_set",
"request_schema": {},
"response_schema": {}
},
{
"description": "Check whether a budget allows an estimated spend.",
"metadata": {},
"name": "budget::check",
"request_schema": {},
"response_schema": {}
},
{
"description": "Create a budget with ceiling + period.",
"metadata": {},
"name": "budget::create",
"request_schema": {},
"response_schema": {}
},
{
"description": "Delete a budget.",
"metadata": {},
"name": "budget::delete",
"request_schema": {},
"response_schema": {}
},
{
"description": "Toggle enforcement on a budget.",
"metadata": {},
"name": "budget::enforce",
"request_schema": {},
"response_schema": {}
},
{
"description": "Add a 24h exemption for a principal.",
"metadata": {},
"name": "budget::exempt",
"request_schema": {},
"response_schema": {}
},
{
"description": "Project spend through period end.",
"metadata": {},
"name": "budget::forecast",
"request_schema": {},
"response_schema": {}
},
{
"description": "Fetch a budget by id.",
"metadata": {},
"name": "budget::get",
"request_schema": {},
"response_schema": {}
},
{
"description": "List budgets, newest first.",
"metadata": {},
"name": "budget::list",
"request_schema": {},
"response_schema": {}
},
{
"description": "Pause or resume a budget.",
"metadata": {},
"name": "budget::pause",
"request_schema": {},
"response_schema": {}
},
{
"description": "Record a spend, fire matching alerts.",
"metadata": {},
"name": "budget::record",
"request_schema": {},
"response_schema": {}
},
{
"description": "Reset spent_usd, archive prior period.",
"metadata": {},
"name": "budget::reset",
"request_schema": {},
"response_schema": {}
},
{
"description": "Update a whitelisted set of budget fields.",
"metadata": {},
"name": "budget::update",
"request_schema": {},
"response_schema": {}
},
{
"description": "Aggregate spend over a window.",
"metadata": {},
"name": "budget::usage",
"request_schema": {},
"response_schema": {}
},
{
"description": "Sync pre-turn compaction triggered by turn-orchestrator when a turn would overflow. Performs prune+summarise+reinject+continue.",
"metadata": {},
"name": "context-compaction::compact_now",
"request_schema": {},
"response_schema": {}
},
{
"description": "User-initiated synchronous compaction of a session. Required: session_id. Optional: model { id, providerID, limit? } to skip auto-resolution. If model is omitted, falls back to (1) most recent assistant message in session-tree, (2) orchestrator run_request.",
"metadata": {},
"name": "context-compaction::compact_session",
"request_schema": {},
"response_schema": {}
},
{
"description": "Internal: subscribes to agent::turn_end; triggers async compaction on TurnEnd when running tokens exceed usable(model).",
"metadata": {},
"name": "context-compaction::on_agent_event",
"request_schema": {},
"response_schema": {}
},
{
"description": "Prune older tool outputs without summarisation (cheap path).",
"metadata": {},
"name": "context-compaction::prune_tool_outputs",
"request_schema": {},
"response_schema": {}
},
{
"description": "Internal: agent::events fanout handler.",
"metadata": {},
"name": "harness::fanout::agent_event_handler",
"request_schema": {},
"response_schema": {}
},
{
"description": "Internal: fans out a newly-created session id to ui::sessions::changed::<browser_id>.",
"metadata": {},
"name": "harness::fanout::session_created",
"request_schema": {},
"response_schema": {}
},
{
"description": "Read a host file via shell::fs::read, drain its channel, and return a {content:[{text}], details:{size, truncated, bytes_read}} envelope (max 256 KiB inline by default).",
"metadata": {},
"name": "harness::fs::read_inline",
"request_schema": {},
"response_schema": {}
},
{
"description": "List providers declared to the harness.",
"metadata": {},
"name": "harness::provider::list",
"request_schema": {},
"response_schema": {}
},
{
"description": "Self-declare an LLM provider (id, config schema, defaults) into the dynamic harness configuration schema.",
"metadata": {},
"name": "harness::provider::register",
"request_schema": {},
"response_schema": {}
},
{
"description": "Resolve a provider credential + settings (api_url, max_tokens) from the harness configuration. Server-side only.",
"metadata": {},
"name": "harness::provider::resolve",
"request_schema": {},
"response_schema": {}
},
{
"description": "Browser kickoff: forward payload to run::start. Used by console/web over the iii-browser-sdk.",
"metadata": {},
"name": "harness::trigger",
"request_schema": {},
"response_schema": {}
},
{
"description": "Publish a topic, collect subscriber replies until timeout, apply merge_rule.",
"metadata": {},
"name": "hook-fanout::publish_collect",
"request_schema": {},
"response_schema": {}
},
{
"description": "Internal: routes agent::hook_reply stream events to pending publish_collect calls.",
"metadata": {},
"name": "hook-fanout::reply_handler",
"request_schema": {},
"response_schema": {}
},
{
"description": "Look up a single model by (provider, model_id). Returns null when no provider has registered it.",
"metadata": {},
"name": "models::get",
"request_schema": {},
"response_schema": {}
},
{
"description": "List models, optionally filtered by provider or capability. Returns only models registered by providers (no embedded seed).",
"metadata": {},
"name": "models::list",
"request_schema": {},
"response_schema": {}
},
{
"description": "Write a model to iii state under models:<provider>:<id>.",
"metadata": {},
"name": "models::register",
"request_schema": {},
"response_schema": {}
},
{
"description": "Check whether a provider-registered model supports a capability (false when unknown).",
"metadata": {},
"name": "models::supports",
"request_schema": {},
"response_schema": {}
},
{
"description": "Check a function call against iii-permissions.yaml; returns allow, deny, or needs_approval.",
"metadata": {},
"name": "policy::check_permissions",
"request_schema": {},
"response_schema": {}
},
{
"description": "Legacy: drain a streamed Anthropic completion and return the final AssistantMessage.",
"metadata": {},
"name": "provider::anthropic::complete",
"request_schema": {},
"response_schema": {}
},
{
"description": "Re-pull the Anthropic model list (GET /v1/models) and register each into the iii models catalog. Idempotent.",
"metadata": {},
"name": "provider::anthropic::refresh_models",
"request_schema": {},
"response_schema": {}
},
{
"description": "Stream a single assistant turn from Anthropic into the caller-supplied channel. Each AssistantMessageEvent is sent as a JSON text message; the terminal event is Done or Error followed by close.",
"metadata": {},
"name": "provider::anthropic::stream",
"request_schema": {},
"response_schema": {}
},
{
"description": "Legacy: drain a streamed Kimi chat-completion and return the final AssistantMessage.",
"metadata": {},
"name": "provider::kimi::complete",
"request_schema": {},
"response_schema": {}
},
{
"description": "Re-pull the Kimi (Moonshot) model list (GET /v1/models) and register each into the iii models catalog. Idempotent.",
"metadata": {},
"name": "provider::kimi::refresh_models",
"request_schema": {},
"response_schema": {}
},
{
"description": "Stream a single assistant turn from Kimi (Moonshot) Chat Completions into the caller-supplied channel.",
"metadata": {},
"name": "provider::kimi::stream",
"request_schema": {},
"response_schema": {}
},
{
"description": "Legacy: drain a streamed llama-server chat-completion and return the final AssistantMessage.",
"metadata": {},
"name": "provider::llamacpp::complete",
"request_schema": {},
"response_schema": {}
},
{
"description": "Re-discover the loaded llama-server model and register it into the iii models catalog. Idempotent.",
"metadata": {},
"name": "provider::llamacpp::refresh_models",
"request_schema": {},
"response_schema": {}
},
{
"description": "Stream a single assistant turn from a local llama-server Chat Completions server into the caller-supplied channel.",
"metadata": {},
"name": "provider::llamacpp::stream",
"request_schema": {},
"response_schema": {}
},
{
"description": "Legacy: drain a streamed LM Studio chat-completion and return the final AssistantMessage.",
"metadata": {},
"name": "provider::lmstudio::complete",
"request_schema": {},
"response_schema": {}
},
{
"description": "Load an LM Studio model into memory via POST /api/v1/models/load. Blocks until ready (up to 120s).",
"metadata": {},
"name": "provider::lmstudio::load_model",
"request_schema": {},
"response_schema": {}
},
{
"description": "Re-discover loaded LM Studio models and register each into the iii models catalog. Idempotent.",
"metadata": {},
"name": "provider::lmstudio::refresh_models",
"request_schema": {},
"response_schema": {}
},
{
"description": "Stream a single assistant turn from a local LM Studio Chat Completions server into the caller-supplied channel.",
"metadata": {},
"name": "provider::lmstudio::stream",
"request_schema": {},
"response_schema": {}
},
{
"description": "Unload an LM Studio model instance via POST /api/v1/models/unload.",
"metadata": {},
"name": "provider::lmstudio::unload_model",
"request_schema": {},
"response_schema": {}
},
{
"description": "Legacy: drain a streamed OpenAI chat-completion and return the final AssistantMessage.",
"metadata": {},
"name": "provider::openai::complete",
"request_schema": {},
"response_schema": {}
},
{
"description": "Re-pull the OpenAI model list (GET /v1/models) and register the chat-capable subset into the iii models catalog. Idempotent.",
"metadata": {},
"name": "provider::openai::refresh_models",
"request_schema": {},
"response_schema": {}
},
{
"description": "Stream a single assistant turn from OpenAI Chat Completions into the caller-supplied channel.",
"metadata": {},
"name": "provider::openai::stream",
"request_schema": {},
"response_schema": {}
},
{
"description": "Start a durable agent session and return immediately.",
"metadata": {},
"name": "run::start",
"request_schema": {},
"response_schema": {}
},
{
"description": "Atomically read and clear all items in a session-scoped inbox.",
"metadata": {},
"name": "session-inbox::drain",
"request_schema": {},
"response_schema": {}
},
{
"description": "Read all items in a session-scoped inbox without mutating.",
"metadata": {},
"name": "session-inbox::peek",
"request_schema": {},
"response_schema": {}
},
{
"description": "Append an item to a session-scoped inbox.",
"metadata": {},
"name": "session-inbox::push",
"request_schema": {},
"response_schema": {}
},
{
"description": "Append an AgentMessage entry to a session",
"metadata": {},
"name": "session-tree::append",
"request_schema": {},
"response_schema": {}
},
{
"description": "Append a synthetic user-role message entry to a session",
"metadata": {},
"name": "session-tree::append_synthetic",
"request_schema": {},
"response_schema": {}
},
{
"description": "Duplicate a session with re-mapped ids",
"metadata": {},
"name": "session-tree::clone",
"request_schema": {},
"response_schema": {}
},
{
"description": "Append a Compaction entry summarising the active path",
"metadata": {},
"name": "session-tree::compact",
"request_schema": {},
"response_schema": {}
},
{
"description": "Return all compaction entries for a session, sorted by timestamp ascending",
"metadata": {},
"name": "session-tree::compactions",
"request_schema": {},
"response_schema": {}
},
{
"description": "Create a new empty session record",
"metadata": {},
"name": "session-tree::create",
"request_schema": {},
"response_schema": {}
},
{
"description": "Idempotently ensure a session exists with the given id",
"metadata": {},
"name": "session-tree::ensure",
"request_schema": {},
"response_schema": {}
},
{
"description": "Render the active path as a self-contained HTML document",
"metadata": {},
"name": "session-tree::export_html",
"request_schema": {},
"response_schema": {}
},
{
"description": "Fork a session at a given entry into a new session id",
"metadata": {},
"name": "session-tree::fork",
"request_schema": {},
"response_schema": {}
},
{
"description": "List sessions with optional pagination and ordering",
"metadata": {},
"name": "session-tree::list",
"request_schema": {},
"response_schema": {}
},
{
"description": "Load every AgentMessage on the active path of a session, paired with its entry_id, oldest first",
"metadata": {},
"name": "session-tree::messages",
"request_schema": {},
"response_schema": {}
},
{
"description": "Mirror missing messages from a state-snapshot into session-tree",
"metadata": {},
"name": "session-tree::reconcile",
"request_schema": {},
"response_schema": {}
},
{
"description": "Return the session tree as a nested TreeNode",
"metadata": {},
"name": "session-tree::tree",
"request_schema": {},
"response_schema": {}
},
{
"description": "Replace content of a function_result message entry with compacted output",
"metadata": {},
"name": "session-tree::update_part",
"request_schema": {},
"response_schema": {}
},
{
"description": "Batch replace content of multiple function_result message entries. Loads entries once.",
"metadata": {},
"name": "session-tree::update_parts",
"request_schema": {},
"response_schema": {}
},
{
"description": "Run one durable FSM transition for session in state assistant_streaming: start turn, stream provider response, finalize, and route onward.",
"metadata": {},
"name": "turn::assistant_streaming",
"request_schema": {},
"response_schema": {}
},
{
"description": "Run one durable FSM transition for session in state function_awaiting_approval: execute each call as its approval decision arrives.",
"metadata": {},
"name": "turn::function_awaiting_approval",
"request_schema": {},
"response_schema": {}
},
{
"description": "Run one durable FSM transition for session in state function_execute: dispatch prepared calls and finalize results.",
"metadata": {},
"name": "turn::function_execute",
"request_schema": {},
"response_schema": {}
},
{
"description": "Read the current turn_state record for a session. Returns null if the session is unknown. UI clients use this on page reload to recover any in-progress modals (e.g. function_awaiting_approval) without reading iii state directly.",
"metadata": {},
"name": "turn::get_state",
"request_schema": {},
"response_schema": {}
},
{
"description": "State trigger on scope=approvals; enqueues turn::function_awaiting_approval when a decision is written.",
"metadata": {},
"name": "turn::on_approval",
"request_schema": {},
"response_schema": {}
},
{
"description": "Run one durable FSM transition for session in state provisioning: build the system prompt, attach the agent_trigger function schema, advance to assistant_streaming.",
"metadata": {},
"name": "turn::provisioning",
"request_schema": {},
"response_schema": {}
},
{
"description": "Run one durable FSM transition for session in state steering_check: drain inboxes and route onward.",
"metadata": {},
"name": "turn::steering_check",
"request_schema": {},
"response_schema": {}
},
{
"description": "Register a browser's interest in a session (or all sessions if session_id is null).",
"metadata": {},
"name": "ui::subscribe",
"request_schema": {},
"response_schema": {}
},
{
"description": "Remove a browser's subscription to a session (or its all-sessions sub if session_id is null).",
"metadata": {},
"name": "ui::unsubscribe",
"request_schema": {},
"response_schema": {}
},
{
"description": "Fetch a URL over HTTP(S) and return the response as a structured envelope. Use this INSTEAD of `shell::exec` with curl for any HTTP request — it returns {ok, status, headers, body} as JSON, enforces size/timeout caps, and blocks private / cloud-metadata / link-local addresses server-side (SSRF guard; loopback is allowed by default for harness dev workflows). For JSON: pass `json: {...}` (auto-stringifies + sets content-type) and `response_format: \"json\"` (auto-parses response into the `json` field). Method is case-insensitive. On failure returns `{ok:false, error, message}` where `error` is one of: invalid_payload, invalid_url, blocked_host, timeout, too_many_redirects, transport_error. Branch on `error`, not on text.",
"metadata": {},
"name": "web::fetch",
"request_schema": {},
"response_schema": {}
}
],
"triggers": []
}