@modelprofile.com/flexharness 10.1.0 → 10.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.flexharness.d.ts +13 -1
- package/dist_ts/classes.flexharness.js +312 -189
- package/dist_ts/interfaces.d.ts +18 -0
- package/dist_ts/interfaces.js +1 -1
- package/dist_ts_chat_harness/classes.transcript.d.ts +21 -6
- package/dist_ts_chat_harness/classes.transcript.js +37 -9
- package/package.json +3 -3
- package/readme.md +4 -2
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.flexharness.ts +339 -211
- package/ts/interfaces.ts +19 -0
- package/ts/readme.md +4 -2
- package/ts_chat_harness/classes.transcript.ts +46 -12
- package/ts_chat_harness/readme.md +9 -6
package/ts/interfaces.ts
CHANGED
|
@@ -513,6 +513,25 @@ export interface IFlexMessagePage {
|
|
|
513
513
|
nextCursor?: string;
|
|
514
514
|
}
|
|
515
515
|
|
|
516
|
+
/**
|
|
517
|
+
* A session as the events up to `sequence` describe it, read in one step. Replaying the events of
|
|
518
|
+
* the same harness instance after `sequence` onto it gives the session's later states.
|
|
519
|
+
*/
|
|
520
|
+
export interface IFlexSessionView {
|
|
521
|
+
/** Current metadata. Its status can be ahead of `sequence`: status changes emit no event. */
|
|
522
|
+
session: IFlexSession;
|
|
523
|
+
/** The visible messages, including what a running turn has streamed so far. */
|
|
524
|
+
messages: IFlexMessage[];
|
|
525
|
+
/**
|
|
526
|
+
* The session's pending permission requests, oldest first. A request its run leaves unanswered,
|
|
527
|
+
* because the run was stopped or failed, leaves this list without an event of its own; the run's
|
|
528
|
+
* `run.finished` follows.
|
|
529
|
+
*/
|
|
530
|
+
pendingPermissions: IFlexPermissionRequest[];
|
|
531
|
+
/** The `sequence` of the instance's last event this view includes; 0 before the first event. */
|
|
532
|
+
sequence: number;
|
|
533
|
+
}
|
|
534
|
+
|
|
516
535
|
export interface IFlexResolvedScope<TScope> {
|
|
517
536
|
storageKey: string;
|
|
518
537
|
scope: TScope;
|
package/ts/readme.md
CHANGED
|
@@ -789,14 +789,14 @@ for (const group of info.groups) {
|
|
|
789
789
|
|
|
790
790
|
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.
|
|
791
791
|
|
|
792
|
-
`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`,
|
|
792
|
+
`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`, 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.
|
|
793
793
|
|
|
794
794
|
The protocol is durable and inspectable:
|
|
795
795
|
|
|
796
796
|
1. FlexHarness persists a `preparing` capture intent before calling `prepare()`, before model or tool execution. The provider must establish exclusive capture ownership for that `storageKey` and retain it until `release()` succeeds.
|
|
797
797
|
2. `inspectCapture()` returns `missing`, `prepared`, `finalized`, or `unknown`. A finalized V1 result includes `reference`; a finalized V2 result includes the complete tagged `outcome`. `missing` is safe only while the durable state is still `preparing`; a missing prepared or finalizing capture, an `unknown` result, or an inspection failure fences the namespace.
|
|
798
798
|
3. `finalize()` closes the capture and returns its JSON-safe V1 reference or complete V2 outcome. FlexHarness may call it during normal finalization or recovery after `inspectCapture()` reports `prepared`. Failed and cancelled root turns are captured too. A root capture spans its admitted subagent effects, including asynchronous children joined before finalization, although child transcript records remain separate. A failed parent cancels its outstanding children before joining them.
|
|
799
|
-
4. Before undo or redo, FlexHarness persists an apply write-ahead record. `inspectApply()` must report the exact durable outcome for the supplied `operationId`: `not-applied` means no effect occurred, `applied` means the complete effect occurred, and `unknown` means the provider cannot prove either result. FlexHarness calls `apply()` only for `not-applied`; after an apply error it inspects again, and an `unknown` result or inspection failure fences the namespace. Progress is persisted after each segment, and the transcript cursor moves only after the complete unit succeeds. Known zero-progress failures leave the cursor unchanged and can be retried; partial progress retains the write-ahead record and resumes after restart. Caller cancellation is honored until the first workspace segment makes progress; recovery then continues with fresh bounded maintenance signals until the unit and cursor commit.
|
|
799
|
+
4. Before undo or redo, FlexHarness persists an apply write-ahead record. `inspectApply()` must report the exact durable outcome for the supplied `operationId`: `not-applied` means no effect occurred, `applied` means the complete effect occurred, and `unknown` means the provider cannot prove either result. FlexHarness calls `apply()` only for `not-applied`; after an apply error it inspects again, and an `unknown` result or inspection failure fences the namespace. Progress is persisted after each segment, and the transcript cursor moves only after the complete unit succeeds. Known zero-progress failures leave the cursor unchanged and can be retried; partial progress retains the write-ahead record and resumes after restart. Caller cancellation is honored until the first workspace segment makes progress; recovery then continues with fresh bounded maintenance signals until the unit and cursor commit. Each `operationId` is unique to one apply of one segment: a later undo or redo gets a new ID even when it moves the same turn between the same cursors, such as undoing a turn again after a redo. Retries of that apply, in-process or after a restart, reuse its persisted ID, so providers must treat `operationId` idempotently and may record applied IDs to answer `inspectApply()`.
|
|
800
800
|
5. `release()` relinquishes the capture after no-change or nonrevertible V2 finalization, branch commitment, retention or archive pruning, session deletion, or other durable removal. It must be idempotent: an unacknowledged release remains persisted and is retried before FlexHarness discards the reference.
|
|
801
801
|
|
|
802
802
|
Under `workspace-required`, a group is a barrier when any segment is pending, nonrevertible, or legacy transcript-only history. It is a candidate when at least one segment is revertible and none is a barrier; otherwise it is no-change. Only candidates can be traversed. No-change groups after a candidate travel with that candidate until the next candidate or barrier. Leading no-change groups remain visible. A retained barrier blocks older groups, while later candidates remain undoable. A mixed revertible/nonrevertible group is a barrier.
|
|
@@ -1034,6 +1034,8 @@ unsubscribe();
|
|
|
1034
1034
|
|
|
1035
1035
|
Events are discriminated, deeply immutable values with one global sequence within each `FlexHarness` instance. Every `IFlexEventBase` carries the exact source `sessionGenerationId` and `sessionGenerationSequence`; `session.deleted` retains the deleted generation after live metadata is removed. Listener exceptions are isolated from runs and other listeners. Every accepted queue entry emits `prompt.queued` and exactly one `prompt.finished`. Every accepted steer emits `prompt.steer.accepted`, and `prompt.steer.applied`, after the `message.*` events of its user message, once the run took it in; `run.finished` lists it in `unappliedSteerIds` unless it is in the model context of later turns. 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.
|
|
1036
1036
|
|
|
1037
|
+
`getSessionView(scopeId, sessionId)` reads a session's metadata, visible messages and pending permission requests in one step, together with the instance's event `sequence` they include. The messages include what a running turn has streamed so far, so a consumer that applies every event of the instance with a greater `sequence` follows the session exactly from the view on; events up to `sequence` are already in it. The read waits while a queued write or a change that emits events for the session is in progress; a session that keeps changing through 64 consecutive waits is refused with `FlexHarnessSessionBusyError`, and the read can be repeated. Session metadata is current: status changes such as `waiting_permission` emit no event. A permission request its run leaves unanswered leaves `pendingPermissions` without an event of its own; the run's `run.finished` follows.
|
|
1038
|
+
|
|
1037
1039
|
`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.
|
|
1038
1040
|
|
|
1039
1041
|
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.
|
|
@@ -25,14 +25,23 @@ export interface IFlexHarnessTranscriptOptions {
|
|
|
25
25
|
sessionId: string;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
/**
|
|
28
|
+
/**
|
|
29
|
+
* What `load()` reads: the session's authoritative state, as the harness APIs return it. Take it
|
|
30
|
+
* from `getSessionView()`, whose `sequence` ties the state to the event stream.
|
|
31
|
+
*/
|
|
29
32
|
export interface IFlexHarnessTranscriptSnapshot {
|
|
30
|
-
/** `getMessages()` of the session. */
|
|
33
|
+
/** `messages` of `getSessionView()`, or `getMessages()` of the session. */
|
|
31
34
|
messages: readonly IFlexMessage[];
|
|
32
|
-
/** `listPendingPermissions()` of the session. */
|
|
35
|
+
/** `pendingPermissions` of `getSessionView()`, or `listPendingPermissions()` of the session. */
|
|
33
36
|
permissions?: readonly IFlexPermissionRequest[];
|
|
34
|
-
/** `getSession()`; states whether a run is active or failed. */
|
|
37
|
+
/** `session` of `getSessionView()`, or `getSession()`; states whether a run is active or failed. */
|
|
35
38
|
session?: IFlexSession;
|
|
39
|
+
/**
|
|
40
|
+
* `sequence` of `getSessionView()`: the last event of the instance the state includes. With it,
|
|
41
|
+
* the transcript skips the events the state already holds and proves that none after it was
|
|
42
|
+
* lost; without it, the first event after `load()` is taken unchecked.
|
|
43
|
+
*/
|
|
44
|
+
sequence?: number;
|
|
36
45
|
/** `getProjectState()`, when project management is enabled. */
|
|
37
46
|
project?: IFlexProjectStateResult;
|
|
38
47
|
}
|
|
@@ -120,6 +129,10 @@ export class FlexHarnessTranscript {
|
|
|
120
129
|
private readonly timings = new Map<string, IFlexPartTiming>();
|
|
121
130
|
private readonly partBytes = new Map<string, number>();
|
|
122
131
|
private readonly permissionRows = new Map<string, plugins.IHarnessPermissionRequest>();
|
|
132
|
+
/** The run of every unanswered permission row; a run that finishes leaves them unanswerable. */
|
|
133
|
+
private readonly permissionRuns = new Map<string, string>();
|
|
134
|
+
/** The last event of the instance the transcript holds, once a view tied it to the stream. */
|
|
135
|
+
private followedSequence?: number;
|
|
123
136
|
private status: plugins.IHarnessStatus = { type: 'idle' };
|
|
124
137
|
private usage: plugins.IHarnessUsage = {};
|
|
125
138
|
private todos: plugins.IHarnessTodoItem[] = [];
|
|
@@ -146,9 +159,9 @@ export class FlexHarnessTranscript {
|
|
|
146
159
|
this.partBytes.clear();
|
|
147
160
|
for (const message of snapshot.messages) this.upsertMessage(message);
|
|
148
161
|
this.permissionRows.clear();
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
162
|
+
this.permissionRuns.clear();
|
|
163
|
+
for (const request of snapshot.permissions ?? []) this.addPermission(request);
|
|
164
|
+
this.followedSequence = snapshot.sequence;
|
|
152
165
|
this.status = snapshot.session ? flexSessionToHarnessStatus(snapshot.session) : { type: 'idle' };
|
|
153
166
|
this.usage = this.flexMessages.reduce<plugins.IHarnessUsage>(
|
|
154
167
|
(total, message) => message.usage ? sumUsage(total, flexUsageToHarness(message.usage)) : total,
|
|
@@ -179,15 +192,23 @@ export class FlexHarnessTranscript {
|
|
|
179
192
|
|
|
180
193
|
/**
|
|
181
194
|
* Takes one event of the harness instance. An event that does not directly follow
|
|
182
|
-
* `delivery.precedingSequence
|
|
183
|
-
* asks for a reload.
|
|
195
|
+
* `delivery.precedingSequence`, or after a load with `sequence` the last event the transcript
|
|
196
|
+
* holds, means events were lost, possibly of this session, so the change asks for a reload. An
|
|
197
|
+
* event the loaded state already includes changes nothing.
|
|
184
198
|
*/
|
|
185
199
|
public applyEvent(
|
|
186
200
|
event: TFlexHarnessEvent,
|
|
187
201
|
delivery: IFlexHarnessEventDelivery,
|
|
188
202
|
): IFlexHarnessTranscriptChange {
|
|
189
203
|
const change = noChange();
|
|
190
|
-
if (
|
|
204
|
+
if (this.followedSequence !== undefined) {
|
|
205
|
+
if (event.sequence <= this.followedSequence) return change;
|
|
206
|
+
if (event.sequence !== this.followedSequence + 1) {
|
|
207
|
+
change.reload = true;
|
|
208
|
+
return change;
|
|
209
|
+
}
|
|
210
|
+
this.followedSequence = event.sequence;
|
|
211
|
+
} else if (
|
|
191
212
|
delivery.precedingSequence !== undefined
|
|
192
213
|
&& event.sequence !== delivery.precedingSequence + 1
|
|
193
214
|
) {
|
|
@@ -202,7 +223,7 @@ export class FlexHarnessTranscript {
|
|
|
202
223
|
this.setStatus(change, flexSessionToHarnessStatus(event.session));
|
|
203
224
|
break;
|
|
204
225
|
case 'session.deleted':
|
|
205
|
-
this.load({ messages: [] });
|
|
226
|
+
this.load({ messages: [], sequence: this.followedSequence });
|
|
206
227
|
change.messages = change.permissions = change.status = change.usage = true;
|
|
207
228
|
break;
|
|
208
229
|
case 'run.started':
|
|
@@ -230,7 +251,7 @@ export class FlexHarnessTranscript {
|
|
|
230
251
|
this.applyPartDelta(change, event);
|
|
231
252
|
break;
|
|
232
253
|
case 'permission.requested':
|
|
233
|
-
this.
|
|
254
|
+
this.addPermission(event.request);
|
|
234
255
|
change.permissions = true;
|
|
235
256
|
break;
|
|
236
257
|
case 'permission.resolved': {
|
|
@@ -238,12 +259,20 @@ export class FlexHarnessTranscript {
|
|
|
238
259
|
if (!row) break;
|
|
239
260
|
row.response = event.decision;
|
|
240
261
|
row.respondedAt = timestamp;
|
|
262
|
+
this.permissionRuns.delete(event.request.permissionId);
|
|
241
263
|
change.permissions = true;
|
|
242
264
|
break;
|
|
243
265
|
}
|
|
244
266
|
case 'run.finished':
|
|
245
267
|
this.upsertMessage(event.message);
|
|
246
268
|
change.messages = true;
|
|
269
|
+
// A request the run left unanswered can no longer be answered, and gets no event of its own.
|
|
270
|
+
for (const [permissionId, runId] of [...this.permissionRuns]) {
|
|
271
|
+
if (runId !== event.runId) continue;
|
|
272
|
+
this.permissionRows.delete(permissionId);
|
|
273
|
+
this.permissionRuns.delete(permissionId);
|
|
274
|
+
change.permissions = true;
|
|
275
|
+
}
|
|
247
276
|
if (event.message.usage) {
|
|
248
277
|
this.usage = sumUsage(this.usage, flexUsageToHarness(event.message.usage));
|
|
249
278
|
change.usage = true;
|
|
@@ -268,6 +297,11 @@ export class FlexHarnessTranscript {
|
|
|
268
297
|
return change;
|
|
269
298
|
}
|
|
270
299
|
|
|
300
|
+
private addPermission(request: IFlexPermissionRequest): void {
|
|
301
|
+
this.permissionRows.set(request.permissionId, flexPermissionToHarness(request));
|
|
302
|
+
this.permissionRuns.set(request.permissionId, request.runId);
|
|
303
|
+
}
|
|
304
|
+
|
|
271
305
|
private setStatus(change: IFlexHarnessTranscriptChange, status: plugins.IHarnessStatus): void {
|
|
272
306
|
if (status.type === this.status.type && status.message === this.status.message) return;
|
|
273
307
|
this.status = status;
|
|
@@ -35,15 +35,18 @@ function apply(event: TFlexHarnessEvent): void {
|
|
|
35
35
|
|
|
36
36
|
async function reloadSession(): Promise<void> {
|
|
37
37
|
buffered = [];
|
|
38
|
+
const view = await harness.getSessionView(scopeId, sessionId);
|
|
38
39
|
transcript.load({
|
|
39
|
-
messages:
|
|
40
|
-
permissions:
|
|
41
|
-
session:
|
|
40
|
+
messages: view.messages,
|
|
41
|
+
permissions: view.pendingPermissions,
|
|
42
|
+
session: view.session,
|
|
43
|
+
sequence: view.sequence,
|
|
42
44
|
});
|
|
43
45
|
precedingSequence = undefined;
|
|
44
46
|
Object.assign(chat, transcript.view());
|
|
45
47
|
const pending = buffered;
|
|
46
48
|
buffered = undefined;
|
|
49
|
+
// Buffered events the view already includes change nothing.
|
|
47
50
|
for (const event of pending) {
|
|
48
51
|
if (buffered) break; // a reported gap started another reload
|
|
49
52
|
apply(event);
|
|
@@ -63,14 +66,14 @@ chat.addEventListener('harness-abort', () => send(flexHarnessAbortIntent(target)
|
|
|
63
66
|
|
|
64
67
|
## Transcript
|
|
65
68
|
|
|
66
|
-
`FlexHarnessTranscript` follows one session. `load()` takes its authoritative state from `
|
|
69
|
+
`FlexHarnessTranscript` follows one session. `load()` takes its authoritative state from `getSessionView()` and, with project management, `getProjectState()`. `applyEvent()` takes every harness event, ignores those of other sessions, and returns what changed.
|
|
67
70
|
|
|
68
|
-
The host forwards every event of the harness instance, of every session, in order, and passes the `sequence` of the event it received before as `precedingSequence` (`undefined` for the first event after `load()`). Event sequences are global to the instance, so only this proves that no event of the session was lost: an event that does not directly follow its predecessor asks for a reload. After a gap or a reconnect, the host calls `load()` and starts again with `precedingSequence: undefined`.
|
|
71
|
+
The host forwards every event of the harness instance, of every session, in order, and passes the `sequence` of the event it received before as `precedingSequence` (`undefined` for the first event after `load()`). Event sequences are global to the instance, so only this proves that no event of the session was lost: an event that does not directly follow its predecessor asks for a reload. After a gap or a reconnect, the host calls `load()` and starts again with `precedingSequence: undefined`. `getSessionView()` states the `sequence` of the last event its state includes; passed to `load()`, it lets the transcript skip the events the state already holds and prove that the first event after it follows. A snapshot from `getMessages()`, `listPendingPermissions()` and `getSession()` carries no sequence, so the first event after such a `load()` is taken unchecked. `view()` returns a copy of the component inputs: `messages`, `permissions`, `questions`, `status`, `usage`, `todos` and `scratchpad`. The transcript keeps its own objects, so the deltas the components apply to theirs never apply twice.
|
|
69
72
|
|
|
70
73
|
- A user message is one row. An assistant message has one row per part, in part order: a text row, a reasoning row, or a tool row, so every streamed delta addresses exactly one row. The run's usage and error attach to its last text or reasoning row. Rows carry `order` from the message and part positions.
|
|
71
74
|
- `change.deltas` stream text, reasoning, tool updates and part ends into the rows the components already hold. When `change.messages` is true, rows were added or replaced: assign `view().messages` instead.
|
|
72
75
|
- `change.reload` means the transcript can no longer follow incrementally: events were lost, the history moved (`session.history.changed` after undo, redo or a branch), or a delta does not continue the text the transcript holds, which the delta's UTF-8 byte counters prove. Read the session again and call `load()`.
|
|
73
|
-
- Permission rows stay in the transcript once answered, showing the decision.
|
|
76
|
+
- Permission rows stay in the transcript once answered, showing the decision. A request its run leaves unanswered, because the run was stopped or failed, is removed when the run finishes.
|
|
74
77
|
- FlexHarness emits no event for project state. After a run, or after a project tool's row completes, read `getProjectState()` and pass it to `applyProjectState()` for the task list and the scratchpad.
|
|
75
78
|
- `questions` is always empty: FlexHarness has no question requests.
|
|
76
79
|
- Messages the session has not taken in yet (a queued prompt, a steer) carry no text in FlexHarness events. The host that sent them shows them as pending rows.
|