COLLECTED FROM SOURCE — NEVER COPIED PROSE

Schemas & contracts

Module docs, @typedef blocks, exported symbols, class members, object keys, and live examples — lifted from the files that own them on every build.

Agent identity and delegation metadata

lib/agent.js

  • get parent() — The creating Agent, or undefined when none was supplied.
  • get children() — Snapshot of directly owned workers.
  • childCreate(options = {…}) — Construct one direct child through the environment factory.
  • get name() — Human-friendly Agent name.
  • get description() — Human-friendly Agent description; an empty string is valid.
  • can — Live agent-owned capability map.

lib/env/extend.js

Env plugins: how a higher layer installs its Env members (Env.extend)

lib/env/extend.js — Env plugins (private to Env; published as Env.extend). A higher layer (IO, Agent) installs the Env members it owns AFTER the fact, so Env never knows those layers' APIs: - methods and getters land on Env.prototype; - events add Symbol-valued keys to Env.EVENT, and only the installing plugin can emit them (the returned emit). Env events report MEMBERSHIP (what exists); an object's activity is its own events, never an Env copy; - settings add the keys the layer owns to the defaults schema (env.settingsSchema(), init templates, API docs); - modelInfo(env, info, settings) contributes fields to every catalog entry (env.models(); settings = the endpoint's settings; Agent adds maxActive/active/available). Installing a name that already exists throws — a plugin extends Env, never overrides it.

A plugin keeps per-env state under its own Symbol key on the env.

lib/agent/env-plugin.js

Agent's Env plugin: the active-Agent registry, agentCreate, capacity, AGENT_* events

lib/agent/env-plugin.js — Agent's Env plugin (private to Agent).

Env knows nothing about Agents: this plugin installs every Env member that concerns them (Env.extend) when lib/agent.js loads — - the ACTIVE-AGENT registry: agents(), _agentAdd(), _agentRemove(); - the private _toolContextCreate(name, context) handshake for manual Env.toolCall dispatches carrying an Agent; - the factory agentCreate(options) (an Agent over the env it is called on — the safe view yields a safe Agent); - active-Agent capacity: the modelInfo catalog hook adds maxActive / active / available to every env.models() pair (the maxActive settings; a limit changes as a settings write: env.settings.providers[endpoint].models[model].maxActive = n); - the MEMBERSHIP events AGENT_ADDED/AGENT_REMOVED ({agent}) — Env reports which agents exist; what an agent does is its own Agent.onEvent vocabulary (subscribe on AGENT_ADDED); - the schema entries of the settings Agent owns: context and retry (the policy each Agent resolves once — lib/agent/policy.js) and maxActive; - systemPrompt(): the seeded system-prompt texts. Registry state lives under a private Symbol key on the Env (shared by its safe view).

lib/agent/hooks.js

Trusted Agent event hooks: scan roots, lifecycle and observer diagnostics

lib/agent/hooks.js — trusted extension observers of existing Agent events.

Scan only hooks/.js in the package root, installed extension roots, and the user settings folder (in that order, no project layer or extra path setting). Each module exports hooks(env) returning an object whose keys are Symbol Agent.EVENT values and whose values are (payload, agent) => . Example: export const hooks = (env) => ({ [Agent.EVENT.MESSAGE_COMMITTED]: (message, agent) => { / observe message; do not alter the turn \/ } }); Import Agent from lib/agent.js to use the documented Symbol constants. Hooks run in a microtask and are never awaited by a turn. Failures (sync or async) reach the Agent.LOG diagnostic stream once per file/event; they cannot propagate back into the turn. Refresh replaces existing listeners and reimports files with the tool refresh revision. Missing folders do nothing.

lib/env.js

  • onEvent(event, callback) — Subscribe to an Env Symbol event.
  • offEvent(handle) — Remove the listener registered with {@link onEvent}.
  • static EVENT — The Env event vocabulary: Symbol constants for onEvent/offEvent.
  • static extend(plugin) — Install a higher layer's Env members after the fact (a plugin): `methods`/`getters` land on Env.prototype and run on the env they were reached through (the safe view included); `events` add Symbol keys to Env.EVENT; `settings` add the layer's settings keys to the defaults schema.
  • static get envs() — The registered project Envs by project folder — at most one per canonical folder.
  • static open(path, options) — Acquire a fully initialized Env for a project folder.
  • get closed() — whether close() has run (the Env left Env.envs).
  • get name() — The project name.

Agent policy — settings resolved once per Agent (context guard, retries, tool timeouts)

lib/agent/policy.js

lib/agent/policy.js — the Agent POLICY (private to Agent): the settings an Agent runs by, resolved when the Agent is built and again only when it selects another model (agent.policy) — a settings edit applies to the next Agent or model selection, never mid-run.

context keys resolve down a TREE, each level overriding the one above per key: the global settings.context, then the endpoint's <endpoint>.context, then the model's <endpoint>.models.<model>.context.

settings.context context policy: cap, turn and autocompact use fractions of the known model window (> 1 means percentage); autocompactMax is an absolute token count: cap the overall ceiling (default 0.9 — always leaves room for /compact); crossing it ends the turn turn the growth ceiling for ONE turn alone (default 0.4) autocompact compact before the next request at 0.4 (40%) by default; false/0/1 disable, true = 0.4 autocompactMax absolute token threshold, default 200,000; 0 disables the absolute threshold; the first threshold reached triggers compaction (lib/agent/compact.js) settings.retry failed provider requests: attempts attempts per turn, the first write included (default 3) base the first retry's delay (default 2s); each next retry waits twice as long max the delay ceiling (default 30s) settings.tools tool dispatch (core schema; folders is Env's): timeout the default per-call timeout (default 120s) timeoutLimit the cap on any call's timeout (default 20m) concurrency the read-only batch size (default 3)

Durations accept milliseconds or unit strings ("30s", "5m").

POLICY_SETTINGS = Object.freeze({…})

The settings keys Agent owns (its Env.extend schema entries).

agentPolicy(settings = {…}, endpoint = {…}, model = {…})

Resolve the policy from a settings tree (env.settings) and, for the context keys, the selected pair's overrides.

[settings={}] object
global settings, including `context`, `retry`, and `tools`
[endpoint={}] object
endpoint settings; its context and selected model context override global context per key
[model={}] object
selected model metadata containing context overrides

Returns Readonly<{context: {cap: number, turn: number, autocompact: number|false, autocompactMax: number}, retry: {attempts: number, base: number, max: number}, tools: {timeout: number, timeoutLimit: number, concurrency: number}}> — Frozen resolved policy.

Throws — {TypeError} If tool concurrency or context.autocompactMax is not a valid integer.

Throws — {Error} If a configured tool timeout duration is malformed.

retryDelay({ base, max }, attempt)

One retry's delay: base doubling per retry (attempt 0 is the first RETRY), bounded by max, spread by up to a quarter of the base so simultaneous retries do not land in lockstep.

attempt number
zero-based retry index; values below zero are treated as zero

Returns number — Delay in milliseconds, including random jitter and capped at `max`.

lib/agent/run.js

contextGuardTrip(agent, turnStartUsed, accounting)

The CONTEXT-USAGE runaway guard (agent._policyCurrent.context — cap / turn, lib/agent/policy.js): checked at the top of every loop iteration, so it gates EVERY continuation alike — the very first request of a turn that inherited an already-critical context, another tool-call round, or the next queued message alike. A model window is required (Number.isFinite(total) > 0); without one the percentage is unknowable and this guard stays silent.

agent object
turnStartUsed number
context usage at this turn's start

Returns string|null — the guard message, or null when under both caps

lib/agent/tool-timeout-settings.js

lib/agent/tool-timeout-settings.js — Agent's tool-timeout constants (private): the defaults behind settings.tools.timeout/timeoutLimit (resolved per Agent in lib/agent/policy.js) and the onTimeout grace.

