@hicaru/pi-rlm 0.2.1 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/README.md +12 -35
  2. package/README.ru.md +18 -23
  3. package/README.zh-CN.md +17 -28
  4. package/package.json +1 -1
  5. package/src/bridge/library.ts +61 -26
  6. package/src/bridge/subcall-handlers.ts +63 -17
  7. package/src/commands/rlm-config.ts +47 -18
  8. package/src/commands/rlm.ts +3 -152
  9. package/src/config/defaults.ts +6 -17
  10. package/src/config/settings.ts +8 -32
  11. package/src/context/library-context.ts +90 -17
  12. package/src/core/engine.ts +55 -335
  13. package/src/core/history.ts +1 -1
  14. package/src/core/limits.ts +5 -12
  15. package/src/core/resource-limits.ts +0 -2
  16. package/src/core/types.ts +3 -36
  17. package/src/index.ts +23 -12
  18. package/src/mode/llm-model.ts +54 -0
  19. package/src/mode/rlm-mode.ts +26 -57
  20. package/src/prompts/glossary.ts +287 -0
  21. package/src/prompts/native.ts +127 -0
  22. package/src/prompts/system.ts +14 -407
  23. package/src/sandbox/context-file.ts +154 -0
  24. package/src/sandbox/interrupts.ts +145 -0
  25. package/src/sandbox/protocol.ts +8 -69
  26. package/src/sandbox/py/guards.py +150 -0
  27. package/src/sandbox/py/retrieval.py +265 -0
  28. package/src/sandbox/py/tasks.py +116 -0
  29. package/src/sandbox/{worker.py → py/worker.py} +76 -696
  30. package/src/sandbox/sandbox-manager.ts +13 -0
  31. package/src/sandbox/sandbox.ts +99 -193
  32. package/src/text/tokens.ts +29 -3
  33. package/src/tool/repl-details.ts +2 -2
  34. package/src/tool/repl-render.ts +58 -0
  35. package/src/tool/repl-result.ts +70 -0
  36. package/src/tool/repl-tool.ts +37 -159
  37. package/src/tool/rlm-aggregator.ts +2 -10
  38. package/src/tool/rlm-details.ts +0 -2
  39. package/src/tool/rlm-events.ts +0 -14
  40. package/src/tool/rlm-tool.ts +1 -12
  41. package/src/ui/config-panel.ts +4 -16
  42. package/src/ui/intro.ts +1 -2
  43. package/src/ui/model-picker.ts +34 -10
  44. package/src/ui/status.ts +3 -7
  45. package/src/util/concurrency.ts +9 -5
  46. package/src/bridge/fallback-todo.ts +0 -148
  47. package/src/bridge/interactive.ts +0 -65
  48. package/src/bridge/pi-interactive.ts +0 -41
  49. package/src/core/artifacts.ts +0 -89
  50. package/src/core/critique.ts +0 -92
  51. package/src/core/gates.ts +0 -301
  52. package/src/core/pipeline-handlers.ts +0 -319
  53. package/src/core/pipeline.ts +0 -268
  54. package/src/prompts/phases.ts +0 -104
  55. package/src/state/index.ts +0 -24
  56. package/src/state/internal.ts +0 -46
  57. package/src/state/paths.ts +0 -44
  58. package/src/state/reads.ts +0 -133
  59. package/src/state/resume.ts +0 -173
  60. package/src/state/rows.ts +0 -123
  61. package/src/state/writes.ts +0 -58
@@ -16,16 +16,15 @@
16
16
  */
17
17
 
18
18
  import { Type } from "typebox";
19
- import type { Theme, ToolDefinition } from "@earendil-works/pi-coding-agent";
20
- import { Container, Spacer, Text } from "@earendil-works/pi-tui";
19
+ import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
20
+ import { Text } from "@earendil-works/pi-tui";
21
21
  import type { Model, Usage, Api } from "@earendil-works/pi-ai";
22
22
  import type { ModelRegistry } from "@earendil-works/pi-coding-agent";
23
- import { buildInteractiveHandlers } from "../bridge/interactive.ts";
24
23
  import { buildLibraryHandler } from "../bridge/library.ts";
25
- import { createPiInteractiveDeps } from "../bridge/pi-interactive.ts";
24
+ import { libraryPrefixesIn } from "../context/library-context.ts";
26
25
  import type { SubcallGates } from "../util/concurrency.ts";
27
26
  import { LimitGuard, limitsFromConfig } from "../core/limits.ts";
28
- import type { InteractiveDeps, RlmConfig, RlmInput, RlmResult } from "../core/types.ts";
27
+ import type { RlmConfig, RlmInput, RlmResult } from "../core/types.ts";
29
28
  import { SandboxManager } from "../sandbox/sandbox-manager.ts";
30
29
  import type { SubcallOpts } from "../sandbox/sandbox.ts";
31
30
  import { createSubcallHandlers, type Invocation } from "../bridge/subcall-handlers.ts";
@@ -39,26 +38,13 @@ import { createEngine } from "../core/engine.ts";
39
38
  import { spinnerFrame } from "../ui/theme.ts";
40
39
  import { previewText } from "../text/preview.ts";
41
40
  import { errorMessage } from "../util/errors.ts";
42
- import {
43
- cardHeader,
44
- cardStatsLine,
45
- renderCollapsedCard,
46
- renderExpandedSubcallTree,
47
- } from "./subcall-render.ts";
48
41
  import { createProgressNotifier, validateToolParams } from "./tool-utils.ts";
