@botbotgo/agent-harness 0.0.400 → 0.0.401

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.
@@ -211,6 +211,48 @@ function hasPriorToolResultForToolName(input, toolName) {
211
211
  }
212
212
  return false;
213
213
  }
214
+ function hasAnyPriorToolResult(input) {
215
+ if (Array.isArray(input)) {
216
+ return input.some((message) => mapMessageRole(message) === "TOOL");
217
+ }
218
+ if (typeof input === "object" && input !== null && Array.isArray(input.messages)) {
219
+ return hasAnyPriorToolResult(input.messages);
220
+ }
221
+ return false;
222
+ }
223
+ function hasPriorNonPlanningToolCall(input) {
224
+ if (Array.isArray(input)) {
225
+ return input.some((message) => readToolCalls(message).some((toolCall) => {
226
+ if (typeof toolCall !== "object" || toolCall === null) {
227
+ return false;
228
+ }
229
+ const name = typeof toolCall.name === "string"
230
+ ? toolCall.name
231
+ : "";
232
+ return name.length > 0 && !isTodoPlanningToolName(name);
233
+ }));
234
+ }
235
+ if (typeof input === "object" && input !== null && Array.isArray(input.messages)) {
236
+ return hasPriorNonPlanningToolCall(input.messages);
237
+ }
238
+ return false;
239
+ }
240
+ function readLatestToolResultContent(input) {
241
+ if (Array.isArray(input)) {
242
+ for (let index = input.length - 1; index >= 0; index -= 1) {
243
+ const message = input[index];
244
+ if (mapMessageRole(message) === "TOOL") {
245
+ const content = readPromptContent(message);
246
+ return content || null;
247
+ }
248
+ }
249
+ return null;
250
+ }
251
+ if (typeof input === "object" && input !== null && Array.isArray(input.messages)) {
252
+ return readLatestToolResultContent(input.messages);
253
+ }
254
+ return null;
255
+ }
214
256
  function readBoundToolName(tool) {
215
257
  return typeof tool === "object" && tool !== null && typeof tool.name === "string"
216
258
  ? tool.name.trim()
@@ -357,47 +399,6 @@ function stringifyNodeLlamaCppInput(input) {
357
399
  }
358
400
  return readPromptContent(input);
359
401
  }
