package loop import ( "context" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/agent" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/capacity" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model" ) // 本文件拥有循环的按 turn 角色解析(DevDocs/design/22 §4.2): // 一个进程现在同时承载多个角色,因此过去从 // Loop 构造期绑定读取的一切 —— 系统提示词(经 agent)、 // 记忆命名空间、情绪机与推理旋钮 —— 改为从该 turn 的 // SessionEnv 解析。 // // 解析刻意做成 ctx 上的几个小函数,而不是 Loop 上的 // 字段:Loop 由所有并发会话共享,把解析出的角色存在 // 它上面会产生数据竞争,而 ctx 本就携带按 turn 的 env。 // 所有角色作用域读取都必须走这些辅助函数,使提示词、记忆、 // 情绪、思考与唤醒档位永远不会对「谁在回答」产生分歧。 // LoopKnobs 是注入循环构建的每个请求的按角色模型/推理 // 旋钮。它镜像三个构造期选项 // (Options.Thinking / Options.ReasoningEffort / Options.ThinkingShowToUser),使 // 调用方能按角色解析它们,同时这些选项仍作为 // 默认值生效。nil Thinking 与空 ReasoningEffort 表示「不干预」, // nil ThinkingShowToUser 表示「照常广播思考帧」。 type LoopKnobs struct { // Thinking 是该角色的推理指令(预算 / 显式开关)。 Thinking *model.Thinking // ReasoningEffort 是该角色兼容 OpenAI 的强度旋钮。 ReasoningEffort string // ThinkingShowToUser 控制该角色各 turn 的思考帧是否广播。 ThinkingShowToUser *bool } // personaFor 返回当前 turn 发言所作为的角色(design/22 §2):调用方 // 解析出角色时取会话环境的显式角色,否则取循环 // 配置的角色(Options.PersonaID,全局默认)。提示词/记忆、 // 情绪钩子与唤醒档位都读它,因此一个 turn 绝不会混用两个角色。 func (l *Loop) personaFor(ctx context.Context) string { if pid := sessionEnvFrom(ctx).PersonaID; pid != "" { return pid } return l.personaID } // agentFor 返回承载当前 turn 系统提示词的 agent:存在 // 按角色 agent 池条目时用它,否则用循环的默认 agent // (design/22 §4.2 方案 A)。会话绑定到一个没有注册 agent 的角色 —— // 未知/已删除的人格,或未接任何池的调用方 —— 时回退到 // 默认 agent 而不是让该 turn 失败,且其记忆作用域仍跟随 // personaFor,因此该回退绝不会悄悄改变记忆作用域。 func (l *Loop) agentFor(ctx context.Context) *agent.Agent { if len(l.agents) > 0 { if ag := l.agents[l.personaFor(ctx)]; ag != nil { return ag } } return l.agent } // knobsFor 解析当前 turn 的推理旋钮:解析器 // 认识该角色时用其按角色取值,否则用循环配置的 // 默认值。不认识该角色的解析器(ok = false)刻意 // 保留默认值而不是把旋钮清零,使未注册的 id 表现 // 如同默认角色,而不是悄悄禁用推理。 func (l *Loop) knobsFor(ctx context.Context) LoopKnobs { def := LoopKnobs{ Thinking: l.thinking, ReasoningEffort: l.reasoningEffort, ThinkingShowToUser: l.thinkingShow, } if l.personaKnobs == nil { return def } pid := l.personaFor(ctx) if pid == "" { return def } knobs, ok := l.personaKnobs(pid) if !ok { return def } return knobs } // awakeningFor 解析当前 turn 的唤醒档位(design/14 §3,裁决 14.4.3):该 turn // 所属角色在档案里声明的 `awakening` 块,映射成决策层的人格覆盖。档位随 turn // 解析而不是在 Loop 构造期绑定,理由与提示词/记忆/旋钮相同 —— 一个进程同时 // 承载多个角色,而 Loop 由所有并发会话共享(存在 Loop 上会产生数据竞争)。 // 解析器由组合根注入(Options.PersonaAwakening,按角色 id 查那份 YAML), // 与 knobsFor 一样按角色 id 解析,因此所有路径(网关、channelbridge、 // backthink、子代理)都从同一个输入落到正确角色的档位。 // // 下列情形一律返回零值 = **无覆盖**,由判定器回落到 Config.Awakening(全局 // 兜底)与全局阈值,绝不在此处凭空补档位: // - 未注入解析器(单角色接线、或未接线的调用方)——逐字节等于接线前的行为; // - 该 turn 没有角色(SessionEnv.PersonaID 与 Options.PersonaID 都为空); // - 该角色档案里没写 `awakening` 块(缺席就是没有)。 func (l *Loop) awakeningFor(ctx context.Context) capacity.Awakening { if l.personaAwakening == nil { return capacity.Awakening{} } pid := l.personaFor(ctx) if pid == "" { return capacity.Awakening{} } return l.personaAwakening(pid) }