Skip to content

fix: recover Kimi bash text tool calls#212

Closed
0xCheetah1 wants to merge 1 commit into
BlockRunAI:mainfrom
0xCheetah1:fix/kimi-cmd-text-tool-call
Closed

fix: recover Kimi bash text tool calls#212
0xCheetah1 wants to merge 1 commit into
BlockRunAI:mainfrom
0xCheetah1:fix/kimi-cmd-text-tool-call

Conversation

@0xCheetah1

@0xCheetah1 0xCheetah1 commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Summary

  • recover Kimi/OpenCode-style bash JSON emitted as assistant text
  • convert {"cmd":["bash","-lc","..."],"timeout":...} into a terminal tool call
  • keep non-bash cmd JSON as normal assistant text

Verification

  • npm test -- textual-tool-calls
  • npm run typecheck
  • npm run lint
  • npm run build
  • npx -y node@22 ./node_modules/vitest/vitest.mjs run

Summary by CodeRabbit

  • Bug Fixes
    • Improved detection of terminal tool calls when provided as JSON with bash cmd arrays, extracting the command and preserving an optional timeout.
    • Added guardrails to avoid misinterpreting non-bash cmd arrays as tool calls.
    • Enhanced handling of narrated web-search inputs so tool calls are inferred only when narration is present.
  • Tests
    • Expanded coverage with positive and negative cases for bash command-array and narrated web-search extraction.

@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c9472817-aef7-47b8-b73e-13e90c79445e

📥 Commits

Reviewing files that changed from the base of the PR and between 0063b94 and 801332a.

⛔ Files ignored due to path filters (4)
  • dist/cli.js is excluded by !**/dist/**
  • dist/cli.js.map is excluded by !**/dist/**, !**/*.map
  • dist/index.js is excluded by !**/dist/**
  • dist/index.js.map is excluded by !**/dist/**, !**/*.map
📒 Files selected for processing (2)
  • src/textual-tool-calls.test.ts
  • src/textual-tool-calls.ts

📝 Walkthrough

Walkthrough

GPT plain-text tool-call extraction now recognizes bash cmd JSON objects and narrated bare web_search arguments, while rejecting invalid command arrays and un-narrated query objects.

Changes

Textual tool-call extraction

Layer / File(s) Summary
JSON recognizer integration
src/textual-tool-calls.ts
Adds validated bash cmd array conversion to terminal calls, preserves finite timeouts, and supports narrated web_search objects that consume preceding prose.
Recognizer validation tests
src/textual-tool-calls.test.ts
Covers valid and invalid bash command extraction plus narrated and un-narrated web-search argument handling.

Estimated code review effort: 2 (Simple) | ~15 minutes

Possibly related PRs

Suggested reviewers: 1bcmax

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: recovering Kimi/OpenCode-style bash text tool calls.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
src/textual-tool-calls.ts (2)

293-302: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Optimize array validation to avoid rest spread allocation.

Using the spread operator (...rest) allocates a new array, which is inefficient if parsed.cmd is unexpectedly large. Enforcing the exact expected length (parsed.cmd.length !== 3) upfront is safer and avoids unnecessary allocations.

