@sema-agent/core 3.0.0 → 5.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 (153) hide show
  1. package/CHANGELOG.md +2382 -0
  2. package/dist/agents/cascade.d.ts +3 -1
  3. package/dist/agents/cascade.js +31 -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 +48 -24
  10. package/dist/agents/retain-ledger.d.ts +2 -0
  11. package/dist/agents/retain-ledger.js +6 -5
  12. package/dist/agents/roster-store.d.ts +1 -0
  13. package/dist/agents/roster-store.js +3 -5
  14. package/dist/agents/send-message-tool.d.ts +1 -0
  15. package/dist/agents/send-message-tool.js +2 -0
  16. package/dist/agents/subagent.d.ts +3 -1
  17. package/dist/agents/subagent.js +79 -94
  18. package/dist/agents/teacher.d.ts +1 -9
  19. package/dist/agents/teacher.js +21 -24
  20. package/dist/agents/tool-filter.js +6 -7
  21. package/dist/agents/verify.d.ts +1 -0
  22. package/dist/agents/verify.js +15 -14
  23. package/dist/brain/anthropic.js +24 -21
  24. package/dist/brain/openai.js +24 -21
  25. package/dist/brain/stream-engine.js +5 -2
  26. package/dist/brain/stream-shared.d.ts +1 -0
  27. package/dist/brain/stream-shared.js +14 -0
  28. package/dist/brain/terminal-cause.d.ts +4 -0
  29. package/dist/brain/terminal-cause.js +12 -0
  30. package/dist/brain/tool-call-id.d.ts +1 -0
  31. package/dist/brain/tool-call-id.js +3 -0
  32. package/dist/brain/tool-call-repair.js +2 -1
  33. package/dist/core/arg-summary.d.ts +21 -1
  34. package/dist/core/arg-summary.js +61 -14
  35. package/dist/core/auto-compaction.d.ts +1 -0
  36. package/dist/core/auto-compaction.js +16 -25
  37. package/dist/core/background-agent-store.d.ts +22 -22
  38. package/dist/core/background-agent-store.js +19 -23
  39. package/dist/core/checkpoint-store.d.ts +6 -13
  40. package/dist/core/checkpoint-store.js +11 -5
  41. package/dist/core/file-snapshot-store.d.ts +8 -0
  42. package/dist/core/file-snapshot-store.js +4 -3
  43. package/dist/core/fs-write-gate-policy.js +2 -3
  44. package/dist/core/hooks.d.ts +4 -1
  45. package/dist/core/hooks.js +8 -18
  46. package/dist/core/lsp.js +4 -3
  47. package/dist/core/mailbox-store.d.ts +7 -10
  48. package/dist/core/mcp.d.ts +7 -10
  49. package/dist/core/mcp.js +7 -9
  50. package/dist/core/memory-engine/engine.d.ts +0 -1
  51. package/dist/core/memory-engine/engine.js +24 -34
  52. package/dist/core/memory-engine/file-backend.d.ts +1 -2
  53. package/dist/core/memory-engine/file-backend.js +25 -36
  54. package/dist/core/memory-engine/layout.d.ts +8 -0
  55. package/dist/core/memory-engine/layout.js +60 -4
  56. package/dist/core/memory-engine/scope-contract.js +9 -3
  57. package/dist/core/memory-engine/types.d.ts +1 -1
  58. package/dist/core/permission-rules.js +2 -3
  59. package/dist/core/runner/active-skill-scope.d.ts +0 -1
  60. package/dist/core/runner/active-skill-scope.js +2 -17
  61. package/dist/core/runner/assemble-result.d.ts +2 -7
  62. package/dist/core/runner/assemble-result.js +3 -4
  63. package/dist/core/runner/compaction-call-options.d.ts +7 -0
  64. package/dist/core/runner/compaction-call-options.js +30 -0
  65. package/dist/core/runner/prepare-task.d.ts +6 -23
  66. package/dist/core/runner/prepare-task.js +36 -61
  67. package/dist/core/runner/runtask.d.ts +1 -0
  68. package/dist/core/runner/runtask.js +84 -172
  69. package/dist/core/runner/session-rule-policy.js +7 -5
  70. package/dist/core/runner/teardown-bounded.d.ts +7 -0
  71. package/dist/core/runner/teardown-bounded.js +36 -0
  72. package/dist/core/runner/tool-disclosure.d.ts +2 -5
  73. package/dist/core/runner/turn-attachments.d.ts +5 -15
  74. package/dist/core/runner/turn-attachments.js +1 -1
  75. package/dist/core/safe-notify.d.ts +17 -0
  76. package/dist/core/safe-notify.js +59 -0
  77. package/dist/core/sensitive-path-policy.js +4 -4
  78. package/dist/core/session-reconcile.js +1 -2
  79. package/dist/core/skill-tool-specifier.js +2 -3
  80. package/dist/core/skills-directory.js +2 -3
  81. package/dist/core/store-contracts/checkpoint-store-contract.js +1 -1
  82. package/dist/core/stub-env.d.ts +2 -6
  83. package/dist/core/surrogate-safe-slice.d.ts +4 -0
  84. package/dist/core/surrogate-safe-slice.js +18 -0
  85. package/dist/core/task-registry-monitor.js +8 -13
  86. package/dist/core/task-registry-shared.d.ts +23 -1
  87. package/dist/core/task-registry-shared.js +16 -0
  88. package/dist/core/task-registry.d.ts +6 -1
  89. package/dist/core/task-registry.js +52 -87
  90. package/dist/core/task-tool-shape.d.ts +0 -2
  91. package/dist/core/task-tool-shape.js +2 -5
  92. package/dist/core/tool-errors.js +5 -4
  93. package/dist/core/tool-name-aliases.d.ts +1 -2
  94. package/dist/core/tool-name-aliases.js +42 -59
  95. package/dist/core/tool-policy.js +21 -17
  96. package/dist/core/tool-result-store.d.ts +1 -0
  97. package/dist/core/tool-result-store.js +17 -1
  98. package/dist/core/trace.d.ts +9 -9
  99. package/dist/core/types.d.ts +15 -12
  100. package/dist/core/untrusted-egress.d.ts +4 -2
  101. package/dist/core/untrusted-egress.js +28 -14
  102. package/dist/core/workflow-journal-store.d.ts +9 -20
  103. package/dist/core/workflow-run-store.d.ts +11 -10
  104. package/dist/core/workflow-run-store.js +22 -16
  105. package/dist/engine/compaction/utils.js +4 -2
  106. package/dist/engine/execution-env/node-execution-env.d.ts +2 -24
  107. package/dist/engine/execution-env/node-execution-env.js +1 -1
  108. package/dist/engine/harness/agent-harness.d.ts +1 -0
  109. package/dist/engine/harness/agent-harness.js +13 -33
  110. package/dist/engine/harness/types.d.ts +29 -67
  111. package/dist/engine/llm/types.d.ts +1 -1
  112. package/dist/engine/loop/agent-loop.js +12 -14
  113. package/dist/engine/loop/types.d.ts +11 -9
  114. package/dist/engine/session/memory-repo.d.ts +2 -6
  115. package/dist/engine/session/session.d.ts +5 -30
  116. package/dist/index.d.ts +6 -3
  117. package/dist/index.js +4 -3
  118. package/dist/internal/harness-types.d.ts +3 -1
  119. package/dist/orchestration/run-spec.js +6 -4
  120. package/dist/orchestration/run-workflow-tool.js +3 -36
  121. package/dist/orchestration/workflow-governance.js +2 -1
  122. package/dist/orchestration/workflow.d.ts +4 -11
  123. package/dist/orchestration/workflow.js +164 -204
  124. package/dist/prompt-assembly/packs/sema-default.js +1 -3
  125. package/dist/prompt-assembly/tool-catalog.d.ts +10 -9
  126. package/dist/prompt-assembly/turn-snapshot.d.ts +2 -5
  127. package/dist/prompts/default.js +1 -3
  128. package/dist/prompts/simple-sections.d.ts +0 -1
  129. package/dist/prompts/simple-sections.js +0 -1
  130. package/dist/stores/file/background-agent-store.d.ts +3 -12
  131. package/dist/stores/file/background-agent-store.js +3 -41
  132. package/dist/stores/file/checkpoint-store.js +6 -6
  133. package/dist/stores/file/mailbox-store.d.ts +2 -6
  134. package/dist/stores/file/session-policy-store.js +15 -1
  135. package/dist/stores/file/session-store.d.ts +2 -6
  136. package/dist/stores/file/workflow-journal-store.d.ts +3 -11
  137. package/dist/stores/file/workflow-run-store.d.ts +3 -7
  138. package/dist/stores/file/workflow-run-store.js +4 -17
  139. package/dist/tools/fs/bash-readonly-classifier.d.ts +1 -0
  140. package/dist/tools/fs/bash-readonly-classifier.js +60 -25
  141. package/dist/tools/fs/fs-bash.js +330 -104
  142. package/dist/tools/fs/fs-pdf.d.ts +2 -12
  143. package/dist/tools/fs/fs-shared.js +2 -2
  144. package/dist/tools/fs/fs-write.js +0 -1
  145. package/dist/tools/fs/notebook.d.ts +2 -5
  146. package/dist/tools/fs/safety.d.ts +3 -0
  147. package/dist/tools/fs/safety.js +43 -14
  148. package/dist/tools/fs/search.js +5 -5
  149. package/dist/tools/monitor.d.ts +1 -0
  150. package/dist/tools/monitor.js +7 -5
  151. package/dist/tools/worktree.d.ts +2 -7
  152. package/dist/tools/worktree.js +3 -1
  153. package/package.json +6 -2
