@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
@@ -1,7 +1,8 @@
1
1
  import {} from "../internal/llm.js";
2
2
  import { BrainError } from "./errors.js";
3
3
  import { DEGENERATE_MESSAGE, trimDegenerateTail } from "./repetition.js";
4
- import { createRepetitionPoll, createWalltimeGate } from "./stream-shared.js";
4
+ import { createRepetitionPoll, createWalltimeGate, parseStreamedToolArgs } from "./stream-shared.js";
5
+ import { mintFallbackToolCallId } from "./tool-call-id.js";
5
6
  import { WALLTIME_CUTOFF_MESSAGE } from "./walltime.js";
6
7
  import { emitBrainTelemetry } from "./status-sink.js";
7
8
  import { ANTHROPIC_RESERVED, applyExtraBody, stripAuthHeaders } from "./request-params.js";
@@ -201,23 +202,11 @@ function closeToolUseBlock(acc) {
201
202
  return acc.closedTc;
202
203
  if (!acc.toolName)
203
204
  return undefined;
204
- let args;
205
- if (acc.toolJson) {
206
- try {
207
- const parsed = JSON.parse(acc.toolJson);
208
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
209
- return undefined;
210
- args = parsed;
211
- }
212
- catch {
213
- return undefined;
214
- }
215
- }
216
- else {
217
- args = {};
218
- }
205
+ const args = parseStreamedToolArgs(acc.toolJson);
206
+ if (args === undefined)
207
+ return undefined;
219
208
  if (!acc.toolId)
220
- acc.toolId = `toolu_${Math.random().toString(36).slice(2)}`;
209
+ acc.toolId = mintFallbackToolCallId("toolu_");
221
210
  acc.closedTc = { type: "toolCall", id: acc.toolId, name: acc.toolName, arguments: args };
222
211
  return acc.closedTc;
223
212
  }
@@ -339,6 +328,8 @@ export function createAnthropicBrain(config = {}) {
339
328
  let stopReason = null;
340
329
  let stopDetails;
341
330
  let nextContentIndex = 0;
331
+ let malformedFrames = 0;
332
+ let malformedSample;
342
333
  let degenerate = false;
343
334
  let degenTextBlock;
344
335
  const rep = createRepetitionPoll();
@@ -512,6 +503,8 @@ export function createAnthropicBrain(config = {}) {
512
503
  evt = JSON.parse(data);
513
504
  }
514
505
  catch {
506
+ malformedFrames++;
507
+ malformedSample ??= data.slice(0, 160);
515
508
  return;
516
509
  }
517
510
  handleEvent(evt);
@@ -563,6 +556,12 @@ export function createAnthropicBrain(config = {}) {
563
556
  text: `\n[note: ${malformed.length} tool call(s) were truncated (stop_reason="${stopReason ?? "?"}") and dropped — re-issue them next turn: ${malformed.join("; ")}]`,
564
557
  });
565
558
  }
559
+ if (malformedFrames > 0) {
560
+ finalContent.push({
561
+ type: "text",
562
+ text: `\n[note: ${malformedFrames} provider stream frame(s) were not valid JSON and were dropped — the streamed output may be missing content (first sample: ${JSON.stringify(malformedSample)})]`,
563
+ });
564
+ }
566
565
  const truncatedEmpty = stopReason === "max_tokens" && noUsableContent && !toolError;
567
566
  const dynamicCut = truncatedEmpty && options?.maxTokensDynamic === true;
568
567
  const staticReasoningCut = truncatedEmpty && !dynamicCut && reasoningSeen && options?.staticReasoningCutDowngrade === true;
@@ -586,7 +585,7 @@ export function createAnthropicBrain(config = {}) {
586
585
  ? `[length_empty] response truncated at max_tokens with no answer text (${maxTokensNote}) — the model emitted reasoning although no thinking was requested (a budget-ignoring gateway?) and it consumed the entire output budget; raise max_tokens`
587
586
  : `[length_empty] response truncated at max_tokens with no answer text (${maxTokensNote}); raise max_tokens`
588
587
  : emptyNoFinish
589
- ? `[stream_torn] model stream ended with no content and no stop_reason — the response was lost (a torn stream or an in-band provider error frame)`
588
+ ? `[stream_torn] model stream ended with no content and no stop_reason — the response was lost (a torn stream or an in-band provider error frame)${malformedFrames > 0 ? ` — ${malformedFrames} unparseable data frame(s) were dropped on this stream (first sample: ${JSON.stringify(malformedSample)})` : ""}`
590
589
  : undefined));
