@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.
- package/dist/cli/chat-interactive.js +1 -1
- package/dist/cli/chat-stream.js +9 -1
- package/dist/package-version.d.ts +2 -2
- package/dist/package-version.js +2 -2
- package/dist/runtime/adapter/compat/openai-compatible.js +12 -0
- package/dist/runtime/adapter/flow/invocation-flow.d.ts +2 -0
- package/dist/runtime/adapter/flow/invocation-flow.js +13 -5
- package/dist/runtime/adapter/flow/invoke-runtime.d.ts +1 -0
- package/dist/runtime/adapter/flow/invoke-runtime.js +1 -0
- package/dist/runtime/adapter/flow/stream-runtime.d.ts +4 -0
- package/dist/runtime/adapter/flow/stream-runtime.js +177 -14
- package/dist/runtime/adapter/invocation-result.js +17 -6
- package/dist/runtime/adapter/local-tool-invocation.d.ts +2 -1
- package/dist/runtime/adapter/local-tool-invocation.js +268 -21
- package/dist/runtime/adapter/model/model-providers.js +269 -58
- package/dist/runtime/adapter/model/prompted-json-tool-call-capture.d.ts +9 -0
- package/dist/runtime/adapter/model/prompted-json-tool-call-capture.js +40 -0
- package/dist/runtime/adapter/runtime-adapter-support.js +58 -12
- package/dist/runtime/adapter/runtime-shell.js +3 -2
- package/dist/runtime/adapter/stream-event-projection.js +22 -5
- package/dist/runtime/adapter/tool/tool-arguments.js +157 -67
- package/dist/runtime/adapter/tool/tool-replay.js +0 -4
- package/dist/runtime/agent-runtime-adapter.d.ts +3 -0
- package/dist/runtime/agent-runtime-adapter.js +217 -73
- package/dist/runtime/harness/run/stream-run.js +31 -3
- package/dist/runtime/parsing/output-tool-args.js +108 -0
- package/dist/workspace/resource-compilers.js +17 -4
- 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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
724
|
-
const
|
|
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
|
|
735
|
-
? withPromptedJsonToolPrompt(input, effectiveBoundTools, {
|
|
736
|
-
|
|
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 (
|
|
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 =
|
|
37
|
-
|
|
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
|
}
|