pi-distill 0.3.0 → 0.3.1

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/README.md CHANGED
@@ -4,6 +4,40 @@
4
4
 
5
5
  `pi-distill` is a Pi extension that controls how tool results enter the agent context. It does not replace tools or change how commands run; it adds an optional result-processing layer after the tool has returned its real output.
6
6
 
7
+ ## What it solves
8
+
9
+ Coding agents often need only the important lines from a command, search, or file read. Passing every byte of a large result into the next turn increases context usage and can hide the signal in logs or generated files. `pi-distill` adds a result-level distillation layer without replacing Pi's built-in tools.
10
+
11
+ ## Context savings in practice
12
+
13
+ Build logs, diff output, and test reports often contain repeated status lines, unchanged context, stack-trace noise, and details that are not needed for the next decision. Those are strong candidates for high compression. In one real Pi session, the result below went from 51,215 characters to 240 characters: **213.40× compression and 99.5% fewer output characters**.
14
+
15
+ ![pi-distill context savings example](./assets/context-savings-example.png)
16
+
17
+ The screenshot reports character reduction, not an exact tokenizer measurement. In practice this usually removes a similar order of magnitude of context tokens, but the exact token saving depends on the language, content, and model tokenizer. Treat 90%+ as an observed outcome for suitable verbose outputs, not a guarantee for every command; use `RAW` whenever the complete output is needed.
18
+
19
+ | Scenario | Typical noise | What the distill result keeps |
20
+ | --- | --- | --- |
21
+ | Build / compile | Repeated progress, warnings, and unchanged setup lines | Pass/fail, first actionable errors, affected files, and next steps |
22
+ | Diff inspection | Large unchanged hunks and formatting noise | Changed files, relevant hunks, and review-relevant facts |
23
+ | Tests | Per-test verbosity, snapshots, and framework boilerplate | Totals, failed cases, key assertions, and useful diagnostics |
24
+
25
+ ## Prompt language
26
+
27
+ The distillation prompt strictly follows the current locale selected by `/pi-language`. Changing the persisted locale is picked up on the next tool call, including when the language command and `pi-distill` are loaded from separate package instances. `PI_EXTENSIONS_LOCALE` remains the explicit environment-variable override. The original user message is included only as language context and never overrides the selected locale.
28
+
29
+ ## How it works
30
+
31
+ - Observes `bash`, `read`, `grep`, and `find` through Pi's native `tool_call` / `tool_result` events.
32
+ - Uses the tool's `outputPrompt` as the source of truth for whether and how to distill a result.
33
+ - Treats a prompt containing only `RAW` as an explicit request for the original output.
34
+ - Uses the current session model by default, or a configured `provider/model` override.
35
+ - Keeps diagnostic metadata such as status, character counts, compression ratio, duration, and anomalies in the tool result details.
36
+ - Writes oversized distilled output or final output to a temporary file and returns its path instead of overflowing the tool result.
37
+ - Adds a compact audit card when the active Pi display middleware is available, with a fallback renderer otherwise. The shared display protocol is provided by `pi-extensions-tool-display`.
38
+
39
+ It does not register a second `bash`, `read`, `grep`, or `find` tool.
40
+
7
41
  ## Install
8
42
 
