@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/sandbox/sandbox.ts
CHANGED
|
@@ -27,6 +27,10 @@ import { trace, traceEnabled } from "../util/trace.ts";
|
|
|
27
27
|
|
|
28
28
|
export type { AddContextResult, SubcallOpts, SubLlmHandlers } from "./interrupts.ts";
|
|
29
29
|
|
|
30
|
+
/** Event-loop guard: frames are model/summary-sized by construction; payloads travel via temp
|
|
31
|
+
* files, never the wire. Anything near this cap is a runaway producer — drop it. */
|
|
32
|
+
const MAX_FRAME_CHARS = 8_000_000;
|
|
33
|
+
|
|
30
34
|
export interface SandboxOptions {
|
|
31
35
|
/** Sandbox recursion depth label (passed to the worker, used in interrupt routing). */
|
|
32
36
|
readonly depth?: number;
|
|
@@ -139,7 +143,7 @@ export class PythonSandbox {
|
|
|
139
143
|
// windowsHide: without it each sandbox flashes a console window on Windows (pi sets
|
|
140
144
|
// this on every spawn — bash.ts / shell.ts). Same Windows surface as issue #7.
|
|
141
145
|
{ stdio: ["pipe", "pipe", "pipe"], env: sanitizedEnv(), windowsHide: true },
|
|
142
|
-
)
|
|
146
|
+
);
|
|
143
147
|
|
|
144
148
|
this.proc.stdout.setEncoding("utf8");
|
|
145
149
|
this.proc.stdout.on("data", (chunk: string) => this.onData(chunk));
|
|
@@ -262,7 +266,8 @@ export class PythonSandbox {
|
|
|
262
266
|
this.pending.set("_init", {
|
|
263
267
|
resolve: (res) => {
|
|
264
268
|
clearTimeout(timer);
|
|
265
|
-
res.ok
|
|
269
|
+
if (res.ok) resolve();
|
|
270
|
+
else reject(new Error(res.error ?? "worker init failed"));
|
|
266
271
|
},
|
|
267
272
|
reject,
|
|
268
273
|
timer,
|
|
@@ -299,7 +304,7 @@ export class PythonSandbox {
|
|
|
299
304
|
settle(value);
|
|
300
305
|
};
|
|
301
306
|
this.pending.set(id, { resolve: once(resolve), reject: once(reject), timer, requestType: payload.type });
|
|
302
|
-
this.send({ id, ...payload }
|
|
307
|
+
this.send({ id, ...payload });
|
|
303
308
|
});
|
|
304
309
|
}
|
|
305
310
|
|
|
@@ -356,7 +361,12 @@ export class PythonSandbox {
|
|
|
356
361
|
this.appendStderr(`[rlm] dropped '${msg.type}' frame: worker ${this.exitDescription()}\n`);
|
|
357
362
|
return;
|
|
358
363
|
}
|
|
359
|
-
|
|
364
|
+
const frame = JSON.stringify(msg);
|
|
365
|
+
if (frame.length > MAX_FRAME_CHARS) {
|
|
366
|
+
this.appendStderr(`[rlm] dropped '${msg.type}' frame: ${frame.length} chars exceed the frame cap\n`);
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
this.proc.stdin.write(`${frame}\n`);
|
|
360
370
|
}
|
|
361
371
|
|
|
362
372
|
/**
|
|
@@ -391,6 +401,13 @@ export class PythonSandbox {
|
|
|
391
401
|
|
|
392
402
|
private onData(chunk: string): void {
|
|
393
403
|
this.buf += chunk;
|
|
404
|
+
// Untrusted-stream guard: a worker that stops emitting newlines would otherwise balloon
|
|
405
|
+
// this buffer without bound and stall the pump. Reset (both cursors) and keep draining.
|
|
406
|
+
if (this.buf.length > MAX_FRAME_CHARS) {
|
|
407
|
+
this.appendStderr(`\n[protocol] stdout buffer exceeded ${MAX_FRAME_CHARS} chars without a newline — truncated\n`);
|
|
408
|
+
this.buf = "";
|
|
409
|
+
this.scanOffset = 0;
|
|
410
|
+
}
|
|
394
411
|
let nl: number;
|
|
395
412
|
while ((nl = this.buf.indexOf("\n", this.scanOffset)) >= 0) {
|
|
396
413
|
const line = this.buf.slice(this.scanOffset, nl).trim();
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text extraction from host AgentMessages (coding-agent `Message | CustomMessage | …` union).
|
|
3
|
+
*
|
|
4
|
+
* The root-Σ modules (core/root-digest.ts, core/root-context.ts) and the index.ts event
|
|
5
|
+
* handlers all need the same "text payload of a message" projection. One implementation
|
|
6
|
+
* here; callers never re-walk content blocks. Everything takes `unknown` and narrows via
|
|
7
|
+
* guards — host shapes evolve, this module is the single adaptation seam.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** Text payload of one content block list / string: string as-is, `{text}` blocks joined. */
|
|
11
|
+
export function textContentOf(content: unknown): string {
|
|
12
|
+
if (typeof content === "string") return content;
|
|
13
|
+
if (!Array.isArray(content)) return "";
|
|
14
|
+
const parts = new Array<string>(content.length);
|
|
15
|
+
let n = 0;
|
|
16
|
+
for (const block of content) {
|
|
17
|
+
if (isTextBlock(block)) parts[n++] = block.text;
|
|
18
|
+
}
|
|
19
|
+
parts.length = n;
|
|
20
|
+
return parts.join("");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function isTextBlock(block: unknown): block is { readonly type: "text"; readonly text: string } {
|
|
24
|
+
return (
|
|
25
|
+
typeof block === "object" && block !== null &&
|
|
26
|
+
(block as Record<string, unknown>).type === "text" &&
|
|
27
|
+
typeof (block as Record<string, unknown>).text === "string"
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Text payload of a whole AgentMessage: role-disciplined extraction. `thinking` blocks and
|
|
33
|
+
* tool-call payloads are deliberately skipped (digests/elision want observable prose and
|
|
34
|
+
* results, never the model's private reasoning). Unknown shapes yield "".
|
|
35
|
+
*/
|
|
36
|
+
export function agentMessageText(message: unknown): string {
|
|
37
|
+
if (typeof message !== "object" || message === null) return "";
|
|
38
|
+
const m = message as Record<string, unknown>;
|
|
39
|
+
switch (m.role) {
|
|
40
|
+
case "user":
|
|
41
|
+
case "assistant":
|
|
42
|
+
case "custom":
|
|
43
|
+
case "toolResult":
|
|
44
|
+
return textContentOf(m.content);
|
|
45
|
+
case "compactionSummary":
|
|
46
|
+
case "branchSummary":
|
|
47
|
+
return typeof m.summary === "string" ? m.summary : "";
|
|
48
|
+
default:
|
|
49
|
+
return "";
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** First non-blank line, trimmed and capped — the error-reason shape trackers store. */
|
|
54
|
+
export function firstLine(text: string, maxChars = 200): string {
|
|
55
|
+
const line = text.split("\n", 1)[0] ?? "";
|
|
56
|
+
const trimmed = line.trim();
|
|
57
|
+
return trimmed.length > maxChars ? trimmed.slice(0, maxChars) : trimmed;
|
|
58
|
+
}
|
package/src/text/parsing.ts
CHANGED
|
@@ -15,6 +15,8 @@ const FENCE = /(`{3,})[ \t]*repl[ \t]*\r?\n([\s\S]*?)\1/g;
|
|
|
15
15
|
const FALLBACK_FENCE = /(`{3,})[ \t]*([^`\r\n]*)[ \t]*\r?\n([\s\S]*?)\1/g;
|
|
16
16
|
const PYTHON_TAG = /^py(thon)?$/i;
|
|
17
17
|
|
|
18
|
+
import { errorMessage } from "../util/errors.ts";
|
|
19
|
+
|
|
18
20
|
/** Shared fence scan: run `re` over `text`, keep bodies the selector accepts (same trimming). */
|
|
19
21
|
function collectFences(text: string, re: RegExp, select: (m: RegExpExecArray) => string | null): string[] {
|
|
20
22
|
const blocks: string[] = [];
|
|
@@ -40,11 +42,41 @@ export function findReplBlocks(text: string): string[] {
|
|
|
40
42
|
});
|
|
41
43
|
}
|
|
42
44
|
|
|
43
|
-
/**
|
|
44
|
-
export
|
|
45
|
+
/** One ```state fence: parsed JSON payload, or the parse error (error-as-observation). */
|
|
46
|
+
export type StateFenceResult =
|
|
47
|
+
| { readonly ok: true; readonly value: unknown }
|
|
48
|
+
| { readonly ok: false; readonly error: string };
|
|
49
|
+
|
|
50
|
+
const STATE_FENCE = /(`{3,})[ \t]*state[ \t]*\r?\n([\s\S]*?)\1/g;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Workstream A: extract ```state fences (model-proposed ΔΣ_t) from a response, in document
|
|
54
|
+
* order. ```repl parsing is untouched — the two fences coexist in one response. Malformed
|
|
55
|
+
* JSON is surfaced as an error result for the retry loop, never thrown.
|
|
56
|
+
*/
|
|
57
|
+
export function findStatePatches(text: string): readonly StateFenceResult[] {
|
|
58
|
+
const out: StateFenceResult[] = [];
|
|
59
|
+
let m: RegExpExecArray | null;
|
|
60
|
+
STATE_FENCE.lastIndex = 0;
|
|
61
|
+
while ((m = STATE_FENCE.exec(text)) !== null) {
|
|
62
|
+
const body = (m[2] ?? "").trim();
|
|
63
|
+
if (body === "") continue;
|
|
64
|
+
try {
|
|
65
|
+
out.push({ ok: true, value: JSON.parse(body) as unknown });
|
|
66
|
+
} catch (err: unknown) {
|
|
67
|
+
out.push({ ok: false, error: errorMessage(err) });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Truncate REPL stdout for the model's context window (head + tail, with an elision note).
|
|
74
|
+
* `mark` lets callers specialize the wording (root elision cites the session log) while the
|
|
75
|
+
* head/tail math stays the one implementation. */
|
|
76
|
+
export function truncateOutput(text: string, limit = 20_000, mark = "chars elided"): string {
|
|
45
77
|
if (text.length <= limit) return text;
|
|
46
78
|
const head = Math.floor(limit * 0.7);
|
|
47
79
|
const tail = limit - head;
|
|
48
80
|
const cut = text.length - head - tail;
|
|
49
|
-
return `${text.slice(0, head)}\n... [${cut}
|
|
81
|
+
return `${text.slice(0, head)}\n... [${cut} ${mark}] ...\n${text.slice(-tail)}`;
|
|
50
82
|
}
|
package/src/text/preview.ts
CHANGED
|
@@ -4,6 +4,9 @@ import type { ReplResult } from "../sandbox/protocol.ts";
|
|
|
4
4
|
|
|
5
5
|
const DEFAULT_PREVIEW_CHARS = 200;
|
|
6
6
|
|
|
7
|
+
/** Chars of a task/prompt shown on the collapsed tool-call line (repl + rlm tools share it). */
|
|
8
|
+
export const CALL_PREVIEW_CHARS = 80;
|
|
9
|
+
|
|
7
10
|
export function previewText(text: string, maxChars = DEFAULT_PREVIEW_CHARS): string {
|
|
8
11
|
const normalized = text.replace(/\s+/g, " ").trim();
|
|
9
12
|
return normalized.length > maxChars ? `${normalized.slice(0, Math.max(0, maxChars - 1))}…` : normalized;
|
package/src/text/repl-output.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { truncateOutput } from "./parsing.ts";
|
|
7
7
|
|
|
8
8
|
/** Max stderr kept in model-visible REPL output (headless history and native tool_result). */
|
|
9
|
-
|
|
9
|
+
const STDERR_LIMIT = 8_000;
|
|
10
10
|
|
|
11
11
|
/** Prefix stderr so the model can tell prints from exceptions. Empty when stderr is blank. */
|
|
12
12
|
export function formatReplStderr(stderr: string, limit = STDERR_LIMIT): string {
|
|
@@ -19,7 +19,7 @@ import type { Invocation } from "../bridge/handlers/index.ts";
|
|
|
19
19
|
import { trace, traceEnabled } from "../util/trace.ts";
|
|
20
20
|
|
|
21
21
|
/** What a drain hands to the turn that is reporting it. */
|
|
22
|
-
|
|
22
|
+
interface BackgroundDrain {
|
|
23
23
|
readonly subcalls: readonly RlmSubcall[];
|
|
24
24
|
readonly totals: SubcallTotals;
|
|
25
25
|
}
|
package/src/tool/repl-render.ts
CHANGED
|
@@ -12,7 +12,7 @@ const EXPANDED_STDERR_CHARS = 500;
|
|
|
12
12
|
|
|
13
13
|
// ── Collapsed view ──
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
function replStats(details: ReplDetails, theme: Theme): string {
|
|
16
16
|
const elapsed = details.executionTimeMs > 0 ? `${details.executionTimeMs}ms` : undefined;
|
|
17
17
|
return cardStatsLine(details.totals, theme, elapsed, details.backgroundPending);
|
|
18
18
|
}
|
package/src/tool/repl-result.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { capReplResultText, replDelegationNudge } from "../mode/native-guards.ts
|
|
|
10
10
|
import { formatReplStderr } from "../text/repl-output.ts";
|
|
11
11
|
|
|
12
12
|
/** Model-visible text assembled from a repl() result. */
|
|
13
|
-
|
|
13
|
+
interface ReplResultText {
|
|
14
14
|
readonly text: string;
|
|
15
15
|
}
|
|
16
16
|
|
package/src/tool/repl-tool.ts
CHANGED
|
@@ -29,7 +29,6 @@ import { SandboxManager } from "../sandbox/sandbox-manager.ts";
|
|
|
29
29
|
import type { SubcallOpts } from "../sandbox/sandbox.ts";
|
|
30
30
|
import { createSubcallHandlers, type Invocation } from "../bridge/handlers/index.ts";
|
|
31
31
|
import { TaskLedger } from "../core/ledger.ts";
|
|
32
|
-
import type { MemoryStore } from "../core/memory.ts";
|
|
33
32
|
import { BackgroundTasks } from "./background-tasks.ts";
|
|
34
33
|
import type { ReplResult } from "../sandbox/protocol.ts";
|
|
35
34
|
import { RlmEmitter } from "./rlm-events.ts";
|
|
@@ -38,18 +37,21 @@ import { SubcallStore } from "./subcall-store.ts";
|
|
|
38
37
|
import type { ReplDetails } from "./repl-details.ts";
|
|
39
38
|
import type { RlmSubcall } from "./rlm-details.ts";
|
|
40
39
|
import { createEngine } from "../core/engine.ts";
|
|
40
|
+
import type { RunState } from "../core/run-state.ts";
|
|
41
41
|
import { modelRef } from "../config/settings.ts";
|
|
42
42
|
import { spinnerFrame } from "../ui/theme.ts";
|
|
43
|
-
import { previewText } from "../text/preview.ts";
|
|
43
|
+
import { CALL_PREVIEW_CHARS, previewText } from "../text/preview.ts";
|
|
44
44
|
import { errorMessage } from "../util/errors.ts";
|
|
45
|
+
import {
|
|
46
|
+
groundLeafPrompt,
|
|
47
|
+
skillSearchHandler,
|
|
48
|
+
type SkillStore,
|
|
49
|
+
} from "../config/skillstate.ts";
|
|
45
50
|
import { createProgressNotifier, validateToolParams } from "./tool-utils.ts";
|
|
46
51
|
import { buildReplResultText, collectReplWarnings } from "./repl-result.ts";
|
|
47
52
|
import { renderReplCollapsed, renderReplExpanded } from "./repl-render.ts";
|
|
48
53
|
import { attachTracer, trace, traceEnabled } from "../util/trace.ts";
|
|
49
54
|
|
|
50
|
-
/** Chars of code shown on the tool call line. */
|
|
51
|
-
const CALL_PREVIEW_CHARS = 80;
|
|
52
|
-
|
|
53
55
|
/** Last non-empty line of a Python traceback — the `TypeError: …` line, not the frames. */
|
|
54
56
|
function lastLine(text: string): string {
|
|
55
57
|
const lines = text.trimEnd().split("\n");
|
|
@@ -62,7 +64,7 @@ function lastLine(text: string): string {
|
|
|
62
64
|
|
|
63
65
|
// ── Parameter schema ──
|
|
64
66
|
|
|
65
|
-
|
|
67
|
+
const ReplToolParams = Object.freeze(Type.Object({
|
|
66
68
|
code: Type.String({ description: "Python code to execute in the persistent REPL sandbox" }),
|
|
67
69
|
}));
|
|
68
70
|
|
|
@@ -103,7 +105,7 @@ class NativeBridgeState {
|
|
|
103
105
|
|
|
104
106
|
// ── Tool factory ──
|
|
105
107
|
|
|
106
|
-
|
|
108
|
+
interface ReplToolDeps {
|
|
107
109
|
readonly sandboxManager: SandboxManager;
|
|
108
110
|
readonly model: Model<Api>;
|
|
109
111
|
readonly llmModel: Model<Api>;
|
|
@@ -119,10 +121,14 @@ export interface ReplToolDeps {
|
|
|
119
121
|
readonly resolveGates?: () => SubcallGates;
|
|
120
122
|
/** Session-scoped home for detached spawn() work. */
|
|
121
123
|
readonly background: BackgroundTasks;
|
|
124
|
+
/** SKILL.state (Workstream B): session store — leaf grounding, skill_search, child Ξ. */
|
|
125
|
+
readonly skillStore?: SkillStore;
|
|
126
|
+
/** Root Σ (WS-4): engine-finalize Σ observer, forwarded to createEngine (one seam). */
|
|
127
|
+
readonly onRunState?: (state: RunState) => void;
|
|
128
|
+
/** Ξ composer for repl()-spawned child engines (first hop; deeper hops copy via childRun). */
|
|
129
|
+
readonly getSkillBlock?: (task: string) => string | undefined;
|
|
122
130
|
/** Session tree panel index; omitted → runs don't appear in the widget. */
|
|
123
131
|
readonly runRegistry?: RunRegistry;
|
|
124
|
-
/** v5 durable memory (session-wide `.rlm` store); omitted → memory off for this tool. */
|
|
125
|
-
readonly memory?: MemoryStore;
|
|
126
132
|
readonly signal?: AbortSignal;
|
|
127
133
|
readonly onUsage?: (usage: Usage, role: "sub") => void;
|
|
128
134
|
readonly ensureContext?: () => Promise<void>;
|
|
@@ -149,25 +155,40 @@ export function createReplTool(deps: ReplToolDeps): ToolDefinition<typeof ReplTo
|
|
|
149
155
|
const getModel = (): Model<Api> => deps.getModel?.() ?? deps.model;
|
|
150
156
|
// v5 (audit C6): resolve lazily per call so provider-cap edits via /rlm-config apply live.
|
|
151
157
|
const currentGates = (): SubcallGates => deps.resolveGates?.() ?? deps.gates;
|
|
158
|
+
// Workstream D: leaf grounding — one closure, self-gating on the live config.
|
|
159
|
+
const groundLeaf = (prompt: string): string => {
|
|
160
|
+
const store = deps.skillStore;
|
|
161
|
+
const cfg = getConfig();
|
|
162
|
+
if (store === undefined || !cfg.enableSkillState) return prompt;
|
|
163
|
+
return groundLeafPrompt(store, cfg, prompt);
|
|
164
|
+
};
|
|
152
165
|
|
|
153
166
|
// Each rlm_query spawns a child RLM with its own sandbox and turn loop, not a flat
|
|
154
167
|
// one-shot llm_query. The engine is created per call so the child's subcalls, turn
|
|
155
168
|
// progress and cost deltas land on the emitter the parent invocation is using.
|
|
156
|
-
const runChild = (input: RlmInput, inv: Invocation): Promise<RlmResult> =>
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
169
|
+
const runChild = (input: RlmInput, inv: Invocation): Promise<RlmResult> => {
|
|
170
|
+
// Ξ inheritance, first hop (Workstream C): a repl()-spawned child has no parent RlmInput
|
|
171
|
+
// to copy from, so the session store composes the block for its task here. Deeper hops
|
|
172
|
+
// copy input.skillBlock inside childRun (DRY #6 — one construction site).
|
|
173
|
+
const block = input.skillBlock ?? deps.getSkillBlock?.(input.rootPrompt);
|
|
174
|
+
const childInput: RlmInput = block === undefined ? input : { ...input, skillBlock: block };
|
|
175
|
+
return createEngine({
|
|
176
|
+
model: getModel(),
|
|
177
|
+
llmModel: getLlmModel(),
|
|
178
|
+
registry,
|
|
179
|
+
config: getConfig(),
|
|
180
|
+
signal,
|
|
181
|
+
gates: currentGates(),
|
|
182
|
+
// Same emitter the parent subcall node lives on — see SubcallHandlerDeps.runChild.
|
|
183
|
+
emitter: inv.emitter,
|
|
184
|
+
// Everything a child engine spends is sub-work from this tool's perspective, including
|
|
185
|
+
// the child's own root turns — so fold both roles into "sub" rather than casting.
|
|
186
|
+
onUsage: onUsage === undefined ? undefined : (usage: Usage) => onUsage(usage, "sub"),
|
|
187
|
+
limits: limitsFromConfig(getConfig()),
|
|
188
|
+
skillStore: deps.skillStore,
|
|
189
|
+
onRunState: deps.onRunState,
|
|
190
|
+
})(childInput);
|
|
191
|
+
};
|
|
171
192
|
|
|
172
193
|
// Built once: the same closures stay correct across repl() calls because everything
|
|
173
194
|
// per-invocation is reached through bridgeState.resolve, not captured here.
|
|
@@ -188,8 +209,10 @@ export function createReplTool(deps: ReplToolDeps): ToolDefinition<typeof ReplTo
|
|
|
188
209
|
// earlier repl() reaches a child spawned in a later one. Populated before any interrupt can
|
|
189
210
|
// fire: execute() awaits ensureContext() before getOrCreate().
|
|
190
211
|
getChildContext: () => sandboxManager.contextPayload ?? undefined,
|
|
212
|
+
// SKILL.state: leaf grounding (Workstream D) + Ξ inheritance for children (DRY #6).
|
|
213
|
+
groundLeaf,
|
|
214
|
+
getSkillBlock: (task) => deps.getSkillBlock?.(task),
|
|
191
215
|
ledger: sessionLedger,
|
|
192
|
-
memory: deps.memory,
|
|
193
216
|
trackDetached: (task) => background.track(task),
|
|
194
217
|
});
|
|
195
218
|
|
|
@@ -322,7 +345,8 @@ export function createReplTool(deps: ReplToolDeps): ToolDefinition<typeof ReplTo
|
|
|
322
345
|
...subcallHandlers,
|
|
323
346
|
...(contextBundle?.handlers ?? {}),
|
|
324
347
|
ledgerClaims: () => Promise.resolve(sessionLedger.listClaims()),
|
|
325
|
-
|
|
348
|
+
// SKILL.state (Workstream E): the model-visible recall surface — one function.
|
|
349
|
+
skillSearch: skillSearchHandler(() => (getConfig().enableSkillState ? deps.skillStore : undefined)),
|
|
326
350
|
});
|
|
327
351
|
|
|
328
352
|
// Detect queue contention AFTER sandbox init (initPromise settled, isExecuting now accurate)
|
package/src/tool/rlm-tool.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { modelRef } from "../config/settings.ts";
|
|
|
13
13
|
import { spinnerFrame } from "../ui/theme.ts";
|
|
14
14
|
import type { RunRegistry } from "../ui/panel/run-registry.ts";
|
|
15
15
|
import { markdownTheme } from "../ui/theme-adapter.ts";
|
|
16
|
-
import { previewText } from "../text/preview.ts";
|
|
16
|
+
import { CALL_PREVIEW_CHARS, previewText } from "../text/preview.ts";
|
|
17
17
|
import { errorMessage } from "../util/errors.ts";
|
|
18
18
|
import { type RlmDetails } from "./rlm-details.ts";
|
|
19
19
|
import { RlmEmitter } from "./rlm-events.ts";
|
|
@@ -21,14 +21,12 @@ import { RlmEventAggregator } from "./rlm-aggregator.ts";
|
|
|
21
21
|
import { cardHeader, cardStatsLine, renderCollapsedCard } from "./subcall-render.ts";
|
|
22
22
|
import { createProgressNotifier, validateToolParams } from "./tool-utils.ts";
|
|
23
23
|
|
|
24
|
-
/** Chars of the prompt shown on the tool call line. */
|
|
25
|
-
const CALL_PREVIEW_CHARS = 80;
|
|
26
|
-
|
|
27
24
|
// ── Parameter schema ──
|
|
28
25
|
|
|
29
|
-
|
|
26
|
+
const RlmToolParams = Object.freeze(Type.Object({
|
|
30
27
|
prompt: Type.String({ description: "The task or question for the RLM engine" }),
|
|
31
28
|
context: Type.Optional(Type.String({ description: "Optional context. If omitted, the working directory is packed into context." })),
|
|
29
|
+
narrative: Type.Optional(Type.Boolean({ description: "History-as-deliverable run (audit/provenance/debug narrative): RunState stays off — the archive is the product." })),
|
|
32
30
|
}));
|
|
33
31
|
|
|
34
32
|
// ── Rendering helpers ──
|
|
@@ -101,6 +99,7 @@ export function createRlmTool(controller: RlmController, runRegistry?: RunRegist
|
|
|
101
99
|
const input: StartInput = {
|
|
102
100
|
rootPrompt: params.prompt,
|
|
103
101
|
context: params.context ?? undefined,
|
|
102
|
+
narrative: params.narrative,
|
|
104
103
|
};
|
|
105
104
|
const { done } = controller.start(ctx, input, emitter);
|
|
106
105
|
const result = await done;
|
|
@@ -14,7 +14,7 @@ import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
|
14
14
|
|
|
15
15
|
// ── Glyphs ──
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
function headlineStatusGlyph(status: SubcallStatus | "aborted" | "done", theme: Theme): string {
|
|
18
18
|
switch (status) {
|
|
19
19
|
case "done": return theme.fg("success", "✓");
|
|
20
20
|
case "error": return theme.fg("error", "✗");
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* subcall accumulation logic.
|
|
8
8
|
*/
|
|
9
9
|
import type { RlmEmitter, SubcallCreatedEvent, SubcallUpdatedEvent } from "./rlm-events.ts";
|
|
10
|
-
import type { RlmSubcall
|
|
10
|
+
import type { RlmSubcall } from "./rlm-details.ts";
|
|
11
11
|
import { EmitterListener } from "./emitter-listener.ts";
|
|
12
12
|
|
|
13
13
|
type MutableSubcall = {
|
|
@@ -153,7 +153,7 @@ export class SubcallStore extends EmitterListener {
|
|
|
153
153
|
tokens += node.tokens;
|
|
154
154
|
tokensIn += node.tokensIn;
|
|
155
155
|
tokensOut += node.tokensOut;
|
|
156
|
-
taken.push(Object.freeze({ ...node, status: node.status
|
|
156
|
+
taken.push(Object.freeze({ ...node, status: node.status }));
|
|
157
157
|
this.subcalls.delete(node.id);
|
|
158
158
|
}
|
|
159
159
|
}
|
package/src/tool/tool-utils.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type { Static, TSchema } from "typebox";
|
|
|
4
4
|
import { Value } from "typebox/value";
|
|
5
5
|
import { err, ok, type Result } from "../util/errors.ts";
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
interface TextToolResponse<Details> {
|
|
8
8
|
readonly content: { readonly type: "text"; readonly text: string }[];
|
|
9
9
|
readonly details: Details;
|
|
10
10
|
}
|
|
@@ -15,7 +15,7 @@ export function validateToolParams<Schema extends TSchema, Details>(
|
|
|
15
15
|
toolName: string,
|
|
16
16
|
createErrorDetails: (errors: string) => Details,
|
|
17
17
|
): Result<Static<Schema>, TextToolResponse<Details>> {
|
|
18
|
-
if (Value.Check(schema, rawParams)) return ok(rawParams
|
|
18
|
+
if (Value.Check(schema, rawParams)) return ok(rawParams);
|
|
19
19
|
const errors = [...Value.Errors(schema, rawParams)]
|
|
20
20
|
.map((error) => `${error.instancePath}: ${error.message}`)
|
|
21
21
|
.join("; ");
|
|
@@ -25,9 +25,9 @@ export function validateToolParams<Schema extends TSchema, Details>(
|
|
|
25
25
|
});
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
type TextUpdateCallback<Details> = (update: TextToolResponse<Details>) => void;
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
interface ProgressNotifierOptions<Details> {
|
|
31
31
|
readonly onUpdate?: TextUpdateCallback<Details>;
|
|
32
32
|
readonly getDetails: () => Details;
|
|
33
33
|
readonly isRunning: (details: Details) => boolean;
|
|
@@ -35,7 +35,7 @@ export interface ProgressNotifierOptions<Details> {
|
|
|
35
35
|
readonly intervalMs?: number;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
interface ProgressNotifier {
|
|
39
39
|
readonly notify: () => void;
|
|
40
40
|
readonly start: () => void;
|
|
41
41
|
readonly stop: () => void;
|
package/src/ui/config-panel.ts
CHANGED
|
@@ -68,6 +68,18 @@ export async function showConfigPanel(ctx: ExtensionContext, config: RlmConfig):
|
|
|
68
68
|
"Allow add_context() to pull an external dir, file, document, or git repo into context."),
|
|
69
69
|
item("autoSeedCwd", "Auto-seed cwd", config.autoSeedCwd ? "on" : "off", CHOICES.autoSeedCwd,
|
|
70
70
|
"Seed the working directory into context on the first repl() call (otherwise starts empty)."),
|
|
71
|
+
// R0 (/tmp/ROOT_FULL_SKILLSTATE_PLAN.md): the SKILL.state / Root Σ paradigm flags are
|
|
72
|
+
// ENFORCED — rendered as a read-only badge so the truth is visible instead of hidden.
|
|
73
|
+
// No toggle exists: applySetting has no case for them and the validator forces true.
|
|
74
|
+
item("__sigma_enforced__", "SKILL.state / Root Σ", "enforced", ["enforced"],
|
|
75
|
+
"ENFORCED (no opt-out): run state, skill state + distill, root context transform, state fences, digest compaction. " +
|
|
76
|
+
"Override attempts in rlm.json are traced (skillstate.override-ignored) and ignored; RLM_BENCH_NO_ROOTCONTEXT=1 is the dev-only measurement hatch."),
|
|
77
|
+
// R5: the window calibrations are rlm.json-only knobs — shown read-only with live values.
|
|
78
|
+
item("__sigma_window__", "Root Σ window (calibration)",
|
|
79
|
+
`keepTurns=${config.rootContextKeepTurns} · elide=${config.rootContextElideChars} · snapshot=${config.rootContextSnapshot ? "on" : "off"}`,
|
|
80
|
+
["rlm.json"],
|
|
81
|
+
"Query-time window calibrations, rlm.json only: rootContextKeepTurns (1 = strict: Σ + current turn; 2 = default), rootContextElideChars, rootContextSnapshot. " +
|
|
82
|
+
"Session resume/fork: the tracker is reborn lazily and Σ re-grows from live observations — the first call after a resume has an empty Σ by design."),
|
|
71
83
|
item("__save__", "Save & close", "↵", ["↵"], "Save these settings and close (Esc also saves)."),
|
|
72
84
|
];
|
|
73
85
|
|
package/src/ui/intro.ts
CHANGED
|
@@ -16,7 +16,7 @@ import type {
|
|
|
16
16
|
} from "../../tool/rlm-events.ts";
|
|
17
17
|
import { EmitterListener } from "../../tool/emitter-listener.ts";
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
const TIMELINE_CAP = 200;
|
|
20
20
|
|
|
21
21
|
export type TimelineIcon = "spawn" | "phase" | "done" | "error" | "note" | "turn";
|
|
22
22
|
|
|
@@ -58,7 +58,7 @@ async function pickFromList(
|
|
|
58
58
|
});
|
|
59
59
|
if (initialIndex > 0) list.setSelectedIndex(initialIndex);
|
|
60
60
|
const isFilterText = (s: string): boolean =>
|
|
61
|
-
s.length > 0 &&
|
|
61
|
+
s.length > 0 && Array.from(s).every((char) => char >= " " && char !== "\x7f");
|
|
62
62
|
const isBackspace = (s: string): boolean => s === "\x7f" || s === "\b";
|
|
63
63
|
list.onSelect = (item) => done(item.value);
|
|
64
64
|
list.onCancel = () => done(null);
|
|
@@ -10,7 +10,7 @@ import { DynamicBorder } from "@earendil-works/pi-coding-agent";
|
|
|
10
10
|
import type { Api, Model, ThinkingLevel } from "@earendil-works/pi-ai";
|
|
11
11
|
import { Container, SelectList, Text } from "@earendil-works/pi-tui";
|
|
12
12
|
|
|
13
|
-
const LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
|
|
13
|
+
const LEVELS = Object.freeze(["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const);
|
|
14
14
|
export type SelectableThinkingLevel = (typeof LEVELS)[number];
|
|
15
15
|
|
|
16
16
|
/** Levels the model actually supports, in canonical order. */
|
|
@@ -14,7 +14,7 @@ import type { RlmRunStatus, RlmSubcall, SubcallPhase } from "../../tool/rlm-deta
|
|
|
14
14
|
import type { RunSnapshot } from "../tree/tree-model.ts";
|
|
15
15
|
import { TimelineStore } from "../modal/timeline-store.ts";
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
interface RunRegistration {
|
|
18
18
|
readonly runId: string;
|
|
19
19
|
/** Root row label — prompt or code preview. */
|
|
20
20
|
readonly label: string;
|
package/src/ui/status.ts
CHANGED
|
@@ -3,23 +3,47 @@
|
|
|
3
3
|
* The footer's extension-status row is sanitized to a single line, so the
|
|
4
4
|
* two-model layout lives in a dedicated multi-line widget instead: one line
|
|
5
5
|
* for the mode, one per model lane (llm = leaf sub-calls, rlm = child engines),
|
|
6
|
-
* each with the live context token spend
|
|
6
|
+
* each with the live context token spend — and, when trace mode is on, a Root Σ
|
|
7
|
+
* telemetry line (R6): Ξ compositions, digest compactions, elisions, Σ splices,
|
|
8
|
+
* idle degrades.
|
|
7
9
|
*/
|
|
8
10
|
|
|
9
11
|
import type { ContextUsage, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
10
12
|
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
11
13
|
import type { RlmController } from "../mode/rlm-mode.ts";
|
|
14
|
+
import { traceEnabled } from "../util/trace.ts";
|
|
12
15
|
import { formatTokens } from "./theme.ts";
|
|
13
16
|
|
|
14
17
|
const KEY = "rlm";
|
|
15
18
|
|
|
19
|
+
/** R6: Root Σ closure-counter snapshot (fresh readonly object per render). */
|
|
20
|
+
export interface RootSigmaTelemetry {
|
|
21
|
+
readonly xiCompositions: number;
|
|
22
|
+
readonly rootDigests: number;
|
|
23
|
+
readonly elidedMessages: number;
|
|
24
|
+
readonly sigmaSplices: number;
|
|
25
|
+
readonly idleDegrades: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
16
28
|
export function modelLabel(model: Model<Api> | undefined, fallback: string): string {
|
|
17
29
|
return model ? `${model.provider}/${model.id}` : fallback;
|
|
18
30
|
}
|
|
19
31
|
|
|
32
|
+
/** R6: the Σ telemetry line — built only when tracing and at least one counter is live. */
|
|
33
|
+
function sigmaLine(telemetry: RootSigmaTelemetry): string | undefined {
|
|
34
|
+
const parts: string[] = [];
|
|
35
|
+
if (telemetry.xiCompositions > 0) parts.push(`Ξ${telemetry.xiCompositions}`);
|
|
36
|
+
if (telemetry.elidedMessages > 0) parts.push(`elided ${telemetry.elidedMessages}`);
|
|
37
|
+
if (telemetry.sigmaSplices > 0) parts.push(`Σ${telemetry.sigmaSplices}`);
|
|
38
|
+
if (telemetry.rootDigests > 0) parts.push(`digest ${telemetry.rootDigests}`);
|
|
39
|
+
if (telemetry.idleDegrades > 0) parts.push(`degraded ${telemetry.idleDegrades}`);
|
|
40
|
+
return parts.length === 0 ? undefined : ` Σ ${parts.join(" · ")}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
20
43
|
export function formatRlmStatusLines(
|
|
21
44
|
controller: RlmController,
|
|
22
45
|
contextUsage?: ContextUsage,
|
|
46
|
+
telemetry?: RootSigmaTelemetry,
|
|
23
47
|
): readonly string[] {
|
|
24
48
|
if (!controller.enabled) return ["○ RLM OFF"];
|
|
25
49
|
const tokens = contextUsage?.tokens;
|
|
@@ -28,14 +52,25 @@ export function formatRlmStatusLines(
|
|
|
28
52
|
const llmSuffix = controller.config.subSampling.reasoning ? `:${controller.config.subSampling.reasoning}` : "";
|
|
29
53
|
const rlm = modelLabel(controller.rlmModel, controller.savedRlmRef ?? "session");
|
|
30
54
|
const rlmSuffix = controller.config.rootSampling?.reasoning ? `:${controller.config.rootSampling.reasoning}` : "";
|
|
31
|
-
|
|
55
|
+
const lines = [
|
|
32
56
|
"● RLM ON",
|
|
33
57
|
` llm=${llm}${llmSuffix}${tokSuffix}`,
|
|
34
58
|
` rlm=${rlm}${rlmSuffix}${tokSuffix}`,
|
|
35
59
|
];
|
|
60
|
+
// R6: counters surface only under trace mode — the default UI stays clean.
|
|
61
|
+
if (traceEnabled && telemetry !== undefined) {
|
|
62
|
+
const sigma = sigmaLine(telemetry);
|
|
63
|
+
if (sigma !== undefined) lines.push(sigma);
|
|
64
|
+
}
|
|
65
|
+
return lines;
|
|
36
66
|
}
|
|
37
67
|
|
|
38
68
|
/** Set the above-editor status widget. Idempotent — call on every state change. */
|
|
39
|
-
export function setRlmModeStatus(
|
|
40
|
-
ctx
|
|
69
|
+
export function setRlmModeStatus(
|
|
70
|
+
ctx: ExtensionContext,
|
|
71
|
+
controller: RlmController,
|
|
72
|
+
contextUsage?: ContextUsage,
|
|
73
|
+
telemetry?: RootSigmaTelemetry,
|
|
74
|
+
): void {
|
|
75
|
+
ctx.ui.setWidget(KEY, [...formatRlmStatusLines(controller, contextUsage, telemetry)], { placement: "aboveEditor" });
|
|
41
76
|
}
|
package/src/ui/tree/tree-rows.ts
CHANGED
|
@@ -59,7 +59,7 @@ function formatGroup(row: GroupRow, selected: boolean, width: number, theme: The
|
|
|
59
59
|
return assembleLine(left, row.tokens, row.tokensIn, row.tokensOut, row.model, selected, width, theme);
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
function formatRow(row: TreeRow, selected: boolean, width: number, theme: Theme): string {
|
|
63
63
|
return row.type === "group" ? formatGroup(row, selected, width, theme) : formatNode(row, selected, width, theme);
|
|
64
64
|
}
|
|
65
65
|
|
|
@@ -28,7 +28,7 @@ const KEYS = Object.freeze({
|
|
|
28
28
|
} as const);
|
|
29
29
|
|
|
30
30
|
/** What the panel should do after a keypress. Discriminated — never a boolean soup. */
|
|
31
|
-
|
|
31
|
+
type KeyAction =
|
|
32
32
|
| { readonly type: "swallowed" }
|
|
33
33
|
| { readonly type: "unfocus" }
|
|
34
34
|
| { readonly type: "open"; readonly runId: string; readonly nodeId: string };
|