Files
Cyrene_v2/internal/runtime/loop/persona.go
T
AskaEth 7afef8d037
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
fix(loop,gateway,persona): awakening 逐 turn 按角色生效 + 修「面板保存角色抹掉 5 个块」的数据丢失(裁决 14.4.3)
- per-role 注入补齐:Options.PersonaAwakening(personaID) → Loop.personaAwakening → l.awakeningFor(ctx)
  → capacity.Input.Awakening(toolSchemas 是全仓唯一构造该 Input 处)
  · 按 turn 解析(不绑 Loop 构造期,避免并发会话数据竞争),与既有 personaFor/knobsFor 同构
  · 未知角色/空角色/档案无该块 ⇒ 零值 ⇒ 回落 Config.Awakening(不声明者行为与今天构造性一致)
- **数据丢失修复**:persona PUT 是整块替换,此前只保留 thinking ⇒ 保存一次会同时抹掉
  awakening / visual(头像·背景) / triggerWords(群聊提醒词) / emotion(情绪机参数) / affection(好感度曲线)。
  新增 applyAwakeningEdit + applyPersonaReadOnlyBlocks 逐一保留(cyrene.yaml 五块全写,属必现故障)
- 文件守 500 行:主人档案段原样搬移到 admin_owner_profile.go(零逻辑变更)
- 复核:build/vet 绿 + 全树 `go test ./... -count=1` 0 FAIL;端到端=真 YAML→loader→buildCapacity→真 loop
  流式请求→断言工具面(tasky 声明 force_state 唤醒 → 含 web-search;cyrene 不声明 → 与无解析器对照逐字相同);
  **8 条变异反证**先红后还原(含逐块删除各自的保留逻辑)
- 残留缺口(登记):wire_runtime.go:254 那一行本身无用例覆盖;loader 非 strict(未知 YAML 键静默忽略,
  需主人裁决是否开 KnownFields);POST /admin/personas 仍写不出这五个块
2026-10-04 13:07:52 +08:00

112 lines
4.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
}