9
43
  ```bash
@@ -99,7 +133,7 @@ The distillation prompt strictly follows the locale selected by `/pi-language`:
99
133
  ## Scope and boundaries
100
134
 
101
135
  - Handles every active tool with an object parameter schema; whether `outputPrompt` can be injected is determined by the tool schema, not a fixed allowlist.
102
- - Registers no replacement tools, does not change tool execution semantics, and does not depend on `pi-tool-display`.
136
+ - Registers no replacement tools, does not change tool execution semantics, and does not depend on the unrelated npm package `pi-tool-display`.
103
137
  - Text distillation is lossy; use `RAW` when completeness matters.
104
138
  - Non-text results are a completeness boundary: images, audio, binary data, and mixed content bypass text distillation.
105
139
  - Oversized distilled or final text is written to a temporary file and represented by its path, preventing unbounded context growth.
package/README.zh-CN.md CHANGED
@@ -4,6 +4,40 @@
4
4
 
5
5
  `pi-distill` 是一个 Pi 扩展:它不替换工具,也不改变命令的执行方式,只在工具已经返回真实结果之后,帮助 Agent 决定哪些内容值得进入下一轮上下文。
6
6
 
7
+ ## 解决什么问题
8
+
9
+ 编码 Agent 通常只需要命令、搜索或文件读取结果中的关键信息。把大段日志、生成文件或搜索结果完整塞入下一轮,会增加上下文消耗,也容易让有效信号被噪声淹没。`pi-distill` 在不替换 Pi 内置工具的前提下,增加一层结果级提炼。
10
+
11
+ ## 实际上下文节省效果
12
+
13
+ 构建日志、diff 输出和测试报告经常包含重复状态行、未变化上下文、堆栈噪声,以及下一步决策并不需要的细节。这些内容通常很适合高比例压缩。下面这张真实 Pi 会话截图中,结果从 51,215 个字符压缩到 240 个字符:**213.40 倍压缩,输出字符减少 99.5%**。
14
+
15
+ ![pi-distill 上下文节省示例](./assets/context-savings-example.png)
16
+
17
+ 截图统计的是字符减少比例,不是 tokenizer 得出的精确 token 统计。实际使用时通常会带来同量级的上下文 token 节省,但精确数值取决于语言、内容和模型 tokenizer。对于适合压缩的冗长输出,90% 以上是已经观察到的效果,但不是每个命令的保证;需要完整输出时请使用 `RAW`。
18
+
19
+ | 场景 | 常见噪声 | 提炼结果保留 |
20
+ | --- | --- | --- |
21
+ | 构建 / 编译 | 重复进度、警告和未变化的环境信息 | 成功/失败、首个可行动错误、受影响文件和后续步骤 |
22
+ | Diff 检查 | 大量未变化 hunk 和格式化噪声 | 变更文件、相关 hunk 和评审所需事实 |
23
+ | 测试 | 单测逐条输出、snapshot 和框架模板 | 总数、失败用例、关键断言和有效诊断 |
24
+
25
+ ## Prompt 语言
26
+
27
+ 提炼 prompt 会严格跟随 `/pi-language` 当前选择的语言。持久化语言发生变化后,下一次工具调用会读取新设置,即使语言命令和 `pi-distill` 来自不同的包实例也可以同步。`PI_EXTENSIONS_LOCALE` 仍然是显式的环境变量覆盖项。原始用户消息只作为语言上下文传入,不能覆盖已选择的语言。
28
+
29
+ ## 工作方式
30
+
31
+ - 通过 Pi 原生的 `tool_call` / `tool_result` 事件监听 `bash`、`read`、`grep` 和 `find`。
32
+ - 以工具的 `outputPrompt` 作为是否提炼、如何提炼的依据。
33
+ - 当提示词严格只有 `RAW` 时,视为明确要求返回原始输出。
34
+ - 默认使用当前会话模型,也可以配置独立的 `provider/model`。
35
+ - 在工具结果 details 中保留状态、字符数、压缩比、耗时和异常等诊断信息。
36
+ - 提炼结果或最终返回结果过大时写入临时文件,只把文件路径返回给 Agent,避免工具结果失控膨胀。
37
+ - 当前 Pi 展示中间件可用时显示紧凑审计卡片,否则使用自己的 fallback renderer。展示协议由公共运行库 `pi-extensions-tool-display` 提供。
38
+
39
+ 它不会注册第二个 `bash`、`read`、`grep` 或 `find` 工具。
40
+
7
41
  ## 安装
8
42
 
9
43
  ```bash
@@ -101,7 +135,7 @@ Agent 消费更适合当前决策的结果,并获得可审计的处理诊断
101
135
  ## 覆盖范围与边界
102
136
 
103
137
  - 自动处理所有当前已启用且参数 schema 为 object 的工具;能否注入 `outputPrompt` 由工具 schema 决定,不维护固定工具名单。
104
- - 不注册替代工具,不改变原工具的执行语义,也不依赖 `pi-tool-display`。
138
+ - 不注册替代工具,不改变原工具的执行语义,也不依赖无关的 npm 包 `pi-tool-display`。
105
139
  - 文本提炼是有损操作;完整性要求应使用 `RAW`。
106
140
  - 非文本结果是完整性边界:图片、音频、二进制和混合 content 不进入文本提炼链路。
107
141
  - 提炼结果或最终文本过大时写入临时文件并返回路径,避免上下文无限膨胀。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-distill",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Pi tool-output distillation with file-first configuration",
5
5
  "type": "module",
6
6
  "files": [
@@ -51,6 +51,9 @@
51
51
  "@earendil-works/pi-tui": ">=0.80.0 <0.81.0",
52
52
  "pi-extensions-i18n": "^0.3.0"
53
53
  },
