package runtime import ( "strings" "sync" "agent-squad/squad" core "agent-system-core" ) // toolResultSummaryLen 是工具结果摘要的截断长度(与既有展示一致)。 const toolResultSummaryLen = 160 // toolEventID 是宿主侧的工具调用身份:优先用上游 CallID(并行同名工具据此配对, // 而不是按到达顺序);空串 = legacy/未知,退回工具名,保持既有展示不变。 func toolEventID(ev squad.ProcessEvent) string { if id := strings.TrimSpace(ev.CallID); id != "" { return id } return ev.Tool } // toolEventOutcome 把上游 tool_execution_end 翻译成宿主展示的真实结局。 // // 取值域与上游一致(ok | error | denied | canceled),不新增、不改写上游状态: // - Status 非空:原样透传; // - Status 为空且没有失败信号(ErrorReason 也空):legacy/未知,沿用旧的 "ok" // 与 Detail 摘要(字节一致,不臆造结局); // - Status 为空但有 ErrorReason:有失败信号而状态未知,报 error 而不是 ok; // - 非 ok 时摘要取 ErrorReason 原文(截断),上游没给原因才回退 Detail。 func toolEventOutcome(ev squad.ProcessEvent) (status, summary string) { status = strings.TrimSpace(ev.Status) if status == "" { if strings.TrimSpace(ev.ErrorReason) == "" { return squad.ToolStatusOK, truncateRun(ev.Detail, toolResultSummaryLen) } status = squad.ToolStatusError } if status == squad.ToolStatusOK { return status, truncateRun(ev.Detail, toolResultSummaryLen) } if strings.TrimSpace(ev.ErrorReason) != "" { return status, truncateRun(ev.ErrorReason, toolResultSummaryLen) } return status, truncateRun(ev.Detail, toolResultSummaryLen) } // toolFailure 是一次未被恢复的工具失败(用于终局文本一致性检查,不覆盖工具部件)。 type toolFailure struct { Tool string CallID string Status string Summary string } // squadEventLog 是小队过程帧在宿主侧的聚合点。 // // 并行工具调用会从多个 goroutine 回调进来(内核 executeParallel 逐个派发), // 因此流式文本、工具部件、用量与 pending 配对表全部在同一把锁下更新: // 既不丢条目,也不会把 A 调用的 start 与 B 调用的 end 交错写坏。 // 事件发射与部件追加在同一临界区内,保持到达顺序与最终部件顺序一致。 type squadEventLog struct { rc RunCtx mu sync.Mutex streamed strings.Builder textIdx int parts []core.Part pending map[string]string // callID → 工具名(start/end 配对) failure *toolFailure // 最近一次失败;同名工具后续再起即视为已恢复 } // newSquadEventLog 绑定一次运行的过程帧回调。 func newSquadEventLog(rc RunCtx) *squadEventLog { return &squadEventLog{rc: rc, pending: map[string]string{}} } // onEvent 是小队过程帧回调(并发安全)。 func (l *squadEventLog) onEvent(ev squad.ProcessEvent) { l.mu.Lock() defer l.mu.Unlock() switch ev.Type { case "text_delta": if ev.Text == "" { return } l.streamed.WriteString(ev.Text) _ = l.rc.Emit(core.EvMessageDelta, core.MessageDeltaPayload{Index: l.textIdx, Text: ev.Text}) case "reasoning_delta": if ev.Text != "" { _ = l.rc.Emit(core.EvThinkingDelta, core.ThinkingDeltaPayload{Text: ev.Text}) } case "tool_execution_start": id := toolEventID(ev) if id != "" { l.pending[id] = ev.Tool } l.noteRecovered(ev.Tool) _ = l.rc.Emit(core.EvToolCall, core.ToolCallPayload{CallID: id, Name: ev.Tool, Arguments: ev.Detail}) l.parts = append(l.parts, core.Part{Type: core.PartToolCall, Process: true, ToolCall: &core.ToolCallPart{ID: id, Name: ev.Tool, Arguments: ev.Detail}}) case "tool_execution_end": id := toolEventID(ev) if tool, ok := l.pending[id]; ok && ev.Tool == "" { ev.Tool = tool } delete(l.pending, id) status, summary := toolEventOutcome(ev) // 发射与部件同源:前端过程行与落库部件说的是同一件事。 _ = l.rc.Emit(core.EvToolResult, core.ToolResultPayload{CallID: id, Status: status, Summary: summary}) l.parts = append(l.parts, core.Part{Type: core.PartToolResult, Process: true, ToolResult: &core.ToolResultPart{CallID: id, Status: status, Summary: summary}}) if status == squad.ToolStatusOK { l.noteRecovered(ev.Tool) } else { l.failure = &toolFailure{Tool: ev.Tool, CallID: id, Status: status, Summary: summary} } case "usage": if sink, ok := l.rc.(interface{ AddUsage(core.Usage) }); ok { sink.AddUsage(core.Usage{ PromptTokens: ev.PromptTokens, CompletionTokens: ev.CompletionTokens, TotalTokens: ev.PromptTokens + ev.CompletionTokens, LastPromptTokens: ev.LastPromptTokens, LastCompletionTokens: ev.LastCompletionTokens, CacheReadTokens: ev.CacheReadTokens, CacheWriteTokens: ev.CacheWriteTokens, }) } } } // noteRecovered 把同名工具的后续进展(再次发起或调用成功)记为已恢复: // 失败部件仍留在过程部件里,只是不再让收尾文本替它宣布成功。 func (l *squadEventLog) noteRecovered(tool string) { if l.failure != nil && l.failure.Tool == tool { l.failure = nil } } // reply 是本次运行聚合到的模型文本(不含任何宿主补写)。 func (l *squadEventLog) reply() string { l.mu.Lock() defer l.mu.Unlock() return l.streamed.String() } // parts 返回过程部件的快照(复制,避免把内部切片交给最终部件)。 func (l *squadEventLog) toolParts() []core.Part { l.mu.Lock() defer l.mu.Unlock() return append([]core.Part(nil), l.parts...) } // unresolvedFailure 返回终局时仍未恢复的工具失败,没有则 nil。 func (l *squadEventLog) unresolvedFailure() *toolFailure { l.mu.Lock() defer l.mu.Unlock() if l.failure == nil { return nil } f := *l.failure return &f } // finalReply 决定终局文本。模型文本原样保留(不代写、不改写);只有宿主兜底的 // 「已完成。」在仍有未恢复的工具失败时会被换成如实说明,避免收尾文本与失败部件 // 自相矛盾。legacy 事件(无 CallID/Status)走不到这里,展示与旧行为一致。 func (l *squadEventLog) finalReply(modelText, summary, fallback string) string { reply := strings.TrimSpace(modelText) if reply == "" { reply = strings.TrimSpace(summary) } if reply != "" { return reply } if f := l.unresolvedFailure(); f != nil { return failedToolLine(f) } return fallback } // failedToolLine 如实描述未恢复的工具失败,不声称成功,也不编造原因。 func failedToolLine(f *toolFailure) string { name := strings.TrimSpace(f.Tool) if name == "" { name = "工具" } reason := strings.TrimSpace(f.Summary) if reason == "" { return name + " 未完成(" + f.Status + ")" } return name + " 未完成(" + f.Status + "):" + reason }