Merge remote-tracking branch 'origin/main'
This commit is contained in:
2
.github/APPROVED_CONTRIBUTORS
vendored
2
.github/APPROVED_CONTRIBUTORS
vendored
@@ -205,3 +205,5 @@ npupko issue
|
|||||||
chrisvariety pr
|
chrisvariety pr
|
||||||
|
|
||||||
maximilianzuern pr
|
maximilianzuern pr
|
||||||
|
|
||||||
|
brianmichel pr
|
||||||
|
|||||||
@@ -545,7 +545,10 @@ export async function generateSummary(
|
|||||||
previousSummary?: string,
|
previousSummary?: string,
|
||||||
thinkingLevel?: ThinkingLevel,
|
thinkingLevel?: ThinkingLevel,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const maxTokens = Math.floor(0.8 * reserveTokens);
|
const maxTokens = Math.min(
|
||||||
|
Math.floor(0.8 * reserveTokens),
|
||||||
|
model.maxTokens > 0 ? model.maxTokens : Number.POSITIVE_INFINITY,
|
||||||
|
);
|
||||||
|
|
||||||
// Use update prompt if we have a previous summary, otherwise initial prompt
|
// Use update prompt if we have a previous summary, otherwise initial prompt
|
||||||
let basePrompt = previousSummary ? UPDATE_SUMMARIZATION_PROMPT : SUMMARIZATION_PROMPT;
|
let basePrompt = previousSummary ? UPDATE_SUMMARIZATION_PROMPT : SUMMARIZATION_PROMPT;
|
||||||
@@ -817,7 +820,10 @@ async function generateTurnPrefixSummary(
|
|||||||
signal?: AbortSignal,
|
signal?: AbortSignal,
|
||||||
thinkingLevel?: ThinkingLevel,
|
thinkingLevel?: ThinkingLevel,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const maxTokens = Math.floor(0.5 * reserveTokens); // Smaller budget for turn prefix
|
const maxTokens = Math.min(
|
||||||
|
Math.floor(0.5 * reserveTokens),
|
||||||
|
model.maxTokens > 0 ? model.maxTokens : Number.POSITIVE_INFINITY,
|
||||||
|
); // Smaller budget for turn prefix
|
||||||
const llmMessages = convertToLlm(messages);
|
const llmMessages = convertToLlm(messages);
|
||||||
const conversationText = serializeConversation(llmMessages);
|
const conversationText = serializeConversation(llmMessages);
|
||||||
const promptText = `<conversation>\n${conversationText}\n</conversation>\n\n${TURN_PREFIX_SUMMARIZATION_PROMPT}`;
|
const promptText = `<conversation>\n${conversationText}\n</conversation>\n\n${TURN_PREFIX_SUMMARIZATION_PROMPT}`;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
} from "@earendil-works/pi-ai";
|
} from "@earendil-works/pi-ai";
|
||||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||||
import {
|
import {
|
||||||
|
type CompactionPreparation,
|
||||||
calculateContextTokens,
|
calculateContextTokens,
|
||||||
compact,
|
compact,
|
||||||
DEFAULT_COMPACTION_SETTINGS,
|
DEFAULT_COMPACTION_SETTINGS,
|
||||||
@@ -113,14 +114,17 @@ function createModelChangeEntry(provider: string, modelId: string, parentId: str
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function createFauxModel(reasoning: boolean): { faux: FauxProviderRegistration; model: Model<string> } {
|
function createFauxModel(
|
||||||
|
reasoning: boolean,
|
||||||
|
maxTokens = 8192,
|
||||||
|
): { faux: FauxProviderRegistration; model: Model<string> } {
|
||||||
const faux = registerFauxProvider({
|
const faux = registerFauxProvider({
|
||||||
models: [
|
models: [
|
||||||
{
|
{
|
||||||
id: reasoning ? "reasoning-model" : "non-reasoning-model",
|
id: reasoning ? "reasoning-model" : "non-reasoning-model",
|
||||||
reasoning,
|
reasoning,
|
||||||
contextWindow: 200000,
|
contextWindow: 200000,
|
||||||
maxTokens: 8192,
|
maxTokens,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@@ -285,6 +289,35 @@ describe("harness compaction", () => {
|
|||||||
expect(seenOptions[2]).not.toHaveProperty("reasoning");
|
expect(seenOptions[2]).not.toHaveProperty("reasoning");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("clamps compaction summary maxTokens to the model output cap", async () => {
|
||||||
|
const messages: AgentMessage[] = [createUserMessage("Summarize this.")];
|
||||||
|
const seenOptions: Array<Record<string, unknown> | undefined> = [];
|
||||||
|
const { faux, model } = createFauxModel(false, 128000);
|
||||||
|
faux.setResponses([
|
||||||
|
(_context, options) => {
|
||||||
|
seenOptions.push(options as Record<string, unknown> | undefined);
|
||||||
|
return fauxAssistantMessage("## Goal\nTest summary");
|
||||||
|
},
|
||||||
|
(_context, options) => {
|
||||||
|
seenOptions.push(options as Record<string, unknown> | undefined);
|
||||||
|
return fauxAssistantMessage("## Goal\nTest summary");
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const preparation: CompactionPreparation = {
|
||||||
|
firstKeptEntryId: "entry-keep",
|
||||||
|
messagesToSummarize: messages,
|
||||||
|
turnPrefixMessages: messages,
|
||||||
|
isSplitTurn: true,
|
||||||
|
tokensBefore: 600000,
|
||||||
|
fileOps: { read: new Set(), written: new Set(), edited: new Set() },
|
||||||
|
settings: { enabled: true, reserveTokens: 500000, keepRecentTokens: 20000 },
|
||||||
|
};
|
||||||
|
|
||||||
|
await compact(preparation, model, "test-key");
|
||||||
|
|
||||||
|
expect(seenOptions.map((options) => options?.maxTokens)).toEqual([128000, 128000]);
|
||||||
|
});
|
||||||
|
|
||||||
it("returns a compaction result with file details", async () => {
|
it("returns a compaction result with file details", async () => {
|
||||||
const u1 = createMessageEntry(createUserMessage("read a file"));
|
const u1 = createMessageEntry(createUserMessage("read a file"));
|
||||||
const assistantMessage: AssistantMessage = {
|
const assistantMessage: AssistantMessage = {
|
||||||
|
|||||||
@@ -538,7 +538,10 @@ export async function generateSummary(
|
|||||||
previousSummary?: string,
|
previousSummary?: string,
|
||||||
thinkingLevel?: ThinkingLevel,
|
thinkingLevel?: ThinkingLevel,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const maxTokens = Math.floor(0.8 * reserveTokens);
|
const maxTokens = Math.min(
|
||||||
|
Math.floor(0.8 * reserveTokens),
|
||||||
|
model.maxTokens > 0 ? model.maxTokens : Number.POSITIVE_INFINITY,
|
||||||
|
);
|
||||||
|
|
||||||
// Use update prompt if we have a previous summary, otherwise initial prompt
|
// Use update prompt if we have a previous summary, otherwise initial prompt
|
||||||
let basePrompt = previousSummary ? UPDATE_SUMMARIZATION_PROMPT : SUMMARIZATION_PROMPT;
|
let basePrompt = previousSummary ? UPDATE_SUMMARIZATION_PROMPT : SUMMARIZATION_PROMPT;
|
||||||
@@ -808,7 +811,10 @@ async function generateTurnPrefixSummary(
|
|||||||
signal?: AbortSignal,
|
signal?: AbortSignal,
|
||||||
thinkingLevel?: ThinkingLevel,
|
thinkingLevel?: ThinkingLevel,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const maxTokens = Math.floor(0.5 * reserveTokens); // Smaller budget for turn prefix
|
const maxTokens = Math.min(
|
||||||
|
Math.floor(0.5 * reserveTokens),
|
||||||
|
model.maxTokens > 0 ? model.maxTokens : Number.POSITIVE_INFINITY,
|
||||||
|
); // Smaller budget for turn prefix
|
||||||
const llmMessages = convertToLlm(messages);
|
const llmMessages = convertToLlm(messages);
|
||||||
const conversationText = serializeConversation(llmMessages);
|
const conversationText = serializeConversation(llmMessages);
|
||||||
const promptText = `<conversation>\n${conversationText}\n</conversation>\n\n${TURN_PREFIX_SUMMARIZATION_PROMPT}`;
|
const promptText = `<conversation>\n${conversationText}\n</conversation>\n\n${TURN_PREFIX_SUMMARIZATION_PROMPT}`;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
||||||
import type { AssistantMessage, Model } from "@earendil-works/pi-ai";
|
import type { AssistantMessage, Model } from "@earendil-works/pi-ai";
|
||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import { generateSummary } from "../src/core/compaction/index.js";
|
import { type CompactionPreparation, compact, generateSummary } from "../src/core/compaction/index.js";
|
||||||
|
|
||||||
const { completeSimpleMock } = vi.hoisted(() => ({
|
const { completeSimpleMock } = vi.hoisted(() => ({
|
||||||
completeSimpleMock: vi.fn(),
|
completeSimpleMock: vi.fn(),
|
||||||
@@ -15,7 +15,7 @@ vi.mock("@earendil-works/pi-ai", async (importOriginal) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
function createModel(reasoning: boolean): Model<"anthropic-messages"> {
|
function createModel(reasoning: boolean, maxTokens = 8192): Model<"anthropic-messages"> {
|
||||||
return {
|
return {
|
||||||
id: reasoning ? "reasoning-model" : "non-reasoning-model",
|
id: reasoning ? "reasoning-model" : "non-reasoning-model",
|
||||||
name: reasoning ? "Reasoning Model" : "Non-reasoning Model",
|
name: reasoning ? "Reasoning Model" : "Non-reasoning Model",
|
||||||
@@ -26,7 +26,7 @@ function createModel(reasoning: boolean): Model<"anthropic-messages"> {
|
|||||||
input: ["text"],
|
input: ["text"],
|
||||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||||
contextWindow: 200000,
|
contextWindow: 200000,
|
||||||
maxTokens: 8192,
|
maxTokens,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,4 +115,20 @@ describe("generateSummary reasoning options", () => {
|
|||||||
});
|
});
|
||||||
expect(completeSimpleMock.mock.calls[0][2]).not.toHaveProperty("reasoning");
|
expect(completeSimpleMock.mock.calls[0][2]).not.toHaveProperty("reasoning");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("clamps compaction summary maxTokens to the model output cap", async () => {
|
||||||
|
const preparation: CompactionPreparation = {
|
||||||
|
firstKeptEntryId: "entry-keep",
|
||||||
|
messagesToSummarize: messages,
|
||||||
|
turnPrefixMessages: messages,
|
||||||
|
isSplitTurn: true,
|
||||||
|
tokensBefore: 600000,
|
||||||
|
fileOps: { read: new Set(), written: new Set(), edited: new Set() },
|
||||||
|
settings: { enabled: true, reserveTokens: 500000, keepRecentTokens: 20000 },
|
||||||
|
};
|
||||||
|
|
||||||
|
await compact(preparation, createModel(false, 128000), "test-key");
|
||||||
|
|
||||||
|
expect(completeSimpleMock.mock.calls.map((call) => call[2]?.maxTokens)).toEqual([128000, 128000]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user