@modelprofile.com/flexharness 11.0.1 → 11.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/classes.codesandbox.d.ts +12 -4
  3. package/dist_ts/classes.codesandbox.js +119 -287
  4. package/dist_ts/classes.codesandbox.worker.d.ts +52 -0
  5. package/dist_ts/classes.codesandbox.worker.js +310 -0
  6. package/dist_ts/classes.flexharness.callbacks.d.ts +37 -1
  7. package/dist_ts/classes.flexharness.callbacks.js +162 -53
  8. package/dist_ts/classes.flexharness.core.d.ts +2 -1
  9. package/dist_ts/classes.flexharness.core.js +22 -18
  10. package/dist_ts/classes.flexharness.results.js +4 -4
  11. package/dist_ts/classes.flexharness.reversions.d.ts +7 -0
  12. package/dist_ts/classes.flexharness.reversions.js +40 -39
  13. package/dist_ts/classes.flexharness.runs.js +7 -3
  14. package/dist_ts/classes.flexharness.tooldispatch.d.ts +2 -1
  15. package/dist_ts/classes.flexharness.tooldispatch.js +27 -9
  16. package/dist_ts/classes.stores.js +15 -13
  17. package/dist_ts/errors.d.ts +5 -0
  18. package/dist_ts/errors.js +10 -1
  19. package/dist_ts/index.d.ts +2 -1
  20. package/dist_ts/index.js +3 -2
  21. package/dist_ts/interfaces.d.ts +31 -9
  22. package/dist_ts/interfaces.flexharness.d.ts +14 -0
  23. package/dist_ts/interfaces.js +3 -3
  24. package/dist_ts/plugins.d.ts +6 -4
  25. package/dist_ts/plugins.js +10 -11
  26. package/dist_ts/utils.harnessoptions.d.ts +3 -1
  27. package/dist_ts/utils.harnessoptions.js +15 -2
  28. package/dist_ts/utils.json.d.ts +9 -1
  29. package/dist_ts/utils.json.js +92 -33
  30. package/dist_ts/utils.storecodecs.d.ts +1 -1
  31. package/dist_ts/utils.storecodecs.js +4 -4
  32. package/dist_ts/utils.storerules.d.ts +3 -3
  33. package/dist_ts/utils.storerules.js +1 -14
  34. package/dist_ts_mcp/index.d.ts +3 -1
  35. package/dist_ts_mcp/index.js +17 -2
  36. package/dist_ts_stores_nosqldb/classes.nosqldbstores.d.ts +18 -6
  37. package/dist_ts_stores_nosqldb/classes.nosqldbstores.js +27 -16
  38. package/dist_ts_stores_nosqldb/classes.recordstore.d.ts +33 -3
  39. package/dist_ts_stores_nosqldb/classes.recordstore.js +78 -18
  40. package/dist_ts_stores_nosqldb/index.d.ts +2 -2
  41. package/dist_ts_stores_nosqldb/index.js +2 -2
  42. package/package.json +3 -3
  43. package/readme.md +8 -2
  44. package/ts/00_commitinfo_data.ts +1 -1
  45. package/ts/classes.codesandbox.ts +132 -276
  46. package/ts/classes.codesandbox.worker.ts +345 -0
  47. package/ts/classes.flexharness.callbacks.ts +207 -55
  48. package/ts/classes.flexharness.core.ts +22 -16
  49. package/ts/classes.flexharness.results.ts +3 -3
  50. package/ts/classes.flexharness.reversions.ts +43 -32
  51. package/ts/classes.flexharness.runs.ts +6 -2
  52. package/ts/classes.flexharness.tooldispatch.ts +23 -7
  53. package/ts/classes.stores.ts +21 -12
  54. package/ts/errors.ts +14 -0
  55. package/ts/index.ts +7 -1
  56. package/ts/interfaces.flexharness.ts +15 -0
  57. package/ts/interfaces.ts +32 -9
  58. package/ts/plugins.ts +9 -12
  59. package/ts/readme.md +8 -2
  60. package/ts/utils.harnessoptions.ts +22 -0
  61. package/ts/utils.json.ts +112 -35
  62. package/ts/utils.storecodecs.ts +3 -3
  63. package/ts/utils.storerules.ts +3 -24
  64. package/ts_mcp/index.ts +17 -1
  65. package/ts_mcp/readme.md +3 -1
  66. package/ts_stores_nosqldb/classes.nosqldbstores.ts +39 -15
  67. package/ts_stores_nosqldb/classes.recordstore.ts +105 -13
  68. package/ts_stores_nosqldb/index.ts +2 -0
  69. package/ts_stores_nosqldb/readme.md +10 -2
