@hicaru/pi-rlm 0.2.0 → 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 (68) 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 +382 -0
  7. package/src/commands/rlm-config.ts +47 -18
  8. package/src/commands/rlm.ts +3 -152
  9. package/src/config/defaults.ts +7 -15
  10. package/src/config/settings.ts +8 -32
  11. package/src/context/library-context.ts +90 -17
  12. package/src/core/engine.ts +115 -360
  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 +49 -10
  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 -386
  23. package/src/sandbox/context-file.ts +154 -0
  24. package/src/sandbox/interrupts.ts +145 -0
  25. package/src/sandbox/protocol.ts +14 -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/py/worker.py +836 -0
  30. package/src/sandbox/sandbox-manager.ts +33 -6
  31. package/src/sandbox/sandbox.ts +153 -182
  32. package/src/text/tokens.ts +29 -3
  33. package/src/tool/background-tasks.ts +95 -0
  34. package/src/tool/repl-details.ts +4 -2
  35. package/src/tool/repl-render.ts +58 -0
  36. package/src/tool/repl-result.ts +70 -0
  37. package/src/tool/repl-tool.ts +178 -216
  38. package/src/tool/rlm-aggregator.ts +2 -10
  39. package/src/tool/rlm-details.ts +0 -2
  40. package/src/tool/rlm-events.ts +10 -16
  41. package/src/tool/rlm-tool.ts +1 -12
  42. package/src/tool/subcall-render.ts +15 -3
  43. package/src/tool/subcall-store.ts +57 -1
  44. package/src/ui/config-panel.ts +4 -16
  45. package/src/ui/intro.ts +1 -2
  46. package/src/ui/model-picker.ts +34 -10
  47. package/src/ui/status.ts +3 -7
  48. package/src/util/concurrency.ts +91 -13
  49. package/src/util/trace.ts +42 -0
  50. package/src/bridge/fallback-todo.ts +0 -137
  51. package/src/bridge/interactive.ts +0 -65
  52. package/src/bridge/llm-query.ts +0 -156
  53. package/src/bridge/pi-interactive.ts +0 -41
  54. package/src/bridge/rlm-query.ts +0 -108
  55. package/src/core/artifacts.ts +0 -89
  56. package/src/core/critique.ts +0 -92
  57. package/src/core/gates.ts +0 -301
  58. package/src/core/pipeline-handlers.ts +0 -319
  59. package/src/core/pipeline.ts +0 -268
  60. package/src/prompts/phases.ts +0 -104
  61. package/src/sandbox/worker.py +0 -1078
  62. package/src/state/index.ts +0 -24
  63. package/src/state/internal.ts +0 -46
  64. package/src/state/paths.ts +0 -44
  65. package/src/state/reads.ts +0 -133
  66. package/src/state/resume.ts +0 -173
  67. package/src/state/rows.ts +0 -123
  68. package/src/state/writes.ts +0 -58
@@ -1,7 +1,9 @@
1
1
  /**
2
- * LimitGuard — wall-clock, token, cost, and consecutive-error caps for a headless RLM run
2
+ * LimitGuard — wall-clock, token, and consecutive-error caps for a headless RLM run
3
3
  * (ported from rlm/core/rlm.py `_check_timeout` / `_check_iteration_limits`). Any breach throws
4
4
  * a LimitError; the engine catches it and returns the best partial answer it has.
5
+ *
6
+ * Cost is tracked for reporting only — there is no USD spend ceiling.
5
7
  */
6
8
 
7
9
  import type { Usage } from "@earendil-works/pi-ai";
@@ -9,14 +11,12 @@ import type { Usage } from "@earendil-works/pi-ai";
9
11
  export interface Limits {
10
12
  readonly maxTimeoutMs?: number;
11
13
  readonly maxTokens?: number;
12
- readonly maxBudgetUsd?: number;
13
14
  readonly maxErrors?: number;
14
15
  }
15
16
 
16
17
  /** Pick the limit caps out of a config (`RlmConfig` satisfies this structurally). */
