Why: Old tool results were filling the window, compaction could split a tool group or commit a stale summary, and reminders arrived as extra user messages. Evidence: Compaction retries a conflict and opens the circuit after three failures; clearing keeps the last five tool results; a stale file write is rejected; RPC reports started, queued, or handled. Impact: Requests over the context window replace older tool results with a placeholder. Rewriting an existing file now requires a read in the same run. Subagent text over 32000 characters is capped. Refs: feature/context-engineering Co-authored-by: Cursor <cursoragent@cursor.com>
185 lines
4.4 KiB
Go
185 lines
4.4 KiB
Go
package session
|
|
|
|
import (
|
|
"fmt"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"agent-native/runtime/llm"
|
|
)
|
|
|
|
func TestR2CompactKeepsToolPair(t *testing.T) {
|
|
s, err := Open(filepath.Join(t.TempDir(), "s.jsonl"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
msgs := []llm.Message{
|
|
{Role: llm.RoleUser, Content: "ask"},
|
|
{Role: llm.RoleAssistant, Content: "", ToolCalls: []llm.ToolCall{{ID: "c", Name: "read"}}},
|
|
{Role: llm.RoleTool, ToolCallID: "c", Content: "file-body"},
|
|
}
|
|
for _, m := range msgs {
|
|
if err := s.Append(m); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
ok, err := s.Compact(1, func(messages []llm.Message) (string, error) {
|
|
return "summary", nil
|
|
})
|
|
if err != nil || !ok {
|
|
t.Fatalf("compact: %v %v", ok, err)
|
|
}
|
|
loaded, err := s.Load()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var sawTool bool
|
|
for _, m := range loaded {
|
|
if m.Role == llm.RoleTool && m.Content == "file-body" {
|
|
sawTool = true
|
|
}
|
|
}
|
|
if !sawTool {
|
|
t.Fatalf("tool result dropped: %+v", loaded)
|
|
}
|
|
}
|
|
|
|
func TestR2CompactRejectsAppendDuringSummary(t *testing.T) {
|
|
s, err := Open(filepath.Join(t.TempDir(), "s.jsonl"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, text := range []string{"a", "b", "c"} {
|
|
if err := s.Append(llm.Message{Role: llm.RoleUser, Content: text}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
_, err = s.Compact(1, func(messages []llm.Message) (string, error) {
|
|
return "summary", s.Append(llm.Message{Role: llm.RoleUser, Content: "raced"})
|
|
})
|
|
if err == nil || !strings.Contains(err.Error(), "compaction conflict") {
|
|
t.Fatalf("want conflict, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestCompactRetriesThenCommits(t *testing.T) {
|
|
s, err := Open(filepath.Join(t.TempDir(), "s.jsonl"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, text := range []string{"a", "b", "c"} {
|
|
if err := s.Append(llm.Message{Role: llm.RoleUser, Content: text}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
raced := false
|
|
ok, err := s.Compact(1, func(messages []llm.Message) (string, error) {
|
|
if !raced {
|
|
raced = true
|
|
if err := s.Append(llm.Message{Role: llm.RoleUser, Content: "raced"}); err != nil {
|
|
return "", err
|
|
}
|
|
return "", fmt.Errorf("compaction conflict: session changed during summary")
|
|
}
|
|
return "summary", nil
|
|
})
|
|
if err != nil || !ok {
|
|
t.Fatalf("retry compact: %v %v", ok, err)
|
|
}
|
|
loaded, err := s.Load()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var saw bool
|
|
for _, m := range loaded {
|
|
if m.Content == "raced" || m.Content == "summary" {
|
|
saw = true
|
|
}
|
|
}
|
|
if !saw {
|
|
t.Fatalf("retried summary missing: %+v", loaded)
|
|
}
|
|
}
|
|
|
|
func TestCompactLeavesNoOrphanToolCall(t *testing.T) {
|
|
s, err := Open(filepath.Join(t.TempDir(), "s.jsonl"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
msgs := []llm.Message{
|
|
{Role: llm.RoleUser, Content: "先看"},
|
|
{Role: llm.RoleAssistant, ToolCalls: []llm.ToolCall{{ID: "old", Name: "read_file"}}},
|
|
{Role: llm.RoleTool, ToolCallID: "old", Name: "read_file", Content: "旧"},
|
|
{Role: llm.RoleUser, Content: "再改"},
|
|
{Role: llm.RoleAssistant, ToolCalls: []llm.ToolCall{{ID: "a", Name: "read_file"}, {ID: "b", Name: "grep"}}},
|
|
{Role: llm.RoleTool, ToolCallID: "a", Name: "read_file", Content: "甲"},
|
|
{Role: llm.RoleTool, ToolCallID: "b", Name: "grep", Content: "乙"},
|
|
}
|
|
for _, m := range msgs {
|
|
if err := s.Append(m); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
ok, err := s.Compact(2, func(messages []llm.Message) (string, error) {
|
|
return "摘要", nil
|
|
})
|
|
if err != nil || !ok {
|
|
t.Fatalf("compact: %v %v", ok, err)
|
|
}
|
|
loaded, err := s.Load()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
assertToolPairs(t, loaded)
|
|
}
|
|
|
|
func assertToolPairs(t *testing.T, msgs []llm.Message) {
|
|
t.Helper()
|
|
calls := map[string]int{}
|
|
for _, m := range msgs {
|
|
if m.Role != llm.RoleAssistant {
|
|
continue
|
|
}
|
|
for _, c := range m.ToolCalls {
|
|
calls[c.ID]++
|
|
}
|
|
}
|
|
for _, m := range msgs {
|
|
if m.Role != llm.RoleTool {
|
|
continue
|
|
}
|
|
if calls[m.ToolCallID] == 0 {
|
|
t.Fatalf("orphan tool result %s in %+v", m.ToolCallID, msgs)
|
|
}
|
|
calls[m.ToolCallID]--
|
|
}
|
|
for id, n := range calls {
|
|
if n != 0 {
|
|
t.Fatalf("orphan tool call %s in %+v", id, msgs)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestR2AppendVisibleAfterReopen(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "s.jsonl")
|
|
s, err := Open(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := s.Append(llm.Message{Role: llm.RoleUser, Content: "durable"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
again, err := Open(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
msgs, err := again.Load()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(msgs) != 1 || msgs[0].Content != "durable" {
|
|
t.Fatalf("reopen: %+v", msgs)
|
|
}
|
|
}
|