The Agent owns enforcement. Tools may DECLARE timeout, but the Agent extracts it before invocation; tools never race a second timer against the harness. A tool's optional Agent-facing onTimeout callback gets one bounded cleanup/final-response grace period.

DEFAULT_TOOL_TIMEOUT = 120_000

Default Agent-enforced cap for one tool call, in milliseconds (two minutes).

DEFAULT_TOOL_TIMEOUT_LIMIT = 20 * 60_000

Maximum configurable tool-call timeout, in milliseconds (20 minutes).

TOOL_ON_TIMEOUT_LIMIT = 60_000

Maximum Agent-facing onTimeout cleanup/final-response grace.

lib/agent/tool-timeout.js

Agent-owned deadlines and cancellation for each dispatch. Tool-declared timeout is extracted before invocation. onTimeout is a bounded final-result hook, not a process-cleanup policy; dispatch always tears down its sandbox.

prepareToolTimeout(agent, entry, args)

Resolve a call's duration and strip a declared timeout from tool arguments.

agent object
Agent policy and call context.
entry object
Tool registry entry, whose input schema may declare `timeout`.
args *
Original tool arguments.

Returns {args: *, timeout: number, requested: number|undefined} — Forwarded arguments, effective timeout capped by policy, and parsed explicit request.

Throws — Propagates durationParse errors for an invalid declared timeout.

async runWithToolTimeout({ agent, entry, name, args, call, timeout, requested, invoke, controller = new AbortController(), sandbox, onResetTimeout, setTimer = setTimeout, clearTimer = clearTimeout, })

Race all call phases against cancellation; dispatch owns final teardown.

options object
Agent, entry, tool name/arguments/call, timeout and optional requested timeout, invoke callback, and optional controller, sandbox, reset hook, and timer functions.

Returns Promise<*> — Tool invocation result or timeout-hook result.

Throws — {Error} Invocation errors and cancellation errors propagate; throws ToolTimeoutError when execution times out without a usable hook result.

Effects — Installs and restores the agent timeout-reset callback, observes abort, and cleans up its abort listener; does not tear down the sandbox.

Context schema — messages and content blocks

lib/context/types.js

lib/context/types.js — context/message/content types (concrete JSDoc).

Own schema, no external dependency. A context is an ordered array of messages. Every message has a numeric type and a content array; all other fields are optional metadata, tolerated by readers (unknown fields never invalidate a value).

Addressing is array/block based — messages have no local ids: message = context[i]; block = context[i].content[j] Provider response/cache/block identifiers may ride as optional metadata and are dropped by Context when an edit makes them stale.

typedef BinaryContent

