@hicaru/pi-rlm 0.2.1 → 0.3.0

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 (79) hide show
  1. package/README.md +28 -47
  2. package/README.ru.md +18 -23
  3. package/README.zh-CN.md +17 -28
  4. package/package.json +22 -19
  5. package/src/bridge/add-context.ts +322 -0
  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 +8 -18
  10. package/src/config/settings.ts +13 -34
  11. package/src/context/anydoc.ts +67 -0
  12. package/src/context/listing.ts +70 -0
  13. package/src/context/md-cache.ts +112 -0
  14. package/src/context/merge.ts +97 -0
  15. package/src/context/namespace.ts +180 -0
  16. package/src/context/resolve.ts +122 -0
  17. package/src/context/source-dir.ts +166 -0
  18. package/src/context/source-doc.ts +71 -0
  19. package/src/context/source-git.ts +51 -0
  20. package/src/context/source-text.ts +45 -0
  21. package/src/context/types.ts +88 -0
  22. package/src/context/walk.ts +250 -0
  23. package/src/core/engine.ts +61 -345
  24. package/src/core/history.ts +1 -1
  25. package/src/core/limits.ts +5 -12
  26. package/src/core/resource-limits.ts +0 -2
  27. package/src/core/types.ts +10 -38
  28. package/src/index.ts +92 -54
  29. package/src/mode/llm-model.ts +54 -0
  30. package/src/mode/rlm-mode.ts +28 -58
  31. package/src/prompts/glossary.ts +290 -0
  32. package/src/prompts/native.ts +127 -0
  33. package/src/prompts/system.ts +15 -408
  34. package/src/sandbox/context-file.ts +154 -0
  35. package/src/sandbox/interrupts.ts +160 -0
  36. package/src/sandbox/protocol.ts +20 -75
  37. package/src/sandbox/py/__pycache__/guards.cpython-314.pyc +0 -0
  38. package/src/sandbox/py/__pycache__/retrieval.cpython-314.pyc +0 -0
  39. package/src/sandbox/py/__pycache__/tasks.cpython-314.pyc +0 -0
  40. package/src/sandbox/py/guards.py +150 -0
  41. package/src/sandbox/py/retrieval.py +265 -0
  42. package/src/sandbox/py/tasks.py +129 -0
  43. package/src/sandbox/py/worker.py +856 -0
  44. package/src/sandbox/sandbox-manager.ts +24 -9
  45. package/src/sandbox/sandbox.ts +99 -193
  46. package/src/text/tokens.ts +31 -5
  47. package/src/tool/repl-details.ts +2 -2
  48. package/src/tool/repl-render.ts +58 -0
  49. package/src/tool/repl-result.ts +70 -0
  50. package/src/tool/repl-tool.ts +60 -170
  51. package/src/tool/rlm-aggregator.ts +2 -10
  52. package/src/tool/rlm-details.ts +0 -2
  53. package/src/tool/rlm-events.ts +0 -14
  54. package/src/tool/rlm-tool.ts +2 -13
  55. package/src/ui/config-panel.ts +12 -20
  56. package/src/ui/intro.ts +1 -2
  57. package/src/ui/model-picker.ts +34 -10
  58. package/src/ui/status.ts +3 -7
  59. package/src/util/concurrency.ts +9 -5
  60. package/src/bridge/fallback-todo.ts +0 -148
  61. package/src/bridge/interactive.ts +0 -65
  62. package/src/bridge/library.ts +0 -155
  63. package/src/bridge/pi-interactive.ts +0 -41
  64. package/src/context/library-context.ts +0 -266
  65. package/src/context/repomix-context.ts +0 -204
  66. package/src/core/artifacts.ts +0 -89
  67. package/src/core/critique.ts +0 -92
  68. package/src/core/gates.ts +0 -301
  69. package/src/core/pipeline-handlers.ts +0 -319
  70. package/src/core/pipeline.ts +0 -268
  71. package/src/prompts/phases.ts +0 -104
  72. package/src/sandbox/worker.py +0 -1456
  73. package/src/state/index.ts +0 -24
  74. package/src/state/internal.ts +0 -46
  75. package/src/state/paths.ts +0 -44
  76. package/src/state/reads.ts +0 -133
  77. package/src/state/resume.ts +0 -173
  78. package/src/state/rows.ts +0 -123
  79. package/src/state/writes.ts +0 -58
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Model-visible text assembly for a repl() result, plus the advisory diagnostics derived from
3
+ * its sub-calls. Split out of repl-tool.ts: this is pure string/array work with no sandbox,
4
+ * emitter, or TUI dependency, and both halves are asserted directly by test/phase-guards.ts.
5
+ */
6
+
7
+ import type { RlmSubcall } from "./rlm-details.ts";
8
+ import { capReplResultText, replDelegationNudge } from "../mode/native-guards.ts";
9
+
10
+ /** Model-visible text assembled from a repl() result. */
11
+ export interface ReplResultText {
12
+ readonly text: string;
13
+ }
14
+
15
+ /**
16
+ * Assemble the model-visible text for a repl() result: cap stdout, append a zero-subcall
17
+ * delegation nudge when a bulk read went undelegated, and report tasks still running.
18
+ *
19
+ * The pending line is the model's only signal that `spawn()`ed work is outstanding — without
20
+ * it a model that spawned and moved on has no way to know it should still collect.
21
+ *
22
+ * `varNames` covers the opposite failure: a block that stores its results in `answers` and
23
+ * prints nothing reads as a bare "(no output)", so the model concludes the block did nothing
24
+ * and re-runs it — paying twice for the same sub-calls. The headless engine already answers
25
+ * this with the same hint (core/answer.ts); native mode was the only path missing it.
26
+ */
27
+ export function buildReplResultText(
28
+ stdout: string,
29
+ finalAnswer: string | undefined,
30
+ subcalls: readonly RlmSubcall[],
31
+ backgroundPending = 0,
32
+ varNames: readonly string[] = [],
33
+ ): ReplResultText {
34
+ const answerSubmitted = finalAnswer !== undefined;
35
+ const noOutput = !answerSubmitted && !stdout;
36
+ const varsHint = noOutput && varNames.length > 0
37
+ ? ` — the block ran fine and these REPL vars are defined: ${varNames.join(", ")}. `
38
+ + "Do NOT re-run it; read them in the next block."
39
+ : "";
40
+ const rawText = answerSubmitted
41
+ ? `ANSWER_SUBMITTED (${finalAnswer.length} chars) — delivered to user. Do not restate it.`
42
+ : stdout || `(no output)${varsHint}`;
43
+ // Model-visible text is capped; the caller keeps full stdout in `details` for the TUI.
44
+ const cappedText = capReplResultText(rawText) ?? rawText;
45
+ const delegated = subcalls.some((s) => s.kind === "llm" || s.kind === "batch" || s.kind === "rlm");
46
+ const nudge = answerSubmitted ? undefined : replDelegationNudge(rawText.length, delegated);
47
+ const failedBg = subcalls.filter((s) => s.id.startsWith("bg") && s.status === "error").length;
48
+ const pendingLine = backgroundPending > 0
49
+ ? `\n\n[rlm] ${backgroundPending} background task(s) still running — rlm_await_all(tasks) to collect.`
50
+ : "";
51
+ const failedLine = failedBg > 0
52
+ ? `\n[rlm] ${failedBg} background sub-call(s) FAILED — their rlm_await value is an "Error: …" string, not data.`
53
+ : "";
54
+ return { text: cappedText + (nudge ?? "") + pendingLine + failedLine };
55
+ }
56
+
57
+ /** Advisory diagnostics derived from a completed invocation's sub-calls. */
58
+ export function collectReplWarnings(subcalls: readonly RlmSubcall[]): readonly string[] | undefined {
59
+ let failed = 0;
60
+ let total = 0;
61
+ for (let i = 0; i < subcalls.length; i++) {
62
+ const call = subcalls[i];
63
+ if (call.status !== "error") continue;
64
+ // A batch subcall stands for many prompts; a single call stands for one.
65
+ failed += call.failedCount ?? 1;
66
+ total += call.totalCount ?? 1;
67
+ }
68
+ if (failed === 0) return undefined;
69
+ return Object.freeze([`${failed}/${total} sub-call(s) failed — results may be incomplete`]);
70
+ }
@@ -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
- import { buildLibraryHandler } from "../bridge/library.ts";
25
- import { createPiInteractiveDeps } from "../bridge/pi-interactive.ts";
23
+ import { buildAddContextHandler, type AddContextHandlerBundle } from "../bridge/add-context.ts";
24
+ import { contextPrefixesIn } from "../context/namespace.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;
@@ -188,18 +115,23 @@ export interface ReplToolDeps {
188
115
  readonly signal?: AbortSignal;
189
116
  readonly onUsage?: (usage: Usage, role: "sub") => void;
190
117
  readonly ensureContext?: () => Promise<void>;
191
- /** Register a reset hook for sandbox death/dispose (e.g. load_library slot counter). */
118
+ /** Register a reset hook for sandbox death/dispose (e.g. add_context prefix cache). */
192
119
  readonly registerDiscardHook?: (reset: () => void) => void;
120
+ /**
121
+ * Hands the live add_context bundle to the extension so the cwd seed can
122
+ * markLoaded("") / markSeededCwd(abs) — without this, add_context(".") doubles the tree.
123
+ */
124
+ readonly registerContextBundle?: (bundle: AddContextHandlerBundle) => void;
193
125
  }
