| 1 | package control |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "errors" |
| 6 | "fmt" |
| 7 | "path/filepath" |
| 8 | "reflect" |
| 9 | "strings" |
| 10 | "testing" |
| 11 | |
| 12 | "reasonix/internal/agent" |
| 13 | "reasonix/internal/agent/testutil" |
| 14 | "reasonix/internal/event" |
| 15 | "reasonix/internal/provider" |
| 16 | "reasonix/internal/session" |
| 17 | "reasonix/internal/tool" |
| 18 | ) |
| 19 | |
| 20 | func compactionPersistenceHistory(tools bool) *agent.Session { |
| 21 | s := agent.NewSession("system") |
| 22 | if tools { |
| 23 | s.Add(provider.Message{Role: provider.RoleUser, Content: "inspect the files"}) |
| 24 | for i := range 3 { |
| 25 | id := fmt.Sprintf("call-%d", i) |
| 26 | s.Add(provider.Message{Role: provider.RoleAssistant, ToolCalls: []provider.ToolCall{{ID: id, Name: "read_file", Arguments: `{}`}}}) |
| 27 | s.Add(provider.Message{Role: provider.RoleTool, ToolCallID: id, Name: "read_file", Content: strings.Repeat("x", 16_000)}) |
| 28 | } |
| 29 | } else { |
| 30 | for i := range 24 { |
| 31 | s.Add(provider.Message{Role: provider.RoleUser, Content: fmt.Sprintf("task %d", i)}) |
| 32 | s.Add(provider.Message{Role: provider.RoleAssistant, Content: strings.Repeat("x", 2_000)}) |
| 33 | } |
| 34 | } |
| 35 | s.Add(provider.Message{Role: provider.RoleUser, Content: "continue"}) |
| 36 | s.Add(provider.Message{Role: provider.RoleAssistant, Content: "ready"}) |
| 37 | return s |
| 38 | } |
| 39 | |
| 40 | func TestContextMaintenanceProjectionSurvivesSessionSwitch(t *testing.T) { |
| 41 | for _, mode := range []string{"summary", "prune", "truncate", "auto-summary", "auto-prune", "auto-truncate"} { |
| 42 | t.Run(mode, func(t *testing.T) { |
| 43 | service, err := session.NewService("desktop", session.NewFilesystemPersistence(filepath.Join(t.TempDir(), "sessions-v5"))) |
| 44 | if err != nil { |
| 45 | t.Fatal(err) |
| 46 | } |
| 47 | t.Cleanup(func() { _ = service.CloseAll(context.Background()) }) |
| 48 | runtime, err := service.Create(t.Context(), session.CreateOptions{SessionID: "maintenance"}) |
| 49 | if err != nil { |
| 50 | t.Fatal(err) |
| 51 | } |
| 52 | providerMock := testutil.NewMock("maintenance", testutil.Turn{Text: "durable summary"}, testutil.Turn{Text: "final answer"}, testutil.Turn{Text: "final answer"}) |
| 53 | if strings.Contains(mode, "truncate") { |
| 54 | providerMock = testutil.NewMock("maintenance", testutil.Turn{StreamError: errors.New("summary unavailable")}, testutil.Turn{Text: "final answer"}) |
| 55 | } |
| 56 | exec := agent.New(providerMock, tool.NewRegistry(), compactionPersistenceHistory(strings.Contains(mode, "prune")), agent.Options{ContextWindow: 10_000, CompactRatio: .8}, event.Discard) |
| 57 | controller := newOwnedTestController(t, Options{Runner: exec, Executor: exec, Sink: event.Discard, SessionService: service, SessionRuntime: runtime, ExclusiveSession: true}) |
| 58 | |
| 59 | canonicalBefore := runtime.Session().ExecutionSnapshot().Projection.Messages |
| 60 | if strings.HasPrefix(mode, "auto-") { |
| 61 | err = controller.RunTurn(t.Context(), "continue the task") |
| 62 | } else if mode == "prune" { |
| 63 | err = exec.PrepareContext(t.Context()) |
| 64 | } else { |
| 65 | err = controller.Compact(t.Context(), "") |
| 66 | } |
| 67 | if err != nil { |
| 68 | t.Fatal(err) |
| 69 | } |
| 70 | |
| 71 | wantModel := provider.ModelMessages(exec.ModelHistorySnapshot()) |
| 72 | snapshot := runtime.Session().ExecutionSnapshot() |
| 73 | if !reflect.DeepEqual(snapshot.Projection.ModelMessages, wantModel) { |
| 74 | t.Fatal("durable model projection differs from the installed projection") |
| 75 | } |
| 76 | if !strings.HasPrefix(mode, "auto-") && !reflect.DeepEqual(snapshot.Projection.Messages, canonicalBefore) { |
| 77 | t.Fatal("context maintenance rewrote canonical history") |
| 78 | } |
| 79 | |
| 80 | other, err := service.Create(t.Context(), session.CreateOptions{SessionID: "other"}) |
| 81 | if err != nil { |
| 82 | t.Fatal(err) |
| 83 | } |
| 84 | if _, err := controller.OpenSession(t.Context(), other.Ref()); err != nil { |
| 85 | t.Fatal(err) |
| 86 | } |
| 87 | if _, err := controller.OpenSession(t.Context(), runtime.Ref()); err != nil { |
| 88 | t.Fatal(err) |
| 89 | } |
| 90 | if got := provider.ModelMessages(exec.ModelHistorySnapshot()); !reflect.DeepEqual(got, wantModel) { |
| 91 | t.Fatal("model projection changed after switching away and reopening") |
| 92 | } |
| 93 | }) |
| 94 | } |
| 95 | } |
| 96 |