# Agent > **The inference boundary and the loop around it.** A `ProviderInterface` is the one pluggable LLM contract — turn a conversation (plus optional tools) into either an assembled `ProviderResult` (`generate`) or a stream of channel-tagged `ProviderDelta`s that RETURNS the same assembled result (`stream`). Everything else in this module is the machinery you reach for once you have a provider: a **tool registry** to dispatch the model's calls, a **conversation store** + **turn context** to assemble the prompt, and an **agent loop** that drives context → provider → tools → repeat until the model stops. This module owns only the ABSTRACT boundary + its runtime — any backend that implements `ProviderInterface` drops in unchanged; a concrete provider is supplied by the host application. > > **What it deliberately is NOT.** This is a lean kit of composable primitives, not a heavyweight agent framework. There is no hidden global state, no plugin lifecycle, no prompt-template DSL, no implicit memory store — tools are advertised STRUCTURALLY (`tools.definitions()`, never serialized into the prompt), the conversation is a plain immutable message list you append to, and a turn is bounded by ONE plain `AbortSignal` (a cancel / [timeout](timeout.md) / [budget](budget.md) folded via `AbortSignal.any`) rather than a bespoke cancellation system. Observation is push-OR-pull and confined to the `Agent` alone; everything you compose under it (the provider contract, the tool registry, the conversation store, the context that wires them) stays **event-free**. Each capability is its own small factory you can use standalone or wire together — you only pay for what you reach for. > > **The load-bearing guarantees.** A turn is always BOUNDED (`signal` + `timeout` + `budget` via `AbortSignal.any`) and always TERMINATES (tool iteration capped at `limit`). A cancel is not an error: it commits a PARTIAL `AgentResult` that RESOLVES (`partial: true`), so only a genuine provider/tool failure ever rejects. `generate` and `stream` share ONE private `#run`, so the one-shot result can never diverge from the live stream. A tool that throws is ISOLATED into a `ToolResult.error` the model can react to — it never escapes the loop. A scoped-out tool is neither advertised nor callable. And a buggy push observer can NEVER corrupt the run — the emitter isolates a listener throw and routes it to its own `error` handler. The deterministic logic is pinned in `src:core` against a scripted `ProviderInterface`. Source: [`src/core`](../../src/core). Surfaced through the `@src/core` barrel. > > **The workspace — an immutable file primitive + an in-memory edit surface a model can drive.** Alongside the loop, this module ships the agent's **files capability** (both environment-agnostic — zero `node:*`, zero DOM). A `File` is a PLAIN frozen record (produced by `createFile`, not a class) — a `path`-addressed body of `FileContent` (a TAGLESS text-vs-binary union narrowed by the `isText` / `isBinary` / `isImage` guards, no `modality` discriminant), carrying a lifecycle `state`, with `size` / `lines` derived ONCE when built so they can never drift; the `path` IS its identity (there is no `id`). A `Workspace` is a mutable, `path`-keyed working set of those immutable files with the full edit surface — read / write / search / replace / move / remove — that REPLACES the file at a path on every edit rather than mutating it in place (the one observable piece below the loop — it owns a §13 `emitter`). The active workspace is the agent's SOLE document/image context: registered on `context.workspaces`, its ACTIVE workspace `build()` renders BY CARRIER every turn (text files → a `## Workspace` system section the model reads in-prompt, image files → multimodal attachments on the last user turn); a workspace-editing tool that wraps a live workspace as an `operation`-keyed `ToolInterface` a model can READ and EDIT through lives in `@orkestrel/tool`. What it deliberately is NOT: no disk, no `fs`, no sync lifecycle, no durable FileStore, no JSON contract over the registry — those are named, additive deferrals (a future non-image binary, e.g. a PDF, is the same binary arm with a new `BinaryMIME`, with the surface below unchanged). `BinaryMIME` is the canonical file-content MIME. The deterministic logic is pinned in `src:core`. ## Surface This module is the ABSTRACT inference boundary: the `ProviderInterface` contract plus the tool registry / conversation store / turn context / agent loop that surround it. It defines only the abstract boundary — a concrete provider implementing `ProviderInterface` is supplied by the host application. Everything below takes any `ProviderInterface`. A provider turns a conversation (plus optional tools) into a turn: `generate` resolves the assembled `ProviderResult` (content + any tool calls + any usage); `stream` yields channel-tagged `ProviderDelta`s as they arrive (`content` for answer text, `thinking` for live reasoning) and RETURNS the same assembled result when the stream completes, so a caller can render tokens / reasoning live and still get the full outcome. Both bound the call with an `AbortSignal`: ```ts import { createAbort } from '@orkestrel/abort' import type { ProviderInterface } from '@orkestrel/agent' declare const provider: ProviderInterface // any concrete implementation supplied by the host app const abort = createAbort() const messages = [{ id: '1', role: 'user', content: 'Say hello.' }] as const const result = await provider.generate(messages, abort.signal) result.content // the assembled content result.usage // { prompt, completion, total } — folds into a token budget const generator = provider.stream(messages, abort.signal) let step = await generator.next() while (!step.done) { if (step.value.type === 'content') process.stdout.write(step.value.text) if (step.value.type === 'thinking') process.stderr.write(step.value.text) step = await generator.next() } const streamed = step.value // the assembled ProviderResult (content === the joined content deltas) ``` Pass `tools` (a non-empty `ToolDefinition[]`) to advertise callable tools for the turn; when the model calls one, `result.tools` is a `ToolCall[]` (each with a guaranteed `id`, the tool `name`, and parsed `arguments`). Aborting a `stream` mid-flight throws a `ProviderAbortError` whose `partial` holds whatever streamed before the cancel. Register the callable tools in a `ToolManager` (a `Tool` binds the schema the model sees to the handler that runs it). `definitions()` yields the `ToolDefinition[]` to hand the provider; when the model emits a `ToolCall`, `execute` runs it with per-call error isolation — a handler throw becomes a `ToolResult.error` (the model can react to it, the call never escapes), an unknown name a not-found error, and the batch form never fails as a whole: ```ts import { createTool, createToolManager } from '@orkestrel/agent' const tools = createToolManager() tools.add([ createTool({ name: 'add', description: 'Add two numbers', parameters: { type: 'object', properties: { a: { type: 'number' }, b: { type: 'number' } } }, execute: (args) => Number(args.a) + Number(args.b), // narrow the model-supplied unknown (§14) }), createTool({ name: 'now', execute: () => Date.now() }), ]) const definitions = tools.definitions() // hand these to provider.generate / .stream as `tools` const results = await tools.execute([ { id: '1', name: 'add', arguments: { a: 2, b: 3 } }, // → { id: '1', name: 'add', value: 5 } { id: '2', name: 'ghost', arguments: {} }, // → { id: '2', name: 'ghost', error: 'tool not found: ghost' } ]) ``` Collect a turn's conversation in an `AgentContext` — the lean `system` + `messages` + `tools` composition. Add turns through `context.messages` (the ACTIVE conversation's live tail — a default conversation is always present; it satisfies `MessageManagerInterface`, minting each `id` on `add` and keeping messages immutable + in insertion order), then `build()` the provider input: `[systemMessage?, ...messages]`. Tools are advertised structurally via `context.tools.definitions()` (handed to `generate` / `stream`), never serialized into the message array: ```ts import { createAgentContext, createToolManager } from '@orkestrel/agent' import type { ProviderInterface } from '@orkestrel/agent' import { createAbort } from '@orkestrel/abort' declare const provider: ProviderInterface const abort = createAbort() const context = createAgentContext({ system: 'You are concise.', tools: createToolManager() }) context.messages.add([ { role: 'user', content: 'What is 2 + 3?' }, // the `id` is minted by add, not supplied { role: 'user', content: 'Reply with just the number.' }, ]) const input = context.build() // [{ role: 'system', content: 'You are concise.' }, …the two user turns] const definitions = context.tools.definitions() // tools reach the provider here, NOT in `input` const result = await provider.generate(input, abort.signal, definitions) ``` `context.messages.add` mints each message's `id` (a random UUID) and returns the created message(s); `build()` is computed fresh on every call, so it always reflects the current conversation. Without a system prompt, `build()` is just the conversation; the tool registry never appears in its output. Drive the whole turn with an `Agent` (`createAgent`) — it composes the provider, its `AgentContext`, and the tool registry into the bounded context → provider → tools → repeat loop. Seed the conversation through `agent.context.messages`, then either `generate()` for a one-shot `AgentResult` or `stream()` for a live `AgentChunk` stream (`token` answer deltas, `think` reasoning deltas, `tool` dispatches, `usage`) whose `result` resolves the same `AgentResult`. `generate` DRAINS that same stream, so the two can't diverge: ```ts import { createAgent, createTool, createToolManager } from '@orkestrel/agent' import type { ProviderInterface } from '@orkestrel/agent' import { createTokenBudget } from '@orkestrel/budget' declare const provider: ProviderInterface const tools = createToolManager() tools.add(createTool({ name: 'add', execute: (args) => Number(args.a) + Number(args.b) })) const agent = createAgent(provider, { system: 'You are concise.', tools, limit: 4, // cap tool iterations timeout: 30_000, // wall-clock deadline for the whole turn budget: createTokenBudget({ max: 50_000, scope: 'total' }), // cost ceiling }) agent.context.messages.add({ role: 'user', content: 'Use the add tool to add 2 and 3.' }) const stream = agent.stream() for await (const chunk of stream.events) { if (chunk.type === 'token') process.stdout.write(chunk.content) // live deltas if (chunk.type === 'think') process.stderr.write(chunk.content) // live reasoning if (chunk.type === 'tool') log(chunk.call, chunk.result) // a dispatched tool + its result } const result = await stream.result // { content, usage?, partial } — usage summed across the turn ``` Both `generate` and `stream` accept optional per-run `AgentRunOptions` -- `think` / `schema` (forwarded to the provider as `ProviderStreamOptions`) plus `limit` / `timeout` / `budget` / `signal`, each OVERRIDING its `AgentOptions` construction default for THIS run only (`??` semantics -- omitted means the constructed default applies, so a caller that passes no options runs exactly as before). A per-run `signal` COMPOSES with (never replaces) a constructed `signal` -- either aborting cancels the run; a per-run `budget` is `start()`ed for that run and is the one the loop charges (the constructed `budget`, if any, is left untouched): ```ts const agent = createAgent(provider, { tools, limit: 10, timeout: 60_000 }) // construction defaults agent.context.messages.add({ role: 'user', content: 'Summarize this doc.' }) // A tighter, structured-output run -- overrides limit + timeout, adds a schema, for THIS call only. const result = await agent.generate({ limit: 2, timeout: 5_000, schema: { type: 'object', properties: { summary: { type: 'string' } } }, }) ``` `schema`, like `think`, rides into `provider.stream` as a `ProviderStreamOptions` -- the loop composes both into ONE options object, omitting whichever key is unset (an options object is passed only when at least one of `think` / `schema` is present, preserving the prior think-only behavior when neither is given). The turn is bounded by ONE cancel folded from the external `signal` + the `timeout` deadline + the `budget` signal via `AbortSignal.any`; `agent.abort(reason)` (or `stream.abort(reason)`) fires it. A cancel — external, deadline, budget, or `abort()` — commits a PARTIAL result: the `result` promise RESOLVES with `{ partial: true, content: }`, never rejects (a cancel is not an error); only a genuine provider / tool error rejects. An optional `scheduler.yield`s between turns; tool iteration is capped at `limit` (default `DEFAULT_AGENT_LIMIT`). Exhausting `limit` while the model still holds unresolved tool intent (it requested tools on the very last allowed turn) is a DISTINCT, non-cancel cause of `partial: true` — it fires an `exhaust` event (the turns reached) instead of `abort`. A natural finish on the last allowed turn, or `limit: 0` (which never enters the loop), stays `partial: false`. `agent.status` transitions `idle` → `running` → `done` / `error`. Gate the model's tool calls with an optional `Authority` (`createAuthority`) — a synchronous policy gate the loop consults BEFORE each call runs, passed via `AgentOptions.authority`. It walks ordered `rules` first-match-wins (a matched rule allows unless its `allowed` is `false`), falling back to a configurable default when none match — allow-unmatched by default (a denylist), or deny-by-default when its `fallback` denies (an allowlist). A DENIED call is fed back to the model as a denial `ToolResult` (a `tool` chunk + a tool message carrying `denied: `) instead of being executed — no tool run, no budget cost — so the model sees the denial and can react; an ALLOWED call dispatches normally. Without an `authority`, every call dispatches as before: ```ts import { createAgent, createAuthority, createTool, createToolManager } from '@orkestrel/agent' const tools = createToolManager() tools.add([ createTool({ name: 'add', execute: (args) => Number(args.a) + Number(args.b) }), createTool({ name: 'delete', execute: (args) => drop(args.id) }), ]) // A denylist: deny `delete`, allow everything else (the default allow fallback). const authority = createAuthority({ rules: [ { match: (c) => c.call.name === 'delete', zone: 'restricted', allowed: false, reason: 'read-only mode', }, ], }) const agent = createAgent(provider, { tools, authority }) agent.context.messages.add({ role: 'user', content: 'Delete record 42.' }) // When the model calls `delete`, the loop feeds back { error: 'denied: read-only mode' } — never runs it. ``` Alongside the conversation store sits the STANDALONE `InstructionManager` a richer context assembles a prompt from — named directives, keyed by `name`, listed by descending `priority`. It mirrors the registry shape — `add` (one or a batch, §9.2) MINTS each `id` and OVERWRITES a same-key entry (last write wins), an `instruction(name)` / `instructions()` accessor pair, `remove` (one or a batch) / `clear` / `count` — holds IMMUTABLE entries, and is observable (`emitter` with an `add` / `remove` / `clear` event map, wired via the reserved `on` option; an `error` option receives a listener's throw, §13). It carries the two **build-contract** members a context's assembly step calls: `description` (the section header text, `'## Instructions'`) and `format(instruction)` (per-item rendering — the instruction's `content`): ```ts import { createInstructionManager } from '@orkestrel/agent' const instructions = createInstructionManager() const safety = instructions.add({ name: 'safety', content: 'Refuse unsafe requests.', priority: 10, }) instructions.description // '## Instructions' instructions.format(safety) // 'Refuse unsafe requests.' ``` Documents and images reach a turn through the ACTIVE WORKSPACE — the SOLE document/image context (see [Workspaces](#workspaces--the-file-edit-surface) below): a workspace's TEXT files render as fenced reference blocks in a `## Workspace` system section, its IMAGE files' base64 `data` attaches to the last user message. For a multimodal turn, a message carries base64 image data on its optional `images` field — `MessageInterface` / `MessageInput` both accept `readonly images?: readonly string[]`, which a vision-capable provider forwards onto the wire message (an empty / absent array is never sent). It is INPUT-only; `ProviderResult` is unchanged. ```ts import type { ProviderInterface } from '@orkestrel/agent' declare const provider: ProviderInterface // a vision-capable model const result = await provider.generate( [{ id: '1', role: 'user', content: 'Describe this image.', images: [''] }], abort.signal, ) ``` `AgentContext` WIRES the instruction manager + the workspace registry in. Beyond `system` + `messages` + `tools`, a context exposes its own `instructions` manager and `workspaces` registry (pass pre-built ones via `AgentContextOptions`, or fresh empty ones are created), and `build()` folds them into the turn. The assembly order is: ONE leading `system` message whose content is the system prompt, then the non-empty instructions block (its `description` header followed by every item's `format`), then the ACTIVE workspace's text files (a `## Workspace` section of fenced reference blocks) — joined by blank lines; then the conversation. With no instructions, no active workspace, and no scope it builds exactly the lean `[systemMessage?, ...messages]`. The active workspace's scoped-in image files' base64 `data` is attached to the LAST user message (a vision provider reads images off a user turn) — the text files ride the system block, the image payload rides the message: ````ts import { createAgentContext } from '@orkestrel/agent' const context = createAgentContext({ system: 'You are a code reviewer.' }) context.instructions.add({ name: 'tone', content: 'Be terse.', priority: 10 }) context.workspaces.add().write('src/main.ts', 'export const x = 1') // the active workspace context.messages.add({ role: 'user', content: 'Review this.' }) const input = context.build() // input[0] = { role: 'system', content: // 'You are a code reviewer.\n\n## Instructions\n\nBe terse.\n\n## Workspace\n\nFile: src/main.ts\n```typescript\nexport const x = 1\n```' } // input[1] = { role: 'user', content: 'Review this.' } ```` #### Conversations & compaction Above the flat `MessageManager` sits the `Conversation` (`createConversation` / a `ConversationManager`) — it OWNS its messages DIRECTLY (the flat store verbs folded in, like a `Workspace` owns its files) and COMPACTS the older ones into summarized `sections` so a long history fits a turn's context window without discarding the originals. Append turns through the conversation's own `add` (the LIVE uncompacted tail; `message` / `messages` / `remove` / `clear` / `count` round it out); `compact()` folds the older live messages into a summarized `SectionInterface` (retaining their originals), regenerates the conversation rollup `summary`, and shrinks `view()` — the model input, where each section becomes ONE summary message followed by the live tail. Compaction is driven by a **provider-agnostic** `ConversationSummarizer` seam (`(messages) => Promise`) the agent RUNTIME supplies — core never imports a provider — so a `compact()` without one throws a `ConversationError`. `keep` retains a recent tail (default `DEFAULT_CONVERSATION_KEEP` = `0`, fold all); `rehydrate(id)` / `search(query)` read the retained originals: ```ts import { createConversation } from '@orkestrel/agent' import type { ProviderInterface } from '@orkestrel/agent' declare const provider: ProviderInterface // The summarizer seam — built from the provider by the runtime; core stays provider-agnostic. // Append the instruction as the FINAL user turn: a chat model emits nothing when the prompt // ends on an assistant turn, so a leading-system instruction is unreliable. const conversation = createConversation({ summarize: async (messages) => ( await provider.generate( [ ...messages, { id: 's', role: 'user', content: 'Summarize the conversation so far concisely.' }, ], AbortSignal.timeout(30_000), ) ).content, keep: 2, // retain the two most recent turns verbatim on each compaction }) conversation.add([ { role: 'user', content: 'My name is Ada.' }, { role: 'assistant', content: 'Nice to meet you, Ada.' }, { role: 'user', content: 'What did I say my name was?' }, ]) const section = await conversation.compact() // folds the older turns → a summarized section conversation.view() // [
, ...the retained recent tail] — the model input conversation.summary // the regenerated rollup (a summary-of-summaries over all sections) conversation.search('ada') // case-insensitive across sections' originals + the live tail section && conversation.rehydrate(section.id) // the section's full original messages (a pure read) ``` Inject a `Conversation` into an `AgentContext` (`AgentContextOptions.conversation`) to make the MESSAGE SOURCE pluggable: `context.messages` then IS the conversation's live tail, and `build()` folds the conversation's `view()` (the per-section summaries + the live tail) — the conversation owns message inclusion via compaction, so the scope's `messages` allow-list is NOT applied (scope still filters instructions / tools / workspace files). With NO conversation, the message path is byte-for-byte the plain-manager behavior. Pass that same `conversation` to `createAgent` (`AgentOptions.conversation`, forwarded into the agent's context) together with an `AgentOptions.window` — a CONTEXT [`Budget`](budget.md) — to enable AUTOMATIC compaction: its `consume` is a token estimator (e.g. the exported `estimateMessages`) and its `max` is the context window. Each turn the loop estimates the CURRENT FULL prompt (the system block + the conversation's view + the turn's new messages — the exact next request) against this budget and, the moment that prompt REACHES the window, COMPACTS the conversation then continues on the rebuilt (smaller) view — the same consume-to-a-ceiling primitive as the cost `budget`, but the ceiling action is compaction (compact-and-continue) instead of abort. Omit `window` (or inject no conversation) and the loop is byte-for-byte unchanged; observe folds via the conversation's `compact` / `summary` events (there is no new agent event). See the [Contract](#contract) (clause 26) for the exact trigger point and the v1 single-level limitation. ```ts import { createAgent, createConversation, estimateMessages } from '@orkestrel/agent' import { createBudget } from '@orkestrel/budget' const conversation = createConversation({ summarize, keep: 2 }) // summarize: the runtime's seam const agent = createAgent(provider, { conversation, // the message source — context.messages IS its live tail // A context Budget: consume = a token estimator, max = the context window. The loop measures // the CURRENT FULL prompt against it each turn; when the prompt reaches the window it compacts // + continues on the rebuilt (smaller) view (compact-and-continue), never aborts. window: createBudget({ max: 8_000, consume: estimateMessages }), }) agent.context.messages.add({ role: 'user', content: 'Hi' }) await agent.generate() // folds older turns into a section mid-run when the prompt reaches the window, then continues ``` ##### One agent, many conversations (switching the active conversation) `agent.context.conversations` is the message-source registry (a SETTABLE mutable property like `context.scope`) — switch its ACTIVE conversation with `conversations.switch(id)` to switch the agent's message source. `context.messages` is DYNAMIC: it always points at the CURRENT active conversation's live tail (the SAME reference, no duplication) and follows a switch. The registry ALWAYS has an active conversation (a default is added when it has none). This is the real app pattern: ONE `Agent` over a `ConversationManager` of threads, switching the active conversation PER REQUEST — not an agent per thread. Each conversation accumulates its OWN history and compacts INDEPENDENTLY (one thread's sections never leak into another). The agent reads `context.conversations` / `context.messages` fresh on each run, so switching BETWEEN runs just works: ```ts import { createAgent, createConversationManager, estimateMessages } from '@orkestrel/agent' import { createBudget } from '@orkestrel/budget' const threads = createConversationManager({ summarize, keep: 2 }) // its defaults flow into each thread const agent = createAgent(provider, { conversations: threads, // the agent's message source window: createBudget({ max: 8_000, consume: estimateMessages }), }) // Per request: make the request's thread active, append the user turn, run. async function handle(threadId: string, text: string): Promise { if (threads.conversation(threadId) === undefined) threads.add({ id: threadId }) threads.switch(threadId) // SWITCH — context.messages now IS this thread's tail agent.context.messages.add({ role: 'user', content: text }) return (await agent.generate()).content } await handle('user-1', 'Hi, I am Ada.') // thread user-1 accumulates + compacts on its own await handle('user-2', 'What is 2 + 2?') // thread user-2 is fully independent await handle('user-1', 'What did I say my name was?') // back to user-1 — its own history is intact ``` > **Concurrency caveat.** Switch the active conversation BETWEEN runs, never DURING one (the loop reads the active conversation at run entry and drives it through to the end). The framework ships the SWITCH mechanism; the app owns concurrency policy — for threads that must run CONCURRENTLY, use a SEPARATE `Agent` per concurrent thread (each agent is cheap; they can share the provider and tool registry). Switching mid-flight would repoint the live run's message source under it. ##### Production behaviors of automatic compaction Auto-compaction (the `window` budget, above) is hardened for a long-running app: - **Pre-first-turn + run-entry reset.** The budget check runs BEFORE the first provider request AND between turns — so a RESUMED or already-long conversation whose INITIAL prompt already exceeds the window compacts immediately (not only after a tool turn). The `window` budget is reset at run entry, so no stale measurement carries across runs or a conversation switch. - **Non-fatal, observable summarizer failure.** If the AUTOMATIC `compact()`'s summarizer THROWS, the agent run does NOT crash: the loop skips compaction that turn, surfaces the error as a `compactError` event (so the failure is observable, never silently lost), and continues (the over-window prompt proceeds to the provider). Only the agent's AUTO path is resilient — a MANUAL `conversation.compact()` you call yourself still propagates its error. - **Futile-compaction guard (the single-level limit).** If `compact()` folds NOTHING (returns `undefined`) while the prompt is still over the window — i.e. the section summaries ALONE already exceed it — the loop STOPS auto-compacting for the rest of that run (a per-run latch), avoiding per-turn churn. The over-window prompt then proceeds to the provider, which surfaces a genuine context-length error if it truly cannot fit (the real limit). v1 compaction is single-level (it folds the live tail, not existing sections); a future tier compacts sections. ```ts agent.emitter.on('compactError', (error) => log('auto-compaction summarizer failed (run continues)', error), ) ``` #### Scoping a turn A `Scope` (`createScope` / a `ScopeManager`) is a NAMED allow-list filter the context applies at `build()` time AND at the loop's tool-advertise step. It carries three optional per-category lists keyed by each category's identity — `instructions` (by `name`), `tools` (by `name`), `files` (the active workspace's files, by `path`) — each THREE-WAY: `undefined` ⇒ NO constraint (all pass), `[]` ⇒ NONE pass, a non-empty list ⇒ only the listed keys. Conversation messages are NOT scoped — the active conversation owns message inclusion via compaction (`view()`), so there is no `messages` allow-list. Set the active filter through the mutable `context.scope` (default `undefined` ⇒ no filtering); `build()` reflects whatever scope is active when it runs (recomputed fresh each call). `narrow(config)` composes a tighter child scope by set-INTERSECTION (an `undefined` side imposes no constraint), so narrowing can only tighten — a parent-excluded key never returns: ```ts import { createAgent, createScope } from '@orkestrel/agent' const agent = createAgent(provider, { tools }) // tools holds `search` + `delete` agent.context.instructions.add([ { name: 'safety', content: 'Refuse unsafe requests.' }, { name: 'verbose', content: 'Explain every step.' }, ]) // This turn: only the `safety` instruction, and only the `search` tool. agent.context.scope = createScope({ name: 'read-only', instructions: ['safety'], tools: ['search'], }) const result = await agent.generate() ``` A scoped-out tool is filtered from BOTH the system context AND the `definitions()` the loop advertises to the provider — so the model never sees it and thus **can't call it** (neither described nor callable, not merely hidden). An `undefined` scope (or an `undefined` `tools` list) advertises every tool, as before; an empty `tools` list (`[]`) advertises none. A `ScopeManager` is the optional reuse registry for named scopes (keyed by a minted `id`, so two scopes may share a `name`); it is observable like the other managers. #### Customizing the format (the cascade) Each context section frames as `[open, ...items.map(render), close]` — a top line rendered once before the items, each item's text, and a bottom line rendered once after — with empty / absent slots dropped and the survivors blank-line (`\n\n`) joined. The three slots resolve INDEPENDENTLY through a cascade, MOST-SPECIFIC-FIRST; each level is OPTIONAL, what you omit falls through to the next, and omitting EVERYTHING leaves each section on its manager's built-in framing (byte-for-byte the prior output — just the header + items, no closing line). From most to least specific: 1. **Item override** — `format?: string` on a single `InstructionInput`: a fully-rendered string for THAT item, round-tripped onto the stored entity. Beats everything for that item's `render`. 2. **Manager-options override** — `format?: ContextSectionFormat<…>` on a manager's `Options` (an `{ open?; render?; close? }` trio): a per-section open / item-render / close override for that whole manager. Beats the provider default + the built-in. A manager exposes it as the `readonly framing` accessor, and its own `description` / `format(item)` already consult its `open` / `render` (so a manager used standalone renders with it). 3. **Provider default** — `format?: ContextFormatInterface` on a `ProviderInterface` (keyed by section kind — currently `instructions`): the model's preferred framing. OPTIONAL — an agnostic provider supplies none, and the agent loop simply passes `provider.format` (often `undefined`) into `build()`. 4. **Built-in** — the manager's hardcoded `description` getter + `format(item)` method (`## Instructions` + the content) — the floor for `open` and `render`. There is NO built-in `close`: an unset `close` simply yields no closing line. So for a section kind `K`, manager `M`, and a provider format `F`: **open** = `M.framing?.open ?? F?.[K]?.open ?? M.description` (manager-options > provider > built-in — the leading text has no per-item level); **per item** `I` = `I.format ?? M.framing?.render?.(I) ?? F?.[K]?.render?.(I) ?? M.format(I)` (item > manager-options > provider > built-in); **close** = `M.framing?.close ?? F?.[K]?.close` (manager-options > provider, NO built-in ⇒ no closing line when unset). The three resolve INDEPENDENTLY, so an override may set only the open, only the rendering, only the close, or any mix — and `open` + `close` together WRAP the whole group. (The `## Workspace` text section has no cascade level of its own — it renders with the fixed `fencedFile` framing.) ```ts import { createAgentContext, createInstructionManager } from '@orkestrel/agent' // Manager-options override — wrap the instructions as a closed XML group for this manager. const instructions = createInstructionManager({ format: { open: '', render: (one) => `${one.content}`, close: '', }, }) const context = createAgentContext({ instructions }) context.instructions.add({ name: 'tone', content: 'Be terse.' }) // An item override beats the manager `render` for THAT item only: context.instructions.add({ name: 'raw', content: 'ignored', format: 'Escalate.', }) context.build() // system block instructions section (the group wrapped by open + close): // '\n\nBe terse.\n\nEscalate.\n\n' ``` A provider declares its framing default by exposing `format` on its `ProviderInterface`; the `Agent` passes it into `build()` automatically. Because it is OPTIONAL, no provider is forced to supply one — omitting it leaves every section on the managers' built-in framing. ### Factories | API | Kind | Summary | | --------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `createTool` | function | Create a `ToolInterface` — a `ToolDefinition` schema bound to the `execute` handler that runs a call. | | `createToolManager` | function | Create an empty `ToolManagerInterface` — the tool registry with per-call error-isolated `execute`. | | `createConversation` | function | Create a `ConversationInterface` — messages above the flat store with compaction into summarized sections + a rollup, driven by a `ConversationSummarizer`. | | `createConversationManager` | function | Create a `ConversationManagerInterface` — the id-keyed registry of conversations WITH an active pointer (`add` auto-activates the first; `switch` re-points it); its default `summarize` / `keep` flow into created conversations. | | `createMemoryConversationStore` | function | Create the in-memory `ConversationStoreInterface` — a process-lifetime `Map` of `ConversationSnapshot`s (the default `open` / `save` backing). | | `createDatabaseConversationStore` | function | Create a `ConversationStoreInterface` over a `DriverInterface` (default `createMemoryDriver()`) — the snapshot as one opaque JSON column (durable twin). | | `createInstruction` | function | Create an immutable `InstructionInterface` — a named directive (`name` / `content` / optional `priority`). | | `createInstructionManager` | function | Create an empty `InstructionManagerInterface` — the name-keyed instruction registry (listed by descending `priority`). | | `createScope` | function | Create an immutable `ScopeInterface` — a named allow-list filter (`narrow` composes by intersection). | | `createScopeManager` | function | Create an empty `ScopeManagerInterface` — the id-keyed registry of reusable named scopes. | | `createAgentContext` | function | Create an `AgentContextInterface` — the richer `system` + managers + `messages` + `tools` + `scope` context; `build()` the input. | | `createAgent` | function | Create an `AgentInterface` — the bounded loop over a `ProviderInterface`; `generate` (one-shot) / `stream` (live). | | `createAuthority` | function | Create an `AuthorityInterface` — the synchronous policy gate (ordered first-match-wins rules + a configurable fallback). | | `createThinkSplitter` | function | Create a fresh `ThinkSplitterInterface` — the stream-stateful `` separator a provider routes content deltas through (one splitter per stream). | | `createAgentRegistry` | function | Create an `AgentRegistryInterface` — the named pools that resolve a job's names + `build` a seeded, signal-wired agent. | | `createAgentQueue` | function | Create a durable, bounded-concurrency `QueueInterface` of `AgentJobInput` → `AgentResult` (composes `createQueue`). | | `createAgentRunner` | function | Create a one-shot, fail-fast `RunnerInterface` of `AgentJobInput` → `AgentResult` with sub-agent fan-out (`createRunner`). | | `createFile` | function | Create a `FileInterface` (a frozen plain record, NO `id`) from a `path` + `FileContent` (+ `state?`); `size` / `lines` derived. | | `createTextContent` | function | Build the text `FileContent` arm (`text` + `language`) — the §4.2.3 split constructor. | | `createBinaryContent` | function | Build the binary `FileContent` arm (base64 `data` + `mime`) — the §4.2.3 split constructor. | | `createWorkspace` | function | Create a `WorkspaceInterface` — the mutable, `path`-keyed in-memory edit surface over immutable `File`s (its `id` minted when omitted). | | `createWorkspaceManager` | function | Create a `WorkspaceManagerInterface` — the id-keyed registry of workspaces WITH an active pointer (`add` auto-activates the first; `switch` re-points it). | | `createMemoryWorkspaceStore` | function | Create the in-memory `WorkspaceStoreInterface` — a process-lifetime `Map` of `WorkspaceSnapshot`s (the default `open` / `save` backing). | | `createDatabaseWorkspaceStore` | function | Create a `WorkspaceStoreInterface` over a `DriverInterface` (default `createMemoryDriver()`) — the snapshot as one opaque JSON column (durable twin). | ### Entities | API | Kind | Summary | | --------------------------- | ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `Tool` | class | A registered tool — a `ToolDefinition` schema (`name` / `description` / `parameters`) bound to an `execute` handler. | | `ToolManager` | class | The tool registry — resolves names, lists `definitions()` for the provider, and runs `execute` with per-call error isolation. | | `Conversation` | class | A conversation that OWNS its live message tail DIRECTLY (the flat store verbs `add` / `message` / `messages` / `remove` / `clear` / `count` folded in, like a `Workspace` owns its files) — the live tail + compacted summarized `sections` + a regenerated rollup `summary` + the `summarizable` flag; `compact` folds older live → a section via the `ConversationSummarizer` seam, `rehydrate` / `search` read the retained originals; observable `emitter` (`ConversationEventMap`). | | `ConversationManager` | class | The id-keyed registry of `Conversation`s WITH an active pointer — `add` (auto-activates the first, flows the manager's default `summarize` / `keep` in, a per-`add` override wins), `switch` re-points `active`, `open` / `save` (the durable `store` seam), `conversation` / `conversations` / `remove` (clears `active` if removed) / `clear` / `count`; event-free (each conversation owns its `emitter`). | | `MemoryConversationStore` | class | The in-memory `ConversationStoreInterface` — a process-lifetime `Map` of `ConversationSnapshot`s keyed by id (`get` / `set` / `delete`, async; no TTL); the default `open` / `save` backing. | | `DatabaseConversationStore` | class | A `ConversationStoreInterface` over one `databases` table — the snapshot as ONE opaque JSON column, narrowed back on `get` by `isConversationSnapshot` (the §14 boundary narrow); the driver-pluggable twin of `MemoryConversationStore`. | | `Instruction` | class | An immutable named directive — `name` / `content` / `priority` (default `0`), the `id` minted at construction. | | `InstructionManager` | class | The instruction registry — immutable instructions keyed by `name` (last write wins), listed by descending `priority`; `description` / `format` build contract; observable `emitter` (`InstructionManagerEventMap`). | | `Scope` | class | A named, immutable allow-list filter — four per-category lists (`undefined` ⇒ all, `[]` ⇒ none, else only-listed); `narrow` composes a tighter child by set-intersection. | | `ScopeManager` | class | The id-keyed registry of reusable named scopes — `create` mints + stores (always adds), `scope` / `scopes` / `remove` / `clear`; observable `emitter` (`ScopeManagerEventMap`). | | `AgentContext` | class | The richer turn context — `system` + the instruction manager + the workspace registry (the SOLE document/image context) + `messages` + `tools` + a mutable `scope`; `build()` folds the scoped managers + the active workspace into one system block then the scoped conversation (tools advertised structurally). | | `Agent` | class | The agent loop — one `#run` shared by `generate` / `stream`, bounded by `AbortSignal.any([signal, timeout, budget])`, paced by `scheduler`, tool iteration capped at `limit`. | | `Authority` | class | The synchronous policy gate — `evaluate` walks ordered rules first-match-wins, falling back to a configurable default (allow-unmatched by default; deny-by-default when its `fallback` denies). | | `AgentRegistry` | class | The job-rehydration bridge — resolves a serializable `AgentJobInput`'s names (`provider` / `tool` / `authority` / `scheduler`, throwing on a miss) and `build`s a seeded, signal-wired `Agent` from it. | | `Channel` | class | An unbounded async channel — a producer `push`es chunks into it and `close` / `fail`s it, a consumer `drain`s them live via the resolver-swap park. The `Agent`'s eager pump writes to one so `result` settles regardless of whether `events` is drained. | | `ThinkSplitter` | class | The stream-stateful `` separator — `split(delta)` returns the CLEAN content of each raw wire delta (reasoning spans accumulate on `thinking`), holding a tag split ACROSS deltas until disambiguated; a bare leading `` (the qwen3-template IMPLICIT open) RECLASSIFIES the surfaced prefix into `thinking` (the `content` accumulation is authoritative); `flush()` settles the stream end (an unclosed span lands on `thinking`, a never-completed partial tag returns as content). One per stream — a provider's think-tag guarantee. | | `Workspace` | class | The mutable, `path`-keyed working set of immutable `File`s — a minted-or-supplied `id`; replaces a file on every edit (transitioning `state` `'created'` / `'modified'`); the full edit surface + the modality matrix + a §13 `emitter`. | | `WorkspaceManager` | class | The id-keyed registry of `Workspace`s WITH an active pointer — `add` (auto-activates the first, flows the manager's default `on` / `error` in), `switch` re-points `active`, `open` / `save` (the durable `store` seam), `workspace` / `workspaces` / `remove` (clears `active` if removed) / `clear` / `count`; event-free (each workspace owns its `emitter`). | | `MemoryWorkspaceStore` | class | The in-memory `WorkspaceStoreInterface` — a process-lifetime `Map` of `WorkspaceSnapshot`s keyed by id (`get` / `set` / `delete`, async; no TTL); the default `open` / `save` backing. | | `DatabaseWorkspaceStore` | class | A `WorkspaceStoreInterface` over one `databases` table — the snapshot as ONE opaque JSON column, narrowed back on `get` by `isWorkspaceSnapshot` (the §14 boundary narrow); the driver-pluggable twin of `MemoryWorkspaceStore`. | ### Constants | API | Kind | Summary | | --------------------------- | ----- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `CONVERSATION_RECAP_PREFIX` | const | The lean framing label `Conversation.view()` prefixes onto each compacted section summary so a small model reads it as a RECAP, not a literal turn — a fixed handful of tokens (no-bloat). | | `DEFAULT_AGENT_LIMIT` | const | The default cap on an agent turn's tool iterations — `10`; overridable via `AgentOptions.limit`. | | `DEFAULT_AUTHORITY_ZONE` | const | The zone an `Authority`'s default allow fallback carries — `'default'` (an unmatched call is allowed under this zone). | | `DEFAULT_CONVERSATION_KEEP` | const | The default recent live messages a `Conversation.compact()` retains — `0` (fold ALL); a `keep` retains a tail. | | `EXTENSION_TO_LANGUAGE` | const | The extension→language table `inferLanguage` reads; an unlisted extension falls back to `'text'`. | | `THINK_OPEN` | const | The opening tag a `ThinkSplitter` recognizes as the start of an in-content reasoning span — `''` (the de-facto thinking-model wire convention). | | `THINK_CLOSE` | const | The closing tag that ends a `THINK_OPEN` reasoning span — `''`; an unclosed span is treated as thinking to the stream's end (`flush`). | | `WORKSPACE_SECTION_HEADER` | const | The `## Workspace` system-block header `AgentContext.build()` renders the ACTIVE workspace's text files under (the active workspace is the SOLE document/image context; `build()` owns the render). | | `MESSAGE_TOKEN_OVERHEAD` | const | The estimated per-message role/framing overhead `estimateMessages` adds on top of a message's content estimate — `4`. | | `IMAGE_TOKEN_ESTIMATE` | const | The coarse, deliberately-approximate per-image token cost `estimateMessages` charges for each attached image — `512` (a base64 length is NOT a reliable token proxy). | ### Helpers | API | Kind | Summary | | ------------------------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `inferLanguage` | function | Infer a fenced-code language tag from a file path's extension (e.g. `'src/main.ts'` → `'typescript'`); `'text'` when unknown. | | `filterAllowList` | function | Filter items by a scope allow-list of keys — `undefined` ⇒ all, `[]` ⇒ none, else only-listed (order-preserving, total). | | `estimateTokens` | function | Estimate a string's context-token footprint — the deterministic `ceil(length / 4)` char heuristic `estimateMessages` sums over. | | `estimateMessages` | function | Estimate a message batch's footprint — content + `MESSAGE_TOKEN_OVERHEAD` per message + a tool-call JSON estimate + `IMAGE_TOKEN_ESTIMATE` per image; the default `consume` for an agent's context `window` budget. Total — never throws, including on a circular `ToolCall.arguments` (falls back to a fixed contribution instead of the unreachable JSON length). The constants (`MESSAGE_TOKEN_OVERHEAD`, `IMAGE_TOKEN_ESTIMATE`, the `ceil(length / 4)` char heuristic) are deliberate, provider-agnostic APPROXIMATIONS, not an exact tokenizer count — actual window-sizing accuracy depends on the target model's own tokenization, so a caller wanting a sharper count supplies its own `consume` to `createBudget`. | | `sanitizeUsage` | function | Normalize a provider-reported `TokenUsage` — non-finite / negative fields floor to `0`, fractional fields floor down; applied automatically to BOTH a normal turn's `result.usage` and an abort's partial usage before either is charged/folded, so a buggy provider's dirty usage can never poison budget accounting. | | `settleAgentJob` | function | Run a rehydrated agent + apply the partial policy (shared by `createAgentQueue` / `createAgentRunner`): a partial throws `AgentJobError` unless `allowPartial`; a natural finish resolves. | | `isText` | function | Narrowing guard: whether a `FileContent` is the text arm (`'text' in content`). | | `isBinary` | function | Narrowing guard: whether a `FileContent` is the binary arm (`'data' in content`). | | `isImage` | function | Whether a `FileContent` is a binary arm with an `image/*` MIME (an image is a binary). | | `computeSize` | function | The byte size of a `FileContent` — UTF-8 bytes (text) / decoded payload bytes (binary). | | `countLines` | function | The line count of a `FileContent` — text line count; `0` for a binary arm. | | `decodedSize` | function | The decoded byte length of a base64 string, computed arithmetically (no decode). | | `isValidRange` | function | Whether a `Range` is structurally valid (every component `>= 1`, `start` ≤ `end`). | | `clampPosition` | function | Pin a 1-based `Position` into a caret that exists in the given text. | | `clampRange` | function | Clamp both ends of a `Range` to a text's bounds (the span a ranged op actually applies). | | `offsetAt` | function | Convert a 1-based `Position` to a 0-based string offset (capped at the text length). | | `sliceRange` | function | The substring spanned by a `Range` (start inclusive, end exclusive), clamped first. | | `spliceRange` | function | The text with a `Range` replaced by a replacement string, clamped first. | | `rangeOf` | function | Assemble a 1-based nested `Range` from four flat splice ints — published for a workspace-editing tool (see `@orkestrel/tool`) to lift a flat range edit into the nested surface. | | `fencedFile` | function | Render a path-addressed text body as a fenced reference block (`File: \n` ` ``` ` ` … `) — the framing `AgentContext`'s active-workspace text render emits. | | `escapeRegExp` | function | Escape a string's regex-special characters so it matches LITERALLY inside a `RegExp` — the literal-text search pattern primitive. | | `buildToolResult` | function | Project a `ToolResult` into the MCP `CallToolResult` shape — an `error` maps to one `isError: true` text block, else the JSON-stringified `value` as one text block. | | `isFile` | function | Narrowing guard: whether an `unknown` is structurally a `FileInterface` record (the per-file step of `isWorkspaceSnapshot`). | | `isWorkspaceSnapshot` | function | The §14 read-boundary guard: whether an `unknown` is a `WorkspaceSnapshot` (`string` `id` + a `files` array of valid `File`s); total, never throws. | | `isToolCall` | function | Narrowing guard: whether an `unknown` is structurally a `ToolCall` record (`string` `id` / `name` + a record `arguments`); the per-call step of `isMessage` — the ASI06 fail-closed element check. | | `isMessage` | function | Narrowing guard: whether an `unknown` is structurally a `MessageInterface` record (the per-message step of `isConversationSnapshot` / `isSection`); a present `calls` must be an array of valid `ToolCall`s (`isToolCall`); total, never throws. | | `isSection` | function | Narrowing guard: whether an `unknown` is structurally a `SectionInterface` record (`string` `id` / `summary` + a `messages` array of valid `Message`s); the per-section step of `isConversationSnapshot`. | | `isConversationSnapshot` | function | The §14 read-boundary guard: whether an `unknown` is a `ConversationSnapshot` (`string` `id` + optional `string` `summary` + valid `sections` / `messages` arrays); total, never throws. | ### Errors | API | Kind | Summary | | ---------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `ProviderAbortError` | class | Thrown by `stream` when its bound signal aborts mid-flight — carries the `partial` result streamed so far. | | `isProviderAbortError` | function | Narrow a caught value to a `ProviderAbortError` (`instanceof`), to recover its `partial`. | | `AgentJobError` | class | Thrown by an agent-job handler when a job ended `partial` and `allowPartial` is false — carries the partial `AgentResult`. | | `isAgentJobError` | function | Narrow a caught value to an `AgentJobError` (`instanceof`), to recover its `partial`. | | `ConversationError` | class | Thrown by `Conversation.compact()` / construction when no `ConversationSummarizer` was supplied, or a `sections` cap is sub-1 — carries a machine `code` (`'SUMMARIZER' \| 'SECTIONS'`). | | `isConversationError` | function | Narrow a caught value to a `ConversationError` (`instanceof`), to branch on its `code`. | | `WorkspaceError` | class | Thrown by the `Workspace` edit surface (also raised by a workspace-editing tool boundary — see `@orkestrel/tool`) — carries a `WorkspaceErrorCode` (+ optional `context`). | | `isWorkspaceError` | function | Narrow a caught value to a `WorkspaceError` (`instanceof`), to branch on its `code`. | | `AgentError` | class | Thrown SYNCHRONOUSLY by `Agent.stream()` (and, since `generate()` calls `stream()` directly with zero loop logic of its own, `Agent.generate()` too) when a concurrent run would corrupt SHARED per-agent accounting — carries a machine `code` (`'CONCURRENCY'`). Synchronous means a fire-and-forget `agent.generate().catch(...)` will NOT catch it (the throw happens on the call itself, before any `Promise` exists to attach `.catch` to) — `await` the call inside `try`/`catch`, or wrap the call expression in `try`/`catch`. | | `isAgentError` | function | Narrow a caught value to an `AgentError` (`instanceof`), to branch on its `code`. | ### Types | Type | Kind | Shape | | ------------------------------ | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `MessageRole` | type | `'system' \| 'user' \| 'assistant' \| 'tool'` — the role a message plays in a turn. | | `MessageInterface` | interface | `{ id; role: MessageRole; content; calls?: readonly ToolCall[]; images?: readonly string[] }` — one stored conversation turn (`calls` only on a replayed assistant turn; `images` base64 on a multimodal turn). | | `MessageInput` | interface | `{ role: MessageRole; content; calls?; images?: readonly string[] }` — the minimal data to author a message (the `id` is assigned by the storage layer; `images` base64 for a multimodal turn). | | `ToolDefinition` | interface | `{ name; description?; parameters?: Readonly> }` — a tool the model may call (open JSON-Schema `parameters`). | | `ToolCall` | interface | `{ id; name; arguments: Readonly> }` — a tool call the model emitted (id minted when the wire omits one). | | `ToolResult` | interface | `{ id; name; value?: unknown; error?: string }` — the outcome of executing a `ToolCall`, keyed back by `id` / `name`. | | `ToolInterface` | interface | `ToolDefinition` + `summary?` + `execute(args)` — a registered tool: the schema the model sees plus the handler that runs a call (`summary`, when set, advertises IN PLACE of `description`; the full `description` stays for on-demand retrieval). | | `ToolOptions` | interface | `{ name; description?; summary?; parameters?; execute }` — `createTool` configuration (the schema fields + the `execute` handler; `summary` a lean advertisement standing in for `description`). | | `ToolManagerInterface` | interface | `count` data member + `add` / `tool` / `tools` / `definitions` / `execute` / `remove` / `clear` — the tool registry with per-call error isolation. | | `ProviderResult` | interface | `{ content; thinking?: string; tools?: readonly ToolCall[]; usage?: TokenUsage }` — one turn's assembled outcome (`thinking` is reasoning the provider SEPARATED from the answer — it never re-enters the conversation; `tools` / `usage` present only when applicable). | | `ProviderDelta` | type | `{ type: 'content'; text } \| { type: 'thinking'; text }` — one live provider stream delta, keeping answer text and reasoning on separate channels. | | `ProviderStreamOptions` | interface | `{ think?: boolean }` — per-call provider options; `think` overrides the provider default for one `generate` / `stream` call. | | `ProviderInterface` | interface | `id` / `name` data members (+ an OPTIONAL `format?: ContextFormatInterface` context-framing default) + the `generate` / `stream` methods — the pluggable LLM inference boundary. | | `ThinkSplitterInterface` | interface | The `content` / `thinking` data members (the AUTHORITATIVE clean-content + reasoning accumulations) + the `split` / `flush` methods — the stream-stateful `` separator a provider routes content deltas through (one per stream; tags split across deltas are held until disambiguated; a bare leading `` — the implicit pre-seeded open — reclassifies the surfaced prefix into `thinking`). | | `ContextSectionFormat` | interface | `{ open?: string; render?: (item: T) => string; close?: string }` — one context section's optional open / item-render / close override trio (a cascade level's unit; `open` + `close` wrap the group). | | `ContextFormatInterface` | interface | `{ instructions?: ContextSectionFormat }` — a provider's optional context-framing default, keyed by section kind (the PROVIDER level of the build cascade). | | `MessageManagerInterface` | interface | `count` data member + `add` / `message` / `messages` / `remove` / `clear` — the immutable message-store contract `AgentContextInterface.messages` is typed to (the active `ConversationInterface` satisfies it structurally; the `id` minted on `add`). | | `InstructionInterface` | interface | `{ id; name; content; priority; format? }` — an immutable named directive (`priority` defaults to `0`; `format` is the per-item render override, present-when-given). | | `InstructionInput` | interface | `{ name; content; priority?; format? }` — the minimal data to author an instruction (the `id` is minted; `format` is the per-item render override, round-tripped onto the entity). | | `InstructionManagerEventMap` | type | `{ add; remove; clear }` — the instruction manager's push event map (the `on` / `emitter` surface). | | `InstructionManagerOptions` | interface | `{ on?: EmitterHooks; format?: ContextSectionFormat }` — `createInstructionManager` configuration (the reserved `on` hooks + the manager-options format override). | | `InstructionManagerInterface` | interface | `emitter` / `count` / `description` / `framing` data members + `add` / `instruction` / `instructions` / `format` / `remove` / `clear` — the name-keyed instruction registry (sorted by descending `priority`). | | `ScopeConfiguration` | interface | `{ instructions?; tools?; files?: readonly string[] }` — the three per-category allow-lists (`undefined` ⇒ all, `[]` ⇒ none, else only-listed); `files` filters the ACTIVE workspace's rendered files by `path`. | | `ScopeInput` | interface | `ScopeConfiguration` + `{ name }` — the data to author a scope (the `id` is minted by the layer that stores it). | | `ScopeInterface` | interface | `id` / `name` data members + the three allow-lists + the `narrow` method — a named, immutable filter; `narrow` composes a tighter child by set-intersection. | | `ScopeManagerEventMap` | type | `{ create; remove; clear }` — the scope manager's push event map (keyed by the minted `id`; `create`, not `add`). | | `ScopeManagerOptions` | interface | `{ on?: EmitterHooks }` — `createScopeManager` configuration (the reserved `on` hooks). | | `ScopeManagerInterface` | interface | `emitter` / `count` data members + `create` / `scope` / `scopes` / `remove` / `clear` — the id-keyed registry of reusable named scopes. | | `AgentContextOptions` | interface | `{ system?; tools?; instructions?; workspaces?: WorkspaceManagerInterface; scope?: ScopeInterface; conversations?: ConversationManagerInterface }` — `createAgentContext` configuration (the optional system prompt + pre-built managers to reuse, incl. a `workspaces` registry — the SOLE document/image context — a `conversations` registry — the message source — + an initial scope). | | `AgentContextInterface` | interface | `system` / `instructions` / `messages` / `tools` data members + the SETTABLE `workspaces` (the ACTIVE workspace, rendered by carrier, is the SOLE document/image context) + the mutable `scope` + the SETTABLE `conversations` registry (the message source — `messages` is its ACTIVE conversation's tail, switchable between runs) + the `build` method — the richer context; `build()` folds the scoped instructions + the active workspace's text files into one system block then the active conversation's view. | | `AgentStatus` | type | `'idle' \| 'running' \| 'done' \| 'error'` — an `AgentInterface` turn's lifecycle state. | | `AgentChunk` | type | `{ type: 'token'; content } \| { type: 'think'; content } \| { type: 'tool'; call; result } \| { type: 'usage'; usage }` — one streamed step of an agent turn (the PULL surface). | | `AgentEventMap` | type | `{ start; turn; tool; usage; deny; finish; error; abort; compactError }` — the agent's PUSH event map (lifecycle + usage/tool/deny + a non-fatal auto-compaction `compactError`, no per-token); the `on` / `emitter` surface. | | `AgentResult` | interface | `{ content; thinking?: string; usage?: TokenUsage; partial }` — an agent turn's settled outcome (`partial: true` when committed from a cancel; `usage` summed; `thinking` the reasoning the run's provider calls separated from the answer, joined — never re-enters the conversation). | | `RunOutcome` | interface | `{ content; thinking: string \| undefined; usage: TokenUsage \| undefined; partial }` — the INTERNAL mutable per-run sink the loop fills, assembled back into a settled `AgentResult` once the run completes. | | `CompactionState` | interface | `{ futile }` — the INTERNAL per-run auto-compaction holder threaded through the loop; `futile` latches once a between-turns `compact()` can't shrink the over-window prompt, stopping auto-compaction for the rest of the run. | | `StreamInterface` | interface | `{ events: AsyncIterable; result: Promise }` data members + the `abort` method — the generic live-events + settled-result + cancel handle. | | `AgentStreamInterface` | type | `StreamInterface` — the agent turn's live handle (events of chunks resolving a result). | | `AgentOptions` | interface | `{ on?; system?; tools?; instructions?; workspaces?; scope?; limit?; timeout?; budget?; scheduler?; signal?; authority?; conversations?; window?; strict? }` — `createAgent` configuration (the loop's bounds + pacing + the reserved `on` hooks; `instructions` / `workspaces` / `scope` forward construction-time context wiring; a `conversations` registry is the message source; a context `window` Budget opts into automatic compaction of the active conversation; `strict` aborts the run on an automatic-compaction summarizer failure instead of the lenient default). | | `AgentRunOptions` | interface | `{ think?: boolean }` — per-run options for `AgentInterface.generate` / `.stream`; `think` forwards to the underlying provider call. | | `AgentInterface` | interface | `emitter` / `id` / `status` / `context` data members + the `generate` / `stream` / `abort` methods — the bounded agent loop. | | `AuthorityContextInterface` | interface | `{ call: ToolCall }` — what an `AuthorityInterface` evaluates for one tool call (a lean seam for richer policy inputs later). | | `AuthorityDecision` | interface | `{ zone; allowed; reason? }` — an authority's verdict on one tool call (`zone` a project classification, `allowed` the gate decision). | | `AuthorityRule` | interface | `{ match; zone; allowed?; reason? }` — one ordered policy rule (first match wins; allows unless `allowed: false`). | | `AuthorityOptions` | interface | `{ rules?: readonly AuthorityRule[]; fallback?: AuthorityDecision }` — `createAuthority` configuration (ordered rules + no-match fallback). | | `AuthorityInterface` | interface | The `evaluate` method — the synchronous policy gate consulted before each tool call (ordered first-match-wins, configurable fallback). | | `AgentJobInput` | interface | `{ provider; messages; system?; tools?; authority?; scheduler?; limit?; timeout?; budget?; children? }` — a JSON-serializable agent job (names resolve via a registry; data carries directly). | | `AgentRegistryInterface` | interface | The `provider` / `tool` / `authority` / `scheduler` / `build` methods — resolves a job's names to live pieces and rehydrates a seeded, signal-wired agent. | | `AgentRegistryOptions` | interface | `{ providers; tools?; authorities?; schedulers?; store? }` — `createAgentRegistry` configuration (the named pools a job's names resolve against + the optional durable `store` every built agent's conversation manager shares). | | `AgentQueueOptions` | interface | `{ registry; allowPartial?; concurrency?; retries?; timeout?; store? }` — `createAgentQueue` configuration (the partial policy + the backing-queue knobs). | | `AgentRunnerOptions` | interface | `{ registry; allowPartial?; concurrency?; retries?; timeout? }` — `createAgentRunner` configuration (the partial policy + the backing-runner knobs). | | `ConversationSummarizer` | type | `(messages: readonly MessageInterface[]) => Promise` — the provider-agnostic summarizer seam the agent runtime supplies (core never imports a provider). | | `SectionInterface` | interface | `{ id; summary; messages: readonly MessageInterface[] }` — a slice of folded messages digested into a summary (the unit of compaction; `messages` RETAINED for `rehydrate` / `search`). | | `ConversationEventMap` | type | `{ compact; summary; rehydrate; collapse }` — a conversation's push event map (the `on` / `emitter` surface; `collapse` fires when the `sections` cap folds the oldest overflow into one merged section). | | `ConversationOptions` | interface | `{ id?; on?; summarize?; keep?; sections? }` — `createConversation` configuration (the seam + retained-tail size + an optional `>= 1` cap on the compacted `sections` list; absent `summarize` ⇒ `compact()` throws; a sub-1 `sections` throws `ConversationError('SECTIONS')`). | | `CompactOptions` | interface | `{ keep?; sections? }` — per-compaction overrides for `Conversation.compact()` (override the retained-tail size and/or the `sections` cap for ONE fold). | | `ConversationReferenceOptions` | interface | `{ label?; summary?; messages? }` — how `Conversation.reference()` renders this conversation as a cross-conversation provenance block (`label` defaults to the `id`, `summary` defaults true, `messages` are cherry-picked excerpts defaulting to none). | | `ConversationInterface` | interface | `id` / `emitter` / `summary` / `sections` / `summarizable` / `messages` data members + the `view` / `compact` / `rehydrate` / `search` / `reference` / `snapshot` methods — messages above the flat store with compaction + rehydrate + search + cross-conversation reference + a JSON `snapshot` (`summarizable` is `true` when a summarizer was supplied — the agent loop gates AUTO-compaction on it). | | `ConversationInput` | interface | `{ id?; summarize?; keep?; sections?; on?; snapshot? }` — the data to author a conversation through a `ConversationManager` (a `summarize` / `keep` / `sections` override + the reserved `on` + a `snapshot` to hydrate from, the analogue of `WorkspaceInput.seed`). | | `ConversationManagerOptions` | interface | `{ summarize?; keep?; sections?; store? }` — `createConversationManager` configuration (the default summarizer + retained-tail size + `sections` cap flowed into created conversations, + the optional durable `store` backing `open` / `save`). | | `ConversationManagerInterface` | interface | `count` / `active` data members + `conversation` / `conversations` / `add` / `switch` / `open` / `save` / `remove` / `clear` — the id-keyed registry of conversations with an active pointer + the durable `store` seam (event-free; `add` auto-activates the first, `switch` re-points `active`). | | `ConversationSnapshot` | interface | `{ id, summary?, sections, messages }` — the JSON-serializable durable payload (`id` + the rollup `summary` + compacted `sections` + the live tail); `Conversation.snapshot()` produces it (the analogue of `WorkspaceSnapshot`). | | `ConversationStoreInterface` | interface | `get` / `set` / `delete` — the async, non-generic persistence seam for a `ConversationSnapshot` keyed by its own id (no TTL); `Memory` / `Database` impls (the analogue of `WorkspaceStoreInterface`). | | `ConversationSnapshotRow` | interface | `{ id, snapshot }` — one `DatabaseConversationStore` table row (the snapshot one opaque JSON column, read back as `unknown`, narrowed on `get`); the analogue of `WorkspaceSnapshotRow`. | | `ToolResultContent` | interface | `{ type: 'text'; text }` — one block of an MCP-shaped tool call response — plain text, the only content kind `buildToolResult` emits. | | `ToolCallResult` | interface | `{ content: readonly ToolResultContent[]; isError? }` — the MCP `CallToolResult` shape a `ToolResult` maps to via `buildToolResult` (`isError` set ONLY on failure, never `false`). | | `BinaryMIME` | type | `'image/png' \| 'image/jpeg' \| 'image/gif' \| 'image/webp'` — the binary `FileContent` arm's MIME, the canonical file-content MIME (additive; a future PDF). | | `FileContent` | type | `{ text; language } \| { data; mime }` — a TAGLESS text-vs-binary union narrowed by `isText` / `isBinary`. | | `FileState` | type | `'created' \| 'modified' \| 'loaded' \| 'deleted'` — a `File`'s lifecycle union (AGENTS §10). | | `FileInput` | interface | `{ path, content, state? }` — the data to author a `File` (`state` defaults to `'created'`). | | `FileInterface` | interface | `path` / `content` / `state` + the derived `size` / `lines` — a data-only immutable frozen plain record (no `id`, no methods). | | `Position` | interface | `{ line, column }` — a 1-based caret inside a text file (line + column both count from `1`). | | `Range` | interface | `{ start, end }` — a half-open span of two `Position`s (`start` inclusive, `end` exclusive). | | `ReadResult` | interface | `{ content, range }` — a ranged read's sliced text + the actual (clamped) range applied. | | `SearchOptions` | interface | `{ regex?, exact?, limit? }` — how `search` matches (`regex` false / `exact` true / unlimited by default). | | `SearchMatch` | interface | `{ path, line, column, length, content }` — one `search` hit (1-based; `content` is the full line). | | `ReplaceOptions` | interface | `{ regex?, exact?, limit? }` — how `replace` matches (same axes as `SearchOptions`). | | `ReplaceResult` | interface | `{ query, replaced, files }` — the `replace` tally (occurrences + files changed). | | `WorkspaceEventMap` | type | `write` / `remove` / `move` / `clear` — the events a `Workspace` emits after each mutation. | | `WorkspaceOptions` | interface | `{ id?, on?, error? }` — the optional identity + initial event listeners + the emitter's listener-error handler (AGENTS §13). | | `WorkspaceErrorCode` | type | `'MODALITY' \| 'PATTERN' \| 'RANGE' \| 'TOOL'` — the machine-readable `WorkspaceError` code (`TOOL` is the tool-boundary parse fault). | | `WorkspaceInterface` | interface | `id` / `emitter` / `count` data members + the edit methods below — the in-memory edit surface over a `path`-keyed set of `File`s. | | `WorkspaceInput` | interface | `{ id?, on?, error?, seed? }` — the data to author a workspace through a `WorkspaceManager` (the reserved `on` / `error` + an initial-files `seed`). | | `WorkspaceManagerOptions` | interface | `{ on?, error?, store? }` — `createWorkspaceManager` config (the default event listeners + listener-error handler flowed into created workspaces, + the optional durable `store` backing `open` / `save`). | | `WorkspaceManagerInterface` | interface | `count` / `active` data members + `workspace` / `workspaces` / `add` / `switch` / `open` / `save` / `remove` / `clear` — the id-keyed registry of workspaces with an active pointer + the durable `store` seam (event-free). | | `WorkspaceSnapshot` | interface | `{ id, files }` — the JSON-serializable durable payload (`id` + a FLAT list of `File`s; each carries its `path`); `Workspace.snapshot()` produces it. | | `WorkspaceStoreInterface` | interface | `get` / `set` / `delete` — the async, non-generic persistence seam for a `WorkspaceSnapshot` keyed by its own id (no TTL); `Memory` / `Database` impls. | | `WorkspaceSnapshotRow` | interface | `{ id, snapshot }` — one `DatabaseWorkspaceStore` table row (the snapshot one opaque JSON column, read back as `unknown`, narrowed on `get`). | The `id` / `name` (+ the optional `format`) members of `ProviderInterface`, the `count` member of `ToolManagerInterface` and `MessageManagerInterface`, the `emitter` / `count` / `description` / `framing` members of `InstructionManagerInterface`, the `id` / `name` (+ the four allow-lists) of `ScopeInterface`, the `emitter` / `count` members of `ScopeManagerInterface`, the `system` / `instructions` / `messages` / `tools` members of `AgentContextInterface`, the `events` / `result` members of `StreamInterface`, and the `emitter` / `id` / `status` / `context` members of `AgentInterface` are `readonly` data members (Surface rows, above); their call-signature methods are documented under [Methods](#methods) — as is `ScopeInterface`'s single `narrow` method, `ScopeManagerInterface`'s `create` / `scope` / `scopes` / `remove` / `clear`, `AuthorityInterface`'s single `evaluate` method, and `AgentRegistryInterface`'s `provider` / `tool` / `authority` / `scheduler` / `build` methods. The `id` / `emitter` / `summary` / `sections` / `summarizable` / `messages` members of `ConversationInterface` and the `count` / `active` members of `ConversationManagerInterface` are likewise `readonly` data members (Surface rows, above — `active` is a `readonly` getter, the active conversation the context renders, a property not a method), with their methods — `ConversationInterface`'s `view` / `compact` / `rehydrate` / `search` / `reference` / `snapshot` and `ConversationManagerInterface`'s `conversation` / `conversations` / `add` / `switch` / `open` / `save` / `remove` / `clear` — under [Methods](#methods) (`Conversation.emitter` is a `readonly` accessor — a property, not a method — so it stays a Surface row). The `count` AND `active` members of `WorkspaceManagerInterface` are `readonly` data members (Surface rows, above — `active` is a `readonly` getter, the active workspace the context renders, a property not a method), with its methods — `workspace` / `workspaces` / `add` / `switch` / `open` / `save` / `remove` / `clear` — under [Methods](#methods). `AgentContextInterface.scope`, `AgentContextInterface.conversations`, AND `AgentContextInterface.workspaces` are the three MUTABLE data members (each a getter + setter — a property, not a method: `scope` the active filter, `conversations` the message-source registry whose ACTIVE conversation IS `messages`, `workspaces` the workspace registry whose ACTIVE workspace `build()` renders by carrier — the SOLE document/image context), so they stay Surface rows and are NOT in a method table. (`workspaces` AND `conversations` are ALWAYS present — a fresh empty `WorkspaceManager` / a `ConversationManager` holding one default conversation when none is supplied — mirroring the always-present `instructions` manager, but settable so the whole registry can be swapped between runs.) `AgentInterface.emitter` and the two registry managers' `emitter` (instruction / scope) are `readonly` accessors (a getter is a property, not a method — §22), so they stay Surface rows and are NOT in a method table; likewise the instruction manager's `description` AND `framing` are `readonly` accessors (a getter is a property, not a method — `framing` exposes the manager-options format override so a context's `build()` can interleave the provider default beneath it). `AgentChunk` / `AgentEventMap` / `AgentResult` / `AgentStatus` / `AgentStreamInterface` / `InstructionManagerEventMap` / `ScopeManagerEventMap` / `ConversationEventMap` are plain data / union / map / alias types (no methods), `ConversationSummarizer` is a function type (the summarizer seam — no methods), and `ContextSectionFormat` / `ContextFormatInterface` / `AuthorityContextInterface` / `AuthorityDecision` / `AuthorityRule` / `AuthorityOptions` / `AgentJobInput` / `AgentRegistryOptions` / `AgentQueueOptions` / `AgentRunnerOptions` / `InstructionInterface` / `InstructionInput` / `InstructionManagerOptions` / `ScopeConfiguration` / `ScopeInput` / `ScopeManagerOptions` / `RunOutcome` / `SectionInterface` / `ConversationOptions` / `CompactOptions` / `ConversationReferenceOptions` / `ConversationInput` / `ConversationManagerOptions` / `ConversationSnapshot` / `ConversationSnapshotRow` are plain data / options types (no methods — `ConversationSnapshot` is a DATA-ONLY JSON record). `ConversationStoreInterface`'s `get` / `set` / `delete` are its async persistence methods, documented in prose with its `MemoryConversationStore` / `DatabaseConversationStore` implementations (a small, non-`createX` seam mirroring `WorkspaceStoreInterface` — no dedicated `## Methods` table). `ProviderResult.usage` and `AgentResult.usage` reuse the [budgets](budget.md) `TokenUsage`. For the workspace surface: `BinaryMIME` / `FileContent` / `FileState` / `WorkspaceEventMap` / `WorkspaceErrorCode` are plain data / union / map types (no methods), and `FileInput` / `FileInterface` / `Position` / `Range` / `ReadResult` / `SearchOptions` / `SearchMatch` / `ReplaceOptions` / `ReplaceResult` / `WorkspaceOptions` / `WorkspaceInput` / `WorkspaceManagerOptions` / `WorkspaceSnapshot` / `WorkspaceSnapshotRow` are plain data / options types (`FileInterface` and `WorkspaceSnapshot` are DATA-ONLY records — no methods). `WorkspaceStoreInterface`'s `get` / `set` / `delete` are its async persistence methods, documented in prose with its `MemoryWorkspaceStore` / `DatabaseWorkspaceStore` implementations (a small, non-`createX` seam mirroring `WorkflowStoreInterface` — no dedicated `## Methods` table). `WorkspaceInterface`'s `id` / `emitter` / `count` are `readonly` data members (Surface rows, above) and its edit methods + `snapshot` are documented under [Methods](#methods); `Workspace` implements exactly that interface. `WorkspaceManagerInterface`'s `count` / `active` are `readonly` data members and its `workspace` / `workspaces` / `add` / `switch` / `open` / `save` / `remove` / `clear` are documented under [Methods](#methods); `WorkspaceManager` implements exactly that interface. ## Methods The public methods of `ProviderInterface`, `ThinkSplitterInterface`, `ToolManagerInterface`, `MessageManagerInterface`, `InstructionManagerInterface`, `ScopeInterface`, `ScopeManagerInterface`, `AgentContextInterface`, `AgentInterface`, `AuthorityInterface`, `AgentRegistryInterface`, `ConversationInterface`, `ConversationManagerInterface`, and `WorkspaceInterface` — every call-signature member listed (their `readonly` data members `id` / `name` / `count` / `active` / `emitter` / `description` / `system` / `messages` / `tools` / `status` / `context` / `summary` / `sections` / `summarizable` / `thinking` / `content`, and `AgentContextInterface`'s mutable `scope` / `workspaces` / `conversations`, stay Surface rows). `Tool` / `ToolManager` / `ThinkSplitter` / `InstructionManager` / `Scope` / `ScopeManager` / `AgentContext` / `Agent` / `Authority` / `AgentRegistry` / `Conversation` / `ConversationManager` / `Workspace` implement their interfaces exactly, so these double as the classes' instance-method surfaces (AGENTS §22) — `MessageManagerInterface` is the contract `AgentContextInterface.messages` is typed to (no concrete class in this module; the active `Conversation` satisfies it structurally); no concrete `ProviderInterface` implementation lives in this module — a host application supplies one. `Tool` adds the single `execute` method declared by `ToolInterface` (atop the `ToolDefinition` data members it extends), so it carries no separate method table. `StreamInterface` is the generic live handle (the `events` / `result` data members plus a single `abort(reason?)` cancel); `AgentStreamInterface` is its `AgentChunk` / `AgentResult` specialization — documented in prose rather than its own method table, since the agent surface is reached through `AgentInterface`. #### `ProviderInterface` `generate` produces one complete turn; `stream` yields `ProviderDelta`s and RETURNS the assembled result. Both take the conversation, a bounding `AbortSignal`, optional `tools`, and optional per-call `ProviderStreamOptions`. | Method | Returns | Behavior | | ---------- | ----------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | | `generate` | `Promise` | Generate one complete turn — resolve the assembled result (content + any tool calls + any usage). An abort rejects the call. | | `stream` | `AsyncGenerator` | Stream one turn — yield channel-tagged content / thinking deltas, RETURN the assembled result. A mid-stream abort throws `ProviderAbortError`-w/-partial. | #### `ThinkSplitterInterface` The stream-stateful `` separator a provider routes raw content deltas through, so it yields clean content and surfaces the reasoning as `ProviderResult.thinking`. The `content` / `thinking` data members (the AUTHORITATIVE clean-content + reasoning accumulations) stay Surface rows — `content` matters because some chat templates PRE-SEED `` into the prompt scaffold (the qwen3 shape), so only a bare `` ever appears on the wire: before any tag event, that bare close RECLASSIFIES everything surfaced so far into `thinking` (one-shot — afterwards a bare close is plain text), correcting `content` retroactively where the already-returned deltas cannot be recalled. One splitter serves ONE stream — create a fresh one per call (`createThinkSplitter`). | Method | Returns | Behavior | | ------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `split` | `string` | Feed one raw delta; returns the CLEAN (non-think) content to surface for it (possibly `''`). A tag split ACROSS deltas is HELD until disambiguated — never leaked as content, never mis-eaten. | | `flush` | `string` | Settle the stream end — a held partial tag that never completed returns as the final content delta (it was real text); an UNCLOSED think span's tail lands on `thinking` (the cut-off model). | #### `ToolManagerInterface` The tool registry the agent loop dispatches model tool-calls through. `add` / `remove` carry §9.2 batch overloads (one or a list); `execute` carries a §9.2 batch overload AND the per-call error isolation — every call resolves a `ToolResult`, a throw or unknown name becomes an `error`, so a tool throw never escapes and a batch never fails as a whole. | Method | Returns | Behavior | | ------------- | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `add` | `void` | Register one tool, or a batch (§9.2) — keyed by `tool.name`; a re-`add` of the same name overwrites it (last write wins). | | `tool` | `ToolInterface \| undefined` | Look up one registered tool by name (`undefined` when absent). | | `tools` | `readonly ToolInterface[]` | List every registered tool, in insertion order. | | `definitions` | `readonly ToolDefinition[]` | The plain `ToolDefinition` of each tool (the `execute` handler stripped) — the schemas a provider advertises to the model; `description` is `tool.summary ?? tool.description` (a lean `summary` stands in for the full text). | | `execute` | `Promise` / `Promise` | Run one `ToolCall` (or a batch, §9.2) — ALWAYS resolves a `ToolResult`: success → `value`, a handler throw → `error`, an unknown name → not-found `error`. The batch correlates by `id` in order and never fails as a whole. | | `remove` | `boolean` | Remove one tool by name, or a batch (§9.2) — `true` when any was removed. | | `clear` | `void` | Remove every registered tool. | #### `MessageManagerInterface` The immutable conversation store. `add` mints each message's `id` and carries §9.2 batch overloads (one input → one message, a batch → the array); `remove` carries §9.2 batch overloads (one or a list). The `count` data member stays a Surface row. | Method | Returns | Behavior | | ---------- | -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | | `add` | `MessageInterface` / `readonly MessageInterface[]` | Store one `MessageInput`, or a batch (§9.2) — MINTS each message's `id`, returns the created message(s); a stored message is immutable. | | `message` | `MessageInterface \| undefined` | Look up one stored message by id (`undefined` when absent). | | `messages` | `readonly MessageInterface[]` | List every stored message, in insertion order. | | `remove` | `boolean` | Remove one message by id, or a batch (§9.2) — `true` when any was removed. | | `clear` | `void` | Remove every stored message. | #### `InstructionManagerInterface` The name-keyed instruction registry a richer context renders a directives block from. `add` mints each `id` and carries §9.2 batch overloads (a re-`add` of the same name overwrites it, last write wins); `remove` carries §9.2 batch overloads. The `emitter` / `count` / `description` data members stay Surface rows. | Method | Returns | Behavior | | -------------- | ---------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | | `add` | `InstructionInterface` / `readonly InstructionInterface[]` | Add one `InstructionInput`, or a batch (§9.2) — MINTS each `id`; a re-`add` of the same name OVERWRITES it (last write wins). | | `instruction` | `InstructionInterface \| undefined` | Look up one instruction by name (`undefined` when absent). | | `instructions` | `readonly InstructionInterface[]` | List every instruction, SORTED by descending `priority` (stable for equal priorities). | | `format` | `string` | Render one instruction for the prompt — its `content`. | | `remove` | `boolean` | Remove one instruction by name, or a batch (§9.2) — `true` when any was removed. | | `clear` | `void` | Remove every instruction. | #### `ScopeInterface` The named, immutable allow-list filter. `narrow` is the only method — the `id` / `name` data members and the three per-category allow-lists stay Surface rows. | Method | Returns | Behavior | | -------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `narrow` | `ScopeInterface` | Compose a tighter child scope — each category the set-INTERSECTION of this scope's list and `config`'s (an `undefined` side imposes no constraint); returns a NEW scope, leaving this one unchanged. | #### `ScopeManagerInterface` The id-keyed registry of reusable named scopes. `create` mints + stores a scope (always adds — never overwrites, since two scopes may share a `name`); `remove` carries §9.2 batch overloads. The `emitter` / `count` data members stay Surface rows. | Method | Returns | Behavior | | -------- | ----------------------------- | ---------------------------------------------------------------------------------------------- | | `create` | `ScopeInterface` | Mint a scope from a `ScopeInput` (an `id` + the three allow-lists) and store it — always adds. | | `scope` | `ScopeInterface \| undefined` | Look up one scope by id (`undefined` when absent). | | `scopes` | `readonly ScopeInterface[]` | List every scope, in insertion order. | | `remove` | `boolean` | Remove one scope by id, or a batch (§9.2) — `true` when any was removed. | | `clear` | `void` | Remove every scope. | #### `AgentContextInterface` The richer turn context. `build()` is the only method — the `system` / `instructions` / `messages` / `tools` data members and the mutable `scope` / `workspaces` / `conversations` accessors stay Surface rows. | Method | Returns | Behavior | | ------- | ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `build` | `readonly MessageInterface[]` | The next turn's provider input: a leading `system` message folding the prompt + the scope-filtered instructions (each section's header + each item's rendering resolved through the FORMAT CASCADE) PLUS the ACTIVE workspace's scope-filtered (`scope.files`) TEXT files in a `## Workspace` section (fenced), then the active conversation's `view()` — with the active workspace's image files' `data` attached to the last user message. Takes an OPTIONAL `format?: ContextFormatInterface` (the PROVIDER level — typically `provider.format`); omitting it (and with no overrides) reproduces the built-in framing byte-for-byte. The `system` message is prepended only when any part exists; the active-workspace render is ACTIVE-ONLY; tools are NOT in the prompt. Built fresh each call. | #### `AgentInterface` The bounded agent loop. `generate` and `stream` share ONE private run (`generate` drains the same stream `stream` exposes, so they can't diverge); `abort` cancels the in-flight turn. The `emitter` / `id` / `status` / `context` data members stay Surface rows (`emitter` is a `readonly` accessor — a property, not a method). | Method | Returns | Behavior | | ---------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `generate` | `Promise` | Run the turn to completion, discarding the live chunks — drains the shared stream and resolves the settled `AgentResult` (`partial: true` when cancelled). | | `stream` | `AgentStreamInterface` | Run the turn as a live stream — iterate `events` for `AgentChunk`s, `await result` for the outcome; `result` RESOLVES partial on a cancel, rejects on a real error. | | `abort` | `void` | Cancel the in-flight turn — fires the turn's signal; the `result` settles `partial: true` with whatever content accumulated. | #### `AuthorityInterface` The synchronous policy gate the agent loop consults before each tool call. `evaluate` is the only method — it has no data members. | Method | Returns | Behavior | | ---------- | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `evaluate` | `AuthorityDecision` | Evaluate one tool call against the ordered rules — return the FIRST matching rule's verdict (allows unless `allowed: false`), or the fallback when none match. | #### `AgentRegistryInterface` The job-rehydration bridge. `provider` / `tool` / `authority` / `scheduler` resolve a name against their pool (throwing `unknown : ` on a miss); `build` rehydrates a seeded, signal-wired agent from a serializable job. It has no data members. | Method | Returns | Behavior | | ----------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `provider` | `ProviderInterface` | Resolve a registered provider by name — THROWS `unknown provider: ` when absent. | | `tool` | `ToolInterface` | Resolve a registered tool by name — THROWS `unknown tool: ` when absent. | | `authority` | `AuthorityInterface` | Resolve a registered authority by name — THROWS `unknown authority: ` when absent. | | `scheduler` | `SchedulerInterface` | Resolve a registered scheduler by name — THROWS `unknown scheduler: ` when absent. | | `build` | `AgentInterface` | Rehydrate a seeded, signal-wired agent from an `AgentJobInput` — resolve its names, rebuild its token budget, seed its conversation, thread `signal`; throws on a miss. | #### `ConversationInterface` A conversation that OWNS its live message tail DIRECTLY (the flat store verbs folded in, like a `Workspace` owns its files). `add` mints each message's `id` and stores it (§9.2 batch overloads); `message` / `messages` look up the live tail; `remove` (§9.2) / `clear` drop from it. `view` is the model input; `compact` folds the older live messages into a summarized `SectionInterface` (regenerating the rollup, emitting `summary` then `compact`); `rehydrate` / `search` read the retained originals; `reference` renders this conversation as a provenance-labeled block to pull INTO another (a pure string, no model call). The `id` / `emitter` / `summary` / `sections` / `count` data members stay Surface rows (`emitter` is a `readonly` accessor — a property, not a method). | Method | Returns | Behavior | | ----------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `add` | `MessageInterface` / `readonly MessageInterface[]` | Append one `MessageInput` to the live tail, or a batch (§9.2) — MINTS each message's `id`, returns the created message(s); a stored message is immutable. | | `message` | `MessageInterface \| undefined` | Look up one LIVE message by id (`undefined` when absent). | | `messages` | `readonly MessageInterface[]` | List the LIVE (uncompacted) tail, in insertion order. | | `remove` | `boolean` | Remove one LIVE message by id, or a batch (§9.2) — `true` when any was removed. | | `clear` | `void` | Empty the live tail (the compacted `sections` are untouched). | | `view` | `readonly MessageInterface[]` | The model input — each section as ONE synthetic RECAP message (its summary prefixed with `CONVERSATION_RECAP_PREFIX`, so a small model reads it as a recap not a literal turn), then the live tail verbatim (the rollup `summary` is NOT injected). | | `compact` | `Promise` | Fold the oldest `count - keep` live messages into a summarized section (via the `ConversationSummarizer`), remove them from the live tail, REGENERATE the rollup, emit `summary` then `compact`; `undefined` when nothing folds (`count <= keep`). THROWS a `ConversationError` when no summarizer was supplied. | | `rehydrate` | `readonly MessageInterface[]` | A section's full original messages — a pure READ that emits `rehydrate` (empty for an unknown id; v1 never auto-reinserts). | | `search` | `readonly MessageInterface[]` | Case-insensitive substring over `content` across ALL messages — every section's retained originals, then the live tail. | | `reference` | `string` | Render THIS conversation as a self-labeled, fenced PROVENANCE block to pull INTO another (by writing it to the active context's active workspace) — a PURE string, no model call: a leading `[Reference — conversation "