49
- import { capReplResultText, replDelegationNudge } from "../mode/native-guards.ts";
42
+ import { buildReplResultText, collectReplWarnings } from "./repl-result.ts";
43
+ import { renderReplCollapsed, renderReplExpanded } from "./repl-render.ts";
50
44
  import { attachTracer, trace, traceEnabled } from "../util/trace.ts";
51
45
 
52
- /** Chars of code shown on the tool call line, and of stdout in the expanded view. */
46
+ /** Chars of code shown on the tool call line. */
53
47
  const CALL_PREVIEW_CHARS = 80;
54
- const EXPANDED_STDOUT_CHARS = 2_000;
55
- const EXPANDED_STDERR_CHARS = 500;
56
-
57
- // ── Parameter schema ──
58
-
59
- export const ReplToolParams = Object.freeze(Type.Object({
60
- code: Type.String({ description: "Python code to execute in the persistent REPL sandbox" }),
61
- }));
62
48
 
63
49
  /** Last non-empty line of a Python traceback — the `TypeError: …` line, not the frames. */
64
50
  function lastLine(text: string): string {
@@ -70,67 +56,11 @@ function lastLine(text: string): string {
70
56
  return "";
71
57
  }
72
58
 
73
- /** Model-visible text assembled from a repl() result. */
74
- export interface ReplResultText {
75
- readonly text: string;
76
- }
77
-
78
- /**
79
- * Assemble the model-visible text for a repl() result: cap stdout, append a zero-subcall
80
- * delegation nudge when a bulk read went undelegated, and report tasks still running.
81
- *
82
- * The pending line is the model's only signal that `spawn()`ed work is outstanding — without
83
- * it a model that spawned and moved on has no way to know it should still collect.
84
- *
85
- * `varNames` covers the opposite failure: a block that stores its results in `answers` and
86
- * prints nothing reads as a bare "(no output)", so the model concludes the block did nothing
87
- * and re-runs it — paying twice for the same sub-calls. The headless engine already answers
88
- * this with the same hint (core/answer.ts); native mode was the only path missing it.
89
- */
90
- export function buildReplResultText(
91
- stdout: string,
92
- finalAnswer: string | undefined,
93
- subcalls: readonly RlmSubcall[],
94
- backgroundPending = 0,
95
- varNames: readonly string[] = [],
96
- ): ReplResultText {
97
- const answerSubmitted = finalAnswer !== undefined;
98
- const noOutput = !answerSubmitted && !stdout;
99
- const varsHint = noOutput && varNames.length > 0
100
- ? ` — the block ran fine and these REPL vars are defined: ${varNames.join(", ")}. `
101
- + "Do NOT re-run it; read them in the next block."
102
- : "";
103
- const rawText = answerSubmitted
104
- ? `ANSWER_SUBMITTED (${finalAnswer.length} chars) — delivered to user. Do not restate it.`
105
- : stdout || `(no output)${varsHint}`;
106
- // Model-visible text is capped; the caller keeps full stdout in `details` for the TUI.
107
- const cappedText = capReplResultText(rawText) ?? rawText;
108
- const delegated = subcalls.some((s) => s.kind === "llm" || s.kind === "batch" || s.kind === "rlm");
109
- const nudge = answerSubmitted ? undefined : replDelegationNudge(rawText.length, delegated);
110
- const failedBg = subcalls.filter((s) => s.id.startsWith("bg") && s.status === "error").length;
111
- const pendingLine = backgroundPending > 0
112
- ? `\n\n[rlm] ${backgroundPending} background task(s) still running — rlm_await_all(tasks) to collect.`
113
- : "";
114
- const failedLine = failedBg > 0
115
- ? `\n[rlm] ${failedBg} background sub-call(s) FAILED — their rlm_await value is an "Error: …" string, not data.`
116
- : "";
117
- return { text: cappedText + (nudge ?? "") + pendingLine + failedLine };
118
- }
59
+ // ── Parameter schema ──
119
60
 
120
- /** Advisory diagnostics derived from a completed invocation's sub-calls. */
121
- export function collectReplWarnings(subcalls: readonly RlmSubcall[]): readonly string[] | undefined {
122
- let failed = 0;
123
- let total = 0;
124
- for (let i = 0; i < subcalls.length; i++) {
125
- const call = subcalls[i];
126
- if (call.status !== "error") continue;
127
- // A batch subcall stands for many prompts; a single call stands for one.
128
- failed += call.failedCount ?? 1;
129
- total += call.totalCount ?? 1;
130
- }
131
- if (failed === 0) return undefined;
132
- return Object.freeze([`${failed}/${total} sub-call(s) failed — results may be incomplete`]);
133
- }
61
+ export const ReplToolParams = Object.freeze(Type.Object({
62
+ code: Type.String({ description: "Python code to execute in the persistent REPL sandbox" }),
63
+ }));
134
64
 
135
65
  // ── Mutable bridge state (handler indirection) ──
136
66
 
@@ -141,21 +71,18 @@ export function collectReplWarnings(subcalls: readonly RlmSubcall[]): readonly s
141
71
  * calls rather than rebuilding handlers (which would lose REPL variable state). Handlers
142
72
  * capture the Invocation synchronously at interrupt entry and never re-read it — with
143
73
  * spawn() a sub-call can outlive its exec, and a later read would attribute it to whichever
144
- * turn happened to be current when it resumed.
74
+ * turn happened to be current when it settled.
145
75
  *
146
76
  * Detached work resolves to the session-scoped background Invocation instead, whose emitter
147
77
  * and LimitGuard are not torn down at the end of a turn.
148
78
  */
149
79
  class NativeBridgeState {
150
80
  private current: Invocation | null = null;
151
- /** Interactive callbacks for the turn in progress; child engines inherit them. */
152
- interactive: InteractiveDeps | null = null;
153
81
 
154
82
  constructor(private readonly background: BackgroundTasks) {}
155
83
 
156
- swap(inv: Invocation, interactive: InteractiveDeps): void {
84
+ swap(inv: Invocation): void {
157
85
  this.current = Object.freeze({ ...inv });
158
- this.interactive = interactive;
159
86
  }
160
87
 
161
88
  /** Detached ⇒ session registry; otherwise the turn that is currently executing. */
@@ -175,9 +102,9 @@ class NativeBridgeState {
175
102
  export interface ReplToolDeps {
176
103
  readonly sandboxManager: SandboxManager;
177
104
  readonly model: Model<Api>;
178
- readonly workerModel: Model<Api>;
105
+ readonly llmModel: Model<Api>;
179
106
  readonly getModel?: () => Model<Api> | undefined;
180
- readonly getWorkerModel?: () => Model<Api> | undefined;
107
+ readonly getLlmModel?: () => Model<Api> | undefined;
181
108
  readonly registry: ModelRegistry;
182
109
  /** Live accessor — `/rlm-config` replaces the config object, so never capture the value. */
183
110
  readonly getConfig: () => RlmConfig;
@@ -193,13 +120,13 @@ export interface ReplToolDeps {
193
120
  }
194
121
 
195
122
  export function createReplTool(deps: ReplToolDeps): ToolDefinition<typeof ReplToolParams, ReplDetails> {
196
- const { sandboxManager, workerModel, registry, getConfig, signal, onUsage, background } = deps;
123
+ const { sandboxManager, llmModel, registry, getConfig, signal, onUsage, background } = deps;
197
124
  const bridgeState = new NativeBridgeState(background);
198
125
 
199
126
  // Late-bound cwd — getOrCreate installs handlers only at spawn; never rebuild the closure.
200
127
  let sessionCwd = process.cwd();
201
128
 
202
- const getWorkerModel = (): Model<Api> => deps.getWorkerModel?.() ?? workerModel;
129
+ const getLlmModel = (): Model<Api> => deps.getLlmModel?.() ?? llmModel;
203
130
  const getModel = (): Model<Api> => deps.getModel?.() ?? deps.model;
204
131
 
205
132
  // Each rlm_query spawns a child RLM with its own sandbox and turn loop, not a flat
@@ -207,7 +134,7 @@ export function createReplTool(deps: ReplToolDeps): ToolDefinition<typeof ReplTo
207
134
  // progress and cost deltas land on the emitter the parent invocation is using.
208
135
  const runChild = (input: RlmInput, inv: Invocation): Promise<RlmResult> => createEngine({
209
136
  model: getModel(),
210
- workerModel: getWorkerModel(),
137
+ llmModel: getLlmModel(),
211
138
  registry,
212
139
  config: getConfig(),
213
140
  signal,
@@ -218,8 +145,6 @@ export function createReplTool(deps: ReplToolDeps): ToolDefinition<typeof ReplTo
218
145
  // the child's own root turns — so fold both roles into "sub" rather than casting.
219
146
  onUsage: onUsage === undefined ? undefined : (usage: Usage) => onUsage(usage, "sub"),
220
147
  limits: limitsFromConfig(getConfig()),
221
- onTodo: bridgeState.interactive?.onTodo,
222
- onAskUserQuestion: bridgeState.interactive?.onAskUserQuestion,
223
148
  })(input);
224
149
 
225
150
  // Built once: the same closures stay correct across repl() calls because everything
@@ -228,12 +153,16 @@ export function createReplTool(deps: ReplToolDeps): ToolDefinition<typeof ReplTo
228
153
  resolve: (opts) => bridgeState.resolve(opts),
229
154
  gates: deps.gates,
230
155
  registry,
231
- getWorkerModel,
156
+ getLlmModel,
232
157
  getModel,
233
158
  getConfig,
234
159
  signal,
235
160
  onUsage,
236
161
  runChild,
162
+ // The session sandbox's context is the child's world. Read lazily so a load_library from an
163
+ // earlier repl() reaches a child spawned in a later one. Populated before any interrupt can
164
+ // fire: execute() awaits ensureContext() before getOrCreate().
165
+ getChildContext: () => sandboxManager.contextPayload ?? undefined,
237
166
  trackDetached: (task) => background.track(task),
238
167
  });
239
168
 
@@ -241,12 +170,22 @@ export function createReplTool(deps: ReplToolDeps): ToolDefinition<typeof ReplTo
241
170
  ? buildLibraryHandler({
242
171
  getCwd: () => sessionCwd,
243
172
  getEmitter: () => bridgeState.currentEmitter,
173
+ // Refuse pre-flight whatever the worker would reject, so host idempotency is never
174
+ // committed for an append that did not happen.
175
+ getContext: () => sandboxManager.contextPayload,
244
176
  parentId: undefined,
245
177
  signal,
246
- startIndex: 1,
178
+ // Keep the manager's replay copy in step with the worker's live `context`, and with it
179
+ // whatever a child spawned after this load will inherit.
180
+ onLoaded: (payload) => { sandboxManager.appendLibrary(payload); },
247
181
  })
248
182
  : undefined;
249
- if (libraryBundle) deps.registerDiscardHook?.(libraryBundle.reset);
183
+ if (libraryBundle) {
184
+ // Re-derive the loaded-prefix cache from the payload that will actually be replayed —
185
+ // clearing it outright would make the host re-clone a library the recreated worker already has.
186
+ const bundle = libraryBundle;
187
+ deps.registerDiscardHook?.(() => bundle.reset(libraryPrefixesIn(sandboxManager.contextPayload)));
188
+ }
250
189
 
251
190
  return {
252
191
  name: "repl",
@@ -258,7 +197,7 @@ export function createReplTool(deps: ReplToolDeps): ToolDefinition<typeof ReplTo
258
197
  "semantic reading to map_files / llm_query / llm_query_batched / llm_query_chunked " +
259
198
  "(rlm_query for iterative sub-tasks) — stdout returned to you is hard-capped at 4K chars, " +
260
199
  "so printing file bodies is useless. Variables, imports, and the `answers`/`plan` memo " +
261
- "persist across calls. Also supports todo, ask_user_question, and load_library.",
200
+ "persist across calls. Also supports load_library.",
262
201
  promptSnippet:
263
202
  "repl: run Python in a persistent sandbox holding the whole repository in `context`; " +
264
203
  "search/grep_context/outline to locate, map_files/llm_query* to read.",
@@ -327,24 +266,11 @@ export function createReplTool(deps: ReplToolDeps): ToolDefinition<typeof ReplTo
327
266
  }
328
267
 
329
268
  try {
330
- // Build interactive handlers (session-stable callbacks)
331
- const interactive = createPiInteractiveDeps(ctx);
332
- const interactiveHandlers = buildInteractiveHandlers({
333
- onAskUserQuestion: getConfig().askUserQuestion ? interactive.onAskUserQuestion : undefined,
334
- onTodo: interactive.onTodo,
335
- onTodoRow: undefined,
336
- emitter,
337
- depth: 0,
338
- parentId: undefined,
339
- });
340
-
341
269
  sessionCwd = ctx.cwd ?? process.cwd();
342
270
 
343
271
  await deps.ensureContext?.();
344
272
  await sandboxManager.getOrCreate({
345
273
  ...subcallHandlers,
346
- askUserQuestion: interactiveHandlers.askUserQuestion,
347
- todo: interactiveHandlers.todo,
348
274
  ...(libraryBundle?.handlers ?? {}),
349
275
  });
350
276
 
@@ -366,7 +292,7 @@ export function createReplTool(deps: ReplToolDeps): ToolDefinition<typeof ReplTo
366
292
  // Wire per-invocation mutable state only after the serialized exec slot
367
293
  // is active. Swapping earlier would let queued repl() calls overwrite
368
294
  // emitter/limits for the currently running REPL execution.
369
- bridgeState.swap({ emitter, parentId: undefined, depth: 0, limits }, interactive);
295
+ bridgeState.swap({ emitter, parentId: undefined, depth: 0, limits });
370
296
  }, execSignal);
371
297
  const elapsed = Date.now() - start;
372
298
  capturedStdout = result.stdout;
@@ -480,51 +406,3 @@ export function createReplTool(deps: ReplToolDeps): ToolDefinition<typeof ReplTo
480
406
  },
481
407
  };
482
408
  }