Raw file bytes for models that accept binary input (vision models fail on base64 *text* but consume binary blocks). Providers map image/* blocks to their binary/image channels and tolerate the rest.

type "binary"
[mimetype] string
media type (application/octet-stream fallback)
[filename] string
safe display/upload basename; never a source path
content string
base64-encoded bytes
typedef Content

Small discriminated objects keyed by `type`. Unknown block types are tolerated (tolerant reader).

typedef Context

An ordered array of messages — the schema everyone talks.

typedef ImageContent
type "image"
[mimetype] string
media type, when known
content string
typedef Message

System messages stay visible in ordered position. Assistant content is an ordered mix of text/thinking/toolCall blocks — thinking is a block, never a separate message.

type number
one of MessageType (1..4)
content Content[]
typedef TextContent

Plain text — the universal block every provider speaks.

type "text"
text string
typedef ThinkingContent

May carry provider metadata (e.g. replay signatures) as extra fields; preserved through Context edits.

type "thinking"
text string
typedef ToolCallContent

May carry provider metadata as extra fields; preserved through Context edits.

type "toolCall"
callId string
provider call id
name string
flattened tool name
arguments *
tool arguments (string while streaming, parsed value after)
typedef ToolResultMessage

A tool's answer, linked to its call; built by resultContent() (see the Tool schema contract).

type 4
content Content[]
result content
callId string
links to the ToolCallContent.callId
[name] string
tool name, when the provider requires it
[error] boolean
error status

Headless usage — running the Agent from your own script, no TUI

lib/agent.js

the headless core entry point: callbacks replace stdio/process.exit/terminal APIs

lib/agent.js — Agent: owns context, provider/model selection, the tool loop, and persistence. Headless: no stdio, no process.exit, no TUI.

PUBLIC CORE ENTRY POINT. Agent coordinates the JSONL session store, crash-safe finish hooks, the forked tool sandbox, the tool loop, tool-call execution, status readouts, and thinking level. One Agent owns one conversation for its lifetime; create another Agent to start or resume a conversation, or use Agent.fork to branch one. App owns view replacement. It composes Context ← Env ← IO ← Agent and publishes that core tree as Agent.Context, Agent.Env, and Agent.IO. It does not import CLI, Markdown, or application/UI layers.

Ownership: - Agent alone owns the ordered in-memory context, provider/model selection per request, the tool loop, and persistence. - IO receives the COMPLETE current context on every request; one IO instance is reused per qualified model and reconstructed after close() leaves it permanently closed. It reports usage PER TURN (the terminal event's usage envelope, see lib/context/usage.js) — nothing above it. - The CONTEXT is a Context object (lib/context.js): Agent names it from a session id (--session/--resume), or adopts a ready one unchanged; its identity never changes. Context owns edits and persistence to env.settings.sessions when saving is on. - Agent alone owns the CUMULATIVE usage total (the usage getter): it sums every terminal's canonical counters in memory as run() sees them. Never persisted (no usage.json) — a fresh Agent starts at zero; bindings that show it (the TUI footer, a one-shot CLI's final line) read it fresh rather than tracking their own copy.

Agent's implementation is split across helper modules in lib/agent/; this file provides the public façade: - run.js the tool loop and its runaway guards; - tool-exec.js tool-call eventDispatch/execution, the result contract; - readouts.js cumulative usage/context-window/connection readouts; - context-lifecycle.js constructor session settings and seeded system prompt; - thinking.js the thinking level.

Model identity is one qualified <endpoint>/<model> string throughout construction, property assignment, child creation and context persistence. Assigning model validates and records a whole state change. run() has no model override. Split endpoint options are rejected; presentation layers derive display fields.

Core rules (see run.js/tool-exec.js for the full contracts): every tool call in the LIVE response executes — identical repeats included; calls execute ONLY in real time, never from history. Tool-call ids belong to AGENT, never to the model (generated when missing, regenerated on collision). Tool-execution lifecycle is observable: TOOL_EXECUTE fires just before a call runs and TOOL_RESULT after each result is appended.

Tool-call SANDBOX (options.toolCall): file-scanned tools execute in a FORKED child process by default (fork: true); built-ins, programmatic tools, and INTERACTIVE tools stay in-process. Options: - fork: false disables the sandbox (in-process execution) - timeout: explicit HOST override of settings.tools.timeout (duration) Read-only (tool schema safe:true) calls overlap in contiguous groups; mutations run in order. Every call is Agent-timed (including in-process). A schema-declared timeout argument is extracted and capped at settings.tools.timeoutLimit; schema.onTimeout gets at most 60s for cleanup or a final result.

WRITE CAPABILITY (options.can.write; assign agent.can.write at runtime): always publish the full available catalog, but execute ONLY read-only tools (schemas with safe: true) while write is denied. Agent passes the effective tool-context safe to Env.toolCall. A child inherits its parent's write restriction. Write denial is FORCED when no supported OS sandbox is available — there is NO opt-out: mutation tools run only under an active OS sandbox, so on a mechanism-less platform the agent is read-only.

SYSTEM PAYLOADS: a tool returning { result, system } answers briefly via its tool result while system (string or string[]) appends as System messages after the complete tool-call batch and any queued user input. DISPLAY PAYLOADS: { result, display } — display rides the outcome to TOOL_RESULT subscribers and is shown to the user WITHOUT ever joining the context: a tool can display data without sending it to the model (the edit tool's git-style diff).

bin/scripts/agent

the reference consumer: a one-shot stdin-to-stdout tool loop, as a CLI

agent — headless tool-loop CLI (thin binding over lib/agent.js).

Usage: <prefix>-agent [--model <endpoint>/<model>] [--url <url>] [--token <key>] [--timeout <dur>] [--tools <list>] [--session <id> | --resume <id>] [--login] [--logout <endpoint>] [--help]

Contract: - Reads stdin to EOF (shared grammar: whole JSON context first, otherwise JSON-per-line with non-JSON lines as user messages). - Runs the Agent tool loop: provider request → tool calls executed in-process → results appended → next request, until done/error. - stdout: normalized response events, one JSON object per line (every request's events stream in order; the last done carries the final assembled message). stderr: diagnostics only. - --session <id> persists the context to a JSONL session log; --resume <id> replays it first. Session wiring lives in Agent. - SIGINT/SIGTERM cancels the in-flight request: the terminal partial is persisted, exit 130.

Exit codes: 0 success · 1 usage/other · 2 auth · 3 network · 4 provider error · 5 malformed input · 130 cancelled

Jobs — activation, scheduling, execution and task files

lib/jobs.js

Portable, folder-only project Jobs. ai-jobs enables, ai-jobs-disabled disables. Operations are best effort: there are no locks, daemon identity records, or cross-process coordination. Concurrent scans/mutations may duplicate work, lose updates, or race archives. Atomic single-file replacement remains used.

PUBLIC MODULE (owner of lib/jobs/). The façade publishes the operations a host or tool needs; task parsing, occurrence state, attempts, and archives stay private to lib/jobs/.

lib/jobs/tasks.js

Pure Markdown/frontmatter parsing with wholesale fallback and task-local rejection.

parseTask(filename, source)

Parse one task without filesystem side effects; declared bad frontmatter rejects the task.

filename string
source string

Returns Readonly<object>

Throws — {JobsError} For invalid source, prompt, frontmatter, or metadata.

parseTasks(tasks)

Parse independently so duplicate ids and empty prompts remain task-local rejections.

Returns Readonly<{tasks: ReadonlyArray<object>, diagnostics: ReadonlyArray<object>}>

Throws — {JobsError} If tasks is not an array; task-level JobsErrors become diagnostics.

lib/jobs/operations.js

async scheduleJobs(root, command, options = {…})

Shared task command boundary. Throws actionable JobsError or filesystem errors; never runs tasks. Create refuses existing files; update replaces whole source (last writer wins); remove does not cancel. Calls validate the current folder before operating. Concurrent mutations are best effort: atomic create refuses an existing file and updates may be last-writer-wins.

root string
Project root.
command object
Command with action `list`, `read`, `create`, `update`, or `remove`; filename/source are required as appropriate.
[options={}] object
Operational options; `readOnly` blocks mutations, `settings` defaults to `{}`, and optional `folder` must match the project root.

Returns Promise<object> — Listing/read result or mutation result.

Throws — {JobsError} For invalid commands, read-only mutations, operational validation failures, or invalid tasks; filesystem errors may propagate.

async validateJobsOperational(root, options = {…})

Validate activation, durable layout, and optional caller folder alignment.

root string
Project root to validate.
[options={}] object
Activation options; `settings` defaults to `{}` and other supported options are forwarded to activation validation. If supplied, `folder` must resolve to the project root.

Returns Promise<object> — Activation result.

Throws — {JobsError} If activation/layout validation fails or `folder` differs from the project root; filesystem errors from resolving `folder` may propagate.

lib/jobs/dispatcher.js

async dispatchJobs(projectRoot, options = {…})

Best-effort serial scan. Concurrent scans may duplicate execution or lose state updates. Problems and warnings are appended to the readable error log (ai-jobs/errors/YYYY-MM-DD.md); the scan record (due, outcomes, errors, warnings) is kept in ai-jobs/last-run.json and runs/. No error names anything outside the project (messageLocal).

projectRoot string
Project root to validate and scan.
[options={}] object
Optional adapters and execution configuration: `io` filesystem; `clock` returning epoch milliseconds; `validate` validator; `executor` custom job runner; `execution` agent/environment options. `execution.createEnv`, if supplied, receives `(projectRoot, environment?)`; environment is omitted unless explicitly set. The dispatcher closes its acquired shared Env once after the scan.

Returns Promise<{outcomes: {id: string, outcome: string}[], errors: object[], warnings: object[], log?: string}> — Scan outcomes, errors and warnings (warnings do not fail the scan), and optional project-relative error-log path.

Throws — {JobsError} Throws JOBS_CLOCK if the clock does not return a non-negative safe integer.

Throws — Rejects if operational validation fails. Scan/recovery failures are generally included in returned errors; log and diagnostic persistence failures are also reported there. Closes a created shared environment after the scan.

cycleRecord(at)

Create the location-neutral run-log record for one Jobs scan (ai-jobs/last-run.json, runs/).

at number
Non-negative safe-integer cycle timestamp in milliseconds.

Returns {version: number, at: number, due: object[], running: object[], outcomes: object[], errors: object[], warnings: object[]} — Empty scan record.

Throws — {JobsError} Throws JOBS_CLOCK if `at` is not a non-negative safe integer.

lib/jobs/agent-execution.js

In-process job Agent execution: one fresh, headless Agent per task against a wake-shared Env. An explicit cancel interrupts it; Jobs has no whole-job deadline and a wake never force-kills a running job.

bin/scripts/jobs

Thin operator CLI: maps argv to the Jobs façade and prints short notes. Problems are logged by the library in ai-jobs/errors/, not printed here.

Model catalog — endpoint/model pairs, their caps and capacity

lib/env/models.js

lib/env/models.js — the published MODEL CATALOG (private to Env): env.models(secret) — a Map keyed endpoint/model whose ModelInfo values hold everything already computed from current state — and env.connection(selector), IO's handle on one pair.

ModelInfo = { endpoint, model, label, listed, secret, disabled, loginRequired, lastUsed, projectLastUsed, caps: { contextWindow, thinking, tools, streaming, input? }, ...plugin fields (Agent: maxActive, active, available) } - listed: the endpoint's catalog publishes the model; an endpoint with no catalog yet (collection pending) still answers its last-used pairs, unlisted; - secret: the endpoint's filter / secret: true, the model's own secret hides it (models() omits it; models(true) includes it, flagged); - disabled: the endpoint's disabled: true (a user switch, e.g. a depleted token budget): models() omits the pair, models(true) includes it flagged, and connection() refuses it; - lastUsed: the global last-model memory's ISO timestamp (or null); projectLastUsed: the pinned project's own timestamp (or null); - caps.thinking: the model entry's native modes, else the provider's; - caps.tools: the provider's built-in tools minus settings opt-outs, first explicit boolean wins: <ep>.models.<m>.tools.<name>, then <ep>.tools.<name>, then providers.tools.<name>. Plugins add fields through Env.extend({modelInfo(env, info)}).

The catalog is collected in the BACKGROUND (collect(): context-window registry, live model lists, bounded retries); every change emits Env.EVENT.MODELS_CHANGED (coalesced per tick) and consumers re-read.

lib/env.js

  • models(secret = false) — The model catalog: every known endpoint/model pair with its state already computed (lib/env/models.js ModelInfo — caps, secret, loginRequired, lastUsed, plugin capacity fields).
  • connection(selector, { remember = true } = {…}) — IO's handle on one pair: {endpoint, model, Protocol, url, settings, authSet(data), authReload(current)} (lib/env/models.js).

settings.json

{
  "providers": {
    "test": {
      "provider": "test",
      "url": "test://script",
      "secret": true
    }
  }
}

Provider contract

lib/env/provider.js

CATALOG side — Env completes the statics

lib/env/provider.js — static-side provider completion (private to Env). A provider plugin default-exports one class and imports only lib/context.js. Env registers it with its CATALOG side completed:

static provider — {label, capabilities: {streaming, thinking, tools}} normalized: thinking = native modes (least -> max, [] = none); tools = false | true | map of built-in tools {name: {description?, schema?, function}} static detectEnvironment(...) — optional process-environment-only discovery; Env runs this once per registered protocol per Env. static detect(...) — live endpoint auto-detection (default: nothing); continues each pass for probes and custom detectors. static detectEnvironmentOnly — true when detect merely delegates to detectEnvironment, avoiding a duplicate scan. static models(...) — the endpoint's model map (default: OpenAI- compatible /models, lib/env/openai-models.js) static testConnection — strict login verification (default: same) static login(input) — credentials -> auth record (default: api key)

The instance (wire) side is IO's concern (lib/io/provider.js).

static methods completed when the class lacks them: detect models testConnection login

lib/io/provider.js

WIRE side — IO completes the instance

lib/io/provider.js — instance-side provider completion (private to IO). A provider class Env registered carries its static catalog side; IO completes the wire side before constructing a connection:

- missing context2msg / msg2events / read / close / reportPlanUsage come from the OpenAI Responses defaults (lib/io/openai.js); - send(msg, base): a provider's own send receives base, the plain HTTP transport, to wrap (missing: the OpenAI default send); - classifyError(err, base) / depletionError(classified, base): base is the shared verdict a provider may refine.

One completed class per provider class (cached).

lib/io/provider-error.js

Errors — providers throw plain errors with `kind` or `status`

lib/io/provider-error.js — the stable provider failure taxonomy (private to IO): auth / network / provider / malformed. Providers throw plain errors carrying kind or status; IO classifies them here, and a provider's own classifyError(err, base) may refine the default verdict.

classifyError(err, providerName)

Classify a raw error into the stable provider taxonomy. An explicit kind wins; 401/403 are credential verdicts, every other status a provider failure (402/429 included — budgets, see depletionError).

err *
raw thrown value; an existing `ProviderError` is returned unchanged
[providerName] string
optional provider name appended to the message (no suffix when omitted)

Returns ProviderError — The existing classified error or a newly classified error with the original as `cause`.

Throws — May propagate errors from accessors or coercion on the supplied error value.

lib/io/thinking.js

Thinking — native modes; IO maps library levels

lib/io/thinking.js — the library thinking vocabulary and its mapping to a model's NATIVE modes (private to IO).

The harness speaks one vocabulary (THINKING_LEVELS). A provider publishes its native modes (capabilities.thinking, least -> max, an off mode such as none first when it has one); a model entry may narrow them (thinking: [...], [] = no thinking control); a model entry or the provider's capabilities may name a default (thinkingDefault). IO maps the think setting to one native mode before a provider reads it, so providers never translate.

providers/ollama.js reference implementation

providers/ollama.js — Ollama connector: the reference implementation of the provider contract (metadata, models(), login(), the context2msg()/msg2events() translators, and endpoint detection), canonical for other provider plugins to follow.

Direct HTTP to the Ollama local server (never the ollama CLI): POST {url}/api/chat — NDJSON streaming chat GET {url}/api/tags — local model listing

Uses the default HTTP backend wholesale (connect/send/read/close) — Ollama's NDJSON stream is line-delimited JSON, which the default blocking-await read already speaks. Only the two translators plus the pi-pattern metadata surface live here.

Wire mapping (context format -> /api/chat): system/user -> {role, content} (text blocks joined) assistant -> {role:"assistant", content, tool_calls?} — thinking blocks are local-only and never hit the wire tool result -> {role:"tool", name, content} — name resolved from the message's name, else the matching toolCall block image and generic binary blocks ride Ollama's sole documented images channel (base64) on user messages; tool-result attachments follow all tool messages in a labeled user message. Ollama exposes no generic-file field in /api/chat. Tools: Env catalog entries -> {type:"function", function:{name, description, parameters}}; omitted when the catalog is empty.

Wire mapping (/api/chat NDJSON -> response events): content strings stream as text_start/text_delta/text_end at contentIndex 0; tool_calls become tool_call_start/tool_call_end pairs (Ollama supplies no call ids — local ollama-N ids are generated for context linkage); the done:true frame maps prompt_eval_count/eval_count into the usage envelope. Native frames ride along as event metadata where useful.

Errors: HTTP statuses and stream-level {error} frames surface through IO's auth/network/provider/malformed taxonomy; login() is a trivial no-auth procedure; models() refreshes the cached list in the provider namespace on every access and falls back to the cache offline.

Request-failure retries and endpoint token-depletion

lib/agent/reliability.js

lib/agent/reliability.js — which failed provider requests an Agent retries (private to Agent). Only failure classes that can plausibly succeed on a later attempt retry (RETRYABLE_KINDS): transport failures and provider-side statuses — a depleted token budget surfaces as one of those and its refill IS time. A malformed context never heals by waiting; a cancellation is the user's own word. How often and how long is the Agent's policy (lib/agent/policy.js, settings.retry).

RETRYABLE_KINDS = Object.freeze(["network", "provider", "auth"])

The classified error kinds an Agent retries after a growing interval (lib/agent/run.js). "network" is transport flakiness; "provider" and "auth" cover provider-side statuses (a token-budget depletion included — its refill is time, so a later attempt may succeed). "malformed" is deterministic and "cancelled" is the user's own word — neither retries.

awaitTimeout(ms, wait)

Races the wait created by wait against a deadline, resolving true on completion or false when the deadline wins. The deadline is a fallback, not a polling mechanism; wait receives a signal aborted when the race settles so its listeners or resources can be released.

ms number
Deadline in milliseconds; negative values are treated as zero.
wait (signal: AbortSignal) => unknown
Creates the completion wait; its result is awaited.

Returns Promise<boolean> — Resolves to `true` on completion or `false` on timeout; rejects if `wait` throws or its result rejects.

Throws — {TypeError} If `wait` is not a function.

lib/io/provider-error.js

depletionError(classified)

The shared TOKEN-DEPLETION predicate: exact signals only. 429/402 are the HTTP rate/quota statuses; otherwise the error body/code must NAME a budget. A 404 (wrong model), a 401/403 (credential), a transient 5xx or a stall never marks the endpoint depleted.

classified object
classified error/event with `kind`, `message`, and optional numeric `status`; may be nullish

Returns boolean — Whether this is a provider/auth failure carrying an explicit 402/429 status or budget signal in its message.

Throws — May propagate errors from accessors or string conversion on the supplied value.

Settings schema — the merged settings tree

lib/env/settings.js

lib/env/settings.js — plain-object predicate and JSON source discovery shared by Env's source graph and trusted extension selection. The graph owns precedence and merge semantics; arrays replace rather than concatenate. Theme files are scanned only when requested from a trusted themes/ folder.

GUIDE.md

keymeaning
`providers`Endpoint URLs, protocol names, model metadata, and endpoint limits. Per-endpoint model `filter` regex; `disabled: true` switches an endpoint off
`tools`Tool policy: `folders` path-to-boolean map (trusted package/user roots only), `timeout` (120000 ms), `timeoutLimit` (1200000 ms), `concurrency` (3)
`skills` / `prompts`Extra roots as path-to-boolean maps (`false` disables a path)
`extensions`Installed packages or existing folders, as name/path-to-boolean maps. Package/user scope only, never project. Extensions may add providers and tools
`env`Variables for child processes: `false` removes one; `true` passes the inherited value through; any other non-nullish value sets it
`mcp`MCP server definitions, trusted package/user scope only: `{url, oauth:{clientId?,issuer?}}` for HTTP OAuth, or `command` for stdio. Never keep tokens in settings
`projects`Global pins, stored only in the user's `projects.json`. Folder-keyed records; see the example below
`tui`Terminal interface settings (cursor, keys, mode); legacy `tui.theme` overrides the shared theme
`web`Browser settings; legacy `web.theme` overrides the shared theme
`theme`Shared theme selection for terminal and browser, e.g. `"theme": "omoya"`. Choosing a theme in either interface updates this value and removes legacy overrides
`themes`Named themes for the terminal and browser. See below for where theme files load from
`last-model`Recently used pairs (user `last-model.json`): `{"<endpoint>/<model>": <ISO timestamp>}`
`think`Default reasoning effort
`safe`Start read-only
`timeout` / `tools.timeout`Time limits for each request and each tool call
variableeffect
`OMOYA_SETTINGS_DIR`, `OMOYA_SKILLS_DIR`, `OMOYA_PROMPTS_DIR`Change where settings, skills, and prompts are found
`OMOYA_OS_SANDBOX`Change sandbox behavior
Provider keys ([listed in the README](README.md#providers))Endpoint auto-detection
`SEARXNG_URL` / `SEARXNG_BASE`, `BRAVE_API_KEY``web-search` backends

Settings store — Env.SettingsStore, the settings aggregator

lib/env/settings-store.js

SettingsStore — a project-agnostic settings aggregator (public as Env.SettingsStore).

A store is a flat, ordered list of nodes. Reads merge objects by key, with later nodes overriding earlier nodes; arrays and scalars replace lower values. A node holds data from one file or memory, mounted at a key path (at). A parent store is the lowest layer: reads merge over it and writes no local node takes fall through to it.

Reads return the current state immediately, even while files are loading. Writes scan nodes from last to first: the first writable node that owns the exact key or accepts it as a catch-all takes the write and marks changed file nodes dirty. Construction and attach may stat a path to identify a directory, but do not load file content; poll loads it. Loading, saving and change notices are asynchronous, grouped per round:

loaded — a poll round finished reading every changed file saved — a storage round finished writing every dirty file updated — settings data changed (after a storage or poll round)

class SettingsStore

An independently usable, ordered settings store, exported as Env.SettingsStore. Create with new SettingsStore({ parent, sources }); parent is an optional SettingsStore below local sources, and sources defaults to []. Each source spec is a plain object with these supported fields:

  • source: nonempty file path, existing directory (*.json discovery), or

glob (discovery). Omit it for an in-memory source; missing file paths are files, not directories. Discovery runs on poll, sorts filenames and skips package manifests/lockfiles and files already attached here or in a parent.

  • data: initial plain object for memory (default {}), copied rather than

retained. File sources cannot specify data.

  • at: safe string-segment mount path (default [], the root); retained by

reference on its node handle, so do not mutate it while attached.

  • catchAll: default false; accepts writes in its mount during the reverse

node scan even when an earlier node owns the path. A catch-all must be writable. Without one, new writes may return false.

  • writable: default true; false excludes the node from store write routing.

A read-only file cannot be edited through its handle; memory owners can still change a read-only memory handle directly.

  • filter(data): optional file-read projection. Return the file data to keep;

dropped keys are omitted when the file is subsequently saved.

  • exclude: optional RegExp matching basenames to omit from discovery.

Read immediately with read(path); plain objects merge by key across layers, while arrays and scalar values replace lower layers. Write synchronously with write(path, value): search nodes from last to first; the first writable node holding the exact path or accepting it as a catch-all takes the write. If none takes it, routing falls through to the parent. File changes are saved asynchronously; call await poll() to load files and await save() to flush edits. A missing file loads as {}; an empty saved file node deletes its file. Construction and attach may stat paths for directory detection but do not load file content. Polling also runs periodically while open.

Standalone file and ephemeral memory sources:

import { Env } from "./lib/env.js";
const store = new Env.SettingsStore({ sources: [
  { source: "./config.json", catchAll: true },
  { at: ["session"], catchAll: true, data: {} },
] });
await store.poll();
store.write(["theme"], "dark");
store.write(["session", "active"], true); // memory only
await store.save();
await store.close();

Missing paths and invalid root writes:

import { Env } from "./lib/env.js";
const store = new Env.SettingsStore();
store.read(["missing"]); // undefined
store.write(["missing"], 1); // false: no writable owner or catch-all
try { store.write([], 1); } catch (error) {
  console.log(error instanceof TypeError); // true: root writes are unsupported
}
await store.close();

Compose a local store over a shared parent:

import { Env } from "./lib/env.js";
const shared = new Env.SettingsStore({ sources: [{ catchAll: true, data: {} }] });
const project = new Env.SettingsStore({ parent: shared,
  sources: [{ at: ["project"], catchAll: true, data: {} }] });
project.write(["shared"], 1); // falls through to shared
project.write(["project", "name"], "demo"); // local only
await project.save(); // also saves shared by default
await project.close(); // shared remains open
await shared.close();

attach(spec, { before } = {…}) method

Attach one source spec from the class contract, last unless placed before a local handle.

spec object
Source spec; memory `data` is copied. File data loads on poll, not attach.
[options={}] object
Placement options, not retained.
[options.before] object
Existing local node or discovery handle to insert before.

Returns object — File/memory node handle (`own`, `has`, `assign`, `replace`), or discovery handle whose `nodes` property is replaced on membership changes during poll. Read the property again after polling. Attaching an already attached file returns its existing node, including from a parent; it is not newly owned by this store.

Throws — {TypeError} Closed store, invalid spec/field, or unknown `before` handle; correct input or use an open store before retrying.

attached(filename) method

Find an attached file node locally or in the parent without changing membership.

filename string
File path in the same spelling used at attach/discovery; no path normalization occurs.

Returns object|undefined — Shared live node handle if attached, otherwise `undefined`; caller does not acquire ownership by lookup.

changed(node, path) method

Internal: a node's data at path (or, without one, this store's membership) changed. Schedules a storage round.

close() method

Stop polling and await local loads and writes; borrowed parent stays open.

Returns Promise<void> — Rejects after cleanup when local persistence fails.

closed field

closing property

Read: get closing()

Whether close has started; node handles reject edits at this boundary.

constructor(options = {…}) constructor

Create an open store and attach ordered sources; starts an unref'ed polling timer.

[options={}] object
Options are not retained; source data is copied, while `parent` is referenced until close.
[options.parent] SettingsStore
Existing parent below this store; the child does not own or close it.
[options.sources=[] object[]
] Source specs documented on the class; attached in order, lowest first.

Throws — {TypeError} Invalid options container, unknown own top-level keys (including symbols), parent, sources, or source spec; fix options before retrying. Unknown top-level keys fail before source attachment and poll timer allocation.

detach(handle) method

Remove a locally attached node or discovery handle and its discovered nodes; file content is not deleted.

handle object
Handle returned by a local `attach()` (or a local node in `nodes`).

Returns boolean — True if removed; false for unknown/already removed or inherited handles. Membership and reads update immediately; the handle is no longer a store layer.

nodes property

Read: get nodes()

Read a new array of current local node handles, lowest first; parent nodes are excluded. Mutating the array does not edit membership; handles remain live only while attached.

on(event, listener) method

Subscribe to a round notice, including notices relayed from the parent. loaded reports file nodes read, saved reports successfully saved file nodes, and updated reports nodes whose data/membership changed. A notice is emitted once per round only when its set is nonempty; listener exceptions are ignored so other listeners still run.

event "loaded"|"saved"|"updated"
Notice name.
listener Function
Receives `{ nodes: Set }` of live node handles; do not mutate the set. The store retains this callback until disposed or closed.

Returns Function — Idempotent disposer; call when no longer interested. Closing the store removes its subscriptions.

Throws — {TypeError} Unknown event or nonfunction listener; correct inputs before retrying.

owner(path) method

Find the last writable node holding the exact path, locally or in the parent; do not use this to predict catch-all routing.

path string[]
Absolute safe string-segment path; `[]` has no exact owner.

Returns object|undefined — Live node handle or `undefined`; does not mutate state or transfer ownership.

Throws — {TypeError} Invalid path; correct it before retrying.

parent property

Read: get parent()

Read the referenced parent store, or undefined; querying does not mutate it.

poll({ parent = true } = {…}) method

Discover files and read changed file nodes, optionally including the parent. Discovery adds sorted matching files and removes vanished clean ones; dirty vanished nodes stay attached until saved. A missing direct file loads as {}; JSONC is accepted, but file roots must be plain objects. Successful reads apply even when another file fails; pending edits made before the first load overlay the file data and remain dirty for a later save. Local edits to an already loaded dirty file are not overwritten by a poll.

[options={}] object
Round options, not retained.
[options.parent=true] boolean
Also poll the parent when present; false limits the round to this store.

Returns Promise<boolean> — True if data or discovery membership changed, false otherwise; emits grouped `loaded` for read nodes and `updated` for changed nodes. Concurrent polls queue another round.

Throws — {Error|AggregateError} Read, JSONC parse, root-shape, or discovery failures; successful files still apply. Repair failing files/permissions and retry `poll()`; last good data remains for failed reads.

read(path = [], { skip } = {…}) method

Read the current merged value immediately; file data can be partial before poll completes.

[path=[] string[]
] Absolute safe string-segment path; `[]` reads the root.
[options={}] object
Query options, not retained.
[options.skip] object
One node handle to exclude from this merge, including a parent node.

Returns * — Detached JSON-shaped value, or `undefined` when absent; changing it never changes stored data. Does not mutate the store.

Throws — {TypeError} Invalid path (empty/nonstring/unsafe segment); correct it before retrying.

save({ parent = true } = {…}) method

Flush queued local edits, optionally including the parent; queued file writes are atomic replacements (empty objects remove files). File nodes not yet loaded are not overwritten: call poll() before expecting their early edits to save. Equal writes do not create notices. A background debounced save also follows changes; await this method to observe completion.

[options={}] object
Round options, not retained.
[options.parent=true] boolean
Also save the parent when present; false limits this round to local nodes.

Returns Promise<void> — Resolves after file writes and grouped `saved`/`updated` notices; listeners receive sets of affected nodes. Safe to retry after failure: failed file nodes remain dirty.

Throws — {Error|AggregateError} One or multiple filesystem write failures; fix access/path and retry `save()`.

version property

Read: get version()

Read a number useful for cache invalidation; local or parent data/membership changes advance it. The query does not mutate state.

write(path, value) method

Route an immediate write from the last node backward to the first writable exact owner or accepting catch-all, otherwise to the parent.

path string[]
Nonempty absolute safe string-segment path; root writes are unsupported.
value *
JSON-shaped value copied on storage; `undefined` deletes the owned path, revealing lower layers. An absent deletion without an owner is not captured by a catch-all.

Returns boolean — Whether a node accepted the write (including an equal no-op); false if no node takes it. Changed file nodes are queued for save; changed memory nodes stay in memory.

Throws — {TypeError} Invalid/root path or closed store; correct the input or use an open store before retrying.

Skills — activation, explicit composition, and resource overlays

lib/env.js

  • skills() — Effective skills, read fresh from package/settings/configured/environment/ project layers.

tools/skill.js

async skill({ names } = {…}, context)

List or atomically activate named skills. Active names succeed without reinjection, even after disk edits; edits apply in the next context. No context means no active state.

context object
Agent capabilities (Env and context-local loadedSkills query).

Returns Promise<string|{result: string, system: string[]}> — Catalog or activation payload.

Throws — {Error} Invalid arguments/unknown names/registry failure; no instructions are injected.

tools/resource.js

async resource(args = {…}, context = {…})

Read/list a skill overlay, falling back per path to a reachable named project. Project reads do not require agent-request permission; copies require writes.

context object
Borrowed Env/Agent and permission capabilities.

Returns Promise<string> — Source-labelled text, listing or copy destinations.

Throws — {Error} Invalid/unsafe paths, missing sources, denied permissions or I/O failure. Correct the input/permission before retrying; a failed folder copy may leave partial output. Owned fallback Envs are closed before settling.

Tool contract — PUBLISHES / REQUIRES / RETURNS (add a tool by reading this)

lib/env/tools.js

PUBLISHES — the module shape a tool file must export

lib/env/tools.js — tool-folder scanning (private to Env).

The scan is NOT recursive: only a root's TOP-LEVEL .js files are tool modules. Sub-folders are private to the tools — a well-designed tool is a thin wrapper (tools/read.js) whose helpers/libraries live beside it (tools/read/engine.js, tools/read/query.js, ...) and are never imported or published by the scan.

Module contract: a module's toolDescription(env) — or describe(env) when toolDescription is undefined (the fallback name) — returns an object keyed by exported function name, each value an MCP-like {description, inputSchema} (plus optional harness metadata: safe/trusted/ sandbox/onTimeout/secret/fn — never published). onTimeout is an Agent-facing callback, not a model-facing schema member. secret: true hides the tool from the PUBLISHED catalog (never sent to the model) while it stays callable directly (bin/scripts/tool, the TUI's /<tool> and Tools menu). A described function is normally published only when the module exports a callable of that name; an entry may instead carry its own fn (a closure built at scan time) for names that have no static export. The scan-time env argument lets a module read live settings to decide which such entries to contribute; it is REBUILT on every refresh, so settings edits take effect on the next tool-refresh. Modules without any of the three functions are omitted from the catalog (their import side effects still run). Duplicate names are diagnosed (throw), never resolved arbitrarily.

Every scan imports modules with a cache-busting ?v=<revision> (bumped on every refresh), so wrappers re-run and re-import their helpers (stamped with Env.toolRevision()). Bun IGNORES query strings on file:// URL imports — plain absolute paths only.

A module may also export settingsSchema() (zero-arg, sync, same scan cadence as toolDescription): { [settingKey]: {default, description} }, merged into the DEFAULTS SCHEMA (see lib/env/settings-schema.js, env.settingsSchema()) — a tool's own self-documentation for the setting(s) it reads (tools/read.js contributes read this way). Purely discovery metadata: an unknown settings key is never rejected either way.

async scanToolRoots(roots, env, { trustedRoots = [] } = {…})

Scan tool roots into a flattened name -> { fn, schema, file, safe? } map, plus every module's contributed settings-schema entries.

roots string[]
Tool roots to scan, in precedence/scan order. Missing or unreadable roots are treated as empty.
[env] object
Owning Env passed to each toolDescription(env) (or fallback describe(env)); omitted when no Env is available.
[options.trustedRoots=[] string[]
] - Roots authorized to honor a tool schema's `trusted: true` setting.

Returns Promise<{tools: Map<string, {fn: Function, schema: object, file: string, safe?: true, trusted?: true}>, settingsSchema: Object}> — A map of unique callable tools and the merged settings schema.

Effects — Publishes the current revision, imports top-level eligible `.js` modules (running their module initialization), and may report suppressed import-time stdout on stderr.

Throws — {TypeError} If a module returns an invalid settings/tool description or declares a non-function `onTimeout`, `detect`, `available`, `readOnly`, or `inProcess` hook.

Throws — {Error} If two scanned modules contribute the same callable tool name, a module import fails, or a module's settings/description callback throws.

lib/env/tool-registry.js

PUBLISHES — registration and the catalog (Env.tools(safe, selector) -> ToolInfo; provider tools shadow)

registerTool(env, name, fn, schema, { builtin = false, file, allowTrusted = builtin } = {…})

Register a tool into the flattened callable lookup (the scan-load calls this; programmatic tools may too). Duplicate flattened names are diagnosed, never resolved arbitrarily. A schema with safe: true publishes the tool for read-only safe mode. A tool with optional mutating arguments MUST reject those arguments in safe mode and publish a synchronous readOnly(args) classifier for sequential scheduling. Otherwise safe tools are read-only for every invocation. Safe-mode Agents publish and execute ONLY safe tools. A missing safe key means false. Every tool receives the same harness context; interaction is not execution policy. A schema with trusted: true requests unrestricted host execution. It is honored only for tools loaded from the package root; other roots cannot grant themselves trust. A package-root inProcess(args) classifier can also opt individual calls out of the forked worker; it is never honored for tools from other roots or programmatic registrations. Trusted tools may still request the OS sandbox. A schema with sandbox: true marks an ordinary tool for the OS-LEVEL write sandbox (lib/sandbox/os.js — the seatbelt/ bwrap kernel jail): the Agent runs its forked worker under the wrapper, so the tool cannot WRITE outside the working folder no matter what its code does. The jail needs a process boundary: only file-scanned tools get it. sandbox: false is invalid policy input and ignored: an unsafe, untrusted tool is always sandboxed. secret: true hides the tool from the published (model-facing) catalog while it stays directly callable (bin/scripts/tool, the TUI's /<tool> input and Tools menu) — for tools meant for the human operator, not the model. onTimeout may be an Agent-facing callback for bounded cleanup or a final result; it is never sent to the provider. All harness keys are metadata, stripped from the published catalog (toolSchemas).

env object
name string
flattened tool name
fn Function
the callable
schema object
MCP-like {description, inputSchema, safe?, trusted?, sandbox?, secret?, onTimeout?, readOnly?}; readOnly(args) must synchronously identify non-mutating calls. Only scanned package-root tools may use inProcess(args).

Returns Function — fn

toolsList(env, safe = false, selector)

The catalog: eligible tools (only safe: true ones when safe), and a pair's provider tools, which SHADOW the same-named global tool (its schema and safety unless the provider declares its own).

env object
[safe=false] boolean
[selector] string
`<endpoint>/<model>`

Returns Map<string, object> — name -> ToolInfo

async callTool(env, name, args, context = {…})

Exact flattened lookup + invoke. Missing names are ordinary errors (Agent surfaces them as tool-result errors), never a crash. The optional TOOL CONTEXT is handed to every tool as its second argument. In-process dispatch supplies agent-local storage and messageSet(text), plus background.start(options)/list()/output(id, from)/stop(id) capabilities. Detectors receive storage, messageSet(text), agent, and env. Forked sandbox workers receive the reduced data context plus explicit JSONL request/reply bridges; host callbacks never cross that boundary.

env object
name string
args object
[context] object
harness tool context ({question, storage, messageSet, background})

Returns Promise<*> — the tool's return value

tools/read.js

PUBLISHES — a worked example: the minimal wrapper shape (read-only, no ctx)

/**
 * tools/read.js — the `read` tool: a cwd-rooted file access tool,
 * read-only unless `target` saves the result. Thin WRAPPER publishing the callable
 * implemented under tools/read/. The tool scan is NOT recursive:
 * sub-folders are never scanned. read/ owns query normalization,
 * guarded execution and serialization; `target` saves through write/save.js.
 * Shared contracts stay explicit. No helper is independently published as a tool.
 *
 * One exception BY DESIGN: tools/guard/ is the shared guard layer
 * EVERY tool imports (read, write, edit, bash) — the deterministic
 * path resolver (guard/resolve.js) and the fast-path content
 * trip-wire (guard/paths.js); path traversal policy is global,
 * never per-tool (see those files).
 *
 * Helper imports are stamped with the shared refresh revision
 * (toolRevision() — lib/tool-runtime.js, the tiny dependency-free
 * runtime the tool scan publishes it through): every refreshTools()
 * bumps it, the wrapper re-runs, and the helpers re-import fresh —
 * editing any read/* module applies on refresh without touching this
 * wrapper.
 */

