Files
pi/packages/coding-agent/test/agent-session-stats.test.ts
Mario Zechner 8a0903ebf2 feat(ai): compat entrypoint, core-only root barrel (phase 5)
The root barrel is now core-only and side-effect free: types,
createModels/createProvider, auth substrate, lazyStream/lazyApi, faux,
utils. Generated catalogs, api-registry, env-api-keys, images, global
stream functions, and per-API lazy wrappers leave the root.

New @earendil-works/pi-ai/compat preserves the old surface verbatim as
a strict superset of the root: api-dispatch stream/complete with env
key injection, the builtin registration side effect (skip-if-present so
it cannot clobber earlier overrides), deprecated getModel/getModels/
getProviders aliases of the new getBuiltin* reads in providers/all,
lazy api wrappers + setBedrockProviderModule, and image generation.
Compat dies with the coding-agent ModelManager migration.

Packaging: exports map gains ./compat, ./providers/*, ./api/*;
sideEffects array lists only the effectful modules.

Old-global imports across agent/coding-agent/examples and pi-ai tests
switch to /compat (path-only; compat is a superset). The coding-agent
extension loader resolves the pi-ai ROOT specifier to compat, so
existing user extensions using the old global API keep working at
runtime until compat is removed. vitest configs alias /compat to src;
browser smoke imports old globals from /compat.
2026-06-10 21:17:12 +02:00

144 lines
4.7 KiB
TypeScript

import { Agent } from "@earendil-works/pi-agent-core";
import { type AssistantMessage, getModel, 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 { ModelRegistry } from "../src/core/model-registry.ts";
import { SessionManager } from "../src/core/session-manager.ts";
import { SettingsManager } from "../src/core/settings-manager.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 createSession() {
const settingsManager = SettingsManager.inMemory();
const sessionManager = SessionManager.inMemory();
const authStorage = AuthStorage.inMemory();
authStorage.setRuntimeApiKey("anthropic", "test-key");
const session = new AgentSession({
agent: new Agent({
getApiKey: () => "test-key",
initialState: {
model,
systemPrompt: "You are a helpful assistant.",
tools: [],
thinkingLevel: "high",
},
}),
sessionManager,
settingsManager,
cwd: process.cwd(),
modelRegistry: ModelRegistry.inMemory(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", () => {
const { session, sessionManager } = 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", () => {
const { session, sessionManager } = 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();
expect(stats.tokens.input).toBe(195_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", () => {
const { session, sessionManager } = 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();
expect(stats.tokens.input).toBe(220_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();
}
});
});