@hicaru/pi-rlm 0.3.1 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -5
- package/README.ru.md +5 -5
- package/README.zh-CN.md +5 -5
- package/package.json +1 -1
- package/src/bridge/handlers/await.ts +148 -0
- package/src/bridge/handlers/completion.ts +72 -0
- package/src/bridge/handlers/emitting.ts +104 -0
- package/src/bridge/handlers/finish.ts +45 -0
- package/src/bridge/handlers/index.ts +48 -0
- package/src/bridge/handlers/llm-query.ts +130 -0
- package/src/bridge/handlers/rlm-query.ts +227 -0
- package/src/bridge/handlers/task-registry.ts +202 -0
- package/src/bridge/handlers/types.ts +136 -0
- package/src/commands/rlm-config.ts +33 -14
- package/src/context/listing.ts +2 -2
- package/src/context/refresh.ts +141 -0
- package/src/core/engine.ts +16 -18
- package/src/core/types.ts +1 -3
- package/src/index.ts +54 -42
- package/src/mode/native-guards.ts +4 -4
- package/src/mode/subagent.ts +1 -1
- package/src/prompts/glossary.ts +71 -74
- package/src/prompts/native.ts +127 -85
- package/src/prompts/system.ts +29 -15
- package/src/sandbox/interrupts.ts +258 -68
- package/src/sandbox/protocol.ts +53 -30
- package/src/sandbox/py/__pycache__/guards.cpython-314.pyc +0 -0
- package/src/sandbox/py/__pycache__/hostio.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 +8 -5
- package/src/sandbox/py/retrieval.py +17 -8
- package/src/sandbox/py/tasks.py +1 -1
- package/src/sandbox/py/worker.py +106 -79
- package/src/sandbox/sandbox-manager.ts +26 -1
- package/src/sandbox/sandbox.ts +1 -1
- package/src/tool/background-tasks.ts +1 -1
- package/src/tool/repl-result.ts +2 -2
- package/src/tool/repl-tool.ts +13 -14
- package/src/ui/config-panel.ts +1 -1
- package/src/ui/intro.ts +1 -4
- package/src/ui/model-picker.ts +28 -2
- package/src/util/concurrency.ts +1 -1
- package/src/bridge/subcall-handlers.ts +0 -382
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keep RLM `context` in sync with the disk after native edit/write.
|
|
3
|
+
*
|
|
4
|
+
* Seed packs file bodies once; without this, search/map_files/llm still see pre-edit text.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { readFile } from "node:fs/promises";
|
|
8
|
+
import { isAbsolute, relative, resolve } from "node:path";
|
|
9
|
+
import { estimateTokens } from "../text/tokens.ts";
|
|
10
|
+
import type { ContextFile } from "./types.ts";
|
|
11
|
+
|
|
12
|
+
/** Paths that look like tool file targets. */
|
|
13
|
+
export function extractEditPaths(input: unknown): readonly string[] {
|
|
14
|
+
if (typeof input !== "object" || input === null) return Object.freeze([]);
|
|
15
|
+
const o = input as Record<string, unknown>;
|
|
16
|
+
const keys = ["path", "file_path", "filePath", "filename", "file"] as const;
|
|
17
|
+
const out: string[] = [];
|
|
18
|
+
for (const k of keys) {
|
|
19
|
+
const v = o[k];
|
|
20
|
+
if (typeof v === "string" && v.trim() !== "") out.push(v.trim());
|
|
21
|
+
}
|
|
22
|
+
// Some tools pass { path, oldText, newText } only — already covered.
|
|
23
|
+
return Object.freeze(out);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Normalize disk path to how cwd-seed entries usually appear (relative to cwd when under cwd).
|
|
28
|
+
*/
|
|
29
|
+
export function normalizeContextPath(filePath: string, cwd: string): string {
|
|
30
|
+
const abs = isAbsolute(filePath) ? resolve(filePath) : resolve(cwd, filePath);
|
|
31
|
+
const rel = relative(cwd, abs);
|
|
32
|
+
if (rel === "" || rel.startsWith("..") || isAbsolute(rel)) return abs;
|
|
33
|
+
return rel.split("\\").join("/");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function pathMatches(entryPath: string, target: string, cwd: string): boolean {
|
|
37
|
+
if (entryPath === target) return true;
|
|
38
|
+
const a = normalizeContextPath(entryPath, cwd);
|
|
39
|
+
const b = normalizeContextPath(target, cwd);
|
|
40
|
+
if (a === b) return true;
|
|
41
|
+
// suffix match for namespaced entries
|
|
42
|
+
return entryPath.endsWith("/" + target) || entryPath.endsWith(target);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Upsert one file into a context payload list. Returns a **new** array (identity change
|
|
47
|
+
* so BM25 stamp invalidates when the worker rebinds `context`).
|
|
48
|
+
*/
|
|
49
|
+
export function upsertContextFile(
|
|
50
|
+
payload: unknown,
|
|
51
|
+
filePath: string,
|
|
52
|
+
content: string,
|
|
53
|
+
cwd: string,
|
|
54
|
+
): ContextFile[] {
|
|
55
|
+
const path = normalizeContextPath(filePath, cwd);
|
|
56
|
+
const tokens = estimateTokens(content.length);
|
|
57
|
+
const entry: ContextFile = Object.freeze({ path, content, tokens });
|
|
58
|
+
|
|
59
|
+
if (!Array.isArray(payload)) {
|
|
60
|
+
return [entry];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const next = new Array<ContextFile>(payload.length + 1);
|
|
64
|
+
let n = 0;
|
|
65
|
+
let replaced = false;
|
|
66
|
+
for (let i = 0; i < payload.length; i++) {
|
|
67
|
+
const item: unknown = payload[i];
|
|
68
|
+
if (
|
|
69
|
+
item !== null &&
|
|
70
|
+
typeof item === "object" &&
|
|
71
|
+
"path" in item &&
|
|
72
|
+
typeof (item as { path: unknown }).path === "string" &&
|
|
73
|
+
pathMatches((item as { path: string }).path, path, cwd)
|
|
74
|
+
) {
|
|
75
|
+
next[n++] = entry;
|
|
76
|
+
replaced = true;
|
|
77
|
+
} else if (
|
|
78
|
+
item !== null &&
|
|
79
|
+
typeof item === "object" &&
|
|
80
|
+
"path" in item &&
|
|
81
|
+
"content" in item &&
|
|
82
|
+
typeof (item as { path: unknown }).path === "string" &&
|
|
83
|
+
typeof (item as { content: unknown }).content === "string"
|
|
84
|
+
) {
|
|
85
|
+
const e = item as { path: string; content: string; tokens?: number };
|
|
86
|
+
next[n++] = Object.freeze({
|
|
87
|
+
path: e.path,
|
|
88
|
+
content: e.content,
|
|
89
|
+
tokens: typeof e.tokens === "number" ? e.tokens : estimateTokens(e.content.length),
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
// drop non-file entries silently (shouldn't appear in file bundles)
|
|
93
|
+
}
|
|
94
|
+
if (!replaced) next[n++] = entry;
|
|
95
|
+
next.length = n;
|
|
96
|
+
return next;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Read file from disk; return null if missing/unreadable. */
|
|
100
|
+
export async function readDiskFile(filePath: string, cwd: string): Promise<string | null> {
|
|
101
|
+
const abs = isAbsolute(filePath) ? resolve(filePath) : resolve(cwd, filePath);
|
|
102
|
+
try {
|
|
103
|
+
return await readFile(abs, "utf8");
|
|
104
|
+
} catch {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Python snippet that rebinds `context` with updated path content and forces a new list id
|
|
111
|
+
* so BM25 rebuilds on next search.
|
|
112
|
+
*/
|
|
113
|
+
export function patchContextExecCode(filePath: string, content: string, cwd: string): string {
|
|
114
|
+
const path = normalizeContextPath(filePath, cwd);
|
|
115
|
+
// JSON for safe embedding in Python string literals
|
|
116
|
+
const pathLit = JSON.stringify(path);
|
|
117
|
+
const contentLit = JSON.stringify(content);
|
|
118
|
+
const tokens = estimateTokens(content.length);
|
|
119
|
+
return `
|
|
120
|
+
_path = ${pathLit}
|
|
121
|
+
_content = ${contentLit}
|
|
122
|
+
_tokens = ${tokens}
|
|
123
|
+
_old = context if isinstance(context, list) else []
|
|
124
|
+
_next = []
|
|
125
|
+
_found = False
|
|
126
|
+
for _e in _old:
|
|
127
|
+
if isinstance(_e, dict) and str(_e.get("path", "")) in (_path, _path.replace("\\\\", "/")):
|
|
128
|
+
_next.append({"path": _path, "content": _content, "tokens": _tokens})
|
|
129
|
+
_found = True
|
|
130
|
+
elif isinstance(_e, dict) and (
|
|
131
|
+
str(_e.get("path", "")).endswith("/" + _path) or str(_e.get("path", "")).endswith(_path)
|
|
132
|
+
):
|
|
133
|
+
_next.append({"path": str(_e.get("path")), "content": _content, "tokens": _tokens})
|
|
134
|
+
_found = True
|
|
135
|
+
else:
|
|
136
|
+
_next.append(_e)
|
|
137
|
+
if not _found:
|
|
138
|
+
_next.append({"path": _path, "content": _content, "tokens": _tokens})
|
|
139
|
+
context = _next
|
|
140
|
+
`.trim();
|
|
141
|
+
}
|
package/src/core/engine.ts
CHANGED
|
@@ -13,10 +13,10 @@ import { buildAddContextHandler } from "../bridge/add-context.ts";
|
|
|
13
13
|
import { mergeIntoContext } from "../context/merge.ts";
|
|
14
14
|
import {
|
|
15
15
|
createSubcallHandlers,
|
|
16
|
+
createTaskRegistry,
|
|
16
17
|
type Invocation,
|
|
17
|
-
} from "../bridge/
|
|
18
|
+
} from "../bridge/handlers/index.ts";
|
|
18
19
|
import { type ChatMsg, modelComplete } from "../bridge/model.ts";
|
|
19
|
-
import { resolveModelId } from "../config/settings.ts";
|
|
20
20
|
import { buildRlmSystemPrompt } from "../prompts/system.ts";
|
|
21
21
|
import { buildTurnPrompt, FINALIZE_PROMPT } from "../prompts/user.ts";
|
|
22
22
|
import type { RlmEmitter } from "../tool/rlm-events.ts";
|
|
@@ -30,7 +30,6 @@ import { appendUserMessage } from "./history.ts";
|
|
|
30
30
|
import { runTurn } from "./iteration.ts";
|
|
31
31
|
import { type Limits, LimitError, LimitGuard } from "./limits.ts";
|
|
32
32
|
import type { RlmConfig, RlmInput, RlmResult, RunRlm, Sampling } from "./types.ts";
|
|
33
|
-
import { formatError } from "../util/errors.ts";
|
|
34
33
|
import { createSubcallGates, type SubcallGates } from "../util/concurrency.ts";
|
|
35
34
|
|
|
36
35
|
/**
|
|
@@ -71,20 +70,7 @@ export function createEngine(deps: EngineDeps): RunRlm {
|
|
|
71
70
|
emitter.emitTurn(0, deps.config.maxIterations);
|
|
72
71
|
}
|
|
73
72
|
|
|
74
|
-
const
|
|
75
|
-
if (input.modelOverride && !overrideModel) {
|
|
76
|
-
if (selfReportId) emitter.emitSubcallUpdated({ id: selfReportId, status: "error", detail: "unknown model override" });
|
|
77
|
-
else emitter.emitStatus("error");
|
|
78
|
-
return {
|
|
79
|
-
answer: formatError(`unknown model override '${input.modelOverride}'`),
|
|
80
|
-
iterations: 0,
|
|
81
|
-
costUsd: 0,
|
|
82
|
-
inputTokens: 0,
|
|
83
|
-
outputTokens: 0,
|
|
84
|
-
durationMs: 0,
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
const model = overrideModel ?? deps.model;
|
|
73
|
+
const model = deps.model;
|
|
88
74
|
|
|
89
75
|
// Create LimitGuard BEFORE the bridge so sub-LLM usage feeds into it.
|
|
90
76
|
// Children inherit the parent's remaining timeout (propagated as remaining amount, not
|
|
@@ -117,6 +103,8 @@ export function createEngine(deps: EngineDeps): RunRlm {
|
|
|
117
103
|
// run can settle or abort it first (a child engine left running would keep spending).
|
|
118
104
|
let detachedInFlight = 0;
|
|
119
105
|
let detachedIdle: (() => void) | undefined;
|
|
106
|
+
// One registry per run — unawaited task reminders share the same map as await handlers.
|
|
107
|
+
const taskRegistry = createTaskRegistry();
|
|
120
108
|
const subcalls = createSubcallHandlers({
|
|
121
109
|
resolve: () => invocation,
|
|
122
110
|
gates: deps.gates
|
|
@@ -140,7 +128,7 @@ export function createEngine(deps: EngineDeps): RunRlm {
|
|
|
140
128
|
if (detachedInFlight === 0) detachedIdle?.();
|
|
141
129
|
}
|
|
142
130
|
},
|
|
143
|
-
});
|
|
131
|
+
}, taskRegistry);
|
|
144
132
|
/** Wait (bounded) for detached work before the sandbox goes away. */
|
|
145
133
|
const settleDetached = async (): Promise<void> => {
|
|
146
134
|
if (detachedInFlight === 0) return;
|
|
@@ -189,6 +177,7 @@ export function createEngine(deps: EngineDeps): RunRlm {
|
|
|
189
177
|
maxPromptChars: deps.config.maxPromptChars,
|
|
190
178
|
contextLoader: deps.config.contextLoader,
|
|
191
179
|
child: input.depth > 0,
|
|
180
|
+
depth: input.depth,
|
|
192
181
|
});
|
|
193
182
|
|
|
194
183
|
const contextHandlers = deps.config.contextLoader
|
|
@@ -251,6 +240,15 @@ export function createEngine(deps: EngineDeps): RunRlm {
|
|
|
251
240
|
pendingReplOutputs = undefined;
|
|
252
241
|
}
|
|
253
242
|
|
|
243
|
+
// Soft runtime nudge (rlm_test parity): remind the model to await pending host tasks.
|
|
244
|
+
const pendingIds = taskRegistry.awaitDeps.unawaitedIds();
|
|
245
|
+
if (pendingIds.length > 0) {
|
|
246
|
+
appendUserMessage(
|
|
247
|
+
history,
|
|
248
|
+
`[runtime] Unawaited task_ids: ${pendingIds.join(", ")} — call await before finish.`,
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
254
252
|
appendUserMessage(history, buildTurnPrompt(i, deps.config.maxIterations));
|
|
255
253
|
|
|
256
254
|
// rootSampling fields win; smartReasoning is the default reasoning when not overridden.
|
package/src/core/types.ts
CHANGED
|
@@ -55,7 +55,7 @@ export interface RlmConfig {
|
|
|
55
55
|
* Keeps each turn short so the next turn's input stays manageable.
|
|
56
56
|
* `reasoning` is read from `smartReasoning` if omitted here. */
|
|
57
57
|
readonly rootSampling?: Readonly<Sampling>;
|
|
58
|
-
/** System prompt injected into every llm_query /
|
|
58
|
+
/** System prompt injected into every llm_query / llm_batch sub-call.
|
|
59
59
|
* Instructs the worker model to respond concisely.
|
|
60
60
|
* undefined = no system prompt (raw completion). */
|
|
61
61
|
readonly subSystemPrompt?: string;
|
|
@@ -73,8 +73,6 @@ export interface RlmInput {
|
|
|
73
73
|
readonly depth: number;
|
|
74
74
|
/** AgentTree node to attach this run's node under (set when recursing). */
|
|
75
75
|
readonly parentNodeId?: string;
|
|
76
|
-
/** "provider/id" — overrides the root model for this run (set by recursive rlm_query). */
|
|
77
|
-
readonly modelOverride?: string;
|
|
78
76
|
/** Remaining timeout for this subtree (set by parent from its LimitGuard). */
|
|
79
77
|
readonly remainingTimeoutMs?: number;
|
|
80
78
|
}
|
package/src/index.ts
CHANGED
|
@@ -18,9 +18,10 @@ import { BackgroundTasks } from "./tool/background-tasks.ts";
|
|
|
18
18
|
import { resolve } from "node:path";
|
|
19
19
|
import { resolveSource } from "./context/resolve.ts";
|
|
20
20
|
import { formatContextListing } from "./context/listing.ts";
|
|
21
|
+
import { extractEditPaths, readDiskFile } from "./context/refresh.ts";
|
|
21
22
|
import type { AddContextHandlerBundle } from "./bridge/add-context.ts";
|
|
22
|
-
import { buildNativeSystemPrompt
|
|
23
|
-
import {
|
|
23
|
+
import { buildNativeSystemPrompt } from "./prompts/native.ts";
|
|
24
|
+
import { capToolResultText } from "./mode/native-guards.ts";
|
|
24
25
|
import {
|
|
25
26
|
isSubagentChildBypass,
|
|
26
27
|
commitSubagentForceActivation,
|
|
@@ -37,14 +38,13 @@ export {
|
|
|
37
38
|
processRlmDepth,
|
|
38
39
|
} from "./mode/subagent.ts";
|
|
39
40
|
|
|
40
|
-
const BLOCKED_NATIVE_TOOLS = Object.freeze(new Set(["read", "grep"]));
|
|
41
41
|
/** How often to keep the parent sandbox's request watchdog alive during detached work. */
|
|
42
42
|
const WATCHDOG_HEARTBEAT_MS = 30_000;
|
|
43
|
-
|
|
43
|
+
/** Soft token guard — cap bulk tool stdout; do NOT hard-block read/grep/bash readers. */
|
|
44
|
+
const CAPPED_RESULT_TOOLS = Object.freeze(new Set(["bash", "find", "ls", "read", "grep"]));
|
|
44
45
|
|
|
45
46
|
export default function rlmExtension(pi: ExtensionAPI): void {
|
|
46
|
-
// Subagent children
|
|
47
|
-
// (block read/grep, route through repl). Env fast path: full bypass when
|
|
47
|
+
// Subagent children may bypass full RLM registration. Env fast path when
|
|
48
48
|
// PI_SUBAGENT_CHILD=1 (unless force-in under the depth cap). See mode/subagent.ts.
|
|
49
49
|
if (isSubagentChildBypass()) {
|
|
50
50
|
if (traceEnabled) {
|
|
@@ -182,7 +182,23 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
182
182
|
|
|
183
183
|
if (controller.savedLlmRef) {
|
|
184
184
|
const resolved = resolveModelId(ctx.modelRegistry, controller.savedLlmRef);
|
|
185
|
-
if (resolved)
|
|
185
|
+
if (resolved) {
|
|
186
|
+
controller.llmModel = resolved;
|
|
187
|
+
} else {
|
|
188
|
+
// Keep the pin on disk/controller — do not fall back permanently. Runtime uses
|
|
189
|
+
// cheapest until the catalog has the model again; surface that once per session.
|
|
190
|
+
console.warn(
|
|
191
|
+
`[rlm] pinned sub-LLM ${controller.savedLlmRef} not in registry; using cheapest until it reappears`,
|
|
192
|
+
);
|
|
193
|
+
try {
|
|
194
|
+
ctx.ui.notify(
|
|
195
|
+
`RLM: pinned llm=${controller.savedLlmRef} unavailable — using cheapest until it is`,
|
|
196
|
+
"warning",
|
|
197
|
+
);
|
|
198
|
+
} catch {
|
|
199
|
+
// Some hosts have no UI at session_start.
|
|
200
|
+
}
|
|
201
|
+
}
|
|
186
202
|
}
|
|
187
203
|
|
|
188
204
|
// Re-register repl tool each session to pick up model provider changes
|
|
@@ -254,7 +270,7 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
254
270
|
const filtered = event.messages.filter(
|
|
255
271
|
(message) =>
|
|
256
272
|
!(message.role === "custom" && message.customType === "rlm-intro")
|
|
257
|
-
|
|
273
|
+
|
|
258
274
|
);
|
|
259
275
|
if (!nativeTradeHolds()) return { messages: filtered };
|
|
260
276
|
|
|
@@ -266,11 +282,11 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
266
282
|
listingPayloadRef = payload;
|
|
267
283
|
const listing = formatContextListing(payload);
|
|
268
284
|
const instruction = [
|
|
269
|
-
"
|
|
270
|
-
"
|
|
271
|
-
"
|
|
272
|
-
"
|
|
273
|
-
"
|
|
285
|
+
"Prefer repl({code}) for bulk analysis: free search/grep/outline, then fan-out Tasks.",
|
|
286
|
+
"Multi-module work → rlm_batch (or rlm_query); one-shot extracts → map_files/llm_batch.",
|
|
287
|
+
"Always-spawn returns Task (↯bg); only await_task has content — fire-all then await.",
|
|
288
|
+
"Large tool/repl outputs are capped. Files live in REPL `context` (cwd seeds first repl()).",
|
|
289
|
+
"add_context(path) for external dirs/files/docs/git. Credits exhausted → report and stop.",
|
|
274
290
|
"",
|
|
275
291
|
].join("\n");
|
|
276
292
|
filtered.unshift({
|
|
@@ -280,42 +296,38 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
280
296
|
} as PiMessage);
|
|
281
297
|
}
|
|
282
298
|
|
|
283
|
-
// Per-turn last-position reminder (not persisted — context hook rebuilds every request)
|
|
284
|
-
filtered.push({
|
|
285
|
-
role: "user" as const,
|
|
286
|
-
content: NATIVE_TURN_REMINDER,
|
|
287
|
-
timestamp: 0,
|
|
288
|
-
} as PiMessage);
|
|
289
299
|
|
|
290
300
|
return { messages: filtered };
|
|
291
301
|
});
|
|
292
302
|
|
|
293
|
-
//
|
|
294
|
-
//
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
+
// Soft token guard only — never hard-block read/grep/bash. Large tool results are capped.
|
|
304
|
+
// After edit/write, re-read disk into RLM context so search/llm see fresh content.
|
|
305
|
+
const MUTATING_FILE_TOOLS = Object.freeze(new Set(["edit", "write"]));
|
|
306
|
+
|
|
307
|
+
pi.on("tool_result", async (event, ctx) => {
|
|
308
|
+
// ── Keep RLM context fresh after native file mutations ──
|
|
309
|
+
if (
|
|
310
|
+
nativeTradeHolds()
|
|
311
|
+
&& MUTATING_FILE_TOOLS.has(event.toolName)
|
|
312
|
+
&& event.isError !== true
|
|
313
|
+
) {
|
|
314
|
+
const cwd = resolve(ctx?.cwd ?? process.cwd());
|
|
315
|
+
const paths = extractEditPaths(event.input);
|
|
316
|
+
for (const p of paths) {
|
|
317
|
+
const body = await readDiskFile(p, cwd);
|
|
318
|
+
if (body === null) continue;
|
|
319
|
+
try {
|
|
320
|
+
await sandboxManager.refreshFileFromDisk(p, body, cwd);
|
|
321
|
+
// Listing must re-inject if we rewrote payload identity
|
|
322
|
+
listingPayloadRef = undefined;
|
|
323
|
+
} catch (err) {
|
|
324
|
+
if (traceEnabled) {
|
|
325
|
+
trace("context.refresh_fail", { path: p, error: errorMessage(err) });
|
|
326
|
+
}
|
|
327
|
+
}
|
|
303
328
|
}
|
|
304
|
-
return;
|
|
305
|
-
}
|
|
306
|
-
if (BLOCKED_NATIVE_TOOLS.has(event.toolName)) {
|
|
307
|
-
return {
|
|
308
|
-
block: true,
|
|
309
|
-
reason: "RLM mode active. Use repl({code}) to read files and search the repository — loaded files live in the REPL `context` variable (cwd seeds on first call). Use `edit`/`write` for file changes. If sub-LLM credits are exhausted, report to the user.",
|
|
310
|
-
};
|
|
311
329
|
}
|
|
312
|
-
const bashCommand = event.toolName === "bash" ? bashCommandFromInput(event.input) : undefined;
|
|
313
|
-
if (bashCommand !== undefined && isFileReadingCommand(bashCommand)) {
|
|
314
|
-
return { block: true, reason: BASH_BLOCK_REASON };
|
|
315
|
-
}
|
|
316
|
-
});
|
|
317
330
|
|
|
318
|
-
pi.on("tool_result", async (event) => {
|
|
319
331
|
if (!nativeTradeHolds() || !CAPPED_RESULT_TOOLS.has(event.toolName)) return;
|
|
320
332
|
let changed = false;
|
|
321
333
|
const content = event.content.map((c) => {
|
|
@@ -43,7 +43,7 @@ export function isFileReadingCommand(command: string): boolean {
|
|
|
43
43
|
export const BASH_BLOCK_REASON =
|
|
44
44
|
"RLM mode: reading files via bash is blocked — that dumps file content into the root model's " +
|
|
45
45
|
"context. All files are pre-loaded in the REPL `context` variable: use repl({code}) with Python " +
|
|
46
|
-
"string/regex search, and delegate bulk analysis to llm_query /
|
|
46
|
+
"string/regex search, and delegate bulk analysis to llm_query / llm_batch / " +
|
|
47
47
|
"llm_query_chunked. bash is for RUNNING things (tests, builds, git).";
|
|
48
48
|
|
|
49
49
|
/** Max chars of tool output forwarded to the root model (≈1K tokens). */
|
|
@@ -51,12 +51,12 @@ export const TOOL_RESULT_CAP = 4_000;
|
|
|
51
51
|
|
|
52
52
|
const CAP_NOTE =
|
|
53
53
|
`\n[RLM: tool output capped at ${TOOL_RESULT_CAP.toLocaleString()} chars to protect the root ` +
|
|
54
|
-
"model's context — route bulk text through repl() + llm_query_chunked /
|
|
54
|
+
"model's context — route bulk text through repl() + llm_query_chunked / llm_batch.]";
|
|
55
55
|
|
|
56
56
|
const REPL_CAP_NOTE =
|
|
57
57
|
`\n[RLM: repl() stdout capped at ${TOOL_RESULT_CAP.toLocaleString()} chars — printing bulk text ` +
|
|
58
58
|
"is useless. Keep results in REPL variables and delegate semantic reading to llm_query / " +
|
|
59
|
-
"
|
|
59
|
+
"llm_batch / llm_query_chunked.]";
|
|
60
60
|
|
|
61
61
|
/** Shared truncation core. Returns undefined when under the cap (leave the text untouched). */
|
|
62
62
|
function capText(text: string, note: string): string | undefined {
|
|
@@ -86,7 +86,7 @@ export function replDelegationNudge(stdoutChars: number, delegated: boolean): st
|
|
|
86
86
|
if (delegated || stdoutChars <= NUDGE_STDOUT_CHARS) return undefined;
|
|
87
87
|
return (
|
|
88
88
|
`\n[RLM: this repl() printed ${stdoutChars.toLocaleString()} chars with 0 sub-LLM calls — ` +
|
|
89
|
-
"if you were READING, delegate via llm_query /
|
|
89
|
+
"if you were READING, delegate via llm_query / llm_batch / llm_query_chunked. " +
|
|
90
90
|
"Authoring an edit body yourself is correct and needs no delegation.]"
|
|
91
91
|
);
|
|
92
92
|
}
|
package/src/mode/subagent.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* 2. Capability gate — never confiscate native readers unless `repl` is in the
|
|
7
7
|
* active tool set (paper trade: scaffold only if the REPL substitute exists).
|
|
8
8
|
*
|
|
9
|
-
* In-process rlm_query depth is handled by
|
|
9
|
+
* In-process rlm_query depth is handled by bridge/handlers childRun; this module
|
|
10
10
|
* only covers OS-process children (pi subagents), which restart at depth 0.
|
|
11
11
|
*/
|
|
12
12
|
import { DEFAULT_CONFIG } from "../config/defaults.ts";
|