@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.
@@ -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
@@ -47,6 +47,7 @@ export type {
47
47
  IAgentRunResult,
48
48
  IAgentSession,
49
49
  IAgentSessionOptions,
50
+ IAgentToolCallUpdateEvent,
50
51
  IAgentRuntimeEventPayload,
51
52
  IGenerationBegunEvent,
52
53
  IGenerationExecutionCompletedEvent,
@@ -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, ''));