@modelprofile.com/flexharness 9.9.1 → 9.10.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 +2 -2
- package/dist_ts/classes.flexharness.d.ts +32 -1
- package/dist_ts/classes.flexharness.js +269 -20
- package/dist_ts/errors.d.ts +11 -0
- package/dist_ts/errors.js +18 -1
- package/dist_ts/interfaces.d.ts +58 -1
- package/dist_ts/interfaces.js +1 -1
- package/dist_ts/plugins.d.ts +3 -3
- package/dist_ts/plugins.js +3 -3
- package/dist_ts/utils.json.js +8 -1
- package/dist_ts/utils.transfer.js +3 -1
- package/package.json +3 -3
- package/readme.md +29 -2
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.flexharness.ts +338 -23
- package/ts/errors.ts +24 -0
- package/ts/interfaces.ts +63 -0
- package/ts/plugins.ts +7 -0
- package/ts/readme.md +29 -2
- package/ts/utils.json.ts +7 -0
- package/ts/utils.transfer.ts +1 -0
package/ts/readme.md
CHANGED
|
@@ -525,7 +525,7 @@ Attachment payloads are never copied into public audit messages or events. Publi
|
|
|
525
525
|
}
|
|
526
526
|
```
|
|
527
527
|
|
|
528
|
-
|
|
528
|
+
The original string remains only in canonical private Agent events, so a later model turn can receive the attachment again. A turn that succeeded stays in future context, and so does a turn stopped through `abort()` or `cancelPrompt()` once its model produced output: its prompt, the steers it took in and what the model produced before the stop, with every tool call that has no recorded result closed by an error result saying its effect is unknown. A turn stopped before any model output leaves no trace, so its prompt can be sent again without appearing twice; its `run.finished` event and finished queue entry carry `keptInContext: false`. Failed, resolver-failed, cleanup-failed, and persistence-failed turns, and turns a process restart interrupted, do not add anything to future context.
|
|
529
529
|
|
|
530
530
|
The main session methods are:
|
|
531
531
|
|
|
@@ -572,6 +572,7 @@ await harness.cancelScheduledPrompt(scopeId, sessionId, scheduled.scheduleKey);
|
|
|
572
572
|
await harness.getPromptQueueEntry(scopeId, sessionId, queued.queueId);
|
|
573
573
|
await harness.listPromptQueueEntries(scopeId, sessionId);
|
|
574
574
|
await harness.cancelPrompt(scopeId, sessionId, queued.queueId);
|
|
575
|
+
await harness.steerPrompt(scopeId, sessionId, admission.queueId, { steerId, prompt });
|
|
575
576
|
await harness.abort(scopeId, sessionId);
|
|
576
577
|
await harness.listPendingPermissions(scopeId, sessionId);
|
|
577
578
|
await harness.respondToPermission(scopeId, sessionId, permissionId, 'once');
|
|
@@ -616,6 +617,24 @@ The reservation save is the admission point. A save failure produces no start ev
|
|
|
616
617
|
|
|
617
618
|
`abort()` returns `true` only while cancellation is still accepted. Terminal persistence is the run's commit point; once it starts, `abort()` returns `false` and the already-fixed terminal outcome completes while the session remains busy.
|
|
618
619
|
|
|
620
|
+
### Steering a Running Turn
|
|
621
|
+
|
|
622
|
+
`steerPrompt()` hands a message to the running run of a prompt instead of queueing it behind that run:
|
|
623
|
+
|
|
624
|
+
```typescript
|
|
625
|
+
const admission = await harness.startPrompt(scopeId, sessionId, 'Plan the release.');
|
|
626
|
+
await harness.steerPrompt(scopeId, sessionId, admission.queueId, {
|
|
627
|
+
steerId: 'steer-1',
|
|
628
|
+
prompt: 'Keep the database migration out of it.',
|
|
629
|
+
});
|
|
630
|
+
const result = await admission.completion;
|
|
631
|
+
console.log(result.unappliedSteerIds); // [] once the run took the steer in
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
The run takes a steer in at its next step boundary: after the tool results of its current model step are recorded and before its next model call. A running tool call or a pending permission is never interrupted; the steer waits for the boundary that follows it. A steer taken in is a user message of the run (`steerId` set) that the model sees in its next call and every later turn. If the run had already answered, the assistant message that answered is completed and the run answers on in a new assistant message after the steer; a steer taken in before the run answered anything is placed before its still empty assistant message. `IFlexPromptResult.userMessage` stays the prompt and `assistantMessage` is the run's last assistant message. Undo and redo treat the run's messages as one turn.
|
|
635
|
+
|
|
636
|
+
`steerPrompt()` resolves with `{ steerId, queueId, runId }` once the run accepted the steer and `prompt.steer.accepted` was emitted; `prompt.steer.applied` carries the `messageId` of the user message the steer became. Every accepted steer ends in exactly one of two ways: it is in the model context of later turns, or it is listed in `unappliedSteerIds`, in arrival order, on `IFlexPromptResult` and `run.finished`, and on the finished queue entry when that list is not empty. A steer is listed when the run did not take it in before it ended, because its last model step was already running or because it was stopped or failed, and also when the run itself does not stay in context. `keptInContext` on `run.finished` and on the finished queue entry says whether it does: it is `false` for a failed run and for a run stopped before any model output, and such a run lists every steer it accepted, including the ones it took in and reported with `prompt.steer.applied`. Send its prompt and its unapplied steers again to have them answered. The run stops taking steers atomically when its generation ends or `abort()`/`cancelPrompt()` stops it. A steer for a prompt without a running run, or whose run is ending, is refused with `FlexHarnessSteerRejectedError` reason `not-running`; the caller sends it as the next turn instead. Reusing a `steerId` within a run is refused with reason `duplicate`, an unknown `queueId` throws `FlexHarnessNotFoundError`, and subagent sessions are not steerable. A run holds at most `maxOutstandingPromptsPerSession` pending steers, and their bytes count against the session's outstanding prompt byte limit until the run takes them in or ends, so `startPrompt()` and `enqueuePrompt()` are refused with `FlexHarnessQueueFullError` while pending steers fill it. They are process-local like queue entries: a process restart ends the run and drops its pending steers, while steers it took in are durable parts of the turn.
|
|
637
|
+
|
|
619
638
|
## Slash Commands
|
|
620
639
|
|
|
621
640
|
`parseSlashCommand()` is the public strict pure parser. It accepts at most 768 KiB, returns `not-command` for input not starting with `/`, `malformed` for invalid slash syntax, and `parsed` with the exact input, lowercase command name, separator-stripped raw argument text, and OpenCode-compatible quoted tokenization. Command names match `[a-z][a-z0-9-]{0,63}`. Single and double quotes group tokens and are stripped; escapes are not interpreted.
|
|
@@ -983,6 +1002,14 @@ const unsubscribe = harness.subscribe((event) => {
|
|
|
983
1002
|
break;
|
|
984
1003
|
case 'run.finished':
|
|
985
1004
|
markRunFinished(event.runId, event.status);
|
|
1005
|
+
// A run that did not stay in context needs its prompt sent again, followed by its unapplied steers.
|
|
1006
|
+
resendAsNextTurn(event.keptInContext ? [] : [event.runId], event.unappliedSteerIds);
|
|
1007
|
+
break;
|
|
1008
|
+
case 'prompt.steer.accepted':
|
|
1009
|
+
markSteerPending(event.steerId);
|
|
1010
|
+
break;
|
|
1011
|
+
case 'prompt.steer.applied':
|
|
1012
|
+
markSteerApplied(event.steerId, event.messageId);
|
|
986
1013
|
break;
|
|
987
1014
|
case 'session.history.changed':
|
|
988
1015
|
refreshSelectedSession(event.sessionId);
|
|
@@ -999,7 +1026,7 @@ const unsubscribe = harness.subscribe((event) => {
|
|
|
999
1026
|
unsubscribe();
|
|
1000
1027
|
```
|
|
1001
1028
|
|
|
1002
|
-
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`. 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.
|
|
1029
|
+
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.
|
|
1003
1030
|
|
|
1004
1031
|
`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.
|
|
1005
1032
|
|
package/ts/utils.json.ts
CHANGED
|
@@ -720,6 +720,7 @@ function validateMessage(value: unknown, path: string): IValidatedMessageIdentit
|
|
|
720
720
|
'usage',
|
|
721
721
|
'error',
|
|
722
722
|
'limit',
|
|
723
|
+
'steerId',
|
|
723
724
|
],
|
|
724
725
|
path,
|
|
725
726
|
);
|
|
@@ -736,6 +737,12 @@ function validateMessage(value: unknown, path: string): IValidatedMessageIdentit
|
|
|
736
737
|
requireOptionalString(message.completedAt, `${path}.completedAt`);
|
|
737
738
|
requireOptionalString(message.error, `${path}.error`);
|
|
738
739
|
requireOptionalModelLimit(message.limit, `${path}.limit`);
|
|
740
|
+
if (message.steerId !== undefined) {
|
|
741
|
+
requireString(message.steerId, `${path}.steerId`);
|
|
742
|
+
if (message.role !== 'user') {
|
|
743
|
+
throw new FlexHarnessStoreFormatError(`${path}.steerId is only valid on a user message.`);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
739
746
|
if (message.model !== undefined) validateModel(message.model, `${path}.model`);
|
|
740
747
|
if (message.usage !== undefined) validateUsage(message.usage, `${path}.usage`);
|
|
741
748
|
if (!Array.isArray(message.parts)) {
|
package/ts/utils.transfer.ts
CHANGED
|
@@ -172,6 +172,7 @@ export function createBoundedTransferMessage(message: IFlexMessage): IFlexMessag
|
|
|
172
172
|
requireTransferIdentifier(projected.messageId, 'messageId');
|
|
173
173
|
requireTransferIdentifier(projected.sessionId, 'sessionId');
|
|
174
174
|
requireTransferIdentifier(projected.runId, 'runId');
|
|
175
|
+
if (projected.steerId !== undefined) requireTransferIdentifier(projected.steerId, 'steerId');
|
|
175
176
|
projected.createdAt = truncateUtf8(projected.createdAt, 128);
|
|
176
177
|
if (projected.completedAt !== undefined) projected.completedAt = truncateUtf8(projected.completedAt, 128);
|
|
177
178
|
if (projected.error !== undefined) projected.error = truncateUtf8(projected.error, maxTransferMetadataBytes);
|