@hicaru/pi-rlm 0.3.15 → 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 +96 -70
- package/README.ru.md +86 -59
- package/README.zh-CN.md +95 -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 +21 -4
- package/src/commands/rlm-llm.ts +1 -10
- package/src/commands/rlm-rlm.ts +1 -8
- package/src/config/defaults.ts +31 -12
- package/src/config/settings.ts +47 -33
- 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 +192 -94
- 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 +56 -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/prompts/user.ts +17 -0
- 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__/retrieval.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/retrieval.py +4 -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 +35 -5
- 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 +39 -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/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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hicaru/pi-rlm",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.17",
|
|
4
4
|
"author": "hicaru",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
"typescript": "^5.0.0"
|
|
11
11
|
},
|
|
12
12
|
"peerDependencies": {
|
|
13
|
-
"@earendil-works/pi-ai": "^0.
|
|
14
|
-
"@earendil-works/pi-coding-agent": "^0.
|
|
15
|
-
"@earendil-works/pi-tui": "^0.
|
|
13
|
+
"@earendil-works/pi-ai": "^0.85.1",
|
|
14
|
+
"@earendil-works/pi-coding-agent": "^0.85.1",
|
|
15
|
+
"@earendil-works/pi-tui": "^0.85.1",
|
|
16
16
|
"typebox": "*"
|
|
17
17
|
},
|
|
18
18
|
"bugs": {
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"node": ">=20"
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
|
-
"@earendil-works/pi-agent-core": "^0.
|
|
55
|
+
"@earendil-works/pi-agent-core": "^0.85.1",
|
|
56
56
|
"@firecrawl/anydoc": "^0.2.4"
|
|
57
57
|
}
|
|
58
58
|
}
|
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
import { resolveSource } from "../context/resolve.ts";
|
|
25
25
|
import { previewText } from "../text/preview.ts";
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
interface AddContextBridgeOpts {
|
|
28
28
|
/** Fixed cwd (headless). Prefer getCwd when the sandbox outlives a single invocation. */
|
|
29
29
|
readonly cwd?: string;
|
|
30
30
|
/** Late-bound cwd (native mode — sandbox handlers outlive a single repl()). */
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { errorMessage, formatError } from "../../util/errors.ts";
|
|
10
10
|
import type { AwaitResult, SubcallHandlerDeps, TaskEntry } from "./types.ts";
|
|
11
11
|
import type { SubcallOpts } from "../../sandbox/interrupts.ts";
|
|
12
|
-
import type
|
|
12
|
+
import { entryToAwaitResult, type AwaitDeps } from "./task-registry.ts";
|
|
13
13
|
|
|
14
14
|
export function createAwaitHandler(_deps: SubcallHandlerDeps, ad: AwaitDeps) {
|
|
15
15
|
return async (
|
|
@@ -37,7 +37,7 @@ export function createAwaitHandler(_deps: SubcallHandlerDeps, ad: AwaitDeps) {
|
|
|
37
37
|
if (entry.status === "pending") {
|
|
38
38
|
try {
|
|
39
39
|
const resolved = await ad.wait(taskId, timeoutMs);
|
|
40
|
-
return
|
|
40
|
+
return entryToAwaitResult(resolved);
|
|
41
41
|
} catch (err: unknown) {
|
|
42
42
|
return {
|
|
43
43
|
ok: false,
|
|
@@ -49,7 +49,7 @@ export function createAwaitHandler(_deps: SubcallHandlerDeps, ad: AwaitDeps) {
|
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
return
|
|
52
|
+
return entryToAwaitResult(entry);
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
// Multiple tasks
|
|
@@ -91,7 +91,7 @@ export function createAwaitHandler(_deps: SubcallHandlerDeps, ad: AwaitDeps) {
|
|
|
91
91
|
}),
|
|
92
92
|
);
|
|
93
93
|
|
|
94
|
-
const awaited = resolved.map(
|
|
94
|
+
const awaited = resolved.map(entryToAwaitResult);
|
|
95
95
|
const allDone = awaited.every((a) => a.status === "done");
|
|
96
96
|
const hasResults = awaited.some((a) => a.results !== undefined);
|
|
97
97
|
const firstError = awaited.find((a) => a.error)?.error;
|
|
@@ -99,12 +99,18 @@ export function createAwaitHandler(_deps: SubcallHandlerDeps, ad: AwaitDeps) {
|
|
|
99
99
|
const kind = first?.kind ?? "unknown";
|
|
100
100
|
|
|
101
101
|
if (hasResults) {
|
|
102
|
-
|
|
102
|
+
// Pre-allocated (rule: no .push() growth when the size is computable) — count, then fill.
|
|
103
|
+
const total = awaited.reduce(
|
|
104
|
+
(n, a) => n + (a.results !== undefined ? a.results.length : a.result !== undefined ? 1 : 0),
|
|
105
|
+
0,
|
|
106
|
+
);
|
|
107
|
+
const allResults = new Array<string>(total);
|
|
108
|
+
let n = 0;
|
|
103
109
|
for (const a of awaited) {
|
|
104
110
|
if (a.results !== undefined) {
|
|
105
|
-
for (const r of a.results) allResults
|
|
111
|
+
for (const r of a.results) allResults[n++] = r;
|
|
106
112
|
} else if (a.result !== undefined) {
|
|
107
|
-
allResults
|
|
113
|
+
allResults[n++] = a.result;
|
|
108
114
|
}
|
|
109
115
|
}
|
|
110
116
|
return {
|
|
@@ -131,18 +137,3 @@ export function createAwaitHandler(_deps: SubcallHandlerDeps, ad: AwaitDeps) {
|
|
|
131
137
|
};
|
|
132
138
|
};
|
|
133
139
|
}
|
|
134
|
-
|
|
135
|
-
function toAwaitResult(entry: TaskEntry): AwaitResult {
|
|
136
|
-
const status = entry.status === "pending" ? "error" : entry.status;
|
|
137
|
-
return {
|
|
138
|
-
ok: entry.status === "done",
|
|
139
|
-
task_id: entry.taskId,
|
|
140
|
-
kind: entry.kind,
|
|
141
|
-
status,
|
|
142
|
-
result: entry.result,
|
|
143
|
-
results: entry.results,
|
|
144
|
-
error:
|
|
145
|
-
entry.error ??
|
|
146
|
-
(entry.status === "pending" ? "Task still pending" : undefined),
|
|
147
|
-
};
|
|
148
|
-
}
|
|
@@ -14,15 +14,33 @@ import { checkResourceLimits } from "../../core/resource-limits.ts";
|
|
|
14
14
|
import { errorMessage, formatError } from "../../util/errors.ts";
|
|
15
15
|
import { retryPolicy } from "../../util/retry.ts";
|
|
16
16
|
import type { Semaphore } from "../../util/concurrency.ts";
|
|
17
|
-
import type { Invocation, SubcallConfig } from "./types.ts";
|
|
17
|
+
import type { Invocation, SubcallConfig, SubcallHandlerDeps } from "./types.ts";
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
interface Complete1Deps {
|
|
20
20
|
readonly leafGate: Semaphore;
|
|
21
21
|
readonly registry: ModelRegistry;
|
|
22
22
|
readonly getLlmModel: () => Model<Api>;
|
|
23
23
|
readonly getConfig: () => SubcallConfig;
|
|
24
24
|
readonly signal?: AbortSignal;
|
|
25
25
|
readonly onUsage?: (usage: Usage, role: "sub") => void;
|
|
26
|
+
/** SKILL.state (Workstream D): the one grounding seam, projected verbatim from deps. */
|
|
27
|
+
readonly groundLeaf?: (prompt: string) => string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* AGENTS.md DRY: the ONE projection of SubcallHandlerDeps for complete1 — shared verbatim by
|
|
32
|
+
* the llm-query and rlm-query handlers. Never inline a second copy.
|
|
33
|
+
*/
|
|
34
|
+
export function completeDeps(deps: SubcallHandlerDeps): Complete1Deps {
|
|
35
|
+
return {
|
|
36
|
+
leafGate: deps.gates.leaf,
|
|
37
|
+
registry: deps.registry,
|
|
38
|
+
getLlmModel: deps.getLlmModel,
|
|
39
|
+
getConfig: deps.getConfig,
|
|
40
|
+
signal: deps.signal,
|
|
41
|
+
onUsage: deps.onUsage,
|
|
42
|
+
groundLeaf: deps.groundLeaf,
|
|
43
|
+
};
|
|
26
44
|
}
|
|
27
45
|
|
|
28
46
|
/**
|
|
@@ -41,14 +59,18 @@ export async function complete1(
|
|
|
41
59
|
timeoutMs: inv.limits.remainingTimeoutMs(),
|
|
42
60
|
});
|
|
43
61
|
if (limitError !== undefined) return limitError;
|
|
44
|
-
|
|
62
|
+
// Workstream D (DRY #1): the ONE grounding application — llm_query, batch items, rlm→llm
|
|
63
|
+
// demotion and depth-cap degrade all pass through here, so all of them get Ξ facts or none
|
|
64
|
+
// does. Below-threshold prompts come back byte-identical from the groundLeaf impl.
|
|
65
|
+
const grounded = deps.groundLeaf !== undefined ? deps.groundLeaf(prompt) : prompt;
|
|
66
|
+
if (grounded.length > config.maxPromptChars) {
|
|
45
67
|
return formatError(
|
|
46
|
-
`sub-LLM prompt exceeded the size limit (${
|
|
68
|
+
`sub-LLM prompt exceeded the size limit (${grounded.length.toLocaleString()} chars > ` +
|
|
47
69
|
`${config.maxPromptChars.toLocaleString()}). Shorten or chunk the prompt before calling llm_query.`,
|
|
48
70
|
);
|
|
49
71
|
}
|
|
50
72
|
try {
|
|
51
|
-
const messages: ChatMsg[] = [{ role: "user", content:
|
|
73
|
+
const messages: ChatMsg[] = [{ role: "user", content: grounded }];
|
|
52
74
|
const res = await deps.leafGate.run(() =>
|
|
53
75
|
modelComplete(messages, {
|
|
54
76
|
model: deps.getLlmModel(),
|
|
@@ -11,14 +11,14 @@ import { previewText } from "../../text/preview.ts";
|
|
|
11
11
|
import type { SubcallPhase } from "../../tool/rlm-details.ts";
|
|
12
12
|
import type { Invocation } from "./types.ts";
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
interface EmitOpts {
|
|
15
15
|
readonly kind: "llm" | "batch";
|
|
16
16
|
readonly label: string;
|
|
17
17
|
readonly args: string;
|
|
18
18
|
readonly model?: string;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
interface EmitSummary {
|
|
22
22
|
readonly preview: string;
|
|
23
23
|
readonly error?: string;
|
|
24
24
|
readonly failed?: number;
|
|
@@ -4,29 +4,20 @@
|
|
|
4
4
|
|
|
5
5
|
import type { Usage } from "@earendil-works/pi-ai";
|
|
6
6
|
import { modelRef } from "../../config/settings.ts";
|
|
7
|
-
import { complete1,
|
|
8
|
-
import { emitting, summarizeLeaf, throttleHooks } from "./emitting.ts";
|
|
7
|
+
import { complete1, completeDeps } from "./completion.ts";
|
|
8
|
+
import { emitting, summarizeLeaf, throttleHooks, type EmitNote } from "./emitting.ts";
|
|
9
9
|
import { formatError, errorMessage } from "../../util/errors.ts";
|
|
10
10
|
import { previewText } from "../../text/preview.ts";
|
|
11
|
-
import type { SpawnResult, SubcallHandlerDeps } from "./types.ts";
|
|
11
|
+
import type { Invocation, SpawnResult, SubcallHandlerDeps } from "./types.ts";
|
|
12
12
|
import type { SubcallOpts } from "../../sandbox/interrupts.ts";
|
|
13
13
|
import { SPAWN_HINT, spawnAndRun, type SpawnDeps } from "./task-registry.ts";
|
|
14
14
|
import { ECHO_STUB, taskKey, type TaskLedger } from "../../core/ledger.ts";
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
/** Shared with rlm-query.ts — the unwired rejection sentinel (AGENTS DRY). */
|
|
17
|
+
export const UNWIRED = formatError("RLM bridge not wired for this invocation");
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
leafGate: deps.gates.leaf,
|
|
21
|
-
registry: deps.registry,
|
|
22
|
-
getLlmModel: deps.getLlmModel,
|
|
23
|
-
getConfig: deps.getConfig,
|
|
24
|
-
signal: deps.signal,
|
|
25
|
-
onUsage: deps.onUsage,
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function displayModel(deps: SubcallHandlerDeps): string | undefined {
|
|
19
|
+
/** Shared with rlm-query.ts — one display-model resolution (AGENTS DRY #3). */
|
|
20
|
+
export function displayModel(deps: SubcallHandlerDeps): string | undefined {
|
|
30
21
|
try {
|
|
31
22
|
const m = deps.getLlmModel();
|
|
32
23
|
return modelRef(m) ?? m.id;
|
|
@@ -36,10 +27,36 @@ function displayModel(deps: SubcallHandlerDeps): string | undefined {
|
|
|
36
27
|
}
|
|
37
28
|
|
|
38
29
|
/** The ledger active for leaf calls — undefined when disabled by config or not threaded in. */
|
|
39
|
-
function activeLedger(deps: SubcallHandlerDeps): TaskLedger | undefined {
|
|
30
|
+
export function activeLedger(deps: SubcallHandlerDeps): TaskLedger | undefined {
|
|
40
31
|
return deps.getConfig().enableLedger ? deps.ledger : undefined;
|
|
41
32
|
}
|
|
42
33
|
|
|
34
|
+
/** DRY: one unwired-spawn shape — kind/n vary, everything else is the same rejection.
|
|
35
|
+
* Shared with rlm-query.ts. */
|
|
36
|
+
export function unwiredSpawn(kind: SpawnResult["kind"], n: number): SpawnResult {
|
|
37
|
+
return { ok: false, task_id: null, kind, n, status: "pending", hint: SPAWN_HINT, error: UNWIRED };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** DRY: the shared leaf recipe — one UI node, one complete1 execution, one summarizer. */
|
|
41
|
+
function emitLeaf(
|
|
42
|
+
inv: Invocation,
|
|
43
|
+
deps: SubcallHandlerDeps,
|
|
44
|
+
prompt: string,
|
|
45
|
+
exec: (track: (u: Usage) => void, note: EmitNote) => Promise<string>,
|
|
46
|
+
): Promise<string> {
|
|
47
|
+
return emitting(
|
|
48
|
+
inv,
|
|
49
|
+
{
|
|
50
|
+
kind: "llm",
|
|
51
|
+
label: "llm_query",
|
|
52
|
+
args: `prompt: ${previewText(prompt)}`,
|
|
53
|
+
model: displayModel(deps),
|
|
54
|
+
},
|
|
55
|
+
(track, note) => exec(track, note),
|
|
56
|
+
summarizeLeaf,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
43
60
|
/** v5 TaskLedger routing for ONE leaf prompt (audit H3 — shared by llm_query and every
|
|
44
61
|
* llm_batch item, which v5 routed through `_spawn_single` too): echo → stub string,
|
|
45
62
|
* coalesce → the twin's result (bounded wait), run → caller executes then finish/fail.
|
|
@@ -84,31 +101,12 @@ export function createLlmQueryHandler(
|
|
|
84
101
|
opts: SubcallOpts,
|
|
85
102
|
): Promise<SpawnResult> => {
|
|
86
103
|
const inv = deps.resolve(opts, depth);
|
|
87
|
-
if (inv === null)
|
|
88
|
-
return {
|
|
89
|
-
ok: false,
|
|
90
|
-
task_id: null,
|
|
91
|
-
kind: "llm",
|
|
92
|
-
n: 1,
|
|
93
|
-
status: "pending",
|
|
94
|
-
hint: SPAWN_HINT,
|
|
95
|
-
error: UNWIRED,
|
|
96
|
-
};
|
|
97
|
-
}
|
|
104
|
+
if (inv === null) return unwiredSpawn("llm", 1);
|
|
98
105
|
|
|
99
106
|
const cdeps = completeDeps(deps);
|
|
100
107
|
const runLeaf = (): Promise<string> =>
|
|
101
|
-
|
|
102
|
-
inv,
|
|
103
|
-
{
|
|
104
|
-
kind: "llm",
|
|
105
|
-
label: "llm_query",
|
|
106
|
-
args: `prompt: ${previewText(prompt)}`,
|
|
107
|
-
model: displayModel(deps),
|
|
108
|
-
},
|
|
109
|
-
(track: (u: Usage) => void, note) => complete1(inv, prompt, track, cdeps, throttleHooks(note)),
|
|
110
|
-
summarizeLeaf,
|
|
111
|
-
);
|
|
108
|
+
emitLeaf(inv, deps, prompt, (track, note) =>
|
|
109
|
+
complete1(inv, prompt, track, cdeps, throttleHooks(note)));
|
|
112
110
|
// v5 TaskLedger for leaves: identical prompts coalesce onto one completion (key has no
|
|
113
111
|
// context — a leaf's entire world is the prompt text itself).
|
|
114
112
|
return spawnAndRun(
|
|
@@ -132,17 +130,7 @@ export function createLlmBatchHandler(
|
|
|
132
130
|
opts: SubcallOpts,
|
|
133
131
|
): Promise<SpawnResult> => {
|
|
134
132
|
const inv = deps.resolve(opts, depth);
|
|
135
|
-
if (inv === null)
|
|
136
|
-
return {
|
|
137
|
-
ok: false,
|
|
138
|
-
task_id: null,
|
|
139
|
-
kind: "llm_batch",
|
|
140
|
-
n: prompts.length,
|
|
141
|
-
status: "pending",
|
|
142
|
-
hint: SPAWN_HINT,
|
|
143
|
-
error: UNWIRED,
|
|
144
|
-
};
|
|
145
|
-
}
|
|
133
|
+
if (inv === null) return unwiredSpawn("llm_batch", prompts.length);
|
|
146
134
|
|
|
147
135
|
const cdeps = completeDeps(deps);
|
|
148
136
|
const ledger = activeLedger(deps);
|
|
@@ -155,27 +143,17 @@ export function createLlmBatchHandler(
|
|
|
155
143
|
() =>
|
|
156
144
|
Promise.all(
|
|
157
145
|
prompts.map((p) =>
|
|
158
|
-
|
|
159
|
-
inv,
|
|
160
|
-
{
|
|
161
|
-
kind: "llm",
|
|
162
|
-
label: "llm_query",
|
|
163
|
-
args: `prompt: ${previewText(p)}`,
|
|
164
|
-
model: displayModel(deps),
|
|
165
|
-
},
|
|
146
|
+
emitLeaf(inv, deps, p, (track, note) =>
|
|
166
147
|
// NO outer gate — complete1 takes the single leaf slot per prompt.
|
|
167
148
|
// v5 (audit H3): every item routes through the ledger — duplicate prompts inside
|
|
168
149
|
// one batch (or twins of other in-flight leaves) coalesce instead of paying N times.
|
|
169
|
-
(
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
),
|
|
177
|
-
summarizeLeaf,
|
|
178
|
-
),
|
|
150
|
+
runClaimedLeaf(
|
|
151
|
+
ledger,
|
|
152
|
+
ledger === undefined ? undefined : leafClaimKey(deps, p),
|
|
153
|
+
p,
|
|
154
|
+
inv.depth,
|
|
155
|
+
() => complete1(inv, p, track, cdeps, throttleHooks(note)),
|
|
156
|
+
)),
|
|
179
157
|
),
|
|
180
158
|
),
|
|
181
159
|
deps.trackDetached,
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* AGENTS.md DRY #2: childRun exists once, here.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { modelLabelOf } from "../../config/settings.ts";
|
|
8
8
|
import { errorMessage, formatError } from "../../util/errors.ts";
|
|
9
9
|
import { filterContextByPaths } from "../../context/merge.ts";
|
|
10
10
|
import { previewText } from "../../text/preview.ts";
|
|
@@ -13,12 +13,11 @@ import { checkResourceLimits } from "../../core/resource-limits.ts";
|
|
|
13
13
|
import { contextSig, ECHO_STUB, taskKey } from "../../core/ledger.ts";
|
|
14
14
|
import type { Invocation, SpawnResult, SubcallHandlerDeps } from "./types.ts";
|
|
15
15
|
import type { SubcallOpts } from "../../sandbox/interrupts.ts";
|
|
16
|
-
import {
|
|
17
|
-
import { complete1,
|
|
16
|
+
import { spawnAndRun, type SpawnDeps } from "./task-registry.ts";
|
|
17
|
+
import { complete1, completeDeps } from "./completion.ts";
|
|
18
18
|
import { emitting, summarizeLeaf, throttleHooks } from "./emitting.ts";
|
|
19
|
-
import { leafClaimKey, runClaimedLeaf } from "./llm-query.ts";
|
|
19
|
+
import { activeLedger, leafClaimKey, runClaimedLeaf, unwiredSpawn } from "./llm-query.ts";
|
|
20
20
|
|
|
21
|
-
const UNWIRED = formatError("RLM bridge not wired for this invocation");
|
|
22
21
|
const NO_UNMATCHED: readonly string[] = Object.freeze([]);
|
|
23
22
|
|
|
24
23
|
function emptyResult(answer: string): RlmResult {
|
|
@@ -56,25 +55,9 @@ function childContextFor(
|
|
|
56
55
|
});
|
|
57
56
|
}
|
|
58
57
|
|
|
59
|
-
function completeDeps(deps: SubcallHandlerDeps): Complete1Deps {
|
|
60
|
-
return {
|
|
61
|
-
leafGate: deps.gates.leaf,
|
|
62
|
-
registry: deps.registry,
|
|
63
|
-
getLlmModel: deps.getLlmModel,
|
|
64
|
-
getConfig: deps.getConfig,
|
|
65
|
-
signal: deps.signal,
|
|
66
|
-
onUsage: deps.onUsage,
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/** Ledger active for this call — undefined when disabled by config or not threaded in. */
|
|
71
|
-
function activeLedger(deps: SubcallHandlerDeps) {
|
|
72
|
-
return deps.getConfig().enableLedger ? deps.ledger : undefined;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
58
|
function claimKeyFor(deps: SubcallHandlerDeps, kind: "llm" | "rlm", prompt: string, paths: readonly string[], ctx: string): string {
|
|
76
59
|
const rootModel = deps.getModel?.();
|
|
77
|
-
const modelId = rootModel === undefined ? "" : (
|
|
60
|
+
const modelId = rootModel === undefined ? "" : modelLabelOf(rootModel);
|
|
78
61
|
return taskKey(kind, prompt, paths, modelId, ctx);
|
|
79
62
|
}
|
|
80
63
|
|
|
@@ -111,31 +94,8 @@ async function childRun(
|
|
|
111
94
|
? prompt
|
|
112
95
|
: `${prompt}\n\n[rlm] paths=${child.unmatched.join(", ")} matched no files; you received the full context.`;
|
|
113
96
|
|
|
114
|
-
// ── v5 memory replay: an identical, still-fresh child answer replays for zero API calls ──
|
|
115
|
-
const memory = deps.memory;
|
|
116
97
|
const sig = contextSig(child.context);
|
|
117
98
|
const key = claimKeyFor(deps, "rlm", prompt, paths ?? [], sig);
|
|
118
|
-
if (memory !== undefined && deps.getConfig().enableMemory !== false) {
|
|
119
|
-
const hit = memory.replay(key);
|
|
120
|
-
if (hit !== undefined) {
|
|
121
|
-
const replayId = inv.emitter.emitSubcallCreated({
|
|
122
|
-
kind: "rlm",
|
|
123
|
-
parentId: inv.parentId,
|
|
124
|
-
label: "rlm_query (replay)",
|
|
125
|
-
detail: prompt.slice(0, 60),
|
|
126
|
-
depth: childDepth,
|
|
127
|
-
});
|
|
128
|
-
inv.emitter.emitSubcallUpdated({ id: replayId, status: "done", resultPreview: hit.result.slice(0, 200) });
|
|
129
|
-
return {
|
|
130
|
-
answer: hit.result,
|
|
131
|
-
iterations: 0,
|
|
132
|
-
costUsd: 0,
|
|
133
|
-
inputTokens: 0,
|
|
134
|
-
outputTokens: 0,
|
|
135
|
-
durationMs: 0,
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
99
|
|
|
140
100
|
// ── v5 TaskLedger: echo → stub; duplicate → coalesce onto the existing runner ──────
|
|
141
101
|
const ledger = activeLedger(deps);
|
|
@@ -148,8 +108,7 @@ async function childRun(
|
|
|
148
108
|
// ONE subcall node per childRun (audit C2 / DRY #5): the decision branch reuses it, the
|
|
149
109
|
// run branch reports the engine's turns/cost on it. Never a second emit below.
|
|
150
110
|
const rootModel = deps.getModel?.();
|
|
151
|
-
const modelLabel =
|
|
152
|
-
rootModel === undefined ? undefined : (modelRef(rootModel) ?? rootModel.id);
|
|
111
|
+
const modelLabel = rootModel === undefined ? undefined : modelLabelOf(rootModel);
|
|
153
112
|
const subId = inv.emitter.emitSubcallCreated({
|
|
154
113
|
kind: "rlm",
|
|
155
114
|
parentId: inv.parentId,
|
|
@@ -168,13 +127,14 @@ async function childRun(
|
|
|
168
127
|
const twin = await ledger
|
|
169
128
|
.waitFor(decision.key)
|
|
170
129
|
.catch((err: unknown) => errorMessage(err));
|
|
171
|
-
inv.emitter.emitSubcallUpdated({ id: subId, status: "done", resultPreview: previewText(
|
|
172
|
-
return emptyResult(
|
|
130
|
+
inv.emitter.emitSubcallUpdated({ id: subId, status: "done", resultPreview: previewText(twin.slice(0, 80)) });
|
|
131
|
+
return emptyResult(twin);
|
|
173
132
|
}
|
|
174
133
|
if (ledger !== undefined && claimKey !== undefined) {
|
|
175
134
|
ledger.markRunning(claimKey);
|
|
176
135
|
}
|
|
177
136
|
|
|
137
|
+
const skillBlock = deps.getSkillBlock?.(prompt);
|
|
178
138
|
const input: RlmInput = {
|
|
179
139
|
rootPrompt,
|
|
180
140
|
context: child.context,
|
|
@@ -182,6 +142,9 @@ async function childRun(
|
|
|
182
142
|
parentNodeId: subId,
|
|
183
143
|
remainingTimeoutMs: remTimeout,
|
|
184
144
|
ledger, // DRY #6: the one seam — children share the parent's blackboard
|
|
145
|
+
// SKILL.state Ξ (Workstream C, DRY #6): the parent's block rides along — the only
|
|
146
|
+
// child-RlmInput construction site, so inheritance cannot grow a second path.
|
|
147
|
+
...(skillBlock === undefined ? {} : { skillBlock }),
|
|
185
148
|
};
|
|
186
149
|
|
|
187
150
|
try {
|
|
@@ -189,19 +152,6 @@ async function childRun(
|
|
|
189
152
|
inv.limits.addRaw(res.costUsd, res.inputTokens, res.outputTokens);
|
|
190
153
|
deps.onChildUsage?.(res.costUsd, res.inputTokens, res.outputTokens);
|
|
191
154
|
if (ledger !== undefined && claimKey !== undefined) ledger.finish(claimKey, res.answer);
|
|
192
|
-
// v5: child answers persist unconditionally — this is what later identical runs replay.
|
|
193
|
-
if (memory !== undefined && deps.getConfig().enableMemory !== false) {
|
|
194
|
-
memory.recordEpisode({
|
|
195
|
-
key,
|
|
196
|
-
kind: "rlm",
|
|
197
|
-
model: modelLabel ?? "",
|
|
198
|
-
prompt,
|
|
199
|
-
paths: paths ?? [],
|
|
200
|
-
result: res.answer,
|
|
201
|
-
tokensIn: res.inputTokens,
|
|
202
|
-
tokensOut: res.outputTokens,
|
|
203
|
-
});
|
|
204
|
-
}
|
|
205
155
|
inv.emitter.emitSubcallUpdated({
|
|
206
156
|
id: subId,
|
|
207
157
|
status: "done",
|
|
@@ -225,17 +175,7 @@ export function createRlmQueryHandler(deps: SubcallHandlerDeps, sd: SpawnDeps) {
|
|
|
225
175
|
opts: SubcallOpts,
|
|
226
176
|
): Promise<SpawnResult> => {
|
|
227
177
|
const inv = deps.resolve(opts, depth);
|
|
228
|
-
if (inv === null)
|
|
229
|
-
return {
|
|
230
|
-
ok: false,
|
|
231
|
-
task_id: null,
|
|
232
|
-
kind: "rlm",
|
|
233
|
-
n: 1,
|
|
234
|
-
status: "pending",
|
|
235
|
-
hint: SPAWN_HINT,
|
|
236
|
-
error: UNWIRED,
|
|
237
|
-
};
|
|
238
|
-
}
|
|
178
|
+
if (inv === null) return unwiredSpawn("rlm", 1);
|
|
239
179
|
|
|
240
180
|
const pathArg = opts.paths;
|
|
241
181
|
|
|
@@ -297,17 +237,7 @@ export function createRlmBatchHandler(deps: SubcallHandlerDeps, sd: SpawnDeps) {
|
|
|
297
237
|
opts: SubcallOpts,
|
|
298
238
|
): Promise<SpawnResult> => {
|
|
299
239
|
const inv = deps.resolve(opts, depth);
|
|
300
|
-
if (inv === null)
|
|
301
|
-
return {
|
|
302
|
-
ok: false,
|
|
303
|
-
task_id: null,
|
|
304
|
-
kind: "rlm_batch",
|
|
305
|
-
n: tasks.length,
|
|
306
|
-
status: "pending",
|
|
307
|
-
hint: SPAWN_HINT,
|
|
308
|
-
error: UNWIRED,
|
|
309
|
-
};
|
|
310
|
-
}
|
|
240
|
+
if (inv === null) return unwiredSpawn("rlm_batch", tasks.length);
|
|
311
241
|
|
|
312
242
|
const pathArg = opts.paths;
|
|
313
243
|
// No wrapper "rlm_batch ×N" node: every task already gets its own rlm_query node from
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import type { AwaitResult, SpawnResult, TaskEntry } from "./types.ts";
|
|
10
|
+
import { formatError, isErrorText } from "../../util/errors.ts";
|
|
10
11
|
|
|
11
12
|
export const SPAWN_HINT =
|
|
12
13
|
"Call await_task(task_id=...) to get the result — this is NOT the answer.";
|
|
@@ -24,6 +25,22 @@ export interface AwaitDeps {
|
|
|
24
25
|
unawaitedIds(): readonly string[];
|
|
25
26
|
}
|
|
26
27
|
|
|
28
|
+
/** DRY: the ONE TaskEntry → AwaitResult mapping — shared by the await handler and the registry. */
|
|
29
|
+
export function entryToAwaitResult(entry: TaskEntry): AwaitResult {
|
|
30
|
+
const status = entry.status === "pending" ? "error" : entry.status;
|
|
31
|
+
return {
|
|
32
|
+
ok: entry.status === "done",
|
|
33
|
+
task_id: entry.taskId,
|
|
34
|
+
kind: entry.kind,
|
|
35
|
+
status,
|
|
36
|
+
result: entry.result,
|
|
37
|
+
results: entry.results,
|
|
38
|
+
error:
|
|
39
|
+
entry.error ??
|
|
40
|
+
(entry.status === "pending" ? "Task still pending" : undefined),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
27
44
|
export interface TaskRegistry {
|
|
28
45
|
readonly spawnDeps: SpawnDeps;
|
|
29
46
|
readonly awaitDeps: AwaitDeps;
|
|
@@ -32,8 +49,8 @@ export interface TaskRegistry {
|
|
|
32
49
|
}
|
|
33
50
|
|
|
34
51
|
interface Waiter {
|
|
35
|
-
resolve: (entry: TaskEntry) => void;
|
|
36
|
-
reject: (err: Error) => void;
|
|
52
|
+
readonly resolve: (entry: TaskEntry) => void;
|
|
53
|
+
readonly reject: (err: Error) => void;
|
|
37
54
|
timer?: ReturnType<typeof setTimeout>;
|
|
38
55
|
}
|
|
39
56
|
|
|
@@ -146,19 +163,7 @@ export function createTaskRegistry(): TaskRegistry {
|
|
|
146
163
|
error: `Task ${taskId} not found`,
|
|
147
164
|
};
|
|
148
165
|
}
|
|
149
|
-
|
|
150
|
-
entry.status === "pending" ? "error" : entry.status;
|
|
151
|
-
return {
|
|
152
|
-
ok: entry.status === "done",
|
|
153
|
-
task_id: entry.taskId,
|
|
154
|
-
kind: entry.kind,
|
|
155
|
-
status,
|
|
156
|
-
result: entry.result,
|
|
157
|
-
results: entry.results,
|
|
158
|
-
error:
|
|
159
|
-
entry.error ??
|
|
160
|
-
(entry.status === "pending" ? "Task still pending" : undefined),
|
|
161
|
-
};
|
|
166
|
+
return entryToAwaitResult(entry);
|
|
162
167
|
},
|
|
163
168
|
};
|
|
164
169
|
}
|
|
@@ -185,14 +190,14 @@ export function spawnAndRun(
|
|
|
185
190
|
sd.resolve(taskId, result);
|
|
186
191
|
} catch (err: unknown) {
|
|
187
192
|
const message = err instanceof Error ? err.message : String(err);
|
|
188
|
-
sd.reject(taskId, message
|
|
193
|
+
sd.reject(taskId, isErrorText(message) ? message : formatError(message));
|
|
189
194
|
}
|
|
190
195
|
};
|
|
191
196
|
|
|
192
197
|
if (trackDetached !== undefined && detached) {
|
|
193
198
|
void trackDetached(run).catch((err: unknown) => {
|
|
194
199
|
const message = err instanceof Error ? err.message : String(err);
|
|
195
|
-
sd.reject(taskId, message
|
|
200
|
+
sd.reject(taskId, isErrorText(message) ? message : formatError(message));
|
|
196
201
|
});
|
|
197
202
|
} else {
|
|
198
203
|
void run();
|
|
@@ -13,7 +13,6 @@ import type { SubcallGates } from "../../util/concurrency.ts";
|
|
|
13
13
|
import type { SubcallOpts } from "../../sandbox/interrupts.ts";
|
|
14
14
|
import type { RlmEmitter } from "../../tool/rlm-events.ts";
|
|
15
15
|
import type { TaskLedger } from "../../core/ledger.ts";
|
|
16
|
-
import type { MemoryStore } from "../../core/memory.ts";
|
|
17
16
|
|
|
18
17
|
// ---------------------------------------------------------------------------
|
|
19
18
|
// Spawn / Await / Finish — the three shapes the model sees
|
|
@@ -73,7 +72,7 @@ export function limitsFromRemaining(
|
|
|
73
72
|
|
|
74
73
|
export interface Invocation {
|
|
75
74
|
readonly emitter: RlmEmitter;
|
|
76
|
-
readonly parentId
|
|
75
|
+
readonly parentId?: string;
|
|
77
76
|
readonly depth: number;
|
|
78
77
|
readonly limits: InvocationLimits;
|
|
79
78
|
}
|
|
@@ -91,8 +90,6 @@ export interface SubcallConfig {
|
|
|
91
90
|
readonly enableLedger?: boolean;
|
|
92
91
|
/** v5: real rlm spawns before demotion to llm (0 = never demote). */
|
|
93
92
|
readonly rlmBudget?: number;
|
|
94
|
-
/** v5 durable memory gates (optional; omitted → memory off). */
|
|
95
|
-
readonly enableMemory?: boolean;
|
|
96
93
|
/** v5.1 retry knobs — structural slice of RlmConfig so retryPolicy() can read them. */
|
|
97
94
|
readonly retryMaxAttempts?: number;
|
|
98
95
|
readonly rateLimitMaxAttempts?: number;
|
|
@@ -121,8 +118,13 @@ export interface SubcallHandlerDeps {
|
|
|
121
118
|
readonly trackDetached?: <T>(run: () => Promise<T>) => Promise<T>;
|
|
122
119
|
/** v5 TaskLedger blackboard shared across the whole run tree (claim/coalesce/echo/demote). */
|
|
123
120
|
readonly ledger?: TaskLedger;
|
|
124
|
-
/**
|
|
125
|
-
|
|
121
|
+
/** SKILL.state (Workstream D): ground a leaf prompt with verified facts. Applied ONCE in
|
|
122
|
+
* complete1 (DRY #1) so every leaf path inherits it; returns the prompt unchanged when
|
|
123
|
+
* nothing clears the score threshold — below it, byte-identical. */
|
|
124
|
+
readonly groundLeaf?: (prompt: string) => string;
|
|
125
|
+
/** SKILL.state (Workstream C): the parent run's Ξ block source, for childRun to copy
|
|
126
|
+
* into the child RlmInput (DRY #6 — one construction site). */
|
|
127
|
+
readonly getSkillBlock?: (task: string) => string | undefined;
|
|
126
128
|
}
|
|
127
129
|
|
|
128
130
|
// ---------------------------------------------------------------------------
|