agentscope-ai / agentscope-ai/QwenPaw

[Bug]: Long console session with heavy tool usage fails with 400 "Messages with role 'tool' must be a response to a preceding message with 'tool_calls'"

Ouverte
#6,726 2 commentaires 0 réactions 2 personnes assignées Réclamée par @niceIrene Voir sur GitHub
bug
Langage dominant
Python
Étoiles
34.9k
Forks
3.1k
Merge moyen
1 j 15 h
PR mergées (30 j)
225

Description

## QwenPaw Version / 版本

**2.0.0**(Tauri desktop build / Tauri 桌面版;`qwenpaw.exe --version` → `QwenPaw, version 2.0.0`)

## Description / 问题描述

**English:** In a long console-channel session where the agent made a **large number of tool calls** (up to 20–30+ `tool_call`/`tool_result` pairs accumulated inside a single context message), the backend failed with an OpenAI-compatible API 400 error and the whole turn aborted. QwenPaw dumped the failing request to `%TEMP%\qwenpaw_query_error_*.json`. The dump shows the problem: the internal context contains an **anomalous `role=assistant, name=system` message whose content includes a `tool_result` block** (see Logs below). When that context is converted to OpenAI-format messages, the `tool` role message has no preceding `assistant` message with matching `tool_calls`, so the model API rejects it. This looks like a message-pairing bug in the context → model-message conversion (likely related to `light_context_config.tool_result_pruning_config` / scroll context or compaction), not a user error.

**中文:** 在 console 渠道的长时间会话中,agent 进行了**大量工具调用**(单条 context 消息内堆积了 20–30+ 组 `tool_call`/`tool_result`),后端以 OpenAI 兼容 API 的 400 错误中止了整个回合。QwenPaw 将失败的请求 dump 到了 `%TEMP%\qwenpaw_query_error_*.json`。dump 显示问题所在:内部 context 中存在一条**异常的 `role=assistant, name=system` 消息,其内容包含 `tool_result` 块**(见下方日志)。当该 context 转换为 OpenAI 格式消息时,`tool` 角色的消息前面没有带匹配 `tool_calls` 的 `assistant` 消息,因此被模型 API 拒绝。这看起来是 context → 模型消息转换中的**消息配对 bug**(可能与 `light_context_config.tool_result_pruning_config` / scroll 上下文或压缩有关),并非用户操作错误。

**Related PR(s):** (optional / 选填)

**Security considerations:** none — no secrets in the dump beyond a local dev password string in the user text. / 无——dump 中除用户文本里的本地开发密码字符串外无其他敏感信息。

## Component(s) Affected / 受影响组件

- [x] Core / Backend (app, agents, config, providers, utils, local_models)
- [ ] Console (frontend web UI)
- [ ] Channels (DingTalk, Feishu, QQ, Discord, iMessage, etc.)
- [ ] Skills
- [ ] CLI
- [ ] Documentation (website)
- [ ] Tests
- [ ] CI/CD
- [ ] Scripts / Deploy

## Environment / 环境

- **QwenPaw version / 版本:** 2.0.0
- **OS:** Windows 10 Pro 22H2 (10.0.19045)
- **Install method / 安装方式:** Desktop app (Tauri) — bundled `qwenpaw-backend.exe` (PyInstaller)
- **Python version (if applicable):** bundled with binary / 随二进制打包,未单独安装

## Steps to Reproduce / 复现步骤

