@sema-agent/core 2.2.0 → 2.4.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 (70) hide show
  1. package/dist/agents/send-message-tool.d.ts +4 -0
  2. package/dist/agents/send-message-tool.js +37 -24
  3. package/dist/agents/subagent.js +275 -127
  4. package/dist/agents/teacher.js +51 -23
  5. package/dist/brain/errors.d.ts +1 -0
  6. package/dist/brain/errors.js +14 -0
  7. package/dist/brain/stream-engine.js +3 -3
  8. package/dist/core/context-edit.js +2 -1
  9. package/dist/core/runner/assemble-result.d.ts +1 -0
  10. package/dist/core/runner/assemble-result.js +12 -8
  11. package/dist/core/runner/prepare-task.js +32 -7
  12. package/dist/core/runner/runtask.js +18 -5
  13. package/dist/core/runner/tool-output-projection.js +2 -1
  14. package/dist/core/store-contracts/checkpoint-store-contract.d.ts +37 -0
  15. package/dist/core/store-contracts/checkpoint-store-contract.js +195 -0
  16. package/dist/core/store-contracts/contract-harness.d.ts +6 -0
  17. package/dist/core/store-contracts/contract-harness.js +16 -0
  18. package/dist/core/store-contracts/contract-kit-version.d.ts +1 -0
  19. package/dist/core/store-contracts/contract-kit-version.js +2 -0
  20. package/dist/core/store-contracts/file-snapshot-store-contract.d.ts +3 -0
  21. package/dist/core/store-contracts/file-snapshot-store-contract.js +126 -0
  22. package/dist/core/store-contracts/mailbox-store-contract.d.ts +6 -0
  23. package/dist/core/store-contracts/mailbox-store-contract.js +193 -0
  24. package/dist/core/store-contracts/session-repo-contract.d.ts +3 -0
  25. package/dist/core/store-contracts/session-repo-contract.js +36 -0
  26. package/dist/core/store-contracts/tool-result-store-contract.d.ts +3 -0
  27. package/dist/core/store-contracts/tool-result-store-contract.js +35 -0
  28. package/dist/core/task-notification.d.ts +2 -0
  29. package/dist/core/task-registry-agent.d.ts +7 -1
  30. package/dist/core/task-registry-agent.js +31 -2
  31. package/dist/core/task-registry-monitor.js +71 -6
  32. package/dist/core/task-registry-shared.d.ts +18 -1
  33. package/dist/core/task-registry-shared.js +5 -2
  34. package/dist/core/task-registry.d.ts +9 -0
  35. package/dist/core/task-registry.js +52 -7
  36. package/dist/core/tool-result-store.d.ts +3 -2
  37. package/dist/core/tool-result-store.js +12 -4
  38. package/dist/core/tools.d.ts +2 -0
  39. package/dist/core/tools.js +9 -0
  40. package/dist/core/trace.d.ts +7 -0
  41. package/dist/core/workflow-journal-store.d.ts +16 -0
  42. package/dist/core/workflow-journal-store.js +28 -0
  43. package/dist/engine/lsp/node-lsp-manager.d.ts +2 -0
  44. package/dist/engine/lsp/node-lsp-manager.js +16 -0
  45. package/dist/index.d.ts +1 -0
  46. package/dist/index.js +1 -0
  47. package/dist/orchestration/builtin-workflows.d.ts +1 -1
  48. package/dist/orchestration/builtin-workflows.js +11 -2
  49. package/dist/orchestration/workflow-governance.d.ts +6 -1
  50. package/dist/orchestration/workflow-governance.js +24 -4
  51. package/dist/orchestration/workflow-primitives.js +7 -1
  52. package/dist/orchestration/workflow.d.ts +16 -0
  53. package/dist/orchestration/workflow.js +94 -21
  54. package/dist/tools/fs/fs-bash.d.ts +7 -1
  55. package/dist/tools/fs/fs-bash.js +51 -20
  56. package/dist/tools/fs/fs-read.js +22 -11
  57. package/dist/tools/fs/fs-search-tools.js +3 -3
  58. package/dist/tools/fs/fs-shared.d.ts +20 -7
  59. package/dist/tools/fs/fs-shared.js +17 -3
  60. package/dist/tools/fs/fs-write.js +4 -4
  61. package/dist/tools/fs/index.d.ts +2 -0
  62. package/dist/tools/fs/index.js +7 -1
  63. package/dist/tools/fs/repo-map.js +2 -2
  64. package/dist/tools/fs/safety.d.ts +10 -0
  65. package/dist/tools/fs/safety.js +15 -1
  66. package/dist/tools/monitor.d.ts +2 -0
  67. package/dist/tools/monitor.js +20 -4
  68. package/dist/tools/web.js +6 -2
  69. package/dist/tools/worktree.js +46 -25
  70. package/package.json +1 -1
@@ -1,5 +1,6 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { mapNestedSuspend, isDurablePause } from "./suspend-guard.js";
3
+ import { isDefineToolProduct, stampDefineToolBrand } from "../core/tools.js";
3
4
  import { delimitUntrusted, inlineUntrusted } from "../core/untrusted-text.js";
