@hicaru/pi-rlm 0.1.2 → 0.1.5

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.
@@ -31,6 +31,7 @@ import type { ProposedEdit, ReplResult } from "../sandbox/protocol.ts";
31
31
  import { RlmEmitter } from "./rlm-events.ts";
32
32
  import { SubcallStore } from "./subcall-store.ts";
33
33
  import type { ReplDetails } from "./repl-details.ts";
34
+ import type { RlmSubcall } from "./rlm-details.ts";
34
35
  import { createEngine } from "../core/engine.ts";
35
36
  import { formatCost, formatTokens, spinnerFrame } from "../ui/theme.ts";
36
37
  import { errorMessage, formatError, isErrorText } from "../util/errors.ts";
@@ -40,6 +41,7 @@ import {
40
41
  renderExpandedSubcallTree,
41
42
  } from "./subcall-render.ts";
42
43
  import { createProgressNotifier, validateToolParams } from "./tool-utils.ts";
44
+ import { capReplResultText, replDelegationNudge } from "../mode/native-guards.ts";
43
45
 
44
46
  // ── Parameter schema ──
45
47
 
@@ -51,6 +53,37 @@ export function surfaceReplEdits(edits: readonly ProposedEdit[], raised: boolean
51
53
  return edits.length > 0 && !raised ? edits : undefined;
52
54
  }
53
55
 
56
+ /** Model-visible text assembled from a repl() result, plus the surfaced edits for `details`. */
57
+ export interface ReplResultText {
58
+ readonly text: string;
59
+ readonly surfacedEdits: readonly ProposedEdit[] | undefined;
60
+ }
61
+
62
+ /**
63
+ * Assemble the model-visible text for a repl() result: cap stdout, append a zero-subcall
64
+ * delegation nudge (suppressed when edits were staged), and append the STAGED_EDITS block
65
+ * AFTER capping so edit JSON is never truncated. Extracted as a pure function so the
66
+ * capping/ordering invariants are testable independently of the sandbox.
67
+ */
68
+ export function buildReplResultText(
69
+ stdout: string,
70
+ answerContent: string | undefined,
71
+ edits: readonly ProposedEdit[],
72
+ raised: boolean,
73
+ subcalls: readonly RlmSubcall[],
74
+ ): ReplResultText {
75
+ const rawText = stdout || answerContent || "(no output)";
76
+ const surfacedEdits = surfaceReplEdits(edits, raised);
77
+ const editsBlock = surfacedEdits
78
+ ? `\n\nSTAGED_EDITS:\n${JSON.stringify(surfacedEdits)}`
79
+ : "";
80
+ // Model-visible text is capped; the caller keeps full stdout in `details.output` for the TUI.
81
+ const cappedText = capReplResultText(rawText) ?? rawText;
82
+ const delegated = subcalls.some((s) => s.kind === "llm" || s.kind === "batch" || s.kind === "rlm");
83
+ const nudge = surfacedEdits ? undefined : replDelegationNudge(rawText.length, delegated);
84
+ return { text: cappedText + (nudge ?? "") + editsBlock, surfacedEdits };
85
+ }
86
+
54
87
  // ── Mutable bridge state (handler indirection) ──
55
88
 
