Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,6 @@ CLAUDE.md

# Build cache
.cache/ # Includes conda_unpack_wheels/ for Windows packaging workaround

# Hypothesis test cache
.hypothesis/
1 change: 1 addition & 0 deletions .kiro/specs/chat-interrupted-cannot-continue/.config.kiro
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"specId": "3a8ced8a-8a8b-41f2-9e3d-dc4d6f3f588c", "workflowType": "requirements-first", "specType": "bugfix"}
43 changes: 43 additions & 0 deletions .kiro/specs/chat-interrupted-cannot-continue/bugfix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Bugfix 需求文档

## 简介

在 Web 聊天界面中,当 LLM 正在执行工具调用(如 `execute_shell_command`)时,用户点击"中断"(Stop)按钮后,聊天会话进入不可恢复的状态:界面显示"Answers have stopped",且用户无法再发送新消息继续对话。此 bug 严重影响用户体验,因为用户必须刷新页面或创建新会话才能继续使用聊天功能。

根本原因涉及前后端两侧:

1. **`api.cancel` 回调是空操作**:在 `console/src/pages/Chat/index.tsx` 中,`api.cancel` 仅执行 `console.log(data)` 而未向后端发送取消请求。当 `@agentscope-ai/chat` 库的中断逻辑调用此回调时,后端 agent 进程不会收到任何取消信号,继续执行工具调用并向 SSE 流写入数据。

2. **前端 SSE 流中断但后端未同步**:虽然 `customFetch` 正确地将 `data.signal`(AbortSignal)传递给了 `fetch` 请求,前端可以通过 `AbortController.abort()` 中断 HTTP 连接。但后端的 `AgentRunner.query_handler` 只有在 asyncio task 被显式取消时才会触发 `CancelledError` 处理逻辑(调用 `agent.interrupt()`)。仅关闭 HTTP 连接可能不足以立即取消后端的 asyncio task,特别是当 agent 正在执行长时间运行的工具(如 shell 命令)时。

3. **中断后会话状态不一致**:前端将最后一条响应消息的 `msgStatus` 设为 `'interrupted'`,但后端不知道这一状态变化。当用户尝试发送新消息时,`@agentscope-ai/chat` 库内部的状态管理可能因为上一轮未正确完成的请求而阻止新消息的提交,导致持续显示"Answers have stopped"。

## Bug 分析

### 当前行为(缺陷)

1.1 WHEN 用户在 LLM 正在执行工具调用(如 execute_shell_command)时点击中断按钮 THEN 系统仅在前端将响应标记为 'interrupted' 并显示"Answers have stopped",但 `api.cancel` 回调仅执行 `console.log` 而未向后端发送取消请求,后端 agent 进程继续执行

1.2 WHEN 用户在中断后尝试发送新消息 THEN 系统无法正常提交新消息,界面持续显示"Answers have stopped"错误,聊天会话处于不可用状态

1.3 WHEN 后端 agent 正在执行长时间运行的工具调用且前端中断了 SSE 连接 THEN 系统的后端 agent 进程未被及时终止,继续占用资源执行已被用户取消的任务

### 期望行为(正确)

2.1 WHEN 用户在 LLM 正在执行工具调用时点击中断按钮 THEN 系统 SHALL 通过 `api.cancel` 回调向后端发送取消请求(取消对应 session_id 的 agent 处理任务),同时中断前端的 SSE 流读取

2.2 WHEN 用户在中断后尝试发送新消息 THEN 系统 SHALL 允许用户正常发送新消息并获得 LLM 响应,聊天会话恢复到可用状态

2.3 WHEN 后端收到取消请求或检测到 SSE 连接断开 THEN 系统 SHALL 终止正在执行的 agent 进程(包括正在运行的工具调用),释放相关资源,并正确保存当前会话状态

### 不变行为(回归预防)

3.1 WHEN LLM 正常完成响应(未被中断)THEN 系统 SHALL CONTINUE TO 正确显示完整的响应内容,消息状态为 'finished'

3.2 WHEN 用户在非工具调用期间(如普通文本生成)正常对话 THEN 系统 SHALL CONTINUE TO 正常处理消息的发送和接收

3.3 WHEN 用户切换会话或创建新会话 THEN 系统 SHALL CONTINUE TO 正确加载聊天历史并解析会话 ID

3.4 WHEN 多个并发请求发生时 THEN 系统 SHALL CONTINUE TO 正确去重请求并保留 realId 映射关系

3.5 WHEN 后端 agent 因其他原因(如异常)终止时 THEN 系统 SHALL CONTINUE TO 正确保存会话状态并允许用户继续对话
335 changes: 335 additions & 0 deletions .kiro/specs/chat-interrupted-cannot-continue/design.md

Large diffs are not rendered by default.

89 changes: 89 additions & 0 deletions .kiro/specs/chat-interrupted-cannot-continue/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# 实施计划

