@sema-agent/core 5.61.0 → 5.63.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 (61) hide show
  1. package/CHANGELOG.md +107 -0
  2. package/dist/agents/subagent.d.ts +12 -2
  3. package/dist/agents/subagent.js +3 -2
  4. package/dist/brain/open-responses.js +8 -3
  5. package/dist/brain/openai.js +4 -4
  6. package/dist/brain/stream-engine.d.ts +13 -2
  7. package/dist/brain/stream-engine.js +3 -3
  8. package/dist/core/auto-compaction.d.ts +6 -4
  9. package/dist/core/auto-compaction.js +3 -0
  10. package/dist/core/auto-mode-prompt-assets.js +1 -1
  11. package/dist/core/checkpoint-store.d.ts +36 -4
  12. package/dist/core/checkpoint-store.js +1 -0
  13. package/dist/core/context-edit.d.ts +36 -29
  14. package/dist/core/context-edit.js +3 -3
  15. package/dist/core/governance-codes.d.ts +1 -1
  16. package/dist/core/governance-codes.js +2 -0
  17. package/dist/core/hooks.d.ts +86 -4
  18. package/dist/core/hooks.js +3 -3
  19. package/dist/core/memory-engine/engine.d.ts +11 -0
  20. package/dist/core/memory-engine/engine.js +29 -3
  21. package/dist/core/memory-engine/index.d.ts +1 -1
  22. package/dist/core/memory-engine/origin-clearance.d.ts +28 -0
  23. package/dist/core/park-selfcheck.js +2 -0
  24. package/dist/core/pricing.d.ts +24 -0
  25. package/dist/core/pricing.js +18 -0
  26. package/dist/core/runner/prepare-config-doors.d.ts +36 -2
  27. package/dist/core/runner/prepare-config-doors.js +66 -8
  28. package/dist/core/runner/prepare-task.d.ts +113 -12
  29. package/dist/core/runner/prepare-task.js +239 -131
  30. package/dist/core/runner/runtask.d.ts +7 -0
  31. package/dist/core/runner/runtask.js +325 -95
  32. package/dist/core/runner/turn-attachments.d.ts +137 -5
  33. package/dist/core/runner/turn-attachments.js +25 -2
  34. package/dist/core/store-contracts/checkpoint-store-contract.js +19 -0
  35. package/dist/core/tool-errors.d.ts +2 -1
  36. package/dist/core/tool-policy.d.ts +27 -0
  37. package/dist/core/trace.d.ts +5 -4
  38. package/dist/core/types.d.ts +163 -29
  39. package/dist/core/untrusted-text.d.ts +5 -4
  40. package/dist/core/untrusted-text.js +8 -0
  41. package/dist/core/usage-window-store.d.ts +109 -8
  42. package/dist/core/usage-window-store.js +79 -12
  43. package/dist/engine/harness/agent-harness.js +20 -5
  44. package/dist/engine/harness/types.d.ts +38 -0
  45. package/dist/engine/loop/agent-loop.js +20 -1
  46. package/dist/engine/loop/types.d.ts +41 -1
  47. package/dist/index.d.ts +1 -1
  48. package/dist/orchestration/run-workflow-tool.d.ts +2 -2
  49. package/dist/orchestration/workflow-types.d.ts +48 -1
  50. package/dist/orchestration/workflow-types.js +12 -4
  51. package/dist/orchestration/workflow.d.ts +14 -3
  52. package/dist/orchestration/workflow.js +44 -19
  53. package/dist/prompt-assembly/event-registry.js +2 -0
  54. package/dist/prompts/default.js +1 -1
  55. package/dist/server/http.d.ts +1 -1
  56. package/dist/stores/file/usage-window-store.d.ts +1 -1
  57. package/dist/stores/file/usage-window-store.js +27 -6
  58. package/dist/tools/loop-tick.js +1 -1
  59. package/dist/tools/scheduler-tools.js +9 -1
  60. package/package.json +1 -1
  61. package/test/export-surface.snapshot.json +3 -1
@@ -24,6 +24,9 @@ export function resolveUsageWindows(windows) {
24
24
  if (typeof w.maxTokens !== "number" || !Number.isFinite(w.maxTokens) || w.maxTokens < 0) {
25
25
  throw usageConfigError("config.usage_window_invalid", `RunnerDeps.usageWindows[].maxTokens must be a finite, non-negative number (got ${String(w.maxTokens)})`);
26
26
  }
27
+ if (w.maxCostUsd !== undefined && (typeof w.maxCostUsd !== "number" || !Number.isFinite(w.maxCostUsd) || w.maxCostUsd < 0)) {
28
+ throw usageConfigError("config.usage_window_invalid", `RunnerDeps.usageWindows[].maxCostUsd must be a finite, non-negative number of USD when present (got ${String(w.maxCostUsd)})`);
29
+ }
27
30
  if (w.anchor !== "first-use" && w.anchor !== "rolling") {
28
31
  throw usageConfigError("config.usage_window_invalid", `RunnerDeps.usageWindows[].anchor must be "first-use" or "rolling" (got ${String(w.anchor)})`);
29
32
  }
@@ -40,11 +43,34 @@ function rollingHorizonMs(windows) {
40
43
  }
41
44
  return widest;
42
45
  }
