add usage info to branch summary, compaction and tool result entries (#6671)

* add usage info to branch summary entries

* add usage to compaction entries

* allow custom tools to report llm usage in tool results

* allow observing and patching usage in tool_result hooks

* agent-harness: save usage in entries

for compaction, branch summaries and tool results
This commit is contained in:
David Brailovsky
2026-07-20 16:41:43 +02:00
committed by GitHub
parent c179395218
commit 2fd3868401
29 changed files with 844 additions and 122 deletions
@@ -97,6 +97,14 @@ describe("AgentSession compaction characterization", () => {
});
it("manually compacts using an extension-provided summary", async () => {
const summaryUsage = {
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 },
};
const harness = await createHarness({
settings: { compaction: { keepRecentTokens: 1 } },
extensionFactories: [
@@ -106,6 +114,7 @@ describe("AgentSession compaction characterization", () => {
summary: "summary from extension",
firstKeptEntryId: event.preparation.firstKeptEntryId,
tokensBefore: event.preparation.tokensBefore,
usage: summaryUsage,
details: { source: "extension" },
},
}));
@@ -116,14 +125,26 @@ describe("AgentSession compaction characterization", () => {
await harness.session.prompt("one");
await harness.session.prompt("two");
const statsBefore = harness.session.getSessionStats();
const result = await harness.session.compact();
const compactionEntries = harness.sessionManager.getEntries().filter((entry) => entry.type === "compaction");
const estimatedTokensAfter = harness.session.messages.reduce((sum, message) => sum + estimateTokens(message), 0);
expect(result.summary).toBe("summary from extension");
expect(result.usage).toEqual(summaryUsage);
expect(result.estimatedTokensAfter).toBe(estimatedTokensAfter);
expect(compactionEntries).toHaveLength(1);
const compactionEntry = compactionEntries[0];
if (compactionEntry?.type === "compaction") {
expect(compactionEntry.usage).toEqual(summaryUsage);
}
const statsAfter = harness.session.getSessionStats();
expect(statsAfter.tokens.input).toBe(statsBefore.tokens.input + summaryUsage.input);
expect(statsAfter.tokens.output).toBe(statsBefore.tokens.output + summaryUsage.output);
expect(statsAfter.tokens.cacheRead).toBe(statsBefore.tokens.cacheRead + summaryUsage.cacheRead);
expect(statsAfter.tokens.cacheWrite).toBe(statsBefore.tokens.cacheWrite + summaryUsage.cacheWrite);
expect(statsAfter.cost).toBe(statsBefore.cost + summaryUsage.cost.total);
expect(harness.session.messages[0]?.role).toBe("compactionSummary");
});
@@ -154,6 +175,22 @@ describe("AgentSession compaction characterization", () => {
expect(getStreamCallCount()).toBe(1);
});
it("persists usage from pi-generated manual compaction", async () => {
const harness = await createHarness({ withConfiguredAuth: false });
harnesses.push(harness);
seedCompactableSession(harness);
useSummaryStreamFn(harness, "summary from custom stream");
const result = await harness.session.compact();
const compactionEntries = harness.sessionManager.getEntries().filter((entry) => entry.type === "compaction");
expect(result.usage).toEqual(createUsage(10));
expect(compactionEntries).toHaveLength(1);
expect(compactionEntries[0]?.type === "compaction" ? compactionEntries[0].usage : undefined).toEqual(
createUsage(10),
);
});
it("auto-compacts with a custom streamFn when registry auth is absent", async () => {
const harness = await createHarness({ withConfiguredAuth: false });
harnesses.push(harness);
@@ -1,5 +1,5 @@
import type { AgentTool, ThinkingLevel } from "@earendil-works/pi-agent-core";
import { fauxAssistantMessage, fauxToolCall, type Model } from "@earendil-works/pi-ai";
import { fauxAssistantMessage, fauxToolCall, type Model, type Usage } from "@earendil-works/pi-ai";
import { Type } from "typebox";
import { afterEach, describe, expect, it } from "vitest";
import type { BuildSystemPromptOptions, ExtensionAPI } from "../../src/index.ts";
@@ -156,6 +156,23 @@ describe("AgentSession model and extension characterization", () => {
});
it("allows extension tool_result handlers to modify tool results", async () => {
const toolUsage: Usage = {
input: 1,
output: 2,
cacheRead: 3,
cacheWrite: 4,
totalTokens: 10,
cost: { input: 0.1, output: 0.2, cacheRead: 0.3, cacheWrite: 0.4, total: 1 },
};
const patchedToolUsage: Usage = {
input: 5,
output: 6,
cacheRead: 7,
cacheWrite: 8,
totalTokens: 26,
cost: { input: 0.5, output: 0.6, cacheRead: 0.7, cacheWrite: 0.8, total: 2.6 },
};
let observedToolUsage: Usage | undefined;
const echoTool: AgentTool = {
name: "echo",
label: "Echo",
@@ -163,17 +180,21 @@ describe("AgentSession model and extension characterization", () => {
parameters: Type.Object({ text: Type.String() }),
execute: async (_toolCallId, params) => {
const text = typeof params === "object" && params !== null && "text" in params ? String(params.text) : "";
return { content: [{ type: "text", text }], details: { text } };
return { content: [{ type: "text", text }], details: { text }, usage: toolUsage };
},
};
const harness = await createHarness({
tools: [echoTool],
extensionFactories: [
(pi) => {
pi.on("tool_result", async () => ({
content: [{ type: "text", text: "patched result" }],
details: { patched: true },
}));
pi.on("tool_result", async (event) => {
observedToolUsage = event.usage;
return {
content: [{ type: "text", text: "patched result" }],
details: { patched: true },
usage: patchedToolUsage,
};
});
},
],
});
@@ -196,9 +217,12 @@ describe("AgentSession model and extension characterization", () => {
await harness.session.prompt("hi");
expect(getAssistantTexts(harness)).toContain("patched result");
expect(
harness.session.messages.find((message) => message.role === "toolResult" && message.details?.patched === true),
).toBeDefined();
const toolResult = harness.session.messages.find(
(message) => message.role === "toolResult" && message.details?.patched === true,
);
expect(observedToolUsage).toEqual(toolUsage);
expect(toolResult).toBeDefined();
expect(toolResult?.role === "toolResult" ? toolResult.usage : undefined).toEqual(patchedToolUsage);
});
it("allows extension context handlers to modify messages before the LLM call", async () => {
@@ -37,7 +37,7 @@ describe("issue #6324 branch summary ambient auth", () => {
cacheRead: 0,
cacheWrite: 0,
totalTokens: 2,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0.25 },
},
stopReason: "stop",
timestamp: Date.now(),
@@ -57,5 +57,6 @@ describe("issue #6324 branch summary ambient auth", () => {
expect(streamCallCount).toBe(1);
expect(result.summaryEntry?.type).toBe("branch_summary");
expect(result.summaryEntry?.summary).toContain("branch summary text");
expect(result.summaryEntry?.usage?.cost.total).toBe(0.25);
});
});