Files
Cyrene_v2/cmd/cyrene/wire_capacity.go
T
AskaEth 52f08dc343
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
docs(cyrene): cmd/cyrene 存量英文注释中文化(22 个非测试文件,仅注释)
- 四组并行完成:main/wire*.go、wire_capacity|channels|runtime、channels|persona_*|emotion|observability、
  voice|plugins|stream|skills|vision|subagent_tool|extractor|mcp|sessionread_tool|terminal_tool。
- 机械校验(git diff -U0 去 // 行与空行)仅剩 wire_frontend.go:84 一处行尾注释:代码前缀
  「\ta.gw = gw 」经 cat -A 与 HEAD 逐字节比对一致(allow 清单已声明)。
- 验收:隔离 worktree(HEAD + 本批 patch)go build/vet 绿、全树 58 包 ok、
  linux/amd64 + darwin/arm64 交叉编译绿;gofmt -l 干净。
- 备注:整树两次红均为负载敏感 flake(backthink / orchestration 的 timeout 类用例单独 -count=20 全过)。
2026-10-04 16:58:06 +08:00

195 lines
8.3 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 main
import (
"encoding/json"
"fmt"
"log/slog"
"strings"
"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"
)
// 本文件拥有唤醒式能力(capacity)决策层的装配:判定器构造、工具集 JSON 解析、
// L3 缓存 TTL 解析与 capacity_awaken 事件出口。它从 wire_runtime.go 拆出
// (AGENTS §10 文件 ≤500 行;wire.go 拆分建议同样适用于本文件)。
// buildCapacity 在启用时构造唤醒式能力判定器;未启用则返回 nil(loop 保持默认的
// 「全工具」行为)。它由 CAPACITY_ENABLED 显式开启;允许集映射取自
// CAPACITY_TOOL_SETS,解析失败时回落到内置默认值。
//
// 模型辅助的 L3 层(裁决 14.4.4)是第二个、彼此独立的开关
// (CAPACITY_L3_ENABLED,默认 false):即使能力层已开,L3 仍保持静默,除非被
// 显式要求;其判定结果的缓存 TTL(CAPACITY_L3_CACHE_TTL,默认 5m)在此解析,
// 因为配置加载器只承载标量。
//
// 人格级唤醒覆盖(裁决 14.4.3)分两处进入决策层。本函数填**全局**兜底
// (Config.Awakening),取自当前主角色 YAML 里的块(见 defaultAwakening):
// 未声明该块的人格继承的就是它。每个 turn 的取值由 awakeningFor 解析,loop 经
// loop.Options.PersonaAwakening 对该 turn 自己的角色调用它——没有它,非默认
// 角色的 `awakening` 块会被解析、校验,然后被静默忽略(design/01 §2.1 禁止)。
func (a *app) buildCapacity() *capacity.Decider {
if !a.cfg.Capacity.Enabled {
return nil
}
sets, err := parseCapacityToolSets(a.cfg.Capacity.ToolSetsJSON)
if err != nil {
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
cfg.L3Classifier = capacity.NewLLMClassifier(a.modelAdapter, a.log)
cfg.L3CacheTTL = parseL3CacheTTL(a.cfg.Capacity.L3CacheTTL, a.log)
}
return capacity.New(cfg, capacity.WithLogger(a.log))
}
// parseL3CacheTTL 解析 CAPACITY_L3_CACHE_TTL 这个时长字符串。空值选代码默认
// (5m,裁决 14.4.4 的落地建议);无法解析的值会告警并回落到同一默认,而不是
// 静默关掉缓存。
func parseL3CacheTTL(raw string, log *slog.Logger) time.Duration {
raw = strings.TrimSpace(raw)
if raw == "" {
return capacity.DefaultL3CacheTTL
}
d, err := time.ParseDuration(raw)
if err != nil {
logger.WithSource(log, "cmd/cyrene").Warn("capacity L3 cache ttl unparsable, using the default",
"value", raw, "default", capacity.DefaultL3CacheTTL.String(), "error", err)
return capacity.DefaultL3CacheTTL
}
return d
}
// parseCapacityToolSets 解码 CAPACITY_TOOL_SETS 的 JSON 映射:态 → 允许的工具名
// 列表。空白串产出 nil(用内置默认);某态取 null 表示不受限(全部工具)。
func parseCapacityToolSets(raw string) (map[capacity.State][]string, error) {
if strings.TrimSpace(raw) == "" {
return nil, nil
}
var m map[string][]string
if err := json.Unmarshal([]byte(raw), &m); err != nil {
return nil, fmt.Errorf("capacity tool sets: %w", err)
}
out := make(map[capacity.State][]string, len(m))
for k, v := range m {
out[capacity.State(k)] = v
}
return out, nil
}
// defaultAwakening 取当前主角色(`configs/users/owner.yaml` 的 `defaultPersona`,
// 未配置时回落 `cyrene`)档案里的 `awakening` 块。它填的是决策层的**全局兜底**
// (capacity.Config.Awakening);每个 turn 真正生效的档位由 awakeningFor 按该
// turn 的角色解析(裁决 14.4.3 的后半)。两者共用同一处映射,因此「全局兜底」
// 与「按角色取值」的口径绝不会分叉。
//
// 未装配人格注册表(降级启动 / 单测构造的 app)时返回零值 —— 注册表缺席时连
// 默认角色都问不出来,此时不补任何档位。
func (a *app) defaultAwakening() capacity.Awakening {
if a.personas == nil {
return capacity.Awakening{}
}
return a.awakeningFor(a.defaultPersonaID())
}
// awakeningFor 取**该角色**档案里的 `awakening` 块,映射为决策层的人格档位覆盖
// (design/14 §3,裁决 14.4.3)。它是 loop.Options.PersonaAwakening 的实现:循环
// 每个 turn 按该 turn 的角色 id 调它,因此绑定非默认角色的会话用**自己**的档位
// 判定能力面,而不是只吃默认角色的全局兜底。
//
// 下列情形一律返回零值 = **无覆盖**,由决策层走全局默认(Config.Awakening +
// 全局阈值),绝不在此处凭空补档位:
// - 尚未装配人格注册表(降级启动 / 单测构造的 app);
// - 角色 id 为空或该角色不在注册表里(未知/已删除人格);
// - 该角色档案里没写 `awakening` 块(缺席就是没有)。
//
// 档位是倾向/呈现,不是能力边界——它只影响本轮暴露的工具面与唤醒阈值。
func (a *app) awakeningFor(personaID string) capacity.Awakening {
if a.personas == nil || personaID == "" {
return capacity.Awakening{}
}
p, ok := a.personas.Persona(personaID)
if !ok || p == nil {
return capacity.Awakening{}
}
return a.awakeningOf(p)
}
// awakeningOf 把一份人格档案的 `awakening` 块映射为决策层的档位覆盖。映射不了
// 的档位名按「无该档位」处理(awakeningStateOf 会告警),因此非法值绝不会被塞进
// 决策层(未知态会落进「不受限工具集」分支)。
func (a *app) awakeningOf(p *loader.Persona) capacity.Awakening {
if 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 是 loop 的 OnCapacity 出口:它观测一次 capacity_awaken 跃迁,并以
// `capacity_awaken` 帧推给 Web UI(design/14 §5)。loop 只在真实发生状态跃迁时
// 才发(绝非每轮都发);事件携带发起会话 id,因此路由到那个会话而非全局广播。
// 载荷是前端的 AwakenPayload {fromState,toState,tool}。
func (a *app) onCapacity(ev capacity.Event) {
logger.WithSource(a.log, "cmd/cyrene").Debug("capacity event",
"type", ev.Type, "from", ev.FromState, "to", ev.ToState,
"tool", ev.Tool, "at", ev.At, "session", ev.SessionID)
if a.capacitySink == nil {
return
}
a.capacitySink.BroadcastCapacityAwaken(ev.SessionID, string(ev.FromState), string(ev.ToState), ev.Tool)
}