@botbotgo/agent-harness 0.0.418 → 0.0.420

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.
Files changed (28) hide show
  1. package/dist/cli/chat-interactive.js +1 -1
  2. package/dist/cli/chat-stream.js +9 -1
  3. package/dist/package-version.d.ts +2 -2
  4. package/dist/package-version.js +2 -2
  5. package/dist/runtime/adapter/compat/openai-compatible.js +12 -0
  6. package/dist/runtime/adapter/flow/invocation-flow.d.ts +2 -0
  7. package/dist/runtime/adapter/flow/invocation-flow.js +13 -5
  8. package/dist/runtime/adapter/flow/invoke-runtime.d.ts +1 -0
  9. package/dist/runtime/adapter/flow/invoke-runtime.js +1 -0
  10. package/dist/runtime/adapter/flow/stream-runtime.d.ts +4 -0
  11. package/dist/runtime/adapter/flow/stream-runtime.js +177 -14
  12. package/dist/runtime/adapter/invocation-result.js +17 -6
  13. package/dist/runtime/adapter/local-tool-invocation.d.ts +2 -1
  14. package/dist/runtime/adapter/local-tool-invocation.js +268 -21
  15. package/dist/runtime/adapter/model/model-providers.js +269 -58
  16. package/dist/runtime/adapter/model/prompted-json-tool-call-capture.d.ts +9 -0
  17. package/dist/runtime/adapter/model/prompted-json-tool-call-capture.js +40 -0
  18. package/dist/runtime/adapter/runtime-adapter-support.js +58 -12
  19. package/dist/runtime/adapter/runtime-shell.js +3 -2
  20. package/dist/runtime/adapter/stream-event-projection.js +22 -5
  21. package/dist/runtime/adapter/tool/tool-arguments.js +157 -67
  22. package/dist/runtime/adapter/tool/tool-replay.js +0 -4
  23. package/dist/runtime/agent-runtime-adapter.d.ts +3 -0
  24. package/dist/runtime/agent-runtime-adapter.js +217 -73
  25. package/dist/runtime/harness/run/stream-run.js +31 -3
  26. package/dist/runtime/parsing/output-tool-args.js +108 -0
  27. package/dist/workspace/resource-compilers.js +17 -4
  28. package/package.json +1 -1
@@ -6,8 +6,10 @@ import { ChatOpenAI } from "@langchain/openai";
6
6
  import { AIMessage } from "langchain";
7
7
  import { initChatModel } from "langchain";
8
8
  import { salvageToolArgs, tryParseJson } from "../../parsing/output-parsing.js";
9
+ import { salvageJsonToolCalls } from "../../parsing/output-tool-args.js";
9
10
  import { normalizeModelFacingToolSchema } from "../tool/resolved-tool.js";
10
11
  import { normalizeOpenAICompatibleInit } from "../compat/openai-compatible.js";
