@sema-agent/core 7.5.2 → 7.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/dist/agents/agent-transcript-tool.d.ts +2 -2
  3. package/dist/agents/cascade.d.ts +4 -5
  4. package/dist/agents/cascade.js +12 -10
  5. package/dist/agents/repair-loop.d.ts +7 -5
  6. package/dist/agents/repair-loop.js +13 -15
  7. package/dist/agents/retain-ledger.d.ts +2 -3
  8. package/dist/agents/send-message-tool.d.ts +2 -2
  9. package/dist/agents/session-util.d.ts +2 -2
  10. package/dist/agents/subagent.d.ts +27 -46
  11. package/dist/agents/subagent.js +119 -105
  12. package/dist/agents/suspend-guard.d.ts +31 -19
  13. package/dist/agents/suspend-guard.js +14 -8
  14. package/dist/agents/teacher.d.ts +2 -2
  15. package/dist/agents/teacher.js +9 -9
  16. package/dist/agents/team.d.ts +6 -5
  17. package/dist/agents/team.js +10 -8
  18. package/dist/agents/verify.d.ts +8 -9
  19. package/dist/agents/verify.js +17 -17
  20. package/dist/core/a2a.js +2 -1
  21. package/dist/core/agent-definition.d.ts +172 -0
  22. package/dist/core/agent-definition.js +1 -0
  23. package/dist/core/ask-origin.d.ts +60 -7
  24. package/dist/core/ask-origin.js +26 -1
  25. package/dist/core/checkpoint-store.d.ts +78 -76
  26. package/dist/core/checkpoint-store.js +17 -1
  27. package/dist/core/delegation-frames.d.ts +298 -0
  28. package/dist/core/delegation-frames.js +21 -0
  29. package/dist/core/engine-notice.d.ts +555 -0
  30. package/dist/core/engine-notice.js +55 -0
  31. package/dist/core/gate-fold.d.ts +12 -0
  32. package/dist/core/gate-fold.js +158 -0
  33. package/dist/core/gate-lanes.d.ts +93 -0
  34. package/dist/core/gate-lanes.js +626 -0
  35. package/dist/core/gate-outcome.d.ts +189 -0
  36. package/dist/core/gate-outcome.js +70 -0
  37. package/dist/core/hands-band.d.ts +134 -0
  38. package/dist/core/hands-band.js +1 -0
  39. package/dist/core/hooks.d.ts +22 -177
  40. package/dist/core/hooks.js +53 -851
  41. package/dist/core/mcp-failure.d.ts +142 -0
  42. package/dist/core/mcp-failure.js +145 -0
  43. package/dist/core/mcp-server-spec.d.ts +217 -0
  44. package/dist/core/mcp-server-spec.js +1 -0
  45. package/dist/core/mcp.d.ts +21 -77
  46. package/dist/core/mcp.js +76 -150
  47. package/dist/core/model-seat.d.ts +99 -0
  48. package/dist/core/model-seat.js +1 -0
  49. package/dist/core/pause-registry.d.ts +131 -0
  50. package/dist/core/pause-registry.js +27 -0
  51. package/dist/core/reminder-mint.d.ts +10 -0
  52. package/dist/core/reminder-mint.js +3 -0
  53. package/dist/core/runner/assemble-result.d.ts +32 -41
  54. package/dist/core/runner/assemble-result.js +55 -74
  55. package/dist/core/runner/contracts.d.ts +427 -69
  56. package/dist/core/runner/denial-limit-arms.d.ts +1 -1
  57. package/dist/core/runner/denial-limit-arms.js +3 -3
  58. package/dist/core/runner/gate-exit.d.ts +242 -0
  59. package/dist/core/runner/gate-exit.js +124 -0
  60. package/dist/core/runner/park-commit.d.ts +17 -23
  61. package/dist/core/runner/park-commit.js +14 -15
  62. package/dist/core/runner/prepare-ask-lane.d.ts +0 -3
  63. package/dist/core/runner/prepare-ask-lane.js +3 -5
  64. package/dist/core/runner/prepare-boundary-parks.d.ts +3 -6
  65. package/dist/core/runner/prepare-boundary-parks.js +3 -3
  66. package/dist/core/runner/prepare-caps-and-workflow.d.ts +2 -7
  67. package/dist/core/runner/prepare-caps-and-workflow.js +1 -1
  68. package/dist/core/runner/prepare-delegation-surface.d.ts +2 -7
  69. package/dist/core/runner/prepare-gate-stations.d.ts +4 -7
  70. package/dist/core/runner/prepare-gate-stations.js +29 -54
  71. package/dist/core/runner/prepare-inherited-gate.js +1 -1
  72. package/dist/core/runner/prepare-memory.d.ts +44 -26
  73. package/dist/core/runner/prepare-park-ask.d.ts +2 -4
  74. package/dist/core/runner/prepare-park-ask.js +5 -5
  75. package/dist/core/runner/prepare-task.d.ts +2 -2
  76. package/dist/core/runner/prepare-task.js +8 -9
  77. package/dist/core/runner/prepare-wiring-manifest.d.ts +7 -15
  78. package/dist/core/runner/prepare-wiring-manifest.js +9 -10
  79. package/dist/core/runner/runtask.d.ts +20 -102
  80. package/dist/core/runner/runtask.js +119 -121
  81. package/dist/core/runner/terminal-projection.d.ts +22 -0
  82. package/dist/core/runner/terminal-projection.js +28 -0
  83. package/dist/core/runner-deps.d.ts +1416 -0
  84. package/dist/core/runner-deps.js +1 -0
  85. package/dist/core/runtime-caps.d.ts +164 -0
  86. package/dist/core/runtime-caps.js +1 -0
  87. package/dist/core/store-contracts/checkpoint-store-contract.d.ts +4 -1
  88. package/dist/core/store-contracts/checkpoint-store-contract.js +8 -2
  89. package/dist/core/task-event.d.ts +910 -0
  90. package/dist/core/task-event.js +1 -0
  91. package/dist/core/task-limits.d.ts +110 -0
  92. package/dist/core/task-limits.js +1 -0
  93. package/dist/core/task-result.d.ts +809 -0
  94. package/dist/core/task-result.js +1 -0
  95. package/dist/core/task-spec.d.ts +1370 -0
  96. package/dist/core/task-spec.js +1 -0
  97. package/dist/core/task-stream.d.ts +382 -0
  98. package/dist/core/task-stream.js +1 -0
  99. package/dist/core/terminal-cause.d.ts +137 -0
  100. package/dist/core/terminal-cause.js +9 -0
  101. package/dist/core/tool-policy.d.ts +43 -139
  102. package/dist/core/tool-policy.js +79 -112
  103. package/dist/core/tool-spec.d.ts +1174 -0
  104. package/dist/core/tool-spec.js +1 -0
  105. package/dist/core/types.d.ts +27 -7789
  106. package/dist/core/types.js +2 -76
  107. package/dist/core/warm-resume.d.ts +2 -2
  108. package/dist/core/wiring-manifest.d.ts +6 -3
  109. package/dist/core/workflow-journal-store.js +3 -4
  110. package/dist/engine/harness/agent-harness.d.ts +1 -1
  111. package/dist/index.d.ts +12 -8
  112. package/dist/index.js +9 -6
  113. package/dist/orchestration/builtin-workflows.d.ts +2 -2
  114. package/dist/orchestration/builtin-workflows.js +1 -1
  115. package/dist/orchestration/goal.d.ts +2 -2
  116. package/dist/orchestration/goal.js +8 -7
  117. package/dist/orchestration/run-spec.d.ts +2 -2
  118. package/dist/orchestration/run-spec.js +5 -3
  119. package/dist/orchestration/run-workflow-tool.d.ts +4 -4
  120. package/dist/orchestration/run-workflow-tool.js +4 -4
  121. package/dist/orchestration/workflow-governance.d.ts +4 -4
  122. package/dist/orchestration/workflow-governance.js +4 -2
  123. package/dist/orchestration/workflow-primitives.d.ts +1 -1
  124. package/dist/orchestration/workflow-primitives.js +1 -1
  125. package/dist/orchestration/workflow.d.ts +15 -4
  126. package/dist/orchestration/workflow.js +64 -39
  127. package/dist/prompts/supervisor.d.ts +1 -1
  128. package/dist/prompts/supervisor.js +3 -3
  129. package/dist/scenarios/scenario-registry.d.ts +3 -3
  130. package/dist/scenarios/scenario-registry.js +1 -1
  131. package/dist/scenarios/teacher-quickstart.d.ts +2 -2
  132. package/dist/server/http.d.ts +2 -2
  133. package/dist/stores/file/fs-atomic.d.ts +88 -12
  134. package/dist/stores/file/fs-atomic.js +184 -55
  135. package/dist/stores/file/index.d.ts +1 -0
  136. package/dist/stores/file/index.js +1 -0
  137. package/package.json +3 -1
  138. package/test/export-surface.snapshot.json +82 -22
