@modelprofile.com/flexharness 3.1.0 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/changelog.md +19 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.flexharness.d.ts +24 -2
- package/dist_ts/classes.flexharness.js +441 -79
- package/dist_ts/errors.d.ts +3 -0
- package/dist_ts/errors.js +7 -1
- package/dist_ts/interfaces.d.ts +34 -3
- package/package.json +1 -1
- package/readme.hints.md +8 -0
- package/readme.md +32 -6
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.flexharness.ts +543 -77
- package/ts/errors.ts +7 -0
- package/ts/interfaces.ts +47 -3
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
FlexHarnessNotFoundError,
|
|
8
8
|
FlexHarnessPermissionRejectedError,
|
|
9
9
|
FlexHarnessPermissionStateError,
|
|
10
|
+
FlexHarnessQueueFullError,
|
|
10
11
|
FlexHarnessRunError,
|
|
11
12
|
FlexHarnessSessionBusyError,
|
|
12
13
|
FlexHarnessStoreConflictError,
|
|
@@ -34,6 +35,9 @@ import type {
|
|
|
34
35
|
IFlexPermissionRequestInput,
|
|
35
36
|
IFlexPermissionSnapshot,
|
|
36
37
|
IFlexPromptAdmission,
|
|
38
|
+
IFlexPromptQueueAdmission,
|
|
39
|
+
IFlexPromptQueueEntry,
|
|
40
|
+
IFlexPromptQueueLimits,
|
|
37
41
|
IFlexPromptOptions,
|
|
38
42
|
IFlexPromptResult,
|
|
39
43
|
IFlexProjectionSnapshot,
|
|
@@ -64,6 +68,7 @@ import type {
|
|
|
64
68
|
TFlexPermissionDecision,
|
|
65
69
|
TFlexPrompt,
|
|
66
70
|
TFlexPromptPart,
|
|
71
|
+
TFlexPromptQueueStatus,
|
|
67
72
|
TFlexToolExecutionReconciliation,
|
|
68
73
|
} from './interfaces.js';
|
|
69
74
|
import { InMemoryFlexHarnessStores } from './classes.stores.js';
|
|
@@ -108,6 +113,11 @@ interface IStoredSessionState {
|
|
|
108
113
|
executionContextCloseCompleted: boolean;
|
|
109
114
|
jobStoreReleased: boolean;
|
|
110
115
|
jobs?: NonNullable<plugins.IToolExecutionContext['jobs']>;
|
|
116
|
+
promptQueue: IQueuedPrompt[];
|
|
117
|
+
outstandingPromptsById: Map<string, IQueuedPrompt>;
|
|
118
|
+
terminalPromptQueueEntries: Map<string, IFlexPromptQueueEntry>;
|
|
119
|
+
outstandingPromptBytes: number;
|
|
120
|
+
promptQueueDrain?: Promise<void>;
|
|
111
121
|
}
|
|
112
122
|
|
|
113
123
|
interface IRetainedSessionCleanup {
|
|
@@ -160,6 +170,7 @@ interface IActiveRun {
|
|
|
160
170
|
scopeId: string;
|
|
161
171
|
scope: unknown;
|
|
162
172
|
sessionId: string;
|
|
173
|
+
queueId: string;
|
|
163
174
|
runId: string;
|
|
164
175
|
userMessageId: string;
|
|
165
176
|
assistantMessageId: string;
|
|
@@ -182,8 +193,37 @@ interface IActiveRun {
|
|
|
182
193
|
reservedAssistantMessage?: IFlexMessage;
|
|
183
194
|
modelResolution?: IFlexResolvedModel;
|
|
184
195
|
completion: Promise<IFlexPromptResult>;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
interface IQueuedPrompt {
|
|
199
|
+
state: IStorageState;
|
|
200
|
+
stored: IStoredSessionState;
|
|
201
|
+
scopeId: string;
|
|
202
|
+
scope: unknown;
|
|
203
|
+
sessionId: string;
|
|
204
|
+
queueId: string;
|
|
205
|
+
queueSequence: number;
|
|
206
|
+
status: Exclude<TFlexPromptQueueStatus, 'completed' | 'failed' | 'cancelled'>;
|
|
207
|
+
queuedAt: string;
|
|
208
|
+
runId?: string;
|
|
209
|
+
scheduleKey?: string;
|
|
210
|
+
debounceMs?: number;
|
|
211
|
+
startedAt?: string;
|
|
212
|
+
prompt?: INormalizedFlexPrompt;
|
|
213
|
+
options?: IFlexPromptOptions;
|
|
214
|
+
byteSize: number;
|
|
215
|
+
completion: Promise<IFlexPromptResult>;
|
|
185
216
|
resolveCompletion: (result: IFlexPromptResult) => void;
|
|
186
217
|
rejectCompletion: (error: unknown) => void;
|
|
218
|
+
started: Promise<IFlexPromptAdmission>;
|
|
219
|
+
resolveStarted: (admission: IFlexPromptAdmission) => void;
|
|
220
|
+
rejectStarted: (error: unknown) => void;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
interface IPendingPromptAdmission {
|
|
224
|
+
controller: AbortController;
|
|
225
|
+
settled: Promise<void>;
|
|
226
|
+
resolveSettled: () => void;
|
|
187
227
|
}
|
|
188
228
|
|
|
189
229
|
interface IPendingPermission {
|
|
@@ -238,6 +278,13 @@ const DEFAULT_CALLBACK_LIMITS: Required<IFlexCallbackLimits> = {
|
|
|
238
278
|
maxOutputBytes: 1024 * 1024,
|
|
239
279
|
maxParts: 2_000,
|
|
240
280
|
};
|
|
281
|
+
const DEFAULT_PROMPT_QUEUE_LIMITS: Required<IFlexPromptQueueLimits> = {
|
|
282
|
+
maxOutstandingPromptsPerSession: 16,
|
|
283
|
+
maxOutstandingBytesPerSession: 64 * 1024 * 1024,
|
|
284
|
+
maxPendingAdmissions: 64,
|
|
285
|
+
maxPendingAdmissionBytes: 128 * 1024 * 1024,
|
|
286
|
+
maxTerminalEntriesPerSession: 64,
|
|
287
|
+
};
|
|
241
288
|
const maxMessagePageSize = 50;
|
|
242
289
|
const maxMessagePageCursorBytes = 4096;
|
|
243
290
|
const maxTransferIdentifierBytes = 512;
|
|
@@ -624,6 +671,29 @@ function resolveCallbackLimits(limits: IFlexCallbackLimits = {}): Required<IFlex
|
|
|
624
671
|
return resolved;
|
|
625
672
|
}
|
|
626
673
|
|
|
674
|
+
function resolvePromptQueueLimits(
|
|
675
|
+
limits: IFlexPromptQueueLimits = {},
|
|
676
|
+
): Required<IFlexPromptQueueLimits> {
|
|
677
|
+
const resolved = {
|
|
678
|
+
maxOutstandingPromptsPerSession: limits.maxOutstandingPromptsPerSession
|
|
679
|
+
?? DEFAULT_PROMPT_QUEUE_LIMITS.maxOutstandingPromptsPerSession,
|
|
680
|
+
maxOutstandingBytesPerSession: limits.maxOutstandingBytesPerSession
|
|
681
|
+
?? DEFAULT_PROMPT_QUEUE_LIMITS.maxOutstandingBytesPerSession,
|
|
682
|
+
maxPendingAdmissions: limits.maxPendingAdmissions
|
|
683
|
+
?? DEFAULT_PROMPT_QUEUE_LIMITS.maxPendingAdmissions,
|
|
684
|
+
maxPendingAdmissionBytes: limits.maxPendingAdmissionBytes
|
|
685
|
+
?? DEFAULT_PROMPT_QUEUE_LIMITS.maxPendingAdmissionBytes,
|
|
686
|
+
maxTerminalEntriesPerSession: limits.maxTerminalEntriesPerSession
|
|
687
|
+
?? DEFAULT_PROMPT_QUEUE_LIMITS.maxTerminalEntriesPerSession,
|
|
688
|
+
};
|
|
689
|
+
for (const [name, value] of Object.entries(resolved)) {
|
|
690
|
+
if (!Number.isSafeInteger(value) || value < 1) {
|
|
691
|
+
throw new FlexHarnessValidationError(`promptQueueLimits.${name} must be a positive integer.`);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
return resolved;
|
|
695
|
+
}
|
|
696
|
+
|
|
627
697
|
function normalizeAgentSessionPolicy(policy: IFlexAgentSessionPolicy = {}): IFlexAgentSessionPolicy {
|
|
628
698
|
return {
|
|
629
699
|
...(policy.contextBuilder === undefined ? {} : { contextBuilder: policy.contextBuilder }),
|
|
@@ -656,6 +726,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
656
726
|
private readonly agentSessionPolicy: IFlexAgentSessionPolicy;
|
|
657
727
|
private readonly toolOutputLimits: Required<NonNullable<IFlexHarnessOptions<TScope>['toolOutputLimits']>>;
|
|
658
728
|
private readonly callbackLimits: Required<IFlexCallbackLimits>;
|
|
729
|
+
private readonly promptQueueLimits: Required<IFlexPromptQueueLimits>;
|
|
659
730
|
private readonly externalErrorProjector?: TFlexExternalErrorProjector;
|
|
660
731
|
private readonly stateLoads = new Map<string, Promise<IStorageState>>();
|
|
661
732
|
private readonly scopeAdmissions = new Map<string, IScopeAdmissionState>();
|
|
@@ -670,7 +741,11 @@ export class FlexHarness<TScope = unknown> {
|
|
|
670
741
|
IOrphanedExecutionContextOwner
|
|
671
742
|
>();
|
|
672
743
|
private readonly orphanedProviderReleases = new Map<string, IOrphanedProviderRelease>();
|
|
744
|
+
private readonly pendingPromptAdmissionOwners = new Set<IPendingPromptAdmission>();
|
|
673
745
|
private sequence = 0;
|
|
746
|
+
private promptQueueSequence = 0;
|
|
747
|
+
private pendingPromptAdmissions = 0;
|
|
748
|
+
private pendingPromptAdmissionBytes = 0;
|
|
674
749
|
private closed = false;
|
|
675
750
|
private disposePromise?: Promise<void>;
|
|
676
751
|
|
|
@@ -686,6 +761,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
686
761
|
this.agentSessionPolicy = normalizeAgentSessionPolicy(options.agentSessionPolicy);
|
|
687
762
|
this.toolOutputLimits = resolveJsonLimits(options.toolOutputLimits);
|
|
688
763
|
this.callbackLimits = resolveCallbackLimits(options.callbackLimits);
|
|
764
|
+
this.promptQueueLimits = resolvePromptQueueLimits(options.promptQueueLimits);
|
|
689
765
|
this.externalErrorProjector = options.externalErrorProjector;
|
|
690
766
|
}
|
|
691
767
|
|
|
@@ -856,7 +932,9 @@ export class FlexHarness<TScope = unknown> {
|
|
|
856
932
|
const { state } = await this.resolveState(scopeId);
|
|
857
933
|
let result!: IFlexSession;
|
|
858
934
|
await this.mutateScope(state, () => {
|
|
859
|
-
const stored =
|
|
935
|
+
const stored = Object.prototype.hasOwnProperty.call(options, 'archived')
|
|
936
|
+
? this.requireMutableSession(state, sessionId)
|
|
937
|
+
: this.requireSession(state, sessionId);
|
|
860
938
|
const timestamp = new Date().toISOString();
|
|
861
939
|
if (Object.prototype.hasOwnProperty.call(options, 'title')) {
|
|
862
940
|
if (options.title === null) delete stored.session.title;
|
|
@@ -1021,7 +1099,19 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1021
1099
|
options: IFlexPromptOptions = {},
|
|
1022
1100
|
): Promise<IFlexPromptAdmission> {
|
|
1023
1101
|
validatePromptOptions(options, false);
|
|
1024
|
-
|
|
1102
|
+
const queued = await this.enqueuePromptInternal(scopeId, sessionId, prompt, options);
|
|
1103
|
+
return queued.started;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
public async enqueuePrompt(
|
|
1107
|
+
scopeId: string,
|
|
1108
|
+
sessionId: string,
|
|
1109
|
+
prompt: TFlexPrompt,
|
|
1110
|
+
options: IFlexPromptOptions = {},
|
|
1111
|
+
): Promise<IFlexPromptQueueAdmission> {
|
|
1112
|
+
validatePromptOptions(options, false);
|
|
1113
|
+
const queued = await this.enqueuePromptInternal(scopeId, sessionId, prompt, options);
|
|
1114
|
+
return queued.admission;
|
|
1025
1115
|
}
|
|
1026
1116
|
|
|
1027
1117
|
public async schedulePrompt(
|
|
@@ -1040,17 +1130,66 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1040
1130
|
`debounceMs must be an integer from 0 through ${maxScheduleDebounceMs}.`,
|
|
1041
1131
|
);
|
|
1042
1132
|
}
|
|
1043
|
-
const
|
|
1133
|
+
const queued = await this.enqueuePromptInternal(
|
|
1044
1134
|
scopeId,
|
|
1045
1135
|
sessionId,
|
|
1046
|
-
|
|
1136
|
+
prompt,
|
|
1047
1137
|
options,
|
|
1048
1138
|
scheduleKey,
|
|
1049
1139
|
debounceMs,
|
|
1050
1140
|
);
|
|
1141
|
+
const admission = await queued.started;
|
|
1051
1142
|
return Object.freeze({ ...admission, scheduleKey });
|
|
1052
1143
|
}
|
|
1053
1144
|
|
|
1145
|
+
public async getPromptQueueEntry(
|
|
1146
|
+
scopeId: string,
|
|
1147
|
+
sessionId: string,
|
|
1148
|
+
queueId: string,
|
|
1149
|
+
): Promise<IFlexPromptQueueEntry> {
|
|
1150
|
+
validateIdentifier(queueId, 'queueId');
|
|
1151
|
+
requireTransferIdentifier(queueId, 'queueId');
|
|
1152
|
+
const { state } = await this.resolveState(scopeId);
|
|
1153
|
+
this.assertStateAcceptingWork(state);
|
|
1154
|
+
const stored = this.requireSession(state, sessionId);
|
|
1155
|
+
const entry = this.promptQueueEntry(stored, queueId);
|
|
1156
|
+
if (!entry) throw new FlexHarnessNotFoundError('Prompt queue entry', queueId);
|
|
1157
|
+
return publicSnapshot(entry);
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
public async listPromptQueueEntries(
|
|
1161
|
+
scopeId: string,
|
|
1162
|
+
sessionId: string,
|
|
1163
|
+
): Promise<IFlexPromptQueueEntry[]> {
|
|
1164
|
+
const { state } = await this.resolveState(scopeId);
|
|
1165
|
+
this.assertStateAcceptingWork(state);
|
|
1166
|
+
const stored = this.requireSession(state, sessionId);
|
|
1167
|
+
return publicSnapshot([
|
|
1168
|
+
...[...stored.outstandingPromptsById.values()].map((entry) => this.projectPromptQueueEntry(entry)),
|
|
1169
|
+
...stored.terminalPromptQueueEntries.values(),
|
|
1170
|
+
].sort((left, right) => left.queueSequence - right.queueSequence));
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
public async cancelPrompt(
|
|
1174
|
+
scopeId: string,
|
|
1175
|
+
sessionId: string,
|
|
1176
|
+
queueId: string,
|
|
1177
|
+
): Promise<boolean> {
|
|
1178
|
+
validateIdentifier(queueId, 'queueId');
|
|
1179
|
+
requireTransferIdentifier(queueId, 'queueId');
|
|
1180
|
+
const { state } = await this.resolveState(scopeId);
|
|
1181
|
+
const stored = this.requireSession(state, sessionId);
|
|
1182
|
+
const queued = stored.outstandingPromptsById.get(queueId);
|
|
1183
|
+
if (!queued) {
|
|
1184
|
+
if (stored.terminalPromptQueueEntries.has(queueId)) return false;
|
|
1185
|
+
throw new FlexHarnessNotFoundError('Prompt queue entry', queueId);
|
|
1186
|
+
}
|
|
1187
|
+
return this.cancelQueuedPrompt(
|
|
1188
|
+
queued,
|
|
1189
|
+
this.trustInternalError(new FlexHarnessAbortError('The queued prompt was cancelled.')),
|
|
1190
|
+
);
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1054
1193
|
public async cancelScheduledPrompt(
|
|
1055
1194
|
scopeId: string,
|
|
1056
1195
|
sessionId: string,
|
|
@@ -1058,18 +1197,12 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1058
1197
|
): Promise<boolean> {
|
|
1059
1198
|
validateIdentifier(scheduleKey, 'scheduleKey');
|
|
1060
1199
|
const { state } = await this.resolveState(scopeId);
|
|
1061
|
-
const
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1200
|
+
const stored = this.requireSession(state, sessionId);
|
|
1201
|
+
const queued = [...stored.outstandingPromptsById.values()]
|
|
1202
|
+
.find((entry) => entry.scheduleKey === scheduleKey);
|
|
1203
|
+
if (!queued) return false;
|
|
1065
1204
|
const cancellation = this.trustInternalError(new FlexHarnessAbortError('The scheduled run was cancelled.'));
|
|
1066
|
-
|
|
1067
|
-
this.rejectRunPermissions(state, run, cancellation);
|
|
1068
|
-
if (!run.controller.signal.aborted) run.controller.abort(cancellation);
|
|
1069
|
-
run.stored.agentSession.cancelScheduledGeneration(scheduleKey, cancellation);
|
|
1070
|
-
const settled = await Promise.allSettled([run.completion]);
|
|
1071
|
-
if (settled[0].status === 'rejected' && !isAbortError(settled[0].reason)) throw settled[0].reason;
|
|
1072
|
-
return true;
|
|
1205
|
+
return this.cancelQueuedPrompt(queued, cancellation);
|
|
1073
1206
|
}
|
|
1074
1207
|
|
|
1075
1208
|
public async abort(scopeId: string, sessionId: string): Promise<boolean> {
|
|
@@ -1321,45 +1454,199 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1321
1454
|
return disposal;
|
|
1322
1455
|
}
|
|
1323
1456
|
|
|
1324
|
-
private async
|
|
1457
|
+
private async enqueuePromptInternal(
|
|
1325
1458
|
scopeId: string,
|
|
1326
1459
|
sessionId: string,
|
|
1327
|
-
prompt:
|
|
1460
|
+
prompt: TFlexPrompt,
|
|
1328
1461
|
options: IFlexPromptOptions,
|
|
1329
1462
|
scheduleKey?: string,
|
|
1330
1463
|
debounceMs?: number,
|
|
1331
|
-
): Promise<IFlexPromptAdmission> {
|
|
1332
|
-
|
|
1333
|
-
const
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
const reason = scheduleKey && active.scheduleKey === scheduleKey
|
|
1341
|
-
? `already has scheduled key "${scheduleKey}"`
|
|
1342
|
-
: 'already has an active run';
|
|
1343
|
-
throw new FlexHarnessSessionBusyError(sessionId, reason);
|
|
1344
|
-
}
|
|
1345
|
-
let resolveCompletion!: (result: IFlexPromptResult) => void;
|
|
1346
|
-
let rejectCompletion!: (error: unknown) => void;
|
|
1347
|
-
const completion = new Promise<IFlexPromptResult>((resolve, reject) => {
|
|
1348
|
-
resolveCompletion = resolve;
|
|
1349
|
-
rejectCompletion = reject;
|
|
1464
|
+
): Promise<{ admission: IFlexPromptQueueAdmission; started: Promise<IFlexPromptAdmission> }> {
|
|
1465
|
+
this.assertOpen();
|
|
1466
|
+
const normalizedPrompt = normalizeFlexPrompt(prompt);
|
|
1467
|
+
const normalizedOptions = cloneSerializable(options);
|
|
1468
|
+
const byteSize = jsonBytes({
|
|
1469
|
+
prompt: normalizedPrompt,
|
|
1470
|
+
options: normalizedOptions,
|
|
1471
|
+
scheduleKey,
|
|
1472
|
+
debounceMs,
|
|
1350
1473
|
});
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1474
|
+
const releasePendingAdmission = this.reservePendingPromptAdmission(byteSize);
|
|
1475
|
+
let resolveSettled!: () => void;
|
|
1476
|
+
const pendingOwner: IPendingPromptAdmission = {
|
|
1477
|
+
controller: new AbortController(),
|
|
1478
|
+
settled: new Promise<void>((resolve) => {
|
|
1479
|
+
resolveSettled = resolve;
|
|
1480
|
+
}),
|
|
1481
|
+
resolveSettled: () => resolveSettled(),
|
|
1482
|
+
};
|
|
1483
|
+
this.pendingPromptAdmissionOwners.add(pendingOwner);
|
|
1484
|
+
let abortAdmission!: () => void;
|
|
1485
|
+
const abortPromise = new Promise<never>((_resolve, reject) => {
|
|
1486
|
+
abortAdmission = () => reject(
|
|
1487
|
+
pendingOwner.controller.signal.reason ?? new FlexHarnessAbortError(),
|
|
1488
|
+
);
|
|
1489
|
+
pendingOwner.controller.signal.addEventListener('abort', abortAdmission, { once: true });
|
|
1490
|
+
if (pendingOwner.controller.signal.aborted) abortAdmission();
|
|
1491
|
+
});
|
|
1492
|
+
try {
|
|
1493
|
+
const resolved = await Promise.race([this.resolveState(scopeId), abortPromise]);
|
|
1494
|
+
const state = resolved.state;
|
|
1495
|
+
await Promise.race([state.scopeQueue, abortPromise]);
|
|
1496
|
+
this.assertStateAcceptingWork(state);
|
|
1497
|
+
const stored = this.requireSession(state, sessionId);
|
|
1498
|
+
if (stored.outstandingPromptsById.size >= this.promptQueueLimits.maxOutstandingPromptsPerSession) {
|
|
1499
|
+
throw new FlexHarnessQueueFullError(
|
|
1500
|
+
`Session "${sessionId}" has reached its outstanding prompt limit.`,
|
|
1501
|
+
);
|
|
1502
|
+
}
|
|
1503
|
+
if (
|
|
1504
|
+
stored.outstandingPromptBytes + byteSize
|
|
1505
|
+
> this.promptQueueLimits.maxOutstandingBytesPerSession
|
|
1506
|
+
) {
|
|
1507
|
+
throw new FlexHarnessQueueFullError(
|
|
1508
|
+
`Session "${sessionId}" has reached its outstanding prompt byte limit.`,
|
|
1509
|
+
);
|
|
1510
|
+
}
|
|
1511
|
+
if (
|
|
1512
|
+
scheduleKey
|
|
1513
|
+
&& [...stored.outstandingPromptsById.values()]
|
|
1514
|
+
.some((entry) => entry.scheduleKey === scheduleKey)
|
|
1515
|
+
) {
|
|
1516
|
+
throw new FlexHarnessSessionBusyError(sessionId, `already has scheduled key "${scheduleKey}"`);
|
|
1517
|
+
}
|
|
1518
|
+
let resolveCompletion!: (result: IFlexPromptResult) => void;
|
|
1519
|
+
let rejectCompletion!: (error: unknown) => void;
|
|
1520
|
+
const completion = new Promise<IFlexPromptResult>((resolve, reject) => {
|
|
1521
|
+
resolveCompletion = resolve;
|
|
1522
|
+
rejectCompletion = reject;
|
|
1523
|
+
});
|
|
1524
|
+
void completion.catch(() => undefined);
|
|
1525
|
+
let resolveStarted!: (admission: IFlexPromptAdmission) => void;
|
|
1526
|
+
let rejectStarted!: (error: unknown) => void;
|
|
1527
|
+
const started = new Promise<IFlexPromptAdmission>((resolve, reject) => {
|
|
1528
|
+
resolveStarted = resolve;
|
|
1529
|
+
rejectStarted = reject;
|
|
1530
|
+
});
|
|
1531
|
+
void started.catch(() => undefined);
|
|
1532
|
+
const queued: IQueuedPrompt = {
|
|
1533
|
+
state,
|
|
1534
|
+
stored,
|
|
1535
|
+
scopeId,
|
|
1536
|
+
scope: resolved.scope.scope,
|
|
1537
|
+
sessionId,
|
|
1538
|
+
queueId: plugins.crypto.randomUUID(),
|
|
1539
|
+
queueSequence: ++this.promptQueueSequence,
|
|
1540
|
+
status: 'queued',
|
|
1541
|
+
queuedAt: new Date().toISOString(),
|
|
1542
|
+
...(scheduleKey === undefined ? {} : { scheduleKey, debounceMs }),
|
|
1543
|
+
prompt: normalizedPrompt,
|
|
1544
|
+
options: normalizedOptions,
|
|
1545
|
+
byteSize,
|
|
1546
|
+
completion,
|
|
1547
|
+
resolveCompletion,
|
|
1548
|
+
rejectCompletion,
|
|
1549
|
+
started,
|
|
1550
|
+
resolveStarted,
|
|
1551
|
+
rejectStarted,
|
|
1552
|
+
};
|
|
1553
|
+
stored.promptQueue.push(queued);
|
|
1554
|
+
stored.outstandingPromptsById.set(queued.queueId, queued);
|
|
1555
|
+
stored.outstandingPromptBytes += byteSize;
|
|
1556
|
+
this.emitPromptQueueEvent(queued, 'prompt.queued');
|
|
1557
|
+
this.schedulePromptQueueDrain(state, stored);
|
|
1558
|
+
return {
|
|
1559
|
+
admission: Object.freeze({ queueId: queued.queueId, completion }),
|
|
1560
|
+
started,
|
|
1561
|
+
};
|
|
1562
|
+
} finally {
|
|
1563
|
+
pendingOwner.controller.signal.removeEventListener('abort', abortAdmission);
|
|
1564
|
+
this.pendingPromptAdmissionOwners.delete(pendingOwner);
|
|
1565
|
+
releasePendingAdmission();
|
|
1566
|
+
pendingOwner.resolveSettled();
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
private reservePendingPromptAdmission(byteSize: number): () => void {
|
|
1571
|
+
if (this.pendingPromptAdmissions >= this.promptQueueLimits.maxPendingAdmissions) {
|
|
1572
|
+
throw new FlexHarnessQueueFullError('FlexHarness has reached its pending prompt admission limit.');
|
|
1573
|
+
}
|
|
1574
|
+
if (
|
|
1575
|
+
this.pendingPromptAdmissionBytes + byteSize
|
|
1576
|
+
> this.promptQueueLimits.maxPendingAdmissionBytes
|
|
1577
|
+
) {
|
|
1578
|
+
throw new FlexHarnessQueueFullError('FlexHarness has reached its pending prompt admission byte limit.');
|
|
1579
|
+
}
|
|
1580
|
+
this.pendingPromptAdmissions++;
|
|
1581
|
+
this.pendingPromptAdmissionBytes += byteSize;
|
|
1582
|
+
let released = false;
|
|
1583
|
+
return () => {
|
|
1584
|
+
if (released) return;
|
|
1585
|
+
released = true;
|
|
1586
|
+
this.pendingPromptAdmissions--;
|
|
1587
|
+
this.pendingPromptAdmissionBytes -= byteSize;
|
|
1588
|
+
};
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
private schedulePromptQueueDrain(state: IStorageState, stored: IStoredSessionState): void {
|
|
1592
|
+
if (stored.promptQueueDrain) return;
|
|
1593
|
+
let drain!: Promise<void>;
|
|
1594
|
+
drain = Promise.resolve()
|
|
1595
|
+
.then(() => this.drainPromptQueue(state, stored))
|
|
1596
|
+
.finally(() => {
|
|
1597
|
+
if (stored.promptQueueDrain === drain) delete stored.promptQueueDrain;
|
|
1598
|
+
if (
|
|
1599
|
+
stored.promptQueue.length > 0
|
|
1600
|
+
&& !state.activeRuns.has(stored.session.sessionId)
|
|
1601
|
+
&& state.lifecycle === 'active'
|
|
1602
|
+
&& state.sessions.get(stored.session.sessionId) === stored
|
|
1603
|
+
) this.schedulePromptQueueDrain(state, stored);
|
|
1604
|
+
});
|
|
1605
|
+
stored.promptQueueDrain = drain;
|
|
1606
|
+
void drain.catch(() => undefined);
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
private async drainPromptQueue(state: IStorageState, stored: IStoredSessionState): Promise<void> {
|
|
1610
|
+
while (
|
|
1611
|
+
state.lifecycle === 'active'
|
|
1612
|
+
&& state.sessions.get(stored.session.sessionId) === stored
|
|
1613
|
+
&& !state.activeRuns.has(stored.session.sessionId)
|
|
1614
|
+
) {
|
|
1615
|
+
const queued = stored.promptQueue.shift();
|
|
1616
|
+
if (!queued) return;
|
|
1617
|
+
if (stored.outstandingPromptsById.get(queued.queueId) !== queued) continue;
|
|
1618
|
+
queued.status = 'starting';
|
|
1619
|
+
queued.runId = plugins.crypto.randomUUID();
|
|
1620
|
+
queued.startedAt = new Date().toISOString();
|
|
1621
|
+
const run = this.createActiveRun(queued);
|
|
1622
|
+
state.activeRuns.set(queued.sessionId, run);
|
|
1623
|
+
try {
|
|
1624
|
+
await this.promoteQueuedPrompt(queued, run);
|
|
1625
|
+
return;
|
|
1626
|
+
} catch (error) {
|
|
1627
|
+
if (state.activeRuns.get(queued.sessionId) === run) state.activeRuns.delete(queued.sessionId);
|
|
1628
|
+
const cancellation = isAbortError(error);
|
|
1629
|
+
this.finishQueuedPrompt(queued, cancellation ? 'cancelled' : 'failed', error);
|
|
1630
|
+
if (state.lifecycle !== 'active') return;
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
private createActiveRun(queued: IQueuedPrompt): IActiveRun {
|
|
1636
|
+
return {
|
|
1637
|
+
state: queued.state,
|
|
1638
|
+
stored: queued.stored,
|
|
1639
|
+
scopeId: queued.scopeId,
|
|
1640
|
+
scope: queued.scope,
|
|
1641
|
+
sessionId: queued.sessionId,
|
|
1642
|
+
queueId: queued.queueId,
|
|
1643
|
+
runId: queued.runId!,
|
|
1359
1644
|
userMessageId: plugins.crypto.randomUUID(),
|
|
1360
1645
|
assistantMessageId: plugins.crypto.randomUUID(),
|
|
1361
1646
|
controller: new AbortController(),
|
|
1362
|
-
...(scheduleKey === undefined
|
|
1647
|
+
...(queued.scheduleKey === undefined
|
|
1648
|
+
? {}
|
|
1649
|
+
: { scheduleKey: queued.scheduleKey, debounceMs: queued.debounceMs }),
|
|
1363
1650
|
callbackEventCount: 0,
|
|
1364
1651
|
callbackOutputBytes: 0,
|
|
1365
1652
|
callbackParts: [],
|
|
@@ -1368,40 +1655,59 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1368
1655
|
toolPartIds: new Map(),
|
|
1369
1656
|
pendingPermissionIds: new Set(),
|
|
1370
1657
|
phase: 'admitting',
|
|
1371
|
-
completion,
|
|
1372
|
-
resolveCompletion,
|
|
1373
|
-
rejectCompletion,
|
|
1658
|
+
completion: queued.completion,
|
|
1374
1659
|
};
|
|
1375
|
-
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
private assertPromptPromotion(queued: IQueuedPrompt, run: IActiveRun): void {
|
|
1663
|
+
if (run.controller.signal.aborted) {
|
|
1664
|
+
throw run.controller.signal.reason ?? new FlexHarnessAbortError();
|
|
1665
|
+
}
|
|
1666
|
+
this.assertStateAcceptingWork(run.state);
|
|
1667
|
+
if (
|
|
1668
|
+
run.state.sessions.get(run.sessionId) !== run.stored
|
|
1669
|
+
|| run.stored.outstandingPromptsById.get(run.queueId) !== queued
|
|
1670
|
+
|| run.state.activeRuns.get(run.sessionId) !== run
|
|
1671
|
+
) throw this.trustInternalError(new FlexHarnessAbortError('The prompt was retired during admission.'));
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
private async promoteQueuedPrompt(queued: IQueuedPrompt, run: IActiveRun): Promise<void> {
|
|
1675
|
+
const prompt = queued.prompt!;
|
|
1676
|
+
const options = queued.options!;
|
|
1376
1677
|
let projectionReserved = false;
|
|
1377
1678
|
try {
|
|
1378
|
-
run.transaction = await stored.agentSession.beginGeneration(
|
|
1679
|
+
run.transaction = await run.stored.agentSession.beginGeneration(
|
|
1379
1680
|
cloneSerializable(prompt.modelMessage.content) as Parameters<
|
|
1380
1681
|
plugins.IAgentSession['beginGeneration']
|
|
1381
1682
|
>[0],
|
|
1382
1683
|
{ generationId: run.runId },
|
|
1383
1684
|
);
|
|
1685
|
+
this.assertPromptPromotion(queued, run);
|
|
1384
1686
|
const reservation = this.createReservation(run, prompt);
|
|
1385
|
-
await this.mutateProjection(state, stored, () => {
|
|
1386
|
-
stored.messages.push(reservation.userMessage, reservation.assistantMessage);
|
|
1687
|
+
await this.mutateProjection(run.state, run.stored, () => {
|
|
1688
|
+
run.stored.messages.push(reservation.userMessage, reservation.assistantMessage);
|
|
1387
1689
|
});
|
|
1388
1690
|
projectionReserved = true;
|
|
1389
1691
|
run.reservedUserMessage = publicSnapshot(reservation.userMessage);
|
|
1390
1692
|
run.reservedAssistantMessage = publicSnapshot(reservation.assistantMessage);
|
|
1391
|
-
|
|
1392
|
-
|
|
1693
|
+
this.assertPromptPromotion(queued, run);
|
|
1694
|
+
await this.mutateScope(run.state, () => {
|
|
1695
|
+
if (run.controller.signal.aborted) {
|
|
1696
|
+
throw run.controller.signal.reason ?? new FlexHarnessAbortError();
|
|
1697
|
+
}
|
|
1698
|
+
if (run.state.tombstones.has(run.sessionId) || run.state.sessions.get(run.sessionId) !== run.stored) {
|
|
1393
1699
|
throw new FlexHarnessAbortError('The session was deleted during prompt admission.');
|
|
1394
1700
|
}
|
|
1395
|
-
stored.session.status = scheduleKey ? 'scheduled' : 'running';
|
|
1396
|
-
stored.session.activity = {
|
|
1701
|
+
run.stored.session.status = run.scheduleKey ? 'scheduled' : 'running';
|
|
1702
|
+
run.stored.session.activity = {
|
|
1397
1703
|
runId: run.runId,
|
|
1398
|
-
status: scheduleKey ? 'scheduled' : 'running',
|
|
1704
|
+
status: run.scheduleKey ? 'scheduled' : 'running',
|
|
1399
1705
|
startedAt: reservation.userMessage.createdAt,
|
|
1400
1706
|
};
|
|
1401
|
-
stored.session.updatedAt = reservation.userMessage.createdAt;
|
|
1707
|
+
run.stored.session.updatedAt = reservation.userMessage.createdAt;
|
|
1402
1708
|
});
|
|
1403
1709
|
} catch (error) {
|
|
1404
|
-
if (containsStoreCommitUncertainty(error)) state.lifecycle = 'fenced';
|
|
1710
|
+
if (containsStoreCommitUncertainty(error)) run.state.lifecycle = 'fenced';
|
|
1405
1711
|
const safeError = this.projectExternalError(run, error, projectionReserved ? 'persistence' : 'agentSession');
|
|
1406
1712
|
let cleanupErrors: unknown[] = [];
|
|
1407
1713
|
let cleanupFailure: unknown;
|
|
@@ -1410,30 +1716,28 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1410
1716
|
} catch (rollbackError) {
|
|
1411
1717
|
cleanupFailure = rollbackError;
|
|
1412
1718
|
}
|
|
1413
|
-
if (state.lifecycle === 'fenced' && cleanupFailure === undefined) {
|
|
1719
|
+
if (run.state.lifecycle === 'fenced' && cleanupFailure === undefined) {
|
|
1414
1720
|
const combined = combineErrors([safeError, ...cleanupErrors]);
|
|
1415
1721
|
try {
|
|
1416
|
-
await this.fenceNamespace(state, run, combined);
|
|
1722
|
+
await this.fenceNamespace(run.state, run, combined);
|
|
1417
1723
|
} catch (fenceError) {
|
|
1418
1724
|
cleanupFailure = fenceError;
|
|
1419
1725
|
}
|
|
1420
1726
|
}
|
|
1421
|
-
if (state.activeRuns.get(sessionId) === run) state.activeRuns.delete(sessionId);
|
|
1422
1727
|
const rejection = cleanupFailure
|
|
1423
1728
|
?? (cleanupErrors.length > 0 ? combineErrors([safeError, ...cleanupErrors]) : safeError);
|
|
1424
|
-
run.rejectCompletion(rejection);
|
|
1425
1729
|
throw rejection;
|
|
1426
1730
|
}
|
|
1427
1731
|
|
|
1428
|
-
const runType = scheduleKey ? 'run.scheduled' : 'run.started';
|
|
1429
|
-
this.emitEvent(scopeId, sessionId, {
|
|
1732
|
+
const runType = run.scheduleKey ? 'run.scheduled' : 'run.started';
|
|
1733
|
+
this.emitEvent(run.scopeId, run.sessionId, {
|
|
1430
1734
|
type: runType,
|
|
1431
1735
|
runId: run.runId,
|
|
1432
1736
|
messageId: run.userMessageId,
|
|
1433
|
-
session: publicSnapshot(stored.session),
|
|
1737
|
+
session: publicSnapshot(run.stored.session),
|
|
1434
1738
|
});
|
|
1435
1739
|
for (const message of [run.reservedUserMessage!, run.reservedAssistantMessage!]) {
|
|
1436
|
-
this.emitEvent(scopeId, sessionId, {
|
|
1740
|
+
this.emitEvent(run.scopeId, run.sessionId, {
|
|
1437
1741
|
type: 'message.created',
|
|
1438
1742
|
runId: run.runId,
|
|
1439
1743
|
messageId: message.messageId,
|
|
@@ -1441,22 +1745,144 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1441
1745
|
});
|
|
1442
1746
|
}
|
|
1443
1747
|
|
|
1444
|
-
|
|
1748
|
+
queued.status = run.scheduleKey ? 'scheduled' : 'starting';
|
|
1749
|
+
this.emitPromptQueueEvent(queued, 'prompt.started');
|
|
1750
|
+
queued.resolveStarted(Object.freeze({
|
|
1751
|
+
queueId: queued.queueId,
|
|
1752
|
+
runId: run.runId,
|
|
1753
|
+
completion: queued.completion,
|
|
1754
|
+
}));
|
|
1755
|
+
run.phase = run.scheduleKey ? 'scheduled' : 'running';
|
|
1445
1756
|
const generateOptions = {
|
|
1446
1757
|
transaction: run.transaction,
|
|
1447
1758
|
abort: run.controller.signal,
|
|
1448
1759
|
prepare: (context: { generationId: string; abortSignal: AbortSignal }) =>
|
|
1449
1760
|
this.prepareGeneration(run, options, context.abortSignal),
|
|
1450
1761
|
};
|
|
1451
|
-
const generation = scheduleKey
|
|
1452
|
-
? stored.agentSession.scheduleGenerate({
|
|
1453
|
-
key: scheduleKey,
|
|
1454
|
-
debounceMs,
|
|
1762
|
+
const generation = run.scheduleKey
|
|
1763
|
+
? run.stored.agentSession.scheduleGenerate({
|
|
1764
|
+
key: run.scheduleKey,
|
|
1765
|
+
debounceMs: run.debounceMs,
|
|
1455
1766
|
...generateOptions,
|
|
1456
1767
|
})
|
|
1457
|
-
: stored.agentSession.generate(generateOptions);
|
|
1458
|
-
|
|
1459
|
-
|
|
1768
|
+
: run.stored.agentSession.generate(generateOptions);
|
|
1769
|
+
const execution = this.executeRun(run, generation).then(
|
|
1770
|
+
(result) => this.finishQueuedPrompt(queued, 'completed', undefined, result),
|
|
1771
|
+
(error) => this.finishQueuedPrompt(
|
|
1772
|
+
queued,
|
|
1773
|
+
isAbortError(error) ? 'cancelled' : 'failed',
|
|
1774
|
+
error,
|
|
1775
|
+
),
|
|
1776
|
+
).finally(() => this.schedulePromptQueueDrain(run.state, run.stored));
|
|
1777
|
+
void execution.catch(() => undefined);
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
private projectPromptQueueEntry(queued: IQueuedPrompt): IFlexPromptQueueEntry {
|
|
1781
|
+
return {
|
|
1782
|
+
queueId: queued.queueId,
|
|
1783
|
+
queueSequence: queued.queueSequence,
|
|
1784
|
+
scopeId: queued.scopeId,
|
|
1785
|
+
sessionId: queued.sessionId,
|
|
1786
|
+
status: queued.status,
|
|
1787
|
+
queuedAt: queued.queuedAt,
|
|
1788
|
+
...(queued.runId ? { runId: queued.runId } : {}),
|
|
1789
|
+
...(queued.scheduleKey ? { scheduleKey: queued.scheduleKey } : {}),
|
|
1790
|
+
...(queued.startedAt ? { startedAt: queued.startedAt } : {}),
|
|
1791
|
+
};
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
private promptQueueEntry(
|
|
1795
|
+
stored: IStoredSessionState,
|
|
1796
|
+
queueId: string,
|
|
1797
|
+
): IFlexPromptQueueEntry | undefined {
|
|
1798
|
+
const outstanding = stored.outstandingPromptsById.get(queueId);
|
|
1799
|
+
return outstanding
|
|
1800
|
+
? this.projectPromptQueueEntry(outstanding)
|
|
1801
|
+
: stored.terminalPromptQueueEntries.get(queueId);
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
private emitPromptQueueEvent(
|
|
1805
|
+
queued: IQueuedPrompt,
|
|
1806
|
+
type: 'prompt.queued' | 'prompt.started' | 'prompt.running' | 'prompt.finished',
|
|
1807
|
+
entry: IFlexPromptQueueEntry = this.projectPromptQueueEntry(queued),
|
|
1808
|
+
): void {
|
|
1809
|
+
this.emitEvent(queued.scopeId, queued.sessionId, {
|
|
1810
|
+
type,
|
|
1811
|
+
queueId: queued.queueId,
|
|
1812
|
+
...(entry.runId ? { runId: entry.runId } : {}),
|
|
1813
|
+
entry: publicSnapshot(entry),
|
|
1814
|
+
});
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
private finishQueuedPrompt(
|
|
1818
|
+
queued: IQueuedPrompt,
|
|
1819
|
+
status: 'completed' | 'failed' | 'cancelled',
|
|
1820
|
+
error?: unknown,
|
|
1821
|
+
result?: IFlexPromptResult,
|
|
1822
|
+
): void {
|
|
1823
|
+
const stored = queued.stored;
|
|
1824
|
+
if (stored.outstandingPromptsById.get(queued.queueId) !== queued) return;
|
|
1825
|
+
stored.promptQueue = stored.promptQueue.filter((entry) => entry !== queued);
|
|
1826
|
+
stored.outstandingPromptsById.delete(queued.queueId);
|
|
1827
|
+
stored.outstandingPromptBytes = Math.max(0, stored.outstandingPromptBytes - queued.byteSize);
|
|
1828
|
+
delete queued.prompt;
|
|
1829
|
+
delete queued.options;
|
|
1830
|
+
const terminal: IFlexPromptQueueEntry = {
|
|
1831
|
+
...this.projectPromptQueueEntry(queued),
|
|
1832
|
+
status,
|
|
1833
|
+
finishedAt: new Date().toISOString(),
|
|
1834
|
+
...(error === undefined ? {} : { error: errorToInfo(error) }),
|
|
1835
|
+
};
|
|
1836
|
+
stored.terminalPromptQueueEntries.set(queued.queueId, terminal);
|
|
1837
|
+
while (
|
|
1838
|
+
stored.terminalPromptQueueEntries.size
|
|
1839
|
+
> this.promptQueueLimits.maxTerminalEntriesPerSession
|
|
1840
|
+
) {
|
|
1841
|
+
const oldest = stored.terminalPromptQueueEntries.keys().next().value as string | undefined;
|
|
1842
|
+
if (!oldest) break;
|
|
1843
|
+
stored.terminalPromptQueueEntries.delete(oldest);
|
|
1844
|
+
}
|
|
1845
|
+
this.emitPromptQueueEvent(queued, 'prompt.finished', terminal);
|
|
1846
|
+
if (status === 'completed') {
|
|
1847
|
+
if (!result) throw new Error('A completed queued prompt requires a result.');
|
|
1848
|
+
queued.resolveCompletion(result);
|
|
1849
|
+
} else {
|
|
1850
|
+
const rejection = error ?? this.trustInternalError(new FlexHarnessAbortError());
|
|
1851
|
+
queued.rejectStarted(rejection);
|
|
1852
|
+
queued.rejectCompletion(rejection);
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
private cancelQueuedPrompt(queued: IQueuedPrompt, reason: FlexHarnessAbortError): boolean {
|
|
1857
|
+
if (queued.stored.outstandingPromptsById.get(queued.queueId) !== queued) return false;
|
|
1858
|
+
if (queued.status === 'queued') {
|
|
1859
|
+
this.finishQueuedPrompt(queued, 'cancelled', reason);
|
|
1860
|
+
this.schedulePromptQueueDrain(queued.state, queued.stored);
|
|
1861
|
+
return true;
|
|
1862
|
+
}
|
|
1863
|
+
const run = queued.state.activeRuns.get(queued.sessionId);
|
|
1864
|
+
if (!run || run.queueId !== queued.queueId) return false;
|
|
1865
|
+
if (run.phase === 'finalizing' || run.phase === 'promoting') return false;
|
|
1866
|
+
this.cancelRun(run, reason);
|
|
1867
|
+
return true;
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
private cancelStoredPromptQueue(
|
|
1871
|
+
stored: IStoredSessionState,
|
|
1872
|
+
reason: FlexHarnessAbortError,
|
|
1873
|
+
excludedQueueId?: string,
|
|
1874
|
+
): void {
|
|
1875
|
+
for (const queued of [...stored.outstandingPromptsById.values()]) {
|
|
1876
|
+
if (queued.queueId === excludedQueueId) continue;
|
|
1877
|
+
this.cancelQueuedPrompt(queued, reason);
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
private purgeStoredPromptQueue(stored: IStoredSessionState): void {
|
|
1882
|
+
stored.promptQueue.length = 0;
|
|
1883
|
+
stored.outstandingPromptsById.clear();
|
|
1884
|
+
stored.terminalPromptQueueEntries.clear();
|
|
1885
|
+
stored.outstandingPromptBytes = 0;
|
|
1460
1886
|
}
|
|
1461
1887
|
|
|
1462
1888
|
private async executeRun(
|
|
@@ -1597,7 +2023,13 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1597
2023
|
options: IFlexPromptOptions,
|
|
1598
2024
|
signal: AbortSignal,
|
|
1599
2025
|
): Promise<plugins.IAgentGenerationLease> {
|
|
2026
|
+
signal.throwIfAborted();
|
|
1600
2027
|
run.phase = 'running';
|
|
2028
|
+
const queued = run.stored.outstandingPromptsById.get(run.queueId);
|
|
2029
|
+
if (queued && (queued.status === 'starting' || queued.status === 'scheduled')) {
|
|
2030
|
+
queued.status = 'running';
|
|
2031
|
+
this.emitPromptQueueEvent(queued, 'prompt.running');
|
|
2032
|
+
}
|
|
1601
2033
|
const modelOutcome = Promise.resolve()
|
|
1602
2034
|
.then(() => this.modelResolver.resolveModel({
|
|
1603
2035
|
scopeId: run.scopeId,
|
|
@@ -2390,6 +2822,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
2390
2822
|
if (run.internalFailure === undefined) run.ownerCancellation ??= reason;
|
|
2391
2823
|
this.rejectRunPermissions(run.state, run, reason);
|
|
2392
2824
|
if (!run.controller.signal.aborted) run.controller.abort(reason);
|
|
2825
|
+
if (run.phase === 'admitting') return;
|
|
2393
2826
|
if (run.scheduleKey) run.stored.agentSession.cancelScheduledGeneration(run.scheduleKey, reason);
|
|
2394
2827
|
else run.stored.agentSession.abortCurrentGeneration(reason);
|
|
2395
2828
|
}
|
|
@@ -2731,6 +3164,10 @@ export class FlexHarness<TScope = unknown> {
|
|
|
2731
3164
|
agentEventStoreReleased: true,
|
|
2732
3165
|
executionContextCloseCompleted: true,
|
|
2733
3166
|
jobStoreReleased: true,
|
|
3167
|
+
promptQueue: [],
|
|
3168
|
+
outstandingPromptsById: new Map(),
|
|
3169
|
+
terminalPromptQueueEntries: new Map(),
|
|
3170
|
+
outstandingPromptBytes: 0,
|
|
2734
3171
|
};
|
|
2735
3172
|
}
|
|
2736
3173
|
|
|
@@ -2813,6 +3250,10 @@ export class FlexHarness<TScope = unknown> {
|
|
|
2813
3250
|
executionContextCloseCompleted: executionContextHandle?.close === undefined,
|
|
2814
3251
|
jobStoreReleased: this.stores.jobs.releaseSession === undefined,
|
|
2815
3252
|
jobs: executionContextHandle?.context.jobs,
|
|
3253
|
+
promptQueue: [],
|
|
3254
|
+
outstandingPromptsById: new Map(),
|
|
3255
|
+
terminalPromptQueueEntries: new Map(),
|
|
3256
|
+
outstandingPromptBytes: 0,
|
|
2816
3257
|
};
|
|
2817
3258
|
const agentSessionOptions: plugins.IAgentSessionOptions & {
|
|
2818
3259
|
transactionOutcomeErrorProjector: (error: unknown) => string;
|
|
@@ -3325,6 +3766,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
3325
3766
|
if (retained) {
|
|
3326
3767
|
const errors: unknown[] = [];
|
|
3327
3768
|
const reason = this.trustInternalError(new FlexHarnessAbortError('The session was deleted.'));
|
|
3769
|
+
this.cancelStoredPromptQueue(retained.stored, reason);
|
|
3328
3770
|
const run = state.activeRuns.get(sessionId);
|
|
3329
3771
|
if (run) this.cancelRun(run, reason);
|
|
3330
3772
|
if (!retained.stored.agentSessionAbortCompleted) {
|
|
@@ -3346,6 +3788,10 @@ export class FlexHarness<TScope = unknown> {
|
|
|
3346
3788
|
errors.push(settled[0].reason);
|
|
3347
3789
|
}
|
|
3348
3790
|
}
|
|
3791
|
+
if (retained.stored.promptQueueDrain) {
|
|
3792
|
+
const settled = await Promise.allSettled([retained.stored.promptQueueDrain]);
|
|
3793
|
+
if (settled[0].status === 'rejected') errors.push(settled[0].reason);
|
|
3794
|
+
}
|
|
3349
3795
|
try {
|
|
3350
3796
|
await this.closeStoredSession(retained.stored);
|
|
3351
3797
|
} catch (error) {
|
|
@@ -3358,6 +3804,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
3358
3804
|
));
|
|
3359
3805
|
}
|
|
3360
3806
|
if (errors.length > 0) throw combineErrors(errors);
|
|
3807
|
+
this.purgeStoredPromptQueue(retained.stored);
|
|
3361
3808
|
}
|
|
3362
3809
|
if (!retained?.domainsCompleted) {
|
|
3363
3810
|
if (this.hasOrphanedSessionResources(state.storageKey, sessionId)) {
|
|
@@ -3384,6 +3831,13 @@ export class FlexHarness<TScope = unknown> {
|
|
|
3384
3831
|
const reason = this.trustInternalError(new FlexHarnessAbortError('The session namespace was fenced.'));
|
|
3385
3832
|
const cleanupErrors: unknown[] = [];
|
|
3386
3833
|
try {
|
|
3834
|
+
for (const stored of state.sessions.values()) {
|
|
3835
|
+
this.cancelStoredPromptQueue(
|
|
3836
|
+
stored,
|
|
3837
|
+
reason,
|
|
3838
|
+
stored === currentRun.stored ? currentRun.queueId : undefined,
|
|
3839
|
+
);
|
|
3840
|
+
}
|
|
3387
3841
|
const otherRuns = [...state.activeRuns.values()].filter((run) => run !== currentRun);
|
|
3388
3842
|
for (const run of otherRuns) this.cancelRun(run, reason);
|
|
3389
3843
|
const runResults = await Promise.allSettled(otherRuns.map((run) => run.completion));
|
|
@@ -3432,6 +3886,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
3432
3886
|
));
|
|
3433
3887
|
}
|
|
3434
3888
|
}
|
|
3889
|
+
if (stored !== currentRun.stored) this.purgeStoredPromptQueue(stored);
|
|
3435
3890
|
}
|
|
3436
3891
|
cleanupErrors.push(...await this.settleDetachedCleanups(state, detachedCleanupAttempts));
|
|
3437
3892
|
state.fenceAdditionalErrors.length = 0;
|
|
@@ -3677,6 +4132,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
3677
4132
|
if (state.lifecycle !== 'fenced') state.lifecycle = 'retiring';
|
|
3678
4133
|
const detachedCleanupAttempts = this.beginDetachedCleanupSettlement(state);
|
|
3679
4134
|
const errors: unknown[] = [];
|
|
4135
|
+
for (const stored of state.sessions.values()) this.cancelStoredPromptQueue(stored, reason);
|
|
3680
4136
|
for (const run of state.activeRuns.values()) {
|
|
3681
4137
|
if (run.phase !== 'finalizing' && run.phase !== 'promoting') this.cancelRun(run, reason);
|
|
3682
4138
|
}
|
|
@@ -3704,6 +4160,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
3704
4160
|
await Promise.all([...state.sessions.values()].flatMap((stored) => [
|
|
3705
4161
|
stored.projectionQueue,
|
|
3706
4162
|
stored.permissionQueue,
|
|
4163
|
+
...(stored.promptQueueDrain ? [stored.promptQueueDrain] : []),
|
|
3707
4164
|
]));
|
|
3708
4165
|
const tombstoneAttempts = [...state.tombstoneCleanups.entries()];
|
|
3709
4166
|
const tombstoneResults = await Promise.allSettled(
|
|
@@ -3726,6 +4183,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
3726
4183
|
'lifecycle-close',
|
|
3727
4184
|
));
|
|
3728
4185
|
}
|
|
4186
|
+
this.purgeStoredPromptQueue(stored);
|
|
3729
4187
|
}
|
|
3730
4188
|
for (const sessionId of [...state.tombstones.keys()]) {
|
|
3731
4189
|
if (attemptedTombstones.has(sessionId)) continue;
|
|
@@ -3753,6 +4211,14 @@ export class FlexHarness<TScope = unknown> {
|
|
|
3753
4211
|
}
|
|
3754
4212
|
|
|
3755
4213
|
private async disposeInternal(): Promise<void> {
|
|
4214
|
+
const pendingAdmissions = [...this.pendingPromptAdmissionOwners];
|
|
4215
|
+
const pendingAdmissionReason = this.trustInternalError(new FlexHarnessAbortError(
|
|
4216
|
+
'The prompt admission was aborted because FlexHarness was disposed.',
|
|
4217
|
+
));
|
|
4218
|
+
for (const pending of pendingAdmissions) {
|
|
4219
|
+
if (!pending.controller.signal.aborted) pending.controller.abort(pendingAdmissionReason);
|
|
4220
|
+
}
|
|
4221
|
+
await Promise.all(pendingAdmissions.map((pending) => pending.settled));
|
|
3756
4222
|
const loads = [...this.stateLoads.entries()];
|
|
3757
4223
|
const results = await Promise.allSettled(loads.map(([storageKey, stateLoad]) =>
|
|
3758
4224
|
this.drainStorage(
|
|
@@ -3878,10 +4344,10 @@ export class FlexHarness<TScope = unknown> {
|
|
|
3878
4344
|
const stored = this.requireSession(state, sessionId);
|
|
3879
4345
|
const pending = [...state.pendingPermissions.values()].some((entry) =>
|
|
3880
4346
|
!entry.settled && entry.request.sessionId === sessionId);
|
|
3881
|
-
if (
|
|
4347
|
+
if (stored.outstandingPromptsById.size > 0 || pending) {
|
|
3882
4348
|
throw new FlexHarnessSessionBusyError(
|
|
3883
4349
|
sessionId,
|
|
3884
|
-
'cannot be changed while it has an
|
|
4350
|
+
'cannot be changed while it has an outstanding prompt or pending permission',
|
|
3885
4351
|
);
|
|
3886
4352
|
}
|
|
3887
4353
|
return stored;
|