feat(persona,capacity): persona awakening 档位真生效 + 当前主角色已填(裁决 14.4.3)
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

- loader 新增可选 awakening 块(default_state/overrides{min_confidence,force_state}),载入期校验 + 归一化,
  非法值显式报错(拒绝「写了却静默无效」);缺失 = nil = 无该档位,不补默认
- 组合根把当前主角色档案的档位填进 capacity.Config.Awakening(未知档位名告警后按无档位处理),
  真的参与 Decide → applyAwakening(MinConfidence 优先于全局 0.6;force_state 钉死态)
- 当前主角色 cyrene.yaml 填 default_state=静息 + min_confidence=0.7(设计 §3 陪伴型取静息;
  0.7 压掉 L2 趋势(0.66)误唤醒、仍低于 L1 强信号(0.9),温柔陪伴不误打扰)
- 复核:build/vet 绿 + persona/capacity/cmd 包测试绿;端到端 4 例锁定「同回合有无档位判定相反」;
  变异两轮(yaml tag 改错 → 8 处红;解析但不接线 → 端到端红)均已还原
- 已知盲区(另派):per-role 注入未接线(loop/request.go 的 Input.Awakening);
  面板整块替换 persona 会抹掉 awakening 块(需 applyAwakeningEdit)
This commit is contained in:
AskaEth committed 2026-10-04 12:49:40 +08:00
1 parent 38b4527949
commit 71fefe5fc3
7 files changed
+602 -1

No files matched your search

+71
View File
@@ -8,6 +8,7 @@ import (
"time"
"git.yeij.top/AskaEth/Cyrene_v2/internal/logger"
"git.yeij.top/AskaEth/Cyrene_v2/internal/persona/loader"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/capacity"
)
@@ -25,6 +26,10 @@ import (
// unless it was explicitly asked for, and its verdict cache TTL
// (CAPACITY_L3_CACHE_TTL, default 5m) is parsed here because the config loader
// only carries scalars.
//
// The persona-level awakening override (裁决 14.4.3) is filled here from the
// current main persona's YAML block (see defaultAwakening); a persona that
// declares no block contributes the zero value, i.e. no override.
func (a *app) buildCapacity() *capacity.Decider {
if !a.cfg.Capacity.Enabled {
return nil
@@ -34,10 +39,17 @@ func (a *app) buildCapacity() *capacity.Decider {
a.log.Warn("capacity tool sets parse failed, using built-in defaults", "error", err)
sets = nil
}
awakening := a.defaultAwakening()
if !awakening.IsZero() {
logger.WithSource(a.log, "cmd/cyrene").Info("capacity awakening override applied",
"default_state", awakening.DefaultState, "min_confidence", awakening.MinConfidence,
"force_state", awakening.ForceState, "persona", a.defaultPersonaID())
}
cfg := capacity.Config{
Enabled: true,
MinConfidence: a.cfg.Capacity.MinConfidence,
ToolSets: sets,
Awakening: awakening,
}
if a.cfg.Capacity.L3Enabled {
cfg.L3Enabled = true
@@ -83,6 +95,65 @@ func parseCapacityToolSets(raw string) (map[capacity.State][]string, error) {
return out, nil
}
// defaultAwakening 取当前主角色(`configs/users/owner.yaml` 的 `defaultPersona`,
// 未配置时回落 `cyrene`)档案里的 `awakening` 块,映射为 capacity 的人格档位覆盖
// (design/14 §3,裁决 14.4.3)。下列情形一律返回零值 = **无覆盖**,由决策层走
// 全局默认,绝不在此处凭空补档位:
// - 尚未装配人格注册表(降级启动 / 单测构造的 app);
// - 该角色档案里没写 `awakening` 块(缺席就是没有);
// - 档位名不是规范态名(loader 已在载入期拒绝非法值;这里按无覆盖处理并
// 告警——未知态若被塞进决策层,会落进「不受限工具集」分支)。
//
// 档位是倾向/呈现,不是能力边界——它只影响本轮暴露的工具面与唤醒阈值。
func (a *app) defaultAwakening() capacity.Awakening {
if a.personas == nil {
return capacity.Awakening{}
}
p, ok := a.defaultPersona()
if !ok || p == nil || p.Awakening == nil {
return capacity.Awakening{}
}
aw := capacity.Awakening{
DefaultState: a.awakeningStateOf(p.Awakening.DefaultState, p.ID, "default_state"),
}
if ov := p.Awakening.Overrides; ov != nil {
aw.MinConfidence = ov.MinConfidence
aw.ForceState = a.awakeningStateOf(ov.ForceState, p.ID, "overrides.force_state")
}
return aw
}
// awakeningStateOf 映射单个档位字面量;映射不了就**告警**再按「无该档位」处理。
// 经 loader.Load 进来的人格不可能走到告警分支(载入期已归一化并拒绝非法值),
// 因此它是给「代码构造的人格」留的响铃:宁可吵一声,也不让配置静默失效
// (design/01 §2.1 禁止静默无效的配置)。
func (a *app) awakeningStateOf(raw, personaID, field string) capacity.State {
if raw == "" {
return ""
}
state := awakeningState(raw)
if state == "" {
logger.WithSource(a.log, "cmd/cyrene").Warn("capacity awakening state unrecognized, treated as no override",
"value", raw, "field", field, "persona", personaID)
}
return state
}
// awakeningState 把 loader 归一化后的档位名映射到决策层的态。空串(未声明、或未知
// 值)返回零值态——映射层不做默认填充,缺失的档位就是缺失。
func awakeningState(raw string) capacity.State {
switch raw {
case loader.AwakeningStateQuiet:
return capacity.StateQuiet
case loader.AwakeningStateAwake:
return capacity.StateAwake
case loader.AwakeningStateFull:
return capacity.StateFull
default:
return ""
}
}
// onCapacity is the loop's OnCapacity sink: it observes a capacity_awaken
// transition and pushes it to the Web UI (design/14 §5) as a `capacity_awaken`
// frame. The loop only emits on a real state transition (never every round), and