import { toolRevision } from "../lib/tool-runtime.js"; // the tool-runtime leaf: one instance across cache-busted imports — no whole-library load for a timestamp

const timestamp = toolRevision(); // shared tool-registry revision
const { read, readDescription, readSettingsSchema } = await import(`./read/read.js?now=${timestamp}`);

export { read };

export function toolDescription() {
  return { read: readDescription() };
}

/** This tool's own contribution to env.settingsSchema(). */
export function settingsSchema() {
  return readSettingsSchema();
}

tools/write.js

PUBLISHES — a worked example: sandboxed (`sandbox: true`, forked, write-jailed)

/** Mutating text writer. Saving read results lives in read.target; both share tools/write/save.js. */
import { toolRevision } from "../lib/tool-runtime.js";
const { saveTarget, savePayload } = await import(`./write/save.js?revision=${toolRevision()}`);

/** Atomically create or overwrite a text file; the destination changes only after guarded checks pass. */
export async function write(args = {}, context) {
  if (context?.safe === true) throw new Error("write is unavailable in safe mode");
  if (!args || typeof args !== "object" || Array.isArray(args)) throw new TypeError("write arguments must be an object");
  for (const key of Object.keys(args)) if (!["path", "content", "ask"].includes(key)) throw new Error(`Unknown write field: ${key}`);
  if (typeof args.content !== "string") throw new TypeError("write.content must be a string");
  if (![undefined, null, "", 0, -1].includes(args.ask) && typeof args.ask !== "boolean") throw new TypeError("write.ask must be a boolean");
  const target = await saveTarget(args.path, context);
  return savePayload(target, { payload: Buffer.from(args.content) }, context, { ask: args.ask === true });
}

