@modelprofile.com/flexharness 15.0.0 → 16.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 +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 +23 -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/package.json +3 -3
- package/readme.md +51 -2
- 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 +26 -9
- package/ts/plugins.ts +1 -0
- package/ts/readme.md +51 -2
- package/ts/utils.json.ts +34 -18
|
@@ -16,7 +16,8 @@ import {
|
|
|
16
16
|
type IFlexMessage,
|
|
17
17
|
type IFlexPermissionRequestInput,
|
|
18
18
|
type IFlexPromptOptions,
|
|
19
|
-
type
|
|
19
|
+
type IFlexContinuePromptResult,
|
|
20
|
+
type TFlexAgentPrompt,
|
|
20
21
|
type IFlexResolvedModel,
|
|
21
22
|
type IFlexRunFinishedEvent,
|
|
22
23
|
type IFlexSession,
|
|
@@ -94,9 +95,7 @@ export class FlexHarnessRuns<TScope> {
|
|
|
94
95
|
for (const steer of steers) {
|
|
95
96
|
steering.input.add({
|
|
96
97
|
inputId: steer.steerId,
|
|
97
|
-
content: cloneSerializable(steer.prompt.modelMessage.content) as
|
|
98
|
-
plugins.IAgentSession['beginGeneration']
|
|
99
|
-
>[0],
|
|
98
|
+
content: cloneSerializable(steer.prompt.modelMessage.content) as TFlexAgentPrompt,
|
|
100
99
|
});
|
|
101
100
|
steering.accepted.set(steer.steerId, { prompt: steer.prompt, byteSize: steer.byteSize });
|
|
102
101
|
}
|
|
@@ -123,7 +122,7 @@ export class FlexHarnessRuns<TScope> {
|
|
|
123
122
|
sessionId: queued.sessionId,
|
|
124
123
|
queueId: queued.queueId,
|
|
125
124
|
runId: queued.runId!,
|
|
126
|
-
userMessageId: plugins.crypto.randomUUID(),
|
|
125
|
+
...(queued.continuation ? { continuation: true as const } : { userMessageId: plugins.crypto.randomUUID() }),
|
|
127
126
|
assistantMessageId: plugins.crypto.randomUUID(),
|
|
128
127
|
controller: new AbortController(),
|
|
129
128
|
...(queued.scheduleKey === undefined
|
|
@@ -162,7 +161,7 @@ export class FlexHarnessRuns<TScope> {
|
|
|
162
161
|
public async executeRun(
|
|
163
162
|
run: IActiveRun,
|
|
164
163
|
generation: Promise<plugins.IAgentGenerateResult>,
|
|
165
|
-
): Promise<
|
|
164
|
+
): Promise<IFlexContinuePromptResult> {
|
|
166
165
|
let generated = false;
|
|
167
166
|
try {
|
|
168
167
|
// The transactional generation settles only after its model calls are reported, so
|
|
@@ -236,7 +235,7 @@ export class FlexHarnessRuns<TScope> {
|
|
|
236
235
|
return {
|
|
237
236
|
runId: run.runId,
|
|
238
237
|
sessionId: run.sessionId,
|
|
239
|
-
userMessage: publicSnapshot(terminal.userMessage),
|
|
238
|
+
...(terminal.userMessage ? { userMessage: publicSnapshot(terminal.userMessage) } : {}),
|
|
240
239
|
assistantMessage: publicSnapshot(terminal.assistantMessage),
|
|
241
240
|
model: publicSnapshot(terminal.model!),
|
|
242
241
|
usage: publicSnapshot(terminal.usage!),
|
|
@@ -729,12 +728,12 @@ export class FlexHarnessRuns<TScope> {
|
|
|
729
728
|
|
|
730
729
|
public createReservation(
|
|
731
730
|
run: IActiveRun,
|
|
732
|
-
prompt: INormalizedFlexPrompt,
|
|
731
|
+
prompt: INormalizedFlexPrompt | undefined,
|
|
733
732
|
promptId?: string,
|
|
734
|
-
): { userMessage
|
|
733
|
+
): { userMessage?: IFlexMessage; assistantMessage: IFlexMessage } {
|
|
735
734
|
const timestamp = new Date().toISOString();
|
|
736
|
-
const userMessage: IFlexMessage = {
|
|
737
|
-
messageId: run.userMessageId
|
|
735
|
+
const userMessage: IFlexMessage | undefined = prompt === undefined ? undefined : {
|
|
736
|
+
messageId: run.userMessageId!,
|
|
738
737
|
sessionId: run.sessionId,
|
|
739
738
|
runId: run.runId,
|
|
740
739
|
role: 'user',
|
|
@@ -754,7 +753,7 @@ export class FlexHarnessRuns<TScope> {
|
|
|
754
753
|
parts: [],
|
|
755
754
|
...(run.cacheWarm ? { cacheWarm: true as const } : {}),
|
|
756
755
|
};
|
|
757
|
-
return { userMessage, assistantMessage };
|
|
756
|
+
return { ...(userMessage ? { userMessage } : {}), assistantMessage };
|
|
758
757
|
}
|
|
759
758
|
|
|
760
759
|
private userMessageParts(prompt: INormalizedFlexPrompt): TFlexMessagePart[] {
|
|
@@ -915,11 +914,11 @@ export class FlexHarnessRuns<TScope> {
|
|
|
915
914
|
error?: Error,
|
|
916
915
|
): IFlexTerminalProjection {
|
|
917
916
|
const timestamp = new Date().toISOString();
|
|
918
|
-
const userMessage
|
|
917
|
+
const userMessage = run.continuation ? undefined : cloneSerializable(
|
|
919
918
|
run.reservedUserMessage
|
|
920
919
|
?? run.stored.messages.find((message) => message.messageId === run.userMessageId)
|
|
921
920
|
?? {
|
|
922
|
-
messageId: run.userMessageId
|
|
921
|
+
messageId: run.userMessageId!,
|
|
923
922
|
sessionId: run.sessionId,
|
|
924
923
|
runId: run.runId,
|
|
925
924
|
role: 'user',
|
|
@@ -957,11 +956,13 @@ export class FlexHarnessRuns<TScope> {
|
|
|
957
956
|
}
|
|
958
957
|
}
|
|
959
958
|
if (run.cacheWarm) {
|
|
960
|
-
userMessage.cacheWarm = true;
|
|
959
|
+
if (userMessage) userMessage.cacheWarm = true;
|
|
961
960
|
assistantMessage.cacheWarm = true;
|
|
962
961
|
}
|
|
963
|
-
userMessage
|
|
964
|
-
|
|
962
|
+
if (userMessage) {
|
|
963
|
+
userMessage.status = status;
|
|
964
|
+
userMessage.completedAt = timestamp;
|
|
965
|
+
}
|
|
965
966
|
assistantMessage.status = status;
|
|
966
967
|
assistantMessage.completedAt = timestamp;
|
|
967
968
|
if (run.modelResolution) assistantMessage.model = cloneSerializable(run.modelResolution.identity);
|
|
@@ -984,6 +985,7 @@ export class FlexHarnessRuns<TScope> {
|
|
|
984
985
|
// A failed turn fails its prompt too. A stopped turn is no fault of its prompt, so the prompt
|
|
985
986
|
// stays an ordinary user message and only the answer it stopped carries the stop.
|
|
986
987
|
for (const failedMessage of status === 'cancelled' ? [assistantMessage] : [userMessage, assistantMessage]) {
|
|
988
|
+
if (!failedMessage) continue;
|
|
987
989
|
failedMessage.error = message;
|
|
988
990
|
if (limit) failedMessage.limit = cloneSerializable(limit);
|
|
989
991
|
if (rejectedPermissionId) failedMessage.rejectedPermissionId = rejectedPermissionId;
|
|
@@ -992,7 +994,7 @@ export class FlexHarnessRuns<TScope> {
|
|
|
992
994
|
return {
|
|
993
995
|
runId: run.runId,
|
|
994
996
|
status,
|
|
995
|
-
userMessage,
|
|
997
|
+
...(userMessage ? { userMessage } : {}),
|
|
996
998
|
assistantMessage,
|
|
997
999
|
...(run.modelResolution ? { model: cloneSerializable(run.modelResolution.identity) } : {}),
|
|
998
1000
|
...(status === 'completed' && result
|
|
@@ -1060,7 +1062,9 @@ export class FlexHarnessRuns<TScope> {
|
|
|
1060
1062
|
* the steers it took in in the model context. `replaceVisibleRun()` writes it onto the steers.
|
|
1061
1063
|
*/
|
|
1062
1064
|
public withKeptInContext(terminal: IFlexTerminalProjection, keptInContext: boolean): IFlexTerminalProjection {
|
|
1063
|
-
return
|
|
1065
|
+
return terminal.userMessage
|
|
1066
|
+
? { ...terminal, userMessage: { ...terminal.userMessage, keptInContext } }
|
|
1067
|
+
: terminal;
|
|
1064
1068
|
}
|
|
1065
1069
|
|
|
1066
1070
|
private replaceStagedTerminal(stored: IStoredSessionState, terminal: IFlexTerminalProjection): void {
|
|
@@ -1076,11 +1080,13 @@ export class FlexHarnessRuns<TScope> {
|
|
|
1076
1080
|
*/
|
|
1077
1081
|
public replaceVisibleRun(stored: IStoredSessionState, terminal: IFlexTerminalProjection): void {
|
|
1078
1082
|
const replacements = new Map([
|
|
1079
|
-
[terminal.userMessage.messageId, terminal.userMessage],
|
|
1083
|
+
...(terminal.userMessage ? [[terminal.userMessage.messageId, terminal.userMessage] as const] : []),
|
|
1080
1084
|
[terminal.assistantMessage.messageId, terminal.assistantMessage],
|
|
1081
1085
|
]);
|
|
1082
1086
|
const placed = new Set<string>();
|
|
1083
|
-
const keptInContext = terminal.userMessage
|
|
1087
|
+
const keptInContext = terminal.userMessage?.keptInContext
|
|
1088
|
+
?? (terminal.status === 'completed'
|
|
1089
|
+
|| plugins.isGenerationInModelContext(stored.agentSession.getEvents(), terminal.runId));
|
|
1084
1090
|
stored.messages = stored.messages.map((message) => {
|
|
1085
1091
|
const replacement = message.runId === terminal.runId
|
|
1086
1092
|
? replacements.get(message.messageId)
|
|
@@ -1096,7 +1102,7 @@ export class FlexHarnessRuns<TScope> {
|
|
|
1096
1102
|
return cloneSerializable(replacement);
|
|
1097
1103
|
});
|
|
1098
1104
|
const firstIndex = stored.messages.findIndex((message) => message.runId === terminal.runId);
|
|
1099
|
-
if (!placed.has(terminal.userMessage.messageId)) {
|
|
1105
|
+
if (terminal.userMessage && !placed.has(terminal.userMessage.messageId)) {
|
|
1100
1106
|
stored.messages.splice(
|
|
1101
1107
|
firstIndex < 0 ? stored.messages.length : firstIndex,
|
|
1102
1108
|
0,
|
|
@@ -1130,10 +1136,14 @@ export class FlexHarnessRuns<TScope> {
|
|
|
1130
1136
|
this.core.emitPartEvent(run, 'part.completed', part);
|
|
1131
1137
|
}
|
|
1132
1138
|
}
|
|
1133
|
-
const
|
|
1134
|
-
?
|
|
1135
|
-
: run.
|
|
1139
|
+
const retentionKnown = terminal.userMessage
|
|
1140
|
+
? terminal.userMessage.keptInContext !== undefined
|
|
1141
|
+
: run.steering.keptInContext !== undefined;
|
|
1142
|
+
const steerMessages = retentionKnown
|
|
1143
|
+
? run.stored.messages.filter((message) => message.runId === run.runId && message.steerId !== undefined)
|
|
1144
|
+
: [];
|
|
1136
1145
|
for (const message of [terminal.userMessage, ...steerMessages, terminal.assistantMessage]) {
|
|
1146
|
+
if (!message) continue;
|
|
1137
1147
|
this.core.emitEvent(run.scopeId, run.stored.session, {
|
|
1138
1148
|
type: 'message.updated',
|
|
1139
1149
|
runId: run.runId,
|
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
IFlexSteerAdmission,
|
|
14
14
|
IFlexSteerInput,
|
|
15
15
|
IFlexPromptAdmission,
|
|
16
|
+
IFlexContinuePromptResult,
|
|
16
17
|
IFlexPromptOptions,
|
|
17
18
|
IFlexSession,
|
|
18
19
|
IFlexSubagentDefinition,
|
|
@@ -378,7 +379,7 @@ export class FlexHarnessSubagents<TScope> {
|
|
|
378
379
|
let child: IStoredSessionState | undefined;
|
|
379
380
|
let childCreated = false;
|
|
380
381
|
let queued: Awaited<ReturnType<typeof this.core.prompts.enqueuePromptInternal>> | undefined;
|
|
381
|
-
let admission: IFlexPromptAdmission | undefined;
|
|
382
|
+
let admission: IFlexPromptAdmission<IFlexContinuePromptResult> | undefined;
|
|
382
383
|
const abortChild = () => {
|
|
383
384
|
const reason = delegatedCancellation(run.controller.signal.reason)
|
|
384
385
|
?? this.core.trustInternalError(new FlexHarnessAbortError('The parent run was aborted.'));
|
|
@@ -4,6 +4,8 @@ import type {
|
|
|
4
4
|
IFlexCacheWarmInput,
|
|
5
5
|
IFlexCacheWarmState,
|
|
6
6
|
IFlexCompactSessionOptions,
|
|
7
|
+
IFlexContinuePromptOptions,
|
|
8
|
+
IFlexContinuePromptResult,
|
|
7
9
|
IFlexCreateProjectTaskInput,
|
|
8
10
|
IFlexCreateSessionOptions,
|
|
9
11
|
IFlexDeleteSessionGenerationCohortInput,
|
|
@@ -320,6 +322,14 @@ export class FlexHarness<TScope = unknown> {
|
|
|
320
322
|
return this.core.prompts.prompt(scopeId, sessionId, prompt, options);
|
|
321
323
|
}
|
|
322
324
|
|
|
325
|
+
public continuePrompt(
|
|
326
|
+
scopeId: string,
|
|
327
|
+
sessionId: string,
|
|
328
|
+
options: IFlexContinuePromptOptions = {},
|
|
329
|
+
): Promise<IFlexPromptQueueAdmission<IFlexContinuePromptResult>> {
|
|
330
|
+
return this.core.prompts.continuePrompt(scopeId, sessionId, options);
|
|
331
|
+
}
|
|
332
|
+
|
|
323
333
|
public startPrompt(
|
|
324
334
|
scopeId: string,
|
|
325
335
|
sessionId: string,
|
|
@@ -15,7 +15,7 @@ import type {
|
|
|
15
15
|
IFlexPromptAdmission,
|
|
16
16
|
IFlexPromptQueueEntry,
|
|
17
17
|
IFlexPromptOptions,
|
|
18
|
-
|
|
18
|
+
IFlexContinuePromptResult,
|
|
19
19
|
IFlexReversionSegment,
|
|
20
20
|
IFlexPendingReversionRelease,
|
|
21
21
|
IFlexResolvedModel,
|
|
@@ -88,7 +88,7 @@ export interface IStoredSessionState {
|
|
|
88
88
|
permissionRevision: number;
|
|
89
89
|
permissionQueue: Promise<void>;
|
|
90
90
|
eventStore: plugins.IAgentEventStore;
|
|
91
|
-
agentSession: plugins.
|
|
91
|
+
agentSession: plugins.IAgentContinuationSession;
|
|
92
92
|
agentSessionAbortCompleted: boolean;
|
|
93
93
|
agentSessionCloseCompleted: boolean;
|
|
94
94
|
agentEventStoreReleased: boolean;
|
|
@@ -226,7 +226,8 @@ export interface IActiveRun {
|
|
|
226
226
|
sessionId: string;
|
|
227
227
|
queueId: string;
|
|
228
228
|
runId: string;
|
|
229
|
-
userMessageId
|
|
229
|
+
userMessageId?: string;
|
|
230
|
+
continuation?: true;
|
|
230
231
|
assistantMessageId: string;
|
|
231
232
|
controller: AbortController;
|
|
232
233
|
transaction?: plugins.IAgentGenerationHandle;
|
|
@@ -292,7 +293,7 @@ export interface IActiveRun {
|
|
|
292
293
|
permissionPolicy?: IFlexResolvedSessionConfiguration<unknown>['permissionPolicy'];
|
|
293
294
|
delegatedRunAdmissionOwner?: IDelegatedRunAdmissionOwner;
|
|
294
295
|
delegatedRunAdmissionClose?: Promise<void>;
|
|
295
|
-
completion: Promise<
|
|
296
|
+
completion: Promise<IFlexContinuePromptResult>;
|
|
296
297
|
steering: IRunSteering;
|
|
297
298
|
/** Stops the run's generation for its owner, so what the model produced stays in context. */
|
|
298
299
|
stop: AbortController;
|
|
@@ -415,6 +416,7 @@ export interface IQueuedPrompt {
|
|
|
415
416
|
/** The prompt's `promptId`; kept after its payload is released. */
|
|
416
417
|
promptId?: string;
|
|
417
418
|
startedAt?: string;
|
|
419
|
+
continuation?: true;
|
|
418
420
|
prompt?: INormalizedFlexPrompt;
|
|
419
421
|
options?: IFlexPromptOptions;
|
|
420
422
|
delegationSeed?: IDelegatedRunAdmissionSeed;
|
|
@@ -426,11 +428,11 @@ export interface IQueuedPrompt {
|
|
|
426
428
|
steering?: IRunSteering;
|
|
427
429
|
/** The steers admitted with the prompt, in their given order. */
|
|
428
430
|
admittedSteerIds?: readonly string[];
|
|
429
|
-
completion: Promise<
|
|
430
|
-
resolveCompletion: (result:
|
|
431
|
+
completion: Promise<IFlexContinuePromptResult>;
|
|
432
|
+
resolveCompletion: (result: IFlexContinuePromptResult) => void;
|
|
431
433
|
rejectCompletion: (error: unknown) => void;
|
|
432
|
-
started: Promise<IFlexPromptAdmission
|
|
433
|
-
resolveStarted: (admission: IFlexPromptAdmission) => void;
|
|
434
|
+
started: Promise<IFlexPromptAdmission<IFlexContinuePromptResult>>;
|
|
435
|
+
resolveStarted: (admission: IFlexPromptAdmission<IFlexContinuePromptResult>) => void;
|
|
434
436
|
rejectStarted: (error: unknown) => void;
|
|
435
437
|
}
|
|
436
438
|
|
package/ts/interfaces.ts
CHANGED
|
@@ -600,6 +600,8 @@ export interface IFlexPromptQueueEntry {
|
|
|
600
600
|
/** The prompt's `promptId`, when it named one. */
|
|
601
601
|
promptId?: string;
|
|
602
602
|
scheduleKey?: string;
|
|
603
|
+
/** This entry answers existing context without adding a prompt message. */
|
|
604
|
+
continuation?: true;
|
|
603
605
|
/** The entry is a cache-warm prompt; see {@link IFlexMessage.cacheWarm}. */
|
|
604
606
|
cacheWarm?: true;
|
|
605
607
|
startedAt?: string;
|
|
@@ -608,8 +610,9 @@ export interface IFlexPromptQueueEntry {
|
|
|
608
610
|
/**
|
|
609
611
|
* On a finished entry: whether its prompt, the steers its run took in and what its model
|
|
610
612
|
* 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.
|
|
613
|
+
* stopped or failed after its model produced output; false for an unsuccessful run without
|
|
614
|
+
* model output and a prompt that never ran. For a continuation, this describes only the new
|
|
615
|
+
* run's history; it does not change the retention of the existing input.
|
|
613
616
|
*/
|
|
614
617
|
keptInContext?: boolean;
|
|
615
618
|
/**
|
|
@@ -620,10 +623,10 @@ export interface IFlexPromptQueueEntry {
|
|
|
620
623
|
unappliedSteerIds?: string[];
|
|
621
624
|
}
|
|
622
625
|
|
|
623
|
-
|
|
626
|
+
/** The result of answering existing context, without adding a prompt message. */
|
|
627
|
+
export interface IFlexContinuePromptResult {
|
|
624
628
|
runId: string;
|
|
625
629
|
sessionId: string;
|
|
626
|
-
userMessage: IFlexMessage;
|
|
627
630
|
/** The run's last assistant message; a steered run completed earlier ones as it took steers in. */
|
|
628
631
|
assistantMessage: IFlexMessage;
|
|
629
632
|
model: IFlexModelIdentity;
|
|
@@ -634,6 +637,10 @@ export interface IFlexPromptResult {
|
|
|
634
637
|
unappliedSteerIds: string[];
|
|
635
638
|
}
|
|
636
639
|
|
|
640
|
+
export interface IFlexPromptResult extends IFlexContinuePromptResult {
|
|
641
|
+
userMessage: IFlexMessage;
|
|
642
|
+
}
|
|
643
|
+
|
|
637
644
|
/** A message for the running run of a session, taken in at the run's next step boundary. */
|
|
638
645
|
export interface IFlexSteerInput {
|
|
639
646
|
/** Caller-chosen identity, unique among the steers of the run. */
|
|
@@ -647,12 +654,13 @@ export interface IFlexSteerAdmission {
|
|
|
647
654
|
runId: string;
|
|
648
655
|
}
|
|
649
656
|
|
|
650
|
-
export interface IFlexPromptQueueAdmission {
|
|
657
|
+
export interface IFlexPromptQueueAdmission<TResult extends IFlexContinuePromptResult = IFlexPromptResult> {
|
|
651
658
|
queueId: string;
|
|
652
|
-
completion: Promise<
|
|
659
|
+
completion: Promise<TResult>;
|
|
653
660
|
}
|
|
654
661
|
|
|
655
|
-
export interface IFlexPromptAdmission extends
|
|
662
|
+
export interface IFlexPromptAdmission<TResult extends IFlexContinuePromptResult = IFlexPromptResult>
|
|
663
|
+
extends IFlexPromptQueueAdmission<TResult> {
|
|
656
664
|
runId: string;
|
|
657
665
|
}
|
|
658
666
|
|
|
@@ -673,6 +681,13 @@ export interface IFlexEnqueuePromptOptions extends IFlexPromptOptions {
|
|
|
673
681
|
steers?: readonly IFlexSteerInput[];
|
|
674
682
|
}
|
|
675
683
|
|
|
684
|
+
/** Options for continuation through the same FIFO, optionally with scheduled execution. */
|
|
685
|
+
export interface IFlexContinuePromptOptions extends Omit<IFlexEnqueuePromptOptions, 'promptId'> {
|
|
686
|
+
scheduleKey?: string;
|
|
687
|
+
/** Requires scheduleKey; defaults to 50 ms when scheduled. */
|
|
688
|
+
debounceMs?: number;
|
|
689
|
+
}
|
|
690
|
+
|
|
676
691
|
export interface IFlexScheduledPromptAdmission extends IFlexPromptAdmission {
|
|
677
692
|
scheduleKey: string;
|
|
678
693
|
}
|
|
@@ -1260,7 +1275,8 @@ export interface IFlexScopeSnapshot {
|
|
|
1260
1275
|
export interface IFlexTerminalProjection {
|
|
1261
1276
|
runId: string;
|
|
1262
1277
|
status: 'completed' | 'failed' | 'cancelled';
|
|
1263
|
-
|
|
1278
|
+
/** Absent for a continuation, which creates no prompt message. */
|
|
1279
|
+
userMessage?: IFlexMessage;
|
|
1264
1280
|
assistantMessage: IFlexMessage;
|
|
1265
1281
|
model?: IFlexModelIdentity;
|
|
1266
1282
|
/** The usage the provider reported for the run's model calls, whatever the status. */
|
|
@@ -1288,7 +1304,8 @@ export interface IFlexAffectedWorkspace {
|
|
|
1288
1304
|
|
|
1289
1305
|
interface IFlexReversionSegmentV2 {
|
|
1290
1306
|
runId: string;
|
|
1291
|
-
|
|
1307
|
+
/** Absent for an assistant-only continuation segment. */
|
|
1308
|
+
userMessageId?: string;
|
|
1292
1309
|
status: TFlexReversionSegmentStatus;
|
|
1293
1310
|
contextAvailable: boolean;
|
|
1294
1311
|
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:
|
|
@@ -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(
|