Skip to content

UI tool ask batching fails when models insert API rows between tool calls #1004

Description

@easonLiangWorldedtech

UI tool ask batching fails when models insert API rows between tool calls

Problem

When using models like qwen, the UI shows multiple separate prompts for the same type of tool call:

Zoo wants to read this file (a.ts)
Zoo wants to read this file (b.ts)
Zoo wants to edit this file (c.ts)

Instead of a single batched prompt:

Zoo wants to read these files:
- a.ts
- b.ts

Root Cause

The existing batchConsecutive() utility only merges tool asks that are truly adjacent in the message array. However, models like qwen insert low-information messages between tool calls during streaming:

  • say: "api_req_started" — API request metadata row
  • say: "api_req_finished" — API request completion row
  • say: "text" with empty content — partial streaming rows
  • say: "reasoning" — hidden reasoning text

These messages break the consecutive chain, so batchConsecutive stops merging.

Current Code (ChatView.tsx ~line 1272)

const readFileBatched = batchConsecutive(filtered, isReadFileAsk, synthesizeReadFileBatch)
const listFilesBatched = batchConsecutive(readFileBatched, isListFilesAsk, synthesizeListFilesBatch)
const result = batchConsecutive(listFilesBatched, isEditFileAsk, synthesizeEditFileBatch)

Proposed Solution: batchNearby() utility

A new utility that merges same-type tool asks even when separated by ignorable messages. It uses three predicates:

  1. isTarget — matches the target tool type (readFile, listFiles, editFile)
  2. isIgnorableBetweenTargets — skips over low-info messages without breaking the batch
  3. isBoundary — stops merging when hitting a semantic boundary

Ignorable Messages (skipped during batching)

  • api_req_started / api_req_finished
  • Empty text rows (say: "text" with no content)
  • Reasoning rows (say: "reasoning")

Semantic Boundaries (stop merging)

  • User feedback (user_feedback, user_feedback_diff)
  • Visible assistant text (text with content)
  • Completion result (completion_result)
  • Checkpoint saved (checkpoint_saved)
  • Errors (error)
  • Context condensation (condense_context)
  • Codebase search results (codebase_search_result)

New Usage in ChatView.tsx

const readFileBatched = batchNearby(filtered, {
  isTarget: isReadFileAsk,
  isIgnorableBetweenTargets, // api_req_started/finished, empty text, reasoning
  isBoundary,                // user_feedback, visible text, completion_result, error, etc.
  synthesize: synthesizeReadFileBatch,
})

Files Changed

  • New: webview-ui/src/utils/batchNearby.ts — the batching utility function
  • New: webview-ui/src/utils/__tests__/batchNearby.spec.ts — 23 comprehensive tests
  • Modified: webview-ui/src/components/chat/ChatView.tsx — replace batchConsecutive with batchNearby

Testing

All 23 unit tests pass, covering:

  • Empty input / no matches / single match passthrough
  • Consecutive vs non-consecutive matching
  • Ignorable messages between targets (api_req_started/finished, empty text, reasoning)
  • Semantic boundaries stopping the merge (user_feedback, visible text, completion_result, checkpoint_saved, error)
  • Multiple batches separated by boundaries
  • Realistic qwen scenarios with API rows between tool calls

Long-term Improvement (separate issue)

Add toolCallGroupId metadata to backend messages so UI can use explicit grouping instead of heuristic-based merging. This would make batching work consistently across all model providers regardless of streaming format differences.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions