@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 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
- ## Telemetry & run logs
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/ agent-tree · events · reads/writes · resume · paths · rows
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/ agent-tree · events · reads/writes · resume · paths · rows
177
+ state/ reads/writes · resume · paths · rows
182
178
  tool/ repl-tool · rlm-events · aggregator · propose-edits · emitter-listener
183
179
  config/ defaults · settings (rlm.json persistence + validation)
184
180
  context/ repomix-based repository packing + caching
185
- telemetry/ MLflow sink · dispatcher · mlflow-config
186
181
  ui/ tree-widget · status · model-picker · config-panel · intro · theme
187
182
  commands/ rlm · rlm-config
188
183
  mode/ rlm-mode (controller) · input-router
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hicaru/pi-rlm",
3
- "version": "0.1.0",
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": ">=0.79.0",
42
- "@earendil-works/pi-coding-agent": ">=0.79.0",
43
- "@earendil-works/pi-tui": ">=0.79.0",
41
+ "@earendil-works/pi-ai": "*",
42
+ "@earendil-works/pi-coding-agent": "*",
43
+ "@earendil-works/pi-tui": "*",
44
44
  "typebox": "*"
45
45
  },
46
46
  "dependencies": {
47
- "@mlflow/core": "~0.2.0",
48
- "diff": "^9.0.0",
49
47
  "repomix": "^1.15.0"
50
48
  },