export function toolDescription() {
  return { write: { trusted: true,
    description: "Create or overwrite a text file in the Agent folder. Use edit for targeted changes; use read with target to copy files or save read results.",
    inputSchema: { type: "object", additionalProperties: false, required: ["path", "content"], properties: {
      path: { type: "string", description: "Destination path inside the Agent folder, relative to it or an absolute contained path." },
      content: { type: "string", description: "Full file text; an empty string creates or clears the file." },
      ask: { type: "boolean", description: "Ask permission if the text references an existing outside path or an existing absolute project path; without permission, the write is refused." },
    } },
  } };
}

tools/process.js

PUBLISHES — Agent-owned background commands: bash(background) and process-list/output/stop

async processList(_args = {…}, context)

List processes started by this Agent's background bash. Read-only.

async processOutput({ id, from } = {…}, context)

Read buffered output from one of this Agent's background processes. Read-only.

async processStop({ id } = {…}, context)

Stop one of this Agent's background process groups. Unavailable in safe mode.

toolDescription()

lib/agent.js

PUBLISHES — Agent's current filtered model-facing descriptors

  • get tools() — Current model-facing tools: `const tools = await agent.tools`; `tools.get(name)` returns a provider-neutral descriptor with its schema.

lib/agent/tool-context.js

REQUIRES — internal dispatch constructs every in-process call's (args, ctx); tools receive capabilities, hosts do not construct them

