@sema-agent/core 2.13.0 → 4.0.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 (184) hide show
  1. package/CHANGELOG.md +2345 -0
  2. package/dist/agents/cascade.d.ts +6 -1
  3. package/dist/agents/cascade.js +76 -32
  4. package/dist/agents/cumulative-stats.d.ts +29 -0
  5. package/dist/agents/cumulative-stats.js +29 -0
  6. package/dist/agents/observer.d.ts +4 -0
  7. package/dist/agents/observer.js +11 -10
  8. package/dist/agents/repair-loop.d.ts +2 -0
  9. package/dist/agents/repair-loop.js +110 -3
  10. package/dist/agents/retain-ledger.d.ts +20 -2
  11. package/dist/agents/retain-ledger.js +38 -8
  12. package/dist/agents/roster-store.d.ts +4 -0
  13. package/dist/agents/roster-store.js +20 -1
  14. package/dist/agents/subagent.js +66 -64
  15. package/dist/agents/teacher.d.ts +1 -9
  16. package/dist/agents/teacher.js +23 -24
  17. package/dist/agents/team.js +7 -4
  18. package/dist/agents/verify.d.ts +15 -2
  19. package/dist/agents/verify.js +39 -3
  20. package/dist/bin/sema-tb.js +2 -2
  21. package/dist/brain/anthropic.js +24 -21
  22. package/dist/brain/circuit-breaker.d.ts +1 -0
  23. package/dist/brain/circuit-breaker.js +26 -9
  24. package/dist/brain/degrading.js +13 -1
  25. package/dist/brain/failover.js +2 -0
  26. package/dist/brain/openai.js +24 -21
  27. package/dist/brain/repetition.js +10 -1
  28. package/dist/brain/routing.js +5 -1
  29. package/dist/brain/status-sink.js +5 -1
  30. package/dist/brain/stream-engine.js +5 -2
  31. package/dist/brain/stream-shared.d.ts +1 -0
  32. package/dist/brain/stream-shared.js +14 -0
  33. package/dist/brain/terminal-cause.d.ts +4 -0
  34. package/dist/brain/terminal-cause.js +12 -0
  35. package/dist/brain/tool-call-id.d.ts +1 -0
  36. package/dist/brain/tool-call-id.js +3 -0
  37. package/dist/brain/tool-call-repair.js +34 -15
  38. package/dist/core/auto-compaction.js +33 -32
  39. package/dist/core/background-agent-store.d.ts +22 -22
  40. package/dist/core/background-agent-store.js +19 -23
  41. package/dist/core/checkpoint-store.d.ts +7 -13
  42. package/dist/core/checkpoint-store.js +39 -15
  43. package/dist/core/consolidate-scope.js +4 -3
  44. package/dist/core/context-edit.js +3 -0
  45. package/dist/core/context-guard.js +3 -0
  46. package/dist/core/file-snapshot-store.d.ts +8 -0
  47. package/dist/core/file-snapshot-store.js +4 -3
  48. package/dist/core/git-worktree-env.d.ts +6 -1
  49. package/dist/core/git-worktree-env.js +12 -1
  50. package/dist/core/hooks.d.ts +4 -1
  51. package/dist/core/hooks.js +35 -7
  52. package/dist/core/lsp-diagnostics.js +13 -7
  53. package/dist/core/lsp-protocol.d.ts +1 -1
  54. package/dist/core/lsp-protocol.js +20 -8
  55. package/dist/core/lsp-session.d.ts +12 -2
  56. package/dist/core/lsp-session.js +114 -49
  57. package/dist/core/lsp.d.ts +13 -1
  58. package/dist/core/lsp.js +65 -9
  59. package/dist/core/mailbox-store.d.ts +7 -10
  60. package/dist/core/mcp.d.ts +7 -10
  61. package/dist/core/mcp.js +7 -3
  62. package/dist/core/memory-engine/data-plane.js +4 -0
  63. package/dist/core/memory-engine/engine.d.ts +2 -2
  64. package/dist/core/memory-engine/engine.js +96 -31
  65. package/dist/core/memory-engine/file-backend.d.ts +2 -2
  66. package/dist/core/memory-engine/file-backend.js +38 -39
  67. package/dist/core/memory-engine/layout.d.ts +8 -0
  68. package/dist/core/memory-engine/layout.js +71 -4
  69. package/dist/core/memory-engine/scope-contract.js +9 -3
  70. package/dist/core/memory-engine/types.d.ts +1 -1
  71. package/dist/core/memory.js +3 -0
  72. package/dist/core/permission-rules.d.ts +2 -1
  73. package/dist/core/permission-rules.js +24 -3
  74. package/dist/core/runner/active-skill-scope.d.ts +0 -1
  75. package/dist/core/runner/active-skill-scope.js +1 -15
  76. package/dist/core/runner/assemble-result.d.ts +3 -8
  77. package/dist/core/runner/assemble-result.js +3 -5
  78. package/dist/core/runner/compaction-call-options.d.ts +7 -0
  79. package/dist/core/runner/compaction-call-options.js +30 -0
  80. package/dist/core/runner/prepare-task.d.ts +6 -23
  81. package/dist/core/runner/prepare-task.js +31 -53
  82. package/dist/core/runner/runtask.d.ts +1 -0
  83. package/dist/core/runner/runtask.js +86 -178
  84. package/dist/core/runner/session-rule-policy.js +4 -1
  85. package/dist/core/runner/teardown-bounded.d.ts +7 -0
  86. package/dist/core/runner/teardown-bounded.js +36 -0
  87. package/dist/core/runner/tool-disclosure.d.ts +2 -5
  88. package/dist/core/runner/turn-attachments.d.ts +5 -15
  89. package/dist/core/runner/turn-attachments.js +1 -1
  90. package/dist/core/runner/usage-accounting.d.ts +6 -0
  91. package/dist/core/runner/usage-accounting.js +9 -8
  92. package/dist/core/safe-notify.d.ts +17 -0
  93. package/dist/core/safe-notify.js +59 -0
  94. package/dist/core/secret-env.js +5 -1
  95. package/dist/core/sensitive-path-policy.js +11 -4
  96. package/dist/core/store-contracts/checkpoint-store-contract.js +1 -1
  97. package/dist/core/store-contracts/session-repo-contract.js +40 -0
  98. package/dist/core/stub-env.d.ts +2 -6
  99. package/dist/core/surrogate-safe-slice.d.ts +4 -0
  100. package/dist/core/surrogate-safe-slice.js +18 -0
  101. package/dist/core/task-registry-monitor.js +15 -14
  102. package/dist/core/task-registry-shared.d.ts +22 -0
  103. package/dist/core/task-registry-shared.js +16 -0
  104. package/dist/core/task-registry.d.ts +1 -0
  105. package/dist/core/task-registry.js +40 -57
  106. package/dist/core/tool-errors.js +5 -4
  107. package/dist/core/tool-name-aliases.js +2 -1
  108. package/dist/core/tool-policy.js +72 -19
  109. package/dist/core/tool-result-store.d.ts +1 -0
  110. package/dist/core/tool-result-store.js +17 -1
  111. package/dist/core/trace.d.ts +3 -9
  112. package/dist/core/types.d.ts +16 -12
  113. package/dist/core/untrusted-egress.js +7 -5
  114. package/dist/core/workflow-journal-store.d.ts +9 -20
  115. package/dist/core/workflow-run-store.d.ts +11 -10
  116. package/dist/core/workflow-run-store.js +22 -16
  117. package/dist/engine/compaction/compaction.d.ts +1 -0
  118. package/dist/engine/compaction/compaction.js +14 -5
  119. package/dist/engine/compaction/utils.js +4 -2
  120. package/dist/engine/execution-env/node-execution-env.d.ts +2 -24
  121. package/dist/engine/harness/agent-harness.d.ts +1 -0
  122. package/dist/engine/harness/agent-harness.js +13 -33
  123. package/dist/engine/harness/types.d.ts +29 -67
  124. package/dist/engine/llm/types.d.ts +1 -1
  125. package/dist/engine/loop/agent-loop.js +11 -4
  126. package/dist/engine/loop/types.d.ts +11 -9
  127. package/dist/engine/lsp/frame-decoder.js +6 -3
  128. package/dist/engine/lsp/node-lsp-manager.d.ts +2 -0
  129. package/dist/engine/lsp/node-lsp-manager.js +103 -26
  130. package/dist/engine/lsp/stdio-lsp-transport.js +16 -7
  131. package/dist/engine/session/memory-repo.d.ts +2 -6
  132. package/dist/engine/session/session.d.ts +5 -30
  133. package/dist/index.d.ts +3 -1
  134. package/dist/index.js +2 -1
  135. package/dist/internal/harness-types.d.ts +3 -1
  136. package/dist/internal/harness.d.ts +1 -1
  137. package/dist/internal/harness.js +1 -1
  138. package/dist/orchestration/run-spec.js +7 -4
  139. package/dist/orchestration/run-workflow-tool.js +3 -35
  140. package/dist/orchestration/workflow-governance.js +43 -9
  141. package/dist/orchestration/workflow-sandbox-conformance.js +85 -51
  142. package/dist/orchestration/workflow-script-store.js +34 -4
  143. package/dist/orchestration/workflow.d.ts +3 -11
  144. package/dist/orchestration/workflow.js +164 -204
  145. package/dist/prompt-assembly/artifact-store.d.ts +2 -0
  146. package/dist/prompt-assembly/artifact-store.js +39 -24
  147. package/dist/prompt-assembly/assemble.js +38 -7
  148. package/dist/prompt-assembly/epoch.js +19 -2
  149. package/dist/prompt-assembly/event-registry.js +3 -3
  150. package/dist/prompt-assembly/explain.js +1 -1
  151. package/dist/prompt-assembly/tool-catalog.d.ts +10 -9
  152. package/dist/prompt-assembly/tool-catalog.js +16 -1
  153. package/dist/prompt-assembly/turn-snapshot.d.ts +2 -5
  154. package/dist/prompt-assembly/turn-snapshot.js +4 -1
  155. package/dist/stores/file/background-agent-store.d.ts +3 -12
  156. package/dist/stores/file/background-agent-store.js +3 -41
  157. package/dist/stores/file/checkpoint-store.js +6 -6
  158. package/dist/stores/file/fs-atomic.d.ts +3 -0
  159. package/dist/stores/file/fs-atomic.js +53 -8
  160. package/dist/stores/file/index.d.ts +1 -0
  161. package/dist/stores/file/index.js +7 -0
  162. package/dist/stores/file/mailbox-store.d.ts +2 -6
  163. package/dist/stores/file/session-policy-store.js +15 -1
  164. package/dist/stores/file/session-store.d.ts +4 -6
  165. package/dist/stores/file/session-store.js +28 -1
  166. package/dist/stores/file/workflow-journal-store.d.ts +3 -11
  167. package/dist/stores/file/workflow-run-store.d.ts +3 -7
  168. package/dist/stores/file/workflow-run-store.js +4 -17
  169. package/dist/tools/fs/bash-readonly-classifier.d.ts +2 -0
  170. package/dist/tools/fs/bash-readonly-classifier.js +73 -21
  171. package/dist/tools/fs/fs-bash.d.ts +1 -1
  172. package/dist/tools/fs/fs-bash.js +407 -91
  173. package/dist/tools/fs/fs-pdf.d.ts +2 -12
  174. package/dist/tools/fs/fs-shared.js +2 -2
  175. package/dist/tools/fs/notebook.d.ts +2 -5
  176. package/dist/tools/fs/safety.d.ts +3 -0
  177. package/dist/tools/fs/safety.js +43 -14
  178. package/dist/tools/fs/search.js +5 -5
  179. package/dist/tools/loop-tick.js +1 -1
  180. package/dist/tools/monitor.d.ts +1 -0
  181. package/dist/tools/monitor.js +57 -9
  182. package/dist/tools/worktree.d.ts +4 -7
  183. package/dist/tools/worktree.js +235 -143
  184. package/package.json +9 -2
