@hicaru/pi-rlm 0.1.0 → 0.1.2
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 +58 -131
- package/README.ru.md +6 -8
- package/README.zh-CN.md +6 -12
- package/package.json +10 -12
- package/src/bridge/pi-interactive.ts +3 -48
- package/src/commands/rlm-config.ts +10 -3
- package/src/commands/rlm.ts +0 -15
- package/src/config/settings.ts +1 -24
- package/src/core/answer.ts +1 -17
- package/src/core/engine.ts +9 -14
- package/src/core/types.ts +1 -13
- package/src/index.ts +52 -35
- package/src/prompts/system.ts +19 -37
- package/src/sandbox/protocol.ts +0 -6
- package/src/sandbox/sandbox-manager.ts +7 -5
- package/src/sandbox/sandbox.ts +4 -1
- package/src/sandbox/worker.py +12 -11
- package/src/tool/repl-details.ts +3 -0
- package/src/tool/repl-tool.ts +34 -5
- package/src/tool/rlm-events.ts +2 -41
- package/src/tool/rlm-tool.ts +0 -13
- package/src/ui/model-picker.ts +12 -4
- package/src/patch/apply.ts +0 -148
- package/src/patch/index.ts +0 -37
- package/src/state/events.ts +0 -22
- package/src/telemetry/dispatcher.ts +0 -116
- package/src/telemetry/index.ts +0 -14
- package/src/telemetry/mlflow-config.ts +0 -15
- package/src/telemetry/mlflow-sink.ts +0 -136
- package/src/telemetry/mlflow.ts +0 -99
- package/src/telemetry/sink.ts +0 -8
- package/src/tool/apply-diff-tool.ts +0 -125
package/README.md
CHANGED
|
@@ -1,25 +1,40 @@
|
|
|
1
|
+
# pi-rlm — Save 99% tokens, Recursive Language Model (RLM) for the Pi
|
|
2
|
+
|
|
1
3
|
<div align="center">
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
**Recursive Language Models (RLMs)**, implemented natively as a Pi extension —
|
|
6
|
+
FULLY LOCAL.
|
|
4
7
|
|
|
5
8
|
</div>
|
|
6
9
|
|
|
7
|
-
|
|
10
|
+
## Install
|
|
8
11
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
+
```bash
|
|
13
|
+
pi install npm:@hicaru/pi-rlm
|
|
14
|
+
```
|
|
12
15
|
|
|
13
|
-
|
|
16
|
+
To remove it later:
|
|
14
17
|
|
|
15
|
-
|
|
18
|
+
```bash
|
|
19
|
+
pi uninstall npm:@hicaru/pi-rlm
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Then run `/reload` or restart Pi. Verify with `pi list` that the package appears in
|
|
23
|
+
`settings.packages`, and check that `/rlm`, `/rlm-config`, and `/rlm-stop` appear under **[Extensions]**.
|
|
24
|
+
|
|
25
|
+
<div align="center">
|
|
26
|
+
|
|
27
|
+
<a href="https://arxiv.org/abs/2512.24601"><img src="../../assets/hero.png" alt="pi-rlm"></a>
|
|
16
28
|
|
|
17
|
-
|
|
29
|
+
<sub>Modeled on the method in the RLM paper, reimplemented natively for Pi.</sub>
|
|
30
|
+
|
|
31
|
+
</div>
|
|
18
32
|
|
|
19
33
|
<div align="center">
|
|
20
34
|
|
|
21
|
-
|
|
22
|
-
|
|
35
|
+
<sub>
|
|
36
|
+
**English** · <a href="README.zh-CN.md">中文</a> · <a href="README.ru.md">Русский</a>
|
|
37
|
+
</sub>
|
|
23
38
|
|
|
24
39
|
</div>
|
|
25
40
|
|
|
@@ -46,59 +61,31 @@ sub-LLM calls, hence the name.
|
|
|
46
61
|
- Hard sub-problems **recurse** into child RLMs via `rlm_query` (depth-capped).
|
|
47
62
|
- Everything runs **in-process** — the only external process is one local `python3` worker.
|
|
48
63
|
|
|
49
|
-
> This is a Pi-plugin reimplementation of the RLM method (see the [RLM paper](https://arxiv.org/abs/2512.24601)
|
|
50
|
-
>
|
|
64
|
+
> This is a Pi-plugin reimplementation of the RLM method (see the [RLM paper](https://arxiv.org/abs/2512.24601)).
|
|
65
|
+
> It is **not** the Python library.
|
|
51
66
|
|
|
52
67
|
## How it works
|
|
53
68
|
|
|
54
69
|
```
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
- The sandbox exposes `context`, `llm_query`, `llm_query_batched`, `rlm_query`,
|
|
69
|
-
`rlm_query_batched`, `SHOW_VARS()`, `todo()`, `ask_user_question()`, and an `answer` dict.
|
|
70
|
-
The model submits its final result by setting `answer["ready"] = True`.
|
|
71
|
-
|
|
72
|
-
## Install
|
|
73
|
-
|
|
74
|
-
`pi-rlm` is a Pi package. Pi provides the `@earendil-works/pi-*` and `typebox` peer
|
|
75
|
-
dependencies; do **not** install a separate copy of them into this package. Requires
|
|
76
|
-
`python3` on `PATH` (standard library only).
|
|
77
|
-
|
|
78
|
-
Recommended local install while developing:
|
|
79
|
-
|
|
80
|
-
```bash
|
|
81
|
-
pi install /path/to/this-repo/pi-plugin/rlm
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
Published npm package install:
|
|
85
|
-
|
|
86
|
-
```bash
|
|
87
|
-
npm publish # e.g. as @<you>/pi-rlm
|
|
88
|
-
pi install npm:@<you>/pi-rlm
|
|
70
|
+
┌─────────────────────────┐
|
|
71
|
+
│ Pi coding agent │
|
|
72
|
+
└────────────┬────────────┘
|
|
73
|
+
│ /rlm
|
|
74
|
+
▼
|
|
75
|
+
┌─────────────────────────┐ spawns ┌────────────────────┐
|
|
76
|
+
│ Smart model (root) │ ────────► │ Worker models │
|
|
77
|
+
│ drives a Python REPL │ ◄──────── │ (cheap, fast) │
|
|
78
|
+
└────────────┬────────────┘ results └────────────────────┘
|
|
79
|
+
│ recursion (depth-capped)
|
|
80
|
+
└────► child RLMs ────► (same loop)
|
|
81
|
+
|
|
82
|
+
All local · one python3 process · no servers
|
|
89
83
|
```
|
|
90
84
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
```bash
|
|
97
|
-
rm -rf ~/.pi/agent/extensions/rlm
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
Then run `/reload` or restart Pi. Verify with `pi list` that the package appears in
|
|
101
|
-
`settings.packages`, and check that `/rlm`, `/rlm-config`, and `/rlm-stop` appear under **[Extensions]**.
|
|
85
|
+
- The **smart model** thinks and writes Python in a REPL.
|
|
86
|
+
- The **worker models** do the heavy lifting (read, summarize, classify).
|
|
87
|
+
- Hard sub-problems **recurse** into child RLMs.
|
|
88
|
+
- Everything runs **fully local** — your API keys never leave Pi.
|
|
102
89
|
|
|
103
90
|
## Commands
|
|
104
91
|
|
|
@@ -129,6 +116,8 @@ These functions are injected into the model's Python namespace inside the REPL:
|
|
|
129
116
|
| `rlm_query_batched` | `(prompts, model=None) -> list[str]` | Concurrent recursive child RLMs |
|
|
130
117
|
| `todo` | `(action, **kwargs) -> str` | Task list: `create`/`update`/`list`/`get`/`delete`/`clear` |
|
|
131
118
|
| `ask_user_question` | `(questions) -> list[dict]` | Ask the user structured questions (depth 0 only) |
|
|
119
|
+
| `stage_edit` | `(path, old_text, new_text) -> str` | Stage a file edit; relayed to the host's native edit flow |
|
|
120
|
+
| `advance_phase` | `(phase, summary=None) -> str` | Move the root pipeline to a new phase |
|
|
132
121
|
| `SHOW_VARS` | `() -> str` | List currently defined variables & their types |
|
|
133
122
|
| `answer` | `dict` | Set `answer["content"]=...; answer["ready"]=True` to finalize |
|
|
134
123
|
|
|
@@ -138,15 +127,18 @@ These functions are injected into the model's Python namespace inside the REPL:
|
|
|
138
127
|
|---|---|---|
|
|
139
128
|
| Smart model | Pi's active model | the root orchestrator |
|
|
140
129
|
| Worker model | cheapest available | answers `llm_query` |
|
|
141
|
-
| Max recursion depth | `4` | `rlm_query` past this
|
|
142
|
-
| Max iterations | `30` | turns before
|
|
143
|
-
|
|
|
144
|
-
| Max
|
|
145
|
-
|
|
|
146
|
-
|
|
|
147
|
-
|
|
|
148
|
-
|
|
|
149
|
-
|
|
|
130
|
+
| Max recursion depth | `4` | `rlm_query` past this degrades to plain `llm_query` |
|
|
131
|
+
| Max iterations | `30` | root REPL turns before RLM asks for a final answer |
|
|
132
|
+
| REPL block timeout (s) | `120` | wall-clock limit for one Python REPL block (SIGALRM) |
|
|
133
|
+
| Max concurrent sub-calls | `4` | concurrency pool size for `*_batched` |
|
|
134
|
+
| Budget ceiling (USD) | none | total spend cap for the whole recursive tree |
|
|
135
|
+
| Wall-clock ceiling (min) | none | total runtime cap for the whole recursive tree |
|
|
136
|
+
| Token ceiling | none | total input+output token cap for the whole recursive tree |
|
|
137
|
+
| Max consecutive errors | `5` | stop after N consecutive failing turns (none = off) |
|
|
138
|
+
| Orchestrator addendum | on | divide-and-conquer guidance in the root system prompt |
|
|
139
|
+
| Trajectory compaction | on (0.65) | summarize old turns when history nears the context window |
|
|
140
|
+
| Root model output cap (tok) | `16384` | max output tokens per root-model turn |
|
|
141
|
+
| Sandbox init timeout | `30000` ms | how long to wait for the Python worker to start |
|
|
150
142
|
| `askUserQuestion` | on | expose `ask_user_question()` to the model |
|
|
151
143
|
| `todo` | on | expose `todo()` to the model |
|
|
152
144
|
|
|
@@ -155,17 +147,6 @@ These functions are injected into the model's Python namespace inside the REPL:
|
|
|
155
147
|
> defaults (depth 4, conc 4) that's 4³ = 64 in the pathological case. Budget and error
|
|
156
148
|
> caps (above) bound total spend regardless of fan-out.
|
|
157
149
|
|
|
158
|
-
## Telemetry & run logs
|
|
159
|
-
|
|
160
|
-
- **Run logs** (`runLog`): always-on by default. Each run writes a JSONL trail to `.rlm/runs/`
|
|
161
|
-
(default), capped at `maxRuns` (50). Supports **snapshots** (`sandbox.pkl`) and **resume**
|
|
162
|
-
of interrupted runs via `/rlm-resume`. Snapshots are protected by a per-session `nonce`
|
|
163
|
-
to prevent cross-session replay.
|
|
164
|
-
- **MLflow tracing** (`telemetry`): optional. Set `MLFLOW_TRACKING_URI` or configure
|
|
165
|
-
`trackingUri` / `experimentId` in `/rlm-config`. The root run is tagged as an MLflow span
|
|
166
|
-
for trace correlation on resume. The Bearer token comes from the `MLFLOW_TRACKING_TOKEN`
|
|
167
|
-
env var and is **never persisted** to `rlm.json`.
|
|
168
|
-
|
|
169
150
|
## Security
|
|
170
151
|
|
|
171
152
|
- **Key isolation**: provider keys live only in TypeScript (`AuthStorage`); the sandbox
|
|
@@ -181,57 +162,3 @@ These functions are injected into the model's Python namespace inside the REPL:
|
|
|
181
162
|
SIGALRM timeout + parent watchdog (SIGKILL on hang); budget / token / timeout /
|
|
182
163
|
consecutive-error caps.
|
|
183
164
|
- **Trust**: project-local install requires Pi project trust.
|
|
184
|
-
|
|
185
|
-
## Project layout
|
|
186
|
-
|
|
187
|
-
```
|
|
188
|
-
src/
|
|
189
|
-
sandbox/ worker.py + JSONL stdio driver (PythonSandbox) · protocol.ts · sandbox-manager.ts
|
|
190
|
-
bridge/ model.ts (one-shot completion) · llm-query.ts · rlm-query.ts (recursion)
|
|
191
|
-
core/ engine.ts (the loop) · iteration · limits · answer · compaction · pipeline · types
|
|
192
|
-
prompts/ system + per-turn prompts (ported from the Python reference)
|
|
193
|
-
text/ parsing (repl blocks) · tokens · preview · edits
|
|
194
|
-
state/ agent-tree · events · reads/writes · resume · paths · rows
|
|
195
|
-
tool/ repl-tool · rlm-events · aggregator · propose-edits · emitter-listener
|
|
196
|
-
config/ defaults · settings (rlm.json persistence + validation)
|
|
197
|
-
context/ repomix-based repository packing + caching
|
|
198
|
-
telemetry/ MLflow sink · dispatcher · mlflow-config
|
|
199
|
-
ui/ tree-widget · status · model-picker · config-panel · intro · theme
|
|
200
|
-
commands/ rlm · rlm-config
|
|
201
|
-
mode/ rlm-mode (controller) · input-router
|
|
202
|
-
patch/ apply · popup · index
|
|
203
|
-
util/ errors · concurrency
|
|
204
|
-
test/ phase1–phase9 · native-smoke · native-mode · helpers
|
|
205
|
-
```
|
|
206
|
-
|
|
207
|
-
## Tests
|
|
208
|
-
|
|
209
|
-
Runtime is **Bun** (`bun install`, `bun run …` — never npm/pnpm/yarn).
|
|
210
|
-
|
|
211
|
-
```bash
|
|
212
|
-
bun run test/phase1.ts # sandbox: exec, persistence, key isolation, timeout kill
|
|
213
|
-
bun run test/phase4.ts # recursion depth-cap logic (no tokens)
|
|
214
|
-
bun run test/phase5.ts # live agent tree rendering (no tokens)
|
|
215
|
-
RLM_TEST_LIVE=1 bun run test/phase2.ts # real llm_query through the sandbox
|
|
216
|
-
RLM_TEST_LIVE=1 bun run test/phase3.ts # real end-to-end /rlm over a file context
|
|
217
|
-
RLM_TEST_LIVE=1 bun run test/phase4.ts # engine solves a 20-doc needle-in-haystack
|
|
218
|
-
```
|
|
219
|
-
|
|
220
|
-
## Background
|
|
221
|
-
|
|
222
|
-
Modeled on the Python reference [`rlm`](https://github.com/alexzhang13/rlm-minimal) and the
|
|
223
|
-
method in the [RLM paper](https://arxiv.org/abs/2512.24601), reimplemented natively for Pi.
|
|
224
|
-
|
|
225
|
-
If you use this in your research, please cite the original RLM work:
|
|
226
|
-
|
|
227
|
-
```bibtex
|
|
228
|
-
@misc{zhang2026recursivelanguagemodels,
|
|
229
|
-
title={Recursive Language Models},
|
|
230
|
-
author={Alex L. Zhang and Tim Kraska and Omar Khattab},
|
|
231
|
-
year={2026},
|
|
232
|
-
eprint={2512.24601},
|
|
233
|
-
archivePrefix={arXiv},
|
|
234
|
-
primaryClass={cs.AI},
|
|
235
|
-
url={https://arxiv.org/abs/2512.24601},
|
|
236
|
-
}
|
|
237
|
-
```
|
package/README.ru.md
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
<div align="center">
|
|
20
20
|
|
|
21
21
|
**Рекурсивные языковые модели (RLMs)**, реализованные нативно как расширение Pi —
|
|
22
|
-
|
|
22
|
+
ПОЛНОСТЬЮ ЛОКАЛЬНО.
|
|
23
23
|
|
|
24
24
|
</div>
|
|
25
25
|
|
|
@@ -36,8 +36,8 @@
|
|
|
36
36
|
- Сложные подзадачи **рекурсивно** передаются в дочерние RLM через `rlm_query` (с ограничением глубины).
|
|
37
37
|
- Все работает **in-process** — единственным внешним процессом является локальный worker `python3`.
|
|
38
38
|
|
|
39
|
-
> This is a Pi-plugin reimplementation of the RLM method (see the [RLM paper](https://arxiv.org/abs/2512.24601)
|
|
40
|
-
>
|
|
39
|
+
> This is a Pi-plugin reimplementation of the RLM method (see the [RLM paper](https://arxiv.org/abs/2512.24601)).
|
|
40
|
+
> It is **not** the Python library.
|
|
41
41
|
|
|
42
42
|
## Как это работает
|
|
43
43
|
|
|
@@ -133,10 +133,9 @@ rm -rf ~/.pi/agent/extensions/rlm
|
|
|
133
133
|
|
|
134
134
|
> **Примечание по параллелизму:** каждый дочерний `rlm_query` запускает собственного worker `python3` (~50–150 мс «холодного старта»). В худшем случае количество параллельных интерпретаторов ≈ `maxConcurrentSubcalls`^(depth−1); при настройках по умолчанию (глубина 4, параллелизм 4) это 4³ = 64 в патологическом случае. Лимиты бюджета и ошибок (см. выше) ограничивают общие затраты независимо от степени разветвления.
|
|
135
135
|
|
|
136
|
-
##
|
|
136
|
+
## Логи запусков
|
|
137
137
|
|
|
138
138
|
- **Логи запусков** (`runLog`): включены по умолчанию. Каждый запуск записывает след в формате JSONL в `.rlm/runs/` (по умолчанию) с ограничением `maxRuns` (50). Поддерживает **снимки** (`sandbox.pkl`) и **возобновление** прерванных запусков через `/rlm-resume`. Снимки защищены сессионным `nonce` для предотвращения повторов между сессиями.
|
|
139
|
-
- **Трассировка MLflow** (`telemetry`): опционально. Установите `MLFLOW_TRACKING_URI` или настройте `trackingUri` / `experimentId` в `/rlm-config`. Корневой запуск помечается как span MLflow для корреляции трасс при возобновлении. Bearer-токен берется из переменной окружения `MLFLOW_TRACKING_TOKEN` и **никогда не сохраняется** в `rlm.json`.
|
|
140
139
|
|
|
141
140
|
## Безопасность
|
|
142
141
|
|
|
@@ -155,11 +154,10 @@ src/
|
|
|
155
154
|
core/ engine.ts (цикл) · iteration · limits · answer · compaction · pipeline · types
|
|
156
155
|
prompts/ системные промпты и промпты для каждого шага (перенесены из Python-референса)
|
|
157
156
|
text/ парсинг (repl-блоки) · токены · превью · правки
|
|
158
|
-
state/
|
|
157
|
+
state/ чтения/записи · возобновление · пути · строки
|
|
159
158
|
tool/ repl-tool · rlm-events · агрегатор · предложение-правок · emitter-listener
|
|
160
159
|
config/ значения по умолчанию · настройки (сохранение и валидация rlm.json)
|
|
161
160
|
context/ упаковка репозитория на базе repomix + кеширование
|
|
162
|
-
telemetry/ MLflow sink · диспетчер · mlflow-config
|
|
163
161
|
ui/ виджет-дерева · статус · выбор-модели · панель-конфигурации · вступление · тема
|
|
164
162
|
commands/ rlm · rlm-config
|
|
165
163
|
mode/ rlm-mode (контроллер) · маршрутизатор-ввода
|
|
@@ -183,7 +181,7 @@ RLM_TEST_LIVE=1 bun run test/phase4.ts # engine solves a 20-doc needle-in-hays
|
|
|
183
181
|
|
|
184
182
|
## Общая информация
|
|
185
183
|
|
|
186
|
-
Реализовано на основе
|
|
184
|
+
Реализовано на основе метода из [статьи RLM](https://arxiv.org/abs/2512.24601), с нативной переработкой для Pi.
|
|
187
185
|
|
|
188
186
|
Если вы используете этот проект в своих исследованиях, пожалуйста, сошлитесь на оригинальную работу RLM:
|
|
189
187
|
|
package/README.zh-CN.md
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
<div align="center">
|
|
20
20
|
|
|
21
21
|
**递归语言模型 (RLMs)** 作为 Pi 扩展原生实现 ——
|
|
22
|
-
|
|
22
|
+
完全本地。
|
|
23
23
|
|
|
24
24
|
</div>
|
|
25
25
|
|
|
@@ -36,8 +36,8 @@
|
|
|
36
36
|
- 困难的子问题通过 `rlm_query` **递归**到子 RLM 中(设有深度限制)。
|
|
37
37
|
- 所有内容均**在进程内**运行 —— 唯一的外部进程是一个本地的 `python3` worker。
|
|
38
38
|
|
|
39
|
-
> 这是 RLM 方法的 Pi 插件重新实现(参见 [RLM 论文](https://arxiv.org/abs/2512.24601)
|
|
40
|
-
>
|
|
39
|
+
> 这是 RLM 方法的 Pi 插件重新实现(参见 [RLM 论文](https://arxiv.org/abs/2512.24601))。
|
|
40
|
+
> 它**不是**那个 Python 库。
|
|
41
41
|
|
|
42
42
|
## 工作原理
|
|
43
43
|
|
|
@@ -143,15 +143,11 @@ rm -rf ~/.pi/agent/extensions/rlm
|
|
|
143
143
|
> 默认设置下 (深度 4, 并发 4),极端情况下为 4³ = 64。预算和错误
|
|
144
144
|
> 上限 (见上文) 无论扇出 (fan-out) 如何都会限制总支出。
|
|
145
145
|
|
|
146
|
-
##
|
|
146
|
+
## 运行日志
|
|
147
147
|
|
|
148
148
|
- **运行日志** (`runLog`):默认始终开启。每次运行将 JSONL 轨迹写入 `.rlm/runs/`
|
|
149
149
|
(默认),上限为 `maxRuns` (50)。支持通过 `/rlm-resume` 进行**快照** (`sandbox.pkl`) 和**恢复**
|
|
150
150
|
被中断的任务。快照受每个会话的 `nonce` 保护,以防止跨会话重放。
|
|
151
|
-
- **MLflow 追踪** (`telemetry`):可选。设置 `MLFLOW_TRACKING_URI` 或在
|
|
152
|
-
`/rlm-config` 中配置 `trackingUri` / `experimentId`。根运行被标记为 MLflow span
|
|
153
|
-
以便在恢复时进行追踪关联。Bearer 令牌来自 `MLFLOW_TRACKING_TOKEN`
|
|
154
|
-
环境变量,且**绝不会**持久化到 `rlm.json`。
|
|
155
151
|
|
|
156
152
|
## 安全性
|
|
157
153
|
|
|
@@ -178,11 +174,10 @@ src/
|
|
|
178
174
|
core/ engine.ts (the loop) · iteration · limits · answer · compaction · pipeline · types
|
|
179
175
|
prompts/ system + per-turn prompts (ported from the Python reference)
|
|
180
176
|
text/ parsing (repl blocks) · tokens · preview · edits
|
|
181
|
-
state/
|
|
177
|
+
state/ reads/writes · resume · paths · rows
|
|
182
178
|
tool/ repl-tool · rlm-events · aggregator · propose-edits · emitter-listener
|
|
183
179
|
config/ defaults · settings (rlm.json persistence + validation)
|
|
184
180
|
context/ repomix-based repository packing + caching
|
|
185
|
-
telemetry/ MLflow sink · dispatcher · mlflow-config
|
|
186
181
|
ui/ tree-widget · status · model-picker · config-panel · intro · theme
|
|
187
182
|
commands/ rlm · rlm-config
|
|
188
183
|
mode/ rlm-mode (controller) · input-router
|
|
@@ -206,8 +201,7 @@ RLM_TEST_LIVE=1 bun run test/phase4.ts # 引擎解决 20 个文档的“大海
|
|
|
206
201
|
|
|
207
202
|
## 背景
|
|
208
203
|
|
|
209
|
-
基于
|
|
210
|
-
[RLM 论文](https://arxiv.org/abs/2512.24601) 中的方法,为 Pi 原生重新实现。
|
|
204
|
+
基于 [RLM 论文](https://arxiv.org/abs/2512.24601) 中的方法,为 Pi 原生重新实现。
|
|
211
205
|
|
|
212
206
|
如果您在研究中使用此项目,请引用原始 RLM 工作:
|
|
213
207
|
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hicaru/pi-rlm",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "Recursive Language Model (RLM) for the Pi
|
|
5
|
+
"description": "Save 99% tokens, Recursive Language Model (RLM) for the Pi",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "hicaru",
|
|
8
8
|
"repository": {
|
|
@@ -13,17 +13,17 @@
|
|
|
13
13
|
"bugs": {
|
|
14
14
|
"url": "https://github.com/hicaru/rlm.pi/issues"
|
|
15
15
|
},
|
|
16
|
-
"exports": {
|
|
17
|
-
".": "./src/index.ts"
|
|
18
|
-
},
|
|
19
16
|
"files": [
|
|
20
|
-
"src",
|
|
17
|
+
"src/",
|
|
21
18
|
"README.md",
|
|
22
19
|
"LICENSE"
|
|
23
20
|
],
|
|
24
21
|
"keywords": [
|
|
25
22
|
"pi-package",
|
|
26
|
-
"pi-extension"
|
|
23
|
+
"pi-extension",
|
|
24
|
+
"rlm",
|
|
25
|
+
"recursive",
|
|
26
|
+
"ai-agent"
|
|
27
27
|
],
|
|
28
28
|
"scripts": {
|
|
29
29
|
"check": "tsc --noEmit",
|
|
@@ -38,14 +38,12 @@
|
|
|
38
38
|
"access": "public"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
|
-
"@earendil-works/pi-ai": "
|
|
42
|
-
"@earendil-works/pi-coding-agent": "
|
|
43
|
-
"@earendil-works/pi-tui": "
|
|
41
|
+
"@earendil-works/pi-ai": "*",
|
|
42
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
43
|
+
"@earendil-works/pi-tui": "*",
|
|
44
44
|
"typebox": "*"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@mlflow/core": "~0.2.0",
|
|
48
|
-
"diff": "^9.0.0",
|
|
49
47
|
"repomix": "^1.15.0"
|
|
50
48
|
},
|
|
51
49
|
"devDependencies": {
|
|
@@ -4,29 +4,6 @@ import type { AskAnswer, AskQuestion } from "../sandbox/protocol.ts";
|
|
|
4
4
|
import { formatError } from "../util/errors.ts";
|
|
5
5
|
import { createTodoFallback } from "./fallback-todo.ts";
|
|
6
6
|
|
|
7
|
-
interface ToolInvoker {
|
|
8
|
-
readonly callTool?: (name: string, params: unknown) => Promise<unknown>;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function hasAnswers(value: unknown): value is { readonly answers: readonly unknown[] } {
|
|
12
|
-
return typeof value === "object" && value !== null && Array.isArray((value as { readonly answers?: unknown }).answers);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function isAskAnswer(value: unknown): value is AskAnswer {
|
|
16
|
-
if (typeof value !== "object" || value === null) return false;
|
|
17
|
-
const candidate = value as { readonly question?: unknown; readonly selected?: unknown; readonly custom?: unknown };
|
|
18
|
-
return typeof candidate.question === "string"
|
|
19
|
-
&& Array.isArray(candidate.selected)
|
|
20
|
-
&& candidate.selected.every((item) => typeof item === "string")
|
|
21
|
-
&& (candidate.custom === undefined || typeof candidate.custom === "string");
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function normalizeAnswers(result: unknown): AskAnswer[] | undefined {
|
|
25
|
-
if (!hasAnswers(result)) return undefined;
|
|
26
|
-
const answers = result.answers;
|
|
27
|
-
return answers.every(isAskAnswer) ? Array.from(answers) : undefined;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
7
|
async function askViaUi(ctx: ExtensionContext, questions: readonly AskQuestion[]): Promise<AskAnswer[]> {
|
|
31
8
|
if (!ctx.hasUI) throw new Error("ask_user_question requires UI");
|
|
32
9
|
const answers = new Array<AskAnswer>(questions.length);
|
|
@@ -57,30 +34,8 @@ async function askViaUi(ctx: ExtensionContext, questions: readonly AskQuestion[]
|
|
|
57
34
|
export function createPiInteractiveDeps(ctx: ExtensionContext): InteractiveDeps {
|
|
58
35
|
const fallbackTodo = createTodoFallback();
|
|
59
36
|
return Object.freeze({
|
|
60
|
-
onAskUserQuestion:
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
try {
|
|
64
|
-
const result = await callTool.call(ctx, "ask_user_question", { questions });
|
|
65
|
-
const answers = normalizeAnswers(result);
|
|
66
|
-
if (answers) return answers;
|
|
67
|
-
} catch {
|
|
68
|
-
// Fall through to native UI fallback when the extension tool is not registered or fails.
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
return askViaUi(ctx, questions);
|
|
72
|
-
},
|
|
73
|
-
onTodo: async (action: string, params: Record<string, unknown>): Promise<string> => {
|
|
74
|
-
const callTool = (ctx as unknown as ToolInvoker).callTool;
|
|
75
|
-
if (typeof callTool === "function") {
|
|
76
|
-
try {
|
|
77
|
-
const result = await callTool.call(ctx, "todo", { action, ...params });
|
|
78
|
-
return typeof result === "string" ? result : JSON.stringify(result);
|
|
79
|
-
} catch {
|
|
80
|
-
// Fall through to in-process task store when the extension tool is not registered or fails.
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
return fallbackTodo(action, params);
|
|
84
|
-
},
|
|
37
|
+
onAskUserQuestion: (questions: readonly AskQuestion[]): Promise<AskAnswer[]> => askViaUi(ctx, questions),
|
|
38
|
+
onTodo: (action: string, params: Record<string, unknown>): Promise<string> =>
|
|
39
|
+
Promise.resolve(fallbackTodo(action, params)),
|
|
85
40
|
});
|
|
86
41
|
}
|
|
@@ -11,15 +11,22 @@ export async function runRlmConfig(controller: RlmController, ctx: ExtensionCont
|
|
|
11
11
|
const models = ctx.modelRegistry.getAvailable();
|
|
12
12
|
|
|
13
13
|
const worker = await selectModel(ctx, "Worker model (sub-LLM / llm_query)", models, controller.workerModel, controller.config.subSampling.reasoning);
|
|
14
|
-
if (worker) {
|
|
14
|
+
if (worker === null) {
|
|
15
|
+
controller.workerModel = undefined;
|
|
16
|
+
controller.config.subSampling.reasoning = undefined;
|
|
17
|
+
} else if (worker) {
|
|
15
18
|
controller.workerModel = worker.model;
|
|
16
19
|
controller.config.subSampling.reasoning = worker.thinkingLevel;
|
|
17
20
|
}
|
|
18
21
|
|
|
19
22
|
await showConfigPanel(ctx, controller.config);
|
|
20
23
|
|
|
21
|
-
|
|
22
|
-
|
|
24
|
+
if (worker === null) {
|
|
25
|
+
controller.savedWorkerRef = undefined;
|
|
26
|
+
} else {
|
|
27
|
+
const effectiveWorker = controller.workerModel ?? cheapestModel(ctx.modelRegistry);
|
|
28
|
+
controller.savedWorkerRef = modelRef(controller.workerModel) ?? modelRef(effectiveWorker);
|
|
29
|
+
}
|
|
23
30
|
const persisted = await controller.persist();
|
|
24
31
|
if (!persisted) ctx.ui.notify("RLM: failed to save settings to ~/.pi/agent/rlm.json", "error");
|
|
25
32
|
setRlmModeStatus(ctx.ui, controller);
|
package/src/commands/rlm.ts
CHANGED
|
@@ -13,9 +13,6 @@ import type { RunHeader } from "../state/rows.ts";
|
|
|
13
13
|
import { buildRlmSystemPrompt } from "../prompts/system.ts";
|
|
14
14
|
import { RlmEmitter } from "../tool/rlm-events.ts";
|
|
15
15
|
import { RlmEventAggregator } from "../tool/rlm-aggregator.ts";
|
|
16
|
-
import { createTelemetrySink } from "../telemetry/index.ts";
|
|
17
|
-
import { applyEdits } from "../patch/index.ts";
|
|
18
|
-
import { tryExtractDiff } from "../core/answer.ts";
|
|
19
16
|
|
|
20
17
|
export function registerRlmCommand(pi: ExtensionAPI, controller: RlmController): void {
|
|
21
18
|
pi.registerCommand("rlm", {
|
|
@@ -112,14 +109,10 @@ async function executeRlmRunWithResume(
|
|
|
112
109
|
context: unknown,
|
|
113
110
|
): Promise<void> {
|
|
114
111
|
let handle: RunHandle | undefined;
|
|
115
|
-
let sink: Awaited<ReturnType<typeof createTelemetrySink>>;
|
|
116
112
|
let emitter: RlmEmitter | undefined;
|
|
117
|
-
let detachSink: (() => void) | undefined;
|
|
118
113
|
let aggregator: RlmEventAggregator | undefined;
|
|
119
114
|
try {
|
|
120
|
-
sink = await createTelemetrySink(controller.config.telemetry);
|
|
121
115
|
emitter = new RlmEmitter();
|
|
122
|
-
if (sink) detachSink = emitter.attachSink(sink);
|
|
123
116
|
aggregator = new RlmEventAggregator(emitter, (partial) => {
|
|
124
117
|
const d = partial.details;
|
|
125
118
|
if (!d) return;
|
|
@@ -146,20 +139,12 @@ async function executeRlmRunWithResume(
|
|
|
146
139
|
try {
|
|
147
140
|
const result = await done;
|
|
148
141
|
pi.sendMessage({ customType: "rlm-answer", content: result.answer, display: true });
|
|
149
|
-
const proposedDiffs = result.diffs?.length ? result.diffs : tryExtractDiff(result.answer);
|
|
150
|
-
await applyEdits(
|
|
151
|
-
result.edits ?? [],
|
|
152
|
-
proposedDiffs,
|
|
153
|
-
ctx,
|
|
154
|
-
);
|
|
155
142
|
} catch (e) {
|
|
156
143
|
ctx.ui.notify(`RLM resume failed: ${e instanceof Error ? e.message : String(e)}`, "error");
|
|
157
144
|
} finally {
|
|
158
145
|
clearRlmStatus(ctx.ui);
|
|
159
146
|
ctx.ui.setWidget?.("rlm-status", undefined);
|
|
160
|
-
detachSink?.();
|
|
161
147
|
aggregator?.dispose();
|
|
162
148
|
emitter?.shutdown();
|
|
163
|
-
try { await sink?.shutdown(); } catch { /* best-effort */ }
|
|
164
149
|
}
|
|
165
150
|
}
|
package/src/config/settings.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
5
|
import { getAgentDir, type ModelRegistry } from "@earendil-works/pi-coding-agent";
|
|
6
6
|
import type { Api, Model, ThinkingLevel } from "@earendil-works/pi-ai";
|
|
7
|
-
import type { RlmConfig, RunLogConfig
|
|
7
|
+
import type { RlmConfig, RunLogConfig } from "../core/types.ts";
|
|
8
8
|
import { DEFAULT_CONFIG } from "./defaults.ts";
|
|
9
9
|
|
|
10
10
|
export interface PersistedSettings {
|
|
@@ -30,26 +30,6 @@ function validateString(v: unknown): string | undefined {
|
|
|
30
30
|
return typeof v === "string" && v.trim() ? v : undefined;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
function validateTelemetry(raw: unknown): TelemetryConfig | undefined {
|
|
34
|
-
if (typeof raw !== "object" || raw === null) return undefined;
|
|
35
|
-
const r = raw as Record<string, unknown>;
|
|
36
|
-
const out: {
|
|
37
|
-
enabled?: boolean;
|
|
38
|
-
trackingUri?: string;
|
|
39
|
-
experimentId?: string;
|
|
40
|
-
maxQueueSize?: number;
|
|
41
|
-
} = {};
|
|
42
|
-
const enabled = validateBoolean(r.enabled);
|
|
43
|
-
if (enabled !== undefined) out.enabled = enabled;
|
|
44
|
-
const trackingUri = validateString(r.trackingUri);
|
|
45
|
-
if (trackingUri !== undefined) out.trackingUri = trackingUri;
|
|
46
|
-
const experimentId = validateString(r.experimentId);
|
|
47
|
-
if (experimentId !== undefined) out.experimentId = experimentId;
|
|
48
|
-
const maxQueueSize = validateNumber(r.maxQueueSize, 1);
|
|
49
|
-
if (maxQueueSize !== undefined) out.maxQueueSize = maxQueueSize;
|
|
50
|
-
return Object.freeze(out);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
33
|
function validateRunLog(raw: unknown): Partial<RunLogConfig> | undefined {
|
|
54
34
|
if (typeof raw !== "object" || raw === null) return undefined;
|
|
55
35
|
const r = raw as Record<string, unknown>;
|
|
@@ -102,8 +82,6 @@ function validateConfig(raw: unknown): Partial<RlmConfig> {
|
|
|
102
82
|
if (typeof r.smartReasoning === "string") out.smartReasoning = r.smartReasoning as ThinkingLevel;
|
|
103
83
|
const subSystemPrompt = validateString(r.subSystemPrompt);
|
|
104
84
|
if (subSystemPrompt !== undefined) out.subSystemPrompt = subSystemPrompt;
|
|
105
|
-
const telemetry = validateTelemetry(r.telemetry);
|
|
106
|
-
if (telemetry) out.telemetry = telemetry;
|
|
107
85
|
const runLog = validateRunLog(r.runLog);
|
|
108
86
|
if (runLog) out.runLog = runLog;
|
|
109
87
|
const sandboxInitTimeoutMs = validateNumber(r.sandboxInitTimeoutMs, 100);
|
|
@@ -167,7 +145,6 @@ export function mergeConfig(partial: Partial<RlmConfig>): RlmConfig {
|
|
|
167
145
|
...partial,
|
|
168
146
|
subSampling: { ...DEFAULT_CONFIG.subSampling, ...partial.subSampling },
|
|
169
147
|
rootSampling: Object.freeze({ ...DEFAULT_CONFIG.rootSampling, ...partial.rootSampling }),
|
|
170
|
-
...(partial.telemetry ? { telemetry: Object.freeze({ ...partial.telemetry }) } : {}),
|
|
171
148
|
...(partial.runLog ? { runLog: Object.freeze({ ...DEFAULT_CONFIG.runLog, ...partial.runLog }) } : {}),
|
|
172
149
|
};
|
|
173
150
|
}
|
package/src/core/answer.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Helpers for detecting and formatting the RLM final answer from a turn's REPL results. */
|
|
2
2
|
|
|
3
|
-
import type {
|
|
3
|
+
import type { ProposedEdit, ReplResult } from "../sandbox/protocol.ts";
|
|
4
4
|
import { truncateOutput } from "../text/parsing.ts";
|
|
5
5
|
|
|
6
6
|
/** First non-null final answer across a turn's executed blocks, or null. */
|
|
@@ -27,22 +27,6 @@ export function collectEdits(results: readonly ReplResult[]): ProposedEdit[] {
|
|
|
27
27
|
return [];
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
/** Last cumulative diff proposal set reported by a turn. */
|
|
31
|
-
export function collectDiffs(results: readonly ReplResult[]): ProposedDiffEdit[] {
|
|
32
|
-
for (let i = results.length - 1; i >= 0; i--) {
|
|
33
|
-
const diffs = results[i]?.diffs;
|
|
34
|
-
if (diffs && diffs.length > 0) return [...diffs];
|
|
35
|
-
}
|
|
36
|
-
return [];
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/** Parses a ```diff fence from answer text; returns [] if none found. */
|
|
40
|
-
export function tryExtractDiff(answer: string): ProposedDiffEdit[] {
|
|
41
|
-
const match = /```diff\n([\s\S]*?)```/.exec(answer);
|
|
42
|
-
if (!match) return [];
|
|
43
|
-
return [{ diff: match[1].trim() }];
|
|
44
|
-
}
|
|
45
|
-
|
|
46
30
|
/** True if any block in the turn raised an exception. Plain stderr does not count. */
|
|
47
31
|
export function turnHadError(results: readonly ReplResult[]): boolean {
|
|
48
32
|
return results.some((r) => r.raised);
|