@botbotgo/agent-harness 0.0.416 → 0.0.419

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.
@@ -8,6 +8,7 @@ import { initChatModel } from "langchain";
8
8
  import { salvageToolArgs, tryParseJson } from "../../parsing/output-parsing.js";
9
9
  import { normalizeModelFacingToolSchema } from "../tool/resolved-tool.js";
10
10
  import { normalizeOpenAICompatibleInit } from "../compat/openai-compatible.js";
11
+ import { recordPromptedJsonToolCall } from "./prompted-json-tool-call-capture.js";
11
12
  const NODE_LLAMA_CPP_TOOL_CALL_INSTRUCTION = [
12
13
  "Available tools are listed below.",
13
14
  "If you need a tool, respond with only one JSON object.",
@@ -17,6 +18,13 @@ const NODE_LLAMA_CPP_TOOL_CALL_INSTRUCTION = [
17
18
  "If the conversation already contains TOOL_RESULT for the requested work, answer from that result instead of repeating the same tool call.",
18
19
  "If no tool is needed, answer normally.",
19
20
  ].join("\n");
21
+ const FORCED_NODE_LLAMA_CPP_TOOL_CALL_INSTRUCTION = [
22
+ "Available tools are listed below.",
23
+ "You must call exactly one available tool now.",
24
+ 'Return only one JSON object with this exact shape: {"name":"tool_name","arguments":{"key":"value"}}',
25
+ "Do not add markdown, prose, explanations, analysis, or code fences.",
26
+ "Do not answer normally on this turn.",
27
+ ].join("\n");
20
28
  const PROMPTED_JSON_FINAL_TOOL_CALL_REMINDER = [
21
29
  "Final tool-call rule:",
22
30
  "If the correct next step is a tool call, return exactly one JSON object and no prose.",
@@ -192,17 +200,51 @@ function canonicalToolName(value) {
192
200
  .replace(/[^a-z0-9]+/g, "_")
193
201
  .replace(/^_+|_+$/g, "");
194
202
  }
203
+ function readToolCallId(value) {
204
+ if (typeof value !== "object" || value === null) {
205
+ return undefined;
206
+ }
207
+ const typed = value;
208
+ return typeof typed.id === "string"
209
+ ? typed.id
210
+ : typeof typed.tool_call_id === "string"
211
+ ? typed.tool_call_id
212
+ : undefined;
213
+ }
214
+ function buildToolResultNameLookup(messages) {
215
+ const namesByToolCallId = new Map();
216
+ for (const message of messages) {
217
+ if (mapMessageRole(message) !== "ASSISTANT") {
218
+ continue;
219
+ }
220
+ for (const toolCall of readToolCalls(message)) {
221
+ if (typeof toolCall !== "object" || toolCall === null) {
222
+ continue;
223
+ }
224
+ const id = readToolCallId(toolCall);
225
+ const name = typeof toolCall.name === "string"
226
+ ? toolCall.name
227
+ : "";
228
+ if (id && name) {
229
+ namesByToolCallId.set(id, name);
230
+ }
231
+ }
232
+ }
233
+ return namesByToolCallId;
234
+ }
195
235
  function hasPriorToolResultForToolName(input, toolName) {
196
236
  if (!toolName) {
197
237
  return false;
198
238
  }
199
239
  const expectedName = canonicalToolName(toolName);
200
240
  if (Array.isArray(input)) {
241
+ const namesByToolCallId = buildToolResultNameLookup(input);
201
242
  return input.some((message) => {
202
243
  if (mapMessageRole(message) !== "TOOL") {
203
244
  return false;
204
245
  }
205
- const observedName = readToolMessageMetadata(message).name;
246
+ const metadata = readToolMessageMetadata(message);
247
+ const observedName = metadata.name ?? (metadata.toolCallId ? namesByToolCallId.get(metadata.toolCallId) : undefined);
206
248
  return typeof observedName === "string" && canonicalToolName(observedName) === expectedName;
207
249
  });
208
250
  }
@@ -258,11 +300,31 @@ function readBoundToolName(tool) {
258
300
  ? tool.name.trim()
259
301
  : "";
260
302
  }
303
+ function readBoundToolDescription(tool) {
304
+ return typeof tool === "object" && tool !== null && typeof tool.description === "string"
305
+ ? tool.description.trim()
306
+ : "";
307
+ }
308
+ function summarizeSchemaKeys(schema) {
309
+ const normalized = normalizeModelFacingToolSchema(schema);
310
+ const properties = typeof normalized.properties === "object" && normalized.properties !== null
311
+ ? Object.keys(normalized.properties)
312
+ : [];
313
+ const required = Array.isArray(normalized.required)
314
+ ? normalized.required.filter((item) => typeof item === "string")
315
+ : [];
316
+ if (properties.length === 0) {
317
+ return "{}";
318
+ }
319
+ return `{${properties.map((key) => required.includes(key) ? `${key}:required` : `${key}:optional`).join(", ")}}`;
320
+ }
261
321
  function isTodoPlanningToolName(name) {
262
322
  return name === "write_todos"
263
323
  || name === "read_todos"
264
324
  || name === "tool_call_write_todos"
265
- || name === "tool_call_read_todos";
325
+ || name === "tool_call_read_todos"
326
+ || name === "call_write_todos"
327
+ || name === "call_read_todos";
266
328
  }
267
329
  function hasPriorNonPlanningToolResult(input, tools) {
268
330
  const toolNames = tools
@@ -270,19 +332,46 @@ function hasPriorNonPlanningToolResult(input, tools) {
270
332
  .filter((name) => name && !isTodoPlanningToolName(name));
271
333
  return toolNames.some((name) => hasPriorToolResultForToolName(input, name));
272
334
  }
273
- function shouldLimitToolsToPlanning(input) {
335
+ function hasPriorPlanningToolResult(input) {
336
+ return hasPriorToolResultForToolName(input, "write_todos")
337
+ || hasPriorToolResultForToolName(input, "tool_call_write_todos")
338
+ || hasPriorToolResultForToolName(input, "call_write_todos")
339
+ || hasPriorToolResultForToolName(input, "read_todos")
340
+ || hasPriorToolResultForToolName(input, "tool_call_read_todos")
341
+ || hasPriorToolResultForToolName(input, "call_read_todos");
342
+ }
343
+ function shouldLimitToolsToPlanning(input, boundTools) {
274
344
  const text = stringifyNodeLlamaCppInput(input);
275
345
  return text.includes("required visible planning contract")
276
346
  && !hasPriorToolResultForToolName(input, "write_todos")
277
- && !hasPriorToolResultForToolName(input, "tool_call_write_todos");
347
+ && !hasPriorToolResultForToolName(input, "tool_call_write_todos")
348
+ && !hasPriorToolResultForToolName(input, "call_write_todos")
349
+ && !hasPriorNonPlanningToolResult(input, boundTools);
278
350
  }
279
351
  function selectPlanningToolsForTurn(input, boundTools) {
280
- if (!shouldLimitToolsToPlanning(input)) {
352
+ if (!shouldLimitToolsToPlanning(input, boundTools)) {
281
353
  return boundTools;
282
354
  }
283
355
  const planningTools = boundTools.filter((tool) => isTodoPlanningToolName(readBoundToolName(tool)));
284
356
  return planningTools.length > 0 ? planningTools : boundTools;
285
357
  }
358
+ function shouldLimitToolsToNonPlanningEvidence(input, boundTools) {
359
+ const text = stringifyNodeLlamaCppInput(input);
360
+ 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);
361
+ return (hasPriorPlanningToolResult(input) || hasNonPlanningEvidenceInstruction)
362
+ && !hasPriorNonPlanningToolResult(input, boundTools);
363
+ }
364
+ function selectNonPlanningToolsForTurn(boundTools) {
365
+ const nonPlanningTools = boundTools.filter((tool) => {
366
+ const name = readBoundToolName(tool);
367
+ return name.length > 0 && !isTodoPlanningToolName(name);
368
+ });
369
+ return nonPlanningTools.length > 0 ? nonPlanningTools : boundTools;
370
+ }
371
+ function isAllowedPromptedJsonToolCall(toolName, effectiveBoundTools) {
372
+ const allowedToolNames = new Set(effectiveBoundTools.map((tool) => readBoundToolName(tool)).filter(Boolean));
373
+ return allowedToolNames.has(toolName);
374
+ }
286
375
  function normalizeReadFileToolContent(name, content) {
287
376
  if (name !== "read_file") {
288
377
  return content;
@@ -547,37 +636,6 @@ function normalizeParsedToolCall(payload) {
547
636
  : salvageToolArgs(argsCandidate) ?? {};
548
637
  return { name, args };
549
638
  }
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
639
  function buildFallbackTodoContents() {
582
640
  return [
583
641
  "Identify the concrete evidence tool required for this request",
@@ -586,20 +644,14 @@ function buildFallbackTodoContents() {
586
644
  "Return the final answer grounded in tool output",
587
645
  ];
588
646
  }
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
647
  function buildFallbackPlanningToolCall(input, planningTools, rawText) {
593
- const toolName = planningTools.map((tool) => readBoundToolName(tool)).find((name) => name === "write_todos" || name === "tool_call_write_todos");
648
+ void input;
649
+ void rawText;
650
+ const toolName = planningTools.map((tool) => readBoundToolName(tool)).find((name) => name === "write_todos" || name === "tool_call_write_todos" || name === "call_write_todos");
594
651
  if (!toolName) {
595
652
  return null;
596
653
  }
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) => ({
654
+ const todos = buildFallbackTodoContents().map((content, index) => ({
603
655
  content,
604
656
  status: index === 0 ? "in_progress" : "pending",
605
657
  }));
@@ -618,7 +670,7 @@ function buildFallbackTodoCompletionToolCall(input, tools) {
618
670
  if (/TODO completed:|\[x\]/i.test(prompt)) {
619
671
  return null;
620
672
  }
621
- const planningToolName = tools.map((tool) => readBoundToolName(tool)).find((name) => name === "write_todos" || name === "tool_call_write_todos");
673
+ const planningToolName = tools.map((tool) => readBoundToolName(tool)).find((name) => name === "write_todos" || name === "tool_call_write_todos" || name === "call_write_todos");
622
674
  if (!planningToolName) {
623
675
  return null;
624
676
  }
@@ -683,29 +735,99 @@ function formatBoundToolInstruction(tool) {
683
735
  `Arguments JSON schema: ${JSON.stringify(schema)}`,
684
736
  ].filter(Boolean).join("\n");
685
737
  }
738
+ function formatCompactBoundToolInstruction(tool) {
739
+ const name = readBoundToolName(tool);
740
+ if (!name) {
741
+ return null;
742
+ }
743
+ const description = readBoundToolDescription(tool).split(/\n/u)[0]?.trim() ?? "";
744
+ return [
745
+ `Tool: ${name}`,
746
+ description ? `Description: ${description}` : "",
747
+ `Arguments keys: ${summarizeSchemaKeys(tool)}`,
748
+ ].filter(Boolean).join("\n");
749
+ }
686
750
  function withPromptedJsonToolPrompt(input, tools, options = {}) {
687
751
  const toolInstructions = tools.map((tool) => formatBoundToolInstruction(tool)).filter((value) => Boolean(value));
752
+ const compactToolInstructions = tools.map((tool) => formatCompactBoundToolInstruction(tool)).filter((value) => Boolean(value));
688
753
  if (toolInstructions.length === 0) {
689
754
  return stringifyNodeLlamaCppInput(input);
690
755
  }
691
- const forcedPlanningInstruction = options.forcePlanningToolCall
756
+ const forcedToolInstruction = options.forceToolCall === "planning"
692
757
  ? [
693
758
  "Required planning tool call:",
694
759
  "You must call write_todos now before any domain tool or final answer.",
695
760
  "Return exactly one JSON object for write_todos with concrete todo items and statuses.",
696
761
  "Do not write prose, markdown, analysis, or a plain-text plan.",
697
762
  ].join("\n")
698
- : "";
699
- const systemContent = `${NODE_LLAMA_CPP_TOOL_CALL_INSTRUCTION}\n\n${toolInstructions.join("\n\n")}`;
763
+ : options.forceToolCall === "nonPlanningEvidence"
764
+ ? [
765
+ "Required evidence tool call:",
766
+ "A todo board already exists. Your next action must be exactly one non-planning tool call chosen from the available tool descriptions and schemas.",
767
+ "Do not call write_todos or read_todos now.",
768
+ "Do not write prose, markdown, analysis, or a plain-text plan.",
769
+ ].join("\n")
770
+ : "";
771
+ const baseToolInstruction = options.forceToolCall
772
+ ? FORCED_NODE_LLAMA_CPP_TOOL_CALL_INSTRUCTION
773
+ : NODE_LLAMA_CPP_TOOL_CALL_INSTRUCTION;
774
+ const systemContent = [
775
+ baseToolInstruction,
776
+ forcedToolInstruction,
777
+ (options.forceToolCall ? compactToolInstructions : toolInstructions).join("\n\n"),
778
+ forcedToolInstruction,
779
+ ].filter(Boolean).join("\n\n");
700
780
  const prompt = stringifyNodeLlamaCppInput(input);
701
781
  return [
702
782
  options.suppressThinking ? NO_THINK_CONTROL_TOKEN : "",
703
783
  systemContent,
704
- forcedPlanningInstruction,
784
+ forcedToolInstruction,
705
785
  prompt,
706
786
  PROMPTED_JSON_FINAL_TOOL_CALL_REMINDER,
707
787
  ].filter(Boolean).join("\n\n");
708
788
  }
789
+ function debugPromptedJsonTurn(input) {
790
+ if (process.env.AGENT_HARNESS_PROMPTED_JSON_DEBUG !== "1") {
791
+ return;
792
+ }
793
+ const promptText = typeof input.prompt === "string" ? input.prompt : stringifyNodeLlamaCppInput(input.prompt);
794
+ console.error(JSON.stringify({
795
+ type: "prompted-json-turn",
796
+ forcePlanningToolCall: input.forcePlanningToolCall,
797
+ forceNonPlanningEvidenceToolCall: input.forceNonPlanningEvidenceToolCall,
798
+ effectiveToolNames: input.effectiveToolNames,
799
+ inputSummary: summarizePromptedJsonInput(input.rawInput),
800
+ promptHead: promptText.slice(0, 2000),
801
+ }));
802
+ }
803
+ function debugPromptedJsonResult(input) {
804
+ if (process.env.AGENT_HARNESS_PROMPTED_JSON_DEBUG !== "1") {
805
+ return;
806
+ }
807
+ console.error(JSON.stringify({
808
+ type: "prompted-json-result",
809
+ forcePlanningToolCall: input.forcePlanningToolCall,
810
+ forceNonPlanningEvidenceToolCall: input.forceNonPlanningEvidenceToolCall,
811
+ parsedToolName: input.parsedToolName ?? null,
812
+ textHead: input.text.slice(0, 2000),
813
+ }));
814
+ }
815
+ function summarizePromptedJsonInput(input) {
816
+ const messages = typeof input === "object" && input !== null && Array.isArray(input.messages)
817
+ ? input.messages
818
+ : Array.isArray(input)
819
+ ? input
820
+ : [];
821
+ return messages.slice(-8).map((message) => ({
822
+ role: mapMessageRole(message),
823
+ type: readMessageType(message),
824
+ name: readToolMessageMetadata(message).name,
825
+ toolCallNames: readToolCalls(message).map((toolCall) => typeof toolCall === "object" && toolCall !== null && typeof toolCall.name === "string"
826
+ ? toolCall.name
827
+ : ""),
828
+ contentHead: readPromptContent(message).slice(0, 120),
829
+ }));
830
+ }
709
831
  function createPromptedJsonToolBindableModel(model, boundTools = [], options = {}) {
710
832
  return new Proxy(model, {
711
833
  has(target, prop) {
@@ -720,8 +842,13 @@ function createPromptedJsonToolBindableModel(model, boundTools = [], options = {
720
842
  }
721
843
  if (prop === "invoke") {
722
844
  return async (input, config) => {
723
- const effectiveBoundTools = selectPlanningToolsForTurn(input, boundTools);
724
- const forcePlanningToolCall = shouldLimitToolsToPlanning(input);
845
+ const forcePlanningToolCall = shouldLimitToolsToPlanning(input, boundTools);
846
+ const forceNonPlanningEvidenceToolCall = !forcePlanningToolCall && shouldLimitToolsToNonPlanningEvidence(input, boundTools);
847
+ const effectiveBoundTools = forcePlanningToolCall
848
+ ? selectPlanningToolsForTurn(input, boundTools)
849
+ : forceNonPlanningEvidenceToolCall
850
+ ? selectNonPlanningToolsForTurn(boundTools)
851
+ : boundTools;
725
852
  if (options.settleCompletedToolResults === true
726
853
  && !forcePlanningToolCall
727
854
  && effectiveBoundTools.length > 0
@@ -731,14 +858,35 @@ function createPromptedJsonToolBindableModel(model, boundTools = [], options = {
731
858
  content: readLatestToolResultContent(input) ?? "",
732
859
  });
733
860
  }
734
- const rawResult = await target.invoke(effectiveBoundTools.length > 0
735
- ? withPromptedJsonToolPrompt(input, effectiveBoundTools, { ...options, forcePlanningToolCall })
736
- : input, config);
861
+ const promptedInput = effectiveBoundTools.length > 0
862
+ ? withPromptedJsonToolPrompt(input, effectiveBoundTools, {
863
+ ...options,
864
+ forceToolCall: forcePlanningToolCall
865
+ ? "planning"
866
+ : forceNonPlanningEvidenceToolCall
867
+ ? "nonPlanningEvidence"
868
+ : undefined,
869
+ })
870
+ : input;
871
+ debugPromptedJsonTurn({
872
+ forcePlanningToolCall,
873
+ forceNonPlanningEvidenceToolCall,
874
+ effectiveToolNames: effectiveBoundTools.map(readBoundToolName).filter(Boolean),
875
+ rawInput: input,
876
+ prompt: promptedInput,
877
+ });
878
+ const rawResult = await target.invoke(promptedInput, config);
737
879
  if (effectiveBoundTools.length === 0) {
738
880
  return rawResult;
739
881
  }
740
882
  const text = readModelText(rawResult);
741
883
  const parsedToolCall = normalizeParsedToolCall(extractToolCallPayload(text));
884
+ debugPromptedJsonResult({
885
+ forcePlanningToolCall,
886
+ forceNonPlanningEvidenceToolCall,
887
+ text,
888
+ parsedToolName: parsedToolCall?.name,
889
+ });
742
890
  if (!parsedToolCall) {
743
891
  if (forcePlanningToolCall) {
744
892
  const fallbackToolCall = buildFallbackPlanningToolCall(input, effectiveBoundTools, text);
@@ -746,7 +894,7 @@ function createPromptedJsonToolBindableModel(model, boundTools = [], options = {
746
894
  return fallbackToolCall;
747
895
  }
748
896
  }
749
- else {
897
+ else if (!forceNonPlanningEvidenceToolCall) {
750
898
  const fallbackCompletionToolCall = buildFallbackTodoCompletionToolCall(input, effectiveBoundTools);
751
899
  if (fallbackCompletionToolCall) {
752
900
  return fallbackCompletionToolCall;
@@ -754,12 +902,20 @@ function createPromptedJsonToolBindableModel(model, boundTools = [], options = {
754
902
  }
755
903
  return rawResult;
756
904
  }
905
+ if (!isAllowedPromptedJsonToolCall(parsedToolCall.name, effectiveBoundTools)) {
906
+ return new AIMessage({ content: "" });
907
+ }
757
908
  const effectiveParsedToolCall = forcePlanningToolCall
758
909
  ? normalizeInitialTodoPlanToolCall(parsedToolCall)
759
910
  : parsedToolCall;
760
- if (hasPriorToolResultForToolName(input, effectiveParsedToolCall.name) || hasAnyPriorToolResult(input)) {
911
+ if (parsedToolCallCompletesTodoPlan(effectiveParsedToolCall) && !hasPriorNonPlanningToolResult(input, effectiveBoundTools)) {
761
912
  return rawResult;
762
913
  }
914
+ recordPromptedJsonToolCall({
915
+ name: effectiveParsedToolCall.name,
916
+ args: effectiveParsedToolCall.args,
917
+ rawArgsInput: text,
918
+ });
763
919
  return new AIMessage({
764
920
  content: "",
765
921
  tool_calls: [{
@@ -810,6 +966,41 @@ function createPromptedJsonToolBindableModel(model, boundTools = [], options = {
810
966
  },
811
967
  });
812
968
  }
969
+ function createPromptedJsonPlanningToolBindableModel(model, boundTools = [], options = {}) {
970
+ return new Proxy(model, {
971
+ has(target, prop) {
972
+ if (prop === "bindTools" || prop === "invoke" || prop === "stream" || prop === "streamEvents" || prop === "withConfig") {
973
+ return true;
974
+ }
975
+ return prop in target;
976
+ },
977
+ get(target, prop, receiver) {
978
+ if (prop === "bindTools") {
979
+ return (tools) => createPromptedJsonPlanningToolBindableModel(target, tools, options);
980
+ }
981
+ if (prop === "invoke") {
982
+ return async (input, config) => {
983
+ if (shouldLimitToolsToPlanning(input, boundTools)) {
984
+ const prompted = createPromptedJsonToolBindableModel(target, boundTools, options);
985
+ return prompted.invoke(input, config);
986
+ }
987
+ const nativeModel = typeof target.bindTools === "function"
988
+ ? target.bindTools(boundTools)
989
+ : target;
990
+ if (typeof nativeModel.invoke === "function") {
991
+ return nativeModel.invoke(input, config);
992
+ }
993
+ return target.invoke(input, config);
994
+ };
995
+ }
996
+ if (prop === "withConfig" && typeof target.withConfig === "function") {
997
+ return (config) => createPromptedJsonPlanningToolBindableModel(target.withConfig(config), boundTools, options);
998
+ }
999
+ const member = Reflect.get(target, prop, receiver);
1000
+ return typeof member === "function" ? member.bind(target) : member;
1001
+ },
1002
+ });
1003
+ }
813
1004
  function inferNodeLlamaCppModelPath(model) {
814
1005
  const modelPath = typeof model.init?.modelPath === "string" ? model.init.modelPath.trim() : "";
815
1006
  if (modelPath) {
@@ -846,6 +1037,12 @@ export async function createResolvedModel(model, modelResolver) {
846
1037
  suppressThinking: init.think === false,
847
1038
  });
848
1039
  }
1040
+ if (toolCallingMode === "prompted-json-planning") {
1041
+ return createPromptedJsonPlanningToolBindableModel(resolved, [], {
1042
+ settleCompletedToolResults: true,
1043
+ suppressThinking: init.think === false,
1044
+ });
1045
+ }
849
1046
  return createProviderToolMessageCompatModel(resolved);
850
1047
  }
851
1048
  if (model.provider === "openai-compatible") {
@@ -860,6 +1057,12 @@ export async function createResolvedModel(model, modelResolver) {
860
1057
  suppressThinking: true,
861
1058
  });
862
1059
  }
1060
+ if (toolCallingMode === "prompted-json-planning") {
1061
+ return createPromptedJsonPlanningToolBindableModel(resolved, [], {
1062
+ settleCompletedToolResults: true,
1063
+ suppressThinking: true,
1064
+ });
1065
+ }
863
1066
  return createProviderToolMessageCompatModel(resolved);
864
1067
  }
865
1068
  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
  }