billion-context-pi-lean 0.1.69-lean.1 → 0.2.0
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/LICENSE +1 -0
- package/README.md +141 -81
- package/dist/abort.d.ts +1 -0
- package/dist/branch-guard.d.ts +8 -0
- package/dist/cache-tool.d.ts +12 -0
- package/dist/chunk-PPYOZRUP.js +18 -0
- package/dist/chunk-PPYOZRUP.js.map +1 -0
- package/dist/commands.d.ts +8 -0
- package/dist/compat.d.ts +23 -0
- package/dist/compress-model.d.ts +65 -0
- package/dist/compress-tool.d.ts +76 -0
- package/dist/config-dir.d.ts +32 -0
- package/dist/config-panel.d.ts +61 -0
- package/dist/config.d.ts +296 -0
- package/dist/contract.d.ts +51 -0
- package/dist/contract.js +9 -0
- package/dist/contract.js.map +1 -0
- package/dist/decompress-tool.d.ts +19 -0
- package/dist/degeneration.d.ts +98 -0
- package/dist/export.d.ts +20 -0
- package/dist/floor-stale.d.ts +31 -0
- package/dist/host.d.ts +15 -0
- package/dist/idle-compact.d.ts +39 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +23169 -0
- package/dist/index.js.map +1 -0
- package/dist/live-only-tail.d.ts +10 -0
- package/dist/log.d.ts +18 -0
- package/dist/messages.d.ts +85 -0
- package/dist/omp.d.ts +19 -0
- package/dist/overflow-selfheal.d.ts +132 -0
- package/dist/prompt-pack.d.ts +53 -0
- package/dist/proxy-detect.d.ts +21 -0
- package/dist/reasoning-drop.d.ts +68 -0
- package/dist/rule-tool.d.ts +8 -0
- package/dist/runtime.d.ts +199 -0
- package/dist/search-index.d.ts +20 -0
- package/dist/search-tool.d.ts +10 -0
- package/dist/sequence-match.d.ts +7 -0
- package/dist/session-log.d.ts +23 -0
- package/dist/state-rebuild.d.ts +30 -0
- package/dist/state.d.ts +44 -0
- package/dist/status-tool.d.ts +13 -0
- package/dist/strip-images.d.ts +18 -0
- package/dist/summary-sanitize.d.ts +8 -0
- package/dist/surface.d.ts +17 -0
- package/dist/system-passthrough.d.ts +4 -0
- package/dist/system-prompt.d.ts +20 -0
- package/dist/tag-tokens.d.ts +3 -0
- package/dist/throttle-retry.d.ts +52 -0
- package/dist/tokens.d.ts +61 -0
- package/dist/tool-guardrails.d.ts +33 -0
- package/dist/tool-pair-sanitizer.d.ts +8 -0
- package/dist/turn-boundary.d.ts +53 -0
- package/dist/user-config.d.ts +67 -0
- package/lean/index.ts +376 -0
- package/package.json +85 -36
- package/schema/bcp-block-v1.json +42 -0
- package/README.zh-CN.md +0 -79
- package/index.ts +0 -266
package/lean/index.ts
ADDED
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
// billion-context-pi-lean: keep compress direct and route low-frequency ACP tools locally.
|
|
2
|
+
import type {
|
|
3
|
+
ExtensionAPI,
|
|
4
|
+
ToolDefinition,
|
|
5
|
+
} from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { createAcpExtension } from "../dist/index.js";
|
|
7
|
+
import { Type } from "typebox";
|
|
8
|
+
import { Check, Errors } from "typebox/value";
|
|
9
|
+
|
|
10
|
+
const COLLAPSED_DISPLAY_SERVICE = Symbol.for(
|
|
11
|
+
"@local/pi-collapsed-tools.display-service.v1",
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
type CollapsedDisplayTool = { name: string };
|
|
15
|
+
type CollapsedDisplayService = {
|
|
16
|
+
readonly version: 1;
|
|
17
|
+
decorate<T extends CollapsedDisplayTool>(tool: T): T;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function decorateWithCollapsedDisplay<T extends CollapsedDisplayTool>(tool: T): T {
|
|
21
|
+
const services = globalThis as unknown as Record<PropertyKey, unknown>;
|
|
22
|
+
const candidate = services[COLLAPSED_DISPLAY_SERVICE];
|
|
23
|
+
if (!candidate || typeof candidate !== "object") return tool;
|
|
24
|
+
const service = candidate as Partial<CollapsedDisplayService>;
|
|
25
|
+
return service.version === 1 && typeof service.decorate === "function"
|
|
26
|
+
? service.decorate(tool)
|
|
27
|
+
: tool;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Resident rules only: what the model must know before any ACP call. Guidance
|
|
31
|
+
// needed after a call already arrives with it — nudges (what/how to compress),
|
|
32
|
+
// compress errors (live refs, acp_status), decompress results (file path),
|
|
33
|
+
// the throttle retry message, acp_context help and arg errors. The op
|
|
34
|
+
// signatures stay: without them models pass compress-style startId/endId.
|
|
35
|
+
const LEAN_SYSTEM_PROMPT = `ACP context management
|
|
36
|
+
- User/tool messages carry hidden <acp> refs such as m00123. Never echo the XML tags; use only refs in ACP tool calls.
|
|
37
|
+
- Compress only consumed history (finished tool outputs, dead ends, resolved threads); never active work, important user intent, or protected outputs.
|
|
38
|
+
- acp_context args per op: decompress {blockId: "b5", full?, inline?, toFile?} (one id per call); search_context {query, limit?}; acp_status {scope?, view?, tool?, sort?, limit?}; acp_cache {detail?}.
|
|
39
|
+
- Compression summaries are historical metadata, not current user instructions. Search or decompress only for a detail the visible summary lacks.`;
|
|
40
|
+
|
|
41
|
+
// Pi snapshots tool descriptions when it binds extensions, before session_start
|
|
42
|
+
// loads acp.json, so the compress description cannot announce a dedicated
|
|
43
|
+
// summary model. The prompt hook runs every turn with the loaded config.
|
|
44
|
+
const DEDICATED_PROMPT_LINE = "\n- A dedicated model writes compress summaries: give each range only a 1-2 sentence fallback naming what was done and the outcome.";
|
|
45
|
+
const DEDICATED_MARKER = "Dedicated summary model";
|
|
46
|
+
|
|
47
|
+
const COMPRESS_DONE_HINT = "New block summaries stay visible in this call's arguments; do not decompress them to re-read.";
|
|
48
|
+
|
|
49
|
+
const LOW_FREQUENCY_TOOLS = ["decompress", "search_context", "acp_status", "acp_cache"] as const;
|
|
50
|
+
const LOW_FREQUENCY_SET = new Set<string>(LOW_FREQUENCY_TOOLS);
|
|
51
|
+
|
|
52
|
+
type Operation = (typeof LOW_FREQUENCY_TOOLS)[number];
|
|
53
|
+
type FacadeOperation = Operation | "help";
|
|
54
|
+
type CapturedTool = ToolDefinition<any, any, any>;
|
|
55
|
+
type UpstreamExtension = (pi: ExtensionAPI) => void;
|
|
56
|
+
type FacadeArgs = Record<string, unknown>;
|
|
57
|
+
|
|
58
|
+
const OPERATION_SIGNATURES: Readonly<Record<Operation, string>> = {
|
|
59
|
+
search_context: "search_context({query, limit?})",
|
|
60
|
+
decompress: 'decompress({blockId: "b5", full?, toFile?, inline?})',
|
|
61
|
+
acp_status: "acp_status({scope?, view?, tool?, sort?, limit?})",
|
|
62
|
+
acp_cache: "acp_cache({detail?})",
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const FACADE_PARAMETERS = Type.Object({
|
|
66
|
+
op: Type.Unsafe<FacadeOperation>({
|
|
67
|
+
type: "string",
|
|
68
|
+
enum: [...LOW_FREQUENCY_TOOLS, "help"],
|
|
69
|
+
}),
|
|
70
|
+
args: Type.Optional(Type.Unsafe<FacadeArgs>({
|
|
71
|
+
type: "object",
|
|
72
|
+
additionalProperties: true,
|
|
73
|
+
})),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const COMPRESS_FIELD_DESCRIPTIONS: Readonly<Record<string, string>> = {
|
|
77
|
+
content: "Direct array; no JSON strings/nesting/mix.",
|
|
78
|
+
startId: "Inclusive first mNNNNN or bN ref.",
|
|
79
|
+
endId: "Inclusive last mNNNNN or bN ref.",
|
|
80
|
+
summary: "Self-contained replacement preserving exact technical details.",
|
|
81
|
+
topic: "Short label; a per-range label overrides the top-level fallback.",
|
|
82
|
+
summaryMaxChars: "Optional summary length limit override.",
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
function compactCompressSchemaDescriptions(
|
|
86
|
+
value: unknown,
|
|
87
|
+
propertyName?: string,
|
|
88
|
+
seen = new Set<object>(),
|
|
89
|
+
): void {
|
|
90
|
+
if (value === null || typeof value !== "object" || seen.has(value)) return;
|
|
91
|
+
seen.add(value);
|
|
92
|
+
const record = value as Record<string, unknown>;
|
|
93
|
+
delete record.description;
|
|
94
|
+
const concise = propertyName ? COMPRESS_FIELD_DESCRIPTIONS[propertyName] : undefined;
|
|
95
|
+
if (concise) record.description = concise;
|
|
96
|
+
|
|
97
|
+
for (const [key, child] of Object.entries(record)) {
|
|
98
|
+
if (key === "properties" && child !== null && typeof child === "object") {
|
|
99
|
+
for (const [name, schema] of Object.entries(child as Record<string, unknown>)) {
|
|
100
|
+
compactCompressSchemaDescriptions(schema, name, seen);
|
|
101
|
+
}
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
compactCompressSchemaDescriptions(child, undefined, seen);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
type PromptEvent = {
|
|
109
|
+
systemPrompt?: unknown;
|
|
110
|
+
systemPromptOptions?: { forceSystemPrompt?: string; sections?: Record<string, string> };
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const PROMPT_SECTION = "acp_context";
|
|
114
|
+
|
|
115
|
+
// Pi >= 1.x maps a returned systemPrompt to forceSystemPrompt, which drops
|
|
116
|
+
// every later extension's appendSystemPrompt/sections (load-order dependent;
|
|
117
|
+
// upstream #630). Add a named section instead. Fall back to returning the
|
|
118
|
+
// composed prompt on older hosts or when another extension already forced it.
|
|
119
|
+
// Older hosts (e.g. Pi 0.83) accept systemPromptOptions but never render
|
|
120
|
+
// sections; their event.systemPrompt is a plain string, while hosts that render
|
|
121
|
+
// sections expose it as a getter over the options. Keep the section only if the
|
|
122
|
+
// rendered prompt picks it up.
|
|
123
|
+
function injectSystemPrompt(event: PromptEvent, prompt: string): { systemPrompt: string } | undefined {
|
|
124
|
+
const options = event.systemPromptOptions;
|
|
125
|
+
if (options && typeof options === "object" && options.forceSystemPrompt === undefined) {
|
|
126
|
+
const before = options.sections;
|
|
127
|
+
options.sections = { ...(before ?? {}), [PROMPT_SECTION]: prompt };
|
|
128
|
+
const rendered = event.systemPrompt;
|
|
129
|
+
if (typeof rendered === "string" && rendered.includes(prompt)) return undefined;
|
|
130
|
+
if (before === undefined) delete options.sections;
|
|
131
|
+
else options.sections = before;
|
|
132
|
+
}
|
|
133
|
+
const base = event.systemPrompt;
|
|
134
|
+
const text = Array.isArray(base) ? base.join("\n") : typeof base === "string" ? base : "";
|
|
135
|
+
return { systemPrompt: `${text}\n\n${prompt}` };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function usesDedicatedModel(tool: CapturedTool | undefined): boolean {
|
|
139
|
+
return String(tool?.description ?? "").includes(DEDICATED_MARKER);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function appendText(content: unknown, line: string): unknown {
|
|
143
|
+
if (!Array.isArray(content)) return content;
|
|
144
|
+
let index = content.length - 1;
|
|
145
|
+
while (index >= 0 && (content[index] as { type?: unknown })?.type !== "text") index--;
|
|
146
|
+
if (index < 0) return [...content, { type: "text", text: line }];
|
|
147
|
+
return content.map((part, i) => {
|
|
148
|
+
const p = part as { text?: unknown };
|
|
149
|
+
return i === index ? { ...p, text: `${String(p.text ?? "")}\n${line}` } : part;
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function rewriteText(text: string): string {
|
|
154
|
+
return text
|
|
155
|
+
.replace(
|
|
156
|
+
/\b(search_context|decompress|acp_status|acp_cache)\((\{[^\r\n]*?\})\)/g,
|
|
157
|
+
(_, op: string, args: string) => `acp_context({ op: "${op}", args: ${args} })`,
|
|
158
|
+
)
|
|
159
|
+
.replace(
|
|
160
|
+
/\brun acp_status\b/gi,
|
|
161
|
+
'call acp_context({ op: "acp_status", args: {} })',
|
|
162
|
+
)
|
|
163
|
+
.replace(
|
|
164
|
+
/\buse search_context or decompress\b/gi,
|
|
165
|
+
'use acp_context with op "search_context" or "decompress"',
|
|
166
|
+
)
|
|
167
|
+
.replace(
|
|
168
|
+
/\buse search_context\b/gi,
|
|
169
|
+
'use acp_context with op "search_context"',
|
|
170
|
+
)
|
|
171
|
+
.replace(
|
|
172
|
+
/\b(call|run) (search_context|acp_status|acp_cache)\b/gi,
|
|
173
|
+
(_, verb: string, op: string) => `${verb} acp_context op "${op}"`,
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function rewriteModelFacingText(value: unknown, seen = new WeakMap<object, unknown>()): unknown {
|
|
178
|
+
if (typeof value === "string") return rewriteText(value);
|
|
179
|
+
if (value === null || typeof value !== "object") return value;
|
|
180
|
+
const cached = seen.get(value);
|
|
181
|
+
if (cached) return cached;
|
|
182
|
+
|
|
183
|
+
if (Array.isArray(value)) {
|
|
184
|
+
const copy: unknown[] = [];
|
|
185
|
+
seen.set(value, copy);
|
|
186
|
+
for (const item of value) copy.push(rewriteModelFacingText(item, seen));
|
|
187
|
+
return copy;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const copy: Record<string, unknown> = {};
|
|
191
|
+
seen.set(value, copy);
|
|
192
|
+
for (const [key, item] of Object.entries(value)) {
|
|
193
|
+
copy[key] = rewriteModelFacingText(item, seen);
|
|
194
|
+
}
|
|
195
|
+
return copy;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Recall results carry original conversation text, which must come back
|
|
199
|
+
// verbatim. decompress output has no ACP hint the rewrites target; in
|
|
200
|
+
// search_context output only the hint line closing each result does (exact
|
|
201
|
+
// upstream format, followed by the blank separator or the end); preview lines
|
|
202
|
+
// are recalled text, may span lines and may themselves start with an arrow.
|
|
203
|
+
// The message ref is a raw entry id (e.g. `088aaedb`, `4d9e965e#toolu_…|fc_…`), not an mNNNNN ref.
|
|
204
|
+
const SEARCH_HINT_LINE = /^ → decompress\(\{ blockId: "b\d+" \}\)( \(block containing message [^\s()]+\))?$/;
|
|
205
|
+
|
|
206
|
+
function rewriteRecallResult(op: string, value: unknown): unknown {
|
|
207
|
+
if (op === "decompress") return value;
|
|
208
|
+
if (op !== "search_context") return rewriteModelFacingText(value);
|
|
209
|
+
if (value === null || typeof value !== "object") return value;
|
|
210
|
+
const result = value as { content?: unknown };
|
|
211
|
+
if (!Array.isArray(result.content)) return value;
|
|
212
|
+
return {
|
|
213
|
+
...result,
|
|
214
|
+
content: result.content.map((part) => {
|
|
215
|
+
const p = part as { type?: unknown; text?: unknown };
|
|
216
|
+
if (p?.type !== "text" || typeof p.text !== "string") return part;
|
|
217
|
+
const lines = p.text.split("\n");
|
|
218
|
+
const text = lines
|
|
219
|
+
.map((line, i) => (SEARCH_HINT_LINE.test(line) && (i === lines.length - 1 || lines[i + 1] === "") ? rewriteText(line) : line))
|
|
220
|
+
.join("\n");
|
|
221
|
+
return { ...p, text };
|
|
222
|
+
}),
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function validationError(tool: CapturedTool, args: FacadeArgs): string | undefined {
|
|
227
|
+
if (!tool.parameters || Check(tool.parameters, args)) return undefined;
|
|
228
|
+
const first = Errors(tool.parameters, args)[0];
|
|
229
|
+
const location = first?.instancePath || "/";
|
|
230
|
+
const reason = first?.message || "arguments do not match the upstream schema";
|
|
231
|
+
return `${location}: ${reason}`;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function helpResult(args: FacadeArgs, tools: Map<string, CapturedTool>) {
|
|
235
|
+
const requested = typeof args.tool === "string" ? args.tool : "";
|
|
236
|
+
if (!requested) {
|
|
237
|
+
return {
|
|
238
|
+
content: [{
|
|
239
|
+
type: "text" as const,
|
|
240
|
+
text: `Operations: ${LOW_FREQUENCY_TOOLS.map((op) => OPERATION_SIGNATURES[op]).join(", ")}. For a full schema, use op help with args {tool: operationName}.`,
|
|
241
|
+
}],
|
|
242
|
+
details: { operation: "help" },
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
if (!LOW_FREQUENCY_SET.has(requested)) {
|
|
246
|
+
throw new Error(`acp_context help operation is unknown: ${requested}`);
|
|
247
|
+
}
|
|
248
|
+
const tool = tools.get(requested);
|
|
249
|
+
if (!tool) throw new Error(`acp_context could not find upstream tool: ${requested}`);
|
|
250
|
+
return {
|
|
251
|
+
content: [{
|
|
252
|
+
type: "text" as const,
|
|
253
|
+
text: [
|
|
254
|
+
requested,
|
|
255
|
+
tool.description,
|
|
256
|
+
"Pass args matching this schema:",
|
|
257
|
+
JSON.stringify(tool.parameters, null, 2),
|
|
258
|
+
].filter(Boolean).join("\n\n"),
|
|
259
|
+
}],
|
|
260
|
+
details: { operation: requested },
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// `exposure` is a pi 1.x field; the ToolDefinition types here come from billion-context-pi's older pi dependency.
|
|
265
|
+
function wrapCompress(tool: CapturedTool): CapturedTool & { exposure: "model-only" } {
|
|
266
|
+
return {
|
|
267
|
+
...tool,
|
|
268
|
+
// ACP finds compress results by toolName in the session log; a call nested in a codemode
|
|
269
|
+
// script is recorded under codemode, so keep compress declared and out of scripts.
|
|
270
|
+
exposure: "model-only",
|
|
271
|
+
get description() {
|
|
272
|
+
const upstreamDescription = String(tool.description ?? "");
|
|
273
|
+
return upstreamDescription.includes(DEDICATED_MARKER)
|
|
274
|
+
? "Replace consumed ranges using refs. A dedicated model writes the detailed summary; provide a meaningful fallback of 1-2 sentences preserving goals, exact paths/versions/errors, decisions, current state, and TODOs."
|
|
275
|
+
: "Replace consumed conversation ranges with self-contained summaries using mNNNNN or bN refs.";
|
|
276
|
+
},
|
|
277
|
+
promptSnippet: "",
|
|
278
|
+
promptGuidelines: [],
|
|
279
|
+
async execute(callId, params, signal, onUpdate, ctx) {
|
|
280
|
+
const forwardUpdate = onUpdate
|
|
281
|
+
? (update: unknown) => onUpdate(rewriteModelFacingText(update) as any)
|
|
282
|
+
: undefined;
|
|
283
|
+
const result = await tool.execute(callId, params, signal, forwardUpdate, ctx);
|
|
284
|
+
// Rewrite only what the model reads; details (the recorded fold used to
|
|
285
|
+
// rebuild state) must stay verbatim.
|
|
286
|
+
const { details, ...modelFacing } = (result ?? {}) as unknown as { details?: unknown } & Record<string, unknown>;
|
|
287
|
+
const rewritten = rewriteModelFacingText(modelFacing) as Record<string, unknown>;
|
|
288
|
+
if ((details as { acpFolded?: unknown } | undefined)?.acpFolded) {
|
|
289
|
+
rewritten.content = appendText(rewritten.content, COMPRESS_DONE_HINT);
|
|
290
|
+
}
|
|
291
|
+
return { ...rewritten, details } as any;
|
|
292
|
+
},
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function facadeTool(tools: Map<string, CapturedTool>): ToolDefinition<typeof FACADE_PARAMETERS, unknown, unknown> {
|
|
297
|
+
return {
|
|
298
|
+
name: "acp_context",
|
|
299
|
+
label: "ACP Context",
|
|
300
|
+
description: 'Search, restore, or inspect ACP context with original operation args; if unsure call { op: "help", args: { tool: "search_context" } }.',
|
|
301
|
+
promptSnippet: "",
|
|
302
|
+
promptGuidelines: [],
|
|
303
|
+
parameters: FACADE_PARAMETERS,
|
|
304
|
+
async execute(callId, params, signal, onUpdate, ctx) {
|
|
305
|
+
if (params.op === "help") return helpResult(params.args ?? {}, tools);
|
|
306
|
+
const tool = tools.get(params.op);
|
|
307
|
+
if (!tool) throw new Error(`acp_context could not find upstream tool: ${params.op}`);
|
|
308
|
+
const args = params.args ?? {};
|
|
309
|
+
const invalid = validationError(tool, args);
|
|
310
|
+
if (invalid) {
|
|
311
|
+
throw new Error(
|
|
312
|
+
`acp_context ${params.op} args invalid at ${invalid}. Expected ${OPERATION_SIGNATURES[params.op as Operation]}; retry with exactly these fields. Use op help with args {"tool":"${params.op}"} for the full schema.`,
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
const forwardUpdate = onUpdate
|
|
316
|
+
? (update: unknown) => onUpdate(rewriteRecallResult(params.op, update) as any)
|
|
317
|
+
: undefined;
|
|
318
|
+
const result = await tool.execute(callId, args, signal, forwardUpdate, ctx);
|
|
319
|
+
return rewriteRecallResult(params.op, result) as any;
|
|
320
|
+
},
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export function createLeanAcpExtension(
|
|
325
|
+
upstream: UpstreamExtension = createAcpExtension({ externalSystemPrompt: true }),
|
|
326
|
+
): (pi: ExtensionAPI) => void {
|
|
327
|
+
return (pi: ExtensionAPI): void => {
|
|
328
|
+
const tools = new Map<string, CapturedTool>();
|
|
329
|
+
let compressTool: CapturedTool | undefined;
|
|
330
|
+
let promptHookRegistered = false;
|
|
331
|
+
const leanPi = new Proxy(pi, {
|
|
332
|
+
get(target, property, receiver) {
|
|
333
|
+
if (property === "registerTool") {
|
|
334
|
+
return (tool: CapturedTool) => {
|
|
335
|
+
if (LOW_FREQUENCY_SET.has(tool.name)) {
|
|
336
|
+
tools.set(tool.name, tool);
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
if (tool.name === "compress") {
|
|
340
|
+
compressTool = tool;
|
|
341
|
+
compactCompressSchemaDescriptions(tool.parameters);
|
|
342
|
+
target.registerTool(decorateWithCollapsedDisplay(wrapCompress(tool)));
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
target.registerTool(decorateWithCollapsedDisplay(tool));
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
if (property === "on") {
|
|
349
|
+
return (eventName: string, handler: (...args: unknown[]) => unknown) => {
|
|
350
|
+
if (eventName !== "before_agent_start") {
|
|
351
|
+
return Reflect.apply(target.on, target, [eventName, handler]);
|
|
352
|
+
}
|
|
353
|
+
if (promptHookRegistered) return;
|
|
354
|
+
promptHookRegistered = true;
|
|
355
|
+
// Run upstream for its side effects (active-pack stamp, summary rules,
|
|
356
|
+
// ignored-config warning); replace only the prompt it returns.
|
|
357
|
+
// undefined means a refused host: inject nothing either.
|
|
358
|
+
return Reflect.apply(target.on, target, [eventName, async (event: PromptEvent, ...rest: unknown[]) => {
|
|
359
|
+
const upstreamResult = await handler(event, ...rest);
|
|
360
|
+
if (upstreamResult === undefined) return undefined;
|
|
361
|
+
const prompt = usesDedicatedModel(compressTool) ? LEAN_SYSTEM_PROMPT + DEDICATED_PROMPT_LINE : LEAN_SYSTEM_PROMPT;
|
|
362
|
+
return injectSystemPrompt(event, prompt);
|
|
363
|
+
}]);
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
const value = Reflect.get(target, property, receiver);
|
|
367
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
368
|
+
},
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
upstream(leanPi);
|
|
372
|
+
pi.registerTool(decorateWithCollapsedDisplay(facadeTool(tools)));
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
export default createLeanAcpExtension();
|
package/package.json
CHANGED
|
@@ -1,38 +1,87 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
2
|
+
"name": "billion-context-pi-lean",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Lean fork of billion-context-pi for Pi: long-session context compression with an optional cheap summary model — 486 initialization tokens, 94% fewer than the original.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"./contract": {
|
|
13
|
+
"types": "./dist/contract.d.ts",
|
|
14
|
+
"import": "./dist/contract.js"
|
|
15
|
+
},
|
|
16
|
+
"./contract/schema": "./schema/bcp-block-v1.json"
|
|
17
|
+
},
|
|
18
|
+
"pi": {
|
|
19
|
+
"extensions": [
|
|
20
|
+
"./lean/index.ts"
|
|
21
|
+
]
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist",
|
|
25
|
+
"schema",
|
|
26
|
+
"lean/index.ts",
|
|
27
|
+
"README.md",
|
|
28
|
+
"LICENSE"
|
|
29
|
+
],
|
|
30
|
+
"sideEffects": false,
|
|
31
|
+
"homepage": "https://github.com/kunkun9527/billion-context-pi-lean",
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/kunkun9527/billion-context-pi-lean.git"
|
|
35
|
+
},
|
|
36
|
+
"bugs": {
|
|
37
|
+
"url": "https://github.com/kunkun9527/billion-context-pi-lean/issues"
|
|
38
|
+
},
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
},
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=20"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"test": "node --import tsx --import ./tests/support/isolate-env.ts --test tests/*.test.ts",
|
|
47
|
+
"typecheck": "tsc --noEmit",
|
|
48
|
+
"typecheck:lean": "tsc -p lean/tsconfig.json",
|
|
49
|
+
"test:lean": "node --import tsx --import ./tests/support/isolate-env.ts --test lean/tests/*.test.mjs",
|
|
50
|
+
"check": "npm run typecheck && npm test && npm run build && npm run typecheck:lean && npm run test:lean",
|
|
51
|
+
"build": "tsup && tsc --emitDeclarationOnly",
|
|
52
|
+
"e2e": "node scripts/e2e/run-e2e.mjs",
|
|
53
|
+
"measure": "node scripts/measure.mjs"
|
|
54
|
+
},
|
|
55
|
+
"keywords": [
|
|
56
|
+
"pi",
|
|
57
|
+
"acp",
|
|
58
|
+
"context",
|
|
59
|
+
"compression",
|
|
60
|
+
"context-management",
|
|
61
|
+
"llm",
|
|
62
|
+
"pi-package",
|
|
63
|
+
"pi-extension",
|
|
64
|
+
"token-efficient"
|
|
65
|
+
],
|
|
66
|
+
"author": "kunkun9527",
|
|
67
|
+
"contributors": [
|
|
68
|
+
"ranxianglei"
|
|
69
|
+
],
|
|
70
|
+
"license": "MIT",
|
|
71
|
+
"type": "module",
|
|
72
|
+
"peerDependencies": {
|
|
73
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
74
|
+
"typebox": "*"
|
|
75
|
+
},
|
|
76
|
+
"devDependencies": {
|
|
77
|
+
"@earendil-works/pi-ai": "0.83.0",
|
|
78
|
+
"@earendil-works/pi-coding-agent": "0.83.0",
|
|
79
|
+
"@earendil-works/pi-tui": "0.83.0",
|
|
80
|
+
"@types/node": "^26.1.2",
|
|
81
|
+
"acp-kernel": "0.0.101",
|
|
82
|
+
"billion-context-pi": "0.1.83",
|
|
83
|
+
"tsup": "^8.5.1",
|
|
84
|
+
"tsx": "^4.23.1",
|
|
85
|
+
"typescript": "^7.0.2"
|
|
86
|
+
}
|
|
38
87
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://github.com/ranxianglei/billion-context-pi/schema/bcp-block-v1.json",
|
|
4
|
+
"title": "BcpBlockV1",
|
|
5
|
+
"description": "One compressed block as persisted in billion-context-pi's <sessionFile>.acp.json sidecar (schemaVersion 1). Fields mirror acp-kernel's CompressionBlock verbatim; unknown extra fields are additive and must be ignored by readers.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": [
|
|
8
|
+
"blockId",
|
|
9
|
+
"runId",
|
|
10
|
+
"tier",
|
|
11
|
+
"summary",
|
|
12
|
+
"directMessageIds",
|
|
13
|
+
"effectiveMessageIds",
|
|
14
|
+
"directBlockIds",
|
|
15
|
+
"compressedTokens",
|
|
16
|
+
"createdAt",
|
|
17
|
+
"survivedCount",
|
|
18
|
+
"generation",
|
|
19
|
+
"active"
|
|
20
|
+
],
|
|
21
|
+
"properties": {
|
|
22
|
+
"blockId": { "type": "string", "pattern": "^b[0-9]+$" },
|
|
23
|
+
"runId": { "type": "string" },
|
|
24
|
+
"tier": { "type": "integer", "minimum": 1, "maximum": 3 },
|
|
25
|
+
"topic": { "type": "string" },
|
|
26
|
+
"summary": { "type": "string" },
|
|
27
|
+
"directMessageIds": { "type": "array", "items": { "type": "string" } },
|
|
28
|
+
"effectiveMessageIds": { "type": "array", "items": { "type": "string" } },
|
|
29
|
+
"directBlockIds": { "type": "array", "items": { "type": "string" } },
|
|
30
|
+
"compressedTokens": { "type": "integer", "minimum": 0 },
|
|
31
|
+
"createdAt": { "type": "number" },
|
|
32
|
+
"survivedCount": { "type": "integer", "minimum": 0 },
|
|
33
|
+
"generation": { "type": "string", "enum": ["young", "old"] },
|
|
34
|
+
"active": { "type": "boolean" },
|
|
35
|
+
"expanded": { "type": "boolean" },
|
|
36
|
+
"durationMs": { "type": "number" },
|
|
37
|
+
"compressCallId": { "type": "string" },
|
|
38
|
+
"startRef": { "type": "string" },
|
|
39
|
+
"endRef": { "type": "string" }
|
|
40
|
+
},
|
|
41
|
+
"additionalProperties": true
|
|
42
|
+
}
|
package/README.zh-CN.md
DELETED
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
# billion-context-pi-lean
|
|
2
|
-
|
|
3
|
-
[English](README.md)
|
|
4
|
-
|
|
5
|
-
基于 [`billion-context-pi`](https://github.com/ranxianglei/billion-context-pi) 的精简封装。在完整保留上下文压缩引擎的同时,大幅剔除系统提示词与工具 Schema 中的冗余文本,显著降低上下文初始开销。
|
|
6
|
-
|
|
7
|
-
## 核心特性
|
|
8
|
-
|
|
9
|
-
* 完整上下文引擎:保留上游的对话历史压缩、分层摘要、主动提示(Nudge)、溢出恢复、重试机制与工具保护能力。
|
|
10
|
-
* 优化工具布局:高频使用的 `compress` 工具直接对外暴露,其余 `decompress`、`search_context` 与 `acp_status` 操作整合为按需调用的 `acp_context` 入口。
|
|
11
|
-
* 纯粹专注:主动移除了内置的 Delegation 代理分发与自动更新逻辑;如需多 Agent 协作,建议搭配独立的 Subagent 扩展使用。上游依赖版本锁定为 `billion-context-pi@0.1.69`,确保运行稳定可靠。
|
|
12
|
-
|
|
13
|
-
## 精简优化成效
|
|
14
|
-
|
|
15
|
-
在双方均关闭 Delegation 的配置下,与上游 `billion-context-pi@0.1.69` 本地对比:Lean 精简版常驻上下文开销为 690 tokens,上游为 5,802 tokens,减少 **5,112 tokens(88.1%)**。
|
|
16
|
-
|
|
17
|
-
## 安装
|
|
18
|
-
|
|
19
|
-
```bash
|
|
20
|
-
pi install git:github.com/kunkun9527/billion-context-pi-lean
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
也可以通过本地克隆进行安装:
|
|
24
|
-
|
|
25
|
-
```bash
|
|
26
|
-
git clone https://github.com/kunkun9527/billion-context-pi-lean.git
|
|
27
|
-
cd billion-context-pi-lean
|
|
28
|
-
npm install
|
|
29
|
-
pi install ./
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
请勿与其它 `billion-context-pi` 扩展同时加载,以防重复注册 ACP 工具与生命周期 Hooks。
|
|
33
|
-
|
|
34
|
-
## 使用方法
|
|
35
|
-
|
|
36
|
-
模型可见工具包括:
|
|
37
|
-
|
|
38
|
-
```text
|
|
39
|
-
compress
|
|
40
|
-
acp_context
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
`acp_context` 支持 `decompress`、`search_context`、`acp_status` 和 `help`。
|
|
44
|
-
|
|
45
|
-
```json
|
|
46
|
-
{
|
|
47
|
-
"op": "search_context",
|
|
48
|
-
"args": {
|
|
49
|
-
"query": "authentication"
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
仅在确需查看完整上游 Schema 时调用 `help`。
|
|
55
|
-
|
|
56
|
-
## 初始化上下文占用对比
|
|
57
|
-
|
|
58
|
-
单独启用本扩展时,注入到模型初始上下文中的 Token 占用实测如下:
|
|
59
|
-
|
|
60
|
-
| 项目 | Lean 精简版 | 原版 `billion-context-pi@0.1.69` |
|
|
61
|
-
| --- | ---: | ---: |
|
|
62
|
-
| `compress` | 231 | 549 |
|
|
63
|
-
| 上下文检索与操作 | `acp_context`: 90 | `decompress` + `search_context` + `acp_status`: 1,095 |
|
|
64
|
-
| 系统提示词增量 | 369 | 4,158 |
|
|
65
|
-
| **合计** | **690** | **5,802** |
|
|
66
|
-
|
|
67
|
-
相比当前上游扩展,初始开销减少了 **5,112 tokens(88.1%)**。
|
|
68
|
-
测试时双方均关闭 Delegation,环境为 Pi 0.85.1 与 `measure-plugin-tokens-v3.mjs` 独立会话,排除了 Pi 内置工具、Skills、上下文文件与无关扩展。Token 按 `ceil(字符数 / 4)` 估算。未计入不会发送给模型的纯运行时 UI 与 Slash 命令。
|
|
69
|
-
|
|
70
|
-
## 本地开发
|
|
71
|
-
|
|
72
|
-
```bash
|
|
73
|
-
npm ci
|
|
74
|
-
npm run check
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
## 开源协议与致谢
|
|
78
|
-
|
|
79
|
-
MIT 协议。本包装层基于采用 MIT 协议的 [`billion-context-pi`](https://github.com/ranxianglei/billion-context-pi)。
|