package loop import ( "sort" "time" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/agent" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/trace" ) // 记录在 span 上的日志事件类型,使 flow 图的节点 log[] 展示 // 每一步发生了什么(design/19 §1 节点实时日志)。仅当该步真正 // 完成时才追加事件,因此日志绝不宣告未运行的工作。 const ( // EventLLMDone 在模型调用返回后记录到 llm_call span 上。 EventLLMDone = "llm.done" // EventToolDone 在工具调用落定后记录到 tool_call span 上。 EventToolDone = "tool.done" ) // maxLogSummaryRunes 在发布前限制日志取值的长度。图在 // 输出时还会再次脱敏与截断;这里只是让单个 span 保持小。 const maxLogSummaryRunes = 240 // recordLLMDone 把一次已完成模型调用的脱敏摘要追加到其 // span:服务该 turn 的模型、两个 stop reason、请求了多少工具调用 // 以及该调用耗时。不记录任何提示词或补全文本, // 因此 llm 节点的日志绝不携带对话内容。ag 是该 turn 的 agent // (按角色池条目),因此多角色进程上报的是实际 // 服务该调用的模型,而不是默认角色的模型。 func (l *Loop) recordLLMDone(span *trace.Span, out *stepOutcome, ag *agent.Agent) { if span == nil || out == nil { return } props := map[string]any{ "stop_reason": out.stopReason, "tool_calls": len(out.toolCalls), "elapsed_ms": elapsedMS(span), } if m := modelName(out, ag); m != "" { props["model"] = m } if out.modelStopReason != "" { props["model_stop_reason"] = out.modelStopReason } _ = span.Record(trace.Event{Type: EventLLMDone, Props: props}) } // recordToolDone 把一次工具调用的脱敏摘要追加到其 span:工具 // 名、是否成功、参数名(绝不记录其值)、失败时的 // 错误种类,以及截断后的结果摘要。参数值被 // 刻意省略 —— 工具参数正是凭据、消息正文 // 或含机密的文件路径会出现的地方 —— 且这里记录的一切 // 在发布前都会由图再次清洗。 func (l *Loop) recordToolDone(span *trace.Span, tc agent.ToolCall, out ToolOutcome) { if span == nil { return } props := map[string]any{ "tool": tc.Name, "ok": out.Err == nil, "args_keys": argKeys(tc.Args), "elapsed_ms": elapsedMS(span), } if out.Err != nil { if out.ErrKind != "" { props["error_kind"] = out.ErrKind } props["error"] = trimSummary(out.Err.Error()) } else { props["result_summary"] = trimSummary(formatToolResult(out.Result)) } _ = span.Record(trace.Event{Type: EventToolDone, Props: props}) } // modelName 报告服务某步的模型:流式路径知道时用路由出的 // 请求模型,否则用该 turn 的 agent 配置的模型。两者 // 都不可用时为空。 func modelName(out *stepOutcome, ag *agent.Agent) string { if out.model != "" { return out.model } if ag == nil { return "" } return ag.Model() } // elapsedMS 报告 span 迄今已打开多久。span 的 Duration 仅由 // Finish 打上,因此在 Finish 之前记录的日志事件在此测量它。 func elapsedMS(span *trace.Span) int64 { if span == nil || span.StartedAt.IsZero() { return 0 } return time.Since(span.StartedAt).Milliseconds() } // argKeys 按排序返回工具调用的参数名。只记录 // 参数名;值留在 trace 之外(见 recordToolDone)。 func argKeys(args map[string]any) []string { if len(args) == 0 { return nil } keys := make([]string, 0, len(args)) for k := range args { keys = append(keys, k) } sort.Strings(keys) return keys } // trimSummary 把日志取值缩短到 maxLogSummaryRunes 个 rune, // 且不切开多字节 rune。 func trimSummary(s string) string { r := []rune(s) if len(r) <= maxLogSummaryRunes { return s } return string(r[:maxLogSummaryRunes]) + "…" }