@modelprofile.com/flexharness 10.12.0 → 10.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/classes.codesandbox.d.ts +39 -0
  3. package/dist_ts/classes.codesandbox.js +539 -0
  4. package/dist_ts/classes.flexharness.d.ts +17 -0
  5. package/dist_ts/classes.flexharness.js +222 -8
  6. package/dist_ts/classes.toolcatalog.d.ts +90 -0
  7. package/dist_ts/classes.toolcatalog.js +1398 -0
  8. package/dist_ts/index.d.ts +1 -0
  9. package/dist_ts/index.js +2 -1
  10. package/dist_ts/interfaces.d.ts +237 -3
  11. package/dist_ts/interfaces.js +50 -1
  12. package/dist_ts/plugins.d.ts +66 -1
  13. package/dist_ts/plugins.js +19 -1
  14. package/dist_ts/utils.harnessoptions.d.ts +7 -1
  15. package/dist_ts/utils.harnessoptions.js +48 -2
  16. package/dist_ts/utils.json.js +3 -1
  17. package/dist_ts/utils.schemadeclarations.d.ts +53 -0
  18. package/dist_ts/utils.schemadeclarations.js +315 -0
  19. package/dist_ts/utils.tooloutput.d.ts +2 -0
  20. package/dist_ts/utils.tooloutput.js +11 -1
  21. package/dist_ts_compaction/plugins.d.ts +3 -3
  22. package/dist_ts_compaction/plugins.js +3 -3
  23. package/dist_ts_compaction/smartagent.compaction.d.ts +10 -1
  24. package/dist_ts_compaction/smartagent.compaction.js +79 -38
  25. package/package.json +17 -4
  26. package/readme.md +1 -1
  27. package/ts/00_commitinfo_data.ts +1 -1
  28. package/ts/classes.codesandbox.ts +565 -0
  29. package/ts/classes.flexharness.ts +242 -7
  30. package/ts/classes.toolcatalog.ts +1512 -0
  31. package/ts/index.ts +1 -0
  32. package/ts/interfaces.ts +285 -3
  33. package/ts/plugins.ts +86 -0
  34. package/ts/readme.md +1 -1
  35. package/ts/utils.harnessoptions.ts +55 -0
  36. package/ts/utils.json.ts +2 -0
  37. package/ts/utils.schemadeclarations.ts +322 -0
  38. package/ts/utils.tooloutput.ts +9 -0
  39. package/ts_compaction/plugins.ts +3 -3
  40. package/ts_compaction/smartagent.compaction.ts +90 -34
package/ts/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from './classes.flexharness.js';
2
2
  export * from './classes.stores.js';
3
+ export { FlexToolCatalog } from './classes.toolcatalog.js';
3
4
  export * from './errors.js';
4
5
  export * from './interfaces.js';
5
6
  export { normalizeJsonValue } from './utils.json.js';
package/ts/interfaces.ts CHANGED
@@ -31,6 +31,8 @@ export type TFlexRetryReason = plugins.TAgentRetryReason;
31
31
  * Every call carries `provider` and `requestedModelId`; key usage caps on those two.
32
32
  */
33
33
  export type TFlexModelCallUsage = plugins.TAgentModelCallUsage;
34
+ /** A model call's usage as harness events carry it: a reported call also states its cache hit ratio. */
35
+ export type TFlexObservedModelCallUsage = plugins.TAgentObservedModelCallUsage;
34
36
 
