@modelprofile.com/flexharness 10.12.0 → 10.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/classes.codesandbox.d.ts +39 -0
  3. package/dist_ts/classes.codesandbox.js +539 -0
  4. package/dist_ts/classes.flexharness.d.ts +17 -0
  5. package/dist_ts/classes.flexharness.js +222 -8
  6. package/dist_ts/classes.toolcatalog.d.ts +90 -0
  7. package/dist_ts/classes.toolcatalog.js +1398 -0
  8. package/dist_ts/index.d.ts +1 -0
  9. package/dist_ts/index.js +2 -1
  10. package/dist_ts/interfaces.d.ts +237 -3
  11. package/dist_ts/interfaces.js +50 -1
  12. package/dist_ts/plugins.d.ts +66 -1
  13. package/dist_ts/plugins.js +19 -1
  14. package/dist_ts/utils.harnessoptions.d.ts +7 -1
  15. package/dist_ts/utils.harnessoptions.js +48 -2
  16. package/dist_ts/utils.json.js +3 -1
  17. package/dist_ts/utils.schemadeclarations.d.ts +53 -0
  18. package/dist_ts/utils.schemadeclarations.js +315 -0
  19. package/dist_ts/utils.tooloutput.d.ts +2 -0
  20. package/dist_ts/utils.tooloutput.js +11 -1
  21. package/dist_ts_compaction/plugins.d.ts +3 -3
  22. package/dist_ts_compaction/plugins.js +3 -3
  23. package/dist_ts_compaction/smartagent.compaction.d.ts +10 -1
  24. package/dist_ts_compaction/smartagent.compaction.js +79 -38
  25. package/package.json +17 -4
  26. package/readme.md +1 -1
  27. package/ts/00_commitinfo_data.ts +1 -1
  28. package/ts/classes.codesandbox.ts +565 -0
  29. package/ts/classes.flexharness.ts +242 -7
  30. package/ts/classes.toolcatalog.ts +1512 -0
  31. package/ts/index.ts +1 -0
  32. package/ts/interfaces.ts +285 -3
  33. package/ts/plugins.ts +86 -0
  34. package/ts/readme.md +1 -1
  35. package/ts/utils.harnessoptions.ts +55 -0
  36. package/ts/utils.json.ts +2 -0
  37. package/ts/utils.schemadeclarations.ts +322 -0
  38. package/ts/utils.tooloutput.ts +9 -0
  39. package/ts_compaction/plugins.ts +3 -3
  40. package/ts_compaction/smartagent.compaction.ts +90 -34
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "author": "Task Venture Capital GmbH",
3
3
  "license": "MIT",
4
4
  "name": "@modelprofile.com/flexharness",
5
- "version": "10.12.0",
5
+ "version": "10.14.0",
6
6
  "type": "module",
7
7
  "description": "Managed agent sessions with tools, MCP, chat, OCR, documents and browser control through focused subpath imports.",
8
8
  "main": "./dist_ts/index.js",
@@ -74,8 +74,9 @@
74
74
  }
75
75
  },
76
76
  "dependencies": {
77
- "@modelprofile.com/flexharness-agent": "10.12.0",
78
- "@modelprofile.com/flexharness-models": "10.12.0",
77
+ "@cfworker/json-schema": "^4.1.1",
78
+ "@modelprofile.com/flexharness-agent": "10.14.0",
79
+ "@modelprofile.com/flexharness-models": "10.14.0",
79
80
  "@push.rocks/smartrequest": "^5.5.1",
80
81
  "ai": "^7.0.111",
81
82
  "zod": "^4.6.5"
@@ -92,7 +93,10 @@
92
93
  "lit": "^3.2.0",
93
94
  "@types/react": "^19.0.0",
94
95
  "@lossless.org/client": "^1.17.0",
95
- "@design.estate/dees-catalog": "^17.0.0 || ^19.5.0 || ^20.0.0 || ^21.0.0"
96
+ "@design.estate/dees-catalog": "^17.0.0 || ^19.5.0 || ^20.0.0 || ^21.0.0",
97
+ "quickjs-emscripten-core": "^0.32.0",
98
+ "@jitl/quickjs-ng-wasmfile-release-sync": "^0.32.0",
99
+ "amaro": "^1.2.1"
96
100
  },