483
-
484
- // ── Collapsed view ──
485
-
486
- function replStats(details: ReplDetails, theme: Theme): string {
487
- const elapsed = details.executionTimeMs > 0 ? `${details.executionTimeMs}ms` : undefined;
488
- return cardStatsLine(details.totals, theme, elapsed, details.backgroundPending);
489
- }
490
-
491
- function renderReplCollapsed(details: ReplDetails, theme: Theme): Text {
492
- return renderCollapsedCard("REPL", details.status, replStats(details, theme), details.subcalls, theme);
493
- }
494
-
495
- // ── Expanded view ──
496
-
497
- function renderReplExpanded(details: ReplDetails, theme: Theme): Container {
498
- const container = new Container();
499
-
500
- container.addChild(new Text(cardHeader("REPL", details.status, replStats(details, theme), theme), 0, 0));
501
-
502
- // Output
503
- if (details.output) {
504
- container.addChild(new Spacer(1));
505
- const out = details.output.length > EXPANDED_STDOUT_CHARS
506
- ? `${details.output.slice(0, EXPANDED_STDOUT_CHARS)}…`
507
- : details.output;
508
- container.addChild(new Text(out, 0, 0));
509
- }
510
-
511
- if (details.warnings && details.warnings.length > 0) {
512
- container.addChild(new Spacer(1));
513
- container.addChild(new Text(theme.fg("muted", details.warnings.join("\n")), 0, 0));
514
- }
515
-
516
- // Stderr
517
- if (details.stderr) {
518
- container.addChild(new Spacer(1));
519
- container.addChild(new Text(theme.fg("error", details.stderr.slice(0, EXPANDED_STDERR_CHARS)), 0, 0));
520
- }
521
-
522
- // Sub-call tree
523
- if (details.subcalls.length > 0) {
524
- container.addChild(new Spacer(1));
525
- container.addChild(new Text(theme.fg("muted", "─── Sub-calls ───"), 0, 0));
526
- container.addChild(renderExpandedSubcallTree(details.subcalls, theme));
527
- }
528
-
529
- return container;
530
- }
@@ -6,14 +6,14 @@
6
6
  * getState(): RlmDetails for direct access (spinner loop, final return).
