@botbotgo/agent-harness 0.0.420 → 0.0.422

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.
@@ -1,2 +1,2 @@
1
- export declare const AGENT_HARNESS_VERSION = "0.0.420";
1
+ export declare const AGENT_HARNESS_VERSION = "0.0.422";
2
2
  export declare const AGENT_HARNESS_RELEASE_DATE = "2026-05-03";
@@ -1,2 +1,2 @@
1
- export const AGENT_HARNESS_VERSION = "0.0.420";
1
+ export const AGENT_HARNESS_VERSION = "0.0.422";
2
2
  export const AGENT_HARNESS_RELEASE_DATE = "2026-05-03";
@@ -16,6 +16,16 @@ export declare function executeRequestInvocation(options: {
16
16
  toolRuntimeContext?: Record<string, unknown>;
17
17
  suppressInitialRequiredPlanInstruction?: boolean;
18
18
  externalPlanEvidence?: boolean;
19
+ externalPlanEvidenceTool?: {
20
+ name: string;
21
+ args?: Record<string, unknown>;
22
+ id?: string;
23
+ };
24
+ externalPlanEvidenceTools?: Array<{
25
+ name: string;
26
+ args?: Record<string, unknown>;
27
+ id?: string;
28
+ }>;
19
29
  };
20
30
  resolveTools: (tools: CompiledTool[], binding?: CompiledAgentBinding) => unknown[];
21
31
  getToolNameMapping: (binding: CompiledAgentBinding) => ToolNameMapping;
@@ -339,6 +339,8 @@ export async function executeRequestInvocation(options) {
339
339
  callRuntimeWithToolParseRecovery: options.callRuntimeWithToolParseRecovery,
340
340
  toolRuntimeContext: invokeOptions.toolRuntimeContext,
341
341
  externalPlanEvidence: invokeOptions.externalPlanEvidence,
342
+ externalPlanEvidenceTool: invokeOptions.externalPlanEvidenceTool,
343
+ externalPlanEvidenceTools: invokeOptions.externalPlanEvidenceTools,
342
344
  });
343
345
  let localOrUpstreamInvocation = await invokeOnce(request);
344
346
  if (options.resumePayload === undefined
@@ -18,6 +18,16 @@ export declare function invokeRuntimeWithLocalTools(options: {
18
18
  callRuntimeWithToolParseRecovery: (request: unknown) => Promise<Record<string, unknown>>;
19
19
  toolRuntimeContext?: Record<string, unknown>;
20
20
  externalPlanEvidence?: boolean;
21
+ externalPlanEvidenceTool?: {
22
+ name: string;
23
+ args?: Record<string, unknown>;
24
+ id?: string;
25
+ };
26
+ externalPlanEvidenceTools?: Array<{
27
+ name: string;
28
+ args?: Record<string, unknown>;
29
+ id?: string;
30
+ }>;
21
31
  }): Promise<{
22
32
  result: Record<string, unknown>;
23
33
  executedToolResults: ExecutedToolResult[];
@@ -16,5 +16,7 @@ export async function invokeRuntimeWithLocalTools(options) {
16
16
  callRuntimeWithToolParseRecovery: options.callRuntimeWithToolParseRecovery,
17
17
  toolRuntimeContext: options.toolRuntimeContext,
18
18
  externalPlanEvidence: options.externalPlanEvidence,
19
+ externalPlanEvidenceTool: options.externalPlanEvidenceTool,
20
+ externalPlanEvidenceTools: options.externalPlanEvidenceTools,
19
21
  });
20
22
  }