- [x] 1. 编写 Bug 条件探索测试
- **Property 1: Bug Condition** - 中断操作未通知后端且会话不可恢复
- **重要**: 此属性测试必须在实施修复之前编写
- **目标**: 发现能证明 bug 存在的反例
- **Scoped PBT 方法**: 将属性范围限定到具体的失败场景 — 用户在 LLM 执行工具调用期间点击中断按钮,`api.cancel` 仅执行 `console.log` 而未向后端发送取消请求
- Bug 条件: `isBugCondition(input)` — `input.action === 'cancel_button_clicked' AND input.agentState IN ['executing_tool', 'streaming_response'] AND api.cancel IS noop`
- 测试 1(前端): 调用 `api.cancel({ session_id })` 后,验证是否向后端 `/api/agent/cancel` 发送了 HTTP POST 请求(在未修复代码上将失败 — 仅执行 console.log)
- 测试 2(后端): 向 `/api/agent/cancel` 发送 POST 请求,验证端点存在且返回正确响应(在未修复代码上将返回 404)
- 测试 3(后端): 模拟一个正在运行的 asyncio task,调用取消端点后验证 task 被 cancel(在未修复代码上无法测试 — 端点不存在)
- 在未修复代码上运行测试 — 预期测试失败(确认 bug 存在)
- 记录发现的反例(例如: "`api.cancel` 调用后没有 HTTP 请求发出"、"后端 `/api/agent/cancel` 返回 404")
- 任务完成标准: 测试已编写、已运行、失败已记录
- _Requirements: 1.1, 1.2, 1.3, 2.1, 2.3_

- [x] 2. 编写 Preservation 属性测试(在实施修复之前)
- **Property 2: Preservation** - 非中断场景的行为不变
- **重要**: 遵循观察优先方法论
- 观察: 在未修复代码上,LLM 正常完成响应时 `customFetch` 正确处理 SSE 流,消息状态为 `'finished'`
- 观察: 在未修复代码上,`customFetch` 正确将 `data.signal`(AbortSignal)传递给 `fetch` 请求
- 观察: 在未修复代码上,会话切换时聊天历史正确加载并解析会话 ID
- 观察: 在未修复代码上,多个并发请求时去重机制和 `realId` 映射正常工作
- 观察: 在未修复代码上,后端 `query_handler` 在正常完成时正确保存会话状态并通过 `finally` 块执行清理
- 编写属性测试: 对于所有不满足 bug 条件的输入(非中断场景),验证正常响应完成、会话管理、并发请求处理、异常处理等行为与原始系统一致
- 在未修复代码上运行测试 — 预期测试通过(确认基线行为)
- 任务完成标准: 测试已编写、已运行、在未修复代码上通过
- _Requirements: 3.1, 3.2, 3.3, 3.4, 3.5_

- [x] 3. 修复聊天中断后无法继续对话 bug

- [x] 3.1 实现前端 `api.cancel` 回调(`console/src/pages/Chat/index.tsx`)
- 将 `api.cancel` 中的 `console.log(data)` 替换为向后端发送 POST 请求
- 请求目标: `getApiUrl("/agent/cancel")`,方法: POST
- 请求体: `{ session_id: data.session_id }`
- 请求头: 包含 `Content-Type: application/json` 和认证 token(通过 `getApiToken()` 获取)
- 使用 `.catch()` 捕获请求失败,避免阻塞前端中断流程
- _Bug_Condition: api.cancel 是空操作,仅执行 console.log 而未通知后端_
- _Expected_Behavior: api.cancel 调用后向后端 /api/agent/cancel 发送 POST 请求_
- _Preservation: customFetch 中的 AbortSignal 传递机制保持不变_
- _Requirements: 2.1_

- [x] 3.2 新增后端取消端点(`src/copaw/app/routers/agent.py`)
- 新增 `CancelRequest` Pydantic model,包含 `session_id: str` 字段
- 新增 `POST /cancel` 端点 `cancel_agent_task`
- 从 `request.app.state.agent_app` 获取 `AgentApp` 实例
- 访问 `agent_app._local_tasks` 查找对应 session_id 的 asyncio task
- 对匹配的未完成 task 调用 `task.cancel()`
- 返回 `{ cancelled: bool }` 响应
- _Bug_Condition: 后端缺少取消端点,无法接收前端的取消请求_
- _Expected_Behavior: 后端收到取消请求后终止对应 session 的 agent 进程_
- _Preservation: 现有 /agent/process 等端点行为不变_
- _Requirements: 2.3_

- [x] 3.3 暴露 agent_app 到 app.state(`src/copaw/app/_app.py`)
- 在 lifespan 函数中,`yield` 之前添加 `app.state.agent_app = agent_app`
- 使取消端点可以通过 `request.app.state.agent_app` 访问 `_local_tasks`
- _Bug_Condition: 取消端点无法访问 AgentApp 实例和 _local_tasks_
- _Expected_Behavior: agent_app 实例可通过 app.state 访问_
- _Preservation: 现有 lifespan 逻辑(runner 初始化、清理等)保持不变_
- _Requirements: 2.3_

