@modelprofile.com/flexharness 11.1.0 → 11.2.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.transcript.d.ts +1 -0
- package/dist_ts_chat_harness/classes.transcript.js +16 -1
- 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.transcript.ts +19 -0
|
@@ -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
|
|
|
@@ -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
|
}
|