@@ -322,10 +322,10 @@ export async function createRunWorkflowTool(d) {
322
322
  "• Research/audit workflows: document and file claims are HYPOTHESES, not evidence — add a VERIFY " +
323
323
  "stage whose agents EXECUTE (run the command, the test, the reproducer) and cross-check claims " +
324
324
  "against actual output before the synthesis stage reports them as fact.\n" +
325
- "• r.status: agent() does NOT throw when the sub-agent FAILS — it RETURNS the result with r.status " +
326
- "('completed' on success; 'failed'/'blocked'/etc otherwise, with the failure text in r.result). ALWAYS " +
327
- "check r.status before building on a result, and GATE later phases on it — e.g. " +
328
- "`const impl = await agent({ objective: '…' }, { label: 'implement-fix' }); if (impl.status !== 'completed') return { failed: 'implement', why: impl.result }` " +
325
+ "• r.terminal.kind: agent() does NOT throw when the sub-agent FAILS — it RETURNS the result with r.terminal " +
326
+ "({ kind: 'completed' } on success; kind 'failed' (with r.terminal.code / r.terminal.message) / 'blocked' (r.terminal.reason) / 'paused' otherwise, " +
327
+ "with any failure text also in r.result). ALWAYS check r.terminal.kind before building on a result, and GATE later phases on it — e.g. " +
328
+ "`const impl = await agent({ objective: '…' }, { label: 'implement-fix' }); if (impl.terminal.kind !== 'completed') return { failed: 'implement', why: impl.result }` " +
329
329
  "— running later phases on a failed prerequisite burns tokens on garbage.\n" +
330
330
  "• phase (two forms, both fine): bare marker — phase('Scan'); groups every later agent() under 'Scan' " +
331
331
  "until the next phase() call. Scoped — await phase('Fix', async () => { ...agents... }); groups just the " +
@@ -194,10 +194,10 @@ export interface ResourceClampNote {
194
194
  * `restoreGatedTools` / `readFace` / … — for exactly the children an untrusted script can route to
195
195
  * the overlay (`isolation: "worktree"`), own-`undefined` keys included (a spread copies them; the
196
196
  * old `=== null` face drop missed that half). One rule at the one merge point: a new baseline key
197
- * is safe with no list to extend. The single exception is {@link NULL_VALUED_BASELINE_KEYS}
198
- * (`checkpointStore: null` the deployment disarming durable suspend for isolated children is a
199
- * capability removal it must be able to spell). Explicit non-nullish overlay values keep winning
200
- * wholesale — the documented deployment override.
197
+ * is safe with no list to extend, and there is no exception list the one key that used to give
198
+ * `null` a meaning of its own (`checkpointStore`, the durable off switch) now spells it as the word
199
+ * `"disabled"`, an ordinary non-nullish value the rule already keeps. Explicit non-nullish overlay
200
+ * values keep winning wholesale — the documented deployment override.
201
201
  *
202
202
  * ACCEPTED COST, stated (adversarial-review round 2): the rule is direction-blind — a CAPABILITY
203
203
  * key's out-of-type overlay `null` used to WITHHOLD the base value by the same spread accident
@@ -4,6 +4,7 @@ import { sanitizeUntrustedText } from "../core/untrusted-text.js";
4
4
  import { compileReadDeny } from "../tools/fs/read-deny.js";
5
5
  import { WorkflowScriptError } from "./workflow-meta.js";
6
6
  import { uuidv7 } from "../engine/session/uuid.js";
7
+ import { retiredCheckpointStoreNull } from "../core/checkpoint-store.js";
7
8
  export class WorkflowModelNotAllowedError extends Error {
8
9
  modelName;
9
10
  code = "workflow.model_not_allowed";
@@ -79,12 +80,13 @@ export function emitIgnoredAgentOptionsNotice(total, sample, accepted, onNotice)
79
80
  }
80
81
  }
81
82
  const VALID_THINKING = new Set(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
82
- const NULL_VALUED_BASELINE_KEYS = new Set(["checkpointStore"]);
83
83
  export function overlayWorktreeBaseline(base, worktreeBase) {
84
84
  let overlay = worktreeBase;
85
85
  for (const key of Object.keys(worktreeBase)) {
86
86
  const v = worktreeBase[key];
87
- if (v !== undefined && (v !== null || NULL_VALUED_BASELINE_KEYS.has(key)))
87
+ if (key === "checkpointStore" && v === null)
88
+ throw retiredCheckpointStoreNull();
89
+ if (v !== undefined && v !== null)
88
90
  continue;
89
91
  if (overlay === worktreeBase)
90
92
  overlay = { ...worktreeBase };
@@ -33,7 +33,7 @@ export interface WorkflowGovernance {
33
33
  * `agent` primitive is GOVERNED when `governance` is set (LLM-authored), else a trusted pass-through.
34
34
  */
35
35
  export declare function buildWorkflowPrimitives(ctx: WorkflowRunContext, governance?: WorkflowGovernance, onAgentSpawn?: (handle: WorkflowAgentHandle) => void, parentThinking?: () => TaskSpec["thinking"], parentPrincipal?: string,
36
- /** ruled 2026-08-04 — the host run set `TaskSpec.checkpointStore: null` (the per-run durable off
36
+ /** ruled 2026-08-04 — the host run set `TaskSpec.checkpointStore: "disabled"` (the per-run durable off
37
37
  * switch). Every agent this workflow spawns inherits it; see the injection below. */
38
38
  parentCheckpointStoreDisabled?: boolean,
39
39
  /** 5.30 merge-rescan (design/199 parity gap) — the host run's RESOLVED read-face containment
@@ -83,7 +83,7 @@ export function buildWorkflowPrimitives(ctx, governance, onAgentSpawn, parentThi
83
83
  childSpec.principal = parentPrincipal;
84
84
  }
85
85
  if (parentCheckpointStoreDisabled === true) {
86
- childSpec.checkpointStore = null;
86
+ childSpec.checkpointStore = "disabled";
87
87
  }
88
88
  if (parentReadFace) {
89
89
  const pf = parentReadFace();
@@ -1,5 +1,5 @@
1
1
  import type { TSchema } from "typebox";
2
- import type { Runner } from "../core/runner/runtask.js";
2
+ import type { RunnerSelfSeat } from "../core/runner/contracts.js";
3
3
  import type { AgentDefinition, TaskEvent, TaskResult, TaskSpec } from "../core/types.js";
4
4
  import type { WorkflowRunStore } from "../core/workflow-run-store.js";
5
5
  import type { WorkflowJournalStore, ResumeClaimArgs } from "../core/workflow-journal-store.js";
@@ -111,6 +111,17 @@ export declare class WorkflowResultTooLargeError extends Error {
111
111
  readonly code = "workflow.result_too_large";
112
112
  constructor(size: number, max: number);
113
113
  }
114
+ /** A resume journal entry whose result carries no terminal cause — a journal written before
115
+ * `TaskResult.terminal` existed, or a malformed entry — is INCOMPATIBLE, and the resume is refused
116
+ * before any work is dispatched. The alternative (letting the entry diverge into a live re-run) would
117
+ * execute the already-completed prefix again, side effects included; a schema mismatch is never a
118
+ * permission to rerun finished work. Start a fresh run instead of resuming such a journal. */
119
+ export declare class WorkflowJournalIncompatibleError extends Error {
120
+ readonly fromRunId: string;
121
+ readonly ordinal: number;
122
+ readonly code = "workflow.journal_incompatible";
123
+ constructor(fromRunId: string, ordinal: number);
124
+ }
114
125
  /**
115
126
  * A TRUSTED, run-scoped internal channel carrying the workflow **nesting depth** across a process boundary
116
127
  * (design/98 §0.1 BLOCKER3). It is NOT a `TaskSpec` field and NOT a `run_workflow` tool param — a worker or
@@ -441,7 +452,7 @@ export interface RunWorkflowOptions {
441
452
  * (`RunInternals.inheritedGate`) on both spawn legs (`agent()` and `agentStream()`). Set by the
442
453
  * `run_workflow` tool wiring; a directly-started workflow may pass its own. Absent ⇒ children run
443
454
  * under only their own gates (pre-seam behavior). TRUSTED: never a script/model argument. */
444
- inheritedGate?: import("../core/runner/prepare-task.js").InheritedGate;
455
+ inheritedGate?: import("../core/runner/contracts.js").InheritedGate;
445
456
  /** RB-201 FO-3 (workflow lane) — the session's auto-mode classifier, consulted BEFORE each script-spawned
446
457
  * child starts (objective + toolset + output schema, finalized). The delegation-tool lane reviews its
447
458
  * children at one shared point; a workflow's children reach `runner.runTask` through this module instead,
@@ -542,11 +553,11 @@ export declare const workflowResumeClaimFallback: {
542
553
  * `done` rejects. `cancel(reason)` aborts the whole run. A nesting/validation violation throws SYNCHRONOUSLY
543
554
  * from this call (before any handle exists). Thin composition over `runner.runTask` — no Runner changes.
544
555
  */
545
- export declare function startWorkflow<T>(runner: Runner, fn: (ctx: WorkflowRunContext) => Promise<T>, opts?: RunWorkflowOptions, internals?: WorkflowInternals): WorkflowHandle<T>;
556
+ export declare function startWorkflow<T>(runner: RunnerSelfSeat, fn: (ctx: WorkflowRunContext) => Promise<T>, opts?: RunWorkflowOptions, internals?: WorkflowInternals): WorkflowHandle<T>;
546
557
  /**
547
558
  * Run a workflow to completion (design/97 S1a) — the thin await over {@link startWorkflow}: returns the
548
559
  * script's value + the assembled {@link WorkflowRun}. A synchronous nesting/validation throw from
549
560
  * `startWorkflow` becomes a rejected promise here (this function is `async`), preserving the original
550
561
  * `runWorkflow` contract (`await runWorkflow(...)` rejects rather than throwing synchronously).
551
562
  */
552
- export declare function runWorkflow<T>(runner: Runner, fn: (ctx: WorkflowRunContext) => Promise<T>, opts?: RunWorkflowOptions, internals?: WorkflowInternals): Promise<RunWorkflowResult<T>>;
563
+ export declare function runWorkflow<T>(runner: RunnerSelfSeat, fn: (ctx: WorkflowRunContext) => Promise<T>, opts?: RunWorkflowOptions, internals?: WorkflowInternals): Promise<RunWorkflowResult<T>>;
@@ -3,6 +3,8 @@ import { AsyncLocalStorage } from "node:async_hooks";
3
3
  import { createHash, randomUUID } from "node:crypto";
4
4
  import { availableParallelism } from "node:os";
5
5
  import { uuidv7 } from "../internal/harness.js";
6
+ import { TERMINAL_CAUSE_IS_REPLAYABLE, isTerminalCauseKind } from "../core/terminal-cause.js";
7
+ import { terminalProjection } from "../core/runner/terminal-projection.js";
6
8
  import { builtinAgentDefinitions } from "../agents/builtin-agents.js";
7
9
  import { GENERAL_PURPOSE_SUBAGENT_TYPE, markerFragment } from "../agents/subagent.js";
8
10
  import { combinePolicies, createAllowDenyPolicy } from "../core/tool-policy.js";
@@ -200,6 +202,19 @@ export class WorkflowResultTooLargeError extends Error {
200
202
  this.name = "WorkflowResultTooLargeError";
201
203
  }
202
204
  }
205
+ export class WorkflowJournalIncompatibleError extends Error {
206
+ fromRunId;
207
+ ordinal;
208
+ code = "workflow.journal_incompatible";
209
+ constructor(fromRunId, ordinal) {
210
+ super(`startWorkflow: resume from "${fromRunId}" was REFUSED — journal entry #${ordinal} carries no terminal cause ` +
211
+ "(a journal written before TaskResult.terminal, or a malformed entry). Completed work must not run again; " +
212
+ "start a fresh run instead of resuming this journal.");
213
+ this.fromRunId = fromRunId;
214
+ this.ordinal = ordinal;
215
+ this.name = "WorkflowJournalIncompatibleError";
216
+ }
217
+ }
203
218
  const workflowDepthStore = new AsyncLocalStorage();
204
219
  function currentWorkflowDepth() {
205
220
  return workflowDepthStore.getStore()?.depth ?? 0;
@@ -768,9 +783,12 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
768
783
  };
769
784
  let journalTail = Promise.resolve();
770
785
  const JOURNAL_DRAIN_MAX_MS = 5_000;
771
- const journalDurableResult = (result) => result.status !== "completed" && typeof result.errorMessage === "string" && result.errorMessage !== ""
772
- ? { ...result, errorMessage: boundedRedactedSummary(result.errorMessage, MAX_TRANSCRIPT_CHARS) }
773
- : result;
786
+ const journalDurableResult = (result) => {
787
+ const t = result.terminal;
788
+ if (t === undefined || t.kind !== "failed" || typeof t.message !== "string" || t.message === "")
789
+ return result;
790
+ return { ...result, terminal: { ...t, message: boundedRedactedSummary(t.message, MAX_TRANSCRIPT_CHARS) } };
791
+ };
774
792
  const journalAppend = async (callKey, result, label) => {
775
793
  const dbg = typeof process !== "undefined" && process.env?.SEMA_DEBUG_WORKFLOW_JOURNAL === "1";
776
794
  if (!journalStore) {
@@ -972,16 +990,17 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
972
990
  }
973
991
  emitRunLog(`resume: replay stopped at agent #${ordinal} (${reason}) — this call and every later call run live.`);
974
992
  };
975
- const settleAgentError = (rec, result, label, rawOutput) => {
976
- if (result.status !== "completed") {
977
- if (result.errorCode !== undefined)
978
- rec.errorCode = result.errorCode;
979
- if (result.errorMessage !== undefined && result.errorMessage !== "") {
980
- rec.errorMessage = boundedRedactedSummary(result.errorMessage, MAX_TRANSCRIPT_CHARS);
993
+ const settleAgentError = (rec, terminal, label, rawOutput) => {
994
+ const face = terminalProjection(terminal);
995
+ if (terminal.kind !== "completed") {
996
+ if (face.errorCode !== undefined)
997
+ rec.errorCode = face.errorCode;
998
+ if (face.errorMessage !== undefined && face.errorMessage !== "") {
999
+ rec.errorMessage = boundedRedactedSummary(face.errorMessage, MAX_TRANSCRIPT_CHARS);
981
1000
  }
982
- emitRunLog(`[${label}] ${result.status}${result.errorCode !== undefined ? ` (${result.errorCode})` : ""}${rec.errorMessage !== undefined ? `: ${rec.errorMessage.slice(0, 300)}` : ""}`);
1001
+ emitRunLog(`[${label}] ${face.status}${face.errorCode !== undefined ? ` (${face.errorCode})` : ""}${rec.errorMessage !== undefined ? `: ${rec.errorMessage.slice(0, 300)}` : ""}`);
983
1002
  }
984
- const output = rawOutput || (result.status !== "completed" ? (rec.errorMessage ?? "") : "");
1003
+ const output = rawOutput || (terminal.kind !== "completed" ? (rec.errorMessage ?? "") : "");
985
1004
  rec.output = output;
986
1005
  return output;
987
1006
  };
@@ -1128,14 +1147,14 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
1128
1147
  const settleAgentResult = async (rec, result, activityTail, agentOpts, journal) => {
1129
1148
  const s = result.stats;
1130
1149
  accumulateStats(result, true);
1131
- rec.status = result.status === "completed" ? "completed" : "failed";
1132
- rec.taskStatus = result.status;
1150
+ rec.status = result.terminal.kind === "completed" ? "completed" : "failed";
1151
+ rec.taskStatus = terminalProjection(result.terminal).status;
1133
1152
  if (result.sessionId)
1134
1153
  rec.sessionId = result.sessionId;
1135
1154
  setLegRunId(rec, result);
1136
1155
  rec.endedAt = now();
1137
1156
  rec.stats = { tokens: s.tokens ?? 0, turns: s.turns ?? 0, costMicroUsd: s.costMicroUsd };
1138
- const output = settleAgentError(rec, result, rec.label, boundedRedactedSummary(result.structuredOutput ?? result.result, MAX_TRANSCRIPT_CHARS));
1157
+ const output = settleAgentError(rec, result.terminal, rec.label, boundedRedactedSummary(result.structuredOutput ?? result.result, MAX_TRANSCRIPT_CHARS));
1139
1158
  if (result.stats.toolCalls !== undefined)
1140
1159
  rec.toolCalls = result.stats.toolCalls;
1141
1160
  if (activityTail.length > 0)
@@ -1149,7 +1168,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
1149
1168
  else {
1150
1169
  void journalAppend(rec.callKey, result, rec.label).catch(() => undefined);
1151
1170
  }
1152
- if (agentOpts.schema && result.status === "completed" && result.structuredOutput === undefined) {
1171
+ if (agentOpts.schema && result.terminal.kind === "completed" && result.structuredOutput === undefined) {
1153
1172
  throw new WorkflowAgentSchemaError(rec.label, result);
1154
1173
  }
1155
1174
  };
@@ -1165,7 +1184,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
1165
1184
  const { label, phase, phaseInstance, groupId, inheritedModelSnap, callKey, prompt, model } = prepareAgentCall(spec, agentOpts);
1166
1185
  if (opts.resumeFromRunId !== undefined && !diverged) {
1167
1186
  const cached = replayByOrdinal[run.agents.length];
1168
- if (cached && cached.callKey === callKey && cached.result.status === "completed") {
1187
+ if (cached && cached.callKey === callKey && TERMINAL_CAUSE_IS_REPLAYABLE[cached.result.terminal.kind]) {
1169
1188
  if (finalized)
1170
1189
  return cached.result;
1171
1190
  const r = cached.result;
@@ -1181,8 +1200,8 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
1181
1200
  prompt,
1182
1201
  ...(replayModel !== undefined ? { model: replayModel } : {}),
1183
1202
  replayed: true,
1184
- status: r.status === "completed" ? "completed" : "failed",
1185
- taskStatus: r.status,
1203
+ status: r.terminal.kind === "completed" ? "completed" : "failed",
1204
+ taskStatus: terminalProjection(r.terminal).status,
1186
1205
  ...(r.sessionId ? { sessionId: r.sessionId } : {}),
1187
1206
  ...(r.runId ? { taskRunId: r.runId } : {}),
1188
1207
  queuedAt: at,
@@ -1204,7 +1223,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
1204
1223
  accumulateStats(r, false);
1205
1224
  void persist("update");
1206
1225
  await journalAppend(callKey, r);
1207
- if (agentOpts.schema && r.status === "completed" && r.structuredOutput === undefined) {
1226
+ if (agentOpts.schema && r.terminal.kind === "completed" && r.structuredOutput === undefined) {
1208
1227
  throw new WorkflowAgentSchemaError(label, r);
1209
1228
  }
1210
1229
  return r;
@@ -1217,9 +1236,9 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
1217
1236
  : "no journal entry at this ordinal (never recorded, e.g. an oversize result under a pre-tombstone engine, or dropped)"
1218
1237
  : cached.callKey !== callKey
1219
1238
  ? "the call key changed — the script or its args differ here"
1220
- : cached.result.errorCode === JOURNAL_OVERSIZE_ERROR_CODE
1239
+ : cached.result.terminal?.kind === "failed" && cached.result.terminal.code === JOURNAL_OVERSIZE_ERROR_CODE
1221
1240
  ? "the prior result exceeded the journal size cap and was never cached"
1222
- : `the journaled result was ${cached.result.status}, not completed`);
1241
+ : `the journaled result was ${cached.result.terminal?.kind ?? "(no terminal record)"}, not completed`);
1223
1242
  diverged = true;
1224
1243
  }
1225
1244
  if (budgetTotal !== null && spent() >= budgetTotal) {
@@ -1379,7 +1398,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
1379
1398
  }
1380
1399
  throw new Error("workflow aborted");
1381
1400
  }
1382
- if (stallFired && attemptResult?.status !== "completed") {
1401
+ if (stallFired && attemptResult?.terminal.kind !== "completed") {
1383
1402
  if (attemptResult !== undefined && !finalized)
1384
1403
  accumulateStats(attemptResult, true);
1385
1404
  if (attemptResult !== undefined && !finalized)
@@ -1403,7 +1422,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
1403
1422
  throw attemptError;
1404
1423
  result = attemptResult;
1405
1424
  const durationMs = now() - attemptStart;
1406
- const degraded = result.status === "completed" &&
1425
+ const degraded = result.terminal.kind === "completed" &&
1407
1426
  result.structuredOutput === undefined &&
1408
1427
  (result.stats.outputTokens ?? Number.POSITIVE_INFINITY) < 50 &&
1409
1428
  lastStopReason === undefined &&
@@ -1451,14 +1470,15 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
1451
1470
  if (salvaged?.runId === undefined)
1452
1471
  delete rec.taskRunId;
1453
1472
  if (salvaged !== undefined) {
1454
- rec.taskStatus = salvaged.status;
1473
+ const salvagedFace = terminalProjection(salvaged.terminal);
1474
+ rec.taskStatus = salvagedFace.status;
1455
1475
  if (salvaged.sessionId)
1456
1476
  rec.sessionId = salvaged.sessionId;
1457
1477
  setLegRunId(rec, salvaged);
1458
- if (salvaged.errorCode !== undefined)
1459
- rec.errorCode = salvaged.errorCode;
1460
- if (salvaged.errorMessage !== undefined && salvaged.errorMessage !== "")
1461
- rec.errorMessage = boundedRedactedSummary(salvaged.errorMessage, MAX_TRANSCRIPT_CHARS);
1478
+ if (salvagedFace.errorCode !== undefined)
1479
+ rec.errorCode = salvagedFace.errorCode;
1480
+ if (salvagedFace.errorMessage !== undefined && salvagedFace.errorMessage !== "")
1481
+ rec.errorMessage = boundedRedactedSummary(salvagedFace.errorMessage, MAX_TRANSCRIPT_CHARS);
1462
1482
  rec.output = boundedRedactedSummary(salvaged.structuredOutput ?? salvaged.result, MAX_TRANSCRIPT_CHARS) || (rec.errorMessage ?? "");
1463
1483
  rec.stats = { tokens: salvaged.stats.tokens ?? 0, turns: salvaged.stats.turns ?? 0, costMicroUsd: salvaged.stats.costMicroUsd };
1464
1484
  }
@@ -1479,10 +1499,12 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
1479
1499
  const journaled = journalAppend(callKey, salvaged ?? {
1480
1500
  taskId: callKey,
1481
1501
  sessionId: "",
1482
- status: "failed",
1483
1502
  result: boundedRedactedSummary(err instanceof Error ? err.message : String(err), 500),
1484
- ...(err instanceof WorkflowAgentBlockedError ? { errorCode: WORKFLOW_SPAWN_BLOCKED_ERROR_CODE } : {}),
1485
- ...(err instanceof WorkflowBudgetExceededError ? { errorCode: err.code } : {}),
1503
+ terminal: {
1504
+ kind: "failed",
1505
+ ...(err instanceof WorkflowAgentBlockedError ? { code: WORKFLOW_SPAWN_BLOCKED_ERROR_CODE } : err instanceof WorkflowBudgetExceededError ? { code: err.code } : {}),
1506
+ message: boundedRedactedSummary(err instanceof Error ? err.message : String(err), 500),
1507
+ },
1486
1508
  stats: { turns: 0, tokens: 0, costMicroUsd: 0 },
1487
1509
  }, label).catch(() => undefined);
1488
1510
  if (!(err instanceof WorkflowBudgetExceededError && rec.startedAt === undefined))
@@ -1496,7 +1518,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
1496
1518
  }
1497
1519
  if (finalized)
1498
1520
  return result;
1499
- if (isDurablePause(result.status)) {
1521
+ if (isDurablePause(result.terminal)) {
1500
1522
  result = mapNestedSuspend(result);
1501
1523
  }
1502
1524
  await settleAgentResult(rec, result, activityTail, agentOpts, "awaited");
@@ -1548,9 +1570,8 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
1548
1570
  void journalAppend(callKey, {
1549
1571
  taskId: callKey,
1550
1572
  sessionId: "",
1551
- status: "failed",
1573
+ terminal: { kind: "failed", code: refusal.code, message: boundedRedactedSummary(refusal.message, 500) },
1552
1574
  result: boundedRedactedSummary(refusal.message, 500),
1553
- errorCode: refusal.code,
1554
1575
  stats: { turns: 0, tokens: 0, costMicroUsd: 0 },
1555
1576
  }).catch(() => undefined);
1556
1577
  throw refusal;
@@ -1607,9 +1628,8 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
1607
1628
  void journalAppend(callKey, {
1608
1629
  taskId: callKey,
1609
1630
  sessionId: "",
1610
- status: "failed",
1631
+ terminal: { kind: "failed", code: WORKFLOW_SPAWN_BLOCKED_ERROR_CODE, message: boundedRedactedSummary(err.message, 500) },
1611
1632
  result: boundedRedactedSummary(err.message, 500),
1612
- errorCode: WORKFLOW_SPAWN_BLOCKED_ERROR_CODE,
1613
1633
  stats: { turns: 0, tokens: 0, costMicroUsd: 0 },
1614
1634
  }).catch(() => undefined);
1615
1635
  }
@@ -1658,7 +1678,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
1658
1678
  void journalAppend(callKey, {
1659
1679
  taskId: callKey,
1660
1680
  sessionId: "",
1661
- status: "failed",
1681
+ terminal: { kind: "failed", message: boundedRedactedSummary(err instanceof Error ? err.message : String(err), 500) },
1662
1682
  result: boundedRedactedSummary(err instanceof Error ? err.message : String(err), 500),
1663
1683
  stats: { turns: partialSpend?.turns ?? 0, tokens: partialSpend?.tokens ?? 0, costMicroUsd: partialSpend?.costMicroUsd ?? 0 },
1664
1684
  }).catch(() => undefined);
@@ -1670,7 +1690,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
1670
1690
  releaseOnce();
1671
1691
  if (finalized)
1672
1692
  return result;
1673
- if (isDurablePause(result.status)) {
1693
+ if (isDurablePause(result.terminal)) {
1674
1694
  result = mapNestedSuspend(result);
1675
1695
  }
1676
1696
  await settleAgentResult(rec, result, activityTail, agentOpts, "best-effort");
@@ -1828,8 +1848,13 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
1828
1848
  "state (or stop it) and resume again.");
1829
1849
  }
1830
1850
  const entries = await journalStore.load(opts.resumeFromRunId, scope);
1831
- for (const e of entries)
1851
+ for (const e of entries) {
1852
+ const t = e.result?.terminal;
1853
+ if (t === null || typeof t !== "object" || !isTerminalCauseKind(t.kind)) {
1854
+ throw new WorkflowJournalIncompatibleError(clampRunIdText(opts.resumeFromRunId), callKeyOrdinal(e.callKey));
1855
+ }
1832
1856
  replayByOrdinal[callKeyOrdinal(e.callKey)] = e;
1857
+ }
1833
1858
  run.resume = {
1834
1859
  fromRunId: clampRunIdText(opts.resumeFromRunId),
1835
1860
  journalEntries: entries.length,
@@ -49,7 +49,7 @@ export declare const SUPERVISOR_PROMPT = "You are a supervisor \u2014 the delega
49
49
  * This block is strategy-blind (it is composed from the prompt context, which carries no strategy
50
50
  * fact), so it must not promise the arm that only one strategy delivers. */
51
51
  export declare const ORCHESTRATION_GUIDANCE_DEFERRED = "You can author and run your own WORKFLOW via the Workflow tool (multi-agent orchestration). Its schema and how-to are deferred: when a task genuinely needs orchestration, activate the tool (see the deferred-tools note) and work from what the activation returns.";
52
- export declare const ORCHESTRATION_GUIDANCE = "You can author and run your own WORKFLOW via the Workflow tool \u2014 a\ndeterministic JS script that spawns and coordinates sub-agents. Use it to be more thorough (decompose and\ncover in parallel), more confident (independent perspectives + adversarial checks before committing), or to\nhandle scale one context can't hold. This is a power tool: reach for it on a SUBSTANTIAL task that genuinely\ndecomposes \u2014 for a simple or sequential task, just do the work directly. Over-orchestrating a trivial task\nwastes tokens and adds latency.\n\nHow a workflow script works (the contract):\n- It begins with `export const meta = { name, description, phases }` \u2014 a PURE LITERAL (no variables, calls,\n or template strings). Use the same phase titles in meta.phases as in your phase() calls and in each\n agent's opts `phase`.\n- \uD83D\uDD34 After the meta line, write the body as TOP-LEVEL async statements \u2014 the primitives are already in\n scope. Do NOT wrap the body in `export default`, a function, or a `body()` method; do NOT use\n `import`/`require`; do NOT put the script inside markdown code fences. End with `return <value>`.\n The script IS the function body. A complete example \u2014 copy this SHAPE exactly:\n\n export const meta = { name: 'risk-scan', description: 'list risks in parallel', phases: [{ title: 'scan' }] }\n const results = await parallel([\n () => agent({ objective: 'Name one risk of X. Reply in one short sentence.' }, { label: 'scan-risk-a', phase: 'scan' }),\n () => agent({ objective: 'Name a DIFFERENT risk of X. Reply in one short sentence.' }, { label: 'scan-risk-b', phase: 'scan' }),\n ])\n return results.filter((r) => r && r.status === 'completed').map((r) => r.result)\n\n- The body is async and uses these injected primitives:\n - agent(spec, opts?) \u2014 run one sub-agent. spec is { objective: string (USE `objective`, not `goal`),\n modelName?, thinking?, systemPrompt? }; opts is { schema?, label?, phase?, isolation? } (schema goes in\n OPTS, not in spec). ALWAYS pass a short kebab-case `label` naming what THIS agent does (e.g.\n { label: 'find-dead-code' }) \u2014 label/phase go in OPTS, never inside spec (a spec-side label is ignored);\n unlabeled agents render as anonymous agent-N rows in the monitor. Set opts `phase` to one of your\n meta.phases titles so the agent groups under its stage.\n `isolation: \"worktree\"` runs the agent in its own isolated git worktree \u2014 use it ONLY\n when concurrent agents WRITE THE SAME repo/files and must not clobber each other (a separate working copy,\n not merely several agents). Returns the task result \u2014 read `r.result` (text) or `r.structuredOutput`\n (when you passed {schema}). agent() does NOT throw when the sub-agent fails \u2014 it RETURNS the result\n with `r.status` set; ALWAYS check `r.status` and GATE later phases on it (the Workflow tool card\n shows the full gate pattern).\n - parallel(thunks) \u2014 run thunks concurrently; BARRIER (awaits all); a thrown thunk resolves to null\n (filter before use). Use when you need all results together.\n - pipeline(items, ...stages) \u2014 each item flows through all stages independently, NO barrier between stages\n (item A can be in stage 3 while B is in stage 1). DEFAULT for multi-stage work. Each stage gets\n (prevResult, originalItem, index). A stage that throws drops that item to null.\n - phase(title, body) \u2014 group work under a named phase (shows in /workflows).\n - budget \u2014 { total, spent(), remaining() }; once spend reaches total, agent() throws. Loop on\n budget.remaining() for budget-scaled depth \u2014 but GUARD the loop on budget.total: with no budget set,\n remaining() returns Infinity and the loop runs straight into the agent cap (add a hard iteration cap).\n spent() moves when an agent SETTLES (authoritative accounting); the live per-turn figures you may see\n in run observability are display-only and never charge the budget gate.\n - log(message) \u2014 emit a progress line.\n - args \u2014 the JSON value passed to Workflow.\n- The script returns a value; you are notified when it completes and can read the result + the run via the\n workflow observability.\n\nDiscipline (this is where orchestration earns its cost):\n- DEFAULT TO pipeline(). Only use parallel() (a barrier) when a stage genuinely needs ALL prior results at\n once (dedup/merge across the full set, early-exit on zero, cross-item comparison). Otherwise pipeline so a\n fast item isn't blocked by a slow one.\n- Give each sub-agent a CLEAR goal + output spec + boundary, so they don't duplicate or conflict. A vague\n delegation produces duplicated or off-scope work. Detailed sub-task instructions matter.\n- Be confident, not just fast: for findings that must be right, spawn INDEPENDENT verifiers prompted to\n REFUTE (default to refuted if uncertain) and keep a finding only if it survives. Diverse lenses\n (correctness / security / does-it-reproduce) catch failure modes redundancy can't. When workers fan out, a\n single bad conclusion gets amplified \u2014 verify before you commit to it.\n- Scale to the task: a quick check needs a couple of agents; \"be comprehensive / audit thoroughly\" warrants a\n larger finder pool + an adversarial verify pass. Don't fan out wider than the task needs.\n\nYou operate under hard caps (a runaway script is bounded, not trusted): a token budget, a concurrency limit,\nper-agent and total timeouts, a max agent count, and a nesting limit of ONE level (a workflow's agent cannot\nitself start another workflow). Every sub-agent you spawn runs under the deployment's permission/approval/\nsafety policy \u2014 you may inherit or TIGHTEN it for a sub-agent, never loosen it. Work within these; they are\nthe safety net that lets you be trusted with this power.";
52
+ export declare const ORCHESTRATION_GUIDANCE = "You can author and run your own WORKFLOW via the Workflow tool \u2014 a\ndeterministic JS script that spawns and coordinates sub-agents. Use it to be more thorough (decompose and\ncover in parallel), more confident (independent perspectives + adversarial checks before committing), or to\nhandle scale one context can't hold. This is a power tool: reach for it on a SUBSTANTIAL task that genuinely\ndecomposes \u2014 for a simple or sequential task, just do the work directly. Over-orchestrating a trivial task\nwastes tokens and adds latency.\n\nHow a workflow script works (the contract):\n- It begins with `export const meta = { name, description, phases }` \u2014 a PURE LITERAL (no variables, calls,\n or template strings). Use the same phase titles in meta.phases as in your phase() calls and in each\n agent's opts `phase`.\n- \uD83D\uDD34 After the meta line, write the body as TOP-LEVEL async statements \u2014 the primitives are already in\n scope. Do NOT wrap the body in `export default`, a function, or a `body()` method; do NOT use\n `import`/`require`; do NOT put the script inside markdown code fences. End with `return <value>`.\n The script IS the function body. A complete example \u2014 copy this SHAPE exactly:\n\n export const meta = { name: 'risk-scan', description: 'list risks in parallel', phases: [{ title: 'scan' }] }\n const results = await parallel([\n () => agent({ objective: 'Name one risk of X. Reply in one short sentence.' }, { label: 'scan-risk-a', phase: 'scan' }),\n () => agent({ objective: 'Name a DIFFERENT risk of X. Reply in one short sentence.' }, { label: 'scan-risk-b', phase: 'scan' }),\n ])\n return results.filter((r) => r && r.terminal.kind === 'completed').map((r) => r.result)\n\n- The body is async and uses these injected primitives:\n - agent(spec, opts?) \u2014 run one sub-agent. spec is { objective: string (USE `objective`, not `goal`),\n modelName?, thinking?, systemPrompt? }; opts is { schema?, label?, phase?, isolation? } (schema goes in\n OPTS, not in spec). ALWAYS pass a short kebab-case `label` naming what THIS agent does (e.g.\n { label: 'find-dead-code' }) \u2014 label/phase go in OPTS, never inside spec (a spec-side label is ignored);\n unlabeled agents render as anonymous agent-N rows in the monitor. Set opts `phase` to one of your\n meta.phases titles so the agent groups under its stage.\n `isolation: \"worktree\"` runs the agent in its own isolated git worktree \u2014 use it ONLY\n when concurrent agents WRITE THE SAME repo/files and must not clobber each other (a separate working copy,\n not merely several agents). Returns the task result \u2014 read `r.result` (text) or `r.structuredOutput`\n (when you passed {schema}). agent() does NOT throw when the sub-agent fails \u2014 it RETURNS the result\n with `r.terminal` set (`r.terminal.kind` is 'completed' | 'failed' | 'blocked' | 'paused'); ALWAYS check\n `r.terminal.kind` and GATE later phases on it (the Workflow tool card shows the full gate pattern).\n - parallel(thunks) \u2014 run thunks concurrently; BARRIER (awaits all); a thrown thunk resolves to null\n (filter before use). Use when you need all results together.\n - pipeline(items, ...stages) \u2014 each item flows through all stages independently, NO barrier between stages\n (item A can be in stage 3 while B is in stage 1). DEFAULT for multi-stage work. Each stage gets\n (prevResult, originalItem, index). A stage that throws drops that item to null.\n - phase(title, body) \u2014 group work under a named phase (shows in /workflows).\n - budget \u2014 { total, spent(), remaining() }; once spend reaches total, agent() throws. Loop on\n budget.remaining() for budget-scaled depth \u2014 but GUARD the loop on budget.total: with no budget set,\n remaining() returns Infinity and the loop runs straight into the agent cap (add a hard iteration cap).\n spent() moves when an agent SETTLES (authoritative accounting); the live per-turn figures you may see\n in run observability are display-only and never charge the budget gate.\n - log(message) \u2014 emit a progress line.\n - args \u2014 the JSON value passed to Workflow.\n- The script returns a value; you are notified when it completes and can read the result + the run via the\n workflow observability.\n\nDiscipline (this is where orchestration earns its cost):\n- DEFAULT TO pipeline(). Only use parallel() (a barrier) when a stage genuinely needs ALL prior results at\n once (dedup/merge across the full set, early-exit on zero, cross-item comparison). Otherwise pipeline so a\n fast item isn't blocked by a slow one.\n- Give each sub-agent a CLEAR goal + output spec + boundary, so they don't duplicate or conflict. A vague\n delegation produces duplicated or off-scope work. Detailed sub-task instructions matter.\n- Be confident, not just fast: for findings that must be right, spawn INDEPENDENT verifiers prompted to\n REFUTE (default to refuted if uncertain) and keep a finding only if it survives. Diverse lenses\n (correctness / security / does-it-reproduce) catch failure modes redundancy can't. When workers fan out, a\n single bad conclusion gets amplified \u2014 verify before you commit to it.\n- Scale to the task: a quick check needs a couple of agents; \"be comprehensive / audit thoroughly\" warrants a\n larger finder pool + an adversarial verify pass. Don't fan out wider than the task needs.\n\nYou operate under hard caps (a runaway script is bounded, not trusted): a token budget, a concurrency limit,\nper-agent and total timeouts, a max agent count, and a nesting limit of ONE level (a workflow's agent cannot\nitself start another workflow). Every sub-agent you spawn runs under the deployment's permission/approval/\nsafety policy \u2014 you may inherit or TIGHTEN it for a sub-agent, never loosen it. Work within these; they are\nthe safety net that lets you be trusted with this power.";
53
53
  /**
54
54
  * Goal-mode completion guidance, composed when goal mode is enabled (design/96 C 节 LLM-self-report gate).
55
55
  * Red line (G1, design/96 §定.0): declaring "done" STOPS iteration and surfaces for review (the completion
@@ -53,7 +53,7 @@ How a workflow script works (the contract):
53
53
  () => agent({ objective: 'Name one risk of X. Reply in one short sentence.' }, { label: 'scan-risk-a', phase: 'scan' }),
54
54
  () => agent({ objective: 'Name a DIFFERENT risk of X. Reply in one short sentence.' }, { label: 'scan-risk-b', phase: 'scan' }),
55
55
  ])
56
- return results.filter((r) => r && r.status === 'completed').map((r) => r.result)
56
+ return results.filter((r) => r && r.terminal.kind === 'completed').map((r) => r.result)
57
57
 
58
58
  - The body is async and uses these injected primitives:
59
59
  - agent(spec, opts?) — run one sub-agent. spec is { objective: string (USE \`objective\`, not \`goal\`),
@@ -66,8 +66,8 @@ How a workflow script works (the contract):
66
66
  when concurrent agents WRITE THE SAME repo/files and must not clobber each other (a separate working copy,
67
67
  not merely several agents). Returns the task result — read \`r.result\` (text) or \`r.structuredOutput\`
68
68
  (when you passed {schema}). agent() does NOT throw when the sub-agent fails — it RETURNS the result
69
- with \`r.status\` set; ALWAYS check \`r.status\` and GATE later phases on it (the ${RUN_WORKFLOW_TOOL_NAME} tool card
70
- shows the full gate pattern).
69
+ with \`r.terminal\` set (\`r.terminal.kind\` is 'completed' | 'failed' | 'blocked' | 'paused'); ALWAYS check
70
+ \`r.terminal.kind\` and GATE later phases on it (the ${RUN_WORKFLOW_TOOL_NAME} tool card shows the full gate pattern).
71
71
  - parallel(thunks) — run thunks concurrently; BARRIER (awaits all); a thrown thunk resolves to null
72
72
  (filter before use). Use when you need all results together.
73
73
  - pipeline(items, ...stages) — each item flows through all stages independently, NO barrier between stages
@@ -11,7 +11,7 @@
11
11
  * yet implemented). The model pairing (who's cheap / who's strong / heterogeneous decorrelation) is
12
12
  * deploy-side knowledge — core fills guard-number defaults + prompts + orchestrator choice, never the model.
13
13
  */
14
- import type { Runner } from "../core/runner/runtask.js";
14
+ import type { RunnerSelfSeat } from "../core/runner/contracts.js";
15
15
  import type { ModelRef, ModelRole, TaskSpec } from "../core/types.js";
16
16
  import { type TeamResult } from "../agents/team.js";
17
17
  import { type VerificationResult } from "../agents/verify.js";
@@ -59,7 +59,7 @@ export interface ScenarioProfile {
59
59
  export declare const SCENARIO_REGISTRY: Record<ScenarioId, ScenarioProfile>;
60
60
  export interface RunScenarioOptions {
61
61
  scenario: ScenarioId;
62
- runner: Runner;
62
+ runner: RunnerSelfSeat;
63
63
  /** What the scenario operates on — the team `topic` (design/code review) or the solo task objective. */
64
64
  objective: string;
65
65
  /**
@@ -83,7 +83,7 @@ export interface RunScenarioOptions {
83
83
  /** Per-scenario result union — each entry returns its orchestrator's native result shape. */
84
84
  export type RunScenarioResult = {
85
85
  scenario: "solo";
86
- result: Awaited<ReturnType<Runner["runTask"]>>;
86
+ result: Awaited<ReturnType<RunnerSelfSeat["runTask"]>>;
87
87
  } | {
88
88
  scenario: "design-review";
89
89
  result: TeamResult;
@@ -125,7 +125,7 @@ async function runCodeReviewVerify(opts) {
125
125
  const reviewed = {
126
126
  taskId: "code-review",
127
127
  sessionId: "code-review",
128
- status: "completed",
128
+ terminal: { kind: "completed" },
129
129
  result: opts.objective,
130
130
  stats: { turns: 0, tokens: 0 },
131
131
  };
@@ -10,7 +10,7 @@
10
10
  * defaults only. The guard defaults are NOT re-stated here: we let teacher.ts own them (single source of
11
11
  * truth, no drift).
12
12
  */
13
- import type { Runner } from "../core/runner/runtask.js";
13
+ import type { RunnerSelfSeat } from "../core/runner/contracts.js";
14
14
  import type { ModelRef, TaskSpec } from "../core/types.js";
15
15
  import { type TeacherRunResult } from "../agents/teacher.js";
16
16
  export interface TeacherModePair {
@@ -25,7 +25,7 @@ export interface TeacherModePair {
25
25
  * student task spec's `model`. All guards use teacher.ts's built-in defaults (not re-stated here, so core
26
26
  * owns the single source of truth). The caller supplies only the two models + the task spec.
27
27
  */
28
- export declare function teacherMode(runner: Runner, spec: TaskSpec, pair: TeacherModePair): Promise<TeacherRunResult>;
28
+ export declare function teacherMode(runner: RunnerSelfSeat, spec: TaskSpec, pair: TeacherModePair): Promise<TeacherRunResult>;
29
29
  /**
30
30
  * Teacher-mode preset profile (G2: a profile = which two models to fill). The two model cells are role-name
31
31
  * placeholders the deploy side maps to concrete models (core never guesses); guard defaults are NOT pinned
@@ -1,6 +1,6 @@
1
1
  import http from "node:http";
2
2
  import type { IncomingMessage } from "node:http";
3
- import type { Runner } from "../core/runner/runtask.js";
3
+ import type { RunnerSelfSeat } from "../core/runner/contracts.js";
4
4
  import type { ImageInput, TaskSpec } from "../core/types.js";
5
5
  /** Inbound request body. Tool *code* can never come over the wire — the server injects tools via resolveSpec. */
6
6
  export interface TaskRequestBody {
@@ -10,7 +10,7 @@ export interface TaskRequestBody {
10
10
  [key: string]: unknown;
11
11
  }
12
12
  export interface TaskServerOptions {
13
- runner: Runner;
13
+ runner: RunnerSelfSeat;
14
14
  /**
15
15
  * Map an inbound request body to a full TaskSpec. This is where the SERVER decides the model,
16
16
  * tools, MCP servers, system prompt, and limits for the request. The body only supplies content