@modelprofile.com/flexharness 10.1.0 → 10.2.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/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.flexharness.d.ts +13 -1
- package/dist_ts/classes.flexharness.js +302 -188
- 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 +2 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.flexharness.ts +324 -205
- package/ts/interfaces.ts +19 -0
- package/ts/readme.md +2 -0
- package/ts_chat_harness/classes.transcript.ts +46 -12
- package/ts_chat_harness/readme.md +9 -6
package/ts/readme.md
CHANGED
|
@@ -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.
|