@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.
Files changed (33) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/classes.codesandbox.d.ts +58 -7
  3. package/dist_ts/classes.codesandbox.js +267 -34
  4. package/dist_ts/classes.codesandbox.worker.d.ts +6 -1
  5. package/dist_ts/classes.codesandbox.worker.js +34 -8
  6. package/dist_ts/classes.flexharness.callbacks.d.ts +6 -0
  7. package/dist_ts/classes.flexharness.callbacks.js +24 -4
  8. package/dist_ts/classes.flexharness.core.d.ts +8 -0
  9. package/dist_ts/classes.flexharness.core.js +29 -1
  10. package/dist_ts/classes.flexharness.lifecycle.js +7 -1
  11. package/dist_ts/classes.flexharness.runs.js +9 -7
  12. package/dist_ts/classes.flexharness.tooldispatch.js +2 -2
  13. package/dist_ts/interfaces.d.ts +9 -1
  14. package/dist_ts/interfaces.flexharness.d.ts +5 -3
  15. package/dist_ts/plugins.d.ts +3 -1
  16. package/dist_ts/plugins.js +2 -2
  17. package/dist_ts_chat_harness/classes.transcript.d.ts +1 -0
  18. package/dist_ts_chat_harness/classes.transcript.js +16 -1
  19. package/package.json +3 -3
  20. package/readme.md +5 -2
  21. package/ts/00_commitinfo_data.ts +1 -1
  22. package/ts/classes.codesandbox.ts +303 -34
  23. package/ts/classes.codesandbox.worker.ts +38 -8
  24. package/ts/classes.flexharness.callbacks.ts +22 -3
  25. package/ts/classes.flexharness.core.ts +31 -0
  26. package/ts/classes.flexharness.lifecycle.ts +5 -0
  27. package/ts/classes.flexharness.runs.ts +8 -5
  28. package/ts/classes.flexharness.tooldispatch.ts +1 -1
  29. package/ts/interfaces.flexharness.ts +5 -3
  30. package/ts/interfaces.ts +10 -1
  31. package/ts/plugins.ts +3 -1
  32. package/ts/readme.md +5 -2
  33. 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
- completedTextPartIds: new Set(),
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) run.codeSandbox = await FlexCodeSandbox.load();
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.completedTextPartIds.clear();
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' && !run.completedTextPartIds.has(part.partId)
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 whose model step is committed. Each emitted its `part.completed`; text that
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
- completedTextPartIds: Set<string>;
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
- export type TFlexPartEvent = IFlexPartSnapshotEvent | IFlexPartDeltaEvent;
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 program's worker loads
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.completed` event carries zero-based `messageIndex` and `partIndex` coordinates from the session's authoritative message and part sequences.
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`, and `IFlexPartDeltaEvent` owns exact delta-only events and their UTF-8 counters.
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
  }