@modelprofile.com/flexharness 4.0.1 → 4.1.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.
@@ -12,6 +12,7 @@ export type TFlexAgentModelMessage = ReturnType<plugins.IAgentSession['getModelM
12
12
  export type TFlexAgentToolSet = NonNullable<TFlexAgentSessionOptions['tools']>;
13
13
  export type TFlexAgentProviderOptions = TFlexAgentSessionOptions['providerOptions'];
14
14
  export type TFlexAgentCacheSetting = TFlexAgentSessionOptions['cache'];
15
+ export type TFlexAgentToolCallUpdateEvent = Parameters<NonNullable<TFlexAgentSessionOptions['onToolCallUpdate']>>[0];
15
16
  export type TFlexAgentToolCallFinishEvent = Parameters<NonNullable<TFlexAgentSessionOptions['onToolCallFinish']>>[0];
16
17
  export interface IFlexTextPromptPart {
17
18
  type: 'text';
@@ -441,6 +442,10 @@ export interface IFlexPermissionSnapshot {
441
442
  }
442
443
  export declare const FLEX_PROJECT_MANAGEMENT_SCHEMA_VERSION: 1;
443
444
  export declare const FLEX_SESSION_GENERATION_ID_MAX_BYTES = 128;
445
+ export interface IFlexSessionGeneration {
446
+ sessionGenerationId: string;
447
+ sessionGenerationSequence: number;
448
+ }
444
449
  export declare const FLEX_PROJECT_MANAGEMENT_LIMITS: Readonly<{
445
450
  maxGoalBytes: number;
446
451
  maxScratchpadBytes: number;
@@ -485,7 +490,7 @@ export interface IFlexProjectManagementWriteContext {
485
490
  toolCallId?: string;
486
491
  }
487
492
  export interface IFlexProjectManagementStore {
488
- load(storageKey: string, sessionId: string): Promise<TFlexProjectManagementRecord | undefined>;
493
+ load(storageKey: string, sessionId: string, sessionGeneration?: Readonly<IFlexSessionGeneration>): Promise<TFlexProjectManagementRecord | undefined>;
489
494
  save(storageKey: string, sessionId: string, snapshot: IFlexProjectManagementSnapshot, expectedRevision: number, writeContext: IFlexProjectManagementWriteContext): Promise<void>;
490
495
  tombstoneSession(storageKey: string, sessionId: string, tombstone: IFlexProjectManagementTombstone, expectedRevision: number): Promise<void>;
491
496
  purgeNamespace(storageKey: string): Promise<void>;
@@ -26,4 +26,4 @@ export const FLEX_REVERSION_MAX_AFFECTED_WORKSPACES = 64;
26
26
  export const FLEX_REVERSION_WORKSPACE_ID_MAX_BYTES = 512;
27
27
  export const FLEX_REVERSION_WORKSPACE_LABEL_MAX_BYTES = 2048;
28
28
  export const FLEX_REVERSION_REASON_CODE_MAX_BYTES = 128;
29
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW50ZXJmYWNlcy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3RzL2ludGVyZmFjZXMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBMGpCQSxNQUFNLENBQUMsTUFBTSxzQ0FBc0MsR0FBRyxDQUFVLENBQUM7QUFDakUsTUFBTSxDQUFDLE1BQU0sb0NBQW9DLEdBQUcsR0FBRyxDQUFDO0FBRXhELE1BQU0sQ0FBQyxNQUFNLDhCQUE4QixHQUFHLE1BQU0sQ0FBQyxNQUFNLENBQUM7SUFDMUQsWUFBWSxFQUFFLENBQUMsR0FBRyxJQUFJO0lBQ3RCLGtCQUFrQixFQUFFLEdBQUcsR0FBRyxJQUFJO0lBQzlCLG1CQUFtQixFQUFFLENBQUMsR0FBRyxJQUFJO0lBQzdCLGNBQWMsRUFBRSxHQUFHO0lBQ25CLFFBQVEsRUFBRSxHQUFHO0lBQ2IsYUFBYSxFQUFFLElBQUk7SUFDbkIsZ0JBQWdCLEVBQUUsSUFBSSxHQUFHLElBQUk7Q0FDOUIsQ0FBQyxDQUFDO0FBeUxILE1BQU0sQ0FBQyxNQUFNLDZCQUE2QixHQUFHLE1BQU0sQ0FBQyxNQUFNLENBQUM7SUFDekQsaUJBQWlCLEVBQUUsR0FBRztJQUN0QixXQUFXLEVBQUUsR0FBRztJQUNoQixpQkFBaUIsRUFBRSxJQUFJO0lBQ3ZCLDJCQUEyQixFQUFFLElBQUk7Q0FDbEMsQ0FBQyxDQUFDO0FBRUgsTUFBTSxDQUFDLE1BQU0sNkJBQTZCLEdBQUcsTUFBTSxDQUFDLE1BQU0sQ0FBQztJQUN6RCxpQkFBaUIsRUFBRSxJQUFJO0lBQ3ZCLFdBQVcsRUFBRSxJQUFJO0lBQ2pCLGlCQUFpQixFQUFFLE1BQU07SUFDekIsMkJBQTJCLEVBQUUsTUFBTTtDQUNwQyxDQUFDLENBQUM7QUFFSCxNQUFNLENBQUMsTUFBTSxrQ0FBa0MsR0FBRyxHQUFHLEdBQUcsSUFBSSxDQUFDO0FBQzdELE1BQU0sQ0FBQyxNQUFNLHNDQUFzQyxHQUFHLEVBQUUsQ0FBQztBQUN6RCxNQUFNLENBQUMsTUFBTSxxQ0FBcUMsR0FBRyxHQUFHLENBQUM7QUFDekQsTUFBTSxDQUFDLE1BQU0sd0NBQXdDLEdBQUcsSUFBSSxDQUFDO0FBQzdELE1BQU0sQ0FBQyxNQUFNLG9DQUFvQyxHQUFHLEdBQUcsQ0FBQyJ9
29
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW50ZXJmYWNlcy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3RzL2ludGVyZmFjZXMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBNmpCQSxNQUFNLENBQUMsTUFBTSxzQ0FBc0MsR0FBRyxDQUFVLENBQUM7QUFDakUsTUFBTSxDQUFDLE1BQU0sb0NBQW9DLEdBQUcsR0FBRyxDQUFDO0FBT3hELE1BQU0sQ0FBQyxNQUFNLDhCQUE4QixHQUFHLE1BQU0sQ0FBQyxNQUFNLENBQUM7SUFDMUQsWUFBWSxFQUFFLENBQUMsR0FBRyxJQUFJO0lBQ3RCLGtCQUFrQixFQUFFLEdBQUcsR0FBRyxJQUFJO0lBQzlCLG1CQUFtQixFQUFFLENBQUMsR0FBRyxJQUFJO0lBQzdCLGNBQWMsRUFBRSxHQUFHO0lBQ25CLFFBQVEsRUFBRSxHQUFHO0lBQ2IsYUFBYSxFQUFFLElBQUk7SUFDbkIsZ0JBQWdCLEVBQUUsSUFBSSxHQUFHLElBQUk7Q0FDOUIsQ0FBQyxDQUFDO0FBMExILE1BQU0sQ0FBQyxNQUFNLDZCQUE2QixHQUFHLE1BQU0sQ0FBQyxNQUFNLENBQUM7SUFDekQsaUJBQWlCLEVBQUUsR0FBRztJQUN0QixXQUFXLEVBQUUsR0FBRztJQUNoQixpQkFBaUIsRUFBRSxJQUFJO0lBQ3ZCLDJCQUEyQixFQUFFLElBQUk7Q0FDbEMsQ0FBQyxDQUFDO0FBRUgsTUFBTSxDQUFDLE1BQU0sNkJBQTZCLEdBQUcsTUFBTSxDQUFDLE1BQU0sQ0FBQztJQUN6RCxpQkFBaUIsRUFBRSxJQUFJO0lBQ3ZCLFdBQVcsRUFBRSxJQUFJO0lBQ2pCLGlCQUFpQixFQUFFLE1BQU07SUFDekIsMkJBQTJCLEVBQUUsTUFBTTtDQUNwQyxDQUFDLENBQUM7QUFFSCxNQUFNLENBQUMsTUFBTSxrQ0FBa0MsR0FBRyxHQUFHLEdBQUcsSUFBSSxDQUFDO0FBQzdELE1BQU0sQ0FBQyxNQUFNLHNDQUFzQyxHQUFHLEVBQUUsQ0FBQztBQUN6RCxNQUFNLENBQUMsTUFBTSxxQ0FBcUMsR0FBRyxHQUFHLENBQUM7QUFDekQsTUFBTSxDQUFDLE1BQU0sd0NBQXdDLEdBQUcsSUFBSSxDQUFDO0FBQzdELE1BQU0sQ0FBQyxNQUFNLG9DQUFvQyxHQUFHLEdBQUcsQ0FBQyJ9
@@ -5,4 +5,4 @@ import * as path from 'node:path';
5
5
  export { AsyncLocalStorage, crypto, fs, path };
6
6
  import { AgentEventStoreConflictError, AgentSession, buildModelMessages, ToolJobStoreConflictError, createAgentEvent, filterModelVisibleAgentEvents, getAgentGenerationTransactions, modelMessagesToAgentEvents, tool, validateAgentEventArchiveV2, validateAgentEventSnapshotV2, z } from '@push.rocks/smartagent';
7
7
  export { AgentEventStoreConflictError, AgentSession, buildModelMessages, ToolJobStoreConflictError, createAgentEvent, filterModelVisibleAgentEvents, getAgentGenerationTransactions, modelMessagesToAgentEvents, tool, validateAgentEventArchiveV2, validateAgentEventSnapshotV2, z, };
8
- export type { IAgentEventArchiveV2, IAgentContextCompactionOptions, IAgentEventSnapshotV2, IAgentEventStoreV2, IAgentGenerateResult, IAgentGenerationHandle, IAgentGenerationLease, IAgentRunResult, IAgentSession, IAgentSessionOptions, IAgentRuntimeEventPayload, IGenerationBegunEvent, IGenerationExecutionCompletedEvent, IGenerationExecutionStartedEvent, IGenerationOutcomeEvent, IToolExecutionContext, IToolExecutionIntentEvent, IToolJobSnapshot, IToolJobState, IToolJobStore, TAgentContextBuilder, TAgentContextCompactor, TAgentEvent, TAgentGenerationOutcome, TAgentToolCallFinishEvent, TAgentToolExecutionReconciliationOptions, } from '@push.rocks/smartagent';
8
+ export type { IAgentEventArchiveV2, IAgentContextCompactionOptions, IAgentEventSnapshotV2, IAgentEventStoreV2, IAgentGenerateResult, IAgentGenerationHandle, IAgentGenerationLease, IAgentRunResult, IAgentSession, IAgentSessionOptions, IAgentToolCallUpdateEvent, IAgentRuntimeEventPayload, IGenerationBegunEvent, IGenerationExecutionCompletedEvent, IGenerationExecutionStartedEvent, IGenerationOutcomeEvent, IToolExecutionContext, IToolExecutionIntentEvent, IToolJobSnapshot, IToolJobState, IToolJobStore, TAgentContextBuilder, TAgentContextCompactor, TAgentEvent, TAgentGenerationOutcome, TAgentToolCallFinishEvent, TAgentToolExecutionReconciliationOptions, } from '@push.rocks/smartagent';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@modelprofile.com/flexharness",
3
- "version": "4.0.1",
3
+ "version": "4.1.0",
4
4
  "private": false,
5
5
  "description": "Provider-neutral model-session runtime with durable history, permissions, typed events, and pluggable local or remote tool execution.",
6
6
  "main": "dist_ts/index.js",
@@ -22,7 +22,7 @@
22
22
  "node": ">=24"
23
23
  },
24
24
  "dependencies": {
25
- "@push.rocks/smartagent": "^4.6.0",
25
+ "@push.rocks/smartagent": "^5.0.0",
26
26
  "@types/json-schema": "7.0.15"
27
27
  },
28
28
  "devDependencies": {
package/readme.hints.md CHANGED
@@ -38,6 +38,7 @@ Implementation findings for flexharness.
38
38
  ## Tool output boundary
39
39
 
40
40
  - Every tool `execute` result and every async-iterable yield is converted to bounded JSON before SmartAgent observes it. Thrown errors and iterator failures are not converted or swallowed.
41
+ - SmartAgent preliminary tool outputs mutate only the run-local running tool part and emit cumulative `part.updated` snapshots. Terminal success replaces them with the authoritative final output; failure, cancellation, and callback overflow remove them before persistence.
41
42
  - JSON byte limits are propagated through traversal. Large strings never enter normalized output, and array/object traversal stops once the remaining allowance is reserved for deterministic truncation metadata.
42
43
  - Model callbacks use bounded synchronous run-local parts. Adjacent text/reasoning deltas coalesce; no per-delta snapshot is written. Callback event, byte, or part overflow aborts internally and is classified as failure, not owner cancellation.
43
44
 
package/readme.md CHANGED
@@ -241,7 +241,7 @@ Public writes use `{ actor: 'application' }`. Tool writes use `{ actor: 'agent',
241
241
 
242
242
  `FLEX_PROJECT_MANAGEMENT_LIMITS` exports the hard UTF-8 and aggregate limits: goal 8 KiB, scratchpad 128 KiB, task content 8 KiB, task ID 512 bytes, title 2048 bytes, 512 tasks, and a 1 MiB serialized snapshot. The aggregate bound leaves room for worst-case JSON escaping of a controller-valid scratchpad. Loaded snapshots reject extra fields, duplicate IDs, invalid status/priority/timestamps, non-JSON data, wrong schema/revision, and every exceeded bound before use.
243
243
 
244
- `IFlexProjectManagementStore` is exact per `(storageKey, sessionId)`: `load`, CAS `save`, CAS `tombstoneSession`, and `purgeNamespace` must not collapse multiple sessions or storage namespaces. `load()` returns `TFlexProjectManagementRecord | undefined`. Same-generation live saves use normal revision CAS, and a same-generation tombstone permanently rejects later live saves. A higher `sessionGenerationSequence` with a different strong `sessionGenerationId` may replace only an older tombstone using expected revision `0`; it cannot replace a live record. This resets the PM revision for a recreated core session while stale saves and tombstones from older generations remain fenced. Deleting a recreated session that made no PM writes still replaces the prior-generation tombstone with a revision-1 tombstone for the new generation.
244
+ `IFlexProjectManagementStore` is exact per `(storageKey, sessionId)`: `load`, CAS `save`, CAS `tombstoneSession`, and `purgeNamespace` must not collapse multiple sessions or storage namespaces. `load()` returns `TFlexProjectManagementRecord | undefined` and receives the current session generation as an optional third argument; FlexHarness always supplies it, while two-argument callers and stores remain compatible. Same-generation live saves use normal revision CAS, and a same-generation tombstone permanently rejects later live saves. A higher `sessionGenerationSequence` with a different strong `sessionGenerationId` may replace only an older tombstone using expected revision `0`; it cannot replace a live record. This resets the PM revision for a recreated core session while stale saves and tombstones from older generations remain fenced. Deleting a recreated session that made no PM writes still replaces the prior-generation tombstone with a revision-1 tombstone for the new generation.
245
245
 
246
246
  Every newly created core session exposes and persists a strong random `sessionGenerationId` plus its monotonic `sessionGenerationSequence`. A legacy scope session without those fields is assigned a deterministic bounded ID derived from its immutable `storageKey`, `sessionId`, and `createdAt`; FlexHarness persists the repaired scope snapshot before accepting work. Grouped core deletion tombstones retain both fields after live metadata is removed.
247
247
 
@@ -632,7 +632,7 @@ FlexHarness wraps every provided tool `execute` method before SmartAgent receive
632
632
 
633
633
  `toolOutputLimits` in the complete setup above bounds traversal depth and encoded bytes. The normalizer enforces its byte allowance incrementally: oversized strings are replaced before entering output, and arrays/objects stop reading entries once only truncation metadata fits.
634
634
 
635
- Streaming callbacks use run-local synchronous state rather than one persistence promise per delta. Adjacent text and reasoning deltas coalesce. `callbackLimits` bounds callback events, accumulated output bytes, and part count; overflow aborts internally with `FlexHarnessCallbackOverflowError` and the turn is recorded as failed. Reservation and terminal finalization are the normal persistence checkpoints, with permission state changes as explicit additional checkpoints.
635
+ Streaming callbacks use run-local synchronous state rather than one persistence promise per delta. Adjacent text and reasoning deltas coalesce. Preliminary async-iterable tool outputs appear as bounded cumulative `part.updated` snapshots while the tool remains `running`; only the authoritative `part.completed` output enters the terminal projection, and failed or interrupted tools discard their preliminary output. `callbackLimits` bounds callback events, accumulated output bytes, and part count; overflow aborts internally with `FlexHarnessCallbackOverflowError` and the turn is recorded as failed. Reservation and terminal finalization are the normal persistence checkpoints, with permission state changes as explicit additional checkpoints.
636
636
 
637
637
  Model resolver, tool provider, AgentSession, tool execution, tool callback, tool cleanup, and run-persistence failures cross an untrusted error boundary. By default they become a fixed immutable `FlexHarnessExternalError` before completion rejection, persistence, events, or detached-cleanup reporting. Raw external messages and aggregate members are not retained. A failed `onToolCallFinish` callback stores and accounts for only the bounded projected message; it does not otherwise reject completion, although exceeding the configured callback limits still fails the run. Scope resolution and the initial store load happen before a run exists and remain outside this boundary.
638
638
 
@@ -685,7 +685,7 @@ const unsubscribe = harness.subscribe((event) => {
685
685
  unsubscribe();
686
686
  ```
687
687
 
688
- Events are discriminated, sequenced, deeply immutable snapshots. Listener exceptions are isolated from runs and other listeners. Every accepted queue entry emits `prompt.queued` and exactly one `prompt.finished`. 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. 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. `part.updated` is cumulative metadata replacement rather than a text delta. `session.history.changed` carries `direction: 'undo' | 'redo' | 'branch'`; `runId` is present for undo and redo. Events contain public IDs and snapshots only; they do not expose prompt payloads, the resolved scope object, storage key, model object, provider options, or raw storage key.
688
+ Events are discriminated, sequenced, deeply immutable snapshots. Listener exceptions are isolated from runs and other listeners. Every accepted queue entry emits `prompt.queued` and exactly one `prompt.finished`. 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. 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. `part.updated` is 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 and snapshots only; they do not expose prompt payloads, the resolved scope object, storage key, model object, provider options, or raw storage key.
689
689
 
690
690
  ## Stores
691
691
 
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@modelprofile.com/flexharness',
6
- version: '4.0.1',
6
+ version: '4.1.0',
7
7
  description: 'Provider-neutral model-session runtime with durable history, permissions, typed events, and pluggable local or remote tool execution.'
8
8
  }
@@ -96,6 +96,7 @@ import type {
96
96
  IFlexUsage,
97
97
  IJsonObject,
98
98
  TFlexAgentRunResult,
99
+ TFlexAgentToolCallUpdateEvent,
99
100
  TFlexAgentToolCallFinishEvent,
100
101
  TFlexAgentToolSet,
101
102
  TFlexAttachmentSource,
@@ -556,7 +557,7 @@ function requireSessionGeneration(
556
557
  function isAsyncIterable(value: unknown): value is AsyncIterable<unknown> {
557
558
  return Boolean(
558
559
  value
559
- && typeof value === 'object'
560
+ && (typeof value === 'object' || typeof value === 'function')
560
561
  && typeof (value as { [Symbol.asyncIterator]?: unknown })[Symbol.asyncIterator] === 'function',
561
562
  );
562
563
  }
@@ -601,6 +602,9 @@ function wrapToolSet(
601
602
  return Promise.resolve(output).then(
602
603
  (value) => {
603
604
  try {
605
+ if (isAsyncIterable(value)) {
606
+ return normalizeAsyncIterable(value, limits, projectError);
607
+ }
604
608
  return normalizeJsonValue(value, limits);
605
609
  } catch (error) {
606
610
  throw projectError(error);
@@ -4391,7 +4395,11 @@ export class FlexHarness<TScope = unknown> {
4391
4395
  ): Promise<IFlexProjectManagementSnapshot> {
4392
4396
  const store = this.stores.projectManagement;
4393
4397
  const generation = requireSessionGeneration(stored.session);
4394
- const record = await store.load(state.storageKey, stored.session.sessionId);
4398
+ const record = await store.load(
4399
+ state.storageKey,
4400
+ stored.session.sessionId,
4401
+ Object.freeze({ ...generation }),
4402
+ );
4395
4403
  if (record === undefined) {
4396
4404
  return createEmptyFlexProjectManagementSnapshot(
4397
4405
  generation.sessionGenerationId,
@@ -5663,6 +5671,7 @@ export class FlexHarness<TScope = unknown> {
5663
5671
  part.status = status === 'completed' ? 'completed' : 'cancelled';
5664
5672
  } else if (part.type === 'tool' && part.status === 'running') {
5665
5673
  part.status = status === 'cancelled' ? 'cancelled' : 'failed';
5674
+ delete part.output;
5666
5675
  part.error = status === 'cancelled'
5667
5676
  ? 'Tool execution was cancelled.'
5668
5677
  : 'Tool execution ended without a terminal event.';
@@ -6924,6 +6933,7 @@ export class FlexHarness<TScope = unknown> {
6924
6933
  part.output = output!;
6925
6934
  } else {
6926
6935
  part.status = 'failed';
6936
+ delete part.output;
6927
6937
  part.error = projectedError!.message;
6928
6938
  }
6929
6939
  this.emitPartEvent(run, 'part.completed', part);
@@ -6932,6 +6942,31 @@ export class FlexHarness<TScope = unknown> {
6932
6942
  }
6933
6943
  }
6934
6944
 
6945
+ private onToolUpdate(
6946
+ state: IStorageState,
6947
+ sessionId: string,
6948
+ event: TFlexAgentToolCallUpdateEvent,
6949
+ ): void {
6950
+ let run = this.callbackRun(state, sessionId);
6951
+ if (!run) return;
6952
+ if (!run.toolPartIds.has(event.toolCallId)) {
6953
+ this.onToolStart(state, sessionId, event);
6954
+ run = this.callbackRun(state, sessionId);
6955
+ if (!run) return;
6956
+ }
6957
+ const partId = run.toolPartIds.get(event.toolCallId);
6958
+ const part = run.callbackParts.find((entry) => entry.partId === partId && entry.type === 'tool');
6959
+ if (!part || part.type !== 'tool' || part.status !== 'running') return;
6960
+ try {
6961
+ const output = normalizeJsonValue(event.output, this.toolOutputLimits);
6962
+ if (!this.reserveCallbackCapacity(run, 1, jsonBytes(output), 0)) return;
6963
+ part.output = output;
6964
+ this.emitPartEvent(run, 'part.updated', part);
6965
+ } catch (error) {
6966
+ this.failCallback(run, this.projectExternalError(run, error, 'toolCallback'));
6967
+ }
6968
+ }
6969
+
6935
6970
  private callbackRun(state: IStorageState, sessionId: string): IActiveRun | undefined {
6936
6971
  const run = state.activeRuns.get(sessionId);
6937
6972
  return !run || run.callbacksClosed || run.callbackError ? undefined : run;
@@ -8096,6 +8131,7 @@ export class FlexHarness<TScope = unknown> {
8096
8131
  onReasoningDelta: (id, delta) => this.onReasoningDelta(state, sessionId, id, delta),
8097
8132
  onReasoningEnd: (id, text) => this.onReasoningEnd(state, sessionId, id, text),
8098
8133
  onToolCallStart: (event) => this.onToolStart(state, sessionId, event),
8134
+ onToolCallUpdate: (event) => this.onToolUpdate(state, sessionId, event),
8099
8135
  onToolCallFinish: (event) => this.onToolFinish(state, sessionId, event),
8100
8136
  };
8101
8137
  stored.agentSession = await this.withCompactorContext(
@@ -8795,7 +8831,11 @@ export class FlexHarness<TScope = unknown> {
8795
8831
  const generation = requireSessionGeneration(sessionTombstone);
8796
8832
  let lastConflict: FlexHarnessStoreConflictError | undefined;
8797
8833
  for (let attempt = 0; attempt <= maxProjectManagementTombstoneConflicts; attempt++) {
8798
- const current = await store.load(storageKey, sessionId);
8834
+ const current = await store.load(
8835
+ storageKey,
8836
+ sessionId,
8837
+ Object.freeze({ ...generation }),
8838
+ );
8799
8839
  if (current !== undefined) {
8800
8840
  assertFlexProjectManagementRecord(current);
8801
8841
  if (current.sessionGenerationId === generation.sessionGenerationId) {
package/ts/interfaces.ts CHANGED
@@ -16,6 +16,9 @@ export type TFlexAgentModelMessage = ReturnType<plugins.IAgentSession['getModelM
16
16
  export type TFlexAgentToolSet = NonNullable<TFlexAgentSessionOptions['tools']>;
17
17
  export type TFlexAgentProviderOptions = TFlexAgentSessionOptions['providerOptions'];
18
18
  export type TFlexAgentCacheSetting = TFlexAgentSessionOptions['cache'];
19
+ export type TFlexAgentToolCallUpdateEvent = Parameters<
20
+ NonNullable<TFlexAgentSessionOptions['onToolCallUpdate']>
21
+ >[0];
19
22
  export type TFlexAgentToolCallFinishEvent = Parameters<
20
23
  NonNullable<TFlexAgentSessionOptions['onToolCallFinish']>
21
24
  >[0];
@@ -571,6 +574,11 @@ export interface IFlexPermissionSnapshot {
571
574
  export const FLEX_PROJECT_MANAGEMENT_SCHEMA_VERSION = 1 as const;
572
575
  export const FLEX_SESSION_GENERATION_ID_MAX_BYTES = 128;
573
576
 
577
+ export interface IFlexSessionGeneration {
578
+ sessionGenerationId: string;
579
+ sessionGenerationSequence: number;
580
+ }
581
+
574
582
  export const FLEX_PROJECT_MANAGEMENT_LIMITS = Object.freeze({
575
583
  maxGoalBytes: 8 * 1024,
576
584
  maxScratchpadBytes: 128 * 1024,
@@ -628,6 +636,7 @@ export interface IFlexProjectManagementStore {
628
636
  load(
629
637
  storageKey: string,
630
638
  sessionId: string,
639
+ sessionGeneration?: Readonly<IFlexSessionGeneration>,
631
640
  ): Promise<TFlexProjectManagementRecord | undefined>;
632
641
  save(
633
642
  storageKey: string,
package/ts/plugins.ts CHANGED
@@ -47,6 +47,7 @@ export type {
47
47
  IAgentRunResult,
48
48
  IAgentSession,
49
49
  IAgentSessionOptions,
50
+ IAgentToolCallUpdateEvent,
50
51
  IAgentRuntimeEventPayload,
51
52
  IGenerationBegunEvent,
52
53
  IGenerationExecutionCompletedEvent,