1. Start a console session with an agent that has many tools (e.g. `recall_history`, `read_file`, `execute_shell_command`). / 在 console 渠道启动一个带有多个工具的 agent(如 `recall_history`、`read_file`、`execute_shell_command`)。
2. Ask the agent to resume/analyze a long previous task, so it repeatedly calls tools to search history. Let it run many turns so the conversation accumulates **20–30+ tool calls/results within a single context message** (see dump: one context message has 23 tool_call + 23 tool_result, another has 33 + 33). / 让 agent 回顾/分析一个较长的历史任务,使其反复调用工具搜索历史,运行多轮直到**单条 context 消息内累积 20–30+ 个工具调用/结果**(dump 中一条 context 消息有 23 个 tool_call + 23 个 tool_result,另一条有 33 + 33)。
3. Include some tool calls that fail with an error result (in our case `recall_history_python` repeatedly returned `RECALL FAILED (exit 1)`). / 让部分工具调用返回错误结果(本例中 `recall_history_python` 反复返回 `RECALL FAILED (exit 1)`)。
4. After the model has produced several more tool calls, the next model call fails with the 400 above and the session cannot continue. / 模型继续产生若干次工具调用后,下一次模型调用即触发上述 400 错误,会话无法继续。

**Expected / 期望:** the agent should keep working regardless of how many tool results accumulated (or pruning/compaction should not produce orphaned `tool` messages). / agent 无论累积多少工具结果都应能继续工作(或裁剪/压缩不应产生孤立的 `tool` 消息)。

## Actual vs Expected / 实际 vs 期望

- **Actual / 实际:** Whole turn aborts with `BadRequestError` 400 "Messages with role 'tool' must be a response to a preceding message with 'tool_calls'"; error dumped to `%TEMP%\qwenpaw_query_error_*.json`; session becomes unusable for further turns. / 整个回合以 `BadRequestError` 400 "Messages with role 'tool' must be a response to a preceding message with 'tool_calls'" 中止;错误 dump 到 `%TEMP%\qwenpaw_query_error_*.json`;该会话后续回合无法继续。
- **Expected / 期望:** Long sessions with many tool calls continue to work; tool-result pruning/compaction must never leave a `role='tool'` message without a preceding `tool_calls` assistant message. / 含大量工具调用的长会话应能继续工作;工具结果裁剪/压缩绝不能留下没有前置 `tool_calls` assistant 消息的 `role='tool'` 消息。

## Logs / Screenshots / 日志与截图

Full traceback (from `qwenpaw_query_error_02fa0p1g.json`):

```
Traceback (most recent call last):
File "qwenpaw\runtime\runtime.py", line 132, in run
File "qwenpaw\runtime\executor.py", line 47, in run
File "qwenpaw\runtime\heartbeat.py", line 26, in _iter_with_heartbeat
File "asyncio\tasks.py", line 489, in wait_for
File "agentscope\agent\_agent.py", line 221, in reply_stream
File "qwenpaw\agents\react_agent.py", line 605, in _reply
File "agentscope\agent\_agent.py", line 602, in _reply
File "agentscope\agent\_agent.py", line 595, in execute_chain
File "qwenpaw\agents\middlewares.py", line 111, in on_reply
File "agentscope\agent\_agent.py", line 589, in next_handler
File "agentscope\agent\_agent.py", line 580, in execute_chain
File "agentscope\agent\_agent.py", line 763, in _reply_impl
File "qwenpaw\agents\react_agent.py", line 425, in _reasoning
File "agentscope\agent\_agent.py", line 939, in _reasoning
File "agentscope\agent\_agent.py", line 1006, in _reasoning_impl
File "agentscope\agent\_agent.py", line 2428, in _call_model
File "agentscope\agent\_agent.py", line 2402, in _call_model
File "agentscope\agent\_agent.py", line 2396, in execute_chain
File "qwenpaw\agents\middlewares.py", line 102, in on_model_call
File "agentscope\agent\_agent.py", line 2391, in next_handler
File "agentscope\agent\_agent.py", line 2370, in execute_chain
File "qwenpaw\providers\retry_chat_model.py", line 499, in __call__
File "qwenpaw\token_usage\model_wrapper.py", line 122, in __call__
File "qwenpaw\providers\openai_chat_model_compat.py", line 615, in __call__
File "agentscope\model\_base.py", line 186, in __call__
File "qwenpaw\providers\openai_chat_model_compat.py", line 630, in _call_api
File "agentscope\model\_openai_chat\_model.py", line 272, in _call_api
File "openai\resources\chat\completions\completions.py", line 2714, in create
File "openai\_base_client.py", line 1913, in post
File "openai\_base_client.py", line 1698, in request
openai.BadRequestError: Error code: 400 - {'error': {'message': "Messages with role 'tool' must be a response to a preceding message with 'tool_calls'", 'type': 'invalid_request_error', 'param': None, 'code': 'invalid_request_error'}}
```

