@hicaru/pi-rlm 0.2.0 → 0.2.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 +12 -35
- package/README.ru.md +18 -23
- package/README.zh-CN.md +17 -28
- package/package.json +1 -1
- package/src/bridge/library.ts +61 -26
- package/src/bridge/subcall-handlers.ts +382 -0
- package/src/commands/rlm-config.ts +47 -18
- package/src/commands/rlm.ts +3 -152
- package/src/config/defaults.ts +7 -15
- package/src/config/settings.ts +8 -32
- package/src/context/library-context.ts +90 -17
- package/src/core/engine.ts +115 -360
- package/src/core/history.ts +1 -1
- package/src/core/limits.ts +5 -12
- package/src/core/resource-limits.ts +0 -2
- package/src/core/types.ts +3 -36
- package/src/index.ts +49 -10
- package/src/mode/llm-model.ts +54 -0
- package/src/mode/rlm-mode.ts +26 -57
- package/src/prompts/glossary.ts +287 -0
- package/src/prompts/native.ts +127 -0
- package/src/prompts/system.ts +14 -386
- package/src/sandbox/context-file.ts +154 -0
- package/src/sandbox/interrupts.ts +145 -0
- package/src/sandbox/protocol.ts +14 -69
- package/src/sandbox/py/guards.py +150 -0
- package/src/sandbox/py/retrieval.py +265 -0
- package/src/sandbox/py/tasks.py +116 -0
- package/src/sandbox/py/worker.py +836 -0
- package/src/sandbox/sandbox-manager.ts +33 -6
- package/src/sandbox/sandbox.ts +153 -182
- package/src/text/tokens.ts +29 -3
- package/src/tool/background-tasks.ts +95 -0
- package/src/tool/repl-details.ts +4 -2
- package/src/tool/repl-render.ts +58 -0
- package/src/tool/repl-result.ts +70 -0
- package/src/tool/repl-tool.ts +178 -216
- package/src/tool/rlm-aggregator.ts +2 -10
- package/src/tool/rlm-details.ts +0 -2
- package/src/tool/rlm-events.ts +10 -16
- package/src/tool/rlm-tool.ts +1 -12
- package/src/tool/subcall-render.ts +15 -3
- package/src/tool/subcall-store.ts +57 -1
- package/src/ui/config-panel.ts +4 -16
- package/src/ui/intro.ts +1 -2
- package/src/ui/model-picker.ts +34 -10
- package/src/ui/status.ts +3 -7
- package/src/util/concurrency.ts +91 -13
- package/src/util/trace.ts +42 -0
- package/src/bridge/fallback-todo.ts +0 -137
- package/src/bridge/interactive.ts +0 -65
- package/src/bridge/llm-query.ts +0 -156
- package/src/bridge/pi-interactive.ts +0 -41
- package/src/bridge/rlm-query.ts +0 -108
- package/src/core/artifacts.ts +0 -89
- package/src/core/critique.ts +0 -92
- package/src/core/gates.ts +0 -301
- package/src/core/pipeline-handlers.ts +0 -319
- package/src/core/pipeline.ts +0 -268
- package/src/prompts/phases.ts +0 -104
- package/src/sandbox/worker.py +0 -1078
- package/src/state/index.ts +0 -24
- package/src/state/internal.ts +0 -46
- package/src/state/paths.ts +0 -44
- package/src/state/reads.ts +0 -133
- package/src/state/resume.ts +0 -173
- package/src/state/rows.ts +0 -123
- package/src/state/writes.ts +0 -58
package/README.md
CHANGED
|
@@ -58,7 +58,10 @@ sub-LLM calls, hence the name.
|
|
|
58
58
|
|
|
59
59
|
- A **root orchestrator** model drives a **persistent Python REPL** turn-by-turn.
|
|
60
60
|
- Long-context work is **delegated** to cheap worker models via `llm_query` / `llm_query_batched`.
|
|
61
|
-
- Hard sub-problems **recurse** into child RLMs via `rlm_query` (depth-capped).
|
|
61
|
+
- Hard sub-problems **recurse** into child RLMs via `rlm_query` (depth-capped). A child inherits
|
|
62
|
+
its parent's `context` — the repository plus every library loaded with `load_library()` — so it
|
|
63
|
+
runs the same retrieval primitives over the same paths. Inheritance costs no extra tokens: the
|
|
64
|
+
content lives in the sandbox, and only a size line reaches the model.
|
|
62
65
|
- Everything runs **in-process** — the only external process is one local `python3` worker.
|
|
63
66
|
|
|
64
67
|
> This is a Pi-plugin reimplementation of the RLM method (see the [RLM paper](https://arxiv.org/abs/2512.24601)).
|
|
@@ -94,9 +97,6 @@ sub-LLM calls, hence the name.
|
|
|
94
97
|
| `/rlm` | `Ctrl+Shift+R` | Toggle persistent RLM mode (route plain prompts through the RLM engine) |
|
|
95
98
|
| `/rlm-stop` | | Abort an in-progress run |
|
|
96
99
|
| `/rlm-config` | | Pick smart + worker models and tune run settings |
|
|
97
|
-
| `/rlm-resume` | | Resume an interrupted run (default `@latest`) |
|
|
98
|
-
| `/rlm-runs` | | List recent runs |
|
|
99
|
-
| `/rlm-help` | | Show the startup guide & cheatsheet |
|
|
100
100
|
|
|
101
101
|
While a run is active, a **live tree** shows the root orchestrator and every sub-LLM /
|
|
102
102
|
recursive child with status, model, cost, tokens, and duration. The final answer is posted
|
|
@@ -113,13 +113,9 @@ These functions are injected into the model's Python namespace inside the REPL:
|
|
|
113
113
|
| `llm_query` | `(prompt, model=None) -> str` | One-shot sub-LLM call (worker model) |
|
|
114
114
|
| `llm_query_batched` | `(prompts, model=None) -> list[str]` | Concurrent sub-LLM calls (pool-bounded) |
|
|
115
115
|
| `llm_query_chunked` | `(text, prompt, model=None) -> list[str]` | Split large text into cap-sized chunks and fan out via sub-LLMs |
|
|
116
|
-
| `rlm_query` | `(prompt, model=None) -> str` | Recursive child RLM with its own sandbox (depth-capped) |
|
|
117
|
-
| `rlm_query_batched` | `(prompts, model=None) -> list[str]` | Concurrent recursive child RLMs |
|
|
118
|
-
| `todo` | `(action, **kwargs) -> str` | Task list: `create`/`update`/`list`/`get`/`delete`/`clear` |
|
|
119
|
-
| `ask_user_question` | `(questions) -> list[dict]` | Ask the user structured questions (depth 0 only) |
|
|
116
|
+
| `rlm_query` | `(prompt, model=None, paths=None) -> str` | Recursive child RLM with its own sandbox (depth-capped). Inherits your `context`; `paths` narrows it by prefix |
|
|
117
|
+
| `rlm_query_batched` | `(prompts, model=None, paths=None) -> list[str]` | Concurrent recursive child RLMs, sharing one `paths` slice |
|
|
120
118
|
| `load_library` | `(source) -> dict \| str` | Append an external dir, file, or git URL into `context` under `lib/<id>/` |
|
|
121
|
-
| `save_artifact` | `(kind, content) -> str` | Persist a stage artifact (`clarification` / `research` / `plan` / `validation`) under `.rlm/artifacts/` (root depth only). Returns preflight gate critique. |
|
|
122
|
-
| `advance_phase` | `(phase, summary=None) -> str` | Advance one step in order `clarify → research → blueprint → validate` (clarify skipped when `askUserQuestion` is off). **Engine-gated** on the latest artifact + interview rounds. Rejected transitions return the gate error. |
|
|
123
119
|
| `SHOW_VARS` | `() -> str` | List currently defined variables & their types |
|
|
124
120
|
| `answer` | `dict` | Set `answer["content"]=...; answer["ready"]=True` to finalize |
|
|
125
121
|
|
|
@@ -139,21 +135,7 @@ lib = [f for f in context if f["path"].startswith(info["path_prefix"])]
|
|
|
139
135
|
|
|
140
136
|
There is no `context_1` / `context_2` — only `context`. Paths are namespaced so multiple
|
|
141
137
|
libraries do not collide. Toggle via `/rlm-config` → **Library loader** (`libraryLoader`,
|
|
142
|
-
default on).
|
|
143
|
-
(`context.<N>.json`) that is **merged back into `context`** on resume.
|
|
144
|
-
|
|
145
|
-
### Artifact-gated pipeline (opt-in via `pipeline: true`)
|
|
146
|
-
|
|
147
|
-
When enabled at root depth:
|
|
148
|
-
|
|
149
|
-
1. **Goal capture** — the brief is written verbatim to `.rlm/artifacts/goal/goal-<ts>.md` with a pre-run dirty-tree baseline.
|
|
150
|
-
2. **Stages** — `clarify → research → blueprint → validate` (**read-only** — produces a validated plan; does not write code). Each produces a durable markdown artifact with frontmatter contracts; chat history is **reset** at every phase boundary (artifacts are the only channel; REPL vars persist).
|
|
151
|
-
3. **Clarify (intake)** — interviews the user via `ask_user_question` (intent first, then evidence-confirmed decisions). Writes `.rlm/artifacts/clarifications/*` with `decisions_count` / `open_questions_count`. Engine gate: **≥1 serviced ask round** + artifact contract. When **`askUserQuestion` is off**, clarify is skipped and the run starts at research.
|
|
152
|
-
4. **Gates (TypeScript, never LLM judgment)** — `status: ready`; clarify structure; plan `phases:` ≡ fence-aware `## Phase N:` headings; every `file:line` citation resolves; validate carries `blockers_count` + `verdict`. Preflight critique runs on every `save_artifact`.
|
|
153
|
-
5. **Validate** — adversarial plan review against the tree (not a post-implementation diff check). Final answer is the validated plan.
|
|
154
|
-
6. **Corrective loop** — `blockers_count > 0` re-enters blueprint (superseded plan kept for context), bounded by `maxBackwardJumps` (default 2).
|
|
155
|
-
|
|
156
|
-
Native RLM mode authors file changes with Pi's native `edit` / `write` tools. Sub-LLMs extract and locate; they never ship code.
|
|
138
|
+
default on). A library loaded at any point is inherited by every child spawned afterwards.
|
|
157
139
|
|
|
158
140
|
## Settings (`/rlm-config`)
|
|
159
141
|
|
|
@@ -164,26 +146,21 @@ Native RLM mode authors file changes with Pi's native `edit` / `write` tools. Su
|
|
|
164
146
|
| Max recursion depth | `4` | `rlm_query` past this degrades to plain `llm_query` |
|
|
165
147
|
| Max iterations | `30` | root REPL turns before RLM asks for a final answer |
|
|
166
148
|
| REPL block timeout (s) | `120` | wall-clock limit for one Python REPL block (SIGALRM) |
|
|
167
|
-
| Max concurrent sub-calls | `
|
|
168
|
-
|
|
|
149
|
+
| Max concurrent sub-calls | `16` | concurrency pool size for `*_batched` |
|
|
150
|
+
| Max concurrent children | `6` | concurrent `rlm_query` child engines per depth |
|
|
169
151
|
| Wall-clock ceiling (min) | none | total runtime cap for the whole recursive tree |
|
|
170
152
|
| Token ceiling | none | total input+output token cap for the whole recursive tree |
|
|
171
153
|
| Max consecutive errors | `5` | stop after N consecutive failing turns (none = off) |
|
|
172
154
|
| Orchestrator addendum | on | divide-and-conquer guidance in the root system prompt |
|
|
173
|
-
| Phase pipeline | off | artifact-gated clarify→research→blueprint→validate (read-only plan pipeline) |
|
|
174
|
-
| Max validate→blueprint loops | `2` | bounded corrective re-entries when validation reports blockers |
|
|
175
|
-
| Ask user question | on | when pipeline is on, enables clarify intake; when off, pipeline starts at research |
|
|
176
155
|
| Trajectory compaction | on (0.65) | summarize old turns when history nears the context window |
|
|
177
156
|
| Root model output cap (tok) | `16384` | max output tokens per root-model turn |
|
|
178
157
|
| Sandbox init timeout | `30000` ms | how long to wait for the Python worker to start |
|
|
179
|
-
| `askUserQuestion` | on | expose `ask_user_question()` to the model |
|
|
180
|
-
| `todo` | on | expose `todo()` to the model |
|
|
181
158
|
| Library loader | on | expose `load_library()` for external dirs/files/git repos |
|
|
182
159
|
|
|
183
160
|
> **Concurrency note:** each `rlm_query` child spawns its own `python3` worker (~50–150 ms
|
|
184
|
-
> cold start).
|
|
185
|
-
>
|
|
186
|
-
> caps (above) bound
|
|
161
|
+
> cold start). Children are bounded separately (`maxConcurrentChildren`, default 6) because
|
|
162
|
+
> each holds a full Python process and its own copy of the inherited context. Error and
|
|
163
|
+
> wall-clock caps (above) still bound a runaway tree.
|
|
187
164
|
|
|
188
165
|
## Security
|
|
189
166
|
|
package/README.ru.md
CHANGED
|
@@ -34,6 +34,9 @@
|
|
|
34
34
|
- **Модель-оркестратор** управляет постоянным Python REPL пошагово.
|
|
35
35
|
- Работа с длинным контекстом **делегируется** дешевым worker-моделям через `llm_query` / `llm_query_batched`.
|
|
36
36
|
- Сложные подзадачи **рекурсивно** передаются в дочерние RLM через `rlm_query` (с ограничением глубины).
|
|
37
|
+
Дочерний RLM наследует `context` родителя — репозиторий и все библиотеки, загруженные через
|
|
38
|
+
`load_library()`, — и работает с теми же путями. Наследование не стоит дополнительных токенов:
|
|
39
|
+
содержимое живёт в песочнице, модель видит только строку с размером.
|
|
37
40
|
- Все работает **in-process** — единственным внешним процессом является локальный worker `python3`.
|
|
38
41
|
|
|
39
42
|
> This is a Pi-plugin reimplementation of the RLM method (see the [RLM paper](https://arxiv.org/abs/2512.24601)).
|
|
@@ -54,7 +57,7 @@ pi process (TypeScript)
|
|
|
54
57
|
|
|
55
58
|
- **Никаких серверов, сокетов или Docker.** Единственным внешним процессом является локальная песочница `python3`. Когда код в песочнице вызывает `llm_query`, worker пишет запрос в stdout и блокируется на stdin; Pi обрабатывает его внутри своего процесса и записывает ответ обратно. **API-ключи провайдеров никогда не попадают в песочницу.**
|
|
56
59
|
- Песочница предоставляет `context`, `llm_query`, `llm_query_batched`, `rlm_query`,
|
|
57
|
-
`rlm_query_batched`, `SHOW_VARS()`, `
|
|
60
|
+
`rlm_query_batched`, `SHOW_VARS()`, `ask_user_question()` и словарь `answer`.
|
|
58
61
|
Модель отправляет окончательный результат, устанавливая `answer["ready"] = True`.
|
|
59
62
|
|
|
60
63
|
## Установка
|
|
@@ -91,9 +94,6 @@ rm -rf ~/.pi/agent/extensions/rlm
|
|
|
91
94
|
| `/rlm` | `Ctrl+Shift+R` | Переключить постоянный режим RLM (направлять обычные промпты через движок RLM) |
|
|
92
95
|
| `/rlm-stop` | | Прервать текущий запуск |
|
|
93
96
|
| `/rlm-config` | | Выбрать smart- и worker-модели и настроить параметры запуска |
|
|
94
|
-
| `/rlm-resume` | | Возобновить прерванный запуск (по умолчанию `@latest`) |
|
|
95
|
-
| `/rlm-runs` | | Список последних запусков |
|
|
96
|
-
| `/rlm-help` | | Показать руководство по запуску и шпаргалку |
|
|
97
97
|
|
|
98
98
|
Пока запуск активен, **живое дерево** отображает корневой оркестратор и каждый sub-LLM / рекурсивный дочерний элемент со статусом, моделью, стоимостью, токенами и длительностью. Окончательный ответ публикуется в чате в формате markdown; любые правки кода собираются в виде диффов и проверяются через всплывающее окно (если не включен `yolo`).
|
|
99
99
|
|
|
@@ -107,9 +107,8 @@ rm -rf ~/.pi/agent/extensions/rlm
|
|
|
107
107
|
| `llm_query` | `(prompt, model=None) -> str` | Одноразовый вызов sub-LLM (worker-модель) |
|
|
108
108
|
| `llm_query_batched` | `(prompts, model=None) -> list[str]` | Параллельные вызовы sub-LLM (с ограничением пула) |
|
|
109
109
|
| `llm_query_chunked` | `(text, prompt, model=None) -> list[str]` | Дробит большой текст на части по лимиту и обрабатывает через sub-LLM |
|
|
110
|
-
| `rlm_query` | `(prompt, model=None) -> str` | Рекурсивный дочерний RLM со своей песочницей (с ограничением глубины) |
|
|
111
|
-
| `rlm_query_batched` | `(prompts, model=None) -> list[str]` | Параллельные рекурсивные дочерние RLM |
|
|
112
|
-
| `todo` | `(action, **kwargs) -> str` | Список задач: `create`/`update`/`list`/`get`/`delete`/`clear` |
|
|
110
|
+
| `rlm_query` | `(prompt, model=None, paths=None) -> str` | Рекурсивный дочерний RLM со своей песочницей (с ограничением глубины). Наследует ваш `context`; `paths` сужает его по префиксу |
|
|
111
|
+
| `rlm_query_batched` | `(prompts, model=None, paths=None) -> list[str]` | Параллельные рекурсивные дочерние RLM с общим срезом `paths` |
|
|
113
112
|
| `ask_user_question` | `(questions) -> list[dict]` | Задать пользователю структурированные вопросы (только на глубине 0) |
|
|
114
113
|
| `SHOW_VARS` | `() -> str` | Список текущих переменных и их типов |
|
|
115
114
|
| `answer` | `dict` | Установите `answer["content"]=...; answer["ready"]=True` для завершения |
|
|
@@ -130,13 +129,11 @@ rm -rf ~/.pi/agent/extensions/rlm
|
|
|
130
129
|
| Trajectory compaction | вкл (0.85) | суммаризация истории при приближении к лимиту окна контекста |
|
|
131
130
|
| `yolo` | выкл | применять предлагаемые правки немедленно, пропуская окно подтверждения |
|
|
132
131
|
| `askUserQuestion` | вкл | предоставить доступ к `ask_user_question()` для модели |
|
|
133
|
-
| `todo` | вкл | предоставить доступ к `todo()` для модели |
|
|
134
132
|
|
|
135
133
|
> **Примечание по параллелизму:** каждый дочерний `rlm_query` запускает собственного worker `python3` (~50–150 мс «холодного старта»). В худшем случае количество параллельных интерпретаторов ≈ `maxConcurrentSubcalls`^(depth−1); при настройках по умолчанию (глубина 4, параллелизм 4) это 4³ = 64 в патологическом случае. Лимиты бюджета и ошибок (см. выше) ограничивают общие затраты независимо от степени разветвления.
|
|
136
134
|
|
|
137
135
|
## Логи запусков
|
|
138
136
|
|
|
139
|
-
- **Логи запусков** (`runLog`): включены по умолчанию. Каждый запуск записывает след в формате JSONL в `.rlm/runs/` (по умолчанию) с ограничением `maxRuns` (50). Поддерживает **снимки** (`sandbox.pkl`) и **возобновление** прерванных запусков через `/rlm-resume`. Снимки защищены сессионным `nonce` для предотвращения повторов между сессиями.
|
|
140
137
|
|
|
141
138
|
## Безопасность
|
|
142
139
|
|
|
@@ -150,21 +147,19 @@ rm -rf ~/.pi/agent/extensions/rlm
|
|
|
150
147
|
|
|
151
148
|
```
|
|
152
149
|
src/
|
|
153
|
-
sandbox/ worker.py
|
|
154
|
-
bridge/ model.ts (
|
|
155
|
-
core/ engine.ts (
|
|
156
|
-
prompts/
|
|
157
|
-
text/
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
ui/ виджет-дерева · статус · выбор-модели · панель-конфигурации · вступление · тема
|
|
150
|
+
sandbox/ py/ (worker.py · guards · retrieval · tasks) · sandbox.ts · interrupts · protocol · sandbox-manager · context-file
|
|
151
|
+
bridge/ model.ts (single completion) · subcall-handlers.ts (the one llm/rlm impl) · ask-user · library
|
|
152
|
+
core/ engine.ts (the loop) · iteration · limits · resource-limits · answer · compaction · history · types
|
|
153
|
+
prompts/ glossary (shared REPL vocabulary) · system (headless) · native · user
|
|
154
|
+
text/ parsing (repl blocks) · tokens · preview
|
|
155
|
+
tool/ repl-tool · repl-result · repl-render · rlm-tool · rlm-events · rlm-aggregator · subcall-store · background-tasks
|
|
156
|
+
config/ defaults · settings (rlm.json persistence + validation)
|
|
157
|
+
context/ repomix repository packing + library context merge
|
|
158
|
+
ui/ status · model-picker · config-panel · intro · theme
|
|
163
159
|
commands/ rlm · rlm-config
|
|
164
|
-
mode/ rlm-mode (
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
test/ фазы 1–9 · native-smoke · native-mode · помощники
|
|
160
|
+
mode/ rlm-mode (controller) · worker-model (cheapest pick) · native-guards
|
|
161
|
+
util/ errors · concurrency · trace
|
|
162
|
+
test/ phase suites · native-smoke · native-mode · helpers
|
|
168
163
|
```
|
|
169
164
|
|
|
170
165
|
## Тесты
|
package/README.zh-CN.md
CHANGED
|
@@ -33,7 +33,9 @@
|
|
|
33
33
|
|
|
34
34
|
- **根编排器**模型逐轮驱动一个**持久化的 Python REPL**。
|
|
35
35
|
- 长上下文工作通过 `llm_query` / `llm_query_batched` **委派**给廉价的工作模型。
|
|
36
|
-
- 困难的子问题通过 `rlm_query` **递归**到子 RLM
|
|
36
|
+
- 困难的子问题通过 `rlm_query` **递归**到子 RLM 中(设有深度限制)。子 RLM 继承父级的 `context`
|
|
37
|
+
——仓库以及通过 `load_library()` 加载的所有库——因此可以在相同的路径上使用相同的检索原语。
|
|
38
|
+
继承不消耗额外的 token:内容存放在沙箱中,模型只看到一行大小信息。
|
|
37
39
|
- 所有内容均**在进程内**运行 —— 唯一的外部进程是一个本地的 `python3` worker。
|
|
38
40
|
|
|
39
41
|
> 这是 RLM 方法的 Pi 插件重新实现(参见 [RLM 论文](https://arxiv.org/abs/2512.24601))。
|
|
@@ -56,7 +58,7 @@ pi 进程 (TypeScript)
|
|
|
56
58
|
当沙箱代码调用 `llm_query` 时,worker 在 stdout 上写入请求并在 stdin 上阻塞;
|
|
57
59
|
Pi 在进程内提供服务并将回复写回。**供应商 API 密钥绝不会进入沙箱。**
|
|
58
60
|
- 沙箱公开了 `context`, `llm_query`, `llm_query_batched`, `rlm_query`,
|
|
59
|
-
`rlm_query_batched`, `SHOW_VARS()`, `
|
|
61
|
+
`rlm_query_batched`, `SHOW_VARS()`, `ask_user_question()` 以及一个 `answer` 字典。
|
|
60
62
|
模型通过设置 `answer["ready"] = True` 来提交最终结果。
|
|
61
63
|
|
|
62
64
|
## 安装
|
|
@@ -96,9 +98,6 @@ rm -rf ~/.pi/agent/extensions/rlm
|
|
|
96
98
|
| `/rlm` | `Ctrl+Shift+R` | 切换持久化 RLM 模式 (通过 RLM 引擎路由普通提示词) |
|
|
97
99
|
| `/rlm-stop` | | 终止正在运行的任务 |
|
|
98
100
|
| `/rlm-config` | | 选择 smart + worker 模型并调整运行设置 |
|
|
99
|
-
| `/rlm-resume` | | 恢复被中断的任务 (默认 `@latest`) |
|
|
100
|
-
| `/rlm-runs` | | 列出最近的任务 |
|
|
101
|
-
| `/rlm-help` | | 显示启动指南和速查表 |
|
|
102
101
|
|
|
103
102
|
在任务激活期间,一个**实时树**会显示根编排器和每个子 LLM /
|
|
104
103
|
递归子节点的状态、模型、成本、token 和持续时间。最终答案将以 markdown 形式发布
|
|
@@ -114,9 +113,8 @@ rm -rf ~/.pi/agent/extensions/rlm
|
|
|
114
113
|
| `llm_query` | `(prompt, model=None) -> str` | 单次子 LLM 调用 (worker 模型) |
|
|
115
114
|
| `llm_query_batched` | `(prompts, model=None) -> list[str]` | 并发子 LLM 调用 (池上限) |
|
|
116
115
|
| `llm_query_chunked` | `(text, prompt, model=None) -> list[str]` | 将大文本拆分为不超过上限的块并通过子 LLM 处理 |
|
|
117
|
-
| `rlm_query` | `(prompt, model=None) -> str` | 具有自有沙箱的递归子 RLM (设有深度限制) |
|
|
118
|
-
| `rlm_query_batched` | `(prompts, model=None) -> list[str]` | 并发递归子 RLM |
|
|
119
|
-
| `todo` | `(action, **kwargs) -> str` | 任务列表:`create`/`update`/`list`/`get`/`delete`/`clear` |
|
|
116
|
+
| `rlm_query` | `(prompt, model=None, paths=None) -> str` | 具有自有沙箱的递归子 RLM (设有深度限制)。继承父级的 `context`;`paths` 按前缀缩小范围 |
|
|
117
|
+
| `rlm_query_batched` | `(prompts, model=None, paths=None) -> list[str]` | 并发递归子 RLM,共享同一个 `paths` 切片 |
|
|
120
118
|
| `ask_user_question` | `(questions) -> list[dict]` | 向用户提出结构化问题 (仅限深度 0) |
|
|
121
119
|
| `SHOW_VARS` | `() -> str` | 列出当前定义的变量及其类型 |
|
|
122
120
|
| `answer` | `dict` | 设置 `answer["content"]=...; answer["ready"]=True` 以结束 |
|
|
@@ -137,7 +135,6 @@ rm -rf ~/.pi/agent/extensions/rlm
|
|
|
137
135
|
| Trajectory compaction | on (0.85) | 当历史记录接近上下文窗口时进行总结 |
|
|
138
136
|
| `yolo` | off | 立即应用建议的修改,跳过审核弹出窗 |
|
|
139
137
|
| `askUserQuestion` | on | 向模型公开 `ask_user_question()` |
|
|
140
|
-
| `todo` | on | 向模型公开 `todo()` |
|
|
141
138
|
|
|
142
139
|
> **并发注意:** 每个 `rlm_query` 子节点都会启动自己的 `python3` worker (冷启动约 50–150 毫秒)。
|
|
143
140
|
> 最坏情况下的并发解释器数量 ≈ `maxConcurrentSubcalls`^(depth−1);在
|
|
@@ -146,12 +143,6 @@ rm -rf ~/.pi/agent/extensions/rlm
|
|
|
146
143
|
|
|
147
144
|
## 运行日志
|
|
148
145
|
|
|
149
|
-
- **运行日志** (`runLog`):默认始终开启。每次运行将 JSONL 轨迹写入 `.rlm/runs/`
|
|
150
|
-
(默认),上限为 `maxRuns` (50)。支持通过 `/rlm-resume` 进行**快照** (`sandbox.pkl`) 和**恢复**
|
|
151
|
-
被中断的任务。快照受每个会话的 `nonce` 保护,以防止跨会话重放。
|
|
152
|
-
|
|
153
|
-
## 安全性
|
|
154
|
-
|
|
155
146
|
- **密钥隔离**:供应商密钥仅存在于 TypeScript (`AuthStorage`) 中;沙箱
|
|
156
147
|
接收提示词并返回文本 —— 绝不接触密钥。
|
|
157
148
|
- **环境清理**:在 worker 启动前会剥离敏感环境变量 (API 密钥, token)。
|
|
@@ -170,21 +161,19 @@ rm -rf ~/.pi/agent/extensions/rlm
|
|
|
170
161
|
|
|
171
162
|
```
|
|
172
163
|
src/
|
|
173
|
-
sandbox/ worker.py
|
|
174
|
-
bridge/ model.ts (
|
|
175
|
-
core/ engine.ts (the loop) · iteration · limits · answer · compaction ·
|
|
176
|
-
prompts/
|
|
177
|
-
text/ parsing (repl blocks) · tokens · preview
|
|
178
|
-
|
|
179
|
-
tool/ repl-tool · rlm-events · aggregator · propose-edits · emitter-listener
|
|
164
|
+
sandbox/ py/ (worker.py · guards · retrieval · tasks) · sandbox.ts · interrupts · protocol · sandbox-manager · context-file
|
|
165
|
+
bridge/ model.ts (single completion) · subcall-handlers.ts (the one llm/rlm impl) · ask-user · library
|
|
166
|
+
core/ engine.ts (the loop) · iteration · limits · resource-limits · answer · compaction · history · types
|
|
167
|
+
prompts/ glossary (shared REPL vocabulary) · system (headless) · native · user
|
|
168
|
+
text/ parsing (repl blocks) · tokens · preview
|
|
169
|
+
tool/ repl-tool · repl-result · repl-render · rlm-tool · rlm-events · rlm-aggregator · subcall-store · background-tasks
|
|
180
170
|
config/ defaults · settings (rlm.json persistence + validation)
|
|
181
|
-
context/ repomix
|
|
182
|
-
ui/
|
|
171
|
+
context/ repomix repository packing + library context merge
|
|
172
|
+
ui/ status · model-picker · config-panel · intro · theme
|
|
183
173
|
commands/ rlm · rlm-config
|
|
184
|
-
mode/ rlm-mode (controller) ·
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
test/ phase1–phase9 · native-smoke · native-mode · helpers
|
|
174
|
+
mode/ rlm-mode (controller) · worker-model (cheapest pick) · native-guards
|
|
175
|
+
util/ errors · concurrency · trace
|
|
176
|
+
test/ phase suites · native-smoke · native-mode · helpers
|
|
188
177
|
```
|
|
189
178
|
|
|
190
179
|
## 测试
|
package/package.json
CHANGED
package/src/bridge/library.ts
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared load_library handler for headless engine and native repl() mode.
|
|
3
3
|
*
|
|
4
|
-
* Host packs the source via resolveLibrarySource (namespaced under lib/<id>/)
|
|
5
|
-
*
|
|
6
|
-
* append into the single `context` list.
|
|
4
|
+
* Host packs the source via resolveLibrarySource (namespaced under lib/<id>/) and returns the
|
|
5
|
+
* payload for the worker to append into the single `context` list.
|
|
7
6
|
*
|
|
8
|
-
* Idempotency is host-side: re-loading a source that was already packed does
|
|
9
|
-
*
|
|
10
|
-
* trails free of duplicate library slots.
|
|
7
|
+
* Idempotency is host-side: re-loading a source that was already packed does not re-clone or
|
|
8
|
+
* re-pack, and the prefix set is the only state that decides it.
|
|
11
9
|
*
|
|
12
10
|
* Late-bound deps (getCwd / getEmitter) keep a single handler closure correct
|
|
13
11
|
* across native repl() calls — getOrCreate only installs handlers at spawn.
|
|
@@ -31,33 +29,67 @@ export interface LibraryBridgeOpts {
|
|
|
31
29
|
readonly getEmitter?: () => RlmEmitter | null | undefined;
|
|
32
30
|
readonly parentId?: string;
|
|
33
31
|
readonly signal?: AbortSignal;
|
|
34
|
-
/**
|
|
35
|
-
readonly
|
|
32
|
+
/** Prefixes already present in context — seeds host-side idempotency after a sandbox restart. */
|
|
33
|
+
readonly loadedPrefixes?: readonly string[];
|
|
36
34
|
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
35
|
+
* The live context this sandbox holds. Read to refuse pre-flight exactly what the worker's
|
|
36
|
+
* `_append_library` would reject, before any prefix is committed.
|
|
39
37
|
*/
|
|
40
|
-
readonly
|
|
41
|
-
/**
|
|
42
|
-
|
|
38
|
+
readonly getContext?: () => unknown;
|
|
39
|
+
/**
|
|
40
|
+
* Post-load hook. The engine grows its live context here; native mode grows
|
|
41
|
+
* SandboxManager.contextPayload.
|
|
42
|
+
*/
|
|
43
|
+
readonly onLoaded?: (payload: unknown) => void | Promise<void>;
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
export interface LibraryHandlerBundle {
|
|
46
47
|
readonly handlers: Pick<SubLlmHandlers, "loadLibrary">;
|
|
47
|
-
/**
|
|
48
|
-
|
|
48
|
+
/**
|
|
49
|
+
* Reset the loaded-prefix cache (call when the sandbox is
|
|
50
|
+
* discarded and will re-spawn).
|
|
51
|
+
*
|
|
52
|
+
* `keep` re-seeds the cache from the payload that will be replayed into the fresh worker.
|
|
53
|
+
* `loaded` is a CACHE of `libraryPrefixesIn(context)`, never independent state, so it may only
|
|
54
|
+
* be cleared by re-deriving it — clearing it outright would make the host re-clone and re-pack
|
|
55
|
+
* a library the recreated worker already holds.
|
|
56
|
+
*/
|
|
57
|
+
readonly reset: (keep?: readonly string[]) => void;
|
|
49
58
|
/** Prefixes loaded in this sandbox lifetime (for tests). */
|
|
50
59
|
readonly loadedPrefixes: () => ReadonlySet<string>;
|
|
51
60
|
}
|
|
52
61
|
|
|
62
|
+
/**
|
|
63
|
+
* JS runtime kind → the Python type name worker.py reports, so both sides emit exactly one
|
|
64
|
+
* message for the same refusal. Covers every shape a context payload can take after JSON
|
|
65
|
+
* transport; anything else is a plain object, which `json.load` materializes as a dict.
|
|
66
|
+
*/
|
|
67
|
+
const PY_TYPE_NAME: Readonly<Record<string, string>> = Object.freeze({
|
|
68
|
+
string: "str", boolean: "bool", number: "int", bigint: "int", undefined: "None",
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
function pythonKindOf(value: unknown): string {
|
|
72
|
+
if (value === null) return "None"; // matches worker.py's `if ctx is not None else "None"`
|
|
73
|
+
return PY_TYPE_NAME[typeof value] ?? "dict";
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Refusal messages shared with worker.py `_append_library`. The worker is the backstop; the host
|
|
78
|
+
* pre-flights the same two conditions so it never commits a prefix for an append that
|
|
79
|
+
* will be rejected. Keep the wording identical — a comment in worker.py points back here.
|
|
80
|
+
*/
|
|
81
|
+
const LIST_CONTEXT_REQUIRED = (kind: string): string =>
|
|
82
|
+
`load_library requires list context (file bundle); got ${kind}`;
|
|
83
|
+
const NO_FILES_PRODUCED = "load_library produced no files";
|
|
84
|
+
|
|
53
85
|
export function buildLibraryHandler(opts: LibraryBridgeOpts): LibraryHandlerBundle {
|
|
54
|
-
let nextIndex = opts.startIndex;
|
|
55
86
|
/** Prefixes already loaded in this sandbox — mirrors the worker's context state. */
|
|
56
87
|
const loaded = new Set<string>(opts.loadedPrefixes ?? []);
|
|
57
88
|
return {
|
|
58
|
-
reset: () => {
|
|
59
|
-
|
|
89
|
+
reset: (keep) => {
|
|
90
|
+
const seed = keep ?? opts.loadedPrefixes ?? [];
|
|
60
91
|
loaded.clear();
|
|
92
|
+
for (const prefix of seed) loaded.add(prefix);
|
|
61
93
|
},
|
|
62
94
|
loadedPrefixes: () => loaded,
|
|
63
95
|
handlers: {
|
|
@@ -74,6 +106,13 @@ export function buildLibraryHandler(opts: LibraryBridgeOpts): LibraryHandlerBund
|
|
|
74
106
|
depth,
|
|
75
107
|
});
|
|
76
108
|
try {
|
|
109
|
+
// Pre-flight the worker's own refusal: a non-list context cannot be appended to, and
|
|
110
|
+
// committing a prefix for it would make the NEXT load lie with already_loaded.
|
|
111
|
+
const current = opts.getContext?.();
|
|
112
|
+
if (current !== undefined && !Array.isArray(current)) {
|
|
113
|
+
throw new Error(LIST_CONTEXT_REQUIRED(pythonKindOf(current)));
|
|
114
|
+
}
|
|
115
|
+
|
|
77
116
|
// Cheap pre-check BEFORE cloning/packing: same namespace ⇒ nothing to do.
|
|
78
117
|
const { sourceId: preId, pathPrefix: prefix } = libraryNamespace(source, cwd);
|
|
79
118
|
if (loaded.has(prefix)) {
|
|
@@ -84,10 +123,8 @@ export function buildLibraryHandler(opts: LibraryBridgeOpts): LibraryHandlerBund
|
|
|
84
123
|
resultPreview: `already loaded (${prefix}*)`,
|
|
85
124
|
});
|
|
86
125
|
}
|
|
87
|
-
// No index consumed, no sidecar written — resume stays consistent.
|
|
88
126
|
return {
|
|
89
127
|
payload: Object.freeze([]),
|
|
90
|
-
index: -1,
|
|
91
128
|
files: 0,
|
|
92
129
|
chars: 0,
|
|
93
130
|
sourceId: preId,
|
|
@@ -99,6 +136,8 @@ export function buildLibraryHandler(opts: LibraryBridgeOpts): LibraryHandlerBund
|
|
|
99
136
|
const resolved = await resolveLibrarySource(source, cwd, opts.signal);
|
|
100
137
|
if (!resolved.ok) throw new Error(resolved.error);
|
|
101
138
|
const { payload, files, chars, sourceId, pathPrefix } = resolved.value;
|
|
139
|
+
// The worker's other refusal, pre-flighted for the same reason.
|
|
140
|
+
if (payload.length === 0) throw new Error(NO_FILES_PRODUCED);
|
|
102
141
|
|
|
103
142
|
// Race: another concurrent load of the same prefix finished while we packed.
|
|
104
143
|
if (loaded.has(pathPrefix)) {
|
|
@@ -111,7 +150,6 @@ export function buildLibraryHandler(opts: LibraryBridgeOpts): LibraryHandlerBund
|
|
|
111
150
|
}
|
|
112
151
|
return {
|
|
113
152
|
payload: Object.freeze([]),
|
|
114
|
-
index: -1,
|
|
115
153
|
files: 0,
|
|
116
154
|
chars: 0,
|
|
117
155
|
sourceId,
|
|
@@ -120,12 +158,10 @@ export function buildLibraryHandler(opts: LibraryBridgeOpts): LibraryHandlerBund
|
|
|
120
158
|
};
|
|
121
159
|
}
|
|
122
160
|
|
|
123
|
-
//
|
|
124
|
-
const index = nextIndex;
|
|
161
|
+
// Mark loaded only after the host has grown its own copy of the context.
|
|
125
162
|
if (opts.onLoaded) {
|
|
126
|
-
await opts.onLoaded(
|
|
163
|
+
await opts.onLoaded(payload);
|
|
127
164
|
}
|
|
128
|
-
nextIndex = index + 1;
|
|
129
165
|
loaded.add(pathPrefix);
|
|
130
166
|
|
|
131
167
|
if (id) {
|
|
@@ -138,7 +174,6 @@ export function buildLibraryHandler(opts: LibraryBridgeOpts): LibraryHandlerBund
|
|
|
138
174
|
}
|
|
139
175
|
return {
|
|
140
176
|
payload,
|
|
141
|
-
index,
|
|
142
177
|
files,
|
|
143
178
|
chars,
|
|
144
179
|
sourceId,
|