主人 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 改动前即未格式化,按铁律保留原样未动。
132 lines
4.8 KiB
Go
132 lines
4.8 KiB
Go
package loop
|
||
|
||
import (
|
||
"context"
|
||
"testing"
|
||
|
||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/agent"
|
||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model"
|
||
)
|
||
|
||
// nativeDatetimeCall 是一个 provider 原生工具调用响应:适配器把
|
||
// 调用放在 Response.ToolCalls(而非 Content)中,这正是
|
||
// 思考模型在 datetime 工具被广告时对「现在几点」返回的
|
||
// 形态。Content 为空,因此忽略 ToolCalls 的循环会产出
|
||
// 静默的空回复。
|
||
func nativeDatetimeCall() model.Response {
|
||
return model.Response{
|
||
Model: "m",
|
||
Content: "",
|
||
ToolCalls: []model.ToolCall{{
|
||
ID: "call_native_1",
|
||
Name: "datetime",
|
||
Args: map[string]any{"timezone": "Asia/Shanghai"},
|
||
}},
|
||
}
|
||
}
|
||
|
||
// assertNativeFeedBack 检查 req 携带 assistant tool_use 轮以及
|
||
// 与 toolCallID 关联的工具结果。
|
||
func assertNativeFeedBack(t *testing.T, req model.Request, toolCallID string) {
|
||
t.Helper()
|
||
for i := 0; i < len(req.Messages)-1; i++ {
|
||
m := req.Messages[i]
|
||
if m.Role != model.RoleAssistant || len(m.Content) == 0 {
|
||
continue
|
||
}
|
||
if m.Content[0].Type != model.ContentTypeToolCall || m.Content[0].ToolCallID != toolCallID {
|
||
continue
|
||
}
|
||
next := req.Messages[i+1]
|
||
if next.Role != model.RoleTool {
|
||
t.Fatalf("message after assistant tool_use has role %q, want tool", next.Role)
|
||
}
|
||
if next.ToolCallID != toolCallID {
|
||
t.Fatalf("tool result ToolCallID = %q, want %q", next.ToolCallID, toolCallID)
|
||
}
|
||
if len(next.Content) == 0 || next.Content[0].Type != model.ContentTypeToolResult {
|
||
t.Fatalf("tool result content = %+v, want a tool_result part", next.Content)
|
||
}
|
||
return
|
||
}
|
||
t.Fatalf("no native assistant tool_use + tool_result pair for %q in %+v", toolCallID, req.Messages)
|
||
}
|
||
|
||
// TestRun_NativeToolCall_Streaming 覆盖真实聊天路径(设置 OnStream 时
|
||
// 循环驱动 ChatStream):Content 为空的 provider 原生工具调用
|
||
// 必须被执行并以原生 tool_use + tool_result 回灌,且
|
||
// run 必须以模型的后续文本结束,而不是空回复。
|
||
func TestRun_NativeToolCall_Streaming(t *testing.T) {
|
||
t.Parallel()
|
||
ad := &mockAdapter{stream: []mgResp{
|
||
{resp: nativeDatetimeCall()},
|
||
{resp: model.Response{Model: "m", Content: "现在是 10:34。"}},
|
||
}}
|
||
td := dispatcherWith(t, &mockTool{name: "datetime", result: map[string]any{"now": "2026-09-08 22:34:00"}})
|
||
a := newTestAgent(t, ad, td)
|
||
l := New(Options{Agent: a, MaxRounds: 8, OnStream: func(model.StreamEvent) {}})
|
||
|
||
res, err := l.Run(context.Background(), "sess", []model.Message{model.TextMessage(model.RoleUser, "瞅瞅当前系统时间是多少")})
|
||
if err != nil {
|
||
t.Fatalf("Run: %v", err)
|
||
}
|
||
if res.Text != "现在是 10:34。" {
|
||
t.Fatalf("Text = %q, want the follow-up answer (empty means the native call was dropped)", res.Text)
|
||
}
|
||
if res.Steps != 2 {
|
||
t.Fatalf("Steps = %d, want 2", res.Steps)
|
||
}
|
||
if res.ToolCallsExecuted != 1 {
|
||
t.Fatalf("ToolCallsExecuted = %d, want 1", res.ToolCallsExecuted)
|
||
}
|
||
if len(ad.streamReqs) < 2 {
|
||
t.Fatalf("stream requests = %d, want >= 2", len(ad.streamReqs))
|
||
}
|
||
assertNativeFeedBack(t, ad.streamReqs[1], "call_native_1")
|
||
}
|
||
|
||
// TestRun_NativeToolCall_NonStreaming 覆盖静音/内部路径,其中
|
||
// 循环委托给 agent.RunStep(adapter.Chat),后者必须解码同一个
|
||
// 原生工具调用。
|
||
func TestRun_NativeToolCall_NonStreaming(t *testing.T) {
|
||
t.Parallel()
|
||
ad := &mockAdapter{chat: []mgResp{
|
||
{resp: nativeDatetimeCall()},
|
||
{resp: model.Response{Model: "m", Content: "现在是 10:34。"}},
|
||
}}
|
||
td := dispatcherWith(t, &mockTool{name: "datetime", result: map[string]any{"now": "2026-09-08 22:34:00"}})
|
||
a := newTestAgent(t, ad, td)
|
||
l := New(Options{Agent: a, MaxRounds: 8})
|
||
|
||
res, err := l.Run(context.Background(), "sess", []model.Message{model.TextMessage(model.RoleUser, "几点了")})
|
||
if err != nil {
|
||
t.Fatalf("Run: %v", err)
|
||
}
|
||
if res.Text != "现在是 10:34。" || res.ToolCallsExecuted != 1 {
|
||
t.Fatalf("Text = %q, ToolCallsExecuted = %d; want follow-up text + 1 call", res.Text, res.ToolCallsExecuted)
|
||
}
|
||
if len(ad.reqs) < 2 {
|
||
t.Fatalf("chat requests = %d, want >= 2", len(ad.reqs))
|
||
}
|
||
assertNativeFeedBack(t, ad.reqs[1], "call_native_1")
|
||
}
|
||
|
||
// TestExtractToolCalls_PrefersNative 钉住解码器契约:provider 原生
|
||
// ToolCalls 优先于 Content 信封并保留 provider id,使
|
||
// 回灌能关联该结果。
|
||
func TestExtractToolCalls_PrefersNative(t *testing.T) {
|
||
t.Parallel()
|
||
resp := nativeDatetimeCall()
|
||
resp.Content = `{"tool_calls":[{"name":"legacy","args":{}}]}`
|
||
calls, ok := agent.ExtractToolCalls(resp)
|
||
if !ok || len(calls) != 1 {
|
||
t.Fatalf("ExtractToolCalls = (%+v, %v), want 1 native call", calls, ok)
|
||
}
|
||
if calls[0].ID != "call_native_1" || calls[0].Name != "datetime" {
|
||
t.Fatalf("native call = %+v, want id=call_native_1 name=datetime", calls[0])
|
||
}
|
||
if calls[0].Args["timezone"] != "Asia/Shanghai" {
|
||
t.Fatalf("native args = %+v, want timezone=Asia/Shanghai", calls[0].Args)
|
||
}
|
||
}
|