Skip to main content

harness

The durable agent turn loop — kick off a turn with `harness::send`, render it from session-manager transcript events, react to `harness::turn-completed`, with deny-by-default tool dispatch and synchronous hook extension points for policy siblings.

Quellinformationen

Repository
iii-hq/workers
Letzte Quellaktivität
7. Oktober 2026 um 21:11
Erkannte Sprache von SKILL.md
Englisch
Sterne
114
Forks
26

Installationsoptionen

Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.

Quelldateien prüfen

Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.

Datei-Explorer
2 Dateien

SKILL.md wird angezeigt

SKILL.md
Quellanweisungen · Schreibgeschützte Vorschau
name
harness
description
The durable agent turn loop — kick off a turn with `harness::send`, render it from session-manager transcript events, react to `harness::turn-completed`, with deny-by-default tool dispatch and synchronous hook extension points for policy siblings.
# harness The harness is the durable turn loop that wires `session-manager`, `llm-router`, and `context-manager` into an agent. A consumer stays thin: it kicks off a turn, renders the conversation from the session transcript, and reacts to turn boundaries and human-gated calls. The harness streams the assistant message into the session as it generates, so you watch the session, not the harness — there is no `agent::events` stream, and `harness::status` is a point-in-time recovery read, not a render feed. Every invocation is a trigger (`iii.trigger({ function_id, payload })`); there is no separate "call" verb. Tool dispatch is deny-by-default: a send with no `options.functions.allow` is a plain chat loop and every model-requested call is refused until you allow globs per send. Sessions are minted by the harness (`s_<uuid>`) or supplied by you; a send into a running turn folds in as steering (`merged: true`) instead of erroring, and a repeated `idempotency_key` returns the original turn without appending. A session-creating send names a `model`, or only a `provider`: the harness then starts on that provider's declared default model (`default_model` in `router::provider::list`, checked against its live catalog; a provider that reports none fails the send as before). A new session that names neither `thinking_level` nor `provider_options` also starts on the provider's `default_thinking_level`; an explicit level wins, and later sends into the session inherit the prior turn's as usual. `thinking_level` is `off`, `minimal`, `low`, `medium`, `high` or `xhigh`; `off` is honoured only on models whose catalog entry says `supports_thinking_off: true`; elsewhere the fallback is the provider's own (most send their lowest effort and warn, Kimi ignores the level). Prerequisites: `session-manager` (required — transcript store and change feed) and `llm-router` (required — generation and the model catalog) must be present. `context-manager` (token budgeting and compaction) is a soft dependency — absent it, the harness sends raw history. `approval-gate` (the human-in-the-loop gate) is optional; without it no call is held and every allowed call runs un-gated. ## When to Use - Start or steer an agent turn and return immediately (`harness::send`). - Cancel an in-flight turn (`harness::stop`) or read coarse turn state for recovery and guards (`harness::status`). - Chain turns or react to outcomes by binding `harness::turn-completed`. - Drive a turn from an arbitrary inbound event (cron tick, webhook, sensor) by translating it into a `harness::send`. ## Boundaries - Not a transcript feed. Render from `session-manager`'s `session::message-added` / `message-updated` / `status-changed` (reconcile by `revision`); do not poll `harness::status` for content. - Not the approvals engine. The harness ships only the gate mechanics; the policy, decision RPCs (`approval::resolve`), inbox (`approval::list-pending`), and prompt triggers live in `approval-gate`. - Not a chain guard. `options.max_turns` bounds a single turn, not a send-completed-send loop; carry your own stop condition. - Do not trigger the internal functions (below) — they forge call ids and turn progress, so calling them out of band corrupts the turn record. - An in-run agent cannot start turns: `send` / `run` / `turn` / `stop` are denied to the model by policy. `harness::spawn` is the only turn-starter an agent calls directly, and it self-enforces depth, fan-out, and policy subsetting. The other path is event-driven: an agent binds `engine::register_trigger` straight to `harness::spawn` (see Reactive triggers), with the spawn spec in the registration `metadata`, and the engine spawns the sub-agent when the event fires. ## Functions Consumer-facing: - `harness::send` — ensure the session, persist the incoming message, and kick off a turn; returns fast or merges into a running turn (steering). - `harness::stop` — request cancellation of an in-flight turn; cascades to spawned children. - `harness::status` — read the current turn state for a session; `null` when no turn ever ran. For recovery and guards, not rendering. - `harness::spawn` — spawn a sub-agent in a child session. Model-facing (invoked through `agent_trigger`), not a consumer entry point. - `harness::ask` — put a decision with discrete options to the user as a clickable card (1–4 questions, 2–4 options each). Model-facing (invoked through `agent_trigger`) and answered by the turn loop itself: the turn ends on the question and the answer arrives as the user's next message. Refused in sub-agents, structured-output turns, and for a second ask in one step. Internal — the harness drives these; never trigger them directly: `harness::turn` (the durable loop step), `harness::function::trigger` / `harness::function::resolve` (dispatch and parked-call settle), `harness::sweep-pending` (cron expiry), and `harness::on-config-change` (hot-reload). ## Filesystem scope `options.metadata.fs_scope.root` on a send is the session's working directory; a later send that omits `fs_scope` keeps it. The harness stamps a trusted `fs_scope { root, grants, boundary }` onto every `shell::*` / `coder::*` call and strips any the model supplies. `boundary` decides what `root` means to the `ide` worker: - `workspace` — `coder::*`, `shell::fs::*` and an exec `cwd` stay inside `root` plus the session's grants (`harness::filesystem::grant`). - `configured_roots` — `root` only anchors relative paths; the worker's own roots apply. The model's prompt says so ("default directory, not an access boundary"). The `filesystem_boundary` config picks it: `auto` (the default) is `workspace` only while approval-gate's access watch is bound; `workspace` or `configured_roots` pins it. `harness::filesystem::info` reports the boundary in effect. A sub-agent spawned in a turn into a new session starts with a copy of its parent's grants; later grants to the parent do not reach it. Under `workspace`, an in-turn spawn's `options.filesystem_root` must lie inside the parent's root or grants. What an exec'd process itself writes is the `ide` worker's `fs.exec_confinement` switch. ## Reactive triggers The harness emits two async turn-boundary trigger types so consumers and siblings react without polling `harness::status`: - `harness::turn-started` — a turn began executing (first loop step). - `harness::turn-completed` — a turn reached a terminal status (`completed` / `cancelled` / `failed`), carrying the result or error for chaining, failure toasts, auto-titling, and result delivery. Bind `turn-completed` for outcomes and to chain the next hop; bind `turn-started` only for observability. Delivery is fire-and-forget, at-least-once, and unordered — treat each event as an edge. Nothing replays on reconnect: re-seed with `harness::status` and `approval::list-pending`, then rebind. Do not bind these for live transcript rendering — that is `session-manager`'s job. Binding `config` filters delivery by `session_id`, or by `parent_session_id` to watch the children a turn `spawn`s (in-turn spawns only — a direct `harness::spawn` call creates no parent link, so filter those by `session_id`). Bind an event to a sub-agent with one call — the spawn spec goes in the registration `metadata`; the fired event is appended to the task: ```json engine::register_trigger { "trigger_type": "harness::turn-completed", "config": { "session_id": "<child>" }, "function_id": "harness::spawn", "metadata": { "task": "Summarize the completed run.", "once": true } } ``` `model` defaults to the registering turn's model. A failed upstream turn does not fire the reaction unless `continue_on_error: true`. Reactive chains stop at depth 8. There is no fan-in join primitive — use the workflow worker to join parallel work. An in-session registration that omits `model` also inherits its provider, when the spec pins none; raw engine-side registrations have no turn to inherit from and must pass one, and any supplied `model` must be a live id from `router::models::list` (validated at registration and again at fire time). Omit `parent_session_id` and the child nests under the registering session's root automatically; pin it only to choose a different REAL session (an invented id shows the children as disconnected roots). A trigger-fired spawn has no parent policy to inherit — it gets the harness's read-only `default_functions` baseline unless `options.functions` grants more (narrowing only, same as a direct `harness::spawn` call). Filter `turn-completed` only by session ids you know exist; registration returns a warning `note` when the filtered session doesn't. Self-edge drop and the depth cap are the ONLY loop breakers — still design filters so a reaction is not matched by its own subscription. A cycle routed through a state write (or any other hop that starts a fresh turn) re-enters at depth 0 and is NOT throttled: every lap around the cycle spawns another paid turn indefinitely. Design reaction graphs acyclically. This is the in-run agent's chaining path; the `registerFunction` recipe below is for workers. ### How to bind 1. Register a handler: `registerFunction('myapp::on-turn-done', handler)`. 2. Register the trigger: ```typescript iii.registerTrigger({ type: 'harness::turn-completed', function_id: 'myapp::on-turn-done', config: { session_id: sessionId }, }) ``` For the event payload shape, call `get function info` on the trigger type. ### Hooks (policy siblings only) The harness also registers five synchronous, in-path hook trigger types: `harness::hook::pre-turn`, `harness::hook::pre-generate`, `harness::hook::post-generate`, `harness::hook::pre-trigger`, and `harness::hook::post-trigger`. A bound hook runs in the turn's critical path and the harness acts on its return value (veto / hold / mutate) under a per-binding `timeout_ms` and `on_error` policy; `pre-trigger` / `post-trigger` bindings take a `functions` glob list to scope which calls they gate. These are for operator-trusted policy siblings (`approval-gate` binds `pre-trigger`); ordinary consumers do not bind hooks.
Auf GitHub ansehen