7
7
  *
8
8
  * Subcall storage and totals are delegated to SubcallStore. Root-level state
9
- * (status, prompt, turns, answer, warnings) is kept in the aggregator.
9
+ * (status, prompt, turns, answer) is kept in the aggregator.
10
10
  *
11
11
  * Replaces RlmToolBridge's internal state accumulation. The emitter is pure
12
12
  * dispatch; the aggregator is pure state. Separated for independent testing.
13
13
  */
14
14
 
15
15
  import type { AgentToolUpdateCallback } from "@earendil-works/pi-agent-core";
16
- import type { RlmEmitter, TurnEvent, RootUsageEvent, AnswerEvent, StatusEvent, RootPromptEvent, WarningsEvent } from "./rlm-events.ts";
16
+ import type { RlmEmitter, TurnEvent, RootUsageEvent, AnswerEvent, StatusEvent, RootPromptEvent } from "./rlm-events.ts";
17
17
  import type { RlmDetails, RlmRunStatus } from "./rlm-details.ts";
18
18
  import { EmitterListener } from "./emitter-listener.ts";
19
19
  import { SubcallStore } from "./subcall-store.ts";
@@ -27,7 +27,6 @@ export class RlmEventAggregator extends EmitterListener {
27
27
  private turnCurrent = 0;
28
28
  private turnMax = 0;
29
29
  private answer?: string;
30
- private warnings?: readonly string[];
31
30
 
32
31
  constructor(
33
32
  emitter: RlmEmitter,
@@ -40,7 +39,6 @@ export class RlmEventAggregator extends EmitterListener {
40
39
  emitter.onTurn((e) => this.handleTurn(e)),
41
40
  emitter.onRootUsage((e) => this.handleRootUsage(e)),
42
41
  emitter.onAnswer((e) => this.handleAnswer(e)),
43
- emitter.onWarnings((e) => this.handleWarnings(e)),
44
42
  emitter.onStatus((e) => this.handleStatus(e)),
45
43
  emitter.onRootPrompt((e) => this.handleRootPrompt(e)),
46
44
  ]);
@@ -64,11 +62,6 @@ export class RlmEventAggregator extends EmitterListener {
64
62
  this.notify();
65
63
  }
66
64
 
67
- private handleWarnings(event: WarningsEvent): void {
68
- this.warnings = event.warnings;
69
- this.notify();
70
- }
71
-
72
65
  private handleStatus(event: StatusEvent): void {
73
66
  this.rootStatus = event.status;
74
67
  this.notify();
@@ -90,7 +83,6 @@ export class RlmEventAggregator extends EmitterListener {
90
83
  subcalls: this.store.getSubcalls(),
91
84
  totals: this.store.getTotals(),
92
85
  answer: this.answer,
93
- warnings: this.warnings,
94
86
  };
95
87
  }
96
88
 
@@ -40,7 +40,5 @@ export interface RlmDetails {
40
40
  readonly subcalls: readonly RlmSubcall[];
41
41
  readonly totals: { readonly costUsd: number; readonly tokens: number };
42
42
  readonly answer?: string;
43
- /** Advisory diagnostics — surfaced to the user, never a failure. */
44
- readonly warnings?: readonly string[];
45
43
  }
46
44
 
@@ -45,10 +45,6 @@ export interface SubcallUpdatedEvent {
45
45
  readonly totalCount?: number;
46
46
  }
47
47
 
48
- export interface WarningsEvent {
49
- readonly warnings: readonly string[];
50
- }
51
-
52
48
  export interface TurnEvent {
53
49
  readonly current: number;
54
50
  readonly max: number;
@@ -121,11 +117,6 @@ export class RlmEmitter {
121
117
  this.ee.emit("answer", { text } satisfies AnswerEvent);
122
118
  }
123
119
 
124
- /** Set advisory warnings (root-only; never a failure). */
125
- emitWarnings(warnings: readonly string[]): void {
126
- this.ee.emit("warnings", { warnings } satisfies WarningsEvent);
127
- }
128
-
129
120
  /** Set the root run status (done/error/aborted). */
130
121
  emitStatus(status: RlmRunStatus): void {
131
122
  this.ee.emit("status", { status } satisfies StatusEvent);
@@ -163,11 +154,6 @@ export class RlmEmitter {
163
154
  return () => { this.ee.off("answer", handler); };
164
155
  }
165
156
 
166
- onWarnings(handler: (event: WarningsEvent) => void): () => void {
167
- this.ee.on("warnings", handler);
168
- return () => { this.ee.off("warnings", handler); };
169
- }
170
-
171
157
  onStatus(handler: (event: StatusEvent) => void): () => void {
172
158
  this.ee.on("status", handler);
173
159
  return () => { this.ee.off("status", handler); };
@@ -8,7 +8,6 @@
8
8
  import { type Theme, type ToolDefinition } from "@earendil-works/pi-coding-agent";
9
9
  import { Container, Markdown, Spacer, Text, type Component } from "@earendil-works/pi-tui";
10
10
  import { Type } from "typebox";
11
- import { createPiInteractiveDeps } from "../bridge/pi-interactive.ts";
12
11
  import type { RlmController, StartInput } from "../mode/rlm-mode.ts";
13
12
  import { spinnerFrame } from "../ui/theme.ts";
14
13
  import { markdownTheme } from "../ui/theme-adapter.ts";
@@ -82,15 +81,10 @@ export function createRlmTool(controller: RlmController): ToolDefinition<typeof
82
81
 
83
82
  try {
84
83
  const input: StartInput = {
85
- kind: "fresh",
86
84
  rootPrompt: params.prompt,
87
85
  context: params.context ?? undefined,
88
86
  };
89
- const interactive = createPiInteractiveDeps(ctx);
90
- const { done } = controller.start(ctx, input, emitter, {
91
- onAskUserQuestion: controller.config.askUserQuestion ? interactive.onAskUserQuestion : undefined,
92
- onTodo: controller.config.todo ? interactive.onTodo : undefined,
93
- });
87
+ const { done } = controller.start(ctx, input, emitter);
94
88
  const result = await done;
95
89
 
96
90
  emitter.emitAnswer(result.answer);
@@ -152,11 +146,6 @@ function renderExpanded(details: RlmDetails, theme: Theme): Component {
152
146
  container.addChild(new Markdown(details.answer, 0, 0, markdownTheme(theme)));
153
147
  }
154
148
 
155
- if (details.warnings && details.warnings.length > 0) {
156
- container.addChild(new Spacer(1));
157
- container.addChild(new Text(theme.fg("muted", details.warnings.join("\n")), 0, 0));
158
- }
159
-
160
149
  return container;
161
150
  }
162
151
 
@@ -9,21 +9,17 @@ const CHOICES = Object.freeze({
9
9
  maxDepth: Object.freeze(["1", "2", "3", "4"]),
10
10
  maxIterations: Object.freeze(["10", "20", "30", "50"]),
11
11
  execTimeoutS: Object.freeze(["30", "60", "120", "300"]),
12
- maxConcurrentSubcalls: Object.freeze(["2", "4", "8", "16"]),
13
- maxBudgetUsd: Object.freeze(["none", "0.50", "1", "5"]),
12
+ maxConcurrentSubcalls: Object.freeze(["2", "4", "8", "16", "32"]),
13
+ maxConcurrentChildren: Object.freeze(["1", "2", "3", "4", "6", "8"]),
14
14
  maxTimeoutMs: Object.freeze(["none", "60", "120", "300"]),
15
15
  maxTokens: Object.freeze(["none", "10000", "50000", "100000"]),
16
16
  maxErrors: Object.freeze(["3", "5", "10", "none"]),
17
17
  orchestrator: Object.freeze(["on", "off"]),
18
- pipeline: Object.freeze(["on", "off"]),
19
- maxBackwardJumps: Object.freeze(["0", "1", "2", "3"]),
20
18
  compaction: Object.freeze(["on", "off"]),
21
19
  compactionThresholdPct: Object.freeze(["50", "65", "80", "90"]),
22
20
  rootSamplingMaxTokens: Object.freeze(["4096", "8192", "16384", "32768"]),
23
21
  sandboxInitTimeoutMs: Object.freeze(["10000", "30000", "60000", "120000"]),
24
22
  requestTimeoutMs: Object.freeze(["2", "5", "10", "20"]),
25
- askUserQuestion: Object.freeze(["on", "off"]),
26
- todo: Object.freeze(["on", "off"]),
27
23
  libraryLoader: Object.freeze(["on", "off"]),
28
24
  });
29
25
 
@@ -43,20 +39,16 @@ export async function showConfigPanel(ctx: ExtensionContext, config: RlmConfig):
43
39
  item("maxIterations", "Max iterations", String(config.maxIterations), CHOICES.maxIterations, "Maximum root REPL turns before RLM asks the model for a final answer."),
44
40
  item("execTimeoutS", "REPL block timeout (s)", String(config.execTimeoutS), CHOICES.execTimeoutS, "Wall-clock limit for one model-authored Python REPL block."),
45
41
  item("maxConcurrentSubcalls", "Max concurrent sub-calls", String(config.maxConcurrentSubcalls), CHOICES.maxConcurrentSubcalls, "Concurrency pool size for llm_query_batched and rlm_query_batched."),
46
- item("maxBudgetUsd", "Budget ceiling (USD)", config.maxBudgetUsd != null ? String(config.maxBudgetUsd) : "none", CHOICES.maxBudgetUsd, "Total spend cap for the whole recursive tree; none disables the cap."),
42
+ item("maxConcurrentChildren", "Max concurrent children", String(config.maxConcurrentChildren), CHOICES.maxConcurrentChildren, "Concurrent rlm_query child engines per depth. Each is a Python process holding its own copy of the inherited context."),
47
43
  item("maxTimeoutMs", "Wall-clock ceiling (min)", config.maxTimeoutMs != null ? String(Math.round(config.maxTimeoutMs / 60_000)) : "none", CHOICES.maxTimeoutMs, "Total runtime cap for the whole recursive tree; none disables the cap."),
48
44
  item("maxTokens", "Token ceiling", config.maxTokens != null ? String(config.maxTokens) : "none", CHOICES.maxTokens, "Total input+output token cap for the whole recursive tree."),
49
45
  item("maxErrors", "Max consecutive errors", config.maxErrors != null ? String(config.maxErrors) : "none", CHOICES.maxErrors, "Stop after this many consecutive failing turns; none disables the guard."),
50
46
  item("orchestrator", "Orchestrator addendum", config.orchestrator ? "on" : "off", CHOICES.orchestrator, "Append extra divide-and-conquer guidance to the root model system prompt."),
51
- item("pipeline", "Phase pipeline", config.pipeline ? "on" : "off", CHOICES.pipeline, "Enable artifact-gated phases: clarify→research→blueprint→validate (read-only plan pipeline; clarify needs Ask user on)."),
52
- item("maxBackwardJumps", "Max validate→blueprint loops", String(config.maxBackwardJumps), CHOICES.maxBackwardJumps, "Bounded corrective re-entries when validation reports blockers_count > 0."),
53
47
  item("compaction", "Trajectory compaction", config.compaction ? "on" : "off", CHOICES.compaction, "Summarize old turns when history approaches the model context window."),
54
48
  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."),
55
49
  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."),
56
50
  item("sandboxInitTimeoutMs", "Sandbox init timeout", String(config.sandboxInitTimeoutMs), CHOICES.sandboxInitTimeoutMs, "How long to wait for the Python worker to start."),
57
51
  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."),
58
- item("askUserQuestion", "[Interactive] Ask user", config.askUserQuestion ? "on" : "off", CHOICES.askUserQuestion, "Allow root REPL code to present structured ask_user_question dialogs."),
59
- item("todo", "[Interactive] Todo", config.todo ? "on" : "off", CHOICES.todo, "Allow REPL code to manage a visible todo task list."),
60
52
  item("libraryLoader", "Library loader", config.libraryLoader ? "on" : "off", CHOICES.libraryLoader,
61
53
  "Allow load_library() to pull an external dir, file, or git repo into the shared context list."),
62
54
  item("__save__", "Save & close", "↵", ["↵"], "Save these settings and close (Esc also saves)."),
@@ -101,21 +93,17 @@ export function applySetting(config: RlmConfig, id: string, value: string): RlmC
101
93
  case "maxIterations": return Object.freeze({ ...config, maxIterations: Number(value) });
102
94
  case "execTimeoutS": return Object.freeze({ ...config, execTimeoutS: Number(value) });
103
95
  case "maxConcurrentSubcalls": return Object.freeze({ ...config, maxConcurrentSubcalls: Number(value) });
104
- case "maxBudgetUsd": return Object.freeze({ ...config, maxBudgetUsd: optionalNumber(value) });
96
+ case "maxConcurrentChildren": return Object.freeze({ ...config, maxConcurrentChildren: Number(value) });
105
97
  case "maxTimeoutMs": return Object.freeze({ ...config, maxTimeoutMs: optionalNumber(value, 60_000) });
106
98
  case "maxTokens": return Object.freeze({ ...config, maxTokens: optionalNumber(value) });
107
99
  case "maxErrors": return Object.freeze({ ...config, maxErrors: optionalNumber(value) });
108
100
  case "orchestrator": return Object.freeze({ ...config, orchestrator: value === "on" });
109
- case "pipeline": return Object.freeze({ ...config, pipeline: value === "on" });
110
- case "maxBackwardJumps": return Object.freeze({ ...config, maxBackwardJumps: Number(value) });
111
101
  case "compaction": return Object.freeze({ ...config, compaction: value === "on" });
112
102
  case "compactionThresholdPct": return Object.freeze({ ...config, compactionThresholdPct: Number(value) / 100 });
113
103
  case "rootSamplingMaxTokens":
114
104
  return Object.freeze({ ...config, rootSampling: Object.freeze({ ...config.rootSampling, maxTokens: Number(value) }) });
115
105
  case "sandboxInitTimeoutMs": return Object.freeze({ ...config, sandboxInitTimeoutMs: Number(value) });
116
106
  case "requestTimeoutMs": return Object.freeze({ ...config, requestTimeoutMs: Number(value) * 60_000 });
117
- case "askUserQuestion": return Object.freeze({ ...config, askUserQuestion: value === "on" });
118
- case "todo": return Object.freeze({ ...config, todo: value === "on" });
119
107
  case "libraryLoader": return Object.freeze({ ...config, libraryLoader: value === "on" });
120
108
  default: return config;
121
109
  }
package/src/ui/intro.ts CHANGED
@@ -11,9 +11,8 @@ export const RLM_GUIDE = `# RLM mode
11
11
  ## Commands
12
12
 
13
13
  - \`/rlm\` — toggle RLM mode (shortcut: Ctrl+Shift+R). Turning it OFF also stops a running query.
14
- - \`/rlm-config\` — choose models, reasoning, and budget limits
14
+ - \`/rlm-config\` — choose models, reasoning, and run limits
15
15
  - \`/rlm-stop\` — cancel the current run but stay in RLM mode (use /rlm or Ctrl+Shift+R to leave)
16
- - \`/rlm-help\` — show this guide again
17
16
 
18
17
  When RLM mode is ON, \`read\`/\`grep\` are disabled and the agent reads the repository through the
19
18
  \`repl\` tool, delegating bulk analysis to sub-LLMs. The footer/status line shows the current state.`;
@@ -1,10 +1,11 @@
1
1
  /** Model picker TUI — choose a model and, when supported, a thinking level. */
2
2
 
3
- import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
3
+ import type { ExtensionContext, ModelRegistry } from "@earendil-works/pi-coding-agent";
4
4
  import { DynamicBorder } from "@earendil-works/pi-coding-agent";
5
5
  import type { Api, Model, ThinkingLevel } from "@earendil-works/pi-ai";
6
6
  import { Container, type Component, type SelectItem, SelectList, Text, truncateToWidth } from "@earendil-works/pi-tui";
7
7
  import { formatCost } from "./theme.ts";
8
+ import { compareLlm } from "../mode/llm-model.ts";
8
9
 
9
10
  export interface ModelSelection {
10
11
  readonly model: Model<Api>;
@@ -16,15 +17,38 @@ type SelectableThinkingLevel = (typeof LEVELS)[number];
16
17
 
17
18
  const CHEAPEST_VALUE = "__rlm_cheapest__";
18
19
 
19
- function items(models: Model<Api>[], includeCheapest = false): SelectItem[] {
20
- const modelItems = models.map((m) => ({
21
- value: `${m.provider}/${m.id}`,
22
- label: `${m.provider}/${m.id}`,
23
- description: `in ${formatCost(m.cost.input)}/Mtok · out ${formatCost(m.cost.output)}/Mtok${m.reasoning ? " · reasoning" : ""}`,
24
- }));
20
+ /**
21
+ * Models Pi itself would offer for this session, cheapest-first.
22
+ *
23
+ * Mirrors the built-in model switcher:
24
+ * - if the session has scoped models (`--models` / enabledModels) those only
25
+ * - else → `getAvailable()` (providers with configured auth)
26
+ *
27
+ * Deliberately NOT `getAll()`: the full catalog dumps every provider's catalog entry and is
28
+ * not what the user sees in Pi natively. See Pi extension docs on `ctx.scopedModels`.
29
+ */
30
+ export function pickableModels(
31
+ registry: ModelRegistry,
32
+ scoped?: readonly { readonly model: Model<Api> }[],
33
+ ): readonly Model<Api>[] {
34
+ const source = scoped !== undefined && scoped.length > 0
35
+ ? scoped.map((s) => s.model)
36
+ : registry.getAvailable();
37
+ return [...source].sort(compareLlm);
38
+ }
39
+
40
+ function items(models: readonly Model<Api>[], includeCheapest: boolean): SelectItem[] {
41
+ const modelItems = models.map((m) => {
42
+ const price = `in ${formatCost(m.cost.input)}/Mtok · out ${formatCost(m.cost.output)}/Mtok`;
43
+ return {
44
+ value: `${m.provider}/${m.id}`,
45
+ label: `${m.provider}/${m.id}`,
46
+ description: `${price}${m.reasoning ? " · reasoning" : ""}`,
47
+ };
48
+ });
25
49
  if (!includeCheapest) return modelItems;
26
50
  return [
27
- { value: CHEAPEST_VALUE, label: "⟳ cheapest (auto)", description: "Always use the cheapest available model" },
51
+ { value: CHEAPEST_VALUE, label: "⟳ cheapest (auto)", description: "Always use the cheapest model with a configured key" },
28
52
  ...modelItems,
29
53
  ];
30
54
  }
@@ -79,12 +103,12 @@ async function selectThinkingLevel(
79
103
  export async function selectModel(
80
104
  ctx: ExtensionContext,
81
105
  title: string,
82
- models: Model<Api>[],
106
+ models: readonly Model<Api>[],
83
107
  current?: Model<Api>,
84
108
  currentThinking?: ThinkingLevel,
85
109
  ): Promise<ModelSelection | null | undefined> {
86
110
  if (models.length === 0) {
87
- ctx.ui.notify("RLM: no models with configured auth", "warning");
111
+ ctx.ui.notify("RLM: no models available (add a provider key in Pi, or widen --models / enabledModels)", "warning");
88
112
  return undefined;
89
113
  }
90
114
  if (ctx.mode !== "tui") {
package/src/ui/status.ts CHANGED
@@ -12,18 +12,14 @@ export function modelLabel(model: Model<Api> | undefined, fallback: string): str
12
12
 
13
13
  export function formatRlmStateLine(controller: RlmController, contextUsage?: ContextUsage): string {
14
14
  if (!controller.enabled) return "○ RLM OFF";
15
- const worker = modelLabel(controller.workerModel, controller.savedWorkerRef ?? "cheapest");
16
- const workerSuffix = controller.config.subSampling.reasoning ? `:${controller.config.subSampling.reasoning}` : "";
15
+ const llm = modelLabel(controller.llmModel, controller.savedLlmRef ?? "cheapest");
16
+ const llmSuffix = controller.config.subSampling.reasoning ? `:${controller.config.subSampling.reasoning}` : "";
17
17
  // `percent` is null right after a compaction, before the next assistant response reports usage.
18
18
  const percent = contextUsage?.percent;
19
19
  const ctxSuffix = percent === null || percent === undefined ? "" : ` · ctx ${Math.round(percent)}%`;
20
- return `● RLM ON · worker=${worker}${workerSuffix}${ctxSuffix}`;
20
+ return `● RLM ON · llm=${llm}${llmSuffix}${ctxSuffix}`;
21
21
  }
22
22
 
23
23
  export function setRlmModeStatus(ui: ExtensionUIContext, controller: RlmController, contextUsage?: ContextUsage): void {
24
24
  ui.setStatus(KEY, formatRlmStateLine(controller, contextUsage));
25
25
  }
26
-
27
- export function clearRlmStatus(ui: ExtensionUIContext): void {
28
- ui.setStatus(KEY, undefined);
29
- }