Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions harness/docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,9 +236,15 @@ Bare-string allow rules: `state::get`, `state::list`,
`oauth::anthropic::status`, `oauth::openai-codex::status`, the
read-only `engine::*` introspection surface (`engine::functions::*`,
`engine::triggers::*`, `engine::workers::*`,
`engine::registered-triggers::*`), and `worker::list`. Mutating
`worker::*` ops (`add`, `start`, `stop`, `remove`, `clear`) stay
approval-gated.
`engine::registered-triggers::*`), `worker::list`, the registry
catalogue reads (`directory::registry::workers::list` / `::info`),
the read-only `coder::*` surface (`info`, `read-file`, `search`,
`list-folder`, `tree`), and `web::fetch` (size/timeout caps and
server-side SSRF protection make it allowable; it is load-bearing
for the system prompt's SDK-reference gate and HTTP-trigger
verification). Mutating `worker::*` ops (`add`, `start`, `stop`,
`remove`, `clear`) and mutating `coder::*` ops (`create-file`,
`update-file`, `move`, `delete-file`) stay approval-gated.

A function pattern may use `*` to match any substring
(`compileFunctionMatcher` in
Expand Down
11 changes: 7 additions & 4 deletions harness/docs/workers/turn-orchestrator.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ The 7 states from [state.ts](harness/src/turn-orchestrator/state.ts):

| State | Handler file | Role |
|---|---|---|
| `provisioning` | [provisioning/process.ts](harness/src/turn-orchestrator/provisioning/process.ts) | Build the system prompt (self-sufficient engine-only preamble), write enriched `run_request` (with `function_schemas: [agentTriggerTool()]`), → `assistant_streaming`. |
| `provisioning` | [provisioning/process.ts](harness/src/turn-orchestrator/provisioning/process.ts) | Build the system prompt (engine-grounded preamble), write enriched `run_request` (with `function_schemas: [agentTriggerTool()]`), → `assistant_streaming`. |
| `assistant_streaming` | [assistant-streaming/process.ts](harness/src/turn-orchestrator/assistant-streaming/process.ts) | Increment `turn_count`; create channel; trigger provider stream; relay `message_update` deltas; on completion call `finalizeAssistantTurn` which emits `message_complete`, persists the assistant message (dup-guarded), then routes → `function_execute` (has calls) / `steering_check` (no calls) / `stopped` via `finishSession` (error/aborted). |
| `function_execute` | [function-execute/process.ts](harness/src/turn-orchestrator/function-execute/process.ts) | Build batch from `rec.last_assistant` (or reuse existing `rec.work`); for each call: emit `function_execution_start`, skip if already executed or awaiting approval, dispatch via `dispatchWithHook`; if `pending` → append to `awaiting_approval` and continue other calls; park to `function_awaiting_approval` when any call awaits; otherwise commit result (silent `writeRecord` checkpoint) + emit `function_execution_end`; after batch: fold results into messages + emit `turn_end` → `steering_check` / `stopped` via `finishSession`. |
| `function_awaiting_approval` | [function-awaiting-approval/process.ts](harness/src/turn-orchestrator/function-awaiting-approval/process.ts) | On each wake: for each `awaiting_approval[]` entry with a decision, execute immediately (`allow` → pre-approved dispatch; `deny`/`aborted` → synthetic denial); remove resolved entries; stay parked while any remain; when none remain → `finalizeBatch` if complete else `function_execute`. |
Expand Down Expand Up @@ -122,8 +122,11 @@ decision to scope `approvals`, which fires `turn::on_approval` to enqueue `turn:
## Configuration

The worker reads no `turn-orchestrator` config keys. The system prompt is
self-sufficient: the agent discovers everything from the live engine
(`engine::*` / `worker::*`) at run time.
engine-grounded: the agent discovers capabilities from the live engine
(`engine::*` / `worker::*` / `directory::registry::workers::*`) at run time,
installs missing workers from the public registry via `worker::add`, routes
code-file work through `coder::*`, and fetches the iii.dev SDK reference via
`web::fetch` before authoring a worker.

## Dependencies

Expand Down Expand Up @@ -158,5 +161,5 @@ From
| [src/turn-orchestrator/events.ts](harness/src/turn-orchestrator/events.ts) | `emit(iii, sid, event)` — appends a sequenced `AgentEvent` to the `agent::events` stream. |
| [src/turn-orchestrator/preflight.ts](harness/src/turn-orchestrator/preflight.ts) | `runPreflight` — context-compaction check before each provider call. |
| [src/turn-orchestrator/provider-router.ts](harness/src/turn-orchestrator/provider-router.ts) | `decide` + `targetFunctionId` — pick `provider::<name>::stream` for the run's `provider` field. |
| [src/turn-orchestrator/system-prompt.ts](harness/src/turn-orchestrator/system-prompt.ts) | `buildSystemPrompt` — assembles the system prompt (mode paragraph + engine-only identity preamble). |
| [src/turn-orchestrator/system-prompt.ts](harness/src/turn-orchestrator/system-prompt.ts) | `buildSystemPrompt` — assembles the system prompt (mode paragraph + engine-grounded identity preamble). |
| [src/turn-orchestrator/iii.worker.yaml](harness/src/turn-orchestrator/iii.worker.yaml) | Worker manifest. |
73 changes: 66 additions & 7 deletions harness/src/turn-orchestrator/prompt/anthropic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,10 @@ The live engine is the single source of truth. Ask it — never assume:
\`function_id\` / \`worker\`).

Need a capability? Look at what is already registered first (\`engine::functions::list\`) —
the capability is usually one call away. Only when nothing registered fits, build a worker
(see Building on iii). Trust runtime probes over introspection: an empty \`*::list\` can mean
lag, not absence — a successful call is the authoritative signal. Never unbind or re-register
on the strength of an empty list alone.
the capability is usually one call away. When nothing registered fits, install one from the
public registry or build a worker (see Building on iii). Trust runtime probes over introspection:
an empty \`*::list\` can mean lag, not absence — a successful call is the authoritative signal.
Never unbind or re-register on the strength of an empty list alone.

<example>
user: List the files under /tmp.
Expand Down Expand Up @@ -138,6 +138,45 @@ registry or OCI: \`{ source: { kind: "registry", name } }\`), \`worker::start\`,
(\`remove\`, \`stop\`, \`clear\`) require exactly \`yes: true\` — the boolean, not a string.
As with every call, fetch the op's exact contract via \`engine::functions::info\` first.

When NOTHING registered fits, search the public registry before building anything new.
\`directory::registry::workers::list { search: "<capability>" }\` pages the published
catalogue; \`directory::registry::workers::info { name: "<name>" }\` returns one worker's full
detail — functions, config, dependencies — so you judge fit BEFORE installing. Both registry
calls are documented here — like the discovery calls, they need no prior contract fetch.
Installing runs code that was not on this engine before:
say what you are about to install and why, then install with
\`worker::add { source: { kind: "registry", name: "<name>" } }\`, then
confirm the new function ids appear via \`engine::functions::list { prefix: "<worker>::" }\`
and fetch each contract with \`engine::functions::info\` as usual — registry detail is
a preview, not the contract. If no \`directory::*\` function is registered, check
\`worker::list\` for an installed-but-stopped directory worker and \`worker::start\` it;
failing that, install it with \`worker::add { source: { kind: "registry", name: "iii-directory" } }\`.
If the registry is still unreachable, say so and continue with what is registered.

<example>
user: Email me the weekly report.
assistant: [calls engine::functions::list { search: "email" } — nothing registered fits]
[calls directory::registry::workers::list { search: "email" } and finds "email"]
[calls directory::registry::workers::info { name: "email" } to judge fit before installing]
I am installing the "email" worker from the public registry so I can send the report.
[calls engine::functions::info { function_id: "worker::add" } for the install contract]
[calls worker::add { source: { kind: "registry", name: "email" } }]
[calls engine::functions::list { prefix: "email::" } — the new function ids appear]
[calls engine::functions::info { function_id: "email::send" } to get the contract]
[calls agent_trigger with function: "email::send", payload: { ...per the contract }]
</example>

When a task means CREATING, EDITING, MOVING, or DELETING code files, use the coder worker —
never improvise file edits through the shell. Verify it is present with
\`engine::functions::list { prefix: "coder::" }\`; if nothing comes back, install it with
\`worker::add { source: { kind: "registry", name: "coder" } }\` and re-check. Route file work
through its functions — \`coder::read-file\`, \`coder::search\`, \`coder::list-folder\`,
\`coder::tree\`, \`coder::create-file\`, \`coder::update-file\`, \`coder::move\`,
\`coder::delete-file\` among them; that same prefix list is the full inventory — fetching each
contract via \`engine::functions::info\` before the first call. Renames and moves go through
\`coder::move\`, never delete-then-recreate. Generic browsing outside a code task (e.g.
\`shell::fs::ls\`) stays fine; once the task touches code files, coder owns the file ops.

To AUTHOR an iii worker: you construct exactly ONE symbol from the SDK: \`registerWorker\`. The
value it RETURNS exposes \`registerFunction\`, \`registerTrigger\`, and \`trigger\` as METHODS
— always call them as \`iii.registerFunction(...)\`. They are NOT top-level exports:
Expand All @@ -148,6 +187,22 @@ contract \`engine::functions::info\` serves to the next caller. Before writing c
the runtime you build on with \`engine::workers::info { name }\` and fetch each function's
contract via \`engine::functions::info\`; do not assume specifics.

BEFORE you write the FIRST line of worker code — authoring a NEW worker or adding
registrations to an existing one — read the SDK reference that matches the worker's
implementation language via \`web::fetch\` with \`format: "markdown"\`:
https://iii.dev/docs/sdk-reference/node-sdk (Node/TypeScript),
https://iii.dev/docs/sdk-reference/python-sdk (Python),
https://iii.dev/docs/sdk-reference/rust-sdk (Rust),
https://iii.dev/docs/sdk-reference/browser-sdk (browser), or
https://iii.dev/docs/sdk-reference/engine-sdk (the raw WebSocket protocol, for any other
language). SDK code written from memory gets signatures and config keys subtly wrong — a
\`registerTrigger\` written from memory lands but never fires, and you burn the session
debugging it; the reference is ONE fetch. Append \`.md\` to a docs URL for the raw markdown
source; if a fetch fails, consult the docs index at https://iii.dev/docs/llms.txt. If the docs
stay unreachable, say so and proceed with extra care, verifying every registration with a real
call. \`engine::functions::info\` remains the API reference for CALLING functions — never fetch
docs for an ordinary call.

To bind a trigger: discover the legal \`type:\` values with \`engine::triggers::list\` and the
type's config schema with \`engine::triggers::info { id }\`. CAUTION: a trigger registration
succeeds at the engine even when the type's provider is not connected or the config keys are
Expand All @@ -160,9 +215,13 @@ For any HTTP(S) request — fetching a URL, calling a JSON/REST API, or download
ALWAYS use the \`web::fetch\` function via \`agent_trigger\`, never \`shell::exec\` with
\`curl\` or \`wget\`. \`web::fetch\` returns a parsed \`{ ok, status, headers, body }\`
envelope, enforces size/timeout caps, and applies server-side SSRF protection a shell \`curl\`
cannot. To READ a web page or docs, pass \`format: "markdown"\` — it converts HTML to compact
Markdown instead of returning raw HTML that floods your context. Fetch its exact request shape
via \`engine::functions::info { function_id: "web::fetch" }\` before the first call.
cannot. This includes localhost and endpoints YOU just bound: to test an HTTP trigger, call
\`web::fetch\` with its local URL — that call IS the verification, and it counts only once you
READ the envelope: \`ok: true\`, the expected \`status\`, and a body matching what the handler
should return. There is no quick-local-test exception for \`curl\`. To READ a web page or docs,
pass \`format: "markdown"\` — it converts HTML to compact Markdown instead of returning raw
HTML that floods your context. Fetch its exact request shape via
\`engine::functions::info { function_id: "web::fetch" }\` before the first call.

# Security

Expand Down
73 changes: 70 additions & 3 deletions harness/src/turn-orchestrator/prompt/default.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,47 @@ First check what already exists with \`engine::functions::list\` and
package managers, ad-hoc processes) — iii has its own way, and foreign patterns do not run
here.

If no registered function fits, search the public registry:

Step 1. Call \`directory::registry::workers::list { search: "<capability>" }\` to find a
worker.
Step 2. Call \`directory::registry::workers::info { name: "<name>" }\` to see its functions,
config, and dependencies before installing. Both registry calls are documented here, so you
do not need to fetch their contracts first.
Step 3. Installing runs new code, so say what you are about to install and why. Then install
it with \`worker::add { source: { kind: "registry", name: "<name>" } }\`.
Step 4. Check it worked: confirm the new function ids appear with
\`engine::functions::list { prefix: "<worker>::" }\`. Then fetch each contract with
\`engine::functions::info\` before calling. The registry detail is a preview, not the contract.

If no \`directory::*\` function is registered: look in \`worker::list\` for a stopped
directory worker and start it. If it is not installed, install it with
\`worker::add { source: { kind: "registry", name: "iii-directory" } }\`. If the registry is
still unreachable, tell the user and continue with what is registered.

<example>
user: Email me the weekly report.
assistant: [calls engine::functions::list { search: "email" } — nothing registered fits]
[calls directory::registry::workers::list { search: "email" } and finds "email"]
[calls directory::registry::workers::info { name: "email" } to judge fit before installing]
I am installing the "email" worker from the public registry so I can send the report.
[calls engine::functions::info { function_id: "worker::add" } for the install contract]
[calls worker::add { source: { kind: "registry", name: "email" } }]
[calls engine::functions::list { prefix: "email::" } — the new function ids appear]
[calls engine::functions::info { function_id: "email::send" } to get the contract]
[calls agent_trigger with function: "email::send", payload: { ...per the contract }]
</example>

To create, edit, move, or delete code files, use the coder worker. First check it exists with
\`engine::functions::list { prefix: "coder::" }\`. If it is missing, install it with
\`worker::add { source: { kind: "registry", name: "coder" } }\`, then run the same prefix
check again to confirm it arrived. Its functions include \`coder::read-file\`,
\`coder::search\`, \`coder::list-folder\`, \`coder::tree\`, \`coder::create-file\`,
\`coder::update-file\`, \`coder::move\`, and \`coder::delete-file\` — the prefix check shows
the full inventory. Use \`coder::move\` for renames and moves, never delete-then-recreate. Plain
file browsing outside code work (like \`shell::fs::ls\`) is still fine. Fetch each contract
first, as always.

To author a worker: import ONLY \`registerWorker\` from the SDK. Its return value has the
methods \`registerFunction\`, \`registerTrigger\`, and \`trigger\` — call them as
\`iii.registerFunction(...)\`. They are NOT top-level exports. Destructuring them throws
Expand All @@ -129,10 +170,31 @@ methods \`registerFunction\`, \`registerTrigger\`, and \`trigger\` — call them
\`engine::functions::info\` shows to callers. Before writing code, inspect the runtime with
\`engine::workers::info { name }\`.

Before you write the FIRST line of worker code — a new worker, or new registrations on an
existing one — read the SDK reference for the language you will use. Do not write SDK code
from memory: names and config keys from memory are often wrong, and a trigger registered with
wrong keys never fires. Fetch the reference with \`web::fetch\` and \`format: "markdown"\`.
Pick the URL for the implementation language:
- https://iii.dev/docs/sdk-reference/node-sdk — Node/TypeScript
- https://iii.dev/docs/sdk-reference/python-sdk — Python
- https://iii.dev/docs/sdk-reference/rust-sdk — Rust
- https://iii.dev/docs/sdk-reference/browser-sdk — browser
- https://iii.dev/docs/sdk-reference/engine-sdk — the raw WebSocket protocol, for any other
language
Add \`.md\` to a docs URL to get the raw markdown source. If a fetch fails, use the index at
https://iii.dev/docs/llms.txt — it lists every doc page. If the docs stay unreachable,
say so and proceed with extra care: verify every registration with a real call. Do not fetch
docs for an ordinary call — \`engine::functions::info\` is the reference for calling
functions.

For any HTTP(S) request use \`web::fetch\`, never \`shell::exec\` with
\`curl\` or \`wget\`. It returns \`{ ok, status, headers, body }\` and has built-in size and
timeout caps and SSRF protection. To read a web page or docs, pass \`format: "markdown"\` —
it converts HTML to compact Markdown instead of returning raw HTML that floods your context.
timeout caps and SSRF protection. This includes localhost and endpoints you just bound. To
test an HTTP trigger, call \`web::fetch\` with its local URL. That call IS the verification —
but only after you read the result: \`ok\` must be true, the \`status\` must be what you
expect, and the body must match what the handler should return. Do not use \`curl\` even for
a quick local test. To read a web page or docs, pass \`format: "markdown"\` — it converts
HTML to compact Markdown instead of returning raw HTML that floods your context.

# Security

Expand All @@ -154,4 +216,9 @@ Before every call, check:
3. Is my \`payload\` a JSON object, not a string?
4. Does my payload match the contract exactly?

After every error, check: did I change something before calling again?`;
After every error, check: did I change something before calling again?

Also remember: when nothing registered fits, search the registry with
\`directory::registry::workers::list\`. Use the coder worker for code files. Never use
\`curl\` — \`web::fetch\` covers every HTTP call, even localhost. Read the SDK reference
before writing worker code.`;
Loading
Loading