@modelprofile.com/flexharness 10.23.0 → 10.24.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.flexharness.admission.d.ts +17 -0
- package/dist_ts/classes.flexharness.admission.js +263 -0
- package/dist_ts/classes.flexharness.callbacks.d.ts +38 -0
- package/dist_ts/classes.flexharness.callbacks.js +313 -0
- package/dist_ts/classes.flexharness.compaction.d.ts +25 -0
- package/dist_ts/classes.flexharness.compaction.js +87 -0
- package/dist_ts/classes.flexharness.configurations.d.ts +11 -0
- package/dist_ts/classes.flexharness.configurations.js +137 -0
- package/dist_ts/classes.flexharness.core.d.ts +143 -0
- package/dist_ts/classes.flexharness.core.js +1538 -0
- package/dist_ts/classes.flexharness.d.ts +2 -437
- package/dist_ts/classes.flexharness.deletion.d.ts +31 -0
- package/dist_ts/classes.flexharness.deletion.js +396 -0
- package/dist_ts/classes.flexharness.imports.d.ts +24 -0
- package/dist_ts/classes.flexharness.imports.js +345 -0
- package/dist_ts/classes.flexharness.js +119 -9776
- package/dist_ts/classes.flexharness.lifecycle.d.ts +47 -0
- package/dist_ts/classes.flexharness.lifecycle.js +625 -0
- package/dist_ts/classes.flexharness.permissions.d.ts +19 -0
- package/dist_ts/classes.flexharness.permissions.js +450 -0
- package/dist_ts/classes.flexharness.projects.d.ts +55 -0
- package/dist_ts/classes.flexharness.projects.js +575 -0
- package/dist_ts/classes.flexharness.prompts.d.ts +44 -0
- package/dist_ts/classes.flexharness.prompts.js +659 -0
- package/dist_ts/classes.flexharness.providers.d.ts +18 -0
- package/dist_ts/classes.flexharness.providers.js +267 -0
- package/dist_ts/classes.flexharness.results.d.ts +26 -0
- package/dist_ts/classes.flexharness.results.js +89 -0
- package/dist_ts/classes.flexharness.reversions.d.ts +52 -0
- package/dist_ts/classes.flexharness.reversions.js +1306 -0
- package/dist_ts/classes.flexharness.runs.d.ts +63 -0
- package/dist_ts/classes.flexharness.runs.js +1058 -0
- package/dist_ts/classes.flexharness.sessions.d.ts +20 -0
- package/dist_ts/classes.flexharness.sessions.js +381 -0
- package/dist_ts/classes.flexharness.slashcommands.d.ts +40 -0
- package/dist_ts/classes.flexharness.slashcommands.js +458 -0
- package/dist_ts/classes.flexharness.subagents.d.ts +31 -0
- package/dist_ts/classes.flexharness.subagents.js +690 -0
- package/dist_ts/classes.flexharness.tooldispatch.d.ts +53 -0
- package/dist_ts/classes.flexharness.tooldispatch.js +418 -0
- package/dist_ts/interfaces.flexharness.d.ts +455 -0
- package/dist_ts/interfaces.flexharness.js +2 -0
- package/dist_ts/utils.flexharness.d.ts +19 -0
- package/dist_ts/utils.flexharness.js +83 -0
- package/dist_ts_compaction/plugins.d.ts +2 -2
- package/dist_ts_compaction/plugins.js +3 -3
- package/dist_ts_compaction/smartagent.compaction.js +6 -5
- package/dist_ts_stores_nosqldb/classes.nosqldbstores.d.ts +9 -0
- package/dist_ts_stores_nosqldb/classes.nosqldbstores.js +13 -1
- package/dist_ts_stores_nosqldb/classes.recordstore.d.ts +2 -2
- package/dist_ts_stores_nosqldb/classes.recordstore.js +3 -3
- package/package.json +3 -3
- package/readme.md +1 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.flexharness.admission.ts +331 -0
- package/ts/classes.flexharness.callbacks.ts +352 -0
- package/ts/classes.flexharness.compaction.ts +167 -0
- package/ts/classes.flexharness.configurations.ts +167 -0
- package/ts/classes.flexharness.core.ts +1830 -0
- package/ts/classes.flexharness.deletion.ts +616 -0
- package/ts/classes.flexharness.imports.ts +414 -0
- package/ts/classes.flexharness.lifecycle.ts +786 -0
- package/ts/classes.flexharness.permissions.ts +535 -0
- package/ts/classes.flexharness.projects.ts +1006 -0
- package/ts/classes.flexharness.prompts.ts +857 -0
- package/ts/classes.flexharness.providers.ts +322 -0
- package/ts/classes.flexharness.results.ts +98 -0
- package/ts/classes.flexharness.reversions.ts +1684 -0
- package/ts/classes.flexharness.runs.ts +1204 -0
- package/ts/classes.flexharness.sessions.ts +483 -0
- package/ts/classes.flexharness.slashcommands.ts +705 -0
- package/ts/classes.flexharness.subagents.ts +860 -0
- package/ts/classes.flexharness.tooldispatch.ts +475 -0
- package/ts/classes.flexharness.ts +147 -12567
- package/ts/interfaces.flexharness.ts +517 -0
- package/ts/readme.md +1 -1
- package/ts/utils.flexharness.ts +121 -0
- package/ts_compaction/plugins.ts +2 -2
- package/ts_compaction/smartagent.compaction.ts +5 -4
- package/ts_stores_nosqldb/classes.nosqldbstores.ts +13 -0
- package/ts_stores_nosqldb/classes.recordstore.ts +5 -5
- package/ts_stores_nosqldb/readme.md +7 -0
|
@@ -0,0 +1,705 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
import {
|
|
3
|
+
FlexHarnessAbortError,
|
|
4
|
+
FlexHarnessSessionBusyError,
|
|
5
|
+
FlexHarnessSlashCommandReentryError,
|
|
6
|
+
FlexHarnessSlashCommandUnavailableError,
|
|
7
|
+
FlexHarnessValidationError,
|
|
8
|
+
} from './errors.js';
|
|
9
|
+
import type {
|
|
10
|
+
IFlexPromptAdmission,
|
|
11
|
+
IFlexSlashCommandDescriptor,
|
|
12
|
+
IFlexSlashCommandExecutionOptions,
|
|
13
|
+
IFlexSlashCommandHandlerRegistration,
|
|
14
|
+
TFlexSlashCommandExecutionResult,
|
|
15
|
+
} from './interfaces.js';
|
|
16
|
+
import { normalizeJsonValue } from './utils.json.js';
|
|
17
|
+
import { normalizeFlexPrompt } from './utils.prompt.js';
|
|
18
|
+
import type { TRegisteredSlashCommand } from './utils.harnessoptions.js';
|
|
19
|
+
import {
|
|
20
|
+
FLEX_SLASH_COMMAND_INITIALIZE_TEMPLATE,
|
|
21
|
+
expandSlashCommandTemplate,
|
|
22
|
+
isReservedSlashCommandName,
|
|
23
|
+
parseSlashCommand,
|
|
24
|
+
slashCommandTemplateHints,
|
|
25
|
+
} from './utils.slashcommands.js';
|
|
26
|
+
import { publicSnapshot } from './utils.transfer.js';
|
|
27
|
+
import { validateSlashCommandExecutionOptions } from './utils.validation.js';
|
|
28
|
+
import type {
|
|
29
|
+
IStoredSessionState,
|
|
30
|
+
IStorageState,
|
|
31
|
+
IActiveSlashCommandExecution,
|
|
32
|
+
IActiveSlashCommandListing,
|
|
33
|
+
ISlashCommandInvocationOwner,
|
|
34
|
+
ISlashCommandActivityOwner,
|
|
35
|
+
} from './interfaces.flexharness.js';
|
|
36
|
+
import {
|
|
37
|
+
slashCommandSessionUnavailableReason,
|
|
38
|
+
requireSessionGeneration,
|
|
39
|
+
} from './utils.flexharness.js';
|
|
40
|
+
import type { FlexHarnessCore } from './classes.flexharness.core.js';
|
|
41
|
+
|
|
42
|
+
const importedSessionUnavailableReason = 'Session is an imported conversation and is read-only.';
|
|
43
|
+
|
|
44
|
+
/** Slash command listing and execution, and the ownership of their activity. */
|
|
45
|
+
export class FlexHarnessSlashCommands<TScope> {
|
|
46
|
+
public readonly activeSlashCommandExecutions = new Map<string, IActiveSlashCommandExecution>();
|
|
47
|
+
private readonly activeSlashCommandListings = new Set<IActiveSlashCommandListing>();
|
|
48
|
+
public readonly slashCommandInvocationContext = new plugins.AsyncLocalStorage<
|
|
49
|
+
ISlashCommandInvocationOwner
|
|
50
|
+
>();
|
|
51
|
+
public readonly slashCommandActivityContext = new plugins.AsyncLocalStorage<
|
|
52
|
+
ISlashCommandActivityOwner
|
|
53
|
+
>();
|
|
54
|
+
|
|
55
|
+
constructor(private readonly core: FlexHarnessCore<TScope>) {}
|
|
56
|
+
|
|
57
|
+
public async listSlashCommands(
|
|
58
|
+
scopeId: string,
|
|
59
|
+
sessionId: string,
|
|
60
|
+
): Promise<IFlexSlashCommandDescriptor[]> {
|
|
61
|
+
const { scope, state } = await this.core.resolveState(scopeId);
|
|
62
|
+
return this.trackSlashCommandListing(scopeId, state, sessionId, async (signal) => {
|
|
63
|
+
await this.core.reversions.awaitReversionOperation(state.scopeQueue, signal);
|
|
64
|
+
signal.throwIfAborted();
|
|
65
|
+
this.core.assertStateAcceptingWork(state);
|
|
66
|
+
const stored = this.core.requireSession(state, sessionId);
|
|
67
|
+
const configuration = await this.core.configurations.resolveSessionConfiguration(
|
|
68
|
+
scopeId, scope.scope, state, stored, signal,
|
|
69
|
+
);
|
|
70
|
+
await this.core.reversions.reconcileContextAvailability(state, stored);
|
|
71
|
+
signal.throwIfAborted();
|
|
72
|
+
const sessionAvailable = this.isSessionAvailableForSlashCommand(state, stored);
|
|
73
|
+
const reversionIdleReason = await this.core.reversions.reversionUnavailableReason(
|
|
74
|
+
state,
|
|
75
|
+
stored,
|
|
76
|
+
false,
|
|
77
|
+
signal,
|
|
78
|
+
);
|
|
79
|
+
signal.throwIfAborted();
|
|
80
|
+
const descriptors = this.createSlashCommandDescriptors(
|
|
81
|
+
state,
|
|
82
|
+
stored,
|
|
83
|
+
sessionAvailable,
|
|
84
|
+
reversionIdleReason,
|
|
85
|
+
configuration.slashCommands,
|
|
86
|
+
);
|
|
87
|
+
if (stored.session.origin === undefined) return descriptors;
|
|
88
|
+
return publicSnapshot(descriptors.map((descriptor): IFlexSlashCommandDescriptor => ({
|
|
89
|
+
...descriptor,
|
|
90
|
+
available: false,
|
|
91
|
+
unavailableReason: importedSessionUnavailableReason,
|
|
92
|
+
})));
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private createSlashCommandDescriptors(
|
|
97
|
+
state: IStorageState,
|
|
98
|
+
stored: IStoredSessionState,
|
|
99
|
+
sessionAvailable: boolean,
|
|
100
|
+
reversionIdleReason?: string,
|
|
101
|
+
slashCommands: ReadonlyMap<string, TRegisteredSlashCommand<TScope>> = this.core.slashCommands,
|
|
102
|
+
): IFlexSlashCommandDescriptor[] {
|
|
103
|
+
const sessionAvailability = sessionAvailable
|
|
104
|
+
? {}
|
|
105
|
+
: { available: false, unavailableReason: slashCommandSessionUnavailableReason };
|
|
106
|
+
const undoUnit = this.core.reversions.reversionUnit(stored, 'undo');
|
|
107
|
+
const redoUnit = this.core.reversions.reversionUnit(stored, 'redo');
|
|
108
|
+
const workspaceReversion = (unit: ReturnType<typeof this.core.reversions.reversionUnit>) =>
|
|
109
|
+
!unit
|
|
110
|
+
|| !this.core.turnReversionProvider
|
|
111
|
+
|| unit.segments.some((segment) => segment.provenance !== 'workspace')
|
|
112
|
+
? 'unsupported' as const
|
|
113
|
+
: 'supported' as const;
|
|
114
|
+
const reversionAvailability = (unit: ReturnType<typeof this.core.reversions.reversionUnit>) => {
|
|
115
|
+
if (reversionIdleReason) return {
|
|
116
|
+
available: false,
|
|
117
|
+
unavailableReason: reversionIdleReason,
|
|
118
|
+
};
|
|
119
|
+
if (!unit) return { available: false, unavailableReason: 'No turn is available.' };
|
|
120
|
+
if (!unit.target.contextAvailable) return {
|
|
121
|
+
available: false,
|
|
122
|
+
unavailableReason: 'The turn is beyond the context archive horizon.',
|
|
123
|
+
};
|
|
124
|
+
if (unit.segments.some((segment) =>
|
|
125
|
+
segment.disposition === 'revertible' && segment.workspaceReference === undefined)) return {
|
|
126
|
+
available: false,
|
|
127
|
+
unavailableReason: 'The turn has no available workspace reversion capture.',
|
|
128
|
+
};
|
|
129
|
+
return { available: true };
|
|
130
|
+
};
|
|
131
|
+
const compactAvailable = Boolean(this.core.agentSessionPolicy.contextCompactor) && sessionAvailable;
|
|
132
|
+
const compactUnavailableReason = !this.core.agentSessionPolicy.contextCompactor
|
|
133
|
+
? 'No context compactor is configured.'
|
|
134
|
+
: !sessionAvailable
|
|
135
|
+
? slashCommandSessionUnavailableReason
|
|
136
|
+
: undefined;
|
|
137
|
+
const initPrompt = expandSlashCommandTemplate(FLEX_SLASH_COMMAND_INITIALIZE_TEMPLATE, '', []);
|
|
138
|
+
const initAvailability = this.slashCommandPromptAvailability(
|
|
139
|
+
state,
|
|
140
|
+
stored,
|
|
141
|
+
this.core.prompts.promptAdmissionByteSize(initPrompt, {}),
|
|
142
|
+
);
|
|
143
|
+
const descriptors: IFlexSlashCommandDescriptor[] = [
|
|
144
|
+
{
|
|
145
|
+
name: 'compact',
|
|
146
|
+
description: 'Compact the current session context.',
|
|
147
|
+
kind: 'builtin',
|
|
148
|
+
hints: [],
|
|
149
|
+
available: compactAvailable,
|
|
150
|
+
...(compactUnavailableReason === undefined
|
|
151
|
+
? {}
|
|
152
|
+
: { unavailableReason: compactUnavailableReason }),
|
|
153
|
+
workspaceReversion: 'not-applicable',
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
name: 'init',
|
|
157
|
+
description: 'Create or update AGENTS.md for the active workspace.',
|
|
158
|
+
kind: 'builtin',
|
|
159
|
+
hints: ['$ARGUMENTS'],
|
|
160
|
+
...initAvailability,
|
|
161
|
+
workspaceReversion: 'not-applicable',
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
name: 'undo',
|
|
165
|
+
description: 'Undo the most recent session operation.',
|
|
166
|
+
kind: 'builtin',
|
|
167
|
+
hints: [],
|
|
168
|
+
...reversionAvailability(undoUnit),
|
|
169
|
+
workspaceReversion: workspaceReversion(undoUnit),
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
name: 'redo',
|
|
173
|
+
description: 'Redo the most recently undone session operation.',
|
|
174
|
+
kind: 'builtin',
|
|
175
|
+
hints: [],
|
|
176
|
+
...reversionAvailability(redoUnit),
|
|
177
|
+
workspaceReversion: workspaceReversion(redoUnit),
|
|
178
|
+
},
|
|
179
|
+
];
|
|
180
|
+
for (const registration of slashCommands.values()) {
|
|
181
|
+
const template = typeof registration.template === 'string';
|
|
182
|
+
const availability = template
|
|
183
|
+
? this.slashCommandPromptAvailability(
|
|
184
|
+
state,
|
|
185
|
+
stored,
|
|
186
|
+
this.core.prompts.promptAdmissionByteSize(
|
|
187
|
+
expandSlashCommandTemplate(registration.template!, '', []),
|
|
188
|
+
{},
|
|
189
|
+
),
|
|
190
|
+
)
|
|
191
|
+
: { available: sessionAvailable, ...sessionAvailability };
|
|
192
|
+
descriptors.push({
|
|
193
|
+
name: registration.name,
|
|
194
|
+
description: registration.description ?? '',
|
|
195
|
+
kind: template ? 'template' : 'handler',
|
|
196
|
+
hints: template ? slashCommandTemplateHints(registration.template!) : [],
|
|
197
|
+
...availability,
|
|
198
|
+
workspaceReversion: 'not-applicable',
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
return publicSnapshot(descriptors);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
public async executeSlashCommand(
|
|
205
|
+
scopeId: string,
|
|
206
|
+
sessionId: string,
|
|
207
|
+
untouchedInput: string,
|
|
208
|
+
options: IFlexSlashCommandExecutionOptions = {},
|
|
209
|
+
): Promise<TFlexSlashCommandExecutionResult> {
|
|
210
|
+
return this.executeSlashCommandInternal(scopeId, sessionId, untouchedInput, options, false);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
public async executeSessionSlashCommand(
|
|
214
|
+
scopeId: string,
|
|
215
|
+
sessionId: string,
|
|
216
|
+
untouchedInput: string,
|
|
217
|
+
options: IFlexSlashCommandExecutionOptions = {},
|
|
218
|
+
): Promise<TFlexSlashCommandExecutionResult> {
|
|
219
|
+
return this.executeSlashCommandInternal(scopeId, sessionId, untouchedInput, options, true);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private async executeSlashCommandInternal(
|
|
223
|
+
scopeId: string,
|
|
224
|
+
sessionId: string,
|
|
225
|
+
untouchedInput: string,
|
|
226
|
+
options: IFlexSlashCommandExecutionOptions,
|
|
227
|
+
sessionAware: boolean,
|
|
228
|
+
): Promise<TFlexSlashCommandExecutionResult> {
|
|
229
|
+
const currentInvocation = this.slashCommandInvocationContext.getStore();
|
|
230
|
+
if (currentInvocation?.scopeId === scopeId && currentInvocation.sessionId === sessionId) {
|
|
231
|
+
throw this.core.trustInternalError(new FlexHarnessSlashCommandReentryError(sessionId));
|
|
232
|
+
}
|
|
233
|
+
const parsed = parseSlashCommand(untouchedInput);
|
|
234
|
+
if (parsed.type !== 'parsed') return parsed;
|
|
235
|
+
let registration = sessionAware ? undefined : this.core.slashCommands.get(parsed.name);
|
|
236
|
+
if (!sessionAware && !isReservedSlashCommandName(parsed.name) && !registration) {
|
|
237
|
+
return Object.freeze({
|
|
238
|
+
type: 'unknown',
|
|
239
|
+
input: parsed.input,
|
|
240
|
+
name: parsed.name,
|
|
241
|
+
rawArguments: parsed.rawArguments,
|
|
242
|
+
arguments: parsed.arguments,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
const promptOptions = validateSlashCommandExecutionOptions(options);
|
|
246
|
+
const { scope, state } = await this.core.resolveState(scopeId);
|
|
247
|
+
await state.scopeQueue;
|
|
248
|
+
this.core.assertOpen();
|
|
249
|
+
this.core.assertStateAcceptingWork(state);
|
|
250
|
+
const stored = this.core.requireSession(state, sessionId);
|
|
251
|
+
this.core.assertSessionWritable(stored);
|
|
252
|
+
if (sessionAware) {
|
|
253
|
+
const configuration = await this.core.configurations.resolveSessionConfiguration(
|
|
254
|
+
scopeId, scope.scope, state, stored, options.signal,
|
|
255
|
+
);
|
|
256
|
+
this.core.assertStateAcceptingWork(state);
|
|
257
|
+
if (state.sessions.get(sessionId) !== stored) {
|
|
258
|
+
throw this.core.trustInternalError(new FlexHarnessAbortError(
|
|
259
|
+
'The session was deleted during slash command resolution.',
|
|
260
|
+
));
|
|
261
|
+
}
|
|
262
|
+
registration = configuration.slashCommands.get(parsed.name);
|
|
263
|
+
if (!isReservedSlashCommandName(parsed.name) && !registration) {
|
|
264
|
+
return Object.freeze({
|
|
265
|
+
type: 'unknown',
|
|
266
|
+
input: parsed.input,
|
|
267
|
+
name: parsed.name,
|
|
268
|
+
rawArguments: parsed.rawArguments,
|
|
269
|
+
arguments: parsed.arguments,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
} else if (stored.session.configurationRef !== undefined) {
|
|
273
|
+
throw new FlexHarnessValidationError(
|
|
274
|
+
'A bound session requires executeSessionSlashCommand().',
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
const sessionKey = this.slashCommandSessionKey(state.storageKey, sessionId);
|
|
278
|
+
const invocationOwner = this.slashCommandInvocationContext.getStore();
|
|
279
|
+
if (invocationOwner?.sessionKey === sessionKey) {
|
|
280
|
+
throw this.core.trustInternalError(new FlexHarnessSlashCommandReentryError(sessionId));
|
|
281
|
+
}
|
|
282
|
+
if (this.activeSlashCommandExecutions.has(sessionKey)) {
|
|
283
|
+
throw new FlexHarnessSessionBusyError(
|
|
284
|
+
sessionId,
|
|
285
|
+
'already has an active slash command execution',
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
if (parsed.name === 'undo' || parsed.name === 'redo') {
|
|
289
|
+
if (parsed.arguments.length > 0) {
|
|
290
|
+
throw new FlexHarnessValidationError(`Slash command "/${parsed.name}" does not accept arguments.`);
|
|
291
|
+
}
|
|
292
|
+
await this.core.reversions.changeReversionCursorResolved(
|
|
293
|
+
scopeId,
|
|
294
|
+
sessionId,
|
|
295
|
+
parsed.name,
|
|
296
|
+
scope.scope,
|
|
297
|
+
state,
|
|
298
|
+
options.signal,
|
|
299
|
+
);
|
|
300
|
+
return Object.freeze({ type: 'operation', name: parsed.name });
|
|
301
|
+
}
|
|
302
|
+
if (parsed.name === 'compact') {
|
|
303
|
+
if (parsed.arguments.length > 0) {
|
|
304
|
+
throw new FlexHarnessValidationError('Slash command "/compact" does not accept arguments.');
|
|
305
|
+
}
|
|
306
|
+
const ignoredOption = (['system', 'maxSteps'] as const)
|
|
307
|
+
.find((key) => promptOptions[key] !== undefined);
|
|
308
|
+
if (ignoredOption) {
|
|
309
|
+
throw new FlexHarnessValidationError(
|
|
310
|
+
`Slash command "/compact" does not support the "${ignoredOption}" option.`,
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
if (!this.core.agentSessionPolicy.contextCompactor) {
|
|
314
|
+
throw new FlexHarnessSlashCommandUnavailableError(
|
|
315
|
+
parsed.name,
|
|
316
|
+
'No context compactor is configured.',
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
this.requireSessionAvailableForSlashCommand(state, stored, parsed.name);
|
|
320
|
+
return this.trackSlashCommandExecution(
|
|
321
|
+
scopeId,
|
|
322
|
+
state,
|
|
323
|
+
sessionId,
|
|
324
|
+
'operation',
|
|
325
|
+
options.signal,
|
|
326
|
+
async (signal) => {
|
|
327
|
+
await this.core.reversions.commitRevertedBranch(state, stored, scopeId, scope.scope);
|
|
328
|
+
await this.core.compaction.compactStoredSession(
|
|
329
|
+
scopeId,
|
|
330
|
+
scope.scope,
|
|
331
|
+
state,
|
|
332
|
+
stored,
|
|
333
|
+
promptOptions.modelHint,
|
|
334
|
+
signal,
|
|
335
|
+
);
|
|
336
|
+
return Object.freeze({ type: 'operation', name: parsed.name });
|
|
337
|
+
},
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
if (options.signal?.aborted) {
|
|
341
|
+
throw this.core.trustInternalError(new FlexHarnessAbortError('The slash command was aborted.'));
|
|
342
|
+
}
|
|
343
|
+
if (parsed.name === 'init' || typeof registration?.template === 'string') {
|
|
344
|
+
const template = parsed.name === 'init'
|
|
345
|
+
? FLEX_SLASH_COMMAND_INITIALIZE_TEMPLATE
|
|
346
|
+
: registration!.template!;
|
|
347
|
+
const expanded = expandSlashCommandTemplate(template, parsed.rawArguments, parsed.arguments);
|
|
348
|
+
normalizeFlexPrompt(expanded);
|
|
349
|
+
this.requireSlashCommandPromptAvailable(
|
|
350
|
+
state,
|
|
351
|
+
stored,
|
|
352
|
+
parsed.name,
|
|
353
|
+
this.core.prompts.promptAdmissionByteSize(expanded, promptOptions),
|
|
354
|
+
);
|
|
355
|
+
return this.trackSlashCommandExecution(
|
|
356
|
+
scopeId,
|
|
357
|
+
state,
|
|
358
|
+
sessionId,
|
|
359
|
+
'prompt-admission',
|
|
360
|
+
options.signal,
|
|
361
|
+
async (signal) => {
|
|
362
|
+
const queued = await this.core.prompts.enqueuePromptInternal(
|
|
363
|
+
scopeId,
|
|
364
|
+
sessionId,
|
|
365
|
+
expanded,
|
|
366
|
+
promptOptions,
|
|
367
|
+
undefined,
|
|
368
|
+
undefined,
|
|
369
|
+
undefined,
|
|
370
|
+
signal,
|
|
371
|
+
true,
|
|
372
|
+
);
|
|
373
|
+
const cancellation = this.core.trustInternalError(
|
|
374
|
+
new FlexHarnessAbortError('The slash command prompt was aborted.'),
|
|
375
|
+
);
|
|
376
|
+
const cancel = () => {
|
|
377
|
+
const entry = queued.queued;
|
|
378
|
+
if (!entry) return;
|
|
379
|
+
this.core.prompts.cancelQueuedPrompt(
|
|
380
|
+
entry,
|
|
381
|
+
cancellation,
|
|
382
|
+
this.core.compaction.createCompactorContext(scopeId, scope.scope, state.storageKey, sessionId),
|
|
383
|
+
);
|
|
384
|
+
};
|
|
385
|
+
const cancellationSignals = options.signal && options.signal !== signal
|
|
386
|
+
? [signal, options.signal]
|
|
387
|
+
: [signal];
|
|
388
|
+
for (const cancellationSignal of cancellationSignals) {
|
|
389
|
+
cancellationSignal.addEventListener('abort', cancel, { once: true });
|
|
390
|
+
}
|
|
391
|
+
if (cancellationSignals.some((cancellationSignal) => cancellationSignal.aborted)) cancel();
|
|
392
|
+
let admission: IFlexPromptAdmission;
|
|
393
|
+
try {
|
|
394
|
+
admission = await queued.started;
|
|
395
|
+
if (cancellationSignals.some((cancellationSignal) => cancellationSignal.aborted)) {
|
|
396
|
+
cancel();
|
|
397
|
+
throw cancellationSignals.find((cancellationSignal) => cancellationSignal.aborted)?.reason
|
|
398
|
+
?? cancellation;
|
|
399
|
+
}
|
|
400
|
+
} catch (error) {
|
|
401
|
+
for (const cancellationSignal of cancellationSignals) {
|
|
402
|
+
cancellationSignal.removeEventListener('abort', cancel);
|
|
403
|
+
}
|
|
404
|
+
throw error;
|
|
405
|
+
}
|
|
406
|
+
void admission.completion.finally(() => {
|
|
407
|
+
for (const cancellationSignal of cancellationSignals) {
|
|
408
|
+
cancellationSignal.removeEventListener('abort', cancel);
|
|
409
|
+
}
|
|
410
|
+
}).catch(() => undefined);
|
|
411
|
+
return Object.freeze({ type: 'prompt-admission', name: parsed.name, admission });
|
|
412
|
+
},
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
this.requireSessionAvailableForSlashCommand(state, stored, parsed.name);
|
|
416
|
+
return this.executeSlashCommandHandler(
|
|
417
|
+
scopeId,
|
|
418
|
+
scope.scope,
|
|
419
|
+
state,
|
|
420
|
+
stored,
|
|
421
|
+
parsed.name,
|
|
422
|
+
parsed.rawArguments,
|
|
423
|
+
parsed.arguments,
|
|
424
|
+
registration!.handler!,
|
|
425
|
+
options.signal,
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
private isSessionAvailableForSlashCommand(
|
|
430
|
+
state: IStorageState,
|
|
431
|
+
stored: IStoredSessionState,
|
|
432
|
+
allowPendingApply = false,
|
|
433
|
+
): boolean {
|
|
434
|
+
return this.core.prompts.isSessionRuntimeIdle(state, stored)
|
|
435
|
+
&& !this.activeSlashCommandExecutions.has(
|
|
436
|
+
this.slashCommandSessionKey(state.storageKey, stored.session.sessionId),
|
|
437
|
+
)
|
|
438
|
+
&& (stored.pendingReversion === undefined
|
|
439
|
+
|| (allowPendingApply && stored.pendingReversion.kind === 'apply'));
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
private slashCommandPromptUnavailableReason(
|
|
443
|
+
state: IStorageState,
|
|
444
|
+
stored: IStoredSessionState,
|
|
445
|
+
byteSize: number,
|
|
446
|
+
): string | undefined {
|
|
447
|
+
if (state.lifecycle !== 'active') return 'The session namespace is not accepting work.';
|
|
448
|
+
if (this.activeSlashCommandExecutions.has(
|
|
449
|
+
this.slashCommandSessionKey(state.storageKey, stored.session.sessionId),
|
|
450
|
+
)) return 'Session already has an active slash command execution.';
|
|
451
|
+
if (stored.pendingReversion) return 'Session has a pending reversion operation.';
|
|
452
|
+
if (stored.session.agent !== undefined) {
|
|
453
|
+
return 'Subagent sessions can only be prompted through the foreground delegate tool.';
|
|
454
|
+
}
|
|
455
|
+
if (stored.outstandingPromptsById.size >= this.core.promptQueueLimits.maxOutstandingPromptsPerSession) {
|
|
456
|
+
return 'Session has reached its outstanding prompt limit.';
|
|
457
|
+
}
|
|
458
|
+
if (stored.outstandingPromptBytes + byteSize > this.core.promptQueueLimits.maxOutstandingBytesPerSession) {
|
|
459
|
+
return 'Session has reached its outstanding prompt byte limit.';
|
|
460
|
+
}
|
|
461
|
+
if (this.core.prompts.pendingPromptAdmissions >= this.core.promptQueueLimits.maxPendingAdmissions) {
|
|
462
|
+
return 'FlexHarness has reached its pending prompt admission limit.';
|
|
463
|
+
}
|
|
464
|
+
if (this.core.prompts.pendingPromptAdmissionBytes + byteSize > this.core.promptQueueLimits.maxPendingAdmissionBytes) {
|
|
465
|
+
return 'FlexHarness has reached its pending prompt admission byte limit.';
|
|
466
|
+
}
|
|
467
|
+
return undefined;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
private slashCommandPromptAvailability(
|
|
471
|
+
state: IStorageState,
|
|
472
|
+
stored: IStoredSessionState,
|
|
473
|
+
byteSize: number,
|
|
474
|
+
): { available: boolean; unavailableReason?: string } {
|
|
475
|
+
const unavailableReason = this.slashCommandPromptUnavailableReason(state, stored, byteSize);
|
|
476
|
+
return unavailableReason === undefined
|
|
477
|
+
? { available: true }
|
|
478
|
+
: { available: false, unavailableReason };
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
private requireSlashCommandPromptAvailable(
|
|
482
|
+
state: IStorageState,
|
|
483
|
+
stored: IStoredSessionState,
|
|
484
|
+
commandName: string,
|
|
485
|
+
byteSize: number,
|
|
486
|
+
): void {
|
|
487
|
+
const reason = this.slashCommandPromptUnavailableReason(state, stored, byteSize);
|
|
488
|
+
if (reason) throw new FlexHarnessSlashCommandUnavailableError(commandName, reason);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
public slashCommandSessionKey(storageKey: string, sessionId: string): string {
|
|
492
|
+
return JSON.stringify([storageKey, sessionId]);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
public requireSessionAvailableForSlashCommand(
|
|
496
|
+
state: IStorageState,
|
|
497
|
+
stored: IStoredSessionState,
|
|
498
|
+
commandName: string,
|
|
499
|
+
): void {
|
|
500
|
+
if (!this.isSessionAvailableForSlashCommand(state, stored)) {
|
|
501
|
+
throw new FlexHarnessSlashCommandUnavailableError(
|
|
502
|
+
commandName,
|
|
503
|
+
slashCommandSessionUnavailableReason,
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
public trackSlashCommandExecution<TResult>(
|
|
509
|
+
scopeId: string,
|
|
510
|
+
state: IStorageState,
|
|
511
|
+
sessionId: string,
|
|
512
|
+
kind: IActiveSlashCommandExecution['kind'],
|
|
513
|
+
externalSignal: AbortSignal | undefined,
|
|
514
|
+
operation: (signal: AbortSignal) => Promise<TResult>,
|
|
515
|
+
): Promise<TResult> {
|
|
516
|
+
const sessionKey = this.slashCommandSessionKey(state.storageKey, sessionId);
|
|
517
|
+
if (this.activeSlashCommandExecutions.has(sessionKey)) {
|
|
518
|
+
throw new FlexHarnessSessionBusyError(
|
|
519
|
+
sessionId,
|
|
520
|
+
'already has an active slash command execution',
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
const controller = new AbortController();
|
|
524
|
+
const abortFromExternalSignal = () => {
|
|
525
|
+
if (!controller.signal.aborted) {
|
|
526
|
+
controller.abort(this.core.trustInternalError(
|
|
527
|
+
new FlexHarnessAbortError('The slash command was aborted.'),
|
|
528
|
+
));
|
|
529
|
+
}
|
|
530
|
+
};
|
|
531
|
+
externalSignal?.addEventListener('abort', abortFromExternalSignal, { once: true });
|
|
532
|
+
if (externalSignal?.aborted) abortFromExternalSignal();
|
|
533
|
+
const activityOwner = Object.freeze({ scopeId, storageKey: state.storageKey, sessionId });
|
|
534
|
+
const completion = Promise.resolve().then(() => this.slashCommandActivityContext.run(activityOwner, () => {
|
|
535
|
+
if (controller.signal.aborted) throw controller.signal.reason;
|
|
536
|
+
return operation(controller.signal);
|
|
537
|
+
}));
|
|
538
|
+
const active: IActiveSlashCommandExecution = {
|
|
539
|
+
kind,
|
|
540
|
+
storageKey: state.storageKey,
|
|
541
|
+
sessionId,
|
|
542
|
+
controller,
|
|
543
|
+
completion,
|
|
544
|
+
};
|
|
545
|
+
this.activeSlashCommandExecutions.set(sessionKey, active);
|
|
546
|
+
return completion.finally(() => {
|
|
547
|
+
externalSignal?.removeEventListener('abort', abortFromExternalSignal);
|
|
548
|
+
if (this.activeSlashCommandExecutions.get(sessionKey) === active) {
|
|
549
|
+
this.activeSlashCommandExecutions.delete(sessionKey);
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
private trackSlashCommandListing<TResult>(
|
|
555
|
+
scopeId: string,
|
|
556
|
+
state: IStorageState,
|
|
557
|
+
sessionId: string,
|
|
558
|
+
operation: (signal: AbortSignal) => Promise<TResult>,
|
|
559
|
+
): Promise<TResult> {
|
|
560
|
+
const controller = new AbortController();
|
|
561
|
+
const activityOwner = Object.freeze({ scopeId, storageKey: state.storageKey, sessionId });
|
|
562
|
+
const completion = Promise.resolve().then(() =>
|
|
563
|
+
this.slashCommandActivityContext.run(activityOwner, () => operation(controller.signal)));
|
|
564
|
+
const active: IActiveSlashCommandListing = {
|
|
565
|
+
storageKey: state.storageKey,
|
|
566
|
+
sessionId,
|
|
567
|
+
controller,
|
|
568
|
+
completion,
|
|
569
|
+
};
|
|
570
|
+
this.activeSlashCommandListings.add(active);
|
|
571
|
+
return completion.finally(() => {
|
|
572
|
+
this.activeSlashCommandListings.delete(active);
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
private async executeSlashCommandHandler(
|
|
577
|
+
scopeId: string,
|
|
578
|
+
scope: TScope,
|
|
579
|
+
state: IStorageState,
|
|
580
|
+
stored: IStoredSessionState,
|
|
581
|
+
commandName: string,
|
|
582
|
+
rawArguments: string,
|
|
583
|
+
arguments_: readonly string[],
|
|
584
|
+
handler: IFlexSlashCommandHandlerRegistration<TScope>['handler'],
|
|
585
|
+
externalSignal?: AbortSignal,
|
|
586
|
+
): Promise<TFlexSlashCommandExecutionResult> {
|
|
587
|
+
const sessionKey = this.slashCommandSessionKey(state.storageKey, stored.session.sessionId);
|
|
588
|
+
const invocationOwner = this.slashCommandInvocationContext.getStore();
|
|
589
|
+
if (invocationOwner?.sessionKey === sessionKey) {
|
|
590
|
+
throw this.core.trustInternalError(
|
|
591
|
+
new FlexHarnessSlashCommandReentryError(stored.session.sessionId),
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
if (this.activeSlashCommandExecutions.has(sessionKey)) {
|
|
595
|
+
throw new FlexHarnessSessionBusyError(
|
|
596
|
+
stored.session.sessionId,
|
|
597
|
+
'already has an active slash command execution',
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
const owner = Object.freeze({
|
|
601
|
+
scopeId,
|
|
602
|
+
storageKey: state.storageKey,
|
|
603
|
+
sessionId: stored.session.sessionId,
|
|
604
|
+
sessionKey,
|
|
605
|
+
});
|
|
606
|
+
return this.trackSlashCommandExecution(
|
|
607
|
+
scopeId,
|
|
608
|
+
state,
|
|
609
|
+
stored.session.sessionId,
|
|
610
|
+
'handler',
|
|
611
|
+
externalSignal,
|
|
612
|
+
(signal) => this.slashCommandInvocationContext.run(owner, async () => {
|
|
613
|
+
const context = Object.freeze({
|
|
614
|
+
scopeId,
|
|
615
|
+
scope,
|
|
616
|
+
storageKey: state.storageKey,
|
|
617
|
+
sessionId: stored.session.sessionId,
|
|
618
|
+
...requireSessionGeneration(stored.session),
|
|
619
|
+
rawArguments,
|
|
620
|
+
arguments: arguments_,
|
|
621
|
+
signal,
|
|
622
|
+
});
|
|
623
|
+
try {
|
|
624
|
+
await this.core.reversions.commitRevertedBranch(state, stored, scopeId, scope);
|
|
625
|
+
const result = await handler(context);
|
|
626
|
+
return Object.freeze({
|
|
627
|
+
type: 'handler-result' as const,
|
|
628
|
+
name: commandName,
|
|
629
|
+
result: normalizeJsonValue(result === undefined ? null : result, this.core.toolOutputLimits),
|
|
630
|
+
});
|
|
631
|
+
} catch (error) {
|
|
632
|
+
throw this.core.projectOperationError(
|
|
633
|
+
error,
|
|
634
|
+
'slashCommand',
|
|
635
|
+
scopeId,
|
|
636
|
+
stored.session.sessionId,
|
|
637
|
+
`slash-command:${commandName}`,
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
}),
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
public async abortSlashCommandExecutions(
|
|
645
|
+
storageKey: string | undefined,
|
|
646
|
+
reason: FlexHarnessAbortError,
|
|
647
|
+
errors: unknown[],
|
|
648
|
+
sessionId?: string,
|
|
649
|
+
): Promise<void> {
|
|
650
|
+
const active = [...this.activeSlashCommandExecutions.values()]
|
|
651
|
+
.filter((execution) =>
|
|
652
|
+
(storageKey === undefined || execution.storageKey === storageKey)
|
|
653
|
+
&& (sessionId === undefined || execution.sessionId === sessionId));
|
|
654
|
+
for (const execution of active) {
|
|
655
|
+
if (!execution.controller.signal.aborted) execution.controller.abort(reason);
|
|
656
|
+
}
|
|
657
|
+
const awaited = active.filter((execution) => execution.kind !== 'prompt-admission');
|
|
658
|
+
const results = await Promise.allSettled(awaited.map((execution) => execution.completion));
|
|
659
|
+
for (let index = 0; index < results.length; index++) {
|
|
660
|
+
const result = results[index];
|
|
661
|
+
if (result.status === 'rejected' && awaited[index].kind === 'handler') {
|
|
662
|
+
this.core.appendUnexpectedErrors(errors, result.reason);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
public abortSlashCommandListings(
|
|
668
|
+
storageKey: string | undefined,
|
|
669
|
+
reason: FlexHarnessAbortError,
|
|
670
|
+
sessionId?: string,
|
|
671
|
+
): Promise<void> | undefined {
|
|
672
|
+
const active = [...this.activeSlashCommandListings]
|
|
673
|
+
.filter((listing) =>
|
|
674
|
+
(storageKey === undefined || listing.storageKey === storageKey)
|
|
675
|
+
&& (sessionId === undefined || listing.sessionId === sessionId));
|
|
676
|
+
if (active.length === 0) return undefined;
|
|
677
|
+
for (const listing of active) {
|
|
678
|
+
if (!listing.controller.signal.aborted) listing.controller.abort(reason);
|
|
679
|
+
}
|
|
680
|
+
return Promise.allSettled(active.map((listing) => listing.completion)).then(() => undefined);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
public ambientSlashCommandOwner(
|
|
684
|
+
criteria: { scopeId?: string; storageKey?: string; sessionId?: string } = {},
|
|
685
|
+
): ISlashCommandInvocationOwner | ISlashCommandActivityOwner | undefined {
|
|
686
|
+
const matches = (owner: ISlashCommandInvocationOwner | ISlashCommandActivityOwner) => (
|
|
687
|
+
(criteria.scopeId === undefined || owner.scopeId === criteria.scopeId)
|
|
688
|
+
&& (criteria.storageKey === undefined || owner.storageKey === criteria.storageKey)
|
|
689
|
+
&& (criteria.sessionId === undefined || owner.sessionId === criteria.sessionId)
|
|
690
|
+
);
|
|
691
|
+
const invocationOwner = this.slashCommandInvocationContext.getStore();
|
|
692
|
+
if (invocationOwner && matches(invocationOwner)) return invocationOwner;
|
|
693
|
+
const activityOwner = this.slashCommandActivityContext.getStore();
|
|
694
|
+
return activityOwner && matches(activityOwner) ? activityOwner : undefined;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
public assertNoAmbientSlashCommandTeardown(
|
|
698
|
+
criteria: { scopeId?: string; storageKey?: string; sessionId?: string } = {},
|
|
699
|
+
): void {
|
|
700
|
+
const owner = this.ambientSlashCommandOwner(criteria);
|
|
701
|
+
if (owner) {
|
|
702
|
+
throw this.core.trustInternalError(new FlexHarnessSlashCommandReentryError(owner.sessionId));
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|