toolContextFor(agent, call, { trusted = false, info } = {…})

Build the context for an in-process tool or its timeout callback.

agent object
Owning Agent.
[call] object
Active call linkage.

Returns object — Live execution capabilities; never sent to a provider.

lib/agent/tool-exec.js

REQUIRES — dispatch: which tools fork (sandboxed worker, REDUCED ctx) vs stay in-process (full ctx)

async callToolFor(agent, name, args, call, dispatch)

Invoke one tool: SANDBOXED (forked child, lib/agent/tool-sandbox.js) when the tool is file-scanned (a child can rebuild it from the tool roots). Execution policy is declarative and independent of harness interaction: every unsafe, untrusted tool forks under the OS write jail; safe/read-only and trusted tools execute in-process unless they explicitly request sandbox: true. sandbox: false is ignored. agent.folder only supplies an OS sandbox root — it never forces a worker.

agent object
the owning Agent (env, safe mode, question bridge, tool-data events)
name string
the tool name to invoke
args *
parsed tool arguments (timeout directives are applied via prepareToolTimeout)
call object
the tool-call record (passed to the tool context and event payloads)
[dispatch] object
the active dispatch scope ({controller, sandbox, done, finished}); when omitted one is created via withToolDispatch

Returns Promise<*> — the tool's raw return value (not normalized to content blocks)