12
+ import { recordPromptedJsonToolCall } from "./prompted-json-tool-call-capture.js";
11
13
  const NODE_LLAMA_CPP_TOOL_CALL_INSTRUCTION = [
12
14
  "Available tools are listed below.",
13
15
  "If you need a tool, respond with only one JSON object.",
@@ -17,6 +19,13 @@ const NODE_LLAMA_CPP_TOOL_CALL_INSTRUCTION = [
17
19
  "If the conversation already contains TOOL_RESULT for the requested work, answer from that result instead of repeating the same tool call.",
18
20
  "If no tool is needed, answer normally.",
19
21
  ].join("\n");
22
+ const FORCED_NODE_LLAMA_CPP_TOOL_CALL_INSTRUCTION = [
23
+ "Available tools are listed below.",
24
+ "You must call exactly one available tool now.",
25
+ 'Return only one JSON object with this exact shape: {"name":"tool_name","arguments":{"key":"value"}}',
26
+ "Do not add markdown, prose, explanations, analysis, or code fences.",
27
+ "Do not answer normally on this turn.",
28
+ ].join("\n");
20
29
  const PROMPTED_JSON_FINAL_TOOL_CALL_REMINDER = [
21
30
  "Final tool-call rule:",
22
31
  "If the correct next step is a tool call, return exactly one JSON object and no prose.",
@@ -192,17 +201,51 @@ function canonicalToolName(value) {
192
201
  .replace(/[^a-z0-9]+/g, "_")
193
202
  .replace(/^_+|_+$/g, "");
194
203
  }
204
+ function readToolCallId(value) {
205
+ if (typeof value !== "object" || value === null) {
206
+ return undefined;
207
+ }
208
+ const typed = value;
209
+ return typeof typed.id === "string"
210
+ ? typed.id
211
+ : typeof typed.tool_call_id === "string"
212
+ ? typed.tool_call_id
213
+ : undefined;
214
+ }
215
+ function buildToolResultNameLookup(messages) {
216
+ const namesByToolCallId = new Map();
217
+ for (const message of messages) {
218
+ if (mapMessageRole(message) !== "ASSISTANT") {
219
+ continue;
220
+ }
221
+ for (const toolCall of readToolCalls(message)) {
222
+ if (typeof toolCall !== "object" || toolCall === null) {
223
+ continue;
224
+ }
225
+ const id = readToolCallId(toolCall);
226
+ const name = typeof toolCall.name === "string"
227
+ ? toolCall.name
228
+ : "";
229
+ if (id && name) {
230
+ namesByToolCallId.set(id, name);
231
+ }
232
+ }
233
+ }
234
+ return namesByToolCallId;
235
+ }
195
236
  function hasPriorToolResultForToolName(input, toolName) {
196
237
  if (!toolName) {
197
238
  return false;
198
239
  }
199
240
  const expectedName = canonicalToolName(toolName);
200
241
  if (Array.isArray(input)) {
242
+ const namesByToolCallId = buildToolResultNameLookup(input);
201
243
  return input.some((message) => {
202
244
  if (mapMessageRole(message) !== "TOOL") {
203
245
  return false;
204
246
  }
205
- const observedName = readToolMessageMetadata(message).name;
247
+ const metadata = readToolMessageMetadata(message);
248
+ const observedName = metadata.name ?? (metadata.toolCallId ? namesByToolCallId.get(metadata.toolCallId) : undefined);
206
249
  return typeof observedName === "string" && canonicalToolName(observedName) === expectedName;
207
250
  });
208
251
  }
@@ -258,11 +301,31 @@ function readBoundToolName(tool) {
258
301
  ? tool.name.trim()
259
302
  : "";
260
303
  }
304
+ function readBoundToolDescription(tool) {
305
+ return typeof tool === "object" && tool !== null && typeof tool.description === "string"
306
+ ? tool.description.trim()
307
+ : "";
308
+ }
309
+ function summarizeSchemaKeys(schema) {
310
+ const normalized = normalizeModelFacingToolSchema(schema);
311
+ const properties = typeof normalized.properties === "object" && normalized.properties !== null
312
+ ? Object.keys(normalized.properties)
313
+ : [];
314
+ const required = Array.isArray(normalized.required)
315
+ ? normalized.required.filter((item) => typeof item === "string")
316
+ : [];
317
+ if (properties.length === 0) {
318
+ return "{}";
319
+ }
320
+ return `{${properties.map((key) => required.includes(key) ? `${key}:required` : `${key}:optional`).join(", ")}}`;
321
+ }
261
322
  function isTodoPlanningToolName(name) {
262
323
  return name === "write_todos"
263
324
  || name === "read_todos"
264
325
  || name === "tool_call_write_todos"
265
- || name === "tool_call_read_todos";
326
+ || name === "tool_call_read_todos"
327
+ || name === "call_write_todos"
328
+ || name === "call_read_todos";
266
329
  }
267
330
  function hasPriorNonPlanningToolResult(input, tools) {
268
331
  const toolNames = tools
@@ -270,19 +333,46 @@ function hasPriorNonPlanningToolResult(input, tools) {
270
333
  .filter((name) => name && !isTodoPlanningToolName(name));
271
334
  return toolNames.some((name) => hasPriorToolResultForToolName(input, name));
272
335
  }
273
- function shouldLimitToolsToPlanning(input) {
336
+ function hasPriorPlanningToolResult(input) {
337
+ return hasPriorToolResultForToolName(input, "write_todos")
338
+ || hasPriorToolResultForToolName(input, "tool_call_write_todos")
339
+ || hasPriorToolResultForToolName(input, "call_write_todos")
340
+ || hasPriorToolResultForToolName(input, "read_todos")
341
+ || hasPriorToolResultForToolName(input, "tool_call_read_todos")
342
+ || hasPriorToolResultForToolName(input, "call_read_todos");
343
+ }
344
+ function shouldLimitToolsToPlanning(input, boundTools) {
274
345
  const text = stringifyNodeLlamaCppInput(input);
275
346
  return text.includes("required visible planning contract")
276
347
  && !hasPriorToolResultForToolName(input, "write_todos")
277
- && !hasPriorToolResultForToolName(input, "tool_call_write_todos");
348
+ && !hasPriorToolResultForToolName(input, "tool_call_write_todos")
349
+ && !hasPriorToolResultForToolName(input, "call_write_todos")
350
+ && !hasPriorNonPlanningToolResult(input, boundTools);
278
351
  }
279
352
  function selectPlanningToolsForTurn(input, boundTools) {
280
- if (!shouldLimitToolsToPlanning(input)) {
353
+ if (!shouldLimitToolsToPlanning(input, boundTools)) {
281
354
  return boundTools;
282
355
  }
283
356
  const planningTools = boundTools.filter((tool) => isTodoPlanningToolName(readBoundToolName(tool)));
284
357
  return planningTools.length > 0 ? planningTools : boundTools;
285
358
  }
359
+ function shouldLimitToolsToNonPlanningEvidence(input, boundTools) {
360
+ const text = stringifyNodeLlamaCppInput(input);
361
+ const hasNonPlanningEvidenceInstruction = /non[-\s]?planning (?:evidence )?tool call|non[-\s]?TODO evidence tool|Do not call write_todos|Do not call write_todos or read_todos/i.test(text);
362
+ return (hasPriorPlanningToolResult(input) || hasNonPlanningEvidenceInstruction)
363
+ && !hasPriorNonPlanningToolResult(input, boundTools);
364
+ }
365
+ function selectNonPlanningToolsForTurn(boundTools) {
366
+ const nonPlanningTools = boundTools.filter((tool) => {
367
+ const name = readBoundToolName(tool);
368
+ return name.length > 0 && !isTodoPlanningToolName(name);
369
+ });
370
+ return nonPlanningTools.length > 0 ? nonPlanningTools : boundTools;
371
+ }
372
+ function isAllowedPromptedJsonToolCall(toolName, effectiveBoundTools) {
373
+ const allowedToolNames = new Set(effectiveBoundTools.map((tool) => readBoundToolName(tool)).filter(Boolean));
374
+ return allowedToolNames.has(toolName);
375
+ }
286
376
  function normalizeReadFileToolContent(name, content) {
287
377
  if (name !== "read_file") {
288
378
  return content;
@@ -408,6 +498,13 @@ function extractToolCallPayload(text) {
408
498
  if (direct) {
409
499
  return direct;
410
500
  }
501
+ const salvagedToolCall = salvageJsonToolCalls(trimmed)[0];
502
+ if (salvagedToolCall) {
503
+ return {
504
+ name: salvagedToolCall.name,
505
+ arguments: salvagedToolCall.args,
506
+ };
507
+ }
411
508
  const firstJsonObject = extractFirstJsonObjectPayload(trimmed);
412
509
  if (firstJsonObject) {
413
510
  return firstJsonObject;
@@ -547,37 +644,6 @@ function normalizeParsedToolCall(payload) {
547
644
  : salvageToolArgs(argsCandidate) ?? {};
548
645
  return { name, args };
549
646
  }
550
- function extractFallbackTodoContentsFromText(text) {
551
- const normalized = text.trim();
552
- if (!normalized) {
553
- return [];
554
- }
555
- const candidates = normalized
556
- .split(/\r?\n/)
557
- .map((line) => line
558
- .replace(/^\s*(?:[-*+]\s+|\d+[.)]\s+|\[[ x~!-]\]\s+)/i, "")
559
- .replace(/^\s*(?:todo|step|task)\s*\d*\s*[:.-]\s*/i, "")
560
- .trim())
561
- .filter((line) => line.length >= 12
562
- && !/^(?:status|summary|findings|blockers|next actions)\s*[::]?$/i.test(line)
563
- && !/\b(?:plan|todo|steps?)\s*[::]\s*$/i.test(line)
564
- && !isLowSignalPlanningLine(line));
565
- const seen = new Set();
566
- return candidates.filter((line) => {
567
- const key = line.toLowerCase();
568
- if (seen.has(key)) {
569
- return false;
570
- }
571
- seen.add(key);
572
- return true;
573
- }).slice(0, 6);
574
- }
575
- function isLowSignalPlanningLine(value) {
576
- const normalized = value.trim().toLowerCase();
577
- return (normalized.length < 12
578
- || /^#+\s*/.test(normalized)
579
- || /^(?:ok|okay|sure|understood|got it|plan|todo|steps?)\.?$/.test(normalized));
580
- }
581
647
  function buildFallbackTodoContents() {
582
648
  return [
583
649
  "Identify the concrete evidence tool required for this request",
@@ -586,20 +652,14 @@ function buildFallbackTodoContents() {
586
652
  "Return the final answer grounded in tool output",
587
653
  ];
588
654
  }
589
- function isGenericFallbackTodoContent(value) {
590
- return /^(?:gather concrete evidence|inspect the most relevant runtime signals|analyze (?:the )?evidence|produce the final rca)/i.test(value.trim());
591
- }
592
655
  function buildFallbackPlanningToolCall(input, planningTools, rawText) {
593
- const toolName = planningTools.map((tool) => readBoundToolName(tool)).find((name) => name === "write_todos" || name === "tool_call_write_todos");
656
+ void input;
657
+ void rawText;
658
+ const toolName = planningTools.map((tool) => readBoundToolName(tool)).find((name) => name === "write_todos" || name === "tool_call_write_todos" || name === "call_write_todos");
594
659
  if (!toolName) {
595
660
  return null;
596
661
  }
597
- const modelPlannedItems = extractFallbackTodoContentsFromText(rawText);
598
- const hasUsefulModelPlan = modelPlannedItems.length >= 2 && !modelPlannedItems.every(isGenericFallbackTodoContent);
599
- const fallbackItems = hasUsefulModelPlan
600
- ? modelPlannedItems
601
- : buildFallbackTodoContents();
602
- const todos = fallbackItems.map((content, index) => ({
662
+ const todos = buildFallbackTodoContents().map((content, index) => ({
603
663
  content,
604
664
  status: index === 0 ? "in_progress" : "pending",
605
665
  }));
@@ -618,7 +678,7 @@ function buildFallbackTodoCompletionToolCall(input, tools) {
618
678
  if (/TODO completed:|\[x\]/i.test(prompt)) {
619
679
  return null;
620
680
  }
621
- const planningToolName = tools.map((tool) => readBoundToolName(tool)).find((name) => name === "write_todos" || name === "tool_call_write_todos");
681
+ const planningToolName = tools.map((tool) => readBoundToolName(tool)).find((name) => name === "write_todos" || name === "tool_call_write_todos" || name === "call_write_todos");
622
682
  if (!planningToolName) {
623
683
  return null;
624
684
  }
@@ -683,29 +743,99 @@ function formatBoundToolInstruction(tool) {
683
743
  `Arguments JSON schema: ${JSON.stringify(schema)}`,
684
744
  ].filter(Boolean).join("\n");
685
745
  }
746
+ function formatCompactBoundToolInstruction(tool) {
747
+ const name = readBoundToolName(tool);
748
+ if (!name) {
749
+ return null;
750
+ }
751
+ const description = readBoundToolDescription(tool).split(/\n/u)[0]?.trim() ?? "";
752
+ return [
753
+ `Tool: ${name}`,
754
+ description ? `Description: ${description}` : "",
755
+ `Arguments keys: ${summarizeSchemaKeys(tool)}`,
756
+ ].filter(Boolean).join("\n");
757
+ }
686
758
  function withPromptedJsonToolPrompt(input, tools, options = {}) {
687
759
  const toolInstructions = tools.map((tool) => formatBoundToolInstruction(tool)).filter((value) => Boolean(value));
760
+ const compactToolInstructions = tools.map((tool) => formatCompactBoundToolInstruction(tool)).filter((value) => Boolean(value));
688
761
  if (toolInstructions.length === 0) {
689
762
  return stringifyNodeLlamaCppInput(input);
690
763
  }
691
- const forcedPlanningInstruction = options.forcePlanningToolCall
764
+ const forcedToolInstruction = options.forceToolCall === "planning"
692
765
  ? [
693
766
  "Required planning tool call:",
694
767
  "You must call write_todos now before any domain tool or final answer.",
695
768
  "Return exactly one JSON object for write_todos with concrete todo items and statuses.",
696
769
  "Do not write prose, markdown, analysis, or a plain-text plan.",
697
770
  ].join("\n")
698
- : "";
699
- const systemContent = `${NODE_LLAMA_CPP_TOOL_CALL_INSTRUCTION}\n\n${toolInstructions.join("\n\n")}`;
771
+ : options.forceToolCall === "nonPlanningEvidence"
772
+ ? [
773
+ "Required evidence tool call:",
774
+ "A todo board already exists. Your next action must be exactly one non-planning tool call chosen from the available tool descriptions and schemas.",
775
+ "Do not call write_todos or read_todos now.",
776
+ "Do not write prose, markdown, analysis, or a plain-text plan.",
777
+ ].join("\n")
778
+ : "";
779
+ const baseToolInstruction = options.forceToolCall
780
+ ? FORCED_NODE_LLAMA_CPP_TOOL_CALL_INSTRUCTION
781
+ : NODE_LLAMA_CPP_TOOL_CALL_INSTRUCTION;
782
+ const systemContent = [
783
+ baseToolInstruction,
784
+ forcedToolInstruction,
785
+ (options.forceToolCall ? compactToolInstructions : toolInstructions).join("\n\n"),
786
+ forcedToolInstruction,
787
+ ].filter(Boolean).join("\n\n");
700
788
  const prompt = stringifyNodeLlamaCppInput(input);
701
789
  return [
702
790
  options.suppressThinking ? NO_THINK_CONTROL_TOKEN : "",
703
791
  systemContent,
704
- forcedPlanningInstruction,
792
+ forcedToolInstruction,
705
793
  prompt,
706
794
  PROMPTED_JSON_FINAL_TOOL_CALL_REMINDER,
707
795
  ].filter(Boolean).join("\n\n");
708
796
  }
797
+ function debugPromptedJsonTurn(input) {
798
+ if (process.env.AGENT_HARNESS_PROMPTED_JSON_DEBUG !== "1") {
799
+ return;
800
+ }
801
+ const promptText = typeof input.prompt === "string" ? input.prompt : stringifyNodeLlamaCppInput(input.prompt);
802
+ console.error(JSON.stringify({
803
+ type: "prompted-json-turn",
804
+ forcePlanningToolCall: input.forcePlanningToolCall,
805
+ forceNonPlanningEvidenceToolCall: input.forceNonPlanningEvidenceToolCall,
806
+ effectiveToolNames: input.effectiveToolNames,
807
+ inputSummary: summarizePromptedJsonInput(input.rawInput),
808
+ promptHead: promptText.slice(0, 2000),
809
+ }));
810
+ }
811
+ function debugPromptedJsonResult(input) {
812
+ if (process.env.AGENT_HARNESS_PROMPTED_JSON_DEBUG !== "1") {
813
+ return;
814
+ }
815
+ console.error(JSON.stringify({
816
+ type: "prompted-json-result",
817
+ forcePlanningToolCall: input.forcePlanningToolCall,
818
+ forceNonPlanningEvidenceToolCall: input.forceNonPlanningEvidenceToolCall,
819
+ parsedToolName: input.parsedToolName ?? null,
820
+ textHead: input.text.slice(0, 2000),
821
+ }));
822
+ }
823
+ function summarizePromptedJsonInput(input) {
824
+ const messages = typeof input === "object" && input !== null && Array.isArray(input.messages)
825
+ ? input.messages
826
+ : Array.isArray(input)
827
+ ? input
828
+ : [];
829
+ return messages.slice(-8).map((message) => ({
830
+ role: mapMessageRole(message),
831
+ type: readMessageType(message),
832
+ name: readToolMessageMetadata(message).name,
833
+ toolCallNames: readToolCalls(message).map((toolCall) => typeof toolCall === "object" && toolCall !== null && typeof toolCall.name === "string"
834
+ ? toolCall.name
835
+ : ""),
836
+ contentHead: readPromptContent(message).slice(0, 120),
837
+ }));
838
+ }
709
839
  function createPromptedJsonToolBindableModel(model, boundTools = [], options = {}) {
710
840
  return new Proxy(model, {
711
841
  has(target, prop) {
@@ -720,8 +850,13 @@ function createPromptedJsonToolBindableModel(model, boundTools = [], options = {
720
850
  }
721
851
  if (prop === "invoke") {
722
852
  return async (input, config) => {
723
- const effectiveBoundTools = selectPlanningToolsForTurn(input, boundTools);
724
- const forcePlanningToolCall = shouldLimitToolsToPlanning(input);
853
+ const forcePlanningToolCall = shouldLimitToolsToPlanning(input, boundTools);
854
+ const forceNonPlanningEvidenceToolCall = !forcePlanningToolCall && shouldLimitToolsToNonPlanningEvidence(input, boundTools);
855
+ const effectiveBoundTools = forcePlanningToolCall
856
+ ? selectPlanningToolsForTurn(input, boundTools)
857
+ : forceNonPlanningEvidenceToolCall
858
+ ? selectNonPlanningToolsForTurn(boundTools)
859
+ : boundTools;
725
860
  if (options.settleCompletedToolResults === true
726
861
  && !forcePlanningToolCall
727
862
  && effectiveBoundTools.length > 0
@@ -731,14 +866,35 @@ function createPromptedJsonToolBindableModel(model, boundTools = [], options = {
731
866
  content: readLatestToolResultContent(input) ?? "",
732
867
  });
733
868
  }
734
- const rawResult = await target.invoke(effectiveBoundTools.length > 0
735
- ? withPromptedJsonToolPrompt(input, effectiveBoundTools, { ...options, forcePlanningToolCall })
736
- : input, config);
869
+ const promptedInput = effectiveBoundTools.length > 0
870
+ ? withPromptedJsonToolPrompt(input, effectiveBoundTools, {
871
+ ...options,
872
+ forceToolCall: forcePlanningToolCall
873
+ ? "planning"
874
+ : forceNonPlanningEvidenceToolCall
875
+ ? "nonPlanningEvidence"
876
+ : undefined,
877
+ })
878
+ : input;
879
+ debugPromptedJsonTurn({
880
+ forcePlanningToolCall,
881
+ forceNonPlanningEvidenceToolCall,
882
+ effectiveToolNames: effectiveBoundTools.map(readBoundToolName).filter(Boolean),
883
+ rawInput: input,
884
+ prompt: promptedInput,
885
+ });
886
+ const rawResult = await target.invoke(promptedInput, config);
737
887
  if (effectiveBoundTools.length === 0) {
738
888
  return rawResult;
739
889
  }
740
890
  const text = readModelText(rawResult);
741
891
  const parsedToolCall = normalizeParsedToolCall(extractToolCallPayload(text));
892
+ debugPromptedJsonResult({
893
+ forcePlanningToolCall,
894
+ forceNonPlanningEvidenceToolCall,
895
+ text,
896
+ parsedToolName: parsedToolCall?.name,
897
+ });
742
898
  if (!parsedToolCall) {
743
899
  if (forcePlanningToolCall) {
744
900
  const fallbackToolCall = buildFallbackPlanningToolCall(input, effectiveBoundTools, text);
@@ -746,7 +902,7 @@ function createPromptedJsonToolBindableModel(model, boundTools = [], options = {
746
902
  return fallbackToolCall;
747
903
  }
748
904
  }
749
- else {
905
+ else if (!forceNonPlanningEvidenceToolCall) {
750
906
  const fallbackCompletionToolCall = buildFallbackTodoCompletionToolCall(input, effectiveBoundTools);
751
907
  if (fallbackCompletionToolCall) {
752
908
  return fallbackCompletionToolCall;
@@ -754,12 +910,20 @@ function createPromptedJsonToolBindableModel(model, boundTools = [], options = {
754
910
  }
755
911
  return rawResult;
756
912
  }
913
+ if (!isAllowedPromptedJsonToolCall(parsedToolCall.name, effectiveBoundTools)) {
914
+ return new AIMessage({ content: "" });
915
+ }
757
916
  const effectiveParsedToolCall = forcePlanningToolCall
758
917
  ? normalizeInitialTodoPlanToolCall(parsedToolCall)
759
918
  : parsedToolCall;
760
- if (hasPriorToolResultForToolName(input, effectiveParsedToolCall.name) || hasAnyPriorToolResult(input)) {
919
+ if (parsedToolCallCompletesTodoPlan(effectiveParsedToolCall) && !hasPriorNonPlanningToolResult(input, effectiveBoundTools)) {
761
920
  return rawResult;
762
921
  }
922
+ recordPromptedJsonToolCall({
923
+ name: effectiveParsedToolCall.name,
924
+ args: effectiveParsedToolCall.args,
925
+ rawArgsInput: text,
926
+ });
763
927
  return new AIMessage({
764
928
  content: "",
765
929
  tool_calls: [{
@@ -810,6 +974,41 @@ function createPromptedJsonToolBindableModel(model, boundTools = [], options = {
810
974
  },
811
975
  });
812
976
  }
977
+ function createPromptedJsonPlanningToolBindableModel(model, boundTools = [], options = {}) {
978
+ return new Proxy(model, {
979
+ has(target, prop) {
980
+ if (prop === "bindTools" || prop === "invoke" || prop === "stream" || prop === "streamEvents" || prop === "withConfig") {
981
+ return true;
982
+ }
983
+ return prop in target;
984
+ },
985
+ get(target, prop, receiver) {
986
+ if (prop === "bindTools") {
987
+ return (tools) => createPromptedJsonPlanningToolBindableModel(target, tools, options);
988
+ }
989
+ if (prop === "invoke") {
990
+ return async (input, config) => {
991
+ if (shouldLimitToolsToPlanning(input, boundTools)) {
992
+ const prompted = createPromptedJsonToolBindableModel(target, boundTools, options);
993
+ return prompted.invoke(input, config);
994
+ }
995
+ const nativeModel = typeof target.bindTools === "function"
996
+ ? target.bindTools(boundTools)
997
+ : target;
998
+ if (typeof nativeModel.invoke === "function") {
999
+ return nativeModel.invoke(input, config);
1000
+ }
1001
+ return target.invoke(input, config);
1002
+ };
1003
+ }
1004
+ if (prop === "withConfig" && typeof target.withConfig === "function") {
1005
+ return (config) => createPromptedJsonPlanningToolBindableModel(target.withConfig(config), boundTools, options);
1006
+ }
1007
+ const member = Reflect.get(target, prop, receiver);
1008
+ return typeof member === "function" ? member.bind(target) : member;
1009
+ },
1010
+ });
1011
+ }
813
1012
  function inferNodeLlamaCppModelPath(model) {
814
1013
  const modelPath = typeof model.init?.modelPath === "string" ? model.init.modelPath.trim() : "";
815
1014
  if (modelPath) {
@@ -846,6 +1045,12 @@ export async function createResolvedModel(model, modelResolver) {
846
1045
  suppressThinking: init.think === false,
847
1046
  });
848
1047
  }
1048
+ if (toolCallingMode === "prompted-json-planning") {
1049
+ return createPromptedJsonPlanningToolBindableModel(resolved, [], {
1050
+ settleCompletedToolResults: true,
1051
+ suppressThinking: init.think === false,
1052
+ });
1053
+ }
849
1054
  return createProviderToolMessageCompatModel(resolved);
850
1055
  }
851
1056
  if (model.provider === "openai-compatible") {
@@ -860,6 +1065,12 @@ export async function createResolvedModel(model, modelResolver) {
860
1065
  suppressThinking: true,
861
1066
  });
862
1067
  }
1068
+ if (toolCallingMode === "prompted-json-planning") {
1069
+ return createPromptedJsonPlanningToolBindableModel(resolved, [], {
1070
+ settleCompletedToolResults: true,
1071
+ suppressThinking: true,
1072
+ });
1073
+ }
863
1074
  return createProviderToolMessageCompatModel(resolved);
864
1075
  }
865
1076
  if (model.provider === "openai") {
@@ -0,0 +1,9 @@
1
+ export type CapturedPromptedJsonToolCall = {
2
+ id?: string;
3
+ name: string;
4
+ args: Record<string, unknown>;
5
+ rawArgsInput?: unknown;
6
+ };
7
+ export declare function recordPromptedJsonToolCall(toolCall: CapturedPromptedJsonToolCall): void;
8
+ export declare function capturePromptedJsonToolCalls<T>(producer: () => Promise<T>): Promise<T>;
9
+ export declare function readCapturedPromptedJsonToolCalls(value: unknown): CapturedPromptedJsonToolCall[];
@@ -0,0 +1,40 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ const CAPTURED_TOOL_CALLS_KEY = "__harnessPromptedJsonToolCalls";
3
+ const storage = new AsyncLocalStorage();
4
+ export function recordPromptedJsonToolCall(toolCall) {
5
+ const active = storage.getStore();
6
+ if (!active) {
7
+ return;
8
+ }
9
+ active.push(toolCall);
10
+ }
11
+ export async function capturePromptedJsonToolCalls(producer) {
12
+ const captured = [];
13
+ const result = await storage.run(captured, producer);
14
+ if (typeof result === "object"
15
+ && result !== null
16
+ && captured.length > 0
17
+ && !Object.prototype.hasOwnProperty.call(result, CAPTURED_TOOL_CALLS_KEY)) {
18
+ Object.defineProperty(result, CAPTURED_TOOL_CALLS_KEY, {
19
+ configurable: true,
20
+ enumerable: false,
21
+ value: [...captured],
22
+ });
23
+ }
24
+ return result;
25
+ }
26
+ export function readCapturedPromptedJsonToolCalls(value) {
27
+ if (typeof value !== "object" || value === null) {
28
+ return [];
29
+ }
30
+ const captured = value[CAPTURED_TOOL_CALLS_KEY];
31
+ if (!Array.isArray(captured)) {
32
+ return [];
33
+ }
34
+ return captured.filter((item) => typeof item === "object"
35
+ && item !== null
36
+ && typeof item.name === "string"
37
+ && typeof item.args === "object"
38
+ && item.args !== null
39
+ && !Array.isArray(item.args));
40
+ }
@@ -27,24 +27,70 @@ function findTodosArray(value, depth = 0) {
27
27
  }
28
28
  return [];
29
29
  }
30
+ function stripWrappingQuotes(value) {
31
+ const trimmed = value.trim().replace(/,$/, "").trim();
32
+ if (trimmed.length >= 2) {
33
+ const first = trimmed[0];
34
+ const last = trimmed[trimmed.length - 1];
35
+ if ((first === "\"" && last === "\"") || (first === "'" && last === "'")) {
36
+ return trimmed.slice(1, -1).trim();
37
+ }
38
+ }
39
+ return trimmed;
40
+ }
41
+ function normalizeTodoContentValue(value) {
42
+ const trimmed = value.trim();
43
+ if (!trimmed) {
44
+ return "";
45
+ }
46
+ if (/^\*{1,2}\s*(?:call\s+)?(?:write|read)[ _-]?todos\s*\*{1,2}$/iu.test(trimmed)) {
47
+ return "";
48
+ }
49
+ const contentField = /^["']?(content|description|title|name|text)["']?\s*:\s*(.+)$/iu.exec(trimmed);
50
+ if (contentField) {
51
+ const normalized = stripWrappingQuotes(contentField[2]);
52
+ return normalized && normalized !== "null" && normalized !== "undefined" ? normalized : "";
53
+ }
54
+ if (/^["']?(status|id|ownerAgentId|startedAt|endedAt|result|metadata)["']?\s*:/u.test(trimmed)) {
55
+ return "";
56
+ }
57
+ return stripWrappingQuotes(trimmed);
58
+ }
59
+ function readTodoContent(todo) {
60
+ const candidates = [todo.content, todo.description, todo.title, todo.name, todo.text];
61
+ for (const candidate of candidates) {
62
+ if (typeof candidate !== "string") {
63
+ continue;
64
+ }
65
+ const normalized = normalizeTodoContentValue(candidate);
66
+ if (normalized) {
67
+ return normalized;
68
+ }
69
+ }
70
+ return "";
71
+ }
72
+ function normalizeTodoStatus(value) {
73
+ if (typeof value !== "string") {
74
+ return "pending";
75
+ }
76
+ const normalized = value.trim().toLowerCase();
77
+ if (normalized === "pending"
78
+ || normalized === "in_progress"
79
+ || normalized === "completed"
80
+ || normalized === "failed"
81
+ || normalized === "cancelled") {
82
+ return normalized;
83
+ }
84
+ return "pending";
85
+ }
30
86
  export function summarizeBuiltinWriteTodosArgs(args) {
31
87
  const todos = findTodosArray(args);
32
88
  const items = todos.flatMap((todo) => {
33
89
  if (!isRecord(todo)) {
34
90
  return [];
35
91
  }
36
- const content = typeof todo.content === "string" && todo.content.trim().length > 0
37
- ? todo.content.trim()
38
- : typeof todo.description === "string" && todo.description.trim().length > 0
39
- ? todo.description.trim()
40
- : typeof todo.title === "string" && todo.title.trim().length > 0
41
- ? todo.title.trim()
42
- : typeof todo.name === "string" && todo.name.trim().length > 0
43
- ? todo.name.trim()
44
- : typeof todo.text === "string" && todo.text.trim().length > 0
45
- ? todo.text.trim()
46
- : "";
47
- const status = typeof todo.status === "string" && todo.status.trim().length > 0 ? todo.status.trim() : "pending";
92
+ const content = readTodoContent(todo);
93
+ const status = normalizeTodoStatus(todo.status);
48
94
  const metadata = isRecord(todo.metadata) ? todo.metadata : undefined;
49
95
  return content ? [{
50
96
  ...((typeof todo.id === "string" && todo.id.trim().length > 0)
@@ -3,6 +3,7 @@ import { appendToolRecoveryInstruction, extractVisibleOutput, isToolCallRecovery
3
3
  import { readStreamDelta } from "../parsing/stream-event-parsing.js";
4
4
  import { computeRemainingTimeoutMs, isRetryableProviderError, resolveProviderRetryPolicy } from "./resilience.js";
5
5
  import { isDeepAgentBinding, isLangChainBinding, withUpdatedBindingExecutionParams, } from "../support/compiled-binding.js";
6
+ import { capturePromptedJsonToolCalls } from "./model/prompted-json-tool-call-capture.js";
6
7
  export class RuntimeOperationTimeoutError extends Error {
7
8
  operation;
8
9
  timeoutMs;
@@ -153,13 +154,13 @@ export function applyToolRecoveryInstruction(binding, instruction) {
153
154
  }
154
155
  export async function callRuntimeWithToolParseRecovery(input) {
155
156
  try {
156
- return await input.callRuntime(input.binding, input.request);
157
+ return await capturePromptedJsonToolCalls(() => input.callRuntime(input.binding, input.request));
157
158
  }
158
159
  catch (error) {
159
160
  const recoveryInstruction = resolveToolCallRecoveryInstruction(error);
160
161
  if (input.resumePayload !== undefined || !recoveryInstruction || !isToolCallRecoveryFailure(error)) {
161
162
  throw error;
162
163
  }
163
- return input.callRuntime(applyToolRecoveryInstruction(input.binding, recoveryInstruction), appendToolRecoveryInstruction(input.request, recoveryInstruction));
164
+ return capturePromptedJsonToolCalls(() => input.callRuntime(applyToolRecoveryInstruction(input.binding, recoveryInstruction), appendToolRecoveryInstruction(input.request, recoveryInstruction)));
164
165
  }
165
166
  }