@modelprofile.com/flexharness 10.15.0 → 10.16.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.js +6 -2
- package/dist_ts/classes.flexharness.d.ts +33 -0
- package/dist_ts/classes.flexharness.js +328 -45
- package/dist_ts/classes.stores.d.ts +3 -1
- package/dist_ts/classes.stores.js +59 -1
- package/dist_ts/classes.toolcatalog.d.ts +9 -1
- package/dist_ts/classes.toolcatalog.js +82 -11
- package/dist_ts/interfaces.d.ts +59 -0
- package/dist_ts/interfaces.js +9 -1
- package/dist_ts/plugins.d.ts +3 -3
- package/dist_ts/plugins.js +3 -3
- package/dist_ts/utils.json.d.ts +5 -0
- package/dist_ts/utils.json.js +35 -1
- package/dist_ts/utils.results.d.ts +21 -0
- package/dist_ts/utils.results.js +158 -0
- package/dist_ts/utils.tooloutput.d.ts +5 -2
- package/dist_ts/utils.tooloutput.js +10 -3
- package/dist_ts_document/index.js +21 -8
- package/dist_ts_document/plugins.d.ts +2 -2
- package/dist_ts_document/plugins.js +3 -3
- package/dist_ts_media/index.js +14 -3
- package/dist_ts_media/plugins.d.ts +2 -2
- package/dist_ts_media/plugins.js +3 -3
- package/dist_ts_stores_nosqldb/classes.nosqldbstores.d.ts +2 -1
- package/dist_ts_stores_nosqldb/classes.nosqldbstores.js +37 -4
- package/dist_ts_stores_nosqldb/classes.recordstore.d.ts +4 -3
- package/dist_ts_stores_nosqldb/classes.recordstore.js +2 -2
- package/package.json +3 -3
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.codesandbox.ts +5 -1
- package/ts/classes.flexharness.ts +382 -44
- package/ts/classes.stores.ts +66 -0
- package/ts/classes.toolcatalog.ts +87 -7
- package/ts/interfaces.ts +60 -0
- package/ts/plugins.ts +3 -0
- package/ts/utils.json.ts +30 -0
- package/ts/utils.results.ts +164 -0
- package/ts/utils.tooloutput.ts +9 -2
- package/ts_document/index.ts +18 -7
- package/ts_document/plugins.ts +2 -2
- package/ts_document/readme.md +2 -0
- package/ts_media/index.ts +11 -2
- package/ts_media/plugins.ts +2 -2
- package/ts_stores_nosqldb/classes.nosqldbstores.ts +39 -2
- package/ts_stores_nosqldb/classes.recordstore.ts +6 -4
|
@@ -29,15 +29,19 @@ import {
|
|
|
29
29
|
FLEX_REVERSION_WORKSPACE_ID_MAX_BYTES,
|
|
30
30
|
FLEX_REVERSION_WORKSPACE_LABEL_MAX_BYTES,
|
|
31
31
|
FLEX_CODE_MODE_TOOL_NAME,
|
|
32
|
+
FLEX_RESULT_LIMITS,
|
|
33
|
+
FLEX_RESULT_METHOD,
|
|
32
34
|
FLEX_SESSION_IMPORT_MAX_DEPTH,
|
|
33
35
|
FLEX_TOOL_CATALOG_TOOL_NAMES,
|
|
36
|
+
type IFlexResultHandle,
|
|
34
37
|
} from './interfaces.js';
|
|
38
|
+
import { describeShape, resultPage, selectPath, summarizeResult } from './utils.results.js';
|
|
35
39
|
import {
|
|
36
40
|
FlexToolCatalog,
|
|
37
41
|
assertNoCatalogCollisions,
|
|
38
42
|
catalogToolInputSchema,
|
|
39
43
|
} from './classes.toolcatalog.js';
|
|
40
|
-
import { FlexCodeSandbox, type IFlexCodeHost } from './classes.codesandbox.js';
|
|
44
|
+
import { FlexCodeSandbox, type IFlexCodeHost, type TFlexCodeCallOutcome } from './classes.codesandbox.js';
|
|
41
45
|
import type {
|
|
42
46
|
IFlexAgentContextCompactionOptions,
|
|
43
47
|
IFlexAgentContextInvocation,
|
|
@@ -159,6 +163,7 @@ import type {
|
|
|
159
163
|
import { InMemoryFlexHarnessStores } from './classes.stores.js';
|
|
160
164
|
import {
|
|
161
165
|
assertJsonSerializable,
|
|
166
|
+
canonicalJson,
|
|
162
167
|
cloneSerializable,
|
|
163
168
|
deepFreeze,
|
|
164
169
|
normalizeJsonValue,
|
|
@@ -225,7 +230,7 @@ import {
|
|
|
225
230
|
validateSessionImportHandle,
|
|
226
231
|
type INormalizedSessionImportOptions,
|
|
227
232
|
} from './utils.sessionimport.js';
|
|
228
|
-
import { settleToolOutput, wrapToolSet } from './utils.tooloutput.js';
|
|
233
|
+
import { isAsyncIterable, settleToolOutput, wrapToolSet } from './utils.tooloutput.js';
|
|
229
234
|
import {
|
|
230
235
|
validateCompactSessionOptions,
|
|
231
236
|
validateCreateProjectTaskInput,
|
|
@@ -435,6 +440,18 @@ interface IActiveRun {
|
|
|
435
440
|
reasoningPartIds: Map<string, string>;
|
|
436
441
|
toolPartIds: Map<string, string>;
|
|
437
442
|
trustedToolExecutionErrors: Map<string, Error>;
|
|
443
|
+
/**
|
|
444
|
+
* The confirmations of read-only calls started while the model still streamed, by tool call ID:
|
|
445
|
+
* their permission requests wait for them.
|
|
446
|
+
*/
|
|
447
|
+
unconfirmedToolCalls: Map<string, Promise<void>>;
|
|
448
|
+
/**
|
|
449
|
+
* The read-only calls of the turn by real name and canonical params, with the call that answers
|
|
450
|
+
* them. A call that is not read-only clears them when it runs.
|
|
451
|
+
*/
|
|
452
|
+
readCalls: Map<string, IRunReadCall>;
|
|
453
|
+
/** Calls an identical earlier read answered: the call's ID to the earlier call's. */
|
|
454
|
+
deduplicatedToolCalls: Map<string, string>;
|
|
438
455
|
subagentCallCount: number;
|
|
439
456
|
subagentSessionIds: Set<string>;
|
|
440
457
|
backgroundSubagents: Set<Promise<void>>;
|
|
@@ -450,6 +467,8 @@ interface IActiveRun {
|
|
|
450
467
|
toolCatalog?: FlexToolCatalog<unknown>;
|
|
451
468
|
/** The sandbox `run_code` programs run in, loaded when the catalog has code mode. */
|
|
452
469
|
codeSandbox?: FlexCodeSandbox;
|
|
470
|
+
/** The tool handle's classifier of read-only calls. */
|
|
471
|
+
isReadOnlyHostCall?: IFlexToolHandle['isReadOnlyToolCall'];
|
|
453
472
|
subagents: ReadonlyMap<string, Readonly<IFlexSubagentDefinition>>;
|
|
454
473
|
allowedTools?: readonly string[];
|
|
455
474
|
agentAllowedTools?: readonly string[];
|
|
@@ -462,6 +481,22 @@ interface IActiveRun {
|
|
|
462
481
|
stop: AbortController;
|
|
463
482
|
}
|
|
464
483
|
|
|
484
|
+
/** The validated params of a `results.get` call. */
|
|
485
|
+
interface IResultRead {
|
|
486
|
+
handle: string;
|
|
487
|
+
page?: number;
|
|
488
|
+
path?: string;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/** A read-only call of a turn and its output, which identical reads after it share. */
|
|
492
|
+
interface IRunReadCall {
|
|
493
|
+
readonly toolCallId: string;
|
|
494
|
+
/** The call's output; `notShared` when it streams its output, which only its own call reads. */
|
|
495
|
+
readonly output: Promise<unknown>;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const notShared = Symbol('not shared');
|
|
499
|
+
|
|
465
500
|
/** The steers of a run: the input its generation takes in, and the prompts it accepted for it. */
|
|
466
501
|
interface IRunSteering {
|
|
467
502
|
readonly input: plugins.AgentPendingInputQueue;
|
|
@@ -4447,6 +4482,9 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4447
4482
|
reasoningPartIds: new Map(),
|
|
4448
4483
|
toolPartIds: new Map(),
|
|
4449
4484
|
trustedToolExecutionErrors: new Map(),
|
|
4485
|
+
unconfirmedToolCalls: new Map(),
|
|
4486
|
+
readCalls: new Map(),
|
|
4487
|
+
deduplicatedToolCalls: new Map(),
|
|
4450
4488
|
subagentCallCount: 0,
|
|
4451
4489
|
subagentSessionIds: new Set(),
|
|
4452
4490
|
backgroundSubagents: new Set(),
|
|
@@ -5781,8 +5819,14 @@ export class FlexHarness<TScope = unknown> {
|
|
|
5781
5819
|
runId: run.runId,
|
|
5782
5820
|
...resolverRelationship,
|
|
5783
5821
|
signal,
|
|
5784
|
-
requestPermission: (request: IFlexPermissionRequestInput) =>
|
|
5785
|
-
|
|
5822
|
+
requestPermission: async (request: IFlexPermissionRequestInput) => {
|
|
5823
|
+
// A call that started early asks once the model's response confirmed it.
|
|
5824
|
+
const confirmed = request.toolCallId === undefined
|
|
5825
|
+
? undefined
|
|
5826
|
+
: run.unconfirmedToolCalls.get(request.toolCallId);
|
|
5827
|
+
if (confirmed) await confirmed;
|
|
5828
|
+
return this.requestPermission(run.state, run, request);
|
|
5829
|
+
},
|
|
5786
5830
|
});
|
|
5787
5831
|
const toolOutcome = Promise.resolve()
|
|
5788
5832
|
.then(() => this.provideRunToolHandle(run, toolProviderContext))
|
|
@@ -5851,10 +5895,13 @@ export class FlexHarness<TScope = unknown> {
|
|
|
5851
5895
|
signal.throwIfAborted();
|
|
5852
5896
|
run.modelResolution = model!;
|
|
5853
5897
|
run.toolProviderContext = toolProviderContext as Readonly<IFlexToolProviderContext<unknown>>;
|
|
5898
|
+
run.isReadOnlyHostCall = toolHandle?.isReadOnlyToolCall;
|
|
5854
5899
|
let catalog: FlexToolCatalog<unknown> | undefined;
|
|
5855
5900
|
if (toolHandle?.catalog) {
|
|
5856
5901
|
try {
|
|
5857
|
-
catalog = await FlexToolCatalog.create(toolHandle.catalog, this.toolCatalogOptions
|
|
5902
|
+
catalog = await FlexToolCatalog.create(toolHandle.catalog, this.toolCatalogOptions, {
|
|
5903
|
+
results: this.stores.results !== undefined,
|
|
5904
|
+
});
|
|
5858
5905
|
if (catalog.codeMode) run.codeSandbox = await FlexCodeSandbox.load();
|
|
5859
5906
|
} catch (error) {
|
|
5860
5907
|
throw this.projectExternalError(run, error, 'toolProvider');
|
|
@@ -5877,17 +5924,18 @@ export class FlexHarness<TScope = unknown> {
|
|
|
5877
5924
|
: Object.fromEntries(Object.entries(combinedTools).filter(([name]) => admitted(name)));
|
|
5878
5925
|
const admittedCatalog = catalog && (globalNames === undefined && agentNames === undefined
|
|
5879
5926
|
? catalog
|
|
5880
|
-
: catalog.restrict(catalog.methods.filter(admitted)));
|
|
5927
|
+
: catalog.restrict(catalog.methods.filter((name) => name === FLEX_RESULT_METHOD || admitted(name))));
|
|
5881
5928
|
if (admittedCatalog && admittedCatalog.methods.length > 0) {
|
|
5882
5929
|
run.toolCatalog = admittedCatalog;
|
|
5883
5930
|
Object.assign(admittedTools, this.createCatalogTools(run, admittedCatalog));
|
|
5884
5931
|
}
|
|
5885
5932
|
tools = Object.keys(admittedTools).length > 0
|
|
5886
|
-
? wrapToolSet(
|
|
5933
|
+
? this.trackToolCalls(run, wrapToolSet(
|
|
5887
5934
|
admittedTools as TFlexAgentToolSet,
|
|
5888
5935
|
this.toolOutputLimits,
|
|
5889
5936
|
(toolError) => this.projectExternalError(run, toolError, 'toolExecution'),
|
|
5890
|
-
|
|
5937
|
+
(output) => this.budgetOutput(run, output),
|
|
5938
|
+
))
|
|
5891
5939
|
: undefined;
|
|
5892
5940
|
} catch (error) {
|
|
5893
5941
|
throw this.projectExternalError(run, error, 'toolProvider');
|
|
@@ -5915,7 +5963,11 @@ export class FlexHarness<TScope = unknown> {
|
|
|
5915
5963
|
return {
|
|
5916
5964
|
model: model!.model,
|
|
5917
5965
|
...(options.system ?? model!.system ? { system: options.system ?? model!.system } : {}),
|
|
5918
|
-
...(tools ? {
|
|
5966
|
+
...(tools ? {
|
|
5967
|
+
tools,
|
|
5968
|
+
isReadOnlyToolCall: ({ toolName, input }: { toolName: string; input: unknown }) =>
|
|
5969
|
+
this.isReadOnlyToolCall(run, toolName, input),
|
|
5970
|
+
} : {}),
|
|
5919
5971
|
...(model!.providerOptions ? { providerOptions: model!.providerOptions } : {}),
|
|
5920
5972
|
...(model!.cache === undefined ? {} : { cache: model!.cache }),
|
|
5921
5973
|
...(options.maxSteps ?? model!.maxSteps ? { maxSteps: options.maxSteps ?? model!.maxSteps } : {}),
|
|
@@ -5935,6 +5987,201 @@ export class FlexHarness<TScope = unknown> {
|
|
|
5935
5987
|
}
|
|
5936
5988
|
}
|
|
5937
5989
|
|
|
5990
|
+
/**
|
|
5991
|
+
* The output the model gets: the output itself within the output budget, or, in a run with a
|
|
5992
|
+
* catalog whose stores keep results, a handle to the stored output with a summary of it.
|
|
5993
|
+
*/
|
|
5994
|
+
private async budgetOutput(run: IActiveRun, output: unknown): Promise<TJsonValue> {
|
|
5995
|
+
const results = this.stores.results;
|
|
5996
|
+
const maxBytes = this.toolOutputLimits.maxBytes;
|
|
5997
|
+
if (!results || !run.toolCatalog) return normalizeJsonValue(output, this.toolOutputLimits);
|
|
5998
|
+
const stored = normalizeJsonValue(output, {
|
|
5999
|
+
...this.toolOutputLimits,
|
|
6000
|
+
maxBytes: Math.max(maxBytes, FLEX_RESULT_LIMITS.maxStoredBytes),
|
|
6001
|
+
});
|
|
6002
|
+
const bytes = jsonBytes(stored);
|
|
6003
|
+
if (bytes <= maxBytes) return stored;
|
|
6004
|
+
const handle = `res_${plugins.crypto.randomBytes(8).toString('hex')}`;
|
|
6005
|
+
const read = `Too large to return whole: read it page by page with execute({ method: "${FLEX_RESULT_METHOD}", `
|
|
6006
|
+
+ `params: { handle: "${handle}", page: 1 } })${run.toolCatalog.codeMode
|
|
6007
|
+
? `, or in run_code with await api.results.get("${handle}", { page: 1 })`
|
|
6008
|
+
: ''}; a dotted \`path\` reads a part of it.`;
|
|
6009
|
+
const result: IFlexResultHandle = {
|
|
6010
|
+
$flexType: 'result',
|
|
6011
|
+
handle,
|
|
6012
|
+
bytes,
|
|
6013
|
+
pages: resultPage(stored, '', 1, this.resultPageBytes())!.pages,
|
|
6014
|
+
summary: summarizeResult(stored, Math.min(FLEX_RESULT_LIMITS.summaryBytes, Math.floor(maxBytes / 4))),
|
|
6015
|
+
read,
|
|
6016
|
+
};
|
|
6017
|
+
// A budget too small for even the handle keeps the truncated output.
|
|
6018
|
+
if (jsonBytes(result) > maxBytes) return normalizeJsonValue(output, this.toolOutputLimits);
|
|
6019
|
+
await results.save(run.state.storageKey, run.sessionId, handle, stored);
|
|
6020
|
+
return result as unknown as TJsonValue;
|
|
6021
|
+
}
|
|
6022
|
+
|
|
6023
|
+
/** The JSON bytes of one `results.get` page: the output budget, less room for the page's fields. */
|
|
6024
|
+
private resultPageBytes(): number {
|
|
6025
|
+
return Math.max(64, this.toolOutputLimits.maxBytes - 512);
|
|
6026
|
+
}
|
|
6027
|
+
|
|
6028
|
+
/** One page of a stored result of the run's session, or the error `results.get` reports. */
|
|
6029
|
+
private async readResult(
|
|
6030
|
+
run: IActiveRun,
|
|
6031
|
+
params: IResultRead,
|
|
6032
|
+
): Promise<{ ok: true; value: TJsonValue } | { ok: false; error: TJsonValue }> {
|
|
6033
|
+
const method = FLEX_RESULT_METHOD;
|
|
6034
|
+
const stored = await this.stores.results!.load(run.state.storageKey, run.sessionId, params.handle);
|
|
6035
|
+
if (stored === undefined) {
|
|
6036
|
+
return { ok: false, error: { error: 'unknown_handle', method, handle: params.handle, hint: 'A handle reads a result this session stored.' } };
|
|
6037
|
+
}
|
|
6038
|
+
const path = params.path ?? '';
|
|
6039
|
+
const value = selectPath(stored, path);
|
|
6040
|
+
if (value === undefined) {
|
|
6041
|
+
return { ok: false, error: { error: 'unknown_path', method, path, hint: `The result is ${describeShape(stored)}.` } };
|
|
6042
|
+
}
|
|
6043
|
+
const page = params.page ?? 1;
|
|
6044
|
+
const content = resultPage(value, path, page, this.resultPageBytes());
|
|
6045
|
+
if (!content) {
|
|
6046
|
+
const pages = resultPage(value, path, 1, this.resultPageBytes())!.pages;
|
|
6047
|
+
return { ok: false, error: { error: 'unknown_page', method, page, pages } };
|
|
6048
|
+
}
|
|
6049
|
+
return {
|
|
6050
|
+
ok: true,
|
|
6051
|
+
value: { handle: params.handle, path, page, pages: content.pages, ...content.range, value: content.value },
|
|
6052
|
+
};
|
|
6053
|
+
}
|
|
6054
|
+
|
|
6055
|
+
/**
|
|
6056
|
+
* Whether a call of the run's tools only reads: search_tools and describe_tools, an `execute` of
|
|
6057
|
+
* a method declared read-only, or a call the tool handle classifies as read-only by its real name.
|
|
6058
|
+
*/
|
|
6059
|
+
private isReadOnlyToolCall(run: IActiveRun, toolName: string, input: unknown): boolean {
|
|
6060
|
+
const names = FLEX_TOOL_CATALOG_TOOL_NAMES;
|
|
6061
|
+
const catalog = run.toolCatalog;
|
|
6062
|
+
if (catalog) {
|
|
6063
|
+
if (toolName === names.search || toolName === names.describe) return true;
|
|
6064
|
+
if (toolName === names.execute) {
|
|
6065
|
+
const target = catalog.callTarget(input);
|
|
6066
|
+
return target !== undefined && this.isReadOnlyCatalogCall(run, catalog, target.method, target.params);
|
|
6067
|
+
}
|
|
6068
|
+
if (toolName === FLEX_CODE_MODE_TOOL_NAME) return false;
|
|
6069
|
+
}
|
|
6070
|
+
if (this.enabledBuiltInToolNames(run).has(toolName)) return false;
|
|
6071
|
+
return this.classifyHostToolCall(run, toolName, input);
|
|
6072
|
+
}
|
|
6073
|
+
|
|
6074
|
+
/** The method's path when the prepared call only reads, for the turn's reads. */
|
|
6075
|
+
private readOnlyMethod(
|
|
6076
|
+
run: IActiveRun,
|
|
6077
|
+
catalog: FlexToolCatalog<unknown>,
|
|
6078
|
+
call: { method: string; params: unknown },
|
|
6079
|
+
): string | undefined {
|
|
6080
|
+
return this.isReadOnlyCatalogCall(run, catalog, call.method, call.params) ? call.method : undefined;
|
|
6081
|
+
}
|
|
6082
|
+
|
|
6083
|
+
/** A method whose `onCall` decides its calls is gated: its calls run in order, whatever they read. */
|
|
6084
|
+
private isReadOnlyCatalogCall(
|
|
6085
|
+
run: IActiveRun,
|
|
6086
|
+
catalog: FlexToolCatalog<unknown>,
|
|
6087
|
+
method: string,
|
|
6088
|
+
params: unknown,
|
|
6089
|
+
): boolean {
|
|
6090
|
+
return !catalog.decides(method)
|
|
6091
|
+
&& (catalog.isReadOnly(method) || this.classifyHostToolCall(run, method, params));
|
|
6092
|
+
}
|
|
6093
|
+
|
|
6094
|
+
private classifyHostToolCall(run: IActiveRun, toolName: string, input: unknown): boolean {
|
|
6095
|
+
try {
|
|
6096
|
+
return run.isReadOnlyHostCall?.({ toolName, input }) === true;
|
|
6097
|
+
} catch (error) {
|
|
6098
|
+
throw this.projectExternalError(run, error, 'toolProvider');
|
|
6099
|
+
}
|
|
6100
|
+
}
|
|
6101
|
+
|
|
6102
|
+
/**
|
|
6103
|
+
* Runs the run's tools through the turn's reads: a read-only call identical to an earlier read of
|
|
6104
|
+
* the turn shares its output, and any other call clears them. Records the confirmation of each
|
|
6105
|
+
* call that started while the model still streamed, which the permission requests naming the
|
|
6106
|
+
* call wait for. The catalog's tools read and run their methods by real name themselves.
|
|
6107
|
+
*/
|
|
6108
|
+
private trackToolCalls(run: IActiveRun, tools: TFlexAgentToolSet): TFlexAgentToolSet {
|
|
6109
|
+
const catalogTools = new Set<string>(run.toolCatalog
|
|
6110
|
+
? [...Object.values(FLEX_TOOL_CATALOG_TOOL_NAMES), FLEX_CODE_MODE_TOOL_NAME]
|
|
6111
|
+
: []);
|
|
6112
|
+
const tracked: Record<string, unknown> = {};
|
|
6113
|
+
for (const [name, tool] of Object.entries(tools)) {
|
|
6114
|
+
const execute = (tool as { execute?: (input: unknown, options: unknown) => unknown }).execute;
|
|
6115
|
+
if (typeof execute !== 'function') {
|
|
6116
|
+
tracked[name] = tool;
|
|
6117
|
+
continue;
|
|
6118
|
+
}
|
|
6119
|
+
tracked[name] = {
|
|
6120
|
+
...tool,
|
|
6121
|
+
execute: (input: unknown, options: plugins.IAgentToolExecutionOptions & { toolCallId?: string }) => {
|
|
6122
|
+
const confirmed = options?.confirmed;
|
|
6123
|
+
const toolCallId = options?.toolCallId ?? '';
|
|
6124
|
+
if (confirmed && toolCallId) {
|
|
6125
|
+
run.unconfirmedToolCalls.set(toolCallId, confirmed);
|
|
6126
|
+
const forget = () => run.unconfirmedToolCalls.delete(toolCallId);
|
|
6127
|
+
confirmed.then(forget, forget);
|
|
6128
|
+
}
|
|
6129
|
+
const executeTool = () => execute.call(tool, input, options);
|
|
6130
|
+
if (catalogTools.has(name)) return executeTool();
|
|
6131
|
+
const readName = this.isReadOnlyToolCall(run, name, input) ? name : undefined;
|
|
6132
|
+
return this.readOnce(run, toolCallId, readName, input, confirmed, executeTool);
|
|
6133
|
+
},
|
|
6134
|
+
};
|
|
6135
|
+
}
|
|
6136
|
+
return tracked as TFlexAgentToolSet;
|
|
6137
|
+
}
|
|
6138
|
+
|
|
6139
|
+
/**
|
|
6140
|
+
* Answers a read-only call (`name` set) from an identical read earlier in the turn, sharing the
|
|
6141
|
+
* output of one still running, or runs it and keeps its output for the reads after it. A failed
|
|
6142
|
+
* read, and a read whose response was discarded, are not kept. Any other call clears the turn's
|
|
6143
|
+
* reads before it runs.
|
|
6144
|
+
*/
|
|
6145
|
+
private readOnce(
|
|
6146
|
+
run: IActiveRun,
|
|
6147
|
+
toolCallId: string,
|
|
6148
|
+
name: string | undefined,
|
|
6149
|
+
params: unknown,
|
|
6150
|
+
confirmed: Promise<void> | undefined,
|
|
6151
|
+
execute: () => unknown,
|
|
6152
|
+
): unknown {
|
|
6153
|
+
if (name === undefined) {
|
|
6154
|
+
run.readCalls.clear();
|
|
6155
|
+
return execute();
|
|
6156
|
+
}
|
|
6157
|
+
const canonicalParams = canonicalJson(params);
|
|
6158
|
+
if (canonicalParams === undefined) return execute();
|
|
6159
|
+
const key = `${name}\n${canonicalParams}`;
|
|
6160
|
+
const earlier = run.readCalls.get(key);
|
|
6161
|
+
if (earlier) {
|
|
6162
|
+
return earlier.output.then((output) => {
|
|
6163
|
+
if (output === notShared) return execute();
|
|
6164
|
+
run.deduplicatedToolCalls.set(toolCallId, earlier.toolCallId);
|
|
6165
|
+
return output;
|
|
6166
|
+
}, (error: unknown) => {
|
|
6167
|
+
run.deduplicatedToolCalls.set(toolCallId, earlier.toolCallId);
|
|
6168
|
+
throw error;
|
|
6169
|
+
});
|
|
6170
|
+
}
|
|
6171
|
+
const result = execute();
|
|
6172
|
+
const entry: IRunReadCall = {
|
|
6173
|
+
toolCallId,
|
|
6174
|
+
output: Promise.resolve(result).then((output) => isAsyncIterable(output) ? notShared : output),
|
|
6175
|
+
};
|
|
6176
|
+
run.readCalls.set(key, entry);
|
|
6177
|
+
const forget = () => {
|
|
6178
|
+
if (run.readCalls.get(key) === entry) run.readCalls.delete(key);
|
|
6179
|
+
};
|
|
6180
|
+
entry.output.catch(forget);
|
|
6181
|
+
confirmed?.catch(forget);
|
|
6182
|
+
return result;
|
|
6183
|
+
}
|
|
6184
|
+
|
|
5938
6185
|
private enabledBuiltInToolNames(run: IActiveRun): Set<string> {
|
|
5939
6186
|
const names = new Set<string>();
|
|
5940
6187
|
if (run.subagents.size > 0 && (run.stored.session.depth ?? 0) < this.maxSubagentDepth) {
|
|
@@ -5995,15 +6242,35 @@ export class FlexHarness<TScope = unknown> {
|
|
|
5995
6242
|
),
|
|
5996
6243
|
execute: async (
|
|
5997
6244
|
input: { method: string; params?: Record<string, unknown> },
|
|
5998
|
-
options?: { toolCallId?: string; abortSignal?: AbortSignal },
|
|
6245
|
+
options?: { toolCallId?: string; abortSignal?: AbortSignal } & plugins.IAgentToolExecutionOptions,
|
|
5999
6246
|
) => {
|
|
6000
6247
|
const toolCallId = options?.toolCallId ?? '';
|
|
6001
6248
|
const abortSignal = options?.abortSignal ?? run.controller.signal;
|
|
6249
|
+
const confirmed = options?.confirmed;
|
|
6002
6250
|
const call = await catalog.prepareCall(input.method, input.params);
|
|
6003
6251
|
if (!call.success) throw this.catalogCallError(run, toolCallId, call.error);
|
|
6004
|
-
|
|
6252
|
+
if (call.method === FLEX_RESULT_METHOD) {
|
|
6253
|
+
const read = await this.readResult(run, call.params as IResultRead);
|
|
6254
|
+
if (!read.ok) throw this.catalogCallError(run, toolCallId, read.error);
|
|
6255
|
+
return read.value;
|
|
6256
|
+
}
|
|
6257
|
+
// A call that started early asks for permission once the response confirmed it.
|
|
6258
|
+
const base = run.toolProviderContext!;
|
|
6259
|
+
const provider = confirmed
|
|
6260
|
+
? Object.freeze({
|
|
6261
|
+
...base,
|
|
6262
|
+
requestPermission: async (request: IFlexPermissionRequestInput) => {
|
|
6263
|
+
await confirmed;
|
|
6264
|
+
return base.requestPermission(request);
|
|
6265
|
+
},
|
|
6266
|
+
})
|
|
6267
|
+
: base;
|
|
6268
|
+
const context = { method: call.method, toolCallId, abortSignal, provider };
|
|
6005
6269
|
const decision = await call.decide(context);
|
|
6006
|
-
if (decision === 'run')
|
|
6270
|
+
if (decision === 'run') {
|
|
6271
|
+
return this.readOnce(run, toolCallId, this.readOnlyMethod(run, catalog, call), call.params, confirmed,
|
|
6272
|
+
() => call.run(context));
|
|
6273
|
+
}
|
|
6007
6274
|
if (decision === 'propose') {
|
|
6008
6275
|
return catalog.propose(
|
|
6009
6276
|
[{ id: toolCallId, method: call.method, params: call.params }],
|
|
@@ -6075,51 +6342,97 @@ export class FlexHarness<TScope = unknown> {
|
|
|
6075
6342
|
});
|
|
6076
6343
|
const stopped = (method: string) => ({ ok: false as const, error: { error: 'aborted', method } });
|
|
6077
6344
|
let callNumber = 0;
|
|
6345
|
+
// Read-only calls run concurrently; any other call runs after every call the program made before it.
|
|
6346
|
+
const order = new plugins.AgentToolCallScheduler(abortSignal);
|
|
6078
6347
|
const finish = (id: string, method: string, input: unknown, outcome: { output: unknown } | { error: Error }) => {
|
|
6079
6348
|
if ('error' in outcome) run.trustedToolExecutionErrors.set(id, outcome.error);
|
|
6080
6349
|
this.onToolFinish(run.state, run.sessionId, 'error' in outcome
|
|
6081
6350
|
? { toolCallId: id, toolName: method, input, success: false, error: outcome.error.message }
|
|
6082
6351
|
: { toolCallId: id, toolName: method, input, success: true, output: outcome.output });
|
|
6083
6352
|
};
|
|
6353
|
+
const callInTurn = async (
|
|
6354
|
+
id: string,
|
|
6355
|
+
method: string,
|
|
6356
|
+
params: unknown,
|
|
6357
|
+
signal: AbortSignal,
|
|
6358
|
+
classification: { readOnly: boolean } | { error: Error },
|
|
6359
|
+
): Promise<TFlexCodeCallOutcome> => {
|
|
6360
|
+
const call = await catalog.prepareCall(method, params);
|
|
6361
|
+
if (!call.success) {
|
|
6362
|
+
if (catalog.has(method)) {
|
|
6363
|
+
this.onToolStart(run.state, run.sessionId, { toolCallId: id, toolName: method, input: params ?? {}, parentToolCallId: toolCallId });
|
|
6364
|
+
finish(id, method, params, { error: this.catalogCallError(run, id, call.error) });
|
|
6365
|
+
}
|
|
6366
|
+
return { ok: false, error: call.error as unknown as TJsonValue };
|
|
6367
|
+
}
|
|
6368
|
+
try {
|
|
6369
|
+
await order.waitForTurn(id, signal);
|
|
6370
|
+
} catch {
|
|
6371
|
+
return stopped(call.method);
|
|
6372
|
+
}
|
|
6373
|
+
if (signal.aborted) return stopped(call.method);
|
|
6374
|
+
this.onToolStart(run.state, run.sessionId, {
|
|
6375
|
+
toolCallId: id, toolName: call.method, input: call.params, parentToolCallId: toolCallId,
|
|
6376
|
+
});
|
|
6377
|
+
if ('error' in classification) {
|
|
6378
|
+
finish(id, call.method, call.params, { error: classification.error });
|
|
6379
|
+
return { ok: false, error: { error: 'failed', method: call.method, message: classification.error.message } };
|
|
6380
|
+
}
|
|
6381
|
+
if (call.method === FLEX_RESULT_METHOD) {
|
|
6382
|
+
const read = await this.readResult(run, call.params as IResultRead);
|
|
6383
|
+
finish(id, call.method, call.params, read.ok
|
|
6384
|
+
? { output: read.value }
|
|
6385
|
+
: { error: this.catalogCallError(run, id, read.error) });
|
|
6386
|
+
return read;
|
|
6387
|
+
}
|
|
6388
|
+
const readOnly = classification.readOnly;
|
|
6389
|
+
const context = { method: call.method, toolCallId: id, abortSignal: signal, provider: providerFor(signal) };
|
|
6390
|
+
try {
|
|
6391
|
+
const decision = await call.decide(context);
|
|
6392
|
+
signal.throwIfAborted();
|
|
6393
|
+
if (decision === 'propose') {
|
|
6394
|
+
proposals.push({ id, method: call.method, params: call.params });
|
|
6395
|
+
const value = { proposed: true, id };
|
|
6396
|
+
finish(id, call.method, call.params, { output: value });
|
|
6397
|
+
return { ok: true, value };
|
|
6398
|
+
}
|
|
6399
|
+
if (decision !== 'run') {
|
|
6400
|
+
const payload = { error: 'denied' as const, method: call.method, reason: decision.deny };
|
|
6401
|
+
finish(id, call.method, call.params, { error: this.catalogCallError(run, id, payload) });
|
|
6402
|
+
return { ok: false, error: payload };
|
|
6403
|
+
}
|
|
6404
|
+
const output = this.readOnce(run, id, readOnly ? call.method : undefined, call.params, undefined,
|
|
6405
|
+
() => call.run(context));
|
|
6406
|
+
const value = await this.budgetOutput(run, await settleToolOutput(output));
|
|
6407
|
+
finish(id, call.method, call.params, { output: value });
|
|
6408
|
+
return { ok: true, value };
|
|
6409
|
+
} catch (error) {
|
|
6410
|
+
const projected = this.projectExternalError(run, error, 'toolExecution');
|
|
6411
|
+
finish(id, call.method, call.params, { error: projected });
|
|
6412
|
+
return { ok: false, error: { error: 'failed', method: call.method, message: projected.message } };
|
|
6413
|
+
}
|
|
6414
|
+
};
|
|
6084
6415
|
const host: IFlexCodeHost = {
|
|
6085
6416
|
waitedMs: () => waitedMs + (waiting > 0 ? performance.now() - waitingSince : 0),
|
|
6086
6417
|
call: async (method, params, signal) => {
|
|
6087
6418
|
if (signal.aborted) return stopped(method);
|
|
6088
6419
|
const id = `${toolCallId}.${++callNumber}`;
|
|
6089
|
-
|
|
6090
|
-
|
|
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) };
|
|
6420
|
+
// The call takes its place in the program's order now, before anything is awaited.
|
|
6421
|
+
let classification: { readOnly: boolean } | { error: Error };
|
|
6102
6422
|
try {
|
|
6103
|
-
|
|
6104
|
-
|
|
6105
|
-
|
|
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 };
|
|
6423
|
+
classification = {
|
|
6424
|
+
readOnly: catalog.has(method) && this.isReadOnlyCatalogCall(run, catalog, method, params ?? {}),
|
|
6425
|
+
};
|
|
6119
6426
|
} catch (error) {
|
|
6120
|
-
|
|
6121
|
-
|
|
6122
|
-
|
|
6427
|
+
classification = {
|
|
6428
|
+
error: error instanceof Error ? error : this.projectExternalError(run, error, 'toolProvider'),
|
|
6429
|
+
};
|
|
6430
|
+
}
|
|
6431
|
+
order.begin(id, 'readOnly' in classification && classification.readOnly);
|
|
6432
|
+
try {
|
|
6433
|
+
return await callInTurn(id, method, params, signal, classification);
|
|
6434
|
+
} finally {
|
|
6435
|
+
order.settle(id);
|
|
6123
6436
|
}
|
|
6124
6437
|
},
|
|
6125
6438
|
};
|
|
@@ -8698,6 +9011,10 @@ export class FlexHarness<TScope = unknown> {
|
|
|
8698
9011
|
delete part.output;
|
|
8699
9012
|
part.error = projectedError!.message;
|
|
8700
9013
|
}
|
|
9014
|
+
const original = run.deduplicatedToolCalls.get(event.toolCallId);
|
|
9015
|
+
if (original !== undefined) {
|
|
9016
|
+
part.deduplicatedFrom = this.boundedIdentifier(original, 'deduplicatedFrom');
|
|
9017
|
+
}
|
|
8701
9018
|
part.completedAt = new Date().toISOString();
|
|
8702
9019
|
this.emitPartEvent(run, 'part.completed', part);
|
|
8703
9020
|
} catch (error) {
|
|
@@ -9362,10 +9679,17 @@ export class FlexHarness<TScope = unknown> {
|
|
|
9362
9679
|
if (catalog !== undefined && (typeof catalog !== 'object' || catalog === null)) {
|
|
9363
9680
|
throw new FlexHarnessValidationError('Tool handle catalog must be an object.');
|
|
9364
9681
|
}
|
|
9682
|
+
const isReadOnlyToolCall = handle.isReadOnlyToolCall;
|
|
9683
|
+
if (isReadOnlyToolCall !== undefined && typeof isReadOnlyToolCall !== 'function') {
|
|
9684
|
+
throw new FlexHarnessValidationError('Tool handle isReadOnlyToolCall must be a function.');
|
|
9685
|
+
}
|
|
9365
9686
|
const closeHandle = close;
|
|
9366
9687
|
return {
|
|
9367
9688
|
tools,
|
|
9368
9689
|
...(catalog === undefined ? {} : { catalog }),
|
|
9690
|
+
...(isReadOnlyToolCall === undefined ? {} : {
|
|
9691
|
+
isReadOnlyToolCall: (call: { toolName: string; input: unknown }) => isReadOnlyToolCall.call(handle, call),
|
|
9692
|
+
}),
|
|
9369
9693
|
...(closeHandle ? { close: () => closeHandle.call(handle) } : {}),
|
|
9370
9694
|
};
|
|
9371
9695
|
} catch (error) {
|
|
@@ -9394,6 +9718,8 @@ export class FlexHarness<TScope = unknown> {
|
|
|
9394
9718
|
const descriptors = this.normalizeResourceToolProviderDescriptors(resolved);
|
|
9395
9719
|
const owned: Array<{ handle: IFlexToolHandle; closed: boolean }> = [];
|
|
9396
9720
|
const tools: Record<string, unknown> = Object.create(null) as Record<string, unknown>;
|
|
9721
|
+
/** The handle and own name of each exposed resource tool, for the handle's classifier. */
|
|
9722
|
+
const resourceTools = new Map<string, { handle: IFlexToolHandle; toolName: string }>();
|
|
9397
9723
|
const closeOwned = async (): Promise<void> => {
|
|
9398
9724
|
const errors: unknown[] = [];
|
|
9399
9725
|
for (const entry of [...owned].reverse()) {
|
|
@@ -9461,12 +9787,23 @@ export class FlexHarness<TScope = unknown> {
|
|
|
9461
9787
|
throw new FlexHarnessValidationError(`Duplicate exposed tool "${exposedName}".`);
|
|
9462
9788
|
}
|
|
9463
9789
|
tools[exposedName] = tool;
|
|
9790
|
+
resourceTools.set(exposedName, { handle, toolName });
|
|
9464
9791
|
}
|
|
9465
9792
|
}
|
|
9466
9793
|
if (owned.length === 0 && Object.keys(tools).length === 0) return undefined;
|
|
9794
|
+
const classifies = applicationHandle?.isReadOnlyToolCall !== undefined
|
|
9795
|
+
|| [...resourceTools.values()].some((entry) => entry.handle.isReadOnlyToolCall !== undefined);
|
|
9467
9796
|
return {
|
|
9468
9797
|
tools: tools as TFlexAgentToolSet,
|
|
9469
9798
|
...(applicationHandle?.catalog === undefined ? {} : { catalog: applicationHandle.catalog }),
|
|
9799
|
+
...(classifies ? {
|
|
9800
|
+
isReadOnlyToolCall: ({ toolName, input }: { toolName: string; input: unknown }) => {
|
|
9801
|
+
const resource = resourceTools.get(toolName);
|
|
9802
|
+
return resource
|
|
9803
|
+
? resource.handle.isReadOnlyToolCall?.({ toolName: resource.toolName, input }) === true
|
|
9804
|
+
: applicationHandle?.isReadOnlyToolCall?.({ toolName, input }) === true;
|
|
9805
|
+
},
|
|
9806
|
+
} : {}),
|
|
9470
9807
|
...(owned.some((entry) => entry.handle.close) ? { close: closeOwned } : {}),
|
|
9471
9808
|
};
|
|
9472
9809
|
} catch (error) {
|
|
@@ -10911,6 +11248,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
10911
11248
|
this.stores.jobs.deleteSession(storageKey, sessionId),
|
|
10912
11249
|
this.stores.projections.deleteSession(storageKey, sessionId),
|
|
10913
11250
|
this.stores.permissions.deleteSession(storageKey, sessionId),
|
|
11251
|
+
...(this.stores.results ? [this.stores.results.deleteSession(storageKey, sessionId)] : []),
|
|
10914
11252
|
// An imported session refuses every project-management write, so it has no state to fence.
|
|
10915
11253
|
...(tombstone.imported ? [] : [projectManagementCleanup()]),
|
|
10916
11254
|
]);
|