@modelprofile.com/flexharness 15.0.0 → 16.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.flexharness.cachewarm.d.ts +2 -2
- package/dist_ts/classes.flexharness.cachewarm.js +1 -1
- package/dist_ts/classes.flexharness.core.js +14 -20
- package/dist_ts/classes.flexharness.d.ts +2 -1
- package/dist_ts/classes.flexharness.js +4 -1
- package/dist_ts/classes.flexharness.prompts.d.ts +9 -5
- package/dist_ts/classes.flexharness.prompts.js +83 -16
- package/dist_ts/classes.flexharness.runs.d.ts +4 -4
- package/dist_ts/classes.flexharness.runs.js +31 -17
- package/dist_ts/classes.flexharness.subagents.js +1 -1
- package/dist_ts/interfaces.d.ts +24 -9
- package/dist_ts/interfaces.flexharness.d.ts +10 -8
- package/dist_ts/interfaces.js +1 -1
- package/dist_ts/plugins.d.ts +1 -1
- package/dist_ts/plugins.js +1 -1
- package/dist_ts/utils.json.js +32 -18
- package/dist_ts/utils.validation.js +3 -2
- package/package.json +3 -3
- package/readme.md +52 -3
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.flexharness.cachewarm.ts +2 -2
- package/ts/classes.flexharness.core.ts +13 -21
- package/ts/classes.flexharness.prompts.ts +113 -38
- package/ts/classes.flexharness.runs.ts +35 -25
- package/ts/classes.flexharness.subagents.ts +2 -1
- package/ts/classes.flexharness.ts +10 -0
- package/ts/interfaces.flexharness.ts +10 -8
- package/ts/interfaces.ts +27 -9
- package/ts/plugins.ts +1 -0
- package/ts/readme.md +52 -3
- package/ts/utils.json.ts +34 -18
- package/ts/utils.validation.ts +2 -1
package/ts/interfaces.ts
CHANGED
|
@@ -547,6 +547,7 @@ export type TFlexSlashCommandRegistration<TScope> =
|
|
|
547
547
|
| IFlexSlashCommandTemplateRegistration
|
|
548
548
|
| IFlexSlashCommandHandlerRegistration<TScope>;
|
|
549
549
|
|
|
550
|
+
/** `promptId` names template and `/init` admissions; results that admit no prompt ignore it. */
|
|
550
551
|
export interface IFlexSlashCommandExecutionOptions extends IFlexPromptOptions {
|
|
551
552
|
signal?: AbortSignal;
|
|
552
553
|
}
|
|
@@ -600,6 +601,8 @@ export interface IFlexPromptQueueEntry {
|
|
|
600
601
|
/** The prompt's `promptId`, when it named one. */
|
|
601
602
|
promptId?: string;
|
|
602
603
|
scheduleKey?: string;
|
|
604
|
+
/** This entry answers existing context without adding a prompt message. */
|
|
605
|
+
continuation?: true;
|
|
603
606
|
/** The entry is a cache-warm prompt; see {@link IFlexMessage.cacheWarm}. */
|
|
604
607
|
cacheWarm?: true;
|
|
605
608
|
startedAt?: string;
|
|
@@ -608,8 +611,9 @@ export interface IFlexPromptQueueEntry {
|
|
|
608
611
|
/**
|
|
609
612
|
* On a finished entry: whether its prompt, the steers its run took in and what its model
|
|
610
613
|
* produced stay in the model context of later turns. True for a completed run and a run
|
|
611
|
-
* stopped after its model produced output; false for
|
|
612
|
-
* model output and a prompt that never ran.
|
|
614
|
+
* stopped or failed after its model produced output; false for an unsuccessful run without
|
|
615
|
+
* model output and a prompt that never ran. For a continuation, this describes only the new
|
|
616
|
+
* run's history; it does not change the retention of the existing input.
|
|
613
617
|
*/
|
|
614
618
|
keptInContext?: boolean;
|
|
615
619
|
/**
|
|
@@ -620,10 +624,10 @@ export interface IFlexPromptQueueEntry {
|
|
|
620
624
|
unappliedSteerIds?: string[];
|
|
621
625
|
}
|
|
622
626
|
|
|
623
|
-
|
|
627
|
+
/** The result of answering existing context, without adding a prompt message. */
|
|
628
|
+
export interface IFlexContinuePromptResult {
|
|
624
629
|
runId: string;
|
|
625
630
|
sessionId: string;
|
|
626
|
-
userMessage: IFlexMessage;
|
|
627
631
|
/** The run's last assistant message; a steered run completed earlier ones as it took steers in. */
|
|
628
632
|
assistantMessage: IFlexMessage;
|
|
629
633
|
model: IFlexModelIdentity;
|
|
@@ -634,6 +638,10 @@ export interface IFlexPromptResult {
|
|
|
634
638
|
unappliedSteerIds: string[];
|
|
635
639
|
}
|
|
636
640
|
|
|
641
|
+
export interface IFlexPromptResult extends IFlexContinuePromptResult {
|
|
642
|
+
userMessage: IFlexMessage;
|
|
643
|
+
}
|
|
644
|
+
|
|
637
645
|
/** A message for the running run of a session, taken in at the run's next step boundary. */
|
|
638
646
|
export interface IFlexSteerInput {
|
|
639
647
|
/** Caller-chosen identity, unique among the steers of the run. */
|
|
@@ -647,12 +655,13 @@ export interface IFlexSteerAdmission {
|
|
|
647
655
|
runId: string;
|
|
648
656
|
}
|
|
649
657
|
|
|
650
|
-
export interface IFlexPromptQueueAdmission {
|
|
658
|
+
export interface IFlexPromptQueueAdmission<TResult extends IFlexContinuePromptResult = IFlexPromptResult> {
|
|
651
659
|
queueId: string;
|
|
652
|
-
completion: Promise<
|
|
660
|
+
completion: Promise<TResult>;
|
|
653
661
|
}
|
|
654
662
|
|
|
655
|
-
export interface IFlexPromptAdmission extends
|
|
663
|
+
export interface IFlexPromptAdmission<TResult extends IFlexContinuePromptResult = IFlexPromptResult>
|
|
664
|
+
extends IFlexPromptQueueAdmission<TResult> {
|
|
656
665
|
runId: string;
|
|
657
666
|
}
|
|
658
667
|
|
|
@@ -673,6 +682,13 @@ export interface IFlexEnqueuePromptOptions extends IFlexPromptOptions {
|
|
|
673
682
|
steers?: readonly IFlexSteerInput[];
|
|
674
683
|
}
|
|
675
684
|
|
|
685
|
+
/** Options for continuation through the same FIFO, optionally with scheduled execution. */
|
|
686
|
+
export interface IFlexContinuePromptOptions extends Omit<IFlexEnqueuePromptOptions, 'promptId'> {
|
|
687
|
+
scheduleKey?: string;
|
|
688
|
+
/** Requires scheduleKey; defaults to 50 ms when scheduled. */
|
|
689
|
+
debounceMs?: number;
|
|
690
|
+
}
|
|
691
|
+
|
|
676
692
|
export interface IFlexScheduledPromptAdmission extends IFlexPromptAdmission {
|
|
677
693
|
scheduleKey: string;
|
|
678
694
|
}
|
|
@@ -1260,7 +1276,8 @@ export interface IFlexScopeSnapshot {
|
|
|
1260
1276
|
export interface IFlexTerminalProjection {
|
|
1261
1277
|
runId: string;
|
|
1262
1278
|
status: 'completed' | 'failed' | 'cancelled';
|
|
1263
|
-
|
|
1279
|
+
/** Absent for a continuation, which creates no prompt message. */
|
|
1280
|
+
userMessage?: IFlexMessage;
|
|
1264
1281
|
assistantMessage: IFlexMessage;
|
|
1265
1282
|
model?: IFlexModelIdentity;
|
|
1266
1283
|
/** The usage the provider reported for the run's model calls, whatever the status. */
|
|
@@ -1288,7 +1305,8 @@ export interface IFlexAffectedWorkspace {
|
|
|
1288
1305
|
|
|
1289
1306
|
interface IFlexReversionSegmentV2 {
|
|
1290
1307
|
runId: string;
|
|
1291
|
-
|
|
1308
|
+
/** Absent for an assistant-only continuation segment. */
|
|
1309
|
+
userMessageId?: string;
|
|
1292
1310
|
status: TFlexReversionSegmentStatus;
|
|
1293
1311
|
contextAvailable: boolean;
|
|
1294
1312
|
eventIds: string[];
|
package/ts/plugins.ts
CHANGED
package/ts/readme.md
CHANGED
|
@@ -498,7 +498,7 @@ Sending requires the exact active parent run, its currently delegated direct chi
|
|
|
498
498
|
|
|
499
499
|
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`.
|
|
500
500
|
|
|
501
|
-
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()` accepts `sessionId`, `sessionGenerationId`, `configurationRef`, and `title`. A child inherits its parent's `configurationRef` and cannot resume under a different reference. 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`.
|
|
501
|
+
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()` accepts `sessionId`, `sessionGenerationId`, `configurationRef`, and `title`. A child inherits its parent's `configurationRef` and cannot resume under a different reference. Child sessions reject direct `continuePrompt()`, `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`.
|
|
502
502
|
|
|
503
503
|
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:
|
|
504
504
|
|
|
@@ -717,6 +717,55 @@ The reservation save is the admission point. A save failure produces no start ev
|
|
|
717
717
|
|
|
718
718
|
`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.
|
|
719
719
|
|
|
720
|
+
### Continuing Without New Input
|
|
721
|
+
|
|
722
|
+
Use `continuePrompt()` after a failed or stopped run whose input remains in context
|
|
723
|
+
(`keptInContext: true`), when the conversation still needs an answer:
|
|
724
|
+
|
|
725
|
+
```typescript
|
|
726
|
+
const continuation = await harness.continuePrompt(scopeId, sessionId, {
|
|
727
|
+
modelHint: 'available-model',
|
|
728
|
+
});
|
|
729
|
+
console.log(continuation.queueId);
|
|
730
|
+
const result = await continuation.completion;
|
|
731
|
+
console.log(result.assistantMessage.parts);
|
|
732
|
+
```
|
|
733
|
+
|
|
734
|
+
The signature is `continuePrompt(scopeId: string, sessionId: string,
|
|
735
|
+
options?: IFlexContinuePromptOptions): Promise<IFlexPromptQueueAdmission<IFlexContinuePromptResult>>`.
|
|
736
|
+
It accepts `modelHint`, `system`, `maxSteps` and initial `steers`, plus optional
|
|
737
|
+
`scheduleKey` and `debounceMs` for the same scheduling behavior as `schedulePrompt()`.
|
|
738
|
+
`debounceMs` requires a schedule key and defaults to 50 ms when scheduled. There is
|
|
739
|
+
no `prompt` or `promptId`: continuation creates a new run and an assistant message
|
|
740
|
+
without appending or modifying a user message. Its result has the prompt result's
|
|
741
|
+
fields except `userMessage`. Explicit steers still become user messages normally.
|
|
742
|
+
|
|
743
|
+
Admission returns `{ queueId, completion }` once queued, like `enqueuePrompt()`.
|
|
744
|
+
The entry and its `prompt.*` events carry `continuation: true`. Inspect its `runId`
|
|
745
|
+
after `prompt.started`, or read it through `getPromptQueueEntry()`. Use the same
|
|
746
|
+
`cancelPrompt()`, `cancelScheduledPrompt()`, `abort()` and `steerPrompt()` methods.
|
|
747
|
+
Queue bounds and FIFO order, permissions, resource cleanup, compaction, usage
|
|
748
|
+
accounting and provider limits follow the ordinary run path.
|
|
749
|
+
|
|
750
|
+
At the front of the FIFO, the harness validates the visible transcript and model
|
|
751
|
+
context with undone and excluded runs filtered out, before committing an undone
|
|
752
|
+
branch or reserving a generation or audit message. A refusal preserves redo history.
|
|
753
|
+
Empty context, an already answered turn, and an unsuccessful input that did not stay
|
|
754
|
+
in context refuse with `FlexHarnessValidationError` through `completion`. A retained
|
|
755
|
+
failure whose model context ends in partial assistant text with no tool call is also
|
|
756
|
+
refused; `keptInContext: true` alone does not guarantee it can be continued. An earlier
|
|
757
|
+
queued answer can therefore invalidate a waiting continuation. Cancellation before
|
|
758
|
+
execution leaves the retained input available for another continuation. Continuing never replays
|
|
759
|
+
previous tool calls; unknown external effects still require reconciliation.
|
|
760
|
+
|
|
761
|
+
Existing projections and Agent event stores remain readable without migration.
|
|
762
|
+
Assistant-only runs require two optional fields in the existing schema-3 projection:
|
|
763
|
+
a staged terminal omits `userMessage`, and its reversion segment omits
|
|
764
|
+
`userMessageId`. Requiring either would invent a user message. Generation claims
|
|
765
|
+
use existing events without a new `user-message` event. Older strict projection
|
|
766
|
+
readers cannot open a session containing these continuation records; do not mix
|
|
767
|
+
versions or downgrade those sessions.
|
|
768
|
+
|
|
720
769
|
### Steering a Running Turn
|
|
721
770
|
|
|
722
771
|
`steerPrompt()` hands a message to the running run of a prompt instead of queueing it behind that run:
|
|
@@ -823,7 +872,7 @@ Two horizons bound undo. Retention pruning removes the oldest complete visible u
|
|
|
823
872
|
|
|
824
873
|
Session metadata archival through `updateSession(..., { archived: true })` only sets `archivedAt`. It does not archive Agent events, retire captures, or remove undo history.
|
|
825
874
|
|
|
826
|
-
`executeSlashCommand()` is the authoritative parser and lookup boundary. Its result distinguishes `not-command`, `malformed`, `unknown`, completed `operation`, bounded `handler-result`, and `prompt-admission`. A prompt admission contains the normal `{ queueId, runId, completion }`; await `admission.completion` for the model result. Unknown commands are never admitted as literal prompts. Known unavailable commands and invalid arguments throw typed FlexHarness errors. Options accept `modelHint`, `system`, `maxSteps`, and `signal`; commands do not accept attachments. `/compact`
|
|
875
|
+
`executeSlashCommand()` is the authoritative parser and lookup boundary. Its result distinguishes `not-command`, `malformed`, `unknown`, completed `operation`, bounded `handler-result`, and `prompt-admission`. A prompt admission contains the normal `{ queueId, runId, completion }`; await `admission.completion` for the model result. Unknown commands are never admitted as literal prompts. Known unavailable commands and invalid arguments throw typed FlexHarness errors. Options accept `promptId`, `modelHint`, `system`, `maxSteps`, and `signal`; commands do not accept attachments. For templates and `/init`, `promptId` follows the normal prompt contract: a non-empty string of at most 512 UTF-8 bytes, recorded on the queue entry and native user message (including exact transcript reads after restart or with `includeUndone`), with duplicate outstanding ids refused by `FlexHarnessDuplicatePromptError`. Results that admit no prompt (`operation`, `handler-result`, `unknown`, `not-command`, `malformed`) ignore `promptId` without recording or reserving it. `/compact` uses `modelHint` and `signal` and refuses `system` and `maxSteps` with `FlexHarnessValidationError`. Aborting a template or `init` execution cancels its exact queued or started prompt without affecting another queue entry.
|
|
827
876
|
|
|
828
877
|
Templates replace every `$ARGUMENTS` with untouched raw argument text. `$1` through the highest referenced positional placeholder use tokenized arguments, with the highest position receiving all remaining tokens joined by spaces. Missing positions become empty. A template with no placeholders appends non-empty raw arguments after a blank line. `/init` uses the OpenCode 1.18.15 `AGENTS.md` initialization prompt with provider-neutral active-workspace wording.
|
|
829
878
|
|
|
@@ -1010,7 +1059,7 @@ if (started.status === 'started') {
|
|
|
1010
1059
|
|
|
1011
1060
|
**Read-only.** An imported session never runs, and nothing writes model context for it: it has no Agent events and no tool jobs. Every operation that would change its history or model context throws `FlexHarnessImportedSessionError` (code `FLEX_SESSION_READ_ONLY`):
|
|
1012
1061
|
|
|
1013
|
-
- `prompt`, `startPrompt`, `enqueuePrompt` and `
|
|
1062
|
+
- `prompt`, `startPrompt`, `enqueuePrompt`, `schedulePrompt` and `continuePrompt`;
|
|
1014
1063
|
- `executeSlashCommand` and `compactSession`;
|
|
1015
1064
|
- `undoSession` and `redoSession`;
|
|
1016
1065
|
- `pushRuntimeEvent`, `reconcileToolExecution` and `archiveSessionEvents`;
|
package/ts/utils.json.ts
CHANGED
|
@@ -762,6 +762,7 @@ interface IValidatedMessageIdentity {
|
|
|
762
762
|
runId: string;
|
|
763
763
|
role: 'user' | 'assistant';
|
|
764
764
|
status: 'streaming' | 'completed' | 'failed' | 'cancelled';
|
|
765
|
+
steerId?: string;
|
|
765
766
|
}
|
|
766
767
|
|
|
767
768
|
interface IValidatedSessionIdentity {
|
|
@@ -932,6 +933,7 @@ function validateMessage(value: unknown, path: string): IValidatedMessageIdentit
|
|
|
932
933
|
runId,
|
|
933
934
|
role: message.role,
|
|
934
935
|
status: message.status as IValidatedMessageIdentity['status'],
|
|
936
|
+
...(message.steerId === undefined ? {} : { steerId: message.steerId as string }),
|
|
935
937
|
};
|
|
936
938
|
}
|
|
937
939
|
|
|
@@ -1226,7 +1228,7 @@ export function assertFlexScopeSnapshot(value: unknown): asserts value is IFlexS
|
|
|
1226
1228
|
|
|
1227
1229
|
interface IValidatedProjectionBody {
|
|
1228
1230
|
messages: IValidatedMessageIdentity[];
|
|
1229
|
-
|
|
1231
|
+
stagedMessages: IValidatedMessageIdentity[];
|
|
1230
1232
|
}
|
|
1231
1233
|
|
|
1232
1234
|
function validateProjectionBody(snapshot: Record<string, unknown>): IValidatedProjectionBody {
|
|
@@ -1240,7 +1242,7 @@ function validateProjectionBody(snapshot: Record<string, unknown>): IValidatedPr
|
|
|
1240
1242
|
}
|
|
1241
1243
|
const runIds = new Set<string>();
|
|
1242
1244
|
const stagedMessageIds = new Set<string>();
|
|
1243
|
-
const
|
|
1245
|
+
const stagedMessages: IValidatedMessageIdentity[] = [];
|
|
1244
1246
|
for (let index = 0; index < snapshot.stagedTerminals.length; index++) {
|
|
1245
1247
|
const path = `$snapshot.stagedTerminals[${index}]`;
|
|
1246
1248
|
const terminal = requireRecord(snapshot.stagedTerminals[index], path);
|
|
@@ -1266,26 +1268,33 @@ function validateProjectionBody(snapshot: Record<string, unknown>): IValidatedPr
|
|
|
1266
1268
|
if (!['completed', 'failed', 'cancelled'].includes(String(terminal.status))) {
|
|
1267
1269
|
throw new FlexHarnessStoreFormatError(`${path}.status is invalid.`);
|
|
1268
1270
|
}
|
|
1269
|
-
const user =
|
|
1271
|
+
const user = terminal.userMessage === undefined
|
|
1272
|
+
? undefined
|
|
1273
|
+
: validateMessage(terminal.userMessage, `${path}.userMessage`);
|
|
1270
1274
|
const assistant = validateMessage(terminal.assistantMessage, `${path}.assistantMessage`);
|
|
1271
|
-
if (user.role !== 'user' || assistant.role !== 'assistant') {
|
|
1275
|
+
if ((user && user.role !== 'user') || assistant.role !== 'assistant') {
|
|
1272
1276
|
throw new FlexHarnessStoreFormatError(`${path} has invalid terminal message roles.`);
|
|
1273
1277
|
}
|
|
1274
|
-
if (user
|
|
1278
|
+
if ((user && (user.runId !== runId || user.sessionId !== assistant.sessionId)) || assistant.runId !== runId) {
|
|
1275
1279
|
throw new FlexHarnessStoreFormatError(`${path} has uncorrelated terminal messages.`);
|
|
1276
1280
|
}
|
|
1277
1281
|
if (assistant.status !== terminal.status) {
|
|
1278
1282
|
throw new FlexHarnessStoreFormatError(`${path}.status does not match its assistant message.`);
|
|
1279
1283
|
}
|
|
1280
|
-
if (user
|
|
1284
|
+
if (!user && visibleMessages.some((message) =>
|
|
1285
|
+
message.runId === runId && message.role === 'user' && message.steerId === undefined)) {
|
|
1286
|
+
throw new FlexHarnessStoreFormatError(`${path} omits its projected prompt message.`);
|
|
1287
|
+
}
|
|
1288
|
+
if (user && user.status !== terminal.status) {
|
|
1281
1289
|
throw new FlexHarnessStoreFormatError(`${path}.status does not match its user message.`);
|
|
1282
1290
|
}
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1291
|
+
if (user) stagedMessages.push(user);
|
|
1292
|
+
stagedMessages.push(assistant);
|
|
1293
|
+
projectionSessionId ??= assistant.sessionId;
|
|
1294
|
+
if (assistant.sessionId !== projectionSessionId) {
|
|
1286
1295
|
throw new FlexHarnessStoreFormatError(`${path} belongs to another session.`);
|
|
1287
1296
|
}
|
|
1288
|
-
for (const messageId of [user.messageId, assistant.messageId]) {
|
|
1297
|
+
for (const messageId of [...(user ? [user.messageId] : []), assistant.messageId]) {
|
|
1289
1298
|
if (stagedMessageIds.has(messageId)) {
|
|
1290
1299
|
throw new FlexHarnessStoreFormatError(
|
|
1291
1300
|
`Snapshot staged terminals contain duplicate message "${messageId}".`,
|
|
@@ -1298,7 +1307,7 @@ function validateProjectionBody(snapshot: Record<string, unknown>): IValidatedPr
|
|
|
1298
1307
|
requireOptionalString(terminal.finishReason, `${path}.finishReason`);
|
|
1299
1308
|
if (terminal.steps !== undefined) requireNonNegativeInteger(terminal.steps, `${path}.steps`);
|
|
1300
1309
|
}
|
|
1301
|
-
return { messages: visibleMessages,
|
|
1310
|
+
return { messages: visibleMessages, stagedMessages };
|
|
1302
1311
|
}
|
|
1303
1312
|
|
|
1304
1313
|
export function assertFlexProjectionSnapshotV1(
|
|
@@ -1358,7 +1367,7 @@ export function assertFlexProjectionSnapshotV2(
|
|
|
1358
1367
|
'workspaceReference',
|
|
1359
1368
|
], path);
|
|
1360
1369
|
const runId = requireString(segment.runId, `${path}.runId`);
|
|
1361
|
-
const userMessageId =
|
|
1370
|
+
const userMessageId = requireOptionalString(segment.userMessageId, `${path}.userMessageId`);
|
|
1362
1371
|
if (segmentRunIds.has(runId)) {
|
|
1363
1372
|
throw new FlexHarnessStoreFormatError(`Snapshot contains duplicate reversion run "${runId}".`);
|
|
1364
1373
|
}
|
|
@@ -1414,12 +1423,19 @@ export function assertFlexProjectionSnapshotV2(
|
|
|
1414
1423
|
throw new FlexHarnessStoreFormatError(`${path}.workspaceReference exceeds its byte limit.`);
|
|
1415
1424
|
}
|
|
1416
1425
|
}
|
|
1417
|
-
const
|
|
1426
|
+
const projectedMessages = [...projectionBody.messages, ...projectionBody.stagedMessages];
|
|
1427
|
+
if (userMessageId === undefined && projectedMessages.some((message) =>
|
|
1428
|
+
message.runId === runId && message.role === 'user' && message.steerId === undefined)) {
|
|
1429
|
+
throw new FlexHarnessStoreFormatError(`${path} omits its projected prompt identity.`);
|
|
1430
|
+
}
|
|
1431
|
+
const correlatedMessages = new Set(projectedMessages
|
|
1418
1432
|
.filter((message) =>
|
|
1419
|
-
message.
|
|
1433
|
+
message.runId === runId && (userMessageId === undefined
|
|
1434
|
+
? message.role === 'assistant'
|
|
1435
|
+
: message.role === 'user' && message.messageId === userMessageId))
|
|
1420
1436
|
.map((message) => `${message.runId}:${message.messageId}`));
|
|
1421
|
-
if (
|
|
1422
|
-
throw new FlexHarnessStoreFormatError(`${path} does not match
|
|
1437
|
+
if (userMessageId === undefined ? correlatedMessages.size === 0 : correlatedMessages.size !== 1) {
|
|
1438
|
+
throw new FlexHarnessStoreFormatError(`${path} does not match its projected turn messages.`);
|
|
1423
1439
|
}
|
|
1424
1440
|
}
|
|
1425
1441
|
requireNonNegativeInteger(snapshot.revertCursor, '$snapshot.revertCursor');
|
|
@@ -1843,7 +1859,7 @@ export function assertFlexProjectionSnapshotV3(
|
|
|
1843
1859
|
const segment = entry as Record<string, unknown>;
|
|
1844
1860
|
return {
|
|
1845
1861
|
runId: segment.runId,
|
|
1846
|
-
userMessageId: segment.userMessageId,
|
|
1862
|
+
...(segment.userMessageId === undefined ? {} : { userMessageId: segment.userMessageId }),
|
|
1847
1863
|
status: segment.status,
|
|
1848
1864
|
contextAvailable: segment.contextAvailable,
|
|
1849
1865
|
eventIds: segment.eventIds,
|
|
@@ -1901,7 +1917,7 @@ function assertProjectionSession(
|
|
|
1901
1917
|
}
|
|
1902
1918
|
for (const terminal of snapshot.stagedTerminals) {
|
|
1903
1919
|
if (
|
|
1904
|
-
terminal.userMessage.sessionId !== sessionId
|
|
1920
|
+
(terminal.userMessage !== undefined && terminal.userMessage.sessionId !== sessionId)
|
|
1905
1921
|
|| terminal.assistantMessage.sessionId !== sessionId
|
|
1906
1922
|
) {
|
|
1907
1923
|
throw new FlexHarnessStoreFormatError(
|
package/ts/utils.validation.ts
CHANGED
|
@@ -376,7 +376,7 @@ export function validateSlashCommandExecutionOptions(
|
|
|
376
376
|
throw new FlexHarnessValidationError('Slash command execution options must be a plain object.');
|
|
377
377
|
}
|
|
378
378
|
const unsupported = Object.keys(options)
|
|
379
|
-
.find((key) => !['modelHint', 'system', 'maxSteps', 'signal'].includes(key));
|
|
379
|
+
.find((key) => !['promptId', 'modelHint', 'system', 'maxSteps', 'signal'].includes(key));
|
|
380
380
|
if (unsupported) {
|
|
381
381
|
throw new FlexHarnessValidationError(
|
|
382
382
|
`Slash command execution options do not support "${unsupported}".`,
|
|
@@ -386,6 +386,7 @@ export function validateSlashCommandExecutionOptions(
|
|
|
386
386
|
throw new FlexHarnessValidationError('Slash command signal must be an AbortSignal.');
|
|
387
387
|
}
|
|
388
388
|
const promptOptions: IFlexPromptOptions = {
|
|
389
|
+
...(options.promptId === undefined ? {} : { promptId: options.promptId }),
|
|
389
390
|
...(options.modelHint === undefined ? {} : { modelHint: options.modelHint }),
|
|
390
391
|
...(options.system === undefined ? {} : { system: options.system }),
|
|
391
392
|
...(options.maxSteps === undefined ? {} : { maxSteps: options.maxSteps }),
|