feat(flow): toolCalls 覆盖率追到全部编排回合(L3)—— 图带权威回合标识

- 新增 StartRoundWithTurnID(ctx, owner, text):从 ctx 取权威回合标识(与 chat_messages.turn_id
  同值);Run 的唯一生产建图入口切到它;原 StartRound 语义逐字不变(1 行包装)
- 归属桶(并行按路分图)保持不变;落盘查询键 lane_turn_id 对单路也有值 ⇒ 消息面按等值查得到图
- 无权威标识时**保持缺席**(不拿桶里的路标识顶上:不许臆造)
- 不需要迁移 0024(0023 的列+索引已在位;老单路行标识根本没存过,回填=编造)

形状变化(已登记契约):graph.id 对单路也追加 #{回合标识}(web-7 → web-7#turn-…),
顺带修掉「重启后单路轮次 upsert 掉旧行」的隐患。3 个变异自检(退回老入口/无视 ctx/臆造回退)。
This commit is contained in:
AskaEth committed 2026-10-06 14:12:25 +08:00
1 parent 6475eb875d
commit 4a937bd269
6 files changed
+353 -21

No files matched your search

+19 -11
View File
@@ -66,10 +66,12 @@ type FlowGraph struct {
Edges []FlowEdge `json:"edges"`
// SessionID 是拥有这张图的会话(帧壳与落盘行的归属坐标)。
SessionID string `json:"sessionId,omitempty"`
// TurnID 是这张图所属**路**的权威回合标识(design/15 §2.5.9):真并行时
// 同一会话有多条路,每条路各有自己的图与本键。**缺席**语义:这条路没有
// 权威标识(单路的历史帧、未接编排器的调用方、旧后端)——客户端据此把图
// 归到会话那一张(兼容口径),绝不臆造一个 id。
// TurnID 是这张图所属**路**的权威回合标识(design/15 §2.5.9):编排器给**每一路**
// 都发(普通单路回合、队首续跑路、真并行路都有),因此它就是这张图属于哪一回合的
// 答案,也是落盘 `flow_graphs.lane_turn_id` 与 `chat_messages.turn_id` 的同值。
// **缺席**语义:这条路没有权威标识(未接编排器的调用方、后台思考、旧后端)——
// 客户端据此把图归到会话那一张(兼容口径),读侧据此让该轮的 `toolCalls` 整体
// 缺席;两侧都**绝不**臆造一个 id。
TurnID string `json:"turnId,omitempty"`
// Round 是这张图所属的**会话轮次号**(每会话单调递增;同一轮的各条路共享它)。
// 客户端据此只保留最新一轮的泳道,避免旧轮的路留在页面上。
@@ -105,7 +107,10 @@ type RunGraph struct {
// 未经 SetSeq 建图的调用方(图构建器单测)保留 `{会话}-{轮次}` 的老形状。
seq int
// turnID 是本图所属**路**的权威回合标识(design/15 §2.5.9)。空串表示这条路
// 没有权威标识(单路 / 未接编排器 / 旧调用方),此时图上该键缺席。
// 没有权威标识(未接编排器 / 旧调用方),此时图上该键缺席。它有一个可见后果:
// 图 id 的后缀取它(见 graphID),因此有权威标识的单路图也不再是 `{会话}-{序号}`
// 而是 `{会话}-{序号}#{回合}` —— 落盘唯一键 `(session_id, turn_id)` 因此跨进程
// 不撞(序号只在进程内递增、重启归零,而回合标识每回合全新)。
turnID string
nodes map[string]*FlowNode
order []string
@@ -151,7 +156,7 @@ func (g *RunGraph) Start(round int, now time.Time) {
}
// SetTurnID 盖上这张图所属**路**的权威回合标识(design/15 §2.5.9)。空串是
// 合法入参(单路 / 未接编排器),此时快照里该键缺席(omitempty),客户端回落
// 合法入参(未接编排器的调用方),此时快照里该键缺席(omitempty),客户端回落
// 到「按会话归一张图」的兼容口径。
func (g *RunGraph) SetTurnID(turnID string) { g.turnID = turnID }
@@ -195,9 +200,11 @@ func (g *RunGraph) SeedOutput(text string) string {
const laneIDSep = "#"
// Snapshot returns a copy of the graph safe to encode and publish. 图 id 是
// `{会话}-{轮次}`;这张图属于一条**路**时追加 `#{路标识}` —— 真并行下同一会话
// 同一轮的两路必须各有各的 id,否则落盘(`flow_graphs` 的 `(session_id, turn_id)`
// 唯一键)会把其中一路整行顶掉、客户端也分不出两张图(design/15 §2.5.10-1)。
// `{会话}-{轮次}`;这张图属于一条**有权威回合标识的路**时追加 `#{回合标识}` ——
// 真并行下同一会话同一轮的两路必须各有各的 id,否则落盘(`flow_graphs` 的
// `(session_id, turn_id)` 唯一键)会把其中一路整行顶掉、客户端也分不出两张图
// (design/15 §2.5.10-1);单路(编排器也给它盖了权威标识)同样带后缀,因此它的
// 图 id 跨进程不撞(见 RunGraph.turnID)。
func (g *RunGraph) Snapshot() *FlowGraph {
nodes := make([]FlowNode, 0, len(g.order))
for _, id := range g.order {
@@ -215,9 +222,10 @@ func (g *RunGraph) Snapshot() *FlowGraph {
}
}
// graphID 拼出这张图的 id:`{会话}-{序号}[#{路标识}]`。序号取会话级建图序号
// graphID 拼出这张图的 id:`{会话}-{序号}[#{权威回合标识}]`。序号取会话级建图序号
// (未设置时退回轮次号,见 RunGraph.seq),因此**同一轮里的每一条路也各有各的 id**;
// 路标识只在「这条路有权威标识」时追加。
// 回合标识只在「这条路有权威标识」时追加(单路在有 ctx 通道时也有 —— 见
// loop.FlowPublisher.StartRoundWithTurnID)。
func (g *RunGraph) graphID() string {
n := g.seq
if n == 0 {