feat(coding-agent): expose post-compaction token estimates
closes #5877
This commit is contained in:
@@ -2,6 +2,10 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- Added estimated post-compaction token counts to compact results and compaction events ([#5877](https://github.com/earendil-works/pi/issues/5877)).
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed compaction to refuse sessions with no eligible messages instead of producing empty summaries ([#4811](https://github.com/earendil-works/pi/issues/4811)).
|
||||
|
||||
@@ -374,11 +374,14 @@ Response:
|
||||
"summary": "Summary of conversation...",
|
||||
"firstKeptEntryId": "abc123",
|
||||
"tokensBefore": 150000,
|
||||
"estimatedTokensAfter": 32000,
|
||||
"details": {}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`estimatedTokensAfter` is a heuristic estimate over the rebuilt message context immediately after compaction, not a provider-exact token count.
|
||||
|
||||
#### set_auto_compaction
|
||||
|
||||
Enable or disable automatic compaction when context is nearly full.
|
||||
@@ -924,6 +927,7 @@ The `reason` field is `"manual"`, `"threshold"`, or `"overflow"`.
|
||||
"summary": "Summary of conversation...",
|
||||
"firstKeptEntryId": "abc123",
|
||||
"tokensBefore": 150000,
|
||||
"estimatedTokensAfter": 32000,
|
||||
"details": {}
|
||||
},
|
||||
"aborted": false,
|
||||
|
||||
@@ -45,6 +45,7 @@ import {
|
||||
collectEntriesForBranchSummary,
|
||||
compact,
|
||||
estimateContextTokens,
|
||||
estimateTokens,
|
||||
generateBranchSummary,
|
||||
prepareCompaction,
|
||||
shouldCompact,
|
||||
@@ -242,6 +243,14 @@ interface ToolDefinitionEntry {
|
||||
sourceInfo: SourceInfo;
|
||||
}
|
||||
|
||||
function estimateMessagesTokens(messages: AgentMessage[]): number {
|
||||
let tokens = 0;
|
||||
for (const message of messages) {
|
||||
tokens += estimateTokens(message);
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Constants
|
||||
// ============================================================================
|
||||
@@ -1726,6 +1735,7 @@ export class AgentSession {
|
||||
const newEntries = this.sessionManager.getEntries();
|
||||
const sessionContext = this.sessionManager.buildSessionContext();
|
||||
this.agent.state.messages = sessionContext.messages;
|
||||
const estimatedTokensAfter = estimateMessagesTokens(sessionContext.messages);
|
||||
|
||||
// Get the saved compaction entry for the extension event
|
||||
const savedCompactionEntry = newEntries.find((e) => e.type === "compaction" && e.summary === summary) as
|
||||
@@ -1740,10 +1750,11 @@ export class AgentSession {
|
||||
});
|
||||
}
|
||||
|
||||
const compactionResult = {
|
||||
const compactionResult: CompactionResult = {
|
||||
summary,
|
||||
firstKeptEntryId,
|
||||
tokensBefore,
|
||||
estimatedTokensAfter,
|
||||
details,
|
||||
};
|
||||
this._emit({
|
||||
@@ -1999,6 +2010,7 @@ export class AgentSession {
|
||||
const newEntries = this.sessionManager.getEntries();
|
||||
const sessionContext = this.sessionManager.buildSessionContext();
|
||||
this.agent.state.messages = sessionContext.messages;
|
||||
const estimatedTokensAfter = estimateMessagesTokens(sessionContext.messages);
|
||||
|
||||
// Get the saved compaction entry for the extension event
|
||||
const savedCompactionEntry = newEntries.find((e) => e.type === "compaction" && e.summary === summary) as
|
||||
@@ -2017,6 +2029,7 @@ export class AgentSession {
|
||||
summary,
|
||||
firstKeptEntryId,
|
||||
tokensBefore,
|
||||
estimatedTokensAfter,
|
||||
details,
|
||||
};
|
||||
this._emit({ type: "compaction_end", reason, result, aborted: false, willRetry });
|
||||
|
||||
@@ -104,6 +104,7 @@ export interface CompactionResult<T = unknown> {
|
||||
summary: string;
|
||||
firstKeptEntryId: string;
|
||||
tokensBefore: number;
|
||||
estimatedTokensAfter?: number;
|
||||
/** Extension-specific data (e.g., ArtifactIndex, version markers for structured compaction) */
|
||||
details?: T;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
type Model,
|
||||
} from "@earendil-works/pi-ai";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { estimateTokens } from "../../src/core/compaction/index.ts";
|
||||
import { createHarness, type Harness } from "./harness.ts";
|
||||
|
||||
type SessionWithCompactionInternals = {
|
||||
@@ -118,8 +119,10 @@ describe("AgentSession compaction characterization", () => {
|
||||
|
||||
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.estimatedTokensAfter).toBe(estimatedTokensAfter);
|
||||
expect(compactionEntries).toHaveLength(1);
|
||||
expect(harness.session.messages[0]?.role).toBe("compactionSummary");
|
||||
});
|
||||
@@ -161,7 +164,9 @@ describe("AgentSession compaction characterization", () => {
|
||||
await sessionInternals._runAutoCompaction("threshold", false);
|
||||
|
||||
const compactionEntries = harness.sessionManager.getEntries().filter((entry) => entry.type === "compaction");
|
||||
const compactionEnd = harness.eventsOfType("compaction_end").at(-1);
|
||||
expect(compactionEntries).toHaveLength(1);
|
||||
expect(compactionEnd?.result?.estimatedTokensAfter).toBeGreaterThan(0);
|
||||
expect(getStreamCallCount()).toBe(1);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user