43
- export function chargeUsageRecord(record, tokens, at, windows) {
46
+ export function windowsGovernCost(windows) {
47
+ return windows.some((w) => w.maxCostUsd !== undefined);
48
+ }
49
+ function maxCostMicroUsdOf(w) {
50
+ return w.maxCostUsd === undefined ? undefined : Math.ceil(Number((w.maxCostUsd * 1e6).toPrecision(12)));
51
+ }
52
+ export function chargeUsageRecord(record, tokens, at, windows, costMicroUsd) {
44
53
  const horizon = rollingHorizonMs(windows);
45
- const keptSlots = horizon === undefined ? [] : record.slots.filter((s) => s.at > at - horizon).map((s) => ({ at: s.at, tokens: s.tokens }));
46
- if (horizon !== undefined && tokens > 0)
47
- keptSlots.push({ at, tokens });
54
+ const governsCost = windowsGovernCost(windows);
55
+ if (governsCost && costMicroUsd === undefined) {
56
+ throw usageConfigError("usage_window.store_cost_unanswered", "a usage-window CHARGE for a deployment that governs maxCostUsd arrived with no cost figure at all. Core supplies one (a number, or an explicit null for spend nothing could price) on every such charge, so an absent one means a store in the chain does not carry the cost argument — folding it to 0 would leave the money ceiling open forever with nothing able to notice. Forward `costMicroUsd` from `UsageWindowStore.charge` into `chargeUsageRecord`.");
57
+ }
58
+ const cost = governsCost && costMicroUsd !== null ? costMicroUsd : undefined;
59
+ const unknown = governsCost && costMicroUsd === null;
60
+ const moves = tokens > 0 || (cost !== undefined && cost > 0) || unknown;
61
+ const keptSlots = horizon === undefined
62
+ ? []
63
+ : record.slots
64
+ .filter((s) => s.at > at - horizon)
65
+ .map((s) => ({
66
+ at: s.at,
67
+ tokens: s.tokens,
68
+ ...(s.costMicroUsd === undefined ? {} : { costMicroUsd: s.costMicroUsd }),
69
+ ...(s.costUnknown === true ? { costUnknown: true } : {}),
70
+ }));
71
+ if (horizon !== undefined && moves) {
72
+ keptSlots.push({ at, tokens, ...(cost === undefined ? {} : { costMicroUsd: cost }), ...(unknown ? { costUnknown: true } : {}) });
73
+ }
48
74
  const buckets = [];
49
75
  for (const w of windows) {
50
76
  if (w.anchor !== "first-use")
@@ -54,11 +80,21 @@ export function chargeUsageRecord(record, tokens, at, windows) {
54
80
  const prior = record.buckets.find((b) => b.windowMs === w.windowMs);
55
81
  const lapsed = prior === undefined || at >= prior.openedAt + w.windowMs;
56
82
  if (lapsed) {
57
- if (tokens > 0)
58
- buckets.push({ windowMs: w.windowMs, openedAt: at, tokens });
83
+ if (moves) {
84
+ buckets.push({ windowMs: w.windowMs, openedAt: at, tokens, ...(cost === undefined ? {} : { costMicroUsd: cost }), ...(unknown ? { costUnknown: true } : {}) });
85
+ }
59
86
  continue;
60
87
  }
61
- buckets.push({ windowMs: w.windowMs, openedAt: prior.openedAt, tokens: prior.tokens + tokens });
88
+ const priorCost = prior.costMicroUsd;
89
+ const nextCost = cost === undefined ? priorCost : (priorCost ?? 0) + cost;
90
+ const nextUnknown = unknown || prior.costUnknown === true;
91
+ buckets.push({
92
+ windowMs: w.windowMs,
93
+ openedAt: prior.openedAt,
94
+ tokens: prior.tokens + tokens,
95
+ ...(nextCost === undefined ? {} : { costMicroUsd: nextCost }),
96
+ ...(nextUnknown ? { costUnknown: true } : {}),
97
+ });
62
98
  }
63
99
  return { slots: keptSlots, buckets };
64
100
  }
@@ -66,6 +102,8 @@ export function readUsageRecord(record, windows, now) {
66
102
  const readings = [];
67
103
  for (const w of windows) {
68
104
  let tokens = 0;
105
+ let costMicroUsd = 0;
106
+ let costUnknown = false;
69
107
  let freesAt;
70
108
  if (w.anchor === "rolling") {
71
109
  const floor = now - w.windowMs;
@@ -74,6 +112,9 @@ export function readUsageRecord(record, windows, now) {
74
112
  if (s.at <= floor)
75
113
  continue;
76
114
  tokens += s.tokens;
115
+ costMicroUsd += s.costMicroUsd ?? 0;
116
+ if (s.costUnknown === true)
117
+ costUnknown = true;
77
118
  if (oldest === undefined || s.at < oldest)
78
119
  oldest = s.at;
79
120
  }
@@ -84,18 +125,44 @@ export function readUsageRecord(record, windows, now) {
84
125
  const row = record.buckets.find((b) => b.windowMs === w.windowMs);
85
126
  if (row !== undefined && now < row.openedAt + w.windowMs) {
86
127
  tokens = row.tokens;
128
+ costMicroUsd = row.costMicroUsd ?? 0;
129
+ if (row.costUnknown === true)
130
+ costUnknown = true;
87
131
  freesAt = row.openedAt + w.windowMs;
88
132
  }
89
133
  }
90
- const exhausted = tokens >= w.maxTokens;
134
+ const maxCostMicroUsd = maxCostMicroUsdOf(w);
135
+ const exhausted = tokens >= w.maxTokens || (maxCostMicroUsd !== undefined && costMicroUsd >= maxCostMicroUsd);
91
136
  const retryAfterMs = exhausted ? Math.max(1, freesAt === undefined ? w.windowMs : freesAt - now) : 0;
92
- readings.push({ window: w, tokens, exhausted, retryAfterMs });
137
+ readings.push({
138
+ window: w,
139
+ tokens,
140
+ ...(maxCostMicroUsd !== undefined ? { costMicroUsd } : {}),
141
+ ...(maxCostMicroUsd !== undefined && costUnknown ? { costUnknown: true } : {}),
142
+ exhausted,
143
+ retryAfterMs,
144
+ });
93
145
  }
94
146
  return readings;
95
147
  }
96
- export function usageRetryAfterMs(readings) {
148
+ export function usageRetryAfterMs(readings, windows) {
149
+ if (windows !== undefined) {
150
+ if (readings.length !== windows.length) {
151
+ throw usageConfigError("usage_window.store_cost_unanswered", `the usage-window ledger answered ${String(readings.length)} reading(s) for ${String(windows.length)} declared window(s) — the read contract is one reading per window, in the order given. A window with no reading was never evaluated: refused rather than treated as open.`);
152
+ }
153
+ for (let i = 0; i < windows.length; i++) {
154
+ const w = windows[i];
155
+ const echoed = readings[i].window;
156
+ if (echoed === undefined || echoed.windowMs !== w.windowMs || echoed.anchor !== w.anchor || echoed.maxTokens !== w.maxTokens || echoed.maxCostUsd !== w.maxCostUsd) {
157
+ throw usageConfigError("usage_window.store_cost_unanswered", `the usage-window ledger's reading #${String(i)} does not echo declared window #${String(i)} verbatim (declared: windowMs ${String(w.windowMs)}, ${String(w.anchor)}, maxTokens ${String(w.maxTokens)}, maxCostUsd ${String(w.maxCostUsd)}) — the read contract requires the window back verbatim, in order, so its ceilings are provably the ones evaluated. Refused rather than re-attributed.`);
158
+ }
159
+ }
160
+ }
97
161
  let worst;
98
162
  for (const r of readings) {
163
+ if (r.window?.maxCostUsd !== undefined && r.costMicroUsd === undefined) {
164
+ throw usageConfigError("usage_window.store_cost_unanswered", `the usage-window ledger answered a window that declares maxCostUsd ${String(r.window.maxCostUsd)} (windowMs ${String(r.window.windowMs)}, ${String(r.window.anchor)}) with NO costMicroUsd — the money ceiling was never evaluated. Refused rather than treated as open: the store must persist the cost charged to it and report what the window holds.`);
165
+ }
99
166
  if (!r.exhausted)
100
167
  continue;
101
168
  if (worst === undefined || r.retryAfterMs > worst)
@@ -105,9 +172,9 @@ export function usageRetryAfterMs(readings) {
105
172
  }
106
173
  export class InMemoryUsageWindowStore {
107
174
  records = new Map();
108
- async charge(key, tokens, at, windows) {
175
+ async charge(key, tokens, at, windows, costMicroUsd) {
109
176
  const prior = this.records.get(key) ?? EMPTY_USAGE_WINDOW_RECORD;
110
- this.records.set(key, chargeUsageRecord(prior, tokens, at, windows));
177
+ this.records.set(key, chargeUsageRecord(prior, tokens, at, windows, costMicroUsd));
111
178
  }
112
179
  async read(key, windows, now) {
113
180
  return readUsageRecord(this.records.get(key) ?? EMPTY_USAGE_WINDOW_RECORD, windows, now);
@@ -547,7 +547,7 @@ export class AgentHarness {
547
547
  userInputParkRecords.delete(message);
548
548
  engineNotePayloads.delete(message);
549
549
  }
550
- createLoopConfig(getTurnState, setTurnState) {
550
+ createLoopConfig(getTurnState, setTurnState, runPromptOverride) {
551
551
  const turnState = getTurnState();
552
552
  const self = this;
553
553
  return {
@@ -564,9 +564,24 @@ export class AgentHarness {
564
564
  : {}),
565
565
  convertToLlm: (messages) => stripEngineMetadata(convertToLlm(messages)),
566
566
  shouldStopAfterTurn: () => this._stopAfterTurn,
567
- transformContext: async (messages) => {
568
- const result = await this.emitHook({ type: "context", messages: [...messages] });
569
- return result?.messages ?? messages;
567
+ transformContext: async (messages, signal) => {
568
+ const result = await this.emitHook({ type: "context", messages: [...messages], ...(signal !== undefined ? { signal } : {}) });
569
+ if (result?.adoptSessionRebuild !== true) {
570
+ return result?.messages ?? messages;
571
+ }
572
+ await this.flushPendingSessionWrites();
573
+ const nextTurnState = await this.createTurnState();
574
+ setTurnState(nextTurnState);
575
+ const rebuiltContext = this.createContext(nextTurnState, runPromptOverride);
576
+ const recheck = await this.emitHook({ type: "context", messages: [...rebuiltContext.messages], ...(signal !== undefined ? { signal } : {}), recheck: true });
577
+ return {
578
+ messages: recheck?.messages ?? rebuiltContext.messages,
579
+ adoptedContext: {
580
+ messages: rebuiltContext.messages,
581
+ systemPrompt: rebuiltContext.systemPrompt,
582
+ ...(rebuiltContext.systemBlocks !== undefined ? { systemBlocks: rebuiltContext.systemBlocks } : {}),
583
+ },
584
+ };
570
585
  },
571
586
  beforeToolCall: async ({ toolCall, args }) => {
572
587
  const result = await this.emitHook({
@@ -780,7 +795,7 @@ export class AgentHarness {
780
795
  this.runAbortController = abortController;
781
796
  const runResultPromise = (async () => {
782
797
  try {
783
- return await runAgentLoop(messages, this.createContext(turnState, beforeResult?.systemPrompt), this.createLoopConfig(getTurnState, setTurnState), (event) => this.handleAgentEvent(event, abortController.signal), abortController.signal, this.createStreamFn(getTurnState), undefined, this.loopTrace);
798
+ return await runAgentLoop(messages, this.createContext(turnState, beforeResult?.systemPrompt), this.createLoopConfig(getTurnState, setTurnState, beforeResult?.systemPrompt), (event) => this.handleAgentEvent(event, abortController.signal), abortController.signal, this.createStreamFn(getTurnState), undefined, this.loopTrace);
784
799
  }
785
800
  catch (error) {
786
801
  try {
@@ -919,6 +919,25 @@ export interface BeforeAgentStartEvent<TSkill extends Skill = Skill, TPromptTemp
919
919
  export interface ContextEvent {
920
920
  type: "context";
921
921
  messages: AgentMessage[];
922
+ /**
923
+ * The TURN-scoped abort signal of the request build being transformed (present when the loop
924
+ * handed one — it always does in production; absent only for bare-harness callers). A handler
925
+ * that runs an EXPENSIVE reduction (the slice-3 arm-B in-turn compaction is the standing case)
926
+ * must honor it: a turn interrupt (design/373) fires this signal, and a reduction that only
927
+ * watches the RUN-level signal would make the interrupt wait out a long summary call and commit
928
+ * a session mutation the interrupted turn no longer needs (adversarial review r3).
929
+ */
930
+ signal?: AbortSignal;
931
+ /**
932
+ * design/374 slice 3 — set on the ONE bounded re-dispatch of the adopt seam
933
+ * ({@link ContextResult.adoptSessionRebuild}): `messages` are the just-adopted session rebuild,
934
+ * re-presented so the handler's request pipeline applies to what the provider will actually
935
+ * receive (the "recheck" step of build→reduce→adopt→recheck). A handler must not request a
936
+ * second adoption on this dispatch — the harness does not honor it (see the result member's
937
+ * doc) — so a reduction that is still over budget takes the handler's own next arm (the trim
938
+ * last resort) instead of looping. Absent on every ordinary dispatch.
939
+ */
940
+ recheck?: boolean;
922
941
  }
923
942
  export interface TurnBoundaryEvent {
924
943
  /** Fires between model-request boundaries, after session flush, before the context rebuild. */
@@ -997,6 +1016,25 @@ export interface BeforeAgentStartResult {
997
1016
  /** Hook result for replacing the full context message list before provider conversion. */
998
1017
  export interface ContextResult {
999
1018
  messages: AgentMessage[];
1019
+ /**
1020
+ * design/374 slice 3 (arm B, the adopt seam) — the handler PERSISTED a session-level reduction
1021
+ * during this dispatch (e.g. an in-turn forced compaction appended to the session) and asks the
1022
+ * harness to adopt it MID-BUILD, the same three-step form as the two existing adoption flows
1023
+ * (turn_boundary / prompt-too-long recovery): flush pending session writes, rebuild the turn
1024
+ * context from the session (`createTurnState`), adopt it (`setTurnState` + the loop-context
1025
+ * adoption via {@link import("../loop/types.js").TransformedContext}), then re-dispatch the
1026
+ * context hook ONCE on the rebuilt view (`ContextEvent.recheck`) so the request pipeline applies
1027
+ * to it. Result: the provider request, every subsequent hook dispatch, and the active turn state
1028
+ * all see the SAME reduced transcript — the three-party co-view the seam exists for. `messages`
1029
+ * on this arm are the handler's best unadopted view and are superseded by the rebuild.
1030
+ *
1031
+ * Honored AT MOST ONCE per request build: on the recheck dispatch this member is ignored (its
1032
+ * `messages` are used verbatim) — the bound that keeps a reduction that cannot get under budget
1033
+ * from re-entering forever. Typed on the hook RESULT deliberately (not a closure side channel):
1034
+ * the adoption is part of the hook's answer, and the one prior closure-ref transport in this
1035
+ * area (`requestLossyRef`) is a recorded bypass shape, not a precedent to grow.
1036
+ */
1037
+ adoptSessionRebuild?: boolean;
1000
1038
  }
1001
1039
  /** Hook result for patching provider request options before payload construction. */
1002
1040
  export interface BeforeProviderRequestResult {
@@ -524,7 +524,26 @@ async function settleInterruptedTurn(state, message, executor, committedResults,
524
524
  async function streamAssistantResponse(context, config, signal, emit, streamFn, runtime, executor, staticReasoningCutDowngrade) {
525
525
  let messages = context.messages;
526
526
  if (config.transformContext) {
527
- messages = await config.transformContext(messages, signal);
527
+ const transformed = await config.transformContext(messages, signal);
528
+ if (Array.isArray(transformed)) {
529
+ messages = transformed;
530
+ }
531
+ else {
532
+ const adopted = transformed.adoptedContext;
533
+ if (adopted !== undefined) {
534
+ context.messages = adopted.messages;
535
+ if (adopted.systemPrompt !== undefined) {
536
+ context.systemPrompt = adopted.systemPrompt;
537
+ if (adopted.systemBlocks !== undefined) {
538
+ context.systemBlocks = adopted.systemBlocks;
539
+ }
540
+ else {
541
+ delete context.systemBlocks;
542
+ }
543
+ }
544
+ }
545
+ messages = transformed.messages;
546
+ }
528
547
  }
529
548
  const llmMessages = await config.convertToLlm(messages);
530
549
  const llmContext = {
@@ -223,6 +223,40 @@ export interface LoopThinkingOnlyRecovery {
223
223
  * side is `recover(messages, attempt)` and also carries `withholdErrorEvents`, so the two can never share
224
224
  * one type. (`agent-harness.ts` is outside this file's edit scope — its side of this mutual-reference
225
225
  * note is pending.) */
226
+ /**
227
+ * design/374 slice 3 — the transform's ADOPTING return shape (see
228
+ * {@link AgentLoopConfig.transformContext}). Two arrays with different standings, on purpose:
229
+ * `messages` is the WIRE view for this one request (a request-only projection — cleared markers,
230
+ * caps, trim — which must NEVER become loop state, the D-4 non-destructive posture), while
231
+ * `adoptedContext` is a session-rebuilt TRANSCRIPT the loop adopts as its live context
232
+ * (`state.context.messages`) so the rest of the turn — later requests, the interrupt reconcile,
233
+ * the recovery pops — continues from the reduced transcript. This is the transform-seam sibling of
234
+ * the ④b recover adoption (`state.context.messages = replaced`); a transform that reduced the
235
+ * SESSION but only re-projected the wire would leave the loop replaying the pre-reduction
236
+ * transcript into every later request of the turn.
237
+ */
238
+ export interface TransformedContext {
239
+ /** The wire view for THIS request (what `convertToLlm` receives). */
240
+ messages: AgentMessage[];
241
+ /** Present ⇒ adopt this rebuilt context as the loop's live state before streaming. */
242
+ adoptedContext?: {
243
+ /** The rebuilt transcript the loop adopts as `state.context.messages`. */
244
+ messages: AgentMessage[];
245
+ /** When present, the rebuilt SYSTEM PROMPT is adopted too — and it applies to THIS very
246
+ * request (the loop reads `context.systemPrompt` after the transform): an in-turn compaction
247
+ * can commit a prompt-epoch change (center-prompt adoption/rollback rides the compaction
248
+ * pass), and a request built from the rebuilt transcript under the PRE-epoch prompt would
249
+ * disagree with the epoch the session just recorded (adversarial review r1). `systemBlocks`
250
+ * must only ever accompany the prompt they byte-correspond to; when the prompt is adopted
251
+ * WITHOUT blocks, stale blocks are dropped (the string face is the truth source — same
252
+ * degrade-to-string posture as the harness's atomic-face guard). */
253
+ systemPrompt?: string;
254
+ systemBlocks?: Array<{
255
+ text: string;
256
+ cacheControlBoundary: boolean;
257
+ }>;
258
+ };
259
+ }
226
260
  export interface LoopRecoveryOptions {
227
261
  promptTooLong?: LoopPromptTooLongRecovery;
228
262
  truncatedOutput?: LoopTruncatedOutputRecovery;
@@ -328,8 +362,14 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
328
362
  * return messages;
329
363
  * }
330
364
  * ```
365
+ *
366
+ * Return shape (design/374 slice 3): a bare array is the historic contract — a REQUEST-ONLY
367
+ * projection, never adopted into the loop's live context. Returning a
368
+ * {@link TransformedContext} additionally lets the transform ADOPT a session-rebuilt transcript
369
+ * mid-turn (the in-turn forced-compaction seam) — the loop half of the same two-half adoption
370
+ * form the prompt-too-long recovery already has (`state.context.messages = replaced`).
331
371
  */
332
- transformContext?: (messages: AgentMessage[], signal?: AbortSignal) => Promise<AgentMessage[]>;
372
+ transformContext?: (messages: AgentMessage[], signal?: AbortSignal) => Promise<AgentMessage[] | TransformedContext>;
333
373
  /**
334
374
  * Resolves an API key dynamically for each LLM call.
335
375
  *
package/dist/index.d.ts CHANGED
@@ -168,7 +168,7 @@ export { AdoptionError, assertAdoptionBootGate, readRootAdoptionFile, writeRootA
168
168
  export { adoptLocalDataRoot, ackAdoptionConfig, witnessAdoptionConfig, listAdoptionQuarantine, readAdoptionStatus, type AdoptionStatus, type AdoptLocalDataRootOptions, type AdoptLocalDataRootResult, type AdoptionCarriageLeg, type AdoptionCarriageLegContext, type AdoptionConfigWitnessReceipt, } from "./stores/file/adoption/adopt.js";
169
169
  export { formatHookFeedback, runToolGate, createHookEnvCapabilities, createPreToolUseConstraintPolicy, type PersistedRuleHit, type PersistedRuleHitRule, type PersistedRuleUnreadable, type PersistedRuleCoverage, type PersistedRuleAnswer, normalizePersistedRuleHit, type Hooks, type HookToolContext, type HookInvocationIdentity, type UserPromptSubmitContext, type PostToolBatchContext, type HookEnvCapabilities, type HookToolOutput, type PreToolUseResult, type PostToolUseResult, type UserPromptSubmitResult, type HookToolFailure, type PostToolUseFailureResult, type PostToolBatchCall, type PostToolBatchResult, type PreCompactContext, type PreCompactResult, type PostCompactContext, type StopFailureContext, type PermissionDeniedPayload, type PermissionDeniedSource, } from "./core/hooks.js";
170
170
  export { InMemoryMemoryStore, composeMemoryBlock, composeLayeredMemoryBlock, normalizeMemorySpec, type NormalizedMemorySpec, type MemorySpecInput, supportsConsolidation, supportsPeriodicConsolidation, firstSentence, expandLexicalTerms, lexicalSearchMatch, type Embedder, classifyPromotable, enforcePromotableWriteGate, guardedMemoryStore, MemoryGateError, detectSecret, enforceSecretWriteGate, enforceStructuredNoteSecretGate, type UtilityGate, type MemoryStore, type MemoryVectorMode, type ScoredMemory, type MemoryNoteHeader, type MemoryNoteRecord, type MemoryNoteType, type StructuredNoteInput, } from "./core/memory.js";
171
- export { MemoryEngine, FileMemoryEngineBackend, memoryBackendContract, assertMemoryBackendSearchEquivalence, type MemoryBackendContractHooks, buildMemoryInstruction, truncateIndex, scanEntryFiles, deriveRepoKey, deriveRepoMemoryDir, deriveControlPlaneDir, deriveRepoControlPlaneDir, resolveMemoryEngineRoot, scopeDirName, ControlPlaneCorruptError, parseEntryFile, serializeEntryFile, computeEntryRev, entryFromFile, MEMORY_ORIGIN_CAUSES, committedOriginOf, originEquals, ambiguousOriginRepresentation, type OriginClearanceRow, type OriginClearanceEvent, committedDistilledOf, distilledEquals, type MemoryEntryDistilled, type MemoryEntryDistilledInput, CONSOLIDATION_DEFAULTS, ConsolidationRefusedError, MEMORY_SEARCH_SUPERSEDED_TAG, consolidationTypeEligible, deriveSupersededSet, memorySupersededNote, readIntentCredentials, supersessionFuseCeiling, memoryConsolidationRecommendedNotice, memoryConsolidationCommittedNotice, memoryConsolidationConflictNotice, memoryConsolidationIncompleteNotice, memoryConsolidationRefusedNotice, type ConsolidationGateRead, type ConsolidationGateRow, type ConsolidationIntent, type ConsolidationIntentCredentialRow, type ConsolidationLeaseSeat, type ConsolidationProductProposal, type ConsolidationProposal, type MemoryConsolidationOptions, type ConsolidationCommitReceipt, type ConsolidationReconcileReport, type ConsolidationResolveReceipt, type ConsolidationPlanSummary, type ConsolidationPlanFoldEvidence, DISTILLER_DEFAULT_MAX_INPUTS_PER_PRODUCT, LLM_DISTILLER_CONTRACT, LLM_DISTILLER_CONTRACT_DL2, LLM_DISTILLER_CONTRACT_DL3, LLM_DISTILLER_CONTRACTS, MEMORY_DISTILLER_CONTRACT_V1, contractGroupingDiff, driveConsolidationToFixpoint, isAliasModelId, llmPlanDistiller, mintExposurePartitionedPlan, mintLlmConsolidationPlan, openAiCompatChatSeat, parseJsonAnswer, planParseRepairs, sanitizeLlmGroups, scheduleUnderFuse, type ConsolidationDistillFn, type ConsolidationDriveCycleRow, type ConsolidationDriveEngine, type ConsolidationDriveResult, type ConsolidationFoldState, type DistillerCandidate, type DistillerChatAnswer, type DistillerChatFn, type DistillerChatRequest, type FuseSchedule, type LlmConsolidationPlan, type LlmConsolidationPlanArm, type LlmConsolidationPlanProduct, type LlmDistillerContract, type MintLlmConsolidationPlanResult, type PlanParseRepairs, type SanitizedLlmGroups, CONSOLIDATION_DRIVER_PLANS_DIR, CONSOLIDATION_DRIVER_RUNS_FILE, CONSOLIDATION_RUN_STOP_REASONS, archiveDistillerPlan, readConsolidationDriverRun, runMemoryConsolidationDriver, type ConsolidationDriverEngine, type ConsolidationDriverRunRow, type ConsolidationRunReceipt, type ConsolidationRunStopReason, type RunMemoryConsolidationOptions, isInstructionEntry, type MemoryEntryOrigin, type MemoryOriginCause, MEMORY_INSTRUCTION_TEMPLATE, MEMORY_RECALL_DISCIPLINE, MEMORY_PREFERENCE_DISCIPLINE, MEMORY_READONLY_NOTICE, readV2HeaderHints, type V2HeaderHints, LINEAGE_FILE, CHALLENGES_FILE, CHALLENGED_HISTORY_FILE, CHALLENGE_LEDGER_MAX_EVENTS, rebuildStrictControlPlaneLedger, type ControlPlaneRebuildReceipt, type StrictControlPlaneLedger, type ChallengeAssignment, type ChallengeEvent, type ChallengedHistoryRow, type LineagePendingTxn, type LineagePromotion, MEMORY_INDEX_FILENAME, MEMORY_INDEX_MAX_LINES, MEMORY_INDEX_MAX_BYTES, MEMORY_SEARCH_TOOL_NAME, MEMORY_GET_TOOL_NAME, MEMORY_ENGINE_TOOL_NAMES, type MemorySearchDetails, type MemorySearchHit, type CleanMemorySearchHit, type ExposedMemorySearchHit, MEMORY_EXPOSURE_BANNER, MEMORY_EXPOSURE_HANDLE_TAG, memoryExposureIndexRow, type MemoryGetDetails, STUB_ARCHIVED_LINE, DEFAULT_MAX_MEMORY_FILES, DEFAULT_MAX_ENTRY_DEPTH, DEFAULT_HARVEST_DEADLINE_MS, DEFAULT_HARVEST_FILE_BUDGET, MASS_DELETION_FUSE_RATIO, scanMemoryWrite, scanMemoryFileName, scanRemediation, MEMORY_FILENAME_SEGMENT_RE, renderAnnouncements, enqueueMemoryAnnouncement, drainMemoryAnnouncements, peekMemoryAnnouncements, bumpScanFuse, scanFuseCount, clearScanFuse, ANNOUNCEMENTS_FILE, MEMORY_ANNOUNCEMENTS_MAX, SCAN_FUSE_FILE, SCAN_FUSE_THRESHOLD, type MemoryAnnouncement, type ScanFinding, type MemoryEngineOptions, type MemoryInjection, type EntryProvenanceAccount, type TransferEvidence, type CommittedBinding, type CommittedEntrySnapshot, type CommittedScopeSnapshots, type EntryCustodyReport, erasureSelectHash, type EraseMemoryEntriesInput, type ErasureSelect, type ErasedBinding, type MemoryErasureAttestation, computeMemoryBundleHash, type MemoryExportBundle, type MemoryImportReport, type MemoryExportSnapshot, type MemoryBundleImportPlan, type BundleChallengeRow, type BundleLineageRow, type BundlePollutedSession, type MemoryBackend, type MemoryEntry, type MemoryEntryFrontmatter, type MemoryEntryHeader, type MemoryScopeEnumeration, type ScoredMemoryEntry, type NotePatch, type PatchReport, type MaterializedFile, type MemorySessionHandle, type HarvestReport, type HarvestRejection, type HarvestRejectionCode, type ParsedEntryFile, type ScannedEntryFile, SCOPE_SEGMENT_MAX_ENCODED, PROJECT_MARKER_PATH, encodeScopeSegment, decodeScopeSegment, parseScopeKey, formatUserScope, formatOrgScope, formatProjScope, formatUserProjScope, isPersonalScope, assertScopeContractPlacement, formatProjectMarker, parseProjectMarker, resolveProjectId, PROJECT_ID_REGEX, type ParsedScopeKey, migrateScope, type MigrateScopeReport, materializeEntriesToFiles, harvestFilesToPatches, projectionsToWriteBack, screenInboundEntries, REMOTE_HARVEST_PER_FILE_BYTES, REMOTE_MASS_DELETION_FUSE_RATIO, type RemoteMemoryFile, type RemoteMemoryBaseline, type RemoteMaterialization, type RemoteHarvestResult, type InboundEntryFinding, deriveProjectMemoryDir, deriveProjectControlDir, recordProjectIdHint, lookupProjectIdHint, PROJECT_ID_HINTS_FILE, reconcileMemoryEntries, nextSyncBaseline, mergeRecallHits, type MemorySyncCursor, type MemorySyncConflict, type MemorySyncPlan, type RecallSource, syncMemoryScope, type MemorySyncTransport, type MemorySyncRequestBody, type MemorySyncResponseBody, type MemorySyncClientConflict, type SyncMemoryScopeOptions, type MemorySyncClientResult, } from "./core/memory-engine/index.js";
171
+ export { MemoryEngine, FileMemoryEngineBackend, memoryBackendContract, assertMemoryBackendSearchEquivalence, type MemoryBackendContractHooks, buildMemoryInstruction, truncateIndex, scanEntryFiles, deriveRepoKey, deriveRepoMemoryDir, deriveControlPlaneDir, deriveRepoControlPlaneDir, resolveMemoryEngineRoot, scopeDirName, ControlPlaneCorruptError, parseEntryFile, serializeEntryFile, computeEntryRev, entryFromFile, MEMORY_ORIGIN_CAUSES, committedOriginOf, originEquals, ambiguousOriginRepresentation, type OriginClearanceRow, type OriginClearanceEvent, type OriginClearanceShadow, committedDistilledOf, distilledEquals, type MemoryEntryDistilled, type MemoryEntryDistilledInput, CONSOLIDATION_DEFAULTS, ConsolidationRefusedError, MEMORY_SEARCH_SUPERSEDED_TAG, consolidationTypeEligible, deriveSupersededSet, memorySupersededNote, readIntentCredentials, supersessionFuseCeiling, memoryConsolidationRecommendedNotice, memoryConsolidationCommittedNotice, memoryConsolidationConflictNotice, memoryConsolidationIncompleteNotice, memoryConsolidationRefusedNotice, type ConsolidationGateRead, type ConsolidationGateRow, type ConsolidationIntent, type ConsolidationIntentCredentialRow, type ConsolidationLeaseSeat, type ConsolidationProductProposal, type ConsolidationProposal, type MemoryConsolidationOptions, type ConsolidationCommitReceipt, type ConsolidationReconcileReport, type ConsolidationResolveReceipt, type ConsolidationPlanSummary, type ConsolidationPlanFoldEvidence, DISTILLER_DEFAULT_MAX_INPUTS_PER_PRODUCT, LLM_DISTILLER_CONTRACT, LLM_DISTILLER_CONTRACT_DL2, LLM_DISTILLER_CONTRACT_DL3, LLM_DISTILLER_CONTRACTS, MEMORY_DISTILLER_CONTRACT_V1, contractGroupingDiff, driveConsolidationToFixpoint, isAliasModelId, llmPlanDistiller, mintExposurePartitionedPlan, mintLlmConsolidationPlan, openAiCompatChatSeat, parseJsonAnswer, planParseRepairs, sanitizeLlmGroups, scheduleUnderFuse, type ConsolidationDistillFn, type ConsolidationDriveCycleRow, type ConsolidationDriveEngine, type ConsolidationDriveResult, type ConsolidationFoldState, type DistillerCandidate, type DistillerChatAnswer, type DistillerChatFn, type DistillerChatRequest, type FuseSchedule, type LlmConsolidationPlan, type LlmConsolidationPlanArm, type LlmConsolidationPlanProduct, type LlmDistillerContract, type MintLlmConsolidationPlanResult, type PlanParseRepairs, type SanitizedLlmGroups, CONSOLIDATION_DRIVER_PLANS_DIR, CONSOLIDATION_DRIVER_RUNS_FILE, CONSOLIDATION_RUN_STOP_REASONS, archiveDistillerPlan, readConsolidationDriverRun, runMemoryConsolidationDriver, type ConsolidationDriverEngine, type ConsolidationDriverRunRow, type ConsolidationRunReceipt, type ConsolidationRunStopReason, type RunMemoryConsolidationOptions, isInstructionEntry, type MemoryEntryOrigin, type MemoryOriginCause, MEMORY_INSTRUCTION_TEMPLATE, MEMORY_RECALL_DISCIPLINE, MEMORY_PREFERENCE_DISCIPLINE, MEMORY_READONLY_NOTICE, readV2HeaderHints, type V2HeaderHints, LINEAGE_FILE, CHALLENGES_FILE, CHALLENGED_HISTORY_FILE, CHALLENGE_LEDGER_MAX_EVENTS, rebuildStrictControlPlaneLedger, type ControlPlaneRebuildReceipt, type StrictControlPlaneLedger, type ChallengeAssignment, type ChallengeEvent, type ChallengedHistoryRow, type LineagePendingTxn, type LineagePromotion, MEMORY_INDEX_FILENAME, MEMORY_INDEX_MAX_LINES, MEMORY_INDEX_MAX_BYTES, MEMORY_SEARCH_TOOL_NAME, MEMORY_GET_TOOL_NAME, MEMORY_ENGINE_TOOL_NAMES, type MemorySearchDetails, type MemorySearchHit, type CleanMemorySearchHit, type ExposedMemorySearchHit, MEMORY_EXPOSURE_BANNER, MEMORY_EXPOSURE_HANDLE_TAG, memoryExposureIndexRow, type MemoryGetDetails, STUB_ARCHIVED_LINE, DEFAULT_MAX_MEMORY_FILES, DEFAULT_MAX_ENTRY_DEPTH, DEFAULT_HARVEST_DEADLINE_MS, DEFAULT_HARVEST_FILE_BUDGET, MASS_DELETION_FUSE_RATIO, scanMemoryWrite, scanMemoryFileName, scanRemediation, MEMORY_FILENAME_SEGMENT_RE, renderAnnouncements, enqueueMemoryAnnouncement, drainMemoryAnnouncements, peekMemoryAnnouncements, bumpScanFuse, scanFuseCount, clearScanFuse, ANNOUNCEMENTS_FILE, MEMORY_ANNOUNCEMENTS_MAX, SCAN_FUSE_FILE, SCAN_FUSE_THRESHOLD, type MemoryAnnouncement, type ScanFinding, type MemoryEngineOptions, type MemoryInjection, type EntryProvenanceAccount, type TransferEvidence, type CommittedBinding, type CommittedEntrySnapshot, type CommittedScopeSnapshots, type EntryCustodyReport, erasureSelectHash, type EraseMemoryEntriesInput, type ErasureSelect, type ErasedBinding, type MemoryErasureAttestation, computeMemoryBundleHash, type MemoryExportBundle, type MemoryImportReport, type MemoryExportSnapshot, type MemoryBundleImportPlan, type BundleChallengeRow, type BundleLineageRow, type BundlePollutedSession, type MemoryBackend, type MemoryEntry, type MemoryEntryFrontmatter, type MemoryEntryHeader, type MemoryScopeEnumeration, type ScoredMemoryEntry, type NotePatch, type PatchReport, type MaterializedFile, type MemorySessionHandle, type HarvestReport, type HarvestRejection, type HarvestRejectionCode, type ParsedEntryFile, type ScannedEntryFile, SCOPE_SEGMENT_MAX_ENCODED, PROJECT_MARKER_PATH, encodeScopeSegment, decodeScopeSegment, parseScopeKey, formatUserScope, formatOrgScope, formatProjScope, formatUserProjScope, isPersonalScope, assertScopeContractPlacement, formatProjectMarker, parseProjectMarker, resolveProjectId, PROJECT_ID_REGEX, type ParsedScopeKey, migrateScope, type MigrateScopeReport, materializeEntriesToFiles, harvestFilesToPatches, projectionsToWriteBack, screenInboundEntries, REMOTE_HARVEST_PER_FILE_BYTES, REMOTE_MASS_DELETION_FUSE_RATIO, type RemoteMemoryFile, type RemoteMemoryBaseline, type RemoteMaterialization, type RemoteHarvestResult, type InboundEntryFinding, deriveProjectMemoryDir, deriveProjectControlDir, recordProjectIdHint, lookupProjectIdHint, PROJECT_ID_HINTS_FILE, reconcileMemoryEntries, nextSyncBaseline, mergeRecallHits, type MemorySyncCursor, type MemorySyncConflict, type MemorySyncPlan, type RecallSource, syncMemoryScope, type MemorySyncTransport, type MemorySyncRequestBody, type MemorySyncResponseBody, type MemorySyncClientConflict, type SyncMemoryScopeOptions, type MemorySyncClientResult, } from "./core/memory-engine/index.js";
172
172
  export { SHARED_MEMORY_READ_CAP_BYTES, SHARED_MEMORY_LIST_PAGE_SIZE, SharedMemoryStoreError, type SharedMemoryStoreProvider, type SharedMemoryStoreReader, type SharedMemoryPagedList, type SharedMemoryStoreInfo, type SharedMemoryDocumentEntry, type SharedMemorySnapshot, type SharedMemoryRequestContext, type MemoryListDetails, type MemoryReadDetails, } from "./core/shared-memory/types.js";
173
173
  export { sharedMemoryStoreContract, type SharedMemoryFixture, type SharedMemoryStoreContractHooks, } from "./core/shared-memory/contract.js";
174
174
  export { termSet, jaccardDistance, cosineDistance } from "./core/memory-vector.js";
@@ -278,8 +278,8 @@ export interface RunWorkflowToolDeps {
278
278
  * (`RunnerDeps.onAsk`, else the fail-closed headless auto-deny). */
279
279
  parentOnAsk?: import("../core/tool-policy.js").OnAsk;
280
280
  /** The HOST run's display sink (its `RunInternals.onForwardEvent` behind the runner's ctx wrapper:
281
- * `task_progress` always, plus the children's content events — `text_delta`/`reasoning_delta`/
282
- * `tool_start`/`tool_end`, UNTRUSTED-RAW: the consumer must redact — when the HOST spec set
281
+ * `task_progress` always, plus the children's content events — `text_delta`/`text_end`/
282
+ * `reasoning_delta`/`tool_start`/`tool_end`, UNTRUSTED-RAW: the consumer must redact — when the HOST spec set
283
283
  * `forwardSubagentEvents: true`) — threaded via `startWorkflow` into every spawned agent's trusted
284
284
  * internals so a workflow child's events bubble to the deployment's one sink, the same
285
285
  * channel a `createSubagentTool` delegation threads. Display-only; absent ⇒ ticks stay in each
@@ -153,6 +153,45 @@ export interface WorkflowRun {
153
153
  * contract (store status filters, terminal checks like `isTerminalWorkflowStatus`, monitor rows all
154
154
  * switch on the closed enum — a new enum value would break them; an optional field cannot). */
155
155
  agentFailures?: number;
156
+ /** The run's TERMINAL token-budget overshoot — present ONLY when the run actually spent MORE than its
157
+ * ceiling (`spentTokens + unsettledTokens > budgetTokens`), absent on every run that stayed within it and
158
+ * on every run that set no budget at all. Same ADDITIVE-observation contract as {@link agentFailures}
159
+ * (never a gate input, never re-read by the engine, and the status enums stay untouched).
160
+ *
161
+ * WHY IT CAN EXIST AT ALL: the budget is a per-CALL ADMISSION gate — it refuses NEW `ctx.agent` calls
162
+ * once settled live spend reaches the ceiling, and agents already IN FLIGHT at that moment are not bound
163
+ * by it (the {@link WorkflowBudgetExceededError} message promises exactly that: "In-flight agents will
164
+ * complete"). So a wide concurrency window can land far past the ceiling while the script's body still
165
+ * returns normally — a run that read as an unqualified `completed` with no trace of the overrun anywhere
166
+ * on its record. This seat is that trace; the same fact is also narrated on the run's log lane.
167
+ *
168
+ * `budgetTokens` is the configured ceiling and `spentTokens` is the gate's OWN input — LIVE settled spend
169
+ * (`ctx.budget.spent()`) at the terminal, which is NOT the same figure as `stats.tokens +
170
+ * stats.nested.tokens` (run stats also count REPLAYED work, which the budget deliberately never charges).
171
+ *
172
+ * `unsettledTokens` (present only when non-zero) is spend OBSERVED on agents still IN FLIGHT at the
173
+ * terminal — a fire-and-forget `agentStream`, or one a deadline abandoned. Nothing downstream ever
174
+ * accounts for it (a settle landing after the run finalized is dropped by design), so it is reported
175
+ * here rather than silently lost: without it this seat would answer "no overshoot" for a run that in
176
+ * fact burned several times its ceiling. It is kept SEPARATE from `spentTokens` — the own/nested
177
+ * discipline {@link WorkflowRunStats} uses — because the two have different standing: settled spend the
178
+ * gate itself read, versus a best-known observation the gate never charged. **A consumer measuring the
179
+ * overshoot adds them** (`spentTokens + unsettledTokens - budgetTokens`).
180
+ *
181
+ * ⚠️ `unsettledTokens` is an ESTIMATE, stated rather than implied — the SAME live per-turn figure the
182
+ * running-agent observation surfaces already show (`stats` while an agent runs), with the same standing:
183
+ * · it omits work those agents DELEGATED (that reaches this engine only through `TaskResult.stats.nested`
184
+ * at their settle, which for an agent still in flight at the terminal never arrives), and
185
+ * · it is the beat's own per-turn arithmetic (cache-inclusive input + output), which APPROXIMATES the
186
+ * authoritative per-call figure a settle would have written rather than reproducing it.
187
+ * So a run whose overshoot rests ENTIRELY on this member is a best-effort disclosure, not a measurement:
188
+ * read it as "this run appears to have overrun, and here is what was seen". `spentTokens` carries no such
189
+ * caveat — it is the gate's own settled figure. */
190
+ budgetOvershoot?: {
191
+ budgetTokens: number;
192
+ spentTokens: number;
193
+ unsettledTokens?: number;
194
+ };
156
195
  phases: WorkflowPhase[];
157
196
  agents: WorkflowAgentRun[];
158
197
  /** design/97 CORE-3: nested `ctx.workflow` sub-groups (the persisted group tree). Empty when the script
@@ -372,9 +411,17 @@ export declare class WorkflowMaxAgentsError extends Error {
372
411
  /** The run's token ceiling AT THE MOMENT THE CAP FIRED, or `null` when the run set no budget. It selects
373
412
  * which cause the message may name — the two are mutually exclusive facts, not one fixed label. */
374
413
  readonly budgetTotal: number | null;
414
+ /** LIVE spend at the moment the cap fired, when the caller knows it. It selects the THIRD arm below:
415
+ * a cap that fires while the token ceiling is ALREADY overshot must not send the reader off to raise
416
+ * `maxAgents` (raising it buys more overshoot, not less). Absent ⇒ the two historic arms only. */
417
+ readonly spentTokens?: number | undefined;
375
418
  readonly code = "workflow.max_agents";
376
419
  constructor(max: number,
377
420
  /** The run's token ceiling AT THE MOMENT THE CAP FIRED, or `null` when the run set no budget. It selects
378
421
  * which cause the message may name — the two are mutually exclusive facts, not one fixed label. */
379
- budgetTotal?: number | null);
422
+ budgetTotal?: number | null,
423
+ /** LIVE spend at the moment the cap fired, when the caller knows it. It selects the THIRD arm below:
424
+ * a cap that fires while the token ceiling is ALREADY overshot must not send the reader off to raise
425
+ * `maxAgents` (raising it buys more overshoot, not less). Absent ⇒ the two historic arms only. */
426
+ spentTokens?: number | undefined);
380
427
  }
@@ -58,17 +58,25 @@ export class WorkflowAgentBlockedError extends Error {
58
58
  export class WorkflowMaxAgentsError extends Error {
59
59
  max;
60
60
  budgetTotal;
61
+ spentTokens;
61
62
  code = "workflow.max_agents";
62
- constructor(max, budgetTotal = null) {
63
+ constructor(max, budgetTotal = null, spentTokens) {
63
64
  super(budgetTotal === null
64
65
  ? `Workflow agent() call cap reached (${max}). This usually means a loop using budget.remaining() never ` +
65
66
  `terminates because no token budget was set — remaining() returns Infinity when budget.total is null. ` +
66
67
  `Add a hard iteration cap to the loop, or pass a token budget.`
67
- : `Workflow agent() call cap reached (${max}). A token budget IS set (${budgetTotal.toLocaleString()} output tokens), ` +
68
- `so this is the CALL-COUNT cap, not the token ceiling: the script asked for more than ${max} agent() calls. ` +
69
- `Fan out over fewer items, or raise maxAgents.`);
68
+ : spentTokens !== undefined && spentTokens > budgetTotal
69
+ ? `Workflow agent() call cap reached (${max}), and the token budget is ALREADY EXCEEDED ` +
70
+ `(${spentTokens.toLocaleString()} spent / ${budgetTotal.toLocaleString()} output tokens): agents already in flight when ` +
71
+ `the ceiling was reached are not bound by the per-call gate, so their spend landed on top of it. BOTH bounds are ` +
72
+ `binding — raising maxAgents alone would only buy more overshoot. Fan out over fewer items, or lower concurrency ` +
73
+ `(it bounds the overshoot) and raise the token budget deliberately.`
74
+ : `Workflow agent() call cap reached (${max}). A token budget IS set (${budgetTotal.toLocaleString()} output tokens), ` +
75
+ `so this is the CALL-COUNT cap, not the token ceiling: the script asked for more than ${max} agent() calls. ` +
76
+ `Fan out over fewer items, or raise maxAgents.`);
70
77
  this.max = max;
71
78
  this.budgetTotal = budgetTotal;
79
+ this.spentTokens = spentTokens;
72
80
  this.name = "WorkflowMaxAgentsError";
73
81
  }
74
82
  }
@@ -201,8 +201,19 @@ export interface WorkflowAgentHandle {
201
201
  * live is parked and delivered into the NEXT turn's context (birth-window delivery, bounded — ledger item 36); steers are
202
202
  * delivered in call order, the birth window included. Rejects with `steering.not_running` once the task has
203
203
  * finished (teardown included).
204
+ *
205
+ * `opts.inputId` is a PASS-THROUGH of the underlying `TaskStream.steer` correlation/idempotency key
206
+ * (design/171 §6.3 — its whole contract, value domain and typed refusals are that verb's; absent ⇒ byte-identical
207
+ * to every pre-existing call). A launcher whose own ingress already minted a message id passes it here so the two
208
+ * legs of one steering ingress are equally replay-safe. ⚠️ ONE arm of that contract is NOT reachable through this
209
+ * handle, deliberately: the idempotent-REPLAY arm ("same id, same payload ⇒ injects nothing"). Each call mints a
210
+ * FRESH unguessable correlation marker into the framing it delivers, so a retry under the same id is a
211
+ * same-id-DIFFERENT-instruction call and refuses typed `steering.duplicate_input_id`. What the key buys on this
212
+ * lane is therefore AT-MOST-ONCE: a retried ingress is refused LOUDLY instead of injecting a second copy.
204
213
  */
205
- steer(content: string): Promise<string>;
214
+ steer(content: string, opts?: {
215
+ inputId?: string;
216
+ }): Promise<string>;
206
217
  /** Await the agent's {@link TaskResult} (the same value the eager recording used; idempotent). */
207
218
  result(): Promise<TaskResult>;
208
219
  }
@@ -384,8 +395,8 @@ export interface RunWorkflowOptions {
384
395
  parentCenterArtifactDigest?: string;
385
396
  parentCenterSourceRevision?: string;
386
397
  /** The launching run's display sink (`RunInternals.onForwardEvent` behind the runner's ctx wrapper:
387
- * `task_progress` always, PLUS the children's content events — `text_delta`/`reasoning_delta`/
388
- * `tool_start`/`tool_end`, UNTRUSTED-RAW: the consumer must redact — when the HOST spec set
398
+ * `task_progress` always, PLUS the children's content events — `text_delta`/`text_end`/
399
+ * `reasoning_delta`/`tool_start`/`tool_end`, UNTRUSTED-RAW: the consumer must redact — when the HOST spec set
389
400
  * `forwardSubagentEvents: true`) — threaded into every spawned agent's trusted internals so the
390
401
  * children's events bubble out of their isolated streams to the deployment's one sink (fleet
391
402
  * footer/monitor rows). Display-only; absent ⇒ ticks stay in each child's own stream. */