@@ -9,6 +9,7 @@ export interface CascadeRung {
9
9
  overrides?: Partial<Pick<TaskSpec, "limits" | "degrade" | "systemPrompt">>;
10
10
  }
11
11
  export interface CascadeConfig {
12
+ onNotifyError?: (failure: import("../core/safe-notify.js").SafeNotifyFailure) => void;
12
13
  ladder: CascadeRung[];
13
14
  gate?: (result: TaskResult, rung: {
14
15
  index: number;
@@ -29,7 +30,7 @@ export interface CascadeAttempt {
29
30
  index: number;
30
31
  model: ModelRef;
31
32
  passed: boolean;
32
- costMicroUsd: number;
33
+ costMicroUsd?: number;
33
34
  status: TaskResult["status"];
34
35
  errorCode?: string;
35
36
  gateError?: string;
@@ -40,6 +41,7 @@ export interface CascadeAttempt {
40
41
  }
41
42
  export interface CascadeRunResult extends TaskResult {
42
43
  cascadeOutcome: "passed" | "exhausted";
44
+ costCeilingUnenforceable?: true;
43
45
  escalated: boolean;
44
46
  finalRung: number;
45
47
  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);
@@ -14,37 +17,33 @@ export async function runCascade(runner, spec, config) {
14
17
  const gate = config.gate ?? createDefaultGate(spec);
15
18
  const startedAt = Date.now();
16
19
  const deadlineAt = config.totalTimeoutMs != null ? startedAt + config.totalTimeoutMs : undefined;
20
+ const notifier = createSafeNotifier(config.onNotifyError !== undefined ? { onError: config.onNotifyError } : undefined);
17
21
  const attempts = [];
18
22
  let totalCost = 0;
23
+ let ceilingCostKnown = true;
19
24
  let ownTokens = 0, ownTurns = 0, ownPrompt = 0, ownCached = 0, ownOutput = 0, ownCostMicro = 0;
20
25
  let ownTotalInput = 0;
21
26
  let ownCacheWrite = 0, ownCacheWriteLong = 0;
22
27
  let nestedTokens = 0, nestedTurns = 0, nestedTasks = 0, nestedCostMicro = 0;
23
28
  let ownLlmRootMicro = 0, ownCompactionMicro = 0;
29
+ let anyRungUnpriced = false;
24
30
  let hrCount = 0, hrWaitMs = 0;
25
31
  const hrGates = [];
26
- const cumulativeStats = (baseStats) => ({
27
- ...baseStats,
32
+ const cumulativeStats = (baseStats) => buildCumulativeStats(baseStats, {
28
33
  tokens: ownTokens,
29
34
  turns: ownTurns,
30
- promptTokens: ownPrompt || undefined,
31
- totalInputTokens: ownTotalInput || undefined,
32
- cachedTokens: ownCached || undefined,
33
- outputTokens: ownOutput || undefined,
34
- cacheWriteTokens: ownCacheWrite || undefined,
35
- cacheWriteTokensLong: ownCacheWriteLong || undefined,
36
- cacheHitRate: ownTotalInput > 0 ? Math.min(1, ownCached / ownTotalInput) : undefined,
35
+ promptTokens: ownPrompt,
36
+ totalInputTokens: ownTotalInput,
37
+ cachedTokens: ownCached,
38
+ outputTokens: ownOutput,
39
+ cacheWriteTokens: ownCacheWrite,
40
+ cacheWriteTokensLong: ownCacheWriteLong,
37
41
  costMicroUsd: ownCostMicro,
38
- nested: nestedTokens > 0 || nestedTasks > 0
39
- ? { tokens: nestedTokens, turns: nestedTurns, tasks: nestedTasks, costMicroUsd: nestedCostMicro || undefined }
40
- : undefined,
41
- costBreakdown: {
42
- llmRootMicroUsd: ownLlmRootMicro,
43
- nestedSubagentMicroUsd: nestedCostMicro,
44
- memoryConsolidationMicroUsd: 0,
45
- compactionMicroUsd: ownCompactionMicro,
46
- },
47
- 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 },
48
47
  });
49
48
  let lastResult;
50
49
  let passedRung = -1;
@@ -52,8 +51,9 @@ export async function runCascade(runner, spec, config) {
52
51
  for (let i = 0; i < maxRungs; i++) {
53
52
  if (deadlineAt !== undefined && Date.now() >= deadlineAt)
54
53
  break;
55
- if (i > 0 && config.costCeilingMicroUsd != null && totalCost >= config.costCeilingMicroUsd)
54
+ if (i > 0 && config.costCeilingMicroUsd != null && (totalCost >= config.costCeilingMicroUsd || !ceilingCostKnown)) {
56
55
  break;
56
+ }
57
57
  const rung = ladder[i];
58
58
  let deadlineFired = false;
59
59
  const rungAbort = new AbortController();
@@ -80,8 +80,11 @@ export async function runCascade(runner, spec, config) {
80
80
  }
81
81
  lastResult = result;
82
82
  const s = result.stats;
83
- const rungCost = (s.costMicroUsd ?? 0) + (s.nested?.costMicroUsd ?? 0);
84
- 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;
85
88
  ownTokens += s.tokens;
86
89
  ownTurns += s.turns;
87
90
  ownPrompt += s.promptTokens ?? 0;
@@ -91,6 +94,8 @@ export async function runCascade(runner, spec, config) {
91
94
  ownCacheWrite += s.cacheWriteTokens ?? 0;
92
95
  ownCacheWriteLong += s.cacheWriteTokensLong ?? 0;
93
96
  ownCostMicro += s.costMicroUsd ?? 0;
97
+ if (s.costMicroUsd === undefined)
98
+ anyRungUnpriced = true;
94
99
  if (s.costBreakdown) {
95
100
  ownLlmRootMicro += s.costBreakdown.llmRootMicroUsd;
96
101
  ownCompactionMicro += s.costBreakdown.compactionMicroUsd;
@@ -113,6 +118,7 @@ export async function runCascade(runner, spec, config) {
113
118
  ...mapped,
114
119
  stats: cumulativeStats(result.stats),
115
120
  cascadeOutcome: "exhausted",
121
+ ...(config.costCeilingMicroUsd != null && !ceilingCostKnown ? { costCeilingUnenforceable: true } : {}),
116
122
  escalated: i > 0,
117
123
  finalRung: i,
118
124
  attempts,
@@ -128,11 +134,7 @@ export async function runCascade(runner, spec, config) {
128
134
  errorCode: result.errorCode,
129
135
  ...(deadlineFired ? { timedOut: true } : { cancelled: true }),
130
136
  });
131
- try {
132
- config.onRung?.({ index: i, model: rung.model, passed: false, result });
133
- }
134
- catch {
135
- }
137
+ notifier.notify(() => config.onRung?.({ index: i, model: rung.model, passed: false, result }), CASCADE_ON_RUNG_SITE);
136
138
  await releaseSession(runner, result.sessionId);
137
139
  break;
138
140
  }
@@ -167,11 +169,7 @@ export async function runCascade(runner, spec, config) {
167
169
  diagnostics,
168
170
  ...(statusRejected ? { statusRejected: true } : {}),
169
171
  });
170
- try {
171
- config.onRung?.({ index: i, model: rung.model, passed, result });
172
- }
173
- catch {
174
- }
172
+ notifier.notify(() => config.onRung?.({ index: i, model: rung.model, passed, result }), CASCADE_ON_RUNG_SITE);
175
173
  }
176
174
  finally {
177
175
  await releaseSession(runner, result.sessionId);
@@ -187,6 +185,7 @@ export async function runCascade(runner, spec, config) {
187
185
  ...base,
188
186
  stats: cumulativeStats(base.stats),
189
187
  cascadeOutcome: passedRung >= 0 ? "passed" : "exhausted",
188
+ ...(config.costCeilingMicroUsd != null && !ceilingCostKnown ? { costCeilingUnenforceable: true } : {}),
190
189
  escalated: finalRung > 0,
191
190
  finalRung,
192
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) {
@@ -21,6 +21,8 @@ export interface RepairBundle {
21
21
  rejectedHypotheses: string[];
22
22
  attemptCount: number;
23
23
  oracleTier: OracleTier;
24
+ spentMicroUsd?: number;
25
+ activeElapsedMs?: number;
24
26
  }
25
27
  export interface RepairLoopConfig {
26
28
  oracle: RepairOracle;
@@ -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
  }
@@ -78,18 +79,31 @@ export async function runRepairLoop(runner, implSpec, config) {
78
79
  else if (config.workerEnv !== undefined && config.workerEnv === config.graderEnv) {
79
80
  return unprotectedResult("graderEnv must not be the worker env");
80
81
  }
81
- 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
+ }
82
86
  const bundle = config.resumeBundle
83
87
  ? { ...config.resumeBundle, diagnostics: [...config.resumeBundle.diagnostics], rejectedHypotheses: [...config.resumeBundle.rejectedHypotheses] }
84
88
  : freshBundle();
85
89
  const startedAt = Date.now();
86
- let spend = 0;
90
+ const seededSpend = config.resumeBundle?.spentMicroUsd;
91
+ const seededActiveMs = config.resumeBundle?.activeElapsedMs;
92
+ const priorActiveMs = typeof seededActiveMs === "number" && Number.isFinite(seededActiveMs) ? Math.max(0, seededActiveMs) : 0;
93
+ let spend = typeof seededSpend === "number" && Number.isFinite(seededSpend) ? Math.max(0, seededSpend) : 0;
94
+ const stampAccount = () => {
95
+ bundle.spentMicroUsd = spend;
96
+ bundle.activeElapsedMs = priorActiveMs + (Date.now() - startedAt);
97
+ };
87
98
  let lastResult;
88
99
  let lastOracle;
89
100
  let ownTokens = 0, ownTurns = 0, ownPrompt = 0, ownTotalInput = 0, ownCached = 0, ownOutput = 0;
90
101
  let ownCacheWrite = 0, ownCacheWriteLong = 0, ownCostMicro = 0;
91
102
  let ownLlmRootMicro = 0, ownCompactionMicro = 0;
103
+ let anyAttemptUnpriced = false;
92
104
  let nestedTokens = 0, nestedTurns = 0, nestedTasks = 0, nestedCostMicro = 0;
105
+ let hrCount = 0, hrWaitMs = 0;
106
+ const hrGates = [];
93
107
  let oracleCostMicroUsd = 0;
94
108
  const accumulate = (s) => {
95
109
  ownTokens += s.tokens;
@@ -101,10 +115,17 @@ export async function runRepairLoop(runner, implSpec, config) {
101
115
  ownCacheWrite += s.cacheWriteTokens ?? 0;
102
116
  ownCacheWriteLong += s.cacheWriteTokensLong ?? 0;
103
117
  ownCostMicro += s.costMicroUsd ?? 0;
118
+ if (s.costMicroUsd === undefined)
119
+ anyAttemptUnpriced = true;
104
120
  if (s.costBreakdown) {
105
121
  ownLlmRootMicro += s.costBreakdown.llmRootMicroUsd;
106
122
  ownCompactionMicro += s.costBreakdown.compactionMicroUsd;
107
123
  }
124
+ if (s.humanReview) {
125
+ hrCount += s.humanReview.count;
126
+ hrWaitMs += s.humanReview.totalWaitMs;
127
+ hrGates.push(...s.humanReview.gates);
128
+ }
108
129
  if (s.nested) {
109
130
  nestedTokens += s.nested.tokens;
110
131
  nestedTurns += s.nested.turns;
@@ -112,27 +133,21 @@ export async function runRepairLoop(runner, implSpec, config) {
112
133
  nestedCostMicro += s.nested.costMicroUsd ?? 0;
113
134
  }
114
135
  };
115
- const cumulativeStats = (base) => ({
116
- ...base,
136
+ const cumulativeStats = (base) => buildCumulativeStats(base, {
117
137
  tokens: ownTokens,
118
138
  turns: ownTurns,
119
- promptTokens: ownPrompt || undefined,
120
- totalInputTokens: ownTotalInput || undefined,
121
- cachedTokens: ownCached || undefined,
122
- outputTokens: ownOutput || undefined,
123
- cacheWriteTokens: ownCacheWrite || undefined,
124
- cacheWriteTokensLong: ownCacheWriteLong || undefined,
125
- cacheHitRate: ownTotalInput > 0 ? Math.min(1, ownCached / ownTotalInput) : undefined,
139
+ promptTokens: ownPrompt,
140
+ totalInputTokens: ownTotalInput,
141
+ cachedTokens: ownCached,
142
+ outputTokens: ownOutput,
143
+ cacheWriteTokens: ownCacheWrite,
144
+ cacheWriteTokensLong: ownCacheWriteLong,
126
145
  costMicroUsd: ownCostMicro,
127
- nested: nestedTokens > 0 || nestedTasks > 0
128
- ? { tokens: nestedTokens, turns: nestedTurns, tasks: nestedTasks, costMicroUsd: nestedCostMicro || undefined }
129
- : undefined,
130
- costBreakdown: {
131
- llmRootMicroUsd: ownLlmRootMicro,
132
- nestedSubagentMicroUsd: nestedCostMicro,
133
- memoryConsolidationMicroUsd: base.costBreakdown?.memoryConsolidationMicroUsd ?? 0,
134
- compactionMicroUsd: ownCompactionMicro,
135
- },
146
+ anyLegUnpriced: anyAttemptUnpriced,
147
+ llmRootMicroUsd: ownLlmRootMicro,
148
+ compactionMicroUsd: ownCompactionMicro,
149
+ nested: { tokens: nestedTokens, turns: nestedTurns, tasks: nestedTasks, costMicroUsd: nestedCostMicro },
150
+ humanReview: { count: hrCount, totalWaitMs: hrWaitMs, gates: hrGates },
136
151
  });
137
152
  let restartedOnce = false;
138
153
  let restartUsed = false;
@@ -140,11 +155,11 @@ export async function runRepairLoop(runner, implSpec, config) {
140
155
  const { sessionId: _drop, ...rest } = implSpec;
141
156
  return { ...rest, objective: `${implSpec.objective}\n\n${repairObjective(bundle)}` };
142
157
  };
143
- const exhaustedBeforeDispatch = () => ({
158
+ const exhaustedBeforeDispatch = (message, errorCode) => ({
144
159
  taskId: implSpec.taskId ?? "",
145
160
  status: "failed",
146
- result: `repair loop: attempt budget already spent (attemptCount ${bundle.attemptCount} >= maxAttempts ${maxAttempts}) — no attempt dispatched`,
147
- errorCode: "repair.attempts_exhausted",
161
+ result: message ?? `repair loop: attempt budget already spent (attemptCount ${bundle.attemptCount} >= maxAttempts ${maxAttempts}) — no attempt dispatched`,
162
+ errorCode: errorCode ?? "repair.attempts_exhausted",
148
163
  sessionId: implSpec.sessionId ?? "",
149
164
  stats: { tokens: 0, turns: 0, costMicroUsd: 0 },
150
165
  terminal: "gave_up",
@@ -156,6 +171,13 @@ export async function runRepairLoop(runner, implSpec, config) {
156
171
  while (true) {
157
172
  if (!restartedOnce && bundle.attemptCount >= maxAttempts)
158
173
  return exhaustedBeforeDispatch();
174
+ if (config.costCeilingMicroUsd != null && spend >= config.costCeilingMicroUsd) {
175
+ return exhaustedBeforeDispatch(`repair loop: cost ceiling already spent (carried ${spend} >= ceiling ${config.costCeilingMicroUsd} micro-USD) — no attempt dispatched`, "repair.budget_exhausted");
176
+ }
177
+ if (config.totalTimeoutMs != null && priorActiveMs + (Date.now() - startedAt) >= config.totalTimeoutMs) {
178
+ return exhaustedBeforeDispatch(`repair loop: active time budget already spent (carried ${priorActiveMs + (Date.now() - startedAt)}ms >= ${config.totalTimeoutMs}ms) — no attempt dispatched`, "repair.budget_exhausted");
179
+ }
180
+ stampAccount();
159
181
  const isRepair = bundle.attemptCount > 0 && lastResult !== undefined && !restartedOnce;
160
182
  const isSeededResume = !restartedOnce &&
161
183
  lastResult === undefined &&
@@ -174,6 +196,7 @@ export async function runRepairLoop(runner, implSpec, config) {
174
196
  restartedOnce = false;
175
197
  const result = await runner.runTask(spec, { repairBundle: bundle });
176
198
  spend += (result.stats.costMicroUsd ?? 0) + (result.stats.nested?.costMicroUsd ?? 0);
199
+ stampAccount();
177
200
  accumulate(result.stats);
178
201
  lastResult = result;
179
202
  bundle.attemptCount += 1;
@@ -202,6 +225,7 @@ export async function runRepairLoop(runner, implSpec, config) {
202
225
  const oracle = await config.oracle(config.graderEnv, result.result);
203
226
  oracleCostMicroUsd += oracle.costMicroUsd ?? 0;
204
227
  spend += oracle.costMicroUsd ?? 0;
228
+ stampAccount();
205
229
  lastOracle = oracle;
206
230
  bundle.oracleTier = oracle.tier;
207
231
  const projected = terminalForTier(oracle);
@@ -222,7 +246,7 @@ export async function runRepairLoop(runner, implSpec, config) {
222
246
  if (bundle.diagnostics.length < 3)
223
247
  bundle.diagnostics.push(oracle.trace);
224
248
  }
225
- const overTime = config.totalTimeoutMs != null && Date.now() - startedAt >= config.totalTimeoutMs;
249
+ const overTime = config.totalTimeoutMs != null && priorActiveMs + (Date.now() - startedAt) >= config.totalTimeoutMs;
226
250
  const overCost = config.costCeilingMicroUsd != null && spend >= config.costCeilingMicroUsd;
227
251
  if (bundle.attemptCount >= maxAttempts || overTime || overCost) {
228
252
  if (bundle.attemptCount >= maxAttempts && !overTime && !overCost && lastResult !== undefined && !restartUsed) {
@@ -40,12 +40,14 @@ export declare class SubagentRetainLedger {
40
40
  private readonly hooks;
41
41
  private failedEvictions;
42
42
  private lastFailure;
43
+ private readonly notifier;
43
44
  constructor(config: true | {
44
45
  ttlMs?: number;
45
46
  max?: number;
46
47
  }, hooks?: RetainLedgerHooks);
47
48
  get evictionFailureCount(): number;
48
49
  get lastEvictionError(): Error | undefined;
50
+ get evictionSinkFailureCount(): number;
49
51
  noteDetachedFailure(trigger: RetainEvictionTrigger, error: unknown, parentToolCallId?: string): void;
50
52
  private evictDetached;
51
53
  disposeAllDetached(trigger: RetainEvictionTrigger): void;
@@ -1,4 +1,5 @@
1
1
  import { delimitUntrusted } from "../core/untrusted-text.js";
2
+ import { createSafeNotifier } from "../core/safe-notify.js";
2
3
  import { RETAIN_DEFAULT_TTL_MS, RETAIN_DEFAULT_MAX } from "../config/defaults.js";
3
4
  export const SUBAGENT_RESUME_CAP = 8;
4
5
  export class SubagentRetainLedger {
@@ -11,6 +12,7 @@ export class SubagentRetainLedger {
11
12
  hooks;
12
13
  failedEvictions = 0;
13
14
  lastFailure;
15
+ notifier = createSafeNotifier();
14
16
  constructor(config, hooks) {
15
17
  const cfg = config === true ? {} : config;
16
18
  this.ttlMs = cfg.ttlMs ?? RETAIN_DEFAULT_TTL_MS;
@@ -23,15 +25,14 @@ export class SubagentRetainLedger {
23
25
  get lastEvictionError() {
24
26
  return this.lastFailure;
25
27
  }
28
+ get evictionSinkFailureCount() {
29
+ return this.notifier.failuresAt("retainLedger.onEvictionError");
30
+ }
26
31
  noteDetachedFailure(trigger, error, parentToolCallId) {
27
32
  const err = error instanceof Error ? error : new Error(String(error));
28
33
  this.failedEvictions += 1;
29
34
  this.lastFailure = err;
30
- try {
31
- this.hooks?.onEvictionError?.({ ...(parentToolCallId !== undefined ? { parentToolCallId } : {}), trigger, error: err });
32
- }
33
- catch {
34
- }
35
+ this.notifier.notify(() => this.hooks?.onEvictionError?.({ ...(parentToolCallId !== undefined ? { parentToolCallId } : {}), trigger, error: err }), "retainLedger.onEvictionError");
35
36
  }
36
37
  evictDetached(parentToolCallId, trigger) {
37
38
  void this.evict(parentToolCallId).catch((e) => this.noteDetachedFailure(trigger, e, parentToolCallId));
@@ -23,6 +23,7 @@ export interface RosterStore {
23
23
  releaseAgent(agentId: string): void | Promise<void>;
24
24
  }
25
25
  export interface RosterGcOptions {
26
+ onNotifyError?: (failure: import("../core/safe-notify.js").SafeNotifyFailure) => void;
26
27
  maxAgeMs?: number;
27
28
  maxEntries?: number;
28
29
  onEvicted?: (entry: RosterEntry) => void;
@@ -2,6 +2,7 @@ import { mkdirSync, readFileSync } from "node:fs";
2
2
  import { atomicWriteFile } from "../stores/file/fs-atomic.js";
3
3
  import { dirname } from "node:path";
4
4
  import { normalizeAgentName } from "../core/task-registry.js";
5
+ import { createSafeNotifier } from "../core/safe-notify.js";
5
6
  export function entryAccessible(e, access) {
6
7
  if (e.scope === undefined || access.scope === undefined || access.scope !== e.scope)
7
8
  return false;
@@ -35,15 +36,12 @@ function capped(entries, opts) {
35
36
  const max = opts?.maxEntries;
36
37
  if (max === undefined || entries.length <= max)
37
38
  return entries;
39
+ const notifier = createSafeNotifier(opts?.onNotifyError !== undefined ? { onError: opts.onNotifyError } : undefined);
38
40
  const byAge = [...entries].sort((a, b) => a.createdAt - b.createdAt);
39
41
  const dropCount = Math.max(0, entries.length - Math.max(0, max));
40
42
  const dropped = new Set(byAge.slice(0, dropCount));
41
43
  for (const e of dropped) {
42
- try {
43
- opts?.onEvicted?.(e);
44
- }
45
- catch {
46
- }
44
+ notifier.notify(() => opts?.onEvicted?.(e), "rosterStore.onEvicted");
47
45
  }
48
46
  return entries.filter((e) => !dropped.has(e));
49
47
  }
@@ -36,6 +36,7 @@ export interface SendMessageToolOptions {
36
36
  content: string;
37
37
  details?: unknown;
38
38
  }>;
39
+ onNotifyError?: (failure: import("../core/safe-notify.js").SafeNotifyFailure) => void;
39
40
  enrichCtx?: ToolCtxEnricher;
40
41
  }
41
42
  export declare const SEND_MESSAGE_SUMMARY_MAX = 200;
@@ -525,6 +525,8 @@ export function createSendMessageTool(opts) {
525
525
  ...(row.parentTaskId !== undefined ? { rowParentTaskId: row.parentTaskId } : {}),
526
526
  ...(row.parentSessionId !== undefined ? { rowParentSessionId: row.parentSessionId } : {}),
527
527
  ...(row.rootSessionId !== undefined ? { rowRootSessionId: row.rootSessionId } : {}),
528
+ ...(row.createdAt !== undefined ? { rowSpawnedAt: row.createdAt } : {}),
529
+ ...(opts.onNotifyError !== undefined ? { onNotifyError: opts.onNotifyError } : {}),
528
530
  ...(opts.notify ? { currentParentNotify: opts.notify } : {}),
529
531
  });
530
532
  const fromPrefix = senderIsChild ? `(message from teammate "${senderLabel}")\n` : "";
@@ -10,7 +10,6 @@ export type { SubagentStep, SubagentEditedFile } from "./subagent-steps.js";
10
10
  export declare function notifyResultField(result: string | undefined): string | undefined;
11
11
  export declare function inheritedManifestScopeFor(snapshot: readonly unknown[] | undefined): RunInternals["inheritedManifestScope"];
12
12
  export declare const DEFAULT_SUBAGENT_TOOL_NAME = "Agent";
13
- export declare const LEGACY_SUBAGENT_TOOL_NAME = "Task";
14
13
  export declare const EXTRA_TOOLS_MAX_FACTORY_CALLS_PER_TREE = 64;
15
14
  export declare const DEFAULT_SUBAGENT_MAX_DEPTH = 3;
16
15
  export declare const EXTRA_TOOLS_FAILED_NOTE = "note: extraTools evaluation failed \u2014 the injected tool set was skipped for this spawn.";
@@ -76,6 +75,8 @@ export declare function createSubagentResume(deps: {
76
75
  rowParentTaskId?: string;
77
76
  rowParentSessionId?: string;
78
77
  rowRootSessionId?: string;
78
+ rowSpawnedAt?: number;
79
+ onNotifyError?: (failure: import("../core/safe-notify.js").SafeNotifyFailure) => void;
79
80
  }): (content: string) => Promise<string>;
80
81
  export interface SubagentToolOptions {
81
82
  runner: Runner;
@@ -109,6 +110,7 @@ export interface SubagentToolOptions {
109
110
  onObserverError?: (err: unknown, info: {
110
111
  observedAgent?: string;
111
112
  observerAgent?: string;
113
+ site?: string;
112
114
  }) => void;
113
115
  extraTools?: (ctx: SubagentSpawnContext) => ToolSpec[] | Promise<ToolSpec[]>;
114
116
  onExtraToolsError?: (err: unknown, info: {