@sema-agent/core
Version:
Stateless, task-oriented AI agent core
1,381 lines • 102 kB
TypeScript
/**
* The TOOL contract: the side-effect word (`ToolEffect`), the content-origin word
* (`ToolContentOrigin`), the reversibility verdict, the spec itself (`ToolSpec`), what a call returns
* (`ToolReturn`), the nested-usage rollup a delegating tool reports (`NestedUsage` /
* `NestedUsageAccum`) and the execute context every call receives. `ToolExecuteContext` lives here
* rather than in a module of its own because it is the parameter of `ToolSpec.execute` — splitting the
* pair would put one contract on two sides of a module boundary. Layer 0 vocabulary; `types.ts`
* re-exports every name below, so no consumer's import changes.
*/
import type { TSchema } from "typebox";
import type { ThinkingLevel, ToolInputValidationContext, ToolInputVerdict } from "../internal/harness.js";
import type { DocumentContent, ImageContent, Model, TextContent } from "../internal/llm.js";
import type { AgentDefinition } from "./agent-definition.js";
import type { TaskEvent } from "./task-event.js";
import type { TaskSpec } from "./task-spec.js";
import type { BackgroundChildEvent } from "./delegation-frames.js";
import type { ModelRef } from "./model-seat.js";
import type { ToolFamily, ToolPathTarget, ToolRenderHints, ToolRuleFace } from "./tool-face.js";
/**
* Side-effect class of a tool. Used by wake/resume reconciliation: when a call was interrupted
* before its result was recorded, the outcome is unknown, so core must decide whether re-doing it
* is safe. See {@link reconcileInterruptedSession}.
* - `read` — no side effects (a query/lookup). Safe to call again.
* - `idempotent` — has a side effect, but repeating it with the same args is equivalent to doing it
* once (e.g. `PUT` a known key, set-to-value). Safe to call again.
* - `write` — a non-idempotent side effect (delete, append, charge, send). **Unsafe to repeat
* blindly.** This is the conservative default when `effect` is omitted.
*/
export type ToolEffect = "read" | "write" | "idempotent";
/** design/178 §3 — the content-origin classes (see {@link ToolSpec.contentOrigin}). */
export type ToolContentOrigin = "external" | "execution" | "local";
/** A native tool the model can call. `parameters` is a typebox schema; `execute` does the work. */
/**
* design/77 §4 — what a {@link ToolSpec.reversibilityProbe} returns for ONE call.
*
* `reversible` alone decides: the gate tightens a surviving `allow` to `ask` unless it is exactly `true`,
* and no optional member can widen that. They exist so the resulting approval card can say WHY, a
* thing the probe knows at the moment it decides and nothing downstream can re-derive.
*
* Supply `cause` when the reason has parts a consumer should render itself; `reason` when it is prose
* this engine cannot interpret. All three are optional and independent, so a probe written against the
* original bare `{ reversible }` shape remains valid.
*/
export interface ReversibilityVerdict {
reversible: boolean;
/** Free-text account, neutralized and length-capped by the gate before it reaches any card. */
reason?: string;
/** Structured account — see {@link import("./checkpoint-store.js").ProbeCause}. Validated by the gate;
* a malformed value costs the DISCLOSURE only, never the ask. */
cause?: import("./checkpoint-store.js").ProbeCause;
/**
* This demotion is STRUCTURAL, not the probe hesitating: the call crosses a
* boundary the deployment declared, so a persisted allow rule must not retire the resulting ask.
* The gate folds it into the #144 mandate family ("allow rules silence the CLASSIFIER's questions,
* never a MANDATED one") beside an operator `shellGate:"always"` and the tool's own
* egress/irreversibility marks: a matching rule is DISCLOSED as shadowed instead of clearing the
* ask, and the card's "stop asking me this" offer is withheld rather than offered and then refused.
*
* Read on the demotion arm only — a `reversible: true` verdict ends the gate with no ask to mandate,
* and this member cannot make one. It is the one member a DEPLOYMENT-supplied probe may set that the
* engine acts on, and it is safe for the same reason the probe itself is: the effect is strictly
* TIGHTENING (one more question survives one more configuration), so an over-claiming probe costs
* its own deployment a re-confirmation and can never turn an ask into an allow.
*
* The built-in shell probe sets it for exactly one family: a listed reader naming a path outside the
* session's allowed directories. That demotion HAS a clearing channel — widening the declared read
* directories retires it — and where the probe can name the directory, {@link readRootCandidate} carries
* it. A mandated ask WITHOUT that member has no configuration behind it: the rest of the family (an
* operator `shellGate:"always"`, the tool's egress/irreversibility marks, an org demand, an out-of-root
* arm with no named directory) is confirm-or-refuse, and must not be worded as though a grant answers it.
*/
mandated?: boolean;
/**
* The WEAKER rung of the same ladder as {@link mandated}: a read boundary the deployment declared was
* consulted and DECLINED TO JUDGE where this command reads — it named no path, so it neither declared an
* objection nor vouched anything. The resulting ask is ORDINARY on purpose: every ANSWERER may settle it (a
* person, the auto-mode classifier, a person's stored allow rule). What it excludes is the other kind of
* clearing — an engine table that answers nothing and merely recognises a command SHAPE
* (`readOnlyShellVerdict`, whose tables read program names and never paths). A mandate would take the
* answerers out too, a trade this class was deliberately not given. TIGHTENING-ONLY like {@link mandated}:
* one more question surviving one more configuration, never an ask becoming an allow, read on the demotion
* arm alone. Read PRESENCE, never absence.
*/
boundaryDeclined?: boolean;
/**
* The directory-scoped GRANT that would clear THIS ask, when the probe can name one: PRESENT ⇒ the only
* thing between this call and a plain `reversible: true` verdict is the session's declared read
* containment, and adding {@link ReadRootGrantCandidate.dir} to it makes the same call run with no card.
*
* ABSENT is the ordinary world and no claim that nothing would clear the ask — it covers every demotion
* with no directory answer (a deny row, a read the walk could not read, an unenumerable expansion, a
* shape refused whatever the roots say) and every probe silent on this member. Read presence, never
* absence. ADVISORY display metadata, never adjudication input.
*/
readRootCandidate?: ReadRootGrantCandidate;
}
/**
* A directory-scoped grant a surface can offer on an approval card — see
* {@link ReversibilityVerdict.readRootCandidate} and `AskRequest.readRootCandidate`.
*
* @contract shell.read_boundary.grant_candidate_is_the_grant — {@link dir} is the string to ADD, not a
* string to show: a consumer that renders a shortened or home-relative form must send this exact spelling
* back when the person accepts. Core mints one spelling for both errands, and the promise is over THAT
* spelling reaching the directories the boundary compares against: a deployment that REWRITES a declared
* directory — canonicalizing a symlinked one included — must rewrite the command's spelling the same way,
* or it will compare a rewritten root against an unrewritten operand and ask again. Where the two differ
* the ask was unclearable before this seat existed and still is; the offer names the directory the command
* spelled, which is the only one derivable from the text.
*/
export interface ReadRootGrantCandidate {
/** An absolute, lexically folded path in the boundary's own spelling; never a pattern. A DIRECTORY
* unless {@link covers} says otherwise. */
readonly dir: string;
/** `"exact"` ⇒ {@link dir} is the path the command NAMED, offered verbatim rather than a directory
* containing it. A consumer ADDS the string itself: expanding it into a pattern grants nothing. */
readonly covers?: "exact";
/** Always the literal `true` — a shape, not a value, so the seat cannot be widened into a "maybe this
* helps" hint without a visible type change. It claims THIS ask: not the deny layer (judged by pattern
* rather than by root, and unmoved by any grant), and not another ask the same call may raise. */
readonly clearsThisAsk: true;
}
export interface ToolSpec<TParams extends TSchema = TSchema> {
/**
* Model-facing tool name. It keeps the caller tool name contract (`caller-tool-name.ts`), judged at prepare-time — a
* name that breaks it is refused there (`config.tool_name_invalid`) and the run does not start:
* - it is a name a rule can name: ASCII letters, digits and `_ - . : *`, starting with a letter, a digit or `_`, not
* ending with `.` or `:`, and not the lone wildcard `*` — so every allow / deny list can name the tool (a leading `-`,
* a character outside ASCII, a blank or an empty name is refused);
* - it does not contain `__` — that separator is reserved by the PROTOCOL TABLE (`protocol-table.ts`), the umbrella over
* every namespaced family the engine speaks: `mcp__<server>__<tool>`, `a2a__<peer>__<skill>`, and whatever a third
* protocol appends.
*
* The live reason (not the retired durable-resume name shim, RB-476-A): every table-driven site — the
* namespace `parse`, the grouped display key, the permission-rule lanes, the tool-manifest origin
* projection — decides "which protocol owns this name, and which peer inside it" from the prefix and the
* FIRST separator. A caller tool named through that shape would be attributed to a remote peer that does
* not exist, on surfaces where attribution is what a policy or an operator acts on.
*/
name: string;
/**
* Backward-compatible model-facing names that should dispatch to this tool. New transcripts and tool rosters
* should use {@link name}; aliases exist only so old model calls / SDK payloads / resumed sessions don't strand
* across a canonical rename. Each alias keeps the same contract as {@link name} (a rule naming an alias reaches the tool).
*/
aliases?: string[];
description: string;
/**
* R2 双形轴(2026-07-18 裁):the CLASSIC-profile variant of {@link description}. CC 2.1.212
* keys its tool descriptions on `LT(model_id)` (simple short form vs classic long form); sema's
* counterpart is {@link TaskSpec.promptProfile} — default "simple" (what CC serves every BYOM
* model id). When the run resolves to "classic" and this field is present, it REPLACES
* `description` at mount time. Absent = the tool has one form (description serves both profiles).
*/
descriptionClassic?: string;
/**
* Human label for UIs. Defaults to `name`.
*
* ⚠️ Currently INERT (design/71 liveness sweep, 2026-06-12): mapped onto the tool object but no
* runtime path or emitted event reads it — the model and TaskStream consumers only ever see
* `name`. Kept for API stability; wire it into tool events or drop it before building on it.
*/
label?: string;
/**
* G1 通告层 (CC `agent_listing_delta` parity): the delegation tool's agent-type roster (name + one-line
* description), filled by `createSubagentTool` so the Runner's announcer can surface the
* "Available agent types" reminder without reaching into the tool's closure. Deployment-authored tools
* normally leave it unset. [c209-C]: this is now the roster's ONLY model-facing carrier (the tool
* description/schema carry a static pointer sentence instead — cache-prefix immunity), delivered on
* the FIRST user turn plus boundary drift deltas; `TaskSpec.attachments.agentListing` is default-ON
* (explicit `false` opts out, with a config warning when a roster is mounted).
*/
agentListing?: ReadonlyArray<{
name: string;
description: string;
}>;
/**
* design/178 §3 (ruled 2026-08-08) — the MACHINE-readable twin of {@link agentListing}'s display
* `(Tools: …)` note: per offered agent type, the allow/deny narrowing that decides which of the
* offered pool a child of that type actually mounts. Filled by `createSubagentTool` beside
* `agentListing`; a type with neither list gets the whole offered pool.
*
* Read by the memory content-origin face, which classifies a delegation RESULT by the
* child's tool face. A child that can reach a network or protocol tool returns content from outside
* the trust boundary just as surely as calling that tool here would, and the parent's transcript is
* where it lands. Static by construction — the child's face is known from its definition, so no
* runtime tracking of what the child actually did is involved (that is the v2 ticket). `authored`
* marks the entries minted from a definition a caller wrote on a tool a deployment mounted (not the
* engine's built-ins, the general-purpose alias, fork, or the engine's nested copy of the tool for a
* delegated child); prepare's deny-name check judges only those.
*/
agentToolFaces?: ReadonlyArray<{
name: string;
allowTools?: readonly string[];
denyTools?: readonly string[];
canRedelegate?: boolean;
authored?: true;
}>;
/**
* The tool-level `denyTools` list of the delegation tool (`SubagentToolOptions.denyTools`, applied when no agent definition is
* selected), exactly as written, filled by `createSubagentTool` beside {@link agentToolFaces}; absent when none was written
* and on the engine's nested copy for a delegated child. Read by prepare's deny-name check, which judges it with
* the `authored` entries of the faces.
*/
agentToolLevelDenyTools?: readonly string[];
/**
* The tool-level `allowTools` list of the delegation tool (`SubagentToolOptions.allowTools`, applied when no agent definition is
* selected), as the tool copied it, filled by `createSubagentTool` beside {@link agentToolLevelDenyTools}; absent when none was
* written and on the engine's nested copy for a delegated child. Read by prepare's allowed-list judgment: a value that is not a
* list of strings is refused before the run.
*/
agentToolLevelAllowTools?: readonly string[];
/**
* design/178 §3 (ruled 2026-08-08) — the POOL a delegated child draws from, as the delegation tool
* captured it. Read together with {@link agentToolFaces} by the memory content-origin face: the face
* narrows, this is what it narrows FROM. It is the CHILD'S pool, deliberately: a delegation tool
* carries its own tool set, and a parent that mounts nothing but the delegation tool can still hand
* its children a network tool, so the parent's roster is the wrong thing to look at.
*
* ABSENT ⇔ the pool is not statically knowable — a spawn-time tool factory with no declared roster
* (`SubagentToolOptions.extraToolNames`) can inject tools whose identity does not exist until the call
* happens — and the reader fails closed on that. A declared roster's tools are rows of the pool.
*/
agentToolPool?: ReadonlyArray<{
name: string;
aliases?: readonly string[];
contentOrigin?: ToolContentOrigin;
}>;
/**
* TRUE ⇔ the children of this delegation tool inherit the MCP tools their spawning run mounts (no explicit pool: the pool is
* the inherited surface, and the MCP mount is part of it). Filled by `createSubagentTool` beside {@link agentToolPool}, which
* is a declaration and cannot list a run's mount: the memory content-origin face reads this to add the spawning run's MCP tools
* to the child's pool, through the same universe reader the spawn filters with.
*/
agentToolInheritsMcp?: true;
/** approval display projection (see AgentTool.approvalPreview): pure args→display value
* for human approval surfaces; clamped + throw-swallowed by the gate; never adjudication input. */
approvalPreview?: (args: unknown) => unknown;
/**
* OPTIONAL input pre-validation — runs BEFORE the permission ask (and before every other tool-call
* gate station: PreToolUse hooks, policy, the auto-mode classifier), AFTER schema validation. A
* refusal (`{ ok: false, message }`) is returned to the model as a typed error result and NO
* approval card is shown for the call; `{ ok: true }` / `undefined` lets the call proceed to the
* gate unchanged. Upstream form (CC 2.1.250 `validateInput` → `{result:false, message, errorCode}`,
* consulted ahead of the permission prompt): a call the tool would refuse on its own precondition —
* an Edit/Write/NotebookEdit whose target file was never Read this session — used to reach the
* operator as a card that could only ever fail; the model then retried the same failing call under
* auto mode until the turn cap. A validator MUST be pure (read tracking state, path grammar; no
* side effects, no writes, nothing the run observes) and MUST refuse with the SAME text its
* execution would have produced for that input, so the model sees one refusal either way. A
* validator that THROWS is read as "no verdict": the call proceeds to the gate and the fault is
* disclosed via `RunnerDeps.onError` (phase `"hook"`) — a broken validator must not refuse tools.
* The execution-time check stays in place (validate and execute are two reads; a direct
* `AgentTool.execute` caller — durable replay — never passes this seat).
*/
validateInput?: (args: unknown, ctx: ToolInputValidationContext) => Promise<ToolInputVerdict | undefined> | ToolInputVerdict | undefined;
/**
* [c209-C] Q4 — the model catalog names (`SubagentToolOptions.models` keys), filled by
* `createSubagentTool` alongside {@link agentListing}. Rendered as the agent_listing INITIAL frame's
* trailing "Models available for the 'model' parameter: …" line — the announce-face replacement for
* the `(available: …)` / `(one of: …)` interpolations the tool description/schema used to carry
* (no CC anchor: CC's model face is a static tier enum; sema's catalog is deployment-dynamic).
*/
agentModels?: readonly string[];
/**
* core half — PER-TASK agent-roster rebuild seam, filled ONLY by `createSubagentTool`
* (deployment-authored tools leave it unset). The delegation tool's roster is a mount-time SNAPSHOT
* baked into its parameter enum/description (see the `SubagentToolOptions` snapshot note), so per-task
* additions ({@link TaskSpec.agents}) cannot mutate the mounted instance — `prepareTask` instead calls
* this to obtain a REBUILT ToolSpec whose roster is the UNION of the mount-time roster and the given
* per-task definitions (a same-name per-task definition WINS over the mount-time one; built-in
* shadowing/fork semantics re-derive over the merged set). Everything else about the tool
* (pool/limits/depth/background lanes) rebuilds from the same options snapshot. The rebuild re-runs the
* full assembly-time validation (duplicate/reserved names, string `model` refs against the catalog) —
* fail-loud at task start, never deep inside a run. POLICY-NEUTRAL: this is a roster LIST injection
* only — the child's effective policy chain (tighten-only parent inheritance via
* `RunInternals.inheritedGate`) applies at spawn time, entirely downstream of the roster.
*/
withAgents?: (agents: ReadonlyArray<AgentDefinition>) => ToolSpec;
/**
* design/136 §2.1.c — OPTIONAL pre-validation argument adapter (mirrors the engine
* `AgentTool.prepareArguments` seam it is forwarded onto): map an alternative argument shape onto the
* current schema BEFORE schema validation. MUST be idempotent and identity for already-canonical args
* (the engine path applies it before validation AND the defineTool wrapper applies it again before its
* own check). RB-479 note: its original LEGACY-replay purpose (pre-rename durable calls resolved via
* the retired canonicalizer) is dead with RB-476-A; what remains is a generic third-party arg-migration
* capability — the first-party legacy adapters riding it are booked for removal (RB-479-A③).
*/
prepareArguments?: (args: unknown) => unknown;
/**
* Side-effect class for wake/resume safety. Defaults to `write` (conservative) when omitted:
* an interrupted call of unknown effect is treated as possibly-applied and never auto-repeated.
*/
effect?: ToolEffect;
/**
* design/70: marks a tool whose execution is an EXTERNAL write (egress) — an outward-visible,
* effectively-irreversible action (open a PR, push a branch, send) as opposed to an internal side
* effect. **Orthogonal to {@link effect}**: `effect` is the repeat-safety axis (wake/resume
* reconciliation), `egress` is the blast-radius axis (the design/37 gate); reconcile/wake ignore it.
* The gate NEVER auto-allows an egress tool: a surviving `allow` (default or policy) is tightened to
* `ask`, so execution always passes an explicit ask-resolution — synchronous `onAsk`, a durable
* suspend (design/45), or a deployment-authored approver. Headless stays deny (an external write
* never happens without explicit confirmation). An egress tool must be repeat-unsafe: declaring
* `effect: "read"`/`"idempotent"` with `egress: true` is a configuration error (fail-fast at
* prepare) — omitting `effect` is fine (the default IS `write`).
*/
egress?: true;
/**
* The tool's PERMISSION SEAT. `"passthrough"`: the tool states no permission of its own — under an asking permission mode
* (`default` / `acceptEdits` / `plan` / `auto` / `dontAsk`) a call to it runs only when some layer STATES an allow (an allow
* rule or allow-list entry naming it, an approver's yes, a PreToolUse hook's own `allow`); a policy fold in which nobody
* stated it ("no objection") gets the mode's question (class `unstated`, origin `policy`), which dontAsk refuses, auto's
* classifier may answer, and `bypassPermissions` never raises. The MCP and A2A materializers set it on a remote peer's
* tool. Absent: the tool answers for itself (the engine's own stations and the deployment's declarations). Tightening only —
* it can make a tool ask, never let one run.
*/
permissionSeat?: "passthrough";
/**
* design/77 §4 (Gate 4 — irreversibility axis): how reversible this tool's effect is. A THIRD axis,
* orthogonal to {@link effect} (repeat-safety) and {@link egress} (blast-radius). For office/finance
* primitives (send money, file a return, send an email) where a wrong action cannot be undone, this
* forces a durable human-approval suspend BEFORE the tool runs:
* - `"never"` — fully reversible; the gate leaves a surviving `allow` untouched (no ask).
* - `"maybe"` — reversibility depends on the args; the gate calls {@link reversibilityProbe} (when one is
* declared) on a surviving `allow` and tightens to `ask` UNLESS the probe reports reversible. A tool
* that declares a `reversibilityProbe` but omits `irreversibility` defaults to `"maybe"` (so it always
* enters the activation set and is gated). Since #502 the probe ALSO runs over a surviving `ask`
* — see {@link reversibilityProbe} for what that arm reads (only {@link ReversibilityVerdict.mandated})
* and what it costs; it tightens nothing there, because an ask cannot be tightened into an ask.
* - `"always"` — irreversible; the gate ALWAYS tightens a surviving `allow` to `ask` (like {@link egress}).
*
* The tighten mirrors the egress tighten (it only ever tightens allow→ask; a `deny`/`ask` is untouched),
* runs immediately AFTER it, and routes through the SAME ask-resolution: synchronous `onAsk`, a durable
* suspend (a new `{kind:"irreversible_ask"}` checkpoint → `status:"suspended"`), or a deployment approver.
* Headless with no approver SUSPENDS durably (never a model-facing deny — that would be a reward-hack
* retry surface), so an irreversible action never runs unattended without explicit confirmation.
*
* **Note (deliberate redundancy):** `egress` already means "effectively-irreversible external write"; an
* `egress:true` + `irreversibility:"always"` tool is DOUBLE-tightened. Harmless (allow→ask is idempotent
* under the deny>ask fold) — the two axes overlap on purpose, they are NOT strictly disjoint.
*/
irreversibility?: "never" | "maybe" | "always";
/**
* design/77 §4: optional deployment-injected probe for an `irreversibility:"maybe"` tool. Given the call's
* (post-hook) args, it reports whether THIS specific call is reversible. It is time-bounded (the
* approval timeout when configured, a 30s default when absent — the probe wait is always finite) and
* **fail-closed**: a non-`reversible` verdict, a timeout, or a throw all tighten to
* `ask`. Declaring this probe defaults `irreversibility` to `"maybe"`. It is read from the spec at
* prepare-time and captured in a closure — NOT a tool argument — so the model cannot monkey-patch it.
*
* **WHEN THE GATE CALLS IT — two arms, and the second is newer than this paragraph's first draft**
* (#502; `irreversibility` must resolve to `"maybe"` for either):
* 1. the surviving decision is `allow` — the TIGHTEN arm: the verdict decides allow→ask, and a
* tightening verdict's `reason`/`cause` ride the minted ask;
* 2. the surviving decision is already `ask` — the MANDATE arm: the probe is the only source of
* {@link ReversibilityVerdict.mandated}, and an ask another layer raised needed it too. (Before
* #502 a policy that asked FIRST skipped this block entirely, so an out-of-root read never
* acquired its mandate and a covering allow rule retired the question — measured, not theorised.)
* Here the `reversible` verdict is DISCARDED (a probe may never un-ask another layer's question),
* `reason`/`cause` are not merged onto somebody else's ask, and only `mandated` is read.
* The TIGHTEN is still `allow`→`ask` only — a `deny`/`ask` is never re-minted — but the INVOCATION
* set is wider than the tighten's, and that difference is the deployment's to budget for: on an
* ask-first policy the probe is called (and awaited, to the bound above) on every `"maybe"`-tier
* call, and a probe that throws or times out reports through `onError` with `phase: "hook"` on
* those calls too. A probe should therefore be a cheap, side-effect-free inspection of the args.
*
* **`reason` (optional, additive both ways)** — WHY this call was not proven reversible, in the probe's
* own words, minted in the same pass that reached the verdict (so nothing downstream re-derives it).
* Read ONLY on a tightening verdict: a `reversible: true` return ends the gate, and a timeout/throw
* produces no verdict at all, so the cause travels exactly when there is an approval card to put it on.
* The gate neutralizes and length-caps it, then carries it to both approval routes — the synchronous
* `AskRequest.probeReason` and the durable park's `RiskDescriptor.probeReason`. Compatible in BOTH
* directions by construction: a probe written against the older bare `{ reversible }` shape stays
* type-correct and simply supplies no cause, and an engine that does not know the member ignores it.
*
* **`cause` (optional)** — the STRUCTURED alternative, for a probe whose verdict is machine-describable:
* a `code` plus operand families as arrays with honest totals ({@link ProbeCause}), carried to
* `AskRequest.probeCause` / `RiskDescriptor.probeCause`. Prefer it wherever the cause has parts. Prose
* has to be written before the edge cases are known — a sentence about what a command DOES is false
* wherever the check over-fires, and a multi-part disclosure flattened into one capped string loses
* whichever part sorts last — whereas a code cannot be false and an array cannot be truncated into a
* lie. `reason` remains for deployments whose cause is genuinely just prose this engine cannot read.
* Both may be supplied; each is validated and carried independently.
*
* TRUST: the text is DEPLOYMENT-authored (and, for the built-in shell probe, derived from the model's
* own command), so it is DISPLAY/TRIAGE metadata only — never adjudication input, and never a channel
* that can widen a verdict. A probe cannot use it to auto-allow: `reversible` alone decides.
*
* That property is STRUCTURAL, and keeping it so constrains where the cause may be written. It is
* carried as a member and deliberately NOT folded into the ask's `message`, because the message is
* what an auto-mode classifier is handed as its `askMessage` — and that classifier may answer an ask
* with `allow`. Text on the prose channel is therefore steering input to a decider that can clear the
* very ask the probe raised, which is exactly the authority a probe must not have. Anything added
* later that renders this cause must keep it on display surfaces only.
*/
reversibilityProbe?: (args: unknown) => ReversibilityVerdict | Promise<ReversibilityVerdict>;
/**
* design/178 §3 — CONTENT-ORIGIN class of what this tool can bring into the session, a fourth axis
* orthogonal to {@link effect} (repeat-safety), {@link egress} (blast-radius) and
* {@link irreversibility}. It feeds long-term-memory write governance only — no gate/policy/roster
* behavior reads it, and on a task without an engine-memory session it is fully inert:
* - `"external"` — the tool takes content from outside the deployment's trust boundary (network
* fetch/search families, external protocol channels). Invoking it marks the session's memory
* EXTERNALLY EXPOSED (one-way, durable). What the mark does at harvest is the deployment's
* {@link RunnerDeps.memoryProvenance} mode's question (design/336): under `"carry"` (default)
* ordinary entries commit WITH an engine-minted `origin` marker and instruction-form files are
* withheld; under `"off"` the harvest quarantines instead of committing — either way,
* third-party text never rides the automatic path into long-term memory UNMARKED.
* - `"execution"` — general execution (shell/exec families). NOT polluting by default — an
* execution tool CAN reach external content indirectly, but excluding every shell session from
* memory would disable memory for the main coding scenario; the write-side scans/fences remain
* as compensating controls, and `TaskSpec.memory.execIsExternalContent` upgrades this class to
* `"external"` for high-assurance deployments.
* - `"local"` — purely local reads/computation.
* Classification is single-sourced with the tool definition: core built-ins declare theirs here;
* a HOST tool that omits it is classified fail-closed as `"external"` (unknown = external) unless
* the deployment exempts the name via `TaskSpec.memory.trustedTools`. A declaration BEATS that
* allowlist (the allowlist is the channel for UNDECLARED tools, never a lever to re-grade a tool
* that classified itself), and only a member of the closed vocabulary counts as one — an unreadable
* value classifies `"external"` rather than exempting anything.
*
* Tools core mints on the deployment's behalf get the same seat one level up: an MCP server's whole
* tool set is declared at its entry ({@link McpServerSpec.contentOrigin}, design/378), because the
* host never touches those tool objects.
*/
contentOrigin?: ToolContentOrigin;
/**
* Batch execution mode (design/64 §1 P1, CC reads-parallel/writes-sequential partition). When omitted,
* `defineTool` derives it FAIL-CLOSED from `effect` (CC 2.1.206 `TOOL_DEFAULTS.isConcurrencySafe → false`):
* `read` → `"parallel"`; `write`/`idempotent`/UNDECLARED → `"sequential"` — an unannotated tool never
* silently rides a parallel batch, and two writes in one batch never race. Set explicitly to override.
*/
executionMode?: "sequential" | "parallel";
/**
* design/120 P1.5 (CC `isConcurrencySafe(parsedInput)` parity): input-level concurrency
* refinement for a non-`"parallel"` tool — return `true` to let THIS CALL run in a parallel batch
* (and, under `streamingToolExecution`, start in-stream). An explicit `executionMode:"parallel"`
* needs no refinement (always safe); a throw = false (fail-closed). In the batch/stream paths the
* hook receives schema-VALIDATED (coerced) args — CC 206 `parsed.data` parity; a schema-invalid
* call is judged unsafe before the hook runs. Still treat defensively (direct callers may pass raw).
* The one real consumer is Bash's read-only classifier.
*/
isConcurrencySafe?: (args: unknown) => boolean;
/** typebox schema for the tool arguments. */
parameters: TParams;
/**
* Large-result offload (design/30): set `false` to NEVER offload this tool's output (e.g. a file-read
* tool whose output IS the deliverable — offloading then immediately re-reading would just churn).
* Default: offload when the result exceeds the resolved threshold.
*/
offload?: boolean;
/** Per-tool offload threshold (chars), overriding the global {@link RunnerDeps.toolResultThresholdChars}.
* Same contract as that global knob (CLS-A-2): `0`, `Infinity` or any non-finite value DISABLES offloading
* for this tool — it is not a "threshold so low that everything offloads". */
offloadThresholdChars?: number;
/**
* Deferred disclosure (design/36): when `true`, this tool is a CANDIDATE of the engine's deferral gate.
* When the gate opens (the candidate set's inlined size reaching 10% of the model's context window; see
* {@link RunnerDeps.deferMode}) it is NOT inlined with its full JSON Schema: it appears as a lightweight
* placeholder (`{name, one-line hint, empty params}`) so the model knows it exists; when the gate stays
* shut it ships inline in full. (A caller that wants a tool deferred on every request names it in
* {@link TaskSpec.deferTools} — that list is literal.) Activation is a DISCLOSURE fact (has the model seen the schema?), not an
* execution precondition: the model activates the tool via the injected ToolSearch, or by making a call
* whose arguments already match the real parameters (see {@link TaskSpec.deferSelfResolve}, on by
* default). Where the schema then lands depends on {@link TaskSpec.toolMaterializeStrategy} — the next
* request's `tools[]` under `"swap"` (the default, cache prefix undisturbed), or the ToolSearch result
* text under `"static"`, where the placeholder is never swapped. Use for large/MCP tool sets where
* inlining hundreds of schemas blows up turn-1 tokens and risks prefix-cache breakage. Default: not
* deferred (full schema inlined). See {@link RunnerDeps.deferMode}.
*
* SCOPE: this flag reaches the classifier through CALLER specs (`TaskSpec.tools`) only — a
* built-in/injected tool's spec never enters that list, so setting `defer` on a spec that shadows a
* built-in name does nothing. To defer an already-mounted tool (built-ins included), list its wire
* name in {@link TaskSpec.deferTools}. Also inert when the tool ends up excluded (exclusion wins)
* or pinned ({@link alwaysLoad} / {@link TaskSpec.alwaysLoadTools}).
*/
defer?: boolean;
/**
* Inline-keep override, judged FIRST in the deferral chain (CC 220 counterpart:
* `alwaysLoad`, declared per MCP tool via `_meta["anthropic/alwaysLoad"]`, whose `isDeferredTool`
* checks it before every deferral arm): `true` pins this tool's full schema inline in every
* request — it is never deferred, regardless of its own {@link defer}, {@link TaskSpec.deferTools},
* or the deferral gate. The deferral sources only ever ADD; this is the explicit subtract valve.
* Exclusion still wins ({@link TaskSpec.excludeTools} unmounts — nothing left to keep inline).
*/
alwaysLoad?: boolean;
/**
* design/277 — model-gate CLASS tag (open vocabulary; the built-in table is
* {@link import("./tool-model-gate.js").TOOL_MODEL_GATE_CLASSES}, v1 vocabulary =
* `"task-scaffold"`). A tagged entry declares "I am a default-mounted scaffold of this class":
* at prepare, when the task's RESOLVED model id matches the class's rule, THIS entry is dropped
* from the roster (true unmount, entry-level — a same-name untagged entry is untouched; the
* removed entry stops occupying its name on every downstream surface). Untagged = never gated.
* The default-bundle assemblers stamp their default arms only; an EXPLICITLY composed tool is
* not tagged (user asked ⇒ user gets) — so don't stamp hand-mounted factories. FAIL-OPEN: an id
* the merged table knows nothing about is never gated (BYOM open set — the table encodes
* positive knowledge only), and a tag naming an unknown class is inert + announced
* (`config.tool_model_gate_unknown_class`). Restore channels: explicit composition (no tag),
* {@link TaskSpec.restoreGatedTools}, env `SEMA_TOOL_MODEL_GATE=off`,
* `RunnerDeps.toolModelGate: false`; {@link TaskSpec.excludeTools} always wins regardless.
* The gate DECISION completes on the ToolSpec face inside `prepareConfigDoors`, before any
* conversion; `defineTool` then forwards the class onto the materialized face (design/388: the
* effect roster reports `modelGate` on a row that survived the gate, so an operator can read which
* class a mounted scaffold belongs to — the forwarded value is a read face, never a second gate).
*/
modelGate?: string;
/**
* Tool contract identity (stage S2, prompt-assembly protocol §7): declares the EXECUTION
* CONTRACT this tool implements, independent of its presentation text. `defineTool` attaches it
* to the materialized tool via the catalog side-table (identity follows the OBJECT, never the
* wire name — a caller tool shadowing a built-in name never inherits the core contract). Omitted
* ⇒ the tool reports a deterministic `legacy:<shapeDigest>` contract in the assembly manifest.
*/
contract?: {
/** Stable versioned id, e.g. `core.read@2` (built-ins) or `acme.deploy@1` (caller tools). */
contractId: string;
/** Declared behavior revision — bump when execute-visible behavior changes. */
implementationRevision: string;
};
/**
* design/388 §2.2 ① — the DISPLAY / DISPATCH family of this tool (closed set {@link TOOL_FAMILIES}).
* Input to grouping, card pre-selection and the rule editor's completion — never an authorization
* source: no gate reads it. Absent on a caller/MCP tool ⇒ the roster reports `"other"`; every
* built-in definition in the static catalog declares one (the catalog completeness gate reds a
* missing declaration rather than defaulting it).
*/
family?: ToolFamily;
/**
* design/388 §2.2 ② — the parameter that names this tool's filesystem TARGET and how it is touched.
* Present ⇔ the tool can be confirmed BY PATH: write confinement (`allowDirs`, the fs-write gate,
* the sensitive-path guard, frozen-path denies), edited-file tracking and the transcript-directory
* gate all read it off the call's face instead of a name table. ABSENT ⇒ the tool is handled as a
* write whose target cannot be confirmed (fail-closed under every path-confining face) — exactly how
* a tool outside the retired name tables was handled. `param` outranks `aliases` (NotebookEdit's
* `notebook_path` beats a decoy `file_path`). `skillScopeEligible` is a SEPARATE grant whose one reader (the
* skill-scope write fence) is retired (Write/Edit set it; NotebookEdit deliberately does not; a caller
* tool that merely takes a built-in's NAME inherits nothing).
*/
pathTarget?: ToolPathTarget;
/**
* design/388 §2.2 ③ — the parameter vocabulary a permission rule (`Tool(param:pattern)`) may name.
* DERIVED from the schema when absent (the scalar top-level parameters plus the path target's
* aliases; object/array parameters never enter — a rule on them can never match). A declaration can
* only NARROW the derived set. A tool whose `prepareArguments` renames keys must declare its face
* explicitly (the schema keys are not what a call carries).
*/
ruleFace?: ToolRuleFace;
/**
* design/388 §2.2 ④ — declarative render hints for a shell that has no local implementation of
* this tool (an MCP / caller / external tool). Every string is UNTRUSTED display input: the
* roster caps lengths, the shell strips control and bidi characters. `approvalCard` is a
* pre-filter, never a compatibility proof — the shell validates the call's args against its own
* card schema before mounting a specialised card. `resultCards` may only name this tool's own
* `cardId` namespace; a declaration naming an engine card (`builtin:*`, `mcp`) is refused at prepare.
*/
renderHints?: ToolRenderHints;
/**
* design/388 §2.2 ⑤ — this tool's results are CLEARABLE by the context-edit micro-compaction
* (the CC `microCompact` whitelist, previously the name table `COMPACTABLE_TOOLS`). Default
* false: an undeclared tool's results survive to the harder defenses.
*/
compactable?: true;
/**
* design/388 §2.2 ⑤ — this tool's results are EXEMPT from the per-batch aggregate result budget
* because they are self-bounded (Read pages at its own cap; the offload reader pages small
* slices). Default false: every undeclared tool counts against the budget.
*/
budgetExempt?: true;
/**
* Execute the tool. Return a string (becomes text content) or a structured result.
* Throw to signal failure (the loop encodes it as an error tool result).
*/
execute: (args: unknown, ctx: ToolExecuteContext) => Promise<ToolReturn> | ToolReturn;
}
/** What a tool's execute may return. */
export type ToolReturn = string | {
content: Array<TextContent | ImageContent | DocumentContent> | string;
/** Arbitrary structured details for logs/UI (not shown to the model unless put in content).
* Producer side of the tool-result 双面契约: `content` is the MODEL face (may carry model-directed
* framing/trailers), `details` is the HOST face (per-tool structured Output). Hosts render from the
* structured face and never re-parse the model face — see the `tool_end.structured` JSDoc
* (CC 2.1.207 @765495, CC207-CORE-SWEEP P2-3) for the full contract. */
details?: unknown;
/** Hint the agent to stop after this tool batch. */
terminate?: boolean;
/**
* A `ToolSpec.execute` can RETURN a failure
* receipt instead of throwing (teaching errors that carry structured `details`, e.g. an
* unrecoverable-but-informative rejection). Before this field existed, `ToolReturn` had NO
* isError channel at all — `defineTool`'s wrapper builds a fresh `{content,details,terminate}`
* object from whatever `execute` returns, so ANY extra property (including a caller trying to
* set `isError` via an unchecked/cast return) was silently discarded before it ever reached the
* wire. Confirmed empirically: a `ToolSpec.execute` explicitly returning `isError:true` still
* produced `isError:false` on the model-facing `toolResult`, in BOTH the live per-turn path and
* the durable-resume path — a returned rejection was structurally unable to look like anything
* other than success. Omitted/false = success (matches `AgentToolResult.isError`'s own
* convention exactly — this is that SAME field, finally reachable from a `ToolSpec`).
*/
isError?: boolean;
};
/**
* REF-D15 — DELEGATED (nested) sub-run usage: the ONE object that flows tool → runner → result. A
* delegation tool reports it via {@link ToolExecuteContext.reportUsage}, the Runner folds it into its
* accumulator, and it surfaces as {@link TaskResult.stats}`.nested`. Named because all four sites carry
* the same quantity and this is money (`costMicroUsd` is micro-USD, the billing figure).
*
* `costMicroUsd` is OPTIONAL here by the RB-368 absence contract: a run whose model had no price entry
* reports NO cost rather than a fabricated `0`, so "unpriced" stays distinguishable from "declared free".
* The RUNNING accumulator uses {@link NestedUsageAccum} instead, where it is required.
*
* NOT a general `UsageStats` base type, deliberately: the own-usage field sets on `Stats` (runner-internal,
* mostly REQUIRED so a new accumulation site that forgets a leg fails the type check) and on
* `TaskResult.stats` (public, all OPTIONAL because a gateway may report no usage at all) differ ON PURPOSE
* — folding them into one shape would delete that type check.
*/
export interface NestedUsage {
tokens: number;
turns: number;
tasks: number;
/** ABSENT when the delegated spend was unpriced (RB-368) — never a fabricated 0. */
costMicroUsd?: number;
}
/**
* REF-D15 — the RUNNING accumulator form of {@link NestedUsage}: `costMicroUsd` is REQUIRED because an
* accumulator is seeded at 0 and summed into (`+= u.costMicroUsd ?? 0`), so it always holds a number.
* Used by the live `Prepared.nestedStats` and by the checkpoint's `nestedStats` snapshot, which must
* round-trip a concrete total across a suspend/resume (an absent field there would lose pre-suspend
* child cost — design/38 §4.bis/Q7).
*/
export interface NestedUsageAccum {
tokens: number;
turns: number;
tasks: number;
costMicroUsd: number;
/** batch-4 F3 (RB-368 reaches the nested face): true once ANY delegated leg reported no cost —
* the summed `costMicroUsd` is then a priced-subtotal, not a total, and the publish site must go
* ABSENT instead of impersonating one. REQUIRED so every seeding/snapshot site is tsc-forced to
* carry knownness alongside the number it qualifies. */
anyUnpriced: boolean;
}
export interface ToolExecuteContext {
toolCallId: string;
signal?: AbortSignal;
/**
* design/147 S1 — the SPAWNING run's taskId when THIS run is a delegated child (Runner-filled from
* the trusted internals chain, absent on a top-level run). Read-only context: the Agent tool's
* hierarchy clamp and SendMessage's "main" uplink judge child-ness on it. NOT a TaskSpec field —
* the untrusted caller surface cannot fake parentage into extra capability (forging it only
* TIGHTENS: the clamp fires and the uplink routes to a parent that ignores unknown children).
*/
parentTaskId?: string;
/** design/148 S1 — this run's adopted center prompt artifact (Runner-filled, trusted; same seat
* as {@link parentTaskId}): delegation tools thread it into child RunInternals so the whole
* agent tree composes one closure. Absent on bundled-only runs. */
centerArtifactDigest?: string;
/** Publish provenance companion of {@link centerArtifactDigest}. */
centerSourceRevision?: string;
/**
* design/147 S2a — the SPAWNING run's sessionId (trusted internals chain, same seat as
* {@link parentTaskId}). Sibling resolution/delivery needs the parent's SESSION axis too: a
* session-scoped sibling registers with owner = the parent's sessionId, which the parent's taskId
* alone cannot satisfy when the two differ.
*/
parentSessionId?: string;
/** A-3 — the ROOT host session of the delegation tree (see RunInternals.rootSessionId:
* fixed point `ctx.rootSessionId ?? ctx.sessionId` at every spawn). Absent for a top-level run
* (its OWN sessionId is the root). */
rootSessionId?: string;
/** design/380 O1② (C12) — the spawning run's EXPLICIT placement fixed point
* (`RunInternals.placementRoot`), present only when its internals carried one (cascade rungs /
* verification legs after the first, or a caller-declared root). A delegation tool threads it
* VERBATIM into child RunInternals so every descendant keeps the ladder/gate placement; absent ⇒
* the `rootSessionId` chain above IS the placement root through prepare's mint middle segment
* (`placementRoot ?? rootSessionId ?? sessionId`) and nothing extra is threaded. */
placementRoot?: string;
/**
* design/151 §7 S3c — TRUSTED tier-3 revival claim (same trusted-internals seat as
* {@link parentTaskId}; never a model argument). Filled ONLY by the auto-mounted SendMessage's
* revival leg after it WON the durable row's claim-CAS: the Agent tool's execute then re-launches
* the claimed row — register-with-id on its ORIGINAL handle, `requireExisting` on its ORIGINAL
* transcript session, identity fields inherited VERBATIM from `row` (never re-derived from the
* reviving caller's ctx) — instead of minting a fresh agent. `rev` is the row's CURRENT revision
* (post-claim); the registration seeds its durable write lane from it so the first settle CASes
* against the claim, not a stale pre-claim rev.
*/
reviveClaim?: {
row: import("./background-agent-store.js").BackgroundAgentRecord;
rev: number;
/**
* design/153 §7.2d (件3c) — the PARKED-RESUME drive: present iff this re-launch redeems a
* parked approval (the caller — server /decide — already won a {@link import("./task-registry.js").ParkedClaimTicket}
* reservation). The Agent tool's bg lane then: registers the handle PARKED-born (checkpoint
* arbitration stays live pre-consume), preflights the transcript session (requireExisting),
* drives `runner.resumeStream(ticket.token, outcome, …)` with the consume-flip hook, and on ANY
* failure converges through `rollbackParkedClaim` (checkpoint-truth dispositions). `rev` above
* must equal `ticket.reservedRev` (the lane seeds from the reserved row).
*/
parkedResume?: {
ticket: import("./task-registry.js").ParkedClaimTicket;
outcome: import("./checkpoint-store.js").ResumeOutcome;
/**
* the CROSS-PROCESS re-supply seat for the checkpoint's opaque parent constraints.
* A checkpoint minted under inherited parent-policy constraints records only
* `requiresParentConstraint` (live ToolPolicy/onAsk closures cannot persist); the same-process
* drive is re-supplied automatically from the Runner's in-memory `parentConstraintRegistry`,
* but a FRESH Runner (server restart / rolling update — the production shape where an operator
* decides hours after the park) has an empty registry, so the resume is rejected pre-CAS
* (`resume.parent_constraint_missing`) and the row becomes permanently unredeemable. A
* deployment that can REBUILD the same-semantics constraint chain (its policies are
* config-driven) passes it here; the drive threads it into `resumeStream(..., internals)`,
* where the recorded-count shape check still applies verbatim (a partial/mismatched re-supply
* is rejected — this seat never widens, it only reopens the documented re-supply channel to
* the parked-decide lane). Absent ⇒ exactly the previous behavior.
*
* One entry kind is NOT a policy the deployment authored: a hook-wired parent contributes a
* PreToolUse SCREENING entry (issue #33), whose policy the engine mints. Rebuild it with the
* exported `createPreToolUseConstraintPolicy(hook, env)` — same callback, same installed env —
* or the chain handed back here will be short and rejected on the count check. A face that only
* observes should be declared `{ observe: true }` instead, and then never enters the chain
* at all; a park under a screening entry announces the requirement on `onError`
* (classification `screening-constraint-in-durable-chain`) at park time.
*/
inheritedGate?: import("./runner/contracts.js").InheritedGate;
};
/**
* design/176 — the peer-chain SEED for a message-driven (tier-3) revival: the hop chain of the
* LAST message in the leased mailbox batch (upstream's "most recent peer-origin message" rule).
* The spawn chain plants it as the revived cycle's initial inbound chain, so a woken agent's
* first forward continues the chain that woke it (delivery itself never appends — a revival is
* not a hop). Absent ⇒ the revived cycle starts with an empty chain.
*/
peerSeed?: {
hopChain: string[];
};
};
/**
* design/147 S1 — the explicit name THIS run was spawned under (`Agent({name})`), when any
* (trusted internals chain, same seat as {@link parentTaskId}). A named child is sema's teammate:
* the CC hierarchy clamp ("teammates cannot spawn other teammates") keys on exactly this.
*/
spawnedAgentName?: string;
/**
* design/176 — THIS run's late-bound peer-identity carrier (Runner-filled trusted seat, same
* chain as {@link parentTaskId}; never a model argument). Delegation tools pair it with the
* child's uplink binding (`RunInternals.parentPeerRef`) so the child's "main" sends are judged
* against the identity the callback actually reaches; SendMessage reads it for its own sender
* key and hop token.
*/
peerSelfRef?: import("../agents/peer-admission.js").PeerSelfRef;
/**
* design/176 — THIS run's inbound peer-chain ref (Runner-filled trusted seat, written at the
* harness consumption boundary): the hop chain of the most recently CONSUMED peer message, which
* outbound SendMessage calls inherit (upstream `Zbr` semantics on sema's carrier).
*/
peerInboundChainRef?: import("../agents/peer-admission.js").PeerInboundChainRef;
/**
* design/147 S1c — the deployment's durable roster ({@link RunnerDeps.rosterStore}), Runner-filled
* read-only. The Agent tool records named background spawns here (advisory try/catch — a store
* fault never fails the spawn); never a model argument.
*/
roster?: import("../agents/roster-store.js").RosterStore;
/**
* fork source-store fix — the HOST Runner's session-fork face, Runner-filled read-only.
* The fork arm's source session lives in the store of the Runner RUNNING this task; `opts.runner`
* is the CHILD-EXECUTION runner, which a deployment may deliberately split (server's subRunner) —
* forking through it queried an empty store and every fork missed ("source session not found").
* The fork arm prefers this face and falls back to `opts.runner.sessions.fork` (single-runner
* deployments unchanged).
*
* ZERO-ARG by design: the source session id AND principal are bound in the Runner's
* closure at injection — a mounted third-party tool holding this context can only branch THIS task's
* own session under THIS task's principal, never copy a foreign session by supplying its id. The
* returned handle's `release` is scoped to the just-created branch through the same closure, so
* failure cleanup releases through the store that OWNS the branch (`opts.runner.sessions.release`
* may be a different store entirely).
*
* Store contract: the branch is created in the HOST store, but the fork child still
* executes on `opts.runner` with `requireExistingSession: true` — if the two runners do not share
* session backing, the child run fails LOUD ("refusing a silent fresh run") instead of silently
* running an empty session under the forked id. Split-runner deployments must share session backing
* (same store instance or same durable backend) for fork to complete.
*/
hostSessionFork?: () => Promise<{
sessionId: string;
release: () => Promise<void>;
} | null>;
/**
* The RUNNING task's effective working root (worktree-aware), Runner-filled — read-only context a
* delegation tool threads into its child's trusted internals so the child's env can inherit the
* parent's cwd (CC parity: a Task sub-agent works in the main session's directory). Undefined when
* the tool runs outside a Runner task.
*/
parentCwd?: string;
/**
* The RUNNING task's file-history LINEAGE (Runner-filled, read-only, never a model/tool argument;
* present iff the run records file history — a `RunnerDeps.fileHistoryStore` wired, capture not `"off"`, not `"disabled"`): the history scope this run
* records its first-touch edits into and the TREE those records' keys are minted against — the
* canonical root spelling plus the env's filesystem identity (`fs`: a remote workspace's handle,
* `"host"` for the control-plane host's filesystem, else a per-env-instance token).
*
* WHICH children are threaded it, exactly: the Agent tool's FRESH spawn legs (sync / steer /
* background / fork) pass it VERBATIM into the child's trusted internals
* (`RunInternals.fileHistoryLineage`), which is how a same-process delegation tree keeps ONE
* history: a child on the SAME tree (both coordinates equal) records into the lineage's scope (the
* root session's), so the root session's `rewindFilesTo` reaches the child's edits; a child on a
* different tree (worktree isolation, an explicit `cwd`, a fresh per-task sandbox) starts its own
* lineage, because root-relative keys re-joined against a different tree would name files that run
* never edited. Two delegated legs deliberately carry NO lineage and keep their own scope: the
* Agent tool's REVIVE arm (a revival's lineage would be the REVIVER's tree, which says nothing
* about the revived row's), and the workflow orchestration spawn leg (`run_workflow` — it builds its
* children's internals without this seat). Edits made by
* those children are therefore NOT reachable from the root session's rewind, and a restore request
* on one of them is not refused either — it converges that child's own scope.
*/
fileHistoryLineage?: {
scope: string;
root: string;
fs: string;
};
/**
* design/319 (A ticket) — the RUNNING task's reminder provenance mark, Runner-filled, read-only,
* NEVER a model/tool argument. The Agent tool's FORK route threads it into the forked child's
* trusted internals (`RunInternals.reminderMark` — the fork runs under the parent's byte-identical
* system-prompt declaration, so its engine mints must carry the PARENT's mark); spawn routes
* deliberately do NOT copy it (a spawned context mints its own — "one declaration, one mark").
* Undefined when the tool runs outside a Runner task.
*/
reminderMark?: string;
/**
* design/319 (B ticket) — the RUNNING task's reminder-disclosure trigger counters
* ({@link import("./reminder-disclosure.js").ReminderDisclosureCounts}), Runner-filled on the same
* trusted seat as {@link reminderMark}. Caller-mounted verbatim outlets that run the
* detect-and-disclose pipeline (the web tools' exact-mark defuse arm) bump their `<outlet>.<form>`
* keys here; the Runner folds the non-zero result into
* `TaskResult.stats.mechanisms.reminderDisclosures`. Mutable by design (a counter seat), but only
* ever additive — never a decision input. Undefined outside a Runner task.
*/
reminderDisclosureCounts?: import("./reminder-disclosure.js").ReminderDisclosureCounts;
/**
* Subagent transcript persistence — the RESOLVED delegation entry caps for this run
* ({@link RunnerDeps.delegationEntryCaps} after prepare's loud validation; both members always
* present). Runner-filled trusted seat, never a model argument — the Agent tool's spawn gate reads
* it on EVERY local lane (background, background-fork, sync fork, sync spawn), each at its own
* launch point. Undefined outside a Runner task (the lane then applies the
* exported defaults itself, so a directly-driven tool is bounded too).
*/
delegationEntryCaps?: {
maxConcurrent: number;
maxCumulativePerSession: number;
};
/**
* Report usage spent in a nested run this tool spawned (e.g. a sub-agent). The Runner accumulates
* it into the parent task's `TaskResult.stats.nested`, so delegated cost — the multi-agent "~15×"
* — is visible at the top level. `createSubagentTool` calls this automatically.
*/
reportUsage?: (usage: NestedUsage) => void;
/**
* The model the PARENT task is currently running on, **snapshotted at the moment this tool is called**
* (design/38 1B). A delegation tool reads it to let a sub-agent `inherit` the caller's model. If the
* parent later degrades, an already-started child is unaffected (it kept the snapshot). Undefined when
* the tool runs outside a Runner task.
*/
model?: Model;
/**
* WHICH REF chose {@link model} — the catalog word the parent leg is running under, or the `Model`
* object itself when no word exists (an object-pinned spec, a `select`-criteria role). Snapshotted at
* the same call-time instant as {@link model}, and its companion in the same sense {@link thinkingLevel}
* is: one read, one moment.
*
* A delegation tool inheriting the caller's model reads THIS, not {@link model}: a word resolves against
* the catalog generation in force at the CHILD's own spawn, so a child spawned after a catalog hot-swap
* lands on the current generation instead of on the object its parent has been holding since its own
* prepare (and a word the swap retired refuses loudly, exactly as a caller-written retired word does).
* Undefined when the tool runs outside a Runner task — the same absence {@link model} has there.
*/
modelRef?: ModelRef;
/**
* The PARENT task's current thinking level, snapshotted at call time — the {@link model} companion.
* A delegation tool threads it into the child so a sub-agent inherits the caller's thinking depth by
* default (an explicit agent-definition/script `thinking` wins). Undefined outside a Runner task.
*/
thinkingLevel?: ThinkingLevel;
/**
* The running task's authenticated end-user {@link TaskSpec.principal} (design/62), **read-only** — the
* Runner fills it; a tool cannot change it. A delegation tool (`createSubagentTool`) propagates it
* verbatim to its child task so the end-user identity is inherited down the delegation tree (the worker
* cannot substitute a different principal — it is not a tool argument). Undefined when the task has no
* principal or the tool runs outside a Runner task.
*/
principal?: string;
/**
* ruled 2026-08-04 — the RUNNING task's EFFECTIVE synchronous approver (`spec.onAsk ?? deps.onAsk`,
* the exact value this task's own gate resolves an `ask` at), Runner-filled read-only on the same
* trusted seat as {@link principal}: never a model/tool argument, so a worker can neither substitute
* nor suppress it. Absent when the run has no approver at all (headless) — the seat is never caller-declared.
*
* A delegation tool (`createSubagentTool`) forwards it into every child it spawns (sync / background /
* fork), wrapped so the approver learns which delegation raised the ask ({@link
* import("./tool-policy.js").AskDelegationProvenance}); the workflow lane forwards the SAME frozen
* value into every workflow-spawned agent (backlog #342: `RunWorkflowToolDeps.parentOnAsk`, folded
* onto the governed baseline's base slot with the same provenance wrapper — the auto-mounted tool's
* execute ctx is minimal, so the value rides a mount dep there; a deployment-pinned `base.onAsk`
* wins). Without this seat, a deployment that wires its
* approver per TASK (one closure per attached connection — the common shape) had it reach the host run
* only: the child's own asks resolved at `deps.onAsk`, i.e. the headless auto-deny, while a live
* operator sat attached to the parent. The ancestor-constraint chain
* ({@link inheritedGateForChildren}) already routed asks raised by an ANCESTOR's policy to that
* ancestor's frozen approver; this seat covers the asks the chain never sees — the child's own
* egress/irreversibility safety tightens, its session-rule asks, its hooks.
*
* Not a widening of what may run: an ask still has to be answered, and every ancestor constraint is
* folded first. It replaces "silently denied without anyone being asked" with "asked".
*/
onAsk?: import("./tool-policy.js").OnAsk;
/**
* The RUNNING task's EFFECTIVE content-ask seam (`spec.onQuestion ?? deps.onQuestion` — the exact
* value this task's own `AskUserQuestion` resolves at), Runner-filled read-only on the same trusted
* seat as {@link onAsk}: never a model/tool argument, so a worker can neither substitute nor suppress
* it. Absent when the run has no live question face at all — the seat is never caller-declared.
*
* A delegation tool (`createSubagentTool`) forwards it into every child it spawns (sync / background /
* fork), because the tool's own MOUNT predicate is keyed on it: without the seat, a deployment that
* wires its question face per TASK (`spec.onQuestion`, one closure per attached connection — the same
* shape {@link onAsk} covers for permissions) gave the child neither the face NOR the tool. The child
* did not fall back to the headless "no human available" text; `AskUserQuestion` was simply absent
* from its roster, so a question it needed to ask silently became a guess.
*
* Forwarded VERBATIM, unlike `onAsk`'s provenance-stamped wrapper: a content ask carries its issuing
* identity in the request itself (`AskQuestionRequest.principal` + `sourceTaskId`, which the child's
* own prepare fills from the child's inherited principal and its Runner-minted session id), and
* nothing in the question path does approver-identity collapse (the reason the permission wrapper
* exists). Wrapping here would add a field no consumer reads and a second function identity for one
* decision-maker.
*
* Not a widening: a question still has to be answered by the human the deployment wired, and the
* child's questions were never anyone else's to answer.
*/
onQuestion?: import("./ask-question.js").OnQuestion;
/**
* The running task's WRITE-HANDS CLAMP ({@link TaskSpec.handsReadOnly} — the read-only band the
* Runner mounts instead of the write hands), Runner-filled read-only on the same trusted seat as
* {@link principal}; present ONLY when the clamp is on, so an ordinary run's ctx gains no key.
*
* A delegation tool (`createSubagentTool`) copies it onto every child spec it builds: the clamp is a
* tighten-only safety axis (`tightenTaskSpec`: `true → false` is a loosening that throws), but the
* delegation lane never carried it, so a clamped parent could derive a FULL-WRITE child and the
* read-only boundary ended at one level. Strictly a capability REMOVAL — it can never widen a child.
*/
handsReadOnly?: true;
/**
* design/199 — the READ-face clamp carrier ({@link TaskSpec.readFace}): present (as "roots") ONLY
* when this run RESOLVED to the roots face, so a delegation tool pins every child spec to roots —
* without it a roots-narrowed parent's children would resolve the shared deployment seat (possibly
* "open") and read wider than the parent could. Strictly a tightening carrier: an open parent adds
* no key, and nothing in the delegation lane can spell "open". F10 ruling (v1): this seat exists
* ONLY as the delegation carrier — no tool adapts its model-facing behavior on it.
*/
readFace?: "roots";
/**
* The WRITE-face clamp carrier ({@link TaskSpec.writeFace}), the read carrier's twin: present (as
* "roots") ONLY when this run RESOLVED to the roots write face, so a delegation tool pins every
* child spec to roots — without it a roots-narrowed parent's children would resolve the shared
* deployment seat (possibly "open") and write wider than the parent could. Strictly a tightening
* carrier, and independent of {@link readFace}.
*/
writeFace?: "roots";
/**
* design/199 件B — the parent task's OWN deny-set additions ({@link TaskSpec.readDenyPatterns}),
* traveling the delegation tree add-only (a child judges at least every entry its parent judged).
* Runner-filled trusted seat, never a model argument.
*/
readDenyPatterns?: readonly import("../tools/fs/read-deny.js").ReadDenyEntry[];
/**
* The parent task's OWN write-protection additions ({@link TaskSpec.writeProtectedAdditions}), traveling the delegation
* tree add-only (a child guards at least every row its parent's request added). Runner-filled trusted seat, never a
* model argument.
*/
writeProtectedAdditions?: readonly import("./write-protect.js").WriteProtectedEntry[];
/**
* The parent task's OWN compliance deny additions ({@link TaskSpec.complianceAdditionalDenies}), traveling the
* delegation tree add-only (a child denies at least every capability its parent's request denied). Runner-filled
* trusted seat, never a model argument.
*/
complianceAdditionalDenies?: readonly import("./compliance.js").ComplianceCapability[];
/**
* The HARD-HEADLESS clamp ({@link TaskSpec.interactiveTools} set to `false` — "never mount a
* human-facing tool on this run, whatever faces exist"), Runner-filled read-only on the same trusted
* seat as {@link principal}; present ONLY when the clamp is on, so every other run's ctx gains no key.
*
* A delegation tool copies it onto every child spec, for the same reason as {@link handsReadOnly}: a
* clamp that stops at one level is not a clamp. Without it a child of a hard-headless parent both
* inherits the live {@link onQuestion} face and has no clamp of its own, so prepare's automatic
* criterion mounts `AskUserQuestion` in the child — re-opening in the subtree exactly the interaction
* channel the task disabled. Only the `false` value travels (a forced-mount `true` is a widening and
* is deliberately NOT inherited).
*/
interactiveTools?: false;
/**
* The run's declaration that nobody on it can be asked to approve ({@link TaskSpec.approvalReach} `"none"`),
* Runner-filled read-only on the same trusted seat, present ONLY when declared. A delegation tool copies it onto every
* child spec (the clamp carrier): the child's own seat resolution would otherwise fall back to the deployment's live
* approver, because a declared-none parent hands down no approver of its own ({@link onAsk} is absent on its ctx).
*/
approvalReach?: "none";
/**
* Read-only mirror of {@link "./types.js".TaskSpec.oneShot} (same trust tier/seat as
* `principal`): this RUN has no later turn for an async background notification to land in (a
* headless `-p` process exits once the turn ends). A delegation tool (`createSubagentTool`) reads
* it to branch its background-launch RECEIPT toward an active block-wait instruction instead of
* "end your turn and wait" — the Workflow tool branches its launch receipt the same way off the seat it is
* mounted with (`RunWorkflowToolDeps.parentSpecSeat.oneShot`). The CHILDREN's copy rides {@link parentSpecSeat}
* (only `true` travels there). Undefined when the tool runs outside a Runner task.
*/
oneShot?: boolean;
/** Does this run mount THIS instance in `spec.tools` as handed, pre-gate (Runner-filled)? The parent bound on `SubagentToolOptions.inheritedTools`. */
mountsCallerTool?: (tool: ToolSpec) => boolean;
/**
* The MCP surface THIS run mounts (Runner-filled, read-only, answered at call time off the live roster): per declared server,
* the tools of it mounted right now, and the aggregate tools no server owns. A delegation tool reads it AT SPAWN TIME for the
* protocol half of the one universe a child's lists govern, and declares the child the servers the lists leave — the child
* connects for itself (it outlives the spawning leg when it runs in the background or is kept for a later turn). Absent when
* the tool runs outside a Runner task, and for a run that declared no server.
*/
mountedMcp?: () => import("./mounted-mcp.js").MountedMcp;
/** The run's compiled tool selector (its `allowTools` / `excludeTools` under the ceiling it was spawned with): `admits` answers
* "may this run use this tool", and `layers` is the ceiling every child this run spawns inherits, so a scenario-wide roster cannot
* be escaped by delegating to a child that remounts a tool this run may not use (Runner-filled, read-only, never a model argument). */
toolSelector?: import("./tool-face.js").ToolSelector;
/** The parent's deferred-disclosure list ({@link TaskSpec.deferTools}), inherited down the delegation tree like `principal`
* (Runner-filled, read-only). */
deferTools?: readonly string[];
/** DD-3 — the INLINE-PIN half of the same tool-face control ({@link TaskSpec.alwaysLoadTools}),
* inherited on the same seat and by the same rule as {@link deferTools} (Runner-filled, read-only).
* The two are one decision expressed in two lists: "defer this batch, but keep X inlined". Passing
* only the deferral half down the delegation tree silently rewrites the scenario for every child —
* X arrives deferred, the exemption having been dropped in transit — which is the one outcome the
* parent explicitly ruled out. Children merge this with their own spec value. */
alwaysLoadTools?: readonly string[];
/** design/277 — the model-gate restore selector ({@link TaskSpec.restoreGatedTools}), inherited
* down the delegation tree on the same trusted seat as the three tool-face controls above
* (Runner-filled, read-only, frozen snapshot). Restoration is a TASK-TREE intent ("this work
* wants the scaffold back") and each child re-judges the gate under its OWN resolved model —
* dropping the selector in transit would trim a same-model child's roster in a way nobody
* chose (the gate is a default, not a policy; there is no tighten-only axis to protect). */
restoreGatedTools?: readonly string[] | true;
/** R2 双形轴 — the parent's resolved prompt profile, inherited down the delegation tree like the
* tool-face controls (Runner-filled): a classic-profile parent's children speak classic too
* unless the child spec says otherwise (child spec wins — profile is presentation, not policy). */
promptProfile?: "simple" | "classic";
/**
* The parent SPEC seat this run hands its children ({@link import("./parent-spec-seat.js").ParentSpecSeat}):
* `principal`, `oneShot` (only `true`), `clientContext` (the user's TZ/email facts), `onQuestion` (the FROZEN
* question face — `AskUserQuestion` mounts on it), `additionalDirectories` / `additionalReadDirectories` (the
* fs-fence widening; the read-only half stays read-only in transit), `envFacts` (the sandbox facts), and the
* DECLARED `memoryPersistenceCapable`. Runner-filled, read-only, NEVER a model/tool argument; minted ONCE per
* prepare from the run's rebound spec + frozen question face (arrays and the facts object frozen copies). ABSENT
* when the tool runs outside a Runner task.
*
* A delegation tool applies it onto every child spec it builds with `applyParentSpecSeat` — the ONE law both
* delegation lanes (Task and Workflow) share: a value the child spec already carries WINS (an agent definition's
* own declaration, a deployment baseline, a trusted script), the seat FILLS an unset one, and the two REMOVAL
* values (`oneShot: true`, `memoryPersistenceCapable: false`) land unconditionally. A deployment that widened the
* fs fence widened it for the TASK, a per-request question face is the child's face too, a one-shot process is
* one-shot for the whole tree — none of that stops at one level.
*/
parentSpecSeat?: import("./parent-spec-seat.js").ParentSpecSeat;
/** This run's per-request question face was STRIPPED by the engine (`RunInternals.questionFaceStripped` — the leg outlives
* the request that spawned it), Runner-filled read-only. A delegation tool forwards it onto every descendant's internals:
* the descendants are faceless by the same engine act, so the posture door's exemption travels with them. */
questionFaceStripped?: true;
/**
* The parent task's DECLARED {@link TaskSpec.memoryPersistenceCapable} as THIS run's own fact — present ONLY when
* the spec set it (an inferred run's ctx gains no key), Runner-filled on the same trusted seat as {@link principal}:
* never a model/tool argument. Read by the revival spawner as the waking run's own clamp (a declared `false` forces
* the read-only-memory disclosure onto the cycle it wakes). The CHILDREN's copy rides {@link parentSpecSeat}.
*/
memoryPersistenceCapable?: boolean;
/**
* design/383 §2.5 — TRUE ⇔ the spawning session is under a memory-capture OPT-OUT at the moment
* a delegation tool reads this seat (declared at its prepare, a standing record, or a mid-run
* flip — the seat is live, not a prepare-time snapshot). The delegation lanes forward it into
* every child's trusted internals as the capture FLOOR (`memoryCaptureFloor`): a child of an
* opted-out session captures nothing, whatever its chosen AgentDefinition says — the opt-out is
* a floor no selection loosens (the `memoryPersistenceCapable:false` floor's exact law, on the
* privacy axis). Trusted Runner-filled seat, never a model/tool argument.
*
* DUAL FORM (#511 件2): on a deployment whose capture record store is synchronous (the default
* file trio) this is a plain boolean, byte-identical to before; a Promise-form
* {@link RunnerDeps.memoryCaptureRecordStore} makes the live read answer a `Promise<boolean>`.
* Consumers must `await` (identity on the boolean arm) — a bare `=== true` on the Promise arm
* would coin `false`, the exact un-floored escape this seat closes.
*/
memoryCaptureOptedOut?: boolean | Promise<boolean>;
/**
* design/383 §2.5 — the floor seat's THIRD state: TRUE ⇔ the spawning
* session's capture opt-out state is INDETERMINATE at the moment a delegation tool reads this
* seat (its capture record store is faulting, and no genuine record is known in-process). The
* delegation lanes forward it into the child's trusted internals as the floor's indeterminate
* twin (`memoryCaptureFloorIndeterminate`): the child's prepare re-resolves it against the live
* record query — an ancestor record found takes the ordinary floor arm; a still-faulting store
* runs the child under the same fail-closed suppression an own-store outage imposes (no
* irreversible record is ever minted off an unreadable state); a readable-and-clean answer
* proceeds clean. Never TRUE beside {@link memoryCaptureOptedOut} — a known opt-out is
* determinate. Trusted Runner-filled seat, never a model/tool argument.
* Dual form like its twin (#511 件2): `boolean` over a sync store, `Promise<boolean>` over a
* Promise-form store — consumers `await`.
*/
memoryCaptureIndeterminate?: boolean | Promise<boolean>;
/** design/383 §2.5 — the spawning session's write-plane control dir (the coordinate its capture
* record is keyed under), forwarded beside the floor bit so a cross-plane child's record-query
* leg reads the PARENT's carrier, not its own plane's. Trusted Runner-filled seat. */
memoryCaptureControlDir?: string;
/** design/383 §2.5 (codex round 3) — the WHOLE ancestor chain's capture-record coordinates
* ({sessionId, controlDir?} per generation, root first), appended one row per spawn. The
* record-query leg walks ALL of it: a root that flips after spawning an already-running child
* AND grandchild is invisible to the grandchild's one-hop parent query (the clean middle
* session has no record of its own to find), while the chain reaches the root directly.
* Trusted Runner-filled seat, never a model argument. */
memoryCaptureAncestors?: ReadonlyArray<{
sessionId: string;
controlDir?: string;
}>;
/**
* the parent task's per-model auth hook ({@link TaskSpec.getApiKeyAndHeaders}), inherited
* verbatim down the delegation tree like `principal`/`clientContext` (Runner-filled, read-only, NEVER
* a model/tool argument). Without this seat a per-task agent pinned to a cross-provider model (its own
* apiKeyEnv) spawned a child whose run had NO auth hook — the brain silently fell back to the global
* key and the child 401'd (or worse, ran on the wrong account). Undefined when the parent spec carries
* no hook or the tool runs outside a Runner task.
*/
getApiKeyAndHeaders?: TaskSpec["getApiKeyAndHeaders"];
/**
* A read-only CAPTURE of this (parent) task's skill grant rows for the run — the tools its active skills list in
* `allowed-tools` / `disallowed-tools` ({@link import("./skill-grant.js").SkillGrantRecord}: skill, list, entry),
* filled by the Runner. A delegation tool (`createSubagentTool`) reads it AT SPAWN TIME and threads it into the child,
* which holds the rows until it ends. A copy: the parent's later activations never reach it. An empty array = no active
* skill lists (the common case). The model can neither read nor set it (it is not a tool argument). Undefined when the
* tool runs outside a Runner task.
*/
activeSkillGrants?: () => readonly import("./skill-grant.js").SkillGrantRecord[];
/**
* A read-only CAPTURE of this (parent) task's request allow entries for the run ({@link import("./task-spec.js").TaskSpec.allowedTools},
* its own and any it inherited), filled by the Runner beside {@link activeSkillGrants}: a delegation tool reads it AT SPAWN
* TIME and threads it into the child, which holds them as its own request rows until it ends. A copy; the model can
* neither read nor set it. Undefined when the tool runs outside a Runner task.
*/
activeRequestAllowedTools?: () => readonly string[];
/**
* Parent effective-policy inheritance (tighten-only) — the Runner-filled chain accessor a delegation tool
* (`createSubagentTool` / the `run_workflow` spawn legs) calls AT SPAWN TIME and threads verbatim into the
* child's trusted `RunInternals.inheritedGate`. Returns the upstream inherited chain PLUS this (parent)
* task's own contribution: its session-rule snapshot (data half), its RESOLVED caller policy
* (`spec.toolPolicy ?? deps.toolPolicy` — the exact slot this task's own gate enforces) with the frozen
* task onAsk (opaque half), and its effective shellGate. The child's prepare folds these as
* ADDITIONAL constraint layers — inheritance can only narrow a child, never widen it. Runner-filled,
* read-only, NEVER a model/tool argument (same posture as {@link activeSkillGrants}). Undefined when the
* tool runs outside a Runner task.
*/
inheritedGateForChildren?: () => import("./runner/contracts.js").InheritedGate;
/**
* design/180 half A — the delegation runtime-provenance ARMING face. Runner-filled; a delegation
* tool calls it at spawn time: a non-undefined return means this (parent) run is armed (it mounts
* a memory session, or is itself recording for its own parent) and carries the chain's FROZEN
* content-safety snapshot — the tool then mints the child's recorder ref and threads both into
* the child's trusted `RunInternals.delegationProvenance`. Undefined return / absent field ⇒ the
* child spawns without a recorder (its deliveries read `unknown` and every judgment stays on the
* static floor — whose MARK action follows the judging run's deployment evidence standard,
* {@link RunnerDeps.memoryDelegationEvidence}; under the `"static-face"` default this is v1
* behavior byte-identical). Same trust posture as
* {@link inheritedGateForChildren}: never a model/tool argument, never a TaskSpec field.
*/
delegationProvenanceForChildren?: () => import("./memory-engine/delegation-provenance.js").DelegationContentSafety | undefined;
/**
* design/336 §3.3 — the delegation-settlement account's coordinates (the WRITE plane's memory
* control dir + this session's id), Runner-filled when the run mounts a memory session under
* `memoryProvenance: "carry"`. A delegation tool's BACKGROUND lane writes its launch write-ahead
* (pending row BEFORE registration and invoke) and its terminal settle through these — as pure
* data, because the terminal observation can run after this task leg returned and must rebuild
* its write handle from the coordinates alone. Undefined ⇒ no settlement account (memory-less
* run, or `"off"`): the background lane launches without rows, the pre-336 accepted-cost shape.
* Same trust posture as {@link delegationProvenanceForChildren}: never a model/tool argument.
*/
delegationSettlement?: () => {
controlDir: string;
sessionId: string;
} | undefined;
/**
* RB-201 FO-3 (form-one audit, CC 220 `Ipd`/`ein` parity) — the auto-mode classifier decider ARMED
* for THIS task (auto-mode intent ∧ `RunnerDeps.autoMode` present ∧ `RuntimeCaps.autoMode !== false`
* — see {@link TaskSpec.permissionMode}; the same
* instance `runToolGate`'s per-call ask review already consults, carrying its own live breaker
* state — not a fresh one built from raw config). Runner-filled, trusted, undefined when auto-mode
* is not armed or the tool runs outside a Runner task.
*
* CC's classifier covers two more scenes beyond a single tool-call ask that sema had NOT ported: a
* delegation tool reviews the CHILD's prompt+toolset BEFORE spawning it (CC `Ipd` — a main agent
* blocked from a dangerous action directly could otherwise write it into a sub-agent's prompt and
* have the child execute it, a delegation-based classifier circumvention), and reviews the child's COMPLETED
* work when it hands control back (CC `ein` — flags the main agent with a security warning before it
* acts on unreviewed sub-agent output). `createSubagentTool`/the workflow spawn legs read this at
* BOTH points and thread it into the child (never a model/tool argument), same posture as
* {@link inheritedGateForChildren}.
*/
autoModeReview?: {
decider: import("./auto-mode.js").AutoModeDecider;
};
/**
* design/153 §7.4 (件4) — the parent task's `durableApproval` opt-in (value copy), Runner-filled so a
* delegation tool can FORWARD it into an eligible background child's spec (named non-fork child + durable
* row + checkpoint store + `ensureChildSessionDurable` attested — the §7.3 park-eligibility family). With
* it forwarded, a child's plain policy `ask` suspends durably and the settle watcher PARKS the row instead
* of the pre-153 `unexpected.suspended` failure. Tighten-only: forwarding grants the child a PARK
* capability, never a permission widening — the park's resume decision stays with the operator, and the
* ancestor mandate chain (frozen at chain assembly) is unchanged by the child's own spec. Runner-filled,
* read-only, NEVER a model/tool argument. Undefined when the parent did not opt in (children keep the
* pre-153 lifecycle byte-for-byte). Side effect worth noting (件4 复审): forwarding also flips the
* child's `durableQuestionFace` true (a wired checkpoint store + this opt-in), so an eligible child
* additionally mounts `AskUserQuestion` — a deliberate consequence (a question the child can't resolve
* synchronously should park too), not a separate grant.
*/
durableApprovalForChildren?: {
scope: string;
ttlMs?: number;
};
/**
* The host run's RESOLVED checkpoint seat, for its children — the store object the run parks in (the ONE value
* `resolveCheckpointStore(spec, deps)` produced), or the word `"disabled"` when the caller disarmed the run.
* Runner-filled, read-only, NEVER a model/tool argument. ABSENT when the tool runs outside a Runner task and
* when the host has no seat at all (unset spec, no deployment store): nothing to inherit, so the child's own
* door resolves its runner's deployment — a split child-execution runner may carry a store the host has none of.
*
* A delegation tool copies this seat VERBATIM onto every child spec it builds (sync, background and fork
* alike) — one rule: a store the parent resolved at SPEC level reaches the child (a child that re-resolved
* `RunnerDeps.checkpointStore` on its own found no store on a deployment that wires it per task, and its ask
* fell to the fail-closed deny while the parent could park), and `"disabled"` presses on the whole subtree (a
* capability REMOVAL, never re-armed one level down by a deployment store — a machine-started run must not
* park in its subtree). Same discipline as `principal`: inherit verbatim off the trusted seat.
*/
checkpointStoreForChildren?: import("./checkpoint-store.js").CheckpointStore | "disabled";
/**
* The host run's checkpoint SCOPE, for its children — the scope this run's own parked rows land in (the one
* value its prepare derived; a delegated host's is the scope ITS parent handed it, so a whole tree answers
* with the root's). A delegation tool copies it verbatim into every child's `RunInternals.checkpointScope`, so
* a child's parked row is listed where its parent's is, on the synchronous lane as on the background one.
* Scope only: the park capability is {@link durableApprovalForChildren}'s business. Runner-filled, read-only,
* NEVER a model/tool argument; ABSENT when the tool runs outside a Runner task (the child then derives its
* scope from its own spec, as a root run does).
*/
checkpointScopeForChildren?: string;
/**
* design/99 (nested-subagent live observability): the host run's stable taskId (`spec.taskId ?? sessionId`),
* filled by the Runner. A delegation tool (`createSubagentTool`) reads it AT SPAWN TIME and threads it into the
* child as the child's `parentTaskId`, so a UI can build the live nested-agent tree (child.parentTaskId === this
* taskId). Undefined when the tool runs outside a Runner task.
*/
taskId?: string;
/** design/129: the host run's SESSION id (Runner-filled, read-only) — a session-scoped background child
* registers under it so later turns (fresh taskId) still reach the entry. */
sessionId?: string;
/** design/129: the host spec's `backgroundScope`, threaded read-only so delegation tools apply the
* deployment's background-lifetime policy (never a model argument). */
backgroundScope?: "task" | "session";
/**
* design/173 §8.3 (review fold r2-F1) — the host run's RESOLVED interaction posture
* (`spec ?? parent ?? deps`), threaded read-only so delegation/workflow lanes carry it into their
* children's TRUSTED internals (`RunInternals.parentInteractionPosture`). Without this carry, 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. Engine children still never get
* the posture COPIED onto their spec (the §8.3 rule) — it rides internals, the engine channel.
*/
interactionPosture?: "interactive" | "headless";
/** design/129-B: the process-level background-child observer ({@link RunnerDeps.onBackgroundChildEvent}),
* threaded read-only so the Agent tool's background lane reports spawn/tick/terminal for the child's
* whole lifetime (a per-leg sink goes stale when the leg settles; this one never does). */
onBackgroundChildEvent?: (event: BackgroundChildEvent) => void;
/**
* design/115 P3 — the run-local task-notification sink (the SAME lane runtask wires for workflow/bash
* completions). Injected by prepare-task's spec.tools wrapper so a caller-mounted delegation tool can
* fire background-agent completion notifications into the SOURCE task's next turn. TRUSTED, run-scoped.
*/
onTaskNotification?: (n: import("./task-notification.js").TaskNotificationPayload, opts?: {
priority?: import("./task-notification.js").SystemInjectionPriority;
}) => unknown;
/**
* design/99 (nested-subagent live observability): the host run's OPT-IN display sink — a deployment sets it (via
* `RunInternals.onForwardEvent`) to receive a SUBAGENT's live `task_progress` ticks that otherwise stay in the
* child's ISOLATED stream. A delegation tool threads it to the child so nested progress bubbles to one sink. This
* is a DISPLAY channel ONLY — the child stream is NEVER merged into the parent's MODEL context, and nothing
* security-relevant consumes a forwarded event. Present ONLY when the deployment opted in. The tool-ctx wrapper
* passes `task_progress` unconditionally and — when the deployment sets `forwardSubagentEvents: true` — the
* transcript classes too (text_delta/text_end/reasoning_delta/tool_start/tool_end); other event types never cross it.
* The delegation lane's OWN tap is trusted and forwards the child's FULL event stream (bg frames tagged
* with bgAgentId). ⚠️ Forwarded ticks are UNTRUSTED display hints — any
* tool holding this ctx could self-declare one, so a consumer validates `parentTaskId` against its known runs.
*/
forwardEvent?: (event: TaskEvent) => void;
/**
* Subagent steer verb (2026-07-03): the host run's opt-in SUBAGENT-STEER-HANDLE sink
* (threaded from `RunInternals.onSubagentSpawn`). When present, `createSubagentTool` runs each sync
* child via `runTaskStream` and emits a steer handle here; the deployment routes a human steer into
* the running child. The handle never reaches the model. Absent ⇒ children run non-steerable.
*/
onSubagentSpawn?: (handle: import("../agents/subagent.js").SubagentSteerHandle) => void;
/**
* design/122 D1 — the parent run's subagent-retain LEDGER, present ONLY when the parent's
* {@link TaskSpec.retainSubagentSessions} is enabled. TRUSTED run-scoped state the Runner fills (never a
* tool argument): `createSubagentTool` registers each retained child here (pre-minted sessionId + a
* FROZEN plain spec snapshot — never the live ctx/spec-builder closures, r1-m5) and the Runner disposes
* the whole ledger (abort in-flight resumes + unpin + release every retained session) when the parent
* run ends (D4).
*/
subagentRetain?: import("../agents/retain-ledger.js").SubagentRetainLedger;
/**
* design/135 §0 — the TRUSTED per-call worktree-isolation lane for the Agent tool (CC
* `isolation: "worktree"`). The Runner fills it (prepare-task builds it over the run's ExecutionEnv +
* task root via `createSubagentWorktreeHelper`) for write-capable runs; a tool cannot self-declare it and the
* model only REQUESTS isolation via the Agent tool's `isolation` parameter — the capability itself is
* never a model argument. Absent ⇒ the deployment cannot mint worktrees (read-only run / outside a
* Runner) and the Agent tool reports isolation as honestly unavailable.
*/
worktreeIsolation?: import("../agents/subagent.js").SubagentWorktreeIsolation;
/**
* design/135 §0 — TRUE when THIS run is itself a forked child ({@link RunInternals.insideFork}),
* threaded read-only so the Agent tool's built-in `subagent_type:"fork"` route can refuse nested forks
* (CC: "fork is not available inside a forked worker") even for a deployment-mounted Agent tool that
* flowed into the forked child via its tool list. Runner-filled; never a model argument.
*/
insideFork?: boolean;
/**
* OBS-2 (CC 2.1.206 observer parity) — present iff this run's principal resolved
* {@link RuntimeCaps.allowObservers}` === true` (EXPLICIT opt-in, default OFF — see the polarity
* note there). The delegation tool honors {@link AgentDefinition.observer} declarations only under
* this flag; absent ⇒ declarations resolve to unobserved with NO warn (the gate, not a mistake).
* Runner-filled at call time (same closure posture as `forkAccess`); never a model argument.
*/
observersAllowed?: true;
/**
* design/136 §6 盲点① — the Runner-computed fork-GOVERNANCE verdict for THIS run (from
* `forkGovernanceDenial(spec.enableFork, runtimeCaps.allowFork)`), threaded read-only so the Agent
* tool's built-in `subagent_type:"fork"` route enforces the SAME per-task/per-principal authorization
* as the standalone Fork tool's mount gate (pre-1.256, `allowFork:false` only prevented mounting Fork
* while Agent-fork ran ungoverned). Present ONLY when governance DENIES (`denied:"task"` =
* `enableFork:false`; `denied:"principal"` = `runtimeCaps.allowFork:false`) — absent means governance
* does not object (or the tool runs outside a Runner ⇒ capability checks only). Runner-filled; never a
* model argument.
*/
forkAccess?: {
denied: "task" | "principal";
};
/**
* design/80 D-B — the general "a tool yields to a HUMAN review gate" primitive. A tool calls this to request
* that, at the next SAFE turn boundary, the engine pause with a durable `plan_review` checkpoint
* (`status:"needs_review"`) so a human approves/edits/rejects before the run continues. The tool still returns
* its `content` normally first (the plan/diff lives there → reaches the reviewer via the transcript). HONORED
* only when the deployment can actually pause (a `checkpointStore` is wired); otherwise the request is a silent
* no-op (read-only/headless degrade). The first-party `present_plan` tool (CC `ExitPlanMode` parity) is a thin
* caller of this; any tool may use it to gate a high-blast step on human review. Idempotent within a batch (the
* first request wins; the suspend fires once at the boundary). The Runner fills it; not a tool arg.
*/
requestReview?: (opts?: {
reason?: string;
}) => void;
/**
* design/105 — request a CLEAN stop of THIS run after the current turn (NOT an abort: the loop finishes the
* turn, then exits gracefully — no orphan-[INTERRUPTED] reconcile). A thin binding to the harness's existing
* one-way `requestStopAfterTurn` flag (the same the human-suspend saga uses). Typical use: a tool that
* persisted a delayed self-wake intent (e.g. CronCreate with kind:"delay") ends the run cleanly so the
* daemon owns the re-wake ("end now, re-wake" — NOT "suspend-continue", see design/105 §3.4). Idempotent
* one-way flag; a no-op for tools that don't call it. The Runner fills it; not a tool arg. Absent outside a Runner.
*/
requestStopAfterTurn?: () => void;
/**
* Enter plan mode mid-run: a steer of the leg's ONE permission-mode seat into `plan` — the same mode the spec's
* `permissionMode` and `TaskStream.setPermissionMode` set. For the rest of THIS run the mode gate asks before every
* covered write and every call to a tool the engine does not define that is not read-only (the plan sentence; refused
* where nobody can answer), while read tools and `ExitPlanMode` run. The word it left is recorded: an approve-resume
* returns to it; a rejected plan steers the resumed leg back into `plan`. The first-party `EnterPlanMode`
* tool is the only caller. Idempotent. The Runner fills it; not a tool arg. Absent outside a Runner.
*/
enterPlanMode?: () => Promise<void>;
}