@modelprofile.com/flexharness 10.24.0 → 10.24.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.flexharness.admission.d.ts +17 -0
- package/dist_ts/classes.flexharness.admission.js +263 -0
- package/dist_ts/classes.flexharness.callbacks.d.ts +38 -0
- package/dist_ts/classes.flexharness.callbacks.js +313 -0
- package/dist_ts/classes.flexharness.compaction.d.ts +25 -0
- package/dist_ts/classes.flexharness.compaction.js +87 -0
- package/dist_ts/classes.flexharness.configurations.d.ts +11 -0
- package/dist_ts/classes.flexharness.configurations.js +137 -0
- package/dist_ts/classes.flexharness.core.d.ts +143 -0
- package/dist_ts/classes.flexharness.core.js +1538 -0
- package/dist_ts/classes.flexharness.d.ts +2 -437
- package/dist_ts/classes.flexharness.deletion.d.ts +31 -0
- package/dist_ts/classes.flexharness.deletion.js +396 -0
- package/dist_ts/classes.flexharness.imports.d.ts +24 -0
- package/dist_ts/classes.flexharness.imports.js +345 -0
- package/dist_ts/classes.flexharness.js +119 -9776
- package/dist_ts/classes.flexharness.lifecycle.d.ts +47 -0
- package/dist_ts/classes.flexharness.lifecycle.js +625 -0
- package/dist_ts/classes.flexharness.permissions.d.ts +19 -0
- package/dist_ts/classes.flexharness.permissions.js +450 -0
- package/dist_ts/classes.flexharness.projects.d.ts +55 -0
- package/dist_ts/classes.flexharness.projects.js +575 -0
- package/dist_ts/classes.flexharness.prompts.d.ts +44 -0
- package/dist_ts/classes.flexharness.prompts.js +659 -0
- package/dist_ts/classes.flexharness.providers.d.ts +18 -0
- package/dist_ts/classes.flexharness.providers.js +267 -0
- package/dist_ts/classes.flexharness.results.d.ts +26 -0
- package/dist_ts/classes.flexharness.results.js +89 -0
- package/dist_ts/classes.flexharness.reversions.d.ts +52 -0
- package/dist_ts/classes.flexharness.reversions.js +1306 -0
- package/dist_ts/classes.flexharness.runs.d.ts +63 -0
- package/dist_ts/classes.flexharness.runs.js +1058 -0
- package/dist_ts/classes.flexharness.sessions.d.ts +20 -0
- package/dist_ts/classes.flexharness.sessions.js +381 -0
- package/dist_ts/classes.flexharness.slashcommands.d.ts +40 -0
- package/dist_ts/classes.flexharness.slashcommands.js +458 -0
- package/dist_ts/classes.flexharness.subagents.d.ts +31 -0
- package/dist_ts/classes.flexharness.subagents.js +690 -0
- package/dist_ts/classes.flexharness.tooldispatch.d.ts +53 -0
- package/dist_ts/classes.flexharness.tooldispatch.js +418 -0
- package/dist_ts/interfaces.flexharness.d.ts +455 -0
- package/dist_ts/interfaces.flexharness.js +2 -0
- package/dist_ts/utils.flexharness.d.ts +19 -0
- package/dist_ts/utils.flexharness.js +83 -0
- package/package.json +3 -3
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.flexharness.admission.ts +331 -0
- package/ts/classes.flexharness.callbacks.ts +352 -0
- package/ts/classes.flexharness.compaction.ts +167 -0
- package/ts/classes.flexharness.configurations.ts +167 -0
- package/ts/classes.flexharness.core.ts +1830 -0
- package/ts/classes.flexharness.deletion.ts +616 -0
- package/ts/classes.flexharness.imports.ts +414 -0
- package/ts/classes.flexharness.lifecycle.ts +786 -0
- package/ts/classes.flexharness.permissions.ts +535 -0
- package/ts/classes.flexharness.projects.ts +1006 -0
- package/ts/classes.flexharness.prompts.ts +857 -0
- package/ts/classes.flexharness.providers.ts +322 -0
- package/ts/classes.flexharness.results.ts +98 -0
- package/ts/classes.flexharness.reversions.ts +1684 -0
- package/ts/classes.flexharness.runs.ts +1204 -0
- package/ts/classes.flexharness.sessions.ts +483 -0
- package/ts/classes.flexharness.slashcommands.ts +705 -0
- package/ts/classes.flexharness.subagents.ts +860 -0
- package/ts/classes.flexharness.tooldispatch.ts +475 -0
- package/ts/classes.flexharness.ts +147 -12567
- package/ts/interfaces.flexharness.ts +517 -0
- package/ts/utils.flexharness.ts +121 -0
|
@@ -0,0 +1,857 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
import {
|
|
3
|
+
FlexHarnessAbortError,
|
|
4
|
+
FlexHarnessNotFoundError,
|
|
5
|
+
FlexHarnessQueueFullError,
|
|
6
|
+
FlexHarnessSessionBusyError,
|
|
7
|
+
FlexHarnessSlashCommandReentryError,
|
|
8
|
+
FlexHarnessSteerRejectedError,
|
|
9
|
+
FlexHarnessStoreCommitUncertainError,
|
|
10
|
+
FlexHarnessValidationError,
|
|
11
|
+
errorToInfo,
|
|
12
|
+
} from './errors.js';
|
|
13
|
+
import type {
|
|
14
|
+
IFlexAgentContextInvocation,
|
|
15
|
+
IFlexSteerAdmission,
|
|
16
|
+
IFlexSteerInput,
|
|
17
|
+
IFlexPromptAdmission,
|
|
18
|
+
IFlexPromptQueueAdmission,
|
|
19
|
+
IFlexPromptQueueEntry,
|
|
20
|
+
IFlexPromptOptions,
|
|
21
|
+
IFlexPromptResult,
|
|
22
|
+
IFlexScheduledPromptAdmission,
|
|
23
|
+
IFlexSchedulePromptOptions,
|
|
24
|
+
TFlexPrompt,
|
|
25
|
+
} from './interfaces.js';
|
|
26
|
+
import { cloneSerializable } from './utils.json.js';
|
|
27
|
+
import { normalizeFlexPrompt, type INormalizedFlexPrompt } from './utils.prompt.js';
|
|
28
|
+
import { jsonBytes, publicSnapshot, requireTransferIdentifier } from './utils.transfer.js';
|
|
29
|
+
import { validateIdentifier, validatePromptOptions } from './utils.validation.js';
|
|
30
|
+
import { createSessionMetricsOperation } from './utils.sessionmetrics.js';
|
|
31
|
+
import type {
|
|
32
|
+
IStoredSessionState,
|
|
33
|
+
IStorageState,
|
|
34
|
+
IActiveRun,
|
|
35
|
+
IDelegatedRunAdmissionSeed,
|
|
36
|
+
IQueuedPrompt,
|
|
37
|
+
IPendingPromptAdmission,
|
|
38
|
+
} from './interfaces.flexharness.js';
|
|
39
|
+
import { combineErrors, isAbortError } from './utils.flexharness.js';
|
|
40
|
+
import type { FlexHarnessCore } from './classes.flexharness.core.js';
|
|
41
|
+
|
|
42
|
+
const maxScheduleDebounceMs = 24 * 60 * 60 * 1000;
|
|
43
|
+
|
|
44
|
+
function containsStoreCommitUncertainty(error: unknown, seen = new Set<unknown>()): boolean {
|
|
45
|
+
if (error instanceof FlexHarnessStoreCommitUncertainError) return true;
|
|
46
|
+
if (!(error instanceof Error) || seen.has(error)) return false;
|
|
47
|
+
seen.add(error);
|
|
48
|
+
try {
|
|
49
|
+
if (error instanceof AggregateError) {
|
|
50
|
+
for (const nested of error.errors) {
|
|
51
|
+
if (containsStoreCommitUncertainty(nested, seen)) return true;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return containsStoreCommitUncertainty(error.cause, seen);
|
|
55
|
+
} catch {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The prompt queue: admission, scheduling, promotion and cancellation of prompts and steers. */
|
|
61
|
+
export class FlexHarnessPromptQueue<TScope> {
|
|
62
|
+
public readonly pendingPromptAdmissionOwners = new Set<IPendingPromptAdmission>();
|
|
63
|
+
private promptQueueSequence = 0;
|
|
64
|
+
public pendingPromptAdmissions = 0;
|
|
65
|
+
public pendingPromptAdmissionBytes = 0;
|
|
66
|
+
|
|
67
|
+
constructor(private readonly core: FlexHarnessCore<TScope>) {}
|
|
68
|
+
|
|
69
|
+
public promptAdmissionByteSize(
|
|
70
|
+
prompt: TFlexPrompt,
|
|
71
|
+
options: IFlexPromptOptions,
|
|
72
|
+
scheduleKey?: string,
|
|
73
|
+
debounceMs?: number,
|
|
74
|
+
): number {
|
|
75
|
+
return jsonBytes({
|
|
76
|
+
prompt: normalizeFlexPrompt(prompt),
|
|
77
|
+
options: cloneSerializable(options),
|
|
78
|
+
scheduleKey,
|
|
79
|
+
debounceMs,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
public isSessionRuntimeIdle(state: IStorageState, stored: IStoredSessionState): boolean {
|
|
84
|
+
return stored.session.status === 'idle'
|
|
85
|
+
&& !state.activeRuns.has(stored.session.sessionId)
|
|
86
|
+
&& stored.outstandingPromptsById.size === 0;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
public async prompt(
|
|
90
|
+
scopeId: string,
|
|
91
|
+
sessionId: string,
|
|
92
|
+
prompt: TFlexPrompt,
|
|
93
|
+
options: IFlexPromptOptions = {},
|
|
94
|
+
): Promise<IFlexPromptResult> {
|
|
95
|
+
const admission = await this.startPrompt(scopeId, sessionId, prompt, options);
|
|
96
|
+
return admission.completion;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
public async startPrompt(
|
|
100
|
+
scopeId: string,
|
|
101
|
+
sessionId: string,
|
|
102
|
+
prompt: TFlexPrompt,
|
|
103
|
+
options: IFlexPromptOptions = {},
|
|
104
|
+
): Promise<IFlexPromptAdmission> {
|
|
105
|
+
validatePromptOptions(options, false);
|
|
106
|
+
const queued = await this.enqueuePromptInternal(scopeId, sessionId, prompt, options);
|
|
107
|
+
return queued.started;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
public async enqueuePrompt(
|
|
111
|
+
scopeId: string,
|
|
112
|
+
sessionId: string,
|
|
113
|
+
prompt: TFlexPrompt,
|
|
114
|
+
options: IFlexPromptOptions = {},
|
|
115
|
+
): Promise<IFlexPromptQueueAdmission> {
|
|
116
|
+
validatePromptOptions(options, false);
|
|
117
|
+
const queued = await this.enqueuePromptInternal(scopeId, sessionId, prompt, options);
|
|
118
|
+
return queued.admission;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
public async schedulePrompt(
|
|
122
|
+
scopeId: string,
|
|
123
|
+
sessionId: string,
|
|
124
|
+
scheduleKey: string,
|
|
125
|
+
prompt: TFlexPrompt,
|
|
126
|
+
options: IFlexSchedulePromptOptions = {},
|
|
127
|
+
): Promise<IFlexScheduledPromptAdmission> {
|
|
128
|
+
validateIdentifier(scheduleKey, 'scheduleKey');
|
|
129
|
+
requireTransferIdentifier(scheduleKey, 'scheduleKey');
|
|
130
|
+
validatePromptOptions(options, true);
|
|
131
|
+
const debounceMs = options.debounceMs ?? 50;
|
|
132
|
+
if (!Number.isSafeInteger(debounceMs) || debounceMs < 0 || debounceMs > maxScheduleDebounceMs) {
|
|
133
|
+
throw new FlexHarnessValidationError(
|
|
134
|
+
`debounceMs must be an integer from 0 through ${maxScheduleDebounceMs}.`,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
const queued = await this.enqueuePromptInternal(
|
|
138
|
+
scopeId,
|
|
139
|
+
sessionId,
|
|
140
|
+
prompt,
|
|
141
|
+
options,
|
|
142
|
+
scheduleKey,
|
|
143
|
+
debounceMs,
|
|
144
|
+
);
|
|
145
|
+
const admission = await queued.started;
|
|
146
|
+
return Object.freeze({ ...admission, scheduleKey });
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
public async getPromptQueueEntry(
|
|
150
|
+
scopeId: string,
|
|
151
|
+
sessionId: string,
|
|
152
|
+
queueId: string,
|
|
153
|
+
): Promise<IFlexPromptQueueEntry> {
|
|
154
|
+
validateIdentifier(queueId, 'queueId');
|
|
155
|
+
requireTransferIdentifier(queueId, 'queueId');
|
|
156
|
+
const { state } = await this.core.resolveState(scopeId);
|
|
157
|
+
this.core.assertStateAcceptingWork(state);
|
|
158
|
+
const stored = this.core.requireSession(state, sessionId);
|
|
159
|
+
const entry = this.promptQueueEntry(stored, queueId);
|
|
160
|
+
if (!entry) throw new FlexHarnessNotFoundError('Prompt queue entry', queueId);
|
|
161
|
+
return publicSnapshot(entry);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
public async listPromptQueueEntries(
|
|
165
|
+
scopeId: string,
|
|
166
|
+
sessionId: string,
|
|
167
|
+
): Promise<IFlexPromptQueueEntry[]> {
|
|
168
|
+
const { state } = await this.core.resolveState(scopeId);
|
|
169
|
+
this.core.assertStateAcceptingWork(state);
|
|
170
|
+
const stored = this.core.requireSession(state, sessionId);
|
|
171
|
+
return publicSnapshot([
|
|
172
|
+
...[...stored.outstandingPromptsById.values()].map((entry) => this.projectPromptQueueEntry(entry)),
|
|
173
|
+
...stored.terminalPromptQueueEntries.values(),
|
|
174
|
+
].sort((left, right) => left.queueSequence - right.queueSequence));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
public async cancelPrompt(
|
|
178
|
+
scopeId: string,
|
|
179
|
+
sessionId: string,
|
|
180
|
+
queueId: string,
|
|
181
|
+
): Promise<boolean> {
|
|
182
|
+
validateIdentifier(queueId, 'queueId');
|
|
183
|
+
requireTransferIdentifier(queueId, 'queueId');
|
|
184
|
+
const { scope, state } = await this.core.resolveState(scopeId);
|
|
185
|
+
const stored = this.core.requireSession(state, sessionId);
|
|
186
|
+
const queued = stored.outstandingPromptsById.get(queueId);
|
|
187
|
+
if (!queued) {
|
|
188
|
+
if (stored.terminalPromptQueueEntries.has(queueId)) return false;
|
|
189
|
+
throw new FlexHarnessNotFoundError('Prompt queue entry', queueId);
|
|
190
|
+
}
|
|
191
|
+
return this.cancelQueuedPrompt(
|
|
192
|
+
queued,
|
|
193
|
+
this.core.trustInternalError(new FlexHarnessAbortError('The queued prompt was cancelled.')),
|
|
194
|
+
this.core.compaction.createCompactorContext(scopeId, scope.scope, state.storageKey, sessionId),
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
public async cancelScheduledPrompt(
|
|
199
|
+
scopeId: string,
|
|
200
|
+
sessionId: string,
|
|
201
|
+
scheduleKey: string,
|
|
202
|
+
): Promise<boolean> {
|
|
203
|
+
validateIdentifier(scheduleKey, 'scheduleKey');
|
|
204
|
+
const { scope, state } = await this.core.resolveState(scopeId);
|
|
205
|
+
const stored = this.core.requireSession(state, sessionId);
|
|
206
|
+
const queued = [...stored.outstandingPromptsById.values()]
|
|
207
|
+
.find((entry) => entry.scheduleKey === scheduleKey);
|
|
208
|
+
if (!queued) return false;
|
|
209
|
+
const cancellation = this.core.trustInternalError(new FlexHarnessAbortError('The scheduled run was cancelled.'));
|
|
210
|
+
return this.cancelQueuedPrompt(
|
|
211
|
+
queued,
|
|
212
|
+
cancellation,
|
|
213
|
+
this.core.compaction.createCompactorContext(scopeId, scope.scope, state.storageKey, sessionId),
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
public async steerPrompt(
|
|
218
|
+
scopeId: string,
|
|
219
|
+
sessionId: string,
|
|
220
|
+
queueId: string,
|
|
221
|
+
steer: IFlexSteerInput,
|
|
222
|
+
): Promise<IFlexSteerAdmission> {
|
|
223
|
+
validateIdentifier(queueId, 'queueId');
|
|
224
|
+
requireTransferIdentifier(queueId, 'queueId');
|
|
225
|
+
validateIdentifier(steer.steerId, 'steerId');
|
|
226
|
+
requireTransferIdentifier(steer.steerId, 'steerId');
|
|
227
|
+
this.core.assertOpen();
|
|
228
|
+
const prompt = normalizeFlexPrompt(steer.prompt);
|
|
229
|
+
const byteSize = this.promptAdmissionByteSize(steer.prompt, {});
|
|
230
|
+
const { state } = await this.core.resolveState(scopeId);
|
|
231
|
+
this.core.assertStateAcceptingWork(state);
|
|
232
|
+
const stored = this.core.requireSession(state, sessionId);
|
|
233
|
+
this.core.assertSessionWritable(stored);
|
|
234
|
+
if (stored.session.agent !== undefined) {
|
|
235
|
+
throw new FlexHarnessValidationError(
|
|
236
|
+
'Subagent sessions can only be prompted through the foreground delegate tool.',
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
const run = state.activeRuns.get(sessionId);
|
|
240
|
+
if (!run || run.queueId !== queueId) {
|
|
241
|
+
if (stored.outstandingPromptsById.has(queueId) || stored.terminalPromptQueueEntries.has(queueId)) {
|
|
242
|
+
throw new FlexHarnessSteerRejectedError('not-running', steer.steerId);
|
|
243
|
+
}
|
|
244
|
+
throw new FlexHarnessNotFoundError('Prompt queue entry', queueId);
|
|
245
|
+
}
|
|
246
|
+
return this.admitRunSteer(run, steer, prompt, byteSize);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** The shared synchronous admission point; neither entrypoint starts or resumes a run. */
|
|
250
|
+
public admitRunSteer(
|
|
251
|
+
run: IActiveRun,
|
|
252
|
+
steer: IFlexSteerInput,
|
|
253
|
+
prompt: INormalizedFlexPrompt,
|
|
254
|
+
byteSize: number,
|
|
255
|
+
): IFlexSteerAdmission {
|
|
256
|
+
if (
|
|
257
|
+
run.phase === 'closing'
|
|
258
|
+
|| run.phase === 'finalizing'
|
|
259
|
+
|| run.phase === 'promoting'
|
|
260
|
+
|| run.callbacksClosed
|
|
261
|
+
|| run.controller.signal.aborted
|
|
262
|
+
|| run.stop.signal.aborted
|
|
263
|
+
|| run.state.activeRuns.get(run.sessionId) !== run
|
|
264
|
+
) throw new FlexHarnessSteerRejectedError('not-running', steer.steerId);
|
|
265
|
+
const steering = run.steering;
|
|
266
|
+
if (!steering.input.closed && !steering.accepted.has(steer.steerId)) {
|
|
267
|
+
if (
|
|
268
|
+
steering.accepted.size - steering.applied.size
|
|
269
|
+
>= this.core.promptQueueLimits.maxOutstandingPromptsPerSession
|
|
270
|
+
) {
|
|
271
|
+
throw new FlexHarnessQueueFullError(`Session "${run.sessionId}" has reached its pending steer limit.`);
|
|
272
|
+
}
|
|
273
|
+
// Pending steers hold outstanding prompt bytes of the session until they are taken in.
|
|
274
|
+
if (
|
|
275
|
+
steering.stored.outstandingPromptBytes + byteSize
|
|
276
|
+
> this.core.promptQueueLimits.maxOutstandingBytesPerSession
|
|
277
|
+
) {
|
|
278
|
+
throw new FlexHarnessQueueFullError(
|
|
279
|
+
`Session "${run.sessionId}" has reached its outstanding prompt byte limit.`,
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
try {
|
|
284
|
+
steering.input.add({
|
|
285
|
+
inputId: steer.steerId,
|
|
286
|
+
content: cloneSerializable(prompt.modelMessage.content) as Parameters<
|
|
287
|
+
plugins.IAgentSession['beginGeneration']
|
|
288
|
+
>[0],
|
|
289
|
+
});
|
|
290
|
+
} catch (error) {
|
|
291
|
+
if (error instanceof plugins.AgentPendingInputRejectedError) {
|
|
292
|
+
throw new FlexHarnessSteerRejectedError(
|
|
293
|
+
error.reason === 'duplicate' ? 'duplicate' : 'not-running',
|
|
294
|
+
steer.steerId,
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
throw error;
|
|
298
|
+
}
|
|
299
|
+
steering.accepted.set(steer.steerId, { prompt, byteSize });
|
|
300
|
+
steering.stored.outstandingPromptBytes += byteSize;
|
|
301
|
+
this.core.emitEvent(run.scopeId, run.stored.session, {
|
|
302
|
+
type: 'prompt.steer.accepted',
|
|
303
|
+
queueId: run.queueId,
|
|
304
|
+
runId: run.runId,
|
|
305
|
+
steerId: steer.steerId,
|
|
306
|
+
});
|
|
307
|
+
return { steerId: steer.steerId, queueId: run.queueId, runId: run.runId };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
public async enqueuePromptInternal(
|
|
311
|
+
scopeId: string,
|
|
312
|
+
sessionId: string,
|
|
313
|
+
prompt: TFlexPrompt,
|
|
314
|
+
options: IFlexPromptOptions,
|
|
315
|
+
scheduleKey?: string,
|
|
316
|
+
debounceMs?: number,
|
|
317
|
+
delegationSeed?: IDelegatedRunAdmissionSeed,
|
|
318
|
+
admissionSignal?: AbortSignal,
|
|
319
|
+
slashCommandAdmission = false,
|
|
320
|
+
): Promise<{
|
|
321
|
+
admission: IFlexPromptQueueAdmission;
|
|
322
|
+
started: Promise<IFlexPromptAdmission>;
|
|
323
|
+
queued?: IQueuedPrompt;
|
|
324
|
+
}> {
|
|
325
|
+
this.core.assertOpen();
|
|
326
|
+
const normalizedPrompt = normalizeFlexPrompt(prompt);
|
|
327
|
+
const normalizedOptions = cloneSerializable(options);
|
|
328
|
+
const byteSize = this.promptAdmissionByteSize(prompt, options, scheduleKey, debounceMs);
|
|
329
|
+
const releasePendingAdmission = this.reservePendingPromptAdmission(byteSize);
|
|
330
|
+
let resolveSettled!: () => void;
|
|
331
|
+
const pendingOwner: IPendingPromptAdmission = {
|
|
332
|
+
controller: new AbortController(),
|
|
333
|
+
settled: new Promise<void>((resolve) => {
|
|
334
|
+
resolveSettled = resolve;
|
|
335
|
+
}),
|
|
336
|
+
resolveSettled: () => resolveSettled(),
|
|
337
|
+
};
|
|
338
|
+
this.pendingPromptAdmissionOwners.add(pendingOwner);
|
|
339
|
+
const admissionSignals = [
|
|
340
|
+
pendingOwner.controller.signal,
|
|
341
|
+
...(admissionSignal === undefined ? [] : [admissionSignal]),
|
|
342
|
+
];
|
|
343
|
+
let abortAdmission!: () => void;
|
|
344
|
+
const abortPromise = new Promise<never>((_resolve, reject) => {
|
|
345
|
+
abortAdmission = () => {
|
|
346
|
+
const aborted = admissionSignals.find((signal) => signal.aborted);
|
|
347
|
+
reject(aborted?.reason ?? new FlexHarnessAbortError());
|
|
348
|
+
};
|
|
349
|
+
for (const signal of admissionSignals) {
|
|
350
|
+
signal.addEventListener('abort', abortAdmission, { once: true });
|
|
351
|
+
}
|
|
352
|
+
if (admissionSignals.some((signal) => signal.aborted)) abortAdmission();
|
|
353
|
+
});
|
|
354
|
+
try {
|
|
355
|
+
const resolved = await Promise.race([this.core.resolveState(scopeId), abortPromise]);
|
|
356
|
+
const state = resolved.state;
|
|
357
|
+
await Promise.race([state.scopeQueue, abortPromise]);
|
|
358
|
+
if (admissionSignal?.aborted) {
|
|
359
|
+
throw admissionSignal.reason ?? new FlexHarnessAbortError();
|
|
360
|
+
}
|
|
361
|
+
this.core.assertStateAcceptingWork(state);
|
|
362
|
+
const stored = this.core.requireSession(state, sessionId);
|
|
363
|
+
this.core.assertSessionWritable(stored);
|
|
364
|
+
const sessionKey = this.core.commands.slashCommandSessionKey(state.storageKey, sessionId);
|
|
365
|
+
const invocationOwner = this.core.commands.slashCommandInvocationContext.getStore();
|
|
366
|
+
if (invocationOwner?.sessionKey === sessionKey) {
|
|
367
|
+
throw this.core.trustInternalError(new FlexHarnessSlashCommandReentryError(sessionId));
|
|
368
|
+
}
|
|
369
|
+
if (!slashCommandAdmission && this.core.commands.activeSlashCommandExecutions.has(sessionKey)) {
|
|
370
|
+
throw new FlexHarnessSessionBusyError(
|
|
371
|
+
sessionId,
|
|
372
|
+
'already has an active slash command execution',
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
if (stored.pendingReversion) {
|
|
376
|
+
throw new FlexHarnessSessionBusyError(sessionId, 'has a pending reversion operation');
|
|
377
|
+
}
|
|
378
|
+
if (stored.session.agent !== undefined && delegationSeed === undefined) {
|
|
379
|
+
throw new FlexHarnessValidationError(
|
|
380
|
+
'Subagent sessions can only be prompted through the foreground delegate tool.',
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
if (delegationSeed !== undefined) {
|
|
384
|
+
this.core.admission.assertDelegatedRunAdmissionOwnership(delegationSeed, state, stored, scopeId);
|
|
385
|
+
}
|
|
386
|
+
if (stored.outstandingPromptsById.size >= this.core.promptQueueLimits.maxOutstandingPromptsPerSession) {
|
|
387
|
+
throw new FlexHarnessQueueFullError(
|
|
388
|
+
`Session "${sessionId}" has reached its outstanding prompt limit.`,
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
if (
|
|
392
|
+
stored.outstandingPromptBytes + byteSize
|
|
393
|
+
> this.core.promptQueueLimits.maxOutstandingBytesPerSession
|
|
394
|
+
) {
|
|
395
|
+
throw new FlexHarnessQueueFullError(
|
|
396
|
+
`Session "${sessionId}" has reached its outstanding prompt byte limit.`,
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
if (
|
|
400
|
+
scheduleKey
|
|
401
|
+
&& [...stored.outstandingPromptsById.values()]
|
|
402
|
+
.some((entry) => entry.scheduleKey === scheduleKey)
|
|
403
|
+
) {
|
|
404
|
+
throw new FlexHarnessSessionBusyError(sessionId, `already has scheduled key "${scheduleKey}"`);
|
|
405
|
+
}
|
|
406
|
+
let resolveCompletion!: (result: IFlexPromptResult) => void;
|
|
407
|
+
let rejectCompletion!: (error: unknown) => void;
|
|
408
|
+
const completion = new Promise<IFlexPromptResult>((resolve, reject) => {
|
|
409
|
+
resolveCompletion = resolve;
|
|
410
|
+
rejectCompletion = reject;
|
|
411
|
+
});
|
|
412
|
+
void completion.catch(() => undefined);
|
|
413
|
+
let resolveStarted!: (admission: IFlexPromptAdmission) => void;
|
|
414
|
+
let rejectStarted!: (error: unknown) => void;
|
|
415
|
+
const started = new Promise<IFlexPromptAdmission>((resolve, reject) => {
|
|
416
|
+
resolveStarted = resolve;
|
|
417
|
+
rejectStarted = reject;
|
|
418
|
+
});
|
|
419
|
+
void started.catch(() => undefined);
|
|
420
|
+
const queued: IQueuedPrompt = {
|
|
421
|
+
state,
|
|
422
|
+
stored,
|
|
423
|
+
scopeId,
|
|
424
|
+
scope: delegationSeed === undefined ? resolved.scope.scope : delegationSeed.scope,
|
|
425
|
+
sessionId,
|
|
426
|
+
queueId: plugins.crypto.randomUUID(),
|
|
427
|
+
queueSequence: ++this.promptQueueSequence,
|
|
428
|
+
status: 'queued',
|
|
429
|
+
queuedAt: new Date().toISOString(),
|
|
430
|
+
...(scheduleKey === undefined ? {} : { scheduleKey, debounceMs }),
|
|
431
|
+
prompt: normalizedPrompt,
|
|
432
|
+
options: normalizedOptions,
|
|
433
|
+
...(delegationSeed === undefined ? {} : { delegationSeed }),
|
|
434
|
+
byteSize,
|
|
435
|
+
completion,
|
|
436
|
+
resolveCompletion,
|
|
437
|
+
rejectCompletion,
|
|
438
|
+
started,
|
|
439
|
+
resolveStarted,
|
|
440
|
+
rejectStarted,
|
|
441
|
+
};
|
|
442
|
+
stored.promptQueue.push(queued);
|
|
443
|
+
stored.outstandingPromptsById.set(queued.queueId, queued);
|
|
444
|
+
stored.outstandingPromptBytes += byteSize;
|
|
445
|
+
this.emitPromptQueueEvent(queued, 'prompt.queued');
|
|
446
|
+
this.schedulePromptQueueDrain(state, stored);
|
|
447
|
+
return {
|
|
448
|
+
admission: Object.freeze({ queueId: queued.queueId, completion }),
|
|
449
|
+
started,
|
|
450
|
+
queued,
|
|
451
|
+
};
|
|
452
|
+
} finally {
|
|
453
|
+
for (const signal of admissionSignals) {
|
|
454
|
+
signal.removeEventListener('abort', abortAdmission);
|
|
455
|
+
}
|
|
456
|
+
this.pendingPromptAdmissionOwners.delete(pendingOwner);
|
|
457
|
+
releasePendingAdmission();
|
|
458
|
+
pendingOwner.resolveSettled();
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
private reservePendingPromptAdmission(byteSize: number): () => void {
|
|
463
|
+
if (this.pendingPromptAdmissions >= this.core.promptQueueLimits.maxPendingAdmissions) {
|
|
464
|
+
throw new FlexHarnessQueueFullError('FlexHarness has reached its pending prompt admission limit.');
|
|
465
|
+
}
|
|
466
|
+
if (
|
|
467
|
+
this.pendingPromptAdmissionBytes + byteSize
|
|
468
|
+
> this.core.promptQueueLimits.maxPendingAdmissionBytes
|
|
469
|
+
) {
|
|
470
|
+
throw new FlexHarnessQueueFullError('FlexHarness has reached its pending prompt admission byte limit.');
|
|
471
|
+
}
|
|
472
|
+
this.pendingPromptAdmissions++;
|
|
473
|
+
this.pendingPromptAdmissionBytes += byteSize;
|
|
474
|
+
let released = false;
|
|
475
|
+
return () => {
|
|
476
|
+
if (released) return;
|
|
477
|
+
released = true;
|
|
478
|
+
this.pendingPromptAdmissions--;
|
|
479
|
+
this.pendingPromptAdmissionBytes -= byteSize;
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
private schedulePromptQueueDrain(state: IStorageState, stored: IStoredSessionState): void {
|
|
484
|
+
if (stored.promptQueueDrain) return;
|
|
485
|
+
let drain!: Promise<void>;
|
|
486
|
+
drain = Promise.resolve()
|
|
487
|
+
.then(() => this.drainPromptQueue(state, stored))
|
|
488
|
+
.finally(() => {
|
|
489
|
+
if (stored.promptQueueDrain === drain) delete stored.promptQueueDrain;
|
|
490
|
+
if (
|
|
491
|
+
stored.promptQueue.length > 0
|
|
492
|
+
&& !state.activeRuns.has(stored.session.sessionId)
|
|
493
|
+
&& state.lifecycle === 'active'
|
|
494
|
+
&& state.sessions.get(stored.session.sessionId) === stored
|
|
495
|
+
) this.schedulePromptQueueDrain(state, stored);
|
|
496
|
+
});
|
|
497
|
+
stored.promptQueueDrain = drain;
|
|
498
|
+
void drain.catch(() => undefined);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
private async drainPromptQueue(state: IStorageState, stored: IStoredSessionState): Promise<void> {
|
|
502
|
+
while (
|
|
503
|
+
state.lifecycle === 'active'
|
|
504
|
+
&& state.sessions.get(stored.session.sessionId) === stored
|
|
505
|
+
&& !state.activeRuns.has(stored.session.sessionId)
|
|
506
|
+
) {
|
|
507
|
+
const queued = stored.promptQueue.shift();
|
|
508
|
+
if (!queued) return;
|
|
509
|
+
if (stored.outstandingPromptsById.get(queued.queueId) !== queued) continue;
|
|
510
|
+
queued.status = 'starting';
|
|
511
|
+
queued.runId = plugins.crypto.randomUUID();
|
|
512
|
+
queued.startedAt = new Date().toISOString();
|
|
513
|
+
const run = this.core.runs.createActiveRun(queued);
|
|
514
|
+
state.activeRuns.set(queued.sessionId, run);
|
|
515
|
+
try {
|
|
516
|
+
await this.promoteQueuedPrompt(queued, run);
|
|
517
|
+
return;
|
|
518
|
+
} catch (error) {
|
|
519
|
+
if (state.activeRuns.get(queued.sessionId) === run) state.activeRuns.delete(queued.sessionId);
|
|
520
|
+
const cancellation = isAbortError(error);
|
|
521
|
+
this.finishQueuedPrompt(queued, cancellation ? 'cancelled' : 'failed', error);
|
|
522
|
+
if (state.lifecycle !== 'active') return;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
private assertPromptPromotion(queued: IQueuedPrompt, run: IActiveRun): void {
|
|
528
|
+
if (run.controller.signal.aborted) {
|
|
529
|
+
throw run.controller.signal.reason ?? new FlexHarnessAbortError();
|
|
530
|
+
}
|
|
531
|
+
this.core.assertStateAcceptingWork(run.state);
|
|
532
|
+
if (
|
|
533
|
+
run.state.sessions.get(run.sessionId) !== run.stored
|
|
534
|
+
|| run.stored.outstandingPromptsById.get(run.queueId) !== queued
|
|
535
|
+
|| run.state.activeRuns.get(run.sessionId) !== run
|
|
536
|
+
) throw this.core.trustInternalError(new FlexHarnessAbortError('The prompt was retired during admission.'));
|
|
537
|
+
if (queued.delegationSeed) {
|
|
538
|
+
this.core.admission.assertDelegatedRunAdmissionOwnership(
|
|
539
|
+
queued.delegationSeed,
|
|
540
|
+
run.state,
|
|
541
|
+
run.stored,
|
|
542
|
+
run.scopeId,
|
|
543
|
+
);
|
|
544
|
+
if (
|
|
545
|
+
run.queueId !== queued.queueId
|
|
546
|
+
|| run.runId !== queued.runId
|
|
547
|
+
|| run.sessionId !== queued.delegationSeed.sessionId
|
|
548
|
+
) {
|
|
549
|
+
throw this.core.trustInternalError(
|
|
550
|
+
new FlexHarnessAbortError('The delegated prompt lost its exact child run ownership.'),
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
private async promoteQueuedPrompt(queued: IQueuedPrompt, run: IActiveRun): Promise<void> {
|
|
557
|
+
const prompt = queued.prompt!;
|
|
558
|
+
const options = queued.options!;
|
|
559
|
+
let projectionReserved = false;
|
|
560
|
+
// Open from the reservation until its messages are announced or rolled back.
|
|
561
|
+
let closeReservationPublication = (): void => undefined;
|
|
562
|
+
try {
|
|
563
|
+
try {
|
|
564
|
+
this.assertPromptPromotion(queued, run);
|
|
565
|
+
await this.core.admission.acquireDelegatedRunAdmission(queued, run);
|
|
566
|
+
this.assertPromptPromotion(queued, run);
|
|
567
|
+
await this.core.reversions.commitRevertedBranch(run.state, run.stored, run.scopeId, run.scope as TScope);
|
|
568
|
+
this.assertPromptPromotion(queued, run);
|
|
569
|
+
const generationPrompt = cloneSerializable(prompt.modelMessage.content) as Parameters<
|
|
570
|
+
plugins.IAgentSession['beginGeneration']
|
|
571
|
+
>[0];
|
|
572
|
+
this.assertPromptPromotion(queued, run);
|
|
573
|
+
run.transaction = await this.core.compaction.withRunCompactorContext(
|
|
574
|
+
run,
|
|
575
|
+
() => run.stored.agentSession.beginGeneration(
|
|
576
|
+
generationPrompt,
|
|
577
|
+
{ generationId: run.runId },
|
|
578
|
+
),
|
|
579
|
+
);
|
|
580
|
+
this.assertPromptPromotion(queued, run);
|
|
581
|
+
const reservation = this.core.runs.createReservation(run, prompt);
|
|
582
|
+
closeReservationPublication = this.core.openPublication(run.state, run.sessionId);
|
|
583
|
+
run.metricsOperation = createSessionMetricsOperation(run.runId, 'generation');
|
|
584
|
+
await this.core.mutateProjection(run.state, run.stored, () => {
|
|
585
|
+
if (run.stored.metrics.pendingGenerationId !== undefined) {
|
|
586
|
+
throw new Error('Session metrics already owns a generation.');
|
|
587
|
+
}
|
|
588
|
+
run.stored.metrics.pendingGenerationId = run.runId;
|
|
589
|
+
run.stored.messages.push(reservation.userMessage, reservation.assistantMessage);
|
|
590
|
+
if (run.stored.session.agent === undefined) {
|
|
591
|
+
this.core.reversions.pruneReversionState(run.stored, true);
|
|
592
|
+
const captureId = this.core.turnReversionProvider
|
|
593
|
+
? this.core.reversions.reversionId('capture', run.state.storageKey, run.sessionId, run.runId)
|
|
594
|
+
: undefined;
|
|
595
|
+
const protocolVersion = this.core.reversions.reversionProviderProtocolVersion() ?? 1;
|
|
596
|
+
run.stored.reversionSegments.push({
|
|
597
|
+
runId: run.runId,
|
|
598
|
+
userMessageId: reservation.userMessage.messageId,
|
|
599
|
+
status: 'capturing',
|
|
600
|
+
contextAvailable: true,
|
|
601
|
+
eventIds: [],
|
|
602
|
+
workspaceCaptured: captureId !== undefined,
|
|
603
|
+
protocolVersion,
|
|
604
|
+
provenance: captureId === undefined ? 'transcript' : 'workspace',
|
|
605
|
+
...(captureId === undefined ? {} : { disposition: 'pending' }),
|
|
606
|
+
...(captureId === undefined ? {} : { captureId }),
|
|
607
|
+
});
|
|
608
|
+
if (captureId) {
|
|
609
|
+
run.stored.pendingReversion = {
|
|
610
|
+
kind: 'capture',
|
|
611
|
+
runId: run.runId,
|
|
612
|
+
captureId,
|
|
613
|
+
state: 'preparing',
|
|
614
|
+
protocolVersion,
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
});
|
|
619
|
+
projectionReserved = true;
|
|
620
|
+
run.reservedUserMessage = publicSnapshot(reservation.userMessage);
|
|
621
|
+
run.reservedAssistantMessage = publicSnapshot(reservation.assistantMessage);
|
|
622
|
+
this.assertPromptPromotion(queued, run);
|
|
623
|
+
await this.core.mutateScope(run.state, () => {
|
|
624
|
+
if (run.controller.signal.aborted) {
|
|
625
|
+
throw run.controller.signal.reason ?? new FlexHarnessAbortError();
|
|
626
|
+
}
|
|
627
|
+
if (run.state.tombstones.has(run.sessionId) || run.state.sessions.get(run.sessionId) !== run.stored) {
|
|
628
|
+
throw new FlexHarnessAbortError('The session was deleted during prompt admission.');
|
|
629
|
+
}
|
|
630
|
+
run.stored.session.status = run.scheduleKey ? 'scheduled' : 'running';
|
|
631
|
+
run.stored.session.activity = {
|
|
632
|
+
runId: run.runId,
|
|
633
|
+
status: run.scheduleKey ? 'scheduled' : 'running',
|
|
634
|
+
startedAt: reservation.userMessage.createdAt,
|
|
635
|
+
};
|
|
636
|
+
run.stored.session.updatedAt = reservation.userMessage.createdAt;
|
|
637
|
+
});
|
|
638
|
+
} catch (error) {
|
|
639
|
+
if (containsStoreCommitUncertainty(error)) run.state.lifecycle = 'fenced';
|
|
640
|
+
const safeError = this.core.projectExternalError(run, error, projectionReserved ? 'persistence' : 'agentSession');
|
|
641
|
+
let cleanupErrors: unknown[] = [];
|
|
642
|
+
let cleanupFailure: unknown;
|
|
643
|
+
try {
|
|
644
|
+
await this.core.admission.closeRunDelegatedRunAdmission(
|
|
645
|
+
run,
|
|
646
|
+
!run.delegatedRunAdmissionOwner?.acquisitionDetached,
|
|
647
|
+
);
|
|
648
|
+
} catch (closeError) {
|
|
649
|
+
cleanupErrors.push(closeError);
|
|
650
|
+
}
|
|
651
|
+
try {
|
|
652
|
+
cleanupErrors.push(...await this.core.runs.rollbackAdmission(run, projectionReserved, safeError));
|
|
653
|
+
} catch (rollbackError) {
|
|
654
|
+
cleanupFailure = rollbackError;
|
|
655
|
+
}
|
|
656
|
+
if (cleanupFailure !== undefined && cleanupErrors.length > 0) {
|
|
657
|
+
cleanupFailure = combineErrors([cleanupFailure, ...cleanupErrors]);
|
|
658
|
+
}
|
|
659
|
+
if (run.state.lifecycle === 'fenced' && cleanupFailure === undefined) {
|
|
660
|
+
const combined = combineErrors([safeError, ...cleanupErrors]);
|
|
661
|
+
try {
|
|
662
|
+
await this.core.lifecycle.fenceNamespace(run.state, run, combined);
|
|
663
|
+
} catch (fenceError) {
|
|
664
|
+
cleanupFailure = fenceError;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
const rejection = cleanupFailure
|
|
668
|
+
?? (cleanupErrors.length > 0 ? combineErrors([safeError, ...cleanupErrors]) : safeError);
|
|
669
|
+
throw rejection;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
const runType = run.scheduleKey ? 'run.scheduled' : 'run.started';
|
|
673
|
+
this.core.emitEvent(run.scopeId, run.stored.session, {
|
|
674
|
+
type: runType,
|
|
675
|
+
runId: run.runId,
|
|
676
|
+
messageId: run.userMessageId,
|
|
677
|
+
session: publicSnapshot(run.stored.session),
|
|
678
|
+
});
|
|
679
|
+
for (const message of [run.reservedUserMessage!, run.reservedAssistantMessage!]) {
|
|
680
|
+
this.core.emitEvent(run.scopeId, run.stored.session, {
|
|
681
|
+
type: 'message.created',
|
|
682
|
+
runId: run.runId,
|
|
683
|
+
messageId: message.messageId,
|
|
684
|
+
message,
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
} finally {
|
|
688
|
+
closeReservationPublication();
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
queued.status = run.scheduleKey ? 'scheduled' : 'starting';
|
|
692
|
+
this.emitPromptQueueEvent(queued, 'prompt.started');
|
|
693
|
+
queued.resolveStarted(Object.freeze({
|
|
694
|
+
queueId: queued.queueId,
|
|
695
|
+
runId: run.runId,
|
|
696
|
+
completion: queued.completion,
|
|
697
|
+
}));
|
|
698
|
+
run.phase = run.scheduleKey ? 'scheduled' : 'running';
|
|
699
|
+
const generateOptions = {
|
|
700
|
+
transaction: run.transaction,
|
|
701
|
+
abort: run.controller.signal,
|
|
702
|
+
stop: run.stop.signal,
|
|
703
|
+
prepare: (context: { generationId: string; abortSignal: AbortSignal }) =>
|
|
704
|
+
this.core.runs.prepareGeneration(run, options, context.abortSignal),
|
|
705
|
+
pendingInput: run.steering.input,
|
|
706
|
+
activateOnBackgroundJobs: true,
|
|
707
|
+
onCacheDispatch: (dispatch: plugins.ISmartAiCacheDispatch) => { run.cacheDispatch = dispatch; },
|
|
708
|
+
onInputApplied: (input: plugins.IAgentAppliedInput) => this.core.runs.applyRunSteer(run, input),
|
|
709
|
+
beforeFinish: (context: { abortSignal: AbortSignal }) =>
|
|
710
|
+
this.core.subagentTasks.awaitNextBackgroundSubagent(run, context.abortSignal),
|
|
711
|
+
};
|
|
712
|
+
const generation = this.core.compaction.withRunCompactorContext(run, () => run.scheduleKey
|
|
713
|
+
? run.stored.agentSession.scheduleGenerate({
|
|
714
|
+
key: run.scheduleKey,
|
|
715
|
+
debounceMs: run.debounceMs,
|
|
716
|
+
...generateOptions,
|
|
717
|
+
})
|
|
718
|
+
: run.stored.agentSession.generate(generateOptions));
|
|
719
|
+
const execution = this.core.runs.executeRun(run, generation).then(
|
|
720
|
+
(result) => this.finishQueuedPrompt(queued, 'completed', undefined, result),
|
|
721
|
+
(error) => this.finishQueuedPrompt(
|
|
722
|
+
queued,
|
|
723
|
+
isAbortError(error) ? 'cancelled' : 'failed',
|
|
724
|
+
error,
|
|
725
|
+
),
|
|
726
|
+
).finally(() => this.schedulePromptQueueDrain(run.state, run.stored));
|
|
727
|
+
void execution.catch(() => undefined);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
private projectPromptQueueEntry(queued: IQueuedPrompt): IFlexPromptQueueEntry {
|
|
731
|
+
return {
|
|
732
|
+
queueId: queued.queueId,
|
|
733
|
+
queueSequence: queued.queueSequence,
|
|
734
|
+
scopeId: queued.scopeId,
|
|
735
|
+
sessionId: queued.sessionId,
|
|
736
|
+
status: queued.status,
|
|
737
|
+
queuedAt: queued.queuedAt,
|
|
738
|
+
...(queued.runId ? { runId: queued.runId } : {}),
|
|
739
|
+
...(queued.scheduleKey ? { scheduleKey: queued.scheduleKey } : {}),
|
|
740
|
+
...(queued.startedAt ? { startedAt: queued.startedAt } : {}),
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
private promptQueueEntry(
|
|
745
|
+
stored: IStoredSessionState,
|
|
746
|
+
queueId: string,
|
|
747
|
+
): IFlexPromptQueueEntry | undefined {
|
|
748
|
+
const outstanding = stored.outstandingPromptsById.get(queueId);
|
|
749
|
+
return outstanding
|
|
750
|
+
? this.projectPromptQueueEntry(outstanding)
|
|
751
|
+
: stored.terminalPromptQueueEntries.get(queueId);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
public emitPromptQueueEvent(
|
|
755
|
+
queued: IQueuedPrompt,
|
|
756
|
+
type: 'prompt.queued' | 'prompt.started' | 'prompt.running' | 'prompt.finished',
|
|
757
|
+
entry: IFlexPromptQueueEntry = this.projectPromptQueueEntry(queued),
|
|
758
|
+
): void {
|
|
759
|
+
this.core.emitEvent(queued.scopeId, queued.stored.session, {
|
|
760
|
+
type,
|
|
761
|
+
queueId: queued.queueId,
|
|
762
|
+
...(entry.runId ? { runId: entry.runId } : {}),
|
|
763
|
+
entry: publicSnapshot(entry),
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
private finishQueuedPrompt(
|
|
768
|
+
queued: IQueuedPrompt,
|
|
769
|
+
status: 'completed' | 'failed' | 'cancelled',
|
|
770
|
+
error?: unknown,
|
|
771
|
+
result?: IFlexPromptResult,
|
|
772
|
+
): void {
|
|
773
|
+
const stored = queued.stored;
|
|
774
|
+
if (stored.outstandingPromptsById.get(queued.queueId) !== queued) return;
|
|
775
|
+
stored.promptQueue = stored.promptQueue.filter((entry) => entry !== queued);
|
|
776
|
+
stored.outstandingPromptsById.delete(queued.queueId);
|
|
777
|
+
stored.outstandingPromptBytes = Math.max(0, stored.outstandingPromptBytes - queued.byteSize);
|
|
778
|
+
delete queued.prompt;
|
|
779
|
+
delete queued.options;
|
|
780
|
+
delete queued.delegationSeed;
|
|
781
|
+
const steering = queued.steering === undefined ? undefined : this.core.runs.closeRunSteering(queued.steering);
|
|
782
|
+
const terminal: IFlexPromptQueueEntry = {
|
|
783
|
+
...this.projectPromptQueueEntry(queued),
|
|
784
|
+
status,
|
|
785
|
+
finishedAt: new Date().toISOString(),
|
|
786
|
+
...(error === undefined ? {} : { error: errorToInfo(error) }),
|
|
787
|
+
keptInContext: steering?.keptInContext ?? false,
|
|
788
|
+
...(steering === undefined || steering.unappliedSteerIds.length === 0
|
|
789
|
+
? {}
|
|
790
|
+
: { unappliedSteerIds: [...steering.unappliedSteerIds] }),
|
|
791
|
+
};
|
|
792
|
+
stored.terminalPromptQueueEntries.set(queued.queueId, terminal);
|
|
793
|
+
while (
|
|
794
|
+
stored.terminalPromptQueueEntries.size
|
|
795
|
+
> this.core.promptQueueLimits.maxTerminalEntriesPerSession
|
|
796
|
+
) {
|
|
797
|
+
const oldest = stored.terminalPromptQueueEntries.keys().next().value as string | undefined;
|
|
798
|
+
if (!oldest) break;
|
|
799
|
+
stored.terminalPromptQueueEntries.delete(oldest);
|
|
800
|
+
}
|
|
801
|
+
this.emitPromptQueueEvent(queued, 'prompt.finished', terminal);
|
|
802
|
+
if (status === 'completed') {
|
|
803
|
+
if (!result) throw new Error('A completed queued prompt requires a result.');
|
|
804
|
+
queued.resolveCompletion(result);
|
|
805
|
+
} else {
|
|
806
|
+
const rejection = error ?? this.core.trustInternalError(new FlexHarnessAbortError());
|
|
807
|
+
queued.rejectStarted(rejection);
|
|
808
|
+
queued.rejectCompletion(rejection);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
public cancelQueuedPrompt(
|
|
813
|
+
queued: IQueuedPrompt,
|
|
814
|
+
reason: FlexHarnessAbortError,
|
|
815
|
+
context?: IFlexAgentContextInvocation<TScope>,
|
|
816
|
+
): boolean {
|
|
817
|
+
if (queued.stored.outstandingPromptsById.get(queued.queueId) !== queued) return false;
|
|
818
|
+
if (queued.status === 'queued') {
|
|
819
|
+
this.finishQueuedPrompt(queued, 'cancelled', reason);
|
|
820
|
+
this.schedulePromptQueueDrain(queued.state, queued.stored);
|
|
821
|
+
return true;
|
|
822
|
+
}
|
|
823
|
+
const run = queued.state.activeRuns.get(queued.sessionId);
|
|
824
|
+
if (!run || run.queueId !== queued.queueId) return false;
|
|
825
|
+
if (run.phase === 'finalizing' || run.phase === 'promoting') return false;
|
|
826
|
+
this.core.runs.cancelRun(run, reason, context);
|
|
827
|
+
return true;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
public cancelStoredPromptQueue(
|
|
831
|
+
stored: IStoredSessionState,
|
|
832
|
+
reason: FlexHarnessAbortError,
|
|
833
|
+
excludedQueueId?: string,
|
|
834
|
+
context?: IFlexAgentContextInvocation<unknown>,
|
|
835
|
+
): void {
|
|
836
|
+
for (const queued of [...stored.outstandingPromptsById.values()]) {
|
|
837
|
+
if (queued.queueId === excludedQueueId) continue;
|
|
838
|
+
this.cancelQueuedPrompt(
|
|
839
|
+
queued,
|
|
840
|
+
reason,
|
|
841
|
+
context as IFlexAgentContextInvocation<TScope> | undefined,
|
|
842
|
+
);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
public purgeStoredPromptQueue(stored: IStoredSessionState): void {
|
|
847
|
+
for (const queued of stored.outstandingPromptsById.values()) {
|
|
848
|
+
delete queued.prompt;
|
|
849
|
+
delete queued.options;
|
|
850
|
+
delete queued.delegationSeed;
|
|
851
|
+
}
|
|
852
|
+
stored.promptQueue.length = 0;
|
|
853
|
+
stored.outstandingPromptsById.clear();
|
|
854
|
+
stored.terminalPromptQueueEntries.clear();
|
|
855
|
+
stored.outstandingPromptBytes = 0;
|
|
856
|
+
}
|
|
857
|
+
}
|