@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.
- package/README.md +28 -47
- package/README.ru.md +18 -23
- package/README.zh-CN.md +17 -28
- package/package.json +22 -19
- package/src/bridge/add-context.ts +322 -0
- package/src/bridge/subcall-handlers.ts +63 -17
- package/src/commands/rlm-config.ts +47 -18
- package/src/commands/rlm.ts +3 -152
- package/src/config/defaults.ts +8 -18
- package/src/config/settings.ts +13 -34
- package/src/context/anydoc.ts +67 -0
- package/src/context/listing.ts +70 -0
- package/src/context/md-cache.ts +112 -0
- package/src/context/merge.ts +97 -0
- package/src/context/namespace.ts +180 -0
- package/src/context/resolve.ts +122 -0
- package/src/context/source-dir.ts +166 -0
- package/src/context/source-doc.ts +71 -0
- package/src/context/source-git.ts +51 -0
- package/src/context/source-text.ts +45 -0
- package/src/context/types.ts +88 -0
- package/src/context/walk.ts +250 -0
- package/src/core/engine.ts +61 -345
- package/src/core/history.ts +1 -1
- package/src/core/limits.ts +5 -12
- package/src/core/resource-limits.ts +0 -2
- package/src/core/types.ts +10 -38
- package/src/index.ts +92 -54
- package/src/mode/llm-model.ts +54 -0
- package/src/mode/rlm-mode.ts +28 -58
- package/src/prompts/glossary.ts +290 -0
- package/src/prompts/native.ts +127 -0
- package/src/prompts/system.ts +15 -408
- package/src/sandbox/context-file.ts +154 -0
- package/src/sandbox/interrupts.ts +160 -0
- package/src/sandbox/protocol.ts +20 -75
- package/src/sandbox/py/__pycache__/guards.cpython-314.pyc +0 -0
- package/src/sandbox/py/__pycache__/retrieval.cpython-314.pyc +0 -0
- package/src/sandbox/py/__pycache__/tasks.cpython-314.pyc +0 -0
- package/src/sandbox/py/guards.py +150 -0
- package/src/sandbox/py/retrieval.py +265 -0
- package/src/sandbox/py/tasks.py +129 -0
- package/src/sandbox/py/worker.py +856 -0
- package/src/sandbox/sandbox-manager.ts +24 -9
- package/src/sandbox/sandbox.ts +99 -193
- package/src/text/tokens.ts +31 -5
- package/src/tool/repl-details.ts +2 -2
- package/src/tool/repl-render.ts +58 -0
- package/src/tool/repl-result.ts +70 -0
- package/src/tool/repl-tool.ts +60 -170
- package/src/tool/rlm-aggregator.ts +2 -10
- package/src/tool/rlm-details.ts +0 -2
- package/src/tool/rlm-events.ts +0 -14
- package/src/tool/rlm-tool.ts +2 -13
- package/src/ui/config-panel.ts +12 -20
- package/src/ui/intro.ts +1 -2
- package/src/ui/model-picker.ts +34 -10
- package/src/ui/status.ts +3 -7
- package/src/util/concurrency.ts +9 -5
- package/src/bridge/fallback-todo.ts +0 -148
- package/src/bridge/interactive.ts +0 -65
- package/src/bridge/library.ts +0 -155
- package/src/bridge/pi-interactive.ts +0 -41
- package/src/context/library-context.ts +0 -266
- package/src/context/repomix-context.ts +0 -204
- package/src/core/artifacts.ts +0 -89
- package/src/core/critique.ts +0 -92
- package/src/core/gates.ts +0 -301
- package/src/core/pipeline-handlers.ts +0 -319
- package/src/core/pipeline.ts +0 -268
- package/src/prompts/phases.ts +0 -104
- package/src/sandbox/worker.py +0 -1456
- package/src/state/index.ts +0 -24
- package/src/state/internal.ts +0 -46
- package/src/state/paths.ts +0 -44
- package/src/state/reads.ts +0 -133
- package/src/state/resume.ts +0 -173
- package/src/state/rows.ts +0 -123
- 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
|
+
}
|
package/src/tool/repl-tool.ts
CHANGED
|
@@ -16,16 +16,15 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import { Type } from "typebox";
|
|
19
|
-
import type {
|
|
20
|
-
import {
|
|
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 {
|
|
24
|
-
import {
|
|
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 {
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
|
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
|
|
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
|
|
105
|
+
readonly llmModel: Model<Api>;
|
|
179
106
|
readonly getModel?: () => Model<Api> | undefined;
|
|
180
|
-
readonly
|
|
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.
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
241
|
-
?
|
|
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
|
-
|
|
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 (
|
|
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
|
|
257
|
-
"free primitives search(query) / grep_context(pattern) /
|
|
258
|
-
"semantic reading to map_files / llm_query /
|
|
259
|
-
"(rlm_query for iterative sub-tasks) — stdout
|
|
260
|
-
"so printing file bodies is useless.
|
|
261
|
-
"persist across calls. Also supports
|
|
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
|
|
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
|
-
|
|
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 }
|
|
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
|
|
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
|
|
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
|
|
package/src/tool/rlm-details.ts
CHANGED
|
@@ -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
|
|
package/src/tool/rlm-events.ts
CHANGED
|
@@ -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); };
|
package/src/tool/rlm-tool.ts
CHANGED
|
@@ -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,
|
|
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
|
|
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
|
|