pi-mono/packages/coding-agent/test/branch-summarization.test.ts
David Brailovsky 6fcb48670f
fix(coding-agent): remove fixed output limits from summary requests (#11036)
Compaction summaries were limited to 0.8 * compaction.reserveTokens (13107 by default), turn-prefix summaries to 0.5 * reserveTokens, and branch summaries to a hardcoded 4096. On providers where reasoning shares the output limit (OpenAI Responses, adaptive Anthropic), high thinking levels used most of that limit and compaction failed with "generation hit the token cap", even though the model allows far more output.

Tying the limit to reserveTokens assumed the summary request is as large as the full context, so its output had to fit in the reserved space. It is usually much smaller: recent messages kept by keepRecentTokens are not sent, a split turn goes to a separate request, tool results are truncated, and tool definitions and the normal system prompt are not included. Manual compaction can also run long before the context is full. The provider layer already sizes output from the actual request, using the model's output limit reduced to fit the remaining context, as for normal turns.

Summary requests now set no maxTokens and rely on that. compaction.reserveTokens only controls when compaction starts.

generateSummary() and generateSummaryWithUsage() now ignore the reserveTokens parameter.

closes #9512, closes #9075, closes #10908
2026-10-16 13:53:16 +02:00

114 lines
3 KiB
TypeScript

import type { StreamFn } from "@earendil-works/pi-agent-core";
import {
type AssistantMessage,
createAssistantMessageEventStream,
fauxAssistantMessage,
type Model,
type SimpleStreamOptions,
} from "@earendil-works/pi-ai";
import { describe, expect, it } from "vitest";
import { generateBranchSummary } from "../src/core/compaction/index.ts";
import type { SessionEntry } from "../src/core/session-manager.ts";
const model: Model<"anthropic-messages"> = {
id: "test-model",
name: "Test Model",
api: "anthropic-messages",
provider: "anthropic",
baseUrl: "https://api.anthropic.com",
reasoning: false,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 200000,
maxTokens: 8192,
};
const entries: SessionEntry[] = [
{
type: "message",
id: "branch-user",
parentId: null,
timestamp: new Date(1).toISOString(),
message: { role: "user", content: "Abandoned request", timestamp: 1 },
},
];
function response(content: AssistantMessage["content"]): AssistantMessage {
return {
...fauxAssistantMessage(""),
content,
api: model.api,
provider: model.provider,
model: model.id,
};
}
describe("branch summarization", () => {
it("does not override tool choice for branch summaries", async () => {
let requestOptions: SimpleStreamOptions | undefined;
const streamFn: StreamFn = (_model, _context, options) => {
requestOptions = options;
const stream = createAssistantMessageEventStream();
queueMicrotask(() =>
stream.push({ type: "done", reason: "stop", message: response([{ type: "text", text: "summary" }]) }),
);
return stream;
};
await generateBranchSummary(entries, {
model,
signal: new AbortController().signal,
streamFn,
});
expect(requestOptions?.toolChoice).toBeUndefined();
});
it("rejects tool calls from branch summaries", async () => {
const streamFn: StreamFn = () => {
const stream = createAssistantMessageEventStream();
queueMicrotask(() =>
stream.push({
type: "done",
reason: "toolUse",
message: response([
{ type: "toolCall", id: "tool-call-1", name: "read", arguments: { path: "README.md" } },
]),
}),
);
return stream;
};
const result = await generateBranchSummary(entries, {
model,
signal: new AbortController().signal,
streamFn,
});
expect(result.error).toBe("Branch summarization attempted to call a tool");
});
it("rejects length-limited branch summaries", async () => {
const streamFn: StreamFn = () => {
const stream = createAssistantMessageEventStream();
queueMicrotask(() =>
stream.push({
type: "done",
reason: "length",
message: { ...response([{ type: "text", text: "partial" }]), stopReason: "length" },
}),
);
return stream;
};
const result = await generateBranchSummary(entries, {
model,
signal: new AbortController().signal,
streamFn,
});
expect(result.error).toBe(
"Branch summarization failed: generation hit the token cap and the summary is incomplete",
);
});
});