@modelprofile.com/flexharness 11.0.1 → 11.1.1
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_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.codesandbox.d.ts +12 -4
- package/dist_ts/classes.codesandbox.js +119 -287
- package/dist_ts/classes.codesandbox.worker.d.ts +52 -0
- package/dist_ts/classes.codesandbox.worker.js +310 -0
- package/dist_ts/classes.flexharness.callbacks.d.ts +37 -1
- package/dist_ts/classes.flexharness.callbacks.js +162 -53
- package/dist_ts/classes.flexharness.core.d.ts +2 -1
- package/dist_ts/classes.flexharness.core.js +22 -18
- package/dist_ts/classes.flexharness.results.js +4 -4
- package/dist_ts/classes.flexharness.reversions.d.ts +7 -0
- package/dist_ts/classes.flexharness.reversions.js +40 -39
- package/dist_ts/classes.flexharness.runs.js +7 -3
- package/dist_ts/classes.flexharness.tooldispatch.d.ts +2 -1
- package/dist_ts/classes.flexharness.tooldispatch.js +27 -9
- package/dist_ts/classes.stores.js +15 -13
- package/dist_ts/errors.d.ts +5 -0
- package/dist_ts/errors.js +10 -1
- package/dist_ts/index.d.ts +2 -1
- package/dist_ts/index.js +3 -2
- package/dist_ts/interfaces.d.ts +31 -9
- package/dist_ts/interfaces.flexharness.d.ts +14 -0
- package/dist_ts/interfaces.js +3 -3
- package/dist_ts/plugins.d.ts +6 -4
- package/dist_ts/plugins.js +10 -11
- package/dist_ts/utils.harnessoptions.d.ts +3 -1
- package/dist_ts/utils.harnessoptions.js +15 -2
- package/dist_ts/utils.json.d.ts +9 -1
- package/dist_ts/utils.json.js +92 -33
- package/dist_ts/utils.storecodecs.d.ts +1 -1
- package/dist_ts/utils.storecodecs.js +4 -4
- package/dist_ts/utils.storerules.d.ts +3 -3
- package/dist_ts/utils.storerules.js +1 -14
- package/dist_ts_mcp/index.d.ts +3 -1
- package/dist_ts_mcp/index.js +17 -2
- package/dist_ts_stores_nosqldb/classes.nosqldbstores.d.ts +18 -6
- package/dist_ts_stores_nosqldb/classes.nosqldbstores.js +27 -16
- package/dist_ts_stores_nosqldb/classes.recordstore.d.ts +33 -3
- package/dist_ts_stores_nosqldb/classes.recordstore.js +78 -18
- package/dist_ts_stores_nosqldb/index.d.ts +2 -2
- package/dist_ts_stores_nosqldb/index.js +2 -2
- package/package.json +3 -3
- package/readme.md +8 -2
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.codesandbox.ts +132 -276
- package/ts/classes.codesandbox.worker.ts +345 -0
- package/ts/classes.flexharness.callbacks.ts +207 -55
- package/ts/classes.flexharness.core.ts +22 -16
- package/ts/classes.flexharness.results.ts +3 -3
- package/ts/classes.flexharness.reversions.ts +43 -32
- package/ts/classes.flexharness.runs.ts +6 -2
- package/ts/classes.flexharness.tooldispatch.ts +23 -7
- package/ts/classes.stores.ts +21 -12
- package/ts/errors.ts +14 -0
- package/ts/index.ts +7 -1
- package/ts/interfaces.flexharness.ts +15 -0
- package/ts/interfaces.ts +32 -9
- package/ts/plugins.ts +9 -12
- package/ts/readme.md +8 -2
- package/ts/utils.harnessoptions.ts +22 -0
- package/ts/utils.json.ts +112 -35
- package/ts/utils.storecodecs.ts +3 -3
- package/ts/utils.storerules.ts +3 -24
- package/ts_mcp/index.ts +17 -1
- package/ts_mcp/readme.md +3 -1
- package/ts_stores_nosqldb/classes.nosqldbstores.ts +39 -15
- package/ts_stores_nosqldb/classes.recordstore.ts +105 -13
- package/ts_stores_nosqldb/index.ts +2 -0
- package/ts_stores_nosqldb/readme.md +10 -2
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The QuickJS side of a `run_code` program. `FlexCodeSandbox` runs every program in a worker thread of
|
|
3
|
+
* its own with this module, and ends it by terminating the worker, which frees its WebAssembly instance
|
|
4
|
+
* too: a native operation that never reaches QuickJS's interrupt handler cannot outlast its limits, and
|
|
5
|
+
* a program never blocks the host's event loop.
|
|
6
|
+
*
|
|
7
|
+
* Written in erasable TypeScript that imports nothing of the package at run time, so the module loads
|
|
8
|
+
* uncompiled as well.
|
|
9
|
+
*/
|
|
10
|
+
import { parentPort } from 'node:worker_threads';
|
|
11
|
+
import type * as plugins from './plugins.js';
|
|
12
|
+
|
|
13
|
+
/** Evaluating the prelude, the program's text, or running the program. */
|
|
14
|
+
export type TCodeWorkerPhase = 'prelude' | 'parse' | 'program';
|
|
15
|
+
|
|
16
|
+
/** The limits of a program that its worker enforces itself. */
|
|
17
|
+
export interface ICodeWorkerLimits {
|
|
18
|
+
cpuMs: number;
|
|
19
|
+
memoryBytes: number;
|
|
20
|
+
stackBytes: number;
|
|
21
|
+
maxLogBytes: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** What the host sends a program's worker. */
|
|
25
|
+
export type TCodeWorkerRequest =
|
|
26
|
+
| { kind: 'start'; program: string; limits: ICodeWorkerLimits }
|
|
27
|
+
| { kind: 'answer'; id: number; text: string };
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* What a program's worker reports. `ready` comes once QuickJS is loaded; every `start` and `answer`
|
|
31
|
+
* ends in `idle`, with the milliseconds computed so far, unless the program stopped. `done` counts the
|
|
32
|
+
* calls the program had no answer to when it returned.
|
|
33
|
+
*/
|
|
34
|
+
export type TCodeWorkerReport =
|
|
35
|
+
| { kind: 'ready' }
|
|
36
|
+
| { kind: 'call'; id: number; method: string; params: string }
|
|
37
|
+
| { kind: 'log'; text: string }
|
|
38
|
+
| { kind: 'idle'; cpuMs: number }
|
|
39
|
+
| { kind: 'done'; text: string; unawaited: number }
|
|
40
|
+
| { kind: 'failed'; phase: TCodeWorkerPhase; name?: string; message?: string; stack?: string; dumped?: string }
|
|
41
|
+
| { kind: 'stopped'; limit: 'cpu' | 'stack' };
|
|
42
|
+
|
|
43
|
+
/** The file name the program's text is evaluated as: the host reads a failure's line in it from the stack. */
|
|
44
|
+
const programFile = 'program.ts';
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Runs in the context before the program: it takes the host functions off the global object and
|
|
48
|
+
* defines `api`, `ApiError` and `console`. Only strings cross the boundary.
|
|
49
|
+
*/
|
|
50
|
+
const prelude = `'use strict';
|
|
51
|
+
(() => {
|
|
52
|
+
const call = globalThis.__flexCall;
|
|
53
|
+
const log = globalThis.__flexLog;
|
|
54
|
+
const done = globalThis.__flexDone;
|
|
55
|
+
delete globalThis.__flexCall;
|
|
56
|
+
delete globalThis.__flexLog;
|
|
57
|
+
delete globalThis.__flexDone;
|
|
58
|
+
class ApiError extends Error {
|
|
59
|
+
constructor(details) {
|
|
60
|
+
const message = details && typeof details.message === 'string' ? details.message : JSON.stringify(details);
|
|
61
|
+
super(message);
|
|
62
|
+
this.name = 'ApiError';
|
|
63
|
+
this.details = details;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const invoke = (path, args) => {
|
|
67
|
+
if (!path) return Promise.reject(new TypeError('Call a method as api.<namespace>.<method>(params).'));
|
|
68
|
+
let params;
|
|
69
|
+
try {
|
|
70
|
+
// api.results.get(handle, { page, path }) reads a stored result.
|
|
71
|
+
const argument = path === 'results.get' && typeof args[0] === 'string'
|
|
72
|
+
? Object.assign({}, args[1], { handle: args[0] })
|
|
73
|
+
: args[0];
|
|
74
|
+
params = JSON.stringify(args.length === 0 || argument === undefined ? {} : argument);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
return Promise.reject(new TypeError('The params of ' + path + ' are not JSON: ' + error.message));
|
|
77
|
+
}
|
|
78
|
+
return call(path, params === undefined ? 'null' : params).then((text) => {
|
|
79
|
+
const outcome = JSON.parse(text);
|
|
80
|
+
if (outcome.ok) return outcome.value;
|
|
81
|
+
throw new ApiError(outcome.error);
|
|
82
|
+
});
|
|
83
|
+
};
|
|
84
|
+
const target = (path) => new Proxy(function () {}, {
|
|
85
|
+
get: (_target, key) => typeof key === 'string' && key !== 'then' ? target(path ? path + '.' + key : key) : undefined,
|
|
86
|
+
apply: (_target, _this, args) => invoke(path, args),
|
|
87
|
+
set: () => false,
|
|
88
|
+
defineProperty: () => false,
|
|
89
|
+
deleteProperty: () => false,
|
|
90
|
+
});
|
|
91
|
+
const format = (value) => {
|
|
92
|
+
if (typeof value === 'string') return value;
|
|
93
|
+
try {
|
|
94
|
+
const text = JSON.stringify(value);
|
|
95
|
+
return text === undefined ? String(value) : text;
|
|
96
|
+
} catch {
|
|
97
|
+
return String(value);
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
const writer = (level) => (...values) => { log(level, values.map(format).join(' ')); };
|
|
101
|
+
const fixed = (value) => ({ value, enumerable: false, writable: false, configurable: false });
|
|
102
|
+
Object.defineProperty(globalThis, 'api', fixed(target('')));
|
|
103
|
+
Object.defineProperty(globalThis, 'ApiError', fixed(ApiError));
|
|
104
|
+
Object.defineProperty(globalThis, 'console', fixed(Object.freeze({
|
|
105
|
+
log: writer('log'), info: writer('info'), warn: writer('warn'), error: writer('error'), debug: writer('debug'),
|
|
106
|
+
})));
|
|
107
|
+
const settle = (value) => {
|
|
108
|
+
try {
|
|
109
|
+
const text = JSON.stringify({ ok: true, value: value === undefined ? null : value });
|
|
110
|
+
return text === undefined ? JSON.stringify({ ok: true, value: null }) : text;
|
|
111
|
+
} catch (error) {
|
|
112
|
+
return JSON.stringify({ ok: false, name: 'TypeError', message: 'The return value is not JSON: ' + error.message });
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
const fail = (error) => JSON.stringify({
|
|
116
|
+
ok: false,
|
|
117
|
+
name: error && typeof error.name === 'string' ? error.name : 'Error',
|
|
118
|
+
message: error && typeof error.message === 'string' ? error.message : String(error),
|
|
119
|
+
stack: error && typeof error.stack === 'string' ? error.stack : '',
|
|
120
|
+
...(error instanceof ApiError ? { details: error.details } : {}),
|
|
121
|
+
});
|
|
122
|
+
Object.defineProperty(globalThis, '__flexStart', {
|
|
123
|
+
configurable: true,
|
|
124
|
+
value: () => {
|
|
125
|
+
const main = globalThis.__flexMain;
|
|
126
|
+
delete globalThis.__flexStart;
|
|
127
|
+
delete globalThis.__flexMain;
|
|
128
|
+
main().then(settle, fail).then(done);
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
})();
|
|
132
|
+
`;
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* One program in its QuickJS runtime. It enforces the limits QuickJS can see: memory, stack, console
|
|
136
|
+
* output, and computing time at every interrupt point. The host enforces the rest, and ends the worker
|
|
137
|
+
* when the program computes past its time between two interrupt points.
|
|
138
|
+
*/
|
|
139
|
+
class CodeWorkerRun {
|
|
140
|
+
private readonly report: (report: TCodeWorkerReport) => void;
|
|
141
|
+
private readonly limits: ICodeWorkerLimits;
|
|
142
|
+
private readonly runtime: plugins.IQuickJsRuntime;
|
|
143
|
+
private readonly vm: plugins.IQuickJsContext;
|
|
144
|
+
private readonly deferreds = new Map<number, plugins.IQuickJsDeferredPromise>();
|
|
145
|
+
private nextCallId = 0;
|
|
146
|
+
private cpuMs = 0;
|
|
147
|
+
private enteredAt?: number;
|
|
148
|
+
private stopped?: 'cpu' | 'stack';
|
|
149
|
+
private ended = false;
|
|
150
|
+
private logBytes = 0;
|
|
151
|
+
private logsCut = false;
|
|
152
|
+
|
|
153
|
+
constructor(module: plugins.IQuickJsModule, limits: ICodeWorkerLimits, report: (report: TCodeWorkerReport) => void) {
|
|
154
|
+
this.limits = limits;
|
|
155
|
+
this.report = report;
|
|
156
|
+
this.runtime = module.newRuntime();
|
|
157
|
+
this.runtime.setMemoryLimit(limits.memoryBytes);
|
|
158
|
+
this.runtime.setMaxStackSize(limits.stackBytes);
|
|
159
|
+
this.runtime.setInterruptHandler(() => this.interrupted());
|
|
160
|
+
this.vm = this.runtime.newContext();
|
|
161
|
+
this.define('__flexCall', (pathHandle, paramsHandle) => this.call(this.vm.getString(pathHandle), this.vm.getString(paramsHandle)));
|
|
162
|
+
this.define('__flexLog', (levelHandle, textHandle) => {
|
|
163
|
+
this.log(this.vm.getString(levelHandle), this.vm.getString(textHandle));
|
|
164
|
+
});
|
|
165
|
+
this.define('__flexDone', (textHandle) => {
|
|
166
|
+
this.ended = true;
|
|
167
|
+
this.report({ kind: 'done', text: this.vm.getString(textHandle), unawaited: this.deferreds.size });
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
public start(program: string): void {
|
|
172
|
+
if (this.evaluate(prelude, 'prelude.js', 'prelude')
|
|
173
|
+
&& this.evaluate(program, programFile, 'parse')
|
|
174
|
+
&& this.evaluate('__flexStart();', 'start.js', 'program')) {
|
|
175
|
+
this.pump();
|
|
176
|
+
}
|
|
177
|
+
this.idle();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Settles a call of the program with the host's outcome, and runs what it unblocked. */
|
|
181
|
+
public answer(id: number, text: string): void {
|
|
182
|
+
const deferred = this.deferreds.get(id);
|
|
183
|
+
if (!deferred || this.ended) return;
|
|
184
|
+
this.deferreds.delete(id);
|
|
185
|
+
const value = this.vm.newString(text);
|
|
186
|
+
deferred.resolve(value);
|
|
187
|
+
value.dispose();
|
|
188
|
+
this.pump();
|
|
189
|
+
this.idle();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private define(name: string, implementation: (...args: plugins.IQuickJsHandle[]) => plugins.IQuickJsHandle | void): void {
|
|
193
|
+
const handle = this.vm.newFunction(name, implementation);
|
|
194
|
+
this.vm.setProp(this.vm.global, name, handle);
|
|
195
|
+
handle.dispose();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private call(method: string, params: string): plugins.IQuickJsHandle {
|
|
199
|
+
const deferred = this.vm.newPromise();
|
|
200
|
+
const id = this.nextCallId++;
|
|
201
|
+
this.deferreds.set(id, deferred);
|
|
202
|
+
this.report({ kind: 'call', id, method, params });
|
|
203
|
+
return deferred.handle;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
private log(level: string, text: string): void {
|
|
207
|
+
if (this.logsCut) return;
|
|
208
|
+
const line = `${level === 'log' ? '' : `[${level}] `}${text}\n`;
|
|
209
|
+
const bytes = Buffer.byteLength(line, 'utf8');
|
|
210
|
+
if (this.logBytes + bytes > this.limits.maxLogBytes) {
|
|
211
|
+
this.logsCut = true;
|
|
212
|
+
this.report({ kind: 'log', text: '[console output cut off]\n' });
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
this.logBytes += bytes;
|
|
216
|
+
this.report({ kind: 'log', text: line });
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private evaluate(code: string, file: string, phase: TCodeWorkerPhase): boolean {
|
|
220
|
+
if (this.over()) return false;
|
|
221
|
+
let result: plugins.TQuickJsResult;
|
|
222
|
+
this.enter();
|
|
223
|
+
try {
|
|
224
|
+
result = this.vm.evalCode(code, file);
|
|
225
|
+
} catch (error) {
|
|
226
|
+
this.hostThrow(error);
|
|
227
|
+
return false;
|
|
228
|
+
} finally {
|
|
229
|
+
this.leave();
|
|
230
|
+
}
|
|
231
|
+
if (result.error) {
|
|
232
|
+
this.failWith(result.error, phase);
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
result.value.dispose();
|
|
236
|
+
return true;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Runs the program's pending jobs after a call settled or the program started. */
|
|
240
|
+
private pump(): void {
|
|
241
|
+
if (this.over()) return;
|
|
242
|
+
let result: { error?: plugins.IQuickJsHandle };
|
|
243
|
+
this.enter();
|
|
244
|
+
try {
|
|
245
|
+
result = this.runtime.executePendingJobs();
|
|
246
|
+
} catch (error) {
|
|
247
|
+
this.hostThrow(error);
|
|
248
|
+
return;
|
|
249
|
+
} finally {
|
|
250
|
+
this.leave();
|
|
251
|
+
}
|
|
252
|
+
if (result.error) this.failWith(result.error, 'program');
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** The worker's own stack ran out under deep recursion; anything else is a failure of the worker. */
|
|
256
|
+
private hostThrow(error: unknown): void {
|
|
257
|
+
if (!(error instanceof RangeError)) throw error;
|
|
258
|
+
this.stop('stack');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
private failWith(handle: plugins.IQuickJsHandle, phase: TCodeWorkerPhase): void {
|
|
262
|
+
let dumped: unknown;
|
|
263
|
+
// Reading the thrown value can run program code, such as a toJSON: it counts as computing.
|
|
264
|
+
this.enter();
|
|
265
|
+
try {
|
|
266
|
+
dumped = this.vm.dump(handle);
|
|
267
|
+
} catch {
|
|
268
|
+
dumped = undefined;
|
|
269
|
+
} finally {
|
|
270
|
+
this.leave();
|
|
271
|
+
}
|
|
272
|
+
if (this.stopped) {
|
|
273
|
+
this.stop(this.stopped);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
this.ended = true;
|
|
277
|
+
const error = (dumped && typeof dumped === 'object' ? dumped : { message: String(dumped) }) as {
|
|
278
|
+
name?: unknown; message?: unknown; stack?: unknown;
|
|
279
|
+
};
|
|
280
|
+
this.report({
|
|
281
|
+
kind: 'failed',
|
|
282
|
+
phase,
|
|
283
|
+
...(typeof error.name === 'string' ? { name: error.name } : {}),
|
|
284
|
+
...(typeof error.message === 'string' ? { message: error.message } : {}),
|
|
285
|
+
...(typeof error.stack === 'string' ? { stack: error.stack } : {}),
|
|
286
|
+
...(phase === 'prelude' ? { dumped: JSON.stringify(dumped) } : {}),
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
private stop(limit: 'cpu' | 'stack'): void {
|
|
291
|
+
this.stopped = limit;
|
|
292
|
+
if (this.ended) return;
|
|
293
|
+
this.ended = true;
|
|
294
|
+
this.report({ kind: 'stopped', limit });
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
private over(): boolean {
|
|
298
|
+
return this.ended || this.stopped !== undefined;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
private idle(): void {
|
|
302
|
+
if (!this.over()) this.report({ kind: 'idle', cpuMs: this.cpuMs });
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
private enter(): void {
|
|
306
|
+
this.enteredAt = performance.now();
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
private leave(): void {
|
|
310
|
+
if (this.enteredAt === undefined) return;
|
|
311
|
+
this.cpuMs += performance.now() - this.enteredAt;
|
|
312
|
+
this.enteredAt = undefined;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
private interrupted(): boolean {
|
|
316
|
+
if (this.stopped) return true;
|
|
317
|
+
if (this.enteredAt !== undefined && this.cpuMs + performance.now() - this.enteredAt > this.limits.cpuMs) {
|
|
318
|
+
this.stopped = 'cpu';
|
|
319
|
+
return true;
|
|
320
|
+
}
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const port = parentPort;
|
|
326
|
+
if (port) {
|
|
327
|
+
// The variant's typings resolve as CommonJS; at run time the default export is the variant.
|
|
328
|
+
const [core, variant] = await Promise.all([
|
|
329
|
+
import('quickjs-emscripten-core'),
|
|
330
|
+
import('@jitl/quickjs-ng-wasmfile-release-sync'),
|
|
331
|
+
]);
|
|
332
|
+
const quickjsVariant = (variant as { default: unknown }).default as Parameters<typeof core.newQuickJSWASMModuleFromVariant>[0];
|
|
333
|
+
const module = await core.newQuickJSWASMModuleFromVariant(quickjsVariant) as unknown as plugins.IQuickJsModule;
|
|
334
|
+
const report = (message: TCodeWorkerReport): void => port.postMessage(message);
|
|
335
|
+
let run: CodeWorkerRun | undefined;
|
|
336
|
+
port.on('message', (request: TCodeWorkerRequest) => {
|
|
337
|
+
if (request.kind === 'start') {
|
|
338
|
+
run = new CodeWorkerRun(module, request.limits, report);
|
|
339
|
+
run.start(request.program);
|
|
340
|
+
} else {
|
|
341
|
+
run?.answer(request.id, request.text);
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
report({ kind: 'ready' });
|
|
345
|
+
}
|
|
@@ -8,17 +8,29 @@ import {
|
|
|
8
8
|
type TFlexAgentToolCallUpdateEvent,
|
|
9
9
|
type TFlexAgentToolCallFinishEvent,
|
|
10
10
|
type TFlexMessagePart,
|
|
11
|
+
type TJsonValue,
|
|
11
12
|
} from './interfaces.js';
|
|
12
|
-
import { normalizeJsonValue } from './utils.json.js';
|
|
13
|
-
import { jsonBytes } from './utils.transfer.js';
|
|
13
|
+
import { MIN_MAX_BYTES, normalizeJsonValue } from './utils.json.js';
|
|
14
|
+
import { jsonBytes, truncateUtf8 } from './utils.transfer.js';
|
|
14
15
|
import {
|
|
15
16
|
observeSessionMetricsCall,
|
|
16
17
|
settleSessionMetricsOperation,
|
|
17
18
|
type ISessionMetricsOperation,
|
|
18
19
|
} from './utils.sessionmetrics.js';
|
|
19
|
-
import type {
|
|
20
|
+
import type {
|
|
21
|
+
IStoredSessionState,
|
|
22
|
+
IStorageState,
|
|
23
|
+
IActiveRun,
|
|
24
|
+
IProgramCallbackShare,
|
|
25
|
+
} from './interfaces.flexharness.js';
|
|
20
26
|
import type { FlexHarnessCore } from './classes.flexharness.core.js';
|
|
21
27
|
|
|
28
|
+
/** How a tool call ended, as its part records it. */
|
|
29
|
+
type TToolPartOutcome = { output: TJsonValue } | { error: string };
|
|
30
|
+
|
|
31
|
+
const outcomeBytes = (outcome: TToolPartOutcome): number =>
|
|
32
|
+
'output' in outcome ? jsonBytes(outcome.output) : Buffer.byteLength(outcome.error);
|
|
33
|
+
|
|
22
34
|
/** The model stream callbacks of a run: text, reasoning, tool, retry and usage events. */
|
|
23
35
|
export class FlexHarnessRunCallbacks<TScope> {
|
|
24
36
|
constructor(private readonly core: FlexHarnessCore<TScope>) {}
|
|
@@ -27,10 +39,11 @@ export class FlexHarnessRunCallbacks<TScope> {
|
|
|
27
39
|
const run = this.callbackRun(state, sessionId);
|
|
28
40
|
if (!run || !delta) return;
|
|
29
41
|
const lastPart = run.callbackParts.at(-1);
|
|
30
|
-
const
|
|
31
|
-
const part = lastPart?.type === 'text'
|
|
42
|
+
const openPart = lastPart?.type === 'text' && !run.completedTextPartIds.has(lastPart.partId)
|
|
32
43
|
? lastPart
|
|
33
|
-
:
|
|
44
|
+
: undefined;
|
|
45
|
+
const created = !openPart;
|
|
46
|
+
const part = openPart ?? { partId: plugins.crypto.randomUUID(), type: 'text' as const, text: '' };
|
|
34
47
|
const accounting = this.textAppendAccounting(run, part, delta, created);
|
|
35
48
|
if (!accounting) return;
|
|
36
49
|
if (!this.reserveCallbackCapacity(
|
|
@@ -105,6 +118,22 @@ export class FlexHarnessRunCallbacks<TScope> {
|
|
|
105
118
|
this.core.emitPartEvent(run, 'part.completed', part);
|
|
106
119
|
}
|
|
107
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Completes the text parts of a committed model step: their text is final and saved. A text part
|
|
123
|
+
* completes once, so the terminal projection completes only the text parts of a step that did
|
|
124
|
+
* not commit. Like the terminal's completions, these events are outside the callback limits:
|
|
125
|
+
* their number is bounded by the run's parts.
|
|
126
|
+
*/
|
|
127
|
+
public onStepCommitted(state: IStorageState, sessionId: string): void {
|
|
128
|
+
const run = this.callbackRun(state, sessionId);
|
|
129
|
+
if (!run) return;
|
|
130
|
+
for (const part of run.callbackParts) {
|
|
131
|
+
if (part.type !== 'text' || run.completedTextPartIds.has(part.partId)) continue;
|
|
132
|
+
run.completedTextPartIds.add(part.partId);
|
|
133
|
+
this.core.emitPartEvent(run, 'part.completed', part);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
108
137
|
private textAppendAccounting(
|
|
109
138
|
run: IActiveRun,
|
|
110
139
|
part: Extract<TFlexMessagePart, { type: 'text' | 'reasoning' }>,
|
|
@@ -147,21 +176,7 @@ export class FlexHarnessRunCallbacks<TScope> {
|
|
|
147
176
|
: undefined;
|
|
148
177
|
const input = normalizeJsonValue(target ? target.params : event.input, this.core.toolOutputLimits);
|
|
149
178
|
if (!this.reserveCallbackCapacity(run, 1, jsonBytes(input), 1)) return;
|
|
150
|
-
|
|
151
|
-
partId: plugins.crypto.randomUUID(),
|
|
152
|
-
type: 'tool',
|
|
153
|
-
toolCallId: this.core.boundedIdentifier(event.toolCallId, 'toolCallId'),
|
|
154
|
-
toolName: this.core.boundedIdentifier(target ? target.method : event.toolName, 'toolName'),
|
|
155
|
-
status: 'running',
|
|
156
|
-
input,
|
|
157
|
-
...(event.parentToolCallId === undefined
|
|
158
|
-
? {}
|
|
159
|
-
: { parentToolCallId: this.core.boundedIdentifier(event.parentToolCallId, 'parentToolCallId') }),
|
|
160
|
-
startedAt: new Date().toISOString(),
|
|
161
|
-
};
|
|
162
|
-
run.toolPartIds.set(event.toolCallId, part.partId);
|
|
163
|
-
run.callbackParts.push(part);
|
|
164
|
-
this.core.emitPartEvent(run, 'part.started', part);
|
|
179
|
+
this.startToolPart(run, { ...event, toolName: target ? target.method : event.toolName }, input);
|
|
165
180
|
} catch (error) {
|
|
166
181
|
this.failCallback(run, this.core.projectExternalError(run, error, 'toolCallback'));
|
|
167
182
|
}
|
|
@@ -175,37 +190,164 @@ export class FlexHarnessRunCallbacks<TScope> {
|
|
|
175
190
|
const run = this.callbackRun(state, sessionId);
|
|
176
191
|
if (!run) return;
|
|
177
192
|
if (!run.toolPartIds.has(event.toolCallId)) this.onToolStart(state, sessionId, event);
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
if (!part || part.type !== 'tool' || part.status !== 'running') return;
|
|
193
|
+
const part = this.runningToolPart(run, event.toolCallId);
|
|
194
|
+
if (!part) return;
|
|
181
195
|
try {
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
?
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
196
|
+
const outcome = this.toolOutcome(run, event, this.core.toolOutputLimits.maxBytes);
|
|
197
|
+
if (!this.reserveCallbackCapacity(
|
|
198
|
+
run,
|
|
199
|
+
1,
|
|
200
|
+
outcomeBytes(outcome) - (part.output === undefined ? 0 : jsonBytes(part.output)),
|
|
201
|
+
0,
|
|
202
|
+
)) return;
|
|
203
|
+
this.completeToolPart(run, part, event.toolCallId, outcome);
|
|
204
|
+
} catch (error) {
|
|
205
|
+
this.failCallback(run, this.core.projectExternalError(run, error, 'toolCallback'));
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* The room a `run_code` program's calls get in the run's callback limits: half of what the run
|
|
211
|
+
* has left once its program's answer is set aside. The rest stays for the model's next steps.
|
|
212
|
+
*/
|
|
213
|
+
public programCallbackShare(state: IStorageState, sessionId: string): IProgramCallbackShare {
|
|
214
|
+
const run = this.callbackRun(state, sessionId);
|
|
215
|
+
if (!run) return { events: 0, outputBytes: 0, parts: 0 };
|
|
216
|
+
const limits = this.core.callbackLimits;
|
|
217
|
+
const half = (room: number): number => Math.max(0, Math.floor(room / 2));
|
|
218
|
+
return {
|
|
219
|
+
events: half(limits.maxEvents - run.callbackEventCount - 1),
|
|
220
|
+
outputBytes: half(limits.maxOutputBytes - run.callbackOutputBytes - this.core.toolOutputLimits.maxBytes),
|
|
221
|
+
parts: half(limits.maxParts - run.callbackParts.length),
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Records the start of a call a program made, in the program's share. The call takes the room of
|
|
227
|
+
* its input, its two events and at least a truncated output now. A call the share or the run
|
|
228
|
+
* cannot hold is not recorded, and the answer is false: the program stops, the run goes on.
|
|
229
|
+
*/
|
|
230
|
+
public onProgramCallStart(
|
|
231
|
+
state: IStorageState,
|
|
232
|
+
sessionId: string,
|
|
233
|
+
share: IProgramCallbackShare,
|
|
234
|
+
event: { toolCallId: string; toolName: string; input: unknown; parentToolCallId: string },
|
|
235
|
+
): boolean {
|
|
236
|
+
const run = this.callbackRun(state, sessionId);
|
|
237
|
+
if (!run) return false;
|
|
238
|
+
try {
|
|
239
|
+
const input = normalizeJsonValue(event.input, this.core.toolOutputLimits);
|
|
240
|
+
const bytes = jsonBytes(input) + MIN_MAX_BYTES;
|
|
241
|
+
if (
|
|
242
|
+
share.events < 2
|
|
243
|
+
|| share.outputBytes < bytes
|
|
244
|
+
|| share.parts < 1
|
|
245
|
+
|| !this.tryReserveCallbackCapacity(run, 2, bytes, 1)
|
|
246
|
+
) return false;
|
|
247
|
+
share.events -= 2;
|
|
248
|
+
share.outputBytes -= bytes;
|
|
249
|
+
share.parts -= 1;
|
|
250
|
+
this.startToolPart(run, event, input);
|
|
251
|
+
return true;
|
|
204
252
|
} catch (error) {
|
|
205
253
|
this.failCallback(run, this.core.projectExternalError(run, error, 'toolCallback'));
|
|
254
|
+
return false;
|
|
206
255
|
}
|
|
207
256
|
}
|
|
208
257
|
|
|
258
|
+
/**
|
|
259
|
+
* Records how a call `onProgramCallStart` recorded ended. Its output or error is recorded in what
|
|
260
|
+
* is left of the share, cut to fit: the program gets the call's answer whole all the same.
|
|
261
|
+
*/
|
|
262
|
+
public onProgramCallFinish(
|
|
263
|
+
state: IStorageState,
|
|
264
|
+
sessionId: string,
|
|
265
|
+
share: IProgramCallbackShare,
|
|
266
|
+
event: TFlexAgentToolCallFinishEvent,
|
|
267
|
+
): void {
|
|
268
|
+
const run = this.callbackRun(state, sessionId);
|
|
269
|
+
if (!run) return;
|
|
270
|
+
const part = this.runningToolPart(run, event.toolCallId);
|
|
271
|
+
if (!part) return;
|
|
272
|
+
try {
|
|
273
|
+
const room = Math.min(share.outputBytes, this.core.callbackLimits.maxOutputBytes - run.callbackOutputBytes);
|
|
274
|
+
const allowance = MIN_MAX_BYTES + Math.max(0, room);
|
|
275
|
+
const finished = this.toolOutcome(run, event, Math.min(this.core.toolOutputLimits.maxBytes, allowance));
|
|
276
|
+
const outcome = 'error' in finished ? { error: truncateUtf8(finished.error, allowance) } : finished;
|
|
277
|
+
// The start reserved the call's events and its least output.
|
|
278
|
+
const bytes = outcomeBytes(outcome) - MIN_MAX_BYTES;
|
|
279
|
+
if (!this.reserveCallbackCapacity(run, 0, bytes, 0)) return;
|
|
280
|
+
share.outputBytes -= bytes;
|
|
281
|
+
this.completeToolPart(run, part, event.toolCallId, outcome);
|
|
282
|
+
} catch (error) {
|
|
283
|
+
this.failCallback(run, this.core.projectExternalError(run, error, 'toolCallback'));
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
private startToolPart(
|
|
288
|
+
run: IActiveRun,
|
|
289
|
+
event: { toolCallId: string; toolName: string; parentToolCallId?: string },
|
|
290
|
+
input: IFlexToolMessagePart['input'],
|
|
291
|
+
): void {
|
|
292
|
+
const part: IFlexToolMessagePart = {
|
|
293
|
+
partId: plugins.crypto.randomUUID(),
|
|
294
|
+
type: 'tool',
|
|
295
|
+
toolCallId: this.core.boundedIdentifier(event.toolCallId, 'toolCallId'),
|
|
296
|
+
toolName: this.core.boundedIdentifier(event.toolName, 'toolName'),
|
|
297
|
+
status: 'running',
|
|
298
|
+
input,
|
|
299
|
+
...(event.parentToolCallId === undefined
|
|
300
|
+
? {}
|
|
301
|
+
: { parentToolCallId: this.core.boundedIdentifier(event.parentToolCallId, 'parentToolCallId') }),
|
|
302
|
+
startedAt: new Date().toISOString(),
|
|
303
|
+
};
|
|
304
|
+
run.toolPartIds.set(event.toolCallId, part.partId);
|
|
305
|
+
run.callbackParts.push(part);
|
|
306
|
+
this.core.emitPartEvent(run, 'part.started', part);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
private runningToolPart(run: IActiveRun, toolCallId: string): IFlexToolMessagePart | undefined {
|
|
310
|
+
const partId = run.toolPartIds.get(toolCallId);
|
|
311
|
+
const part = run.callbackParts.find((entry) => entry.partId === partId && entry.type === 'tool');
|
|
312
|
+
return part?.type === 'tool' && part.status === 'running' ? part : undefined;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** A finished call's output in at most `maxOutputBytes`, or its projected error. */
|
|
316
|
+
private toolOutcome(
|
|
317
|
+
run: IActiveRun,
|
|
318
|
+
event: TFlexAgentToolCallFinishEvent,
|
|
319
|
+
maxOutputBytes: number,
|
|
320
|
+
): TToolPartOutcome {
|
|
321
|
+
const trustedExecutionError = run.trustedToolExecutionErrors.get(event.toolCallId);
|
|
322
|
+
run.trustedToolExecutionErrors.delete(event.toolCallId);
|
|
323
|
+
if (event.success) {
|
|
324
|
+
return { output: normalizeJsonValue(event.output, { ...this.core.toolOutputLimits, maxBytes: maxOutputBytes }) };
|
|
325
|
+
}
|
|
326
|
+
return { error: this.core.projectExternalError(run, trustedExecutionError ?? event.error, 'toolCallback').message };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
private completeToolPart(
|
|
330
|
+
run: IActiveRun,
|
|
331
|
+
part: IFlexToolMessagePart,
|
|
332
|
+
toolCallId: string,
|
|
333
|
+
outcome: TToolPartOutcome,
|
|
334
|
+
): void {
|
|
335
|
+
if ('output' in outcome) {
|
|
336
|
+
part.status = 'completed';
|
|
337
|
+
part.output = outcome.output;
|
|
338
|
+
} else {
|
|
339
|
+
part.status = 'failed';
|
|
340
|
+
delete part.output;
|
|
341
|
+
part.error = outcome.error;
|
|
342
|
+
}
|
|
343
|
+
const original = run.deduplicatedToolCalls.get(toolCallId);
|
|
344
|
+
if (original !== undefined) {
|
|
345
|
+
part.deduplicatedFrom = this.core.boundedIdentifier(original, 'deduplicatedFrom');
|
|
346
|
+
}
|
|
347
|
+
part.completedAt = new Date().toISOString();
|
|
348
|
+
this.core.emitPartEvent(run, 'part.completed', part);
|
|
349
|
+
}
|
|
350
|
+
|
|
209
351
|
public onToolUpdate(
|
|
210
352
|
state: IStorageState,
|
|
211
353
|
sessionId: string,
|
|
@@ -218,9 +360,8 @@ export class FlexHarnessRunCallbacks<TScope> {
|
|
|
218
360
|
run = this.callbackRun(state, sessionId);
|
|
219
361
|
if (!run) return;
|
|
220
362
|
}
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
if (!part || part.type !== 'tool' || part.status !== 'running') return;
|
|
363
|
+
const part = this.runningToolPart(run, event.toolCallId);
|
|
364
|
+
if (!part) return;
|
|
224
365
|
try {
|
|
225
366
|
const output = normalizeJsonValue(event.output, this.core.toolOutputLimits);
|
|
226
367
|
if (!this.reserveCallbackCapacity(run, 1, jsonBytes(output) - (part.output === undefined ? 0 : jsonBytes(part.output)), 0)) return;
|
|
@@ -318,11 +459,27 @@ export class FlexHarnessRunCallbacks<TScope> {
|
|
|
318
459
|
return !run || run.callbacksClosed || run.callbackError ? undefined : run;
|
|
319
460
|
}
|
|
320
461
|
|
|
462
|
+
/** Reserves room in the run's callback limits; a reservation past them fails the run. */
|
|
321
463
|
public reserveCallbackCapacity(
|
|
322
464
|
run: IActiveRun,
|
|
323
465
|
eventCount: number,
|
|
324
466
|
outputBytes: number,
|
|
325
467
|
partCount: number,
|
|
468
|
+
): boolean {
|
|
469
|
+
if (this.tryReserveCallbackCapacity(run, eventCount, outputBytes, partCount)) return true;
|
|
470
|
+
this.failCallback(run, this.core.trustInternalError(new FlexHarnessCallbackOverflowError(
|
|
471
|
+
`Callback buffer exceeded its limit (${run.callbackEventCount + eventCount} events, `
|
|
472
|
+
+ `${run.callbackOutputBytes + outputBytes} bytes, ${run.callbackParts.length + partCount} parts).`,
|
|
473
|
+
)));
|
|
474
|
+
return false;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/** Reserves room in the run's callback limits; false, reserving nothing, when it does not fit. */
|
|
478
|
+
private tryReserveCallbackCapacity(
|
|
479
|
+
run: IActiveRun,
|
|
480
|
+
eventCount: number,
|
|
481
|
+
outputBytes: number,
|
|
482
|
+
partCount: number,
|
|
326
483
|
): boolean {
|
|
327
484
|
const nextEvents = run.callbackEventCount + eventCount;
|
|
328
485
|
const nextOutputBytes = run.callbackOutputBytes + outputBytes;
|
|
@@ -334,12 +491,7 @@ export class FlexHarnessRunCallbacks<TScope> {
|
|
|
334
491
|
|| nextEvents > this.core.callbackLimits.maxEvents
|
|
335
492
|
|| nextOutputBytes > this.core.callbackLimits.maxOutputBytes
|
|
336
493
|
|| nextParts > this.core.callbackLimits.maxParts
|
|
337
|
-
)
|
|
338
|
-
this.failCallback(run, this.core.trustInternalError(new FlexHarnessCallbackOverflowError(
|
|
339
|
-
`Callback buffer exceeded its limit (${nextEvents} events, ${nextOutputBytes} bytes, ${nextParts} parts).`,
|
|
340
|
-
)));
|
|
341
|
-
return false;
|
|
342
|
-
}
|
|
494
|
+
) return false;
|
|
343
495
|
run.callbackEventCount = nextEvents;
|
|
344
496
|
run.callbackOutputBytes = nextOutputBytes;
|
|
345
497
|
return true;
|