@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.
Files changed (69) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/classes.flexharness.admission.d.ts +17 -0
  3. package/dist_ts/classes.flexharness.admission.js +263 -0
  4. package/dist_ts/classes.flexharness.callbacks.d.ts +38 -0
  5. package/dist_ts/classes.flexharness.callbacks.js +313 -0
  6. package/dist_ts/classes.flexharness.compaction.d.ts +25 -0
  7. package/dist_ts/classes.flexharness.compaction.js +87 -0
  8. package/dist_ts/classes.flexharness.configurations.d.ts +11 -0
  9. package/dist_ts/classes.flexharness.configurations.js +137 -0
  10. package/dist_ts/classes.flexharness.core.d.ts +143 -0
  11. package/dist_ts/classes.flexharness.core.js +1538 -0
  12. package/dist_ts/classes.flexharness.d.ts +2 -437
  13. package/dist_ts/classes.flexharness.deletion.d.ts +31 -0
  14. package/dist_ts/classes.flexharness.deletion.js +396 -0
  15. package/dist_ts/classes.flexharness.imports.d.ts +24 -0
  16. package/dist_ts/classes.flexharness.imports.js +345 -0
  17. package/dist_ts/classes.flexharness.js +119 -9776
  18. package/dist_ts/classes.flexharness.lifecycle.d.ts +47 -0
  19. package/dist_ts/classes.flexharness.lifecycle.js +625 -0
  20. package/dist_ts/classes.flexharness.permissions.d.ts +19 -0
  21. package/dist_ts/classes.flexharness.permissions.js +450 -0
  22. package/dist_ts/classes.flexharness.projects.d.ts +55 -0
  23. package/dist_ts/classes.flexharness.projects.js +575 -0
  24. package/dist_ts/classes.flexharness.prompts.d.ts +44 -0
  25. package/dist_ts/classes.flexharness.prompts.js +659 -0
  26. package/dist_ts/classes.flexharness.providers.d.ts +18 -0
  27. package/dist_ts/classes.flexharness.providers.js +267 -0
  28. package/dist_ts/classes.flexharness.results.d.ts +26 -0
  29. package/dist_ts/classes.flexharness.results.js +89 -0
  30. package/dist_ts/classes.flexharness.reversions.d.ts +52 -0
  31. package/dist_ts/classes.flexharness.reversions.js +1306 -0
  32. package/dist_ts/classes.flexharness.runs.d.ts +63 -0
  33. package/dist_ts/classes.flexharness.runs.js +1058 -0
  34. package/dist_ts/classes.flexharness.sessions.d.ts +20 -0
  35. package/dist_ts/classes.flexharness.sessions.js +381 -0
  36. package/dist_ts/classes.flexharness.slashcommands.d.ts +40 -0
  37. package/dist_ts/classes.flexharness.slashcommands.js +458 -0
  38. package/dist_ts/classes.flexharness.subagents.d.ts +31 -0
  39. package/dist_ts/classes.flexharness.subagents.js +690 -0
  40. package/dist_ts/classes.flexharness.tooldispatch.d.ts +53 -0
  41. package/dist_ts/classes.flexharness.tooldispatch.js +418 -0
  42. package/dist_ts/interfaces.flexharness.d.ts +455 -0
  43. package/dist_ts/interfaces.flexharness.js +2 -0
  44. package/dist_ts/utils.flexharness.d.ts +19 -0
  45. package/dist_ts/utils.flexharness.js +83 -0
  46. package/package.json +3 -3
  47. package/ts/00_commitinfo_data.ts +1 -1
  48. package/ts/classes.flexharness.admission.ts +331 -0
  49. package/ts/classes.flexharness.callbacks.ts +352 -0
  50. package/ts/classes.flexharness.compaction.ts +167 -0
  51. package/ts/classes.flexharness.configurations.ts +167 -0
  52. package/ts/classes.flexharness.core.ts +1830 -0
  53. package/ts/classes.flexharness.deletion.ts +616 -0
  54. package/ts/classes.flexharness.imports.ts +414 -0
  55. package/ts/classes.flexharness.lifecycle.ts +786 -0
  56. package/ts/classes.flexharness.permissions.ts +535 -0
  57. package/ts/classes.flexharness.projects.ts +1006 -0
  58. package/ts/classes.flexharness.prompts.ts +857 -0
  59. package/ts/classes.flexharness.providers.ts +322 -0
  60. package/ts/classes.flexharness.results.ts +98 -0
  61. package/ts/classes.flexharness.reversions.ts +1684 -0
  62. package/ts/classes.flexharness.runs.ts +1204 -0
  63. package/ts/classes.flexharness.sessions.ts +483 -0
  64. package/ts/classes.flexharness.slashcommands.ts +705 -0
  65. package/ts/classes.flexharness.subagents.ts +860 -0
  66. package/ts/classes.flexharness.tooldispatch.ts +475 -0
  67. package/ts/classes.flexharness.ts +147 -12567
  68. package/ts/interfaces.flexharness.ts +517 -0
  69. 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
+ }