@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
|
@@ -13,6 +13,8 @@ import {
|
|
|
13
13
|
} from './errors.js';
|
|
14
14
|
import type {
|
|
15
15
|
IFlexAgentContextInvocation,
|
|
16
|
+
IFlexContinuePromptOptions,
|
|
17
|
+
IFlexContinuePromptResult,
|
|
16
18
|
IFlexEnqueuePromptOptions,
|
|
17
19
|
IFlexSteerAdmission,
|
|
18
20
|
IFlexSteerInput,
|
|
@@ -24,6 +26,7 @@ import type {
|
|
|
24
26
|
IFlexScheduledPromptAdmission,
|
|
25
27
|
IFlexSchedulePromptOptions,
|
|
26
28
|
TFlexPrompt,
|
|
29
|
+
TFlexAgentPrompt,
|
|
27
30
|
} from './interfaces.js';
|
|
28
31
|
import { cloneSerializable } from './utils.json.js';
|
|
29
32
|
import { normalizeFlexPrompt, type INormalizedFlexPrompt } from './utils.prompt.js';
|
|
@@ -71,13 +74,15 @@ export class FlexHarnessPromptQueue<TScope> {
|
|
|
71
74
|
constructor(private readonly core: FlexHarnessCore<TScope>) {}
|
|
72
75
|
|
|
73
76
|
public promptAdmissionByteSize(
|
|
74
|
-
prompt: TFlexPrompt,
|
|
77
|
+
prompt: TFlexPrompt | undefined,
|
|
75
78
|
options: IFlexPromptOptions,
|
|
76
79
|
scheduleKey?: string,
|
|
77
80
|
debounceMs?: number,
|
|
78
81
|
): number {
|
|
79
82
|
return jsonBytes({
|
|
80
|
-
|
|
83
|
+
...(prompt === undefined
|
|
84
|
+
? {}
|
|
85
|
+
: { prompt: normalizeFlexPrompt(prompt, this.core.attachmentReferenceProtocols) }),
|
|
81
86
|
options: cloneSerializable(options),
|
|
82
87
|
scheduleKey,
|
|
83
88
|
debounceMs,
|
|
@@ -100,12 +105,52 @@ export class FlexHarnessPromptQueue<TScope> {
|
|
|
100
105
|
return admission.completion;
|
|
101
106
|
}
|
|
102
107
|
|
|
108
|
+
public async continuePrompt(
|
|
109
|
+
scopeId: string,
|
|
110
|
+
sessionId: string,
|
|
111
|
+
options: IFlexContinuePromptOptions = {},
|
|
112
|
+
): Promise<IFlexPromptQueueAdmission<IFlexContinuePromptResult>> {
|
|
113
|
+
if (!options || typeof options !== 'object' || Array.isArray(options)) {
|
|
114
|
+
throw new FlexHarnessValidationError('Continuation options must be a plain object.');
|
|
115
|
+
}
|
|
116
|
+
const { scheduleKey, debounceMs, ...enqueueOptions } = options;
|
|
117
|
+
if ('promptId' in enqueueOptions) {
|
|
118
|
+
throw new FlexHarnessValidationError('A continuation has no new prompt to identify.');
|
|
119
|
+
}
|
|
120
|
+
const admitted = this.splitEnqueueOptions(enqueueOptions);
|
|
121
|
+
if (scheduleKey === undefined && debounceMs !== undefined) {
|
|
122
|
+
throw new FlexHarnessValidationError('debounceMs requires scheduleKey.');
|
|
123
|
+
}
|
|
124
|
+
if (scheduleKey !== undefined) {
|
|
125
|
+
validateIdentifier(scheduleKey, 'scheduleKey');
|
|
126
|
+
requireTransferIdentifier(scheduleKey, 'scheduleKey');
|
|
127
|
+
this.validateScheduleDebounce(debounceMs ?? 50);
|
|
128
|
+
}
|
|
129
|
+
const queued = await this.enqueuePromptInternal(
|
|
130
|
+
scopeId, sessionId, undefined, admitted.options,
|
|
131
|
+
scheduleKey, scheduleKey === undefined ? undefined : debounceMs ?? 50,
|
|
132
|
+
undefined, undefined, false, false, admitted.steers,
|
|
133
|
+
);
|
|
134
|
+
return queued.admission;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private validateScheduleDebounce(debounceMs: number): void {
|
|
138
|
+
if (!Number.isSafeInteger(debounceMs) || debounceMs < 0 || debounceMs > maxScheduleDebounceMs) {
|
|
139
|
+
throw new FlexHarnessValidationError(
|
|
140
|
+
`debounceMs must be an integer from 0 through ${maxScheduleDebounceMs}.`,
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
103
145
|
public async startPrompt(
|
|
104
146
|
scopeId: string,
|
|
105
147
|
sessionId: string,
|
|
106
148
|
prompt: TFlexPrompt,
|
|
107
149
|
options: IFlexEnqueuePromptOptions = {},
|
|
108
150
|
): Promise<IFlexPromptAdmission> {
|
|
151
|
+
if (prompt === undefined) {
|
|
152
|
+
throw new FlexHarnessValidationError('prompt must be a string or a non-empty array of parts.');
|
|
153
|
+
}
|
|
109
154
|
const admitted = this.splitEnqueueOptions(options);
|
|
110
155
|
const queued = await this.enqueuePromptInternal(
|
|
111
156
|
scopeId, sessionId, prompt, admitted.options,
|
|
@@ -120,6 +165,9 @@ export class FlexHarnessPromptQueue<TScope> {
|
|
|
120
165
|
prompt: TFlexPrompt,
|
|
121
166
|
options: IFlexEnqueuePromptOptions = {},
|
|
122
167
|
): Promise<IFlexPromptQueueAdmission> {
|
|
168
|
+
if (prompt === undefined) {
|
|
169
|
+
throw new FlexHarnessValidationError('prompt must be a string or a non-empty array of parts.');
|
|
170
|
+
}
|
|
123
171
|
const admitted = this.splitEnqueueOptions(options);
|
|
124
172
|
const queued = await this.enqueuePromptInternal(
|
|
125
173
|
scopeId, sessionId, prompt, admitted.options,
|
|
@@ -166,15 +214,14 @@ export class FlexHarnessPromptQueue<TScope> {
|
|
|
166
214
|
prompt: TFlexPrompt,
|
|
167
215
|
options: IFlexSchedulePromptOptions = {},
|
|
168
216
|
): Promise<IFlexScheduledPromptAdmission> {
|
|
217
|
+
if (prompt === undefined) {
|
|
218
|
+
throw new FlexHarnessValidationError('prompt must be a string or a non-empty array of parts.');
|
|
219
|
+
}
|
|
169
220
|
validateIdentifier(scheduleKey, 'scheduleKey');
|
|
170
221
|
requireTransferIdentifier(scheduleKey, 'scheduleKey');
|
|
171
222
|
validatePromptOptions(options, true);
|
|
172
223
|
const debounceMs = options.debounceMs ?? 50;
|
|
173
|
-
|
|
174
|
-
throw new FlexHarnessValidationError(
|
|
175
|
-
`debounceMs must be an integer from 0 through ${maxScheduleDebounceMs}.`,
|
|
176
|
-
);
|
|
177
|
-
}
|
|
224
|
+
this.validateScheduleDebounce(debounceMs);
|
|
178
225
|
const queued = await this.enqueuePromptInternal(
|
|
179
226
|
scopeId,
|
|
180
227
|
sessionId,
|
|
@@ -331,9 +378,7 @@ export class FlexHarnessPromptQueue<TScope> {
|
|
|
331
378
|
try {
|
|
332
379
|
steering.input.add({
|
|
333
380
|
inputId: steer.steerId,
|
|
334
|
-
content: cloneSerializable(prompt.modelMessage.content) as
|
|
335
|
-
plugins.IAgentSession['beginGeneration']
|
|
336
|
-
>[0],
|
|
381
|
+
content: cloneSerializable(prompt.modelMessage.content) as TFlexAgentPrompt,
|
|
337
382
|
});
|
|
338
383
|
} catch (error) {
|
|
339
384
|
if (error instanceof plugins.AgentPendingInputRejectedError) {
|
|
@@ -355,10 +400,10 @@ export class FlexHarnessPromptQueue<TScope> {
|
|
|
355
400
|
return { steerId: steer.steerId, queueId: run.queueId, runId: run.runId };
|
|
356
401
|
}
|
|
357
402
|
|
|
358
|
-
public async enqueuePromptInternal(
|
|
403
|
+
public async enqueuePromptInternal<TPrompt extends TFlexPrompt | undefined>(
|
|
359
404
|
scopeId: string,
|
|
360
405
|
sessionId: string,
|
|
361
|
-
prompt:
|
|
406
|
+
prompt: TPrompt,
|
|
362
407
|
options: IFlexPromptOptions,
|
|
363
408
|
scheduleKey?: string,
|
|
364
409
|
debounceMs?: number,
|
|
@@ -368,13 +413,15 @@ export class FlexHarnessPromptQueue<TScope> {
|
|
|
368
413
|
cacheWarm = false,
|
|
369
414
|
steers: readonly IAdmittedSteer[] = [],
|
|
370
415
|
): Promise<{
|
|
371
|
-
admission: IFlexPromptQueueAdmission
|
|
372
|
-
started: Promise<IFlexPromptAdmission
|
|
416
|
+
admission: IFlexPromptQueueAdmission<TPrompt extends undefined ? IFlexContinuePromptResult : IFlexPromptResult>;
|
|
417
|
+
started: Promise<IFlexPromptAdmission<TPrompt extends undefined ? IFlexContinuePromptResult : IFlexPromptResult>>;
|
|
373
418
|
queued?: IQueuedPrompt;
|
|
374
419
|
}> {
|
|
375
420
|
this.core.assertOpen();
|
|
376
421
|
if (options.promptId !== undefined) requireTransferIdentifier(options.promptId, 'promptId');
|
|
377
|
-
const normalizedPrompt =
|
|
422
|
+
const normalizedPrompt = prompt === undefined
|
|
423
|
+
? undefined
|
|
424
|
+
: normalizeFlexPrompt(prompt, this.core.attachmentReferenceProtocols);
|
|
378
425
|
const normalizedOptions = cloneSerializable(options);
|
|
379
426
|
const byteSize = this.promptAdmissionByteSize(prompt, options, scheduleKey, debounceMs);
|
|
380
427
|
const steerBytes = steers.reduce((sum, steer) => sum + steer.byteSize, 0);
|
|
@@ -467,16 +514,16 @@ export class FlexHarnessPromptQueue<TScope> {
|
|
|
467
514
|
) {
|
|
468
515
|
throw new FlexHarnessDuplicatePromptError(options.promptId);
|
|
469
516
|
}
|
|
470
|
-
let resolveCompletion!: (result:
|
|
517
|
+
let resolveCompletion!: (result: IFlexContinuePromptResult) => void;
|
|
471
518
|
let rejectCompletion!: (error: unknown) => void;
|
|
472
|
-
const completion = new Promise<
|
|
519
|
+
const completion = new Promise<IFlexContinuePromptResult>((resolve, reject) => {
|
|
473
520
|
resolveCompletion = resolve;
|
|
474
521
|
rejectCompletion = reject;
|
|
475
522
|
});
|
|
476
523
|
void completion.catch(() => undefined);
|
|
477
|
-
let resolveStarted!: (admission: IFlexPromptAdmission) => void;
|
|
524
|
+
let resolveStarted!: (admission: IFlexPromptAdmission<IFlexContinuePromptResult>) => void;
|
|
478
525
|
let rejectStarted!: (error: unknown) => void;
|
|
479
|
-
const started = new Promise<IFlexPromptAdmission
|
|
526
|
+
const started = new Promise<IFlexPromptAdmission<IFlexContinuePromptResult>>((resolve, reject) => {
|
|
480
527
|
resolveStarted = resolve;
|
|
481
528
|
rejectStarted = reject;
|
|
482
529
|
});
|
|
@@ -494,7 +541,7 @@ export class FlexHarnessPromptQueue<TScope> {
|
|
|
494
541
|
...(scheduleKey === undefined ? {} : { scheduleKey, debounceMs }),
|
|
495
542
|
...(cacheWarm ? { cacheWarm: true as const } : {}),
|
|
496
543
|
...(options.promptId === undefined ? {} : { promptId: options.promptId }),
|
|
497
|
-
prompt: normalizedPrompt,
|
|
544
|
+
...(normalizedPrompt === undefined ? { continuation: true as const } : { prompt: normalizedPrompt }),
|
|
498
545
|
options: normalizedOptions,
|
|
499
546
|
...(delegationSeed === undefined ? {} : { delegationSeed }),
|
|
500
547
|
byteSize,
|
|
@@ -517,8 +564,13 @@ export class FlexHarnessPromptQueue<TScope> {
|
|
|
517
564
|
this.emitPromptQueueEvent(queued, 'prompt.queued');
|
|
518
565
|
this.schedulePromptQueueDrain(state, stored);
|
|
519
566
|
return {
|
|
520
|
-
|
|
521
|
-
|
|
567
|
+
// The input determines whether the completed run has a user message.
|
|
568
|
+
admission: Object.freeze({ queueId: queued.queueId, completion }) as IFlexPromptQueueAdmission<
|
|
569
|
+
TPrompt extends undefined ? IFlexContinuePromptResult : IFlexPromptResult
|
|
570
|
+
>,
|
|
571
|
+
started: started as Promise<IFlexPromptAdmission<
|
|
572
|
+
TPrompt extends undefined ? IFlexContinuePromptResult : IFlexPromptResult
|
|
573
|
+
>>,
|
|
522
574
|
queued,
|
|
523
575
|
};
|
|
524
576
|
} finally {
|
|
@@ -625,8 +677,28 @@ export class FlexHarnessPromptQueue<TScope> {
|
|
|
625
677
|
}
|
|
626
678
|
}
|
|
627
679
|
|
|
680
|
+
/** Check the actual model context at the FIFO boundary, before any durable run claim. */
|
|
681
|
+
private assertContinuableContext(stored: IStoredSessionState): void {
|
|
682
|
+
const visibleEvents = this.core.reversions.filterReversionContextEvents(stored, stored.agentSession.getEvents());
|
|
683
|
+
const retainedRunIds = new Set(plugins.filterModelVisibleAgentEvents(visibleEvents)
|
|
684
|
+
.map((event) => event.generationId));
|
|
685
|
+
const lastTurnMessage = [...this.core.visibleMessages(stored)].reverse().find((message) =>
|
|
686
|
+
!message.cacheWarm && (message.status === 'completed'
|
|
687
|
+
|| message.keptInContext === true || retainedRunIds.has(message.runId)));
|
|
688
|
+
// A summary or runtime update can look like a user message even after a completed answer.
|
|
689
|
+
const answered = !lastTurnMessage || (lastTurnMessage.role === 'assistant'
|
|
690
|
+
&& lastTurnMessage.status === 'completed');
|
|
691
|
+
const last = answered ? undefined : stored.agentSession.getModelMessages().at(-1);
|
|
692
|
+
if (last?.role === 'user' || last?.role === 'tool') return;
|
|
693
|
+
if (last?.role === 'assistant' && Array.isArray(last.content)
|
|
694
|
+
&& last.content.some((part) => part.type === 'tool-call')) return;
|
|
695
|
+
throw this.core.trustInternalError(new FlexHarnessValidationError(
|
|
696
|
+
'The conversation has no unanswered turn to continue.',
|
|
697
|
+
));
|
|
698
|
+
}
|
|
699
|
+
|
|
628
700
|
private async promoteQueuedPrompt(queued: IQueuedPrompt, run: IActiveRun): Promise<void> {
|
|
629
|
-
const prompt = queued.prompt
|
|
701
|
+
const prompt = queued.prompt;
|
|
630
702
|
const options = queued.options!;
|
|
631
703
|
let projectionReserved = false;
|
|
632
704
|
// Open from the reservation until its messages are announced or rolled back.
|
|
@@ -636,18 +708,18 @@ export class FlexHarnessPromptQueue<TScope> {
|
|
|
636
708
|
this.assertPromptPromotion(queued, run);
|
|
637
709
|
await this.core.admission.acquireDelegatedRunAdmission(queued, run);
|
|
638
710
|
this.assertPromptPromotion(queued, run);
|
|
711
|
+
if (queued.continuation) this.assertContinuableContext(run.stored);
|
|
639
712
|
await this.core.reversions.commitRevertedBranch(run.state, run.stored, run.scopeId, run.scope as TScope);
|
|
640
713
|
this.assertPromptPromotion(queued, run);
|
|
641
|
-
const generationPrompt =
|
|
642
|
-
|
|
643
|
-
|
|
714
|
+
const generationPrompt = prompt === undefined
|
|
715
|
+
? undefined
|
|
716
|
+
: cloneSerializable(prompt.modelMessage.content) as TFlexAgentPrompt;
|
|
644
717
|
this.assertPromptPromotion(queued, run);
|
|
645
718
|
run.transaction = await this.core.compaction.withRunCompactorContext(
|
|
646
719
|
run,
|
|
647
|
-
() =>
|
|
648
|
-
|
|
649
|
-
{ generationId: run.runId },
|
|
650
|
-
),
|
|
720
|
+
() => generationPrompt === undefined
|
|
721
|
+
? run.stored.agentSession.beginContinuation({ generationId: run.runId })
|
|
722
|
+
: run.stored.agentSession.beginGeneration(generationPrompt, { generationId: run.runId }),
|
|
651
723
|
);
|
|
652
724
|
this.assertPromptPromotion(queued, run);
|
|
653
725
|
const reservation = this.core.runs.createReservation(run, prompt, queued.promptId);
|
|
@@ -658,7 +730,8 @@ export class FlexHarnessPromptQueue<TScope> {
|
|
|
658
730
|
throw new Error('Session metrics already owns a generation.');
|
|
659
731
|
}
|
|
660
732
|
run.stored.metrics.pendingGenerationId = run.runId;
|
|
661
|
-
run.stored.messages.push(reservation.userMessage
|
|
733
|
+
if (reservation.userMessage) run.stored.messages.push(reservation.userMessage);
|
|
734
|
+
run.stored.messages.push(reservation.assistantMessage);
|
|
662
735
|
// A cache-warm turn is no turn of the user's: it gets no reversion segment of its own.
|
|
663
736
|
if (run.stored.session.agent === undefined && !run.cacheWarm) {
|
|
664
737
|
this.core.reversions.pruneReversionState(run.stored, true);
|
|
@@ -668,7 +741,7 @@ export class FlexHarnessPromptQueue<TScope> {
|
|
|
668
741
|
const protocolVersion = this.core.reversions.reversionProviderProtocolVersion() ?? 1;
|
|
669
742
|
run.stored.reversionSegments.push({
|
|
670
743
|
runId: run.runId,
|
|
671
|
-
userMessageId: reservation.userMessage.messageId,
|
|
744
|
+
...(reservation.userMessage ? { userMessageId: reservation.userMessage.messageId } : {}),
|
|
672
745
|
status: 'capturing',
|
|
673
746
|
contextAvailable: true,
|
|
674
747
|
eventIds: [],
|
|
@@ -690,7 +763,7 @@ export class FlexHarnessPromptQueue<TScope> {
|
|
|
690
763
|
}
|
|
691
764
|
});
|
|
692
765
|
projectionReserved = true;
|
|
693
|
-
run.reservedUserMessage = publicSnapshot(reservation.userMessage);
|
|
766
|
+
if (reservation.userMessage) run.reservedUserMessage = publicSnapshot(reservation.userMessage);
|
|
694
767
|
run.reservedAssistantMessage = publicSnapshot(reservation.assistantMessage);
|
|
695
768
|
this.assertPromptPromotion(queued, run);
|
|
696
769
|
await this.core.mutateScope(run.state, () => {
|
|
@@ -707,9 +780,9 @@ export class FlexHarnessPromptQueue<TScope> {
|
|
|
707
780
|
run.stored.session.activity = {
|
|
708
781
|
runId: run.runId,
|
|
709
782
|
status: run.scheduleKey ? 'scheduled' : 'running',
|
|
710
|
-
startedAt: reservation.
|
|
783
|
+
startedAt: reservation.assistantMessage.createdAt,
|
|
711
784
|
};
|
|
712
|
-
run.stored.session.updatedAt = reservation.
|
|
785
|
+
run.stored.session.updatedAt = reservation.assistantMessage.createdAt;
|
|
713
786
|
const preview = run.stored.session.preview ?? sessionPreview(run.stored.messages);
|
|
714
787
|
if (preview !== undefined) run.stored.session.preview = preview;
|
|
715
788
|
});
|
|
@@ -751,11 +824,12 @@ export class FlexHarnessPromptQueue<TScope> {
|
|
|
751
824
|
this.core.emitEvent(run.scopeId, run.stored.session, {
|
|
752
825
|
type: runType,
|
|
753
826
|
runId: run.runId,
|
|
754
|
-
messageId: run.userMessageId,
|
|
827
|
+
messageId: run.userMessageId ?? run.assistantMessageId,
|
|
755
828
|
session: publicSnapshot(run.stored.session),
|
|
756
829
|
...(run.cacheWarm ? { cacheWarm: true } : {}),
|
|
757
830
|
});
|
|
758
|
-
for (const message of [run.reservedUserMessage
|
|
831
|
+
for (const message of [run.reservedUserMessage, run.reservedAssistantMessage!]) {
|
|
832
|
+
if (!message) continue;
|
|
759
833
|
this.core.emitEvent(run.scopeId, run.stored.session, {
|
|
760
834
|
type: 'message.created',
|
|
761
835
|
runId: run.runId,
|
|
@@ -828,6 +902,7 @@ export class FlexHarnessPromptQueue<TScope> {
|
|
|
828
902
|
queuedAt: queued.queuedAt,
|
|
829
903
|
...(queued.runId ? { runId: queued.runId } : {}),
|
|
830
904
|
...(queued.promptId === undefined ? {} : { promptId: queued.promptId }),
|
|
905
|
+
...(queued.continuation ? { continuation: true as const } : {}),
|
|
831
906
|
...(queued.scheduleKey ? { scheduleKey: queued.scheduleKey } : {}),
|
|
832
907
|
...(queued.cacheWarm ? { cacheWarm: true } : {}),
|
|
833
908
|
...(queued.startedAt ? { startedAt: queued.startedAt } : {}),
|
|
@@ -861,7 +936,7 @@ export class FlexHarnessPromptQueue<TScope> {
|
|
|
861
936
|
queued: IQueuedPrompt,
|
|
862
937
|
status: 'completed' | 'failed' | 'cancelled',
|
|
863
938
|
error?: unknown,
|
|
864
|
-
result?:
|
|
939
|
+
result?: IFlexContinuePromptResult,
|
|
865
940
|
): void {
|
|
866
941
|
const stored = queued.stored;
|
|
867
942
|
if (stored.outstandingPromptsById.get(queued.queueId) !== queued) return;
|
|
@@ -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
|
|