@modelprofile.com/flexharness 10.23.0 → 10.24.1
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.flexharness.admission.d.ts +17 -0
- package/dist_ts/classes.flexharness.admission.js +263 -0
- package/dist_ts/classes.flexharness.callbacks.d.ts +38 -0
- package/dist_ts/classes.flexharness.callbacks.js +313 -0
- package/dist_ts/classes.flexharness.compaction.d.ts +25 -0
- package/dist_ts/classes.flexharness.compaction.js +87 -0
- package/dist_ts/classes.flexharness.configurations.d.ts +11 -0
- package/dist_ts/classes.flexharness.configurations.js +137 -0
- package/dist_ts/classes.flexharness.core.d.ts +143 -0
- package/dist_ts/classes.flexharness.core.js +1538 -0
- package/dist_ts/classes.flexharness.d.ts +2 -437
- package/dist_ts/classes.flexharness.deletion.d.ts +31 -0
- package/dist_ts/classes.flexharness.deletion.js +396 -0
- package/dist_ts/classes.flexharness.imports.d.ts +24 -0
- package/dist_ts/classes.flexharness.imports.js +345 -0
- package/dist_ts/classes.flexharness.js +119 -9776
- package/dist_ts/classes.flexharness.lifecycle.d.ts +47 -0
- package/dist_ts/classes.flexharness.lifecycle.js +625 -0
- package/dist_ts/classes.flexharness.permissions.d.ts +19 -0
- package/dist_ts/classes.flexharness.permissions.js +450 -0
- package/dist_ts/classes.flexharness.projects.d.ts +55 -0
- package/dist_ts/classes.flexharness.projects.js +575 -0
- package/dist_ts/classes.flexharness.prompts.d.ts +44 -0
- package/dist_ts/classes.flexharness.prompts.js +659 -0
- package/dist_ts/classes.flexharness.providers.d.ts +18 -0
- package/dist_ts/classes.flexharness.providers.js +267 -0
- package/dist_ts/classes.flexharness.results.d.ts +26 -0
- package/dist_ts/classes.flexharness.results.js +89 -0
- package/dist_ts/classes.flexharness.reversions.d.ts +52 -0
- package/dist_ts/classes.flexharness.reversions.js +1306 -0
- package/dist_ts/classes.flexharness.runs.d.ts +63 -0
- package/dist_ts/classes.flexharness.runs.js +1058 -0
- package/dist_ts/classes.flexharness.sessions.d.ts +20 -0
- package/dist_ts/classes.flexharness.sessions.js +381 -0
- package/dist_ts/classes.flexharness.slashcommands.d.ts +40 -0
- package/dist_ts/classes.flexharness.slashcommands.js +458 -0
- package/dist_ts/classes.flexharness.subagents.d.ts +31 -0
- package/dist_ts/classes.flexharness.subagents.js +690 -0
- package/dist_ts/classes.flexharness.tooldispatch.d.ts +53 -0
- package/dist_ts/classes.flexharness.tooldispatch.js +418 -0
- package/dist_ts/interfaces.flexharness.d.ts +455 -0
- package/dist_ts/interfaces.flexharness.js +2 -0
- package/dist_ts/utils.flexharness.d.ts +19 -0
- package/dist_ts/utils.flexharness.js +83 -0
- package/dist_ts_compaction/plugins.d.ts +2 -2
- package/dist_ts_compaction/plugins.js +3 -3
- package/dist_ts_compaction/smartagent.compaction.js +6 -5
- package/dist_ts_stores_nosqldb/classes.nosqldbstores.d.ts +9 -0
- package/dist_ts_stores_nosqldb/classes.nosqldbstores.js +13 -1
- package/dist_ts_stores_nosqldb/classes.recordstore.d.ts +2 -2
- package/dist_ts_stores_nosqldb/classes.recordstore.js +3 -3
- package/package.json +3 -3
- package/readme.md +1 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.flexharness.admission.ts +331 -0
- package/ts/classes.flexharness.callbacks.ts +352 -0
- package/ts/classes.flexharness.compaction.ts +167 -0
- package/ts/classes.flexharness.configurations.ts +167 -0
- package/ts/classes.flexharness.core.ts +1830 -0
- package/ts/classes.flexharness.deletion.ts +616 -0
- package/ts/classes.flexharness.imports.ts +414 -0
- package/ts/classes.flexharness.lifecycle.ts +786 -0
- package/ts/classes.flexharness.permissions.ts +535 -0
- package/ts/classes.flexharness.projects.ts +1006 -0
- package/ts/classes.flexharness.prompts.ts +857 -0
- package/ts/classes.flexharness.providers.ts +322 -0
- package/ts/classes.flexharness.results.ts +98 -0
- package/ts/classes.flexharness.reversions.ts +1684 -0
- package/ts/classes.flexharness.runs.ts +1204 -0
- package/ts/classes.flexharness.sessions.ts +483 -0
- package/ts/classes.flexharness.slashcommands.ts +705 -0
- package/ts/classes.flexharness.subagents.ts +860 -0
- package/ts/classes.flexharness.tooldispatch.ts +475 -0
- package/ts/classes.flexharness.ts +147 -12567
- package/ts/interfaces.flexharness.ts +517 -0
- package/ts/readme.md +1 -1
- package/ts/utils.flexharness.ts +121 -0
- package/ts_compaction/plugins.ts +2 -2
- package/ts_compaction/smartagent.compaction.ts +5 -4
- package/ts_stores_nosqldb/classes.nosqldbstores.ts +13 -0
- package/ts_stores_nosqldb/classes.recordstore.ts +5 -5
- package/ts_stores_nosqldb/readme.md +7 -0
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
import { FlexHarnessValidationError } from './errors.js';
|
|
3
|
+
import {
|
|
4
|
+
FLEX_CODE_MODE_TOOL_NAME,
|
|
5
|
+
FLEX_RESULT_METHOD,
|
|
6
|
+
FLEX_TOOL_CATALOG_TOOL_NAMES,
|
|
7
|
+
type IFlexPermissionRequestInput,
|
|
8
|
+
type IFlexCatalogDescribeInput,
|
|
9
|
+
type IFlexCatalogProposal,
|
|
10
|
+
type IFlexCatalogSearchInput,
|
|
11
|
+
type IFlexToolProviderContext,
|
|
12
|
+
type IFlexUncertainToolExecution,
|
|
13
|
+
type TFlexAgentToolSet,
|
|
14
|
+
type TFlexToolExecutionReconciliation,
|
|
15
|
+
type TJsonValue,
|
|
16
|
+
} from './interfaces.js';
|
|
17
|
+
import { FlexToolCatalog, catalogToolInputSchema } from './classes.toolcatalog.js';
|
|
18
|
+
import type { IFlexCodeHost, TFlexCodeCallOutcome } from './classes.codesandbox.js';
|
|
19
|
+
import { canonicalJson, normalizeJsonValue } from './utils.json.js';
|
|
20
|
+
import {
|
|
21
|
+
maxTransferMetadataBytes,
|
|
22
|
+
publicSnapshot,
|
|
23
|
+
requireTransferIdentifier,
|
|
24
|
+
truncateUtf8,
|
|
25
|
+
} from './utils.transfer.js';
|
|
26
|
+
import { isAsyncIterable, settleToolOutput } from './utils.tooloutput.js';
|
|
27
|
+
import { validateIdentifier } from './utils.validation.js';
|
|
28
|
+
import type { IActiveRun, IResultRead, IRunReadCall } from './interfaces.flexharness.js';
|
|
29
|
+
import type { FlexHarnessCore } from './classes.flexharness.core.js';
|
|
30
|
+
|
|
31
|
+
const notShared = Symbol('not shared');
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Tool call dispatch: read-only classification, call tracking, reconciliation and the tool catalog.
|
|
35
|
+
*/
|
|
36
|
+
export class FlexHarnessToolDispatch<TScope> {
|
|
37
|
+
constructor(private readonly core: FlexHarnessCore<TScope>) {}
|
|
38
|
+
|
|
39
|
+
public async listUncertainToolExecutions(
|
|
40
|
+
scopeId: string,
|
|
41
|
+
sessionId: string,
|
|
42
|
+
): Promise<IFlexUncertainToolExecution[]> {
|
|
43
|
+
const { state } = await this.core.resolveState(scopeId);
|
|
44
|
+
const stored = this.core.requireSession(state, sessionId);
|
|
45
|
+
this.core.assertStateAcceptingWork(state);
|
|
46
|
+
try {
|
|
47
|
+
return publicSnapshot(stored.agentSession.listUncertainToolExecutions().map((intent) => ({
|
|
48
|
+
intentId: this.core.boundedIdentifier(intent.id, 'intentId'),
|
|
49
|
+
generationId: this.core.boundedIdentifier(intent.generationId, 'generationId'),
|
|
50
|
+
toolCallId: this.core.boundedIdentifier(intent.toolCallId, 'toolCallId'),
|
|
51
|
+
toolName: this.core.boundedIdentifier(intent.toolName, 'toolName'),
|
|
52
|
+
input: normalizeJsonValue(intent.input, this.core.toolOutputLimits),
|
|
53
|
+
timestamp: new Date(intent.timestamp).toISOString(),
|
|
54
|
+
})));
|
|
55
|
+
} catch (error) {
|
|
56
|
+
throw this.core.projectOperationError(error, 'agentSession', scopeId, sessionId, 'uncertain-tools');
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
public async reconcileToolExecution(
|
|
61
|
+
scopeId: string,
|
|
62
|
+
sessionId: string,
|
|
63
|
+
intentId: string,
|
|
64
|
+
reconciliation: TFlexToolExecutionReconciliation,
|
|
65
|
+
): Promise<void> {
|
|
66
|
+
validateIdentifier(intentId, 'intentId');
|
|
67
|
+
requireTransferIdentifier(intentId, 'intentId');
|
|
68
|
+
if (!reconciliation || typeof reconciliation !== 'object' || Array.isArray(reconciliation)) {
|
|
69
|
+
throw new FlexHarnessValidationError('Tool execution reconciliation must be a plain object.');
|
|
70
|
+
}
|
|
71
|
+
const { scope, state } = await this.core.resolveState(scopeId);
|
|
72
|
+
const stored = this.core.requireSession(state, sessionId);
|
|
73
|
+
this.core.assertSessionWritable(stored);
|
|
74
|
+
this.core.assertStateAcceptingWork(state);
|
|
75
|
+
let canonical: plugins.TAgentToolExecutionReconciliationOptions;
|
|
76
|
+
if (reconciliation.resolution === 'executed') {
|
|
77
|
+
const output = normalizeJsonValue(reconciliation.output, this.core.toolOutputLimits);
|
|
78
|
+
canonical = { resolution: 'executed', output, modelOutput: { type: 'json', value: output } };
|
|
79
|
+
} else if (
|
|
80
|
+
reconciliation.resolution === 'not-executed'
|
|
81
|
+
|| reconciliation.resolution === 'abandoned-unknown'
|
|
82
|
+
) {
|
|
83
|
+
if (reconciliation.error !== undefined && typeof reconciliation.error !== 'string') {
|
|
84
|
+
throw new FlexHarnessValidationError('Reconciliation error must be a string.');
|
|
85
|
+
}
|
|
86
|
+
canonical = {
|
|
87
|
+
resolution: reconciliation.resolution,
|
|
88
|
+
...(reconciliation.error === undefined
|
|
89
|
+
? {}
|
|
90
|
+
: { error: truncateUtf8(reconciliation.error, maxTransferMetadataBytes) }),
|
|
91
|
+
};
|
|
92
|
+
} else {
|
|
93
|
+
throw new FlexHarnessValidationError('Unknown tool execution reconciliation.');
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
await this.core.compaction.withCompactorContext(
|
|
97
|
+
this.core.compaction.createCompactorContext(scopeId, scope.scope, state.storageKey, sessionId),
|
|
98
|
+
() => stored.agentSession.reconcileToolExecution(intentId, canonical),
|
|
99
|
+
);
|
|
100
|
+
} catch (error) {
|
|
101
|
+
throw this.core.projectOperationError(error, 'agentSession', scopeId, sessionId, 'tool-reconciliation');
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Whether a call of the run's tools only reads: search_tools and describe_tools, an `execute` of
|
|
107
|
+
* a method declared read-only, or a call the tool handle classifies as read-only by its real name.
|
|
108
|
+
*/
|
|
109
|
+
public isReadOnlyToolCall(run: IActiveRun, toolName: string, input: unknown): boolean {
|
|
110
|
+
const names = FLEX_TOOL_CATALOG_TOOL_NAMES;
|
|
111
|
+
const catalog = run.toolCatalog;
|
|
112
|
+
if (catalog) {
|
|
113
|
+
if (toolName === names.search || toolName === names.describe) return true;
|
|
114
|
+
if (toolName === names.execute) {
|
|
115
|
+
const target = catalog.callTarget(input);
|
|
116
|
+
return target !== undefined && this.isReadOnlyCatalogCall(run, catalog, target.method, target.params);
|
|
117
|
+
}
|
|
118
|
+
if (toolName === FLEX_CODE_MODE_TOOL_NAME) return false;
|
|
119
|
+
}
|
|
120
|
+
if (this.core.runs.enabledBuiltInToolNames(run).has(toolName)) return false;
|
|
121
|
+
return this.classifyHostToolCall(run, toolName, input);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** The method's path when the prepared call only reads, for the turn's reads. */
|
|
125
|
+
private readOnlyMethod(
|
|
126
|
+
run: IActiveRun,
|
|
127
|
+
catalog: FlexToolCatalog<unknown>,
|
|
128
|
+
call: { method: string; params: unknown },
|
|
129
|
+
): string | undefined {
|
|
130
|
+
return this.isReadOnlyCatalogCall(run, catalog, call.method, call.params) ? call.method : undefined;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** A method whose `onCall` decides its calls is gated: its calls run in order, whatever they read. */
|
|
134
|
+
private isReadOnlyCatalogCall(
|
|
135
|
+
run: IActiveRun,
|
|
136
|
+
catalog: FlexToolCatalog<unknown>,
|
|
137
|
+
method: string,
|
|
138
|
+
params: unknown,
|
|
139
|
+
): boolean {
|
|
140
|
+
return !catalog.decides(method)
|
|
141
|
+
&& (catalog.isReadOnly(method) || this.classifyHostToolCall(run, method, params));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private classifyHostToolCall(run: IActiveRun, toolName: string, input: unknown): boolean {
|
|
145
|
+
try {
|
|
146
|
+
return run.isReadOnlyHostCall?.({ toolName, input }) === true;
|
|
147
|
+
} catch (error) {
|
|
148
|
+
throw this.core.projectExternalError(run, error, 'toolProvider');
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Runs the run's tools through the turn's reads: a read-only call identical to an earlier read of
|
|
154
|
+
* the turn shares its output, and any other call clears them. Records the confirmation of each
|
|
155
|
+
* call that started while the model still streamed, which the permission requests naming the
|
|
156
|
+
* call wait for. The catalog's tools read and run their methods by real name themselves.
|
|
157
|
+
*/
|
|
158
|
+
public trackToolCalls(run: IActiveRun, tools: TFlexAgentToolSet): TFlexAgentToolSet {
|
|
159
|
+
const catalogTools = new Set<string>(run.toolCatalog
|
|
160
|
+
? [...Object.values(FLEX_TOOL_CATALOG_TOOL_NAMES), FLEX_CODE_MODE_TOOL_NAME]
|
|
161
|
+
: []);
|
|
162
|
+
const tracked: Record<string, unknown> = {};
|
|
163
|
+
for (const [name, tool] of Object.entries(tools)) {
|
|
164
|
+
const execute = (tool as { execute?: (input: unknown, options: unknown) => unknown }).execute;
|
|
165
|
+
if (typeof execute !== 'function') {
|
|
166
|
+
tracked[name] = tool;
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
tracked[name] = {
|
|
170
|
+
...tool,
|
|
171
|
+
execute: (input: unknown, options: plugins.IAgentToolExecutionOptions & { toolCallId?: string }) => {
|
|
172
|
+
const confirmed = options?.confirmed;
|
|
173
|
+
const toolCallId = options?.toolCallId ?? '';
|
|
174
|
+
if (confirmed && toolCallId) {
|
|
175
|
+
run.unconfirmedToolCalls.set(toolCallId, confirmed);
|
|
176
|
+
const forget = () => run.unconfirmedToolCalls.delete(toolCallId);
|
|
177
|
+
confirmed.then(forget, forget);
|
|
178
|
+
}
|
|
179
|
+
const executeTool = () => execute.call(tool, input, options);
|
|
180
|
+
if (catalogTools.has(name)) return executeTool();
|
|
181
|
+
const readName = this.isReadOnlyToolCall(run, name, input) ? name : undefined;
|
|
182
|
+
return this.readOnce(run, toolCallId, readName, input, confirmed, executeTool);
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
return tracked as TFlexAgentToolSet;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Answers a read-only call (`name` set) from an identical read earlier in the turn, sharing the
|
|
191
|
+
* output of one still running, or runs it and keeps its output for the reads after it. A failed
|
|
192
|
+
* read, and a read whose response was discarded, are not kept. Any other call clears the turn's
|
|
193
|
+
* reads before it runs.
|
|
194
|
+
*/
|
|
195
|
+
private readOnce(
|
|
196
|
+
run: IActiveRun,
|
|
197
|
+
toolCallId: string,
|
|
198
|
+
name: string | undefined,
|
|
199
|
+
params: unknown,
|
|
200
|
+
confirmed: Promise<void> | undefined,
|
|
201
|
+
execute: () => unknown,
|
|
202
|
+
): unknown {
|
|
203
|
+
if (name === undefined) {
|
|
204
|
+
run.readCalls.clear();
|
|
205
|
+
return execute();
|
|
206
|
+
}
|
|
207
|
+
const canonicalParams = canonicalJson(params);
|
|
208
|
+
if (canonicalParams === undefined) return execute();
|
|
209
|
+
const key = `${name}\n${canonicalParams}`;
|
|
210
|
+
const earlier = run.readCalls.get(key);
|
|
211
|
+
if (earlier) {
|
|
212
|
+
return earlier.output.then((output) => {
|
|
213
|
+
if (output === notShared) return execute();
|
|
214
|
+
run.deduplicatedToolCalls.set(toolCallId, earlier.toolCallId);
|
|
215
|
+
return output;
|
|
216
|
+
}, (error: unknown) => {
|
|
217
|
+
run.deduplicatedToolCalls.set(toolCallId, earlier.toolCallId);
|
|
218
|
+
throw error;
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
const result = execute();
|
|
222
|
+
const entry: IRunReadCall = {
|
|
223
|
+
toolCallId,
|
|
224
|
+
output: Promise.resolve(result).then((output) => isAsyncIterable(output) ? notShared : output),
|
|
225
|
+
};
|
|
226
|
+
run.readCalls.set(key, entry);
|
|
227
|
+
const forget = () => {
|
|
228
|
+
if (run.readCalls.get(key) === entry) run.readCalls.delete(key);
|
|
229
|
+
};
|
|
230
|
+
entry.output.catch(forget);
|
|
231
|
+
confirmed?.catch(forget);
|
|
232
|
+
return result;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* search_tools, describe_tools and execute. `execute` validates the call and runs the method
|
|
237
|
+
* inside this tool call, so the tool wrapper bounds its output and projects its errors, and the
|
|
238
|
+
* call's part, events and permission requests carry the method's path.
|
|
239
|
+
*/
|
|
240
|
+
public createCatalogTools(run: IActiveRun, catalog: FlexToolCatalog<unknown>): Record<string, unknown> {
|
|
241
|
+
const names = FLEX_TOOL_CATALOG_TOOL_NAMES;
|
|
242
|
+
const definitions = catalog.toolDefinitions();
|
|
243
|
+
const describeBytes = Math.max(1024, this.core.toolOutputLimits.maxBytes - 1024);
|
|
244
|
+
return {
|
|
245
|
+
[names.search]: plugins.tool({
|
|
246
|
+
description: definitions[names.search].description,
|
|
247
|
+
strict: false,
|
|
248
|
+
inputSchema: catalogToolInputSchema<IFlexCatalogSearchInput>(definitions[names.search]),
|
|
249
|
+
execute: async (input: IFlexCatalogSearchInput, options?: { messages?: plugins.ModelMessage[] }) =>
|
|
250
|
+
catalog.search(input, { declaredTypes: catalog.declaredTypesIn(options?.messages), maxBytes: describeBytes }),
|
|
251
|
+
}),
|
|
252
|
+
[names.describe]: plugins.tool({
|
|
253
|
+
description: definitions[names.describe].description,
|
|
254
|
+
strict: false,
|
|
255
|
+
inputSchema: catalogToolInputSchema<IFlexCatalogDescribeInput>(definitions[names.describe]),
|
|
256
|
+
execute: async (input: IFlexCatalogDescribeInput, options?: { messages?: plugins.ModelMessage[] }) =>
|
|
257
|
+
catalog.describe(input, {
|
|
258
|
+
declaredTypes: catalog.declaredTypesIn(options?.messages),
|
|
259
|
+
maxBytes: describeBytes,
|
|
260
|
+
}).output,
|
|
261
|
+
}),
|
|
262
|
+
[names.execute]: plugins.tool({
|
|
263
|
+
description: definitions[names.execute].description,
|
|
264
|
+
strict: false,
|
|
265
|
+
inputSchema: catalogToolInputSchema<{ method: string; params?: Record<string, unknown> }>(
|
|
266
|
+
definitions[names.execute],
|
|
267
|
+
),
|
|
268
|
+
execute: async (
|
|
269
|
+
input: { method: string; params?: Record<string, unknown> },
|
|
270
|
+
options?: { toolCallId?: string; abortSignal?: AbortSignal } & plugins.IAgentToolExecutionOptions,
|
|
271
|
+
) => {
|
|
272
|
+
const toolCallId = options?.toolCallId ?? '';
|
|
273
|
+
const abortSignal = options?.abortSignal ?? run.controller.signal;
|
|
274
|
+
const confirmed = options?.confirmed;
|
|
275
|
+
const call = await catalog.prepareCall(input.method, input.params);
|
|
276
|
+
if (!call.success) throw this.catalogCallError(run, toolCallId, call.error);
|
|
277
|
+
if (call.method === FLEX_RESULT_METHOD) {
|
|
278
|
+
const read = await this.core.results.readResult(run, call.params as IResultRead);
|
|
279
|
+
if (!read.ok) throw this.catalogCallError(run, toolCallId, read.error);
|
|
280
|
+
return read.value;
|
|
281
|
+
}
|
|
282
|
+
// A call that started early asks for permission once the response confirmed it.
|
|
283
|
+
const base = run.toolProviderContext!;
|
|
284
|
+
const provider = confirmed
|
|
285
|
+
? Object.freeze({
|
|
286
|
+
...base,
|
|
287
|
+
requestPermission: async (request: IFlexPermissionRequestInput) => {
|
|
288
|
+
await confirmed;
|
|
289
|
+
return base.requestPermission(request);
|
|
290
|
+
},
|
|
291
|
+
})
|
|
292
|
+
: base;
|
|
293
|
+
const context = { method: call.method, toolCallId, abortSignal, provider };
|
|
294
|
+
const decision = await call.decide(context);
|
|
295
|
+
if (decision === 'run') {
|
|
296
|
+
return this.readOnce(run, toolCallId, this.readOnlyMethod(run, catalog, call), call.params, confirmed,
|
|
297
|
+
() => call.run(context));
|
|
298
|
+
}
|
|
299
|
+
if (decision === 'propose') {
|
|
300
|
+
return catalog.propose(
|
|
301
|
+
[{ id: toolCallId, method: call.method, params: call.params }],
|
|
302
|
+
{ toolCallId, abortSignal, provider: run.toolProviderContext! },
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
throw this.catalogCallError(run, toolCallId, { error: 'denied', method: call.method, reason: decision.deny });
|
|
306
|
+
},
|
|
307
|
+
}),
|
|
308
|
+
...(catalog.codeMode && run.codeSandbox ? {
|
|
309
|
+
[FLEX_CODE_MODE_TOOL_NAME]: plugins.tool({
|
|
310
|
+
description: definitions[FLEX_CODE_MODE_TOOL_NAME].description,
|
|
311
|
+
strict: false,
|
|
312
|
+
inputSchema: catalogToolInputSchema<{ code: string }>(definitions[FLEX_CODE_MODE_TOOL_NAME]),
|
|
313
|
+
execute: async (input: { code: string }, options?: { toolCallId?: string; abortSignal?: AbortSignal }) =>
|
|
314
|
+
this.runCatalogProgram(run, catalog, input.code, options?.toolCallId ?? '', options?.abortSignal ?? run.controller.signal),
|
|
315
|
+
}),
|
|
316
|
+
} : {}),
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** A refused catalog call as the trusted error the model and the tool part show. */
|
|
321
|
+
private catalogCallError(run: IActiveRun, toolCallId: string, payload: unknown): Error {
|
|
322
|
+
const error = this.core.trustInternalError(new FlexHarnessValidationError(JSON.stringify(payload)));
|
|
323
|
+
if (toolCallId) run.trustedToolExecutionErrors.set(toolCallId, error);
|
|
324
|
+
return error;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Runs a `run_code` program. Every catalog call it makes is dispatched like an `execute`
|
|
329
|
+
* (validation, `onCall`, the method's run with its permission requests, the output limits, error
|
|
330
|
+
* projection) and recorded as a tool part of its own, named by the method, with the `run_code`
|
|
331
|
+
* call as its parent. Proposals go to `onProposals` as one batch once the program finished; a
|
|
332
|
+
* program that fails proposes nothing.
|
|
333
|
+
*/
|
|
334
|
+
private async runCatalogProgram(
|
|
335
|
+
run: IActiveRun,
|
|
336
|
+
catalog: FlexToolCatalog<unknown>,
|
|
337
|
+
code: string,
|
|
338
|
+
toolCallId: string,
|
|
339
|
+
abortSignal: AbortSignal,
|
|
340
|
+
): Promise<unknown> {
|
|
341
|
+
const proposals: IFlexCatalogProposal[] = [];
|
|
342
|
+
const base = run.toolProviderContext!;
|
|
343
|
+
// Waiting for a permission decision is the user's time, not the program's.
|
|
344
|
+
let waitedMs = 0;
|
|
345
|
+
let waiting = 0;
|
|
346
|
+
let waitingSince = 0;
|
|
347
|
+
/** The provider context of one call: its permission waits are timed, and end when the program stops. */
|
|
348
|
+
const providerFor = (signal: AbortSignal): Readonly<IFlexToolProviderContext<unknown>> => Object.freeze({
|
|
349
|
+
...base,
|
|
350
|
+
requestPermission: async (request: IFlexPermissionRequestInput) => {
|
|
351
|
+
signal.throwIfAborted();
|
|
352
|
+
if (waiting++ === 0) waitingSince = performance.now();
|
|
353
|
+
let onAbort: (() => void) | undefined;
|
|
354
|
+
try {
|
|
355
|
+
await Promise.race([
|
|
356
|
+
base.requestPermission(request),
|
|
357
|
+
new Promise<never>((_resolve, reject) => {
|
|
358
|
+
onAbort = () => reject(signal.reason);
|
|
359
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
360
|
+
}),
|
|
361
|
+
]);
|
|
362
|
+
} finally {
|
|
363
|
+
if (onAbort) signal.removeEventListener('abort', onAbort);
|
|
364
|
+
if (--waiting === 0) waitedMs += performance.now() - waitingSince;
|
|
365
|
+
}
|
|
366
|
+
},
|
|
367
|
+
});
|
|
368
|
+
const stopped = (method: string) => ({ ok: false as const, error: { error: 'aborted', method } });
|
|
369
|
+
let callNumber = 0;
|
|
370
|
+
// Read-only calls run concurrently; any other call runs after every call the program made before it.
|
|
371
|
+
const order = new plugins.AgentToolCallScheduler(abortSignal);
|
|
372
|
+
const finish = (id: string, method: string, input: unknown, outcome: { output: unknown } | { error: Error }) => {
|
|
373
|
+
if ('error' in outcome) run.trustedToolExecutionErrors.set(id, outcome.error);
|
|
374
|
+
this.core.callbacks.onToolFinish(run.state, run.sessionId, 'error' in outcome
|
|
375
|
+
? { toolCallId: id, toolName: method, input, success: false, error: outcome.error.message }
|
|
376
|
+
: { toolCallId: id, toolName: method, input, success: true, output: outcome.output });
|
|
377
|
+
};
|
|
378
|
+
const callInTurn = async (
|
|
379
|
+
id: string,
|
|
380
|
+
method: string,
|
|
381
|
+
params: unknown,
|
|
382
|
+
signal: AbortSignal,
|
|
383
|
+
classification: { readOnly: boolean } | { error: Error },
|
|
384
|
+
): Promise<TFlexCodeCallOutcome> => {
|
|
385
|
+
const call = await catalog.prepareCall(method, params);
|
|
386
|
+
if (!call.success) {
|
|
387
|
+
if (catalog.has(method)) {
|
|
388
|
+
this.core.callbacks.onToolStart(run.state, run.sessionId, { toolCallId: id, toolName: method, input: params ?? {}, parentToolCallId: toolCallId });
|
|
389
|
+
finish(id, method, params, { error: this.catalogCallError(run, id, call.error) });
|
|
390
|
+
}
|
|
391
|
+
return { ok: false, error: call.error as unknown as TJsonValue };
|
|
392
|
+
}
|
|
393
|
+
try {
|
|
394
|
+
await order.waitForTurn(id, signal);
|
|
395
|
+
} catch {
|
|
396
|
+
return stopped(call.method);
|
|
397
|
+
}
|
|
398
|
+
if (signal.aborted) return stopped(call.method);
|
|
399
|
+
this.core.callbacks.onToolStart(run.state, run.sessionId, {
|
|
400
|
+
toolCallId: id, toolName: call.method, input: call.params, parentToolCallId: toolCallId,
|
|
401
|
+
});
|
|
402
|
+
if ('error' in classification) {
|
|
403
|
+
finish(id, call.method, call.params, { error: classification.error });
|
|
404
|
+
return { ok: false, error: { error: 'failed', method: call.method, message: classification.error.message } };
|
|
405
|
+
}
|
|
406
|
+
if (call.method === FLEX_RESULT_METHOD) {
|
|
407
|
+
const read = await this.core.results.readResult(run, call.params as IResultRead);
|
|
408
|
+
finish(id, call.method, call.params, read.ok
|
|
409
|
+
? { output: read.value }
|
|
410
|
+
: { error: this.catalogCallError(run, id, read.error) });
|
|
411
|
+
return read;
|
|
412
|
+
}
|
|
413
|
+
const readOnly = classification.readOnly;
|
|
414
|
+
const context = { method: call.method, toolCallId: id, abortSignal: signal, provider: providerFor(signal) };
|
|
415
|
+
try {
|
|
416
|
+
const decision = await call.decide(context);
|
|
417
|
+
signal.throwIfAborted();
|
|
418
|
+
if (decision === 'propose') {
|
|
419
|
+
proposals.push({ id, method: call.method, params: call.params });
|
|
420
|
+
const value = { proposed: true, id };
|
|
421
|
+
finish(id, call.method, call.params, { output: value });
|
|
422
|
+
return { ok: true, value };
|
|
423
|
+
}
|
|
424
|
+
if (decision !== 'run') {
|
|
425
|
+
const payload = { error: 'denied' as const, method: call.method, reason: decision.deny };
|
|
426
|
+
finish(id, call.method, call.params, { error: this.catalogCallError(run, id, payload) });
|
|
427
|
+
return { ok: false, error: payload };
|
|
428
|
+
}
|
|
429
|
+
const output = this.readOnce(run, id, readOnly ? call.method : undefined, call.params, undefined,
|
|
430
|
+
() => call.run(context));
|
|
431
|
+
const value = await this.core.results.budgetOutput(run, await settleToolOutput(output));
|
|
432
|
+
finish(id, call.method, call.params, { output: value });
|
|
433
|
+
return { ok: true, value };
|
|
434
|
+
} catch (error) {
|
|
435
|
+
const projected = this.core.projectExternalError(run, error, 'toolExecution');
|
|
436
|
+
finish(id, call.method, call.params, { error: projected });
|
|
437
|
+
return { ok: false, error: { error: 'failed', method: call.method, message: projected.message } };
|
|
438
|
+
}
|
|
439
|
+
};
|
|
440
|
+
const host: IFlexCodeHost = {
|
|
441
|
+
waitedMs: () => waitedMs + (waiting > 0 ? performance.now() - waitingSince : 0),
|
|
442
|
+
call: async (method, params, signal) => {
|
|
443
|
+
if (signal.aborted) return stopped(method);
|
|
444
|
+
const id = `${toolCallId}.${++callNumber}`;
|
|
445
|
+
// The call takes its place in the program's order now, before anything is awaited.
|
|
446
|
+
let classification: { readOnly: boolean } | { error: Error };
|
|
447
|
+
try {
|
|
448
|
+
classification = {
|
|
449
|
+
readOnly: catalog.has(method) && this.isReadOnlyCatalogCall(run, catalog, method, params ?? {}),
|
|
450
|
+
};
|
|
451
|
+
} catch (error) {
|
|
452
|
+
classification = {
|
|
453
|
+
error: error instanceof Error ? error : this.core.projectExternalError(run, error, 'toolProvider'),
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
order.begin(id, 'readOnly' in classification && classification.readOnly);
|
|
457
|
+
try {
|
|
458
|
+
return await callInTurn(id, method, params, signal, classification);
|
|
459
|
+
} finally {
|
|
460
|
+
order.settle(id);
|
|
461
|
+
}
|
|
462
|
+
},
|
|
463
|
+
};
|
|
464
|
+
const result = await run.codeSandbox!.run(code, host, catalog.codeMode!, abortSignal);
|
|
465
|
+
if (!result.ok) throw this.catalogCallError(run, toolCallId, result.failure);
|
|
466
|
+
return {
|
|
467
|
+
result: result.value,
|
|
468
|
+
calls: result.calls,
|
|
469
|
+
...(result.logs ? { logs: result.logs } : {}),
|
|
470
|
+
...(proposals.length > 0
|
|
471
|
+
? { proposals: await catalog.propose(proposals, { toolCallId, abortSignal, provider: base }) }
|
|
472
|
+
: {}),
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
}
|