@sema-agent/core
Version:
Stateless, task-oriented AI agent core
1,581 lines • 124 kB
TypeScript
/**
* The task INPUT surface: `TaskSpec` — everything a caller hands `runTask` — plus the two shapes that
* exist only as one of its fields (`ImageInput`, `StaleToolResultOffloadOptions`). The limits live
* beside it in `task-limits.ts`; the terminal record it produces in `task-result.ts`. Layer 0
* vocabulary; `types.ts` re-exports the three, so no consumer's import changes.
*/
import type { PermissionMode } from "./ask-origin.js";
import type { TSchema } from "typebox";
import type { ThinkingLevel } from "../internal/harness.js";
import type { ActorAssertion, Model, ResilienceOptions } from "../internal/llm.js";
import type { AgentDefinition, SkillSpec } from "./agent-definition.js";
import type { A2aServerSpec, McpServerSpec } from "./mcp-server-spec.js";
import type { ModelRef, ModelRole, ModelRoles } from "./model-seat.js";
import type { TaskLimits } from "./task-limits.js";
import type { ToolSpec } from "./tool-spec.js";
/** design/169-F — knobs for the opt-in stale tool-result offload (see
* `TaskSpec.compaction.staleToolResultOffload`, where the semantics are documented). */
export interface StaleToolResultOffloadOptions {
/** How many most-recent results PER TOOL NAME stay verbatim in the request. Default 3. */
keepRecentPerTool?: number;
/** Minimum chars a replacement must save (raw text − pointer text) to happen. Default 2000. */
minSavingsChars?: number;
}
/** An image attached to the task's user message: either inline base64 or a URL to fetch. */
export type ImageInput = {
/** Base64-encoded image bytes (no data: prefix). */
data: string;
/** MIME type, e.g. "image/png" / "image/jpeg". */
mimeType: string;
} | {
/** URL the runner will fetch and inline as base64 before sending to the model. */
url: string;
};
/**
* One file the user attached to the objective, by WHO HOLDS ITS BYTES (never by what kind of file it is — the engine
* finds that out and reports it):
* - `{ path, lines? }` — a file or directory the engine can reach in its workspace: the engine reads it through the
* Read tool's own read and admission (read-deny table, roots, the run's deny rules), as if the model had read it,
* minus the questions — the user's declaration is the answer to an ordinary ask; a read some layer would put to a
* person other than the asker (an organization rule, a deployment-authored or real-approval ask) is not attached.
* `lines` reads a line range (1-based, `end` inclusive).
* - `{ name, text, startLine? }` — text the host holds (a local file a remote engine cannot reach, an unsaved buffer):
* delivered as given, line-numbered from `startLine` (default 1). It opens nothing an objective could not carry,
* and records no read state.
* - `{ name, image: { data, mimeType } }` — an image file the host holds (base64), admitted like an image the Read tool
* serves.
* Each attachment reaches the model as its own message right after the objective, marked as the engine's: two
* marked sentences around the data, the name or path only in the data. See `USER_ATTACHMENT_SEAT` for the limits.
*/
export type UserAttachment = {
readonly path: string;
readonly lines?: {
readonly start: number;
readonly end?: number;
};
} | {
readonly name: string;
readonly text: string;
readonly startLine?: number;
} | {
readonly name: string;
readonly image: {
readonly data: string;
readonly mimeType: string;
};
};
/** A single self-contained task with its full configuration. */
/** The shell-gate FLOOR vocabulary — {@link TaskSpec.shellGate}'s value set, the word the engine persists on a
* shell-gated ask (`RiskDescriptor.shellGateDoctrine`) and carries down a delegation chain. One value: `"always"`
* (every shell command is confirmed, under every permission mode — an operator's mandate). The shape tier itself
* is installed on every full shell regardless; what a permission mode does with the resulting ask is the gate's
* arbitration, never a doctrine word. */
export type ShellGateDoctrine = "always";
export interface TaskSpec {
taskId?: string;
/** The instruction / latest user message for this turn. */
objective: string;
/**
* Authenticated **end-user principal** this task runs on behalf of (design/62) — an opaque identity string
* (user id, email, tenant-scoped subject) the **trusted caller** sets at task creation. The Runner holds it
* for the whole run and injects it into MCP requests for any server with a `principalHeader`
* ({@link McpServerSpec}), enabling per-user RBAC in the MCP server. Delegated child tasks (sub-agent / team /
* fan-out) inherit it.
*
* 🔴 **Trust contract (design/53 — authority stays in the control plane):** this is a control-plane value,
* **NOT model/worker input.** Set it only from the authenticated task-submission context; it is not exposed
* as a tool argument and the model cannot read or change it (a worker filling its own principal = a privilege
* circumvention). Treat it as write-once for the run. The MCP server MUST trust this header only on the Runner's
* authenticated/TLS channel and default to deny/public when it is absent — never default-admin.
*
* 🔴🔴 **This is the GOVERNANCE key.** Per-principal entitlements are resolved off this value:
* {@link RunnerDeps.runtimeCapsResolver}`(spec.principal)` decides `allowWorkflows` (and thus whether
* self-orchestration / `RunWorkflow` is permitted). So a caller MUST set `spec.principal` to a
* **cryptographically VERIFIED** identity — **never a raw, spoofable request header**. A deployment that holds
* both an unverified header value AND a verified one (e.g. a "direct-door" path) MUST pass the VERIFIED one
* here: keying a gate on a spoofable principal is a multi-tenant governance BYPASS (a caller-declared header would
* inherit another tenant's entitlements). Core cannot verify identity itself (no crypto context) — it trusts
* this value by contract; getting it wrong is a privilege-escalation hole, not a core bug.
*/
principal?: string;
/**
* design/171 §6.4 — WHO opened this turn (the thread's first-post author in a shared session):
* the CANONICAL attribution seat for the objective. Rendered as the speaker envelope on the first
* user message (single projection point) and persisted as `UserMessage.actor`; absent = anonymous
* (every single-user host — the model-facing bytes are then unchanged).
*
* Attribution ONLY, never authority: permission/approval/scope decisions key on
* {@link TaskSpec.principal}, never on this (the normative clause on {@link ActorAssertion}).
* Relationship to `clientContext.userEmail`: that field keeps its environment-facts role (an
* UNVERIFIED display string in the env block) and carries no attribution duty — this seat is the
* honest one, because `hostAsserted` states whether the identity was ingress-derived.
*/
actor?: ActorAssertion;
/**
* design/173 §3/§8.3 — the caller's DECLARED interaction posture for this run. `"interactive"`
* asserts a live human is reachable and makes prepare REFUSE the leg (fail-loud
* `config.interaction_posture`) when the assembly cannot deliver on it: it requires the resolved
* permission-ask seat to be a live approver callback (`ask.effective === "human_reachable"` — a
* blanket allow/deny or an absent seat fails the door) AND a content-question channel (a resolved
* `onQuestion` face, OR the engine's own `stripped_bg_lane` legs — core's deliberate strip of a
* long-lived child's per-request face is correct design, not a configuration lie, so those legs
* are exempt). The park lane is deliberately NOT required (interactive-without-park is a
* legitimate light deployment — a synchronous CLI/local UI). `"headless"` declares the opposite
* and checks nothing (a batch deployment carrying an onAsk for audit is legal). Absent = no
* check, bytes unchanged.
*
* Seat resolution is THREE-leg: `spec.interactionPosture ?? <the spawning run's resolved posture>
* ?? deps.interactionPosture` — a Runner serving both an interactive session and cron/batch legs
* declares at deps level and overrides per spec, while a delegated child inherits the posture its
* SPAWNER resolved (carried on trusted engine internals, never copied onto the child's spec). The
* middle leg is load-bearing: without it a root that overrode a deps-level "interactive" with
* "headless" spawned children that fell back to the deps default and were refused at their own door.
* TRUST BOUNDARY (§8.3): only the HOST-authored spec carries this (spec is the deployment-face
* input in this library's trust model); the engine never copies it onto the child specs it
* constructs (a child's lane is the engine's decision — the strip flag is its expression), and a
* RESUME leg re-resolves posture from the CURRENT deps/taskConfig — never from the checkpoint.
* This door is an HONESTY gate, not a security gate.
*
* Runtime side (design/173 §8.2): under `"interactive"` BOTH no-answer arms of a WIRED question
* channel harden into coded tool failures instead of the silent synthetic self-answer continuation —
* a channel that THROWS returns `question.human_channel_failed`, and one that honestly reports nobody
* reachable returns `question.human_unavailable`. See {@link interactiveQuestionFallback} for the
* explicit opt-out, which covers both arms.
*/
interactionPosture?: "interactive" | "headless";
/**
* The run's declaration that NOBODY on it can be asked to approve a call — read by the approval seat's one resolution
* ({@link import("./wiring-manifest.js").resolveAskSeat}). `"none"`: an approver callback the spec or the deployment wires
* is not this run's approver (never called; the manifest reports `ask.form: "declared_none"`); with the deployment's park
* facility armed an ask parks for a person to answer later (`park_only`), without one it is refused as nobody's. It
* approves and releases nothing; a blanket `"allow"` / `"deny"` seat stays a policy; under `dontAsk` the mode refuses
* first. Absent = today's reading. Not checkpointed (a resumed leg holds it only when its taskConfig supplies it again);
* children inherit it (the clamp seats). `interactionPosture: "interactive"` beside it is refused
* (`config.interaction_posture`); a value outside the closed set too (`config.approval_reach_invalid`). Independent of
* {@link interactiveTools} — a content question still goes to `onQuestion`.
*/
approvalReach?: import("./wiring-manifest.js").ApprovalReach;
/**
* design/173 §8.2 — the EXPLICIT knob (default off) letting a posture-`"interactive"` run
* CONTINUE with the synthetic self-answer instruction after all on BOTH hardened question arms
* (the disclosure card + onError leg still fire). Without it those arms return the coded
* `question.human_channel_failed` (channel threw) / `question.human_unavailable` (channel reported
* nobody reachable) tool errors. Meaningless (ignored) outside posture `"interactive"` — headless
* runs already continue.
*/
interactiveQuestionFallback?: boolean;
/** Optional images attached to this turn's user message (model must support vision). */
images?: ImageInput[];
/**
* Files the user attached to this objective ({@link UserAttachment}): at most 20, delivered after the
* UserPromptSubmit hook lets the objective through (a hook that blocks it reads nothing), each as its own
* engine-minted message right after the objective. A path entry needs a workspace (hands on). A malformed entry,
* a text entry over the Read limit (100 000 rendered characters), an image the API would not accept, a path entry
* on a run without a workspace, or any entry on a durable resume leg is refused at the door:
* `config.user_attachment_invalid`, the message opening with the field path (`TaskSpec.userAttachments[2].text: …`).
* A file the engine may not read is not refused: its message says it was not attached and why, and the
* `human_input` event's `attachments[]` reports each outcome.
*/
userAttachments?: readonly UserAttachment[];
/** Reuse an existing in-memory session for chat continuity; omit to start fresh. */
sessionId?: string;
/**
* design/114 Phase3 — with `sessionId` set, REQUIRE that session to already exist: if it is gone (e.g. a
* non-durable store lost it across a restart, or a typo'd id), the run FAILS LOUD (`resume.session_not_found`)
* instead of silently creating a fresh empty session and running with NO warm context. Use this for a
* reuse-style warm-resume (`runTask({sessionId, requireExistingSession: true})`) so "looks warm, actually
* fresh" can never happen. (The `fork`-first path via `warmResume` is already fail-loud without this flag.)
* Ignored when `sessionId` is absent. Default `false` = today's create-on-miss behavior.
*/
requireExistingSession?: boolean;
/**
* Per-request semantics — this SUBMISSION is one-shot:
* no later turn exists in which an async background notification could land (the archetypal case is a
* headless `sema -p` invocation, whose process exits once this turn ends). Per-request rather than
* per-connection/per-process on purpose: "does this particular submission expect to be steered/continued"
* is a property of the submission, not of the channel it arrived on — a persistent connection can still
* mix interactive and one-shot submissions, which a connection-level flag could not express.
*
* Currently consumed by the `RunWorkflow` tool's guidance text only (RB-215 candidate ①):
* a workflow started from a one-shot submission is told to actively block-wait via
* `TaskOutput({ block: true })` before ending its turn, instead of the default "end your turn, you will
* be notified" guidance — the latter is actively wrong here (BGB drilldown case 2, `../sema-test/bench-bg/
* intel/drilldown-C101-B401-20260727.md`: a model that followed that exact guidance lost background
* results because the one-shot process had nothing left alive to receive the notification). Default
* `false`/absent = today's behavior (unchanged for interactive sessions). A deployment MAY thread it
* into other one-shot-sensitive decisions of its own.
*
* INHERITED down the delegation tree (only the `true` value, off the trusted
* {@link ToolExecuteContext.oneShot} seat): the property being described — "the process this
* submission runs in exits when the turn ends" — is a fact about the whole run tree, not about one
* level of it. A delegated child of a one-shot submission that launched its own background workflow
* was told to end its turn and wait for a notification that could not arrive, which is the exact
* lost-results shape this flag exists to prevent.
*/
oneShot?: boolean;
/**
* design/112 C1/C5 — CLIENT-supplied USER facts (distinct from execution-env/container facts). In TOB the
* worker container runs UTC and has no idea who the user is or where they are; the client/REPL/gateway knows.
* It threads these so the `# Environment` block localizes "today" to the user's zone (else it annotates UTC)
* and names who the agent acts for. All optional; absent ⇒ current behavior (UTC date, no user line). This is
* the USER half of the env block — the WORKSPACE half (cwd/git/platform/shell) is probed THROUGH the
* ExecutionEnv (the container), never from here.
*/
clientContext?: {
/** IANA time zone of the user (e.g. `Asia/Tokyo`) — localizes the env block's date. Invalid zones fall back
* to UTC (date AND annotation) rather than mislabeling — see `isValidTimeZone`. */
timeZone?: string;
/** The user's identity (e.g. email) — surfaced in the env block so the agent knows who it acts for. */
userEmail?: string;
};
/**
* design/99 §E18 (resume-at) — BRANCH the session at this prior entry BEFORE the turn: replay root→here,
* then run the new `objective` as the next turn (the shell's "rewind to this message, ask differently").
* Requires `sessionId`. **The id is a core `SessionTreeEntry.id`** — a persisted session-entry id — **NOT a
* live {@link TaskEventIdentity.eventId}** (eventIds are never persisted; a deployment that holds eventIds
* owns the eventId→entryId map). Must reference a `message`/`custom_message` entry, and NOT an assistant
* message that ends mid-tool-call (a SETTLED turn boundary — else the replay would orphan-close a done call).
* Non-destructive: recorded as a branch `leaf` entry, so prior leaves stay in the tree (re-resume any time).
* Mutually exclusive with a durable `runner.resume(...)` (the Runner rejects both at once).
*/
resumeAt?: string;
/**
* design/99 §E18 — how {@link resumeAt} positions the new branch relative to the resolved entry.
*
* - `"at"` (default, and the behavior when this field is omitted): branch AT the entry — **inclusive**.
* The target message stays in the replayed context as the leaf; the new `objective` is the next turn
* after it. This is the "keep that message, continue differently from it" shape.
* - `"before"`: branch BEFORE the entry — **exclusive**. The leaf becomes the target's PARENT
* (`entry.parentId`). The target message and its whole subtree are OFF the new branch (still in the
* tree — non-destructive, re-resume any time).
*
* 🔴 Consumer guidance: a UI that truncates its transcript EXCLUSIVELY — CC-style rewind, where the
* selected user message is REMOVED from the transcript and refilled into the input box — MUST send
* `"before"`. With `"at"` the removed message silently remains the model's leaf, so the model keeps
* answering a message the user no longer sees (transcript and model context split).
*
* `"before"`-specific target rules (STRICTER than the mode-independent settled-boundary validation,
* which still runs first):
* - The target must be a plain USER message (`resume_at.before_target_not_user` otherwise). That is the
* exclusive-rewind product shape — the thing the UI removed is a user message — and it is the only
* target whose parent is guaranteed to be a settled replay point (an assistant or custom-message
* target's parent can be a mid-turn entry that wake-reconcile would mishandle).
* - The target must not be the session's FIRST message (`resume_at.before_root_unsupported`): branching
* before the root would append the new objective as a SECOND root entry and break the session log's
* exactly-one-root invariant (export/import validation rejects such a log). Product semantics:
* rewinding to before the very first message = start a NEW session. (A first-class branch-reset entry
* that could support this without a second root is a recorded design candidate, not built.)
*
* File-history interaction (design/381): with {@link restoreFiles}, the `"before"` file anchor is the
* target's OWN entry id — history boundaries are keyed by a turn's initial entry and mean "file state
* when that turn began", so B(target) IS the before-state. A target with no boundary of its own (a
* steer / follow-up entry, a turn that recorded no history, a mixed-epoch session) refuses at prepare
* (`rewind_snapshot.unresolvable`) with the files untouched: an earlier boundary would also revert edits
* the conversation keeps, so the anchor never falls back to an ancestor and never crosses into an
* earlier turn.
* Fail-loud: setting this without {@link resumeAt}, or an unknown mode value, rejects at prepare.
*/
resumeAtMode?: "at" | "before";
/**
* Which model to run on. If omitted, resolved from the `modelRole` (default `"default"`) against
* `roles` / `RunnerDeps.roles`. Either `model` or a resolvable role must be available.
*/
model?: ModelRef;
/** Which role to resolve when `model` is omitted. Default `"default"`. */
modelRole?: ModelRole;
/** Per-task/scenario role→model overrides (merged over `RunnerDeps.roles`, task wins). */
roles?: ModelRoles;
/** Optional cheaper model for compaction/summarization. Falls back to the `summarize` role if set.
* design/145 §1 semantics: used only while WINDOW-SAFE and budget/walltime allow — when its
* window would clamp-truncate the conversation past `compaction.clampTolerance`, the summary
* call falls back to the MAIN model (CC's unconditional posture; the independent compaction
* model is sema's superset) and the `compacted` event says so (`modelFallback`). Subagent
* declaration (design/145 §5): delegated children never inherit this field — a child always
* self-compacts on its own main model, so the window-safety judgment is structurally never
* triggered in a subagent (guard-tested). */
compactionModel?: ModelRef;
/** Thinking level. If omitted, a default from the resolved role's `RoleSpec.thinking` applies. */
thinking?: ThinkingLevel;
/** This task's role text: it replaces the selected role base (`promptRole`). Text carrying a locked section verbatim
* (an already-assembled prompt) is refused by name (`prompt.assembled_role_refused`). */
systemPrompt?: string;
/** The role base `core/role.base` selects: `"general"` (absent), `"code"`, `"coordinator"`, `"cowork"` (rendered from the
* mounted tools and the working directory); `systemPrompt` replaces whichever is selected. Not inherited (a child gets
* the sub-agent persona). A value outside the closed set is refused at prepare. */
promptRole?: "general" | "code" | "coordinator" | "cowork";
/** `"minimal"` leaves the core-behavior band out; the locked sections (mode sections in play among them) and
* `appendSystemPrompt` stay. Absent ⇒ standard. Rides the parent spec seat to every engine-built child (a clamp-table
* row: no child authors the standard form back). A value outside the closed set is refused at prepare. */
promptForm?: "minimal";
/** The deployment's binding (a scenario name): a prompt selector value, read by every overlay's `when.binding` (center, deploy and task alike); rides the parent spec seat; out of grammar (non-blank, ≤ 128 Unicode code points — `PROMPT_BINDING_MAX_CHARS`, counted `[...s].length`, not UTF-16 units) ⇒ `config.prompt_selector_invalid`. */
promptBinding?: string;
/**
* APPEND a caller block to the assembled system prompt, keeping the engine's base + harness/safety blocks
* (unlike `systemPrompt`, which REPLACES the role base).
* The lightweight user/scenario-extension seam (≈ CC `--append-system-prompt`): add task-specific rules,
* domain context, or a persona tweak without losing the neutral base, cyber/url safety, harness context, or
* memory discipline. CALLER-TRUSTED (same trust level as `systemPrompt`/`skills` — NOT user input). Composed
* as a STABLE block (prepare-time) at the END of the stable prefix, before the volatile `<user_memory>` tail,
* so it never breaks prefix-cache friendliness. Composes WITH `systemPrompt` (appended after it).
*/
appendSystemPrompt?: string;
/**
* The OUTPUT STYLE the deployment's shell has active, declared so the engine can state it to the model in its
* own marked reminder: `"<name> output style is active. <turnReminder>"` (the reference implementation's
* per-turn reminder sentence; absent `turnReminder` ⇒ its default "Remember to follow the specific guidelines
* for this style."). The style's guidelines themselves are not carried here — they go in `systemPrompt` /
* `appendSystemPrompt`; this seat is the per-turn statement that the style is in force, which a reminder the
* shell splices into the objective cannot be (unmarked reminder-shaped text is data). Declare it only for a
* non-default style; there is no client-trusted reminder seat behind it — the engine renders the sentence.
*
* Cadence: the first turn of every leg (a resumed leg's continuation included) and every continuing turn
* boundary — the reference attaches it at every attachment cycle, with no window. Echoed as
* `steering_injected` with source `output_style`.
*
* Refused at prepare (`config.output_style_invalid`), never ignored: a value that is not an object of exactly
* these two members; a `name` that is not a string, is empty, or is longer than 256 characters
* (`OUTPUT_STYLE_NAME_MAX`, the reference implementation's own bound — it drops the reminder past it); a
* `turnReminder` that is present but not a non-empty string.
*/
outputStyle?: {
name: string;
turnReminder?: string;
};
/**
* Enable long-term memory for this task. **design/138 S4 (BREAKING): requires `RunnerDeps.memoryBackend`**
* (the injection-first memory engine — materialize → file ops → harvest). With a backend configured the
* task gets the CC-verbatim `# Memory` instruction + fenced derived index and works memory through its
* ordinary file tools. WITHOUT a backend this field is INERT: the legacy `RunnerDeps.memoryStore` fallback
* (remember/recall tools + legacy `<user_memory>` injection) was retired — the task runs memory-less and
* one deprecation warning is emitted via `onError` (phase `"config"`).
*
* **design/84 Seam A** — single opaque scope → an **ordered, opaque scope list + a single writeScope**.
* The engine only does read-side **layered injection** (one `<user_memory>` block, a `<scope …>` subsection
* per scope, ordered by priority — the LAST scope is highest-priority and lands at the prompt TAIL) and
* write-side **single-point routing** (remember/consolidation only ever write `writeScope`; the model never
* picks a layer). Scope strings stay OPAQUE — the client/profile derives them (TOC: managed/user/project/
* agent/local; TOB: tenant hierarchy). The fields are normalized to one canonical shape
* ({@link import("./memory.js").normalizeMemorySpec}) — `writeScope` defaults to the LAST scope — so the
* consumer side reads one array with zero narrowing.
*
* Cache discipline (§5, implemented in prepare-task): the scopes are injected in list order, so the
* caller places STABLE layers (managed/user/org — rarely change) FIRST (cacheable prefix) and VOLATILE
* layers (project/agent/local) LAST (only the tail re-computes). The stable prefix is byte-stable.
*
* ONE form (RB-479-B②): `scopes` is the only spelling of the read layering. The singular `scope`
* field was removed, and a spec still carrying it is REFUSED at normalization
* (`config.memory_scope_spelling`) rather than read as "no scopes" — a silent memory-off task is the
* one outcome this field must never produce by accident.
*
* The accepted shape is {@link import("./memory.js").MemorySpecInput} (the SAME type
* {@link import("./memory.js").normalizeMemorySpec} consumes) — DESIGN-2 (council): typed by import rather
* than re-declared inline so the input contract and its normalizer can never silently diverge. Per-field
* semantics: `scopes` = ordered opaque layering (STABLE first / VOLATILE last; last = highest priority);
* `writeScope` = the single layer all writes land in (default = last scope; explicit `null` = read-only —
* no `remember` tool, consolidation off); `enabled` = master switch (default true).
*/
memory?: import("./memory.js").MemorySpecInput;
/**
* design/100 §E12 — after the task COMPLETES, run ONE extra opt-in LLM pass to propose "next prompts" the
* user might send (the shell's follow-up suggestions). OFF/omitted = zero extra LLM calls, behavior unchanged.
* Fire-and-forget (never blocks the result); the suggestions are surfaced via {@link TaskStream.suggestions}
* and the (budget-excluded) cost via {@link TaskStats.suggestions}. `count` default 3, `role` default
* `"summarize"` (a cheap tier → falls back to the task model). The output is UNTRUSTED model text for the
* shell UI ONLY — never re-feed it to a model.
*/
suggestNextPrompts?: boolean | {
count?: number;
role?: ModelRole;
};
/**
* The per-run OFF SWITCH for file history (`"disabled"`, the only word; `checkpointStore`'s spelling). Omitted ⇒
* the deployment (`RunnerDeps.fileHistoryStore` / `fileHistoryCapture`) decides. `"disabled"` ⇒ this run makes no history-store call: no first-touch record
* (the edit proceeds), no turn boundary, no fork carry, no liveness advisory; a restore request ({@link restoreFiles}
* / {@link rewindFilesTo}) refuses `rewind.capture_disabled` (the store may be wired; the request turned it off).
* The manifest reports `fileHistory: "disabled"`. Children inherit it (parent spec seat; a retained child woken by
* such a run, for that cycle); no word turns it back on below. Any other value refuses (`rewind.invalid_spec`).
*/
fileHistory?: "disabled";
/**
* design/381 (DV-9) — the RESTORE half of file rewind: when this task uses {@link resumeAt} to branch at
* a prior entry, ALSO converge the working tree's **agent-edited files** (the tracked set — files this
* session touched through Edit/Write/NotebookEdit; nothing else is ever visible to the history, so a
* user's parallel work is structurally untouchable) to the history boundary of that entry (pairs with
* E18: branch the session + restore the tracked files). SCOPE = THIS session: a delegated
* subagent runs under its own session and its edits land in its own history scope, so a parent
* rewind does not converge files only a subagent touched (the retired whole-tree restore did —
* a design/381 narrowing, disclosed here). Requires `RunnerDeps.fileHistoryStore`.
* Fail-loud: setting this without {@link resumeAt} rejects at prepare (`rewind.invalid_spec`) — a
* restore with no anchor is a contradiction, not a default.
*
* There is NO capture request anymore: first-touch tracking is ON whenever a `RunnerDeps.fileHistoryStore`
* is wired, `RunnerDeps.fileHistoryCapture` is not `"off"`, and no {@link fileHistory} off switch is set (per-edited-file backups are near-zero cost — no tree walk,
* no size gate), which is what retired the old `rewindFiles` capture request. The retired
* `rewindFiles` spelling is handled at prepare: its capture sense (true, no `resumeAt`) is a
* tolerated no-op with a deprecation disclosure (the intent is structurally satisfied); its restore
* sense (true, with `resumeAt`) is a typed retirement rejection naming THIS field — the restore
* semantics genuinely changed (whole-tree ⇒ tracked set), so that migration must be explicit.
*/
restoreFiles?: boolean;
/**
* design/381 (DV-15) — opt-in tolerance for a PARTIAL file restore: when the {@link restoreFiles} /
* {@link rewindFilesTo} convergence ends with ≥1 per-file refusal or failure (the others applied),
* the default is a loud terminal failure carrying the per-file ledger (`rewind.restore_failed`) —
* a silent continue would run the agent on a mixed-epoch tree without anyone knowing. Setting this
* `true` keeps the run going instead and discloses the same ledger as a `restore_partial` note on
* {@link TaskResult.rewindNotes} (the reference UI's "skipped files" seat in library form). The
* documented recovery either way: re-run the same restore — it is per-file idempotent and converges.
*/
acceptPartialRestore?: boolean;
/**
* design/101 §E19 R8 + design/381 (CC Rewind `code`-only mode) — converge the tracked set (see
* {@link restoreFiles} for what that is and is not) to the history boundary of this prior entryId
* WITHOUT forking the conversation (no {@link resumeAt} leaf-branch). This is CC's "Restore code"
* mode (vs "Restore code and conversation" = {@link resumeAt}+{@link restoreFiles}, and "Restore
* conversation" = {@link resumeAt} alone). Legal ONLY when {@link resumeAt} is ABSENT — handing
* BOTH anchors is refused loud (`rewind.conflicting_targets`): two file targets is a
* contradiction, and silently preferring one is the silent-swallow family this seam refuses; a
* target with no history boundary fails loud (`rewind_snapshot.unresolvable`). Requires
* `RunnerDeps.fileHistoryStore`. The session leaf is untouched — the next turn continues the
* CURRENT conversation, only the files moved.
*
* A same-process child THE AGENT TOOL SPAWNED FRESH (the legs that are handed a lineage — see
* `ToolExecuteContext.fileHistoryLineage` for the exact set, and for the legs that are not) which
* shares its root session's tree records its edits into the ROOT session's history, so a rewind
* that should cover those edits is the root session's action; a restore request on such a child run
* is refused loud (`rewind.child_scope_unsupported`) rather than converging a scope that holds none
* of its edits. A delegated child that was handed NO lineage — a revival, a workflow/team member —
* keeps its own scope: its edits are outside this target's reach, and its own restore request is an
* ordinary one.
*/
rewindFilesTo?: string;
/**
* Native tools available to this task.
*
* NAME OCCUPANCY IS A DECLARATION: a caller tool that takes a built-in's reserved wire name
* shadows that built-in (the hands band name-shadows caller tools the other way around only for
* its own mount), and the engine reads the occupancy itself as the caller declaring it will serve
* that CHANNEL — most concretely, a tool named `Write` on this roster counts as the memory write
* channel, so the `# Memory` write instruction (and its index read-seed) is supplied whenever a
* `Write` is mounted and not excluded, whatever the tool's `execute` actually does (the engine
* cannot read execute semantics; the NAME is the only judgeable fact). A shadowing mount is also
* surfaced to the operator as a config-phase warning. Protocol tools (MCP/A2A) can never collide
* here: their wire names are namespaced (`mcp__<server>__…`) and a caller name containing `__`
* is rejected at prepare.
*
* Mount point: the roster (with `defer`/`excludeTools`/`deferTools`) lives on this PER-TASK spec —
* the Runner constructor's deps carry no tool roster. Plain-JS callers beware: an unrecognized key
* passed to the constructor is dropped by ordinary object semantics (TypeScript callers get an
* excess-property error), so a roster placed there never mounts and its `defer` flags never apply.
*/
tools?: ToolSpec[];
/**
* The run's tool ALLOWLIST — the availability selector (true unmount, NOT a permission gate): when set, the run may use only the
* tools it names, out of everything the engine would otherwise mount for it (the caller's tools, the hands, the engine's own
* tools, MCP and A2A tools, scenario and leg tools); the rest never reach `tools[]`. Omitted ⇒ the default set (a delegated
* child: whatever its parent could use). `[]` ⇒ no tools. An entry `"*"` ⇒ no allowlist on this layer (still bounded by the
* ceiling a spawning run handed down — a child's `"*"` never widens its parent).
*
* Entries are read like {@link excludeTools}: a wire name or an alias it declares, `mcp__srv` / `mcp__srv__*` for every tool
* of a server (including tools it lists later), a glob over the tool segment. The engine resolves the list against its OWN
* roster at prepare — a host passes the names it was given. A spelling no tool name can have, `mcp__*`, or an entry that is not
* a string is REFUSED at prepare (`config.tool_face_control`); a name no tool answers to is kept and told once (`onError`, phase
* `"config"`). The same compiled selector is applied on every later roster change (an MCP refresh or re-dial, the
* deferred-search lane). Deferral is reached through ToolSearch, so a list that leaves it out defers nothing.
*
* A tool a leg relies on (a delegated child's ReportBlocked, a teammate's SendMessage, a goal iteration's DeclareDone, an
* observer's report tool — the catalog's protocol column) is not an allowlist's to name; a denylist that reaches one on such a
* leg is refused at prepare.
*/
allowTools?: string[];
/**
* per-scenario tool ROSTER removal (true unmount, NOT a permission gate): the tools this list names are removed from the
* mounted tool set AFTER assembly — their schemas never reach `tools[]` (unlike a policy deny, which gates calls but still
* ships the schema bytes every request — measured at zero token savings). The DENY half of the run's tool selector, read
* exactly as a sub-agent's denied tools are: a wire name or an alias it declares; `Tool(content)` names the whole `Tool`;
* `mcp__srv` / `mcp__srv__*` / a glob over the tool segment for a server's tools, `mcp__*` for every MCP tool (including tools
* a server lists later). A spelling no tool name can have, `"*"`, or an entry that is not a string is REFUSED at prepare; a name
* no tool answers to is kept (it hides a tool of that name mounted later) and told once. A deny wins over {@link allowTools}.
* ⚠️ A list that reaches a tool this kind of leg relies on (see {@link allowTools}) is refused; excluding a host-requested
* structural tool (the structured-output tool) can still strand the task — the caller owns that risk.
*/
excludeTools?: string[];
/**
* design/388 §2.2 ⑥ — unmount the WHOLE tool face: the leg mounts nothing (no hands, no
* scaffolds, no MCP or skill tools; the effect roster reads `entries: []` and the model request
* carries `tools: []`). The utility-task shape a shell used to express with a hand-kept exclusion
* list. Not spelled `excludeTools: ["*"]` on purpose — `"*"` is a legal caller tool name (only `__`
* is reserved). `excludeTools` beside it is redundant, not an error.
*/
excludeAllTools?: true;
/**
* Per-request DEFERRED disclosure for ALREADY-MOUNTED tools (built-ins
* included — `ToolSpec.defer` only covers caller specs): wire names listed here ship as
* lightweight placeholders (name + one-line hint, schema bytes OFF the cache prefix) and are
* activated on demand via the injected ToolSearch (design/36 materialization — prefix untouched).
* This list is LITERAL — an intentional deferral, deferred on every request regardless of the
* deferral gate ({@link RunnerDeps.deferMode}) and not counted toward its threshold.
* "Default-on but not exposed" = the deployment lists the tool here on requests where no
* activation source fired (e.g. Workflow unless the user said "workflow"/"ultracode"); omitting
* the name on a later request mounts it fully (activation = not deferring). Unknown names are
* ignored. Names also in `excludeTools` are excluded (exclusion wins — nothing to defer).
*/
deferTools?: string[];
/**
* design/168 (P3) — how a deferred tool's ACTIVATION reaches the provider-facing tools block.
* `"swap"` (default, prior behavior): ToolSearch activation swaps the placeholder for the full
* schema via setTools — the tools block changes mid-run, which invalidates the provider prompt
* cache for everything after it (tools serialize FIRST in the cache hierarchy). `"static"`: the
* tools block stays byte-identical for the whole run — placeholders never swap; the full schema
* reaches the model only through the ToolSearch result text (the existing delta-as-tool-result
* carrier), and argument validation against the REAL schema is unchanged (engine-side). Genuine
* roster changes (an MCP refresh adding/removing names) still propagate on either setting.
*
* DEFAULT `"swap"` (rolled back 2026-08-06 after a brief default-`"static"` window). `"static"` is
* a STRICT EXPLICIT OPT-IN, and the condition to opt in is a property of the whole serving route,
* not of one model: every model the run can reach — the prepare-time model, every cross-provider
* fallback target of the degrading brain, and any near-budget model hand-off — must be able to
* emit arguments it read from RESULT TEXT while the advertised wire schema stays empty. A model
* served with CONSTRAINED DECODING cannot: its sampler is bound by the advertised
* `{"type":"object","properties":{}}`. On the mainstream implementations — the ones that compile a
* grammar from the declared `properties` — that admits exactly one argument object, `{}`, so the
* engine's corrective rejection can never be acted on and the run burns turns instead of
* converging. (A decoder that instead reads the schema by strict draft semantics would treat the
* absent `additionalProperties` as permissive and let other keys through; the model still has no
* schema to form them FROM, so the best case there is guessing, not converging.) A protocol dialect
* is no guarantee either (an Anthropic-shaped endpoint may serve a constrained-decoding model),
* which is why the opt-in is the deployment's declaration that it verified the route, fallback
* targets included, rather than anything core can infer.
*
* Interlock with {@link TaskSpec.deferSelfResolve}: `"static"` requires the direct-call lane — a
* never-swapped placeholder with the lane disabled would make every deferred tool permanently
* uncallable. Explicitly pairing `"static"` with `deferSelfResolve: false` is refused at prepare
* (`config.tool_materialize_unreachable`); a `"static"` arriving from the env channel degrades to
* `"swap"` on such a task instead. A value outside the closed set refuses loudly
* (`config.tool_materialize_invalid`) — a typo must not silently select the opposite arm.
*/
toolMaterializeStrategy?: "swap" | "static";
/**
* The inline-pin counterpart of {@link deferTools} (same wire-name addressing, opposite
* direction; CC 220 `alwaysLoad` 对位): names listed here are NEVER deferred — not by
* `ToolSpec.defer`, not by the MCP constant-defer arm (besides the server's own
* `_meta["anthropic/alwaysLoad"]` declaration this list is the only channel that keeps a chosen
* MCP tool's full schema inlined), not by `deferTools`, not by the deferral gate under any `deferMode`. Unknown names
* are ignored (the valve only subtracts). Names also in `excludeTools` stay excluded (exclusion
* wins — an unmounted tool has no schema to keep inline).
*/
alwaysLoadTools?: string[];
/**
* design/277 — per-task restore valve for the tool-registration MODEL GATE. The gate trims
* default-mounted scaffold entries (tagged via {@link ToolSpec.modelGate}) from the roster when
* the task's resolved model id matches the gate table — this valve opts the task (and its whole
* delegation tree — the selector inherits down like `excludeTools`) back in:
* · an ARRAY of wire names — naming ANY tagged tool restores that tool's WHOLE class for this
* task (CC parity: opting into any member of the family restores the family). Judged against
* the full stamp set, so a name that `excludeTools` also lists still works as a class
* SELECTOR — while the exclusion itself still wins for that name (exclusion is the final
* valve; no restore channel resurrects an excluded name). Unknown names are inert (a
* center-distributed list may be a superset), same posture as `excludeTools`.
* · literal `true` — every gate class restored for this task (the shortest "this task wants
* all its scaffolding" spelling for center-distributed specs).
* A malformed value refuses at prepare (`config.tool_model_gate_invalid`, #123) — garbage must
* not silently read as "restore nothing" in the trimming direction. Absent = the gate's verdict
* stands. Explicit composition needs no valve: a hand-mounted (untagged) tool is never gated.
*/
restoreGatedTools?: string[] | true;
/**
* Whether a schema-VALID call on a still-deferred tool runs the real tool directly
* (activating it as a side effect). Default `true`, matching the upstream posture where the
* defer gate is shape validation, not activation state (a deferred tool called with correct
* arguments succeeds; activation only controls whether the model has SEEN the schema). Set
* `false` to restore the strict posture: every call on an un-activated placeholder is rejected
* with the ToolSearch teaching message regardless of argument shape. Governance is unaffected
* either way — policy/effect/irreversibility gates key on the tool NAME maps and run upstream
* of any execute.
*/
deferSelfResolve?: boolean;
/**
* R2 双形轴(2026-07-18 裁): which prompt-profile face this task speaks — "simple" (default;
* CC 2.1.212's short-form tool descriptions + lean system sections, what CC serves every non-claude
* model id) or "classic" (the long-form face, ≈ CC 4.x-era wording — switchable per task/model,
* e.g. serving classic to a model that scores better on it; center distribution rides this field).
* Affects PRESENTATION only (descriptions/section wording), never tool behavior or policy.
* Inherited by delegated children unless their spec overrides.
*/
promptProfile?: "simple" | "classic";
/**
* core half — PER-TASK custom sub-agents. Same shape as the boot injection point
* ({@link RunnerDeps.agents} / `SubagentToolOptions.agents`): full {@link AgentDefinition} objects
* (the shell resolves agent files / frontmatter into this shape; `model` is the already-resolved
* real model ref passed through — core carries no model-word vocabulary). When set (non-empty),
* `prepareTask` REBUILDS the mounted delegation tool (via {@link ToolSpec.withAgents}) so its roster
* is the UNION of the boot roster and these definitions — a same-name per-task definition WINS over
* the boot one. Validation contract (the reference's shape — an agent's tool list filters by name,
* and a name nothing answers never fails the run): both lists are judged once, here and for the
* delegation tool's own lists alike; a `denyTools` entry is read as the reference reads it
* (`Tool(content)` hides the whole `Tool`, with a host notice); an entry whose spelling no tool name
* can have, or is another spelling of a tool the child is handed (a tool of the pool the delegation tool declares, or the engine's catalog: a case twin, a retired name with a successor: `"near_miss"`, while that pool can be listed), is REJECTED at task start (`config.agent.unknown_tool`); an entry naming a tool that does
* not exist (a server this deployment does not declare, a misspelt name) is kept as written
* — it denies, or admits, a tool of that name if one is mounted later — and told to the host once
* via `RunnerDeps.onError` phase `"config"` (zero model-facing warning); and
* providing this field with NO delegation tool mounted is rejected too
* (`config.agents.no_delegation_tool`) — never a silent no-op. POLICY-NEUTRAL: a
* roster listing only; the 1.294 tighten-only parent-policy inheritance is applied at spawn time,
* downstream of this list, so a per-task agent can never widen what deployment/parent policy allows.
* Omitted/empty ⇒ byte-identical prior behavior.
*/
agents?: AgentDefinition[];
/** Gate tool calls before they run (allow/deny/approval). Overrides `RunnerDeps.toolPolicy`. */
toolPolicy?: import("./tool-policy.js").ToolPolicy;
/**
* #93 (F-012 L3) — the OVERRIDE seat for the durable resume-edit re-adjudication policy: the policy
* an approver's `updatedInput` EDIT is re-checked against before a resumed pending call executes
* (and, at park-mint time, the policy a store-codec-moved projection is re-adjudicated by). ABSENT
* falls back to the caller policy (`toolPolicy ?? RunnerDeps.toolPolicy`) — never to a silent skip:
* the pre-#93 shape skipped the whole recheck for deployments with no caller policy, which let a
* fidelity-projection-moved value land unadjudicated. Supply this only to make the resume-edit
* boundary STRICTER/different from the live caller policy; it never widens (the frozen ancestor
* projections and the deny-narrowing layers still apply regardless).
*/
basePolicyForResumeEdit?: import("./tool-policy.js").ToolPolicy;
/** How `ask` decisions resolve for this task (headless auto-deny by default). Overrides `RunnerDeps.onAsk` when present.
* A callback here is set aside when the run declares {@link approvalReach} `"none"`. */
onAsk?: import("./tool-policy.js").OnAsk;
/** Content-ask seam (design/64 §5): routes an AskUserQuestion tool call to a real human/UI. When set, the
* AskUserQuestion tool is mounted (one of the delivery faces in the criterion — see
* {@link interactiveTools}). Overrides `RunnerDeps.onQuestion`. Distinct from `onAsk` (permission). */
onQuestion?: import("./ask-question.js").OnQuestion;
/** LSP code-intelligence seam (design/64 §13.1): when set, the `lsp` tool is mounted on a leg with hands and routed to
* this manager. Overrides `RunnerDeps.lspManager`. Neither seat set, or no hands ⇒ the engine mounts no `lsp` tool (the model uses grep/read_file). */
lspManager?: import("./lsp.js").LspServerManager;
/**
* design/121 (CC 198 parity): after the agent edits a file, NEW language-server diagnostics are
* injected into the model (`<new-diagnostics>` context message, 10/file · 30 total · 4000-char cap)
* and emitted as a structured `diagnostics` event for the shell. **Default ON** whenever the
* preconditions hold — an LSP manager with a diagnostics registry is wired AND the roster has
* write-capable fs tools (CC's same gate); zero cost/behavior change otherwise. `false` opts out.
*/
lspDiagnostics?: boolean;
/** The task seat of the mods bus — `user` tier only, beside `RunnerDeps.mods` (never instead); INTEGRATION §10. Its
* registrations hang handlers on the `classic.*` events the engine's stations raise (tool pre/post/failure/batch,
* prompt submit, stop, compaction, permission denied). A delegated child dispatches its parent's task seat as its own
* (the parent spec seat fills an unset one). */
mods?: readonly import("./mods/kernel/index.js").ModRegistration<import("./mods/dollar.js").EngineDollar>[];
/**
* The session's SELF-REPORTED presence for the peer directory (`agent` = what it is, `intent` = what
* it is doing; each ≤ 120 chars). The engine never writes a directory row: the same object goes out
* on the leg's `wiring_manifest` so a host copies it into the row it writes, and `ListAgents` renders
* it on other sessions' listings prefixed `self:`. Labels only — never read by any judgment.
*/
peerPresence?: import("../agents/peer-directory.js").PeerPresence;
/**
* design/45 **durable suspend-on-approval** (F4). When set AND a `CheckpointStore` is wired
* (`checkpointStore` here or on `RunnerDeps`), a tool-policy `ask` **can suspend durably**: when no LIVE
* approver is present, or a control-plane `forceDurableGate` mandates it, the ask is **persisted as a
* checkpoint** and the task ends `status:"suspended"` + `checkpointToken` — resume later with
* `runner.resume(token, outcome)`. With a live approver, the ask resolves synchronously in-stream FIRST
* (same turn, no checkpoint) — this option is the durable FALLBACK for when no live approver can answer,
* not an "always park" mandate. It is the cross-process / long / headless version of the `onAsk` gate.
* **opt-in, default-off**: unset → the synchronous `onAsk`-only path (a headless ask then auto-denies).
* Only safe at the top-level `runTask` (orchestrators fail-fast, §11 Q6).
*
* **Precedence vs a live `onAsk`:** "live approver" means a FUNCTION-valued `spec.onAsk ?? deps.onAsk`.
* With one wired, BOTH plain and SAFETY asks (egress/irreversibility tightens) resolve through it
* in-stream — same turn, no checkpoint, the gate recorded in `humanReview.gates` — and this option acts
* as the no-approver fallback. The durable park keeps priority only when: (a) there is no function seat
* (absent, or a blanket `"allow"`/`"deny"` string mode — a policy setting is not a reachable human);
* (b) a control-plane `forceDurableGate` entitlement is in force; (c) the live face already answered
* `"unavailable"` for this ask; or (d) the call is marked inherited-unavailable by an ancestor.
*/
durableApproval?: {
/** Multi-tenant isolation key, forced into the checkpoint + `resolve`/`reap` WHERE. Required. */
scope: string;
/** Optional awaiting-human TTL (ms). Past it a reaper CAS-expires the checkpoint (≈ deny). */
ttlMs?: number;
};
/**
* design/74: opt into resource-slice suspend. When a resource limit (maxTurns / budget / walltime) is hit
* AND the task is eligible (a `checkpointStore` + a remote, suspendable `executionEnvFactory` env), suspend
* as a resumable `resource_limit` checkpoint instead of failing (the "失败≠没做完" third state) — resume
* continues the run with the next slice's allowance. `scope` is the multi-tenant isolation key (like
* `durableApproval.scope`). Absent ⇒ a limit hit still FAILS (backward compatible). Independent of
* `durableApproval` (a run can have one, both, or neither).
*/
resourceSuspend?: {
/** Multi-tenant isolation key, forced into the checkpoint + `resolve`/`reap` WHERE. Required. */
scope: string;
/**
* design/74 Slice 4: the task's TOTAL $ budget (USD) across the whole resume→re-suspend chain — the
* human's allocation, distinct from `limits.maxCostUsd` (the per-SLICE window). Each slice may spend
* `min(limits.maxCostUsd, total − alreadySpent)`; the ledger (carried on the checkpoint) accumulates
* spend so a resume can't refresh the budget. `undefined` ⇒ no $ ceiling on the whole task (each slice
* is bounded only by its own window). Set ONLY on the FIRST slice; later slices read the frozen total
* from the ledger.
*/
totalBudgetUsd?: number;
/**
* design/164 (the TOKEN sibling of `totalBudgetUsd`, replacing the retired walltime total): the task's
* TOTAL token allocation across the whole resume→re-suspend chain — distinct from `limits.maxTokens`
* (the per-SLICE window). Each slice may spend `min(limits.maxTokens, total − alreadySpent)`; spend
* accumulates on the ledger's `spentTokens`, so a resume can't refresh the allocation. An EXHAUSTED
* resume (0 remaining) fails fast without running a model turn. `undefined` ⇒ no token ceiling on the
* whole task. Set ONLY on the FIRST slice; later slices read the frozen total from the ledger.
*/
totalTokens?: number;
/**
* design/74 (HIGH-1): max resource SLICES (resume legs) before the run fails with
* `"suspend.loop"`. The resource-slice analogue of `maxSuspends`, but counted on the ledger's
* `sliceCount` and INDEPENDENT of the restart-loop `maxSuspends`: a legitimate long resource run must
* not be capped by the (low) restart-loop safety, and restart-loop suspends must not consume the slice
* allowance (the bug: both shared one `suspendCount` + `maxSuspends`). The cross-slice allocations
* (`totalTokens` / `totalBudgetUsd`) are the PRIMARY bound; this is a backstop against a
* zero-progress slice loop. **Unset ⇒ no slice ceiling** (design/164 default retirement): a driver
* that resumes a task which makes no progress will loop until an allocation binds, so a host driving
* resumes unattended should set this.
*/
maxSlices?: number;
/**
* design/74 (HIGH-2): retention TTL (ms) stamped onto the resource checkpoint's `deadline`
* so the durable reaper can GC an orphaned (never-resumed) resource suspend. WITHOUT it the checkpoint
* has no `deadline`, both reapers filter on `deadline`, and the `pending` row (+ in-memory pin) leaks
* forever. Default `DEFAULT_RESOURCE_TTL_MS` (30 days, the spec `RESOURCE_SUSPEND_TTL_SEC`). A legitimate
* resume happens long before this; the deadline only reaps abandonment.
*/
ttlMs?: number;
};
/**
* design/80 Seam #2 (external preempt/yield): the SCHEDULER raises this `AbortSignal` to PREEMPT this task —
* a graceful, RESUMABLE durable yield (free resources for a higher-priority task), NOT an abort (abort kills
* the run and loses its state). When raised, the task durably suspends at the next CLEAN turn boundary as a
* `resource_limit` checkpoint with reason `"preempt"` (the SAME mechanism as a budget/turns/walltime resource
* suspend — only the trigger differs), and is resumed later via `runner.resume(token, {gate:"resource_limit",
* decision:"continue"}, …)` exactly like any resource-limit resume (continue the work, no decision).
*
* **Eligibility (v1 reuses the resource-suspend opt-in):** preempt only fires when the task is eligible for
* durable suspend — i.e. it opted into {@link resourceSuspend} AND a durable `checkpointStore` is wired AND
* tool-results are durable AND the per-task env is a remote (suspendable) `RemoteExecutionEnv`. This is
* correct: a non-durable task cannot be preempted-AND-resumed anyway. If `preemptSignal` is raised on a task
* that is NOT resource-suspend-eligible, preempt is a SILENT NO-OP (the run continues normally to its natural
* end) — decoupling preempt's own opt-in from `resourceSuspend` is a future enhancement, not built here.
*
* **Re-supply on resume:** like `resourceSuspend`/`durableApproval`, this is part of the resume task config —
* a long task that is resumed may need re-preemption, so supply a FRESH `preemptSignal` (a new
* `AbortController.signal`) in the resume `taskConfig` to make the resumed leg preempt-able again.
*/
preemptSignal?: AbortSignal;
/**
* design/131 — per-task resilience overrides, INTENT-ONLY (no decorator
* topology on the spec; the deployment's brain stack stays the single owner). For bench/eval or
* critical tasks that need the provider's TRUE failure shape instead of a silent fallback:
* - `allowDegrade:false` — reactive model degrade is off for this task (failure passes through).
* - `allowFailover:false` — cross-gateway failover is off (only the primary brain is tried).
* - `bypassBreaker:true` — an open circuit breaker's fast-fail is waived for THIS task; outcomes
* are still recorded (shared observation undiminished). Operator-facing — deployments SHOULD
* NOT expose this to wire users directly (it defeats provider protection).
* All absent ⇒ exactly today's behavior. Threaded per-call as `StreamOptions.resilience`; the
* first-party degrading/failover/breaker decorators honor it, custom brains may ignore it.
*/
resilience?: ResilienceOptions;
/**
* design/132 — final-verification gate (OPT-IN: `true` enables). When a task is about to end
* NATURALLY (inside budget) and this run made at least one non-`read` tool call, the engine
* injects a reminder to re-verify the final deliverable through its REAL entry point
* (execute it directly; don't trust earlier self-tests or shell redirections) before finishing.
* Oracle-grounding upgrade (docs/DESIGN-ORACLE-GROUNDING-GATE-2026-07-10.md §3.A): if the run's
* tool-call record carried a deterministic verifiable-structure signal (raw-byte read /
* structural parse / digest-reconciliation word-faces in shell commands — never a semantic scan
* of closing text) and the model ends AGAIN without headroom concerns, ONE grounding re-entry
* reminder is injected; total injections are hard-capped at 2, then the run always ends. A
* submitted structured output and the maxTurns-1 boundary always pass through un-gated.
* Evidence: TB full-run autopsy — the biggest shell-gap bucket (7-8 tasks) finished convinced by
* their own fake self-check; prompt discipline alone (1.242) had a low landing rate. Opt-in
* because the evidence is from UNATTENDED long-task legs; consumers default it OFF and expose an
* explicit opt-in (ruled 2026-08-04). Interactive deployments choose per scenario. Read-only /
* Q&A runs never trigger it. Telemetry: `stats.mechanisms.finalVerifyInjected`.
*/
finalVerification?: boolean;
/**
* design/72 §2.2 (B): max times this task may durably suspend before the run fails with
* `"suspend.loop"` instead of minting another checkpoint. Caps a resume/restart loop (a model that
* re-issues the gated/egress call every resume, re-suspending forever). Counted across resume chains.
* Default 5 (loose — a real human-approval flow re-suspends rarely, so 5 separates a normal resume from a
* pathological loop; a deployment that genuinely needs more sets this). Overrides `RunnerDeps.maxSuspends`.
*/
maxSuspends?: number;
/**
* Per-task {@link import("./checkpoint-store.js").CheckpointStore} for durable suspend/resume
* (design/45). Overrides `RunnerDeps.checkpointStore`. Needed (with `durableApproval`) to suspend.
*
* THREE-VALUED (ruled 2026-08-04):
* · `undefined` — inherit `RunnerDeps.checkpointStore` (the historic shape, unchanged);
* · a store — this run's store, overriding the deployment's;
* · `null` — this run explicitly has NO store, even on a deployment that wired one.
*
* The `null` arm is the per-run OFF SWITCH for the whole durable machine, meant for a run a MACHINE
* started rather than a person (a notification redelivery, a maintenance sweep). Such a run must
* never reach `status:"suspended"`, because the checkpoint it would mint has no operator who will
* ever come back and resolve it — a permanently pending row plus a permanently pinned session.
*
* It works BY CONSTRUCTION rather than by a new rule: all four legs into `suspended` — the approval
* park, the design/74 resource-slice pause, the design/164 platform pause (`env_lifetime` /
* `usage_window`), and the design/80 `plan_review` pause — are already gated on a store being
* present, so removing the store removes all four at once and the run falls into the ALREADY-TESTED
* no-store deployment semantics: a policy `ask` resolves at a live `onAsk` when one is wired and
* otherwise fail-closed denies; a resource/platform limit is a loud terminal (`limits.*_exceeded` /
* `env.lifetime_expired` / `usage.window_exhausted`, still carrying `retryAfterMs`); the
* interactive plan/question tools are not mounted (no delivery face exists). Nothing about it is a
* second set of rules to keep in sync with the first.
*
* Consequence worth stating, because this field used to answer only "WHICH store": on a store-null
* run {@link Runner.preempt} is a documented no-op — there is no suspend to preempt into, so the
* request is dropped with the existing `preempt.ignored` trace instead of pausing the run.
*
* Delegated children inherit it: the sync / background / fork spawn lanes copy the `"disabled"` word
* into every child spec (same discipline as `principal`), so a store-less run's whole tree stays
* store-less.
*
* The off switch is the WORD `"disabled"`, never `null`: a string is not nullish, so the ordinary
* `spec ?? deps` coalesce is the whole resolution rule and no reader has to remember a special
* null test (the retired `null` spelling is refused loudly at the first read, with the word to write).
*/
checkpointStore?: import("./checkpoint-store.js").CheckpointStore | "disabled";
/**
* Restrict the injected "hand" band (design/44) to its effect:read tools for this task — read_file /
* grep / glob (+ `bash_readonly` when a shell is available); edit_file / write_file / `bash` are not
* mounted. This is the verifier read-only boundary (§6): an falsification-style verifier inspects but must not
* modify the project. No effect unless an `executionEnv` is injected. Default false.
*/
handsReadOnly?: boolean;
/**
* Whether this run can persist a user's "remember X" somewhere durable — the deployment's own
* statement, overriding the runner's inference. The runner mounts a read-only-memory disclosure
* (the model must decline to "remember" instead of receipting a save that never happens) when the
* session provably cannot persist; provability is inferred from KNOWN store paths only (a mounted
* file-write tool or a write-capable shell). A caller tool that persists through its own closure
* (a custom memory writer) is invisible to that inference and would be contradicted by the
* disclosure — set `true` to declare the channel and suppress it. Set `false` to force the
* disclosure even when write-capable tools mount (e.g. they cannot reach any durable store);
* a forced `false` also replaces the `# Memory` write instruction itself and drops the
* preference-writing discipline and index seed that serve it.
* Default: inferred. The inference also treats a REMOTE execution env's hand band as unable to
* reach the host-side memory root (sandbox filesystem); a deployment whose remote env shares a
* mount with the memory root declares `true` — on such a remote, `true` also restores the
* `# Memory` write instruction when a Write tool mounts (a declared-capable session must be told
* the path, not left silent). If your persistence channel is NOT the file face, do not mount a
* tool named `Write` alongside the declaration, or keep the default and teach your own channel.
* Delegation: a DECLARED value crosses the delegation boundary (sync / background / fork spawns and
* the retained-resume snapshot alike). An explicit `false` is a FLOOR — a chosen
* {@link AgentDefinition.memoryPersistenceCapable} `true` cannot loosen it (the disclosure is
* about the deployment's storage, which no agent selection changes); an explicit `true` is a
* DEFAULT the chosen definition may narrow back to `false`; absent = absent downstream too — each
* child runs its own inference over its own roster. A retained child woken by a DIFFERENT run
* additionally folds the waker's declared `false` on top of the spawn snapshot (tighten-only,
* like the other resume clamps).
* KNOWN GAPS (registered, whole-clamp-family shapes — `handsReadOnly`/`interactiveTools` share
* them): a TIER-3 durable revival rebuilds the child from the REVIVER's context (the durable row
* records lookup keys, never a serialized spec), so the spawn-time declaration does not survive
* that lane — the reviver's own declaration governs; and the workflow HOST lane does not forward
* the host TaskSpec's declaration into `agent()` children (an {@link AgentDefinition} on the
* workflow agent type does carry).
*/
memoryPersistenceCapable?: boolean;
/**
* design/119 (CC --add-dir parity): extra directories the FILE tools may access in addition to the
* containment root — each is canonicalized into the containment allowlist and listed in the
* `# Environment` block so the model knows they're available. Same defense-in-depth caveat as the
* root: this bounds the file tools, not `bash` (OS-level containment is the env's job).
*/
additionalDirectories?: string[];
/**
* Interactive read-boundary whitelist (ruled 2026-08-05, both-arms ruling, arm A): extra directories
* the READ faces may access — and nothing else. Joins the containment allowlist of the read-only
* tools (read_file / grep / glob / repo_map / `bash_readonly`) and the shell read station's
* boundary (so a provably read-only command like `head <dir>/settings.json` auto-allows instead
* of asking on every call), but is NEVER admitted by the write faces (edit_file / write_file /
* notebook_edit refuse these paths exactly as before). Use `additionalDirectories` when the task
* should also write there. Same canonicalization and fail-closed skip rules as
* `additionalDirectories`. Typical use: a deployment's own user-config directory in an interactive
* assembly, where a read-classifying shell gate would otherwise consult the approver for every
* config read.
*/
additionalReadDirectories?: string[];
/**
* design/199 件B — TASK-layer ADDITIONS to the built-in sensitive-path READ deny set
* ({@link import("../tools/fs/read-deny.js").READ_FACE_DEFAULT_DENY_ENTRIES}). Judged by the
* structured read faces (Read/Grep/Glob/RepoMap, their traversals, the compaction attachment reader) and
* by the shell READ STATION under every mode — the full shell (`Bash`, `Monitor`, whatever
* {@link shellGate} says) and the `bash_readonly` shell mounted by {@link handsReadOnly} alike: a
* deny-listed shell read is REFUSED at the gate (`deniedBy: "read_boundary"`), never asked, with the same
* sentence the structured faces refuse with — one deny judge for every read face — in BOTH containment
* modes. Add-only at
* the task layer: entries here UNION with the active built-ins and the deployment's
* {@link RunnerDeps.readDenyPatterns}; `[]` ≡ absent (union identity); NOTHING at the task layer
* can remove a built-in — the one removal channel is the DEPLOYMENT's built-in configuration
* ({@link RunnerDeps.readDenyBuiltinTiers} / {@link RunnerDeps.readDenyBuiltinExclude}, #245
* revision of D-4), which no TaskSpec key reaches (an unconfigured deployment judges the default tiers,
* {@link import("../tools/fs/read-deny.js").READ_DENY_DEFAULT_TIERS}). Bad entry shapes refuse loudly at prepare
* (#123). The write faces are untouched.
*/
readDenyPatterns?: readonly import("../tools/fs/read-deny.js").ReadDenyEntry[];
/**
* TASK-layer rows ADDED to the write-protection table ({@link readDenyPatterns}' law): the run's table is the deployment's
* ({@link RunnerDeps.writeProtectedPaths}) UNION these; `[]` ≡ absent; no task-layer value removes a deployment row. A
* covered write is the judge's mandated ask (`write_protection`), as for a deployment row. Bad shapes refuse at prepare
* (`config.invalid_argument`); rides every engine-built child spec add-only.
*/
writeProtectedAdditions?: readonly import("./write-protect.js").WriteProtectedEntry[];
/**
* TASK-layer denies ADDED to the compliance posture ({@link readDenyPatterns}' law): the run's deny set is the deployment's
* ({@link RunnerDeps.compliancePostureResolver}) UNION these; `[]` ≡ absent; nothing here removes a deployment deny. A hit
* is refused exactly as a deployment deny is. Members outside the closed capability set refuse at prepare
* (`config.invalid_argument`) — never a silently dropped deny; rides every engine-built child spec add-only.
*/
complianceAdditionalDenies?: readonly import("./compliance.js").ComplianceCapability[];
/**
* design/199 件A — the TASK layer's read-face containment declaration
* ({@link import("../tools/fs/containment-face.js").ContainmentFace}). "roots" pins this task (and, via the
* tighten-only clamp, its whole delegation subtree) to the historical containment; "open" lifts
* the roots judgment for the structured read faces — refused loudly under org governance (the
* task layer only tightens there; the deployment opens via {@link RunnerDeps.readFace}) and
* beside `handsReadOnly: true` (the verifier containment is load-bearing). Absent ⇒ the
* deployment seat, then the engine default ("roots" — D-1b: an upgrade never opens implicitly).
* Bad values refuse loudly (#123). Never affects the write faces — that is {@link writeFace}, a
* separate seat with its own default.
*/
readFace?: import("../tools/fs/containment-face.js").ContainmentFace;
/**
* The TASK layer's WRITE-face containment declaration
* ({@link import("../tools/fs/containment-face.js").ContainmentFace}) — the read seat's twin,
* resolved through the SAME order (`resolveContainmentFace`, one body, one set of rows). "roots"
* pins this task (and, via the tighten-only clamp, its whole delegation subtree) to the historical
* write fence; "open" lifts the roots judgment for the structured WRITE faces (`edit_file` /
* `write_file` / `notebook_edit`) — refused loudly under org governance (the task layer only
* tightens there; the deployment opens via {@link RunnerDeps.writeFace}) and beside
* `handsReadOnly: true` (that mount carries no write faces at all). Absent ⇒ the deployment seat,
* then the engine default ("roots" — an upgrade never opens implicitly). Bad values refuse loudly.
*
* What "open" does NOT change: the write-side sensitive-path policy, the blocked-device gate, the
* UNC out-of-set refusal, the read-before-edit requirement and the deployment's approval policy. Seat
* independence is owned by `fs.containment_face.seat_independence` ({@link import("../tools/fs/containment-face.js").resolveContainmentFace}).
*/
writeFace?: import("../tools/fs/containment-face.js").ContainmentFace;
/**
* design/80 D-B — opt in to PLAN MODE: mount the first-party `present_plan` tool (CC `ExitPlanMode` parity).
* The model calls it to present a plan; the engine then pauses with a durable `plan_review` checkpoint
* (`status:"needs_review"`) so a human approves/edits/rejects before any action. Typically paired with
* `handsReadOnly:true` (research read-only until the plan is approved, then the resume drops it). Default
* false — when unset the tool is NOT mounted, NOT disclosed, and a `ToolReturn.requestReview` is ignored, so
* a non-plan-mode task is byte-identical. Needs a `checkpointStore` wired to actually pause (else a
* `requestReview` is a no-op). The tool is a thin composition over the general `requestReview` primitive.
*/
enablePlanMode?: boolean;
/**
* Interactive-tool mounting override (CC 2.1.207/209 parity: AskUserQuestion / ExitPlanMode /
* EnterPlanMode share one `isEnabled` gate — in non-interactive mode without an SDK permission-prompt
* tool the tool is NOT offered at all, so the model never asks into the void). sema's automatic
* criterion (this knob unset): a human-facing tool mounts only when the deployment has SOME face that
* can deliver it to a person —
* · `AskUserQuestion`: a live `onQuestion` (spec ?? deps), OR a durable park consumer = a wired
* `checkpointStore` AND (`durableApproval` opted in OR a `forceDurableGate` entitlement) — the
* approval-center posture (≅ CC's SDK permission-prompt-tool exception).
* · `present_plan` (within `enablePlanMode`): a wired `checkpointStore` — the `plan_review` park's
* only consumer (without one the pause is a silent no-op the model mistakes for a delivered plan).
* Neither face ⇒ not mounted, not disclosed (a bare headless run must not park on a question nobody
* will answer and then be projected as an empty success). Override: `true` ⇒ always mount (the
* deployment consumes asks some other way; `enter_plan_mode` still requires a `checkpointStore` —
* without one its read-only latch would be inescapable); `false` ⇒ never mount (hard headless);
* unset ⇒ the automatic criterion. `enablePlanMode` stays the plan family's opt-in — this knob never
* mounts plan tools on its own.
*/
interactiveTools?: boolean;
/**
* design/110 / design/136 §2.1.a (⚠️ BREAKING semantics change) — the per-task FORK governance gate for
* `Agent(subagent_type:"fork")` (the standalone `Fork` tool was retired; the capability lives in the Agent
* tool's `subagent_type` value domain — CC-exact shape). The fork spawns a child that INHERITS this
* conversation's full context (vs the clean-context delegation) and shares the prompt-cache prefix.
*
* OPT-OUT default (flipped from the pre-136 opt-IN): `undefined`/`true` = fork is AVAILABLE, bounded by the
* session capability (`hasSessionFork` durable store + a session id + not inside a fork — honest refusal
* otherwise); an EXPLICIT `false` = fork is DENIED for this task with the honest `fork.disabled` result
* (never a silent downgrade). Multi-tenant deployments that previously relied on `undefined` being OFF must
* now send `enableFork: false` explicitly (or deny per-principal via `RuntimeCaps.allowFork`).
*/
enableFork?: boolean;
/**
* The OPERATOR's shell floor: `"always"` ⇒ every `bash` / `Monitor` command is a confirmed ask (an
* `operator_forced` ask — kept under `bypassPermissions`, refused by `dontAsk` like every ask; a standing allow rule never
* clears it). Absent ⇒ no floor: the shape tier the engine installs on every full shell (a provably read-only
* single command runs, everything else asks) is arbitrated by the task's {@link permissionMode}. The retired
* values `"off"` / `"classify"` are refused at the door (`config.shell_gate_value_retired`): the tier is no longer a
* doctrine a caller chooses, and the read boundary is judged on every shell call under every mode. A positive
* per-tool mark keeps its seat (an explicit `irreversibility:"always"` is never downgraded). No effect under
* `handsReadOnly` or with no execution env. Re-supplied on resume like `toolPolicy` (a checkpoint carries the
* chain's floor). Delegation: the floor travels the inherited-gate lane; a child cannot lift it.
*/
shellGate?: ShellGateDoctrine;
/**
* The task's PERMISSION MODE — the ONE key that says how the engine answers a surviving ask (six words,
* {@link import("./ask-origin.js").PERMISSION_MODES}; spelled as the wire spells it). ROOT task absent ⇒
* `"default"`; an engine-spawned CHILD absent ⇒ its parent's effective mode (a child's own declaration is
* adopted only where the reference implementation's rank table admits it under the parent's — a folded
* declaration is disclosed on `wiring_manifest.permissionMode.foldedBy: "inherited_ceiling"` and announced,
* never silent). A value outside the set is refused at the door (`config.permission_mode_invalid`).
*
* What the mode DOES is one table and one predicate ({@link import("./ask-origin.js").MODE_ARBITRATION},
* {@link import("./ask-origin.js").arbitrateByMode}) read after every rule, tighten and classifier station:
* `bypassPermissions` releases every ask but the immune classes (an explicit ask rule, a hook's ask, the
* question tool, an unverifiable recursive delete, an org rule, an operator's forced confirmation, unreadable
* governance, the classifier's denial-limit fallback) and discloses each release; `dontAsk` refuses every ask
* but a hook's own (no rule or safety check also asked); `auto` arms the classifier (`RunnerDeps.autoMode` present and
* `RuntimeCaps.autoMode` not `false`, else folded to `default` and announced); `plan` reads only and mounts the
* plan pair; `acceptEdits` writes inside the root without asking; `default` asks. The mode also installs its own
* seats ({@link import("./mode-policy.js").modePolicyOf}).
*
* `bypassPermissions` is a REQUEST: it takes effect only where the deployment does not deny it
* ({@link RuntimeCaps.bypassPermissions} not `false`); a denied request runs in `default`, announced
* (`config.permission_mode_unavailable`). A running leg changes mode through `TaskStream.setPermissionMode`.
* Persists on the checkpoint's data half (`CheckpointState.inheritedGate.permissionMode`), re-judged on every leg.
*/
permissionMode?: PermissionMode;
/** Task-scoped MCP servers, materialized into tools then disposed. */
mcp?: McpServerSpec[];
/** Task-scoped A2A peers (remote agents), whose advertised skills mount as `a2a__<peer>__<skill>`
* tools for this task. Every mounted skill is an external write by default — see {@link A2aServerSpec}. */
a2a?: A2aServerSpec[];
/**
* Task-scoped skills (object form). Two first-party halves (design/71 黑盒#5 fix; [c209-C] R2 C8
* delivery reality): a `<skills>` disclosure block listing name+description (deterministic order,
* capped) delivered as a `<system-reminder>` on the FIRST user turn — plus boundary drift deltas —
* NOT part of the stable system prompt (design/C moved it off the cached prefix), and an
* injected `skill` tool (effect:read) that returns the skill's FULL content as a tool result —
* no invoke-time truncation (CC parity; the only doors are a 1MB per-skill load gate at spec
* assembly, which REJECTS an oversized skill whole with a config-phase error, and the
* compaction-time invoked-skill retention caps). Duplicate names: first wins. The name
* `skill` becomes reserved when this field is non-empty (`config.reserved_tool_name`).
*
* Skill content is CALLER-trusted — same trust level as `systemPrompt`. Callers forwarding
* end-user-authored skills are responsible for vetting them: a skill IS instructions by design.
*/
skills?: SkillSpec[];
/**
* The skills THIS run's human input invoked by name (a shell that parsed `/name` from the person's line), by
* {@link SkillSpec.name}. Each one's {@link SkillSpec.allowedTools} / {@link SkillSpec.disallowedTools} take effect
* at the start of the run with no `Skill` call and no card — the person's own invocation is the statement — and last
* for this run (a steer does not end them; the next runTask starts without them), like a grant the model's activation wrote. Delivering the skill's body
* stays the caller's business (this key carries names only). Every name must be one of this run's {@link skills};
* anything else refuses the run before it starts (`config.user_invoked_skill_unknown`), never dropped. Present only
* when the person actually invoked one; a mid-turn invocation (one riding a steer) grants nothing.
*/
userInvokedSkills?: string[];
/**
* This run's ALLOW RULES — the reference implementation's `allowedTools` (its `--allowedTools` list): each entry, in the
* rule grammar a skill's `allowed-tools` reads ({@link SkillSpec.allowedTools}), is a standing yes for this run. A
* whole tool name (`Write`, `Bash`), a protocol namespace (`mcp__srv`, `mcp__srv__*`), a shell command rule
* (`Bash(git:*)`, `Bash(npm test)`) or a Read directory (`Read(//srv/data/**)`) clears the question the permission
* layers raise for a call it covers — read in the allow layer beside a person's stored rules, a skill's list and the
* deployment's {@link import("./runner-deps.js").RunnerDeps.allowedTools}, under the same predicate: deny and ask rules
* still win, and a question no allow rule clears (a mandated, governance, hook or plan question) is still put to a person,
* with the entry disclosed on the card. In `auto` mode an entry that hands over arbitrary code (the whole shell, an
* interpreter or launcher prefix) is left to the classifier.
*
* Lives for this run — the steers it takes in included; a delegated child holds the entries until it ends; a resumed leg
* holds them only when its spec supplies them again (they are not written into a checkpoint). An entry this grammar
* reads as nothing (`*`, `mcp__*`, a write-path or domain pattern) grants nothing and is announced at the start of the
* run (`permission.allowed_tool_not_granted`); a list holding `*` grants nothing at all. A value that is not a list of
* strings refuses the run before it starts (`config.invalid_permission_rules`, with its issues).
*/
allowedTools?: readonly string[];
/**
* Fraction of the model's context window the `<skills>` listing may spend. Absent ⇒ `0.01` (1%).
* Domain `(0, 1]`; anything else — including `0`, a string, `NaN` — REFUSES at the door with
* `config.skill_listing_budget_invalid` rather than folding back to the default.
*
* The budget is `(contextTokens ?? contextWindow ?? 200 000) × 4 × fraction`, in UTF-8 bytes, and the
* listing degrades inside it: every skill stays listed, the lower-priority ones (declaration order)
* lose their descriptions first. Raising this is opting in to a higher per-turn context cost.
*
* Enforced as declared (`@contract skills.listing_budget_enforced_as_declared`): the listing frames are
* intact-class turn attachments the delivery lane never clips, so the resolved budget has no ceiling
* above it — a fraction that asks for more gets more (a 1 000 000-token window at the default 0.01 is
* 40 000 bytes). The same fraction is EACH listing attachment's budget: the agent-type roster frame
* folds into `window × 4 × fraction` too (every type stays listed, the last-declared lose their detail
* first; `agents.listing_truncated` reports it).
*
* ⚠️ A model declaring a window SMALLER than 200 000 tokens now gets a proportionally smaller listing
* budget than the flat 8 000 bytes that preceded this seat.
*/
skillListingBudgetFraction?: number;
/**
* Per-skill description cap inside the `<skills>` listing, in code units, ellipsis included. Absent ⇒
* `1536`. Must be a positive integer; anything else REFUSES with
* `config.skill_listing_budget_invalid`. A description longer than the cap is truncated to exactly
* this many units; lowering it is how a deployment fits more skills' descriptions inside one budget.
*/
skillListingMaxDescChars?: number;
/**
* design/129 — lifetime scope for BACKGROUND children (Agent/Fork `run_in_background`). Default `"task"`:
* the parent runTask's terminal reaps them (design/115 — no orphans burning tokens; turn-end killed
* frame). `"session"`: children OUTLIVE the turn (CC Backgrounded semantics) — registered under the
* SESSION owner (cross-turn TaskOutput poll works), skipped by the parent-teardown reap, force-capped by
* a default child timeout when none is given, and finally reapable via
* `TaskRegistry.reapSessionBackground(sessionId, scope)` (call it from your session release/sweep).
* CALLER-trusted (systemPrompt tier): never a model-facing parameter, never on the governed workflow
* whitelist — a child's lifetime crossing the turn boundary is an isolation-contract surface.
*/
backgroundScope?: "task" | "session";
/**
* A1 — structured SANDBOX facts for the `# Environment` block, so the agent knows what
* toolchains the environment already carries (no blind reinstall), which package source installs go
* through, and whether the network is reachable at all (egress=none ⇒ don't burn budget on downloads).
* DEPLOYMENT-trusted (systemPrompt tier — the deployment knows its own sandbox binding): rendered
* sanitized+bounded by prepare-task; never a model-facing parameter, never on the governed workflow
* whitelist. All fields optional; absent ⇒ block unchanged.
*/
envFacts?: {
/** The bound sandbox image profile (e.g. `dev-mobile`). */
profile?: string;
/** Toolchains/capabilities the image carries (e.g. `android-sdk`, `gradle`). Rendered bounded. */
capabilities?: readonly string[];
/** Which package-source lane installs go through (e.g. `cn` | `global` | `custom`). */
pkgSource?: string;
/** Outbound-network posture. `none` renders an explicit "downloads will fail" caveat. */
egress?: "none" | "allowlist" | "full";
/** Session-specific scratch directory for temporary files (CC Scratchpad parity, 1.257.3): when set,
* the environment block instructs the model to use this dir instead of `/tmp` — prevents parallel
* tasks trampling each other's temp files and project-dir pollution. Unset ⇒ no section rendered. */
scratchpadDir?: string;
/**
* B-half — PER-LANE resume-continuity facts, rendered ONLY on a durable-resume leg so
* the model reasons honestly about what survived the suspension. DEPLOYMENT-supplied per its own lane
* capability (a VM snapshot lane preserves processes; an ssh park-only workspace usually keeps /tmp;
* a rebuilt container loses both) — core hardcodes NO lane table and renders NOTHING when a field is
* absent (an unstated fact must not become a confident sentence; a blanket "lost" wording would
* misinform the model on lanes where the state genuinely persists). All fields optional.
*/
resumeFacts?: {
/** Whether processes started before the suspension are still running after resume. */
processes?: "preserved" | "lost";
/** Whether out-of-workspace temp state (e.g. `/tmp`) survived the suspension. */
scratch?: "preserved" | "lost";
/** Free-form lane-specific caveat (sanitized + bounded at render). */
note?: string;
};
};
/**
* design/164 — the SINGLE task-limit surface. Every budget axis lives here (the former top-level
* `maxTokens` / `maxCostUsd` / `budgetStreamCancel` / `degrade` were folded in), and **every axis is
* DEFAULT-EMPTY**: a task with no `limits` runs with no engine-imposed turn / token / cost / wall-clock
* ceiling at all.
*
* Consequence, stated plainly (ruled 2026-08-04): a zero-config runaway has NO task-level backstop.
* What still binds is the ENGINEERING GUARDRAIL family — {@link TaskLimits.brainCallGuardrailMs} (a
* brain call that never settles), the stall watchdogs, and the deployment's own usage governance. Those
* answer "did the system hang", never "how much may this task spend"; the second question is the
* caller's to answer, by setting a limit here.
*/
limits?: TaskLimits;
/**
* Config-catalog override declarations (ruled 2026-07-19, anti-chaos contract): a HOST that
* overrides a recommended knob at its own layer (env cap, adapter flag, harness default) declares
* it here — key + value + REASON — so the override rides the `config.assembled` trace with
* provenance `host-declared` instead of being invisible to forensics. Advisory observability
* metadata only: declarations never change an effective value (set the real field for that).
* See `describeConfigCatalog()` for the recommended scale.
*/
configOverrides?: ReadonlyArray<import("../config/catalog.js").ConfigOverrideDeclaration>;
/**
* Subagent viewing pane (dogfood finding 2026-07-03): widen the opt-in display sink
* (`RunInternals.onForwardEvent`) from `task_progress`-only to a SUBAGENT's live CONTENT events
* (`text_delta` / `text_end` (#447) / `reasoning_delta` / `tool_start` / `tool_end`), so a UI can render a delegated
* child's transcript live. Default OFF (progress-only, prior behavior). The child stream is still
* NEVER merged into the parent's model context — this is purely a render channel; forwarded events
* carry `parentToolCallId` (attribution) and the same UNTRUSTED-RAW contract as the main stream's
* `tool_start.args`/`tool_end.output` (consumer MUST redact — design/99 §E1). Single-level: each
* delegation level opts in for ITS direct children.
*/
forwardSubagentEvents?: boolean;
/**
* design/122 D1 — RETAIN sync sub-agent sessions after they settle so an operator can RESUME (revive) a
* finished child with a new prompt via {@link import("../agents/subagent.js").SubagentSteerHandle}'s
* `resume` verb (CC `dfe` resumeAgentBackground parity). Default OFF = prior behavior byte-for-byte:
* child sessions are throwaway (released eagerly after the delegation returns).
*
* ON (`true` or a config object): each SYNC delegation spawned while the deployment's
* `RunInternals.onSubagentSpawn` sink is wired (the handle-emitting path — without a handle there is no
* resume capability, so retaining would be a pure leak) pre-mints the child's sessionId, PINS it against
* the store's idle sweep, and SKIPS the eager release. The session is retained in a parent-run-scoped
* ledger until `ttlMs` after settle (default 30 min), eviction past `max` retained children (default 16),
* or the PARENT run reaching a terminal state — whichever comes first; then it is unpinned + explicitly
* released (throwaway semantics restored — a retained session never dangles to the store's 7-day TTL).
* `run_in_background` children are OUT OF SCOPE (no handle, no retain — r1-M3; the bg lane's resume needs
* durable support and is a separate item). Resume is reachable only while the parent run lives (the
* ledger is run-scoped, not a durable registry).
*/
retainSubagentSessions?: boolean | {
ttlMs?: number;
max?: number;
};
/**
* Keep BACKGROUND PROCESSES this task started (background bash shells / monitored processes) ALIVE
* after the task reaches a terminal state, instead of the default run-teardown reap. Default `false`
* = current behavior byte-for-byte: the teardown settles this run's shell-lane children and disposes
* the env's background shells.
*
* `true` = the "deliver a living service" contract (the task's deliverable is a running daemon that
* must survive for a later probe/consumer): the run teardown neither kills nor disposes this task's
* background processes. Semantics are BEST-EFFORT "do not actively kill":
* - **OR-composed with the env-level opt-in** (e.g. `NodeExecutionEnv({ retainBackgroundProcesses })`):
* either being `true` retains. The env-level lane is the stronger form — it also spools child stdio
* to files at SPAWN time, so retained processes survive the host process exiting; a spec-level
* retain keeps processes alive past the task, but their stdio may still be tied to host pipes.
* - A per-task ISOLATED owned env (container/VM) still destroys its processes with the env teardown —
* retention there would be illusory, so the teardown keeps its honest killed receipts instead.
* - win32: best-effort "no active kill" only; without detached process groups, process-tree survival
* past the host is a platform boundary (some children may still exit with it).
* - Automatic reaps leave a retained process alone: the run-teardown sweep (settle + dispose), the
* session-release reap, and the exit-boundary hard face under `includeRetained: false`. What still ends
* it: the TaskStop tool, its per-shell background timeout (while the host runs), env destruction, and the
* hard face under `includeRetained: true`. The row carries the lane it was launched under (a later run's
* declaration does not re-label a session-resident row) — see docs/background-process-lifetime.md.
* - NOT inherited by delegated child tasks: each child's own spec decides for the child's own
* background processes.
*/
retainBackgroundProcesses?: boolean;
/**
* design/120 P2: opt in to in-stream tool execution — safe tool calls start while the model is
* still streaming its response instead of waiting for the full reply. Default OFF. Forwarded to
* the harness, which additionally withholds it whenever a `tool_call` gate handler is registered
* (policy / approval / egress runs — a gate could durably suspend, which is impossible
* mid-stream), so setting this on a gated task is safe and simply has no effect. A run whose only
* gate concern is resource-suspend does NOT register that handler (it stops at clean turn
* boundaries), so in-stream execution stays available there.
*
* Known limitation (documented): with this ON, a tool's side effect can complete BEFORE the
* assistant message persists; a hard process crash in that window leaves no transcript trace of
* the call (wake reconcile cannot see an orphan that was never written). Prefer OFF for
* deployments where crash-exactly-once accounting of tool effects matters more than latency.
*/
streamingToolExecution?: boolean;
/**
* **Structured output** (1.41): constrain the task's FINAL result to this JSON schema (a typebox
* object schema). Injects a built-in `submit_output` tool whose parameters ARE this schema; the model
* calls it to finish, the validated object is surfaced as `TaskResult.structuredOutput`, and the task
* ends. Provider-agnostic (it's just a tool). If the model can't produce a valid object within a few
* tries the task fails with `errorCode="output.invalid"`. A caller tool named `submit_output` is
* rejected (reserved). The model may still answer in prose (then `structuredOutput` is undefined) or
* `report_blocked`. Cast the result with `Static<typeof yourSchema>`.
*/
outputSchema?: TSchema;
/**
* How many times the model may **retry** a rejected `submit_output` before the task fails with
* `errorCode="output.invalid"` (only meaningful when {@link outputSchema} is set). `0` = fail on the
* first invalid submit (no in-loop retry); the caller decides whether to re-run. Default `2`
* (3 total attempts). Clamped to a non-negative integer. This bounds only the *in-task* retry of a
* malformed object — it is not an auto-fixer; pair it with a caller-side retry on `output.invalid`.
*/
outputRetries?: number;
/** Best-effort fire-and-forget trace sink (task/turn/brain/tool). Overrides `RunnerDeps.tracer`. */
tracer?: import("./trace.js").TracerHook;
/** Per-task override of the large-tool-result offload threshold (chars); see `RunnerDeps.toolResultThresholdChars`. */
toolResultThresholdChars?: number;
/**
* Override auto-compaction thresholds for this task's session. Compaction (LLM summary, persisted
* to the session) runs in two places sharing these settings: at every within-task turn boundary
* (design/64 §25 (A) — the long-single-task case) and once at task end (the between-task case).
* Defaults: enabled, reserveTokens 16384, keepRecentTokens 0 (design/169-C; 20000 was the
* pre-169 default). A zero budget is NOT "no verbatim tail": the cut-point search has a
* structural floor under the budget, so the default already leaves the last exchange verbatim
* after the summary (a trailing tool-result batch keeps its emitting assistant with it) — the
* same one-group posture CC's reactive compaction ships. Set `keepRecentTokens` explicitly only
* to keep a WIDER tail than that. Repaired
* automatically when
* pathological for the model's window: threshold ≤ 0, keepRecent ≥ threshold, or a threshold above
* the clearStale point (0.7×window — the prefix-cache "death band", design/64 §26.4) is clamped
* down to it. The clamp applies to explicit values too (the clearStale point is not configurable,
* so a threshold above it cannot express consistent intent); explicit values below all three
* bounds are never overridden (see `sanitizeCompactionSettings`).
* `instructions` is appended to the summarization prompt — use it to guarantee critical
* facts survive (e.g. "preserve verbatim any codes, IDs, names, and explicit user instructions").
* design/145 §4 hygiene (every instructions channel, incl. a `TaskStream.compact({instructions})`
* per-call override and a preCompact hook's additions): break-out tags (`</system-reminder>`) and
* fence sentinels are defused, and the text is capped at 2048 code points; clean text under the
* cap passes through byte-identical.
* Scope caveat (design/141 examples 批实测): `instructions` rides the WHOLE-TURN summarization
* request only; a split-turn cut point (mid-turn prefix summarization) uses the engine-owned
* turn-prefix prompt and does not carry it (same A2 boundary as `RunnerDeps.summaryProvider`).
* `withinTask: false` keeps end-of-task compaction but disables the ROUTINE within-task
* (turn-boundary) trigger — the escape hatch for the §25 (A) control-flow change; the system
* prompt then stops claiming ROUTINE mid-task summarization (§6.3 honesty). It does NOT silence
* the recovery-class forced lanes (design/374 slice 3): a prompt-too-long recovery or the guard
* chain's arm B may still legitimately compact WITHIN the task — error/pressure recovery is not
* a routine boundary pass, so a within-task compaction under this flag is contract-conforming.
* `attachWorkingFiles` (LONGRUN-2; **default ON since 2026-07-03** — CC 198 hard-codes its
* post-compact file restore, and LONGRUN-2 measured ≈2.3 extra read round-trips per compaction
* without it): after each compaction, re-read the task's most recently READ files (CC
* readFileState semantics; falls back to modified-by-recency when read tracking is absent) and
* append their current contents to the summary (bounded: `maxFiles` default 3,
* `maxCharsPerFile` default 16k chars ≈ 4k tokens, plus a total cap that self-scales to ~15% of
* the model window). The `compacted` wire event carries the attached list as `attachedFiles`
* (shell renders CC's post-compact Read cards). `false` opts out; an object customizes caps;
* no-op when the task has no execution env (no hands, nothing to re-read).
*/
compaction?: {
enabled?: boolean;
withinTask?: boolean;
reserveTokens?: number;
keepRecentTokens?: number;
instructions?: string;
attachWorkingFiles?: boolean | {
maxFiles?: number;
maxCharsPerFile?: number;
};
/**
* design/169-F (CC 220 :386605-386659/:597965 lightweight-clear posture) — OPT-IN stale
* tool-result offload, a projection-layer clear below full compaction: in the REQUEST sent to
* the model, same-tool results older than the most recent `keepRecentPerTool` are replaced by a
* short pointer ("offloaded … saved to persisted output ref …", readable back via the
* `read_tool_result` tool) once the replacement saves ≥ `minSavingsChars` chars. The on-disk /
* session transcript is NEVER rewritten (append-only + projection is this repo's constitution);
* only the outgoing request projection changes. Replacement decisions are BYTE-STABLE across
* turns by construction (a result is replaced exactly when ≥ keepRecentPerTool newer same-tool
* results exist — monotone, so a decision never reverses — and the pointer text is a pure
* function of the result), so provider prompt-cache prefixes are never churned by re-decisions.
* `undefined` (default) = completely off, requests byte-identical to pre-169 behavior. Requires
* the tool-result offload store (on by default; a deployment that disabled offloading via
* `toolResultThresholdChars: 0` gets one onError config warning and the knob stays inert).
* Field defaults when the object is present: keepRecentPerTool 3, minSavingsChars 2000.
* Values must be non-negative integers — anything else (negative, fractional, non-finite) fails
* loudly at prepare (`code: "config.stale_tool_result_offload_invalid"`, no silent re-default).
*/
staleToolResultOffload?: StaleToolResultOffloadOptions;
/** design/145 §1 — window-safety tolerance for an independent `compactionModel`: when the
* summary input would be clamp-truncated by MORE than this ratio to fit the compaction
* model's window, the call falls back to the main model (subject to the budget/walltime
* gates). 0 = any truncation triggers the fallback candidacy; 1 = never fall back (the
* pre-145 clamp-only escape hatch). Default 0.10 — the honest read: head+tail clamping keeps
* ≤10% mid-section loss BOUNDED AND DISCLOSED (`compaction.clamp_disclosure`), not lossless. */
clampTolerance?: number;
};
/**
* design/133 §R3 — turn-boundary attachment producers (CC `getContextAttachments` parity), OPT-IN
* with one carve-out: [c209-C] the LISTING FAMILY (`agentListing`/`skillsListing`) is DEFAULT-ON —
* the agent roster and skills listing were evicted from the tool descriptions / system prompt
* (cache-prefix immunity, design/C), so the reminder frame is their ONLY model-facing carrier and
* an opt-in default would leave the static pointer sentences ("Available agent types are listed in
* <system-reminder> messages…") pointing at reminders that never arrive. Explicit `false` still
* opts out (config warning when the corresponding face is mounted). Every OTHER member keeps the
* design/132 default-face judgment (B2/H2): the evidence for those reminders comes from UNATTENDED
* long-task legs (weak-model drift safety net — CC's own trigger counts proved unreproducible: the
* observed CC runs never mounted TodoWrite), so long-run deployments turn them on and
* interactive deployments stay byte-identical until a behavior leg proves them neutral. At most ONE
* coalesced `<system-reminder>` steer per clean turn boundary, lowest lane: finalize/deadline-nudge/
* diagnostics at the same boundary — and the design/130 P2 / design/132 end-game latches — all
* suppress it. The listing family ALSO delivers its initial full frames on the FIRST user turn
* (prompt-adjacent, CC 209 classic wire shape) — that first-frame delivery is part of the first
* message, not a boundary steer, so none of the boundary gates apply to it.
*
* - `todoReminder`: CC 10/10 cadence — when TodoWrite (or the task tools; both mounted ⇒ the
* task family wins, CC isTodoV2) hasn't been used for 10 assistant turns, re-surface the list
* (thin projection; an all-completed list is cleared, never regurgitated). No tools mounted ⇒
* permanently silent.
* - `changedFiles`: boundary-time stat of ≤20 (or `maxFiles`) most-recently-READ files; a file
* whose mtime moved past the recorded read time (+2s epsilon) is listed (path-only v1, "do not
* blindly revert — re-read"). OFF ⇒ zero stat calls. Self-edits are immune (write-back
* refreshes the read stamp); mtime skew on exotic fs remains a documented false-positive source.
* - `backgroundTasks` (G1 通告层 — CC /compact parity): after a compaction LANDS, the
* next boundary announces this run's still pending/running background tasks (task registry
* snapshot: id/description/status, CC three-state copy). ONE-SHOT per compaction — never
* per-boundary nagging; no compaction ⇒ permanently silent.
* - `toolsDelta` (G1 — CC deferred-tools boundary notice): when design/36 `tool_search`
* materializes deferred tools mid-task (setTools + fingerprint refresh), the next boundary
* announces the NEWLY available names once. Already-active and resume-reseeded tools are never
* (re)announced; no deferred tools ⇒ permanently silent.
* - `agentListing` (G1 续批 — CC `agent_listing_delta` parity; [c209-C] DEFAULT-ON, see above):
* when a delegation tool built by `createSubagentTool` is mounted, the FIRST user turn carries
* the full agent-type roster ("Available agent types for the Agent tool:", CC-verbatim; + the
* concurrency note when more than one type; + the Q4 "Models available for the 'model'
* parameter:" tail when a catalog is mounted). Boundaries announce only roster drift
* (adds/removals — static in practice; the delta lane serves dynamic-registration deployments
* and cross-leg drift). A durable resume leg is seeded as already-announced from the
* checkpoint's `announcedListings` name-set (drift since suspend IS delta-announced), falling
* back to a transcript probe for older checkpoints — never replayed when already delivered.
* - `skillsListing` ([c209-C], DEFAULT-ON, mechanism-family sibling of `agentListing` — no CC
* delta anchor, registered in cc-divergences): the FIRST user turn carries the full `<skills>`
* disclosure block (the exact render that used to sit in the stable system prompt — folded
* byte-exact into the listing budget, `skillListingBudgetFraction` × the window, with
* name-only overflow); boundaries announce only skill-set drift. Resume seeding mirrors
* `agentListing`.
* - `mcpInstructions` (G1 续批 — CC `mcp_instructions_delta` parity): the INITIAL server
* instructions stay in the stable system prompt (`# MCP Server Instructions`, design/64 §17.3 —
* deliberate CC deviation: CC injects them as reminders, sema's stable-prompt carrier is
* prefix-cache-correct). This member announces only DELTAS: a mid-task disconnect of an
* instruction-bearing server ("The following MCP servers have disconnected…", CC-verbatim), and
* the add lane for a future dynamic-connect face. Added instruction text is UNTRUSTED server
* text → fenced with `delimitUntrusted` (deliberate CC deviation: CC injects it raw).
*
* The read-only planning posture is not a member: the engine states it whenever the leg's mode is `plan`
* (with its restatement cadence and its exit frame), as it states the bypass and auto postures. A spec still
* carrying the retired `planModeReminder` key is refused at prepare (`config.spec_retired_key`).
*
* Telemetry: `steering_injected` (source `todo_reminder`/`task_reminder`/`changed_files`/
* `output_style`/`background_tasks`/`tools_delta`/`agent_listing`/`skills_listing`/
* `mcp_instructions`) + `stats.mechanisms.attachmentsInjected` (first-frame listing deliveries
* emit the same echo frames and count too — source-accurate observability for the shell).
*/
attachments?: {
todoReminder?: true;
/**
* task #51 口径③ — CC `r2o()` kill-switch parity (CC 198 pretty.js:479167-479171; both reminder
* producers consume it as `if (r2o() === "off") return []`, :480232/:480268). `"off"` silences the
* todo/task reminder lane even when `todoReminder` is wired on (the other attachment producers are
* untouched — CC's nudge gate is lane-scoped); any other value (or absence) = CC `"baseline"`.
* CC resolves this from env `CLAUDE_CODE_TODO_REMINDER_MODE` / the `tengu_soft_slate_nudge` gate;
* sema keeps env resolution at the deployment shell — core reads no env.
*/
todoReminderMode?: "baseline" | "off";
/**
* `tool_search_usage_reminder` (CC 2.1.220 producer `Nop` @608915, renderer @626269): the
* REVERSE nudge for delayed tool disclosure. When tools are deferred (design/36), a model that has
* gone N turns without calling `ToolSearch` and still has unloaded schemas is told they exist and
* how to load them, so "this capability does not exist, I will build a workaround" stops being the
* silent failure mode of the mechanism.
*
* Activation (all four, CC-exact): opted in here; the task actually HAS deferred tools; at least one
* is still undiscovered; and BOTH windows are open — ≥15 cadence units since the last `ToolSearch`
* call AND since the last such reminder (`TOOL_SEARCH_REMINDER_CONFIG`, CC's shipped 15/10 defaults).
* MUTUALLY EXCLUSIVE with the todo/task reminder in the same boundary (CC `task_reminder_same_turn`):
* at most one work-hygiene nudge per boundary. Opt-in, default OFF like every other 133 member —
* CC's own mount is likewise config-gated (`cXi()` null ⇒ the lane is never registered).
*/
toolSearchReminder?: true;
changedFiles?: true | {
maxFiles?: number;
};
/**
* SR-3 (CC 198 `budget_usd` lane, producer QFm pretty.js:480424-480428 + renderer :600225): the
* progressive $-budget readout `USD budget: $used/$total; $remaining remaining` (CC-verbatim
* template). CC-exact activation: live ONLY while the task carries an effective cost ceiling
* (`maxCostUsd`, or a resource-slice ledger remaining — CC `options.maxBudgetUsd`); no ceiling ⇒
* permanently silent even when opted in. CC-exact cadence: NO threshold ladder and NO throttle —
* CC mounts it on every attachment cycle whose task carries a ceiling (RB-311: CC 220 @607474 puts
* it in the SHARED producer group, so a SUBAGENT with its own ceiling sees it too — the `!agentId`
* flag gates the main-agent-exclusive group next to it, not this lane), so
* sema emits one line at every collected clean turn boundary; the "progression" is the numbers
* advancing with spend. Recorded deviation (continuation gate): CC attaches at request assembly
* and can never EXTEND a run, while a sema boundary steer revives an idle harness — so this lane
* only rides boundaries whose turn resolved ≥1 tool call (a next request is already guaranteed);
* a text-only final turn is never dragged past its natural end. Numbers ride the enforcement
* coordinate (`stats.costMicroUsd` vs the effective ceiling, ÷1e6 → USD) so the readout never
* disagrees with `limits.max_cost_exceeded`.
* NOTE: this deliberately relaxes the design/74 "no budget language reaches the model" default —
* that judgment shaped the DEFAULT-OFF posture; an opted-in deployment chooses CC parity.
*/
budgetUsd?: true;
/**
* `total_tokens_reminder` (CC 2.1.245 producer `MPs`, renderer `Oje`):
* the TOKEN twin of {@link budgetUsd}. One line per collected boundary,
* `<total_tokens>N tokens left</total_tokens>`, CC-verbatim bytes.
*
* Activation, CC-exact in shape and BYOM-honest in substance: opted in here AND — for the two
* COUNTDOWN arms — the task carries a token ceiling (`limits.maxTokens`, or a resource-slice
* allocation's remainder). No ceiling ⇒ permanently silent, because core does not know and will not
* guess your model's context window: the number it publishes is the one it already ENFORCES
* (`stats.tokens` against that ceiling), so the readout can never disagree with an eventual
* `limits.max_tokens_exceeded`. The two CONSTANT arms (`infinite` / `fixed`) read no measurement at
* all and need no ceiling.
*
* Cadence, CC-exact: no threshold ladder and no throttle — every collected boundary carries it, and
* the "progression" is the numbers advancing with spend. Recorded deviation (shared with
* {@link budgetUsd}, and for the same reason): the lane only rides boundaries whose turn resolved
* ≥1 tool call, so a boundary steer never EXTENDS a run that reached its natural end.
*
* DEFAULT ON since design/374 slice 4 (ruled 2026-08-28) — boolean, not `true`: explicit
* `false` is the opt-out, the {@link backgroundTasks} / listing-family contract rather than the
* `true`-only 133 form. BREAKING for a deployment that declares a token ceiling: pre-flip it saw
* no `<total_tokens>` frame without opting in, and a probe pinning that absence reds.
*
* WHY THIS LANE AND NOT {@link budgetUsd}. CC 2.1.245+ ships the readout on by default (its own
* settings schema reads "Defaults to padded-countdown"), which is the fact that reopened the
* 2026-07-30 registration; the design/74 "no budget language reaches the model" default is
* relaxed here for the same reason CC relaxed it. The BYOM honesty objection does not survive
* either: core estimates nothing, publishes only the ceiling it already ENFORCES, and a task
* that declares no token ceiling never fires the countdown arms at all — so the flip is
* observable exactly in the deployments that asked for a ceiling. That property is EXACT, not
* approximate: CC's two CONSTANT arms (`fixed`/`infinite`) publish a number with no ceiling
* behind it, so the DEFAULT deliberately does not reach them — they stay behind an explicit
* `true` here (see the runner's lane gate). The $ twin keeps its opt-in
* default: CC has no equivalent default-on posture for it.
*/
totalTokensReminder?: boolean;
/**
* Which arm of the readout, CC 2.1.245's closed set (`off` / `infinite` / `fixed` /
* `countdown` / `padded-countdown`); absent ⇒ CC's own default `"padded-countdown"`.
*
* - `countdown` — remaining = ceiling − spend, raw.
* - `padded-countdown` — the same, through a monotone floor, so the number NEVER JUMPS BACK
* within one engine leg (CC's per-agent smoothing floor; ONE `runTask` invocation is the
* epoch). A durable-resume leg is a NEW epoch by design: the slice window, the spend
* coordinate and the floor all restart, so the resumed leg's readout RE-ANCHORS to its own
* real remaining — which may sit above the prior leg's last readout (the fresh slice window
* is a genuine new allowance; CC's own re-anchor arm counts a new task epoch down from the
* full budget again). Carrying the floor across legs would publish "0 tokens left" against a
* window the run genuinely still holds — a frozen falsehood, deliberately not done.
* - `off` — silences the lane while leaving it wired, for a deployment that resolves the mode from
* its own env/settings the way CC does (core reads no env).
* - `infinite` / `fixed` — CC's two constant arms: the literal `Infinite`, and the constant
* 5000000. They publish no measurement (that is what they are for) and need no ceiling.
*
* A value outside the set is REFUSED at prepare (`config.attachment_invalid`), never folded to the
* default — a near-miss spelling must not silently publish a different readout than the one asked for.
*/
totalTokensReminderMode?: "off" | "infinite" | "fixed" | "countdown" | "padded-countdown";
/** Post-compact background-task restatement — DEFAULT ON since 5.12.0 (boolean, not `true`:
* explicit `false` is the opt-out; same contract as the listing family below). CC hard-codes
* this behavior, and the opt-in default left every non-shell host (server-driven runs) with a
* window where the model had no context evidence of an in-flight task after a compaction —
* duplicate spawns / premature turn ends (board case, ruled 2026-08-04). BREAKING: pre-5.12.0
* the default was off — a probe pinning "no background_tasks frame without opt-in" reds. */
backgroundTasks?: boolean;
toolsDelta?: true;
/** [c209-C] listing family — DEFAULT ON (boolean, not `true`: explicit `false` is the opt-out;
* see the family carve-out note above). BREAKING (B-3): pre-[c209-C] the default was off. */
agentListing?: boolean;
/** [c209-C] listing family — DEFAULT ON (same contract as `agentListing`). */
skillsListing?: boolean;
mcpInstructions?: true;
};
/**
* Inject a built-in `report_blocked` tool the agent can call when it cannot finish
* (missing info, permission, ambiguity). When called, the task ends with status "blocked".
* Default: true.
*/
enableBlockedReport?: boolean;
/**
* **LLM self-orchestration** (design/98, S8). Allow THIS task's model to AUTHOR + RUN its own workflow
* (a deterministic JS orchestration script) via the `run_workflow` tool. Default `false`.
*
* 🔴 REQUIRES a deployment-provided HARD sandbox AND a governance baseline:
* `RunnerDeps.workflowScriptRunner.safeForUntrustedScripts === true` (an isolated-vm / separate-process
* runner — the Node `vm` dev runner is NOT a security boundary) AND `RunnerDeps.workflowGovernanceBaseline`
* (the deployment-trusted governance every spawned sub-agent inherits, tighten-only). With EITHER missing
* the tool is NOT mounted and the orchestration prompt is NOT injected (FAIL-CLOSED) — core never runs an
* LLM-authored script in the dev `vm`, and never lets a script-spawned agent run with no governed baseline.
*
* Recommended ONLY for strong models; weak models default-off (force-enableable, but the hard-sandbox +
* baseline bar still applies — the gate is the deployment's sandbox/governance, NOT the model id). NOT
* enforced by model id.
*/
selfOrchestration?: boolean;
/** Per-model auth. Returns the api key (and optional headers) for a given provider/model. */
getApiKeyAndHeaders?: (model: Model) => Promise<{
apiKey: string;
headers?: Record<string, string>;
} | undefined>;
/**
* External cancellation. When it aborts, the task aborts (the brain request is cancelled and any
* pending tool-policy gate releases). Pass a parent tool's `ToolExecuteContext.signal` here so a
* nested `runTask` stops the instant the parent does instead of running to its own `limits.maxWalltimeMs`.
*/
signal?: AbortSignal;
}