@ny4n/durable 1.2.0 → 1.3.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/LICENSE +1 -0
- package/README.md +41 -5
- package/dist/codemode/codemode.d.ts +132 -0
- package/dist/codemode/codemode.d.ts.map +1 -0
- package/dist/codemode/codemode.js +791 -0
- package/dist/codemode/codemode.js.map +1 -0
- package/dist/codemode/describe.d.ts +38 -0
- package/dist/codemode/describe.d.ts.map +1 -0
- package/dist/codemode/describe.js +117 -0
- package/dist/codemode/describe.js.map +1 -0
- package/dist/codemode/index.d.ts +3 -0
- package/dist/codemode/index.d.ts.map +1 -0
- package/dist/codemode/index.js +3 -0
- package/dist/codemode/index.js.map +1 -0
- package/dist/codemode/output.d.ts +39 -0
- package/dist/codemode/output.d.ts.map +1 -0
- package/dist/codemode/output.js +160 -0
- package/dist/codemode/output.js.map +1 -0
- package/dist/harness/agent.d.ts +4 -2
- package/dist/harness/agent.d.ts.map +1 -1
- package/dist/harness/agent.js +56 -26
- package/dist/harness/agent.js.map +1 -1
- package/dist/harness/compaction.d.ts +6 -1
- package/dist/harness/compaction.d.ts.map +1 -1
- package/dist/harness/compaction.js +10 -7
- package/dist/harness/compaction.js.map +1 -1
- package/dist/harness/events.d.ts +8 -2
- package/dist/harness/events.d.ts.map +1 -1
- package/dist/harness/events.js +4 -1
- package/dist/harness/events.js.map +1 -1
- package/dist/harness/generation.d.ts +2 -0
- package/dist/harness/generation.d.ts.map +1 -1
- package/dist/harness/generation.js +25 -4
- package/dist/harness/generation.js.map +1 -1
- package/dist/harness/inbox.d.ts +16 -4
- package/dist/harness/inbox.d.ts.map +1 -1
- package/dist/harness/inbox.js +26 -3
- package/dist/harness/inbox.js.map +1 -1
- package/dist/harness/live.d.ts +15 -1
- package/dist/harness/live.d.ts.map +1 -1
- package/dist/harness/live.js +10 -2
- package/dist/harness/live.js.map +1 -1
- package/dist/harness/scheduler.d.ts.map +1 -1
- package/dist/harness/scheduler.js +72 -10
- package/dist/harness/scheduler.js.map +1 -1
- package/dist/harness/submissions.d.ts +7 -4
- package/dist/harness/submissions.d.ts.map +1 -1
- package/dist/harness/submissions.js +29 -10
- package/dist/harness/submissions.js.map +1 -1
- package/dist/harness/tool.d.ts.map +1 -1
- package/dist/harness/tool.js +13 -1
- package/dist/harness/tool.js.map +1 -1
- package/dist/harness/types.d.ts +38 -11
- package/dist/harness/types.d.ts.map +1 -1
- package/dist/harness/types.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/tool-search/index.d.ts +2 -0
- package/dist/tool-search/index.d.ts.map +1 -0
- package/dist/tool-search/index.js +2 -0
- package/dist/tool-search/index.js.map +1 -0
- package/dist/tool-search/tool-search.d.ts +18 -0
- package/dist/tool-search/tool-search.d.ts.map +1 -0
- package/dist/tool-search/tool-search.js +62 -0
- package/dist/tool-search/tool-search.js.map +1 -0
- package/package.json +16 -3
|
@@ -0,0 +1,791 @@
|
|
|
1
|
+
import { copyJson } from "@ny4n/chord";
|
|
2
|
+
import { toCodemodeIdentifier, } from "@ny4n/codemode/portable";
|
|
3
|
+
import { Bm25Ranker } from "@ny4n/codemode/search";
|
|
4
|
+
import { CODEMODE_SOURCE_GRAMMAR, parseCodemodeSource } from "@ny4n/codemode/source";
|
|
5
|
+
import { Type } from "typebox";
|
|
6
|
+
import { defineDoc } from "../documents.js";
|
|
7
|
+
import { errorMessage } from "../errors.js";
|
|
8
|
+
import { defineExtension, defineTool, section } from "../harness/define.js";
|
|
9
|
+
import { NestedCallDoc } from "../harness/tool.js";
|
|
10
|
+
import { codemodeDescription, codemodeSection, DEFAULT_CODEMODE_INLINE_BUDGET, namespaceOf, sampleOf, } from "./describe.js";
|
|
11
|
+
import { DEFAULT_MAX_OUTPUT_TOKENS, formatResult } from "./output.js";
|
|
12
|
+
/**
|
|
13
|
+
* Version of everything in this module that turns stored results into what a script sees: result mapping, globals,
|
|
14
|
+
* start data. Bump it when that changes, so a call started before an upgrade is not replayed by different code.
|
|
15
|
+
*/
|
|
16
|
+
export const CODEMODE_MODULE_VERSION = 1;
|
|
17
|
+
export const CODEMODE_TOOL_NAME = "codemode";
|
|
18
|
+
export const CodemodeRunDoc = defineDoc({
|
|
19
|
+
kind: "nyan.codemode.run",
|
|
20
|
+
version: 1,
|
|
21
|
+
scope: "task",
|
|
22
|
+
initial: () => ({}),
|
|
23
|
+
});
|
|
24
|
+
export const CodemodeJournalDoc = defineDoc({
|
|
25
|
+
kind: "nyan.codemode.journal",
|
|
26
|
+
version: 1,
|
|
27
|
+
scope: "task",
|
|
28
|
+
initial: () => ({ entries: [] }),
|
|
29
|
+
});
|
|
30
|
+
/**
|
|
31
|
+
* What scripts keep with `store()` and read with `load()`, per conversation. Rewindable and copied as of the fork
|
|
32
|
+
* point, so a fork sees the values as they were when its entry was committed.
|
|
33
|
+
*/
|
|
34
|
+
export const CodemodeStoreDoc = defineDoc({
|
|
35
|
+
kind: "nyan.codemode.store",
|
|
36
|
+
version: 1,
|
|
37
|
+
scope: "conversation",
|
|
38
|
+
history: "rewindable",
|
|
39
|
+
fork: "asOf",
|
|
40
|
+
initial: () => ({ values: {} }),
|
|
41
|
+
});
|
|
42
|
+
const ARGS_PREVIEW_CHARS = 200;
|
|
43
|
+
const ERROR_PREVIEW_CHARS = 500;
|
|
44
|
+
function cut(text, max) {
|
|
45
|
+
return text.length > max ? `${text.slice(0, max - 3)}...` : text;
|
|
46
|
+
}
|
|
47
|
+
const codemodeSchema = Type.Object({ code: Type.String({ description: "Raw JavaScript source." }) });
|
|
48
|
+
/** Outputs bounded by the script's own token budget; the Harness's limits only stop a runaway. */
|
|
49
|
+
const OUTPUT_LIMITS = { maxBytes: 4 * 1024 * 1024, maxLines: 100_000 };
|
|
50
|
+
/** Globals for finding tools, answered from the start data, so a replay sees what the first run saw. */
|
|
51
|
+
const DISCOVERY_GLOBALS = ["searchTools", "describeTool", "describeNamespace"];
|
|
52
|
+
const DEFAULT_SEARCH_LIMIT = 8;
|
|
53
|
+
/** The code mode tool. Scripts call the conversation's callable tools as `tools.<name>(args)`. */
|
|
54
|
+
export function createCodemodeTool(options) {
|
|
55
|
+
return defineTool({
|
|
56
|
+
name: CODEMODE_TOOL_NAME,
|
|
57
|
+
description: codemodeDescription(),
|
|
58
|
+
parameters: codemodeSchema,
|
|
59
|
+
outputLimits: OUTPUT_LIMITS,
|
|
60
|
+
// Capable models write the script as raw text instead of a JSON-escaped string; nyan-ai applies it only for them.
|
|
61
|
+
constrainedSampling: { type: "grammar", variants: { openai_lark: CODEMODE_SOURCE_GRAMMAR } },
|
|
62
|
+
callers: ["model"],
|
|
63
|
+
replay: "safe",
|
|
64
|
+
execute: (args, api, context) => new CodemodeCall(options, api, context).execute(args.code),
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* The `codemode` extension: the code mode tool, and the `codemode` prompt section listing the tools scripts can call.
|
|
69
|
+
*/
|
|
70
|
+
export function createCodemodeExtension(options) {
|
|
71
|
+
return defineExtension({
|
|
72
|
+
name: "codemode",
|
|
73
|
+
tools: [createCodemodeTool(options)],
|
|
74
|
+
sections: [
|
|
75
|
+
section("codemode", (input) => codemodeSection(
|
|
76
|
+
// Deferred tools are found with `searchTools()`, as the coding agent leaves them out too.
|
|
77
|
+
input.agent.callable.filter((tool) => tool.deferred !== true), options.namespaces ?? [], options.inlineBudget ?? DEFAULT_CODEMODE_INLINE_BUDGET)),
|
|
78
|
+
],
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* What a script gets for a nested call, as in the coding agent. A tool with a schema resolves with its value, also on
|
|
83
|
+
* an error result that carries one (`bash`'s `{ output, exitCode }` for a nonzero exit); a schema-less tool
|
|
84
|
+
* resolves with its text output, images left out, as its declaration says. Any other error rejects, with its text: a
|
|
85
|
+
* schema-less tool's output, else its error diagnostics.
|
|
86
|
+
*/
|
|
87
|
+
function outcomeOf(name, structured, result) {
|
|
88
|
+
const value = structured ? result.structuredOutput : textOf(result.structuredOutput);
|
|
89
|
+
if (!result.isError || (structured && value !== undefined))
|
|
90
|
+
return { ok: true, value };
|
|
91
|
+
const message = structured ? "" : value;
|
|
92
|
+
const diagnostics = result.diagnostics
|
|
93
|
+
.filter((diagnostic) => diagnostic.severity === "error")
|
|
94
|
+
.map((diagnostic) => diagnostic.message)
|
|
95
|
+
.join("\n");
|
|
96
|
+
return {
|
|
97
|
+
ok: false,
|
|
98
|
+
message: message || diagnostics || `Tool ${name} failed`,
|
|
99
|
+
...(value === undefined ? {} : { properties: { structuredOutput: value } }),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
/** The text of a schema-less tool's collapsed output: a string, one image, or an array of text and image blocks. */
|
|
103
|
+
function textOf(value) {
|
|
104
|
+
if (typeof value === "string")
|
|
105
|
+
return value;
|
|
106
|
+
if (!Array.isArray(value))
|
|
107
|
+
return "";
|
|
108
|
+
return value.flatMap((item) => (isText(item) ? [item.text] : [])).join("\n");
|
|
109
|
+
}
|
|
110
|
+
function isText(item) {
|
|
111
|
+
return typeof item === "object" && item !== null && item.type === "text";
|
|
112
|
+
}
|
|
113
|
+
function errorResult(message) {
|
|
114
|
+
return { output: [{ type: "text", text: message }], isError: true };
|
|
115
|
+
}
|
|
116
|
+
/** One execution of the code mode tool: a first run, or a rerun after a restart that replays the journal. */
|
|
117
|
+
class CodemodeCall {
|
|
118
|
+
options;
|
|
119
|
+
api;
|
|
120
|
+
context;
|
|
121
|
+
controller = new AbortController();
|
|
122
|
+
run;
|
|
123
|
+
start;
|
|
124
|
+
/** The journal as this attempt found it; replay delivers exactly these. */
|
|
125
|
+
replayed = [];
|
|
126
|
+
/** Entries in the journal, replayed and live; the next delivery is `length + 1`. */
|
|
127
|
+
journalLength = 0;
|
|
128
|
+
/** Replayed entries delivered so far. */
|
|
129
|
+
nextReplayed = 0;
|
|
130
|
+
replaying = false;
|
|
131
|
+
/** Whether an earlier attempt ran this call: its nested call keys may already exist. */
|
|
132
|
+
rerun = false;
|
|
133
|
+
/** Whether live calls may launch; before, they are held (see `release()`). */
|
|
134
|
+
released = false;
|
|
135
|
+
/** The count an earlier attempt recorded after the script's first run. */
|
|
136
|
+
firstIssued;
|
|
137
|
+
/** The highest issued count known to be committed: calls up to it may launch. */
|
|
138
|
+
durableIssued = 0;
|
|
139
|
+
/** Live calls waiting for their issued count to be committed. */
|
|
140
|
+
waiting = [];
|
|
141
|
+
/** Journal index of each replayed call ID. */
|
|
142
|
+
replayIndex = new Map();
|
|
143
|
+
/** Calls the VM issued so far; its call IDs are 1, 2, ... */
|
|
144
|
+
issued = 0;
|
|
145
|
+
/** Replay: results of journaled calls, ready to deliver in journal order. */
|
|
146
|
+
ready = new Map();
|
|
147
|
+
/** Replay: calls the journal does not have, admitted once replay is done. */
|
|
148
|
+
held = [];
|
|
149
|
+
/** Live: finished calls waiting for their journal commit. */
|
|
150
|
+
queue = [];
|
|
151
|
+
/** Acknowledged counts to record, by result number (0: the script's first run). */
|
|
152
|
+
acks = new Map();
|
|
153
|
+
committing = false;
|
|
154
|
+
/** Set once the call decided an outcome other than the script's own (divergence, mismatch) or failed. */
|
|
155
|
+
ended;
|
|
156
|
+
failure;
|
|
157
|
+
/** The script's tool calls for `details`, by VM call ID. */
|
|
158
|
+
rows = new Map();
|
|
159
|
+
/** Resolves once the VM's fingerprint is checked or recorded; nothing is delivered or admitted before. */
|
|
160
|
+
gate;
|
|
161
|
+
openGate;
|
|
162
|
+
constructor(options, api, context) {
|
|
163
|
+
this.options = options;
|
|
164
|
+
this.api = api;
|
|
165
|
+
this.context = context;
|
|
166
|
+
this.gate = new Promise((resolve) => {
|
|
167
|
+
this.openGate = resolve;
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
async execute(source) {
|
|
171
|
+
const { code, options: sourceOptions } = parseCodemodeSource(source);
|
|
172
|
+
const { api, context } = this;
|
|
173
|
+
const agent = await api.agent(context);
|
|
174
|
+
const tools = agent.callable.map((tool) => ({
|
|
175
|
+
name: tool.name,
|
|
176
|
+
jsName: toCodemodeIdentifier(tool.name),
|
|
177
|
+
description: sampleOf(tool),
|
|
178
|
+
...(tool.structuredOutputSchema === undefined ? {} : { structured: true }),
|
|
179
|
+
}));
|
|
180
|
+
const callable = agent.callable;
|
|
181
|
+
const globals = [
|
|
182
|
+
...DISCOVERY_GLOBALS.map((name) => ({ name, spread: true })),
|
|
183
|
+
...(this.options.globals ?? []).map((global) => ({ name: global.name, spread: global.spread === true })),
|
|
184
|
+
];
|
|
185
|
+
const namespaces = [];
|
|
186
|
+
for (const tool of callable) {
|
|
187
|
+
const namespace = namespaceOf(tool.name, this.options.namespaces ?? []);
|
|
188
|
+
if (namespace === undefined)
|
|
189
|
+
continue;
|
|
190
|
+
let entry = namespaces.find((candidate) => candidate.name === namespace.name);
|
|
191
|
+
if (entry === undefined) {
|
|
192
|
+
entry = {
|
|
193
|
+
name: namespace.name,
|
|
194
|
+
...(namespace.description === undefined ? {} : { description: namespace.description }),
|
|
195
|
+
...(namespace.instructions === undefined ? {} : { instructions: namespace.instructions }),
|
|
196
|
+
tools: [],
|
|
197
|
+
};
|
|
198
|
+
namespaces.push(entry);
|
|
199
|
+
}
|
|
200
|
+
entry.tools.push(tool.name);
|
|
201
|
+
}
|
|
202
|
+
const now = Date.now();
|
|
203
|
+
const seed = crypto.getRandomValues(new Uint32Array(1))[0];
|
|
204
|
+
const found = await api.commit(async (tx) => {
|
|
205
|
+
const run = await tx.doc(CodemodeRunDoc, api.taskId);
|
|
206
|
+
if (run.result !== undefined)
|
|
207
|
+
return { done: copyJson(run.result) };
|
|
208
|
+
const rerun = run.start !== undefined;
|
|
209
|
+
if (run.start === undefined) {
|
|
210
|
+
const store = await tx.doc(CodemodeStoreDoc, api.conversationId);
|
|
211
|
+
const timeout = sourceOptions.timeoutMs;
|
|
212
|
+
run.start = {
|
|
213
|
+
version: CODEMODE_MODULE_VERSION,
|
|
214
|
+
startedAt: now,
|
|
215
|
+
seed,
|
|
216
|
+
...(timeout === undefined ? {} : { deadline: now + timeout }),
|
|
217
|
+
tools,
|
|
218
|
+
namespaces,
|
|
219
|
+
globals,
|
|
220
|
+
maxOutputTokens: sourceOptions.maxOutputTokens ?? DEFAULT_MAX_OUTPUT_TOKENS,
|
|
221
|
+
store: copyJson(store.values),
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
const journal = await tx.doc(CodemodeJournalDoc, api.taskId);
|
|
225
|
+
return {
|
|
226
|
+
start: copyJson(run.start),
|
|
227
|
+
journal: copyJson(journal.entries),
|
|
228
|
+
firstIssued: journal.firstIssued,
|
|
229
|
+
rerun,
|
|
230
|
+
};
|
|
231
|
+
}, context);
|
|
232
|
+
if ("done" in found)
|
|
233
|
+
return found.done;
|
|
234
|
+
this.start = found.start;
|
|
235
|
+
this.replayed = found.journal;
|
|
236
|
+
this.replayIndex = new Map(found.journal.map((entry, index) => [entry.id, index]));
|
|
237
|
+
this.journalLength = found.journal.length;
|
|
238
|
+
this.replaying = found.journal.length > 0;
|
|
239
|
+
this.rerun = found.rerun;
|
|
240
|
+
this.firstIssued = found.firstIssued;
|
|
241
|
+
// A loop, not spread arguments: a journal can hold more entries than a call takes arguments.
|
|
242
|
+
this.durableIssued = found.journal.reduce((max, entry) => Math.max(max, entry.issuedAfter ?? 0), found.firstIssued ?? 0);
|
|
243
|
+
if (this.start.version !== CODEMODE_MODULE_VERSION) {
|
|
244
|
+
return this.decide(errorResult(`The script cannot resume: it started under code mode version ${this.start.version}, this is ${CODEMODE_MODULE_VERSION}.`));
|
|
245
|
+
}
|
|
246
|
+
const remaining = this.start.deadline === undefined ? undefined : this.start.deadline - Date.now();
|
|
247
|
+
if (remaining !== undefined && remaining <= 0) {
|
|
248
|
+
return this.decide(errorResult(`Script timed out: its deadline passed while it was interrupted.`));
|
|
249
|
+
}
|
|
250
|
+
const onAbort = () => this.controller.abort(context.abortSignal?.reason);
|
|
251
|
+
context.abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
252
|
+
if (context.abortSignal?.aborted)
|
|
253
|
+
onAbort();
|
|
254
|
+
try {
|
|
255
|
+
this.run = this.options.sandbox.start(code, {
|
|
256
|
+
startedAt: this.start.startedAt,
|
|
257
|
+
seed: this.start.seed,
|
|
258
|
+
tools: this.start.tools,
|
|
259
|
+
globals: this.start.globals,
|
|
260
|
+
store: this.start.store,
|
|
261
|
+
signal: this.controller.signal,
|
|
262
|
+
...(remaining === undefined ? {} : { timeoutMs: remaining }),
|
|
263
|
+
onStarted: (fingerprint) => void this.checkFingerprint(fingerprint),
|
|
264
|
+
onCall: (call) => this.onCall(call),
|
|
265
|
+
onIdle: (seq, issued) => this.onIdle(seq, issued),
|
|
266
|
+
});
|
|
267
|
+
const result = await this.run.result;
|
|
268
|
+
if (this.failure !== undefined)
|
|
269
|
+
throw this.failure;
|
|
270
|
+
if (this.ended !== undefined)
|
|
271
|
+
return this.ended;
|
|
272
|
+
context.abortSignal?.throwIfAborted();
|
|
273
|
+
// Aborted, but not by this call: the host closed the sandbox, for example while shutting down before closing the
|
|
274
|
+
// Harness. That decides nothing about the script: store nothing and end with this invocation, so a later attempt
|
|
275
|
+
// replays it.
|
|
276
|
+
if (!result.ok && result.error.kind === "aborted" && !this.controller.signal.aborted) {
|
|
277
|
+
await new Promise((resolve) => {
|
|
278
|
+
if (context.abortSignal === undefined || context.abortSignal.aborted)
|
|
279
|
+
resolve();
|
|
280
|
+
else
|
|
281
|
+
context.abortSignal.addEventListener("abort", () => resolve(), { once: true });
|
|
282
|
+
});
|
|
283
|
+
context.abortSignal?.throwIfAborted();
|
|
284
|
+
throw new Error("The code mode sandbox was closed while the script ran");
|
|
285
|
+
}
|
|
286
|
+
// A replay that ends where the live run went on is a divergence (design §4.3).
|
|
287
|
+
const during = result.during;
|
|
288
|
+
// Within the replayed part, the first run included: a rerun without journal entries replays that much too.
|
|
289
|
+
if (this.rerun && during !== undefined && during.seq <= this.replayed.length) {
|
|
290
|
+
// The live run went on: it acknowledged that result, or it made a call that later results answer, which the
|
|
291
|
+
// replay never made.
|
|
292
|
+
const continued = (during.seq === 0
|
|
293
|
+
? this.replayed.length > 0 || this.firstIssued !== undefined
|
|
294
|
+
: this.replayed[during.seq - 1]?.issuedAfter !== undefined) ||
|
|
295
|
+
this.replayed.slice(during.seq).some((entry) => entry.id > during.issued);
|
|
296
|
+
if (continued) {
|
|
297
|
+
return this.decide(errorResult(`The script took a different path when resumed after a restart: it ended after result ${during.seq}, where it went on before.`));
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return this.complete(result);
|
|
301
|
+
}
|
|
302
|
+
finally {
|
|
303
|
+
context.abortSignal?.removeEventListener("abort", onAbort);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
/** Store an outcome the call decided, and end the VM if it runs; returns the outcome. */
|
|
307
|
+
async decide(decided) {
|
|
308
|
+
const result = { ...decided, details: this.details() };
|
|
309
|
+
await this.api.commit(async (tx) => {
|
|
310
|
+
const run = await tx.doc(CodemodeRunDoc, this.api.taskId);
|
|
311
|
+
run.result ??= copyJson(result, { omitUndefinedProperties: true });
|
|
312
|
+
}, this.context);
|
|
313
|
+
return result;
|
|
314
|
+
}
|
|
315
|
+
/** End the call with `message`: store it as the outcome, then stop the VM with an error the script cannot catch. */
|
|
316
|
+
diverge(message) {
|
|
317
|
+
if (this.ended !== undefined || this.failure !== undefined)
|
|
318
|
+
return;
|
|
319
|
+
const result = errorResult(`The script took a different path when resumed after a restart: ${message}`);
|
|
320
|
+
this.ended = result;
|
|
321
|
+
this.decide(result).then(() => this.controller.abort(new Error(message)), (error) => this.fail(error));
|
|
322
|
+
}
|
|
323
|
+
/** Whether nothing more may happen: the run has its outcome, or the call decided or failed. */
|
|
324
|
+
get stopped() {
|
|
325
|
+
return this.ended !== undefined || this.failure !== undefined || this.run?.finished === true;
|
|
326
|
+
}
|
|
327
|
+
fail(error) {
|
|
328
|
+
if (this.failure !== undefined)
|
|
329
|
+
return;
|
|
330
|
+
this.failure = error;
|
|
331
|
+
this.controller.abort(error);
|
|
332
|
+
}
|
|
333
|
+
/** The script's own outcome, stored with its store writes in one transaction. */
|
|
334
|
+
async complete(result) {
|
|
335
|
+
const details = this.details();
|
|
336
|
+
const formatted = await formatResult(result, {
|
|
337
|
+
wallMs: Date.now() - this.start.startedAt,
|
|
338
|
+
maxOutputTokens: this.start.maxOutputTokens,
|
|
339
|
+
calls: details.calls,
|
|
340
|
+
limits: OUTPUT_LIMITS,
|
|
341
|
+
write: this.outputFiles(),
|
|
342
|
+
});
|
|
343
|
+
const tool = {
|
|
344
|
+
output: formatted.content,
|
|
345
|
+
isError: !result.ok,
|
|
346
|
+
details: {
|
|
347
|
+
...details,
|
|
348
|
+
...(formatted.fullOutputPath === undefined ? {} : { fullOutputPath: formatted.fullOutputPath }),
|
|
349
|
+
},
|
|
350
|
+
};
|
|
351
|
+
const writes = result.ok ? result.storeWrites : undefined;
|
|
352
|
+
return this.api.commit(async (tx) => {
|
|
353
|
+
const run = await tx.doc(CodemodeRunDoc, this.api.taskId);
|
|
354
|
+
if (run.result !== undefined)
|
|
355
|
+
return run.result;
|
|
356
|
+
run.result = copyJson(tool, { omitUndefinedProperties: true });
|
|
357
|
+
if (writes !== undefined && (Object.keys(writes.set).length > 0 || writes.delete.length > 0)) {
|
|
358
|
+
const store = await tx.doc(CodemodeStoreDoc, this.api.conversationId);
|
|
359
|
+
for (const key of writes.delete)
|
|
360
|
+
delete store.values[key];
|
|
361
|
+
for (const [key, value] of Object.entries(writes.set))
|
|
362
|
+
store.values[key] = value;
|
|
363
|
+
}
|
|
364
|
+
return tool;
|
|
365
|
+
}, this.context);
|
|
366
|
+
}
|
|
367
|
+
/** Writes the result's files under fixed names into the run's output directory, so a rerun overwrites them. */
|
|
368
|
+
outputFiles() {
|
|
369
|
+
const env = this.api.env;
|
|
370
|
+
let directory;
|
|
371
|
+
return async (name, data) => {
|
|
372
|
+
if (env === undefined)
|
|
373
|
+
return { error: "no execution environment" };
|
|
374
|
+
try {
|
|
375
|
+
directory ??= this.outputDir(env);
|
|
376
|
+
const path = `${await directory}/${name}`;
|
|
377
|
+
const written = await env.writeFile(path, data, this.context);
|
|
378
|
+
return written.ok ? { path } : { error: written.error.message };
|
|
379
|
+
}
|
|
380
|
+
catch (error) {
|
|
381
|
+
return { error: errorMessage(error) };
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
/** The run's output directory: the recorded one, else a new temp directory, recorded before any file is written. */
|
|
386
|
+
async outputDir(env) {
|
|
387
|
+
const recorded = await this.api.commit(async (tx) => (await tx.doc(CodemodeRunDoc, this.api.taskId)).outputDir, this.context);
|
|
388
|
+
if (recorded !== undefined)
|
|
389
|
+
return recorded;
|
|
390
|
+
const created = await env.createTempDir("nyan-codemode-", this.context);
|
|
391
|
+
if (!created.ok)
|
|
392
|
+
throw new Error(created.error.message);
|
|
393
|
+
return this.api.commit(async (tx) => {
|
|
394
|
+
const run = await tx.doc(CodemodeRunDoc, this.api.taskId);
|
|
395
|
+
run.outputDir ??= created.value;
|
|
396
|
+
return run.outputDir;
|
|
397
|
+
}, this.context);
|
|
398
|
+
}
|
|
399
|
+
async checkFingerprint(fingerprint) {
|
|
400
|
+
const recorded = this.start.fingerprint;
|
|
401
|
+
if (recorded !== undefined) {
|
|
402
|
+
if (recorded !== fingerprint) {
|
|
403
|
+
this.ended = errorResult(`The script cannot resume: the sandbox changed since it started (${recorded} -> ${fingerprint}).`);
|
|
404
|
+
await this.decide(this.ended).catch((error) => this.fail(error));
|
|
405
|
+
this.controller.abort(new Error("Sandbox fingerprint changed"));
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
else {
|
|
410
|
+
try {
|
|
411
|
+
await this.api.commit(async (tx) => {
|
|
412
|
+
const run = await tx.doc(CodemodeRunDoc, this.api.taskId);
|
|
413
|
+
if (run.start !== undefined)
|
|
414
|
+
run.start.fingerprint = fingerprint;
|
|
415
|
+
}, this.context);
|
|
416
|
+
}
|
|
417
|
+
catch (error) {
|
|
418
|
+
this.fail(error);
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
this.openGate();
|
|
423
|
+
// Nothing was delivered before the gate: start now. A rerun without a journal releases its calls at the
|
|
424
|
+
// script's first acknowledgment instead.
|
|
425
|
+
if (this.replaying)
|
|
426
|
+
this.deliverReplayed();
|
|
427
|
+
else if (!this.rerun)
|
|
428
|
+
void this.release();
|
|
429
|
+
}
|
|
430
|
+
onCall(made) {
|
|
431
|
+
if (this.stopped)
|
|
432
|
+
return;
|
|
433
|
+
const call = made;
|
|
434
|
+
if (call.target === "tool")
|
|
435
|
+
this.started(call);
|
|
436
|
+
this.issued = Math.max(this.issued, call.id);
|
|
437
|
+
const index = this.replaying ? (this.replayIndex.get(call.id) ?? -1) : -1;
|
|
438
|
+
if (index === -1) {
|
|
439
|
+
if (this.released) {
|
|
440
|
+
this.waiting.push(call);
|
|
441
|
+
this.launchCovered();
|
|
442
|
+
}
|
|
443
|
+
else
|
|
444
|
+
this.held.push(call);
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
const entry = this.replayed[index];
|
|
448
|
+
if (entry.global !== undefined) {
|
|
449
|
+
if (call.target !== "global" || call.name !== entry.global.name || !sameJson(call.args, entry.global.args)) {
|
|
450
|
+
this.diverge(`call ${call.id} is ${describe(call)}, but was a call to ${entry.global.name}`);
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
this.ready.set(call.id, entry.global.outcome);
|
|
454
|
+
void this.gate.then(() => this.deliverReplayed());
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
if (call.target !== "tool") {
|
|
458
|
+
this.diverge(`call ${call.id} is ${describe(call)}, but was a tool call`);
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
void this.gate
|
|
462
|
+
.then(() => this.callTool(call))
|
|
463
|
+
.then((outcome) => {
|
|
464
|
+
this.ready.set(call.id, outcome);
|
|
465
|
+
this.deliverReplayed();
|
|
466
|
+
}, (error) => {
|
|
467
|
+
if (this.stopped)
|
|
468
|
+
return;
|
|
469
|
+
if (this.context.abortSignal?.aborted)
|
|
470
|
+
this.fail(error);
|
|
471
|
+
// The key names another tool or other arguments.
|
|
472
|
+
else
|
|
473
|
+
this.diverge(`call ${call.id} is ${describe(call)}: ${errorMessage(error)}`);
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
/** Replay: deliver journaled results in journal order, each once the VM has issued its call. */
|
|
477
|
+
deliverReplayed() {
|
|
478
|
+
while (this.nextReplayed < this.replayed.length && !this.stopped) {
|
|
479
|
+
const entry = this.replayed[this.nextReplayed];
|
|
480
|
+
const outcome = this.ready.get(entry.id);
|
|
481
|
+
if (entry.id > this.issued || outcome === undefined)
|
|
482
|
+
return;
|
|
483
|
+
this.ready.delete(entry.id);
|
|
484
|
+
this.nextReplayed++;
|
|
485
|
+
this.run.deliver(this.nextReplayed, entry.at, entry.id, outcome);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
onIdle(seq, issued) {
|
|
489
|
+
if (this.stopped)
|
|
490
|
+
return;
|
|
491
|
+
// A count an earlier attempt recorded must match; a missing one is recorded now.
|
|
492
|
+
if (this.rerun && seq <= this.replayed.length) {
|
|
493
|
+
const recorded = seq === 0 ? this.firstIssued : this.replayed[seq - 1].issuedAfter;
|
|
494
|
+
if (recorded !== undefined && recorded !== issued) {
|
|
495
|
+
this.diverge(seq === 0
|
|
496
|
+
? `its first run made ${issued} calls, where it had made ${recorded}`
|
|
497
|
+
: `after result ${seq} it had made ${issued} calls, where it had made ${recorded}`);
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
this.acks.set(seq, issued);
|
|
502
|
+
if (!this.replaying) {
|
|
503
|
+
// A rerun without a journal: every call an earlier attempt could have admitted is made by now.
|
|
504
|
+
if (seq === 0 && this.rerun)
|
|
505
|
+
void this.release();
|
|
506
|
+
// Committed at once when the script has processed everything delivered: calls it made wait for it, and a script
|
|
507
|
+
// waiting on a long call is where a restart usually finds it. Otherwise with the next commit; the VM catches up
|
|
508
|
+
// without waiting on anything.
|
|
509
|
+
if (seq === this.journalLength)
|
|
510
|
+
void this.pump();
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
// Caught up: the VM processed everything delivered and waits.
|
|
514
|
+
if (seq !== this.nextReplayed)
|
|
515
|
+
return;
|
|
516
|
+
const next = this.replayed[seq];
|
|
517
|
+
if (next !== undefined) {
|
|
518
|
+
if (next.id > issued)
|
|
519
|
+
this.diverge(`it waits without making call ${next.id}, which it made before`);
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
// The last replayed result is acknowledged: continue live, admitting the calls the journal did not have once their
|
|
523
|
+
// count is committed.
|
|
524
|
+
this.replaying = false;
|
|
525
|
+
void this.pump();
|
|
526
|
+
void this.release();
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Let the held calls launch, and later live calls, each once its issued count is committed. On a rerun, an earlier
|
|
530
|
+
* attempt may have admitted some of them under their keys, as other calls: when the replay releases (idle after the
|
|
531
|
+
* last journaled result, or after its first run without a journal) its counts matched every recorded one, so it has
|
|
532
|
+
* made every call an earlier attempt could have admitted. All held calls are checked first, whatever their target,
|
|
533
|
+
* and a divergence launches none. Calls made after this are new.
|
|
534
|
+
*/
|
|
535
|
+
async release() {
|
|
536
|
+
await this.gate;
|
|
537
|
+
if (this.stopped || this.released)
|
|
538
|
+
return;
|
|
539
|
+
const calls = this.held.splice(0);
|
|
540
|
+
if (this.rerun) {
|
|
541
|
+
for (const call of calls) {
|
|
542
|
+
let other;
|
|
543
|
+
try {
|
|
544
|
+
other = await this.earlier(call);
|
|
545
|
+
}
|
|
546
|
+
catch (error) {
|
|
547
|
+
this.fail(error);
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
if (this.stopped)
|
|
551
|
+
return;
|
|
552
|
+
if (other !== undefined) {
|
|
553
|
+
this.diverge(`call ${call.id} is ${describe(call)}, but was ${other}`);
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
this.released = true;
|
|
559
|
+
this.waiting.push(...calls, ...this.held.splice(0));
|
|
560
|
+
this.launchCovered();
|
|
561
|
+
}
|
|
562
|
+
/** Launch the waiting calls whose issued count is committed. */
|
|
563
|
+
launchCovered() {
|
|
564
|
+
if (!this.released || this.stopped)
|
|
565
|
+
return;
|
|
566
|
+
const covered = this.waiting.filter((call) => call.id <= this.durableIssued);
|
|
567
|
+
if (covered.length === 0)
|
|
568
|
+
return;
|
|
569
|
+
this.waiting = this.waiting.filter((call) => call.id > this.durableIssued);
|
|
570
|
+
for (const call of covered)
|
|
571
|
+
this.launch(call);
|
|
572
|
+
}
|
|
573
|
+
/** The call an earlier attempt made under this call's key, when it is another one: another target, tool, or arguments. */
|
|
574
|
+
async earlier(call) {
|
|
575
|
+
const { api, context } = this;
|
|
576
|
+
const taskId = (await api.snapshot(NestedCallDoc, api.taskId, `c${call.id}`, context))?.taskId;
|
|
577
|
+
if (taskId === undefined)
|
|
578
|
+
return undefined;
|
|
579
|
+
const input = (await api.getTask(taskId, context))?.input;
|
|
580
|
+
if (input?.kind !== "nested")
|
|
581
|
+
return undefined;
|
|
582
|
+
const made = input.call;
|
|
583
|
+
if (call.target === "tool" && made.name === call.name && sameJson(made.arguments, call.args ?? {}))
|
|
584
|
+
return undefined;
|
|
585
|
+
return `tools.${made.name}(${JSON.stringify(made.arguments)})`;
|
|
586
|
+
}
|
|
587
|
+
/** Run one live call and journal its result. */
|
|
588
|
+
launch(call) {
|
|
589
|
+
if (this.stopped)
|
|
590
|
+
return;
|
|
591
|
+
const running = call.target === "tool"
|
|
592
|
+
? this.callTool(call).then((outcome) => ({ outcome }))
|
|
593
|
+
: this.callGlobal(call).then((outcome) => ({
|
|
594
|
+
outcome,
|
|
595
|
+
global: {
|
|
596
|
+
name: call.name,
|
|
597
|
+
...(call.args === undefined ? {} : { args: call.args }),
|
|
598
|
+
outcome: outcome,
|
|
599
|
+
},
|
|
600
|
+
}));
|
|
601
|
+
running.then((done) => {
|
|
602
|
+
this.queue.push({ id: call.id, at: Date.now(), ...done });
|
|
603
|
+
void this.pump();
|
|
604
|
+
}, (error) => {
|
|
605
|
+
// Stopped meanwhile: the outcome is decided, or a waiting model call gave up.
|
|
606
|
+
if (this.stopped)
|
|
607
|
+
return;
|
|
608
|
+
if (this.context.abortSignal?.aborted)
|
|
609
|
+
this.fail(error);
|
|
610
|
+
// A key an earlier attempt admitted with another tool or other arguments: this attempt took another path.
|
|
611
|
+
else
|
|
612
|
+
this.diverge(`call ${call.id} is ${describe(call)}: ${errorMessage(error)}`);
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
async callTool(call) {
|
|
616
|
+
// Passed as the script gave them; validation reports arguments that are not an object.
|
|
617
|
+
const args = (call.args ?? {});
|
|
618
|
+
const result = await this.api.executeTool(call.name, args, this.context, { key: `c${call.id}` });
|
|
619
|
+
const structured = this.start.tools.find((tool) => tool.name === call.name)?.structured === true;
|
|
620
|
+
const outcome = outcomeOf(call.name, structured, result);
|
|
621
|
+
this.finishRow(call.id, outcome, result);
|
|
622
|
+
return outcome;
|
|
623
|
+
}
|
|
624
|
+
/** The script made a tool call: its row appears, running, also while it waits to launch. */
|
|
625
|
+
started(call) {
|
|
626
|
+
if (this.rows.has(call.id))
|
|
627
|
+
return;
|
|
628
|
+
const args = call.args === undefined ? "" : (JSON.stringify(call.args) ?? "");
|
|
629
|
+
this.rows.set(call.id, { id: call.id, name: call.name, args: cut(args, ARGS_PREVIEW_CHARS), status: "running" });
|
|
630
|
+
}
|
|
631
|
+
/** A tool call ended: its row gets its status, duration, and error. */
|
|
632
|
+
/** The call ended: an error result is an error row, also when the script got its value (bash's nonzero exit). */
|
|
633
|
+
finishRow(id, outcome, result) {
|
|
634
|
+
const row = this.rows.get(id);
|
|
635
|
+
if (row === undefined)
|
|
636
|
+
return;
|
|
637
|
+
row.status = result.isError ? "error" : "ok";
|
|
638
|
+
if (result.durationMs !== undefined)
|
|
639
|
+
row.durationMs = result.durationMs;
|
|
640
|
+
const errors = result.diagnostics.filter((diagnostic) => diagnostic.severity === "error");
|
|
641
|
+
const error = !outcome.ok ? outcome.message : errors.map((diagnostic) => diagnostic.message).join("\n");
|
|
642
|
+
if (result.isError && error !== "")
|
|
643
|
+
row.error = cut(error, ERROR_PREVIEW_CHARS);
|
|
644
|
+
}
|
|
645
|
+
/** The rows in call order; calls still running when the script ended were cancelled. */
|
|
646
|
+
details() {
|
|
647
|
+
const calls = [...this.rows.values()]
|
|
648
|
+
.sort((a, b) => a.id - b.id)
|
|
649
|
+
.map((row) => (row.status === "running" ? { ...row, status: "cancelled" } : row));
|
|
650
|
+
return { calls };
|
|
651
|
+
}
|
|
652
|
+
async callGlobal(call) {
|
|
653
|
+
const discovery = DISCOVERY_GLOBALS.includes(call.name);
|
|
654
|
+
const global = discovery ? undefined : this.options.globals?.find((candidate) => candidate.name === call.name);
|
|
655
|
+
if (!discovery && global === undefined)
|
|
656
|
+
return { ok: false, message: `Unknown global ${call.name}` };
|
|
657
|
+
try {
|
|
658
|
+
// `undefined` stays undefined: the journal omits the value, and a replay resolves with undefined again.
|
|
659
|
+
const value = discovery ? this.discover(call) : await global.execute(call.args, this.api, this.context);
|
|
660
|
+
return { ok: true, value };
|
|
661
|
+
}
|
|
662
|
+
catch (error) {
|
|
663
|
+
if (this.context.abortSignal?.aborted)
|
|
664
|
+
throw error;
|
|
665
|
+
return { ok: false, message: errorMessage(error) };
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
/** `searchTools()`, `describeTool()`, `describeNamespace()`: answered from the start data. */
|
|
669
|
+
discover(call) {
|
|
670
|
+
const args = Array.isArray(call.args) ? call.args : [];
|
|
671
|
+
const { namespaces } = this.start;
|
|
672
|
+
const { tools } = this.start;
|
|
673
|
+
const namespaceOfTool = (name) => namespaces.find((namespace) => namespace.tools.includes(name));
|
|
674
|
+
const matchesNamespace = (namespace, query) => namespace.name === query || toCodemodeIdentifier(namespace.name) === toCodemodeIdentifier(query);
|
|
675
|
+
if (call.name === "describeTool") {
|
|
676
|
+
const [name] = args;
|
|
677
|
+
if (typeof name !== "string")
|
|
678
|
+
throw new Error("describeTool() expects a tool name");
|
|
679
|
+
return tools.find((tool) => tool.name === name || tool.jsName === name)?.description;
|
|
680
|
+
}
|
|
681
|
+
if (call.name === "describeNamespace") {
|
|
682
|
+
const [name] = args;
|
|
683
|
+
if (typeof name !== "string")
|
|
684
|
+
throw new Error("describeNamespace() expects a namespace name");
|
|
685
|
+
const namespace = namespaces.find((candidate) => matchesNamespace(candidate, name));
|
|
686
|
+
if (namespace === undefined)
|
|
687
|
+
return undefined;
|
|
688
|
+
return {
|
|
689
|
+
name: namespace.name,
|
|
690
|
+
...(namespace.description === undefined ? {} : { description: namespace.description }),
|
|
691
|
+
...(namespace.instructions === undefined ? {} : { instructions: namespace.instructions }),
|
|
692
|
+
tools: namespace.tools.map((tool) => toCodemodeIdentifier(tool)),
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
const [query, options] = args;
|
|
696
|
+
if (typeof query !== "string")
|
|
697
|
+
throw new Error("searchTools() expects a query string");
|
|
698
|
+
const limit = options?.limit ?? DEFAULT_SEARCH_LIMIT;
|
|
699
|
+
if (typeof limit !== "number" || !Number.isInteger(limit) || limit <= 0) {
|
|
700
|
+
throw new Error("searchTools() limit must be a positive integer");
|
|
701
|
+
}
|
|
702
|
+
const namespace = options?.namespace;
|
|
703
|
+
if (namespace !== undefined && namespace !== null && typeof namespace !== "string") {
|
|
704
|
+
throw new Error("searchTools() namespace must be a string");
|
|
705
|
+
}
|
|
706
|
+
const documents = tools.flatMap((tool) => {
|
|
707
|
+
const own = namespaceOfTool(tool.name);
|
|
708
|
+
if (typeof namespace === "string" && (own === undefined || !matchesNamespace(own, namespace)))
|
|
709
|
+
return [];
|
|
710
|
+
const text = [
|
|
711
|
+
tool.name,
|
|
712
|
+
tool.name.replaceAll("_", " "),
|
|
713
|
+
tool.description,
|
|
714
|
+
own?.name ?? "",
|
|
715
|
+
own?.description ?? "",
|
|
716
|
+
];
|
|
717
|
+
return [{ name: tool.name, text: text.join(" ") }];
|
|
718
|
+
});
|
|
719
|
+
return new Bm25Ranker().rank(query, documents, limit).map(({ name }) => {
|
|
720
|
+
const tool = tools.find((candidate) => candidate.name === name);
|
|
721
|
+
return { name: tool.jsName, description: tool.description };
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
/**
|
|
725
|
+
* Journal finished calls and acknowledged counts in one commit, then deliver the calls and launch the calls the
|
|
726
|
+
* counts cover; one commit in flight at a time, so what arrives meanwhile goes into the next.
|
|
727
|
+
*/
|
|
728
|
+
async pump() {
|
|
729
|
+
if (this.committing || this.stopped)
|
|
730
|
+
return;
|
|
731
|
+
if (this.queue.length === 0 && this.acks.size === 0)
|
|
732
|
+
return;
|
|
733
|
+
this.committing = true;
|
|
734
|
+
const batch = this.queue.splice(0);
|
|
735
|
+
const acks = [...this.acks];
|
|
736
|
+
this.acks.clear();
|
|
737
|
+
try {
|
|
738
|
+
await this.api.commit(async (tx) => {
|
|
739
|
+
const journal = await tx.doc(CodemodeJournalDoc, this.api.taskId);
|
|
740
|
+
for (const [seq, issued] of acks) {
|
|
741
|
+
if (seq === 0)
|
|
742
|
+
journal.firstIssued ??= issued;
|
|
743
|
+
else {
|
|
744
|
+
const entry = journal.entries[seq - 1];
|
|
745
|
+
if (entry !== undefined && entry.issuedAfter === undefined)
|
|
746
|
+
entry.issuedAfter = issued;
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
for (const item of batch) {
|
|
750
|
+
const entry = {
|
|
751
|
+
id: item.id,
|
|
752
|
+
at: item.at,
|
|
753
|
+
...(item.global === undefined ? {} : { global: item.global }),
|
|
754
|
+
};
|
|
755
|
+
journal.entries.push(copyJson(entry, { omitUndefinedProperties: true }));
|
|
756
|
+
}
|
|
757
|
+
}, this.context);
|
|
758
|
+
}
|
|
759
|
+
catch (error) {
|
|
760
|
+
this.fail(error);
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
for (const [, issued] of acks)
|
|
764
|
+
this.durableIssued = Math.max(this.durableIssued, issued);
|
|
765
|
+
for (const item of batch) {
|
|
766
|
+
this.journalLength++;
|
|
767
|
+
if (!this.run.finished)
|
|
768
|
+
this.run.deliver(this.journalLength, item.at, item.id, item.outcome);
|
|
769
|
+
}
|
|
770
|
+
this.launchCovered();
|
|
771
|
+
this.committing = false;
|
|
772
|
+
void this.pump();
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
function describe(call) {
|
|
776
|
+
return `${call.target === "tool" ? "tools." : ""}${call.name}(${JSON.stringify(call.args) ?? ""})`;
|
|
777
|
+
}
|
|
778
|
+
/** Structural JSON equality; key order does not matter. */
|
|
779
|
+
function sameJson(a, b) {
|
|
780
|
+
if (a === b)
|
|
781
|
+
return true;
|
|
782
|
+
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null)
|
|
783
|
+
return false;
|
|
784
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
785
|
+
return (Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((item, i) => sameJson(item, b[i])));
|
|
786
|
+
}
|
|
787
|
+
const keys = Object.keys(a);
|
|
788
|
+
return (keys.length === Object.keys(b).length &&
|
|
789
|
+
keys.every((key) => Object.hasOwn(b, key) && sameJson(a[key], b[key])));
|
|
790
|
+
}
|
|
791
|
+
//# sourceMappingURL=codemode.js.map
|