@modelprofile.com/flexharness 10.15.1 → 10.17.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 +36 -0
- package/dist_ts/classes.flexharness.js +360 -56
- package/dist_ts/classes.stores.d.ts +3 -1
- package/dist_ts/classes.stores.js +92 -2
- package/dist_ts/classes.toolcatalog.d.ts +18 -1
- package/dist_ts/classes.toolcatalog.js +179 -18
- package/dist_ts/interfaces.d.ts +67 -0
- package/dist_ts/interfaces.js +12 -2
- 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.storerules.d.ts +5 -0
- package/dist_ts/utils.storerules.js +10 -1
- package/dist_ts/utils.tooloutput.d.ts +5 -2
- package/dist_ts/utils.tooloutput.js +10 -3
- package/dist_ts_stores_nosqldb/classes.nosqldbstores.d.ts +2 -1
- package/dist_ts_stores_nosqldb/classes.nosqldbstores.js +63 -5
- package/dist_ts_stores_nosqldb/classes.recordstore.d.ts +6 -3
- package/dist_ts_stores_nosqldb/classes.recordstore.js +8 -2
- package/dist_ts_typedrequest/catalog.d.ts +28 -0
- package/dist_ts_typedrequest/catalog.js +79 -0
- package/dist_ts_typedrequest/describe.d.ts +10 -0
- package/dist_ts_typedrequest/describe.js +189 -0
- package/dist_ts_typedrequest/index.d.ts +3 -0
- package/dist_ts_typedrequest/index.js +4 -0
- package/dist_ts_typedrequest/interfaces.d.ts +107 -0
- package/dist_ts_typedrequest/interfaces.js +2 -0
- package/dist_ts_typedrequest/plugins.d.ts +7 -0
- package/dist_ts_typedrequest/plugins.js +11 -0
- package/package.json +14 -4
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.codesandbox.ts +5 -1
- package/ts/classes.flexharness.ts +419 -55
- package/ts/classes.stores.ts +109 -0
- package/ts/classes.toolcatalog.ts +177 -14
- package/ts/interfaces.ts +69 -1
- package/ts/plugins.ts +3 -0
- package/ts/utils.json.ts +30 -0
- package/ts/utils.results.ts +164 -0
- package/ts/utils.storerules.ts +15 -0
- package/ts/utils.tooloutput.ts +9 -2
- package/ts_stores_nosqldb/classes.nosqldbstores.ts +68 -2
- package/ts_stores_nosqldb/classes.recordstore.ts +14 -4
- package/ts_typedrequest/catalog.ts +91 -0
- package/ts_typedrequest/describe.ts +211 -0
- package/ts_typedrequest/index.ts +3 -0
- package/ts_typedrequest/interfaces.ts +124 -0
- package/ts_typedrequest/plugins.ts +15 -0
- package/ts_typedrequest/readme.md +66 -0
|
@@ -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,38 @@ 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
|
+
|
|
500
|
+
/** Runs `operation` after the operations queued before it under `key`, whatever their outcome. */
|
|
501
|
+
function queueOperation<TResult>(
|
|
502
|
+
queues: Map<string, Promise<void>>,
|
|
503
|
+
key: string,
|
|
504
|
+
operation: () => Promise<TResult>,
|
|
505
|
+
): Promise<TResult> {
|
|
506
|
+
const previous = queues.get(key) ?? Promise.resolve();
|
|
507
|
+
const result = previous.then(operation);
|
|
508
|
+
const barrier = result.then(() => undefined, () => undefined);
|
|
509
|
+
queues.set(key, barrier);
|
|
510
|
+
void barrier.then(() => {
|
|
511
|
+
if (queues.get(key) === barrier) queues.delete(key);
|
|
512
|
+
});
|
|
513
|
+
return result;
|
|
514
|
+
}
|
|
515
|
+
|
|
465
516
|
/** The steers of a run: the input its generation takes in, and the prompts it accepted for it. */
|
|
466
517
|
interface IRunSteering {
|
|
467
518
|
readonly input: plugins.AgentPendingInputQueue;
|
|
@@ -893,6 +944,8 @@ export class FlexHarness<TScope = unknown> {
|
|
|
893
944
|
private readonly orphanedTombstoneOwners = new Map<string, IOrphanedTombstoneOwner<TScope>>();
|
|
894
945
|
private readonly pendingPromptAdmissionOwners = new Set<IPendingPromptAdmission>();
|
|
895
946
|
private readonly projectManagementQueues = new Map<string, Promise<void>>();
|
|
947
|
+
/** A session's result writes, one at a time: they share the session's index of results. */
|
|
948
|
+
private readonly resultQueues = new Map<string, Promise<void>>();
|
|
896
949
|
private readonly activeSlashCommandExecutions = new Map<string, IActiveSlashCommandExecution>();
|
|
897
950
|
private readonly activeSlashCommandListings = new Set<IActiveSlashCommandListing>();
|
|
898
951
|
private readonly stagedSessionImports = new Map<string, IStagedSessionImport>();
|
|
@@ -4447,6 +4500,9 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4447
4500
|
reasoningPartIds: new Map(),
|
|
4448
4501
|
toolPartIds: new Map(),
|
|
4449
4502
|
trustedToolExecutionErrors: new Map(),
|
|
4503
|
+
unconfirmedToolCalls: new Map(),
|
|
4504
|
+
readCalls: new Map(),
|
|
4505
|
+
deduplicatedToolCalls: new Map(),
|
|
4450
4506
|
subagentCallCount: 0,
|
|
4451
4507
|
subagentSessionIds: new Set(),
|
|
4452
4508
|
backgroundSubagents: new Set(),
|
|
@@ -5280,17 +5336,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
5280
5336
|
sessionId: string,
|
|
5281
5337
|
operation: () => Promise<TResult>,
|
|
5282
5338
|
): Promise<TResult> {
|
|
5283
|
-
|
|
5284
|
-
const previous = this.projectManagementQueues.get(key) ?? Promise.resolve();
|
|
5285
|
-
const result = previous.then(operation);
|
|
5286
|
-
const barrier = result.then(() => undefined, () => undefined);
|
|
5287
|
-
this.projectManagementQueues.set(key, barrier);
|
|
5288
|
-
void barrier.then(() => {
|
|
5289
|
-
if (this.projectManagementQueues.get(key) === barrier) {
|
|
5290
|
-
this.projectManagementQueues.delete(key);
|
|
5291
|
-
}
|
|
5292
|
-
});
|
|
5293
|
-
return result;
|
|
5339
|
+
return queueOperation(this.projectManagementQueues, this.projectManagementKey(storageKey, sessionId), operation);
|
|
5294
5340
|
}
|
|
5295
5341
|
|
|
5296
5342
|
private async awaitProjectManagementOperations(
|
|
@@ -5781,8 +5827,14 @@ export class FlexHarness<TScope = unknown> {
|
|
|
5781
5827
|
runId: run.runId,
|
|
5782
5828
|
...resolverRelationship,
|
|
5783
5829
|
signal,
|
|
5784
|
-
requestPermission: (request: IFlexPermissionRequestInput) =>
|
|
5785
|
-
|
|
5830
|
+
requestPermission: async (request: IFlexPermissionRequestInput) => {
|
|
5831
|
+
// A call that started early asks once the model's response confirmed it.
|
|
5832
|
+
const confirmed = request.toolCallId === undefined
|
|
5833
|
+
? undefined
|
|
5834
|
+
: run.unconfirmedToolCalls.get(request.toolCallId);
|
|
5835
|
+
if (confirmed) await confirmed;
|
|
5836
|
+
return this.requestPermission(run.state, run, request);
|
|
5837
|
+
},
|
|
5786
5838
|
});
|
|
5787
5839
|
const toolOutcome = Promise.resolve()
|
|
5788
5840
|
.then(() => this.provideRunToolHandle(run, toolProviderContext))
|
|
@@ -5851,10 +5903,13 @@ export class FlexHarness<TScope = unknown> {
|
|
|
5851
5903
|
signal.throwIfAborted();
|
|
5852
5904
|
run.modelResolution = model!;
|
|
5853
5905
|
run.toolProviderContext = toolProviderContext as Readonly<IFlexToolProviderContext<unknown>>;
|
|
5906
|
+
run.isReadOnlyHostCall = toolHandle?.isReadOnlyToolCall;
|
|
5854
5907
|
let catalog: FlexToolCatalog<unknown> | undefined;
|
|
5855
5908
|
if (toolHandle?.catalog) {
|
|
5856
5909
|
try {
|
|
5857
|
-
catalog = await FlexToolCatalog.create(toolHandle.catalog, this.toolCatalogOptions
|
|
5910
|
+
catalog = await FlexToolCatalog.create(toolHandle.catalog, this.toolCatalogOptions, {
|
|
5911
|
+
results: this.stores.results !== undefined,
|
|
5912
|
+
});
|
|
5858
5913
|
if (catalog.codeMode) run.codeSandbox = await FlexCodeSandbox.load();
|
|
5859
5914
|
} catch (error) {
|
|
5860
5915
|
throw this.projectExternalError(run, error, 'toolProvider');
|
|
@@ -5877,17 +5932,18 @@ export class FlexHarness<TScope = unknown> {
|
|
|
5877
5932
|
: Object.fromEntries(Object.entries(combinedTools).filter(([name]) => admitted(name)));
|
|
5878
5933
|
const admittedCatalog = catalog && (globalNames === undefined && agentNames === undefined
|
|
5879
5934
|
? catalog
|
|
5880
|
-
: catalog.restrict(catalog.methods.filter(admitted)));
|
|
5935
|
+
: catalog.restrict(catalog.methods.filter((name) => name === FLEX_RESULT_METHOD || admitted(name))));
|
|
5881
5936
|
if (admittedCatalog && admittedCatalog.methods.length > 0) {
|
|
5882
5937
|
run.toolCatalog = admittedCatalog;
|
|
5883
5938
|
Object.assign(admittedTools, this.createCatalogTools(run, admittedCatalog));
|
|
5884
5939
|
}
|
|
5885
5940
|
tools = Object.keys(admittedTools).length > 0
|
|
5886
|
-
? wrapToolSet(
|
|
5941
|
+
? this.trackToolCalls(run, wrapToolSet(
|
|
5887
5942
|
admittedTools as TFlexAgentToolSet,
|
|
5888
5943
|
this.toolOutputLimits,
|
|
5889
5944
|
(toolError) => this.projectExternalError(run, toolError, 'toolExecution'),
|
|
5890
|
-
|
|
5945
|
+
(output) => this.budgetOutput(run, output),
|
|
5946
|
+
))
|
|
5891
5947
|
: undefined;
|
|
5892
5948
|
} catch (error) {
|
|
5893
5949
|
throw this.projectExternalError(run, error, 'toolProvider');
|
|
@@ -5915,7 +5971,11 @@ export class FlexHarness<TScope = unknown> {
|
|
|
5915
5971
|
return {
|
|
5916
5972
|
model: model!.model,
|
|
5917
5973
|
...(options.system ?? model!.system ? { system: options.system ?? model!.system } : {}),
|
|
5918
|
-
...(tools ? {
|
|
5974
|
+
...(tools ? {
|
|
5975
|
+
tools,
|
|
5976
|
+
isReadOnlyToolCall: ({ toolName, input }: { toolName: string; input: unknown }) =>
|
|
5977
|
+
this.isReadOnlyToolCall(run, toolName, input),
|
|
5978
|
+
} : {}),
|
|
5919
5979
|
...(model!.providerOptions ? { providerOptions: model!.providerOptions } : {}),
|
|
5920
5980
|
...(model!.cache === undefined ? {} : { cache: model!.cache }),
|
|
5921
5981
|
...(options.maxSteps ?? model!.maxSteps ? { maxSteps: options.maxSteps ?? model!.maxSteps } : {}),
|
|
@@ -5935,6 +5995,213 @@ export class FlexHarness<TScope = unknown> {
|
|
|
5935
5995
|
}
|
|
5936
5996
|
}
|
|
5937
5997
|
|
|
5998
|
+
/**
|
|
5999
|
+
* The output the model gets: the output itself within the output budget, or, in a run with a
|
|
6000
|
+
* catalog whose stores keep results, a handle to the stored output with a summary of it.
|
|
6001
|
+
*/
|
|
6002
|
+
private async budgetOutput(run: IActiveRun, output: unknown): Promise<TJsonValue> {
|
|
6003
|
+
const results = this.stores.results;
|
|
6004
|
+
const maxBytes = this.toolOutputLimits.maxBytes;
|
|
6005
|
+
if (!results || !run.toolCatalog) return normalizeJsonValue(output, this.toolOutputLimits);
|
|
6006
|
+
const stored = normalizeJsonValue(output, {
|
|
6007
|
+
...this.toolOutputLimits,
|
|
6008
|
+
maxBytes: Math.max(maxBytes, FLEX_RESULT_LIMITS.maxStoredBytes),
|
|
6009
|
+
});
|
|
6010
|
+
const bytes = jsonBytes(stored);
|
|
6011
|
+
if (bytes <= maxBytes) return stored;
|
|
6012
|
+
const handle = `res_${plugins.crypto.randomBytes(8).toString('hex')}`;
|
|
6013
|
+
const read = `Too large to return whole: read it page by page with execute({ method: "${FLEX_RESULT_METHOD}", `
|
|
6014
|
+
+ `params: { handle: "${handle}", page: 1 } })${run.toolCatalog.codeMode
|
|
6015
|
+
? `, or in run_code with await api.results.get("${handle}", { page: 1 })`
|
|
6016
|
+
: ''}; a dotted \`path\` reads a part of it.`;
|
|
6017
|
+
const result: IFlexResultHandle = {
|
|
6018
|
+
$flexType: 'result',
|
|
6019
|
+
handle,
|
|
6020
|
+
bytes,
|
|
6021
|
+
pages: resultPage(stored, '', 1, this.resultPageBytes())!.pages,
|
|
6022
|
+
summary: summarizeResult(stored, Math.min(FLEX_RESULT_LIMITS.summaryBytes, Math.floor(maxBytes / 4))),
|
|
6023
|
+
read,
|
|
6024
|
+
};
|
|
6025
|
+
// A budget too small for even the handle keeps the truncated output.
|
|
6026
|
+
if (jsonBytes(result) > maxBytes) return normalizeJsonValue(output, this.toolOutputLimits);
|
|
6027
|
+
const { storageKey } = run.state;
|
|
6028
|
+
await queueOperation(this.resultQueues, this.resultQueueKey(storageKey, run.sessionId), async () => {
|
|
6029
|
+
await results.save(storageKey, run.sessionId, handle, stored);
|
|
6030
|
+
// The session keeps its latest results; older ones are deleted.
|
|
6031
|
+
const handles = await results.list(storageKey, run.sessionId);
|
|
6032
|
+
for (const old of handles.slice(0, Math.max(0, handles.length - FLEX_RESULT_LIMITS.maxResultsPerSession))) {
|
|
6033
|
+
await results.delete(storageKey, run.sessionId, old);
|
|
6034
|
+
}
|
|
6035
|
+
});
|
|
6036
|
+
return result as unknown as TJsonValue;
|
|
6037
|
+
}
|
|
6038
|
+
|
|
6039
|
+
private resultQueueKey(storageKey: string, sessionId: string): string {
|
|
6040
|
+
return JSON.stringify([storageKey, sessionId]);
|
|
6041
|
+
}
|
|
6042
|
+
|
|
6043
|
+
/** The JSON bytes of one `results.get` page: the output budget, less room for the page's fields. */
|
|
6044
|
+
private resultPageBytes(): number {
|
|
6045
|
+
return Math.max(64, this.toolOutputLimits.maxBytes - 512);
|
|
6046
|
+
}
|
|
6047
|
+
|
|
6048
|
+
/** One page of a stored result of the run's session, or the error `results.get` reports. */
|
|
6049
|
+
private async readResult(
|
|
6050
|
+
run: IActiveRun,
|
|
6051
|
+
params: IResultRead,
|
|
6052
|
+
): Promise<{ ok: true; value: TJsonValue } | { ok: false; error: TJsonValue }> {
|
|
6053
|
+
const method = FLEX_RESULT_METHOD;
|
|
6054
|
+
const stored = await this.stores.results!.load(run.state.storageKey, run.sessionId, params.handle);
|
|
6055
|
+
if (stored === undefined) {
|
|
6056
|
+
return { ok: false, error: { error: 'unknown_handle', method, handle: params.handle, hint: `A handle reads one of the latest ${FLEX_RESULT_LIMITS.maxResultsPerSession} results this session stored.` } };
|
|
6057
|
+
}
|
|
6058
|
+
const path = params.path ?? '';
|
|
6059
|
+
const value = selectPath(stored, path);
|
|
6060
|
+
if (value === undefined) {
|
|
6061
|
+
return { ok: false, error: { error: 'unknown_path', method, path, hint: `The result is ${describeShape(stored)}.` } };
|
|
6062
|
+
}
|
|
6063
|
+
const page = params.page ?? 1;
|
|
6064
|
+
const content = resultPage(value, path, page, this.resultPageBytes());
|
|
6065
|
+
if (!content) {
|
|
6066
|
+
const pages = resultPage(value, path, 1, this.resultPageBytes())!.pages;
|
|
6067
|
+
return { ok: false, error: { error: 'unknown_page', method, page, pages } };
|
|
6068
|
+
}
|
|
6069
|
+
return {
|
|
6070
|
+
ok: true,
|
|
6071
|
+
value: { handle: params.handle, path, page, pages: content.pages, ...content.range, value: content.value },
|
|
6072
|
+
};
|
|
6073
|
+
}
|
|
6074
|
+
|
|
6075
|
+
/**
|
|
6076
|
+
* Whether a call of the run's tools only reads: search_tools and describe_tools, an `execute` of
|
|
6077
|
+
* a method declared read-only, or a call the tool handle classifies as read-only by its real name.
|
|
6078
|
+
*/
|
|
6079
|
+
private isReadOnlyToolCall(run: IActiveRun, toolName: string, input: unknown): boolean {
|
|
6080
|
+
const names = FLEX_TOOL_CATALOG_TOOL_NAMES;
|
|
6081
|
+
const catalog = run.toolCatalog;
|
|
6082
|
+
if (catalog) {
|
|
6083
|
+
if (toolName === names.search || toolName === names.describe) return true;
|
|
6084
|
+
if (toolName === names.execute) {
|
|
6085
|
+
const target = catalog.callTarget(input);
|
|
6086
|
+
return target !== undefined && this.isReadOnlyCatalogCall(run, catalog, target.method, target.params);
|
|
6087
|
+
}
|
|
6088
|
+
if (toolName === FLEX_CODE_MODE_TOOL_NAME) return false;
|
|
6089
|
+
}
|
|
6090
|
+
if (this.enabledBuiltInToolNames(run).has(toolName)) return false;
|
|
6091
|
+
return this.classifyHostToolCall(run, toolName, input);
|
|
6092
|
+
}
|
|
6093
|
+
|
|
6094
|
+
/** The method's path when the prepared call only reads, for the turn's reads. */
|
|
6095
|
+
private readOnlyMethod(
|
|
6096
|
+
run: IActiveRun,
|
|
6097
|
+
catalog: FlexToolCatalog<unknown>,
|
|
6098
|
+
call: { method: string; params: unknown },
|
|
6099
|
+
): string | undefined {
|
|
6100
|
+
return this.isReadOnlyCatalogCall(run, catalog, call.method, call.params) ? call.method : undefined;
|
|
6101
|
+
}
|
|
6102
|
+
|
|
6103
|
+
/** A method whose `onCall` decides its calls is gated: its calls run in order, whatever they read. */
|
|
6104
|
+
private isReadOnlyCatalogCall(
|
|
6105
|
+
run: IActiveRun,
|
|
6106
|
+
catalog: FlexToolCatalog<unknown>,
|
|
6107
|
+
method: string,
|
|
6108
|
+
params: unknown,
|
|
6109
|
+
): boolean {
|
|
6110
|
+
return !catalog.decides(method)
|
|
6111
|
+
&& (catalog.isReadOnly(method) || this.classifyHostToolCall(run, method, params));
|
|
6112
|
+
}
|
|
6113
|
+
|
|
6114
|
+
private classifyHostToolCall(run: IActiveRun, toolName: string, input: unknown): boolean {
|
|
6115
|
+
try {
|
|
6116
|
+
return run.isReadOnlyHostCall?.({ toolName, input }) === true;
|
|
6117
|
+
} catch (error) {
|
|
6118
|
+
throw this.projectExternalError(run, error, 'toolProvider');
|
|
6119
|
+
}
|
|
6120
|
+
}
|
|
6121
|
+
|
|
6122
|
+
/**
|
|
6123
|
+
* Runs the run's tools through the turn's reads: a read-only call identical to an earlier read of
|
|
6124
|
+
* the turn shares its output, and any other call clears them. Records the confirmation of each
|
|
6125
|
+
* call that started while the model still streamed, which the permission requests naming the
|
|
6126
|
+
* call wait for. The catalog's tools read and run their methods by real name themselves.
|
|
6127
|
+
*/
|
|
6128
|
+
private trackToolCalls(run: IActiveRun, tools: TFlexAgentToolSet): TFlexAgentToolSet {
|
|
6129
|
+
const catalogTools = new Set<string>(run.toolCatalog
|
|
6130
|
+
? [...Object.values(FLEX_TOOL_CATALOG_TOOL_NAMES), FLEX_CODE_MODE_TOOL_NAME]
|
|
6131
|
+
: []);
|
|
6132
|
+
const tracked: Record<string, unknown> = {};
|
|
6133
|
+
for (const [name, tool] of Object.entries(tools)) {
|
|
6134
|
+
const execute = (tool as { execute?: (input: unknown, options: unknown) => unknown }).execute;
|
|
6135
|
+
if (typeof execute !== 'function') {
|
|
6136
|
+
tracked[name] = tool;
|
|
6137
|
+
continue;
|
|
6138
|
+
}
|
|
6139
|
+
tracked[name] = {
|
|
6140
|
+
...tool,
|
|
6141
|
+
execute: (input: unknown, options: plugins.IAgentToolExecutionOptions & { toolCallId?: string }) => {
|
|
6142
|
+
const confirmed = options?.confirmed;
|
|
6143
|
+
const toolCallId = options?.toolCallId ?? '';
|
|
6144
|
+
if (confirmed && toolCallId) {
|
|
6145
|
+
run.unconfirmedToolCalls.set(toolCallId, confirmed);
|
|
6146
|
+
const forget = () => run.unconfirmedToolCalls.delete(toolCallId);
|
|
6147
|
+
confirmed.then(forget, forget);
|
|
6148
|
+
}
|
|
6149
|
+
const executeTool = () => execute.call(tool, input, options);
|
|
6150
|
+
if (catalogTools.has(name)) return executeTool();
|
|
6151
|
+
const readName = this.isReadOnlyToolCall(run, name, input) ? name : undefined;
|
|
6152
|
+
return this.readOnce(run, toolCallId, readName, input, confirmed, executeTool);
|
|
6153
|
+
},
|
|
6154
|
+
};
|
|
6155
|
+
}
|
|
6156
|
+
return tracked as TFlexAgentToolSet;
|
|
6157
|
+
}
|
|
6158
|
+
|
|
6159
|
+
/**
|
|
6160
|
+
* Answers a read-only call (`name` set) from an identical read earlier in the turn, sharing the
|
|
6161
|
+
* output of one still running, or runs it and keeps its output for the reads after it. A failed
|
|
6162
|
+
* read, and a read whose response was discarded, are not kept. Any other call clears the turn's
|
|
6163
|
+
* reads before it runs.
|
|
6164
|
+
*/
|
|
6165
|
+
private readOnce(
|
|
6166
|
+
run: IActiveRun,
|
|
6167
|
+
toolCallId: string,
|
|
6168
|
+
name: string | undefined,
|
|
6169
|
+
params: unknown,
|
|
6170
|
+
confirmed: Promise<void> | undefined,
|
|
6171
|
+
execute: () => unknown,
|
|
6172
|
+
): unknown {
|
|
6173
|
+
if (name === undefined) {
|
|
6174
|
+
run.readCalls.clear();
|
|
6175
|
+
return execute();
|
|
6176
|
+
}
|
|
6177
|
+
const canonicalParams = canonicalJson(params);
|
|
6178
|
+
if (canonicalParams === undefined) return execute();
|
|
6179
|
+
const key = `${name}\n${canonicalParams}`;
|
|
6180
|
+
const earlier = run.readCalls.get(key);
|
|
6181
|
+
if (earlier) {
|
|
6182
|
+
return earlier.output.then((output) => {
|
|
6183
|
+
if (output === notShared) return execute();
|
|
6184
|
+
run.deduplicatedToolCalls.set(toolCallId, earlier.toolCallId);
|
|
6185
|
+
return output;
|
|
6186
|
+
}, (error: unknown) => {
|
|
6187
|
+
run.deduplicatedToolCalls.set(toolCallId, earlier.toolCallId);
|
|
6188
|
+
throw error;
|
|
6189
|
+
});
|
|
6190
|
+
}
|
|
6191
|
+
const result = execute();
|
|
6192
|
+
const entry: IRunReadCall = {
|
|
6193
|
+
toolCallId,
|
|
6194
|
+
output: Promise.resolve(result).then((output) => isAsyncIterable(output) ? notShared : output),
|
|
6195
|
+
};
|
|
6196
|
+
run.readCalls.set(key, entry);
|
|
6197
|
+
const forget = () => {
|
|
6198
|
+
if (run.readCalls.get(key) === entry) run.readCalls.delete(key);
|
|
6199
|
+
};
|
|
6200
|
+
entry.output.catch(forget);
|
|
6201
|
+
confirmed?.catch(forget);
|
|
6202
|
+
return result;
|
|
6203
|
+
}
|
|
6204
|
+
|
|
5938
6205
|
private enabledBuiltInToolNames(run: IActiveRun): Set<string> {
|
|
5939
6206
|
const names = new Set<string>();
|
|
5940
6207
|
if (run.subagents.size > 0 && (run.stored.session.depth ?? 0) < this.maxSubagentDepth) {
|
|
@@ -5995,15 +6262,35 @@ export class FlexHarness<TScope = unknown> {
|
|
|
5995
6262
|
),
|
|
5996
6263
|
execute: async (
|
|
5997
6264
|
input: { method: string; params?: Record<string, unknown> },
|
|
5998
|
-
options?: { toolCallId?: string; abortSignal?: AbortSignal },
|
|
6265
|
+
options?: { toolCallId?: string; abortSignal?: AbortSignal } & plugins.IAgentToolExecutionOptions,
|
|
5999
6266
|
) => {
|
|
6000
6267
|
const toolCallId = options?.toolCallId ?? '';
|
|
6001
6268
|
const abortSignal = options?.abortSignal ?? run.controller.signal;
|
|
6269
|
+
const confirmed = options?.confirmed;
|
|
6002
6270
|
const call = await catalog.prepareCall(input.method, input.params);
|
|
6003
6271
|
if (!call.success) throw this.catalogCallError(run, toolCallId, call.error);
|
|
6004
|
-
|
|
6272
|
+
if (call.method === FLEX_RESULT_METHOD) {
|
|
6273
|
+
const read = await this.readResult(run, call.params as IResultRead);
|
|
6274
|
+
if (!read.ok) throw this.catalogCallError(run, toolCallId, read.error);
|
|
6275
|
+
return read.value;
|
|
6276
|
+
}
|
|
6277
|
+
// A call that started early asks for permission once the response confirmed it.
|
|
6278
|
+
const base = run.toolProviderContext!;
|
|
6279
|
+
const provider = confirmed
|
|
6280
|
+
? Object.freeze({
|
|
6281
|
+
...base,
|
|
6282
|
+
requestPermission: async (request: IFlexPermissionRequestInput) => {
|
|
6283
|
+
await confirmed;
|
|
6284
|
+
return base.requestPermission(request);
|
|
6285
|
+
},
|
|
6286
|
+
})
|
|
6287
|
+
: base;
|
|
6288
|
+
const context = { method: call.method, toolCallId, abortSignal, provider };
|
|
6005
6289
|
const decision = await call.decide(context);
|
|
6006
|
-
if (decision === 'run')
|
|
6290
|
+
if (decision === 'run') {
|
|
6291
|
+
return this.readOnce(run, toolCallId, this.readOnlyMethod(run, catalog, call), call.params, confirmed,
|
|
6292
|
+
() => call.run(context));
|
|
6293
|
+
}
|
|
6007
6294
|
if (decision === 'propose') {
|
|
6008
6295
|
return catalog.propose(
|
|
6009
6296
|
[{ id: toolCallId, method: call.method, params: call.params }],
|
|
@@ -6075,51 +6362,97 @@ export class FlexHarness<TScope = unknown> {
|
|
|
6075
6362
|
});
|
|
6076
6363
|
const stopped = (method: string) => ({ ok: false as const, error: { error: 'aborted', method } });
|
|
6077
6364
|
let callNumber = 0;
|
|
6365
|
+
// Read-only calls run concurrently; any other call runs after every call the program made before it.
|
|
6366
|
+
const order = new plugins.AgentToolCallScheduler(abortSignal);
|
|
6078
6367
|
const finish = (id: string, method: string, input: unknown, outcome: { output: unknown } | { error: Error }) => {
|
|
6079
6368
|
if ('error' in outcome) run.trustedToolExecutionErrors.set(id, outcome.error);
|
|
6080
6369
|
this.onToolFinish(run.state, run.sessionId, 'error' in outcome
|
|
6081
6370
|
? { toolCallId: id, toolName: method, input, success: false, error: outcome.error.message }
|
|
6082
6371
|
: { toolCallId: id, toolName: method, input, success: true, output: outcome.output });
|
|
6083
6372
|
};
|
|
6373
|
+
const callInTurn = async (
|
|
6374
|
+
id: string,
|
|
6375
|
+
method: string,
|
|
6376
|
+
params: unknown,
|
|
6377
|
+
signal: AbortSignal,
|
|
6378
|
+
classification: { readOnly: boolean } | { error: Error },
|
|
6379
|
+
): Promise<TFlexCodeCallOutcome> => {
|
|
6380
|
+
const call = await catalog.prepareCall(method, params);
|
|
6381
|
+
if (!call.success) {
|
|
6382
|
+
if (catalog.has(method)) {
|
|
6383
|
+
this.onToolStart(run.state, run.sessionId, { toolCallId: id, toolName: method, input: params ?? {}, parentToolCallId: toolCallId });
|
|
6384
|
+
finish(id, method, params, { error: this.catalogCallError(run, id, call.error) });
|
|
6385
|
+
}
|
|
6386
|
+
return { ok: false, error: call.error as unknown as TJsonValue };
|
|
6387
|
+
}
|
|
6388
|
+
try {
|
|
6389
|
+
await order.waitForTurn(id, signal);
|
|
6390
|
+
} catch {
|
|
6391
|
+
return stopped(call.method);
|
|
6392
|
+
}
|
|
6393
|
+
if (signal.aborted) return stopped(call.method);
|
|
6394
|
+
this.onToolStart(run.state, run.sessionId, {
|
|
6395
|
+
toolCallId: id, toolName: call.method, input: call.params, parentToolCallId: toolCallId,
|
|
6396
|
+
});
|
|
6397
|
+
if ('error' in classification) {
|
|
6398
|
+
finish(id, call.method, call.params, { error: classification.error });
|
|
6399
|
+
return { ok: false, error: { error: 'failed', method: call.method, message: classification.error.message } };
|
|
6400
|
+
}
|
|
6401
|
+
if (call.method === FLEX_RESULT_METHOD) {
|
|
6402
|
+
const read = await this.readResult(run, call.params as IResultRead);
|
|
6403
|
+
finish(id, call.method, call.params, read.ok
|
|
6404
|
+
? { output: read.value }
|
|
6405
|
+
: { error: this.catalogCallError(run, id, read.error) });
|
|
6406
|
+
return read;
|
|
6407
|
+
}
|
|
6408
|
+
const readOnly = classification.readOnly;
|
|
6409
|
+
const context = { method: call.method, toolCallId: id, abortSignal: signal, provider: providerFor(signal) };
|
|
6410
|
+
try {
|
|
6411
|
+
const decision = await call.decide(context);
|
|
6412
|
+
signal.throwIfAborted();
|
|
6413
|
+
if (decision === 'propose') {
|
|
6414
|
+
proposals.push({ id, method: call.method, params: call.params });
|
|
6415
|
+
const value = { proposed: true, id };
|
|
6416
|
+
finish(id, call.method, call.params, { output: value });
|
|
6417
|
+
return { ok: true, value };
|
|
6418
|
+
}
|
|
6419
|
+
if (decision !== 'run') {
|
|
6420
|
+
const payload = { error: 'denied' as const, method: call.method, reason: decision.deny };
|
|
6421
|
+
finish(id, call.method, call.params, { error: this.catalogCallError(run, id, payload) });
|
|
6422
|
+
return { ok: false, error: payload };
|
|
6423
|
+
}
|
|
6424
|
+
const output = this.readOnce(run, id, readOnly ? call.method : undefined, call.params, undefined,
|
|
6425
|
+
() => call.run(context));
|
|
6426
|
+
const value = await this.budgetOutput(run, await settleToolOutput(output));
|
|
6427
|
+
finish(id, call.method, call.params, { output: value });
|
|
6428
|
+
return { ok: true, value };
|
|
6429
|
+
} catch (error) {
|
|
6430
|
+
const projected = this.projectExternalError(run, error, 'toolExecution');
|
|
6431
|
+
finish(id, call.method, call.params, { error: projected });
|
|
6432
|
+
return { ok: false, error: { error: 'failed', method: call.method, message: projected.message } };
|
|
6433
|
+
}
|
|
6434
|
+
};
|
|
6084
6435
|
const host: IFlexCodeHost = {
|
|
6085
6436
|
waitedMs: () => waitedMs + (waiting > 0 ? performance.now() - waitingSince : 0),
|
|
6086
6437
|
call: async (method, params, signal) => {
|
|
6087
6438
|
if (signal.aborted) return stopped(method);
|
|
6088
6439
|
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) };
|
|
6440
|
+
// The call takes its place in the program's order now, before anything is awaited.
|
|
6441
|
+
let classification: { readOnly: boolean } | { error: Error };
|
|
6102
6442
|
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 };
|
|
6443
|
+
classification = {
|
|
6444
|
+
readOnly: catalog.has(method) && this.isReadOnlyCatalogCall(run, catalog, method, params ?? {}),
|
|
6445
|
+
};
|
|
6119
6446
|
} catch (error) {
|
|
6120
|
-
|
|
6121
|
-
|
|
6122
|
-
|
|
6447
|
+
classification = {
|
|
6448
|
+
error: error instanceof Error ? error : this.projectExternalError(run, error, 'toolProvider'),
|
|
6449
|
+
};
|
|
6450
|
+
}
|
|
6451
|
+
order.begin(id, 'readOnly' in classification && classification.readOnly);
|
|
6452
|
+
try {
|
|
6453
|
+
return await callInTurn(id, method, params, signal, classification);
|
|
6454
|
+
} finally {
|
|
6455
|
+
order.settle(id);
|
|
6123
6456
|
}
|
|
6124
6457
|
},
|
|
6125
6458
|
};
|
|
@@ -8698,6 +9031,10 @@ export class FlexHarness<TScope = unknown> {
|
|
|
8698
9031
|
delete part.output;
|
|
8699
9032
|
part.error = projectedError!.message;
|
|
8700
9033
|
}
|
|
9034
|
+
const original = run.deduplicatedToolCalls.get(event.toolCallId);
|
|
9035
|
+
if (original !== undefined) {
|
|
9036
|
+
part.deduplicatedFrom = this.boundedIdentifier(original, 'deduplicatedFrom');
|
|
9037
|
+
}
|
|
8701
9038
|
part.completedAt = new Date().toISOString();
|
|
8702
9039
|
this.emitPartEvent(run, 'part.completed', part);
|
|
8703
9040
|
} catch (error) {
|
|
@@ -9362,10 +9699,17 @@ export class FlexHarness<TScope = unknown> {
|
|
|
9362
9699
|
if (catalog !== undefined && (typeof catalog !== 'object' || catalog === null)) {
|
|
9363
9700
|
throw new FlexHarnessValidationError('Tool handle catalog must be an object.');
|
|
9364
9701
|
}
|
|
9702
|
+
const isReadOnlyToolCall = handle.isReadOnlyToolCall;
|
|
9703
|
+
if (isReadOnlyToolCall !== undefined && typeof isReadOnlyToolCall !== 'function') {
|
|
9704
|
+
throw new FlexHarnessValidationError('Tool handle isReadOnlyToolCall must be a function.');
|
|
9705
|
+
}
|
|
9365
9706
|
const closeHandle = close;
|
|
9366
9707
|
return {
|
|
9367
9708
|
tools,
|
|
9368
9709
|
...(catalog === undefined ? {} : { catalog }),
|
|
9710
|
+
...(isReadOnlyToolCall === undefined ? {} : {
|
|
9711
|
+
isReadOnlyToolCall: (call: { toolName: string; input: unknown }) => isReadOnlyToolCall.call(handle, call),
|
|
9712
|
+
}),
|
|
9369
9713
|
...(closeHandle ? { close: () => closeHandle.call(handle) } : {}),
|
|
9370
9714
|
};
|
|
9371
9715
|
} catch (error) {
|
|
@@ -9394,6 +9738,8 @@ export class FlexHarness<TScope = unknown> {
|
|
|
9394
9738
|
const descriptors = this.normalizeResourceToolProviderDescriptors(resolved);
|
|
9395
9739
|
const owned: Array<{ handle: IFlexToolHandle; closed: boolean }> = [];
|
|
9396
9740
|
const tools: Record<string, unknown> = Object.create(null) as Record<string, unknown>;
|
|
9741
|
+
/** The handle and own name of each exposed resource tool, for the handle's classifier. */
|
|
9742
|
+
const resourceTools = new Map<string, { handle: IFlexToolHandle; toolName: string }>();
|
|
9397
9743
|
const closeOwned = async (): Promise<void> => {
|
|
9398
9744
|
const errors: unknown[] = [];
|
|
9399
9745
|
for (const entry of [...owned].reverse()) {
|
|
@@ -9461,12 +9807,23 @@ export class FlexHarness<TScope = unknown> {
|
|
|
9461
9807
|
throw new FlexHarnessValidationError(`Duplicate exposed tool "${exposedName}".`);
|
|
9462
9808
|
}
|
|
9463
9809
|
tools[exposedName] = tool;
|
|
9810
|
+
resourceTools.set(exposedName, { handle, toolName });
|
|
9464
9811
|
}
|
|
9465
9812
|
}
|
|
9466
9813
|
if (owned.length === 0 && Object.keys(tools).length === 0) return undefined;
|
|
9814
|
+
const classifies = applicationHandle?.isReadOnlyToolCall !== undefined
|
|
9815
|
+
|| [...resourceTools.values()].some((entry) => entry.handle.isReadOnlyToolCall !== undefined);
|
|
9467
9816
|
return {
|
|
9468
9817
|
tools: tools as TFlexAgentToolSet,
|
|
9469
9818
|
...(applicationHandle?.catalog === undefined ? {} : { catalog: applicationHandle.catalog }),
|
|
9819
|
+
...(classifies ? {
|
|
9820
|
+
isReadOnlyToolCall: ({ toolName, input }: { toolName: string; input: unknown }) => {
|
|
9821
|
+
const resource = resourceTools.get(toolName);
|
|
9822
|
+
return resource
|
|
9823
|
+
? resource.handle.isReadOnlyToolCall?.({ toolName: resource.toolName, input }) === true
|
|
9824
|
+
: applicationHandle?.isReadOnlyToolCall?.({ toolName, input }) === true;
|
|
9825
|
+
},
|
|
9826
|
+
} : {}),
|
|
9470
9827
|
...(owned.some((entry) => entry.handle.close) ? { close: closeOwned } : {}),
|
|
9471
9828
|
};
|
|
9472
9829
|
} catch (error) {
|
|
@@ -10906,11 +11263,18 @@ export class FlexHarness<TScope = unknown> {
|
|
|
10906
11263
|
tombstone,
|
|
10907
11264
|
);
|
|
10908
11265
|
};
|
|
11266
|
+
const resultStore = this.stores.results;
|
|
11267
|
+
const resultCleanup = async (): Promise<void> => {
|
|
11268
|
+
// A result being saved for the session is written before its results are deleted.
|
|
11269
|
+
await this.resultQueues.get(this.resultQueueKey(storageKey, sessionId));
|
|
11270
|
+
await resultStore!.deleteSession(storageKey, sessionId);
|
|
11271
|
+
};
|
|
10909
11272
|
const results = await Promise.allSettled([
|
|
10910
11273
|
this.stores.agentEvents.deleteSession(storageKey, sessionId),
|
|
10911
11274
|
this.stores.jobs.deleteSession(storageKey, sessionId),
|
|
10912
11275
|
this.stores.projections.deleteSession(storageKey, sessionId),
|
|
10913
11276
|
this.stores.permissions.deleteSession(storageKey, sessionId),
|
|
11277
|
+
...(resultStore ? [resultCleanup()] : []),
|
|
10914
11278
|
// An imported session refuses every project-management write, so it has no state to fence.
|
|
10915
11279
|
...(tombstone.imported ? [] : [projectManagementCleanup()]),
|
|
10916
11280
|
]);
|