Throws — {Error} on abort, timeout (ToolTimeoutError), or any tool failure; a forked failure's `system` payload rides back on error.system

tools/question.js

REQUIRES — a worked example: an interactive tool (`interactive: true`) using ctx.question.ask

tools/question.js — the question tool: ask the user one or more structured questions mid-task (pi question-tool semantics): 1–4 questions, each with a ≤16-character header chip, 2–16 options (label ≤60 characters + description, optional preview), and an optional multiSelect flag. The engine appends its own free-text affordance (pi's "Type something." row) — it is NOT an option here.

DISENTANGLED from the rendering engine: the tool owns argument validation and answer normalization; the harness's QUESTION BRIDGE (context.question.ask — provided by the Agent, which received it from the TUI/HTML/WebSocket binding) owns rendering the questions and previews and collecting the answers. The bridge is the INTERNAL second argument, never part of the published schema. Bridge answer shape, one per question: { labels: string[] } — the chosen option label(s) { text: string } — a custom typed answer { abandoned: true } — the user pressed Esc Without a bridge the tool REFUSES (throws): a session with no one to ask cannot ask — CLI bindings wire the bridge ONLY into the ACTIVE agent (the one being displayed), so a session running headless execution has no question handler and every question is refused. The refusal text tells the model to proceed with its best judgment instead.

Read-only (safe: true): asking the user mutates nothing — safe-mode Agents may ask questions. The forked, OS-sandboxed worker uses the Agent's typed fd-3/fd-4 JSONL ask/answer bridge; no host callback crosses the sandbox.

lib/agent/tool-worker.js

REQUIRES — forked worker's serializable ctx: env, sandbox, deadline, onData, agent.folder; question bridge when enabled; no live Agent or IO

lib/agent/tool-worker.js — the child-process side of lib/agent/tool-sandbox.js.

Spawned as <runtime> lib/agent/tool-worker.js: reads ONE JSON line from stdin — {dir, settings, roots, name, args, cwd, projectCwd, file?, questionBridge?, detached?} — rebuilds an Env from it, loads the ONE tool module the parent's registry named (file; never a root rescan), invokes the tool, and writes ONE JSON result line to stdout: {ok: true, value} or {ok: false, error}. All ordinary failures are reported on stdout with exit 0; a nonzero exit (or silence) means the tool destroyed the process (e.g. process.exit) and the parent reports that instead — which is the entire point of the sandbox.

THE RESULT LINE IS THE LAST THING WRITTEN — and it must survive everything a tool printed at IMPORT (a script without an import.meta.main guard can dump megabytes onto stdout while the tool scan loads it): the process exits only after the writable side FLUSHED. process.exit() right after write() discards user-space pipe buffers — the truncated-result race the parent reports as "worker exited without a result (code 0)".

This file is executed, not imported, and must stay runtime-agnostic (no Bun-only APIs).

lib/agent/tool-exec.js

RETURNS — every shape a tool's return value may take

resultContent(value)

Normalize a tool return value into result content blocks — the full tool-answer contract. A tool may return: - a string → one text block; - an array of content blocks → used as-is; - {content: [...]} → the content array; - any other JSON value → JSON.stringify'd into one text block; - {result, system?, display?} → result is normalized by the rules above, while system (string or string[]) appends as System messages after the complete tool-call batch and display is retained on it for context viewers (handled by the caller, not here). Throwing fails the call: the error message becomes the tool-result error content (error: true on the result message).

value *
the tool's return value

Returns Array<object> — content blocks for the tool-result message

async executeToolCall(agent, call, { notify = false } = {…})

Execute one tool call. NEVER throws — failures become tool-result error messages for the model.

Returns { message, system, display }: the tool result, plus any SYSTEM PAYLOAD and any DISPLAY PAYLOAD the tool attached. A tool returning a plain object with a system and/or display key answers briefly via result (any normal return shape) while system (a string or string[]) appends as System messages after all results in the assistant's tool-call batch, and display (a string or string[]) is retained on that ToolResult for context viewers and also rides to the binding. It is not exposed as normal tool-result content to the model; a tool can show data without sending it to the model. A THROWN error may carry the same system payload as error.system: the error becomes the tool-result error AND the payload appends as System messages.

agent object
the owning Agent
call object
the tool call ({name, arguments, callId})
[options] object
[options.notify=false] boolean
emit EVENT.TOOL_EXECUTE before execution

Returns Promise<{message: object, system: Array<string>, display: Array<object>}> — the outcome (see executeOutcome); never rejects

TUI input, cursor, mouse-selection, and questionnaire editing

lib/app/tui/input-controller.js

lib/app/tui/input-controller.js — the draft input box's MODEL-OWNED state: value/caret/selection (mirrored from GTUI's input control — see lib/app/gtui/controls.js's controlled-input contract: the control computes edits from the LAST rendered value/caret/selection, so the app must hold and re-supply them) plus completions, which are pure tui-app policy (completion.js's computeCompletions) that GTUI's input node only ever displays.

