package loop import ( "encoding/json" "fmt" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/trace" ) // 本文件是 `tool_execution.end` 帧上 `resultSummary` 的**生产侧**(契约 v0.82)。 // 它回答「这次工具调用到底拿到了什么」,让实时工具卡与事后**回看**看到同一份文本: // 客户端回看一条回复时读的是 `toolCalls[].resultSummary`(图节点 `props.result_summary`, // 见 gateway/history_toolcalls.go),实时帧读的就是这里的产物。 // // 🔴 **失败也是结果**:回看面对一次失败的调用回的是图上的 `error` 摘要 // (`toolCallResultSummary` 在 `props.result_summary` 缺席时回落 `props.error`), // 所以实时帧在失败时同样带一份**已脱敏**的错误文本 —— 同一份脱敏路径、同一份上限, // 客户端不必为「实时」与「回看」写两套判据。键**只在「真的没有结果」时缺席**: // 调用成功但结果为空,或错误文本本身为空(见 `toolEndFrameSummary`)。 // // 🔴 为什么必须在 loop 侧算:`.end` 帧发出的那一刻,工具的返回值与错误原因只在这一层 // 存在;网关拿到的 `model.StreamEvent` 里没有原文(也**不该**有 —— 帧一旦携带上游原文, // 脱敏面就被绕过了)。所以摘要只在这里、在**同一份已脱敏文本**上生成,网关只做搬运 // 与截断,绝不反解上游原文。 // toolFrameResultSummary 把工具返回值渲染成 loop 侧的结果摘要文本,口径与图上那份 // `props.result_summary`(读回看面的数据源)**逐字一致**。 // // 图侧的取值链是:tool span 的 `result_summary` prop(由 spanlog.go 的 // recordToolResult 用**本函数**写入)→ `trace.safeSpanProps` 的白名单投影 → // 节点 `props.result_summary`。因此两侧不再是「各自重述同一步渲染」,而是同一个 // 生产函数:span 里不再存结果原文(那正是 M40 修掉的写侧泄漏),出图也不可能 // 再从原文反推 —— 摘要只在这里生成一次。 // // 🔴 两边必须逐字一致:由 `tool_frame_summary_test.go` 用**真建图器** // (`trace.RunGraph` + 真 span → 反读图上那份 `result_summary`)逐字比对来钉住 —— // 照字面量写断言会与实现一起漂移。 func toolFrameResultSummary(result any) string { return trace.RedactText(renderToolResultSummary(result)) } // toolEndFrameSummary 是一次调用结束时**要不要带摘要**(以及带哪一份)的取舍点。 // **失败也是结果**:调用成功 ⇒ 工具返回值的渲染;失败 —— 含工具自身报错、取消、 // 超时(三者对调用方都是 `Invoke` 返回 error)—— ⇒ **错误文本的脱敏渲染**,因为 // 回看面对失败调用回的就是图上那份 `error` 摘要。两边给同一份文本,「实时」与 // 「回看」不再分叉(这一次调用到底发生了什么,只有一种说法)。 // // 三条边界:① **超时 / 取消 / 工具报错一视同仁** —— 都是「这次调用有了结论」,错误文本 // 就是结论;失败时 `err` 优先,部分结果不顶掉失败原因(部分结果是过程,失败原因才是结论)。 // ② **脱敏不放宽**:错误文本走的仍是唯一脱敏源 `trace.RedactText`(凭据形状清洗 + 截断), // 与结果摘要是同一条路径,不新开第二条旁路;下游网关再按与回看面共用的 ≤500 rune 上限兜底 // (loop 侧本就被 trace 收到更短)。③ **只有真的没有结果才留空**:`err == nil` 且结果为空 // (nil / 空串),或 `err` 的文本本身为空时返回空串 ⇒ 帧上该键**整键缺席**,绝不用空串顶位 // (空串会让客户端渲染出一个空的结果区,比没有结果区更糟)。 func toolEndFrameSummary(result any, err error) string { if err != nil { return trace.RedactText(err.Error()) } return toolFrameResultSummary(result) } // renderToolResultSummary 把工具返回值渲染成一行文本(口径与 trace/summarize 相同)。 func renderToolResultSummary(v any) string { switch x := v.(type) { case nil: return "" case string: return x default: b, err := json.Marshal(x) if err != nil { return fmt.Sprintf("%v", x) } return string(b) } }