Files
Agent-System-Backend/internal/ctxasm/replay_test.go
T
weichen 43297795f5
ci / vet-and-test (pull_request) Successful in 48s
Keep original materials and cleared results reachable across turns
Why: Real-run before/after evidence (phase01 card) showed later turns never
re-read the uploaded source (0 original reads in plan-followup) because the
attachment only existed in the current turn's injection and the context window
hard default was 32000 chars - a 25k-char transcript tripped the clear-on-first
request, replacing old tool results with a pointer the model has no tool to
follow. MiniMax is an interleaved-thinking model whose docs require <think>
blocks in assistant history; dropping them degrades later turns.

Evidence: SELF - new tests TestThinkReplayWrapsAssistantHistory,
TestContextWindowScalesFromTokens, TestThinkReplayEnabled,
TestPersistInputTextsAndListWorkspaceInputs; go test ./internal/... passes;
live before/after comparison on the golden task set recorded on the phase02
task card per the new policy clause.

Impact: models.chat.contextWindow/outputReserve/contextBuffer (tokens,
defaults 128k/16k/4k) drive the in-run clear threshold, converted to chars;
cleared tool results spill to <sandbox>/.agent/results with readable paths;
attachment parse text persists as inputs/<name>.md and the env section lists
all workspace inputs every turn; context.thinkReplay (auto|on|off, auto =
MiniMax) replays thinking blocks into request-side assistant history only.
2026-10-09 14:28:39 +00:00

181 lines
7.5 KiB
Go

