@hicaru/pi-rlm 0.3.5 → 0.3.8
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 +1 -1
- package/package.json +2 -2
- package/src/bridge/handlers/llm-query.ts +73 -17
- package/src/bridge/handlers/rlm-query.ts +130 -8
- package/src/bridge/handlers/types.ts +12 -0
- package/src/config/defaults.ts +17 -0
- package/src/config/settings.ts +45 -2
- 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 +171 -5
- package/src/core/ledger.ts +451 -0
- package/src/core/memory.ts +577 -0
- package/src/core/model-registry.ts +88 -0
- package/src/core/types.ts +44 -3
- package/src/index.ts +60 -9
- package/src/mode/rlm-mode.ts +47 -9
- package/src/prompts/glossary.ts +139 -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-result.ts +54 -10
- package/src/tool/repl-tool.ts +43 -9
- package/src/ui/status.ts +4 -1
- package/src/util/concurrency.ts +47 -0
package/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<img src="https://
|
|
2
|
+
<img src="https://github.com/openzebra/rlm.pi/blob/master/assets/plugin-cover.png?raw=true" width="100%" alt="pi-rlm — Recursive Language Model plugin for Pi">
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
<p align="center">
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hicaru/pi-rlm",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.8",
|
|
4
4
|
"author": "hicaru",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"extensions": [
|
|
38
38
|
"./src/index.ts"
|
|
39
39
|
],
|
|
40
|
-
"image": "https://
|
|
40
|
+
"image": "https://github.com/openzebra/rlm.pi/blob/master/assets/plugin-cover.png?raw=true"
|
|
41
41
|
},
|
|
42
42
|
"publishConfig": {
|
|
43
43
|
"access": "public"
|
|
@@ -6,11 +6,12 @@ import type { Usage } from "@earendil-works/pi-ai";
|
|
|
6
6
|
import { modelRef } from "../../config/settings.ts";
|
|
7
7
|
import { complete1, type Complete1Deps } from "./completion.ts";
|
|
8
8
|
import { emitting, summarizeBatch } from "./emitting.ts";
|
|
9
|
-
import { formatError, isErrorText } from "../../util/errors.ts";
|
|
9
|
+
import { formatError, isErrorText, errorMessage } from "../../util/errors.ts";
|
|
10
10
|
import { previewText } from "../../text/preview.ts";
|
|
11
11
|
import type { SpawnResult, SubcallHandlerDeps } from "./types.ts";
|
|
12
12
|
import type { SubcallOpts } from "../../sandbox/interrupts.ts";
|
|
13
13
|
import { SPAWN_HINT, spawnAndRun, type SpawnDeps } from "./task-registry.ts";
|
|
14
|
+
import { ECHO_STUB, taskKey, type TaskLedger } from "../../core/ledger.ts";
|
|
14
15
|
|
|
15
16
|
const UNWIRED = formatError("RLM bridge not wired for this invocation");
|
|
16
17
|
|
|
@@ -34,6 +35,45 @@ function displayModel(deps: SubcallHandlerDeps): string | undefined {
|
|
|
34
35
|
}
|
|
35
36
|
}
|
|
36
37
|
|
|
38
|
+
/** The ledger active for leaf calls — undefined when disabled by config or not threaded in. */
|
|
39
|
+
function activeLedger(deps: SubcallHandlerDeps): TaskLedger | undefined {
|
|
40
|
+
return deps.getConfig().enableLedger ? deps.ledger : undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** v5 TaskLedger routing for ONE leaf prompt (audit H3 — shared by llm_query and every
|
|
44
|
+
* llm_batch item, which v5 routed through `_spawn_single` too): echo → stub string,
|
|
45
|
+
* coalesce → the twin's result (bounded wait), run → caller executes then finish/fail.
|
|
46
|
+
* Exported for tests. */
|
|
47
|
+
export async function runClaimedLeaf(
|
|
48
|
+
ledger: TaskLedger | undefined,
|
|
49
|
+
key: string | undefined,
|
|
50
|
+
prompt: string,
|
|
51
|
+
depth: number,
|
|
52
|
+
exec: () => Promise<string>,
|
|
53
|
+
): Promise<string> {
|
|
54
|
+
if (ledger === undefined || key === undefined) return exec();
|
|
55
|
+
const decision = ledger.tryClaim({ kind: "llm", prompt, paths: [], depth }, key);
|
|
56
|
+
if (decision.type === "echo") return ECHO_STUB;
|
|
57
|
+
if (decision.type === "coalesce") {
|
|
58
|
+
return ledger.waitFor(decision.key).catch((err: unknown): string => formatError(errorMessage(err)));
|
|
59
|
+
}
|
|
60
|
+
ledger.markRunning(key);
|
|
61
|
+
try {
|
|
62
|
+
const out = await exec();
|
|
63
|
+
ledger.finish(key, out);
|
|
64
|
+
return out;
|
|
65
|
+
} catch (err: unknown) {
|
|
66
|
+
ledger.fail(key, errorMessage(err));
|
|
67
|
+
throw err;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function leafClaimKey(deps: SubcallHandlerDeps, prompt: string): string | undefined {
|
|
72
|
+
const ledger = activeLedger(deps);
|
|
73
|
+
if (ledger === undefined) return undefined;
|
|
74
|
+
return taskKey("llm", prompt, [], displayModel(deps) ?? "", "");
|
|
75
|
+
}
|
|
76
|
+
|
|
37
77
|
export function createLlmQueryHandler(
|
|
38
78
|
deps: SubcallHandlerDeps,
|
|
39
79
|
sd: SpawnDeps,
|
|
@@ -57,25 +97,28 @@ export function createLlmQueryHandler(
|
|
|
57
97
|
}
|
|
58
98
|
|
|
59
99
|
const cdeps = completeDeps(deps);
|
|
100
|
+
const runLeaf = (): Promise<string> =>
|
|
101
|
+
emitting(
|
|
102
|
+
inv,
|
|
103
|
+
{
|
|
104
|
+
kind: "llm",
|
|
105
|
+
label: "llm_query",
|
|
106
|
+
args: `prompt: ${previewText(prompt)}`,
|
|
107
|
+
model: displayModel(deps),
|
|
108
|
+
},
|
|
109
|
+
(track: (u: Usage) => void) => complete1(inv, prompt, track, cdeps),
|
|
110
|
+
(out) => ({
|
|
111
|
+
preview: previewText(out),
|
|
112
|
+
error: isErrorText(out) ? out : undefined,
|
|
113
|
+
}),
|
|
114
|
+
);
|
|
115
|
+
// v5 TaskLedger for leaves: identical prompts coalesce onto one completion (key has no
|
|
116
|
+
// context — a leaf's entire world is the prompt text itself).
|
|
60
117
|
return spawnAndRun(
|
|
61
118
|
sd,
|
|
62
119
|
"llm",
|
|
63
120
|
1,
|
|
64
|
-
() =>
|
|
65
|
-
emitting(
|
|
66
|
-
inv,
|
|
67
|
-
{
|
|
68
|
-
kind: "llm",
|
|
69
|
-
label: "llm_query",
|
|
70
|
-
args: `prompt: ${previewText(prompt)}`,
|
|
71
|
-
model: displayModel(deps),
|
|
72
|
-
},
|
|
73
|
-
(track: (u: Usage) => void) => complete1(inv, prompt, track, cdeps),
|
|
74
|
-
(out) => ({
|
|
75
|
-
preview: previewText(out),
|
|
76
|
-
error: isErrorText(out) ? out : undefined,
|
|
77
|
-
}),
|
|
78
|
-
),
|
|
121
|
+
() => runClaimedLeaf(activeLedger(deps), leafClaimKey(deps, prompt), prompt, inv.depth, runLeaf),
|
|
79
122
|
deps.trackDetached,
|
|
80
123
|
opts.detached,
|
|
81
124
|
);
|
|
@@ -105,6 +148,7 @@ export function createLlmBatchHandler(
|
|
|
105
148
|
}
|
|
106
149
|
|
|
107
150
|
const cdeps = completeDeps(deps);
|
|
151
|
+
const ledger = activeLedger(deps);
|
|
108
152
|
return spawnAndRun(
|
|
109
153
|
sd,
|
|
110
154
|
"llm_batch",
|
|
@@ -119,8 +163,20 @@ export function createLlmBatchHandler(
|
|
|
119
163
|
model: displayModel(deps),
|
|
120
164
|
},
|
|
121
165
|
// NO outer gate — complete1 takes the single leaf slot per prompt.
|
|
166
|
+
// v5 (audit H3): every item routes through the ledger — duplicate prompts inside
|
|
167
|
+
// one batch (or twins of other in-flight leaves) coalesce instead of paying N times.
|
|
122
168
|
(track: (u: Usage) => void) =>
|
|
123
|
-
Promise.all(
|
|
169
|
+
Promise.all(
|
|
170
|
+
prompts.map((p) =>
|
|
171
|
+
runClaimedLeaf(
|
|
172
|
+
ledger,
|
|
173
|
+
ledger === undefined ? undefined : leafClaimKey(deps, p),
|
|
174
|
+
p,
|
|
175
|
+
inv.depth,
|
|
176
|
+
() => complete1(inv, p, track, cdeps),
|
|
177
|
+
),
|
|
178
|
+
),
|
|
179
|
+
),
|
|
124
180
|
summarizeBatch,
|
|
125
181
|
),
|
|
126
182
|
deps.trackDetached,
|
|
@@ -10,10 +10,14 @@ import { filterContextByPaths } from "../../context/merge.ts";
|
|
|
10
10
|
import { previewText } from "../../text/preview.ts";
|
|
11
11
|
import type { RlmInput, RlmResult } from "../../core/types.ts";
|
|
12
12
|
import { checkResourceLimits } from "../../core/resource-limits.ts";
|
|
13
|
+
import { contextSig, ECHO_STUB, taskKey } from "../../core/ledger.ts";
|
|
13
14
|
import type { Invocation, SpawnResult, SubcallHandlerDeps } from "./types.ts";
|
|
14
15
|
import type { SubcallOpts } from "../../sandbox/interrupts.ts";
|
|
15
16
|
import { SPAWN_HINT, spawnAndRun, type SpawnDeps } from "./task-registry.ts";
|
|
16
17
|
import { complete1, type Complete1Deps } from "./completion.ts";
|
|
18
|
+
import { emitting } from "./emitting.ts";
|
|
19
|
+
import { isErrorText } from "../../util/errors.ts";
|
|
20
|
+
import { leafClaimKey, runClaimedLeaf } from "./llm-query.ts";
|
|
17
21
|
|
|
18
22
|
const UNWIRED = formatError("RLM bridge not wired for this invocation");
|
|
19
23
|
const NO_UNMATCHED: readonly string[] = Object.freeze([]);
|
|
@@ -64,9 +68,20 @@ function completeDeps(deps: SubcallHandlerDeps): Complete1Deps {
|
|
|
64
68
|
};
|
|
65
69
|
}
|
|
66
70
|
|
|
71
|
+
/** Ledger active for this call — undefined when disabled by config or not threaded in. */
|
|
72
|
+
function activeLedger(deps: SubcallHandlerDeps) {
|
|
73
|
+
return deps.getConfig().enableLedger ? deps.ledger : undefined;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function claimKeyFor(deps: SubcallHandlerDeps, kind: "llm" | "rlm", prompt: string, paths: readonly string[], ctx: string): string {
|
|
77
|
+
const rootModel = deps.getModel?.();
|
|
78
|
+
const modelId = rootModel === undefined ? "" : (modelRef(rootModel) ?? rootModel.id);
|
|
79
|
+
return taskKey(kind, prompt, paths, modelId, ctx);
|
|
80
|
+
}
|
|
81
|
+
|
|
67
82
|
/**
|
|
68
|
-
* One child RLM run: depth cap → resource guard → depth gate → spawn engine →
|
|
69
|
-
* Emits its own subcall node (do not wrap in emitting()).
|
|
83
|
+
* One child RLM run: depth cap → resource guard → ledger gate → depth gate → spawn engine →
|
|
84
|
+
* debit parent. Emits its own subcall node (do not wrap in emitting()).
|
|
70
85
|
*/
|
|
71
86
|
async function childRun(
|
|
72
87
|
deps: SubcallHandlerDeps,
|
|
@@ -91,23 +106,74 @@ async function childRun(
|
|
|
91
106
|
const limitError = checkResourceLimits({ timeoutMs: remTimeout });
|
|
92
107
|
if (limitError !== undefined) return emptyResult(limitError);
|
|
93
108
|
|
|
109
|
+
const child = childContextFor(deps, prompt, paths);
|
|
110
|
+
const rootPrompt =
|
|
111
|
+
child.unmatched.length === 0
|
|
112
|
+
? prompt
|
|
113
|
+
: `${prompt}\n\n[rlm] paths=${child.unmatched.join(", ")} matched no files; you received the full context.`;
|
|
114
|
+
|
|
115
|
+
// ── v5 memory replay: an identical, still-fresh child answer replays for zero API calls ──
|
|
116
|
+
const memory = deps.memory;
|
|
117
|
+
const sig = contextSig(child.context);
|
|
118
|
+
const key = claimKeyFor(deps, "rlm", prompt, paths ?? [], sig);
|
|
119
|
+
if (memory !== undefined && deps.getConfig().enableMemory !== false) {
|
|
120
|
+
const hit = memory.replay(key);
|
|
121
|
+
if (hit !== undefined) {
|
|
122
|
+
const replayId = inv.emitter.emitSubcallCreated({
|
|
123
|
+
kind: "rlm",
|
|
124
|
+
parentId: inv.parentId,
|
|
125
|
+
label: "rlm_query (replay)",
|
|
126
|
+
detail: prompt.slice(0, 60),
|
|
127
|
+
depth: childDepth,
|
|
128
|
+
});
|
|
129
|
+
inv.emitter.emitSubcallUpdated({ id: replayId, status: "done", resultPreview: hit.result.slice(0, 200) });
|
|
130
|
+
return {
|
|
131
|
+
answer: hit.result,
|
|
132
|
+
iterations: 0,
|
|
133
|
+
costUsd: 0,
|
|
134
|
+
inputTokens: 0,
|
|
135
|
+
outputTokens: 0,
|
|
136
|
+
durationMs: 0,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ── v5 TaskLedger: echo → stub; duplicate → coalesce onto the existing runner ──────
|
|
142
|
+
const ledger = activeLedger(deps);
|
|
143
|
+
const claimKey = ledger === undefined ? undefined : key;
|
|
144
|
+
const decision =
|
|
145
|
+
ledger !== undefined && claimKey !== undefined
|
|
146
|
+
? ledger.tryClaim({ kind: "rlm", prompt, paths: paths ?? [], depth: childDepth }, claimKey)
|
|
147
|
+
: undefined;
|
|
148
|
+
|
|
149
|
+
// ONE subcall node per childRun (audit C2 / DRY #5): the decision branch reuses it, the
|
|
150
|
+
// run branch reports the engine's turns/cost on it. Never a second emit below.
|
|
94
151
|
const rootModel = deps.getModel?.();
|
|
95
152
|
const modelLabel =
|
|
96
153
|
rootModel === undefined ? undefined : (modelRef(rootModel) ?? rootModel.id);
|
|
97
154
|
const subId = inv.emitter.emitSubcallCreated({
|
|
98
155
|
kind: "rlm",
|
|
99
156
|
parentId: inv.parentId,
|
|
100
|
-
label: "rlm_query"
|
|
157
|
+
label: decision === undefined || decision.type === "run" ? "rlm_query" : `rlm_query (${decision.type})`,
|
|
101
158
|
model: modelLabel,
|
|
102
159
|
detail: prompt.slice(0, 60),
|
|
103
160
|
depth: childDepth,
|
|
104
161
|
});
|
|
105
162
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
163
|
+
if (decision?.type === "echo") {
|
|
164
|
+
inv.emitter.emitSubcallUpdated({ id: subId, status: "done", resultPreview: ECHO_STUB.slice(0, 80) });
|
|
165
|
+
return emptyResult(ECHO_STUB);
|
|
166
|
+
}
|
|
167
|
+
if (decision?.type === "coalesce" && ledger !== undefined) {
|
|
168
|
+
const twin = await ledger
|
|
169
|
+
.waitFor(decision.key)
|
|
170
|
+
.catch((err: unknown) => errorMessage(err));
|
|
171
|
+
inv.emitter.emitSubcallUpdated({ id: subId, status: "done", resultPreview: previewText(String(twin).slice(0, 80)) });
|
|
172
|
+
return emptyResult(String(twin));
|
|
173
|
+
}
|
|
174
|
+
if (ledger !== undefined && claimKey !== undefined) {
|
|
175
|
+
ledger.markRunning(claimKey);
|
|
176
|
+
}
|
|
111
177
|
|
|
112
178
|
const input: RlmInput = {
|
|
113
179
|
rootPrompt,
|
|
@@ -115,12 +181,27 @@ async function childRun(
|
|
|
115
181
|
depth: childDepth,
|
|
116
182
|
parentNodeId: subId,
|
|
117
183
|
remainingTimeoutMs: remTimeout,
|
|
184
|
+
ledger, // DRY #6: the one seam — children share the parent's blackboard
|
|
118
185
|
};
|
|
119
186
|
|
|
120
187
|
try {
|
|
121
188
|
const res = await deps.gates.rlm.at(childDepth).run(() => run(input, inv));
|
|
122
189
|
inv.limits.addRaw(res.costUsd, res.inputTokens, res.outputTokens);
|
|
123
190
|
deps.onChildUsage?.(res.costUsd, res.inputTokens, res.outputTokens);
|
|
191
|
+
if (ledger !== undefined && claimKey !== undefined) ledger.finish(claimKey, res.answer);
|
|
192
|
+
// v5: child answers persist unconditionally — this is what later identical runs replay.
|
|
193
|
+
if (memory !== undefined && deps.getConfig().enableMemory !== false) {
|
|
194
|
+
memory.recordEpisode({
|
|
195
|
+
key,
|
|
196
|
+
kind: "rlm",
|
|
197
|
+
model: modelLabel ?? "",
|
|
198
|
+
prompt,
|
|
199
|
+
paths: paths ?? [],
|
|
200
|
+
result: res.answer,
|
|
201
|
+
tokensIn: res.inputTokens,
|
|
202
|
+
tokensOut: res.outputTokens,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
124
205
|
inv.emitter.emitSubcallUpdated({
|
|
125
206
|
id: subId,
|
|
126
207
|
status: "done",
|
|
@@ -129,6 +210,7 @@ async function childRun(
|
|
|
129
210
|
return res;
|
|
130
211
|
} catch (err: unknown) {
|
|
131
212
|
const msg = errorMessage(err);
|
|
213
|
+
if (ledger !== undefined && claimKey !== undefined) ledger.fail(claimKey, msg);
|
|
132
214
|
inv.emitter.emitSubcallUpdated({ id: subId, status: "error", detail: msg });
|
|
133
215
|
return emptyResult(formatError(`child RLM failed - ${msg}`));
|
|
134
216
|
}
|
|
@@ -155,6 +237,46 @@ export function createRlmQueryHandler(deps: SubcallHandlerDeps, sd: SpawnDeps) {
|
|
|
155
237
|
|
|
156
238
|
const pathArg = opts.paths;
|
|
157
239
|
|
|
240
|
+
// v5 rlmBudget demotion: once the ledger has started `rlmBudget` real rlm runs, extra
|
|
241
|
+
// rlm_query spawns demote to the leaf path (batch spawns are exempt — v5 `_spawn_single`).
|
|
242
|
+
const ledger = activeLedger(deps);
|
|
243
|
+
const rlmBudget = deps.getConfig().rlmBudget;
|
|
244
|
+
if (
|
|
245
|
+
ledger !== undefined &&
|
|
246
|
+
rlmBudget !== undefined &&
|
|
247
|
+
rlmBudget > 0 &&
|
|
248
|
+
ledger.rlmCount() >= rlmBudget
|
|
249
|
+
) {
|
|
250
|
+
return spawnAndRun(
|
|
251
|
+
sd,
|
|
252
|
+
"llm",
|
|
253
|
+
1,
|
|
254
|
+
() =>
|
|
255
|
+
runClaimedLeaf(
|
|
256
|
+
ledger,
|
|
257
|
+
leafClaimKey(deps, task),
|
|
258
|
+
task,
|
|
259
|
+
inv.depth,
|
|
260
|
+
() =>
|
|
261
|
+
emitting(
|
|
262
|
+
inv,
|
|
263
|
+
{
|
|
264
|
+
kind: "llm",
|
|
265
|
+
label: "rlm_query→llm (demoted)",
|
|
266
|
+
args: previewText(task),
|
|
267
|
+
},
|
|
268
|
+
(track) => complete1(inv, task, track, completeDeps(deps)),
|
|
269
|
+
(out) => ({
|
|
270
|
+
preview: previewText(out),
|
|
271
|
+
error: isErrorText(out) ? out : undefined,
|
|
272
|
+
}),
|
|
273
|
+
),
|
|
274
|
+
),
|
|
275
|
+
deps.trackDetached,
|
|
276
|
+
opts.detached,
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
158
280
|
return spawnAndRun(
|
|
159
281
|
sd,
|
|
160
282
|
"rlm",
|
|
@@ -12,6 +12,8 @@ import type { RlmInput, RlmResult, Sampling } from "../../core/types.ts";
|
|
|
12
12
|
import type { SubcallGates } from "../../util/concurrency.ts";
|
|
13
13
|
import type { SubcallOpts } from "../../sandbox/interrupts.ts";
|
|
14
14
|
import type { RlmEmitter } from "../../tool/rlm-events.ts";
|
|
15
|
+
import type { TaskLedger } from "../../core/ledger.ts";
|
|
16
|
+
import type { MemoryStore } from "../../core/memory.ts";
|
|
15
17
|
|
|
16
18
|
// ---------------------------------------------------------------------------
|
|
17
19
|
// Spawn / Await / Finish — the three shapes the model sees
|
|
@@ -85,6 +87,12 @@ export interface SubcallConfig {
|
|
|
85
87
|
readonly maxDepth: number;
|
|
86
88
|
readonly subSampling?: Sampling;
|
|
87
89
|
readonly subSystemPrompt?: string;
|
|
90
|
+
/** v5 TaskLedger: claim/coalesce/echo gates (optional — unwired callers keep ledger off). */
|
|
91
|
+
readonly enableLedger?: boolean;
|
|
92
|
+
/** v5: real rlm spawns before demotion to llm (0 = never demote). */
|
|
93
|
+
readonly rlmBudget?: number;
|
|
94
|
+
/** v5 durable memory gates (optional; omitted → memory off). */
|
|
95
|
+
readonly enableMemory?: boolean;
|
|
88
96
|
}
|
|
89
97
|
|
|
90
98
|
export interface SubcallHandlerDeps {
|
|
@@ -104,6 +112,10 @@ export interface SubcallHandlerDeps {
|
|
|
104
112
|
readonly degrade?: (prompt: string, depth: number) => Promise<string>;
|
|
105
113
|
readonly onChildUsage?: (costUsd: number, inputTokens: number, outputTokens: number) => void;
|
|
106
114
|
readonly trackDetached?: <T>(run: () => Promise<T>) => Promise<T>;
|
|
115
|
+
/** v5 TaskLedger blackboard shared across the whole run tree (claim/coalesce/echo/demote). */
|
|
116
|
+
readonly ledger?: TaskLedger;
|
|
117
|
+
/** v5 durable memory (session-wide store) for child replay + episode persistence. */
|
|
118
|
+
readonly memory?: MemoryStore;
|
|
107
119
|
}
|
|
108
120
|
|
|
109
121
|
// ---------------------------------------------------------------------------
|
package/src/config/defaults.ts
CHANGED
|
@@ -31,4 +31,21 @@ export const DEFAULT_CONFIG: Readonly<RlmConfig> = Object.freeze({
|
|
|
31
31
|
rootSampling: Object.freeze({ maxTokens: 16_384 }),
|
|
32
32
|
subSystemPrompt: DEFAULT_SUB_SYSTEM_PROMPT,
|
|
33
33
|
subSampling: Object.freeze({ maxTokens: 8192 }),
|
|
34
|
+
// v5 token budget cascade — the primary run-length control (wall-clock stays a hang backstop).
|
|
35
|
+
enableTokenBudget: true,
|
|
36
|
+
budgetShare: 0.25,
|
|
37
|
+
budgetSoftFrac: 0.8,
|
|
38
|
+
budgetTaskCap: 400_000,
|
|
39
|
+
budgetMaxContinuations: 2,
|
|
40
|
+
budgetHandoffChars: 4_000,
|
|
41
|
+
// v5 TaskLedger blackboard
|
|
42
|
+
enableLedger: true,
|
|
43
|
+
rlmBudget: 8,
|
|
44
|
+
// v5 durable memory
|
|
45
|
+
enableMemory: true,
|
|
46
|
+
injectNoteTokens: 2_000,
|
|
47
|
+
evolveEvery: 8,
|
|
48
|
+
memoryDir: null,
|
|
49
|
+
// v5 role separation: children delegate (llm + memory/ledger); "legacy" = full child surface.
|
|
50
|
+
childSurface: "delegation",
|
|
34
51
|
});
|
package/src/config/settings.ts
CHANGED
|
@@ -45,7 +45,9 @@ function validateThinkingLevel(v: unknown): ThinkingLevel | undefined {
|
|
|
45
45
|
return typeof v === "string" && Object.hasOwn(THINKING_LEVELS, v) ? (v as ThinkingLevel) : undefined;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
/** Validate an unknown (e.g. hand-edited rlm.json) config blob into a partial — the single
|
|
49
|
+
* validation seam; exported for tests. */
|
|
50
|
+
export function validateConfig(raw: unknown): Partial<RlmConfig> {
|
|
49
51
|
if (typeof raw !== "object" || raw === null) return {};
|
|
50
52
|
const r = raw as Record<string, unknown>;
|
|
51
53
|
const out: MutablePartialRlmConfig = {};
|
|
@@ -90,6 +92,47 @@ function validateConfig(raw: unknown): Partial<RlmConfig> {
|
|
|
90
92
|
if (contextLoader !== undefined) out.contextLoader = contextLoader;
|
|
91
93
|
const autoSeedCwd = validateBoolean(r.autoSeedCwd);
|
|
92
94
|
if (autoSeedCwd !== undefined) out.autoSeedCwd = autoSeedCwd;
|
|
95
|
+
// v5 token budget cascade
|
|
96
|
+
const enableTokenBudget = validateBoolean(r.enableTokenBudget);
|
|
97
|
+
if (enableTokenBudget !== undefined) out.enableTokenBudget = enableTokenBudget;
|
|
98
|
+
const budgetShare = validateNumber(r.budgetShare, 0.01);
|
|
99
|
+
if (budgetShare !== undefined && budgetShare <= 1) out.budgetShare = budgetShare;
|
|
100
|
+
const budgetSoftFrac = validateNumber(r.budgetSoftFrac, 0.5);
|
|
101
|
+
if (budgetSoftFrac !== undefined && budgetSoftFrac < 1) out.budgetSoftFrac = budgetSoftFrac;
|
|
102
|
+
const budgetTaskCap = validateNumber(r.budgetTaskCap, 0);
|
|
103
|
+
if (budgetTaskCap !== undefined) out.budgetTaskCap = budgetTaskCap;
|
|
104
|
+
const budgetMaxContinuations = validateNumber(r.budgetMaxContinuations, 0);
|
|
105
|
+
if (budgetMaxContinuations !== undefined) out.budgetMaxContinuations = budgetMaxContinuations;
|
|
106
|
+
const budgetHandoffChars = validateNumber(r.budgetHandoffChars, 500);
|
|
107
|
+
if (budgetHandoffChars !== undefined) out.budgetHandoffChars = budgetHandoffChars;
|
|
108
|
+
// v5 TaskLedger blackboard
|
|
109
|
+
const enableLedger = validateBoolean(r.enableLedger);
|
|
110
|
+
if (enableLedger !== undefined) out.enableLedger = enableLedger;
|
|
111
|
+
const rlmBudget = validateNumber(r.rlmBudget, 0);
|
|
112
|
+
if (rlmBudget !== undefined) out.rlmBudget = rlmBudget;
|
|
113
|
+
// v5 durable memory
|
|
114
|
+
const enableMemory = validateBoolean(r.enableMemory);
|
|
115
|
+
if (enableMemory !== undefined) out.enableMemory = enableMemory;
|
|
116
|
+
const injectNoteTokens = validateNumber(r.injectNoteTokens, 100);
|
|
117
|
+
if (injectNoteTokens !== undefined) out.injectNoteTokens = injectNoteTokens;
|
|
118
|
+
const evolveEvery = validateNumber(r.evolveEvery, 0);
|
|
119
|
+
if (evolveEvery !== undefined) out.evolveEvery = evolveEvery;
|
|
120
|
+
if (r.memoryDir === null) out.memoryDir = null;
|
|
121
|
+
else {
|
|
122
|
+
const memoryDir = validateString(r.memoryDir);
|
|
123
|
+
if (memoryDir !== undefined) out.memoryDir = memoryDir;
|
|
124
|
+
}
|
|
125
|
+
// v5 provider concurrency caps: { provider: minConcurrent }
|
|
126
|
+
if (typeof r.providerMaxConcurrent === "object" && r.providerMaxConcurrent !== null) {
|
|
127
|
+
const caps: Record<string, number> = {};
|
|
128
|
+
for (const [provider, cap] of Object.entries(r.providerMaxConcurrent as Record<string, unknown>)) {
|
|
129
|
+
const n = validateNumber(cap, 1);
|
|
130
|
+
if (n !== undefined) caps[provider] = n;
|
|
131
|
+
}
|
|
132
|
+
if (Object.keys(caps).length > 0) out.providerMaxConcurrent = Object.freeze(caps);
|
|
133
|
+
}
|
|
134
|
+
// v5 child surface doctrine
|
|
135
|
+
if (r.childSurface === "delegation" || r.childSurface === "legacy") out.childSurface = r.childSurface;
|
|
93
136
|
if (typeof r.subSampling === "object" && r.subSampling !== null) {
|
|
94
137
|
const ss = r.subSampling as Record<string, unknown>;
|
|
95
138
|
const sampling: { maxTokens?: number; temperature?: number; reasoning?: ThinkingLevel } = {};
|
|
@@ -99,7 +142,7 @@ function validateConfig(raw: unknown): Partial<RlmConfig> {
|
|
|
99
142
|
if (temperature !== undefined) sampling.temperature = temperature;
|
|
100
143
|
const ssReasoning = validateThinkingLevel(ss.reasoning);
|
|
101
144
|
if (ssReasoning !== undefined) sampling.reasoning = ssReasoning;
|
|
102
|
-
out.subSampling = sampling;
|
|
145
|
+
out.subSampling = Object.freeze(sampling);
|
|
103
146
|
}
|
|
104
147
|
if (typeof r.rootSampling === "object" && r.rootSampling !== null) {
|
|
105
148
|
const rs = r.rootSampling as Record<string, unknown>;
|
package/src/core/answer.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** Helpers for detecting and formatting the RLM final answer from a turn's REPL results. */
|
|
2
2
|
|
|
3
3
|
import type { ReplResult } from "../sandbox/protocol.ts";
|
|
4
|
-
import {
|
|
4
|
+
import { formatReplStderr } from "../text/repl-output.ts";
|
|
5
5
|
|
|
6
6
|
/** First non-null final answer across a turn's executed blocks, or null. */
|
|
7
7
|
export function finalAnswerOf(results: readonly ReplResult[]): string | null {
|
|
@@ -29,9 +29,10 @@ export function turnHadError(results: readonly ReplResult[]): boolean {
|
|
|
29
29
|
const SMALL_STDOUT_LIMIT = 800;
|
|
30
30
|
const STDOUT_PREVIEW_LIMIT = 200;
|
|
31
31
|
const STDOUT_TAIL_LIMIT = 200;
|
|
32
|
-
const STDERR_LIMIT = 8_000;
|
|
33
32
|
|
|
34
|
-
/** The REPL output fed back to the model as the next user message.
|
|
33
|
+
/** The REPL output fed back to the model as the next user message. Prefixed `REPL stdout:`
|
|
34
|
+
* (v5 parity, audit C4): `distillTrajectory` keys on this needle to harvest the working
|
|
35
|
+
* set for a budget-capped continuation — without it a hard-cap chain starts blind. */
|
|
35
36
|
export function formatReplOutputs(results: readonly ReplResult[], skippedBlocks = 0): string {
|
|
36
37
|
if (results.length === 0) {
|
|
37
38
|
return "No ```repl``` block found in your response. Write one to interact with the REPL.";
|
|
@@ -44,21 +45,21 @@ export function formatReplOutputs(results: readonly ReplResult[], skippedBlocks
|
|
|
44
45
|
const head = multi ? `[block ${i + 1}]\n` : "";
|
|
45
46
|
const { text, elided } = formatStdout(r);
|
|
46
47
|
hadElision ||= elided;
|
|
47
|
-
parts[i] = `${head}${text}${
|
|
48
|
+
parts[i] = `${head}${text}${formatReplStderr(r.stderr)}`;
|
|
48
49
|
}
|
|
49
50
|
const body = parts.join("\n\n");
|
|
50
51
|
const skipNote = skippedBlocks > 0
|
|
51
52
|
? `\n\n[${skippedBlocks} later \`\`\`repl\`\`\` block(s) skipped because an earlier block raised — fix and re-run them]`
|
|
52
53
|
: "";
|
|
53
54
|
// Orientation hint only when the model lost output to elision — otherwise it sees everything.
|
|
54
|
-
if (!hadElision) return
|
|
55
|
+
if (!hadElision) return `REPL stdout:\n${body}${skipNote}`;
|
|
55
56
|
// The REPL namespace is persistent across blocks in a turn, so the last block's varNames reflect
|
|
56
57
|
// every variable created in any earlier block too.
|
|
57
58
|
const varNames = results.at(-1)?.varNames ?? [];
|
|
58
59
|
const hint = varNames.length > 0
|
|
59
60
|
? `REPL vars: ${varNames.join(", ")}`
|
|
60
61
|
: `No REPL vars yet — assign results to variables before printing large outputs.`;
|
|
61
|
-
return
|
|
62
|
+
return `REPL stdout:\n${body}${skipNote}\n\n${hint}`;
|
|
62
63
|
}
|
|
63
64
|
|
|
64
65
|
/** Stdout ≤ SMALL_STDOUT_LIMIT flows through verbatim; larger output keeps a short head + a note
|
|
@@ -78,7 +79,3 @@ function formatStdout(r: ReplResult): { text: string; elided: boolean } {
|
|
|
78
79
|
};
|
|
79
80
|
}
|
|
80
81
|
|
|
81
|
-
function formatStderr(r: ReplResult): string {
|
|
82
|
-
const err = r.stderr.trim();
|
|
83
|
-
return err ? `\n[stderr]\n${truncateOutput(err, STDERR_LIMIT)}` : "";
|
|
84
|
-
}
|