56
89
  /**
@@ -91,6 +124,11 @@ class NativeBridgeState {
91
124
  modelRef(model ? (resolveModelId(deps.registry, model) ?? workerModel()) : workerModel()) ?? workerModel().id;
92
125
 
93
126
  async function complete1(prompt: string, model: string | null, track: (u: Usage) => void): Promise<string> {
127
+ const limits = state.currentLimits;
128
+ if (limits) {
129
+ const limitError = checkResourceLimits({ budgetUsd: limits.remainingBudgetUsd(), timeoutMs: limits.remainingTimeoutMs() });
130
+ if (limitError !== undefined) return limitError;
131
+ }
94
132
  if (prompt.length > deps.maxPromptChars) {
95
133
  return formatError(`sub-LLM prompt exceeded size limit (${prompt.length.toLocaleString()} chars > ${deps.maxPromptChars.toLocaleString()})`);
96
134
  }
@@ -107,6 +145,7 @@ class NativeBridgeState {
107
145
  reasoning: deps.sampling?.reasoning,
108
146
  signal: deps.signal,
109
147
  });
148
+ limits?.addUsage(res.usage);
110
149
  track(res.usage);
111
150
  return res.text;
112
151
  } catch (err) {
@@ -132,7 +171,6 @@ class NativeBridgeState {
132
171
  costUsd: cost, tokens, resultPreview: previewText(out),
133
172
  detail: isErrorText(out) ? out : undefined,
134
173
  });
135
- state.currentLimits?.addRaw(cost, 0, tokens);
136
174
  return out;
137
175
  },
138
176
 
@@ -155,7 +193,6 @@ class NativeBridgeState {
155
193
  status: error ? "error" : "done", costUsd: cost, tokens,
156
194
  resultPreview: previewText(out[0] ?? ""), detail: error,
157
195
  });
158
- state.currentLimits?.addRaw(cost, 0, tokens);
159
196
  return out;
160
197
  },
161
198
  };
@@ -317,7 +354,13 @@ export function createReplTool(deps: ReplToolDeps): ToolDefinition<typeof ReplTo
317
354
  return {
318
355
  name: "repl",
319
356
  label: "REPL",
320
- description: "Execute Python code in a persistent REPL sandbox with the full repository context pre-loaded. Variables, imports, and state persist across calls. Supports llm_query, rlm_query, todo, and ask_user_question inside the sandbox.",
357
+ description:
358
+ "PRIMARY tool for ALL repository reading and analysis (read/grep are disabled in RLM mode). " +
359
+ "Persistent Python sandbox with every file pre-loaded in `context`. You are an orchestrator: " +
360
+ "chunk `context` and delegate semantic work to llm_query / llm_query_batched / " +
361
+ "llm_query_chunked / rlm_query — stdout returned to you is hard-capped at 4K chars, so " +
362
+ "printing file bodies is useless. Variables, imports, and state persist across calls. " +
363
+ "Also supports todo and ask_user_question inside the sandbox.",
321
364
  parameters: ReplToolParams,
322
365
 
323
366
  async execute(_toolCallId, rawParams, _execSignal, onUpdate, ctx) {
@@ -421,11 +464,13 @@ export function createReplTool(deps: ReplToolDeps): ToolDefinition<typeof ReplTo
421
464
 
422
465
  if (queuedId) emitter.emitSubcallUpdated({ id: queuedId, status: "done" });
423
466
 
424
- const baseText = result.stdout || result.answerContent || "(no output)";
425
- const surfacedEdits = surfaceReplEdits(result.edits, result.raised);
426
- const editsBlock = surfacedEdits
427
- ? `\n\nSTAGED_EDITS:\n${JSON.stringify(surfacedEdits)}`
428
- : "";
467
+ const { text: resultText, surfacedEdits } = buildReplResultText(
468
+ result.stdout,
469
+ result.answerContent,
470
+ result.edits,
471
+ result.raised,
472
+ store.getSubcalls(),
473
+ );
429
474
 
430
475
  const details: ReplDetails = {
431
476
  status: "done",
@@ -438,7 +483,7 @@ export function createReplTool(deps: ReplToolDeps): ToolDefinition<typeof ReplTo
438
483
  };
439
484
  // Final progressive update
440
485
  onUpdate?.({ content: [{ type: "text", text: result.stdout.slice(0, 500) || "(no output)" }], details });
441
- return { content: [{ type: "text", text: baseText + editsBlock }], details };
486
+ return { content: [{ type: "text", text: resultText }], details };
442
487
  } catch (e) {
443
488
  progressStatus = "error";
444
489
  const msg = errorMessage(e);
@@ -15,6 +15,7 @@ const CHOICES = Object.freeze({
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"]),
18
19
  compaction: Object.freeze(["on", "off"]),
19
20
  rootSamplingMaxTokens: Object.freeze(["4096", "8192", "16384", "32768"]),
20
21
  sandboxInitTimeoutMs: Object.freeze(["10000", "30000", "60000", "120000"]),
@@ -38,6 +39,7 @@ export async function showConfigPanel(ctx: ExtensionContext, config: RlmConfig):
38
39
  item("maxTokens", "Token ceiling", config.maxTokens != null ? String(config.maxTokens) : "none", CHOICES.maxTokens, "Total input+output token cap for the whole recursive tree."),
39
40
  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."),
40
41
  item("orchestrator", "Orchestrator addendum", config.orchestrator ? "on" : "off", CHOICES.orchestrator, "Append extra divide-and-conquer guidance to the root model system prompt."),
42
+ item("pipeline", "Phase pipeline", config.pipeline ? "on" : "off", CHOICES.pipeline, "Enable advance_phase plus phase-stall reminders at root depth."),
41
43
  item("compaction", "Trajectory compaction", config.compaction ? "on" : "off", CHOICES.compaction, "Summarize old turns when history approaches the model context window."),
42
44
  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."),
43
45
  item("sandboxInitTimeoutMs", "Sandbox init timeout", String(config.sandboxInitTimeoutMs), CHOICES.sandboxInitTimeoutMs, "How long to wait for the Python worker to start."),
@@ -83,6 +85,7 @@ function applySetting(config: RlmConfig, id: string, value: string): void {
83
85
  case "maxTokens": config.maxTokens = value === "none" ? undefined : Number(value); break;
84
86
  case "maxErrors": config.maxErrors = value === "none" ? undefined : Number(value); break;
85
87
  case "orchestrator": config.orchestrator = value === "on"; break;
88
+ case "pipeline": config.pipeline = value === "on"; break;
86
89
  case "compaction": config.compaction = value === "on"; break;
87
90
  case "rootSamplingMaxTokens": config.rootSampling = Object.freeze({ ...config.rootSampling, maxTokens: Number(value) }); break;
88
91
  case "sandboxInitTimeoutMs": config.sandboxInitTimeoutMs = Number(value); break;