Context analysis from the dump (`agent_state.state.context`, 8 messages). Note `[6]` is the anomalous `role=assistant name=system` message that carries a `tool_result`:

```
[0] role=user name=user blocks=['text']
[1] role=assistant name=QA Agent blocks=['thinking','text']
[2] role=user name=user blocks=['text']
[3] role=assistant name=QA Agent tool_call=23 tool_result=23 thinking=17 text=17
[4] role=user name=user blocks=['text']
[5] role=assistant name=QA Agent tool_call=33 tool_result=33 thinking=26 text=26
[6] role=assistant name=system tool_call=0 tool_result=1 thinking=0 text=1 <-- anomalous / 异常消息
[7] role=user name=user blocks=['text']
```

Relevant runtime config (`~/.copaw/config.json` → `agents.running.light_context_config`):

```json
{
"strategy": "scroll",
"context_compact_config": {"enabled": true, "compact_threshold_ratio": 0.8, "reserve_threshold_ratio": 0.1},
"tool_result_pruning_config": {
"enabled": true,
"pruning_recent_n": 2,
"pruning_old_msg_max_bytes": 3000,
"pruning_recent_msg_max_bytes": 50000,
"execution_layer_max_bytes": 50000,
"offload_retention_days": 5,
"tool_results_cache": "tool_results",
"exempt_file_extensions": [".md"],
"exempt_tool_names": ["chat_with_agent"]
},
"scroll_config": {"tool_output_token_cap": 3000}
}
```

Full dump file (contains traceback + request + agent_state): `%TEMP%\qwenpaw_query_error_02fa0p1g.json`. Screenshot attached / 截图见附件。

## Additional Notes / 补充说明

- **Workaround for users / 用户临时规避:** start a **new session** instead of continuing the corrupted one; progress is not lost (files/memory still present). Avoid extremely long single sessions with many accumulated tool results. / **新开一个会话**继续,不要在被损坏的会话里续聊;任务进度不会丢失(文件/记忆仍在)。避免在单个会话中堆积极多工具结果。
- **Suspected root cause / 疑似根因:** tool-result pruning / scroll / compaction leaves a `tool_result` block inside an `assistant`/`system` message whose `tool_calls` were pruned, producing an unpaired `role='tool'` message on OpenAI-format conversion. Happens more easily when a tool repeatedly errors (e.g. `recall_history_python` exit 1) because the agent retries many times, accumulating results fast. / 工具结果裁剪 / scroll / 压缩把 `tool_result` 块留在了 `tool_calls` 已被裁剪的 `assistant`/`system` 消息中,转换为 OpenAI 格式时产生未配对的 `role='tool'` 消息。当某个工具反复报错(如 `recall_history_python` exit 1)时更容易触发,因为 agent 会多次重试、快速累积结果。
- Happy to provide the full `qwenpaw_query_error_*.json` on request (it contains everything: request payload + agent_state + traceback). / 如有需要可提供完整 `qwenpaw_query_error_*.json`(含 request payload + agent_state + traceback)。

- **Actual:**
- **Expected:**

## Logs / Screenshots

[Paste relevant log output or attach screenshots. Use code blocks for logs.]

```
(paste logs here)
```

## Additional Notes

[Optional: workarounds, similar issues, etc.]

Guide de contribution

Ouvrir le guide de contribution

Évaluation

Cette issue n'a pas encore été évaluée.

Recevez les nouvelles issues par e-mail

Un résumé court des issues GitHub adaptées aux débutants.