194
126
 
195
127
  export function createReplTool(deps: ReplToolDeps): ToolDefinition<typeof ReplToolParams, ReplDetails> {
196
- const { sandboxManager, workerModel, registry, getConfig, signal, onUsage, background } = deps;
128
+ const { sandboxManager, llmModel, registry, getConfig, signal, onUsage, background } = deps;
197
129
  const bridgeState = new NativeBridgeState(background);
198
130
 
199
131
  // Late-bound cwd — getOrCreate installs handlers only at spawn; never rebuild the closure.
200
132
  let sessionCwd = process.cwd();
201
133
 
202
- const getWorkerModel = (): Model<Api> => deps.getWorkerModel?.() ?? workerModel;
134
+ const getLlmModel = (): Model<Api> => deps.getLlmModel?.() ?? llmModel;
203
135
  const getModel = (): Model<Api> => deps.getModel?.() ?? deps.model;
204
136
 
205
137
  // Each rlm_query spawns a child RLM with its own sandbox and turn loop, not a flat
@@ -207,7 +139,7 @@ export function createReplTool(deps: ReplToolDeps): ToolDefinition<typeof ReplTo
207
139
  // progress and cost deltas land on the emitter the parent invocation is using.
208
140
  const runChild = (input: RlmInput, inv: Invocation): Promise<RlmResult> => createEngine({
209
141
  model: getModel(),
210
- workerModel: getWorkerModel(),
142
+ llmModel: getLlmModel(),
211
143
  registry,
212
144
  config: getConfig(),
213
145
  signal,
@@ -218,8 +150,6 @@ export function createReplTool(deps: ReplToolDeps): ToolDefinition<typeof ReplTo
218
150
  // the child's own root turns — so fold both roles into "sub" rather than casting.
219
151
  onUsage: onUsage === undefined ? undefined : (usage: Usage) => onUsage(usage, "sub"),
220
152
  limits: limitsFromConfig(getConfig()),
221
- onTodo: bridgeState.interactive?.onTodo,
222
- onAskUserQuestion: bridgeState.interactive?.onAskUserQuestion,
223
153
  })(input);
224
154
 
225
155
  // Built once: the same closures stay correct across repl() calls because everything
@@ -228,39 +158,60 @@ export function createReplTool(deps: ReplToolDeps): ToolDefinition<typeof ReplTo
228
158
  resolve: (opts) => bridgeState.resolve(opts),
229
159
  gates: deps.gates,
230
160
  registry,
231
- getWorkerModel,
161
+ getLlmModel,
232
162
  getModel,
233
163
  getConfig,
234
164
  signal,
235
165
  onUsage,
236
166
  runChild,
167
+ // The session sandbox's context is the child's world. Read lazily so an add_context from an
168
+ // earlier repl() reaches a child spawned in a later one. Populated before any interrupt can
169
+ // fire: execute() awaits ensureContext() before getOrCreate().
170
+ getChildContext: () => sandboxManager.contextPayload ?? undefined,
237
171
  trackDetached: (task) => background.track(task),
238
172
  });
