package loop import ( "context" "testing" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/tool" ) // 本文件锁定切片 KB1 的 loop 侧一半:**本回合角色必须发布给工具**。 // // 复现的形状:模型省略 knowledge 工具的可选参数 persona,工具此前把空角色透给 // 记忆存储,后者以 ErrMissingPersona 硬失败 —— 模型既不知道该填哪个角色 id, // 参数说明还写着 optional,于是连撞同一堵墙。修复是循环在 Run 顶部把 personaFor // 解析出的角色经 tool.WithTurnPersona 发布到本轮 ctx 上,工具据此查到「以谁的 // 名义」检索。 // // 断言走真路径:真 Run → 真 tool.Dispatcher.Invoke → 真工具 Execute 读 ctx。 // 工具看到的值必须与提示词/记忆同源(personaFor):会话环境有显式角色时取它, // 否则取循环配置的默认角色。 // personaProbeTool 记录 Execute 时在 ctx 上读到的本回合角色。 type personaProbeTool struct { got []string } func (p *personaProbeTool) Name() string { return "persona_probe" } func (p *personaProbeTool) Description() string { return "记录本回合角色" } func (p *personaProbeTool) Parameters() (map[string]any, error) { return map[string]any{"type": "object"}, nil } func (p *personaProbeTool) Execute(ctx context.Context, _ any) (any, error) { p.got = append(p.got, tool.TurnPersonaFrom(ctx)) return "ok", nil } // runPersonaProbe 跑一个「一次工具调用 + 收尾」的回合,返回工具在 ctx 上读到的角色。 func runPersonaProbe(t *testing.T, ctx context.Context, configured string) string { t.Helper() call := `{"tool_calls":[{"name":"persona_probe","args":{}}]}` ad := &mockAdapter{chat: []mgResp{ {resp: model.Response{Content: call}}, {resp: model.Response{Content: "收尾"}}, }} probe := &personaProbeTool{} td := dispatcherWith(t, probe) a := newTestAgent(t, ad, td) l := New(Options{Agent: a, MaxRounds: 8, PersonaID: configured}) if _, err := l.Run(ctx, "sess", []model.Message{model.TextMessage(model.RoleUser, "查一下")}); err != nil { t.Fatalf("Run: %v", err) } if len(probe.got) != 1 { t.Fatalf("工具执行次数 = %d, want 1", len(probe.got)) } return probe.got[0] } // TestRun_PublishesTurnPersonaToTools 断言:会话环境给了显式角色时,工具看到的是 // 那个角色(而不是循环配置的默认角色)。 func TestRun_PublishesTurnPersonaToTools(t *testing.T) { t.Parallel() env := OwnerEnv() env.PersonaID = "persona-x" ctx := WithSessionEnv(context.Background(), env) if got := runPersonaProbe(t, ctx, "default-persona"); got != "persona-x" { t.Fatalf("工具读到的本回合角色 = %q, want persona-x(应与提示词/记忆同源)", got) } } // TestRun_PublishesConfiguredPersonaWhenEnvEmpty 断言:会话环境没给角色 // (channelbridge / backthink 等既有调用方)时,工具看到的是循环配置的默认角色 —— // 正是「空角色不许透给下游」的兜底。 func TestRun_PublishesConfiguredPersonaWhenEnvEmpty(t *testing.T) { t.Parallel() if got := runPersonaProbe(t, context.Background(), "default-persona"); got != "default-persona" { t.Fatalf("工具读到的本回合角色 = %q, want default-persona", got) } }