4
5
  export const TEACHER_PROMPT = `You are an expert advisor to a less-capable "student" agent that got stuck.
5
6
  You receive the task and the student's recent failed attempts. Your job is to help the student RECOVER,
@@ -119,29 +120,56 @@ async function runTeacherCore(runner, studentSpec, teacher) {
119
120
  errorStreak = sig === lastErrSig ? errorStreak + 1 : 1;
120
121
  lastErrSig = sig;
121
122
  };
122
- const wrap = (t) => ({
123
- ...t,
124
- execute: async (args, ctx) => {
125
- try {
126
- const out = await t.execute(args, ctx);
127
- const text = typeof out === "string"
128
- ? out
129
- : out && typeof out === "object" && "content" in out
130
- ? String(out.content)
131
- : (JSON.stringify(out) ?? "");
132
- logTool(t.name, args, text);
133
- errorStreak = 0;
134
- lastErrSig = "";
135
- return out;
136
- }
137
- catch (e) {
138
- const errText = String(e);
139
- logTool(t.name, args, `ERROR: ${errText}`);
140
- onToolFail(t.name, errText);
141
- throw e;
142
- }
143
- },
144
- });
123
+ const wrap = (t) => {
124
+ if (isDefineToolProduct(t)) {
125
+ const product = t;
126
+ return stampDefineToolBrand({
127
+ ...t,
128
+ execute: async (toolCallId, rawParams, signal, onUpdate) => {
129
+ try {
130
+ const out = await product.execute(toolCallId, rawParams, signal, onUpdate);
131
+ const blocks = out && typeof out === "object" && "content" in out ? out.content : out;
132
+ const text = Array.isArray(blocks)
133
+ ? blocks.map((b) => (b && typeof b === "object" && "text" in b ? String(b.text) : "")).join("\n")
134
+ : String(blocks ?? "");
135
+ logTool(t.name, rawParams, text);
136
+ errorStreak = 0;
137
+ lastErrSig = "";
138
+ return out;
139
+ }
140
+ catch (e) {
141
+ const errText = String(e);
142
+ logTool(t.name, rawParams, `ERROR: ${errText}`);
143
+ onToolFail(t.name, errText);
144
+ throw e;
145
+ }
146
+ },
147
+ });
148
+ }
149
+ return {
150
+ ...t,
151
+ execute: async (args, ctx) => {
152
+ try {
153
+ const out = await t.execute(args, ctx);
154
+ const text = typeof out === "string"
155
+ ? out
156
+ : out && typeof out === "object" && "content" in out
157
+ ? String(out.content)
158
+ : (JSON.stringify(out) ?? "");
159
+ logTool(t.name, args, text);
160
+ errorStreak = 0;
161
+ lastErrSig = "";
162
+ return out;
163
+ }
164
+ catch (e) {
165
+ const errText = String(e);
166
+ logTool(t.name, args, `ERROR: ${errText}`);
167
+ onToolFail(t.name, errText);
168
+ throw e;
169
+ }
170
+ },
171
+ };
172
+ };
145
173
  const tools = studentSpec.tools?.map(wrap);
146
174
  const helperBase = () => teacher.helperModel
147
175
  ? { model: teacher.helperModel }
@@ -12,4 +12,5 @@ export declare class BrainError extends Error {
12
12
  export declare function classifyHttp(status: number): BrainErrorCode;
13
13
  export declare function extractErrorCode(errorMessage: string | undefined): BrainErrorCode | undefined;
14
14
  export declare function stripErrorCodePrefix(errorMessage: string): string;
15
+ export declare function describeNetworkError(e: unknown): string;
15
16
  export {};
@@ -42,3 +42,17 @@ export function extractErrorCode(errorMessage) {
42
42
  export function stripErrorCodePrefix(errorMessage) {
43
43
  return errorMessage.replace(CODE_PREFIX_RE, "");
44
44
  }
45
+ export function describeNetworkError(e) {
46
+ const top = e instanceof Error ? e.message : String(e);
47
+ const parts = [];
48
+ let cur = e instanceof Error ? e.cause : undefined;
49
+ for (let depth = 0; depth < 4 && cur !== undefined && cur !== null; depth++) {
50
+ const code = typeof cur.code === "string" ? cur.code : undefined;
51
+ const msg = cur instanceof Error ? cur.message : String(cur);
52
+ const piece = code !== undefined && !msg.includes(code) ? `${code}: ${msg}` : msg;
53
+ if (piece.length > 0 && piece !== top && !parts.includes(piece))
54
+ parts.push(piece);
55
+ cur = cur instanceof Error ? cur.cause : undefined;
56
+ }
57
+ return parts.length > 0 ? `${top} (${parts.join(" ← ")})` : top;
58
+ }
@@ -1,5 +1,5 @@
1
1
  import { createAssistantMessageEventStream, } from "../internal/llm.js";
2
- import { BrainError, classifyHttp } from "./errors.js";
2
+ import { BrainError, classifyHttp, describeNetworkError } from "./errors.js";
3
3
  import { retryBackoffMs } from "./retry.js";
4
4
  import { emitBrainStatus, emitBrainTelemetry } from "./status-sink.js";
5
5
  import { createConnectController } from "./timeout.js";
@@ -139,7 +139,7 @@ export function runStreamingBrain(args) {
139
139
  continue;
140
140
  }
141
141
  if (netErr)
142
- throw new BrainError("network", netErr instanceof Error ? netErr.message : String(netErr));
142
+ throw new BrainError("network", describeNetworkError(netErr));
143
143
  const detail = r ? await r.text().catch(() => "") : "";
144
144
  const status = r?.status ?? 0;
145
145
  throw new BrainError(classifyHttp(status), `${httpLabel} HTTP ${status || "ERR"}: ${detail.slice(0, 500)}`, status);
@@ -250,7 +250,7 @@ export function runStreamingBrain(args) {
250
250
  throw e;
251
251
  failure = {
252
252
  kind: "connection",
253
- err: new BrainError("network", `mid-stream read failed: ${e instanceof Error ? e.message : String(e)}`),
253
+ err: new BrainError("network", `mid-stream read failed: ${describeNetworkError(e)}`),
254
254
  };
255
255
  break readLoop;
256
256
  }
@@ -1,7 +1,8 @@
1
1
  import { DEFAULT_CHARS_PER_TOKEN, estimateContextTokens, estimateTokens } from "../internal/harness.js";
2
2
  import { isToolResult } from "./message-utils.js";
3
+ import { offloadPagebackHint } from "./tool-result-store.js";
3
4
  const CLEARED_MARKER = "[tool result cleared to save context]";
4
- const refNote = (ref) => `full text persisted; call ReadToolResult with ref "${ref}" to read it back`;
5
+ const refNote = (ref) => offloadPagebackHint(ref, "cleared");
5
6
  const mediaNote = (blocks) => {
6
7
  const byType = new Map();
7
8
  for (const b of blocks) {
@@ -68,6 +68,7 @@ export interface Stats {
68
68
  export interface ResultFlags {
69
69
  threw: unknown;
70
70
  model?: string;
71
+ unpricedSpend?: boolean;
71
72
  abortedForTimeout: boolean;
72
73
  abortedForTurns: boolean;
73
74
  abortedLive?: boolean;
@@ -36,14 +36,16 @@ export function assembleResult(spec, sessionId, final, stats, flags) {
36
36
  const taskId = spec.taskId ?? sessionId;
37
37
  const text = final ? assistantText(final) : "";
38
38
  stats.totalInputTokens = stats.promptTokens;
39
- const compactionMicroUsd = stats.compactionMicroUsd ?? 0;
40
- const nestedSubagentMicroUsd = stats.nested?.costMicroUsd ?? 0;
41
- stats.costBreakdown = {
42
- llmRootMicroUsd: Math.max(0, stats.costMicroUsd - compactionMicroUsd),
43
- nestedSubagentMicroUsd,
44
- memoryConsolidationMicroUsd: 0,
45
- compactionMicroUsd,
46
- };
39
+ if (!flags.unpricedSpend) {
40
+ const compactionMicroUsd = stats.compactionMicroUsd ?? 0;
41
+ const nestedSubagentMicroUsd = stats.nested?.costMicroUsd ?? 0;
42
+ stats.costBreakdown = {
43
+ llmRootMicroUsd: Math.max(0, stats.costMicroUsd - compactionMicroUsd),
44
+ nestedSubagentMicroUsd,
45
+ memoryConsolidationMicroUsd: 0,
46
+ compactionMicroUsd,
47
+ };
48
+ }
47
49
  let status;
48
50
  const result = text;
49
51
  let errorMessage;
@@ -139,5 +141,7 @@ export function assembleResult(spec, sessionId, final, stats, flags) {
139
141
  }
140
142
  const { compactionMicroUsd: _internalCompaction, ...publicStats } = stats;
141
143
  void _internalCompaction;
144
+ if (flags.unpricedSpend)
145
+ delete publicStats.costMicroUsd;
142
146
  return { taskId, sessionId, status, ...(flags.model !== undefined ? { model: flags.model } : {}), result: result.trim(), salvagedOutput, blockedReason, errorMessage, errorCode, checkpointToken, checkpointGate, stats: publicStats };
143
147
  }
@@ -28,7 +28,7 @@ import { cloneObserverInput, formatHookFeedback, runToolGate } from "../hooks.js
28
28
  import { reconcileInterruptedSession } from "../session-reconcile.js";
29
29
  import { CacheBreakDetector, toolsToFingerprintInputs } from "../cache-break-detector.js";
30
30
  import { reservedCollisions, reservedFor } from "../../brain/request-params.js";
31
- import { defineTool } from "../tools.js";
31
+ import { defineTool, isDefineToolProduct } from "../tools.js";
32
32
  import { canonicalToolName } from "../tool-name-aliases.js";
33
33
  import { pathToUri } from "../lsp-protocol.js";
34
34
  import { DEFAULT_TOOL_RESULT_THRESHOLD_CHARS, buildToolResultRef, firstPartyOffloadPolicy, InMemoryToolResultStore, RunnerSharedToolResultStore, ScopedToolResultStore, isVolatileOffloadStore, OFFLOAD_TOOL_NAME, createReadToolResultTool, withToolResultOffload, } from "../tool-result-store.js";
@@ -435,10 +435,11 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
435
435
  const offloadStore = rawOffloadStore instanceof RunnerSharedToolResultStore
436
436
  ? new ScopedToolResultStore(rawOffloadStore, taskScope)
437
437
  : rawOffloadStore;
438
+ const offloadReachableToolsRef = {};
438
439
  const maybeOffload = (tool, perTool) => {
439
440
  if (!offloadStore || perTool?.offload === false)
440
441
  return tool;
441
- return withToolResultOffload(tool, offloadStore, perTool?.offloadThresholdChars ?? offloadThreshold, sessionId);
442
+ return withToolResultOffload(tool, offloadStore, perTool?.offloadThresholdChars ?? offloadThreshold, sessionId, () => offloadReachableToolsRef.current?.());
442
443
  };
443
444
  const firstPartyOffload = (tool) => {
444
445
  const policy = firstPartyOffloadPolicy(tool.name);
@@ -796,10 +797,15 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
796
797
  requestStopAfterTurn,
797
798
  ...(spec.enablePlanMode === true ? { enterPlanMode } : {}),
798
799
  });
799
- const tools = (spec.tools ?? []).map((t) => maybeOffload(defineTool({
800
- ...t,
801
- execute: (args, ctx) => t.execute(args, enrichSpecToolCtx(ctx)),
802
- }), t));
800
+ const tools = (spec.tools ?? []).map((t) => {
801
+ if (isDefineToolProduct(t)) {
802
+ return maybeOffload(t, t);
803
+ }
804
+ return maybeOffload(defineTool({
805
+ ...t,
806
+ execute: (args, ctx) => t.execute(args, enrichSpecToolCtx(ctx)),
807
+ }), t);
808
+ });
803
809
  const blockedRef = {};
804
810
  if (spec.enableBlockedReport !== false) {
805
811
  tools.push(createReportBlockedTool(blockedRef));
@@ -1118,6 +1124,14 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1118
1124
  phase: "config",
1119
1125
  sessionId,
1120
1126
  });
1127
+ emitTrace(deps.tracer, () => ({
1128
+ kind: "config.additional_directory_skipped",
1129
+ version: 1,
1130
+ taskId: hostTaskId,
1131
+ entry: dir,
1132
+ reason: `${c.error.code}: ${c.error.message}`,
1133
+ ts: Date.now(),
1134
+ }));
1121
1135
  }
1122
1136
  }
1123
1137
  if (spec.envFacts?.scratchpadDir) {
@@ -1217,7 +1231,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1217
1231
  if (backgroundTaskToolsActive || workflowToolsActive) {
1218
1232
  toolEffects.set("TaskOutput", "read");
1219
1233
  toolEffects.set("TaskStop", "write");
1220
- tools.push(firstPartyOffload(createTaskOutputTool({ registry: defaultTaskRegistry, owner: hostTaskId, scope: taskScope, sessionId, workflowStore: deps.workflowRunStore, agentStore: deps.backgroundAgentStore, notificationWired: internals?.onTaskNotification !== undefined, oneShot: spec.oneShot, ...(callCapRef ? { deadlineMs: () => toolCutDeadlineMs(callCapRef, Date.now()) } : {}) })), firstPartyOffload(createTaskStopTool({ registry: defaultTaskRegistry, owner: hostTaskId, scope: taskScope, sessionId, workflowStore: deps.workflowRunStore, agentStore: deps.backgroundAgentStore })));
1234
+ tools.push(firstPartyOffload(createTaskOutputTool({ registry: defaultTaskRegistry, owner: hostTaskId, scope: taskScope, sessionId, workflowStore: deps.workflowRunStore, agentStore: deps.backgroundAgentStore, notificationWired: internals?.onTaskNotification !== undefined, oneShot: spec.oneShot, toolResultStore: offloadStore, ...(callCapRef ? { deadlineMs: () => toolCutDeadlineMs(callCapRef, Date.now()) } : {}) })), firstPartyOffload(createTaskStopTool({ registry: defaultTaskRegistry, owner: hostTaskId, scope: taskScope, sessionId, workflowStore: deps.workflowRunStore, agentStore: deps.backgroundAgentStore })));
1221
1235
  if (runnerSelf && !(spec.tools ?? []).some((t) => t.name === SEND_MESSAGE_TOOL_NAME)) {
1222
1236
  const delegationForRevive = (spec.tools ?? []).find((t) => t.agentListing !== undefined);
1223
1237
  const reviveSpawn = delegationForRevive !== undefined && deps.backgroundAgentStore !== undefined && deps.mailboxStore !== undefined
@@ -1250,6 +1264,8 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1250
1264
  ...(internals?.parentNotify !== undefined ? { uplink: internals.parentNotify } : {}),
1251
1265
  ...(internals?.explicitAgentName !== undefined ? { senderName: internals.explicitAgentName } : {}),
1252
1266
  ...(internals?.parentRetainLedger !== undefined ? { siblingRetain: internals.parentRetainLedger } : {}),
1267
+ ...(internals?.parentTaskId !== undefined ? { parentTaskId: internals.parentTaskId } : {}),
1268
+ ...(internals?.parentSessionId !== undefined ? { parentSessionId: internals.parentSessionId } : {}),
1253
1269
  ...(deps.rosterStore !== undefined ? { roster: deps.rosterStore } : {}),
1254
1270
  ...(deps.onBackgroundChildEvent ? { onBackgroundChildEvent: deps.onBackgroundChildEvent } : {}),
1255
1271
  ...(deps.backgroundAgentStore !== undefined ? { agentStore: deps.backgroundAgentStore } : {}),
@@ -1277,6 +1293,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1277
1293
  ...(sessionId !== undefined ? { sessionId } : {}),
1278
1294
  ...(internals?.onTaskNotification !== undefined ? { onTaskNotification: internals.onTaskNotification } : {}),
1279
1295
  ...(handsCwdRef !== undefined ? { cwdRef: handsCwdRef } : {}),
1296
+ ...(offloadStore !== undefined ? { toolResultStore: offloadStore } : {}),
1280
1297
  })));
1281
1298
  }
1282
1299
  if (handsCwdRef !== undefined) {
@@ -1808,6 +1825,14 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1808
1825
  if (deferred.has(n))
1809
1826
  activeTools.add(n);
1810
1827
  }
1828
+ offloadReachableToolsRef.current = () => {
1829
+ const s = new Set(tools.map((t) => t.name));
1830
+ for (const n of deferred)
1831
+ if (!activeTools.has(n))
1832
+ s.delete(n);
1833
+ s.add(TOOL_SEARCH_NAME);
1834
+ return s;
1835
+ };
1811
1836
  let toolSearch;
1812
1837
  const buildToolList = (active) => {
1813
1838
  const list = tools.map((t) => (deferred.has(t.name) && !active.has(t.name) ? placeholders.get(t.name) : t));
@@ -45,7 +45,7 @@ import { structuredFrom, toolOutputFrom } from "./tool-output-projection.js";
45
45
  export { DEFAULT_MAX_TURNS };
46
46
  function createRunState() {
47
47
  return {
48
- telemetry: { cacheFamily: "input-excludes-cached", pricing: { inputPer1M: 0, outputPer1M: 0 }, tracer: undefined, taskId: "", taskStart: 0, taskStartMonotonic: 0, cacheBreakReported: false },
48
+ telemetry: { cacheFamily: "input-excludes-cached", pricing: { inputPer1M: 0, outputPer1M: 0 }, pricingConfigured: true, unpricedSpend: false, tracer: undefined, taskId: "", taskStart: 0, taskStartMonotonic: 0, cacheBreakReported: false },
49
49
  degrade: { degradeToModel: undefined, degraded: undefined, outputErrorStreak: 0, outputInvalid: false, recordDegraded: () => { } },
50
50
  limits: { turnsExceeded: false, budgetHit: undefined, outputRetryCap: 0, effectiveMaxTurns: undefined },
51
51
  budget: { remainingMicroUsd: undefined, maxCostMicroUsd: undefined, overBudget: () => false, streamCancel: false, callOutputChars: 0, lastStreamBudgetCheck: 0, projectedOverBudget: () => false },
@@ -788,6 +788,8 @@ function makeHarnessHandlers(prepared, stats, rs, deps) {
788
788
  const u = m.usage;
789
789
  stats.tokens += u.totalTokens || 0;
790
790
  const { totalInputTokens: turnInput, costMicroUsd: turnCostMicroUsd } = usageCostMicroUsd(rs.telemetry.cacheFamily, u, rs.telemetry.pricing);
791
+ if (!rs.telemetry.pricingConfigured)
792
+ rs.telemetry.unpricedSpend = true;
791
793
  stats.promptTokens += turnInput;
792
794
  stats.cachedTokens += u.cacheRead || 0;
793
795
  stats.cacheWriteTokens += u.cacheWrite || 0;
@@ -826,7 +828,7 @@ function makeHarnessHandlers(prepared, stats, rs, deps) {
826
828
  latencyMs: rs.turn.callStartAt !== undefined ? callEndAt - rs.turn.callStartAt : 0,
827
829
  firstTokenMs: rs.turn.callStartAt !== undefined && rs.turn.firstTokenAt !== undefined ? rs.turn.firstTokenAt - rs.turn.callStartAt : undefined,
828
830
  ...(callIssuedAt !== undefined ? { callStartedAt: callIssuedAt } : {}),
829
- costMicroUsd: turnCostMicroUsd,
831
+ ...(rs.telemetry.pricingConfigured ? { costMicroUsd: turnCostMicroUsd } : {}),
830
832
  ...(capLast !== undefined ? { callCap: capLast.cap, capThinkingSkipped: capLast.thinkingSkipped } : {}),
831
833
  ...(m.role === "assistant" && typeof m.stopReason === "string"
832
834
  ? { stopReason: m.stopReason }
@@ -1628,6 +1630,7 @@ export class Runner {
1628
1630
  const rs = createRunState();
1629
1631
  rs.telemetry.cacheFamily = cacheFamilyOf(prepared.model);
1630
1632
  rs.telemetry.pricing = this.deps.pricing?.[prepared.model.id] ?? modelCostToPricing(prepared.model.cost);
1633
+ rs.telemetry.pricingConfigured = this.deps.pricing?.[prepared.model.id] !== undefined || prepared.model.cost !== undefined;
1631
1634
  if (spec.degrade) {
1632
1635
  try {
1633
1636
  rs.degrade.degradeToModel = resolveModel(spec.degrade.to, this.deps.models);
@@ -1669,11 +1672,17 @@ export class Runner {
1669
1672
  }
1670
1673
  if (m) {
1671
1674
  rs.telemetry.pricing = this.deps.pricing?.[m.id] ?? modelCostToPricing(m.cost);
1675
+ rs.telemetry.pricingConfigured = this.deps.pricing?.[m.id] !== undefined || m.cost !== undefined;
1672
1676
  rs.telemetry.cacheFamily = cacheFamilyOf(m);
1673
1677
  }
1674
1678
  else {
1675
- if (this.deps.pricing?.[info.to])
1679
+ if (this.deps.pricing?.[info.to]) {
1676
1680
  rs.telemetry.pricing = this.deps.pricing[info.to];
1681
+ rs.telemetry.pricingConfigured = true;
1682
+ }
1683
+ else {
1684
+ rs.telemetry.pricingConfigured = false;
1685
+ }
1677
1686
  rs.telemetry.cacheFamily = "input-excludes-cached";
1678
1687
  }
1679
1688
  rs.degrade.degraded = info;
@@ -2076,6 +2085,9 @@ export class Runner {
2076
2085
  return;
2077
2086
  const fam = cacheFamilyOf(m);
2078
2087
  const price = this.deps.pricing?.[m.id] ?? modelCostToPricing(m.cost);
2088
+ const priced = this.deps.pricing?.[m.id] !== undefined || m.cost !== undefined;
2089
+ if (!priced)
2090
+ rs.telemetry.unpricedSpend = true;
2079
2091
  const { totalInputTokens, costMicroUsd } = usageCostMicroUsd(fam, u, price);
2080
2092
  stats.tokens += u.totalTokens || 0;
2081
2093
  stats.promptTokens += totalInputTokens;
@@ -2088,7 +2100,7 @@ export class Runner {
2088
2100
  kind: "brain.call", version: 1, taskId: rs.telemetry.taskId, model: m.id, provider: m.provider,
2089
2101
  promptTokens: totalInputTokens, completionTokens: u.output || 0,
2090
2102
  cacheRead: u.cacheRead || 0, cacheWrite: u.cacheWrite || 0,
2091
- latencyMs: 0, costMicroUsd, ...(typeof msg?.stopReason === "string" ? { stopReason: msg.stopReason } : {}), ts: Date.now(),
2103
+ latencyMs: 0, ...(priced ? { costMicroUsd } : {}), ...(typeof msg?.stopReason === "string" ? { stopReason: msg.stopReason } : {}), ts: Date.now(),
2092
2104
  }));
2093
2105
  };
2094
2106
  const compactionBrain = {
@@ -2717,6 +2729,7 @@ export class Runner {
2717
2729
  const result = assembleResult(spec, prepared.sessionId, final, stats, {
2718
2730
  threw,
2719
2731
  model: prepared.model.id,
2732
+ unpricedSpend: rs.telemetry.unpricedSpend,
2720
2733
  abortedForTimeout: timeout.fired,
2721
2734
  abortedForTurns: rs.limits.turnsExceeded,
2722
2735
  abortedLive,
@@ -2835,7 +2848,7 @@ export class Runner {
2835
2848
  errorCode: result.errorCode,
2836
2849
  turns: stats.turns,
2837
2850
  tokens: stats.tokens,
2838
- costMicroUsd: stats.costMicroUsd,
2851
+ ...(rs.telemetry.unpricedSpend ? {} : { costMicroUsd: stats.costMicroUsd }),
2839
2852
  durationMs: Date.now() - rs.telemetry.taskStart,
2840
2853
  ...(prepared.outputRef.set ? { hasStructuredOutput: true } : {}),
2841
2854
  ...(stats.mechanisms !== undefined
@@ -57,10 +57,11 @@ export const toolOutputFrom = (result) => {
57
57
  return { output: raw, truncated: true, totalChars };
58
58
  };
59
59
  const CC_DETAIL_TYPES = new Set([
60
- "edit", "multiedit", "create", "update", "bash", "notebook-edit", "notebook", "text", "grep", "glob", "mcp",
60
+ "edit", "multiedit", "create", "update", "bash", "notebook-edit", "notebook", "file_unchanged", "worktree", "text", "grep", "glob", "mcp",
61
61
  "agent", "task", "task-list", "task-output", "memory-saved", "workflow-run",
62
62
  "web-fetch", "web-search", "todo", "cron-create", "cron-delete", "cron-list", "image",
63
63
  "task-stop", "tool-search", "memory-recall", "repo-map", "fork", "enter-plan-mode", "exit-plan-mode",
64
+ "monitor-start", "path_not_in_root",
64
65
  ]);
65
66
  export const structuredFrom = (result) => {
66
67
  const details = result !== null && typeof result === "object" ? result.details : undefined;
@@ -0,0 +1,37 @@
1
+ import { type Checkpoint, type CheckpointStore, type ResumeOutcome } from "../checkpoint-store.js";
2
+ import { type ContractAssertionRunner } from "./contract-harness.js";
3
+ export declare function createCheckpointFixture(over?: Partial<Checkpoint>): Checkpoint;
4
+ export declare const ALLOW: Extract<ResumeOutcome, {
5
+ gate: "policy_ask";
6
+ }>;
7
+ export declare function checkpointStoreContract(make: () => CheckpointStore, runAssertion?: ContractAssertionRunner): Promise<void>;
8
+ export declare function checkpointListByScopeSummaries(store: CheckpointStore): Promise<import("../checkpoint-store.js").CheckpointSummary[]>;
9
+ export declare const EXPECTED_LISTBYSCOPE_SUMMARIES: ({
10
+ token: string;
11
+ sessionId: string;
12
+ scope: string;
13
+ gateKind: string;
14
+ severity: number;
15
+ deadline: number;
16
+ createdAt: number;
17
+ sourceTaskId: string;
18
+ principal: string;
19
+ toolCallId: string;
20
+ toolName: string;
21
+ toolInput: string;
22
+ spentMicroUsd?: undefined;
23
+ } | {
24
+ token: string;
25
+ sessionId: string;
26
+ scope: string;
27
+ gateKind: string;
28
+ spentMicroUsd: number;
29
+ createdAt: number;
30
+ severity?: undefined;
31
+ deadline?: undefined;
32
+ sourceTaskId?: undefined;
33
+ principal?: undefined;
34
+ toolCallId?: undefined;
35
+ toolName?: undefined;
36
+ toolInput?: undefined;
37
+ })[];
@@ -0,0 +1,195 @@
1
+ import { strict as assert } from "node:assert";
2
+ import { mintCheckpointToken, } from "../checkpoint-store.js";
3
+ import { beginContract } from "./contract-harness.js";
4
+ export function createCheckpointFixture(over = {}) {
5
+ const token = over.token ?? mintCheckpointToken();
6
+ return {
7
+ token,
8
+ scope: "tenant-a",
9
+ sessionId: "sess-1",
10
+ leafId: "leaf-1",
11
+ gate: { kind: "human", reason: "approve", toolName: "Write" },
12
+ pendingAction: {
13
+ kind: "tool_approval",
14
+ toolCallId: "call-3",
15
+ toolName: "Write",
16
+ args: { path: "/x", content: "y" },
17
+ boundInputHash: "h0",
18
+ batchToolCallIds: ["call-3"],
19
+ completedCallIds: [],
20
+ },
21
+ state: {
22
+ activeTools: [],
23
+ nestedStats: { tokens: 0, turns: 0, tasks: 0, costMicroUsd: 0 },
24
+ },
25
+ status: "pending",
26
+ createdAt: 1_700_000_000_000,
27
+ ...over,
28
+ };
29
+ }
30
+ export const ALLOW = {
31
+ gate: "policy_ask",
32
+ decision: "allow",
33
+ boundCallId: "call-3",
34
+ boundInputHash: "h0",
35
+ };
36
+ export async function checkpointStoreContract(make, runAssertion) {
37
+ const { run, settle } = beginContract(runAssertion);
38
+ run("kit prerequisites: reopen/setPendingSteer/listByScope are implemented (REQUIRED by this kit)", async () => {
39
+ const probe = make();
40
+ const missing = ["reopen", "setPendingSteer", "listByScope"].filter((m) => typeof probe[m] !== "function");
41
+ assert.equal(missing.length, 0, `backend does not implement ${missing.join(", ")} — required for the CheckpointStore contract kit`);
42
+ });
43
+ run("put create-once → already_exists", async () => {
44
+ const store = make();
45
+ const cp = createCheckpointFixture();
46
+ await store.put(cp.token, cp);
47
+ await assert.rejects(store.put(cp.token, cp), (e) => e.code === "checkpoint.already_exists");
48
+ });
49
+ run("resolve is an atomic CAS: first wins, double-resume false", async () => {
50
+ const store = make();
51
+ const cp = createCheckpointFixture();
52
+ await store.put(cp.token, cp);
53
+ assert.equal(await store.resolve(cp.token, cp.scope, ALLOW), true);
54
+ assert.equal(await store.resolve(cp.token, cp.scope, ALLOW), false);
55
+ assert.equal((await store.get(cp.token)).status, "resolved");
56
+ });
57
+ run("wrong-scope resolve never wins (multi-tenant isolation)", async () => {
58
+ const store = make();
59
+ const cp = createCheckpointFixture({ scope: "tenant-a" });
60
+ await store.put(cp.token, cp);
61
+ assert.equal(await store.resolve(cp.token, "tenant-b", ALLOW), false);
62
+ assert.equal((await store.get(cp.token)).status, "pending");
63
+ });
64
+ run("resolve records the winner; reopen(env_failed) preserves it + bumps rev", async () => {
65
+ const store = make();
66
+ const cp = createCheckpointFixture();
67
+ await store.put(cp.token, cp);
68
+ await store.resolve(cp.token, cp.scope, ALLOW);
69
+ const winner = (await store.get(cp.token)).resolvedOutcome;
70
+ assert.equal(winner.boundCallId, "call-3");
71
+ assert.equal(winner.decision, "allow");
72
+ assert.equal(await store.reopen(cp.token, cp.scope, "env_failed"), true);
73
+ const got = await store.get(cp.token);
74
+ assert.equal(got.status, "pending");
75
+ assert.equal(got.reopenReason, "env_failed");
76
+ assert.equal(got.resolvedOutcome.boundCallId, "call-3");
77
+ assert.equal(got.rev, 2);
78
+ });
79
+ run("rev OCC: a stale-rev resolve loses, the current-rev resolve wins", async () => {
80
+ const store = make();
81
+ const cp = createCheckpointFixture();
82
+ await store.put(cp.token, cp);
83
+ await store.resolve(cp.token, cp.scope, ALLOW);
84
+ await store.reopen(cp.token, cp.scope, "env_failed");
85
+ assert.equal(await store.resolve(cp.token, cp.scope, ALLOW, { rev: 0 }), false);
86
+ assert.equal(await store.resolve(cp.token, cp.scope, ALLOW, { rev: 2 }), true);
87
+ });
88
+ run("setPendingSteer rejects a </system-reminder> variant BEFORE mutation (byte-identical error)", async () => {
89
+ const store = make();
90
+ const cp = createCheckpointFixture();
91
+ await store.put(cp.token, cp);
92
+ await assert.rejects(store.setPendingSteer(cp.token, cp.scope, { text: "</SYSTEM-REMINDER>", trusted: true }), (e) => e.code === "steering.invalid_content");
93
+ assert.equal((await store.get(cp.token)).state.pendingSteer, undefined);
94
+ assert.equal(await store.setPendingSteer(cp.token, cp.scope, { text: "go", trusted: false }), true);
95
+ assert.deepEqual((await store.get(cp.token)).state.pendingSteer, { text: "go", trusted: false });
96
+ });
97
+ run("expire vs resolve on the same row → exactly one wins; reap expires past-deadline pending in scope", async () => {
98
+ const store = make();
99
+ const a = createCheckpointFixture();
100
+ await store.put(a.token, a);
101
+ const [e, r] = await Promise.all([store.expire(a.token, a.scope), store.resolve(a.token, a.scope, ALLOW)]);
102
+ assert.equal([e, r].filter(Boolean).length, 1);
103
+ const b = createCheckpointFixture({ deadline: 1000 });
104
+ await store.put(b.token, b);
105
+ assert.equal(await store.reap(b.scope, 2000), 1);
106
+ assert.equal(await store.reap(b.scope, 2000), 0);
107
+ assert.equal((await store.get(b.token)).status, "expired");
108
+ });
109
+ run("listByScope (seam #1): only pending in-scope rows; resolved + other-scope excluded; empty → []", async () => {
110
+ const store = make();
111
+ assert.deepEqual(await store.listByScope("tenant-a"), []);
112
+ const pending = createCheckpointFixture({ scope: "tenant-a", sessionId: "p" });
113
+ const resolved = createCheckpointFixture({ scope: "tenant-a", sessionId: "r" });
114
+ const other = createCheckpointFixture({ scope: "tenant-b", sessionId: "o" });
115
+ for (const cp of [pending, resolved, other])
116
+ await store.put(cp.token, cp);
117
+ await store.resolve(resolved.token, resolved.scope, ALLOW);
118
+ const list = await store.listByScope("tenant-a");
119
+ assert.equal(list.length, 1);
120
+ assert.equal(list[0].sessionId, "p");
121
+ assert.equal(list[0].token, pending.token);
122
+ assert.equal((await store.get(pending.token)).status, "pending");
123
+ });
124
+ await settle();
125
+ }
126
+ const sortByToken = (a, b) => a.token.localeCompare(b.token);
127
+ export async function checkpointListByScopeSummaries(store) {
128
+ if (typeof store.listByScope !== "function") {
129
+ throw new Error("backend does not implement listByScope — required for checkpointListByScopeSummaries");
130
+ }
131
+ const escalation = createCheckpointFixture({
132
+ token: "tok-escalation",
133
+ scope: "tenant-a",
134
+ sessionId: "needs-approval",
135
+ deadline: 1_999_999_999_999,
136
+ createdAt: 1_700_000_000_000,
137
+ sourceTaskId: "needs-approval",
138
+ principal: "alice@corp",
139
+ gate: {
140
+ kind: "human",
141
+ reason: "approve",
142
+ toolName: "open_pr",
143
+ riskDescriptor: { severity: 5, axes: { egress: true, irreversible: true }, toolName: "open_pr" },
144
+ },
145
+ pendingAction: {
146
+ kind: "tool_approval",
147
+ toolCallId: "call-pr",
148
+ toolName: "open_pr",
149
+ args: { repo: "x" },
150
+ boundInputHash: "h0",
151
+ batchToolCallIds: ["call-pr"],
152
+ completedCallIds: [],
153
+ },
154
+ });
155
+ const resource = createCheckpointFixture({
156
+ token: "tok-resource",
157
+ scope: "tenant-a",
158
+ sessionId: "out-of-budget",
159
+ createdAt: 1_700_000_000_001,
160
+ gate: { kind: "resource_limit", reason: "budget" },
161
+ pendingAction: { kind: "resource_limit", reason: "budget" },
162
+ resourceLedger: { totalBudgetMicroUsd: 10_000_000, spentMicroUsd: 4_200_000, spentTokens: 9, spentTurns: 3, sliceCount: 2 },
163
+ });
164
+ const resolved = createCheckpointFixture({ token: "tok-resolved", scope: "tenant-a", sessionId: "done" });
165
+ const other = createCheckpointFixture({ token: "tok-other", scope: "tenant-b", sessionId: "x" });
166
+ const fixture = [escalation, resource, resolved, other];
167
+ for (const cp of fixture)
168
+ await store.put(cp.token, cp);
169
+ await store.resolve(resolved.token, resolved.scope, ALLOW);
170
+ return (await store.listByScope("tenant-a")).sort(sortByToken);
171
+ }
172
+ export const EXPECTED_LISTBYSCOPE_SUMMARIES = [
173
+ {
174
+ token: "tok-escalation",
175
+ sessionId: "needs-approval",
176
+ scope: "tenant-a",
177
+ gateKind: "human",
178
+ severity: 5,
179
+ deadline: 1_999_999_999_999,
180
+ createdAt: 1_700_000_000_000,
181
+ sourceTaskId: "needs-approval",
182
+ principal: "alice@corp",
183
+ toolCallId: "call-pr",
184
+ toolName: "open_pr",
185
+ toolInput: '{"repo":"x"}',
186
+ },
187
+ {
188
+ token: "tok-resource",
189
+ sessionId: "out-of-budget",
190
+ scope: "tenant-a",
191
+ gateKind: "resource_limit",
192
+ spentMicroUsd: 4_200_000,
193
+ createdAt: 1_700_000_000_001,
194
+ },
195
+ ];
@@ -0,0 +1,6 @@
1
+ export type ContractAssertionRunner = (name: string, fn: () => Promise<void>) => void | Promise<void>;
2
+ export declare function defaultSequentialRunner(_name: string, fn: () => Promise<void>): Promise<void>;
3
+ export declare function beginContract(runAssertion?: ContractAssertionRunner): {
4
+ run: (name: string, fn: () => Promise<void>) => void;
5
+ settle: () => Promise<void>;
6
+ };