@hicaru/pi-rlm 0.1.9 → 0.2.1
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/package.json +1 -1
- package/src/bridge/fallback-todo.ts +12 -1
- package/src/bridge/subcall-handlers.ts +336 -0
- package/src/commands/rlm-config.ts +8 -8
- package/src/commands/rlm.ts +48 -12
- package/src/config/defaults.ts +4 -1
- package/src/config/settings.ts +33 -3
- package/src/context/repomix-context.ts +5 -10
- package/src/core/answer.ts +4 -3
- package/src/core/artifacts.ts +4 -3
- package/src/core/engine.ts +101 -267
- package/src/core/gates.ts +3 -3
- package/src/core/limits.ts +19 -1
- package/src/core/pipeline-handlers.ts +319 -0
- package/src/core/pipeline.ts +2 -2
- package/src/core/types.ts +25 -27
- package/src/index.ts +63 -17
- package/src/mode/rlm-mode.ts +8 -11
- package/src/prompts/system.ts +164 -52
- package/src/prompts/user.ts +1 -5
- package/src/sandbox/protocol.ts +6 -7
- package/src/sandbox/sandbox-manager.ts +25 -11
- package/src/sandbox/sandbox.ts +93 -22
- package/src/sandbox/worker.py +798 -66
- package/src/state/paths.ts +1 -1
- package/src/state/reads.ts +12 -4
- package/src/state/resume.ts +5 -11
- package/src/text/parsing.ts +0 -6
- package/src/tool/background-tasks.ts +95 -0
- package/src/tool/repl-details.ts +2 -0
- package/src/tool/repl-tool.ts +223 -318
- package/src/tool/rlm-details.ts +0 -10
- package/src/tool/rlm-events.ts +10 -2
- package/src/tool/rlm-tool.ts +18 -31
- package/src/tool/subcall-render.ts +75 -11
- package/src/tool/subcall-store.ts +57 -1
- package/src/ui/config-panel.ts +41 -21
- package/src/ui/intro.ts +2 -1
- package/src/ui/status.ts +8 -5
- package/src/ui/theme-adapter.ts +36 -0
- package/src/ui/theme.ts +0 -25
- package/src/util/concurrency.ts +87 -13
- package/src/util/trace.ts +42 -0
- package/src/bridge/llm-query.ts +0 -133
- package/src/bridge/rlm-query.ts +0 -122
- package/src/mode/input-router.ts +0 -23
package/src/tool/repl-tool.ts
CHANGED
|
@@ -6,9 +6,13 @@
|
|
|
6
6
|
* and collects sub-calls manually from emitter events. No RlmEventAggregator is used
|
|
7
7
|
* (ReplDetails ≠ RlmDetails structural mismatch).
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* Sub-call handling itself lives in bridge/subcall-handlers.ts; this file only supplies the
|
|
10
|
+
* per-invocation Invocation those handlers resolve against, swapping it inside the
|
|
11
|
+
* serialized exec slot so a queued repl() cannot claim the running one's emitter.
|
|
12
|
+
*
|
|
13
|
+
* Work started with `spawn()` may still be running when the call returns, so it resolves to
|
|
14
|
+
* the session-scoped BackgroundTasks registry instead and is drained back into whichever
|
|
15
|
+
* turn is reporting next.
|
|
12
16
|
*/
|
|
13
17
|
|
|
14
18
|
import { Type } from "typebox";
|
|
@@ -16,33 +20,39 @@ import type { Theme, ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
|
16
20
|
import { Container, Spacer, Text } from "@earendil-works/pi-tui";
|
|
17
21
|
import type { Model, Usage, Api } from "@earendil-works/pi-ai";
|
|
18
22
|
import type { ModelRegistry } from "@earendil-works/pi-coding-agent";
|
|
19
|
-
import { modelRef, resolveModelId } from "../config/settings.ts";
|
|
20
23
|
import { buildInteractiveHandlers } from "../bridge/interactive.ts";
|
|
21
24
|
import { buildLibraryHandler } from "../bridge/library.ts";
|
|
22
25
|
import { createPiInteractiveDeps } from "../bridge/pi-interactive.ts";
|
|
23
|
-
import
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
import { LimitGuard } from "../core/limits.ts";
|
|
27
|
-
import { checkResourceLimits } from "../core/resource-limits.ts";
|
|
28
|
-
import type { InteractiveDeps, RlmConfig, Sampling } from "../core/types.ts";
|
|
26
|
+
import type { SubcallGates } from "../util/concurrency.ts";
|
|
27
|
+
import { LimitGuard, limitsFromConfig } from "../core/limits.ts";
|
|
28
|
+
import type { InteractiveDeps, RlmConfig, RlmInput, RlmResult } from "../core/types.ts";
|
|
29
29
|
import { SandboxManager } from "../sandbox/sandbox-manager.ts";
|
|
30
|
-
import type {
|
|
30
|
+
import type { SubcallOpts } from "../sandbox/sandbox.ts";
|
|
31
|
+
import { createSubcallHandlers, type Invocation } from "../bridge/subcall-handlers.ts";
|
|
32
|
+
import { BackgroundTasks } from "./background-tasks.ts";
|
|
31
33
|
import type { ReplResult } from "../sandbox/protocol.ts";
|
|
32
34
|
import { RlmEmitter } from "./rlm-events.ts";
|
|
33
35
|
import { SubcallStore } from "./subcall-store.ts";
|
|
34
36
|
import type { ReplDetails } from "./repl-details.ts";
|
|
35
37
|
import type { RlmSubcall } from "./rlm-details.ts";
|
|
36
38
|
import { createEngine } from "../core/engine.ts";
|
|
37
|
-
import {
|
|
38
|
-
import {
|
|
39
|
+
import { spinnerFrame } from "../ui/theme.ts";
|
|
40
|
+
import { previewText } from "../text/preview.ts";
|
|
41
|
+
import { errorMessage } from "../util/errors.ts";
|
|
39
42
|
import {
|
|
40
|
-
|
|
41
|
-
|
|
43
|
+
cardHeader,
|
|
44
|
+
cardStatsLine,
|
|
45
|
+
renderCollapsedCard,
|
|
42
46
|
renderExpandedSubcallTree,
|
|
43
47
|
} from "./subcall-render.ts";
|
|
44
48
|
import { createProgressNotifier, validateToolParams } from "./tool-utils.ts";
|
|
45
49
|
import { capReplResultText, replDelegationNudge } from "../mode/native-guards.ts";
|
|
50
|
+
import { attachTracer, trace, traceEnabled } from "../util/trace.ts";
|
|
51
|
+
|
|
52
|
+
/** Chars of code shown on the tool call line, and of stdout in the expanded view. */
|
|
53
|
+
const CALL_PREVIEW_CHARS = 80;
|
|
54
|
+
const EXPANDED_STDOUT_CHARS = 2_000;
|
|
55
|
+
const EXPANDED_STDERR_CHARS = 500;
|
|
46
56
|
|
|
47
57
|
// ── Parameter schema ──
|
|
48
58
|
|
|
@@ -50,29 +60,61 @@ export const ReplToolParams = Object.freeze(Type.Object({
|
|
|
50
60
|
code: Type.String({ description: "Python code to execute in the persistent REPL sandbox" }),
|
|
51
61
|
}));
|
|
52
62
|
|
|
63
|
+
/** Last non-empty line of a Python traceback — the `TypeError: …` line, not the frames. */
|
|
64
|
+
function lastLine(text: string): string {
|
|
65
|
+
const lines = text.trimEnd().split("\n");
|
|
66
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
67
|
+
const line = lines[i]?.trim();
|
|
68
|
+
if (line) return line.slice(0, 200);
|
|
69
|
+
}
|
|
70
|
+
return "";
|
|
71
|
+
}
|
|
72
|
+
|
|
53
73
|
/** Model-visible text assembled from a repl() result. */
|
|
54
74
|
export interface ReplResultText {
|
|
55
75
|
readonly text: string;
|
|
56
76
|
}
|
|
57
77
|
|
|
58
78
|
/**
|
|
59
|
-
* Assemble the model-visible text for a repl() result: cap stdout
|
|
60
|
-
*
|
|
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.
|
|
61
89
|
*/
|
|
62
90
|
export function buildReplResultText(
|
|
63
91
|
stdout: string,
|
|
64
92
|
finalAnswer: string | undefined,
|
|
65
93
|
subcalls: readonly RlmSubcall[],
|
|
94
|
+
backgroundPending = 0,
|
|
95
|
+
varNames: readonly string[] = [],
|
|
66
96
|
): ReplResultText {
|
|
67
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
|
+
: "";
|
|
68
103
|
const rawText = answerSubmitted
|
|
69
104
|
? `ANSWER_SUBMITTED (${finalAnswer.length} chars) — delivered to user. Do not restate it.`
|
|
70
|
-
: stdout ||
|
|
105
|
+
: stdout || `(no output)${varsHint}`;
|
|
71
106
|
// Model-visible text is capped; the caller keeps full stdout in `details` for the TUI.
|
|
72
107
|
const cappedText = capReplResultText(rawText) ?? rawText;
|
|
73
108
|
const delegated = subcalls.some((s) => s.kind === "llm" || s.kind === "batch" || s.kind === "rlm");
|
|
74
109
|
const nudge = answerSubmitted ? undefined : replDelegationNudge(rawText.length, delegated);
|
|
75
|
-
|
|
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 };
|
|
76
118
|
}
|
|
77
119
|
|
|
78
120
|
/** Advisory diagnostics derived from a completed invocation's sub-calls. */
|
|
@@ -93,226 +135,41 @@ export function collectReplWarnings(subcalls: readonly RlmSubcall[]): readonly s
|
|
|
93
135
|
// ── Mutable bridge state (handler indirection) ──
|
|
94
136
|
|
|
95
137
|
/**
|
|
96
|
-
* Holds per-invocation
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
138
|
+
* Holds per-invocation state that the sandbox handlers resolve against.
|
|
139
|
+
*
|
|
140
|
+
* The sandbox is created once, so the tool swaps the current Invocation between repl()
|
|
141
|
+
* calls rather than rebuilding handlers (which would lose REPL variable state). Handlers
|
|
142
|
+
* capture the Invocation synchronously at interrupt entry and never re-read it — with
|
|
143
|
+
* spawn() a sub-call can outlive its exec, and a later read would attribute it to whichever
|
|
144
|
+
* turn happened to be current when it resumed.
|
|
145
|
+
*
|
|
146
|
+
* Detached work resolves to the session-scoped background Invocation instead, whose emitter
|
|
147
|
+
* and LimitGuard are not torn down at the end of a turn.
|
|
100
148
|
*/
|
|
101
149
|
class NativeBridgeState {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
currentLimits: LimitGuard | null = null;
|
|
106
|
-
currentInteractive: InteractiveDeps | null = null;
|
|
107
|
-
|
|
108
|
-
swap(inv: { emitter: RlmEmitter; parentId?: string; depth: number; limits: LimitGuard; interactive: InteractiveDeps }): void {
|
|
109
|
-
this.currentEmitter = inv.emitter;
|
|
110
|
-
this.currentParentId = inv.parentId;
|
|
111
|
-
this.currentDepth = inv.depth;
|
|
112
|
-
this.currentLimits = inv.limits;
|
|
113
|
-
this.currentInteractive = inv.interactive;
|
|
114
|
-
}
|
|
150
|
+
private current: Invocation | null = null;
|
|
151
|
+
/** Interactive callbacks for the turn in progress; child engines inherit them. */
|
|
152
|
+
interactive: InteractiveDeps | null = null;
|
|
115
153
|
|
|
116
|
-
|
|
117
|
-
workerModel: Model<Api>;
|
|
118
|
-
getWorkerModel?: () => Model<Api> | undefined;
|
|
119
|
-
registry: ModelRegistry;
|
|
120
|
-
maxPromptChars: number;
|
|
121
|
-
maxConcurrent: number;
|
|
122
|
-
sampling?: Sampling;
|
|
123
|
-
subSystem?: string;
|
|
124
|
-
signal?: AbortSignal;
|
|
125
|
-
}): Pick<SubLlmHandlers, "llmQuery" | "llmQueryBatched"> {
|
|
126
|
-
const state = this;
|
|
127
|
-
|
|
128
|
-
const workerModel = (): Model<Api> => deps.getWorkerModel?.() ?? deps.workerModel;
|
|
129
|
-
const displayModel = (model: string | null): string =>
|
|
130
|
-
modelRef(model ? (resolveModelId(deps.registry, model) ?? workerModel()) : workerModel()) ?? workerModel().id;
|
|
131
|
-
|
|
132
|
-
async function complete1(prompt: string, model: string | null, track: (u: Usage) => void): Promise<string> {
|
|
133
|
-
const limits = state.currentLimits;
|
|
134
|
-
if (limits) {
|
|
135
|
-
const limitError = checkResourceLimits({ budgetUsd: limits.remainingBudgetUsd(), timeoutMs: limits.remainingTimeoutMs() });
|
|
136
|
-
if (limitError !== undefined) return limitError;
|
|
137
|
-
}
|
|
138
|
-
if (prompt.length > deps.maxPromptChars) {
|
|
139
|
-
return formatError(`sub-LLM prompt exceeded size limit (${prompt.length.toLocaleString()} chars > ${deps.maxPromptChars.toLocaleString()})`);
|
|
140
|
-
}
|
|
141
|
-
const resolved = model ? resolveModelId(deps.registry, model) : undefined;
|
|
142
|
-
if (model && !resolved) return formatError(`unknown model override '${model}'`);
|
|
143
|
-
try {
|
|
144
|
-
const messages: ChatMsg[] = [{ role: "user", content: prompt }];
|
|
145
|
-
const res = await modelComplete(messages, {
|
|
146
|
-
model: resolved ?? workerModel(),
|
|
147
|
-
registry: deps.registry,
|
|
148
|
-
system: deps.subSystem,
|
|
149
|
-
maxTokens: deps.sampling?.maxTokens,
|
|
150
|
-
temperature: deps.sampling?.temperature,
|
|
151
|
-
reasoning: deps.sampling?.reasoning,
|
|
152
|
-
signal: deps.signal,
|
|
153
|
-
});
|
|
154
|
-
limits?.addUsage(res.usage);
|
|
155
|
-
track(res.usage);
|
|
156
|
-
return res.text;
|
|
157
|
-
} catch (err) {
|
|
158
|
-
const msg = errorMessage(err);
|
|
159
|
-
const hint = /credit|402|payment|quota|rate.limit/i.test(msg)
|
|
160
|
-
? " — try smaller batches or individual llm_query calls"
|
|
161
|
-
: "";
|
|
162
|
-
return formatError(`${msg}${hint}`);
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
return {
|
|
167
|
-
async llmQuery(prompt, model, _depth) {
|
|
168
|
-
const id = state.currentEmitter?.emitSubcallCreated({
|
|
169
|
-
kind: "llm", parentId: state.currentParentId, label: "llm_query",
|
|
170
|
-
model: displayModel(model), args: `prompt: ${previewText(prompt)}`,
|
|
171
|
-
depth: state.currentDepth,
|
|
172
|
-
});
|
|
173
|
-
let cost = 0; let tokens = 0;
|
|
174
|
-
const out = await complete1(prompt, model, (u) => { cost += u.cost.total; tokens += u.totalTokens; });
|
|
175
|
-
if (id) state.currentEmitter?.emitSubcallUpdated({ id,
|
|
176
|
-
status: isErrorText(out) ? "error" : "done",
|
|
177
|
-
costUsd: cost, tokens, resultPreview: previewText(out),
|
|
178
|
-
detail: isErrorText(out) ? out : undefined,
|
|
179
|
-
});
|
|
180
|
-
return out;
|
|
181
|
-
},
|
|
182
|
-
|
|
183
|
-
async llmQueryBatched(prompts: readonly string[], model, _depth): Promise<string[]> {
|
|
184
|
-
const id = state.currentEmitter?.emitSubcallCreated({
|
|
185
|
-
kind: "batch", parentId: state.currentParentId, label: `llm_query ×${prompts.length}`,
|
|
186
|
-
model: displayModel(model), args: `prompt: ${previewText(prompts[0] ?? "")}`,
|
|
187
|
-
depth: state.currentDepth,
|
|
188
|
-
});
|
|
189
|
-
let cost = 0; let tokens = 0;
|
|
190
|
-
const out: string[] = await mapPool(prompts, deps.maxConcurrent, (p) =>
|
|
191
|
-
complete1(p, model, (u) => { cost += u.cost.total; tokens += u.totalTokens; }),
|
|
192
|
-
);
|
|
193
|
-
const failed = out.filter(isErrorText).length;
|
|
194
|
-
const allFailed = failed === out.length;
|
|
195
|
-
const error = allFailed
|
|
196
|
-
? `all ${out.length} sub-calls failed — reduce batch size or try llm_query individually`
|
|
197
|
-
: failed > 0 ? `${failed}/${out.length} sub-calls failed` : undefined;
|
|
198
|
-
if (id) state.currentEmitter?.emitSubcallUpdated({ id,
|
|
199
|
-
status: error ? "error" : "done", costUsd: cost, tokens,
|
|
200
|
-
resultPreview: previewText(out[0] ?? ""), detail: error,
|
|
201
|
-
failedCount: failed, totalCount: out.length,
|
|
202
|
-
});
|
|
203
|
-
return out;
|
|
204
|
-
},
|
|
205
|
-
};
|
|
206
|
-
}
|
|
154
|
+
constructor(private readonly background: BackgroundTasks) {}
|
|
207
155
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
*
|
|
213
|
-
* At the maxDepth cap the handler degrades to a plain llm_query via the
|
|
214
|
-
* already-wired llmHandlers (which read from the same mutable state).
|
|
215
|
-
*/
|
|
216
|
-
buildRlmHandlers(deps: {
|
|
217
|
-
model: Model<Api>;
|
|
218
|
-
workerModel: Model<Api>;
|
|
219
|
-
getModel?: () => Model<Api> | undefined;
|
|
220
|
-
getWorkerModel?: () => Model<Api> | undefined;
|
|
221
|
-
registry: ModelRegistry;
|
|
222
|
-
config: RlmConfig;
|
|
223
|
-
signal?: AbortSignal;
|
|
224
|
-
onUsage?: (usage: Usage, role: "sub") => void;
|
|
225
|
-
llmHandlers: Pick<SubLlmHandlers, "llmQuery" | "llmQueryBatched">;
|
|
226
|
-
}): Pick<SubLlmHandlers, "rlmQuery" | "rlmQueryBatched"> {
|
|
227
|
-
const state = this;
|
|
228
|
-
|
|
229
|
-
async function rlmQueryImpl(prompt: string, model: string | null, depth: number): Promise<string> {
|
|
230
|
-
const emitter = state.currentEmitter;
|
|
231
|
-
const limits = state.currentLimits;
|
|
232
|
-
if (!emitter || !limits) return formatError("RLM bridge not wired for this invocation");
|
|
233
|
-
|
|
234
|
-
const childDepth = state.currentDepth + 1;
|
|
235
|
-
|
|
236
|
-
// Depth cap: degrade to a one-shot llm_query.
|
|
237
|
-
if (childDepth >= deps.config.maxDepth) {
|
|
238
|
-
return deps.llmHandlers.llmQuery(prompt, model, depth);
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
const remBudget = limits.remainingBudgetUsd();
|
|
242
|
-
const remTimeout = limits.remainingTimeoutMs();
|
|
243
|
-
const limitError = checkResourceLimits({ budgetUsd: remBudget, timeoutMs: remTimeout });
|
|
244
|
-
if (limitError) return limitError;
|
|
245
|
-
|
|
246
|
-
const rootModel = deps.getModel?.() ?? deps.model;
|
|
247
|
-
const workerModel = deps.getWorkerModel?.() ?? deps.workerModel;
|
|
248
|
-
const resolvedOverride = model ? resolveModelId(deps.registry, model) : undefined;
|
|
249
|
-
const subId = emitter.emitSubcallCreated({
|
|
250
|
-
kind: "rlm", parentId: state.currentParentId, label: "rlm_query",
|
|
251
|
-
model: model ? (modelRef(resolvedOverride) ?? `unknown/${model}`) : (modelRef(rootModel) ?? rootModel.id),
|
|
252
|
-
detail: prompt.slice(0, 60),
|
|
253
|
-
depth: childDepth,
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
// Per-call engine creation with the visible emitter — child llm_query subcalls,
|
|
257
|
-
// turn progress, and cost deltas land on the per-invocation emitter, visible to
|
|
258
|
-
// SubcallStore and the live visual tree.
|
|
259
|
-
const runRlm = createEngine({
|
|
260
|
-
model: rootModel,
|
|
261
|
-
workerModel,
|
|
262
|
-
registry: deps.registry,
|
|
263
|
-
config: deps.config,
|
|
264
|
-
signal: deps.signal,
|
|
265
|
-
emitter: emitter,
|
|
266
|
-
onUsage: deps.onUsage as ((usage: Usage, role: "root" | "sub") => void) | undefined,
|
|
267
|
-
limits: {
|
|
268
|
-
maxBudgetUsd: deps.config.maxBudgetUsd,
|
|
269
|
-
maxTimeoutMs: deps.config.maxTimeoutMs,
|
|
270
|
-
maxTokens: deps.config.maxTokens,
|
|
271
|
-
maxErrors: deps.config.maxErrors,
|
|
272
|
-
},
|
|
273
|
-
onTodo: state.currentInteractive?.onTodo,
|
|
274
|
-
onAskUserQuestion: state.currentInteractive?.onAskUserQuestion,
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
try {
|
|
278
|
-
const res = await runRlm({
|
|
279
|
-
rootPrompt: "",
|
|
280
|
-
context: prompt,
|
|
281
|
-
depth: childDepth,
|
|
282
|
-
parentNodeId: subId,
|
|
283
|
-
modelOverride: model ?? undefined,
|
|
284
|
-
remainingBudgetUsd: remBudget,
|
|
285
|
-
remainingTimeoutMs: remTimeout,
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
// Debit parent limit guard for the entire child run.
|
|
289
|
-
limits.addRaw(res.costUsd, res.inputTokens, res.outputTokens);
|
|
290
|
-
|
|
291
|
-
// Child engine emits live usage deltas via the shared emitter — SubcallStore
|
|
292
|
-
// accumulates them. No final aggregate costUsd/tokens to prevent double-counting
|
|
293
|
-
// (matches canonical rlm-query.ts:60-63).
|
|
294
|
-
emitter.emitSubcallUpdated({
|
|
295
|
-
id: subId,
|
|
296
|
-
status: "done",
|
|
297
|
-
resultPreview: res.answer.slice(0, 200),
|
|
298
|
-
});
|
|
156
|
+
swap(inv: Invocation, interactive: InteractiveDeps): void {
|
|
157
|
+
this.current = Object.freeze({ ...inv });
|
|
158
|
+
this.interactive = interactive;
|
|
159
|
+
}
|
|
299
160
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
return formatError(`child RLM failed - ${msg}`);
|
|
305
|
-
}
|
|
306
|
-
}
|
|
161
|
+
/** Detached ⇒ session registry; otherwise the turn that is currently executing. */
|
|
162
|
+
resolve(opts: SubcallOpts): Invocation | null {
|
|
163
|
+
return opts.detached ? this.background.invocation : this.current;
|
|
164
|
+
}
|
|
307
165
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
mapPool(prompts, deps.config.maxConcurrentSubcalls, (p) => rlmQueryImpl(p, model, depth)),
|
|
312
|
-
};
|
|
166
|
+
/** The turn emitter, for library-load reporting. Null between repl() calls. */
|
|
167
|
+
get currentEmitter(): RlmEmitter | null {
|
|
168
|
+
return this.current?.emitter ?? null;
|
|
313
169
|
}
|
|
314
170
|
}
|
|
315
171
|
|
|
172
|
+
|
|
316
173
|
// ── Tool factory ──
|
|
317
174
|
|
|
318
175
|
export interface ReplToolDeps {
|
|
@@ -322,7 +179,12 @@ export interface ReplToolDeps {
|
|
|
322
179
|
readonly getModel?: () => Model<Api> | undefined;
|
|
323
180
|
readonly getWorkerModel?: () => Model<Api> | undefined;
|
|
324
181
|
readonly registry: ModelRegistry;
|
|
325
|
-
|
|
182
|
+
/** Live accessor — `/rlm-config` replaces the config object, so never capture the value. */
|
|
183
|
+
readonly getConfig: () => RlmConfig;
|
|
184
|
+
/** Session-wide sub-call admission, shared with every child engine this tool spawns. */
|
|
185
|
+
readonly gates: SubcallGates;
|
|
186
|
+
/** Session-scoped home for detached spawn() work. */
|
|
187
|
+
readonly background: BackgroundTasks;
|
|
326
188
|
readonly signal?: AbortSignal;
|
|
327
189
|
readonly onUsage?: (usage: Usage, role: "sub") => void;
|
|
328
190
|
readonly ensureContext?: () => Promise<void>;
|
|
@@ -331,40 +193,51 @@ export interface ReplToolDeps {
|
|
|
331
193
|
}
|
|
332
194
|
|
|
333
195
|
export function createReplTool(deps: ReplToolDeps): ToolDefinition<typeof ReplToolParams, ReplDetails> {
|
|
334
|
-
const { sandboxManager, workerModel, registry,
|
|
335
|
-
const bridgeState = new NativeBridgeState();
|
|
196
|
+
const { sandboxManager, workerModel, registry, getConfig, signal, onUsage, background } = deps;
|
|
197
|
+
const bridgeState = new NativeBridgeState(background);
|
|
336
198
|
|
|
337
199
|
// Late-bound cwd — getOrCreate installs handlers only at spawn; never rebuild the closure.
|
|
338
200
|
let sessionCwd = process.cwd();
|
|
339
201
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
202
|
+
const getWorkerModel = (): Model<Api> => deps.getWorkerModel?.() ?? workerModel;
|
|
203
|
+
const getModel = (): Model<Api> => deps.getModel?.() ?? deps.model;
|
|
204
|
+
|
|
205
|
+
// Each rlm_query spawns a child RLM with its own sandbox and turn loop, not a flat
|
|
206
|
+
// one-shot llm_query. The engine is created per call so the child's subcalls, turn
|
|
207
|
+
// progress and cost deltas land on the emitter the parent invocation is using.
|
|
208
|
+
const runChild = (input: RlmInput, inv: Invocation): Promise<RlmResult> => createEngine({
|
|
209
|
+
model: getModel(),
|
|
210
|
+
workerModel: getWorkerModel(),
|
|
345
211
|
registry,
|
|
346
|
-
|
|
347
|
-
maxConcurrent: config.maxConcurrentSubcalls,
|
|
348
|
-
sampling: config.subSampling,
|
|
349
|
-
subSystem: config.subSystemPrompt,
|
|
212
|
+
config: getConfig(),
|
|
350
213
|
signal,
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
214
|
+
gates: deps.gates,
|
|
215
|
+
// Same emitter the parent subcall node lives on — see SubcallHandlerDeps.runChild.
|
|
216
|
+
emitter: inv.emitter,
|
|
217
|
+
// Everything a child engine spends is sub-work from this tool's perspective, including
|
|
218
|
+
// the child's own root turns — so fold both roles into "sub" rather than casting.
|
|
219
|
+
onUsage: onUsage === undefined ? undefined : (usage: Usage) => onUsage(usage, "sub"),
|
|
220
|
+
limits: limitsFromConfig(getConfig()),
|
|
221
|
+
onTodo: bridgeState.interactive?.onTodo,
|
|
222
|
+
onAskUserQuestion: bridgeState.interactive?.onAskUserQuestion,
|
|
223
|
+
})(input);
|
|
224
|
+
|
|
225
|
+
// Built once: the same closures stay correct across repl() calls because everything
|
|
226
|
+
// per-invocation is reached through bridgeState.resolve, not captured here.
|
|
227
|
+
const subcallHandlers = createSubcallHandlers({
|
|
228
|
+
resolve: (opts) => bridgeState.resolve(opts),
|
|
229
|
+
gates: deps.gates,
|
|
360
230
|
registry,
|
|
361
|
-
|
|
231
|
+
getWorkerModel,
|
|
232
|
+
getModel,
|
|
233
|
+
getConfig,
|
|
362
234
|
signal,
|
|
363
235
|
onUsage,
|
|
364
|
-
|
|
236
|
+
runChild,
|
|
237
|
+
trackDetached: (task) => background.track(task),
|
|
365
238
|
});
|
|
366
239
|
|
|
367
|
-
const libraryBundle =
|
|
240
|
+
const libraryBundle = getConfig().libraryLoader
|
|
368
241
|
? buildLibraryHandler({
|
|
369
242
|
getCwd: () => sessionCwd,
|
|
370
243
|
getEmitter: () => bridgeState.currentEmitter,
|
|
@@ -380,14 +253,22 @@ export function createReplTool(deps: ReplToolDeps): ToolDefinition<typeof ReplTo
|
|
|
380
253
|
label: "REPL",
|
|
381
254
|
description:
|
|
382
255
|
"PRIMARY tool for ALL repository reading and analysis (read/grep are disabled in RLM mode). " +
|
|
383
|
-
"Persistent Python sandbox with every file pre-loaded in `context`.
|
|
384
|
-
"
|
|
385
|
-
"
|
|
386
|
-
"
|
|
387
|
-
"
|
|
256
|
+
"Persistent Python sandbox with every file pre-loaded in `context`. Locate first with the " +
|
|
257
|
+
"free primitives search(query) / grep_context(pattern) / outline(path), then delegate the " +
|
|
258
|
+
"semantic reading to map_files / llm_query / llm_query_batched / llm_query_chunked " +
|
|
259
|
+
"(rlm_query for iterative sub-tasks) — stdout returned to you is hard-capped at 4K chars, " +
|
|
260
|
+
"so printing file bodies is useless. Variables, imports, and the `answers`/`plan` memo " +
|
|
261
|
+
"persist across calls. Also supports todo, ask_user_question, and load_library.",
|
|
262
|
+
promptSnippet:
|
|
263
|
+
"repl: run Python in a persistent sandbox holding the whole repository in `context`; " +
|
|
264
|
+
"search/grep_context/outline to locate, map_files/llm_query* to read.",
|
|
265
|
+
promptGuidelines: [
|
|
266
|
+
"In RLM mode, read the repository through `repl` only — `read`/`grep` and bash readers are blocked.",
|
|
267
|
+
"Inside `repl`, locate with search()/grep_context()/outline() before delegating bulk reading to map_files()/llm_query_batched().",
|
|
268
|
+
],
|
|
388
269
|
parameters: ReplToolParams,
|
|
389
270
|
|
|
390
|
-
async execute(_toolCallId, rawParams,
|
|
271
|
+
async execute(_toolCallId, rawParams, execSignal, onUpdate, ctx) {
|
|
391
272
|
const validation = validateToolParams(ReplToolParams, rawParams, "REPL", (errors): ReplDetails => ({
|
|
392
273
|
status: "error",
|
|
393
274
|
output: "",
|
|
@@ -405,24 +286,31 @@ export function createReplTool(deps: ReplToolDeps): ToolDefinition<typeof ReplTo
|
|
|
405
286
|
let capturedStderr = "";
|
|
406
287
|
let progressStatus: ReplDetails["status"] = "running";
|
|
407
288
|
const startedAt = Date.now();
|
|
408
|
-
const limits = new LimitGuard(
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
});
|
|
289
|
+
const limits = new LimitGuard(limitsFromConfig(getConfig()));
|
|
290
|
+
|
|
291
|
+
const detachTracers = traceEnabled
|
|
292
|
+
? [attachTracer(emitter, "turn"), attachTracer(background.emitter, "background")]
|
|
293
|
+
: [];
|
|
414
294
|
|
|
415
295
|
// ── Progressive rendering: spinner + live sub-call tree ──
|
|
416
296
|
const progress = createProgressNotifier<ReplDetails>({
|
|
417
297
|
onUpdate,
|
|
418
|
-
getDetails: () =>
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
298
|
+
getDetails: () => {
|
|
299
|
+
// Detached spawn() nodes live on the SESSION emitter, so without this merge the card
|
|
300
|
+
// stays empty for the entire time background work is running.
|
|
301
|
+
const live = background.liveSubcalls();
|
|
302
|
+
const bg = background.liveTotals();
|
|
303
|
+
const own = store.getTotals();
|
|
304
|
+
return {
|
|
305
|
+
status: progressStatus,
|
|
306
|
+
output: capturedStdout,
|
|
307
|
+
stderr: capturedStderr,
|
|
308
|
+
executionTimeMs: Date.now() - startedAt,
|
|
309
|
+
subcalls: live.length > 0 ? [...store.getSubcalls(), ...live] : store.getSubcalls(),
|
|
310
|
+
totals: { costUsd: own.costUsd + bg.costUsd, tokens: own.tokens + bg.tokens },
|
|
311
|
+
backgroundPending: background.pending > 0 ? background.pending : undefined,
|
|
312
|
+
};
|
|
313
|
+
},
|
|
426
314
|
isRunning: (details) => details.status === "running",
|
|
427
315
|
renderText: (details) => details.output.slice(0, 500) || (details.status === "running" ? `${spinnerFrame()} Running…` : "(no output)"),
|
|
428
316
|
});
|
|
@@ -442,7 +330,7 @@ export function createReplTool(deps: ReplToolDeps): ToolDefinition<typeof ReplTo
|
|
|
442
330
|
// Build interactive handlers (session-stable callbacks)
|
|
443
331
|
const interactive = createPiInteractiveDeps(ctx);
|
|
444
332
|
const interactiveHandlers = buildInteractiveHandlers({
|
|
445
|
-
onAskUserQuestion:
|
|
333
|
+
onAskUserQuestion: getConfig().askUserQuestion ? interactive.onAskUserQuestion : undefined,
|
|
446
334
|
onTodo: interactive.onTodo,
|
|
447
335
|
onTodoRow: undefined,
|
|
448
336
|
emitter,
|
|
@@ -454,8 +342,7 @@ export function createReplTool(deps: ReplToolDeps): ToolDefinition<typeof ReplTo
|
|
|
454
342
|
|
|
455
343
|
await deps.ensureContext?.();
|
|
456
344
|
await sandboxManager.getOrCreate({
|
|
457
|
-
...
|
|
458
|
-
...rlmHandlers,
|
|
345
|
+
...subcallHandlers,
|
|
459
346
|
askUserQuestion: interactiveHandlers.askUserQuestion,
|
|
460
347
|
todo: interactiveHandlers.todo,
|
|
461
348
|
...(libraryBundle?.handlers ?? {}),
|
|
@@ -470,19 +357,46 @@ export function createReplTool(deps: ReplToolDeps): ToolDefinition<typeof ReplTo
|
|
|
470
357
|
});
|
|
471
358
|
}
|
|
472
359
|
|
|
360
|
+
if (traceEnabled) {
|
|
361
|
+
trace("repl.exec.start", { chars: params.code.length, code: params.code.slice(0, 400) });
|
|
362
|
+
}
|
|
363
|
+
|
|
473
364
|
const start = Date.now();
|
|
474
365
|
const result: ReplResult = await sandboxManager.execWithSetup(params.code, () => {
|
|
475
366
|
// Wire per-invocation mutable state only after the serialized exec slot
|
|
476
367
|
// is active. Swapping earlier would let queued repl() calls overwrite
|
|
477
368
|
// emitter/limits for the currently running REPL execution.
|
|
478
|
-
bridgeState.swap({ emitter, parentId: undefined, depth: 0, limits, interactive
|
|
479
|
-
});
|
|
369
|
+
bridgeState.swap({ emitter, parentId: undefined, depth: 0, limits }, interactive);
|
|
370
|
+
}, execSignal);
|
|
480
371
|
const elapsed = Date.now() - start;
|
|
481
372
|
capturedStdout = result.stdout;
|
|
482
373
|
capturedStderr = result.stderr;
|
|
483
374
|
progressStatus = "done";
|
|
484
375
|
|
|
485
|
-
|
|
376
|
+
if (traceEnabled) {
|
|
377
|
+
trace("repl.exec.end", {
|
|
378
|
+
ms: elapsed,
|
|
379
|
+
stdout: result.stdout.length,
|
|
380
|
+
raised: result.raised,
|
|
381
|
+
pending: background.pending,
|
|
382
|
+
// A block that raised delegated nothing; without the exception the trace shows a
|
|
383
|
+
// silent turn and the reason is only in the TUI card.
|
|
384
|
+
error: result.raised ? lastLine(result.stderr) : undefined,
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Adopt every background subtree that has settled, whether or not this turn awaited
|
|
389
|
+
// it — otherwise a spawn the model never collects would never reach the user's cost
|
|
390
|
+
// totals. IDs are "bg"-prefixed, so they cannot collide with this turn's.
|
|
391
|
+
// (drain() removes what it hands over, so live view + accounted view never double-count.)
|
|
392
|
+
const adopted = background.drain();
|
|
393
|
+
const subcalls: readonly RlmSubcall[] = adopted.subcalls.length > 0
|
|
394
|
+
? [...store.getSubcalls(), ...adopted.subcalls]
|
|
395
|
+
: store.getSubcalls();
|
|
396
|
+
const totals = {
|
|
397
|
+
costUsd: store.getTotals().costUsd + adopted.totals.costUsd,
|
|
398
|
+
tokens: store.getTotals().tokens + adopted.totals.tokens,
|
|
399
|
+
};
|
|
486
400
|
const subUsage: Usage = {
|
|
487
401
|
input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: totals.tokens,
|
|
488
402
|
cost: { total: totals.costUsd, input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
@@ -495,7 +409,9 @@ export function createReplTool(deps: ReplToolDeps): ToolDefinition<typeof ReplTo
|
|
|
495
409
|
const { text: resultText } = buildReplResultText(
|
|
496
410
|
result.stdout,
|
|
497
411
|
finalAnswer,
|
|
498
|
-
|
|
412
|
+
subcalls,
|
|
413
|
+
background.pending,
|
|
414
|
+
result.varNames,
|
|
499
415
|
);
|
|
500
416
|
|
|
501
417
|
const details: ReplDetails = {
|
|
@@ -503,10 +419,11 @@ export function createReplTool(deps: ReplToolDeps): ToolDefinition<typeof ReplTo
|
|
|
503
419
|
output: result.stdout,
|
|
504
420
|
stderr: result.stderr,
|
|
505
421
|
executionTimeMs: elapsed,
|
|
506
|
-
subcalls
|
|
507
|
-
totals
|
|
422
|
+
subcalls,
|
|
423
|
+
totals,
|
|
508
424
|
finalAnswer,
|
|
509
|
-
|
|
425
|
+
backgroundPending: background.pending > 0 ? background.pending : undefined,
|
|
426
|
+
warnings: collectReplWarnings(subcalls),
|
|
510
427
|
};
|
|
511
428
|
const progressText = finalAnswer !== undefined
|
|
512
429
|
? `ANSWER_SUBMITTED (${finalAnswer.length} chars)`
|
|
@@ -517,13 +434,20 @@ export function createReplTool(deps: ReplToolDeps): ToolDefinition<typeof ReplTo
|
|
|
517
434
|
} catch (e) {
|
|
518
435
|
progressStatus = "error";
|
|
519
436
|
const msg = errorMessage(e);
|
|
437
|
+
// Drain here too: a failing turn must not swallow the cost of background work that
|
|
438
|
+
// settled during it, or a run that keeps erroring would never report any of it.
|
|
439
|
+
const adopted = background.drain();
|
|
520
440
|
const details: ReplDetails = {
|
|
521
441
|
status: "error",
|
|
522
442
|
output: "",
|
|
523
443
|
stderr: msg,
|
|
524
444
|
executionTimeMs: 0,
|
|
525
|
-
subcalls: store.getSubcalls(),
|
|
526
|
-
totals:
|
|
445
|
+
subcalls: [...store.getSubcalls(), ...adopted.subcalls],
|
|
446
|
+
totals: {
|
|
447
|
+
costUsd: store.getTotals().costUsd + adopted.totals.costUsd,
|
|
448
|
+
tokens: store.getTotals().tokens + adopted.totals.tokens,
|
|
449
|
+
},
|
|
450
|
+
backgroundPending: background.pending > 0 ? background.pending : undefined,
|
|
527
451
|
};
|
|
528
452
|
onUpdate?.({ content: [{ type: "text", text: `REPL error: ${msg}` }], details });
|
|
529
453
|
return {
|
|
@@ -532,16 +456,15 @@ export function createReplTool(deps: ReplToolDeps): ToolDefinition<typeof ReplTo
|
|
|
532
456
|
};
|
|
533
457
|
} finally {
|
|
534
458
|
progress.stop();
|
|
459
|
+
for (const off of detachTracers) off();
|
|
535
460
|
store.dispose();
|
|
536
461
|
emitter.shutdown();
|
|
537
462
|
}
|
|
538
463
|
},
|
|
539
464
|
|
|
540
465
|
renderCall(args, theme) {
|
|
541
|
-
const preview = args.code.length > 80 ? `${args.code.slice(0, 80)}...` : args.code;
|
|
542
466
|
return new Text(
|
|
543
|
-
theme.fg("toolTitle", theme.bold("repl ")) +
|
|
544
|
-
theme.fg("dim", preview.replace(/\n/g, " ")),
|
|
467
|
+
theme.fg("toolTitle", theme.bold("repl ")) + theme.fg("dim", previewText(args.code, CALL_PREVIEW_CHARS)),
|
|
545
468
|
0, 0,
|
|
546
469
|
);
|
|
547
470
|
},
|
|
@@ -560,26 +483,13 @@ export function createReplTool(deps: ReplToolDeps): ToolDefinition<typeof ReplTo
|
|
|
560
483
|
|
|
561
484
|
// ── Collapsed view ──
|
|
562
485
|
|
|
563
|
-
function
|
|
564
|
-
const
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
const parts: string[] = [];
|
|
569
|
-
parts.push(formatCost(details.totals.costUsd));
|
|
570
|
-
if (details.totals.tokens > 0) parts.push(`${formatTokens(details.totals.tokens)} tok`);
|
|
571
|
-
if (details.executionTimeMs > 0) parts.push(`${details.executionTimeMs}ms`);
|
|
572
|
-
const stats = parts.length > 0 ? ` ${theme.fg("dim", parts.join(" · "))}` : "";
|
|
573
|
-
|
|
574
|
-
const header = `${glyph} ${theme.fg("toolTitle", theme.bold("REPL"))}${stats}`;
|
|
575
|
-
|
|
576
|
-
let body = "";
|
|
577
|
-
if (details.subcalls.length > 0) {
|
|
578
|
-
body = `\n${renderCollapsedSubcallTree(details.subcalls, theme)}`;
|
|
579
|
-
}
|
|
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
|
+
}
|
|
580
490
|
|
|
581
|
-
|
|
582
|
-
return
|
|
491
|
+
function renderReplCollapsed(details: ReplDetails, theme: Theme): Text {
|
|
492
|
+
return renderCollapsedCard("REPL", details.status, replStats(details, theme), details.subcalls, theme);
|
|
583
493
|
}
|
|
584
494
|
|
|
585
495
|
// ── Expanded view ──
|
|
@@ -587,19 +497,14 @@ function renderReplCollapsed(details: ReplDetails, theme: Theme): Text {
|
|
|
587
497
|
function renderReplExpanded(details: ReplDetails, theme: Theme): Container {
|
|
588
498
|
const container = new Container();
|
|
589
499
|
|
|
590
|
-
|
|
591
|
-
const glyph = details.status === "error" ? theme.fg("error", "✗") : theme.fg("success", "✓");
|
|
592
|
-
const parts: string[] = [];
|
|
593
|
-
parts.push(formatCost(details.totals.costUsd));
|
|
594
|
-
if (details.totals.tokens > 0) parts.push(`${formatTokens(details.totals.tokens)} tok`);
|
|
595
|
-
if (details.executionTimeMs > 0) parts.push(`${details.executionTimeMs}ms`);
|
|
596
|
-
const stats = parts.length > 0 ? ` · ${theme.fg("dim", parts.join(" · "))}` : "";
|
|
597
|
-
container.addChild(new Text(`${glyph} ${theme.fg("toolTitle", theme.bold("REPL"))}${stats}`, 0, 0));
|
|
500
|
+
container.addChild(new Text(cardHeader("REPL", details.status, replStats(details, theme), theme), 0, 0));
|
|
598
501
|
|
|
599
502
|
// Output
|
|
600
503
|
if (details.output) {
|
|
601
504
|
container.addChild(new Spacer(1));
|
|
602
|
-
const out = details.output.length >
|
|
505
|
+
const out = details.output.length > EXPANDED_STDOUT_CHARS
|
|
506
|
+
? `${details.output.slice(0, EXPANDED_STDOUT_CHARS)}…`
|
|
507
|
+
: details.output;
|
|
603
508
|
container.addChild(new Text(out, 0, 0));
|
|
604
509
|
}
|
|
605
510
|
|
|
@@ -611,7 +516,7 @@ function renderReplExpanded(details: ReplDetails, theme: Theme): Container {
|
|
|
611
516
|
// Stderr
|
|
612
517
|
if (details.stderr) {
|
|
613
518
|
container.addChild(new Spacer(1));
|
|
614
|
-
container.addChild(new Text(theme.fg("error", details.stderr.slice(0,
|
|
519
|
+
container.addChild(new Text(theme.fg("error", details.stderr.slice(0, EXPANDED_STDERR_CHARS)), 0, 0));
|
|
615
520
|
}
|
|
616
521
|
|
|
617
522
|
// Sub-call tree
|