@sema-agent/core
Version:
Stateless, task-oriented AI agent core
680 lines • 54.5 kB
TypeScript
import type { TSchema } from "typebox";
import type { InheritedGate, RunnerSelfSeat } from "../core/runner/contracts.js";
import type { CheckpointStore, CheckpointToken, ResumeOutcome } from "../core/checkpoint-store.js";
import type { AgentDefinition, TaskEvent, TaskResult, TaskSpec, ThinkingLevel } from "../core/types.js";
import { type ParentSpecSeat } from "../core/parent-spec-seat.js";
import type { WorkflowRunStore } from "../core/workflow-run-store.js";
import type { WorkflowJournalStore, ResumeClaimArgs } from "../core/workflow-journal-store.js";
import type { WorkflowRun, WorkflowEvent } from "./workflow-types.js";
export * from "./workflow-types.js";
/** 198 `P0m` (pretty.js:447248) — prose state: the child's final text is the script-facing return value. */
export declare const WORKFLOW_SUBAGENT_PROMPT = "You are a subagent spawned by a workflow orchestration script. Use the tools available to complete the task.\n\nCRITICAL: Your final text response is returned **verbatim** as a string to the calling script \u2014 it is your return value, not a message to a human.\n- Output the literal result (data, JSON, text). Do NOT output confirmations like \"Done.\" or \"Sent.\"\n- If asked for JSON, return ONLY the raw JSON \u2014 no code fences, no prose, no markdown.\n- Do NOT address a human user \u2014 there is no user on the other end. Put your answer in your final text response.\n- Do NOT delegate to further sub-agents or start nested workflows; complete the task yourself.\n- Be concise. The script will parse your output.";
/** 198 `M0m` (pretty.js:447333) — schema state: the answer goes through the StructuredOutput tool, once. */
export declare const WORKFLOW_SUBAGENT_PROMPT_SCHEMA = "You are a subagent spawned by a workflow orchestration script. Use the tools available to complete the task.\n\nCRITICAL: You MUST call the StructuredOutput tool exactly once to return your final answer. The tool's input schema defines the required shape.\n- Do your work (Read files, run commands, etc.), then call StructuredOutput with your answer.\n- Do NOT put your answer in a text response. The script reads ONLY the StructuredOutput tool call.\n- If the schema validation fails, read the error and call StructuredOutput again with a corrected shape.\n- After calling StructuredOutput successfully, end your turn. No acknowledgment needed.\n- Do NOT delegate to further sub-agents or start nested workflows; complete the task yourself.";
/** 198 composite prose-append `O0m` (pretty.js:447255) — a script-supplied CUSTOM persona gets the return
* contract APPENDED (never replaced), mirroring 198's `opts.agentType` + schema composition. VERBATIM
* (PARITY-SPOT-WORKFLOW F6/B6): 198's constant opens with a `---` separator line and closes with the
* "Output the literal result; do not output confirmations…" tail — the previous sema paraphrase drifted. */
export declare const WORKFLOW_SUBAGENT_APPEND = "---\n\nNOTE: You are running inside a workflow script. Your final text response is returned verbatim as a string to the calling script \u2014 it is your return value, not a message to a human. Output the literal result; do not output confirmations like \"Done.\" Be concise \u2014 the script will parse your output.";
/** 198 composite schema-append `L0m` (pretty.js:447328) — VERBATIM incl. the `---` opener (F6/B6; the old
* sema text compressed the "Do your work, then call X" instruction out). */
export declare const WORKFLOW_SUBAGENT_APPEND_SCHEMA = "---\n\nNOTE: You are running inside a workflow script. You MUST return your final answer by calling the StructuredOutput tool exactly once \u2014 the tool's input schema defines the required shape. Do your work, then call StructuredOutput; do NOT put your answer in a text response (the script reads ONLY the tool call). If validation fails, read the error and call StructuredOutput again with a corrected shape.";
/**
* Apply the G5 default persona to a `ctx.agent`/`ctx.agentStream` child spec. Two-state on the EFFECTIVE
* schema (`agentOpts.schema` wins over a spec-carried `outputSchema`, same as the runSpec injection):
* • no `spec.systemPrompt` → the dedicated workflow-subagent persona REPLACES the role base (198
* default `workflow-subagent` agent-type semantics) — reached only when neither the script, the
* agentType definition nor the deployment's stated subagent preset supplied one ({@link withWorkflowChildSeats}
* writes the preset first);
* • a `spec.systemPrompt` (custom persona: script, definition or stated preset) → the matching NOTE is
* APPENDED via `appendSystemPrompt` (198 composite semantics — append, never replace), after any existing append.
* Called AFTER the call-key is computed (the journal identity keys the AUTHORED spec, so a resume across
* core versions replays cleanly; the persona is an execution detail, not call identity).
*/
/**
* F4 agentType (CC 198 锚 pretty.js:446608-446627): resolve `opts.agentType` against the registry
* (deployment SHADOW over built-ins) and fold the definition into the child spec — persona as
* `systemPrompt` (so {@link withWorkflowChildPersona} composes the return-contract NOTE via the
* custom-persona APPEND arm = CC `O0m` semantics), model/thinking/maxTurns/skills/memory/
* memoryPersistenceCapable when the spec didn't pin them, and allow/denyTools as a ToolPolicy
* (combined deny-wins with any spec policy).
* The definition is DEPLOYMENT-TRUSTED (registry-declared, not script-authored), so its model bypasses
* the script-facing modelName allowlist by design — same trust tier as the Agent tool's registry.
*/
export declare function applyWorkflowAgentType(spec: TaskSpec, agentType: string | undefined, registry: AgentDefinition[]): TaskSpec;
/** The agentType registry for one workflow run: built-ins (unless opted out) with deployment SHADOW. */
export declare function workflowAgentRegistry(opts: {
agents?: AgentDefinition[];
builtinAgents?: boolean;
}): AgentDefinition[];
/**
* design/97 CORE-1 (foundation) — the STABLE, deterministic identity of one `ctx.agent` call, so the resume
* journal (CORE-7) can key a cached result and the observe tree (CORE-3/5) can hang per-agent data off it.
* `ordinal` is the synchronous call-order index (`run.agents.length` at RECORD time — captured BEFORE the
* `sem.acquire` await, so for `parallel(direct thunks)`/sequential it is deterministic run-to-run. `pipeline`'s
* stage-2+ ordinals are LATENCY-dependent (NOT deterministic) — so `pipeline` conservatively forces replay
* divergence and runs live on resume; deterministic stage-scoped keys are a CORE-7.1 follow-on. The hash is over
* the WHOLE behaviour-defining spec (audit fix — a cherry-pick missed images/thinking/limits/maxTokens/model
* runtime fields), with non-behavioural identity (taskId/sessionId/signal) stripped and tools/mcp reduced to
* NAMES (their closures aren't serializable). SHARED pure fn (anti-drift): the journal MUST key through this.
*/
export declare function workflowAgentCallKey(ordinal: number, spec: TaskSpec, opts: {
schema?: TSchema;
isolation?: "worktree";
}): string;
/**
* RB-394 — runtime rejection of an unsupported `isolation` value at the workflow spawn
* boundary. The TYPE (`isolation?: "worktree"`) only binds TS callers: a workflow script is plain JS at
* runtime, so a value like `"remote"` previously sailed through every `=== "worktree"` consumption gate
* as a truthy non-match — the trusted internals channel carried it verbatim, prepareTask's fail-closed
* worktree enforcement never fired (it keys on `=== "worktree"` too), and the agent ran SILENTLY in the
* shared working tree: the exact degrade the worktree contract calls out as forbidden. The same value at
* the Agent tool boundary dies a loud schema rejection — this is that boundary's workflow-lane
* counterpart. The literal `"worktree"` and absence pass unchanged. Module-level export for the script
* membrane (`safeAgentOptions` in workflow-primitives.ts, which must REJECT rather than silently DROP the
* value — the drop re-created the identical silent outcome on the LLM-script lane); not on the package face.
*
* RB-425 — ABSENCE has more than one runtime spelling. `null` / `false` / `""` is what a wire-parsed or
* defensively-defaulted script option looks like when the field is simply NOT SET, and every downstream
* consumption gate (`=== "worktree"`, or plain truthiness at the internals-channel and advisory sites)
* already treats them identically to `undefined`. Rejecting them turned a no-op option into a hard spawn
* failure while changing nothing about where the agent would actually run — a fail-closed gate firing
* where there is nothing to close. Only a TRUTHY non-match is the hazard above: a value that LOOKS like a
* request but silently degrades to the shared tree. That still dies loud.
*
* Not a narrowing assertion: the accepted set is "worktree" plus every falsy spelling of absence, which
* no type predicate states truthfully — and no caller needs the narrowing (all three read the value with
* `=== "worktree"` afterwards).
*/
export declare function assertSupportedAgentIsolation(isolation: unknown): void;
/** One failed fan-out slot, reported through the opt-in `errors` collector of `ctx.parallel`/`ctx.pipeline`.
* The default fan-out contract is unchanged (a thrown slot folds to `null`); a script that passes a
* collector array can discriminate WHY a slot folded (budget vs schema vs other) without any change to the
* shape of the results array. */
export interface WorkflowFanOutSlotError {
/** The failed slot's index in the input array (thunk index for `parallel`, item index for `pipeline`). */
index: number;
/** A stable discriminator: the error's `code` when it carries one (typed workflow errors), else the error
* class name, else `"Error"`. */
kind: string;
/** Bounded, redacted message summary (same egress discipline as the run log). */
message: string;
}
/** Opt-in options for `ctx.parallel` (second argument) and `ctx.pipeline` (trailing argument after the
* stages). Purely observational — omitting it keeps the exact legacy behavior. */
export interface WorkflowFanOutOptions {
/** Caller-supplied array; the engine pushes one {@link WorkflowFanOutSlotError} per failed slot (including
* budget-dropped slots, which fold silently otherwise). Results still fold to `null`. */
errors?: WorkflowFanOutSlotError[];
}
/** design/98 §D.6 hard cap: thrown when the script's returned result exceeds `maxResultChars` (a runaway
* script must not return an unbounded payload to the originator). */
export declare class WorkflowResultTooLargeError extends Error {
readonly size: number;
readonly max: number;
readonly code = "workflow.result_too_large";
constructor(size: number, max: number);
}
/** A resume journal entry whose result carries no terminal cause — a journal written before
* `TaskResult.terminal` existed, or a malformed entry — is INCOMPATIBLE, and the resume is refused
* before any work is dispatched. The alternative (letting the entry diverge into a live re-run) would
* execute the already-completed prefix again, side effects included; a schema mismatch is never a
* permission to rerun finished work. Start a fresh run instead of resuming such a journal. */
export declare class WorkflowJournalIncompatibleError extends Error {
readonly fromRunId: string;
readonly ordinal: number;
readonly code = "workflow.journal_incompatible";
constructor(fromRunId: string, ordinal: number);
}
/**
* A TRUSTED, run-scoped internal channel carrying the workflow **nesting depth** across a process boundary
* (design/98 §0.1 BLOCKER3). It is NOT a `TaskSpec` field and NOT a `run_workflow` tool param — a worker or
* an LLM-authored script can never set it. When a deployment (e.g. the service) initiates a workflow on
* behalf of a parent run that is itself inside a workflow, it passes `workflowDepth = parentDepth + 1` here
* so the entry guard fires cross-process. In-process nesting needs no internals — the
* {@link workflowDepthStore} AsyncLocalStorage propagates depth into every `runner.runTask` child
* automatically. `startWorkflow` takes depth = `internals?.workflowDepth ?? ALS.depth ?? 0`.
*/
export interface WorkflowInternals {
workflowDepth?: number;
}
export interface WorkflowBudget {
/** The token ceiling, or null when none was set (then `remaining()` is Infinity). */
readonly total: number | null;
/** Cumulative tokens spent so far (own + nested across all agents). AUTHORITATIVE terminal accounting
* only: it advances when an agent attempt SETTLES. The per-turn runtime usage beat feeds the
* `run.stats` observation face (monitor rows move while a long agent runs) but never charges the
* budget gate — a provisional figure that may be rolled back must never have gated a spawn. */
spent(): number;
/** `max(0, total - spent())`, or Infinity when no budget was set. */
remaining(): number;
}
export interface WorkflowAgentOptions {
/** Display label for this agent-run (else `agent-N`). */
label?: string;
/**
* F4 (CC 198 parity, 锚 pretty.js:446608-446627; DEFER 解除 2026-07-11 — 前提「无 agent 注册表」被
* 1.262.0 agents F1 推翻): run this agent AS a named agent type from the deployment's registry
* (built-in Explore/Plan + `RunWorkflowOptions.agents`, deployment SHADOW wins on a name collision —
* the same registry the Agent tool's `subagent_type` resolves). The definition supplies persona
* (composed with the workflow return-contract NOTE — CC `O0m` append semantics), model (tier words OK,
* the catalog is tier-expanded), thinking, maxTurns, skills, memory and the allow/deny tool policy.
* An unknown name throws a teaching error listing the known types. Slot-tools carrier (design/140
* ②-3): a collab member slot's tool surface = its agent type's allow/denyTools — no new mechanism.
*/
agentType?: string;
/** Explicitly group this agent under a phase title (use this inside `parallel`/`pipeline` where the
* global enclosing-phase state races — design/97 §D race note). */
phase?: string;
/** Per-agent cancellation; falls back to the workflow signal. */
signal?: AbortSignal;
/**
* Force a STRUCTURED result for this sub-agent (design/98 §0.2). Sets the child task's `outputSchema` to
* this typebox schema; the validated object is surfaced as `TaskResult.structuredOutput`. **Strict**: if
* the child COMPLETES without a `structuredOutput` (the model answered in prose — core permits that),
* `ctx.agent` throws {@link WorkflowAgentSchemaError} rather than silently returning `undefined`. A child
* that ends non-`completed` is returned as-is (the script checks `status`). If the script's `spec` already
* carried an `outputSchema`, this option wins.
*/
schema?: TSchema;
/**
* design/97 CORE-6: run this agent in an ISOLATED git WORKTREE so parallel agents editing the same repo
* don't conflict. A SCRIPT-FACING OPTION (NOT a TaskSpec field — an untrusted spec can't self-select an env,
* design/44 §7 Q4): threaded via the TRUSTED RunInternals.isolation to the control-plane executionEnvFactory,
* which mints a worktree-rooted env (e.g. {@link addWorktree}). The worktree is INSIDE the one configured env
* (local OR a single E2B) — not a per-agent container. ISOLATE-ONLY: the runtime never merges; the
* orchestrator script reads each worktree's result and decides verify/merge in userland. Requires a
* worktree-capable `executionEnvFactory` — FAIL-CLOSED: a request the deployment cannot honor fails the
* spawn at prepare time (the child never starts; no silent fallback to the shared working tree, which
* would be unsafe under a relaxed `WorkflowGovernanceBaseline.worktreeBase`). Durable-suspend is
* incompatible (the factory env is non-remote).
*/
isolation?: "worktree";
}
/**
* design/97 CORE-5 (#6 steer) — the handle returned by {@link WorkflowRunContext.agentStream}: a STILL-RUNNING
* agent the launcher can STEER mid-flight and then await. The recording (callKey/groupId/prompt + stats/output +
* `agent_end`) and the concurrency slot are tied to the underlying stream settling (an eager completion), NOT to
* `result()` being called — so a script that defers `result()` still records and releases. ⚠️ EXCEPTION (audit
* MAJOR): if the script returns and the run FINALIZES before the stream settles, the eager completion hits the
* finalized-guard and skips the trailing record — so a fire-and-forget stream the body never awaits may be left
* as `running`. A leader that wants the outcome (the normal case) awaits `result()`, which is fully recorded.
*/
export interface WorkflowAgentHandle {
/** The enclosing workflow run id (NOT a per-agent id — agentStream does not mint a sub-run). */
runId: string;
/** This agent's display label. */
label: string;
/** This agent's deterministic {@link workflowAgentCallKey}. */
callKey: string;
/**
* Inject an operator/leader STEER into the running worker (design/47 `TaskStream.steer`). The content is
* FENCED (untrusted data — it can't pose as authority) inside a TRUSTED framing that asks the worker to TAG
* its reply with the returned marker. Returns that MARKER so the launcher can correlate the worker's tagged
* reply via the #5 transcript (the worker self-stamps; reply is best-effort). One-directional + leader-driven
* (the worker can't address the leader except by the marker). A steer issued BEFORE the worker's loop goes
* live is parked and delivered into the NEXT turn's context (birth-window delivery, bounded — ledger item 36); steers are
* delivered in call order, the birth window included. Rejects with `steering.not_running` once the task has
* finished (teardown included).
*
* `opts.inputId` is a PASS-THROUGH of the underlying `TaskStream.steer` correlation/idempotency key
* (design/171 §6.3 — its whole contract, value domain and typed refusals are that verb's; absent ⇒ byte-identical
* to every pre-existing call). A launcher whose own ingress already minted a message id passes it here so the two
* legs of one steering ingress are equally replay-safe. ⚠️ ONE arm of that contract is NOT reachable through this
* handle, deliberately: the idempotent-REPLAY arm ("same id, same payload ⇒ injects nothing"). Each call mints a
* FRESH unguessable correlation marker into the framing it delivers, so a retry under the same id is a
* same-id-DIFFERENT-instruction call and refuses typed `steering.duplicate_input_id`. What the key buys on this
* lane is therefore AT-MOST-ONCE: a retried ingress is refused LOUDLY instead of injecting a second copy.
*/
steer(content: string, opts?: {
inputId?: string;
}): Promise<string>;
/** Await the agent's {@link TaskResult} (the same value the eager recording used; idempotent). */
result(): Promise<TaskResult>;
}
export interface WorkflowRunContext {
readonly runId: string;
readonly budget: WorkflowBudget;
/** The composed workflow abort signal (caller signal + `cancel()` + `totalTimeoutMs` deadline). A script
* runner (the hard sandbox) should thread THIS into its execution so a cancel/timeout actually aborts the
* script body — not just the agents it spawns (workflow review, caps). */
readonly signal: AbortSignal;
/** Run one `runner.runTask(spec)` as a recorded agent-run (script-driven, NOT an LLM delegation). Returns
* the TaskResult (any terminal status — it does not throw on a task-level failure). Throws on workflow
* abort or budget exhaustion before spawning.
*
* F5 terminal semantics vs the CC anchor (206-pretty.js:17488900-17489600), verified 2026-07-12:
* - stall-retry EXHAUSTION **throws** `agent stalled on all N attempts…` — CC throws here too (null is
* NOT the stall terminal in CC). The thrown {@link WorkflowAgentStalledError} carries the last
* attempt's TaskResult (when one was produced) so partial output and the session handle survive.
* - CC's `agent() → null` legs are user-skip and a terminal API error (post inner query-layer retries).
* This seam has no user-skip, and a terminal API death surfaces as a **`status:"failed"` TaskResult**
* (the script checks `status`) — the seam adaptation of CC's null; recorded divergence, not a gap.
* - inside `parallel`/`pipeline` a THROWN leg folds to `null` (F11/B8), so the CC-taught
* `.filter(Boolean)` idiom still holds at the fan-out level. */
agent(spec: TaskSpec, opts?: WorkflowAgentOptions): Promise<TaskResult>;
/** design/97 CORE-5 (#6): spawn a STEERABLE agent — same recording/caps/budget as {@link agent}, but returns a
* {@link WorkflowAgentHandle} the launcher can `steer()` mid-flight and then `await result()`. Resolves once
* a concurrency slot is acquired and the underlying stream has started (so `steer` won't hit a not-running
* task). Use for long-running workers a leader wants to redirect; use `agent` for fire-and-await. */
agentStream(spec: TaskSpec, opts?: WorkflowAgentOptions): Promise<WorkflowAgentHandle>;
/** Run thunks concurrently (capped by the workflow concurrency limit via `agent`). A thunk that throws
* resolves to `null` — filter before use. BARRIER: awaits all. Opt-in: pass `{ errors }` (a caller-owned
* array) to receive one {@link WorkflowFanOutSlotError} per folded slot — the results shape is unchanged. */
parallel<T>(thunks: Array<() => Promise<T>>, opts?: WorkflowFanOutOptions): Promise<Array<T | null>>;
/** Run each item through all stages independently, NO barrier between stages. Each stage receives
* `(prevResult, originalItem, index)`. A stage that throws drops that item to `null`. Opt-in: a trailing
* non-function `{ errors }` argument (after the stages) collects one {@link WorkflowFanOutSlotError} per
* folded item — the results shape is unchanged. The options bag must be the LAST argument (fidelity
* R4-5: the tuple type rejects other positions at compile time, and the runtime throws on them too —
* a mid-position bag used to be silently invoked as a stage, folding every item to null). */
pipeline<I>(items: I[], ...stagesAndOpts: Array<(prev: any, item: I, index: number) => Promise<any>> | [...Array<(prev: any, item: I, index: number) => Promise<any>>, WorkflowFanOutOptions]): Promise<any[]>;
/** Group work under a named phase (observability). TWO forms (CC-心智兼容):
* - scoped `phase(title, body)` — `body`'s agents default to this phase; closes when body settles.
* - bare `phase(title)` (CC marker style) — subsequent agents group under `title` until the NEXT
* `phase()` call, the end of the enclosing scoped `phase()` body (T2A-10: a marker never leaks its
* lexical scope), or the run's end — whichever comes first. Original contract line follows:
* `phase()` call (which closes it) or the run's end. Returns `undefined`. */
phase<T>(title: string, body?: () => Promise<T>): Promise<T | undefined>;
/**
* design/97 CORE-3: run `body` as a nested SUB-WORKFLOW — IN-PROCESS over the SAME ctx (shares budget / caps
* / abort / the cumulative agent counter / the run object), under a fresh sub-`groupId` so the observe tree
* can render it. It does NOT mint a new runId (it is NOT `startWorkflow`, so it never trips the run_workflow
* nesting guard). Agents/phases spawned inside carry the sub-group's id. Group nesting is depth-capped.
*/
workflow<T>(body: (ctx: WorkflowRunContext) => Promise<T>): Promise<T>;
/** Emit a narrator log line (observability only — not stored on the run). */
log(message: string): void;
}
/**
* #642 — one decision a resume carries for a PARKED ordinal of the prior run ({@link RunWorkflowOptions.parkedResume}):
* the checkpoint token the wa* row is parked on (the row's `parkedCheckpointToken`, the redemption key), the
* caller's decision for it (the same {@link ResumeOutcome} a top-level `runner.resume` takes), and the optional
* cross-process re-supply of the checkpoint's opaque parent constraints. The token is matched by string equality
* against the parked ordinal's token — an ordinal whose token no entry names re-parks.
*
* `outcome` is OPTIONAL. `{ token, outcome }` is a DECISION: a pending checkpoint is driven with it; one the store no
* longer calls pending is settled (the acknowledgement is implied). `{ token }` alone is an ACKNOWLEDGEMENT: it settles an
* ordinal whose checkpoint is resolved / expired / absent as a failed row (`workflow.park_not_pending`) so the script
* continues; if the re-read finds the checkpoint pending again it RE-PARKS — an acknowledgement never authorizes a drive.
*/
export interface WorkflowParkedResume {
token: CheckpointToken;
outcome?: ResumeOutcome;
inheritedGate?: InheritedGate;
}
export interface RunWorkflowOptions {
/** Trusted caller-supplied run id. Used by the unified task registry so tool-launched workflows expose
* `task_id === runId` (`w*`) without making the model choose an id. Omit for the legacy UUID mint path. */
runId?: string;
/** design/97 S1b/S1c — persist the {@link WorkflowRun} to a {@link WorkflowRunStore} for `/workflows`
* history + cross-replica visibility. **Opt-in (default none)**: with no store the workflow still runs
* + emits + is subscribable in-process (S1a behavior, unchanged). Persistence is **best-effort**: a store
* throw is swallowed and NEVER breaks the workflow (the run/script is the source of truth; the store is an
* observation layer). The owner is the sole writer (rev CAS). */
store?: WorkflowRunStore;
/** Tenant/grouping key for the run (default `"default"`). */
scope?: string;
/** design/99 MF-W: the workflow's display name + description (the run_workflow tool passes the script's
* `meta.name`/`meta.description`) — recorded on the {@link WorkflowRun} for the /workflows monitor header. */
name?: string;
description?: string;
/** design/114 #3 — the host task id that started this workflow → recorded on the {@link WorkflowRun} so a
* crash-recovered consumer can re-associate the run with its initiator (the run_workflow tool passes it). */
sourceTaskId?: string;
/** The originating session id — recorded on the {@link WorkflowRun}(same rationale as
* sourceTaskId: live notify carries it, recovery otherwise can't). */
originatingSessionId?: string;
/** the ROOT host session of the delegation tree (the run-workflow tool passes
* `ctx.rootSessionId ?? originatingSessionId`); rides the wa* observer frames so a recovery face
* groups workflow-agent rows under the root host session too. */
rootSessionId?: string;
/** design/380 O1② (C12) — the host run's EXPLICIT placement fixed point
* (`RunInternals.placementRoot`, threaded by the run-workflow tool when the host carried one):
* every workflow-spawned agent inherits it verbatim so descendants of a cascade/verify leg stay
* placed at the ladder/gate root. Absent ⇒ nothing extra travels (the rootSessionId chain is the
* placement root through prepare's mint middle segment). */
placementRoot?: string;
/** design/149 — the PROCESS-level child observer (same seam as
* {@link import("../core/types.js").RunnerDeps.onBackgroundChildEvent}). When set, every workflow
* agent emits spawn/tick/terminal {@link import("../core/types.js").BackgroundChildEvent} frames with a
* synthetic `wa*` row key (runId+callKey hash — stable across resume replays), owner/parentTaskId = the
* workflow's own task id (the fleet row nests under the w* row), sessionScoped false. This lane is
* process-anchored — a host run that settles BEFORE the workflow finishes does not mute it (the
* run-leg uuid tick lane's settledLeg freeze). Swallow-guarded.
*/
onBackgroundChildEvent?: (event: import("../core/types.js").BackgroundChildEvent) => void;
/** design/140 §6 1a — the merged effective args snapshot recorded on the {@link WorkflowRun} (the
* run_workflow tool passes the value it resolved at parse time; a JSON value, observation-only). */
effectiveArgs?: unknown;
/** Cancels the whole workflow: propagated to each agent's runTask; new agents short-circuit once aborted. */
signal?: AbortSignal;
/**
* F4 agentType registry (2026-07-11): deployment agent definitions resolvable by `agent(…, {agentType})`.
* Same SHADOW rule as the Agent tool: a deployment definition whose name collides with a built-in
* (Explore/Plan) wins. The run_workflow tool threads the deployment's `agents` here.
*/
agents?: AgentDefinition[];
/** `false` removes the built-in Explore/Plan from the agentType registry (mirror of the Agent tool's
* `builtinAgents:false`, CC `CLAUDE_AGENT_SDK_DISABLE_BUILTIN_AGENTS` analog). Default true. */
builtinAgents?: boolean;
/** Token ceiling — `ctx.agent` throws {@link WorkflowBudgetExceededError} once spend reaches it. */
budget?: number;
/** Max concurrent agents (default `min(16, cpus-2)`, floor 1). Validated fail-loud, like the three
* §D.6 hard caps below (CLS-A-5) — a non-finite ceiling would silently remove itself. */
concurrency?: number;
/** design/98 §D.6 hard cap — max CUMULATIVE agents the workflow may spawn (LLM-authored bound). `ctx.agent`
* throws {@link WorkflowMaxAgentsError} once reached. Undefined ⇒ only `budget`/`concurrency` bound it. */
maxAgents?: number;
/** design/98 §D.6 hard cap — whole-workflow wall-clock deadline (ms). An internal AbortController cancels
* the run at the deadline (agents short-circuit as aborted; the run finalizes `failed`). */
totalTimeoutMs?: number;
/** design/98 §D.6 hard cap — cap on a single `log()` message length (chars); longer messages are truncated
* with an honest marker (a runaway script must not flood the log). */
maxLogChars?: number;
/** design/98 §D.6 hard cap — cap on the script's RETURNED result size (chars, JSON-serialized). Exceeding
* finalizes the run `failed` with {@link WorkflowResultTooLargeError}. */
maxResultChars?: number;
/** Progress sink (run/phase/agent/log/run_end events). */
onEvent?: (e: WorkflowEvent) => void;
/** RB-473 batch 2b — bounded disclosure when a host sink (`onEvent` / the bg-child event sink) throws
* inside this run's isolation scope: first failure per site, later ones counted only. */
onNotifyError?: (failure: import("../core/safe-notify.js").SafeNotifyFailure) => void;
/** Injected clock (design/87 §4.3 — testable without the wall clock). Default `Date.now`. */
now?: () => number;
/**
* design/97 CORE-7 — the LOAD-BEARING resume journal. With a `journalStore`, every `ctx.agent` result (NOT
* `ctx.agentStream` — see below) is recorded keyed by its deterministic callKey. Supplying `resumeFromRunId`
* (a PRIOR run id, requires `journalStore`) replays the longest UNCHANGED PREFIX of those results instantly and
* runs only the first changed/new call + everything after it live (same script + args → 100% cache hit). A
* `journalStore` WITHOUT `resumeFromRunId` just records (so a future run can resume from THIS run). Replayed
* agents accumulate into `run.stats` (total work) but NOT into `ctx.budget.spent()` (live spend only — a resume
* is not re-charged for cached work). NOT-REPLAYED (they force divergence so the suffix runs live): `ctx.agentStream`
* (a finished result can't be steered — RECORDED for the journal read-face, never replayed) and `ctx.pipeline` (latency-dependent ordinals aren't deterministic —
* a CORE-7.1 follow-on adds stage-scoped deterministic keys). Sequential + `parallel(direct thunks)` replay.
*/
journalStore?: WorkflowJournalStore;
/** design/97 CORE-7 — resume by replaying a prior run's journal (requires `journalStore`). See `journalStore`. */
resumeFromRunId?: string;
/** The HOST task's effective working root — threaded into every spawned agent's trusted internals
* (`RunInternals.parentCwd`) so a TOC env factory roots children at the parent's cwd (CC parity;
* `isolation: "worktree"` on an individual agent wins over it). */
parentCwd?: string;
/**
* The SPAWNING tool call's id (`run_workflow`'s own `ToolExecuteContext.toolCallId`) — threaded into every
* spawned agent's trusted internals (`RunInternals.parentToolCallId`) so each child runs AS a sub-agent:
* its per-turn `task_progress` ticks actually EMIT (the runner gates them on `parentToolCallId`) and carry
* this id for attribution, the same contract as a `createSubagentTool` delegation. Set by the `run_workflow`
* tool wiring; a directly-started workflow (no launching tool call) leaves it unset — children then emit no
* progress ticks (status quo), and no id is ever fabricated.
*/
parentToolCallId?: string;
/** The HOST task's taskId — threaded as every spawned agent's `RunInternals.parentTaskId` so its
* `task_progress` ticks nest under the launching task in a live agent-tree view (same channel a
* subagent spawn threads from `ToolExecuteContext.taskId`). Optional, `parentToolCallId`'s companion. */
parentTaskId?: string;
/** design/173 §8.3 (r2-F1) — the HOST run's RESOLVED interaction posture, threaded as every
* spawned agent's `RunInternals.parentInteractionPosture` (trusted internals, never the child
* spec) so a root's per-run posture override governs its workflow children too. Set by the
* `run_workflow` tool wiring from `ToolExecuteContext.interactionPosture`. */
interactionPosture?: "interactive" | "headless";
/**
* design/383 §2.5 (rescan post-6.0.0-RC) — the HOST session's capture opt-out state, read AT
* EACH SPAWN (call-time getter, deliberately not a launch snapshot: `optedOut` is live — a
* mid-run `optOutMemoryCapture` flip must floor agents spawned after it). Threaded onto both
* spawn legs' internals as the same floor/indeterminate/queryDir/ancestor seats the subagent
* lane forwards, so a workflow child of an opted-out host commits nothing — the design's ruled
* disposition for the workflow HOST lane. Set by the `run_workflow` tool wiring (enriched ctx
* getters, else the auto-mount dep). Absent ⇒ no capture seat rides (a direct `runWorkflow`
* caller with no memory-mounted host — `originatingSessionId` still rides as `parentSessionId`
* for the record-query leg).
*/
parentMemoryCaptureState?: () => {
optedOut: boolean;
indeterminate: boolean;
controlDir?: string;
ancestors?: ReadonlyArray<{
sessionId: string;
controlDir?: string;
}>;
} | Promise<{
optedOut: boolean;
indeterminate: boolean;
controlDir?: string;
ancestors?: ReadonlyArray<{
sessionId: string;
controlDir?: string;
}>;
}>;
/** Call-time getter for the HOST run's RESOLVED Model object. A spawned agent whose
* fold chain (script spec → agentType → governance baseline) produced NO model inherits the
* parent's full object — baseUrl/key routing included — instead of falling to a string/role
* re-resolution that loses per-model routing (the 4/4-agents-404 incident: session model id
* re-resolved against the base gateway). Mirrors the subagent lane's `ctx.model` semantics. */
defaultModel?: () => import("../agents/child-model-seat.js").InheritedModelPick | undefined;
/** Call-time getter for the thinking level a spawned agent runs at when neither the script, the
* agentType definition nor the governance baseline set `thinking` — the child thinking seat's lower rungs
* (the deployment's STATED `roles.subagent.thinking`, else the host's current level), as the run-workflow
* mount folds them. Snapshotted ONCE at call entry and folded into the call identity like `defaultModel`;
* applied at the launch site AFTER the agentType fold ({@link withWorkflowChildSeats}). */
defaultThinking?: () => ThinkingLevel | undefined;
/** The HOST's per-model auth hook, inherited into every spawned agent's spec (the
* credential half of model inheritance; scripts can never set it — governance strips it — so
* this is always the host's). NOT call identity (auth, not behavior; a function, not data). */
defaultGetApiKeyAndHeaders?: TaskSpec["getApiKeyAndHeaders"];
/**
* #642 — the HOST run's `durableApproval` opt-in (value copy), inherited into every spawned agent whose
* fold left the seat unpinned (a deployment baseline that pins `durableApproval` wins; scripts can never set
* it — governance strips it). With it a workflow child's plain policy `ask` — an ask the host's approver
* answers `unavailable` for, or a durable mandate — PARKS durably instead of denying fail-closed, exactly as
* a background delegation child's does under the §7.3 park family: the wa* row records `status:"parked"` +
* the token, the run SUSPENDS (the script cannot continue past a parked call), and a resume drives the
* parked child on with the caller's decision ({@link parkedResume}). Tighten-only: a park capability,
* never a widening — the decision stays with a person. REQUIRES {@link store} (the wa* row's durable home;
* refused loudly at entry without it). Threaded by the Workflow tool from the host's trusted seat; a
* directly-started workflow passes its own. NOT call identity. REQUIRES {@link checkpointStore} as well: a
* durable approval whose checkpoint this workflow cannot read back is a park no resume can prove.
*/
defaultDurableApproval?: TaskSpec["durableApproval"];
/**
* The HOST run's parent SPEC seat ({@link import("../core/parent-spec-seat.js").ParentSpecSeat}: principal / oneShot /
* clientContext / the frozen question face / the fs-fence widening / envFacts / the declared persistence statement),
* applied onto every spawned agent at the launch site AFTER the agentType fold by {@link applyParentSpecSeat} — the
* Task delegation lane's one law: a value the child spec carries (script, definition, baseline) WINS, the seat FILLS
* an unset one, the two removals (`oneShot: true`, `memoryPersistenceCapable: false`) land unconditionally. Threaded
* by the Workflow tool from the host's seat; a directly-started workflow passes its own. NOT call identity (host
* wiring, like the checkpoint seat). Absent ⇒ nothing is applied. The seat the Workflow tool threads carries NO
* `onQuestion`: the tool strips the host's per-request face (every agent it spawns outlives the launching request)
* and says so through {@link questionFaceStripped}; a directly-started workflow passes what its own lifetime allows.
*/
parentSpecSeat?: ParentSpecSeat;
/** The engine stripped the spawn turn's per-request question face from the seat above (pair-produced with the strip,
* `stripSpawnTurnQuestionFace`); rides every spawned agent's internals as `RunInternals.questionFaceStripped` — the
* wiring manifest's `question.wired: "stripped_bg_lane"` and the posture door's exemption read it. */
questionFaceStripped?: true;
/**
* The checkpoint store this run's children park in — the RESOLVED seat object itself (the same value the Workflow
* tool injects onto every child spec; a directly-started workflow passes the store its children park in). Its
* presence ARMS the park lane: a resume then proves every parked ordinal against `get(token)` before anything is
* dispatched (`WorkflowRun.parks` supplies the keys), and a child whose effective `checkpointStore` is a DIFFERENT
* object is refused at spawn (`config.invalid_checkpoint_store` — identity, not duck-typing: a caller-built `{get}`
* wrapper is not the seat). REQUIRES {@link store} (the park's durable row). Absent ⇒ the lane is not armed: a
* resume of a run that recorded parks is refused (`workflow.park_truth_unreadable` / `capability_not_armed`), a run
* without parks resumes as before. NOT call identity (deployment wiring, like the store seats).
*/
checkpointStore?: CheckpointStore;
/**
* #642 — the decisions a resume ({@link resumeFromRunId}) carries for PARKED ordinals of the prior run: at
* an ordinal whose journal entry is `parked` (a matching call key), the run drives the parked child's own
* resume — `runner.resumeStream(token, outcome, …)` on the pinned session, the child's spec rebuilt from
* the script call exactly as at spawn — and settles its result at that ordinal, so the script continues past
* the call it parked on. A parked ordinal with NO decision here re-parks (the row and the journal say the
* same thing again, the run suspends again); it is never run live. `inheritedGate` is the cross-process
* re-supply of the checkpoint's opaque parent constraints (the same seat the background lane's drive
* takes). TRUSTED: a host-supplied decision, never a script/model argument; requires `resumeFromRunId`.
*/
parkedResume?: ReadonlyArray<WorkflowParkedResume>;
/** design/148 S1 — the HOST run's adopted center artifact (trusted, threaded by the Workflow tool
* from ctx): every spawned agent composes the same closure. */
parentCenterArtifactDigest?: string;
parentCenterSourceRevision?: string;
/** The launching run's display sink (`RunInternals.onForwardEvent` behind the runner's ctx wrapper:
* `task_progress` always, PLUS the children's content events — `text_delta`/`text_end`/
* `reasoning_delta`/`tool_start`/`tool_end`, UNTRUSTED-RAW: the consumer must redact — when the HOST spec set
* `forwardSubagentEvents: true`) — threaded into every spawned agent's trusted internals so the
* children's events bubble out of their isolated streams to the deployment's one sink (fleet
* footer/monitor rows). Display-only; absent ⇒ ticks stay in each child's own stream. */
onForwardEvent?: (event: TaskEvent) => void;
/** Parent effective-policy inheritance (tighten-only): the HOST task's evaluated gate chain
* (`ToolExecuteContext.inheritedGateForChildren()` — its session rules + toolPolicy/frozen onAsk +
* shellGate plus every ancestor's), threaded into EVERY spawned agent's trusted internals
* (`RunInternals.inheritedGate`) on both spawn legs (`agent()` and `agentStream()`). Set by the
* `run_workflow` tool wiring; a directly-started workflow may pass its own. Absent ⇒ children run
* under only their own gates (pre-seam behavior). TRUSTED: never a script/model argument. */
inheritedGate?: import("../core/runner/contracts.js").InheritedGate;
/** RB-201 FO-3 (workflow lane) — the session's auto-mode classifier, consulted BEFORE each script-spawned
* child starts (objective + toolset + output schema, finalized). The delegation-tool lane reviews its
* children at one shared point; a workflow's children reach `runner.runTask` through this module instead,
* so they need their own review point rather than inheriting that one. Threaded by the `run_workflow` tool
* wiring from the host's `ToolExecuteContext.autoModeReview`; a directly-started workflow may pass its own.
* ABSENT ⇒ every leg here is a complete no-op (auto-mode not armed = byte-identical to before).
* TRUSTED: never a script/model argument. */
autoModeReview?: {
decider: import("../core/auto-mode.js").AutoModeDecider;
};
/** F7/B7 (CC pretty.js:446478): PRE-REGISTER the script's `meta.phases` as pending progress groups at run
* start — the plan is visible before execution; `phase(title)` adopts the matching pending entry. */
phases?: ReadonlyArray<{
title: string;
detail?: string;
model?: string;
}>;
/** F5/B4: override the per-agent stall watchdog (ms). Default {@link WORKFLOW_AGENT_STALL_MS}. A deployment
* may tune it; tests set it small to exercise the retry path without waiting 180s. */
stallMs?: number;
/** F5/B4: override the max stalled-attempt retries. Default {@link WORKFLOW_AGENT_MAX_RETRIES}. */
agentMaxRetries?: number;
/** F5/B4: override the throttle-degradation backoff (ms). Default {@link WORKFLOW_AGENT_THROTTLE_BACKOFF_MS}. */
throttleBackoffMs?: number;
/** F5/B4 (design/87 §4.3): injectable timers for the resilience layer (stall watchdog + throttle backoff
* sleep) — tests advance a virtual clock instead of really waiting {@link WORKFLOW_AGENT_STALL_MS}.
* Default: real unref'd `setTimeout`/`clearTimeout`. Pair with `now` for a fully virtual clock. */
timers?: WorkflowTimers;
}
export interface RunWorkflowResult<T> {
result: T;
runId: string;
run: WorkflowRun;
}
/**
* design/98 §D.4 (S8c) — the handle {@link startWorkflow} returns: the `runId` is available SYNCHRONOUSLY
* (the run row + live subscription exist before the body runs, so a caller can subscribe + return the id to an
* LLM immediately), `done` resolves/rejects exactly like {@link runWorkflow}, and `cancel` aborts the whole
* run (every agent's signal fires; new agents short-circuit).
*/
export interface WorkflowHandle<T> {
readonly runId: string;
readonly done: Promise<RunWorkflowResult<T>>;
cancel(reason?: string): void;
}
/** Cap on items a single `parallel`/`pipeline` call accepts (matches the CC Workflow tool). */
export declare const MAX_WORKFLOW_ITEMS = 4096;
/**
* CC `MTy` (206-pretty.js:17501680; 198 `F0m`): no tool activity for this long marks the attempt STALLED
* (progress-based, unlike the governance perAgentTimeoutSec hard cap, which still bounds each attempt).
* `<= 0` DISABLES the watchdog (CC arms only `if (ae > 0)`, :17477830).
*
* DERIVED, not a number of its own (#682): an OUTER, COARSER clock must never fire before the engine's
* own. This watchdog does not re-send a model call — it aborts the attempt and restarts the whole agent
* on a FRESH session, so tool effects from before the stall can be repeated. The engine, one layer down,
* already answers a silent provider call the cheap way: it re-sends that ONE call. So the floor here is
* what a single provider call may legitimately occupy while producing NOTHING — the whole first-byte lane at the default knob
* ({@link firstByteLaneKnobMs}) plus the first-token wait; a floor (upload allowance and backoff between attempts not counted). It was a hand-written
* 180_000 and CC-same until the first-token bound rose to 600s past it (measured: the same silent call
* then produced two agent runs on two sessions instead of one run with a re-sent call), which is the
* systematic inversion a derived value cannot re-open: raise or lower the engine's bound and this follows.
*
* WHAT THIS IS NOT (measured, adversarial round — do not read the sum as a proof of ordering). The two
* clocks do not start together and this one does not reset per inner retry, so it can still fire first:
* this timer starts before the Runner prepares the attempt (an execution-env factory, a hook, a store
* read all run inside this window and outside the engine's), and the first-token timer only starts once
* headers arrive — so the margin by which the inner deadline can land AFTER this one is preparation plus
* the header wait actually spent, less the {@link STALL_FIRST_BYTE_MS} this window already budgets for it
* (instant headers and more than 300s of preparation put the first inner deadline past this timer). An engine retry does not re-arm this timer either, so two silent provider attempts
* exceed it by construction. The derivation removes the case that was guaranteed to invert; it does not
* make inversion impossible. A deployment that wants the old aggression passes `stallMs` explicitly, and
* a deployment that wants a real guarantee must set it from its own preparation budget.
*/
export declare const WORKFLOW_AGENT_STALL_MS: number;
/**
* The "took suspiciously long" half of the throttle-shaped-degradation predicate (CC `Nt`,
* 206-pretty.js:17487400, which spells it `MTy * 0.5` = 90s). It used to be computed from the stall
* window here too, and that was safe only while the window was CC's own 180_000 — the moment the window
* became derived from the engine's bounds (#682/#693) the same expression silently moved the throttle
* threshold 90s → 315s, and a 2-minute empty answer that used to earn a 45s-sleep-and-retry was simply
* returned. Two unrelated meanings were riding one number. This is the second meaning, at CC's own
* value, so the progress window can move without touching it.
*
* Still a MINIMUM with the window, not a replacement for it: a deployment that shortens `stallMs` below
* this is asking for a tighter agent, and a throttle threshold above its own stall window could never
* fire. So the effective threshold is `min(stallMs / 2, this)` — one rule, both directions.
*/
export declare const WORKFLOW_THROTTLE_SLOW_MS = 90000;
/** CC `j_d` (206-pretty.js:17501680; 198 `Mxl`): max stalled-attempt retries per `ctx.agent` call
* (initial + 5 = 6 attempts). */
export declare const WORKFLOW_AGENT_MAX_RETRIES = 5;
/** CC throttle backoff (206-pretty.js:17488618): ONE fixed 45s sleep-then-retry for a throttle-shaped
* degraded response (a flat sleep, NOT exponential backoff — and only on the FIRST attempt's result). */
export declare const WORKFLOW_AGENT_THROTTLE_BACKOFF_MS = 45000;
/**
* RB-355 (2026-07-29) — the cap on how long the TERMINAL boundary waits on the resume-claim
* store (the admission verdict, then the release). Both awaits sit in the run's `finally`, i.e. AFTER the
* outcome is already decided: an unbounded wait there means a store implementation that hangs (a wedged
* connection pool, a lost network write, a promise that simply never settles) leaves the run's `done` promise
* pending FOREVER, silently discarding the guarantee `totalTimeoutMs` makes to the caller — the deadline
* aborts the body, and then the cleanup hangs on the same broken store. Timing out here loses nothing that
* was not already lost: a release that cannot complete leaves a claim row behind, which is precisely what the
* store's TTL (invalidation layer 2) exists to reap. Scheduled on the injectable {@link WorkflowTimers} seam,
* so the timer is unref'd by default and tests can drive it without waiting.
*/
export declare const WORKFLOW_RESUME_CLAIM_FINALIZE_TIMEOUT_MS = 10000;
/** F5/B4 + design/87 §4.3 — the INJECTABLE timer seam the per-agent resilience layer schedules on (the
* stall watchdog + the throttle backoff sleep). Tests drive VIRTUAL time instead of really waiting
* 180s/45s. Default: the real `setTimeout`/`clearTimeout`, unref'd (a pending watchdog must never hold
* the process open). */
export interface WorkflowTimers {
setTimeout(fn: () => void, ms: number): unknown;
clearTimeout(handle: unknown): void;
}
export declare const workflowResumeClaimFallback: {
/** Grant (or idempotently re-confirm) the in-process claim on `(sourceRunId, scope)` for `store`. Pure
* synchronous Map read-then-write — no `await` between the check and the publish, so there is no window
* (matching the atomicity RB-354 requires of a store-level implementation) in which a second same-tick
* caller could observe the key as free. */
acquire(store: WorkflowJournalStore, input: ResumeClaimArgs): {
granted: boolean;
holder?: string;
};
/** Release a claim ONLY if `newRunId` still holds it — mirrors the store contract's `releaseResumeClaim`:
* a missing/foreign claim is a no-op, never an error (a claim already taken over by a later resume must
* never be unlinked by an earlier one's cleanup). */
release(store: WorkflowJournalStore, input: ResumeClaimArgs): void;
};
/**
* Start a workflow script and return its {@link WorkflowHandle} SYNCHRONOUSLY (design/98 §D.4): the `runId`,
* run row, and live subscription exist before the body runs — so a caller (e.g. the `run_workflow` tool) can
* subscribe + hand the id to an LLM immediately, then await `done`. Establishes a {@link WorkflowRunContext},
* runs `fn(ctx)` asynchronously, and resolves `done` with the script's value + the assembled
* {@link WorkflowRun} (status `completed`). If `fn` throws, the run finalizes `failed` (events emitted) and
* `done` rejects. `cancel(reason)` aborts the whole run. A nesting/validation violation throws SYNCHRONOUSLY
* from this call (before any handle exists). Thin composition over `runner.runTask` — no Runner changes.
*/
export declare function startWorkflow<T>(runner: RunnerSelfSeat, fn: (ctx: WorkflowRunContext) => Promise<T>, opts?: RunWorkflowOptions, internals?: WorkflowInternals): WorkflowHandle<T>;
/**
* Run a workflow to completion (design/97 S1a) — the thin await over {@link startWorkflow}: returns the
* script's value + the assembled {@link WorkflowRun}. A synchronous nesting/validation throw from
* `startWorkflow` becomes a rejected promise here (this function is `async`), preserving the original
* `runWorkflow` contract (`await runWorkflow(...)` rejects rather than throwing synchronously).
*/
export declare function runWorkflow<T>(runner: RunnerSelfSeat, fn: (ctx: WorkflowRunContext) => Promise<T>, opts?: RunWorkflowOptions, internals?: WorkflowInternals): Promise<RunWorkflowResult<T>>;