@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
@@ -28,8 +28,16 @@ import {
28
28
  FLEX_REVERSION_REFERENCE_MAX_BYTES,
29
29
  FLEX_REVERSION_WORKSPACE_ID_MAX_BYTES,
30
30
  FLEX_REVERSION_WORKSPACE_LABEL_MAX_BYTES,
31
+ FLEX_CODE_MODE_TOOL_NAME,
31
32
  FLEX_SESSION_IMPORT_MAX_DEPTH,
33
+ FLEX_TOOL_CATALOG_TOOL_NAMES,
32
34
  } from './interfaces.js';
35
+ import {
36
+ FlexToolCatalog,
37
+ assertNoCatalogCollisions,
38
+ catalogToolInputSchema,
39
+ } from './classes.toolcatalog.js';
40
+ import { FlexCodeSandbox, type IFlexCodeHost } from './classes.codesandbox.js';
33
41
  import type {
34
42
  IFlexAgentContextCompactionOptions,
35
43
  IFlexAgentContextInvocation,
@@ -112,6 +120,10 @@ import type {
112
120
  IFlexSubagentDefinition,
113
121
  IFlexSubagentProvenance,
114
122
  IFlexTerminalProjection,
123
+ IFlexCatalogDescribeInput,
124
+ IFlexCatalogProposal,
125
+ IFlexCatalogSearchInput,
126
+ IFlexToolCatalogOptions,
115
127
  IFlexToolHandle,
116
128
  IFlexToolMessagePart,
117
129
  IFlexToolProviderContext,
@@ -168,6 +180,7 @@ import {
168
180
  normalizeAgentSessionPolicy,
169
181
  normalizeBuiltInTools,
170
182
  normalizeSlashCommands,
183
+ normalizeToolCatalogOptions,
171
184
  normalizeSubagents,
172
185
  requireHarnessStores,
173
186
  resolveBoundedPositiveInteger,
@@ -212,7 +225,7 @@ import {
212
225
  validateSessionImportHandle,
213
226
  type INormalizedSessionImportOptions,
214
227
  } from './utils.sessionimport.js';
215
- import { wrapToolSet } from './utils.tooloutput.js';
228
+ import { settleToolOutput, wrapToolSet } from './utils.tooloutput.js';
216
229
  import {
217
230
  validateCompactSessionOptions,
218
231
  validateCreateProjectTaskInput,
@@ -431,6 +444,12 @@ interface IActiveRun {
431
444
  reservedUserMessage?: IFlexMessage;
432
445
  reservedAssistantMessage?: IFlexMessage;
433
446
  modelResolution?: IFlexResolvedModel;
447
+ /** The tool-provider context of the run, passed to catalog methods. */
448
+ toolProviderContext?: Readonly<IFlexToolProviderContext<unknown>>;
449
+ /** The catalog methods the run offers through search_tools, describe_tools and execute. */
450
+ toolCatalog?: FlexToolCatalog<unknown>;
451
+ /** The sandbox `run_code` programs run in, loaded when the catalog has code mode. */
452
+ codeSandbox?: FlexCodeSandbox;
434
453
  subagents: ReadonlyMap<string, Readonly<IFlexSubagentDefinition>>;
435
454
  allowedTools?: readonly string[];
436
455
  agentAllowedTools?: readonly string[];
@@ -851,6 +870,7 @@ export class FlexHarness<TScope = unknown> {
851
870
  private readonly externalErrorProjector?: TFlexExternalErrorProjector;
852
871
  private readonly subagents: ReadonlyMap<string, Readonly<IFlexSubagentDefinition>>;
853
872
  private readonly builtInTools: INormalizedBuiltInTools;
873
+ private readonly toolCatalogOptions: Readonly<IFlexToolCatalogOptions>;
854
874
  private readonly slashCommands: ReadonlyMap<string, TRegisteredSlashCommand<TScope>>;
855
875
  private readonly maxSubagentDepth: number;
856
876
  private readonly maxSubagentCallsPerRun: number;
@@ -937,6 +957,11 @@ export class FlexHarness<TScope = unknown> {
937
957
  this.externalErrorProjector = options.externalErrorProjector;
938
958
  this.subagents = normalizeSubagents(options.subagents);
939
959
  this.builtInTools = normalizeBuiltInTools(options.builtInTools);
960
+ const toolCatalogOptions = normalizeToolCatalogOptions(options.toolCatalog);
961
+ this.toolCatalogOptions = Object.freeze({
962
+ declarations: toolCatalogOptions.declarations,
963
+ ...(toolCatalogOptions.codeMode ? { codeMode: { limits: toolCatalogOptions.codeMode } } : {}),
964
+ });
940
965
  this.slashCommands = normalizeSlashCommands(options.slashCommands);
941
966
  this.maxSubagentDepth = resolveBoundedPositiveInteger(
942
967
  options.maxSubagentDepth,
@@ -5823,18 +5848,38 @@ export class FlexHarness<TScope = unknown> {
5823
5848
  }
5824
5849
  signal.throwIfAborted();
5825
5850
  run.modelResolution = model!;
5851
+ run.toolProviderContext = toolProviderContext as Readonly<IFlexToolProviderContext<unknown>>;
5852
+ let catalog: FlexToolCatalog<unknown> | undefined;
5853
+ if (toolHandle?.catalog) {
5854
+ try {
5855
+ catalog = await FlexToolCatalog.create(toolHandle.catalog, this.toolCatalogOptions);
5856
+ if (catalog.codeMode) run.codeSandbox = await FlexCodeSandbox.load();
5857
+ } catch (error) {
5858
+ throw this.projectExternalError(run, error, 'toolProvider');
5859
+ }
5860
+ signal.throwIfAborted();
5861
+ }
5826
5862
  let tools: TFlexAgentToolSet | undefined;
5827
5863
  try {
5828
5864
  const providedTools = toolHandle?.tools;
5829
5865
  const combinedTools: Record<string, unknown> = { ...(providedTools ?? {}) };
5830
5866
  Object.assign(combinedTools, this.createBuiltInTools(run));
5867
+ if (catalog) assertNoCatalogCollisions(catalog, combinedTools);
5831
5868
  const globalNames = run.allowedTools === undefined ? undefined : new Set(run.allowedTools);
5832
5869
  const agentNames = run.agentAllowedTools === undefined ? undefined : new Set(run.agentAllowedTools);
5870
+ const admitted = (name: string): boolean =>
5871
+ (globalNames === undefined || globalNames.has(name))
5872
+ && (agentNames === undefined || agentNames.has(name));
5833
5873
  const admittedTools = globalNames === undefined && agentNames === undefined
5834
5874
  ? combinedTools
5835
- : Object.fromEntries(Object.entries(combinedTools).filter(([name]) =>
5836
- (globalNames === undefined || globalNames.has(name))
5837
- && (agentNames === undefined || agentNames.has(name))));
5875
+ : Object.fromEntries(Object.entries(combinedTools).filter(([name]) => admitted(name)));
5876
+ const admittedCatalog = catalog && (globalNames === undefined && agentNames === undefined
5877
+ ? catalog
5878
+ : catalog.restrict(catalog.methods.filter(admitted)));
5879
+ if (admittedCatalog && admittedCatalog.methods.length > 0) {
5880
+ run.toolCatalog = admittedCatalog;
5881
+ Object.assign(admittedTools, this.createCatalogTools(run, admittedCatalog));
5882
+ }
5838
5883
  tools = Object.keys(admittedTools).length > 0
5839
5884
  ? wrapToolSet(
5840
5885
  admittedTools as TFlexAgentToolSet,
@@ -5913,6 +5958,180 @@ export class FlexHarness<TScope = unknown> {
5913
5958
  }
5914
5959
  }
5915
5960
 
5961
+ /**
5962
+ * search_tools, describe_tools and execute. `execute` validates the call and runs the method
5963
+ * inside this tool call, so the tool wrapper bounds its output and projects its errors, and the
5964
+ * call's part, events and permission requests carry the method's path.
5965
+ */
5966
+ private createCatalogTools(run: IActiveRun, catalog: FlexToolCatalog<unknown>): Record<string, unknown> {
5967
+ const names = FLEX_TOOL_CATALOG_TOOL_NAMES;
5968
+ const definitions = catalog.toolDefinitions();
5969
+ const describeBytes = Math.max(1024, this.toolOutputLimits.maxBytes - 1024);
5970
+ return {
5971
+ [names.search]: plugins.tool({
5972
+ description: definitions[names.search].description,
5973
+ strict: false,
5974
+ inputSchema: catalogToolInputSchema<IFlexCatalogSearchInput>(definitions[names.search]),
5975
+ execute: async (input: IFlexCatalogSearchInput) => catalog.search(input),
5976
+ }),
5977
+ [names.describe]: plugins.tool({
5978
+ description: definitions[names.describe].description,
5979
+ strict: false,
5980
+ inputSchema: catalogToolInputSchema<IFlexCatalogDescribeInput>(definitions[names.describe]),
5981
+ execute: async (input: IFlexCatalogDescribeInput, options?: { messages?: plugins.ModelMessage[] }) =>
5982
+ catalog.describe(input, {
5983
+ declaredTypes: catalog.declaredTypesIn(options?.messages),
5984
+ maxBytes: describeBytes,
5985
+ }).output,
5986
+ }),
5987
+ [names.execute]: plugins.tool({
5988
+ description: definitions[names.execute].description,
5989
+ strict: false,
5990
+ inputSchema: catalogToolInputSchema<{ method: string; params?: Record<string, unknown> }>(
5991
+ definitions[names.execute],
5992
+ ),
5993
+ execute: async (
5994
+ input: { method: string; params?: Record<string, unknown> },
5995
+ options?: { toolCallId?: string; abortSignal?: AbortSignal },
5996
+ ) => {
5997
+ const toolCallId = options?.toolCallId ?? '';
5998
+ const abortSignal = options?.abortSignal ?? run.controller.signal;
5999
+ const call = await catalog.prepareCall(input.method, input.params);
6000
+ if (!call.success) throw this.catalogCallError(run, toolCallId, call.error);
6001
+ const context = { method: call.method, toolCallId, abortSignal, provider: run.toolProviderContext! };
6002
+ const decision = await call.decide(context);
6003
+ if (decision === 'run') return call.run(context);
6004
+ if (decision === 'propose') {
6005
+ return catalog.propose(
6006
+ [{ id: toolCallId, method: call.method, params: call.params }],
6007
+ { toolCallId, abortSignal, provider: run.toolProviderContext! },
6008
+ );
6009
+ }
6010
+ throw this.catalogCallError(run, toolCallId, { error: 'denied', method: call.method, reason: decision.deny });
6011
+ },
6012
+ }),
6013
+ ...(catalog.codeMode && run.codeSandbox ? {
6014
+ [FLEX_CODE_MODE_TOOL_NAME]: plugins.tool({
6015
+ description: definitions[FLEX_CODE_MODE_TOOL_NAME].description,
6016
+ strict: false,
6017
+ inputSchema: catalogToolInputSchema<{ code: string }>(definitions[FLEX_CODE_MODE_TOOL_NAME]),
6018
+ execute: async (input: { code: string }, options?: { toolCallId?: string; abortSignal?: AbortSignal }) =>
6019
+ this.runCatalogProgram(run, catalog, input.code, options?.toolCallId ?? '', options?.abortSignal ?? run.controller.signal),
6020
+ }),
6021
+ } : {}),
6022
+ };
6023
+ }
6024
+
6025
+ /** A refused catalog call as the trusted error the model and the tool part show. */
6026
+ private catalogCallError(run: IActiveRun, toolCallId: string, payload: unknown): Error {
6027
+ const error = this.trustInternalError(new FlexHarnessValidationError(JSON.stringify(payload)));
6028
+ if (toolCallId) run.trustedToolExecutionErrors.set(toolCallId, error);
6029
+ return error;
6030
+ }
6031
+
6032
+ /**
6033
+ * Runs a `run_code` program. Every catalog call it makes is dispatched like an `execute`
6034
+ * (validation, `onCall`, the method's run with its permission requests, the output limits, error
6035
+ * projection) and recorded as a tool part of its own, named by the method, with the `run_code`
6036
+ * call as its parent. Proposals go to `onProposals` as one batch once the program finished; a
6037
+ * program that fails proposes nothing.
6038
+ */
6039
+ private async runCatalogProgram(
6040
+ run: IActiveRun,
6041
+ catalog: FlexToolCatalog<unknown>,
6042
+ code: string,
6043
+ toolCallId: string,
6044
+ abortSignal: AbortSignal,
6045
+ ): Promise<unknown> {
6046
+ const proposals: IFlexCatalogProposal[] = [];
6047
+ const base = run.toolProviderContext!;
6048
+ // Waiting for a permission decision is the user's time, not the program's.
6049
+ let waitedMs = 0;
6050
+ let waiting = 0;
6051
+ let waitingSince = 0;
6052
+ /** The provider context of one call: its permission waits are timed, and end when the program stops. */
6053
+ const providerFor = (signal: AbortSignal): Readonly<IFlexToolProviderContext<unknown>> => Object.freeze({
6054
+ ...base,
6055
+ requestPermission: async (request: IFlexPermissionRequestInput) => {
6056
+ signal.throwIfAborted();
6057
+ if (waiting++ === 0) waitingSince = performance.now();
6058
+ let onAbort: (() => void) | undefined;
6059
+ try {
6060
+ await Promise.race([
6061
+ base.requestPermission(request),
6062
+ new Promise<never>((_resolve, reject) => {
6063
+ onAbort = () => reject(signal.reason);
6064
+ signal.addEventListener('abort', onAbort, { once: true });
6065
+ }),
6066
+ ]);
6067
+ } finally {
6068
+ if (onAbort) signal.removeEventListener('abort', onAbort);
6069
+ if (--waiting === 0) waitedMs += performance.now() - waitingSince;
6070
+ }
6071
+ },
6072
+ });
6073
+ const stopped = (method: string) => ({ ok: false as const, error: { error: 'aborted', method } });
6074
+ let callNumber = 0;
6075
+ const finish = (id: string, method: string, input: unknown, outcome: { output: unknown } | { error: Error }) => {
6076
+ if ('error' in outcome) run.trustedToolExecutionErrors.set(id, outcome.error);
6077
+ this.onToolFinish(run.state, run.sessionId, 'error' in outcome
6078
+ ? { toolCallId: id, toolName: method, input, success: false, error: outcome.error.message }
6079
+ : { toolCallId: id, toolName: method, input, success: true, output: outcome.output });
6080
+ };
6081
+ const host: IFlexCodeHost = {
6082
+ waitedMs: () => waitedMs + (waiting > 0 ? performance.now() - waitingSince : 0),
6083
+ call: async (method, params, signal) => {
6084
+ if (signal.aborted) return stopped(method);
6085
+ const id = `${toolCallId}.${++callNumber}`;
6086
+ const call = await catalog.prepareCall(method, params);
6087
+ if (!call.success) {
6088
+ if (catalog.has(method)) {
6089
+ this.onToolStart(run.state, run.sessionId, { toolCallId: id, toolName: method, input: params ?? {}, parentToolCallId: toolCallId });
6090
+ finish(id, method, params, { error: this.catalogCallError(run, id, call.error) });
6091
+ }
6092
+ return { ok: false, error: call.error as unknown as TJsonValue };
6093
+ }
6094
+ if (signal.aborted) return stopped(call.method);
6095
+ this.onToolStart(run.state, run.sessionId, {
6096
+ toolCallId: id, toolName: call.method, input: call.params, parentToolCallId: toolCallId,
6097
+ });
6098
+ const context = { method: call.method, toolCallId: id, abortSignal: signal, provider: providerFor(signal) };
6099
+ try {
6100
+ const decision = await call.decide(context);
6101
+ signal.throwIfAborted();
6102
+ if (decision === 'propose') {
6103
+ proposals.push({ id, method: call.method, params: call.params });
6104
+ const value = { proposed: true, id };
6105
+ finish(id, call.method, call.params, { output: value });
6106
+ return { ok: true, value };
6107
+ }
6108
+ if (decision !== 'run') {
6109
+ const payload = { error: 'denied' as const, method: call.method, reason: decision.deny };
6110
+ finish(id, call.method, call.params, { error: this.catalogCallError(run, id, payload) });
6111
+ return { ok: false, error: payload };
6112
+ }
6113
+ const value = normalizeJsonValue(await settleToolOutput(call.run(context)), this.toolOutputLimits);
6114
+ finish(id, call.method, call.params, { output: value });
6115
+ return { ok: true, value };
6116
+ } catch (error) {
6117
+ const projected = this.projectExternalError(run, error, 'toolExecution');
6118
+ finish(id, call.method, call.params, { error: projected });
6119
+ return { ok: false, error: { error: 'failed', method: call.method, message: projected.message } };
6120
+ }
6121
+ },
6122
+ };
6123
+ const result = await run.codeSandbox!.run(code, host, catalog.codeMode!, abortSignal);
6124
+ if (!result.ok) throw this.catalogCallError(run, toolCallId, result.failure);
6125
+ return {
6126
+ result: result.value,
6127
+ calls: result.calls,
6128
+ ...(result.logs ? { logs: result.logs } : {}),
6129
+ ...(proposals.length > 0
6130
+ ? { proposals: await catalog.propose(proposals, { toolCallId, abortSignal, provider: base }) }
6131
+ : {}),
6132
+ };
6133
+ }
6134
+
5916
6135
  private createBuiltInTools(run: IActiveRun): Record<string, unknown> {
5917
6136
  const tools: Record<string, unknown> = {};
5918
6137
  if (run.subagents.size > 0 && (run.stored.session.depth ?? 0) < this.maxSubagentDepth) {
@@ -8417,20 +8636,27 @@ export class FlexHarness<TScope = unknown> {
8417
8636
  private onToolStart(
8418
8637
  state: IStorageState,
8419
8638
  sessionId: string,
8420
- event: { toolCallId: string; toolName: string; input: unknown },
8639
+ event: { toolCallId: string; toolName: string; input: unknown; parentToolCallId?: string },
8421
8640
  ): void {
8422
8641
  const run = this.callbackRun(state, sessionId);
8423
8642
  if (!run || run.toolPartIds.has(event.toolCallId)) return;
8424
8643
  try {
8425
- const input = normalizeJsonValue(event.input, this.toolOutputLimits);
8644
+ // An execute call is the call of the catalog method it names.
8645
+ const target = event.toolName === FLEX_TOOL_CATALOG_TOOL_NAMES.execute
8646
+ ? run.toolCatalog?.callTarget(event.input)
8647
+ : undefined;
8648
+ const input = normalizeJsonValue(target ? target.params : event.input, this.toolOutputLimits);
8426
8649
  if (!this.reserveCallbackCapacity(run, 1, jsonBytes(input), 1)) return;
8427
8650
  const part: IFlexToolMessagePart = {
8428
8651
  partId: plugins.crypto.randomUUID(),
8429
8652
  type: 'tool',
8430
8653
  toolCallId: this.boundedIdentifier(event.toolCallId, 'toolCallId'),
8431
- toolName: this.boundedIdentifier(event.toolName, 'toolName'),
8654
+ toolName: this.boundedIdentifier(target ? target.method : event.toolName, 'toolName'),
8432
8655
  status: 'running',
8433
8656
  input,
8657
+ ...(event.parentToolCallId === undefined
8658
+ ? {}
8659
+ : { parentToolCallId: this.boundedIdentifier(event.parentToolCallId, 'parentToolCallId') }),
8434
8660
  startedAt: new Date().toISOString(),
8435
8661
  };
8436
8662
  run.toolPartIds.set(event.toolCallId, part.partId);
@@ -9129,9 +9355,14 @@ export class FlexHarness<TScope = unknown> {
9129
9355
  }
9130
9356
  const tools = handle.tools;
9131
9357
  this.assertNoBuiltInToolCollisions(run, tools);
9358
+ const catalog = handle.catalog;
9359
+ if (catalog !== undefined && (typeof catalog !== 'object' || catalog === null)) {
9360
+ throw new FlexHarnessValidationError('Tool handle catalog must be an object.');
9361
+ }
9132
9362
  const closeHandle = close;
9133
9363
  return {
9134
9364
  tools,
9365
+ ...(catalog === undefined ? {} : { catalog }),
9135
9366
  ...(closeHandle ? { close: () => closeHandle.call(handle) } : {}),
9136
9367
  };
9137
9368
  } catch (error) {
@@ -9218,6 +9449,9 @@ export class FlexHarness<TScope = unknown> {
9218
9449
  );
9219
9450
  if (!handle) continue;
9220
9451
  owned.push({ handle, closed: false });
9452
+ if (handle.catalog !== undefined) {
9453
+ throw new FlexHarnessValidationError('Resource tool providers cannot supply a tool catalog.');
9454
+ }
9221
9455
  for (const [toolName, tool] of Object.entries(handle.tools)) {
9222
9456
  const exposedName = createFlexResourceToolName(namespace, toolName);
9223
9457
  if (Object.prototype.hasOwnProperty.call(tools, exposedName)) {
@@ -9229,6 +9463,7 @@ export class FlexHarness<TScope = unknown> {
9229
9463
  if (owned.length === 0 && Object.keys(tools).length === 0) return undefined;
9230
9464
  return {
9231
9465
  tools: tools as TFlexAgentToolSet,
9466
+ ...(applicationHandle?.catalog === undefined ? {} : { catalog: applicationHandle.catalog }),
9232
9467
  ...(owned.some((entry) => entry.handle.close) ? { close: closeOwned } : {}),
9233
9468
  };
9234
9469
  } catch (error) {