@modelprofile.com/flexharness 4.1.2 → 5.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.
@@ -160,7 +160,7 @@ export interface IFlexSlashCommandDescriptor {
160
160
  unavailableReason?: string;
161
161
  workspaceReversion: TFlexSlashCommandWorkspaceReversion;
162
162
  }
163
- export interface IFlexSlashCommandHandlerContext<TScope> {
163
+ export interface IFlexSlashCommandHandlerContext<TScope> extends IFlexSessionGeneration {
164
164
  readonly scopeId: string;
165
165
  readonly scope: TScope;
166
166
  readonly storageKey: string;
@@ -292,7 +292,7 @@ export interface IFlexPermissionRequest extends IFlexPermissionRequestInput {
292
292
  createdAt: string;
293
293
  }
294
294
  export type TFlexPermissionDecision = 'once' | 'always' | 'reject';
295
- export interface IFlexToolProviderContext<TScope> {
295
+ export interface IFlexToolProviderContext<TScope> extends IFlexSessionGeneration {
296
296
  scopeId: string;
297
297
  scope: TScope;
298
298
  sessionId: string;
@@ -309,7 +309,7 @@ export interface IFlexToolHandle {
309
309
  export interface IFlexToolProvider<TScope> {
310
310
  provideTools(context: IFlexToolProviderContext<TScope>): Promise<IFlexToolHandle | undefined> | IFlexToolHandle | undefined;
311
311
  }
312
- export interface IFlexResourceToolProviderResolverContext<TScope> {
312
+ export interface IFlexResourceToolProviderResolverContext<TScope> extends IFlexSessionGeneration {
313
313
  scopeId: string;
314
314
  scope: TScope;
315
315
  sessionId: string;
@@ -594,7 +594,7 @@ export declare const FLEX_REVERSION_MAX_AFFECTED_WORKSPACES = 64;
594
594
  export declare const FLEX_REVERSION_WORKSPACE_ID_MAX_BYTES = 512;
595
595
  export declare const FLEX_REVERSION_WORKSPACE_LABEL_MAX_BYTES = 2048;
596
596
  export declare const FLEX_REVERSION_REASON_CODE_MAX_BYTES = 128;
597
- export interface IFlexTurnReversionBaseContext<TScope> {
597
+ export interface IFlexTurnReversionBaseContext<TScope> extends IFlexSessionGeneration {
598
598
  readonly scopeId: string;
599
599
  readonly scope: TScope;
600
600
  readonly storageKey: string;
@@ -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.2",
3
+ "version": "5.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",
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.
@@ -47,7 +48,7 @@ Implementation findings for flexharness.
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
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
@@ -122,7 +122,7 @@ const harness = new FlexHarness<IProjectScope>({
122
122
  });
123
123
  ```
124
124
 
125
- `modelRegistry`, `projectRegistry`, `createProjectTools`, and `closeProjectTools` in this example are application-owned integrations. FlexHarness passes the same run `AbortSignal` to the model resolver and tool provider.
125
+ `modelRegistry`, `projectRegistry`, `createProjectTools`, and `closeProjectTools` in this example are application-owned integrations. FlexHarness passes the same run `AbortSignal` to the model resolver and tool provider. Every application and resource tool-provider context also carries the required canonical `sessionGenerationId` and `sessionGenerationSequence`, allowing host operations to authorize the exact session generation rather than a reusable session ID alone.
126
126
 
127
127
  ## Resource Tool Providers
128
128
 
@@ -130,10 +130,19 @@ const harness = new FlexHarness<IProjectScope>({
130
130
 
131
131
  ```typescript
132
132
  resourceToolProviderResolver: {
133
- async resolveResourceToolProviders({ scope, sessionId, runId, signal }) {
133
+ async resolveResourceToolProviders({
134
+ scope,
135
+ sessionId,
136
+ sessionGenerationId,
137
+ sessionGenerationSequence,
138
+ runId,
139
+ signal,
140
+ }) {
134
141
  const attachments = await resourceRegistry.listAttached({
135
142
  scope,
136
143
  sessionId,
144
+ sessionGenerationId,
145
+ sessionGenerationSequence,
137
146
  runId,
138
147
  signal,
139
148
  });
@@ -146,7 +155,7 @@ resourceToolProviderResolver: {
146
155
  },
147
156
  ```
148
157
 
149
- Each descriptor uses the existing `IFlexToolProvider<TScope>` contract. Its provider receives the normal run context and must return a fresh run-scoped handle. The original `toolProvider` remains optional and its tool names remain unchanged. Resource tool names are deterministic and bounded:
158
+ Each descriptor uses the existing `IFlexToolProvider<TScope>` contract. Its provider receives the normal run context and must return a fresh run-scoped handle. The resource resolver context carries the same required canonical session generation as the tool-provider contexts. The original `toolProvider` remains optional and its tool names remain unchanged. Resource tool names are deterministic and bounded:
150
159
 
151
160
  1. `resourceIdentity` is the lowercase hexadecimal SHA-256 of `JSON.stringify([resourceId, attachmentRevision])`.
152
161
  2. The namespace is `resource_` plus the first 16 digest characters.
@@ -438,12 +447,24 @@ const harness = new FlexHarness({
438
447
  {
439
448
  name: 'refresh-index',
440
449
  description: 'Refresh the application-owned workspace index.',
441
- async handler({ scopeId, scope, storageKey, sessionId, rawArguments, arguments, signal }) {
450
+ async handler({
451
+ scopeId,
452
+ scope,
453
+ storageKey,
454
+ sessionId,
455
+ sessionGenerationId,
456
+ sessionGenerationSequence,
457
+ rawArguments,
458
+ arguments,
459
+ signal,
460
+ }) {
442
461
  return indexer.refresh({
443
462
  scopeId,
444
463
  scope,
445
464
  storageKey,
446
465
  sessionId,
466
+ sessionGenerationId,
467
+ sessionGenerationSequence,
447
468
  rawArguments,
448
469
  arguments,
449
470
  signal,
@@ -470,7 +491,7 @@ Session metadata archival through `updateSession(..., { archived: true })` only
470
491
 
471
492
  Templates replace every `$ARGUMENTS` with untouched raw argument text. `$1` through the highest referenced positional placeholder use tokenized arguments, with the highest position receiving all remaining tokens joined by spaces. Missing positions become empty. A template with no placeholders appends non-empty raw arguments after a blank line. `/init` uses the OpenCode 1.18.15 `AGENTS.md` initialization prompt with provider-neutral active-workspace wording.
472
493
 
473
- Handler context is frozen and contains only the resolved scope identity, session identity, raw and tokenized arguments, and an `AbortSignal`. Handler results are converted with the configured `toolOutputLimits`; `void` becomes JSON `null`. Handler failures use `externalErrorProjector` with source `slashCommand`. Same-session command overlap is rejected, including reentry from a handler. Scope retirement and disposal abort and await active handlers; prompt-admission commands transfer immediately to the normal prompt lifecycle.
494
+ Handler context is frozen and contains only the resolved scope identity, session identity, required canonical `sessionGenerationId` and `sessionGenerationSequence`, raw and tokenized arguments, and an `AbortSignal`. Handler results are converted with the configured `toolOutputLimits`; `void` becomes JSON `null`. Handler failures use `externalErrorProjector` with source `slashCommand`. Same-session command overlap is rejected, including reentry from a handler. Scope retirement and disposal abort and await active handlers; prompt-admission commands transfer immediately to the normal prompt lifecycle.
474
495
 
475
496
  ### Workspace Reversion Provider
476
497
 
@@ -557,7 +578,7 @@ for (const group of info.groups) {
557
578
 
558
579
  The immutable result exposes `undoAvailable`, `redoAvailable`, and groups classified as `candidate`, `barrier`, or `no-change`. Group metadata contains at most 64 unique affected workspaces; `affectedWorkspacesTruncated` is `true` when additional unique descriptors were omitted. It never includes capture IDs or provider references.
559
580
 
560
- `workspaceSnapshots` is application-owned. Every context contains `scopeId`, `scope`, `storageKey`, `sessionId`, `runId`, deterministic `captureId`, and an `AbortSignal`. Apply contexts additionally contain the normalized `reference`, deterministic per-segment `operationId`, and `direction`; release contexts contain the reference.
581
+ `workspaceSnapshots` is application-owned. Every context contains `scopeId`, `scope`, `storageKey`, `sessionId`, the required canonical `sessionGenerationId` and `sessionGenerationSequence`, `runId`, deterministic `captureId`, and an `AbortSignal`. Apply contexts additionally contain the normalized `reference`, deterministic per-segment `operationId`, and `direction`; release contexts contain the reference. Deleted-session recovery retains the same generation from the durable tombstone, so providers can reject callbacks from an older same-ID generation.
561
582
 
562
583
  The protocol is durable and inspectable:
563
584
 
@@ -632,7 +653,7 @@ FlexHarness wraps every provided tool `execute` method before SmartAgent receive
632
653
 
633
654
  `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
655
 
635
- Streaming callbacks use run-local synchronous state rather than one persistence promise per delta. Adjacent text and reasoning deltas coalesce. 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.
656
+ 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
657
 
637
658
  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
659
 
@@ -656,7 +677,15 @@ Transactional tool calls persist an execution intent before the tool side effect
656
677
  const unsubscribe = harness.subscribe((event) => {
657
678
  switch (event.type) {
658
679
  case 'part.delta':
659
- renderDelta(event.sessionId, event.messageIndex, event.partIndex, event.delta);
680
+ applyExactDelta(
681
+ event.sessionId,
682
+ event.messageIndex,
683
+ event.partIndex,
684
+ event.partType,
685
+ event.delta,
686
+ event.baseTextUtf8Bytes,
687
+ event.textUtf8Bytes,
688
+ );
660
689
  break;
661
690
  case 'permission.requested':
662
691
  showPermission(event.request);
@@ -685,7 +714,15 @@ const unsubscribe = harness.subscribe((event) => {
685
714
  unsubscribe();
686
715
  ```
687
716
 
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.
717
+ 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.
718
+
719
+ `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.
720
+
721
+ 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.
722
+
723
+ ### Migrating Part Events to 5.x
724
+
725
+ 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
726
 
690
727
  ## Stores
691
728
 
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@modelprofile.com/flexharness',
6
- version: '4.1.2',
6
+ version: '5.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
  }