pi-roundtable 0.9.5 → 0.9.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,12 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [0.9.6] - 2026-10-10
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- The compaction tiers measure the context a compaction left as a request sends it, system prompt and tool definitions included: the first request after the compaction reports it, and until then the conversation's estimate gets the fixed part the last request before it carried (what it sent less the estimate of its messages). They measured the conversation alone, so a session with a large fixed part (over 100k of tools) that compacted to about 270k kept its next compaction at 300k and compacted again every few tool results. `LatestCompaction` adds `measured`, and the `conversation compacted` log adds `contextAfter`, the measure the tiers use, beside `tokensAfter`.
|
|
13
|
+
|
|
8
14
|
## [0.9.5] - 2026-10-10
|
|
9
15
|
|
|
10
16
|
0.9.4 was tagged but never published (its release checks failed on the recorded prompt texts); everything listed under it is released in 0.9.5.
|
package/docs/plugins.md
CHANGED
|
@@ -68,7 +68,7 @@ Use the kit's building blocks for a plugin that runs Pi itself, such as a coding
|
|
|
68
68
|
- Tools: `textToolsExtension`, `requiredString`, `stringList` (with `toolText` and `toolError`) for tools that return text.
|
|
69
69
|
- Mirroring a built-in tool in a worker that cannot reach the host: `SCHEDULE_TOOLS`, `scheduleToolSpecs({ locale, timeZone })`, `isScheduleTool`, `callScheduleTool`, `DELEGATE_TOOL` and `DELEGATE_TOOL_SPEC`.
|
|
70
70
|
The specs take the locale and time zone for their descriptions, so the worker needs no process-wide setting.
|
|
71
|
-
- Compaction: `CompactionTiers` gives a session the core's compaction tiers (`settings()` for Pi's `SettingsManager`, `wrapCompactor(factory, onBypass, prepare?)` to hold a compaction extension back past the ceiling and change what it is given first, `latest()`), with `SOFT_COMPACT_TOKENS` (300,000), `HARD_COMPACT_TOKENS` (500,000), `COMPACT_HEADROOM_TOKENS` (50,000), `compactionEngine(details, engine)` and the types `CompactionEngine`, `CompactionHistory` and `LatestCompaction`. `summaryProjection(messages, history?)` is what a shared conversation's compaction may summarize, without anyone's private memory, prompt states, or reasoning, and `privateCompaction(preparation, additionalMessages?)` sets a `session_before_compact` preparation to it. Optional surrounding history pairs calls/results split across summary, prefix and kept context. Independent shared sessions use `memoryProjection(messages, view)` on requests and `hidesPrivateExchange(messages, view)` for raw replay guards (`MemoryView`: `shared` and `reader`; legacy attribution is internal to the bridge guard, not a projection option). `recordMemoryTurn(manager, reader, privateMemory)` appends `MEMORY_TURN_ENTRY` (`roundtable-memory-turn`) before prompting and returns a marker for memory first used later in that turn; `carriesMemory(message)` recognizes built-in memory calls/results and custom `privateTo` results, including immediate failures and unanswered calls. `bridgeHistoryHidesMemory(rawBranch, reader)` checks those records and raw exchanges across compaction; SYSTEM is a reader of its own there, and the core's SYSTEM-as-primary-owner attribution and its pre-scope legacy attribution stay inside the core.
|
|
71
|
+
- Compaction: `CompactionTiers` gives a session the core's compaction tiers (`settings()` for Pi's `SettingsManager`, `wrapCompactor(factory, onBypass, prepare?)` to hold a compaction extension back past the ceiling and change what it is given first, `latest()`: the latest compaction's `contextTokens`, the context it left as a request sends it, system prompt and tools included, `measured` once a request after it reported its usage), with `SOFT_COMPACT_TOKENS` (300,000), `HARD_COMPACT_TOKENS` (500,000), `COMPACT_HEADROOM_TOKENS` (50,000), `compactionEngine(details, engine)` and the types `CompactionEngine`, `CompactionHistory` and `LatestCompaction`. `summaryProjection(messages, history?)` is what a shared conversation's compaction may summarize, without anyone's private memory, prompt states, or reasoning, and `privateCompaction(preparation, additionalMessages?)` sets a `session_before_compact` preparation to it. Optional surrounding history pairs calls/results split across summary, prefix and kept context. Independent shared sessions use `memoryProjection(messages, view)` on requests and `hidesPrivateExchange(messages, view)` for raw replay guards (`MemoryView`: `shared` and `reader`; legacy attribution is internal to the bridge guard, not a projection option). `recordMemoryTurn(manager, reader, privateMemory)` appends `MEMORY_TURN_ENTRY` (`roundtable-memory-turn`) before prompting and returns a marker for memory first used later in that turn; `carriesMemory(message)` recognizes built-in memory calls/results and custom `privateTo` results, including immediate failures and unanswered calls. `bridgeHistoryHidesMemory(rawBranch, reader)` checks those records and raw exchanges across compaction; SYSTEM is a reader of its own there, and the core's SYSTEM-as-primary-owner attribution and its pre-scope legacy attribution stay inside the core.
|
|
72
72
|
- Jev compaction: `jevCompact(input, options)` compacts through Jev (pi-jev-compaction 1.0.0) and returns `{ compaction }` or `{ skipped, detail? }` (a `JevSkipReason`: pi-jev-compaction's fallbacks `no_key`, `aborted`, `nothing_to_compact`, `no_candidates`, `cannot_fit`, `jev_error`, `reduction_too_small`, or `previous_summary_too_large`), for Pi's own summary to run instead.
|
|
73
73
|
Its summary carries the previous summary once, as the transcript's `[previous compaction]` message, with `estimatedTokensAfter` measured on the final text; a previous summary over `previousSummaryLimitTokens` (default `JEV_PREVIOUS_SUMMARY_LIMIT_TOKENS`, 60,000) skips without calling Jev, so Pi's summary condenses the chain.
|
|
74
74
|
Jev judges with `goal` (default `JEV_GOAL`: keep the tool results that set rules still in force, drop stale lookups, listings and finished edits), and the summary ends with a `## Rules loaded before this compaction` section naming each rule load among the summarized messages (tool name and JSON arguments cut at 200 characters) for the agent to load again; `ruleLoad(tool, args)` picks them (default `isRuleLoad`: tools ending in `invoke-skill` or `get-system-prompt`, and tools ending in `read` whose `path` is a `SKILL.md`, an `AGENTS.md` or under `.agents/skills/`).
|
package/package.json
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
|
+
buildSessionProjection,
|
|
2
3
|
type ExtensionAPI,
|
|
3
4
|
type ExtensionFactory,
|
|
4
5
|
estimateTokens,
|
|
5
6
|
type SessionBeforeCompactEvent,
|
|
7
|
+
type SessionEntry,
|
|
6
8
|
type SessionManager,
|
|
9
|
+
type SessionProjection,
|
|
7
10
|
SettingsManager,
|
|
8
11
|
} from "@earendil-works/pi-coding-agent";
|
|
9
12
|
|
|
@@ -29,8 +32,14 @@ export type CompactionHistory = Pick<
|
|
|
29
32
|
export interface LatestCompaction {
|
|
30
33
|
id: string;
|
|
31
34
|
engine: CompactionEngine;
|
|
32
|
-
/**
|
|
35
|
+
/**
|
|
36
|
+
* The context the compaction left, as a request sends it: system prompt and tools included.
|
|
37
|
+
* The first request after it reports this; until then, its summary and kept entries plus the
|
|
38
|
+
* fixed part the last request before it carried.
|
|
39
|
+
*/
|
|
33
40
|
contextTokens: number;
|
|
41
|
+
/** Whether a request after the compaction reported `contextTokens`, or it is still estimated. */
|
|
42
|
+
measured: boolean;
|
|
34
43
|
}
|
|
35
44
|
|
|
36
45
|
/** The extension's compactions record its `engine` in their details; Pi's own do not. */
|
|
@@ -44,6 +53,41 @@ export function compactionEngine(
|
|
|
44
53
|
: "pi";
|
|
45
54
|
}
|
|
46
55
|
|
|
56
|
+
/** What a request sent, as its reply's usage reports it: undefined for a reply without usable usage. */
|
|
57
|
+
function sentTokens(entry: SessionEntry | undefined): number | undefined {
|
|
58
|
+
if (entry?.type !== "message") return undefined;
|
|
59
|
+
const message = entry.message;
|
|
60
|
+
if (message.role !== "assistant") return undefined;
|
|
61
|
+
if (message.stopReason === "aborted" || message.stopReason === "error")
|
|
62
|
+
return undefined;
|
|
63
|
+
const { input, cacheRead, cacheWrite } = message.usage;
|
|
64
|
+
const sent = input + cacheRead + cacheWrite;
|
|
65
|
+
return sent > 0 ? sent : undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function estimated(messages: SessionProjection["messages"]): number {
|
|
69
|
+
let tokens = 0;
|
|
70
|
+
for (const message of messages) tokens += estimateTokens(message);
|
|
71
|
+
return tokens;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The part of a request that is not the conversation, system prompt and tool definitions, as the
|
|
76
|
+
* last reply before `index` reported it: what that request sent less the estimate of its messages.
|
|
77
|
+
*/
|
|
78
|
+
function fixedTokens(branch: readonly SessionEntry[], index: number): number {
|
|
79
|
+
for (let at = index - 1; at >= 0; at--) {
|
|
80
|
+
const sent = sentTokens(branch[at]);
|
|
81
|
+
if (sent === undefined) continue;
|
|
82
|
+
const messages = buildSessionProjection(
|
|
83
|
+
branch.slice(0, at),
|
|
84
|
+
branch[at - 1]?.id ?? null,
|
|
85
|
+
).messages;
|
|
86
|
+
return Math.max(0, sent - estimated(messages));
|
|
87
|
+
}
|
|
88
|
+
return 0;
|
|
89
|
+
}
|
|
90
|
+
|
|
47
91
|
/** The branch's latest compaction, measured from the session as it stands now. */
|
|
48
92
|
export function latestCompaction(
|
|
49
93
|
history: CompactionHistory,
|
|
@@ -56,16 +100,22 @@ export function latestCompaction(
|
|
|
56
100
|
const upToCompaction = new Set(
|
|
57
101
|
branch.slice(0, index + 1).map((each) => each.id),
|
|
58
102
|
);
|
|
59
|
-
|
|
103
|
+
const engine = compactionEngine(entry.details, extensionEngine);
|
|
104
|
+
for (const after of branch.slice(index + 1)) {
|
|
105
|
+
const sent = sentTokens(after);
|
|
106
|
+
if (sent !== undefined)
|
|
107
|
+
return { id: entry.id, engine, contextTokens: sent, measured: true };
|
|
108
|
+
}
|
|
109
|
+
let messages = 0;
|
|
60
110
|
for (const projected of history.buildSessionProjection().entries) {
|
|
61
111
|
if (!upToCompaction.has(projected.sourceEntry.id)) continue;
|
|
62
|
-
|
|
63
|
-
contextTokens += estimateTokens(message);
|
|
112
|
+
messages += estimated(projected.messages);
|
|
64
113
|
}
|
|
65
114
|
return {
|
|
66
115
|
id: entry.id,
|
|
67
|
-
engine
|
|
68
|
-
contextTokens,
|
|
116
|
+
engine,
|
|
117
|
+
contextTokens: messages + fixedTokens(branch, index),
|
|
118
|
+
measured: false,
|
|
69
119
|
};
|
|
70
120
|
}
|
|
71
121
|
|
|
@@ -124,12 +174,15 @@ export class CompactionTiers {
|
|
|
124
174
|
this.#extensionEngine = extensionEngine;
|
|
125
175
|
}
|
|
126
176
|
|
|
127
|
-
/**
|
|
177
|
+
/**
|
|
178
|
+
* The latest compaction; pi checks before every request, so it is measured once per compaction
|
|
179
|
+
* after a request reports it, and estimated again until one does.
|
|
180
|
+
*/
|
|
128
181
|
latest(): LatestCompaction | undefined {
|
|
129
182
|
const branch = this.#history.getBranch();
|
|
130
183
|
const entry = branch.findLast((each) => each.type === "compaction");
|
|
131
184
|
if (!entry) return undefined;
|
|
132
|
-
if (this.#cached?.id !== entry.id) {
|
|
185
|
+
if (this.#cached?.id !== entry.id || !this.#cached.latest.measured) {
|
|
133
186
|
const latest = latestCompaction(this.#history, this.#extensionEngine);
|
|
134
187
|
if (!latest) return undefined;
|
|
135
188
|
this.#cached = { id: entry.id, latest };
|
|
@@ -207,12 +207,14 @@ export class SessionFactory {
|
|
|
207
207
|
|
|
208
208
|
/**
|
|
209
209
|
* Logs who compacted a conversation, the compaction extension or Pi's summary, by how much, and the context size
|
|
210
|
-
* that compacts it next, so a move to the hard ceiling shows.
|
|
210
|
+
* that compacts it next, so a move to the hard ceiling shows. `contextAfter` is the context the tiers measure,
|
|
211
|
+
* system prompt and tools included, which `tokensAfter`, the conversation alone, leaves out.
|
|
211
212
|
*/
|
|
212
213
|
#logCompaction(
|
|
213
214
|
channel: ChannelKey,
|
|
214
215
|
event: CompactionEnd,
|
|
215
216
|
session: AgentSession,
|
|
217
|
+
tiers: CompactionTiers,
|
|
216
218
|
): void {
|
|
217
219
|
const { logger } = this.#options;
|
|
218
220
|
const trigger = event.reason === "manual" ? "self" : event.reason;
|
|
@@ -239,6 +241,7 @@ export class SessionFactory {
|
|
|
239
241
|
),
|
|
240
242
|
tokensBefore: event.result.tokensBefore,
|
|
241
243
|
tokensAfter: event.result.estimatedTokensAfter,
|
|
244
|
+
contextAfter: tiers.latest()?.contextTokens,
|
|
242
245
|
nextCompactionAt:
|
|
243
246
|
model &&
|
|
244
247
|
model.contextWindow -
|
|
@@ -423,7 +426,7 @@ export class SessionFactory {
|
|
|
423
426
|
await this.#awaitTools(session, awaited);
|
|
424
427
|
session.subscribe((event) => {
|
|
425
428
|
if (event.type === "compaction_end")
|
|
426
|
-
this.#logCompaction(channel, event, session);
|
|
429
|
+
this.#logCompaction(channel, event, session, tiers);
|
|
427
430
|
});
|
|
428
431
|
return Object.assign(state, { session });
|
|
429
432
|
}
|