@modelprofile.com/flexharness 3.1.0 → 3.2.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 +10 -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 +438 -78
- 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 +540 -76
- 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
|
|
|
@@ -1021,7 +1097,19 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1021
1097
|
options: IFlexPromptOptions = {},
|
|
1022
1098
|
): Promise<IFlexPromptAdmission> {
|
|
1023
1099
|
validatePromptOptions(options, false);
|
|
1024
|
-
|
|
1100
|
+
const queued = await this.enqueuePromptInternal(scopeId, sessionId, prompt, options);
|
|
1101
|
+
return queued.started;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
public async enqueuePrompt(
|
|
1105
|
+
scopeId: string,
|
|
1106
|
+
sessionId: string,
|
|
1107
|
+
prompt: TFlexPrompt,
|
|
1108
|
+
options: IFlexPromptOptions = {},
|
|
1109
|
+
): Promise<IFlexPromptQueueAdmission> {
|
|
1110
|
+
validatePromptOptions(options, false);
|
|
1111
|
+
const queued = await this.enqueuePromptInternal(scopeId, sessionId, prompt, options);
|
|
1112
|
+
return queued.admission;
|
|
1025
1113
|
}
|
|
1026
1114
|
|
|
1027
1115
|
public async schedulePrompt(
|
|
@@ -1040,17 +1128,66 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1040
1128
|
`debounceMs must be an integer from 0 through ${maxScheduleDebounceMs}.`,
|
|
1041
1129
|
);
|
|
1042
1130
|
}
|
|
1043
|
-
const
|
|
1131
|
+
const queued = await this.enqueuePromptInternal(
|
|
1044
1132
|
scopeId,
|
|
1045
1133
|
sessionId,
|
|
1046
|
-
|
|
1134
|
+
prompt,
|
|
1047
1135
|
options,
|
|
1048
1136
|
scheduleKey,
|
|
1049
1137
|
debounceMs,
|
|
1050
1138
|
);
|
|
1139
|
+
const admission = await queued.started;
|
|
1051
1140
|
return Object.freeze({ ...admission, scheduleKey });
|
|
1052
1141
|
}
|
|
1053
1142
|
|
|
1143
|
+
public async getPromptQueueEntry(
|
|
1144
|
+
scopeId: string,
|
|
1145
|
+
sessionId: string,
|
|
1146
|
+
queueId: string,
|
|
1147
|
+
): Promise<IFlexPromptQueueEntry> {
|
|
1148
|
+
validateIdentifier(queueId, 'queueId');
|
|
1149
|
+
requireTransferIdentifier(queueId, 'queueId');
|
|
1150
|
+
const { state } = await this.resolveState(scopeId);
|
|
1151
|
+
this.assertStateAcceptingWork(state);
|
|
1152
|
+
const stored = this.requireSession(state, sessionId);
|
|
1153
|
+
const entry = this.promptQueueEntry(stored, queueId);
|
|
1154
|
+
if (!entry) throw new FlexHarnessNotFoundError('Prompt queue entry', queueId);
|
|
1155
|
+
return publicSnapshot(entry);
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
public async listPromptQueueEntries(
|
|
1159
|
+
scopeId: string,
|
|
1160
|
+
sessionId: string,
|
|
1161
|
+
): Promise<IFlexPromptQueueEntry[]> {
|
|
1162
|
+
const { state } = await this.resolveState(scopeId);
|
|
1163
|
+
this.assertStateAcceptingWork(state);
|
|
1164
|
+
const stored = this.requireSession(state, sessionId);
|
|
1165
|
+
return publicSnapshot([
|
|
1166
|
+
...[...stored.outstandingPromptsById.values()].map((entry) => this.projectPromptQueueEntry(entry)),
|
|
1167
|
+
...stored.terminalPromptQueueEntries.values(),
|
|
1168
|
+
].sort((left, right) => left.queueSequence - right.queueSequence));
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
public async cancelPrompt(
|
|
1172
|
+
scopeId: string,
|
|
1173
|
+
sessionId: string,
|
|
1174
|
+
queueId: string,
|
|
1175
|
+
): Promise<boolean> {
|
|
1176
|
+
validateIdentifier(queueId, 'queueId');
|
|
1177
|
+
requireTransferIdentifier(queueId, 'queueId');
|
|
1178
|
+
const { state } = await this.resolveState(scopeId);
|
|
1179
|
+
const stored = this.requireSession(state, sessionId);
|
|
1180
|
+
const queued = stored.outstandingPromptsById.get(queueId);
|
|
1181
|
+
if (!queued) {
|
|
1182
|
+
if (stored.terminalPromptQueueEntries.has(queueId)) return false;
|
|
1183
|
+
throw new FlexHarnessNotFoundError('Prompt queue entry', queueId);
|
|
1184
|
+
}
|
|
1185
|
+
return this.cancelQueuedPrompt(
|
|
1186
|
+
queued,
|
|
1187
|
+
this.trustInternalError(new FlexHarnessAbortError('The queued prompt was cancelled.')),
|
|
1188
|
+
);
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1054
1191
|
public async cancelScheduledPrompt(
|
|
1055
1192
|
scopeId: string,
|
|
1056
1193
|
sessionId: string,
|
|
@@ -1058,18 +1195,12 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1058
1195
|
): Promise<boolean> {
|
|
1059
1196
|
validateIdentifier(scheduleKey, 'scheduleKey');
|
|
1060
1197
|
const { state } = await this.resolveState(scopeId);
|
|
1061
|
-
const
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1198
|
+
const stored = this.requireSession(state, sessionId);
|
|
1199
|
+
const queued = [...stored.outstandingPromptsById.values()]
|
|
1200
|
+
.find((entry) => entry.scheduleKey === scheduleKey);
|
|
1201
|
+
if (!queued) return false;
|
|
1065
1202
|
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;
|
|
1203
|
+
return this.cancelQueuedPrompt(queued, cancellation);
|
|
1073
1204
|
}
|
|
1074
1205
|
|
|
1075
1206
|
public async abort(scopeId: string, sessionId: string): Promise<boolean> {
|
|
@@ -1321,45 +1452,199 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1321
1452
|
return disposal;
|
|
1322
1453
|
}
|
|
1323
1454
|
|
|
1324
|
-
private async
|
|
1455
|
+
private async enqueuePromptInternal(
|
|
1325
1456
|
scopeId: string,
|
|
1326
1457
|
sessionId: string,
|
|
1327
|
-
prompt:
|
|
1458
|
+
prompt: TFlexPrompt,
|
|
1328
1459
|
options: IFlexPromptOptions,
|
|
1329
1460
|
scheduleKey?: string,
|
|
1330
1461
|
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;
|
|
1462
|
+
): Promise<{ admission: IFlexPromptQueueAdmission; started: Promise<IFlexPromptAdmission> }> {
|
|
1463
|
+
this.assertOpen();
|
|
1464
|
+
const normalizedPrompt = normalizeFlexPrompt(prompt);
|
|
1465
|
+
const normalizedOptions = cloneSerializable(options);
|
|
1466
|
+
const byteSize = jsonBytes({
|
|
1467
|
+
prompt: normalizedPrompt,
|
|
1468
|
+
options: normalizedOptions,
|
|
1469
|
+
scheduleKey,
|
|
1470
|
+
debounceMs,
|
|
1350
1471
|
});
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1472
|
+
const releasePendingAdmission = this.reservePendingPromptAdmission(byteSize);
|
|
1473
|
+
let resolveSettled!: () => void;
|
|
1474
|
+
const pendingOwner: IPendingPromptAdmission = {
|
|
1475
|
+
controller: new AbortController(),
|
|
1476
|
+
settled: new Promise<void>((resolve) => {
|
|
1477
|
+
resolveSettled = resolve;
|
|
1478
|
+
}),
|
|
1479
|
+
resolveSettled: () => resolveSettled(),
|
|
1480
|
+
};
|
|
1481
|
+
this.pendingPromptAdmissionOwners.add(pendingOwner);
|
|
1482
|
+
let abortAdmission!: () => void;
|
|
1483
|
+
const abortPromise = new Promise<never>((_resolve, reject) => {
|
|
1484
|
+
abortAdmission = () => reject(
|
|
1485
|
+
pendingOwner.controller.signal.reason ?? new FlexHarnessAbortError(),
|
|
1486
|
+
);
|
|
1487
|
+
pendingOwner.controller.signal.addEventListener('abort', abortAdmission, { once: true });
|
|
1488
|
+
if (pendingOwner.controller.signal.aborted) abortAdmission();
|
|
1489
|
+
});
|
|
1490
|
+
try {
|
|
1491
|
+
const resolved = await Promise.race([this.resolveState(scopeId), abortPromise]);
|
|
1492
|
+
const state = resolved.state;
|
|
1493
|
+
await Promise.race([state.scopeQueue, abortPromise]);
|
|
1494
|
+
this.assertStateAcceptingWork(state);
|
|
1495
|
+
const stored = this.requireSession(state, sessionId);
|
|
1496
|
+
if (stored.outstandingPromptsById.size >= this.promptQueueLimits.maxOutstandingPromptsPerSession) {
|
|
1497
|
+
throw new FlexHarnessQueueFullError(
|
|
1498
|
+
`Session "${sessionId}" has reached its outstanding prompt limit.`,
|
|
1499
|
+
);
|
|
1500
|
+
}
|
|
1501
|
+
if (
|
|
1502
|
+
stored.outstandingPromptBytes + byteSize
|
|
1503
|
+
> this.promptQueueLimits.maxOutstandingBytesPerSession
|
|
1504
|
+
) {
|
|
1505
|
+
throw new FlexHarnessQueueFullError(
|
|
1506
|
+
`Session "${sessionId}" has reached its outstanding prompt byte limit.`,
|
|
1507
|
+
);
|
|
1508
|
+
}
|
|
1509
|
+
if (
|
|
1510
|
+
scheduleKey
|
|
1511
|
+
&& [...stored.outstandingPromptsById.values()]
|
|
1512
|
+
.some((entry) => entry.scheduleKey === scheduleKey)
|
|
1513
|
+
) {
|
|
1514
|
+
throw new FlexHarnessSessionBusyError(sessionId, `already has scheduled key "${scheduleKey}"`);
|
|
1515
|
+
}
|
|
1516
|
+
let resolveCompletion!: (result: IFlexPromptResult) => void;
|
|
1517
|
+
let rejectCompletion!: (error: unknown) => void;
|
|
1518
|
+
const completion = new Promise<IFlexPromptResult>((resolve, reject) => {
|
|
1519
|
+
resolveCompletion = resolve;
|
|
1520
|
+
rejectCompletion = reject;
|
|
1521
|
+
});
|
|
1522
|
+
void completion.catch(() => undefined);
|
|
1523
|
+
let resolveStarted!: (admission: IFlexPromptAdmission) => void;
|
|
1524
|
+
let rejectStarted!: (error: unknown) => void;
|
|
1525
|
+
const started = new Promise<IFlexPromptAdmission>((resolve, reject) => {
|
|
1526
|
+
resolveStarted = resolve;
|
|
1527
|
+
rejectStarted = reject;
|
|
1528
|
+
});
|
|
1529
|
+
void started.catch(() => undefined);
|
|
1530
|
+
const queued: IQueuedPrompt = {
|
|
1531
|
+
state,
|
|
1532
|
+
stored,
|
|
1533
|
+
scopeId,
|
|
1534
|
+
scope: resolved.scope.scope,
|
|
1535
|
+
sessionId,
|
|
1536
|
+
queueId: plugins.crypto.randomUUID(),
|
|
1537
|
+
queueSequence: ++this.promptQueueSequence,
|
|
1538
|
+
status: 'queued',
|
|
1539
|
+
queuedAt: new Date().toISOString(),
|
|
1540
|
+
...(scheduleKey === undefined ? {} : { scheduleKey, debounceMs }),
|
|
1541
|
+
prompt: normalizedPrompt,
|
|
1542
|
+
options: normalizedOptions,
|
|
1543
|
+
byteSize,
|
|
1544
|
+
completion,
|
|
1545
|
+
resolveCompletion,
|
|
1546
|
+
rejectCompletion,
|
|
1547
|
+
started,
|
|
1548
|
+
resolveStarted,
|
|
1549
|
+
rejectStarted,
|
|
1550
|
+
};
|
|
1551
|
+
stored.promptQueue.push(queued);
|
|
1552
|
+
stored.outstandingPromptsById.set(queued.queueId, queued);
|
|
1553
|
+
stored.outstandingPromptBytes += byteSize;
|
|
1554
|
+
this.emitPromptQueueEvent(queued, 'prompt.queued');
|
|
1555
|
+
this.schedulePromptQueueDrain(state, stored);
|
|
1556
|
+
return {
|
|
1557
|
+
admission: Object.freeze({ queueId: queued.queueId, completion }),
|
|
1558
|
+
started,
|
|
1559
|
+
};
|
|
1560
|
+
} finally {
|
|
1561
|
+
pendingOwner.controller.signal.removeEventListener('abort', abortAdmission);
|
|
1562
|
+
this.pendingPromptAdmissionOwners.delete(pendingOwner);
|
|
1563
|
+
releasePendingAdmission();
|
|
1564
|
+
pendingOwner.resolveSettled();
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
private reservePendingPromptAdmission(byteSize: number): () => void {
|
|
1569
|
+
if (this.pendingPromptAdmissions >= this.promptQueueLimits.maxPendingAdmissions) {
|
|
1570
|
+
throw new FlexHarnessQueueFullError('FlexHarness has reached its pending prompt admission limit.');
|
|
1571
|
+
}
|
|
1572
|
+
if (
|
|
1573
|
+
this.pendingPromptAdmissionBytes + byteSize
|
|
1574
|
+
> this.promptQueueLimits.maxPendingAdmissionBytes
|
|
1575
|
+
) {
|
|
1576
|
+
throw new FlexHarnessQueueFullError('FlexHarness has reached its pending prompt admission byte limit.');
|
|
1577
|
+
}
|
|
1578
|
+
this.pendingPromptAdmissions++;
|
|
1579
|
+
this.pendingPromptAdmissionBytes += byteSize;
|
|
1580
|
+
let released = false;
|
|
1581
|
+
return () => {
|
|
1582
|
+
if (released) return;
|
|
1583
|
+
released = true;
|
|
1584
|
+
this.pendingPromptAdmissions--;
|
|
1585
|
+
this.pendingPromptAdmissionBytes -= byteSize;
|
|
1586
|
+
};
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
private schedulePromptQueueDrain(state: IStorageState, stored: IStoredSessionState): void {
|
|
1590
|
+
if (stored.promptQueueDrain) return;
|
|
1591
|
+
let drain!: Promise<void>;
|
|
1592
|
+
drain = Promise.resolve()
|
|
1593
|
+
.then(() => this.drainPromptQueue(state, stored))
|
|
1594
|
+
.finally(() => {
|
|
1595
|
+
if (stored.promptQueueDrain === drain) delete stored.promptQueueDrain;
|
|
1596
|
+
if (
|
|
1597
|
+
stored.promptQueue.length > 0
|
|
1598
|
+
&& !state.activeRuns.has(stored.session.sessionId)
|
|
1599
|
+
&& state.lifecycle === 'active'
|
|
1600
|
+
&& state.sessions.get(stored.session.sessionId) === stored
|
|
1601
|
+
) this.schedulePromptQueueDrain(state, stored);
|
|
1602
|
+
});
|
|
1603
|
+
stored.promptQueueDrain = drain;
|
|
1604
|
+
void drain.catch(() => undefined);
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
private async drainPromptQueue(state: IStorageState, stored: IStoredSessionState): Promise<void> {
|
|
1608
|
+
while (
|
|
1609
|
+
state.lifecycle === 'active'
|
|
1610
|
+
&& state.sessions.get(stored.session.sessionId) === stored
|
|
1611
|
+
&& !state.activeRuns.has(stored.session.sessionId)
|
|
1612
|
+
) {
|
|
1613
|
+
const queued = stored.promptQueue.shift();
|
|
1614
|
+
if (!queued) return;
|
|
1615
|
+
if (stored.outstandingPromptsById.get(queued.queueId) !== queued) continue;
|
|
1616
|
+
queued.status = 'starting';
|
|
1617
|
+
queued.runId = plugins.crypto.randomUUID();
|
|
1618
|
+
queued.startedAt = new Date().toISOString();
|
|
1619
|
+
const run = this.createActiveRun(queued);
|
|
1620
|
+
state.activeRuns.set(queued.sessionId, run);
|
|
1621
|
+
try {
|
|
1622
|
+
await this.promoteQueuedPrompt(queued, run);
|
|
1623
|
+
return;
|
|
1624
|
+
} catch (error) {
|
|
1625
|
+
if (state.activeRuns.get(queued.sessionId) === run) state.activeRuns.delete(queued.sessionId);
|
|
1626
|
+
const cancellation = isAbortError(error);
|
|
1627
|
+
this.finishQueuedPrompt(queued, cancellation ? 'cancelled' : 'failed', error);
|
|
1628
|
+
if (state.lifecycle !== 'active') return;
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
private createActiveRun(queued: IQueuedPrompt): IActiveRun {
|
|
1634
|
+
return {
|
|
1635
|
+
state: queued.state,
|
|
1636
|
+
stored: queued.stored,
|
|
1637
|
+
scopeId: queued.scopeId,
|
|
1638
|
+
scope: queued.scope,
|
|
1639
|
+
sessionId: queued.sessionId,
|
|
1640
|
+
queueId: queued.queueId,
|
|
1641
|
+
runId: queued.runId!,
|
|
1359
1642
|
userMessageId: plugins.crypto.randomUUID(),
|
|
1360
1643
|
assistantMessageId: plugins.crypto.randomUUID(),
|
|
1361
1644
|
controller: new AbortController(),
|
|
1362
|
-
...(scheduleKey === undefined
|
|
1645
|
+
...(queued.scheduleKey === undefined
|
|
1646
|
+
? {}
|
|
1647
|
+
: { scheduleKey: queued.scheduleKey, debounceMs: queued.debounceMs }),
|
|
1363
1648
|
callbackEventCount: 0,
|
|
1364
1649
|
callbackOutputBytes: 0,
|
|
1365
1650
|
callbackParts: [],
|
|
@@ -1368,40 +1653,59 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1368
1653
|
toolPartIds: new Map(),
|
|
1369
1654
|
pendingPermissionIds: new Set(),
|
|
1370
1655
|
phase: 'admitting',
|
|
1371
|
-
completion,
|
|
1372
|
-
resolveCompletion,
|
|
1373
|
-
rejectCompletion,
|
|
1656
|
+
completion: queued.completion,
|
|
1374
1657
|
};
|
|
1375
|
-
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
private assertPromptPromotion(queued: IQueuedPrompt, run: IActiveRun): void {
|
|
1661
|
+
if (run.controller.signal.aborted) {
|
|
1662
|
+
throw run.controller.signal.reason ?? new FlexHarnessAbortError();
|
|
1663
|
+
}
|
|
1664
|
+
this.assertStateAcceptingWork(run.state);
|
|
1665
|
+
if (
|
|
1666
|
+
run.state.sessions.get(run.sessionId) !== run.stored
|
|
1667
|
+
|| run.stored.outstandingPromptsById.get(run.queueId) !== queued
|
|
1668
|
+
|| run.state.activeRuns.get(run.sessionId) !== run
|
|
1669
|
+
) throw this.trustInternalError(new FlexHarnessAbortError('The prompt was retired during admission.'));
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
private async promoteQueuedPrompt(queued: IQueuedPrompt, run: IActiveRun): Promise<void> {
|
|
1673
|
+
const prompt = queued.prompt!;
|
|
1674
|
+
const options = queued.options!;
|
|
1376
1675
|
let projectionReserved = false;
|
|
1377
1676
|
try {
|
|
1378
|
-
run.transaction = await stored.agentSession.beginGeneration(
|
|
1677
|
+
run.transaction = await run.stored.agentSession.beginGeneration(
|
|
1379
1678
|
cloneSerializable(prompt.modelMessage.content) as Parameters<
|
|
1380
1679
|
plugins.IAgentSession['beginGeneration']
|
|
1381
1680
|
>[0],
|
|
1382
1681
|
{ generationId: run.runId },
|
|
1383
1682
|
);
|
|
1683
|
+
this.assertPromptPromotion(queued, run);
|
|
1384
1684
|
const reservation = this.createReservation(run, prompt);
|
|
1385
|
-
await this.mutateProjection(state, stored, () => {
|
|
1386
|
-
stored.messages.push(reservation.userMessage, reservation.assistantMessage);
|
|
1685
|
+
await this.mutateProjection(run.state, run.stored, () => {
|
|
1686
|
+
run.stored.messages.push(reservation.userMessage, reservation.assistantMessage);
|
|
1387
1687
|
});
|
|
1388
1688
|
projectionReserved = true;
|
|
1389
1689
|
run.reservedUserMessage = publicSnapshot(reservation.userMessage);
|
|
1390
1690
|
run.reservedAssistantMessage = publicSnapshot(reservation.assistantMessage);
|
|
1391
|
-
|
|
1392
|
-
|
|
1691
|
+
this.assertPromptPromotion(queued, run);
|
|
1692
|
+
await this.mutateScope(run.state, () => {
|
|
1693
|
+
if (run.controller.signal.aborted) {
|
|
1694
|
+
throw run.controller.signal.reason ?? new FlexHarnessAbortError();
|
|
1695
|
+
}
|
|
1696
|
+
if (run.state.tombstones.has(run.sessionId) || run.state.sessions.get(run.sessionId) !== run.stored) {
|
|
1393
1697
|
throw new FlexHarnessAbortError('The session was deleted during prompt admission.');
|
|
1394
1698
|
}
|
|
1395
|
-
stored.session.status = scheduleKey ? 'scheduled' : 'running';
|
|
1396
|
-
stored.session.activity = {
|
|
1699
|
+
run.stored.session.status = run.scheduleKey ? 'scheduled' : 'running';
|
|
1700
|
+
run.stored.session.activity = {
|
|
1397
1701
|
runId: run.runId,
|
|
1398
|
-
status: scheduleKey ? 'scheduled' : 'running',
|
|
1702
|
+
status: run.scheduleKey ? 'scheduled' : 'running',
|
|
1399
1703
|
startedAt: reservation.userMessage.createdAt,
|
|
1400
1704
|
};
|
|
1401
|
-
stored.session.updatedAt = reservation.userMessage.createdAt;
|
|
1705
|
+
run.stored.session.updatedAt = reservation.userMessage.createdAt;
|
|
1402
1706
|
});
|
|
1403
1707
|
} catch (error) {
|
|
1404
|
-
if (containsStoreCommitUncertainty(error)) state.lifecycle = 'fenced';
|
|
1708
|
+
if (containsStoreCommitUncertainty(error)) run.state.lifecycle = 'fenced';
|
|
1405
1709
|
const safeError = this.projectExternalError(run, error, projectionReserved ? 'persistence' : 'agentSession');
|
|
1406
1710
|
let cleanupErrors: unknown[] = [];
|
|
1407
1711
|
let cleanupFailure: unknown;
|
|
@@ -1410,30 +1714,28 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1410
1714
|
} catch (rollbackError) {
|
|
1411
1715
|
cleanupFailure = rollbackError;
|
|
1412
1716
|
}
|
|
1413
|
-
if (state.lifecycle === 'fenced' && cleanupFailure === undefined) {
|
|
1717
|
+
if (run.state.lifecycle === 'fenced' && cleanupFailure === undefined) {
|
|
1414
1718
|
const combined = combineErrors([safeError, ...cleanupErrors]);
|
|
1415
1719
|
try {
|
|
1416
|
-
await this.fenceNamespace(state, run, combined);
|
|
1720
|
+
await this.fenceNamespace(run.state, run, combined);
|
|
1417
1721
|
} catch (fenceError) {
|
|
1418
1722
|
cleanupFailure = fenceError;
|
|
1419
1723
|
}
|
|
1420
1724
|
}
|
|
1421
|
-
if (state.activeRuns.get(sessionId) === run) state.activeRuns.delete(sessionId);
|
|
1422
1725
|
const rejection = cleanupFailure
|
|
1423
1726
|
?? (cleanupErrors.length > 0 ? combineErrors([safeError, ...cleanupErrors]) : safeError);
|
|
1424
|
-
run.rejectCompletion(rejection);
|
|
1425
1727
|
throw rejection;
|
|
1426
1728
|
}
|
|
1427
1729
|
|
|
1428
|
-
const runType = scheduleKey ? 'run.scheduled' : 'run.started';
|
|
1429
|
-
this.emitEvent(scopeId, sessionId, {
|
|
1730
|
+
const runType = run.scheduleKey ? 'run.scheduled' : 'run.started';
|
|
1731
|
+
this.emitEvent(run.scopeId, run.sessionId, {
|
|
1430
1732
|
type: runType,
|
|
1431
1733
|
runId: run.runId,
|
|
1432
1734
|
messageId: run.userMessageId,
|
|
1433
|
-
session: publicSnapshot(stored.session),
|
|
1735
|
+
session: publicSnapshot(run.stored.session),
|
|
1434
1736
|
});
|
|
1435
1737
|
for (const message of [run.reservedUserMessage!, run.reservedAssistantMessage!]) {
|
|
1436
|
-
this.emitEvent(scopeId, sessionId, {
|
|
1738
|
+
this.emitEvent(run.scopeId, run.sessionId, {
|
|
1437
1739
|
type: 'message.created',
|
|
1438
1740
|
runId: run.runId,
|
|
1439
1741
|
messageId: message.messageId,
|
|
@@ -1441,22 +1743,144 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1441
1743
|
});
|
|
1442
1744
|
}
|
|
1443
1745
|
|
|
1444
|
-
|
|
1746
|
+
queued.status = run.scheduleKey ? 'scheduled' : 'starting';
|
|
1747
|
+
this.emitPromptQueueEvent(queued, 'prompt.started');
|
|
1748
|
+
queued.resolveStarted(Object.freeze({
|
|
1749
|
+
queueId: queued.queueId,
|
|
1750
|
+
runId: run.runId,
|
|
1751
|
+
completion: queued.completion,
|
|
1752
|
+
}));
|
|
1753
|
+
run.phase = run.scheduleKey ? 'scheduled' : 'running';
|
|
1445
1754
|
const generateOptions = {
|
|
1446
1755
|
transaction: run.transaction,
|
|
1447
1756
|
abort: run.controller.signal,
|
|
1448
1757
|
prepare: (context: { generationId: string; abortSignal: AbortSignal }) =>
|
|
1449
1758
|
this.prepareGeneration(run, options, context.abortSignal),
|
|
1450
1759
|
};
|
|
1451
|
-
const generation = scheduleKey
|
|
1452
|
-
? stored.agentSession.scheduleGenerate({
|
|
1453
|
-
key: scheduleKey,
|
|
1454
|
-
debounceMs,
|
|
1760
|
+
const generation = run.scheduleKey
|
|
1761
|
+
? run.stored.agentSession.scheduleGenerate({
|
|
1762
|
+
key: run.scheduleKey,
|
|
1763
|
+
debounceMs: run.debounceMs,
|
|
1455
1764
|
...generateOptions,
|
|
1456
1765
|
})
|
|
1457
|
-
: stored.agentSession.generate(generateOptions);
|
|
1458
|
-
|
|
1459
|
-
|
|
1766
|
+
: run.stored.agentSession.generate(generateOptions);
|
|
1767
|
+
const execution = this.executeRun(run, generation).then(
|
|
1768
|
+
(result) => this.finishQueuedPrompt(queued, 'completed', undefined, result),
|
|
1769
|
+
(error) => this.finishQueuedPrompt(
|
|
1770
|
+
queued,
|
|
1771
|
+
isAbortError(error) ? 'cancelled' : 'failed',
|
|
1772
|
+
error,
|
|
1773
|
+
),
|
|
1774
|
+
).finally(() => this.schedulePromptQueueDrain(run.state, run.stored));
|
|
1775
|
+
void execution.catch(() => undefined);
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
private projectPromptQueueEntry(queued: IQueuedPrompt): IFlexPromptQueueEntry {
|
|
1779
|
+
return {
|
|
1780
|
+
queueId: queued.queueId,
|
|
1781
|
+
queueSequence: queued.queueSequence,
|
|
1782
|
+
scopeId: queued.scopeId,
|
|
1783
|
+
sessionId: queued.sessionId,
|
|
1784
|
+
status: queued.status,
|
|
1785
|
+
queuedAt: queued.queuedAt,
|
|
1786
|
+
...(queued.runId ? { runId: queued.runId } : {}),
|
|
1787
|
+
...(queued.scheduleKey ? { scheduleKey: queued.scheduleKey } : {}),
|
|
1788
|
+
...(queued.startedAt ? { startedAt: queued.startedAt } : {}),
|
|
1789
|
+
};
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
private promptQueueEntry(
|
|
1793
|
+
stored: IStoredSessionState,
|
|
1794
|
+
queueId: string,
|
|
1795
|
+
): IFlexPromptQueueEntry | undefined {
|
|
1796
|
+
const outstanding = stored.outstandingPromptsById.get(queueId);
|
|
1797
|
+
return outstanding
|
|
1798
|
+
? this.projectPromptQueueEntry(outstanding)
|
|
1799
|
+
: stored.terminalPromptQueueEntries.get(queueId);
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
private emitPromptQueueEvent(
|
|
1803
|
+
queued: IQueuedPrompt,
|
|
1804
|
+
type: 'prompt.queued' | 'prompt.started' | 'prompt.running' | 'prompt.finished',
|
|
1805
|
+
entry: IFlexPromptQueueEntry = this.projectPromptQueueEntry(queued),
|
|
1806
|
+
): void {
|
|
1807
|
+
this.emitEvent(queued.scopeId, queued.sessionId, {
|
|
1808
|
+
type,
|
|
1809
|
+
queueId: queued.queueId,
|
|
1810
|
+
...(entry.runId ? { runId: entry.runId } : {}),
|
|
1811
|
+
entry: publicSnapshot(entry),
|
|
1812
|
+
});
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
private finishQueuedPrompt(
|
|
1816
|
+
queued: IQueuedPrompt,
|
|
1817
|
+
status: 'completed' | 'failed' | 'cancelled',
|
|
1818
|
+
error?: unknown,
|
|
1819
|
+
result?: IFlexPromptResult,
|
|
1820
|
+
): void {
|
|
1821
|
+
const stored = queued.stored;
|
|
1822
|
+
if (stored.outstandingPromptsById.get(queued.queueId) !== queued) return;
|
|
1823
|
+
stored.promptQueue = stored.promptQueue.filter((entry) => entry !== queued);
|
|
1824
|
+
stored.outstandingPromptsById.delete(queued.queueId);
|
|
1825
|
+
stored.outstandingPromptBytes = Math.max(0, stored.outstandingPromptBytes - queued.byteSize);
|
|
1826
|
+
delete queued.prompt;
|
|
1827
|
+
delete queued.options;
|
|
1828
|
+
const terminal: IFlexPromptQueueEntry = {
|
|
1829
|
+
...this.projectPromptQueueEntry(queued),
|
|
1830
|
+
status,
|
|
1831
|
+
finishedAt: new Date().toISOString(),
|
|
1832
|
+
...(error === undefined ? {} : { error: errorToInfo(error) }),
|
|
1833
|
+
};
|
|
1834
|
+
stored.terminalPromptQueueEntries.set(queued.queueId, terminal);
|
|
1835
|
+
while (
|
|
1836
|
+
stored.terminalPromptQueueEntries.size
|
|
1837
|
+
> this.promptQueueLimits.maxTerminalEntriesPerSession
|
|
1838
|
+
) {
|
|
1839
|
+
const oldest = stored.terminalPromptQueueEntries.keys().next().value as string | undefined;
|
|
1840
|
+
if (!oldest) break;
|
|
1841
|
+
stored.terminalPromptQueueEntries.delete(oldest);
|
|
1842
|
+
}
|
|
1843
|
+
this.emitPromptQueueEvent(queued, 'prompt.finished', terminal);
|
|
1844
|
+
if (status === 'completed') {
|
|
1845
|
+
if (!result) throw new Error('A completed queued prompt requires a result.');
|
|
1846
|
+
queued.resolveCompletion(result);
|
|
1847
|
+
} else {
|
|
1848
|
+
const rejection = error ?? this.trustInternalError(new FlexHarnessAbortError());
|
|
1849
|
+
queued.rejectStarted(rejection);
|
|
1850
|
+
queued.rejectCompletion(rejection);
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
private cancelQueuedPrompt(queued: IQueuedPrompt, reason: FlexHarnessAbortError): boolean {
|
|
1855
|
+
if (queued.stored.outstandingPromptsById.get(queued.queueId) !== queued) return false;
|
|
1856
|
+
if (queued.status === 'queued') {
|
|
1857
|
+
this.finishQueuedPrompt(queued, 'cancelled', reason);
|
|
1858
|
+
this.schedulePromptQueueDrain(queued.state, queued.stored);
|
|
1859
|
+
return true;
|
|
1860
|
+
}
|
|
1861
|
+
const run = queued.state.activeRuns.get(queued.sessionId);
|
|
1862
|
+
if (!run || run.queueId !== queued.queueId) return false;
|
|
1863
|
+
if (run.phase === 'finalizing' || run.phase === 'promoting') return false;
|
|
1864
|
+
this.cancelRun(run, reason);
|
|
1865
|
+
return true;
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
private cancelStoredPromptQueue(
|
|
1869
|
+
stored: IStoredSessionState,
|
|
1870
|
+
reason: FlexHarnessAbortError,
|
|
1871
|
+
excludedQueueId?: string,
|
|
1872
|
+
): void {
|
|
1873
|
+
for (const queued of [...stored.outstandingPromptsById.values()]) {
|
|
1874
|
+
if (queued.queueId === excludedQueueId) continue;
|
|
1875
|
+
this.cancelQueuedPrompt(queued, reason);
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
private purgeStoredPromptQueue(stored: IStoredSessionState): void {
|
|
1880
|
+
stored.promptQueue.length = 0;
|
|
1881
|
+
stored.outstandingPromptsById.clear();
|
|
1882
|
+
stored.terminalPromptQueueEntries.clear();
|
|
1883
|
+
stored.outstandingPromptBytes = 0;
|
|
1460
1884
|
}
|
|
1461
1885
|
|
|
1462
1886
|
private async executeRun(
|
|
@@ -1597,7 +2021,13 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1597
2021
|
options: IFlexPromptOptions,
|
|
1598
2022
|
signal: AbortSignal,
|
|
1599
2023
|
): Promise<plugins.IAgentGenerationLease> {
|
|
2024
|
+
signal.throwIfAborted();
|
|
1600
2025
|
run.phase = 'running';
|
|
2026
|
+
const queued = run.stored.outstandingPromptsById.get(run.queueId);
|
|
2027
|
+
if (queued && (queued.status === 'starting' || queued.status === 'scheduled')) {
|
|
2028
|
+
queued.status = 'running';
|
|
2029
|
+
this.emitPromptQueueEvent(queued, 'prompt.running');
|
|
2030
|
+
}
|
|
1601
2031
|
const modelOutcome = Promise.resolve()
|
|
1602
2032
|
.then(() => this.modelResolver.resolveModel({
|
|
1603
2033
|
scopeId: run.scopeId,
|
|
@@ -2390,6 +2820,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
2390
2820
|
if (run.internalFailure === undefined) run.ownerCancellation ??= reason;
|
|
2391
2821
|
this.rejectRunPermissions(run.state, run, reason);
|
|
2392
2822
|
if (!run.controller.signal.aborted) run.controller.abort(reason);
|
|
2823
|
+
if (run.phase === 'admitting') return;
|
|
2393
2824
|
if (run.scheduleKey) run.stored.agentSession.cancelScheduledGeneration(run.scheduleKey, reason);
|
|
2394
2825
|
else run.stored.agentSession.abortCurrentGeneration(reason);
|
|
2395
2826
|
}
|
|
@@ -2731,6 +3162,10 @@ export class FlexHarness<TScope = unknown> {
|
|
|
2731
3162
|
agentEventStoreReleased: true,
|
|
2732
3163
|
executionContextCloseCompleted: true,
|
|
2733
3164
|
jobStoreReleased: true,
|
|
3165
|
+
promptQueue: [],
|
|
3166
|
+
outstandingPromptsById: new Map(),
|
|
3167
|
+
terminalPromptQueueEntries: new Map(),
|
|
3168
|
+
outstandingPromptBytes: 0,
|
|
2734
3169
|
};
|
|
2735
3170
|
}
|
|
2736
3171
|
|
|
@@ -2813,6 +3248,10 @@ export class FlexHarness<TScope = unknown> {
|
|
|
2813
3248
|
executionContextCloseCompleted: executionContextHandle?.close === undefined,
|
|
2814
3249
|
jobStoreReleased: this.stores.jobs.releaseSession === undefined,
|
|
2815
3250
|
jobs: executionContextHandle?.context.jobs,
|
|
3251
|
+
promptQueue: [],
|
|
3252
|
+
outstandingPromptsById: new Map(),
|
|
3253
|
+
terminalPromptQueueEntries: new Map(),
|
|
3254
|
+
outstandingPromptBytes: 0,
|
|
2816
3255
|
};
|
|
2817
3256
|
const agentSessionOptions: plugins.IAgentSessionOptions & {
|
|
2818
3257
|
transactionOutcomeErrorProjector: (error: unknown) => string;
|
|
@@ -3325,6 +3764,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
3325
3764
|
if (retained) {
|
|
3326
3765
|
const errors: unknown[] = [];
|
|
3327
3766
|
const reason = this.trustInternalError(new FlexHarnessAbortError('The session was deleted.'));
|
|
3767
|
+
this.cancelStoredPromptQueue(retained.stored, reason);
|
|
3328
3768
|
const run = state.activeRuns.get(sessionId);
|
|
3329
3769
|
if (run) this.cancelRun(run, reason);
|
|
3330
3770
|
if (!retained.stored.agentSessionAbortCompleted) {
|
|
@@ -3346,6 +3786,10 @@ export class FlexHarness<TScope = unknown> {
|
|
|
3346
3786
|
errors.push(settled[0].reason);
|
|
3347
3787
|
}
|
|
3348
3788
|
}
|
|
3789
|
+
if (retained.stored.promptQueueDrain) {
|
|
3790
|
+
const settled = await Promise.allSettled([retained.stored.promptQueueDrain]);
|
|
3791
|
+
if (settled[0].status === 'rejected') errors.push(settled[0].reason);
|
|
3792
|
+
}
|
|
3349
3793
|
try {
|
|
3350
3794
|
await this.closeStoredSession(retained.stored);
|
|
3351
3795
|
} catch (error) {
|
|
@@ -3358,6 +3802,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
3358
3802
|
));
|
|
3359
3803
|
}
|
|
3360
3804
|
if (errors.length > 0) throw combineErrors(errors);
|
|
3805
|
+
this.purgeStoredPromptQueue(retained.stored);
|
|
3361
3806
|
}
|
|
3362
3807
|
if (!retained?.domainsCompleted) {
|
|
3363
3808
|
if (this.hasOrphanedSessionResources(state.storageKey, sessionId)) {
|
|
@@ -3384,6 +3829,13 @@ export class FlexHarness<TScope = unknown> {
|
|
|
3384
3829
|
const reason = this.trustInternalError(new FlexHarnessAbortError('The session namespace was fenced.'));
|
|
3385
3830
|
const cleanupErrors: unknown[] = [];
|
|
3386
3831
|
try {
|
|
3832
|
+
for (const stored of state.sessions.values()) {
|
|
3833
|
+
this.cancelStoredPromptQueue(
|
|
3834
|
+
stored,
|
|
3835
|
+
reason,
|
|
3836
|
+
stored === currentRun.stored ? currentRun.queueId : undefined,
|
|
3837
|
+
);
|
|
3838
|
+
}
|
|
3387
3839
|
const otherRuns = [...state.activeRuns.values()].filter((run) => run !== currentRun);
|
|
3388
3840
|
for (const run of otherRuns) this.cancelRun(run, reason);
|
|
3389
3841
|
const runResults = await Promise.allSettled(otherRuns.map((run) => run.completion));
|
|
@@ -3432,6 +3884,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
3432
3884
|
));
|
|
3433
3885
|
}
|
|
3434
3886
|
}
|
|
3887
|
+
if (stored !== currentRun.stored) this.purgeStoredPromptQueue(stored);
|
|
3435
3888
|
}
|
|
3436
3889
|
cleanupErrors.push(...await this.settleDetachedCleanups(state, detachedCleanupAttempts));
|
|
3437
3890
|
state.fenceAdditionalErrors.length = 0;
|
|
@@ -3677,6 +4130,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
3677
4130
|
if (state.lifecycle !== 'fenced') state.lifecycle = 'retiring';
|
|
3678
4131
|
const detachedCleanupAttempts = this.beginDetachedCleanupSettlement(state);
|
|
3679
4132
|
const errors: unknown[] = [];
|
|
4133
|
+
for (const stored of state.sessions.values()) this.cancelStoredPromptQueue(stored, reason);
|
|
3680
4134
|
for (const run of state.activeRuns.values()) {
|
|
3681
4135
|
if (run.phase !== 'finalizing' && run.phase !== 'promoting') this.cancelRun(run, reason);
|
|
3682
4136
|
}
|
|
@@ -3704,6 +4158,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
3704
4158
|
await Promise.all([...state.sessions.values()].flatMap((stored) => [
|
|
3705
4159
|
stored.projectionQueue,
|
|
3706
4160
|
stored.permissionQueue,
|
|
4161
|
+
...(stored.promptQueueDrain ? [stored.promptQueueDrain] : []),
|
|
3707
4162
|
]));
|
|
3708
4163
|
const tombstoneAttempts = [...state.tombstoneCleanups.entries()];
|
|
3709
4164
|
const tombstoneResults = await Promise.allSettled(
|
|
@@ -3726,6 +4181,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
3726
4181
|
'lifecycle-close',
|
|
3727
4182
|
));
|
|
3728
4183
|
}
|
|
4184
|
+
this.purgeStoredPromptQueue(stored);
|
|
3729
4185
|
}
|
|
3730
4186
|
for (const sessionId of [...state.tombstones.keys()]) {
|
|
3731
4187
|
if (attemptedTombstones.has(sessionId)) continue;
|
|
@@ -3753,6 +4209,14 @@ export class FlexHarness<TScope = unknown> {
|
|
|
3753
4209
|
}
|
|
3754
4210
|
|
|
3755
4211
|
private async disposeInternal(): Promise<void> {
|
|
4212
|
+
const pendingAdmissions = [...this.pendingPromptAdmissionOwners];
|
|
4213
|
+
const pendingAdmissionReason = this.trustInternalError(new FlexHarnessAbortError(
|
|
4214
|
+
'The prompt admission was aborted because FlexHarness was disposed.',
|
|
4215
|
+
));
|
|
4216
|
+
for (const pending of pendingAdmissions) {
|
|
4217
|
+
if (!pending.controller.signal.aborted) pending.controller.abort(pendingAdmissionReason);
|
|
4218
|
+
}
|
|
4219
|
+
await Promise.all(pendingAdmissions.map((pending) => pending.settled));
|
|
3756
4220
|
const loads = [...this.stateLoads.entries()];
|
|
3757
4221
|
const results = await Promise.allSettled(loads.map(([storageKey, stateLoad]) =>
|
|
3758
4222
|
this.drainStorage(
|
|
@@ -3878,10 +4342,10 @@ export class FlexHarness<TScope = unknown> {
|
|
|
3878
4342
|
const stored = this.requireSession(state, sessionId);
|
|
3879
4343
|
const pending = [...state.pendingPermissions.values()].some((entry) =>
|
|
3880
4344
|
!entry.settled && entry.request.sessionId === sessionId);
|
|
3881
|
-
if (
|
|
4345
|
+
if (stored.outstandingPromptsById.size > 0 || pending) {
|
|
3882
4346
|
throw new FlexHarnessSessionBusyError(
|
|
3883
4347
|
sessionId,
|
|
3884
|
-
'cannot be changed while it has an
|
|
4348
|
+
'cannot be changed while it has an outstanding prompt or pending permission',
|
|
3885
4349
|
);
|
|
3886
4350
|
}
|
|
3887
4351
|
return stored;
|