- 四组并行完成: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 全过)。
81 lines
4.1 KiB
Go
81 lines
4.1 KiB
Go
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
|
||
}
|
||
}
|