@hicaru/pi-rlm 0.3.16 → 0.3.18
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 +32 -12
- package/src/config/settings.ts +53 -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 +184 -0
- package/src/core/root-digest.ts +213 -0
- package/src/core/root-state.ts +310 -0
- package/src/core/run-state.ts +587 -0
- package/src/core/types.ts +51 -12
- package/src/index.ts +220 -39
- 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 +46 -25
- package/src/prompts/native.ts +27 -5
- 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/config-panel.ts +12 -0
- 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/status.ts +39 -4
- 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
package/src/index.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { loadSettings, mergeConfig, resolveModelId } from "./config/settings.ts"
|
|
|
13
13
|
import { RlmController } from "./mode/rlm-mode.ts";
|
|
14
14
|
import { cheapestModel } from "./mode/llm-model.ts";
|
|
15
15
|
import { postRlmGuide } from "./ui/intro.ts";
|
|
16
|
-
import { setRlmModeStatus } from "./ui/status.ts";
|
|
16
|
+
import { setRlmModeStatus, type RootSigmaTelemetry } from "./ui/status.ts";
|
|
17
17
|
import { RunRegistry } from "./ui/panel/run-registry.ts";
|
|
18
18
|
import { installTreePanel } from "./ui/panel/tree-panel.ts";
|
|
19
19
|
import { markdownTheme } from "./ui/theme-adapter.ts";
|
|
@@ -21,15 +21,18 @@ import { SANDBOX_WATCHDOG_HEARTBEAT_MS } from "./sandbox/sandbox.ts";
|
|
|
21
21
|
import { SandboxManager } from "./sandbox/sandbox-manager.ts";
|
|
22
22
|
import { buildSessionGates, type SubcallGates } from "./util/concurrency.ts";
|
|
23
23
|
import { BackgroundTasks } from "./tool/background-tasks.ts";
|
|
24
|
-
import { MemoryStore } from "./core/memory.ts";
|
|
25
|
-
import { modelComplete } from "./bridge/model.ts";
|
|
26
|
-
import { retryPolicy } from "./util/retry.ts";
|
|
27
24
|
import { resolve } from "node:path";
|
|
28
25
|
import { resolveSource } from "./context/resolve.ts";
|
|
29
26
|
import { formatContextListing } from "./context/listing.ts";
|
|
30
27
|
import { extractEditPaths, readDiskFile } from "./context/refresh.ts";
|
|
31
28
|
import type { AddContextHandlerBundle } from "./bridge/add-context.ts";
|
|
32
29
|
import { buildNativeSystemPrompt } from "./prompts/native.ts";
|
|
30
|
+
import { SkillStore, notesFromRunState, xiQuery } from "./config/skillstate.ts";
|
|
31
|
+
import { buildRootDigestCompaction } from "./core/root-digest.ts";
|
|
32
|
+
import { RootStateTracker } from "./core/root-state.ts";
|
|
33
|
+
import { elideStalePayloads, spliceSigmaSnapshot } from "./core/root-context.ts";
|
|
34
|
+
import { agentMessageText, firstLine, textContentOf } from "./text/agent-text.ts";
|
|
35
|
+
import { findStatePatches } from "./text/parsing.ts";
|
|
33
36
|
import { capToolResultText } from "./mode/native-guards.ts";
|
|
34
37
|
import {
|
|
35
38
|
isSubagentChildBypass,
|
|
@@ -69,21 +72,10 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
69
72
|
|
|
70
73
|
// Init synchronously with defaults — ensures commands/tools/handlers register before session_start
|
|
71
74
|
const config = mergeConfig({});
|
|
72
|
-
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
|
|
76
|
-
// The consolidation LLM + real workspace root are attached in session_start (setLlm/setRoot).
|
|
77
|
-
const memory = new MemoryStore(
|
|
78
|
-
process.cwd(),
|
|
79
|
-
{
|
|
80
|
-
dir: config.memoryDir ?? undefined,
|
|
81
|
-
injectNoteTokens: config.injectNoteTokens,
|
|
82
|
-
evolveEvery: config.evolveEvery,
|
|
83
|
-
},
|
|
84
|
-
config.enableMemory,
|
|
85
|
-
);
|
|
86
|
-
const controller = new RlmController(config, memory);
|
|
75
|
+
const controller = new RlmController(config);
|
|
76
|
+
// Root Σ WS-4: engine-finalize Σ flows into the root tracker (assigned once; buildEngine
|
|
77
|
+
// reads it per run, so /rlm-config and session resets never stale this wire).
|
|
78
|
+
controller.onRunState = (state) => { rootTracker?.absorbEngineState(state); };
|
|
87
79
|
let onSandboxDiscardExtra: (() => void) | undefined;
|
|
88
80
|
const sandboxManager = new SandboxManager({
|
|
89
81
|
execTimeoutS: config.execTimeoutS,
|
|
@@ -116,6 +108,25 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
116
108
|
hideWhenEmpty: true,
|
|
117
109
|
});
|
|
118
110
|
let treePanelInstalled = false;
|
|
111
|
+
/** SKILL.state (Workstream B): session store — hydrated at session_start, flushed at shutdown. */
|
|
112
|
+
let skillStore: SkillStore | undefined;
|
|
113
|
+
/** Root Σ (WS-3/4): the native session's digest-level Σ_t — runtime-derived (tool outcomes,
|
|
114
|
+
* engine mirrors, prompts); lazily born on the first prompt, harvested + dropped at shutdown. */
|
|
115
|
+
let rootTracker: RootStateTracker | undefined;
|
|
116
|
+
// Root Σ WS-5.1 telemetry — journal counters (trace lines + status widget when tracing).
|
|
117
|
+
let xiCompositions = 0;
|
|
118
|
+
let rootDigests = 0;
|
|
119
|
+
let elidedMessages = 0;
|
|
120
|
+
let sigmaSplices = 0;
|
|
121
|
+
let idleDegrades = 0;
|
|
122
|
+
/** R6: Σ counter snapshot for the status line — a fresh readonly object per render. */
|
|
123
|
+
const sigmaTelemetry = (): RootSigmaTelemetry => ({
|
|
124
|
+
xiCompositions,
|
|
125
|
+
rootDigests,
|
|
126
|
+
elidedMessages,
|
|
127
|
+
sigmaSplices,
|
|
128
|
+
idleDegrades,
|
|
129
|
+
});
|
|
119
130
|
// A detached child works in its OWN sandbox, so this one sees no frames and its request
|
|
120
131
|
// watchdog would fire mid-await and SIGKILL a healthy worker, taking the REPL namespace
|
|
121
132
|
// with it. Keep it alive while detached work is genuinely in flight.
|
|
@@ -157,17 +168,26 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
157
168
|
await seedPromise;
|
|
158
169
|
};
|
|
159
170
|
|
|
171
|
+
/** Ξ (Workstream C): BM25 slice of the SkillState store for a query; undefined when off. */
|
|
172
|
+
const composeSkillBlock = (query: string): string | undefined => {
|
|
173
|
+
const cfg = controller.config;
|
|
174
|
+
// R0: enableSkillState is enforced (validateEnforcedOn) — no config check remains.
|
|
175
|
+
if (skillStore === undefined) return undefined;
|
|
176
|
+
const block = skillStore.blockFor(query, cfg.skillStateMaxTokens);
|
|
177
|
+
return block === "" ? undefined : block;
|
|
178
|
+
};
|
|
179
|
+
|
|
160
180
|
// ── Message renderers ──
|
|
161
181
|
// Markdown themes are derived from the injected `theme`, never pi's module-global
|
|
162
182
|
// `getMarkdownTheme()` — under jiti that global can be undefined inside a plugin.
|
|
163
183
|
pi.registerMessageRenderer("rlm-answer", (message, _options, theme) =>
|
|
164
|
-
new Markdown(
|
|
184
|
+
new Markdown(textContentOf(message.content), 1, 0, markdownTheme(theme)),
|
|
165
185
|
);
|
|
166
186
|
pi.registerMessageRenderer("rlm-question", (message, _options, theme) =>
|
|
167
|
-
new Markdown(`**RLM question**\n\n${
|
|
187
|
+
new Markdown(`**RLM question**\n\n${textContentOf(message.content)}`, 1, 0, markdownTheme(theme)),
|
|
168
188
|
);
|
|
169
189
|
pi.registerMessageRenderer("rlm-intro", (message, _options, theme) =>
|
|
170
|
-
new Markdown(
|
|
190
|
+
new Markdown(textContentOf(message.content), 1, 0, markdownTheme(theme)),
|
|
171
191
|
);
|
|
172
192
|
|
|
173
193
|
// ── CLI flag: `pi --rlm` / `pi --rlm=false` overrides the persisted mode for this run ──
|
|
@@ -194,11 +214,17 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
194
214
|
controller.savedLlmRef = persisted.llm ?? undefined;
|
|
195
215
|
controller.savedRlmRef = persisted.rlm ?? undefined;
|
|
196
216
|
|
|
217
|
+
// SKILL.state (Workstream B / R0): hydrate the cross-session note store UNCONDITIONALLY —
|
|
218
|
+
// the SkillStore is operating law; no rlm.json, command, or UI path can prevent its birth
|
|
219
|
+
// (hostile configs are traced + ignored at the validateEnforcedOn seam; fail-soft).
|
|
220
|
+
skillStore = await SkillStore.hydrate(controller.config.skillStateNotesPerProject);
|
|
221
|
+
controller.skillStore = skillStore;
|
|
222
|
+
|
|
197
223
|
// An explicit --rlm flag wins over the persisted setting for this session.
|
|
198
224
|
const flag = pi.getFlag("rlm");
|
|
199
225
|
if (typeof flag === "boolean") controller.setConfig(Object.freeze({ ...controller.config, enabled: flag }));
|
|
200
226
|
|
|
201
|
-
// Reload the catalog before the worker
|
|
227
|
+
// Reload the catalog before the worker pick below reads it. Newer pi builds make
|
|
202
228
|
// `getAvailable()` an async-populated snapshot that starts empty, and picking from an empty
|
|
203
229
|
// catalog silently falls back to the root model. Called with no arguments and awaited so it
|
|
204
230
|
// is valid whether `refresh` returns void (current) or a promise (newer); fail-soft, because
|
|
@@ -253,17 +279,6 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
253
279
|
const llmModel = controller.llmModel ?? cheapestModel(ctx.modelRegistry) ?? ctx.model;
|
|
254
280
|
const model = ctx.model;
|
|
255
281
|
if (llmModel && model) {
|
|
256
|
-
// Consolidation runs on the cheap worker model through the single completion entry point;
|
|
257
|
-
// the workspace root is only known once the session starts.
|
|
258
|
-
const consolidateModel = llmModel;
|
|
259
|
-
memory.setLlm((prompt) =>
|
|
260
|
-
modelComplete([{ role: "user", content: prompt }], {
|
|
261
|
-
model: consolidateModel,
|
|
262
|
-
registry: ctx.modelRegistry,
|
|
263
|
-
retry: retryPolicy(controller.config),
|
|
264
|
-
})
|
|
265
|
-
.then((r) => r.text));
|
|
266
|
-
memory.setRoot(ctx.cwd ?? process.cwd());
|
|
267
282
|
// v5 provider caps (audit C1/C6): ONE resolver shared by both composition roots — the
|
|
268
283
|
// repl() tool and RlmController.start admit through the same pool, each gate capped
|
|
269
284
|
// against the model that actually runs on it (leaves = worker, children = smart).
|
|
@@ -294,7 +309,9 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
294
309
|
try {
|
|
295
310
|
pi.registerTool(createReplTool({
|
|
296
311
|
sandboxManager,
|
|
312
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- pi-ai registry types Model<any>; runtime models conform to Model<Api>
|
|
297
313
|
model,
|
|
314
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- see above
|
|
298
315
|
llmModel,
|
|
299
316
|
getModel: () => controller.resolveModels(ctx)?.model,
|
|
300
317
|
getLlmModel: () => controller.resolveModels(ctx)?.llm,
|
|
@@ -304,7 +321,9 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
304
321
|
resolveGates: resolveSessionGates,
|
|
305
322
|
background,
|
|
306
323
|
runRegistry,
|
|
307
|
-
|
|
324
|
+
skillStore,
|
|
325
|
+
onRunState: (state) => { rootTracker?.absorbEngineState(state); },
|
|
326
|
+
getSkillBlock: composeSkillBlock,
|
|
308
327
|
registerDiscardHook: (reset) => { onSandboxDiscardExtra = reset; },
|
|
309
328
|
registerContextBundle: (bundle) => {
|
|
310
329
|
contextBundleRef = bundle;
|
|
@@ -327,7 +346,7 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
327
346
|
}
|
|
328
347
|
}
|
|
329
348
|
|
|
330
|
-
setRlmModeStatus(ctx, controller, ctx.getContextUsage());
|
|
349
|
+
setRlmModeStatus(ctx, controller, ctx.getContextUsage(), sigmaTelemetry());
|
|
331
350
|
if (!treePanelInstalled) {
|
|
332
351
|
treePanelInstalled = true;
|
|
333
352
|
installTreePanel(ctx, runRegistry);
|
|
@@ -340,7 +359,7 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
340
359
|
|
|
341
360
|
// ── Keep the footer's context reading live (RLM exists to shrink this number) ──
|
|
342
361
|
pi.on("turn_end", async (_event, ctx) => {
|
|
343
|
-
setRlmModeStatus(ctx, controller, ctx.getContextUsage());
|
|
362
|
+
setRlmModeStatus(ctx, controller, ctx.getContextUsage(), sigmaTelemetry());
|
|
344
363
|
});
|
|
345
364
|
|
|
346
365
|
/** True when the native-mode trade holds: enabled AND repl is in the active tool set. */
|
|
@@ -350,10 +369,116 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
350
369
|
activeToolNames: typeof pi.getActiveTools === "function" ? pi.getActiveTools() : undefined,
|
|
351
370
|
});
|
|
352
371
|
|
|
372
|
+
/** Root Σ WS-3 gate: config flag AND the bench A/B toggle (RLM_BENCH_NO_ROOTCONTEXT=1 skips). */
|
|
373
|
+
const rootContextActive = (): boolean =>
|
|
374
|
+
controller.config.enableRootContextTransform && process.env.RLM_BENCH_NO_ROOTCONTEXT !== "1";
|
|
375
|
+
|
|
353
376
|
// ── System prompt: native RLM mode addendum (only when the trade holds) ──
|
|
354
377
|
pi.on("before_agent_start", async (event) => {
|
|
355
378
|
if (!nativeTradeHolds()) return;
|
|
356
|
-
|
|
379
|
+
// Root Σ: lazy tracker birth on the session's first prompt (the task IS that prompt).
|
|
380
|
+
rootTracker ??= RootStateTracker.fresh(event.prompt, controller.config.runStateRetryMax);
|
|
381
|
+
// Ξ (Workstream C / Root Σ WS-1): the block is PER-PROMPT text — BM25 ranks the store
|
|
382
|
+
// against the live user prompt (mid-session harvests surface immediately), falling back
|
|
383
|
+
// to the static slice when the prompt is empty. Concatenated here, so NATIVE_PROMPT_STATIC
|
|
384
|
+
// (the module-load snapshot) and its budget math stay untouched.
|
|
385
|
+
const xi = composeSkillBlock(xiQuery(event.prompt, event.systemPrompt.slice(0, 2_000)));
|
|
386
|
+
if (xi !== undefined) {
|
|
387
|
+
xiCompositions += 1;
|
|
388
|
+
if (traceEnabled) trace("root-xi.compose", { total: xiCompositions, chars: xi.length });
|
|
389
|
+
}
|
|
390
|
+
const xiPart = xi === undefined ? "" : `${xi}\n\n`;
|
|
391
|
+
// Root Σ WS-4: the user's latest ask IS the next step by definition; feed the tracker
|
|
392
|
+
// before the turn so the Σ snapshot in this turn's context carries it.
|
|
393
|
+
rootTracker?.setNextStep(event.prompt);
|
|
394
|
+
// Root Σ WS-4.2 (enableRootStateFences): a rejected ΔΣ_t from the previous turn surfaces
|
|
395
|
+
// as an observation message — the same error-as-observation retry ladder engine runs use.
|
|
396
|
+
const observation = rootTracker?.takePendingObservation();
|
|
397
|
+
const message = observation === undefined
|
|
398
|
+
? undefined
|
|
399
|
+
: { customType: "rlm-sigma-observation", content: observation, display: false, details: undefined };
|
|
400
|
+
// R1 + R3 soak instrumentation: what the plugin RETURNS as the system prompt — the host
|
|
401
|
+
// applies result.systemPrompt verbatim (agent-session.js), so this line + the journal
|
|
402
|
+
// decide "wiring bug vs model non-compliance" for the fence contract.
|
|
403
|
+
const nativePrompt = buildNativeSystemPrompt({ stateFences: controller.config.enableRootStateFences });
|
|
404
|
+
if (traceEnabled) {
|
|
405
|
+
trace("root-prompt.composed", {
|
|
406
|
+
chars: nativePrompt.length,
|
|
407
|
+
fences: controller.config.enableRootStateFences,
|
|
408
|
+
contract: nativePrompt.includes("[state] Alongside"),
|
|
409
|
+
xi: xi !== undefined,
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
return {
|
|
413
|
+
...(message === undefined ? {} : { message }),
|
|
414
|
+
systemPrompt: event.systemPrompt + "\n\n" + xiPart + nativePrompt,
|
|
415
|
+
};
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
// Root Σ WS-4.2 + v2 R4: capture model-proposed ΔΣ_t fences from finalized assistant
|
|
419
|
+
// replies and run them through the ONE patch validator (run-state.ts applyPatch). EVERY
|
|
420
|
+
// assistant turn feeds the ladder — a fence-free turn grows the idle streak, and
|
|
421
|
+
// RUN_STATE_IDLE_DEGRADE_TURNS consecutive idle turns degrade the tracker (G6 parity).
|
|
422
|
+
pi.on("message_end", async (event) => {
|
|
423
|
+
const tracker = rootTracker;
|
|
424
|
+
if (tracker === undefined || !controller.config.enableRootStateFences) return;
|
|
425
|
+
if (event.message.role !== "assistant") return;
|
|
426
|
+
const wasActive = tracker.isActive;
|
|
427
|
+
const outcome = tracker.applyFences(findStatePatches(agentMessageText(event.message)));
|
|
428
|
+
// R3 soak observability: per-turn fence outcomes — the soak-B bars (≥50% of turns commit
|
|
429
|
+
// ≥1 accepted delta, rejection storms <10%) are computed from these journal lines.
|
|
430
|
+
if (traceEnabled) {
|
|
431
|
+
trace("root-state.turn", {
|
|
432
|
+
...outcome,
|
|
433
|
+
idle: tracker.idleTurns,
|
|
434
|
+
active: tracker.isActive,
|
|
435
|
+
degraded: wasActive && !tracker.isActive,
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
if (wasActive && !tracker.isActive) {
|
|
439
|
+
idleDegrades += 1;
|
|
440
|
+
if (traceEnabled) {
|
|
441
|
+
const reason = tracker.degradeReason ?? "unknown";
|
|
442
|
+
trace(reason.startsWith("idle") ? "root-state.idle-degrade" : "root-state.degrade", {
|
|
443
|
+
idleTurns: tracker.idleTurns,
|
|
444
|
+
reason,
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
// ── Root Σ WS-2: deterministic root compaction (no summary LLM call) ──
|
|
451
|
+
pi.on("session_before_compact", async (event) => {
|
|
452
|
+
if (!controller.config.enableRootDigestCompaction) return undefined;
|
|
453
|
+
if (!nativeTradeHolds() && !controller.enabled) return undefined;
|
|
454
|
+
try {
|
|
455
|
+
const result = buildRootDigestCompaction({
|
|
456
|
+
preparation: event.preparation,
|
|
457
|
+
branchEntries: event.branchEntries,
|
|
458
|
+
config: controller.config,
|
|
459
|
+
store: skillStore,
|
|
460
|
+
});
|
|
461
|
+
if (result !== undefined) {
|
|
462
|
+
rootDigests += 1;
|
|
463
|
+
if (traceEnabled) {
|
|
464
|
+
// V1 soak probe: host-consumed tokensBefore vs our recomputation over the same span.
|
|
465
|
+
trace("root-digest.built", {
|
|
466
|
+
count: rootDigests,
|
|
467
|
+
chars: result.compaction.summary.length,
|
|
468
|
+
tokensBefore: result.compaction.tokensBefore,
|
|
469
|
+
tokensBeforeRecomputed: result.tokensBeforeRecomputed,
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
// SessionBeforeCompactResult SHAPE: the host consumes `result.compaction` — return the
|
|
474
|
+
// WRAPPER, never the bare CompactionResult (a bare one reads as `compaction: undefined`
|
|
475
|
+
// host-side and silently falls back to the LLM summarizer).
|
|
476
|
+
return result === undefined ? undefined : { compaction: result.compaction };
|
|
477
|
+
} catch (err) {
|
|
478
|
+
// Fail-soft by contract (packages/agent/src/types.ts:191): Pi falls back to its summarizer.
|
|
479
|
+
if (traceEnabled) trace("root-digest.fail", { error: errorMessage(err) });
|
|
480
|
+
return undefined;
|
|
481
|
+
}
|
|
357
482
|
});
|
|
358
483
|
|
|
359
484
|
// ── Context injection: listing of whatever is currently loaded ──
|
|
@@ -367,6 +492,35 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
367
492
|
);
|
|
368
493
|
if (!nativeTradeHolds()) return { messages: filtered };
|
|
369
494
|
|
|
495
|
+
// Root Σ WS-3: per-call A_t on the clone Pi hands us (runner.ts structuredClone — the last
|
|
496
|
+
// returned array wins; the disk transcript is untouched). Elide stale payloads (§5.3),
|
|
497
|
+
// then splice exactly one fresh Σ snapshot. Fail-soft: a throw here must never break a turn.
|
|
498
|
+
if (rootContextActive()) {
|
|
499
|
+
try {
|
|
500
|
+
const elided = elideStalePayloads(filtered, {
|
|
501
|
+
keepTurns: controller.config.rootContextKeepTurns,
|
|
502
|
+
elideChars: controller.config.rootContextElideChars,
|
|
503
|
+
});
|
|
504
|
+
elidedMessages += elided;
|
|
505
|
+
const tracker = rootTracker;
|
|
506
|
+
// R4: a DEGRADED tracker stops splicing Σ (isActive gate) — pure input tax otherwise.
|
|
507
|
+
if (
|
|
508
|
+
controller.config.rootContextSnapshot && tracker !== undefined &&
|
|
509
|
+
tracker.isActive && !tracker.isEmpty
|
|
510
|
+
) {
|
|
511
|
+
spliceSigmaSnapshot(filtered, tracker.snapshot(), tracker.rectifyHint(), {
|
|
512
|
+
withContract: controller.config.enableRootStateFences,
|
|
513
|
+
});
|
|
514
|
+
sigmaSplices += 1;
|
|
515
|
+
}
|
|
516
|
+
if (traceEnabled && (elided > 0 || sigmaSplices > 0)) {
|
|
517
|
+
trace("root-context.transform", { elided, total: elidedMessages, splices: sigmaSplices });
|
|
518
|
+
}
|
|
519
|
+
} catch (err) {
|
|
520
|
+
if (traceEnabled) trace("root-context.fail", { error: errorMessage(err) });
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
370
524
|
type PiMessage = (typeof filtered)[number];
|
|
371
525
|
|
|
372
526
|
const payload = sandboxManager.contextPayload;
|
|
@@ -398,17 +552,29 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
398
552
|
const MUTATING_FILE_TOOLS = Object.freeze(new Set(["edit", "write"]));
|
|
399
553
|
|
|
400
554
|
pi.on("tool_result", async (event, ctx) => {
|
|
555
|
+
// Root Σ WS-4: the tracker learns the session's shape from outcomes — deterministic, zero
|
|
556
|
+
// model cooperation; fresh results always outrank Σ (paper §5.3 observation override).
|
|
557
|
+
const tracker = rootTracker;
|
|
558
|
+
if (tracker !== undefined) {
|
|
559
|
+
tracker.observeToolResult(
|
|
560
|
+
event.toolName,
|
|
561
|
+
event.isError,
|
|
562
|
+
event.isError ? firstLine(textContentOf(event.content)) : "",
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
|
|
401
566
|
// ── Keep RLM context fresh after native file mutations ──
|
|
402
567
|
if (
|
|
403
568
|
nativeTradeHolds()
|
|
404
569
|
&& MUTATING_FILE_TOOLS.has(event.toolName)
|
|
405
|
-
&& event.isError
|
|
570
|
+
&& !event.isError
|
|
406
571
|
) {
|
|
407
572
|
const cwd = resolve(ctx?.cwd ?? process.cwd());
|
|
408
573
|
const paths = extractEditPaths(event.input);
|
|
409
574
|
for (const p of paths) {
|
|
410
575
|
const body = await readDiskFile(p, cwd);
|
|
411
576
|
if (body === null) continue;
|
|
577
|
+
rootTracker?.noteFact(`edited ${p}`); // WS-4: the tracker learns the session's file shape
|
|
412
578
|
try {
|
|
413
579
|
await sandboxManager.refreshFileFromDisk(p, body, cwd);
|
|
414
580
|
// Listing must re-inject if we rewrote payload identity
|
|
@@ -435,6 +601,19 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
435
601
|
|
|
436
602
|
// ── Session shutdown: cleanup ──
|
|
437
603
|
pi.on("session_shutdown", async () => {
|
|
604
|
+
// Root Σ WS-4 harvest symmetry: the root tracker teaches the store exactly like engine
|
|
605
|
+
// runs — the ONE notesFromRunState path — then the existing flush persists everything.
|
|
606
|
+
// R0: enableSkillState is enforced; no config check remains on this path.
|
|
607
|
+
const tracker = rootTracker;
|
|
608
|
+
if (skillStore !== undefined && tracker !== undefined && tracker.dirty) {
|
|
609
|
+
try {
|
|
610
|
+
skillStore.merge(notesFromRunState(tracker.snapshot()));
|
|
611
|
+
if (traceEnabled) trace("root-harvest.merged", { notes: tracker.snapshot().verifiedFacts.length });
|
|
612
|
+
} catch (err) {
|
|
613
|
+
if (traceEnabled) trace("root-harvest.fail", { error: errorMessage(err) });
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
await skillStore?.flush(); // SKILL.state (Workstream B): persist distilled notes
|
|
438
617
|
controller.abort();
|
|
439
618
|
clearInterval(watchdogHeartbeat);
|
|
440
619
|
background.dispose();
|
|
@@ -444,6 +623,8 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
444
623
|
contextBundleRef = undefined;
|
|
445
624
|
listingPayloadRef = undefined;
|
|
446
625
|
listingInjected = false;
|
|
626
|
+
skillStore = undefined;
|
|
627
|
+
rootTracker = undefined;
|
|
447
628
|
sandboxManager.contextPayload = [];
|
|
448
629
|
});
|
|
449
630
|
}
|
package/src/mode/llm-model.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
12
|
-
import type { ModelRegistry } from "@earendil-works/pi-coding-agent";
|
|
12
|
+
import type { ExtensionContext, ModelRegistry } from "@earendil-works/pi-coding-agent";
|
|
13
13
|
|
|
14
14
|
/** $/Mtok, input-weighted 3:1 — a sub-call sends a file body and gets back a sentence. */
|
|
15
15
|
function priceOf(model: Model<Api>): number {
|
|
@@ -48,7 +48,19 @@ export function cheapestModel(registry: ModelRegistry): Model<Api> | undefined {
|
|
|
48
48
|
for (let i = 0; i < models.length; i++) {
|
|
49
49
|
const model = models[i];
|
|
50
50
|
if (model === undefined) continue;
|
|
51
|
+
// Sentinel negative cost (openrouter/auto reports −1e6) = unpriceable router. priceOf()
|
|
52
|
+
// would crown it "cheapest" (−4M) — skip; fail closed on unknown prices.
|
|
53
|
+
if ((model.cost?.input ?? 0) < 0 || (model.cost?.output ?? 0) < 0) continue;
|
|
51
54
|
if (best === undefined || compareLlm(model, best) < 0) best = model;
|
|
52
55
|
}
|
|
53
56
|
return best;
|
|
54
57
|
}
|
|
58
|
+
|
|
59
|
+
/** Newer Pi hosts expose session-scoped models; 0.79 peers do not — duck-type safely.
|
|
60
|
+
* DRY: the ONE reflection probe — shared by /rlm-llm and /rlm-rlm. */
|
|
61
|
+
export function sessionScopedModels(
|
|
62
|
+
ctx: ExtensionContext,
|
|
63
|
+
): readonly { readonly model: Model<Api> }[] | undefined {
|
|
64
|
+
const scoped: unknown = Reflect.get(ctx, "scopedModels");
|
|
65
|
+
return Array.isArray(scoped) ? scoped as readonly { readonly model: Model<Api> }[] : undefined;
|
|
66
|
+
}
|
|
@@ -40,12 +40,6 @@ export function isFileReadingCommand(command: string): boolean {
|
|
|
40
40
|
return statementHeads(command).some((h) => READER_COMMANDS.has(h));
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
export const BASH_BLOCK_REASON =
|
|
44
|
-
"RLM mode: reading files via bash is blocked — that dumps file content into the root model's " +
|
|
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 / llm_batch / " +
|
|
47
|
-
"llm_query_chunked. bash is for RUNNING things (tests, builds, git).";
|
|
48
|
-
|
|
49
43
|
/** Max chars of tool output forwarded to the root model (≈1K tokens). */
|
|
50
44
|
export const TOOL_RESULT_CAP = 4_000;
|
|
51
45
|
|
package/src/mode/rlm-mode.ts
CHANGED
|
@@ -16,11 +16,12 @@ import { resolveSource } from "../context/resolve.ts";
|
|
|
16
16
|
import { RlmEmitter } from "../tool/rlm-events.ts";
|
|
17
17
|
import { formatError } from "../util/errors.ts";
|
|
18
18
|
import { cheapestModel } from "./llm-model.ts";
|
|
19
|
-
import type {
|
|
19
|
+
import type { SkillStore } from "../config/skillstate.ts";
|
|
20
|
+
import type { RunState } from "../core/run-state.ts";
|
|
20
21
|
import type { RunRlm } from "../core/types.ts";
|
|
21
22
|
import type { SubcallGates } from "../util/concurrency.ts";
|
|
22
23
|
|
|
23
|
-
|
|
24
|
+
interface RunHandle {
|
|
24
25
|
readonly abort: () => void;
|
|
25
26
|
readonly done: Promise<RlmResult>;
|
|
26
27
|
}
|
|
@@ -28,16 +29,23 @@ export interface RunHandle {
|
|
|
28
29
|
export interface StartInput {
|
|
29
30
|
readonly rootPrompt: string;
|
|
30
31
|
readonly context: unknown;
|
|
32
|
+
/** History-as-deliverable opt-out (§12.1): RunState stays off — the archive is the product. */
|
|
33
|
+
readonly narrative?: boolean;
|
|
31
34
|
}
|
|
32
35
|
|
|
33
36
|
export class RlmController {
|
|
34
37
|
llmModel: Model<Api> | undefined;
|
|
35
|
-
savedLlmRef
|
|
38
|
+
savedLlmRef?: string;
|
|
36
39
|
/** Set by applyLlmSelection when the user explicitly picks "cheapest (auto)". */
|
|
37
40
|
explicitClearPin = false;
|
|
38
41
|
/** Pinned rlm root/worker model — when unset, child engines follow pi's session model. */
|
|
39
42
|
rlmModel: Model<Api> | undefined;
|
|
40
|
-
savedRlmRef
|
|
43
|
+
savedRlmRef?: string;
|
|
44
|
+
/** SKILL.state (Workstream B): the session store — hydrated by index.ts at session_start.
|
|
45
|
+
* Undefined ⇒ no Ξ composition, no harvest: the headless path runs exactly as built. */
|
|
46
|
+
skillStore: SkillStore | undefined;
|
|
47
|
+
/** Root Σ (WS-4): set by the extension — engine-finalize Σ flows to the root tracker. */
|
|
48
|
+
onRunState: ((state: RunState) => void) | undefined;
|
|
41
49
|
/** Set by applyRlmSelection when the user explicitly picks "(follow session model)". */
|
|
42
50
|
explicitClearRlmPin = false;
|
|
43
51
|
private active: AbortController | null = null;
|
|
@@ -45,11 +53,7 @@ export class RlmController {
|
|
|
45
53
|
* session_start so BOTH composition roots admit through one pool (audit C1). */
|
|
46
54
|
private sessionGates: (() => SubcallGates) | undefined;
|
|
47
55
|
|
|
48
|
-
constructor(
|
|
49
|
-
public config: RlmConfig,
|
|
50
|
-
/** v5 durable memory — shared with the repl tool so child runs replay/persist too. */
|
|
51
|
-
public readonly memory?: MemoryStore,
|
|
52
|
-
) {}
|
|
56
|
+
constructor(public config: RlmConfig) {}
|
|
53
57
|
|
|
54
58
|
setSessionGates(getGates: () => SubcallGates): void {
|
|
55
59
|
this.sessionGates = getGates;
|
|
@@ -101,9 +105,12 @@ export class RlmController {
|
|
|
101
105
|
if (!this.llmModel && this.savedLlmRef) this.llmModel = resolveModelId(ctx.modelRegistry, this.savedLlmRef);
|
|
102
106
|
if (!this.rlmModel && this.savedRlmRef) this.rlmModel = resolveModelId(ctx.modelRegistry, this.savedRlmRef);
|
|
103
107
|
// The rlm pin wins over the session model; unset → follow pi's active model.
|
|
108
|
+
// pi's ctx.model is typed Model<any>; runtime models conform to Model<Api> — the two
|
|
109
|
+
// directives below mark exactly where that external-boundary any enters and leaves.
|
|
104
110
|
const model = this.rlmModel ?? ctx.model ?? cheapestModel(ctx.modelRegistry);
|
|
105
111
|
if (!model) return undefined;
|
|
106
112
|
const llm = this.llmModel ?? cheapestModel(ctx.modelRegistry) ?? model;
|
|
113
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
107
114
|
return { model, llm };
|
|
108
115
|
}
|
|
109
116
|
|
|
@@ -112,6 +119,14 @@ export class RlmController {
|
|
|
112
119
|
return createEngine(deps);
|
|
113
120
|
}
|
|
114
121
|
|
|
122
|
+
/** Ξ (Workstream C): BM25 slice of the session SkillState for a root prompt; undefined when
|
|
123
|
+
* the store is absent/disabled or nothing is relevant. */
|
|
124
|
+
private skillBlockFor(query: string): string | undefined {
|
|
125
|
+
if (this.skillStore === undefined || !this.config.enableSkillState) return undefined;
|
|
126
|
+
const block = this.skillStore.blockFor(query, this.config.skillStateMaxTokens);
|
|
127
|
+
return block === "" ? undefined : block;
|
|
128
|
+
}
|
|
129
|
+
|
|
115
130
|
/** The ONE engine construction path for this controller (DRY #6 — a second path that
|
|
116
131
|
* forgets to grow is exactly how issue #4 and audit C1 happened). Protected so tests can
|
|
117
132
|
* subclass and assert the wiring without touching the network. */
|
|
@@ -129,8 +144,9 @@ export class RlmController {
|
|
|
129
144
|
signal: args.signal,
|
|
130
145
|
emitter: args.emitter,
|
|
131
146
|
limits: limitsFromConfig(this.config),
|
|
132
|
-
memory: this.memory,
|
|
133
147
|
gates: this.sessionGates?.(),
|
|
148
|
+
skillStore: this.skillStore,
|
|
149
|
+
onRunState: this.onRunState,
|
|
134
150
|
});
|
|
135
151
|
}
|
|
136
152
|
|
|
@@ -159,7 +175,14 @@ export class RlmController {
|
|
|
159
175
|
signal: abortController.signal,
|
|
160
176
|
emitter: emitter ?? new RlmEmitter(),
|
|
161
177
|
});
|
|
162
|
-
|
|
178
|
+
const skillBlock = this.skillBlockFor(input.rootPrompt);
|
|
179
|
+
return await engine({
|
|
180
|
+
rootPrompt: input.rootPrompt,
|
|
181
|
+
context: contextValue,
|
|
182
|
+
depth: 0,
|
|
183
|
+
...(skillBlock === undefined ? {} : { skillBlock }), // Ξ (Workstream C)
|
|
184
|
+
...(input.narrative === undefined ? {} : { narrative: input.narrative }),
|
|
185
|
+
});
|
|
163
186
|
})().finally(() => {
|
|
164
187
|
if (this.active === abortController) this.active = null;
|
|
165
188
|
});
|
package/src/mode/subagent.ts
CHANGED
|
@@ -11,9 +11,9 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { DEFAULT_CONFIG } from "../config/defaults.ts";
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
const SUBAGENT_CHILD_ENV = "PI_SUBAGENT_CHILD";
|
|
15
|
+
const RLM_FORCE_IN_SUBAGENT_ENV = "PI_RLM_FORCE_IN_SUBAGENT";
|
|
16
|
+
const RLM_DEPTH_ENV = "PI_RLM_DEPTH";
|
|
17
17
|
|
|
18
18
|
/** Cross-process depth from env. Missing / invalid → 0. */
|
|
19
19
|
export function processRlmDepth(): number {
|
|
@@ -48,7 +48,7 @@ export function commitSubagentForceActivation(maxDepth: number = DEFAULT_CONFIG.
|
|
|
48
48
|
if (process.env[RLM_FORCE_IN_SUBAGENT_ENV] !== "1") return;
|
|
49
49
|
if (processRlmDepth() >= maxDepth) return;
|
|
50
50
|
const next = processRlmDepth() + 1;
|
|
51
|
-
|
|
51
|
+
Reflect.deleteProperty(process.env, RLM_FORCE_IN_SUBAGENT_ENV);
|
|
52
52
|
process.env[RLM_DEPTH_ENV] = String(next);
|
|
53
53
|
}
|
|
54
54
|
|
|
@@ -59,7 +59,7 @@ export function commitSubagentForceActivation(maxDepth: number = DEFAULT_CONFIG.
|
|
|
59
59
|
*/
|
|
60
60
|
export function shouldEnforceNativeReaderBlock(opts: {
|
|
61
61
|
readonly enabled: boolean;
|
|
62
|
-
readonly activeToolNames
|
|
62
|
+
readonly activeToolNames?: readonly string[];
|
|
63
63
|
}): boolean {
|
|
64
64
|
if (!opts.enabled) return false;
|
|
65
65
|
const names = opts.activeToolNames;
|