Files
Cyrene_v2/cmd/cyrene/persona_agents.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

81 lines
4.1 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 (
"git.yeij.top/AskaEth/Cyrene_v2/internal/logger"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/agent"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/guardrail"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/tool"
)
// 本文件构建按角色的 agent 池(design/22 §4.2 方案 A)。agent.Agent 的指令在构造
// 之后不可变,因此「会话的角色决定 system 提示」被实现为每个角色一个 agent:它们
// 共享同一个 dispatcher、guardrail 流水线、模型适配器与选项 —— 只有指令字符串不同。
// 循环在每一步为当轮角色挑选池中条目(internal/runtime/loop/persona.go),因此角色
// 切换在下一轮即生效,无需重建循环。
//
// design/22 §5 的要求:提示、记忆、情绪与 thinking 必须全部由同一个角色解析。
// 池拥有提示,循环拥有另外三者(personaFor / knobsFor),因此一轮绝不会混用两个
// 角色。
// personaAgentPool 返回按角色的 agent 池,以及用来跑轮次的 agent:池中有默认角色时
// 用池里的那个,否则用调用方的兜底 agent,因此未注册任何角色的部署仍保持与多角色
// 之前完全一致的单 agent 形态。
func (a *app) personaAgentPool(llm *model.RuntimeLLM, dispatcher *tool.Dispatcher, guards *guardrail.Pipeline, fallback *agent.Agent) (map[string]*agent.Agent, *agent.Agent) {
pool := a.buildPersonaAgents(llm, dispatcher, guards)
if pooled, ok := pool[a.defaultPersonaID()]; ok {
return pool, pooled
}
return pool, fallback
}
// buildPersonaAgents 返回「角色 → agent」池:每个已注册角色对应的 persona 都得到
// 一个携带该 persona system 指令的 agent(instructionsFromPersona,与默认 agent
// 是同一个装配点 —— 见 personaInstructions),design/01 §2.2。未注册 persona 时
// 返回空 map,循环便保持其单一默认 agent。
//
// 池里的指令是**静态**的,因此主人身份说明书不在其中:那段知识按会话注入
// (loop.Options.PersonaKnowledgeFor),否则它会跟着每个角色进入群聊与
// 陌生人私聊(design/11 §0 红线)。
//
// 只有通过已注册角色可达的 persona 才入池:角色才是可选单位(1 角色 ↔ 1 persona),
// 因此没有角色的 persona 永远无法绑定到会话。绑定到未知 id 的会话于是退回默认
// agent,而其记忆作用域仍跟随所绑定的 id —— 过期的绑定退化为默认提示,而不是让
// 该轮失败。
func (a *app) buildPersonaAgents(llm *model.RuntimeLLM, dispatcher *tool.Dispatcher, guards *guardrail.Pipeline) map[string]*agent.Agent {
log := logger.WithSource(a.log, "cmd/cyrene")
pool := make(map[string]*agent.Agent, a.personas.CountPersonas())
for _, c := range a.personas.List() {
if _, ok := pool[c.PersonaID]; ok {
continue
}
p, ok := a.personas.Persona(c.PersonaID)
if !ok || p == nil {
continue
}
// 该角色的 agent 必须绑它自己的 persona 视图:否则 models.json 的 personaModels /
// personaProviders(角色级模型/提供商路由)在生产里不会被消费,形同虚设。
stack := llm.ForPersona(c.PersonaID)
ag, err := agent.New(a.personaInstructions(p), dispatcher, guards,
agent.WithModel(stack, stack),
agent.WithOptions(agent.Options{MaxRounds: defaultMaxRounds, Purpose: "chat"}))
if err != nil {
log.Warn("persona agent skipped", "persona", c.PersonaID, "error", err)
continue
}
pool[c.PersonaID] = ag
}
return pool
}
// personaExists 返回编排层用来校验被派发的子代理目标角色的解析器(design/22 §8-2)。
// 某角色可派发,当且仅当池中有它的 agent,而这正是循环会用该角色指令作答的条件。
// 其他任何情况 —— 未注册的 persona、没有角色的 persona、空池 —— 都会让派发以
// orchestration.ErrUnknownPersona 失败,而不是悄悄按默认角色运行子代理。
func personaExists(pool map[string]*agent.Agent) func(personaID string) bool {
return func(personaID string) bool {
_, ok := pool[personaID]
return ok
}
}