@hicaru/pi-rlm 0.3.15 → 0.3.16
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 +100 -70
- package/README.ru.md +37 -0
- package/README.zh-CN.md +34 -0
- package/package.json +1 -1
- package/src/bridge/model.ts +15 -1
- package/src/config/defaults.ts +3 -0
- package/src/config/settings.ts +6 -2
- package/src/core/engine.ts +52 -11
- package/src/core/types.ts +5 -0
- package/src/prompts/user.ts +17 -0
- package/src/sandbox/py/__pycache__/retrieval.cpython-314.pyc +0 -0
- package/src/sandbox/py/retrieval.py +4 -1
- package/src/sandbox/sandbox.ts +14 -1
- package/src/ui/config-panel.ts +39 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
# rlm.pi PI plugin
|
|
2
|
+
|
|
3
|
+
> pi-rlm — Large contexts on cheap models: Recursive Language Model (RLM) for Pi
|
|
4
4
|
|
|
5
5
|
<p align="center">
|
|
6
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>
|
|
@@ -14,45 +14,78 @@
|
|
|
14
14
|
<a href="https://www.npmjs.com/package/@hicaru/pi-rlm">📦 npm</a>
|
|
15
15
|
</p>
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pi install npm:@hicaru/pi-rlm
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Run `/reload` in Pi — `/rlm`, `/rlm-config`, `/rlm-stop` appear under **[Extensions]**.
|
|
24
|
+
Toggle with `Ctrl+Shift+R` or `/rlm`.
|
|
25
|
+
|
|
26
|
+
To remove it later:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pi uninstall npm:@hicaru/pi-rlm
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
<p align="center">
|
|
33
|
+
<img src="https://github.com/openzebra/rlm.pi/blob/master/assets/hero.png?raw=true" width="100%" alt="rlm.pi — OOLONG benchmark results">
|
|
34
|
+
</p>
|
|
35
|
+
|
|
36
|
+
## What is pi-rlm?
|
|
18
37
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
38
|
+
A Pi plugin that turns your session into a **Recursive Language Model (RLM)**: instead of
|
|
39
|
+
stuffing a huge document into the prompt, the context lives in a Python REPL and your best
|
|
40
|
+
model orchestrates it — searching, decomposing, and delegating leaf reads to cheap worker
|
|
41
|
+
models, recursively. Same Pi session, same tools, same keys: `/rlm` and go. Reads
|
|
42
|
+
`.pdf` `.docx` `.xlsx` `.epub` and more, works with any OpenRouter model, 100% local.
|
|
23
43
|
|
|
24
|
-
|
|
44
|
+
## Benchmarks
|
|
25
45
|
|
|
26
|
-
|
|
46
|
+
<p align="center">
|
|
47
|
+
<img src="https://github.com/openzebra/rlm.pi/blob/master/assets/hero.png?raw=true" width="100%" alt="OOLONG benchmark — latest results">
|
|
48
|
+
</p>
|
|
27
49
|
|
|
28
|
-
|
|
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. |
|
|
50
|
+
**OOLONG (oolong-synth)** — paper-tier long-context suite; latest journal per model,
|
|
51
|
+
cost per task from real `costUsd` (older journals estimated at OpenRouter list prices):
|
|
36
52
|
|
|
37
|
-
|
|
53
|
+
| Model | Score | Avg. cost/task |
|
|
54
|
+
|-------|-------|----------------|
|
|
55
|
+
| `qwen/qwen3.8-27b` | **100%** | $0.0127 |
|
|
56
|
+
| `google/gemma-3-27b-it` | 83.3% | $0.0013 |
|
|
57
|
+
| `qwen/qwen3-30b-a3b-instruct-2507` | 66.7% | $0.0009 |
|
|
58
|
+
| `mistralai/mistral-small-3.2-24b-instruct` | 66.7% | $0.0025 |
|
|
38
59
|
|
|
39
|
-
|
|
60
|
+
Lite suite — `needle` multi-needle recall, `codeqa` repo-QA, `coding` fix task
|
|
61
|
+
(7 tasks × 2 passes per model, deterministic graders, no LLM-as-judge):
|
|
40
62
|
|
|
41
|
-
|
|
63
|
+
| Model | Score | Accuracy |
|
|
64
|
+
|-------|-------|----------|
|
|
65
|
+
| `qwen/qwen3-30b-a3b-instruct-2507` | **14/14** | **100%** |
|
|
66
|
+
| `google/gemma-3-27b-it` | 12/14 | 86% |
|
|
67
|
+
| `mistralai/mistral-small-3.2-24b-instruct` | 12/14 | 86% |
|
|
42
68
|
|
|
43
|
-
|
|
69
|
+
Raw per-task rows (correct, recall, latency, tokens, cost) live in
|
|
70
|
+
`bench/runs/*.jsonl` — one JSONL row per task, committed as history.
|
|
44
71
|
|
|
45
|
-
|
|
72
|
+
### Run the benchmarks
|
|
46
73
|
|
|
47
74
|
```bash
|
|
48
|
-
|
|
49
|
-
```
|
|
75
|
+
export OPENROUTER_API_KEY=sk-or-... # required — env vars are the only key transport
|
|
50
76
|
|
|
51
|
-
|
|
77
|
+
bun run bench # lite suite: needle + codeqa + coding
|
|
78
|
+
bun run bench --suite needle --limit 1 # one suite, first task only
|
|
79
|
+
bun run bench --model openrouter/qwen/qwen3-30b-a3b-instruct-2507
|
|
80
|
+
bun run bench --list # print tasks, no engine / no key
|
|
81
|
+
bun run bench --suite paper # paper tier: s_niah, oolong, browsecomp, codeqa_lb (downloads datasets)
|
|
82
|
+
```
|
|
52
83
|
|
|
53
|
-
|
|
84
|
+
Suites: `all` (lite, default) · `needle` · `codeqa` · `coding` · `paper` · `s_niah` ·
|
|
85
|
+
`oolong` · `browsecomp` · `codeqa_lb`. Regenerate the hero chart:
|
|
86
|
+
`python3 bench/hero.py` (needs `matplotlib`).
|
|
54
87
|
|
|
55
|
-
##
|
|
88
|
+
## How it works
|
|
56
89
|
|
|
57
90
|
```
|
|
58
91
|
┌─────────────────────────┐
|
|
@@ -106,10 +139,6 @@ A **Recursive Language Model (RLM)** replaces `llm.completion(prompt)` with
|
|
|
106
139
|
launch sub-LLM and sub-RLM calls as ordinary Python functions — decomposing,
|
|
107
140
|
delegating, and synthesizing across a tree of models, not a single context window.
|
|
108
141
|
|
|
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.
|
|
112
|
-
|
|
113
142
|
## Commands
|
|
114
143
|
|
|
115
144
|
| Command | Shortcut | What it does |
|
|
@@ -129,6 +158,45 @@ inside Pi — same session, same tools, same everything.
|
|
|
129
158
|
| REPL timeout | `120`s | Bump for slow computations |
|
|
130
159
|
| Max concurrent subs | `16` | More parallelism (costs RAM) |
|
|
131
160
|
|
|
161
|
+
## Sampling & reproducibility
|
|
162
|
+
|
|
163
|
+
The r3 bench showed the biggest capability lever is not the model — it is the sampling:
|
|
164
|
+
temperature 0 took OOLONG from 71% pooled / 40% flips to 91.7–100% all-stable for $1.74
|
|
165
|
+
total. Those knobs are first-class in `rlm.json` (`~/.pi/agent/rlm.json`) and on the
|
|
166
|
+
`/rlm-config` panel:
|
|
167
|
+
|
|
168
|
+
| Field | Where | Default | What it governs |
|
|
169
|
+
|-------|-------|---------|-----------------|
|
|
170
|
+
| `rootSampling.maxTokens` | rlm.json, panel | `16384` | Output cap per root-model turn (finalize included) |
|
|
171
|
+
| `rootSampling.temperature` | rlm.json, panel | provider default | Root + finalize sampling temperature; `0` = deterministic |
|
|
172
|
+
| `smartReasoning` | rlm.json, panel | none | Thinking effort for the root model |
|
|
173
|
+
| `subSampling.maxTokens` | rlm.json, panel | `8192` | Output cap per leaf sub-call (`llm_query`, `llm_batch`, `map_files`) |
|
|
174
|
+
| `subSampling.temperature` | rlm.json, panel | provider default | Leaf sampling temperature |
|
|
175
|
+
| `enableVerificationNudge` | rlm.json | off | One coached redo when the root finalizes early with a bare number / short label |
|
|
176
|
+
|
|
177
|
+
**Reproducibility recipe (validated by r3):**
|
|
178
|
+
|
|
179
|
+
```json
|
|
180
|
+
{
|
|
181
|
+
"config": {
|
|
182
|
+
"rootSampling": { "maxTokens": 8192, "temperature": 0, "reasoning": "high" }
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Reasoning tokens share the completion budget with the answer — with thinking on, keep
|
|
188
|
+
`maxTokens` generous (the bench doubles it to 8192; the engine warns once on turn 0 when it
|
|
189
|
+
is tight).
|
|
190
|
+
|
|
191
|
+
**Scope boundary:** `rlm.json` sampling applies to RLM-mode runs, `rlm()` delegation, and
|
|
192
|
+
child recursion at any depth (same engine function). The native Pi agent loop follows Pi's
|
|
193
|
+
own session settings — rlm.json never touches it.
|
|
194
|
+
|
|
195
|
+
**Model capability:** reasoning requires a model whose registry entry has `reasoning: true`.
|
|
196
|
+
Anything else has the level dropped before it reaches the provider (pi-ai clamps unsupported
|
|
197
|
+
levels to off); capability comes from the registry, so OpenRouter hybrids like
|
|
198
|
+
`qwen/qwen3.8-27b` just work.
|
|
199
|
+
|
|
132
200
|
## Prompt Architecture
|
|
133
201
|
|
|
134
202
|
The system prompt follows a **contract / routing / examples / rules** pattern
|
|
@@ -142,38 +210,6 @@ The system prompt follows a **contract / routing / examples / rules** pattern
|
|
|
142
210
|
**Key insight:** children see `Recursion depth: N` and calibrate ambition —
|
|
143
211
|
delegating only when their task genuinely decomposes further.
|
|
144
212
|
|
|
145
|
-
## Benchmarks
|
|
146
|
-
|
|
147
|
-
E2E runs of the real engine against OpenRouter chat models — the lite suite
|
|
148
|
-
(`needle` multi-needle recall, `codeqa` repo-QA, `coding` fix task; 7 tasks × 2 passes
|
|
149
|
-
per model). Deterministic graders (recall / gold containment / regex), no LLM-as-judge.
|
|
150
|
-
|
|
151
|
-
Latest results — small models (≤32B parameters, paid tier):
|
|
152
|
-
|
|
153
|
-
| Model | Params | Score | Accuracy | Latency/task |
|
|
154
|
-
|-------|--------|-------|----------|--------------|
|
|
155
|
-
| `qwen/qwen3-30b-a3b-instruct-2507` | MoE 30B / 3B active | **14/14** | **100%** | ~15s |
|
|
156
|
-
| `google/gemma-3-27b-it` | dense 27B | 12/14 | 86% | ~26s |
|
|
157
|
-
| `mistralai/mistral-small-3.2-24b-instruct` | dense 24B | 12/14 | 86% | ~28s |
|
|
158
|
-
|
|
159
|
-
Raw per-task rows (correct, recall, latency, tokens, cost) live in
|
|
160
|
-
`bench/runs/bench-<ts>.jsonl` — one JSONL row per task, committed as history.
|
|
161
|
-
|
|
162
|
-
### Run the benchmarks
|
|
163
|
-
|
|
164
|
-
```bash
|
|
165
|
-
export OPENROUTER_API_KEY=sk-or-... # required — env vars are the only key transport
|
|
166
|
-
|
|
167
|
-
bun run bench # lite suite: needle + codeqa + coding
|
|
168
|
-
bun run bench --suite needle --limit 1 # one suite, first task only
|
|
169
|
-
bun run bench --model openrouter/qwen/qwen3-30b-a3b-instruct-2507
|
|
170
|
-
bun run bench --list # print tasks, no engine / no key
|
|
171
|
-
bun run bench --suite paper # paper tier: s_niah, oolong, browsecomp, codeqa_lb (downloads datasets)
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
Suites: `all` (lite, default) · `needle` · `codeqa` · `coding` · `paper` · `s_niah` ·
|
|
175
|
-
`oolong` · `browsecomp` · `codeqa_lb`.
|
|
176
|
-
|
|
177
213
|
## Security
|
|
178
214
|
|
|
179
215
|
- **Key isolation** — provider keys live in TypeScript only; sandbox receives prompts, returns text.
|
|
@@ -182,12 +218,6 @@ Suites: `all` (lite, default) · `needle` · `codeqa` · `coding` · `paper` ·
|
|
|
182
218
|
- **Per-block timeout** — SIGALRM + parent watchdog (SIGKILL on hang).
|
|
183
219
|
- **Trust** — project-local install requires Pi project trust.
|
|
184
220
|
|
|
185
|
-
## Uninstall
|
|
186
|
-
|
|
187
|
-
```bash
|
|
188
|
-
pi uninstall npm:@hicaru/pi-rlm
|
|
189
|
-
```
|
|
190
|
-
|
|
191
221
|
## License
|
|
192
222
|
|
|
193
223
|
MIT — see [LICENSE](./LICENSE).
|
package/README.ru.md
CHANGED
|
@@ -132,6 +132,43 @@ rm -rf ~/.pi/agent/extensions/rlm
|
|
|
132
132
|
|
|
133
133
|
> **Примечание по параллелизму:** каждый дочерний `rlm_query` запускает собственного worker `python3` (~50–150 мс «холодного старта»). В худшем случае количество параллельных интерпретаторов ≈ `maxConcurrentSubcalls`^(depth−1); при настройках по умолчанию (глубина 4, параллелизм 4) это 4³ = 64 в патологическом случае. Лимиты бюджета и ошибок (см. выше) ограничивают общие затраты независимо от степени разветвления.
|
|
134
134
|
|
|
135
|
+
## Сэмплинг и воспроизводимость
|
|
136
|
+
|
|
137
|
+
Бенчмарк r3 показал: самый большой рычаг качества — не модель, а сэмплинг. Temperature 0
|
|
138
|
+
перевела OOLONG с 71% (pooled) / 40% (flips) на 91.7–100% all-stable за $1.74 суммарно.
|
|
139
|
+
Эти ручки — полноправные поля `rlm.json` (`~/.pi/agent/rlm.json`) и панели `/rlm-config`:
|
|
140
|
+
|
|
141
|
+
| Поле | Где | По умолчанию | За что отвечает |
|
|
142
|
+
|---|---|---|---|
|
|
143
|
+
| `rootSampling.maxTokens` | rlm.json, панель | `16384` | Лимит вывода на ход корневой модели (включая finalize) |
|
|
144
|
+
| `rootSampling.temperature` | rlm.json, панель | провайдер | Температура корневых ходов и finalize; `0` = детерминированно |
|
|
145
|
+
| `smartReasoning` | rlm.json, панель | нет | Усилие «размышления» корневой модели |
|
|
146
|
+
| `subSampling.maxTokens` | rlm.json, панель | `8192` | Лимит вывода одного листового вызова (`llm_query`, `llm_batch`, `map_files`) |
|
|
147
|
+
| `subSampling.temperature` | rlm.json, панель | провайдер | Температура листовых вызовов |
|
|
148
|
+
| `enableVerificationNudge` | rlm.json | выкл | Один повтор с подсказкой, если корень рано финализирует «голым числом» |
|
|
149
|
+
|
|
150
|
+
**Рецепт воспроизводимости (проверен r3):**
|
|
151
|
+
|
|
152
|
+
```json
|
|
153
|
+
{
|
|
154
|
+
"config": {
|
|
155
|
+
"rootSampling": { "maxTokens": 8192, "temperature": 0, "reasoning": "high" }
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Токены размышлений делят бюджет завершения с ответом — при включённом thinking держите
|
|
161
|
+
`maxTokens` щедрым (бенчмарк удваивает его до 8192; движок один раз предупредит на ходе 0,
|
|
162
|
+
если лимит тесный).
|
|
163
|
+
|
|
164
|
+
**Граница действия:** сэмплинг из `rlm.json` управляет запусками RLM-режима, делегированием
|
|
165
|
+
`rlm()` и рекурсией на любой глубине (тот же движок). Собственный цикл агента Pi следует
|
|
166
|
+
настройкам сессии Pi — rlm.json его не касается.
|
|
167
|
+
|
|
168
|
+
**Возможности модели:** reasoning требует модель с `reasoning: true` в записи реестра.
|
|
169
|
+
Для остальных уровень отбрасывается до отправки провайдеру (pi-ai приводит его к off);
|
|
170
|
+
возможность берётся из реестра, а не из конфига.
|
|
171
|
+
|
|
135
172
|
## Логи запусков
|
|
136
173
|
|
|
137
174
|
|
package/README.zh-CN.md
CHANGED
|
@@ -141,6 +141,40 @@ rm -rf ~/.pi/agent/extensions/rlm
|
|
|
141
141
|
> 默认设置下 (深度 4, 并发 4),极端情况下为 4³ = 64。预算和错误
|
|
142
142
|
> 上限 (见上文) 无论扇出 (fan-out) 如何都会限制总支出。
|
|
143
143
|
|
|
144
|
+
## 采样与可复现性
|
|
145
|
+
|
|
146
|
+
r3 基准测试表明:最大的能力杠杆不是模型,而是采样。仅 temperature 0 就让 OOLONG 从
|
|
147
|
+
71% (pooled) / 40% (flips) 提升到 91.7–100% all-stable,总计仅花费 $1.74。这些旋钮在
|
|
148
|
+
`rlm.json` (`~/.pi/agent/rlm.json`) 和 `/rlm-config` 面板中都是一等公民:
|
|
149
|
+
|
|
150
|
+
| 字段 | 位置 | 默认值 | 作用范围 |
|
|
151
|
+
|---|---|---|---|
|
|
152
|
+
| `rootSampling.maxTokens` | rlm.json, 面板 | `16384` | 根模型每轮的输出上限 (包括 finalize) |
|
|
153
|
+
| `rootSampling.temperature` | rlm.json, 面板 | 提供商默认 | 根轮次与 finalize 的采样温度;`0` = 确定性 |
|
|
154
|
+
| `smartReasoning` | rlm.json, 面板 | 无 | 根模型的思考强度 |
|
|
155
|
+
| `subSampling.maxTokens` | rlm.json, 面板 | `8192` | 每个叶子子调用 (`llm_query`, `llm_batch`, `map_files`) 的输出上限 |
|
|
156
|
+
| `subSampling.temperature` | rlm.json, 面板 | 提供商默认 | 叶子调用的采样温度 |
|
|
157
|
+
| `enableVerificationNudge` | rlm.json | 关 | 当根节点过早以“裸数字”定稿时,给予一次带提示的重做 |
|
|
158
|
+
|
|
159
|
+
**可复现性配方 (经 r3 验证):**
|
|
160
|
+
|
|
161
|
+
```json
|
|
162
|
+
{
|
|
163
|
+
"config": {
|
|
164
|
+
"rootSampling": { "maxTokens": 8192, "temperature": 0, "reasoning": "high" }
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
思考 token 与答案共享补全预算——开启 thinking 时请保持充裕的 `maxTokens`
|
|
170
|
+
(基准测试将其翻倍至 8192;若过紧,引擎会在第 0 轮警告一次)。
|
|
171
|
+
|
|
172
|
+
**作用边界:** `rlm.json` 的采样适用于 RLM 模式运行、`rlm()` 委托以及任意深度的子递归
|
|
173
|
+
(同一个引擎函数)。Pi 原生代理循环遵循 Pi 自身的会话设置——rlm.json 不会触及它。
|
|
174
|
+
|
|
175
|
+
**模型能力:** reasoning 要求模型的注册表条目为 `reasoning: true`。其他情况会在发送给
|
|
176
|
+
提供商之前丢弃该级别 (pi-ai 会将不支持的级别钳制为 off);能力来自注册表,而非配置。
|
|
177
|
+
|
|
144
178
|
## 运行日志
|
|
145
179
|
|
|
146
180
|
- **密钥隔离**:供应商密钥仅存在于 TypeScript (`AuthStorage`) 中;沙箱
|
package/package.json
CHANGED
package/src/bridge/model.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* RLM root (full history).
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
import { clampThinkingLevel } from "@earendil-works/pi-ai";
|
|
12
13
|
import { type Api, completeSimple, type Message, type Model, type ThinkingLevel, type Usage } from "@earendil-works/pi-ai/compat";
|
|
13
14
|
import type { ModelRegistry } from "@earendil-works/pi-coding-agent";
|
|
14
15
|
import { completeWithRetry, DEFAULT_RETRY_POLICY, type RetryPolicy } from "../util/retry.ts";
|
|
@@ -68,6 +69,19 @@ function toPiMessages(messages: readonly ChatMsg[], model: Model<Api>): { readon
|
|
|
68
69
|
return { systemPrompt, messages: out };
|
|
69
70
|
}
|
|
70
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Explicit reasoning gate. pi-ai clamps unsupported levels too (a model with `reasoning:
|
|
74
|
+
* false` supports only "off", so any requested level clamps to it and nothing goes out on
|
|
75
|
+
* the wire) — owning the gate here makes the behavior engine policy, testable without a
|
|
76
|
+
* provider, and drops the option outright instead of forwarding a level the model cannot
|
|
77
|
+
* honor. Capability comes from the registry entry, never from config.
|
|
78
|
+
*/
|
|
79
|
+
export function effectiveReasoning(model: Model<Api>, level: ThinkingLevel | undefined): ThinkingLevel | undefined {
|
|
80
|
+
if (level === undefined) return undefined;
|
|
81
|
+
const clamped = clampThinkingLevel(model, level);
|
|
82
|
+
return clamped === "off" ? undefined : clamped;
|
|
83
|
+
}
|
|
84
|
+
|
|
71
85
|
/** Extract the assistant's plain text from a completion. */
|
|
72
86
|
function extractText(content: readonly { readonly type: string; readonly text?: string }[]): string {
|
|
73
87
|
return content
|
|
@@ -97,7 +111,7 @@ export async function modelComplete(messages: readonly ChatMsg[], opts: Complete
|
|
|
97
111
|
headers: auth.headers,
|
|
98
112
|
maxTokens: opts.maxTokens,
|
|
99
113
|
temperature: opts.temperature,
|
|
100
|
-
reasoning: opts.reasoning,
|
|
114
|
+
reasoning: effectiveReasoning(opts.model, opts.reasoning),
|
|
101
115
|
signal: opts.signal,
|
|
102
116
|
onResponse: (res) => { note(res.status, res.headers); },
|
|
103
117
|
},
|
package/src/config/defaults.ts
CHANGED
|
@@ -58,4 +58,7 @@ export const DEFAULT_CONFIG: Readonly<RlmConfig> = Object.freeze({
|
|
|
58
58
|
memoryDir: null,
|
|
59
59
|
// v5 role separation: children delegate (llm + memory/ledger); "legacy" = full child surface.
|
|
60
60
|
childSurface: "delegation",
|
|
61
|
+
// Verification-discipline nudge — deliberately OFF (plan guardrail): when on, an early
|
|
62
|
+
// bare-number finalize gets one coached redo instead of being accepted. Opt-in via rlm.json.
|
|
63
|
+
enableVerificationNudge: false,
|
|
61
64
|
});
|
package/src/config/settings.ts
CHANGED
|
@@ -38,9 +38,10 @@ function validateString(v: unknown): string | undefined {
|
|
|
38
38
|
/**
|
|
39
39
|
* Every value pi-ai accepts for `reasoning`. Keyed by the union so a new level added upstream
|
|
40
40
|
* is a compile error here rather than a silently-rejected setting. Note `off` is NOT a
|
|
41
|
-
* ThinkingLevel — a hand-edited rlm.json carrying one is dropped, not forwarded.
|
|
41
|
+
* ThinkingLevel — a hand-edited rlm.json carrying one is dropped, not forwarded. Exported so
|
|
42
|
+
* the config panel derives its choices from the same source of truth.
|
|
42
43
|
*/
|
|
43
|
-
const THINKING_LEVELS: Readonly<Record<ThinkingLevel, true>> = Object.freeze({
|
|
44
|
+
export const THINKING_LEVELS: Readonly<Record<ThinkingLevel, true>> = Object.freeze({
|
|
44
45
|
minimal: true, low: true, medium: true, high: true, xhigh: true, max: true,
|
|
45
46
|
});
|
|
46
47
|
|
|
@@ -149,6 +150,9 @@ export function validateConfig(raw: unknown): Partial<RlmConfig> {
|
|
|
149
150
|
}
|
|
150
151
|
// v5 child surface doctrine
|
|
151
152
|
if (r.childSurface === "delegation" || r.childSurface === "legacy") out.childSurface = r.childSurface;
|
|
153
|
+
// Verification-discipline nudge (default OFF).
|
|
154
|
+
const enableVerificationNudge = validateBoolean(r.enableVerificationNudge);
|
|
155
|
+
if (enableVerificationNudge !== undefined) out.enableVerificationNudge = enableVerificationNudge;
|
|
152
156
|
if (typeof r.subSampling === "object" && r.subSampling !== null) {
|
|
153
157
|
const ss = r.subSampling as Record<string, unknown>;
|
|
154
158
|
const sampling: { maxTokens?: number; temperature?: number; reasoning?: ThinkingLevel } = {};
|
package/src/core/engine.ts
CHANGED
|
@@ -20,7 +20,7 @@ import { TaskLedger, contextSig, taskKey } from "./ledger.ts";
|
|
|
20
20
|
import { type MemoryStore, rootContextPaths } from "./memory.ts";
|
|
21
21
|
import { type ChatMsg, modelComplete } from "../bridge/model.ts";
|
|
22
22
|
import { buildRlmSystemPrompt } from "../prompts/system.ts";
|
|
23
|
-
import { buildTurnPrompt, FINALIZE_PROMPT, RETRIEVAL_NUDGE } from "../prompts/user.ts";
|
|
23
|
+
import { buildTurnPrompt, FINALIZE_PROMPT, RETRIEVAL_NUDGE, REASONING_BUDGET_HINT, VERIFICATION_NUDGE } from "../prompts/user.ts";
|
|
24
24
|
import type { RlmEmitter } from "../tool/rlm-events.ts";
|
|
25
25
|
import type { SubcallPhase } from "../tool/rlm-details.ts";
|
|
26
26
|
import { PythonSandbox, SANDBOX_WATCHDOG_HEARTBEAT_MS } from "../sandbox/sandbox.ts";
|
|
@@ -46,6 +46,9 @@ import { createSubcallGates, type SubcallGates } from "../util/concurrency.ts";
|
|
|
46
46
|
* hold a finished run open on work whose result nobody can receive.
|
|
47
47
|
*/
|
|
48
48
|
const DETACHED_SETTLE_MS = 5_000;
|
|
49
|
+
/** Verification nudge (enableVerificationNudge): only an EARLY finalize is suspicious — from
|
|
50
|
+
* turn 4 on, a bare answer is just... an answer. "Before iteration ~4", per the bench data. */
|
|
51
|
+
const VERIFICATION_NUDGE_TURN_CAP = 4;
|
|
49
52
|
/** H6 (audit): root episodes snapshot at most this many real files — replay invalidation for
|
|
50
53
|
* the disk-backed slice of the context without hashing an unbounded repository. */
|
|
51
54
|
const ROOT_HASH_MAX = 64;
|
|
@@ -253,6 +256,10 @@ export function createEngine(deps: EngineDeps): RunRlm {
|
|
|
253
256
|
// H3: retrieval-discipline coach — one-shot per run; children inherit it via the same loop.
|
|
254
257
|
let sawRetrieval = false;
|
|
255
258
|
let retrievalNudged = false;
|
|
259
|
+
// Verification-discipline coach (enableVerificationNudge, default OFF): one coached redo
|
|
260
|
+
// when an early finalize looks like the confident-wrong bench shape.
|
|
261
|
+
let verificationNudged = false;
|
|
262
|
+
let verificationNudgePending = false;
|
|
256
263
|
|
|
257
264
|
try {
|
|
258
265
|
const meta = {
|
|
@@ -320,6 +327,13 @@ export function createEngine(deps: EngineDeps): RunRlm {
|
|
|
320
327
|
liveContext = input.context ?? [];
|
|
321
328
|
contextPin = await pinContext(liveContext);
|
|
322
329
|
await sandbox.loadContextPinned(contextPin);
|
|
330
|
+
|
|
331
|
+
// rootSampling fields win; smartReasoning is the default reasoning when not overridden.
|
|
332
|
+
// Loop-invariant — built once here; finalize() applies the same merge to its own turn.
|
|
333
|
+
const rootSampling: Sampling = {
|
|
334
|
+
reasoning: deps.config.smartReasoning,
|
|
335
|
+
...deps.config.rootSampling,
|
|
336
|
+
};
|
|
323
337
|
for (let i = 0; i < deps.config.maxIterations; i++) {
|
|
324
338
|
limits.checkTimeout();
|
|
325
339
|
if (selfReportId) emitter.emitSubcallUpdated({ id: selfReportId, detail: `turn ${i + 1}/${deps.config.maxIterations}` });
|
|
@@ -369,16 +383,18 @@ export function createEngine(deps: EngineDeps): RunRlm {
|
|
|
369
383
|
ledgerBlock === "" ? undefined : ledgerBlock,
|
|
370
384
|
memoryBlock === "" ? undefined : memoryBlock,
|
|
371
385
|
nudgeNow ? RETRIEVAL_NUDGE : undefined,
|
|
386
|
+
verificationNudgePending ? VERIFICATION_NUDGE : undefined,
|
|
387
|
+
// One-shot (turn 0 only): thinking tokens share the completion budget — mirror of
|
|
388
|
+
// the bench's doubling rule. Advisory; never fatal, never repeated.
|
|
389
|
+
i === 0 && rootSampling.reasoning !== undefined && (rootSampling.maxTokens ?? 16_384) < 8_192
|
|
390
|
+
? REASONING_BUDGET_HINT
|
|
391
|
+
: undefined,
|
|
372
392
|
]
|
|
373
393
|
.filter((s): s is string => s !== undefined)
|
|
374
394
|
.join("\n\n") || undefined;
|
|
395
|
+
verificationNudgePending = false;
|
|
375
396
|
appendUserMessage(history, buildTurnPrompt(i, deps.config.maxIterations, notes));
|
|
376
397
|
|
|
377
|
-
// rootSampling fields win; smartReasoning is the default reasoning when not overridden.
|
|
378
|
-
const rootSampling: Sampling = {
|
|
379
|
-
reasoning: deps.config.smartReasoning,
|
|
380
|
-
...deps.config.rootSampling,
|
|
381
|
-
};
|
|
382
398
|
const turn = await runTurn(history, sandbox, {
|
|
383
399
|
model: model,
|
|
384
400
|
registry: deps.registry,
|
|
@@ -414,10 +430,19 @@ export function createEngine(deps: EngineDeps): RunRlm {
|
|
|
414
430
|
completedTurns = i + 1;
|
|
415
431
|
const final = finalAnswerOf(turn.results);
|
|
416
432
|
if (final != null) {
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
433
|
+
// Verification-discipline nudge (enableVerificationNudge, default OFF): an early
|
|
434
|
+
// finalize whose answer is a bare number / short label is the confident-wrong shape
|
|
435
|
+
// that dominated bench failures. ONE coached redo, then the answer is accepted.
|
|
436
|
+
if (deps.config.enableVerificationNudge === true && !verificationNudged
|
|
437
|
+
&& completedTurns < VERIFICATION_NUDGE_TURN_CAP && isBareAnswer(final)) {
|
|
438
|
+
verificationNudged = true;
|
|
439
|
+
verificationNudgePending = true;
|
|
440
|
+
} else {
|
|
441
|
+
const done = result(final, i + 1, limits);
|
|
442
|
+
persistRoot(done.answer);
|
|
443
|
+
lastAnswer = done.answer;
|
|
444
|
+
return done;
|
|
445
|
+
}
|
|
421
446
|
}
|
|
422
447
|
|
|
423
448
|
limits.observe(turnHadError(turn.results));
|
|
@@ -535,6 +560,13 @@ function contextWindowOrFallback(model: Model<Api>, registry: ModelContextRegist
|
|
|
535
560
|
return registry.limitFor(`${model.provider}/${model.id}`);
|
|
536
561
|
}
|
|
537
562
|
|
|
563
|
+
/** Bare number / short label — the early-confident answer shape the verification nudge
|
|
564
|
+
* targets (28/33 bench failures were early confident wrong answers). */
|
|
565
|
+
function isBareAnswer(answer: string): boolean {
|
|
566
|
+
const t = answer.trim();
|
|
567
|
+
return t.length <= 12 || /^[-+$(€£¥]?\d+(?:[.,]\d+)*\s*%?$/.test(t);
|
|
568
|
+
}
|
|
569
|
+
|
|
538
570
|
/** Out of turns: ask the model for its best final answer. FINALIZE_PROMPT asks for a fenced
|
|
539
571
|
* ```repl``` block, so execute it like any turn and prefer the captured answer (H2) — a raw
|
|
540
572
|
* fence echoed verbatim must never become the run answer. Plain text stays the fallback. */
|
|
@@ -548,10 +580,19 @@ async function finalize(
|
|
|
548
580
|
const finalHistory = [...history];
|
|
549
581
|
appendUserMessage(finalHistory, FINALIZE_PROMPT);
|
|
550
582
|
const complete = deps.complete ?? modelComplete;
|
|
583
|
+
// Same merge rule as the main loop (the `rootSampling` construction in run()): rootSampling
|
|
584
|
+
// wins, smartReasoning is the reasoning default. Finalize is a root turn — it must obey the
|
|
585
|
+
// user's sampling too, or the last turn of every run silently reverts to provider defaults.
|
|
586
|
+
const rootSampling: Sampling = {
|
|
587
|
+
reasoning: deps.config.smartReasoning,
|
|
588
|
+
...deps.config.rootSampling,
|
|
589
|
+
};
|
|
551
590
|
const { text, usage } = await complete(finalHistory, {
|
|
552
591
|
model,
|
|
553
592
|
registry: deps.registry,
|
|
554
|
-
|
|
593
|
+
maxTokens: rootSampling.maxTokens,
|
|
594
|
+
temperature: rootSampling.temperature,
|
|
595
|
+
reasoning: rootSampling.reasoning,
|
|
555
596
|
signal: deps.signal,
|
|
556
597
|
});
|
|
557
598
|
limits.addUsage(usage);
|
package/src/core/types.ts
CHANGED
|
@@ -110,6 +110,11 @@ export interface RlmConfig {
|
|
|
110
110
|
/** v5 doctrine: "delegation" = child engines get llm/memory/ledger only (no repo retrieval);
|
|
111
111
|
* "legacy" keeps today's full child surface as a one-flip rollback. */
|
|
112
112
|
readonly childSurface: "delegation" | "legacy";
|
|
113
|
+
/** Verification-discipline nudge (default OFF — it changes interactive behavior): when the
|
|
114
|
+
* root finalizes before turn 4 with a bare number / short label, it gets ONE coached redo
|
|
115
|
+
* ("recompute and sanity-check in Python") instead of accepting the answer. Opt-in via
|
|
116
|
+
* rlm.json; evidence: 28/33 bench failures were early confident wrong answers. */
|
|
117
|
+
readonly enableVerificationNudge?: boolean;
|
|
113
118
|
}
|
|
114
119
|
|
|
115
120
|
/** Input to a (headless) RLM run. */
|
package/src/prompts/user.ts
CHANGED
|
@@ -34,3 +34,20 @@ export const RETRIEVAL_NUDGE =
|
|
|
34
34
|
"[coach] You have not inspected the external context yet — it is NOT included in this " +
|
|
35
35
|
"chat, and guessing is useless: the text is padding. On THIS turn, call search(\"...\") " +
|
|
36
36
|
"or grep_context(\"...\") inside a ```repl block before answering.";
|
|
37
|
+
|
|
38
|
+
/** One-shot budget hint (engine-owned, turn 0 only): reasoning tokens share the completion
|
|
39
|
+
* budget with the answer, mirroring the bench's doubling rule — a reasoning root with a
|
|
40
|
+
* small output cap risks truncated thought. Advisory only; never fatal. */
|
|
41
|
+
export const REASONING_BUDGET_HINT =
|
|
42
|
+
"[budget] Reasoning is on while the root output cap is below 8192 tokens: thinking shares " +
|
|
43
|
+
"the completion budget with the answer, so long thought may be cut off mid-reasoning. " +
|
|
44
|
+
"Keep thought concise, or raise rootSampling.maxTokens.";
|
|
45
|
+
|
|
46
|
+
/** One-shot verification-discipline nudge (default OFF — enableVerificationNudge): fired when
|
|
47
|
+
* the root finalizes suspiciously early with a bare number / short label. The model gets ONE
|
|
48
|
+
* coached redo instead of having the answer accepted. */
|
|
49
|
+
export const VERIFICATION_NUDGE =
|
|
50
|
+
"[coach] That answer was submitted suspiciously early and looks under-verified. Before " +
|
|
51
|
+
"finalizing: recompute the key quantity inside a ```repl block (show the actual computation, " +
|
|
52
|
+
"not a restatement), sanity-check it against the source material, and only then set " +
|
|
53
|
+
"answer[\"content\"] again.";
|
|
Binary file
|
|
@@ -235,7 +235,10 @@ def grep_context(
|
|
|
235
235
|
`counts` is complete even when `hits` is capped.
|
|
236
236
|
"""
|
|
237
237
|
try:
|
|
238
|
-
|
|
238
|
+
# MULTILINE: the doc-level gate below must not veto line-anchored patterns (^/$) —
|
|
239
|
+
# without it, `^foo` on a multi-line doc never matches outside position 0 and every
|
|
240
|
+
# line hit is silently filtered out (grep is line-oriented; match that).
|
|
241
|
+
rx = re.compile(pattern, re.MULTILINE)
|
|
239
242
|
except re.error as e:
|
|
240
243
|
return {"hits": [], "counts": {}, "total": 0, "truncated": False, "error": f"bad regex: {e}"}
|
|
241
244
|
try:
|
package/src/sandbox/sandbox.ts
CHANGED
|
@@ -344,6 +344,8 @@ export class PythonSandbox {
|
|
|
344
344
|
frame: msg.type,
|
|
345
345
|
id: "id" in msg ? msg.id : undefined,
|
|
346
346
|
rid: "rid" in msg ? msg.rid : undefined,
|
|
347
|
+
// Post-mortem needs the payload, not just the shape (mirrors repl-tool's 400-char cap).
|
|
348
|
+
...(msg.type === "exec" ? { chars: msg.code.length, code: msg.code.slice(0, 400) } : {}),
|
|
347
349
|
});
|
|
348
350
|
}
|
|
349
351
|
// Never write to a corpse. The write would fail asynchronously and, historically, take the
|
|
@@ -423,7 +425,18 @@ export class PythonSandbox {
|
|
|
423
425
|
prompts: "prompts" in msg ? msg.prompts?.length : 1,
|
|
424
426
|
});
|
|
425
427
|
} else {
|
|
426
|
-
trace("frame.in", {
|
|
428
|
+
trace("frame.in", {
|
|
429
|
+
frame: "response",
|
|
430
|
+
id: msg.id,
|
|
431
|
+
ok: msg.ok,
|
|
432
|
+
...(msg.ok
|
|
433
|
+
? {
|
|
434
|
+
stdout: msg.stdout?.slice(0, 300),
|
|
435
|
+
finalAnswer: msg.final_answer ?? undefined,
|
|
436
|
+
vars: msg.var_names?.length,
|
|
437
|
+
}
|
|
438
|
+
: { error: msg.error?.slice(0, 200) }),
|
|
439
|
+
});
|
|
427
440
|
}
|
|
428
441
|
}
|
|
429
442
|
if (isInterrupt(msg)) {
|
package/src/ui/config-panel.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/** Config panel TUI — toggle RLM run parameters with descriptions. */
|
|
2
2
|
|
|
3
3
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import type { ThinkingLevel } from "@earendil-works/pi-ai";
|
|
4
5
|
import { getSettingsListTheme } from "@earendil-works/pi-coding-agent";
|
|
5
6
|
import { Container, type SettingItem, SettingsList, Text } from "@earendil-works/pi-tui";
|
|
6
7
|
import type { RlmConfig } from "../core/types.ts";
|
|
8
|
+
import { THINKING_LEVELS } from "../config/settings.ts";
|
|
7
9
|
|
|
8
10
|
const CHOICES = Object.freeze({
|
|
9
11
|
maxDepth: Object.freeze(["1", "2", "3", "4"]),
|
|
@@ -18,6 +20,10 @@ const CHOICES = Object.freeze({
|
|
|
18
20
|
compaction: Object.freeze(["on", "off"]),
|
|
19
21
|
compactionThresholdPct: Object.freeze(["50", "65", "80", "90"]),
|
|
20
22
|
rootSamplingMaxTokens: Object.freeze(["4096", "8192", "16384", "32768"]),
|
|
23
|
+
rootSamplingTemperature: Object.freeze(["0", "0.3", "0.7", "1.0", "default"]),
|
|
24
|
+
smartReasoning: Object.freeze(["default", ...Object.keys(THINKING_LEVELS)]),
|
|
25
|
+
subSamplingMaxTokens: Object.freeze(["1024", "2048", "4096", "8192"]),
|
|
26
|
+
subSamplingTemperature: Object.freeze(["0", "0.3", "0.7", "1.0", "default"]),
|
|
21
27
|
sandboxInitTimeoutMs: Object.freeze(["10000", "30000", "60000", "120000"]),
|
|
22
28
|
requestTimeoutMs: Object.freeze(["2", "5", "10", "15", "20"]),
|
|
23
29
|
contextLoader: Object.freeze(["on", "off"]),
|
|
@@ -48,6 +54,14 @@ export async function showConfigPanel(ctx: ExtensionContext, config: RlmConfig):
|
|
|
48
54
|
item("compaction", "Trajectory compaction", config.compaction ? "on" : "off", CHOICES.compaction, "Summarize old turns when history approaches the model context window."),
|
|
49
55
|
item("compactionThresholdPct", "Compaction threshold (%)", String(Math.round(config.compactionThresholdPct * 100)), CHOICES.compactionThresholdPct, "Compact once estimated history tokens reach this share of the root model's context window."),
|
|
50
56
|
item("rootSamplingMaxTokens", "Root model output cap (tok)", String(config.rootSampling?.maxTokens ?? 16384), CHOICES.rootSamplingMaxTokens, "Max output tokens per root-model turn. Lower values keep each turn lean."),
|
|
57
|
+
item("rootSamplingTemperature", "Root sampling temperature", config.rootSampling?.temperature === undefined ? "default" : String(config.rootSampling?.temperature), CHOICES.rootSamplingTemperature,
|
|
58
|
+
"Sampling temperature for RLM root turns, finalize included — 0 = deterministic (the r3 reproducibility setting); 'default' = provider default. Applies to RLM-mode runs, rlm() delegation and child recursion; the native Pi agent loop follows Pi's own session settings."),
|
|
59
|
+
item("smartReasoning", "Root reasoning effort", config.smartReasoning ?? "default", CHOICES.smartReasoning,
|
|
60
|
+
"Thinking effort for the root model ('default' = none). Only models whose registry entry supports reasoning will think; others silently run without it. Reasoning tokens share the output cap — raise the root output cap when thinking is on."),
|
|
61
|
+
item("subSamplingMaxTokens", "Worker output cap (tok)", String(config.subSampling?.maxTokens ?? 8192), CHOICES.subSamplingMaxTokens,
|
|
62
|
+
"Max output tokens per leaf sub-call (llm_query / llm_batch / map_files)."),
|
|
63
|
+
item("subSamplingTemperature", "Worker sampling temperature", config.subSampling?.temperature === undefined ? "default" : String(config.subSampling?.temperature), CHOICES.subSamplingTemperature,
|
|
64
|
+
"Sampling temperature for leaf sub-calls; 'default' = provider default. Deterministic extraction (temp 0) is what made the r3 bench stable."),
|
|
51
65
|
item("sandboxInitTimeoutMs", "Sandbox init timeout", String(config.sandboxInitTimeoutMs), CHOICES.sandboxInitTimeoutMs, "How long to wait for the Python worker to start."),
|
|
52
66
|
item("requestTimeoutMs", "Sandbox request timeout (min)", String(Math.round(config.requestTimeoutMs / 60_000)), CHOICES.requestTimeoutMs, "Parent-side watchdog per sandbox request; on breach the Python worker is killed."),
|
|
53
67
|
item("contextLoader", "Context loader", config.contextLoader ? "on" : "off", CHOICES.contextLoader,
|
|
@@ -89,6 +103,13 @@ function optionalNumber(value: string, scale = 1): number | undefined {
|
|
|
89
103
|
return value === "none" ? undefined : Number(value) * scale;
|
|
90
104
|
}
|
|
91
105
|
|
|
106
|
+
/** Optional temperature: the literal "default" clears it (provider default); else [0, 2]. */
|
|
107
|
+
function optionalTemperature(value: string): number | undefined {
|
|
108
|
+
if (value === "default") return undefined;
|
|
109
|
+
const n = Number(value);
|
|
110
|
+
return Number.isFinite(n) && n >= 0 && n <= 2 ? n : undefined;
|
|
111
|
+
}
|
|
112
|
+
|
|
92
113
|
/** Pure: returns a new frozen config with `id` set to `value`; unknown ids pass through. */
|
|
93
114
|
export function applySetting(config: RlmConfig, id: string, value: string): RlmConfig {
|
|
94
115
|
switch (id) {
|
|
@@ -105,6 +126,24 @@ export function applySetting(config: RlmConfig, id: string, value: string): RlmC
|
|
|
105
126
|
case "compactionThresholdPct": return Object.freeze({ ...config, compactionThresholdPct: Number(value) / 100 });
|
|
106
127
|
case "rootSamplingMaxTokens":
|
|
107
128
|
return Object.freeze({ ...config, rootSampling: Object.freeze({ ...config.rootSampling, maxTokens: Number(value) }) });
|
|
129
|
+
case "rootSamplingTemperature": {
|
|
130
|
+
const t = optionalTemperature(value);
|
|
131
|
+
// Reject invalid values (NaN / out of range) — keep the current setting.
|
|
132
|
+
if (t === undefined && value !== "default") return config;
|
|
133
|
+
return Object.freeze({ ...config, rootSampling: Object.freeze({ ...config.rootSampling, temperature: t }) });
|
|
134
|
+
}
|
|
135
|
+
case "smartReasoning":
|
|
136
|
+
if (value === "default") return Object.freeze({ ...config, smartReasoning: undefined });
|
|
137
|
+
return Object.hasOwn(THINKING_LEVELS, value)
|
|
138
|
+
? Object.freeze({ ...config, smartReasoning: value as ThinkingLevel })
|
|
139
|
+
: config;
|
|
140
|
+
case "subSamplingMaxTokens":
|
|
141
|
+
return Object.freeze({ ...config, subSampling: Object.freeze({ ...config.subSampling, maxTokens: Number(value) }) });
|
|
142
|
+
case "subSamplingTemperature": {
|
|
143
|
+
const st = optionalTemperature(value);
|
|
144
|
+
if (st === undefined && value !== "default") return config;
|
|
145
|
+
return Object.freeze({ ...config, subSampling: Object.freeze({ ...config.subSampling, temperature: st }) });
|
|
146
|
+
}
|
|
108
147
|
case "sandboxInitTimeoutMs": return Object.freeze({ ...config, sandboxInitTimeoutMs: Number(value) });
|
|
109
148
|
case "requestTimeoutMs": return Object.freeze({ ...config, requestTimeoutMs: Number(value) * 60_000 });
|
|
110
149
|
case "contextLoader": return Object.freeze({ ...config, contextLoader: value === "on" });
|