Files
AskaEth 91e8d7a029 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 改动前即未格式化,按铁律保留原样未动。
2026-10-03 22:28:21 +08:00

246 lines
10 KiB
Go
Raw Permalink 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 loop
import (
"context"
"strings"
"testing"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/agent"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/memory"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model"
)
// 本文件从模型一侧覆盖 design/22 §7-5:一段其 assistant 消息
// 由不同角色说出的对话,必须在归属可见的情况下抵达 provider,
// 且不向消息正文添加任何一个字符,
// 也不打乱组装好的提示词的既定顺序。
// speakerMsg 构建一条标注了说出它的角色的对话消息,
// 与 gateway 的 transcript 播种标注 assistant 轮的方式完全一致
// (model.Message.SenderName 携带发言角色 id)。
func speakerMsg(role model.Role, personaID, text string) model.Message {
m := model.TextMessage(role, text)
m.SenderName = personaID
return m
}
// attributedLoop 构建一个双角色循环:指令说明是哪个角色作答,
// 名字解析器镜像 persona registry 的 id→显示名映射。
func attributedLoop(t *testing.T, ad model.Adapter) *Loop {
t.Helper()
xilian := personaAgent(t, "你是昔涟。", ad)
yuki := personaAgent(t, "你是由纪。", ad)
names := map[string]string{"xilian": "昔涟", "yuki": "由纪"}
return New(Options{
Agent: xilian,
Agents: map[string]*agent.Agent{"xilian": xilian, "yuki": yuki},
PersonaID: "xilian",
PersonaName: func(id string) string { return names[id] },
})
}
// seedMemory 为 personaID 写入一条按人格作用域的记忆条目,使该轮的
// 检索有东西可注入。
func seedMemory(t *testing.T, store *memory.Store, personaID, content string) {
t.Helper()
if _, err := store.Add(context.Background(), memory.MemoryItem{
PersonaID: personaID, Content: content,
Scope: memory.ScopePersona, Kind: memory.KindRelationship,
}); err != nil {
t.Fatalf("seed memory: %v", err)
}
}
// systemTexts 按顺序返回 req 中每条系统消息的文本。
func systemTexts(req model.Request) []string {
out := make([]string, 0, len(req.Messages))
for _, m := range req.Messages {
if m.Role == model.RoleSystem {
out = append(out, messageText(m))
}
}
return out
}
// indexOfSystemContaining 返回文本包含 want 的第一条消息的索引,
// 没有则返回 -1。
func indexOfSystemContaining(req model.Request, want string) int {
for i, m := range req.Messages {
if strings.Contains(messageText(m), want) {
return i
}
}
return -1
}
// TestAttribution_SpeakersReachTheModelRequest 是 design/22 §7-5 的
// 验收用例:会话现在以 Y 发言,而上下文中仍留有
// X 说过的话。适配器收到的请求必须 (a) 以 Y 自己的
// 指令开头,(b) 声明哪个角色在作答,(c) 标出发言人
// 何处变化,使「第 N 条是谁说的」可判定,且 (d) 保持每条消息正文
// 不动 —— 归属只在系统消息中传递。
func TestAttribution_SpeakersReachTheModelRequest(t *testing.T) {
t.Parallel()
ad := &mockAdapter{chat: []mgResp{{resp: model.Response{Content: "在呢"}}}}
l := attributedLoop(t, ad)
msgs := []model.Message{
speakerMsg(model.RoleAssistant, "xilian", "我记住了"),
speakerMsg(model.RoleAssistant, "xilian", "还有一件事"),
model.TextMessage(model.RoleUser, "继续"),
speakerMsg(model.RoleAssistant, "yuki", "我在呢"),
model.TextMessage(model.RoleUser, "你好"),
}
if _, err := l.Run(WithSessionEnv(context.Background(), roleEnv("yuki")), "sess-y", msgs); err != nil {
t.Fatalf("Run: %v", err)
}
req := lastChatRequest(t, ad)
// (a) 现在作答的角色仍然拥有第一条系统消息。
if got := messageText(req.Messages[0]); got != "你是由纪。" {
t.Fatalf("first system = %q, want the bound role's instructions", got)
}
sys := systemTexts(req)
// instructions + 角色上下文片段 + 每个发言人片段一条边界说明
// (X 的两个 assistant 轮是一个片段)。
if len(sys) != 4 {
t.Fatalf("system messages = %d (%q), want 4 (instructions + role context + two speaker boundaries)", len(sys), sys)
}
if sys[0] != "你是由纪。" {
t.Fatalf("system[0] = %q, want the instructions untouched at the head", sys[0])
}
if !strings.HasPrefix(sys[1], roleContextMarker) {
t.Fatalf("system[1] = %q, want the role context segment", sys[1])
}
for _, want := range []string{"「由纪」(yuki)", "不同角色相继发言"} {
if !strings.Contains(sys[1], want) {
t.Fatalf("role context %q missing %q", sys[1], want)
}
}
if strings.Contains(sys[1], speakerSwitchMarker) {
t.Fatalf("role context %q must not be mistaken for a boundary note", sys[1])
}
if !strings.HasPrefix(sys[2], speakerSwitchMarker) || !strings.Contains(sys[2], "「昔涟」(xilian)") || !strings.Contains(sys[2], "不是你说的") {
t.Fatalf("system[2] = %q, want X's boundary note marking its utterances as not yours", sys[2])
}
if !strings.HasPrefix(sys[3], speakerSwitchMarker) || !strings.Contains(sys[3], "来自你自己") || !strings.Contains(sys[3], "「由纪」(yuki)") {
t.Fatalf("system[3] = %q, want Y's own boundary note naming the answering role", sys[3])
}
// (c) 边界就位于它所覆盖的第一句话之前,且
// 第二句 X 的话继承同一发言人,无需第二条说明。
firstX := indexOfSystemContaining(req, "我记住了")
if firstX < 0 || req.Messages[firstX-1].Role != model.RoleSystem {
t.Fatalf("X's first utterance at %d is not preceded by a boundary note (%+v)", firstX, req.Messages)
}
if idx := indexOfSystemContaining(req, "「昔涟」(xilian)"); idx > firstX {
t.Fatalf("X boundary at %d must precede its utterance at %d", idx, firstX)
}
// 作答角色自己的历史由它自己的边界说明重新开启。
self := indexOfSystemContaining(req, "我在呢")
if self < 0 || !strings.Contains(messageText(req.Messages[self-1]), "来自你自己") {
t.Fatalf("Y's utterance is not preceded by its self boundary: %+v", req.Messages)
}
// (d) 没有任何正文被改写:用户文本保持逐字且不携带元数据,
// 且 assistant 自己的话也未变。
var users []string
for _, m := range req.Messages {
if m.Role != model.RoleUser {
continue
}
users = append(users, messageText(m))
}
if len(users) != 2 || users[0] != "继续" || users[1] != "你好" {
t.Fatalf("user bodies = %q, want the verbatim [继续 你好] (no system metadata in the user lane)", users)
}
for _, u := range users {
if strings.Contains(u, speakerSwitchMarker) || strings.Contains(u, roleContextMarker) {
t.Fatalf("user body %q carries system attribution metadata", u)
}
}
for _, body := range []string{"我记住了", "还有一件事", "我在呢"} {
if idx := indexOfSystemContaining(req, body); idx >= 0 && strings.Contains(messageText(req.Messages[idx]), "「") {
t.Fatalf("assistant body %q was rewritten with attribution", messageText(req.Messages[idx]))
}
}
}
// TestAssembleContext_AttributionKeepsBlockOrder 钉住放置规则:角色
// 声明被加在既定的前导块(时间、工作记忆、
// 记忆)之后、紧邻对话之前,因此记忆/时间块的顺序
// 与指令优先规则不受影响。
func TestAssembleContext_AttributionKeepsBlockOrder(t *testing.T) {
t.Parallel()
store := newTestMemoryStore(t)
seedMemory(t, store, "yuki", "她喜欢冰美式")
ad := &mockAdapter{chat: []mgResp{{resp: model.Response{Content: "嗯"}}}}
xilian := personaAgent(t, "你是昔涟。", ad)
yuki := personaAgent(t, "你是由纪。", ad)
l := New(Options{
Agent: xilian,
Agents: map[string]*agent.Agent{"xilian": xilian, "yuki": yuki},
PersonaID: "xilian",
PersonaName: func(id string) string { return map[string]string{"xilian": "昔涟", "yuki": "由纪"}[id] },
Retriever: memory.NewRetriever(store),
Timezone: "Asia/Shanghai",
})
msgs := []model.Message{
speakerMsg(model.RoleAssistant, "xilian", "我记住了"),
model.TextMessage(model.RoleUser, "继续"),
}
if _, err := l.Run(WithSessionEnv(context.Background(), roleEnv("yuki")), "sess-order", msgs); err != nil {
t.Fatalf("Run: %v", err)
}
req := lastChatRequest(t, ad)
timeIdx := indexOfSystemContaining(req, "当前时间")
memIdx := indexOfSystemContaining(req, "冰美式")
declIdx := indexOfSystemContaining(req, roleContextMarker)
noteIdx := indexOfSystemContaining(req, speakerSwitchMarker)
lastUser := -1
for i, m := range req.Messages {
if m.Role == model.RoleUser {
lastUser = i
}
}
if timeIdx < 0 || memIdx < 0 || declIdx < 0 || noteIdx < 0 || lastUser < 0 {
t.Fatalf("missing block: time=%d mem=%d decl=%d note=%d user=%d (%+v)", timeIdx, memIdx, declIdx, noteIdx, lastUser, req.Messages)
}
if !(memIdx < declIdx && declIdx < noteIdx && noteIdx < lastUser) {
t.Fatalf("block order = time:%d mem:%d decl:%d note:%d user:%d, want instructions < time < memory < declaration < notes < user", timeIdx, memIdx, declIdx, noteIdx, lastUser)
}
if got := messageText(req.Messages[0]); got != "你是由纪。" {
t.Fatalf("first system = %q, want the instructions untouched at the head", got)
}
}
// TestAttribution_UnlabeledConversationIsUntouched 钉住向后兼容的
// 那一半:什么都不标注的调用方(每条单角色路径、IM、backthink、
// 自主流程)拿到与从前完全一样的提示词,没有角色片段
// 也没有边界说明。
func TestAttribution_UnlabeledConversationIsUntouched(t *testing.T) {
t.Parallel()
ad := &mockAdapter{chat: []mgResp{{resp: model.Response{Content: "done"}}}}
l := attributedLoop(t, ad)
msgs := []model.Message{
model.TextMessage(model.RoleAssistant, "我记住了"),
model.TextMessage(model.RoleUser, "继续"),
}
if _, err := l.Run(WithSessionEnv(context.Background(), roleEnv("yuki")), "sess-plain", msgs); err != nil {
t.Fatalf("Run: %v", err)
}
req := lastChatRequest(t, ad)
if len(req.Messages) != len(msgs)+1 {
t.Fatalf("messages = %d, want %d (instructions + conversation, nothing added)", len(req.Messages), len(msgs)+1)
}
for _, s := range systemTexts(req) {
if strings.Contains(s, roleContextMarker) || strings.Contains(s, speakerSwitchMarker) {
t.Fatalf("unlabeled conversation got attribution %q", s)
}
}
}