- [x] 3.4 优化 CancelledError 处理(`src/copaw/app/runner/runner.py`)
- 在 `query_handler` 的 `asyncio.CancelledError` 异常处理中,将 `raise RuntimeError("Task has been cancelled!") from exc` 替换为 `raise`(重新抛出 CancelledError)
- 确保 `finally` 块中的 `save_session_state` 正常执行
- 让框架正确识别任务是被取消而非出错
- _Bug_Condition: CancelledError 被转换为 RuntimeError,可能影响上层框架的取消处理逻辑_
- _Expected_Behavior: CancelledError 被正确传播,finally 块保存会话状态_
- _Preservation: 正常完成和其他异常的处理逻辑保持不变_
- _Requirements: 2.3, 3.5_

- [x] 3.5 验证 Bug 条件探索测试现在通过
- **Property 1: Expected Behavior** - 中断操作应终止后端 agent 并恢复会话可用性
- **重要**: 重新运行任务 1 中的同一测试,不要编写新测试
- 任务 1 中的测试编码了期望行为:`api.cancel` 向后端发送取消请求,后端终止对应 task
- 当此测试通过时,确认期望行为已满足
- 运行 Bug 条件探索测试
- **预期结果**: 测试通过(确认 bug 已修复)
- _Requirements: 2.1, 2.2, 2.3_

- [x] 3.6 验证 Preservation 测试仍然通过
- **Property 2: Preservation** - 非中断场景的行为不变
- **重要**: 重新运行任务 2 中的同一测试,不要编写新测试
- 运行 Preservation 属性测试
- **预期结果**: 测试通过(确认无回归)
- 确认修复后所有测试仍然通过

- [x] 4. 检查点 - 确保所有测试通过
- 确保所有测试通过,如有问题请咨询用户。
1 change: 1 addition & 0 deletions .kiro/specs/chat-session-messages-lost/.config.kiro
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"specId": "3a8ced8a-8a8b-41f2-9e3d-dc4d6f3f588c", "workflowType": "requirements-first", "specType": "bugfix"}
37 changes: 37 additions & 0 deletions .kiro/specs/chat-session-messages-lost/bugfix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Bugfix 需求文档

## 简介

Web聊天界面中,当用户在LLM正在处理请求(特别是工具调用期间)时切换到其他页面(如MCP、模型配置等),再返回聊天页面后,之前正在进行的聊天消息丢失,且刷新页面也无法恢复。

根本原因分析:`SessionApi.updateSession` 方法在第一行执行了 `session.messages = []`,强制清空了传入会话对象的消息数组。当UI组件在流式响应期间调用 `updateSession` 保存会话状态时,消息被清空并覆盖到 `sessionList` 中。同时,由于流式响应被中断(用户离开页面),后端可能也未完整保存该轮对话的消息,导致消息永久丢失。

## Bug 分析

### 当前行为(缺陷)

1.1 WHEN 用户在LLM正在进行工具调用/流式响应时切换到其他页面再返回 THEN 系统显示的聊天消息不完整,正在进行的对话轮次的消息丢失

1.2 WHEN `updateSession` 被调用时 THEN 系统强制将 `session.messages` 设为空数组 `[]`,导致内存中 `sessionList` 对应会话的消息被清空

1.3 WHEN 用户在消息丢失后刷新页面 THEN 系统无法恢复丢失的消息,因为后端在流式响应中断时也未完整保存该轮对话

### 期望行为(正确)

2.1 WHEN 用户在LLM正在进行工具调用/流式响应时切换到其他页面再返回 THEN 系统 SHALL 显示切换前已接收到的所有聊天消息,包括部分完成的响应

2.2 WHEN `updateSession` 被调用时 THEN 系统 SHALL 不清空已有的消息数据,仅更新会话的元数据(如名称、ID映射等)

2.3 WHEN 用户返回聊天页面时 THEN 系统 SHALL 从后端重新获取该会话的完整聊天历史,确保显示所有已持久化的消息

### 不变行为(回归预防)

3.1 WHEN 用户在非流式响应期间正常切换会话 THEN 系统 SHALL CONTINUE TO 正确加载目标会话的聊天历史

3.2 WHEN 用户创建新会话并发送第一条消息 THEN 系统 SHALL CONTINUE TO 正确解析临时时间戳ID到后端真实UUID

3.3 WHEN 用户删除会话 THEN 系统 SHALL CONTINUE TO 正确从列表中移除会话并清理URL

3.4 WHEN 多个并发的 `getSessionList` 或 `getSession` 请求发生时 THEN 系统 SHALL CONTINUE TO 正确去重请求,保留 `realId` 映射关系

3.5 WHEN `updateSession` 被调用更新会话元数据时 THEN 系统 SHALL CONTINUE TO 正确触发 `realId` 解析流程(对于本地时间戳ID的会话)
Loading