package ctxasm
import (
"encoding/json"
"strings"
"testing"
"unicode/utf8"
agentllm "agent-native/runtime/llm"
core "agent-system-core"
)
func assistantWith(id string, parts ...core.Part) core.Message {
return core.Message{ID: id, Role: core.RoleAssistant, Parts: parts}
}
func assembleHistory(hist []core.Message, resolve ArtifactResolver) []agentllm.Message {
msgs, _ := AssembleInput(core.AgentProfile{}, core.EffectiveSettings{}, hist, fakeUser("继续"), nil, nil, nil, nil, nil, resolve, AssembleOptions{})
if len(msgs) == 0 {
return nil
}
return msgs[:len(msgs)-1]
}
func joinedContent(msgs []agentllm.Message) string {
var b strings.Builder
for _, m := range msgs {
b.WriteString(m.Content)
b.WriteByte('\n')
}
return b.String()
}
func TestAssembleInputReplaysToolCallsNatively(t *testing.T) {
hist := []core.Message{assistantWith("m2",
core.Part{Type: core.PartText, Text: "好的,开始生成。"},
core.Part{Type: core.PartToolCall, ToolCall: &core.ToolCallPart{ID: "call_1", Name: "run_python", Arguments: `{"script":"print(1)"}`}},
core.Part{Type: core.PartToolResult, ToolResult: &core.ToolResultPart{CallID: "call_1", Status: "ok", Summary: "完成"}},
core.Part{Type: core.PartText, Text: "纪要已整理。"},
core.Part{Type: core.PartArtifactRef, Artifact: &core.ArtifactRef{ArtifactID: "art_1", Name: "纪要.md", Kind: "document"}},
)}
resolve := func(id core.ID) (core.Artifact, []byte, bool) {
if id == "art_1" {
return core.Artifact{ID: id, Name: "纪要.md"}, []byte("关键结论。"), true
}
return core.Artifact{}, nil, false
}
msgs := assembleHistory(hist, resolve)
if len(msgs) != 3 {
t.Fatalf("replay messages = %d, want assistant + tool + assistant, got %#v", len(msgs), msgs)
}
if msgs[0].Role != agentllm.RoleAssistant || len(msgs[0].ToolCalls) != 1 || msgs[0].ToolCalls[0].Name != "run_python" {
t.Fatalf("first message = %+v, want assistant tool call run_python", msgs[0])
}
if !strings.Contains(msgs[0].Content, "好的,开始生成。") || strings.Contains(msgs[0].Content, "[调用工具") {
t.Fatalf("preamble must stay text and not flatten the call: %q", msgs[0].Content)
}
if msgs[1].Role != agentllm.RoleTool || msgs[1].ToolCallID != "call_1" || msgs[1].Name != "run_python" || msgs[1].Content != "完成" || msgs[1].IsError {
t.Fatalf("tool result = %+v, want paired ok summary", msgs[1])
}
if msgs[2].Role != agentllm.RoleAssistant || len(msgs[2].ToolCalls) != 0 {
t.Fatalf("final segment = %+v, want plain assistant text", msgs[2])
}
if !strings.Contains(msgs[2].Content, "纪要已整理。") || !strings.Contains(msgs[2].Content, "关键结论。") {
t.Fatalf("final segment missing reply or artifact body: %q", msgs[2].Content)
}
if strings.Contains(joinedContent(msgs), "[调用工具") || strings.Contains(joinedContent(msgs), "[工具结果") {
t.Fatalf("flattened markers leaked into replay:\n%s", joinedContent(msgs))
}
}
func TestAssembleInputPairsMissingToolResult(t *testing.T) {
hist := []core.Message{assistantWith("m3",
core.Part{Type: core.PartToolCall, ToolCall: &core.ToolCallPart{ID: "call_x", Name: "run_python", Arguments: `{}`}},
)}
msgs := assembleHistory(hist, nil)
if len(msgs) != 2 || msgs[1].Role != agentllm.RoleTool {
t.Fatalf("messages = %#v, want assistant plus synthetic tool result", msgs)
}
if msgs[1].ToolCallID != "call_x" || !msgs[1].IsError || !strings.Contains(msgs[1].Content, "无结果:运行中断") {
t.Fatalf("synthetic result = %+v", msgs[1])
}
}
func TestAssembleInputDropsOrphanToolResult(t *testing.T) {
hist := []core.Message{assistantWith("m4",
core.Part{Type: core.PartToolResult, ToolResult: &core.ToolResultPart{CallID: "ghost", Status: "ok", Summary: "不该出现"}},
core.Part{Type: core.PartText, Text: "只有正文。"},
)}
msgs := assembleHistory(hist, nil)
for _, m := range msgs {
if m.Role == agentllm.RoleTool || strings.Contains(m.Content, "不该出现") {
t.Fatalf("orphan result leaked: %+v", m)
}
}
if len(msgs) != 1 || !strings.Contains(msgs[0].Content, "只有正文。") {
t.Fatalf("text-only replay = %#v", msgs)
}
}
func TestAssembleInputFillsEmptyToolCallID(t *testing.T) {
hist := []core.Message{assistantWith("m2",
core.Part{Type: core.PartToolCall, ToolCall: &core.ToolCallPart{Name: "run_shell", Arguments: `{"cmd":"ls"}`}},
core.Part{Type: core.PartToolResult, ToolResult: &core.ToolResultPart{Status: "ok", Summary: "a.txt"}},
)}
msgs := assembleHistory(hist, nil)
if len(msgs) != 2 {
t.Fatalf("messages = %d, want 2", len(msgs))
}
id := msgs[0].ToolCalls[0].ID
if id != "hist_m2_0" || msgs[1].ToolCallID != id || msgs[1].Content != "a.txt" {
t.Fatalf("empty id pairing = call %q result %+v", id, msgs[1])
}
}
func TestAssembleInputCompactsLongToolArguments(t *testing.T) {
script := strings.Repeat("甲", 5000)
raw, err := json.Marshal(map[string]string{"script": script})
if err != nil {
t.Fatal(err)
}
hist := []core.Message{assistantWith("m5",
core.Part{Type: core.PartToolCall, ToolCall: &core.ToolCallPart{ID: "call_long", Name: "run_python", Arguments: string(raw)}},
core.Part{Type: core.PartToolResult, ToolResult: &core.ToolResultPart{CallID: "call_long", Status: "ok", Summary: "ok"}},
)}
msgs := assembleHistory(hist, nil)
args := msgs[0].ToolCalls[0].Arguments
if utf8.RuneCountInString(args) > 4000 {
t.Fatalf("arguments still %d runes", utf8.RuneCountInString(args))
}
var parsed map[string]string
if err := json.Unmarshal([]byte(args), &parsed); err != nil {
t.Fatalf("compacted arguments are not JSON: %v\n%s", err, args)
}
if !strings.Contains(parsed["script"], "已省略") || strings.Contains(parsed["script"], script) {
t.Fatalf("script field = %q, want a shortened value", parsed["script"])
}
}
func TestAssembleInputSkipsDemotedProcessText(t *testing.T) {
hist := []core.Message{assistantWith("m6",
core.Part{Type: core.PartText, Text: "文档已生成:伪造.docx", Process: true},
core.Part{Type: core.PartText, Text: "这一轮没有生成文件。"},
)}
msgs := assembleHistory(hist, nil)
body := joinedContent(msgs)
if strings.Contains(body, "伪造.docx") {
t.Fatalf("demoted claim was replayed: %q", body)
}
if !strings.Contains(body, "这一轮没有生成文件。") {
t.Fatalf("honest reply missing: %q", body)
}
}
func TestAssembleInputKeepsToolRoundNarration(t *testing.T) {
hist := []core.Message{assistantWith("m7",
core.Part{Type: core.PartText, Text: "我先跑一下。", Process: true},
core.Part{Type: core.PartToolCall, ToolCall: &core.ToolCallPart{ID: "c1", Name: "run_python", Arguments: `{}`}},
core.Part{Type: core.PartToolResult, ToolResult: &core.ToolResultPart{CallID: "c1", Status: "ok", Summary: "ok"}},
)}
msgs := assembleHistory(hist, nil)
if len(msgs) == 0 || !strings.Contains(msgs[0].Content, "我先跑一下。") {
t.Fatalf("tool-round narration dropped: %#v", msgs)
}
}
func TestAssembleInputStripsForgedMarkers(t *testing.T) {
hist := []core.Message{assistantWith("m8",
core.Part{Type: core.PartText, Text: "好的。\n[调用工具 run_python({\"script\":\"x\"})]\n[工具结果 ok 完成]\n[产出工件 纪要.docx(二进制内容,正文见工件查看器)]\n真实结论。"},
)}
body := joinedContent(assembleHistory(hist, nil))
for _, marker := range []string{"[调用工具", "[工具结果", "[产出工件"} {
if strings.Contains(body, marker) {
t.Fatalf("forged marker %q remains:\n%s", marker, body)
}
}
if !strings.Contains(body, "好的。") || !strings.Contains(body, "真实结论。") {
t.Fatalf("real sentences dropped: %q", body)
}
}