@modelprofile.com/flexharness 4.0.2 → 4.1.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/changelog.md +23 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.flexharness.d.ts +1 -0
- package/dist_ts/classes.flexharness.js +36 -664
- package/dist_ts/classes.stores.js +2 -198
- package/dist_ts/interfaces.d.ts +1 -0
- package/dist_ts/interfaces.js +1 -1
- package/dist_ts/plugins.d.ts +1 -1
- package/dist_ts/utils.harnessoptions.d.ts +21 -0
- package/dist_ts/utils.harnessoptions.js +267 -0
- package/dist_ts/utils.slashcommands.d.ts +1 -0
- package/dist_ts/utils.slashcommands.js +5 -1
- package/dist_ts/utils.storecodecs.d.ts +9 -0
- package/dist_ts/utils.storecodecs.js +201 -0
- package/dist_ts/utils.tooloutput.d.ts +2 -0
- package/dist_ts/utils.tooloutput.js +56 -0
- package/dist_ts/utils.transfer.d.ts +24 -0
- package/dist_ts/utils.transfer.js +195 -0
- package/dist_ts/utils.validation.d.ts +13 -0
- package/dist_ts/utils.validation.js +158 -0
- package/package.json +2 -2
- package/readme.hints.md +8 -0
- package/readme.md +2 -2
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.flexharness.ts +76 -815
- package/ts/classes.stores.ts +10 -243
- package/ts/interfaces.ts +3 -0
- package/ts/plugins.ts +1 -0
- package/ts/utils.harnessoptions.ts +372 -0
- package/ts/utils.slashcommands.ts +5 -0
- package/ts/utils.storecodecs.ts +246 -0
- package/ts/utils.tooloutput.ts +72 -0
- package/ts/utils.transfer.ts +210 -0
- package/ts/utils.validation.ts +204 -0
package/ts/classes.stores.ts
CHANGED
|
@@ -34,6 +34,16 @@ import {
|
|
|
34
34
|
assertFlexProjectManagementSnapshot,
|
|
35
35
|
assertFlexProjectManagementTombstone,
|
|
36
36
|
} from './utils.projectmanagement.js';
|
|
37
|
+
import {
|
|
38
|
+
assertExactKeys,
|
|
39
|
+
assertRecord,
|
|
40
|
+
createAgentEventSnapshot,
|
|
41
|
+
createToolJobSnapshot,
|
|
42
|
+
normalizeJsonObjectUndefined,
|
|
43
|
+
validateAgentEventArchive,
|
|
44
|
+
validateAgentEventSnapshot,
|
|
45
|
+
validateToolJobSnapshot,
|
|
46
|
+
} from './utils.storecodecs.js';
|
|
37
47
|
|
|
38
48
|
type TSnapshot =
|
|
39
49
|
| IFlexScopeSnapshot
|
|
@@ -130,249 +140,6 @@ function assertProjectionSession(snapshot: TFlexProjectionSnapshot, sessionId: s
|
|
|
130
140
|
}
|
|
131
141
|
}
|
|
132
142
|
|
|
133
|
-
function normalizeJsonObjectUndefined<TValue>(value: TValue, path = '$'): TValue {
|
|
134
|
-
const seen = new WeakSet<object>();
|
|
135
|
-
const visit = (current: unknown, currentPath: string): unknown => {
|
|
136
|
-
if (
|
|
137
|
-
current === null
|
|
138
|
-
|| typeof current === 'string'
|
|
139
|
-
|| typeof current === 'boolean'
|
|
140
|
-
) return current;
|
|
141
|
-
if (typeof current === 'number') {
|
|
142
|
-
if (!Number.isFinite(current)) {
|
|
143
|
-
throw new FlexHarnessStoreFormatError(`${currentPath} contains a non-finite number.`);
|
|
144
|
-
}
|
|
145
|
-
return current;
|
|
146
|
-
}
|
|
147
|
-
if (typeof current !== 'object') {
|
|
148
|
-
throw new FlexHarnessStoreFormatError(
|
|
149
|
-
`${currentPath} contains ${typeof current}, which is not JSON-safe.`,
|
|
150
|
-
);
|
|
151
|
-
}
|
|
152
|
-
if (seen.has(current)) {
|
|
153
|
-
throw new FlexHarnessStoreFormatError(`${currentPath} contains a circular reference.`);
|
|
154
|
-
}
|
|
155
|
-
const prototype = Object.getPrototypeOf(current);
|
|
156
|
-
if (!Array.isArray(current) && prototype !== Object.prototype && prototype !== null) {
|
|
157
|
-
throw new FlexHarnessStoreFormatError(`${currentPath} contains a non-plain object.`);
|
|
158
|
-
}
|
|
159
|
-
if (Object.getOwnPropertySymbols(current).length > 0) {
|
|
160
|
-
throw new FlexHarnessStoreFormatError(`${currentPath} contains a symbol-keyed property.`);
|
|
161
|
-
}
|
|
162
|
-
seen.add(current);
|
|
163
|
-
if (Array.isArray(current)) {
|
|
164
|
-
const result = current.map((entry, index) => {
|
|
165
|
-
if (!(index in current) || entry === undefined) {
|
|
166
|
-
throw new FlexHarnessStoreFormatError(`${currentPath}[${index}] is not JSON-safe.`);
|
|
167
|
-
}
|
|
168
|
-
return visit(entry, `${currentPath}[${index}]`);
|
|
169
|
-
});
|
|
170
|
-
seen.delete(current);
|
|
171
|
-
return result;
|
|
172
|
-
}
|
|
173
|
-
const result: Record<string, unknown> = {};
|
|
174
|
-
for (const key of Object.getOwnPropertyNames(current)) {
|
|
175
|
-
const descriptor = Object.getOwnPropertyDescriptor(current, key)!;
|
|
176
|
-
if (!descriptor.enumerable || !('value' in descriptor)) {
|
|
177
|
-
throw new FlexHarnessStoreFormatError(`${currentPath}.${key} is not a plain JSON property.`);
|
|
178
|
-
}
|
|
179
|
-
if (descriptor.value !== undefined) {
|
|
180
|
-
result[key] = visit(descriptor.value, `${currentPath}.${key}`);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
seen.delete(current);
|
|
184
|
-
return result;
|
|
185
|
-
};
|
|
186
|
-
const normalized = visit(value, path);
|
|
187
|
-
assertJsonSerializable(normalized, path);
|
|
188
|
-
return normalized as TValue;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function assertNonEmptyString(value: unknown, path: string): asserts value is string {
|
|
192
|
-
if (typeof value !== 'string' || !value.trim()) {
|
|
193
|
-
throw new FlexHarnessStoreFormatError(`${path} must be a non-empty string.`);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
function assertNonNegativeInteger(value: unknown, path: string): asserts value is number {
|
|
198
|
-
if (!Number.isSafeInteger(value) || Number(value) < 0) {
|
|
199
|
-
throw new FlexHarnessStoreFormatError(`${path} must be a non-negative integer.`);
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
function assertExactKeys(
|
|
204
|
-
value: Record<string, unknown>,
|
|
205
|
-
keys: readonly string[],
|
|
206
|
-
path: string,
|
|
207
|
-
): void {
|
|
208
|
-
const invalidKey = Object.keys(value).find((key) => !keys.includes(key));
|
|
209
|
-
if (invalidKey) {
|
|
210
|
-
throw new FlexHarnessStoreFormatError(`${path}.${invalidKey} is not supported.`);
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
function assertRecord(value: unknown, path: string): asserts value is Record<string, unknown> {
|
|
215
|
-
if (
|
|
216
|
-
!value
|
|
217
|
-
|| typeof value !== 'object'
|
|
218
|
-
|| Array.isArray(value)
|
|
219
|
-
|| (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null)
|
|
220
|
-
) {
|
|
221
|
-
throw new FlexHarnessStoreFormatError(`${path} must be a plain object.`);
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
function createAgentEventSnapshot(
|
|
226
|
-
sessionId: string,
|
|
227
|
-
events: readonly plugins.TAgentEvent[],
|
|
228
|
-
revision: number,
|
|
229
|
-
): plugins.IAgentEventSnapshotV2 {
|
|
230
|
-
return {
|
|
231
|
-
schemaVersion: 2,
|
|
232
|
-
sessionId,
|
|
233
|
-
revision,
|
|
234
|
-
updatedAt: events.reduce((latest, event) => Math.max(latest, event.timestamp), 0),
|
|
235
|
-
events: normalizeJsonObjectUndefined([...events], '$events'),
|
|
236
|
-
};
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
function validateAgentEventSnapshot(
|
|
240
|
-
value: unknown,
|
|
241
|
-
sessionId: string,
|
|
242
|
-
): plugins.IAgentEventSnapshotV2 {
|
|
243
|
-
assertJsonSerializable(value, '$snapshot');
|
|
244
|
-
try {
|
|
245
|
-
return plugins.validateAgentEventSnapshotV2(value, sessionId);
|
|
246
|
-
} catch (error) {
|
|
247
|
-
throw new FlexHarnessStoreFormatError(
|
|
248
|
-
`$snapshot is not a canonical schema-2 Agent event snapshot: ${error instanceof Error ? error.message : String(error)}`,
|
|
249
|
-
{ cause: error },
|
|
250
|
-
);
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
function validateAgentEventArchive(
|
|
255
|
-
value: unknown,
|
|
256
|
-
sessionId: string,
|
|
257
|
-
archiveId: string,
|
|
258
|
-
): plugins.IAgentEventArchiveV2 {
|
|
259
|
-
assertJsonSerializable(value, '$archive');
|
|
260
|
-
try {
|
|
261
|
-
return plugins.validateAgentEventArchiveV2(value, sessionId, archiveId);
|
|
262
|
-
} catch (error) {
|
|
263
|
-
throw new FlexHarnessStoreFormatError(
|
|
264
|
-
`$archive is not a canonical schema-2 Agent event archive: ${error instanceof Error ? error.message : String(error)}`,
|
|
265
|
-
{ cause: error },
|
|
266
|
-
);
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
function validateToolJobSnapshot(value: unknown): plugins.IToolJobSnapshot {
|
|
271
|
-
assertJsonSerializable(value, '$snapshot');
|
|
272
|
-
assertRecord(value, '$snapshot');
|
|
273
|
-
assertExactKeys(value, ['schemaVersion', 'revision', 'updatedAt', 'jobs'], '$snapshot');
|
|
274
|
-
if (value.schemaVersion !== 1) {
|
|
275
|
-
throw new FlexHarnessStoreFormatError('Tool job snapshot schemaVersion must be 1.');
|
|
276
|
-
}
|
|
277
|
-
assertNonNegativeInteger(value.revision, '$snapshot.revision');
|
|
278
|
-
assertNonNegativeInteger(value.updatedAt, '$snapshot.updatedAt');
|
|
279
|
-
if (!Array.isArray(value.jobs)) {
|
|
280
|
-
throw new FlexHarnessStoreFormatError('$snapshot.jobs must be an array.');
|
|
281
|
-
}
|
|
282
|
-
const executionIds = new Set<string>();
|
|
283
|
-
for (let index = 0; index < value.jobs.length; index++) {
|
|
284
|
-
const path = `$snapshot.jobs[${index}]`;
|
|
285
|
-
assertRecord(value.jobs[index], path);
|
|
286
|
-
const job = value.jobs[index];
|
|
287
|
-
assertExactKeys(
|
|
288
|
-
job,
|
|
289
|
-
[
|
|
290
|
-
'executionId',
|
|
291
|
-
'type',
|
|
292
|
-
'state',
|
|
293
|
-
'request',
|
|
294
|
-
'exitCode',
|
|
295
|
-
'stdout',
|
|
296
|
-
'stderr',
|
|
297
|
-
'signal',
|
|
298
|
-
'startedAt',
|
|
299
|
-
'updatedAt',
|
|
300
|
-
'finishedAt',
|
|
301
|
-
],
|
|
302
|
-
path,
|
|
303
|
-
);
|
|
304
|
-
assertNonEmptyString(job.executionId, `${path}.executionId`);
|
|
305
|
-
if (executionIds.has(job.executionId)) {
|
|
306
|
-
throw new FlexHarnessStoreFormatError(`$snapshot.jobs contains duplicate job "${job.executionId}".`);
|
|
307
|
-
}
|
|
308
|
-
executionIds.add(job.executionId);
|
|
309
|
-
assertNonEmptyString(job.type, `${path}.type`);
|
|
310
|
-
if (!['running', 'finished', 'failed', 'aborted'].includes(String(job.state))) {
|
|
311
|
-
throw new FlexHarnessStoreFormatError(`${path}.state is invalid.`);
|
|
312
|
-
}
|
|
313
|
-
if (job.request !== undefined) {
|
|
314
|
-
const requestPath = `${path}.request`;
|
|
315
|
-
assertRecord(job.request, requestPath);
|
|
316
|
-
assertExactKeys(
|
|
317
|
-
job.request,
|
|
318
|
-
['type', 'command', 'cwd', 'timeoutMs', 'metadata'],
|
|
319
|
-
requestPath,
|
|
320
|
-
);
|
|
321
|
-
assertNonEmptyString(job.request.type, `${requestPath}.type`);
|
|
322
|
-
if (job.request.command !== undefined) {
|
|
323
|
-
const commandPath = `${requestPath}.command`;
|
|
324
|
-
assertRecord(job.request.command, commandPath);
|
|
325
|
-
assertExactKeys(job.request.command, ['executable', 'args'], commandPath);
|
|
326
|
-
assertNonEmptyString(job.request.command.executable, `${commandPath}.executable`);
|
|
327
|
-
if (!Array.isArray(job.request.command.args)) {
|
|
328
|
-
throw new FlexHarnessStoreFormatError(`${commandPath}.args must be an array.`);
|
|
329
|
-
}
|
|
330
|
-
for (let argumentIndex = 0; argumentIndex < job.request.command.args.length; argumentIndex++) {
|
|
331
|
-
if (typeof job.request.command.args[argumentIndex] !== 'string') {
|
|
332
|
-
throw new FlexHarnessStoreFormatError(
|
|
333
|
-
`${commandPath}.args[${argumentIndex}] must be a string.`,
|
|
334
|
-
);
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
if (job.request.cwd !== undefined && typeof job.request.cwd !== 'string') {
|
|
339
|
-
throw new FlexHarnessStoreFormatError(`${requestPath}.cwd must be a string.`);
|
|
340
|
-
}
|
|
341
|
-
if (job.request.timeoutMs !== undefined) {
|
|
342
|
-
assertNonNegativeInteger(job.request.timeoutMs, `${requestPath}.timeoutMs`);
|
|
343
|
-
}
|
|
344
|
-
if (job.request.metadata !== undefined) {
|
|
345
|
-
assertRecord(job.request.metadata, `${requestPath}.metadata`);
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
if (job.exitCode !== undefined && job.exitCode !== null && !Number.isSafeInteger(job.exitCode)) {
|
|
349
|
-
throw new FlexHarnessStoreFormatError(`${path}.exitCode must be an integer or null.`);
|
|
350
|
-
}
|
|
351
|
-
for (const key of ['stdout', 'stderr', 'signal'] as const) {
|
|
352
|
-
if (job[key] !== undefined && typeof job[key] !== 'string') {
|
|
353
|
-
throw new FlexHarnessStoreFormatError(`${path}.${key} must be a string.`);
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
for (const key of ['startedAt', 'updatedAt', 'finishedAt'] as const) {
|
|
357
|
-
if (job[key] !== undefined) assertNonNegativeInteger(job[key], `${path}.${key}`);
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
return value as unknown as plugins.IToolJobSnapshot;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
function createToolJobSnapshot(
|
|
364
|
-
jobs: readonly plugins.IToolJobState[],
|
|
365
|
-
revision: number,
|
|
366
|
-
): plugins.IToolJobSnapshot {
|
|
367
|
-
const snapshot: plugins.IToolJobSnapshot = {
|
|
368
|
-
schemaVersion: 1,
|
|
369
|
-
revision,
|
|
370
|
-
updatedAt: Date.now(),
|
|
371
|
-
jobs: normalizeJsonObjectUndefined([...jobs], '$jobs'),
|
|
372
|
-
};
|
|
373
|
-
return validateToolJobSnapshot(snapshot);
|
|
374
|
-
}
|
|
375
|
-
|
|
376
143
|
class InMemoryScopeStore implements IFlexScopeStore {
|
|
377
144
|
private readonly snapshots = new Map<string, IFlexScopeSnapshot>();
|
|
378
145
|
|
package/ts/interfaces.ts
CHANGED
|
@@ -16,6 +16,9 @@ export type TFlexAgentModelMessage = ReturnType<plugins.IAgentSession['getModelM
|
|
|
16
16
|
export type TFlexAgentToolSet = NonNullable<TFlexAgentSessionOptions['tools']>;
|
|
17
17
|
export type TFlexAgentProviderOptions = TFlexAgentSessionOptions['providerOptions'];
|
|
18
18
|
export type TFlexAgentCacheSetting = TFlexAgentSessionOptions['cache'];
|
|
19
|
+
export type TFlexAgentToolCallUpdateEvent = Parameters<
|
|
20
|
+
NonNullable<TFlexAgentSessionOptions['onToolCallUpdate']>
|
|
21
|
+
>[0];
|
|
19
22
|
export type TFlexAgentToolCallFinishEvent = Parameters<
|
|
20
23
|
NonNullable<TFlexAgentSessionOptions['onToolCallFinish']>
|
|
21
24
|
>[0];
|
package/ts/plugins.ts
CHANGED
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import { FlexHarnessValidationError } from './errors.js';
|
|
2
|
+
import {
|
|
3
|
+
FLEX_REVERSION_DEFAULT_LIMITS,
|
|
4
|
+
FLEX_REVERSION_MAXIMUM_LIMITS,
|
|
5
|
+
} from './interfaces.js';
|
|
6
|
+
import type {
|
|
7
|
+
IFlexAgentSessionPolicy,
|
|
8
|
+
IFlexCallbackLimits,
|
|
9
|
+
IFlexHarnessOptions,
|
|
10
|
+
IFlexHarnessStores,
|
|
11
|
+
IFlexPromptQueueLimits,
|
|
12
|
+
IFlexReversionLimits,
|
|
13
|
+
IFlexSlashCommandHandlerRegistration,
|
|
14
|
+
IFlexSlashCommandTemplateRegistration,
|
|
15
|
+
IFlexSubagentDefinition,
|
|
16
|
+
TFlexSlashCommandRegistration,
|
|
17
|
+
} from './interfaces.js';
|
|
18
|
+
import {
|
|
19
|
+
FLEX_SLASH_COMMAND_MAX_INPUT_BYTES,
|
|
20
|
+
isReservedSlashCommandName,
|
|
21
|
+
isValidSlashCommandName,
|
|
22
|
+
} from './utils.slashcommands.js';
|
|
23
|
+
import { validateUtf8String } from './utils.validation.js';
|
|
24
|
+
|
|
25
|
+
export type TRegisteredSlashCommand<TScope> =
|
|
26
|
+
| Readonly<IFlexSlashCommandTemplateRegistration>
|
|
27
|
+
| Readonly<IFlexSlashCommandHandlerRegistration<TScope>>;
|
|
28
|
+
|
|
29
|
+
export interface INormalizedProjectManagementTools {
|
|
30
|
+
readonly task: boolean;
|
|
31
|
+
readonly goal: boolean;
|
|
32
|
+
readonly scratchpad: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface INormalizedBuiltInTools {
|
|
36
|
+
readonly renameSession: boolean;
|
|
37
|
+
readonly projectManagement?: INormalizedProjectManagementTools;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const DEFAULT_CALLBACK_LIMITS: Required<IFlexCallbackLimits> = {
|
|
41
|
+
maxEvents: 10_000,
|
|
42
|
+
maxOutputBytes: 1024 * 1024,
|
|
43
|
+
maxParts: 2_000,
|
|
44
|
+
};
|
|
45
|
+
const DEFAULT_PROMPT_QUEUE_LIMITS: Required<IFlexPromptQueueLimits> = {
|
|
46
|
+
maxOutstandingPromptsPerSession: 16,
|
|
47
|
+
maxOutstandingBytesPerSession: 64 * 1024 * 1024,
|
|
48
|
+
maxPendingAdmissions: 64,
|
|
49
|
+
maxPendingAdmissionBytes: 128 * 1024 * 1024,
|
|
50
|
+
maxTerminalEntriesPerSession: 64,
|
|
51
|
+
};
|
|
52
|
+
const maxSubagentDefinitions = 32;
|
|
53
|
+
export const maxSubagentNameBytes = 128;
|
|
54
|
+
const maxSubagentDescriptionBytes = 2048;
|
|
55
|
+
const maxSubagentModelHintBytes = 512;
|
|
56
|
+
const maxSubagentSystemBytes = 64 * 1024;
|
|
57
|
+
const maxSlashCommandRegistrations = 128;
|
|
58
|
+
const maxSlashCommandDescriptionBytes = 2048;
|
|
59
|
+
const maxSlashCommandTemplateBytes = FLEX_SLASH_COMMAND_MAX_INPUT_BYTES;
|
|
60
|
+
|
|
61
|
+
export function normalizeSubagents(
|
|
62
|
+
definitions: IFlexSubagentDefinition[] | undefined,
|
|
63
|
+
): ReadonlyMap<string, Readonly<IFlexSubagentDefinition>> {
|
|
64
|
+
if (definitions === undefined) return new Map();
|
|
65
|
+
if (!Array.isArray(definitions) || definitions.length > maxSubagentDefinitions) {
|
|
66
|
+
throw new FlexHarnessValidationError(
|
|
67
|
+
`subagents must be an array with at most ${maxSubagentDefinitions} definitions.`,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
const normalized = new Map<string, Readonly<IFlexSubagentDefinition>>();
|
|
71
|
+
for (let index = 0; index < definitions.length; index++) {
|
|
72
|
+
const definition = definitions[index];
|
|
73
|
+
if (
|
|
74
|
+
!definition
|
|
75
|
+
|| typeof definition !== 'object'
|
|
76
|
+
|| Array.isArray(definition)
|
|
77
|
+
|| ![Object.prototype, null].includes(Object.getPrototypeOf(definition))
|
|
78
|
+
) {
|
|
79
|
+
throw new FlexHarnessValidationError(`subagents[${index}] must be a plain object.`);
|
|
80
|
+
}
|
|
81
|
+
const unsupported = Object.keys(definition).find(
|
|
82
|
+
(key) => !['name', 'description', 'modelHint', 'system', 'maxSteps'].includes(key),
|
|
83
|
+
);
|
|
84
|
+
if (unsupported) {
|
|
85
|
+
throw new FlexHarnessValidationError(`subagents[${index}] does not support "${unsupported}".`);
|
|
86
|
+
}
|
|
87
|
+
validateUtf8String(definition.name, `subagents[${index}].name`, maxSubagentNameBytes, true);
|
|
88
|
+
validateUtf8String(
|
|
89
|
+
definition.description,
|
|
90
|
+
`subagents[${index}].description`,
|
|
91
|
+
maxSubagentDescriptionBytes,
|
|
92
|
+
);
|
|
93
|
+
if (definition.modelHint !== undefined) {
|
|
94
|
+
validateUtf8String(
|
|
95
|
+
definition.modelHint,
|
|
96
|
+
`subagents[${index}].modelHint`,
|
|
97
|
+
maxSubagentModelHintBytes,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
if (definition.system !== undefined) {
|
|
101
|
+
validateUtf8String(
|
|
102
|
+
definition.system,
|
|
103
|
+
`subagents[${index}].system`,
|
|
104
|
+
maxSubagentSystemBytes,
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
if (
|
|
108
|
+
definition.maxSteps !== undefined
|
|
109
|
+
&& (!Number.isSafeInteger(definition.maxSteps) || definition.maxSteps < 1)
|
|
110
|
+
) {
|
|
111
|
+
throw new FlexHarnessValidationError(`subagents[${index}].maxSteps must be a positive integer.`);
|
|
112
|
+
}
|
|
113
|
+
if (normalized.has(definition.name)) {
|
|
114
|
+
throw new FlexHarnessValidationError(`Duplicate subagent definition "${definition.name}".`);
|
|
115
|
+
}
|
|
116
|
+
normalized.set(definition.name, Object.freeze({
|
|
117
|
+
name: definition.name,
|
|
118
|
+
description: definition.description,
|
|
119
|
+
...(definition.modelHint === undefined ? {} : { modelHint: definition.modelHint }),
|
|
120
|
+
...(definition.system === undefined ? {} : { system: definition.system }),
|
|
121
|
+
...(definition.maxSteps === undefined ? {} : { maxSteps: definition.maxSteps }),
|
|
122
|
+
}));
|
|
123
|
+
}
|
|
124
|
+
return Object.freeze(normalized);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function normalizeBuiltInTools(
|
|
128
|
+
options: IFlexHarnessOptions<unknown>['builtInTools'],
|
|
129
|
+
): INormalizedBuiltInTools {
|
|
130
|
+
if (options === undefined) return Object.freeze({ renameSession: false });
|
|
131
|
+
if (
|
|
132
|
+
!options
|
|
133
|
+
|| typeof options !== 'object'
|
|
134
|
+
|| Array.isArray(options)
|
|
135
|
+
|| ![Object.prototype, null].includes(Object.getPrototypeOf(options))
|
|
136
|
+
) {
|
|
137
|
+
throw new FlexHarnessValidationError('builtInTools must be a plain object.');
|
|
138
|
+
}
|
|
139
|
+
const unsupported = Object.keys(options)
|
|
140
|
+
.find((key) => !['renameSession', 'projectManagement'].includes(key));
|
|
141
|
+
if (unsupported) throw new FlexHarnessValidationError(`builtInTools does not support "${unsupported}".`);
|
|
142
|
+
if (options.renameSession !== undefined && typeof options.renameSession !== 'boolean') {
|
|
143
|
+
throw new FlexHarnessValidationError('builtInTools.renameSession must be a boolean.');
|
|
144
|
+
}
|
|
145
|
+
const projectManagement = options.projectManagement;
|
|
146
|
+
if (projectManagement === undefined) {
|
|
147
|
+
return Object.freeze({ renameSession: options.renameSession ?? false });
|
|
148
|
+
}
|
|
149
|
+
if (
|
|
150
|
+
!projectManagement
|
|
151
|
+
|| typeof projectManagement !== 'object'
|
|
152
|
+
|| Array.isArray(projectManagement)
|
|
153
|
+
|| ![Object.prototype, null].includes(Object.getPrototypeOf(projectManagement))
|
|
154
|
+
) {
|
|
155
|
+
throw new FlexHarnessValidationError('builtInTools.projectManagement must be a plain object.');
|
|
156
|
+
}
|
|
157
|
+
const unsupportedProjectOption = Object.keys(projectManagement)
|
|
158
|
+
.find((key) => !['task', 'goal', 'scratchpad'].includes(key));
|
|
159
|
+
if (unsupportedProjectOption) {
|
|
160
|
+
throw new FlexHarnessValidationError(
|
|
161
|
+
`builtInTools.projectManagement does not support "${unsupportedProjectOption}".`,
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
for (const key of ['task', 'goal', 'scratchpad'] as const) {
|
|
165
|
+
if (projectManagement[key] !== undefined && typeof projectManagement[key] !== 'boolean') {
|
|
166
|
+
throw new FlexHarnessValidationError(`builtInTools.projectManagement.${key} must be a boolean.`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return Object.freeze({
|
|
170
|
+
renameSession: options.renameSession ?? false,
|
|
171
|
+
projectManagement: Object.freeze({
|
|
172
|
+
task: projectManagement.task ?? true,
|
|
173
|
+
goal: projectManagement.goal ?? true,
|
|
174
|
+
scratchpad: projectManagement.scratchpad ?? true,
|
|
175
|
+
}),
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function normalizeSlashCommands<TScope>(
|
|
180
|
+
registrations: readonly TFlexSlashCommandRegistration<TScope>[] | undefined,
|
|
181
|
+
): ReadonlyMap<string, TRegisteredSlashCommand<TScope>> {
|
|
182
|
+
if (registrations === undefined) return new Map();
|
|
183
|
+
if (!Array.isArray(registrations) || registrations.length > maxSlashCommandRegistrations) {
|
|
184
|
+
throw new FlexHarnessValidationError(
|
|
185
|
+
`slashCommands must be an array with at most ${maxSlashCommandRegistrations} registrations.`,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
const normalized = new Map<string, TRegisteredSlashCommand<TScope>>();
|
|
189
|
+
for (let index = 0; index < registrations.length; index++) {
|
|
190
|
+
const registration = registrations[index];
|
|
191
|
+
if (
|
|
192
|
+
!registration
|
|
193
|
+
|| typeof registration !== 'object'
|
|
194
|
+
|| Array.isArray(registration)
|
|
195
|
+
|| ![Object.prototype, null].includes(Object.getPrototypeOf(registration))
|
|
196
|
+
) {
|
|
197
|
+
throw new FlexHarnessValidationError(`slashCommands[${index}] must be a plain object.`);
|
|
198
|
+
}
|
|
199
|
+
const keys = Object.keys(registration);
|
|
200
|
+
const hasTemplate = Object.prototype.hasOwnProperty.call(registration, 'template');
|
|
201
|
+
const hasHandler = Object.prototype.hasOwnProperty.call(registration, 'handler');
|
|
202
|
+
const supported = hasTemplate
|
|
203
|
+
? ['name', 'description', 'template']
|
|
204
|
+
: ['name', 'description', 'handler'];
|
|
205
|
+
const unsupported = keys.find((key) => !supported.includes(key));
|
|
206
|
+
if (unsupported || hasTemplate === hasHandler) {
|
|
207
|
+
throw new FlexHarnessValidationError(
|
|
208
|
+
`slashCommands[${index}] must define exactly one of template or handler.`,
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
if (typeof registration.name !== 'string' || !isValidSlashCommandName(registration.name)) {
|
|
212
|
+
throw new FlexHarnessValidationError(`slashCommands[${index}].name is invalid.`);
|
|
213
|
+
}
|
|
214
|
+
if (isReservedSlashCommandName(registration.name)) {
|
|
215
|
+
throw new FlexHarnessValidationError(
|
|
216
|
+
`Slash command "${registration.name}" is reserved.`,
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
if (normalized.has(registration.name)) {
|
|
220
|
+
throw new FlexHarnessValidationError(`Duplicate slash command "${registration.name}".`);
|
|
221
|
+
}
|
|
222
|
+
if (registration.description !== undefined) {
|
|
223
|
+
validateUtf8String(
|
|
224
|
+
registration.description,
|
|
225
|
+
`slashCommands[${index}].description`,
|
|
226
|
+
maxSlashCommandDescriptionBytes,
|
|
227
|
+
true,
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
if (hasTemplate) {
|
|
231
|
+
const template = registration.template;
|
|
232
|
+
validateUtf8String(
|
|
233
|
+
template,
|
|
234
|
+
`slashCommands[${index}].template`,
|
|
235
|
+
maxSlashCommandTemplateBytes,
|
|
236
|
+
true,
|
|
237
|
+
);
|
|
238
|
+
normalized.set(registration.name, Object.freeze({
|
|
239
|
+
name: registration.name,
|
|
240
|
+
...(registration.description === undefined
|
|
241
|
+
? {}
|
|
242
|
+
: { description: registration.description }),
|
|
243
|
+
template,
|
|
244
|
+
}));
|
|
245
|
+
} else {
|
|
246
|
+
const handler = registration.handler;
|
|
247
|
+
if (typeof handler !== 'function') {
|
|
248
|
+
throw new FlexHarnessValidationError(`slashCommands[${index}].handler must be a function.`);
|
|
249
|
+
}
|
|
250
|
+
normalized.set(registration.name, Object.freeze({
|
|
251
|
+
name: registration.name,
|
|
252
|
+
...(registration.description === undefined
|
|
253
|
+
? {}
|
|
254
|
+
: { description: registration.description }),
|
|
255
|
+
handler,
|
|
256
|
+
}));
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return Object.freeze(normalized);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export function resolveBoundedPositiveInteger(
|
|
263
|
+
value: number | undefined,
|
|
264
|
+
name: string,
|
|
265
|
+
defaultValue: number,
|
|
266
|
+
maximum: number,
|
|
267
|
+
): number {
|
|
268
|
+
const resolved = value ?? defaultValue;
|
|
269
|
+
if (!Number.isSafeInteger(resolved) || resolved < 1 || resolved > maximum) {
|
|
270
|
+
throw new FlexHarnessValidationError(`${name} must be an integer from 1 through ${maximum}.`);
|
|
271
|
+
}
|
|
272
|
+
return resolved;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export function resolveCallbackLimits(limits: IFlexCallbackLimits = {}): Required<IFlexCallbackLimits> {
|
|
276
|
+
const resolved = {
|
|
277
|
+
maxEvents: limits.maxEvents ?? DEFAULT_CALLBACK_LIMITS.maxEvents,
|
|
278
|
+
maxOutputBytes: limits.maxOutputBytes ?? DEFAULT_CALLBACK_LIMITS.maxOutputBytes,
|
|
279
|
+
maxParts: limits.maxParts ?? DEFAULT_CALLBACK_LIMITS.maxParts,
|
|
280
|
+
};
|
|
281
|
+
for (const [name, value] of Object.entries(resolved)) {
|
|
282
|
+
if (!Number.isSafeInteger(value) || value < 1) {
|
|
283
|
+
throw new FlexHarnessValidationError(`callbackLimits.${name} must be a positive integer.`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return resolved;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function resolvePromptQueueLimits(
|
|
290
|
+
limits: IFlexPromptQueueLimits = {},
|
|
291
|
+
): Required<IFlexPromptQueueLimits> {
|
|
292
|
+
const resolved = {
|
|
293
|
+
maxOutstandingPromptsPerSession: limits.maxOutstandingPromptsPerSession
|
|
294
|
+
?? DEFAULT_PROMPT_QUEUE_LIMITS.maxOutstandingPromptsPerSession,
|
|
295
|
+
maxOutstandingBytesPerSession: limits.maxOutstandingBytesPerSession
|
|
296
|
+
?? DEFAULT_PROMPT_QUEUE_LIMITS.maxOutstandingBytesPerSession,
|
|
297
|
+
maxPendingAdmissions: limits.maxPendingAdmissions
|
|
298
|
+
?? DEFAULT_PROMPT_QUEUE_LIMITS.maxPendingAdmissions,
|
|
299
|
+
maxPendingAdmissionBytes: limits.maxPendingAdmissionBytes
|
|
300
|
+
?? DEFAULT_PROMPT_QUEUE_LIMITS.maxPendingAdmissionBytes,
|
|
301
|
+
maxTerminalEntriesPerSession: limits.maxTerminalEntriesPerSession
|
|
302
|
+
?? DEFAULT_PROMPT_QUEUE_LIMITS.maxTerminalEntriesPerSession,
|
|
303
|
+
};
|
|
304
|
+
for (const [name, value] of Object.entries(resolved)) {
|
|
305
|
+
if (!Number.isSafeInteger(value) || value < 1) {
|
|
306
|
+
throw new FlexHarnessValidationError(`promptQueueLimits.${name} must be a positive integer.`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return resolved;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export function resolveReversionLimits(
|
|
313
|
+
limits: IFlexReversionLimits = {},
|
|
314
|
+
): Required<IFlexReversionLimits> {
|
|
315
|
+
const resolved = {
|
|
316
|
+
maxCompletedTurns: limits.maxCompletedTurns ?? FLEX_REVERSION_DEFAULT_LIMITS.maxCompletedTurns,
|
|
317
|
+
maxSegments: limits.maxSegments ?? FLEX_REVERSION_DEFAULT_LIMITS.maxSegments,
|
|
318
|
+
maxExcludedRunIds: limits.maxExcludedRunIds ?? FLEX_REVERSION_DEFAULT_LIMITS.maxExcludedRunIds,
|
|
319
|
+
maxPendingReversionReleases: limits.maxPendingReversionReleases
|
|
320
|
+
?? FLEX_REVERSION_DEFAULT_LIMITS.maxPendingReversionReleases,
|
|
321
|
+
};
|
|
322
|
+
for (const [name, value] of Object.entries(resolved)) {
|
|
323
|
+
const maximum = FLEX_REVERSION_MAXIMUM_LIMITS[name as keyof IFlexReversionLimits];
|
|
324
|
+
if (!Number.isSafeInteger(value) || value < 1 || value > maximum) {
|
|
325
|
+
throw new FlexHarnessValidationError(
|
|
326
|
+
`reversionLimits.${name} must be an integer from 1 through ${maximum}.`,
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return resolved;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export function normalizeAgentSessionPolicy<TScope>(
|
|
334
|
+
policy: IFlexAgentSessionPolicy<TScope> = {},
|
|
335
|
+
): IFlexAgentSessionPolicy<TScope> {
|
|
336
|
+
return {
|
|
337
|
+
...(policy.contextBuilder === undefined ? {} : { contextBuilder: policy.contextBuilder }),
|
|
338
|
+
...(policy.contextCompactor === undefined ? {} : { contextCompactor: policy.contextCompactor }),
|
|
339
|
+
...(policy.eventRetention === undefined ? {} : { eventRetention: policy.eventRetention }),
|
|
340
|
+
...(policy.changeListenerTimeoutMs === undefined
|
|
341
|
+
? {}
|
|
342
|
+
: { changeListenerTimeoutMs: policy.changeListenerTimeoutMs }),
|
|
343
|
+
...(policy.maxPendingSessionChanges === undefined
|
|
344
|
+
? {}
|
|
345
|
+
: { maxPendingSessionChanges: policy.maxPendingSessionChanges }),
|
|
346
|
+
...(policy.generationLeaseCleanupTimeoutMs === undefined
|
|
347
|
+
? {}
|
|
348
|
+
: { generationLeaseCleanupTimeoutMs: policy.generationLeaseCleanupTimeoutMs }),
|
|
349
|
+
...(policy.maxArchivedTransactionTombstones === undefined
|
|
350
|
+
? {}
|
|
351
|
+
: { maxArchivedTransactionTombstones: policy.maxArchivedTransactionTombstones }),
|
|
352
|
+
...(policy.maxContextOverflowRetries === undefined
|
|
353
|
+
? {}
|
|
354
|
+
: { maxContextOverflowRetries: policy.maxContextOverflowRetries }),
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
export function requireHarnessStores(stores: IFlexHarnessStores): IFlexHarnessStores {
|
|
359
|
+
const projectManagement = stores?.projectManagement;
|
|
360
|
+
if (
|
|
361
|
+
!projectManagement
|
|
362
|
+
|| typeof projectManagement.load !== 'function'
|
|
363
|
+
|| typeof projectManagement.save !== 'function'
|
|
364
|
+
|| typeof projectManagement.tombstoneSession !== 'function'
|
|
365
|
+
|| typeof projectManagement.purgeNamespace !== 'function'
|
|
366
|
+
) {
|
|
367
|
+
throw new FlexHarnessValidationError(
|
|
368
|
+
'stores.projectManagement requires load/save/tombstoneSession/purgeNamespace methods.',
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
return stores;
|
|
372
|
+
}
|
|
@@ -12,6 +12,7 @@ const slashCommandArgumentsPattern = /(?:\[Image\s+\d+\]|"[^"]*"|'[^']*'|[^\s"']
|
|
|
12
12
|
const slashCommandQuoteTrimPattern = /^["']|["']$/g;
|
|
13
13
|
const slashCommandPlaceholderPattern = /\$(\d+)/g;
|
|
14
14
|
const slashCommandAllPlaceholderPattern = /\$ARGUMENTS|\$(\d+)/g;
|
|
15
|
+
const reservedSlashCommandNames = new Set(['compact', 'undo', 'redo', 'init']);
|
|
15
16
|
|
|
16
17
|
export const FLEX_SLASH_COMMAND_INITIALIZE_TEMPLATE = `Create or update \`AGENTS.md\` for this repository.
|
|
17
18
|
|
|
@@ -85,6 +86,10 @@ export function isValidSlashCommandName(name: string): boolean {
|
|
|
85
86
|
return slashCommandNamePattern.test(name);
|
|
86
87
|
}
|
|
87
88
|
|
|
89
|
+
export function isReservedSlashCommandName(name: string): boolean {
|
|
90
|
+
return reservedSlashCommandNames.has(name);
|
|
91
|
+
}
|
|
92
|
+
|
|
88
93
|
function tokenizeSlashCommandArguments(rawArguments: string): string[] {
|
|
89
94
|
const tokens = rawArguments.match(slashCommandArgumentsPattern) ?? [];
|
|
90
95
|
return tokens.map((token) => token.replace(slashCommandQuoteTrimPattern, ''));
|