@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
|
@@ -20,8 +20,6 @@ import {
|
|
|
20
20
|
import {
|
|
21
21
|
FLEX_PROJECT_MANAGEMENT_LIMITS,
|
|
22
22
|
FLEX_REVERSION_MAX_AFFECTED_WORKSPACES,
|
|
23
|
-
FLEX_REVERSION_DEFAULT_LIMITS,
|
|
24
|
-
FLEX_REVERSION_MAXIMUM_LIMITS,
|
|
25
23
|
FLEX_REVERSION_REASON_CODE_MAX_BYTES,
|
|
26
24
|
FLEX_REVERSION_REFERENCE_MAX_BYTES,
|
|
27
25
|
FLEX_REVERSION_WORKSPACE_ID_MAX_BYTES,
|
|
@@ -31,7 +29,6 @@ import type {
|
|
|
31
29
|
IFlexAgentContextInvocation,
|
|
32
30
|
IFlexAgentSessionPolicy,
|
|
33
31
|
IFlexAffectedWorkspace,
|
|
34
|
-
IFlexAttachmentMessagePart,
|
|
35
32
|
IFlexBackgroundExecution,
|
|
36
33
|
IFlexCallbackLimits,
|
|
37
34
|
IFlexCreateProjectTaskInput,
|
|
@@ -83,7 +80,6 @@ import type {
|
|
|
83
80
|
IFlexSlashCommandDescriptor,
|
|
84
81
|
IFlexSlashCommandExecutionOptions,
|
|
85
82
|
IFlexSlashCommandHandlerRegistration,
|
|
86
|
-
IFlexSlashCommandTemplateRegistration,
|
|
87
83
|
IFlexSubagentDefinition,
|
|
88
84
|
IFlexTerminalProjection,
|
|
89
85
|
IFlexToolHandle,
|
|
@@ -93,12 +89,10 @@ import type {
|
|
|
93
89
|
IFlexUndoSessionResult,
|
|
94
90
|
IFlexUpdateSessionOptions,
|
|
95
91
|
IFlexUpdateProjectTaskInput,
|
|
96
|
-
IFlexUsage,
|
|
97
92
|
IJsonObject,
|
|
98
|
-
|
|
93
|
+
TFlexAgentToolCallUpdateEvent,
|
|
99
94
|
TFlexAgentToolCallFinishEvent,
|
|
100
95
|
TFlexAgentToolSet,
|
|
101
|
-
TFlexAttachmentSource,
|
|
102
96
|
TFlexExternalErrorProjector,
|
|
103
97
|
TFlexExternalErrorSource,
|
|
104
98
|
TFlexHarnessEvent,
|
|
@@ -108,10 +102,8 @@ import type {
|
|
|
108
102
|
TFlexProjectTaskPriority,
|
|
109
103
|
TFlexProjectTaskStatus,
|
|
110
104
|
TFlexPrompt,
|
|
111
|
-
TFlexPromptPart,
|
|
112
105
|
TFlexPromptQueueStatus,
|
|
113
106
|
TFlexSlashCommandExecutionResult,
|
|
114
|
-
TFlexSlashCommandRegistration,
|
|
115
107
|
TFlexPendingReversion,
|
|
116
108
|
TFlexProjectionSnapshot,
|
|
117
109
|
TFlexReversionPolicy,
|
|
@@ -138,14 +130,58 @@ import {
|
|
|
138
130
|
normalizeFlexPrompt,
|
|
139
131
|
type INormalizedFlexPrompt,
|
|
140
132
|
} from './utils.prompt.js';
|
|
133
|
+
import {
|
|
134
|
+
maxSubagentNameBytes,
|
|
135
|
+
normalizeAgentSessionPolicy,
|
|
136
|
+
normalizeBuiltInTools,
|
|
137
|
+
normalizeSlashCommands,
|
|
138
|
+
normalizeSubagents,
|
|
139
|
+
requireHarnessStores,
|
|
140
|
+
resolveBoundedPositiveInteger,
|
|
141
|
+
resolveCallbackLimits,
|
|
142
|
+
resolvePromptQueueLimits,
|
|
143
|
+
resolveReversionLimits,
|
|
144
|
+
type INormalizedBuiltInTools,
|
|
145
|
+
type INormalizedProjectManagementTools,
|
|
146
|
+
type TRegisteredSlashCommand,
|
|
147
|
+
} from './utils.harnessoptions.js';
|
|
141
148
|
import {
|
|
142
149
|
FLEX_SLASH_COMMAND_INITIALIZE_TEMPLATE,
|
|
143
|
-
FLEX_SLASH_COMMAND_MAX_INPUT_BYTES,
|
|
144
150
|
expandSlashCommandTemplate,
|
|
145
|
-
|
|
151
|
+
isReservedSlashCommandName,
|
|
146
152
|
parseSlashCommand,
|
|
147
153
|
slashCommandTemplateHints,
|
|
148
154
|
} from './utils.slashcommands.js';
|
|
155
|
+
import {
|
|
156
|
+
createBoundedTransferMessage,
|
|
157
|
+
describeAttachment,
|
|
158
|
+
jsonBytes,
|
|
159
|
+
maxMessagePageCursorBytes,
|
|
160
|
+
maxMessagePageSize,
|
|
161
|
+
maxTransferIdentifierBytes,
|
|
162
|
+
maxTransferMetadataBytes,
|
|
163
|
+
maxTransferPageBytes,
|
|
164
|
+
maxTransferTextBytes,
|
|
165
|
+
normalizeModelIdentity,
|
|
166
|
+
normalizeResolvedModel,
|
|
167
|
+
normalizeRunResult,
|
|
168
|
+
publicSnapshot,
|
|
169
|
+
requireTransferIdentifier,
|
|
170
|
+
truncateUtf8,
|
|
171
|
+
type IRunResultProjection,
|
|
172
|
+
} from './utils.transfer.js';
|
|
173
|
+
import { wrapToolSet } from './utils.tooloutput.js';
|
|
174
|
+
import {
|
|
175
|
+
validateCreateProjectTaskInput,
|
|
176
|
+
validateIdentifier,
|
|
177
|
+
validateProjectTaskId,
|
|
178
|
+
validateProjectedErrorInfo,
|
|
179
|
+
validatePromptOptions,
|
|
180
|
+
validateSlashCommandExecutionOptions,
|
|
181
|
+
validateUpdateProjectTaskInput,
|
|
182
|
+
validateUpdateSessionOptions,
|
|
183
|
+
validateUtf8String,
|
|
184
|
+
} from './utils.validation.js';
|
|
149
185
|
|
|
150
186
|
type TCanonicalOutcome = 'accepted' | 'rejected' | 'interrupted';
|
|
151
187
|
type TRunPhase =
|
|
@@ -347,10 +383,6 @@ interface IScopeAdmissionState {
|
|
|
347
383
|
retiring: boolean;
|
|
348
384
|
}
|
|
349
385
|
|
|
350
|
-
type TRegisteredSlashCommand<TScope> =
|
|
351
|
-
| Readonly<IFlexSlashCommandTemplateRegistration>
|
|
352
|
-
| Readonly<IFlexSlashCommandHandlerRegistration<TScope>>;
|
|
353
|
-
|
|
354
386
|
interface IActiveSlashCommandExecution {
|
|
355
387
|
kind: 'handler' | 'operation' | 'prompt-admission';
|
|
356
388
|
storageKey: string;
|
|
@@ -379,13 +411,6 @@ interface ISlashCommandActivityOwner {
|
|
|
379
411
|
sessionId: string;
|
|
380
412
|
}
|
|
381
413
|
|
|
382
|
-
interface IRunResultProjection {
|
|
383
|
-
text: string;
|
|
384
|
-
steps: number;
|
|
385
|
-
finishReason: string;
|
|
386
|
-
usage: IFlexUsage;
|
|
387
|
-
}
|
|
388
|
-
|
|
389
414
|
interface IResolverOutcome<TValue> {
|
|
390
415
|
source: 'model' | 'tools' | 'abort';
|
|
391
416
|
success: boolean;
|
|
@@ -400,10 +425,6 @@ interface IFlexMessageCursor {
|
|
|
400
425
|
anchorMessageId: string;
|
|
401
426
|
}
|
|
402
427
|
|
|
403
|
-
interface IExecutableToolRecord extends Record<string, unknown> {
|
|
404
|
-
execute?: (input: unknown, options: unknown) => unknown;
|
|
405
|
-
}
|
|
406
|
-
|
|
407
428
|
interface IFlexSubagentTaskInput {
|
|
408
429
|
description: string;
|
|
409
430
|
prompt: string;
|
|
@@ -416,17 +437,6 @@ interface IFlexSubagentAcquisition {
|
|
|
416
437
|
created: boolean;
|
|
417
438
|
}
|
|
418
439
|
|
|
419
|
-
interface INormalizedProjectManagementTools {
|
|
420
|
-
readonly task: boolean;
|
|
421
|
-
readonly goal: boolean;
|
|
422
|
-
readonly scratchpad: boolean;
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
interface INormalizedBuiltInTools {
|
|
426
|
-
readonly renameSession: boolean;
|
|
427
|
-
readonly projectManagement?: INormalizedProjectManagementTools;
|
|
428
|
-
}
|
|
429
|
-
|
|
430
440
|
type TProjectTaskToolInput =
|
|
431
441
|
| { action: 'list' }
|
|
432
442
|
| {
|
|
@@ -461,35 +471,8 @@ type TEventDetails = Record<string, unknown> & {
|
|
|
461
471
|
type: TFlexHarnessEvent['type'];
|
|
462
472
|
};
|
|
463
473
|
|
|
464
|
-
const DEFAULT_CALLBACK_LIMITS: Required<IFlexCallbackLimits> = {
|
|
465
|
-
maxEvents: 10_000,
|
|
466
|
-
maxOutputBytes: 1024 * 1024,
|
|
467
|
-
maxParts: 2_000,
|
|
468
|
-
};
|
|
469
|
-
const DEFAULT_PROMPT_QUEUE_LIMITS: Required<IFlexPromptQueueLimits> = {
|
|
470
|
-
maxOutstandingPromptsPerSession: 16,
|
|
471
|
-
maxOutstandingBytesPerSession: 64 * 1024 * 1024,
|
|
472
|
-
maxPendingAdmissions: 64,
|
|
473
|
-
maxPendingAdmissionBytes: 128 * 1024 * 1024,
|
|
474
|
-
maxTerminalEntriesPerSession: 64,
|
|
475
|
-
};
|
|
476
|
-
const maxMessagePageSize = 50;
|
|
477
|
-
const maxMessagePageCursorBytes = 4096;
|
|
478
|
-
const maxTransferIdentifierBytes = 512;
|
|
479
|
-
const maxTransferMetadataBytes = 2048;
|
|
480
|
-
const maxTransferTextBytes = 96 * 1024;
|
|
481
|
-
const maxTransferMessageBytes = 480 * 1024;
|
|
482
|
-
const maxTransferPageBytes = 512 * 1024;
|
|
483
|
-
const maxProjectedErrorNameBytes = 128;
|
|
484
|
-
const maxProjectedErrorMessageBytes = 2048;
|
|
485
|
-
const maxProjectedErrorCodeBytes = 128;
|
|
486
474
|
const maxScheduleDebounceMs = 24 * 60 * 60 * 1000;
|
|
487
475
|
const maxBackgroundExecutions = 100;
|
|
488
|
-
const maxSubagentDefinitions = 32;
|
|
489
|
-
const maxSubagentNameBytes = 128;
|
|
490
|
-
const maxSubagentDescriptionBytes = 2048;
|
|
491
|
-
const maxSubagentModelHintBytes = 512;
|
|
492
|
-
const maxSubagentSystemBytes = 64 * 1024;
|
|
493
476
|
const maxSubagentTaskDescriptionBytes = 256;
|
|
494
477
|
const maxSubagentPromptBytes = 64 * 1024;
|
|
495
478
|
const maxSubagentTaskIdBytes = 512;
|
|
@@ -501,15 +484,11 @@ const maximumMaxSubagentCallsPerRun = 128;
|
|
|
501
484
|
const maxResourceToolProviders = 128;
|
|
502
485
|
const maxResourceIdBytes = 512;
|
|
503
486
|
const maxResourceToolNameBytes = 512;
|
|
504
|
-
const maxSlashCommandRegistrations = 128;
|
|
505
|
-
const maxSlashCommandDescriptionBytes = 2048;
|
|
506
|
-
const maxSlashCommandTemplateBytes = FLEX_SLASH_COMMAND_MAX_INPUT_BYTES;
|
|
507
487
|
const maxProjectManagementTombstoneConflicts = 8;
|
|
508
488
|
const resourceToolStemLength = 16;
|
|
509
489
|
const repairCancellationMessage = 'The process stopped before this run completed.';
|
|
510
490
|
const scopeRetirementMessage = 'The scope is being retired.';
|
|
511
491
|
const slashCommandSessionUnavailableReason = 'Session must be idle with no queued prompts.';
|
|
512
|
-
const reservedSlashCommandNames = new Set(['compact', 'undo', 'redo', 'init']);
|
|
513
492
|
const externalErrorFallback: IFlexErrorInfo = Object.freeze({
|
|
514
493
|
name: 'FlexHarnessExternalError',
|
|
515
494
|
message: 'The model operation failed.',
|
|
@@ -553,97 +532,6 @@ function requireSessionGeneration(
|
|
|
553
532
|
};
|
|
554
533
|
}
|
|
555
534
|
|
|
556
|
-
function isAsyncIterable(value: unknown): value is AsyncIterable<unknown> {
|
|
557
|
-
return Boolean(
|
|
558
|
-
value
|
|
559
|
-
&& typeof value === 'object'
|
|
560
|
-
&& typeof (value as { [Symbol.asyncIterator]?: unknown })[Symbol.asyncIterator] === 'function',
|
|
561
|
-
);
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
async function* normalizeAsyncIterable(
|
|
565
|
-
iterable: AsyncIterable<unknown>,
|
|
566
|
-
limits: Required<NonNullable<IFlexHarnessOptions<unknown>['toolOutputLimits']>>,
|
|
567
|
-
projectError: (error: unknown) => Error,
|
|
568
|
-
): AsyncGenerator<unknown> {
|
|
569
|
-
try {
|
|
570
|
-
for await (const value of iterable) yield normalizeJsonValue(value, limits);
|
|
571
|
-
} catch (error) {
|
|
572
|
-
throw projectError(error);
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
function wrapToolSet(
|
|
577
|
-
tools: TFlexAgentToolSet,
|
|
578
|
-
limits: Required<NonNullable<IFlexHarnessOptions<unknown>['toolOutputLimits']>>,
|
|
579
|
-
projectError: (error: unknown) => Error,
|
|
580
|
-
): TFlexAgentToolSet {
|
|
581
|
-
const wrapped: Record<string, unknown> = {};
|
|
582
|
-
for (const [name, tool] of Object.entries(tools)) {
|
|
583
|
-
const toolRecord = tool as unknown as IExecutableToolRecord;
|
|
584
|
-
if (typeof toolRecord.execute !== 'function') {
|
|
585
|
-
wrapped[name] = tool;
|
|
586
|
-
continue;
|
|
587
|
-
}
|
|
588
|
-
const execute = toolRecord.execute;
|
|
589
|
-
wrapped[name] = {
|
|
590
|
-
...toolRecord,
|
|
591
|
-
execute(input: unknown, options: unknown): unknown {
|
|
592
|
-
let output: unknown;
|
|
593
|
-
try {
|
|
594
|
-
output = execute.call(tool, input, options);
|
|
595
|
-
if (isAsyncIterable(output)) {
|
|
596
|
-
return normalizeAsyncIterable(output, limits, projectError);
|
|
597
|
-
}
|
|
598
|
-
} catch (error) {
|
|
599
|
-
throw projectError(error);
|
|
600
|
-
}
|
|
601
|
-
return Promise.resolve(output).then(
|
|
602
|
-
(value) => {
|
|
603
|
-
try {
|
|
604
|
-
return normalizeJsonValue(value, limits);
|
|
605
|
-
} catch (error) {
|
|
606
|
-
throw projectError(error);
|
|
607
|
-
}
|
|
608
|
-
},
|
|
609
|
-
(error: unknown) => {
|
|
610
|
-
throw projectError(error);
|
|
611
|
-
},
|
|
612
|
-
);
|
|
613
|
-
},
|
|
614
|
-
};
|
|
615
|
-
}
|
|
616
|
-
return wrapped as TFlexAgentToolSet;
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
function publicSnapshot<TValue>(value: TValue): TValue {
|
|
620
|
-
return deepFreeze(cloneSerializable(value));
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
function jsonBytes(value: unknown): number {
|
|
624
|
-
return Buffer.byteLength(JSON.stringify(value), 'utf8');
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
function truncateUtf8(value: string, maxBytes: number): string {
|
|
628
|
-
if (Buffer.byteLength(value, 'utf8') <= maxBytes) return value;
|
|
629
|
-
const suffix = ' [truncated]';
|
|
630
|
-
const suffixBytes = Buffer.byteLength(suffix, 'utf8');
|
|
631
|
-
let truncated = Buffer.from(value, 'utf8')
|
|
632
|
-
.subarray(0, Math.max(0, maxBytes - suffixBytes))
|
|
633
|
-
.toString('utf8')
|
|
634
|
-
.replace(/\uFFFD$/u, '');
|
|
635
|
-
while (Buffer.byteLength(`${truncated}${suffix}`, 'utf8') > maxBytes) {
|
|
636
|
-
truncated = truncated.slice(0, -1);
|
|
637
|
-
}
|
|
638
|
-
return `${truncated}${suffix}`;
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
function validateIdentifier(value: string, name: string): void {
|
|
642
|
-
if (typeof value !== 'string' || value.length === 0) {
|
|
643
|
-
throw new FlexHarnessValidationError(`${name} must be a non-empty string.`);
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
|
|
647
535
|
function sha256Hex(value: string): string {
|
|
648
536
|
return plugins.crypto.createHash('sha256').update(value, 'utf8').digest('hex');
|
|
649
537
|
}
|
|
@@ -678,346 +566,6 @@ export function createFlexResourceToolName(namespace: string, toolName: string):
|
|
|
678
566
|
return `${namespace}__${stem}__${sha256Hex(toolName).slice(0, 12)}`;
|
|
679
567
|
}
|
|
680
568
|
|
|
681
|
-
function validateUtf8String(
|
|
682
|
-
value: unknown,
|
|
683
|
-
name: string,
|
|
684
|
-
maxBytes: number,
|
|
685
|
-
nonEmpty = false,
|
|
686
|
-
): asserts value is string {
|
|
687
|
-
if (
|
|
688
|
-
typeof value !== 'string'
|
|
689
|
-
|| (nonEmpty && !value.trim())
|
|
690
|
-
|| Buffer.byteLength(value, 'utf8') > maxBytes
|
|
691
|
-
) {
|
|
692
|
-
throw new FlexHarnessValidationError(
|
|
693
|
-
`${name} must be ${nonEmpty ? 'a non-empty ' : 'a '}string of at most ${maxBytes} UTF-8 bytes.`,
|
|
694
|
-
);
|
|
695
|
-
}
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
function normalizeSubagents(
|
|
699
|
-
definitions: IFlexSubagentDefinition[] | undefined,
|
|
700
|
-
): ReadonlyMap<string, Readonly<IFlexSubagentDefinition>> {
|
|
701
|
-
if (definitions === undefined) return new Map();
|
|
702
|
-
if (!Array.isArray(definitions) || definitions.length > maxSubagentDefinitions) {
|
|
703
|
-
throw new FlexHarnessValidationError(
|
|
704
|
-
`subagents must be an array with at most ${maxSubagentDefinitions} definitions.`,
|
|
705
|
-
);
|
|
706
|
-
}
|
|
707
|
-
const normalized = new Map<string, Readonly<IFlexSubagentDefinition>>();
|
|
708
|
-
for (let index = 0; index < definitions.length; index++) {
|
|
709
|
-
const definition = definitions[index];
|
|
710
|
-
if (
|
|
711
|
-
!definition
|
|
712
|
-
|| typeof definition !== 'object'
|
|
713
|
-
|| Array.isArray(definition)
|
|
714
|
-
|| ![Object.prototype, null].includes(Object.getPrototypeOf(definition))
|
|
715
|
-
) {
|
|
716
|
-
throw new FlexHarnessValidationError(`subagents[${index}] must be a plain object.`);
|
|
717
|
-
}
|
|
718
|
-
const unsupported = Object.keys(definition).find(
|
|
719
|
-
(key) => !['name', 'description', 'modelHint', 'system', 'maxSteps'].includes(key),
|
|
720
|
-
);
|
|
721
|
-
if (unsupported) {
|
|
722
|
-
throw new FlexHarnessValidationError(`subagents[${index}] does not support "${unsupported}".`);
|
|
723
|
-
}
|
|
724
|
-
validateUtf8String(definition.name, `subagents[${index}].name`, maxSubagentNameBytes, true);
|
|
725
|
-
validateUtf8String(
|
|
726
|
-
definition.description,
|
|
727
|
-
`subagents[${index}].description`,
|
|
728
|
-
maxSubagentDescriptionBytes,
|
|
729
|
-
);
|
|
730
|
-
if (definition.modelHint !== undefined) {
|
|
731
|
-
validateUtf8String(
|
|
732
|
-
definition.modelHint,
|
|
733
|
-
`subagents[${index}].modelHint`,
|
|
734
|
-
maxSubagentModelHintBytes,
|
|
735
|
-
);
|
|
736
|
-
}
|
|
737
|
-
if (definition.system !== undefined) {
|
|
738
|
-
validateUtf8String(
|
|
739
|
-
definition.system,
|
|
740
|
-
`subagents[${index}].system`,
|
|
741
|
-
maxSubagentSystemBytes,
|
|
742
|
-
);
|
|
743
|
-
}
|
|
744
|
-
if (
|
|
745
|
-
definition.maxSteps !== undefined
|
|
746
|
-
&& (!Number.isSafeInteger(definition.maxSteps) || definition.maxSteps < 1)
|
|
747
|
-
) {
|
|
748
|
-
throw new FlexHarnessValidationError(`subagents[${index}].maxSteps must be a positive integer.`);
|
|
749
|
-
}
|
|
750
|
-
if (normalized.has(definition.name)) {
|
|
751
|
-
throw new FlexHarnessValidationError(`Duplicate subagent definition "${definition.name}".`);
|
|
752
|
-
}
|
|
753
|
-
normalized.set(definition.name, Object.freeze({
|
|
754
|
-
name: definition.name,
|
|
755
|
-
description: definition.description,
|
|
756
|
-
...(definition.modelHint === undefined ? {} : { modelHint: definition.modelHint }),
|
|
757
|
-
...(definition.system === undefined ? {} : { system: definition.system }),
|
|
758
|
-
...(definition.maxSteps === undefined ? {} : { maxSteps: definition.maxSteps }),
|
|
759
|
-
}));
|
|
760
|
-
}
|
|
761
|
-
return Object.freeze(normalized);
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
function normalizeBuiltInTools(
|
|
765
|
-
options: IFlexHarnessOptions<unknown>['builtInTools'],
|
|
766
|
-
): INormalizedBuiltInTools {
|
|
767
|
-
if (options === undefined) return Object.freeze({ renameSession: false });
|
|
768
|
-
if (
|
|
769
|
-
!options
|
|
770
|
-
|| typeof options !== 'object'
|
|
771
|
-
|| Array.isArray(options)
|
|
772
|
-
|| ![Object.prototype, null].includes(Object.getPrototypeOf(options))
|
|
773
|
-
) {
|
|
774
|
-
throw new FlexHarnessValidationError('builtInTools must be a plain object.');
|
|
775
|
-
}
|
|
776
|
-
const unsupported = Object.keys(options)
|
|
777
|
-
.find((key) => !['renameSession', 'projectManagement'].includes(key));
|
|
778
|
-
if (unsupported) throw new FlexHarnessValidationError(`builtInTools does not support "${unsupported}".`);
|
|
779
|
-
if (options.renameSession !== undefined && typeof options.renameSession !== 'boolean') {
|
|
780
|
-
throw new FlexHarnessValidationError('builtInTools.renameSession must be a boolean.');
|
|
781
|
-
}
|
|
782
|
-
const projectManagement = options.projectManagement;
|
|
783
|
-
if (projectManagement === undefined) {
|
|
784
|
-
return Object.freeze({ renameSession: options.renameSession ?? false });
|
|
785
|
-
}
|
|
786
|
-
if (
|
|
787
|
-
!projectManagement
|
|
788
|
-
|| typeof projectManagement !== 'object'
|
|
789
|
-
|| Array.isArray(projectManagement)
|
|
790
|
-
|| ![Object.prototype, null].includes(Object.getPrototypeOf(projectManagement))
|
|
791
|
-
) {
|
|
792
|
-
throw new FlexHarnessValidationError('builtInTools.projectManagement must be a plain object.');
|
|
793
|
-
}
|
|
794
|
-
const unsupportedProjectOption = Object.keys(projectManagement)
|
|
795
|
-
.find((key) => !['task', 'goal', 'scratchpad'].includes(key));
|
|
796
|
-
if (unsupportedProjectOption) {
|
|
797
|
-
throw new FlexHarnessValidationError(
|
|
798
|
-
`builtInTools.projectManagement does not support "${unsupportedProjectOption}".`,
|
|
799
|
-
);
|
|
800
|
-
}
|
|
801
|
-
for (const key of ['task', 'goal', 'scratchpad'] as const) {
|
|
802
|
-
if (projectManagement[key] !== undefined && typeof projectManagement[key] !== 'boolean') {
|
|
803
|
-
throw new FlexHarnessValidationError(`builtInTools.projectManagement.${key} must be a boolean.`);
|
|
804
|
-
}
|
|
805
|
-
}
|
|
806
|
-
return Object.freeze({
|
|
807
|
-
renameSession: options.renameSession ?? false,
|
|
808
|
-
projectManagement: Object.freeze({
|
|
809
|
-
task: projectManagement.task ?? true,
|
|
810
|
-
goal: projectManagement.goal ?? true,
|
|
811
|
-
scratchpad: projectManagement.scratchpad ?? true,
|
|
812
|
-
}),
|
|
813
|
-
});
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
function normalizeSlashCommands<TScope>(
|
|
817
|
-
registrations: readonly TFlexSlashCommandRegistration<TScope>[] | undefined,
|
|
818
|
-
): ReadonlyMap<string, TRegisteredSlashCommand<TScope>> {
|
|
819
|
-
if (registrations === undefined) return new Map();
|
|
820
|
-
if (!Array.isArray(registrations) || registrations.length > maxSlashCommandRegistrations) {
|
|
821
|
-
throw new FlexHarnessValidationError(
|
|
822
|
-
`slashCommands must be an array with at most ${maxSlashCommandRegistrations} registrations.`,
|
|
823
|
-
);
|
|
824
|
-
}
|
|
825
|
-
const normalized = new Map<string, TRegisteredSlashCommand<TScope>>();
|
|
826
|
-
for (let index = 0; index < registrations.length; index++) {
|
|
827
|
-
const registration = registrations[index];
|
|
828
|
-
if (
|
|
829
|
-
!registration
|
|
830
|
-
|| typeof registration !== 'object'
|
|
831
|
-
|| Array.isArray(registration)
|
|
832
|
-
|| ![Object.prototype, null].includes(Object.getPrototypeOf(registration))
|
|
833
|
-
) {
|
|
834
|
-
throw new FlexHarnessValidationError(`slashCommands[${index}] must be a plain object.`);
|
|
835
|
-
}
|
|
836
|
-
const keys = Object.keys(registration);
|
|
837
|
-
const hasTemplate = Object.prototype.hasOwnProperty.call(registration, 'template');
|
|
838
|
-
const hasHandler = Object.prototype.hasOwnProperty.call(registration, 'handler');
|
|
839
|
-
const supported = hasTemplate
|
|
840
|
-
? ['name', 'description', 'template']
|
|
841
|
-
: ['name', 'description', 'handler'];
|
|
842
|
-
const unsupported = keys.find((key) => !supported.includes(key));
|
|
843
|
-
if (unsupported || hasTemplate === hasHandler) {
|
|
844
|
-
throw new FlexHarnessValidationError(
|
|
845
|
-
`slashCommands[${index}] must define exactly one of template or handler.`,
|
|
846
|
-
);
|
|
847
|
-
}
|
|
848
|
-
if (typeof registration.name !== 'string' || !isValidSlashCommandName(registration.name)) {
|
|
849
|
-
throw new FlexHarnessValidationError(`slashCommands[${index}].name is invalid.`);
|
|
850
|
-
}
|
|
851
|
-
if (reservedSlashCommandNames.has(registration.name)) {
|
|
852
|
-
throw new FlexHarnessValidationError(
|
|
853
|
-
`Slash command "${registration.name}" is reserved.`,
|
|
854
|
-
);
|
|
855
|
-
}
|
|
856
|
-
if (normalized.has(registration.name)) {
|
|
857
|
-
throw new FlexHarnessValidationError(`Duplicate slash command "${registration.name}".`);
|
|
858
|
-
}
|
|
859
|
-
if (registration.description !== undefined) {
|
|
860
|
-
validateUtf8String(
|
|
861
|
-
registration.description,
|
|
862
|
-
`slashCommands[${index}].description`,
|
|
863
|
-
maxSlashCommandDescriptionBytes,
|
|
864
|
-
true,
|
|
865
|
-
);
|
|
866
|
-
}
|
|
867
|
-
if (hasTemplate) {
|
|
868
|
-
const template = registration.template;
|
|
869
|
-
validateUtf8String(
|
|
870
|
-
template,
|
|
871
|
-
`slashCommands[${index}].template`,
|
|
872
|
-
maxSlashCommandTemplateBytes,
|
|
873
|
-
true,
|
|
874
|
-
);
|
|
875
|
-
normalized.set(registration.name, Object.freeze({
|
|
876
|
-
name: registration.name,
|
|
877
|
-
...(registration.description === undefined
|
|
878
|
-
? {}
|
|
879
|
-
: { description: registration.description }),
|
|
880
|
-
template,
|
|
881
|
-
}));
|
|
882
|
-
} else {
|
|
883
|
-
const handler = registration.handler;
|
|
884
|
-
if (typeof handler !== 'function') {
|
|
885
|
-
throw new FlexHarnessValidationError(`slashCommands[${index}].handler must be a function.`);
|
|
886
|
-
}
|
|
887
|
-
normalized.set(registration.name, Object.freeze({
|
|
888
|
-
name: registration.name,
|
|
889
|
-
...(registration.description === undefined
|
|
890
|
-
? {}
|
|
891
|
-
: { description: registration.description }),
|
|
892
|
-
handler,
|
|
893
|
-
}));
|
|
894
|
-
}
|
|
895
|
-
}
|
|
896
|
-
return Object.freeze(normalized);
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
function resolveBoundedPositiveInteger(
|
|
900
|
-
value: number | undefined,
|
|
901
|
-
name: string,
|
|
902
|
-
defaultValue: number,
|
|
903
|
-
maximum: number,
|
|
904
|
-
): number {
|
|
905
|
-
const resolved = value ?? defaultValue;
|
|
906
|
-
if (!Number.isSafeInteger(resolved) || resolved < 1 || resolved > maximum) {
|
|
907
|
-
throw new FlexHarnessValidationError(`${name} must be an integer from 1 through ${maximum}.`);
|
|
908
|
-
}
|
|
909
|
-
return resolved;
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
function requireTransferIdentifier(value: string, field: string): void {
|
|
913
|
-
if (Buffer.byteLength(value, 'utf8') > maxTransferIdentifierBytes) {
|
|
914
|
-
throw new FlexHarnessValidationError(`${field} exceeds the transfer limit.`);
|
|
915
|
-
}
|
|
916
|
-
}
|
|
917
|
-
|
|
918
|
-
function validateProjectedErrorInfo(value: unknown): IFlexErrorInfo {
|
|
919
|
-
if (
|
|
920
|
-
!value
|
|
921
|
-
|| typeof value !== 'object'
|
|
922
|
-
|| Array.isArray(value)
|
|
923
|
-
|| ![Object.prototype, null].includes(Object.getPrototypeOf(value))
|
|
924
|
-
) {
|
|
925
|
-
throw new FlexHarnessValidationError('External error projection is invalid.');
|
|
926
|
-
}
|
|
927
|
-
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
928
|
-
const unsupportedKey = Reflect.ownKeys(descriptors).find(
|
|
929
|
-
(key) => typeof key !== 'string' || !['name', 'message', 'code'].includes(key),
|
|
930
|
-
);
|
|
931
|
-
const name = descriptors.name?.value;
|
|
932
|
-
const message = descriptors.message?.value;
|
|
933
|
-
const code = descriptors.code?.value;
|
|
934
|
-
if (
|
|
935
|
-
unsupportedKey
|
|
936
|
-
|| descriptors.name?.get !== undefined
|
|
937
|
-
|| descriptors.name?.set !== undefined
|
|
938
|
-
|| descriptors.message?.get !== undefined
|
|
939
|
-
|| descriptors.message?.set !== undefined
|
|
940
|
-
|| descriptors.code?.get !== undefined
|
|
941
|
-
|| descriptors.code?.set !== undefined
|
|
942
|
-
|| typeof name !== 'string'
|
|
943
|
-
|| !name
|
|
944
|
-
|| Buffer.byteLength(name, 'utf8') > maxProjectedErrorNameBytes
|
|
945
|
-
|| typeof message !== 'string'
|
|
946
|
-
|| !message
|
|
947
|
-
|| Buffer.byteLength(message, 'utf8') > maxProjectedErrorMessageBytes
|
|
948
|
-
|| (code !== undefined
|
|
949
|
-
&& (typeof code !== 'string'
|
|
950
|
-
|| !code
|
|
951
|
-
|| Buffer.byteLength(code, 'utf8') > maxProjectedErrorCodeBytes))
|
|
952
|
-
) {
|
|
953
|
-
throw new FlexHarnessValidationError('External error projection is invalid.');
|
|
954
|
-
}
|
|
955
|
-
return Object.freeze({ name, message, ...(code === undefined ? {} : { code }) });
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
function normalizeModelIdentity(identity: IFlexModelIdentity): IFlexModelIdentity {
|
|
959
|
-
const normalized: IFlexModelIdentity = {
|
|
960
|
-
provider: identity.provider,
|
|
961
|
-
model: identity.model,
|
|
962
|
-
...(identity.displayName === undefined ? {} : { displayName: identity.displayName }),
|
|
963
|
-
...(identity.variant === undefined ? {} : { variant: identity.variant }),
|
|
964
|
-
};
|
|
965
|
-
validateIdentifier(normalized.provider, 'model identity provider');
|
|
966
|
-
validateIdentifier(normalized.model, 'model identity model');
|
|
967
|
-
if (normalized.displayName !== undefined) {
|
|
968
|
-
validateIdentifier(normalized.displayName, 'model identity displayName');
|
|
969
|
-
}
|
|
970
|
-
if (normalized.variant !== undefined) validateIdentifier(normalized.variant, 'model identity variant');
|
|
971
|
-
if (
|
|
972
|
-
Buffer.byteLength(normalized.provider, 'utf8') > maxTransferIdentifierBytes
|
|
973
|
-
|| Buffer.byteLength(normalized.model, 'utf8') > maxTransferIdentifierBytes
|
|
974
|
-
|| (normalized.displayName !== undefined
|
|
975
|
-
&& Buffer.byteLength(normalized.displayName, 'utf8') > maxTransferMetadataBytes)
|
|
976
|
-
|| (normalized.variant !== undefined && Buffer.byteLength(normalized.variant, 'utf8') > 128)
|
|
977
|
-
) {
|
|
978
|
-
throw new FlexHarnessValidationError('Model identity exceeds its transfer limit.');
|
|
979
|
-
}
|
|
980
|
-
return normalized;
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
function normalizeResolvedModel(resolvedModel: IFlexResolvedModel): IFlexResolvedModel {
|
|
984
|
-
return {
|
|
985
|
-
model: resolvedModel.model,
|
|
986
|
-
identity: normalizeModelIdentity(resolvedModel.identity),
|
|
987
|
-
...(resolvedModel.system === undefined ? {} : { system: resolvedModel.system }),
|
|
988
|
-
...(resolvedModel.providerOptions === undefined
|
|
989
|
-
? {}
|
|
990
|
-
: { providerOptions: resolvedModel.providerOptions }),
|
|
991
|
-
...(resolvedModel.cache === undefined ? {} : { cache: resolvedModel.cache }),
|
|
992
|
-
...(resolvedModel.maxSteps === undefined ? {} : { maxSteps: resolvedModel.maxSteps }),
|
|
993
|
-
};
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
function normalizeRunResult(result: TFlexAgentRunResult): IRunResultProjection {
|
|
997
|
-
const normalized: IRunResultProjection = {
|
|
998
|
-
text: result.text,
|
|
999
|
-
steps: result.steps,
|
|
1000
|
-
finishReason: result.finishReason,
|
|
1001
|
-
usage: {
|
|
1002
|
-
inputTokens: result.usage.inputTokens,
|
|
1003
|
-
outputTokens: result.usage.outputTokens,
|
|
1004
|
-
totalTokens: result.usage.totalTokens,
|
|
1005
|
-
cacheReadTokens: result.usage.cacheReadTokens,
|
|
1006
|
-
cacheWriteTokens: result.usage.cacheWriteTokens,
|
|
1007
|
-
},
|
|
1008
|
-
};
|
|
1009
|
-
if (
|
|
1010
|
-
typeof normalized.text !== 'string'
|
|
1011
|
-
|| typeof normalized.finishReason !== 'string'
|
|
1012
|
-
|| !Number.isSafeInteger(normalized.steps)
|
|
1013
|
-
|| normalized.steps < 0
|
|
1014
|
-
|| Object.values(normalized.usage).some((value) => !Number.isFinite(value) || value < 0)
|
|
1015
|
-
) {
|
|
1016
|
-
throw new FlexHarnessValidationError('Agent generation result is invalid.');
|
|
1017
|
-
}
|
|
1018
|
-
return normalized;
|
|
1019
|
-
}
|
|
1020
|
-
|
|
1021
569
|
function combineErrors(errors: unknown[]): unknown {
|
|
1022
570
|
if (errors.length === 1) return errors[0];
|
|
1023
571
|
const combined = new FlexHarnessRunError(errors);
|
|
@@ -1047,321 +595,6 @@ function containsStoreCommitUncertainty(error: unknown, seen = new Set<unknown>(
|
|
|
1047
595
|
}
|
|
1048
596
|
}
|
|
1049
597
|
|
|
1050
|
-
function base64Size(data: string): number | undefined {
|
|
1051
|
-
const compact = data.replace(/\s/g, '');
|
|
1052
|
-
if (!/^[A-Za-z0-9+/]*={0,2}$/.test(compact) || compact.length % 4 === 1) return undefined;
|
|
1053
|
-
return Buffer.from(compact, 'base64').byteLength;
|
|
1054
|
-
}
|
|
1055
|
-
|
|
1056
|
-
function describeAttachment(
|
|
1057
|
-
part: Exclude<TFlexPromptPart, { type: 'text' }>,
|
|
1058
|
-
): Omit<IFlexAttachmentMessagePart, 'partId' | 'type' | 'attachmentType'> {
|
|
1059
|
-
let source: TFlexAttachmentSource = 'inline-base64';
|
|
1060
|
-
let sizeBytes: number | undefined;
|
|
1061
|
-
let inferredMediaType: string | undefined;
|
|
1062
|
-
if (/^data:/i.test(part.data)) {
|
|
1063
|
-
source = 'data-url';
|
|
1064
|
-
const commaIndex = part.data.indexOf(',');
|
|
1065
|
-
if (commaIndex >= 0) {
|
|
1066
|
-
const metadata = part.data.slice(5, commaIndex);
|
|
1067
|
-
const payload = part.data.slice(commaIndex + 1);
|
|
1068
|
-
const metadataParts = metadata.split(';');
|
|
1069
|
-
if (metadataParts[0]?.includes('/')) inferredMediaType = metadataParts[0];
|
|
1070
|
-
if (metadataParts.some((entry) => entry.toLowerCase() === 'base64')) {
|
|
1071
|
-
sizeBytes = base64Size(payload);
|
|
1072
|
-
} else {
|
|
1073
|
-
try {
|
|
1074
|
-
sizeBytes = Buffer.byteLength(decodeURIComponent(payload));
|
|
1075
|
-
} catch {
|
|
1076
|
-
// Malformed URL encoding leaves size unknown without exposing the payload.
|
|
1077
|
-
}
|
|
1078
|
-
}
|
|
1079
|
-
}
|
|
1080
|
-
} else {
|
|
1081
|
-
try {
|
|
1082
|
-
const url = new URL(part.data);
|
|
1083
|
-
if (url.protocol === 'http:' || url.protocol === 'https:') source = 'remote-url';
|
|
1084
|
-
else sizeBytes = base64Size(part.data);
|
|
1085
|
-
} catch {
|
|
1086
|
-
sizeBytes = base64Size(part.data);
|
|
1087
|
-
}
|
|
1088
|
-
}
|
|
1089
|
-
return {
|
|
1090
|
-
source,
|
|
1091
|
-
...(sizeBytes === undefined ? {} : { sizeBytes }),
|
|
1092
|
-
...(part.mediaType ?? inferredMediaType
|
|
1093
|
-
? { mediaType: part.mediaType ?? inferredMediaType }
|
|
1094
|
-
: {}),
|
|
1095
|
-
...(part.name ? { name: part.name } : {}),
|
|
1096
|
-
};
|
|
1097
|
-
}
|
|
1098
|
-
|
|
1099
|
-
function createBoundedTransferMessage(message: IFlexMessage): IFlexMessage {
|
|
1100
|
-
const projected = cloneSerializable(message);
|
|
1101
|
-
requireTransferIdentifier(projected.messageId, 'messageId');
|
|
1102
|
-
requireTransferIdentifier(projected.sessionId, 'sessionId');
|
|
1103
|
-
requireTransferIdentifier(projected.runId, 'runId');
|
|
1104
|
-
projected.createdAt = truncateUtf8(projected.createdAt, 128);
|
|
1105
|
-
if (projected.completedAt !== undefined) projected.completedAt = truncateUtf8(projected.completedAt, 128);
|
|
1106
|
-
if (projected.error !== undefined) projected.error = truncateUtf8(projected.error, maxTransferMetadataBytes);
|
|
1107
|
-
if (projected.model) {
|
|
1108
|
-
projected.model.provider = truncateUtf8(projected.model.provider, maxTransferIdentifierBytes);
|
|
1109
|
-
projected.model.model = truncateUtf8(projected.model.model, maxTransferIdentifierBytes);
|
|
1110
|
-
if (projected.model.displayName !== undefined) {
|
|
1111
|
-
projected.model.displayName = truncateUtf8(projected.model.displayName, maxTransferMetadataBytes);
|
|
1112
|
-
}
|
|
1113
|
-
if (projected.model.variant !== undefined) projected.model.variant = truncateUtf8(projected.model.variant, 128);
|
|
1114
|
-
}
|
|
1115
|
-
for (const part of projected.parts) {
|
|
1116
|
-
requireTransferIdentifier(part.partId, 'partId');
|
|
1117
|
-
if (part.type === 'text' || part.type === 'reasoning') {
|
|
1118
|
-
part.text = truncateUtf8(part.text, maxTransferTextBytes);
|
|
1119
|
-
} else if (part.type === 'tool') {
|
|
1120
|
-
part.toolCallId = truncateUtf8(part.toolCallId, maxTransferIdentifierBytes);
|
|
1121
|
-
part.toolName = truncateUtf8(part.toolName, maxTransferIdentifierBytes);
|
|
1122
|
-
if (part.error !== undefined) part.error = truncateUtf8(part.error, maxTransferMetadataBytes);
|
|
1123
|
-
} else {
|
|
1124
|
-
if (part.mediaType !== undefined) part.mediaType = truncateUtf8(part.mediaType, maxTransferIdentifierBytes);
|
|
1125
|
-
if (part.name !== undefined) part.name = truncateUtf8(part.name, maxTransferMetadataBytes);
|
|
1126
|
-
}
|
|
1127
|
-
}
|
|
1128
|
-
if (jsonBytes(projected) > maxTransferMessageBytes) {
|
|
1129
|
-
projected.parts = [{
|
|
1130
|
-
partId: 'transfer-elided',
|
|
1131
|
-
type: 'text',
|
|
1132
|
-
text: '[elided: message exceeds the transfer budget]',
|
|
1133
|
-
}];
|
|
1134
|
-
}
|
|
1135
|
-
if (jsonBytes(projected) > maxTransferMessageBytes) {
|
|
1136
|
-
throw new FlexHarnessValidationError('Stored message metadata exceeds the transfer limit.');
|
|
1137
|
-
}
|
|
1138
|
-
return projected;
|
|
1139
|
-
}
|
|
1140
|
-
|
|
1141
|
-
function validateUpdateSessionOptions(options: IFlexUpdateSessionOptions): void {
|
|
1142
|
-
if (!options || typeof options !== 'object' || Array.isArray(options)) {
|
|
1143
|
-
throw new FlexHarnessValidationError('updateSession options must be a plain object.');
|
|
1144
|
-
}
|
|
1145
|
-
const keys = Object.keys(options);
|
|
1146
|
-
if (keys.length === 0) throw new FlexHarnessValidationError('updateSession requires title or archived.');
|
|
1147
|
-
const unsupportedKey = keys.find((key) => key !== 'title' && key !== 'archived');
|
|
1148
|
-
if (unsupportedKey) throw new FlexHarnessValidationError(`updateSession does not support "${unsupportedKey}".`);
|
|
1149
|
-
if (Object.prototype.hasOwnProperty.call(options, 'title') && options.title !== null) {
|
|
1150
|
-
validateUtf8String(
|
|
1151
|
-
options.title,
|
|
1152
|
-
'title',
|
|
1153
|
-
FLEX_PROJECT_MANAGEMENT_LIMITS.maxTitleBytes,
|
|
1154
|
-
true,
|
|
1155
|
-
);
|
|
1156
|
-
}
|
|
1157
|
-
if (Object.prototype.hasOwnProperty.call(options, 'archived') && typeof options.archived !== 'boolean') {
|
|
1158
|
-
throw new FlexHarnessValidationError('archived must be a boolean.');
|
|
1159
|
-
}
|
|
1160
|
-
}
|
|
1161
|
-
|
|
1162
|
-
function validateProjectTaskStatus(value: unknown): asserts value is TFlexProjectTaskStatus {
|
|
1163
|
-
if (!['pending', 'in_progress', 'completed', 'cancelled'].includes(String(value))) {
|
|
1164
|
-
throw new FlexHarnessValidationError('Project task status is invalid.');
|
|
1165
|
-
}
|
|
1166
|
-
}
|
|
1167
|
-
|
|
1168
|
-
function validateProjectTaskPriority(value: unknown): asserts value is TFlexProjectTaskPriority {
|
|
1169
|
-
if (!['high', 'medium', 'low'].includes(String(value))) {
|
|
1170
|
-
throw new FlexHarnessValidationError('Project task priority is invalid.');
|
|
1171
|
-
}
|
|
1172
|
-
}
|
|
1173
|
-
|
|
1174
|
-
function validateProjectTaskId(value: unknown): asserts value is string {
|
|
1175
|
-
validateUtf8String(
|
|
1176
|
-
value,
|
|
1177
|
-
'project task id',
|
|
1178
|
-
FLEX_PROJECT_MANAGEMENT_LIMITS.maxTaskIdBytes,
|
|
1179
|
-
true,
|
|
1180
|
-
);
|
|
1181
|
-
}
|
|
1182
|
-
|
|
1183
|
-
function validateProjectTaskContent(value: unknown): asserts value is string {
|
|
1184
|
-
validateUtf8String(
|
|
1185
|
-
value,
|
|
1186
|
-
'project task content',
|
|
1187
|
-
FLEX_PROJECT_MANAGEMENT_LIMITS.maxTaskContentBytes,
|
|
1188
|
-
true,
|
|
1189
|
-
);
|
|
1190
|
-
}
|
|
1191
|
-
|
|
1192
|
-
function validateCreateProjectTaskInput(input: IFlexCreateProjectTaskInput): void {
|
|
1193
|
-
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
1194
|
-
throw new FlexHarnessValidationError('Project task create input must be a plain object.');
|
|
1195
|
-
}
|
|
1196
|
-
const unsupported = Object.keys(input)
|
|
1197
|
-
.find((key) => !['id', 'content', 'status', 'priority'].includes(key));
|
|
1198
|
-
if (unsupported) {
|
|
1199
|
-
throw new FlexHarnessValidationError(`Project task create input does not support "${unsupported}".`);
|
|
1200
|
-
}
|
|
1201
|
-
validateProjectTaskId(input.id);
|
|
1202
|
-
validateProjectTaskContent(input.content);
|
|
1203
|
-
if (input.status !== undefined) validateProjectTaskStatus(input.status);
|
|
1204
|
-
if (input.priority !== undefined) validateProjectTaskPriority(input.priority);
|
|
1205
|
-
}
|
|
1206
|
-
|
|
1207
|
-
function validateUpdateProjectTaskInput(input: IFlexUpdateProjectTaskInput): void {
|
|
1208
|
-
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
1209
|
-
throw new FlexHarnessValidationError('Project task update input must be a plain object.');
|
|
1210
|
-
}
|
|
1211
|
-
const keys = Object.keys(input);
|
|
1212
|
-
const unsupported = keys.find((key) => !['id', 'content', 'status', 'priority'].includes(key));
|
|
1213
|
-
if (unsupported) {
|
|
1214
|
-
throw new FlexHarnessValidationError(`Project task update input does not support "${unsupported}".`);
|
|
1215
|
-
}
|
|
1216
|
-
if (!keys.some((key) => key !== 'id')) {
|
|
1217
|
-
throw new FlexHarnessValidationError('Project task update requires a changed field.');
|
|
1218
|
-
}
|
|
1219
|
-
validateProjectTaskId(input.id);
|
|
1220
|
-
if (input.content !== undefined) validateProjectTaskContent(input.content);
|
|
1221
|
-
if (input.status !== undefined) validateProjectTaskStatus(input.status);
|
|
1222
|
-
if (input.priority !== undefined) validateProjectTaskPriority(input.priority);
|
|
1223
|
-
}
|
|
1224
|
-
|
|
1225
|
-
function validatePromptOptions(options: IFlexPromptOptions, scheduled: boolean): void {
|
|
1226
|
-
if (!options || typeof options !== 'object' || Array.isArray(options)) {
|
|
1227
|
-
throw new FlexHarnessValidationError('Prompt options must be a plain object.');
|
|
1228
|
-
}
|
|
1229
|
-
const supported = scheduled
|
|
1230
|
-
? ['modelHint', 'system', 'maxSteps', 'debounceMs']
|
|
1231
|
-
: ['modelHint', 'system', 'maxSteps'];
|
|
1232
|
-
const unsupported = Object.keys(options).find((key) => !supported.includes(key));
|
|
1233
|
-
if (unsupported) throw new FlexHarnessValidationError(`Prompt options do not support "${unsupported}".`);
|
|
1234
|
-
if (options.modelHint !== undefined) validateIdentifier(options.modelHint, 'modelHint');
|
|
1235
|
-
if (options.system !== undefined) validateIdentifier(options.system, 'system');
|
|
1236
|
-
if (options.maxSteps !== undefined && (!Number.isSafeInteger(options.maxSteps) || options.maxSteps < 1)) {
|
|
1237
|
-
throw new FlexHarnessValidationError('maxSteps must be a positive integer.');
|
|
1238
|
-
}
|
|
1239
|
-
}
|
|
1240
|
-
|
|
1241
|
-
function validateSlashCommandExecutionOptions(
|
|
1242
|
-
options: IFlexSlashCommandExecutionOptions,
|
|
1243
|
-
): IFlexPromptOptions {
|
|
1244
|
-
if (!options || typeof options !== 'object' || Array.isArray(options)) {
|
|
1245
|
-
throw new FlexHarnessValidationError('Slash command execution options must be a plain object.');
|
|
1246
|
-
}
|
|
1247
|
-
const unsupported = Object.keys(options)
|
|
1248
|
-
.find((key) => !['modelHint', 'system', 'maxSteps', 'signal'].includes(key));
|
|
1249
|
-
if (unsupported) {
|
|
1250
|
-
throw new FlexHarnessValidationError(
|
|
1251
|
-
`Slash command execution options do not support "${unsupported}".`,
|
|
1252
|
-
);
|
|
1253
|
-
}
|
|
1254
|
-
if (options.signal !== undefined && !(options.signal instanceof AbortSignal)) {
|
|
1255
|
-
throw new FlexHarnessValidationError('Slash command signal must be an AbortSignal.');
|
|
1256
|
-
}
|
|
1257
|
-
const promptOptions: IFlexPromptOptions = {
|
|
1258
|
-
...(options.modelHint === undefined ? {} : { modelHint: options.modelHint }),
|
|
1259
|
-
...(options.system === undefined ? {} : { system: options.system }),
|
|
1260
|
-
...(options.maxSteps === undefined ? {} : { maxSteps: options.maxSteps }),
|
|
1261
|
-
};
|
|
1262
|
-
validatePromptOptions(promptOptions, false);
|
|
1263
|
-
return promptOptions;
|
|
1264
|
-
}
|
|
1265
|
-
|
|
1266
|
-
function resolveCallbackLimits(limits: IFlexCallbackLimits = {}): Required<IFlexCallbackLimits> {
|
|
1267
|
-
const resolved = {
|
|
1268
|
-
maxEvents: limits.maxEvents ?? DEFAULT_CALLBACK_LIMITS.maxEvents,
|
|
1269
|
-
maxOutputBytes: limits.maxOutputBytes ?? DEFAULT_CALLBACK_LIMITS.maxOutputBytes,
|
|
1270
|
-
maxParts: limits.maxParts ?? DEFAULT_CALLBACK_LIMITS.maxParts,
|
|
1271
|
-
};
|
|
1272
|
-
for (const [name, value] of Object.entries(resolved)) {
|
|
1273
|
-
if (!Number.isSafeInteger(value) || value < 1) {
|
|
1274
|
-
throw new FlexHarnessValidationError(`callbackLimits.${name} must be a positive integer.`);
|
|
1275
|
-
}
|
|
1276
|
-
}
|
|
1277
|
-
return resolved;
|
|
1278
|
-
}
|
|
1279
|
-
|
|
1280
|
-
function resolvePromptQueueLimits(
|
|
1281
|
-
limits: IFlexPromptQueueLimits = {},
|
|
1282
|
-
): Required<IFlexPromptQueueLimits> {
|
|
1283
|
-
const resolved = {
|
|
1284
|
-
maxOutstandingPromptsPerSession: limits.maxOutstandingPromptsPerSession
|
|
1285
|
-
?? DEFAULT_PROMPT_QUEUE_LIMITS.maxOutstandingPromptsPerSession,
|
|
1286
|
-
maxOutstandingBytesPerSession: limits.maxOutstandingBytesPerSession
|
|
1287
|
-
?? DEFAULT_PROMPT_QUEUE_LIMITS.maxOutstandingBytesPerSession,
|
|
1288
|
-
maxPendingAdmissions: limits.maxPendingAdmissions
|
|
1289
|
-
?? DEFAULT_PROMPT_QUEUE_LIMITS.maxPendingAdmissions,
|
|
1290
|
-
maxPendingAdmissionBytes: limits.maxPendingAdmissionBytes
|
|
1291
|
-
?? DEFAULT_PROMPT_QUEUE_LIMITS.maxPendingAdmissionBytes,
|
|
1292
|
-
maxTerminalEntriesPerSession: limits.maxTerminalEntriesPerSession
|
|
1293
|
-
?? DEFAULT_PROMPT_QUEUE_LIMITS.maxTerminalEntriesPerSession,
|
|
1294
|
-
};
|
|
1295
|
-
for (const [name, value] of Object.entries(resolved)) {
|
|
1296
|
-
if (!Number.isSafeInteger(value) || value < 1) {
|
|
1297
|
-
throw new FlexHarnessValidationError(`promptQueueLimits.${name} must be a positive integer.`);
|
|
1298
|
-
}
|
|
1299
|
-
}
|
|
1300
|
-
return resolved;
|
|
1301
|
-
}
|
|
1302
|
-
|
|
1303
|
-
function resolveReversionLimits(
|
|
1304
|
-
limits: IFlexReversionLimits = {},
|
|
1305
|
-
): Required<IFlexReversionLimits> {
|
|
1306
|
-
const resolved = {
|
|
1307
|
-
maxCompletedTurns: limits.maxCompletedTurns ?? FLEX_REVERSION_DEFAULT_LIMITS.maxCompletedTurns,
|
|
1308
|
-
maxSegments: limits.maxSegments ?? FLEX_REVERSION_DEFAULT_LIMITS.maxSegments,
|
|
1309
|
-
maxExcludedRunIds: limits.maxExcludedRunIds ?? FLEX_REVERSION_DEFAULT_LIMITS.maxExcludedRunIds,
|
|
1310
|
-
maxPendingReversionReleases: limits.maxPendingReversionReleases
|
|
1311
|
-
?? FLEX_REVERSION_DEFAULT_LIMITS.maxPendingReversionReleases,
|
|
1312
|
-
};
|
|
1313
|
-
for (const [name, value] of Object.entries(resolved)) {
|
|
1314
|
-
const maximum = FLEX_REVERSION_MAXIMUM_LIMITS[name as keyof IFlexReversionLimits];
|
|
1315
|
-
if (!Number.isSafeInteger(value) || value < 1 || value > maximum) {
|
|
1316
|
-
throw new FlexHarnessValidationError(
|
|
1317
|
-
`reversionLimits.${name} must be an integer from 1 through ${maximum}.`,
|
|
1318
|
-
);
|
|
1319
|
-
}
|
|
1320
|
-
}
|
|
1321
|
-
return resolved;
|
|
1322
|
-
}
|
|
1323
|
-
|
|
1324
|
-
function normalizeAgentSessionPolicy<TScope>(
|
|
1325
|
-
policy: IFlexAgentSessionPolicy<TScope> = {},
|
|
1326
|
-
): IFlexAgentSessionPolicy<TScope> {
|
|
1327
|
-
return {
|
|
1328
|
-
...(policy.contextBuilder === undefined ? {} : { contextBuilder: policy.contextBuilder }),
|
|
1329
|
-
...(policy.contextCompactor === undefined ? {} : { contextCompactor: policy.contextCompactor }),
|
|
1330
|
-
...(policy.eventRetention === undefined ? {} : { eventRetention: policy.eventRetention }),
|
|
1331
|
-
...(policy.changeListenerTimeoutMs === undefined
|
|
1332
|
-
? {}
|
|
1333
|
-
: { changeListenerTimeoutMs: policy.changeListenerTimeoutMs }),
|
|
1334
|
-
...(policy.maxPendingSessionChanges === undefined
|
|
1335
|
-
? {}
|
|
1336
|
-
: { maxPendingSessionChanges: policy.maxPendingSessionChanges }),
|
|
1337
|
-
...(policy.generationLeaseCleanupTimeoutMs === undefined
|
|
1338
|
-
? {}
|
|
1339
|
-
: { generationLeaseCleanupTimeoutMs: policy.generationLeaseCleanupTimeoutMs }),
|
|
1340
|
-
...(policy.maxArchivedTransactionTombstones === undefined
|
|
1341
|
-
? {}
|
|
1342
|
-
: { maxArchivedTransactionTombstones: policy.maxArchivedTransactionTombstones }),
|
|
1343
|
-
...(policy.maxContextOverflowRetries === undefined
|
|
1344
|
-
? {}
|
|
1345
|
-
: { maxContextOverflowRetries: policy.maxContextOverflowRetries }),
|
|
1346
|
-
};
|
|
1347
|
-
}
|
|
1348
|
-
|
|
1349
|
-
function requireHarnessStores(stores: IFlexHarnessStores): IFlexHarnessStores {
|
|
1350
|
-
const projectManagement = stores?.projectManagement;
|
|
1351
|
-
if (
|
|
1352
|
-
!projectManagement
|
|
1353
|
-
|| typeof projectManagement.load !== 'function'
|
|
1354
|
-
|| typeof projectManagement.save !== 'function'
|
|
1355
|
-
|| typeof projectManagement.tombstoneSession !== 'function'
|
|
1356
|
-
|| typeof projectManagement.purgeNamespace !== 'function'
|
|
1357
|
-
) {
|
|
1358
|
-
throw new FlexHarnessValidationError(
|
|
1359
|
-
'stores.projectManagement requires load/save/tombstoneSession/purgeNamespace methods.',
|
|
1360
|
-
);
|
|
1361
|
-
}
|
|
1362
|
-
return stores;
|
|
1363
|
-
}
|
|
1364
|
-
|
|
1365
598
|
export class FlexHarness<TScope = unknown> {
|
|
1366
599
|
private readonly scopeResolver: IFlexHarnessOptions<TScope>['scopeResolver'];
|
|
1367
600
|
private readonly modelResolver: IFlexHarnessOptions<TScope>['modelResolver'];
|
|
@@ -2253,7 +1486,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
2253
1486
|
const parsed = parseSlashCommand(untouchedInput);
|
|
2254
1487
|
if (parsed.type !== 'parsed') return parsed;
|
|
2255
1488
|
const registration = this.slashCommands.get(parsed.name);
|
|
2256
|
-
if (!
|
|
1489
|
+
if (!isReservedSlashCommandName(parsed.name) && !registration) {
|
|
2257
1490
|
return Object.freeze({
|
|
2258
1491
|
type: 'unknown',
|
|
2259
1492
|
input: parsed.input,
|
|
@@ -5667,6 +4900,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
5667
4900
|
part.status = status === 'completed' ? 'completed' : 'cancelled';
|
|
5668
4901
|
} else if (part.type === 'tool' && part.status === 'running') {
|
|
5669
4902
|
part.status = status === 'cancelled' ? 'cancelled' : 'failed';
|
|
4903
|
+
delete part.output;
|
|
5670
4904
|
part.error = status === 'cancelled'
|
|
5671
4905
|
? 'Tool execution was cancelled.'
|
|
5672
4906
|
: 'Tool execution ended without a terminal event.';
|
|
@@ -6928,6 +6162,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
6928
6162
|
part.output = output!;
|
|
6929
6163
|
} else {
|
|
6930
6164
|
part.status = 'failed';
|
|
6165
|
+
delete part.output;
|
|
6931
6166
|
part.error = projectedError!.message;
|
|
6932
6167
|
}
|
|
6933
6168
|
this.emitPartEvent(run, 'part.completed', part);
|
|
@@ -6936,6 +6171,31 @@ export class FlexHarness<TScope = unknown> {
|
|
|
6936
6171
|
}
|
|
6937
6172
|
}
|
|
6938
6173
|
|
|
6174
|
+
private onToolUpdate(
|
|
6175
|
+
state: IStorageState,
|
|
6176
|
+
sessionId: string,
|
|
6177
|
+
event: TFlexAgentToolCallUpdateEvent,
|
|
6178
|
+
): void {
|
|
6179
|
+
let run = this.callbackRun(state, sessionId);
|
|
6180
|
+
if (!run) return;
|
|
6181
|
+
if (!run.toolPartIds.has(event.toolCallId)) {
|
|
6182
|
+
this.onToolStart(state, sessionId, event);
|
|
6183
|
+
run = this.callbackRun(state, sessionId);
|
|
6184
|
+
if (!run) return;
|
|
6185
|
+
}
|
|
6186
|
+
const partId = run.toolPartIds.get(event.toolCallId);
|
|
6187
|
+
const part = run.callbackParts.find((entry) => entry.partId === partId && entry.type === 'tool');
|
|
6188
|
+
if (!part || part.type !== 'tool' || part.status !== 'running') return;
|
|
6189
|
+
try {
|
|
6190
|
+
const output = normalizeJsonValue(event.output, this.toolOutputLimits);
|
|
6191
|
+
if (!this.reserveCallbackCapacity(run, 1, jsonBytes(output), 0)) return;
|
|
6192
|
+
part.output = output;
|
|
6193
|
+
this.emitPartEvent(run, 'part.updated', part);
|
|
6194
|
+
} catch (error) {
|
|
6195
|
+
this.failCallback(run, this.projectExternalError(run, error, 'toolCallback'));
|
|
6196
|
+
}
|
|
6197
|
+
}
|
|
6198
|
+
|
|
6939
6199
|
private callbackRun(state: IStorageState, sessionId: string): IActiveRun | undefined {
|
|
6940
6200
|
const run = state.activeRuns.get(sessionId);
|
|
6941
6201
|
return !run || run.callbacksClosed || run.callbackError ? undefined : run;
|
|
@@ -8100,6 +7360,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
8100
7360
|
onReasoningDelta: (id, delta) => this.onReasoningDelta(state, sessionId, id, delta),
|
|
8101
7361
|
onReasoningEnd: (id, text) => this.onReasoningEnd(state, sessionId, id, text),
|
|
8102
7362
|
onToolCallStart: (event) => this.onToolStart(state, sessionId, event),
|
|
7363
|
+
onToolCallUpdate: (event) => this.onToolUpdate(state, sessionId, event),
|
|
8103
7364
|
onToolCallFinish: (event) => this.onToolFinish(state, sessionId, event),
|
|
8104
7365
|
};
|
|
8105
7366
|
stored.agentSession = await this.withCompactorContext(
|