主人 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 改动前即未格式化,按铁律保留原样未动。
111 lines
4.0 KiB
Go
111 lines
4.0 KiB
Go
package loop
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
|
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/config"
|
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/agent"
|
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model"
|
|
)
|
|
|
|
// anthropicToolRoundSSE 是一个原生 Anthropic 流:它先思考,然后发出
|
|
// 一个带空文本部件的 tool_use 块 —— 正是拖垮旧循环(它只在
|
|
// Content 中寻找 JSON 信封)的那种形态。
|
|
const anthropicToolRoundSSE = `data: {"type":"message_start","message":{"model":"m","usage":{"input_tokens":10}}}
|
|
|
|
data: {"type":"content_block_start","index":0,"content_block":{"type":"thinking","thinking":""}}
|
|
|
|
data: {"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":"checking the clock"}}
|
|
|
|
data: {"type":"content_block_stop","index":0}
|
|
|
|
data: {"type":"content_block_start","index":1,"content_block":{"type":"tool_use","id":"toolu_1","name":"datetime","input":{}}}
|
|
|
|
data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"{\"timezone\":\"Asia/Shanghai\"}"}}
|
|
|
|
data: {"type":"content_block_stop","index":1}
|
|
|
|
data: {"type":"message_delta","delta":{"stop_reason":"tool_use"},"usage":{"output_tokens":20}}
|
|
|
|
data: {"type":"message_stop"}
|
|
|
|
`
|
|
|
|
// anthropicTextRoundSSE 是后续流:工具结果回灌后的一段
|
|
// 纯文本答案。
|
|
const anthropicTextRoundSSE = `data: {"type":"message_start","message":{"model":"m","usage":{"input_tokens":30}}}
|
|
|
|
data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}
|
|
|
|
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"现在是 10:34。"}}
|
|
|
|
data: {"type":"content_block_stop","index":0}
|
|
|
|
data: {"type":"message_delta","delta":{"stop_reason":"end_turn"},"usage":{"output_tokens":8}}
|
|
|
|
data: {"type":"message_stop"}
|
|
|
|
`
|
|
|
|
// TestRun_NativeToolCall_AnthropicAdapter_HTTP 端到端演练真实循环路径:
|
|
// 经由真实的 AnthropicChatAdapter 打向一个桩 /v1/messages
|
|
// 服务器:第 1 轮流式输出不带文本的 thinking + tool_use 块,
|
|
// 循环必须执行它并重新提问,第 2 轮必须携带 assistant tool_use 轮
|
|
// 以及关联的 tool_result,并返回模型的文本(而非空回复)。
|
|
func TestRun_NativeToolCall_AnthropicAdapter_HTTP(t *testing.T) {
|
|
t.Parallel()
|
|
var mu sync.Mutex
|
|
round := 0
|
|
var secondBody string
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
body, _ := io.ReadAll(r.Body)
|
|
mu.Lock()
|
|
round++
|
|
n := round
|
|
if n == 2 {
|
|
secondBody = string(body)
|
|
}
|
|
mu.Unlock()
|
|
w.Header().Set("Content-Type", "text/event-stream")
|
|
if n == 1 {
|
|
_, _ = io.WriteString(w, anthropicToolRoundSSE)
|
|
return
|
|
}
|
|
_, _ = io.WriteString(w, anthropicTextRoundSSE)
|
|
}))
|
|
t.Cleanup(srv.Close)
|
|
|
|
ad := model.NewAnthropicChat(config.LLMConfig{AnthropicBaseURL: srv.URL, AnthropicModel: "m"})
|
|
td := dispatcherWith(t, &mockTool{name: "datetime", result: map[string]any{"now": "2026-09-08 22:34:00"}})
|
|
ag, err := agent.New("你是昔涟。", td, nil, agent.WithModel(ad, nil))
|
|
if err != nil {
|
|
t.Fatalf("agent.New: %v", err)
|
|
}
|
|
l := New(Options{Agent: ag, 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 tool call was dropped)", res.Text)
|
|
}
|
|
if res.ToolCallsExecuted != 1 {
|
|
t.Fatalf("ToolCallsExecuted = %d, want 1", res.ToolCallsExecuted)
|
|
}
|
|
if secondBody == "" {
|
|
t.Fatal("adapter never made a second request (tool result was not fed back)")
|
|
}
|
|
for _, want := range []string{`"type":"tool_use"`, `"id":"toolu_1"`, `"type":"tool_result"`, `"tool_use_id":"toolu_1"`} {
|
|
if !strings.Contains(secondBody, want) {
|
|
t.Fatalf("second request missing %s:\n%s", want, secondBody)
|
|
}
|
|
}
|
|
}
|