billion-context-pi 0.0.6 → 0.1.25
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 +31 -17
- package/README.zh-CN.md +31 -17
- package/dist/config.d.ts +18 -0
- package/dist/index.js +162 -22
- package/dist/index.js.map +1 -1
- package/dist/tool-guardrails.d.ts +9 -0
- package/dist/user-config.d.ts +2 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
#
|
|
1
|
+
# billion-context-pi
|
|
2
2
|
|
|
3
3
|
[English](./README.md) | [中文](./README.zh-CN.md)
|
|
4
4
|
|
|
5
5
|
<p align="center">
|
|
6
|
-
<strong>
|
|
6
|
+
<strong>Billion-Context</strong> for <a href="https://pi.dev">Pi</a>
|
|
7
7
|
<br />
|
|
8
8
|
The model decides <em>when</em> and <em>what</em> to compress — not a hard limit.
|
|
9
9
|
</p>
|
|
@@ -11,22 +11,22 @@ The model decides <em>when</em> and <em>what</em> to compress — not a hard lim
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
13
|
<p align="center">
|
|
14
|
-
<a href="https://www.npmjs.com/package/
|
|
15
|
-
<a href="https://github.com/ranxianglei/
|
|
16
|
-
<a href="https://github.com/ranxianglei/
|
|
14
|
+
<a href="https://www.npmjs.com/package/billion-context-pi"><img src="https://img.shields.io/npm/v/billion-context-pi.svg?style=flat-square" alt="npm"></a>
|
|
15
|
+
<a href="https://github.com/ranxianglei/billion-context-pi/blob/master/LICENSE"><img src="https://img.shields.io/npm/l/billion-context-pi.svg?style=flat-square" alt="license"></a>
|
|
16
|
+
<a href="https://github.com/ranxianglei/billion-context-pi"><img src="https://img.shields.io/badge/GitHub-ranxianglei%2Fbillion--context--pi-181717?style=flat-square&logo=github" alt="GitHub"></a>
|
|
17
17
|
</p>
|
|
18
18
|
|
|
19
19
|
<p align="center">
|
|
20
|
-
<code>pi install npm:
|
|
20
|
+
<code>pi install npm:billion-context-pi</code>
|
|
21
21
|
</p>
|
|
22
22
|
|
|
23
23
|
---
|
|
24
24
|
|
|
25
25
|
## Why?
|
|
26
26
|
|
|
27
|
-
When conversations get long, the model runs out of context. Most tools hard-truncate — silently dropping earlier messages. **
|
|
27
|
+
When conversations get long, the model runs out of context. Most tools hard-truncate — silently dropping earlier messages. **billion-context** gives the model a `compress` tool: the LLM decides **when** and **what** to compress into high-fidelity summaries, preserving critical details (file paths, decisions, error strings) while reclaiming context space.
|
|
28
28
|
|
|
29
|
-
Unlike Pi's built-in auto-compaction (which replaces everything with a single summary),
|
|
29
|
+
Unlike Pi's built-in auto-compaction (which replaces everything with a single summary), billion-context:
|
|
30
30
|
- **Preserves structure** — compressed ranges become labeled blocks you can decompress later
|
|
31
31
|
- **Multi-tier** — summaries can be further distilled (T1 → T2 → T3) as sessions grow
|
|
32
32
|
- **Searchable** — `search_context` finds information inside compressed blocks without decompressing
|
|
@@ -40,19 +40,19 @@ This means:
|
|
|
40
40
|
## Install
|
|
41
41
|
|
|
42
42
|
```bash
|
|
43
|
-
pi install npm:
|
|
43
|
+
pi install npm:billion-context-pi
|
|
44
44
|
```
|
|
45
45
|
|
|
46
46
|
That's it. The extension auto-loads on next Pi startup. No configuration needed — it reads your model's context window automatically.
|
|
47
47
|
|
|
48
|
-
> **Uninstall `pi-subagents` first (optional, recommended).**
|
|
48
|
+
> **Uninstall `pi-subagents` first (optional, recommended).** billion-context-pi ships its own `acp_delegate` sub-agent tool (see below) that replaces pi-subagents at a fraction of the context cost (~600 tok vs ~7K tok/turn). If you have pi-subagents installed, remove it to avoid duplicate delegation tools:
|
|
49
49
|
> ```bash
|
|
50
50
|
> pi remove npm:pi-subagents
|
|
51
51
|
> ```
|
|
52
52
|
|
|
53
53
|
## How it works
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
billion-context intercepts Pi's `context` event (fired before each LLM call) and runs an 8-stage pipeline:
|
|
56
56
|
|
|
57
57
|
```
|
|
58
58
|
assign refs → sync blocks → prune → filter → hide calls → recommend → nudge → emergency truncate
|
|
@@ -60,7 +60,17 @@ assign refs → sync blocks → prune → filter → hide calls → recommend
|
|
|
60
60
|
|
|
61
61
|
Each message gets an invisible `<acp>` ref tag (`m00001`, `m00002`, ...) visible to the model but not the user. The model uses these refs to specify compression ranges.
|
|
62
62
|
|
|
63
|
-
Pi's built-in auto-compaction is cancelled —
|
|
63
|
+
Pi's built-in auto-compaction is cancelled — billion-context is the sole context manager.
|
|
64
|
+
|
|
65
|
+
## Plugin compatibility & ordering
|
|
66
|
+
|
|
67
|
+
billion-context takes over context management by intercepting Pi's `context` event. **Pi has no plugin priority mechanism** — when multiple extensions register handlers for the same event, they run in a fixed sequence (load order), with no `priority`/`weight` field and no way for the user to control the order. The `context` event specifically is a *pipeline*: every handler receives the previous handler's output, there is no short-circuit, and the **last** handler has the final say over what reaches the model.
|
|
68
|
+
|
|
69
|
+
This has two practical implications:
|
|
70
|
+
|
|
71
|
+
1. **Keep exactly one context-compression plugin installed.** If you run two compression plugins together (e.g. billion-context-pi alongside another), both will rewrite the message list and clobber each other's work — compressed ranges can be re-expanded or corrupted. Pi's built-in auto-compaction is already cancelled automatically by billion-context-pi, but any *third-party* compression/compaction extension should be uninstalled.
|
|
72
|
+
|
|
73
|
+
2. **Even with a single compression plugin, interference is still possible in rare cases.** Load order under Pi is determined by filesystem discovery order (`fs.readdirSync` over `.pi/extensions/` → global → packages), which is not fully deterministic. If another (non-compression) extension also hooks the `context` event and happens to load *after* billion-context-pi, it could modify the compressed output. billion-context-pi rebuilds its working set from the session log rather than the chained input, which makes it robust to handlers that run *before* it — but it cannot defend against a handler that runs *after* it. This is a limitation of Pi's extension model; if you observe unexpected context behavior, check whether other installed extensions intercept the `context` event.
|
|
64
74
|
|
|
65
75
|
## Model-facing tools
|
|
66
76
|
|
|
@@ -101,7 +111,7 @@ Rich status display for the user:
|
|
|
101
111
|
╭─────────────────────────────────────────────╮
|
|
102
112
|
│ ACP Context Analysis │
|
|
103
113
|
╰─────────────────────────────────────────────╯
|
|
104
|
-
|
|
114
|
+
billion-context-pi@0.1.14
|
|
105
115
|
|
|
106
116
|
Context: 12% (120K / 1.0M)
|
|
107
117
|
Growth: +15K since last nudge
|
|
@@ -121,7 +131,7 @@ Blocks: 3 active (3.7K summary, 15.2K original compressed)
|
|
|
121
131
|
|
|
122
132
|
## Configuration
|
|
123
133
|
|
|
124
|
-
|
|
134
|
+
billion-context-pi works out of the box with no configuration. Three optional keys can be set in a JSON config file.
|
|
125
135
|
|
|
126
136
|
### Config file
|
|
127
137
|
|
|
@@ -132,7 +142,9 @@ Create `~/.pi/acp.json` (global) and/or `<project>/.pi/acp.json` (project-local,
|
|
|
132
142
|
"debug": false,
|
|
133
143
|
"autoUpdate": true,
|
|
134
144
|
"modelContextLimit": 200000,
|
|
135
|
-
"delegate": true
|
|
145
|
+
"delegate": true,
|
|
146
|
+
"toolBashDefaultTimeout": 60,
|
|
147
|
+
"toolOutputMaxBytes": 200000
|
|
136
148
|
}
|
|
137
149
|
```
|
|
138
150
|
|
|
@@ -142,8 +154,10 @@ Create `~/.pi/acp.json` (global) and/or `<project>/.pi/acp.json` (project-local,
|
|
|
142
154
|
| `autoUpdate` | `true` | On Pi startup, check npm for a newer version and auto-install it (throttled to one check per 3 minutes). Disable to avoid all startup network calls. |
|
|
143
155
|
| `modelContextLimit` | *(auto)* | Override the context limit (in tokens). Defaults to the model's `contextWindow`. |
|
|
144
156
|
| `delegate` | `true` | Enable the `acp_delegate` tools (delegate/wait/cancel) and their system-prompt section. Set `false` to skip registering them (e.g. you use a different sub-agent extension, or run headless where async injection adds no value). |
|
|
157
|
+
| `toolBashDefaultTimeout` | `60` | Seconds injected into the `bash` tool when the model omits `timeout`. Pi has **no** default of its own, so without this a forgotten timeout can hang for thousands of seconds. On timeout the model is guided to re-run with a larger `timeout`. `0` restores Pi's unbounded behavior. |
|
|
158
|
+
| `toolOutputMaxBytes` | `200000` | Hard byte cap on tool result text (~5000 lines at ~40 B/line; applied via the `tool_result` hook). Stops runaway output that Pi's own 50KB/2000-line cap can't catch (e.g. tools Pi doesn't cap). When it fires the model is told how to see the full output — for `bash` the full output is in its temp file (`BashToolDetails.fullOutputPath`); set lower (e.g. `8192`) for a tighter context budget, or `0` to disable. |
|
|
145
159
|
|
|
146
|
-
> **Only these
|
|
160
|
+
> **Only these six keys are read from `acp.json`.** Other tuning knobs (`preserveRecentMessages`, `protectedTools`, nudge thresholds) are code-level and not user-overridable.
|
|
147
161
|
|
|
148
162
|
### Environment variables
|
|
149
163
|
|
|
@@ -159,7 +173,7 @@ The model receives detailed guidance (in its system prompt) on **when** to compr
|
|
|
159
173
|
|
|
160
174
|
### What gets protected
|
|
161
175
|
|
|
162
|
-
|
|
176
|
+
billion-context protects three categories of content from compression:
|
|
163
177
|
|
|
164
178
|
1. **Always-protected tools** — `compress` calls are hard-protected (they're load-bearing metadata; compressing them breaks decompress and the "summary is historical" contract).
|
|
165
179
|
2. **Soft recent-zone** — the last N messages (default 5) and last ~5K tokens are soft-protected so the model keeps its working set. Tool results from `decompress`, `search_context`, `read`, and `bash` are **excluded** from this zone: they're large and meant to be compressible once consumed, so they don't eat the protected budget.
|
package/README.zh-CN.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
[English](./README.md) | [中文](./README.zh-CN.md)
|
|
2
2
|
|
|
3
3
|
<p align="center">
|
|
4
|
-
<strong
|
|
4
|
+
<strong>Billion-Context</strong> — <a href="https://pi.dev">Pi</a> 的上下文压缩插件
|
|
5
5
|
<br />
|
|
6
6
|
由模型决定<em>何时</em>压缩、压缩<em>什么</em> — 而非硬性截断。
|
|
7
7
|
</p>
|
|
@@ -9,22 +9,22 @@
|
|
|
9
9
|
---
|
|
10
10
|
|
|
11
11
|
<p align="center">
|
|
12
|
-
<a href="https://www.npmjs.com/package/
|
|
13
|
-
<a href="https://github.com/ranxianglei/
|
|
14
|
-
<a href="https://github.com/ranxianglei/
|
|
12
|
+
<a href="https://www.npmjs.com/package/billion-context-pi"><img src="https://img.shields.io/npm/v/billion-context-pi.svg?style=flat-square" alt="npm"></a>
|
|
13
|
+
<a href="https://github.com/ranxianglei/billion-context-pi/blob/master/LICENSE"><img src="https://img.shields.io/npm/l/billion-context-pi.svg?style=flat-square" alt="license"></a>
|
|
14
|
+
<a href="https://github.com/ranxianglei/billion-context-pi"><img src="https://img.shields.io/badge/GitHub-ranxianglei%2Fbillion--context--pi-181717?style=flat-square&logo=github" alt="GitHub"></a>
|
|
15
15
|
</p>
|
|
16
16
|
|
|
17
17
|
<p align="center">
|
|
18
|
-
<code>pi install npm:
|
|
18
|
+
<code>pi install npm:billion-context-pi</code>
|
|
19
19
|
</p>
|
|
20
20
|
|
|
21
21
|
---
|
|
22
22
|
|
|
23
|
-
## 为什么选择
|
|
23
|
+
## 为什么选择 billion-context
|
|
24
24
|
|
|
25
|
-
当对话变长,模型的上下文会耗尽。多数工具采用硬截断 —— 静默丢弃早期消息。**
|
|
25
|
+
当对话变长,模型的上下文会耗尽。多数工具采用硬截断 —— 静默丢弃早期消息。**billion-context** 把 `compress` 工具交给模型:由 LLM 决定**何时**压缩、压缩**什么**,将内容压缩成高保真摘要,在回收上下文空间的同时保留关键细节(文件路径、决策、错误字符串)。
|
|
26
26
|
|
|
27
|
-
与 Pi 内置的自动压缩(把所有内容替换成单个摘要)不同,
|
|
27
|
+
与 Pi 内置的自动压缩(把所有内容替换成单个摘要)不同,billion-context:
|
|
28
28
|
|
|
29
29
|
- **保留结构** — 压缩的范围变成带标签的块,可后续解压
|
|
30
30
|
- **多级压缩** — 摘要可被进一步蒸馏(T1 → T2 → T3),随会话增长保持有界
|
|
@@ -39,19 +39,19 @@
|
|
|
39
39
|
## 安装
|
|
40
40
|
|
|
41
41
|
```bash
|
|
42
|
-
pi install npm:
|
|
42
|
+
pi install npm:billion-context-pi
|
|
43
43
|
```
|
|
44
44
|
|
|
45
45
|
完成。扩展在下次 Pi 启动时自动加载。无需配置 —— 它会自动读取模型的上下文窗口。
|
|
46
46
|
|
|
47
|
-
> **建议先卸载 `pi-subagents`(可选,推荐)。**
|
|
47
|
+
> **建议先卸载 `pi-subagents`(可选,推荐)。** billion-context-pi 自带 `acp_delegate` 子代理工具(见下文),以极低的上下文成本(~600 tok vs ~7K tok/轮)替代 pi-subagents。如果你已安装 pi-subagents,卸载它以避免重复的委派工具:
|
|
48
48
|
> ```bash
|
|
49
49
|
> pi remove npm:pi-subagents
|
|
50
50
|
> ```
|
|
51
51
|
|
|
52
52
|
## 工作原理
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
billion-context 拦截 Pi 的 `context` 事件(每次 LLM 调用前触发),运行一个 8 阶段管线:
|
|
55
55
|
|
|
56
56
|
```
|
|
57
57
|
assign refs → sync blocks → prune → filter → hide calls → recommend → nudge → emergency truncate
|
|
@@ -59,7 +59,17 @@ assign refs → sync blocks → prune → filter → hide calls → recommend
|
|
|
59
59
|
|
|
60
60
|
每条消息获得一个不可见的 `<acp>` 引用标签(`m00001`、`m00002`、...),对模型可见但用户不可见。模型用这些引用来指定压缩范围。
|
|
61
61
|
|
|
62
|
-
Pi 内置的自动压缩会被取消 ——
|
|
62
|
+
Pi 内置的自动压缩会被取消 —— billion-context 是唯一的上下文管理者。
|
|
63
|
+
|
|
64
|
+
## 插件兼容性与排序
|
|
65
|
+
|
|
66
|
+
billion-context 通过拦截 Pi 的 `context` 事件接管上下文管理。**Pi 没有插件优先级机制** —— 当多个扩展为同一个事件注册 handler 时,它们按固定顺序(加载顺序)执行,没有 `priority`/`weight` 字段,用户也无法控制顺序。`context` 事件尤其是一个*管线*:每个 handler 都接收上一个 handler 的输出,没有短路,**最后一个** handler 对发给模型的内容拥有最终决定权。
|
|
67
|
+
|
|
68
|
+
这带来两个实际影响:
|
|
69
|
+
|
|
70
|
+
1. **只保留一个上下文压缩插件。** 如果同时运行两个压缩插件(例如 billion-context-pi 和另一个),它们都会改写消息列表、互相覆盖 —— 已压缩的范围可能被重新展开或破坏。Pi 的内置自动压缩已由 billion-context-pi 自动取消,但任何*第三方*压缩/compaction 扩展都应卸载。
|
|
71
|
+
|
|
72
|
+
2. **即使只有一个压缩插件,在少数情况下仍可能出现干扰。** Pi 下的加载顺序由文件系统发现顺序(`fs.readdirSync` 遍历 `.pi/extensions/` → 全局 → 包)决定,并不完全确定。如果另一个(非压缩类)扩展也 hook 了 `context` 事件、且恰好加载在 billion-context-pi *之后*,它可能修改压缩后的输出。billion-context-pi 从会话日志重建工作集(而非链式输入),这让它对*排在它之前*的 handler 鲁棒 —— 但无法防御*排在它之后*的 handler。这是 Pi 扩展模型的固有限制;若你观察到上下文行为异常,请检查是否有其他已安装扩展拦截了 `context` 事件。
|
|
63
73
|
|
|
64
74
|
## 模型工具
|
|
65
75
|
|
|
@@ -100,7 +110,7 @@ Pi 内置的自动压缩会被取消 —— ACP 是唯一的上下文管理者
|
|
|
100
110
|
╭─────────────────────────────────────────────╮
|
|
101
111
|
│ ACP Context Analysis │
|
|
102
112
|
╰─────────────────────────────────────────────╯
|
|
103
|
-
|
|
113
|
+
billion-context-pi@0.1.14
|
|
104
114
|
|
|
105
115
|
Context: 12% (120K / 1.0M)
|
|
106
116
|
Growth: +15K since last nudge
|
|
@@ -120,7 +130,7 @@ Blocks: 3 active (3.7K summary, 15.2K original compressed)
|
|
|
120
130
|
|
|
121
131
|
## 配置
|
|
122
132
|
|
|
123
|
-
|
|
133
|
+
billion-context-pi 开箱即用,无需任何配置。可以在 JSON 配置文件中设置三个可选 key。
|
|
124
134
|
|
|
125
135
|
### 配置文件
|
|
126
136
|
|
|
@@ -131,7 +141,9 @@ pai-acp 开箱即用,无需任何配置。可以在 JSON 配置文件中设置
|
|
|
131
141
|
"debug": false,
|
|
132
142
|
"autoUpdate": true,
|
|
133
143
|
"modelContextLimit": 200000,
|
|
134
|
-
"delegate": true
|
|
144
|
+
"delegate": true,
|
|
145
|
+
"toolBashDefaultTimeout": 60,
|
|
146
|
+
"toolOutputMaxBytes": 200000
|
|
135
147
|
}
|
|
136
148
|
```
|
|
137
149
|
|
|
@@ -141,8 +153,10 @@ pai-acp 开箱即用,无需任何配置。可以在 JSON 配置文件中设置
|
|
|
141
153
|
| `autoUpdate` | `true` | Pi 启动时检查 npm 是否有更新版本并自动安装(限频:每 3 分钟最多一次检查)。禁用以避免所有启动时的网络请求。 |
|
|
142
154
|
| `modelContextLimit` | *(自动)* | 覆盖上下文上限(token 数)。默认为模型的 `contextWindow`。 |
|
|
143
155
|
| `delegate` | `true` | 启用 `acp_delegate` 工具(delegate/wait/cancel)及其系统提示词段落。设为 `false` 则不注册这些工具(例如你用了别的子代理扩展,或跑 headless 场景异步注入没有意义)。 |
|
|
156
|
+
| `toolBashDefaultTimeout` | `60` | 当模型未指定 `timeout` 时注入 `bash` 工具的超时秒数。Pi **本身没有默认超时**,不加这个,一次遗漏的超时可能挂起几千秒。超时后会提示模型用更大的 `timeout` 重跑。设为 `0` 恢复 Pi 的无界行为。 |
|
|
157
|
+
| `toolOutputMaxBytes` | `200000` | 工具结果文本硬上限(字节,约 5000 行 @ ~40 字节/行,通过 `tool_result` hook 应用)。用于兜住 Pi 自身 50KB/2000 行截断管不到的输出(例如 Pi 未加限制的工具)。触发截断时会告诉模型如何查看完整输出——对 `bash`,完整输出在其临时文件(`BashToolDetails.fullOutputPath`)中;设更小(如 `8192`)可更省上下文,设 `0` 关闭。 |
|
|
144
158
|
|
|
145
|
-
>
|
|
159
|
+
> **只有这六个 key 会被 `acp.json` 读取。** 其他调优参数(`preserveRecentMessages`、`protectedTools`、nudge 阈值)是代码级的,不向用户开放。
|
|
146
160
|
|
|
147
161
|
### 环境变量
|
|
148
162
|
|
|
@@ -158,7 +172,7 @@ pai-acp 开箱即用,无需任何配置。可以在 JSON 配置文件中设置
|
|
|
158
172
|
|
|
159
173
|
### 哪些内容会被保护
|
|
160
174
|
|
|
161
|
-
|
|
175
|
+
billion-context 保护三类内容不被压缩:
|
|
162
176
|
|
|
163
177
|
1. **永久保护的工具** — `compress` 调用被硬保护(它们是承载关键元数据的;压缩它们会破坏 decompress 和"摘要是历史"的契约)。
|
|
164
178
|
2. **软近期区** — 最后 N 条消息(默认 5)和最后约 5K token 被软保护,让模型保留工作集。来自 `decompress`、`search_context`、`read`、`bash` 的工具结果被**排除**出此区:它们体量大、消费后就该能压缩,所以不该占用保护预算。
|
package/dist/config.d.ts
CHANGED
|
@@ -20,6 +20,24 @@ export interface AdapterConfig {
|
|
|
20
20
|
* section. Default: true. Set `delegate: false` (adapter config or
|
|
21
21
|
* ~/.pi/acp.json) to skip registering them. */
|
|
22
22
|
delegate?: boolean;
|
|
23
|
+
/** Default timeout in seconds injected into the bash tool when the model
|
|
24
|
+
* omits `timeout`. Pi has NO built-in default, so without this a command
|
|
25
|
+
* that the model forgets to time out can hang for thousands of seconds.
|
|
26
|
+
* Default: 60 (catches hangs quickly). On timeout the model is guided to
|
|
27
|
+
* re-run with a larger `timeout`. Set to 0 to disable (restore Pi's
|
|
28
|
+
* unbounded behavior). */
|
|
29
|
+
toolBashDefaultTimeout?: number;
|
|
30
|
+
/** Hard byte cap applied to tool result text via the `tool_result` hook.
|
|
31
|
+
* Default: 200000 (~200KB, roughly 5000 lines at ~40 bytes/line) — a
|
|
32
|
+
* generous ceiling that stops runaway output. Pi already caps bash/read/grep
|
|
33
|
+
* at 50KB/2000 lines (bash full output is saved to a temp file), so this
|
|
34
|
+
* default mainly caps tools Pi doesn't cap. Set lower (e.g. 8192) for a
|
|
35
|
+
* tighter context budget, or 0 to disable. When capped, oversized text is
|
|
36
|
+
* head-truncated with a notice telling the model how to see the full output
|
|
37
|
+
* (bash: read BashToolDetails.fullOutputPath). */
|
|
38
|
+
toolOutputMaxBytes?: number;
|
|
23
39
|
coreOverrides?: Partial<Config>;
|
|
24
40
|
}
|
|
41
|
+
export declare const DEFAULT_TOOL_BASH_TIMEOUT = 60;
|
|
42
|
+
export declare const DEFAULT_TOOL_OUTPUT_MAX_BYTES = 200000;
|
|
25
43
|
export declare function resolveConfig(adapter: AdapterConfig, liveContextLimit: number): Config;
|
package/dist/index.js
CHANGED
|
@@ -611,9 +611,13 @@ var TAG_CLOSE = LT + "/acp" + GT;
|
|
|
611
611
|
function acpTag(ref, tokens, type) {
|
|
612
612
|
return TAG_OPEN + 'tokens="' + formatTokens(tokens) + '" type="' + type + '"' + GT + ref + TAG_CLOSE;
|
|
613
613
|
}
|
|
614
|
-
function renderMessage(message, map, countTokens) {
|
|
614
|
+
function renderMessage(message, map, countTokens, strategy) {
|
|
615
615
|
const ref = refForRaw(map, message.id);
|
|
616
616
|
if (!ref || ref === BLOCKED_REF) return message;
|
|
617
|
+
if (strategy === "none") return message;
|
|
618
|
+
if (strategy === "text-only" && message.contentType !== "text") {
|
|
619
|
+
return message;
|
|
620
|
+
}
|
|
617
621
|
const ownTagRe = new RegExp(
|
|
618
622
|
"^" + escapeRegex(TAG_OPEN) + "[^>]*" + GT + escapeRegex(ref) + escapeRegex(TAG_CLOSE) + "\\n?"
|
|
619
623
|
);
|
|
@@ -624,21 +628,24 @@ function renderMessage(message, map, countTokens) {
|
|
|
624
628
|
if (!cleanText) return { ...message, text: prefix };
|
|
625
629
|
return { ...message, text: prefix + cleanText };
|
|
626
630
|
}
|
|
627
|
-
function renderVisibleRefs(messages, state, countTokens = (text) => Math.ceil(text.length / 4)) {
|
|
631
|
+
function renderVisibleRefs(messages, state, countTokens = (text) => Math.ceil(text.length / 4), strategy = "all") {
|
|
628
632
|
const map = state.messageRefs;
|
|
629
633
|
return messages.map(
|
|
630
|
-
(message) => renderMessage(message, map, countTokens)
|
|
634
|
+
(message) => renderMessage(message, map, countTokens, strategy)
|
|
631
635
|
);
|
|
632
636
|
}
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
}
|
|
637
|
+
function createRenderRefsNode(strategy) {
|
|
638
|
+
return {
|
|
639
|
+
name: "render-refs",
|
|
640
|
+
run(io, ctx) {
|
|
641
|
+
return {
|
|
642
|
+
...io,
|
|
643
|
+
messages: renderVisibleRefs(io.messages, io.state, ctx.countTokens, strategy)
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
var renderRefsNode = createRenderRefsNode("all");
|
|
642
649
|
var ALWAYS_PROTECTED_TOOLS = ["compress"];
|
|
643
650
|
var NEVER_PRESERVE_RECENT_TOOLS = [
|
|
644
651
|
"decompress",
|
|
@@ -1009,7 +1016,9 @@ function createCore(ports = {}) {
|
|
|
1009
1016
|
state: input.state,
|
|
1010
1017
|
effects: {}
|
|
1011
1018
|
};
|
|
1012
|
-
const
|
|
1019
|
+
const strategy = input.renderTags ?? "all";
|
|
1020
|
+
const nodes = buildNodes(strategy);
|
|
1021
|
+
const result = runPipeline(nodes, initial, ctx);
|
|
1013
1022
|
return {
|
|
1014
1023
|
messages: result.messages,
|
|
1015
1024
|
state: result.state,
|
|
@@ -1039,7 +1048,10 @@ function createCore(ports = {}) {
|
|
|
1039
1048
|
};
|
|
1040
1049
|
}
|
|
1041
1050
|
function defaultNodes() {
|
|
1042
|
-
return
|
|
1051
|
+
return buildNodes("all");
|
|
1052
|
+
}
|
|
1053
|
+
function buildNodes(strategy) {
|
|
1054
|
+
const base = [
|
|
1043
1055
|
assignRefsNode,
|
|
1044
1056
|
syncBlocksNode,
|
|
1045
1057
|
pruneNode,
|
|
@@ -1047,9 +1059,10 @@ function createCore(ports = {}) {
|
|
|
1047
1059
|
hideCompressCallsNode,
|
|
1048
1060
|
recommendNode,
|
|
1049
1061
|
nudgeNode,
|
|
1050
|
-
emergencyTruncateNode
|
|
1051
|
-
renderRefsNode
|
|
1062
|
+
emergencyTruncateNode
|
|
1052
1063
|
];
|
|
1064
|
+
if (strategy === "none") return base;
|
|
1065
|
+
return [...base, createRenderRefsNode(strategy)];
|
|
1053
1066
|
}
|
|
1054
1067
|
return { processTurn, applyCompression, defaultNodes, decompress, search, status };
|
|
1055
1068
|
}
|
|
@@ -2342,6 +2355,8 @@ function makePreview(text, query, len) {
|
|
|
2342
2355
|
}
|
|
2343
2356
|
|
|
2344
2357
|
// src/config.ts
|
|
2358
|
+
var DEFAULT_TOOL_BASH_TIMEOUT = 60;
|
|
2359
|
+
var DEFAULT_TOOL_OUTPUT_MAX_BYTES = 2e5;
|
|
2345
2360
|
function resolveConfig(adapter, liveContextLimit) {
|
|
2346
2361
|
const envLimit = process.env.ACP_MODEL_CONTEXT_LIMIT;
|
|
2347
2362
|
const envLimitNum = envLimit ? Number(envLimit) : NaN;
|
|
@@ -2359,7 +2374,15 @@ var REF_TAG = new RegExp("^(?:<acp\\s[^>]*>m\\d{5}</acp>|\\[m\\d{1,5}\\])\\s?\\n
|
|
|
2359
2374
|
function entriesToCoreMessages(entries) {
|
|
2360
2375
|
const out = [];
|
|
2361
2376
|
for (const entry of entries) {
|
|
2362
|
-
if (entry.type !== "message")
|
|
2377
|
+
if (entry.type !== "message") {
|
|
2378
|
+
if (entry.type === "custom_message") {
|
|
2379
|
+
const text = extractText(entry.content);
|
|
2380
|
+
if (text.length > 0) {
|
|
2381
|
+
out.push({ id: entry.id, role: "user", contentType: "text", text });
|
|
2382
|
+
}
|
|
2383
|
+
}
|
|
2384
|
+
continue;
|
|
2385
|
+
}
|
|
2363
2386
|
const cores = projectMessage(entry.message, entry.id);
|
|
2364
2387
|
out.push(...cores);
|
|
2365
2388
|
}
|
|
@@ -7183,7 +7206,7 @@ async function handleCompress(args, runtime, ctx, toolCallId) {
|
|
|
7183
7206
|
config
|
|
7184
7207
|
});
|
|
7185
7208
|
await runtime.save(applied.state, ctx);
|
|
7186
|
-
const { blocksCreated, tokensCompressed, errors } = applied.result;
|
|
7209
|
+
const { blocksCreated, tokensCompressed, errors, warnings } = applied.result;
|
|
7187
7210
|
const afterTokens = Math.max(0, beforeTokens - tokensCompressed);
|
|
7188
7211
|
const newBlocks = applied.state.blocks.slice(-blocksCreated);
|
|
7189
7212
|
debug.event("compress-out", {
|
|
@@ -7200,6 +7223,7 @@ async function handleCompress(args, runtime, ctx, toolCallId) {
|
|
|
7200
7223
|
newBlocks: newBlocks.map((b) => ({ blockId: b.blockId, tier: b.tier, summaryLen: b.summary.length, directMsgCount: b.directMessageIds.length, effectiveMsgCount: b.effectiveMessageIds.length, summary: b.summary }))
|
|
7201
7224
|
});
|
|
7202
7225
|
const lines = [`\u25A3 ACP | ${formatK2(beforeTokens)} \u2192 ${formatK2(afterTokens)} tokens (~${formatK2(tokensCompressed)} reclaimed, ${blocksCreated} block${blocksCreated > 1 ? "s" : ""})`];
|
|
7226
|
+
if (warnings.length > 0) lines.push("\u26A0\uFE0F " + warnings.join("; "));
|
|
7203
7227
|
if (errors.length > 0) lines.push("Errors: " + errors.join("; "));
|
|
7204
7228
|
return lines.join("\n");
|
|
7205
7229
|
}
|
|
@@ -8185,7 +8209,7 @@ async function statusReport(runtime, ctx) {
|
|
|
8185
8209
|
const activeBlocksList = state.blocks.filter((b) => b.active);
|
|
8186
8210
|
const totalBlocksList = state.blocks;
|
|
8187
8211
|
const lines = [];
|
|
8188
|
-
const versionStr = "0.
|
|
8212
|
+
const versionStr = "0.1.25" ? `billion-context-pi@${"0.1.25"}` : "";
|
|
8189
8213
|
lines.push("\u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E");
|
|
8190
8214
|
lines.push("\u2502 ACP Context Analysis \u2502");
|
|
8191
8215
|
lines.push("\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F");
|
|
@@ -8327,6 +8351,116 @@ When a background delegate finishes, an automated completion notification is inj
|
|
|
8327
8351
|
- Arrive asynchronously: if you have moved on to other work, only act on a notification if it is relevant to the current task; otherwise note it and continue.
|
|
8328
8352
|
`;
|
|
8329
8353
|
|
|
8354
|
+
// src/tool-guardrails.ts
|
|
8355
|
+
import {
|
|
8356
|
+
isBashToolResult,
|
|
8357
|
+
isToolCallEventType
|
|
8358
|
+
} from "@earendil-works/pi-coding-agent";
|
|
8359
|
+
function resolveBashTimeout(input, defaultTimeout) {
|
|
8360
|
+
if (input.timeout !== void 0) return void 0;
|
|
8361
|
+
const d = defaultTimeout ?? DEFAULT_TOOL_BASH_TIMEOUT;
|
|
8362
|
+
if (!Number.isFinite(d) || d <= 0) return void 0;
|
|
8363
|
+
return d;
|
|
8364
|
+
}
|
|
8365
|
+
function capToolOutput(content, maxBytes, fullPath) {
|
|
8366
|
+
const max = maxBytes ?? DEFAULT_TOOL_OUTPUT_MAX_BYTES;
|
|
8367
|
+
if (!Number.isFinite(max) || max <= 0) return void 0;
|
|
8368
|
+
const kept = [];
|
|
8369
|
+
const texts = [];
|
|
8370
|
+
for (const c of content) {
|
|
8371
|
+
if (c.type === "text") texts.push(c.text);
|
|
8372
|
+
else kept.push(c);
|
|
8373
|
+
}
|
|
8374
|
+
if (texts.length === 0) return void 0;
|
|
8375
|
+
const combined = texts.join("\n");
|
|
8376
|
+
const total = Buffer.byteLength(combined, "utf8");
|
|
8377
|
+
if (total <= max) return void 0;
|
|
8378
|
+
const head = keepHead(combined, max);
|
|
8379
|
+
const dropped = total - Buffer.byteLength(head, "utf8");
|
|
8380
|
+
kept.push({ type: "text", text: head + buildCapNotice(dropped, max, fullPath) });
|
|
8381
|
+
return kept;
|
|
8382
|
+
}
|
|
8383
|
+
var TIMEOUT_RE = /Command timed out after (\d+) seconds/;
|
|
8384
|
+
function detectBashTimeout(content) {
|
|
8385
|
+
for (const c of content) {
|
|
8386
|
+
if (c.type !== "text") continue;
|
|
8387
|
+
const m = c.text.match(TIMEOUT_RE);
|
|
8388
|
+
if (m) return Number(m[1]);
|
|
8389
|
+
}
|
|
8390
|
+
return void 0;
|
|
8391
|
+
}
|
|
8392
|
+
function appendTimeoutNotice(content, secs) {
|
|
8393
|
+
const notice = buildTimeoutNotice(secs);
|
|
8394
|
+
const next = [...content];
|
|
8395
|
+
for (let i = next.length - 1; i >= 0; i--) {
|
|
8396
|
+
const part = next[i];
|
|
8397
|
+
if (part && part.type === "text") {
|
|
8398
|
+
next[i] = { type: "text", text: part.text + notice };
|
|
8399
|
+
return next;
|
|
8400
|
+
}
|
|
8401
|
+
}
|
|
8402
|
+
next.push({ type: "text", text: notice });
|
|
8403
|
+
return next;
|
|
8404
|
+
}
|
|
8405
|
+
function keepHead(str, maxBytes) {
|
|
8406
|
+
const buf = Buffer.from(str, "utf8");
|
|
8407
|
+
if (buf.length <= maxBytes) return str;
|
|
8408
|
+
let end = maxBytes;
|
|
8409
|
+
while (end > 0) {
|
|
8410
|
+
const b = buf[end];
|
|
8411
|
+
if (b === void 0 || (b & 192) !== 128) break;
|
|
8412
|
+
end--;
|
|
8413
|
+
}
|
|
8414
|
+
let head = buf.subarray(0, end).toString("utf8");
|
|
8415
|
+
const nl = head.lastIndexOf("\n");
|
|
8416
|
+
if (nl >= Math.floor(maxBytes / 2)) head = head.slice(0, nl);
|
|
8417
|
+
return head;
|
|
8418
|
+
}
|
|
8419
|
+
function buildCapNotice(dropped, maxBytes, fullPath) {
|
|
8420
|
+
const where = fullPath ? `Full output saved to: ${fullPath} \u2014 read it to see everything.` : "To see more, narrow the query or redirect output to a file and read the relevant slice.";
|
|
8421
|
+
return `
|
|
8422
|
+
|
|
8423
|
+
[ACP guardrail: output capped at ${formatBytes(maxBytes)} (~${formatBytes(dropped)} dropped). ${where}]`;
|
|
8424
|
+
}
|
|
8425
|
+
function buildTimeoutNotice(secs) {
|
|
8426
|
+
const suggested = Math.min(Math.max(Math.ceil(secs * 2), 120), 3600);
|
|
8427
|
+
return `
|
|
8428
|
+
|
|
8429
|
+
[ACP guardrail: command killed after ${secs}s. To give it more time, re-run the bash tool with a larger \`timeout\` argument (e.g. \`"timeout": ${suggested}\`).]`;
|
|
8430
|
+
}
|
|
8431
|
+
function formatBytes(n) {
|
|
8432
|
+
return n >= 1024 ? `${(n / 1024).toFixed(1)}KB` : `${n}B`;
|
|
8433
|
+
}
|
|
8434
|
+
function wireToolGuardrails(pi, runtime) {
|
|
8435
|
+
pi.on("tool_call", (event) => {
|
|
8436
|
+
if (!isToolCallEventType("bash", event)) return;
|
|
8437
|
+
const t = resolveBashTimeout(event.input, runtime.adapter.toolBashDefaultTimeout);
|
|
8438
|
+
if (t !== void 0) {
|
|
8439
|
+
event.input.timeout = t;
|
|
8440
|
+
debug.event("guardrail-bash-timeout", { applied: t });
|
|
8441
|
+
}
|
|
8442
|
+
});
|
|
8443
|
+
pi.on("tool_result", (event) => {
|
|
8444
|
+
const isBash = isBashToolResult(event);
|
|
8445
|
+
const fullPath = isBash ? event.details?.fullOutputPath : void 0;
|
|
8446
|
+
const timeoutSecs = isBash && event.isError ? detectBashTimeout(event.content) : void 0;
|
|
8447
|
+
let modified;
|
|
8448
|
+
const max = runtime.adapter.toolOutputMaxBytes;
|
|
8449
|
+
if (max !== void 0 && max > 0) {
|
|
8450
|
+
const next = capToolOutput(event.content, max, fullPath);
|
|
8451
|
+
if (next) {
|
|
8452
|
+
modified = next;
|
|
8453
|
+
debug.event("guardrail-output-cap", { max, hadPath: !!fullPath });
|
|
8454
|
+
}
|
|
8455
|
+
}
|
|
8456
|
+
if (timeoutSecs !== void 0) {
|
|
8457
|
+
modified = appendTimeoutNotice(modified ?? event.content, timeoutSecs);
|
|
8458
|
+
debug.event("guardrail-bash-timeout-notice", { secs: timeoutSecs });
|
|
8459
|
+
}
|
|
8460
|
+
if (modified) return { content: modified };
|
|
8461
|
+
});
|
|
8462
|
+
}
|
|
8463
|
+
|
|
8330
8464
|
// src/update.ts
|
|
8331
8465
|
import { readFile, writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
|
|
8332
8466
|
import { join as join5, dirname as dirname3 } from "path";
|
|
@@ -8433,7 +8567,7 @@ async function checkForUpdate(autoUpdate, notify) {
|
|
|
8433
8567
|
const data = await res.json();
|
|
8434
8568
|
const latest = data.version;
|
|
8435
8569
|
if (!latest) return;
|
|
8436
|
-
const current = runtimeVersion ?? "0.
|
|
8570
|
+
const current = runtimeVersion ?? "0.1.25";
|
|
8437
8571
|
debug.event("update-check", {
|
|
8438
8572
|
current,
|
|
8439
8573
|
latest,
|
|
@@ -8613,7 +8747,7 @@ async function loadUserConfig(cwd) {
|
|
|
8613
8747
|
function join8(...parts) {
|
|
8614
8748
|
return path3.join(...parts);
|
|
8615
8749
|
}
|
|
8616
|
-
var KNOWN = /* @__PURE__ */ new Set(["debug", "autoUpdate", "modelContextLimit", "delegate"]);
|
|
8750
|
+
var KNOWN = /* @__PURE__ */ new Set(["debug", "autoUpdate", "modelContextLimit", "delegate", "toolBashDefaultTimeout", "toolOutputMaxBytes"]);
|
|
8617
8751
|
function pickKnown(parsed) {
|
|
8618
8752
|
const out = {};
|
|
8619
8753
|
for (const [k, v] of Object.entries(parsed)) {
|
|
@@ -8641,6 +8775,7 @@ function createAcpExtension(adapter = {}) {
|
|
|
8641
8775
|
wireSessionLifecycle(pi, runtime);
|
|
8642
8776
|
wireContextTransform(pi, runtime);
|
|
8643
8777
|
wireSystemPrompt(pi, runtime);
|
|
8778
|
+
wireToolGuardrails(pi, runtime);
|
|
8644
8779
|
pi.registerTool(makeCompressTool(runtime));
|
|
8645
8780
|
pi.registerTool(makeDecompressTool(runtime));
|
|
8646
8781
|
pi.registerTool(makeSearchTool(runtime));
|
|
@@ -8767,7 +8902,12 @@ ${prompt}` };
|
|
|
8767
8902
|
function collectOriginals(entries) {
|
|
8768
8903
|
const map = /* @__PURE__ */ new Map();
|
|
8769
8904
|
for (const entry of entries) {
|
|
8770
|
-
if (entry.type === "message")
|
|
8905
|
+
if (entry.type === "message") {
|
|
8906
|
+
map.set(entry.id, entry.message);
|
|
8907
|
+
} else if (entry.type === "custom_message") {
|
|
8908
|
+
const content = typeof entry.content === "string" ? [{ type: "text", text: entry.content }] : entry.content;
|
|
8909
|
+
map.set(entry.id, { role: "user", content });
|
|
8910
|
+
}
|
|
8771
8911
|
}
|
|
8772
8912
|
return map;
|
|
8773
8913
|
}
|