@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 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
- <img src="../../assets/hero.png" alt="pi-rlm">
5
+ **Recursive Language Models (RLMs)**, implemented natively as a Pi extension —
6
+ FULLY LOCAL.
4
7
 
5
8
  </div>
6
9
 
7
- <div align="center">
10
+ ## Install
8
11
 
9
- <sub>
10
- **English** &nbsp;·&nbsp; <a href="README.zh-CN.md">中文</a> &nbsp;·&nbsp; <a href="README.ru.md">Русский</a>
11
- </sub>
12
+ ```bash
13
+ pi install npm:@hicaru/pi-rlm
14
+ ```
12
15
 
13
- </div>
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
- # pi-rlm Recursive Language Models for the [Pi](https://github.com/earendil-works) Coding Agent
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
- **Recursive Language Models (RLMs)**, implemented natively as a Pi extension —
22
- no extra servers, no Docker, no sockets.
35
+ <sub>
36
+ **English** &nbsp;·&nbsp; <a href="README.zh-CN.md">中文</a> &nbsp;·&nbsp; <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
- > and the [Python `rlm` library](https://github.com/alexzhang13/rlm-minimal)). It is **not** the Python library.
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
- pi process (TypeScript)
56
- ├─ /rlm ──► engine drives the SMART (root) model turn-by-turn (writes ```repl``` Python)
57
- │ │ each turn: parse repl blocks ──► run in sandbox ──► feed stdout back
58
-
59
- ├─ bridge ── llm_query / llm_query_batched ──► WORKER model (serverless, in-process)
60
- │ rlm_query ──► recursive child RLM (own sandbox), depth-capped
61
- ├─ AgentTree ──► live agent/subagent tree above the editor (roles, depth, cost, tokens)
62
- └─ PythonSandbox ── `python3 worker.py` ──[JSONL over stdio, bidirectional]── persistent REPL
63
- ```
64
-
65
- - **No servers, no sockets, no Docker.** The only external process is one local `python3` sandbox.
66
- When sandbox code calls `llm_query`, the worker writes a request on stdout and blocks on stdin;
67
- Pi services it in-process and writes the reply back. **Provider API keys never enter the sandbox.**
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
- > **Git installs** require the package manifest to live at the installed repository root.
92
- > For monorepo subdirectories like this one, prefer the local-path or npm flow above.
93
-
94
- If you previously copied the extension folder directly, remove it so it does not shadow the package:
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 falls back to `llm_query` |
142
- | Max iterations | `30` | turns before the engine finalizes |
143
- | Budget ceiling | none | stops the whole tree when USD spend exceeds this |
144
- | Max consecutive errors | `5` | stops after N consecutive error turns |
145
- | REPL block timeout | `120s` | per-`repl`-block wall-clock (SIGALRM in the worker) |
146
- | Max concurrent sub-calls | `4` | pool size for `*_batched` |
147
- | Orchestrator addendum | on | "delegate, don't solve" guidance |
148
- | Trajectory compaction | on (0.85) | summarize history when it nears the context window |
149
- | `yolo` | off | apply proposed edits immediately, skipping the review popup |
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
- без дополнительных серверов, Docker или сокетов.
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
- > and the [Python `rlm` library](https://github.com/alexzhang13/rlm-minimal)). It is **not** the Python library.
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
- Реализовано на основе эталонного проекта [`rlm`](https://github.com/alexzhang13/rlm-minimal) на Python и метода из [статьи RLM](https://arxiv.org/abs/2512.24601), с нативной переработкой для Pi.
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
- 无需额外服务器,无需 Docker,无需 socket。
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
- > [Python `rlm` 库](https://github.com/alexzhang13/rlm-minimal))。它**不是**那个 Python 库。
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/ agent-tree · events · reads/writes · resume · paths · rows
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
- 基于 Python 参考实现 [`rlm`](https://github.com/alexzhang13/rlm-minimal)
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.0",
3
+ "version": "0.1.2",
4
4
  "type": "module",
5
- "description": "Recursive Language Model (RLM) for the Pi coding agent — native, server-less.",
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": ">=0.79.0",
42
- "@earendil-works/pi-coding-agent": ">=0.79.0",
43
- "@earendil-works/pi-tui": ">=0.79.0",
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: async (questions: readonly AskQuestion[]): Promise<AskAnswer[]> => {
61
- const callTool = (ctx as unknown as ToolInvoker).callTool;
62
- if (typeof callTool === "function") {
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
- const effectiveWorker = controller.workerModel ?? cheapestModel(ctx.modelRegistry);
22
- controller.savedWorkerRef = modelRef(controller.workerModel) ?? modelRef(effectiveWorker);
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);
@@ -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
  }
@@ -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, TelemetryConfig } from "../core/types.ts";
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
  }
@@ -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 { ProposedDiffEdit, ProposedEdit, ReplResult } from "../sandbox/protocol.ts";
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);