Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 11 additions & 13 deletions e2e/compaction-read-paging.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,20 +41,18 @@ function logLines(count: number): string {
async function openFoldSession(): Promise<E2ESession> {
const session = await openE2ESession({
permissionGate: e2ePermissionGate(),
// A tiny tail budget keeps the fold honest: the default 7500-token
// A tiny tail budget keeps the fold honest: the default 2500-token
// budget would absorb this small scenario into the live tail and the
// compactor would correctly no-op.
compactionShape: { tailBudgetTokens: 10 },
// Echo the summarized turns verbatim: the spine must exist, and this
// scenario asserts on the kept live bodies, not the summary text.
compactionCompletion: async (turns) =>
turns
.flatMap((turn) =>
turn.content.flatMap((block) =>
block.type === "text" ? [block.text] : [],
),
)
.join("\n"),
// Budget fits the paged read windows in the live tail but not the bulky
// pad turns, so the fold still fires.
compactionShape: { tailBudgetTokens: 500 },
// A short handoff, not a prompt echo: complete() receives the summarizer
// prompt turns, and a 4k echo gets truncated off the verify-repair tail
// so the fold aborts. This scenario asserts on kept live bodies, not the
// summary text.
compactionCompletion: async () =>
"Goal: page through var/log/big.log. Next: keep reading remaining windows.",
});
seedFile(session, "var/log/big.log", `${logLines(80)}\n`);
return session;
Expand All @@ -78,7 +76,7 @@ async function lastRequestBody(session: E2ESession): Promise<string> {
async function padTurns(session: E2ESession, count: number): Promise<void> {
for (let i = 0; i < count; i++) {
session.harness.scenario.replyOnce("anthropic", {
text: `Acknowledged ${i}.`,
text: `Acknowledged ${i}. ${"pad".repeat(800)}`,
headUsage: LOW_USAGE,
});
await runUntilDone(session, `Pad turn ${i}.`);
Expand Down
2 changes: 1 addition & 1 deletion evals/compaction/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export const BASELINE = {
syntheticLowInput: 100,
syntheticTriggerInput: 200000,
outputTokens: 1,
keepRecentTurns: 6,
tailBudgetTokens: 2500,
summaryMaxChars: 4000,
wallTimeoutMs: 30000,
} as const;
Expand Down
25 changes: 25 additions & 0 deletions src/agent/chat-event-subscribers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test";
import {
CHAT_TASKS_CHANGED_EVENT,
CHAT_TOOLS_ACTIVATE_EVENT,
COMPACTION_FOLD_NONCONVERGED_EVENT,
} from "./director.js";
import { handleChatDirectorEvent } from "./chat-event-subscribers.js";
import type { Task } from "./tasks.js";
Expand Down Expand Up @@ -119,4 +120,28 @@ describe("handleChatDirectorEvent", () => {
expect(dispatched).toBe(false);
expect(log.calls).toEqual([]);
});

test("dispatches a fold-nonconverged notice", () => {
const seen: string[] = [];
const log = makeLog();
const handled = handleChatDirectorEvent(
{
type: COMPACTION_FOLD_NONCONVERGED_EVENT,
data: { notice: "Context fold did not reduce occupancy." },
},
{
onTasksChanged: () => {
throw new Error("unexpected tasks-changed dispatch");
},
onToolsActivate: () => {
throw new Error("unexpected tools-activate dispatch");
},
onFoldNonConverged: (notice) => seen.push(notice),
},
log.log,
);
expect(handled).toBe(true);
expect(seen).toEqual(["Context fold did not reduce occupancy."]);
expect(log.calls).toEqual([]);
});
});
15 changes: 15 additions & 0 deletions src/agent/chat-event-subscribers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,17 @@ import { type } from "arktype";
import {
CHAT_TASKS_CHANGED_EVENT,
CHAT_TOOLS_ACTIVATE_EVENT,
COMPACTION_FOLD_NONCONVERGED_EVENT,
ChatTasksChangedDataSchema,
ChatToolsActivateDataSchema,
CompactionFoldNonConvergedDataSchema,
} from "./director.js";
import type { Task } from "./tasks.js";

export interface ChatDirectorEventHandlers {
onTasksChanged: (tasks: Task[]) => void;
onToolsActivate: (names: string[]) => void;
onFoldNonConverged?: (notice: string) => void;
}

export type ChatDirectorEventDebugLog = (
Expand Down Expand Up @@ -58,5 +61,17 @@ export function handleChatDirectorEvent(
handlers.onToolsActivate(parsed.names);
return true;
}
if (event.type === COMPACTION_FOLD_NONCONVERGED_EVENT) {
const parsed = CompactionFoldNonConvergedDataSchema(event.data);
if (parsed instanceof type.errors) {
logDebug(
"chat fold-nonconverged event dropped invalid payload: {error}",
{ error: parsed.summary },
);
return true;
}
handlers.onFoldNonConverged?.(parsed.notice);
return true;
}
return false;
}
Loading
Loading