@modelprofile.com/flexharness 10.13.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.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.codesandbox.d.ts +39 -0
- package/dist_ts/classes.codesandbox.js +539 -0
- package/dist_ts/classes.flexharness.d.ts +10 -0
- package/dist_ts/classes.flexharness.js +145 -15
- package/dist_ts/classes.toolcatalog.d.ts +14 -4
- package/dist_ts/classes.toolcatalog.js +80 -18
- package/dist_ts/interfaces.d.ts +88 -2
- package/dist_ts/interfaces.js +22 -1
- package/dist_ts/plugins.d.ts +62 -1
- package/dist_ts/plugins.js +15 -1
- package/dist_ts/utils.harnessoptions.d.ts +7 -2
- package/dist_ts/utils.harnessoptions.js +37 -9
- package/dist_ts/utils.json.js +3 -1
- package/dist_ts/utils.tooloutput.d.ts +2 -0
- package/dist_ts/utils.tooloutput.js +11 -1
- package/dist_ts_compaction/plugins.d.ts +3 -3
- package/dist_ts_compaction/plugins.js +3 -3
- package/dist_ts_compaction/smartagent.compaction.d.ts +10 -1
- package/dist_ts_compaction/smartagent.compaction.js +79 -38
- package/package.json +16 -4
- package/readme.md +1 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.codesandbox.ts +565 -0
- package/ts/classes.flexharness.ts +148 -14
- package/ts/classes.toolcatalog.ts +94 -13
- package/ts/interfaces.ts +119 -2
- package/ts/plugins.ts +79 -0
- package/ts/readme.md +1 -1
- package/ts/utils.harnessoptions.ts +45 -12
- package/ts/utils.json.ts +2 -0
- package/ts/utils.tooloutput.ts +9 -0
- package/ts_compaction/plugins.ts +3 -3
- package/ts_compaction/smartagent.compaction.ts +90 -34
|
@@ -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<
|
|
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,11 @@ 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
|
-
|
|
960
|
+
const toolCatalogOptions = normalizeToolCatalogOptions(options.toolCatalog);
|
|
961
|
+
this.toolCatalogOptions = Object.freeze({
|
|
962
|
+
declarations: toolCatalogOptions.declarations,
|
|
963
|
+
...(toolCatalogOptions.codeMode ? { codeMode: { limits: toolCatalogOptions.codeMode } } : {}),
|
|
964
|
+
});
|
|
956
965
|
this.slashCommands = normalizeSlashCommands(options.slashCommands);
|
|
957
966
|
this.maxSubagentDepth = resolveBoundedPositiveInteger(
|
|
958
967
|
options.maxSubagentDepth,
|
|
@@ -5844,6 +5853,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
5844
5853
|
if (toolHandle?.catalog) {
|
|
5845
5854
|
try {
|
|
5846
5855
|
catalog = await FlexToolCatalog.create(toolHandle.catalog, this.toolCatalogOptions);
|
|
5856
|
+
if (catalog.codeMode) run.codeSandbox = await FlexCodeSandbox.load();
|
|
5847
5857
|
} catch (error) {
|
|
5848
5858
|
throw this.projectExternalError(run, error, 'toolProvider');
|
|
5849
5859
|
}
|
|
@@ -5984,20 +5994,141 @@ export class FlexHarness<TScope = unknown> {
|
|
|
5984
5994
|
input: { method: string; params?: Record<string, unknown> },
|
|
5985
5995
|
options?: { toolCallId?: string; abortSignal?: AbortSignal },
|
|
5986
5996
|
) => {
|
|
5997
|
+
const toolCallId = options?.toolCallId ?? '';
|
|
5998
|
+
const abortSignal = options?.abortSignal ?? run.controller.signal;
|
|
5987
5999
|
const call = await catalog.prepareCall(input.method, input.params);
|
|
5988
|
-
if (!call.success)
|
|
5989
|
-
|
|
5990
|
-
|
|
5991
|
-
|
|
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
|
+
);
|
|
5992
6009
|
}
|
|
5993
|
-
|
|
5994
|
-
method: call.method,
|
|
5995
|
-
toolCallId: options?.toolCallId ?? '',
|
|
5996
|
-
abortSignal: options?.abortSignal ?? run.controller.signal,
|
|
5997
|
-
provider: run.toolProviderContext!,
|
|
5998
|
-
});
|
|
6010
|
+
throw this.catalogCallError(run, toolCallId, { error: 'denied', method: call.method, reason: decision.deny });
|
|
5999
6011
|
},
|
|
6000
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
|
+
: {}),
|
|
6001
6132
|
};
|
|
6002
6133
|
}
|
|
6003
6134
|
|
|
@@ -8505,7 +8636,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
8505
8636
|
private onToolStart(
|
|
8506
8637
|
state: IStorageState,
|
|
8507
8638
|
sessionId: string,
|
|
8508
|
-
event: { toolCallId: string; toolName: string; input: unknown },
|
|
8639
|
+
event: { toolCallId: string; toolName: string; input: unknown; parentToolCallId?: string },
|
|
8509
8640
|
): void {
|
|
8510
8641
|
const run = this.callbackRun(state, sessionId);
|
|
8511
8642
|
if (!run || run.toolPartIds.has(event.toolCallId)) return;
|
|
@@ -8523,6 +8654,9 @@ export class FlexHarness<TScope = unknown> {
|
|
|
8523
8654
|
toolName: this.boundedIdentifier(target ? target.method : event.toolName, 'toolName'),
|
|
8524
8655
|
status: 'running',
|
|
8525
8656
|
input,
|
|
8657
|
+
...(event.parentToolCallId === undefined
|
|
8658
|
+
? {}
|
|
8659
|
+
: { parentToolCallId: this.boundedIdentifier(event.parentToolCallId, 'parentToolCallId') }),
|
|
8526
8660
|
startedAt: new Date().toISOString(),
|
|
8527
8661
|
};
|
|
8528
8662
|
run.toolPartIds.set(event.toolCallId, part.partId);
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import * as plugins from './plugins.js';
|
|
2
2
|
import { FlexHarnessValidationError } from './errors.js';
|
|
3
3
|
import {
|
|
4
|
+
FLEX_CODE_MODE_TOOL_NAME,
|
|
4
5
|
FLEX_TOOL_CATALOG_LIMITS as limits,
|
|
5
6
|
FLEX_TOOL_CATALOG_TOOL_NAMES as toolNames,
|
|
7
|
+
type IFlexCatalogProposal,
|
|
8
|
+
type IFlexCatalogProposalContext,
|
|
9
|
+
type IFlexCodeModeLimits,
|
|
10
|
+
type TFlexCatalogCallDecision,
|
|
6
11
|
type IFlexCatalogDescribeInput,
|
|
7
12
|
type IFlexCatalogDescribeResult,
|
|
8
13
|
type IFlexCatalogMethod,
|
|
@@ -25,6 +30,7 @@ import {
|
|
|
25
30
|
type IRenderedDeclaration,
|
|
26
31
|
type TJsonSchemaNode,
|
|
27
32
|
} from './utils.schemadeclarations.js';
|
|
33
|
+
import { normalizeToolCatalogOptions } from './utils.harnessoptions.js';
|
|
28
34
|
import { validateUtf8String } from './utils.validation.js';
|
|
29
35
|
|
|
30
36
|
type TValidationResult = { success: true; value: unknown } | { success: false; error: Error };
|
|
@@ -58,6 +64,7 @@ interface IMethodEntry {
|
|
|
58
64
|
readonly outputScope?: ISchemaScope;
|
|
59
65
|
readonly declaration?: string;
|
|
60
66
|
readonly run: IFlexCatalogMethod['run'];
|
|
67
|
+
readonly onCall?: NonNullable<IFlexCatalogMethod['onCall']>;
|
|
61
68
|
}
|
|
62
69
|
|
|
63
70
|
interface ITypeEntry {
|
|
@@ -76,6 +83,8 @@ interface IPreparedCall {
|
|
|
76
83
|
readonly success: true;
|
|
77
84
|
readonly method: string;
|
|
78
85
|
readonly params: unknown;
|
|
86
|
+
/** The method's `onCall` decision for the call; `run` without the hook. */
|
|
87
|
+
decide(context: IFlexCatalogRunContext<any>): Promise<TFlexCatalogCallDecision>;
|
|
79
88
|
run(context: IFlexCatalogRunContext<any>): ReturnType<IFlexCatalogMethod['run']>;
|
|
80
89
|
}
|
|
81
90
|
|
|
@@ -99,10 +108,10 @@ const controlPattern = /[\u0000-\u001f\u007f]/;
|
|
|
99
108
|
const definitionRefPattern = /^#\/(\$defs|definitions)\/([^/]+)$/;
|
|
100
109
|
const declaredTypesHeader = '// Declares types: ';
|
|
101
110
|
const identifierScanPattern = /[A-Za-z_$][A-Za-z0-9_$]*/g;
|
|
102
|
-
const describeFormats = new Set(['typescript', 'json-schema']);
|
|
103
111
|
const methodKeys = new Set([
|
|
104
|
-
'method', 'summary', 'description', 'tags', 'examples', 'inputSchema', 'outputSchema', 'declaration', 'run',
|
|
112
|
+
'method', 'summary', 'description', 'tags', 'examples', 'inputSchema', 'outputSchema', 'declaration', 'run', 'onCall',
|
|
105
113
|
]);
|
|
114
|
+
const catalogKeys = new Set(['methods', 'types', 'onProposals']);
|
|
106
115
|
const typeKeys = new Set(['name', 'description', 'schema', 'declaration']);
|
|
107
116
|
const stopWords = new Set([
|
|
108
117
|
'a', 'an', 'and', 'are', 'all', 'as', 'be', 'by', 'for', 'from', 'get', 'i', 'in', 'is', 'it', 'me', 'my',
|
|
@@ -421,14 +430,19 @@ class FlexToolCatalogCore {
|
|
|
421
430
|
public readonly mode: TDeclarationMode,
|
|
422
431
|
public readonly methods: ReadonlyMap<string, IMethodEntry>,
|
|
423
432
|
public readonly types: ReadonlyMap<string, ITypeEntry>,
|
|
433
|
+
public readonly onProposals: IFlexToolCatalog['onProposals'] | undefined,
|
|
424
434
|
) {
|
|
425
435
|
this.index = new CatalogSearchIndex([...methods.values()]);
|
|
426
436
|
}
|
|
427
437
|
|
|
428
438
|
public static async build(catalog: unknown, mode: TDeclarationMode): Promise<FlexToolCatalogCore> {
|
|
429
439
|
if (!isPlainObject(catalog)) throw new FlexHarnessValidationError('A tool catalog must be a plain object.');
|
|
430
|
-
const unsupported = Object.keys(catalog).find((key) => key
|
|
440
|
+
const unsupported = Object.keys(catalog).find((key) => !catalogKeys.has(key));
|
|
431
441
|
if (unsupported) throw new FlexHarnessValidationError(`A tool catalog does not support "${unsupported}".`);
|
|
442
|
+
const onProposals = catalog.onProposals;
|
|
443
|
+
if (onProposals !== undefined && typeof onProposals !== 'function') {
|
|
444
|
+
throw new FlexHarnessValidationError('A tool catalog onProposals must be a function.');
|
|
445
|
+
}
|
|
432
446
|
if (!Array.isArray(catalog.methods) || catalog.methods.length > limits.maxMethods) {
|
|
433
447
|
throw new FlexHarnessValidationError(`Tool catalog methods must be an array with at most ${limits.maxMethods} entries.`);
|
|
434
448
|
}
|
|
@@ -506,7 +520,14 @@ class FlexToolCatalogCore {
|
|
|
506
520
|
});
|
|
507
521
|
}
|
|
508
522
|
const types = new Map([...registry.entries()].sort(([left], [right]) => compareCodePoints(left, right)));
|
|
509
|
-
return new FlexToolCatalogCore(
|
|
523
|
+
return new FlexToolCatalogCore(
|
|
524
|
+
mode,
|
|
525
|
+
methods,
|
|
526
|
+
types,
|
|
527
|
+
onProposals === undefined
|
|
528
|
+
? undefined
|
|
529
|
+
: (proposals, context) => (onProposals as NonNullable<IFlexToolCatalog['onProposals']>).call(catalog, proposals, context),
|
|
530
|
+
);
|
|
510
531
|
}
|
|
511
532
|
|
|
512
533
|
private static async normalizeType(value: unknown, label: string): Promise<ITypeEntry> {
|
|
@@ -590,6 +611,9 @@ class FlexToolCatalogCore {
|
|
|
590
611
|
throw new FlexHarnessValidationError(`${methodLabel} declaration must declare "function ${name}(".`);
|
|
591
612
|
}
|
|
592
613
|
if (typeof value.run !== 'function') throw new FlexHarnessValidationError(`${methodLabel} run must be a function.`);
|
|
614
|
+
if (value.onCall !== undefined && typeof value.onCall !== 'function') {
|
|
615
|
+
throw new FlexHarnessValidationError(`${methodLabel} onCall must be a function.`);
|
|
616
|
+
}
|
|
593
617
|
if (value.inputSchema === undefined) throw new FlexHarnessValidationError(`${methodLabel} needs an inputSchema.`);
|
|
594
618
|
const input = await resolveCatalogSchema(value.inputSchema, `${methodLabel} inputSchema`);
|
|
595
619
|
const inputJson = input.json;
|
|
@@ -604,6 +628,7 @@ class FlexToolCatalogCore {
|
|
|
604
628
|
? undefined
|
|
605
629
|
: await resolveCatalogSchema(value.outputSchema, `${methodLabel} outputSchema`);
|
|
606
630
|
const run = value.run as IFlexCatalogMethod['run'];
|
|
631
|
+
const onCall = value.onCall as IFlexCatalogMethod['onCall'];
|
|
607
632
|
return {
|
|
608
633
|
path,
|
|
609
634
|
namespace: segments.slice(0, -1).join('.'),
|
|
@@ -616,6 +641,9 @@ class FlexToolCatalogCore {
|
|
|
616
641
|
...(output ? { output } : {}),
|
|
617
642
|
...(declaration === undefined ? {} : { declaration }),
|
|
618
643
|
run: (params, context) => run.call(value, params, context),
|
|
644
|
+
...(onCall === undefined ? {} : {
|
|
645
|
+
onCall: (params: unknown, context: IFlexCatalogRunContext) => onCall.call(value, params, context),
|
|
646
|
+
}),
|
|
619
647
|
};
|
|
620
648
|
}
|
|
621
649
|
|
|
@@ -791,6 +819,8 @@ export class FlexToolCatalog<TScope = unknown> {
|
|
|
791
819
|
private constructor(
|
|
792
820
|
private readonly core: FlexToolCatalogCore,
|
|
793
821
|
private readonly admitted: ReadonlySet<string> | undefined,
|
|
822
|
+
/** The limits of `run_code`; absent while code mode is off. */
|
|
823
|
+
public readonly codeMode: Readonly<IFlexCodeModeLimits> | undefined,
|
|
794
824
|
) {}
|
|
795
825
|
|
|
796
826
|
/**
|
|
@@ -801,10 +831,8 @@ export class FlexToolCatalog<TScope = unknown> {
|
|
|
801
831
|
catalog: IFlexToolCatalog<TScope>,
|
|
802
832
|
options: IFlexToolCatalogOptions = {},
|
|
803
833
|
): Promise<FlexToolCatalog<TScope>> {
|
|
804
|
-
const
|
|
805
|
-
|
|
806
|
-
throw new FlexHarnessValidationError('toolCatalog.declarations must be "typescript" or "json-schema".');
|
|
807
|
-
}
|
|
834
|
+
const normalized = normalizeToolCatalogOptions(options);
|
|
835
|
+
const mode = normalized.declarations;
|
|
808
836
|
if (typeof catalog !== 'object' || catalog === null) {
|
|
809
837
|
throw new FlexHarnessValidationError('A tool catalog must be a plain object.');
|
|
810
838
|
}
|
|
@@ -818,7 +846,7 @@ export class FlexToolCatalog<TScope = unknown> {
|
|
|
818
846
|
core = FlexToolCatalogCore.build(catalog, mode);
|
|
819
847
|
byMode.set(mode, core);
|
|
820
848
|
}
|
|
821
|
-
return new FlexToolCatalog<TScope>(await core, undefined);
|
|
849
|
+
return new FlexToolCatalog<TScope>(await core, undefined, normalized.codeMode);
|
|
822
850
|
}
|
|
823
851
|
|
|
824
852
|
/** Every method path this catalog offers, sorted. */
|
|
@@ -837,6 +865,7 @@ export class FlexToolCatalog<TScope = unknown> {
|
|
|
837
865
|
return new FlexToolCatalog<TScope>(
|
|
838
866
|
this.core,
|
|
839
867
|
new Set([...this.core.methods.keys()].filter((path) => allowed.has(path) && this.isAdmitted(path))),
|
|
868
|
+
this.codeMode,
|
|
840
869
|
);
|
|
841
870
|
}
|
|
842
871
|
|
|
@@ -844,7 +873,10 @@ export class FlexToolCatalog<TScope = unknown> {
|
|
|
844
873
|
return this.core.methods.has(path) && this.isAdmitted(path);
|
|
845
874
|
}
|
|
846
875
|
|
|
847
|
-
/**
|
|
876
|
+
/**
|
|
877
|
+
* The built-in tools as the model sees them: the three catalog tools, and `run_code` in code
|
|
878
|
+
* mode. Their text depends only on the catalog and its options.
|
|
879
|
+
*/
|
|
848
880
|
public toolDefinitions(): Record<string, IFlexCatalogToolDefinition> {
|
|
849
881
|
const typescript = this.core.mode === 'typescript';
|
|
850
882
|
const methodPath = { type: 'string', maxLength: limits.maxMethodPathCharacters } as const;
|
|
@@ -899,9 +931,42 @@ export class FlexToolCatalog<TScope = unknown> {
|
|
|
899
931
|
additionalProperties: false,
|
|
900
932
|
},
|
|
901
933
|
},
|
|
934
|
+
...(this.codeMode ? { [FLEX_CODE_MODE_TOOL_NAME]: this.codeToolDefinition(this.codeMode) } : {}),
|
|
902
935
|
};
|
|
903
936
|
}
|
|
904
937
|
|
|
938
|
+
private codeToolDefinition(codeMode: Readonly<IFlexCodeModeLimits>): IFlexCatalogToolDefinition {
|
|
939
|
+
return {
|
|
940
|
+
description: [
|
|
941
|
+
'Run a short TypeScript or JavaScript program against the catalog, for work that takes several calls:',
|
|
942
|
+
'loops, filters, joins. Call a method as `await api.<dotted path>(params)`, for example',
|
|
943
|
+
'`await api.banking.listMovements({ accountId })`, with the declarations describe_tools returns; a refused',
|
|
944
|
+
'call throws an `ApiError` whose `details` say why. `return` a JSON value: it and the `console.log` output',
|
|
945
|
+
'come back, nothing else does. No network, files, timers or imports;',
|
|
946
|
+
`at most ${codeMode.maxCalls} calls and ${Math.round(codeMode.wallMs / 1000)} seconds.`,
|
|
947
|
+
].join(' '),
|
|
948
|
+
inputSchema: {
|
|
949
|
+
type: 'object',
|
|
950
|
+
properties: { code: { type: 'string', minLength: 1, maxLength: codeMode.maxSourceBytes } },
|
|
951
|
+
required: ['code'],
|
|
952
|
+
additionalProperties: false,
|
|
953
|
+
},
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
/** Hands proposed calls to the catalog's `onProposals` and returns what the model sees for them. */
|
|
958
|
+
public async propose(
|
|
959
|
+
proposals: readonly IFlexCatalogProposal[],
|
|
960
|
+
context: IFlexCatalogProposalContext<TScope>,
|
|
961
|
+
): Promise<unknown> {
|
|
962
|
+
if (!this.core.onProposals) {
|
|
963
|
+
throw new FlexHarnessValidationError(
|
|
964
|
+
`Tool catalog method "${proposals[0]?.method}" proposed a call, but the catalog has no onProposals.`,
|
|
965
|
+
);
|
|
966
|
+
}
|
|
967
|
+
return await this.core.onProposals(proposals, context as IFlexCatalogProposalContext);
|
|
968
|
+
}
|
|
969
|
+
|
|
905
970
|
/** Search results or a namespace listing as text. */
|
|
906
971
|
public search(input: IFlexCatalogSearchInput = {}): string {
|
|
907
972
|
const limit = input.limit ?? limits.defaultSearchResults;
|
|
@@ -1041,6 +1106,19 @@ export class FlexToolCatalog<TScope = unknown> {
|
|
|
1041
1106
|
success: true,
|
|
1042
1107
|
method: path,
|
|
1043
1108
|
params: validated,
|
|
1109
|
+
decide: async (context) => {
|
|
1110
|
+
const decision = entry.onCall ? await entry.onCall(validated, context) : 'run';
|
|
1111
|
+
if (decision === 'run' || decision === 'propose') return decision;
|
|
1112
|
+
if (
|
|
1113
|
+
isSchemaRecord(decision)
|
|
1114
|
+
&& Object.keys(decision).length === 1
|
|
1115
|
+
&& typeof decision.deny === 'string'
|
|
1116
|
+
&& decision.deny.trim()
|
|
1117
|
+
) {
|
|
1118
|
+
return { deny: decision.deny.slice(0, 2000) };
|
|
1119
|
+
}
|
|
1120
|
+
throw new FlexHarnessValidationError(`Tool catalog method "${path}" onCall returned an invalid decision.`);
|
|
1121
|
+
},
|
|
1044
1122
|
run: (context) => entry.run(validated, context),
|
|
1045
1123
|
};
|
|
1046
1124
|
}
|
|
@@ -1383,14 +1461,17 @@ const constraintText = (subset: Record<string, unknown>): string => {
|
|
|
1383
1461
|
};
|
|
1384
1462
|
|
|
1385
1463
|
/**
|
|
1386
|
-
* While a run has a catalog, its
|
|
1387
|
-
* every tool name.
|
|
1464
|
+
* While a run has a catalog, its tool names are reserved (`run_code` in code mode too), and a
|
|
1465
|
+
* method path must differ from every tool name.
|
|
1388
1466
|
*/
|
|
1389
1467
|
export const assertNoCatalogCollisions = (
|
|
1390
1468
|
catalog: FlexToolCatalog<unknown>,
|
|
1391
1469
|
tools: Readonly<Record<string, unknown>>,
|
|
1392
1470
|
): void => {
|
|
1393
|
-
const catalogToolNames = new Set<string>(
|
|
1471
|
+
const catalogToolNames = new Set<string>([
|
|
1472
|
+
...Object.values(toolNames),
|
|
1473
|
+
...(catalog.codeMode ? [FLEX_CODE_MODE_TOOL_NAME] : []),
|
|
1474
|
+
]);
|
|
1394
1475
|
for (const name of catalogToolNames) {
|
|
1395
1476
|
if (Object.prototype.hasOwnProperty.call(tools, name)) {
|
|
1396
1477
|
throw new FlexHarnessValidationError(`Tool provider cannot define enabled built-in tool "${name}".`);
|
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;
|
|
@@ -743,6 +747,11 @@ export type TFlexCatalogCallError =
|
|
|
743
747
|
more?: number;
|
|
744
748
|
/** The lookup that returns the method's declaration. */
|
|
745
749
|
see: string;
|
|
750
|
+
}
|
|
751
|
+
| {
|
|
752
|
+
error: 'denied';
|
|
753
|
+
method: string;
|
|
754
|
+
reason: string;
|
|
746
755
|
};
|
|
747
756
|
|
|
748
757
|
export interface IFlexCatalogToolDefinition {
|
|
@@ -761,7 +770,10 @@ export type TFlexCatalogSchema = { readonly [keyword: string]: unknown } | plugi
|
|
|
761
770
|
export interface IFlexCatalogRunContext<TScope = unknown> {
|
|
762
771
|
/** The dotted path the model called. */
|
|
763
772
|
readonly method: string;
|
|
764
|
-
/**
|
|
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
|
+
*/
|
|
765
777
|
readonly toolCallId: string;
|
|
766
778
|
readonly abortSignal: AbortSignal;
|
|
767
779
|
/** The run's tool-provider context: scope, session, run and `requestPermission`. */
|
|
@@ -797,6 +809,31 @@ export interface IFlexCatalogMethod<TScope = unknown, TParams = any, TResult = u
|
|
|
797
809
|
params: TParams,
|
|
798
810
|
context: IFlexCatalogRunContext<TScope>,
|
|
799
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>>;
|
|
800
837
|
}
|
|
801
838
|
|
|
802
839
|
/** A named type that method declarations refer to; it needs a `declaration`, a `schema`, or both. */
|
|
@@ -813,6 +850,15 @@ export interface IFlexCatalogType {
|
|
|
813
850
|
export interface IFlexToolCatalog<TScope = unknown> {
|
|
814
851
|
methods: readonly IFlexCatalogMethod<TScope>[];
|
|
815
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;
|
|
816
862
|
}
|
|
817
863
|
|
|
818
864
|
export interface IFlexToolCatalogOptions {
|
|
@@ -821,8 +867,79 @@ export interface IFlexToolCatalogOptions {
|
|
|
821
867
|
* across the session, or `json-schema` with each method's self-contained JSON Schema.
|
|
822
868
|
*/
|
|
823
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>;
|
|
824
880
|
}
|
|
825
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
|
+
}
|
|
826
943
|
export interface IFlexResourceToolProviderResolverContext<TScope> extends IFlexSessionGeneration {
|
|
827
944
|
scopeId: string;
|
|
828
945
|
scope: TScope;
|
|
@@ -1762,7 +1879,7 @@ export type TFlexModelUsageEvent = IFlexEventBase & {
|
|
|
1762
1879
|
readonly type: 'model.usage';
|
|
1763
1880
|
readonly runId?: string;
|
|
1764
1881
|
readonly source: 'generation' | 'compaction';
|
|
1765
|
-
} & Readonly<
|
|
1882
|
+
} & Readonly<TFlexObservedModelCallUsage>;
|
|
1766
1883
|
|
|
1767
1884
|
export interface IFlexPromptQueueEvent extends IFlexEventBase {
|
|
1768
1885
|
readonly type: 'prompt.queued' | 'prompt.started' | 'prompt.running' | 'prompt.finished';
|