@modelprofile.com/flexharness 4.1.1 → 5.0.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.
@@ -817,8 +817,7 @@ export interface IFlexMessageChangedEvent extends IFlexEventBase {
817
817
  readonly messageId: string;
818
818
  readonly message: IFlexMessage;
819
819
  }
820
- export interface IFlexPartChangedEvent extends IFlexEventBase {
821
- readonly type: 'part.started' | 'part.delta' | 'part.updated' | 'part.completed';
820
+ export interface IFlexPartEventBase extends IFlexEventBase {
822
821
  readonly runId: string;
823
822
  readonly messageId: string;
824
823
  /** Zero-based position in the session's authoritative message sequence. */
@@ -826,9 +825,19 @@ export interface IFlexPartChangedEvent extends IFlexEventBase {
826
825
  readonly partId: string;
827
826
  /** Zero-based position in the message's authoritative part sequence. */
828
827
  readonly partIndex: number;
828
+ }
829
+ export interface IFlexPartSnapshotEvent extends IFlexPartEventBase {
830
+ readonly type: 'part.started' | 'part.updated' | 'part.completed';
829
831
  readonly part: TFlexMessagePart;
830
- readonly delta?: string;
831
832
  }
833
+ export interface IFlexPartDeltaEvent extends IFlexPartEventBase {
834
+ readonly type: 'part.delta';
835
+ readonly partType: 'text' | 'reasoning';
836
+ readonly delta: string;
837
+ readonly baseTextUtf8Bytes: number;
838
+ readonly textUtf8Bytes: number;
839
+ }
840
+ export type TFlexPartEvent = IFlexPartSnapshotEvent | IFlexPartDeltaEvent;
832
841
  export interface IFlexPermissionRequestedEvent extends IFlexEventBase {
833
842
  readonly type: 'permission.requested';
834
843
  readonly runId: string;
@@ -864,6 +873,6 @@ export interface IFlexErrorInfo {
864
873
  message: string;
865
874
  code?: string;
866
875
  }
867
- export type TFlexHarnessEvent = IFlexSessionCreatedEvent | IFlexSessionUpdatedEvent | IFlexSessionDeletedEvent | IFlexRunStartedEvent | IFlexMessageChangedEvent | IFlexPartChangedEvent | IFlexPermissionRequestedEvent | IFlexPermissionResolvedEvent | IFlexRunFinishedEvent | IFlexPromptQueueEvent | IFlexSessionHistoryChangedEvent;
876
+ export type TFlexHarnessEvent = IFlexSessionCreatedEvent | IFlexSessionUpdatedEvent | IFlexSessionDeletedEvent | IFlexRunStartedEvent | IFlexMessageChangedEvent | TFlexPartEvent | IFlexPermissionRequestedEvent | IFlexPermissionResolvedEvent | IFlexRunFinishedEvent | IFlexPromptQueueEvent | IFlexSessionHistoryChangedEvent;
868
877
  export type TFlexHarnessEventListener = (event: TFlexHarnessEvent) => void;
869
878
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@modelprofile.com/flexharness",
3
- "version": "4.1.1",
3
+ "version": "5.0.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": "^5.0.0",
25
+ "@push.rocks/smartagent": "^5.0.1",
26
26
  "@types/json-schema": "7.0.15"
27
27
  },
28
28
  "devDependencies": {
package/readme.hints.md CHANGED
@@ -12,6 +12,7 @@ Implementation findings for flexharness.
12
12
  ## SmartAgent boundary
13
13
 
14
14
  - SmartAgent is the canonical private conversation and execution runtime. FlexHarness creates one transactional `AgentSession` per public session and derives its public model, prompt, provider-option, tool-set, and result aliases from SmartAgent exports.
15
+ - Non-empty SmartAgent text and reasoning callback deltas cross the public event boundary immediately and exactly once. Delta events never carry a cumulative part and are not subject to snapshot text truncation.
15
16
  - Public prompts remain JSON-safe. URL strings are converted to `URL` instances only at the private SmartAgent invocation boundary.
16
17
  - Prompt attachment payloads exist only in canonical private Agent events. Public messages, prompt results, projection snapshots, and events expose only `attachmentType`, source kind, optional media/name, and decoded size when determinable.
17
18
  - Resolver calls start in promise continuations so synchronous throws are observed. The first failure aborts the shared internal signal without awaiting an ignoring sibling; detached tool-provider settlement is observed and late handles are closed.
@@ -45,9 +46,9 @@ Implementation findings for flexharness.
45
46
  ## Tool output boundary
46
47
 
47
48
  - 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.
48
- - 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.
49
+ - Every distinct SmartAgent streamed tool output mutates only the run-local running tool part and immediately emits a cumulative `part.updated` snapshot, including the final yielded value. Terminal success replaces it with the authoritative final output; failure, cancellation, and callback overflow remove transient output before persistence.
49
50
  - 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.
50
- - 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.
51
+ - Model callbacks use bounded synchronous run-local parts. Source text/reasoning deltas accumulate there without per-delta persistence, while public `part.delta` events retain each exact source delta. Per-part UTF-8 counters are updated incrementally, including correction when a source boundary splits a surrogate pair; cumulative text is never rescanned per token. Callback event, byte, or part overflow aborts internally and is classified as failure, not owner cancellation.
51
52
 
52
53
  ## Prompt queue boundary
53
54
 
package/readme.md CHANGED
@@ -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. 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.
635
+ Streaming callbacks use run-local synchronous state rather than one persistence promise per source delta. Text and reasoning accumulate only in the run-local terminal projection while each source delta remains an immediate exact public event. Every distinct async-iterable tool output appears immediately as a bounded cumulative `part.updated` snapshot while the tool remains `running`, including the final yielded value before completion. Only the authoritative `part.completed` output enters the terminal projection, and failed or interrupted tools discard their transient 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
 
@@ -656,7 +656,15 @@ Transactional tool calls persist an execution intent before the tool side effect
656
656
  const unsubscribe = harness.subscribe((event) => {
657
657
  switch (event.type) {
658
658
  case 'part.delta':
659
- renderDelta(event.sessionId, event.messageIndex, event.partIndex, event.delta);
659
+ applyExactDelta(
660
+ event.sessionId,
661
+ event.messageIndex,
662
+ event.partIndex,
663
+ event.partType,
664
+ event.delta,
665
+ event.baseTextUtf8Bytes,
666
+ event.textUtf8Bytes,
667
+ );
660
668
  break;
661
669
  case 'permission.requested':
662
670
  showPermission(event.request);
@@ -685,7 +693,15 @@ const unsubscribe = harness.subscribe((event) => {
685
693
  unsubscribe();
686
694
  ```
687
695
 
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.
696
+ Events are discriminated, deeply immutable values with one global sequence within each `FlexHarness` instance. 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.
697
+
698
+ `part.started`, `part.updated`, and `part.completed` are snapshot events with a complete immutable `part`. 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.
699
+
700
+ 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.
701
+
702
+ ### Migrating Part Events to 5.x
703
+
704
+ Version `5.x` replaces cumulative `part.delta` payloads with the exact delta-only contract above. Consumers must stop reading `event.part` from `part.delta`; use `event.partType`, `event.delta`, `event.baseTextUtf8Bytes`, and `event.textUtf8Bytes`, then hydrate or settle from the complete `part` carried by snapshot events. `IFlexPartChangedEvent` has been removed; use `TFlexPartEvent`, `IFlexPartSnapshotEvent`, or `IFlexPartDeltaEvent` according to the required narrowing. New streamed text and reasoning parts start with empty text, so consumers must apply subsequent deltas in sequence within that harness instance.
689
705
 
690
706
  ## Stores
691
707
 
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@modelprofile.com/flexharness',
6
- version: '4.1.1',
6
+ version: '5.0.0',
7
7
  description: 'Provider-neutral model-session runtime with durable history, permissions, typed events, and pluggable local or remote tool execution.'
8
8
  }
@@ -42,7 +42,7 @@ import type {
42
42
  IFlexMessagePage,
43
43
  IFlexMessagePageOptions,
44
44
  IFlexModelIdentity,
45
- IFlexPartChangedEvent,
45
+ IFlexPartSnapshotEvent,
46
46
  IFlexPermissionRequest,
47
47
  IFlexPermissionRequestInput,
48
48
  IFlexPermissionSnapshot,
@@ -321,6 +321,7 @@ interface IActiveRun {
321
321
  callbackEventCount: number;
322
322
  callbackOutputBytes: number;
323
323
  callbackParts: TFlexMessagePart[];
324
+ partTextUtf8Bytes: Map<string, number>;
324
325
  callbacksClosed: boolean;
325
326
  reasoningPartIds: Map<string, string>;
326
327
  toolPartIds: Map<string, string>;
@@ -3110,6 +3111,7 @@ export class FlexHarness<TScope = unknown> {
3110
3111
  callbackEventCount: 0,
3111
3112
  callbackOutputBytes: 0,
3112
3113
  callbackParts: [],
3114
+ partTextUtf8Bytes: new Map(),
3113
3115
  callbacksClosed: false,
3114
3116
  reasoningPartIds: new Map(),
3115
3117
  toolPartIds: new Map(),
@@ -6062,14 +6064,25 @@ export class FlexHarness<TScope = unknown> {
6062
6064
  if (!run || !delta) return;
6063
6065
  const lastPart = run.callbackParts.at(-1);
6064
6066
  const created = lastPart?.type !== 'text';
6065
- if (!this.reserveCallbackCapacity(run, created ? 2 : 1, Buffer.byteLength(delta), created ? 1 : 0)) return;
6066
6067
  const part = lastPart?.type === 'text'
6067
6068
  ? lastPart
6068
6069
  : { partId: plugins.crypto.randomUUID(), type: 'text' as const, text: '' };
6069
- if (created) run.callbackParts.push(part);
6070
+ const accounting = this.textAppendAccounting(run, part, delta, created);
6071
+ if (!accounting) return;
6072
+ if (!this.reserveCallbackCapacity(
6073
+ run,
6074
+ created ? 2 : 1,
6075
+ accounting.textUtf8Bytes - accounting.baseTextUtf8Bytes,
6076
+ created ? 1 : 0,
6077
+ )) return;
6078
+ if (created) {
6079
+ run.callbackParts.push(part);
6080
+ run.partTextUtf8Bytes.set(part.partId, 0);
6081
+ this.emitPartEvent(run, 'part.started', part);
6082
+ }
6070
6083
  part.text += delta;
6071
- if (created) this.emitPartEvent(run, 'part.started', part);
6072
- this.emitPartEvent(run, 'part.delta', part, delta);
6084
+ run.partTextUtf8Bytes.set(part.partId, accounting.textUtf8Bytes);
6085
+ this.emitPartDeltaEvent(run, part, delta, accounting);
6073
6086
  }
6074
6087
 
6075
6088
  private onReasoningStart(state: IStorageState, sessionId: string, id: string): void {
@@ -6084,6 +6097,7 @@ export class FlexHarness<TScope = unknown> {
6084
6097
  };
6085
6098
  run.reasoningPartIds.set(id, part.partId);
6086
6099
  run.callbackParts.push(part);
6100
+ run.partTextUtf8Bytes.set(part.partId, 0);
6087
6101
  this.emitPartEvent(run, 'part.started', part);
6088
6102
  }
6089
6103
 
@@ -6094,9 +6108,17 @@ export class FlexHarness<TScope = unknown> {
6094
6108
  const partId = run.reasoningPartIds.get(id);
6095
6109
  const part = run.callbackParts.find((entry) => entry.partId === partId && entry.type === 'reasoning');
6096
6110
  if (!part || part.type !== 'reasoning' || part.status !== 'running') return;
6097
- if (!this.reserveCallbackCapacity(run, 1, Buffer.byteLength(delta), 0)) return;
6111
+ const accounting = this.textAppendAccounting(run, part, delta);
6112
+ if (!accounting) return;
6113
+ if (!this.reserveCallbackCapacity(
6114
+ run,
6115
+ 1,
6116
+ accounting.textUtf8Bytes - accounting.baseTextUtf8Bytes,
6117
+ 0,
6118
+ )) return;
6098
6119
  part.text += delta;
6099
- this.emitPartEvent(run, 'part.delta', part, delta);
6120
+ run.partTextUtf8Bytes.set(part.partId, accounting.textUtf8Bytes);
6121
+ this.emitPartDeltaEvent(run, part, delta, accounting);
6100
6122
  }
6101
6123
 
6102
6124
  private onReasoningEnd(state: IStorageState, sessionId: string, id: string, text: string): void {
@@ -6107,12 +6129,44 @@ export class FlexHarness<TScope = unknown> {
6107
6129
  const part = run.callbackParts.find((entry) => entry.partId === partId && entry.type === 'reasoning');
6108
6130
  if (!part || part.type !== 'reasoning' || part.status !== 'running') return;
6109
6131
  const missingText = part.text ? '' : text;
6110
- if (!this.reserveCallbackCapacity(run, 1, Buffer.byteLength(missingText), 0)) return;
6111
- if (missingText) part.text = missingText;
6132
+ const missingTextUtf8Bytes = Buffer.byteLength(missingText, 'utf8');
6133
+ if (!this.reserveCallbackCapacity(run, 1, missingTextUtf8Bytes, 0)) return;
6134
+ if (missingText) {
6135
+ part.text = missingText;
6136
+ run.partTextUtf8Bytes.set(part.partId, missingTextUtf8Bytes);
6137
+ }
6112
6138
  part.status = 'completed';
6113
6139
  this.emitPartEvent(run, 'part.completed', part);
6114
6140
  }
6115
6141
 
6142
+ private textAppendAccounting(
6143
+ run: IActiveRun,
6144
+ part: Extract<TFlexMessagePart, { type: 'text' | 'reasoning' }>,
6145
+ delta: string,
6146
+ created = false,
6147
+ ): { baseTextUtf8Bytes: number; textUtf8Bytes: number } | undefined {
6148
+ const baseTextUtf8Bytes = created ? 0 : run.partTextUtf8Bytes.get(part.partId);
6149
+ if (baseTextUtf8Bytes === undefined) {
6150
+ throw new Error('Flex part text byte accounting is unavailable.');
6151
+ }
6152
+ const trailingCodeUnit = part.text.charCodeAt(part.text.length - 1);
6153
+ const leadingCodeUnit = delta.charCodeAt(0);
6154
+ const joinsSurrogatePair = trailingCodeUnit >= 0xD800
6155
+ && trailingCodeUnit <= 0xDBFF
6156
+ && leadingCodeUnit >= 0xDC00
6157
+ && leadingCodeUnit <= 0xDFFF;
6158
+ const textUtf8Bytes = baseTextUtf8Bytes
6159
+ + Buffer.byteLength(delta, 'utf8')
6160
+ - (joinsSurrogatePair ? 2 : 0);
6161
+ if (!Number.isSafeInteger(textUtf8Bytes)) {
6162
+ this.failCallback(run, this.trustInternalError(new FlexHarnessCallbackOverflowError(
6163
+ 'Callback text byte accounting exceeded the safe integer range.',
6164
+ )));
6165
+ return undefined;
6166
+ }
6167
+ return { baseTextUtf8Bytes, textUtf8Bytes };
6168
+ }
6169
+
6116
6170
  private onToolStart(
6117
6171
  state: IStorageState,
6118
6172
  sessionId: string,
@@ -6211,7 +6265,10 @@ export class FlexHarness<TScope = unknown> {
6211
6265
  const nextOutputBytes = run.callbackOutputBytes + outputBytes;
6212
6266
  const nextParts = run.callbackParts.length + partCount;
6213
6267
  if (
6214
- nextEvents > this.callbackLimits.maxEvents
6268
+ !Number.isSafeInteger(nextEvents)
6269
+ || !Number.isSafeInteger(nextOutputBytes)
6270
+ || !Number.isSafeInteger(nextParts)
6271
+ || nextEvents > this.callbackLimits.maxEvents
6215
6272
  || nextOutputBytes > this.callbackLimits.maxOutputBytes
6216
6273
  || nextParts > this.callbackLimits.maxParts
6217
6274
  ) {
@@ -9329,20 +9386,10 @@ export class FlexHarness<TScope = unknown> {
9329
9386
 
9330
9387
  private emitPartEvent(
9331
9388
  run: IActiveRun,
9332
- type: IFlexPartChangedEvent['type'],
9389
+ type: IFlexPartSnapshotEvent['type'],
9333
9390
  part: TFlexMessagePart,
9334
- delta?: string,
9335
9391
  ): void {
9336
- const messageIndex = run.stored.messages.findIndex(
9337
- (message) => message.messageId === run.assistantMessageId,
9338
- );
9339
- const partIndex = run.callbackParts.findIndex((entry) => entry.partId === part.partId);
9340
- if (
9341
- messageIndex < 0
9342
- || run.stored.messages[messageIndex]?.messageId !== run.assistantMessageId
9343
- || partIndex < 0
9344
- || run.callbackParts[partIndex]?.partId !== part.partId
9345
- ) throw new Error('Flex part event source coordinates are unavailable.');
9392
+ const { messageIndex, partIndex } = this.partEventCoordinates(run, part.partId);
9346
9393
  this.emitEvent(run.scopeId, run.sessionId, {
9347
9394
  type,
9348
9395
  runId: run.runId,
@@ -9351,10 +9398,46 @@ export class FlexHarness<TScope = unknown> {
9351
9398
  partId: part.partId,
9352
9399
  partIndex,
9353
9400
  part: publicSnapshot(part),
9354
- ...(delta === undefined ? {} : { delta: truncateUtf8(delta, maxTransferTextBytes) }),
9355
9401
  });
9356
9402
  }
9357
9403
 
9404
+ private emitPartDeltaEvent(
9405
+ run: IActiveRun,
9406
+ part: Extract<TFlexMessagePart, { type: 'text' | 'reasoning' }>,
9407
+ delta: string,
9408
+ accounting: { baseTextUtf8Bytes: number; textUtf8Bytes: number },
9409
+ ): void {
9410
+ const { messageIndex, partIndex } = this.partEventCoordinates(run, part.partId);
9411
+ this.emitEvent(run.scopeId, run.sessionId, {
9412
+ type: 'part.delta',
9413
+ runId: run.runId,
9414
+ messageId: run.assistantMessageId,
9415
+ messageIndex,
9416
+ partId: part.partId,
9417
+ partIndex,
9418
+ partType: part.type,
9419
+ delta,
9420
+ ...accounting,
9421
+ });
9422
+ }
9423
+
9424
+ private partEventCoordinates(
9425
+ run: IActiveRun,
9426
+ partId: string,
9427
+ ): { messageIndex: number; partIndex: number } {
9428
+ const messageIndex = run.stored.messages.findIndex(
9429
+ (message) => message.messageId === run.assistantMessageId,
9430
+ );
9431
+ const partIndex = run.callbackParts.findIndex((entry) => entry.partId === partId);
9432
+ if (
9433
+ messageIndex < 0
9434
+ || run.stored.messages[messageIndex]?.messageId !== run.assistantMessageId
9435
+ || partIndex < 0
9436
+ || run.callbackParts[partIndex]?.partId !== partId
9437
+ ) throw new Error('Flex part event source coordinates are unavailable.');
9438
+ return { messageIndex, partIndex };
9439
+ }
9440
+
9358
9441
  private emitEvent(scopeId: string, sessionId: string, details: TEventDetails): void {
9359
9442
  const event = deepFreeze(cloneSerializable({
9360
9443
  eventId: plugins.crypto.randomUUID(),
package/ts/interfaces.ts CHANGED
@@ -1107,8 +1107,7 @@ export interface IFlexMessageChangedEvent extends IFlexEventBase {
1107
1107
  readonly message: IFlexMessage;
1108
1108
  }
1109
1109
 
1110
- export interface IFlexPartChangedEvent extends IFlexEventBase {
1111
- readonly type: 'part.started' | 'part.delta' | 'part.updated' | 'part.completed';
1110
+ export interface IFlexPartEventBase extends IFlexEventBase {
1112
1111
  readonly runId: string;
1113
1112
  readonly messageId: string;
1114
1113
  /** Zero-based position in the session's authoritative message sequence. */
@@ -1116,10 +1115,23 @@ export interface IFlexPartChangedEvent extends IFlexEventBase {
1116
1115
  readonly partId: string;
1117
1116
  /** Zero-based position in the message's authoritative part sequence. */
1118
1117
  readonly partIndex: number;
1118
+ }
1119
+
1120
+ export interface IFlexPartSnapshotEvent extends IFlexPartEventBase {
1121
+ readonly type: 'part.started' | 'part.updated' | 'part.completed';
1119
1122
  readonly part: TFlexMessagePart;
1120
- readonly delta?: string;
1121
1123
  }
1122
1124
 
1125
+ export interface IFlexPartDeltaEvent extends IFlexPartEventBase {
1126
+ readonly type: 'part.delta';
1127
+ readonly partType: 'text' | 'reasoning';
1128
+ readonly delta: string;
1129
+ readonly baseTextUtf8Bytes: number;
1130
+ readonly textUtf8Bytes: number;
1131
+ }
1132
+
1133
+ export type TFlexPartEvent = IFlexPartSnapshotEvent | IFlexPartDeltaEvent;
1134
+
1123
1135
  export interface IFlexPermissionRequestedEvent extends IFlexEventBase {
1124
1136
  readonly type: 'permission.requested';
1125
1137
  readonly runId: string;
@@ -1167,7 +1179,7 @@ export type TFlexHarnessEvent =
1167
1179
  | IFlexSessionDeletedEvent
1168
1180
  | IFlexRunStartedEvent
1169
1181
  | IFlexMessageChangedEvent
1170
- | IFlexPartChangedEvent
1182
+ | TFlexPartEvent
1171
1183
  | IFlexPermissionRequestedEvent
1172
1184
  | IFlexPermissionResolvedEvent
1173
1185
  | IFlexRunFinishedEvent