@sema-agent/core
Version:
Stateless, task-oriented AI agent core
1,531 lines • 121 kB
TypeScript
/**
* The HOST wiring seat: `RunnerDeps` — every store, sink, hook and policy provider a deployment hands
* the engine — plus the three structured shapes only its own callbacks produce (`ProjectMemoryLoad`,
* `ResumePreflightInfo`, `ResumePreflightVerdict`). Layer 0 vocabulary; `types.ts` re-exports all four,
* so no consumer's import changes.
*/
import type { Model } from "../internal/llm.js";
import type { AgentDefinition } from "./agent-definition.js";
import type { ContextSettings } from "./context-settings.js";
import type { BackgroundChildEvent, DelegationLifecycleEvent } from "./delegation-frames.js";
import type { EngineNotice } from "./engine-notice.js";
import type { HandsBandOptions } from "./hands-band.js";
import type { OnElicit } from "./mcp-server-spec.js";
import type { Brain, ModelRef, ModelRoles } from "./model-seat.js";
import type { RuntimeCaps, WorkflowGovernanceBaseline } from "./runtime-caps.js";
/**
* parity-204 — structured `RunnerDeps.loadProjectMemory` return (backward-compatible: a bare
* `string | null` keeps its exact historical meaning). Adds the CC 2.1.204 `seededFromContext`
* seam (cc204-bundle @17917159: CC preloads readFileState for CLAUDE.md/nested-memory files it
* placed in context, and a DEFAULT whole-file Read of an unchanged seeded file answers with the
* already-in-context reminder instead of re-transmitting the body — token economy, GA in 204).
*/
export interface ProjectMemoryLoad {
/** The project-context text to inject — same semantics as returning a bare string (null/blank ⇒ no injection). */
content: string | null;
/**
* Files whose FULL, disk-verbatim text the deployment has placed in the model's context (e.g. the
* CLAUDE.md that `content` embeds). The Runner pre-seeds the hands' read state (through the same
* resolveKey containment as a real Read — an out-of-root path is silently skipped), so a default
* whole-file Read of an UNCHANGED seeded file returns the CC-verbatim reminder
* (`This file is already in your context (see "Contents of <path>" above) and has not changed on
* disk…`) instead of the body. `content` here MUST be the file's exact disk text at load time —
* the unchanged check hashes it; never declare a truncated/annotated variant (CC's `isPartialView`
* exemption). A file that later changes on disk always reads normally (hash mismatch bypasses the
* dedup and disarms it). Requires hands (an executionEnv); ignored on a hands-less task.
*/
seededFiles?: ReadonlyArray<{
path: string;
content: string;
}>;
/**
* design-A C-3 (AGENTS.md×ingest, [A1]) — the deployment DECLARES which instruction files this
* snapshot was built from and their fingerprints (e.g. `AGENTS.md` / `CLAUDE.md`, hash over the
* raw pre-cap file bytes). Core does NOT interpret the hash algorithm or re-derive it — the pair
* is an OPAQUE comparison baseline (`path` + verbatim `contentHash` equality only), kept so the
* mid-session change probe can answer "did the file behind my snapshot move?" without core
* ever learning a file-name policy (types.ts seam rule: core never hardcodes instruction file
* names). [A2]: this list now FEEDS the C-4 `instructions_change` lane — when the deployment also
* wires {@link RunnerDeps.probeInstructionSources}, the run loop probes these paths at eligible
* turn boundaries and a moved hash is announced via a fixed tail attachment. Optional and inert
* when absent (today's loaders keep their exact behavior; no sources ⇒ the lane never runs).
*/
instructionSources?: ReadonlyArray<{
path: string;
/** (additive widening): `null` = the source is DECLARED but currently
* ABSENT from disk — the change probe then watches for its APPEARANCE (renders "absent → hash").
* Runtime already handled null (collectInstructionsChange's from===null arm); this widening
* only aligns the declared type with the shipped behavior. */
contentHash: string | null;
}>;
}
/**
* design/380 O2 — the facts handed to a deployment's {@link RunnerDeps.resumePreflight}: the row's
* own recorded identity + placement record, read off the persisted checkpoint (never off the resume
* caller's bag), so the hook judges the SAME row the CAS is about to consume.
*/
export interface ResumePreflightInfo {
token: import("./checkpoint-store.js").CheckpointToken;
sessionId: string;
/** design/380 O1③ — the row's recorded placement root, when stamped. */
placementRootSessionId?: string;
/** The row's recorded identity ({@link import("./checkpoint-store.js").Checkpoint.principal}), when stamped —
* the resume entry's identity-continuity rung already refused a contradicting supplied principal
* before this hook runs, so a present value IS the resumed leg's identity. */
principal?: string;
/** provider / deviceId / mountPath — the placement facts. */
workspaceHandle?: import("./remote-env.js").WorkspaceHandle;
gateKind: import("./checkpoint-store.js").CheckpointGate["kind"];
}
/** design/380 O2 — a {@link RunnerDeps.resumePreflight} answer. */
export type ResumePreflightVerdict = {
ok: true;
}
/** Transient obstacle (device offline, dependency briefly down): the checkpoint stays `pending`,
* the SAME token is redeemable later; retryAfterMs threads the wait hint (the #449 G1 carrier). */
| {
ok: false;
disposition?: "retry_later";
message: string;
retryAfterMs?: number;
}
/** PERMANENT obstacle (binding revoked, placement identity gone): the row is settled TERMINALLY —
* core CASes it out of `pending` before answering, so a dead binding cannot
* be redialed forever (prose-terminal + mechanically-retriable = unbounded redeem). The `expire`
* CAS is a pure STATUS flip — the row records no reason (the store seam has no seat for one);
* the REASON travels on the typed `resume.preflight_rejected` refusal (the hook's `message`
* inlined), the `onError` disclosure sink, and the deployment hook's own audit plane — it
* authored the verdict and owns the durable record of why. The exact
* terminal CAS form is the single-shot `expire` CAS (reaper parity — exactly one settler wins; a
* LOST race is re-read and reported as concurrent movement, never claimed as this refusal's
* settle); the CONTRACT is: terminal verdict ⇒ atomic single-shot settle, never a silent
* pending-forever. PHYSICAL reclamation of a suspended workspace stays with the DEPLOYMENT that
* ruled the binding dead — core holds no env pre-restore and a factory cannot reconnect to a
* revoked target; the session pin follows the store contract's documented reap posture. Trust
* grant is acceptable: the hook lives on RunnerDeps, the same deployment plane that owns the
* checkpoint store itself. */
| {
ok: false;
disposition: "terminal";
message: string;
};
/** Runtime dependencies shared across tasks. */
export interface RunnerDeps {
brain: Brain;
/** Catalog used to resolve string ModelRefs to Model objects. */
models?: Record<string, Model>;
/**
* The deployment's per-model CONTEXT-WINDOW overlay, keyed by CATALOG WORD ({@link ContextSettings}):
* `contextTokens` / `autoCompactTokens` / `enabled` over what the model card declares.
*
* It is the ONE hot-swappable seat an IN-FLIGHT leg re-reads. Every other seat follows the snapshot law
* (a running task keeps the deps object it prepared under) because those seats are the routing card —
* endpoint, credentials, price, fence — and a request in flight must reach what it was priced for. The
* usable window is not the card: it is a setting, and an operator who lowers it is asking the legs
* running RIGHT NOW to compact sooner, which is exactly what they were unable to do while the number
* lived on the model object. Read at every turn boundary, every recovery pass, the end-of-task summary
* and every request build; clamped to the physical window by the one resolver.
*
* A leg that was handed a `Model` OBJECT (or resolved one through a `select` criteria) has no catalog
* word and therefore no overlay — the pin is the caller saying "this exact model, as declared".
*/
contextSettings?: ContextSettings;
/**
* design/338 (mid-turn MCP revocation) — the HOST's revocation ledger, probed synchronously at
* every MCP dispatch (tool call + the three resource tools) BEFORE the transport. The engine
* never caches the answer: the ledger's one authority lives on the host (a cached copy would be
* a split-state second authority). A revoked server's calls settle as the coded refusal
* `mcp.server_revoked` with known-not-executed wording; in-flight calls a revocation raced are
* deliberately not chased (the threat shape is "new calls after removal"). Absent seat = the
* pre-338 semantics. A THROWING probe fails open (revocation is a tightening face — a broken
* probe must not brick every MCP call) with a once-per-MATERIALIZATION `mcp.revocation_probe_failed`
* notice (a resume re-materializes and may announce again — the standing condition is re-news at
* each fresh mount, never per-call). The notice is a deployment wiring fact: `detail: { message }`
* only, no session attribution, `"operator"` audience by its explicit {@link NOTICE_AUDIENCE}
* row (#433 made the registry total over the catalog — no engine-minted code is
* audience-defaulted any more) — a
* wire projector forwards it operator-tier and needs no per-session de-duplication of its own.
* The probe's second argument, `{ fingerprint }`, is passed exactly when the server's declaration carries one.
*/
mcpRevocations?: import("./mcp-server-spec.js").McpRevocationLedger;
/**
* design/147 S1c (ruled 2026-07-18) — the DURABLE name→agent roster behind explicit-name
* addressing, a storage-tier seam like the checkpoint store: core bundles `MemoryRosterStore`
* (default when omitted... none — omitted means NO durable roster; the in-memory task registry
* still resolves live/recent names exactly as before) and `FileRosterStore`; a server deployment
* plugs pg/tidb behind the same interface. Named background-agent spawns are recorded here
* (advisory — a throwing store never faults a spawn), and SendMessage consults it AFTER the live
* registry misses, re-applying the caller's access axes (owner/scope) before disclosing anything.
*/
rosterStore?: import("../agents/roster-store.js").RosterStore;
/**
* The center prompt-artifact source (artifact v3 only — a v2 body is refused `schema`, INTEGRATION-CORE §12.11).
* `current()` supplies the verified
* candidate a top-level session adopts at session start and at a compaction; `get(digest)` resolves a PINNED center
* layer on resume (a miss is fail-loud `prompt.snapshot_unavailable` — core never recomposes a hybrid from newer
* content; a copy the engine refuses is told apart from a missing one, in the failure's sentence and on `onError`);
* `sourceState()` is the tri-state every adoption decision keys on. What a source hands a run is re-checked
* before it is adopted. Core bundles `CenterPromptSource` (+ memory/file artifact & source-state stores); a server
* feeds it published artifacts via `applySnapshot`. Omitted = no center layer.
*/
promptSource?: import("../prompt-assembly/artifact-store.js").PromptArtifactSource;
/**
* 模型档位绑定(2026-07-11):sema 档位词(`max`/`pro`/`flash`/`lite`,`ultra` 预留)→ 具体模型
* (catalog 名或 Model)。Runner 构造时经 `expandTiers` 增广进 catalog,使档位词与 CC alias
* (`fable`/`mythos`/`opus`/`sonnet`/`haiku`,内置固定映射)在全部模型寻址面直接可用(TaskSpec.model /
* roles / Agent 工具 model / workflow agent() / `@model` mention)。无绑定档沿 DEFAULT_TIER_ORDER 降档;
* 与部署 catalog 同名键时 catalog WINS(SHADOW)。center 经配置域下发/覆写此表。
*/
tiers?: Record<string, ModelRef>;
/**
* F4 agentType 注册表(2026-07-11):部署声明的 {@link AgentDefinition} 集,workflow `agent(…,{agentType})`
* 经此解析(内置 Explore/Plan 之上 SHADOW)。design/141 件A:Agent 工具 lane(`createSubagentTool`)未显式
* 传 `agents` 时经 `Runner.agentCatalog` 自动取此表——同一 Runner 两 lane 单源;显式传参仍 WINS。
*/
agents?: AgentDefinition[];
/** `false` 从 workflow agentType 注册表移除内置 Explore/Plan(镜像 createSubagentTool 同名开关)。 */
builtinAgents?: boolean;
/**
* design/141 件2 — hands 工具带(Read/Write/Edit/Bash…)的部署方窄配置面。刻意不导出
* `createHandsToolkit` 整厂(部署自装配塞 `spec.tools` 会被 Runner 的自动装配同名覆盖=反模式;
* CC 亦无 options-bag 开放面,走单例+hook)。只开放安全子集,Runner 装配时合成:
* - `beforeWrite` 与引擎的 MemoryEngine 写扫描门 **COMPOSE 而非替换**(部署 hook 先判,拒即拒;
* 放行后引擎门仍全量生效——写保护不可能被部署配置规避)。
* Runner 私有编排字段(taskRegistry/detachHub/execClamp/cwdRef/…)不在此面,永不开放。
*/
hands?: HandsBandOptions;
/**
* design/199 件B — DEPLOYMENT-layer ADDITIONS to the built-in sensitive-path READ deny set
* ({@link import("../tools/fs/read-deny.js").READ_FACE_DEFAULT_DENY_ENTRIES}); see
* {@link TaskSpec.readDenyPatterns} for the judged surfaces. INDEPENDENT of the built-in switch. Add-only on
* THIS seat: unions with the active built-ins and any task-layer additions, and `[]` ≡ absent — narrowing the built-in
* set is the sibling knobs' job ({@link readDenyBuiltinTiers} / {@link readDenyBuiltinExclude},
* #245 revision of D-4), not a replacement escape hatch here. Bad entry shapes refuse loudly at
* prepare (#123).
*/
readDenyPatterns?: readonly import("../tools/fs/read-deny.js").ReadDenyEntry[];
/**
* The deployment's built-in deny-table TIER selection — the switch for the built-in table. EXACTLY the
* listed tiers of {@link import("../tools/fs/read-deny.js").READ_FACE_BUILTIN_DENY_TABLE} are active (`[]` =
* none — explicit and legal: nothing from that table refuses a read); ABSENT =
* {@link import("../tools/fs/read-deny.js").READ_DENY_DEFAULT_TIERS} (`credentials` and the home-anchored
* `agent-config`). A leg's active tiers are REPORTED on its wiring manifest
* ({@link import("./wiring-manifest.js").WiringManifest.readDeny}). Composes with
* {@link readDenyBuiltinExclude} (tiers first, then row removal); never touches the additions
* seats or the write faces. Unknown tier names refuse loudly at prepare (#123). DEPLOYMENT seat
* ONLY: deliberately not a TaskSpec key and not in the governed workflow whitelist — a task or
* governed script can only ADD deny entries, never widen the built-in face below its deployment.
* Not frozen into checkpoints: the checkpoint face section persists ADDITIONS only, so a resumed
* row's built-in face follows the CURRENT deployment configuration (the operator's live
* authority), while frozen additions stay stricter-wins.
*/
readDenyBuiltinTiers?: readonly string[];
/**
* Per-row removal from the built-in deny table by STABLE NAME (= the row's canonical
* pattern text, e.g. `".ssh"`, `".config/gcloud"`; the admin delete channel), applied after
* {@link readDenyBuiltinTiers} selection. Naming a row of an inactive tier is a satisfied intent
* (no-op); a name matching NO table row refuses loudly at prepare (#123 — never silently
* ignored). Same deployment-only seat, clamp argument and checkpoint posture as the tiers key.
*/
readDenyBuiltinExclude?: readonly string[];
/**
* Where the host keeps the agent's configuration directory when it is not `<home>/.sema` — an ABSOLUTE path of an
* existing directory, resolved by the host (the engine reads no environment variable for it). The rows of the
* `agent-config` tier that name that directory (`~/.sema/settings.json`, `~/.sema/settings.local.json`,
* `~/.sema/engine-data/.env`, `~/.sema/config-lkg-*.json`) are then ALSO placed at the same files below this root, so the model can neither read
* nor (through the write-protection table and a wired `createSensitivePathPolicy({ agentConfigDir })`) rewrite the
* relocated settings; the default `~/.sema` location stays refused. Rows of the home's dotted family (`~/.sema.*`)
* and the external tools' files are not part of the directory and stay anchored at the home. A symlink the root is
* reached through is followed (the target is guarded too). ABSENT = today's behaviour, the literal `~/.sema` under
* the declared and account homes. A value that is not a string, is empty or relative, or names no existing directory
* refuses `new Runner` with `config.agent_config_dir_invalid` — never read as absent. DEPLOYMENT seat only (no
* TaskSpec twin) and fixed at construction: `swapDeps` does not carry it.
*/
agentConfigDir?: string;
/**
* backlog #286 (#279 — CC 2.1.233 `DANGEROUS_FILES`/`DANGEROUS_DIRECTORIES`/
* `DANGEROUS_DIRECTORY_PATHS` parity): the WRITE-protection table. DEFAULT-ON: a path-confinable
* write (Write/Edit/NotebookEdit) whose target lands on a table row has a surviving `allow`
* demoted to `ask` at the tool gate (`decisionReason: "safety"`; a deny/ask verdict is untouched;
* approval flows the ordinary ask-resolution chain — classifier, blanket `onAsk`, durable park —
* with no `requiresRealApproval` mandate). Absent =
* {@link import("./write-protect.js").WRITE_PROTECTED_DEFAULT_TABLE} (the CC triple verbatim +
* the two argued sema rows). This key IS the whole-table escape hatch, deployment seat ONLY — the
* task layer's twin is add-only ({@link TaskSpec.writeProtectedAdditions}: its rows UNION this
* table and can remove none), and no governed-workflow channel reaches either: `[]` = no table (explicit and legal); a non-empty
* list REPLACES the built-in table whole (compose additions as
* `[...WRITE_PROTECTED_DEFAULT_TABLE, …]`; drop rows by filtering the exported table — the
* visible/deletable admin face). Bad values refuse loudly at prepare (#123): garbage shapes,
* glob metacharacters (the table speaks LITERAL names — glob semantics live in
* `createSensitivePathPolicy`), unknown kinds, impossible kind/name combinations. Matching is
* lexical over the spelled target with one case fold (ı/ſ included) — a symlink alias evades it
* by construction; the canonicalizing opt-in deny policy remains the hard layer. Not frozen into
* checkpoints: a resumed task follows the CURRENT deployment table (an approved parked call
* bypasses the gate as always — the human already adjudicated it).
*/
writeProtectedPaths?: readonly import("./write-protect.js").WriteProtectedEntry[];
/**
* design/324 (#324 ruling ① containment) — the EVIDENCE STANDARD the delegation arm of the
* content-origin wrap applies when deciding whether a delegation call marks THIS session's memory
* polluted (design/178 §3 / design/180 half A):
* - `"static-face"` (absent ≡ this; the default) — today's behavior, byte-identical: a delegation
* whose delivered attestation is missing/unknown and whose static tool face can reach external
* content marks the session (the capability over-approximation: possibility counts as exposure).
* - `"attested-only"` — exactly that ONE static-face mark is waived, and each prepared leg
* announces the first waiver (`"memory.delegation_static_mark_waived"`). Everything else is
* unchanged: a delivered `"external"` attestation still marks, the chain's `incomplete`
* recording still happens, a non-delegation polluting-class tool still marks, and a delegation
* tool that is ITSELF classified polluting still marks pre-call.
* ACCEPTED COST (the deployment's to own, stated as mechanism, not as absence of risk): under
* `"attested-only"` a BACKGROUND child's real external contact does not mark this session — its
* content re-enters through the TaskOutput result, the task-notification injection, or the
* AgentTranscript step summaries, none of which carries an attestation — and a foreground child
* that ended abnormally (crash/salvage) is likewise not marked on its face alone (the chain still
* records `incomplete`). Already-marked sessions are never retroactively cleaned; the key only
* governs NEW marks.
* DEPLOYMENT seat ONLY (same posture as {@link readDenyBuiltinTiers}): deliberately no TaskSpec
* twin and not in the governed workflow whitelist — a task author or governed script gets no
* channel to loosen the evidence standard below its deployment. Not frozen into checkpoints: a
* resumed leg follows the CURRENT deployment configuration. Each run reads the deps of the Runner
* that PREPARES it — a multi-runner assembly should configure every runner with the same value
* (drift is the deployment's own configuration hazard; recorder/attestation semantics are
* value-independent, so the chain's evidence quality never varies with this key). Any other value
* refuses loudly at prepare (`config.memory_delegation_evidence`, #123 — exact spellings only,
* never truthiness).
*/
memoryDelegationEvidence?: "static-face" | "attested-only";
/**
* design/336 §13-3 — the memory-provenance master switch (F-020 深修):
* - `"carry"` (absent ≡ this; the default) — an externally exposed session's ORDINARY memory
* writes are committed WITH an engine-minted `origin` marker that travels with the entry
* (backend/sync/bundle); instruction-form files (type: feedback / pinned/triggers/applies-when
* hints) from exposed sessions are withheld and quarantined; the derived index's session prose
* still rolls back; the content scan gates run unchanged (a marker is not an exemption).
* - `"off"` — the pre-336 behavior: no origin marker is minted and an exposed session's harvest
* admits nothing (full containment, quarantined for host review). Standing committed markers
* still carry forward on edits — "off" stops minting, it never strips facts already recorded.
* ORTHOGONAL to {@link memoryDelegationEvidence} on purpose: that key decides WHEN a delegation
* marks the session exposed (the evidence standard); this key decides WHAT the mark does to the
* session's writes (tag-admit vs containment). All four combinations are legal and pinned.
* DEPLOYMENT seat ONLY (same posture as {@link memoryDelegationEvidence}): no TaskSpec twin and
* not in the governed workflow whitelist — a task author or governed script gets no channel to
* change the provenance posture below its deployment; an org administers it through this seat.
* Not frozen into checkpoints: a resumed leg follows the CURRENT deployment configuration. Any
* other value refuses loudly at prepare (`config.memory_provenance`, #123 — exact spellings,
* never truthiness).
*/
memoryProvenance?: "off" | "carry";
/**
* design/383 §3.1 — the deployment's memory-capture POSTURE (the frame the per-principal
* {@link RuntimeCaps.allowMemoryOptOut} verdict is read inside). Closed spellings, screened at
* the config doors (`config.memory_capture_policy` — a garbage value refuses the prepare, never
* folds to any posture):
* - absent / `"open"` — an opt-out declaration takes effect at face value; only a resolver's
* EXPLICIT `false` denies it (`memory.capture_optout_denied`, terminal). Resolver fault
* (throw / non-boolean / absent-on-fault) ⇒ the opt-out STILL TAKES EFFECT, with a named
* `onError({phase:"config"})` disclosure — the polarity-reversal arm: on this posture the
* irreversible failure is capturing a session whose user said no, so the fail-safe faces the
* other way from the caps family's deny-on-outage degrade. Under the ABSENT posture the first
* opt-out actually honored additionally announces one operator line (once per process): the
* switch is in use and no policy was ever configured.
* - `"governed"` — the per-principal verdict is MANDATORY (the mixed-fleet compliance
* deployment: some principals are retention-required). A verdict of `true` allows, `false`
* denies; resolver fault / absent verdict ⇒ the task is REFUSED (fail-closed): this posture
* is the deployment explicitly declaring the compliance side's irreversibility (a
* retention-required user acquiring an opt-out during an outage window) the heavier one.
* - `"capture-required"` — every opt-out declaration is refused (deployment-wide forced
* retention, one line, resolver-independent).
* The two fault polarities are BOTH deliberate and BOTH pinned; the cost table lives beside the
* adjudicator. Authority vs record separation (§3.3): this posture governs whether a NEW
* declaration (or a resumed run) may proceed — it never retroactively re-enables capture for a
* session whose one-way opt-out record already stands (record wins for capture; under
* `"governed"`/`"capture-required"` a standing record whose entitlement has been revoked refuses
* to CONTINUE instead — zero capture and zero out-of-policy running, availability paid).
*/
memoryCapturePolicy?: "open" | "governed" | "capture-required";
/**
* design/383 §2.1b — an optional host-supplied carrier for the session capture opt-out record
* (per memory plane, keyed by that plane's control dir). Absent = the control-plane file trio
* (single-process/single-host deployments: byte-identical behavior). A deployment whose control
* plane does not survive into resume replicas (the stateless multi-replica server form) supplies
* a durable store here; see {@link import("./memory-engine/layout.js").SessionCaptureRecordStore}
* for the per-leg fail-closed contract. Also the §2.1b capability signal: on a REMOTE-execution
* deployment with no store supplied here, a `capture:"off"` declaration is refused
* (`config.memory_capture_unsupported`) rather than accepted onto a carrier that the next resume
* cannot see — a false privacy promise is worse than a loud one.
*/
memoryCaptureRecordStore?: (plane: {
controlDir: string;
}) => import("./memory-engine/layout.js").SessionCaptureRecordStore;
/**
* design/339 §6.1 — the v3 memory-consolidation switch, threaded verbatim to the engine seat
* ({@link import("../core/memory-engine/engine.js").MemoryEngineOptions.consolidation} — same
* transport as {@link memoryProvenance}). ABSENT = OFF (the shipped default): no gate state is
* ever written, no recommendation is ever minted, and the consolidation verbs on an engine the
* host constructs over the same store refuse coded. PRESENT = enabled: the engine counts
* distinct terminal-harvest sessions per scope and surfaces the advisory
* `memory.consolidation_recommended` notice when the configured thresholds cross — EXECUTION
* stays host-owned (the four verbs are engine host API; nothing in a task can trigger a run).
* `provenance: "off"` beside this refuses loudly at prepare
* (`config.memory_consolidation_provenance_off` — the fold law must be able to mint);
* `multiNode: true` without a lease refuses (`config.memory_consolidation_lease_required`);
* every other bad value refuses under `config.memory_consolidation` (#123). DEPLOYMENT seat
* ONLY (no TaskSpec twin, not in the governed workflow whitelist), same law as
* {@link memoryProvenance}.
*/
memoryConsolidation?: import("../core/memory-engine/consolidation.js").MemoryConsolidationOptions;
/**
* design/376 片② — the memory-consolidation DRIVER seat (the LLM half), ORTHOGONAL to
* {@link memoryConsolidation} on purpose: that key is the write PROTOCOL's parameters (fuse,
* caps, cadence — engine-side), this key is who thinks (model seat + mint knobs — driver-side).
* The ENGINE itself keeps zero model seats (design/339 §1.1: engine adjudicates, driver
* proposes; an engine-held model would put distillation authority on the wrong side of the
* trust boundary), so the seat lives here and is consumed by the host driver verb
* (`runMemoryConsolidationDriver` + `resolveMemoryConsolidationDriver(deps)`).
*
* `chat` — the explicit model seat: an endpoint triple (screened loudly: explicit model id, no
* alias spellings, key present) or any JSON-only completion function. ABSENT ⇒ the driver
* resolves the `consolidate` role (`consolidate → summarize → coded refusal`; deliberately
* never `default` — see the {@link ModelRole} doc). Every other knob mirrors the driver verb's
* options and is screened there (#123: bad values refuse coded, never fold to defaults).
* DEPLOYMENT seat ONLY, same law as {@link memoryConsolidation}: no TaskSpec twin — a task
* author gets no channel to point the library's consolidation at a model of its choosing.
* Absent alongside absent {@link memoryConsolidation} ⇒ nothing runs and nothing changes
* (the v3 default posture).
*
* ⚠️ EGRESS — read this before configuring the seat (design/383 §1.5). Turning consolidation on
* SENDS MEMORY CONTENT TO A MODEL PROVIDER. Specifically:
* - WHAT: that cycle's ELIGIBLE CANDIDATES — not the whole library (entries already superseded,
* of an ineligible type, or held out by a governance gate are not in the set) — each rendered
* as its `name`, `description` and `body`. The bodies are the user's own notes, verbatim.
* - WHERE: the provider this seat resolves to. When `chat` is absent that is whatever the
* `consolidate → summarize` role chain lands on, which MAY BE THE SAME PROVIDER AS THE MAIN
* MODEL and may equally be a different vendor — the resolution is the deployment's, and the
* driver announces the model id and endpoint it actually used at the moment of the first call.
* - MARKED CONTENT: entries carrying an external-origin (or repo-ingest `trust`) marker are
* withheld from the CLEAN products' prompts and minted through a separate marked-only call.
* "Withheld from the clean arm" is NOT "not sent" — such rows still go to the provider in
* their own call, except a lone marked row, which is sent nowhere. The run receipt reports the
* two counts separately (`withheldFromCleanArm` / `withheldNeverSent`) and so should any UI.
* - IRREVERSIBILITY: bytes that have already crossed to a provider cannot be recalled by any
* later configuration change. Disabling the seat stops FUTURE egress and nothing else.
*/
memoryConsolidationDriver?: {
chat?: {
baseUrl: string;
apiKey: string;
model: string;
} | import("../core/memory-engine/distiller.js").DistillerChatFn;
/** The audit model id — REQUIRED when `chat` is a bare function (a function carries no id and
* the run/archive must not record a guess); derived from the endpoint triple or the resolved
* role otherwise. */
model?: string;
contract?: import("../core/memory-engine/distiller.js").LlmDistillerContract;
maxCycles?: number;
mintRepairBudget?: number;
};
/**
* The advisory-to-execution BRIDGE: when `true`, a terminal harvest that crosses
* the consolidation threshold (the same edge that mints `memory.consolidation_recommended`) fires
* ONE host consolidation run for the recommended scope at the post-task tail — fire-and-forget,
* result-first, never on the harvest tail itself. Explicit `false` = OFF (the deployment's own off
* switch; it beats the default). ABSENT = the named default `AUTO_RUN_ON_RECOMMENDATION_DEFAULT` (ON)
* (`memory-consolidation-driver.ts`, read through `autoRunOnRecommendationEffective` — the ONE
* resolver), applied ONLY where a run could actually happen (the protocol switch wired AND a model
* seat resolvable): a default-on value never arms a deployment that wired no consolidation.
*
* DEPLOYMENT seat ONLY (no TaskSpec twin), the same law as {@link RunnerDeps.memoryConsolidation} /
* {@link RunnerDeps.memoryConsolidationDriver}: arming this is an EGRESS decision — memory content crosses
* to the consolidation model automatically after every recommending task, never a task author's call.
*
* `true` with nothing to run (no {@link RunnerDeps.memoryConsolidation}, or no resolvable driver model
* seat), or a non-boolean, is refused at prepare (`config.auto_consolidation`) — never folded to off.
*
* Reported on the effective wiring manifest as `WiringManifest.autoConsolidation`, PRESENT IFF armed
* (absent and fingerprint-identical when off).
*/
autoRunOnRecommendation?: boolean;
/**
* design/199 件A — the DEPLOYMENT's read-face declaration
* ({@link import("../tools/fs/containment-face.js").ContainmentFace}; see {@link TaskSpec.readFace} for the
* task layer and the resolution order). An interactive single-user product declares
* `readFace: "open"` here in one line; the engine default stays "roots" (D-1b). Under org
* governance this seat may still open (it is the deployment's own declaration); the task layer
* may not. Beside a read-only (verifier) mount this seat's "open" silently CLAMPS to roots (a
* deployment default cannot override a load-bearing containment wall) — TaskSpec.readFace's own
* "open" there is the genuine per-task contradiction, and that one still refuses loudly (#123).
*
* A LIVE seat: `Runner.swapDeps({ readFace })` replaces it without a restart (the same natural
* snapshot the model catalog has — a leg reads the seat at its prepare and keeps that reading; every
* later prepare reads the new declaration; announced as `config.read_face_swapped`). A host whose
* declaration arrives late (a governance source read after boot) swaps it in rather than restarting.
*/
readFace?: import("../tools/fs/containment-face.js").ContainmentFace;
/**
* The DEPLOYMENT's WRITE-face declaration
* ({@link import("../tools/fs/containment-face.js").ContainmentFace}; see {@link TaskSpec.writeFace}
* for the task layer and the resolution order — the SAME order the read seat runs, one body). The
* engine default stays "roots". Under org governance this seat may still open (it is the deployment's
* own declaration); the task layer may not. Beside a read-only (verifier) mount this seat's "open"
* silently CLAMPS to roots and is ANNOUNCED (`config.write_face_deployment_clamped`); TaskSpec.writeFace's
* own "open" there refuses loudly. INDEPENDENT of {@link readFace}: neither seat reads the other. A LIVE
* seat — `Runner.swapDeps({ writeFace })` replaces it without a restart (announced as
* `config.write_face_swapped`).
*/
writeFace?: import("../tools/fs/containment-face.js").ContainmentFace;
/**
* design/96 §20 (P1a) — DEPLOYMENT-DECLARED training knowledge cutoff per model id (e.g.
* `{ "qwen-3.5-35b": "2025-01", "deepseek-pro": "2024-07" }`). When the resolved model id is present, the
* `# Environment` block shows `Knowledge cutoff: <value>` so the agent reasons about what it may not know.
* Deployment-owned ON PURPOSE: core hardcodes no model→date map (a stale cutoff = a §25 honesty breach, and
* the model catalog is a deployment/profile concern). Omitted ⇒ no cutoff line (honest absence). Optional.
*/
modelKnowledgeCutoffs?: Record<string, string>;
/**
* Default model roles: declare your models once and let every subsystem (main task, compaction,
* subagent, team) resolve its role instead of repeating a model name. `TaskSpec.roles` overrides
* per task/scenario. e.g. `{ default: "strong", summarize: "cheap", subagent: "cheap" }`.
*/
roles?: ModelRoles;
/**
* Session lifecycle/persistence backend. Defaults to `TtlSessionStore` (in-memory, 7-day idle TTL
* — lost on restart). For **durable** sessions (a "session center" that survives restarts and is
* shared across stateless replicas), supply a `TtlSessionStore` over a durable `SessionRepo`
* (e.g. TiDB-backed), or a fully custom `SessionStore`. See `design/10-会话持久化与会话中心.md`.
*/
sessionStore?: import("./session.js").SessionStore;
/**
* Durable-checkpoint backend for suspend/resume (design/45). A task with `durableApproval` set
* persists its awaiting-human checkpoints here and ends `status:"suspended"`; `runner.resume()` reads
* them back — cross-replica when this is durable (service's approval/run-store). A task's own
* `checkpointStore` overrides this. Omitted → no durable suspension (policy `ask` stays synchronous).
*/
checkpointStore?: import("./checkpoint-store.js").CheckpointStore;
/**
* design/380 O2 — deployment-supplied resume preflight, called INSIDE the pre-CAS ladder (after the
* row-integrity rungs, immediately before the CAS) with a bounded deadline
* ({@link resumePreflightTimeoutMs}). Refusal / throw / timeout ⇒ typed `resume.preflight_rejected`
* (CheckpointError closed-set addition); a wait ended by the RESUME SIGNAL is the caller's own
* cancellation and answers `checkpoint.resume_aborted` instead (one code at the resume entrance);
* the DEFAULT/absent disposition is `retry_later` (the safe
* arm — fail-closed hooks that just throw can never accidentally terminalize a row) — the
* entrance-screen posture of the wake-message hook (design/373 D2), generalized from "screen the
* wake message" to "screen the resume". A deployment binding runs to targets (a device lane) checks
* its placement/binding tables here: a transient obstacle answers `retry_later` (+`retryAfterMs`,
* the #449 G1 wait-hint carrier — the row stays `pending`, the SAME token redeems later); a
* PERMANENT one answers `terminal` (the row is settled by the single-shot `expire` CAS — never a
* silent pending-forever; bounded waiting for a target to come back also belongs HERE, inside the
* deadline, never in the env factory). Absent ⇒ no preflight, byte-identical resume behavior.
*/
resumePreflight?: (info: ResumePreflightInfo, signal: AbortSignal) => Promise<ResumePreflightVerdict>;
/**
* design/380 O2 — the {@link resumePreflight} deadline in ms. Default 10s (its own bound,
* deliberately its own: a resume preflight sits on every redeem attempt of a
* parked row and must answer promptly or get out of the way). Bad values (non-finite, ≤ 0,
* non-number) are DISCLOSED loudly through `onError` and fall back to the default (the bad-value
* loudness default: announce, never silently absorb).
*/
resumePreflightTimeoutMs?: number;
/**
* design/381 — the per-edited-file rewind history backend (CC FileHistory topology; replaces the
* E19 whole-tree `fileSnapshotStore`). WIRING IS THE OPT-IN (DV-9, the reference's own SDK
* posture): with a store present (and {@link fileHistoryCapture} not `"off"`), every Edit/Write/NotebookEdit first-touch is backed up before
* the byte changes, every turn mints a history boundary keyed by its own initial session entry
* ("file state when this turn began"), and a {@link TaskSpec.resumeAt}+{@link TaskSpec.restoreFiles}
* (or {@link TaskSpec.rewindFilesTo}) run converges the tracked set back to a boundary. Omitted ⇒
* rewind is OFF: zero track calls, zero boundary work, zero disclosure noise; a restore request
* then fails loud (`rewind.store_unconfigured`). The reference impl is
* {@link InMemoryFileHistoryStore}; a deployment supplies a durable backend behind the same seam
* (backends MUST reuse the exported env-half helpers — see the contract JSDoc). Works against ANY
* ExecutionEnv (local or remote): bytes are read/written through the env's FileSystem face and
* stored host-side, so an env rebuild does not lose history. `"disabled"` = the deployment DECIDED
* not to capture: the omitted-key semantics minus the once-per-Runner `onError` advisory an omitted
* key earns on a filesystem-capable run (`classification: "file-history-no-store"`).
*/
fileHistoryStore?: import("./file-history-store.js").FileHistoryStore | "disabled";
/** The deployment's CAPTURE switch, apart from the store seat: `"off"` (the only word, and only beside a wired store; anything else refuses prepare, `config.invalid_file_history_capture`) ⇒ no run records — no first-touch record, no turn boundary, no fork carry, no liveness advisory, manifest `fileHistory: "disabled"` — while a wired {@link fileHistoryStore} still serves {@link TaskSpec.restoreFiles} / {@link TaskSpec.rewindFilesTo} from what it already holds. */
fileHistoryCapture?: "off";
/**
* design/381 DV-14 (终审落锤) — what an Edit/Write/NotebookEdit does when its FIRST-TOUCH history
* record cannot be persisted (`FileHistoryStore.trackEdit` answered `{ok:false}` = NO durable
* first-touch state exists). Default **"refuse"**: the edit is refused with a typed tool error
* naming the history-store failure — proceeding would leave the path indistinguishable from
* never-touched, and the store's post-recovery retry would then mint already-modified bytes as
* pristine v1 (the fake-pristine hole this default welds shut).
*
* `"proceed-unprotected"` is the availability concession arm: the edit proceeds and the failure is
* disclosed via `onError` (phase "rewind"). ITS RESIDUAL, stated so the choice is informed: during
* a store outage NO durable trace of the touch can be written, so "the pristine promise is void"
* lives only in this process's memory — after a process restart the risk is exactly the default
* arm's nightmare again (the store recovers, the next edit mints already-modified bytes as v1).
* A deployment choosing this arm owns that window and is expected to record the disclosed events
* in its own audit surface. Values outside the two-member set refuse prepare loudly.
*/
onTrackFailure?: "refuse" | "proceed-unprotected";
/**
* design/99 §E6 — DURABLE per-session permission rules (a deny-narrowing folded into the task-time tool
* policy). Omitted ⇒ the feature is OFF (no rules read, zero behavior change). A deployment supplies a
* cross-replica backend whose CAS-rev + tighten-only semantics match the core
* {@link import("./session-policy-store.js").InMemorySessionPolicyStore}. Rules are read at prepare time for a
* task carrying its own `sessionId` (a delegated subagent inherits none) and composed BEFORE the caller
* policy, so they can only SUBTRACT capability. core does NOT verify owner/operator identity — the store
* honors the `operator` flag the (service) caller freezes from a verified principal.
*/
sessionPolicyStore?: import("./session-policy-store.js").SessionPolicyStore;
/**
* design/389 — the ONE permission-rule seam: the unified store, partitioned by source
* (`org | user | project | session`), built by `createPermissionRuleStoreProvider({ durable?, org?, session? })`.
*
* It is the one seam in this file that LOOSENS, and it is shaped so it can only do so within limits the
* engine holds. The provider hands out a store whose ONE read query answers every partition with each
* row's source and effective status; the write face is an exported BACKEND CONTRACT for the durable
* partition (an out-of-repo store twin builds against the same definitions instead of mirroring them),
* but the ENGINE reaches it only on the consent lanes — redemption of a confirmed human decision, the
* tighten-delete, and the sync join — a wiring invariant pinned by a registered-caller scan
* (test/permission-rule-writer-callers); no exported convenience mints a rule around consent. Personal
* rules are consumed post-fold, in the gate's ask branch, and never resolve an ask carrying
* `requiresRealApproval` or one a PreToolUse hook raised.
*
* The ORG partition (present iff the provider was composed with an `org` config — the governance
* declaration, design/182 §7.4) is the outermost TIGHTENING authority: every tool call is adjudicated
* against the org's published deny/ask snapshot BEFORE the ask-resolution chain (org deny > org ask >
* personal allow rule > bare ask), and while the org partition cannot adjudicate the WHOLE decision
* boundary fails closed — every terminal allow becomes a real-approval ask and both ask→allow seams are
* disarmed. The SESSION partition (present iff composed with one) is where `{kind:"session"}` grants
* land and are read from, spliced in front of the durable rows on every adjudication of that session.
*
* Omitted ⇒ no rule lane, no org layer, no session source: no rules are read, no field is added to any
* ask, and the decision path is byte-identical to a build without it. An unauthenticated task (no
* `principal`) resolves to zero personal rules rather than to a shared bucket (the org partition still
* adjudicates), and a store whose durable partition cannot be read yields zero rules plus a disclosure
* — a loosening face fails toward asking.
*/
permissionRuleStore?: import("./permission-rule-provider.js").PermissionRuleStoreProvider;
/**
* design/182 §4.5 (F-011) — declare that this deployment keeps its permission rules in the
* IDENTITY-LESS local bucket: a task with no `principal` resolves rules through the provider's
* `forLocalOwner()` face instead of resolving to zero rules.
*
* A DECLARATION, never an inference: the local bucket is "this machine's owner", a fact only the
* deployment knows. Omitted/`false` ⇒ an unauthenticated task reads no rules. Declared `true` while the
* wired provider has no local-owner face (or while no provider is wired at all) is a configuration
* contradiction and is refused loudly at prepare, rather than silently degrading to "the rules this
* person approved stopped applying".
*
* A local-owner bucket cannot cloud-sync (syncing is an authenticated act); it is adopted into a
* principal bucket by `adoptFilePermissionRuleStore`, after which the local-owner face resolves the
* adopted principal's bucket forever.
*/
localOwnerRules?: boolean;
/**
* The DEPLOYMENT's allow rules — standing yeses its operator writes for every run of this Runner, in the grammar of
* {@link import("./task-spec.js").TaskSpec.allowedTools} (the same compiler, matcher and auto-mode strip). One of the allow
* layer's members, read beside the request's list, a person's stored rules and a skill's list under one predicate: deny
* and ask rules still win, a question no allow rule clears is still put to a person (the entry disclosed as the
* deployment's), and a call it clears is attributed to it (`decisionReason: "deployment_rule"`). Lives for the Runner —
* read once at construction: a value that is not a list of strings refuses construction (`config.invalid_permission_rules`,
* with its issues), and the entries that read as nothing are announced once, when the Runner is built
* (`permission.allowed_tool_not_granted`, `detail.entries[].source: "deployment"`).
*/
allowedTools?: readonly string[];
/**
* design/182 §9 — DECLARE that this deployment drives cloud sync (`syncPermissionRules`) for the
* wired rule store. Purely a disclosure input: it is reported as `permissionRules.syncWired` on the
* wiring manifest and changes no decision. It exists because the sync loop is HOST-driven (core
* bundles no fetch and owns no timer), so there is nothing for the engine to infer — and "are this
* machine's standing approvals shared with other devices and a server?" is exactly the trust-domain
* fact an operator must be able to read off the manifest instead of guessing.
*/
permissionRuleSyncWired?: boolean;
/**
* design/99 §K — resolve the **per-principal runtime ENTITLEMENTS** the engine
* ENFORCES server-side, keyed by `spec.principal`. A deployment (the SERVICE) implements it over center's
* `GET /api/config/effective?principal=` `runtimeCaps`. Resolved once at prepare time.
*
* 🔐 This is a TRUSTED deployment seam, NOT a caller-facing `TaskSpec` field: entitlements are purely
* server-governed — an untrusted caller must never be able to self-grant a capability. `undefined` (no
* resolver, or an unset cap) = NO per-principal restriction (TIGHTEN-ONLY: a cap can only ever DENY or
* FORCE; absence falls back to the deployment-level default). Symmetric with {@link sessionPolicyStore}.
*
* core enforces the six caps it CAN enforce in the engine (polarity per member on {@link RuntimeCaps}):
* `allowWorkflows` (the third stage of the workflows gate — task opt-in ∧ deployment capability ∧ this),
* `allowFork` (the Agent-fork route), `allowObservers` (explicit opt-in for observer auto-spawn),
* `autoMode` (the per-principal deny bit of the user-enabled auto mode), `allowMemoryOptOut` (may this
* principal declare a capture opt-out), and `forceDurableGate` (forces a run onto the durable-approval
* path so a policy `ask` suspends to the wire — where a checkpoint store is wired; a store-less leg
* announces `config.durable_gate_unavailable`), and `bypassPermissions` (the deny bit of the
* `bypassPermissions` mode — a denied request folds to `default`, announced). `allowUltracode` is a shell/UX
* concern (reasoning-tier, not a core primitive).
*/
runtimeCapsResolver?: (principal: string | undefined) => RuntimeCaps | undefined | Promise<RuntimeCaps | undefined>;
/**
* Administrator lock layer (ruled 2026-08-05) — the deployment's config-time declaration of which
* lockable keys are locked (closed registry: `mcp` / `toolPolicy` / `compliancePosture` /
* `retentionPolicy`; see {@link import("./locked-config.js").LOCKED_KEY_REGISTRY}). A task spec
* carrying a value for a locked key is refused wholesale at the prepare door
* (`config.locked_key`); an unknown key in this declaration is itself refused
* (`config.locked_registry` — a security control that doesn't parse must not half-arm).
*
* 🔐 TRUSTED deployment seam, never a `TaskSpec` field. Locks are minted only by the deployment's
* own configuration plane (env / central governance domain) under that plane's existing
* authentication — "who can unlock" ≡ "who can change the deployment config"; there is no
* request-time unlock.
*/
lockedConfig?: import("./locked-config.js").LockedConfig;
/**
* Compliance capability veto (ruled 2026-08-05) — resolve the running principal's compliance
* POSTURE (closed profile + closed additional denies; effective denies =
* `BUILTIN_COMPLIANCE_DENIES[profile] ∪ additionalDenies`, supply can only tighten). Mirrors
* {@link runtimeCapsResolver}: a 🔐 TRUSTED deployment seam keyed by `spec.principal`, resolved
* ONCE per prepare; the verdict is frozen for the run and follows the delegation tree through the
* inherited principal. Fail-closed: a resolver that throws (or returns an unparseable posture)
* denies EVERY managed capability for the run and surfaces via `onError` (phase `"config"`); a
* denied capability that the spec explicitly requests refuses the prepare loudly
* (`config.compliance_denied`, or `config.compliance_required` when the refusal came from the
* resolver fault rather than an adjudicated posture). `undefined` (no resolver / no posture for
* the principal) = no compliance restriction from the deployment — existing deployments are untouched. The run's deny
* set is this posture's UNION the request's own denies ({@link TaskSpec.complianceAdditionalDenies}, add-only).
*/
compliancePostureResolver?: (principal: string | undefined) => import("./compliance.js").CompliancePosture | undefined | Promise<import("./compliance.js").CompliancePosture | undefined>;
/**
* Org-memory tenant admission (ruled 2026-08-05) — the fail-closed adjudicator for REQUEST-origin
* `org:` memory scopes (`TaskSpec.memory` / a subagent's `AgentDefinition.memory`, under the v2
* scope contract). Runs at prepare, after `normalizeMemorySpec` and before any memory-backend I/O.
* Verdicts are two-state ({@link import("./memory-admission.js").MemoryAdmissionVerdict}): any
* request-origin org scope not admitted refuses the WHOLE prepare (`memory.admission_denied`) —
* silent narrowing would turn mount visibility into an org-membership probe. Absent resolver +
* request-origin org scope ⇒ `memory.admission_required` (fail-closed, transient — the control
* plane may be mid-rollout); resolver throw ⇒ same code. Deployment-origin scopes
* ({@link deploymentMemoryScopes}) pass WITHOUT this resolver; when present it may still narrow
* them (disclosed via `onError`, phase `"memory"`).
*
* 🔐 TRUSTED deployment seam, never a `TaskSpec` field (an untrusted caller must never adjudicate
* its own tenant admission). The admitted org set is frozen into the run's child chain — a
* delegated child's request plane can only narrow it, never exceed it.
*/
memoryScopeAdmission?: import("./memory-admission.js").MemoryScopeAdmission;
/**
* The deployment's SELF-CERTIFIED org memory scopes (ruled 2026-08-05): org keys the operator
* pinned in the deployment's own configuration (env / registry defaultScopes). A scope listed here
* is `origin: "deployment"` at the admission door — the operator writing the config IS the
* authorization: no principal required, no {@link memoryScopeAdmission} resolver required (the
* single-user zero-migration path). Everything not listed here that requests an org plane is
* `origin: "request"` and must pass fail-closed admission.
*/
deploymentMemoryScopes?: readonly string[];
/**
* Managed retention policy (ruled 2026-08-05, retention.ts). Core consumes it ONLY for the
* startup/prepare capability validation: when the locked-config plane LOCKS the `retentionPolicy`
* key AND this policy is configured, every wired store (session/checkpoint/tool-result) must
* declare `retention: "managed"` — otherwise the deployment refuses to start
* (`config.retention_capability`; the prepare preflight is the per-leg backstop). The engine
* itself never deletes data on the task path — retention EXECUTION (scheduling, retries, audit)
* is the deployment scheduler's job against the `ManagedRetentionCapability` contract.
*/
retentionPolicy?: import("./retention.js").RetentionPolicy;
/**
* design/143 批2b — the auto-mode classifier's DEPLOYMENT assembly face. This is the
* CAPABILITY arm of the three-arm arming: a run arms auto mode only when the task carries the
* auto-mode INTENT (`TaskSpec.permissionMode: "auto"`, or the mode inherited on the chain by an
* engine-spawned child) AND this face is present AND the per-principal DENY bit is not set
* ({@link RuntimeCaps.autoMode}` !== false` — absent is not a denial).
*
* 🔐 Trust gate (the CC 2.1.207 three-source invariant): classifier RULES enter EXCLUSIVELY
* here — a deployment-constructed object, never a `TaskSpec` field, never a repo-file plane. The
* prompt is assembled from the SHA-locked CC 2.1.207 assets (`buildAutoModePrompt`) with these
* rules merged CC-exactly (user rules replace a section; the `"$defaults"` sentinel splices the
* stock rules back in). The classify leg runs on the `classifier` role's model (fallback
* `summarize → default`; tier default `flash`) via the brain's simple-completion pipe — same
* lane as compaction/consolidation, no tools, no session writes.
*/
/**
* The deployment's A2A peer switches. `isolatePeers: true` makes every A2A peer skill's egress default an
* OPERATOR mandate: its ask is `operator_forced` (kept under `bypassPermissions`; `dontAsk` refuses it like every ask)
* instead of the releasable `tool_marks_a2a`. Absent ⇒ the default (a permission mode may release it, disclosed).
*/
a2a?: {
isolatePeers?: true;
};
autoMode?: {
/** Rule overrides for the four permissions sections (omitted section = CC defaults verbatim). */
rules?: import("./auto-mode-prompt.js").AutoModeRules;
/** Settings-plane deny rules rendered into the `<settings_deny_rules>` slot (circumvention catch). */
settingsDenyRules?: readonly string[];
/** Extra `## Session Context` facts (e.g. the user-identity line). */
sessionContext?: readonly string[];
/** Transcript-window bounds for the classify prompt (defaults: 40 entries / 2000 chars each). */
window?: import("./auto-mode-prompt.js").AutoModeWindowOptions;
/** Classify round-trip cap, ms — ONE ladder rung (see `classifierFallback`), not the whole classification;
* default `AUTO_MODE_DEFAULT_TIMEOUT_MS` (180 000, sized for a self-hosted gateway's queue + prefill; worst
* case two rungs = 360 s). A rung past the cap did not rule: the walk moves to the next candidate; with none
* left the classification is `unavailable` — the call goes on to a person where the leg has an approval seat
* (a live approver or a durable park), and is DENIED with a sentence that says so where it has none; the
* user hears it either way (`classifier.unavailable`). The next call is classified afresh. A present value
* that is not a number of milliseconds in [1, 2147483647] is REFUSED loudly at every leg's prepare while this face
* is present, whatever mode the leg starts in (`config.auto_mode_timeout_invalid`) — never clamped, never read as the
* default; there is no OFF. */
timeoutMs?: number;
/**
* What a classification does when the classifier SEAT does not rule (the leg threw / rejected, the cap
* elapsed, or the reply carried no verdict):
* - `"main"` (DEFAULT) — re-ask the SAME prompt on this run's own main model, once, and act on what it
* says; only if that rung does not rule either does the call reach the fail-closed deny. The record
* says which model answered (`GateDisposition.classifier`) and the first move per (rung, cause)
* announces `classifier.fallback` to the operator.
* - `"none"` — one rung: a classifier that does not rule denies the call exactly as it did before this
* knob existed, at exactly one model call.
*
* COST of the default, stated: while the seat is broken every gated call pays a second round-trip
* (worst case two `timeoutMs` caps); `"none"` buys that back for the deny.
*
* A value outside the closed set ({@link import("./auto-mode.js").AUTO_MODE_CLASSIFIER_FALLBACKS}) is
* REFUSED loudly at every leg's prepare while this face is present, whatever mode the leg starts in
* (`config.auto_mode_classifier_fallback_invalid`) — never clamped, never read as the default.
*/
classifierFallback?: import("./auto-mode.js").AutoModeClassifierFallback;
/**
* The classifier DENIAL LIMIT (CC 2.1.250 `FO`/`AKe`): a run whose classifier keeps blocking falls
* back to a PERSON instead of being denied without end. Per run: a `block` first increments the
* consecutive and total counts and then judges `consecutive >= maxConsecutive || total >= maxTotal`
* — the block that reaches a bound is itself the one that becomes an `ask` (the 3rd consecutive
* block asks). That ask carries `requiresRealApproval: true` (no automatic lane may clear it — not
* a sandbox admission, not an inherited resolver, not a blanket `onAsk:"allow"`) plus the additive
* `denialLimitFallback` member with the counts and its own auto-deny window; an ask handed to a LIVE
* function approver and left unanswered auto-denies after `autoDenyAfterMs` (default 120s; `0` = no
* window) — the window is armed at that hand-out only, so a durably PARKED fallback carries `0`
* (nothing counts down on that lane). A classifier allow, or a
* person's allow of the fallback ask, zeroes the consecutive count; reaching the total bound
* zeroes everything. With no approver wired at all (headless), the fallback has nowhere to go and
* the run STOPS with `TaskResult.errorCode = "classifier.denial_limit"` (a notice of the same
* code is minted).
*
* Every member optional (defaults 3 / 20 / 120_000). A present member with a bad value, a member that is
* not one of the three, or a value that is not a record, is refused loudly at every leg's prepare while this face is present, whatever mode the
* leg starts in (`config.auto_mode_denial_limit_invalid`) — never clamped, never silently read as the default.
* See {@link import("./auto-mode.js").AutoModeDenialLimitOptions}.
*/
denialLimit?: import("./auto-mode.js").AutoModeDenialLimitOptions;
/**
* OPT IN to recording this arming's serializable criteria (an
* {@link import("./auto-mode-arming.js").AutoModeArmingRecipe}) on the constraint-chain entries a
* durable park freezes, so a CROSS-PROCESS redemption can rebuild an equivalent classifier over its
* own model leg ({@link import("./auto-mode-rebuild.js").rebuildAutoModeDecider}) instead of
* answering `unavailable` forever and parking the same approval for a human on every redemption.
*
* DEFAULT OFF, and the default is the safety story: a row minted without it is byte-identical to
* the pre-#503 shape, so a mixed-version fleet (a worker that predates this field resuming a row a
* newer worker minted) keeps the digest it can compute. Turn it on fleet-wide, not per-worker.
*
* What gets written down: the rule overrides, the settings-deny rules, the session-context facts,
* the window bounds and the two knobs — i.e. deployment-authored configuration, on a durable row.
* `sessionContext` is the one field that routinely carries per-session facts (a user-identity
* line); a deployment that treats those as sensitive at rest should keep them out of the arming
* face rather than out of this flag.
*
* A `true` here with a non-serializable face announces once and records nothing (`phase:"config"`).
*/
persistArming?: boolean;
/**
* The deployment's auto-mode SETTINGS GENERATION (a config revision, a settings hash, a
* publish id — the string is opaque to the engine). Recorded in the arming recipe and compared at
* rebuild time against the REDEEMING deployment's own: it is a declaration axis that can only make
* the rebuild stricter (see {@link import("./auto-mode-arming.js").foldAutoModeArming}). Absent =
* undeclared, which the fold treats as "cannot assume unchanged".
*/
settingsEpoch?: string;
};
/**
* design/129-B — PROCESS-level observer for background delegation children (Agent/Fork
* `run_in_background`): `spawn` (at registry registration) / `tick` (every forwarded `task_progress`)
* / `terminal` (after the registry settle), for the child's WHOLE lifetime. Motivation: every per-leg
* sink (`RunInternals.onForwardEvent` etc.) carries the SPAWNING turn's lifecycle — correct for sync
* children, structurally wrong for a session-scoped background child that outlives the turn (the
* deployment rightly freezes per-leg publishers at leg settle, which silences the child). This seam is
* keyed to nothing but the process: wire it once at Runner construction and publish fleet rows /
* completion pushes from it. Observability only — a throwing observer is swallowed, never faults a run.
*/
onBackgroundChildEvent?: (event: BackgroundChildEvent) => void;
/**
* #281 件B — PROCESS-level observer for EVERY delegated child leg's lifecycle (`spawn`/`terminal`),
* all delegation lanes at once: synchronous and steer-handle delegations (whose spawn/settle
* previously reached only the trusted `RunInternals.onSubagentSpawn` third parameter — and whose
* `settled` is a void promise), background/fork/revive children (whose {@link BackgroundChildEvent}
* family this seat complements, not replaces — BCE stays the fleet-row lane with registry `a*`
* handles and ticks; this seat is the leg-identity lane), and workflow-spawned agents. Frames are
* minted at the child leg's own runner chokepoint and carry the #281 件A identity envelope plus a
* terminal status summary; the `spawn` phase is the projection of the child's `task_started` frame
* (once per cycle), the `terminal` phase is per leg — see {@link DelegationLifecycleEvent} for the
* emission points and the recorded honest absences. Wire it once at Runner construction; a
* deployment needs NO RunInternals access to observe delegation any more. Observation only — a
* throwing or rejecting observer is contained ({@link deliverDelegationLifecycle}) and never
* faults the child run; a PRESENT non-function value here is announced once per process and the
* frames are simply not delivered (#123 — a bad seat must be loud, and this stream's loud exit is
* the seat defect, not a per-frame console flood).
*/
onDelegationLifecycle?: (event: DelegationLifecycleEvent) => void;
/**
* design/98 (S8) — the HARD sandbox seam for LLM-AUTHORED workflow scripts (`TaskSpec.selfOrchestration`).
* A deployment supplies an isolated-vm / separate-process runner whose `safeForUntrustedScripts === true`;
* core does NOT ship one (the Node `vm` dev runner is NOT a security boundary). With this unset (or
* `safeForUntrustedScripts:false`), `selfOrchestration` is FAIL-CLOSED: the `run_workflow` tool is not
* mounted and the orchestration prompt is not injected. See {@link WorkflowGovernanceBaseline}.
*/
workflowScriptRunner?: import("../orchestration/workflow-script-runner.js").WorkflowScriptRunner;
/**
* design/98 §2.5 (S8b) — the deployment-trusted governance every workflow-spawned sub-agent inherits
* (tighten-only). REQUIRED (alongside a hard `workflowScriptRunner`) for `selfOrchestration` to activate:
* unset ⇒ FAIL-CLOSED (the `run_workflow` tool is not mounted — a script-spawned agent must never run
* without a governed baseline). See {@link WorkflowGovernanceBaseline}.
*/
workflowGovernanceBaseline?: WorkflowGovernanceBaseline;
/** design/98 §D.6 (S8c) — deployment hard CEILINGS for an LLM-authored workflow (script length / agent
* count / timeouts / per-child cost+token caps / budget / result+log size). Defaults bound a runaway. */
workflowLimits?: import("../orchestration/run-workflow-tool.js").WorkflowLimits;
/** design/98 §D.5 (S8c) — re-invoke seam: called once when an LLM-authored workflow completes, with a
* redacted, bounded summary. Unset ⇒ the run still completes + is queryable; the originator just isn't
* pushed a notification. */
workflowCompletionNotifier?: import("../orchestration/run-workflow-tool.js").WorkflowCompletionNotifier;
/** design/97 S1b / design/98 §D.5 — persist LLM-authored workflow runs here for cross-replica `/workflows`
* history (opt-in; in-process runs are still subscribable without it). */
workflowRunStore?: import("./workflow-run-store.js").WorkflowRunStore;
/** design/151 S1a — the durable background-agent execution ledger (the `workflowRunStore` twin for
* `a*` rows): registrations write restart-surviving rows, and the auto-mounted TaskOutput/TaskStop
* fall back to it on a live-registry miss (terminal snapshots / honest cross-instance reads, behind
* the fail-closed §3.1c predicate). Opt-in; absent ⇒ the in-memory-only pre-151 lifecycle. A
* deployment that composes its own Agent tool MUST pass the SAME instance as
* `SubagentToolOptions.background.agentStore` (spawn-side writer half) — the engine cannot
* verify the pairing (the Agent tool is deployment-composed), and a half-wired deployment is a
* SILENT degrade: reader-only ⇒ no rows ever exist; writer-only ⇒ the auto-mounted fallbacks
* here never see them (RB-37). Third consumer: delegated
* CHILDREN's auto-mounted TaskOutput/TaskStop/AgentTranscript read THIS deps too (prepare-task
* wires them from the child's runner) — a sub-runner with different deps splits the read face. */
backgroundAgentStore?: import("./background-agent-store.js").BackgroundAgentStore;
/** design/151 §7 S3c — the durable SendMessage mailbox (delivery-ladder third rung): a message to a
* SETTLED named teammate is parked here ONLY inside the tier-3 claim-winner window and delivered
* when the revival spawns (claim/lease + ack — never a destructive drain; a crashed revival's
* lease expires and the messages re-serve with their original seq). Tier-3 activates only when
* BOTH this and `backgroundAgentStore` are wired AND a delegation tool (createSubagentTool) is
* mounted on the task — anything less keeps SendMessage's honest refusal texts, byte for byte.
* Same single-instance pairing discipline as `backgroundAgentStore` (RB-37): a deployment that
* composes its own tools must thread the SAME instance everywhere. */
mailboxStore?: import("./mailbox-store.js").MailboxStore;
/**
* Subagent transcript persistence — the delegation ENTRY caps (CC parity values: 20 concurrent /
* 200 cumulative per session tree; defaults exported as `DELEGATION_MAX_CONCURRENT_DEFAULT` /
* `DELEGATION_MAX_PER_SESSION_DEFAULT`). Key = `(scope, rootSessionId)`, full depth (grandchildren
* share the tree's pool). `maxConcurrent` bounds the tree's RUNNING children of every local lane:
* running/pending a* handles in this process's registry PLUS the synchronous and forked children in
* flight in this process (parked does not burn a slot — a suspension is not concurrency; a revival
* claim counts like a spawn); `maxCumulativePerSession` bounds the RETAINED-WINDOW cumulative count
* (registry-retained + store-retained rows, plus those same in-flight local children — a reaped row
* returns its quota, and a synchronous child retains nothing so it leaves the window when it
* settles; deliberately NOT CC's lifetime-monotonic session counter, which would require a
* persistent counting surface this economic bound does not justify — registered divergence). One
* consequence, stated rather than left to be discovered: a tree that only ever spawns synchronously
* is bounded by the concurrency cap alone. Refusals are coded (`delegation.concurrency_cap` /
* `delegation.session_cap`) with the current value and this knob's name in the text.
*
* BAD VALUES REFUSE LOUDLY at prepare (`config.delegation_entry_caps`, the #123 posture): a
* non-positive/non-integer/NaN member, or a resolved pair where `maxConcurrent` exceeds
* `maxCumulativePerSession` (you cannot run more at once than you may ever create) — never a
* silent fold to the defaults. Per-replica bound (multi-replica deployments are each honestly
* bounded; row-level CAS owns correctness, this cap owns economics).
*/
delegationEntryCaps?: {
maxConcurrent?: number;
maxCumulativePerSession?: number;
};
/** The peer lane's ONE knob table (`PeerLaneConfig`): the always-on admission gate (rate/dedup/hop-chain/
* queue bounds), the wake and per-pass delivery budgets, and the `ListAgents` freshness window. Per-field
* range-validated with out-of-range values falling back to that field's default, announced once; there
* is NO off switch — absent means the default table. Threaded into the auto-mounted SendMessage as
* `SendMessageToolOptions.lane` and into the session-box drain; read per call / per pass. */
peerLane?: Partial<import("../agents/peer-admission.js").PeerLaneConfig>;
/**
* design/385 §2.1 / §5.1 — the peer-session DIRECTORY: the discovery truth of the cross-session
* lane (other sessions of this engine for the same user). A HOST implements the contract (cli: the
* pid-keyed registration files; server: its session table) and writes the rows; the engine only
* reads them. Seat semantics = the mailbox seat's: NO seat, NO lane — every face stays byte-identical
* to a pre-385 build. With the seat wired the engine (a) mounts `ListAgents` (alias `ListPeers`),
* (b) opens SendMessage's `session.<id>` address arm and its last `name [ref]` rung, (c) drains this
* session's own `session.<sessionId>` box at every turn boundary (the mailbox contract's third
* consumption chain), and (d) splices the cross-session rule into the auto-mode classifier slot.
* HARD PRECONDITION (design/385 §1.2⑥): `mailboxStore` must be wired AND declare
* `crossProcessSafe: true`; otherwise the lane is REFUSED at prepare with a named
* `config.peer_lane_unmounted` notice (never mounted on luck), and the seat is inert for that run.
*/
peerDirectory?: import("../agents/peer-directory.js").PeerDirectory;
/**
* design/385 §4.4 — this deployment's `crossSessionInbound` setting LAYERS for the drain-point
* judgment (managed / user / repo, resolved by `resolveCrossSessionInboundSetting` — explicit value
* wins, unset ⇒ mode parity, an unrecognized value forces `hold` loudly). Read at every drain, so a
* host that re-reads its settings files hands the current layers through a getter. Absent = unset
* everywhere (mode parity, the CC default). Inert without {@link peerDirectory}.
*/
crossSessionInbound?: import("../agents/cross-session-judge.js").CrossSessionInboundSettingLayers | (() => import("../agents/cross-session-judge.js").CrossSessionInboundSettingLayers);
/**
* WHO consumes this deployment's principal inbox (`principal.inbox`, where the
* organization's delivery gate appends cross-principal letters; the mailbox contract's fourth chain).
* Absent = no consumer, no lane (byte for byte as before); a deployment that mounts a gate DECLARES
* `{ assistant: "<agent handle>" }` (the scope's assistant is the only consumer; no terminal leg touches the
* box). `"terminals"` (each terminal drains the box as a second lane) is refused until held decisions are
* shared across terminals (`detail.reason: "terminals_unsupported"`). Letters are judged on the principal's
* `crossSessionInbound` layers (managed, user), held when unset. ANY other value fails the prepare of a leg
* that drains a session box with `config.principal_inbox_consumer_invalid` (`detail.reason: "malformed"`).
*/
principalInboxConsumer?: "terminals" | {
assistant: string;
};
/**
* design/385 §4.4 (slice 4) — this deployment's `dialogExpiry` (CC settings key, verbatim vocabulary
* `"60s" | "5m" | "10m" | "never"`, default `"5m"`): how long a HELD cross-session message whose cause
* a human review could resolve (mode-mismatch / no-mode-asserted / invalid attestation) waits in this
* session's held queue before it resolves to its safe no-action default — EXPIRED, dropped WITH a
* receipt to the sender, never silently. `"never"` disables the deadline. Read at every drain round
* (getter form for a host that re-reads its settings). A value outside the vocabulary is announced
* ONCE per leg through `onError` (`classification: "peer-dialog-expiry"`) and the default applies —
* a garbage setting never silently reads as a policy. Absent = the default. Inert without
* {@link peerDirectory}.
*/
crossSessionDialogExpiry?: import("../agents/peer-notices.js").CrossSessionDialogExpiry | (() => import("../agents/peer-notices.js").CrossSessionDialogExpiry | undefined);
/**
* The deployment's CAPABILITY FLOOR for durable engine frames: `true` declares that EVERY process
* that drains this deployment's session boxes reads `engine_frame` records (this engine's codec or
* newer). With it, an engine-minted task-notification frame addressed to an idle session parks in
* the session's durable box (`MailboxPeerMeta.kind: "engine_frame"`) and survives a process restart;
* without it (absent / `false`) such a frame parks in process exactly as before. A deployment flips
* it only after every drainer is upgraded — a pre-codec drainer reads the record as a peer's words.
* Inert without a cross-process-safe {@link mailboxStore}.
*/
sessionBoxEngineFrames?: boolean;
/**
* design/164 件五 — DEPLOYMENT-level usage governance: allowances that span TASKS, evaluated per
* principal (or once for the whole deployment when a task declares none). A different axis from
* `TaskSpec.limits`, which is the allowance ONE task asked for — an operator granting "N tokens per 5
* hours" cannot express it as a task limit, because nothing stops the next task from asking again.
*
* A window carries a TOKEN ceiling (`UsageWindow.maxTokens`), a MONEY ceiling (`UsageWindow.maxCostUsd`,
* absolute USD — the `TaskLimits.maxCostUsd` quantity one governance level up), or both: each is
* optional on its own and at least one is required, so a deployment governed by spend alone declares
* only `maxCostUsd` (a window with neither ceiling is refused, `config.usage_window_invalid`). The two
* are independent and every declared one binds. A $ ceiling requires a PRICED run: a task whose model
* has neither a {@link pricing}
* entry nor a `Model.cost` declaration is refused at the door (`config.usage_window_unpriced`) rather
* than charged the fabricated 0 an unpriced run would otherwise file into an operator's ceiling.
*
* Unset (the default) ⇒ NO governance: no ledger is read or written and no task can be refused for
* usage. When set, every window is evaluated at two moments:
* - **entry** (before the first model call of a fresh task): an exhausted window REFUSES the task with
* `usage.window_exhausted`, since there is no checkpoint seat yet to suspend into;
* - **each clean turn boundary**: an exhausted window suspends the run durably
* (`ResourceLimitReason:"usage_window"`, with a `resumeAfterMs` hint on the gate) when the
* infrastructure supports it, and otherwise stops it loudly with the same code.
*
* Resume is CALLER-DRIVEN, as everywhere else in this engine: core mints the checkpoint and the hint,
* and a host schedules the resume (its own scheduler, delayed by `resumeAfterMs`).
*
* Requires {@link usageWindowStore} — windows without a ledger are reported through `onError` and NOT
* enforced (a ledger-less window cannot be evaluated, and failing every task closed on a config typo
* would take the deployment down instead of telling the operator).
*/
usageWindows?: readonly import("./usage-window-store.js").UsageWindow[];
/** design/164 件五 — the cross-task usage ledger {@link usageWindows} is evaluated against. Core bundles
* `InMemoryUsageWindowStore` (process-local) and `FileUsageWindowStore` (restart-surviving); a fleet
* deployment plugs a database behind the same two-method seam. Ignored when `usageWindows` is unset. */
usageWindowStore?: import("./usage-window-store.js").UsageWindowStore;
/** design/97 CORE-9 (Part A) — the LOAD-BEARING resume journal for LLM-authored (`run_workflow` tool) workflows
* (the twin of `workflowRunStore`; service ships a durable Postgres/TiDB one). Wired into the auto-mounted
* run_workflow tool so a tool-launched workflow can durably RESUME across a replica crash. Opt-in. */
workflowJournalStore?: import("./workflow-journal-store.js").WorkflowJournalStore;
/** B5/F2 + design/140 §6 1a — the script-persistence / named-registry seam, threaded into the auto-mounted
* run_workflow tool: every invocation's resolved script is persisted (iterate via `{scriptPath}`), `{name}`
* resolves a saved workflow, and a registration's `defaultArgs` merge under the call-time args. Opt-in
* (absent ⇒ inline `script` only — the engine ships no built-in named workflows). */
workflowScriptStore?: import("../orchestration/workflow-script-store.js").WorkflowScriptStore;
/**
* design/97 CORE-9 (Part B) — a TRUSTED deployment sink for the steerable handle of every agent an LLM-authored
* (`run_workflow` tool) workflow spawns. When set, the tool's `agent()` primitive runs each agent STEERABLE and
* emits its {@link import("../orchestration/workflow.js").WorkflowAgentHandle} here (the script never receives
* the handle — no host-context leak into the runner); the deployment registers it by `runId`+`label` to route a
* human/cross-replica steer to `handle.steer(...)`. Unset ⇒ agents run non-steerable (no overhead). Opt-in.
*
* **Interaction with {@link workflowJournalStore}**: a steerable agent runs via `agentStream`, which reads the
* resume journal exactly as the non-steerable lane does — recorded under the same call key, replayed by the same
* unchanged-prefix rule. A completed agent a `resumeFromRunId` replays is emitted here as an already-settled
* handle: `result()` resolves at once to the journaled result, `steer()` is refused `steering.not_running`.
* A result the operator steered replays as what it was — the run's record of the work, like any model answer
* that would come out differently if asked again.
*/
onWorkflowAgentSpawn?: (handle: import("../orchestration/workflow.js").WorkflowAgentHandle) => void;
/** design/72 §2.2 (B): deployment default for the suspend-loop cap (see `TaskSpec.maxSuspends`).
* Default 5 (loose — human-approval flows re-suspend rarely). A task's own `maxSuspends` overrides. */
maxSuspends?: number;
/** Deployment default for the outer per-brain-call guardrail — see
* `TaskSpec.limits.brainCallGuardrailMs` for the semantics (default one hour; `false`/`0` = off;
* an invalid value is a fail-loud config error). A task's own `limits.brainCallGuardrailMs`
* overrides it. Set it here to cover EVERY run of this Runner, delegated children included — a
* sub-agent's spec carries only its own `maxTurns`/`maxWalltimeMs`, so the deployment-level knob is
* the one that reaches a background child. */
brainCallGuardrailMs?: import("../brain/timeout.js").BrainCallGuardrailKnob;
/**
* design/91: injectable wall-clock source for **human-review timing** (and `Checkpoint.suspendedAt`).
* Defaults to `Date.now`. The Runner reads EVERY human-time timestamp through this — the synchronous
* `resolveAsk` wait (`t1 − t0`), the durable suspend's `suspendedAt`, and the resume's derived human
* latency (`now() − suspendedAt`) — so a test can inject a controllable clock (`let t = 0; now = () => t`)
* and assert `stats.humanReview` deterministically (design/87 §4.3 flaky-guard: no bare `Date.now()` in a
* hard-gate test). **Scope = human-time only**: this is NOT a general clock for token/cost/turn accounting
* (those keep `Date.now`); `humanReview` is a budget-EXCLUDED side observable, never folded into cost. */
now?: () => number;
/**
* design/138 S1 — the NEW-ARCHITECTURE memory seam (MemoryEngine over a {@link import("./memory-engine/types.js").MemoryBackend}).
* When set AND `spec.memory.enabled`, the task runs the INJECTION-FIRST file-based memory lifecycle
* (materialize → session file ops → harvest with the full gate set), the CC-verbatim `# Memory`
* instruction + fenced derived index replace the legacy `<user_memory>` block, and NO remember/recall
* tools are mounted (memory = the model's ordinary file skills). **S4: unset ⇒
* the task runs MEMORY-LESS** — the legacy `memoryStore` runtime path was retired (one deprecation
* warning via `onError` phase `"config"`); this field is the ONLY live memory seam.
*/
memoryBackend?: import("./memory-engine/types.js").MemoryBackend;
/**
* design/177 — the SHARED memory-store supply seam: connected team/deployment memory libraries the
* model may browse and read through the `memory_list` / `memory_read` tool pair. Presence of this dep
* IS the deployment's intent, so it is the single mount switch — there is no TaskSpec companion knob.
* Per-task removal goes through the existing `TaskSpec.excludeTools` valve (a true unmount).
*
* ORTHOGONAL to {@link memoryBackend}, deliberately and completely. That one is the LOCAL memory chain
* (fenced index injection, file projection, harvest, entry transactions with a write path); this one is
* a document-only READ face reached exclusively through tool calls. Shared content never enters the
* injected memory block or index, never lands on a model-writable directory, and is never harvested;
* the two seams are not bridged in either direction. Write governance for a shared store stays entirely
* with the host — core's model face has no write channel at all and only relays each store's
* `writable` bit.
*
* Unset ⇒ neither tool is mounted (not even a deferred placeholder).
*/
sharedMemoryStores?: import("./shared-memory/types.js").SharedMemoryStoreProvider;
/**
* design/138 §2.7 — the memory-engine directory CONFIG ROOT used when {@link memoryBackend} does not
* pin its own directory (a {@link import("./memory-engine/file-backend.js").FileMemoryEngineBackend}
* exposes `directoryRoot` and wins). The per-repo memory dir derives as `<root>/<repoKey>/memory`
* (repoKey = CC-style slug of the task root). Default: `AGENT_DATA_DIR` env or `~/.ai-agent`.
* Location is PURE CONFIG (§2.7 r2.1): a deployment may equally construct the FileBackend over an
* IN-REPO git-tracked directory for a project scope.
*/
memoryEngineDir?: string;
/**
* design/138 S2-B (O-F7/C-F3) — the Runner-owned harvest-report sink. Both harvest boundaries
* (the checkpoint mint inside commitSuspendSaga, `phase: "checkpoint"`, and the task terminal in
* runtask, `phase: "terminal"`) deliver their {@link import("./memory-engine/types.js").HarvestReport}
* here instead of dropping it — trace/accounting/alerting is the deployment's call. Swallow-guarded:
* a throwing consumer never fails the boundary. (The model-facing announcement lane is separate:
* the engine enqueues gate events at its harvest tail; the NEXT session's first injection drains
* them — 时机①.) Optional; unset ⇒ no behavior change beyond the report no longer being discarded.
*/
onMemoryHarvestReport?: (report: import("./memory-engine/types.js").HarvestReport, info: {
sessionId: string;
phase: "checkpoint" | "terminal";
}) => void;
/**
* design/113 C4 — proactive project startup. The Runner calls this once during `prepareTask` (every prepare,
* incl. durable resume — design/113 §4 H8) and injects the returned text as a READ-ONLY, FENCED, virtual
* `"project"` memory layer at the TAIL (so the agent "sees" what project it is in from turn 1, like CC's
* CLAUDE.md/git injection — the cure for "say hi → only greets"). The deployment/profile owns the CONTENT
* (read CLAUDE.md / `.claude/` + fold in `git log -5`/status + a byte budget); core owns the seam + fence +
* cache placement and NEVER hardcodes a filename (same trust posture as {@link executionEnvFactory}/
* {@link mods}/{@link modelKnowledgeCutoffs}). A RunnerDeps FUNCTION, not a `TaskSpec` field: it is
* unserializable (durable resume) and reading files for an UNTRUSTED caller would be a confused-deputy hole
* (design/44 §7 Q4). DECOUPLED from `memoryStore` — injects even with NO long-term memory configured (H5).
*
* `undefined` (default) ⇒ the whole path is skipped = byte-identical to today (full parity, H3). Returning
* `null`/blank ⇒ no injection (also byte-identical). The content is fenced/sanitized like every other memory
* layer (repo-controlled CLAUDE.md is UNTRUSTED data, not an instruction — H2). The ctx lets a profile tailor
* or suppress: `handsEnabled:false` = a hands-less chat with no real project (return null); `isSubagent` =
* a delegated child (a profile may trim CLAUDE.md for a cheap "grep this" subagent — H7).
*/
loadProjectMemory?: (ctx: {
/** The task's working directory (= the execution env's cwd; a worktree task = its worktree root). */
cwd: string;
/** False ⇒ a hands-less chat (no real filesystem/project) — a profile typically returns null. */
handsEnabled: boolean;
/** True ⇒ a delegated subagent run (a profile may trim/suppress project content to control cost). */
isSubagent: boolean;
/** The subagent's type name, when `isSubagent` — lets a profile tailor content per agent type. */
agentName?: string;
/**
* design-A C-1 ([A2]) — the acquired session's id (uuid domain; equals the run's `sessionId`).
* A deployment keys its SESSION-LEVEL snapshot cache on this (design-A §4.1 freeze unit: read
* once per session, reuse across the session's prepares) — core itself caches nothing across
* prepares. Always filled by the Runner; optional so hand-built ctx objects stay valid.
*/
sessionId?: string;
/** The run this prepare belongs to — the id the `task.start` trace frame, `noteTaskRun` and `TaskResult.runId` carry. Always filled by the Runner. */
runId?: string;
/**
* design-A C-1/C-2 ([A2]) — where this prepare sits in the snapshot lifecycle; the deployment's
* cache-invalidation signal (core only derives and forwards it, and ignores the answer):
* - `"fresh"` — no prior context: a brand-new session (no `spec.sessionId`, or an empty one).
* - `"resume"` — an existing session's context continues (a durable-resume leg, a same-session
* follow-up run, or a `resumeAt` rewind): a cached snapshot is still the one
* the model has been seeing.
* - `"post-compact"`— a compaction landed since the model's last full turn on this branch (branch
* tail shows a `compaction` entry with no *user* message after it): the context
* was rebuilt — the design-A §4.1 re-read boundary; a deployment should drop
* its snapshot cache and read fresh. HONEST APPROXIMATION (recorded, see
* docs/REVIEW-BACKLOG.md): a compaction followed by a mid-run user-role
* injection (steer) before the next prepare reads as `"resume"` — the freshness
* gap is covered by the C-4 `instructions_change` lane, not by this signal.
* - `"post-clear"` — RESERVED: core has no session-clear primitive today (a CC `/clear` maps to a
* NEW session ⇒ `"fresh"`; rewinding past the root is rejected), so core never
* emits this value — it exists so the seam's enum is stable when a clear verb lands.
*/
phase?: "fresh" | "resume" | "post-compact" | "post-clear";
}) => Promise<string | null | ProjectMemoryLoad> | string | null | ProjectMemoryLoad;
/**
* design-A C-4 ([A2]) — the mid-session instruction-file change PROBE (the server half of the
* `instructions_change` attachment lane). At eligible turn boundaries (continuation-gated like
* `date_change`) the run loop calls this with the snapshot's declared
* {@link ProjectMemoryLoad.instructionSources} and compares the answer against the lane's
* per-path last-announced hash; a moved hash appends a fixed ≤512-byte tail notice (the model is
* told the file changed and can Read the truth — the snapshot itself stays frozen, §4.1).
* Contract:
* - return `null` ⇒ this boundary is SKIPPED silently (nothing announced, nothing committed; the
* next boundary retries) — the fail-soft path for a remote cwd / EACCES / transient IO.
* - a per-entry `contentHash: null` ⇒ the file is GONE from disk (announced once as removed).
* - paths not in `sources` are ignored; the probe should never throw (core guards anyway and
* treats a throw exactly like `null`).
* - core never reads files here and never interprets the hash (opaque equality only — the same
* posture as `instructionSources`); the lstat/read/hash economics live in the deployment.
* Unset ⇒ the lane does not exist: zero calls, zero state, byte-identical output.
*/
probeInstructionSources?: (sources: ReadonlyArray<{
path: string;
contentHash: string | null;
}>) => Promise<ReadonlyArray<{
path: string;
contentHash: string | null;
}> | null>;
/**
* design/99 §E23 — the LIVE-ONLY inbound-elicitation seam (an MCP server asking the END USER for input
* mid-tool-call). Wired ONLY for servers that opted in via {@link McpServerSpec.elicitation}; absent ⇒ core
* advertises no elicitation capability (fail-closed). The deployment answers synchronously (e.g. prompts the
* terminal user); core replies to the server on the open connection.
*/
onElicit?: OnElicit;
/**
* design/116 CONFIRM-1 (加 seam 保体验): resize an over-limit MCP image so the model still SEES a
* degraded image instead of a spill-to-disk note. Absent ⇒ core auto-detects `sharp` (dynamic import;
* turnkey deployments with sharp installed get the CC experience with zero config) and otherwise spills.
*/
mcpImageResizer?: import("./mcp.js").McpImageResizer;
/** Default tool-call gate for all tasks (a task's own `toolPolicy` overrides this). */
toolPolicy?: import("./tool-policy.js").ToolPolicy;
/**
* design/277 — the deployment seat of the tool-registration MODEL GATE (default ON; the gate
* itself only ever touches entries tagged via {@link ToolSpec.modelGate}, so an untagged roster
* is byte-identical under any value here):
* · `false` — deployment kill switch: the gate never trims anything.
* · `{ classes }` — per-class rule rows merged into the built-in table
* ({@link import("./tool-model-gate.js").TOOL_MODEL_GATE_CLASSES}) PER AXIS: a present
* `floors`/`modelIds` axis REPLACES that axis, an absent axis INHERITS the built-in one — so
* a row adding only `modelIds` (the BYOM channel for gating a deployment's own strong model)
* keeps the built-in claude floors armed. `floors: []` / `modelIds: []` are the EXPLICIT
* per-axis clears; a class whose merged rule has both axes empty gates nothing (legal
* per-class off). New class names extend the open vocabulary for deployment-authored tags.
* The legality set is CLOSED (#123): any other shape — array/null/true seat, unknown keys at
* either level, malformed floors/modelIds rows, families that can never match the canonical id
* grammar, duplicate families/ids, `/`-prefixed ids — refuses the prepare loudly
* (`config.tool_model_gate_invalid`), never folds to a guess. Process-level counterpart: env
* `SEMA_TOOL_MODEL_GATE=off`. Removals are announced per (model, class) through `onNotice`
* (`config.tool_model_gate_removed`) — a silent default-face trim is forbidden (#237).
*/
toolModelGate?: false | {
classes?: Record<string, import("./tool-model-gate.js").ToolModelGateRule>;
};
/** Deployment default for {@link TaskSpec.basePolicyForResumeEdit} (#93 / F-012 L3): the resume-edit
* re-adjudication override. Resolution: `spec.basePolicyForResumeEdit ?? THIS ?? (spec.toolPolicy ??
* deps.toolPolicy)` — absence falls back to the caller policy, never to a silent skip. */
basePolicyForResumeEdit?: import("./tool-policy.js").ToolPolicy;
/**
* How `ask` decisions resolve when a policy/hook requests human confirmation (design/37). Default
* (omitted) = `"deny"`: **headless auto-deny** — no approver, so `ask` resolves deterministically to
* deny with a model-readable reason (the safe default for stateless automation). `"allow"` to
* auto-approve, or a fail-closed approver function. A task's own `onAsk` overrides this.
*/
onAsk?: import("./tool-policy.js").OnAsk;
/** Content-ask seam (design/64 §5): when set, every task mounts an AskUserQuestion tool that routes a
* model-issued question to this callback (a real human/UI). Distinct from `onAsk` (permission). A task's
* own `onQuestion` overrides this. Unset ⇒ the tool mounts only if another delivery face exists
* (durable park consumer / explicit `TaskSpec.interactiveTools` — see that knob's criterion table). */
onQuestion?: import("./ask-question.js").OnQuestion;
/**
* design/173 §3 — the deployment-level default interaction posture (see
* {@link TaskSpec.interactionPosture} for the full contract; the spec seat overrides per run, so
* one Runner can serve an interactive session AND cron/batch legs without the deps-level
* declaration refusing the batch legs). Absent = no posture check anywhere.
*/
interactionPosture?: "interactive" | "headless";
/** LSP code-intelligence seam (design/64 §13.1): when set, every task WITH HANDS mounts an `lsp` tool routed to this
* manager (a deployment-provided language-server bridge, e.g. an E2B LSP sidecar). The manager returns
* `undefined` for a file whose language has no server, and the tool degrades to "fall back to grep". A
* task's own `lspManager` overrides this. Neither seat set, or a leg without an execution env ⇒ the engine mounts no `lsp` tool. */
lspManager?: import("./lsp.js").LspServerManager;
/**
* The Artifacts hosting seat — a storage/service-tier seam of the same family as
* {@link checkpointStore} and {@link rosterStore}. Three states, judged once at prepare: ABSENT ⇒ no
* `Artifact` tool and no announcement (the default: a library deployment has no hosting); a host
* whose `capabilities()` satisfies the declaration invariants ⇒ ONE `Artifact` tool assembled from
* that declaration (`plane: "hosted"` mounts the hosted face over the declared action subset,
* `plane: "local-stub"` — `localArtifactStub({ dir })` — mounts the publish-only local face with
* `eval-stub://` links); a host whose declaration fails an invariant ⇒ no tool and a loud
* `config.artifact_host_invalid` notice. Filling the seat IS the authorization to publish: there is
* no second switch. `TaskSpec.excludeTools` unmounts the tool as it does any other.
*/
artifactHost?: import("./artifact-host.js").ArtifactHost;
/**
* The deployment seat of the mods bus: extension registrations every task of this Runner carries (every leg — root,
* delegated child, resume — admits them at its own prepare). Each declares its tier (`prepend`, `user` or `append`;
* absent or another word refuses the prepare, `config.mods_invalid`). A task's own `TaskSpec.mods` run beside these,
* never instead of them. A `classic.PreToolUse` handler's `allow` never bypasses `toolPolicy` — the policy is always
* the final say.
*/
mods?: readonly import("./mods/kernel/index.js").ModRegistration<import("./mods/dollar.js").EngineDollar>[];
/**
* Policy for remote image URLs (`TaskSpec.images[].url`). Return false to reject. When set it
* **replaces** the built-in SSRF guard (https-only + private/reserved-host block) — e.g. supply a
* CDN allowlist, or a stricter resolve-and-pin check. Omit to use the safe default.
*/
allowImageUrl?: (url: string) => boolean;
/**
* Optional sink for best-effort background errors/warnings that are otherwise swallowed:
* - `"compaction"` — a compaction failure (F8); a persistently failing one (flaky gateway, empty
* summaries) is invisible without this and lets a session grow until it tops the window.
* - `"prompt-cache"` — a prefix-cache hazard on the wire: a detected cache break (design/31), or a
* mis-declared/mismatched `promptCacheFamily`. The prefix cache will miss. Log/alert on both.
* - `"prompt-constitution"` — a prompt overlay did not take effect as written: a later source (or a later declaration in the same source) superseded
* it on the same target, its target was not in this run's prompt, or a mods handler's `prompt.compose` answer was skipped (it rewrote another's
* overlay, or wrote what the band table does not grant its seat); each overlay is named once per run. The center prompt too: a pinned session's
* stored artifact refused or unreadable (it fails `prompt.snapshot_unavailable`), a fresh session's candidate that does not verify, and a
* `CenterPromptSource` boot restore with no usable copy (`sessionId: ""`, `classification: "center-source-restore"`) — that last line only when the
* host passes this same sink to `CenterPromptSource` as its own `onError` (the source restores at boot, apart from any Runner; given none, it is silent).
* - `"degraded"` — the task kept running with a capability quietly reduced: a question auto-answered with no human
* present, a skipped env git snapshot, a lost checkpoint-put confirmation, an unroutable descendant notification, and
* similar best-effort arms. The run proceeds; the reduced arm is what is being disclosed (`classification` names it).
* - `"config"` — a deployment-wiring problem detected while preparing or tearing down a task: tool mount conflicts,
* refused/ignored knob values, gate-off notices, store/env teardown legs. The broadest class by call-site count — most
* misconfigurations announce here rather than failing the task.
* - `"memory"` — a post-task memory-consolidation pass failed or skipped a malformed reconcile
* decision (design/41). Best-effort: the notes the model saved are kept; the task still succeeds.
* - `"mcp"` — an MCP server failed to connect / list its tools and was skipped (fail-open, design/29);
* the task runs with the remaining servers' tools. One call per failed server.
* - `"a2a"` — the A2A sibling of `"mcp"`: a declared peer's agent card could not be fetched/read, or it
* advertises no transport this client speaks, so the peer was skipped. One call per skipped peer.
* - `"interrupt-reconcile"` — repairing an interrupted session's transcript on re-entry (synthesizing
* tool results for orphaned calls, flushing held session writes) failed; the run proceeds on the
* unrepaired transcript.
* - `"suggestions"` — the best-effort follow-up-suggestions pass failed or timed out; the result
* simply carries no suggestions.
* - `"rewind"` — the file-history machinery backing rewind reported a best-effort failure: a
* turn-start boundary capture failed or timed out (that turn cannot be rewound to; the turn
* itself proceeds), or a first-touch failure was tolerated under
* `onTrackFailure:"proceed-unprotected"`. (design/381: the whole-tree size gate and its
* `classification:"too_large"` payload are retired — per-edited-file history has no size axis.)
* - `"hook"` — a deployment hook misbehaved: a callback threw, or returned a verdict it was not
* allowed to (e.g. a declared-observational hook). The engine applies the hook's own documented
* fallback (swallow, or fail-closed deny, per its contract) and reports the fact here.
*/
onError?: (err: unknown, context: {
phase: "compaction" | "prompt-cache" | "prompt-constitution" | "degraded" | "config" | "memory" | "mcp" | "a2a" | "interrupt-reconcile" | "suggestions" | "rewind" | "hook";
sessionId: string;
/**
* Machine-readable sub-classification when the site has one (a kebab-case word naming the arm) —
* e.g. the design/31 cache-break detector's root cause (`"model-switch" | "tool-schema" |
* "tool-set" | "system-prefix" | "server-or-ttl"`), or the file-history liveness arms
* (`"file-history-no-store"` / `"file-history-no-env"`). Lets the host pick its own log level:
* `"server-or-ttl"` is usually benign in agentic tasks (slow tools ⇒ 5min+ request gaps expire
* provider caches — service[64] flag②) and warrants info/warn, while the prefix-bug causes
* warrant attention.
*/
classification?: string;
}) => void;
/**
* Structured sink for operator-facing NOTICES ({@link EngineNotice}) — announcements that are not
* errors and do not affect the run, which the engine otherwise prints via `console.warn` (e.g. a
* configured timeout/env value discarded in favor of another). When wired, a notice goes HERE
* INSTEAD of `console.warn` (structured replaces the console line — a host forwarding notices to
* its own surface must not show every fact twice); when absent, the historic `console.warn` line
* is printed verbatim, so an unwired build keeps its exact loudness. Swallow-guarded at every
* emission point (`onError`/`tracer` posture): neither a throwing sink nor an async sink's
* rejected promise ever affects the run. A PRESENT NON-function value (an untyped host wiring
* null/junk) is a bad deployment value, not a wired sink: every notice then falls back to the
* `console.warn` line and the seat defect itself is announced once per process (#170 — a bad seat
* must not silence both channels). Per-process announcement de-duplication is unchanged and
* sits BEFORE the sink branch — a deduplicated repeat reaches neither channel.
*/
onNotice?: (notice: EngineNotice) => void;
/** Best-effort fire-and-forget trace sink (task/turn/brain/tool); `TaskSpec.tracer` overrides per task. */
tracer?: import("./trace.js").TracerHook;
/**
* design/73 §1 v1 — fire-and-forget sink for {@link import("./task-outcome.js").TaskOutcome} facts
* (the feedback ledger's core seam; tracer/onError posture — swallow-guarded, a throwing sink never
* affects the run). 🔴 Red lines: core emits ONLY the `mechanical` tier (objective-oracle results —
* llm_assisted/self-report never enter v1); every outcome carries `oracleHadRedRun`; ONE emission
* point — `runGoal`'s terminal state (goal `doneCheck` = the natural mechanical oracle) plus explicit
* `emitTaskOutcome` from harnesses that own a real oracle. Plain `runTask` never auto-emits (no
* objective oracle → no fact). core emits facts only; aggregation/policy live in the service ledger.
*/
onTaskOutcome?: (outcome: import("./task-outcome.js").TaskOutcome) => void;
/** Per-model pricing (per-1M absolute USD) keyed by model id; falls back to each `Model.cost`. */
pricing?: Record<string, import("./pricing.js").ModelPricing>;
/**
* The execution environment the agent's "hand" acts on (design/44) — file system + shell. Default:
* `StubExecutionEnv` (no real I/O, current behavior). Inject `NodeExecutionEnv` for local fs/shell, or
* a sandboxed/remote impl for multi-tenant isolation. **Injecting a non-stub env is the opt-in** that
* activates the file tool band. ⚠️ A bare `NodeExecutionEnv` is unsandboxed — a multi-tenant deployment
* MUST inject a chroot/container/per-principal-scoped impl; do not expose raw `NodeExecutionEnv`.
*/
executionEnv?: import("../internal/harness.js").ExecutionEnv;
/**
* design/48 remote seam — a **trusted control-plane** factory that mints a FRESH execution env per
* task (e.g. one isolated container per task in the remote model). Called once at task start; the env
* it returns is owned by that task and released on task end. **Wins over** the shared static
* `executionEnv`. Injecting it also activates the hand tool band (design/44), exactly like
* `executionEnv`. This is the ONLY per-task env injection point — `TaskSpec` deliberately has no
* `executionEnv`, so an untrusted caller can't swap the sandbox (design/44 §7 Q4); the trusted
* deployment wires the factory.
*/
executionEnvFactory?: import("./remote-env.js").ExecutionEnvFactory;
/**
* Containment root for the file tools (design/44 §4 invariant 7): every path must resolve within it,
* else the tool rejects (`PathNotInRoot`). Default `process.cwd()`. A defense-in-depth guard rail at the
* tool layer — NOT a sandbox (it does not constrain `bash`; that needs OS-level containment) and not a
* substitute for an isolated `executionEnv`.
*/
rootPath?: string;
/**
* Backing store for offloaded large tool results (design/30). Default: a task-scoped
* `InMemoryToolResultStore` (no cross-wake durability). Inject a durable store (keyed by ref) so an
* offloaded result survives wake/resume.
*/
toolResultStore?: import("./tool-result-store.js").ToolResultStore;
/**
* Offload a tool result to the store + leave a preview when its text exceeds this many chars. Default
* ~20000; set `0` or `Infinity` to disable offloading. Per-tool override via `ToolSpec.offloadThresholdChars`.
*/
toolResultThresholdChars?: number;
/**
* design/374 — the microCompact knobs. Both default ON (`machine: "cc"`, `clearOnRejection: true`).
*
* - `machine`: `"cc"` (default: the CC 2.1.223 rejection-leg form — keep 5, ≥20k minimum-savings
* gate, one deep clear beyond the keep window, CC marker bytes) or `"off"` (no proactive
* frontier clearing at all — the D-2 off switch; the machine then gets its ONE shot at the
* blocking point instead, the slice-3 arm A, trace trigger `"blocking"`). The vocabulary is
* {@link import("./context-edit.js").MICRO_COMPACT_MACHINES}; the guard chain behind either
* value is the same (blocking-point run → in-turn forced compaction behind the adopt seam →
* trim demoted to the disaster-only last resort), so an under-20k refusal never falls straight
* into a message-dropping trim.
* - `clearOnRejection` (MC-R, slice 2): on a provider input-too-long rejection, run ONE cheap
* deterministic clear over the rejected projection (the same machine, savings ≥20k or nothing)
* and retry inside the turn BEFORE the forced-compaction recovery. Default true. Independent of
* `machine` — an enabled MC-R clears even with the frontier pass `"off"`. BUDGET ACCOUNTING (design/374
* §3.2.1, stated here because it is otherwise invisible to a deployment): a successful MC-R
* clear-and-retry SPENDS one attempt of the shared prompt-too-long recovery budget (default 2
* attempts per chain), so a chain that clears and is rejected AGAIN has one forced-compaction
* attempt left where the knob-off chain nominally had two — the trade costs no effective
* compaction pass, because the second forced-compaction call of the off chain is structurally
* a no-op whenever the first one landed (the branch leaf is already a compaction entry).
*
* A declaration outside the closed vocabulary (a `machine` string not in the union, a
* non-boolean `clearOnRejection` — JSON/env-derived config the type cannot guard) refuses the
* whole prepare loudly (`code: "config.microcompact_invalid"`, no silent re-default): folding it
* would silently run the DEFAULT machine while the deployment believes its declaration took
* effect.
*/
microCompact?: {
machine?: import("./context-edit.js").MicroCompactMachine;
clearOnRejection?: boolean;
};
/**
* Two-phase prefix-cache-break detection (design/31): per turn, fingerprint the prefix and, on a
* confirmed `cacheRead` drop, emit a root-cause finding via `onError(phase:"prompt-cache")`. Cheap
* (hashes only) and only fires on a detected break. Default **on**; set `false` to disable.
*/
cacheBreakDetection?: boolean;
/**
* Aggregate inline-MEDIA (image) byte budget for the request (batch-C/§17.2 sibling of the text budget;
* CC 2.1.196 `tengu_media_byte_cap` parity). Image reads (`Read` → ImageContent) are per-image-capped but
* accumulate across turns; this bounds the SUM in the request by stripping the OLDEST images (replaced with a
* marker) — request-only, non-destructive (the durable session keeps the image; the model can re-Read it).
* The same budget bounds the media of one `tool_end.output` (spent there as base64 on the wire, x 4/3); both readers
* read it through `mediaBudgetBytes`. Default {@link AGGREGATE_MEDIA_BUDGET_BYTES}; set `Infinity`/`0` to disable. A
* non-number, `NaN` or a negative is refused at `new Runner` (`config.invalid_argument`), never read as "no budget".
*/
mediaByteCapBytes?: number;
/** Optional telemetry fired when {@link mediaByteCapBytes} strips media in a request (CC's
* `tengu_media_byte_cap_stripped` fields). Pure observability — no effect on the request. */
onMediaStripped?: (info: {
totalBytes: number;
limitBytes: number;
removedCount: number;
removedBytes: number;
}) => void;
/**
* Deferred-tool disclosure policy (design/36). Two rules govern deferral. ① The caller's list is
* literal: {@link TaskSpec.deferTools} names defer on every request, `ToolSpec.alwaysLoad` /
* {@link TaskSpec.alwaysLoadTools} pins inline on every request (judged first) — neither reads this
* knob. ② The engine's own candidates — `ToolSpec.defer === true`, every REMOTE-PROTOCOL tool name
* (MCP **and** A2A peer tools), and built-in tools that declare their own `defer` — sit behind ONE
* threshold gate, all or nothing: deferred iff their inlined size (name + description + schema; the
* candidate set only — the caller's list, pins and non-candidates are not measured) reaches the
* {@link toolSearch} threshold (10% of the model's context window when that seat is absent); below that
* they ship inline in full and no ToolSearch is mounted.
*
* `"auto"` is a CANDIDATE-WIDENING knob, not a second gate: it adds every caller spec — undeclared
* caller tools included — to the candidate set, measured and decided by the same threshold. Engine
* built-ins the deployment did not name are candidates under no setting. An explicit opt-in (council
* minor #6) because it makes `tools[]` depend on the caller roster's total weight, which a deployment
* with many thin tools may not want. Any other value refuses at prepare (`config.defer_mode_invalid`).
* `classifyDeferred` / `deferralGate` in `src/core/runner/tool-disclosure.ts` are the single authority
* if this text drifts.
*/
deferMode?: "auto";
/**
* Rule ②'s GATE POLICY — when the engine's candidate set ships as placeholders — in three states:
* `true` (always), `false` (never; with no caller list no ToolSearch mounts at all), `{mode:"auto",
* thresholdPercent}` (the threshold gate at that percent of the model's context window). Absent ⇒
* `{mode:"auto", thresholdPercent:10}`. ORTHOGONAL to {@link deferMode}, which decides WHICH tools are
* candidates; neither touches rule ① ({@link TaskSpec.deferTools} still defers,
* {@link TaskSpec.alwaysLoadTools} still pins first). A value outside the domain refuses at prepare
* (`config.tool_search_invalid`), a `thresholdPercent` outside a finite `[0, 100]` included.
*/
toolSearch?: import("./tool-search-setting.js").ToolSearchSetting;
/**
* design/84 Seam C — compact-boundary COST optimization (data-gated, default OFF). A FUNCTION seam (a
* trusted control-plane injection), so it lives on `RunnerDeps` — NOT `TaskSpec`: `TaskSpec` is the
* serializable, untrusted-caller, durable-resume-round-trippable per-task config (a function can be
* neither serialized nor durably checkpointed, and the sandbox/env-class injections deliberately sit on
* `RunnerDeps` for the same trust reason). Same placement as `mods` / `executionEnvFactory`.
*
* When `summaryProvider` is set, BOTH compaction call sites (the within-task turn boundary AND `finish()`
* at task end) consult it: a NON-EMPTY string is reused verbatim (the real `generateSummary` LLM call is
* skipped); `null`/`undefined`/blank falls back to the existing LLM path (byte-identical to today).
* `onCompaction` is a pure post-compaction observer. `maxConsecutiveProviderReuse` (default 3, CC parity)
* forces a real summary after N consecutive reuses to bound drift; the Runner OWNS the consecutive-reuse
* counter across both call sites for the task (reset on a real summary, incremented on a reused one), so
* the caller does not have to thread it back. **Absent (default) ⇒ no behavior change whatsoever.**
*
* SCOPE (design/141 examples 批实测): the seam covers the whole-turn compaction sites only. A
* SPLIT-TURN cut point (mid-turn prefix summarization — the common shape inside a long single-user-
* message tool loop) bypasses the provider by design (A2 algorithm sovereignty): the turn-prefix
* summary is engine-owned. Deployments needing a clean provider-visible boundary should shape work
* as separate tasks sharing `sessionId`.
*/
summaryProvider?: import("./auto-compaction.js").MaybeCompactOptions["summaryProvider"];
/** design/84 Seam C — pure post-compaction observer (fires only on a real compaction, never a no-op).
* Consumed ONLY when `summaryProvider` is also set (the Seam C options early-return otherwise) — a
* pure-observer deployment passes a provider that always returns `null` (keeps the LLM path intact). */
onCompaction?: import("./auto-compaction.js").MaybeCompactOptions["onCompaction"];
/** design/84 Seam C — force a real `generateSummary` after this many consecutive provider reuses (default
* 3, CC parity). The Runner tracks the consecutive-reuse counter per task across both compaction sites. */
maxConsecutiveProviderReuse?: number;
}