@modelprofile.com/flexharness 9.6.1 → 9.8.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 +8 -0
- package/dist_ts/classes.flexharness.js +298 -21
- package/dist_ts/interfaces.d.ts +48 -1
- package/dist_ts/interfaces.js +2 -1
- package/dist_ts/utils.harnessoptions.d.ts +2 -1
- package/dist_ts/utils.harnessoptions.js +30 -2
- package/dist_ts/utils.json.js +10 -1
- package/dist_ts/utils.validation.d.ts +2 -0
- package/dist_ts/utils.validation.js +16 -2
- package/package.json +3 -3
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.flexharness.ts +375 -15
- package/ts/interfaces.ts +58 -0
- package/ts/utils.harnessoptions.ts +32 -2
- package/ts/utils.json.ts +12 -0
- package/ts/utils.validation.ts +19 -0
|
@@ -79,6 +79,7 @@ import type {
|
|
|
79
79
|
IFlexPendingReversionRelease,
|
|
80
80
|
IFlexRedoSessionResult,
|
|
81
81
|
IFlexResolvedModel,
|
|
82
|
+
IFlexResolvedSessionConfiguration,
|
|
82
83
|
IFlexResolvedScope,
|
|
83
84
|
IFlexResourceToolProviderDescriptor,
|
|
84
85
|
IFlexRunFinishedEvent,
|
|
@@ -86,6 +87,7 @@ import type {
|
|
|
86
87
|
IFlexSchedulePromptOptions,
|
|
87
88
|
IFlexScopeSnapshot,
|
|
88
89
|
IFlexSession,
|
|
90
|
+
IFlexSessionPermissionPolicyContext,
|
|
89
91
|
IFlexSessionActivity,
|
|
90
92
|
IFlexSessionGeneration,
|
|
91
93
|
IFlexSessionGenerationCohortEntry,
|
|
@@ -154,6 +156,7 @@ import {
|
|
|
154
156
|
} from './utils.prompt.js';
|
|
155
157
|
import {
|
|
156
158
|
maxSubagentNameBytes,
|
|
159
|
+
normalizeAllowedTools,
|
|
157
160
|
normalizeAgentSessionPolicy,
|
|
158
161
|
normalizeBuiltInTools,
|
|
159
162
|
normalizeSlashCommands,
|
|
@@ -212,6 +215,7 @@ import {
|
|
|
212
215
|
validatePromptOptions,
|
|
213
216
|
validateSlashCommandExecutionOptions,
|
|
214
217
|
validateSessionGenerationId,
|
|
218
|
+
validateSessionConfigurationRef,
|
|
215
219
|
validateUpdateProjectTaskInput,
|
|
216
220
|
validateUpdateSessionOptions,
|
|
217
221
|
validateUtf8String,
|
|
@@ -396,6 +400,10 @@ interface IActiveRun {
|
|
|
396
400
|
reservedUserMessage?: IFlexMessage;
|
|
397
401
|
reservedAssistantMessage?: IFlexMessage;
|
|
398
402
|
modelResolution?: IFlexResolvedModel;
|
|
403
|
+
subagents: ReadonlyMap<string, Readonly<IFlexSubagentDefinition>>;
|
|
404
|
+
allowedTools?: readonly string[];
|
|
405
|
+
agentAllowedTools?: readonly string[];
|
|
406
|
+
permissionPolicy?: IFlexResolvedSessionConfiguration<unknown>['permissionPolicy'];
|
|
399
407
|
delegatedRunAdmissionOwner?: IDelegatedRunAdmissionOwner;
|
|
400
408
|
delegatedRunAdmissionClose?: Promise<void>;
|
|
401
409
|
completion: Promise<IFlexPromptResult>;
|
|
@@ -511,6 +519,22 @@ interface IActiveSlashCommandListing {
|
|
|
511
519
|
completion: Promise<unknown>;
|
|
512
520
|
}
|
|
513
521
|
|
|
522
|
+
interface IResolvedSessionConfiguration<TScope> {
|
|
523
|
+
subagents: ReadonlyMap<string, Readonly<IFlexSubagentDefinition>>;
|
|
524
|
+
slashCommands: ReadonlyMap<string, TRegisteredSlashCommand<TScope>>;
|
|
525
|
+
allowedTools?: readonly string[];
|
|
526
|
+
permissionPolicy?: IFlexResolvedSessionConfiguration<TScope>['permissionPolicy'];
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
interface ISessionConfigurationOwner<TScope> {
|
|
530
|
+
storageKey: string;
|
|
531
|
+
sessionId: string;
|
|
532
|
+
controller: AbortController;
|
|
533
|
+
completion: Promise<IResolvedSessionConfiguration<TScope>>;
|
|
534
|
+
settled: boolean;
|
|
535
|
+
waiters: number;
|
|
536
|
+
}
|
|
537
|
+
|
|
514
538
|
interface ISlashCommandInvocationOwner {
|
|
515
539
|
scopeId: string;
|
|
516
540
|
storageKey: string;
|
|
@@ -743,6 +767,7 @@ function containsStoreCommitUncertainty(error: unknown, seen = new Set<unknown>(
|
|
|
743
767
|
export class FlexHarness<TScope = unknown> {
|
|
744
768
|
private readonly scopeResolver: IFlexHarnessOptions<TScope>['scopeResolver'];
|
|
745
769
|
private readonly modelResolver: IFlexHarnessOptions<TScope>['modelResolver'];
|
|
770
|
+
private readonly sessionConfigurationResolver: IFlexHarnessOptions<TScope>['sessionConfigurationResolver'];
|
|
746
771
|
private readonly toolProvider: IFlexHarnessOptions<TScope>['toolProvider'];
|
|
747
772
|
private readonly resourceToolProviderResolver: IFlexHarnessOptions<TScope>['resourceToolProviderResolver'];
|
|
748
773
|
private readonly executionContextProvider: IFlexHarnessOptions<TScope>['executionContextProvider'];
|
|
@@ -764,6 +789,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
764
789
|
private readonly maxSubagentDepth: number;
|
|
765
790
|
private readonly maxSubagentCallsPerRun: number;
|
|
766
791
|
private readonly stateLoads = new Map<string, Promise<IStorageState>>();
|
|
792
|
+
private readonly sessionConfigurations = new Map<string, ISessionConfigurationOwner<TScope>>();
|
|
767
793
|
private readonly scopeAdmissions = new Map<string, IScopeAdmissionState>();
|
|
768
794
|
private readonly scopeRetirements = new Map<string, Promise<void>>();
|
|
769
795
|
private readonly storageDrains = new Map<string, Promise<void>>();
|
|
@@ -810,6 +836,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
810
836
|
}
|
|
811
837
|
this.scopeResolver = options.scopeResolver;
|
|
812
838
|
this.modelResolver = options.modelResolver;
|
|
839
|
+
this.sessionConfigurationResolver = options.sessionConfigurationResolver;
|
|
813
840
|
this.toolProvider = options.toolProvider;
|
|
814
841
|
this.resourceToolProviderResolver = options.resourceToolProviderResolver;
|
|
815
842
|
this.executionContextProvider = options.executionContextProvider;
|
|
@@ -878,13 +905,14 @@ export class FlexHarness<TScope = unknown> {
|
|
|
878
905
|
throw new FlexHarnessValidationError('createSession options must be a plain object.');
|
|
879
906
|
}
|
|
880
907
|
const unsupported = Object.keys(options).find(
|
|
881
|
-
(key) => !['sessionId', 'sessionGenerationId', 'title'].includes(key),
|
|
908
|
+
(key) => !['sessionId', 'sessionGenerationId', 'configurationRef', 'title'].includes(key),
|
|
882
909
|
);
|
|
883
910
|
if (unsupported) throw new FlexHarnessValidationError(`createSession does not support "${unsupported}".`);
|
|
884
911
|
const requestedSessionGenerationId = options.sessionGenerationId;
|
|
885
912
|
if (requestedSessionGenerationId !== undefined) {
|
|
886
913
|
validateSessionGenerationId(requestedSessionGenerationId);
|
|
887
914
|
}
|
|
915
|
+
if (options.configurationRef !== undefined) validateSessionConfigurationRef(options.configurationRef);
|
|
888
916
|
const resolved = await this.resolveState(scopeId);
|
|
889
917
|
const sessionId = options.sessionId ?? plugins.crypto.randomUUID();
|
|
890
918
|
validateIdentifier(sessionId, 'sessionId');
|
|
@@ -933,6 +961,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
933
961
|
sessionId,
|
|
934
962
|
sessionGenerationId: requestedSessionGenerationId ?? createSessionGenerationId(),
|
|
935
963
|
sessionGenerationSequence: resolved.state.revision + 1,
|
|
964
|
+
...(options.configurationRef === undefined ? {} : { configurationRef: options.configurationRef }),
|
|
936
965
|
...(options.title ? { title: options.title } : {}),
|
|
937
966
|
createdAt: timestamp,
|
|
938
967
|
updatedAt: timestamp,
|
|
@@ -1205,6 +1234,9 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1205
1234
|
sessionId,
|
|
1206
1235
|
sessionGenerationId: createSessionGenerationId(),
|
|
1207
1236
|
sessionGenerationSequence: state.revision + 1,
|
|
1237
|
+
...(parent?.session.configurationRef === undefined
|
|
1238
|
+
? {}
|
|
1239
|
+
: { configurationRef: parent.session.configurationRef }),
|
|
1208
1240
|
...(options.title === undefined ? {} : { title: options.title }),
|
|
1209
1241
|
createdAt: options.createdAt,
|
|
1210
1242
|
updatedAt: options.updatedAt,
|
|
@@ -1800,6 +1832,12 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1800
1832
|
): Promise<void> {
|
|
1801
1833
|
const { deletedSessions, descendantRoots } = reservation;
|
|
1802
1834
|
const reason = this.trustInternalError(new FlexHarnessAbortError('The session subtree was deleted.'));
|
|
1835
|
+
const configurationSettlement = this.abortSessionConfigurations(
|
|
1836
|
+
state.storageKey,
|
|
1837
|
+
reason,
|
|
1838
|
+
new Set(deletedSessions.map((deleted) => deleted.sessionId)),
|
|
1839
|
+
);
|
|
1840
|
+
if (configurationSettlement) await configurationSettlement;
|
|
1803
1841
|
const slashCommandErrors: unknown[] = [];
|
|
1804
1842
|
for (const deleted of deletedSessions) {
|
|
1805
1843
|
const listingSettlement = this.abortSlashCommandListings(
|
|
@@ -1940,16 +1978,165 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1940
1978
|
return publicSnapshot(createBoundedTransferMessage(this.requireMessage(stored, messageId)));
|
|
1941
1979
|
}
|
|
1942
1980
|
|
|
1981
|
+
private async resolveSessionConfiguration(
|
|
1982
|
+
scopeId: string,
|
|
1983
|
+
scope: TScope,
|
|
1984
|
+
state: IStorageState,
|
|
1985
|
+
stored: IStoredSessionState,
|
|
1986
|
+
signal?: AbortSignal,
|
|
1987
|
+
): Promise<IResolvedSessionConfiguration<TScope>> {
|
|
1988
|
+
const configurationRef = stored.session.configurationRef;
|
|
1989
|
+
if (configurationRef === undefined) {
|
|
1990
|
+
return { subagents: this.subagents, slashCommands: this.slashCommands };
|
|
1991
|
+
}
|
|
1992
|
+
const sessionId = stored.session.sessionId;
|
|
1993
|
+
const generation = requireSessionGeneration(stored.session);
|
|
1994
|
+
const key = JSON.stringify([
|
|
1995
|
+
state.storageKey,
|
|
1996
|
+
sessionId,
|
|
1997
|
+
generation.sessionGenerationId,
|
|
1998
|
+
generation.sessionGenerationSequence,
|
|
1999
|
+
configurationRef,
|
|
2000
|
+
]);
|
|
2001
|
+
let owner = this.sessionConfigurations.get(key);
|
|
2002
|
+
if (!owner) {
|
|
2003
|
+
const controller = new AbortController();
|
|
2004
|
+
const parent = stored.session.parentSessionId === undefined
|
|
2005
|
+
? undefined
|
|
2006
|
+
: state.sessions.get(stored.session.parentSessionId);
|
|
2007
|
+
if (stored.session.parentSessionId !== undefined && !parent) {
|
|
2008
|
+
throw this.trustInternalError(new FlexHarnessAbortError('The session parent is no longer available.'));
|
|
2009
|
+
}
|
|
2010
|
+
const parentGeneration = parent === undefined ? undefined : requireSessionGeneration(parent.session);
|
|
2011
|
+
const context = Object.freeze({
|
|
2012
|
+
scopeId,
|
|
2013
|
+
scope,
|
|
2014
|
+
storageKey: state.storageKey,
|
|
2015
|
+
sessionId,
|
|
2016
|
+
...generation,
|
|
2017
|
+
configurationRef,
|
|
2018
|
+
...(parent === undefined ? {} : {
|
|
2019
|
+
parentSessionId: parent.session.sessionId,
|
|
2020
|
+
parentSessionGenerationId: parentGeneration!.sessionGenerationId,
|
|
2021
|
+
parentSessionGenerationSequence: parentGeneration!.sessionGenerationSequence,
|
|
2022
|
+
}),
|
|
2023
|
+
...(stored.session.agent === undefined ? {} : { agent: stored.session.agent }),
|
|
2024
|
+
depth: stored.session.depth ?? 0,
|
|
2025
|
+
signal: controller.signal,
|
|
2026
|
+
});
|
|
2027
|
+
let rejectAbort!: (reason: unknown) => void;
|
|
2028
|
+
const aborted = new Promise<never>((_resolve, reject) => { rejectAbort = reject; });
|
|
2029
|
+
const onAbort = () => rejectAbort(controller.signal.reason);
|
|
2030
|
+
controller.signal.addEventListener('abort', onAbort, { once: true });
|
|
2031
|
+
const resolved = Promise.resolve().then(() => {
|
|
2032
|
+
if (!this.sessionConfigurationResolver) {
|
|
2033
|
+
throw new Error('A sessionConfigurationResolver is required for bound sessions.');
|
|
2034
|
+
}
|
|
2035
|
+
return this.sessionConfigurationResolver.resolveSessionConfiguration(context);
|
|
2036
|
+
});
|
|
2037
|
+
const completion = Promise.race([resolved, aborted]).then((configuration) => {
|
|
2038
|
+
controller.signal.throwIfAborted();
|
|
2039
|
+
if (
|
|
2040
|
+
state.lifecycle !== 'active'
|
|
2041
|
+
|| state.sessions.get(sessionId) !== stored
|
|
2042
|
+
|| stored.session.configurationRef !== configurationRef
|
|
2043
|
+
|| stored.session.sessionGenerationId !== generation.sessionGenerationId
|
|
2044
|
+
|| stored.session.sessionGenerationSequence !== generation.sessionGenerationSequence
|
|
2045
|
+
) {
|
|
2046
|
+
throw this.trustInternalError(new FlexHarnessAbortError(
|
|
2047
|
+
'The session configuration lost its session generation ownership.',
|
|
2048
|
+
));
|
|
2049
|
+
}
|
|
2050
|
+
if (
|
|
2051
|
+
!configuration
|
|
2052
|
+
|| typeof configuration !== 'object'
|
|
2053
|
+
|| Array.isArray(configuration)
|
|
2054
|
+
|| ![Object.prototype, null].includes(Object.getPrototypeOf(configuration))
|
|
2055
|
+
|| Reflect.ownKeys(configuration).some((name) =>
|
|
2056
|
+
typeof name !== 'string' || !['subagents', 'slashCommands', 'allowedTools', 'permissionPolicy'].includes(name))
|
|
2057
|
+
) throw new FlexHarnessValidationError('Resolved session configuration is invalid.');
|
|
2058
|
+
if (configuration.permissionPolicy !== undefined && typeof configuration.permissionPolicy !== 'function') {
|
|
2059
|
+
throw new FlexHarnessValidationError('Resolved session permissionPolicy must be a function.');
|
|
2060
|
+
}
|
|
2061
|
+
return Object.freeze({
|
|
2062
|
+
subagents: normalizeSubagents(configuration.subagents),
|
|
2063
|
+
slashCommands: normalizeSlashCommands(configuration.slashCommands),
|
|
2064
|
+
allowedTools: normalizeAllowedTools(configuration.allowedTools),
|
|
2065
|
+
permissionPolicy: configuration.permissionPolicy,
|
|
2066
|
+
});
|
|
2067
|
+
}).catch((error: unknown) => {
|
|
2068
|
+
if (this.sessionConfigurations.get(key)?.completion === completion) {
|
|
2069
|
+
this.sessionConfigurations.delete(key);
|
|
2070
|
+
}
|
|
2071
|
+
if (controller.signal.aborted) throw controller.signal.reason;
|
|
2072
|
+
throw this.projectOperationError(
|
|
2073
|
+
error,
|
|
2074
|
+
'sessionConfigurationResolver',
|
|
2075
|
+
scopeId,
|
|
2076
|
+
sessionId,
|
|
2077
|
+
'session-configuration',
|
|
2078
|
+
);
|
|
2079
|
+
}).finally(() => {
|
|
2080
|
+
controller.signal.removeEventListener('abort', onAbort);
|
|
2081
|
+
const current = this.sessionConfigurations.get(key);
|
|
2082
|
+
if (current?.completion === completion) current.settled = true;
|
|
2083
|
+
});
|
|
2084
|
+
owner = { storageKey: state.storageKey, sessionId, controller, completion, settled: false, waiters: 0 };
|
|
2085
|
+
this.sessionConfigurations.set(key, owner);
|
|
2086
|
+
}
|
|
2087
|
+
owner.waiters++;
|
|
2088
|
+
let onConsumerAbort: (() => void) | undefined;
|
|
2089
|
+
const consumerAbort = signal && new Promise<never>((_resolve, reject) => {
|
|
2090
|
+
onConsumerAbort = () => reject(signal.reason ?? this.trustInternalError(
|
|
2091
|
+
new FlexHarnessAbortError('Session configuration resolution was aborted.'),
|
|
2092
|
+
));
|
|
2093
|
+
signal.addEventListener('abort', onConsumerAbort, { once: true });
|
|
2094
|
+
if (signal.aborted) onConsumerAbort();
|
|
2095
|
+
});
|
|
2096
|
+
try {
|
|
2097
|
+
return await (consumerAbort
|
|
2098
|
+
? Promise.race([owner.completion, consumerAbort])
|
|
2099
|
+
: owner.completion);
|
|
2100
|
+
} finally {
|
|
2101
|
+
if (signal && onConsumerAbort) signal.removeEventListener('abort', onConsumerAbort);
|
|
2102
|
+
owner.waiters--;
|
|
2103
|
+
if (owner.waiters === 0 && !owner.settled && !owner.controller.signal.aborted) {
|
|
2104
|
+
if (this.sessionConfigurations.get(key) === owner) this.sessionConfigurations.delete(key);
|
|
2105
|
+
owner.controller.abort(this.trustInternalError(new FlexHarnessAbortError(
|
|
2106
|
+
'Session configuration resolution has no active callers.',
|
|
2107
|
+
)));
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2112
|
+
private abortSessionConfigurations(
|
|
2113
|
+
storageKey: string,
|
|
2114
|
+
reason: FlexHarnessAbortError,
|
|
2115
|
+
sessionIds?: ReadonlySet<string>,
|
|
2116
|
+
): Promise<void> | undefined {
|
|
2117
|
+
const owners = [...this.sessionConfigurations.entries()].filter(([, owner]) =>
|
|
2118
|
+
owner.storageKey === storageKey && (sessionIds === undefined || sessionIds.has(owner.sessionId)));
|
|
2119
|
+
if (owners.length === 0) return undefined;
|
|
2120
|
+
for (const [key, owner] of owners) {
|
|
2121
|
+
this.sessionConfigurations.delete(key);
|
|
2122
|
+
if (!owner.controller.signal.aborted) owner.controller.abort(reason);
|
|
2123
|
+
}
|
|
2124
|
+
return Promise.allSettled(owners.map(([, owner]) => owner.completion)).then(() => undefined);
|
|
2125
|
+
}
|
|
2126
|
+
|
|
1943
2127
|
public async listSlashCommands(
|
|
1944
2128
|
scopeId: string,
|
|
1945
2129
|
sessionId: string,
|
|
1946
2130
|
): Promise<IFlexSlashCommandDescriptor[]> {
|
|
1947
|
-
const { state } = await this.resolveState(scopeId);
|
|
2131
|
+
const { scope, state } = await this.resolveState(scopeId);
|
|
1948
2132
|
return this.trackSlashCommandListing(scopeId, state, sessionId, async (signal) => {
|
|
1949
2133
|
await this.awaitReversionOperation(state.scopeQueue, signal);
|
|
1950
2134
|
signal.throwIfAborted();
|
|
1951
2135
|
this.assertStateAcceptingWork(state);
|
|
1952
2136
|
const stored = this.requireSession(state, sessionId);
|
|
2137
|
+
const configuration = await this.resolveSessionConfiguration(
|
|
2138
|
+
scopeId, scope.scope, state, stored, signal,
|
|
2139
|
+
);
|
|
1953
2140
|
await this.reconcileContextAvailability(state, stored);
|
|
1954
2141
|
signal.throwIfAborted();
|
|
1955
2142
|
const sessionAvailable = this.isSessionAvailableForSlashCommand(state, stored);
|
|
@@ -1965,6 +2152,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1965
2152
|
stored,
|
|
1966
2153
|
sessionAvailable,
|
|
1967
2154
|
reversionIdleReason,
|
|
2155
|
+
configuration.slashCommands,
|
|
1968
2156
|
);
|
|
1969
2157
|
if (stored.session.origin === undefined) return descriptors;
|
|
1970
2158
|
return publicSnapshot(descriptors.map((descriptor): IFlexSlashCommandDescriptor => ({
|
|
@@ -1980,6 +2168,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1980
2168
|
stored: IStoredSessionState,
|
|
1981
2169
|
sessionAvailable: boolean,
|
|
1982
2170
|
reversionIdleReason?: string,
|
|
2171
|
+
slashCommands: ReadonlyMap<string, TRegisteredSlashCommand<TScope>> = this.slashCommands,
|
|
1983
2172
|
): IFlexSlashCommandDescriptor[] {
|
|
1984
2173
|
const sessionAvailability = sessionAvailable
|
|
1985
2174
|
? {}
|
|
@@ -2058,7 +2247,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
2058
2247
|
workspaceReversion: workspaceReversion(redoUnit),
|
|
2059
2248
|
},
|
|
2060
2249
|
];
|
|
2061
|
-
for (const registration of
|
|
2250
|
+
for (const registration of slashCommands.values()) {
|
|
2062
2251
|
const template = typeof registration.template === 'string';
|
|
2063
2252
|
const availability = template
|
|
2064
2253
|
? this.slashCommandPromptAvailability(
|
|
@@ -2087,6 +2276,26 @@ export class FlexHarness<TScope = unknown> {
|
|
|
2087
2276
|
sessionId: string,
|
|
2088
2277
|
untouchedInput: string,
|
|
2089
2278
|
options: IFlexSlashCommandExecutionOptions = {},
|
|
2279
|
+
): Promise<TFlexSlashCommandExecutionResult> {
|
|
2280
|
+
return this.executeSlashCommandInternal(scopeId, sessionId, untouchedInput, options, false);
|
|
2281
|
+
}
|
|
2282
|
+
|
|
2283
|
+
/** Resolves the exact session configuration before looking up custom commands. */
|
|
2284
|
+
public async executeSessionSlashCommand(
|
|
2285
|
+
scopeId: string,
|
|
2286
|
+
sessionId: string,
|
|
2287
|
+
untouchedInput: string,
|
|
2288
|
+
options: IFlexSlashCommandExecutionOptions = {},
|
|
2289
|
+
): Promise<TFlexSlashCommandExecutionResult> {
|
|
2290
|
+
return this.executeSlashCommandInternal(scopeId, sessionId, untouchedInput, options, true);
|
|
2291
|
+
}
|
|
2292
|
+
|
|
2293
|
+
private async executeSlashCommandInternal(
|
|
2294
|
+
scopeId: string,
|
|
2295
|
+
sessionId: string,
|
|
2296
|
+
untouchedInput: string,
|
|
2297
|
+
options: IFlexSlashCommandExecutionOptions,
|
|
2298
|
+
sessionAware: boolean,
|
|
2090
2299
|
): Promise<TFlexSlashCommandExecutionResult> {
|
|
2091
2300
|
const currentInvocation = this.slashCommandInvocationContext.getStore();
|
|
2092
2301
|
if (currentInvocation?.scopeId === scopeId && currentInvocation.sessionId === sessionId) {
|
|
@@ -2094,8 +2303,8 @@ export class FlexHarness<TScope = unknown> {
|
|
|
2094
2303
|
}
|
|
2095
2304
|
const parsed = parseSlashCommand(untouchedInput);
|
|
2096
2305
|
if (parsed.type !== 'parsed') return parsed;
|
|
2097
|
-
|
|
2098
|
-
if (!isReservedSlashCommandName(parsed.name) && !registration) {
|
|
2306
|
+
let registration = sessionAware ? undefined : this.slashCommands.get(parsed.name);
|
|
2307
|
+
if (!sessionAware && !isReservedSlashCommandName(parsed.name) && !registration) {
|
|
2099
2308
|
return Object.freeze({
|
|
2100
2309
|
type: 'unknown',
|
|
2101
2310
|
input: parsed.input,
|
|
@@ -2111,6 +2320,31 @@ export class FlexHarness<TScope = unknown> {
|
|
|
2111
2320
|
this.assertStateAcceptingWork(state);
|
|
2112
2321
|
const stored = this.requireSession(state, sessionId);
|
|
2113
2322
|
this.assertSessionWritable(stored);
|
|
2323
|
+
if (sessionAware) {
|
|
2324
|
+
const configuration = await this.resolveSessionConfiguration(
|
|
2325
|
+
scopeId, scope.scope, state, stored, options.signal,
|
|
2326
|
+
);
|
|
2327
|
+
this.assertStateAcceptingWork(state);
|
|
2328
|
+
if (state.sessions.get(sessionId) !== stored) {
|
|
2329
|
+
throw this.trustInternalError(new FlexHarnessAbortError(
|
|
2330
|
+
'The session was deleted during slash command resolution.',
|
|
2331
|
+
));
|
|
2332
|
+
}
|
|
2333
|
+
registration = configuration.slashCommands.get(parsed.name);
|
|
2334
|
+
if (!isReservedSlashCommandName(parsed.name) && !registration) {
|
|
2335
|
+
return Object.freeze({
|
|
2336
|
+
type: 'unknown',
|
|
2337
|
+
input: parsed.input,
|
|
2338
|
+
name: parsed.name,
|
|
2339
|
+
rawArguments: parsed.rawArguments,
|
|
2340
|
+
arguments: parsed.arguments,
|
|
2341
|
+
});
|
|
2342
|
+
}
|
|
2343
|
+
} else if (stored.session.configurationRef !== undefined) {
|
|
2344
|
+
throw new FlexHarnessValidationError(
|
|
2345
|
+
'A bound session requires executeSessionSlashCommand().',
|
|
2346
|
+
);
|
|
2347
|
+
}
|
|
2114
2348
|
const sessionKey = this.slashCommandSessionKey(state.storageKey, sessionId);
|
|
2115
2349
|
const invocationOwner = this.slashCommandInvocationContext.getStore();
|
|
2116
2350
|
if (invocationOwner?.sessionKey === sessionKey) {
|
|
@@ -3733,6 +3967,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
3733
3967
|
|
|
3734
3968
|
private createActiveRun(queued: IQueuedPrompt): IActiveRun {
|
|
3735
3969
|
return {
|
|
3970
|
+
subagents: this.subagents,
|
|
3736
3971
|
state: queued.state,
|
|
3737
3972
|
stored: queued.stored,
|
|
3738
3973
|
originCompactorContext: this.createCompactorContext(
|
|
@@ -4959,6 +5194,22 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4959
5194
|
signal: AbortSignal,
|
|
4960
5195
|
): Promise<plugins.IAgentGenerationLease> {
|
|
4961
5196
|
signal.throwIfAborted();
|
|
5197
|
+
const configuration = await this.resolveSessionConfiguration(
|
|
5198
|
+
run.scopeId,
|
|
5199
|
+
run.scope as TScope,
|
|
5200
|
+
run.state,
|
|
5201
|
+
run.stored,
|
|
5202
|
+
signal,
|
|
5203
|
+
);
|
|
5204
|
+
run.subagents = configuration.subagents;
|
|
5205
|
+
run.allowedTools = configuration.allowedTools;
|
|
5206
|
+
run.permissionPolicy = configuration.permissionPolicy as IActiveRun['permissionPolicy'];
|
|
5207
|
+
const agent = run.stored.session.agent;
|
|
5208
|
+
if (agent !== undefined) {
|
|
5209
|
+
const definition = configuration.subagents.get(agent);
|
|
5210
|
+
if (!definition) throw new FlexHarnessValidationError(`Subagent "${agent}" is disabled.`);
|
|
5211
|
+
run.agentAllowedTools = definition.allowedTools;
|
|
5212
|
+
}
|
|
4962
5213
|
await this.prepareRunReversion(run, signal);
|
|
4963
5214
|
run.phase = 'running';
|
|
4964
5215
|
const queued = run.stored.outstandingPromptsById.get(run.queueId);
|
|
@@ -5069,9 +5320,16 @@ export class FlexHarness<TScope = unknown> {
|
|
|
5069
5320
|
const providedTools = toolHandle?.tools;
|
|
5070
5321
|
const combinedTools: Record<string, unknown> = { ...(providedTools ?? {}) };
|
|
5071
5322
|
Object.assign(combinedTools, this.createBuiltInTools(run));
|
|
5072
|
-
|
|
5323
|
+
const globalNames = run.allowedTools === undefined ? undefined : new Set(run.allowedTools);
|
|
5324
|
+
const agentNames = run.agentAllowedTools === undefined ? undefined : new Set(run.agentAllowedTools);
|
|
5325
|
+
const admittedTools = globalNames === undefined && agentNames === undefined
|
|
5326
|
+
? combinedTools
|
|
5327
|
+
: Object.fromEntries(Object.entries(combinedTools).filter(([name]) =>
|
|
5328
|
+
(globalNames === undefined || globalNames.has(name))
|
|
5329
|
+
&& (agentNames === undefined || agentNames.has(name))));
|
|
5330
|
+
tools = Object.keys(admittedTools).length > 0
|
|
5073
5331
|
? wrapToolSet(
|
|
5074
|
-
|
|
5332
|
+
admittedTools as TFlexAgentToolSet,
|
|
5075
5333
|
this.toolOutputLimits,
|
|
5076
5334
|
(toolError) => this.projectExternalError(run, toolError, 'toolExecution'),
|
|
5077
5335
|
)
|
|
@@ -5121,7 +5379,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
5121
5379
|
|
|
5122
5380
|
private enabledBuiltInToolNames(run: IActiveRun): Set<string> {
|
|
5123
5381
|
const names = new Set<string>();
|
|
5124
|
-
if (
|
|
5382
|
+
if (run.subagents.size > 0 && (run.stored.session.depth ?? 0) < this.maxSubagentDepth) {
|
|
5125
5383
|
names.add('delegate');
|
|
5126
5384
|
}
|
|
5127
5385
|
if (this.builtInTools.renameSession) names.add('rename_session');
|
|
@@ -5144,7 +5402,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
5144
5402
|
|
|
5145
5403
|
private createBuiltInTools(run: IActiveRun): Record<string, unknown> {
|
|
5146
5404
|
const tools: Record<string, unknown> = {};
|
|
5147
|
-
if (
|
|
5405
|
+
if (run.subagents.size > 0 && (run.stored.session.depth ?? 0) < this.maxSubagentDepth) {
|
|
5148
5406
|
tools.delegate = this.createSubagentTool(run);
|
|
5149
5407
|
}
|
|
5150
5408
|
if (this.builtInTools.renameSession) tools.rename_session = this.createRenameSessionTool(run);
|
|
@@ -5475,7 +5733,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
5475
5733
|
}
|
|
5476
5734
|
|
|
5477
5735
|
private createSubagentTool(run: IActiveRun): unknown {
|
|
5478
|
-
const available = [...
|
|
5736
|
+
const available = [...run.subagents.values()]
|
|
5479
5737
|
.map((definition) => `- ${definition.name}: ${definition.description}`)
|
|
5480
5738
|
.join('\n');
|
|
5481
5739
|
return plugins.tool({
|
|
@@ -5527,7 +5785,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
5527
5785
|
validateUtf8String(input.taskId, 'taskId', maxSubagentTaskIdBytes, true);
|
|
5528
5786
|
}
|
|
5529
5787
|
validateUtf8String(toolCallId, 'delegate toolCallId', maxTransferIdentifierBytes, true);
|
|
5530
|
-
const definition =
|
|
5788
|
+
const definition = run.subagents.get(input.subagentType);
|
|
5531
5789
|
if (!definition || (run.stored.session.depth ?? 0) >= this.maxSubagentDepth) {
|
|
5532
5790
|
throw new FlexHarnessValidationError(`Subagent "${input.subagentType}" is not available.`);
|
|
5533
5791
|
}
|
|
@@ -5738,7 +5996,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
5738
5996
|
if (state.activeRuns.get(run.sessionId) !== run || run.callbacksClosed) {
|
|
5739
5997
|
throw new FlexHarnessAbortError('The parent run no longer owns this subagent request.');
|
|
5740
5998
|
}
|
|
5741
|
-
if (run.stored.session.agent !== undefined && !
|
|
5999
|
+
if (run.stored.session.agent !== undefined && !run.subagents.has(run.stored.session.agent)) {
|
|
5742
6000
|
throw new FlexHarnessValidationError(`Parent subagent "${run.stored.session.agent}" is disabled.`);
|
|
5743
6001
|
}
|
|
5744
6002
|
if ((run.stored.session.depth ?? 0) >= this.maxSubagentDepth) {
|
|
@@ -5760,8 +6018,11 @@ export class FlexHarness<TScope = unknown> {
|
|
|
5760
6018
|
if (
|
|
5761
6019
|
existing.session.parentSessionId !== run.sessionId
|
|
5762
6020
|
|| existing.session.agent !== definition.name
|
|
6021
|
+
|| existing.session.configurationRef !== run.stored.session.configurationRef
|
|
5763
6022
|
) {
|
|
5764
|
-
throw new FlexHarnessValidationError(
|
|
6023
|
+
throw new FlexHarnessValidationError(
|
|
6024
|
+
`Task session "${sessionId}" is not owned by this parent, agent, and configuration.`,
|
|
6025
|
+
);
|
|
5765
6026
|
}
|
|
5766
6027
|
if (
|
|
5767
6028
|
taskId === undefined
|
|
@@ -5802,6 +6063,9 @@ export class FlexHarness<TScope = unknown> {
|
|
|
5802
6063
|
sessionId,
|
|
5803
6064
|
sessionGenerationId: createSessionGenerationId(),
|
|
5804
6065
|
sessionGenerationSequence: state.revision + 1,
|
|
6066
|
+
...(run.stored.session.configurationRef === undefined
|
|
6067
|
+
? {}
|
|
6068
|
+
: { configurationRef: run.stored.session.configurationRef }),
|
|
5805
6069
|
title: `Subagent: ${definition.name}`,
|
|
5806
6070
|
createdAt: timestamp,
|
|
5807
6071
|
updatedAt: timestamp,
|
|
@@ -7522,8 +7786,10 @@ export class FlexHarness<TScope = unknown> {
|
|
|
7522
7786
|
if (state.activeRuns.get(run.sessionId) !== run || run.callbacksClosed) {
|
|
7523
7787
|
throw new FlexHarnessPermissionStateError(`Run "${run.runId}" no longer accepts permission requests.`);
|
|
7524
7788
|
}
|
|
7525
|
-
if (input.rememberKey && run.stored.rememberedPermissionKeys.has(input.rememberKey))
|
|
7526
|
-
|
|
7789
|
+
if (!run.permissionPolicy && input.rememberKey && run.stored.rememberedPermissionKeys.has(input.rememberKey)) {
|
|
7790
|
+
return;
|
|
7791
|
+
}
|
|
7792
|
+
const candidate: IFlexPermissionRequest = {
|
|
7527
7793
|
permissionId: plugins.crypto.randomUUID(),
|
|
7528
7794
|
scopeId: run.scopeId,
|
|
7529
7795
|
sessionId: run.sessionId,
|
|
@@ -7540,6 +7806,33 @@ export class FlexHarness<TScope = unknown> {
|
|
|
7540
7806
|
: { metadata: normalizeJsonValue(input.metadata, this.toolOutputLimits) }),
|
|
7541
7807
|
createdAt: new Date().toISOString(),
|
|
7542
7808
|
};
|
|
7809
|
+
const policyDecision = run.permissionPolicy
|
|
7810
|
+
? await this.decideSessionPermissionPolicy(state, run, candidate)
|
|
7811
|
+
: 'inherit';
|
|
7812
|
+
if (policyDecision === 'allow' || policyDecision === 'deny') {
|
|
7813
|
+
this.emitEvent(run.scopeId, run.stored.session, {
|
|
7814
|
+
type: 'permission.resolved',
|
|
7815
|
+
runId: run.runId,
|
|
7816
|
+
request: publicSnapshot(candidate),
|
|
7817
|
+
decision: policyDecision === 'allow' ? 'once' : 'reject',
|
|
7818
|
+
origin: 'policy',
|
|
7819
|
+
});
|
|
7820
|
+
if (policyDecision === 'deny') {
|
|
7821
|
+
const rejection = this.trustInternalError(
|
|
7822
|
+
new FlexHarnessPermissionRejectedError(candidate.permissionId),
|
|
7823
|
+
);
|
|
7824
|
+
this.abortRunInternally(run, rejection);
|
|
7825
|
+
throw rejection;
|
|
7826
|
+
}
|
|
7827
|
+
return;
|
|
7828
|
+
}
|
|
7829
|
+
if (policyDecision === 'inherit'
|
|
7830
|
+
&& input.rememberKey && run.stored.rememberedPermissionKeys.has(input.rememberKey)) return;
|
|
7831
|
+
const request: IFlexPermissionRequest = { ...candidate };
|
|
7832
|
+
if (policyDecision === 'ask') {
|
|
7833
|
+
delete request.rememberKey;
|
|
7834
|
+
request.requiresExplicitResponse = true;
|
|
7835
|
+
}
|
|
7543
7836
|
let resolvePermission!: () => void;
|
|
7544
7837
|
let rejectPermission!: (error: unknown) => void;
|
|
7545
7838
|
const promise = new Promise<void>((resolve, reject) => {
|
|
@@ -7587,6 +7880,70 @@ export class FlexHarness<TScope = unknown> {
|
|
|
7587
7880
|
return promise;
|
|
7588
7881
|
}
|
|
7589
7882
|
|
|
7883
|
+
private async decideSessionPermissionPolicy(
|
|
7884
|
+
state: IStorageState,
|
|
7885
|
+
run: IActiveRun,
|
|
7886
|
+
request: IFlexPermissionRequest,
|
|
7887
|
+
): Promise<'inherit' | 'allow' | 'ask' | 'deny'> {
|
|
7888
|
+
const policy = run.permissionPolicy!;
|
|
7889
|
+
const signal = run.controller.signal;
|
|
7890
|
+
const context: Readonly<IFlexSessionPermissionPolicyContext<unknown>> = Object.freeze({
|
|
7891
|
+
scopeId: run.scopeId,
|
|
7892
|
+
scope: run.scope,
|
|
7893
|
+
storageKey: state.storageKey,
|
|
7894
|
+
sessionId: run.sessionId,
|
|
7895
|
+
...requireSessionGeneration(run.stored.session),
|
|
7896
|
+
runId: run.runId,
|
|
7897
|
+
configurationRef: run.stored.session.configurationRef!,
|
|
7898
|
+
...(run.stored.session.agent === undefined ? {} : { agent: run.stored.session.agent }),
|
|
7899
|
+
request: publicSnapshot(request),
|
|
7900
|
+
signal,
|
|
7901
|
+
});
|
|
7902
|
+
let onAbort!: () => void;
|
|
7903
|
+
const aborted = new Promise<never>((_resolve, reject) => {
|
|
7904
|
+
onAbort = () => reject(signal.reason ?? this.trustInternalError(new FlexHarnessAbortError()));
|
|
7905
|
+
});
|
|
7906
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
7907
|
+
if (signal.aborted) onAbort();
|
|
7908
|
+
let decision: unknown;
|
|
7909
|
+
try {
|
|
7910
|
+
decision = await Promise.race([
|
|
7911
|
+
Promise.resolve().then(() => {
|
|
7912
|
+
signal.throwIfAborted();
|
|
7913
|
+
return policy(context);
|
|
7914
|
+
}),
|
|
7915
|
+
aborted,
|
|
7916
|
+
]);
|
|
7917
|
+
if (!['inherit', 'allow', 'ask', 'deny'].includes(decision as string)) {
|
|
7918
|
+
throw new FlexHarnessValidationError('Session permissionPolicy returned an invalid decision.');
|
|
7919
|
+
}
|
|
7920
|
+
} catch (error) {
|
|
7921
|
+
if (signal.aborted) throw signal.reason;
|
|
7922
|
+
const projected = this.projectExternalError(run, error, 'permissionPolicy');
|
|
7923
|
+
this.abortRunInternally(run, projected);
|
|
7924
|
+
throw projected;
|
|
7925
|
+
} finally {
|
|
7926
|
+
signal.removeEventListener('abort', onAbort);
|
|
7927
|
+
}
|
|
7928
|
+
signal.throwIfAborted();
|
|
7929
|
+
const generation = requireSessionGeneration(run.stored.session);
|
|
7930
|
+
if (
|
|
7931
|
+
state.lifecycle !== 'active'
|
|
7932
|
+
|| state.activeRuns.get(run.sessionId) !== run
|
|
7933
|
+
|| state.sessions.get(run.sessionId) !== run.stored
|
|
7934
|
+
|| run.callbacksClosed
|
|
7935
|
+
|| generation.sessionGenerationId !== request.sessionGenerationId
|
|
7936
|
+
|| generation.sessionGenerationSequence !== request.sessionGenerationSequence
|
|
7937
|
+
) {
|
|
7938
|
+
const ownershipError = this.trustInternalError(new FlexHarnessAbortError(
|
|
7939
|
+
'Permission policy lost its exact run ownership.',
|
|
7940
|
+
));
|
|
7941
|
+
this.abortRunInternally(run, ownershipError);
|
|
7942
|
+
throw ownershipError;
|
|
7943
|
+
}
|
|
7944
|
+
return decision as 'inherit' | 'allow' | 'ask' | 'deny';
|
|
7945
|
+
}
|
|
7946
|
+
|
|
7590
7947
|
private async applyPermissionResponse(
|
|
7591
7948
|
state: IStorageState,
|
|
7592
7949
|
pending: IPendingPermission,
|
|
@@ -10254,6 +10611,8 @@ export class FlexHarness<TScope = unknown> {
|
|
|
10254
10611
|
return;
|
|
10255
10612
|
}
|
|
10256
10613
|
if (state.lifecycle !== 'fenced') state.lifecycle = 'retiring';
|
|
10614
|
+
const configurationSettlement = this.abortSessionConfigurations(storageKey, reason);
|
|
10615
|
+
if (configurationSettlement) await configurationSettlement;
|
|
10257
10616
|
for (const staged of [...state.sessionImports.values()]) {
|
|
10258
10617
|
if (staged.phase === 'staging') this.releaseStagedSessionImport(staged);
|
|
10259
10618
|
}
|
|
@@ -10444,6 +10803,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
10444
10803
|
this.scopeRetirements.clear();
|
|
10445
10804
|
this.storageDrains.clear();
|
|
10446
10805
|
this.storageCompactorLifecycleControllers.clear();
|
|
10806
|
+
this.sessionConfigurations.clear();
|
|
10447
10807
|
}
|
|
10448
10808
|
|
|
10449
10809
|
private async abortSlashCommandExecutions(
|