@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,1204 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
import {
|
|
3
|
+
FlexHarnessAbortError,
|
|
4
|
+
FlexHarnessExternalError,
|
|
5
|
+
FlexHarnessNotFoundError,
|
|
6
|
+
FlexHarnessRunError,
|
|
7
|
+
FlexHarnessStepLimitError,
|
|
8
|
+
FlexHarnessValidationError,
|
|
9
|
+
errorToInfo,
|
|
10
|
+
} from './errors.js';
|
|
11
|
+
import {
|
|
12
|
+
FLEX_PROJECT_MANAGEMENT_LIMITS,
|
|
13
|
+
FLEX_RESULT_METHOD,
|
|
14
|
+
type IFlexAgentContextInvocation,
|
|
15
|
+
type IFlexMessage,
|
|
16
|
+
type IFlexPermissionRequestInput,
|
|
17
|
+
type IFlexPromptOptions,
|
|
18
|
+
type IFlexPromptResult,
|
|
19
|
+
type IFlexResolvedModel,
|
|
20
|
+
type IFlexRunFinishedEvent,
|
|
21
|
+
type IFlexSession,
|
|
22
|
+
type IFlexTerminalProjection,
|
|
23
|
+
type IFlexToolHandle,
|
|
24
|
+
type IFlexToolProviderContext,
|
|
25
|
+
type TFlexAgentToolSet,
|
|
26
|
+
type TFlexMessagePart,
|
|
27
|
+
} from './interfaces.js';
|
|
28
|
+
import { FlexToolCatalog, assertNoCatalogCollisions } from './classes.toolcatalog.js';
|
|
29
|
+
import { FlexCodeSandbox } from './classes.codesandbox.js';
|
|
30
|
+
import { cloneSerializable } from './utils.json.js';
|
|
31
|
+
import type { INormalizedFlexPrompt } from './utils.prompt.js';
|
|
32
|
+
import {
|
|
33
|
+
describeAttachment,
|
|
34
|
+
maxTransferMetadataBytes,
|
|
35
|
+
normalizeResolvedModel,
|
|
36
|
+
normalizeRunResult,
|
|
37
|
+
publicSnapshot,
|
|
38
|
+
truncateUtf8,
|
|
39
|
+
type IRunResultProjection,
|
|
40
|
+
} from './utils.transfer.js';
|
|
41
|
+
import { wrapToolSet } from './utils.tooloutput.js';
|
|
42
|
+
import { validateUtf8String } from './utils.validation.js';
|
|
43
|
+
import type {
|
|
44
|
+
TCanonicalOutcome,
|
|
45
|
+
IStoredSessionState,
|
|
46
|
+
IStorageState,
|
|
47
|
+
IActiveRun,
|
|
48
|
+
IRunSteering,
|
|
49
|
+
IRunSteeringOutcome,
|
|
50
|
+
IQueuedPrompt,
|
|
51
|
+
IResolverOutcome,
|
|
52
|
+
} from './interfaces.flexharness.js';
|
|
53
|
+
import {
|
|
54
|
+
externalErrorFallback,
|
|
55
|
+
requireSessionGeneration,
|
|
56
|
+
combineErrors,
|
|
57
|
+
terminalActivityError,
|
|
58
|
+
} from './utils.flexharness.js';
|
|
59
|
+
import type { FlexHarnessCore } from './classes.flexharness.core.js';
|
|
60
|
+
|
|
61
|
+
/** The run execution engine: generations, terminal outcomes, steering and cancellation. */
|
|
62
|
+
export class FlexHarnessRuns<TScope> {
|
|
63
|
+
constructor(private readonly core: FlexHarnessCore<TScope>) {}
|
|
64
|
+
|
|
65
|
+
public async abort(scopeId: string, sessionId: string): Promise<boolean> {
|
|
66
|
+
const { scope, state } = await this.core.resolveState(scopeId);
|
|
67
|
+
const run = state.activeRuns.get(sessionId);
|
|
68
|
+
if (!run || run.phase === 'finalizing' || run.phase === 'promoting') return false;
|
|
69
|
+
this.cancelRun(
|
|
70
|
+
run,
|
|
71
|
+
this.core.trustInternalError(new FlexHarnessAbortError()),
|
|
72
|
+
this.core.compaction.createCompactorContext(scopeId, scope.scope, state.storageKey, sessionId),
|
|
73
|
+
);
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
public createActiveRun(queued: IQueuedPrompt): IActiveRun {
|
|
78
|
+
const steering: IRunSteering = {
|
|
79
|
+
input: new plugins.AgentPendingInputQueue(),
|
|
80
|
+
stored: queued.stored,
|
|
81
|
+
accepted: new Map(),
|
|
82
|
+
applied: new Set(),
|
|
83
|
+
};
|
|
84
|
+
queued.steering = steering;
|
|
85
|
+
return {
|
|
86
|
+
steering,
|
|
87
|
+
stop: new AbortController(),
|
|
88
|
+
subagents: this.core.subagents,
|
|
89
|
+
state: queued.state,
|
|
90
|
+
stored: queued.stored,
|
|
91
|
+
originCompactorContext: this.core.compaction.createCompactorContext(
|
|
92
|
+
queued.scopeId,
|
|
93
|
+
queued.scope as TScope,
|
|
94
|
+
queued.state.storageKey,
|
|
95
|
+
queued.sessionId,
|
|
96
|
+
),
|
|
97
|
+
scopeId: queued.scopeId,
|
|
98
|
+
scope: queued.scope,
|
|
99
|
+
sessionId: queued.sessionId,
|
|
100
|
+
queueId: queued.queueId,
|
|
101
|
+
runId: queued.runId!,
|
|
102
|
+
userMessageId: plugins.crypto.randomUUID(),
|
|
103
|
+
assistantMessageId: plugins.crypto.randomUUID(),
|
|
104
|
+
controller: new AbortController(),
|
|
105
|
+
...(queued.scheduleKey === undefined
|
|
106
|
+
? {}
|
|
107
|
+
: { scheduleKey: queued.scheduleKey, debounceMs: queued.debounceMs }),
|
|
108
|
+
callbackEventCount: 0,
|
|
109
|
+
callbackOutputBytes: 0,
|
|
110
|
+
callbackParts: [],
|
|
111
|
+
partTextUtf8Bytes: new Map(),
|
|
112
|
+
callbacksClosed: false,
|
|
113
|
+
finishedEventEmitted: false,
|
|
114
|
+
usage: {
|
|
115
|
+
inputTokens: 0,
|
|
116
|
+
outputTokens: 0,
|
|
117
|
+
totalTokens: 0,
|
|
118
|
+
cacheReadTokens: 0,
|
|
119
|
+
cacheWriteTokens: 0,
|
|
120
|
+
},
|
|
121
|
+
reasoningPartIds: new Map(),
|
|
122
|
+
toolPartIds: new Map(),
|
|
123
|
+
trustedToolExecutionErrors: new Map(),
|
|
124
|
+
unconfirmedToolCalls: new Map(),
|
|
125
|
+
readCalls: new Map(),
|
|
126
|
+
deduplicatedToolCalls: new Map(),
|
|
127
|
+
subagentCallCount: 0,
|
|
128
|
+
subagentSessionIds: new Set(),
|
|
129
|
+
backgroundSubagents: new Set(),
|
|
130
|
+
pendingPermissionIds: new Set(),
|
|
131
|
+
phase: 'admitting',
|
|
132
|
+
completion: queued.completion,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
public async executeRun(
|
|
137
|
+
run: IActiveRun,
|
|
138
|
+
generation: Promise<plugins.IAgentGenerateResult>,
|
|
139
|
+
): Promise<IFlexPromptResult> {
|
|
140
|
+
let generated = false;
|
|
141
|
+
try {
|
|
142
|
+
// The transactional generation settles only after its model calls are reported, so
|
|
143
|
+
// `run.usage` is complete once it settles, also for a cancelled run.
|
|
144
|
+
let rawResult: plugins.IAgentGenerateResult;
|
|
145
|
+
try {
|
|
146
|
+
rawResult = await generation;
|
|
147
|
+
generated = true;
|
|
148
|
+
} finally {
|
|
149
|
+
await this.core.callbacks.settleMetricsOperation(run.state, run.stored, run.metricsOperation!);
|
|
150
|
+
}
|
|
151
|
+
if (rawResult.terminationReason === 'step-limit') {
|
|
152
|
+
throw this.core.trustInternalError(new FlexHarnessStepLimitError(rawResult.steps));
|
|
153
|
+
}
|
|
154
|
+
// Background tools do not block inference, but the owning turn joins its admitted children
|
|
155
|
+
// before releasing its authority or committing its terminal projection.
|
|
156
|
+
await Promise.all([...run.backgroundSubagents]);
|
|
157
|
+
if (run.callbackError) throw run.callbackError;
|
|
158
|
+
if (run.controller.signal.aborted) throw run.controller.signal.reason;
|
|
159
|
+
const result = normalizeRunResult(rawResult);
|
|
160
|
+
if (!run.modelResolution) throw new FlexHarnessValidationError('Generation completed without a resolved model.');
|
|
161
|
+
await this.core.reversions.finalizeRunReversion(run, 'completed');
|
|
162
|
+
await this.core.admission.closeRunDelegatedRunAdmission(run);
|
|
163
|
+
const terminal = this.buildTerminal(run, 'completed', result);
|
|
164
|
+
try {
|
|
165
|
+
await this.core.mutateProjection(run.state, run.stored, () => {
|
|
166
|
+
this.replaceStagedTerminal(run.stored, terminal);
|
|
167
|
+
}, true);
|
|
168
|
+
} catch (error) {
|
|
169
|
+
throw this.core.projectExternalError(run, error, 'persistence');
|
|
170
|
+
}
|
|
171
|
+
if (run.controller.signal.aborted) throw run.controller.signal.reason;
|
|
172
|
+
run.phase = 'finalizing';
|
|
173
|
+
try {
|
|
174
|
+
await this.core.compaction.withRunCompactorContext(
|
|
175
|
+
run,
|
|
176
|
+
() => run.stored.agentSession.finalizeGeneration(run.transaction!, 'accepted'),
|
|
177
|
+
);
|
|
178
|
+
} catch (error) {
|
|
179
|
+
const projected = this.core.projectExternalError(run, error, 'agentSession');
|
|
180
|
+
await this.core.lifecycle.fenceNamespace(run.state, run, projected);
|
|
181
|
+
throw projected;
|
|
182
|
+
}
|
|
183
|
+
run.steering.keptInContext = true;
|
|
184
|
+
this.core.reversions.refreshAutomaticArchiveHorizon(run.stored);
|
|
185
|
+
run.phase = 'promoting';
|
|
186
|
+
const closePublication = this.core.openPublication(run.state, run.sessionId);
|
|
187
|
+
try {
|
|
188
|
+
try {
|
|
189
|
+
await this.promoteCompletedTerminal(run, terminal);
|
|
190
|
+
} catch (error) {
|
|
191
|
+
const projected = this.core.projectExternalError(run, error, 'persistence');
|
|
192
|
+
await this.core.lifecycle.fenceNamespace(run.state, run, projected);
|
|
193
|
+
throw projected;
|
|
194
|
+
}
|
|
195
|
+
this.emitTerminalProjection(run, terminal);
|
|
196
|
+
this.emitFinalRunEvent(run, terminal.assistantMessage, undefined, false);
|
|
197
|
+
} finally {
|
|
198
|
+
closePublication();
|
|
199
|
+
}
|
|
200
|
+
await this.core.reversions.drainReversionReleases(
|
|
201
|
+
run.state,
|
|
202
|
+
run.stored,
|
|
203
|
+
run.scopeId,
|
|
204
|
+
run.scope as TScope,
|
|
205
|
+
false,
|
|
206
|
+
);
|
|
207
|
+
return {
|
|
208
|
+
runId: run.runId,
|
|
209
|
+
sessionId: run.sessionId,
|
|
210
|
+
userMessage: publicSnapshot(terminal.userMessage),
|
|
211
|
+
assistantMessage: publicSnapshot(terminal.assistantMessage),
|
|
212
|
+
model: publicSnapshot(terminal.model!),
|
|
213
|
+
usage: publicSnapshot(terminal.usage!),
|
|
214
|
+
finishReason: terminal.finishReason!,
|
|
215
|
+
steps: terminal.steps!,
|
|
216
|
+
unappliedSteerIds: [...this.closeRunSteering(run.steering).unappliedSteerIds],
|
|
217
|
+
};
|
|
218
|
+
} catch (error) {
|
|
219
|
+
// A failed parent must cancel its admitted children before joining them. Otherwise a
|
|
220
|
+
// child waiting for permission or model I/O can keep a failed parent alive indefinitely.
|
|
221
|
+
if (run.backgroundSubagents.size > 0 && !run.controller.signal.aborted) run.controller.abort(error);
|
|
222
|
+
await Promise.allSettled([...run.backgroundSubagents]);
|
|
223
|
+
try {
|
|
224
|
+
if (run.state.lifecycle === 'fenced') {
|
|
225
|
+
await this.core.lifecycle.fenceNamespace(run.state, run, error);
|
|
226
|
+
throw error;
|
|
227
|
+
}
|
|
228
|
+
if (run.state.lifecycle === 'retired') throw error;
|
|
229
|
+
return await this.finishFailedRun(run, error, generated);
|
|
230
|
+
} catch (finalError) {
|
|
231
|
+
this.emitUnpublishedRunFinish(run, finalError, generated);
|
|
232
|
+
throw finalError;
|
|
233
|
+
}
|
|
234
|
+
} finally {
|
|
235
|
+
run.callbacksClosed = true;
|
|
236
|
+
this.core.permissions.rejectRunPermissions(
|
|
237
|
+
run.state,
|
|
238
|
+
run,
|
|
239
|
+
run.ownerCancellation ?? new FlexHarnessAbortError('The run ended before permission was resolved.'),
|
|
240
|
+
);
|
|
241
|
+
if (run.state.activeRuns.get(run.sessionId) === run) run.state.activeRuns.delete(run.sessionId);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
private async finishFailedRun(
|
|
246
|
+
run: IActiveRun,
|
|
247
|
+
error: unknown,
|
|
248
|
+
generated: boolean,
|
|
249
|
+
): Promise<never> {
|
|
250
|
+
run.callbacksClosed = true;
|
|
251
|
+
run.phase = 'closing';
|
|
252
|
+
const cancelled = run.ownerCancellation !== undefined && run.internalFailure === undefined;
|
|
253
|
+
const safeError = cancelled
|
|
254
|
+
? run.ownerCancellation!
|
|
255
|
+
: this.core.projectExternalError(run, error, generated ? 'persistence' : 'agentSession');
|
|
256
|
+
const errors: unknown[] = [safeError];
|
|
257
|
+
let reversionFailed = false;
|
|
258
|
+
try {
|
|
259
|
+
await this.core.reversions.finalizeRunReversion(run, cancelled ? 'cancelled' : 'failed');
|
|
260
|
+
} catch (reversionError) {
|
|
261
|
+
errors.push(reversionError);
|
|
262
|
+
reversionFailed = true;
|
|
263
|
+
}
|
|
264
|
+
try {
|
|
265
|
+
await this.core.admission.closeRunDelegatedRunAdmission(run);
|
|
266
|
+
} catch (closeError) {
|
|
267
|
+
if (!errors.includes(closeError)) errors.push(closeError);
|
|
268
|
+
}
|
|
269
|
+
if (reversionFailed) {
|
|
270
|
+
const combined = combineErrors(errors);
|
|
271
|
+
await this.core.lifecycle.fenceNamespace(run.state, run, combined);
|
|
272
|
+
throw combined;
|
|
273
|
+
}
|
|
274
|
+
if (generated && !this.core.isTombstoned(run)) {
|
|
275
|
+
const failedStage = this.buildTerminal(run, cancelled ? 'cancelled' : 'failed', undefined, safeError);
|
|
276
|
+
try {
|
|
277
|
+
await this.core.mutateProjection(run.state, run.stored, () => {
|
|
278
|
+
this.replaceStagedTerminal(run.stored, failedStage);
|
|
279
|
+
}, true);
|
|
280
|
+
} catch (stageError) {
|
|
281
|
+
errors.push(this.core.projectExternalError(run, stageError, 'persistence'));
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
run.phase = 'finalizing';
|
|
286
|
+
const desiredOutcome: TCanonicalOutcome = cancelled || !generated ? 'interrupted' : 'rejected';
|
|
287
|
+
try {
|
|
288
|
+
await this.core.compaction.withRunCompactorContext(
|
|
289
|
+
run,
|
|
290
|
+
() => run.stored.agentSession.finalizeGeneration(
|
|
291
|
+
run.transaction!,
|
|
292
|
+
desiredOutcome,
|
|
293
|
+
{ stopped: desiredOutcome === 'interrupted' && cancelled },
|
|
294
|
+
),
|
|
295
|
+
);
|
|
296
|
+
} catch (finalizationError) {
|
|
297
|
+
errors.push(this.core.projectExternalError(run, finalizationError, 'agentSession'));
|
|
298
|
+
}
|
|
299
|
+
let outcome: TCanonicalOutcome | undefined;
|
|
300
|
+
try {
|
|
301
|
+
const snapshot = await run.stored.eventStore.load(run.stored.session.sessionId);
|
|
302
|
+
const events = snapshot?.events ?? [];
|
|
303
|
+
outcome = this.core.canonicalOutcomes(events).get(run.runId);
|
|
304
|
+
// A generation without a durable outcome is interrupted by the next load, which leaves it
|
|
305
|
+
// out of the model context.
|
|
306
|
+
run.steering.keptInContext = outcome !== undefined
|
|
307
|
+
&& plugins.isGenerationInModelContext(events, run.runId);
|
|
308
|
+
this.core.reversions.refreshAutomaticArchiveHorizon(run.stored);
|
|
309
|
+
} catch (outcomeError) {
|
|
310
|
+
errors.push(this.core.projectExternalError(run, outcomeError, 'persistence'));
|
|
311
|
+
}
|
|
312
|
+
if (!outcome) {
|
|
313
|
+
const combined = combineErrors(errors);
|
|
314
|
+
await this.core.lifecycle.fenceNamespace(run.state, run, combined);
|
|
315
|
+
throw combined;
|
|
316
|
+
}
|
|
317
|
+
if (outcome === 'accepted') {
|
|
318
|
+
const combined = combineErrors(errors);
|
|
319
|
+
await this.core.lifecycle.fenceNamespace(run.state, run, combined);
|
|
320
|
+
throw combined;
|
|
321
|
+
}
|
|
322
|
+
const publicStatus = outcome === 'rejected' || !cancelled ? 'failed' : 'cancelled';
|
|
323
|
+
const terminalError = publicStatus === 'cancelled'
|
|
324
|
+
? run.ownerCancellation ?? this.core.trustInternalError(new FlexHarnessAbortError())
|
|
325
|
+
: safeError;
|
|
326
|
+
const terminal = this.buildTerminal(run, publicStatus, undefined, terminalError);
|
|
327
|
+
if (!this.core.isTombstoned(run)) {
|
|
328
|
+
const closePublication = this.core.openPublication(run.state, run.sessionId);
|
|
329
|
+
try {
|
|
330
|
+
try {
|
|
331
|
+
await this.publishFailedTerminal(run, terminal);
|
|
332
|
+
} catch (persistenceError) {
|
|
333
|
+
errors.push(this.core.projectExternalError(run, persistenceError, 'persistence'));
|
|
334
|
+
const combined = combineErrors(errors);
|
|
335
|
+
await this.core.lifecycle.fenceNamespace(run.state, run, combined);
|
|
336
|
+
throw combined;
|
|
337
|
+
}
|
|
338
|
+
this.emitTerminalProjection(run, terminal);
|
|
339
|
+
this.emitFinalRunEvent(run, terminal.assistantMessage, terminalError, publicStatus === 'cancelled');
|
|
340
|
+
} finally {
|
|
341
|
+
closePublication();
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
await this.core.reversions.drainReversionReleases(
|
|
345
|
+
run.state,
|
|
346
|
+
run.stored,
|
|
347
|
+
run.scopeId,
|
|
348
|
+
run.scope as TScope,
|
|
349
|
+
false,
|
|
350
|
+
);
|
|
351
|
+
throw errors.length > 1 ? combineErrors(errors) : terminalError;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
public async prepareGeneration(
|
|
355
|
+
run: IActiveRun,
|
|
356
|
+
options: IFlexPromptOptions,
|
|
357
|
+
signal: AbortSignal,
|
|
358
|
+
): Promise<plugins.IAgentGenerationLease> {
|
|
359
|
+
signal.throwIfAborted();
|
|
360
|
+
const configuration = await this.core.configurations.resolveSessionConfiguration(
|
|
361
|
+
run.scopeId,
|
|
362
|
+
run.scope as TScope,
|
|
363
|
+
run.state,
|
|
364
|
+
run.stored,
|
|
365
|
+
signal,
|
|
366
|
+
);
|
|
367
|
+
run.subagents = configuration.subagents;
|
|
368
|
+
run.allowedTools = configuration.allowedTools;
|
|
369
|
+
run.permissionPolicy = configuration.permissionPolicy as IActiveRun['permissionPolicy'];
|
|
370
|
+
const agent = run.stored.session.agent;
|
|
371
|
+
if (agent !== undefined) {
|
|
372
|
+
const definition = configuration.subagents.get(agent);
|
|
373
|
+
if (!definition) throw new FlexHarnessValidationError(`Subagent "${agent}" is disabled.`);
|
|
374
|
+
run.agentAllowedTools = definition.allowedTools;
|
|
375
|
+
}
|
|
376
|
+
await this.core.reversions.prepareRunReversion(run, signal);
|
|
377
|
+
run.phase = 'running';
|
|
378
|
+
const queued = run.stored.outstandingPromptsById.get(run.queueId);
|
|
379
|
+
if (queued && (queued.status === 'starting' || queued.status === 'scheduled')) {
|
|
380
|
+
queued.status = 'running';
|
|
381
|
+
this.core.prompts.emitPromptQueueEvent(queued, 'prompt.running');
|
|
382
|
+
}
|
|
383
|
+
const resolverRelationship = {
|
|
384
|
+
...(run.stored.session.parentSessionId === undefined
|
|
385
|
+
? {}
|
|
386
|
+
: { parentSessionId: run.stored.session.parentSessionId }),
|
|
387
|
+
...(run.stored.session.agent === undefined ? {} : { agent: run.stored.session.agent }),
|
|
388
|
+
};
|
|
389
|
+
const modelOutcome = Promise.resolve()
|
|
390
|
+
.then(() => this.core.modelResolver.resolveModel(Object.freeze({
|
|
391
|
+
scopeId: run.scopeId,
|
|
392
|
+
scope: run.scope as TScope,
|
|
393
|
+
sessionId: run.sessionId,
|
|
394
|
+
...requireSessionGeneration(run.stored.session),
|
|
395
|
+
runId: run.runId,
|
|
396
|
+
...(options.modelHint ? { modelHint: options.modelHint } : {}),
|
|
397
|
+
...resolverRelationship,
|
|
398
|
+
signal,
|
|
399
|
+
})))
|
|
400
|
+
.then(
|
|
401
|
+
(value): IResolverOutcome<IFlexResolvedModel> => ({ source: 'model', success: true, value }),
|
|
402
|
+
(error): IResolverOutcome<IFlexResolvedModel> => ({ source: 'model', success: false, error }),
|
|
403
|
+
);
|
|
404
|
+
const toolProviderContext: Readonly<IFlexToolProviderContext<TScope>> = Object.freeze({
|
|
405
|
+
scopeId: run.scopeId,
|
|
406
|
+
scope: run.scope as TScope,
|
|
407
|
+
sessionId: run.sessionId,
|
|
408
|
+
...requireSessionGeneration(run.stored.session),
|
|
409
|
+
runId: run.runId,
|
|
410
|
+
...resolverRelationship,
|
|
411
|
+
signal,
|
|
412
|
+
requestPermission: async (request: IFlexPermissionRequestInput) => {
|
|
413
|
+
// A call that started early asks once the model's response confirmed it.
|
|
414
|
+
const confirmed = request.toolCallId === undefined
|
|
415
|
+
? undefined
|
|
416
|
+
: run.unconfirmedToolCalls.get(request.toolCallId);
|
|
417
|
+
if (confirmed) await confirmed;
|
|
418
|
+
return this.core.permissions.requestPermission(run.state, run, request);
|
|
419
|
+
},
|
|
420
|
+
});
|
|
421
|
+
const toolOutcome = Promise.resolve()
|
|
422
|
+
.then(() => this.core.providers.provideRunToolHandle(run, toolProviderContext))
|
|
423
|
+
.then(
|
|
424
|
+
(value): IResolverOutcome<IFlexToolHandle | undefined> => ({ source: 'tools', success: true, value }),
|
|
425
|
+
(error): IResolverOutcome<IFlexToolHandle | undefined> => ({ source: 'tools', success: false, error }),
|
|
426
|
+
);
|
|
427
|
+
let resolveAbort!: (outcome: IResolverOutcome<never>) => void;
|
|
428
|
+
const abortOutcome = new Promise<IResolverOutcome<never>>((resolve) => {
|
|
429
|
+
resolveAbort = resolve;
|
|
430
|
+
});
|
|
431
|
+
const onAbort = () => resolveAbort({
|
|
432
|
+
source: 'abort',
|
|
433
|
+
success: false,
|
|
434
|
+
error: signal.reason ?? new FlexHarnessAbortError(),
|
|
435
|
+
});
|
|
436
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
437
|
+
if (signal.aborted) onAbort();
|
|
438
|
+
let model: IFlexResolvedModel | undefined;
|
|
439
|
+
let toolHandle: IFlexToolHandle | undefined;
|
|
440
|
+
try {
|
|
441
|
+
const first = await Promise.race([modelOutcome, toolOutcome, abortOutcome]);
|
|
442
|
+
if (!first.success) {
|
|
443
|
+
const failure = first.source === 'abort'
|
|
444
|
+
? first.error
|
|
445
|
+
: this.core.projectExternalError(run, first.error, first.source === 'model' ? 'modelResolver' : 'toolProvider');
|
|
446
|
+
if (first.source !== 'abort') this.abortRunInternally(run, failure);
|
|
447
|
+
if (first.source !== 'tools') this.core.providers.observeDetachedToolProvider(toolOutcome, run);
|
|
448
|
+
throw failure;
|
|
449
|
+
}
|
|
450
|
+
try {
|
|
451
|
+
if (first.source === 'model') model = normalizeResolvedModel(first.value as IFlexResolvedModel);
|
|
452
|
+
else toolHandle = this.core.providers.normalizeToolHandle(run, first.value as IFlexToolHandle | undefined);
|
|
453
|
+
} catch (error) {
|
|
454
|
+
const projected = first.source === 'model'
|
|
455
|
+
? this.core.projectExternalError(run, error, 'modelResolver')
|
|
456
|
+
: this.core.projectExternalError(run, error, 'toolProvider');
|
|
457
|
+
this.abortRunInternally(run, projected);
|
|
458
|
+
if (first.source === 'model') this.core.providers.observeDetachedToolProvider(toolOutcome, run);
|
|
459
|
+
throw projected;
|
|
460
|
+
}
|
|
461
|
+
const second = await Promise.race([
|
|
462
|
+
first.source === 'model' ? toolOutcome : modelOutcome,
|
|
463
|
+
abortOutcome,
|
|
464
|
+
]);
|
|
465
|
+
if (!second.success) {
|
|
466
|
+
const failure = second.source === 'abort'
|
|
467
|
+
? second.error
|
|
468
|
+
: this.core.projectExternalError(run, second.error, second.source === 'model' ? 'modelResolver' : 'toolProvider');
|
|
469
|
+
if (second.source !== 'abort') this.abortRunInternally(run, failure);
|
|
470
|
+
if (!toolHandle && first.source === 'model' && second.source === 'abort') {
|
|
471
|
+
this.core.providers.observeDetachedToolProvider(toolOutcome, run);
|
|
472
|
+
}
|
|
473
|
+
throw failure;
|
|
474
|
+
}
|
|
475
|
+
try {
|
|
476
|
+
if (second.source === 'model') model = normalizeResolvedModel(second.value as IFlexResolvedModel);
|
|
477
|
+
else toolHandle = this.core.providers.normalizeToolHandle(run, second.value as IFlexToolHandle | undefined);
|
|
478
|
+
} catch (error) {
|
|
479
|
+
const projected = second.source === 'model'
|
|
480
|
+
? this.core.projectExternalError(run, error, 'modelResolver')
|
|
481
|
+
: this.core.projectExternalError(run, error, 'toolProvider');
|
|
482
|
+
this.abortRunInternally(run, projected);
|
|
483
|
+
throw projected;
|
|
484
|
+
}
|
|
485
|
+
signal.throwIfAborted();
|
|
486
|
+
run.modelResolution = model!;
|
|
487
|
+
run.toolProviderContext = toolProviderContext as Readonly<IFlexToolProviderContext<unknown>>;
|
|
488
|
+
run.isReadOnlyHostCall = toolHandle?.isReadOnlyToolCall;
|
|
489
|
+
let catalog: FlexToolCatalog<unknown> | undefined;
|
|
490
|
+
if (toolHandle?.catalog) {
|
|
491
|
+
try {
|
|
492
|
+
catalog = await FlexToolCatalog.create(toolHandle.catalog, this.core.toolCatalogOptions, {
|
|
493
|
+
results: this.core.stores.results !== undefined,
|
|
494
|
+
});
|
|
495
|
+
if (catalog.codeMode) run.codeSandbox = await FlexCodeSandbox.load();
|
|
496
|
+
} catch (error) {
|
|
497
|
+
throw this.core.projectExternalError(run, error, 'toolProvider');
|
|
498
|
+
}
|
|
499
|
+
signal.throwIfAborted();
|
|
500
|
+
}
|
|
501
|
+
let tools: TFlexAgentToolSet | undefined;
|
|
502
|
+
try {
|
|
503
|
+
const providedTools = toolHandle?.tools;
|
|
504
|
+
const combinedTools: Record<string, unknown> = { ...(providedTools ?? {}) };
|
|
505
|
+
Object.assign(combinedTools, this.createBuiltInTools(run));
|
|
506
|
+
if (catalog) assertNoCatalogCollisions(catalog, combinedTools);
|
|
507
|
+
const globalNames = run.allowedTools === undefined ? undefined : new Set(run.allowedTools);
|
|
508
|
+
const agentNames = run.agentAllowedTools === undefined ? undefined : new Set(run.agentAllowedTools);
|
|
509
|
+
const admitted = (name: string): boolean =>
|
|
510
|
+
(globalNames === undefined || globalNames.has(name))
|
|
511
|
+
&& (agentNames === undefined || agentNames.has(name));
|
|
512
|
+
const admittedTools = globalNames === undefined && agentNames === undefined
|
|
513
|
+
? combinedTools
|
|
514
|
+
: Object.fromEntries(Object.entries(combinedTools).filter(([name]) => admitted(name)));
|
|
515
|
+
const admittedCatalog = catalog && (globalNames === undefined && agentNames === undefined
|
|
516
|
+
? catalog
|
|
517
|
+
: catalog.restrict(catalog.methods.filter((name) => name === FLEX_RESULT_METHOD || admitted(name))));
|
|
518
|
+
if (admittedCatalog && admittedCatalog.methods.length > 0) {
|
|
519
|
+
run.toolCatalog = admittedCatalog;
|
|
520
|
+
Object.assign(admittedTools, this.core.toolDispatch.createCatalogTools(run, admittedCatalog));
|
|
521
|
+
}
|
|
522
|
+
tools = Object.keys(admittedTools).length > 0
|
|
523
|
+
? this.core.toolDispatch.trackToolCalls(run, wrapToolSet(
|
|
524
|
+
admittedTools as TFlexAgentToolSet,
|
|
525
|
+
this.core.toolOutputLimits,
|
|
526
|
+
(toolError) => this.core.projectExternalError(run, toolError, 'toolExecution'),
|
|
527
|
+
(output) => this.core.results.budgetOutput(run, output),
|
|
528
|
+
))
|
|
529
|
+
: undefined;
|
|
530
|
+
} catch (error) {
|
|
531
|
+
throw this.core.projectExternalError(run, error, 'toolProvider');
|
|
532
|
+
}
|
|
533
|
+
let closed = false;
|
|
534
|
+
let closePromise: Promise<void> | undefined;
|
|
535
|
+
const close = toolHandle?.close
|
|
536
|
+
? async () => {
|
|
537
|
+
if (closed) return;
|
|
538
|
+
// Inference has ended before its tool lease closes. Fence follow-up admission
|
|
539
|
+
// synchronously, even when external cleanup has not settled yet.
|
|
540
|
+
run.steering.input.close();
|
|
541
|
+
closePromise ??= Promise.resolve()
|
|
542
|
+
.then(() => toolHandle!.close!())
|
|
543
|
+
.then(() => {
|
|
544
|
+
closed = true;
|
|
545
|
+
})
|
|
546
|
+
.catch((cleanupError) => {
|
|
547
|
+
closePromise = undefined;
|
|
548
|
+
throw this.core.projectExternalError(run, cleanupError, 'toolCleanup');
|
|
549
|
+
});
|
|
550
|
+
await closePromise;
|
|
551
|
+
}
|
|
552
|
+
: undefined;
|
|
553
|
+
return {
|
|
554
|
+
model: model!.model,
|
|
555
|
+
...(options.system ?? model!.system ? { system: options.system ?? model!.system } : {}),
|
|
556
|
+
...(tools ? {
|
|
557
|
+
tools,
|
|
558
|
+
isReadOnlyToolCall: ({ toolName, input }: { toolName: string; input: unknown }) =>
|
|
559
|
+
this.core.toolDispatch.isReadOnlyToolCall(run, toolName, input),
|
|
560
|
+
} : {}),
|
|
561
|
+
...(model!.providerOptions ? { providerOptions: model!.providerOptions } : {}),
|
|
562
|
+
...(model!.cache === undefined ? {} : { cache: model!.cache }),
|
|
563
|
+
...(options.maxSteps ?? model!.maxSteps ? { maxSteps: options.maxSteps ?? model!.maxSteps } : {}),
|
|
564
|
+
...(close ? { close } : {}),
|
|
565
|
+
};
|
|
566
|
+
} catch (error) {
|
|
567
|
+
if (toolHandle) {
|
|
568
|
+
try {
|
|
569
|
+
await this.core.providers.closePartialToolHandle(toolHandle, run);
|
|
570
|
+
} catch (cleanupError) {
|
|
571
|
+
throw combineErrors([error, cleanupError]);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
throw error;
|
|
575
|
+
} finally {
|
|
576
|
+
signal.removeEventListener('abort', onAbort);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
public enabledBuiltInToolNames(run: IActiveRun): Set<string> {
|
|
581
|
+
const names = new Set<string>();
|
|
582
|
+
if (run.subagents.size > 0 && (run.stored.session.depth ?? 0) < this.core.maxSubagentDepth) {
|
|
583
|
+
names.add('delegate');
|
|
584
|
+
names.add('delegate_result');
|
|
585
|
+
names.add('delegate_send');
|
|
586
|
+
}
|
|
587
|
+
if (this.core.builtInTools.renameSession) names.add('rename_session');
|
|
588
|
+
const projectManagement = this.core.builtInTools.projectManagement;
|
|
589
|
+
if (projectManagement?.task) names.add('task');
|
|
590
|
+
if (projectManagement?.goal) names.add('goal');
|
|
591
|
+
if (projectManagement?.scratchpad) names.add('scratchpad');
|
|
592
|
+
return names;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
public assertNoBuiltInToolCollisions(run: IActiveRun, tools: TFlexAgentToolSet): void {
|
|
596
|
+
const reserved = this.enabledBuiltInToolNames(run);
|
|
597
|
+
const collision = Object.keys(tools).find((name) => reserved.has(name));
|
|
598
|
+
if (collision) {
|
|
599
|
+
throw new FlexHarnessValidationError(
|
|
600
|
+
`Tool provider cannot define enabled built-in tool "${collision}".`,
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
private createBuiltInTools(run: IActiveRun): Record<string, unknown> {
|
|
606
|
+
const tools: Record<string, unknown> = {};
|
|
607
|
+
if (run.subagents.size > 0 && (run.stored.session.depth ?? 0) < this.core.maxSubagentDepth) {
|
|
608
|
+
tools.delegate = this.core.subagentTasks.createSubagentTool(run);
|
|
609
|
+
tools.delegate_result = this.core.subagentTasks.createSubagentResultTool(run);
|
|
610
|
+
tools.delegate_send = this.core.subagentTasks.createSubagentSendTool(run);
|
|
611
|
+
}
|
|
612
|
+
if (this.core.builtInTools.renameSession) tools.rename_session = this.createRenameSessionTool(run);
|
|
613
|
+
const projectManagement = this.core.builtInTools.projectManagement;
|
|
614
|
+
if (projectManagement?.task) tools.task = this.core.projects.createProjectTaskTool(run);
|
|
615
|
+
if (projectManagement?.goal) tools.goal = this.core.projects.createProjectGoalTool(run);
|
|
616
|
+
if (projectManagement?.scratchpad) tools.scratchpad = this.core.projects.createProjectScratchpadTool(run);
|
|
617
|
+
return tools;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
private createRenameSessionTool(run: IActiveRun): unknown {
|
|
621
|
+
return plugins.tool({
|
|
622
|
+
description: 'Set the current FlexHarness session title.',
|
|
623
|
+
inputSchema: plugins.z.object({
|
|
624
|
+
title: plugins.z.string().min(1).max(FLEX_PROJECT_MANAGEMENT_LIMITS.maxTitleBytes),
|
|
625
|
+
}).strict(),
|
|
626
|
+
execute: async ({ title }: { title: string }) => {
|
|
627
|
+
validateUtf8String(
|
|
628
|
+
title,
|
|
629
|
+
'title',
|
|
630
|
+
FLEX_PROJECT_MANAGEMENT_LIMITS.maxTitleBytes,
|
|
631
|
+
true,
|
|
632
|
+
);
|
|
633
|
+
let session!: IFlexSession;
|
|
634
|
+
await this.core.mutateScope(run.state, () => {
|
|
635
|
+
if (run.state.sessions.get(run.sessionId) !== run.stored) {
|
|
636
|
+
throw new FlexHarnessNotFoundError('Session', run.sessionId);
|
|
637
|
+
}
|
|
638
|
+
run.stored.session.title = title;
|
|
639
|
+
run.stored.session.updatedAt = new Date().toISOString();
|
|
640
|
+
session = publicSnapshot(run.stored.session);
|
|
641
|
+
});
|
|
642
|
+
this.core.emitEvent(run.scopeId, session, {
|
|
643
|
+
type: 'session.updated',
|
|
644
|
+
session,
|
|
645
|
+
});
|
|
646
|
+
return { session };
|
|
647
|
+
},
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
public abortExactRun(
|
|
652
|
+
state: IStorageState,
|
|
653
|
+
sessionId: string,
|
|
654
|
+
runId: string,
|
|
655
|
+
reason: unknown,
|
|
656
|
+
): boolean {
|
|
657
|
+
const active = state.activeRuns.get(sessionId);
|
|
658
|
+
if (!active || active.runId !== runId || active.phase === 'finalizing' || active.phase === 'promoting') {
|
|
659
|
+
return false;
|
|
660
|
+
}
|
|
661
|
+
const cancellation = reason instanceof FlexHarnessAbortError
|
|
662
|
+
? reason
|
|
663
|
+
: this.core.trustInternalError(new FlexHarnessAbortError('The parent run was aborted.'));
|
|
664
|
+
this.cancelRun(active, cancellation);
|
|
665
|
+
return true;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
public createReservation(
|
|
669
|
+
run: IActiveRun,
|
|
670
|
+
prompt: INormalizedFlexPrompt,
|
|
671
|
+
): { userMessage: IFlexMessage; assistantMessage: IFlexMessage } {
|
|
672
|
+
const timestamp = new Date().toISOString();
|
|
673
|
+
const userMessage: IFlexMessage = {
|
|
674
|
+
messageId: run.userMessageId,
|
|
675
|
+
sessionId: run.sessionId,
|
|
676
|
+
runId: run.runId,
|
|
677
|
+
role: 'user',
|
|
678
|
+
status: 'streaming',
|
|
679
|
+
createdAt: timestamp,
|
|
680
|
+
parts: this.userMessageParts(prompt),
|
|
681
|
+
};
|
|
682
|
+
const assistantMessage: IFlexMessage = {
|
|
683
|
+
messageId: run.assistantMessageId,
|
|
684
|
+
sessionId: run.sessionId,
|
|
685
|
+
runId: run.runId,
|
|
686
|
+
role: 'assistant',
|
|
687
|
+
status: 'streaming',
|
|
688
|
+
createdAt: timestamp,
|
|
689
|
+
parts: [],
|
|
690
|
+
};
|
|
691
|
+
return { userMessage, assistantMessage };
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
private userMessageParts(prompt: INormalizedFlexPrompt): TFlexMessagePart[] {
|
|
695
|
+
return prompt.parts.map((part) => {
|
|
696
|
+
if (part.type === 'text') {
|
|
697
|
+
return { partId: plugins.crypto.randomUUID(), type: 'text' as const, text: part.text };
|
|
698
|
+
}
|
|
699
|
+
return {
|
|
700
|
+
partId: plugins.crypto.randomUUID(),
|
|
701
|
+
type: 'attachment' as const,
|
|
702
|
+
attachmentType: part.type,
|
|
703
|
+
...describeAttachment(part),
|
|
704
|
+
};
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* Ends steering intake once the run's generation has settled and fixes what became of its
|
|
710
|
+
* steers. A run that stays in the model context reports the steers its generation did not
|
|
711
|
+
* record; a run that does not, because it failed, was stopped before any model output or its
|
|
712
|
+
* outcome could not be established, reports every steer it accepted, since the model context
|
|
713
|
+
* of later turns holds none of them. The bytes of the steers not taken in are released.
|
|
714
|
+
*/
|
|
715
|
+
public closeRunSteering(steering: IRunSteering): IRunSteeringOutcome {
|
|
716
|
+
if (steering.outcome) return steering.outcome;
|
|
717
|
+
steering.input.close();
|
|
718
|
+
const keptInContext = steering.keptInContext === true;
|
|
719
|
+
const unappliedSteerIds: string[] = [];
|
|
720
|
+
let heldBytes = 0;
|
|
721
|
+
for (const [steerId, entry] of steering.accepted) {
|
|
722
|
+
const applied = steering.applied.has(steerId);
|
|
723
|
+
if (!applied) heldBytes += entry.byteSize;
|
|
724
|
+
if (!keptInContext || !applied) unappliedSteerIds.push(steerId);
|
|
725
|
+
}
|
|
726
|
+
steering.stored.outstandingPromptBytes = Math.max(0, steering.stored.outstandingPromptBytes - heldBytes);
|
|
727
|
+
steering.outcome = Object.freeze({
|
|
728
|
+
keptInContext,
|
|
729
|
+
unappliedSteerIds: Object.freeze(unappliedSteerIds),
|
|
730
|
+
});
|
|
731
|
+
return steering.outcome;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* Projects a steer the run's generation took in as a user message of the run. The assistant
|
|
736
|
+
* message that answered before it is completed and the run answers on in a new one; a steer
|
|
737
|
+
* taken in before the run answered anything is placed before its still empty assistant message.
|
|
738
|
+
*/
|
|
739
|
+
public async applyRunSteer(run: IActiveRun, applied: plugins.IAgentAppliedInput): Promise<void> {
|
|
740
|
+
const steering = run.steering;
|
|
741
|
+
const accepted = steering.accepted.get(applied.inputId);
|
|
742
|
+
if (
|
|
743
|
+
!accepted
|
|
744
|
+
|| applied.generationId !== run.runId
|
|
745
|
+
|| steering.applied.has(applied.inputId)
|
|
746
|
+
|| steering.outcome !== undefined
|
|
747
|
+
) {
|
|
748
|
+
throw new Error(`Steer "${applied.inputId}" was taken in without being accepted by its run.`);
|
|
749
|
+
}
|
|
750
|
+
// The generation recorded the steer durably: it is in the model context whenever the run is.
|
|
751
|
+
steering.applied.add(applied.inputId);
|
|
752
|
+
steering.stored.outstandingPromptBytes = Math.max(
|
|
753
|
+
0,
|
|
754
|
+
steering.stored.outstandingPromptBytes - accepted.byteSize,
|
|
755
|
+
);
|
|
756
|
+
const timestamp = new Date().toISOString();
|
|
757
|
+
const steerMessage: IFlexMessage = {
|
|
758
|
+
messageId: plugins.crypto.randomUUID(),
|
|
759
|
+
sessionId: run.sessionId,
|
|
760
|
+
runId: run.runId,
|
|
761
|
+
role: 'user',
|
|
762
|
+
status: 'completed',
|
|
763
|
+
createdAt: timestamp,
|
|
764
|
+
completedAt: timestamp,
|
|
765
|
+
steerId: applied.inputId,
|
|
766
|
+
parts: this.userMessageParts(accepted.prompt),
|
|
767
|
+
};
|
|
768
|
+
const answered = run.callbackParts.length > 0;
|
|
769
|
+
const answeredAssistantId = run.assistantMessageId;
|
|
770
|
+
let completedAssistant: IFlexMessage | undefined;
|
|
771
|
+
const nextAssistant: IFlexMessage | undefined = answered
|
|
772
|
+
? {
|
|
773
|
+
messageId: plugins.crypto.randomUUID(),
|
|
774
|
+
sessionId: run.sessionId,
|
|
775
|
+
runId: run.runId,
|
|
776
|
+
role: 'assistant',
|
|
777
|
+
status: 'streaming',
|
|
778
|
+
createdAt: timestamp,
|
|
779
|
+
parts: [],
|
|
780
|
+
}
|
|
781
|
+
: undefined;
|
|
782
|
+
const closePublication = this.core.openPublication(run.state, run.sessionId);
|
|
783
|
+
try {
|
|
784
|
+
await this.core.mutateProjection(run.state, run.stored, () => {
|
|
785
|
+
const index = run.stored.messages.findIndex((message) => message.messageId === answeredAssistantId);
|
|
786
|
+
if (index < 0) throw new Error('The steered run has no projected assistant message.');
|
|
787
|
+
if (!nextAssistant) {
|
|
788
|
+
run.stored.messages.splice(index, 0, cloneSerializable(steerMessage));
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
completedAssistant = {
|
|
792
|
+
...cloneSerializable(run.stored.messages[index]),
|
|
793
|
+
status: 'completed',
|
|
794
|
+
completedAt: timestamp,
|
|
795
|
+
parts: cloneSerializable(run.callbackParts),
|
|
796
|
+
...(run.modelResolution ? { model: cloneSerializable(run.modelResolution.identity) } : {}),
|
|
797
|
+
};
|
|
798
|
+
run.stored.messages.splice(
|
|
799
|
+
index,
|
|
800
|
+
1,
|
|
801
|
+
cloneSerializable(completedAssistant),
|
|
802
|
+
cloneSerializable(steerMessage),
|
|
803
|
+
cloneSerializable(nextAssistant),
|
|
804
|
+
);
|
|
805
|
+
});
|
|
806
|
+
if (completedAssistant && nextAssistant) {
|
|
807
|
+
run.assistantMessageId = nextAssistant.messageId;
|
|
808
|
+
run.reservedAssistantMessage = publicSnapshot(nextAssistant);
|
|
809
|
+
run.callbackParts = [];
|
|
810
|
+
run.reasoningPartIds.clear();
|
|
811
|
+
this.core.emitEvent(run.scopeId, run.stored.session, {
|
|
812
|
+
type: 'message.updated',
|
|
813
|
+
runId: run.runId,
|
|
814
|
+
messageId: completedAssistant.messageId,
|
|
815
|
+
message: publicSnapshot(completedAssistant),
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
this.core.emitEvent(run.scopeId, run.stored.session, {
|
|
819
|
+
type: 'message.created',
|
|
820
|
+
runId: run.runId,
|
|
821
|
+
messageId: steerMessage.messageId,
|
|
822
|
+
message: publicSnapshot(steerMessage),
|
|
823
|
+
});
|
|
824
|
+
if (nextAssistant) {
|
|
825
|
+
this.core.emitEvent(run.scopeId, run.stored.session, {
|
|
826
|
+
type: 'message.created',
|
|
827
|
+
runId: run.runId,
|
|
828
|
+
messageId: nextAssistant.messageId,
|
|
829
|
+
message: publicSnapshot(nextAssistant),
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
this.core.emitEvent(run.scopeId, run.stored.session, {
|
|
833
|
+
type: 'prompt.steer.applied',
|
|
834
|
+
queueId: run.queueId,
|
|
835
|
+
runId: run.runId,
|
|
836
|
+
steerId: applied.inputId,
|
|
837
|
+
messageId: steerMessage.messageId,
|
|
838
|
+
});
|
|
839
|
+
} finally {
|
|
840
|
+
closePublication();
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
private buildTerminal(
|
|
845
|
+
run: IActiveRun,
|
|
846
|
+
status: 'completed' | 'failed' | 'cancelled',
|
|
847
|
+
result?: IRunResultProjection,
|
|
848
|
+
error?: Error,
|
|
849
|
+
): IFlexTerminalProjection {
|
|
850
|
+
const timestamp = new Date().toISOString();
|
|
851
|
+
const userMessage: IFlexMessage = cloneSerializable(
|
|
852
|
+
run.reservedUserMessage
|
|
853
|
+
?? run.stored.messages.find((message) => message.messageId === run.userMessageId)
|
|
854
|
+
?? {
|
|
855
|
+
messageId: run.userMessageId,
|
|
856
|
+
sessionId: run.sessionId,
|
|
857
|
+
runId: run.runId,
|
|
858
|
+
role: 'user',
|
|
859
|
+
status: 'streaming',
|
|
860
|
+
createdAt: timestamp,
|
|
861
|
+
parts: [],
|
|
862
|
+
} satisfies IFlexMessage,
|
|
863
|
+
);
|
|
864
|
+
const assistantMessage: IFlexMessage = cloneSerializable(
|
|
865
|
+
run.reservedAssistantMessage
|
|
866
|
+
?? run.stored.messages.find((message) => message.messageId === run.assistantMessageId)
|
|
867
|
+
?? {
|
|
868
|
+
messageId: run.assistantMessageId,
|
|
869
|
+
sessionId: run.sessionId,
|
|
870
|
+
runId: run.runId,
|
|
871
|
+
role: 'assistant',
|
|
872
|
+
status: 'streaming',
|
|
873
|
+
createdAt: timestamp,
|
|
874
|
+
parts: [],
|
|
875
|
+
} satisfies IFlexMessage,
|
|
876
|
+
);
|
|
877
|
+
assistantMessage.parts = cloneSerializable(run.callbackParts);
|
|
878
|
+
const terminalParts = assistantMessage.parts;
|
|
879
|
+
for (const part of terminalParts) {
|
|
880
|
+
if (part.type === 'reasoning' && part.status === 'running') {
|
|
881
|
+
part.status = status === 'completed' ? 'completed' : 'cancelled';
|
|
882
|
+
part.completedAt = timestamp;
|
|
883
|
+
} else if (part.type === 'tool' && part.status === 'running') {
|
|
884
|
+
part.status = status === 'cancelled' ? 'cancelled' : 'failed';
|
|
885
|
+
delete part.output;
|
|
886
|
+
part.error = status === 'cancelled'
|
|
887
|
+
? 'Tool execution was cancelled.'
|
|
888
|
+
: 'Tool execution ended without a terminal event.';
|
|
889
|
+
part.completedAt = timestamp;
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
userMessage.status = status;
|
|
893
|
+
userMessage.completedAt = timestamp;
|
|
894
|
+
assistantMessage.status = status;
|
|
895
|
+
assistantMessage.completedAt = timestamp;
|
|
896
|
+
if (run.modelResolution) assistantMessage.model = cloneSerializable(run.modelResolution.identity);
|
|
897
|
+
if (status === 'completed' && result) {
|
|
898
|
+
if (!assistantMessage.parts.some((part) => part.type === 'text')) {
|
|
899
|
+
assistantMessage.parts.push({
|
|
900
|
+
partId: plugins.crypto.randomUUID(),
|
|
901
|
+
type: 'text',
|
|
902
|
+
text: truncateUtf8(result.text, this.core.callbackLimits.maxOutputBytes),
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
assistantMessage.usage = cloneSerializable(result.usage);
|
|
906
|
+
} else {
|
|
907
|
+
assistantMessage.usage = cloneSerializable(run.usage);
|
|
908
|
+
const message = truncateUtf8(error?.message ?? externalErrorFallback.message, maxTransferMetadataBytes);
|
|
909
|
+
const limit = error instanceof FlexHarnessExternalError ? error.limit : undefined;
|
|
910
|
+
for (const failedMessage of [userMessage, assistantMessage]) {
|
|
911
|
+
failedMessage.error = message;
|
|
912
|
+
if (limit) failedMessage.limit = cloneSerializable(limit);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
return {
|
|
916
|
+
runId: run.runId,
|
|
917
|
+
status,
|
|
918
|
+
userMessage,
|
|
919
|
+
assistantMessage,
|
|
920
|
+
...(run.modelResolution ? { model: cloneSerializable(run.modelResolution.identity) } : {}),
|
|
921
|
+
...(status === 'completed' && result
|
|
922
|
+
? {
|
|
923
|
+
usage: cloneSerializable(result.usage),
|
|
924
|
+
finishReason: result.finishReason,
|
|
925
|
+
steps: result.steps,
|
|
926
|
+
}
|
|
927
|
+
: { usage: cloneSerializable(run.usage) }),
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
private async promoteCompletedTerminal(
|
|
932
|
+
run: IActiveRun,
|
|
933
|
+
terminal: IFlexTerminalProjection,
|
|
934
|
+
): Promise<void> {
|
|
935
|
+
await this.core.mutateProjection(run.state, run.stored, () => {
|
|
936
|
+
this.replaceVisibleRun(run.stored, terminal);
|
|
937
|
+
run.stored.stagedTerminals = run.stored.stagedTerminals.filter((entry) => entry.runId !== run.runId);
|
|
938
|
+
}, true);
|
|
939
|
+
if (!this.core.isTombstoned(run)) {
|
|
940
|
+
await this.core.mutateScope(run.state, () => {
|
|
941
|
+
if (run.state.sessions.get(run.sessionId) !== run.stored) return;
|
|
942
|
+
const timestamp = terminal.assistantMessage.completedAt!;
|
|
943
|
+
run.stored.session.status = 'idle';
|
|
944
|
+
run.stored.session.activity = {
|
|
945
|
+
runId: run.runId,
|
|
946
|
+
status: 'idle',
|
|
947
|
+
startedAt: run.stored.session.activity.startedAt,
|
|
948
|
+
completedAt: timestamp,
|
|
949
|
+
};
|
|
950
|
+
run.stored.session.updatedAt = timestamp;
|
|
951
|
+
}, true);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
private async publishFailedTerminal(
|
|
956
|
+
run: IActiveRun,
|
|
957
|
+
terminal: IFlexTerminalProjection,
|
|
958
|
+
): Promise<void> {
|
|
959
|
+
await this.core.mutateProjection(run.state, run.stored, () => {
|
|
960
|
+
this.replaceVisibleRun(run.stored, terminal);
|
|
961
|
+
run.stored.stagedTerminals = run.stored.stagedTerminals.filter((entry) => entry.runId !== run.runId);
|
|
962
|
+
}, true);
|
|
963
|
+
await this.core.mutateScope(run.state, () => {
|
|
964
|
+
if (run.state.sessions.get(run.sessionId) !== run.stored) return;
|
|
965
|
+
const timestamp = terminal.assistantMessage.completedAt!;
|
|
966
|
+
const activityStatus = terminal.status === 'completed' ? 'idle' : terminal.status;
|
|
967
|
+
run.stored.session.status = activityStatus;
|
|
968
|
+
run.stored.session.activity = {
|
|
969
|
+
runId: run.runId,
|
|
970
|
+
status: activityStatus,
|
|
971
|
+
startedAt: run.stored.session.activity.startedAt,
|
|
972
|
+
completedAt: timestamp,
|
|
973
|
+
...terminalActivityError(terminal.assistantMessage),
|
|
974
|
+
};
|
|
975
|
+
run.stored.session.updatedAt = timestamp;
|
|
976
|
+
}, true);
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
private replaceStagedTerminal(stored: IStoredSessionState, terminal: IFlexTerminalProjection): void {
|
|
980
|
+
stored.stagedTerminals = stored.stagedTerminals.filter((entry) => entry.runId !== terminal.runId);
|
|
981
|
+
stored.stagedTerminals.push(cloneSerializable(terminal));
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
/**
|
|
985
|
+
* Puts a run's terminal messages in place of their projected versions: its prompt, and its last
|
|
986
|
+
* assistant message. The steers a run took in, and the assistant messages it completed before
|
|
987
|
+
* them, stay where they are. A terminal message that is not projected yet is added: the prompt
|
|
988
|
+
* before the run's other messages, the assistant message after them.
|
|
989
|
+
*/
|
|
990
|
+
public replaceVisibleRun(stored: IStoredSessionState, terminal: IFlexTerminalProjection): void {
|
|
991
|
+
const replacements = new Map([
|
|
992
|
+
[terminal.userMessage.messageId, terminal.userMessage],
|
|
993
|
+
[terminal.assistantMessage.messageId, terminal.assistantMessage],
|
|
994
|
+
]);
|
|
995
|
+
const placed = new Set<string>();
|
|
996
|
+
stored.messages = stored.messages.map((message) => {
|
|
997
|
+
const replacement = message.runId === terminal.runId
|
|
998
|
+
? replacements.get(message.messageId)
|
|
999
|
+
: undefined;
|
|
1000
|
+
if (!replacement) return message;
|
|
1001
|
+
placed.add(replacement.messageId);
|
|
1002
|
+
return cloneSerializable(replacement);
|
|
1003
|
+
});
|
|
1004
|
+
const firstIndex = stored.messages.findIndex((message) => message.runId === terminal.runId);
|
|
1005
|
+
if (!placed.has(terminal.userMessage.messageId)) {
|
|
1006
|
+
stored.messages.splice(
|
|
1007
|
+
firstIndex < 0 ? stored.messages.length : firstIndex,
|
|
1008
|
+
0,
|
|
1009
|
+
cloneSerializable(terminal.userMessage),
|
|
1010
|
+
);
|
|
1011
|
+
}
|
|
1012
|
+
if (!placed.has(terminal.assistantMessage.messageId)) {
|
|
1013
|
+
const lastIndex = stored.messages.reduce(
|
|
1014
|
+
(last, message, index) => message.runId === terminal.runId ? index : last,
|
|
1015
|
+
-1,
|
|
1016
|
+
);
|
|
1017
|
+
stored.messages.splice(
|
|
1018
|
+
lastIndex < 0 ? stored.messages.length : lastIndex + 1,
|
|
1019
|
+
0,
|
|
1020
|
+
cloneSerializable(terminal.assistantMessage),
|
|
1021
|
+
);
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
private emitTerminalProjection(run: IActiveRun, terminal: IFlexTerminalProjection): void {
|
|
1026
|
+
for (const part of terminal.assistantMessage.parts) {
|
|
1027
|
+
const callbackPart = run.callbackParts.find((entry) => entry.partId === part.partId);
|
|
1028
|
+
const shouldComplete = part.type === 'text'
|
|
1029
|
+
? callbackPart?.type === 'text'
|
|
1030
|
+
: part.type === 'reasoning'
|
|
1031
|
+
? callbackPart?.type === 'reasoning' && callbackPart.status === 'running'
|
|
1032
|
+
: part.type === 'tool'
|
|
1033
|
+
? callbackPart?.type === 'tool' && callbackPart.status === 'running'
|
|
1034
|
+
: false;
|
|
1035
|
+
if (shouldComplete) {
|
|
1036
|
+
this.core.emitPartEvent(run, 'part.completed', part);
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
for (const message of [terminal.userMessage, terminal.assistantMessage]) {
|
|
1040
|
+
this.core.emitEvent(run.scopeId, run.stored.session, {
|
|
1041
|
+
type: 'message.updated',
|
|
1042
|
+
runId: run.runId,
|
|
1043
|
+
messageId: message.messageId,
|
|
1044
|
+
message: publicSnapshot(message),
|
|
1045
|
+
});
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
/**
|
|
1050
|
+
* Ends a run that fails without publishing a terminal projection, because its namespace was
|
|
1051
|
+
* fenced or retired or its durable outcome could not be established: listeners still receive
|
|
1052
|
+
* its terminal message events and its one `run.finished`. Nothing here is persisted; the next
|
|
1053
|
+
* load of the namespace repairs the stored projection from the canonical outcome.
|
|
1054
|
+
*/
|
|
1055
|
+
private emitUnpublishedRunFinish(run: IActiveRun, error: unknown, generated: boolean): void {
|
|
1056
|
+
if (run.finishedEventEmitted || run.state.tombstones.has(run.sessionId)) return;
|
|
1057
|
+
const cancelled = run.ownerCancellation !== undefined && run.internalFailure === undefined;
|
|
1058
|
+
const terminalError = cancelled
|
|
1059
|
+
? run.ownerCancellation!
|
|
1060
|
+
: error instanceof FlexHarnessRunError
|
|
1061
|
+
? error
|
|
1062
|
+
: this.core.projectExternalError(run, error, generated ? 'persistence' : 'agentSession');
|
|
1063
|
+
const terminal = this.buildTerminal(run, cancelled ? 'cancelled' : 'failed', undefined, terminalError);
|
|
1064
|
+
this.emitTerminalProjection(run, terminal);
|
|
1065
|
+
this.emitFinalRunEvent(run, terminal.assistantMessage, terminalError, cancelled);
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
private emitFinalRunEvent(
|
|
1069
|
+
run: IActiveRun,
|
|
1070
|
+
message: IFlexMessage,
|
|
1071
|
+
error: unknown,
|
|
1072
|
+
cancelled: boolean,
|
|
1073
|
+
): void {
|
|
1074
|
+
const status: IFlexRunFinishedEvent['status'] = error
|
|
1075
|
+
? cancelled ? 'cancelled' : 'failed'
|
|
1076
|
+
: 'completed';
|
|
1077
|
+
run.finishedEventEmitted = true;
|
|
1078
|
+
const steering = this.closeRunSteering(run.steering);
|
|
1079
|
+
this.core.emitEvent(run.scopeId, run.stored.session, {
|
|
1080
|
+
type: 'run.finished',
|
|
1081
|
+
runId: run.runId,
|
|
1082
|
+
messageId: run.assistantMessageId,
|
|
1083
|
+
status,
|
|
1084
|
+
message: publicSnapshot(message),
|
|
1085
|
+
...(error === undefined ? {} : { error: errorToInfo(error) }),
|
|
1086
|
+
keptInContext: steering.keptInContext,
|
|
1087
|
+
unappliedSteerIds: [...steering.unappliedSteerIds],
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
public cancelRun(
|
|
1092
|
+
run: IActiveRun,
|
|
1093
|
+
reason: FlexHarnessAbortError,
|
|
1094
|
+
context?: IFlexAgentContextInvocation<TScope>,
|
|
1095
|
+
): void {
|
|
1096
|
+
const invocationContext = context ?? this.core.compaction.createRunCompactorContext(run);
|
|
1097
|
+
// A stopped run takes no further steers; the ones it has not taken in are reported unapplied.
|
|
1098
|
+
run.steering.input.close();
|
|
1099
|
+
if (run.internalFailure === undefined && run.ownerCancellation === undefined) {
|
|
1100
|
+
run.ownerCancellation = reason;
|
|
1101
|
+
run.deferredCompactorContext = invocationContext;
|
|
1102
|
+
this.core.compaction.deferredCompactorContexts.set(run.originCompactorContext, invocationContext);
|
|
1103
|
+
}
|
|
1104
|
+
this.core.compaction.withCompactorContext(
|
|
1105
|
+
(run.deferredCompactorContext ?? invocationContext) as IFlexAgentContextInvocation<TScope>,
|
|
1106
|
+
() => {
|
|
1107
|
+
this.core.permissions.rejectRunPermissions(run.state, run, reason);
|
|
1108
|
+
// The owner stops the generation: what its model produced stays in the model context.
|
|
1109
|
+
if (run.ownerCancellation === reason && !run.stop.signal.aborted) run.stop.abort(reason);
|
|
1110
|
+
if (!run.controller.signal.aborted) run.controller.abort(reason);
|
|
1111
|
+
if (run.phase === 'admitting') return;
|
|
1112
|
+
if (run.scheduleKey) {
|
|
1113
|
+
run.stored.agentSession.cancelScheduledGeneration(run.scheduleKey, reason);
|
|
1114
|
+
} else {
|
|
1115
|
+
run.stored.agentSession.abortCurrentGeneration(reason);
|
|
1116
|
+
}
|
|
1117
|
+
},
|
|
1118
|
+
);
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
public abortRunInternally(
|
|
1122
|
+
run: IActiveRun,
|
|
1123
|
+
error: unknown,
|
|
1124
|
+
context?: IFlexAgentContextInvocation<TScope>,
|
|
1125
|
+
): void {
|
|
1126
|
+
const invocationContext = context ?? this.core.compaction.createRunCompactorContext(run);
|
|
1127
|
+
run.steering.input.close();
|
|
1128
|
+
if (run.internalFailure === undefined) {
|
|
1129
|
+
run.internalFailure = error ?? new Error('Internal FlexHarness run failure.');
|
|
1130
|
+
run.deferredCompactorContext = invocationContext;
|
|
1131
|
+
this.core.compaction.deferredCompactorContexts.set(run.originCompactorContext, invocationContext);
|
|
1132
|
+
}
|
|
1133
|
+
this.core.compaction.withCompactorContext(
|
|
1134
|
+
(run.deferredCompactorContext ?? invocationContext) as IFlexAgentContextInvocation<TScope>,
|
|
1135
|
+
() => {
|
|
1136
|
+
if (!run.controller.signal.aborted) run.controller.abort(run.internalFailure);
|
|
1137
|
+
run.stored.agentSession.abortCurrentGeneration(run.internalFailure);
|
|
1138
|
+
},
|
|
1139
|
+
);
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
public async rollbackAdmission(
|
|
1143
|
+
run: IActiveRun,
|
|
1144
|
+
projectionReserved: boolean,
|
|
1145
|
+
originalError: Error,
|
|
1146
|
+
): Promise<unknown[]> {
|
|
1147
|
+
const errors: unknown[] = [];
|
|
1148
|
+
if (run.transaction) {
|
|
1149
|
+
try {
|
|
1150
|
+
await this.core.compaction.withRunCompactorContext(
|
|
1151
|
+
run,
|
|
1152
|
+
() => run.stored.agentSession.finalizeGeneration(run.transaction!, 'interrupted'),
|
|
1153
|
+
);
|
|
1154
|
+
} catch (error) {
|
|
1155
|
+
errors.push(this.core.projectExternalError(run, error, 'agentSession'));
|
|
1156
|
+
}
|
|
1157
|
+
try {
|
|
1158
|
+
const outcome = (await this.core.durableCanonicalOutcomes(run.stored)).get(run.runId);
|
|
1159
|
+
if (outcome !== 'interrupted') {
|
|
1160
|
+
errors.push(this.core.projectExternalError(
|
|
1161
|
+
run,
|
|
1162
|
+
new Error('Prompt admission interruption is not durably established.'),
|
|
1163
|
+
'persistence',
|
|
1164
|
+
));
|
|
1165
|
+
}
|
|
1166
|
+
} catch (error) {
|
|
1167
|
+
errors.push(this.core.projectExternalError(run, error, 'persistence'));
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
if (projectionReserved || run.stored.metrics.pendingGenerationId === run.runId) {
|
|
1171
|
+
try {
|
|
1172
|
+
await this.core.mutateProjection(run.state, run.stored, () => {
|
|
1173
|
+
if (run.stored.metrics.pendingGenerationId === run.runId) {
|
|
1174
|
+
delete run.stored.metrics.pendingGenerationId;
|
|
1175
|
+
}
|
|
1176
|
+
run.stored.messages = run.stored.messages.filter((message) => message.runId !== run.runId);
|
|
1177
|
+
run.stored.stagedTerminals = run.stored.stagedTerminals.filter((entry) => entry.runId !== run.runId);
|
|
1178
|
+
run.stored.reversionSegments = run.stored.reversionSegments.filter(
|
|
1179
|
+
(segment) => segment.runId !== run.runId,
|
|
1180
|
+
);
|
|
1181
|
+
if (
|
|
1182
|
+
run.stored.pendingReversion?.kind === 'capture'
|
|
1183
|
+
&& run.stored.pendingReversion.runId === run.runId
|
|
1184
|
+
) delete run.stored.pendingReversion;
|
|
1185
|
+
}, true);
|
|
1186
|
+
} catch (error) {
|
|
1187
|
+
errors.push(this.core.projectExternalError(run, error, 'persistence'));
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
if (run.state.sessions.get(run.sessionId) === run.stored && run.stored.session.activity.runId === run.runId) {
|
|
1191
|
+
try {
|
|
1192
|
+
await this.core.mutateScope(run.state, () => {
|
|
1193
|
+
run.stored.session.status = 'idle';
|
|
1194
|
+
run.stored.session.activity = { status: 'idle' };
|
|
1195
|
+
run.stored.session.updatedAt = new Date().toISOString();
|
|
1196
|
+
}, true);
|
|
1197
|
+
} catch (error) {
|
|
1198
|
+
errors.push(this.core.projectExternalError(run, error, 'persistence'));
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
if (errors.length > 0) await this.core.lifecycle.fenceNamespace(run.state, run, combineErrors([originalError, ...errors]));
|
|
1202
|
+
return errors;
|
|
1203
|
+
}
|
|
1204
|
+
}
|