@modelprofile.com/flexharness 14.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.d.ts +8 -1
- package/dist_ts/classes.flexharness.core.js +44 -31
- package/dist_ts/classes.flexharness.d.ts +5 -4
- package/dist_ts/classes.flexharness.js +10 -7
- package/dist_ts/classes.flexharness.prompts.d.ts +9 -5
- package/dist_ts/classes.flexharness.prompts.js +93 -18
- package/dist_ts/classes.flexharness.runs.d.ts +9 -4
- package/dist_ts/classes.flexharness.runs.js +70 -18
- package/dist_ts/classes.flexharness.sessions.d.ts +4 -5
- package/dist_ts/classes.flexharness.sessions.js +45 -12
- package/dist_ts/classes.flexharness.subagents.js +1 -1
- package/dist_ts/errors.d.ts +9 -0
- package/dist_ts/errors.js +14 -1
- package/dist_ts/index.d.ts +2 -0
- package/dist_ts/index.js +2 -1
- package/dist_ts/interfaces.d.ts +119 -11
- package/dist_ts/interfaces.flexharness.d.ts +12 -8
- package/dist_ts/interfaces.js +1 -1
- package/dist_ts/plugins.d.ts +3 -3
- package/dist_ts/plugins.js +3 -3
- package/dist_ts/utils.harnessoptions.js +6 -3
- package/dist_ts/utils.json.js +74 -18
- package/dist_ts/utils.sessionmetrics.d.ts +12 -1
- package/dist_ts/utils.sessionmetrics.js +18 -1
- package/dist_ts/utils.transfer.js +14 -1
- package/dist_ts/utils.validation.d.ts +7 -2
- package/dist_ts/utils.validation.js +28 -7
- package/package.json +3 -3
- package/readme.md +75 -6
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.flexharness.cachewarm.ts +2 -2
- package/ts/classes.flexharness.core.ts +49 -30
- package/ts/classes.flexharness.prompts.ts +124 -39
- package/ts/classes.flexharness.runs.ts +78 -24
- package/ts/classes.flexharness.sessions.ts +54 -9
- package/ts/classes.flexharness.subagents.ts +2 -1
- package/ts/classes.flexharness.ts +25 -5
- package/ts/errors.ts +15 -0
- package/ts/index.ts +6 -0
- package/ts/interfaces.flexharness.ts +12 -8
- package/ts/interfaces.ts +125 -11
- package/ts/plugins.ts +13 -0
- package/ts/readme.md +75 -6
- package/ts/utils.harnessoptions.ts +9 -2
- package/ts/utils.json.ts +75 -18
- package/ts/utils.sessionmetrics.ts +23 -0
- package/ts/utils.transfer.ts +13 -0
- package/ts/utils.validation.ts +31 -5
|
@@ -542,7 +542,7 @@ export class FlexHarnessCore<TScope> {
|
|
|
542
542
|
storageKey: string,
|
|
543
543
|
compactorLifecycleController = new AbortController(),
|
|
544
544
|
): IStoredSessionState {
|
|
545
|
-
const unavailable = new Proxy({} as plugins.
|
|
545
|
+
const unavailable = new Proxy({} as plugins.IAgentContinuationSession, {
|
|
546
546
|
get() {
|
|
547
547
|
throw new Error('AgentSession is not initialized.');
|
|
548
548
|
},
|
|
@@ -699,7 +699,7 @@ export class FlexHarnessCore<TScope> {
|
|
|
699
699
|
permissionRevision: permission?.revision ?? 0,
|
|
700
700
|
permissionQueue: Promise.resolve(),
|
|
701
701
|
eventStore,
|
|
702
|
-
agentSession: undefined as unknown as plugins.
|
|
702
|
+
agentSession: undefined as unknown as plugins.IAgentContinuationSession,
|
|
703
703
|
agentSessionAbortCompleted: false,
|
|
704
704
|
agentSessionCloseCompleted: false,
|
|
705
705
|
agentEventStoreReleased: this.stores.agentEvents.releaseSession === undefined,
|
|
@@ -728,8 +728,10 @@ export class FlexHarnessCore<TScope> {
|
|
|
728
728
|
delete stored.metrics.pendingCompactionId;
|
|
729
729
|
});
|
|
730
730
|
}
|
|
731
|
+
// The compaction setting reaches each generation on its lease, with its model's window.
|
|
731
732
|
const {
|
|
732
733
|
contextCompactor,
|
|
734
|
+
contextCompaction: _contextCompaction,
|
|
733
735
|
...agentSessionPolicy
|
|
734
736
|
} = this.agentSessionPolicy;
|
|
735
737
|
const executionContext = executionContextHandle?.context;
|
|
@@ -959,7 +961,8 @@ export class FlexHarnessCore<TScope> {
|
|
|
959
961
|
await this.reversions.drainReversionReleases(state, stored, scopeId, scope, false);
|
|
960
962
|
this.assertStateAcceptingWork(state);
|
|
961
963
|
}
|
|
962
|
-
const
|
|
964
|
+
const agentEvents = stored.agentSession.getEvents();
|
|
965
|
+
const outcomes = this.canonicalOutcomes(agentEvents);
|
|
963
966
|
const firstHiddenCandidate = this.reversions.completedReversionCandidates(stored)[stored.revertCursor];
|
|
964
967
|
const reversionVisibilityBoundary = firstHiddenCandidate
|
|
965
968
|
? stored.reversionSegments.findIndex((segment) => segment.runId === firstHiddenCandidate.runId)
|
|
@@ -975,8 +978,8 @@ export class FlexHarnessCore<TScope> {
|
|
|
975
978
|
const outcome = outcomes.get(runId);
|
|
976
979
|
const stage = stages.get(runId);
|
|
977
980
|
if (outcome === 'accepted' && stage?.status === 'completed') {
|
|
978
|
-
this.runs.replaceVisibleRun(stored, stage);
|
|
979
|
-
if (!stage.
|
|
981
|
+
this.runs.replaceVisibleRun(stored, this.runs.withKeptInContext(stage, true));
|
|
982
|
+
if (!stage.assistantMessage.cacheWarm) latestTerminal = stage;
|
|
980
983
|
} else if (outcome !== 'accepted') {
|
|
981
984
|
const visible = stored.messages.filter((message) => message.runId === runId);
|
|
982
985
|
// A run that failed or was stopped keeps its terminal and its error: the staged one, or the
|
|
@@ -985,13 +988,21 @@ export class FlexHarnessCore<TScope> {
|
|
|
985
988
|
? stage
|
|
986
989
|
: stage ? undefined : this.publishedTerminal(runId, visible);
|
|
987
990
|
if (ended) {
|
|
988
|
-
if (ended === stage)
|
|
989
|
-
|
|
991
|
+
if (ended === stage) {
|
|
992
|
+
this.runs.replaceVisibleRun(stored, this.runs.withKeptInContext(
|
|
993
|
+
stage,
|
|
994
|
+
plugins.isGenerationInModelContext(agentEvents, runId),
|
|
995
|
+
));
|
|
996
|
+
}
|
|
997
|
+
if (!ended.assistantMessage.cacheWarm) latestTerminal = ended;
|
|
990
998
|
} else if (visible.length > 0 || stage) {
|
|
991
999
|
const status = outcome === 'rejected' ? 'failed' : 'cancelled';
|
|
992
|
-
const terminal = this.
|
|
1000
|
+
const terminal = this.runs.withKeptInContext(
|
|
1001
|
+
this.repairTerminal(stored.session.sessionId, runId, visible, stage, status),
|
|
1002
|
+
plugins.isGenerationInModelContext(agentEvents, runId),
|
|
1003
|
+
);
|
|
993
1004
|
this.runs.replaceVisibleRun(stored, terminal);
|
|
994
|
-
if (!terminal.
|
|
1005
|
+
if (!terminal.assistantMessage.cacheWarm) latestTerminal = terminal;
|
|
995
1006
|
}
|
|
996
1007
|
} else {
|
|
997
1008
|
const visibleSuccess = stored.messages.some((message) =>
|
|
@@ -1004,7 +1015,7 @@ export class FlexHarnessCore<TScope> {
|
|
|
1004
1015
|
undefined,
|
|
1005
1016
|
'completed',
|
|
1006
1017
|
);
|
|
1007
|
-
if (!terminal.
|
|
1018
|
+
if (!terminal.assistantMessage.cacheWarm) latestTerminal = terminal;
|
|
1008
1019
|
}
|
|
1009
1020
|
}
|
|
1010
1021
|
}
|
|
@@ -1109,13 +1120,13 @@ export class FlexHarnessCore<TScope> {
|
|
|
1109
1120
|
if (visible.some((message) => message.status === 'streaming')) return undefined;
|
|
1110
1121
|
const userMessage = visible.find((message) => message.role === 'user' && message.steerId === undefined);
|
|
1111
1122
|
const assistantMessage = [...visible].reverse().find((message) => message.role === 'assistant');
|
|
1112
|
-
if (!
|
|
1123
|
+
if (!assistantMessage) return undefined;
|
|
1113
1124
|
const status = assistantMessage.status;
|
|
1114
1125
|
if (status !== 'failed' && status !== 'cancelled') return undefined;
|
|
1115
1126
|
return {
|
|
1116
1127
|
runId,
|
|
1117
1128
|
status,
|
|
1118
|
-
userMessage,
|
|
1129
|
+
...(userMessage ? { userMessage } : {}),
|
|
1119
1130
|
assistantMessage,
|
|
1120
1131
|
...(assistantMessage.model ? { model: assistantMessage.model } : {}),
|
|
1121
1132
|
...(assistantMessage.usage ? { usage: assistantMessage.usage } : {}),
|
|
@@ -1136,15 +1147,7 @@ export class FlexHarnessCore<TScope> {
|
|
|
1136
1147
|
?? visible.find((message) => message.role === 'user' && message.steerId === undefined);
|
|
1137
1148
|
const sourceAssistant = stage?.assistantMessage
|
|
1138
1149
|
?? [...visible].reverse().find((message) => message.role === 'assistant');
|
|
1139
|
-
const userMessage
|
|
1140
|
-
messageId: plugins.crypto.randomUUID(),
|
|
1141
|
-
sessionId,
|
|
1142
|
-
runId,
|
|
1143
|
-
role: 'user',
|
|
1144
|
-
status,
|
|
1145
|
-
createdAt: timestamp,
|
|
1146
|
-
parts: [],
|
|
1147
|
-
});
|
|
1150
|
+
const userMessage = sourceUser ? cloneSerializable(sourceUser) : undefined;
|
|
1148
1151
|
const assistantMessage: IFlexMessage = cloneSerializable(sourceAssistant ?? {
|
|
1149
1152
|
messageId: plugins.crypto.randomUUID(),
|
|
1150
1153
|
sessionId,
|
|
@@ -1154,9 +1157,9 @@ export class FlexHarnessCore<TScope> {
|
|
|
1154
1157
|
createdAt: timestamp,
|
|
1155
1158
|
parts: [],
|
|
1156
1159
|
});
|
|
1157
|
-
userMessage.status = status;
|
|
1160
|
+
if (userMessage) userMessage.status = status;
|
|
1158
1161
|
assistantMessage.status = status;
|
|
1159
|
-
userMessage.completedAt ??= timestamp;
|
|
1162
|
+
if (userMessage) userMessage.completedAt ??= timestamp;
|
|
1160
1163
|
assistantMessage.completedAt ??= timestamp;
|
|
1161
1164
|
for (const part of assistantMessage.parts) {
|
|
1162
1165
|
if (part.type === 'reasoning' && part.status === 'running') {
|
|
@@ -1172,7 +1175,7 @@ export class FlexHarnessCore<TScope> {
|
|
|
1172
1175
|
if (status !== 'completed') {
|
|
1173
1176
|
const message = status === 'cancelled' ? repairCancellationMessage : externalErrorFallback.message;
|
|
1174
1177
|
// As for a run that ends live: a failed run marks its prompt too, a cancelled one only its answer.
|
|
1175
|
-
if (status === 'failed') userMessage.error = message;
|
|
1178
|
+
if (status === 'failed' && userMessage) userMessage.error = message;
|
|
1176
1179
|
assistantMessage.error = message;
|
|
1177
1180
|
if (stage?.usage) {
|
|
1178
1181
|
assistantMessage.usage = cloneSerializable(stage.usage);
|
|
@@ -1183,7 +1186,7 @@ export class FlexHarnessCore<TScope> {
|
|
|
1183
1186
|
return {
|
|
1184
1187
|
runId,
|
|
1185
1188
|
status,
|
|
1186
|
-
userMessage,
|
|
1189
|
+
...(userMessage ? { userMessage } : {}),
|
|
1187
1190
|
assistantMessage,
|
|
1188
1191
|
...(status === 'completed' && stage?.model ? { model: stage.model } : {}),
|
|
1189
1192
|
...(stage?.usage ? { usage: stage.usage } : {}),
|
|
@@ -1747,12 +1750,28 @@ export class FlexHarnessCore<TScope> {
|
|
|
1747
1750
|
}
|
|
1748
1751
|
|
|
1749
1752
|
public visibleMessages(stored: IStoredSessionState): IFlexMessage[] {
|
|
1750
|
-
const
|
|
1751
|
-
if (!firstHiddenSegment) return stored.messages;
|
|
1752
|
-
const boundary = stored.messages.findIndex((message) => message.runId === firstHiddenSegment.runId);
|
|
1753
|
+
const boundary = this.undoneBoundary(stored);
|
|
1753
1754
|
return boundary < 0 ? stored.messages : stored.messages.slice(0, boundary);
|
|
1754
1755
|
}
|
|
1755
1756
|
|
|
1757
|
+
/**
|
|
1758
|
+
* The transcript: the visible messages, and with `includeUndone` the messages of undone turns
|
|
1759
|
+
* after them, each marked `undone: true`.
|
|
1760
|
+
*/
|
|
1761
|
+
public transcriptMessages(stored: IStoredSessionState, includeUndone: boolean): IFlexMessage[] {
|
|
1762
|
+
if (!includeUndone) return this.visibleMessages(stored);
|
|
1763
|
+
const boundary = this.undoneBoundary(stored);
|
|
1764
|
+
if (boundary < 0) return stored.messages;
|
|
1765
|
+
return stored.messages.map((message, index) => index < boundary ? message : { ...message, undone: true });
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
/** The index of the first message of an undone turn, or -1 when no turn is undone. */
|
|
1769
|
+
private undoneBoundary(stored: IStoredSessionState): number {
|
|
1770
|
+
const firstHiddenSegment = this.reversions.reversionGroups(stored)[stored.revertCursor]?.segments[0];
|
|
1771
|
+
if (!firstHiddenSegment) return -1;
|
|
1772
|
+
return stored.messages.findIndex((message) => message.runId === firstHiddenSegment.runId);
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1756
1775
|
public requireMutableSession(state: IStorageState, sessionId: string): IStoredSessionState {
|
|
1757
1776
|
const stored = this.requireSession(state, sessionId);
|
|
1758
1777
|
const pending = [...state.pendingPermissions.values()].some((entry) =>
|
|
@@ -1789,8 +1808,8 @@ export class FlexHarnessCore<TScope> {
|
|
|
1789
1808
|
}
|
|
1790
1809
|
}
|
|
1791
1810
|
|
|
1792
|
-
public requireMessage(stored: IStoredSessionState, messageId: string): IFlexMessage {
|
|
1793
|
-
const message = this.
|
|
1811
|
+
public requireMessage(stored: IStoredSessionState, messageId: string, includeUndone = false): IFlexMessage {
|
|
1812
|
+
const message = this.transcriptMessages(stored, includeUndone).find((entry) => entry.messageId === messageId);
|
|
1794
1813
|
if (!message) throw new FlexHarnessNotFoundError('Message', messageId);
|
|
1795
1814
|
return message;
|
|
1796
1815
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as plugins from './plugins.js';
|
|
2
2
|
import {
|
|
3
3
|
FlexHarnessAbortError,
|
|
4
|
+
FlexHarnessDuplicatePromptError,
|
|
4
5
|
FlexHarnessNotFoundError,
|
|
5
6
|
FlexHarnessQueueFullError,
|
|
6
7
|
FlexHarnessSessionBusyError,
|
|
@@ -12,6 +13,8 @@ import {
|
|
|
12
13
|
} from './errors.js';
|
|
13
14
|
import type {
|
|
14
15
|
IFlexAgentContextInvocation,
|
|
16
|
+
IFlexContinuePromptOptions,
|
|
17
|
+
IFlexContinuePromptResult,
|
|
15
18
|
IFlexEnqueuePromptOptions,
|
|
16
19
|
IFlexSteerAdmission,
|
|
17
20
|
IFlexSteerInput,
|
|
@@ -23,6 +26,7 @@ import type {
|
|
|
23
26
|
IFlexScheduledPromptAdmission,
|
|
24
27
|
IFlexSchedulePromptOptions,
|
|
25
28
|
TFlexPrompt,
|
|
29
|
+
TFlexAgentPrompt,
|
|
26
30
|
} from './interfaces.js';
|
|
27
31
|
import { cloneSerializable } from './utils.json.js';
|
|
28
32
|
import { normalizeFlexPrompt, type INormalizedFlexPrompt } from './utils.prompt.js';
|
|
@@ -70,13 +74,15 @@ export class FlexHarnessPromptQueue<TScope> {
|
|
|
70
74
|
constructor(private readonly core: FlexHarnessCore<TScope>) {}
|
|
71
75
|
|
|
72
76
|
public promptAdmissionByteSize(
|
|
73
|
-
prompt: TFlexPrompt,
|
|
77
|
+
prompt: TFlexPrompt | undefined,
|
|
74
78
|
options: IFlexPromptOptions,
|
|
75
79
|
scheduleKey?: string,
|
|
76
80
|
debounceMs?: number,
|
|
77
81
|
): number {
|
|
78
82
|
return jsonBytes({
|
|
79
|
-
|
|
83
|
+
...(prompt === undefined
|
|
84
|
+
? {}
|
|
85
|
+
: { prompt: normalizeFlexPrompt(prompt, this.core.attachmentReferenceProtocols) }),
|
|
80
86
|
options: cloneSerializable(options),
|
|
81
87
|
scheduleKey,
|
|
82
88
|
debounceMs,
|
|
@@ -99,12 +105,52 @@ export class FlexHarnessPromptQueue<TScope> {
|
|
|
99
105
|
return admission.completion;
|
|
100
106
|
}
|
|
101
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
|
+
|
|
102
145
|
public async startPrompt(
|
|
103
146
|
scopeId: string,
|
|
104
147
|
sessionId: string,
|
|
105
148
|
prompt: TFlexPrompt,
|
|
106
149
|
options: IFlexEnqueuePromptOptions = {},
|
|
107
150
|
): Promise<IFlexPromptAdmission> {
|
|
151
|
+
if (prompt === undefined) {
|
|
152
|
+
throw new FlexHarnessValidationError('prompt must be a string or a non-empty array of parts.');
|
|
153
|
+
}
|
|
108
154
|
const admitted = this.splitEnqueueOptions(options);
|
|
109
155
|
const queued = await this.enqueuePromptInternal(
|
|
110
156
|
scopeId, sessionId, prompt, admitted.options,
|
|
@@ -119,6 +165,9 @@ export class FlexHarnessPromptQueue<TScope> {
|
|
|
119
165
|
prompt: TFlexPrompt,
|
|
120
166
|
options: IFlexEnqueuePromptOptions = {},
|
|
121
167
|
): Promise<IFlexPromptQueueAdmission> {
|
|
168
|
+
if (prompt === undefined) {
|
|
169
|
+
throw new FlexHarnessValidationError('prompt must be a string or a non-empty array of parts.');
|
|
170
|
+
}
|
|
122
171
|
const admitted = this.splitEnqueueOptions(options);
|
|
123
172
|
const queued = await this.enqueuePromptInternal(
|
|
124
173
|
scopeId, sessionId, prompt, admitted.options,
|
|
@@ -165,15 +214,14 @@ export class FlexHarnessPromptQueue<TScope> {
|
|
|
165
214
|
prompt: TFlexPrompt,
|
|
166
215
|
options: IFlexSchedulePromptOptions = {},
|
|
167
216
|
): Promise<IFlexScheduledPromptAdmission> {
|
|
217
|
+
if (prompt === undefined) {
|
|
218
|
+
throw new FlexHarnessValidationError('prompt must be a string or a non-empty array of parts.');
|
|
219
|
+
}
|
|
168
220
|
validateIdentifier(scheduleKey, 'scheduleKey');
|
|
169
221
|
requireTransferIdentifier(scheduleKey, 'scheduleKey');
|
|
170
222
|
validatePromptOptions(options, true);
|
|
171
223
|
const debounceMs = options.debounceMs ?? 50;
|
|
172
|
-
|
|
173
|
-
throw new FlexHarnessValidationError(
|
|
174
|
-
`debounceMs must be an integer from 0 through ${maxScheduleDebounceMs}.`,
|
|
175
|
-
);
|
|
176
|
-
}
|
|
224
|
+
this.validateScheduleDebounce(debounceMs);
|
|
177
225
|
const queued = await this.enqueuePromptInternal(
|
|
178
226
|
scopeId,
|
|
179
227
|
sessionId,
|
|
@@ -330,9 +378,7 @@ export class FlexHarnessPromptQueue<TScope> {
|
|
|
330
378
|
try {
|
|
331
379
|
steering.input.add({
|
|
332
380
|
inputId: steer.steerId,
|
|
333
|
-
content: cloneSerializable(prompt.modelMessage.content) as
|
|
334
|
-
plugins.IAgentSession['beginGeneration']
|
|
335
|
-
>[0],
|
|
381
|
+
content: cloneSerializable(prompt.modelMessage.content) as TFlexAgentPrompt,
|
|
336
382
|
});
|
|
337
383
|
} catch (error) {
|
|
338
384
|
if (error instanceof plugins.AgentPendingInputRejectedError) {
|
|
@@ -354,10 +400,10 @@ export class FlexHarnessPromptQueue<TScope> {
|
|
|
354
400
|
return { steerId: steer.steerId, queueId: run.queueId, runId: run.runId };
|
|
355
401
|
}
|
|
356
402
|
|
|
357
|
-
public async enqueuePromptInternal(
|
|
403
|
+
public async enqueuePromptInternal<TPrompt extends TFlexPrompt | undefined>(
|
|
358
404
|
scopeId: string,
|
|
359
405
|
sessionId: string,
|
|
360
|
-
prompt:
|
|
406
|
+
prompt: TPrompt,
|
|
361
407
|
options: IFlexPromptOptions,
|
|
362
408
|
scheduleKey?: string,
|
|
363
409
|
debounceMs?: number,
|
|
@@ -367,12 +413,15 @@ export class FlexHarnessPromptQueue<TScope> {
|
|
|
367
413
|
cacheWarm = false,
|
|
368
414
|
steers: readonly IAdmittedSteer[] = [],
|
|
369
415
|
): Promise<{
|
|
370
|
-
admission: IFlexPromptQueueAdmission
|
|
371
|
-
started: Promise<IFlexPromptAdmission
|
|
416
|
+
admission: IFlexPromptQueueAdmission<TPrompt extends undefined ? IFlexContinuePromptResult : IFlexPromptResult>;
|
|
417
|
+
started: Promise<IFlexPromptAdmission<TPrompt extends undefined ? IFlexContinuePromptResult : IFlexPromptResult>>;
|
|
372
418
|
queued?: IQueuedPrompt;
|
|
373
419
|
}> {
|
|
374
420
|
this.core.assertOpen();
|
|
375
|
-
|
|
421
|
+
if (options.promptId !== undefined) requireTransferIdentifier(options.promptId, 'promptId');
|
|
422
|
+
const normalizedPrompt = prompt === undefined
|
|
423
|
+
? undefined
|
|
424
|
+
: normalizeFlexPrompt(prompt, this.core.attachmentReferenceProtocols);
|
|
376
425
|
const normalizedOptions = cloneSerializable(options);
|
|
377
426
|
const byteSize = this.promptAdmissionByteSize(prompt, options, scheduleKey, debounceMs);
|
|
378
427
|
const steerBytes = steers.reduce((sum, steer) => sum + steer.byteSize, 0);
|
|
@@ -459,16 +508,22 @@ export class FlexHarnessPromptQueue<TScope> {
|
|
|
459
508
|
) {
|
|
460
509
|
throw new FlexHarnessSessionBusyError(sessionId, `already has scheduled key "${scheduleKey}"`);
|
|
461
510
|
}
|
|
462
|
-
|
|
511
|
+
if (
|
|
512
|
+
options.promptId !== undefined
|
|
513
|
+
&& [...stored.outstandingPromptsById.values()].some((entry) => entry.promptId === options.promptId)
|
|
514
|
+
) {
|
|
515
|
+
throw new FlexHarnessDuplicatePromptError(options.promptId);
|
|
516
|
+
}
|
|
517
|
+
let resolveCompletion!: (result: IFlexContinuePromptResult) => void;
|
|
463
518
|
let rejectCompletion!: (error: unknown) => void;
|
|
464
|
-
const completion = new Promise<
|
|
519
|
+
const completion = new Promise<IFlexContinuePromptResult>((resolve, reject) => {
|
|
465
520
|
resolveCompletion = resolve;
|
|
466
521
|
rejectCompletion = reject;
|
|
467
522
|
});
|
|
468
523
|
void completion.catch(() => undefined);
|
|
469
|
-
let resolveStarted!: (admission: IFlexPromptAdmission) => void;
|
|
524
|
+
let resolveStarted!: (admission: IFlexPromptAdmission<IFlexContinuePromptResult>) => void;
|
|
470
525
|
let rejectStarted!: (error: unknown) => void;
|
|
471
|
-
const started = new Promise<IFlexPromptAdmission
|
|
526
|
+
const started = new Promise<IFlexPromptAdmission<IFlexContinuePromptResult>>((resolve, reject) => {
|
|
472
527
|
resolveStarted = resolve;
|
|
473
528
|
rejectStarted = reject;
|
|
474
529
|
});
|
|
@@ -485,7 +540,8 @@ export class FlexHarnessPromptQueue<TScope> {
|
|
|
485
540
|
queuedAt: new Date().toISOString(),
|
|
486
541
|
...(scheduleKey === undefined ? {} : { scheduleKey, debounceMs }),
|
|
487
542
|
...(cacheWarm ? { cacheWarm: true as const } : {}),
|
|
488
|
-
|
|
543
|
+
...(options.promptId === undefined ? {} : { promptId: options.promptId }),
|
|
544
|
+
...(normalizedPrompt === undefined ? { continuation: true as const } : { prompt: normalizedPrompt }),
|
|
489
545
|
options: normalizedOptions,
|
|
490
546
|
...(delegationSeed === undefined ? {} : { delegationSeed }),
|
|
491
547
|
byteSize,
|
|
@@ -508,8 +564,13 @@ export class FlexHarnessPromptQueue<TScope> {
|
|
|
508
564
|
this.emitPromptQueueEvent(queued, 'prompt.queued');
|
|
509
565
|
this.schedulePromptQueueDrain(state, stored);
|
|
510
566
|
return {
|
|
511
|
-
|
|
512
|
-
|
|
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
|
+
>>,
|
|
513
574
|
queued,
|
|
514
575
|
};
|
|
515
576
|
} finally {
|
|
@@ -616,8 +677,28 @@ export class FlexHarnessPromptQueue<TScope> {
|
|
|
616
677
|
}
|
|
617
678
|
}
|
|
618
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
|
+
|
|
619
700
|
private async promoteQueuedPrompt(queued: IQueuedPrompt, run: IActiveRun): Promise<void> {
|
|
620
|
-
const prompt = queued.prompt
|
|
701
|
+
const prompt = queued.prompt;
|
|
621
702
|
const options = queued.options!;
|
|
622
703
|
let projectionReserved = false;
|
|
623
704
|
// Open from the reservation until its messages are announced or rolled back.
|
|
@@ -627,21 +708,21 @@ export class FlexHarnessPromptQueue<TScope> {
|
|
|
627
708
|
this.assertPromptPromotion(queued, run);
|
|
628
709
|
await this.core.admission.acquireDelegatedRunAdmission(queued, run);
|
|
629
710
|
this.assertPromptPromotion(queued, run);
|
|
711
|
+
if (queued.continuation) this.assertContinuableContext(run.stored);
|
|
630
712
|
await this.core.reversions.commitRevertedBranch(run.state, run.stored, run.scopeId, run.scope as TScope);
|
|
631
713
|
this.assertPromptPromotion(queued, run);
|
|
632
|
-
const generationPrompt =
|
|
633
|
-
|
|
634
|
-
|
|
714
|
+
const generationPrompt = prompt === undefined
|
|
715
|
+
? undefined
|
|
716
|
+
: cloneSerializable(prompt.modelMessage.content) as TFlexAgentPrompt;
|
|
635
717
|
this.assertPromptPromotion(queued, run);
|
|
636
718
|
run.transaction = await this.core.compaction.withRunCompactorContext(
|
|
637
719
|
run,
|
|
638
|
-
() =>
|
|
639
|
-
|
|
640
|
-
{ generationId: run.runId },
|
|
641
|
-
),
|
|
720
|
+
() => generationPrompt === undefined
|
|
721
|
+
? run.stored.agentSession.beginContinuation({ generationId: run.runId })
|
|
722
|
+
: run.stored.agentSession.beginGeneration(generationPrompt, { generationId: run.runId }),
|
|
642
723
|
);
|
|
643
724
|
this.assertPromptPromotion(queued, run);
|
|
644
|
-
const reservation = this.core.runs.createReservation(run, prompt);
|
|
725
|
+
const reservation = this.core.runs.createReservation(run, prompt, queued.promptId);
|
|
645
726
|
closeReservationPublication = this.core.openPublication(run.state, run.sessionId);
|
|
646
727
|
run.metricsOperation = createSessionMetricsOperation(run.runId, 'generation');
|
|
647
728
|
await this.core.mutateProjection(run.state, run.stored, () => {
|
|
@@ -649,7 +730,8 @@ export class FlexHarnessPromptQueue<TScope> {
|
|
|
649
730
|
throw new Error('Session metrics already owns a generation.');
|
|
650
731
|
}
|
|
651
732
|
run.stored.metrics.pendingGenerationId = run.runId;
|
|
652
|
-
run.stored.messages.push(reservation.userMessage
|
|
733
|
+
if (reservation.userMessage) run.stored.messages.push(reservation.userMessage);
|
|
734
|
+
run.stored.messages.push(reservation.assistantMessage);
|
|
653
735
|
// A cache-warm turn is no turn of the user's: it gets no reversion segment of its own.
|
|
654
736
|
if (run.stored.session.agent === undefined && !run.cacheWarm) {
|
|
655
737
|
this.core.reversions.pruneReversionState(run.stored, true);
|
|
@@ -659,7 +741,7 @@ export class FlexHarnessPromptQueue<TScope> {
|
|
|
659
741
|
const protocolVersion = this.core.reversions.reversionProviderProtocolVersion() ?? 1;
|
|
660
742
|
run.stored.reversionSegments.push({
|
|
661
743
|
runId: run.runId,
|
|
662
|
-
userMessageId: reservation.userMessage.messageId,
|
|
744
|
+
...(reservation.userMessage ? { userMessageId: reservation.userMessage.messageId } : {}),
|
|
663
745
|
status: 'capturing',
|
|
664
746
|
contextAvailable: true,
|
|
665
747
|
eventIds: [],
|
|
@@ -681,7 +763,7 @@ export class FlexHarnessPromptQueue<TScope> {
|
|
|
681
763
|
}
|
|
682
764
|
});
|
|
683
765
|
projectionReserved = true;
|
|
684
|
-
run.reservedUserMessage = publicSnapshot(reservation.userMessage);
|
|
766
|
+
if (reservation.userMessage) run.reservedUserMessage = publicSnapshot(reservation.userMessage);
|
|
685
767
|
run.reservedAssistantMessage = publicSnapshot(reservation.assistantMessage);
|
|
686
768
|
this.assertPromptPromotion(queued, run);
|
|
687
769
|
await this.core.mutateScope(run.state, () => {
|
|
@@ -698,9 +780,9 @@ export class FlexHarnessPromptQueue<TScope> {
|
|
|
698
780
|
run.stored.session.activity = {
|
|
699
781
|
runId: run.runId,
|
|
700
782
|
status: run.scheduleKey ? 'scheduled' : 'running',
|
|
701
|
-
startedAt: reservation.
|
|
783
|
+
startedAt: reservation.assistantMessage.createdAt,
|
|
702
784
|
};
|
|
703
|
-
run.stored.session.updatedAt = reservation.
|
|
785
|
+
run.stored.session.updatedAt = reservation.assistantMessage.createdAt;
|
|
704
786
|
const preview = run.stored.session.preview ?? sessionPreview(run.stored.messages);
|
|
705
787
|
if (preview !== undefined) run.stored.session.preview = preview;
|
|
706
788
|
});
|
|
@@ -742,11 +824,12 @@ export class FlexHarnessPromptQueue<TScope> {
|
|
|
742
824
|
this.core.emitEvent(run.scopeId, run.stored.session, {
|
|
743
825
|
type: runType,
|
|
744
826
|
runId: run.runId,
|
|
745
|
-
messageId: run.userMessageId,
|
|
827
|
+
messageId: run.userMessageId ?? run.assistantMessageId,
|
|
746
828
|
session: publicSnapshot(run.stored.session),
|
|
747
829
|
...(run.cacheWarm ? { cacheWarm: true } : {}),
|
|
748
830
|
});
|
|
749
|
-
for (const message of [run.reservedUserMessage
|
|
831
|
+
for (const message of [run.reservedUserMessage, run.reservedAssistantMessage!]) {
|
|
832
|
+
if (!message) continue;
|
|
750
833
|
this.core.emitEvent(run.scopeId, run.stored.session, {
|
|
751
834
|
type: 'message.created',
|
|
752
835
|
runId: run.runId,
|
|
@@ -818,6 +901,8 @@ export class FlexHarnessPromptQueue<TScope> {
|
|
|
818
901
|
status: queued.status,
|
|
819
902
|
queuedAt: queued.queuedAt,
|
|
820
903
|
...(queued.runId ? { runId: queued.runId } : {}),
|
|
904
|
+
...(queued.promptId === undefined ? {} : { promptId: queued.promptId }),
|
|
905
|
+
...(queued.continuation ? { continuation: true as const } : {}),
|
|
821
906
|
...(queued.scheduleKey ? { scheduleKey: queued.scheduleKey } : {}),
|
|
822
907
|
...(queued.cacheWarm ? { cacheWarm: true } : {}),
|
|
823
908
|
...(queued.startedAt ? { startedAt: queued.startedAt } : {}),
|
|
@@ -851,7 +936,7 @@ export class FlexHarnessPromptQueue<TScope> {
|
|
|
851
936
|
queued: IQueuedPrompt,
|
|
852
937
|
status: 'completed' | 'failed' | 'cancelled',
|
|
853
938
|
error?: unknown,
|
|
854
|
-
result?:
|
|
939
|
+
result?: IFlexContinuePromptResult,
|
|
855
940
|
): void {
|
|
856
941
|
const stored = queued.stored;
|
|
857
942
|
if (stored.outstandingPromptsById.get(queued.queueId) !== queued) return;
|