35
37
  export interface IFlexTextPromptPart {
36
38
  type: 'text';
@@ -135,6 +137,8 @@ export interface IFlexToolMessagePart {
135
137
  output?: TJsonValue;
136
138
  error?: string;
137
139
  childSessionId?: string;
140
+ /** For a catalog call a `run_code` program made: the `run_code` call's ID. */
141
+ parentToolCallId?: string;
138
142
  model?: IFlexModelIdentity;
139
143
  /** ISO time the tool call began. Absent on parts recorded before part times existed. */
140
144
  startedAt?: string;
@@ -648,17 +652,294 @@ export interface IFlexToolProviderContext<TScope> extends IFlexSessionGeneration
648
652
  requestPermission(request: IFlexPermissionRequestInput): Promise<void>;
649
653
  }
650
654
 
651
- export interface IFlexToolHandle {
655
+ export interface IFlexToolHandle<TScope = unknown> {
652
656
  tools: TFlexAgentToolSet;
657
+ /**
658
+ * Methods the model finds with `search_tools`, reads with `describe_tools` and calls with
659
+ * `execute`, instead of seeing each one as a tool on every step. Return the same object on
660
+ * every run: FlexHarness prepares a catalog once per object and treats it as immutable.
661
+ */
662
+ catalog?: IFlexToolCatalog<TScope>;
653
663
  close?(): Promise<void> | void;
654
664
  }
655
665
 
656
666
  export interface IFlexToolProvider<TScope> {
657
667
  provideTools(
658
668
  context: IFlexToolProviderContext<TScope>,
659
- ): Promise<IFlexToolHandle | undefined> | IFlexToolHandle | undefined;
669
+ ): Promise<IFlexToolHandle<TScope> | undefined> | IFlexToolHandle<TScope> | undefined;
670
+ }
671
+
672
+ /** The names of the built-in catalog tools, reserved while a run has a catalog. */
673
+ export const FLEX_TOOL_CATALOG_TOOL_NAMES = Object.freeze({
674
+ search: 'search_tools',
675
+ describe: 'describe_tools',
676
+ execute: 'execute',
677
+ } as const);
678
+
679
+ export const FLEX_TOOL_CATALOG_LIMITS = Object.freeze({
680
+ maxMethods: 4096,
681
+ maxTypes: 4096,
682
+ maxMethodPathCharacters: 160,
683
+ maxMethodPathSegments: 8,
684
+ maxIdentifierCharacters: 128,
685
+ maxSummaryCharacters: 120,
686
+ maxDescriptionBytes: 16 * 1024,
687
+ maxDeclarationBytes: 16 * 1024,
688
+ maxTags: 16,
689
+ maxTagCharacters: 48,
690
+ maxExamples: 4,
691
+ maxExampleBytes: 1024,
692
+ maxSchemaBytes: 64 * 1024,
693
+ defaultSearchResults: 10,
694
+ maxSearchResults: 50,
695
+ maxQueryCharacters: 512,
696
+ maxDescribeMethods: 16,
697
+ maxDescribeTypes: 32,
698
+ maxDescribeBytes: 64 * 1024,
699
+ maxParamIssues: 10,
700
+ });
701
+
702
+ export interface IFlexCatalogSearchInput {
703
+ /** Words to rank methods by. */
704
+ query?: string;
705
+ /** A dotted namespace to search in or, without `query`, to list. */
706
+ namespace?: string;
707
+ limit?: number;
708
+ offset?: number;
709
+ }
710
+
711
+ export interface IFlexCatalogDescribeInput {
712
+ /** Dotted method paths. */
713
+ methods?: readonly string[];
714
+ /** Named types to declare again or for the first time. */
715
+ types?: readonly string[];
716
+ }
717
+
718
+ export interface IFlexCatalogDescribeResult {
719
+ /** TypeScript text, or a JSON object in `json-schema` mode. */
720
+ output: TJsonValue;
721
+ /** The named types the output declares. */
722
+ declaredTypes: string[];
723
+ }
724
+
725
+ /** One way `execute`'s params miss the method's schema. */
726
+ export interface IFlexCatalogParamIssue {
727
+ /** Where, such as `params.lines[0].amount`. */
728
+ path: string;
729
+ expected: string;
730
+ /** The given value as compact JSON, or `missing`. */
731
+ got: string;
660
732
  }
661
733
 
734
+ /** What `execute` reports to the model, as JSON, when it cannot call a method. */
735
+ export type TFlexCatalogCallError =
736
+ | {
737
+ error: 'unknown_method';
738
+ method: string;
739
+ didYouMean: string[];
740
+ hint: string;
741
+ }
742
+ | {
743
+ error: 'invalid_params';
744
+ method: string;
745
+ issues: IFlexCatalogParamIssue[];
746
+ /** How many more issues were found than listed. */
747
+ more?: number;
748
+ /** The lookup that returns the method's declaration. */
749
+ see: string;
750
+ }
751
+ | {
752
+ error: 'denied';
753
+ method: string;
754
+ reason: string;
755
+ };
756
+
757
+ export interface IFlexCatalogToolDefinition {
758
+ description: string;
759
+ /** The tool's input as JSON Schema. */
760
+ inputSchema: plugins.JSONSchema7;
761
+ }
762
+
763
+ /**
764
+ * A parameter or result schema: a plain JSON Schema object, or a zod, Standard Schema or AI SDK
765
+ * schema. `$ref`s to `#/$defs/<Name>` or `#/definitions/<Name>` that the schema does not define name
766
+ * a catalog type.
767
+ */
768
+ export type TFlexCatalogSchema = { readonly [keyword: string]: unknown } | plugins.FlexibleSchema<unknown>;
769
+
770
+ export interface IFlexCatalogRunContext<TScope = unknown> {
771
+ /** The dotted path the model called. */
772
+ readonly method: string;
773
+ /**
774
+ * The call's ID, which its tool part and permission requests carry: the `execute` call's, or for
775
+ * a call inside `run_code` the inner call's, `<run_code call ID>.<n>`.
776
+ */
777
+ readonly toolCallId: string;
778
+ readonly abortSignal: AbortSignal;
779
+ /** The run's tool-provider context: scope, session, run and `requestPermission`. */
780
+ readonly provider: Readonly<IFlexToolProviderContext<TScope>>;
781
+ }
782
+
783
+ export interface IFlexCatalogMethod<TScope = unknown, TParams = any, TResult = unknown> {
784
+ /**
785
+ * The dotted path the model calls, such as `banking.listMovements`: up to 8 segments, each a
786
+ * JavaScript identifier of letters, digits and `_`.
787
+ */
788
+ method: string;
789
+ /** One line of at most 120 characters, shown in search results. */
790
+ summary: string;
791
+ /** The full description, shown with the declaration. */
792
+ description?: string;
793
+ /** More words `search_tools` matches. */
794
+ tags?: readonly string[];
795
+ /** Code shown as `@example` in the declaration. */
796
+ examples?: readonly string[];
797
+ /** The parameter object's schema; `execute` validates every call against it. */
798
+ inputSchema: TFlexCatalogSchema;
799
+ /** The result's schema, declared as the method's return type. */
800
+ outputSchema?: TFlexCatalogSchema;
801
+ /**
802
+ * Declaration text that replaces the generated one, such as a declaration built from TypeScript
803
+ * sources with their JSDoc: `function listMovements(params: …): Promise<…>;`, optionally after
804
+ * its JSDoc, with the last path segment as the function name.
805
+ */
806
+ declaration?: string;
807
+ /** Runs the method with validated parameters. */
808
+ run(
809
+ params: TParams,
810
+ context: IFlexCatalogRunContext<TScope>,
811
+ ): TResult | PromiseLike<TResult> | AsyncIterable<TResult>;
812
+ /**
813
+ * Decides each validated call before it runs: `run` (the default without this hook), `propose`
814
+ * to hand it to the catalog's `onProposals` instead of running it, or `{ deny }` to refuse it.
815
+ */
816
+ onCall?(
817
+ params: TParams,
818
+ context: IFlexCatalogRunContext<TScope>,
819
+ ): TFlexCatalogCallDecision | PromiseLike<TFlexCatalogCallDecision>;
820
+ }
821
+
822
+ export type TFlexCatalogCallDecision = 'run' | 'propose' | { deny: string };
823
+
824
+ /** A call a method's `onCall` decided to propose instead of running. */
825
+ export interface IFlexCatalogProposal {
826
+ /** The call's ID: the `execute` call's, or the inner call's inside `run_code`. */
827
+ id: string;
828
+ method: string;
829
+ params: unknown;
830
+ }
831
+
832
+ export interface IFlexCatalogProposalContext<TScope = unknown> {
833
+ /** The `execute` or `run_code` call that made the proposals. */
834
+ readonly toolCallId: string;
835
+ readonly abortSignal: AbortSignal;
836
+ readonly provider: Readonly<IFlexToolProviderContext<TScope>>;
837
+ }
838
+
839
+ /** A named type that method declarations refer to; it needs a `declaration`, a `schema`, or both. */
840
+ export interface IFlexCatalogType {
841
+ /** A JavaScript identifier such as `IMoney`. */
842
+ name: string;
843
+ description?: string;
844
+ /** Validates parameters that refer to this type, and generates its declaration. */
845
+ schema?: TFlexCatalogSchema;
846
+ /** Declaration text that replaces the generated one, such as `interface IMoney { … }`. */
847
+ declaration?: string;
848
+ }
849
+
850
+ export interface IFlexToolCatalog<TScope = unknown> {
851
+ methods: readonly IFlexCatalogMethod<TScope>[];
852
+ types?: readonly IFlexCatalogType[];
853
+ /**
854
+ * Receives the calls `onCall` proposed, as one batch: the proposed call of an `execute`, or every
855
+ * call a `run_code` program proposed, once the program finished. A failed program proposes
856
+ * nothing. What it returns is what the model sees for them.
857
+ */
858
+ onProposals?(
859
+ proposals: readonly IFlexCatalogProposal[],
860
+ context: IFlexCatalogProposalContext<TScope>,
861
+ ): unknown;
862
+ }
863
+
864
+ export interface IFlexToolCatalogOptions {
865
+ /**
866
+ * What `describe_tools` returns: `typescript` (default) declarations with named types shared
867
+ * across the session, or `json-schema` with each method's self-contained JSON Schema.
868
+ */
869
+ declarations?: 'typescript' | 'json-schema';
870
+ /**
871
+ * Adds `run_code`, which runs a short TypeScript or JavaScript program against the catalog in a
872
+ * QuickJS sandbox. Needs the optional peers `quickjs-emscripten-core`,
873
+ * `@jitl/quickjs-ng-wasmfile-release-sync` and `amaro`.
874
+ */
875
+ codeMode?: boolean | IFlexCodeModeOptions;
876
+ }
877
+
878
+ export interface IFlexCodeModeOptions {
879
+ limits?: Partial<IFlexCodeModeLimits>;
880
+ }
881
+
882
+ /** The bounds of one `run_code` program. */
883
+ export interface IFlexCodeModeLimits {
884
+ /** UTF-8 bytes of the program. */
885
+ maxSourceBytes: number;
886
+ /** Milliseconds the program may compute; time waiting for calls does not count. */
887
+ cpuMs: number;
888
+ /** Milliseconds the program may take in all, not counting waits for a permission decision. */
889
+ wallMs: number;
890
+ /** Bytes of the sandbox's heap. */
891
+ memoryBytes: number;
892
+ /**
893
+ * Bytes of the sandbox's stack. QuickJS runs on the host's stack as well, so a larger value lets
894
+ * deep recursion reach the host's limit first; the program then fails at its `stack` limit all
895
+ * the same.
896
+ */
897
+ stackBytes: number;
898
+ /** Catalog calls per program. */
899
+ maxCalls: number;
900
+ /** Catalog calls running at once; further calls wait for a slot. */
901
+ maxConcurrentCalls: number;
902
+ /** UTF-8 bytes of console output kept; more is cut off. */
903
+ maxLogBytes: number;
904
+ }
905
+
906
+ export const FLEX_CODE_MODE_TOOL_NAME = 'run_code';
907
+
908
+ export const FLEX_CODE_MODE_DEFAULT_LIMITS: Readonly<IFlexCodeModeLimits> = Object.freeze({
909
+ maxSourceBytes: 32 * 1024,
910
+ cpuMs: 5_000,
911
+ wallMs: 60_000,
912
+ memoryBytes: 64 * 1024 * 1024,
913
+ stackBytes: 64 * 1024,
914
+ maxCalls: 200,
915
+ maxConcurrentCalls: 8,
916
+ maxLogBytes: 16 * 1024,
917
+ });
918
+
919
+ export const FLEX_CODE_MODE_MAXIMUM_LIMITS: Readonly<IFlexCodeModeLimits> = Object.freeze({
920
+ maxSourceBytes: 256 * 1024,
921
+ cpuMs: 60_000,
922
+ wallMs: 30 * 60_000,
923
+ memoryBytes: 512 * 1024 * 1024,
924
+ stackBytes: 256 * 1024,
925
+ maxCalls: 10_000,
926
+ maxConcurrentCalls: 64,
927
+ maxLogBytes: 256 * 1024,
928
+ });
929
+
930
+ /** What `run_code` reports to the model, as JSON, when a program does not finish. */
931
+ export interface IFlexCodeRunFailure {
932
+ error: 'syntax_error' | 'program_error' | 'limit_exceeded' | 'aborted';
933
+ /** The limit that stopped the program, for `limit_exceeded`. */
934
+ limit?: 'source' | 'cpu' | 'wall' | 'memory' | 'stack' | 'calls';
935
+ name?: string;
936
+ message: string;
937
+ /** The program's line, when known. */
938
+ line?: number;
939
+ /** Catalog calls the program made. */
940
+ calls: number;
941
+ logs?: string;
942
+ }
662
943
  export interface IFlexResourceToolProviderResolverContext<TScope> extends IFlexSessionGeneration {
663
944
  scopeId: string;
664
945
  scope: TScope;
@@ -1419,6 +1700,7 @@ export interface IFlexHarnessOptions<TScope> {
1419
1700
  externalErrorProjector?: TFlexExternalErrorProjector;
1420
1701
  subagents?: IFlexSubagentDefinition[];
1421
1702
  builtInTools?: IFlexBuiltInToolsOptions;
1703
+ toolCatalog?: IFlexToolCatalogOptions;
1422
1704
  slashCommands?: readonly TFlexSlashCommandRegistration<TScope>[];
1423
1705
  /** Maximum child depth (root is 0). Defaults to 6; configurable from 1 through 8. */
1424
1706
  maxSubagentDepth?: number;
@@ -1597,7 +1879,7 @@ export type TFlexModelUsageEvent = IFlexEventBase & {
1597
1879
  readonly type: 'model.usage';
1598
1880
  readonly runId?: string;
1599
1881
  readonly source: 'generation' | 'compaction';
1600
- } & Readonly<TFlexModelCallUsage>;
1882
+ } & Readonly<TFlexObservedModelCallUsage>;
1601
1883
 
1602
1884
  export interface IFlexPromptQueueEvent extends IFlexEventBase {
1603
1885
  readonly type: 'prompt.queued' | 'prompt.started' | 'prompt.running' | 'prompt.finished';
package/ts/plugins.ts CHANGED
@@ -6,6 +6,91 @@ import * as path from 'node:path';
6
6
 
7
7
  export { AsyncLocalStorage, crypto, fs, path };
8
8
 
9
+ // third party scope
10
+ import { asSchema, jsonSchema } from 'ai';
11
+ import { Validator as JsonSchemaValidator } from '@cfworker/json-schema';
12
+
13
+ export { asSchema, jsonSchema, JsonSchemaValidator };
14
+ export type { FlexibleSchema, JSONSchema7, ModelMessage, Schema } from 'ai';
15
+
16
+ // optional code mode peers: QuickJS-ng compiled to WebAssembly, and amaro to strip TypeScript types
17
+
18
+ /** A value inside a QuickJS context; the host disposes every handle it owns. */
19
+ export interface IQuickJsHandle {
20
+ readonly alive: boolean;
21
+ dispose(): void;
22
+ }
23
+
24
+ export type TQuickJsResult =
25
+ | { value: IQuickJsHandle; error?: undefined }
26
+ | { error: IQuickJsHandle; value?: undefined };
27
+
28
+ export interface IQuickJsDeferredPromise {
29
+ readonly handle: IQuickJsHandle;
30
+ readonly alive: boolean;
31
+ resolve(value?: IQuickJsHandle): void;
32
+ reject(value?: IQuickJsHandle): void;
33
+ dispose(): void;
34
+ }
35
+
36
+ export interface IQuickJsContext {
37
+ readonly global: IQuickJsHandle;
38
+ newString(value: string): IQuickJsHandle;
39
+ getString(handle: IQuickJsHandle): string;
40
+ newFunction(name: string, implementation: (...args: IQuickJsHandle[]) => IQuickJsHandle | void): IQuickJsHandle;
41
+ setProp(target: IQuickJsHandle, key: string, value: IQuickJsHandle): void;
42
+ newPromise(): IQuickJsDeferredPromise;
43
+ newError(message: string): IQuickJsHandle;
44
+ evalCode(code: string, filename?: string): TQuickJsResult;
45
+ resolvePromise(handle: IQuickJsHandle): Promise<TQuickJsResult>;
46
+ dump(handle: IQuickJsHandle): unknown;
47
+ dispose(): void;
48
+ }
49
+
50
+ export interface IQuickJsRuntime {
51
+ readonly alive: boolean;
52
+ setMemoryLimit(bytes: number): void;
53
+ setMaxStackSize(bytes: number): void;
54
+ setInterruptHandler(handler: () => boolean): void;
55
+ newContext(): IQuickJsContext;
56
+ /** Runs the queued promise jobs; `error` is an exception none of them caught. */
57
+ executePendingJobs(): { error?: IQuickJsHandle };
58
+ dispose(): void;
59
+ }
60
+
61
+ export interface IQuickJsModule {
62
+ newRuntime(): IQuickJsRuntime;
63
+ }
64
+
65
+ /** A failed strip, as amaro reports it. */
66
+ export interface IAmaroError {
67
+ code?: string;
68
+ message?: string;
69
+ startLine?: number;
70
+ }
71
+
72
+ export interface ICodeSandboxEngine {
73
+ /** A new WebAssembly instance of QuickJS-ng, with a memory of its own. */
74
+ newModule(): Promise<IQuickJsModule>;
75
+ /** Removes TypeScript type syntax, keeping every line where it was. */
76
+ stripTypes(source: string): string;
77
+ }
78
+
79
+ /** Loads QuickJS and amaro, the optional peers code mode needs. */
80
+ export const importCodeSandboxEngine = async (): Promise<ICodeSandboxEngine> => {
81
+ const [core, variant, amaro] = await Promise.all([
82
+ import('quickjs-emscripten-core'),
83
+ import('@jitl/quickjs-ng-wasmfile-release-sync'),
84
+ import('amaro'),
85
+ ]);
86
+ // The variant's typings resolve as CommonJS; at run time the default export is the variant.
87
+ const quickjsVariant = (variant as { default: unknown }).default as Parameters<typeof core.newQuickJSWASMModuleFromVariant>[0];
88
+ return {
89
+ newModule: async () => await core.newQuickJSWASMModuleFromVariant(quickjsVariant) as unknown as IQuickJsModule,
90
+ stripTypes: (source) => amaro.transformSync(source, { mode: 'strip-only' }).code,
91
+ };
92
+ };
93
+
9
94
  // @push.rocks scope
10
95
  import {
11
96
  AgentEventStoreConflictError,
@@ -73,6 +158,7 @@ export type {
73
158
  TAgentGenerationOutcome,
74
159
  TAgentModelCallUsage,
75
160
  TAgentModelCallUsageEvent,
161
+ TAgentObservedModelCallUsage,
76
162
  TAgentRetryReason,
77
163
  TAgentToolCallFinishEvent,
78
164
  TAgentToolExecutionReconciliationOptions,
package/ts/readme.md CHANGED
@@ -1026,7 +1026,7 @@ A `contextCompactor` states which model to compact with from the model the compa
1026
1026
 
1027
1027
  A compactor uses `resolvedModel.model` directly, and resolves `modelHint` the way its model resolver resolves a prompt's `modelHint`. Only when neither member is present, because nothing dispatched states a model, does the host choose the compaction model itself.
1028
1028
 
1029
- A `context-overflow` or `context-budget` compaction also carries `system` and `providerOptions`: the instructions and the provider options (cache options applied, so `store: false` on OpenAI) the run's turns are sent with. `compactMessages()` sends its summary call with them as one streamed call, the shape of every turn, so a compactor that passes its options through reaches the provider the way the run does. Without `system` it sends its own instructions.
1029
+ A `context-overflow` or `context-budget` compaction also carries `system` and `providerOptions`: the instructions and the provider options (cache options applied, so `store: false` on OpenAI) the run's turns are sent with. A `context-budget` compaction on a provider that caches the prefix also carries the turns' `tools`. `compactMessages()` sends its summary call with them as one streamed call, the shape of every turn, with the tools' definitions but none of their code, so a compactor that passes its options through repeats the run's prefix and reads it from the provider's prompt cache; definitions that outweigh the history nine times are left out. Without `system` it sends its own instructions.
1030
1030
 
1031
1031
  ```typescript
1032
1032
  agentSessionPolicy: {
@@ -1,5 +1,7 @@
1
1
  import { FlexHarnessValidationError } from './errors.js';
2
2
  import {
3
+ FLEX_CODE_MODE_DEFAULT_LIMITS,
4
+ FLEX_CODE_MODE_MAXIMUM_LIMITS,
3
5
  FLEX_REVERSION_DEFAULT_LIMITS,
4
6
  FLEX_REVERSION_MAXIMUM_LIMITS,
5
7
  } from './interfaces.js';
@@ -12,7 +14,9 @@ import type {
12
14
  IFlexReversionLimits,
13
15
  IFlexSlashCommandHandlerRegistration,
14
16
  IFlexSlashCommandTemplateRegistration,
17
+ IFlexCodeModeLimits,
15
18
  IFlexSubagentDefinition,
19
+ IFlexToolCatalogOptions,
16
20
  TFlexSlashCommandRegistration,
17
21
  } from './interfaces.js';
18
22
  import {
@@ -156,6 +160,57 @@ export function normalizeSubagents(
156
160
  return Object.freeze(normalized);
157
161
  }
158
162
 
163
+ export interface INormalizedToolCatalogOptions {
164
+ readonly declarations: 'typescript' | 'json-schema';
165
+ /** The limits of `run_code`; absent while code mode is off. */
166
+ readonly codeMode?: Readonly<IFlexCodeModeLimits>;
167
+ }
168
+
169
+ const isPlainOptions = (value: unknown): value is Record<string, unknown> =>
170
+ !!value
171
+ && typeof value === 'object'
172
+ && !Array.isArray(value)
173
+ && [Object.prototype, null].includes(Object.getPrototypeOf(value));
174
+
175
+ export function normalizeToolCatalogOptions(
176
+ options: IFlexToolCatalogOptions | undefined,
177
+ ): INormalizedToolCatalogOptions {
178
+ if (options === undefined) return Object.freeze({ declarations: 'typescript' });
179
+ if (!isPlainOptions(options)) throw new FlexHarnessValidationError('toolCatalog must be a plain object.');
180
+ const unsupported = Object.keys(options).find((key) => key !== 'declarations' && key !== 'codeMode');
181
+ if (unsupported) throw new FlexHarnessValidationError(`toolCatalog does not support "${unsupported}".`);
182
+ const declarations = options.declarations ?? 'typescript';
183
+ if (declarations !== 'typescript' && declarations !== 'json-schema') {
184
+ throw new FlexHarnessValidationError('toolCatalog.declarations must be "typescript" or "json-schema".');
185
+ }
186
+ const codeMode = normalizeCodeModeLimits(options.codeMode);
187
+ return Object.freeze({ declarations, ...(codeMode ? { codeMode } : {}) });
188
+ }
189
+
190
+ function normalizeCodeModeLimits(codeMode: unknown): Readonly<IFlexCodeModeLimits> | undefined {
191
+ if (codeMode === undefined || codeMode === false) return undefined;
192
+ if (codeMode === true) return FLEX_CODE_MODE_DEFAULT_LIMITS;
193
+ if (!isPlainOptions(codeMode)) {
194
+ throw new FlexHarnessValidationError('toolCatalog.codeMode must be a boolean or a plain object.');
195
+ }
196
+ const unsupported = Object.keys(codeMode).find((key) => key !== 'limits');
197
+ if (unsupported) throw new FlexHarnessValidationError(`toolCatalog.codeMode does not support "${unsupported}".`);
198
+ const limits = codeMode.limits ?? {};
199
+ if (!isPlainOptions(limits)) throw new FlexHarnessValidationError('toolCatalog.codeMode.limits must be a plain object.');
200
+ const resolved: Record<string, number> = { ...FLEX_CODE_MODE_DEFAULT_LIMITS };
201
+ for (const [key, value] of Object.entries(limits)) {
202
+ if (!Object.prototype.hasOwnProperty.call(FLEX_CODE_MODE_MAXIMUM_LIMITS, key)) {
203
+ throw new FlexHarnessValidationError(`toolCatalog.codeMode.limits does not support "${key}".`);
204
+ }
205
+ const maximum = FLEX_CODE_MODE_MAXIMUM_LIMITS[key as keyof IFlexCodeModeLimits];
206
+ if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 1 || value > maximum) {
207
+ throw new FlexHarnessValidationError(`toolCatalog.codeMode.limits.${key} must be an integer from 1 through ${maximum}.`);
208
+ }
209
+ resolved[key] = value;
210
+ }
211
+ return Object.freeze(resolved as unknown as IFlexCodeModeLimits);
212
+ }
213
+
159
214
  export function normalizeBuiltInTools(
160
215
  options: IFlexHarnessOptions<unknown>['builtInTools'],
161
216
  ): INormalizedBuiltInTools {
package/ts/utils.json.ts CHANGED
@@ -785,6 +785,7 @@ function validateMessage(value: unknown, path: string): IValidatedMessageIdentit
785
785
  'output',
786
786
  'error',
787
787
  'childSessionId',
788
+ 'parentToolCallId',
788
789
  'model',
789
790
  'startedAt',
790
791
  'completedAt',
@@ -801,6 +802,7 @@ function validateMessage(value: unknown, path: string): IValidatedMessageIdentit
801
802
  }
802
803
  requireOptionalString(part.error, `${partPath}.error`);
803
804
  requireOptionalString(part.childSessionId, `${partPath}.childSessionId`);
805
+ requireOptionalString(part.parentToolCallId, `${partPath}.parentToolCallId`);
804
806
  if (part.model !== undefined) validateModel(part.model, `${partPath}.model`);
805
807
  requireOptionalString(part.startedAt, `${partPath}.startedAt`);
806
808
  requireOptionalString(part.completedAt, `${partPath}.completedAt`);