@hicaru/pi-rlm 0.1.0 → 0.1.1
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 +2 -7
- package/README.ru.md +2 -4
- package/README.zh-CN.md +2 -7
- package/package.json +9 -11
- package/src/bridge/pi-interactive.ts +3 -48
- package/src/commands/rlm-config.ts +10 -3
- package/src/commands/rlm.ts +0 -15
- package/src/config/settings.ts +1 -24
- package/src/core/answer.ts +1 -17
- package/src/core/engine.ts +9 -14
- package/src/core/types.ts +1 -13
- package/src/index.ts +23 -26
- package/src/prompts/system.ts +18 -36
- package/src/sandbox/protocol.ts +0 -6
- package/src/sandbox/sandbox.ts +0 -1
- package/src/sandbox/worker.py +12 -11
- package/src/tool/repl-details.ts +3 -0
- package/src/tool/repl-tool.ts +25 -2
- package/src/tool/rlm-events.ts +2 -41
- package/src/tool/rlm-tool.ts +0 -13
- package/src/ui/model-picker.ts +12 -4
- package/src/patch/apply.ts +0 -148
- package/src/patch/index.ts +0 -37
- package/src/state/events.ts +0 -22
- package/src/telemetry/dispatcher.ts +0 -116
- package/src/telemetry/index.ts +0 -14
- package/src/telemetry/mlflow-config.ts +0 -15
- package/src/telemetry/mlflow-sink.ts +0 -136
- package/src/telemetry/mlflow.ts +0 -99
- package/src/telemetry/sink.ts +0 -8
- package/src/tool/apply-diff-tool.ts +0 -125
package/README.md
CHANGED
|
@@ -155,16 +155,12 @@ These functions are injected into the model's Python namespace inside the REPL:
|
|
|
155
155
|
> defaults (depth 4, conc 4) that's 4³ = 64 in the pathological case. Budget and error
|
|
156
156
|
> caps (above) bound total spend regardless of fan-out.
|
|
157
157
|
|
|
158
|
-
##
|
|
158
|
+
## Run logs
|
|
159
159
|
|
|
160
160
|
- **Run logs** (`runLog`): always-on by default. Each run writes a JSONL trail to `.rlm/runs/`
|
|
161
161
|
(default), capped at `maxRuns` (50). Supports **snapshots** (`sandbox.pkl`) and **resume**
|
|
162
162
|
of interrupted runs via `/rlm-resume`. Snapshots are protected by a per-session `nonce`
|
|
163
163
|
to prevent cross-session replay.
|
|
164
|
-
- **MLflow tracing** (`telemetry`): optional. Set `MLFLOW_TRACKING_URI` or configure
|
|
165
|
-
`trackingUri` / `experimentId` in `/rlm-config`. The root run is tagged as an MLflow span
|
|
166
|
-
for trace correlation on resume. The Bearer token comes from the `MLFLOW_TRACKING_TOKEN`
|
|
167
|
-
env var and is **never persisted** to `rlm.json`.
|
|
168
164
|
|
|
169
165
|
## Security
|
|
170
166
|
|
|
@@ -191,11 +187,10 @@ src/
|
|
|
191
187
|
core/ engine.ts (the loop) · iteration · limits · answer · compaction · pipeline · types
|
|
192
188
|
prompts/ system + per-turn prompts (ported from the Python reference)
|
|
193
189
|
text/ parsing (repl blocks) · tokens · preview · edits
|
|
194
|
-
state/
|
|
190
|
+
state/ reads/writes · resume · paths · rows
|
|
195
191
|
tool/ repl-tool · rlm-events · aggregator · propose-edits · emitter-listener
|
|
196
192
|
config/ defaults · settings (rlm.json persistence + validation)
|
|
197
193
|
context/ repomix-based repository packing + caching
|
|
198
|
-
telemetry/ MLflow sink · dispatcher · mlflow-config
|
|
199
194
|
ui/ tree-widget · status · model-picker · config-panel · intro · theme
|
|
200
195
|
commands/ rlm · rlm-config
|
|
201
196
|
mode/ rlm-mode (controller) · input-router
|
package/README.ru.md
CHANGED
|
@@ -133,10 +133,9 @@ rm -rf ~/.pi/agent/extensions/rlm
|
|
|
133
133
|
|
|
134
134
|
> **Примечание по параллелизму:** каждый дочерний `rlm_query` запускает собственного worker `python3` (~50–150 мс «холодного старта»). В худшем случае количество параллельных интерпретаторов ≈ `maxConcurrentSubcalls`^(depth−1); при настройках по умолчанию (глубина 4, параллелизм 4) это 4³ = 64 в патологическом случае. Лимиты бюджета и ошибок (см. выше) ограничивают общие затраты независимо от степени разветвления.
|
|
135
135
|
|
|
136
|
-
##
|
|
136
|
+
## Логи запусков
|
|
137
137
|
|
|
138
138
|
- **Логи запусков** (`runLog`): включены по умолчанию. Каждый запуск записывает след в формате JSONL в `.rlm/runs/` (по умолчанию) с ограничением `maxRuns` (50). Поддерживает **снимки** (`sandbox.pkl`) и **возобновление** прерванных запусков через `/rlm-resume`. Снимки защищены сессионным `nonce` для предотвращения повторов между сессиями.
|
|
139
|
-
- **Трассировка MLflow** (`telemetry`): опционально. Установите `MLFLOW_TRACKING_URI` или настройте `trackingUri` / `experimentId` в `/rlm-config`. Корневой запуск помечается как span MLflow для корреляции трасс при возобновлении. Bearer-токен берется из переменной окружения `MLFLOW_TRACKING_TOKEN` и **никогда не сохраняется** в `rlm.json`.
|
|
140
139
|
|
|
141
140
|
## Безопасность
|
|
142
141
|
|
|
@@ -155,11 +154,10 @@ src/
|
|
|
155
154
|
core/ engine.ts (цикл) · iteration · limits · answer · compaction · pipeline · types
|
|
156
155
|
prompts/ системные промпты и промпты для каждого шага (перенесены из Python-референса)
|
|
157
156
|
text/ парсинг (repl-блоки) · токены · превью · правки
|
|
158
|
-
state/
|
|
157
|
+
state/ чтения/записи · возобновление · пути · строки
|
|
159
158
|
tool/ repl-tool · rlm-events · агрегатор · предложение-правок · emitter-listener
|
|
160
159
|
config/ значения по умолчанию · настройки (сохранение и валидация rlm.json)
|
|
161
160
|
context/ упаковка репозитория на базе repomix + кеширование
|
|
162
|
-
telemetry/ MLflow sink · диспетчер · mlflow-config
|
|
163
161
|
ui/ виджет-дерева · статус · выбор-модели · панель-конфигурации · вступление · тема
|
|
164
162
|
commands/ rlm · rlm-config
|
|
165
163
|
mode/ rlm-mode (контроллер) · маршрутизатор-ввода
|
package/README.zh-CN.md
CHANGED
|
@@ -143,15 +143,11 @@ rm -rf ~/.pi/agent/extensions/rlm
|
|
|
143
143
|
> 默认设置下 (深度 4, 并发 4),极端情况下为 4³ = 64。预算和错误
|
|
144
144
|
> 上限 (见上文) 无论扇出 (fan-out) 如何都会限制总支出。
|
|
145
145
|
|
|
146
|
-
##
|
|
146
|
+
## 运行日志
|
|
147
147
|
|
|
148
148
|
- **运行日志** (`runLog`):默认始终开启。每次运行将 JSONL 轨迹写入 `.rlm/runs/`
|
|
149
149
|
(默认),上限为 `maxRuns` (50)。支持通过 `/rlm-resume` 进行**快照** (`sandbox.pkl`) 和**恢复**
|
|
150
150
|
被中断的任务。快照受每个会话的 `nonce` 保护,以防止跨会话重放。
|
|
151
|
-
- **MLflow 追踪** (`telemetry`):可选。设置 `MLFLOW_TRACKING_URI` 或在
|
|
152
|
-
`/rlm-config` 中配置 `trackingUri` / `experimentId`。根运行被标记为 MLflow span
|
|
153
|
-
以便在恢复时进行追踪关联。Bearer 令牌来自 `MLFLOW_TRACKING_TOKEN`
|
|
154
|
-
环境变量,且**绝不会**持久化到 `rlm.json`。
|
|
155
151
|
|
|
156
152
|
## 安全性
|
|
157
153
|
|
|
@@ -178,11 +174,10 @@ src/
|
|
|
178
174
|
core/ engine.ts (the loop) · iteration · limits · answer · compaction · pipeline · types
|
|
179
175
|
prompts/ system + per-turn prompts (ported from the Python reference)
|
|
180
176
|
text/ parsing (repl blocks) · tokens · preview · edits
|
|
181
|
-
state/
|
|
177
|
+
state/ reads/writes · resume · paths · rows
|
|
182
178
|
tool/ repl-tool · rlm-events · aggregator · propose-edits · emitter-listener
|
|
183
179
|
config/ defaults · settings (rlm.json persistence + validation)
|
|
184
180
|
context/ repomix-based repository packing + caching
|
|
185
|
-
telemetry/ MLflow sink · dispatcher · mlflow-config
|
|
186
181
|
ui/ tree-widget · status · model-picker · config-panel · intro · theme
|
|
187
182
|
commands/ rlm · rlm-config
|
|
188
183
|
mode/ rlm-mode (controller) · input-router
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hicaru/pi-rlm",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Recursive Language Model (RLM) for the Pi coding agent — native, server-less.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -13,17 +13,17 @@
|
|
|
13
13
|
"bugs": {
|
|
14
14
|
"url": "https://github.com/hicaru/rlm.pi/issues"
|
|
15
15
|
},
|
|
16
|
-
"exports": {
|
|
17
|
-
".": "./src/index.ts"
|
|
18
|
-
},
|
|
19
16
|
"files": [
|
|
20
|
-
"src",
|
|
17
|
+
"src/",
|
|
21
18
|
"README.md",
|
|
22
19
|
"LICENSE"
|
|
23
20
|
],
|
|
24
21
|
"keywords": [
|
|
25
22
|
"pi-package",
|
|
26
|
-
"pi-extension"
|
|
23
|
+
"pi-extension",
|
|
24
|
+
"rlm",
|
|
25
|
+
"recursive",
|
|
26
|
+
"ai-agent"
|
|
27
27
|
],
|
|
28
28
|
"scripts": {
|
|
29
29
|
"check": "tsc --noEmit",
|
|
@@ -38,14 +38,12 @@
|
|
|
38
38
|
"access": "public"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
|
-
"@earendil-works/pi-ai": "
|
|
42
|
-
"@earendil-works/pi-coding-agent": "
|
|
43
|
-
"@earendil-works/pi-tui": "
|
|
41
|
+
"@earendil-works/pi-ai": "*",
|
|
42
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
43
|
+
"@earendil-works/pi-tui": "*",
|
|
44
44
|
"typebox": "*"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@mlflow/core": "~0.2.0",
|
|
48
|
-
"diff": "^9.0.0",
|
|
49
47
|
"repomix": "^1.15.0"
|
|
50
48
|
},
|
|
51
49
|
"devDependencies": {
|
|
@@ -4,29 +4,6 @@ import type { AskAnswer, AskQuestion } from "../sandbox/protocol.ts";
|
|
|
4
4
|
import { formatError } from "../util/errors.ts";
|
|
5
5
|
import { createTodoFallback } from "./fallback-todo.ts";
|
|
6
6
|
|
|
7
|
-
interface ToolInvoker {
|
|
8
|
-
readonly callTool?: (name: string, params: unknown) => Promise<unknown>;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function hasAnswers(value: unknown): value is { readonly answers: readonly unknown[] } {
|
|
12
|
-
return typeof value === "object" && value !== null && Array.isArray((value as { readonly answers?: unknown }).answers);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function isAskAnswer(value: unknown): value is AskAnswer {
|
|
16
|
-
if (typeof value !== "object" || value === null) return false;
|
|
17
|
-
const candidate = value as { readonly question?: unknown; readonly selected?: unknown; readonly custom?: unknown };
|
|
18
|
-
return typeof candidate.question === "string"
|
|
19
|
-
&& Array.isArray(candidate.selected)
|
|
20
|
-
&& candidate.selected.every((item) => typeof item === "string")
|
|
21
|
-
&& (candidate.custom === undefined || typeof candidate.custom === "string");
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function normalizeAnswers(result: unknown): AskAnswer[] | undefined {
|
|
25
|
-
if (!hasAnswers(result)) return undefined;
|
|
26
|
-
const answers = result.answers;
|
|
27
|
-
return answers.every(isAskAnswer) ? Array.from(answers) : undefined;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
7
|
async function askViaUi(ctx: ExtensionContext, questions: readonly AskQuestion[]): Promise<AskAnswer[]> {
|
|
31
8
|
if (!ctx.hasUI) throw new Error("ask_user_question requires UI");
|
|
32
9
|
const answers = new Array<AskAnswer>(questions.length);
|
|
@@ -57,30 +34,8 @@ async function askViaUi(ctx: ExtensionContext, questions: readonly AskQuestion[]
|
|
|
57
34
|
export function createPiInteractiveDeps(ctx: ExtensionContext): InteractiveDeps {
|
|
58
35
|
const fallbackTodo = createTodoFallback();
|
|
59
36
|
return Object.freeze({
|
|
60
|
-
onAskUserQuestion:
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
try {
|
|
64
|
-
const result = await callTool.call(ctx, "ask_user_question", { questions });
|
|
65
|
-
const answers = normalizeAnswers(result);
|
|
66
|
-
if (answers) return answers;
|
|
67
|
-
} catch {
|
|
68
|
-
// Fall through to native UI fallback when the extension tool is not registered or fails.
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
return askViaUi(ctx, questions);
|
|
72
|
-
},
|
|
73
|
-
onTodo: async (action: string, params: Record<string, unknown>): Promise<string> => {
|
|
74
|
-
const callTool = (ctx as unknown as ToolInvoker).callTool;
|
|
75
|
-
if (typeof callTool === "function") {
|
|
76
|
-
try {
|
|
77
|
-
const result = await callTool.call(ctx, "todo", { action, ...params });
|
|
78
|
-
return typeof result === "string" ? result : JSON.stringify(result);
|
|
79
|
-
} catch {
|
|
80
|
-
// Fall through to in-process task store when the extension tool is not registered or fails.
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
return fallbackTodo(action, params);
|
|
84
|
-
},
|
|
37
|
+
onAskUserQuestion: (questions: readonly AskQuestion[]): Promise<AskAnswer[]> => askViaUi(ctx, questions),
|
|
38
|
+
onTodo: (action: string, params: Record<string, unknown>): Promise<string> =>
|
|
39
|
+
Promise.resolve(fallbackTodo(action, params)),
|
|
85
40
|
});
|
|
86
41
|
}
|
|
@@ -11,15 +11,22 @@ export async function runRlmConfig(controller: RlmController, ctx: ExtensionCont
|
|
|
11
11
|
const models = ctx.modelRegistry.getAvailable();
|
|
12
12
|
|
|
13
13
|
const worker = await selectModel(ctx, "Worker model (sub-LLM / llm_query)", models, controller.workerModel, controller.config.subSampling.reasoning);
|
|
14
|
-
if (worker) {
|
|
14
|
+
if (worker === null) {
|
|
15
|
+
controller.workerModel = undefined;
|
|
16
|
+
controller.config.subSampling.reasoning = undefined;
|
|
17
|
+
} else if (worker) {
|
|
15
18
|
controller.workerModel = worker.model;
|
|
16
19
|
controller.config.subSampling.reasoning = worker.thinkingLevel;
|
|
17
20
|
}
|
|
18
21
|
|
|
19
22
|
await showConfigPanel(ctx, controller.config);
|
|
20
23
|
|
|
21
|
-
|
|
22
|
-
|
|
24
|
+
if (worker === null) {
|
|
25
|
+
controller.savedWorkerRef = undefined;
|
|
26
|
+
} else {
|
|
27
|
+
const effectiveWorker = controller.workerModel ?? cheapestModel(ctx.modelRegistry);
|
|
28
|
+
controller.savedWorkerRef = modelRef(controller.workerModel) ?? modelRef(effectiveWorker);
|
|
29
|
+
}
|
|
23
30
|
const persisted = await controller.persist();
|
|
24
31
|
if (!persisted) ctx.ui.notify("RLM: failed to save settings to ~/.pi/agent/rlm.json", "error");
|
|
25
32
|
setRlmModeStatus(ctx.ui, controller);
|
package/src/commands/rlm.ts
CHANGED
|
@@ -13,9 +13,6 @@ import type { RunHeader } from "../state/rows.ts";
|
|
|
13
13
|
import { buildRlmSystemPrompt } from "../prompts/system.ts";
|
|
14
14
|
import { RlmEmitter } from "../tool/rlm-events.ts";
|
|
15
15
|
import { RlmEventAggregator } from "../tool/rlm-aggregator.ts";
|
|
16
|
-
import { createTelemetrySink } from "../telemetry/index.ts";
|
|
17
|
-
import { applyEdits } from "../patch/index.ts";
|
|
18
|
-
import { tryExtractDiff } from "../core/answer.ts";
|
|
19
16
|
|
|
20
17
|
export function registerRlmCommand(pi: ExtensionAPI, controller: RlmController): void {
|
|
21
18
|
pi.registerCommand("rlm", {
|
|
@@ -112,14 +109,10 @@ async function executeRlmRunWithResume(
|
|
|
112
109
|
context: unknown,
|
|
113
110
|
): Promise<void> {
|
|
114
111
|
let handle: RunHandle | undefined;
|
|
115
|
-
let sink: Awaited<ReturnType<typeof createTelemetrySink>>;
|
|
116
112
|
let emitter: RlmEmitter | undefined;
|
|
117
|
-
let detachSink: (() => void) | undefined;
|
|
118
113
|
let aggregator: RlmEventAggregator | undefined;
|
|
119
114
|
try {
|
|
120
|
-
sink = await createTelemetrySink(controller.config.telemetry);
|
|
121
115
|
emitter = new RlmEmitter();
|
|
122
|
-
if (sink) detachSink = emitter.attachSink(sink);
|
|
123
116
|
aggregator = new RlmEventAggregator(emitter, (partial) => {
|
|
124
117
|
const d = partial.details;
|
|
125
118
|
if (!d) return;
|
|
@@ -146,20 +139,12 @@ async function executeRlmRunWithResume(
|
|
|
146
139
|
try {
|
|
147
140
|
const result = await done;
|
|
148
141
|
pi.sendMessage({ customType: "rlm-answer", content: result.answer, display: true });
|
|
149
|
-
const proposedDiffs = result.diffs?.length ? result.diffs : tryExtractDiff(result.answer);
|
|
150
|
-
await applyEdits(
|
|
151
|
-
result.edits ?? [],
|
|
152
|
-
proposedDiffs,
|
|
153
|
-
ctx,
|
|
154
|
-
);
|
|
155
142
|
} catch (e) {
|
|
156
143
|
ctx.ui.notify(`RLM resume failed: ${e instanceof Error ? e.message : String(e)}`, "error");
|
|
157
144
|
} finally {
|
|
158
145
|
clearRlmStatus(ctx.ui);
|
|
159
146
|
ctx.ui.setWidget?.("rlm-status", undefined);
|
|
160
|
-
detachSink?.();
|
|
161
147
|
aggregator?.dispose();
|
|
162
148
|
emitter?.shutdown();
|
|
163
|
-
try { await sink?.shutdown(); } catch { /* best-effort */ }
|
|
164
149
|
}
|
|
165
150
|
}
|
package/src/config/settings.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
5
|
import { getAgentDir, type ModelRegistry } from "@earendil-works/pi-coding-agent";
|
|
6
6
|
import type { Api, Model, ThinkingLevel } from "@earendil-works/pi-ai";
|
|
7
|
-
import type { RlmConfig, RunLogConfig
|
|
7
|
+
import type { RlmConfig, RunLogConfig } from "../core/types.ts";
|
|
8
8
|
import { DEFAULT_CONFIG } from "./defaults.ts";
|
|
9
9
|
|
|
10
10
|
export interface PersistedSettings {
|
|
@@ -30,26 +30,6 @@ function validateString(v: unknown): string | undefined {
|
|
|
30
30
|
return typeof v === "string" && v.trim() ? v : undefined;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
function validateTelemetry(raw: unknown): TelemetryConfig | undefined {
|
|
34
|
-
if (typeof raw !== "object" || raw === null) return undefined;
|
|
35
|
-
const r = raw as Record<string, unknown>;
|
|
36
|
-
const out: {
|
|
37
|
-
enabled?: boolean;
|
|
38
|
-
trackingUri?: string;
|
|
39
|
-
experimentId?: string;
|
|
40
|
-
maxQueueSize?: number;
|
|
41
|
-
} = {};
|
|
42
|
-
const enabled = validateBoolean(r.enabled);
|
|
43
|
-
if (enabled !== undefined) out.enabled = enabled;
|
|
44
|
-
const trackingUri = validateString(r.trackingUri);
|
|
45
|
-
if (trackingUri !== undefined) out.trackingUri = trackingUri;
|
|
46
|
-
const experimentId = validateString(r.experimentId);
|
|
47
|
-
if (experimentId !== undefined) out.experimentId = experimentId;
|
|
48
|
-
const maxQueueSize = validateNumber(r.maxQueueSize, 1);
|
|
49
|
-
if (maxQueueSize !== undefined) out.maxQueueSize = maxQueueSize;
|
|
50
|
-
return Object.freeze(out);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
33
|
function validateRunLog(raw: unknown): Partial<RunLogConfig> | undefined {
|
|
54
34
|
if (typeof raw !== "object" || raw === null) return undefined;
|
|
55
35
|
const r = raw as Record<string, unknown>;
|
|
@@ -102,8 +82,6 @@ function validateConfig(raw: unknown): Partial<RlmConfig> {
|
|
|
102
82
|
if (typeof r.smartReasoning === "string") out.smartReasoning = r.smartReasoning as ThinkingLevel;
|
|
103
83
|
const subSystemPrompt = validateString(r.subSystemPrompt);
|
|
104
84
|
if (subSystemPrompt !== undefined) out.subSystemPrompt = subSystemPrompt;
|
|
105
|
-
const telemetry = validateTelemetry(r.telemetry);
|
|
106
|
-
if (telemetry) out.telemetry = telemetry;
|
|
107
85
|
const runLog = validateRunLog(r.runLog);
|
|
108
86
|
if (runLog) out.runLog = runLog;
|
|
109
87
|
const sandboxInitTimeoutMs = validateNumber(r.sandboxInitTimeoutMs, 100);
|
|
@@ -167,7 +145,6 @@ export function mergeConfig(partial: Partial<RlmConfig>): RlmConfig {
|
|
|
167
145
|
...partial,
|
|
168
146
|
subSampling: { ...DEFAULT_CONFIG.subSampling, ...partial.subSampling },
|
|
169
147
|
rootSampling: Object.freeze({ ...DEFAULT_CONFIG.rootSampling, ...partial.rootSampling }),
|
|
170
|
-
...(partial.telemetry ? { telemetry: Object.freeze({ ...partial.telemetry }) } : {}),
|
|
171
148
|
...(partial.runLog ? { runLog: Object.freeze({ ...DEFAULT_CONFIG.runLog, ...partial.runLog }) } : {}),
|
|
172
149
|
};
|
|
173
150
|
}
|
package/src/core/answer.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Helpers for detecting and formatting the RLM final answer from a turn's REPL results. */
|
|
2
2
|
|
|
3
|
-
import type {
|
|
3
|
+
import type { ProposedEdit, ReplResult } from "../sandbox/protocol.ts";
|
|
4
4
|
import { truncateOutput } from "../text/parsing.ts";
|
|
5
5
|
|
|
6
6
|
/** First non-null final answer across a turn's executed blocks, or null. */
|
|
@@ -27,22 +27,6 @@ export function collectEdits(results: readonly ReplResult[]): ProposedEdit[] {
|
|
|
27
27
|
return [];
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
/** Last cumulative diff proposal set reported by a turn. */
|
|
31
|
-
export function collectDiffs(results: readonly ReplResult[]): ProposedDiffEdit[] {
|
|
32
|
-
for (let i = results.length - 1; i >= 0; i--) {
|
|
33
|
-
const diffs = results[i]?.diffs;
|
|
34
|
-
if (diffs && diffs.length > 0) return [...diffs];
|
|
35
|
-
}
|
|
36
|
-
return [];
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/** Parses a ```diff fence from answer text; returns [] if none found. */
|
|
40
|
-
export function tryExtractDiff(answer: string): ProposedDiffEdit[] {
|
|
41
|
-
const match = /```diff\n([\s\S]*?)```/.exec(answer);
|
|
42
|
-
if (!match) return [];
|
|
43
|
-
return [{ diff: match[1].trim() }];
|
|
44
|
-
}
|
|
45
|
-
|
|
46
30
|
/** True if any block in the turn raised an exception. Plain stderr does not count. */
|
|
47
31
|
export function turnHadError(results: readonly ReplResult[]): boolean {
|
|
48
32
|
return results.some((r) => r.raised);
|
package/src/core/engine.ts
CHANGED
|
@@ -21,7 +21,7 @@ import { PythonSandbox } from "../sandbox/sandbox.ts";
|
|
|
21
21
|
import { advancePhase as validatePhaseTransition, phaseGatePrompt, type PhaseState } from "./pipeline.ts";
|
|
22
22
|
import { previewStdout, previewText } from "../text/preview.ts";
|
|
23
23
|
import { contextLength, contextSizeStats, contextTypeLabel } from "../text/tokens.ts";
|
|
24
|
-
import {
|
|
24
|
+
import { collectEdits, finalAnswerOf, formatReplOutputs, latestAnswerContentOf, turnHadError } from "./answer.ts";
|
|
25
25
|
import { compactHistory, shouldCompact } from "./compaction.ts";
|
|
26
26
|
import { appendUserMessage } from "./history.ts";
|
|
27
27
|
import { runTurn } from "./iteration.ts";
|
|
@@ -32,7 +32,7 @@ import { appendRow, appendTodoRow, generateRunId, pruneRuns, snapshotPath, write
|
|
|
32
32
|
import { STATE_SCHEMA_VERSION } from "../state/rows.ts";
|
|
33
33
|
import type { PhaseRow, RunHeader } from "../state/rows.ts";
|
|
34
34
|
import { serializeForSandbox, type ContextBundle } from "../context/repomix-context.ts";
|
|
35
|
-
import type {
|
|
35
|
+
import type { ProposedEdit } from "../sandbox/protocol.ts";
|
|
36
36
|
import { formatError } from "../util/errors.ts";
|
|
37
37
|
|
|
38
38
|
|
|
@@ -57,7 +57,7 @@ export function createEngine(deps: EngineDeps): RunRlm {
|
|
|
57
57
|
const run: RunRlm = async (input: RlmInput): Promise<RlmResult> => {
|
|
58
58
|
const nowIso = (): string => new Date().toISOString(); // local helper — 4 call sites below
|
|
59
59
|
const persist = input.depth === 0 && deps.runState !== undefined;
|
|
60
|
-
// Compute runId early
|
|
60
|
+
// Compute runId early for run-state correlation on resume.
|
|
61
61
|
const runId = persist
|
|
62
62
|
? (input.resume ? input.resume.header.runId : generateRunId())
|
|
63
63
|
: undefined;
|
|
@@ -136,7 +136,6 @@ export function createEngine(deps: EngineDeps): RunRlm {
|
|
|
136
136
|
let compactions = 0;
|
|
137
137
|
let completedTurns = 0;
|
|
138
138
|
let editsAcc: ProposedEdit[] = [];
|
|
139
|
-
let diffsAcc: ProposedDiffEdit[] = [];
|
|
140
139
|
let phaseState: PhaseState | undefined;
|
|
141
140
|
let nodeStatus: "done" | "error" = "done";
|
|
142
141
|
let persistOn = persist;
|
|
@@ -228,7 +227,6 @@ export function createEngine(deps: EngineDeps): RunRlm {
|
|
|
228
227
|
limits.addRaw(input.resume.usageSeed.costUsd, input.resume.usageSeed.inputTokens, input.resume.usageSeed.outputTokens);
|
|
229
228
|
best = input.resume.best;
|
|
230
229
|
editsAcc = [];
|
|
231
|
-
diffsAcc = [];
|
|
232
230
|
compactions = input.resume.compactions;
|
|
233
231
|
completedTurns = input.resume.completedTurns;
|
|
234
232
|
if (input.resume.phase) {
|
|
@@ -312,12 +310,9 @@ export function createEngine(deps: EngineDeps): RunRlm {
|
|
|
312
310
|
completedTurns = i + 1;
|
|
313
311
|
const proposedEdits = collectEdits(turn.results);
|
|
314
312
|
if (proposedEdits.length > 0) editsAcc = proposedEdits;
|
|
315
|
-
const proposedDiffs = collectDiffs(turn.results);
|
|
316
|
-
if (proposedDiffs.length > 0) diffsAcc = proposedDiffs;
|
|
317
|
-
|
|
318
313
|
const final = finalAnswerOf(turn.results);
|
|
319
314
|
if (final != null) {
|
|
320
|
-
const done = result(final, i + 1, limits, editsAcc
|
|
315
|
+
const done = result(final, i + 1, limits, editsAcc);
|
|
321
316
|
await recordTerminal("completed", done);
|
|
322
317
|
lastAnswer = done.answer;
|
|
323
318
|
return done;
|
|
@@ -348,21 +343,21 @@ export function createEngine(deps: EngineDeps): RunRlm {
|
|
|
348
343
|
}
|
|
349
344
|
}
|
|
350
345
|
if (pendingReplOutputs) appendUserMessage(history, pendingReplOutputs);
|
|
351
|
-
const finalized = result(await finalize(history, deps, limits), deps.config.maxIterations, limits, editsAcc
|
|
346
|
+
const finalized = result(await finalize(history, deps, limits), deps.config.maxIterations, limits, editsAcc);
|
|
352
347
|
await recordTerminal("finalized", finalized);
|
|
353
348
|
lastAnswer = finalized.answer;
|
|
354
349
|
return finalized;
|
|
355
350
|
} catch (err) {
|
|
356
351
|
// Abort is a user action — resolve with the best partial, not an error.
|
|
357
352
|
if (deps.signal?.aborted) {
|
|
358
|
-
const aborted = result(best.trim() || "(aborted)", completedTurns, limits, editsAcc
|
|
353
|
+
const aborted = result(best.trim() || "(aborted)", completedTurns, limits, editsAcc);
|
|
359
354
|
await recordTerminal("aborted", aborted);
|
|
360
355
|
lastAnswer = aborted.answer;
|
|
361
356
|
return aborted;
|
|
362
357
|
}
|
|
363
358
|
if (err instanceof LimitError) {
|
|
364
359
|
nodeStatus = "error";
|
|
365
|
-
const stopped = result(best.trim() || `(stopped: ${err.message})`, completedTurns, limits, editsAcc
|
|
360
|
+
const stopped = result(best.trim() || `(stopped: ${err.message})`, completedTurns, limits, editsAcc);
|
|
366
361
|
await recordTerminal("stopped", stopped);
|
|
367
362
|
lastAnswer = stopped.answer;
|
|
368
363
|
return stopped;
|
|
@@ -388,9 +383,9 @@ export function createEngine(deps: EngineDeps): RunRlm {
|
|
|
388
383
|
return run;
|
|
389
384
|
}
|
|
390
385
|
|
|
391
|
-
function result(answer: string, iterations: number, limits: LimitGuard, edits: ProposedEdit[] = []
|
|
386
|
+
function result(answer: string, iterations: number, limits: LimitGuard, edits: ProposedEdit[] = []): RlmResult {
|
|
392
387
|
const u = limits.usage();
|
|
393
|
-
return { answer, edits,
|
|
388
|
+
return { answer, edits, iterations, costUsd: u.costUsd, inputTokens: u.inputTokens, outputTokens: u.outputTokens, durationMs: u.durationMs };
|
|
394
389
|
}
|
|
395
390
|
|
|
396
391
|
/** Out of turns: ask the model for its best final answer (plain text). */
|
package/src/core/types.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** Shared configuration + runtime types for the RLM engine. */
|
|
2
2
|
|
|
3
3
|
import type { ThinkingLevel } from "@earendil-works/pi-ai";
|
|
4
|
-
import type { AskAnswer, AskQuestion,
|
|
4
|
+
import type { AskAnswer, AskQuestion, ProposedEdit } from "../sandbox/protocol.ts";
|
|
5
5
|
import type { ReconstructResult } from "../state/resume.ts";
|
|
6
6
|
|
|
7
7
|
export interface Sampling {
|
|
@@ -12,15 +12,6 @@ export interface Sampling {
|
|
|
12
12
|
|
|
13
13
|
type MutableSampling = { -readonly [Key in keyof Sampling]?: Sampling[Key] };
|
|
14
14
|
|
|
15
|
-
export interface TelemetryConfig {
|
|
16
|
-
/** Default: enabled iff a tracking URI resolves from config or MLFLOW_TRACKING_URI. */
|
|
17
|
-
readonly enabled?: boolean;
|
|
18
|
-
readonly trackingUri?: string;
|
|
19
|
-
readonly experimentId?: string;
|
|
20
|
-
/** Bearer token is env-only via MLFLOW_TRACKING_TOKEN; never persisted in rlm.json. */
|
|
21
|
-
readonly maxQueueSize?: number;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
15
|
export interface RunLogConfig {
|
|
25
16
|
/** Default: true — always-on, opt-out. */
|
|
26
17
|
readonly enabled?: boolean;
|
|
@@ -81,8 +72,6 @@ export interface RlmConfig {
|
|
|
81
72
|
subSystemPrompt?: string;
|
|
82
73
|
/** Sampling for sub-LLM (worker) calls. */
|
|
83
74
|
subSampling: MutableSampling;
|
|
84
|
-
/** Optional MLflow telemetry export configuration. Omitted by default. */
|
|
85
|
-
readonly telemetry?: TelemetryConfig;
|
|
86
75
|
/** Optional run-state persistence configuration. Enabled by default. */
|
|
87
76
|
readonly runLog?: RunLogConfig;
|
|
88
77
|
}
|
|
@@ -112,7 +101,6 @@ export interface RlmResult {
|
|
|
112
101
|
readonly answer: string;
|
|
113
102
|
/** Legacy anchor edits retained for compatibility while older run-state rows exist. */
|
|
114
103
|
readonly edits?: readonly ProposedEdit[];
|
|
115
|
-
readonly diffs?: readonly ProposedDiffEdit[];
|
|
116
104
|
readonly iterations: number;
|
|
117
105
|
readonly costUsd: number;
|
|
118
106
|
readonly inputTokens: number;
|
package/src/index.ts
CHANGED
|
@@ -6,7 +6,6 @@ import { Markdown } from "@earendil-works/pi-tui";
|
|
|
6
6
|
import { registerRlmCommand } from "./commands/rlm.ts";
|
|
7
7
|
import { registerRlmConfigCommand } from "./commands/rlm-config.ts";
|
|
8
8
|
import { createRlmTool } from "./tool/rlm-tool.ts";
|
|
9
|
-
import { createApplyDiffTool } from "./tool/apply-diff-tool.ts";
|
|
10
9
|
import { createReplTool } from "./tool/repl-tool.ts";
|
|
11
10
|
import { loadSettings, mergeConfig, resolveModelId } from "./config/settings.ts";
|
|
12
11
|
import { RlmController, cheapestModel } from "./mode/rlm-mode.ts";
|
|
@@ -17,21 +16,12 @@ import { packRepository, formatForLLM, serializeForSandbox } from "./context/rep
|
|
|
17
16
|
import { buildNativeSystemPrompt } from "./prompts/system.ts";
|
|
18
17
|
import { errorMessage } from "./util/errors.ts";
|
|
19
18
|
|
|
20
|
-
const BLOCKED_NATIVE_TOOLS = Object.freeze(new Set(["read", "grep", "bash"
|
|
19
|
+
const BLOCKED_NATIVE_TOOLS = Object.freeze(new Set(["read", "grep", "bash"]));
|
|
21
20
|
|
|
22
21
|
export default function rlmExtension(pi: ExtensionAPI): void {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
async function setupRlmExtension(pi: ExtensionAPI): Promise<void> {
|
|
29
|
-
const persisted = await loadSettings();
|
|
30
|
-
const config = mergeConfig(persisted.config);
|
|
22
|
+
// Init synchronously with defaults — ensures commands/tools/handlers register before session_start
|
|
23
|
+
const config = mergeConfig({});
|
|
31
24
|
const controller = new RlmController(config);
|
|
32
|
-
controller.savedWorkerRef = persisted.worker;
|
|
33
|
-
|
|
34
|
-
// ── SandboxManager — persistent singleton for native-mode repl() ──
|
|
35
25
|
const sandboxManager = new SandboxManager({
|
|
36
26
|
execTimeoutS: config.execTimeoutS,
|
|
37
27
|
requestTimeoutMs: config.requestTimeoutMs,
|
|
@@ -39,6 +29,16 @@ async function setupRlmExtension(pi: ExtensionAPI): Promise<void> {
|
|
|
39
29
|
sandboxInitTimeoutMs: config.sandboxInitTimeoutMs,
|
|
40
30
|
});
|
|
41
31
|
|
|
32
|
+
// Load persisted settings async — applied before session_start handler reads controller state
|
|
33
|
+
const settingsReady = loadSettings()
|
|
34
|
+
.then((persisted) => {
|
|
35
|
+
controller.config = mergeConfig(persisted.config);
|
|
36
|
+
controller.savedWorkerRef = persisted.worker;
|
|
37
|
+
})
|
|
38
|
+
.catch((err) => {
|
|
39
|
+
console.warn(`[rlm] settings load failed: ${errorMessage(err)}`);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
42
|
// ── Message renderers ──
|
|
43
43
|
pi.registerMessageRenderer(
|
|
44
44
|
"rlm-answer",
|
|
@@ -56,23 +56,19 @@ async function setupRlmExtension(pi: ExtensionAPI): Promise<void> {
|
|
|
56
56
|
registerRlmConfigCommand(pi, controller);
|
|
57
57
|
|
|
58
58
|
// ── Tool registration ──
|
|
59
|
-
// Existing rlm tool (stays for backward compat with /rlm mode)
|
|
60
59
|
pi.registerTool(createRlmTool(controller));
|
|
61
|
-
pi.registerTool(createApplyDiffTool());
|
|
62
|
-
|
|
63
|
-
// Native repl tool — re-registered each session to pick up model provider changes
|
|
64
60
|
let guidePosted = false;
|
|
65
61
|
|
|
66
62
|
pi.on("session_start", async (_event, ctx) => {
|
|
67
|
-
//
|
|
68
|
-
|
|
69
|
-
|
|
63
|
+
// Wait for persisted settings before reading controller state
|
|
64
|
+
await settingsReady;
|
|
65
|
+
|
|
70
66
|
if (controller.savedWorkerRef) {
|
|
71
67
|
const resolved = resolveModelId(ctx.modelRegistry, controller.savedWorkerRef);
|
|
72
68
|
if (resolved) controller.workerModel = resolved;
|
|
73
69
|
}
|
|
74
70
|
|
|
75
|
-
//
|
|
71
|
+
// Re-register repl tool each session to pick up model provider changes
|
|
76
72
|
const workerModel = controller.workerModel ?? cheapestModel(ctx.modelRegistry) ?? ctx.model;
|
|
77
73
|
const model = ctx.model;
|
|
78
74
|
if (workerModel && model) {
|
|
@@ -84,7 +80,7 @@ async function setupRlmExtension(pi: ExtensionAPI): Promise<void> {
|
|
|
84
80
|
getModel: () => controller.resolveModels(ctx)?.model,
|
|
85
81
|
getWorkerModel: () => controller.resolveModels(ctx)?.worker,
|
|
86
82
|
registry: ctx.modelRegistry,
|
|
87
|
-
config,
|
|
83
|
+
config: controller.config,
|
|
88
84
|
}));
|
|
89
85
|
} catch { /* re-registration on provider change — ignore if already registered */ }
|
|
90
86
|
}
|
|
@@ -139,19 +135,20 @@ async function setupRlmExtension(pi: ExtensionAPI): Promise<void> {
|
|
|
139
135
|
});
|
|
140
136
|
|
|
141
137
|
// ── Input routing: native mode — agent decides whether to use repl() or other tools ──
|
|
142
|
-
// The old black-box rlm() routing is removed; the main agent receives messages normally
|
|
143
|
-
// and chooses natively when to call repl(), read, grep, zebra-mcp, etc.
|
|
144
138
|
pi.on("input", async (_event, _ctx) => {
|
|
145
139
|
return { action: "continue" };
|
|
146
140
|
});
|
|
147
141
|
|
|
148
|
-
// ── Tool restriction: block
|
|
142
|
+
// ── Tool restriction: block analysis tools when RLM is ON ──
|
|
143
|
+
// `edit`/`write` stay unblocked so the agent modifies files through Pi's native
|
|
144
|
+
// tool flow (visible to all plugins, +/- diff preview). Only read/grep/bash are
|
|
145
|
+
// blocked — the repository is pre-loaded in the REPL `context` variable.
|
|
149
146
|
pi.on("tool_call", async (event) => {
|
|
150
147
|
if (!controller.enabled) return;
|
|
151
148
|
if (BLOCKED_NATIVE_TOOLS.has(event.toolName)) {
|
|
152
149
|
return {
|
|
153
150
|
block: true,
|
|
154
|
-
reason: "RLM mode active. Use repl({code}) to read files and
|
|
151
|
+
reason: "RLM mode active. Use repl({code}) to read files and search the repository — all files are pre-loaded in the REPL `context` variable. Use `edit`/`write` for file changes. If sub-LLM credits are exhausted, report to the user.",
|
|
155
152
|
};
|
|
156
153
|
}
|
|
157
154
|
});
|