17
18
  export function limitsFromConfig(config: Limits): Limits {
18
19
  return {
19
- maxBudgetUsd: config.maxBudgetUsd,
20
20
  maxTimeoutMs: config.maxTimeoutMs,
21
21
  maxTokens: config.maxTokens,
22
22
  maxErrors: config.maxErrors,
@@ -33,7 +33,7 @@ export interface UsageSnapshot {
33
33
 
34
34
  export class LimitError extends Error {
35
35
  constructor(
36
- public readonly kind: "timeout" | "tokens" | "budget" | "errors",
36
+ public readonly kind: "timeout" | "tokens" | "errors",
37
37
  message: string,
38
38
  ) {
39
39
  super(message);
@@ -77,16 +77,13 @@ export class LimitGuard {
77
77
  /** Call after each turn with whether the turn's REPL produced an error. */
78
78
  observe(hadError: boolean): void {
79
79
  this.consecutiveErrors = hadError ? this.consecutiveErrors + 1 : 0;
80
- const { maxErrors, maxTokens, maxBudgetUsd } = this.limits;
80
+ const { maxErrors, maxTokens } = this.limits;
81
81
  if (maxErrors && this.consecutiveErrors >= maxErrors) {
82
82
  throw new LimitError("errors", `${this.consecutiveErrors} consecutive errors (limit ${maxErrors})`);
83
83
  }
84
84
  if (maxTokens && this.inputTokens + this.outputTokens > maxTokens) {
85
85
  throw new LimitError("tokens", `${this.inputTokens + this.outputTokens} tokens (limit ${maxTokens})`);
86
86
  }
87
- if (maxBudgetUsd && this.costUsd > maxBudgetUsd) {
88
- throw new LimitError("budget", `$${this.costUsd.toFixed(4)} spent (limit $${maxBudgetUsd})`);
89
- }
90
87
  }
91
88
 
92
89
  usage(): UsageSnapshot {
@@ -98,10 +95,6 @@ export class LimitGuard {
98
95
  };
99
96
  }
100
97
 
101
- remainingBudgetUsd(): number | undefined {
102
- return this.limits.maxBudgetUsd === undefined ? undefined : this.limits.maxBudgetUsd - this.costUsd;
103
- }
104
-
105
98
  remainingTimeoutMs(): number | undefined {
106
99
  return this.limits.maxTimeoutMs === undefined ? undefined : this.limits.maxTimeoutMs - (Date.now() - this.start);
107
100
  }
@@ -3,12 +3,10 @@
3
3
  import { formatError } from "../util/errors.ts";
4
4
 
5
5
  export interface RemainingResources {
6
- readonly budgetUsd?: number;
7
6
  readonly timeoutMs?: number;
8
7
  }
9
8
 
10
9
  export function checkResourceLimits(resources: RemainingResources): string | undefined {
11
- if (resources.budgetUsd !== undefined && resources.budgetUsd <= 0) return formatError("budget exhausted");
12
10
  if (resources.timeoutMs !== undefined && resources.timeoutMs <= 0) return formatError("timeout exhausted");
13
11
  return undefined;
14
12
  }
package/src/core/types.ts CHANGED
@@ -1,8 +1,6 @@
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 } from "../sandbox/protocol.ts";
5
- import type { ReconstructResult } from "../state/resume.ts";
6
4
 
7
5
  export interface Sampling {
8
6
  readonly maxTokens?: number;
@@ -10,17 +8,6 @@ export interface Sampling {
10
8
  readonly reasoning?: ThinkingLevel;
11
9
  }
12
10
 
13
- export interface RunLogConfig {
14
- /** Default: true — always-on, opt-out. */
15
- readonly enabled?: boolean;
16
- /** Default: ".rlm/runs". Directory under cwd for run artifacts. */
17
- readonly dir?: string;
18
- /** Default: true — whether to write sandbox.pkl snapshots. */
19
- readonly snapshot?: boolean;
20
- /** Default: 50 — prune oldest runs beyond this count on each new run. */
21
- readonly maxRuns?: number;
22
- }
23
-
24
11
  export interface RlmConfig {
25
12
  /** Persistent editor-routing mode; when enabled, plain interactive prompts use RLM. */
26
13
  readonly enabled: boolean;
@@ -34,10 +21,11 @@ export interface RlmConfig {
34
21
  readonly requestTimeoutMs: number;
35
22
  /** Concurrency pool for *_batched sub-calls. */
36
23
  readonly maxConcurrentSubcalls: number;
24
+ /** Concurrent recursive child engines admitted per depth. Lower than maxConcurrentSubcalls:
25
+ * each child is a Python subprocess holding its own copy of the inherited context. */
26
+ readonly maxConcurrentChildren: number;
37
27
  /** Reject sub-LLM prompts larger than this many chars. */
38
28
  readonly maxPromptChars: number;
39
- /** Max USD spend across the whole tree before the engine stops (undefined = no cap). */
40
- readonly maxBudgetUsd?: number;
41
29
  /** Max wall-clock ms across the whole tree before the engine stops (undefined = no cap). */
42
30
  readonly maxTimeoutMs?: number;
43
31
  /** Max total input+output tokens across the whole tree before the engine stops (undefined = no cap). */
@@ -46,10 +34,6 @@ export interface RlmConfig {
46
34
  readonly maxErrors?: number;
47
35
  /** Append the orchestrator addendum to the system prompt. */
48
36
  readonly orchestrator: boolean;
49
- /** Enable the phase pipeline (advance_phase + stall nags) at depth 0. */
50
- readonly pipeline: boolean;
51
- /** Max validate→blueprint corrective re-entries when validation reports blockers (default 2). */
52
- readonly maxBackwardJumps: number;
53
37
  /** Summarize the trajectory when it grows past the threshold (keeps the root window small). */
54
38
  readonly compaction: boolean;
55
39
  /** Compact when estimated history tokens reach this fraction of the model's context window. */
@@ -58,10 +42,6 @@ export interface RlmConfig {
58
42
  readonly python: string;
59
43
  /** Worker startup wait before treating sandbox init as failed (ms). */
60
44
  readonly sandboxInitTimeoutMs: number;
61
- /** Allow ask_user_question() calls from the root REPL. */
62
- readonly askUserQuestion: boolean;
63
- /** Allow todo() calls from the REPL. */
64
- readonly todo: boolean;
65
45
  /** Enable the load_library() REPL scaffold (external dirs/files/git repos as extra context slots). */
66
46
  readonly libraryLoader: boolean;
67
47
  /** ThinkingLevel for the root smart model (set via /rlm-config). */
@@ -76,8 +56,6 @@ export interface RlmConfig {
76
56
  readonly subSystemPrompt?: string;
77
57
  /** Sampling for sub-LLM (worker) calls. */
78
58
  readonly subSampling: Readonly<Sampling>;
79
- /** Optional run-state persistence configuration. Enabled by default. */
80
- readonly runLog?: RunLogConfig;
81
59
  }
82
60
 
83
61
  /** Input to a (headless) RLM run. */
@@ -92,12 +70,8 @@ export interface RlmInput {
92
70
  readonly parentNodeId?: string;
93
71
  /** "provider/id" — overrides the root model for this run (set by recursive rlm_query). */
94
72
  readonly modelOverride?: string;
95
- /** Remaining budget for this subtree (set by parent from its LimitGuard). */
96
- readonly remainingBudgetUsd?: number;
97
73
  /** Remaining timeout for this subtree (set by parent from its LimitGuard). */
98
74
  readonly remainingTimeoutMs?: number;
99
- /** Depth-0 resume payload — controller rebuilds this from the trail's `reconstructRlmState()`. */
100
- readonly resume?: ReconstructResult & { readonly ok: true };
101
75
  }
102
76
 
103
77
  /** Result of a completed RLM run. */
@@ -111,11 +85,4 @@ export interface RlmResult {
111
85
  }
112
86
 
113
87
  /** A function that runs an RLM to completion — used to wire recursion (rlm_query). */
114
- export interface InteractiveDeps {
115
- /** Called when the sandbox issues ask_user_question; undefined = feature disabled. */
116
- readonly onAskUserQuestion?: (questions: readonly AskQuestion[]) => Promise<AskAnswer[]>;
117
- /** Called when the sandbox issues todo; undefined = feature disabled. */
118
- readonly onTodo?: (action: string, params: Record<string, unknown>) => Promise<string>;
119
- }
120
-
121
88
  export type RunRlm = (input: RlmInput) => Promise<RlmResult>;
package/src/index.ts CHANGED
@@ -7,17 +7,22 @@ import { registerRlmConfigCommand } from "./commands/rlm-config.ts";
7
7
  import { createRlmTool } from "./tool/rlm-tool.ts";
8
8
  import { createReplTool } from "./tool/repl-tool.ts";
9
9
  import { loadSettings, mergeConfig, resolveModelId } from "./config/settings.ts";
10
- import { RlmController, cheapestModel } from "./mode/rlm-mode.ts";
10
+ import { RlmController } from "./mode/rlm-mode.ts";
11
+ import { cheapestModel } from "./mode/llm-model.ts";
11
12
  import { postRlmGuide } from "./ui/intro.ts";
12
13
  import { setRlmModeStatus } from "./ui/status.ts";
13
14
  import { markdownTheme } from "./ui/theme-adapter.ts";
14
15
  import { SandboxManager } from "./sandbox/sandbox-manager.ts";
16
+ import { createSubcallGates } from "./util/concurrency.ts";
17
+ import { BackgroundTasks } from "./tool/background-tasks.ts";
15
18
  import { packRepository, formatForLLM, serializeForSandbox } from "./context/repomix-context.ts";
16
- import { buildNativeSystemPrompt, NATIVE_TURN_REMINDER } from "./prompts/system.ts";
19
+ import { buildNativeSystemPrompt, NATIVE_TURN_REMINDER } from "./prompts/native.ts";
17
20
  import { bashCommandFromInput, isFileReadingCommand, capToolResultText, BASH_BLOCK_REASON } from "./mode/native-guards.ts";
18
21
  import { errorMessage } from "./util/errors.ts";
19
22
 
20
23
  const BLOCKED_NATIVE_TOOLS = Object.freeze(new Set(["read", "grep"]));
24
+ /** How often to keep the parent sandbox's request watchdog alive during detached work. */
25
+ const WATCHDOG_HEARTBEAT_MS = 30_000;
21
26
  const CAPPED_RESULT_TOOLS = Object.freeze(new Set(["bash", "find", "ls"]));
22
27
 
23
28
  export default function rlmExtension(pi: ExtensionAPI): void {
@@ -31,8 +36,27 @@ export default function rlmExtension(pi: ExtensionAPI): void {
31
36
  python: config.python,
32
37
  sandboxInitTimeoutMs: config.sandboxInitTimeoutMs,
33
38
  maxPromptChars: config.maxPromptChars,
39
+ // Same wall budget as the parent request watchdog — a stalled sub-call should surface
40
+ // inside the cell rather than hang the session forever.
41
+ awaitTimeoutS: Math.round(config.requestTimeoutMs / 1000),
34
42
  onSandboxDiscarded: () => { onSandboxDiscardExtra?.(); },
35
43
  });
44
+ // One admission gate for the whole session: spawn() lets the sandbox put many requests on
45
+ // the wire at once, so nothing smaller than session scope actually bounds fan-out.
46
+ const gates = createSubcallGates(config.maxConcurrentSubcalls, config.maxConcurrentChildren);
47
+ const background = new BackgroundTasks({
48
+ maxTimeoutMs: config.maxTimeoutMs,
49
+ maxTokens: config.maxTokens,
50
+ maxErrors: config.maxErrors,
51
+ });
52
+ // A detached child works in its OWN sandbox, so this one sees no frames and its request
53
+ // watchdog would fire mid-await and SIGKILL a healthy worker, taking the REPL namespace
54
+ // with it. Keep it alive while detached work is genuinely in flight.
55
+ const watchdogHeartbeat = setInterval(() => {
56
+ if (background.pending > 0) sandboxManager.refreshWatchdog();
57
+ }, WATCHDOG_HEARTBEAT_MS);
58
+ watchdogHeartbeat.unref();
59
+
36
60
  let packedContextText: string | undefined;
37
61
  let contextPackPromise: Promise<string | undefined> | undefined;
38
62
  const ensureRepositoryContext = async (cwd: string): Promise<string | undefined> => {
@@ -55,7 +79,7 @@ export default function rlmExtension(pi: ExtensionAPI): void {
55
79
  const settingsReady = loadSettings()
56
80
  .then((persisted) => {
57
81
  controller.config = mergeConfig(persisted.config);
58
- controller.savedWorkerRef = persisted.worker;
82
+ controller.savedLlmRef = persisted.llm;
59
83
  })
60
84
  .catch((err) => {
61
85
  console.warn(`[rlm] settings load failed: ${errorMessage(err)}`);
@@ -96,24 +120,37 @@ export default function rlmExtension(pi: ExtensionAPI): void {
96
120
  const flag = pi.getFlag("rlm");
97
121
  if (typeof flag === "boolean") controller.setConfig(Object.freeze({ ...controller.config, enabled: flag }));
98
122
 
99
- if (controller.savedWorkerRef) {
100
- const resolved = resolveModelId(ctx.modelRegistry, controller.savedWorkerRef);
101
- if (resolved) controller.workerModel = resolved;
123
+ // Reload the catalog before the worker-model pick below reads it. Newer pi builds make
124
+ // `getAvailable()` an async-populated snapshot that starts empty, and picking from an empty
125
+ // catalog silently falls back to the root model. Called with no arguments and awaited so it
126
+ // is valid whether `refresh` returns void (current) or a promise (newer); fail-soft, because
127
+ // a refresh error must not abort session start.
128
+ try {
129
+ await ctx.modelRegistry.refresh();
130
+ } catch (err) {
131
+ console.warn(`[rlm] model registry refresh failed: ${errorMessage(err)}`);
132
+ }
133
+
134
+ if (controller.savedLlmRef) {
135
+ const resolved = resolveModelId(ctx.modelRegistry, controller.savedLlmRef);
136
+ if (resolved) controller.llmModel = resolved;
102
137
  }
103
138
 
104
139
  // Re-register repl tool each session to pick up model provider changes
105
- const workerModel = controller.workerModel ?? cheapestModel(ctx.modelRegistry) ?? ctx.model;
140
+ const llmModel = controller.llmModel ?? cheapestModel(ctx.modelRegistry) ?? ctx.model;
106
141
  const model = ctx.model;
107
- if (workerModel && model) {
142
+ if (llmModel && model) {
108
143
  try {
109
144
  pi.registerTool(createReplTool({
110
145
  sandboxManager,
111
146
  model,
112
- workerModel,
147
+ llmModel,
113
148
  getModel: () => controller.resolveModels(ctx)?.model,
114
- getWorkerModel: () => controller.resolveModels(ctx)?.worker,
149
+ getLlmModel: () => controller.resolveModels(ctx)?.llm,
115
150
  registry: ctx.modelRegistry,
116
151
  getConfig: () => controller.config,
152
+ gates,
153
+ background,
117
154
  registerDiscardHook: (reset) => { onSandboxDiscardExtra = reset; },
118
155
  ensureContext: async () => {
119
156
  const contextText = await ensureRepositoryContext(ctx.cwd ?? process.cwd());
@@ -224,6 +261,8 @@ export default function rlmExtension(pi: ExtensionAPI): void {
224
261
  // ── Session shutdown: cleanup ──
225
262
  pi.on("session_shutdown", async () => {
226
263
  controller.abort();
264
+ clearInterval(watchdogHeartbeat);
265
+ background.dispose();
227
266
  await sandboxManager.dispose();
228
267
  contextInjected = false;
229
268
  packedContextText = undefined;
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Sub-LLM model ranking — "cheapest available", with free models winning outright.
3
+ *
4
+ * Pi's `ModelCost` is non-nullable (`packages/ai/src/types.ts`), so a free model is a literal 0,
5
+ * not a null. That makes plain price sorting ambiguous rather than wrong: subscription and
6
+ * token-plan providers also publish 0, and a stable sort would hand back whichever 0-cost entry
7
+ * happened to be first in catalog order. The tie-breaks below are what actually pick a usable
8
+ * free model — and what make the pick identical across sessions and catalog reorderings.
9
+ */
10
+
11
+ import type { Api, Model } from "@earendil-works/pi-ai";
12
+ import type { ModelRegistry } from "@earendil-works/pi-coding-agent";
13
+
14
+ /** $/Mtok, input-weighted 3:1 — a sub-call sends a file body and gets back a sentence. */
15
+ function priceOf(model: Model<Api>): number {
16
+ const { input, output, cacheRead } = model.cost;
17
+ return input * 3 + output + cacheRead;
18
+ }
19
+
20
+ /** True when the model costs nothing to call on any axis. */
21
+ export function isFreeModel(model: Model<Api>): boolean {
22
+ return priceOf(model) === 0;
23
+ }
24
+
25
+ /**
26
+ * Negative when `a` is the better sub-LLM.
27
+ *
28
+ * Window before maxTokens before id: a free model with a 4K context is useless for bulk reading,
29
+ * so price alone must not decide. The final id comparison exists only to make the result
30
+ * deterministic — without it the pick drifts whenever a provider reorders its catalog.
31
+ */
32
+ export function compareLlm(a: Model<Api>, b: Model<Api>): number {
33
+ return (priceOf(a) - priceOf(b))
34
+ || (b.contextWindow - a.contextWindow)
35
+ || (b.maxTokens - a.maxTokens)
36
+ || `${a.provider}/${a.id}`.localeCompare(`${b.provider}/${b.id}`);
37
+ }
38
+
39
+ /**
40
+ * Best sub-LLM among the models whose provider has configured auth.
41
+ *
42
+ * Single pass rather than `[...models].sort()[0]`: the copy and the sort both allocate for a
43
+ * result that is one element.
44
+ */
45
+ export function cheapestModel(registry: ModelRegistry): Model<Api> | undefined {
46
+ const models = registry.getAvailable();
47
+ let best: Model<Api> | undefined;
48
+ for (let i = 0; i < models.length; i++) {
49
+ const model = models[i];
50
+ if (model === undefined) continue;
51
+ if (best === undefined || compareLlm(model, best) < 0) best = model;
52
+ }
53
+ return best;
54
+ }
@@ -1,42 +1,35 @@
1
1
  /**
2
2
  * RlmController — holds RLM config + chosen models.
3
3
  *
4
- * The engine drives the root model turn-by-turn over ```repl``` blocks with full budget/token/
4
+ * The engine drives the root model turn-by-turn over ```repl``` blocks with full token/
5
5
  * timeout/error guards, compaction, and a finalize fallback. `start()` returns a RunHandle with
6
6
  * the completion promise.
7
7
  */
8
8
 
9
9
  import type { Api, Model } from "@earendil-works/pi-ai";
10
- import type { ExtensionContext, ModelRegistry } from "@earendil-works/pi-coding-agent";
11
- import { DEFAULT_RUN_DIR } from "../config/defaults.ts";
10
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
12
11
  import { modelRef, resolveModelId, saveSettings } from "../config/settings.ts";
13
12
  import { createEngine } from "../core/engine.ts";
14
13
  import { limitsFromConfig } from "../core/limits.ts";
15
- import type { InteractiveDeps, RlmConfig, RlmInput, RlmResult } from "../core/types.ts";
16
- import type { ReconstructResult } from "../state/resume.ts";
14
+ import type { RlmConfig, RlmResult } from "../core/types.ts";
17
15
  import { packRepository, serializeForSandbox } from "../context/repomix-context.ts";
18
16
  import { RlmEmitter } from "../tool/rlm-events.ts";
19
17
  import { formatError } from "../util/errors.ts";
20
-
21
- export function cheapestModel(registry: ModelRegistry): Model<Api> | undefined {
22
- const models = registry.getAvailable();
23
- if (models.length === 0) return undefined;
24
- return [...models].sort((a, b) => a.cost.input + a.cost.output - (b.cost.input + b.cost.output))[0];
25
- }
18
+ import { cheapestModel } from "./llm-model.ts";
26
19
 
27
20
  export interface RunHandle {
28
21
  readonly abort: () => void;
29
22
  readonly done: Promise<RlmResult>;
30
23
  }
31
24
 
32
- /** B5+SA: discriminated union removes non-null `!` assertions and the `context: ""` hack. */
33
- export type StartInput =
34
- | { readonly kind: "fresh"; readonly rootPrompt: string; readonly context: unknown }
35
- | { readonly kind: "resume"; readonly resume: ReconstructResult & { ok: true }; readonly context: unknown };
25
+ export interface StartInput {
26
+ readonly rootPrompt: string;
27
+ readonly context: unknown;
28
+ }
36
29
 
37
30
  export class RlmController {
38
- workerModel: Model<Api> | undefined;
39
- savedWorkerRef: string | undefined;
31
+ llmModel: Model<Api> | undefined;
32
+ savedLlmRef: string | undefined;
40
33
  private active: AbortController | null = null;
41
34
 
42
35
  constructor(public config: RlmConfig) {}
@@ -65,7 +58,7 @@ export class RlmController {
65
58
  async persist(): Promise<boolean> {
66
59
  return await saveSettings({
67
60
  config: this.config,
68
- worker: modelRef(this.workerModel) ?? this.savedWorkerRef,
61
+ llm: modelRef(this.llmModel) ?? this.savedLlmRef,
69
62
  });
70
63
  }
71
64
 
@@ -77,15 +70,15 @@ export class RlmController {
77
70
  this.active?.abort();
78
71
  }
79
72
 
80
- resolveModels(ctx: ExtensionContext): { model: Model<Api>; worker: Model<Api> } | undefined {
81
- if (!this.workerModel && this.savedWorkerRef) this.workerModel = resolveModelId(ctx.modelRegistry, this.savedWorkerRef);
73
+ resolveModels(ctx: ExtensionContext): { model: Model<Api>; llm: Model<Api> } | undefined {
74
+ if (!this.llmModel && this.savedLlmRef) this.llmModel = resolveModelId(ctx.modelRegistry, this.savedLlmRef);
82
75
  const model = ctx.model ?? cheapestModel(ctx.modelRegistry);
83
76
  if (!model) return undefined;
84
- const worker = this.workerModel ?? cheapestModel(ctx.modelRegistry) ?? model;
85
- return { model, worker };
77
+ const llm = this.llmModel ?? cheapestModel(ctx.modelRegistry) ?? model;
78
+ return { model, llm };
86
79
  }
87
80
 
88
- start(ctx: ExtensionContext, input: StartInput, emitter?: RlmEmitter, interactive?: InteractiveDeps): RunHandle {
81
+ start(ctx: ExtensionContext, input: StartInput, emitter?: RlmEmitter): RunHandle {
89
82
  const models = this.resolveModels(ctx);
90
83
  if (!models) throw new Error("no model with configured auth is available");
91
84
  if (this.active) throw new Error("RLM run already in progress"); // QC: mutual-exclusion guard
@@ -93,50 +86,26 @@ export class RlmController {
93
86
  const abortController = new AbortController();
94
87
  this.active = abortController;
95
88
 
96
- const runState = this.config.runLog?.enabled !== false
97
- ? { cwd: ctx.cwd ?? process.cwd(), dir: this.config.runLog?.dir ?? DEFAULT_RUN_DIR, snapshot: this.config.runLog?.snapshot !== false }
98
- : undefined;
99
-
100
89
  const done = (async () => {
101
- let engineInput: RlmInput;
102
- if (input.kind === "fresh") {
103
- // Auto-pack empty/undefined context via repomix; pass explicit context through.
104
- let contextValue: unknown = input.context;
105
- if (contextValue === undefined || contextValue === "" || (typeof contextValue === "string" && contextValue.trim() === "")) {
106
- const cwd = ctx.cwd ?? process.cwd();
107
- const result = await packRepository(cwd, abortController.signal);
108
- if (result.ok) {
109
- contextValue = serializeForSandbox(result.value);
110
- } else {
111
- contextValue = formatError(`failed to pack repository — ${result.error}`);
112
- }
113
- }
114
- engineInput = {
115
- rootPrompt: input.rootPrompt,
116
- context: contextValue,
117
- depth: 0,
118
- };
119
- } else {
120
- engineInput = {
121
- rootPrompt: input.resume.header.rootPrompt,
122
- context: input.context, // B5: load the actual context from the sidecar, not ""
123
- depth: 0,
124
- resume: input.resume,
125
- };
90
+ // Auto-pack empty/undefined context via repomix; pass explicit context through.
91
+ let contextValue: unknown = input.context;
92
+ if (contextValue === undefined || (typeof contextValue === "string" && contextValue.trim() === "")) {
93
+ const cwd = ctx.cwd ?? process.cwd();
94
+ const result = await packRepository(cwd, abortController.signal);
95
+ contextValue = result.ok
96
+ ? serializeForSandbox(result.value)
97
+ : formatError(`failed to pack repository — ${result.error}`);
126
98
  }
127
99
  const engine = createEngine({
128
100
  model: models.model,
129
- workerModel: models.worker,
101
+ llmModel: models.llm,
130
102
  registry: ctx.modelRegistry,
131
103
  config: this.config,
132
104
  signal: abortController.signal,
133
105
  emitter: emitter ?? new RlmEmitter(),
134
- runState,
135
- onAskUserQuestion: interactive?.onAskUserQuestion,
136
- onTodo: interactive?.onTodo,
137
106
  limits: limitsFromConfig(this.config),
138
107
  });
139
- return await engine(engineInput);
108
+ return await engine({ rootPrompt: input.rootPrompt, context: contextValue, depth: 0 });
140
109
  })().finally(() => {
141
110
  if (this.active === abortController) this.active = null;
142
111
  });