Download packages/coding-agent/test/agent-session-stats.test.ts from SaylorTwift/pi: direct link, hf CLI and curl.
- Browser
- Download file 9.82 kB
-
https://huggingface.co/SaylorTwift/pi/resolve/main/packages/coding-agent/test/agent-session-stats.test.ts
- Command line
-
hf download hf://SaylorTwift/pi/packages/coding-agent/test/agent-session-stats.test.ts
-
curl -L -o agent-session-stats.test.ts https://huggingface.co/SaylorTwift/pi/resolve/main/packages/coding-agent/test/agent-session-stats.test.ts
9.82 kB
| import { Agent } from "@earendil-works/pi-agent-core"; | |
| import { | |
| type AssistantMessage, | |
| getModel, | |
| streamSimple, | |
| type ToolResultMessage, | |
| type Usage, | |
| } from "@earendil-works/pi-ai/compat"; | |
| import { describe, expect, it } from "vitest"; | |
| 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"; | |
| const model = getModel("anthropic", "claude-sonnet-4-5")!; | |
| function createUsage(totalTokens: number): Usage { | |
| return { | |
| input: totalTokens, | |
| output: 0, | |
| cacheRead: 0, | |
| cacheWrite: 0, | |
| totalTokens, | |
| cost: { | |
| input: 0, | |
| output: 0, | |
| cacheRead: 0, | |
| cacheWrite: 0, | |
| total: 0, | |
| }, | |
| }; | |
| } | |
| function createAssistantMessage(text: string, totalTokens: number, timestamp: number): AssistantMessage { | |
| return { | |
| role: "assistant", | |
| content: [{ type: "text", text }], | |
| api: model.api, | |
| provider: model.provider, | |
| model: model.id, | |
| usage: createUsage(totalTokens), | |
| stopReason: "stop", | |
| timestamp, | |
| }; | |
| } | |
| function createUserMessage(text: string, timestamp: number) { | |
| return { | |
| role: "user" as const, | |
| content: text, | |
| timestamp, | |
| }; | |
| } | |
| function createToolResultMessage(usage: Usage): ToolResultMessage { | |
| return { | |
| role: "toolResult", | |
| toolCallId: "tool-call-1", | |
| toolName: "test_tool", | |
| content: [{ type: "text", text: "tool result" }], | |
| usage, | |
| isError: false, | |
| timestamp: 1, | |
| }; | |
| } | |
| async function createSession() { | |
| const settingsManager = SettingsManager.inMemory(); | |
| const sessionManager = SessionManager.inMemory(); | |
| const authStorage = AuthStorage.inMemory(); | |
| await authStorage.modify("anthropic", async () => ({ type: "api_key", key: "test-key" })); | |
| const session = new AgentSession({ | |
| agent: new Agent({ | |
| getApiKey: () => "test-key", | |
| streamFn: streamSimple, | |
| initialState: { | |
| model, | |
| systemPrompt: "You are a helpful assistant.", | |
| tools: [], | |
| thinkingLevel: "high", | |
| }, | |
| }), | |
| sessionManager, | |
| settingsManager, | |
| cwd: process.cwd(), | |
| modelRuntime: getModelRuntime(await createInMemoryModelRegistry(authStorage)), | |
| resourceLoader: createTestResourceLoader(), | |
| }); | |
| return { session, sessionManager }; | |
| } | |
| function syncAgentMessages(session: AgentSession, sessionManager: SessionManager): void { | |
| session.agent.state.messages = sessionManager.buildSessionContext().messages; | |
| } | |
| describe("AgentSession.getSessionStats", () => { | |
| it("exposes the current context usage alongside token totals", async () => { | |
| const { session, sessionManager } = await createSession(); | |
| try { | |
| sessionManager.appendMessage(createUserMessage("hello", 1)); | |
| sessionManager.appendMessage(createAssistantMessage("hi", 200, 2)); | |
| syncAgentMessages(session, sessionManager); | |
| const stats = session.getSessionStats(); | |
| expect(stats.contextUsage).toEqual(session.getContextUsage()); | |
| expect(stats.contextUsage?.tokens).toBe(200); | |
| expect(stats.contextUsage?.contextWindow).toBe(model.contextWindow); | |
| expect(stats.contextUsage?.percent).toBe((200 / model.contextWindow) * 100); | |
| } finally { | |
| session.dispose(); | |
| } | |
| }); | |
| it("reports unknown current context usage immediately after compaction", async () => { | |
| const { session, sessionManager } = await createSession(); | |
| try { | |
| sessionManager.appendMessage(createUserMessage("first", 1)); | |
| sessionManager.appendMessage(createAssistantMessage("response1", 180_000, 2)); | |
| const keptUserId = sessionManager.appendMessage(createUserMessage("second", 3)); | |
| sessionManager.appendMessage(createAssistantMessage("response2", 195_000, 4)); | |
| sessionManager.appendCompaction("summary", keptUserId, 195_000); | |
| sessionManager.appendMessage(createUserMessage("third", 5)); | |
| syncAgentMessages(session, sessionManager); | |
| const stats = session.getSessionStats(); | |
| // Totals cover ALL entries, including history compacted away (180k + 195k). | |
| expect(stats.tokens.input).toBe(375_000); | |
| expect(stats.contextUsage).toBeDefined(); | |
| expect(stats.contextUsage?.tokens).toBeNull(); | |
| expect(stats.contextUsage?.percent).toBeNull(); | |
| } finally { | |
| session.dispose(); | |
| } | |
| }); | |
| it("uses post-compaction usage for current context instead of stale kept usage", async () => { | |
| const { session, sessionManager } = await createSession(); | |
| try { | |
| sessionManager.appendMessage(createUserMessage("first", 1)); | |
| sessionManager.appendMessage(createAssistantMessage("response1", 180_000, 2)); | |
| const keptUserId = sessionManager.appendMessage(createUserMessage("second", 3)); | |
| sessionManager.appendMessage(createAssistantMessage("response2", 195_000, 4)); | |
| sessionManager.appendCompaction("summary", keptUserId, 195_000); | |
| sessionManager.appendMessage(createUserMessage("third", 5)); | |
| sessionManager.appendMessage(createAssistantMessage("response3", 25_000, 6)); | |
| syncAgentMessages(session, sessionManager); | |
| const stats = session.getSessionStats(); | |
| // Totals cover ALL entries, including history compacted away (180k + 195k + 25k). | |
| expect(stats.tokens.input).toBe(400_000); | |
| expect(stats.contextUsage).toBeDefined(); | |
| expect(stats.contextUsage?.tokens).toBe(25_000); | |
| expect(stats.contextUsage?.percent).toBe((25_000 / model.contextWindow) * 100); | |
| } finally { | |
| session.dispose(); | |
| } | |
| }); | |
| it("includes branch summary usage in session totals", async () => { | |
| const { session, sessionManager } = await createSession(); | |
| try { | |
| sessionManager.branchWithSummary(null, "summary", undefined, false, { | |
| 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 }, | |
| }); | |
| syncAgentMessages(session, sessionManager); | |
| const stats = session.getSessionStats(); | |
| expect(stats.tokens).toEqual({ input: 10, output: 20, cacheRead: 30, cacheWrite: 40, total: 100 }); | |
| expect(stats.cost).toBe(1); | |
| } finally { | |
| session.dispose(); | |
| } | |
| }); | |
| it("includes compaction usage in session totals", async () => { | |
| const { session, sessionManager } = await createSession(); | |
| try { | |
| const firstKeptEntryId = sessionManager.appendMessage(createUserMessage("hello", 1)); | |
| sessionManager.appendCompaction("summary", firstKeptEntryId, 100, undefined, false, { | |
| 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 }, | |
| }); | |
| syncAgentMessages(session, sessionManager); | |
| const stats = session.getSessionStats(); | |
| expect(stats.tokens).toEqual({ input: 10, output: 20, cacheRead: 30, cacheWrite: 40, total: 100 }); | |
| expect(stats.cost).toBe(1); | |
| } finally { | |
| session.dispose(); | |
| } | |
| }); | |
| it("includes tool result usage in session totals", async () => { | |
| const { session, sessionManager } = await createSession(); | |
| try { | |
| sessionManager.appendMessage( | |
| createToolResultMessage({ | |
| 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 }, | |
| }), | |
| ); | |
| syncAgentMessages(session, sessionManager); | |
| const stats = session.getSessionStats(); | |
| expect(stats.tokens).toEqual({ input: 10, output: 20, cacheRead: 30, cacheWrite: 40, total: 100 }); | |
| expect(stats.cost).toBe(1); | |
| } finally { | |
| session.dispose(); | |
| } | |
| }); | |
| 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(); | |
| try { | |
| sessionManager.appendMessage(createUserMessage("first", 1)); | |
| sessionManager.appendMessage(createAssistantMessage("response1", 180_000, 2)); | |
| const keptUserId = sessionManager.appendMessage(createUserMessage("second", 3)); | |
| sessionManager.appendMessage(createAssistantMessage("response2", 195_000, 4)); | |
| sessionManager.appendCompaction("summary", keptUserId, 195_000); | |
| sessionManager.appendMessage(createUserMessage("third", 5)); | |
| sessionManager.appendMessage(createAssistantMessage("response3", 25_000, 6)); | |
| sessionManager.appendMessage(createUserMessage("continue", 7)); | |
| sessionManager.appendMessage(createAssistantMessage("partial", 0, 8)); | |
| syncAgentMessages(session, sessionManager); | |
| const stats = session.getSessionStats(); | |
| expect(stats.contextUsage).toBeDefined(); | |
| expect(stats.contextUsage?.tokens).not.toBeNull(); | |
| expect(stats.contextUsage?.tokens ?? 0).toBeGreaterThan(25_000); | |
| } finally { | |
| session.dispose(); | |
| } | |
| }); | |
| }); | |