@@ -23,6 +23,16 @@ export declare function streamRuntimeExecution(options: {
23
23
  toolRuntimeContext?: Record<string, unknown>;
24
24
  suppressInitialRequiredPlanInstruction?: boolean;
25
25
  externalPlanEvidence?: boolean;
26
+ externalPlanEvidenceTool?: {
27
+ name: string;
28
+ args?: Record<string, unknown>;
29
+ id?: string;
30
+ };
31
+ externalPlanEvidenceTools?: Array<{
32
+ name: string;
33
+ args?: Record<string, unknown>;
34
+ id?: string;
35
+ }>;
26
36
  };
27
37
  primaryTools: CompiledTool[];
28
38
  toolNameMapping: ToolNameMapping;
@@ -51,6 +61,16 @@ export declare function streamRuntimeExecution(options: {
51
61
  toolRuntimeContext?: Record<string, unknown>;
52
62
  suppressInitialRequiredPlanInstruction?: boolean;
53
63
  externalPlanEvidence?: boolean;
64
+ externalPlanEvidenceTool?: {
65
+ name: string;
66
+ args?: Record<string, unknown>;
67
+ id?: string;
68
+ };
69
+ externalPlanEvidenceTools?: Array<{
70
+ name: string;
71
+ args?: Record<string, unknown>;
72
+ id?: string;
73
+ }>;
54
74
  }) => Promise<{
55
75
  output: string;
56
76
  metadata?: Record<string, unknown>;
@@ -128,6 +128,182 @@ function hasIncompletePlanOutput(value) {
128
128
  }
129
129
  return null;
130
130
  }
131
+ function extractInProgressTodoContents(value) {
132
+ if (typeof value !== "object" || value === null) {
133
+ return [];
134
+ }
135
+ const typed = value;
136
+ const arrays = [typed.todos, typed.items];
137
+ const contents = [];
138
+ for (const candidate of arrays) {
139
+ if (!Array.isArray(candidate)) {
140
+ continue;
141
+ }
142
+ for (const todo of candidate) {
143
+ if (typeof todo !== "object" || todo === null) {
144
+ continue;
145
+ }
146
+ const item = todo;
147
+ if (item.status !== "in_progress") {
148
+ continue;
149
+ }
150
+ const content = [item.content, item.description, item.title, item.name, item.text]
151
+ .find((value) => typeof value === "string" && value.trim().length > 0);
152
+ if (content) {
153
+ contents.push(content.trim());
154
+ }
155
+ }
156
+ }
157
+ for (const nested of [typed.summary, typed.update, typed.data, typed.output]) {
158
+ contents.push(...extractInProgressTodoContents(nested));
159
+ }
160
+ return [...new Set(contents)];
161
+ }
162
+ function extractTodoContentsForEvidenceResolution(value) {
163
+ const inProgress = extractTodoContentsByStatus(value, "in_progress");
164
+ if (inProgress.length > 0) {
165
+ return inProgress;
166
+ }
167
+ return extractTodoContentsByStatus(value, "pending");
168
+ }
169
+ function extractTodoContentsByStatus(value, status) {
170
+ if (Array.isArray(value)) {
171
+ return value.flatMap((item) => extractTodoContentsByStatus(item, status));
172
+ }
173
+ if (typeof value !== "object" || value === null) {
174
+ return [];
175
+ }
176
+ const typed = value;
177
+ const arrays = [typed.todos, typed.items];
178
+ const contents = [];
179
+ for (const array of arrays) {
180
+ if (!Array.isArray(array)) {
181
+ continue;
182
+ }
183
+ for (const item of array) {
184
+ if (typeof item !== "object" || item === null) {
185
+ continue;
186
+ }
187
+ const typedItem = item;
188
+ if (typedItem.status !== status) {
189
+ continue;
190
+ }
191
+ const content = [
192
+ typedItem.content,
193
+ typedItem.description,
194
+ typedItem.title,
195
+ typedItem.name,
196
+ typedItem.text,
197
+ ].find((value) => typeof value === "string" && value.trim().length > 0);
198
+ if (content) {
199
+ contents.push(content.trim());
200
+ }
201
+ }
202
+ }
203
+ for (const nested of [typed.summary, typed.update, typed.data, typed.output]) {
204
+ contents.push(...extractTodoContentsByStatus(nested, status));
205
+ }
206
+ return [...new Set(contents)];
207
+ }
208
+ function buildRunCommittedTodoEvidenceInstruction(primaryTools, planToolOutput) {
209
+ const todoContents = extractInProgressTodoContents(planToolOutput);
210
+ const todoText = todoContents.length > 0
211
+ ? todoContents.map((content, index) => `${index + 1}. ${content}`).join("\n")
212
+ : "(no in-progress todo content was readable)";
213
+ return [
214
+ buildRunEvidenceAfterPlanInstruction(primaryTools),
215
+ "",
216
+ "The completed write_todos result contains these in-progress evidence commitments:",
217
+ todoText,
218
+ "",
219
+ "Your next action must execute the non-planning tool named by the in-progress TODO. If every TODO is pending, execute the non-planning tool named by the first pending TODO. Do not call write_todos or read_todos again before that evidence tool returns.",
220
+ ].join("\n");
221
+ }
222
+ function resolveCommittedPlanEvidenceTool(primaryTools, planToolOutput) {
223
+ return resolveCommittedPlanEvidenceTools(primaryTools, planToolOutput)[0];
224
+ }
225
+ function resolveCommittedPlanEvidenceTools(primaryTools, planToolOutput) {
226
+ const availableToolNames = primaryTools
227
+ .map(readPrimaryToolName)
228
+ .filter((name) => name.length > 0 && !isPlanToolName(name));
229
+ const todoContents = extractTodoContentsForEvidenceResolution(planToolOutput);
230
+ if (todoContents.length === 0) {
231
+ return [];
232
+ }
233
+ const toolsByName = new Map(primaryTools.map((tool) => [tool.name, tool]));
234
+ const resolved = [];
235
+ const seen = new Set();
236
+ for (const content of todoContents) {
237
+ const todoText = content.toLowerCase();
238
+ const matches = availableToolNames.filter((name) => todoText.includes(name.toLowerCase()));
239
+ const selectedNames = matches.length === 1
240
+ ? [matches[0]]
241
+ : resolveBestScoredToolNames(availableToolNames, toolsByName, todoText);
242
+ for (const selectedName of selectedNames) {
243
+ if (seen.has(selectedName)) {
244
+ continue;
245
+ }
246
+ seen.add(selectedName);
247
+ const matchedTool = toolsByName.get(selectedName);
248
+ const args = buildCommittedPlanEvidenceToolArgs(matchedTool, content);
249
+ resolved.push({
250
+ name: selectedName,
251
+ args,
252
+ id: `stream-committed-plan-evidence-tool-${resolved.length + 1}`,
253
+ });
254
+ }
255
+ }
256
+ return resolved;
257
+ }
258
+ function extractSelectionTokens(value) {
259
+ const tokens = new Set();
260
+ for (const match of value.matchAll(/[\p{L}\p{N}_-]+/gu)) {
261
+ const token = match[0].toLowerCase();
262
+ if (token.length >= 2) {
263
+ tokens.add(token);
264
+ }
265
+ for (const part of token.split(/[_-]+/u)) {
266
+ if (part.length >= 2) {
267
+ tokens.add(part);
268
+ }
269
+ }
270
+ }
271
+ return tokens;
272
+ }
273
+ function resolveBestScoredToolNames(availableToolNames, toolsByName, todoText) {
274
+ const requestTokens = extractSelectionTokens(todoText);
275
+ const scored = availableToolNames
276
+ .map((name) => {
277
+ const tool = toolsByName.get(name);
278
+ const toolTokens = extractSelectionTokens(`${name} ${tool?.description ?? ""}`);
279
+ const toolNameTokens = extractSelectionTokens(name);
280
+ let score = 0;
281
+ for (const token of requestTokens) {
282
+ if (toolNameTokens.has(token)) {
283
+ score += 10;
284
+ continue;
285
+ }
286
+ if (toolTokens.has(token)) {
287
+ score += token.length > 3 ? 2 : 1;
288
+ }
289
+ }
290
+ return { name, score };
291
+ })
292
+ .filter((item) => item.score > 0)
293
+ .sort((left, right) => right.score - left.score);
294
+ const topScore = scored[0]?.score ?? 0;
295
+ return topScore > 0 ? scored.filter((item) => item.score === topScore).map((item) => item.name) : [];
296
+ }
297
+ function buildCommittedPlanEvidenceToolArgs(tool, todoText) {
298
+ const properties = typeof tool?.modelSchema === "object" && tool.modelSchema !== null
299
+ ? tool.modelSchema.properties
300
+ : undefined;
301
+ if (typeof properties !== "object" || properties === null) {
302
+ return {};
303
+ }
304
+ const queryLikeField = ["query", "question", "prompt", "input", "text"].find((field) => Object.prototype.hasOwnProperty.call(properties, field));
305
+ return queryLikeField ? { [queryLikeField]: todoText } : {};
306
+ }
131
307
  function normalizePlanToolName(toolName) {
132
308
  return typeof toolName === "string" ? toolName.trim().toLowerCase().replace(/[\s-]+/gu, "_") : "";
133
309
  }
@@ -655,6 +831,7 @@ export async function* streamRuntimeExecution(options) {
655
831
  let sawCompletedPlanToolResult = false;
656
832
  let sawSuccessfulNonTodoToolResult = false;
657
833
  let earlyStreamRecoveryInstruction = null;
834
+ let earlyStreamExternalPlanEvidenceTools;
658
835
  let earlyStreamRecoverySuppressInitialPlan = false;
659
836
  let completedPlanToolResultCount = 0;
660
837
  for await (const event of options.iterateWithTimeout(events, options.streamIdleTimeoutMs, "agent streamEvents", options.streamDeadlineAt, options.invokeTimeoutMs)) {
@@ -677,12 +854,15 @@ export async function* streamRuntimeExecution(options) {
677
854
  && chunk.kind !== "content"
678
855
  && !(chunk.kind === "tool-result" && isPlanToolName(chunk.toolName))
679
856
  && !(chunk.kind === "tool-result" && chunk.isError === true && isRetrySafeInvalidToolSelectionError(chunk.output)));
857
+ const hadPriorPlanToolResult = completedPlanToolResultCount > 0;
680
858
  const repeatedPlanToolResultBeforeEvidence = requiresPlanEvidence(options.binding)
681
859
  && !sawSuccessfulNonTodoToolResult
682
- && completedPlanToolResultCount > 0
860
+ && hadPriorPlanToolResult
683
861
  && projectedChunks.some((chunk) => chunk.kind === "tool-result" && isPlanToolName(chunk.toolName));
684
862
  if (repeatedPlanToolResultBeforeEvidence) {
685
- earlyStreamRecoveryInstruction = buildRunEvidenceAfterPlanInstruction(options.primaryTools);
863
+ const planToolResult = projectedChunks.find((chunk) => chunk.kind === "tool-result" && isPlanToolName(chunk.toolName) && chunk.isError !== true);
864
+ earlyStreamExternalPlanEvidenceTools = resolveCommittedPlanEvidenceTools(options.primaryTools, planToolResult?.kind === "tool-result" ? planToolResult.output : undefined);
865
+ earlyStreamRecoveryInstruction = buildRunCommittedTodoEvidenceInstruction(options.primaryTools, planToolResult?.kind === "tool-result" ? planToolResult.output : undefined);
686
866
  earlyStreamRecoverySuppressInitialPlan = true;
687
867
  break;
688
868
  }
@@ -721,6 +901,18 @@ export async function* streamRuntimeExecution(options) {
721
901
  }
722
902
  yield chunk;
723
903
  }
904
+ const eventContainsPlanToolResult = projectedChunks.some((chunk) => chunk.kind === "tool-result" && isPlanToolName(chunk.toolName) && chunk.isError !== true);
905
+ if (requiresPlanEvidence(options.binding)
906
+ && eventContainsPlanToolResult
907
+ && (hadPriorPlanToolResult
908
+ || projectedChunks.some((chunk) => isCompletedPlanToolResultChunk(chunk)))
909
+ && !sawSuccessfulNonTodoToolResult) {
910
+ const planToolResult = projectedChunks.find((chunk) => chunk.kind === "tool-result" && isPlanToolName(chunk.toolName) && chunk.isError !== true);
911
+ earlyStreamExternalPlanEvidenceTools = resolveCommittedPlanEvidenceTools(options.primaryTools, planToolResult?.kind === "tool-result" ? planToolResult.output : undefined);
912
+ earlyStreamRecoveryInstruction = buildRunCommittedTodoEvidenceInstruction(options.primaryTools, planToolResult?.kind === "tool-result" ? planToolResult.output : undefined);
913
+ earlyStreamRecoverySuppressInitialPlan = true;
914
+ break;
915
+ }
724
916
  if (requiresPlanEvidence(options.binding) && sawCompletedPlanToolResult && sawSuccessfulNonTodoToolResult) {
725
917
  if (hasUsefulVisibleSynthesis(projectionState.emittedOutput)) {
726
918
  if (deferredStreamContent.length > 0) {
@@ -794,7 +986,13 @@ export async function* streamRuntimeExecution(options) {
794
986
  }
795
987
  if (earlyStreamRecoveryInstruction) {
796
988
  const earlyRecoveryRuntimeOptions = earlyStreamRecoverySuppressInitialPlan
797
- ? withSuppressedInitialRequiredPlanInstruction(options.runtimeOptions)
989
+ ? {
990
+ ...withSuppressedInitialRequiredPlanInstruction(options.runtimeOptions),
991
+ externalPlanEvidence: true,
992
+ ...(earlyStreamExternalPlanEvidenceTools && earlyStreamExternalPlanEvidenceTools.length > 0
993
+ ? { externalPlanEvidenceTools: earlyStreamExternalPlanEvidenceTools }
994
+ : {}),
995
+ }
798
996
  : options.runtimeOptions;
799
997
  const recovered = await options.invoke(options.applyToolRecoveryInstruction(options.binding, earlyStreamRecoveryInstruction), options.input, options.sessionId, options.runtimeOptions.requestId ?? options.sessionId, undefined, options.history, earlyRecoveryRuntimeOptions);
800
998
  const recoveredToolResults = Array.isArray(recovered.metadata?.executedToolResults)
@@ -16,10 +16,20 @@ type LocalToolInvocationParams = {
16
16
  callRuntimeWithToolParseRecovery: (request: unknown) => Promise<Record<string, unknown>>;
17
17
  toolRuntimeContext?: Record<string, unknown>;
18
18
  externalPlanEvidence?: boolean;
19
+ externalPlanEvidenceTool?: {
20
+ name: string;
21
+ args?: Record<string, unknown>;
22
+ id?: string;
23
+ };
24
+ externalPlanEvidenceTools?: Array<{
25
+ name: string;
26
+ args?: Record<string, unknown>;
27
+ id?: string;
28
+ }>;
19
29
  };
20
30
  type LocalToolInvocationResult = {
21
31
  result: Record<string, unknown>;
22
32
  executedToolResults: ExecutedToolResult[];
23
33
  };
24
- export declare function runLocalToolInvocationLoop({ binding, request, primaryTools, toolNameMapping, executableTools, builtinExecutableTools, callRuntimeWithToolParseRecovery, toolRuntimeContext, externalPlanEvidence, }: LocalToolInvocationParams): Promise<LocalToolInvocationResult>;
34
+ export declare function runLocalToolInvocationLoop({ binding, request, primaryTools, toolNameMapping, executableTools, builtinExecutableTools, callRuntimeWithToolParseRecovery, toolRuntimeContext, externalPlanEvidence, externalPlanEvidenceTool, externalPlanEvidenceTools, }: LocalToolInvocationParams): Promise<LocalToolInvocationResult>;
25
35
  export {};
@@ -27,20 +27,57 @@ function stringifyRequestForToolSelection(request) {
27
27
  return "";
28
28
  }
29
29
  }
30
+ function extractSelectionTokens(value) {
31
+ const tokens = new Set();
32
+ for (const match of value.matchAll(/[\p{L}\p{N}_-]+/gu)) {
33
+ const token = match[0].toLowerCase();
34
+ if (token.length >= 2) {
35
+ tokens.add(token);
36
+ }
37
+ }
38
+ for (const match of value.matchAll(/[\p{Script=Han}]{2,}/gu)) {
39
+ const sequence = match[0];
40
+ for (let size = 2; size <= Math.min(4, sequence.length); size += 1) {
41
+ for (let index = 0; index <= sequence.length - size; index += 1) {
42
+ tokens.add(sequence.slice(index, index + size).toLowerCase());
43
+ }
44
+ }
45
+ }
46
+ return tokens;
47
+ }
30
48
  function prioritizeBootstrapEvidenceTools(primaryTools, request) {
31
49
  const requestText = stringifyRequestForToolSelection(request);
50
+ const requestTokens = extractSelectionTokens(requestText);
32
51
  const isFinanceRequest = /\b(?:stock|ticker|finance|market|valuation|quote)\b|股票|股价|行情|估值|财报/iu.test(requestText);
33
52
  const evidenceTools = primaryTools
34
- .map((tool) => typeof tool.name === "string" ? tool.name.trim() : "")
35
- .filter((name) => name.length > 0 && !isPlanToolName(name))
53
+ .map((tool) => {
54
+ const name = typeof tool.name === "string" ? tool.name.trim() : "";
55
+ const description = typeof tool.description === "string" ? tool.description : "";
56
+ const toolTokens = extractSelectionTokens(`${name} ${description}`);
57
+ let score = 0;
58
+ for (const token of requestTokens) {
59
+ if (toolTokens.has(token)) {
60
+ score += token.length > 3 ? 2 : 1;
61
+ }
62
+ }
63
+ if (requestText.toLowerCase().includes(name.toLowerCase())) {
64
+ score += 6;
65
+ }
66
+ return { name, score };
67
+ })
68
+ .filter((tool) => tool.name.length > 0 && !isPlanToolName(tool.name))
36
69
  .sort((left, right) => {
70
+ if (right.score !== left.score) {
71
+ return right.score - left.score;
72
+ }
37
73
  if (!isFinanceRequest) {
38
74
  return 0;
39
75
  }
40
- const leftFinance = left.includes("finance") ? 0 : 1;
41
- const rightFinance = right.includes("finance") ? 0 : 1;
76
+ const leftFinance = left.name.includes("finance") ? 0 : 1;
77
+ const rightFinance = right.name.includes("finance") ? 0 : 1;
42
78
  return leftFinance - rightFinance;
43
- });
79
+ })
80
+ .map((tool) => tool.name);
44
81
  return evidenceTools.slice(0, 4);
45
82
  }
46
83
  function createBootstrapTodoPlan(primaryTools, request) {
@@ -90,6 +127,19 @@ function buildBootstrapPlanToolResult(primaryTools, request) {
90
127
  })],
91
128
  };
92
129
  }