♻️ Proposed refactor
-  if (!isPlainObject(parsed) || !Array.isArray(parsed.cmd)) return null;
-  const [bin, flag, command, ...rest] = parsed.cmd;
-  if (
-    rest.length > 0 ||
-    typeof bin !== "string" ||
-    typeof flag !== "string" ||
-    typeof command !== "string"
-  ) {
-    return null;
-  }
+  if (!isPlainObject(parsed) || !Array.isArray(parsed.cmd) || parsed.cmd.length !== 3) return null;
+  const [bin, flag, command] = parsed.cmd;
+  if (typeof bin !== "string" || typeof flag !== "string" || typeof command !== "string") {
+    return null;
+  }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/textual-tool-calls.ts` around lines 293 - 302, Update the parsed.cmd
validation in the textual tool-call parsing flow to check parsed.cmd.length !==
3 before destructuring, then destructure only the three expected values without
a rest array. Preserve the existing string-type validation and null-return
behavior.

384-390: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Avoid duplicate parsing and validation of the tool call.

callFromBashCmdObject(trailing.parsed) is evaluated twice when processing bash tool calls. Caching the result from the initial fallback avoids redundant array destructuring, string splitting, and object allocations.

♻️ Proposed refactor
-    const built = callFromGptObject(trailing.parsed) ?? callFromBashCmdObject(trailing.parsed);
+    const gptBuilt = callFromGptObject(trailing.parsed);
+    const bashBuilt = gptBuilt ? null : callFromBashCmdObject(trailing.parsed);
+    const built = gptBuilt ?? bashBuilt;
     if (built) {
       const hasProseBefore = content.slice(0, trailing.start).trim() !== "";
       const typeIsFunction = (trailing.parsed as Record<string, unknown>).type === "function";
-      const isBashCmd =
-        built.name === "terminal" && callFromBashCmdObject(trailing.parsed) !== null;
+      const isBashCmd = bashBuilt !== null;
       if (!hasProseBefore || typeIsFunction || isBashCmd) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/textual-tool-calls.ts` around lines 384 - 390, Cache the result of
callFromBashCmdObject(trailing.parsed) when constructing built, then reuse that
cached value for the isBashCmd check instead of invoking the parser again.
Preserve the existing fallback order and bash-tool detection behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/textual-tool-calls.ts`:
- Around line 293-302: Update the parsed.cmd validation in the textual tool-call
parsing flow to check parsed.cmd.length !== 3 before destructuring, then
destructure only the three expected values without a rest array. Preserve the
existing string-type validation and null-return behavior.
- Around line 384-390: Cache the result of
callFromBashCmdObject(trailing.parsed) when constructing built, then reuse that
cached value for the isBashCmd check instead of invoking the parser again.
Preserve the existing fallback order and bash-tool detection behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: bae19bbc-1473-4c37-acd1-99151f844d6d

📥 Commits

Reviewing files that changed from the base of the PR and between 8268cb5 and 32f08ea.

⛔ Files ignored due to path filters (4)
  • dist/cli.js is excluded by !**/dist/**
  • dist/cli.js.map is excluded by !**/dist/**, !**/*.map
  • dist/index.js is excluded by !**/dist/**
  • dist/index.js.map is excluded by !**/dist/**, !**/*.map
📒 Files selected for processing (2)
  • src/textual-tool-calls.test.ts
  • src/textual-tool-calls.ts

@0xCheetah1
0xCheetah1 force-pushed the fix/kimi-cmd-text-tool-call branch from 32f08ea to 0063b94 Compare July 18, 2026 10:32

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
src/textual-tool-calls.ts (1)

384-390: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Avoid redundant evaluation of callFromBashCmdObject.

callFromBashCmdObject(trailing.parsed) is evaluated twice in the worst case (once when computing built and once when checking isBashCmd). You can refactor this to capture the origin of the parsed object and avoid the duplicate call.

♻️ Proposed refactor
-    const built = callFromGptObject(trailing.parsed) ?? callFromBashCmdObject(trailing.parsed);
+    const gptCall = callFromGptObject(trailing.parsed);
+    const bashCall = gptCall ? null : callFromBashCmdObject(trailing.parsed);
+    const built = gptCall ?? bashCall;
     if (built) {
       const hasProseBefore = content.slice(0, trailing.start).trim() !== "";
       const typeIsFunction = (trailing.parsed as Record<string, unknown>).type === "function";
-      const isBashCmd =
-        built.name === "terminal" && callFromBashCmdObject(trailing.parsed) !== null;
+      const isBashCmd = bashCall !== null;
       if (!hasProseBefore || typeIsFunction || isBashCmd) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/textual-tool-calls.ts` around lines 384 - 390, Update the parsing logic
