@sema-agent/core 7.5.1 → 7.6.0

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 (132) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/dist/agents/cascade.d.ts +2 -2
  3. package/dist/agents/cascade.js +12 -10
  4. package/dist/agents/repair-loop.d.ts +5 -3
  5. package/dist/agents/repair-loop.js +13 -15
  6. package/dist/agents/subagent.d.ts +24 -42
  7. package/dist/agents/subagent.js +119 -105
  8. package/dist/agents/suspend-guard.d.ts +31 -19
  9. package/dist/agents/suspend-guard.js +14 -8
  10. package/dist/agents/teacher.js +9 -9
  11. package/dist/agents/team.d.ts +4 -3
  12. package/dist/agents/team.js +10 -8
  13. package/dist/agents/verify.d.ts +3 -3
  14. package/dist/agents/verify.js +17 -17
  15. package/dist/core/a2a.js +2 -1
  16. package/dist/core/ask-origin.d.ts +60 -7
  17. package/dist/core/ask-origin.js +26 -1
  18. package/dist/core/checkpoint-store.d.ts +78 -76
  19. package/dist/core/checkpoint-store.js +17 -1
  20. package/dist/core/gate-outcome.d.ts +189 -0
  21. package/dist/core/gate-outcome.js +70 -0
  22. package/dist/core/hooks.d.ts +18 -92
  23. package/dist/core/hooks.js +88 -85
  24. package/dist/core/mcp-failure.d.ts +104 -0
  25. package/dist/core/mcp-failure.js +128 -0
  26. package/dist/core/mcp.d.ts +21 -77
  27. package/dist/core/mcp.js +76 -150
  28. package/dist/core/pause-registry.d.ts +131 -0
  29. package/dist/core/pause-registry.js +27 -0
  30. package/dist/core/protocol-table.d.ts +5 -0
  31. package/dist/core/protocol-table.js +1 -0
  32. package/dist/core/runner/abort-race.d.ts +41 -0
  33. package/dist/core/runner/abort-race.js +38 -0
  34. package/dist/core/runner/assemble-result.d.ts +32 -41
  35. package/dist/core/runner/assemble-result.js +55 -74
  36. package/dist/core/runner/checkpoint-scope.d.ts +15 -3
  37. package/dist/core/runner/checkpoint-scope.js +3 -0
  38. package/dist/core/runner/compaction-call-options.d.ts +1 -1
  39. package/dist/core/runner/content-ask-bindings.d.ts +27 -0
  40. package/dist/core/runner/content-ask-bindings.js +1 -0
  41. package/dist/core/runner/contracts.d.ts +144 -50
  42. package/dist/core/runner/denial-limit-arms.d.ts +24 -1
  43. package/dist/core/runner/denial-limit-arms.js +24 -3
  44. package/dist/core/runner/gate-exit.d.ts +74 -0
  45. package/dist/core/runner/gate-exit.js +55 -0
  46. package/dist/core/runner/inherited-ask-grants.d.ts +46 -0
  47. package/dist/core/runner/inherited-ask-grants.js +29 -0
  48. package/dist/core/runner/park-commit.d.ts +102 -0
  49. package/dist/core/runner/park-commit.js +31 -0
  50. package/dist/core/runner/{prepare-permission-rules.d.ts → permission-rule-lanes.d.ts} +109 -3
  51. package/dist/core/runner/{prepare-permission-rules.js → permission-rule-lanes.js} +47 -1
  52. package/dist/core/runner/prepare-ask-lane.d.ts +107 -0
  53. package/dist/core/runner/prepare-ask-lane.js +131 -0
  54. package/dist/core/runner/prepare-boundary-parks.d.ts +102 -0
  55. package/dist/core/runner/prepare-boundary-parks.js +169 -0
  56. package/dist/core/runner/prepare-caps-and-workflow.js +1 -1
  57. package/dist/core/runner/prepare-context-lane.d.ts +119 -0
  58. package/dist/core/runner/prepare-context-lane.js +230 -0
  59. package/dist/core/runner/prepare-gate-stations.d.ts +174 -0
  60. package/dist/core/runner/prepare-gate-stations.js +265 -0
  61. package/dist/core/runner/prepare-hands-readface.d.ts +4 -4
  62. package/dist/core/runner/prepare-inherited-gate.d.ts +4 -4
  63. package/dist/core/runner/prepare-inherited-gate.js +1 -1
  64. package/dist/core/runner/prepare-memory-engine-session.d.ts +84 -0
  65. package/dist/core/runner/prepare-memory-engine-session.js +233 -0
  66. package/dist/core/runner/prepare-memory.d.ts +44 -26
  67. package/dist/core/runner/prepare-park-ask.d.ts +162 -0
  68. package/dist/core/runner/prepare-park-ask.js +377 -0
  69. package/dist/core/runner/prepare-policy-chain.d.ts +208 -0
  70. package/dist/core/runner/prepare-policy-chain.js +584 -0
  71. package/dist/core/runner/prepare-project-context.d.ts +1 -13
  72. package/dist/core/runner/prepare-project-context.js +1 -3
  73. package/dist/core/runner/prepare-prompt-assembly.d.ts +95 -0
  74. package/dist/core/runner/prepare-prompt-assembly.js +162 -0
  75. package/dist/core/runner/prepare-prompt-inputs.d.ts +1 -20
  76. package/dist/core/runner/prepare-protocol-tools.d.ts +3 -3
  77. package/dist/core/runner/prepare-protocol-tools.js +0 -3
  78. package/dist/core/runner/prepare-question-face.d.ts +3 -21
  79. package/dist/core/runner/prepare-question-face.js +2 -1
  80. package/dist/core/runner/prepare-safety-scan.d.ts +0 -5
  81. package/dist/core/runner/prepare-safety-scan.js +1 -2
  82. package/dist/core/runner/prepare-suspend-saga.d.ts +170 -0
  83. package/dist/core/runner/prepare-suspend-saga.js +308 -0
  84. package/dist/core/runner/prepare-task.d.ts +9 -136
  85. package/dist/core/runner/prepare-task.js +47 -2745
  86. package/dist/core/runner/prepare-turn-wiring.d.ts +154 -0
  87. package/dist/core/runner/prepare-turn-wiring.js +201 -0
  88. package/dist/core/runner/prepare-wiring-manifest.d.ts +17 -17
  89. package/dist/core/runner/prepare-wiring-manifest.js +16 -10
  90. package/dist/core/runner/prepare-workspace-restore.d.ts +2 -29
  91. package/dist/core/runner/prepare-workspace-restore.js +3 -16
  92. package/dist/core/runner/prompt-hash-salt.d.ts +1 -0
  93. package/dist/core/runner/prompt-hash-salt.js +2 -0
  94. package/dist/core/runner/remote-env-retry.d.ts +29 -0
  95. package/dist/core/runner/remote-env-retry.js +16 -0
  96. package/dist/core/runner/runtask.d.ts +16 -31
  97. package/dist/core/runner/runtask.js +110 -121
  98. package/dist/core/runner/terminal-projection.d.ts +22 -0
  99. package/dist/core/runner/terminal-projection.js +28 -0
  100. package/dist/core/session.d.ts +12 -0
  101. package/dist/core/session.js +3 -0
  102. package/dist/core/store-contracts/checkpoint-store-contract.d.ts +4 -1
  103. package/dist/core/store-contracts/checkpoint-store-contract.js +8 -2
  104. package/dist/core/terminal-cause.d.ts +137 -0
  105. package/dist/core/terminal-cause.js +9 -0
  106. package/dist/core/tool-policy.d.ts +43 -139
  107. package/dist/core/tool-policy.js +79 -112
  108. package/dist/core/types.d.ts +67 -164
  109. package/dist/core/wiring-manifest.d.ts +6 -3
  110. package/dist/core/workflow-journal-store.js +3 -4
  111. package/dist/engine/harness/agent-harness.d.ts +1 -1
  112. package/dist/index.d.ts +10 -7
  113. package/dist/index.js +8 -5
  114. package/dist/orchestration/builtin-workflows.d.ts +2 -2
  115. package/dist/orchestration/builtin-workflows.js +1 -1
  116. package/dist/orchestration/goal.js +8 -7
  117. package/dist/orchestration/run-spec.js +5 -3
  118. package/dist/orchestration/run-workflow-tool.d.ts +1 -1
  119. package/dist/orchestration/run-workflow-tool.js +4 -4
  120. package/dist/orchestration/workflow-governance.d.ts +4 -4
  121. package/dist/orchestration/workflow-governance.js +4 -2
  122. package/dist/orchestration/workflow-primitives.d.ts +1 -1
  123. package/dist/orchestration/workflow-primitives.js +1 -1
  124. package/dist/orchestration/workflow.d.ts +11 -0
  125. package/dist/orchestration/workflow.js +64 -39
  126. package/dist/prompts/supervisor.d.ts +1 -1
  127. package/dist/prompts/supervisor.js +3 -3
  128. package/dist/scenarios/scenario-registry.js +1 -1
  129. package/package.json +3 -1
  130. package/test/export-surface.snapshot.json +74 -22
  131. /package/dist/core/runner/{prepare-announce-once.d.ts → announce-once-ledger.d.ts} +0 -0
  132. /package/dist/core/runner/{prepare-announce-once.js → announce-once-ledger.js} +0 -0
