fix: complete extension usage accounting

closes #6509
This commit is contained in:
Mario Zechner
2026-07-20 17:04:12 +02:00
parent 2fd3868401
commit f8b74a4507
20 changed files with 267 additions and 36 deletions
@@ -5,6 +5,7 @@ import { AgentSession } from "../src/core/agent-session.ts";
import { AuthStorage } from "../src/core/auth-storage.ts";
import { SessionManager } from "../src/core/session-manager.ts";
import { SettingsManager } from "../src/core/settings-manager.ts";
import { getUsageCostBreakdown } from "../src/core/usage-totals.ts";
import { createInMemoryModelRegistry, getModelRuntime } from "./model-runtime-test-utils.ts";
import { createTestResourceLoader } from "./utilities.ts";
@@ -224,6 +225,31 @@ describe("AgentSession.getSessionStats", () => {
}
});
it("groups tool and summary usage separately from model-attributed usage", () => {
const sessionManager = SessionManager.inMemory();
const rootId = sessionManager.appendMessage(createUserMessage("hello", 1));
sessionManager.appendMessage({
...createAssistantMessage("response", 100, 2),
usage: { ...createUsage(100), cost: { ...createUsage(100).cost, total: 0.5 } },
});
sessionManager.appendMessage(
createToolResultMessage({ ...createUsage(100), cost: { ...createUsage(100).cost, total: 1 } }),
);
sessionManager.appendCompaction("summary", rootId, 100, undefined, false, {
...createUsage(100),
cost: { ...createUsage(100).cost, total: 2 },
});
sessionManager.branchWithSummary(null, "branch summary", undefined, false, {
...createUsage(100),
cost: { ...createUsage(100).cost, total: 3 },
});
expect(getUsageCostBreakdown(sessionManager.getEntries())).toEqual([
{ key: "Tools/summaries", cost: 6, tokens: 300 },
{ key: `${model.provider}/${model.id}`, cost: 0.5, tokens: 100 },
]);
});
it("ignores zero-usage messages when checking for post-compaction context usage", async () => {
const { session, sessionManager } = await createSession();
@@ -1,7 +1,12 @@
import type { AgentMessage } from "@earendil-works/pi-agent-core";
import type { AssistantMessage, Model } from "@earendil-works/pi-ai";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { type CompactionPreparation, compact, generateSummary } from "../src/core/compaction/index.ts";
import {
type CompactionPreparation,
compact,
generateSummary,
generateSummaryWithUsage,
} from "../src/core/compaction/index.ts";
const { completeSimpleMock } = vi.hoisted(() => ({
completeSimpleMock: vi.fn(),
@@ -57,7 +62,7 @@ describe("generateSummary reasoning options", () => {
});
it("uses the provided thinking level for reasoning-capable models", async () => {
const result = await generateSummary(
const result = await generateSummaryWithUsage(
messages,
createModel(true),
2000,
@@ -79,6 +84,12 @@ describe("generateSummary reasoning options", () => {
});
});
it("preserves the string result from generateSummary", async () => {
await expect(generateSummary(messages, createModel(false), 2000, "test-key")).resolves.toBe(
"## Goal\nTest summary",
);
});
it("does not set reasoning when thinking is off", async () => {
await generateSummary(
messages,
@@ -523,6 +523,52 @@ describe("createBranchedSession", () => {
}
});
it("preserves tool and summary usage across a file-backed reload", () => {
const tempDir = join(tmpdir(), `session-usage-roundtrip-${Date.now()}`);
mkdirSync(tempDir, { recursive: true });
try {
const session = SessionManager.create(tempDir, tempDir);
const rootId = session.appendMessage(userMsg("question"));
session.appendMessage(assistantMsg("answer"));
const usage = {
input: 10,
output: 20,
cacheRead: 30,
cacheWrite: 40,
totalTokens: 100,
cost: { input: 0.1, output: 0.2, cacheRead: 0.3, cacheWrite: 0.4, total: 1 },
};
session.appendMessage({
role: "toolResult",
toolCallId: "call-1",
toolName: "nested-model",
content: [{ type: "text", text: "result" }],
isError: false,
usage,
timestamp: Date.now(),
});
session.appendCompaction("summary", rootId, 100, undefined, false, usage);
session.branchWithSummary(rootId, "branch summary", undefined, false, usage);
const file = session.getSessionFile();
expect(file).toBeDefined();
const reopened = SessionManager.open(file!, tempDir);
expect(reopened.getEntries()).toEqual(
expect.arrayContaining([
expect.objectContaining({ type: "compaction", usage }),
expect.objectContaining({ type: "branch_summary", usage }),
expect.objectContaining({
type: "message",
message: expect.objectContaining({ role: "toolResult", usage }),
}),
]),
);
} finally {
rmSync(tempDir, { recursive: true, force: true });
}
});
it("writes file immediately when forking from a point with assistant messages", () => {
const tempDir = join(tmpdir(), `session-fork-with-assistant-${Date.now()}`);
mkdirSync(tempDir, { recursive: true });