pi-tuck 1.0.25 → 1.0.26
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 +18 -0
- package/dist/src/build-id.d.ts +1 -1
- package/dist/src/build-id.js +1 -1
- package/dist/src/compaction.js +5 -6
- package/dist/src/diagnostics.d.ts +1 -1
- package/dist/src/request-capacity.d.ts +15 -2
- package/dist/src/request-capacity.js +43 -5
- package/package.json +1 -1
- package/src/build-id.ts +1 -1
- package/src/compaction.ts +5 -6
- package/src/request-capacity.ts +38 -5
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,24 @@
|
|
|
3
3
|
All notable changes to pi-tuck. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and versions follow
|
|
4
4
|
[Semantic Versioning](https://semver.org/).
|
|
5
5
|
|
|
6
|
+
## [1.0.26] - 2026-11-10
|
|
7
|
+
|
|
8
|
+
### Fixed
|
|
9
|
+
|
|
10
|
+
- **1.0.25 failed to load in an installed Pi** ("Cannot find module '@earendil-works/pi-ai/utils/estimate'"). Pi's extension loader resolves
|
|
11
|
+
only a few pi-ai entry points for an installed package, and 1.0.25 imported another one by name.
|
|
12
|
+
- **Since 1.0.23, the check of Pi's own reply cap never ran in an installed Pi**, for the same reason. It loaded only in tests and the eval
|
|
13
|
+
harness, where pi-ai resolves by name; the installed package silently fell back without it. The 4,096-token margin in the hard line held,
|
|
14
|
+
because it fell back to the same value.
|
|
15
|
+
- pi-tuck now loads Pi's reply clamp and estimator from the running Pi's own install. If it can't find them (a host embedding Pi), it
|
|
16
|
+
keeps the fallback as before.
|
|
17
|
+
|
|
18
|
+
### Release gates
|
|
19
|
+
|
|
20
|
+
- **New gate:** the packed tarball was loaded in the real installed Pi before publishing. It loaded, and Pi's clamp resolved from Pi's own
|
|
21
|
+
install.
|
|
22
|
+
- Requests are unchanged from 1.0.25 wherever pi-ai resolves by name, which covers replay, stress and tests. Tests and CI pass.
|
|
23
|
+
|
|
6
24
|
## [1.0.25] - 2026-11-10
|
|
7
25
|
|
|
8
26
|
### Fixed
|
package/dist/src/build-id.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const BUILD_ID = "
|
|
1
|
+
export declare const BUILD_ID = "e95edde95c614d01e1cbf6489134676fac0c1bbf6aa57ef639ffac1e7e105041";
|
package/dist/src/build-id.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const BUILD_ID = "
|
|
1
|
+
export const BUILD_ID = "e95edde95c614d01e1cbf6489134676fac0c1bbf6aa57ef639ffac1e7e105041";
|
package/dist/src/compaction.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { textOf } from "./tuck/core/content.js";
|
|
2
|
-
import { requestReplyFloor, carriesPrompt, needsFullReplyReserve, thinkingOnlyLength, piContextSafetyMargin } from "./request-capacity.js";
|
|
2
|
+
import { requestReplyFloor, carriesPrompt, needsFullReplyReserve, thinkingOnlyLength, piContextSafetyMargin, loadPiRequestMath } from "./request-capacity.js";
|
|
3
3
|
import { BUILD_ID } from "./build-id.js";
|
|
4
4
|
import { createSemanticRecall, budgetSemantic } from "./semantic-recall.js";
|
|
5
5
|
import { DIAGNOSTIC_BUILD, IncidentRecorder, attachReceipt, permittedEntries, receiptId, findReceiptEntry } from "./diagnostics.js";
|
|
@@ -11,7 +11,6 @@ import { projectedMessages } from "./omissions.js";
|
|
|
11
11
|
import { applyClips, clipBudgets, clipFor } from "./clip.js";
|
|
12
12
|
import { DEFAULT_CUT_AT, bandOf, chooseCut, conversationTokens, gateCut, incomingEventTokens, incomingTokens, measurableConversationTokens, planningReserve, projectedConversationTokens, systemPart, validCutAt } from "./budget.js";
|
|
13
13
|
import { getCurrentSystemMessage, isContextOverflow, normalizeContext } from "@earendil-works/pi-ai";
|
|
14
|
-
import { estimateContextTokens } from "@earendil-works/pi-ai/utils/estimate";
|
|
15
14
|
import { calculateContextTokens, convertToLlm, estimateTokens, shouldCompact, SessionManager, SettingsManager } from "@earendil-works/pi-coding-agent";
|
|
16
15
|
import { Type } from "typebox";
|
|
17
16
|
import { Check } from "typebox/value";
|
|
@@ -105,9 +104,9 @@ export function seedWindow(branch, key) {
|
|
|
105
104
|
return initialWindow(key);
|
|
106
105
|
}
|
|
107
106
|
export async function installCompaction(pi, options = {}) {
|
|
108
|
-
const
|
|
109
|
-
const
|
|
110
|
-
|
|
107
|
+
const piMath = await loadPiRequestMath();
|
|
108
|
+
const piMargin = options.requestReserve ? 0 : await piContextSafetyMargin(piMath);
|
|
109
|
+
const piClamp = piMath?.clampMaxTokensToContext;
|
|
111
110
|
const cutAt = options.cutAt ?? DEFAULT_CUT_AT;
|
|
112
111
|
if (!validCutAt(cutAt))
|
|
113
112
|
throw new Error("Invalid cut point: cutAt must be a number above 0 and at most 1");
|
|
@@ -367,7 +366,7 @@ export async function installCompaction(pi, options = {}) {
|
|
|
367
366
|
const context = normalizeContext({ messages: convertToLlm(messages) });
|
|
368
367
|
const cap = piClamp?.(ctx.model, context, ctx.model.maxTokens);
|
|
369
368
|
return { cap, leavesFloor: cap === undefined || cap >= limits(ctx).replyFloor,
|
|
370
|
-
estimate: estimateContextTokens(context).tokens };
|
|
369
|
+
estimate: piMath ? piMath.estimateContextTokens(context).tokens : 0 };
|
|
371
370
|
};
|
|
372
371
|
const projectedRequest = (messages, ctx, host, incoming, prompt, tools, cut) => {
|
|
373
372
|
const enabled = cut || historyToolsEnabled(ctx), shapes = toolShapes(historyToolNames(pi, tools, enabled));
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type RecallPolicy } from './recall.js';
|
|
2
2
|
import type { SessionEntry } from '@earendil-works/pi-coding-agent';
|
|
3
|
-
export declare const DIAGNOSTIC_BUILD = "
|
|
3
|
+
export declare const DIAGNOSTIC_BUILD = "e95edde95c614d01e1cbf6489134676fac0c1bbf6aa57ef639ffac1e7e105041";
|
|
4
4
|
export interface Receipt {
|
|
5
5
|
id: string;
|
|
6
6
|
build: string;
|
|
@@ -1,9 +1,21 @@
|
|
|
1
1
|
import type { SessionEntry } from '@earendil-works/pi-coding-agent';
|
|
2
2
|
import type { AgentMessage, Messages } from './budget.js';
|
|
3
|
+
type SimpleOptions = typeof import('@earendil-works/pi-ai/api/simple-options');
|
|
4
|
+
type Estimate = typeof import('@earendil-works/pi-ai/utils/estimate');
|
|
5
|
+
export type PiRequestMath = {
|
|
6
|
+
clampMaxTokensToContext: SimpleOptions['clampMaxTokensToContext'];
|
|
7
|
+
estimateContextTokens: Estimate['estimateContextTokens'];
|
|
8
|
+
};
|
|
9
|
+
/** Pi's own request arithmetic: its reply clamp and the estimator behind it, as the running Pi has them.
|
|
10
|
+
* An installed extension cannot import these subpaths by name: Pi's extension loader aliases only pi-ai's root, compat, models, oauth and
|
|
11
|
+
* providers/all, and an installed package has no node_modules of its own. So resolve them first by name (tests, the eval harness, a dev
|
|
12
|
+
* checkout), then from the running Pi's own install, beside its executable. If neither works (an SDK host), there is none, and callers
|
|
13
|
+
* keep their fallbacks. */
|
|
14
|
+
export declare function loadPiRequestMath(): Promise<PiRequestMath | undefined>;
|
|
3
15
|
/** Read Pi's private safety allowance through its public clamp, rather than copying it.
|
|
4
|
-
*
|
|
16
|
+
* Without Pi's request arithmetic, fall back to Pi 1.2–1.5's documented 4,096-token allowance.
|
|
5
17
|
*/
|
|
6
|
-
export declare function piContextSafetyMargin(): Promise<number>;
|
|
18
|
+
export declare function piContextSafetyMargin(math?: PiRequestMath): Promise<number>;
|
|
7
19
|
/** The smallest reply room a request above the room must leave after Pi's own clamp: above the user's p99 reply (3,270 tokens). */
|
|
8
20
|
export declare const REPLY_FLOOR_MIN = 3584;
|
|
9
21
|
/** A spent thinking budget that produced neither a visible answer nor work. Empty/text/tool replies keep the existing length recovery. */
|
|
@@ -16,3 +28,4 @@ export declare function branchReplyFloor(branch: readonly SessionEntry[], reserv
|
|
|
16
28
|
export declare function requestReplyFloor(branch: readonly SessionEntry[], reserve: number, maxTokens?: number): number;
|
|
17
29
|
/** Check decoded text, including a whole verbatim user quote in our summary; never JSON-escaped substring matching. */
|
|
18
30
|
export declare function carriesPrompt(messages: Messages, prompt: string): boolean;
|
|
31
|
+
export {};
|
|
@@ -1,13 +1,51 @@
|
|
|
1
1
|
import { isContextOverflow, normalizeContext } from '@earendil-works/pi-ai';
|
|
2
2
|
import { parseRecords, splitSummary } from './tuck/core/transcript.js';
|
|
3
|
+
/** Pi's own request arithmetic: its reply clamp and the estimator behind it, as the running Pi has them.
|
|
4
|
+
* An installed extension cannot import these subpaths by name: Pi's extension loader aliases only pi-ai's root, compat, models, oauth and
|
|
5
|
+
* providers/all, and an installed package has no node_modules of its own. So resolve them first by name (tests, the eval harness, a dev
|
|
6
|
+
* checkout), then from the running Pi's own install, beside its executable. If neither works (an SDK host), there is none, and callers
|
|
7
|
+
* keep their fallbacks. */
|
|
8
|
+
export async function loadPiRequestMath() {
|
|
9
|
+
try {
|
|
10
|
+
const [simple, estimate] = await Promise.all([import('@earendil-works/pi-ai/api/simple-options'), import('@earendil-works/pi-ai/utils/estimate')]);
|
|
11
|
+
return { clampMaxTokensToContext: simple.clampMaxTokensToContext, estimateContextTokens: estimate.estimateContextTokens };
|
|
12
|
+
}
|
|
13
|
+
catch { /* not resolvable by name: an installed extension */ }
|
|
14
|
+
try {
|
|
15
|
+
// pi-ai's exports map these subpaths for `import` only (./api/* -> ./dist/api/*.js), so find the package beside Pi's executable and
|
|
16
|
+
// import the files that map names.
|
|
17
|
+
const [{ existsSync, realpathSync }, { dirname, join }, { pathToFileURL }] = await Promise.all([import('node:fs'), import('node:path'), import('node:url')]);
|
|
18
|
+
const entry = process.argv[1];
|
|
19
|
+
if (!entry)
|
|
20
|
+
return undefined;
|
|
21
|
+
let dir = dirname(realpathSync(entry)), root;
|
|
22
|
+
for (let up = 0; up < 12 && !root; up++, dir = dirname(dir)) {
|
|
23
|
+
const candidate = join(dir, 'node_modules', '@earendil-works', 'pi-ai');
|
|
24
|
+
if (existsSync(join(candidate, 'dist', 'api', 'simple-options.js')))
|
|
25
|
+
root = candidate;
|
|
26
|
+
if (dirname(dir) === dir)
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
if (!root)
|
|
30
|
+
return undefined;
|
|
31
|
+
const [simple, estimate] = await Promise.all([
|
|
32
|
+
import(pathToFileURL(join(root, 'dist', 'api', 'simple-options.js')).href),
|
|
33
|
+
import(pathToFileURL(join(root, 'dist', 'utils', 'estimate.js')).href)
|
|
34
|
+
]);
|
|
35
|
+
if (typeof simple.clampMaxTokensToContext !== 'function' || typeof estimate.estimateContextTokens !== 'function')
|
|
36
|
+
return undefined;
|
|
37
|
+
return { clampMaxTokensToContext: simple.clampMaxTokensToContext, estimateContextTokens: estimate.estimateContextTokens };
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
3
43
|
/** Read Pi's private safety allowance through its public clamp, rather than copying it.
|
|
4
|
-
*
|
|
44
|
+
* Without Pi's request arithmetic, fall back to Pi 1.2–1.5's documented 4,096-token allowance.
|
|
5
45
|
*/
|
|
6
|
-
export async function piContextSafetyMargin() {
|
|
46
|
+
export async function piContextSafetyMargin(math) {
|
|
7
47
|
try {
|
|
8
|
-
const
|
|
9
|
-
import('@earendil-works/pi-ai/api/simple-options'), import('@earendil-works/pi-ai/utils/estimate'),
|
|
10
|
-
]);
|
|
48
|
+
const { clampMaxTokensToContext, estimateContextTokens } = math ?? await loadPiRequestMath() ?? (() => { throw new Error('No Pi request math'); })();
|
|
11
49
|
const window = 1_000_000, context = normalizeContext({ messages: [] });
|
|
12
50
|
const margin = window - estimateContextTokens(context).tokens - clampMaxTokensToContext({ contextWindow: window, maxTokens: window }, context, window);
|
|
13
51
|
if (!Number.isSafeInteger(margin) || margin < 0 || margin >= window)
|
package/package.json
CHANGED
package/src/build-id.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const BUILD_ID = "
|
|
1
|
+
export const BUILD_ID = "e95edde95c614d01e1cbf6489134676fac0c1bbf6aa57ef639ffac1e7e105041";
|
package/src/compaction.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { textOf } from "./tuck/core/content.js";
|
|
2
|
-
import { requestReplyFloor, carriesPrompt, needsFullReplyReserve, thinkingOnlyLength, piContextSafetyMargin } from "./request-capacity.js";
|
|
2
|
+
import { requestReplyFloor, carriesPrompt, needsFullReplyReserve, thinkingOnlyLength, piContextSafetyMargin, loadPiRequestMath } from "./request-capacity.js";
|
|
3
3
|
import { BUILD_ID } from "./build-id.js";
|
|
4
4
|
import { createSemanticRecall, budgetSemantic, type SemanticRecallController } from "./semantic-recall";
|
|
5
5
|
import { DIAGNOSTIC_BUILD, IncidentRecorder, attachReceipt, permittedEntries, receiptId, findReceiptEntry, type Receipt, type RecorderOptions } from "./diagnostics.js";
|
|
@@ -12,7 +12,6 @@ import { applyClips, clipBudgets, clipFor, type Clip } from "./clip.js";
|
|
|
12
12
|
import { DEFAULT_CUT_AT, bandOf, chooseCut, conversationTokens, gateCut, incomingEventTokens, incomingTokens, measurableConversationTokens, planningReserve, projectedConversationTokens, systemPart, validCutAt,
|
|
13
13
|
type AgentMessage, type Decision, type Messages, type Plan, type Target } from "./budget.js";
|
|
14
14
|
import { getCurrentSystemMessage, isContextOverflow, normalizeContext, type ImageContent, type ToolResultMessage } from "@earendil-works/pi-ai";
|
|
15
|
-
import { estimateContextTokens } from "@earendil-works/pi-ai/utils/estimate";
|
|
16
15
|
import { calculateContextTokens, convertToLlm, estimateTokens, shouldCompact, SessionManager, SettingsManager, type ExtensionAPI, type ExtensionContext, type SessionEntry, type SessionBoundaryDraft, type TurnEndEvent } from "@earendil-works/pi-coding-agent";
|
|
17
16
|
import { Type } from "typebox";
|
|
18
17
|
import { Check } from "typebox/value";
|
|
@@ -135,9 +134,9 @@ export interface CompactionOptions {
|
|
|
135
134
|
}
|
|
136
135
|
|
|
137
136
|
export async function installCompaction(pi: ExtensionAPI, options: CompactionOptions = {}) {
|
|
138
|
-
const
|
|
139
|
-
const
|
|
140
|
-
|
|
137
|
+
const piMath = await loadPiRequestMath();
|
|
138
|
+
const piMargin = options.requestReserve ? 0 : await piContextSafetyMargin(piMath);
|
|
139
|
+
const piClamp = piMath?.clampMaxTokensToContext;
|
|
141
140
|
const cutAt = options.cutAt ?? DEFAULT_CUT_AT;
|
|
142
141
|
if (!validCutAt(cutAt)) throw new Error("Invalid cut point: cutAt must be a number above 0 and at most 1");
|
|
143
142
|
const semantic = options.semanticRecall ?? createSemanticRecall(), search = options.search ?? true;
|
|
@@ -350,7 +349,7 @@ export async function installCompaction(pi: ExtensionAPI, options: CompactionOpt
|
|
|
350
349
|
const context = normalizeContext({ messages: convertToLlm(messages) });
|
|
351
350
|
const cap = piClamp?.(ctx.model!, context, ctx.model!.maxTokens);
|
|
352
351
|
return { cap, leavesFloor: cap === undefined || cap >= limits(ctx).replyFloor,
|
|
353
|
-
estimate: estimateContextTokens(context).tokens };
|
|
352
|
+
estimate: piMath ? piMath.estimateContextTokens(context).tokens : 0 };
|
|
354
353
|
};
|
|
355
354
|
const projectedRequest = (messages: Messages, ctx: ExtensionContext, host: Messages, incoming: Messages, prompt: string, tools: readonly string[], cut: boolean) => {
|
|
356
355
|
const enabled = cut || historyToolsEnabled(ctx), shapes = toolShapes(historyToolNames(pi, tools, enabled));
|
package/src/request-capacity.ts
CHANGED
|
@@ -3,14 +3,47 @@ import type { SessionEntry } from '@earendil-works/pi-coding-agent';
|
|
|
3
3
|
import type { AgentMessage, Messages } from './budget.js';
|
|
4
4
|
import { parseRecords, splitSummary } from './tuck/core/transcript.js';
|
|
5
5
|
|
|
6
|
+
type SimpleOptions = typeof import('@earendil-works/pi-ai/api/simple-options');
|
|
7
|
+
type Estimate = typeof import('@earendil-works/pi-ai/utils/estimate');
|
|
8
|
+
export type PiRequestMath = { clampMaxTokensToContext: SimpleOptions['clampMaxTokensToContext']; estimateContextTokens: Estimate['estimateContextTokens'] };
|
|
9
|
+
|
|
10
|
+
/** Pi's own request arithmetic: its reply clamp and the estimator behind it, as the running Pi has them.
|
|
11
|
+
* An installed extension cannot import these subpaths by name: Pi's extension loader aliases only pi-ai's root, compat, models, oauth and
|
|
12
|
+
* providers/all, and an installed package has no node_modules of its own. So resolve them first by name (tests, the eval harness, a dev
|
|
13
|
+
* checkout), then from the running Pi's own install, beside its executable. If neither works (an SDK host), there is none, and callers
|
|
14
|
+
* keep their fallbacks. */
|
|
15
|
+
export async function loadPiRequestMath(): Promise<PiRequestMath | undefined> {
|
|
16
|
+
try {
|
|
17
|
+
const [simple, estimate] = await Promise.all([import('@earendil-works/pi-ai/api/simple-options'), import('@earendil-works/pi-ai/utils/estimate')]);
|
|
18
|
+
return { clampMaxTokensToContext: simple.clampMaxTokensToContext, estimateContextTokens: estimate.estimateContextTokens };
|
|
19
|
+
} catch { /* not resolvable by name: an installed extension */ }
|
|
20
|
+
try {
|
|
21
|
+
// pi-ai's exports map these subpaths for `import` only (./api/* -> ./dist/api/*.js), so find the package beside Pi's executable and
|
|
22
|
+
// import the files that map names.
|
|
23
|
+
const [{ existsSync, realpathSync }, { dirname, join }, { pathToFileURL }] = await Promise.all([import('node:fs'), import('node:path'), import('node:url')]);
|
|
24
|
+
const entry = process.argv[1];
|
|
25
|
+
if (!entry) return undefined;
|
|
26
|
+
let dir = dirname(realpathSync(entry)), root: string | undefined;
|
|
27
|
+
for (let up = 0; up < 12 && !root; up++, dir = dirname(dir)) {
|
|
28
|
+
const candidate = join(dir, 'node_modules', '@earendil-works', 'pi-ai');
|
|
29
|
+
if (existsSync(join(candidate, 'dist', 'api', 'simple-options.js'))) root = candidate;
|
|
30
|
+
if (dirname(dir) === dir) break;
|
|
31
|
+
}
|
|
32
|
+
if (!root) return undefined;
|
|
33
|
+
const [simple, estimate] = await Promise.all([
|
|
34
|
+
import(pathToFileURL(join(root, 'dist', 'api', 'simple-options.js')).href) as Promise<SimpleOptions>,
|
|
35
|
+
import(pathToFileURL(join(root, 'dist', 'utils', 'estimate.js')).href) as Promise<Estimate>]);
|
|
36
|
+
if (typeof simple.clampMaxTokensToContext !== 'function' || typeof estimate.estimateContextTokens !== 'function') return undefined;
|
|
37
|
+
return { clampMaxTokensToContext: simple.clampMaxTokensToContext, estimateContextTokens: estimate.estimateContextTokens };
|
|
38
|
+
} catch { return undefined; }
|
|
39
|
+
}
|
|
40
|
+
|
|
6
41
|
/** Read Pi's private safety allowance through its public clamp, rather than copying it.
|
|
7
|
-
*
|
|
42
|
+
* Without Pi's request arithmetic, fall back to Pi 1.2–1.5's documented 4,096-token allowance.
|
|
8
43
|
*/
|
|
9
|
-
export async function piContextSafetyMargin(): Promise<number> {
|
|
44
|
+
export async function piContextSafetyMargin(math?: PiRequestMath): Promise<number> {
|
|
10
45
|
try {
|
|
11
|
-
const
|
|
12
|
-
import('@earendil-works/pi-ai/api/simple-options'), import('@earendil-works/pi-ai/utils/estimate'),
|
|
13
|
-
]);
|
|
46
|
+
const { clampMaxTokensToContext, estimateContextTokens } = math ?? await loadPiRequestMath() ?? (() => { throw new Error('No Pi request math'); })();
|
|
14
47
|
const window = 1_000_000, context = normalizeContext({ messages: [] });
|
|
15
48
|
const margin = window - estimateContextTokens(context).tokens - clampMaxTokensToContext({ contextWindow: window, maxTokens: window } as never, context, window);
|
|
16
49
|
if (!Number.isSafeInteger(margin) || margin < 0 || margin >= window) throw new Error('Invalid Pi context margin');
|