@modelprofile.com/flexharness 10.13.0 → 10.15.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.
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;
@@ -683,6 +687,9 @@ export const FLEX_TOOL_CATALOG_LIMITS = Object.freeze({
683
687
  maxDeclarationBytes: 16 * 1024,
684
688
  maxTags: 16,
685
689
  maxTagCharacters: 48,
690
+ maxAliases: 16,
691
+ maxAliasCharacters: 64,
692
+ maxVocabularyWords: 1024,
686
693
  maxExamples: 4,
687
694
  maxExampleBytes: 1024,
688
695
  maxSchemaBytes: 64 * 1024,
@@ -693,8 +700,16 @@ export const FLEX_TOOL_CATALOG_LIMITS = Object.freeze({
693
700
  maxDescribeTypes: 32,
694
701
  maxDescribeBytes: 64 * 1024,
695
702
  maxParamIssues: 10,
703
+ maxOutputTypeDepth: 32,
696
704
  });
697
705
 
706
+ /**
707
+ * How many levels of named types a TypeScript declaration declares for a method's result by
708
+ * default: none. The result names its types, and `describe_tools({ types })` declares them on
709
+ * request, as a model needs them only to read fields of a result it has not seen.
710
+ */
711
+ export const FLEX_TOOL_CATALOG_DEFAULT_OUTPUT_TYPE_DEPTH = 0;
712
+
698
713
  export interface IFlexCatalogSearchInput {
699
714
  /** Words to rank methods by. */
700
715
  query?: string;
@@ -743,6 +758,11 @@ export type TFlexCatalogCallError =
743
758
  more?: number;
744
759
  /** The lookup that returns the method's declaration. */
745
760
  see: string;
761
+ }
762
+ | {
763
+ error: 'denied';
764
+ method: string;
765
+ reason: string;
746
766
  };
747
767
 
748
768
  export interface IFlexCatalogToolDefinition {
@@ -761,7 +781,10 @@ export type TFlexCatalogSchema = { readonly [keyword: string]: unknown } | plugi
761
781
  export interface IFlexCatalogRunContext<TScope = unknown> {
762
782
  /** The dotted path the model called. */
763
783
  readonly method: string;
764
- /** The `execute` call's ID; pass it with permission requests. */
784
+ /**
785
+ * The call's ID, which its tool part and permission requests carry: the `execute` call's, or for
786
+ * a call inside `run_code` the inner call's, `<run_code call ID>.<n>`.
787
+ */
765
788
  readonly toolCallId: string;
766
789
  readonly abortSignal: AbortSignal;
767
790
  /** The run's tool-provider context: scope, session, run and `requestPermission`. */
@@ -776,10 +799,16 @@ export interface IFlexCatalogMethod<TScope = unknown, TParams = any, TResult = u
776
799
  method: string;
777
800
  /** One line of at most 120 characters, shown in search results. */
778
801
  summary: string;
779
- /** The full description, shown with the declaration. */
802
+ /** The full description, shown with the declaration. An empty or blank one counts as absent. */
780
803
  description?: string;
781
804
  /** More words `search_tools` matches. */
782
805
  tags?: readonly string[];
806
+ /**
807
+ * Other names of the method, such as the one its users' language or a former API gives it:
808
+ * `search_tools` weighs them like the method's name, and ranks the method first for a query that
809
+ * names one.
810
+ */
811
+ aliases?: readonly string[];
783
812
  /** Code shown as `@example` in the declaration. */
784
813
  examples?: readonly string[];
785
814
  /** The parameter object's schema; `execute` validates every call against it. */
@@ -797,22 +826,72 @@ export interface IFlexCatalogMethod<TScope = unknown, TParams = any, TResult = u
797
826
  params: TParams,
798
827
  context: IFlexCatalogRunContext<TScope>,
799
828
  ): TResult | PromiseLike<TResult> | AsyncIterable<TResult>;
829
+ /**
830
+ * Decides each validated call before it runs: `run` (the default without this hook), `propose`
831
+ * to hand it to the catalog's `onProposals` instead of running it, or `{ deny }` to refuse it.
832
+ */
833
+ onCall?(
834
+ params: TParams,
835
+ context: IFlexCatalogRunContext<TScope>,
836
+ ): TFlexCatalogCallDecision | PromiseLike<TFlexCatalogCallDecision>;
837
+ }
838
+
839
+ export type TFlexCatalogCallDecision = 'run' | 'propose' | { deny: string };
840
+
841
+ /** A call a method's `onCall` decided to propose instead of running. */
842
+ export interface IFlexCatalogProposal {
843
+ /** The call's ID: the `execute` call's, or the inner call's inside `run_code`. */
844
+ id: string;
845
+ method: string;
846
+ params: unknown;
847
+ }
848
+
849
+ export interface IFlexCatalogProposalContext<TScope = unknown> {
850
+ /** The `execute` or `run_code` call that made the proposals. */
851
+ readonly toolCallId: string;
852
+ readonly abortSignal: AbortSignal;
853
+ readonly provider: Readonly<IFlexToolProviderContext<TScope>>;
800
854
  }
801
855
 
802
856
  /** A named type that method declarations refer to; it needs a `declaration`, a `schema`, or both. */
803
857
  export interface IFlexCatalogType {
804
- /** A JavaScript identifier such as `IMoney`. */
858
+ /**
859
+ * The type's name, such as `IMoney`, which `$ref`s to `#/$defs/<name>` use. A name that is no
860
+ * JavaScript identifier, such as `IEvent_order.created` as schema generators name instances of
861
+ * generics, is declared under an identifier made of it: each other character becomes `_`, and a
862
+ * reserved word or a leading digit is escaped with `_` (`IEvent_order_created`). Its declaration
863
+ * names the original, and `describe_tools` takes either.
864
+ */
805
865
  name: string;
866
+ /** An empty or blank description counts as absent. */
806
867
  description?: string;
807
868
  /** Validates parameters that refer to this type, and generates its declaration. */
808
869
  schema?: TFlexCatalogSchema;
809
- /** Declaration text that replaces the generated one, such as `interface IMoney { … }`. */
870
+ /**
871
+ * Declaration text that replaces the generated one, such as `interface IMoney { … }`. It declares
872
+ * the identifier the type is declared under.
873
+ */
810
874
  declaration?: string;
811
875
  }
812
876
 
813
877
  export interface IFlexToolCatalog<TScope = unknown> {
814
878
  methods: readonly IFlexCatalogMethod<TScope>[];
815
879
  types?: readonly IFlexCatalogType[];
880
+ /**
881
+ * The words the catalog's users say for its own words, by the catalog's word:
882
+ * `{ voucher: ['Beleg', 'Belege'], 'gross amount': ['Bruttobetrag'] }`. A `search_tools` query that
883
+ * says every word of an alias searches for the catalog's words as well.
884
+ */
885
+ vocabulary?: Readonly<Record<string, readonly string[]>>;
886
+ /**
887
+ * Receives the calls `onCall` proposed, as one batch: the proposed call of an `execute`, or every
888
+ * call a `run_code` program proposed, once the program finished. A failed program proposes
889
+ * nothing. What it returns is what the model sees for them.
890
+ */
891
+ onProposals?(
892
+ proposals: readonly IFlexCatalogProposal[],
893
+ context: IFlexCatalogProposalContext<TScope>,
894
+ ): unknown;
816
895
  }
817
896
 
818
897
  export interface IFlexToolCatalogOptions {
@@ -821,8 +900,93 @@ export interface IFlexToolCatalogOptions {
821
900
  * across the session, or `json-schema` with each method's self-contained JSON Schema.
822
901
  */
823
902
  declarations?: 'typescript' | 'json-schema';
903
+ /**
904
+ * How many levels of named types a TypeScript declaration declares for a method's result:
905
+ * `0` (the default) none, `1` the types the result names, each further level the types those
906
+ * name. A parameter's types are always declared in full, since a call has to match them. The
907
+ * declaration lists the types it names but does not declare; `describe_tools({ types })`
908
+ * declares them on request.
909
+ */
910
+ outputTypeDepth?: number;
911
+ /**
912
+ * `search_tools` follows its results with the declaration of the top match when one match
913
+ * clearly leads, so the model can call it without a `describe_tools` step: the only match, or one
914
+ * whose name or alias the query names that ranks 1.25 times higher than the next. Default `true`.
915
+ */
916
+ describeTopMatch?: boolean;
917
+ /**
918
+ * Adds `run_code`, which runs a short TypeScript or JavaScript program against the catalog in a
919
+ * QuickJS sandbox. Needs the optional peers `quickjs-emscripten-core`,
920
+ * `@jitl/quickjs-ng-wasmfile-release-sync` and `amaro`.
921
+ */
922
+ codeMode?: boolean | IFlexCodeModeOptions;
923
+ }
924
+
925
+ export interface IFlexCodeModeOptions {
926
+ limits?: Partial<IFlexCodeModeLimits>;
824
927
  }
825
928
 
929
+ /** The bounds of one `run_code` program. */
930
+ export interface IFlexCodeModeLimits {
931
+ /** UTF-8 bytes of the program. */
932
+ maxSourceBytes: number;
933
+ /** Milliseconds the program may compute; time waiting for calls does not count. */
934
+ cpuMs: number;
935
+ /** Milliseconds the program may take in all, not counting waits for a permission decision. */
936
+ wallMs: number;
937
+ /** Bytes of the sandbox's heap. */
938
+ memoryBytes: number;
939
+ /**
940
+ * Bytes of the sandbox's stack. QuickJS runs on the host's stack as well, so a larger value lets
941
+ * deep recursion reach the host's limit first; the program then fails at its `stack` limit all
942
+ * the same.
943
+ */
944
+ stackBytes: number;
945
+ /** Catalog calls per program. */
946
+ maxCalls: number;
947
+ /** Catalog calls running at once; further calls wait for a slot. */
948
+ maxConcurrentCalls: number;
949
+ /** UTF-8 bytes of console output kept; more is cut off. */
950
+ maxLogBytes: number;
951
+ }
952
+
953
+ export const FLEX_CODE_MODE_TOOL_NAME = 'run_code';
954
+
955
+ export const FLEX_CODE_MODE_DEFAULT_LIMITS: Readonly<IFlexCodeModeLimits> = Object.freeze({
956
+ maxSourceBytes: 32 * 1024,
957
+ cpuMs: 5_000,
958
+ wallMs: 60_000,
959
+ memoryBytes: 64 * 1024 * 1024,
960
+ stackBytes: 64 * 1024,
961
+ maxCalls: 200,
962
+ maxConcurrentCalls: 8,
963
+ maxLogBytes: 16 * 1024,
964
+ });
965
+
966
+ export const FLEX_CODE_MODE_MAXIMUM_LIMITS: Readonly<IFlexCodeModeLimits> = Object.freeze({
967
+ maxSourceBytes: 256 * 1024,
968
+ cpuMs: 60_000,
969
+ wallMs: 30 * 60_000,
970
+ memoryBytes: 512 * 1024 * 1024,
971
+ stackBytes: 256 * 1024,
972
+ maxCalls: 10_000,
973
+ maxConcurrentCalls: 64,
974
+ maxLogBytes: 256 * 1024,
975
+ });
976
+
977
+ /** What `run_code` reports to the model, as JSON, when a program does not finish. */
978
+ export interface IFlexCodeRunFailure {
979
+ error: 'syntax_error' | 'program_error' | 'limit_exceeded' | 'aborted';
980
+ /** The limit that stopped the program, for `limit_exceeded`. */
981
+ limit?: 'source' | 'cpu' | 'wall' | 'memory' | 'stack' | 'calls';
982
+ name?: string;
983
+ message: string;
984
+ /** The program's line, when known. */
985
+ line?: number;
986
+ /** Catalog calls the program made. */
987
+ calls: number;
988
+ logs?: string;
989
+ }
826
990
  export interface IFlexResourceToolProviderResolverContext<TScope> extends IFlexSessionGeneration {
827
991
  scopeId: string;
828
992
  scope: TScope;
@@ -1762,7 +1926,7 @@ export type TFlexModelUsageEvent = IFlexEventBase & {
1762
1926
  readonly type: 'model.usage';
1763
1927
  readonly runId?: string;
1764
1928
  readonly source: 'generation' | 'compaction';
1765
- } & Readonly<TFlexModelCallUsage>;
1929
+ } & Readonly<TFlexObservedModelCallUsage>;
1766
1930
 
1767
1931
  export interface IFlexPromptQueueEvent extends IFlexEventBase {
1768
1932
  readonly type: 'prompt.queued' | 'prompt.started' | 'prompt.running' | 'prompt.finished';
package/ts/plugins.ts CHANGED
@@ -13,6 +13,84 @@ import { Validator as JsonSchemaValidator } from '@cfworker/json-schema';
13
13
  export { asSchema, jsonSchema, JsonSchemaValidator };
14
14
  export type { FlexibleSchema, JSONSchema7, ModelMessage, Schema } from 'ai';
15
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
+
16
94
  // @push.rocks scope
17
95
  import {
18
96
  AgentEventStoreConflictError,
@@ -80,6 +158,7 @@ export type {
80
158
  TAgentGenerationOutcome,
81
159
  TAgentModelCallUsage,
82
160
  TAgentModelCallUsageEvent,
161
+ TAgentObservedModelCallUsage,
83
162
  TAgentRetryReason,
84
163
  TAgentToolCallFinishEvent,
85
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,6 +1,10 @@
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,
6
+ FLEX_TOOL_CATALOG_DEFAULT_OUTPUT_TYPE_DEPTH,
7
+ FLEX_TOOL_CATALOG_LIMITS,
4
8
  FLEX_REVERSION_MAXIMUM_LIMITS,
5
9
  } from './interfaces.js';
6
10
  import type {
@@ -12,6 +16,7 @@ import type {
12
16
  IFlexReversionLimits,
13
17
  IFlexSlashCommandHandlerRegistration,
14
18
  IFlexSlashCommandTemplateRegistration,
19
+ IFlexCodeModeLimits,
15
20
  IFlexSubagentDefinition,
16
21
  IFlexToolCatalogOptions,
17
22
  TFlexSlashCommandRegistration,
@@ -157,25 +162,76 @@ export function normalizeSubagents(
157
162
  return Object.freeze(normalized);
158
163
  }
159
164
 
165
+ export interface INormalizedToolCatalogOptions {
166
+ readonly declarations: 'typescript' | 'json-schema';
167
+ readonly outputTypeDepth: number;
168
+ readonly describeTopMatch: boolean;
169
+ /** The limits of `run_code`; absent while code mode is off. */
170
+ readonly codeMode?: Readonly<IFlexCodeModeLimits>;
171
+ }
172
+
173
+ const isPlainOptions = (value: unknown): value is Record<string, unknown> =>
174
+ !!value
175
+ && typeof value === 'object'
176
+ && !Array.isArray(value)
177
+ && [Object.prototype, null].includes(Object.getPrototypeOf(value));
178
+
160
179
  export function normalizeToolCatalogOptions(
161
- options: IFlexHarnessOptions<unknown>['toolCatalog'],
162
- ): Readonly<Required<IFlexToolCatalogOptions>> {
163
- if (options === undefined) return Object.freeze({ declarations: 'typescript' });
164
- if (
165
- !options
166
- || typeof options !== 'object'
167
- || Array.isArray(options)
168
- || ![Object.prototype, null].includes(Object.getPrototypeOf(options))
169
- ) {
170
- throw new FlexHarnessValidationError('toolCatalog must be a plain object.');
180
+ options: IFlexToolCatalogOptions | undefined,
181
+ ): INormalizedToolCatalogOptions {
182
+ if (options === undefined) {
183
+ return Object.freeze({
184
+ declarations: 'typescript',
185
+ outputTypeDepth: FLEX_TOOL_CATALOG_DEFAULT_OUTPUT_TYPE_DEPTH,
186
+ describeTopMatch: true,
187
+ });
171
188
  }
172
- const unsupported = Object.keys(options).find((key) => key !== 'declarations');
189
+ if (!isPlainOptions(options)) throw new FlexHarnessValidationError('toolCatalog must be a plain object.');
190
+ const unsupported = Object.keys(options).find(
191
+ (key) => !['declarations', 'outputTypeDepth', 'describeTopMatch', 'codeMode'].includes(key),
192
+ );
173
193
  if (unsupported) throw new FlexHarnessValidationError(`toolCatalog does not support "${unsupported}".`);
174
194
  const declarations = options.declarations ?? 'typescript';
175
195
  if (declarations !== 'typescript' && declarations !== 'json-schema') {
176
196
  throw new FlexHarnessValidationError('toolCatalog.declarations must be "typescript" or "json-schema".');
177
197
  }
178
- return Object.freeze({ declarations });
198
+ const outputTypeDepth: unknown = options.outputTypeDepth ?? FLEX_TOOL_CATALOG_DEFAULT_OUTPUT_TYPE_DEPTH;
199
+ if (
200
+ typeof outputTypeDepth !== 'number' || !Number.isSafeInteger(outputTypeDepth)
201
+ || outputTypeDepth < 0 || outputTypeDepth > FLEX_TOOL_CATALOG_LIMITS.maxOutputTypeDepth
202
+ ) {
203
+ throw new FlexHarnessValidationError(
204
+ `toolCatalog.outputTypeDepth must be an integer from 0 through ${FLEX_TOOL_CATALOG_LIMITS.maxOutputTypeDepth}.`,
205
+ );
206
+ }
207
+ const describeTopMatch = options.describeTopMatch ?? true;
208
+ if (typeof describeTopMatch !== 'boolean') throw new FlexHarnessValidationError('toolCatalog.describeTopMatch must be a boolean.');
209
+ const codeMode = normalizeCodeModeLimits(options.codeMode);
210
+ return Object.freeze({ declarations, outputTypeDepth, describeTopMatch, ...(codeMode ? { codeMode } : {}) });
211
+ }
212
+
213
+ function normalizeCodeModeLimits(codeMode: unknown): Readonly<IFlexCodeModeLimits> | undefined {
214
+ if (codeMode === undefined || codeMode === false) return undefined;
215
+ if (codeMode === true) return FLEX_CODE_MODE_DEFAULT_LIMITS;
216
+ if (!isPlainOptions(codeMode)) {
217
+ throw new FlexHarnessValidationError('toolCatalog.codeMode must be a boolean or a plain object.');
218
+ }
219
+ const unsupported = Object.keys(codeMode).find((key) => key !== 'limits');
220
+ if (unsupported) throw new FlexHarnessValidationError(`toolCatalog.codeMode does not support "${unsupported}".`);
221
+ const limits = codeMode.limits ?? {};
222
+ if (!isPlainOptions(limits)) throw new FlexHarnessValidationError('toolCatalog.codeMode.limits must be a plain object.');
223
+ const resolved: Record<string, number> = { ...FLEX_CODE_MODE_DEFAULT_LIMITS };
224
+ for (const [key, value] of Object.entries(limits)) {
225
+ if (!Object.prototype.hasOwnProperty.call(FLEX_CODE_MODE_MAXIMUM_LIMITS, key)) {
226
+ throw new FlexHarnessValidationError(`toolCatalog.codeMode.limits does not support "${key}".`);
227
+ }
228
+ const maximum = FLEX_CODE_MODE_MAXIMUM_LIMITS[key as keyof IFlexCodeModeLimits];
229
+ if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 1 || value > maximum) {
230
+ throw new FlexHarnessValidationError(`toolCatalog.codeMode.limits.${key} must be an integer from 1 through ${maximum}.`);
231
+ }
232
+ resolved[key] = value;
233
+ }
234
+ return Object.freeze(resolved as unknown as IFlexCodeModeLimits);
179
235
  }
180
236
 
181
237
  export function normalizeBuiltInTools(
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`);
@@ -25,6 +25,15 @@ async function* normalizeAsyncIterable(
25
25
  }
26
26
  }
27
27
 
28
+ /** A tool output's final value: the value itself, or the last value an async iterable yields. */
29
+ export async function settleToolOutput(output: unknown): Promise<unknown> {
30
+ const value = await output;
31
+ if (!isAsyncIterable(value)) return value;
32
+ let last: unknown;
33
+ for await (const item of value) last = item;
34
+ return last;
35
+ }
36
+
28
37
  export function wrapToolSet(
29
38
  tools: TFlexAgentToolSet,
30
39
  limits: Required<NonNullable<IFlexHarnessOptions<unknown>['toolOutputLimits']>>,
@@ -1,8 +1,8 @@
1
- import { streamText, wrapLanguageModel } from 'ai';
1
+ import { asSchema, streamText, wrapLanguageModel } from 'ai';
2
2
 
3
- export { streamText, wrapLanguageModel };
3
+ export { asSchema, streamText, wrapLanguageModel };
4
4
 
5
- export type { ModelMessage } from 'ai';
5
+ export type { ModelMessage, ToolSet } from 'ai';
6
6
  export type { TSmartAiLanguageModel } from '@modelprofile.com/flexharness-models';
7
7
 
8
8
  import { AgentModelCallUsageRecorder } from '@modelprofile.com/flexharness-agent';