@modelprofile.com/flexharness 3.4.0 → 3.6.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/changelog.md +19 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.flexharness.d.ts +24 -0
- package/dist_ts/classes.flexharness.js +920 -97
- package/dist_ts/interfaces.d.ts +43 -1
- package/dist_ts/plugins.d.ts +2 -2
- package/dist_ts/plugins.js +3 -3
- package/dist_ts/utils.json.js +176 -8
- package/package.json +1 -1
- package/readme.hints.md +13 -0
- package/readme.md +84 -3
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.flexharness.ts +1203 -106
- package/ts/interfaces.ts +50 -1
- package/ts/plugins.ts +4 -0
- package/ts/utils.json.ts +202 -8
|
@@ -44,15 +44,18 @@ import type {
|
|
|
44
44
|
IFlexProjectionSnapshot,
|
|
45
45
|
IFlexResolvedModel,
|
|
46
46
|
IFlexResolvedScope,
|
|
47
|
+
IFlexResourceToolProviderDescriptor,
|
|
47
48
|
IFlexRunFinishedEvent,
|
|
48
49
|
IFlexScheduledPromptAdmission,
|
|
49
50
|
IFlexSchedulePromptOptions,
|
|
50
51
|
IFlexScopeSnapshot,
|
|
51
52
|
IFlexSession,
|
|
52
53
|
IFlexSessionTombstone,
|
|
54
|
+
IFlexSubagentDefinition,
|
|
53
55
|
IFlexTerminalProjection,
|
|
54
56
|
IFlexToolHandle,
|
|
55
57
|
IFlexToolMessagePart,
|
|
58
|
+
IFlexToolProviderContext,
|
|
56
59
|
IFlexUncertainToolExecution,
|
|
57
60
|
IFlexUpdateSessionOptions,
|
|
58
61
|
IFlexUsage,
|
|
@@ -199,6 +202,8 @@ interface IActiveRun {
|
|
|
199
202
|
callbacksClosed: boolean;
|
|
200
203
|
reasoningPartIds: Map<string, string>;
|
|
201
204
|
toolPartIds: Map<string, string>;
|
|
205
|
+
subagentCallCount: number;
|
|
206
|
+
subagentSessionIds: Set<string>;
|
|
202
207
|
pendingPermissionIds: Set<string>;
|
|
203
208
|
phase: TRunPhase;
|
|
204
209
|
reservedUserMessage?: IFlexMessage;
|
|
@@ -281,6 +286,18 @@ interface IExecutableToolRecord extends Record<string, unknown> {
|
|
|
281
286
|
execute?: (input: unknown, options: unknown) => unknown;
|
|
282
287
|
}
|
|
283
288
|
|
|
289
|
+
interface IFlexSubagentTaskInput {
|
|
290
|
+
description: string;
|
|
291
|
+
prompt: string;
|
|
292
|
+
subagentType: string;
|
|
293
|
+
taskId?: string;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
interface IFlexSubagentAcquisition {
|
|
297
|
+
stored: IStoredSessionState;
|
|
298
|
+
created: boolean;
|
|
299
|
+
}
|
|
300
|
+
|
|
284
301
|
type TEventDetails = Record<string, unknown> & {
|
|
285
302
|
type: TFlexHarnessEvent['type'];
|
|
286
303
|
};
|
|
@@ -309,6 +326,23 @@ const maxProjectedErrorMessageBytes = 2048;
|
|
|
309
326
|
const maxProjectedErrorCodeBytes = 128;
|
|
310
327
|
const maxScheduleDebounceMs = 24 * 60 * 60 * 1000;
|
|
311
328
|
const maxBackgroundExecutions = 100;
|
|
329
|
+
const maxSubagentDefinitions = 32;
|
|
330
|
+
const maxSubagentNameBytes = 128;
|
|
331
|
+
const maxSubagentDescriptionBytes = 2048;
|
|
332
|
+
const maxSubagentModelHintBytes = 512;
|
|
333
|
+
const maxSubagentSystemBytes = 64 * 1024;
|
|
334
|
+
const maxSubagentTaskDescriptionBytes = 256;
|
|
335
|
+
const maxSubagentPromptBytes = 64 * 1024;
|
|
336
|
+
const maxSubagentTaskIdBytes = 512;
|
|
337
|
+
const maxSubagentResultTextBytes = 64 * 1024;
|
|
338
|
+
const defaultMaxSubagentDepth = 1;
|
|
339
|
+
const maximumMaxSubagentDepth = 8;
|
|
340
|
+
const defaultMaxSubagentCallsPerRun = 32;
|
|
341
|
+
const maximumMaxSubagentCallsPerRun = 128;
|
|
342
|
+
const maxResourceToolProviders = 128;
|
|
343
|
+
const maxResourceIdBytes = 512;
|
|
344
|
+
const maxResourceToolNameBytes = 512;
|
|
345
|
+
const resourceToolStemLength = 16;
|
|
312
346
|
const repairCancellationMessage = 'The process stopped before this run completed.';
|
|
313
347
|
const scopeRetirementMessage = 'The scope is being retired.';
|
|
314
348
|
const externalErrorFallback: IFlexErrorInfo = Object.freeze({
|
|
@@ -408,6 +442,136 @@ function validateIdentifier(value: string, name: string): void {
|
|
|
408
442
|
}
|
|
409
443
|
}
|
|
410
444
|
|
|
445
|
+
function sha256Hex(value: string): string {
|
|
446
|
+
return plugins.crypto.createHash('sha256').update(value, 'utf8').digest('hex');
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function createFlexResourceIdentityDigest(
|
|
450
|
+
resourceId: string,
|
|
451
|
+
attachmentRevision: number,
|
|
452
|
+
): string {
|
|
453
|
+
validateUtf8String(resourceId, 'resourceId', maxResourceIdBytes, true);
|
|
454
|
+
if (!Number.isSafeInteger(attachmentRevision) || attachmentRevision < 0) {
|
|
455
|
+
throw new FlexHarnessValidationError('attachmentRevision must be a non-negative safe integer.');
|
|
456
|
+
}
|
|
457
|
+
return sha256Hex(JSON.stringify([resourceId, attachmentRevision]));
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
export function createFlexResourceToolNamespace(
|
|
461
|
+
resourceId: string,
|
|
462
|
+
attachmentRevision: number,
|
|
463
|
+
): string {
|
|
464
|
+
const digest = createFlexResourceIdentityDigest(resourceId, attachmentRevision);
|
|
465
|
+
return `resource_${digest.slice(0, 16)}`;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
export function createFlexResourceToolName(namespace: string, toolName: string): string {
|
|
469
|
+
if (!/^resource_[a-f0-9]{16}$/u.test(namespace)) {
|
|
470
|
+
throw new FlexHarnessValidationError('Resource tool namespace is invalid.');
|
|
471
|
+
}
|
|
472
|
+
validateUtf8String(toolName, 'resource tool name', maxResourceToolNameBytes, true);
|
|
473
|
+
const stem = toolName
|
|
474
|
+
.replace(/[^A-Za-z0-9_-]/gu, '_')
|
|
475
|
+
.slice(0, resourceToolStemLength) || 'tool';
|
|
476
|
+
return `${namespace}__${stem}__${sha256Hex(toolName).slice(0, 12)}`;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function validateUtf8String(
|
|
480
|
+
value: unknown,
|
|
481
|
+
name: string,
|
|
482
|
+
maxBytes: number,
|
|
483
|
+
nonEmpty = false,
|
|
484
|
+
): asserts value is string {
|
|
485
|
+
if (
|
|
486
|
+
typeof value !== 'string'
|
|
487
|
+
|| (nonEmpty && !value.trim())
|
|
488
|
+
|| Buffer.byteLength(value, 'utf8') > maxBytes
|
|
489
|
+
) {
|
|
490
|
+
throw new FlexHarnessValidationError(
|
|
491
|
+
`${name} must be ${nonEmpty ? 'a non-empty ' : 'a '}string of at most ${maxBytes} UTF-8 bytes.`,
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function normalizeSubagents(
|
|
497
|
+
definitions: IFlexSubagentDefinition[] | undefined,
|
|
498
|
+
): ReadonlyMap<string, Readonly<IFlexSubagentDefinition>> {
|
|
499
|
+
if (definitions === undefined) return new Map();
|
|
500
|
+
if (!Array.isArray(definitions) || definitions.length > maxSubagentDefinitions) {
|
|
501
|
+
throw new FlexHarnessValidationError(
|
|
502
|
+
`subagents must be an array with at most ${maxSubagentDefinitions} definitions.`,
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
const normalized = new Map<string, Readonly<IFlexSubagentDefinition>>();
|
|
506
|
+
for (let index = 0; index < definitions.length; index++) {
|
|
507
|
+
const definition = definitions[index];
|
|
508
|
+
if (
|
|
509
|
+
!definition
|
|
510
|
+
|| typeof definition !== 'object'
|
|
511
|
+
|| Array.isArray(definition)
|
|
512
|
+
|| ![Object.prototype, null].includes(Object.getPrototypeOf(definition))
|
|
513
|
+
) {
|
|
514
|
+
throw new FlexHarnessValidationError(`subagents[${index}] must be a plain object.`);
|
|
515
|
+
}
|
|
516
|
+
const unsupported = Object.keys(definition).find(
|
|
517
|
+
(key) => !['name', 'description', 'modelHint', 'system', 'maxSteps'].includes(key),
|
|
518
|
+
);
|
|
519
|
+
if (unsupported) {
|
|
520
|
+
throw new FlexHarnessValidationError(`subagents[${index}] does not support "${unsupported}".`);
|
|
521
|
+
}
|
|
522
|
+
validateUtf8String(definition.name, `subagents[${index}].name`, maxSubagentNameBytes, true);
|
|
523
|
+
validateUtf8String(
|
|
524
|
+
definition.description,
|
|
525
|
+
`subagents[${index}].description`,
|
|
526
|
+
maxSubagentDescriptionBytes,
|
|
527
|
+
);
|
|
528
|
+
if (definition.modelHint !== undefined) {
|
|
529
|
+
validateUtf8String(
|
|
530
|
+
definition.modelHint,
|
|
531
|
+
`subagents[${index}].modelHint`,
|
|
532
|
+
maxSubagentModelHintBytes,
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
if (definition.system !== undefined) {
|
|
536
|
+
validateUtf8String(
|
|
537
|
+
definition.system,
|
|
538
|
+
`subagents[${index}].system`,
|
|
539
|
+
maxSubagentSystemBytes,
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
if (
|
|
543
|
+
definition.maxSteps !== undefined
|
|
544
|
+
&& (!Number.isSafeInteger(definition.maxSteps) || definition.maxSteps < 1)
|
|
545
|
+
) {
|
|
546
|
+
throw new FlexHarnessValidationError(`subagents[${index}].maxSteps must be a positive integer.`);
|
|
547
|
+
}
|
|
548
|
+
if (normalized.has(definition.name)) {
|
|
549
|
+
throw new FlexHarnessValidationError(`Duplicate subagent definition "${definition.name}".`);
|
|
550
|
+
}
|
|
551
|
+
normalized.set(definition.name, Object.freeze({
|
|
552
|
+
name: definition.name,
|
|
553
|
+
description: definition.description,
|
|
554
|
+
...(definition.modelHint === undefined ? {} : { modelHint: definition.modelHint }),
|
|
555
|
+
...(definition.system === undefined ? {} : { system: definition.system }),
|
|
556
|
+
...(definition.maxSteps === undefined ? {} : { maxSteps: definition.maxSteps }),
|
|
557
|
+
}));
|
|
558
|
+
}
|
|
559
|
+
return Object.freeze(normalized);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function resolveBoundedPositiveInteger(
|
|
563
|
+
value: number | undefined,
|
|
564
|
+
name: string,
|
|
565
|
+
defaultValue: number,
|
|
566
|
+
maximum: number,
|
|
567
|
+
): number {
|
|
568
|
+
const resolved = value ?? defaultValue;
|
|
569
|
+
if (!Number.isSafeInteger(resolved) || resolved < 1 || resolved > maximum) {
|
|
570
|
+
throw new FlexHarnessValidationError(`${name} must be an integer from 1 through ${maximum}.`);
|
|
571
|
+
}
|
|
572
|
+
return resolved;
|
|
573
|
+
}
|
|
574
|
+
|
|
411
575
|
function requireTransferIdentifier(value: string, field: string): void {
|
|
412
576
|
if (Buffer.byteLength(value, 'utf8') > maxTransferIdentifierBytes) {
|
|
413
577
|
throw new FlexHarnessValidationError(`${field} exceeds the transfer limit.`);
|
|
@@ -735,6 +899,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
735
899
|
private readonly scopeResolver: IFlexHarnessOptions<TScope>['scopeResolver'];
|
|
736
900
|
private readonly modelResolver: IFlexHarnessOptions<TScope>['modelResolver'];
|
|
737
901
|
private readonly toolProvider: IFlexHarnessOptions<TScope>['toolProvider'];
|
|
902
|
+
private readonly resourceToolProviderResolver: IFlexHarnessOptions<TScope>['resourceToolProviderResolver'];
|
|
738
903
|
private readonly executionContextProvider: IFlexHarnessOptions<TScope>['executionContextProvider'];
|
|
739
904
|
private readonly stores: IFlexHarnessStores;
|
|
740
905
|
private readonly agentSessionPolicy: IFlexAgentSessionPolicy<TScope>;
|
|
@@ -742,6 +907,9 @@ export class FlexHarness<TScope = unknown> {
|
|
|
742
907
|
private readonly callbackLimits: Required<IFlexCallbackLimits>;
|
|
743
908
|
private readonly promptQueueLimits: Required<IFlexPromptQueueLimits>;
|
|
744
909
|
private readonly externalErrorProjector?: TFlexExternalErrorProjector;
|
|
910
|
+
private readonly subagents: ReadonlyMap<string, Readonly<IFlexSubagentDefinition>>;
|
|
911
|
+
private readonly maxSubagentDepth: number;
|
|
912
|
+
private readonly maxSubagentCallsPerRun: number;
|
|
745
913
|
private readonly stateLoads = new Map<string, Promise<IStorageState>>();
|
|
746
914
|
private readonly scopeAdmissions = new Map<string, IScopeAdmissionState>();
|
|
747
915
|
private readonly scopeRetirements = new Map<string, Promise<void>>();
|
|
@@ -779,6 +947,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
779
947
|
this.scopeResolver = options.scopeResolver;
|
|
780
948
|
this.modelResolver = options.modelResolver;
|
|
781
949
|
this.toolProvider = options.toolProvider;
|
|
950
|
+
this.resourceToolProviderResolver = options.resourceToolProviderResolver;
|
|
782
951
|
this.executionContextProvider = options.executionContextProvider;
|
|
783
952
|
this.stores = options.stores ?? new InMemoryFlexHarnessStores();
|
|
784
953
|
this.agentSessionPolicy = normalizeAgentSessionPolicy(options.agentSessionPolicy);
|
|
@@ -786,6 +955,19 @@ export class FlexHarness<TScope = unknown> {
|
|
|
786
955
|
this.callbackLimits = resolveCallbackLimits(options.callbackLimits);
|
|
787
956
|
this.promptQueueLimits = resolvePromptQueueLimits(options.promptQueueLimits);
|
|
788
957
|
this.externalErrorProjector = options.externalErrorProjector;
|
|
958
|
+
this.subagents = normalizeSubagents(options.subagents);
|
|
959
|
+
this.maxSubagentDepth = resolveBoundedPositiveInteger(
|
|
960
|
+
options.maxSubagentDepth,
|
|
961
|
+
'maxSubagentDepth',
|
|
962
|
+
defaultMaxSubagentDepth,
|
|
963
|
+
maximumMaxSubagentDepth,
|
|
964
|
+
);
|
|
965
|
+
this.maxSubagentCallsPerRun = resolveBoundedPositiveInteger(
|
|
966
|
+
options.maxSubagentCallsPerRun,
|
|
967
|
+
'maxSubagentCallsPerRun',
|
|
968
|
+
defaultMaxSubagentCallsPerRun,
|
|
969
|
+
maximumMaxSubagentCallsPerRun,
|
|
970
|
+
);
|
|
789
971
|
}
|
|
790
972
|
|
|
791
973
|
public async listSessions(scopeId: string): Promise<IFlexSession[]> {
|
|
@@ -851,6 +1033,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
851
1033
|
updatedAt: timestamp,
|
|
852
1034
|
status: 'idle',
|
|
853
1035
|
activity: { status: 'idle' },
|
|
1036
|
+
depth: 0,
|
|
854
1037
|
};
|
|
855
1038
|
resolved.state.sessions.set(
|
|
856
1039
|
sessionId,
|
|
@@ -864,6 +1047,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
864
1047
|
metadata,
|
|
865
1048
|
scopeId,
|
|
866
1049
|
resolved.scope.scope,
|
|
1050
|
+
placeholder.compactorLifecycleController,
|
|
867
1051
|
);
|
|
868
1052
|
const loaded = await initialization;
|
|
869
1053
|
if (resolved.state.sessions.get(sessionId) !== placeholder) {
|
|
@@ -890,6 +1074,8 @@ export class FlexHarness<TScope = unknown> {
|
|
|
890
1074
|
const tombstone: IFlexSessionTombstone = {
|
|
891
1075
|
sessionId,
|
|
892
1076
|
deletedAt: new Date().toISOString(),
|
|
1077
|
+
rootSessionId: sessionId,
|
|
1078
|
+
depth: 0,
|
|
893
1079
|
};
|
|
894
1080
|
let tombstoneCommitted = false;
|
|
895
1081
|
let domainCleanupCompleted = false;
|
|
@@ -899,11 +1085,12 @@ export class FlexHarness<TScope = unknown> {
|
|
|
899
1085
|
resolved.state.tombstones.set(sessionId, tombstone);
|
|
900
1086
|
}, true);
|
|
901
1087
|
tombstoneCommitted = true;
|
|
1088
|
+
completeInitialization();
|
|
902
1089
|
await this.finishTombstoneCleanup(
|
|
903
1090
|
resolved.state,
|
|
904
1091
|
sessionId,
|
|
905
1092
|
scopeId,
|
|
906
|
-
resolved.scope.scope,
|
|
1093
|
+
{ scopeId, scope: resolved.scope.scope },
|
|
907
1094
|
);
|
|
908
1095
|
domainCleanupCompleted = true;
|
|
909
1096
|
} catch (cleanupError) {
|
|
@@ -981,15 +1168,17 @@ export class FlexHarness<TScope = unknown> {
|
|
|
981
1168
|
public async deleteSession(scopeId: string, sessionId: string): Promise<void> {
|
|
982
1169
|
const { scope, state } = await this.resolveState(scopeId);
|
|
983
1170
|
validateIdentifier(sessionId, 'sessionId');
|
|
984
|
-
|
|
1171
|
+
await state.scopeQueue;
|
|
1172
|
+
const deletionKey = state.tombstones.get(sessionId)?.rootSessionId ?? sessionId;
|
|
1173
|
+
const existingDeletion = state.sessionDeletions.get(deletionKey);
|
|
985
1174
|
if (existingDeletion) return existingDeletion;
|
|
986
1175
|
let deletion!: Promise<void>;
|
|
987
1176
|
deletion = this.deleteSessionInternal(state, scopeId, scope.scope, sessionId).finally(() => {
|
|
988
|
-
if (state.sessionDeletions.get(
|
|
989
|
-
state.sessionDeletions.delete(
|
|
1177
|
+
if (state.sessionDeletions.get(deletionKey) === deletion) {
|
|
1178
|
+
state.sessionDeletions.delete(deletionKey);
|
|
990
1179
|
}
|
|
991
1180
|
});
|
|
992
|
-
state.sessionDeletions.set(
|
|
1181
|
+
state.sessionDeletions.set(deletionKey, deletion);
|
|
993
1182
|
return deletion;
|
|
994
1183
|
}
|
|
995
1184
|
|
|
@@ -1001,35 +1190,91 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1001
1190
|
): Promise<void> {
|
|
1002
1191
|
const existingTombstone = state.tombstones.get(sessionId);
|
|
1003
1192
|
if (existingTombstone) {
|
|
1193
|
+
const rootSessionId = existingTombstone.rootSessionId ?? existingTombstone.sessionId;
|
|
1194
|
+
const descendantRoots = this.descendantTombstoneRoots(state, sessionId)
|
|
1195
|
+
.filter((candidate) => candidate !== rootSessionId);
|
|
1004
1196
|
try {
|
|
1005
|
-
await this.
|
|
1197
|
+
const orphanErrors = await this.closeOrphanedResources(state.storageKey);
|
|
1198
|
+
if (orphanErrors.length > 0) throw combineErrors(orphanErrors);
|
|
1199
|
+
for (const descendantRoot of descendantRoots) {
|
|
1200
|
+
await this.finishTombstoneCleanup(
|
|
1201
|
+
state,
|
|
1202
|
+
descendantRoot,
|
|
1203
|
+
scopeId,
|
|
1204
|
+
{ scopeId, scope },
|
|
1205
|
+
);
|
|
1206
|
+
}
|
|
1207
|
+
await this.finishTombstoneCleanup(state, rootSessionId, scopeId, { scopeId, scope });
|
|
1006
1208
|
} catch (error) {
|
|
1007
1209
|
throw this.projectOperationError(error, 'persistence', scopeId, sessionId, 'session-delete');
|
|
1008
1210
|
}
|
|
1009
1211
|
return;
|
|
1010
1212
|
}
|
|
1011
1213
|
const stored = this.requireSession(state, sessionId);
|
|
1012
|
-
|
|
1214
|
+
let deletedSessions: IFlexSession[] = [];
|
|
1215
|
+
let descendantRoots: string[] = [];
|
|
1013
1216
|
await this.mutateScope(state, () => {
|
|
1014
1217
|
if (state.sessions.get(sessionId) !== stored) {
|
|
1015
1218
|
throw new FlexHarnessNotFoundError('Session', sessionId);
|
|
1016
1219
|
}
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
sessionId
|
|
1024
|
-
|
|
1025
|
-
|
|
1220
|
+
const subtree = this.collectSessionSubtree(state, sessionId);
|
|
1221
|
+
descendantRoots = this.descendantTombstoneRoots(state, sessionId);
|
|
1222
|
+
const rootDepth = stored.session.depth ?? 0;
|
|
1223
|
+
const deletedAt = new Date().toISOString();
|
|
1224
|
+
deletedSessions = subtree.map((entry) => cloneSerializable(entry.session));
|
|
1225
|
+
for (const entry of subtree) {
|
|
1226
|
+
const entrySessionId = entry.session.sessionId;
|
|
1227
|
+
state.sessions.delete(entrySessionId);
|
|
1228
|
+
state.retainedSessionCleanups.set(entrySessionId, {
|
|
1229
|
+
stored: entry,
|
|
1230
|
+
domainsCompleted: false,
|
|
1231
|
+
});
|
|
1232
|
+
state.tombstones.set(entrySessionId, {
|
|
1233
|
+
sessionId: entrySessionId,
|
|
1234
|
+
deletedAt,
|
|
1235
|
+
rootSessionId: sessionId,
|
|
1236
|
+
depth: (entry.session.depth ?? rootDepth) - rootDepth,
|
|
1237
|
+
...(entry.session.parentSessionId === undefined
|
|
1238
|
+
? {}
|
|
1239
|
+
: { parentSessionId: entry.session.parentSessionId }),
|
|
1240
|
+
});
|
|
1241
|
+
}
|
|
1026
1242
|
});
|
|
1243
|
+
const reason = this.trustInternalError(new FlexHarnessAbortError('The session subtree was deleted.'));
|
|
1244
|
+
const childFirstDeletedSessions = [...deletedSessions].sort((left, right) =>
|
|
1245
|
+
(right.depth ?? 0) - (left.depth ?? 0)
|
|
1246
|
+
|| left.sessionId.localeCompare(right.sessionId));
|
|
1247
|
+
for (const deleted of childFirstDeletedSessions) {
|
|
1248
|
+
const active = state.activeRuns.get(deleted.sessionId);
|
|
1249
|
+
if (active) {
|
|
1250
|
+
this.cancelRun(
|
|
1251
|
+
active,
|
|
1252
|
+
reason,
|
|
1253
|
+
this.createCompactorContext(scopeId, scope, state.storageKey, deleted.sessionId),
|
|
1254
|
+
);
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1027
1257
|
try {
|
|
1028
|
-
await this.
|
|
1258
|
+
const orphanErrors = await this.closeOrphanedResources(state.storageKey);
|
|
1259
|
+
if (orphanErrors.length > 0) throw combineErrors(orphanErrors);
|
|
1260
|
+
for (const descendantRoot of descendantRoots) {
|
|
1261
|
+
await this.finishTombstoneCleanup(
|
|
1262
|
+
state,
|
|
1263
|
+
descendantRoot,
|
|
1264
|
+
scopeId,
|
|
1265
|
+
{ scopeId, scope },
|
|
1266
|
+
);
|
|
1267
|
+
}
|
|
1268
|
+
await this.finishTombstoneCleanup(state, sessionId, scopeId, { scopeId, scope });
|
|
1029
1269
|
} catch (error) {
|
|
1030
1270
|
throw this.projectOperationError(error, 'persistence', scopeId, sessionId, 'session-delete');
|
|
1031
1271
|
}
|
|
1032
|
-
|
|
1272
|
+
for (const deleted of childFirstDeletedSessions) {
|
|
1273
|
+
this.emitEvent(scopeId, deleted.sessionId, {
|
|
1274
|
+
type: 'session.deleted',
|
|
1275
|
+
session: publicSnapshot(deleted),
|
|
1276
|
+
});
|
|
1277
|
+
}
|
|
1033
1278
|
}
|
|
1034
1279
|
|
|
1035
1280
|
public async getMessages(scopeId: string, sessionId: string): Promise<IFlexMessage[]> {
|
|
@@ -1516,6 +1761,8 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1516
1761
|
options: IFlexPromptOptions,
|
|
1517
1762
|
scheduleKey?: string,
|
|
1518
1763
|
debounceMs?: number,
|
|
1764
|
+
subagentAdmission = false,
|
|
1765
|
+
admissionSignal?: AbortSignal,
|
|
1519
1766
|
): Promise<{ admission: IFlexPromptQueueAdmission; started: Promise<IFlexPromptAdmission> }> {
|
|
1520
1767
|
this.assertOpen();
|
|
1521
1768
|
const normalizedPrompt = normalizeFlexPrompt(prompt);
|
|
@@ -1536,20 +1783,35 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1536
1783
|
resolveSettled: () => resolveSettled(),
|
|
1537
1784
|
};
|
|
1538
1785
|
this.pendingPromptAdmissionOwners.add(pendingOwner);
|
|
1786
|
+
const admissionSignals = [
|
|
1787
|
+
pendingOwner.controller.signal,
|
|
1788
|
+
...(admissionSignal === undefined ? [] : [admissionSignal]),
|
|
1789
|
+
];
|
|
1539
1790
|
let abortAdmission!: () => void;
|
|
1540
1791
|
const abortPromise = new Promise<never>((_resolve, reject) => {
|
|
1541
|
-
abortAdmission = () =>
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1792
|
+
abortAdmission = () => {
|
|
1793
|
+
const aborted = admissionSignals.find((signal) => signal.aborted);
|
|
1794
|
+
reject(aborted?.reason ?? new FlexHarnessAbortError());
|
|
1795
|
+
};
|
|
1796
|
+
for (const signal of admissionSignals) {
|
|
1797
|
+
signal.addEventListener('abort', abortAdmission, { once: true });
|
|
1798
|
+
}
|
|
1799
|
+
if (admissionSignals.some((signal) => signal.aborted)) abortAdmission();
|
|
1546
1800
|
});
|
|
1547
1801
|
try {
|
|
1548
1802
|
const resolved = await Promise.race([this.resolveState(scopeId), abortPromise]);
|
|
1549
1803
|
const state = resolved.state;
|
|
1550
1804
|
await Promise.race([state.scopeQueue, abortPromise]);
|
|
1805
|
+
if (admissionSignal?.aborted) {
|
|
1806
|
+
throw admissionSignal.reason ?? new FlexHarnessAbortError();
|
|
1807
|
+
}
|
|
1551
1808
|
this.assertStateAcceptingWork(state);
|
|
1552
1809
|
const stored = this.requireSession(state, sessionId);
|
|
1810
|
+
if (stored.session.agent !== undefined && !subagentAdmission) {
|
|
1811
|
+
throw new FlexHarnessValidationError(
|
|
1812
|
+
'Subagent sessions can only be prompted through the foreground task tool.',
|
|
1813
|
+
);
|
|
1814
|
+
}
|
|
1553
1815
|
if (stored.outstandingPromptsById.size >= this.promptQueueLimits.maxOutstandingPromptsPerSession) {
|
|
1554
1816
|
throw new FlexHarnessQueueFullError(
|
|
1555
1817
|
`Session "${sessionId}" has reached its outstanding prompt limit.`,
|
|
@@ -1615,7 +1877,9 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1615
1877
|
started,
|
|
1616
1878
|
};
|
|
1617
1879
|
} finally {
|
|
1618
|
-
|
|
1880
|
+
for (const signal of admissionSignals) {
|
|
1881
|
+
signal.removeEventListener('abort', abortAdmission);
|
|
1882
|
+
}
|
|
1619
1883
|
this.pendingPromptAdmissionOwners.delete(pendingOwner);
|
|
1620
1884
|
releasePendingAdmission();
|
|
1621
1885
|
pendingOwner.resolveSettled();
|
|
@@ -1714,6 +1978,8 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1714
1978
|
callbacksClosed: false,
|
|
1715
1979
|
reasoningPartIds: new Map(),
|
|
1716
1980
|
toolPartIds: new Map(),
|
|
1981
|
+
subagentCallCount: 0,
|
|
1982
|
+
subagentSessionIds: new Set(),
|
|
1717
1983
|
pendingPermissionIds: new Set(),
|
|
1718
1984
|
phase: 'admitting',
|
|
1719
1985
|
completion: queued.completion,
|
|
@@ -2109,28 +2375,38 @@ export class FlexHarness<TScope = unknown> {
|
|
|
2109
2375
|
queued.status = 'running';
|
|
2110
2376
|
this.emitPromptQueueEvent(queued, 'prompt.running');
|
|
2111
2377
|
}
|
|
2378
|
+
const resolverRelationship = {
|
|
2379
|
+
...(run.stored.session.parentSessionId === undefined
|
|
2380
|
+
? {}
|
|
2381
|
+
: { parentSessionId: run.stored.session.parentSessionId }),
|
|
2382
|
+
...(run.stored.session.agent === undefined ? {} : { agent: run.stored.session.agent }),
|
|
2383
|
+
};
|
|
2112
2384
|
const modelOutcome = Promise.resolve()
|
|
2113
|
-
.then(() => this.modelResolver.resolveModel({
|
|
2385
|
+
.then(() => this.modelResolver.resolveModel(Object.freeze({
|
|
2114
2386
|
scopeId: run.scopeId,
|
|
2115
2387
|
scope: run.scope as TScope,
|
|
2116
2388
|
sessionId: run.sessionId,
|
|
2117
2389
|
runId: run.runId,
|
|
2118
2390
|
...(options.modelHint ? { modelHint: options.modelHint } : {}),
|
|
2391
|
+
...resolverRelationship,
|
|
2119
2392
|
signal,
|
|
2120
|
-
}))
|
|
2393
|
+
})))
|
|
2121
2394
|
.then(
|
|
2122
2395
|
(value): IResolverOutcome<IFlexResolvedModel> => ({ source: 'model', success: true, value }),
|
|
2123
2396
|
(error): IResolverOutcome<IFlexResolvedModel> => ({ source: 'model', success: false, error }),
|
|
2124
2397
|
);
|
|
2398
|
+
const toolProviderContext: Readonly<IFlexToolProviderContext<TScope>> = Object.freeze({
|
|
2399
|
+
scopeId: run.scopeId,
|
|
2400
|
+
scope: run.scope as TScope,
|
|
2401
|
+
sessionId: run.sessionId,
|
|
2402
|
+
runId: run.runId,
|
|
2403
|
+
...resolverRelationship,
|
|
2404
|
+
signal,
|
|
2405
|
+
requestPermission: (request: IFlexPermissionRequestInput) =>
|
|
2406
|
+
this.requestPermission(run.state, run, request),
|
|
2407
|
+
});
|
|
2125
2408
|
const toolOutcome = Promise.resolve()
|
|
2126
|
-
.then(() => this.
|
|
2127
|
-
scopeId: run.scopeId,
|
|
2128
|
-
scope: run.scope as TScope,
|
|
2129
|
-
sessionId: run.sessionId,
|
|
2130
|
-
runId: run.runId,
|
|
2131
|
-
signal,
|
|
2132
|
-
requestPermission: (request) => this.requestPermission(run.state, run, request),
|
|
2133
|
-
}))
|
|
2409
|
+
.then(() => this.provideRunToolHandle(run, toolProviderContext))
|
|
2134
2410
|
.then(
|
|
2135
2411
|
(value): IResolverOutcome<IFlexToolHandle | undefined> => ({ source: 'tools', success: true, value }),
|
|
2136
2412
|
(error): IResolverOutcome<IFlexToolHandle | undefined> => ({ source: 'tools', success: false, error }),
|
|
@@ -2197,9 +2473,21 @@ export class FlexHarness<TScope = unknown> {
|
|
|
2197
2473
|
run.modelResolution = model!;
|
|
2198
2474
|
let tools: TFlexAgentToolSet | undefined;
|
|
2199
2475
|
try {
|
|
2200
|
-
|
|
2476
|
+
const providedTools = toolHandle?.tools;
|
|
2477
|
+
if (
|
|
2478
|
+
this.subagents.size > 0
|
|
2479
|
+
&& providedTools
|
|
2480
|
+
&& Object.prototype.hasOwnProperty.call(providedTools, 'task')
|
|
2481
|
+
) {
|
|
2482
|
+
throw new FlexHarnessValidationError('The application tool provider cannot define reserved tool "task".');
|
|
2483
|
+
}
|
|
2484
|
+
const combinedTools: Record<string, unknown> = { ...(providedTools ?? {}) };
|
|
2485
|
+
if (this.subagents.size > 0 && (run.stored.session.depth ?? 0) < this.maxSubagentDepth) {
|
|
2486
|
+
combinedTools.task = this.createSubagentTool(run);
|
|
2487
|
+
}
|
|
2488
|
+
tools = Object.keys(combinedTools).length > 0
|
|
2201
2489
|
? wrapToolSet(
|
|
2202
|
-
|
|
2490
|
+
combinedTools as TFlexAgentToolSet,
|
|
2203
2491
|
this.toolOutputLimits,
|
|
2204
2492
|
(toolError) => this.projectExternalError(run, toolError, 'toolExecution'),
|
|
2205
2493
|
)
|
|
@@ -2247,6 +2535,426 @@ export class FlexHarness<TScope = unknown> {
|
|
|
2247
2535
|
}
|
|
2248
2536
|
}
|
|
2249
2537
|
|
|
2538
|
+
private createSubagentTool(run: IActiveRun): unknown {
|
|
2539
|
+
const available = [...this.subagents.values()]
|
|
2540
|
+
.map((definition) => `- ${definition.name}: ${definition.description}`)
|
|
2541
|
+
.join('\n');
|
|
2542
|
+
return plugins.tool({
|
|
2543
|
+
description: `Run one configured FlexHarness subagent in the foreground and return its final text.\nAvailable subagents:\n${available}`,
|
|
2544
|
+
inputSchema: plugins.z.object({
|
|
2545
|
+
description: plugins.z.string(),
|
|
2546
|
+
prompt: plugins.z.string(),
|
|
2547
|
+
subagentType: plugins.z.string(),
|
|
2548
|
+
taskId: plugins.z.string().optional(),
|
|
2549
|
+
}).strict(),
|
|
2550
|
+
execute: (input: IFlexSubagentTaskInput, options?: { toolCallId?: string }) =>
|
|
2551
|
+
this.executeSubagentTask(run, input, options?.toolCallId),
|
|
2552
|
+
});
|
|
2553
|
+
}
|
|
2554
|
+
|
|
2555
|
+
private async executeSubagentTask(
|
|
2556
|
+
run: IActiveRun,
|
|
2557
|
+
input: IFlexSubagentTaskInput,
|
|
2558
|
+
toolCallId: string | undefined,
|
|
2559
|
+
): Promise<{ taskId: string; status: 'completed'; text: string; model: IFlexModelIdentity }> {
|
|
2560
|
+
run.subagentCallCount++;
|
|
2561
|
+
if (run.subagentCallCount > this.maxSubagentCallsPerRun) {
|
|
2562
|
+
throw new FlexHarnessValidationError(
|
|
2563
|
+
`Run "${run.runId}" exceeds maxSubagentCallsPerRun (${this.maxSubagentCallsPerRun}).`,
|
|
2564
|
+
);
|
|
2565
|
+
}
|
|
2566
|
+
validateUtf8String(
|
|
2567
|
+
input.description,
|
|
2568
|
+
'task description',
|
|
2569
|
+
maxSubagentTaskDescriptionBytes,
|
|
2570
|
+
true,
|
|
2571
|
+
);
|
|
2572
|
+
validateUtf8String(input.prompt, 'task prompt', maxSubagentPromptBytes, true);
|
|
2573
|
+
validateUtf8String(input.subagentType, 'subagentType', maxSubagentNameBytes, true);
|
|
2574
|
+
if (input.taskId !== undefined) {
|
|
2575
|
+
validateUtf8String(input.taskId, 'taskId', maxSubagentTaskIdBytes, true);
|
|
2576
|
+
}
|
|
2577
|
+
validateUtf8String(toolCallId, 'task toolCallId', maxTransferIdentifierBytes, true);
|
|
2578
|
+
const definition = this.subagents.get(input.subagentType);
|
|
2579
|
+
if (!definition || (run.stored.session.depth ?? 0) >= this.maxSubagentDepth) {
|
|
2580
|
+
throw new FlexHarnessValidationError(`Subagent "${input.subagentType}" is not available.`);
|
|
2581
|
+
}
|
|
2582
|
+
const reservedSessionId = input.taskId ?? this.createSubagentSessionId(
|
|
2583
|
+
run.state.storageKey,
|
|
2584
|
+
run.sessionId,
|
|
2585
|
+
run.runId,
|
|
2586
|
+
toolCallId,
|
|
2587
|
+
);
|
|
2588
|
+
if (run.subagentSessionIds.has(reservedSessionId)) {
|
|
2589
|
+
throw new FlexHarnessValidationError(
|
|
2590
|
+
`Subagent task "${reservedSessionId}" has already been acquired by this parent run.`,
|
|
2591
|
+
);
|
|
2592
|
+
}
|
|
2593
|
+
run.subagentSessionIds.add(reservedSessionId);
|
|
2594
|
+
let child: IStoredSessionState | undefined;
|
|
2595
|
+
let childCreated = false;
|
|
2596
|
+
let queued: Awaited<ReturnType<typeof this.enqueuePromptInternal>> | undefined;
|
|
2597
|
+
let admission: IFlexPromptAdmission | undefined;
|
|
2598
|
+
const abortChild = () => {
|
|
2599
|
+
const reason = run.controller.signal.reason instanceof FlexHarnessAbortError
|
|
2600
|
+
? run.controller.signal.reason
|
|
2601
|
+
: this.trustInternalError(new FlexHarnessAbortError('The parent run was aborted.'));
|
|
2602
|
+
const ownedChild = child ?? run.state.sessions.get(reservedSessionId);
|
|
2603
|
+
if (
|
|
2604
|
+
ownedChild
|
|
2605
|
+
&& ownedChild.session.parentSessionId === run.sessionId
|
|
2606
|
+
&& ownedChild.session.agent === definition.name
|
|
2607
|
+
&& run.state.initializingSessions.has(reservedSessionId)
|
|
2608
|
+
) this.abortCompactorLifecycle(ownedChild, reason);
|
|
2609
|
+
const childQueue = queued === undefined
|
|
2610
|
+
? undefined
|
|
2611
|
+
: ownedChild?.outstandingPromptsById.get(queued.admission.queueId);
|
|
2612
|
+
if (childQueue) this.cancelQueuedPrompt(childQueue, reason);
|
|
2613
|
+
else if (admission) {
|
|
2614
|
+
this.abortExactRun(run.state, reservedSessionId, admission.runId, reason);
|
|
2615
|
+
}
|
|
2616
|
+
};
|
|
2617
|
+
run.controller.signal.addEventListener('abort', abortChild, { once: true });
|
|
2618
|
+
if (run.controller.signal.aborted) abortChild();
|
|
2619
|
+
try {
|
|
2620
|
+
await this.requestPermission(run.state, run, {
|
|
2621
|
+
kind: 'subagent.start',
|
|
2622
|
+
description: `Start foreground subagent "${definition.name}": ${input.description}`,
|
|
2623
|
+
toolCallId,
|
|
2624
|
+
metadata: {
|
|
2625
|
+
agent: definition.name,
|
|
2626
|
+
description: input.description,
|
|
2627
|
+
...(input.taskId === undefined ? {} : { taskId: input.taskId }),
|
|
2628
|
+
},
|
|
2629
|
+
});
|
|
2630
|
+
const acquired = await this.acquireSubagentSession(
|
|
2631
|
+
run,
|
|
2632
|
+
definition,
|
|
2633
|
+
toolCallId,
|
|
2634
|
+
input.taskId,
|
|
2635
|
+
reservedSessionId,
|
|
2636
|
+
);
|
|
2637
|
+
child = acquired.stored;
|
|
2638
|
+
childCreated = acquired.created;
|
|
2639
|
+
if (run.controller.signal.aborted) throw run.controller.signal.reason;
|
|
2640
|
+
this.updateSubagentToolPart(run, toolCallId, child.session.sessionId);
|
|
2641
|
+
const childOptions: IFlexPromptOptions = {
|
|
2642
|
+
...(definition.modelHint === undefined ? {} : { modelHint: definition.modelHint }),
|
|
2643
|
+
...(definition.system === undefined ? {} : { system: definition.system }),
|
|
2644
|
+
...(definition.maxSteps === undefined ? {} : { maxSteps: definition.maxSteps }),
|
|
2645
|
+
};
|
|
2646
|
+
queued = await this.enqueuePromptInternal(
|
|
2647
|
+
run.scopeId,
|
|
2648
|
+
child.session.sessionId,
|
|
2649
|
+
input.prompt,
|
|
2650
|
+
childOptions,
|
|
2651
|
+
undefined,
|
|
2652
|
+
undefined,
|
|
2653
|
+
true,
|
|
2654
|
+
run.controller.signal,
|
|
2655
|
+
);
|
|
2656
|
+
admission = await queued.started;
|
|
2657
|
+
const result = await queued.admission.completion;
|
|
2658
|
+
this.updateSubagentToolPart(run, toolCallId, child.session.sessionId, result.model);
|
|
2659
|
+
return {
|
|
2660
|
+
taskId: child.session.sessionId,
|
|
2661
|
+
status: 'completed',
|
|
2662
|
+
text: truncateUtf8(result.assistantMessage.parts
|
|
2663
|
+
.filter((part) => part.type === 'text')
|
|
2664
|
+
.map((part) => part.text)
|
|
2665
|
+
.join(''), maxSubagentResultTextBytes),
|
|
2666
|
+
model: publicSnapshot(result.model),
|
|
2667
|
+
};
|
|
2668
|
+
} catch (error) {
|
|
2669
|
+
if (
|
|
2670
|
+
childCreated
|
|
2671
|
+
&& child
|
|
2672
|
+
&& queued === undefined
|
|
2673
|
+
&& run.state.sessions.get(child.session.sessionId) === child
|
|
2674
|
+
&& !run.state.tombstones.has(child.session.sessionId)
|
|
2675
|
+
) {
|
|
2676
|
+
try {
|
|
2677
|
+
await this.deleteSessionInternal(
|
|
2678
|
+
run.state,
|
|
2679
|
+
run.scopeId,
|
|
2680
|
+
run.scope as TScope,
|
|
2681
|
+
child.session.sessionId,
|
|
2682
|
+
);
|
|
2683
|
+
} catch (cleanupError) {
|
|
2684
|
+
throw combineErrors([error, cleanupError]);
|
|
2685
|
+
}
|
|
2686
|
+
}
|
|
2687
|
+
const childModel = child?.messages
|
|
2688
|
+
.filter((message) => message.runId === admission?.runId && message.role === 'assistant')
|
|
2689
|
+
.at(-1)?.model
|
|
2690
|
+
?? child?.stagedTerminals.find((terminal) => terminal.runId === admission?.runId)?.model;
|
|
2691
|
+
const parentPart = run.callbackParts.find((part) =>
|
|
2692
|
+
part.type === 'tool' && part.toolCallId === toolCallId);
|
|
2693
|
+
if (
|
|
2694
|
+
child
|
|
2695
|
+
&& childModel !== undefined
|
|
2696
|
+
&& parentPart?.type === 'tool'
|
|
2697
|
+
&& parentPart.model === undefined
|
|
2698
|
+
) {
|
|
2699
|
+
this.updateSubagentToolPart(run, toolCallId, child.session.sessionId, childModel);
|
|
2700
|
+
}
|
|
2701
|
+
throw error;
|
|
2702
|
+
} finally {
|
|
2703
|
+
run.controller.signal.removeEventListener('abort', abortChild);
|
|
2704
|
+
}
|
|
2705
|
+
}
|
|
2706
|
+
|
|
2707
|
+
private async acquireSubagentSession(
|
|
2708
|
+
run: IActiveRun,
|
|
2709
|
+
definition: Readonly<IFlexSubagentDefinition>,
|
|
2710
|
+
toolCallId: string,
|
|
2711
|
+
taskId?: string,
|
|
2712
|
+
reservedSessionId?: string,
|
|
2713
|
+
): Promise<IFlexSubagentAcquisition> {
|
|
2714
|
+
const state = run.state;
|
|
2715
|
+
const sessionId = reservedSessionId ?? taskId ?? this.createSubagentSessionId(
|
|
2716
|
+
state.storageKey,
|
|
2717
|
+
run.sessionId,
|
|
2718
|
+
run.runId,
|
|
2719
|
+
toolCallId,
|
|
2720
|
+
);
|
|
2721
|
+
let metadata: IFlexSession | undefined;
|
|
2722
|
+
let placeholder: IStoredSessionState | undefined;
|
|
2723
|
+
let initializationCompletion: Promise<void> | undefined;
|
|
2724
|
+
let resolveInitialization: (() => void) | undefined;
|
|
2725
|
+
let initializationCompleted = false;
|
|
2726
|
+
const completeInitialization = () => {
|
|
2727
|
+
if (initializationCompleted || initializationCompletion === undefined) return;
|
|
2728
|
+
initializationCompleted = true;
|
|
2729
|
+
state.initializingSessions.delete(sessionId);
|
|
2730
|
+
if (state.sessionInitializations.get(sessionId) === initializationCompletion) {
|
|
2731
|
+
state.sessionInitializations.delete(sessionId);
|
|
2732
|
+
}
|
|
2733
|
+
resolveInitialization?.();
|
|
2734
|
+
};
|
|
2735
|
+
try {
|
|
2736
|
+
await this.mutateScope(state, () => {
|
|
2737
|
+
if (state.sessions.get(run.sessionId) !== run.stored || state.tombstones.has(run.sessionId)) {
|
|
2738
|
+
throw new FlexHarnessAbortError('The parent session no longer owns this subagent request.');
|
|
2739
|
+
}
|
|
2740
|
+
if (state.activeRuns.get(run.sessionId) !== run || run.callbacksClosed) {
|
|
2741
|
+
throw new FlexHarnessAbortError('The parent run no longer owns this subagent request.');
|
|
2742
|
+
}
|
|
2743
|
+
if (run.stored.session.agent !== undefined && !this.subagents.has(run.stored.session.agent)) {
|
|
2744
|
+
throw new FlexHarnessValidationError(`Parent subagent "${run.stored.session.agent}" is disabled.`);
|
|
2745
|
+
}
|
|
2746
|
+
if ((run.stored.session.depth ?? 0) >= this.maxSubagentDepth) {
|
|
2747
|
+
throw new FlexHarnessValidationError('The maximum subagent depth has been reached.');
|
|
2748
|
+
}
|
|
2749
|
+
let ancestor: IFlexSession | undefined = run.stored.session;
|
|
2750
|
+
const visited = new Set<string>();
|
|
2751
|
+
while (ancestor) {
|
|
2752
|
+
if (visited.has(ancestor.sessionId) || state.tombstones.has(ancestor.sessionId)) {
|
|
2753
|
+
throw new FlexHarnessValidationError('The subagent ancestor chain is invalid or deleted.');
|
|
2754
|
+
}
|
|
2755
|
+
visited.add(ancestor.sessionId);
|
|
2756
|
+
ancestor = ancestor.parentSessionId
|
|
2757
|
+
? state.sessions.get(ancestor.parentSessionId)?.session
|
|
2758
|
+
: undefined;
|
|
2759
|
+
}
|
|
2760
|
+
const existing = state.sessions.get(sessionId);
|
|
2761
|
+
if (existing) {
|
|
2762
|
+
if (
|
|
2763
|
+
existing.session.parentSessionId !== run.sessionId
|
|
2764
|
+
|| existing.session.agent !== definition.name
|
|
2765
|
+
) {
|
|
2766
|
+
throw new FlexHarnessValidationError(`Task session "${sessionId}" is not owned by this parent and agent.`);
|
|
2767
|
+
}
|
|
2768
|
+
if (
|
|
2769
|
+
taskId === undefined
|
|
2770
|
+
&& (existing.session.parentRunId !== run.runId
|
|
2771
|
+
|| existing.session.parentToolCallId !== toolCallId)
|
|
2772
|
+
) {
|
|
2773
|
+
throw new FlexHarnessValidationError(
|
|
2774
|
+
`Subagent task "${sessionId}" does not match its deterministic invocation origin.`,
|
|
2775
|
+
);
|
|
2776
|
+
}
|
|
2777
|
+
if (state.initializingSessions.has(sessionId)) {
|
|
2778
|
+
throw new FlexHarnessSessionBusyError(sessionId, 'is still being initialized');
|
|
2779
|
+
}
|
|
2780
|
+
if (taskId === undefined && existing.messages.length > 0) {
|
|
2781
|
+
throw new FlexHarnessValidationError(
|
|
2782
|
+
`Subagent task "${sessionId}" has an uncertain prior execution and cannot be replayed automatically.`,
|
|
2783
|
+
);
|
|
2784
|
+
}
|
|
2785
|
+
if (state.activeRuns.has(sessionId) || existing.session.status !== 'idle') {
|
|
2786
|
+
throw new FlexHarnessSessionBusyError(sessionId, 'cannot be resumed while it is not idle');
|
|
2787
|
+
}
|
|
2788
|
+
if (taskId !== undefined && existing.session.parentRunId === run.runId) {
|
|
2789
|
+
throw new FlexHarnessValidationError('taskId can only resume a child from a later parent run.');
|
|
2790
|
+
}
|
|
2791
|
+
placeholder = existing;
|
|
2792
|
+
return;
|
|
2793
|
+
}
|
|
2794
|
+
if (taskId !== undefined || state.tombstones.has(sessionId)) {
|
|
2795
|
+
throw new FlexHarnessNotFoundError('Subagent task', sessionId);
|
|
2796
|
+
}
|
|
2797
|
+
const timestamp = new Date().toISOString();
|
|
2798
|
+
metadata = {
|
|
2799
|
+
scopeId: run.scopeId,
|
|
2800
|
+
sessionId,
|
|
2801
|
+
title: `Subagent: ${definition.name}`,
|
|
2802
|
+
createdAt: timestamp,
|
|
2803
|
+
updatedAt: timestamp,
|
|
2804
|
+
status: 'idle',
|
|
2805
|
+
activity: { status: 'idle' },
|
|
2806
|
+
parentSessionId: run.sessionId,
|
|
2807
|
+
parentRunId: run.runId,
|
|
2808
|
+
parentToolCallId: toolCallId,
|
|
2809
|
+
agent: definition.name,
|
|
2810
|
+
depth: (run.stored.session.depth ?? 0) + 1,
|
|
2811
|
+
};
|
|
2812
|
+
placeholder = this.createUninitializedStoredSession(metadata, state.storageKey);
|
|
2813
|
+
state.sessions.set(sessionId, placeholder);
|
|
2814
|
+
state.initializingSessions.add(sessionId);
|
|
2815
|
+
initializationCompletion = new Promise<void>((resolve) => {
|
|
2816
|
+
resolveInitialization = resolve;
|
|
2817
|
+
});
|
|
2818
|
+
state.sessionInitializations.set(sessionId, initializationCompletion);
|
|
2819
|
+
});
|
|
2820
|
+
if (!metadata) return { stored: placeholder!, created: false };
|
|
2821
|
+
const loaded = await this.loadSessionRuntime(
|
|
2822
|
+
state,
|
|
2823
|
+
metadata,
|
|
2824
|
+
run.scopeId,
|
|
2825
|
+
run.scope as TScope,
|
|
2826
|
+
placeholder!.compactorLifecycleController,
|
|
2827
|
+
);
|
|
2828
|
+
const parentLostOwnership = run.controller.signal.aborted
|
|
2829
|
+
|| state.sessions.get(run.sessionId) !== run.stored
|
|
2830
|
+
|| state.activeRuns.get(run.sessionId) !== run
|
|
2831
|
+
|| run.callbacksClosed;
|
|
2832
|
+
if (
|
|
2833
|
+
parentLostOwnership
|
|
2834
|
+
|| state.sessions.get(sessionId) !== placeholder
|
|
2835
|
+
|| state.tombstones.has(sessionId)
|
|
2836
|
+
) {
|
|
2837
|
+
const aborted = run.controller.signal.aborted
|
|
2838
|
+
? run.controller.signal.reason
|
|
2839
|
+
: new FlexHarnessAbortError('The subagent session lost parent ownership during initialization.');
|
|
2840
|
+
try {
|
|
2841
|
+
await this.closeStoredSession(loaded);
|
|
2842
|
+
} catch (error) {
|
|
2843
|
+
this.orphanedStoredSessions.add(loaded);
|
|
2844
|
+
throw combineErrors([aborted, error]);
|
|
2845
|
+
}
|
|
2846
|
+
throw aborted;
|
|
2847
|
+
}
|
|
2848
|
+
state.sessions.set(sessionId, loaded);
|
|
2849
|
+
this.emitEvent(run.scopeId, sessionId, {
|
|
2850
|
+
type: 'session.created',
|
|
2851
|
+
session: publicSnapshot(metadata),
|
|
2852
|
+
});
|
|
2853
|
+
return { stored: loaded, created: true };
|
|
2854
|
+
} catch (error) {
|
|
2855
|
+
if (metadata === undefined) throw error;
|
|
2856
|
+
if (
|
|
2857
|
+
state.tombstones.has(sessionId)
|
|
2858
|
+
&& placeholder?.compactorLifecycleController.signal.aborted
|
|
2859
|
+
) {
|
|
2860
|
+
throw placeholder.compactorLifecycleController.signal.reason;
|
|
2861
|
+
}
|
|
2862
|
+
const projected = this.projectExternalError(run, error, 'agentSession');
|
|
2863
|
+
if (
|
|
2864
|
+
placeholder === undefined
|
|
2865
|
+
|| (state.sessions.get(sessionId) !== placeholder && !state.tombstones.has(sessionId))
|
|
2866
|
+
) throw projected;
|
|
2867
|
+
if (state.tombstones.has(sessionId)) throw projected;
|
|
2868
|
+
try {
|
|
2869
|
+
await this.mutateScope(state, () => {
|
|
2870
|
+
if (state.sessions.get(sessionId) !== placeholder) return;
|
|
2871
|
+
state.sessions.delete(sessionId);
|
|
2872
|
+
state.tombstones.set(sessionId, {
|
|
2873
|
+
sessionId,
|
|
2874
|
+
deletedAt: new Date().toISOString(),
|
|
2875
|
+
rootSessionId: sessionId,
|
|
2876
|
+
depth: 0,
|
|
2877
|
+
parentSessionId: run.sessionId,
|
|
2878
|
+
});
|
|
2879
|
+
}, true);
|
|
2880
|
+
completeInitialization();
|
|
2881
|
+
await this.finishTombstoneCleanup(
|
|
2882
|
+
state,
|
|
2883
|
+
sessionId,
|
|
2884
|
+
run.scopeId,
|
|
2885
|
+
{ scopeId: run.scopeId, scope: run.scope as TScope },
|
|
2886
|
+
);
|
|
2887
|
+
} catch (cleanupError) {
|
|
2888
|
+
const combined = combineErrors([
|
|
2889
|
+
projected,
|
|
2890
|
+
this.projectExternalError(run, cleanupError, 'persistence'),
|
|
2891
|
+
]);
|
|
2892
|
+
this.deferNamespaceDrain(
|
|
2893
|
+
state,
|
|
2894
|
+
combined,
|
|
2895
|
+
{ scopeId: run.scopeId, scope: run.scope as TScope },
|
|
2896
|
+
);
|
|
2897
|
+
throw combined;
|
|
2898
|
+
}
|
|
2899
|
+
throw projected;
|
|
2900
|
+
} finally {
|
|
2901
|
+
completeInitialization();
|
|
2902
|
+
}
|
|
2903
|
+
}
|
|
2904
|
+
|
|
2905
|
+
private createSubagentSessionId(
|
|
2906
|
+
storageKey: string,
|
|
2907
|
+
parentSessionId: string,
|
|
2908
|
+
parentRunId: string,
|
|
2909
|
+
parentToolCallId: string,
|
|
2910
|
+
): string {
|
|
2911
|
+
return `subagent_${plugins.crypto.createHash('sha256').update(JSON.stringify([
|
|
2912
|
+
'flexharness-subagent-v1',
|
|
2913
|
+
storageKey,
|
|
2914
|
+
parentSessionId,
|
|
2915
|
+
parentRunId,
|
|
2916
|
+
parentToolCallId,
|
|
2917
|
+
])).digest('hex')}`;
|
|
2918
|
+
}
|
|
2919
|
+
|
|
2920
|
+
private updateSubagentToolPart(
|
|
2921
|
+
run: IActiveRun,
|
|
2922
|
+
toolCallId: string,
|
|
2923
|
+
childSessionId: string,
|
|
2924
|
+
model?: IFlexModelIdentity,
|
|
2925
|
+
): void {
|
|
2926
|
+
const partId = run.toolPartIds.get(toolCallId);
|
|
2927
|
+
const part = run.callbackParts.find((entry) => entry.partId === partId && entry.type === 'tool');
|
|
2928
|
+
if (!part || part.type !== 'tool' || part.status !== 'running') {
|
|
2929
|
+
throw new FlexHarnessValidationError(`Running task part "${toolCallId}" is unavailable.`);
|
|
2930
|
+
}
|
|
2931
|
+
const bytes = Buffer.byteLength(childSessionId, 'utf8')
|
|
2932
|
+
+ (model === undefined ? 0 : jsonBytes(model));
|
|
2933
|
+
if (!this.reserveCallbackCapacity(run, 1, bytes, 0)) {
|
|
2934
|
+
throw run.callbackError ?? new FlexHarnessCallbackOverflowError('Task metadata exceeded callback limits.');
|
|
2935
|
+
}
|
|
2936
|
+
part.childSessionId = childSessionId;
|
|
2937
|
+
if (model !== undefined) part.model = cloneSerializable(model);
|
|
2938
|
+
this.emitPartEvent(run, 'part.updated', part);
|
|
2939
|
+
}
|
|
2940
|
+
|
|
2941
|
+
private abortExactRun(
|
|
2942
|
+
state: IStorageState,
|
|
2943
|
+
sessionId: string,
|
|
2944
|
+
runId: string,
|
|
2945
|
+
reason: unknown,
|
|
2946
|
+
): boolean {
|
|
2947
|
+
const active = state.activeRuns.get(sessionId);
|
|
2948
|
+
if (!active || active.runId !== runId || active.phase === 'finalizing' || active.phase === 'promoting') {
|
|
2949
|
+
return false;
|
|
2950
|
+
}
|
|
2951
|
+
const cancellation = reason instanceof FlexHarnessAbortError
|
|
2952
|
+
? reason
|
|
2953
|
+
: this.trustInternalError(new FlexHarnessAbortError('The parent run was aborted.'));
|
|
2954
|
+
this.cancelRun(active, cancellation);
|
|
2955
|
+
return true;
|
|
2956
|
+
}
|
|
2957
|
+
|
|
2250
2958
|
private createReservation(
|
|
2251
2959
|
run: IActiveRun,
|
|
2252
2960
|
prompt: INormalizedFlexPrompt,
|
|
@@ -3020,6 +3728,161 @@ export class FlexHarness<TScope = unknown> {
|
|
|
3020
3728
|
}
|
|
3021
3729
|
}
|
|
3022
3730
|
|
|
3731
|
+
private async provideRunToolHandle(
|
|
3732
|
+
run: IActiveRun,
|
|
3733
|
+
context: Readonly<IFlexToolProviderContext<TScope>>,
|
|
3734
|
+
): Promise<IFlexToolHandle | undefined> {
|
|
3735
|
+
if (!this.resourceToolProviderResolver) return this.toolProvider?.provideTools(context);
|
|
3736
|
+
|
|
3737
|
+
const resolverContext = Object.freeze({
|
|
3738
|
+
scopeId: context.scopeId,
|
|
3739
|
+
scope: context.scope,
|
|
3740
|
+
sessionId: context.sessionId,
|
|
3741
|
+
runId: context.runId,
|
|
3742
|
+
...(context.parentSessionId === undefined ? {} : { parentSessionId: context.parentSessionId }),
|
|
3743
|
+
...(context.agent === undefined ? {} : { agent: context.agent }),
|
|
3744
|
+
signal: context.signal,
|
|
3745
|
+
});
|
|
3746
|
+
const resolved = await this.resourceToolProviderResolver.resolveResourceToolProviders(resolverContext);
|
|
3747
|
+
const descriptors = this.normalizeResourceToolProviderDescriptors(resolved);
|
|
3748
|
+
const owned: Array<{ handle: IFlexToolHandle; closed: boolean }> = [];
|
|
3749
|
+
const tools: Record<string, unknown> = Object.create(null) as Record<string, unknown>;
|
|
3750
|
+
const closeOwned = async (): Promise<void> => {
|
|
3751
|
+
const errors: unknown[] = [];
|
|
3752
|
+
for (const entry of [...owned].reverse()) {
|
|
3753
|
+
if (entry.closed || !entry.handle.close) continue;
|
|
3754
|
+
try {
|
|
3755
|
+
await entry.handle.close();
|
|
3756
|
+
entry.closed = true;
|
|
3757
|
+
} catch (error) {
|
|
3758
|
+
errors.push(error);
|
|
3759
|
+
}
|
|
3760
|
+
}
|
|
3761
|
+
if (errors.length > 0) throw combineErrors(errors);
|
|
3762
|
+
};
|
|
3763
|
+
try {
|
|
3764
|
+
context.signal.throwIfAborted();
|
|
3765
|
+
const applicationHandle = this.normalizeToolHandle(
|
|
3766
|
+
run,
|
|
3767
|
+
await this.toolProvider?.provideTools(context),
|
|
3768
|
+
);
|
|
3769
|
+
if (applicationHandle) {
|
|
3770
|
+
owned.push({ handle: applicationHandle, closed: false });
|
|
3771
|
+
for (const [toolName, tool] of Object.entries(applicationHandle.tools)) {
|
|
3772
|
+
tools[toolName] = tool;
|
|
3773
|
+
}
|
|
3774
|
+
}
|
|
3775
|
+
for (const descriptor of descriptors) {
|
|
3776
|
+
context.signal.throwIfAborted();
|
|
3777
|
+
const namespace = createFlexResourceToolNamespace(
|
|
3778
|
+
descriptor.resourceId,
|
|
3779
|
+
descriptor.attachmentRevision,
|
|
3780
|
+
);
|
|
3781
|
+
const resourceIdentity = createFlexResourceIdentityDigest(
|
|
3782
|
+
descriptor.resourceId,
|
|
3783
|
+
descriptor.attachmentRevision,
|
|
3784
|
+
);
|
|
3785
|
+
const resourceContext = Object.freeze({
|
|
3786
|
+
...context,
|
|
3787
|
+
requestPermission: (request: IFlexPermissionRequestInput) => context.requestPermission({
|
|
3788
|
+
...request,
|
|
3789
|
+
kind: `resource.${resourceIdentity}.${request.kind}`,
|
|
3790
|
+
...(request.rememberKey === undefined
|
|
3791
|
+
? {}
|
|
3792
|
+
: { rememberKey: `resource:${resourceIdentity}:${request.rememberKey}` }),
|
|
3793
|
+
metadata: {
|
|
3794
|
+
resourceId: descriptor.resourceId,
|
|
3795
|
+
attachmentRevision: descriptor.attachmentRevision,
|
|
3796
|
+
resourceIdentity,
|
|
3797
|
+
toolNamespace: namespace,
|
|
3798
|
+
...(request.metadata === undefined ? {} : { providerMetadata: request.metadata }),
|
|
3799
|
+
},
|
|
3800
|
+
}),
|
|
3801
|
+
});
|
|
3802
|
+
const handle = this.normalizeToolHandle(
|
|
3803
|
+
run,
|
|
3804
|
+
await descriptor.provider.provideTools(resourceContext),
|
|
3805
|
+
);
|
|
3806
|
+
if (!handle) continue;
|
|
3807
|
+
owned.push({ handle, closed: false });
|
|
3808
|
+
for (const [toolName, tool] of Object.entries(handle.tools)) {
|
|
3809
|
+
const exposedName = createFlexResourceToolName(namespace, toolName);
|
|
3810
|
+
if (Object.prototype.hasOwnProperty.call(tools, exposedName)) {
|
|
3811
|
+
throw new FlexHarnessValidationError(`Duplicate exposed tool "${exposedName}".`);
|
|
3812
|
+
}
|
|
3813
|
+
tools[exposedName] = tool;
|
|
3814
|
+
}
|
|
3815
|
+
}
|
|
3816
|
+
if (owned.length === 0 && Object.keys(tools).length === 0) return undefined;
|
|
3817
|
+
return {
|
|
3818
|
+
tools: tools as TFlexAgentToolSet,
|
|
3819
|
+
...(owned.some((entry) => entry.handle.close) ? { close: closeOwned } : {}),
|
|
3820
|
+
};
|
|
3821
|
+
} catch (error) {
|
|
3822
|
+
if (owned.some((entry) => entry.handle.close && !entry.closed)) {
|
|
3823
|
+
try {
|
|
3824
|
+
await this.closePartialToolHandle({ tools: {}, close: closeOwned }, run);
|
|
3825
|
+
} catch (cleanupError) {
|
|
3826
|
+
throw combineErrors([error, cleanupError]);
|
|
3827
|
+
}
|
|
3828
|
+
}
|
|
3829
|
+
throw error;
|
|
3830
|
+
}
|
|
3831
|
+
}
|
|
3832
|
+
|
|
3833
|
+
private normalizeResourceToolProviderDescriptors(
|
|
3834
|
+
descriptors: readonly IFlexResourceToolProviderDescriptor<TScope>[],
|
|
3835
|
+
): IFlexResourceToolProviderDescriptor<TScope>[] {
|
|
3836
|
+
if (!Array.isArray(descriptors) || descriptors.length > maxResourceToolProviders) {
|
|
3837
|
+
throw new FlexHarnessValidationError(
|
|
3838
|
+
`Resource tool providers must be an array with at most ${maxResourceToolProviders} descriptors.`,
|
|
3839
|
+
);
|
|
3840
|
+
}
|
|
3841
|
+
const resourceIds = new Set<string>();
|
|
3842
|
+
const namespaces = new Set<string>();
|
|
3843
|
+
return descriptors.map((descriptor, index) => {
|
|
3844
|
+
if (
|
|
3845
|
+
!descriptor
|
|
3846
|
+
|| typeof descriptor !== 'object'
|
|
3847
|
+
|| Array.isArray(descriptor)
|
|
3848
|
+
|| ![Object.prototype, null].includes(Object.getPrototypeOf(descriptor))
|
|
3849
|
+
) {
|
|
3850
|
+
throw new FlexHarnessValidationError(`Resource tool provider descriptor ${index} is invalid.`);
|
|
3851
|
+
}
|
|
3852
|
+
const unsupported = Object.keys(descriptor).find(
|
|
3853
|
+
(key) => !['resourceId', 'attachmentRevision', 'provider'].includes(key),
|
|
3854
|
+
);
|
|
3855
|
+
if (unsupported) {
|
|
3856
|
+
throw new FlexHarnessValidationError(
|
|
3857
|
+
`Resource tool provider descriptor ${index} does not support "${unsupported}".`,
|
|
3858
|
+
);
|
|
3859
|
+
}
|
|
3860
|
+
const resourceId = descriptor.resourceId;
|
|
3861
|
+
const attachmentRevision = descriptor.attachmentRevision;
|
|
3862
|
+
const provider = descriptor.provider;
|
|
3863
|
+
const provideTools = provider?.provideTools;
|
|
3864
|
+
const namespace = createFlexResourceToolNamespace(resourceId, attachmentRevision);
|
|
3865
|
+
if (resourceIds.has(resourceId)) {
|
|
3866
|
+
throw new FlexHarnessValidationError(`Duplicate resourceId "${resourceId}".`);
|
|
3867
|
+
}
|
|
3868
|
+
if (namespaces.has(namespace)) {
|
|
3869
|
+
throw new FlexHarnessValidationError(`Duplicate resource tool namespace "${namespace}".`);
|
|
3870
|
+
}
|
|
3871
|
+
if (!provider || typeof provideTools !== 'function') {
|
|
3872
|
+
throw new FlexHarnessValidationError(`Resource tool provider descriptor ${index} has no tool provider.`);
|
|
3873
|
+
}
|
|
3874
|
+
resourceIds.add(resourceId);
|
|
3875
|
+
namespaces.add(namespace);
|
|
3876
|
+
return {
|
|
3877
|
+
resourceId,
|
|
3878
|
+
attachmentRevision,
|
|
3879
|
+
provider: {
|
|
3880
|
+
provideTools: (context) => provideTools.call(provider, context),
|
|
3881
|
+
},
|
|
3882
|
+
};
|
|
3883
|
+
});
|
|
3884
|
+
}
|
|
3885
|
+
|
|
3023
3886
|
private async closePartialToolHandle(handle: IFlexToolHandle, run: IActiveRun): Promise<void> {
|
|
3024
3887
|
try {
|
|
3025
3888
|
await handle.close?.();
|
|
@@ -3224,10 +4087,37 @@ export class FlexHarness<TScope = unknown> {
|
|
|
3224
4087
|
scopeChanged = (await this.repairLoadedSession(state.storageKey, stored)) || scopeChanged;
|
|
3225
4088
|
if (stored.projectionRevision < 0) throw new Error('Invalid projection revision.');
|
|
3226
4089
|
}
|
|
3227
|
-
for (const
|
|
4090
|
+
for (const stored of state.sessions.values()) {
|
|
4091
|
+
let ancestorId = stored.session.parentSessionId;
|
|
4092
|
+
const visited = new Set<string>([stored.session.sessionId]);
|
|
4093
|
+
while (ancestorId) {
|
|
4094
|
+
if (visited.has(ancestorId)) {
|
|
4095
|
+
throw new FlexHarnessValidationError(
|
|
4096
|
+
`Live session "${stored.session.sessionId}" has a cyclic ancestor chain.`,
|
|
4097
|
+
);
|
|
4098
|
+
}
|
|
4099
|
+
visited.add(ancestorId);
|
|
4100
|
+
if (state.tombstones.has(ancestorId)) {
|
|
4101
|
+
throw new FlexHarnessValidationError(
|
|
4102
|
+
`Live session "${stored.session.sessionId}" descends from tombstoned ancestor "${ancestorId}".`,
|
|
4103
|
+
);
|
|
4104
|
+
}
|
|
4105
|
+
ancestorId = state.sessions.get(ancestorId)?.session.parentSessionId;
|
|
4106
|
+
}
|
|
4107
|
+
}
|
|
4108
|
+
const tombstoneRoots = this.orderTombstoneRootsChildFirst(
|
|
4109
|
+
state,
|
|
4110
|
+
[...new Set([...state.tombstones.values()].map((tombstone) =>
|
|
4111
|
+
tombstone.rootSessionId ?? tombstone.sessionId))],
|
|
4112
|
+
);
|
|
4113
|
+
for (const rootSessionId of tombstoneRoots) {
|
|
4114
|
+
if (this.descendantTombstoneRoots(state, rootSessionId).length > 0) continue;
|
|
3228
4115
|
try {
|
|
3229
|
-
|
|
3230
|
-
|
|
4116
|
+
const group = this.tombstoneGroup(state, rootSessionId);
|
|
4117
|
+
for (const tombstone of group) {
|
|
4118
|
+
await this.cleanupSessionDomains(storageKey, tombstone.sessionId);
|
|
4119
|
+
}
|
|
4120
|
+
for (const tombstone of group) state.tombstones.delete(tombstone.sessionId);
|
|
3231
4121
|
scopeChanged = true;
|
|
3232
4122
|
} catch {
|
|
3233
4123
|
// A retained tombstone is retried by the next load or explicit delete call.
|
|
@@ -3260,6 +4150,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
3260
4150
|
private createUninitializedStoredSession(
|
|
3261
4151
|
session: IFlexSession,
|
|
3262
4152
|
storageKey: string,
|
|
4153
|
+
compactorLifecycleController = new AbortController(),
|
|
3263
4154
|
): IStoredSessionState {
|
|
3264
4155
|
const unavailable = new Proxy({} as plugins.IAgentSession, {
|
|
3265
4156
|
get() {
|
|
@@ -3283,7 +4174,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
3283
4174
|
agentEventStoreReleased: true,
|
|
3284
4175
|
executionContextCloseCompleted: true,
|
|
3285
4176
|
jobStoreReleased: true,
|
|
3286
|
-
compactorLifecycleController
|
|
4177
|
+
compactorLifecycleController,
|
|
3287
4178
|
promptQueue: [],
|
|
3288
4179
|
outstandingPromptsById: new Map(),
|
|
3289
4180
|
terminalPromptQueueEntries: new Map(),
|
|
@@ -3296,6 +4187,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
3296
4187
|
metadata: IFlexSession,
|
|
3297
4188
|
scopeId: string,
|
|
3298
4189
|
scope: TScope,
|
|
4190
|
+
compactorLifecycleController = new AbortController(),
|
|
3299
4191
|
): Promise<IStoredSessionState> {
|
|
3300
4192
|
const sessionId = metadata.sessionId;
|
|
3301
4193
|
const compactorContext = this.createCompactorContext(
|
|
@@ -3380,7 +4272,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
3380
4272
|
executionContextCloseCompleted: executionContextHandle?.close === undefined,
|
|
3381
4273
|
jobStoreReleased: this.stores.jobs.releaseSession === undefined,
|
|
3382
4274
|
jobs: executionContextHandle?.context.jobs,
|
|
3383
|
-
compactorLifecycleController
|
|
4275
|
+
compactorLifecycleController,
|
|
3384
4276
|
promptQueue: [],
|
|
3385
4277
|
outstandingPromptsById: new Map(),
|
|
3386
4278
|
terminalPromptQueueEntries: new Map(),
|
|
@@ -3927,50 +4819,136 @@ export class FlexHarness<TScope = unknown> {
|
|
|
3927
4819
|
if (errors.length > 0) throw combineErrors(errors);
|
|
3928
4820
|
}
|
|
3929
4821
|
|
|
4822
|
+
private collectSessionSubtree(
|
|
4823
|
+
state: IStorageState,
|
|
4824
|
+
rootSessionId: string,
|
|
4825
|
+
): IStoredSessionState[] {
|
|
4826
|
+
const subtree: IStoredSessionState[] = [];
|
|
4827
|
+
const pending = [rootSessionId];
|
|
4828
|
+
const seen = new Set<string>();
|
|
4829
|
+
while (pending.length > 0) {
|
|
4830
|
+
const sessionId = pending.pop()!;
|
|
4831
|
+
if (seen.has(sessionId)) {
|
|
4832
|
+
throw new FlexHarnessValidationError('Session relationships contain a cycle.');
|
|
4833
|
+
}
|
|
4834
|
+
seen.add(sessionId);
|
|
4835
|
+
const stored = state.sessions.get(sessionId);
|
|
4836
|
+
if (!stored) continue;
|
|
4837
|
+
subtree.push(stored);
|
|
4838
|
+
const children = [...state.sessions.values()]
|
|
4839
|
+
.filter((candidate) => candidate.session.parentSessionId === sessionId)
|
|
4840
|
+
.map((candidate) => candidate.session.sessionId)
|
|
4841
|
+
.sort()
|
|
4842
|
+
.reverse();
|
|
4843
|
+
pending.push(...children);
|
|
4844
|
+
}
|
|
4845
|
+
return subtree;
|
|
4846
|
+
}
|
|
4847
|
+
|
|
4848
|
+
private tombstoneGroup(
|
|
4849
|
+
state: IStorageState,
|
|
4850
|
+
rootSessionId: string,
|
|
4851
|
+
): IFlexSessionTombstone[] {
|
|
4852
|
+
return [...state.tombstones.values()]
|
|
4853
|
+
.filter((tombstone) =>
|
|
4854
|
+
(tombstone.rootSessionId ?? tombstone.sessionId) === rootSessionId)
|
|
4855
|
+
.sort((left, right) =>
|
|
4856
|
+
(right.depth ?? 0) - (left.depth ?? 0)
|
|
4857
|
+
|| left.sessionId.localeCompare(right.sessionId));
|
|
4858
|
+
}
|
|
4859
|
+
|
|
4860
|
+
private descendantTombstoneRoots(
|
|
4861
|
+
state: IStorageState,
|
|
4862
|
+
ancestorSessionId: string,
|
|
4863
|
+
): string[] {
|
|
4864
|
+
return [...new Set([...state.tombstones.values()]
|
|
4865
|
+
.map((tombstone) => tombstone.rootSessionId ?? tombstone.sessionId)
|
|
4866
|
+
.filter((rootSessionId) =>
|
|
4867
|
+
rootSessionId !== ancestorSessionId
|
|
4868
|
+
&& this.isSessionAncestor(state, ancestorSessionId, rootSessionId)))]
|
|
4869
|
+
.sort((left, right) =>
|
|
4870
|
+
this.sessionAncestryDepth(state, right) - this.sessionAncestryDepth(state, left)
|
|
4871
|
+
|| left.localeCompare(right));
|
|
4872
|
+
}
|
|
4873
|
+
|
|
4874
|
+
private orderTombstoneRootsChildFirst(
|
|
4875
|
+
state: IStorageState,
|
|
4876
|
+
rootSessionIds: readonly string[],
|
|
4877
|
+
): string[] {
|
|
4878
|
+
return [...rootSessionIds].sort((left, right) =>
|
|
4879
|
+
this.sessionAncestryDepth(state, right) - this.sessionAncestryDepth(state, left)
|
|
4880
|
+
|| left.localeCompare(right));
|
|
4881
|
+
}
|
|
4882
|
+
|
|
3930
4883
|
private finishTombstoneCleanup(
|
|
3931
4884
|
state: IStorageState,
|
|
3932
|
-
|
|
4885
|
+
rootSessionId: string,
|
|
3933
4886
|
scopeId: string,
|
|
3934
|
-
|
|
4887
|
+
invocation?: { scopeId: string; scope: TScope },
|
|
3935
4888
|
): Promise<void> {
|
|
3936
|
-
const existing = state.tombstoneCleanups.get(
|
|
4889
|
+
const existing = state.tombstoneCleanups.get(rootSessionId);
|
|
3937
4890
|
if (existing) return existing;
|
|
3938
4891
|
let cleanup!: Promise<void>;
|
|
3939
|
-
cleanup = this.finishTombstoneCleanupInternal(
|
|
3940
|
-
|
|
3941
|
-
|
|
4892
|
+
cleanup = this.finishTombstoneCleanupInternal(
|
|
4893
|
+
state,
|
|
4894
|
+
rootSessionId,
|
|
4895
|
+
scopeId,
|
|
4896
|
+
invocation,
|
|
4897
|
+
).finally(() => {
|
|
4898
|
+
if (state.tombstoneCleanups.get(rootSessionId) === cleanup) {
|
|
4899
|
+
state.tombstoneCleanups.delete(rootSessionId);
|
|
3942
4900
|
}
|
|
3943
4901
|
});
|
|
3944
|
-
state.tombstoneCleanups.set(
|
|
4902
|
+
state.tombstoneCleanups.set(rootSessionId, cleanup);
|
|
3945
4903
|
return cleanup;
|
|
3946
4904
|
}
|
|
3947
4905
|
|
|
3948
4906
|
private async finishTombstoneCleanupInternal(
|
|
3949
4907
|
state: IStorageState,
|
|
3950
|
-
|
|
4908
|
+
rootSessionId: string,
|
|
3951
4909
|
scopeId: string,
|
|
3952
|
-
|
|
4910
|
+
invocation?: { scopeId: string; scope: TScope },
|
|
3953
4911
|
): Promise<void> {
|
|
3954
|
-
const
|
|
3955
|
-
|
|
4912
|
+
for (const descendantRoot of this.descendantTombstoneRoots(state, rootSessionId)) {
|
|
4913
|
+
await this.finishTombstoneCleanup(state, descendantRoot, scopeId, invocation);
|
|
4914
|
+
}
|
|
4915
|
+
const group = this.tombstoneGroup(state, rootSessionId);
|
|
4916
|
+
if (group.length === 0) return;
|
|
4917
|
+
const groupIds = new Set(group.map((tombstone) => tombstone.sessionId));
|
|
4918
|
+
const reason = this.trustInternalError(new FlexHarnessAbortError('The session subtree was deleted.'));
|
|
4919
|
+
const contextFor = (
|
|
4920
|
+
sessionId: string,
|
|
4921
|
+
retained?: IRetainedSessionCleanup,
|
|
4922
|
+
): IFlexAgentContextInvocation<TScope> | undefined => invocation
|
|
4923
|
+
? this.createCompactorContext(
|
|
4924
|
+
invocation.scopeId,
|
|
4925
|
+
invocation.scope,
|
|
4926
|
+
state.storageKey,
|
|
4927
|
+
sessionId,
|
|
4928
|
+
)
|
|
4929
|
+
: retained?.stored.compactorContext as IFlexAgentContextInvocation<TScope> | undefined;
|
|
4930
|
+
for (const sessionId of groupIds) {
|
|
4931
|
+
const retained = state.retainedSessionCleanups.get(sessionId);
|
|
4932
|
+
const context = contextFor(sessionId, retained);
|
|
4933
|
+
if (retained) {
|
|
4934
|
+
this.abortCompactorLifecycle(retained.stored, reason);
|
|
4935
|
+
this.cancelStoredPromptQueue(retained.stored, reason, undefined, context);
|
|
4936
|
+
}
|
|
4937
|
+
const run = state.activeRuns.get(sessionId);
|
|
4938
|
+
if (run) this.cancelRun(run, reason, context);
|
|
4939
|
+
}
|
|
4940
|
+
await Promise.allSettled([...groupIds]
|
|
4941
|
+
.map((sessionId) => state.sessionInitializations.get(sessionId))
|
|
4942
|
+
.filter((completion): completion is Promise<void> => completion !== undefined));
|
|
4943
|
+
for (const tombstone of group) {
|
|
4944
|
+
const sessionId = tombstone.sessionId;
|
|
4945
|
+
const retained = state.retainedSessionCleanups.get(sessionId);
|
|
3956
4946
|
const errors: unknown[] = [];
|
|
3957
|
-
const reason = this.trustInternalError(new FlexHarnessAbortError('The session was deleted.'));
|
|
3958
|
-
const invocation = scope === undefined
|
|
3959
|
-
? retained.stored.compactorContext
|
|
3960
|
-
: this.createCompactorContext(scopeId, scope, state.storageKey, sessionId);
|
|
3961
|
-
this.abortCompactorLifecycle(retained.stored, reason);
|
|
3962
|
-
this.cancelStoredPromptQueue(retained.stored, reason, undefined, invocation);
|
|
3963
4947
|
const run = state.activeRuns.get(sessionId);
|
|
3964
|
-
|
|
3965
|
-
if (
|
|
3966
|
-
const settled = await Promise.allSettled([run.completion]);
|
|
3967
|
-
if (settled[0].status === 'rejected') {
|
|
3968
|
-
this.appendUnexpectedErrors(errors, settled[0].reason);
|
|
3969
|
-
}
|
|
3970
|
-
}
|
|
3971
|
-
if (!retained.stored.agentSessionAbortCompleted) {
|
|
4948
|
+
const context = contextFor(sessionId, retained);
|
|
4949
|
+
if (retained && !retained.stored.agentSessionAbortCompleted) {
|
|
3972
4950
|
try {
|
|
3973
|
-
await this.abortStoredSession(retained.stored, reason,
|
|
4951
|
+
await this.abortStoredSession(retained.stored, reason, context);
|
|
3974
4952
|
} catch (error) {
|
|
3975
4953
|
errors.push(this.projectOperationError(
|
|
3976
4954
|
error,
|
|
@@ -3981,53 +4959,66 @@ export class FlexHarness<TScope = unknown> {
|
|
|
3981
4959
|
));
|
|
3982
4960
|
}
|
|
3983
4961
|
}
|
|
3984
|
-
if (
|
|
3985
|
-
const settled = await Promise.allSettled([
|
|
4962
|
+
if (run) {
|
|
4963
|
+
const settled = await Promise.allSettled([run.completion]);
|
|
3986
4964
|
if (settled[0].status === 'rejected') {
|
|
3987
4965
|
this.appendUnexpectedErrors(errors, settled[0].reason);
|
|
3988
4966
|
}
|
|
3989
4967
|
}
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4968
|
+
if (retained) {
|
|
4969
|
+
if (retained.stored.promptQueueDrain) {
|
|
4970
|
+
const settled = await Promise.allSettled([retained.stored.promptQueueDrain]);
|
|
4971
|
+
if (settled[0].status === 'rejected') errors.push(settled[0].reason);
|
|
4972
|
+
}
|
|
4973
|
+
try {
|
|
4974
|
+
await this.closeStoredSession(retained.stored, context);
|
|
4975
|
+
} catch (error) {
|
|
4976
|
+
this.appendUnexpectedErrors(errors, this.projectOperationError(
|
|
4977
|
+
error,
|
|
4978
|
+
'toolCleanup',
|
|
4979
|
+
scopeId,
|
|
4980
|
+
sessionId,
|
|
4981
|
+
'session-delete',
|
|
4982
|
+
));
|
|
4983
|
+
}
|
|
4000
4984
|
}
|
|
4001
4985
|
if (errors.length > 0) {
|
|
4002
|
-
if (
|
|
4003
|
-
|
|
4004
|
-
|
|
4986
|
+
if (
|
|
4987
|
+
retained
|
|
4988
|
+
&& state.lifecycle === 'retired'
|
|
4989
|
+
&& !this.storedSessionCleanupCompleted(retained.stored)
|
|
4990
|
+
) this.orphanedStoredSessions.add(retained.stored);
|
|
4005
4991
|
throw combineErrors(errors);
|
|
4006
4992
|
}
|
|
4007
|
-
this.purgeStoredPromptQueue(retained.stored);
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
|
|
4993
|
+
if (retained) this.purgeStoredPromptQueue(retained.stored);
|
|
4994
|
+
if (!retained?.domainsCompleted) {
|
|
4995
|
+
if (this.hasOrphanedSessionResources(state.storageKey, sessionId)) {
|
|
4996
|
+
throw new Error(`Session "${sessionId}" still has runtime cleanup ownership.`);
|
|
4997
|
+
}
|
|
4998
|
+
await this.cleanupSessionDomains(state.storageKey, sessionId);
|
|
4999
|
+
if (retained) retained.domainsCompleted = true;
|
|
4012
5000
|
}
|
|
4013
|
-
await this.cleanupSessionDomains(state.storageKey, sessionId);
|
|
4014
|
-
if (retained) retained.domainsCompleted = true;
|
|
4015
5001
|
}
|
|
4016
5002
|
await this.mutateScope(state, () => {
|
|
4017
|
-
|
|
4018
|
-
|
|
5003
|
+
for (const tombstone of group) {
|
|
5004
|
+
state.tombstones.delete(tombstone.sessionId);
|
|
5005
|
+
state.retainedSessionCleanups.delete(tombstone.sessionId);
|
|
5006
|
+
}
|
|
4019
5007
|
}, true);
|
|
4020
5008
|
}
|
|
4021
5009
|
|
|
4022
5010
|
private async fenceNamespace(state: IStorageState, currentRun: IActiveRun, cause: unknown): Promise<void> {
|
|
4023
5011
|
if (state.lifecycle === 'retired') return;
|
|
5012
|
+
if (this.storageDrains.has(state.storageKey)) {
|
|
5013
|
+
state.fenceAdditionalErrors.push(cause);
|
|
5014
|
+
return;
|
|
5015
|
+
}
|
|
4024
5016
|
if (state.fenceInProgress) {
|
|
4025
5017
|
state.fenceAdditionalErrors.push(cause);
|
|
4026
5018
|
return;
|
|
4027
5019
|
}
|
|
4028
5020
|
state.fenceInProgress = true;
|
|
4029
5021
|
state.lifecycle = 'fenced';
|
|
4030
|
-
const detachedCleanupAttempts = this.beginDetachedCleanupSettlement(state);
|
|
4031
5022
|
const reason = this.trustInternalError(new FlexHarnessAbortError('The session namespace was fenced.'));
|
|
4032
5023
|
if (!state.compactorLifecycleController.signal.aborted) {
|
|
4033
5024
|
state.compactorLifecycleController.abort(reason);
|
|
@@ -4056,23 +5047,49 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4056
5047
|
}
|
|
4057
5048
|
const otherRuns = [...state.activeRuns.values()].filter((run) => run !== currentRun);
|
|
4058
5049
|
for (const run of otherRuns) this.cancelRun(run, reason, contextFor(run.sessionId));
|
|
4059
|
-
const
|
|
5050
|
+
const dependentRuns = otherRuns.filter((run) =>
|
|
5051
|
+
this.sessionsAreDependencyRelated(state, currentRun.sessionId, run.sessionId));
|
|
5052
|
+
const independentRuns = otherRuns.filter((run) => !dependentRuns.includes(run));
|
|
5053
|
+
const runResults = await Promise.allSettled(independentRuns.map((run) => run.completion));
|
|
4060
5054
|
for (const result of runResults) {
|
|
4061
5055
|
if (result.status === 'rejected') this.appendUnexpectedErrors(cleanupErrors, result.reason);
|
|
4062
5056
|
}
|
|
5057
|
+
if (dependentRuns.length > 0) {
|
|
5058
|
+
state.fenceAdditionalErrors.push(cause, ...cleanupErrors);
|
|
5059
|
+
const stateLoad = this.stateLoads.get(state.storageKey);
|
|
5060
|
+
if (!stateLoad) {
|
|
5061
|
+
throw combineErrors([cause, ...cleanupErrors, new Error(
|
|
5062
|
+
'The fenced namespace no longer has durable cleanup ownership.',
|
|
5063
|
+
)]);
|
|
5064
|
+
}
|
|
5065
|
+
const deferredDrain = this.drainStorage(
|
|
5066
|
+
state.storageKey,
|
|
5067
|
+
stateLoad,
|
|
5068
|
+
reason,
|
|
5069
|
+
{ scopeId: invocation.scopeId, scope: invocation.scope },
|
|
5070
|
+
);
|
|
5071
|
+
void deferredDrain.catch(() => undefined);
|
|
5072
|
+
return;
|
|
5073
|
+
}
|
|
5074
|
+
const detachedCleanupAttempts = this.beginDetachedCleanupSettlement(state);
|
|
4063
5075
|
await Promise.allSettled([...state.sessionInitializations.values()]);
|
|
4064
5076
|
cleanupErrors.push(...await this.closeOrphanedResources(state.storageKey));
|
|
4065
5077
|
await state.scopeQueue;
|
|
4066
|
-
const
|
|
5078
|
+
const currentTombstoneRoot = state.tombstones.get(currentRun.sessionId)?.rootSessionId
|
|
5079
|
+
?? currentRun.sessionId;
|
|
5080
|
+
const currentTombstoneCleanup = state.tombstoneCleanups.get(currentTombstoneRoot);
|
|
5081
|
+
const currentTombstoneSessions = new Set(
|
|
5082
|
+
this.tombstoneGroup(state, currentTombstoneRoot).map((tombstone) => tombstone.sessionId),
|
|
5083
|
+
);
|
|
4067
5084
|
if (currentTombstoneCleanup) {
|
|
4068
5085
|
this.retainOrphanedTombstoneCleanup(
|
|
4069
5086
|
state.storageKey,
|
|
4070
|
-
|
|
5087
|
+
currentTombstoneRoot,
|
|
4071
5088
|
currentTombstoneCleanup,
|
|
4072
5089
|
);
|
|
4073
5090
|
}
|
|
4074
5091
|
const tombstoneAttempts = [...state.tombstoneCleanups.entries()]
|
|
4075
|
-
.filter(([
|
|
5092
|
+
.filter(([rootSessionId]) => rootSessionId !== currentTombstoneRoot);
|
|
4076
5093
|
const tombstoneResults = await Promise.allSettled(
|
|
4077
5094
|
tombstoneAttempts.map(([, cleanup]) => cleanup),
|
|
4078
5095
|
);
|
|
@@ -4080,11 +5097,14 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4080
5097
|
if (result.status === 'rejected') this.appendUnexpectedErrors(cleanupErrors, result.reason);
|
|
4081
5098
|
}
|
|
4082
5099
|
const attemptedTombstones = new Set(tombstoneAttempts.map(([sessionId]) => sessionId));
|
|
5100
|
+
const attemptedTombstoneSessions = new Set([...attemptedTombstones].flatMap((rootSessionId) =>
|
|
5101
|
+
this.tombstoneGroup(state, rootSessionId).map((tombstone) => tombstone.sessionId)));
|
|
4083
5102
|
const storedSessions = new Set([
|
|
4084
5103
|
...state.sessions.values(),
|
|
4085
5104
|
...[...state.retainedSessionCleanups]
|
|
4086
5105
|
.filter(([sessionId]) =>
|
|
4087
|
-
|
|
5106
|
+
!currentTombstoneSessions.has(sessionId)
|
|
5107
|
+
&& !attemptedTombstoneSessions.has(sessionId))
|
|
4088
5108
|
.map(([, retained]) => retained.stored),
|
|
4089
5109
|
]);
|
|
4090
5110
|
for (const stored of storedSessions) {
|
|
@@ -4119,15 +5139,16 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4119
5139
|
if (cleanupErrors.length > 0) throw combineErrors([cause, ...cleanupErrors]);
|
|
4120
5140
|
state.lifecycle = 'retired';
|
|
4121
5141
|
state.sessions.clear();
|
|
4122
|
-
const
|
|
5142
|
+
const currentRetainedCleanups = [...state.retainedSessionCleanups]
|
|
5143
|
+
.filter(([sessionId]) => currentTombstoneSessions.has(sessionId));
|
|
4123
5144
|
state.retainedSessionCleanups.clear();
|
|
4124
|
-
|
|
4125
|
-
state.retainedSessionCleanups.set(
|
|
5145
|
+
for (const [sessionId, retained] of currentRetainedCleanups) {
|
|
5146
|
+
state.retainedSessionCleanups.set(sessionId, retained);
|
|
4126
5147
|
}
|
|
4127
5148
|
state.sessionDeletions.clear();
|
|
4128
5149
|
state.tombstoneCleanups.clear();
|
|
4129
5150
|
if (currentTombstoneCleanup) {
|
|
4130
|
-
state.tombstoneCleanups.set(
|
|
5151
|
+
state.tombstoneCleanups.set(currentTombstoneRoot, currentTombstoneCleanup);
|
|
4131
5152
|
}
|
|
4132
5153
|
state.activeRuns.clear();
|
|
4133
5154
|
state.pendingPermissions.clear();
|
|
@@ -4143,6 +5164,75 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4143
5164
|
}
|
|
4144
5165
|
}
|
|
4145
5166
|
|
|
5167
|
+
private deferNamespaceDrain(
|
|
5168
|
+
state: IStorageState,
|
|
5169
|
+
cause: unknown,
|
|
5170
|
+
invocation?: { scopeId: string; scope: TScope },
|
|
5171
|
+
): void {
|
|
5172
|
+
state.lifecycle = 'fenced';
|
|
5173
|
+
state.fenceAdditionalErrors.push(cause);
|
|
5174
|
+
const stateLoad = this.stateLoads.get(state.storageKey);
|
|
5175
|
+
if (!stateLoad) return;
|
|
5176
|
+
const drain = this.drainStorage(
|
|
5177
|
+
state.storageKey,
|
|
5178
|
+
stateLoad,
|
|
5179
|
+
this.trustInternalError(new FlexHarnessAbortError('The session namespace was fenced.')),
|
|
5180
|
+
invocation,
|
|
5181
|
+
);
|
|
5182
|
+
void drain.catch(() => undefined);
|
|
5183
|
+
}
|
|
5184
|
+
|
|
5185
|
+
private sessionsAreDependencyRelated(
|
|
5186
|
+
state: IStorageState,
|
|
5187
|
+
leftSessionId: string,
|
|
5188
|
+
rightSessionId: string,
|
|
5189
|
+
): boolean {
|
|
5190
|
+
return this.isSessionAncestor(state, leftSessionId, rightSessionId)
|
|
5191
|
+
|| this.isSessionAncestor(state, rightSessionId, leftSessionId);
|
|
5192
|
+
}
|
|
5193
|
+
|
|
5194
|
+
private isSessionAncestor(
|
|
5195
|
+
state: IStorageState,
|
|
5196
|
+
ancestorSessionId: string,
|
|
5197
|
+
descendantSessionId: string,
|
|
5198
|
+
): boolean {
|
|
5199
|
+
const visited = new Set<string>();
|
|
5200
|
+
let currentId: string | undefined = descendantSessionId;
|
|
5201
|
+
while (currentId) {
|
|
5202
|
+
if (visited.has(currentId)) return false;
|
|
5203
|
+
visited.add(currentId);
|
|
5204
|
+
const parentSessionId = this.sessionParentSessionId(state, currentId);
|
|
5205
|
+
if (parentSessionId === ancestorSessionId) return true;
|
|
5206
|
+
currentId = parentSessionId;
|
|
5207
|
+
}
|
|
5208
|
+
return false;
|
|
5209
|
+
}
|
|
5210
|
+
|
|
5211
|
+
private sessionAncestryDepth(state: IStorageState, sessionId: string): number {
|
|
5212
|
+
const visited = new Set<string>();
|
|
5213
|
+
let currentId: string | undefined = sessionId;
|
|
5214
|
+
let depth = 0;
|
|
5215
|
+
while (currentId) {
|
|
5216
|
+
if (visited.has(currentId)) return depth;
|
|
5217
|
+
visited.add(currentId);
|
|
5218
|
+
const parentSessionId = this.sessionParentSessionId(state, currentId);
|
|
5219
|
+
if (!parentSessionId) return depth;
|
|
5220
|
+
depth++;
|
|
5221
|
+
currentId = parentSessionId;
|
|
5222
|
+
}
|
|
5223
|
+
return depth;
|
|
5224
|
+
}
|
|
5225
|
+
|
|
5226
|
+
private sessionParentSessionId(state: IStorageState, sessionId: string): string | undefined {
|
|
5227
|
+
return this.sessionMetadata(state, sessionId)?.parentSessionId
|
|
5228
|
+
?? state.tombstones.get(sessionId)?.parentSessionId;
|
|
5229
|
+
}
|
|
5230
|
+
|
|
5231
|
+
private sessionMetadata(state: IStorageState, sessionId: string): IFlexSession | undefined {
|
|
5232
|
+
return state.sessions.get(sessionId)?.session
|
|
5233
|
+
?? state.retainedSessionCleanups.get(sessionId)?.stored.session;
|
|
5234
|
+
}
|
|
5235
|
+
|
|
4146
5236
|
private async closeStoredSession(
|
|
4147
5237
|
stored: IStoredSessionState,
|
|
4148
5238
|
context: IFlexAgentContextInvocation<unknown> = stored.compactorContext!,
|
|
@@ -4497,20 +5587,27 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4497
5587
|
}
|
|
4498
5588
|
this.purgeStoredPromptQueue(stored);
|
|
4499
5589
|
}
|
|
4500
|
-
|
|
4501
|
-
|
|
4502
|
-
|
|
5590
|
+
const roots = this.orderTombstoneRootsChildFirst(
|
|
5591
|
+
state,
|
|
5592
|
+
[...new Set([...state.tombstones.values()].map((tombstone) =>
|
|
5593
|
+
tombstone.rootSessionId ?? tombstone.sessionId))],
|
|
5594
|
+
);
|
|
5595
|
+
for (const rootSessionId of roots) {
|
|
5596
|
+
if (attemptedTombstones.has(rootSessionId)) continue;
|
|
5597
|
+
if (this.descendantTombstoneRoots(state, rootSessionId).length > 0) continue;
|
|
5598
|
+
const retained = state.retainedSessionCleanups.get(rootSessionId);
|
|
4503
5599
|
try {
|
|
4504
5600
|
await this.finishTombstoneCleanup(
|
|
4505
5601
|
state,
|
|
4506
|
-
|
|
5602
|
+
rootSessionId,
|
|
4507
5603
|
invocation?.scopeId ?? retained?.stored.session.scopeId ?? state.scopeIdHint,
|
|
4508
|
-
invocation
|
|
5604
|
+
invocation,
|
|
4509
5605
|
);
|
|
4510
5606
|
} catch (error) {
|
|
4511
5607
|
this.appendUnexpectedErrors(errors, error);
|
|
4512
5608
|
}
|
|
4513
5609
|
}
|
|
5610
|
+
errors.push(...state.fenceAdditionalErrors.splice(0));
|
|
4514
5611
|
if (errors.length > 0) throw combineErrors(errors);
|
|
4515
5612
|
state.lifecycle = 'retired';
|
|
4516
5613
|
state.sessions.clear();
|