591
590
  const errored = walltime.cut ||
592
591
  degenerate ||
@@ -595,9 +594,13 @@ export function createAnthropicBrain(config = {}) {
595
594
  emptyNoFinish ||
596
595
  (toolError !== undefined && noUsableContent) ||
597
596
  (truncatedEmpty && !dynamicCut && !staticReasoningCut);
598
- const errorKind = !walltime.cut && !degenerate && !safetyCut && toolError === undefined && truncatedEmpty && !dynamicCut && !staticReasoningCut
599
- ? "length_empty"
600
- : undefined;
597
+ const errorKind = walltime.cut
598
+ ? "walltime_cutoff"
599
+ : degenerate
600
+ ? "degenerate"
601
+ : !safetyCut && toolError === undefined && truncatedEmpty && !dynamicCut && !staticReasoningCut
602
+ ? "length_empty"
603
+ : undefined;
601
604
  const finalMessage = {
602
605
  ...partial,
603
606
  content: finalContent.length > 0 ? finalContent : [{ type: "text", text: "" }],
@@ -1,7 +1,8 @@
1
1
  import {} from "../internal/llm.js";
2
2
  import { repairTextToolCalls } from "./tool-call-repair.js";
3
3
  import { DEGENERATE_MESSAGE, trimDegenerateTail } from "./repetition.js";
4
- import { createRepetitionPoll, createWalltimeGate } from "./stream-shared.js";
4
+ import { createRepetitionPoll, createWalltimeGate, parseStreamedToolArgs } from "./stream-shared.js";
5
+ import { mintFallbackToolCallId } from "./tool-call-id.js";
5
6
  import { WALLTIME_CUTOFF_MESSAGE } from "./walltime.js";
6
7
  import { emitBrainTelemetry } from "./status-sink.js";
7
8
  import { OPENAI_RESERVED, applyExtraBody, stripAuthHeaders } from "./request-params.js";
@@ -12,23 +13,11 @@ function closeToolCallAccum(acc) {
12
13
  return acc.closedTc;
13
14
  if (!acc.name)
14
15
  return undefined;
15
- let args;
16
- if (acc.args) {
17
- try {
18
- const parsed = JSON.parse(acc.args);
19
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
20
- return undefined;
21
- args = parsed;
22
- }
23
- catch {
24
- return undefined;
25
- }
26
- }
27
- else {
28
- args = {};
29
- }
16
+ const args = parseStreamedToolArgs(acc.args);
17
+ if (args === undefined)
18
+ return undefined;
30
19
  if (!acc.id)
31
- acc.id = `call_${Math.random().toString(36).slice(2)}`;
20
+ acc.id = mintFallbackToolCallId("call_");
32
21
  acc.closedTc = { type: "toolCall", id: acc.id, name: acc.name, arguments: args };
33
22
  return acc.closedTc;
34
23
  }
@@ -356,6 +345,8 @@ export function createOpenAIBrain(config = {}) {
356
345
  const toolAccum = new Map();
357
346
  let finishReason = null;
358
347
  let usageRaw;
348
+ let malformedFrames = 0;
349
+ let malformedSample;
359
350
  const thinkingBlock = { type: "thinking", thinking: "" };
360
351
  const textBlock = { type: "text", text: "" };
361
352
  const pushReasoningDelta = (delta) => {
@@ -438,6 +429,8 @@ export function createOpenAIBrain(config = {}) {
438
429
  chunk = JSON.parse(data);
439
430
  }
440
431
  catch {
432
+ malformedFrames++;
433
+ malformedSample ??= data.slice(0, 160);
441
434
  return;
442
435
  }
443
436
  if (chunk.usage)
@@ -535,6 +528,12 @@ export function createOpenAIBrain(config = {}) {
535
528
  text: `\n[note: ${malformed.length} tool call(s) were truncated (finish_reason="${finishReason ?? "?"}") and dropped — re-issue them next turn: ${malformed.join("; ")}]`,
536
529
  });
537
530
  }
531
+ if (malformedFrames > 0) {
532
+ finalContent.push({
533
+ type: "text",
534
+ text: `\n[note: ${malformedFrames} provider stream frame(s) were not valid JSON and were dropped — the streamed output may be missing content (first sample: ${JSON.stringify(malformedSample)})]`,
535
+ });
536
+ }
538
537
  const truncatedEmpty = finishReason === "length" && noUsableContent && !toolError;
539
538
  const dynamicCut = truncatedEmpty && options?.maxTokensDynamic === true;
540
539
  const safetyCut = finishReason === "content_filter";
@@ -557,7 +556,7 @@ export function createOpenAIBrain(config = {}) {
557
556
  ? `[length_empty] response truncated at max_tokens (finish_reason="length", ${maxTokensNote}) with no answer text — the model spent the whole budget on reasoning (${accumReasoning.length} reasoning chars). Raise max_tokens, or disable thinking for this task.`
558
557
  : `[length_empty] response truncated at max_tokens (finish_reason="length", ${maxTokensNote}) with no answer text — raise max_tokens`
559
558
  : emptyNoFinish
560
- ? `[stream_torn] model stream ended with no content and no finish_reason — the response was lost (a torn stream or an in-band provider error frame)`
559
+ ? `[stream_torn] model stream ended with no content and no finish_reason — the response was lost (a torn stream or an in-band provider error frame)${malformedFrames > 0 ? ` — ${malformedFrames} unparseable data frame(s) were dropped on this stream (first sample: ${JSON.stringify(malformedSample)})` : ""}`
561
560
  : undefined));
562
561
  const errored = walltime.cut ||
563
562
  degenerate ||
@@ -566,9 +565,13 @@ export function createOpenAIBrain(config = {}) {
566
565
  emptyNoFinish ||
567
566
  (toolError !== undefined && noUsableContent) ||
568
567
  (truncatedEmpty && !dynamicCut);
569
- const errorKind = !walltime.cut && !degenerate && !safetyCut && !refused && toolError === undefined && truncatedEmpty && !dynamicCut
570
- ? "length_empty"
571
- : undefined;
568
+ const errorKind = walltime.cut
569
+ ? "walltime_cutoff"
570
+ : degenerate
571
+ ? "degenerate"
572
+ : !safetyCut && !refused && toolError === undefined && truncatedEmpty && !dynamicCut
573
+ ? "length_empty"
574
+ : undefined;
572
575
  const finalMessage = {
573
576
  ...partial,
574
577
  content: finalContent.length > 0 ? finalContent : [{ type: "text", text: "" }],
@@ -8,7 +8,7 @@ const DEFAULT_MAX_RETRIES = 10;
8
8
  const MAX_RETRIES_ENV_CEILING = 15;
9
9
  const MAX_RETRIES_ENV = "SEMA_MAX_RETRIES";
10
10
  export function resolveMaxRetries(configured) {
11
- if (configured !== undefined)
11
+ if (configured !== undefined && Number.isFinite(configured))
12
12
  return configured;
13
13
  const raw = process.env[MAX_RETRIES_ENV];
14
14
  if (raw !== undefined && raw.trim() !== "") {
@@ -88,10 +88,13 @@ export function runStreamingBrain(args) {
88
88
  .catch((err) => {
89
89
  const aborted = signal?.aborted === true || isAbortError(err);
90
90
  terminalRetryPhase = "gave_up";
91
- terminalRetryDetail = aborted ? "cancelled while retrying" : isWalltimeCutoff(err) ? "wall-clock deadline reached while retrying" : "retries exhausted";
91
+ const walltimeCut = isWalltimeCutoff(err);
92
+ terminalRetryDetail = aborted ? "cancelled while retrying" : walltimeCut ? "wall-clock deadline reached while retrying" : "retries exhausted";
92
93
  const errorMsg = emptyAssistant(model);
93
94
  errorMsg.stopReason = aborted ? "aborted" : "error";
94
95
  errorMsg.errorMessage = err instanceof Error ? err.message : String(err);
96
+ if (walltimeCut)
97
+ errorMsg.errorKind = "walltime_cutoff";
95
98
  errorMsg.usageMissing = true;
96
99
  out.push({ type: "error", reason: aborted ? "aborted" : "error", error: errorMsg });
97
100
  })
@@ -1,4 +1,5 @@
1
1
  import { type RepetitionEvent } from "./repetition.js";
2
+ export declare function parseStreamedToolArgs(json: string): Record<string, unknown> | undefined;
2
3
  export interface RepetitionPoll {
3
4
  poll(text: string): boolean;
4
5
  readonly cut: RepetitionEvent | undefined;
@@ -1,4 +1,18 @@
1
1
  import { inspectDegenerate } from "./repetition.js";
2
+ export function parseStreamedToolArgs(json) {
3
+ if (!json)
4
+ return {};
5
+ let parsed;
6
+ try {
7
+ parsed = JSON.parse(json);
8
+ }
9
+ catch {
10
+ return undefined;
11
+ }
12
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
13
+ return undefined;
14
+ return parsed;
15
+ }
2
16
  export function createRepetitionPoll() {
3
17
  let repCut;
4
18
  const repSpared = new Map();
@@ -0,0 +1,4 @@
1
+ import type { AssistantMessage } from "../internal/llm.js";
2
+ export type TerminalCauseCarrier = Pick<AssistantMessage, "errorKind" | "errorMessage">;
3
+ export declare function isDegenerateCutMessage(message: TerminalCauseCarrier): boolean;
4
+ export declare function isWalltimeCutMessage(message: TerminalCauseCarrier): boolean;
@@ -0,0 +1,12 @@
1
+ import { DEGENERATE_MESSAGE } from "./repetition.js";
2
+ import { WALLTIME_CUTOFF_MESSAGE } from "./walltime.js";
3
+ export function isDegenerateCutMessage(message) {
4
+ if (message.errorKind === "degenerate")
5
+ return true;
6
+ return message.errorMessage === DEGENERATE_MESSAGE;
7
+ }
8
+ export function isWalltimeCutMessage(message) {
9
+ if (message.errorKind === "walltime_cutoff")
10
+ return true;
11
+ return message.errorMessage === WALLTIME_CUTOFF_MESSAGE;
12
+ }
@@ -0,0 +1 @@
1
+ export declare function mintFallbackToolCallId(prefix: string): string;
@@ -0,0 +1,3 @@
1
+ export function mintFallbackToolCallId(prefix) {
2
+ return `${prefix}${Math.random().toString(36).slice(2)}`;
3
+ }
@@ -1,3 +1,4 @@
1
+ import { mintFallbackToolCallId } from "./tool-call-id.js";
1
2
  const TAG_RE = /<(tool_call|function_call|tool_calls|function_calls)>\s*([\s\S]*?)\s*<\/\1>/gi;
2
3
  const FENCE_RE = /```(?:json|tool_call|tool|function)?\s*([\s\S]*?)```/gi;
3
4
  function asRecord(v) {
@@ -41,7 +42,7 @@ function extractCall(obj, toolNames, framed) {
41
42
  }
42
43
  return {
43
44
  type: "toolCall",
44
- id: `repair_${name}_${Math.random().toString(36).slice(2, 8)}`,
45
+ id: mintFallbackToolCallId(`repair_${name}_`),
45
46
  name,
46
47
  arguments: args,
47
48
  };
@@ -1,2 +1,22 @@
1
- export declare function scrubSecrets(s: string): string;
1
+ export type RedactionConfidence = "high" | "medium" | "low";
2
+ export interface RedactionFinding {
3
+ kind: string;
4
+ span: readonly [number, number];
5
+ confidence: RedactionConfidence;
6
+ marker: string;
7
+ }
8
+ export interface RedactionReport {
9
+ findings: RedactionFinding[];
10
+ preexistingMarkers?: number;
11
+ }
12
+ export interface RedactionPass {
13
+ kind: string;
14
+ confidence: RedactionConfidence;
15
+ marker: string;
16
+ re: RegExp;
17
+ replace: string | ((match: string, ...groups: string[]) => string);
18
+ }
19
+ export declare function runRedactionPasses(input: string, passes: readonly RedactionPass[], report?: RedactionReport): string;
20
+ export declare const SECRET_PASSES: readonly RedactionPass[];
21
+ export declare function scrubSecrets(s: string, report?: RedactionReport): string;
2
22
  export declare function primaryActivityArg(args: unknown): string | undefined;
@@ -1,24 +1,71 @@
1
1
  const ACTIVITY_ARG_MAX = 80;
2
2
  const SENSITIVE_KEY = /token|secret|key|password|passwd|credential|auth/i;
3
- const SECRET_PATTERNS = [
4
- { re: /(?<![A-Za-z0-9])(?:sk|pk|rk|gh[opsur])[-_][A-Za-z0-9_-]{8,}/g, replace: "[redacted]" },
5
- { re: /(?<![A-Za-z0-9])AIza[A-Za-z0-9_-]{20,}/g, replace: "[redacted]" },
6
- { re: /(?<![A-Za-z0-9])xox[baprs]-[A-Za-z0-9-]{10,}/g, replace: "[redacted]" },
7
- { re: /(?<![A-Za-z0-9])AKIA[0-9A-Z]{12,}/g, replace: "[redacted]" },
8
- { re: /(?<![A-Za-z0-9])hf_[A-Za-z0-9]{20,}/g, replace: "[redacted]" },
9
- { re: /(?<![A-Za-z0-9])eyJ[A-Za-z0-9._-]{20,}/g, replace: "[redacted]" },
10
- { re: /-----BEGIN[ A-Z]*PRIVATE KEY-----[\s\S]*?(?:-----END[ A-Z]*PRIVATE KEY-----|$)/g, replace: "[redacted]" },
11
- { re: /(?<![A-Za-z0-9])Bearer\s+[A-Za-z0-9._~+/-]{8,}=*/gi, replace: "[redacted]" },
3
+ function mapBackOnePass(edits, pos) {
4
+ let delta = 0;
5
+ for (const e of edits) {
6
+ const outStart = e.at + delta;
7
+ if (pos < outStart)
8
+ return pos - delta;
9
+ const outEnd = outStart + e.insertedLen;
10
+ if (pos < outEnd)
11
+ return e.at;
12
+ delta += e.insertedLen - e.removedLen;
13
+ }
14
+ return pos - delta;
15
+ }
16
+ const PREEXISTING_MARKER_RE = /\[redacted(?:-[a-z]+)?\]/g;
17
+ export function runRedactionPasses(input, passes, report) {
18
+ if (report !== undefined && report.preexistingMarkers === undefined) {
19
+ report.preexistingMarkers = input.match(PREEXISTING_MARKER_RE)?.length ?? 0;
20
+ }
21
+ const batches = [];
22
+ let cur = input;
23
+ for (const pass of passes) {
24
+ const edits = [];
25
+ cur = cur.replace(pass.re, (...args) => {
26
+ const match = args[0];
27
+ const offset = args[args.length - 2];
28
+ const groups = args.slice(1, -2);
29
+ const inserted = typeof pass.replace === "string"
30
+ ? pass.replace.replace(/\$(\d)/g, (_m, d) => groups[Number(d) - 1] ?? "")
31
+ : pass.replace(match, ...groups);
32
+ if (inserted !== match) {
33
+ if (report !== undefined) {
34
+ let s0 = offset;
35
+ let e0 = offset + match.length;
36
+ for (let i = batches.length - 1; i >= 0; i--) {
37
+ s0 = mapBackOnePass(batches[i], s0);
38
+ e0 = mapBackOnePass(batches[i], e0);
39
+ }
40
+ report.findings.push({ kind: pass.kind, confidence: pass.confidence, span: [s0, e0], marker: pass.marker });
41
+ }
42
+ edits.push({ at: offset, removedLen: match.length, insertedLen: inserted.length });
43
+ }
44
+ return inserted;
45
+ });
46
+ batches.push(edits);
47
+ }
48
+ return cur;
49
+ }
50
+ export const SECRET_PASSES = [
51
+ { kind: "prefixed-token", confidence: "high", marker: "[redacted]", re: /(?<![A-Za-z0-9])(?:sk|pk|rk|gh[opsur])[-_][A-Za-z0-9_-]{8,}/g, replace: "[redacted]" },
52
+ { kind: "prefixed-token", confidence: "high", marker: "[redacted]", re: /(?<![A-Za-z0-9])AIza[A-Za-z0-9_-]{20,}/g, replace: "[redacted]" },
53
+ { kind: "prefixed-token", confidence: "high", marker: "[redacted]", re: /(?<![A-Za-z0-9])xox[baprs]-[A-Za-z0-9-]{10,}/g, replace: "[redacted]" },
54
+ { kind: "prefixed-token", confidence: "high", marker: "[redacted]", re: /(?<![A-Za-z0-9])AKIA[0-9A-Z]{12,}/g, replace: "[redacted]" },
55
+ { kind: "prefixed-token", confidence: "high", marker: "[redacted]", re: /(?<![A-Za-z0-9])hf_[A-Za-z0-9]{20,}/g, replace: "[redacted]" },
56
+ { kind: "jwt", confidence: "high", marker: "[redacted]", re: /(?<![A-Za-z0-9])eyJ[A-Za-z0-9._-]{20,}/g, replace: "[redacted]" },
57
+ { kind: "private-key-block", confidence: "high", marker: "[redacted]", re: /-----BEGIN[ A-Z]*PRIVATE KEY-----[\s\S]*?(?:-----END[ A-Z]*PRIVATE KEY-----|$)/g, replace: "[redacted]" },
58
+ { kind: "bearer-token", confidence: "high", marker: "[redacted]", re: /(?<![A-Za-z0-9])Bearer\s+[A-Za-z0-9._~+/-]{8,}=*/gi, replace: "[redacted]" },
12
59
  {
60
+ kind: "keyword-value",
61
+ confidence: "medium",
62
+ marker: "[redacted]",
13
63
  re: /(?<![A-Za-z0-9])((?:token|secret|key|password|passwd|credential|authorization)["']?\s*[=:]\s*["']?)[^\s"';|&]{4,}/gi,
14
64
  replace: "$1[redacted]",
15
65
  },
16
66
  ];
17
- export function scrubSecrets(s) {
18
- let out = s;
19
- for (const { re, replace } of SECRET_PATTERNS)
20
- out = out.replace(re, replace);
21
- return out;
67
+ export function scrubSecrets(s, report) {
68
+ return runRedactionPasses(s, SECRET_PASSES, report);
22
69
  }
23
70
  function truncCodePoints(s) {
24
71
  const cp = Array.from(s);
@@ -23,6 +23,7 @@ export interface CompactionWindowSafetyInfo {
23
23
  export declare const STALE_ANCHOR_STRUCTURAL_MARGIN = 2;
24
24
  export declare function sanitizeCompactionSettings(settings: CompactionSettings, contextWindow: number | undefined): CompactionSettings;
25
25
  export interface MaybeCompactOptions {
26
+ onNotifyError?: (failure: import("./safe-notify.js").SafeNotifyFailure) => void;
26
27
  session: Session;
27
28
  epochDeclaredSections?: import("../prompt-assembly/epoch.js").EpochDeclaredSections;
28
29
  centerAdoption?: {
@@ -5,6 +5,7 @@ import { selectCompactionEpoch } from "../prompt-assembly/epoch.js";
5
5
  import { SEMA_DEFAULT_PACK } from "../prompt-assembly/packs/sema-default.js";
6
6
  import { brainToRuntime } from "./runtime.js";
7
7
  import { defuseFenceMarkers, sanitizeUntrustedText } from "./untrusted-text.js";
8
+ import { createSafeNotifier } from "./safe-notify.js";
8
9
  export const DEFAULT_COMPACTION_INSTRUCTIONS = "Preserve verbatim every code, identifier, name, number, file path, error message, credential " +
9
10
  "reference, and explicit user instruction or decision mentioned anywhere in the conversation. " +
10
11
  "When in doubt, keep the specific detail rather than generalizing it.";
@@ -96,6 +97,7 @@ export async function maybeCompact(opts) {
96
97
  return { contextUsage, compacted: false, noop: true };
97
98
  }
98
99
  const trigger = opts.trigger ?? "auto";
100
+ const notifier = createSafeNotifier(opts.onNotifyError !== undefined ? { onError: opts.onNotifyError } : undefined);
99
101
  let hookInstructions;
100
102
  if (opts.preCompact) {
101
103
  let pre;
@@ -183,11 +185,7 @@ export async function maybeCompact(opts) {
183
185
  }
184
186
  const contentTokens = dry !== undefined ? Math.ceil((dry.conversationChars + (prep.value.previousSummary?.length ?? 0)) / cpt) : 0;
185
187
  const observeWindowSafety = (info) => {
186
- try {
187
- opts.onWindowSafety?.(info);
188
- }
189
- catch {
190
- }
188
+ notifier.notify(() => opts.onWindowSafety?.(info), "compaction.onWindowSafety");
191
189
  };
192
190
  if (wantsWindowSafety && dry !== undefined) {
193
191
  const tolerance = settings.clampTolerance ?? DEFAULT_CLAMP_TOLERANCE;
@@ -498,27 +496,20 @@ export async function maybeCompact(opts) {
498
496
  }
499
497
  catch {
500
498
  }
501
- try {
502
- opts.onCompaction?.({
503
- messagesCompacted: prep.value.messagesToSummarize.length,
504
- freedTokens,
505
- tokensBefore: summaryTokensBefore,
506
- reused,
507
- });
508
- }
509
- catch {
510
- }
499
+ const messagesCompacted = prep.value.messagesToSummarize.length;
500
+ notifier.notify(() => opts.onCompaction?.({
501
+ messagesCompacted,
502
+ freedTokens,
503
+ tokensBefore: summaryTokensBefore,
504
+ reused,
505
+ }), "compaction.onCompaction");
511
506
  if (opts.postCompact) {
512
- try {
513
- await opts.postCompact({
514
- trigger,
515
- summary,
516
- tokensBefore: summaryTokensBefore,
517
- ...(postTriggerTokens !== undefined ? { tokensAfter: postTriggerTokens } : {}),
518
- });
519
- }
520
- catch {
521
- }
507
+ await notifier.notifyAsync(() => opts.postCompact?.({
508
+ trigger,
509
+ summary,
510
+ tokensBefore: summaryTokensBefore,
511
+ ...(postTriggerTokens !== undefined ? { tokensAfter: postTriggerTokens } : {}),
512
+ }), "compaction.postCompact");
522
513
  }
523
514
  const endAt = Date.now();
524
515
  return {
@@ -73,18 +73,24 @@ export declare function canAccessAgentRecord(record: Pick<BackgroundAgentRecord,
73
73
  sessionId?: string;
74
74
  }): boolean;
75
75
  export declare const STALE_RUNNING_REAP_ATTRIBUTION = "the host process was interrupted while this agent was running \u2014 its in-process state was lost. Check its worktree / output file for partial work before assuming the task landed (stale running row reaped).";
76
+ export interface BackgroundAgentUpdateIfExpect {
77
+ rev: number;
78
+ status?: BackgroundAgentRecord["status"];
79
+ parkedCheckpointToken?: string | null;
80
+ parkClaimId?: string | null;
81
+ }
82
+ export interface BackgroundAgentReapOptions {
83
+ maxAgeMs?: number;
84
+ keep?: number;
85
+ staleRunningMaxAgeMs?: number;
86
+ }
76
87
  export interface BackgroundAgentStore {
77
88
  put(record: BackgroundAgentRecord): Promise<void>;
78
89
  get(handle: string, scope: string): Promise<BackgroundAgentRecord | null>;
79
90
  update(handle: string, scope: string, record: BackgroundAgentRecord, expect: {
80
91
  rev: number;
81
92
  }): Promise<boolean>;
82
- updateIf(handle: string, scope: string, record: BackgroundAgentRecord, expect: {
83
- rev: number;
84
- status?: BackgroundAgentRecord["status"];
85
- parkedCheckpointToken?: string | null;
86
- parkClaimId?: string | null;
87
- }): Promise<boolean>;
93
+ updateIf(handle: string, scope: string, record: BackgroundAgentRecord, expect: BackgroundAgentUpdateIfExpect): Promise<boolean>;
88
94
  listBySession(scope: string, sessionId: string, opts?: {
89
95
  status?: BackgroundAgentRecord["status"];
90
96
  limit?: number;
@@ -97,11 +103,7 @@ export interface BackgroundAgentStore {
97
103
  rev: number;
98
104
  }): Promise<boolean>;
99
105
  listScopes?(): Promise<string[]>;
100
- reap(scope: string, now: number, opts?: {
101
- maxAgeMs?: number;
102
- keep?: number;
103
- staleRunningMaxAgeMs?: number;
104
- }): Promise<number>;
106
+ reap(scope: string, now: number, opts?: BackgroundAgentReapOptions): Promise<number>;
105
107
  }
106
108
  export declare function reconcileParkedAgents(stores: {
107
109
  agentStore: BackgroundAgentStore;
@@ -114,6 +116,13 @@ export declare function reconcileParkedAgents(stores: {
114
116
  failed: number;
115
117
  rolledBack: number;
116
118
  }>;
119
+ export interface BackgroundAgentQuery {
120
+ scope: string;
121
+ sessionId?: string;
122
+ status?: BackgroundAgentRecord["status"];
123
+ limit?: number;
124
+ }
125
+ export declare function queryBackgroundAgents(rows: Iterable<BackgroundAgentRecord>, query: BackgroundAgentQuery): BackgroundAgentRowSummary[];
117
126
  export declare class InMemoryBackgroundAgentStore implements BackgroundAgentStore {
118
127
  private rows;
119
128
  private key;
@@ -122,12 +131,7 @@ export declare class InMemoryBackgroundAgentStore implements BackgroundAgentStor
122
131
  update(handle: string, scope: string, record: BackgroundAgentRecord, expect: {
123
132
  rev: number;
124
133
  }): Promise<boolean>;
125
- updateIf(handle: string, scope: string, record: BackgroundAgentRecord, expect: {
126
- rev: number;
127
- status?: BackgroundAgentRecord["status"];
128
- parkedCheckpointToken?: string | null;
129
- parkClaimId?: string | null;
130
- }): Promise<boolean>;
134
+ updateIf(handle: string, scope: string, record: BackgroundAgentRecord, expect: BackgroundAgentUpdateIfExpect): Promise<boolean>;
131
135
  listBySession(scope: string, sessionId: string, opts?: {
132
136
  status?: BackgroundAgentRecord["status"];
133
137
  limit?: number;
@@ -140,9 +144,5 @@ export declare class InMemoryBackgroundAgentStore implements BackgroundAgentStor
140
144
  rev: number;
141
145
  }): Promise<boolean>;
142
146
  listScopes(): Promise<string[]>;
143
- reap(scope: string, now: number, opts?: {
144
- maxAgeMs?: number;
145
- keep?: number;
146
- staleRunningMaxAgeMs?: number;
147
- }): Promise<number>;
147
+ reap(scope: string, now: number, opts?: BackgroundAgentReapOptions): Promise<number>;
148
148
  }
@@ -173,6 +173,23 @@ function toSummary(r) {
173
173
  ...(r.usage !== undefined ? { usage: structuredClone(r.usage) } : {}),
174
174
  };
175
175
  }
176
+ export function queryBackgroundAgents(rows, query) {
177
+ const out = [];
178
+ for (const r of rows) {
179
+ if (r.scope !== query.scope)
180
+ continue;
181
+ if (query.sessionId !== undefined) {
182
+ const anchor = r.sessionScoped ? r.owner : r.parentSessionId;
183
+ if (anchor !== query.sessionId && r.rootSessionId !== query.sessionId)
184
+ continue;
185
+ }
186
+ if (query.status !== undefined && r.status !== query.status)
187
+ continue;
188
+ out.push(toSummary(r));
189
+ }
190
+ out.sort((a, b) => b.spawnedAt - a.spawnedAt);
191
+ return query.limit !== undefined ? out.slice(0, Math.max(0, query.limit)) : out;
192
+ }
176
193
  export class InMemoryBackgroundAgentStore {
177
194
  rows = new Map();
178
195
  key(handle, scope) {
@@ -220,31 +237,10 @@ export class InMemoryBackgroundAgentStore {
220
237
  return this.update(handle, scope, record, { rev: expect.rev });
221
238
  }
222
239
  async listBySession(scope, sessionId, opts) {
223
- const out = [];
224
- for (const r of this.rows.values()) {
225
- if (r.scope !== scope)
226
- continue;
227
- const anchor = r.sessionScoped ? r.owner : r.parentSessionId;
228
- if (anchor !== sessionId && r.rootSessionId !== sessionId)
229
- continue;
230
- if (opts?.status !== undefined && r.status !== opts.status)
231
- continue;
232
- out.push(toSummary(r));
233
- }
234
- out.sort((a, b) => b.spawnedAt - a.spawnedAt);
235
- return opts?.limit !== undefined ? out.slice(0, Math.max(0, opts.limit)) : out;
240
+ return queryBackgroundAgents(this.rows.values(), { scope, sessionId, status: opts?.status, limit: opts?.limit });
236
241
  }
237
242
  async listByScope(scope, opts) {
238
- const out = [];
239
- for (const r of this.rows.values()) {
240
- if (r.scope !== scope)
241
- continue;
242
- if (opts?.status !== undefined && r.status !== opts.status)
243
- continue;
244
- out.push(toSummary(r));
245
- }
246
- out.sort((a, b) => b.spawnedAt - a.spawnedAt);
247
- return opts?.limit !== undefined ? out.slice(0, Math.max(0, opts.limit)) : out;
243
+ return queryBackgroundAgents(this.rows.values(), { scope, status: opts?.status, limit: opts?.limit });
248
244
  }
249
245
  async delete(handle, scope, expect) {
250
246
  const k = this.key(handle, scope);