@hicaru/pi-rlm 0.3.6 → 0.3.9
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 +2 -2
- package/package.json +1 -1
- package/src/bridge/handlers/emitting.ts +5 -23
- package/src/bridge/handlers/index.ts +1 -1
- package/src/bridge/handlers/llm-query.ts +84 -29
- package/src/bridge/handlers/rlm-query.ts +133 -33
- package/src/bridge/handlers/types.ts +12 -0
- package/src/commands/pins.ts +51 -0
- package/src/commands/rlm-config.ts +4 -88
- package/src/commands/rlm-llm.ts +59 -0
- package/src/commands/rlm-rlm.ts +58 -0
- package/src/commands/rlm.ts +2 -2
- package/src/config/defaults.ts +17 -0
- package/src/config/settings.ts +58 -5
- package/src/core/answer.ts +7 -10
- package/src/core/budget.ts +182 -0
- package/src/core/compaction.ts +46 -0
- package/src/core/engine.ts +185 -5
- package/src/core/iteration.ts +5 -0
- package/src/core/ledger.ts +343 -0
- package/src/core/memory.ts +589 -0
- package/src/core/model-registry.ts +88 -0
- package/src/core/types.ts +44 -3
- package/src/index.ts +107 -12
- package/src/mode/rlm-mode.ts +58 -10
- package/src/prompts/glossary.ts +147 -57
- package/src/prompts/native.ts +12 -7
- package/src/prompts/system.ts +22 -7
- package/src/prompts/user.ts +6 -3
- package/src/sandbox/interrupts.ts +24 -0
- package/src/sandbox/protocol.ts +69 -5
- package/src/sandbox/py/__pycache__/guards.cpython-314.pyc +0 -0
- package/src/sandbox/py/__pycache__/scaffold.cpython-314.pyc +0 -0
- package/src/sandbox/py/guards.py +11 -6
- package/src/sandbox/py/scaffold.py +615 -0
- package/src/sandbox/py/worker.py +53 -506
- package/src/sandbox/sandbox.ts +21 -3
- package/src/text/repl-output.ts +15 -0
- package/src/tool/repl-render.ts +4 -10
- package/src/tool/repl-result.ts +54 -10
- package/src/tool/repl-tool.ts +50 -3
- package/src/tool/rlm-aggregator.ts +16 -3
- package/src/tool/rlm-details.ts +7 -0
- package/src/tool/rlm-events.ts +17 -1
- package/src/tool/rlm-tool.ts +25 -14
- package/src/tool/subcall-render.ts +14 -129
- package/src/tool/subcall-store.ts +11 -1
- package/src/ui/intro.ts +13 -4
- package/src/ui/modal/agent-modal.ts +104 -0
- package/src/ui/modal/modal-view.ts +132 -0
- package/src/ui/modal/timeline-store.ts +85 -0
- package/src/ui/model-picker/drilldown.ts +173 -0
- package/src/ui/model-picker/grouping.ts +81 -0
- package/src/ui/model-picker/levels.ts +63 -0
- package/src/ui/model-picker.ts +7 -197
- package/src/ui/panel/run-registry.ts +135 -0
- package/src/ui/panel/tree-panel.ts +46 -0
- package/src/ui/status.ts +26 -10
- package/src/ui/theme.ts +0 -4
- package/src/ui/tree/tree-model.ts +221 -0
- package/src/ui/tree/tree-rows.ts +73 -0
- package/src/ui/tree/tree-widget.ts +186 -0
- package/src/util/concurrency.ts +47 -0
package/src/sandbox/sandbox.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { fileURLToPath } from "node:url";
|
|
|
14
14
|
import {
|
|
15
15
|
isInterrupt,
|
|
16
16
|
isWorkerMessage,
|
|
17
|
+
parsePendingTasks,
|
|
17
18
|
type ParentMessage,
|
|
18
19
|
type ReplResult,
|
|
19
20
|
type WorkerMessage,
|
|
@@ -29,6 +30,9 @@ export type { AddContextResult, SubcallOpts, SubLlmHandlers } from "./interrupts
|
|
|
29
30
|
export interface SandboxOptions {
|
|
30
31
|
/** Sandbox recursion depth label (passed to the worker, used in interrupt routing). */
|
|
31
32
|
readonly depth?: number;
|
|
33
|
+
/** v5 role separation: "child" sandboxes install the delegation-only scaffold (no
|
|
34
|
+
* search/grep_context/outline/add_context — retrieval belongs to the root). */
|
|
35
|
+
readonly surface?: "root" | "child";
|
|
32
36
|
/** Per-`repl`-block wall-clock timeout inside the worker (seconds). */
|
|
33
37
|
readonly execTimeoutS?: number;
|
|
34
38
|
/** Parent-side watchdog per request (ms); on breach the worker is SIGKILLed. */
|
|
@@ -52,6 +56,8 @@ export interface SandboxOptions {
|
|
|
52
56
|
|
|
53
57
|
const WORKER_PATH = join(dirname(fileURLToPath(import.meta.url)), "py", "worker.py");
|
|
54
58
|
const STDERR_TAIL_CHARS = 8_192;
|
|
59
|
+
/** How often to refresh the parent request watchdog (and ping the worker) during silent host work. */
|
|
60
|
+
export const SANDBOX_WATCHDOG_HEARTBEAT_MS = 30_000;
|
|
55
61
|
/** How long dispose() waits for a clean worker exit before escalating to SIGKILL. */
|
|
56
62
|
const SHUTDOWN_GRACE_MS = 50;
|
|
57
63
|
|
|
@@ -118,6 +124,7 @@ export class PythonSandbox {
|
|
|
118
124
|
"-X", "utf8=1",
|
|
119
125
|
"-u", WORKER_PATH,
|
|
120
126
|
"--depth", String(opts.depth ?? 1),
|
|
127
|
+
"--surface", opts.surface === "child" ? "child" : "root",
|
|
121
128
|
"--timeout", String(opts.execTimeoutS ?? 600),
|
|
122
129
|
];
|
|
123
130
|
if (opts.maxPromptChars !== undefined) {
|
|
@@ -224,6 +231,7 @@ export class PythonSandbox {
|
|
|
224
231
|
raised: res.raised ?? false,
|
|
225
232
|
executionTimeMs: Math.round((res.execution_time ?? 0) * 1000),
|
|
226
233
|
varNames: res.var_names ?? [],
|
|
234
|
+
pendingTasks: parsePendingTasks(res.pending_tasks),
|
|
227
235
|
};
|
|
228
236
|
}
|
|
229
237
|
|
|
@@ -312,12 +320,22 @@ export class PythonSandbox {
|
|
|
312
320
|
}
|
|
313
321
|
|
|
314
322
|
/**
|
|
315
|
-
* Refresh the parent-side request watchdog
|
|
316
|
-
* Used during long mid-exec work that does not
|
|
317
|
-
*
|
|
323
|
+
* Refresh the parent-side request watchdog and ping the worker.
|
|
324
|
+
* Used during long mid-exec work that does not produce additional worker
|
|
325
|
+
* interrupts on this sandbox. The heartbeat rearms the worker's stall alarm
|
|
326
|
+
* so a healthy long sub-call is not reported as `_StallTimeout`.
|
|
318
327
|
*/
|
|
319
328
|
refreshWatchdog(): void {
|
|
320
329
|
this.touchPending();
|
|
330
|
+
if (this.hasPendingRequest()) this.send({ type: "heartbeat" });
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** True when an exec/load_context/shutdown (not the init handshake) is in flight. */
|
|
334
|
+
private hasPendingRequest(): boolean {
|
|
335
|
+
for (const id of this.pending.keys()) {
|
|
336
|
+
if (id !== "_init") return true;
|
|
337
|
+
}
|
|
338
|
+
return false;
|
|
321
339
|
}
|
|
322
340
|
|
|
323
341
|
private send(msg: ParentMessage): void {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared REPL output formatting used by headless history and native repl() tool results.
|
|
3
|
+
* Lives in text/ so tool/ does not import from core/.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { truncateOutput } from "./parsing.ts";
|
|
7
|
+
|
|
8
|
+
/** Max stderr kept in model-visible REPL output (headless history and native tool_result). */
|
|
9
|
+
export const STDERR_LIMIT = 8_000;
|
|
10
|
+
|
|
11
|
+
/** Prefix stderr so the model can tell prints from exceptions. Empty when stderr is blank. */
|
|
12
|
+
export function formatReplStderr(stderr: string, limit = STDERR_LIMIT): string {
|
|
13
|
+
const err = stderr.trim();
|
|
14
|
+
return err ? `\n[stderr]\n${truncateOutput(err, limit)}` : "";
|
|
15
|
+
}
|
package/src/tool/repl-render.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
/** repl() tool TUI views —
|
|
1
|
+
/** repl() tool TUI views — the single-line card and the expanded output view.
|
|
2
|
+
* Sub-call trees are not rendered here; the live tree widget owns agent visualization. */
|
|
2
3
|
|
|
3
4
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
4
5
|
import { Container, Spacer, Text } from "@earendil-works/pi-tui";
|
|
5
6
|
import type { ReplDetails } from "./repl-details.ts";
|
|
6
|
-
import { cardHeader, cardStatsLine, renderCollapsedCard
|
|
7
|
+
import { cardHeader, cardStatsLine, renderCollapsedCard } from "./subcall-render.ts";
|
|
7
8
|
|
|
8
9
|
/** Chars of stdout/stderr shown in the expanded view. */
|
|
9
10
|
const EXPANDED_STDOUT_CHARS = 2_000;
|
|
@@ -17,7 +18,7 @@ export function replStats(details: ReplDetails, theme: Theme): string {
|
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
export function renderReplCollapsed(details: ReplDetails, theme: Theme): Text {
|
|
20
|
-
return renderCollapsedCard("REPL", details.status, replStats(details, theme),
|
|
21
|
+
return renderCollapsedCard("REPL", details.status, replStats(details, theme), theme);
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
// ── Expanded view ──
|
|
@@ -47,12 +48,5 @@ export function renderReplExpanded(details: ReplDetails, theme: Theme): Containe
|
|
|
47
48
|
container.addChild(new Text(theme.fg("error", details.stderr.slice(0, EXPANDED_STDERR_CHARS)), 0, 0));
|
|
48
49
|
}
|
|
49
50
|
|
|
50
|
-
// Sub-call tree
|
|
51
|
-
if (details.subcalls.length > 0) {
|
|
52
|
-
container.addChild(new Spacer(1));
|
|
53
|
-
container.addChild(new Text(theme.fg("muted", "─── Sub-calls ───"), 0, 0));
|
|
54
|
-
container.addChild(renderExpandedSubcallTree(details.subcalls, theme));
|
|
55
|
-
}
|
|
56
|
-
|
|
57
51
|
return container;
|
|
58
52
|
}
|
package/src/tool/repl-result.ts
CHANGED
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { RlmSubcall } from "./rlm-details.ts";
|
|
8
|
+
import type { PendingTaskInfo } from "../sandbox/protocol.ts";
|
|
8
9
|
import { capReplResultText, replDelegationNudge } from "../mode/native-guards.ts";
|
|
10
|
+
import { formatReplStderr } from "../text/repl-output.ts";
|
|
9
11
|
|
|
10
12
|
/** Model-visible text assembled from a repl() result. */
|
|
11
13
|
export interface ReplResultText {
|
|
@@ -30,23 +32,20 @@ export function buildReplResultText(
|
|
|
30
32
|
subcalls: readonly RlmSubcall[],
|
|
31
33
|
backgroundPending = 0,
|
|
32
34
|
varNames: readonly string[] = [],
|
|
35
|
+
stderr = "",
|
|
36
|
+
raised = false,
|
|
37
|
+
pendingTasks: readonly PendingTaskInfo[] = [],
|
|
33
38
|
): ReplResultText {
|
|
34
39
|
const answerSubmitted = finalAnswer !== undefined;
|
|
35
|
-
const
|
|
36
|
-
const
|
|
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}`;
|
|
40
|
+
const stderrBlock = formatReplStderr(stderr);
|
|
41
|
+
const rawText = assembleReplBody(stdout, finalAnswer, varNames, stderrBlock, raised);
|
|
43
42
|
// Model-visible text is capped; the caller keeps full stdout in `details` for the TUI.
|
|
44
43
|
const cappedText = capReplResultText(rawText) ?? rawText;
|
|
45
44
|
const delegated = subcalls.some((s) => s.kind === "llm" || s.kind === "batch" || s.kind === "rlm");
|
|
46
|
-
const nudge = answerSubmitted ? undefined : replDelegationNudge(rawText.length, delegated);
|
|
45
|
+
const nudge = answerSubmitted || raised ? undefined : replDelegationNudge(rawText.length, delegated);
|
|
47
46
|
const failedBg = subcalls.filter((s) => s.id.startsWith("bg") && s.status === "error").length;
|
|
48
47
|
const pendingLine = backgroundPending > 0
|
|
49
|
-
? `\n\n[rlm] ${backgroundPending} background task(s) still running —
|
|
48
|
+
? `\n\n[rlm] ${backgroundPending} background task(s) still running — ${pendingCollectHint(pendingTasks)}.`
|
|
50
49
|
: "";
|
|
51
50
|
const failedLine = failedBg > 0
|
|
52
51
|
? `\n[rlm] ${failedBg} background sub-call(s) FAILED — their await_task value is an "Error: …" string, not data.`
|
|
@@ -54,6 +53,51 @@ export function buildReplResultText(
|
|
|
54
53
|
return { text: cappedText + (nudge ?? "") + pendingLine + failedLine };
|
|
55
54
|
}
|
|
56
55
|
|
|
56
|
+
function assembleReplBody(
|
|
57
|
+
stdout: string,
|
|
58
|
+
finalAnswer: string | undefined,
|
|
59
|
+
varNames: readonly string[],
|
|
60
|
+
stderrBlock: string,
|
|
61
|
+
raised: boolean,
|
|
62
|
+
): string {
|
|
63
|
+
if (finalAnswer !== undefined) {
|
|
64
|
+
return `ANSWER_SUBMITTED (${finalAnswer.length} chars) — delivered to user. Do not restate it.`;
|
|
65
|
+
}
|
|
66
|
+
if (raised) {
|
|
67
|
+
const body = [stdout.trimEnd(), stderrBlock.trimStart()].filter((s) => s.length > 0).join("\n");
|
|
68
|
+
return body.length > 0 ? body : "(raised — see [stderr])";
|
|
69
|
+
}
|
|
70
|
+
const noOutput = !stdout;
|
|
71
|
+
const varsHint = noOutput && varNames.length > 0
|
|
72
|
+
? ` — the block ran fine and these REPL vars are defined: ${varNames.join(", ")}. `
|
|
73
|
+
+ "Do NOT re-run it; read them in the next block."
|
|
74
|
+
: "";
|
|
75
|
+
return stdout || `(no output)${varsHint}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Name the Python handle when we know it; otherwise point at await_task() / list_tasks(). */
|
|
79
|
+
function pendingCollectHint(pendingTasks: readonly PendingTaskInfo[]): string {
|
|
80
|
+
if (pendingTasks.length === 0) {
|
|
81
|
+
return "collect with await_task(<Task var>) or await_task(); answers[] is for results AFTER await_task";
|
|
82
|
+
}
|
|
83
|
+
const names = new Array<string>(pendingTasks.length);
|
|
84
|
+
for (let i = 0; i < pendingTasks.length; i++) {
|
|
85
|
+
const t = pendingTasks[i];
|
|
86
|
+
const kind = t?.kind ?? "task";
|
|
87
|
+
const label = t?.label ?? "";
|
|
88
|
+
names[i] = t?.var ?? `<${kind}${label.length > 0 ? ` ${label}` : ""}>`;
|
|
89
|
+
}
|
|
90
|
+
if (pendingTasks.length === 1) {
|
|
91
|
+
const only = names[0] ?? "";
|
|
92
|
+
const first = pendingTasks[0];
|
|
93
|
+
const detail = first !== undefined && first.label.length > 0 ? ` (${first.kind} ${first.label})` : "";
|
|
94
|
+
return only.startsWith("<")
|
|
95
|
+
? `collect with await_task()${detail}; answers[] is for results AFTER await_task`
|
|
96
|
+
: `collect with await_task(${only})${detail}; answers[] is for results AFTER await_task`;
|
|
97
|
+
}
|
|
98
|
+
return `collect with await_task() (${names.join(", ")}); answers[] is for results AFTER await_task`;
|
|
99
|
+
}
|
|
100
|
+
|
|
57
101
|
/** Advisory diagnostics derived from a completed invocation's sub-calls. */
|
|
58
102
|
export function collectReplWarnings(subcalls: readonly RlmSubcall[]): readonly string[] | undefined {
|
|
59
103
|
let failed = 0;
|
package/src/tool/repl-tool.ts
CHANGED
|
@@ -28,13 +28,17 @@ import type { RlmConfig, RlmInput, RlmResult } from "../core/types.ts";
|
|
|
28
28
|
import { SandboxManager } from "../sandbox/sandbox-manager.ts";
|
|
29
29
|
import type { SubcallOpts } from "../sandbox/sandbox.ts";
|
|
30
30
|
import { createSubcallHandlers, type Invocation } from "../bridge/handlers/index.ts";
|
|
31
|
+
import { TaskLedger } from "../core/ledger.ts";
|
|
32
|
+
import type { MemoryStore } from "../core/memory.ts";
|
|
31
33
|
import { BackgroundTasks } from "./background-tasks.ts";
|
|
32
34
|
import type { ReplResult } from "../sandbox/protocol.ts";
|
|
33
35
|
import { RlmEmitter } from "./rlm-events.ts";
|
|
36
|
+
import type { RunRegistry } from "../ui/panel/run-registry.ts";
|
|
34
37
|
import { SubcallStore } from "./subcall-store.ts";
|
|
35
38
|
import type { ReplDetails } from "./repl-details.ts";
|
|
36
39
|
import type { RlmSubcall } from "./rlm-details.ts";
|
|
37
40
|
import { createEngine } from "../core/engine.ts";
|
|
41
|
+
import { modelRef } from "../config/settings.ts";
|
|
38
42
|
import { spinnerFrame } from "../ui/theme.ts";
|
|
39
43
|
import { previewText } from "../text/preview.ts";
|
|
40
44
|
import { errorMessage } from "../util/errors.ts";
|
|
@@ -110,8 +114,15 @@ export interface ReplToolDeps {
|
|
|
110
114
|
readonly getConfig: () => RlmConfig;
|
|
111
115
|
/** Session-wide sub-call admission, shared with every child engine this tool spawns. */
|
|
112
116
|
readonly gates: SubcallGates;
|
|
117
|
+
/** v5: live re-resolution of the session gates (provider caps change via /rlm-config without
|
|
118
|
+
* a restart). Falls back to `gates` when omitted. Read lazily per sub-call. */
|
|
119
|
+
readonly resolveGates?: () => SubcallGates;
|
|
113
120
|
/** Session-scoped home for detached spawn() work. */
|
|
114
121
|
readonly background: BackgroundTasks;
|
|
122
|
+
/** Session tree panel index; omitted → runs don't appear in the widget. */
|
|
123
|
+
readonly runRegistry?: RunRegistry;
|
|
124
|
+
/** v5 durable memory (session-wide `.rlm` store); omitted → memory off for this tool. */
|
|
125
|
+
readonly memory?: MemoryStore;
|
|
115
126
|
readonly signal?: AbortSignal;
|
|
116
127
|
readonly onUsage?: (usage: Usage, role: "sub") => void;
|
|
117
128
|
readonly ensureContext?: () => Promise<void>;
|
|
@@ -127,12 +138,17 @@ export interface ReplToolDeps {
|
|
|
127
138
|
export function createReplTool(deps: ReplToolDeps): ToolDefinition<typeof ReplToolParams, ReplDetails> {
|
|
128
139
|
const { sandboxManager, llmModel, registry, getConfig, signal, onUsage, background } = deps;
|
|
129
140
|
const bridgeState = new NativeBridgeState(background);
|
|
141
|
+
// v5: one session-wide blackboard for the native repl() path — the same claim/coalesce/
|
|
142
|
+
// demote logic the engine gets per run, shared by every turn and every child it spawns.
|
|
143
|
+
const sessionLedger = new TaskLedger();
|
|
130
144
|
|
|
131
145
|
// Late-bound cwd — getOrCreate installs handlers only at spawn; never rebuild the closure.
|
|
132
146
|
let sessionCwd = process.cwd();
|
|
133
147
|
|
|
134
148
|
const getLlmModel = (): Model<Api> => deps.getLlmModel?.() ?? llmModel;
|
|
135
149
|
const getModel = (): Model<Api> => deps.getModel?.() ?? deps.model;
|
|
150
|
+
// v5 (audit C6): resolve lazily per call so provider-cap edits via /rlm-config apply live.
|
|
151
|
+
const currentGates = (): SubcallGates => deps.resolveGates?.() ?? deps.gates;
|
|
136
152
|
|
|
137
153
|
// Each rlm_query spawns a child RLM with its own sandbox and turn loop, not a flat
|
|
138
154
|
// one-shot llm_query. The engine is created per call so the child's subcalls, turn
|
|
@@ -143,7 +159,8 @@ export function createReplTool(deps: ReplToolDeps): ToolDefinition<typeof ReplTo
|
|
|
143
159
|
registry,
|
|
144
160
|
config: getConfig(),
|
|
145
161
|
signal,
|
|
146
|
-
|
|
162
|
+
memory: deps.memory,
|
|
163
|
+
gates: currentGates(),
|
|
147
164
|
// Same emitter the parent subcall node lives on — see SubcallHandlerDeps.runChild.
|
|
148
165
|
emitter: inv.emitter,
|
|
149
166
|
// Everything a child engine spends is sub-work from this tool's perspective, including
|
|
@@ -156,7 +173,10 @@ export function createReplTool(deps: ReplToolDeps): ToolDefinition<typeof ReplTo
|
|
|
156
173
|
// per-invocation is reached through bridgeState.resolve, not captured here.
|
|
157
174
|
const subcallHandlers = createSubcallHandlers({
|
|
158
175
|
resolve: (opts) => bridgeState.resolve(opts),
|
|
159
|
-
|
|
176
|
+
// Getter, not a captured value: sub-call deps read gates lazily per call.
|
|
177
|
+
get gates(): SubcallGates {
|
|
178
|
+
return currentGates();
|
|
179
|
+
},
|
|
160
180
|
registry,
|
|
161
181
|
getLlmModel,
|
|
162
182
|
getModel,
|
|
@@ -168,6 +188,8 @@ export function createReplTool(deps: ReplToolDeps): ToolDefinition<typeof ReplTo
|
|
|
168
188
|
// earlier repl() reaches a child spawned in a later one. Populated before any interrupt can
|
|
169
189
|
// fire: execute() awaits ensureContext() before getOrCreate().
|
|
170
190
|
getChildContext: () => sandboxManager.contextPayload ?? undefined,
|
|
191
|
+
ledger: sessionLedger,
|
|
192
|
+
memory: deps.memory,
|
|
171
193
|
trackDetached: (task) => background.track(task),
|
|
172
194
|
});
|
|
173
195
|
|
|
@@ -213,7 +235,7 @@ export function createReplTool(deps: ReplToolDeps): ToolDefinition<typeof ReplTo
|
|
|
213
235
|
"repl: free search/outline; fire rlm_batch|map_files|llm_batch as Task (BG); await_task for results.",
|
|
214
236
|
promptGuidelines: [
|
|
215
237
|
"Multi-area analysis: rlm_batch([...]) or map_files(paths, q); free search; await_task — not serial native read.",
|
|
216
|
-
"Always-spawn tools return Task; only await_task has content. Fire-all
|
|
238
|
+
"Always-spawn tools return Task; only await_task has content. Fire-all then await; await_task() collects every still-running Task.",
|
|
217
239
|
"llm_query/llm_batch have no disk — never 'Read path/to/file.ts'; use map_files or rlm_* (see context).",
|
|
218
240
|
],
|
|
219
241
|
parameters: ReplToolParams,
|
|
@@ -232,6 +254,20 @@ export function createReplTool(deps: ReplToolDeps): ToolDefinition<typeof ReplTo
|
|
|
232
254
|
|
|
233
255
|
const emitter = new RlmEmitter();
|
|
234
256
|
const store = new SubcallStore(emitter);
|
|
257
|
+
// Surface the cell in the tree panel for its whole lifetime (unregistered in finally).
|
|
258
|
+
const unregisterRun = deps.runRegistry?.register({
|
|
259
|
+
runId: `repl-${_toolCallId}`,
|
|
260
|
+
label: `repl: ${previewText(params.code, 40)}`,
|
|
261
|
+
emitter,
|
|
262
|
+
subcalls: () => store.getSubcalls(),
|
|
263
|
+
totals: () => store.getTotals(),
|
|
264
|
+
// Root = the session's default (pi) model driving this repl cell, own spend only.
|
|
265
|
+
rootModel: () => {
|
|
266
|
+
const m = getModel();
|
|
267
|
+
return modelRef(m) ?? m.id;
|
|
268
|
+
},
|
|
269
|
+
rootTokens: () => store.getRootUsage().tokens,
|
|
270
|
+
});
|
|
235
271
|
let capturedStdout = "";
|
|
236
272
|
let capturedStderr = "";
|
|
237
273
|
let progressStatus: ReplDetails["status"] = "running";
|
|
@@ -283,6 +319,8 @@ export function createReplTool(deps: ReplToolDeps): ToolDefinition<typeof ReplTo
|
|
|
283
319
|
await sandboxManager.getOrCreate({
|
|
284
320
|
...subcallHandlers,
|
|
285
321
|
...(contextBundle?.handlers ?? {}),
|
|
322
|
+
ledgerClaims: () => Promise.resolve(sessionLedger.listClaims()),
|
|
323
|
+
memoryOp: (op, args) => Promise.resolve(deps.memory?.serviceOp(op, args) ?? "memory off"),
|
|
286
324
|
});
|
|
287
325
|
|
|
288
326
|
// Detect queue contention AFTER sandbox init (initPromise settled, isExecuting now accurate)
|
|
@@ -299,6 +337,11 @@ export function createReplTool(deps: ReplToolDeps): ToolDefinition<typeof ReplTo
|
|
|
299
337
|
}
|
|
300
338
|
|
|
301
339
|
const start = Date.now();
|
|
340
|
+
// v5 blackboard (audit C3 / BUG-1): ancestors are RUNNING engines only — engine.ts
|
|
341
|
+
// brackets each child run with beginRun/endRun. A native cell pushes NOTHING: its
|
|
342
|
+
// spawns claim against an empty stack, so an originator can never echo against
|
|
343
|
+
// itself. Duplicates are caught by the ledger's claim store (exact/near
|
|
344
|
+
// coalescing + rlmBudget demotion), never by silent suppression.
|
|
302
345
|
const result: ReplResult = await sandboxManager.execWithSetup(params.code, () => {
|
|
303
346
|
// Wire per-invocation mutable state only after the serialized exec slot
|
|
304
347
|
// is active. Swapping earlier would let queued repl() calls overwrite
|
|
@@ -349,6 +392,9 @@ export function createReplTool(deps: ReplToolDeps): ToolDefinition<typeof ReplTo
|
|
|
349
392
|
subcalls,
|
|
350
393
|
background.pending,
|
|
351
394
|
result.varNames,
|
|
395
|
+
result.stderr,
|
|
396
|
+
result.raised,
|
|
397
|
+
result.pendingTasks,
|
|
352
398
|
);
|
|
353
399
|
|
|
354
400
|
const details: ReplDetails = {
|
|
@@ -393,6 +439,7 @@ export function createReplTool(deps: ReplToolDeps): ToolDefinition<typeof ReplTo
|
|
|
393
439
|
};
|
|
394
440
|
} finally {
|
|
395
441
|
progress.stop();
|
|
442
|
+
unregisterRun?.();
|
|
396
443
|
for (const off of detachTracers) off();
|
|
397
444
|
store.dispose();
|
|
398
445
|
emitter.shutdown();
|
|
@@ -13,16 +13,17 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import type { AgentToolUpdateCallback } from "@earendil-works/pi-agent-core";
|
|
16
|
-
import type { RlmEmitter, TurnEvent, RootUsageEvent, AnswerEvent, StatusEvent, RootPromptEvent } from "./rlm-events.ts";
|
|
17
|
-
import type { RlmDetails, RlmRunStatus } from "./rlm-details.ts";
|
|
16
|
+
import type { RlmEmitter, TurnEvent, RootUsageEvent, AnswerEvent, StatusEvent, RootPromptEvent, RootPhaseEvent } from "./rlm-events.ts";
|
|
17
|
+
import type { RlmDetails, RlmRunStatus, SubcallPhase } from "./rlm-details.ts";
|
|
18
18
|
import { EmitterListener } from "./emitter-listener.ts";
|
|
19
|
-
import { SubcallStore } from "./subcall-store.ts";
|
|
19
|
+
import { SubcallStore, type SubcallTotals } from "./subcall-store.ts";
|
|
20
20
|
|
|
21
21
|
export class RlmEventAggregator extends EmitterListener {
|
|
22
22
|
private readonly store: SubcallStore;
|
|
23
23
|
|
|
24
24
|
// Root-level state
|
|
25
25
|
private rootStatus: RlmRunStatus = "running";
|
|
26
|
+
private rootPhase?: SubcallPhase;
|
|
26
27
|
private rootPrompt = "";
|
|
27
28
|
private turnCurrent = 0;
|
|
28
29
|
private turnMax = 0;
|
|
@@ -41,6 +42,7 @@ export class RlmEventAggregator extends EmitterListener {
|
|
|
41
42
|
emitter.onAnswer((e) => this.handleAnswer(e)),
|
|
42
43
|
emitter.onStatus((e) => this.handleStatus(e)),
|
|
43
44
|
emitter.onRootPrompt((e) => this.handleRootPrompt(e)),
|
|
45
|
+
emitter.onRootPhase((e) => this.handleRootPhase(e)),
|
|
44
46
|
]);
|
|
45
47
|
}
|
|
46
48
|
|
|
@@ -72,12 +74,18 @@ export class RlmEventAggregator extends EmitterListener {
|
|
|
72
74
|
// No notify — root prompt is set before listeners exist; no TUI re-render needed
|
|
73
75
|
}
|
|
74
76
|
|
|
77
|
+
private handleRootPhase(event: RootPhaseEvent): void {
|
|
78
|
+
this.rootPhase = event.phase;
|
|
79
|
+
this.notify();
|
|
80
|
+
}
|
|
81
|
+
|
|
75
82
|
// ── Read ──
|
|
76
83
|
|
|
77
84
|
/** Snapshot the current accumulated state. O(1). */
|
|
78
85
|
getState(): RlmDetails {
|
|
79
86
|
return {
|
|
80
87
|
status: this.rootStatus,
|
|
88
|
+
rootPhase: this.rootPhase,
|
|
81
89
|
rootPrompt: this.rootPrompt,
|
|
82
90
|
turns: { current: this.turnCurrent, max: this.turnMax },
|
|
83
91
|
subcalls: this.store.getSubcalls(),
|
|
@@ -86,6 +94,11 @@ export class RlmEventAggregator extends EmitterListener {
|
|
|
86
94
|
};
|
|
87
95
|
}
|
|
88
96
|
|
|
97
|
+
/** Root engine's OWN spend (driver-model turns) — never blends sub-call models. */
|
|
98
|
+
getRootUsage(): SubcallTotals {
|
|
99
|
+
return this.store.getRootUsage();
|
|
100
|
+
}
|
|
101
|
+
|
|
89
102
|
// ── Lifecycle ──
|
|
90
103
|
|
|
91
104
|
/** Detach all emitter listeners. Call after the run completes. */
|
package/src/tool/rlm-details.ts
CHANGED
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
|
|
9
9
|
export type SubcallKind = "root" | "rlm" | "llm" | "batch" | "tool";
|
|
10
10
|
export type SubcallStatus = "running" | "done" | "error";
|
|
11
|
+
|
|
12
|
+
/** Live activity of a node while status is "running" — powers the tree/modal UI. */
|
|
13
|
+
export type SubcallPhase = "thinking" | "texting" | "repl" | "waiting" | "spawning";
|
|
11
14
|
export type RlmRunStatus = "running" | "done" | "error" | "aborted";
|
|
12
15
|
|
|
13
16
|
export interface RlmSubcall {
|
|
@@ -20,6 +23,8 @@ export interface RlmSubcall {
|
|
|
20
23
|
readonly label: string;
|
|
21
24
|
readonly model?: string;
|
|
22
25
|
readonly status: SubcallStatus;
|
|
26
|
+
/** Current activity while running (undefined = not reported). */
|
|
27
|
+
readonly phase?: SubcallPhase;
|
|
23
28
|
readonly detail?: string;
|
|
24
29
|
readonly args?: string;
|
|
25
30
|
readonly resultPreview?: string;
|
|
@@ -35,6 +40,8 @@ export interface RlmSubcall {
|
|
|
35
40
|
|
|
36
41
|
export interface RlmDetails {
|
|
37
42
|
readonly status: RlmRunStatus;
|
|
43
|
+
/** Root node's live activity phase (root has no subcall entry). */
|
|
44
|
+
readonly rootPhase?: SubcallPhase;
|
|
38
45
|
readonly rootPrompt: string;
|
|
39
46
|
readonly turns: { readonly current: number; readonly max: number };
|
|
40
47
|
readonly subcalls: readonly RlmSubcall[];
|
package/src/tool/rlm-events.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { EventEmitter } from "node:events";
|
|
14
|
-
import type { SubcallKind, SubcallStatus, RlmRunStatus } from "./rlm-details.ts";
|
|
14
|
+
import type { SubcallKind, SubcallStatus, RlmRunStatus, SubcallPhase } from "./rlm-details.ts";
|
|
15
15
|
|
|
16
16
|
// ── Event payloads ──
|
|
17
17
|
|
|
@@ -32,6 +32,8 @@ export interface SubcallCreatedEvent {
|
|
|
32
32
|
export interface SubcallUpdatedEvent {
|
|
33
33
|
readonly id: string;
|
|
34
34
|
readonly status?: SubcallStatus;
|
|
35
|
+
/** Live activity while running (thinking/repl/waiting…). */
|
|
36
|
+
readonly phase?: SubcallPhase;
|
|
35
37
|
readonly detail?: string;
|
|
36
38
|
readonly args?: string;
|
|
37
39
|
readonly resultPreview?: string;
|
|
@@ -67,6 +69,10 @@ export interface RootPromptEvent {
|
|
|
67
69
|
readonly text: string;
|
|
68
70
|
}
|
|
69
71
|
|
|
72
|
+
export interface RootPhaseEvent {
|
|
73
|
+
readonly phase: SubcallPhase;
|
|
74
|
+
}
|
|
75
|
+
|
|
70
76
|
// ── RlmEmitter ──
|
|
71
77
|
|
|
72
78
|
/**
|
|
@@ -127,6 +133,11 @@ export class RlmEmitter {
|
|
|
127
133
|
this.ee.emit("root-prompt", { text } satisfies RootPromptEvent);
|
|
128
134
|
}
|
|
129
135
|
|
|
136
|
+
/** Set the root node's live activity phase (root-only; children use subcall updates). */
|
|
137
|
+
emitRootPhase(phase: SubcallPhase): void {
|
|
138
|
+
this.ee.emit("root-phase", { phase } satisfies RootPhaseEvent);
|
|
139
|
+
}
|
|
140
|
+
|
|
130
141
|
// ── Subscribe (returns unsubscribe function) ──
|
|
131
142
|
|
|
132
143
|
onSubcallCreated(handler: (event: SubcallCreatedEvent) => void): () => void {
|
|
@@ -149,6 +160,11 @@ export class RlmEmitter {
|
|
|
149
160
|
return () => { this.ee.off("root-usage", handler); };
|
|
150
161
|
}
|
|
151
162
|
|
|
163
|
+
onRootPhase(handler: (event: RootPhaseEvent) => void): () => void {
|
|
164
|
+
this.ee.on("root-phase", handler);
|
|
165
|
+
return () => { this.ee.off("root-phase", handler); };
|
|
166
|
+
}
|
|
167
|
+
|
|
152
168
|
onAnswer(handler: (event: AnswerEvent) => void): () => void {
|
|
153
169
|
this.ee.on("answer", handler);
|
|
154
170
|
return () => { this.ee.off("answer", handler); };
|
package/src/tool/rlm-tool.ts
CHANGED
|
@@ -9,19 +9,16 @@ 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
11
|
import type { RlmController, StartInput } from "../mode/rlm-mode.ts";
|
|
12
|
+
import { modelRef } from "../config/settings.ts";
|
|
12
13
|
import { spinnerFrame } from "../ui/theme.ts";
|
|
14
|
+
import type { RunRegistry } from "../ui/panel/run-registry.ts";
|
|
13
15
|
import { markdownTheme } from "../ui/theme-adapter.ts";
|
|
14
16
|
import { previewText } from "../text/preview.ts";
|
|
15
17
|
import { errorMessage } from "../util/errors.ts";
|
|
16
18
|
import { type RlmDetails } from "./rlm-details.ts";
|
|
17
19
|
import { RlmEmitter } from "./rlm-events.ts";
|
|
18
20
|
import { RlmEventAggregator } from "./rlm-aggregator.ts";
|
|
19
|
-
import {
|
|
20
|
-
cardHeader,
|
|
21
|
-
cardStatsLine,
|
|
22
|
-
renderCollapsedCard,
|
|
23
|
-
renderExpandedSubcallTree,
|
|
24
|
-
} from "./subcall-render.ts";
|
|
21
|
+
import { cardHeader, cardStatsLine, renderCollapsedCard } from "./subcall-render.ts";
|
|
25
22
|
import { createProgressNotifier, validateToolParams } from "./tool-utils.ts";
|
|
26
23
|
|
|
27
24
|
/** Chars of the prompt shown on the tool call line. */
|
|
@@ -43,7 +40,7 @@ function rootStats(details: RlmDetails, theme: Theme): string {
|
|
|
43
40
|
|
|
44
41
|
// ── Tool definition ──
|
|
45
42
|
|
|
46
|
-
export function createRlmTool(controller: RlmController): ToolDefinition<typeof RlmToolParams, RlmDetails> {
|
|
43
|
+
export function createRlmTool(controller: RlmController, runRegistry?: RunRegistry): ToolDefinition<typeof RlmToolParams, RlmDetails> {
|
|
47
44
|
return {
|
|
48
45
|
name: "rlm",
|
|
49
46
|
label: "RLM",
|
|
@@ -65,6 +62,25 @@ export function createRlmTool(controller: RlmController): ToolDefinition<typeof
|
|
|
65
62
|
const aggregator = new RlmEventAggregator(emitter, onUpdate ?? (() => {}));
|
|
66
63
|
emitter.emitRootPrompt(params.prompt);
|
|
67
64
|
|
|
65
|
+
// Surface the run in the tree panel for its whole lifetime (unregistered in finally).
|
|
66
|
+
const unregisterRun = runRegistry?.register({
|
|
67
|
+
runId: `rlm-${_toolCallId}`,
|
|
68
|
+
label: previewText(params.prompt, 48),
|
|
69
|
+
emitter,
|
|
70
|
+
subcalls: () => aggregator.getState().subcalls,
|
|
71
|
+
totals: () => aggregator.getState().totals,
|
|
72
|
+
rootStatus: () => aggregator.getState().status,
|
|
73
|
+
rootPhase: () => aggregator.getState().rootPhase,
|
|
74
|
+
turns: () => aggregator.getState().turns,
|
|
75
|
+
// Reuses controller.resolveModels — the ONE model-resolution path (DRY); lazy so
|
|
76
|
+
// the pin resolution inside start() is reflected, and it shows own spend only.
|
|
77
|
+
rootModel: () => {
|
|
78
|
+
const m = controller.resolveModels(ctx)?.model;
|
|
79
|
+
return m === undefined ? undefined : modelRef(m) ?? m.id;
|
|
80
|
+
},
|
|
81
|
+
rootTokens: () => aggregator.getRootUsage().tokens,
|
|
82
|
+
});
|
|
83
|
+
|
|
68
84
|
// Wire abort signal to controller
|
|
69
85
|
if (signal) {
|
|
70
86
|
signal.addEventListener("abort", () => controller.abort(), { once: true });
|
|
@@ -102,6 +118,7 @@ export function createRlmTool(controller: RlmController): ToolDefinition<typeof
|
|
|
102
118
|
};
|
|
103
119
|
} finally {
|
|
104
120
|
progress.stop();
|
|
121
|
+
unregisterRun?.();
|
|
105
122
|
aggregator.dispose();
|
|
106
123
|
emitter.shutdown();
|
|
107
124
|
}
|
|
@@ -134,12 +151,6 @@ function renderExpanded(details: RlmDetails, theme: Theme): Component {
|
|
|
134
151
|
const container = new Container();
|
|
135
152
|
container.addChild(new Text(cardHeader("RLM", details.status, rootStats(details, theme), theme), 0, 0));
|
|
136
153
|
|
|
137
|
-
if (details.subcalls.length > 0) {
|
|
138
|
-
container.addChild(new Spacer(1));
|
|
139
|
-
container.addChild(new Text(theme.fg("muted", "─── Sub-calls ───"), 0, 0));
|
|
140
|
-
container.addChild(renderExpandedSubcallTree(details.subcalls, theme));
|
|
141
|
-
}
|
|
142
|
-
|
|
143
154
|
if (details.answer) {
|
|
144
155
|
container.addChild(new Spacer(1));
|
|
145
156
|
container.addChild(new Text(theme.fg("muted", "─── Answer ───"), 0, 0));
|
|
@@ -152,5 +163,5 @@ function renderExpanded(details: RlmDetails, theme: Theme): Component {
|
|
|
152
163
|
// ── Collapsed view ──
|
|
153
164
|
|
|
154
165
|
function renderCollapsed(details: RlmDetails, theme: Theme): Text {
|
|
155
|
-
return renderCollapsedCard("RLM", details.status, rootStats(details, theme),
|
|
166
|
+
return renderCollapsedCard("RLM", details.status, rootStats(details, theme), theme);
|
|
156
167
|
}
|