@modelprofile.com/flexharness 9.9.1 → 10.0.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 +34 -1
- package/dist_ts/classes.flexharness.js +369 -26
- package/dist_ts/errors.d.ts +11 -0
- package/dist_ts/errors.js +18 -1
- package/dist_ts/interfaces.d.ts +59 -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.harnessoptions.js +2 -1
- package/dist_ts/utils.json.js +8 -1
- package/dist_ts/utils.transfer.js +3 -1
- package/dist_ts_kvm/classes.browserkvm.js +3 -3
- package/dist_ts_kvm/plugins.d.ts +1 -1
- package/dist_ts_kvm/plugins.js +2 -2
- package/package.json +6 -6
- package/readme.md +39 -8
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.flexharness.ts +440 -30
- package/ts/errors.ts +24 -0
- package/ts/interfaces.ts +64 -0
- package/ts/plugins.ts +7 -0
- package/ts/readme.md +39 -8
- package/ts/utils.harnessoptions.ts +1 -0
- package/ts/utils.json.ts +7 -0
- package/ts/utils.transfer.ts +1 -0
- package/ts_kvm/classes.browserkvm.ts +2 -2
- package/ts_kvm/plugins.ts +1 -1
package/ts/errors.ts
CHANGED
|
@@ -44,6 +44,30 @@ export class FlexHarnessSessionBusyError extends FlexHarnessError {
|
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
export type TFlexSteerRejection = 'not-running' | 'duplicate';
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The run refused a steer. `not-running`: the prompt has no run that takes input, because it
|
|
51
|
+
* has not started or its run is ending; the steer belongs to the next turn. `duplicate`: the run
|
|
52
|
+
* already accepted a steer with this id.
|
|
53
|
+
*/
|
|
54
|
+
export class FlexHarnessSteerRejectedError extends FlexHarnessError {
|
|
55
|
+
public readonly reason: TFlexSteerRejection;
|
|
56
|
+
public readonly steerId: string;
|
|
57
|
+
|
|
58
|
+
constructor(reason: TFlexSteerRejection, steerId: string) {
|
|
59
|
+
super(
|
|
60
|
+
reason === 'duplicate' ? 'FLEX_STEER_DUPLICATE' : 'FLEX_STEER_NOT_RUNNING',
|
|
61
|
+
reason === 'duplicate'
|
|
62
|
+
? `Steer "${steerId}" was already accepted.`
|
|
63
|
+
: `Steer "${steerId}" has no running turn to join.`,
|
|
64
|
+
);
|
|
65
|
+
this.name = 'FlexHarnessSteerRejectedError';
|
|
66
|
+
this.reason = reason;
|
|
67
|
+
this.steerId = steerId;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
47
71
|
/** An operation would change the history or model context of an imported, read-only session. */
|
|
48
72
|
export class FlexHarnessImportedSessionError extends FlexHarnessError {
|
|
49
73
|
public readonly sessionId: string;
|
package/ts/interfaces.ts
CHANGED
|
@@ -141,6 +141,8 @@ export interface IFlexMessage {
|
|
|
141
141
|
error?: string;
|
|
142
142
|
/** The model limit that failed this run, next to its error. */
|
|
143
143
|
limit?: TFlexModelLimit;
|
|
144
|
+
/** On a user message that steered its running run: the caller's steer identity. */
|
|
145
|
+
steerId?: string;
|
|
144
146
|
}
|
|
145
147
|
|
|
146
148
|
export interface IFlexSessionActivity {
|
|
@@ -436,17 +438,46 @@ export interface IFlexPromptQueueEntry {
|
|
|
436
438
|
startedAt?: string;
|
|
437
439
|
finishedAt?: string;
|
|
438
440
|
error?: IFlexErrorInfo;
|
|
441
|
+
/**
|
|
442
|
+
* On a finished entry: whether its prompt, the steers its run took in and what its model
|
|
443
|
+
* produced stay in the model context of later turns. True for a completed run and a run
|
|
444
|
+
* stopped after its model produced output; false for a failed run, a run stopped before any
|
|
445
|
+
* model output and a prompt that never ran. Send the prompt again to have it answered.
|
|
446
|
+
*/
|
|
447
|
+
keptInContext?: boolean;
|
|
448
|
+
/**
|
|
449
|
+
* On a finished entry whose run left steers unapplied: the steers it accepted that the model
|
|
450
|
+
* context of later turns does not hold, in the order they arrived. The caller decides what
|
|
451
|
+
* becomes of them.
|
|
452
|
+
*/
|
|
453
|
+
unappliedSteerIds?: string[];
|
|
439
454
|
}
|
|
440
455
|
|
|
441
456
|
export interface IFlexPromptResult {
|
|
442
457
|
runId: string;
|
|
443
458
|
sessionId: string;
|
|
444
459
|
userMessage: IFlexMessage;
|
|
460
|
+
/** The run's last assistant message; a steered run completed earlier ones as it took steers in. */
|
|
445
461
|
assistantMessage: IFlexMessage;
|
|
446
462
|
model: IFlexModelIdentity;
|
|
447
463
|
usage: IFlexUsage;
|
|
448
464
|
finishReason: string;
|
|
449
465
|
steps: number;
|
|
466
|
+
/** Steers the run accepted but never took in, in the order they arrived. */
|
|
467
|
+
unappliedSteerIds: string[];
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/** A message for the running run of a session, taken in at the run's next step boundary. */
|
|
471
|
+
export interface IFlexSteerInput {
|
|
472
|
+
/** Caller-chosen identity, unique among the steers of the run. */
|
|
473
|
+
steerId: string;
|
|
474
|
+
prompt: TFlexPrompt;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
export interface IFlexSteerAdmission {
|
|
478
|
+
steerId: string;
|
|
479
|
+
queueId: string;
|
|
480
|
+
runId: string;
|
|
450
481
|
}
|
|
451
482
|
|
|
452
483
|
export interface IFlexPromptQueueAdmission {
|
|
@@ -1276,6 +1307,7 @@ export type TFlexAgentContextCompactor<TScope = unknown> = (
|
|
|
1276
1307
|
export interface IFlexAgentSessionPolicy<TScope = unknown> {
|
|
1277
1308
|
contextBuilder?: plugins.TAgentContextBuilder;
|
|
1278
1309
|
contextCompactor?: TFlexAgentContextCompactor<TScope>;
|
|
1310
|
+
contextCompactionBytes?: plugins.IAgentSessionOptions['contextCompactionBytes'];
|
|
1279
1311
|
eventRetention?: TFlexAgentSessionOptions['eventRetention'];
|
|
1280
1312
|
changeListenerTimeoutMs?: number;
|
|
1281
1313
|
maxPendingSessionChanges?: number;
|
|
@@ -1455,6 +1487,36 @@ export interface IFlexRunFinishedEvent extends IFlexEventBase {
|
|
|
1455
1487
|
readonly status: 'completed' | 'failed' | 'cancelled';
|
|
1456
1488
|
readonly message: IFlexMessage;
|
|
1457
1489
|
readonly error?: IFlexErrorInfo;
|
|
1490
|
+
/**
|
|
1491
|
+
* Whether the run's prompt, the steers it took in and what its model produced stay in the
|
|
1492
|
+
* model context of later turns. True for a completed run and a run stopped after its model
|
|
1493
|
+
* produced output; false for a failed run and a run stopped before any model output, whose
|
|
1494
|
+
* prompt the caller sends again to have it answered.
|
|
1495
|
+
*/
|
|
1496
|
+
readonly keptInContext: boolean;
|
|
1497
|
+
/**
|
|
1498
|
+
* Steers the run accepted that the model context of later turns does not hold, in the order
|
|
1499
|
+
* they arrived: the ones it never took in, and, when the run is not kept in context, every
|
|
1500
|
+
* steer it accepted.
|
|
1501
|
+
*/
|
|
1502
|
+
readonly unappliedSteerIds: readonly string[];
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
/** The running run accepted a steer; it is taken in at the run's next step boundary. */
|
|
1506
|
+
export interface IFlexSteerAcceptedEvent extends IFlexEventBase {
|
|
1507
|
+
readonly type: 'prompt.steer.accepted';
|
|
1508
|
+
readonly queueId: string;
|
|
1509
|
+
readonly runId: string;
|
|
1510
|
+
readonly steerId: string;
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
/** The run took a steer in as the user message `messageId`, before its next model call. */
|
|
1514
|
+
export interface IFlexSteerAppliedEvent extends IFlexEventBase {
|
|
1515
|
+
readonly type: 'prompt.steer.applied';
|
|
1516
|
+
readonly queueId: string;
|
|
1517
|
+
readonly runId: string;
|
|
1518
|
+
readonly steerId: string;
|
|
1519
|
+
readonly messageId: string;
|
|
1458
1520
|
}
|
|
1459
1521
|
|
|
1460
1522
|
/** The run waits `delayMs` before retrying a rate-limited or unavailable model call. */
|
|
@@ -1513,6 +1575,8 @@ export type TFlexHarnessEvent =
|
|
|
1513
1575
|
| IFlexRunRetryingEvent
|
|
1514
1576
|
| TFlexModelUsageEvent
|
|
1515
1577
|
| IFlexPromptQueueEvent
|
|
1578
|
+
| IFlexSteerAcceptedEvent
|
|
1579
|
+
| IFlexSteerAppliedEvent
|
|
1516
1580
|
| IFlexSessionHistoryChangedEvent;
|
|
1517
1581
|
|
|
1518
1582
|
export type TFlexHarnessEventListener = (event: TFlexHarnessEvent) => void;
|
package/ts/plugins.ts
CHANGED
|
@@ -9,12 +9,15 @@ export { AsyncLocalStorage, crypto, fs, path };
|
|
|
9
9
|
// @push.rocks scope
|
|
10
10
|
import {
|
|
11
11
|
AgentEventStoreConflictError,
|
|
12
|
+
AgentPendingInputQueue,
|
|
13
|
+
AgentPendingInputRejectedError,
|
|
12
14
|
AgentSession,
|
|
13
15
|
buildModelMessages,
|
|
14
16
|
ToolJobStoreConflictError,
|
|
15
17
|
createAgentEvent,
|
|
16
18
|
filterModelVisibleAgentEvents,
|
|
17
19
|
getAgentGenerationTransactions,
|
|
20
|
+
isGenerationInModelContext,
|
|
18
21
|
modelMessagesToAgentEvents,
|
|
19
22
|
tool,
|
|
20
23
|
validateAgentEventArchiveV2,
|
|
@@ -24,12 +27,15 @@ import {
|
|
|
24
27
|
|
|
25
28
|
export {
|
|
26
29
|
AgentEventStoreConflictError,
|
|
30
|
+
AgentPendingInputQueue,
|
|
31
|
+
AgentPendingInputRejectedError,
|
|
27
32
|
AgentSession,
|
|
28
33
|
buildModelMessages,
|
|
29
34
|
ToolJobStoreConflictError,
|
|
30
35
|
createAgentEvent,
|
|
31
36
|
filterModelVisibleAgentEvents,
|
|
32
37
|
getAgentGenerationTransactions,
|
|
38
|
+
isGenerationInModelContext,
|
|
33
39
|
modelMessagesToAgentEvents,
|
|
34
40
|
tool,
|
|
35
41
|
validateAgentEventArchiveV2,
|
|
@@ -37,6 +43,7 @@ export {
|
|
|
37
43
|
z,
|
|
38
44
|
};
|
|
39
45
|
export type {
|
|
46
|
+
IAgentAppliedInput,
|
|
40
47
|
IAgentEventArchiveV2,
|
|
41
48
|
IAgentContextCompactionOptions,
|
|
42
49
|
IAgentEventSnapshotV2,
|
package/ts/readme.md
CHANGED
|
@@ -403,26 +403,29 @@ Normal Flex session cleanup always waits in-flight local project operations, the
|
|
|
403
403
|
|
|
404
404
|
`InMemoryFlexProjectManagementStore` is the standalone in-memory implementation. `InMemoryFlexHarnessStores` and `JsonFileFlexHarnessStores` include `projectManagement` as a required bundle member. `assertFlexProjectManagementSnapshot()` validates live records, `assertFlexProjectManagementTombstone()` validates tombstones, and `assertFlexProjectManagementRecord()` validates the union. `createEmptyFlexProjectManagementSnapshot(sessionGenerationId, sessionGenerationSequence)` returns a revision-0 live state for the supplied current generation.
|
|
405
405
|
|
|
406
|
-
##
|
|
406
|
+
## Subagents
|
|
407
407
|
|
|
408
|
-
`subagents` enables
|
|
408
|
+
`subagents` enables the harness-owned `delegate` and `delegate_result` tools when at least one definition exists and the session depth is below `maxSubagentDepth`. A tool provider cannot replace either enabled built-in. Delegation is asynchronous by default: `delegate` returns `{ taskId, status: 'running', runId }` after durable child admission, so the parent can continue independent work. `delegate_result({ taskId, waitMs? })` reads live status and, once settled, bounded final text and model identity; its optional wait is limited to 240 seconds and observes parent cancellation. Set `foreground: true` to await the final answer in `delegate` itself. Children remain owned by the parent turn: Stop cancels its exact child runs, and the parent joins them before releasing authority and finalizing. A child result arriving while inference is still active enters the parent's next model context as untrusted task data.
|
|
409
409
|
|
|
410
410
|
The model calls it with this exact input shape:
|
|
411
411
|
|
|
412
|
+
When upgrading from 9.x, callers that require the previous synchronous delegate result must pass `foreground: true`. Tool providers must also leave the newly reserved `delegate_result` name available when subagents are enabled.
|
|
413
|
+
|
|
412
414
|
```typescript
|
|
413
415
|
interface IDelegateInput {
|
|
414
416
|
description: string;
|
|
415
417
|
prompt: string;
|
|
416
418
|
subagentType: string;
|
|
417
419
|
taskId?: string;
|
|
420
|
+
foreground?: boolean;
|
|
418
421
|
}
|
|
419
422
|
```
|
|
420
423
|
|
|
421
424
|
Before creating or resuming a child, FlexHarness requests permission on the parent run with `kind: 'subagent.start'`, the parent `toolCallId`, and separately bounded harness-owned agent/task metadata. Its metadata includes `childSessionId`, the exact ID reserved for this call: FlexHarness derives it deterministically when `taskId` is omitted and copies the supplied candidate when `taskId` is present. A resume also retains that candidate as `taskId`. This block is not truncated by `toolOutputLimits`. The reserved ID binds permission handling before child creation but does not prove that the child exists or is owned: applications must treat the later delegated admission context as authoritative. The controller answers through the normal permission APIs. This request has no `rememberKey`, so `always` is invalid; controllers use `once` or `reject`.
|
|
422
425
|
|
|
423
|
-
Each new invocation creates a durable child `IFlexSession` with immutable `parentSessionId`, origin `parentRunId`, origin `parentToolCallId`, `agent`, and `depth`. New public roots persist `depth: 0`; legacy schema-1 roots may omit it. These fields are harness-owned; public `createSession()` remains limited to `sessionId`, `sessionGenerationId`, and `title`. Child sessions reject direct `prompt()`, `startPrompt()`, `enqueuePrompt()`, and `schedulePrompt()` calls and run only through the
|
|
426
|
+
Each new invocation creates a durable child `IFlexSession` with immutable `parentSessionId`, origin `parentRunId`, origin `parentToolCallId`, `agent`, and `depth`. New public roots persist `depth: 0`; legacy schema-1 roots may omit it. These fields are harness-owned; public `createSession()` remains limited to `sessionId`, `sessionGenerationId`, and `title`. Child sessions reject direct `prompt()`, `startPrompt()`, `enqueuePrompt()`, and `schedulePrompt()` calls and run only through the `delegate` tool. The model and tool resolver contexts receive optional immutable `parentSessionId` and `agent` values so integrations can apply agent-specific model and tool policy. Child prompts use the definition's `modelHint`, `system`, and `maxSteps`.
|
|
424
427
|
|
|
425
|
-
The parent tool part receives `childSessionId`
|
|
428
|
+
The parent tool part receives `childSessionId` as soon as the child is acquired, so a host can offer an authorized live transcript. A foreground call also adds the resolved child model to its part and returns bounded JSON:
|
|
426
429
|
|
|
427
430
|
```typescript
|
|
428
431
|
{
|
|
@@ -525,7 +528,7 @@ Attachment payloads are never copied into public audit messages or events. Publi
|
|
|
525
528
|
}
|
|
526
529
|
```
|
|
527
530
|
|
|
528
|
-
|
|
531
|
+
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
532
|
|
|
530
533
|
The main session methods are:
|
|
531
534
|
|
|
@@ -572,6 +575,7 @@ await harness.cancelScheduledPrompt(scopeId, sessionId, scheduled.scheduleKey);
|
|
|
572
575
|
await harness.getPromptQueueEntry(scopeId, sessionId, queued.queueId);
|
|
573
576
|
await harness.listPromptQueueEntries(scopeId, sessionId);
|
|
574
577
|
await harness.cancelPrompt(scopeId, sessionId, queued.queueId);
|
|
578
|
+
await harness.steerPrompt(scopeId, sessionId, admission.queueId, { steerId, prompt });
|
|
575
579
|
await harness.abort(scopeId, sessionId);
|
|
576
580
|
await harness.listPendingPermissions(scopeId, sessionId);
|
|
577
581
|
await harness.respondToPermission(scopeId, sessionId, permissionId, 'once');
|
|
@@ -616,6 +620,24 @@ The reservation save is the admission point. A save failure produces no start ev
|
|
|
616
620
|
|
|
617
621
|
`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
622
|
|
|
623
|
+
### Steering a Running Turn
|
|
624
|
+
|
|
625
|
+
`steerPrompt()` hands a message to the running run of a prompt instead of queueing it behind that run:
|
|
626
|
+
|
|
627
|
+
```typescript
|
|
628
|
+
const admission = await harness.startPrompt(scopeId, sessionId, 'Plan the release.');
|
|
629
|
+
await harness.steerPrompt(scopeId, sessionId, admission.queueId, {
|
|
630
|
+
steerId: 'steer-1',
|
|
631
|
+
prompt: 'Keep the database migration out of it.',
|
|
632
|
+
});
|
|
633
|
+
const result = await admission.completion;
|
|
634
|
+
console.log(result.unappliedSteerIds); // [] once the run took the steer in
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
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.
|
|
638
|
+
|
|
639
|
+
`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.
|
|
640
|
+
|
|
619
641
|
## Slash Commands
|
|
620
642
|
|
|
621
643
|
`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.
|
|
@@ -668,7 +690,7 @@ At most 128 custom commands may be registered. Every registration must be a plai
|
|
|
668
690
|
|
|
669
691
|
`/undo` and `/redo`, plus `undoSession()` and `redoSession()`, move a durable history cursor. The direct methods return `{ revertedRunId }` and `{ restoredRunId }`; the slash forms return `{ type: 'operation', name: 'undo' | 'redo' }`. Each committed cursor move emits one `session.history.changed` event with `direction`, `runId`, and the selected session identity. A committed branch emits the same event with `direction: 'branch'` and no `runId`, so controllers should refresh the complete selected session. Capture finalization, cleanup, and metadata-only changes do not emit this event.
|
|
670
692
|
|
|
671
|
-
A completed root-session turn defines an operation-group boundary. Failed and cancelled turns after it belong to that group; leading failed or cancelled turns belong to the first completed group. No completed boundary means there is nothing to undo. Undo applies selected segments in reverse order and redo applies them in forward order. Without `turnReversionProvider`, only transcript and future model context move. Hidden messages disappear from `getMessages()`, message pages, exact message lookup, and future model context. Starting a new prompt, template, handler, or compaction from an undone position commits a branch: hidden messages and segments are removed durably and cannot be redone. Successful event archival also commits hidden redo history; a missing compaction or failed archive leaves it intact.
|
|
693
|
+
A completed root-session turn defines an operation-group boundary. Failed and cancelled turns after it belong to that group; leading failed or cancelled turns belong to the first completed group. No completed boundary means there is nothing to undo. Undo applies selected segments in reverse order and redo applies them in forward order. Without `turnReversionProvider`, only transcript and future model context move. Hidden messages disappear from `getMessages()`, message pages, exact message lookup, and future model context. Starting a new prompt, template, handler, or compaction from an undone position commits a branch: hidden messages and segments are removed durably and cannot be redone. Successful event archival also commits hidden redo history; a missing compaction or failed archive leaves it intact. Automatic budget compaction archives covered events after generation finalization and advances the same undo horizon; set `contextCompactionBytes: false` to disable that automatic policy.
|
|
672
694
|
|
|
673
695
|
Two horizons bound undo. Retention pruning removes the oldest complete visible units when `reversionLimits` is exceeded. Explicit event archival marks covered turns context-unavailable and prunes complete prefixes that can no longer be rebuilt; FlexHarness never crosses that archive horizon. Manual compaction without archival retains the original events and remains undoable. Schema-1 projection history and sessions migrated from `2.x` have no reversion segments, so historical turns are not retroactively undoable; newly written turns are tracked normally.
|
|
674
696
|
|
|
@@ -771,7 +793,7 @@ The protocol is durable and inspectable:
|
|
|
771
793
|
|
|
772
794
|
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.
|
|
773
795
|
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.
|
|
774
|
-
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
|
|
796
|
+
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.
|
|
775
797
|
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. Providers must treat `operationId` idempotently.
|
|
776
798
|
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.
|
|
777
799
|
|
|
@@ -941,6 +963,7 @@ A `contextCompactor` states which model to compact with from the model the compa
|
|
|
941
963
|
| `reason` | Compaction | Model members |
|
|
942
964
|
| --- | --- | --- |
|
|
943
965
|
| `context-overflow` | A run's context overflowed, including a delegated run's | `resolvedModel`: the `IFlexResolvedModel` the model resolver returned for that run, frozen; a delegated run's overflow carries the delegated run's own model |
|
|
966
|
+
| `context-budget` | Settled history exceeds `contextCompactionBytes` before a generation's first inference (256 KiB by default with a compactor) | `resolvedModel`: the exact model resolved for that run, including its delegated model |
|
|
944
967
|
| `manual` | `/compact` or `compactSession()` | `modelHint`: the `modelHint` of the `/compact` execution options or of `compactSession(scopeId, sessionId, { modelHint })`, when one was passed |
|
|
945
968
|
| `retention` | Event retention | none |
|
|
946
969
|
|
|
@@ -983,6 +1006,14 @@ const unsubscribe = harness.subscribe((event) => {
|
|
|
983
1006
|
break;
|
|
984
1007
|
case 'run.finished':
|
|
985
1008
|
markRunFinished(event.runId, event.status);
|
|
1009
|
+
// A run that did not stay in context needs its prompt sent again, followed by its unapplied steers.
|
|
1010
|
+
resendAsNextTurn(event.keptInContext ? [] : [event.runId], event.unappliedSteerIds);
|
|
1011
|
+
break;
|
|
1012
|
+
case 'prompt.steer.accepted':
|
|
1013
|
+
markSteerPending(event.steerId);
|
|
1014
|
+
break;
|
|
1015
|
+
case 'prompt.steer.applied':
|
|
1016
|
+
markSteerApplied(event.steerId, event.messageId);
|
|
986
1017
|
break;
|
|
987
1018
|
case 'session.history.changed':
|
|
988
1019
|
refreshSelectedSession(event.sessionId);
|
|
@@ -999,7 +1030,7 @@ const unsubscribe = harness.subscribe((event) => {
|
|
|
999
1030
|
unsubscribe();
|
|
1000
1031
|
```
|
|
1001
1032
|
|
|
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.
|
|
1033
|
+
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
1034
|
|
|
1004
1035
|
`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
1036
|
|
|
@@ -380,6 +380,7 @@ export function normalizeAgentSessionPolicy<TScope>(
|
|
|
380
380
|
return {
|
|
381
381
|
...(policy.contextBuilder === undefined ? {} : { contextBuilder: policy.contextBuilder }),
|
|
382
382
|
...(policy.contextCompactor === undefined ? {} : { contextCompactor: policy.contextCompactor }),
|
|
383
|
+
...(policy.contextCompactionBytes === undefined ? {} : { contextCompactionBytes: policy.contextCompactionBytes }),
|
|
383
384
|
...(policy.eventRetention === undefined ? {} : { eventRetention: policy.eventRetention }),
|
|
384
385
|
...(policy.changeListenerTimeoutMs === undefined
|
|
385
386
|
? {}
|
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);
|
|
@@ -256,9 +256,9 @@ export class BrowserKvm implements IKvmDriver {
|
|
|
256
256
|
return;
|
|
257
257
|
}
|
|
258
258
|
|
|
259
|
-
await usernameElement.click({
|
|
259
|
+
await usernameElement.click({ count: 3 });
|
|
260
260
|
await usernameElement.type(this.options.username);
|
|
261
|
-
await passwordElement.click({
|
|
261
|
+
await passwordElement.click({ count: 3 });
|
|
262
262
|
await passwordElement.type(this.options.password);
|
|
263
263
|
await passwordElement.press('Enter');
|
|
264
264
|
|