@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
|
@@ -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/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/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 };
|
package/src/util/bm25.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-side Okapi BM25 (Workstreams C/D/E ranking).
|
|
3
|
+
*
|
|
4
|
+
* BM25 DUALITY, documented not accidental: this file and `sandbox/py/retrieval.py`
|
|
5
|
+
* (`_Bm25Index`) implement the SAME scoring for two runtimes — identical constants, identical
|
|
6
|
+
* tokenizer, identical idf/norm formulas — so a note ranked here lands in the same order the
|
|
7
|
+
* sandbox-side `search` would put it. Keep the two files in lockstep (AGENTS.md convention).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const BM25_K1 = 1.2; // mirrors retrieval.py:_BM25_K1
|
|
11
|
+
const BM25_B = 0.75; // mirrors retrieval.py:_BM25_B
|
|
12
|
+
// Mirrors retrieval.py:_TOKEN_SPLIT / _CAMEL_SPLIT: lowercased alphanumeric runs plus
|
|
13
|
+
// camelCase parts, so `resolveModelId`-style identifiers match their prose spellings.
|
|
14
|
+
const TOKEN_SPLIT = /[^0-9A-Za-z]+/;
|
|
15
|
+
const CAMEL_SPLIT = /(?<=[a-z0-9])(?=[A-Z])/;
|
|
16
|
+
|
|
17
|
+
export function bm25Tokenize(text: string): readonly string[] {
|
|
18
|
+
const out: string[] = [];
|
|
19
|
+
for (const raw of text.split(TOKEN_SPLIT)) {
|
|
20
|
+
if (raw === "") continue;
|
|
21
|
+
const lowered = raw.toLowerCase();
|
|
22
|
+
out.push(lowered);
|
|
23
|
+
if (raw.length > 3) {
|
|
24
|
+
const parts = raw.split(CAMEL_SPLIT);
|
|
25
|
+
if (parts.length > 1) {
|
|
26
|
+
for (const part of parts) {
|
|
27
|
+
const piece = part.toLowerCase();
|
|
28
|
+
if (piece !== "" && piece !== lowered) out.push(piece);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface Bm25Entry<T> {
|
|
37
|
+
readonly item: T;
|
|
38
|
+
readonly text: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface Bm25Hit<T> {
|
|
42
|
+
readonly item: T;
|
|
43
|
+
readonly score: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Rank entries against a query, best first, top-k, score > 0 only.
|
|
48
|
+
* Pre-allocated score/index arrays; no growth in the scoring loops.
|
|
49
|
+
*/
|
|
50
|
+
export function bm25Rank<T>(
|
|
51
|
+
query: string,
|
|
52
|
+
entries: readonly Bm25Entry<T>[],
|
|
53
|
+
k: number,
|
|
54
|
+
): readonly Bm25Hit<T>[] {
|
|
55
|
+
const n = entries.length;
|
|
56
|
+
if (n === 0 || k <= 0) return [];
|
|
57
|
+
const docLens = new Array<number>(n);
|
|
58
|
+
const postings = new Map<string, Array<readonly [number, number]>>();
|
|
59
|
+
for (let i = 0; i < n; i++) {
|
|
60
|
+
const terms = bm25Tokenize(entries[i].text);
|
|
61
|
+
docLens[i] = terms.length;
|
|
62
|
+
const freq = new Map<string, number>();
|
|
63
|
+
for (const term of terms) freq.set(term, (freq.get(term) ?? 0) + 1);
|
|
64
|
+
for (const [term, tf] of freq) {
|
|
65
|
+
const list = postings.get(term);
|
|
66
|
+
if (list !== undefined) list.push([i, tf]);
|
|
67
|
+
else postings.set(term, [[i, tf]]);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
let totalLen = 0;
|
|
71
|
+
for (let i = 0; i < n; i++) totalLen += docLens[i];
|
|
72
|
+
const avgLen = totalLen > 0 ? totalLen / n : 1.0;
|
|
73
|
+
|
|
74
|
+
const scores = new Array<number>(n).fill(0);
|
|
75
|
+
const seen = new Set<string>();
|
|
76
|
+
for (const term of bm25Tokenize(query)) {
|
|
77
|
+
if (seen.has(term)) continue; // Python twin scores set(terms)
|
|
78
|
+
seen.add(term);
|
|
79
|
+
const posting = postings.get(term);
|
|
80
|
+
if (posting === undefined) continue;
|
|
81
|
+
const df = posting.length;
|
|
82
|
+
const idf = Math.log(1.0 + (n - df + 0.5) / (df + 0.5));
|
|
83
|
+
for (const [idx, tf] of posting) {
|
|
84
|
+
const norm = BM25_K1 * (1.0 - BM25_B + BM25_B * (docLens[idx] / avgLen));
|
|
85
|
+
scores[idx] += (idf * (tf * (BM25_K1 + 1.0))) / (tf + norm);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const order = Array.from({ length: n }, (_, i) => i);
|
|
90
|
+
order.sort((a, b) => scores[b] - scores[a] || a - b); // deterministic tie-break
|
|
91
|
+
const out: Bm25Hit<T>[] = [];
|
|
92
|
+
for (let i = 0; i < n && out.length < k; i++) {
|
|
93
|
+
const idx = order[i];
|
|
94
|
+
if (scores[idx] > 0) out.push({ item: entries[idx].item, score: scores[idx] });
|
|
95
|
+
}
|
|
96
|
+
return out;
|
|
97
|
+
}
|
package/src/util/concurrency.ts
CHANGED
|
@@ -93,7 +93,7 @@ export function createSubcallGates(leafLimit: number, childLimit: number = leafL
|
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
/** Provider-capped config slice (RlmConfig satisfies this structurally). */
|
|
96
|
-
|
|
96
|
+
interface ProviderCapConfig {
|
|
97
97
|
readonly maxConcurrentSubcalls: number;
|
|
98
98
|
readonly maxConcurrentChildren: number;
|
|
99
99
|
/** v5: per-provider concurrent-request caps, e.g. { zai: 4 }. Caps only ever LOWER a limit. */
|
package/src/util/errors.ts
CHANGED
|
@@ -12,7 +12,7 @@ export function err<T = never, E = string>(error: E): Result<T, E> {
|
|
|
12
12
|
return { ok: false, error };
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
const ERROR_PREFIX = "Error:";
|
|
16
16
|
|
|
17
17
|
export function formatError(message: string): string {
|
|
18
18
|
return `${ERROR_PREFIX} ${message}`;
|