360
- function readLatestUserPromptContent(input) {
361
- const messages = typeof input === "object"
362
- && input !== null
363
- && Array.isArray(input.messages)
364
- ? input.messages
365
- : Array.isArray(input)
366
- ? input
367
- : null;
368
- if (!messages) {
369
- return readPromptContent(input);
370
- }
371
- for (let index = messages.length - 1; index >= 0; index -= 1) {
372
- if (mapMessageRole(messages[index]) !== "USER") {
373
- continue;
374
- }
375
- const content = readPromptContent(messages[index]);
376
- if (content) {
377
- return content;
378
- }
379
- }
380
- return stringifyNodeLlamaCppInput(input);
381
- }
382
- function inferStockRequest(input) {
383
- const prompt = readLatestUserPromptContent(input);
384
- const directSymbol = prompt.match(/\b[A-Z]{1,5}(?:\.[A-Z])?\b/u)?.[0];
385
- const lower = prompt.toLowerCase();
386
- const enterpriseHcmName = ["work", "day"].join("");
387
- const symbol = directSymbol
388
- ?? (/\bapple\b/u.test(lower) || /苹果/u.test(prompt) ? "AAPL" : undefined)
389
- ?? (new RegExp(`\\b${enterpriseHcmName}\\b`, "u").test(lower) ? "WDAY" : undefined);
390
- const company = symbol === "AAPL"
391
- ? "Apple Inc."
392
- : symbol === "WDAY"
393
- ? `${enterpriseHcmName[0].toUpperCase()}${enterpriseHcmName.slice(1)} Inc.`
394
- : undefined;
395
- return {
396
- ...(symbol ? { symbol } : {}),
397
- ...(company ? { company } : {}),
398
- query: symbol ? `${company ?? symbol} ${symbol} stock briefing` : prompt || "public company stock briefing",
399
- };
400
- }
401
402
  function extractToolCallPayload(text) {
402
403
  const trimmed = text.trim();
403
404
  if (!trimmed) {
@@ -580,34 +581,19 @@ function isLowSignalPlanningLine(value) {
580
581
  function normalizeDomainToolName(name) {
581
582
  return name.startsWith("tool_call_") ? name.slice("tool_call_".length) : name;
582
583
  }
583
- function selectFallbackEvidenceToolName(input, tools) {
584
- const prompt = stringifyNodeLlamaCppInput(input).toLowerCase();
584
+ function selectFallbackEvidenceToolName(tools, rawText = "") {
585
+ const normalizedRawText = rawText.toLowerCase();
585
586
  const available = tools
586
587
  .map((tool) => normalizeDomainToolName(readBoundToolName(tool)))
587
588
  .filter((name) => name && !isTodoPlanningToolName(name) && !isTodoPlanningToolName(`tool_call_${name}`));
588
- const hasTool = (name) => available.includes(name);
589
- const rules = [
590
- [/\b(k8s|kubernetes|kubectl|pod|pods|node|nodes|cluster)\b|集群|节点|调度/u, ["k8s_cluster_investigate", "k8s_cluster_triage", "kubectl_command"]],
591
- [/\b(disk|cache|storage|workspace)\b|磁盘|缓存|占用/u, ["disk_space_investigate"]],
592
- [/\b(agent config|agent configuration|repository structure|repo structure|code evidence|specialist)\b|agent\s*配置|配置结构|新增\s*specialist|代码层证据|改哪些文件/u, ["git_command", "codex_repo_analysis"]],
593
- [/\b(stock|ticker|finance|market brief|aapl|wday)\b|股票|公开股票简报|金融|市场简报/u, ["finance_stock_report", "finance_quote_snapshot"]],
594
- [/\b(test|qa|coverage|regression|playwright)\b|测试|覆盖率|回归|验证/u, ["git_command", "codex_repo_analysis", "playwright_capture"]],
595
- [/\b(release|readiness|branch|tag|github actions|ci)\b|发版|分支|流水线|可发版/u, ["git_command", "gh_actions_command"]],
596
- [/\b(youtube|transcript|brief|briefing|summary)\b|摘要|简报|讲稿/u, ["youtube_video_summary", "llamaindex_source_analysis", "finance_stock_report"]],
597
- ];
598
- for (const [pattern, toolNames] of rules) {
599
- if (!pattern.test(prompt)) {
600
- continue;
601
- }
602
- const matched = toolNames.find((name) => hasTool(name));
603
- if (matched) {
604
- return matched;
605
- }
589
+ if (available.length === 0) {
590
+ return null;
606
591
  }
607
- return available[0] ?? null;
592
+ const mentioned = available.find((name) => normalizedRawText.includes(name.toLowerCase()));
593
+ return mentioned ?? available[0] ?? null;
608
594
  }
609
- function buildToolAwareFallbackTodoContents(input, tools) {
610
- const evidenceToolName = selectFallbackEvidenceToolName(input, tools);
595
+ function buildToolAwareFallbackTodoContents(tools, rawText = "") {
596
+ const evidenceToolName = selectFallbackEvidenceToolName(tools, rawText);
611
597
  if (!evidenceToolName) {
612
598
  return [
613
599
  "Identify the concrete evidence tool required for this request",
@@ -635,7 +621,7 @@ function buildFallbackPlanningToolCall(input, planningTools, allTools, rawText)
635
621
  const hasUsefulModelPlan = modelPlannedItems.length >= 2 && !modelPlannedItems.every(isGenericFallbackTodoContent);
636
622
  const fallbackItems = hasUsefulModelPlan
637
623
  ? modelPlannedItems
638
- : buildToolAwareFallbackTodoContents(input, allTools);
624
+ : buildToolAwareFallbackTodoContents(allTools, rawText);
639
625
  const todos = fallbackItems.map((content, index) => ({
640
626
  content,
641
627
  status: index === 0 ? "in_progress" : "pending",
@@ -650,60 +636,45 @@ function buildFallbackPlanningToolCall(input, planningTools, allTools, rawText)
650
636
  }],
651
637
  });
652
638
  }
653
- function buildFallbackEvidenceToolArgs(toolName, input) {
654
- const prompt = stringifyNodeLlamaCppInput(input);
655
- const lower = prompt.toLowerCase();
656
- if (toolName === "git_command") {
657
- if (/\b(agent config|agent configuration|specialist)\b|agent\s*配置|配置结构|新增\s*specialist|代码层证据|改哪些文件/u.test(lower)) {
658
- return {
659
- args: ["ls-files", "config/agents", "config/agent-context.md", "config/runtime", "config/models.yaml"],
660
- cwd: ".",
661
- timeoutMs: 10000,
662
- };
663
- }
664
- return { args: ["status", "--short"], cwd: ".", timeoutMs: 10000 };
665
- }
666
- if (toolName === "disk_space_investigate") {
667
- return { targetPath: ".", cwd: ".", timeoutMs: 10000 };
668
- }
669
- if (toolName === "finance_stock_report") {
670
- return {
671
- ...inferStockRequest(input),
672
- market: "us",
673
- count: 5,
674
- };
675
- }
676
- if (toolName === "codex_repo_analysis") {
677
- return {
678
- repoPath: ".",
679
- question: "Analyze the repository structure and provide concrete file-level evidence for the requested code/configuration question.",
680
- timeoutMs: 30000,
681
- skipGitRepoCheck: true,
682
- };
683
- }
684
- if (toolName === "k8s_cluster_investigate") {
685
- return { timeoutMs: 30000 };
686
- }
687
- if (toolName === "k8s_cluster_triage") {
688
- return { timeoutMs: 30000 };
639
+ function buildFallbackEvidenceToolArgs(tool) {
640
+ const schema = normalizeModelFacingToolSchema(tool);
641
+ if (typeof schema.properties !== "object" || schema.properties === null || Array.isArray(schema.properties)) {
642
+ return {};
689
643
  }
690
- if (toolName === "gh_actions_command") {
691
- return { args: ["list", "--limit", "5"], cwd: ".", timeoutMs: 30000 };
644
+ const required = Array.isArray(schema.required)
645
+ ? schema.required.filter((name) => typeof name === "string")
646
+ : [];
647
+ const values = {};
648
+ for (const [key, value] of Object.entries(schema.properties)) {
649
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
650
+ continue;
651
+ }
652
+ const property = value;
653
+ if ("default" in property) {
654
+ values[key] = property.default;
655
+ continue;
656
+ }
657
+ if ("const" in property) {
658
+ values[key] = property.const;
659
+ continue;
660
+ }
661
+ if (required.includes(key) && Array.isArray(property.enum) && property.enum.length > 0) {
662
+ values[key] = property.enum[0];
663
+ }
692
664
  }
693
- return {};
665
+ return values;
694
666
  }
695
- function buildFallbackEvidenceToolCall(input, tools) {
667
+ function buildFallbackEvidenceToolCall(input, tools, rawText = "") {
696
668
  if (!hasPriorToolResultForToolName(input, "write_todos") && !hasPriorToolResultForToolName(input, "tool_call_write_todos")) {
697
669
  return null;
698
670
  }
699
- const evidenceToolName = selectFallbackEvidenceToolName(input, tools);
671
+ const evidenceToolName = selectFallbackEvidenceToolName(tools, rawText);
700
672
  if (!evidenceToolName) {
701
673
  return null;
702
674
  }
703
- const boundToolName = tools
704
- .map((tool) => readBoundToolName(tool))
705
- .find((name) => normalizeDomainToolName(name) === evidenceToolName);
706
- if (!boundToolName || hasPriorToolResultForToolName(input, boundToolName) || hasPriorToolResultForToolName(input, evidenceToolName)) {
675
+ const boundTool = tools.find((tool) => normalizeDomainToolName(readBoundToolName(tool)) === evidenceToolName);
676
+ const boundToolName = readBoundToolName(boundTool);
677
+ if (!boundTool || !boundToolName || hasPriorToolResultForToolName(input, boundToolName) || hasPriorToolResultForToolName(input, evidenceToolName)) {
707
678
  return null;
708
679
  }
709
680
  return new AIMessage({
@@ -711,7 +682,7 @@ function buildFallbackEvidenceToolCall(input, tools) {
711
682
  tool_calls: [{
712
683
  id: `fallback-evidence-${Math.random().toString(36).slice(2, 10)}`,
713
684
  name: boundToolName,
714
- args: buildFallbackEvidenceToolArgs(evidenceToolName, input),
685
+ args: buildFallbackEvidenceToolArgs(boundTool),
715
686
  type: "tool_call",
716
687
  }],
717
688
  });
@@ -725,7 +696,7 @@ function buildFallbackTodoCompletionToolCall(input, tools) {
725
696
  if (!planningToolName) {
726
697
  return null;
727
698
  }
728
- const evidenceToolName = selectFallbackEvidenceToolName(input, tools);
699
+ const evidenceToolName = selectFallbackEvidenceToolName(tools, prompt);
729
700
  if (!evidenceToolName) {
730
701
  return null;
731
702
  }
@@ -735,7 +706,7 @@ function buildFallbackTodoCompletionToolCall(input, tools) {
735
706
  if (!hasEvidenceResult) {
736
707
  return null;
737
708
  }
738
- const todos = buildToolAwareFallbackTodoContents(input, tools).map((content) => ({
709
+ const todos = buildToolAwareFallbackTodoContents(tools, prompt).map((content) => ({
739
710
  content,
740
711
  status: "completed",
741
712
  }));
@@ -749,6 +720,33 @@ function buildFallbackTodoCompletionToolCall(input, tools) {
749
720
  }],
750
721
  });
751
722
  }
723
+ function parsedToolCallCompletesTodoPlan(toolCall) {
724
+ if (!isTodoPlanningToolName(toolCall.name)) {
725
+ return false;
726
+ }
727
+ const todos = toolCall.args.todos;
728
+ if (!Array.isArray(todos) || todos.length === 0) {
729
+ return false;
730
+ }
731
+ return todos.every((todo) => typeof todo === "object"
732
+ && todo !== null
733
+ && todo.status === "completed");
734
+ }
735
+ function normalizeInitialTodoPlanToolCall(toolCall) {
736
+ if (!parsedToolCallCompletesTodoPlan(toolCall)) {
737
+ return toolCall;
738
+ }
739
+ const todos = toolCall.args.todos.map((todo, index) => typeof todo === "object" && todo !== null && !Array.isArray(todo)
740
+ ? { ...todo, status: index === 0 ? "in_progress" : "pending" }
741
+ : todo);
742
+ return {
743
+ name: toolCall.name,
744
+ args: {
745
+ ...toolCall.args,
746
+ todos,
747
+ },
748
+ };
749
+ }
752
750
  function formatBoundToolInstruction(tool) {
753
751
  if (typeof tool !== "object" || tool === null) {
754
752
  return null;
@@ -805,6 +803,15 @@ function createPromptedJsonToolBindableModel(model, boundTools = [], options = {
805
803
  return async (input, config) => {
806
804
  const effectiveBoundTools = selectPlanningToolsForTurn(input, boundTools);
807
805
  const forcePlanningToolCall = shouldLimitToolsToPlanning(input);
806
+ if (options.settleCompletedToolResults === true
807
+ && !forcePlanningToolCall
808
+ && effectiveBoundTools.length > 0
809
+ && hasAnyPriorToolResult(input)
810
+ && hasPriorNonPlanningToolCall(input)) {
811
+ return new AIMessage({
812
+ content: readLatestToolResultContent(input) ?? "",
813
+ });
814
+ }
808
815
  const rawResult = await target.invoke(effectiveBoundTools.length > 0
809
816
  ? withPromptedJsonToolPrompt(input, effectiveBoundTools, { ...options, forcePlanningToolCall })
810
817
  : input, config);
@@ -825,22 +832,32 @@ function createPromptedJsonToolBindableModel(model, boundTools = [], options = {
825
832
  if (fallbackCompletionToolCall) {
826
833
  return fallbackCompletionToolCall;
827
834
  }
828
- const fallbackToolCall = buildFallbackEvidenceToolCall(input, effectiveBoundTools);
835
+ const fallbackToolCall = buildFallbackEvidenceToolCall(input, effectiveBoundTools, text);
829
836
  if (fallbackToolCall) {
830
837
  return fallbackToolCall;
831
838
  }
832
839
  }
833
840
  return rawResult;
834
841
  }
835
- if (hasPriorToolResultForToolName(input, parsedToolCall.name)) {
842
+ const effectiveParsedToolCall = forcePlanningToolCall
843
+ ? normalizeInitialTodoPlanToolCall(parsedToolCall)
844
+ : parsedToolCall;
845
+ if (parsedToolCallCompletesTodoPlan(effectiveParsedToolCall)
846
+ && !hasPriorNonPlanningToolResult(input, effectiveBoundTools)) {
847
+ const fallbackToolCall = buildFallbackEvidenceToolCall(input, effectiveBoundTools, text);
848
+ if (fallbackToolCall) {
849
+ return fallbackToolCall;
850
+ }
851
+ }
852
+ if (hasPriorToolResultForToolName(input, effectiveParsedToolCall.name) || hasAnyPriorToolResult(input)) {
836
853
  return rawResult;
837
854
  }
838
855
  return new AIMessage({
839
856
  content: "",
840
857
  tool_calls: [{
841
858
  id: `tool-${Math.random().toString(36).slice(2, 10)}`,
842
- name: parsedToolCall.name,
843
- args: parsedToolCall.args,
859
+ name: effectiveParsedToolCall.name,
860
+ args: effectiveParsedToolCall.args,
844
861
  type: "tool_call",
845
862
  }],
846
863
  });
@@ -916,7 +933,10 @@ export async function createResolvedModel(model, modelResolver) {
916
933
  const { toolCallingMode, ...init } = model.init ?? {};
917
934
  const resolved = new ChatOllama({ model: model.model, ...init });
918
935
  if (toolCallingMode === "prompted-json") {
919
- return createPromptedJsonToolBindableModel(resolved, [], { suppressThinking: init.think === false });
936
+ return createPromptedJsonToolBindableModel(resolved, [], {
937
+ settleCompletedToolResults: true,
938
+ suppressThinking: init.think === false,
939
+ });
920
940
  }
921
941
  return createProviderToolMessageCompatModel(resolved);
922
942
  }
@@ -927,7 +947,10 @@ export async function createResolvedModel(model, modelResolver) {
927
947
  ...normalizeOpenAICompatibleInit(init),
928
948
  });
929
949
  if (toolCallingMode === "prompted-json") {
930
- return createPromptedJsonToolBindableModel(resolved);
950
+ return createPromptedJsonToolBindableModel(resolved, [], {
951
+ settleCompletedToolResults: true,
952
+ suppressThinking: true,
953
+ });
931
954
  }
932
955
  return createProviderToolMessageCompatModel(resolved);
933
956
  }
@@ -36,13 +36,16 @@ function shouldSuppressVisibleToolCallText(value) {
36
36
  if (/^(?:model_request|tool_call|call_tool)\b/iu.test(trimmed)) {
37
37
  return true;
38
38
  }
39
+ if (/^(?:name|tool_call_id)\s*=/iu.test(trimmed)) {
40
+ return true;
41
+ }
39
42
  if (/^(?:we\s+need\s+to|so\s+next\s+step\b)/iu.test(trimmed)) {
40
43
  return true;
41
44
  }
42
45
  if (/\b(?:must|need|needs|should|will)\s+(?:now\s+)?(?:call|use|run|produce)\s+[A-Za-z_][A-Za-z0-9_]*\b/iu.test(trimmed)) {
43
46
  return true;
44
47
  }
45
- if (/^\{\s*"(?:name|arguments|todos|symbol|query|market|count)"\s*:/iu.test(trimmed)) {
48
+ if (/^\{\s*"(?:name|arguments|args|argv|todos|symbol|query|market|count|stdout|stderr|exitCode)"\s*:/iu.test(trimmed)) {
46
49
  return true;
47
50
  }
48
51
  try {
@@ -55,7 +58,12 @@ function shouldSuppressVisibleToolCallText(value) {
55
58
  || "market" in parsed
56
59
  || "count" in parsed
57
60
  || "arguments" in parsed
58
- || "name" in parsed)) {
61
+ || "args" in parsed
62
+ || "argv" in parsed
63
+ || "name" in parsed
64
+ || "stdout" in parsed
65
+ || "stderr" in parsed
66
+ || "exitCode" in parsed)) {
59
67
  return true;
60
68
  }
61
69
  }
@@ -71,6 +79,63 @@ function shouldSuppressVisibleToolCallText(value) {
71
79
  }
72
80
  return salvageFunctionLikeToolCall(prefixedToolCallMatch[1]) !== null;
73
81
  }
82
+ function stripVisibleToolCallResidue(value) {
83
+ const lines = value.split(/\r?\n/);
84
+ let changed = false;
85
+ const kept = lines.filter((line) => {
86
+ if (shouldSuppressVisibleToolCallText(line)) {
87
+ changed = true;
88
+ return false;
89
+ }
90
+ return true;
91
+ });
92
+ if (changed) {
93
+ return kept.join("\n").trim();
94
+ }
95
+ const trimmedStart = value.trimStart();
96
+ if (!trimmedStart.startsWith("{")) {
97
+ return value;
98
+ }
99
+ const parsedPrefix = extractLeadingJsonObject(trimmedStart);
100
+ if (!parsedPrefix || !shouldSuppressVisibleToolCallText(parsedPrefix.json)) {
101
+ return value;
102
+ }
103
+ return parsedPrefix.rest.trimStart();
104
+ }
105
+ function extractLeadingJsonObject(value) {
106
+ let depth = 0;
107
+ let inString = false;
108
+ let escaped = false;
109
+ for (let index = 0; index < value.length; index += 1) {
110
+ const char = value[index];
111
+ if (escaped) {
112
+ escaped = false;
113
+ continue;
114
+ }
115
+ if (char === "\\") {
116
+ escaped = inString;
117
+ continue;
118
+ }
119
+ if (char === "\"") {
120
+ inString = !inString;
121
+ continue;
122
+ }
123
+ if (inString) {
124
+ continue;
125
+ }
126
+ if (char === "{") {
127
+ depth += 1;
128
+ continue;
129
+ }
130
+ if (char === "}") {
131
+ depth -= 1;
132
+ if (depth === 0) {
133
+ return { json: value.slice(0, index + 1), rest: value.slice(index + 1) };
134
+ }
135
+ }
136
+ }
137
+ return null;
138
+ }
74
139
  function readSummaryCounts(summary) {
75
140
  if (typeof summary !== "object" || summary === null) {
76
141
  return null;
@@ -375,7 +440,7 @@ export function projectRuntimeStreamEvent(params) {
375
440
  const allowVisibleContent = isRootVisibleEvent && state.openTaskDelegations === 0;
376
441
  const allowStreamedVisibleContent = allowVisibleContent && !state.emittedToolResult && !state.emittedToolError;
377
442
  if (allowVisibleStreamDeltas && allowStreamedVisibleContent) {
378
- const visibleStreamOutput = extractVisibleStreamOutput(event);
443
+ const visibleStreamOutput = stripVisibleToolCallResidue(extractVisibleStreamOutput(event));
379
444
  if (visibleStreamOutput && !shouldSuppressVisibleToolCallText(visibleStreamOutput)) {
380
445
  const nextOutput = computeIncrementalOutput(state.emittedOutput, visibleStreamOutput);
381
446
  state.emittedOutput = nextOutput.accumulated;
@@ -385,7 +450,7 @@ export function projectRuntimeStreamEvent(params) {
385
450
  }
386
451
  }
387
452
  if (includeStateStreamOutput && allowVisibleContent) {
388
- const stateStreamOutput = extractStateStreamOutput(event);
453
+ const stateStreamOutput = stripVisibleToolCallResidue(extractStateStreamOutput(event));
389
454
  if (stateStreamOutput && !shouldSuppressVisibleToolCallText(stateStreamOutput)) {
390
455
  const nextOutput = computeIncrementalOutput(state.emittedOutput, stateStreamOutput);
391
456
  state.emittedOutput = nextOutput.accumulated;
@@ -456,7 +521,7 @@ export function projectRuntimeStreamEvent(params) {
456
521
  state.lastCompletedTaskDelegationFindings = "";
457
522
  }
458
523
  }
459
- const output = allowVisibleContent ? extractTerminalStreamOutput(event) : "";
524
+ const output = allowVisibleContent ? stripVisibleToolCallResidue(extractTerminalStreamOutput(event)) : "";
460
525
  if (!allowVisibleContent) {
461
526
  const delegatedTerminalOutput = extractTerminalStreamOutput(event);
462
527
  if (delegatedTerminalOutput) {
@@ -1,5 +1,6 @@
1
1
  import { interrupt } from "@langchain/langgraph";
2
2
  import path from "node:path";
3
+ import { validateToolGatewayInput } from "../../harness/tool-gateway/index.js";
3
4
  const DEDUPED_REMOTE_TOOL_NAMES = new Set(["builtin.fetch_url", "builtin.web_search"]);
4
5
  const PATH_LIKE_INPUT_KEYS = new Set(["path", "root", "dir", "directory", "cwd"]);
5
6
  const ROOT_SCOPING_INPUT_KEYS = new Set(["root", "dir", "directory", "cwd"]);
@@ -169,8 +170,17 @@ export function wrapToolForHumanInTheLoop(resolvedTool, compiledTool, binding, i
169
170
  }
170
171
  const target = resolvedTool;
171
172
  const runWithApproval = async (input, config, invokeOriginal) => {
173
+ const gateway = validateToolGatewayInput({
174
+ toolName: compiledTool.name,
175
+ schema: target.schema,
176
+ args: input,
177
+ requiresApproval: true,
178
+ });
179
+ if (!gateway.ok) {
180
+ return gateway.error;
181
+ }
172
182
  if (decisionMode === "auto-approve") {
173
- return invokeOriginal(input, config);
183
+ return invokeOriginal(gateway.input, config);
174
184
  }
175
185
  if (decisionMode === "auto-reject" || decisionMode === "deny-and-continue") {
176
186
  return "Tool execution denied by runtime policy.";
@@ -179,11 +189,11 @@ export function wrapToolForHumanInTheLoop(resolvedTool, compiledTool, binding, i
179
189
  toolName: compiledTool.name,
180
190
  toolId: compiledTool.id,
181
191
  allowedDecisions: compiledTool.hitl?.allow ?? ["approve", "edit", "reject"],
182
- inputPreview: toInputPreview(input),
192
+ inputPreview: toInputPreview(gateway.input),
183
193
  decisionMode,
184
194
  ...(toolApprovalReason(compiledTool) ? { approvalReason: toolApprovalReason(compiledTool) } : {}),
185
195
  });
186
- const approvedInput = resolveApprovedInput(input, resumed);
196
+ const approvedInput = resolveApprovedInput(gateway.input, resumed);
187
197
  if (approvedInput === Symbol.for("agent-harness.hitl.reject")) {
188
198
  return "Tool execution rejected by human reviewer.";
189
199
  }
@@ -295,13 +305,46 @@ export function wrapToolForExecution(resolvedTool, compiledTool, binding, interr
295
305
  get(obj, prop, receiver) {
296
306
  const value = Reflect.get(obj, prop, receiver);
297
307
  if (prop === "invoke" && typeof value === "function") {
298
- return (input, config) => obj.invoke(guardWorkspaceBoundToolInput(input, compiledTool, binding), config);
308
+ return (input, config) => {
309
+ const gateway = validateToolGatewayInput({
310
+ toolName: compiledTool.name,
311
+ schema: obj.schema,
312
+ args: input,
313
+ requiresApproval: resolveToolApprovalDecisionMode(compiledTool, binding) !== "none",
314
+ });
315
+ if (!gateway.ok) {
316
+ return gateway.error;
317
+ }
318
+ return obj.invoke(guardWorkspaceBoundToolInput(gateway.input, compiledTool, binding), config);
319
+ };
299
320
  }
300
321
  if (prop === "call" && typeof value === "function") {
301
- return (input, config) => obj.call(guardWorkspaceBoundToolInput(input, compiledTool, binding), config);
322
+ return (input, config) => {
323
+ const gateway = validateToolGatewayInput({
324
+ toolName: compiledTool.name,
325
+ schema: obj.schema,
326
+ args: input,
327
+ requiresApproval: resolveToolApprovalDecisionMode(compiledTool, binding) !== "none",
328
+ });
329
+ if (!gateway.ok) {
330
+ return gateway.error;
331
+ }
332
+ return obj.call(guardWorkspaceBoundToolInput(gateway.input, compiledTool, binding), config);
333
+ };
302
334
  }
303
335
  if (prop === "func" && typeof value === "function") {
304
- return (input, config) => obj.func(guardWorkspaceBoundToolInput(input, compiledTool, binding), config);
336
+ return (input, config) => {
337
+ const gateway = validateToolGatewayInput({
338
+ toolName: compiledTool.name,
339
+ schema: obj.schema,
340
+ args: input,
341
+ requiresApproval: resolveToolApprovalDecisionMode(compiledTool, binding) !== "none",
342
+ });
343
+ if (!gateway.ok) {
344
+ return gateway.error;
345
+ }
346
+ return obj.func(guardWorkspaceBoundToolInput(gateway.input, compiledTool, binding), config);
347
+ };
305
348
  }
306
349
  return value;
307
350
  },