package loop import ( "context" "strings" ) // flowOwnerSep 是 flow 图**归属桶**(`{会话}#{路}`)的分隔符。它与 // design/15 §2.5.9 的 checkpoint 归属键(`{sessionID}#{turnID}`)以及 // trace 的图 id 拼接口径(`{会话}-{轮次}#{路}`)取同一个字符:**一条路** // 在流程图、checkpoint 与落盘行三处因此是同一个标识。 // // 之所以能在同一个字符串里编码「会话 + 路」:会话 id 的取值族是 // `web` / `web:` / `sess-backthink`(`cmd/cyrene` 的 flowSessionAllowed), // 权威 turnId 是 `turn-`(`gateway.newTurnID`),两者都不含 `#`。 const flowOwnerSep = "#" // FlowOwnerKey 拼出「会话[#路]」的 flow 归属桶;lane 为空时就是会话本身 // (单路 / 未声明并行的运行,逐字等于引入本特性之前的会话 id)。 // // 导出它是因为**归属桶的串形状只有一个权威**:发布器的建图出口、子代理 span 的 // 归属、以及跨包的验收测试都按它拼串,任何一处自己拼 `#` 都会在换分隔符时静默走偏。 func FlowOwnerKey(sessionID, lane string) string { if lane == "" { return sessionID } return sessionID + flowOwnerSep + lane } // splitFlowOwner 把归属桶拆回(会话, 路)。没有 `#` 时整串就是会话、路为空 —— // 这正是单路运行与**旧调用方**传给发布器的形态,因此拆分是向后兼容的。 func splitFlowOwner(owner string) (string, string) { if i := strings.Index(owner, flowOwnerSep); i >= 0 { return owner[:i], owner[i+1:] } return owner, "" } // flowBranchKey 是标记某个 Run 为另一会话 flow 图分支的 context key。 // 它由 orchestration 层(经 WithFlowBranch)在驱动 // 子代理 Run 之前设置。 type flowBranchKey struct{} // WithFlowBranch 返回 ctx,把当前 Run 标记为分支:它的 span 并入 // parent 的实时 flow 图,且该 Run 自身既不开始也不结束 // 轮次 —— 由所属会话的轮次负责。orchestration 层正是靠它把 // 子代理的工作归属到派生它的会话,使并行的 // 子代理渲染为同一张图的泳道,而不是用各自的图 // 替换前端唯一的 flowGraph(design/19 §2)。 // // parent 是**归属桶**(`{会话}#{路}`,见 FlowOwnerKey):并行路里派发的子代理必须 // 并进**那一路**的图,而不是会话主干那张 —— 否则两路的子代理节点会混进同一张图, // 正是本切片要治的「两路互相覆盖」(design/15 §2.5.10-1)。单路时它逐字等于会话 id。 // // 空的 parent 仍把该 Run 标记为分支:它不拥有轮次、 // 不发布任何东西。这正是无人归属到会话的后台子代理的 // 状态,它不得悄悄变成属于自己的一张图。nil ctx // 原样返回。 func WithFlowBranch(ctx context.Context, parent string) context.Context { if ctx == nil { return ctx } return context.WithValue(ctx, flowBranchKey{}, parent) } // flowBranch 报告 WithFlowBranch 记录的父归属桶以及 // 该 Run 是否本就是分支。非分支的 Run 拥有自己的轮次并在 // 自己的 session id 下发布。 func flowBranch(ctx context.Context) (string, bool) { if ctx == nil { return "", false } v := ctx.Value(flowBranchKey{}) if v == nil { return "", false } s, _ := v.(string) return s, true } // flowOwner 返回应当接收该 Run span 的 flow 归属桶: // 当该 Run 是子代理分支时取分支父级(分支未归属时为空, // 此时抑制发布),否则取该 Run 自身的 session 加上它所属的**路** // (真并行路的标识由网关经 WithParallelLeg 盖上;单路时为会话本身)。 func flowOwner(ctx context.Context, sessionID string) string { if parent, isBranch := flowBranch(ctx); isBranch { return parent } return FlowOwnerKey(sessionID, ParallelLegFrom(ctx)) } // FlowOwnerFromContext 返回拥有从 ctx 启动的工作的 flow 图**归属桶** // (`{会话}#{路}`,见 FlowOwnerKey):当 ctx 运行于子代理分支内时取分支父级, // 否则取外层 Run 被驱动的 session id 加上它所属的路(两者都未设置时为空)。 // // 派发后台工作的工具 —— 子代理派生工具 —— 把它作为父归属桶传入,使在另一个 // 子代理内部派生的子代理保持在同一个顶层图的归属之下,而不是归属到一个不拥有 // 任何图的 id;真并行路里派发的子代理也因此落进**那一路**的图。 // // ⚠️ 它返回的是**归属桶**而不是会话 id。要把子代理的归属会话交给总线载荷 // (`subagent_result` 的 sessionId 必须是真会话 id)时必须用 // FlowSessionFromContext 取会话那一半。 func FlowOwnerFromContext(ctx context.Context) string { if owner, isBranch := flowBranch(ctx); isBranch { return owner } session := SessionFromContext(ctx) if session == "" { // 没有会话就没有归属:绝不回一个 `#路` 形态的串(那既查不到图,也会让 // 调用方误以为有归属)。分支与子代理因此保持「不发布」的既有语义。 return "" } return FlowOwnerKey(session, FlowLaneFromContext(ctx)) } // FlowSessionFromContext 返回从 ctx 启动的工作的**归属会话 id**(不含路): // 真并行时同一会话的每一路都是同一个会话 id,总线载荷与日志要的就是它。 func FlowSessionFromContext(ctx context.Context) string { session, _ := splitFlowOwner(FlowOwnerFromContext(ctx)) return session } // FlowLaneFromContext 返回从 ctx 启动的工作所属的**路**标识(design/15 §2.5.9 // 的 turnId);单路 / 未声明并行时为空。分支运行取分支归属桶里的那一半(子代理 // 因此继承派生它的那一路),非分支运行取本 Run 的并发路。 func FlowLaneFromContext(ctx context.Context) string { if owner, isBranch := flowBranch(ctx); isBranch { _, lane := splitFlowOwner(owner) return lane } return ParallelLegFrom(ctx) }