97
101
  "peerDependenciesMeta": {
98
102
  "@modelcontextprotocol/sdk": {
@@ -130,6 +134,15 @@
130
134
  },
131
135
  "@design.estate/dees-catalog": {
132
136
  "optional": true
137
+ },
138
+ "quickjs-emscripten-core": {
139
+ "optional": true
140
+ },
141
+ "@jitl/quickjs-ng-wasmfile-release-sync": {
142
+ "optional": true
143
+ },
144
+ "amaro": {
145
+ "optional": true
133
146
  }
134
147
  },
135
148
  "engines": {
package/readme.md CHANGED
@@ -1026,7 +1026,7 @@ A `contextCompactor` states which model to compact with from the model the compa
1026
1026
 
1027
1027
  A compactor uses `resolvedModel.model` directly, and resolves `modelHint` the way its model resolver resolves a prompt's `modelHint`. Only when neither member is present, because nothing dispatched states a model, does the host choose the compaction model itself.
1028
1028
 
1029
- A `context-overflow` or `context-budget` compaction also carries `system` and `providerOptions`: the instructions and the provider options (cache options applied, so `store: false` on OpenAI) the run's turns are sent with. `compactMessages()` sends its summary call with them as one streamed call, the shape of every turn, so a compactor that passes its options through reaches the provider the way the run does. Without `system` it sends its own instructions.
1029
+ A `context-overflow` or `context-budget` compaction also carries `system` and `providerOptions`: the instructions and the provider options (cache options applied, so `store: false` on OpenAI) the run's turns are sent with. A `context-budget` compaction on a provider that caches the prefix also carries the turns' `tools`. `compactMessages()` sends its summary call with them as one streamed call, the shape of every turn, with the tools' definitions but none of their code, so a compactor that passes its options through repeats the run's prefix and reads it from the provider's prompt cache; definitions that outweigh the history nine times are left out. Without `system` it sends its own instructions.
1030
1030
 
1031
1031
  ```typescript
1032
1032
  agentSessionPolicy: {
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@modelprofile.com/flexharness',
6
- version: '10.12.0',
6
+ version: '10.14.0',
7
7
  description: 'The FlexHarness toolbox source repository and shared dependency catalog.'
8
8
  }
@@ -0,0 +1,565 @@
1
+ import * as plugins from './plugins.js';
2
+ import { FlexHarnessValidationError } from './errors.js';
3
+ import type { IFlexCodeModeLimits, IFlexCodeRunFailure, TJsonValue } from './interfaces.js';
4
+
5
+ /** What one catalog call of a program comes to: a JSON value, or the error the program sees. */
6
+ export type TFlexCodeCallOutcome = { ok: true; value: TJsonValue } | { ok: false; error: TJsonValue };
7
+
8
+ export interface IFlexCodeHost {
9
+ /**
10
+ * Runs one catalog call of the program; `signal` aborts when the program stops. A rejection is a
11
+ * failure of the host, which stops the program and rejects `run()` with it.
12
+ */
13
+ call(method: string, params: unknown, signal: AbortSignal): Promise<TFlexCodeCallOutcome>;
14
+ /** Milliseconds the program has waited for permission decisions; they do not count as wall time. */
15
+ waitedMs?(): number;
16
+ }
17
+
18
+ export type TFlexCodeRunResult =
19
+ | { ok: true; value: TJsonValue; calls: number; logs?: string }
20
+ | { ok: false; failure: IFlexCodeRunFailure };
21
+
22
+ type TStop =
23
+ | { kind: 'limit'; limit: NonNullable<IFlexCodeRunFailure['limit']>; message: string }
24
+ | { kind: 'aborted'; message: string }
25
+ | { kind: 'host'; error: unknown };
26
+
27
+ const programFile = 'program.ts';
28
+ /** How long calls in flight get to settle after their program stopped and their signal aborted. */
29
+ const stopGraceMs = 5_000;
30
+
31
+ /** Evaluating the prelude, the program's text, or running the program. */
32
+ type TPhase = 'prelude' | 'parse' | 'program';
33
+ const programPrefix = 'globalThis.__flexMain = async function () {\n';
34
+ const programSuffix = '\n};';
35
+
36
+ /**
37
+ * Runs in the context before the program: it takes the host functions off the global object and
38
+ * defines `api`, `ApiError` and `console`. Only strings cross the boundary.
39
+ */
40
+ const prelude = `'use strict';
41
+ (() => {
42
+ const call = globalThis.__flexCall;
43
+ const log = globalThis.__flexLog;
44
+ const done = globalThis.__flexDone;
45
+ delete globalThis.__flexCall;
46
+ delete globalThis.__flexLog;
47
+ delete globalThis.__flexDone;
48
+ class ApiError extends Error {
49
+ constructor(details) {
50
+ const message = details && typeof details.message === 'string' ? details.message : JSON.stringify(details);
51
+ super(message);
52
+ this.name = 'ApiError';
53
+ this.details = details;
54
+ }
55
+ }
56
+ const invoke = (path, args) => {
57
+ if (!path) return Promise.reject(new TypeError('Call a method as api.<namespace>.<method>(params).'));
58
+ let params;
59
+ try {
60
+ params = JSON.stringify(args.length === 0 || args[0] === undefined ? {} : args[0]);
61
+ } catch (error) {
62
+ return Promise.reject(new TypeError('The params of ' + path + ' are not JSON: ' + error.message));
63
+ }
64
+ return call(path, params === undefined ? 'null' : params).then((text) => {
65
+ const outcome = JSON.parse(text);
66
+ if (outcome.ok) return outcome.value;
67
+ throw new ApiError(outcome.error);
68
+ });
69
+ };
70
+ const target = (path) => new Proxy(function () {}, {
71
+ get: (_target, key) => typeof key === 'string' && key !== 'then' ? target(path ? path + '.' + key : key) : undefined,
72
+ apply: (_target, _this, args) => invoke(path, args),
73
+ set: () => false,
74
+ defineProperty: () => false,
75
+ deleteProperty: () => false,
76
+ });
77
+ const format = (value) => {
78
+ if (typeof value === 'string') return value;
79
+ try {
80
+ const text = JSON.stringify(value);
81
+ return text === undefined ? String(value) : text;
82
+ } catch {
83
+ return String(value);
84
+ }
85
+ };
86
+ const writer = (level) => (...values) => { log(level, values.map(format).join(' ')); };
87
+ const fixed = (value) => ({ value, enumerable: false, writable: false, configurable: false });
88
+ Object.defineProperty(globalThis, 'api', fixed(target('')));
89
+ Object.defineProperty(globalThis, 'ApiError', fixed(ApiError));
90
+ Object.defineProperty(globalThis, 'console', fixed(Object.freeze({
91
+ log: writer('log'), info: writer('info'), warn: writer('warn'), error: writer('error'), debug: writer('debug'),
92
+ })));
93
+ const settle = (value) => {
94
+ try {
95
+ const text = JSON.stringify({ ok: true, value: value === undefined ? null : value });
96
+ return text === undefined ? JSON.stringify({ ok: true, value: null }) : text;
97
+ } catch (error) {
98
+ return JSON.stringify({ ok: false, name: 'TypeError', message: 'The return value is not JSON: ' + error.message });
99
+ }
100
+ };
101
+ const fail = (error) => JSON.stringify({
102
+ ok: false,
103
+ name: error && typeof error.name === 'string' ? error.name : 'Error',
104
+ message: error && typeof error.message === 'string' ? error.message : String(error),
105
+ stack: error && typeof error.stack === 'string' ? error.stack : '',
106
+ ...(error instanceof ApiError ? { details: error.details } : {}),
107
+ });
108
+ Object.defineProperty(globalThis, '__flexStart', {
109
+ configurable: true,
110
+ value: () => {
111
+ const main = globalThis.__flexMain;
112
+ delete globalThis.__flexStart;
113
+ delete globalThis.__flexMain;
114
+ main().then(settle, fail).then(done);
115
+ },
116
+ });
117
+ })();
118
+ `;
119
+
120
+ let enginePromise: Promise<plugins.ICodeSandboxEngine> | undefined;
121
+
122
+ /**
123
+ * Runs `run_code` programs in QuickJS-ng compiled to WebAssembly: every program in a WebAssembly
124
+ * instance of its own, with no globals beyond the language, `api`, `ApiError` and `console`, and
125
+ * every limit of `IFlexCodeModeLimits` enforced. Program types are stripped with amaro first.
126
+ */
127
+ export class FlexCodeSandbox {
128
+ private constructor(private readonly engine: plugins.ICodeSandboxEngine) {}
129
+
130
+ /** Loads QuickJS and amaro, once per process. */
131
+ public static async load(): Promise<FlexCodeSandbox> {
132
+ enginePromise ??= plugins.importCodeSandboxEngine().catch((error: unknown) => {
133
+ enginePromise = undefined;
134
+ throw new FlexHarnessValidationError(
135
+ 'Code mode needs the optional peers quickjs-emscripten-core, '
136
+ + '@jitl/quickjs-ng-wasmfile-release-sync and amaro.',
137
+ { cause: error },
138
+ );
139
+ });
140
+ return new FlexCodeSandbox(await enginePromise);
141
+ }
142
+
143
+ public run(
144
+ source: string,
145
+ host: IFlexCodeHost,
146
+ limits: Readonly<IFlexCodeModeLimits>,
147
+ signal?: AbortSignal,
148
+ ): Promise<TFlexCodeRunResult> {
149
+ return new CodeRun(this.engine, source, host, limits, signal).start();
150
+ }
151
+ }
152
+
153
+ class CodeRun {
154
+ private runtime?: plugins.IQuickJsRuntime;
155
+ private vm?: plugins.IQuickJsContext;
156
+ private readonly deferreds = new Set<plugins.IQuickJsDeferredPromise>();
157
+ private readonly calls = new AbortController();
158
+ private readonly queue: Array<() => void> = [];
159
+ /** Host calls in flight; the run settles only after every one of them did. */
160
+ private readonly inFlight = new Set<Promise<void>>();
161
+ private running = 0;
162
+ private callCount = 0;
163
+ private cpuMs = 0;
164
+ private enteredAt?: number;
165
+ private stop?: TStop;
166
+ private logs = '';
167
+ private logBytes = 0;
168
+ private logsCut = false;
169
+ private settle!: (result: TFlexCodeRunResult) => void;
170
+ private reject!: (error: unknown) => void;
171
+ /** The outcome is decided; the run waits for its calls in flight, then settles. */
172
+ private closing = false;
173
+ private finished = false;
174
+ private broken = false;
175
+ private wallTimer?: ReturnType<typeof setTimeout>;
176
+ private readonly startedAt = performance.now();
177
+
178
+ constructor(
179
+ private readonly engine: plugins.ICodeSandboxEngine,
180
+ private readonly source: string,
181
+ private readonly host: IFlexCodeHost,
182
+ private readonly limits: Readonly<IFlexCodeModeLimits>,
183
+ private readonly signal: AbortSignal | undefined,
184
+ ) {}
185
+
186
+ public async start(): Promise<TFlexCodeRunResult> {
187
+ if (Buffer.byteLength(this.source, 'utf8') > this.limits.maxSourceBytes) {
188
+ return this.failure({
189
+ error: 'limit_exceeded',
190
+ limit: 'source',
191
+ message: `The program is longer than ${this.limits.maxSourceBytes} bytes.`,
192
+ });
193
+ }
194
+ try {
195
+ this.program = this.engine.stripTypes(`${programPrefix}${this.source}${programSuffix}`);
196
+ } catch (error) {
197
+ const reported = (error ?? {}) as plugins.IAmaroError;
198
+ return this.failure({
199
+ error: 'syntax_error',
200
+ message: typeof reported.message === 'string' ? reported.message : 'The program cannot be parsed.',
201
+ ...(typeof reported.startLine === 'number' && reported.startLine > 1 ? { line: reported.startLine - 1 } : {}),
202
+ });
203
+ }
204
+ const outcome = new Promise<TFlexCodeRunResult>((resolve, reject) => {
205
+ this.settle = resolve;
206
+ this.reject = reject;
207
+ });
208
+ const onAbort = () => this.halt({ kind: 'aborted', message: 'The program was stopped.' });
209
+ this.signal?.addEventListener('abort', onAbort, { once: true });
210
+ try {
211
+ const module = await this.engine.newModule();
212
+ // QuickJS runs on the host's stack too: start it from a fresh, shallow one.
213
+ await new Promise<void>((resolve) => setImmediate(resolve));
214
+ if (this.signal?.aborted) onAbort();
215
+ else {
216
+ this.armWallTimer();
217
+ this.boot(module);
218
+ }
219
+ return await outcome;
220
+ } finally {
221
+ this.signal?.removeEventListener('abort', onAbort);
222
+ this.dispose();
223
+ }
224
+ }
225
+
226
+ private program = '';
227
+
228
+ private boot(module: plugins.IQuickJsModule): void {
229
+ const runtime = module.newRuntime();
230
+ this.runtime = runtime;
231
+ runtime.setMemoryLimit(this.limits.memoryBytes);
232
+ runtime.setMaxStackSize(this.limits.stackBytes);
233
+ runtime.setInterruptHandler(() => this.interrupted());
234
+ const vm = runtime.newContext();
235
+ this.vm = vm;
236
+ this.define('__flexCall', (pathHandle, paramsHandle) => this.bridgeCall(vm.getString(pathHandle), vm.getString(paramsHandle)));
237
+ this.define('__flexLog', (levelHandle, textHandle) => {
238
+ this.log(vm.getString(levelHandle), vm.getString(textHandle));
239
+ });
240
+ this.define('__flexDone', (textHandle) => {
241
+ this.complete(vm.getString(textHandle));
242
+ });
243
+ if (!this.evaluate(prelude, 'prelude.js', 'prelude')) return;
244
+ if (!this.evaluate(this.program, programFile, 'parse')) return;
245
+ if (!this.evaluate('__flexStart();', 'start.js', 'program')) return;
246
+ this.pump();
247
+ }
248
+
249
+ private define(name: string, implementation: (...args: plugins.IQuickJsHandle[]) => plugins.IQuickJsHandle | void): void {
250
+ const handle = this.vm!.newFunction(name, implementation);
251
+ this.vm!.setProp(this.vm!.global, name, handle);
252
+ handle.dispose();
253
+ }
254
+
255
+ private evaluate(code: string, file: string, phase: TPhase): boolean {
256
+ if (this.closing) return false;
257
+ let result: plugins.TQuickJsResult;
258
+ this.enter();
259
+ try {
260
+ result = this.vm!.evalCode(code, file);
261
+ } catch (error) {
262
+ this.hostThrow(error);
263
+ return false;
264
+ } finally {
265
+ this.leave();
266
+ }
267
+ if (result.error) {
268
+ this.failWith(result.error, phase);
269
+ return false;
270
+ }
271
+ result.value.dispose();
272
+ return true;
273
+ }
274
+
275
+ /** Runs the program's pending jobs after a call settled or the program started. */
276
+ private pump(): void {
277
+ if (this.closing || !this.runtime?.alive) return;
278
+ let result: { error?: plugins.IQuickJsHandle };
279
+ this.enter();
280
+ try {
281
+ result = this.runtime.executePendingJobs();
282
+ } catch (error) {
283
+ this.hostThrow(error);
284
+ return;
285
+ } finally {
286
+ this.leave();
287
+ }
288
+ if (result.error) this.failWith(result.error, 'program');
289
+ }
290
+
291
+ /**
292
+ * An exception out of the WebAssembly instance: the host's stack ran out under deep recursion, or
293
+ * the instance failed. Its state is not trusted afterwards; `dispose()` abandons it.
294
+ */
295
+ private hostThrow(error: unknown): void {
296
+ this.broken = true;
297
+ if (error instanceof RangeError) {
298
+ this.halt({ kind: 'limit', limit: 'stack', message: 'The program exceeded its stack.' });
299
+ return;
300
+ }
301
+ this.halt({ kind: 'host', error });
302
+ }
303
+
304
+ private enter(): void {
305
+ this.enteredAt = performance.now();
306
+ }
307
+
308
+ private leave(): void {
309
+ if (this.enteredAt === undefined) return;
310
+ this.cpuMs += performance.now() - this.enteredAt;
311
+ this.enteredAt = undefined;
312
+ }
313
+
314
+ private interrupted(): boolean {
315
+ if (this.stop) return true;
316
+ if (this.enteredAt !== undefined && this.cpuMs + performance.now() - this.enteredAt > this.limits.cpuMs) {
317
+ this.stop = { kind: 'limit', limit: 'cpu', message: `The program computed for more than ${this.limits.cpuMs} ms.` };
318
+ return true;
319
+ }
320
+ return false;
321
+ }
322
+
323
+ private bridgeCall(method: string, paramsText: string): plugins.IQuickJsHandle {
324
+ const deferred = this.vm!.newPromise();
325
+ this.deferreds.add(deferred);
326
+ this.callCount += 1;
327
+ if (this.callCount > this.limits.maxCalls) {
328
+ this.halt({
329
+ kind: 'limit',
330
+ limit: 'calls',
331
+ message: `The program made more than ${this.limits.maxCalls} calls.`,
332
+ });
333
+ return deferred.handle;
334
+ }
335
+ let params: unknown;
336
+ try {
337
+ params = JSON.parse(paramsText);
338
+ } catch {
339
+ params = undefined;
340
+ }
341
+ const run = (): void => {
342
+ if (this.closing) return;
343
+ this.running += 1;
344
+ // A settled call stops counting before the program sees its answer, which may end the program.
345
+ const settled = (): void => {
346
+ this.running -= 1;
347
+ this.inFlight.delete(call);
348
+ };
349
+ const call: Promise<void> = this.host.call(method, params, this.calls.signal).then(
350
+ (outcome) => {
351
+ settled();
352
+ this.answer(deferred, outcome);
353
+ },
354
+ (error: unknown) => {
355
+ settled();
356
+ this.halt({ kind: 'host', error });
357
+ },
358
+ ).finally(() => this.queue.shift()?.());
359
+ this.inFlight.add(call);
360
+ };
361
+ if (this.running < this.limits.maxConcurrentCalls) run();
362
+ else this.queue.push(run);
363
+ return deferred.handle;
364
+ }
365
+
366
+ private answer(deferred: plugins.IQuickJsDeferredPromise, outcome: TFlexCodeCallOutcome): void {
367
+ if (this.closing || !deferred.alive) return;
368
+ let text: string;
369
+ try {
370
+ text = JSON.stringify(outcome);
371
+ } catch {
372
+ text = JSON.stringify({ ok: false, error: { error: 'failed', message: 'The result is not JSON.' } });
373
+ }
374
+ const value = this.vm!.newString(text);
375
+ deferred.resolve(value);
376
+ value.dispose();
377
+ this.deferreds.delete(deferred);
378
+ this.pump();
379
+ }
380
+
381
+ private log(level: string, text: string): void {
382
+ if (this.logsCut || this.closing) return;
383
+ const line = `${level === 'log' ? '' : `[${level}] `}${text}\n`;
384
+ const bytes = Buffer.byteLength(line, 'utf8');
385
+ if (this.logBytes + bytes > this.limits.maxLogBytes) {
386
+ this.logsCut = true;
387
+ this.logs += '[console output cut off]\n';
388
+ return;
389
+ }
390
+ this.logBytes += bytes;
391
+ this.logs += line;
392
+ }
393
+
394
+ private complete(text: string): void {
395
+ if (this.closing) return;
396
+ let outcome: { ok: boolean; value?: TJsonValue; name?: string; message?: string; stack?: string };
397
+ try {
398
+ outcome = JSON.parse(text);
399
+ } catch {
400
+ outcome = { ok: false, name: 'Error', message: 'The program ended without a result.' };
401
+ }
402
+ if (outcome.ok) {
403
+ const unawaited = this.running + this.queue.length;
404
+ if (unawaited > 0) {
405
+ // Calls the program did not await are not part of its result: it fails, and proposes nothing.
406
+ this.done(this.failure({
407
+ error: 'program_error',
408
+ message: `The program returned with ${unawaited} call${unawaited === 1 ? '' : 's'} it did not await; await every call.`,
409
+ }));
410
+ return;
411
+ }
412
+ this.done({
413
+ ok: true,
414
+ value: outcome.value ?? null,
415
+ calls: Math.min(this.callCount, this.limits.maxCalls),
416
+ ...(this.logs ? { logs: this.logs } : {}),
417
+ });
418
+ return;
419
+ }
420
+ this.done(this.failure(this.classify(outcome, 'program')));
421
+ }
422
+
423
+ private failWith(handle: plugins.IQuickJsHandle, phase: TPhase): void {
424
+ let dumped: unknown;
425
+ // Reading the thrown value can run program code, such as a toJSON: it counts as computing.
426
+ this.enter();
427
+ try {
428
+ dumped = this.vm!.dump(handle);
429
+ } catch (error) {
430
+ if (!(error instanceof Error) || error instanceof RangeError) this.broken = true;
431
+ dumped = undefined;
432
+ } finally {
433
+ this.leave();
434
+ if (!this.broken && handle.alive) handle.dispose();
435
+ }
436
+ if (phase === 'prelude' && !this.stop) {
437
+ this.halt({ kind: 'host', error: new Error(`The code mode prelude failed: ${JSON.stringify(dumped)}`) });
438
+ return;
439
+ }
440
+ const error = (dumped && typeof dumped === 'object' ? dumped : { message: String(dumped) }) as {
441
+ name?: string; message?: string; stack?: string;
442
+ };
443
+ this.done(this.failure(this.classify(error, phase)));
444
+ }
445
+
446
+ private classify(
447
+ error: { name?: string; message?: string; stack?: string },
448
+ phase: TPhase,
449
+ ): Omit<IFlexCodeRunFailure, 'calls'> {
450
+ if (this.stop?.kind === 'limit') return { error: 'limit_exceeded', limit: this.stop.limit, message: this.stop.message };
451
+ if (this.stop?.kind === 'aborted') return { error: 'aborted', message: this.stop.message };
452
+ const message = typeof error.message === 'string' ? error.message : 'The program failed.';
453
+ if (error.name === 'InternalError' && message === 'out of memory') {
454
+ return { error: 'limit_exceeded', limit: 'memory', message: `The program used more than ${this.limits.memoryBytes} bytes of memory.` };
455
+ }
456
+ if (
457
+ (error.name === 'InternalError' && message === 'stack overflow')
458
+ || (error.name === 'RangeError' && message === 'Maximum call stack size exceeded')
459
+ ) {
460
+ return { error: 'limit_exceeded', limit: 'stack', message: 'The program exceeded its stack.' };
461
+ }
462
+ if (error.name === 'InternalError' && message === 'interrupted') {
463
+ return { error: 'limit_exceeded', limit: 'cpu', message: `The program computed for more than ${this.limits.cpuMs} ms.` };
464
+ }
465
+ const line = /program\.ts:(\d+)/.exec(error.stack ?? '')?.[1];
466
+ return {
467
+ error: phase === 'parse' && error.name === 'SyntaxError' ? 'syntax_error' : 'program_error',
468
+ ...(typeof error.name === 'string' ? { name: error.name } : {}),
469
+ message,
470
+ ...(line && Number(line) > 1 ? { line: Number(line) - 1 } : {}),
471
+ };
472
+ }
473
+
474
+ private failure(failure: Omit<IFlexCodeRunFailure, 'calls'>): TFlexCodeRunResult {
475
+ return {
476
+ ok: false,
477
+ failure: {
478
+ ...failure,
479
+ calls: Math.min(this.callCount, this.limits.maxCalls),
480
+ ...(this.logs ? { logs: this.logs } : {}),
481
+ },
482
+ };
483
+ }
484
+
485
+ /** Stops the program from outside it: a limit, an abort, or a failure of the host. */
486
+ private halt(stop: TStop): void {
487
+ if (this.closing) return;
488
+ this.stop ??= stop;
489
+ const reason = this.stop;
490
+ if (reason.kind === 'host') {
491
+ this.close(() => this.reject(reason.error));
492
+ return;
493
+ }
494
+ this.done(this.failure(reason.kind === 'limit'
495
+ ? { error: 'limit_exceeded', limit: reason.limit, message: reason.message }
496
+ : { error: 'aborted', message: reason.message }));
497
+ }
498
+
499
+ private done(result: TFlexCodeRunResult): void {
500
+ this.close(() => this.settle(result));
501
+ }
502
+
503
+ /**
504
+ * Decides the run's outcome. Calls not started yet never start. Calls in flight are told to stop
505
+ * through their signal, and the outcome is delivered once they settled, so no call runs on after
506
+ * its program; a host call that ignores its signal gets `stopGraceMs`, and its answer is dropped.
507
+ */
508
+ private close(deliver: () => void): void {
509
+ if (this.closing) return;
510
+ this.closing = true;
511
+ this.queue.length = 0;
512
+ if (this.inFlight.size === 0) {
513
+ this.finished = true;
514
+ deliver();
515
+ return;
516
+ }
517
+ this.calls.abort();
518
+ let grace: ReturnType<typeof setTimeout> | undefined;
519
+ void Promise.race([
520
+ Promise.allSettled([...this.inFlight]),
521
+ new Promise<void>((resolve) => {
522
+ grace = setTimeout(resolve, stopGraceMs);
523
+ }),
524
+ ]).then(() => {
525
+ clearTimeout(grace);
526
+ this.finished = true;
527
+ deliver();
528
+ });
529
+ }
530
+
531
+ private armWallTimer(): void {
532
+ const check = (): void => {
533
+ if (this.closing) return;
534
+ const elapsed = performance.now() - this.startedAt - (this.host.waitedMs?.() ?? 0);
535
+ if (elapsed > this.limits.wallMs) {
536
+ this.halt({ kind: 'limit', limit: 'wall', message: `The program ran for more than ${this.limits.wallMs} ms.` });
537
+ return;
538
+ }
539
+ this.wallTimer = setTimeout(check, Math.max(10, Math.min(250, this.limits.wallMs - elapsed)));
540
+ };
541
+ this.wallTimer = setTimeout(check, Math.max(10, Math.min(250, this.limits.wallMs)));
542
+ }
543
+
544
+ /**
545
+ * Frees the program's runtime. An instance that threw, or whose runtime cannot be freed after a
546
+ * stack overflow left objects behind, is abandoned instead: it is the program's own, and garbage
547
+ * collection reclaims its memory.
548
+ */
549
+ private dispose(): void {
550
+ this.closing = true;
551
+ this.finished = true;
552
+ if (this.wallTimer) clearTimeout(this.wallTimer);
553
+ this.queue.length = 0;
554
+ this.calls.abort();
555
+ if (this.broken) return;
556
+ try {
557
+ for (const deferred of this.deferreds) if (deferred.alive) deferred.dispose();
558
+ this.deferreds.clear();
559
+ this.vm?.dispose();
560
+ if (this.runtime?.alive) this.runtime.dispose();
561
+ } catch {
562
+ this.broken = true;
563
+ }
564
+ }
565
+ }