@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/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
- ## Foreground Subagents
406
+ ## Subagents
407
407
 
408
- `subagents` enables a harness-owned built-in tool named `delegate`. It is available only when at least one definition exists and the current session depth is below `maxSubagentDepth`. An application or resource `toolProvider` must not return its own `delegate` tool while the built-in is enabled for that run. The built-in is foreground-only: the parent tool call does not complete until the child prompt reaches a terminal outcome.
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 foreground `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`.
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` in a cumulative `part.updated` event as soon as the child is acquired. If child model resolution completes, a later cumulative update adds `model`; failures before model resolution leave it absent. The terminal tool part retains every value that became available. A successful delegate call always has model identity and returns bounded JSON:
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
- When the turn succeeds, the original string remains only in canonical private Agent events so a later model turn can receive the attachment again. Failed, cancelled, resolver-failed, cleanup-failed, and persistence-failed turns do not add it to future context.
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 foreground subagent effects, although child transcript records remain separate.
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)) {
@@ -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({ clickCount: 3 });
259
+ await usernameElement.click({ count: 3 });
260
260
  await usernameElement.type(this.options.username);
261
- await passwordElement.click({ clickCount: 3 });
261
+ await passwordElement.click({ count: 3 });
262
262
  await passwordElement.type(this.options.password);
263
263
  await passwordElement.press('Enter');
264
264
 
package/ts_kvm/plugins.ts CHANGED
@@ -4,7 +4,7 @@ import * as crypto from 'node:crypto';
4
4
  export { crypto };
5
5
 
6
6
  // third party scope
7
- import puppeteer from 'puppeteer';
7
+ import * as puppeteer from 'puppeteer';
8
8
 
9
9
  export { puppeteer };
10
10