@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.
- package/dist/agents/send-message-tool.d.ts +4 -0
- package/dist/agents/send-message-tool.js +37 -24
- package/dist/agents/subagent.js +275 -127
- package/dist/agents/teacher.js +51 -23
- package/dist/brain/errors.d.ts +1 -0
- package/dist/brain/errors.js +14 -0
- package/dist/brain/stream-engine.js +3 -3
- package/dist/core/context-edit.js +2 -1
- package/dist/core/runner/assemble-result.d.ts +1 -0
- package/dist/core/runner/assemble-result.js +12 -8
- package/dist/core/runner/prepare-task.js +32 -7
- package/dist/core/runner/runtask.js +18 -5
- package/dist/core/runner/tool-output-projection.js +2 -1
- package/dist/core/store-contracts/checkpoint-store-contract.d.ts +37 -0
- package/dist/core/store-contracts/checkpoint-store-contract.js +195 -0
- package/dist/core/store-contracts/contract-harness.d.ts +6 -0
- package/dist/core/store-contracts/contract-harness.js +16 -0
- package/dist/core/store-contracts/contract-kit-version.d.ts +1 -0
- package/dist/core/store-contracts/contract-kit-version.js +2 -0
- package/dist/core/store-contracts/file-snapshot-store-contract.d.ts +3 -0
- package/dist/core/store-contracts/file-snapshot-store-contract.js +126 -0
- package/dist/core/store-contracts/mailbox-store-contract.d.ts +6 -0
- package/dist/core/store-contracts/mailbox-store-contract.js +193 -0
- package/dist/core/store-contracts/session-repo-contract.d.ts +3 -0
- package/dist/core/store-contracts/session-repo-contract.js +36 -0
- package/dist/core/store-contracts/tool-result-store-contract.d.ts +3 -0
- package/dist/core/store-contracts/tool-result-store-contract.js +35 -0
- package/dist/core/task-notification.d.ts +2 -0
- package/dist/core/task-registry-agent.d.ts +7 -1
- package/dist/core/task-registry-agent.js +31 -2
- package/dist/core/task-registry-monitor.js +71 -6
- package/dist/core/task-registry-shared.d.ts +18 -1
- package/dist/core/task-registry-shared.js +5 -2
- package/dist/core/task-registry.d.ts +9 -0
- package/dist/core/task-registry.js +52 -7
- package/dist/core/tool-result-store.d.ts +3 -2
- package/dist/core/tool-result-store.js +12 -4
- package/dist/core/tools.d.ts +2 -0
- package/dist/core/tools.js +9 -0
- package/dist/core/trace.d.ts +7 -0
- package/dist/core/workflow-journal-store.d.ts +16 -0
- package/dist/core/workflow-journal-store.js +28 -0
- package/dist/engine/lsp/node-lsp-manager.d.ts +2 -0
- package/dist/engine/lsp/node-lsp-manager.js +16 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/orchestration/builtin-workflows.d.ts +1 -1
- package/dist/orchestration/builtin-workflows.js +11 -2
- package/dist/orchestration/workflow-governance.d.ts +6 -1
- package/dist/orchestration/workflow-governance.js +24 -4
- package/dist/orchestration/workflow-primitives.js +7 -1
- package/dist/orchestration/workflow.d.ts +16 -0
- package/dist/orchestration/workflow.js +94 -21
- package/dist/tools/fs/fs-bash.d.ts +7 -1
- package/dist/tools/fs/fs-bash.js +51 -20
- package/dist/tools/fs/fs-read.js +22 -11
- package/dist/tools/fs/fs-search-tools.js +3 -3
- package/dist/tools/fs/fs-shared.d.ts +20 -7
- package/dist/tools/fs/fs-shared.js +17 -3
- package/dist/tools/fs/fs-write.js +4 -4
- package/dist/tools/fs/index.d.ts +2 -0
- package/dist/tools/fs/index.js +7 -1
- package/dist/tools/fs/repo-map.js +2 -2
- package/dist/tools/fs/safety.d.ts +10 -0
- package/dist/tools/fs/safety.js +15 -1
- package/dist/tools/monitor.d.ts +2 -0
- package/dist/tools/monitor.js +20 -4
- package/dist/tools/web.js +6 -2
- package/dist/tools/worktree.js +46 -25
- package/package.json +1 -1
package/dist/agents/teacher.js
CHANGED
|
@@ -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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
?
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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 }
|
package/dist/brain/errors.d.ts
CHANGED
|
@@ -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 {};
|
package/dist/brain/errors.js
CHANGED
|
@@ -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",
|
|
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: ${
|
|
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) =>
|
|
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) {
|
|
@@ -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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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) =>
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
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
|
+
};
|