@hicaru/pi-rlm 0.3.16 → 0.3.17
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 +0 -4
- package/README.ru.md +56 -66
- package/README.zh-CN.md +61 -65
- package/package.json +5 -5
- package/src/bridge/add-context.ts +1 -1
- package/src/bridge/handlers/await.ts +13 -22
- package/src/bridge/handlers/completion.ts +27 -5
- package/src/bridge/handlers/emitting.ts +2 -2
- package/src/bridge/handlers/llm-query.ts +46 -68
- package/src/bridge/handlers/rlm-query.ts +14 -84
- package/src/bridge/handlers/task-registry.ts +22 -17
- package/src/bridge/handlers/types.ts +8 -6
- package/src/bridge/model.ts +6 -3
- package/src/commands/rlm-llm.ts +1 -10
- package/src/commands/rlm-rlm.ts +1 -8
- package/src/config/defaults.ts +28 -12
- package/src/config/settings.ts +41 -31
- package/src/config/skillstate.ts +465 -0
- package/src/context/md-cache.ts +1 -1
- package/src/context/merge.ts +1 -1
- package/src/context/namespace.ts +2 -2
- package/src/context/refresh.ts +1 -1
- package/src/context/source-dir.ts +21 -11
- package/src/context/source-doc.ts +1 -1
- package/src/context/source-git.ts +3 -15
- package/src/context/source-text.ts +1 -1
- package/src/context/walk.ts +6 -14
- package/src/core/budget.ts +107 -21
- package/src/core/compaction.ts +44 -1
- package/src/core/engine.ts +141 -84
- package/src/core/iteration.ts +1 -1
- package/src/core/ledger.ts +10 -13
- package/src/core/limits.ts +1 -1
- package/src/core/model-registry.ts +1 -1
- package/src/core/resource-limits.ts +1 -1
- package/src/core/root-context.ts +126 -0
- package/src/core/root-digest.ts +213 -0
- package/src/core/root-state.ts +240 -0
- package/src/core/run-state.ts +577 -0
- package/src/core/types.ts +51 -12
- package/src/index.ts +167 -36
- package/src/mode/llm-model.ts +13 -1
- package/src/mode/native-guards.ts +0 -6
- package/src/mode/rlm-mode.ts +34 -11
- package/src/mode/subagent.ts +5 -5
- package/src/prompts/glossary.ts +41 -25
- package/src/prompts/native.ts +1 -3
- package/src/prompts/system.ts +12 -4
- package/src/sandbox/context-file.ts +1 -1
- package/src/sandbox/interrupts.ts +25 -31
- package/src/sandbox/protocol.ts +14 -20
- 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/__pycache__/worker.cpython-314.pyc +0 -0
- package/src/sandbox/py/guards.py +1 -1
- package/src/sandbox/py/scaffold.py +24 -31
- package/src/sandbox/py/worker.py +3 -1
- package/src/sandbox/sandbox-manager.ts +2 -2
- package/src/sandbox/sandbox.ts +21 -4
- package/src/text/agent-text.ts +58 -0
- package/src/text/parsing.ts +35 -3
- package/src/text/preview.ts +3 -0
- package/src/text/repl-output.ts +1 -1
- package/src/tool/background-tasks.ts +1 -1
- package/src/tool/repl-render.ts +1 -1
- package/src/tool/repl-result.ts +1 -1
- package/src/tool/repl-tool.ts +50 -26
- package/src/tool/rlm-tool.ts +4 -5
- package/src/tool/subcall-render.ts +1 -1
- package/src/tool/subcall-store.ts +2 -2
- package/src/tool/tool-utils.ts +5 -5
- package/src/ui/intro.ts +1 -1
- package/src/ui/modal/timeline-store.ts +1 -1
- package/src/ui/model-picker/drilldown.ts +1 -1
- package/src/ui/model-picker/levels.ts +1 -1
- package/src/ui/panel/run-registry.ts +1 -1
- package/src/ui/tree/tree-rows.ts +1 -1
- package/src/ui/tree/tree-widget.ts +1 -1
- package/src/util/bm25.ts +97 -0
- package/src/util/concurrency.ts +1 -1
- package/src/util/errors.ts +1 -1
- package/src/util/retry.ts +22 -7
- package/src/util/state-merge.ts +34 -0
- package/src/util/throttle.ts +1 -1
- package/src/util/type-guards.ts +6 -0
- package/src/core/memory.ts +0 -589
|
@@ -4,29 +4,20 @@
|
|
|
4
4
|
|
|
5
5
|
import type { Usage } from "@earendil-works/pi-ai";
|
|
6
6
|
import { modelRef } from "../../config/settings.ts";
|
|
7
|
-
import { complete1,
|
|
8
|
-
import { emitting, summarizeLeaf, throttleHooks } from "./emitting.ts";
|
|
7
|
+
import { complete1, completeDeps } from "./completion.ts";
|
|
8
|
+
import { emitting, summarizeLeaf, throttleHooks, type EmitNote } from "./emitting.ts";
|
|
9
9
|
import { formatError, errorMessage } from "../../util/errors.ts";
|
|
10
10
|
import { previewText } from "../../text/preview.ts";
|
|
11
|
-
import type { SpawnResult, SubcallHandlerDeps } from "./types.ts";
|
|
11
|
+
import type { Invocation, 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
14
|
import { ECHO_STUB, taskKey, type TaskLedger } from "../../core/ledger.ts";
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
/** Shared with rlm-query.ts — the unwired rejection sentinel (AGENTS DRY). */
|
|
17
|
+
export const UNWIRED = formatError("RLM bridge not wired for this invocation");
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
leafGate: deps.gates.leaf,
|
|
21
|
-
registry: deps.registry,
|
|
22
|
-
getLlmModel: deps.getLlmModel,
|
|
23
|
-
getConfig: deps.getConfig,
|
|
24
|
-
signal: deps.signal,
|
|
25
|
-
onUsage: deps.onUsage,
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function displayModel(deps: SubcallHandlerDeps): string | undefined {
|
|
19
|
+
/** Shared with rlm-query.ts — one display-model resolution (AGENTS DRY #3). */
|
|
20
|
+
export function displayModel(deps: SubcallHandlerDeps): string | undefined {
|
|
30
21
|
try {
|
|
31
22
|
const m = deps.getLlmModel();
|
|
32
23
|
return modelRef(m) ?? m.id;
|
|
@@ -36,10 +27,36 @@ function displayModel(deps: SubcallHandlerDeps): string | undefined {
|
|
|
36
27
|
}
|
|
37
28
|
|
|
38
29
|
/** The ledger active for leaf calls — undefined when disabled by config or not threaded in. */
|
|
39
|
-
function activeLedger(deps: SubcallHandlerDeps): TaskLedger | undefined {
|
|
30
|
+
export function activeLedger(deps: SubcallHandlerDeps): TaskLedger | undefined {
|
|
40
31
|
return deps.getConfig().enableLedger ? deps.ledger : undefined;
|
|
41
32
|
}
|
|
42
33
|
|
|
34
|
+
/** DRY: one unwired-spawn shape — kind/n vary, everything else is the same rejection.
|
|
35
|
+
* Shared with rlm-query.ts. */
|
|
36
|
+
export function unwiredSpawn(kind: SpawnResult["kind"], n: number): SpawnResult {
|
|
37
|
+
return { ok: false, task_id: null, kind, n, status: "pending", hint: SPAWN_HINT, error: UNWIRED };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** DRY: the shared leaf recipe — one UI node, one complete1 execution, one summarizer. */
|
|
41
|
+
function emitLeaf(
|
|
42
|
+
inv: Invocation,
|
|
43
|
+
deps: SubcallHandlerDeps,
|
|
44
|
+
prompt: string,
|
|
45
|
+
exec: (track: (u: Usage) => void, note: EmitNote) => Promise<string>,
|
|
46
|
+
): Promise<string> {
|
|
47
|
+
return emitting(
|
|
48
|
+
inv,
|
|
49
|
+
{
|
|
50
|
+
kind: "llm",
|
|
51
|
+
label: "llm_query",
|
|
52
|
+
args: `prompt: ${previewText(prompt)}`,
|
|
53
|
+
model: displayModel(deps),
|
|
54
|
+
},
|
|
55
|
+
(track, note) => exec(track, note),
|
|
56
|
+
summarizeLeaf,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
43
60
|
/** v5 TaskLedger routing for ONE leaf prompt (audit H3 — shared by llm_query and every
|
|
44
61
|
* llm_batch item, which v5 routed through `_spawn_single` too): echo → stub string,
|
|
45
62
|
* coalesce → the twin's result (bounded wait), run → caller executes then finish/fail.
|
|
@@ -84,31 +101,12 @@ export function createLlmQueryHandler(
|
|
|
84
101
|
opts: SubcallOpts,
|
|
85
102
|
): Promise<SpawnResult> => {
|
|
86
103
|
const inv = deps.resolve(opts, depth);
|
|
87
|
-
if (inv === null)
|
|
88
|
-
return {
|
|
89
|
-
ok: false,
|
|
90
|
-
task_id: null,
|
|
91
|
-
kind: "llm",
|
|
92
|
-
n: 1,
|
|
93
|
-
status: "pending",
|
|
94
|
-
hint: SPAWN_HINT,
|
|
95
|
-
error: UNWIRED,
|
|
96
|
-
};
|
|
97
|
-
}
|
|
104
|
+
if (inv === null) return unwiredSpawn("llm", 1);
|
|
98
105
|
|
|
99
106
|
const cdeps = completeDeps(deps);
|
|
100
107
|
const runLeaf = (): Promise<string> =>
|
|
101
|
-
|
|
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, note) => complete1(inv, prompt, track, cdeps, throttleHooks(note)),
|
|
110
|
-
summarizeLeaf,
|
|
111
|
-
);
|
|
108
|
+
emitLeaf(inv, deps, prompt, (track, note) =>
|
|
109
|
+
complete1(inv, prompt, track, cdeps, throttleHooks(note)));
|
|
112
110
|
// v5 TaskLedger for leaves: identical prompts coalesce onto one completion (key has no
|
|
113
111
|
// context — a leaf's entire world is the prompt text itself).
|
|
114
112
|
return spawnAndRun(
|
|
@@ -132,17 +130,7 @@ export function createLlmBatchHandler(
|
|
|
132
130
|
opts: SubcallOpts,
|
|
133
131
|
): Promise<SpawnResult> => {
|
|
134
132
|
const inv = deps.resolve(opts, depth);
|
|
135
|
-
if (inv === null)
|
|
136
|
-
return {
|
|
137
|
-
ok: false,
|
|
138
|
-
task_id: null,
|
|
139
|
-
kind: "llm_batch",
|
|
140
|
-
n: prompts.length,
|
|
141
|
-
status: "pending",
|
|
142
|
-
hint: SPAWN_HINT,
|
|
143
|
-
error: UNWIRED,
|
|
144
|
-
};
|
|
145
|
-
}
|
|
133
|
+
if (inv === null) return unwiredSpawn("llm_batch", prompts.length);
|
|
146
134
|
|
|
147
135
|
const cdeps = completeDeps(deps);
|
|
148
136
|
const ledger = activeLedger(deps);
|
|
@@ -155,27 +143,17 @@ export function createLlmBatchHandler(
|
|
|
155
143
|
() =>
|
|
156
144
|
Promise.all(
|
|
157
145
|
prompts.map((p) =>
|
|
158
|
-
|
|
159
|
-
inv,
|
|
160
|
-
{
|
|
161
|
-
kind: "llm",
|
|
162
|
-
label: "llm_query",
|
|
163
|
-
args: `prompt: ${previewText(p)}`,
|
|
164
|
-
model: displayModel(deps),
|
|
165
|
-
},
|
|
146
|
+
emitLeaf(inv, deps, p, (track, note) =>
|
|
166
147
|
// NO outer gate — complete1 takes the single leaf slot per prompt.
|
|
167
148
|
// v5 (audit H3): every item routes through the ledger — duplicate prompts inside
|
|
168
149
|
// one batch (or twins of other in-flight leaves) coalesce instead of paying N times.
|
|
169
|
-
(
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
),
|
|
177
|
-
summarizeLeaf,
|
|
178
|
-
),
|
|
150
|
+
runClaimedLeaf(
|
|
151
|
+
ledger,
|
|
152
|
+
ledger === undefined ? undefined : leafClaimKey(deps, p),
|
|
153
|
+
p,
|
|
154
|
+
inv.depth,
|
|
155
|
+
() => complete1(inv, p, track, cdeps, throttleHooks(note)),
|
|
156
|
+
)),
|
|
179
157
|
),
|
|
180
158
|
),
|
|
181
159
|
deps.trackDetached,
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* AGENTS.md DRY #2: childRun exists once, here.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { modelLabelOf } from "../../config/settings.ts";
|
|
8
8
|
import { errorMessage, formatError } from "../../util/errors.ts";
|
|
9
9
|
import { filterContextByPaths } from "../../context/merge.ts";
|
|
10
10
|
import { previewText } from "../../text/preview.ts";
|
|
@@ -13,12 +13,11 @@ import { checkResourceLimits } from "../../core/resource-limits.ts";
|
|
|
13
13
|
import { contextSig, ECHO_STUB, taskKey } from "../../core/ledger.ts";
|
|
14
14
|
import type { Invocation, SpawnResult, SubcallHandlerDeps } from "./types.ts";
|
|
15
15
|
import type { SubcallOpts } from "../../sandbox/interrupts.ts";
|
|
16
|
-
import {
|
|
17
|
-
import { complete1,
|
|
16
|
+
import { spawnAndRun, type SpawnDeps } from "./task-registry.ts";
|
|
17
|
+
import { complete1, completeDeps } from "./completion.ts";
|
|
18
18
|
import { emitting, summarizeLeaf, throttleHooks } from "./emitting.ts";
|
|
19
|
-
import { leafClaimKey, runClaimedLeaf } from "./llm-query.ts";
|
|
19
|
+
import { activeLedger, leafClaimKey, runClaimedLeaf, unwiredSpawn } from "./llm-query.ts";
|
|
20
20
|
|
|
21
|
-
const UNWIRED = formatError("RLM bridge not wired for this invocation");
|
|
22
21
|
const NO_UNMATCHED: readonly string[] = Object.freeze([]);
|
|
23
22
|
|
|
24
23
|
function emptyResult(answer: string): RlmResult {
|
|
@@ -56,25 +55,9 @@ function childContextFor(
|
|
|
56
55
|
});
|
|
57
56
|
}
|
|
58
57
|
|
|
59
|
-
function completeDeps(deps: SubcallHandlerDeps): Complete1Deps {
|
|
60
|
-
return {
|
|
61
|
-
leafGate: deps.gates.leaf,
|
|
62
|
-
registry: deps.registry,
|
|
63
|
-
getLlmModel: deps.getLlmModel,
|
|
64
|
-
getConfig: deps.getConfig,
|
|
65
|
-
signal: deps.signal,
|
|
66
|
-
onUsage: deps.onUsage,
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/** Ledger active for this call — undefined when disabled by config or not threaded in. */
|
|
71
|
-
function activeLedger(deps: SubcallHandlerDeps) {
|
|
72
|
-
return deps.getConfig().enableLedger ? deps.ledger : undefined;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
58
|
function claimKeyFor(deps: SubcallHandlerDeps, kind: "llm" | "rlm", prompt: string, paths: readonly string[], ctx: string): string {
|
|
76
59
|
const rootModel = deps.getModel?.();
|
|
77
|
-
const modelId = rootModel === undefined ? "" : (
|
|
60
|
+
const modelId = rootModel === undefined ? "" : modelLabelOf(rootModel);
|
|
78
61
|
return taskKey(kind, prompt, paths, modelId, ctx);
|
|
79
62
|
}
|
|
80
63
|
|
|
@@ -111,31 +94,8 @@ async function childRun(
|
|
|
111
94
|
? prompt
|
|
112
95
|
: `${prompt}\n\n[rlm] paths=${child.unmatched.join(", ")} matched no files; you received the full context.`;
|
|
113
96
|
|
|
114
|
-
// ── v5 memory replay: an identical, still-fresh child answer replays for zero API calls ──
|
|
115
|
-
const memory = deps.memory;
|
|
116
97
|
const sig = contextSig(child.context);
|
|
117
98
|
const key = claimKeyFor(deps, "rlm", prompt, paths ?? [], sig);
|
|
118
|
-
if (memory !== undefined && deps.getConfig().enableMemory !== false) {
|
|
119
|
-
const hit = memory.replay(key);
|
|
120
|
-
if (hit !== undefined) {
|
|
121
|
-
const replayId = inv.emitter.emitSubcallCreated({
|
|
122
|
-
kind: "rlm",
|
|
123
|
-
parentId: inv.parentId,
|
|
124
|
-
label: "rlm_query (replay)",
|
|
125
|
-
detail: prompt.slice(0, 60),
|
|
126
|
-
depth: childDepth,
|
|
127
|
-
});
|
|
128
|
-
inv.emitter.emitSubcallUpdated({ id: replayId, status: "done", resultPreview: hit.result.slice(0, 200) });
|
|
129
|
-
return {
|
|
130
|
-
answer: hit.result,
|
|
131
|
-
iterations: 0,
|
|
132
|
-
costUsd: 0,
|
|
133
|
-
inputTokens: 0,
|
|
134
|
-
outputTokens: 0,
|
|
135
|
-
durationMs: 0,
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
99
|
|
|
140
100
|
// ── v5 TaskLedger: echo → stub; duplicate → coalesce onto the existing runner ──────
|
|
141
101
|
const ledger = activeLedger(deps);
|
|
@@ -148,8 +108,7 @@ async function childRun(
|
|
|
148
108
|
// ONE subcall node per childRun (audit C2 / DRY #5): the decision branch reuses it, the
|
|
149
109
|
// run branch reports the engine's turns/cost on it. Never a second emit below.
|
|
150
110
|
const rootModel = deps.getModel?.();
|
|
151
|
-
const modelLabel =
|
|
152
|
-
rootModel === undefined ? undefined : (modelRef(rootModel) ?? rootModel.id);
|
|
111
|
+
const modelLabel = rootModel === undefined ? undefined : modelLabelOf(rootModel);
|
|
153
112
|
const subId = inv.emitter.emitSubcallCreated({
|
|
154
113
|
kind: "rlm",
|
|
155
114
|
parentId: inv.parentId,
|
|
@@ -168,13 +127,14 @@ async function childRun(
|
|
|
168
127
|
const twin = await ledger
|
|
169
128
|
.waitFor(decision.key)
|
|
170
129
|
.catch((err: unknown) => errorMessage(err));
|
|
171
|
-
inv.emitter.emitSubcallUpdated({ id: subId, status: "done", resultPreview: previewText(
|
|
172
|
-
return emptyResult(
|
|
130
|
+
inv.emitter.emitSubcallUpdated({ id: subId, status: "done", resultPreview: previewText(twin.slice(0, 80)) });
|
|
131
|
+
return emptyResult(twin);
|
|
173
132
|
}
|
|
174
133
|
if (ledger !== undefined && claimKey !== undefined) {
|
|
175
134
|
ledger.markRunning(claimKey);
|
|
176
135
|
}
|
|
177
136
|
|
|
137
|
+
const skillBlock = deps.getSkillBlock?.(prompt);
|
|
178
138
|
const input: RlmInput = {
|
|
179
139
|
rootPrompt,
|
|
180
140
|
context: child.context,
|
|
@@ -182,6 +142,9 @@ async function childRun(
|
|
|
182
142
|
parentNodeId: subId,
|
|
183
143
|
remainingTimeoutMs: remTimeout,
|
|
184
144
|
ledger, // DRY #6: the one seam — children share the parent's blackboard
|
|
145
|
+
// SKILL.state Ξ (Workstream C, DRY #6): the parent's block rides along — the only
|
|
146
|
+
// child-RlmInput construction site, so inheritance cannot grow a second path.
|
|
147
|
+
...(skillBlock === undefined ? {} : { skillBlock }),
|
|
185
148
|
};
|
|
186
149
|
|
|
187
150
|
try {
|
|
@@ -189,19 +152,6 @@ async function childRun(
|
|
|
189
152
|
inv.limits.addRaw(res.costUsd, res.inputTokens, res.outputTokens);
|
|
190
153
|
deps.onChildUsage?.(res.costUsd, res.inputTokens, res.outputTokens);
|
|
191
154
|
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
|
-
}
|
|
205
155
|
inv.emitter.emitSubcallUpdated({
|
|
206
156
|
id: subId,
|
|
207
157
|
status: "done",
|
|
@@ -225,17 +175,7 @@ export function createRlmQueryHandler(deps: SubcallHandlerDeps, sd: SpawnDeps) {
|
|
|
225
175
|
opts: SubcallOpts,
|
|
226
176
|
): Promise<SpawnResult> => {
|
|
227
177
|
const inv = deps.resolve(opts, depth);
|
|
228
|
-
if (inv === null)
|
|
229
|
-
return {
|
|
230
|
-
ok: false,
|
|
231
|
-
task_id: null,
|
|
232
|
-
kind: "rlm",
|
|
233
|
-
n: 1,
|
|
234
|
-
status: "pending",
|
|
235
|
-
hint: SPAWN_HINT,
|
|
236
|
-
error: UNWIRED,
|
|
237
|
-
};
|
|
238
|
-
}
|
|
178
|
+
if (inv === null) return unwiredSpawn("rlm", 1);
|
|
239
179
|
|
|
240
180
|
const pathArg = opts.paths;
|
|
241
181
|
|
|
@@ -297,17 +237,7 @@ export function createRlmBatchHandler(deps: SubcallHandlerDeps, sd: SpawnDeps) {
|
|
|
297
237
|
opts: SubcallOpts,
|
|
298
238
|
): Promise<SpawnResult> => {
|
|
299
239
|
const inv = deps.resolve(opts, depth);
|
|
300
|
-
if (inv === null)
|
|
301
|
-
return {
|
|
302
|
-
ok: false,
|
|
303
|
-
task_id: null,
|
|
304
|
-
kind: "rlm_batch",
|
|
305
|
-
n: tasks.length,
|
|
306
|
-
status: "pending",
|
|
307
|
-
hint: SPAWN_HINT,
|
|
308
|
-
error: UNWIRED,
|
|
309
|
-
};
|
|
310
|
-
}
|
|
240
|
+
if (inv === null) return unwiredSpawn("rlm_batch", tasks.length);
|
|
311
241
|
|
|
312
242
|
const pathArg = opts.paths;
|
|
313
243
|
// No wrapper "rlm_batch ×N" node: every task already gets its own rlm_query node from
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import type { AwaitResult, SpawnResult, TaskEntry } from "./types.ts";
|
|
10
|
+
import { formatError, isErrorText } from "../../util/errors.ts";
|
|
10
11
|
|
|
11
12
|
export const SPAWN_HINT =
|
|
12
13
|
"Call await_task(task_id=...) to get the result — this is NOT the answer.";
|
|
@@ -24,6 +25,22 @@ export interface AwaitDeps {
|
|
|
24
25
|
unawaitedIds(): readonly string[];
|
|
25
26
|
}
|
|
26
27
|
|
|
28
|
+
/** DRY: the ONE TaskEntry → AwaitResult mapping — shared by the await handler and the registry. */
|
|
29
|
+
export function entryToAwaitResult(entry: TaskEntry): AwaitResult {
|
|
30
|
+
const status = entry.status === "pending" ? "error" : entry.status;
|
|
31
|
+
return {
|
|
32
|
+
ok: entry.status === "done",
|
|
33
|
+
task_id: entry.taskId,
|
|
34
|
+
kind: entry.kind,
|
|
35
|
+
status,
|
|
36
|
+
result: entry.result,
|
|
37
|
+
results: entry.results,
|
|
38
|
+
error:
|
|
39
|
+
entry.error ??
|
|
40
|
+
(entry.status === "pending" ? "Task still pending" : undefined),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
27
44
|
export interface TaskRegistry {
|
|
28
45
|
readonly spawnDeps: SpawnDeps;
|
|
29
46
|
readonly awaitDeps: AwaitDeps;
|
|
@@ -32,8 +49,8 @@ export interface TaskRegistry {
|
|
|
32
49
|
}
|
|
33
50
|
|
|
34
51
|
interface Waiter {
|
|
35
|
-
resolve: (entry: TaskEntry) => void;
|
|
36
|
-
reject: (err: Error) => void;
|
|
52
|
+
readonly resolve: (entry: TaskEntry) => void;
|
|
53
|
+
readonly reject: (err: Error) => void;
|
|
37
54
|
timer?: ReturnType<typeof setTimeout>;
|
|
38
55
|
}
|
|
39
56
|
|
|
@@ -146,19 +163,7 @@ export function createTaskRegistry(): TaskRegistry {
|
|
|
146
163
|
error: `Task ${taskId} not found`,
|
|
147
164
|
};
|
|
148
165
|
}
|
|
149
|
-
|
|
150
|
-
entry.status === "pending" ? "error" : entry.status;
|
|
151
|
-
return {
|
|
152
|
-
ok: entry.status === "done",
|
|
153
|
-
task_id: entry.taskId,
|
|
154
|
-
kind: entry.kind,
|
|
155
|
-
status,
|
|
156
|
-
result: entry.result,
|
|
157
|
-
results: entry.results,
|
|
158
|
-
error:
|
|
159
|
-
entry.error ??
|
|
160
|
-
(entry.status === "pending" ? "Task still pending" : undefined),
|
|
161
|
-
};
|
|
166
|
+
return entryToAwaitResult(entry);
|
|
162
167
|
},
|
|
163
168
|
};
|
|
164
169
|
}
|
|
@@ -185,14 +190,14 @@ export function spawnAndRun(
|
|
|
185
190
|
sd.resolve(taskId, result);
|
|
186
191
|
} catch (err: unknown) {
|
|
187
192
|
const message = err instanceof Error ? err.message : String(err);
|
|
188
|
-
sd.reject(taskId, message
|
|
193
|
+
sd.reject(taskId, isErrorText(message) ? message : formatError(message));
|
|
189
194
|
}
|
|
190
195
|
};
|
|
191
196
|
|
|
192
197
|
if (trackDetached !== undefined && detached) {
|
|
193
198
|
void trackDetached(run).catch((err: unknown) => {
|
|
194
199
|
const message = err instanceof Error ? err.message : String(err);
|
|
195
|
-
sd.reject(taskId, message
|
|
200
|
+
sd.reject(taskId, isErrorText(message) ? message : formatError(message));
|
|
196
201
|
});
|
|
197
202
|
} else {
|
|
198
203
|
void run();
|
|
@@ -13,7 +13,6 @@ 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
15
|
import type { TaskLedger } from "../../core/ledger.ts";
|
|
16
|
-
import type { MemoryStore } from "../../core/memory.ts";
|
|
17
16
|
|
|
18
17
|
// ---------------------------------------------------------------------------
|
|
19
18
|
// Spawn / Await / Finish — the three shapes the model sees
|
|
@@ -73,7 +72,7 @@ export function limitsFromRemaining(
|
|
|
73
72
|
|
|
74
73
|
export interface Invocation {
|
|
75
74
|
readonly emitter: RlmEmitter;
|
|
76
|
-
readonly parentId
|
|
75
|
+
readonly parentId?: string;
|
|
77
76
|
readonly depth: number;
|
|
78
77
|
readonly limits: InvocationLimits;
|
|
79
78
|
}
|
|
@@ -91,8 +90,6 @@ export interface SubcallConfig {
|
|
|
91
90
|
readonly enableLedger?: boolean;
|
|
92
91
|
/** v5: real rlm spawns before demotion to llm (0 = never demote). */
|
|
93
92
|
readonly rlmBudget?: number;
|
|
94
|
-
/** v5 durable memory gates (optional; omitted → memory off). */
|
|
95
|
-
readonly enableMemory?: boolean;
|
|
96
93
|
/** v5.1 retry knobs — structural slice of RlmConfig so retryPolicy() can read them. */
|
|
97
94
|
readonly retryMaxAttempts?: number;
|
|
98
95
|
readonly rateLimitMaxAttempts?: number;
|
|
@@ -121,8 +118,13 @@ export interface SubcallHandlerDeps {
|
|
|
121
118
|
readonly trackDetached?: <T>(run: () => Promise<T>) => Promise<T>;
|
|
122
119
|
/** v5 TaskLedger blackboard shared across the whole run tree (claim/coalesce/echo/demote). */
|
|
123
120
|
readonly ledger?: TaskLedger;
|
|
124
|
-
/**
|
|
125
|
-
|
|
121
|
+
/** SKILL.state (Workstream D): ground a leaf prompt with verified facts. Applied ONCE in
|
|
122
|
+
* complete1 (DRY #1) so every leaf path inherits it; returns the prompt unchanged when
|
|
123
|
+
* nothing clears the score threshold — below it, byte-identical. */
|
|
124
|
+
readonly groundLeaf?: (prompt: string) => string;
|
|
125
|
+
/** SKILL.state (Workstream C): the parent run's Ξ block source, for childRun to copy
|
|
126
|
+
* into the child RlmInput (DRY #6 — one construction site). */
|
|
127
|
+
readonly getSkillBlock?: (task: string) => string | undefined;
|
|
126
128
|
}
|
|
127
129
|
|
|
128
130
|
// ---------------------------------------------------------------------------
|
package/src/bridge/model.ts
CHANGED
|
@@ -14,7 +14,7 @@ import { type Api, completeSimple, type Message, type Model, type ThinkingLevel,
|
|
|
14
14
|
import type { ModelRegistry } from "@earendil-works/pi-coding-agent";
|
|
15
15
|
import { completeWithRetry, DEFAULT_RETRY_POLICY, type RetryPolicy } from "../util/retry.ts";
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
type Role = "system" | "user" | "assistant";
|
|
18
18
|
export interface ChatMsg {
|
|
19
19
|
readonly role: Role;
|
|
20
20
|
readonly content: string;
|
|
@@ -55,17 +55,20 @@ function assistantMessage(text: string, model: Model<Api>): Message {
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
function toPiMessages(messages: readonly ChatMsg[], model: Model<Api>): { readonly systemPrompt?: string; readonly messages: Message[] } {
|
|
58
|
-
|
|
58
|
+
// System segments joined once below — no quadratic re-copy of the accumulated prompt (rule:
|
|
59
|
+
// never build large text with `+`/template concat inside a loop).
|
|
60
|
+
const systemParts: string[] = [];
|
|
59
61
|
const out: Message[] = [];
|
|
60
62
|
for (const m of messages) {
|
|
61
63
|
if (m.role === "system") {
|
|
62
|
-
|
|
64
|
+
systemParts.push(m.content);
|
|
63
65
|
} else if (m.role === "user") {
|
|
64
66
|
out.push({ role: "user", content: m.content, timestamp: Date.now() });
|
|
65
67
|
} else {
|
|
66
68
|
out.push(assistantMessage(m.content, model));
|
|
67
69
|
}
|
|
68
70
|
}
|
|
71
|
+
const systemPrompt = systemParts.length > 0 ? systemParts.join("\n\n") : undefined;
|
|
69
72
|
return { systemPrompt, messages: out };
|
|
70
73
|
}
|
|
71
74
|
|
package/src/commands/rlm-llm.ts
CHANGED
|
@@ -1,22 +1,13 @@
|
|
|
1
1
|
/** `/rlm-llm` — pin the leaf-LLM model (llm_query / llm_batch / map_files). */
|
|
2
2
|
|
|
3
|
-
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
4
3
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
5
4
|
import { modelRef } from "../config/settings.ts";
|
|
6
|
-
import { cheapestModel } from "../mode/llm-model.ts";
|
|
5
|
+
import { cheapestModel, sessionScopedModels } from "../mode/llm-model.ts";
|
|
7
6
|
import type { RlmController } from "../mode/rlm-mode.ts";
|
|
8
7
|
import { pickableModels, selectModel } from "../ui/model-picker.ts";
|
|
9
8
|
import { setRlmModeStatus } from "../ui/status.ts";
|
|
10
9
|
import { applyLlmSelection } from "./pins.ts";
|
|
11
10
|
|
|
12
|
-
/** Newer Pi hosts expose session-scoped models; 0.79 peers do not — duck-type safely. */
|
|
13
|
-
function sessionScopedModels(
|
|
14
|
-
ctx: ExtensionContext,
|
|
15
|
-
): readonly { readonly model: Model<Api> }[] | undefined {
|
|
16
|
-
const scoped: unknown = Reflect.get(ctx, "scopedModels");
|
|
17
|
-
return Array.isArray(scoped) ? scoped as readonly { readonly model: Model<Api> }[] : undefined;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
11
|
async function runRlmLlm(controller: RlmController, ctx: ExtensionContext): Promise<void> {
|
|
21
12
|
try {
|
|
22
13
|
await ctx.modelRegistry.refresh();
|
package/src/commands/rlm-rlm.ts
CHANGED
|
@@ -4,21 +4,14 @@
|
|
|
4
4
|
* the pre-pin behavior, now an explicit picker row.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
8
7
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
9
8
|
import { modelRef } from "../config/settings.ts";
|
|
9
|
+
import { sessionScopedModels } from "../mode/llm-model.ts";
|
|
10
10
|
import type { RlmController } from "../mode/rlm-mode.ts";
|
|
11
11
|
import { pickableModels, selectModel } from "../ui/model-picker.ts";
|
|
12
12
|
import { setRlmModeStatus } from "../ui/status.ts";
|
|
13
13
|
import { applyRlmSelection } from "./pins.ts";
|
|
14
14
|
|
|
15
|
-
function sessionScopedModels(
|
|
16
|
-
ctx: ExtensionContext,
|
|
17
|
-
): readonly { readonly model: Model<Api> }[] | undefined {
|
|
18
|
-
const scoped: unknown = Reflect.get(ctx, "scopedModels");
|
|
19
|
-
return Array.isArray(scoped) ? scoped as readonly { readonly model: Model<Api> }[] : undefined;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
15
|
async function runRlmRlm(controller: RlmController, ctx: ExtensionContext): Promise<void> {
|
|
23
16
|
try {
|
|
24
17
|
await ctx.modelRegistry.refresh();
|
package/src/config/defaults.ts
CHANGED
|
@@ -20,11 +20,12 @@ export const DEFAULT_CONFIG: Readonly<RlmConfig> = Object.freeze({
|
|
|
20
20
|
// of the context it inherited, where a leaf is one HTTP request. Worst case is
|
|
21
21
|
// (maxDepth - 1) × this many concurrent child engines. Default 4.
|
|
22
22
|
maxConcurrentChildren: 4,
|
|
23
|
-
// v5.1 rate-limit resilience (util/retry.ts):
|
|
24
|
-
// 2s→60s adaptive per-provider cooldown.
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
// v5.1 rate-limit resilience (util/retry.ts): 15 total attempts on the SAME model —
|
|
24
|
+
// 500ms→15s backoff, 2s→60s adaptive per-provider cooldown. DOCTRINE: NO fallback —
|
|
25
|
+
// when the attempts are exhausted the call fails loudly; model/provider never switch.
|
|
26
|
+
retryMaxAttempts: 15,
|
|
27
|
+
// 429s park on the cooldown instead of dying — up to 15 windows (2s→4s→…≤60s).
|
|
28
|
+
rateLimitMaxAttempts: 15,
|
|
28
29
|
retryBaseDelayMs: 500,
|
|
29
30
|
retryMaxDelayMs: 15_000,
|
|
30
31
|
throttleBaseMs: 2_000,
|
|
@@ -51,14 +52,29 @@ export const DEFAULT_CONFIG: Readonly<RlmConfig> = Object.freeze({
|
|
|
51
52
|
// v5 TaskLedger blackboard
|
|
52
53
|
enableLedger: true,
|
|
53
54
|
rlmBudget: 8,
|
|
54
|
-
// v5 durable memory
|
|
55
|
-
enableMemory: true,
|
|
56
|
-
injectNoteTokens: 2_000,
|
|
57
|
-
evolveEvery: 8,
|
|
58
|
-
memoryDir: null,
|
|
59
|
-
// v5 role separation: children delegate (llm + memory/ledger); "legacy" = full child surface.
|
|
60
|
-
childSurface: "delegation",
|
|
61
55
|
// Verification-discipline nudge — deliberately OFF (plan guardrail): when on, an early
|
|
62
56
|
// bare-number finalize gets one coached redo instead of being accepted. Opt-in via rlm.json.
|
|
63
57
|
enableVerificationNudge: false,
|
|
58
|
+
// SKILL.state integration: Σ_t execution state + cross-session distilled knowledge.
|
|
59
|
+
enableRunState: true,
|
|
60
|
+
runStateRetryMax: 2,
|
|
61
|
+
enableSkillState: true,
|
|
62
|
+
// Default ON (bench rec #3): deterministic harvest — one cheap distill leaf per finalize
|
|
63
|
+
// replaces the stochastic fence-emission harvest (0 vs 4 notes across identical ON arms).
|
|
64
|
+
enableSkillStateDistill: true,
|
|
65
|
+
skillStateMaxTokens: 1_200,
|
|
66
|
+
skillStateLeafTokens: 200,
|
|
67
|
+
skillStateMinScore: 4.0,
|
|
68
|
+
skillStateNotesPerProject: 128,
|
|
69
|
+
// Root Σ integration (WS-2..WS-4): digest compaction ON (it only swaps the summarizer for
|
|
70
|
+
// a deterministic digest — zero tokens, strictly less latency); the context transform and
|
|
71
|
+
// model-proposed fences soak with flags OFF until the A/B says otherwise.
|
|
72
|
+
enableRootDigestCompaction: true,
|
|
73
|
+
rootDigestKeepRecentChars: 12_000,
|
|
74
|
+
rootDigestMaxChars: 8_000,
|
|
75
|
+
enableRootContextTransform: false,
|
|
76
|
+
rootContextKeepTurns: 2,
|
|
77
|
+
rootContextElideChars: 1_500,
|
|
78
|
+
rootContextSnapshot: true,
|
|
79
|
+
enableRootStateFences: false,
|
|
64
80
|
});
|