@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
|
@@ -0,0 +1,587 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workstream A — RunState: the structured execution state Σ_t carried across iterations of a
|
|
3
|
+
* headless run (SKILL.state paper §3).
|
|
4
|
+
*
|
|
5
|
+
* Per step the model receives exactly A_t = (P, Σ_t, O_t) and may propose a patch ΔΣ_t in a
|
|
6
|
+
* fenced ```state block: {"state_patch": {...}}. The runtime validates deterministically
|
|
7
|
+
* (V(ΔΣ_t, Σ_t) — paper §7: validation is runtime-side, never model-side), merges with
|
|
8
|
+
* null-deletion (Σ_{t+1} = Σ_t ⊕ ΔΣ_t — the one merge in util/state-merge.ts), and on
|
|
9
|
+
* rejection rolls back and surfaces the error as the next observation (error-as-observation
|
|
10
|
+
* retry). After `runStateRetryMax` failed patches the tracker DEGRADES to as-built
|
|
11
|
+
* append-only behavior — nothing in here ever throws into the turn loop.
|
|
12
|
+
*
|
|
13
|
+
* Serialization is compact (paper A.4): JSON.stringify(Σ) — no pretty-printing. Caps
|
|
14
|
+
* (RUN_STATE_LIMITS) keep |Σ_t| flat across iterations so per-turn prompt tokens stay flat.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { type Result, err, formatError, ok } from "../util/errors.ts";
|
|
18
|
+
import { isRecord } from "../util/type-guards.ts";
|
|
19
|
+
import type { StateFenceResult } from "../text/parsing.ts";
|
|
20
|
+
import type { RlmConfig } from "./types.ts";
|
|
21
|
+
import { SKILL_RECALL_LINE } from "../prompts/glossary.ts";
|
|
22
|
+
|
|
23
|
+
/** Outcome of a tried approach — closed union, no boolean flags. */
|
|
24
|
+
export type ApproachOutcome =
|
|
25
|
+
| { readonly status: "failed"; readonly reason: string }
|
|
26
|
+
| { readonly status: "partial"; readonly note: string }
|
|
27
|
+
| { readonly status: "succeeded"; readonly evidence: string };
|
|
28
|
+
|
|
29
|
+
/** RunState — the sufficient statistic carried across iterations. Compact-serialized. */
|
|
30
|
+
export interface RunState {
|
|
31
|
+
readonly task: string; // invariant restatement (≤200 chars)
|
|
32
|
+
readonly findings: readonly string[]; // verified discoveries (dedup by claim key)
|
|
33
|
+
readonly verifiedFacts: readonly string[]; // grounding facts: paths/symbols/configs
|
|
34
|
+
readonly testedApproaches: Readonly<Record<string, ApproachOutcome>>;
|
|
35
|
+
readonly artifacts: Readonly<Record<string, string>>; // name → REPL variable holding it
|
|
36
|
+
readonly openQuestions: readonly string[];
|
|
37
|
+
readonly nextStep: string;
|
|
38
|
+
/** Iteration counter (not wall clock) — runtime-stamped; the model can never set it. */
|
|
39
|
+
readonly updatedAt: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Model-proposed patch: exactly this shape inside a fenced ```state block (paper §3.2). */
|
|
43
|
+
export interface StatePatch {
|
|
44
|
+
readonly state_patch: Readonly<Record<string, unknown>>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Anti-bloat caps (§6.1): |Σ_t| never exceeds `bytesTotal` (≈3K tokens worst case), keeping
|
|
48
|
+
* per-turn prompt tokens flat. Eviction is deterministic: arrays drop oldest-first, records
|
|
49
|
+
* drop oldest-inserted (write refreshes insertion order = last-mention order). */
|
|
50
|
+
export const RUN_STATE_LIMITS = Object.freeze({
|
|
51
|
+
findings: 12,
|
|
52
|
+
verifiedFacts: 16,
|
|
53
|
+
testedApproaches: 24,
|
|
54
|
+
artifacts: 8,
|
|
55
|
+
openQuestions: 8,
|
|
56
|
+
bytesTotal: 12_000,
|
|
57
|
+
/** Per-turn patch byte cap (bench campaign rec #1): oversized `state_patch` fences are
|
|
58
|
+
* rejected whole with error-as-observation feedback — the model must commit deltas,
|
|
59
|
+
* not restate Σ. Quantifies the oolong ×5.4 output tax from verbose patches. */
|
|
60
|
+
patchBytes: 1_500,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
/** Bench campaign rec #2: after this many consecutive fence-requested turns with zero
|
|
64
|
+
* accepted deltas the run auto-degrades to as-built — an idle Σ is pure input tax on
|
|
65
|
+
* every prompt (weak-model fence tax, paper §5.7, live-confirmed by the bench). */
|
|
66
|
+
export const RUN_STATE_IDLE_DEGRADE_TURNS = 4;
|
|
67
|
+
|
|
68
|
+
export type PatchError =
|
|
69
|
+
| { readonly kind: "schema"; readonly detail: string }
|
|
70
|
+
| { readonly kind: "type"; readonly path: string; readonly expected: string }
|
|
71
|
+
| { readonly kind: "implicit-drop"; readonly path: string } // small-model guard (§5.7)
|
|
72
|
+
| { readonly kind: "cap"; readonly field: string };
|
|
73
|
+
|
|
74
|
+
/** Degradation state machine (§6.6) — discriminated union, no boolean flags. `retries`
|
|
75
|
+
* counts rejected patches (retry-cap degrade); `idle` counts consecutive fence-requested
|
|
76
|
+
* turns with zero accepted deltas (idle degrade, bench rec #2). */
|
|
77
|
+
export type RunStateMode =
|
|
78
|
+
| {
|
|
79
|
+
readonly kind: "active";
|
|
80
|
+
readonly state: RunState;
|
|
81
|
+
readonly retries: number;
|
|
82
|
+
readonly idle: number;
|
|
83
|
+
}
|
|
84
|
+
| { readonly kind: "degraded"; readonly reason: string };
|
|
85
|
+
|
|
86
|
+
/** Mutable working shape — the runtime draft patches apply to before capping/freezing.
|
|
87
|
+
* Exported so the root tracker (core/root-state.ts) builds on the exact same shape. */
|
|
88
|
+
export interface MutableState {
|
|
89
|
+
task: string;
|
|
90
|
+
nextStep: string;
|
|
91
|
+
updatedAt: number;
|
|
92
|
+
findings: string[];
|
|
93
|
+
verifiedFacts: string[];
|
|
94
|
+
openQuestions: string[];
|
|
95
|
+
testedApproaches: Record<string, unknown>;
|
|
96
|
+
artifacts: Record<string, unknown>;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const RUN_STATE_FIELDS: ReadonlySet<string> = new Set([
|
|
100
|
+
"task",
|
|
101
|
+
"findings",
|
|
102
|
+
"verifiedFacts",
|
|
103
|
+
"testedApproaches",
|
|
104
|
+
"artifacts",
|
|
105
|
+
"openQuestions",
|
|
106
|
+
"nextStep",
|
|
107
|
+
"updatedAt",
|
|
108
|
+
]);
|
|
109
|
+
const ARRAY_FIELDS: ReadonlySet<string> = new Set(["findings", "verifiedFacts", "openQuestions"]);
|
|
110
|
+
const TASK_MAX_CHARS = 200;
|
|
111
|
+
const NEXT_STEP_MAX_CHARS = 300;
|
|
112
|
+
// `findings` | `findings[+]` | `findings[2]` | `testedApproaches.h1`
|
|
113
|
+
const PATCH_KEY = /^([a-zA-Z_][a-zA-Z0-9_]*)((?:\.[a-zA-Z_][a-zA-Z0-9_]*)*)(\[\+\]|\[\d+\])?$/;
|
|
114
|
+
|
|
115
|
+
export function freshRunState(task: string): RunState {
|
|
116
|
+
return {
|
|
117
|
+
task,
|
|
118
|
+
findings: [],
|
|
119
|
+
verifiedFacts: [],
|
|
120
|
+
testedApproaches: {},
|
|
121
|
+
artifacts: {},
|
|
122
|
+
openQuestions: [],
|
|
123
|
+
nextStep: "",
|
|
124
|
+
updatedAt: 0,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Compact serialization (paper A.4) — the byte budget `bytesTotal` defends. */
|
|
129
|
+
export function compactJSON(state: RunState): string {
|
|
130
|
+
return JSON.stringify(state);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function isRunState(value: unknown): value is RunState {
|
|
134
|
+
if (!isRecord(value)) return false;
|
|
135
|
+
if (typeof value.task !== "string" || typeof value.nextStep !== "string") return false;
|
|
136
|
+
if (typeof value.updatedAt !== "number") return false;
|
|
137
|
+
for (const field of ["findings", "verifiedFacts", "openQuestions"]) {
|
|
138
|
+
if (!isStringArray(value[field])) return false;
|
|
139
|
+
}
|
|
140
|
+
if (!isRecord(value.testedApproaches) || !isRecord(value.artifacts)) return false;
|
|
141
|
+
return Object.values(value.testedApproaches).every((v) => isApproachOutcome(v));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function isStatePatch(value: unknown): value is StatePatch {
|
|
145
|
+
return isRecord(value) && isRecord(value.state_patch);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function isStringArray(value: unknown): value is readonly string[] {
|
|
149
|
+
return Array.isArray(value) && value.every((v) => typeof v === "string");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function isApproachOutcome(value: unknown): value is ApproachOutcome {
|
|
153
|
+
if (!isRecord(value)) return false;
|
|
154
|
+
if (value.status === "failed") return typeof value.reason === "string";
|
|
155
|
+
if (value.status === "partial") return typeof value.note === "string";
|
|
156
|
+
if (value.status === "succeeded") return typeof value.evidence === "string";
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
161
|
+
return isRecord(value) && !Array.isArray(value);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Trim + casefold + whitespace collapse — the dedup key for array claims. */
|
|
165
|
+
function claimKey(s: string): string {
|
|
166
|
+
return s.trim().toLowerCase().replace(/\s+/g, " ");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Order-preserving dedup by claim key — shared by the engine caps path and the root tracker. */
|
|
170
|
+
export function dedupStrings(list: readonly string[]): string[] {
|
|
171
|
+
const seen = new Set<string>();
|
|
172
|
+
const out: string[] = [];
|
|
173
|
+
for (const s of list) {
|
|
174
|
+
const key = claimKey(s);
|
|
175
|
+
if (key === "" || seen.has(key)) continue;
|
|
176
|
+
seen.add(key);
|
|
177
|
+
out.push(s);
|
|
178
|
+
}
|
|
179
|
+
return out;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function firstKey(record: Record<string, unknown>): string | undefined {
|
|
183
|
+
for (const key of Object.keys(record)) return key;
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Whole-record strict merge with implicit-key-drop rejection (paper §5.7: 68% of small-model
|
|
189
|
+
* failures are premature key overwrites). A key present in `prev` but absent from `patch` is
|
|
190
|
+
* an ERROR, not a silent keep — deletion must be explicit (`null`). Incremental updates use
|
|
191
|
+
* dotted paths instead, which never hit this check.
|
|
192
|
+
*/
|
|
193
|
+
function strictMergeRecord(
|
|
194
|
+
prev: Readonly<Record<string, unknown>>,
|
|
195
|
+
patch: Readonly<Record<string, unknown>>,
|
|
196
|
+
path: string,
|
|
197
|
+
): Result<Record<string, unknown>, PatchError> {
|
|
198
|
+
for (const key of Object.keys(prev)) {
|
|
199
|
+
if (!(key in patch)) return err({ kind: "implicit-drop", path: `${path}.${key}` });
|
|
200
|
+
}
|
|
201
|
+
const out: Record<string, unknown> = { ...prev };
|
|
202
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
203
|
+
if (value === null) {
|
|
204
|
+
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete -- ⊕ explicit null = delete (paper §3.2)
|
|
205
|
+
delete out[key]; // explicit delete
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
const current = out[key];
|
|
209
|
+
if (isPlainObject(current) && isPlainObject(value)) {
|
|
210
|
+
const nested = strictMergeRecord(current, value, `${path}.${key}`);
|
|
211
|
+
if (!nested.ok) return nested;
|
|
212
|
+
out[key] = nested.value;
|
|
213
|
+
} else {
|
|
214
|
+
out[key] = value;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return ok(out);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Evict oldest-inserted entries past a record cap; writes refresh order (last-mention). */
|
|
221
|
+
function dropOldestRecord(record: Record<string, unknown>, cap: number): void {
|
|
222
|
+
while (Object.keys(record).length > cap) {
|
|
223
|
+
const oldest = firstKey(record);
|
|
224
|
+
if (oldest === undefined) return;
|
|
225
|
+
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete -- deterministic oldest-inserted eviction (§6.1)
|
|
226
|
+
delete record[oldest];
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function mutableArrayOf(draft: MutableState, field: string): string[] | undefined {
|
|
231
|
+
if (field === "findings") return draft.findings;
|
|
232
|
+
if (field === "verifiedFacts") return draft.verifiedFacts;
|
|
233
|
+
if (field === "openQuestions") return draft.openQuestions;
|
|
234
|
+
return undefined;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Deterministic caps enforcement — arrays → records → bytesTotal cascade (§6.1).
|
|
238
|
+
* Shared by the engine patch path and the root tracker (one implementation, DRY). */
|
|
239
|
+
export function enforceCaps(draft: MutableState): Result<RunState, PatchError> {
|
|
240
|
+
const findings = dedupStrings(draft.findings);
|
|
241
|
+
const verifiedFacts = dedupStrings(draft.verifiedFacts);
|
|
242
|
+
const openQuestions = dedupStrings(draft.openQuestions);
|
|
243
|
+
while (findings.length > RUN_STATE_LIMITS.findings) findings.shift();
|
|
244
|
+
while (verifiedFacts.length > RUN_STATE_LIMITS.verifiedFacts) verifiedFacts.shift();
|
|
245
|
+
while (openQuestions.length > RUN_STATE_LIMITS.openQuestions) openQuestions.shift();
|
|
246
|
+
dropOldestRecord(draft.testedApproaches, RUN_STATE_LIMITS.testedApproaches);
|
|
247
|
+
dropOldestRecord(draft.artifacts, RUN_STATE_LIMITS.artifacts);
|
|
248
|
+
const capped: MutableState = {
|
|
249
|
+
task: draft.task,
|
|
250
|
+
nextStep: draft.nextStep,
|
|
251
|
+
updatedAt: draft.updatedAt,
|
|
252
|
+
findings,
|
|
253
|
+
verifiedFacts,
|
|
254
|
+
openQuestions,
|
|
255
|
+
testedApproaches: { ...draft.testedApproaches },
|
|
256
|
+
artifacts: { ...draft.artifacts },
|
|
257
|
+
};
|
|
258
|
+
// bytesTotal: |Σ_t| must never exceed κ_Σ — deterministic eviction order keeps runs flat.
|
|
259
|
+
let guard = 0;
|
|
260
|
+
while (JSON.stringify(capped).length > RUN_STATE_LIMITS.bytesTotal && guard++ < 10_000) {
|
|
261
|
+
if (capped.findings.length > 0) {
|
|
262
|
+
capped.findings = capped.findings.slice(1);
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
if (capped.verifiedFacts.length > 0) {
|
|
266
|
+
capped.verifiedFacts = capped.verifiedFacts.slice(1);
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
if (capped.openQuestions.length > 0) {
|
|
270
|
+
capped.openQuestions = capped.openQuestions.slice(1);
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
const approach = firstKey(capped.testedApproaches);
|
|
274
|
+
if (approach !== undefined) {
|
|
275
|
+
const rest = { ...capped.testedApproaches };
|
|
276
|
+
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete -- cap eviction (§6.1)
|
|
277
|
+
delete rest[approach];
|
|
278
|
+
capped.testedApproaches = rest;
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
const artifact = firstKey(capped.artifacts);
|
|
282
|
+
if (artifact !== undefined) {
|
|
283
|
+
const rest = { ...capped.artifacts };
|
|
284
|
+
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete -- cap eviction (§6.1)
|
|
285
|
+
delete rest[artifact];
|
|
286
|
+
capped.artifacts = rest;
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
return err({ kind: "cap", field: "bytesTotal" });
|
|
290
|
+
}
|
|
291
|
+
// Guard over cast: every write path validated entry shapes, but the record index types
|
|
292
|
+
// widened during mutation — re-narrow deterministically before handing Σ back.
|
|
293
|
+
const finalized: unknown = capped;
|
|
294
|
+
if (!isRunState(finalized)) {
|
|
295
|
+
return err({ kind: "schema", detail: "post-cap state failed validation" });
|
|
296
|
+
}
|
|
297
|
+
return ok(finalized);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** Apply one patch key (dotted path + optional array op) to the mutable draft. */
|
|
301
|
+
function applyKey(draft: MutableState, rawKey: string, value: unknown): Result<null, PatchError> {
|
|
302
|
+
const m = PATCH_KEY.exec(rawKey);
|
|
303
|
+
if (m === null) return err({ kind: "schema", detail: `malformed patch key "${rawKey}"` });
|
|
304
|
+
const root = m[1];
|
|
305
|
+
if (!RUN_STATE_FIELDS.has(root)) {
|
|
306
|
+
return err({ kind: "schema", detail: `unknown state field "${root}"` });
|
|
307
|
+
}
|
|
308
|
+
if (root === "updatedAt") {
|
|
309
|
+
return err({ kind: "schema", detail: "updatedAt is runtime-stamped and cannot be patched" });
|
|
310
|
+
}
|
|
311
|
+
const middles = m[2] === "" ? [] : m[2].slice(1).split(".");
|
|
312
|
+
const op = m[3];
|
|
313
|
+
|
|
314
|
+
// Scalars: task / nextStep — string replace with a length cap.
|
|
315
|
+
if (middles.length === 0 && (root === "task" || root === "nextStep")) {
|
|
316
|
+
if (typeof value !== "string") {
|
|
317
|
+
return err({ kind: "type", path: rawKey, expected: "string" });
|
|
318
|
+
}
|
|
319
|
+
const cap = root === "task" ? TASK_MAX_CHARS : NEXT_STEP_MAX_CHARS;
|
|
320
|
+
if (value.length > cap) return err({ kind: "cap", field: root });
|
|
321
|
+
if (root === "task") draft.task = value;
|
|
322
|
+
else draft.nextStep = value;
|
|
323
|
+
return ok(null);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Arrays (top level only): full replace, `[+]` append, or `[N]` set.
|
|
327
|
+
if (middles.length === 0 && ARRAY_FIELDS.has(root)) {
|
|
328
|
+
const list = arrayOf(draft, root);
|
|
329
|
+
if (list === undefined) {
|
|
330
|
+
return err({ kind: "schema", detail: `unknown array field "${root}"` });
|
|
331
|
+
}
|
|
332
|
+
if (op === "[+]") {
|
|
333
|
+
if (typeof value !== "string") return err({ kind: "type", path: rawKey, expected: "string" });
|
|
334
|
+
list.push(value);
|
|
335
|
+
} else if (op !== undefined) {
|
|
336
|
+
const index = Number.parseInt(op.slice(1, -1), 10);
|
|
337
|
+
if (value === null) {
|
|
338
|
+
// Explicit delete by index (must not leave a hole).
|
|
339
|
+
if (index >= list.length) {
|
|
340
|
+
return err({ kind: "schema", detail: `${rawKey} out of range (len=${list.length})` });
|
|
341
|
+
}
|
|
342
|
+
list.splice(index, 1);
|
|
343
|
+
return ok(null);
|
|
344
|
+
}
|
|
345
|
+
if (typeof value !== "string") return err({ kind: "type", path: rawKey, expected: "string" });
|
|
346
|
+
if (index > list.length) {
|
|
347
|
+
return err({ kind: "schema", detail: `${rawKey} would leave a hole (len=${list.length})` });
|
|
348
|
+
}
|
|
349
|
+
list[index] = value;
|
|
350
|
+
} else {
|
|
351
|
+
if (!isStringArray(value)) return err({ kind: "type", path: rawKey, expected: "string[]" });
|
|
352
|
+
list.length = 0;
|
|
353
|
+
list.push(...dedupStrings(value));
|
|
354
|
+
}
|
|
355
|
+
return ok(null);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Records: `testedApproaches` / `artifacts`.
|
|
359
|
+
if (!isRecordField(root)) {
|
|
360
|
+
return err({ kind: "schema", detail: `field "${root}" takes no sub-paths` });
|
|
361
|
+
}
|
|
362
|
+
if (middles.length === 0 && op !== undefined) {
|
|
363
|
+
return err({ kind: "type", path: rawKey, expected: "object (records use dotted keys)" });
|
|
364
|
+
}
|
|
365
|
+
if (middles.length === 0) {
|
|
366
|
+
// Whole-record patch — strict merge, implicit drops rejected (§5.7 guard).
|
|
367
|
+
if (value === null) {
|
|
368
|
+
if (root === "testedApproaches") draft.testedApproaches = {};
|
|
369
|
+
else draft.artifacts = {};
|
|
370
|
+
return ok(null);
|
|
371
|
+
}
|
|
372
|
+
if (!isPlainObject(value)) return err({ kind: "type", path: rawKey, expected: "object" });
|
|
373
|
+
const prev = root === "testedApproaches" ? draft.testedApproaches : draft.artifacts;
|
|
374
|
+
const merged = strictMergeRecord(prev, value, root);
|
|
375
|
+
if (!merged.ok) return merged;
|
|
376
|
+
if (root === "testedApproaches") draft.testedApproaches = merged.value;
|
|
377
|
+
else draft.artifacts = merged.value;
|
|
378
|
+
return ok(null);
|
|
379
|
+
}
|
|
380
|
+
if (op !== undefined) {
|
|
381
|
+
return err({ kind: "schema", detail: `array ops are not valid inside record "${root}"` });
|
|
382
|
+
}
|
|
383
|
+
// Dotted entry write: create-or-replace one entry; null deletes it.
|
|
384
|
+
const record: Record<string, unknown> =
|
|
385
|
+
root === "testedApproaches" ? { ...draft.testedApproaches } : { ...draft.artifacts };
|
|
386
|
+
let cursor = record;
|
|
387
|
+
for (let i = 0; i < middles.length - 1; i++) {
|
|
388
|
+
const segment = middles[i];
|
|
389
|
+
const next: unknown = cursor[segment];
|
|
390
|
+
if (next === undefined) {
|
|
391
|
+
const created: Record<string, unknown> = {};
|
|
392
|
+
cursor[segment] = created;
|
|
393
|
+
cursor = created;
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
if (!isPlainObject(next)) {
|
|
397
|
+
return err({ kind: "type", path: rawKey, expected: "object" });
|
|
398
|
+
}
|
|
399
|
+
cursor = next;
|
|
400
|
+
}
|
|
401
|
+
const leaf = middles[middles.length - 1];
|
|
402
|
+
if (value === null) {
|
|
403
|
+
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete -- ⊕ null = delete (paper §3.2)
|
|
404
|
+
delete cursor[leaf];
|
|
405
|
+
commitRecord(draft, root, record);
|
|
406
|
+
return ok(null);
|
|
407
|
+
}
|
|
408
|
+
if (root === "testedApproaches") {
|
|
409
|
+
if (!isPlainObject(value) || !isApproachOutcome(value)) {
|
|
410
|
+
return err({ kind: "type", path: rawKey, expected: "ApproachOutcome" });
|
|
411
|
+
}
|
|
412
|
+
const prev: unknown = cursor[leaf];
|
|
413
|
+
const merged = isPlainObject(prev)
|
|
414
|
+
? strictMergeRecord(prev, value, rawKey)
|
|
415
|
+
: ok<Record<string, unknown>, PatchError>({ ...value });
|
|
416
|
+
if (!merged.ok) return merged;
|
|
417
|
+
refreshOrder(cursor, leaf, merged.value); // last-mention ordering for eviction
|
|
418
|
+
} else {
|
|
419
|
+
if (typeof value !== "string") return err({ kind: "type", path: rawKey, expected: "string" });
|
|
420
|
+
refreshOrder(cursor, leaf, value);
|
|
421
|
+
}
|
|
422
|
+
commitRecord(draft, root, record);
|
|
423
|
+
return ok(null);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function isRecordField(root: string): root is "testedApproaches" | "artifacts" {
|
|
427
|
+
return root === "testedApproaches" || root === "artifacts";
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function arrayOf(draft: MutableState, root: string): string[] | undefined {
|
|
431
|
+
return mutableArrayOf(draft, root);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function commitRecord(draft: MutableState, root: "testedApproaches" | "artifacts", record: Record<string, unknown>): void {
|
|
435
|
+
if (root === "testedApproaches") draft.testedApproaches = record;
|
|
436
|
+
else draft.artifacts = record;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/** Rewrite a record entry so its insertion order reflects the latest mention. */
|
|
440
|
+
function refreshOrder(record: Record<string, unknown>, key: string, value: unknown): void {
|
|
441
|
+
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete -- last-mention insertion-order refresh (§6.1)
|
|
442
|
+
delete record[key];
|
|
443
|
+
record[key] = value;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* V(ΔΣ_t, Σ_t) → Result<Σ_{t+1}, PatchError> — the deterministic, runtime-side validator
|
|
448
|
+
* (paper §7). Invalid patches never touch the previous state (rollback is structural: the
|
|
449
|
+
* draft is discarded). `t` stamps `updatedAt` (iteration counter).
|
|
450
|
+
*/
|
|
451
|
+
export function applyPatch(prev: RunState, patch: unknown, t: number): Result<RunState, PatchError> {
|
|
452
|
+
if (!isStatePatch(patch)) {
|
|
453
|
+
return err({ kind: "schema", detail: 'patch must be {"state_patch": {...}}' });
|
|
454
|
+
}
|
|
455
|
+
const ops = Object.entries(patch.state_patch);
|
|
456
|
+
if (ops.length === 0) return err({ kind: "schema", detail: "empty state_patch" });
|
|
457
|
+
// Per-turn byte cap (bench rec #1): reject BEFORE the draft clone — a verbose restatement
|
|
458
|
+
// must come back as an error observation, never silently eat output tokens.
|
|
459
|
+
if (JSON.stringify(patch).length > RUN_STATE_LIMITS.patchBytes) {
|
|
460
|
+
return err({ kind: "cap", field: "patchBytes" });
|
|
461
|
+
}
|
|
462
|
+
// JSON round-trip clone: RunState is JSON-safe by construction and stays well under κ_Σ.
|
|
463
|
+
const draft = JSON.parse(JSON.stringify(prev)) as MutableState;
|
|
464
|
+
for (const [key, value] of ops) {
|
|
465
|
+
const applied = applyKey(draft, key, value);
|
|
466
|
+
if (!applied.ok) return applied;
|
|
467
|
+
}
|
|
468
|
+
draft.updatedAt = t;
|
|
469
|
+
return enforceCaps(draft);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/** Human-facing patch rejection — becomes the next O_t prefix (error-as-observation). */
|
|
473
|
+
export function patchErrorText(error: PatchError): string {
|
|
474
|
+
switch (error.kind) {
|
|
475
|
+
case "schema":
|
|
476
|
+
return `schema violation: ${error.detail}`;
|
|
477
|
+
case "type":
|
|
478
|
+
return `type mismatch at "${error.path}" (expected ${error.expected})`;
|
|
479
|
+
case "implicit-drop":
|
|
480
|
+
return `implicit key drop at "${error.path}" — restate the key or delete it with null`;
|
|
481
|
+
case "cap":
|
|
482
|
+
return `cap exceeded on "${error.field}"`;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Apply every ```state fence found in a turn response, in document order, to an ACTIVE mode.
|
|
488
|
+
* Rejections accumulate into one observation (the next O_t prefix); once failures exceed
|
|
489
|
+
* `runStateRetryMax` the mode DEGRADES — the engine then behaves exactly as built.
|
|
490
|
+
*
|
|
491
|
+
* Idle degrade (bench rec #2): when `fenceRequested` is true (the turn conditioned on Σ)
|
|
492
|
+
* and zero patches were accepted, the turn is IDLE — Σ inflated the prompt for nothing.
|
|
493
|
+
* `RUN_STATE_IDLE_DEGRADE_TURNS` consecutive idle turns degrade, same as-built outcome.
|
|
494
|
+
*/
|
|
495
|
+
export function applyStatePatches(
|
|
496
|
+
mode: Extract<RunStateMode, { kind: "active" }>,
|
|
497
|
+
parsed: readonly StateFenceResult[],
|
|
498
|
+
iteration: number,
|
|
499
|
+
config: Pick<RlmConfig, "runStateRetryMax">,
|
|
500
|
+
fenceRequested = false,
|
|
501
|
+
): { readonly mode: RunStateMode; readonly observation: string | undefined } {
|
|
502
|
+
// Identity fast-path: a fence-free turn on a run that never asked for fences changes
|
|
503
|
+
// nothing — keep the same mode object (callers may compare identity).
|
|
504
|
+
if (parsed.length === 0 && !fenceRequested) return { mode, observation: undefined };
|
|
505
|
+
let state = mode.state;
|
|
506
|
+
let retries = mode.retries;
|
|
507
|
+
let accepted = 0;
|
|
508
|
+
const problems: string[] = [];
|
|
509
|
+
for (const fence of parsed) {
|
|
510
|
+
if (!fence.ok) {
|
|
511
|
+
retries += 1;
|
|
512
|
+
problems.push(malformedFenceProblem(fence.error));
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
515
|
+
const result = applyPatch(state, fence.value, iteration);
|
|
516
|
+
if (result.ok) {
|
|
517
|
+
state = result.value;
|
|
518
|
+
accepted += 1;
|
|
519
|
+
} else {
|
|
520
|
+
retries += 1;
|
|
521
|
+
problems.push(patchErrorText(result.error));
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
// Bench rec #2: accepted deltas reset the idle streak; a requested-but-empty turn grows it.
|
|
525
|
+
const idle = accepted > 0 || !fenceRequested ? 0 : mode.idle + 1;
|
|
526
|
+
let nextMode: RunStateMode = { kind: "active", state, retries, idle };
|
|
527
|
+
if (retries > config.runStateRetryMax) {
|
|
528
|
+
nextMode = {
|
|
529
|
+
kind: "degraded",
|
|
530
|
+
reason: `runStateRetryMax exceeded (${retries} rejected state patches)`,
|
|
531
|
+
};
|
|
532
|
+
} else if (idle >= RUN_STATE_IDLE_DEGRADE_TURNS) {
|
|
533
|
+
nextMode = {
|
|
534
|
+
kind: "degraded",
|
|
535
|
+
reason: `idle degrade — ${idle} consecutive fence-requested turns with zero accepted deltas`,
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
return { mode: nextMode, observation: statePatchObservation(problems) };
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/** Problem entry for an unparsable fence body — shared by both fence loops (one wording). */
|
|
542
|
+
export function malformedFenceProblem(error: string): string {
|
|
543
|
+
return `malformed \`\`\`state fence (${error})`;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/** ONE wording source for patch-rejection observations — engine turns AND root fences. */
|
|
547
|
+
export function statePatchObservation(problems: readonly string[]): string | undefined {
|
|
548
|
+
return problems.length === 0
|
|
549
|
+
? undefined
|
|
550
|
+
: `${formatError(`state patch rejected — rolled back (${problems.join("; ")})`)}\n` +
|
|
551
|
+
"Fix the ```state fence and re-commit; the raw observation will not be shown again.";
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
export const STATE_FENCE_INSTRUCTION: string =
|
|
555
|
+
"[state] Alongside your ```repl block(s), commit durable progress to Σ with a ```state fence:\n" +
|
|
556
|
+
'{"state_patch": {"verifiedFacts[+]": "src/x.ts — fact", "testedApproaches.h1.status": "failed"}}\n' +
|
|
557
|
+
"Keys: dotted paths write record leaves; [+] appends; [N] sets an array slot; null deletes.\n" +
|
|
558
|
+
"Commit DELTAS only — never restate unchanged records or arrays; touch single dotted keys " +
|
|
559
|
+
`or append with [+]. Whole-record restatements must keep EVERY key (implicit drops are ` +
|
|
560
|
+
`rejected), and a patch over ${RUN_STATE_LIMITS.patchBytes} bytes is rejected whole.\n` +
|
|
561
|
+
"Commit anything possibly relevant NOW; the raw observation will not be shown again.";
|
|
562
|
+
|
|
563
|
+
/** ONE Σ-block composer (R2): the engine turn block and the root splice both delegate here —
|
|
564
|
+
* never duplicate the contract + Σ assembly. `withContract` is paper A.4 authoring mode;
|
|
565
|
+
* without it, observation-only mode. */
|
|
566
|
+
function sigmaBlock(state: RunState, withContract: boolean): string {
|
|
567
|
+
const sigma = `[Σ] ${compactJSON(state)}`;
|
|
568
|
+
return withContract ? `${STATE_FENCE_INSTRUCTION}\n\n${sigma}` : sigma;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/** The per-turn A_t block: the fence contract + the current Σ (paper A_t = (P, Σ_t, O_t)). */
|
|
572
|
+
export function runStateTurnBlock(state: RunState): string {
|
|
573
|
+
return sigmaBlock(state, true);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Root Σ (WS-3b): the ROOT's A_t block. R2 (G2): with `withContract` the splice carries the
|
|
578
|
+
* SAME fence contract (delegating to the one composer above — no re-wording), making the
|
|
579
|
+
* splice paper-faithful A.4 authoring mode; without it, byte-identical to the v1
|
|
580
|
+
* observation-only snapshot. Recall line comes from the glossary (one wording source).
|
|
581
|
+
*/
|
|
582
|
+
export function runStateRootBlock(state: RunState, opts?: { readonly withContract?: boolean }): string {
|
|
583
|
+
return sigmaBlock(state, opts?.withContract === true) +
|
|
584
|
+
"\n" +
|
|
585
|
+
"Fresh tool results outrank Σ when they disagree.\n" +
|
|
586
|
+
`[Project facts recall: skill_search()] — ${SKILL_RECALL_LINE}`;
|
|
587
|
+
}
|
package/src/core/types.ts
CHANGED
|
@@ -96,25 +96,54 @@ export interface RlmConfig {
|
|
|
96
96
|
readonly enableLedger: boolean;
|
|
97
97
|
/** Real rlm spawns allowed before extra rlm_query demotes to llm_query (0 = never). */
|
|
98
98
|
readonly rlmBudget: number;
|
|
99
|
-
/** v5 durable memory: L1 episode replay + L2 BM25 notes under `<root>/.rlm/memory`. */
|
|
100
|
-
readonly enableMemory: boolean;
|
|
101
|
-
/** Char budget for the `[memory]` injection = tokens × 4. */
|
|
102
|
-
readonly injectNoteTokens: number;
|
|
103
|
-
/** Pending episodes per L2 consolidation batch (0 = never auto-consolidate). */
|
|
104
|
-
readonly evolveEvery: number;
|
|
105
|
-
/** Override the memory dir. `null` (default) = `<root>/.rlm/memory` — this field only
|
|
106
|
-
* RELOCATES the store; the on/off switch is `enableMemory` (audit M3). */
|
|
107
|
-
readonly memoryDir: string | null;
|
|
108
99
|
/** v5: per-provider concurrent-request caps (e.g. `{ zai: 4 }`). Caps only lower limits. */
|
|
109
100
|
readonly providerMaxConcurrent?: Readonly<Record<string, number>>;
|
|
110
|
-
/** v5 doctrine: "delegation" = child engines get llm/memory/ledger only (no repo retrieval);
|
|
111
|
-
* "legacy" keeps today's full child surface as a one-flip rollback. */
|
|
112
|
-
readonly childSurface: "delegation" | "legacy";
|
|
113
101
|
/** Verification-discipline nudge (default OFF — it changes interactive behavior): when the
|
|
114
102
|
* root finalizes before turn 4 with a bare number / short label, it gets ONE coached redo
|
|
115
103
|
* ("recompute and sanity-check in Python") instead of accepting the answer. Opt-in via
|
|
116
104
|
* rlm.json; evidence: 28/33 bench failures were early confident wrong answers. */
|
|
117
105
|
readonly enableVerificationNudge?: boolean;
|
|
106
|
+
// ── SKILL.state integration (Workstreams A–F) ──
|
|
107
|
+
/** Structured execution state Σ_t in headless runs (paper §3); false = as-built append-only
|
|
108
|
+
* history + prose compaction. Degraded automatically on malformed-patch retry exhaustion. */
|
|
109
|
+
readonly enableRunState: boolean;
|
|
110
|
+
/** Failed state-patch applications tolerated before the run degrades to as-built behavior. */
|
|
111
|
+
readonly runStateRetryMax: number;
|
|
112
|
+
/** Cross-session SkillState store: Ξ prompt injection (C), leaf grounding (D), skill_search (E). */
|
|
113
|
+
readonly enableSkillState: boolean;
|
|
114
|
+
/** Opt-in: one cheap leaf call at run finalize to phrase A-Mem-style notes from the run. */
|
|
115
|
+
readonly enableSkillStateDistill: boolean;
|
|
116
|
+
/** Token budget for the injected Ξ skill block (Workstream C). */
|
|
117
|
+
readonly skillStateMaxTokens: number;
|
|
118
|
+
/** Per-leaf-call grounding budget in tokens (Workstream D). */
|
|
119
|
+
readonly skillStateLeafTokens: number;
|
|
120
|
+
/** BM25 score a note must clear before a leaf prompt gets grounded (below ⇒ byte-identical). */
|
|
121
|
+
readonly skillStateMinScore: number;
|
|
122
|
+
/** Per-project note cap; LRU by ts with the top-hits quartile pinned (Workstream B). */
|
|
123
|
+
readonly skillStateNotesPerProject: number;
|
|
124
|
+
|
|
125
|
+
// ── Root Σ integration (Root Σ plan WS-2..WS-4) ──
|
|
126
|
+
/** Deterministic root compaction: when the Pi session compacts, supply a no-LLM structural
|
|
127
|
+
* digest ([Task]/[Findings]/[State]/[Next]/[Project facts]) instead of Pi's LLM prose
|
|
128
|
+
* summarizer. Fail-soft: any error falls back to the host path. */
|
|
129
|
+
readonly enableRootDigestCompaction: boolean;
|
|
130
|
+
/** Verbatim tail (chars) the root digest keeps out of the digest span — entries after the
|
|
131
|
+
* chosen cut stay byte-identical in context (never summarized). */
|
|
132
|
+
readonly rootDigestKeepRecentChars: number;
|
|
133
|
+
/** Cap for the generated digest string; over-cap drops sections in continuity order. */
|
|
134
|
+
readonly rootDigestMaxChars: number;
|
|
135
|
+
/** Per-LLM-call root A_t assembly via the `context` event (WS-3): elide stale tool
|
|
136
|
+
* payloads (paper §5.3 discard) + splice the fresh Σ snapshot. Default OFF until soak. */
|
|
137
|
+
readonly enableRootContextTransform: boolean;
|
|
138
|
+
/** Newest assistant turns kept verbatim by the root elision (mirrors engine keepTurns). */
|
|
139
|
+
readonly rootContextKeepTurns: number;
|
|
140
|
+
/** Tool-result payloads older than the keep window are preview-capped at this many chars. */
|
|
141
|
+
readonly rootContextElideChars: number;
|
|
142
|
+
/** Splice the RootStateTracker Σ snapshot before the last user message each call. */
|
|
143
|
+
readonly rootContextSnapshot: boolean;
|
|
144
|
+
/** WS-4.2 (default OFF, paper §5.7 fence tax): the root may commit ΔΣ_t via a ```state
|
|
145
|
+
* fence in a normal reply; patches ride the same V(ΔΣ_t,Σ_t) ladder as engine runs. */
|
|
146
|
+
readonly enableRootStateFences: boolean;
|
|
118
147
|
}
|
|
119
148
|
|
|
120
149
|
/** Input to a (headless) RLM run. */
|
|
@@ -135,6 +164,16 @@ export interface RlmInput {
|
|
|
135
164
|
/** v5: the shared TaskLedger blackboard. Children inherit the parent's instance —
|
|
136
165
|
* set by childRun (the one child-RlmInput construction path); a fresh run gets a new one. */
|
|
137
166
|
readonly ledger?: import("./ledger.ts").TaskLedger;
|
|
167
|
+
/** SKILL.state Ξ (Workstream C) — the BM25-selected SkillState block this run conditions on.
|
|
168
|
+
* Set by the composition root / copied by childRun (DRY #6 — one construction site). */
|
|
169
|
+
readonly skillBlock?: string;
|
|
170
|
+
/** History-as-deliverable opt-out (paper §7-c): when true, RunState never activates — the
|
|
171
|
+
* archive is the product (audit / provenance / debug-narrative runs). */
|
|
172
|
+
readonly narrative?: boolean;
|
|
173
|
+
/** Workstream F: a rectification chosen by the prior run at its budget hard-state, applied to
|
|
174
|
+
* THIS continuation invocation (narrowed child paths / reduced leaf admission). DOCTRINE:
|
|
175
|
+
* never a model/provider switch — a failing model retries to exhaustion and fails. */
|
|
176
|
+
readonly rectification?: import("./budget.ts").RectifyAction;
|
|
138
177
|
}
|
|
139
178
|
|
|
140
179
|
/** Result of a completed RLM run. */
|