@@ -26,6 +26,7 @@ import type {
26
26
  IFlexReversionSegment,
27
27
  IFlexPendingReversionRelease,
28
28
  IFlexResolvedScope,
29
+ IFlexResultLimits,
29
30
  IFlexScopeSnapshot,
30
31
  IFlexSession,
31
32
  IFlexSessionImportLimits,
@@ -59,6 +60,7 @@ import {
59
60
  resolveBoundedPositiveInteger,
60
61
  resolveCallbackLimits,
61
62
  resolvePromptQueueLimits,
63
+ resolveResultLimits,
62
64
  resolveReversionMaintenanceTimeoutMs,
63
65
  resolveReversionLimits,
64
66
  type INormalizedBuiltInTools,
@@ -153,6 +155,7 @@ export class FlexHarnessCore<TScope> {
153
155
  public readonly promptQueueLimits: Required<IFlexPromptQueueLimits>;
154
156
  public readonly reversionLimits: Required<IFlexReversionLimits>;
155
157
  public readonly sessionImportLimits: Required<IFlexSessionImportLimits>;
158
+ public readonly resultLimits: Required<IFlexResultLimits>;
156
159
  public readonly reversionMaintenanceTimeoutMs: number;
157
160
  public readonly turnReversionProvider: IFlexHarnessOptions<TScope>['turnReversionProvider'];
158
161
  public readonly reversionPolicy: TFlexReversionPolicy;
@@ -206,6 +209,7 @@ export class FlexHarnessCore<TScope> {
206
209
  this.promptQueueLimits = resolvePromptQueueLimits(options.promptQueueLimits);
207
210
  this.reversionLimits = resolveReversionLimits(options.reversionLimits);
208
211
  this.sessionImportLimits = resolveSessionImportLimits(options.sessionImportLimits);
212
+ this.resultLimits = resolveResultLimits(options.resultLimits, this.toolOutputLimits);
209
213
  this.reversionMaintenanceTimeoutMs = resolveReversionMaintenanceTimeoutMs(options);
210
214
  this.turnReversionProvider = options.turnReversionProvider;
211
215
  this.reversionPolicy = options.reversionPolicy ?? 'transcript-optional';
@@ -819,6 +823,7 @@ export class FlexHarnessCore<TScope> {
819
823
  onReasoningStart: (id) => this.callbacks.onReasoningStart(state, sessionId, id),
820
824
  onReasoningDelta: (id, delta) => this.callbacks.onReasoningDelta(state, sessionId, id, delta),
821
825
  onReasoningEnd: (id, text) => this.callbacks.onReasoningEnd(state, sessionId, id, text),
826
+ onStepCommitted: () => this.callbacks.onStepCommitted(state, sessionId),
822
827
  onToolCallStart: (event) => this.callbacks.onToolStart(state, sessionId, event),
823
828
  onToolCallUpdate: (event) => this.callbacks.onToolUpdate(state, sessionId, event),
824
829
  onToolCallFinish: (event) => this.callbacks.onToolFinish(state, sessionId, event),
@@ -1145,16 +1150,17 @@ export class FlexHarnessCore<TScope> {
1145
1150
  }]));
1146
1151
  const beforeTombstones = new Map([...state.tombstones].map(([id, value]) => [id, cloneSerializable(value)]));
1147
1152
  const beforeRetainedCleanups = new Map(state.retainedSessionCleanups);
1148
- const beforeSnapshot: IFlexScopeSnapshot = {
1153
+ // The copies above stay untouched until a restore, so the scope as it was before the mutation
1154
+ // is built from them only when a failed save has to be reconciled.
1155
+ const beforeSnapshot = (): IFlexScopeSnapshot => ({
1149
1156
  schemaVersion: 1,
1150
1157
  revision: beforeRevision,
1151
1158
  sessions: [...beforeSessions.values()]
1152
- .map((entry) => cloneSerializable(entry.metadata))
1159
+ .map((entry) => entry.metadata)
1153
1160
  .sort((left, right) => left.sessionId.localeCompare(right.sessionId)),
1154
1161
  tombstones: [...beforeTombstones.values()]
1155
- .map((value) => cloneSerializable(value))
1156
1162
  .sort((left, right) => left.sessionId.localeCompare(right.sessionId)),
1157
- };
1163
+ });
1158
1164
  const restore = () => {
1159
1165
  state.revision = beforeRevision;
1160
1166
  state.sessions.clear();
@@ -1204,7 +1210,7 @@ export class FlexHarnessCore<TScope> {
1204
1210
  }
1205
1211
  if (
1206
1212
  (current === undefined && beforeRevision === 0)
1207
- || (current !== undefined && JSON.stringify(current) === JSON.stringify(beforeSnapshot))
1213
+ || (current !== undefined && JSON.stringify(current) === JSON.stringify(beforeSnapshot()))
1208
1214
  ) {
1209
1215
  restore();
1210
1216
  throw error;
@@ -1240,19 +1246,19 @@ export class FlexHarnessCore<TScope> {
1240
1246
  ? undefined
1241
1247
  : cloneSerializable(stored.pendingReversion);
1242
1248
  const beforePendingReversionReleases = cloneSerializable(stored.pendingReversionReleases);
1243
- const beforeSnapshot: TFlexProjectionSnapshot = beforeBaseline ?? {
1249
+ // The copies above stay untouched until a restore, so the projection as it was before the
1250
+ // mutation is built from them only when a failed save has to be reconciled.
1251
+ const beforeSnapshot = (): TFlexProjectionSnapshot => beforeBaseline ?? {
1244
1252
  schemaVersion: 3,
1245
1253
  revision: beforeRevision,
1246
- metrics: cloneSerializable(beforeMetrics),
1247
- messages: cloneSerializable(beforeMessages),
1248
- stagedTerminals: cloneSerializable(beforeStages),
1249
- reversionSegments: cloneSerializable(beforeSegments),
1254
+ metrics: beforeMetrics,
1255
+ messages: beforeMessages,
1256
+ stagedTerminals: beforeStages,
1257
+ reversionSegments: beforeSegments,
1250
1258
  revertCursor: beforeCursor,
1251
- excludedRunIds: cloneSerializable(beforeExcludedRunIds),
1252
- ...(beforePendingReversion === undefined
1253
- ? {}
1254
- : { pendingReversion: cloneSerializable(beforePendingReversion) }),
1255
- pendingReversionReleases: cloneSerializable(beforePendingReversionReleases),
1259
+ excludedRunIds: beforeExcludedRunIds,
1260
+ ...(beforePendingReversion === undefined ? {} : { pendingReversion: beforePendingReversion }),
1261
+ pendingReversionReleases: beforePendingReversionReleases,
1256
1262
  };
1257
1263
  const restore = () => {
1258
1264
  stored.projectionSchemaVersion = beforeSchemaVersion;
@@ -1321,7 +1327,7 @@ export class FlexHarnessCore<TScope> {
1321
1327
  }
1322
1328
  if (
1323
1329
  (current === undefined && beforeRevision === 0)
1324
- || (current !== undefined && JSON.stringify(current) === JSON.stringify(beforeSnapshot))
1330
+ || (current !== undefined && JSON.stringify(current) === JSON.stringify(beforeSnapshot()))
1325
1331
  ) {
1326
1332
  restore();
1327
1333
  throw error;
@@ -29,7 +29,7 @@ export class FlexHarnessResults<TScope> {
29
29
  if (!results || !run.toolCatalog) return normalizeJsonValue(output, this.core.toolOutputLimits);
30
30
  const stored = normalizeJsonValue(output, {
31
31
  ...this.core.toolOutputLimits,
32
- maxBytes: Math.max(maxBytes, FLEX_RESULT_LIMITS.maxStoredBytes),
32
+ maxBytes: this.core.resultLimits.maxStoredBytes,
33
33
  });
34
34
  const bytes = jsonBytes(stored);
35
35
  if (bytes <= maxBytes) return stored;
@@ -53,7 +53,7 @@ export class FlexHarnessResults<TScope> {
53
53
  await results.save(storageKey, run.sessionId, handle, stored);
54
54
  // The session keeps its latest results; older ones are deleted.
55
55
  const handles = await results.list(storageKey, run.sessionId);
56
- for (const old of handles.slice(0, Math.max(0, handles.length - FLEX_RESULT_LIMITS.maxResultsPerSession))) {
56
+ for (const old of handles.slice(0, Math.max(0, handles.length - this.core.resultLimits.maxResultsPerSession))) {
57
57
  await results.delete(storageKey, run.sessionId, old);
58
58
  }
59
59
  });
@@ -77,7 +77,7 @@ export class FlexHarnessResults<TScope> {
77
77
  const method = FLEX_RESULT_METHOD;
78
78
  const stored = await this.core.stores.results!.load(run.state.storageKey, run.sessionId, params.handle);
79
79
  if (stored === undefined) {
80
- return { ok: false, error: { error: 'unknown_handle', method, handle: params.handle, hint: `A handle reads one of the latest ${FLEX_RESULT_LIMITS.maxResultsPerSession} results this session stored.` } };
80
+ return { ok: false, error: { error: 'unknown_handle', method, handle: params.handle, hint: `A handle reads one of the latest ${this.core.resultLimits.maxResultsPerSession} results this session stored.` } };
81
81
  }
82
82
  const path = params.path ?? '';
83
83
  const value = selectPath(stored, path);
@@ -997,38 +997,37 @@ export class FlexHarnessReversions<TScope> {
997
997
  run: IActiveRun,
998
998
  status: 'completed' | 'failed' | 'cancelled',
999
999
  ): Promise<void> {
1000
- if (run.stored.session.agent !== undefined) return;
1000
+ const close = await this.prepareRunReversionClose(run, status);
1001
+ if (close) await this.core.mutateProjection(run.state, run.stored, close, true);
1002
+ }
1003
+
1004
+ /**
1005
+ * Finalizes a run's workspace capture and returns the projection mutation that closes the run's
1006
+ * reversion segment, so a completed run saves the close together with its staged terminal.
1007
+ */
1008
+ public async prepareRunReversionClose(
1009
+ run: IActiveRun,
1010
+ status: 'completed' | 'failed' | 'cancelled',
1011
+ ): Promise<(() => void) | undefined> {
1012
+ if (run.stored.session.agent !== undefined) return undefined;
1001
1013
  const segment = run.stored.reversionSegments.find((entry) => entry.runId === run.runId);
1002
- if (!segment) return;
1003
- const eventIds = run.stored.agentSession.getEvents()
1004
- .filter((event) => event.generationId === run.runId)
1005
- .map((event) => event.id);
1014
+ if (!segment) return undefined;
1006
1015
  if (!this.core.turnReversionProvider || segment.provenance === 'transcript') {
1007
- await this.core.mutateProjection(run.state, run.stored, () => {
1008
- const wasCompleted = segment.status === 'completed';
1009
- segment.status = status;
1010
- segment.eventIds = eventIds;
1011
- if (!wasCompleted && status === 'completed') run.stored.revertCursor++;
1012
- else if (wasCompleted && status !== 'completed') run.stored.revertCursor--;
1016
+ return () => {
1017
+ this.closeRunReversionSegment(run, status);
1013
1018
  delete run.stored.pendingReversion;
1014
1019
  this.pruneReversionState(run.stored);
1015
- }, true);
1016
- return;
1020
+ };
1017
1021
  }
1018
1022
  if (!segment.captureId) {
1019
1023
  throw new FlexHarnessValidationError('Workspace reversion segment has no capture ownership.');
1020
1024
  }
1021
1025
  const captureId = segment.captureId;
1022
1026
  if (segment.workspaceReference !== undefined) {
1023
- await this.core.mutateProjection(run.state, run.stored, () => {
1024
- const wasCompleted = segment.status === 'completed';
1025
- segment.status = status;
1026
- segment.eventIds = eventIds;
1027
- if (!wasCompleted && status === 'completed') run.stored.revertCursor++;
1028
- else if (wasCompleted && status !== 'completed') run.stored.revertCursor--;
1027
+ return () => {
1028
+ this.closeRunReversionSegment(run, status);
1029
1029
  delete run.stored.pendingReversion;
1030
- }, true);
1031
- return;
1030
+ };
1032
1031
  }
1033
1032
  const pending = run.stored.pendingReversion;
1034
1033
  if (pending?.kind !== 'capture' || pending.runId !== run.runId) {
@@ -1059,28 +1058,40 @@ export class FlexHarnessReversions<TScope> {
1059
1058
  pending.state === 'prepared' ? 'finalizing' : pending.state,
1060
1059
  );
1061
1060
  if (outcome === undefined) {
1062
- await this.core.mutateProjection(run.state, run.stored, () => {
1061
+ return () => {
1063
1062
  run.stored.reversionSegments = run.stored.reversionSegments.filter(
1064
1063
  (entry) => entry.runId !== run.runId,
1065
1064
  );
1066
1065
  delete run.stored.pendingReversion;
1067
- }, true);
1068
- return;
1066
+ };
1069
1067
  }
1070
- await this.core.mutateProjection(run.state, run.stored, () => {
1068
+ return () => {
1071
1069
  const current = run.stored.pendingReversion;
1072
1070
  if (current?.kind !== 'capture' || current.runId !== run.runId) {
1073
1071
  throw new FlexHarnessValidationError('Reversion capture intent changed before finalization.');
1074
1072
  }
1075
- const wasCompleted = segment.status === 'completed';
1076
- segment.status = status;
1077
- segment.eventIds = eventIds;
1078
- this.applyFinalizedReversionOutcome(run.stored, segment, outcome);
1079
- if (!wasCompleted && status === 'completed') run.stored.revertCursor++;
1080
- else if (wasCompleted && status !== 'completed') run.stored.revertCursor--;
1073
+ const closed = this.closeRunReversionSegment(run, status);
1074
+ this.applyFinalizedReversionOutcome(run.stored, closed, outcome);
1081
1075
  delete run.stored.pendingReversion;
1082
1076
  this.pruneReversionState(run.stored);
1083
- }, true);
1077
+ };
1078
+ }
1079
+
1080
+ /** Records a run's terminal status and events on its segment and moves the revert cursor. */
1081
+ private closeRunReversionSegment(
1082
+ run: IActiveRun,
1083
+ status: 'completed' | 'failed' | 'cancelled',
1084
+ ): IFlexReversionSegment {
1085
+ const segment = run.stored.reversionSegments.find((entry) => entry.runId === run.runId);
1086
+ if (!segment) throw new FlexHarnessValidationError('Reversion segment changed before finalization.');
1087
+ const wasCompleted = segment.status === 'completed';
1088
+ segment.status = status;
1089
+ segment.eventIds = run.stored.agentSession.getEvents()
1090
+ .filter((event) => event.generationId === run.runId)
1091
+ .map((event) => event.id);
1092
+ if (!wasCompleted && status === 'completed') run.stored.revertCursor++;
1093
+ else if (wasCompleted && status !== 'completed') run.stored.revertCursor--;
1094
+ return segment;
1084
1095
  }
1085
1096
 
1086
1097
  private applyFinalizedReversionOutcome(
@@ -119,6 +119,7 @@ export class FlexHarnessRuns<TScope> {
119
119
  cacheWriteTokens: 0,
120
120
  },
121
121
  reasoningPartIds: new Map(),
122
+ completedTextPartIds: new Set(),
122
123
  toolPartIds: new Map(),
123
124
  trustedToolExecutionErrors: new Map(),
124
125
  unconfirmedToolCalls: new Map(),
@@ -158,11 +159,13 @@ export class FlexHarnessRuns<TScope> {
158
159
  if (run.controller.signal.aborted) throw run.controller.signal.reason;
159
160
  const result = normalizeRunResult(rawResult);
160
161
  if (!run.modelResolution) throw new FlexHarnessValidationError('Generation completed without a resolved model.');
161
- await this.core.reversions.finalizeRunReversion(run, 'completed');
162
+ const closeReversion = await this.core.reversions.prepareRunReversionClose(run, 'completed');
162
163
  await this.core.admission.closeRunDelegatedRunAdmission(run);
163
164
  const terminal = this.buildTerminal(run, 'completed', result);
164
165
  try {
166
+ // The reversion segment closes in the save that stages the terminal.
165
167
  await this.core.mutateProjection(run.state, run.stored, () => {
168
+ closeReversion?.();
166
169
  this.replaceStagedTerminal(run.stored, terminal);
167
170
  }, true);
168
171
  } catch (error) {
@@ -808,6 +811,7 @@ export class FlexHarnessRuns<TScope> {
808
811
  run.reservedAssistantMessage = publicSnapshot(nextAssistant);
809
812
  run.callbackParts = [];
810
813
  run.reasoningPartIds.clear();
814
+ run.completedTextPartIds.clear();
811
815
  this.core.emitEvent(run.scopeId, run.stored.session, {
812
816
  type: 'message.updated',
813
817
  runId: run.runId,
@@ -1026,7 +1030,7 @@ export class FlexHarnessRuns<TScope> {
1026
1030
  for (const part of terminal.assistantMessage.parts) {
1027
1031
  const callbackPart = run.callbackParts.find((entry) => entry.partId === part.partId);
1028
1032
  const shouldComplete = part.type === 'text'
1029
- ? callbackPart?.type === 'text'
1033
+ ? callbackPart?.type === 'text' && !run.completedTextPartIds.has(part.partId)
1030
1034
  : part.type === 'reasoning'
1031
1035
  ? callbackPart?.type === 'reasoning' && callbackPart.status === 'running'
1032
1036
  : part.type === 'tool'
@@ -329,7 +329,8 @@ export class FlexHarnessToolDispatch<TScope> {
329
329
  * (validation, `onCall`, the method's run with its permission requests, the output limits, error
330
330
  * projection) and recorded as a tool part of its own, named by the method, with the `run_code`
331
331
  * call as its parent. Proposals go to `onProposals` as one batch once the program finished; a
332
- * program that fails proposes nothing.
332
+ * program that fails proposes nothing. The calls are recorded in the program's share of the run's
333
+ * callback limits; a call the share cannot hold stops the program at its `callbacks` limit.
333
334
  */
334
335
  private async runCatalogProgram(
335
336
  run: IActiveRun,
@@ -369,9 +370,24 @@ export class FlexHarnessToolDispatch<TScope> {
369
370
  let callNumber = 0;
370
371
  // Read-only calls run concurrently; any other call runs after every call the program made before it.
371
372
  const order = new plugins.AgentToolCallScheduler(abortSignal);
373
+ const share = this.core.callbacks.programCallbackShare(run.state, run.sessionId);
374
+ let recorded = 0;
375
+ /** Records the start of a call; false when the run cannot record it, and the program stops. */
376
+ const record = (id: string, method: string, input: unknown): boolean => {
377
+ if (!this.core.callbacks.onProgramCallStart(run.state, run.sessionId, share, {
378
+ toolCallId: id, toolName: method, input, parentToolCallId: toolCallId,
379
+ })) return false;
380
+ recorded += 1;
381
+ return true;
382
+ };
383
+ const unrecorded = (): TFlexCodeCallOutcome => ({
384
+ ok: false,
385
+ limit: 'callbacks',
386
+ message: `The run cannot record more than ${recorded} call${recorded === 1 ? '' : 's'} of this program.`,
387
+ });
372
388
  const finish = (id: string, method: string, input: unknown, outcome: { output: unknown } | { error: Error }) => {
373
389
  if ('error' in outcome) run.trustedToolExecutionErrors.set(id, outcome.error);
374
- this.core.callbacks.onToolFinish(run.state, run.sessionId, 'error' in outcome
390
+ this.core.callbacks.onProgramCallFinish(run.state, run.sessionId, share, 'error' in outcome
375
391
  ? { toolCallId: id, toolName: method, input, success: false, error: outcome.error.message }
376
392
  : { toolCallId: id, toolName: method, input, success: true, output: outcome.output });
377
393
  };
@@ -385,7 +401,7 @@ export class FlexHarnessToolDispatch<TScope> {
385
401
  const call = await catalog.prepareCall(method, params);
386
402
  if (!call.success) {
387
403
  if (catalog.has(method)) {
388
- this.core.callbacks.onToolStart(run.state, run.sessionId, { toolCallId: id, toolName: method, input: params ?? {}, parentToolCallId: toolCallId });
404
+ if (!record(id, method, params ?? {})) return unrecorded();
389
405
  finish(id, method, params, { error: this.catalogCallError(run, id, call.error) });
390
406
  }
391
407
  return { ok: false, error: call.error as unknown as TJsonValue };
@@ -396,9 +412,7 @@ export class FlexHarnessToolDispatch<TScope> {
396
412
  return stopped(call.method);
397
413
  }
398
414
  if (signal.aborted) return stopped(call.method);
399
- this.core.callbacks.onToolStart(run.state, run.sessionId, {
400
- toolCallId: id, toolName: call.method, input: call.params, parentToolCallId: toolCallId,
401
- });
415
+ if (!record(id, call.method, call.params)) return unrecorded();
402
416
  if ('error' in classification) {
403
417
  finish(id, call.method, call.params, { error: classification.error });
404
418
  return { ok: false, error: { error: 'failed', method: call.method, message: classification.error.message } };
@@ -462,7 +476,9 @@ export class FlexHarnessToolDispatch<TScope> {
462
476
  },
463
477
  };
464
478
  const result = await run.codeSandbox!.run(code, host, catalog.codeMode!, abortSignal);
465
- if (!result.ok) throw this.catalogCallError(run, toolCallId, result.failure);
479
+ if (!result.ok) {
480
+ throw this.catalogCallError(run, toolCallId, normalizeJsonValue(result.failure, this.core.toolOutputLimits));
481
+ }
466
482
  return {
467
483
  result: result.value,
468
484
  calls: result.calls,
@@ -37,15 +37,14 @@ import {
37
37
  assertFlexProjectManagementTombstone,
38
38
  } from './utils.projectmanagement.js';
39
39
  import {
40
+ assertFlexToolJobSnapshot,
40
41
  createAgentEventSnapshot,
41
42
  createToolJobSnapshot,
42
43
  normalizeJsonObjectUndefined,
43
44
  validateAgentEventArchive,
44
45
  validateAgentEventSnapshot,
45
- validateToolJobSnapshot,
46
46
  } from './utils.storecodecs.js';
47
47
  import {
48
- assertProjectionSession,
49
48
  assertProjectManagementSaveAdmitted,
50
49
  assertSameArchiveEvents,
51
50
  isProjectManagementTombstoneAdmitted,
@@ -100,8 +99,11 @@ class InMemoryProjectionStore implements IFlexProjectionStore {
100
99
  expectedRevision: number,
101
100
  ): Promise<void> {
102
101
  const key = providerKey(storageKey, sessionId);
103
- const validated = validateSnapshotSave(snapshot, expectedRevision, assertFlexProjectionSnapshot);
104
- assertProjectionSession(validated, sessionId);
102
+ const validated = validateSnapshotSave(
103
+ snapshot,
104
+ expectedRevision,
105
+ (value) => assertFlexProjectionSnapshot(value, sessionId),
106
+ );
105
107
  const actualRevision = this.snapshots.get(key)?.revision ?? 0;
106
108
  if (actualRevision !== expectedRevision) {
107
109
  throw new FlexHarnessStoreConflictError(key, expectedRevision, actualRevision);
@@ -820,8 +822,7 @@ class JsonProjectionStore implements IFlexProjectionStore {
820
822
  const filePath = this.root.sessionPath('projections', storageKey, sessionId);
821
823
  return JsonFileRoot.inQueue(filePath, async () => {
822
824
  const snapshot = await this.root.read(filePath, (value) => {
823
- assertFlexProjectionSnapshot(value);
824
- assertProjectionSession(value, sessionId);
825
+ assertFlexProjectionSnapshot(value, sessionId);
825
826
  return value;
826
827
  });
827
828
  return snapshot ? cloneSerializable(snapshot) : undefined;
@@ -834,13 +835,15 @@ class JsonProjectionStore implements IFlexProjectionStore {
834
835
  snapshot: IFlexProjectionSnapshotV3,
835
836
  expectedRevision: number,
836
837
  ): Promise<void> {
837
- const validated = validateSnapshotSave(snapshot, expectedRevision, assertFlexProjectionSnapshot);
838
- assertProjectionSession(validated, sessionId);
838
+ const validated = validateSnapshotSave(
839
+ snapshot,
840
+ expectedRevision,
841
+ (value) => assertFlexProjectionSnapshot(value, sessionId),
842
+ );
839
843
  const filePath = this.root.sessionPath('projections', storageKey, sessionId);
840
844
  await JsonFileRoot.inQueue(filePath, async () => {
841
845
  const current = await this.root.read(filePath, (value) => {
842
- assertFlexProjectionSnapshot(value);
843
- assertProjectionSession(value, sessionId);
846
+ assertFlexProjectionSnapshot(value, sessionId);
844
847
  return value;
845
848
  });
846
849
  const actualRevision = current?.revision ?? 0;
@@ -1214,7 +1217,10 @@ class JsonToolJobStore implements plugins.IToolJobStore {
1214
1217
  const filePath = this.filePath();
1215
1218
  return JsonFileRoot.inQueue(filePath, async () => {
1216
1219
  abortSignal?.throwIfAborted();
1217
- const snapshot = await this.root.read(filePath, validateToolJobSnapshot);
1220
+ const snapshot = await this.root.read(filePath, (value) => {
1221
+ assertFlexToolJobSnapshot(value);
1222
+ return value;
1223
+ });
1218
1224
  abortSignal?.throwIfAborted();
1219
1225
  return snapshot ? cloneSerializable(snapshot) : undefined;
1220
1226
  });
@@ -1231,7 +1237,10 @@ class JsonToolJobStore implements plugins.IToolJobStore {
1231
1237
  const filePath = this.filePath();
1232
1238
  return JsonFileRoot.inQueue(filePath, async () => {
1233
1239
  abortSignal?.throwIfAborted();
1234
- const current = await this.root.read(filePath, validateToolJobSnapshot);
1240
+ const current = await this.root.read(filePath, (value) => {
1241
+ assertFlexToolJobSnapshot(value);
1242
+ return value;
1243
+ });
1235
1244
  const actualRevision = current?.revision ?? 0;
1236
1245
  if (actualRevision !== expectedRevision) {
1237
1246
  throw new plugins.ToolJobStoreConflictError(expectedRevision, actualRevision);
package/ts/errors.ts CHANGED
@@ -217,6 +217,20 @@ export class FlexHarnessStoreCommitUncertainError extends FlexHarnessError {
217
217
  }
218
218
  }
219
219
 
220
+ /** A record the store refused to create because a fenced purge removed its storage namespace. */
221
+ export class FlexHarnessStorePurgedError extends FlexHarnessError {
222
+ public readonly path: string;
223
+
224
+ constructor(path: string) {
225
+ super(
226
+ 'FLEX_STORE_PURGED',
227
+ `Store write at "${path}" was refused: a fenced purge removed its storage namespace.`,
228
+ );
229
+ this.name = 'FlexHarnessStorePurgedError';
230
+ this.path = path;
231
+ }
232
+ }
233
+
220
234
  export class FlexHarnessRunError extends AggregateError {
221
235
  public readonly code = 'FLEX_RUN_FAILED';
222
236
 
package/ts/index.ts CHANGED
@@ -9,13 +9,19 @@ export {
9
9
  type TFlexToolPermissionDecision,
10
10
  type TFlexToolPermissionRequester,
11
11
  } from './functions.toolpermissions.js';
12
- export { normalizeJsonValue } from './utils.json.js';
12
+ export {
13
+ assertFlexPermissionSnapshot,
14
+ assertFlexProjectionSnapshot,
15
+ assertFlexScopeSnapshot,
16
+ normalizeJsonValue,
17
+ } from './utils.json.js';
13
18
  export {
14
19
  assertFlexProjectManagementSnapshot,
15
20
  assertFlexProjectManagementRecord,
16
21
  assertFlexProjectManagementTombstone,
17
22
  createEmptyFlexProjectManagementSnapshot,
18
23
  } from './utils.projectmanagement.js';
24
+ export { assertFlexToolJobSnapshot } from './utils.storecodecs.js';
19
25
  export {
20
26
  FLEX_SLASH_COMMAND_MAX_INPUT_BYTES,
21
27
  isReservedSlashCommandName,
@@ -224,6 +224,11 @@ export interface IActiveRun {
224
224
  usage: IFlexUsage;
225
225
  metricsOperation?: ISessionMetricsOperation;
226
226
  reasoningPartIds: Map<string, string>;
227
+ /**
228
+ * The text parts whose model step is committed. Each emitted its `part.completed`; text that
229
+ * follows starts a new part.
230
+ */
231
+ completedTextPartIds: Set<string>;
227
232
  toolPartIds: Map<string, string>;
228
233
  trustedToolExecutionErrors: Map<string, Error>;
229
234
  /**
@@ -281,6 +286,16 @@ export interface IRunReadCall {
281
286
  readonly output: Promise<unknown>;
282
287
  }
283
288
 
289
+ /**
290
+ * What is left of the room a `run_code` program's calls may take in its run's callback limits:
291
+ * events, output bytes and parts.
292
+ */
293
+ export interface IProgramCallbackShare {
294
+ events: number;
295
+ outputBytes: number;
296
+ parts: number;
297
+ }
298
+
284
299
  /** The steers of a run: the input its generation takes in, and the prompts it accepted for it. */
285
300
  export interface IRunSteering {
286
301
  readonly input: plugins.AgentPendingInputQueue;
package/ts/interfaces.ts CHANGED
@@ -706,10 +706,28 @@ export interface IFlexToolProvider<TScope> {
706
706
  /** The catalog method that reads stored results, reserved while a run stores results. */
707
707
  export const FLEX_RESULT_METHOD = 'results.get';
708
708
 
709
+ /**
710
+ * Bounds what a session stores in the result store: at most `maxResultsPerSession` results of at
711
+ * most `maxStoredBytes` each.
712
+ */
713
+ export interface IFlexResultLimits {
714
+ /**
715
+ * The most JSON bytes of one output a result stores; beyond it the stored value is truncated. At
716
+ * least `toolOutputLimits.maxBytes`. Defaults to `FLEX_RESULT_LIMITS.maxStoredBytes`, or to
717
+ * `toolOutputLimits.maxBytes` if that is larger.
718
+ */
719
+ maxStoredBytes?: number;
720
+ /**
721
+ * The results a session keeps; a new one beyond them deletes the oldest. Defaults to
722
+ * `FLEX_RESULT_LIMITS.maxResultsPerSession`.
723
+ */
724
+ maxResultsPerSession?: number;
725
+ }
726
+
709
727
  export const FLEX_RESULT_LIMITS = Object.freeze({
710
- /** The most of one output a result stores; beyond it the stored value is truncated. */
728
+ /** The default of `resultLimits.maxStoredBytes`. */
711
729
  maxStoredBytes: 8 * 1024 * 1024,
712
- /** The results a session keeps; a new one beyond them deletes the oldest. */
730
+ /** The default of `resultLimits.maxResultsPerSession`. */
713
731
  maxResultsPerSession: 32,
714
732
  /** The size of the summary the model gets in place of the output. */
715
733
  summaryBytes: 2 * 1024,
@@ -1044,8 +1062,11 @@ export const FLEX_CODE_MODE_MAXIMUM_LIMITS: Readonly<IFlexCodeModeLimits> = Obje
1044
1062
  /** What `run_code` reports to the model, as JSON, when a program does not finish. */
1045
1063
  export interface IFlexCodeRunFailure {
1046
1064
  error: 'syntax_error' | 'program_error' | 'limit_exceeded' | 'aborted';
1047
- /** The limit that stopped the program, for `limit_exceeded`. */
1048
- limit?: 'source' | 'cpu' | 'wall' | 'memory' | 'stack' | 'calls';
1065
+ /**
1066
+ * The limit that stopped the program, for `limit_exceeded`; `callbacks` when its run could not
1067
+ * record another of its calls.
1068
+ */
1069
+ limit?: 'source' | 'cpu' | 'wall' | 'memory' | 'stack' | 'calls' | 'callbacks';
1049
1070
  name?: string;
1050
1071
  message: string;
1051
1072
  /** The program's line, when known. */
@@ -1456,9 +1477,9 @@ export interface IFlexToolJobStoreProvider {
1456
1477
  /**
1457
1478
  * Tool outputs over the output budget, stored whole for the session that produced them. A result
1458
1479
  * is written once under its handle and read by `results.get`; FlexHarness keeps a session's latest
1459
- * `FLEX_RESULT_LIMITS.maxResultsPerSession` results and deletes older ones. A FlexHarness writes a
1460
- * session's results one operation at a time, so a store need not order concurrent writes of one
1461
- * session.
1480
+ * `resultLimits.maxResultsPerSession` results, each of at most `resultLimits.maxStoredBytes` JSON
1481
+ * bytes, and deletes older ones. A FlexHarness writes a session's results one operation at a time,
1482
+ * so a store need not order concurrent writes of one session.
1462
1483
  */
1463
1484
  export interface IFlexResultStore {
1464
1485
  save(storageKey: string, sessionId: string, handle: string, value: TJsonValue): Promise<void>;
@@ -1478,8 +1499,8 @@ export interface IFlexHarnessStores {
1478
1499
  jobs: IFlexToolJobStoreProvider;
1479
1500
  /**
1480
1501
  * Stores the outputs of a run with a tool catalog that exceed the output budget: the model gets a
1481
- * summary and a handle it reads with `results.get`. Without it, such outputs are truncated to
1482
- * the budget. The built-in stores provide it.
1502
+ * summary and a handle it reads with `results.get`; `resultLimits` bounds what a session stores.
1503
+ * Without it, such outputs are truncated to the budget. The built-in stores provide it.
1483
1504
  */
1484
1505
  results?: IFlexResultStore;
1485
1506
  }
@@ -1826,6 +1847,8 @@ export interface IFlexHarnessOptions<TScope> {
1826
1847
  promptQueueLimits?: IFlexPromptQueueLimits;
1827
1848
  reversionLimits?: IFlexReversionLimits;
1828
1849
  sessionImportLimits?: IFlexSessionImportLimits;
1850
+ /** Bounds the outputs over the budget that a session stores; see `stores.results`. */
1851
+ resultLimits?: IFlexResultLimits;
1829
1852
  /**
1830
1853
  * Bounds each turn-reversion maintenance operation. Falls back to
1831
1854
  * agentSessionPolicy.generationLeaseCleanupTimeoutMs, then 30 seconds.
package/ts/plugins.ts CHANGED
@@ -3,8 +3,9 @@ import { AsyncLocalStorage } from 'node:async_hooks';
3
3
  import * as crypto from 'node:crypto';
4
4
  import * as fs from 'node:fs/promises';
5
5
  import * as path from 'node:path';
6
+ import { Worker } from 'node:worker_threads';
6
7
 
7
- export { AsyncLocalStorage, crypto, fs, path };
8
+ export { AsyncLocalStorage, crypto, fs, path, Worker };
8
9
 
9
10
  // third party scope
10
11
  import { asSchema, jsonSchema } from 'ai';
@@ -70,23 +71,19 @@ export interface IAmaroError {
70
71
  }
71
72
 
72
73
  export interface ICodeSandboxEngine {
73
- /** A new WebAssembly instance of QuickJS-ng, with a memory of its own. */
74
- newModule(): Promise<IQuickJsModule>;
75
74
  /** Removes TypeScript type syntax, keeping every line where it was. */
76
75
  stripTypes(source: string): string;
77
76
  }
78
77
 
79
- /** Loads QuickJS and amaro, the optional peers code mode needs. */
78
+ /**
79
+ * Loads amaro, and finds QuickJS: the optional peers code mode needs. Every program's worker loads
80
+ * QuickJS itself.
81
+ */
80
82
  export const importCodeSandboxEngine = async (): Promise<ICodeSandboxEngine> => {
81
- const [core, variant, amaro] = await Promise.all([
82
- import('quickjs-emscripten-core'),
83
- import('@jitl/quickjs-ng-wasmfile-release-sync'),
84
- import('amaro'),
85
- ]);
86
- // The variant's typings resolve as CommonJS; at run time the default export is the variant.
87
- const quickjsVariant = (variant as { default: unknown }).default as Parameters<typeof core.newQuickJSWASMModuleFromVariant>[0];
83
+ import.meta.resolve('quickjs-emscripten-core');
84
+ import.meta.resolve('@jitl/quickjs-ng-wasmfile-release-sync');
85
+ const amaro = await import('amaro');
88
86
  return {
89
- newModule: async () => await core.newQuickJSWASMModuleFromVariant(quickjsVariant) as unknown as IQuickJsModule,
90
87
  stripTypes: (source) => amaro.transformSync(source, { mode: 'strip-only' }).code,
91
88
  };
92
89
  };