UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

1,489 lines • 116 kB
/**
 * 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;
};
/** 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";
    /**
     * 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[];
    /** 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;
    /** Preset system prompt for this task. */
    systemPrompt?: string;
    /**
     * APPEND a caller block to the assembled system prompt, keeping the engine's base + harness/safety blocks
     * (unlike `systemPrompt`, which REPLACES the base, and `promptProvider`, which rewrites the whole assembly).
     * 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;
    };
    /**
     * Per-task ROLE-LAYER prompt provider (overrides `RunnerDeps.promptProvider`). Lets one Runner serve
     * different scenarios with different role prompts. 提示词主权批: `stableSystem` supplies the role base
     * only — core appends the constitution (harness/safety/mode/memory blocks) structurally, so a provider
     * cannot drop it by accident; `replaceAll: true` is the explicit full-replacement escape hatch.
     * TRUST BOUNDARY: a provider is function-valued and therefore CODE — it can only be set by in-process
     * deployment code, never by a wire-level (JSON) task submission; untrusted self-orchestration specs
     * additionally have this field stripped by workflow governance. The `prompt.assembled` trace records
     * which mode ran, so a `replaceAll` is always observable.
     */
    promptProvider?: import("../prompts/default.js").PromptProvider;
    /**
     * 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[];
    /**
     * per-scenario tool ROSTER (true unmount, NOT a permission gate): wire names listed here
     * 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). Exact wire-name match (aliases resolve to their
     * canonical wire name at mount, so list the canonical name). Unknown names are ignored (a
     * center-distributed roster may cover tools this deployment doesn't mount). The injected
     * ToolSearch cannot be excluded (it mounts after this filter, only when deferred tools exist).
     * ⚠️ Excluding structural tools (submit_output / ReportBlocked) can strand the task — the caller
     * owns that risk, same trust level as assembling `spec.tools` itself.
     */
    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 (revision of the posture, dual-leg verified against
     * upstream): an `allowTools` entry naming a tool that does not exist is ITEM-FILTERED at spawn (the
     * agent stays listed and delegable; the only host disclosure is a prepare-time HEURISTIC advisory
     * via `RunnerDeps.onError` phase `"config"` for entries unknown to the parent's assembled roster —
     * zero model-facing warning, the verified upstream shape); a `denyTools` entry naming an
     * unknown tool is REJECTED at task start (`config.agent.unknown_tool` — a deny-typo silently WIDENS,
     * the opposite failure direction, sema extension with no upstream deny-list to defer to); 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`. */
    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 and routed to this
     *  manager. Overrides `RunnerDeps.lspManager`. Unset ⇒ 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;
    /** In-process hooks for this task — the full `Hooks` lifecycle seam (tool pre/post/failure/batch
     *  interception, prompt gating, stop pushback, compaction taps, permission-denied observation; each
     *  member's contract is on the interface). Overrides `RunnerDeps.hooks`. */
    hooks?: import("./hooks.js").Hooks;
    /**
     * 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[];
    /**
     * 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;
}