@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
|
@@ -29,7 +29,7 @@ import { applyPathPrefix, contextSourceId, pathPrefixFor } from "./namespace.ts"
|
|
|
29
29
|
/** Conversions are libuv-threadpool bound (default pool 4); 8 keeps the pool busy without thrash. */
|
|
30
30
|
const CONVERT_CONCURRENCY = 8;
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
interface PackDirResult {
|
|
33
33
|
readonly files: readonly ContextFile[];
|
|
34
34
|
readonly chars: number;
|
|
35
35
|
readonly documents: number;
|
|
@@ -37,6 +37,25 @@ export interface PackDirResult {
|
|
|
37
37
|
readonly skipped: readonly SkippedFile[];
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
/** DRY: the ONE PackDirResult → SourceResult flattening — shared by the local-dir source and
|
|
41
|
+
* the git-clone source (which wraps it in Result). Never inline a second copy. */
|
|
42
|
+
export function packToSourceResult(
|
|
43
|
+
packed: PackDirResult,
|
|
44
|
+
sourceId: string,
|
|
45
|
+
pathPrefix: string,
|
|
46
|
+
): SourceResult {
|
|
47
|
+
return Object.freeze({
|
|
48
|
+
payload: packed.files,
|
|
49
|
+
files: packed.files.length,
|
|
50
|
+
chars: packed.chars,
|
|
51
|
+
sourceId,
|
|
52
|
+
pathPrefix,
|
|
53
|
+
documents: packed.documents,
|
|
54
|
+
converted: packed.converted,
|
|
55
|
+
skipped: packed.skipped,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
40
59
|
/**
|
|
41
60
|
* Walk `dir` and produce ContextFiles. Resolves the anydoc handle once per source
|
|
42
61
|
* (not per file) and threads it into the per-file router.
|
|
@@ -153,14 +172,5 @@ export async function sourceDir(
|
|
|
153
172
|
// Explicit pathPrefix (including "") wins; otherwise derive ctx/<id>/.
|
|
154
173
|
const pathPrefix = opts.pathPrefix !== undefined ? opts.pathPrefix : pathPrefixFor(sourceId);
|
|
155
174
|
const packed = await packDirectory(dir, pathPrefix, opts.signal);
|
|
156
|
-
return
|
|
157
|
-
payload: packed.files,
|
|
158
|
-
files: packed.files.length,
|
|
159
|
-
chars: packed.chars,
|
|
160
|
-
sourceId,
|
|
161
|
-
pathPrefix,
|
|
162
|
-
documents: packed.documents,
|
|
163
|
-
converted: packed.converted,
|
|
164
|
-
skipped: packed.skipped,
|
|
165
|
-
});
|
|
175
|
+
return packToSourceResult(packed, sourceId, pathPrefix);
|
|
166
176
|
}
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
type SkippedFile,
|
|
17
17
|
} from "./types.ts";
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
type DocFileResult =
|
|
20
20
|
| { readonly ok: true; readonly value: ContextFile; /** true only for a real anydoc conversion */ readonly converted: boolean }
|
|
21
21
|
| { readonly ok: false; readonly skipped: SkippedFile };
|
|
22
22
|
|
|
@@ -2,18 +2,15 @@
|
|
|
2
2
|
* Shallow-clone a git URL, then pack with source-dir.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { execFile } from "node:child_process";
|
|
6
5
|
import { mkdtemp, rm } from "node:fs/promises";
|
|
7
6
|
import { tmpdir } from "node:os";
|
|
8
7
|
import { join } from "node:path";
|
|
9
|
-
import { promisify } from "node:util";
|
|
10
8
|
import { errorMessage, type Result } from "../util/errors.ts";
|
|
9
|
+
import { execFileP } from "./walk.ts";
|
|
11
10
|
import { contextSourceId, pathPrefixFor } from "./namespace.ts";
|
|
12
|
-
import { packDirectory } from "./source-dir.ts";
|
|
11
|
+
import { packDirectory, packToSourceResult } from "./source-dir.ts";
|
|
13
12
|
import type { ResolveOpts, SourceResult } from "./types.ts";
|
|
14
13
|
|
|
15
|
-
const execFileP = promisify(execFile);
|
|
16
|
-
|
|
17
14
|
/** Shallow-clone `url` into a temp dir, pack, then remove the clone. */
|
|
18
15
|
export async function sourceGit(
|
|
19
16
|
url: string,
|
|
@@ -32,16 +29,7 @@ export async function sourceGit(
|
|
|
32
29
|
const packed = await packDirectory(dir, pathPrefix, opts.signal);
|
|
33
30
|
return {
|
|
34
31
|
ok: true,
|
|
35
|
-
value:
|
|
36
|
-
payload: packed.files,
|
|
37
|
-
files: packed.files.length,
|
|
38
|
-
chars: packed.chars,
|
|
39
|
-
sourceId,
|
|
40
|
-
pathPrefix,
|
|
41
|
-
documents: packed.documents,
|
|
42
|
-
converted: packed.converted,
|
|
43
|
-
skipped: packed.skipped,
|
|
44
|
-
}),
|
|
32
|
+
value: packToSourceResult(packed, sourceId, pathPrefix),
|
|
45
33
|
};
|
|
46
34
|
} catch (err: unknown) {
|
|
47
35
|
return { ok: false, error: `git clone failed for ${url} — ${errorMessage(err)}` };
|
|
@@ -9,7 +9,7 @@ import { applyPathPrefix } from "./namespace.ts";
|
|
|
9
9
|
import { isBinary } from "./walk.ts";
|
|
10
10
|
import { MAX_WALK_FILE_BYTES, type ContextFile, type SkippedFile } from "./types.ts";
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
type TextFileResult =
|
|
13
13
|
| { readonly ok: true; readonly value: ContextFile }
|
|
14
14
|
| { readonly ok: false; readonly skipped: SkippedFile };
|
|
15
15
|
|
package/src/context/walk.ts
CHANGED
|
@@ -13,12 +13,13 @@
|
|
|
13
13
|
|
|
14
14
|
import { execFile } from "node:child_process";
|
|
15
15
|
import type { Dirent } from "node:fs";
|
|
16
|
-
import { lstat, open, readdir, realpath
|
|
16
|
+
import { lstat, open, readdir, realpath } from "node:fs/promises";
|
|
17
17
|
import { basename, join, relative, resolve, sep } from "node:path";
|
|
18
18
|
import { promisify } from "node:util";
|
|
19
19
|
import type { SkipReason } from "./types.ts";
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
/** Shared promisified execFile (git listing) — source-git.ts reuses it (DRY). */
|
|
22
|
+
export const execFileP = promisify(execFile);
|
|
22
23
|
|
|
23
24
|
/** 8KB probe window for the NUL-byte binary check. */
|
|
24
25
|
const BINARY_PROBE_BYTES = 8 * 1024;
|
|
@@ -94,7 +95,7 @@ function absKey(path: string): string {
|
|
|
94
95
|
}
|
|
95
96
|
|
|
96
97
|
/** True when `fileAbs` is `rootAbs` or a descendant (prefix + separator). */
|
|
97
|
-
|
|
98
|
+
function isInsideRoot(fileAbs: string, rootAbs: string): boolean {
|
|
98
99
|
const root = absKey(rootAbs);
|
|
99
100
|
const file = absKey(fileAbs);
|
|
100
101
|
if (file === root) return true;
|
|
@@ -102,7 +103,7 @@ export function isInsideRoot(fileAbs: string, rootAbs: string): boolean {
|
|
|
102
103
|
return file.startsWith(prefix);
|
|
103
104
|
}
|
|
104
105
|
|
|
105
|
-
|
|
106
|
+
type PathSafety =
|
|
106
107
|
| { readonly ok: true; readonly realAbs: string }
|
|
107
108
|
| { readonly ok: false; readonly reason: Extract<SkipReason, "sensitive" | "symlink-escape" | "unreadable"> };
|
|
108
109
|
|
|
@@ -147,7 +148,7 @@ export async function checkPathSafety(absPath: string, packRoot: string): Promis
|
|
|
147
148
|
* Paths are returned raw from git (forward-slashed). Sensitive paths are NOT filtered here —
|
|
148
149
|
* packDirectory reports them as skipped: "sensitive" so the drop is visible.
|
|
149
150
|
*/
|
|
150
|
-
|
|
151
|
+
async function gitFiles(cwd: string, signal?: AbortSignal): Promise<readonly string[] | undefined> {
|
|
151
152
|
try {
|
|
152
153
|
const { stdout } = await execFileP(
|
|
153
154
|
"git",
|
|
@@ -239,12 +240,3 @@ export async function enumerateFiles(root: string, signal?: AbortSignal): Promis
|
|
|
239
240
|
return await walkFs(root, signal);
|
|
240
241
|
}
|
|
241
242
|
|
|
242
|
-
/** True when a path exists and is a regular file (or symlink to one). */
|
|
243
|
-
export async function isRegularFile(absPath: string): Promise<boolean> {
|
|
244
|
-
try {
|
|
245
|
-
const s = await stat(absPath);
|
|
246
|
-
return s.isFile();
|
|
247
|
-
} catch {
|
|
248
|
-
return false;
|
|
249
|
-
}
|
|
250
|
-
}
|
package/src/core/budget.ts
CHANGED
|
@@ -14,28 +14,29 @@
|
|
|
14
14
|
|
|
15
15
|
import type { ChatMsg } from "../bridge/model.ts";
|
|
16
16
|
import type { RlmConfig } from "./types.ts";
|
|
17
|
+
import type { RunState } from "./run-state.ts";
|
|
18
|
+
import { compactJSON } from "./run-state.ts";
|
|
17
19
|
|
|
18
|
-
|
|
20
|
+
interface TokenBudgetOptions {
|
|
19
21
|
readonly softFrac?: number;
|
|
20
22
|
readonly continuations?: number;
|
|
21
23
|
readonly maxContinuations?: number;
|
|
22
24
|
}
|
|
23
25
|
|
|
24
|
-
|
|
26
|
+
type BudgetState = "" | "soft" | "hard";
|
|
25
27
|
|
|
26
28
|
/** v5 verbatim: the soft wrap-up note prepended to the single turn after crossing soft. */
|
|
27
|
-
export const WRAP_UP_BUDGET: string =
|
|
29
|
+
export const WRAP_UP_BUDGET: string =
|
|
28
30
|
"[budget] ~80% of your token cap — ONE turn left. If the task is answerable NOW, finalize " +
|
|
29
31
|
'(set answer["ready"] = True). Otherwise print a compact findings dump: what is confirmed, ' +
|
|
30
32
|
"current file/line or search position, and the exact next step — a fresh continuation picks " +
|
|
31
|
-
"it up. Do not start new exploration."
|
|
32
|
-
);
|
|
33
|
+
"it up. Do not start new exploration.";
|
|
33
34
|
|
|
34
35
|
export const DEFAULT_NEXT_STEP: string =
|
|
35
|
-
|
|
36
|
+
"continue the probing that was in flight, then finalize";
|
|
36
37
|
|
|
37
38
|
/** v5 verbatim template (adapting the finalize spelling to this plugin's REPL). */
|
|
38
|
-
const HANDOFF_TEMPLATE: string =
|
|
39
|
+
const HANDOFF_TEMPLATE: string =
|
|
39
40
|
"A prior RLM run hit its token cap mid-task.\n" +
|
|
40
41
|
"You are its continuation — pick up EXACTLY where it stopped.\n\n" +
|
|
41
42
|
"ORIGINAL TASK:\n{query}\n\n" +
|
|
@@ -43,8 +44,7 @@ const HANDOFF_TEMPLATE: string = Object.freeze(
|
|
|
43
44
|
"CURRENT STATE / LAST ACTIONS:\n{state}\n\n" +
|
|
44
45
|
"NEXT STEP: {next}\n" +
|
|
45
46
|
"Do not re-do confirmed work; continue from the NEXT STEP and finalize as\n" +
|
|
46
|
-
'soon as the task is answerable (answer["ready"] = True).'
|
|
47
|
-
);
|
|
47
|
+
'soon as the task is answerable (answer["ready"] = True).';
|
|
48
48
|
|
|
49
49
|
/** v5's elision marker, used whenever a handoff section is trimmed. */
|
|
50
50
|
const ELISION_MARK = "\n…(+N chars elided [v5 handoff])…\n";
|
|
@@ -119,24 +119,26 @@ export class TokenBudget {
|
|
|
119
119
|
*/
|
|
120
120
|
export const BUDGET_WINDOW_FLOOR = 250_000;
|
|
121
121
|
|
|
122
|
-
/**
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
return new TokenBudget(Number.MAX_SAFE_INTEGER, {
|
|
122
|
+
/** One TokenBudget construction shape — the cap varies, the policy knobs never do (DRY). */
|
|
123
|
+
function makeBudget(config: RlmConfig, cap: number): TokenBudget {
|
|
124
|
+
return new TokenBudget(cap, {
|
|
126
125
|
softFrac: config.budgetSoftFrac,
|
|
127
126
|
maxContinuations: config.budgetMaxContinuations,
|
|
128
127
|
});
|
|
129
128
|
}
|
|
130
129
|
|
|
130
|
+
/** An effective budget that can never trigger — the cascade "switched off" without changing
|
|
131
|
+
* any call-site types (budget: TokenBudget | undefined). */
|
|
132
|
+
function unboundedBudget(config: RlmConfig): TokenBudget {
|
|
133
|
+
return makeBudget(config, Number.MAX_SAFE_INTEGER);
|
|
134
|
+
}
|
|
135
|
+
|
|
131
136
|
export function resolveBudget(contextWindow: number | undefined, config: RlmConfig): TokenBudget {
|
|
132
137
|
const ctx = contextWindow !== undefined && contextWindow > 0 ? contextWindow : 32_000;
|
|
133
138
|
if (ctx < BUDGET_WINDOW_FLOOR) return unboundedBudget(config);
|
|
134
139
|
const shareCap = Math.floor(ctx * config.budgetShare);
|
|
135
140
|
const cap = config.budgetTaskCap > 0 ? Math.min(shareCap, config.budgetTaskCap) : shareCap;
|
|
136
|
-
return
|
|
137
|
-
softFrac: config.budgetSoftFrac,
|
|
138
|
-
maxContinuations: config.budgetMaxContinuations,
|
|
139
|
-
});
|
|
141
|
+
return makeBudget(config, Math.max(cap, 1));
|
|
140
142
|
}
|
|
141
143
|
|
|
142
144
|
/**
|
|
@@ -150,12 +152,15 @@ export function truncateMid(text: string, maxChars: number): string {
|
|
|
150
152
|
return text.slice(0, half) + ELISION_MARK.replace("N", String(elided)) + text.slice(text.length - half);
|
|
151
153
|
}
|
|
152
154
|
|
|
155
|
+
/** Digest/handoff section caps — ONE source: budget.ts's handoff distillation and the root
|
|
156
|
+
* digest (core/root-digest.ts) must never drift apart on the same trajectory heuristics. */
|
|
157
|
+
export const FINDINGS_MAX = 6;
|
|
158
|
+
export const FINDINGS_MIN_CHARS = 20;
|
|
159
|
+
export const STATE_MAX = 8;
|
|
153
160
|
const QUERY_CHARS = 800;
|
|
154
|
-
const FINDINGS_MAX = 6;
|
|
155
|
-
const FINDINGS_MIN_CHARS = 20;
|
|
156
|
-
const STATE_MAX = 8;
|
|
157
161
|
const STATE_NEEDLE = "REPL stdout";
|
|
158
|
-
|
|
162
|
+
/** Next-step probe shared by the engine handoff and the root digest (one wording source). */
|
|
163
|
+
export const NEXT_STEP_RE = /next|then|will |todo/i;
|
|
159
164
|
|
|
160
165
|
/**
|
|
161
166
|
* Deterministic trajectory → handoff (v5 `distill_trajectory`). No LLM call: the model was
|
|
@@ -202,3 +207,84 @@ export function distillTrajectory(
|
|
|
202
207
|
export function continuationPrompt(n: number, handoff: string): string {
|
|
203
208
|
return `[continuation ${n}]\n${handoff}`;
|
|
204
209
|
}
|
|
210
|
+
|
|
211
|
+
// ── Workstream A: state-shaped hard-budget handoff ─────────────────────────────────
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* With Σ active, the hard-budget handoff IS the execution state: compactJSON(Σ) replaces the
|
|
215
|
+
* prose walk — smaller and lossless where it matters (findings/verifiedFacts survive
|
|
216
|
+
* verbatim; the paper's exact-state > prose-summary result, Tables 1/5). Reuses the v5
|
|
217
|
+
* HANDOFF_TEMPLATE slots; `distillTrajectory` remains only for degraded runs.
|
|
218
|
+
*/
|
|
219
|
+
export function stateHandoff(state: RunState, query: string, handoffChars = 4_000): string {
|
|
220
|
+
const findingsBlock = state.findings.slice(-3).join("\n");
|
|
221
|
+
return HANDOFF_TEMPLATE.replace("{query}", truncateMid(query.slice(0, QUERY_CHARS), Math.floor(handoffChars * 0.3)))
|
|
222
|
+
.replace("{findings}", truncateMid(findingsBlock, Math.floor(handoffChars * 0.2)))
|
|
223
|
+
.replace("{state}", truncateMid(compactJSON(state), Math.floor(handoffChars * 0.35)))
|
|
224
|
+
.replace("{next}", state.nextStep !== "" ? state.nextStep : DEFAULT_NEXT_STEP);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ── Workstream F: rectification at budget hard-state (MAS2 Eq. 5, local tier) ─────────
|
|
228
|
+
|
|
229
|
+
/** One deterministic local fix for a continuation — discriminated union, no flags.
|
|
230
|
+
* DOCTRINE: no arm ever switches models or providers — a failing model retries on itself
|
|
231
|
+
* until the attempt budget is exhausted, then fails loudly. */
|
|
232
|
+
export type RectifyAction =
|
|
233
|
+
| { readonly kind: "narrow-paths"; readonly paths: readonly string[] }
|
|
234
|
+
| { readonly kind: "reduce-concurrency"; readonly maxConcurrentSubcalls: number }
|
|
235
|
+
| { readonly kind: "none"; readonly reason: string };
|
|
236
|
+
|
|
237
|
+
/** Compact telemetry label for a rectification (run-node detail line). */
|
|
238
|
+
export function rectifyLabel(action: RectifyAction): string {
|
|
239
|
+
switch (action.kind) {
|
|
240
|
+
case "narrow-paths":
|
|
241
|
+
return `narrow-paths (${action.paths.length})`;
|
|
242
|
+
case "reduce-concurrency":
|
|
243
|
+
return `reduce-concurrency → ${action.maxConcurrentSubcalls}`;
|
|
244
|
+
case "none":
|
|
245
|
+
return "none";
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** Path-like tokens harvested from Σ — must contain a separator and an extension. */
|
|
250
|
+
const STATE_PATH_TOKEN = /(?:[\w@.-]+\/+)+[\w@.-]+\.[A-Za-z]{1,6}/g;
|
|
251
|
+
|
|
252
|
+
/** Top repeated paths from Σ, deterministic: frequency desc, then lexicographic; ≤4. */
|
|
253
|
+
function topPathsFromState(state: RunState | undefined): readonly string[] {
|
|
254
|
+
if (state === undefined) return [];
|
|
255
|
+
const freq = new Map<string, number>();
|
|
256
|
+
const sources = [...state.verifiedFacts, ...state.findings];
|
|
257
|
+
for (const line of sources) {
|
|
258
|
+
for (const match of line.matchAll(STATE_PATH_TOKEN)) {
|
|
259
|
+
const path = match[0];
|
|
260
|
+
freq.set(path, (freq.get(path) ?? 0) + 1);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return [...freq.entries()]
|
|
264
|
+
.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
|
|
265
|
+
.slice(0, 4)
|
|
266
|
+
.map(([path]) => path);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* MAS2 rectification (Eq. 5 adapted): at a budget hard-state, sample ONE local fix — no LLM.
|
|
271
|
+
* Deterministic priority: (1) narrow child paths from the run's top Σ paths, (2) halve leaf
|
|
272
|
+
* admission, (3) none. The model/provider pair is NEVER a rectification axis: a failing
|
|
273
|
+
* model retries until its attempt budget is exhausted and then the call fails loudly —
|
|
274
|
+
* no silent fallback, no swapping. The engine applies the choice to the continuation
|
|
275
|
+
* invocation and logs it on the run node.
|
|
276
|
+
*/
|
|
277
|
+
export function rectify(args: {
|
|
278
|
+
readonly state?: RunState;
|
|
279
|
+
readonly config: RlmConfig;
|
|
280
|
+
}): RectifyAction {
|
|
281
|
+
const paths = topPathsFromState(args.state);
|
|
282
|
+
if (paths.length >= 2) return { kind: "narrow-paths", paths };
|
|
283
|
+
if (args.config.maxConcurrentSubcalls >= 4) {
|
|
284
|
+
return {
|
|
285
|
+
kind: "reduce-concurrency",
|
|
286
|
+
maxConcurrentSubcalls: Math.max(1, Math.floor(args.config.maxConcurrentSubcalls / 2)),
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
return { kind: "none", reason: "no local fix available" };
|
|
290
|
+
}
|
package/src/core/compaction.ts
CHANGED
|
@@ -11,6 +11,8 @@ import type { ModelRegistry } from "@earendil-works/pi-coding-agent";
|
|
|
11
11
|
import { type ChatMsg, modelComplete } from "../bridge/model.ts";
|
|
12
12
|
import type { RetryPolicy } from "../util/retry.ts";
|
|
13
13
|
import { estimateMessageTokens } from "../text/tokens.ts";
|
|
14
|
+
import type { RunState } from "../core/run-state.ts";
|
|
15
|
+
import { compactJSON } from "../core/run-state.ts";
|
|
14
16
|
|
|
15
17
|
const DEFAULT_CONTEXT_WINDOW = 128_000;
|
|
16
18
|
|
|
@@ -19,7 +21,7 @@ const SUMMARY_REQUEST =
|
|
|
19
21
|
"(2) any concrete intermediate results — numbers, values, variable names — preserved exactly; " +
|
|
20
22
|
"(3) your next action. Be concise (1–3 paragraphs) but preserve all key results.";
|
|
21
23
|
|
|
22
|
-
|
|
24
|
+
interface CompactionDeps {
|
|
23
25
|
readonly model: Model<Api>;
|
|
24
26
|
readonly registry: ModelRegistry;
|
|
25
27
|
readonly contextWindow?: number;
|
|
@@ -112,3 +114,44 @@ export async function compactHistory(
|
|
|
112
114
|
},
|
|
113
115
|
];
|
|
114
116
|
}
|
|
117
|
+
|
|
118
|
+
/** Same protected-tail rule as elideOldToolPayloads: the working set survives verbatim. */
|
|
119
|
+
const REBASE_KEEP_TURNS = 2;
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Workstream A: structural rebase — the SKILL.state replacement for the LLM summary path.
|
|
123
|
+
*
|
|
124
|
+
* history := [ P , user("[Σ] …") , window(O) ]
|
|
125
|
+
*
|
|
126
|
+
* Everything older than the protected tail is superseded by the EXACT execution state — no
|
|
127
|
+
* model call is made at all (the summarizer LLM call disappears by design; the `compactions`
|
|
128
|
+
* counter still moves so telemetry stays comparable). Failure semantics: pure function, no
|
|
129
|
+
* fallible operations — the degraded path keeps `compactHistory` for as-built runs.
|
|
130
|
+
*/
|
|
131
|
+
export function rebaseWithState(history: ChatMsg[], state: RunState, count = 1): ChatMsg[] {
|
|
132
|
+
const system = history.find((m) => m.role === "system");
|
|
133
|
+
const head: ChatMsg[] = system ? [system] : [];
|
|
134
|
+
let tailStart = history.length;
|
|
135
|
+
let seen = 0;
|
|
136
|
+
for (let i = history.length - 1; i >= 0; i--) {
|
|
137
|
+
if (history[i].role === "assistant") {
|
|
138
|
+
seen += 1;
|
|
139
|
+
if (seen >= REBASE_KEEP_TURNS) {
|
|
140
|
+
tailStart = i;
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const window: ChatMsg[] = tailStart < history.length ? history.slice(tailStart) : [];
|
|
146
|
+
return [
|
|
147
|
+
...head,
|
|
148
|
+
{
|
|
149
|
+
role: "user",
|
|
150
|
+
content:
|
|
151
|
+
`Your conversation was structurally rebased ${count} time(s): older turns are superseded ` +
|
|
152
|
+
`by the exact execution state below — do NOT repeat completed work; fresh tool results ` +
|
|
153
|
+
`outrank Σ when they disagree.\n[Σ] ${compactJSON(state)}`,
|
|
154
|
+
},
|
|
155
|
+
...window,
|
|
156
|
+
];
|
|
157
|
+
}
|