@@ -14,6 +14,7 @@ export interface CascadeConfig {
14
14
  index: number;
15
15
  model: ModelRef;
16
16
  }) => GateVerdict | Promise<GateVerdict>;
17
+ acceptPartial?: boolean;
17
18
  maxEscalations?: number;
18
19
  costCeilingMicroUsd?: number;
19
20
  totalTimeoutMs?: number;
@@ -28,14 +29,18 @@ export interface CascadeAttempt {
28
29
  index: number;
29
30
  model: ModelRef;
30
31
  passed: boolean;
31
- costMicroUsd: number;
32
+ costMicroUsd?: number;
32
33
  status: TaskResult["status"];
33
34
  errorCode?: string;
34
35
  gateError?: string;
36
+ timedOut?: true;
37
+ cancelled?: true;
38
+ statusRejected?: true;
35
39
  diagnostics?: string;
36
40
  }
37
41
  export interface CascadeRunResult extends TaskResult {
38
42
  cascadeOutcome: "passed" | "exhausted";
43
+ costCeilingUnenforceable?: true;
39
44
  escalated: boolean;
40
45
  finalRung: number;
41
46
  attempts: CascadeAttempt[];
@@ -1,5 +1,8 @@
1
1
  import { releaseSession } from "./session-util.js";
2
2
  import { mapNestedSuspend, isDurablePause } from "./suspend-guard.js";
3
+ import { buildCumulativeStats } from "./cumulative-stats.js";
4
+ import { createSafeNotifier } from "../core/safe-notify.js";
5
+ const CASCADE_ON_RUNG_SITE = "cascade.onRung";
3
6
  function createDefaultGate(spec) {
4
7
  const requiresStructured = spec.outputSchema != null;
5
8
  return (result) => result.status === "completed" && (!requiresStructured || result.structuredOutput !== undefined);
@@ -13,61 +16,86 @@ export async function runCascade(runner, spec, config) {
13
16
  const maxRungs = Math.min(ladder.length, Math.max(0, maxEscalations) + 1);
14
17
  const gate = config.gate ?? createDefaultGate(spec);
15
18
  const startedAt = Date.now();
19
+ const deadlineAt = config.totalTimeoutMs != null ? startedAt + config.totalTimeoutMs : undefined;
20
+ const notifier = createSafeNotifier();
16
21
  const attempts = [];
17
22
  let totalCost = 0;
23
+ let ceilingCostKnown = true;
18
24
  let ownTokens = 0, ownTurns = 0, ownPrompt = 0, ownCached = 0, ownOutput = 0, ownCostMicro = 0;
25
+ let ownTotalInput = 0;
19
26
  let ownCacheWrite = 0, ownCacheWriteLong = 0;
20
27
  let nestedTokens = 0, nestedTurns = 0, nestedTasks = 0, nestedCostMicro = 0;
21
28
  let ownLlmRootMicro = 0, ownCompactionMicro = 0;
29
+ let anyRungUnpriced = false;
22
30
  let hrCount = 0, hrWaitMs = 0;
23
31
  const hrGates = [];
24
- const cumulativeStats = (baseStats) => ({
25
- ...baseStats,
32
+ const cumulativeStats = (baseStats) => buildCumulativeStats(baseStats, {
26
33
  tokens: ownTokens,
27
34
  turns: ownTurns,
28
- promptTokens: ownPrompt || undefined,
29
- totalInputTokens: ownPrompt || undefined,
30
- cachedTokens: ownCached || undefined,
31
- outputTokens: ownOutput || undefined,
32
- cacheWriteTokens: ownCacheWrite || undefined,
33
- cacheWriteTokensLong: ownCacheWriteLong || undefined,
34
- cacheHitRate: ownPrompt > 0 ? ownCached / ownPrompt : undefined,
35
+ promptTokens: ownPrompt,
36
+ totalInputTokens: ownTotalInput,
37
+ cachedTokens: ownCached,
38
+ outputTokens: ownOutput,
39
+ cacheWriteTokens: ownCacheWrite,
40
+ cacheWriteTokensLong: ownCacheWriteLong,
35
41
  costMicroUsd: ownCostMicro,
36
- nested: nestedTokens > 0 || nestedTasks > 0
37
- ? { tokens: nestedTokens, turns: nestedTurns, tasks: nestedTasks, costMicroUsd: nestedCostMicro || undefined }
38
- : undefined,
39
- costBreakdown: {
40
- llmRootMicroUsd: ownLlmRootMicro,
41
- nestedSubagentMicroUsd: nestedCostMicro,
42
- memoryConsolidationMicroUsd: 0,
43
- compactionMicroUsd: ownCompactionMicro,
44
- },
45
- humanReview: hrCount > 0 ? { count: hrCount, totalWaitMs: hrWaitMs, gates: hrGates } : undefined,
42
+ anyLegUnpriced: anyRungUnpriced,
43
+ llmRootMicroUsd: ownLlmRootMicro,
44
+ compactionMicroUsd: ownCompactionMicro,
45
+ nested: { tokens: nestedTokens, turns: nestedTurns, tasks: nestedTasks, costMicroUsd: nestedCostMicro },
46
+ humanReview: { count: hrCount, totalWaitMs: hrWaitMs, gates: hrGates },
46
47
  });
47
48
  let lastResult;
48
49
  let passedRung = -1;
49
50
  const { sessionId: _drop, ...specBase } = spec;
50
51
  for (let i = 0; i < maxRungs; i++) {
51
- if (i > 0) {
52
- if (config.totalTimeoutMs != null && Date.now() - startedAt >= config.totalTimeoutMs)
53
- break;
54
- if (config.costCeilingMicroUsd != null && totalCost >= config.costCeilingMicroUsd)
55
- break;
52
+ if (deadlineAt !== undefined && Date.now() >= deadlineAt)
53
+ break;
54
+ if (i > 0 && config.costCeilingMicroUsd != null && (totalCost >= config.costCeilingMicroUsd || !ceilingCostKnown)) {
55
+ break;
56
56
  }
57
57
  const rung = ladder[i];
58
- const result = await runner.runTask({ ...specBase, model: rung.model, ...(rung.overrides ?? {}) });
58
+ let deadlineFired = false;
59
+ const rungAbort = new AbortController();
60
+ let deadlineTimer;
61
+ if (deadlineAt !== undefined) {
62
+ deadlineTimer = setTimeout(() => {
63
+ deadlineFired = true;
64
+ rungAbort.abort(new Error(`runCascade: totalTimeoutMs (${config.totalTimeoutMs}ms) reached`));
65
+ }, Math.max(0, deadlineAt - Date.now()));
66
+ deadlineTimer.unref?.();
67
+ }
68
+ const rungSignal = deadlineAt === undefined
69
+ ? specBase.signal
70
+ : specBase.signal
71
+ ? AbortSignal.any([specBase.signal, rungAbort.signal])
72
+ : rungAbort.signal;
73
+ let result;
74
+ try {
75
+ result = await runner.runTask({ ...specBase, model: rung.model, ...(rung.overrides ?? {}), ...(rungSignal ? { signal: rungSignal } : {}) });
76
+ }
77
+ finally {
78
+ if (deadlineTimer !== undefined)
79
+ clearTimeout(deadlineTimer);
80
+ }
59
81
  lastResult = result;
60
82
  const s = result.stats;
61
- const rungCost = (s.costMicroUsd ?? 0) + (s.nested?.costMicroUsd ?? 0);
62
- totalCost += rungCost;
83
+ const rungCostKnown = s.costMicroUsd !== undefined && (s.nested === undefined || s.nested.costMicroUsd !== undefined);
84
+ const rungCost = rungCostKnown ? (s.costMicroUsd ?? 0) + (s.nested?.costMicroUsd ?? 0) : undefined;
85
+ totalCost += rungCost ?? 0;
86
+ if (!rungCostKnown)
87
+ ceilingCostKnown = false;
63
88
  ownTokens += s.tokens;
64
89
  ownTurns += s.turns;
65
90
  ownPrompt += s.promptTokens ?? 0;
91
+ ownTotalInput += s.totalInputTokens ?? 0;
66
92
  ownCached += s.cachedTokens ?? 0;
67
93
  ownOutput += s.outputTokens ?? 0;
68
94
  ownCacheWrite += s.cacheWriteTokens ?? 0;
69
95
  ownCacheWriteLong += s.cacheWriteTokensLong ?? 0;
70
96
  ownCostMicro += s.costMicroUsd ?? 0;
97
+ if (s.costMicroUsd === undefined)
98
+ anyRungUnpriced = true;
71
99
  if (s.costBreakdown) {
72
100
  ownLlmRootMicro += s.costBreakdown.llmRootMicroUsd;
73
101
  ownCompactionMicro += s.costBreakdown.compactionMicroUsd;
@@ -90,11 +118,26 @@ export async function runCascade(runner, spec, config) {
90
118
  ...mapped,
91
119
  stats: cumulativeStats(result.stats),
92
120
  cascadeOutcome: "exhausted",
121
+ ...(config.costCeilingMicroUsd != null && !ceilingCostKnown ? { costCeilingUnenforceable: true } : {}),
93
122
  escalated: i > 0,
94
123
  finalRung: i,
95
124
  attempts,
96
125
  };
97
126
  }
127
+ if (deadlineFired || specBase.signal?.aborted === true) {
128
+ attempts.push({
129
+ index: i,
130
+ model: rung.model,
131
+ passed: false,
132
+ costMicroUsd: rungCost,
133
+ status: result.status,
134
+ errorCode: result.errorCode,
135
+ ...(deadlineFired ? { timedOut: true } : { cancelled: true }),
136
+ });
137
+ notifier.notify(() => config.onRung?.({ index: i, model: rung.model, passed: false, result }), CASCADE_ON_RUNG_SITE);
138
+ await releaseSession(runner, result.sessionId);
139
+ break;
140
+ }
98
141
  let passed = false;
99
142
  let gateError;
100
143
  let diagnostics;
@@ -112,6 +155,9 @@ export async function runCascade(runner, spec, config) {
112
155
  passed = false;
113
156
  gateError = e instanceof Error ? e.message : String(e);
114
157
  }
158
+ const statusRejected = passed && config.acceptPartial !== true && result.status !== "completed";
159
+ if (statusRejected)
160
+ passed = false;
115
161
  attempts.push({
116
162
  index: i,
117
163
  model: rung.model,
@@ -121,12 +167,9 @@ export async function runCascade(runner, spec, config) {
121
167
  errorCode: result.errorCode,
122
168
  gateError,
123
169
  diagnostics,
170
+ ...(statusRejected ? { statusRejected: true } : {}),
124
171
  });
125
- try {
126
- config.onRung?.({ index: i, model: rung.model, passed, result });
127
- }
128
- catch {
129
- }
172
+ notifier.notify(() => config.onRung?.({ index: i, model: rung.model, passed, result }), CASCADE_ON_RUNG_SITE);
130
173
  }
131
174
  finally {
132
175
  await releaseSession(runner, result.sessionId);
@@ -142,6 +185,7 @@ export async function runCascade(runner, spec, config) {
142
185
  ...base,
143
186
  stats: cumulativeStats(base.stats),
144
187
  cascadeOutcome: passedRung >= 0 ? "passed" : "exhausted",
188
+ ...(config.costCeilingMicroUsd != null && !ceilingCostKnown ? { costCeilingUnenforceable: true } : {}),
145
189
  escalated: finalRung > 0,
146
190
  finalRung,
147
191
  attempts,
@@ -0,0 +1,29 @@
1
+ import type { TaskResult } from "../core/types.js";
2
+ export interface CumulativeNestedTotals {
3
+ tokens: number;
4
+ turns: number;
5
+ tasks: number;
6
+ costMicroUsd: number;
7
+ }
8
+ export interface CumulativeHumanReviewTotals {
9
+ count: number;
10
+ totalWaitMs: number;
11
+ gates: NonNullable<TaskResult["stats"]["humanReview"]>["gates"];
12
+ }
13
+ export interface CumulativeStatsAccumulator {
14
+ tokens: number;
15
+ turns: number;
16
+ promptTokens: number;
17
+ totalInputTokens: number;
18
+ cachedTokens: number;
19
+ outputTokens: number;
20
+ cacheWriteTokens: number;
21
+ cacheWriteTokensLong: number;
22
+ costMicroUsd: number | undefined;
23
+ anyLegUnpriced: boolean;
24
+ llmRootMicroUsd: number;
25
+ compactionMicroUsd: number;
26
+ nested: CumulativeNestedTotals;
27
+ humanReview: CumulativeHumanReviewTotals;
28
+ }
29
+ export declare function buildCumulativeStats(base: TaskResult["stats"], acc: CumulativeStatsAccumulator): TaskResult["stats"];
@@ -0,0 +1,29 @@
1
+ export function buildCumulativeStats(base, acc) {
2
+ return {
3
+ ...base,
4
+ tokens: acc.tokens,
5
+ turns: acc.turns,
6
+ promptTokens: acc.promptTokens || undefined,
7
+ totalInputTokens: acc.totalInputTokens || undefined,
8
+ cachedTokens: acc.cachedTokens || undefined,
9
+ outputTokens: acc.outputTokens || undefined,
10
+ cacheWriteTokens: acc.cacheWriteTokens || undefined,
11
+ cacheWriteTokensLong: acc.cacheWriteTokensLong || undefined,
12
+ cacheHitRate: acc.totalInputTokens > 0 ? Math.min(1, acc.cachedTokens / acc.totalInputTokens) : undefined,
13
+ costMicroUsd: acc.anyLegUnpriced ? undefined : acc.costMicroUsd,
14
+ nested: acc.nested.tokens > 0 || acc.nested.tasks > 0
15
+ ? { tokens: acc.nested.tokens, turns: acc.nested.turns, tasks: acc.nested.tasks, costMicroUsd: acc.nested.costMicroUsd || undefined }
16
+ : undefined,
17
+ costBreakdown: acc.anyLegUnpriced
18
+ ? undefined
19
+ : {
20
+ llmRootMicroUsd: acc.llmRootMicroUsd,
21
+ nestedSubagentMicroUsd: acc.nested.costMicroUsd,
22
+ memoryConsolidationMicroUsd: 0,
23
+ compactionMicroUsd: acc.compactionMicroUsd,
24
+ },
25
+ humanReview: acc.humanReview.count > 0
26
+ ? { count: acc.humanReview.count, totalWaitMs: acc.humanReview.totalWaitMs, gates: acc.humanReview.gates }
27
+ : undefined,
28
+ };
29
+ }
@@ -63,7 +63,9 @@ export declare class ObserverDigestTap {
63
63
  constructor(flush: (activity: ObserverDigestEvent[]) => void);
64
64
  record(e: TaskEvent): void;
65
65
  private flushText;
66
+ private readonly notifier;
66
67
  private safeFlush;
68
+ get flushFailures(): number;
67
69
  flushSegment(): void;
68
70
  finish(reason: string): void;
69
71
  }
@@ -112,7 +114,9 @@ export declare class ObserverPairing {
112
114
  });
113
115
  enqueueSegment(activity: ObserverDigestEvent[], trigger?: string): void;
114
116
  private pumping?;
117
+ private readonly notifier;
115
118
  private safeOnError;
119
+ get onErrorFailures(): number;
116
120
  drain(): Promise<void>;
117
121
  private pump;
118
122
  private deliverBatch;
@@ -1,5 +1,6 @@
1
1
  import { Type } from "typebox";
2
2
  import { defineTool } from "../core/tools.js";
3
+ import { createSafeNotifier } from "../core/safe-notify.js";
3
4
  export const OBSERVER_EVENT_TAGS = ["tool-call", "user-message", "tool-result", "turn-ended"];
4
5
  const TAG_ESCAPE_RE = new RegExp(`<(?=/?(?:${OBSERVER_EVENT_TAGS.join("|")})(?:[>\\s/]|$))`, "gi");
5
6
  export function escapeObserverTags(text) {
@@ -181,12 +182,12 @@ export class ObserverDigestTap {
181
182
  this.pending.push({ type: "assistant_text", text: this.textBuf });
182
183
  this.textBuf = "";
183
184
  }
185
+ notifier = createSafeNotifier();
184
186
  safeFlush(slice) {
185
- try {
186
- this.flush(slice);
187
- }
188
- catch {
189
- }
187
+ this.notifier.notify(() => this.flush(slice), "observer.digestTap.flush");
188
+ }
189
+ get flushFailures() {
190
+ return this.notifier.failuresAt("observer.digestTap.flush");
190
191
  }
191
192
  flushSegment() {
192
193
  if (this.finished)
@@ -270,12 +271,12 @@ export class ObserverPairing {
270
271
  }).catch(() => undefined);
271
272
  }
272
273
  pumping;
274
+ notifier = createSafeNotifier();
273
275
  safeOnError(err) {
274
- try {
275
- this.onError?.(err);
276
- }
277
- catch {
278
- }
276
+ this.notifier.notify(() => this.onError?.(err), "observer.pairing.onError");
277
+ }
278
+ get onErrorFailures() {
279
+ return this.notifier.failuresAt("observer.pairing.onError");
279
280
  }
280
281
  async drain() {
281
282
  while (this.pumping) {
@@ -12,6 +12,7 @@ export interface OracleResult {
12
12
  trace?: string;
13
13
  flaky: boolean;
14
14
  retries: number;
15
+ costMicroUsd?: number;
15
16
  }
16
17
  export type RepairOracle = (graderEnv: ExecutionEnv, evidence: string | undefined) => Promise<OracleResult>;
17
18
  export interface RepairBundle {
@@ -41,6 +42,7 @@ export interface RepairResult extends VerificationResult {
41
42
  terminal: RepairTerminal;
42
43
  bundle: RepairBundle;
43
44
  isolationClass?: OracleIsolationVerdict["isolationClass"];
45
+ oracleCostMicroUsd: number;
44
46
  }
45
47
  export declare function isolationPermitsAutoAccept(verdict: OracleIsolationVerdict): boolean;
46
48
  export declare function repairBundleFromCheckpoint(cp: Checkpoint): RepairBundle | undefined;
@@ -1,6 +1,7 @@
1
1
  import { isDurablePause } from "./suspend-guard.js";
2
2
  import { delimitUntrusted, sanitizeUntrustedText } from "../core/untrusted-text.js";
3
3
  import { assertOracleIsolation } from "../core/oracle-isolation.js";
4
+ import { buildCumulativeStats } from "./cumulative-stats.js";
4
5
  export function isolationPermitsAutoAccept(verdict) {
5
6
  return verdict.protected && verdict.isolationClass === "out_of_process";
6
7
  }
@@ -55,6 +56,7 @@ export async function runRepairLoop(runner, implSpec, config) {
55
56
  terminal: "oracle.unprotected",
56
57
  ...(isolationClass ? { isolationClass } : {}),
57
58
  bundle: { ...freshBundle(), attemptCount: config.resumeBundle?.attemptCount ?? 0 },
59
+ oracleCostMicroUsd: 0,
58
60
  verification: outcomeFor("oracle.unprotected", oracle, 0),
59
61
  };
60
62
  };
@@ -77,7 +79,10 @@ export async function runRepairLoop(runner, implSpec, config) {
77
79
  else if (config.workerEnv !== undefined && config.workerEnv === config.graderEnv) {
78
80
  return unprotectedResult("graderEnv must not be the worker env");
79
81
  }
80
- const maxAttempts = Math.max(1, Math.floor(config.maxAttempts));
82
+ const maxAttempts = Number.isFinite(config.maxAttempts) ? Math.max(1, Math.floor(config.maxAttempts)) : 1;
83
+ if (!Number.isFinite(config.maxAttempts)) {
84
+ config.onWarn?.(new Error(`repair-loop: maxAttempts must be a finite number (got ${config.maxAttempts}) — falling back to 1 attempt`));
85
+ }
81
86
  const bundle = config.resumeBundle
82
87
  ? { ...config.resumeBundle, diagnostics: [...config.resumeBundle.diagnostics], rejectedHypotheses: [...config.resumeBundle.rejectedHypotheses] }
83
88
  : freshBundle();
@@ -85,34 +90,126 @@ export async function runRepairLoop(runner, implSpec, config) {
85
90
  let spend = 0;
86
91
  let lastResult;
87
92
  let lastOracle;
93
+ let ownTokens = 0, ownTurns = 0, ownPrompt = 0, ownTotalInput = 0, ownCached = 0, ownOutput = 0;
94
+ let ownCacheWrite = 0, ownCacheWriteLong = 0, ownCostMicro = 0;
95
+ let ownLlmRootMicro = 0, ownCompactionMicro = 0;
96
+ let anyAttemptUnpriced = false;
97
+ let nestedTokens = 0, nestedTurns = 0, nestedTasks = 0, nestedCostMicro = 0;
98
+ let hrCount = 0, hrWaitMs = 0;
99
+ const hrGates = [];
100
+ let oracleCostMicroUsd = 0;
101
+ const accumulate = (s) => {
102
+ ownTokens += s.tokens;
103
+ ownTurns += s.turns;
104
+ ownPrompt += s.promptTokens ?? 0;
105
+ ownTotalInput += s.totalInputTokens ?? 0;
106
+ ownCached += s.cachedTokens ?? 0;
107
+ ownOutput += s.outputTokens ?? 0;
108
+ ownCacheWrite += s.cacheWriteTokens ?? 0;
109
+ ownCacheWriteLong += s.cacheWriteTokensLong ?? 0;
110
+ ownCostMicro += s.costMicroUsd ?? 0;
111
+ if (s.costMicroUsd === undefined)
112
+ anyAttemptUnpriced = true;
113
+ if (s.costBreakdown) {
114
+ ownLlmRootMicro += s.costBreakdown.llmRootMicroUsd;
115
+ ownCompactionMicro += s.costBreakdown.compactionMicroUsd;
116
+ }
117
+ if (s.humanReview) {
118
+ hrCount += s.humanReview.count;
119
+ hrWaitMs += s.humanReview.totalWaitMs;
120
+ hrGates.push(...s.humanReview.gates);
121
+ }
122
+ if (s.nested) {
123
+ nestedTokens += s.nested.tokens;
124
+ nestedTurns += s.nested.turns;
125
+ nestedTasks += s.nested.tasks;
126
+ nestedCostMicro += s.nested.costMicroUsd ?? 0;
127
+ }
128
+ };
129
+ const cumulativeStats = (base) => buildCumulativeStats(base, {
130
+ tokens: ownTokens,
131
+ turns: ownTurns,
132
+ promptTokens: ownPrompt,
133
+ totalInputTokens: ownTotalInput,
134
+ cachedTokens: ownCached,
135
+ outputTokens: ownOutput,
136
+ cacheWriteTokens: ownCacheWrite,
137
+ cacheWriteTokensLong: ownCacheWriteLong,
138
+ costMicroUsd: ownCostMicro,
139
+ anyLegUnpriced: anyAttemptUnpriced,
140
+ llmRootMicroUsd: ownLlmRootMicro,
141
+ compactionMicroUsd: ownCompactionMicro,
142
+ nested: { tokens: nestedTokens, turns: nestedTurns, tasks: nestedTasks, costMicroUsd: nestedCostMicro },
143
+ humanReview: { count: hrCount, totalWaitMs: hrWaitMs, gates: hrGates },
144
+ });
88
145
  let restartedOnce = false;
89
146
  let restartUsed = false;
90
147
  const cleanRestartSpec = () => {
91
148
  const { sessionId: _drop, ...rest } = implSpec;
92
149
  return { ...rest, objective: `${implSpec.objective}\n\n${repairObjective(bundle)}` };
93
150
  };
151
+ const exhaustedBeforeDispatch = () => ({
152
+ taskId: implSpec.taskId ?? "",
153
+ status: "failed",
154
+ result: `repair loop: attempt budget already spent (attemptCount ${bundle.attemptCount} >= maxAttempts ${maxAttempts}) — no attempt dispatched`,
155
+ errorCode: "repair.attempts_exhausted",
156
+ sessionId: implSpec.sessionId ?? "",
157
+ stats: { tokens: 0, turns: 0, costMicroUsd: 0 },
158
+ terminal: "gave_up",
159
+ bundle,
160
+ ...(isolationClass ? { isolationClass } : {}),
161
+ oracleCostMicroUsd,
162
+ verification: outcomeFor("gave_up", undefined, bundle.attemptCount),
163
+ });
94
164
  while (true) {
165
+ if (!restartedOnce && bundle.attemptCount >= maxAttempts)
166
+ return exhaustedBeforeDispatch();
95
167
  const isRepair = bundle.attemptCount > 0 && lastResult !== undefined && !restartedOnce;
168
+ const isSeededResume = !restartedOnce &&
169
+ lastResult === undefined &&
170
+ config.resumeBundle !== undefined &&
171
+ (bundle.failureTrace.trim() !== "" || bundle.diagnostics.length > 0);
96
172
  const spec = restartedOnce
97
173
  ? cleanRestartSpec()
98
174
  : isRepair
99
175
  ? { ...implSpec, sessionId: lastResult.sessionId, objective: repairObjective(bundle) }
100
- : implSpec;
176
+ : isSeededResume
177
+ ? {
178
+ ...implSpec,
179
+ objective: implSpec.sessionId ? repairObjective(bundle) : `${implSpec.objective}\n\n${repairObjective(bundle)}`,
180
+ }
181
+ : implSpec;
101
182
  restartedOnce = false;
102
183
  const result = await runner.runTask(spec, { repairBundle: bundle });
103
184
  spend += (result.stats.costMicroUsd ?? 0) + (result.stats.nested?.costMicroUsd ?? 0);
185
+ accumulate(result.stats);
104
186
  lastResult = result;
105
187
  bundle.attemptCount += 1;
106
188
  if (isDurablePause(result.status)) {
107
189
  return {
108
190
  ...result,
191
+ stats: cumulativeStats(result.stats),
109
192
  terminal: "gave_up",
110
193
  bundle,
111
194
  ...(isolationClass ? { isolationClass } : {}),
195
+ oracleCostMicroUsd,
112
196
  verification: { verdict: "unverified", unverifiedReason: result.status === "needs_review" ? "needs_review" : "suspended", rounds: bundle.attemptCount, findings: [] },
113
197
  };
114
198
  }
199
+ if (result.status !== "completed") {
200
+ return {
201
+ ...result,
202
+ stats: cumulativeStats(result.stats),
203
+ terminal: "gave_up",
204
+ bundle,
205
+ ...(isolationClass ? { isolationClass } : {}),
206
+ oracleCostMicroUsd,
207
+ verification: { verdict: "unverified", unverifiedReason: "impl_incomplete", rounds: bundle.attemptCount, findings: [] },
208
+ };
209
+ }
115
210
  const oracle = await config.oracle(config.graderEnv, result.result);
211
+ oracleCostMicroUsd += oracle.costMicroUsd ?? 0;
212
+ spend += oracle.costMicroUsd ?? 0;
116
213
  lastOracle = oracle;
117
214
  bundle.oracleTier = oracle.tier;
118
215
  const projected = terminalForTier(oracle);
@@ -120,9 +217,11 @@ export async function runRepairLoop(runner, implSpec, config) {
120
217
  if (projected !== undefined) {
121
218
  return {
122
219
  ...result,
220
+ stats: cumulativeStats(result.stats),
123
221
  terminal: projected,
124
222
  bundle,
125
223
  ...(isolationClass ? { isolationClass } : {}),
224
+ oracleCostMicroUsd,
126
225
  verification: outcomeFor(projected, oracle, bundle.attemptCount),
127
226
  };
128
227
  }
@@ -140,7 +239,15 @@ export async function runRepairLoop(runner, implSpec, config) {
140
239
  continue;
141
240
  }
142
241
  const terminal = "gave_up";
143
- return { ...lastResult, terminal, bundle, ...(isolationClass ? { isolationClass } : {}), verification: outcomeFor(terminal, lastOracle, bundle.attemptCount) };
242
+ return {
243
+ ...lastResult,
244
+ stats: cumulativeStats(lastResult.stats),
245
+ terminal,
246
+ bundle,
247
+ ...(isolationClass ? { isolationClass } : {}),
248
+ oracleCostMicroUsd,
249
+ verification: outcomeFor(terminal, lastOracle, bundle.attemptCount),
250
+ };
144
251
  }
145
252
  }
146
253
  }
@@ -22,6 +22,14 @@ export interface SubagentRetainEntry {
22
22
  activeAbort?: AbortController;
23
23
  release: () => Promise<void>;
24
24
  }
25
+ export type RetainEvictionTrigger = "ttl_sweep" | "ttl_timer" | "lru_overflow" | "session_ledger_lru" | "session_reap";
26
+ export interface RetainLedgerHooks {
27
+ onEvictionError?: (info: {
28
+ parentToolCallId?: string;
29
+ trigger: RetainEvictionTrigger;
30
+ error: Error;
31
+ }) => void;
32
+ }
25
33
  export declare class SubagentRetainLedger {
26
34
  readonly ttlMs: number;
27
35
  readonly max: number;
@@ -29,10 +37,20 @@ export declare class SubagentRetainLedger {
29
37
  private evictedIds;
30
38
  private ttlTimers;
31
39
  private isDisposed;
40
+ private readonly hooks;
41
+ private failedEvictions;
42
+ private lastFailure;
43
+ private readonly notifier;
32
44
  constructor(config: true | {
33
45
  ttlMs?: number;
34
46
  max?: number;
35
- });
47
+ }, hooks?: RetainLedgerHooks);
48
+ get evictionFailureCount(): number;
49
+ get lastEvictionError(): Error | undefined;
50
+ get evictionSinkFailureCount(): number;
51
+ noteDetachedFailure(trigger: RetainEvictionTrigger, error: unknown, parentToolCallId?: string): void;
52
+ private evictDetached;
53
+ disposeAllDetached(trigger: RetainEvictionTrigger): void;
36
54
  private static readonly TOMBSTONES_MAX;
37
55
  private tombstone;
38
56
  private effectiveTtlMs;
@@ -54,7 +72,7 @@ export declare function getSessionRetainLedger(sessionId: string): SubagentRetai
54
72
  export declare function getOrCreateSessionRetainLedger(sessionId: string, config: {
55
73
  ttlMs: number;
56
74
  max: number;
57
- }): SubagentRetainLedger;
75
+ }, hooks?: RetainLedgerHooks): SubagentRetainLedger;
58
76
  export declare function releaseSessionRetainLedger(sessionId: string): Promise<void>;
59
77
  export declare function ensureSessionReapHook(registry: import("../core/task-registry.js").TaskRegistry): void;
60
78
  export declare function createResumePrompt(marker: string, content: string): string;