@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,331 @@
|
|
|
1
|
+
import { FlexHarnessAbortError, FlexHarnessValidationError } from './errors.js';
|
|
2
|
+
import type {
|
|
3
|
+
IFlexDelegatedRunAdmissionLease,
|
|
4
|
+
IFlexSession,
|
|
5
|
+
IFlexSubagentDefinition,
|
|
6
|
+
IFlexToolMessagePart,
|
|
7
|
+
} from './interfaces.js';
|
|
8
|
+
import type {
|
|
9
|
+
IStoredSessionState,
|
|
10
|
+
IStorageState,
|
|
11
|
+
IActiveRun,
|
|
12
|
+
IDelegatedRunAdmissionSeed,
|
|
13
|
+
IDelegatedRunAdmissionOwner,
|
|
14
|
+
IQueuedPrompt,
|
|
15
|
+
} from './interfaces.flexharness.js';
|
|
16
|
+
import { requireSessionGeneration } from './utils.flexharness.js';
|
|
17
|
+
import type { FlexHarnessCore } from './classes.flexharness.core.js';
|
|
18
|
+
|
|
19
|
+
/** Admission of delegated runs through the delegated run admission provider. */
|
|
20
|
+
export class FlexHarnessDelegatedRunAdmission<TScope> {
|
|
21
|
+
constructor(private readonly core: FlexHarnessCore<TScope>) {}
|
|
22
|
+
|
|
23
|
+
public assertDelegatedRunAdmissionOwnership(
|
|
24
|
+
seed: IDelegatedRunAdmissionSeed,
|
|
25
|
+
state: IStorageState,
|
|
26
|
+
childStored: IStoredSessionState,
|
|
27
|
+
scopeId: string,
|
|
28
|
+
): void {
|
|
29
|
+
const child = childStored.session;
|
|
30
|
+
const parentRun = seed.parentRun;
|
|
31
|
+
const parent = seed.parentStored.session;
|
|
32
|
+
const parentPartId = parentRun.toolPartIds.get(seed.parentToolCallId);
|
|
33
|
+
const parentPart = parentRun.callbackParts.find((part): part is IFlexToolMessagePart =>
|
|
34
|
+
part.type === 'tool'
|
|
35
|
+
&& part.partId === parentPartId
|
|
36
|
+
&& part.toolCallId === seed.parentToolCallId);
|
|
37
|
+
if (
|
|
38
|
+
seed.state !== state
|
|
39
|
+
|| seed.storageKey !== state.storageKey
|
|
40
|
+
|| seed.scopeId !== scopeId
|
|
41
|
+
|| seed.childStored !== childStored
|
|
42
|
+
|| childStored.storageKey !== seed.storageKey
|
|
43
|
+
|| state.sessions.get(seed.sessionId) !== childStored
|
|
44
|
+
|| state.tombstones.has(seed.sessionId)
|
|
45
|
+
|| state.initializingSessions.has(seed.sessionId)
|
|
46
|
+
|| child.sessionId !== seed.sessionId
|
|
47
|
+
|| child.sessionGenerationId !== seed.sessionGenerationId
|
|
48
|
+
|| child.sessionGenerationSequence !== seed.sessionGenerationSequence
|
|
49
|
+
|| child.parentSessionId !== seed.parentSessionId
|
|
50
|
+
|| child.parentRunId !== seed.originParentRunId
|
|
51
|
+
|| child.parentToolCallId !== seed.originParentToolCallId
|
|
52
|
+
|| child.agent !== seed.agent
|
|
53
|
+
|| child.depth !== seed.depth
|
|
54
|
+
|| seed.parentStored !== parentRun.stored
|
|
55
|
+
|| seed.parentStored.storageKey !== seed.storageKey
|
|
56
|
+
|| parentRun.state !== state
|
|
57
|
+
|| parentRun.scopeId !== seed.scopeId
|
|
58
|
+
|| parentRun.scope !== seed.scope
|
|
59
|
+
|| parentRun.sessionId !== seed.parentSessionId
|
|
60
|
+
|| parentRun.queueId !== seed.parentQueueId
|
|
61
|
+
|| parentRun.runId !== seed.parentRunId
|
|
62
|
+
|| state.sessions.get(seed.parentSessionId) !== seed.parentStored
|
|
63
|
+
|| state.tombstones.has(seed.parentSessionId)
|
|
64
|
+
|| parent.sessionId !== seed.parentSessionId
|
|
65
|
+
|| parent.sessionGenerationId !== seed.parentSessionGenerationId
|
|
66
|
+
|| parent.sessionGenerationSequence !== seed.parentSessionGenerationSequence
|
|
67
|
+
|| state.activeRuns.get(seed.parentSessionId) !== parentRun
|
|
68
|
+
|| parentRun.callbacksClosed
|
|
69
|
+
|| parentRun.controller.signal.aborted
|
|
70
|
+
|| parentPart?.status !== 'running'
|
|
71
|
+
) {
|
|
72
|
+
throw this.core.trustInternalError(
|
|
73
|
+
new FlexHarnessAbortError('The delegated prompt lost its exact parent or child ownership.'),
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
public async acquireDelegatedRunAdmission(
|
|
79
|
+
queued: IQueuedPrompt,
|
|
80
|
+
run: IActiveRun,
|
|
81
|
+
): Promise<void> {
|
|
82
|
+
const seed = queued.delegationSeed;
|
|
83
|
+
const provider = this.core.delegatedRunAdmissionProvider;
|
|
84
|
+
if (!seed || !provider) return;
|
|
85
|
+
const context = Object.freeze({
|
|
86
|
+
scopeId: seed.scopeId,
|
|
87
|
+
scope: seed.scope as TScope,
|
|
88
|
+
storageKey: seed.storageKey,
|
|
89
|
+
sessionId: seed.sessionId,
|
|
90
|
+
sessionGenerationId: seed.sessionGenerationId,
|
|
91
|
+
sessionGenerationSequence: seed.sessionGenerationSequence,
|
|
92
|
+
queueId: queued.queueId,
|
|
93
|
+
runId: run.runId,
|
|
94
|
+
parentSessionId: seed.parentSessionId,
|
|
95
|
+
parentSessionGenerationId: seed.parentSessionGenerationId,
|
|
96
|
+
parentSessionGenerationSequence: seed.parentSessionGenerationSequence,
|
|
97
|
+
parentQueueId: seed.parentQueueId,
|
|
98
|
+
parentRunId: seed.parentRunId,
|
|
99
|
+
parentToolCallId: seed.parentToolCallId,
|
|
100
|
+
originParentRunId: seed.originParentRunId,
|
|
101
|
+
originParentToolCallId: seed.originParentToolCallId,
|
|
102
|
+
agent: seed.agent,
|
|
103
|
+
depth: seed.depth,
|
|
104
|
+
signal: run.controller.signal,
|
|
105
|
+
});
|
|
106
|
+
const owner: IDelegatedRunAdmissionOwner = {
|
|
107
|
+
storageState: run.state,
|
|
108
|
+
run,
|
|
109
|
+
context,
|
|
110
|
+
status: 'acquiring',
|
|
111
|
+
acquisitionSettled: false,
|
|
112
|
+
acquisitionDetached: false,
|
|
113
|
+
closeAttemptSequence: 0,
|
|
114
|
+
};
|
|
115
|
+
run.state.delegatedRunAdmissionOwners.add(owner);
|
|
116
|
+
run.delegatedRunAdmissionOwner = owner;
|
|
117
|
+
const acquisition = Promise.resolve()
|
|
118
|
+
.then(() => provider.acquireDelegatedRunAdmission(context))
|
|
119
|
+
.then((lease) => this.normalizeDelegatedRunAdmissionLease(lease))
|
|
120
|
+
.then(
|
|
121
|
+
(lease) => {
|
|
122
|
+
owner.lease = lease;
|
|
123
|
+
owner.acquisitionSettled = true;
|
|
124
|
+
if (owner.status === 'acquiring') owner.status = 'acquired';
|
|
125
|
+
if (owner.status === 'close-requested') {
|
|
126
|
+
void this.startDelegatedRunAdmissionCloseAttempt(owner).catch(() => undefined);
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
(error: unknown) => {
|
|
130
|
+
owner.acquisitionError = this.core.projectExternalError(
|
|
131
|
+
run,
|
|
132
|
+
error,
|
|
133
|
+
'delegatedRunAdmissionProvider',
|
|
134
|
+
);
|
|
135
|
+
owner.acquisitionSettled = true;
|
|
136
|
+
owner.status = 'closed';
|
|
137
|
+
run.state.delegatedRunAdmissionOwners.delete(owner);
|
|
138
|
+
},
|
|
139
|
+
);
|
|
140
|
+
owner.acquisitionCompletion = acquisition;
|
|
141
|
+
|
|
142
|
+
let resolveAbort!: () => void;
|
|
143
|
+
const abort = new Promise<'aborted'>((resolve) => {
|
|
144
|
+
resolveAbort = () => resolve('aborted');
|
|
145
|
+
});
|
|
146
|
+
run.controller.signal.addEventListener('abort', resolveAbort, { once: true });
|
|
147
|
+
if (run.controller.signal.aborted) resolveAbort();
|
|
148
|
+
try {
|
|
149
|
+
const outcome = await Promise.race([
|
|
150
|
+
acquisition.then(() => 'settled' as const),
|
|
151
|
+
abort,
|
|
152
|
+
]);
|
|
153
|
+
if (outcome === 'aborted') {
|
|
154
|
+
owner.acquisitionDetached = true;
|
|
155
|
+
throw run.controller.signal.reason ?? this.core.trustInternalError(new FlexHarnessAbortError());
|
|
156
|
+
}
|
|
157
|
+
if (run.controller.signal.aborted) {
|
|
158
|
+
throw run.controller.signal.reason ?? this.core.trustInternalError(new FlexHarnessAbortError());
|
|
159
|
+
}
|
|
160
|
+
if (owner.acquisitionError) throw owner.acquisitionError;
|
|
161
|
+
if (owner.status !== 'acquired' || !owner.lease) {
|
|
162
|
+
throw this.core.trustInternalError(
|
|
163
|
+
new FlexHarnessAbortError('The delegated run admission was retired during acquisition.'),
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
} finally {
|
|
167
|
+
run.controller.signal.removeEventListener('abort', resolveAbort);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private normalizeDelegatedRunAdmissionLease(
|
|
172
|
+
value: IFlexDelegatedRunAdmissionLease,
|
|
173
|
+
): IFlexDelegatedRunAdmissionLease {
|
|
174
|
+
if (!value || (typeof value !== 'object' && typeof value !== 'function')) {
|
|
175
|
+
throw new FlexHarnessValidationError(
|
|
176
|
+
'Delegated run admission provider returned an invalid lease.',
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
const close = value.close;
|
|
180
|
+
if (typeof close !== 'function') {
|
|
181
|
+
throw new FlexHarnessValidationError(
|
|
182
|
+
'Delegated run admission provider returned an invalid lease.',
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
return Object.freeze({ close: () => close.call(value) });
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private requestDelegatedRunAdmissionClose(
|
|
189
|
+
owner: IDelegatedRunAdmissionOwner,
|
|
190
|
+
): Promise<void> {
|
|
191
|
+
const priorAttemptSequence = owner.closeAttemptSequence;
|
|
192
|
+
const joinedAttempt = owner.closeAttempt;
|
|
193
|
+
if (owner.status === 'closed') return Promise.resolve();
|
|
194
|
+
if (owner.status !== 'retrying') owner.status = 'close-requested';
|
|
195
|
+
return (async () => {
|
|
196
|
+
await owner.acquisitionCompletion;
|
|
197
|
+
if (owner.status === 'closed') return;
|
|
198
|
+
if (joinedAttempt) {
|
|
199
|
+
await joinedAttempt;
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (owner.closeAttempt) {
|
|
203
|
+
await owner.closeAttempt;
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (owner.closeFailure && owner.closeFailure.sequence > priorAttemptSequence) {
|
|
207
|
+
throw owner.closeFailure.error;
|
|
208
|
+
}
|
|
209
|
+
await this.startDelegatedRunAdmissionCloseAttempt(owner);
|
|
210
|
+
})();
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private startDelegatedRunAdmissionCloseAttempt(
|
|
214
|
+
owner: IDelegatedRunAdmissionOwner,
|
|
215
|
+
): Promise<void> {
|
|
216
|
+
if (owner.closeAttempt) return owner.closeAttempt;
|
|
217
|
+
if (!owner.lease) {
|
|
218
|
+
throw new Error('Acquired delegated run admission has no lease.');
|
|
219
|
+
}
|
|
220
|
+
owner.status = 'retrying';
|
|
221
|
+
owner.closeFailure = undefined;
|
|
222
|
+
const sequence = ++owner.closeAttemptSequence;
|
|
223
|
+
let attempt!: Promise<void>;
|
|
224
|
+
attempt = Promise.resolve()
|
|
225
|
+
.then(() => owner.lease!.close())
|
|
226
|
+
.then(() => {
|
|
227
|
+
owner.status = 'closed';
|
|
228
|
+
delete owner.lease;
|
|
229
|
+
owner.storageState.delegatedRunAdmissionOwners.delete(owner);
|
|
230
|
+
}, (error: unknown) => {
|
|
231
|
+
const projected = this.core.projectExternalError(
|
|
232
|
+
owner.run,
|
|
233
|
+
error,
|
|
234
|
+
'delegatedRunAdmissionProvider',
|
|
235
|
+
);
|
|
236
|
+
owner.closeFailure = { sequence, error: projected };
|
|
237
|
+
owner.status = 'close-requested';
|
|
238
|
+
throw projected;
|
|
239
|
+
})
|
|
240
|
+
.finally(() => {
|
|
241
|
+
if (owner.closeAttempt === attempt) owner.closeAttempt = undefined;
|
|
242
|
+
});
|
|
243
|
+
owner.closeAttempt = attempt;
|
|
244
|
+
return attempt;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
public async closeRunDelegatedRunAdmission(
|
|
248
|
+
run: IActiveRun,
|
|
249
|
+
awaitSettlement = true,
|
|
250
|
+
): Promise<void> {
|
|
251
|
+
const owner = run.delegatedRunAdmissionOwner;
|
|
252
|
+
if (!owner) return;
|
|
253
|
+
if (!run.delegatedRunAdmissionClose) {
|
|
254
|
+
run.delegatedRunAdmissionClose = this.requestDelegatedRunAdmissionClose(owner);
|
|
255
|
+
void run.delegatedRunAdmissionClose.catch(() => undefined);
|
|
256
|
+
}
|
|
257
|
+
if (awaitSettlement) await run.delegatedRunAdmissionClose;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
public createDelegatedRunAdmissionSeed(
|
|
261
|
+
run: IActiveRun,
|
|
262
|
+
child: IStoredSessionState,
|
|
263
|
+
definition: Readonly<IFlexSubagentDefinition>,
|
|
264
|
+
toolCallId: string,
|
|
265
|
+
): IDelegatedRunAdmissionSeed {
|
|
266
|
+
const childGeneration = requireSessionGeneration(child.session);
|
|
267
|
+
const parentGeneration = requireSessionGeneration(run.stored.session);
|
|
268
|
+
if (!child.session.parentRunId || !child.session.parentToolCallId) {
|
|
269
|
+
throw new FlexHarnessValidationError('Subagent session origin metadata is missing.');
|
|
270
|
+
}
|
|
271
|
+
const seed = Object.freeze({
|
|
272
|
+
state: run.state,
|
|
273
|
+
childStored: child,
|
|
274
|
+
parentRun: run,
|
|
275
|
+
parentStored: run.stored,
|
|
276
|
+
scopeId: run.scopeId,
|
|
277
|
+
scope: run.scope,
|
|
278
|
+
storageKey: run.state.storageKey,
|
|
279
|
+
sessionId: child.session.sessionId,
|
|
280
|
+
...childGeneration,
|
|
281
|
+
originParentRunId: child.session.parentRunId,
|
|
282
|
+
originParentToolCallId: child.session.parentToolCallId,
|
|
283
|
+
parentSessionId: run.sessionId,
|
|
284
|
+
parentSessionGenerationId: parentGeneration.sessionGenerationId,
|
|
285
|
+
parentSessionGenerationSequence: parentGeneration.sessionGenerationSequence,
|
|
286
|
+
parentQueueId: run.queueId,
|
|
287
|
+
parentRunId: run.runId,
|
|
288
|
+
parentToolCallId: toolCallId,
|
|
289
|
+
...(run.stored.session.configurationRef === undefined
|
|
290
|
+
? {}
|
|
291
|
+
: { configurationRef: run.stored.session.configurationRef }),
|
|
292
|
+
agent: definition.name,
|
|
293
|
+
depth: (run.stored.session.depth ?? 0) + 1,
|
|
294
|
+
});
|
|
295
|
+
this.assertDelegatedRunAdmissionOwnership(seed, run.state, child, run.scopeId);
|
|
296
|
+
return seed;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
public delegatedRunAdmissionOwnersForGeneration(
|
|
300
|
+
state: IStorageState,
|
|
301
|
+
session: Readonly<Pick<
|
|
302
|
+
IFlexSession,
|
|
303
|
+
'sessionId' | 'sessionGenerationId' | 'sessionGenerationSequence'
|
|
304
|
+
>>,
|
|
305
|
+
): IDelegatedRunAdmissionOwner[] {
|
|
306
|
+
return [...state.delegatedRunAdmissionOwners].filter((owner) =>
|
|
307
|
+
owner.storageState === state
|
|
308
|
+
&& owner.context.sessionId === session.sessionId
|
|
309
|
+
&& owner.context.sessionGenerationId === session.sessionGenerationId
|
|
310
|
+
&& owner.context.sessionGenerationSequence === session.sessionGenerationSequence);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
public async settleDelegatedRunAdmissionOwners(
|
|
314
|
+
owners: readonly IDelegatedRunAdmissionOwner[],
|
|
315
|
+
): Promise<Error[]> {
|
|
316
|
+
const results = await Promise.allSettled(
|
|
317
|
+
owners.map((owner) => this.requestDelegatedRunAdmissionClose(owner)),
|
|
318
|
+
);
|
|
319
|
+
return results.flatMap((result, index) => {
|
|
320
|
+
if (result.status === 'fulfilled') return [];
|
|
321
|
+
const error = result.reason instanceof Error
|
|
322
|
+
? result.reason
|
|
323
|
+
: this.core.projectExternalError(
|
|
324
|
+
owners[index].run,
|
|
325
|
+
result.reason,
|
|
326
|
+
'delegatedRunAdmissionProvider',
|
|
327
|
+
);
|
|
328
|
+
return [error];
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
}
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
import { FlexHarnessCallbackOverflowError } from './errors.js';
|
|
3
|
+
import {
|
|
4
|
+
FLEX_TOOL_CATALOG_TOOL_NAMES,
|
|
5
|
+
type IFlexReasoningMessagePart,
|
|
6
|
+
type IFlexToolMessagePart,
|
|
7
|
+
type IFlexUsage,
|
|
8
|
+
type TFlexAgentToolCallUpdateEvent,
|
|
9
|
+
type TFlexAgentToolCallFinishEvent,
|
|
10
|
+
type TFlexMessagePart,
|
|
11
|
+
} from './interfaces.js';
|
|
12
|
+
import { normalizeJsonValue } from './utils.json.js';
|
|
13
|
+
import { jsonBytes } from './utils.transfer.js';
|
|
14
|
+
import {
|
|
15
|
+
observeSessionMetricsCall,
|
|
16
|
+
settleSessionMetricsOperation,
|
|
17
|
+
type ISessionMetricsOperation,
|
|
18
|
+
} from './utils.sessionmetrics.js';
|
|
19
|
+
import type { IStoredSessionState, IStorageState, IActiveRun } from './interfaces.flexharness.js';
|
|
20
|
+
import type { FlexHarnessCore } from './classes.flexharness.core.js';
|
|
21
|
+
|
|
22
|
+
/** The model stream callbacks of a run: text, reasoning, tool, retry and usage events. */
|
|
23
|
+
export class FlexHarnessRunCallbacks<TScope> {
|
|
24
|
+
constructor(private readonly core: FlexHarnessCore<TScope>) {}
|
|
25
|
+
|
|
26
|
+
public onTextDelta(state: IStorageState, sessionId: string, delta: string): void {
|
|
27
|
+
const run = this.callbackRun(state, sessionId);
|
|
28
|
+
if (!run || !delta) return;
|
|
29
|
+
const lastPart = run.callbackParts.at(-1);
|
|
30
|
+
const created = lastPart?.type !== 'text';
|
|
31
|
+
const part = lastPart?.type === 'text'
|
|
32
|
+
? lastPart
|
|
33
|
+
: { partId: plugins.crypto.randomUUID(), type: 'text' as const, text: '' };
|
|
34
|
+
const accounting = this.textAppendAccounting(run, part, delta, created);
|
|
35
|
+
if (!accounting) return;
|
|
36
|
+
if (!this.reserveCallbackCapacity(
|
|
37
|
+
run,
|
|
38
|
+
created ? 2 : 1,
|
|
39
|
+
accounting.textUtf8Bytes - accounting.baseTextUtf8Bytes,
|
|
40
|
+
created ? 1 : 0,
|
|
41
|
+
)) return;
|
|
42
|
+
if (created) {
|
|
43
|
+
run.callbackParts.push(part);
|
|
44
|
+
run.partTextUtf8Bytes.set(part.partId, 0);
|
|
45
|
+
this.core.emitPartEvent(run, 'part.started', part);
|
|
46
|
+
}
|
|
47
|
+
part.text += delta;
|
|
48
|
+
run.partTextUtf8Bytes.set(part.partId, accounting.textUtf8Bytes);
|
|
49
|
+
this.core.emitPartDeltaEvent(run, part, delta, accounting);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
public onReasoningStart(state: IStorageState, sessionId: string, id: string): void {
|
|
53
|
+
const run = this.callbackRun(state, sessionId);
|
|
54
|
+
if (!run || run.reasoningPartIds.has(id)) return;
|
|
55
|
+
if (!this.reserveCallbackCapacity(run, 1, 0, 1)) return;
|
|
56
|
+
const part: IFlexReasoningMessagePart = {
|
|
57
|
+
partId: plugins.crypto.randomUUID(),
|
|
58
|
+
type: 'reasoning',
|
|
59
|
+
text: '',
|
|
60
|
+
status: 'running',
|
|
61
|
+
startedAt: new Date().toISOString(),
|
|
62
|
+
};
|
|
63
|
+
run.reasoningPartIds.set(id, part.partId);
|
|
64
|
+
run.callbackParts.push(part);
|
|
65
|
+
run.partTextUtf8Bytes.set(part.partId, 0);
|
|
66
|
+
this.core.emitPartEvent(run, 'part.started', part);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
public onReasoningDelta(state: IStorageState, sessionId: string, id: string, delta: string): void {
|
|
70
|
+
const run = this.callbackRun(state, sessionId);
|
|
71
|
+
if (!run || !delta) return;
|
|
72
|
+
if (!run.reasoningPartIds.has(id)) this.onReasoningStart(state, sessionId, id);
|
|
73
|
+
const partId = run.reasoningPartIds.get(id);
|
|
74
|
+
const part = run.callbackParts.find((entry) => entry.partId === partId && entry.type === 'reasoning');
|
|
75
|
+
if (!part || part.type !== 'reasoning' || part.status !== 'running') return;
|
|
76
|
+
const accounting = this.textAppendAccounting(run, part, delta);
|
|
77
|
+
if (!accounting) return;
|
|
78
|
+
if (!this.reserveCallbackCapacity(
|
|
79
|
+
run,
|
|
80
|
+
1,
|
|
81
|
+
accounting.textUtf8Bytes - accounting.baseTextUtf8Bytes,
|
|
82
|
+
0,
|
|
83
|
+
)) return;
|
|
84
|
+
part.text += delta;
|
|
85
|
+
run.partTextUtf8Bytes.set(part.partId, accounting.textUtf8Bytes);
|
|
86
|
+
this.core.emitPartDeltaEvent(run, part, delta, accounting);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
public onReasoningEnd(state: IStorageState, sessionId: string, id: string, text: string): void {
|
|
90
|
+
const run = this.callbackRun(state, sessionId);
|
|
91
|
+
if (!run) return;
|
|
92
|
+
if (!run.reasoningPartIds.has(id)) this.onReasoningStart(state, sessionId, id);
|
|
93
|
+
const partId = run.reasoningPartIds.get(id);
|
|
94
|
+
const part = run.callbackParts.find((entry) => entry.partId === partId && entry.type === 'reasoning');
|
|
95
|
+
if (!part || part.type !== 'reasoning' || part.status !== 'running') return;
|
|
96
|
+
const missingText = part.text ? '' : text;
|
|
97
|
+
const missingTextUtf8Bytes = Buffer.byteLength(missingText, 'utf8');
|
|
98
|
+
if (!this.reserveCallbackCapacity(run, 1, missingTextUtf8Bytes, 0)) return;
|
|
99
|
+
if (missingText) {
|
|
100
|
+
part.text = missingText;
|
|
101
|
+
run.partTextUtf8Bytes.set(part.partId, missingTextUtf8Bytes);
|
|
102
|
+
}
|
|
103
|
+
part.status = 'completed';
|
|
104
|
+
part.completedAt = new Date().toISOString();
|
|
105
|
+
this.core.emitPartEvent(run, 'part.completed', part);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private textAppendAccounting(
|
|
109
|
+
run: IActiveRun,
|
|
110
|
+
part: Extract<TFlexMessagePart, { type: 'text' | 'reasoning' }>,
|
|
111
|
+
delta: string,
|
|
112
|
+
created = false,
|
|
113
|
+
): { baseTextUtf8Bytes: number; textUtf8Bytes: number } | undefined {
|
|
114
|
+
const baseTextUtf8Bytes = created ? 0 : run.partTextUtf8Bytes.get(part.partId);
|
|
115
|
+
if (baseTextUtf8Bytes === undefined) {
|
|
116
|
+
throw new Error('Flex part text byte accounting is unavailable.');
|
|
117
|
+
}
|
|
118
|
+
const trailingCodeUnit = part.text.charCodeAt(part.text.length - 1);
|
|
119
|
+
const leadingCodeUnit = delta.charCodeAt(0);
|
|
120
|
+
const joinsSurrogatePair = trailingCodeUnit >= 0xD800
|
|
121
|
+
&& trailingCodeUnit <= 0xDBFF
|
|
122
|
+
&& leadingCodeUnit >= 0xDC00
|
|
123
|
+
&& leadingCodeUnit <= 0xDFFF;
|
|
124
|
+
const textUtf8Bytes = baseTextUtf8Bytes
|
|
125
|
+
+ Buffer.byteLength(delta, 'utf8')
|
|
126
|
+
- (joinsSurrogatePair ? 2 : 0);
|
|
127
|
+
if (!Number.isSafeInteger(textUtf8Bytes)) {
|
|
128
|
+
this.failCallback(run, this.core.trustInternalError(new FlexHarnessCallbackOverflowError(
|
|
129
|
+
'Callback text byte accounting exceeded the safe integer range.',
|
|
130
|
+
)));
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
return { baseTextUtf8Bytes, textUtf8Bytes };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
public onToolStart(
|
|
137
|
+
state: IStorageState,
|
|
138
|
+
sessionId: string,
|
|
139
|
+
event: { toolCallId: string; toolName: string; input: unknown; parentToolCallId?: string },
|
|
140
|
+
): void {
|
|
141
|
+
const run = this.callbackRun(state, sessionId);
|
|
142
|
+
if (!run || run.toolPartIds.has(event.toolCallId)) return;
|
|
143
|
+
try {
|
|
144
|
+
// An execute call is the call of the catalog method it names.
|
|
145
|
+
const target = event.toolName === FLEX_TOOL_CATALOG_TOOL_NAMES.execute
|
|
146
|
+
? run.toolCatalog?.callTarget(event.input)
|
|
147
|
+
: undefined;
|
|
148
|
+
const input = normalizeJsonValue(target ? target.params : event.input, this.core.toolOutputLimits);
|
|
149
|
+
if (!this.reserveCallbackCapacity(run, 1, jsonBytes(input), 1)) return;
|
|
150
|
+
const part: IFlexToolMessagePart = {
|
|
151
|
+
partId: plugins.crypto.randomUUID(),
|
|
152
|
+
type: 'tool',
|
|
153
|
+
toolCallId: this.core.boundedIdentifier(event.toolCallId, 'toolCallId'),
|
|
154
|
+
toolName: this.core.boundedIdentifier(target ? target.method : event.toolName, 'toolName'),
|
|
155
|
+
status: 'running',
|
|
156
|
+
input,
|
|
157
|
+
...(event.parentToolCallId === undefined
|
|
158
|
+
? {}
|
|
159
|
+
: { parentToolCallId: this.core.boundedIdentifier(event.parentToolCallId, 'parentToolCallId') }),
|
|
160
|
+
startedAt: new Date().toISOString(),
|
|
161
|
+
};
|
|
162
|
+
run.toolPartIds.set(event.toolCallId, part.partId);
|
|
163
|
+
run.callbackParts.push(part);
|
|
164
|
+
this.core.emitPartEvent(run, 'part.started', part);
|
|
165
|
+
} catch (error) {
|
|
166
|
+
this.failCallback(run, this.core.projectExternalError(run, error, 'toolCallback'));
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
public onToolFinish(
|
|
171
|
+
state: IStorageState,
|
|
172
|
+
sessionId: string,
|
|
173
|
+
event: TFlexAgentToolCallFinishEvent,
|
|
174
|
+
): void {
|
|
175
|
+
const run = this.callbackRun(state, sessionId);
|
|
176
|
+
if (!run) return;
|
|
177
|
+
if (!run.toolPartIds.has(event.toolCallId)) this.onToolStart(state, sessionId, event);
|
|
178
|
+
const partId = run.toolPartIds.get(event.toolCallId);
|
|
179
|
+
const part = run.callbackParts.find((entry) => entry.partId === partId && entry.type === 'tool');
|
|
180
|
+
if (!part || part.type !== 'tool' || part.status !== 'running') return;
|
|
181
|
+
try {
|
|
182
|
+
const trustedExecutionError = run.trustedToolExecutionErrors.get(event.toolCallId);
|
|
183
|
+
run.trustedToolExecutionErrors.delete(event.toolCallId);
|
|
184
|
+
const output = event.success ? normalizeJsonValue(event.output, this.core.toolOutputLimits) : undefined;
|
|
185
|
+
const projectedError = event.success
|
|
186
|
+
? undefined
|
|
187
|
+
: this.core.projectExternalError(run, trustedExecutionError ?? event.error, 'toolCallback');
|
|
188
|
+
const bytes = event.success ? jsonBytes(output) : Buffer.byteLength(projectedError!.message);
|
|
189
|
+
if (!this.reserveCallbackCapacity(run, 1, bytes - (part.output === undefined ? 0 : jsonBytes(part.output)), 0)) return;
|
|
190
|
+
if (event.success) {
|
|
191
|
+
part.status = 'completed';
|
|
192
|
+
part.output = output!;
|
|
193
|
+
} else {
|
|
194
|
+
part.status = 'failed';
|
|
195
|
+
delete part.output;
|
|
196
|
+
part.error = projectedError!.message;
|
|
197
|
+
}
|
|
198
|
+
const original = run.deduplicatedToolCalls.get(event.toolCallId);
|
|
199
|
+
if (original !== undefined) {
|
|
200
|
+
part.deduplicatedFrom = this.core.boundedIdentifier(original, 'deduplicatedFrom');
|
|
201
|
+
}
|
|
202
|
+
part.completedAt = new Date().toISOString();
|
|
203
|
+
this.core.emitPartEvent(run, 'part.completed', part);
|
|
204
|
+
} catch (error) {
|
|
205
|
+
this.failCallback(run, this.core.projectExternalError(run, error, 'toolCallback'));
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
public onToolUpdate(
|
|
210
|
+
state: IStorageState,
|
|
211
|
+
sessionId: string,
|
|
212
|
+
event: TFlexAgentToolCallUpdateEvent,
|
|
213
|
+
): void {
|
|
214
|
+
let run = this.callbackRun(state, sessionId);
|
|
215
|
+
if (!run) return;
|
|
216
|
+
if (!run.toolPartIds.has(event.toolCallId)) {
|
|
217
|
+
this.onToolStart(state, sessionId, event);
|
|
218
|
+
run = this.callbackRun(state, sessionId);
|
|
219
|
+
if (!run) return;
|
|
220
|
+
}
|
|
221
|
+
const partId = run.toolPartIds.get(event.toolCallId);
|
|
222
|
+
const part = run.callbackParts.find((entry) => entry.partId === partId && entry.type === 'tool');
|
|
223
|
+
if (!part || part.type !== 'tool' || part.status !== 'running') return;
|
|
224
|
+
try {
|
|
225
|
+
const output = normalizeJsonValue(event.output, this.core.toolOutputLimits);
|
|
226
|
+
if (!this.reserveCallbackCapacity(run, 1, jsonBytes(output) - (part.output === undefined ? 0 : jsonBytes(part.output)), 0)) return;
|
|
227
|
+
part.output = output;
|
|
228
|
+
this.core.emitPartEvent(run, 'part.updated', part);
|
|
229
|
+
} catch (error) {
|
|
230
|
+
this.failCallback(run, this.core.projectExternalError(run, error, 'toolCallback'));
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
public onRetry(state: IStorageState, sessionId: string, retry: plugins.IAgentRetryEvent): void {
|
|
235
|
+
const run = this.callbackRun(state, sessionId);
|
|
236
|
+
if (!run || !this.reserveCallbackCapacity(run, 1, 0, 0)) return;
|
|
237
|
+
this.core.emitEvent(run.scopeId, run.stored.session, {
|
|
238
|
+
type: 'run.retrying',
|
|
239
|
+
runId: run.runId,
|
|
240
|
+
attempt: retry.attempt,
|
|
241
|
+
maxAttempts: retry.maxAttempts,
|
|
242
|
+
delayMs: retry.delayMs,
|
|
243
|
+
reason: retry.reason,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Emits the usage of every model call and adds reported usage to its run. A run executes a
|
|
249
|
+
* transactional generation, and `AgentSession.generate()` settles a transactional generation only
|
|
250
|
+
* after it has reported each of its model calls, also when it is aborted. `executeRun()` awaits
|
|
251
|
+
* that settlement before it builds the terminal, so the run is still active for each of its
|
|
252
|
+
* events: they all reach `run.usage` and precede its `run.finished`. The events bypass the
|
|
253
|
+
* callback limits and a failed callback: their number is bounded by the run's model calls, and
|
|
254
|
+
* the usage of a run that exhausted its callback budget or failed in a callback must not be lost.
|
|
255
|
+
*/
|
|
256
|
+
public onModelCallUsage(
|
|
257
|
+
state: IStorageState,
|
|
258
|
+
stored: IStoredSessionState,
|
|
259
|
+
event: plugins.TAgentModelCallUsageEvent,
|
|
260
|
+
): void {
|
|
261
|
+
try {
|
|
262
|
+
const { source, generationId, ...call } = event;
|
|
263
|
+
const sessionId = stored.session.sessionId;
|
|
264
|
+
const activeRun = state.activeRuns.get(sessionId);
|
|
265
|
+
const run = generationId !== undefined && activeRun?.runId === generationId ? activeRun : undefined;
|
|
266
|
+
const operation = source === 'compaction' ? stored.metricsCompaction : run?.metricsOperation;
|
|
267
|
+
if (!operation) throw new Error('Model call usage has no owning metrics operation.');
|
|
268
|
+
observeSessionMetricsCall(operation, call);
|
|
269
|
+
let scopeId = run?.scopeId;
|
|
270
|
+
if (scopeId === undefined) {
|
|
271
|
+
const ambientContext = this.core.compaction.compactorInvocationContext.getStore();
|
|
272
|
+
const invocationContext = ambientContext
|
|
273
|
+
? this.core.compaction.deferredCompactorContexts.get(ambientContext) ?? ambientContext
|
|
274
|
+
: undefined;
|
|
275
|
+
if (
|
|
276
|
+
!invocationContext
|
|
277
|
+
|| invocationContext.storageKey !== state.storageKey
|
|
278
|
+
|| invocationContext.sessionId !== sessionId
|
|
279
|
+
) {
|
|
280
|
+
throw new Error('Model call usage is missing its exact FlexHarness invocation context.');
|
|
281
|
+
}
|
|
282
|
+
scopeId = invocationContext.scopeId;
|
|
283
|
+
}
|
|
284
|
+
if (run && call.status === 'reported') {
|
|
285
|
+
for (const key of Object.keys(run.usage) as (keyof IFlexUsage)[]) run.usage[key] += call.usage[key];
|
|
286
|
+
}
|
|
287
|
+
this.core.emitEvent(scopeId, stored.session, {
|
|
288
|
+
type: 'model.usage',
|
|
289
|
+
...(generationId === undefined ? {} : { runId: generationId }),
|
|
290
|
+
source,
|
|
291
|
+
...call,
|
|
292
|
+
});
|
|
293
|
+
} catch (error) {
|
|
294
|
+
// AgentSession isolates the entire callback, including event/context failures. Join it later.
|
|
295
|
+
stored.metricsFailure ??= error;
|
|
296
|
+
throw error;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
public async settleMetricsOperation(
|
|
301
|
+
state: IStorageState,
|
|
302
|
+
stored: IStoredSessionState,
|
|
303
|
+
operation: ISessionMetricsOperation,
|
|
304
|
+
): Promise<void> {
|
|
305
|
+
try {
|
|
306
|
+
if (stored.metricsFailure !== undefined) throw stored.metricsFailure;
|
|
307
|
+
await this.core.mutateProjection(state, stored, () => settleSessionMetricsOperation(stored.metrics, operation), true);
|
|
308
|
+
} catch (error) {
|
|
309
|
+
stored.metricsFailure ??= error;
|
|
310
|
+
// Never allow a later operation to overwrite a marker whose spend was not durably joined.
|
|
311
|
+
state.lifecycle = 'fenced';
|
|
312
|
+
throw error;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
private callbackRun(state: IStorageState, sessionId: string): IActiveRun | undefined {
|
|
317
|
+
const run = state.activeRuns.get(sessionId);
|
|
318
|
+
return !run || run.callbacksClosed || run.callbackError ? undefined : run;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
public reserveCallbackCapacity(
|
|
322
|
+
run: IActiveRun,
|
|
323
|
+
eventCount: number,
|
|
324
|
+
outputBytes: number,
|
|
325
|
+
partCount: number,
|
|
326
|
+
): boolean {
|
|
327
|
+
const nextEvents = run.callbackEventCount + eventCount;
|
|
328
|
+
const nextOutputBytes = run.callbackOutputBytes + outputBytes;
|
|
329
|
+
const nextParts = run.callbackParts.length + partCount;
|
|
330
|
+
if (
|
|
331
|
+
!Number.isSafeInteger(nextEvents)
|
|
332
|
+
|| !Number.isSafeInteger(nextOutputBytes)
|
|
333
|
+
|| !Number.isSafeInteger(nextParts)
|
|
334
|
+
|| nextEvents > this.core.callbackLimits.maxEvents
|
|
335
|
+
|| nextOutputBytes > this.core.callbackLimits.maxOutputBytes
|
|
336
|
+
|| nextParts > this.core.callbackLimits.maxParts
|
|
337
|
+
) {
|
|
338
|
+
this.failCallback(run, this.core.trustInternalError(new FlexHarnessCallbackOverflowError(
|
|
339
|
+
`Callback buffer exceeded its limit (${nextEvents} events, ${nextOutputBytes} bytes, ${nextParts} parts).`,
|
|
340
|
+
)));
|
|
341
|
+
return false;
|
|
342
|
+
}
|
|
343
|
+
run.callbackEventCount = nextEvents;
|
|
344
|
+
run.callbackOutputBytes = nextOutputBytes;
|
|
345
|
+
return true;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
private failCallback(run: IActiveRun, error: Error): void {
|
|
349
|
+
run.callbackError ??= error;
|
|
350
|
+
this.core.runs.abortRunInternally(run, error);
|
|
351
|
+
}
|
|
352
|
+
}
|