@hicaru/pi-rlm 0.3.1 → 0.3.3
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 +131 -154
- package/README.ru.md +5 -5
- package/README.zh-CN.md +5 -5
- package/package.json +3 -2
- package/src/bridge/handlers/await.ts +148 -0
- package/src/bridge/handlers/completion.ts +72 -0
- package/src/bridge/handlers/emitting.ts +104 -0
- package/src/bridge/handlers/finish.ts +45 -0
- package/src/bridge/handlers/index.ts +48 -0
- package/src/bridge/handlers/llm-query.ts +130 -0
- package/src/bridge/handlers/rlm-query.ts +227 -0
- package/src/bridge/handlers/task-registry.ts +202 -0
- package/src/bridge/handlers/types.ts +136 -0
- package/src/commands/rlm-config.ts +40 -12
- package/src/config/settings.ts +13 -3
- package/src/context/listing.ts +2 -2
- package/src/context/refresh.ts +141 -0
- package/src/core/engine.ts +16 -18
- package/src/core/types.ts +1 -3
- package/src/index.ts +59 -54
- package/src/mode/native-guards.ts +4 -4
- package/src/mode/rlm-mode.ts +6 -1
- package/src/mode/subagent.ts +1 -1
- package/src/prompts/glossary.ts +71 -74
- package/src/prompts/native.ts +127 -85
- package/src/prompts/system.ts +29 -15
- package/src/sandbox/interrupts.ts +258 -68
- package/src/sandbox/protocol.ts +53 -30
- package/src/sandbox/py/__pycache__/guards.cpython-314.pyc +0 -0
- package/src/sandbox/py/__pycache__/hostio.cpython-314.pyc +0 -0
- package/src/sandbox/py/__pycache__/retrieval.cpython-314.pyc +0 -0
- package/src/sandbox/py/__pycache__/tasks.cpython-314.pyc +0 -0
- package/src/sandbox/py/guards.py +8 -5
- package/src/sandbox/py/retrieval.py +17 -8
- package/src/sandbox/py/tasks.py +1 -1
- package/src/sandbox/py/worker.py +106 -79
- package/src/sandbox/sandbox-manager.ts +26 -1
- package/src/sandbox/sandbox.ts +1 -1
- package/src/tool/background-tasks.ts +1 -1
- package/src/tool/repl-result.ts +2 -2
- package/src/tool/repl-tool.ts +13 -14
- package/src/ui/config-panel.ts +1 -1
- package/src/ui/intro.ts +1 -4
- package/src/ui/model-picker.ts +32 -2
- package/src/util/concurrency.ts +1 -1
- package/src/bridge/subcall-handlers.ts +0 -382
package/README.md
CHANGED
|
@@ -1,73 +1,58 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/openzebra/rlm.pi/main/assets/plugin-cover.png" width="100%" alt="pi-rlm — Recursive Language Model plugin for Pi">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
<a href="https://www.npmjs.com/package/@hicaru/pi-rlm"><img src="https://img.shields.io/npm/v/@hicaru/pi-rlm?color=cb3837&logo=npm" alt="npm version"></a>
|
|
7
|
+
<a href="https://github.com/openzebra/rlm.pi/blob/master/pi-plugin/rlm/LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue" alt="MIT license"></a>
|
|
8
|
+
<a href="https://github.com/earendil-works/pi"><img src="https://img.shields.io/badge/for-Pi-7c3aed" alt="Built for Pi"></a>
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<p align="center">
|
|
12
|
+
<a href="https://arxiv.org/abs/2512.24601">📄 RLM Paper</a> ·
|
|
13
|
+
<a href="https://github.com/openzebra/rlm.pi">💻 Source</a> ·
|
|
14
|
+
<a href="https://www.npmjs.com/package/@hicaru/pi-rlm">📦 npm</a>
|
|
15
|
+
</p>
|
|
2
16
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
**Recursive Language Models (RLMs)**, implemented natively as a Pi extension —
|
|
6
|
-
FULLY LOCAL.
|
|
7
|
-
|
|
8
|
-
</div>
|
|
9
|
-
|
|
10
|
-
## Install
|
|
11
|
-
|
|
12
|
-
```bash
|
|
13
|
-
pi install npm:@hicaru/pi-rlm
|
|
14
|
-
```
|
|
15
|
-
|
|
16
|
-
To remove it later:
|
|
17
|
-
|
|
18
|
-
```bash
|
|
19
|
-
pi uninstall npm:@hicaru/pi-rlm
|
|
20
|
-
```
|
|
17
|
+
---
|
|
21
18
|
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
**The ONLY Recursive Language Model plugin for Pi.** No new agent to learn, no
|
|
20
|
+
separate CLI, no YAML workflows — just `/rlm` and your existing Pi session becomes a
|
|
21
|
+
recursive orchestration engine that saves **99% tokens** by delegating work to cheap
|
|
22
|
+
worker models.
|
|
24
23
|
|
|
25
|
-
|
|
24
|
+
> **One install. One toggle. Infinite context.**
|
|
26
25
|
|
|
27
|
-
|
|
26
|
+
## Why pi-rlm?
|
|
28
27
|
|
|
29
|
-
|
|
28
|
+
| Advantage | What it means |
|
|
29
|
+
|-----------|---------------|
|
|
30
|
+
| 🔌 **Plugin, not a new agent** | Stays inside Pi. You keep your keybindings, your theme, your tools, your muscle memory. |
|
|
31
|
+
| 📄 **Reads ANY document** | `.pdf` `.docx` `.pptx` `.xlsx` `.epub` `.rtf` `.odt` `.csv` `.html` `.xml` — drop them in, they become Markdown in `context`. |
|
|
32
|
+
| 🪶 **Unix-style — tiny & composable** | Does ONE thing (RLM orchestration). Pair it with any other Pi plugin. No lock-in. |
|
|
33
|
+
| 🧠 **Smartest model orchestrates, cheapest model researches** | Root uses your best model; workers auto-pick the cheapest. Recursive children inherit the full `context` for free. |
|
|
34
|
+
| ⏳ **Long-running with goals** | Toggle `/rlm` on, set a goal, let it loop. Runs survive across chat turns — go make coffee. |
|
|
35
|
+
| 🔒 **100% local, 100% private** | No servers. Your API keys never leave your machine. One `python3` subprocess — that's it. |
|
|
30
36
|
|
|
31
|
-
|
|
37
|
+
## See it in action
|
|
32
38
|
|
|
33
39
|
<div align="center">
|
|
34
40
|
|
|
35
|
-
<
|
|
36
|
-
**English** · <a href="README.zh-CN.md">中文</a> · <a href="README.ru.md">Русский</a>
|
|
37
|
-
</sub>
|
|
41
|
+
<video src="https://github.com/openzebra/rlm.pi/raw/refs/heads/master/animation/rlm_pi_explainer.mp4" controls width="854" poster="https://raw.githubusercontent.com/openzebra/rlm.pi/main/assets/hero.png"></video>
|
|
38
42
|
|
|
39
43
|
</div>
|
|
40
44
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
A **Recursive Language Model (RLM)** is a task-agnostic inference paradigm where a
|
|
44
|
-
root language model orchestrates over near-infinite context by *programmatically*
|
|
45
|
-
examining, decomposing, and **recursively calling itself** over its input. RLMs
|
|
46
|
-
replace the canonical `llm.completion(prompt, model)` call with an
|
|
47
|
-
`rlm.completion(prompt, model)` call: the prompt/context is offloaded as a variable
|
|
48
|
-
in a REPL environment that the model interacts with, and the model can launch
|
|
49
|
-
sub-LLM and sub-RLM calls as ordinary functions in code.
|
|
50
|
-
|
|
51
|
-
This is a bet on a [CodeAct](https://arxiv.org/abs/2402.01030)-style harness — every
|
|
52
|
-
language model gets access to a code environment, sub-(R)LM calls are functions, and
|
|
53
|
-
context/prompts are objects in code — moving away from the JSON tool-calling standard.
|
|
54
|
-
A system built this way is *itself* a language model that relies on recursive
|
|
55
|
-
sub-LLM calls, hence the name.
|
|
45
|
+
## Install (30 seconds)
|
|
56
46
|
|
|
57
|
-
|
|
47
|
+
```bash
|
|
48
|
+
pi install npm:@hicaru/pi-rlm
|
|
49
|
+
```
|
|
58
50
|
|
|
59
|
-
|
|
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). A child inherits
|
|
62
|
-
its parent's `context` — every file loaded so far, including sources added with `add_context()` —
|
|
63
|
-
so it runs the same retrieval primitives over the same paths. Inheritance costs no extra tokens:
|
|
64
|
-
the content lives in the sandbox, and only a size line reaches the model.
|
|
65
|
-
- Everything runs **in-process** — the only external process is one local `python3` worker.
|
|
51
|
+
Run `/reload` in Pi. Done. `/rlm`, `/rlm-config`, `/rlm-stop` appear under **[Extensions]**.
|
|
66
52
|
|
|
67
|
-
|
|
68
|
-
> It is **not** the Python library.
|
|
53
|
+
Toggle with `Ctrl+Shift+R` or `/rlm` — plain prompts now route through the RLM engine.
|
|
69
54
|
|
|
70
|
-
##
|
|
55
|
+
## What you get
|
|
71
56
|
|
|
72
57
|
```
|
|
73
58
|
┌─────────────────────────┐
|
|
@@ -76,8 +61,8 @@ sub-LLM calls, hence the name.
|
|
|
76
61
|
│ /rlm
|
|
77
62
|
▼
|
|
78
63
|
┌─────────────────────────┐ spawns ┌────────────────────┐
|
|
79
|
-
│ Smart model (root) │ ────────►
|
|
80
|
-
│ drives a Python REPL │ ◄────────
|
|
64
|
+
│ Smart model (root) │ ────────► │ Worker models │
|
|
65
|
+
│ drives a Python REPL │ ◄──────── │ (cheap, fast) │
|
|
81
66
|
└────────────┬────────────┘ results └────────────────────┘
|
|
82
67
|
│ recursion (depth-capped)
|
|
83
68
|
└────► child RLMs ────► (same loop)
|
|
@@ -85,117 +70,109 @@ sub-LLM calls, hence the name.
|
|
|
85
70
|
All local · one python3 process · no servers
|
|
86
71
|
```
|
|
87
72
|
|
|
88
|
-
-
|
|
89
|
-
-
|
|
90
|
-
-
|
|
91
|
-
-
|
|
73
|
+
- **Smart model** thinks and writes Python in a persistent REPL.
|
|
74
|
+
- **Worker models** do the heavy lifting — read, summarize, classify, search.
|
|
75
|
+
- **Child RLMs** recurse into hard sub-problems, inheriting the full `context` for free.
|
|
76
|
+
- **Live tree** shows every sub-call with model, cost, tokens, and duration.
|
|
77
|
+
|
|
78
|
+
## Document format support
|
|
79
|
+
|
|
80
|
+
Drop ANY of these into `add_context()` — they auto-convert to Markdown and land in `context`:
|
|
81
|
+
|
|
82
|
+
| Category | Formats |
|
|
83
|
+
|----------|---------|
|
|
84
|
+
| **Word** | `.docx` |
|
|
85
|
+
| **PDF** | `.pdf` |
|
|
86
|
+
| **PowerPoint** | `.pptx` |
|
|
87
|
+
| **Excel** | `.xlsx` |
|
|
88
|
+
| **EPUB** | `.epub` |
|
|
89
|
+
| **Rich Text** | `.rtf` |
|
|
90
|
+
| **OpenDocument** | `.odt` |
|
|
91
|
+
| **CSV / TSV** | `.csv` `.tsv` |
|
|
92
|
+
| **HTML / XML** | `.html` `.htm` `.xml` `.rss` `.atom` |
|
|
93
|
+
| **+ Pandoc fallback** | `.doc` `.ppt` `.xls` `.pptm` `.xlsm` `.xlsb` `.ppsm` `.docm` `.odp` `.ods` |
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
add_context("report.pdf") # → Markdown in context
|
|
97
|
+
add_context("data.xlsx") # → Markdown in context
|
|
98
|
+
add_context("../some-lib") # → entire directory packed
|
|
99
|
+
add_context("https://github.com/x/y.git") # → shallow clone + pack
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## RECURSION — the core idea
|
|
103
|
+
|
|
104
|
+
A **Recursive Language Model (RLM)** replaces `llm.completion(prompt)` with
|
|
105
|
+
`rlm.completion(prompt)`. The prompt becomes a variable in a REPL. The model can
|
|
106
|
+
launch sub-LLM and sub-RLM calls as ordinary Python functions — decomposing,
|
|
107
|
+
delegating, and synthesizing across a tree of models, not a single context window.
|
|
108
|
+
|
|
109
|
+
**This is the only plugin that brings true RLM recursion to Pi.** Prime Agent and
|
|
110
|
+
the reference Python library are separate agents you must switch to. pi-rlm lives
|
|
111
|
+
inside Pi — same session, same tools, same everything.
|
|
92
112
|
|
|
93
113
|
## Commands
|
|
94
114
|
|
|
95
|
-
| Command | Shortcut |
|
|
96
|
-
|
|
97
|
-
| `/rlm` | `Ctrl+Shift+R` | Toggle
|
|
98
|
-
| `/rlm-stop` | | Abort
|
|
99
|
-
| `/rlm-config` | | Pick
|
|
115
|
+
| Command | Shortcut | What it does |
|
|
116
|
+
|---------|----------|--------------|
|
|
117
|
+
| `/rlm` | `Ctrl+Shift+R` | Toggle RLM mode on/off |
|
|
118
|
+
| `/rlm-stop` | | Abort current run |
|
|
119
|
+
| `/rlm-config` | | Pick models, tune limits |
|
|
100
120
|
|
|
101
|
-
|
|
102
|
-
recursive child with status, model, cost, tokens, and duration. The final answer is posted
|
|
103
|
-
to the chat as markdown. File changes use Pi's native `edit` / `write` tools (with their
|
|
104
|
-
built-in diff preview).
|
|
121
|
+
## Settings (`/rlm-config`)
|
|
105
122
|
|
|
106
|
-
|
|
123
|
+
| Setting | Default | Why you'd change it |
|
|
124
|
+
|---------|---------|---------------------|
|
|
125
|
+
| Smart model | Pi's active | Use your best model as orchestrator |
|
|
126
|
+
| Worker model | cheapest available | Free/cheap model for leaf `llm_query` calls |
|
|
127
|
+
| Max recursion depth | `4` | Deeper trees for harder problems |
|
|
128
|
+
| Max iterations | `30` | Longer runs for complex tasks |
|
|
129
|
+
| REPL timeout | `120`s | Bump for slow computations |
|
|
130
|
+
| Max concurrent subs | `16` | More parallelism (costs RAM) |
|
|
107
131
|
|
|
108
|
-
|
|
132
|
+
## Prompt Architecture
|
|
109
133
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
| `context` | `list[dict]` | Loaded files as `[{"path","content","tokens"}, …]` — starts empty; cwd seeds on first `repl()` |
|
|
113
|
-
| `llm_query` | `(prompt, model=None) -> str` | One-shot sub-LLM call (worker model) |
|
|
114
|
-
| `llm_query_batched` | `(prompts, model=None) -> list[str]` | Concurrent sub-LLM calls (pool-bounded) |
|
|
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, 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 |
|
|
118
|
-
| `add_context` | `(source) -> dict \| str` | Append a dir, file, document, or git URL into `context` under `ctx/<id>/` |
|
|
119
|
-
| `SHOW_VARS` | `() -> str` | List currently defined variables & their types |
|
|
120
|
-
| `answer` | `dict` | Set `answer["content"]=...; answer["ready"]=True` to finalize |
|
|
134
|
+
The system prompt follows a **contract / routing / examples / rules** pattern
|
|
135
|
+
(api_v5), modeled on the best-performing arm from the RLM paper bake-off:
|
|
121
136
|
|
|
122
|
-
|
|
137
|
+
- `<contract>` — every heavy call returns a `Task`, only `await_task` returns content
|
|
138
|
+
- `<routing>` — decision tree: which tool for which job
|
|
139
|
+
- `<examples>` — concrete E1–E7 patterns with anti-patterns
|
|
140
|
+
- `<rules>` — locate-then-delegate, memoize, cap workers, author edits yourself
|
|
123
141
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
tree, document, or git URL**, call `add_context(source)`:
|
|
142
|
+
**Key insight:** children see `Recursion depth: N` and calibrate ambition —
|
|
143
|
+
delegating only when their task genuinely decomposes further.
|
|
127
144
|
|
|
128
|
-
|
|
129
|
-
info = add_context("../some-lib") # local directory → packed + appended
|
|
130
|
-
info = add_context("docs/api.md") # single file → one entry in context
|
|
131
|
-
info = add_context("report.pdf") # document → Markdown, then appended
|
|
132
|
-
info = add_context("https://github.com/x/y.git") # shallow clone, then pack + append
|
|
133
|
-
# Files land in the SAME `context` list under ctx/<source_id>/…
|
|
134
|
-
# info == {"source_id", "path_prefix", "files", "chars", "context_len", "already_loaded", "converted", "skipped", …}
|
|
135
|
-
lib = [f for f in context if f["path"].startswith(info["path_prefix"])]
|
|
136
|
-
```
|
|
145
|
+
## Benchmarks
|
|
137
146
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
default on) and **Auto-seed cwd** (`autoSeedCwd`, default on). A source loaded at any point is
|
|
141
|
-
inherited by every child spawned afterwards.
|
|
147
|
+
Tested against `rlm-lab` prompt bake-off and full dual-mode RLM runtime benchmarks
|
|
148
|
+
on `poolside/laguna-xs-2.1:free` (a free ~32B model):
|
|
142
149
|
|
|
143
|
-
|
|
150
|
+
| Benchmark | Mode | Result |
|
|
151
|
+
|-----------|------|--------|
|
|
152
|
+
| Main orchestrator (7 scenarios) | prompt bake-off | **0.958** mean score (v3 fewshot arm) |
|
|
153
|
+
| RLM worker (4 scenarios) | prompt bake-off | **0.94** mean score (v2 contract arm) |
|
|
154
|
+
| Needle-in-haystack (3 needles) | classic RLM | **recall 1.0** |
|
|
155
|
+
| CodeQA timeout | classic RLM | **correct** (~3.7k tokens) |
|
|
156
|
+
| Coding (retry fix) | orchestrator | **correct** (file edited) |
|
|
157
|
+
| Live smoke needle | classic RLM | **hit** (~5k tokens) |
|
|
144
158
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
| Smart model | Pi's active model | the root orchestrator |
|
|
148
|
-
| Worker model | cheapest available | answers `llm_query` |
|
|
149
|
-
| Max recursion depth | `4` | `rlm_query` past this degrades to plain `llm_query` |
|
|
150
|
-
| Max iterations | `30` | root REPL turns before RLM asks for a final answer |
|
|
151
|
-
| REPL block timeout (s) | `120` | wall-clock limit for one Python REPL block (SIGALRM) |
|
|
152
|
-
| Max concurrent sub-calls | `16` | concurrency pool size for `*_batched` |
|
|
153
|
-
| Max concurrent children | `6` | concurrent `rlm_query` child engines per depth |
|
|
154
|
-
| Wall-clock ceiling (min) | none | total runtime cap for the whole recursive tree |
|
|
155
|
-
| Token ceiling | none | total input+output token cap for the whole recursive tree |
|
|
156
|
-
| Max consecutive errors | `5` | stop after N consecutive failing turns (none = off) |
|
|
157
|
-
| Orchestrator addendum | on | divide-and-conquer guidance in the root system prompt |
|
|
158
|
-
| Trajectory compaction | on (0.65) | summarize old turns when history nears the context window |
|
|
159
|
-
| Root model output cap (tok) | `16384` | max output tokens per root-model turn |
|
|
160
|
-
| Sandbox init timeout | `30000` ms | how long to wait for the Python worker to start |
|
|
161
|
-
| Context loader | on | expose `add_context()` for external dirs/files/documents/git repos |
|
|
162
|
-
| Auto-seed cwd | on | seed the working directory into `context` on the first `repl()` |
|
|
163
|
-
|
|
164
|
-
> **Concurrency note:** each `rlm_query` child spawns its own `python3` worker (~50–150 ms
|
|
165
|
-
> cold start). Children are bounded separately (`maxConcurrentChildren`, default 6) because
|
|
166
|
-
> each holds a full Python process and its own copy of the inherited context. Error and
|
|
167
|
-
> wall-clock caps (above) still bound a runaway tree.
|
|
168
|
-
|
|
169
|
-
## Subagents and environment
|
|
170
|
-
|
|
171
|
-
RLM never confiscates native file tools (`read` / `grep` / bash readers) unless `repl` is in
|
|
172
|
-
the **active** tool set — the paper's trade is all-or-nothing. Process-boundary subagents that
|
|
173
|
-
spawn pi with a `--tools` allowlist without `repl` therefore keep ordinary file access.
|
|
174
|
-
|
|
175
|
-
Optional env conventions (for packages that want an explicit full bypass):
|
|
176
|
-
|
|
177
|
-
| Env | Meaning |
|
|
178
|
-
|---|---|
|
|
179
|
-
| `PI_SUBAGENT_CHILD=1` | Full RLM bypass in this process (no tools / hooks / flags). |
|
|
180
|
-
| `PI_RLM_FORCE_IN_SUBAGENT=1` | Experimental: opt a child back into RLM. **Consumed on activate** (not inherited after). Refused when `PI_RLM_DEPTH >= maxDepth`. |
|
|
181
|
-
| `PI_RLM_DEPTH` | Cross-process depth counter (default `0`). Bumped when force-in activates. |
|
|
182
|
-
|
|
183
|
-
In-process recursion (`rlm_query`) still uses `maxDepth` from `/rlm-config` and is unrelated to
|
|
184
|
-
these env vars. Set `RLM_TRACE_FILE` to a path for JSONL traces of bypass / force / block-skip
|
|
185
|
-
decisions.
|
|
159
|
+
> On a *free* model. Frontier models do even better. See `rlm_test/RESULTS_AGENT.md`
|
|
160
|
+
> and `rlm_test/RESULTS.md` for full methodology.
|
|
186
161
|
|
|
187
162
|
## Security
|
|
188
163
|
|
|
189
|
-
- **Key isolation
|
|
190
|
-
|
|
191
|
-
- **
|
|
192
|
-
|
|
193
|
-
- **
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
164
|
+
- **Key isolation** — provider keys live in TypeScript only; sandbox receives prompts, returns text.
|
|
165
|
+
- **Environment sanitization** — sensitive env vars stripped before worker spawns.
|
|
166
|
+
- **Restricted builtins** — no `eval`/`exec`/`compile`/`input` in the sandbox.
|
|
167
|
+
- **Per-block timeout** — SIGALRM + parent watchdog (SIGKILL on hang).
|
|
168
|
+
- **Trust** — project-local install requires Pi project trust.
|
|
169
|
+
|
|
170
|
+
## Uninstall
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
pi uninstall npm:@hicaru/pi-rlm
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## License
|
|
177
|
+
|
|
178
|
+
MIT — see [LICENSE](./LICENSE).
|
package/README.ru.md
CHANGED
|
@@ -104,11 +104,11 @@ rm -rf ~/.pi/agent/extensions/rlm
|
|
|
104
104
|
| Функция | Сигнатура | Описание |
|
|
105
105
|
|---|---|---|
|
|
106
106
|
| `context` | `list[dict]` | Репозиторий, упакованный как `[{"path","content","tokens"}, ...]` — вся кодовая база |
|
|
107
|
-
| `llm_query` | `(prompt
|
|
108
|
-
| `llm_query_batched` | `(prompts
|
|
109
|
-
| `llm_query_chunked` | `(text, prompt
|
|
110
|
-
| `rlm_query` | `(prompt,
|
|
111
|
-
| `rlm_query_batched` | `(prompts,
|
|
107
|
+
| `llm_query` | `(prompt) -> str` | Одноразовый вызов sub-LLM (настроенная RLM LLM) |
|
|
108
|
+
| `llm_query_batched` | `(prompts) -> list[str]` | Параллельные вызовы sub-LLM (с ограничением пула) |
|
|
109
|
+
| `llm_query_chunked` | `(text, prompt) -> list[str]` | Дробит большой текст на части по лимиту и обрабатывает через sub-LLM |
|
|
110
|
+
| `rlm_query` | `(prompt, paths=None) -> str` | Рекурсивный дочерний RLM со своей песочницей (с ограничением глубины). Наследует ваш `context`; `paths` сужает его по префиксу |
|
|
111
|
+
| `rlm_query_batched` | `(prompts, paths=None) -> list[str]` | Параллельные рекурсивные дочерние RLM с общим срезом `paths` |
|
|
112
112
|
| `ask_user_question` | `(questions) -> list[dict]` | Задать пользователю структурированные вопросы (только на глубине 0) |
|
|
113
113
|
| `SHOW_VARS` | `() -> str` | Список текущих переменных и их типов |
|
|
114
114
|
| `answer` | `dict` | Установите `answer["content"]=...; answer["ready"]=True` для завершения |
|
package/README.zh-CN.md
CHANGED
|
@@ -110,11 +110,11 @@ rm -rf ~/.pi/agent/extensions/rlm
|
|
|
110
110
|
| 函数 | 签名 | 描述 |
|
|
111
111
|
|---|---|---|
|
|
112
112
|
| `context` | `list[dict]` | 打包为 `[{"path","content","tokens"}, ...]` 的仓库 —— 完整的代码库 |
|
|
113
|
-
| `llm_query` | `(prompt
|
|
114
|
-
| `llm_query_batched` | `(prompts
|
|
115
|
-
| `llm_query_chunked` | `(text, prompt
|
|
116
|
-
| `rlm_query` | `(prompt,
|
|
117
|
-
| `rlm_query_batched` | `(prompts,
|
|
113
|
+
| `llm_query` | `(prompt) -> str` | 单次子 LLM 调用(配置的 RLM LLM) |
|
|
114
|
+
| `llm_query_batched` | `(prompts) -> list[str]` | 并发子 LLM 调用 (池上限) |
|
|
115
|
+
| `llm_query_chunked` | `(text, prompt) -> list[str]` | 将大文本拆分为不超过上限的块并通过子 LLM 处理 |
|
|
116
|
+
| `rlm_query` | `(prompt, paths=None) -> str` | 具有自有沙箱的递归子 RLM (设有深度限制)。继承父级的 `context`;`paths` 按前缀缩小范围 |
|
|
117
|
+
| `rlm_query_batched` | `(prompts, paths=None) -> list[str]` | 并发递归子 RLM,共享同一个 `paths` 切片 |
|
|
118
118
|
| `ask_user_question` | `(questions) -> list[dict]` | 向用户提出结构化问题 (仅限深度 0) |
|
|
119
119
|
| `SHOW_VARS` | `() -> str` | 列出当前定义的变量及其类型 |
|
|
120
120
|
| `answer` | `dict` | 设置 `answer["content"]=...; answer["ready"]=True` 以结束 |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hicaru/pi-rlm",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"author": "hicaru",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -36,7 +36,8 @@
|
|
|
36
36
|
"pi": {
|
|
37
37
|
"extensions": [
|
|
38
38
|
"./src/index.ts"
|
|
39
|
-
]
|
|
39
|
+
],
|
|
40
|
+
"image": "https://raw.githubusercontent.com/openzebra/rlm.pi/main/assets/plugin-cover.png"
|
|
40
41
|
},
|
|
41
42
|
"publishConfig": {
|
|
42
43
|
"access": "public"
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* await handler — collect results from background tasks.
|
|
3
|
+
*
|
|
4
|
+
* Supports single-task await (await_task(task_id="...")) and multi-task await
|
|
5
|
+
* (await_task(task_ids=[...])). Returns AwaitResult with the collected result(s)
|
|
6
|
+
* in input order for batches.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { errorMessage, formatError } from "../../util/errors.ts";
|
|
10
|
+
import type { AwaitResult, SubcallHandlerDeps, TaskEntry } from "./types.ts";
|
|
11
|
+
import type { SubcallOpts } from "../../sandbox/interrupts.ts";
|
|
12
|
+
import type { AwaitDeps } from "./task-registry.ts";
|
|
13
|
+
|
|
14
|
+
export function createAwaitHandler(_deps: SubcallHandlerDeps, ad: AwaitDeps) {
|
|
15
|
+
return async (
|
|
16
|
+
taskId: string | undefined,
|
|
17
|
+
taskIds: readonly string[] | undefined,
|
|
18
|
+
timeoutS: number | undefined,
|
|
19
|
+
_depth: number,
|
|
20
|
+
_opts: SubcallOpts,
|
|
21
|
+
): Promise<AwaitResult> => {
|
|
22
|
+
const timeoutMs = timeoutS !== undefined ? timeoutS * 1000 : undefined;
|
|
23
|
+
|
|
24
|
+
// Single task
|
|
25
|
+
if (taskId !== undefined && (taskIds === undefined || taskIds.length === 0)) {
|
|
26
|
+
const entry = ad.get(taskId);
|
|
27
|
+
if (entry === undefined) {
|
|
28
|
+
return {
|
|
29
|
+
ok: false,
|
|
30
|
+
task_id: taskId,
|
|
31
|
+
kind: "unknown",
|
|
32
|
+
status: "error",
|
|
33
|
+
error: `Task ${taskId} not found — was it already awaited or never spawned?`,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (entry.status === "pending") {
|
|
38
|
+
try {
|
|
39
|
+
const resolved = await ad.wait(taskId, timeoutMs);
|
|
40
|
+
return toAwaitResult(resolved);
|
|
41
|
+
} catch (err: unknown) {
|
|
42
|
+
return {
|
|
43
|
+
ok: false,
|
|
44
|
+
task_id: taskId,
|
|
45
|
+
kind: entry.kind,
|
|
46
|
+
status: "error",
|
|
47
|
+
error: formatError(errorMessage(err)),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return toAwaitResult(entry);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Multiple tasks
|
|
56
|
+
const ids = taskIds ?? (taskId !== undefined ? [taskId] : []);
|
|
57
|
+
if (ids.length === 0) {
|
|
58
|
+
return {
|
|
59
|
+
ok: false,
|
|
60
|
+
task_id: "",
|
|
61
|
+
kind: "unknown",
|
|
62
|
+
status: "error",
|
|
63
|
+
error: "No task_id or task_ids provided to await",
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const entries = ids
|
|
68
|
+
.map((id) => ad.get(id))
|
|
69
|
+
.filter((e): e is TaskEntry => e !== undefined);
|
|
70
|
+
|
|
71
|
+
if (entries.length === 0) {
|
|
72
|
+
return {
|
|
73
|
+
ok: false,
|
|
74
|
+
task_id: ids[0] ?? "",
|
|
75
|
+
kind: "unknown",
|
|
76
|
+
status: "error",
|
|
77
|
+
error: "None of the requested task IDs were found",
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const resolved = await Promise.all(
|
|
82
|
+
entries.map(async (e) => {
|
|
83
|
+
if (e.status === "pending") {
|
|
84
|
+
try {
|
|
85
|
+
return await ad.wait(e.taskId, timeoutMs);
|
|
86
|
+
} catch {
|
|
87
|
+
return e;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return e;
|
|
91
|
+
}),
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
const awaited = resolved.map(toAwaitResult);
|
|
95
|
+
const allDone = awaited.every((a) => a.status === "done");
|
|
96
|
+
const hasResults = awaited.some((a) => a.results !== undefined);
|
|
97
|
+
const firstError = awaited.find((a) => a.error)?.error;
|
|
98
|
+
const first = awaited[0];
|
|
99
|
+
const kind = first?.kind ?? "unknown";
|
|
100
|
+
|
|
101
|
+
if (hasResults) {
|
|
102
|
+
const allResults: string[] = [];
|
|
103
|
+
for (const a of awaited) {
|
|
104
|
+
if (a.results !== undefined) {
|
|
105
|
+
for (const r of a.results) allResults.push(r);
|
|
106
|
+
} else if (a.result !== undefined) {
|
|
107
|
+
allResults.push(a.result);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
ok: allDone,
|
|
112
|
+
task_id: ids.join(","),
|
|
113
|
+
kind,
|
|
114
|
+
status: allDone ? "done" : "error",
|
|
115
|
+
results: Object.freeze(allResults),
|
|
116
|
+
error: firstError,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (awaited.length === 1 && first !== undefined) {
|
|
121
|
+
return first;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
ok: allDone,
|
|
126
|
+
task_id: ids.join(","),
|
|
127
|
+
kind,
|
|
128
|
+
status: allDone ? "done" : "error",
|
|
129
|
+
results: Object.freeze(awaited.map((a) => a.result ?? a.error ?? "")),
|
|
130
|
+
error: firstError,
|
|
131
|
+
};
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function toAwaitResult(entry: TaskEntry): AwaitResult {
|
|
136
|
+
const status = entry.status === "pending" ? "error" : entry.status;
|
|
137
|
+
return {
|
|
138
|
+
ok: entry.status === "done",
|
|
139
|
+
task_id: entry.taskId,
|
|
140
|
+
kind: entry.kind,
|
|
141
|
+
status,
|
|
142
|
+
result: entry.result,
|
|
143
|
+
results: entry.results,
|
|
144
|
+
error:
|
|
145
|
+
entry.error ??
|
|
146
|
+
(entry.status === "pending" ? "Task still pending" : undefined),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single LLM completion — the ONE place a sub-LLM call is made.
|
|
3
|
+
*
|
|
4
|
+
* Every leaf completion takes exactly ONE slot on `gates.leaf` here.
|
|
5
|
+
* Never wrap a whole batch in the leaf gate (deadlock); only complete1 acquires it.
|
|
6
|
+
*
|
|
7
|
+
* AGENTS.md DRY #1: complete1 exists once. Never inline another one.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { Api, Model, Usage } from "@earendil-works/pi-ai";
|
|
11
|
+
import type { ModelRegistry } from "@earendil-works/pi-coding-agent";
|
|
12
|
+
import { type ChatMsg, modelComplete } from "../model.ts";
|
|
13
|
+
import { checkResourceLimits } from "../../core/resource-limits.ts";
|
|
14
|
+
import { errorMessage, formatError } from "../../util/errors.ts";
|
|
15
|
+
import type { Semaphore } from "../../util/concurrency.ts";
|
|
16
|
+
import type { Invocation, SubcallConfig } from "./types.ts";
|
|
17
|
+
|
|
18
|
+
export interface Complete1Deps {
|
|
19
|
+
readonly leafGate: Semaphore;
|
|
20
|
+
readonly registry: ModelRegistry;
|
|
21
|
+
readonly getLlmModel: () => Model<Api>;
|
|
22
|
+
readonly getConfig: () => SubcallConfig;
|
|
23
|
+
readonly signal?: AbortSignal;
|
|
24
|
+
readonly onUsage?: (usage: Usage, role: "sub") => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Run one LLM completion inside the leaf gate.
|
|
29
|
+
* Never throws — returns formatError(...) strings on failure.
|
|
30
|
+
*/
|
|
31
|
+
export async function complete1(
|
|
32
|
+
inv: Invocation,
|
|
33
|
+
prompt: string,
|
|
34
|
+
track: (usage: Usage) => void,
|
|
35
|
+
deps: Complete1Deps,
|
|
36
|
+
): Promise<string> {
|
|
37
|
+
const config = deps.getConfig();
|
|
38
|
+
const limitError = checkResourceLimits({
|
|
39
|
+
timeoutMs: inv.limits.remainingTimeoutMs(),
|
|
40
|
+
});
|
|
41
|
+
if (limitError !== undefined) return limitError;
|
|
42
|
+
if (prompt.length > config.maxPromptChars) {
|
|
43
|
+
return formatError(
|
|
44
|
+
`sub-LLM prompt exceeded the size limit (${prompt.length.toLocaleString()} chars > ` +
|
|
45
|
+
`${config.maxPromptChars.toLocaleString()}). Shorten or chunk the prompt before calling llm_query.`,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
const messages: ChatMsg[] = [{ role: "user", content: prompt }];
|
|
50
|
+
const res = await deps.leafGate.run(() =>
|
|
51
|
+
modelComplete(messages, {
|
|
52
|
+
model: deps.getLlmModel(),
|
|
53
|
+
registry: deps.registry,
|
|
54
|
+
system: config.subSystemPrompt,
|
|
55
|
+
maxTokens: config.subSampling?.maxTokens,
|
|
56
|
+
temperature: config.subSampling?.temperature,
|
|
57
|
+
reasoning: config.subSampling?.reasoning,
|
|
58
|
+
signal: deps.signal,
|
|
59
|
+
}),
|
|
60
|
+
);
|
|
61
|
+
inv.limits.addUsage(res.usage);
|
|
62
|
+
deps.onUsage?.(res.usage, "sub");
|
|
63
|
+
track(res.usage);
|
|
64
|
+
return res.text;
|
|
65
|
+
} catch (err: unknown) {
|
|
66
|
+
const msg = errorMessage(err);
|
|
67
|
+
const hint = /credit|402|payment|quota|rate.limit/i.test(msg)
|
|
68
|
+
? " — try smaller batches or individual llm_query calls"
|
|
69
|
+
: "";
|
|
70
|
+
return formatError(`${msg}${hint}`);
|
|
71
|
+
}
|
|
72
|
+
}
|