51
49
  "devDependencies": {
@@ -4,29 +4,6 @@ import type { AskAnswer, AskQuestion } from "../sandbox/protocol.ts";
4
4
  import { formatError } from "../util/errors.ts";
5
5
  import { createTodoFallback } from "./fallback-todo.ts";
6
6
 
7
- interface ToolInvoker {
8
- readonly callTool?: (name: string, params: unknown) => Promise<unknown>;
9
- }
10
-
11
- function hasAnswers(value: unknown): value is { readonly answers: readonly unknown[] } {
12
- return typeof value === "object" && value !== null && Array.isArray((value as { readonly answers?: unknown }).answers);
13
- }
14
-
15
- function isAskAnswer(value: unknown): value is AskAnswer {
16
- if (typeof value !== "object" || value === null) return false;
17
- const candidate = value as { readonly question?: unknown; readonly selected?: unknown; readonly custom?: unknown };
18
- return typeof candidate.question === "string"
19
- && Array.isArray(candidate.selected)
20
- && candidate.selected.every((item) => typeof item === "string")
21
- && (candidate.custom === undefined || typeof candidate.custom === "string");
22
- }
23
-
24
- function normalizeAnswers(result: unknown): AskAnswer[] | undefined {
25
- if (!hasAnswers(result)) return undefined;
26
- const answers = result.answers;
27
- return answers.every(isAskAnswer) ? Array.from(answers) : undefined;
28
- }
29
-
30
7
  async function askViaUi(ctx: ExtensionContext, questions: readonly AskQuestion[]): Promise<AskAnswer[]> {
31
8
  if (!ctx.hasUI) throw new Error("ask_user_question requires UI");
32
9
  const answers = new Array<AskAnswer>(questions.length);
@@ -57,30 +34,8 @@ async function askViaUi(ctx: ExtensionContext, questions: readonly AskQuestion[]
57
34
  export function createPiInteractiveDeps(ctx: ExtensionContext): InteractiveDeps {
58
35
  const fallbackTodo = createTodoFallback();
59
36
  return Object.freeze({
60
- onAskUserQuestion: async (questions: readonly AskQuestion[]): Promise<AskAnswer[]> => {
61
- const callTool = (ctx as unknown as ToolInvoker).callTool;
62
- if (typeof callTool === "function") {
63
- try {
64
- const result = await callTool.call(ctx, "ask_user_question", { questions });
65
- const answers = normalizeAnswers(result);
66
- if (answers) return answers;
67
- } catch {
68
- // Fall through to native UI fallback when the extension tool is not registered or fails.
69
- }
70
- }
71
- return askViaUi(ctx, questions);
72
- },
73
- onTodo: async (action: string, params: Record<string, unknown>): Promise<string> => {
74
- const callTool = (ctx as unknown as ToolInvoker).callTool;
75
- if (typeof callTool === "function") {
76
- try {
77
- const result = await callTool.call(ctx, "todo", { action, ...params });
78
- return typeof result === "string" ? result : JSON.stringify(result);
79
- } catch {
80
- // Fall through to in-process task store when the extension tool is not registered or fails.
81
- }
82
- }
83
- return fallbackTodo(action, params);
84
- },
37
+ onAskUserQuestion: (questions: readonly AskQuestion[]): Promise<AskAnswer[]> => askViaUi(ctx, questions),
38
+ onTodo: (action: string, params: Record<string, unknown>): Promise<string> =>
39
+ Promise.resolve(fallbackTodo(action, params)),
85
40
  });
86
41
  }
@@ -11,15 +11,22 @@ export async function runRlmConfig(controller: RlmController, ctx: ExtensionCont
11
11
  const models = ctx.modelRegistry.getAvailable();
12
12
 
13
13
  const worker = await selectModel(ctx, "Worker model (sub-LLM / llm_query)", models, controller.workerModel, controller.config.subSampling.reasoning);
14
- if (worker) {
14
+ if (worker === null) {
15
+ controller.workerModel = undefined;
16
+ controller.config.subSampling.reasoning = undefined;
17
+ } else if (worker) {
15
18
  controller.workerModel = worker.model;
16
19
  controller.config.subSampling.reasoning = worker.thinkingLevel;
17
20
  }
18
21
 
19
22
  await showConfigPanel(ctx, controller.config);
20
23
 
21
- const effectiveWorker = controller.workerModel ?? cheapestModel(ctx.modelRegistry);
22
- controller.savedWorkerRef = modelRef(controller.workerModel) ?? modelRef(effectiveWorker);
24
+ if (worker === null) {
25
+ controller.savedWorkerRef = undefined;
26
+ } else {
27
+ const effectiveWorker = controller.workerModel ?? cheapestModel(ctx.modelRegistry);
28
+ controller.savedWorkerRef = modelRef(controller.workerModel) ?? modelRef(effectiveWorker);
29
+ }
23
30
  const persisted = await controller.persist();
24
31
  if (!persisted) ctx.ui.notify("RLM: failed to save settings to ~/.pi/agent/rlm.json", "error");
25
32
  setRlmModeStatus(ctx.ui, controller);
@@ -13,9 +13,6 @@ import type { RunHeader } from "../state/rows.ts";
13
13
  import { buildRlmSystemPrompt } from "../prompts/system.ts";
14
14
  import { RlmEmitter } from "../tool/rlm-events.ts";
15
15
  import { RlmEventAggregator } from "../tool/rlm-aggregator.ts";
16
- import { createTelemetrySink } from "../telemetry/index.ts";
17
- import { applyEdits } from "../patch/index.ts";
18
- import { tryExtractDiff } from "../core/answer.ts";
19
16
 
20
17
  export function registerRlmCommand(pi: ExtensionAPI, controller: RlmController): void {
21
18
  pi.registerCommand("rlm", {
@@ -112,14 +109,10 @@ async function executeRlmRunWithResume(
112
109
  context: unknown,
113
110
  ): Promise<void> {
114
111
  let handle: RunHandle | undefined;
115
- let sink: Awaited<ReturnType<typeof createTelemetrySink>>;
116
112
  let emitter: RlmEmitter | undefined;
117
- let detachSink: (() => void) | undefined;
118
113
  let aggregator: RlmEventAggregator | undefined;
119
114
  try {
120
- sink = await createTelemetrySink(controller.config.telemetry);
121
115
  emitter = new RlmEmitter();
122
- if (sink) detachSink = emitter.attachSink(sink);
123
116
  aggregator = new RlmEventAggregator(emitter, (partial) => {
124
117
  const d = partial.details;
125
118
  if (!d) return;
@@ -146,20 +139,12 @@ async function executeRlmRunWithResume(
146
139
  try {
147
140
  const result = await done;
148
141
  pi.sendMessage({ customType: "rlm-answer", content: result.answer, display: true });
149
- const proposedDiffs = result.diffs?.length ? result.diffs : tryExtractDiff(result.answer);
150
- await applyEdits(
151
- result.edits ?? [],
152
- proposedDiffs,
153
- ctx,
154
- );
155
142
  } catch (e) {
156
143
  ctx.ui.notify(`RLM resume failed: ${e instanceof Error ? e.message : String(e)}`, "error");
157
144
  } finally {
158
145
  clearRlmStatus(ctx.ui);
159
146
  ctx.ui.setWidget?.("rlm-status", undefined);
160
- detachSink?.();
161
147
  aggregator?.dispose();
162
148
  emitter?.shutdown();
163
- try { await sink?.shutdown(); } catch { /* best-effort */ }
164
149
  }
165
150
  }
@@ -4,7 +4,7 @@ import { mkdir, readFile, writeFile } from "node:fs/promises";
4
4
  import { dirname, join } from "node:path";
5
5
  import { getAgentDir, type ModelRegistry } from "@earendil-works/pi-coding-agent";
6
6
  import type { Api, Model, ThinkingLevel } from "@earendil-works/pi-ai";
7
- import type { RlmConfig, RunLogConfig, TelemetryConfig } from "../core/types.ts";
7
+ import type { RlmConfig, RunLogConfig } from "../core/types.ts";
8
8
  import { DEFAULT_CONFIG } from "./defaults.ts";
9
9
 
10
10
  export interface PersistedSettings {
@@ -30,26 +30,6 @@ function validateString(v: unknown): string | undefined {
30
30
  return typeof v === "string" && v.trim() ? v : undefined;
31
31
  }
32
32
 
33
- function validateTelemetry(raw: unknown): TelemetryConfig | undefined {
34
- if (typeof raw !== "object" || raw === null) return undefined;
35
- const r = raw as Record<string, unknown>;
36
- const out: {
37
- enabled?: boolean;
38
- trackingUri?: string;
39
- experimentId?: string;
40
- maxQueueSize?: number;
41
- } = {};
42
- const enabled = validateBoolean(r.enabled);
43
- if (enabled !== undefined) out.enabled = enabled;
44
- const trackingUri = validateString(r.trackingUri);
45
- if (trackingUri !== undefined) out.trackingUri = trackingUri;
46
- const experimentId = validateString(r.experimentId);
47
- if (experimentId !== undefined) out.experimentId = experimentId;
48
- const maxQueueSize = validateNumber(r.maxQueueSize, 1);
49
- if (maxQueueSize !== undefined) out.maxQueueSize = maxQueueSize;
50
- return Object.freeze(out);
51
- }
52
-
53
33
  function validateRunLog(raw: unknown): Partial<RunLogConfig> | undefined {
54
34
  if (typeof raw !== "object" || raw === null) return undefined;
55
35
  const r = raw as Record<string, unknown>;
@@ -102,8 +82,6 @@ function validateConfig(raw: unknown): Partial<RlmConfig> {
102
82
  if (typeof r.smartReasoning === "string") out.smartReasoning = r.smartReasoning as ThinkingLevel;
103
83
  const subSystemPrompt = validateString(r.subSystemPrompt);
104
84
  if (subSystemPrompt !== undefined) out.subSystemPrompt = subSystemPrompt;
105
- const telemetry = validateTelemetry(r.telemetry);
106
- if (telemetry) out.telemetry = telemetry;
107
85
  const runLog = validateRunLog(r.runLog);
108
86
  if (runLog) out.runLog = runLog;
109
87
  const sandboxInitTimeoutMs = validateNumber(r.sandboxInitTimeoutMs, 100);
@@ -167,7 +145,6 @@ export function mergeConfig(partial: Partial<RlmConfig>): RlmConfig {
167
145
  ...partial,
168
146
  subSampling: { ...DEFAULT_CONFIG.subSampling, ...partial.subSampling },
169
147
  rootSampling: Object.freeze({ ...DEFAULT_CONFIG.rootSampling, ...partial.rootSampling }),
170
- ...(partial.telemetry ? { telemetry: Object.freeze({ ...partial.telemetry }) } : {}),
171
148
  ...(partial.runLog ? { runLog: Object.freeze({ ...DEFAULT_CONFIG.runLog, ...partial.runLog }) } : {}),
172
149
  };
173
150
  }
@@ -1,6 +1,6 @@
1
1
  /** Helpers for detecting and formatting the RLM final answer from a turn's REPL results. */
2
2
 
3
- import type { ProposedDiffEdit, ProposedEdit, ReplResult } from "../sandbox/protocol.ts";
3
+ import type { ProposedEdit, ReplResult } from "../sandbox/protocol.ts";
4
4
  import { truncateOutput } from "../text/parsing.ts";
5
5
 
6
6
  /** First non-null final answer across a turn's executed blocks, or null. */
@@ -27,22 +27,6 @@ export function collectEdits(results: readonly ReplResult[]): ProposedEdit[] {
27
27
  return [];
28
28
  }
29
29
 
30
- /** Last cumulative diff proposal set reported by a turn. */
31
- export function collectDiffs(results: readonly ReplResult[]): ProposedDiffEdit[] {
32
- for (let i = results.length - 1; i >= 0; i--) {
33
- const diffs = results[i]?.diffs;
34
- if (diffs && diffs.length > 0) return [...diffs];
35
- }
36
- return [];
37
- }
38
-
39
- /** Parses a ```diff fence from answer text; returns [] if none found. */
40
- export function tryExtractDiff(answer: string): ProposedDiffEdit[] {
41
- const match = /```diff\n([\s\S]*?)```/.exec(answer);
42
- if (!match) return [];
43
- return [{ diff: match[1].trim() }];
44
- }
45
-
46
30
  /** True if any block in the turn raised an exception. Plain stderr does not count. */
47
31
  export function turnHadError(results: readonly ReplResult[]): boolean {
48
32
  return results.some((r) => r.raised);
@@ -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 { collectDiffs, collectEdits, finalAnswerOf, formatReplOutputs, latestAnswerContentOf, turnHadError } from "./answer.ts";
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 { ProposedDiffEdit, ProposedEdit } from "../sandbox/protocol.ts";
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 so it can tag the MLflow root span (Ops: trace correlation on resume).
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, diffsAcc);
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, diffsAcc);
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, diffsAcc);
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, diffsAcc);
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[] = [], diffs: ProposedDiffEdit[] = []): RlmResult {
386
+ function result(answer: string, iterations: number, limits: LimitGuard, edits: ProposedEdit[] = []): RlmResult {
392
387
  const u = limits.usage();
393
- return { answer, edits, diffs, iterations, costUsd: u.costUsd, inputTokens: u.inputTokens, outputTokens: u.outputTokens, durationMs: u.durationMs };
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, ProposedDiffEdit, ProposedEdit } from "../sandbox/protocol.ts";
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", "write", "edit"]));
19
+ const BLOCKED_NATIVE_TOOLS = Object.freeze(new Set(["read", "grep", "bash"]));
21
20
 
22
21
  export default function rlmExtension(pi: ExtensionAPI): void {
23
- void setupRlmExtension(pi).catch((error) => {
24
- console.warn(`[rlm] extension setup failed: ${errorMessage(error)}`);
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
- // Restore saved worker ref retry lazily if registry/auth is not ready yet.
68
- // session_start can fire before the provider/auth registry is fully loaded, so a miss
69
- // here does NOT mean the ref is stale: RlmController.resolveModels() retries later.
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
- // Register repl tool with current models (re-registers each session for provider changes)
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 read/grep/bash when RLM is ON ──
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 apply_diff({diff}) to modify them. All files are pre-loaded in the REPL. If sub-LLM credits are exhausted, report to the user.",
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
  });