@modelprofile.com/flexharness 11.1.1 → 11.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/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.codesandbox.d.ts +58 -7
- package/dist_ts/classes.codesandbox.js +267 -34
- package/dist_ts/classes.codesandbox.worker.d.ts +6 -1
- package/dist_ts/classes.codesandbox.worker.js +34 -8
- package/dist_ts/classes.flexharness.callbacks.d.ts +6 -0
- package/dist_ts/classes.flexharness.callbacks.js +24 -4
- package/dist_ts/classes.flexharness.core.d.ts +8 -0
- package/dist_ts/classes.flexharness.core.js +29 -1
- package/dist_ts/classes.flexharness.lifecycle.js +7 -1
- package/dist_ts/classes.flexharness.runs.js +9 -7
- package/dist_ts/classes.flexharness.tooldispatch.js +2 -2
- package/dist_ts/interfaces.d.ts +9 -1
- package/dist_ts/interfaces.flexharness.d.ts +5 -3
- package/dist_ts/plugins.d.ts +3 -1
- package/dist_ts/plugins.js +2 -2
- package/dist_ts_chat_harness/classes.chatclient.d.ts +7 -4
- package/dist_ts_chat_harness/classes.chatclient.js +3 -3
- package/dist_ts_chat_harness/classes.transcript.d.ts +1 -0
- package/dist_ts_chat_harness/classes.transcript.js +16 -1
- package/dist_ts_chat_harness_server/classes.chathub.js +2 -1
- package/dist_ts_chat_harness_server/classes.chatsubscription.d.ts +6 -3
- package/dist_ts_chat_harness_server/classes.chatsubscription.js +18 -4
- package/dist_ts_chat_harness_server/interfaces.d.ts +6 -0
- package/package.json +3 -3
- package/readme.md +5 -2
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.codesandbox.ts +303 -34
- package/ts/classes.codesandbox.worker.ts +38 -8
- package/ts/classes.flexharness.callbacks.ts +22 -3
- package/ts/classes.flexharness.core.ts +31 -0
- package/ts/classes.flexharness.lifecycle.ts +5 -0
- package/ts/classes.flexharness.runs.ts +8 -5
- package/ts/classes.flexharness.tooldispatch.ts +1 -1
- package/ts/interfaces.flexharness.ts +5 -3
- package/ts/interfaces.ts +10 -1
- package/ts/plugins.ts +3 -1
- package/ts/readme.md +5 -2
- package/ts_chat_harness/classes.chatclient.ts +9 -6
- package/ts_chat_harness/classes.transcript.ts +19 -0
- package/ts_chat_harness_server/classes.chathub.ts +1 -0
- package/ts_chat_harness_server/classes.chatsubscription.ts +19 -3
- package/ts_chat_harness_server/interfaces.ts +6 -0
- package/ts_chat_harness_server/readme.md +1 -1
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The QuickJS side of
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* a
|
|
2
|
+
* The QuickJS side of `run_code` programs. A worker thread of `FlexCodeSandbox` loads QuickJS once and
|
|
3
|
+
* runs one program at a time, each in a QuickJS runtime and context of its own, which it frees once the
|
|
4
|
+
* program ended by itself. A program stopped at a limit ends with its worker: terminating the worker
|
|
5
|
+
* frees its WebAssembly instance too, so a native operation that never reaches QuickJS's interrupt
|
|
6
|
+
* handler cannot outlast its limits, and a program never blocks the host's event loop.
|
|
6
7
|
*
|
|
7
8
|
* Written in erasable TypeScript that imports nothing of the package at run time, so the module loads
|
|
8
9
|
* uncompiled as well.
|
|
@@ -29,10 +30,13 @@ export type TCodeWorkerRequest =
|
|
|
29
30
|
/**
|
|
30
31
|
* What a program's worker reports. `ready` comes once QuickJS is loaded; every `start` and `answer`
|
|
31
32
|
* 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
|
+
* calls the program had no answer to when it returned. `released` follows a program that ended by
|
|
34
|
+
* itself (`done`, or `failed` short of a limit): its runtime is freed, and the worker's WebAssembly
|
|
35
|
+
* memory, which never shrinks, has `memoryBytes`.
|
|
33
36
|
*/
|
|
34
37
|
export type TCodeWorkerReport =
|
|
35
38
|
| { kind: 'ready' }
|
|
39
|
+
| { kind: 'released'; memoryBytes: number }
|
|
36
40
|
| { kind: 'call'; id: number; method: string; params: string }
|
|
37
41
|
| { kind: 'log'; text: string }
|
|
38
42
|
| { kind: 'idle'; cpuMs: number }
|
|
@@ -134,7 +138,8 @@ const prelude = `'use strict';
|
|
|
134
138
|
/**
|
|
135
139
|
* One program in its QuickJS runtime. It enforces the limits QuickJS can see: memory, stack, console
|
|
136
140
|
* 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.
|
|
141
|
+
* when the program computes past its time between two interrupt points. Once the program's outcome is
|
|
142
|
+
* reported, nothing more of it runs: work it left pending is interrupted and dropped with its runtime.
|
|
138
143
|
*/
|
|
139
144
|
class CodeWorkerRun {
|
|
140
145
|
private readonly report: (report: TCodeWorkerReport) => void;
|
|
@@ -177,6 +182,19 @@ class CodeWorkerRun {
|
|
|
177
182
|
this.idle();
|
|
178
183
|
}
|
|
179
184
|
|
|
185
|
+
/** The program ended by itself, so its runtime can be freed and the worker run the next program. */
|
|
186
|
+
public get finished(): boolean {
|
|
187
|
+
return this.ended && this.stopped === undefined;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Frees the program's runtime with everything in it, and the calls it left unanswered. */
|
|
191
|
+
public release(): void {
|
|
192
|
+
for (const deferred of this.deferreds.values()) deferred.dispose();
|
|
193
|
+
this.deferreds.clear();
|
|
194
|
+
this.vm.dispose();
|
|
195
|
+
this.runtime.dispose();
|
|
196
|
+
}
|
|
197
|
+
|
|
180
198
|
/** Settles a call of the program with the host's outcome, and runs what it unblocked. */
|
|
181
199
|
public answer(id: number, text: string): void {
|
|
182
200
|
const deferred = this.deferreds.get(id);
|
|
@@ -233,7 +251,7 @@ class CodeWorkerRun {
|
|
|
233
251
|
return false;
|
|
234
252
|
}
|
|
235
253
|
result.value.dispose();
|
|
236
|
-
return
|
|
254
|
+
return !this.ended;
|
|
237
255
|
}
|
|
238
256
|
|
|
239
257
|
/** Runs the program's pending jobs after a call settled or the program started. */
|
|
@@ -259,6 +277,11 @@ class CodeWorkerRun {
|
|
|
259
277
|
}
|
|
260
278
|
|
|
261
279
|
private failWith(handle: plugins.IQuickJsHandle, phase: TCodeWorkerPhase): void {
|
|
280
|
+
if (this.ended) {
|
|
281
|
+
// The interruption of work the program left pending once its outcome was out.
|
|
282
|
+
handle.dispose();
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
262
285
|
let dumped: unknown;
|
|
263
286
|
// Reading the thrown value can run program code, such as a toJSON: it counts as computing.
|
|
264
287
|
this.enter();
|
|
@@ -268,6 +291,7 @@ class CodeWorkerRun {
|
|
|
268
291
|
dumped = undefined;
|
|
269
292
|
} finally {
|
|
270
293
|
this.leave();
|
|
294
|
+
handle.dispose();
|
|
271
295
|
}
|
|
272
296
|
if (this.stopped) {
|
|
273
297
|
this.stop(this.stopped);
|
|
@@ -313,7 +337,7 @@ class CodeWorkerRun {
|
|
|
313
337
|
}
|
|
314
338
|
|
|
315
339
|
private interrupted(): boolean {
|
|
316
|
-
if (this.stopped) return true;
|
|
340
|
+
if (this.stopped || this.ended) return true;
|
|
317
341
|
if (this.enteredAt !== undefined && this.cpuMs + performance.now() - this.enteredAt > this.limits.cpuMs) {
|
|
318
342
|
this.stopped = 'cpu';
|
|
319
343
|
return true;
|
|
@@ -340,6 +364,12 @@ if (port) {
|
|
|
340
364
|
} else {
|
|
341
365
|
run?.answer(request.id, request.text);
|
|
342
366
|
}
|
|
367
|
+
// A failure to free the runtime throws: the worker fails, and the host ends it.
|
|
368
|
+
if (run?.finished) {
|
|
369
|
+
run.release();
|
|
370
|
+
run = undefined;
|
|
371
|
+
report({ kind: 'released', memoryBytes: module.getWasmMemory().buffer.byteLength });
|
|
372
|
+
}
|
|
343
373
|
});
|
|
344
374
|
report({ kind: 'ready' });
|
|
345
375
|
}
|
|
@@ -39,7 +39,7 @@ export class FlexHarnessRunCallbacks<TScope> {
|
|
|
39
39
|
const run = this.callbackRun(state, sessionId);
|
|
40
40
|
if (!run || !delta) return;
|
|
41
41
|
const lastPart = run.callbackParts.at(-1);
|
|
42
|
-
const openPart = lastPart?.type === 'text' &&
|
|
42
|
+
const openPart = lastPart?.type === 'text' && run.stepPartIds.has(lastPart.partId)
|
|
43
43
|
? lastPart
|
|
44
44
|
: undefined;
|
|
45
45
|
const created = !openPart;
|
|
@@ -54,6 +54,7 @@ export class FlexHarnessRunCallbacks<TScope> {
|
|
|
54
54
|
)) return;
|
|
55
55
|
if (created) {
|
|
56
56
|
run.callbackParts.push(part);
|
|
57
|
+
run.stepPartIds.add(part.partId);
|
|
57
58
|
run.partTextUtf8Bytes.set(part.partId, 0);
|
|
58
59
|
this.core.emitPartEvent(run, 'part.started', part);
|
|
59
60
|
}
|
|
@@ -75,6 +76,7 @@ export class FlexHarnessRunCallbacks<TScope> {
|
|
|
75
76
|
};
|
|
76
77
|
run.reasoningPartIds.set(id, part.partId);
|
|
77
78
|
run.callbackParts.push(part);
|
|
79
|
+
run.stepPartIds.add(part.partId);
|
|
78
80
|
run.partTextUtf8Bytes.set(part.partId, 0);
|
|
79
81
|
this.core.emitPartEvent(run, 'part.started', part);
|
|
80
82
|
}
|
|
@@ -115,6 +117,9 @@ export class FlexHarnessRunCallbacks<TScope> {
|
|
|
115
117
|
}
|
|
116
118
|
part.status = 'completed';
|
|
117
119
|
part.completedAt = new Date().toISOString();
|
|
120
|
+
// Model calls number their reasoning summaries afresh, so a later call's summary with the
|
|
121
|
+
// same ID is another one.
|
|
122
|
+
run.reasoningPartIds.delete(id);
|
|
118
123
|
this.core.emitPartEvent(run, 'part.completed', part);
|
|
119
124
|
}
|
|
120
125
|
|
|
@@ -128,10 +133,10 @@ export class FlexHarnessRunCallbacks<TScope> {
|
|
|
128
133
|
const run = this.callbackRun(state, sessionId);
|
|
129
134
|
if (!run) return;
|
|
130
135
|
for (const part of run.callbackParts) {
|
|
131
|
-
if (part.type !== 'text' || run.
|
|
132
|
-
run.completedTextPartIds.add(part.partId);
|
|
136
|
+
if (part.type !== 'text' || !run.stepPartIds.has(part.partId)) continue;
|
|
133
137
|
this.core.emitPartEvent(run, 'part.completed', part);
|
|
134
138
|
}
|
|
139
|
+
run.stepPartIds.clear();
|
|
135
140
|
}
|
|
136
141
|
|
|
137
142
|
private textAppendAccounting(
|
|
@@ -372,9 +377,23 @@ export class FlexHarnessRunCallbacks<TScope> {
|
|
|
372
377
|
}
|
|
373
378
|
}
|
|
374
379
|
|
|
380
|
+
/**
|
|
381
|
+
* Announces a retry. The failed model call's text and reasoning are not in the session, so their
|
|
382
|
+
* parts are removed first, last part first, each with a `part.removed` event; its tool calls
|
|
383
|
+
* stay. The removals are outside the callback limits, like completions: their number is bounded
|
|
384
|
+
* by the run's parts. What the removed parts took of the limits stays taken.
|
|
385
|
+
*/
|
|
375
386
|
public onRetry(state: IStorageState, sessionId: string, retry: plugins.IAgentRetryEvent): void {
|
|
376
387
|
const run = this.callbackRun(state, sessionId);
|
|
377
388
|
if (!run || !this.reserveCallbackCapacity(run, 1, 0, 0)) return;
|
|
389
|
+
for (let partIndex = run.callbackParts.length - 1; partIndex >= 0; partIndex--) {
|
|
390
|
+
const part = run.callbackParts[partIndex]!;
|
|
391
|
+
if (!run.stepPartIds.has(part.partId)) continue;
|
|
392
|
+
this.core.emitPartRemovedEvent(run, part);
|
|
393
|
+
run.callbackParts.splice(partIndex, 1);
|
|
394
|
+
run.partTextUtf8Bytes.delete(part.partId);
|
|
395
|
+
}
|
|
396
|
+
run.stepPartIds.clear();
|
|
378
397
|
this.core.emitEvent(run.scopeId, run.stored.session, {
|
|
379
398
|
type: 'run.retrying',
|
|
380
399
|
runId: run.runId,
|
|
@@ -43,6 +43,7 @@ import type {
|
|
|
43
43
|
TFlexReversionPolicy,
|
|
44
44
|
} from './interfaces.js';
|
|
45
45
|
import { InMemoryFlexHarnessStores } from './classes.stores.js';
|
|
46
|
+
import { FlexCodeSandbox } from './classes.codesandbox.js';
|
|
46
47
|
import {
|
|
47
48
|
assertJsonSerializable,
|
|
48
49
|
cloneSerializable,
|
|
@@ -172,6 +173,7 @@ export class FlexHarnessCore<TScope> {
|
|
|
172
173
|
public readonly trustedInternalErrors = new WeakSet<object>();
|
|
173
174
|
public sequence = 0;
|
|
174
175
|
public closed = false;
|
|
176
|
+
private codeSandbox?: Promise<FlexCodeSandbox>;
|
|
175
177
|
public readonly results = new FlexHarnessResults<TScope>(this);
|
|
176
178
|
public readonly toolDispatch = new FlexHarnessToolDispatch<TScope>(this);
|
|
177
179
|
public readonly providers = new FlexHarnessToolProviders<TScope>(this);
|
|
@@ -1716,6 +1718,19 @@ export class FlexHarnessCore<TScope> {
|
|
|
1716
1718
|
});
|
|
1717
1719
|
}
|
|
1718
1720
|
|
|
1721
|
+
/** Announces that `part` leaves the run's parts; call it while the part is still among them. */
|
|
1722
|
+
public emitPartRemovedEvent(run: IActiveRun, part: TFlexMessagePart): void {
|
|
1723
|
+
const { messageIndex, partIndex } = this.partEventCoordinates(run, part.partId);
|
|
1724
|
+
this.emitEvent(run.scopeId, run.stored.session, {
|
|
1725
|
+
type: 'part.removed',
|
|
1726
|
+
runId: run.runId,
|
|
1727
|
+
messageId: run.assistantMessageId,
|
|
1728
|
+
messageIndex,
|
|
1729
|
+
partId: part.partId,
|
|
1730
|
+
partIndex,
|
|
1731
|
+
});
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1719
1734
|
private partEventCoordinates(
|
|
1720
1735
|
run: IActiveRun,
|
|
1721
1736
|
partId: string,
|
|
@@ -1833,4 +1848,20 @@ export class FlexHarnessCore<TScope> {
|
|
|
1833
1848
|
public assertOpen(): void {
|
|
1834
1849
|
if (this.closed) throw new FlexHarnessClosedError();
|
|
1835
1850
|
}
|
|
1851
|
+
|
|
1852
|
+
/** The harness's code mode sandbox, loaded with its first code mode run; its workers are the harness's. */
|
|
1853
|
+
public loadCodeSandbox(): Promise<FlexCodeSandbox> {
|
|
1854
|
+
this.assertOpen();
|
|
1855
|
+
this.codeSandbox ??= FlexCodeSandbox.load().catch((error: unknown) => {
|
|
1856
|
+
this.codeSandbox = undefined;
|
|
1857
|
+
throw error;
|
|
1858
|
+
});
|
|
1859
|
+
return this.codeSandbox;
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
/** Stops the code mode programs still running and ends every worker of the sandbox. */
|
|
1863
|
+
public async disposeCodeSandbox(): Promise<void> {
|
|
1864
|
+
const sandbox = await this.codeSandbox?.catch(() => undefined);
|
|
1865
|
+
await sandbox?.dispose();
|
|
1866
|
+
}
|
|
1836
1867
|
}
|
|
@@ -771,6 +771,11 @@ export class FlexHarnessLifecycle<TScope> {
|
|
|
771
771
|
errors.push(...await this.closeOrphanedResources(storageKey));
|
|
772
772
|
}
|
|
773
773
|
}
|
|
774
|
+
try {
|
|
775
|
+
await this.core.disposeCodeSandbox();
|
|
776
|
+
} catch (error) {
|
|
777
|
+
this.core.appendUnexpectedErrors(errors, error);
|
|
778
|
+
}
|
|
774
779
|
this.core.listeners.clear();
|
|
775
780
|
if (errors.length > 0) throw combineErrors(errors);
|
|
776
781
|
this.core.compaction.compactorInvocationContext.disable();
|
|
@@ -26,7 +26,6 @@ import {
|
|
|
26
26
|
type TFlexMessagePart,
|
|
27
27
|
} from './interfaces.js';
|
|
28
28
|
import { FlexToolCatalog, assertNoCatalogCollisions } from './classes.toolcatalog.js';
|
|
29
|
-
import { FlexCodeSandbox } from './classes.codesandbox.js';
|
|
30
29
|
import { cloneSerializable } from './utils.json.js';
|
|
31
30
|
import type { INormalizedFlexPrompt } from './utils.prompt.js';
|
|
32
31
|
import {
|
|
@@ -119,7 +118,7 @@ export class FlexHarnessRuns<TScope> {
|
|
|
119
118
|
cacheWriteTokens: 0,
|
|
120
119
|
},
|
|
121
120
|
reasoningPartIds: new Map(),
|
|
122
|
-
|
|
121
|
+
stepPartIds: new Set(),
|
|
123
122
|
toolPartIds: new Map(),
|
|
124
123
|
trustedToolExecutionErrors: new Map(),
|
|
125
124
|
unconfirmedToolCalls: new Map(),
|
|
@@ -495,7 +494,11 @@ export class FlexHarnessRuns<TScope> {
|
|
|
495
494
|
catalog = await FlexToolCatalog.create(toolHandle.catalog, this.core.toolCatalogOptions, {
|
|
496
495
|
results: this.core.stores.results !== undefined,
|
|
497
496
|
});
|
|
498
|
-
if (catalog.codeMode)
|
|
497
|
+
if (catalog.codeMode) {
|
|
498
|
+
run.codeSandbox = await this.core.loadCodeSandbox();
|
|
499
|
+
// The worker loads QuickJS while the model writes the program.
|
|
500
|
+
run.codeSandbox.prewarm(run.scopeId);
|
|
501
|
+
}
|
|
499
502
|
} catch (error) {
|
|
500
503
|
throw this.core.projectExternalError(run, error, 'toolProvider');
|
|
501
504
|
}
|
|
@@ -811,7 +814,7 @@ export class FlexHarnessRuns<TScope> {
|
|
|
811
814
|
run.reservedAssistantMessage = publicSnapshot(nextAssistant);
|
|
812
815
|
run.callbackParts = [];
|
|
813
816
|
run.reasoningPartIds.clear();
|
|
814
|
-
run.
|
|
817
|
+
run.stepPartIds.clear();
|
|
815
818
|
this.core.emitEvent(run.scopeId, run.stored.session, {
|
|
816
819
|
type: 'message.updated',
|
|
817
820
|
runId: run.runId,
|
|
@@ -1030,7 +1033,7 @@ export class FlexHarnessRuns<TScope> {
|
|
|
1030
1033
|
for (const part of terminal.assistantMessage.parts) {
|
|
1031
1034
|
const callbackPart = run.callbackParts.find((entry) => entry.partId === part.partId);
|
|
1032
1035
|
const shouldComplete = part.type === 'text'
|
|
1033
|
-
? callbackPart?.type === 'text' &&
|
|
1036
|
+
? callbackPart?.type === 'text' && run.stepPartIds.has(part.partId)
|
|
1034
1037
|
: part.type === 'reasoning'
|
|
1035
1038
|
? callbackPart?.type === 'reasoning' && callbackPart.status === 'running'
|
|
1036
1039
|
: part.type === 'tool'
|
|
@@ -475,7 +475,7 @@ export class FlexHarnessToolDispatch<TScope> {
|
|
|
475
475
|
}
|
|
476
476
|
},
|
|
477
477
|
};
|
|
478
|
-
const result = await run.codeSandbox!.run(code, host, catalog.codeMode!, abortSignal);
|
|
478
|
+
const result = await run.codeSandbox!.run(code, host, catalog.codeMode!, abortSignal, run.scopeId);
|
|
479
479
|
if (!result.ok) {
|
|
480
480
|
throw this.catalogCallError(run, toolCallId, normalizeJsonValue(result.failure, this.core.toolOutputLimits));
|
|
481
481
|
}
|
|
@@ -223,12 +223,14 @@ export interface IActiveRun {
|
|
|
223
223
|
/** The usage the provider reported for the run's model calls so far. */
|
|
224
224
|
usage: IFlexUsage;
|
|
225
225
|
metricsOperation?: ISessionMetricsOperation;
|
|
226
|
+
/** The part of each reasoning summary in progress, by the model's reasoning ID. */
|
|
226
227
|
reasoningPartIds: Map<string, string>;
|
|
227
228
|
/**
|
|
228
|
-
* The text parts
|
|
229
|
-
* follows starts a new part
|
|
229
|
+
* The text and reasoning parts the model step in progress streamed. When the step commits, its
|
|
230
|
+
* text parts complete and text that follows starts a new part; when its model call is retried,
|
|
231
|
+
* they are removed.
|
|
230
232
|
*/
|
|
231
|
-
|
|
233
|
+
stepPartIds: Set<string>;
|
|
232
234
|
toolPartIds: Map<string, string>;
|
|
233
235
|
trustedToolExecutionErrors: Map<string, Error>;
|
|
234
236
|
/**
|
package/ts/interfaces.ts
CHANGED
|
@@ -1962,7 +1962,16 @@ export interface IFlexPartDeltaEvent extends IFlexPartEventBase {
|
|
|
1962
1962
|
readonly textUtf8Bytes: number;
|
|
1963
1963
|
}
|
|
1964
1964
|
|
|
1965
|
-
|
|
1965
|
+
/**
|
|
1966
|
+
* A streamed part left its message: the model call that streamed it failed and is retried, and
|
|
1967
|
+
* the retry streams its own parts. `partIndex` is the part's position before it left; the parts
|
|
1968
|
+
* after it move up by one.
|
|
1969
|
+
*/
|
|
1970
|
+
export interface IFlexPartRemovedEvent extends IFlexPartEventBase {
|
|
1971
|
+
readonly type: 'part.removed';
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1974
|
+
export type TFlexPartEvent = IFlexPartSnapshotEvent | IFlexPartDeltaEvent | IFlexPartRemovedEvent;
|
|
1966
1975
|
|
|
1967
1976
|
export interface IFlexPermissionRequestedEvent extends IFlexEventBase {
|
|
1968
1977
|
readonly type: 'permission.requested';
|
package/ts/plugins.ts
CHANGED
|
@@ -61,6 +61,8 @@ export interface IQuickJsRuntime {
|
|
|
61
61
|
|
|
62
62
|
export interface IQuickJsModule {
|
|
63
63
|
newRuntime(): IQuickJsRuntime;
|
|
64
|
+
/** The module's WebAssembly memory, shared by all its runtimes; it grows and never shrinks. */
|
|
65
|
+
getWasmMemory(): WebAssembly.Memory;
|
|
64
66
|
}
|
|
65
67
|
|
|
66
68
|
/** A failed strip, as amaro reports it. */
|
|
@@ -76,7 +78,7 @@ export interface ICodeSandboxEngine {
|
|
|
76
78
|
}
|
|
77
79
|
|
|
78
80
|
/**
|
|
79
|
-
* Loads amaro, and finds QuickJS: the optional peers code mode needs. Every
|
|
81
|
+
* Loads amaro, and finds QuickJS: the optional peers code mode needs. Every code mode worker loads
|
|
80
82
|
* QuickJS itself.
|
|
81
83
|
*/
|
|
82
84
|
export const importCodeSandboxEngine = async (): Promise<ICodeSandboxEngine> => {
|
package/ts/readme.md
CHANGED
|
@@ -1065,6 +1065,9 @@ const unsubscribe = harness.subscribe((event) => {
|
|
|
1065
1065
|
event.textUtf8Bytes,
|
|
1066
1066
|
);
|
|
1067
1067
|
break;
|
|
1068
|
+
case 'part.removed':
|
|
1069
|
+
removePart(event.sessionId, event.messageIndex, event.partIndex);
|
|
1070
|
+
break;
|
|
1068
1071
|
case 'permission.requested':
|
|
1069
1072
|
showPermission(event.request);
|
|
1070
1073
|
break;
|
|
@@ -1100,13 +1103,13 @@ const unsubscribe = harness.subscribe((event) => {
|
|
|
1100
1103
|
unsubscribe();
|
|
1101
1104
|
```
|
|
1102
1105
|
|
|
1103
|
-
Events are discriminated, deeply immutable values with one global sequence within each `FlexHarness` instance. Every `IFlexEventBase` carries the exact source `sessionGenerationId` and `sessionGenerationSequence`; `session.deleted` retains the deleted generation after live metadata is removed. Listener exceptions are isolated from runs and other listeners. Every accepted queue entry emits `prompt.queued` and exactly one `prompt.finished`. Every accepted steer emits `prompt.steer.accepted`, and `prompt.steer.applied`, after the `message.*` events of its user message, once the run took it in; `run.finished` lists it in `unappliedSteerIds` unless it is in the model context of later turns. Durable promotion additionally emits `prompt.started`, and actual model preparation emits `prompt.running`; cancellation or failure can omit either intermediate event. Existing durable run/message terminal events precede `prompt.finished`. Every callback-backed streamed text part emits exactly one `part.completed` event before the corresponding `run.finished` event: once the model step that streamed it is committed, so its text is final and saved, or with the run's terminal projection when that step did not commit. Text a later model step streams starts a new part. Every `part.started`, `part.delta`, `part.updated`, and `part.
|
|
1106
|
+
Events are discriminated, deeply immutable values with one global sequence within each `FlexHarness` instance. Every `IFlexEventBase` carries the exact source `sessionGenerationId` and `sessionGenerationSequence`; `session.deleted` retains the deleted generation after live metadata is removed. Listener exceptions are isolated from runs and other listeners. Every accepted queue entry emits `prompt.queued` and exactly one `prompt.finished`. Every accepted steer emits `prompt.steer.accepted`, and `prompt.steer.applied`, after the `message.*` events of its user message, once the run took it in; `run.finished` lists it in `unappliedSteerIds` unless it is in the model context of later turns. Durable promotion additionally emits `prompt.started`, and actual model preparation emits `prompt.running`; cancellation or failure can omit either intermediate event. Existing durable run/message terminal events precede `prompt.finished`. Every callback-backed streamed text part that stays in its message emits exactly one `part.completed` event before the corresponding `run.finished` event: once the model step that streamed it is committed, so its text is final and saved, or with the run's terminal projection when that step did not commit. Text a later model step streams starts a new part. When a model call fails and is retried, the text and reasoning parts it streamed are not in the session: `part.removed` takes each of them out of its message, last part first, and the retry streams its own parts. Its `partIndex` is the part's position before it left; the parts after it move up by one. A removed part has no further events, and `part.removed` does not count against `callbackLimits`. The tool parts of calls the failed model call executed stay. Every `part.started`, `part.delta`, `part.updated`, `part.completed`, and `part.removed` event carries zero-based `messageIndex` and `partIndex` coordinates from the session's authoritative message and part sequences.
|
|
1104
1107
|
|
|
1105
1108
|
`getSessionView(scopeId, sessionId)` reads a session's metadata, visible messages and pending permission requests in one step, together with the instance's event `sequence` they include. The messages include what a running turn has streamed so far, so a consumer that applies every event of the instance with a greater `sequence` follows the session exactly from the view on; events up to `sequence` are already in it. The read waits while a queued write or a change that emits events for the session is in progress; a session that keeps changing through 64 consecutive waits is refused with `FlexHarnessSessionBusyError`, and the read can be repeated. Session metadata is current: status changes such as `waiting_permission` emit no event. A permission request its run leaves unanswered leaves `pendingPermissions` without an event of its own; the run's `run.finished` follows.
|
|
1106
1109
|
|
|
1107
1110
|
`part.started`, `part.updated`, and `part.completed` are snapshot events with a complete immutable `part`. Tool and reasoning parts carry `startedAt` (ISO time the call or reasoning began) and, once they reach a terminal status, `completedAt`; parts recorded by earlier versions have neither, so a consumer falls back to the message times for them. A newly streamed text part emits `part.started` with empty text before its first delta; reasoning parts also start empty. `part.delta` is a separate delta-only event: it has no cumulative `part`, and carries `partType`, the required exact source `delta`, and `baseTextUtf8Bytes`/`textUtf8Bytes` for the cumulative text before and after that delta. The counters remain correct when a UTF-16 surrogate pair is split across callbacks. Exact deltas are never truncated, including a single delta above the 96 KiB message-transfer text limit. `part.updated` remains a cumulative replacement snapshot for running tool output or metadata, not a text delta. `session.history.changed` carries `direction: 'undo' | 'redo' | 'branch'`; `runId` is present for undo and redo. Events contain public IDs, exact delta payloads, and public snapshots only; they do not expose prompt payloads, the resolved scope object, storage key, model object, provider options, or raw storage key.
|
|
1108
1111
|
|
|
1109
|
-
Part events narrow through the exported `TFlexPartEvent` union. `IFlexPartEventBase` contains their shared coordinates, `IFlexPartSnapshotEvent` owns snapshot events and their complete `part`,
|
|
1112
|
+
Part events narrow through the exported `TFlexPartEvent` union. `IFlexPartEventBase` contains their shared coordinates, `IFlexPartSnapshotEvent` owns snapshot events and their complete `part`, `IFlexPartDeltaEvent` owns exact delta-only events and their UTF-8 counters, and `IFlexPartRemovedEvent` is `part.removed`, which carries only the coordinates.
|
|
1110
1113
|
|
|
1111
1114
|
### Migrating Part Events to 5.x
|
|
1112
1115
|
|
|
@@ -44,10 +44,13 @@ export interface IFlexHarnessChatClientListener {
|
|
|
44
44
|
permissions?(permissions: plugins.IHarnessPermissionRequest[]): void;
|
|
45
45
|
history?(history: IFlexHarnessChatHistoryState): void;
|
|
46
46
|
steer?(outcome: IFlexHarnessChatSteerOutcome): void;
|
|
47
|
-
/**
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
47
|
+
/**
|
|
48
|
+
* the open conversation could not be followed again and was closed; the message says why, and
|
|
49
|
+
* the error is what the transport failed with, for a view that words its own refusals
|
|
50
|
+
*/
|
|
51
|
+
closed?(message: string, error: unknown): void;
|
|
52
|
+
/** attaching again after a reconnect failed; the message says why, and the error is the transport's */
|
|
53
|
+
error?(message: string, error: unknown): void;
|
|
51
54
|
}
|
|
52
55
|
|
|
53
56
|
/** Runs one tool of a turn in this tab and answers with what the model reads. */
|
|
@@ -104,7 +107,7 @@ export class FlexHarnessChatClient {
|
|
|
104
107
|
});
|
|
105
108
|
this.stopWatchingReconnects = this.transport.onReconnect(() => {
|
|
106
109
|
void this.attach().catch((error) =>
|
|
107
|
-
this.listener?.error?.(errorText(error, 'The conversation could not be reattached after a reconnect.')),
|
|
110
|
+
this.listener?.error?.(errorText(error, 'The conversation could not be reattached after a reconnect.'), error),
|
|
108
111
|
);
|
|
109
112
|
});
|
|
110
113
|
}
|
|
@@ -267,7 +270,7 @@ export class FlexHarnessChatClient {
|
|
|
267
270
|
// another conversation was opened or a new one started meanwhile: that one stays
|
|
268
271
|
if (this.openEpoch !== epoch) return;
|
|
269
272
|
this.clear();
|
|
270
|
-
this.listener?.closed?.(errorText(error, 'The conversation could not be opened again.'));
|
|
273
|
+
this.listener?.closed?.(errorText(error, 'The conversation could not be opened again.'), error);
|
|
271
274
|
}
|
|
272
275
|
}
|
|
273
276
|
|
|
@@ -250,6 +250,9 @@ export class FlexHarnessTranscript {
|
|
|
250
250
|
case 'part.delta':
|
|
251
251
|
this.applyPartDelta(change, event);
|
|
252
252
|
break;
|
|
253
|
+
case 'part.removed':
|
|
254
|
+
this.removePart(change, event);
|
|
255
|
+
break;
|
|
253
256
|
case 'permission.requested':
|
|
254
257
|
this.addPermission(event.request);
|
|
255
258
|
change.permissions = true;
|
|
@@ -396,4 +399,20 @@ export class FlexHarnessTranscript {
|
|
|
396
399
|
? { type: 'text', messageId: rowId, delta: event.delta }
|
|
397
400
|
: { type: 'reasoning', messageId: rowId, partId: event.partId, delta: event.delta });
|
|
398
401
|
}
|
|
402
|
+
|
|
403
|
+
private removePart(
|
|
404
|
+
change: IFlexHarnessTranscriptChange,
|
|
405
|
+
event: Extract<TFlexHarnessEvent, { type: 'part.removed' }>,
|
|
406
|
+
): void {
|
|
407
|
+
const message = this.flexMessages.find((entry) => entry.messageId === event.messageId);
|
|
408
|
+
if (!message || message.parts[event.partIndex]?.partId !== event.partId) {
|
|
409
|
+
change.reload = true;
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
message.parts.splice(event.partIndex, 1);
|
|
413
|
+
const key = flexPartTimingKey(event.messageId, event.partId);
|
|
414
|
+
this.timings.delete(key);
|
|
415
|
+
this.partBytes.delete(key);
|
|
416
|
+
change.messages = true;
|
|
417
|
+
}
|
|
399
418
|
}
|
|
@@ -38,6 +38,7 @@ export const defaultFlexHarnessChatLimits: Readonly<IFlexHarnessChatLimits> = Ob
|
|
|
38
38
|
admissionWaitMs: 30 * 1000,
|
|
39
39
|
toolTimeoutMs: 2 * 60 * 1000,
|
|
40
40
|
maxPendingDeltas: 2_000,
|
|
41
|
+
minUpdateIntervalMs: 100,
|
|
41
42
|
maxMessageChars: 100_000,
|
|
42
43
|
maxAttachments: 16,
|
|
43
44
|
maxAttachmentChars: 8 * 1024 * 1024,
|
|
@@ -26,6 +26,12 @@ const emptyPending = (): IPendingUpdate => ({
|
|
|
26
26
|
steers: [],
|
|
27
27
|
});
|
|
28
28
|
|
|
29
|
+
/** Resolves after the time; its timer keeps no process alive. */
|
|
30
|
+
const delay = (msArg: number): Promise<void> =>
|
|
31
|
+
new Promise<void>((resolve) => {
|
|
32
|
+
setTimeout(resolve, msArg).unref?.();
|
|
33
|
+
});
|
|
34
|
+
|
|
29
35
|
const hasPending = (pendingArg: IPendingUpdate): boolean =>
|
|
30
36
|
pendingArg.reset
|
|
31
37
|
|| pendingArg.rows
|
|
@@ -58,8 +64,9 @@ export interface IFlexHarnessChatSubscriptionOptions {
|
|
|
58
64
|
|
|
59
65
|
/**
|
|
60
66
|
* One tab following one conversation. A `FlexHarnessTranscript` takes every event of the harness
|
|
61
|
-
* instance; what changed reaches the tab as numbered updates, one at a time
|
|
62
|
-
* loses track, or an update the tab does not acknowledge,
|
|
67
|
+
* instance; what changed reaches the tab as numbered updates, one at a time and at most one per
|
|
68
|
+
* `minUpdateIntervalMs`. A transcript that loses track, or an update the tab does not acknowledge,
|
|
69
|
+
* makes the next update a `reset`.
|
|
63
70
|
*/
|
|
64
71
|
export class FlexHarnessChatSubscription {
|
|
65
72
|
public readonly id: string;
|
|
@@ -80,6 +87,8 @@ export class FlexHarnessChatSubscription {
|
|
|
80
87
|
private sentRows = new Map<string, string>();
|
|
81
88
|
private pending: IPendingUpdate = emptyPending();
|
|
82
89
|
private sending = false;
|
|
90
|
+
/** when the last update was taken to be sent, by `performance.now()` */
|
|
91
|
+
private lastUpdateAt = Number.NEGATIVE_INFINITY;
|
|
83
92
|
private closed = false;
|
|
84
93
|
|
|
85
94
|
constructor(optionsArg: IFlexHarnessChatSubscriptionOptions) {
|
|
@@ -176,13 +185,19 @@ export class FlexHarnessChatSubscription {
|
|
|
176
185
|
}
|
|
177
186
|
}
|
|
178
187
|
|
|
179
|
-
/** Sends what is pending, one update at a time, in seq order. */
|
|
188
|
+
/** Sends what is pending, one update at a time, in seq order, `minUpdateIntervalMs` apart. */
|
|
180
189
|
private async pump(): Promise<void> {
|
|
181
190
|
if (this.sending) return;
|
|
182
191
|
this.sending = true;
|
|
183
192
|
let inFlight: TUpdate | null = null;
|
|
184
193
|
try {
|
|
185
194
|
while (!this.closed && this.state === 'live' && hasPending(this.pending)) {
|
|
195
|
+
const wait = this.lastUpdateAt + this.limits.minUpdateIntervalMs - performance.now();
|
|
196
|
+
if (wait > 0) {
|
|
197
|
+
// what changes meanwhile goes into this update
|
|
198
|
+
await delay(wait);
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
186
201
|
const peer = await this.peers.find(this.tab);
|
|
187
202
|
if (!peer) {
|
|
188
203
|
this.closeSelf();
|
|
@@ -190,6 +205,7 @@ export class FlexHarnessChatSubscription {
|
|
|
190
205
|
}
|
|
191
206
|
if (this.closed || this.state !== 'live') return;
|
|
192
207
|
inFlight = await this.takeUpdate();
|
|
208
|
+
this.lastUpdateAt = performance.now();
|
|
193
209
|
await peer.update(inFlight);
|
|
194
210
|
inFlight = null;
|
|
195
211
|
}
|
|
@@ -75,6 +75,12 @@ export interface IFlexHarnessChatLimits {
|
|
|
75
75
|
toolTimeoutMs: number;
|
|
76
76
|
/** Deltas one update may carry; a tab further behind gets the changed rows. Default: 2000. */
|
|
77
77
|
maxPendingDeltas: number;
|
|
78
|
+
/**
|
|
79
|
+
* The least time between two updates of one subscription; what changes meanwhile goes into the
|
|
80
|
+
* next. It bounds the requests a streaming turn costs a transport that answers every update with
|
|
81
|
+
* one, as TypedSocket pulls do. Default: 100 ms.
|
|
82
|
+
*/
|
|
83
|
+
minUpdateIntervalMs: number;
|
|
78
84
|
/** Characters of a message's text. Default: 100 000. */
|
|
79
85
|
maxMessageChars: number;
|
|
80
86
|
/** Attachments of one message. Default: 16. */
|
|
@@ -47,7 +47,7 @@ The host keeps its harness, models, system prompt, tools and scopes. It authenti
|
|
|
47
47
|
## Conversations
|
|
48
48
|
|
|
49
49
|
- `attach(caller)` lists the tab's conversations, newest first, and forgets what the tab followed. A tab attaches after loading and after every reconnect; conversations created, renamed or deleted afterwards reach it as list pushes (`IFlexHarnessChatPeer.sessions`, an `IFlexHarnessChatSessionsPush`).
|
|
50
|
-
- `open(caller, { sessionId, subscriptionId })` follows a conversation. Its state reaches the tab as numbered `IFlexHarnessChatUpdate` pushes, starting with a `reset`: changed rows, streamed deltas, status, usage, pending permissions, undo and redo availability, and what became of steers. A tab follows one conversation at a time. An update the tab does not acknowledge makes the next change a `reset`; a tab that sees a gap in `seq` opens the conversation again.
|
|
50
|
+
- `open(caller, { sessionId, subscriptionId })` follows a conversation. Its state reaches the tab as numbered `IFlexHarnessChatUpdate` pushes, starting with a `reset`: changed rows, streamed deltas, status, usage, pending permissions, undo and redo availability, and what became of steers. Updates go one at a time and at most one per `limits.minUpdateIntervalMs` (100 ms by default); what changes meanwhile goes into the next. A transport that answers every update with a request, as TypedSocket pulls do, so makes at most one request per interval for a streaming turn. A tab follows one conversation at a time. An update the tab does not acknowledge makes the next change a `reset`; a tab that sees a gap in `seq` opens the conversation again.
|
|
51
51
|
- `send(caller, { sessionId?, send, context })` starts a conversation without `sessionId`. While a turn runs, the message steers it; one the turn no longer takes waits up to `admissionWaitMs` for the ending turn and starts the next one. `readContext` checks the context the message is sent with; the turn carries it. The composer's model, account and effort choices are not used: the host's model resolver decides from the context.
|
|
52
52
|
- `abort`, `respondToPermission`, `moveHistory` (FlexHarness undo and redo), `renameSession` and `deleteSession` act on a conversation the caller reaches. An imported conversation can be read, not continued.
|
|
53
53
|
|