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
22 changes: 12 additions & 10 deletions docs/buglist.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
| BUG-013 | P1 | ✅ 已修复 | 刷新页面后 AI 不记得对话历史(sessionId 未持久化) |
| BUG-014 | P2 | ✅ 已修复 | ContextPicker 抢焦点,按 `@` 后无法在主输入框继续输入 |
| BUG-015 | P2 | ✅ 已修复 | @设计/@测试 等 Profile 标签被 ContextPicker 拦截,无法路由到后端 |
| BUG-016 | P2 | ⏸ 挂起 | Agentic loop 8 轮耗尽后 AI 无文字输出(safety net 未生效) |
| BUG-016 | P2 | ✅ 已修复 | Agentic loop 轮数耗尽后 AI 无文字输出(safety net 增强 + fallback 机制) |
| BUG-017 | P1 | ✅ 已修复 | Knowledge Services 页面白屏崩溃(ServiceType/ServiceStatus 枚举大小写) |
| BUG-018 | P2 | ✅ 已修复 | Context Picker Knowledge tab 无内容(空字符串未 fallback 到通配搜索) |
| BUG-019 | P2 | ✅ 已修复 | 代码块 Apply/Copy 按钮不可见(CSS opacity-0 隐藏) |
Expand Down Expand Up @@ -153,15 +153,17 @@
2. `AiChatSidebar.tsx`: `handleContextSelect` 判断 `item.type === "profile"` 时,将 `@标签名 ` 插入到输入框文本前缀,而非添加为 context attachment。这样 `@设计` 等标签保留在消息文本中,后端 ProfileRouter 可以正常检测
- **文件**: `ContextPicker.tsx`, `AiChatSidebar.tsx`

### BUG-016: Agentic loop 耗尽后无文字输出(⏸ 挂起
### BUG-016: Agentic loop 耗尽后无文字输出(✅ 已修复
- **发现**: Session 16, Phase 1.6 验收测试 TC-2.3
- **症状**: `@设计 帮我看下这个系统的架构`,AI 调了 8 轮工具(workspace_list_files, workspace_read_file, search_knowledge 等),全部 stopReason=TOOL_USE,最终无文字总结输出
- **已尝试修复**:
1. MAX_AGENTIC_TURNS 5 → 8(仍不够)
2. Safety net:轮数耗尽后注入 user message + 无工具最终轮 → 产出 0 chars,未生效
- **根因推测**: Claude API 在长工具链后的空工具调用可能不产生 ContentDelta;或 conversation 过长导致模型输出为空
- **状态**: ⏸ 挂起,后续排查
- **文件**: `ClaudeAgentService.kt`
- **根因**: 原 safety net 触发条件为 `finalContent.isBlank()`,但:
1. 当 `stopReason == TOOL_USE` 但 `currentToolUses` 为空时,循环直接 break,safety net 没机会检查
2. Summary turn 本身可能因对话历史过长而不产生 ContentDelta
- **修复**:
1. 扩展 safety net 触发条件:`finalContent.isBlank() && allToolCalls.isNotEmpty()` 或 `turn > MAX_AGENTIC_TURNS`
2. 增强 summary prompt:包含工具调用摘要和明确的指令
3. 添加 fallback 机制:若 summary turn 仍为空,基于 `allToolCalls` 生成合成回复
- **文件**: `web-ide/backend/src/main/kotlin/com/forge/webide/service/ClaudeAgentService.kt`

### BUG-017: Knowledge Services 页面白屏崩溃
- **发现**: Session 16, Phase 1.6 验收测试 TC-5.4
Expand Down Expand Up @@ -202,8 +204,8 @@
## 统计

- **总计**: 20 个 Bug
- **已修复**: 19
- **挂起**: 1 个 (BUG-016)
- **已修复**: 20
- **挂起**: 0 个
- **P0 (阻塞)**: 2 个 (BUG-008, BUG-012)
- **P1 (严重)**: 4 个 (BUG-001, BUG-005, BUG-013, BUG-017)
- **P2 (一般)**: 14 个
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -427,16 +427,19 @@ class ClaudeAgentService(
break
}

// Safety net: if all turns exhausted and AI still wanted to use tools,
// Safety net: if all turns exhausted or AI didn't produce text output,
// inject a user message requesting summary and do one final turn WITHOUT tools
if (finalContent.isBlank()) {
logger.info("All $MAX_AGENTIC_TURNS turns exhausted with tool_use. Forcing final summary turn.")
val needsSummaryTurn = finalContent.isBlank() && allToolCalls.isNotEmpty()
if (needsSummaryTurn || turn > MAX_AGENTIC_TURNS) {
Copy link
Contributor

Choose a reason for hiding this comment

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

critical

这里存在一个严重的编译错误:变量 turn 是在 for 循环内部定义的(第 255 行),在循环外部无法访问。此外,由于循环范围是 1..MAX_AGENTIC_TURNS,条件 turn > MAX_AGENTIC_TURNS 在逻辑上也是永远不会成立的。建议在循环外部定义一个变量来记录是否达到了最大轮数,或者检查最后的 stopReason 是否仍为 TOOL_USE

val reason = if (finalContent.isBlank()) "AI produced no text output" else "Max turns exhausted"
logger.info("Safety net triggered: $reason. Forcing final summary turn. allToolCalls=${allToolCalls.size}")
onEvent(mapOf("type" to "ooda_phase", "phase" to "complete"))
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

在这里发送 complete 状态事件可能过早。Safety net 触发后,系统还会进行一轮总结(Summary turn),此时任务并未真正完成。streamMessage 方法(第 208 行)在 agenticStream 返回后会统一发送 complete 事件,建议移除此处的冗余调用,以免 UI 提前进入完成状态导致显示异常。


// Add a user message to explicitly instruct the AI to summarize
// Enhanced prompt with multiple fallback options
currentMessages.add(Message(
role = Message.Role.USER,
content = "你已经收集了足够的信息。请基于以上工具调用的结果,直接给出完整、详细的回复。不要再调用任何工具。"
content = buildSummaryPrompt(allToolCalls)
))

var summaryText = StringBuilder()
Expand All @@ -451,6 +454,13 @@ class ClaudeAgentService(
}
finalContent = summaryText.toString()
logger.debug("Summary turn produced ${finalContent.length} chars")

// If still no content, generate a fallback response based on tool calls
if (finalContent.isBlank()) {
logger.warn("Summary turn produced no content. Generating fallback response based on ${allToolCalls.size} tool calls.")
finalContent = generateFallbackFromToolCalls(allToolCalls)
onEvent(mapOf("type" to "content", "content" to finalContent))
}
}

return AgenticResult(content = finalContent, toolCalls = allToolCalls)
Expand Down Expand Up @@ -584,6 +594,61 @@ class ClaudeAgentService(
""".trimMargin()
}

/**
* Build an enhanced summary prompt for the safety net turn.
*
* The prompt is designed to:
* 1. Force the AI to produce text output
* 2. Provide clear instructions for summarizing tool results
* 3. Offer fallback options if summarization is not possible
*/
private fun buildSummaryPrompt(toolCalls: List<ToolCallRecord>): String {
val toolNames = toolCalls.map { it.name }.distinct().joinToString(", ")
val toolCount = toolCalls.size

return """
|你已经完成了 $toolCount 次工具调用,使用的工具包括:$toolNames。
|
|请基于工具调用的结果,为用户提供一个完整、详细的回复。回复应该:
|1. 总结你通过工具发现或获取的关键信息
|2. 直接回答用户的问题,而不是描述你做了什么
|3. 如果信息不完整,明确说明你能确认的和不能确认的部分
|
|如果无法基于工具调用结果给出回复,至少提供一个基于你的理解的合理回答。
|
|请现在开始给出你的回复:
""".trimMargin()
}
Comment on lines +605 to +621
Copy link
Contributor

Choose a reason for hiding this comment

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

security-high high

The buildSummaryPrompt function constructs a new prompt that includes tool names originating from the LLM's output without any sanitization. This creates a prompt injection vulnerability. If an attacker can influence the LLM to generate a tool name containing malicious instructions, they can hijack the agent's behavior. To fix this, you must validate the tool names against a strict allow-list of known tools before embedding them into the prompt.


/**
* Generate a fallback response based on tool call results.
*
* This is used when the safety net summary turn also produces no content,
* which can happen in edge cases with long conversation histories.
*/
private fun generateFallbackFromToolCalls(toolCalls: List<ToolCallRecord>): String {
if (toolCalls.isEmpty()) {
return "已完成工具调用,但未能生成回复。"
}

val toolSummaries = toolCalls.map { call ->
val status = when (call.status) {
"complete" -> "成功"
"error" -> "失败"
else -> call.status
}
"- ${call.name}: $status"
}.joinToString("\n")
Comment on lines +634 to +641
Copy link
Contributor

Choose a reason for hiding this comment

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

security-high high

The generateFallbackFromToolCalls function includes raw tool names, which originate from the LLM, in a fallback message sent to the client. If the client application renders this message as HTML, it can lead to a Cross-Site Scripting (XSS) vulnerability. An attacker could trick the LLM into generating a tool name with a JavaScript payload, which would then execute in the user's browser. To remediate this, you must HTML-escape the tool names before including them in the response.

        val toolSummaries = toolCalls.map { call ->
            val status = when (call.status) {
                "complete" -> "成功"
                "error" -> "失败"
                else -> call.status
            }
            val safeName = call.name
                .replace("&", "&amp;")
                .replace("<", "&lt;")
                .replace(">", "&gt;")
                .replace("\"", "&quot;")
                .replace("'", "&#39;")
            "- ${safeName}: $status"
        }.joinToString("\n")


return """
|根据工具调用结果:
|
|$toolSummaries
|
|已执行完成所有操作。如果需要进一步分析或有其他问题,请重新提问。
""".trimMargin()
}

private fun collectStreamResult(events: List<StreamEvent>): AgenticResult {
val text = StringBuilder()
val toolCalls = mutableListOf<ToolCallRecord>()
Expand Down
Loading