@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.
- package/dist/package-version.d.ts +1 -1
- package/dist/package-version.js +1 -1
- 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 +241 -21
- package/dist/runtime/adapter/model/model-providers.js +261 -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 +20 -1
- package/dist/workspace/resource-compilers.js +17 -4
- package/package.json +1 -1
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
724
|
-
const
|
|
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
|
|
735
|
-
? withPromptedJsonToolPrompt(input, effectiveBoundTools, {
|
|
736
|
-
|
|
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 (
|
|
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 =
|
|
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
|
}
|