Tab/Shift+Tab/Down/Up/Enter double as completion-list navigation ONLY while completions are showing — the app declares them via GTUI's bindings(model) so those keys bypass the input control entirely and move, accept, or dismiss the completion list instead.

lib/app/gtui/controls/manager.js

createControls(emit, options = {…})

Host-private controlled input/menu/scroll/overlay mechanics.

lib/app/gtui/terminal-input.js

createTerminalInput(input, emit, { beginBurst = () => {}, endBurst = () => {}, onBackground = () => {} } = {…})

Decode raw terminal input into GTUI key, paste, and pointer events.

input NodeJS.ReadableStream
Optional readable stream; data listeners are attached when supported.
emit function(object): void
Receives decoded semantic events. Exceptions propagate through the emitting callback.
[options={}] object
Hooks for input burst boundaries and background notifications.
[options.beginBurst=()=>{}] function(): void
Called before each input chunk is filtered.
[options.endBurst=()=>{}] function(): void
Called after each chunk, even if filtering throws.
[options.onBackground=()=>{}] function(...*): void
Receives background notifications from the byte filter.

Returns function(): void — Cleanup function that detaches listeners and destroys the decoder.

lib/app/tui/questionnaire-view.js

lib/app/tui/questionnaire-view.js — the open question (agent-adapter.js's question bridge + questionnaire.js's pure state) into GTUI nodes: the options render through the SAME controlled menu used everywhere else (arrow/enter/escape are native), plus a real input control for a custom typed answer, not just a display label.

Enter and Space both select/toggle the focused row. On a single-select answer Enter also moves the highlight to the Submit row for confirmation (the input's Enter does the same jump). Only selecting the final submit row commits the answer. The question text, option labels, and option DESCRIPTIONS all SOFT-WRAP (labels through the menu's own word-wrap; descriptions as their own wrapped text nodes), so nothing clips at the overlay edge. A description/preview belongs to the option: it shows ONLY while that option is the HIGHLIGHTED menu row (tui-app tracks menu.change into question.highlight), so extra information never floods the overlay. A code preview renders through the shared markdown pipeline (fenced block) — the same highlighting the transcript uses. Crossing the input's top row with ↑ (or its bottom with ↓) returns focus to the menu (tui-app's key policy on GTUI's bubbled boundary move). GTUI still owns only generic menu navigation/selection and never sees questionnaire state.