239
173
 
240
- const libraryBundle = getConfig().libraryLoader
241
- ? buildLibraryHandler({
174
+ const contextBundle = getConfig().contextLoader
175
+ ? buildAddContextHandler({
242
176
  getCwd: () => sessionCwd,
243
177
  getEmitter: () => bridgeState.currentEmitter,
178
+ // Refuse pre-flight whatever the worker would reject, so host idempotency is never
179
+ // committed for an append that did not happen.
180
+ getContext: () => sandboxManager.contextPayload,
244
181
  parentId: undefined,
245
182
  signal,
246
- startIndex: 1,
183
+ // Keep the manager's replay copy in step with the worker's live `context`, and with it
184
+ // whatever a child spawned after this load will inherit.
185
+ onLoaded: (payload) => { sandboxManager.appendContext(payload); },
247
186
  })
248
187
  : undefined;
249
- if (libraryBundle) deps.registerDiscardHook?.(libraryBundle.reset);
188
+ if (contextBundle) {
189
+ // Re-derive the loaded-prefix cache from the payload that will actually be replayed —
190
+ // clearing it outright would make the host re-clone a source the recreated worker already has.
191
+ // Re-plant the cwd sentinel if the seed is still in the payload (un-prefixed files).
192
+ const bundle = contextBundle;
193
+ deps.registerDiscardHook?.(() => {
194
+ const prefixes = contextPrefixesIn(sandboxManager.contextPayload);
195
+ bundle.reset(prefixes);
196
+ if (bundle.seededCwd() !== undefined) bundle.markLoaded("");
197
+ });
198
+ deps.registerContextBundle?.(contextBundle);
199
+ }
250
200
 
251
201
  return {
252
202
  name: "repl",
253
203
  label: "REPL",
254
204
  description:
255
205
  "PRIMARY tool for ALL repository reading and analysis (read/grep are disabled in RLM mode). " +
256
- "Persistent Python sandbox with every file pre-loaded in `context`. Locate first with the " +
257
- "free primitives search(query) / grep_context(pattern) / outline(path), then delegate the " +
258
- "semantic reading to map_files / llm_query / llm_query_batched / llm_query_chunked " +
259
- "(rlm_query for iterative sub-tasks) — stdout returned to you is hard-capped at 4K chars, " +
260
- "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.",
206
+ "Persistent Python sandbox with loaded files in `context` (starts empty; cwd seeds on first " +
207
+ "call). Locate first with the free primitives search(query) / grep_context(pattern) / " +
208
+ "outline(path), then delegate the semantic reading to map_files / llm_query / " +
209
+ "llm_query_batched / llm_query_chunked (rlm_query for iterative sub-tasks) — stdout " +
210
+ "returned to you is hard-capped at 4K chars, so printing file bodies is useless. " +
211
+ "Variables, imports, and the `answers`/`plan` memo persist across calls. Also supports " +
212
+ "add_context for external dirs/files/git URLs and document conversion.",
262
213
  promptSnippet:
263
- "repl: run Python in a persistent sandbox holding the whole repository in `context`; " +
214
+ "repl: run Python in a persistent sandbox holding loaded files in `context`; " +
264
215
  "search/grep_context/outline to locate, map_files/llm_query* to read.",
265
216
  promptGuidelines: [
266
217
  "In RLM mode, read the repository through `repl` only — `read`/`grep` and bash readers are blocked.",
@@ -327,25 +278,12 @@ export function createReplTool(deps: ReplToolDeps): ToolDefinition<typeof ReplTo
327
278
  }
328
279
 
329
280
  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
281
  sessionCwd = ctx.cwd ?? process.cwd();
342
282
 
343
283
  await deps.ensureContext?.();
344
284
  await sandboxManager.getOrCreate({
345
285
  ...subcallHandlers,
346
- askUserQuestion: interactiveHandlers.askUserQuestion,
347
- todo: interactiveHandlers.todo,
348
- ...(libraryBundle?.handlers ?? {}),
286
+ ...(contextBundle?.handlers ?? {}),
349
287
  });
350
288
 
351
289
  // Detect queue contention AFTER sandbox init (initPromise settled, isExecuting now accurate)
@@ -366,7 +304,7 @@ export function createReplTool(deps: ReplToolDeps): ToolDefinition<typeof ReplTo
366
304
  // Wire per-invocation mutable state only after the serialized exec slot
367
305
  // is active. Swapping earlier would let queued repl() calls overwrite
368
306
  // emitter/limits for the currently running REPL execution.
369
- bridgeState.swap({ emitter, parentId: undefined, depth: 0, limits }, interactive);
307
+ bridgeState.swap({ emitter, parentId: undefined, depth: 0, limits });
370
308
  }, execSignal);
371
309
  const elapsed = Date.now() - start;
372
310
  capturedStdout = result.stdout;
@@ -480,51 +418,3 @@ export function createReplTool(deps: ReplToolDeps): ToolDefinition<typeof ReplTo
480
418
  },
481
419
  };
482
420
  }
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";
@@ -32,7 +31,7 @@ const CALL_PREVIEW_CHARS = 80;
32
31
 
33
32
  export const RlmToolParams = Object.freeze(Type.Object({
34
33
  prompt: Type.String({ description: "The task or question for the RLM engine" }),
35
- context: Type.Optional(Type.String({ description: "Optional context. If omitted, repo is auto-packed via repomix." })),
34
+ context: Type.Optional(Type.String({ description: "Optional context. If omitted, the working directory is packed into context." })),
36
35
  }));
37
36
 
38
37
  // ── Rendering helpers ──
@@ -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