@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.
- package/changelog.md +18 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.flexharness.d.ts +3 -0
- package/dist_ts/classes.flexharness.js +104 -36
- package/dist_ts/interfaces.d.ts +17 -8
- package/package.json +1 -1
- package/readme.hints.md +2 -1
- package/readme.md +46 -9
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.flexharness.ts +137 -27
- package/ts/interfaces.ts +20 -8
package/dist_ts/interfaces.d.ts
CHANGED
|
@@ -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
|
|
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 |
|
|
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": "
|
|
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.
|
|
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({
|
|
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({
|
|
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.
|
|
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
|
-
|
|
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,
|
|
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
|
|
package/ts/00_commitinfo_data.ts
CHANGED
|
@@ -3,6 +3,6 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export const commitinfo = {
|
|
5
5
|
name: '@modelprofile.com/flexharness',
|
|
6
|
-
version: '
|
|
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
|
}
|