130
+ function buildExternalPlanEvidenceToolResult(tools) {
131
+ return {
132
+ messages: [{
133
+ content: "",
134
+ tool_calls: tools.map((tool, index) => ({
135
+ id: tool.id ?? `external-plan-evidence-${index + 1}-${Math.random().toString(36).slice(2, 10)}`,
136
+ name: tool.name,
137
+ args: tool.args ?? {},
138
+ type: "tool_call",
139
+ })),
140
+ }],
141
+ };
142
+ }
93
143
  function readPlanStateSummary(output) {
94
144
  if (typeof output !== "object" || output === null) {
95
145
  return null;
@@ -210,6 +260,76 @@ function terminalToolErrorRecoveryInstruction(terminalText) {
210
260
  function requiresPlanEvidence(binding) {
211
261
  return binding.harnessRuntime.executionContract?.requiresPlan === true;
212
262
  }
263
+ function resolveCommittedTodoEvidenceTool(executedToolResults, primaryTools) {
264
+ const availableTools = primaryTools
265
+ .filter((tool) => typeof tool.name === "string" && tool.name.length > 0 && !isPlanToolName(tool.name));
266
+ if (availableTools.length === 0) {
267
+ return null;
268
+ }
269
+ for (let index = executedToolResults.length - 1; index >= 0; index -= 1) {
270
+ const result = executedToolResults[index];
271
+ if (!result || result.isError === true || !isPlanToolName(result.toolName)) {
272
+ continue;
273
+ }
274
+ const output = result.output;
275
+ const summary = typeof output === "object" && output !== null
276
+ ? output.summary
277
+ : undefined;
278
+ const items = typeof summary === "object" && summary !== null && Array.isArray(summary.items)
279
+ ? summary.items
280
+ : [];
281
+ const activeItems = items.filter((item) => item.status === "in_progress");
282
+ const candidateItems = activeItems.length > 0
283
+ ? activeItems
284
+ : items.filter((item) => item.status === "pending").slice(0, 1);
285
+ for (const item of candidateItems) {
286
+ const content = [
287
+ item.content,
288
+ item.description,
289
+ item.title,
290
+ item.name,
291
+ item.text,
292
+ ].filter((value) => typeof value === "string").join(" ").toLowerCase();
293
+ const matched = availableTools.map((tool) => tool.name).filter((toolName) => content.includes(toolName.toLowerCase()));
294
+ if (matched.length === 1) {
295
+ return {
296
+ name: matched[0],
297
+ args: {},
298
+ id: `todo-committed-evidence-${index}`,
299
+ };
300
+ }
301
+ const requestTokens = extractSelectionTokens(content);
302
+ const scored = availableTools
303
+ .map((tool) => {
304
+ const toolTokens = extractSelectionTokens(`${tool.name} ${tool.description ?? ""}`);
305
+ let score = 0;
306
+ for (const token of requestTokens) {
307
+ if (toolTokens.has(token)) {
308
+ score += token.length > 3 ? 2 : 1;
309
+ }
310
+ }
311
+ return { name: tool.name, score };
312
+ })
313
+ .filter((item) => item.score > 0)
314
+ .sort((left, right) => right.score - left.score);
315
+ if (scored[0] && (!scored[1] || scored[0].score > scored[1].score)) {
316
+ return {
317
+ name: scored[0].name,
318
+ args: {},
319
+ id: `todo-committed-evidence-${index}`,
320
+ };
321
+ }
322
+ if (matched.length === 1) {
323
+ return {
324
+ name: matched[0],
325
+ args: {},
326
+ id: `todo-committed-evidence-${index}`,
327
+ };
328
+ }
329
+ }
330
+ }
331
+ return null;
332
+ }
213
333
  function extractLatestUserInput(request) {
214
334
  const typedRequest = request;
215
335
  const messages = Array.isArray(typedRequest.messages) ? typedRequest.messages : [];
@@ -268,7 +388,7 @@ function summarizeResultMessages(result) {
268
388
  };
269
389
  });
270
390
  }
271
- export async function runLocalToolInvocationLoop({ binding, request, primaryTools, toolNameMapping, executableTools, builtinExecutableTools, callRuntimeWithToolParseRecovery, toolRuntimeContext, externalPlanEvidence, }) {
391
+ export async function runLocalToolInvocationLoop({ binding, request, primaryTools, toolNameMapping, executableTools, builtinExecutableTools, callRuntimeWithToolParseRecovery, toolRuntimeContext, externalPlanEvidence, externalPlanEvidenceTool, externalPlanEvidenceTools, }) {
272
392
  const executedToolResults = [];
273
393
  let activeRequest = request;
274
394
  let currentMessages = Array.isArray(activeRequest.messages) ? [...activeRequest.messages] : [];
@@ -289,9 +409,39 @@ export async function runLocalToolInvocationLoop({ binding, request, primaryTool
289
409
  }
290
410
  for (let iteration = 0; iteration < maxToolIterations; iteration += 1) {
291
411
  const isFinalIteration = iteration + 1 === maxToolIterations;
292
- result = pendingResult ?? await callRuntimeWithToolParseRecovery(activeRequest);
412
+ const externalPlanEvidenceToolCalls = externalPlanEvidenceTools && externalPlanEvidenceTools.length > 0
413
+ ? externalPlanEvidenceTools
414
+ : externalPlanEvidenceTool
415
+ ? [externalPlanEvidenceTool]
416
+ : [];
417
+ const shouldRunExternalPlanEvidenceTool = pendingResult === undefined
418
+ && requiresPlanEvidence(binding)
419
+ && externalPlanEvidence === true
420
+ && externalPlanEvidenceToolCalls.length > 0
421
+ && !hasNonTodoToolEvidence(executedToolResults);
422
+ const usedExternalPlanEvidenceToolThisIteration = shouldRunExternalPlanEvidenceTool;
423
+ result = pendingResult
424
+ ?? (shouldRunExternalPlanEvidenceTool
425
+ ? buildExternalPlanEvidenceToolResult(externalPlanEvidenceToolCalls)
426
+ : await callRuntimeWithToolParseRecovery(activeRequest));
293
427
  pendingResult = undefined;
294
- const toolCalls = extractToolCallsFromResult(result);
428
+ let toolCalls = extractToolCallsFromResult(result);
429
+ const committedTodoEvidenceTool = requiresPlanEvidence(binding)
430
+ && hasPlanStateEvidence(executedToolResults, externalPlanEvidence)
431
+ && !hasNonTodoToolEvidence(executedToolResults)
432
+ && (externalPlanEvidenceTool !== undefined || !hasIncompleteExecutedPlan(executedToolResults, externalPlanEvidence))
433
+ && (toolCalls.length === 0 || toolCalls.every((toolCall) => isPlanToolName(toolCall.name)))
434
+ ? externalPlanEvidenceTool
435
+ ? {
436
+ name: externalPlanEvidenceTool.name,
437
+ args: externalPlanEvidenceTool.args ?? {},
438
+ id: externalPlanEvidenceTool.id ?? "external-plan-evidence-tool",
439
+ }
440
+ : resolveCommittedTodoEvidenceTool(executedToolResults, primaryTools)
441
+ : null;
442
+ if (committedTodoEvidenceTool) {
443
+ toolCalls = [committedTodoEvidenceTool];
444
+ }
295
445
  if (toolCalls.length === 0) {
296
446
  const terminalText = sanitizeVisibleText(extractVisibleOutput(result) || "");
297
447
  const hasIncompletePlanState = hasIncompleteExecutedPlan(executedToolResults, externalPlanEvidence);
@@ -415,7 +565,8 @@ export async function runLocalToolInvocationLoop({ binding, request, primaryTool
415
565
  }
416
566
  repeatedRecoveryWithoutProgress = 0;
417
567
  repeatedPlanOnlyAfterPlan = 0;
418
- const canReplayToolCalls = canReplayToolCallsLocally(binding, toolCalls, primaryTools, toolNameMapping, executableTools, builtinExecutableTools);
568
+ const canReplayToolCalls = usedExternalPlanEvidenceToolThisIteration
569
+ || canReplayToolCallsLocally(binding, toolCalls, primaryTools, toolNameMapping, executableTools, builtinExecutableTools);
419
570
  debugLocalToolReplay({
420
571
  toolCalls,
421
572
  result,
@@ -502,6 +653,67 @@ export async function runLocalToolInvocationLoop({ binding, request, primaryTool
502
653
  content: stringifyToolOutput(safeToolResult),
503
654
  }));
504
655
  }
656
+ const committedEvidenceTool = requiresPlanEvidence(binding)
657
+ && !hadNonTodoEvidenceBeforeToolReplay
658
+ && !hasNonTodoToolEvidence(executedToolResults)
659
+ && !hasIncompleteExecutedPlan(executedToolResults, externalPlanEvidence)
660
+ ? resolveCommittedTodoEvidenceTool(executedToolResults, primaryTools)
661
+ : null;
662
+ if (committedEvidenceTool) {
663
+ const resolvedToolName = resolveModelFacingToolName(committedEvidenceTool.name, toolNameMapping, primaryTools);
664
+ const executable = executableTools.get(committedEvidenceTool.name) ?? executableTools.get(resolvedToolName);
665
+ if (executable) {
666
+ const compiledTool = toolCatalog.get(committedEvidenceTool.name) ?? toolCatalog.get(resolvedToolName);
667
+ const normalizedArgs = normalizeToolArgsForSchema(committedEvidenceTool.args, executable.schema, undefined, {
668
+ latestUserInput,
669
+ });
670
+ const gateway = validateToolGatewayInput({
671
+ toolName: executable.name,
672
+ schema: executable.schema,
673
+ args: normalizedArgs,
674
+ requiresApproval: compiledTool ? toolRequiresRuntimeApproval(compiledTool) : false,
675
+ });
676
+ if (gateway.ok) {
677
+ const toolResult = toolRuntimeContext
678
+ ? await executable.invoke(gateway.input, { toolRuntimeContext })
679
+ : await executable.invoke(gateway.input);
680
+ const memoryCandidates = compiledTool ? extractMemoryCandidatesFromToolOutput(compiledTool, toolResult) : [];
681
+ const safeToolResult = await maybePersistLargeToolOutput({
682
+ toolName: executable.name,
683
+ output: toolResult,
684
+ toolRuntimeContext,
685
+ });
686
+ executedToolResults.push({
687
+ toolName: executable.name,
688
+ output: safeToolResult,
689
+ ...(memoryCandidates.length > 0 ? { memoryCandidates } : {}),
690
+ });
691
+ nextMessages.push(new ToolMessage({
692
+ name: executable.name,
693
+ tool_call_id: committedEvidenceTool.id,
694
+ content: stringifyToolOutput(safeToolResult),
695
+ }));
696
+ }
697
+ else {
698
+ executedToolResults.push({
699
+ toolName: executable.name,
700
+ output: gateway.error,
701
+ isError: true,
702
+ });
703
+ nextMessages.push(new ToolMessage({
704
+ name: executable.name,
705
+ tool_call_id: committedEvidenceTool.id,
706
+ content: stringifyToolOutput(gateway.error),
707
+ }));
708
+ }
709
+ }
710
+ }
711
+ if (usedExternalPlanEvidenceToolThisIteration && hasNonTodoToolEvidence(executedToolResults)) {
712
+ return {
713
+ result: buildDeterministicFinalFromToolEvidence(executedToolResults),
714
+ executedToolResults,
715
+ };
716
+ }
505
717
  if (requiresPlanEvidence(binding)
506
718
  && toolCalls.length > 0
507
719
  && toolCalls.every((toolCall) => isPlanToolName(toolCall.name))