ci / vet-and-test (pull_request) Successful in 48s
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.
181 lines
7.5 KiB
Go
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)
|
|
}
|
|
}
|