docs(comments): 注释中文化 · internal/runtime/orchestration(29 文件,纯替换)
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

- 只翻注释/文档字符串:剥离注释后代码行逐字节不变(机械校验只剩允许的行尾注释形态)
- 裁决编号与 design 节号(02.4.3 / 11.4.2 / design/05 §9 等)逐字保留
- 复核:go build 绿;orchestration 及其子包(audit/comm)测试绿;gofmt 干净
This commit is contained in:
AskaEth committed 2026-10-04 13:30:04 +08:00
1 parent fe90f64a56
commit eb1828ee7a
29 files changed
+751 -949

No files matched your search

@@ -17,35 +17,33 @@ import (
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model"
)
// This file locks design/22 §8-2 at the dispatch layer: `subagent_spawn` may name a
// target persona, and the sub-agent then runs AS that role — that role's instructions
// become the leading system message and that role's memory scope is what it reads and
// writes ("她以该角色身份临时替班"). Everything is exercised in process over a REAL
// *loop.Loop with a per-role agent pool and a REAL memory store, so the assertions are
// about what the sub-agent's run actually saw, not about wiring.
// 本文件在派发层锁定 design/22 §8-2:`subagent_spawn` 可以指名一个目标 persona,随后
// 子代理以那个角色身份运行 —— 该角色的指令成为前置 system 消息,该角色的记忆作用域
// 就是它读写的那一份(「她以该角色身份临时替班」)。一切都以真实 *loop.Loop 搭配按角色
// agent 池与真实记忆存储、在进程内走通,因此断言的是子代理运行实际看到了什么,而不是
// 接线。
//
// The other half of the file pins the backward-compatibility contract: omitting the
// persona must reproduce the pre-change behavior exactly (the sub-agent inherits the
// spawning turn's role), an unknown persona must be rejected instead of silently
// degrading, and naming a persona must never widen the identity/permission lane.
// 本文件的另一半钉住向后兼容契约:省略 persona 必须逐字复现改动前的行为(子代理继承
// 派发回合的角色);未知 persona 必须被拒绝,而不是静默降级;指名 persona 绝不得放宽
// 身份/权限车道。
const (
// parentRole is the role the dispatching session is bound to — deliberately NOT
// the role a persona dispatch asks for, so a leak shows up as the wrong prompt.
// parentRole 是派发会话所绑定到的角色 —— 故意不是 persona 派发所要求的角色,
// 因此一旦泄漏就会以错误的提示词暴露出来。
parentRole = "xilian"
// targetRole is the role a persona dispatch asks the sub-agent to stand in for.
// targetRole 是 persona 派发要求子代理替班的那个角色。
targetRole = "yuki"
)
// parentPrompt and targetPrompt are the two roles' instructions; a test tells which
// pool entry the loop resolved from the leading system message alone.
// parentPrompt 与 targetPrompt 是两个角色的指令;测试仅凭前置 system 消息就能判断循环
// 解析到了 agent 池里的哪一项。
const (
parentPrompt = "你是昔涟。"
targetPrompt = "你是由纪。"
)
// roleAdapter records every request the loop builds and answers with a fixed final
// text, so a test can read the exact prompt and memory block a sub-agent's run got.
// roleAdapter 记录循环构建的每次请求,并以固定最终文本作答,使测试能读到子代理运行实际
// 拿到的提示词与记忆块。
type roleAdapter struct {
mu sync.Mutex
reqs []model.Request
@@ -64,8 +62,8 @@ func (a *roleAdapter) ChatStream(ctx context.Context, req model.Request, _ func(
return a.Chat(ctx, req, model.Options{})
}
// request returns the n-th recorded request, failing the test when the run produced
// fewer. Requests are recorded in call order, i.e. one per sub-agent run.
// request 返回第 n 次记录的请求;运行产出的请求更少时判测试失败。请求按调用顺序记录,
// 即每个子代理运行一条。
func (a *roleAdapter) request(t *testing.T, n int) model.Request {
t.Helper()
a.mu.Lock()
@@ -76,8 +74,8 @@ func (a *roleAdapter) request(t *testing.T, n int) model.Request {
return a.reqs[n]
}
// leadingSystem returns the first message of the request, which is where the agent's
// instructions land (agent/request.go 首条 system 消息).
// leadingSystem 返回请求的第一条消息,agent 的指令正落在这里
// (agent/request.go 首条 system 消息)。
func (a *roleAdapter) leadingSystem(t *testing.T, n int) string {
t.Helper()
msgs := a.request(t, n).Messages
@@ -87,7 +85,7 @@ func (a *roleAdapter) leadingSystem(t *testing.T, n int) string {
return messageText(msgs[0])
}
// carries reports whether any message of the n-th request contains want.
// carries 报告第 n 次请求里是否有任何消息包含 want。
func (a *roleAdapter) carries(t *testing.T, n int, want string) bool {
t.Helper()
for _, m := range a.request(t, n).Messages {
@@ -98,10 +96,9 @@ func (a *roleAdapter) carries(t *testing.T, n int, want string) bool {
return false
}
// personaRig is the in-process e2e rig: a real loop with a two-role agent pool over
// a capturing adapter, the real scheduler on top (the production shape), the dispatch
// tool above that, and a real memory store so role scoping is asserted on persisted
// items. The turn hook records what role the loop resolved for each settled run.
// personaRig 是进程内端到端装置:一个在捕获式适配器之上、带双角色 agent 池的真实循环,
// 其上叠真实调度器(即生产形状),再往上是派发工具,另配真实记忆存储,使角色作用域能
// 针对已持久化的条目断言。turn 钩子记录循环对每次已结算运行解析出的角色。
type personaRig struct {
tool *SpawnTool
sched *Scheduler
@@ -112,8 +109,8 @@ type personaRig struct {
turns []loop.TurnEnd
}
// newPersonaRig builds the rig. personaExists is the role resolver under test: nil
// mirrors a composition root that wired no pool.
// newPersonaRig 组装装置。personaExists 是被测的角色解析器:nil 对应一个没有接 agent 池
// 的组合根。
func newPersonaRig(t *testing.T, personaExists func(string) bool) *personaRig {
t.Helper()
rig := &personaRig{ad: &roleAdapter{}, memStore: newPersonaMemoryStore(t)}
@@ -132,8 +129,8 @@ func newPersonaRig(t *testing.T, personaExists func(string) bool) *personaRig {
PersonaID: parentRole,
Memory: rig.memStore,
Retriever: memory.NewRetriever(rig.memStore),
// Built without a persona, exactly as cmd/cyrene builds it: stamping the
// turn's role onto each item is the loop's job.
// 构建时不带 persona,与 cmd/cyrene 的构建方式完全一致:把回合的角色盖到
// 每个条目上是循环的职责。
Extractor: memory.NewManualExtractor("", "owner"),
OnTurnEnd: func(end loop.TurnEnd) string {
rig.mu.Lock()
@@ -147,8 +144,7 @@ func newPersonaRig(t *testing.T, personaExists func(string) bool) *personaRig {
return rig
}
// turn returns the n-th settled turn the loop reported, failing the test when the
// rig observed fewer.
// turn 返回循环报告的第 n 个已结算回合;装置观察到的回合更少时判测试失败。
func (r *personaRig) turn(t *testing.T, n int) loop.TurnEnd {
t.Helper()
r.mu.Lock()
@@ -159,16 +155,15 @@ func (r *personaRig) turn(t *testing.T, n int) loop.TurnEnd {
return r.turns[n]
}
// parentTurnCtx is the context the dispatching turn runs under: the parent session's
// own role on the owner-full identity lane, as the gateway resolves it. A persona
// dispatch must be able to change the first without touching the second.
// parentTurnCtx 是派发回合所运行的上下文:父会话自己的角色,位于 owner 全权身份车道上,
// 即网关解析出的样子。persona 派发必须能改变前者而不触碰后者。
func parentTurnCtx() context.Context {
env := loop.OwnerEnv()
env.PersonaID = parentRole
return loop.WithSessionEnv(context.Background(), env)
}
// newPersonaMemoryStore opens an in-memory store for the rig.
// newPersonaMemoryStore 为装置打开一个内存存储。
func newPersonaMemoryStore(t *testing.T) *memory.Store {
t.Helper()
ctx := context.Background()
@@ -184,7 +179,7 @@ func newPersonaMemoryStore(t *testing.T) *memory.Store {
return store
}
// seedRoleMemory files one persona-scope memory under roleID.
// seedRoleMemory 在 roleID 下归档一条 persona 作用域的记忆。
func seedRoleMemory(t *testing.T, store *memory.Store, roleID, content string) {
t.Helper()
_, err := store.Add(context.Background(), memory.MemoryItem{
@@ -199,8 +194,7 @@ func seedRoleMemory(t *testing.T, store *memory.Store, roleID, content string) {
}
}
// storedPersonaOf returns the persona a memory with the given content was filed
// under, or "" when it is absent.
// storedPersonaOf 返回内容包含给定字符串的那条记忆所归档到的 persona;不存在时返回 ""。
func storedPersonaOf(t *testing.T, store *memory.Store, content string) string {
t.Helper()
items, err := store.List(context.Background())
@@ -215,15 +209,13 @@ func storedPersonaOf(t *testing.T, store *memory.Store, content string) string {
return ""
}
// TestSpawnToolRunsAsTargetPersona is the design/22 §8-2 acceptance, end to end in
// process: a dispatch naming a persona runs with THAT role's instructions and THAT
// role's memory, while the dispatching session's own role stays untouched. The same
// rig then proves the omitted-persona path is unchanged, which is the pre-change
// behavior: the sub-agent inherits the spawning turn's role.
// TestSpawnToolRunsAsTargetPersona 是 design/22 §8-2 的验收,进程内端到端:指名 persona
// 的派发以那个角色的指令与那个角色的记忆运行,而派发会话自己的角色保持不变。同一装置
// 随后证明省略 persona 的路径未变,即改动前的行为:子代理继承派发回合的角色。
func TestSpawnToolRunsAsTargetPersona(t *testing.T) {
t.Parallel()
rig := newPersonaRig(t, func(id string) bool { return id == parentRole || id == targetRole })
// One memory per role, so the injected block names which scope the run read from.
// 每个角色一条记忆,使注入的块能点名该次运行读的是哪个作用域。
seedRoleMemory(t, rig.memStore, targetRole, "由纪的暗号是香菜")
seedRoleMemory(t, rig.memStore, parentRole, "昔涟的暗号是乌冬")
@@ -237,38 +229,37 @@ func TestSpawnToolRunsAsTargetPersona(t *testing.T) {
if len(got.Agents) != 1 || got.Agents[0].Status != string(StatusDone) {
t.Fatalf("agents = %+v, want one settled sub-agent", got.Agents)
}
// The reported role is the target persona, so the tool result, the progress events
// and the ws subagent_event frame all name the role it really answered as.
// 报告的角色就是目标 persona,因此工具结果、进度事件与 ws subagent_event 帧都点名
// 它真正以之作答的角色。
if got.Agents[0].Role != targetRole {
t.Fatalf("reported role = %q, want the target persona %q", got.Agents[0].Role, targetRole)
}
// 1) The prompt the model actually received is the TARGET role's instructions —
// not the dispatching role's, and not a shared default.
// 1) 模型实际收到的提示词是目标角色的指令 —— 既不是派发角色的,也不是共享默认值。
if prompt := rig.ad.leadingSystem(t, 0); prompt != targetPrompt {
t.Fatalf("sub-agent system prompt = %q, want the target persona's %q", prompt, targetPrompt)
}
// 2) The role the loop resolved for the run (which scopes memory, emotion and the
// thinking knobs) is the target persona, although the dispatching env said xilian.
// 2) 循环为本次运行解析出的角色(它决定记忆、情绪与思考旋钮的作用域)是目标 persona,
// 尽管派发的 env 说的是 xilian。
if turn := rig.turn(t, 0); turn.PersonaID != targetRole {
t.Fatalf("sub-agent turn persona = %q, want %q", turn.PersonaID, targetRole)
}
// 3) Memory reads follow the target role: only yuki's memory was injected.
// 3) 记忆读取跟随目标角色:只有由纪的记忆被注入。
if !rig.ad.carries(t, 0, "由纪的暗号是香菜") {
t.Fatalf("sub-agent did not recall the target persona's memory: %+v", rig.ad.request(t, 0).Messages)
}
if rig.ad.carries(t, 0, "昔涟的暗号是乌冬") {
t.Fatalf("sub-agent read the dispatching persona's memory: %+v", rig.ad.request(t, 0).Messages)
}
// 4) The identity lane travelled unchanged: naming a persona is never a permission
// (project red line: 身份识别靠元数据 + 策略引擎).
// 4) 身份车道原样传递:指名 persona 从来不是一种权限
// (项目红线:身份识别靠元数据 + 策略引擎)。
env := rig.turn(t, 0).Env
if env.ActorEnv != restrict.ActorOwner || !env.MemWritable {
t.Fatalf("sub-agent env = %+v, want the dispatching owner lane unchanged", env)
}
// Backward compatibility: a dispatch without persona still runs as the spawning
// role (xilian) and reads xilian's memory — byte for byte the pre-change behavior.
// 向后兼容:不带 persona 的派发仍以派发角色(xilian)运行、读昔涟的记忆 —— 逐字节
// 等同改动前的行为。
raw, err = rig.tool.Execute(parentTurnCtx(), map[string]any{
"goal": "老路子", "role": "legacy-label",
})
@@ -288,17 +279,16 @@ func TestSpawnToolRunsAsTargetPersona(t *testing.T) {
if !rig.ad.carries(t, 1, "昔涟的暗号是乌冬") {
t.Fatalf("persona-less sub-agent lost the inherited role's memory: %+v", rig.ad.request(t, 1).Messages)
}
// The legacy role label still round-trips untouched when no persona is named.
// 未指名 persona 时,旧的角色标签仍原样往返。
if got.Agents[0].Role != "legacy-label" {
t.Fatalf("legacy role = %q, want it untouched when persona is omitted", got.Agents[0].Role)
}
}
// TestSpawnedSubAgentMemoryWriteScopeIsTargetPersona pins the write half of design/22
// §8-2: a memory the sub-agent's turn extracts is filed under the target persona, not
// under the role of the session that dispatched it. It also pins that a persona never
// widens the identity lane: in a restricted (read-only) env the same dispatch still
// writes nothing, even though it names a role.
// TestSpawnedSubAgentMemoryWriteScopeIsTargetPersona 钉住 design/22 §8-2 的写侧:子代理
// 回合提取出的记忆归档到目标 persona 下,而不是派发它的那个会话的角色下。它同时钉住
// persona 绝不放宽身份车道:在受限(只读)env 下,同一次派发即使指名了角色也依然什么
// 都不写。
func TestSpawnedSubAgentMemoryWriteScopeIsTargetPersona(t *testing.T) {
t.Parallel()
rig := newPersonaRig(t, func(string) bool { return true })
@@ -319,8 +309,8 @@ func TestSpawnedSubAgentMemoryWriteScopeIsTargetPersona(t *testing.T) {
t.Fatalf("extracted memory filed under %q, want the target persona %q", got, targetRole)
}
// Restricted env: the dispatching turn was a group chat (read-only). Naming a
// persona must not create a write path where the lane forbids one.
// 受限 env:派发回合来自群聊(只读)。指名 persona 绝不得在车道禁止的地方开出一条
// 写入路径。
restricted := loop.WithSessionEnv(context.Background(), loop.SessionEnv{
ActorEnv: restrict.ActorGroupMember,
MemWritable: false,