around built, callFromGptObject, and callFromBashCmdObject to evaluate each
parser once while retaining which parser produced the result. Use that captured
origin when computing isBashCmd, preserving the existing terminal-name and
condition behavior without invoking callFromBashCmdObject a second time.
src/textual-tool-calls.test.ts (1)

275-285: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider adding a test case for trailing bash command JSON after prose.

The source code explicitly allows bash command objects to be extracted even when preceded by prose (if (!hasProseBefore || typeIsFunction || isBashCmd)). Adding a test for this specific trailing scenario would lock in this behavior and prevent regressions.

🧪 Proposed test addition
     it("does NOT overwrite an existing command when cmd is also present", () => {
       const content = '{"name":"terminal","parameters":{"cmd":"a","command":"b"}}';
       const result = extractTextualToolCalls(content);
       const args = JSON.parse(result.toolCalls[0]!.function.arguments) as Record<string, unknown>;
       expect(args.command).toBe("b");
       expect(args.cmd).toBe("a");
     });
 
     it("extracts Kimi/OpenCode bash cmd JSON as terminal", () => {
       const content = '{"cmd":["bash","-lc","ls -1a"],"timeout":10000}';
       const result = extractTextualToolCalls(content);
       expect(result.toolCalls).toHaveLength(1);
       expect(result.toolCalls[0]?.function.name).toBe("terminal");
       expect(JSON.parse(result.toolCalls[0]!.function.arguments)).toEqual({
         command: "ls -1a",
         timeout: 10000,
       });
       expect(result.cleanedContent).toBe("");
     });
+
+    it("extracts a trailing bash cmd JSON after prose", () => {
+      const content = 'I will run this.\n{"cmd":["bash","-lc","ls -1a"],"timeout":10000}';
+      const result = extractTextualToolCalls(content);
+      expect(result.toolCalls).toHaveLength(1);
+      expect(result.toolCalls[0]?.function.name).toBe("terminal");
+      expect(result.cleanedContent.trim()).toBe("I will run this.");
+    });
 
     it("does NOT fire on non-bash cmd JSON", () => {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/textual-tool-calls.test.ts` around lines 275 - 285, Add a test in the
textual tool-call extraction suite for prose followed by a bash command JSON
object, using extractTextualToolCalls. Assert that the trailing object becomes
one terminal tool call with the expected command and timeout, and that
cleanedContent retains only the preceding prose.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/textual-tool-calls.test.ts`:
- Around line 275-285: Add a test in the textual tool-call extraction suite for
prose followed by a bash command JSON object, using extractTextualToolCalls.
Assert that the trailing object becomes one terminal tool call with the expected
command and timeout, and that cleanedContent retains only the preceding prose.

In `@src/textual-tool-calls.ts`:
- Around line 384-390: Update the parsing logic around built, callFromGptObject,
and callFromBashCmdObject to evaluate each parser once while retaining which
parser produced the result. Use that captured origin when computing isBashCmd,
preserving the existing terminal-name and condition behavior without invoking
callFromBashCmdObject a second time.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6d9f7879-0220-4845-9c65-0b879f9c0906

📥 Commits

Reviewing files that changed from the base of the PR and between 32f08ea and 0063b94.

⛔ Files ignored due to path filters (4)
  • dist/cli.js is excluded by !**/dist/**
  • dist/cli.js.map is excluded by !**/dist/**, !**/*.map
  • dist/index.js is excluded by !**/dist/**
  • dist/index.js.map is excluded by !**/dist/**, !**/*.map
📒 Files selected for processing (2)
  • src/textual-tool-calls.test.ts
  • src/textual-tool-calls.ts

@0xCheetah1
0xCheetah1 force-pushed the fix/kimi-cmd-text-tool-call branch from 0063b94 to 801332a Compare July 18, 2026 11:58
@0xCheetah1

Copy link
Copy Markdown
Contributor Author

Closing for now while testbed validation continues. Will reopen or recreate after the fix is fully tested.

@0xCheetah1 0xCheetah1 closed this Jul 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant