mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-21 09:28:14 +00:00
【根因】内核流式解析层丢弃了上游 SSE 分片的 OpenAI index 字段:
- openAIToolCall 结构体无 index 字段,JSON 解析即丢
- homed 的 openai.lua 转换为扁平结构时同样未透传 index
- accumulateStream 退而用 Go range slice 序号做累积桶 key,
但每个 SSE chunk 只含一个 tool_call 元素,序号恒为 0
于是并行多工具调用(index=0,1,2,3)的所有分片全部写入同一个桶:
name 相互覆盖、args 碎片混拼成非法 JSON → parseToolArgsJSON
失败返回空 map → 工具以空参数被调用(spawn_child 报'请提供 task'、
cmd_run 报'command is required'等),agent 只能串行重试自愈。
单工具场景只有一个 index 无污染,故简单请求一直正常;
pi 直连同一 llmsproxy 正常(其实现标准按 index 累积)。
【修复】
- ToolCall 增加 StreamIndex(json:stream_index),openAIToolCall
解析上游 index 并透传;openai.lua 输出 stream_index 字段
- accumulateStream 以 tc.StreamIndex 为累积 key
- flushToolCall 区分三种空参:未收到分片/碎片非合法 JSON/合法空
对象({}),分别打诊断日志,避免误报
- 回归测试 TestAccumulateStreamParallelToolCallsByIndex 模拟
4 路并行分片流验证按 index 正确分组与参数完整性
另含 spawn_child max_turns 参数、child_result 运行中状态区分、
provider 层非流式空参诊断日志。
200 lines
5.7 KiB
Go
200 lines
5.7 KiB
Go
package core
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
"log"
|
||
"strings"
|
||
|
||
agentAPI "gitcode.com/JianFeeeee/HomeAgent/internal/agent/api"
|
||
)
|
||
|
||
func (a *Agent) executeSpawnChild(tc agentAPI.ToolCall) string {
|
||
task, _ := tc.Arguments["task"].(string)
|
||
if task == "" {
|
||
if b, _ := json.Marshal(tc.Arguments); len(b) > 2 {
|
||
log.Printf("[spawn] task empty but arguments present: %s", truncateStr(string(b), 300))
|
||
}
|
||
return "请提供 task 参数"
|
||
}
|
||
maxTurns := 0
|
||
if v, ok := tc.Arguments["max_turns"].(float64); ok {
|
||
maxTurns = int(v)
|
||
}
|
||
if maxTurns < 1 {
|
||
maxTurns = defaultChildMaxTurns
|
||
}
|
||
if maxTurns > 30 {
|
||
maxTurns = 30
|
||
}
|
||
|
||
a.childMu.Lock()
|
||
a.childNextID++
|
||
taskID := fmt.Sprintf("child_%d", a.childNextID)
|
||
a.childMu.Unlock()
|
||
|
||
// 捕获父 Agent 当前输出通道:子任务完成通知需回到发起对话的通道,
|
||
// 让父 Agent 正常感知并可回复用户(而非走无记忆整理路径丢失通知)。
|
||
parentChannel := a.currentOutputChannel
|
||
if parentChannel == "" || parentChannel == channelConsolidation {
|
||
parentChannel = "cli"
|
||
}
|
||
|
||
a.childMu.Lock()
|
||
a.childRunning[taskID] = true
|
||
a.childMu.Unlock()
|
||
go a.runChildTask(taskID, task, parentChannel, maxTurns)
|
||
|
||
return fmt.Sprintf("子任务已启动(ID: %s,最多 %d 轮),完成后会自动通知你,届时请使用 child_result 工具查看输出", taskID, maxTurns)
|
||
}
|
||
|
||
// defaultChildMaxTurns 子 Agent 默认工具轮数(可被 spawn_child 的 max_turns 参数覆盖)。
|
||
const defaultChildMaxTurns = 5
|
||
|
||
func (a *Agent) runChildTask(taskID, task string, parentChannel string, maxTurns int) {
|
||
if a.provider == nil {
|
||
log.Printf("[child] %s failed: no LLM provider configured", taskID)
|
||
return
|
||
}
|
||
log.Printf("[child] %s started: %s", taskID, truncateStr(task, 80))
|
||
|
||
sysPrompt := fmt.Sprintf(`你是 HomeAgent 的子任务助手。
|
||
请完成以下任务。完成即可,无需保留记忆或查询历史。
|
||
任务: %s`, task)
|
||
|
||
msgs := []agentAPI.Message{
|
||
{Role: "system", Content: sysPrompt},
|
||
{Role: "user", Content: task},
|
||
}
|
||
|
||
allTools := a.buildToolDefs()
|
||
childTools := make([]interface{}, 0, len(allTools))
|
||
for _, t := range allTools {
|
||
toolMap, ok := t.(map[string]interface{})
|
||
if !ok {
|
||
continue
|
||
}
|
||
fn, ok := toolMap["function"].(map[string]interface{})
|
||
if !ok {
|
||
continue
|
||
}
|
||
name, _ := fn["name"].(string)
|
||
if strings.HasPrefix(name, "output_send__") || name == "output_list_channels" || name == "spawn_child" || name == "plgreload" {
|
||
continue
|
||
}
|
||
childTools = append(childTools, t)
|
||
}
|
||
|
||
var finalResult string
|
||
for turn := 0; turn < maxTurns; turn++ {
|
||
req := &agentAPI.CompletionRequest{
|
||
Messages: msgs,
|
||
MaxTokens: 4096,
|
||
Tools: childTools,
|
||
ToolChoice: "auto",
|
||
DisableThinking: !a.thinkingEnabled,
|
||
}
|
||
|
||
resp, err := a.provider.Chat(a.ctx, req)
|
||
if err != nil {
|
||
finalResult = fmt.Sprintf("子 Agent 执行失败: %v", err)
|
||
break
|
||
}
|
||
|
||
if len(resp.ToolCalls) == 0 {
|
||
finalResult = resp.Content
|
||
break
|
||
}
|
||
|
||
for _, ct := range resp.ToolCalls {
|
||
var result string
|
||
switch {
|
||
case strings.HasPrefix(ct.Name, "output_send__") || ct.Name == "output_list_channels":
|
||
result = fmt.Sprintf("子 Agent 不允许调用输出工具: %s", ct.Name)
|
||
case ct.Name == "spawn_child" || ct.Name == "plgreload":
|
||
result = fmt.Sprintf("子 Agent 不允许调用系统工具: %s", ct.Name)
|
||
default:
|
||
result = a.executeToolCall(ct)
|
||
}
|
||
msgs = append(msgs, agentAPI.Message{Role: "assistant", Content: resp.Content, ToolCalls: []agentAPI.ToolCall{ct}})
|
||
msgs = append(msgs, agentAPI.Message{Role: "tool", ToolCallID: ct.ID, Content: result})
|
||
}
|
||
}
|
||
|
||
if finalResult == "" {
|
||
finalResult = "子 Agent 执行超时(超过 5 轮)"
|
||
}
|
||
|
||
a.childMu.Lock()
|
||
a.childResults[taskID] = finalResult
|
||
delete(a.childRunning, taskID)
|
||
a.childMu.Unlock()
|
||
|
||
log.Printf("[child] %s done: %s", taskID, truncateStr(finalResult, 100))
|
||
|
||
notification := fmt.Sprintf("子任务 %s 已完成,请调用 child_result 工具查看输出", taskID)
|
||
a.injectSelfChannel(selfInputMsg{
|
||
text: notification,
|
||
channel: parentChannel, // 回到父对话通道,正常处理(写入上下文 + emit 响应)
|
||
})
|
||
}
|
||
|
||
func (a *Agent) executeChildResultTool(tc agentAPI.ToolCall) string {
|
||
taskID, _ := tc.Arguments["task_id"].(string)
|
||
if taskID == "" {
|
||
return "请提供 task_id 参数"
|
||
}
|
||
|
||
a.childMu.Lock()
|
||
result, ok := a.childResults[taskID]
|
||
if ok {
|
||
delete(a.childResults, taskID)
|
||
a.childMu.Unlock()
|
||
return fmt.Sprintf("【子任务 %s 结果】\n%s", taskID, result)
|
||
}
|
||
if a.childRunning[taskID] {
|
||
a.childMu.Unlock()
|
||
return fmt.Sprintf("子任务 %s 仍在运行中,尚未完成。请等待完成通知后再查询。", taskID)
|
||
}
|
||
a.childMu.Unlock()
|
||
return fmt.Sprintf("子任务 %s 不存在或已过期", taskID)
|
||
}
|
||
|
||
func (a *Agent) executeLLMTool(tc agentAPI.ToolCall) string {
|
||
if a.providerManager == nil {
|
||
return "LLM 源管理器不可用"
|
||
}
|
||
switch tc.Name {
|
||
case "llm_list_sources":
|
||
sources := a.providerManager.List()
|
||
if len(sources) == 0 {
|
||
return "没有可用的 LLM 源"
|
||
}
|
||
parts := []string{"可用 LLM 源:"}
|
||
for _, name := range sources {
|
||
mark := " "
|
||
if p := a.providerManager.Get(""); p != nil && p.Name() == name {
|
||
mark = "→"
|
||
}
|
||
parts = append(parts, fmt.Sprintf(" %s %s", mark, name))
|
||
}
|
||
return strings.Join(parts, "\n")
|
||
|
||
case "llm_set_source":
|
||
name, _ := tc.Arguments["name"].(string)
|
||
if name == "" {
|
||
return "请提供源名称"
|
||
}
|
||
if err := a.providerManager.SetDefault(name); err != nil {
|
||
return fmt.Sprintf("切换失败: %v", err)
|
||
}
|
||
a.provider = a.providerManager.Get(name)
|
||
return fmt.Sprintf("已切换到 LLM 源: %s", name)
|
||
|
||
default:
|
||
return fmt.Sprintf("未知的 LLM 工具: %s", tc.Name)
|
||
}
|
||
}
|
||
|
||
|