docs(runtime): internal/runtime/loop 44 个文件注释改中文(逐行 1:1,代码零字节改动)
主人 2026-10-03 拍板「注释一律中文」;本模块按长期分批协议(一个模块一次派工)执行。 注释行数 1329 → 1329 逐行 1:1(含中文 33 → 1304);行尾注释 5 条一并译。 校验:机械校验(非注释改动行)= 空;独立 Go AST 程序(剥注释复渲)证明 44/44 文件代码全等; 全树 go test ./... 58 包 ok / 0 FAIL;交叉编译绿。 gofmt:capacity_loop_test.go / flow.go 改动前即未格式化,按铁律保留原样未动。
This commit is contained in:
1 parent
355f81d04a
commit
91e8d7a029
44 files changed
+1315
-1315
No files matched your search
@@ -11,13 +11,13 @@ import (
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model"
|
||||
)
|
||||
|
||||
// This file locks the multi-character behavior of design/22 §4.2: prompt, memory
|
||||
// scope, emotion persona and reasoning knobs must all be resolved from the SAME
|
||||
// per-turn role, so a session can never answer with role Y's voice but role X's
|
||||
// memory (design/22 §5 风险表 "提示词/情绪/记忆错配").
|
||||
// 本文件锁定 design/22 §4.2 的多角色行为:提示词、记忆
|
||||
// 作用域、情绪人格与推理旋钮必须全部从同一个
|
||||
// 按 turn 的角色解析,使会话绝不以角色 Y 的声音作答却用角色 X 的
|
||||
// 记忆(design/22 §5 风险表 "提示词/情绪/记忆错配")。
|
||||
|
||||
// personaAgent builds an agent whose instructions name the role, so a test can tell
|
||||
// which pool entry the loop resolved from the leading system message.
|
||||
// personaAgent 构建一个指令中指明角色的 agent,使测试能从
|
||||
// 前导系统消息看出循环解析了哪个池条目。
|
||||
func personaAgent(t *testing.T, instructions string, ad model.Adapter) *agent.Agent {
|
||||
t.Helper()
|
||||
ag, err := agent.New(instructions, nil, nil, agent.WithModel(ad, nil))
|
||||
@@ -27,13 +27,13 @@ func personaAgent(t *testing.T, instructions string, ad model.Adapter) *agent.Ag
|
||||
return ag
|
||||
}
|
||||
|
||||
// roleEnv is the environment a session resolver produces for personaID: full-access
|
||||
// owner identity (the identity lane is unchanged) plus the explicitly bound role.
|
||||
// roleEnv 是会话解析器为 personaID 产出的环境:全权限
|
||||
// owner 身份(身份车道不变)加上显式绑定的角色。
|
||||
func roleEnv(personaID string) SessionEnv {
|
||||
return SessionEnv{ActorEnv: restrict.ActorOwner, MemWritable: true, PersonaID: personaID}
|
||||
}
|
||||
|
||||
// runPersonaTurn drives one turn for sessionID under env and fails the test on error.
|
||||
// runPersonaTurn 在 env 下为 sessionID 驱动一个 turn,出错即让测试失败。
|
||||
func runPersonaTurn(t *testing.T, l *Loop, env SessionEnv, sessionID, text string) *Result {
|
||||
t.Helper()
|
||||
res, err := l.Run(WithSessionEnv(context.Background(), env), sessionID, []model.Message{model.TextMessage(model.RoleUser, text)})
|
||||
@@ -43,7 +43,7 @@ func runPersonaTurn(t *testing.T, l *Loop, env SessionEnv, sessionID, text strin
|
||||
return res
|
||||
}
|
||||
|
||||
// lastChatRequest returns the newest request the non-streaming path recorded.
|
||||
// lastChatRequest 返回非流式路径记录的最新请求。
|
||||
func lastChatRequest(t *testing.T, ad *mockAdapter) model.Request {
|
||||
t.Helper()
|
||||
if len(ad.reqs) == 0 {
|
||||
@@ -52,7 +52,7 @@ func lastChatRequest(t *testing.T, ad *mockAdapter) model.Request {
|
||||
return ad.reqs[len(ad.reqs)-1]
|
||||
}
|
||||
|
||||
// requestContains reports whether any message of req carries want.
|
||||
// requestContains 报告 req 是否有任何消息携带 want。
|
||||
func requestContains(req model.Request, want string) bool {
|
||||
for _, m := range req.Messages {
|
||||
if strings.Contains(messageText(m), want) {
|
||||
@@ -62,11 +62,11 @@ func requestContains(req model.Request, want string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// TestTurnResolvesSystemPromptFromSessionRole is design/22 acceptance 3: switching a
|
||||
// session's role switches the identity the model is given. The first system message
|
||||
// must be the bound role's prompt — that is the whole point of the per-role agent
|
||||
// pool — while an unbound session and an unknown role both stay on the default role
|
||||
// instead of failing the turn.
|
||||
// TestTurnResolvesSystemPromptFromSessionRole 是 design/22 验收 3:切换
|
||||
// 会话角色即切换给模型的身份。第一条系统消息
|
||||
// 必须是所绑定角色的提示词 —— 这正是按角色 agent
|
||||
// 池的全部意义 —— 而未绑定的会话与未知角色都留在默认
|
||||
// 角色上,而不是让该 turn 失败。
|
||||
func TestTurnResolvesSystemPromptFromSessionRole(t *testing.T) {
|
||||
t.Parallel()
|
||||
ad := &mockAdapter{}
|
||||
@@ -84,26 +84,26 @@ func TestTurnResolvesSystemPromptFromSessionRole(t *testing.T) {
|
||||
t.Fatalf("first system message = %q, want the bound role Y's prompt", got)
|
||||
}
|
||||
|
||||
// A session with no explicit binding inherits the loop's configured default.
|
||||
// 没有显式绑定的会话继承循环配置的默认角色。
|
||||
runPersonaTurn(t, l, roleEnv(""), "web", "你好")
|
||||
if got := messageText(lastChatRequest(t, ad).Messages[0]); got != "你是角色X。" {
|
||||
t.Fatalf("unbound session prompt = %q, want the default role X's prompt", got)
|
||||
}
|
||||
|
||||
// A binding to a role with no pooled agent (deleted/unknown persona) degrades to
|
||||
// the default agent rather than erroring.
|
||||
// 绑定到一个没有池中 agent 的角色(已删除/未知人格)会降级到
|
||||
// 默认 agent,而不是报错。
|
||||
runPersonaTurn(t, l, roleEnv("nobody"), "web:z1", "你好")
|
||||
if got := messageText(lastChatRequest(t, ad).Messages[0]); got != "你是角色X。" {
|
||||
t.Fatalf("unknown role prompt = %q, want the default role X's prompt", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestPersonaScopeMemoryIsolationAndExtraction is design/22 acceptance 2 and 5: the
|
||||
// memory namespace follows the turn's role, so two sessions bound to different roles
|
||||
// each recall only their own role's memories while two sessions of the SAME role see
|
||||
// the same memories (口径 1: memory is persona-scoped, not session-scoped); and the
|
||||
// extracted item is filed under the role that produced the turn — the regression
|
||||
// guard for the removal of the extractor's construction-time persona.
|
||||
// TestPersonaScopeMemoryIsolationAndExtraction 是 design/22 验收 2 与 5:
|
||||
// 记忆命名空间跟随该 turn 的角色,因此绑定到不同角色的两个会话
|
||||
// 各自只召回自己角色的记忆,而同一角色的两个会话看到
|
||||
// 相同的记忆(口径 1: 记忆按人格作用域,而非按会话作用域);且
|
||||
// 提取出的条目归到产出该 turn 的角色之下 —— 这也是
|
||||
// 移除 extractor 构造期人格的回归护栏。
|
||||
func TestPersonaScopeMemoryIsolationAndExtraction(t *testing.T) {
|
||||
t.Parallel()
|
||||
store := newTestMemoryStore(t)
|
||||
@@ -116,15 +116,15 @@ func TestPersonaScopeMemoryIsolationAndExtraction(t *testing.T) {
|
||||
PersonaID: "X",
|
||||
Memory: store,
|
||||
Retriever: memory.NewRetriever(store),
|
||||
// The extractor is built WITHOUT a persona, exactly as cmd/cyrene builds it:
|
||||
// the loop is responsible for stamping the turn's role onto each item.
|
||||
// 该 extractor 构建时不带 persona,与 cmd/cyrene 的构建方式完全一致:
|
||||
// 循环负责把该 turn 的角色打到每个条目上。
|
||||
Extractor: memory.NewManualExtractor("", memoryOwnerRoleIDForTest),
|
||||
})
|
||||
|
||||
runPersonaTurn(t, l, roleEnv("X"), "web:x1", "记住 我喜欢喝乌龙茶")
|
||||
runPersonaTurn(t, l, roleEnv("Y"), "web:y1", "记住 我讨厌香菜")
|
||||
|
||||
// Acceptance 5: each extracted memory is filed under the role of its turn.
|
||||
// 验收 5:每条提取出的记忆都归到其 turn 的角色之下。
|
||||
byContent := map[string]string{}
|
||||
for _, it := range listMemories(t, store) {
|
||||
byContent[it.Content] = it.PersonaID
|
||||
@@ -136,7 +136,7 @@ func TestPersonaScopeMemoryIsolationAndExtraction(t *testing.T) {
|
||||
t.Fatalf("memory extracted in a Y turn is filed under %q, want Y (items=%v)", got, byContent)
|
||||
}
|
||||
|
||||
// Acceptance 2a: the X session recalls X's memory and never Y's.
|
||||
// 验收 2a:X 会话召回 X 的记忆,绝不召回 Y 的。
|
||||
runPersonaTurn(t, l, roleEnv("X"), "web:x1", "乌龙茶怎么样")
|
||||
req := lastChatRequest(t, ad)
|
||||
if !requestContains(req, "我喜欢喝乌龙茶") {
|
||||
@@ -146,14 +146,14 @@ func TestPersonaScopeMemoryIsolationAndExtraction(t *testing.T) {
|
||||
t.Fatalf("X's turn leaked Y's memory: %+v", req.Messages)
|
||||
}
|
||||
|
||||
// Acceptance 2b: another session of the SAME role sees it too (memory is
|
||||
// persona-scoped, so a new session does not start from a blank memory).
|
||||
// 验收 2b:同一角色的另一个会话也能看到它(记忆按
|
||||
// 人格作用域,因此新会话不是从空白记忆开始)。
|
||||
runPersonaTurn(t, l, roleEnv("X"), "web:x2", "乌龙茶怎么样")
|
||||
if !requestContains(lastChatRequest(t, ad), "我喜欢喝乌龙茶") {
|
||||
t.Fatalf("a second X session did not see X's memory: %+v", lastChatRequest(t, ad).Messages)
|
||||
}
|
||||
|
||||
// Acceptance 2c: the Y session recalls only Y's memory.
|
||||
// 验收 2c:Y 会话只召回 Y 的记忆。
|
||||
runPersonaTurn(t, l, roleEnv("Y"), "web:y1", "香菜怎么样")
|
||||
req = lastChatRequest(t, ad)
|
||||
if !requestContains(req, "我讨厌香菜") {
|
||||
@@ -164,11 +164,11 @@ func TestPersonaScopeMemoryIsolationAndExtraction(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// memoryOwnerRoleIDForTest mirrors the composition root's authoring role id, so the
|
||||
// test builds the extractor the same way production does.
|
||||
// memoryOwnerRoleIDForTest 镜像组合根的作者角色 id,使该
|
||||
// 测试以与生产相同的方式构建 extractor。
|
||||
const memoryOwnerRoleIDForTest = "owner"
|
||||
|
||||
// listMemories returns every stored memory item.
|
||||
// listMemories 返回每条已存记忆条目。
|
||||
func listMemories(t *testing.T, store *memory.Store) []memory.MemoryItem {
|
||||
t.Helper()
|
||||
items, err := store.List(context.Background())
|
||||
@@ -178,9 +178,9 @@ func listMemories(t *testing.T, store *memory.Store) []memory.MemoryItem {
|
||||
return items
|
||||
}
|
||||
|
||||
// frameAdapter streams one thinking frame and one text frame, so a test can assert
|
||||
// both the request the loop built (thinking knobs) and what it broadcast
|
||||
// (showToUser gate) on the streaming path.
|
||||
// frameAdapter 流式发出一个思考帧与一个文本帧,使测试能在
|
||||
// 流式路径上同时断言循环构建的请求(思考旋钮)与它广播的
|
||||
// 内容(showToUser 门)。
|
||||
type frameAdapter struct {
|
||||
mockAdapter
|
||||
}
|
||||
@@ -194,11 +194,11 @@ func (f *frameAdapter) ChatStream(_ context.Context, req model.Request, on func(
|
||||
return model.Response{Content: "答案"}, nil
|
||||
}
|
||||
|
||||
// TestThinkingKnobsFollowTheTurnRole locks the second half of design/22 §4.2: the
|
||||
// reasoning budget / effort / thinking-broadcast switch that batch A resolved once
|
||||
// at wiring time must now follow the turn's role, so a session bound to role Y runs
|
||||
// with Y's budget and Y's broadcast switch instead of the default role's. The
|
||||
// configured options stay the fallback for an unknown role.
|
||||
// TestThinkingKnobsFollowTheTurnRole 钉住 design/22 §4.2 的后半:批次 A
|
||||
// 在接线时一次性解析的推理预算 / 强度 / 思考广播开关,现在
|
||||
// 必须跟随该 turn 的角色,因此绑定到角色 Y 的会话以
|
||||
// Y 的预算与 Y 的广播开关运行,而不是默认角色的。配置的
|
||||
// 选项仍是未知角色的回退值。
|
||||
func TestThinkingKnobsFollowTheTurnRole(t *testing.T) {
|
||||
t.Parallel()
|
||||
ad := &frameAdapter{}
|
||||
@@ -221,8 +221,8 @@ func TestThinkingKnobsFollowTheTurnRole(t *testing.T) {
|
||||
PersonaKnobs: func(personaID string) (LoopKnobs, bool) { k, ok := knobs[personaID]; return k, ok },
|
||||
})
|
||||
|
||||
// Role X: its budget reaches the request and its thinking frames are broadcast
|
||||
// (showToUser unset = the documented "broadcast as usual").
|
||||
// 角色 X:其预算到达请求,其思考帧被广播
|
||||
// (showToUser 未设置 = 文档所述的「照常广播」)。
|
||||
runPersonaTurn(t, l, roleEnv("X"), "web:x1", "你好")
|
||||
req := ad.streamReqs[len(ad.streamReqs)-1]
|
||||
if req.Thinking == nil || req.Thinking.BudgetTokens != 100 {
|
||||
@@ -235,9 +235,9 @@ func TestThinkingKnobsFollowTheTurnRole(t *testing.T) {
|
||||
t.Fatalf("X's thinking frames were withheld: %+v", events)
|
||||
}
|
||||
|
||||
// Role Y: Y configures no budget and hides thinking frames, so Y's request must
|
||||
// carry NO thinking directive (the default role's budget must not leak in) and
|
||||
// only the text frame may reach the client.
|
||||
// 角色 Y:Y 未配置预算并隐藏思考帧,因此 Y 的请求必须
|
||||
// 不携带任何思考指令(默认角色的预算不得渗入),且
|
||||
// 只有文本帧可以抵达客户端。
|
||||
events = nil
|
||||
runPersonaTurn(t, l, roleEnv("Y"), "web:y1", "你好")
|
||||
req = ad.streamReqs[len(ad.streamReqs)-1]
|
||||
@@ -254,7 +254,7 @@ func TestThinkingKnobsFollowTheTurnRole(t *testing.T) {
|
||||
t.Fatal("Y's text frames were suppressed too, want the answer to still stream")
|
||||
}
|
||||
|
||||
// An unknown role keeps the loop's configured defaults instead of zeroing them.
|
||||
// 未知角色保持循环配置的默认值,而不是把它们清零。
|
||||
events = nil
|
||||
runPersonaTurn(t, l, roleEnv("nobody"), "web:z1", "你好")
|
||||
req = ad.streamReqs[len(ad.streamReqs)-1]
|
||||
@@ -266,7 +266,7 @@ func TestThinkingKnobsFollowTheTurnRole(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// hasThinkingFrame reports whether the broadcast set carried a reasoning frame.
|
||||
// hasThinkingFrame 报告广播集是否携带了推理帧。
|
||||
func hasThinkingFrame(events []model.StreamEvent) bool {
|
||||
for _, ev := range events {
|
||||
if isThinkingKind(ev.Kind) {
|
||||
|
||||
Reference in new issue
Block a user