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 } }