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.
Files changed (60) hide show
  1. package/LICENSE +1 -0
  2. package/README.md +141 -81
  3. package/dist/abort.d.ts +1 -0
  4. package/dist/branch-guard.d.ts +8 -0
  5. package/dist/cache-tool.d.ts +12 -0
  6. package/dist/chunk-PPYOZRUP.js +18 -0
  7. package/dist/chunk-PPYOZRUP.js.map +1 -0
  8. package/dist/commands.d.ts +8 -0
  9. package/dist/compat.d.ts +23 -0
  10. package/dist/compress-model.d.ts +65 -0
  11. package/dist/compress-tool.d.ts +76 -0
  12. package/dist/config-dir.d.ts +32 -0
  13. package/dist/config-panel.d.ts +61 -0
  14. package/dist/config.d.ts +296 -0
  15. package/dist/contract.d.ts +51 -0
  16. package/dist/contract.js +9 -0
  17. package/dist/contract.js.map +1 -0
  18. package/dist/decompress-tool.d.ts +19 -0
  19. package/dist/degeneration.d.ts +98 -0
  20. package/dist/export.d.ts +20 -0
  21. package/dist/floor-stale.d.ts +31 -0
  22. package/dist/host.d.ts +15 -0
  23. package/dist/idle-compact.d.ts +39 -0
  24. package/dist/index.d.ts +10 -0
  25. package/dist/index.js +23169 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/live-only-tail.d.ts +10 -0
  28. package/dist/log.d.ts +18 -0
  29. package/dist/messages.d.ts +85 -0
  30. package/dist/omp.d.ts +19 -0
  31. package/dist/overflow-selfheal.d.ts +132 -0
  32. package/dist/prompt-pack.d.ts +53 -0
  33. package/dist/proxy-detect.d.ts +21 -0
  34. package/dist/reasoning-drop.d.ts +68 -0
  35. package/dist/rule-tool.d.ts +8 -0
  36. package/dist/runtime.d.ts +199 -0
  37. package/dist/search-index.d.ts +20 -0
  38. package/dist/search-tool.d.ts +10 -0
  39. package/dist/sequence-match.d.ts +7 -0
  40. package/dist/session-log.d.ts +23 -0
  41. package/dist/state-rebuild.d.ts +30 -0
  42. package/dist/state.d.ts +44 -0
  43. package/dist/status-tool.d.ts +13 -0
  44. package/dist/strip-images.d.ts +18 -0
  45. package/dist/summary-sanitize.d.ts +8 -0
  46. package/dist/surface.d.ts +17 -0
  47. package/dist/system-passthrough.d.ts +4 -0
  48. package/dist/system-prompt.d.ts +20 -0
  49. package/dist/tag-tokens.d.ts +3 -0
  50. package/dist/throttle-retry.d.ts +52 -0
  51. package/dist/tokens.d.ts +61 -0
  52. package/dist/tool-guardrails.d.ts +33 -0
  53. package/dist/tool-pair-sanitizer.d.ts +8 -0
  54. package/dist/turn-boundary.d.ts +53 -0
  55. package/dist/user-config.d.ts +67 -0
  56. package/lean/index.ts +376 -0
  57. package/package.json +85 -36
  58. package/schema/bcp-block-v1.json +42 -0
  59. package/README.zh-CN.md +0 -79
  60. 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
- "name": "billion-context-pi-lean",
3
- "version": "0.1.69-lean.1",
4
- "type": "module",
5
- "description": "Token-lean Pi facade for billion-context-pi with the full upstream context engine.",
6
- "license": "MIT",
7
- "repository": {
8
- "type": "git",
9
- "url": "git+https://github.com/kunkun9527/billion-context-pi-lean.git"
10
- },
11
- "homepage": "https://github.com/kunkun9527/billion-context-pi-lean#readme",
12
- "bugs": {
13
- "url": "https://github.com/kunkun9527/billion-context-pi-lean/issues"
14
- },
15
- "files": [
16
- "index.ts",
17
- "README.md",
18
- "README.zh-CN.md",
19
- "LICENSE"
20
- ],
21
- "scripts": {
22
- "test": "node --test tests/*.test.mjs",
23
- "typecheck": "tsc -p tsconfig.json",
24
- "check": "npm run typecheck && npm test"
25
- },
26
- "dependencies": {
27
- "billion-context-pi": "0.1.69",
28
- "typebox": "1.3.7"
29
- },
30
- "devDependencies": {
31
- "typescript": "5.9.3"
32
- },
33
- "pi": {
34
- "extensions": [
35
- "./index.ts"
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)。