@modelprofile.com/flexharness 10.11.0 → 10.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.flexharness.d.ts +21 -2
- package/dist_ts/classes.flexharness.js +430 -63
- package/dist_ts/classes.toolcatalog.d.ts +80 -0
- package/dist_ts/classes.toolcatalog.js +1336 -0
- package/dist_ts/errors.d.ts +5 -0
- package/dist_ts/errors.js +10 -1
- package/dist_ts/index.d.ts +1 -0
- package/dist_ts/index.js +2 -1
- package/dist_ts/interfaces.d.ts +179 -2
- package/dist_ts/interfaces.js +29 -1
- package/dist_ts/plugins.d.ts +7 -3
- package/dist_ts/plugins.js +7 -3
- package/dist_ts/utils.harnessoptions.d.ts +2 -1
- package/dist_ts/utils.harnessoptions.js +19 -1
- package/dist_ts/utils.json.js +37 -2
- package/dist_ts/utils.schemadeclarations.d.ts +53 -0
- package/dist_ts/utils.schemadeclarations.js +315 -0
- package/dist_ts/utils.sessionmetrics.d.ts +17 -0
- package/dist_ts/utils.sessionmetrics.js +72 -0
- package/package.json +6 -5
- package/readme.md +58 -8
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.flexharness.ts +484 -58
- package/ts/classes.toolcatalog.ts +1431 -0
- package/ts/errors.ts +8 -0
- package/ts/index.ts +1 -0
- package/ts/interfaces.ts +198 -2
- package/ts/plugins.ts +10 -2
- package/ts/readme.md +58 -8
- package/ts/utils.harnessoptions.ts +22 -0
- package/ts/utils.json.ts +33 -1
- package/ts/utils.schemadeclarations.ts +322 -0
- package/ts/utils.sessionmetrics.ts +104 -0
- package/ts_chat_harness/readme.md +1 -1
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
FlexHarnessSlashCommandReentryError,
|
|
15
15
|
FlexHarnessSlashCommandUnavailableError,
|
|
16
16
|
FlexHarnessSteerRejectedError,
|
|
17
|
+
FlexHarnessStepLimitError,
|
|
17
18
|
FlexHarnessStoreConflictError,
|
|
18
19
|
FlexHarnessStoreCommitUncertainError,
|
|
19
20
|
FlexHarnessValidationError,
|
|
@@ -28,7 +29,13 @@ import {
|
|
|
28
29
|
FLEX_REVERSION_WORKSPACE_ID_MAX_BYTES,
|
|
29
30
|
FLEX_REVERSION_WORKSPACE_LABEL_MAX_BYTES,
|
|
30
31
|
FLEX_SESSION_IMPORT_MAX_DEPTH,
|
|
32
|
+
FLEX_TOOL_CATALOG_TOOL_NAMES,
|
|
31
33
|
} from './interfaces.js';
|
|
34
|
+
import {
|
|
35
|
+
FlexToolCatalog,
|
|
36
|
+
assertNoCatalogCollisions,
|
|
37
|
+
catalogToolInputSchema,
|
|
38
|
+
} from './classes.toolcatalog.js';
|
|
32
39
|
import type {
|
|
33
40
|
IFlexAgentContextCompactionOptions,
|
|
34
41
|
IFlexAgentContextInvocation,
|
|
@@ -95,6 +102,8 @@ import type {
|
|
|
95
102
|
IFlexSessionPermissionPolicyContext,
|
|
96
103
|
IFlexSessionActivity,
|
|
97
104
|
IFlexSessionGeneration,
|
|
105
|
+
IFlexSessionMetrics,
|
|
106
|
+
IFlexSessionMetricsState,
|
|
98
107
|
IFlexSessionGenerationCohortEntry,
|
|
99
108
|
IFlexSessionImportHandle,
|
|
100
109
|
IFlexSessionImportLimits,
|
|
@@ -109,6 +118,9 @@ import type {
|
|
|
109
118
|
IFlexSubagentDefinition,
|
|
110
119
|
IFlexSubagentProvenance,
|
|
111
120
|
IFlexTerminalProjection,
|
|
121
|
+
IFlexCatalogDescribeInput,
|
|
122
|
+
IFlexCatalogSearchInput,
|
|
123
|
+
IFlexToolCatalogOptions,
|
|
112
124
|
IFlexToolHandle,
|
|
113
125
|
IFlexToolMessagePart,
|
|
114
126
|
IFlexToolProviderContext,
|
|
@@ -165,6 +177,7 @@ import {
|
|
|
165
177
|
normalizeAgentSessionPolicy,
|
|
166
178
|
normalizeBuiltInTools,
|
|
167
179
|
normalizeSlashCommands,
|
|
180
|
+
normalizeToolCatalogOptions,
|
|
168
181
|
normalizeSubagents,
|
|
169
182
|
requireHarnessStores,
|
|
170
183
|
resolveBoundedPositiveInteger,
|
|
@@ -226,6 +239,15 @@ import {
|
|
|
226
239
|
validateUtf8String,
|
|
227
240
|
} from './utils.validation.js';
|
|
228
241
|
|
|
242
|
+
import {
|
|
243
|
+
createSessionMetricsOperation,
|
|
244
|
+
createSessionMetricsState,
|
|
245
|
+
observeSessionMetricsCall,
|
|
246
|
+
sessionMetricsView,
|
|
247
|
+
settleSessionMetricsOperation,
|
|
248
|
+
type ISessionMetricsOperation,
|
|
249
|
+
} from './utils.sessionmetrics.js';
|
|
250
|
+
|
|
229
251
|
type TCanonicalOutcome = 'accepted' | 'rejected' | 'interrupted';
|
|
230
252
|
type TRunPhase =
|
|
231
253
|
| 'admitting'
|
|
@@ -256,6 +278,10 @@ interface IStoredSessionState {
|
|
|
256
278
|
projectionReconciliationRequired: boolean;
|
|
257
279
|
projectionRevision: number;
|
|
258
280
|
projectionQueue: Promise<void>;
|
|
281
|
+
metrics: IFlexSessionMetricsState;
|
|
282
|
+
metricsCompaction?: ISessionMetricsOperation;
|
|
283
|
+
/** Retained independently of the error-neutral projectionQueue. */
|
|
284
|
+
metricsFailure?: unknown;
|
|
259
285
|
rememberedPermissionKeys: Set<string>;
|
|
260
286
|
permissionRevision: number;
|
|
261
287
|
permissionQueue: Promise<void>;
|
|
@@ -402,17 +428,23 @@ interface IActiveRun {
|
|
|
402
428
|
finishedEventEmitted: boolean;
|
|
403
429
|
/** The usage the provider reported for the run's model calls so far. */
|
|
404
430
|
usage: IFlexUsage;
|
|
431
|
+
metricsOperation?: ISessionMetricsOperation;
|
|
405
432
|
reasoningPartIds: Map<string, string>;
|
|
406
433
|
toolPartIds: Map<string, string>;
|
|
407
434
|
trustedToolExecutionErrors: Map<string, Error>;
|
|
408
435
|
subagentCallCount: number;
|
|
409
436
|
subagentSessionIds: Set<string>;
|
|
410
437
|
backgroundSubagents: Set<Promise<void>>;
|
|
438
|
+
cacheDispatch?: plugins.ISmartAiCacheDispatch;
|
|
411
439
|
pendingPermissionIds: Set<string>;
|
|
412
440
|
phase: TRunPhase;
|
|
413
441
|
reservedUserMessage?: IFlexMessage;
|
|
414
442
|
reservedAssistantMessage?: IFlexMessage;
|
|
415
443
|
modelResolution?: IFlexResolvedModel;
|
|
444
|
+
/** The tool-provider context of the run, passed to catalog methods. */
|
|
445
|
+
toolProviderContext?: Readonly<IFlexToolProviderContext<unknown>>;
|
|
446
|
+
/** The catalog methods the run offers through search_tools, describe_tools and execute. */
|
|
447
|
+
toolCatalog?: FlexToolCatalog<unknown>;
|
|
416
448
|
subagents: ReadonlyMap<string, Readonly<IFlexSubagentDefinition>>;
|
|
417
449
|
allowedTools?: readonly string[];
|
|
418
450
|
agentAllowedTools?: readonly string[];
|
|
@@ -466,6 +498,7 @@ interface IDelegatedRunAdmissionSeed {
|
|
|
466
498
|
readonly parentQueueId: string;
|
|
467
499
|
readonly parentRunId: string;
|
|
468
500
|
readonly parentToolCallId: string;
|
|
501
|
+
readonly configurationRef?: string;
|
|
469
502
|
readonly agent: string;
|
|
470
503
|
readonly depth: number;
|
|
471
504
|
}
|
|
@@ -672,7 +705,7 @@ const maxSubagentPromptBytes = 64 * 1024;
|
|
|
672
705
|
const maxSubagentTaskIdBytes = 512;
|
|
673
706
|
const maxSubagentPermissionMetadataBytes = 16 * 1024;
|
|
674
707
|
const maxSubagentResultTextBytes = 64 * 1024;
|
|
675
|
-
const defaultMaxSubagentDepth =
|
|
708
|
+
const defaultMaxSubagentDepth = 6;
|
|
676
709
|
const maximumMaxSubagentDepth = 8;
|
|
677
710
|
const defaultMaxSubagentCallsPerRun = 32;
|
|
678
711
|
const maximumMaxSubagentCallsPerRun = 128;
|
|
@@ -832,6 +865,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
832
865
|
private readonly externalErrorProjector?: TFlexExternalErrorProjector;
|
|
833
866
|
private readonly subagents: ReadonlyMap<string, Readonly<IFlexSubagentDefinition>>;
|
|
834
867
|
private readonly builtInTools: INormalizedBuiltInTools;
|
|
868
|
+
private readonly toolCatalogOptions: Readonly<Required<IFlexToolCatalogOptions>>;
|
|
835
869
|
private readonly slashCommands: ReadonlyMap<string, TRegisteredSlashCommand<TScope>>;
|
|
836
870
|
private readonly maxSubagentDepth: number;
|
|
837
871
|
private readonly maxSubagentCallsPerRun: number;
|
|
@@ -918,6 +952,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
918
952
|
this.externalErrorProjector = options.externalErrorProjector;
|
|
919
953
|
this.subagents = normalizeSubagents(options.subagents);
|
|
920
954
|
this.builtInTools = normalizeBuiltInTools(options.builtInTools);
|
|
955
|
+
this.toolCatalogOptions = normalizeToolCatalogOptions(options.toolCatalog);
|
|
921
956
|
this.slashCommands = normalizeSlashCommands(options.slashCommands);
|
|
922
957
|
this.maxSubagentDepth = resolveBoundedPositiveInteger(
|
|
923
958
|
options.maxSubagentDepth,
|
|
@@ -1029,6 +1064,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1029
1064
|
scopeId,
|
|
1030
1065
|
resolved.scope.scope,
|
|
1031
1066
|
placeholder.compactorLifecycleController,
|
|
1067
|
+
true,
|
|
1032
1068
|
);
|
|
1033
1069
|
const loaded = await initialization;
|
|
1034
1070
|
if (resolved.state.sessions.get(sessionId) !== placeholder) {
|
|
@@ -1314,6 +1350,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1314
1350
|
await this.stores.projections.save(state.storageKey, sessionId, {
|
|
1315
1351
|
schemaVersion: 3,
|
|
1316
1352
|
revision: 1,
|
|
1353
|
+
metrics: createSessionMetricsState(false),
|
|
1317
1354
|
messages: staged.runs.flatMap((run) => [run.user, run.assistant]),
|
|
1318
1355
|
stagedTerminals: [],
|
|
1319
1356
|
reversionSegments: [],
|
|
@@ -1390,6 +1427,16 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1390
1427
|
return publicSnapshot(this.requireSession(state, sessionId).session);
|
|
1391
1428
|
}
|
|
1392
1429
|
|
|
1430
|
+
/** Bounded projection-state read: no message, event, archive or child-history scan. */
|
|
1431
|
+
public async getSessionMetrics(scopeId: string, sessionId: string): Promise<IFlexSessionMetrics> {
|
|
1432
|
+
const { state } = await this.resolveState(scopeId);
|
|
1433
|
+
const stored = this.requireSession(state, sessionId);
|
|
1434
|
+
await stored.projectionQueue;
|
|
1435
|
+
this.assertStateAcceptingWork(state);
|
|
1436
|
+
if (stored.metricsFailure !== undefined) throw stored.metricsFailure;
|
|
1437
|
+
return publicSnapshot(sessionMetricsView(stored.metrics));
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1393
1440
|
public async getSessionReversionInfo(
|
|
1394
1441
|
scopeId: string,
|
|
1395
1442
|
sessionId: string,
|
|
@@ -3014,13 +3061,53 @@ export class FlexHarness<TScope = unknown> {
|
|
|
3014
3061
|
}
|
|
3015
3062
|
throw new FlexHarnessNotFoundError('Prompt queue entry', queueId);
|
|
3016
3063
|
}
|
|
3064
|
+
return this.admitRunSteer(run, steer, prompt, byteSize);
|
|
3065
|
+
}
|
|
3066
|
+
|
|
3067
|
+
/** Sends follow-up input only to an exact running direct child owned by this parent run. */
|
|
3068
|
+
public async sendSubagentInput(
|
|
3069
|
+
scopeId: string,
|
|
3070
|
+
parentSessionId: string,
|
|
3071
|
+
parentRunId: string,
|
|
3072
|
+
taskId: string,
|
|
3073
|
+
steer: IFlexSteerInput,
|
|
3074
|
+
): Promise<IFlexSteerAdmission> {
|
|
3075
|
+
validateIdentifier(parentSessionId, 'parentSessionId');
|
|
3076
|
+
requireTransferIdentifier(parentSessionId, 'parentSessionId');
|
|
3077
|
+
validateIdentifier(parentRunId, 'parentRunId');
|
|
3078
|
+
requireTransferIdentifier(parentRunId, 'parentRunId');
|
|
3079
|
+
this.assertOpen();
|
|
3080
|
+
const { state } = await this.resolveState(scopeId);
|
|
3081
|
+
const parent = state.activeRuns.get(parentSessionId);
|
|
3082
|
+
if (!parent || parent.runId !== parentRunId) {
|
|
3083
|
+
throw new FlexHarnessValidationError('The exact parent run is not active.');
|
|
3084
|
+
}
|
|
3085
|
+
return this.sendSubagentInputInRun(parent, taskId, steer);
|
|
3086
|
+
}
|
|
3087
|
+
|
|
3088
|
+
/** The shared synchronous admission point; neither entrypoint starts or resumes a run. */
|
|
3089
|
+
private admitRunSteer(
|
|
3090
|
+
run: IActiveRun,
|
|
3091
|
+
steer: IFlexSteerInput,
|
|
3092
|
+
prompt: INormalizedFlexPrompt,
|
|
3093
|
+
byteSize: number,
|
|
3094
|
+
): IFlexSteerAdmission {
|
|
3095
|
+
if (
|
|
3096
|
+
run.phase === 'closing'
|
|
3097
|
+
|| run.phase === 'finalizing'
|
|
3098
|
+
|| run.phase === 'promoting'
|
|
3099
|
+
|| run.callbacksClosed
|
|
3100
|
+
|| run.controller.signal.aborted
|
|
3101
|
+
|| run.stop.signal.aborted
|
|
3102
|
+
|| run.state.activeRuns.get(run.sessionId) !== run
|
|
3103
|
+
) throw new FlexHarnessSteerRejectedError('not-running', steer.steerId);
|
|
3017
3104
|
const steering = run.steering;
|
|
3018
3105
|
if (!steering.input.closed && !steering.accepted.has(steer.steerId)) {
|
|
3019
3106
|
if (
|
|
3020
3107
|
steering.accepted.size - steering.applied.size
|
|
3021
3108
|
>= this.promptQueueLimits.maxOutstandingPromptsPerSession
|
|
3022
3109
|
) {
|
|
3023
|
-
throw new FlexHarnessQueueFullError(`Session "${sessionId}" has reached its pending steer limit.`);
|
|
3110
|
+
throw new FlexHarnessQueueFullError(`Session "${run.sessionId}" has reached its pending steer limit.`);
|
|
3024
3111
|
}
|
|
3025
3112
|
// Pending steers hold outstanding prompt bytes of the session until they are taken in.
|
|
3026
3113
|
if (
|
|
@@ -3028,7 +3115,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
3028
3115
|
> this.promptQueueLimits.maxOutstandingBytesPerSession
|
|
3029
3116
|
) {
|
|
3030
3117
|
throw new FlexHarnessQueueFullError(
|
|
3031
|
-
`Session "${sessionId}" has reached its outstanding prompt byte limit.`,
|
|
3118
|
+
`Session "${run.sessionId}" has reached its outstanding prompt byte limit.`,
|
|
3032
3119
|
);
|
|
3033
3120
|
}
|
|
3034
3121
|
}
|
|
@@ -3052,11 +3139,130 @@ export class FlexHarness<TScope = unknown> {
|
|
|
3052
3139
|
steering.stored.outstandingPromptBytes += byteSize;
|
|
3053
3140
|
this.emitEvent(run.scopeId, run.stored.session, {
|
|
3054
3141
|
type: 'prompt.steer.accepted',
|
|
3055
|
-
queueId,
|
|
3142
|
+
queueId: run.queueId,
|
|
3056
3143
|
runId: run.runId,
|
|
3057
3144
|
steerId: steer.steerId,
|
|
3058
3145
|
});
|
|
3059
|
-
return { steerId: steer.steerId, queueId, runId: run.runId };
|
|
3146
|
+
return { steerId: steer.steerId, queueId: run.queueId, runId: run.runId };
|
|
3147
|
+
}
|
|
3148
|
+
|
|
3149
|
+
private sendSubagentInputInRun(
|
|
3150
|
+
parent: IActiveRun,
|
|
3151
|
+
taskId: string,
|
|
3152
|
+
steer: IFlexSteerInput,
|
|
3153
|
+
): IFlexSteerAdmission {
|
|
3154
|
+
this.assertOpen();
|
|
3155
|
+
validateUtf8String(taskId, 'taskId', maxSubagentTaskIdBytes, true);
|
|
3156
|
+
validateIdentifier(steer.steerId, 'steerId');
|
|
3157
|
+
requireTransferIdentifier(steer.steerId, 'steerId');
|
|
3158
|
+
const prompt = normalizeFlexPrompt(steer.prompt);
|
|
3159
|
+
const byteSize = this.promptAdmissionByteSize(steer.prompt, {});
|
|
3160
|
+
this.assertStateAcceptingWork(parent.state);
|
|
3161
|
+
const child = parent.state.activeRuns.get(taskId);
|
|
3162
|
+
if (!child) throw new FlexHarnessSteerRejectedError('not-running', steer.steerId);
|
|
3163
|
+
this.assertLiveSubagentOwnership(child, parent);
|
|
3164
|
+
return this.admitRunSteer(child, steer, prompt, byteSize);
|
|
3165
|
+
}
|
|
3166
|
+
|
|
3167
|
+
/** Fences live ownership by exact runtime pointers and admission seeds all the way to root. */
|
|
3168
|
+
private assertLiveSubagentOwnership(child: IActiveRun, parent: IActiveRun): void {
|
|
3169
|
+
const state = parent.state;
|
|
3170
|
+
const visited = new Set<IActiveRun>();
|
|
3171
|
+
let current = child;
|
|
3172
|
+
let expectedParent: IActiveRun | undefined = parent;
|
|
3173
|
+
while (true) {
|
|
3174
|
+
const stored = current.stored;
|
|
3175
|
+
const session = stored.session;
|
|
3176
|
+
const queued = stored.outstandingPromptsById.get(current.queueId);
|
|
3177
|
+
if (
|
|
3178
|
+
visited.has(current)
|
|
3179
|
+
|| current.state !== state
|
|
3180
|
+
|| state.activeRuns.get(current.sessionId) !== current
|
|
3181
|
+
|| state.sessions.get(current.sessionId) !== stored
|
|
3182
|
+
|| session.sessionId !== current.sessionId
|
|
3183
|
+
|| state.tombstones.has(current.sessionId)
|
|
3184
|
+
|| state.initializingSessions.has(current.sessionId)
|
|
3185
|
+
|| current.phase !== 'running'
|
|
3186
|
+
|| current.callbacksClosed
|
|
3187
|
+
|| current.controller.signal.aborted
|
|
3188
|
+
|| current.stop.signal.aborted
|
|
3189
|
+
|| current.steering.input.closed
|
|
3190
|
+
|| !queued
|
|
3191
|
+
|| queued.state !== state
|
|
3192
|
+
|| queued.stored !== stored
|
|
3193
|
+
|| queued.sessionId !== current.sessionId
|
|
3194
|
+
|| queued.queueId !== current.queueId
|
|
3195
|
+
|| queued.runId !== current.runId
|
|
3196
|
+
|| queued.steering !== current.steering
|
|
3197
|
+
) throw new FlexHarnessValidationError('The subagent or ancestor run is no longer live and owned.');
|
|
3198
|
+
this.assertSessionWritable(stored);
|
|
3199
|
+
visited.add(current);
|
|
3200
|
+
if (session.parentSessionId === undefined) {
|
|
3201
|
+
if (expectedParent !== undefined || queued.delegationSeed !== undefined) {
|
|
3202
|
+
throw new FlexHarnessValidationError('The task is not a direct child of this parent run.');
|
|
3203
|
+
}
|
|
3204
|
+
return;
|
|
3205
|
+
}
|
|
3206
|
+
const seed = queued.delegationSeed;
|
|
3207
|
+
if (!seed) throw new FlexHarnessValidationError('The child has no live delegated admission owner.');
|
|
3208
|
+
const owner = current.delegatedRunAdmissionOwner;
|
|
3209
|
+
const owningParent = seed.parentRun;
|
|
3210
|
+
const parentSession = owningParent.stored.session;
|
|
3211
|
+
if (
|
|
3212
|
+
(expectedParent !== undefined && owningParent !== expectedParent)
|
|
3213
|
+
|| seed.state !== state
|
|
3214
|
+
|| seed.storageKey !== state.storageKey
|
|
3215
|
+
|| seed.childStored !== stored
|
|
3216
|
+
|| stored.storageKey !== seed.storageKey
|
|
3217
|
+
|| seed.sessionId !== current.sessionId
|
|
3218
|
+
|| session.sessionId !== seed.sessionId
|
|
3219
|
+
|| session.sessionGenerationId !== seed.sessionGenerationId
|
|
3220
|
+
|| session.sessionGenerationSequence !== seed.sessionGenerationSequence
|
|
3221
|
+
|| session.parentSessionId !== seed.parentSessionId
|
|
3222
|
+
|| session.parentRunId !== seed.originParentRunId
|
|
3223
|
+
|| session.parentToolCallId !== seed.originParentToolCallId
|
|
3224
|
+
|| session.agent !== seed.agent
|
|
3225
|
+
|| session.depth !== seed.depth
|
|
3226
|
+
|| session.configurationRef !== seed.configurationRef
|
|
3227
|
+
|| parentSession.configurationRef !== seed.configurationRef
|
|
3228
|
+
|| seed.depth !== (parentSession.depth ?? 0) + 1
|
|
3229
|
+
|| !owningParent.subagents.has(seed.agent)
|
|
3230
|
+
|| seed.parentStored !== owningParent.stored
|
|
3231
|
+
|| seed.parentStored.storageKey !== seed.storageKey
|
|
3232
|
+
|| owningParent.sessionId !== seed.parentSessionId
|
|
3233
|
+
|| owningParent.runId !== seed.parentRunId
|
|
3234
|
+
|| owningParent.queueId !== seed.parentQueueId
|
|
3235
|
+
|| parentSession.sessionGenerationId !== seed.parentSessionGenerationId
|
|
3236
|
+
|| parentSession.sessionGenerationSequence !== seed.parentSessionGenerationSequence
|
|
3237
|
+
|| current.scopeId !== seed.scopeId
|
|
3238
|
+
|| current.scope !== seed.scope
|
|
3239
|
+
|| owningParent.scopeId !== seed.scopeId
|
|
3240
|
+
|| owningParent.scope !== seed.scope
|
|
3241
|
+
|| (this.delegatedRunAdmissionProvider !== undefined && owner === undefined)
|
|
3242
|
+
|| (owner !== undefined && (
|
|
3243
|
+
owner.storageState !== state || owner.run !== current
|
|
3244
|
+
|| !state.delegatedRunAdmissionOwners.has(owner)
|
|
3245
|
+
|| owner.status !== 'acquired' || !owner.acquisitionSettled || owner.acquisitionDetached
|
|
3246
|
+
|| owner.context.runId !== current.runId || owner.context.queueId !== current.queueId
|
|
3247
|
+
|| owner.context.parentRunId !== owningParent.runId
|
|
3248
|
+
|| owner.context.parentQueueId !== owningParent.queueId
|
|
3249
|
+
|| owner.context.scopeId !== seed.scopeId || owner.context.scope !== seed.scope
|
|
3250
|
+
|| owner.context.storageKey !== seed.storageKey
|
|
3251
|
+
|| owner.context.sessionId !== seed.sessionId
|
|
3252
|
+
|| owner.context.parentSessionId !== seed.parentSessionId
|
|
3253
|
+
|| owner.context.parentToolCallId !== seed.parentToolCallId
|
|
3254
|
+
|| owner.context.originParentRunId !== seed.originParentRunId
|
|
3255
|
+
|| owner.context.originParentToolCallId !== seed.originParentToolCallId
|
|
3256
|
+
|| owner.context.agent !== seed.agent || owner.context.depth !== seed.depth
|
|
3257
|
+
|| owner.context.sessionGenerationId !== seed.sessionGenerationId
|
|
3258
|
+
|| owner.context.sessionGenerationSequence !== seed.sessionGenerationSequence
|
|
3259
|
+
|| owner.context.parentSessionGenerationId !== seed.parentSessionGenerationId
|
|
3260
|
+
|| owner.context.parentSessionGenerationSequence !== seed.parentSessionGenerationSequence
|
|
3261
|
+
))
|
|
3262
|
+
) throw new FlexHarnessValidationError('The task is not owned by this exact parent run and configuration.');
|
|
3263
|
+
current = owningParent;
|
|
3264
|
+
expectedParent = undefined;
|
|
3265
|
+
}
|
|
3060
3266
|
}
|
|
3061
3267
|
|
|
3062
3268
|
public async abort(scopeId: string, sessionId: string): Promise<boolean> {
|
|
@@ -4532,7 +4738,12 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4532
4738
|
this.assertPromptPromotion(queued, run);
|
|
4533
4739
|
const reservation = this.createReservation(run, prompt);
|
|
4534
4740
|
closeReservationPublication = this.openPublication(run.state, run.sessionId);
|
|
4741
|
+
run.metricsOperation = createSessionMetricsOperation(run.runId, 'generation');
|
|
4535
4742
|
await this.mutateProjection(run.state, run.stored, () => {
|
|
4743
|
+
if (run.stored.metrics.pendingGenerationId !== undefined) {
|
|
4744
|
+
throw new Error('Session metrics already owns a generation.');
|
|
4745
|
+
}
|
|
4746
|
+
run.stored.metrics.pendingGenerationId = run.runId;
|
|
4536
4747
|
run.stored.messages.push(reservation.userMessage, reservation.assistantMessage);
|
|
4537
4748
|
if (run.stored.session.agent === undefined) {
|
|
4538
4749
|
this.pruneReversionState(run.stored, true);
|
|
@@ -4650,6 +4861,8 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4650
4861
|
prepare: (context: { generationId: string; abortSignal: AbortSignal }) =>
|
|
4651
4862
|
this.prepareGeneration(run, options, context.abortSignal),
|
|
4652
4863
|
pendingInput: run.steering.input,
|
|
4864
|
+
activateOnBackgroundJobs: true,
|
|
4865
|
+
onCacheDispatch: (dispatch: plugins.ISmartAiCacheDispatch) => { run.cacheDispatch = dispatch; },
|
|
4653
4866
|
onInputApplied: (input: plugins.IAgentAppliedInput) => this.applyRunSteer(run, input),
|
|
4654
4867
|
beforeFinish: (context: { abortSignal: AbortSignal }) =>
|
|
4655
4868
|
this.awaitNextBackgroundSubagent(run, context.abortSignal),
|
|
@@ -4803,7 +5016,8 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4803
5016
|
/**
|
|
4804
5017
|
* Holds the end of a run's generation until one more of its background subagents settles. The
|
|
4805
5018
|
* subagent commits its result to the generation before it settles, so the model takes the result
|
|
4806
|
-
* in with another step instead of the run ending without it.
|
|
5019
|
+
* in with another step instead of the run ending without it. The engine bounds the scoped
|
|
5020
|
+
* wait from its observed dispatch and commits timer check-ins. Returns at once when none runs.
|
|
4807
5021
|
*/
|
|
4808
5022
|
private async awaitNextBackgroundSubagent(run: IActiveRun, signal: AbortSignal): Promise<void> {
|
|
4809
5023
|
if (run.backgroundSubagents.size === 0) return;
|
|
@@ -4828,8 +5042,16 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4828
5042
|
try {
|
|
4829
5043
|
// The transactional generation settles only after its model calls are reported, so
|
|
4830
5044
|
// `run.usage` is complete once it settles, also for a cancelled run.
|
|
4831
|
-
|
|
4832
|
-
|
|
5045
|
+
let rawResult: plugins.IAgentGenerateResult;
|
|
5046
|
+
try {
|
|
5047
|
+
rawResult = await generation;
|
|
5048
|
+
generated = true;
|
|
5049
|
+
} finally {
|
|
5050
|
+
await this.settleMetricsOperation(run.state, run.stored, run.metricsOperation!);
|
|
5051
|
+
}
|
|
5052
|
+
if (rawResult.terminationReason === 'step-limit') {
|
|
5053
|
+
throw this.trustInternalError(new FlexHarnessStepLimitError(rawResult.steps));
|
|
5054
|
+
}
|
|
4833
5055
|
// Background tools do not block inference, but the owning turn joins its admitted children
|
|
4834
5056
|
// before releasing its authority or committing its terminal projection.
|
|
4835
5057
|
await Promise.all([...run.backgroundSubagents]);
|
|
@@ -5617,18 +5839,37 @@ export class FlexHarness<TScope = unknown> {
|
|
|
5617
5839
|
}
|
|
5618
5840
|
signal.throwIfAborted();
|
|
5619
5841
|
run.modelResolution = model!;
|
|
5842
|
+
run.toolProviderContext = toolProviderContext as Readonly<IFlexToolProviderContext<unknown>>;
|
|
5843
|
+
let catalog: FlexToolCatalog<unknown> | undefined;
|
|
5844
|
+
if (toolHandle?.catalog) {
|
|
5845
|
+
try {
|
|
5846
|
+
catalog = await FlexToolCatalog.create(toolHandle.catalog, this.toolCatalogOptions);
|
|
5847
|
+
} catch (error) {
|
|
5848
|
+
throw this.projectExternalError(run, error, 'toolProvider');
|
|
5849
|
+
}
|
|
5850
|
+
signal.throwIfAborted();
|
|
5851
|
+
}
|
|
5620
5852
|
let tools: TFlexAgentToolSet | undefined;
|
|
5621
5853
|
try {
|
|
5622
5854
|
const providedTools = toolHandle?.tools;
|
|
5623
5855
|
const combinedTools: Record<string, unknown> = { ...(providedTools ?? {}) };
|
|
5624
5856
|
Object.assign(combinedTools, this.createBuiltInTools(run));
|
|
5857
|
+
if (catalog) assertNoCatalogCollisions(catalog, combinedTools);
|
|
5625
5858
|
const globalNames = run.allowedTools === undefined ? undefined : new Set(run.allowedTools);
|
|
5626
5859
|
const agentNames = run.agentAllowedTools === undefined ? undefined : new Set(run.agentAllowedTools);
|
|
5860
|
+
const admitted = (name: string): boolean =>
|
|
5861
|
+
(globalNames === undefined || globalNames.has(name))
|
|
5862
|
+
&& (agentNames === undefined || agentNames.has(name));
|
|
5627
5863
|
const admittedTools = globalNames === undefined && agentNames === undefined
|
|
5628
5864
|
? combinedTools
|
|
5629
|
-
: Object.fromEntries(Object.entries(combinedTools).filter(([name]) =>
|
|
5630
|
-
|
|
5631
|
-
|
|
5865
|
+
: Object.fromEntries(Object.entries(combinedTools).filter(([name]) => admitted(name)));
|
|
5866
|
+
const admittedCatalog = catalog && (globalNames === undefined && agentNames === undefined
|
|
5867
|
+
? catalog
|
|
5868
|
+
: catalog.restrict(catalog.methods.filter(admitted)));
|
|
5869
|
+
if (admittedCatalog && admittedCatalog.methods.length > 0) {
|
|
5870
|
+
run.toolCatalog = admittedCatalog;
|
|
5871
|
+
Object.assign(admittedTools, this.createCatalogTools(run, admittedCatalog));
|
|
5872
|
+
}
|
|
5632
5873
|
tools = Object.keys(admittedTools).length > 0
|
|
5633
5874
|
? wrapToolSet(
|
|
5634
5875
|
admittedTools as TFlexAgentToolSet,
|
|
@@ -5644,6 +5885,9 @@ export class FlexHarness<TScope = unknown> {
|
|
|
5644
5885
|
const close = toolHandle?.close
|
|
5645
5886
|
? async () => {
|
|
5646
5887
|
if (closed) return;
|
|
5888
|
+
// Inference has ended before its tool lease closes. Fence follow-up admission
|
|
5889
|
+
// synchronously, even when external cleanup has not settled yet.
|
|
5890
|
+
run.steering.input.close();
|
|
5647
5891
|
closePromise ??= Promise.resolve()
|
|
5648
5892
|
.then(() => toolHandle!.close!())
|
|
5649
5893
|
.then(() => {
|
|
@@ -5684,6 +5928,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
5684
5928
|
if (run.subagents.size > 0 && (run.stored.session.depth ?? 0) < this.maxSubagentDepth) {
|
|
5685
5929
|
names.add('delegate');
|
|
5686
5930
|
names.add('delegate_result');
|
|
5931
|
+
names.add('delegate_send');
|
|
5687
5932
|
}
|
|
5688
5933
|
if (this.builtInTools.renameSession) names.add('rename_session');
|
|
5689
5934
|
const projectManagement = this.builtInTools.projectManagement;
|
|
@@ -5703,11 +5948,65 @@ export class FlexHarness<TScope = unknown> {
|
|
|
5703
5948
|
}
|
|
5704
5949
|
}
|
|
5705
5950
|
|
|
5951
|
+
/**
|
|
5952
|
+
* search_tools, describe_tools and execute. `execute` validates the call and runs the method
|
|
5953
|
+
* inside this tool call, so the tool wrapper bounds its output and projects its errors, and the
|
|
5954
|
+
* call's part, events and permission requests carry the method's path.
|
|
5955
|
+
*/
|
|
5956
|
+
private createCatalogTools(run: IActiveRun, catalog: FlexToolCatalog<unknown>): Record<string, unknown> {
|
|
5957
|
+
const names = FLEX_TOOL_CATALOG_TOOL_NAMES;
|
|
5958
|
+
const definitions = catalog.toolDefinitions();
|
|
5959
|
+
const describeBytes = Math.max(1024, this.toolOutputLimits.maxBytes - 1024);
|
|
5960
|
+
return {
|
|
5961
|
+
[names.search]: plugins.tool({
|
|
5962
|
+
description: definitions[names.search].description,
|
|
5963
|
+
strict: false,
|
|
5964
|
+
inputSchema: catalogToolInputSchema<IFlexCatalogSearchInput>(definitions[names.search]),
|
|
5965
|
+
execute: async (input: IFlexCatalogSearchInput) => catalog.search(input),
|
|
5966
|
+
}),
|
|
5967
|
+
[names.describe]: plugins.tool({
|
|
5968
|
+
description: definitions[names.describe].description,
|
|
5969
|
+
strict: false,
|
|
5970
|
+
inputSchema: catalogToolInputSchema<IFlexCatalogDescribeInput>(definitions[names.describe]),
|
|
5971
|
+
execute: async (input: IFlexCatalogDescribeInput, options?: { messages?: plugins.ModelMessage[] }) =>
|
|
5972
|
+
catalog.describe(input, {
|
|
5973
|
+
declaredTypes: catalog.declaredTypesIn(options?.messages),
|
|
5974
|
+
maxBytes: describeBytes,
|
|
5975
|
+
}).output,
|
|
5976
|
+
}),
|
|
5977
|
+
[names.execute]: plugins.tool({
|
|
5978
|
+
description: definitions[names.execute].description,
|
|
5979
|
+
strict: false,
|
|
5980
|
+
inputSchema: catalogToolInputSchema<{ method: string; params?: Record<string, unknown> }>(
|
|
5981
|
+
definitions[names.execute],
|
|
5982
|
+
),
|
|
5983
|
+
execute: async (
|
|
5984
|
+
input: { method: string; params?: Record<string, unknown> },
|
|
5985
|
+
options?: { toolCallId?: string; abortSignal?: AbortSignal },
|
|
5986
|
+
) => {
|
|
5987
|
+
const call = await catalog.prepareCall(input.method, input.params);
|
|
5988
|
+
if (!call.success) {
|
|
5989
|
+
const error = this.trustInternalError(new FlexHarnessValidationError(JSON.stringify(call.error)));
|
|
5990
|
+
if (options?.toolCallId) run.trustedToolExecutionErrors.set(options.toolCallId, error);
|
|
5991
|
+
throw error;
|
|
5992
|
+
}
|
|
5993
|
+
return call.run({
|
|
5994
|
+
method: call.method,
|
|
5995
|
+
toolCallId: options?.toolCallId ?? '',
|
|
5996
|
+
abortSignal: options?.abortSignal ?? run.controller.signal,
|
|
5997
|
+
provider: run.toolProviderContext!,
|
|
5998
|
+
});
|
|
5999
|
+
},
|
|
6000
|
+
}),
|
|
6001
|
+
};
|
|
6002
|
+
}
|
|
6003
|
+
|
|
5706
6004
|
private createBuiltInTools(run: IActiveRun): Record<string, unknown> {
|
|
5707
6005
|
const tools: Record<string, unknown> = {};
|
|
5708
6006
|
if (run.subagents.size > 0 && (run.stored.session.depth ?? 0) < this.maxSubagentDepth) {
|
|
5709
6007
|
tools.delegate = this.createSubagentTool(run);
|
|
5710
6008
|
tools.delegate_result = this.createSubagentResultTool(run);
|
|
6009
|
+
tools.delegate_send = this.createSubagentSendTool(run);
|
|
5711
6010
|
}
|
|
5712
6011
|
if (this.builtInTools.renameSession) tools.rename_session = this.createRenameSessionTool(run);
|
|
5713
6012
|
const projectManagement = this.builtInTools.projectManagement;
|
|
@@ -6253,9 +6552,33 @@ export class FlexHarness<TScope = unknown> {
|
|
|
6253
6552
|
}
|
|
6254
6553
|
}
|
|
6255
6554
|
|
|
6555
|
+
private createSubagentSendTool(run: IActiveRun): unknown {
|
|
6556
|
+
return plugins.tool({
|
|
6557
|
+
description: 'Send follow-up instructions to a RUNNING direct child owned by this exact parent turn. The child takes the message at its next inference boundary; running tools and permission waits are not interrupted. Never starts or resumes a settled child. Use delegate_result for progress and results.',
|
|
6558
|
+
inputSchema: plugins.z.object({
|
|
6559
|
+
taskId: plugins.z.string().min(1).max(maxSubagentTaskIdBytes),
|
|
6560
|
+
prompt: plugins.z.string().min(1).max(maxSubagentPromptBytes),
|
|
6561
|
+
}).strict(),
|
|
6562
|
+
execute: async (
|
|
6563
|
+
{ taskId, prompt }: { taskId: string; prompt: string },
|
|
6564
|
+
options?: { toolCallId?: string },
|
|
6565
|
+
) => {
|
|
6566
|
+
validateUtf8String(prompt, 'delegate_send prompt', maxSubagentPromptBytes, true);
|
|
6567
|
+
const toolCallId = options?.toolCallId;
|
|
6568
|
+
validateUtf8String(toolCallId, 'delegate_send toolCallId', maxTransferIdentifierBytes, true);
|
|
6569
|
+
return this.sendSubagentInputInRun(run, taskId, {
|
|
6570
|
+
steerId: `steer_${sha256Hex(JSON.stringify([
|
|
6571
|
+
'flexharness-subagent-input-v1', run.state.storageKey, run.sessionId, run.runId, toolCallId,
|
|
6572
|
+
]))}`,
|
|
6573
|
+
prompt,
|
|
6574
|
+
});
|
|
6575
|
+
},
|
|
6576
|
+
});
|
|
6577
|
+
}
|
|
6578
|
+
|
|
6256
6579
|
private createSubagentResultTool(run: IActiveRun): unknown {
|
|
6257
6580
|
return plugins.tool({
|
|
6258
|
-
description: 'Read a subagent owned by this parent. Returns live status and the final text when settled. waitMs optionally waits for completion, bounded to 240 seconds; omit it to continue immediately.',
|
|
6581
|
+
description: 'Read a subagent owned by this parent. Returns live status and the final text when settled. waitMs optionally waits for completion, bounded to 240 seconds and the remaining configured cache idle budget; omit it to continue immediately.',
|
|
6259
6582
|
inputSchema: plugins.z.object({
|
|
6260
6583
|
taskId: plugins.z.string().min(1).max(maxSubagentTaskIdBytes),
|
|
6261
6584
|
waitMs: plugins.z.number().int().min(0).max(240_000).optional(),
|
|
@@ -6274,7 +6597,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
6274
6597
|
await Promise.race([
|
|
6275
6598
|
active.completion.catch(() => undefined),
|
|
6276
6599
|
new Promise<void>((resolve, reject) => {
|
|
6277
|
-
timer = setTimeout(resolve, waitMs);
|
|
6600
|
+
timer = setTimeout(resolve, plugins.getSmartAiCacheWaitMs(waitMs, run.cacheDispatch));
|
|
6278
6601
|
onAbort = () => reject(run.controller.signal.reason);
|
|
6279
6602
|
run.controller.signal.addEventListener('abort', onAbort, { once: true });
|
|
6280
6603
|
if (run.controller.signal.aborted) onAbort();
|
|
@@ -6328,6 +6651,9 @@ export class FlexHarness<TScope = unknown> {
|
|
|
6328
6651
|
parentQueueId: run.queueId,
|
|
6329
6652
|
parentRunId: run.runId,
|
|
6330
6653
|
parentToolCallId: toolCallId,
|
|
6654
|
+
...(run.stored.session.configurationRef === undefined
|
|
6655
|
+
? {}
|
|
6656
|
+
: { configurationRef: run.stored.session.configurationRef }),
|
|
6331
6657
|
agent: definition.name,
|
|
6332
6658
|
depth: (run.stored.session.depth ?? 0) + 1,
|
|
6333
6659
|
});
|
|
@@ -6467,6 +6793,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
6467
6793
|
run.scopeId,
|
|
6468
6794
|
run.scope as TScope,
|
|
6469
6795
|
placeholder!.compactorLifecycleController,
|
|
6796
|
+
true,
|
|
6470
6797
|
);
|
|
6471
6798
|
const parentLostOwnership = run.controller.signal.aborted
|
|
6472
6799
|
|| state.sessions.get(run.sessionId) !== run.stored
|
|
@@ -8183,13 +8510,17 @@ export class FlexHarness<TScope = unknown> {
|
|
|
8183
8510
|
const run = this.callbackRun(state, sessionId);
|
|
8184
8511
|
if (!run || run.toolPartIds.has(event.toolCallId)) return;
|
|
8185
8512
|
try {
|
|
8186
|
-
|
|
8513
|
+
// An execute call is the call of the catalog method it names.
|
|
8514
|
+
const target = event.toolName === FLEX_TOOL_CATALOG_TOOL_NAMES.execute
|
|
8515
|
+
? run.toolCatalog?.callTarget(event.input)
|
|
8516
|
+
: undefined;
|
|
8517
|
+
const input = normalizeJsonValue(target ? target.params : event.input, this.toolOutputLimits);
|
|
8187
8518
|
if (!this.reserveCallbackCapacity(run, 1, jsonBytes(input), 1)) return;
|
|
8188
8519
|
const part: IFlexToolMessagePart = {
|
|
8189
8520
|
partId: plugins.crypto.randomUUID(),
|
|
8190
8521
|
type: 'tool',
|
|
8191
8522
|
toolCallId: this.boundedIdentifier(event.toolCallId, 'toolCallId'),
|
|
8192
|
-
toolName: this.boundedIdentifier(event.toolName, 'toolName'),
|
|
8523
|
+
toolName: this.boundedIdentifier(target ? target.method : event.toolName, 'toolName'),
|
|
8193
8524
|
status: 'running',
|
|
8194
8525
|
input,
|
|
8195
8526
|
startedAt: new Date().toISOString(),
|
|
@@ -8289,34 +8620,59 @@ export class FlexHarness<TScope = unknown> {
|
|
|
8289
8620
|
stored: IStoredSessionState,
|
|
8290
8621
|
event: plugins.TAgentModelCallUsageEvent,
|
|
8291
8622
|
): void {
|
|
8292
|
-
|
|
8293
|
-
|
|
8294
|
-
|
|
8295
|
-
|
|
8296
|
-
|
|
8297
|
-
|
|
8298
|
-
|
|
8299
|
-
|
|
8300
|
-
|
|
8301
|
-
|
|
8302
|
-
|
|
8303
|
-
|
|
8304
|
-
|
|
8305
|
-
|
|
8306
|
-
|
|
8307
|
-
|
|
8623
|
+
try {
|
|
8624
|
+
const { source, generationId, ...call } = event;
|
|
8625
|
+
const sessionId = stored.session.sessionId;
|
|
8626
|
+
const activeRun = state.activeRuns.get(sessionId);
|
|
8627
|
+
const run = generationId !== undefined && activeRun?.runId === generationId ? activeRun : undefined;
|
|
8628
|
+
const operation = source === 'compaction' ? stored.metricsCompaction : run?.metricsOperation;
|
|
8629
|
+
if (!operation) throw new Error('Model call usage has no owning metrics operation.');
|
|
8630
|
+
observeSessionMetricsCall(operation, call);
|
|
8631
|
+
let scopeId = run?.scopeId;
|
|
8632
|
+
if (scopeId === undefined) {
|
|
8633
|
+
const ambientContext = this.compactorInvocationContext.getStore();
|
|
8634
|
+
const invocationContext = ambientContext
|
|
8635
|
+
? this.deferredCompactorContexts.get(ambientContext) ?? ambientContext
|
|
8636
|
+
: undefined;
|
|
8637
|
+
if (
|
|
8638
|
+
!invocationContext
|
|
8639
|
+
|| invocationContext.storageKey !== state.storageKey
|
|
8640
|
+
|| invocationContext.sessionId !== sessionId
|
|
8641
|
+
) {
|
|
8642
|
+
throw new Error('Model call usage is missing its exact FlexHarness invocation context.');
|
|
8643
|
+
}
|
|
8644
|
+
scopeId = invocationContext.scopeId;
|
|
8645
|
+
}
|
|
8646
|
+
if (run && call.status === 'reported') {
|
|
8647
|
+
for (const key of Object.keys(run.usage) as (keyof IFlexUsage)[]) run.usage[key] += call.usage[key];
|
|
8308
8648
|
}
|
|
8309
|
-
scopeId
|
|
8649
|
+
this.emitEvent(scopeId, stored.session, {
|
|
8650
|
+
type: 'model.usage',
|
|
8651
|
+
...(generationId === undefined ? {} : { runId: generationId }),
|
|
8652
|
+
source,
|
|
8653
|
+
...call,
|
|
8654
|
+
});
|
|
8655
|
+
} catch (error) {
|
|
8656
|
+
// AgentSession isolates the entire callback, including event/context failures. Join it later.
|
|
8657
|
+
stored.metricsFailure ??= error;
|
|
8658
|
+
throw error;
|
|
8310
8659
|
}
|
|
8311
|
-
|
|
8312
|
-
|
|
8660
|
+
}
|
|
8661
|
+
|
|
8662
|
+
private async settleMetricsOperation(
|
|
8663
|
+
state: IStorageState,
|
|
8664
|
+
stored: IStoredSessionState,
|
|
8665
|
+
operation: ISessionMetricsOperation,
|
|
8666
|
+
): Promise<void> {
|
|
8667
|
+
try {
|
|
8668
|
+
if (stored.metricsFailure !== undefined) throw stored.metricsFailure;
|
|
8669
|
+
await this.mutateProjection(state, stored, () => settleSessionMetricsOperation(stored.metrics, operation), true);
|
|
8670
|
+
} catch (error) {
|
|
8671
|
+
stored.metricsFailure ??= error;
|
|
8672
|
+
// Never allow a later operation to overwrite a marker whose spend was not durably joined.
|
|
8673
|
+
state.lifecycle = 'fenced';
|
|
8674
|
+
throw error;
|
|
8313
8675
|
}
|
|
8314
|
-
this.emitEvent(scopeId, stored.session, {
|
|
8315
|
-
type: 'model.usage',
|
|
8316
|
-
...(generationId === undefined ? {} : { runId: generationId }),
|
|
8317
|
-
source,
|
|
8318
|
-
...call,
|
|
8319
|
-
});
|
|
8320
8676
|
}
|
|
8321
8677
|
|
|
8322
8678
|
private callbackRun(state: IStorageState, sessionId: string): IActiveRun | undefined {
|
|
@@ -8820,9 +9176,12 @@ export class FlexHarness<TScope = unknown> {
|
|
|
8820
9176
|
errors.push(this.projectExternalError(run, error, 'persistence'));
|
|
8821
9177
|
}
|
|
8822
9178
|
}
|
|
8823
|
-
if (projectionReserved) {
|
|
9179
|
+
if (projectionReserved || run.stored.metrics.pendingGenerationId === run.runId) {
|
|
8824
9180
|
try {
|
|
8825
9181
|
await this.mutateProjection(run.state, run.stored, () => {
|
|
9182
|
+
if (run.stored.metrics.pendingGenerationId === run.runId) {
|
|
9183
|
+
delete run.stored.metrics.pendingGenerationId;
|
|
9184
|
+
}
|
|
8826
9185
|
run.stored.messages = run.stored.messages.filter((message) => message.runId !== run.runId);
|
|
8827
9186
|
run.stored.stagedTerminals = run.stored.stagedTerminals.filter((entry) => entry.runId !== run.runId);
|
|
8828
9187
|
run.stored.reversionSegments = run.stored.reversionSegments.filter(
|
|
@@ -8862,9 +9221,14 @@ export class FlexHarness<TScope = unknown> {
|
|
|
8862
9221
|
}
|
|
8863
9222
|
const tools = handle.tools;
|
|
8864
9223
|
this.assertNoBuiltInToolCollisions(run, tools);
|
|
9224
|
+
const catalog = handle.catalog;
|
|
9225
|
+
if (catalog !== undefined && (typeof catalog !== 'object' || catalog === null)) {
|
|
9226
|
+
throw new FlexHarnessValidationError('Tool handle catalog must be an object.');
|
|
9227
|
+
}
|
|
8865
9228
|
const closeHandle = close;
|
|
8866
9229
|
return {
|
|
8867
9230
|
tools,
|
|
9231
|
+
...(catalog === undefined ? {} : { catalog }),
|
|
8868
9232
|
...(closeHandle ? { close: () => closeHandle.call(handle) } : {}),
|
|
8869
9233
|
};
|
|
8870
9234
|
} catch (error) {
|
|
@@ -8951,6 +9315,9 @@ export class FlexHarness<TScope = unknown> {
|
|
|
8951
9315
|
);
|
|
8952
9316
|
if (!handle) continue;
|
|
8953
9317
|
owned.push({ handle, closed: false });
|
|
9318
|
+
if (handle.catalog !== undefined) {
|
|
9319
|
+
throw new FlexHarnessValidationError('Resource tool providers cannot supply a tool catalog.');
|
|
9320
|
+
}
|
|
8954
9321
|
for (const [toolName, tool] of Object.entries(handle.tools)) {
|
|
8955
9322
|
const exposedName = createFlexResourceToolName(namespace, toolName);
|
|
8956
9323
|
if (Object.prototype.hasOwnProperty.call(tools, exposedName)) {
|
|
@@ -8962,6 +9329,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
8962
9329
|
if (owned.length === 0 && Object.keys(tools).length === 0) return undefined;
|
|
8963
9330
|
return {
|
|
8964
9331
|
tools: tools as TFlexAgentToolSet,
|
|
9332
|
+
...(applicationHandle?.catalog === undefined ? {} : { catalog: applicationHandle.catalog }),
|
|
8965
9333
|
...(owned.some((entry) => entry.handle.close) ? { close: closeOwned } : {}),
|
|
8966
9334
|
};
|
|
8967
9335
|
} catch (error) {
|
|
@@ -9403,6 +9771,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
9403
9771
|
projectionReconciliationRequired: false,
|
|
9404
9772
|
projectionRevision: 0,
|
|
9405
9773
|
projectionQueue: Promise.resolve(),
|
|
9774
|
+
metrics: createSessionMetricsState(false),
|
|
9406
9775
|
rememberedPermissionKeys: new Set(),
|
|
9407
9776
|
permissionRevision: 0,
|
|
9408
9777
|
permissionQueue: Promise.resolve(),
|
|
@@ -9427,6 +9796,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
9427
9796
|
scopeId: string,
|
|
9428
9797
|
scope: TScope,
|
|
9429
9798
|
compactorLifecycleController = new AbortController(),
|
|
9799
|
+
newSession = false,
|
|
9430
9800
|
): Promise<IStoredSessionState> {
|
|
9431
9801
|
const sessionId = metadata.sessionId;
|
|
9432
9802
|
const compactorContext = this.createCompactorContext(
|
|
@@ -9530,11 +9900,12 @@ export class FlexHarness<TScope = unknown> {
|
|
|
9530
9900
|
pendingReversionReleases,
|
|
9531
9901
|
projectionSchemaVersion: projection?.schemaVersion ?? 3,
|
|
9532
9902
|
projectionReconciliationRequired: false,
|
|
9533
|
-
...(projection
|
|
9903
|
+
...(projection
|
|
9534
9904
|
? { projectionBaseline: cloneSerializable(projection) }
|
|
9535
9905
|
: {}),
|
|
9536
9906
|
projectionRevision: projection?.revision ?? 0,
|
|
9537
9907
|
projectionQueue: Promise.resolve(),
|
|
9908
|
+
metrics: cloneSerializable(currentProjection?.metrics ?? createSessionMetricsState(newSession)),
|
|
9538
9909
|
rememberedPermissionKeys: new Set(permission?.rememberedPermissionKeys ?? []),
|
|
9539
9910
|
permissionRevision: permission?.revision ?? 0,
|
|
9540
9911
|
permissionQueue: Promise.resolve(),
|
|
@@ -9553,6 +9924,19 @@ export class FlexHarness<TScope = unknown> {
|
|
|
9553
9924
|
terminalPromptQueueEntries: new Map(),
|
|
9554
9925
|
outstandingPromptBytes: 0,
|
|
9555
9926
|
};
|
|
9927
|
+
if (currentProjection?.metrics === undefined
|
|
9928
|
+
|| stored.metrics.pendingGenerationId !== undefined
|
|
9929
|
+
|| stored.metrics.pendingCompactionId !== undefined
|
|
9930
|
+
|| (metadata.origin !== undefined && stored.metrics.lifetimeUsageComplete)) {
|
|
9931
|
+
await this.mutateProjection(state, stored, () => {
|
|
9932
|
+
if (stored.metrics.pendingGenerationId !== undefined || stored.metrics.pendingCompactionId !== undefined
|
|
9933
|
+
|| metadata.origin !== undefined) {
|
|
9934
|
+
stored.metrics.lifetimeUsageComplete = false;
|
|
9935
|
+
}
|
|
9936
|
+
delete stored.metrics.pendingGenerationId;
|
|
9937
|
+
delete stored.metrics.pendingCompactionId;
|
|
9938
|
+
});
|
|
9939
|
+
}
|
|
9556
9940
|
const {
|
|
9557
9941
|
contextCompactor,
|
|
9558
9942
|
...agentSessionPolicy
|
|
@@ -9609,22 +9993,55 @@ export class FlexHarness<TScope = unknown> {
|
|
|
9609
9993
|
events: filteredEvents,
|
|
9610
9994
|
}),
|
|
9611
9995
|
);
|
|
9612
|
-
|
|
9613
|
-
|
|
9614
|
-
|
|
9615
|
-
|
|
9616
|
-
|
|
9617
|
-
|
|
9618
|
-
|
|
9619
|
-
|
|
9620
|
-
|
|
9621
|
-
|
|
9622
|
-
|
|
9623
|
-
|
|
9624
|
-
|
|
9625
|
-
|
|
9626
|
-
|
|
9627
|
-
|
|
9996
|
+
const operation = createSessionMetricsOperation(plugins.crypto.randomUUID(), 'compaction');
|
|
9997
|
+
try {
|
|
9998
|
+
await this.mutateProjection(state, stored, () => {
|
|
9999
|
+
if (stored.metrics.pendingCompactionId !== undefined || stored.metricsFailure !== undefined) {
|
|
10000
|
+
throw new Error('Session metrics compaction ownership is unresolved.');
|
|
10001
|
+
}
|
|
10002
|
+
stored.metrics.pendingCompactionId = operation.id;
|
|
10003
|
+
});
|
|
10004
|
+
} catch (error) {
|
|
10005
|
+
// No compactor ran. Reconcile a known committed reservation without inventing spend.
|
|
10006
|
+
// Unknown outcomes remain fenced and are repaired conservatively on namespace reload.
|
|
10007
|
+
if (state.lifecycle !== 'fenced' && stored.metrics.pendingCompactionId === operation.id) {
|
|
10008
|
+
try {
|
|
10009
|
+
await this.mutateProjection(state, stored, () => {
|
|
10010
|
+
delete stored.metrics.pendingCompactionId;
|
|
10011
|
+
}, true);
|
|
10012
|
+
} catch (cleanupError) {
|
|
10013
|
+
stored.metricsFailure ??= cleanupError;
|
|
10014
|
+
state.lifecycle = 'fenced';
|
|
10015
|
+
throw combineErrors([error, cleanupError]);
|
|
10016
|
+
}
|
|
10017
|
+
}
|
|
10018
|
+
throw error;
|
|
10019
|
+
}
|
|
10020
|
+
stored.metricsCompaction = operation;
|
|
10021
|
+
try {
|
|
10022
|
+
return await contextCompactor(filteredMessages, filteredEvents, {
|
|
10023
|
+
...options,
|
|
10024
|
+
...invocation,
|
|
10025
|
+
scope: invocation.scope as TScope,
|
|
10026
|
+
...this.compactionModel(state, sessionId, ambientContext, options.reason, modelHint),
|
|
10027
|
+
abortSignal: options.abortSignal
|
|
10028
|
+
? AbortSignal.any([
|
|
10029
|
+
options.abortSignal,
|
|
10030
|
+
state.compactorLifecycleController.signal,
|
|
10031
|
+
stored.compactorLifecycleController.signal,
|
|
10032
|
+
])
|
|
10033
|
+
: AbortSignal.any([
|
|
10034
|
+
state.compactorLifecycleController.signal,
|
|
10035
|
+
stored.compactorLifecycleController.signal,
|
|
10036
|
+
]),
|
|
10037
|
+
});
|
|
10038
|
+
} finally {
|
|
10039
|
+
try {
|
|
10040
|
+
await this.settleMetricsOperation(state, stored, operation);
|
|
10041
|
+
} finally {
|
|
10042
|
+
delete stored.metricsCompaction;
|
|
10043
|
+
}
|
|
10044
|
+
}
|
|
9628
10045
|
},
|
|
9629
10046
|
}
|
|
9630
10047
|
: {}),
|
|
@@ -10044,6 +10461,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
10044
10461
|
const beforeBaseline = stored.projectionBaseline === undefined
|
|
10045
10462
|
? undefined
|
|
10046
10463
|
: cloneSerializable(stored.projectionBaseline);
|
|
10464
|
+
const beforeMetrics = cloneSerializable(stored.metrics);
|
|
10047
10465
|
const beforeMessages = cloneSerializable(stored.messages);
|
|
10048
10466
|
const beforeStages = cloneSerializable(stored.stagedTerminals);
|
|
10049
10467
|
const beforeSegments = cloneSerializable(stored.reversionSegments);
|
|
@@ -10056,6 +10474,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
10056
10474
|
const beforeSnapshot: TFlexProjectionSnapshot = beforeBaseline ?? {
|
|
10057
10475
|
schemaVersion: 3,
|
|
10058
10476
|
revision: beforeRevision,
|
|
10477
|
+
metrics: cloneSerializable(beforeMetrics),
|
|
10059
10478
|
messages: cloneSerializable(beforeMessages),
|
|
10060
10479
|
stagedTerminals: cloneSerializable(beforeStages),
|
|
10061
10480
|
reversionSegments: cloneSerializable(beforeSegments),
|
|
@@ -10072,6 +10491,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
10072
10491
|
if (beforeBaseline === undefined) delete stored.projectionBaseline;
|
|
10073
10492
|
else stored.projectionBaseline = beforeBaseline;
|
|
10074
10493
|
stored.projectionRevision = beforeRevision;
|
|
10494
|
+
stored.metrics = beforeMetrics;
|
|
10075
10495
|
stored.messages = beforeMessages;
|
|
10076
10496
|
stored.stagedTerminals = beforeStages;
|
|
10077
10497
|
stored.reversionSegments = beforeSegments;
|
|
@@ -10251,6 +10671,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
10251
10671
|
return {
|
|
10252
10672
|
schemaVersion: 3,
|
|
10253
10673
|
revision,
|
|
10674
|
+
metrics: cloneSerializable(stored.metrics),
|
|
10254
10675
|
messages: cloneSerializable(stored.messages),
|
|
10255
10676
|
stagedTerminals: cloneSerializable(stored.stagedTerminals),
|
|
10256
10677
|
reversionSegments: cloneSerializable(stored.reversionSegments),
|
|
@@ -10962,6 +11383,11 @@ export class FlexHarness<TScope = unknown> {
|
|
|
10962
11383
|
errors.push(error);
|
|
10963
11384
|
}
|
|
10964
11385
|
}
|
|
11386
|
+
if (stored.metricsFailure !== undefined) {
|
|
11387
|
+
errors.push(stored.metricsFailure);
|
|
11388
|
+
// Runtime ownership has settled; the fenced namespace will be reloaded from its durable WAL.
|
|
11389
|
+
if (stored.agentSessionCloseCompleted) delete stored.metricsFailure;
|
|
11390
|
+
}
|
|
10965
11391
|
if (errors.length > 0) throw combineErrors(errors);
|
|
10966
11392
|
}
|
|
10967
11393
|
|