54
+ "dependencies": {
55
+ "pi-extensions-tool-display": "^0.1.1"
56
+ },
54
57
  "devDependencies": {
55
58
  "@earendil-works/pi-ai": "0.80.10",
56
59
  "@earendil-works/pi-coding-agent": "0.80.10",
@@ -1,47 +1,14 @@
1
- import { Container, type Component } from "@earendil-works/pi-tui";
1
+ import {
2
+ appendResultRenderPanel,
3
+ isResultRenderMiddlewareActive,
4
+ registerResultRenderMiddleware,
5
+ type ResultMiddleware,
6
+ } from "pi-extensions-tool-display";
2
7
  import { buildDistillAuditLines, createDistillAuditComponent, resolveDistillRenderConfig } from "./fallback-renderer.ts";
3
8
  import { loadDistillConfig } from "./summary-utils.ts";
4
9
 
5
- const TOOL_DISPLAY_API_KEY = Symbol.for("pi-tool-display.api.v1");
6
- const PENDING_MIDDLEWARES_KEY = Symbol.for("pi-tool-display.pendingResultRenderMiddlewares.v1");
7
10
  const DISTILL_MIDDLEWARE_ID = "pi-distill.result-renderer.v1";
8
11
 
9
- type RenderTheme = {
10
- fg(color: string, text: string): string;
11
- bold(text: string): string;
12
- };
13
-
14
- type MiddlewareContext = {
15
- toolName: string;
16
- result: unknown;
17
- options: { expanded?: boolean };
18
- theme: RenderTheme;
19
- };
20
-
21
- type ResultMiddleware = (context: MiddlewareContext, next: () => unknown) => unknown;
22
-
23
- type MiddlewareRegistration = {
24
- id: string;
25
- toolName: string;
26
- middleware: ResultMiddleware;
27
- };
28
-
29
- type ToolDisplayApi = {
30
- registerResultRenderMiddleware?(registration: MiddlewareRegistration): string;
31
- unregisterResultRenderMiddleware?(id: string): boolean;
32
- hasResultRenderMiddleware?(id: string): boolean;
33
- isResultRenderPipelineActive?(toolName: string): boolean;
34
- };
35
-
36
- type GlobalProtocol = typeof globalThis & {
37
- [TOOL_DISPLAY_API_KEY]?: ToolDisplayApi;
38
- [PENDING_MIDDLEWARES_KEY]?: MiddlewareRegistration[];
39
- };
40
-
41
- function getApi(): ToolDisplayApi | undefined {
42
- return (globalThis as GlobalProtocol)[TOOL_DISPLAY_API_KEY];
43
- }
44
-
45
12
  function getDetails(result: unknown): Record<string, unknown> | undefined {
46
13
  if (!result || typeof result !== "object" || Array.isArray(result)) return undefined;
47
14
  const details = (result as Record<string, unknown>).details;
@@ -50,12 +17,6 @@ function getDetails(result: unknown): Record<string, unknown> | undefined {
50
17
  : undefined;
51
18
  }
52
19
 
53
- function asComponent(value: unknown): Component | undefined {
54
- return value && typeof value === "object" && typeof (value as Component).render === "function"
55
- ? value as Component
56
- : undefined;
57
- }
58
-
59
20
  const distillMiddleware: ResultMiddleware = (context, next) => {
60
21
  const details = getDetails(context.result);
61
22
  if (!details) return next();
@@ -75,49 +36,17 @@ const distillMiddleware: ResultMiddleware = (context, next) => {
75
36
  && details.summaryText.trim().length > 0;
76
37
  if (summarized) return panel;
77
38
 
78
- const base = asComponent(next());
79
- if (!base) return panel;
80
- const container = new Container();
81
- container.addChild(base);
82
- container.addChild(panel);
83
- return container;
39
+ return appendResultRenderPanel(next(), panel);
84
40
  };
85
41
 
86
- function queueRegistration(registration: MiddlewareRegistration): void {
87
- const globalProtocol = globalThis as GlobalProtocol;
88
- const queue = Array.isArray(globalProtocol[PENDING_MIDDLEWARES_KEY])
89
- ? globalProtocol[PENDING_MIDDLEWARES_KEY]!
90
- : [];
91
- const index = queue.findIndex((entry) => entry?.id === registration.id);
92
- if (index >= 0) queue[index] = registration;
93
- else queue.push(registration);
94
- globalProtocol[PENDING_MIDDLEWARES_KEY] = queue;
95
- }
96
-
97
42
  export function registerDistillToolDisplayMiddleware(): () => void {
98
- const registration: MiddlewareRegistration = {
43
+ return registerResultRenderMiddleware({
99
44
  id: DISTILL_MIDDLEWARE_ID,
100
45
  toolName: "*",
101
46
  middleware: distillMiddleware,
102
- };
103
- const api = getApi();
104
- if (typeof api?.registerResultRenderMiddleware === "function") {
105
- api.registerResultRenderMiddleware(registration);
106
- } else {
107
- queueRegistration(registration);
108
- }
109
-
110
- return () => {
111
- getApi()?.unregisterResultRenderMiddleware?.(DISTILL_MIDDLEWARE_ID);
112
- const queue = (globalThis as GlobalProtocol)[PENDING_MIDDLEWARES_KEY];
113
- if (!Array.isArray(queue)) return;
114
- const index = queue.findIndex((entry) => entry?.id === DISTILL_MIDDLEWARE_ID);
115
- if (index >= 0) queue.splice(index, 1);
116
- };
47
+ });
117
48
  }
118
49
 
119
50
  export function isDistillToolDisplayMiddlewareActive(toolName: string): boolean {
120
- const api = getApi();
121
- return api?.hasResultRenderMiddleware?.(DISTILL_MIDDLEWARE_ID) === true
122
- && api.isResultRenderPipelineActive?.(toolName) === true;
51
+ return isResultRenderMiddlewareActive(DISTILL_MIDDLEWARE_ID, toolName);
123
52
  }