@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.
@@ -28,6 +28,7 @@ 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,
32
33
  FLEX_TOOL_CATALOG_TOOL_NAMES,
33
34
  } from './interfaces.js';
@@ -36,6 +37,7 @@ import {
36
37
  assertNoCatalogCollisions,
37
38
  catalogToolInputSchema,
38
39
  } from './classes.toolcatalog.js';
40
+ import { FlexCodeSandbox, type IFlexCodeHost } from './classes.codesandbox.js';
39
41
  import type {
40
42
  IFlexAgentContextCompactionOptions,
41
43
  IFlexAgentContextInvocation,
@@ -119,6 +121,7 @@ import type {
119
121
  IFlexSubagentProvenance,
120
122
  IFlexTerminalProjection,
121
123
  IFlexCatalogDescribeInput,
124
+ IFlexCatalogProposal,
122
125
  IFlexCatalogSearchInput,
123
126
  IFlexToolCatalogOptions,
124
127
  IFlexToolHandle,
@@ -222,7 +225,7 @@ import {
222
225
  validateSessionImportHandle,
223
226
  type INormalizedSessionImportOptions,
224
227
  } from './utils.sessionimport.js';
225
- import { wrapToolSet } from './utils.tooloutput.js';
228
+ import { settleToolOutput, wrapToolSet } from './utils.tooloutput.js';
226
229
  import {
227
230
  validateCompactSessionOptions,
228
231
  validateCreateProjectTaskInput,
@@ -445,6 +448,8 @@ interface IActiveRun {
445
448
  toolProviderContext?: Readonly<IFlexToolProviderContext<unknown>>;
446
449
  /** The catalog methods the run offers through search_tools, describe_tools and execute. */
447
450
  toolCatalog?: FlexToolCatalog<unknown>;
451
+ /** The sandbox `run_code` programs run in, loaded when the catalog has code mode. */
452
+ codeSandbox?: FlexCodeSandbox;
448
453
  subagents: ReadonlyMap<string, Readonly<IFlexSubagentDefinition>>;
449
454
  allowedTools?: readonly string[];
450
455
  agentAllowedTools?: readonly string[];
@@ -865,7 +870,7 @@ export class FlexHarness<TScope = unknown> {
865
870
  private readonly externalErrorProjector?: TFlexExternalErrorProjector;
866
871
  private readonly subagents: ReadonlyMap<string, Readonly<IFlexSubagentDefinition>>;
867
872
  private readonly builtInTools: INormalizedBuiltInTools;
868
- private readonly toolCatalogOptions: Readonly<Required<IFlexToolCatalogOptions>>;
873
+ private readonly toolCatalogOptions: Readonly<IFlexToolCatalogOptions>;
869
874
  private readonly slashCommands: ReadonlyMap<string, TRegisteredSlashCommand<TScope>>;
870
875
  private readonly maxSubagentDepth: number;
871
876
  private readonly maxSubagentCallsPerRun: number;
@@ -952,7 +957,13 @@ export class FlexHarness<TScope = unknown> {
952
957
  this.externalErrorProjector = options.externalErrorProjector;
953
958
  this.subagents = normalizeSubagents(options.subagents);
954
959
  this.builtInTools = normalizeBuiltInTools(options.builtInTools);
955
- this.toolCatalogOptions = normalizeToolCatalogOptions(options.toolCatalog);
960
+ const toolCatalogOptions = normalizeToolCatalogOptions(options.toolCatalog);
961
+ this.toolCatalogOptions = Object.freeze({
962
+ declarations: toolCatalogOptions.declarations,
963
+ outputTypeDepth: toolCatalogOptions.outputTypeDepth,
964
+ describeTopMatch: toolCatalogOptions.describeTopMatch,
965
+ ...(toolCatalogOptions.codeMode ? { codeMode: { limits: toolCatalogOptions.codeMode } } : {}),
966
+ });
956
967
  this.slashCommands = normalizeSlashCommands(options.slashCommands);
957
968
  this.maxSubagentDepth = resolveBoundedPositiveInteger(
958
969
  options.maxSubagentDepth,
@@ -5844,6 +5855,7 @@ export class FlexHarness<TScope = unknown> {
5844
5855
  if (toolHandle?.catalog) {
5845
5856
  try {
5846
5857
  catalog = await FlexToolCatalog.create(toolHandle.catalog, this.toolCatalogOptions);
5858
+ if (catalog.codeMode) run.codeSandbox = await FlexCodeSandbox.load();
5847
5859
  } catch (error) {
5848
5860
  throw this.projectExternalError(run, error, 'toolProvider');
5849
5861
  }
@@ -5962,7 +5974,8 @@ export class FlexHarness<TScope = unknown> {
5962
5974
  description: definitions[names.search].description,
5963
5975
  strict: false,
5964
5976
  inputSchema: catalogToolInputSchema<IFlexCatalogSearchInput>(definitions[names.search]),
5965
- execute: async (input: IFlexCatalogSearchInput) => catalog.search(input),
5977
+ execute: async (input: IFlexCatalogSearchInput, options?: { messages?: plugins.ModelMessage[] }) =>
5978
+ catalog.search(input, { declaredTypes: catalog.declaredTypesIn(options?.messages), maxBytes: describeBytes }),
5966
5979
  }),
5967
5980
  [names.describe]: plugins.tool({
5968
5981
  description: definitions[names.describe].description,
@@ -5984,20 +5997,141 @@ export class FlexHarness<TScope = unknown> {
5984
5997
  input: { method: string; params?: Record<string, unknown> },
5985
5998
  options?: { toolCallId?: string; abortSignal?: AbortSignal },
5986
5999
  ) => {
6000
+ const toolCallId = options?.toolCallId ?? '';
6001
+ const abortSignal = options?.abortSignal ?? run.controller.signal;
5987
6002
  const call = await catalog.prepareCall(input.method, input.params);
5988
- if (!call.success) {
5989
- const error = this.trustInternalError(new FlexHarnessValidationError(JSON.stringify(call.error)));
5990
- if (options?.toolCallId) run.trustedToolExecutionErrors.set(options.toolCallId, error);
5991
- throw error;
6003
+ if (!call.success) throw this.catalogCallError(run, toolCallId, call.error);
6004
+ const context = { method: call.method, toolCallId, abortSignal, provider: run.toolProviderContext! };
6005
+ const decision = await call.decide(context);
6006
+ if (decision === 'run') return call.run(context);
6007
+ if (decision === 'propose') {
6008
+ return catalog.propose(
6009
+ [{ id: toolCallId, method: call.method, params: call.params }],
6010
+ { toolCallId, abortSignal, provider: run.toolProviderContext! },
6011
+ );
5992
6012
  }
5993
- return call.run({
5994
- method: call.method,
5995
- toolCallId: options?.toolCallId ?? '',
5996
- abortSignal: options?.abortSignal ?? run.controller.signal,
5997
- provider: run.toolProviderContext!,
5998
- });
6013
+ throw this.catalogCallError(run, toolCallId, { error: 'denied', method: call.method, reason: decision.deny });
5999
6014
  },
6000
6015
  }),
6016
+ ...(catalog.codeMode && run.codeSandbox ? {
6017
+ [FLEX_CODE_MODE_TOOL_NAME]: plugins.tool({
6018
+ description: definitions[FLEX_CODE_MODE_TOOL_NAME].description,
6019
+ strict: false,
6020
+ inputSchema: catalogToolInputSchema<{ code: string }>(definitions[FLEX_CODE_MODE_TOOL_NAME]),
6021
+ execute: async (input: { code: string }, options?: { toolCallId?: string; abortSignal?: AbortSignal }) =>
6022
+ this.runCatalogProgram(run, catalog, input.code, options?.toolCallId ?? '', options?.abortSignal ?? run.controller.signal),
6023
+ }),
6024
+ } : {}),
6025
+ };
6026
+ }
6027
+
6028
+ /** A refused catalog call as the trusted error the model and the tool part show. */
6029
+ private catalogCallError(run: IActiveRun, toolCallId: string, payload: unknown): Error {
6030
+ const error = this.trustInternalError(new FlexHarnessValidationError(JSON.stringify(payload)));
6031
+ if (toolCallId) run.trustedToolExecutionErrors.set(toolCallId, error);
6032
+ return error;
6033
+ }
6034
+
6035
+ /**
6036
+ * Runs a `run_code` program. Every catalog call it makes is dispatched like an `execute`
6037
+ * (validation, `onCall`, the method's run with its permission requests, the output limits, error
6038
+ * projection) and recorded as a tool part of its own, named by the method, with the `run_code`
6039
+ * call as its parent. Proposals go to `onProposals` as one batch once the program finished; a
6040
+ * program that fails proposes nothing.
6041
+ */
6042
+ private async runCatalogProgram(
6043
+ run: IActiveRun,
6044
+ catalog: FlexToolCatalog<unknown>,
6045
+ code: string,
6046
+ toolCallId: string,
6047
+ abortSignal: AbortSignal,
6048
+ ): Promise<unknown> {
6049
+ const proposals: IFlexCatalogProposal[] = [];
6050
+ const base = run.toolProviderContext!;
6051
+ // Waiting for a permission decision is the user's time, not the program's.
6052
+ let waitedMs = 0;
6053
+ let waiting = 0;
6054
+ let waitingSince = 0;
6055
+ /** The provider context of one call: its permission waits are timed, and end when the program stops. */
6056
+ const providerFor = (signal: AbortSignal): Readonly<IFlexToolProviderContext<unknown>> => Object.freeze({
6057
+ ...base,
6058
+ requestPermission: async (request: IFlexPermissionRequestInput) => {
6059
+ signal.throwIfAborted();
6060
+ if (waiting++ === 0) waitingSince = performance.now();
6061
+ let onAbort: (() => void) | undefined;
6062
+ try {
6063
+ await Promise.race([
6064
+ base.requestPermission(request),
6065
+ new Promise<never>((_resolve, reject) => {
6066
+ onAbort = () => reject(signal.reason);
6067
+ signal.addEventListener('abort', onAbort, { once: true });
6068
+ }),
6069
+ ]);
6070
+ } finally {
6071
+ if (onAbort) signal.removeEventListener('abort', onAbort);
6072
+ if (--waiting === 0) waitedMs += performance.now() - waitingSince;
6073
+ }
6074
+ },
6075
+ });
6076
+ const stopped = (method: string) => ({ ok: false as const, error: { error: 'aborted', method } });
6077
+ let callNumber = 0;
6078
+ const finish = (id: string, method: string, input: unknown, outcome: { output: unknown } | { error: Error }) => {
6079
+ if ('error' in outcome) run.trustedToolExecutionErrors.set(id, outcome.error);
6080
+ this.onToolFinish(run.state, run.sessionId, 'error' in outcome
6081
+ ? { toolCallId: id, toolName: method, input, success: false, error: outcome.error.message }
6082
+ : { toolCallId: id, toolName: method, input, success: true, output: outcome.output });
6083
+ };
6084
+ const host: IFlexCodeHost = {
6085
+ waitedMs: () => waitedMs + (waiting > 0 ? performance.now() - waitingSince : 0),
6086
+ call: async (method, params, signal) => {
6087
+ if (signal.aborted) return stopped(method);
6088
+ const id = `${toolCallId}.${++callNumber}`;
6089
+ const call = await catalog.prepareCall(method, params);
6090
+ if (!call.success) {
6091
+ if (catalog.has(method)) {
6092
+ this.onToolStart(run.state, run.sessionId, { toolCallId: id, toolName: method, input: params ?? {}, parentToolCallId: toolCallId });
6093
+ finish(id, method, params, { error: this.catalogCallError(run, id, call.error) });
6094
+ }
6095
+ return { ok: false, error: call.error as unknown as TJsonValue };
6096
+ }
6097
+ if (signal.aborted) return stopped(call.method);
6098
+ this.onToolStart(run.state, run.sessionId, {
6099
+ toolCallId: id, toolName: call.method, input: call.params, parentToolCallId: toolCallId,
6100
+ });
6101
+ const context = { method: call.method, toolCallId: id, abortSignal: signal, provider: providerFor(signal) };
6102
+ try {
6103
+ const decision = await call.decide(context);
6104
+ signal.throwIfAborted();
6105
+ if (decision === 'propose') {
6106
+ proposals.push({ id, method: call.method, params: call.params });
6107
+ const value = { proposed: true, id };
6108
+ finish(id, call.method, call.params, { output: value });
6109
+ return { ok: true, value };
6110
+ }
6111
+ if (decision !== 'run') {
6112
+ const payload = { error: 'denied' as const, method: call.method, reason: decision.deny };
6113
+ finish(id, call.method, call.params, { error: this.catalogCallError(run, id, payload) });
6114
+ return { ok: false, error: payload };
6115
+ }
6116
+ const value = normalizeJsonValue(await settleToolOutput(call.run(context)), this.toolOutputLimits);
6117
+ finish(id, call.method, call.params, { output: value });
6118
+ return { ok: true, value };
6119
+ } catch (error) {
6120
+ const projected = this.projectExternalError(run, error, 'toolExecution');
6121
+ finish(id, call.method, call.params, { error: projected });
6122
+ return { ok: false, error: { error: 'failed', method: call.method, message: projected.message } };
6123
+ }
6124
+ },
6125
+ };
6126
+ const result = await run.codeSandbox!.run(code, host, catalog.codeMode!, abortSignal);
6127
+ if (!result.ok) throw this.catalogCallError(run, toolCallId, result.failure);
6128
+ return {
6129
+ result: result.value,
6130
+ calls: result.calls,
6131
+ ...(result.logs ? { logs: result.logs } : {}),
6132
+ ...(proposals.length > 0
6133
+ ? { proposals: await catalog.propose(proposals, { toolCallId, abortSignal, provider: base }) }
6134
+ : {}),
6001
6135
  };
6002
6136
  }
6003
6137
 
@@ -8505,7 +8639,7 @@ export class FlexHarness<TScope = unknown> {
8505
8639
  private onToolStart(
8506
8640
  state: IStorageState,
8507
8641
  sessionId: string,
8508
- event: { toolCallId: string; toolName: string; input: unknown },
8642
+ event: { toolCallId: string; toolName: string; input: unknown; parentToolCallId?: string },
8509
8643
  ): void {
8510
8644
  const run = this.callbackRun(state, sessionId);
8511
8645
  if (!run || run.toolPartIds.has(event.toolCallId)) return;
@@ -8523,6 +8657,9 @@ export class FlexHarness<TScope = unknown> {
8523
8657
  toolName: this.boundedIdentifier(target ? target.method : event.toolName, 'toolName'),
8524
8658
  status: 'running',
8525
8659
  input,
8660
+ ...(event.parentToolCallId === undefined
8661
+ ? {}
8662
+ : { parentToolCallId: this.boundedIdentifier(event.parentToolCallId, 'parentToolCallId') }),
8526
8663
  startedAt: new Date().toISOString(),
8527
8664
  };
8528
8665
  run.toolPartIds.set(event.toolCallId, part.partId);