SPB Git

spb/khaelor Public

KHAELOR — a terminal-native autonomous engineering agent powered by Anthropic.

TypeScript 82.9% HTML 14.9% CSS 1.1% JavaScript 0.7%
9.2 KB · 229 lines typescript
Raw Blame History
1/**2 * KHAELOR3 * File: tests/context/compaction.test.ts4 * Description: Compaction tests — prune selection, pairing-safe cuts, prune-then-summarize pipeline ordering.5 *6 * Author: Simon-Pierre Boucher7 * Contact: contact@spboucher.ai8 */910import { describe, expect, it } from "vitest";11import {12  DEFAULT_COMPACTION_OPTIONS,13  PRUNE_PLACEHOLDER,14  collectRunningProcesses,15  compressEvents,16  parseCheckpoint,17  selectCompactionCut,18  selectPruneCandidates,19} from "../../src/context/index.js";20import type { CompactionOptions } from "../../src/context/index.js";21import { isPairingSafeCut } from "../../src/session/index.js";22import { FAKE_CHECKPOINT_TEXT, FakeModelClient, StreamBuilder, sampleSession } from "./fixtures.js";2324const opts = (overrides: Partial<CompactionOptions>): CompactionOptions => ({25  ...DEFAULT_COMPACTION_OPTIONS,26  ...overrides,27});2829describe("selectPruneCandidates", () => {30  it("prunes old tool results while protecting the newest N tokens of tool output", () => {31    const big = "x".repeat(2_000); // ~500 estimated tokens each32    const session = sampleSession({ bigToolOutput: big });33    // Make toolu_2's result big too, then add a third big recent result.34    const b = new StreamBuilder();35    for (const event of session.events) {36      if (event.type === "tool.completed" && event.payload.toolUseId === "toolu_2") {37        b.add({ type: "tool.completed", payload: { ...event.payload, modelText: big } });38      } else {39        b.add({ type: event.type, payload: event.payload } as never);40      }41    }42    b.add({43      type: "tool.requested",44      payload: { requestId: "req_4", blockIndex: 0, toolUseId: "toolu_3", toolName: "read", input: {} },45    });46    b.add({47      type: "tool.completed",48      payload: { toolUseId: "toolu_3", modelText: big, durationMs: 1, ui: { kind: "read", summary: "r" } },49    });5051    const decision = selectPruneCandidates(b.events, opts({ protectRecentToolTokens: 600 }));52    expect(decision.toolUseIds).toEqual(["toolu_1", "toolu_2"]);53    expect(decision.placeholder).toBe(PRUNE_PLACEHOLDER);54    expect(decision.tokensReclaimedEstimate).toBeGreaterThan(900);55  });5657  it("skips results already pruned and tiny results not worth a placeholder", () => {58    const big = "x".repeat(2_000);59    const session = sampleSession({ bigToolOutput: big });60    session.add({61      type: "context.pruned",62      payload: { toolUseIds: ["toolu_1"], placeholder: PRUNE_PLACEHOLDER, tokensReclaimedEstimate: 470 },63    });64    // toolu_2's result is tiny; toolu_1 already pruned → nothing left.65    const decision = selectPruneCandidates(session.events, opts({ protectRecentToolTokens: 0 }));66    expect(decision.toolUseIds).toEqual([]);67    expect(decision.tokensReclaimedEstimate).toBe(0);68  });69});7071describe("selectCompactionCut", () => {72  it("protects the first user message (head) and the latest user message (tail)", () => {73    const session = sampleSession();74    const cut = selectCompactionCut(session.events, opts({ protectTailTokens: 1 }));75    expect(cut).not.toBeNull();76    expect(cut as object).toEqual({ fromSeq: 4, toSeq: 14 });77    expect(isPairingSafeCut(session.events, cut as { fromSeq: number; toSeq: number })).toBe(true);78  });7980  it("never orphans a tool_use/tool_result pair — toSeq walks down to a safe boundary", () => {81    const b = sampleSession();82    // A tool round-trip whose result lands inside the protected tail.83    b.add({84      type: "tool.requested",85      payload: { requestId: "req_5", blockIndex: 0, toolUseId: "toolu_9", toolName: "bash", input: { cmd: "npm test" } },86    }); // seq 1787    b.add({88      type: "tool.completed",89      payload: {90        toolUseId: "toolu_9",91        modelText: "148 passed — full suite green, nothing else to report here",92        durationMs: 5,93        ui: { kind: "exec", summary: "npm test" },94      },95    }); // seq 1896    b.add({ type: "user.message-created", payload: { text: "Great, now document it", mentions: [] } }); // seq 199798    // Tail protection reaches back into seq 18 (the tool_result) but not seq 17:99    // a naive toSeq of 17 would orphan toolu_9, so the cut must retreat to 16.100    const cut = selectCompactionCut(b.events, opts({ protectTailTokens: 8 }));101    expect(cut).not.toBeNull();102    expect((cut as { toSeq: number }).toSeq).toBeLessThan(17);103    expect(isPairingSafeCut(b.events, cut as { fromSeq: number; toSeq: number })).toBe(true);104  });105106  it("returns null when there is no compactable middle", () => {107    const b = new StreamBuilder();108    b.add({ type: "user.message-created", payload: { text: "hello", mentions: [] } });109    expect(selectCompactionCut(b.events, opts({}))).toBeNull();110    expect(selectCompactionCut([], opts({}))).toBeNull();111  });112});113114describe("compressEvents pipeline", () => {115  it("prunes first: the summarizer sees recorded placeholders, never the reclaimed output", async () => {116    const big = "GIANT_TOOL_OUTPUT ".repeat(500);117    const session = sampleSession({ bigToolOutput: big });118    // Simulate the kernel: record the prune decision as a durable event first.119    const decision = selectPruneCandidates(session.events, opts({ protectRecentToolTokens: 10 }));120    expect(decision.toolUseIds).toContain("toolu_1");121    session.add({ type: "context.pruned", payload: decision });122123    const cut = selectCompactionCut(session.events, opts({ protectTailTokens: 1 }));124    expect(cut).not.toBeNull();125126    const client = new FakeModelClient(FAKE_CHECKPOINT_TEXT);127    const result = await compressEvents(128      { modelClient: client, model: "aux-model", maxOutputTokens: 2_000 },129      {130        events: session.events,131        cut: cut as { fromSeq: number; toSeq: number },132        trigger: "proactive-token-budget",133        tokensBefore: 150_000,134      },135    );136137    // Ordering: exactly one summarization call, made AFTER pruning was applied.138    expect(client.requests).toHaveLength(1);139    const sent = JSON.stringify(client.requests[0]);140    expect(sent).toContain(PRUNE_PLACEHOLDER);141    expect(sent).not.toContain("GIANT_TOOL_OUTPUT");142143    expect(result.summaryModel).toBe("aux-model");144    expect(result.trigger).toBe("proactive-token-budget");145    expect(result.tokensBefore).toBe(150_000);146    const parsed = parseCheckpoint(result.checkpointYaml);147    expect(parsed.ok).toBe(true);148    if (parsed.ok) expect(parsed.value.objective).toBe("Fix the bug in the session store");149  });150151  it("uses a small output budget and passes the AbortSignal through (cancellable)", async () => {152    const session = sampleSession();153    const cut = selectCompactionCut(session.events, opts({ protectTailTokens: 1 }));154    const client = new FakeModelClient(FAKE_CHECKPOINT_TEXT);155    const options = opts({ protectTailTokens: 1, summaryMaxOutputTokens: 777 });156    const controller = new AbortController();157    await compressEvents(158      { modelClient: client, model: "aux-model", maxOutputTokens: options.summaryMaxOutputTokens },159      {160        events: session.events,161        cut: cut as { fromSeq: number; toSeq: number },162        trigger: "user-command",163        tokensBefore: 0,164        signal: controller.signal,165      },166      options,167    );168    expect(client.requests[0]?.maxOutputTokens).toBe(777);169    expect(client.signals[0]).toBe(controller.signal);170171    const aborted = new AbortController();172    aborted.abort();173    await expect(174      compressEvents(175        { modelClient: new FakeModelClient(FAKE_CHECKPOINT_TEXT), model: "aux-model", maxOutputTokens: 100 },176        {177          events: session.events,178          cut: cut as { fromSeq: number; toSeq: number },179          trigger: "user-command",180          tokensBefore: 0,181          signal: aborted.signal,182        },183        options,184      ),185    ).rejects.toThrow();186  });187188  it("falls back to current_state when the model reply is unstructured, and derives running_processes from events", async () => {189    const session = sampleSession();190    session.add({191      type: "process.started",192      payload: { processId: "proc_9", pid: 1, command: "npm run dev", cwd: "/p", toolUseId: "t" },193    });194    expect(collectRunningProcesses(session.events)).toEqual([195      { id: "proc_9", command: "npm run dev", status: "running" },196    ]);197198    const cut = selectCompactionCut(session.events, opts({ protectTailTokens: 1 }));199    const client = new FakeModelClient("Sorry, here is a prose summary instead.");200    const result = await compressEvents(201      { modelClient: client, model: "aux-model", maxOutputTokens: 100 },202      {203        events: session.events,204        cut: cut as { fromSeq: number; toSeq: number },205        trigger: "reactive-overflow",206        tokensBefore: 10,207      },208    );209    const parsed = parseCheckpoint(result.checkpointYaml);210    expect(parsed.ok).toBe(true);211    if (parsed.ok) {212      expect(parsed.value.currentState).toBe("Sorry, here is a prose summary instead.");213      expect(parsed.value.runningProcesses).toEqual([214        { id: "proc_9", command: "npm run dev", status: "running" },215      ]);216    }217  });218219  it("refuses a pairing-unsafe cut", async () => {220    const session = sampleSession();221    await expect(222      compressEvents(223        { modelClient: new FakeModelClient(FAKE_CHECKPOINT_TEXT), model: "aux-model", maxOutputTokens: 100 },224        { events: session.events, cut: { fromSeq: 4, toSeq: 5 }, trigger: "user-command", tokensBefore: 0 },225      ),226    ).rejects.toThrow(/pairing/);227  });228});229