@modelprofile.com/flexharness 13.2.1 → 13.3.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.
@@ -46,6 +46,7 @@ import type {
46
46
  IStoredSessionState,
47
47
  IStorageState,
48
48
  IActiveRun,
49
+ IAdmittedSteer,
49
50
  IRunSteering,
50
51
  IRunSteeringOutcome,
51
52
  IQueuedPrompt,
@@ -76,13 +77,34 @@ export class FlexHarnessRuns<TScope> {
76
77
  return true;
77
78
  }
78
79
 
79
- public createActiveRun(queued: IQueuedPrompt): IActiveRun {
80
+ /**
81
+ * The steering of a run, holding `steers` as accepted and waiting for its first step boundary.
82
+ * The caller already counted their bytes against the session.
83
+ */
84
+ public createRunSteering(
85
+ stored: IStoredSessionState,
86
+ steers: readonly IAdmittedSteer[] = [],
87
+ ): IRunSteering {
80
88
  const steering: IRunSteering = {
81
89
  input: new plugins.AgentPendingInputQueue(),
82
- stored: queued.stored,
90
+ stored,
83
91
  accepted: new Map(),
84
92
  applied: new Set(),
85
93
  };
94
+ for (const steer of steers) {
95
+ steering.input.add({
96
+ inputId: steer.steerId,
97
+ content: cloneSerializable(steer.prompt.modelMessage.content) as Parameters<
98
+ plugins.IAgentSession['beginGeneration']
99
+ >[0],
100
+ });
101
+ steering.accepted.set(steer.steerId, { prompt: steer.prompt, byteSize: steer.byteSize });
102
+ }
103
+ return steering;
104
+ }
105
+
106
+ public createActiveRun(queued: IQueuedPrompt): IActiveRun {
107
+ const steering = queued.steering ?? this.createRunSteering(queued.stored);
86
108
  queued.steering = steering;
87
109
  return {
88
110
  steering,
@@ -8,6 +8,7 @@ import type {
8
8
  IFlexCreateSessionOptions,
9
9
  IFlexDeleteSessionGenerationCohortInput,
10
10
  IFlexDeleteSessionGenerationCohortResult,
11
+ IFlexEnqueuePromptOptions,
11
12
  IFlexEventArchiveMetadata,
12
13
  IFlexHarnessOptions,
13
14
  IFlexImportedRun,
@@ -25,7 +26,6 @@ import type {
25
26
  IFlexPromptAdmission,
26
27
  IFlexPromptQueueAdmission,
27
28
  IFlexPromptQueueEntry,
28
- IFlexPromptOptions,
29
29
  IFlexPromptResult,
30
30
  IFlexRedoSessionResult,
31
31
  IFlexScheduledPromptAdmission,
@@ -305,7 +305,7 @@ export class FlexHarness<TScope = unknown> {
305
305
  scopeId: string,
306
306
  sessionId: string,
307
307
  prompt: TFlexPrompt,
308
- options?: IFlexPromptOptions,
308
+ options?: IFlexEnqueuePromptOptions,
309
309
  ): Promise<IFlexPromptResult> {
310
310
  return this.core.prompts.prompt(scopeId, sessionId, prompt, options);
311
311
  }
@@ -314,7 +314,7 @@ export class FlexHarness<TScope = unknown> {
314
314
  scopeId: string,
315
315
  sessionId: string,
316
316
  prompt: TFlexPrompt,
317
- options?: IFlexPromptOptions,
317
+ options?: IFlexEnqueuePromptOptions,
318
318
  ): Promise<IFlexPromptAdmission> {
319
319
  return this.core.prompts.startPrompt(scopeId, sessionId, prompt, options);
320
320
  }
@@ -323,7 +323,7 @@ export class FlexHarness<TScope = unknown> {
323
323
  scopeId: string,
324
324
  sessionId: string,
325
325
  prompt: TFlexPrompt,
326
- options?: IFlexPromptOptions,
326
+ options?: IFlexEnqueuePromptOptions,
327
327
  ): Promise<IFlexPromptQueueAdmission> {
328
328
  return this.core.prompts.enqueuePrompt(scopeId, sessionId, prompt, options);
329
329
  }
@@ -337,6 +337,13 @@ export interface IRunSteering {
337
337
  outcome?: IRunSteeringOutcome;
338
338
  }
339
339
 
340
+ /** A steer admitted together with the prompt whose run takes it in before its first model call. */
341
+ export interface IAdmittedSteer {
342
+ readonly steerId: string;
343
+ readonly prompt: INormalizedFlexPrompt;
344
+ readonly byteSize: number;
345
+ }
346
+
340
347
  export interface IRunSteeringOutcome {
341
348
  /** Whether the run's prompt, the steers it took in and what its model produced stay in context. */
342
349
  readonly keptInContext: boolean;
@@ -410,8 +417,13 @@ export interface IQueuedPrompt {
410
417
  options?: IFlexPromptOptions;
411
418
  delegationSeed?: IDelegatedRunAdmissionSeed;
412
419
  byteSize: number;
413
- /** The steering of the run the prompt started, once it started one. */
420
+ /**
421
+ * The steering of the run the prompt started, once it started one. A prompt admitted with steers
422
+ * has it from admission on, holding those steers and their bytes.
423
+ */
414
424
  steering?: IRunSteering;
425
+ /** The steers admitted with the prompt, in their given order. */
426
+ admittedSteerIds?: readonly string[];
415
427
  completion: Promise<IFlexPromptResult>;
416
428
  resolveCompletion: (result: IFlexPromptResult) => void;
417
429
  rejectCompletion: (error: unknown) => void;
package/ts/interfaces.ts CHANGED
@@ -588,6 +588,19 @@ export interface IFlexSchedulePromptOptions extends IFlexPromptOptions {
588
588
  debounceMs?: number;
589
589
  }
590
590
 
591
+ /**
592
+ * Options of a prompt that starts a run now or queues it. `steers` are further user messages that
593
+ * join the prompt's run in the order given: the run accepts them when it starts and its generation
594
+ * takes them in before its first model call, each as a user message of its own carrying its
595
+ * `steerId`. They count against the session's outstanding prompt limits from admission on, and
596
+ * report like every other steer of the run: `prompt.steer.applied` names each message, and
597
+ * `unappliedSteerIds` names the ones the model context of later turns does not hold, also when the
598
+ * prompt is cancelled before its run started.
599
+ */
600
+ export interface IFlexEnqueuePromptOptions extends IFlexPromptOptions {
601
+ steers?: readonly IFlexSteerInput[];
602
+ }
603
+
591
604
  export interface IFlexScheduledPromptAdmission extends IFlexPromptAdmission {
592
605
  scheduleKey: string;
593
606
  }
package/ts/readme.md CHANGED
@@ -730,6 +730,8 @@ The run takes a steer in at its next step boundary: after the tool results of it
730
730
 
731
731
  `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 child sessions reject this general API (their exact parent run uses `sendSubagentInput()` or `delegate_send` instead). 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.
732
732
 
733
+ A prompt can bring further user messages into its run: `startPrompt()`, `enqueuePrompt()` and `prompt()` take `steers`, a list of `{ steerId, prompt }`, in their options. The run accepts them when it starts, emitting `prompt.steer.accepted` for each in the given order right after `prompt.started`, and takes them in before its first model call, so the model sees the prompt and its steers as consecutive user messages of their own in one call, each with its own `steerId`, `messageId` and attachments. From then on they report like every other steer of the run. They count against the session's outstanding prompt limits from admission on. The whole prompt is refused with `FlexHarnessValidationError` when a `steerId` repeats, and with `FlexHarnessQueueFullError` when it brings more steers than `maxOutstandingPromptsPerSession` or its steers' bytes exceed the outstanding byte limit. A prompt cancelled before its run started lists them in `unappliedSteerIds` on its finished queue entry and `prompt.finished`, with `keptInContext: false`. A caller holding several waiting messages for a session delivers them in one turn this way, without merging them into one message.
734
+
733
735
  ### Keeping the Prompt Cache Warm
734
736
 
735
737
  A provider's prompt cache expires when nothing reads it for its TTL, and the next turn then writes the whole prefix again. `keepCacheWarm()` keeps a root session's cache warm until a deadline:
@@ -1157,7 +1159,7 @@ const unsubscribe = harness.subscribe((event) => {
1157
1159
  unsubscribe();
1158
1160
  ```
1159
1161
 
1160
- 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 that stays in its message emits exactly one `part.completed` event before the corresponding `run.finished` event: once the model step that streamed it is committed, so its text is final and saved, or with the run's terminal projection when that step did not commit. Text a later model step streams starts a new part. When a model call fails and is retried, the text and reasoning parts it streamed are not in the session: `part.removed` takes each of them out of its message, last part first, and the retry streams its own parts. Its `partIndex` is the part's position before it left; the parts after it move up by one. A removed part has no further events, and `part.removed` does not count against `callbackLimits`. The tool parts of calls the failed model call executed stay. Every `part.started`, `part.delta`, `part.updated`, `part.completed`, and `part.removed` event carries zero-based `messageIndex` and `partIndex` coordinates from the session's authoritative message and part sequences.
1162
+ 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` (a steer admitted with its prompt when the run starts, and not at all when the prompt is cancelled or fails before), 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 that stays in its message emits exactly one `part.completed` event before the corresponding `run.finished` event: once the model step that streamed it is committed, so its text is final and saved, or with the run's terminal projection when that step did not commit. Text a later model step streams starts a new part. When a model call fails and is retried, the text and reasoning parts it streamed are not in the session: `part.removed` takes each of them out of its message, last part first, and the retry streams its own parts. Its `partIndex` is the part's position before it left; the parts after it move up by one. A removed part has no further events, and `part.removed` does not count against `callbackLimits`. The tool parts of calls the failed model call executed stay. Every `part.started`, `part.delta`, `part.updated`, `part.completed`, and `part.removed` event carries zero-based `messageIndex` and `partIndex` coordinates from the session's authoritative message and part sequences.
1161
1163
 
1162
1164
  `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.
1163
1165