@@ -1,5 +1,5 @@
1
1
  import type { AssistantMessage } from "../../internal/llm.js";
2
- import type { NestedUsage, TaskResult, TaskSpec } from "../types.js";
2
+ import type { NestedUsage, PausedCause, TaskResult, TaskSpec, TerminalCause } from "../types.js";
3
3
  /** Per-task usage/cost accumulator (filled across the run loop, surfaced as `TaskResult.stats`). */
4
4
  export interface Stats {
5
5
  turns: number;
@@ -194,40 +194,16 @@ export interface ResultFlags {
194
194
  * NOT `suspended` — slotted ABOVE `suspendRef` and the aborted branches (this suspend never minted a
195
195
  * checkpoint, so `suspendRef` is unset anyway; the ordering makes the intent explicit). */
196
196
  suspendLoop?: boolean;
197
- /** design/45: the task deliberately suspended at a durable tool gate (capture + abort fired). When set,
198
- * the result is `status:"suspended"` carrying this token/gate a clean pause, NOT a failure, so it is
199
- * slotted ABOVE the `!final`/aborted branches (slot 8.5: it beats them but yields to a real
200
- * budget/threw failure that happened to coincide). The abort it used sets `final.stopReason="aborted"`
201
- * but does not set `threw`, so it never reaches the `flags.threw` branch. */
202
- suspendRef?: {
203
- token: import("../checkpoint-store.js").CheckpointToken;
204
- /** The non-secret identity twin ({@link import("../checkpoint-store.js").Checkpoint.checkpointId}). */
205
- checkpointId?: string;
206
- gate: import("../checkpoint-store.js").CheckpointGate;
207
- /** RB-439-b: how the paused workspace comes back (`"park_only"` = a non-suspendable target that was
208
- * never actually paused). Echoed on `TaskResult.workspaceRestoreMode`; absent for a process-local
209
- * suspend, which captured no remote workspace. */
210
- restoreMode?: "snapshot" | "park_only";
211
- };
212
- /** design/76 §2.5 (dry-run / shadow) + design/80 D-B (plan-gate): the task deliberately paused at a
213
- * REVIEW-PAUSE gate — `{kind:"needs_review"}` (a profile's dry-run interception produced a predicted
214
- * state-diff a human/judge must REVIEW) OR `{kind:"plan_review"}` (a profile's plan-gate produced a
215
- * proposed PLAN a human must approve/edit/reject). When set, the result is `status:"needs_review"` +
216
- * errorCode `"review.pending"`, carrying this token/gate (keyed off ref-PRESENCE, not `gate.kind`, so it
217
- * serves both review kinds uniformly). It is the DUAL of `suspendRef` but for DIFFERENT semantics (a human
218
- * REVIEW, not a pre-action tool-call APPROVAL), so it has its OWN flag and its OWN assemble slot (8.6,
219
- * between `suspendRef` 8.5 and the aborted branches 9) — **never both set at once** (the commit-side
220
- * discriminant in prepare-task branches on `gate.kind` and sets exactly one). Disjoint from `suspendRef`
221
- * so this branch is reachable: keying status off `suspendRef` alone (reusing it) would report `"suspended"`
222
- * and make this dead code (v4 MAJOR-A). Like `suspendRef`, it used an abort to stop the loop (no `threw`),
223
- * so it never hits the failure branches above. */
224
- reviewRef?: {
225
- token: import("../checkpoint-store.js").CheckpointToken;
226
- checkpointId?: string;
227
- gate: import("../checkpoint-store.js").CheckpointGate;
228
- /** RB-439-b: the review pause pauses the same workspace the approval pause does — same discriminant. */
229
- restoreMode?: "snapshot" | "park_only";
230
- };
197
+ /** The run's ONE committed durable pause (the holder's `pausedRef.current.cause`): the run stopped at a
198
+ * gate an approval or content ask, a resource slice, a platform pause, a plan or dry-run review —
199
+ * and a checkpoint is durably committed. It used an abort to stop the loop (`final.stopReason ===
200
+ * "aborted"`) but set no `threw`/timeout/turns, so it is slotted ABOVE the `!final`/aborted arms (a
201
+ * clean pause, not a failure) and BELOW the real budget/threw failures (a limit that coincided still
202
+ * owns the ending). WHICH terminal status it reads (`suspended` / `needs_review`) is the pause's own
203
+ * `gate.kind` registry row, not a second flag: the two same-shaped refs that used to split the approval
204
+ * family from the review family and the slot order between them that only ever mattered if both were
205
+ * somehow set — are gone with the commit-side routing that fed them. */
206
+ paused?: PausedCause;
231
207
  }
232
208
  /**
233
209
  * Extract a machine-readable code from a thrown error (e.g. `SessionError.code`, a Node error code).
@@ -236,6 +212,23 @@ export interface ResultFlags {
236
212
  * prefer the first specific code over a generic `"unknown"` wrapper.
237
213
  */
238
214
  export declare function errorCodeOf(err: unknown): string | undefined;
215
+ /**
216
+ * WHY the run ended — the ONE terminal arbitration ({@link TerminalCause}). First match wins, in the
217
+ * slot order documented on {@link assembleResult} (a public contract: do not reorder). Every plane
218
+ * field a result shows (`status`, `errorCode`, `errorMessage`, `blockedReason`, the checkpoint seats)
219
+ * is projected from the value this returns by {@link terminalProjection}, so a terminal that carries a
220
+ * token cannot be reported under a status that does not, and a review pause cannot be reported as an
221
+ * approval pause: the pairing is the cause's shape, not the arbitration's discipline.
222
+ *
223
+ * `apiFailure` rides beside the cause rather than inside it on purpose: it is the message's own
224
+ * provider-failure disclosure, filled only when a provider failure IS the terminal (the same
225
+ * in-presence rule the usage-window wait hint states for itself), and it is not part of the cause's
226
+ * public projection.
227
+ */
228
+ export declare function terminalCauseOf(final: AssistantMessage | undefined, stats: Pick<Stats, "humanReview">, flags: ResultFlags): {
229
+ terminal: TerminalCause;
230
+ apiFailure: TaskResult["apiFailure"];
231
+ };
239
232
  /**
240
233
  * Build the final {@link TaskResult} from the run's terminal state. **The errorCode priority chain is a
241
234
  * public contract** (callers `switch` on these strings) — first match wins, in this exact order
@@ -252,12 +245,10 @@ export declare function errorCodeOf(err: unknown): string | undefined;
252
245
  * 6. `threw` + conflict→ `"conflict"`
253
246
  * 7. `threw` (other) → `errorCodeOf(threw)`
254
247
  * 8. `blockedReason` → status `"blocked"` (no errorCode)
255
- * 8.5 `suspendRef` → status `"suspended"` + checkpointToken/gate (design/45 B4 — a deliberate
256
- * durable pause beats the abort it used, but yields to a real budget/threw failure above)
257
- * 8.6 `reviewRef` → status `"needs_review"` + `"review.pending"` + checkpointToken/gate (design/76 §2.5
258
- * a dry-run post-prediction REVIEW pause; the DUAL of 8.5, between it and the aborts, same red-line:
259
- * a `suspendRef` at 8.5 still wins if both were somehow set, proving slot order — but the commit-side
260
- * discriminant guarantees only one is ever set)
248
+ * 8.5 `paused` → status by the pause's registry row (`suspended` for an approval / resource
249
+ * slice / platform pause, `needs_review` + `"review.pending"` for a plan or dry-run review) +
250
+ * checkpointToken/gate (design/45 B4 a deliberate durable pause beats the abort it used, but
251
+ * yields to a real budget/threw failure above)
261
252
  * 9. `abortedLive` | stopReason aborted → `"limits.max_walltime_exceeded"` | `"limits.max_turns_exceeded"` | undefined
262
253
  * (audit A-1: hoisted ABOVE no-final — the loop exits cleanly on abort, so `final` may be a
263
254
  * normal message or absent; a plain user interrupt carries NO errorCode — switch on `status`),
@@ -1,5 +1,6 @@
1
1
  import { isDegenerateCutMessage } from "../../brain/terminal-cause.js";
2
2
  import { extractErrorCode, stripErrorCodePrefix } from "../../brain/errors.js";
3
+ import { terminalProjection } from "./terminal-projection.js";
3
4
  const SALVAGE_ELIGIBLE_TERMINALS = new Set([
4
5
  "output.degenerate",
5
6
  "limits.max_tokens_exceeded",
@@ -40,30 +41,9 @@ function assistantText(msg) {
40
41
  .join("");
41
42
  return stripThinkTags(text);
42
43
  }
43
- export function assembleResult(spec, sessionId, final, stats, flags) {
44
- const taskId = spec.taskId ?? sessionId;
45
- const text = final ? assistantText(final) : "";
46
- if (!flags.unpricedSpend) {
47
- const compactionMicroUsd = stats.compactionMicroUsd ?? 0;
48
- const nestedSubagentMicroUsd = stats.nested?.costMicroUsd ?? 0;
49
- stats.costBreakdown = {
50
- llmRootMicroUsd: Math.max(0, stats.costMicroUsd - compactionMicroUsd),
51
- nestedSubagentMicroUsd,
52
- memoryConsolidationMicroUsd: 0,
53
- compactionMicroUsd,
54
- };
55
- }
56
- let status;
57
- const result = text;
58
- let errorMessage;
59
- let errorCode;
44
+ export function terminalCauseOf(final, stats, flags) {
45
+ let terminal;
60
46
  let apiFailure;
61
- let blockedReason;
62
- let salvagedOutput;
63
- let checkpointToken;
64
- let checkpointId;
65
- let checkpointGate;
66
- let workspaceRestoreMode;
67
47
  const haltOwnsAbortedFinal = flags.userHalted === true &&
68
48
  final?.stopReason === "aborted" &&
69
49
  !flags.abortedLive &&
@@ -71,107 +51,108 @@ export function assembleResult(spec, sessionId, final, stats, flags) {
71
51
  flags.abortedForTurns !== true;
72
52
  const isDegenerate = final?.stopReason === "error" && isDegenerateCutMessage(final);
73
53
  if (flags.outputInvalid) {
74
- status = "failed";
75
- errorCode = "output.invalid";
76
- errorMessage = "model did not produce a valid structured output within the retry limit";
54
+ terminal = { kind: "failed", code: "output.invalid", message: "model did not produce a valid structured output within the retry limit" };
77
55
  }
78
56
  else if (isDegenerate) {
79
- status = "failed";
80
- errorCode = "output.degenerate";
81
- errorMessage = final?.errorMessage;
57
+ terminal = { kind: "failed", code: "output.degenerate", ...(final?.errorMessage !== undefined ? { message: final.errorMessage } : {}) };
82
58
  }
83
59
  else if (flags.budgetHit) {
84
- status = "failed";
85
- errorCode = flags.budgetAxis === "tokens" ? "limits.max_tokens_exceeded" : "limits.max_cost_exceeded";
86
60
  const axisName = flags.budgetAxis === "tokens" ? "limits.maxTokens" : "limits.maxCostUsd";
87
- errorMessage =
88
- flags.budgetHit === "precall"
61
+ terminal = {
62
+ kind: "failed",
63
+ code: flags.budgetAxis === "tokens" ? "limits.max_tokens_exceeded" : "limits.max_cost_exceeded",
64
+ message: flags.budgetHit === "precall"
89
65
  ? `the estimated cost of the first call already exceeds ${axisName}; the task was not started`
90
- : `cumulative usage exceeded ${axisName}`;
66
+ : `cumulative usage exceeded ${axisName}`,
67
+ };
91
68
  }
92
69
  else if (flags.suspendLoop) {
93
- status = "failed";
94
- errorCode = "suspend.loop";
95
- errorMessage = "task suspended too many times (resume/restart loop) — exceeded the suspend limit";
70
+ let message = "task suspended too many times (resume/restart loop) — exceeded the suspend limit";
96
71
  const gates = stats.humanReview?.gates ?? [];
97
72
  if (gates.length > 0 && gates.every((g) => g.decision === "allow")) {
98
- errorMessage +=
73
+ message +=
99
74
  `. Diagnosis: all ${gates.length} recorded gate decision(s) on this run were ALLOW — a gate that keeps ` +
100
75
  `asking combined with an approver that keeps approving consumes the suspend allowance on legitimate ` +
101
76
  `work. Remedies: answer asks at a LIVE onAsk (a synchronous allow parks nothing and consumes no ` +
102
77
  `suspend), keep provably-benign commands inside the read boundary so the classifier auto-allows them, ` +
103
78
  `or raise maxSuspends for genuinely approval-heavy tasks.`;
104
79
  }
80
+ terminal = { kind: "failed", code: "suspend.loop", message };
105
81
  }
106
82
  else if (flags.threw) {
107
- status = "failed";
108
- errorMessage = flags.threw instanceof Error ? flags.threw.message : String(flags.threw);
109
- errorCode = flags.abortedForTimeout
83
+ const message = flags.threw instanceof Error ? flags.threw.message : String(flags.threw);
84
+ const code = flags.abortedForTimeout
110
85
  ? "limits.max_walltime_exceeded"
111
86
  : flags.abortedForTurns
112
87
  ? "limits.max_turns_exceeded"
113
88
  : flags.conflict
114
89
  ? "conflict"
115
90
  : errorCodeOf(flags.threw);
91
+ terminal = { kind: "failed", ...(code !== undefined ? { code } : {}), message };
116
92
  }
117
93
  else if (flags.blockedReason) {
118
- status = "blocked";
119
- blockedReason = flags.blockedReason;
120
- }
121
- else if (flags.suspendRef) {
122
- status = "suspended";
123
- checkpointToken = flags.suspendRef.token;
124
- checkpointId = flags.suspendRef.checkpointId;
125
- checkpointGate = flags.suspendRef.gate;
126
- workspaceRestoreMode = flags.suspendRef.restoreMode;
127
- }
128
- else if (flags.reviewRef) {
129
- status = "needs_review";
130
- errorCode = "review.pending";
131
- checkpointToken = flags.reviewRef.token;
132
- checkpointId = flags.reviewRef.checkpointId;
133
- checkpointGate = flags.reviewRef.gate;
134
- workspaceRestoreMode = flags.reviewRef.restoreMode;
94
+ terminal = { kind: "blocked", reason: flags.blockedReason };
95
+ }
96
+ else if (flags.paused) {
97
+ terminal = flags.paused;
135
98
  }
136
99
  else if ((flags.abortedLive || final?.stopReason === "aborted") && !haltOwnsAbortedFinal) {
137
- status = "failed";
138
- errorCode = flags.abortedForTimeout ? "limits.max_walltime_exceeded" : flags.abortedForTurns ? "limits.max_turns_exceeded" : undefined;
139
- errorMessage = final?.errorMessage ?? (flags.abortedForTurns ? "max turns exceeded" : "run aborted");
100
+ const code = flags.abortedForTimeout ? "limits.max_walltime_exceeded" : flags.abortedForTurns ? "limits.max_turns_exceeded" : undefined;
101
+ terminal = { kind: "failed", ...(code !== undefined ? { code } : {}), message: final?.errorMessage ?? (flags.abortedForTurns ? "max turns exceeded" : "run aborted") };
140
102
  }
141
103
  else if (flags.userHalted === true && (!final || haltOwnsAbortedFinal)) {
142
- status = "completed";
104
+ terminal = { kind: "completed" };
143
105
  }
144
106
  else if (!final) {
145
- status = "failed";
146
- errorMessage = "no assistant message produced";
107
+ terminal = { kind: "failed", message: "no assistant message produced" };
147
108
  }
148
109
  else if (final.stopReason === "error") {
149
- status = "failed";
150
- errorMessage = final.errorMessage ?? "model error";
110
+ let message = final.errorMessage ?? "model error";
111
+ let code;
151
112
  if (final.isApiErrorMessage === true) {
152
113
  apiFailure = {
153
114
  ...(final.apiErrorStatus !== undefined ? { status: final.apiErrorStatus } : {}),
154
115
  ...(final.requestId !== undefined ? { requestId: final.requestId } : {}),
155
116
  };
156
117
  }
157
- const lifted = extractErrorCode(errorMessage);
118
+ const lifted = extractErrorCode(message);
158
119
  if (lifted) {
159
- errorCode = lifted;
160
- errorMessage = stripErrorCodePrefix(errorMessage);
120
+ code = lifted;
121
+ message = stripErrorCodePrefix(message);
161
122
  }
162
123
  else if (flags.gitCoreOverBudget) {
163
- errorCode = "irreducible_core_over_budget";
124
+ code = "irreducible_core_over_budget";
164
125
  }
165
126
  else if (flags.conflict) {
166
- errorCode = "conflict";
127
+ code = "conflict";
167
128
  }
168
129
  else {
169
- errorCode = "provider.error";
130
+ code = "provider.error";
170
131
  }
132
+ terminal = { kind: "failed", code, message };
171
133
  }
172
134
  else {
173
- status = "completed";
135
+ terminal = { kind: "completed" };
174
136
  }
137
+ return { terminal, apiFailure };
138
+ }
139
+ export function assembleResult(spec, sessionId, final, stats, flags) {
140
+ const taskId = spec.taskId ?? sessionId;
141
+ const text = final ? assistantText(final) : "";
142
+ if (!flags.unpricedSpend) {
143
+ const compactionMicroUsd = stats.compactionMicroUsd ?? 0;
144
+ const nestedSubagentMicroUsd = stats.nested?.costMicroUsd ?? 0;
145
+ stats.costBreakdown = {
146
+ llmRootMicroUsd: Math.max(0, stats.costMicroUsd - compactionMicroUsd),
147
+ nestedSubagentMicroUsd,
148
+ memoryConsolidationMicroUsd: 0,
149
+ compactionMicroUsd,
150
+ };
151
+ }
152
+ const result = text;
153
+ let salvagedOutput;
154
+ const { terminal, apiFailure } = terminalCauseOf(final, stats, flags);
155
+ const { status, errorCode } = terminalProjection(terminal);
175
156
  if (errorCode !== undefined && SALVAGE_ELIGIBLE_TERMINALS.has(errorCode)) {
176
157
  salvagedOutput = text.trim() || undefined;
177
158
  }
@@ -181,5 +162,5 @@ export function assembleResult(spec, sessionId, final, stats, flags) {
181
162
  if (flags.unpricedSpend)
182
163
  delete publicStats.costMicroUsd;
183
164
  const stampHaltedByUser = flags.userHalted === true && status !== "suspended" && status !== "needs_review";
184
- return { taskId, ...(flags.runId !== undefined ? { runId: flags.runId } : {}), sessionId, status, ...(flags.model !== undefined ? { model: flags.model } : {}), result: result.trim(), salvagedOutput, blockedReason, errorMessage, errorCode, ...(apiFailure !== undefined ? { apiFailure } : {}), ...(retryAfterMs !== undefined ? { retryAfterMs } : {}), checkpointToken, ...(checkpointId !== undefined ? { checkpointId } : {}), checkpointGate, ...(workspaceRestoreMode !== undefined ? { workspaceRestoreMode } : {}), ...(flags.rewindNotes !== undefined && flags.rewindNotes.length > 0 ? { rewindNotes: flags.rewindNotes } : {}), ...(flags.editedFiles !== undefined && flags.editedFiles.length > 0 ? { editedFiles: flags.editedFiles } : {}), ...(flags.haltedOnUserRejection === true ? { haltedOnUserRejection: true } : {}), ...(stampHaltedByUser ? { haltedByUser: true } : {}), ...(flags.remoteEnvFailures !== undefined && flags.remoteEnvFailures.length > 0 ? { remoteEnvFailures: [...flags.remoteEnvFailures] } : {}), ...(flags.strandedHumanAnswers !== undefined && flags.strandedHumanAnswers.length > 0 ? { strandedHumanAnswers: flags.strandedHumanAnswers } : {}), ...(flags.effectiveReadFace !== undefined ? { effectiveReadFace: flags.effectiveReadFace } : {}), ...(flags.effectiveReadDenyPatterns !== undefined && flags.effectiveReadDenyPatterns.length > 0 ? { effectiveReadDenyPatterns: flags.effectiveReadDenyPatterns } : {}), ...(flags.effectiveMemoryScopes !== undefined ? { effectiveMemoryScopes: flags.effectiveMemoryScopes } : {}), ...(flags.effectiveReasoning !== undefined ? { effectiveReasoning: flags.effectiveReasoning } : {}), stats: publicStats };
165
+ return { taskId, ...(flags.runId !== undefined ? { runId: flags.runId } : {}), sessionId, terminal, ...(flags.model !== undefined ? { model: flags.model } : {}), result: result.trim(), salvagedOutput, ...(apiFailure !== undefined ? { apiFailure } : {}), ...(retryAfterMs !== undefined ? { retryAfterMs } : {}), ...(flags.rewindNotes !== undefined && flags.rewindNotes.length > 0 ? { rewindNotes: flags.rewindNotes } : {}), ...(flags.editedFiles !== undefined && flags.editedFiles.length > 0 ? { editedFiles: flags.editedFiles } : {}), ...(flags.haltedOnUserRejection === true ? { haltedOnUserRejection: true } : {}), ...(stampHaltedByUser ? { haltedByUser: true } : {}), ...(flags.remoteEnvFailures !== undefined && flags.remoteEnvFailures.length > 0 ? { remoteEnvFailures: [...flags.remoteEnvFailures] } : {}), ...(flags.strandedHumanAnswers !== undefined && flags.strandedHumanAnswers.length > 0 ? { strandedHumanAnswers: flags.strandedHumanAnswers } : {}), ...(flags.effectiveReadFace !== undefined ? { effectiveReadFace: flags.effectiveReadFace } : {}), ...(flags.effectiveReadDenyPatterns !== undefined && flags.effectiveReadDenyPatterns.length > 0 ? { effectiveReadDenyPatterns: flags.effectiveReadDenyPatterns } : {}), ...(flags.effectiveMemoryScopes !== undefined ? { effectiveMemoryScopes: flags.effectiveMemoryScopes } : {}), ...(flags.effectiveReasoning !== undefined ? { effectiveReasoning: flags.effectiveReasoning } : {}), stats: publicStats };
185
166
  }
@@ -1,7 +1,9 @@
1
1
  /**
2
- * The durable-suspend checkpoint SCOPE vocabulary — the default isolation scope and the ONE derivation of a row's scope —
3
- * moved whole out of the orchestrator so the wiring-manifest phase (the synthesized durableApproval) and the driver's
4
- * park-lane mint sites reach DOWN for the same two names. Pure: a constant and a string function over two spec fields.
2
+ * The durable-suspend checkpoint ROW-IDENTITY vocabulary — the default isolation scope, the ONE derivation of a row's
3
+ * scope, and the ONE reading of the placement fields' empty string moved whole out of the orchestrator so the
4
+ * wiring-manifest phase (the synthesized durableApproval), the park-lane phases (the mint sites and the suspend stamp),
5
+ * the driver (the restore fold) and runtask (the resume rung) reach DOWN for the same names. Pure: a constant and two
6
+ * string functions.
5
7
  */
6
8
  /** design/77 §4.4: the multi-tenant scope used when a durable suspend fires for an IRREVERSIBLE tool in an
7
9
  * unattended deployment that did NOT opt into `durableApproval` (so there is no caller-supplied scope). The
@@ -30,3 +32,13 @@ export declare function checkpointScopeOf(spec: {
30
32
  };
31
33
  principal?: string;
32
34
  }): string;
35
+ /**
36
+ * rescan C5 — the ONE spelling of the placement fields' empty-string discipline: `""` is absence
37
+ * wearing clothes (the resume entry's principal-rung posture), and every placement read that must
38
+ * treat it so — the resume rung's two sides (runtask), the restore fold's seed and the suspend
39
+ * stamp — goes through THIS helper, so three sites cannot drift into three readings. Deliberately
40
+ * NOT applied to the live `internals.placementRoot`/`rootSessionId` reads of the RESOLUTION fold:
41
+ * an empty supplied claim there is an assembly error the execution-env phase's `mintPlacementRootSessionId` refuses
42
+ * loudly on the factory path, and normalizing it away would silently repair what should be loud.
43
+ */
44
+ export declare function placementValueOrAbsent(value: string | undefined): string | undefined;
@@ -2,3 +2,6 @@ export const DEFAULT_IRREVERSIBLE_SCOPE = "irreversible";
2
2
  export function checkpointScopeOf(spec) {
3
3
  return spec.durableApproval?.scope || spec.principal || DEFAULT_IRREVERSIBLE_SCOPE;
4
4
  }
5
+ export function placementValueOrAbsent(value) {
6
+ return value || undefined;
7
+ }
@@ -3,7 +3,7 @@ import type { MaybeCompactOptions } from "../auto-compaction.js";
3
3
  import type { StaleToolResultOffloadOptions, TaskSpec } from "../types.js";
4
4
  import type { Context } from "../../internal/llm.js";
5
5
  import { type ToolResultStore } from "../tool-result-store.js";
6
- import type { Prepared } from "./prepare-task.js";
6
+ import type { Prepared } from "./contracts.js";
7
7
  /**
8
8
  * REF-A2 (car R11) — the maybeCompact call arguments that MUST be assembled identically on all three
9
9
  * of runtask's compaction lanes (turn boundary / prompt-too-long recovery / end-of-task).
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The content-ask BINDING vocabulary: the leak bound the routing lane checks against and the one entry shape the
3
+ * binding table holds. It lived in the question-face phase (which mints the table) while the routing lane was still
4
+ * orchestrator code; the lane is a phase now, and a phase reaching SIDEWAYS into a sibling for a constant and a type
5
+ * is the wrong reading — both phases reach DOWN for them here.
6
+ */
7
+ import type { QuestionAnswer } from "../ask-question.js";
8
+ export declare const CONTENT_ASK_BINDING_CAP = 32;
9
+ /** One gate-routed content-ask outcome, keyed by the call id it was produced for (see the table's own note). */
10
+ export type ContentAskBinding = ({
11
+ kind: "answered";
12
+ answer: QuestionAnswer;
13
+ } | {
14
+ kind: "failed";
15
+ error: unknown;
16
+ }) & {
17
+ /** Digest of the exact question batch this outcome belongs to. The call id alone is not enough
18
+ * to redeem a binding: it comes from the model/provider and can repeat, so an entry that
19
+ * outlived its call (its call was allowed but never executed) could otherwise be handed to a
20
+ * LATER call that happens to reuse the id. Matching on the batch as well means an outcome can
21
+ * only ever be delivered for the question it was produced for. */
22
+ questionsHash: string;
23
+ /** The engine-minted per-delivery identity (see AskQuestionRequest.deliveryId) — carried so a
24
+ * stranded-answer disclosure names DELIVERIES, not call ids: call ids can repeat, and a
25
+ * disclosure keyed on them would collapse two lost answers into one. */
26
+ deliveryId: string;
27
+ };
@@ -0,0 +1 @@
1
+ export const CONTENT_ASK_BINDING_CAP = 32;