@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/src/core/engine.ts
CHANGED
|
@@ -16,11 +16,10 @@ import {
|
|
|
16
16
|
createTaskRegistry,
|
|
17
17
|
type Invocation,
|
|
18
18
|
} from "../bridge/handlers/index.ts";
|
|
19
|
-
import { TaskLedger
|
|
20
|
-
import { type MemoryStore, rootContextPaths } from "./memory.ts";
|
|
19
|
+
import { TaskLedger } from "./ledger.ts";
|
|
21
20
|
import { type ChatMsg, modelComplete } from "../bridge/model.ts";
|
|
22
21
|
import { buildRlmSystemPrompt } from "../prompts/system.ts";
|
|
23
|
-
import { buildTurnPrompt, FINALIZE_PROMPT, RETRIEVAL_NUDGE } from "../prompts/user.ts";
|
|
22
|
+
import { buildTurnPrompt, FINALIZE_PROMPT, RETRIEVAL_NUDGE, REASONING_BUDGET_HINT, VERIFICATION_NUDGE } from "../prompts/user.ts";
|
|
24
23
|
import type { RlmEmitter } from "../tool/rlm-events.ts";
|
|
25
24
|
import type { SubcallPhase } from "../tool/rlm-details.ts";
|
|
26
25
|
import { PythonSandbox, SANDBOX_WATCHDOG_HEARTBEAT_MS } from "../sandbox/sandbox.ts";
|
|
@@ -30,12 +29,32 @@ import { previewStdout, previewText } from "../text/preview.ts";
|
|
|
30
29
|
import { findReplBlocks } from "../text/parsing.ts";
|
|
31
30
|
import { contextLength, contextSizeStats, contextTypeLabel } from "../text/tokens.ts";
|
|
32
31
|
import { finalAnswerOf, formatReplOutputs, latestAnswerContentOf, turnHadError } from "./answer.ts";
|
|
33
|
-
import { compactHistory, elideOldToolPayloads, shouldCompact } from "./compaction.ts";
|
|
32
|
+
import { compactHistory, elideOldToolPayloads, rebaseWithState, shouldCompact } from "./compaction.ts";
|
|
33
|
+
import { applyStatePatches, freshRunState, runStateTurnBlock, type RunState, type RunStateMode } from "./run-state.ts";
|
|
34
|
+
import { findStatePatches } from "../text/parsing.ts";
|
|
35
|
+
import { complete1, completeDeps } from "../bridge/handlers/completion.ts";
|
|
36
|
+
import type { SubcallHandlerDeps } from "../bridge/handlers/types.ts";
|
|
37
|
+
import {
|
|
38
|
+
distillPromptFor,
|
|
39
|
+
groundLeafPrompt,
|
|
40
|
+
notesFromRunState,
|
|
41
|
+
parseDistilledNotes,
|
|
42
|
+
skillSearchHandler,
|
|
43
|
+
type SkillStore,
|
|
44
|
+
} from "../config/skillstate.ts";
|
|
34
45
|
import { retryPolicy } from "../util/retry.ts";
|
|
35
46
|
import { appendUserMessage } from "./history.ts";
|
|
36
47
|
import { runTurn } from "./iteration.ts";
|
|
37
48
|
import { type Limits, LimitError, LimitGuard } from "./limits.ts";
|
|
38
|
-
import {
|
|
49
|
+
import {
|
|
50
|
+
continuationPrompt,
|
|
51
|
+
distillTrajectory,
|
|
52
|
+
rectify,
|
|
53
|
+
rectifyLabel,
|
|
54
|
+
resolveBudget,
|
|
55
|
+
stateHandoff,
|
|
56
|
+
WRAP_UP_BUDGET,
|
|
57
|
+
} from "./budget.ts";
|
|
39
58
|
import { ModelContextRegistry, modelsCachePath } from "./model-registry.ts";
|
|
40
59
|
import type { RlmConfig, RlmInput, RlmResult, RunRlm, Sampling } from "./types.ts";
|
|
41
60
|
import { createSubcallGates, type SubcallGates } from "../util/concurrency.ts";
|
|
@@ -46,11 +65,9 @@ import { createSubcallGates, type SubcallGates } from "../util/concurrency.ts";
|
|
|
46
65
|
* hold a finished run open on work whose result nobody can receive.
|
|
47
66
|
*/
|
|
48
67
|
const DETACHED_SETTLE_MS = 5_000;
|
|
49
|
-
/**
|
|
50
|
-
*
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
68
|
+
/** Verification nudge (enableVerificationNudge): only an EARLY finalize is suspicious — from
|
|
69
|
+
* turn 4 on, a bare answer is just... an answer. "Before iteration ~4", per the bench data. */
|
|
70
|
+
const VERIFICATION_NUDGE_TURN_CAP = 4;
|
|
54
71
|
export interface EngineDeps {
|
|
55
72
|
readonly model: Model<Api>;
|
|
56
73
|
readonly llmModel: Model<Api>;
|
|
@@ -66,8 +83,13 @@ export interface EngineDeps {
|
|
|
66
83
|
readonly onUsage?: (usage: Usage, role: "root" | "sub") => void;
|
|
67
84
|
/** Test-only: override model completion (scripted multi-turn responses). */
|
|
68
85
|
readonly complete?: import("./iteration.ts").CompleteFn;
|
|
69
|
-
/**
|
|
70
|
-
|
|
86
|
+
/** SKILL.state (Workstream B): session-scoped distilled-knowledge store. Omitted ⇒ no Ξ
|
|
87
|
+
* harvest, no leaf grounding, no skill_search — zero behavior change. */
|
|
88
|
+
readonly skillStore?: SkillStore;
|
|
89
|
+
/** Root Σ (WS-4): observer for the FINAL Σ of accepted runs — the root session tracker
|
|
90
|
+
* mirrors it (one source, two sinks: the SkillStore harvest below + the tracker). Fires
|
|
91
|
+
* for child engines (depth > 0) too; the tracker's dedup keeps the volume bounded. */
|
|
92
|
+
readonly onRunState?: (state: RunState) => void;
|
|
71
93
|
}
|
|
72
94
|
|
|
73
95
|
/** Build a `runRlm` bound to the given deps. The returned function is reused for recursion. */
|
|
@@ -84,6 +106,7 @@ export function createEngine(deps: EngineDeps): RunRlm {
|
|
|
84
106
|
}
|
|
85
107
|
|
|
86
108
|
const model = deps.model;
|
|
109
|
+
const skillStore = deps.skillStore;
|
|
87
110
|
|
|
88
111
|
// Create LimitGuard BEFORE the bridge so sub-LLM usage feeds into it.
|
|
89
112
|
// Children inherit the parent's remaining timeout (propagated as remaining amount, not
|
|
@@ -139,54 +162,26 @@ export function createEngine(deps: EngineDeps): RunRlm {
|
|
|
139
162
|
// childRun (RlmInput.ledger — the one construction seam, DRY #6).
|
|
140
163
|
const runLedger = input.ledger ?? new TaskLedger();
|
|
141
164
|
if (deps.config.enableLedger) runLedger.beginRun(input.rootPrompt);
|
|
165
|
+
// ── Workstream A: Σ_t — the structured execution state (paper §3). Degraded ⇒ the run
|
|
166
|
+
// behaves exactly as built. History-as-deliverable runs (narrative) keep their archive
|
|
167
|
+
// as the product — RunState never activates there (§12.1).
|
|
168
|
+
let runStateMode: RunStateMode =
|
|
169
|
+
deps.config.enableRunState && input.narrative !== true
|
|
170
|
+
? { kind: "active", state: freshRunState(input.rootPrompt.slice(0, 200)), retries: 0, idle: 0 }
|
|
171
|
+
: { kind: "degraded", reason: input.narrative === true ? "narrative" : "disabled" };
|
|
142
172
|
|
|
143
|
-
//
|
|
144
|
-
//
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
return {
|
|
154
|
-
answer: hit.result,
|
|
155
|
-
iterations: 0,
|
|
156
|
-
costUsd: 0,
|
|
157
|
-
inputTokens: 0,
|
|
158
|
-
outputTokens: 0,
|
|
159
|
-
durationMs: 0,
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
const persistRoot = (
|
|
164
|
-
answer: string,
|
|
165
|
-
spend?: { readonly inputTokens: number; readonly outputTokens: number },
|
|
166
|
-
): void => {
|
|
167
|
-
if (rootMemory === undefined || input.depth !== 0) return;
|
|
168
|
-
// H2 (audit): only clean root runs persist — a continuation leaf carries the ORIGINAL
|
|
169
|
-
// run's key (it persists the chain itself), and stopped/aborted partials must never
|
|
170
|
-
// replay as if they were real answers.
|
|
171
|
-
if (input.budget !== undefined) return;
|
|
172
|
-
if (answer === "" || answer === "(aborted)" || answer.startsWith("(stopped")) return;
|
|
173
|
-
const u = spend ?? limits.usage();
|
|
174
|
-
rootMemory.recordEpisode({
|
|
175
|
-
key: rootKey,
|
|
176
|
-
kind: "root",
|
|
177
|
-
model: modelRefStr,
|
|
178
|
-
prompt: input.rootPrompt,
|
|
179
|
-
paths: rootContextPaths(input.context, ROOT_HASH_MAX),
|
|
180
|
-
result: answer,
|
|
181
|
-
tokensIn: u.inputTokens,
|
|
182
|
-
tokensOut: u.outputTokens,
|
|
183
|
-
});
|
|
184
|
-
};
|
|
185
|
-
|
|
186
|
-
const subcalls = createSubcallHandlers({
|
|
173
|
+
// Workstream F: a rectified continuation may shrink leaf admission for THIS run only —
|
|
174
|
+
// the choice rides in on input.rectification (deterministic, logged). DOCTRINE: the
|
|
175
|
+
// model/provider pair is never a rectification axis — a failing model retries on itself
|
|
176
|
+
// until the attempt budget is exhausted, then fails loudly. No silent fallback.
|
|
177
|
+
const leafLimit = input.rectification?.kind === "reduce-concurrency"
|
|
178
|
+
? input.rectification.maxConcurrentSubcalls
|
|
179
|
+
: deps.config.maxConcurrentSubcalls;
|
|
180
|
+
const gates = deps.gates
|
|
181
|
+
?? createSubcallGates(leafLimit, deps.config.maxConcurrentChildren);
|
|
182
|
+
const subcallDeps = {
|
|
187
183
|
resolve: () => invocation,
|
|
188
|
-
gates
|
|
189
|
-
?? createSubcallGates(deps.config.maxConcurrentSubcalls, deps.config.maxConcurrentChildren),
|
|
184
|
+
gates,
|
|
190
185
|
registry: deps.registry,
|
|
191
186
|
getLlmModel: () => deps.llmModel,
|
|
192
187
|
getModel: () => model,
|
|
@@ -198,7 +193,6 @@ export function createEngine(deps: EngineDeps): RunRlm {
|
|
|
198
193
|
// interrupt during runTurn, which is strictly after loadContext below.
|
|
199
194
|
getChildContext: () => liveContext,
|
|
200
195
|
ledger: runLedger,
|
|
201
|
-
memory: rootMemory,
|
|
202
196
|
trackDetached: async (task) => {
|
|
203
197
|
detachedInFlight += 1;
|
|
204
198
|
try {
|
|
@@ -208,7 +202,39 @@ export function createEngine(deps: EngineDeps): RunRlm {
|
|
|
208
202
|
if (detachedInFlight === 0) detachedIdle?.();
|
|
209
203
|
}
|
|
210
204
|
},
|
|
211
|
-
|
|
205
|
+
// SKILL.state: leaf grounding (Workstream D, DRY #1) + the parent Ξ for children (C, DRY #6).
|
|
206
|
+
groundLeaf:
|
|
207
|
+
skillStore === undefined || !deps.config.enableSkillState
|
|
208
|
+
? undefined
|
|
209
|
+
: (prompt: string) => groundLeafPrompt(skillStore, deps.config, prompt),
|
|
210
|
+
getSkillBlock: () => input.skillBlock,
|
|
211
|
+
} satisfies SubcallHandlerDeps;
|
|
212
|
+
const subcalls = createSubcallHandlers(subcallDeps, taskRegistry);
|
|
213
|
+
|
|
214
|
+
// ── Workstream B write path: distill Σ into the session store at run end ──────────
|
|
215
|
+
// Deterministic harvest is free (Σ is already structured); the opt-in A-Mem phrasing is
|
|
216
|
+
// ONE cheap leaf call. Fail-soft: a distill failure must never damage a finished run.
|
|
217
|
+
const harvestSkillNotes = async (): Promise<void> => {
|
|
218
|
+
if (runStateMode.kind !== "active") return;
|
|
219
|
+
// Root Σ WS-4 mirror FIRST — the tracker absorbs the final Σ even when the store is
|
|
220
|
+
// off (one source, two sinks; never a second harvest implementation).
|
|
221
|
+
deps.onRunState?.(runStateMode.state);
|
|
222
|
+
if (skillStore === undefined || !deps.config.enableSkillState) return;
|
|
223
|
+
skillStore.merge(notesFromRunState(runStateMode.state));
|
|
224
|
+
if (!deps.config.enableSkillStateDistill || deps.signal?.aborted === true) return;
|
|
225
|
+
try {
|
|
226
|
+
const raw = await complete1(
|
|
227
|
+
invocation,
|
|
228
|
+
distillPromptFor(runStateMode.state),
|
|
229
|
+
() => {},
|
|
230
|
+
completeDeps(subcallDeps),
|
|
231
|
+
);
|
|
232
|
+
const parsed = parseDistilledNotes(raw);
|
|
233
|
+
if (parsed.length > 0) skillStore.merge(parsed);
|
|
234
|
+
} catch {
|
|
235
|
+
// fail-soft by design
|
|
236
|
+
}
|
|
237
|
+
};
|
|
212
238
|
/** Wait (bounded) for detached work before the sandbox goes away. */
|
|
213
239
|
const settleDetached = async (): Promise<void> => {
|
|
214
240
|
if (detachedInFlight === 0) return;
|
|
@@ -253,6 +279,10 @@ export function createEngine(deps: EngineDeps): RunRlm {
|
|
|
253
279
|
// H3: retrieval-discipline coach — one-shot per run; children inherit it via the same loop.
|
|
254
280
|
let sawRetrieval = false;
|
|
255
281
|
let retrievalNudged = false;
|
|
282
|
+
// Verification-discipline coach (enableVerificationNudge, default OFF): one coached redo
|
|
283
|
+
// when an early finalize looks like the confident-wrong bench shape.
|
|
284
|
+
let verificationNudged = false;
|
|
285
|
+
let verificationNudgePending = false;
|
|
256
286
|
|
|
257
287
|
try {
|
|
258
288
|
const meta = {
|
|
@@ -260,6 +290,7 @@ export function createEngine(deps: EngineDeps): RunRlm {
|
|
|
260
290
|
contextChars: contextLength(input.context),
|
|
261
291
|
contextStats: contextSizeStats(input.context),
|
|
262
292
|
rootPrompt: input.rootPrompt || undefined,
|
|
293
|
+
skillBlock: input.skillBlock,
|
|
263
294
|
};
|
|
264
295
|
const system = buildRlmSystemPrompt(meta, {
|
|
265
296
|
orchestrator: deps.config.orchestrator,
|
|
@@ -267,13 +298,13 @@ export function createEngine(deps: EngineDeps): RunRlm {
|
|
|
267
298
|
maxPromptChars: deps.config.maxPromptChars,
|
|
268
299
|
contextLoader: deps.config.contextLoader,
|
|
269
300
|
child: input.depth > 0,
|
|
270
|
-
delegation: input.depth > 0
|
|
301
|
+
delegation: input.depth > 0,
|
|
271
302
|
depth: input.depth,
|
|
272
303
|
});
|
|
273
304
|
|
|
274
305
|
// v5 (audit M5): a delegation child does not grow the world — add_context stays root-only.
|
|
275
306
|
const contextHandlers =
|
|
276
|
-
deps.config.contextLoader &&
|
|
307
|
+
deps.config.contextLoader && input.depth === 0
|
|
277
308
|
? buildAddContextHandler({
|
|
278
309
|
cwd: runCwd,
|
|
279
310
|
emitter,
|
|
@@ -289,9 +320,8 @@ export function createEngine(deps: EngineDeps): RunRlm {
|
|
|
289
320
|
}).handlers
|
|
290
321
|
: {};
|
|
291
322
|
|
|
292
|
-
// v5 doctrine:
|
|
293
|
-
|
|
294
|
-
const surface = input.depth > 0 && deps.config.childSurface === "delegation" ? "child" : "root";
|
|
323
|
+
// v5 doctrine: delegation children keep the llm/ledger surface — never repo retrieval.
|
|
324
|
+
const surface = input.depth > 0 ? "child" : "root";
|
|
295
325
|
sandbox = await PythonSandbox.spawn({
|
|
296
326
|
depth: input.depth,
|
|
297
327
|
surface,
|
|
@@ -306,10 +336,8 @@ export function createEngine(deps: EngineDeps): RunRlm {
|
|
|
306
336
|
...subcalls,
|
|
307
337
|
...contextHandlers,
|
|
308
338
|
ledgerClaims: () => Promise.resolve(runLedger.listClaims()),
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
rootMemory === undefined ? "memory off" : rootMemory.serviceOp(op, args, surface === "child" ? "child" : "root"),
|
|
312
|
-
),
|
|
339
|
+
// SKILL.state (Workstream E): the model-visible recall surface — one function.
|
|
340
|
+
skillSearch: skillSearchHandler(() => (deps.config.enableSkillState ? skillStore : undefined)),
|
|
313
341
|
},
|
|
314
342
|
});
|
|
315
343
|
|
|
@@ -320,6 +348,13 @@ export function createEngine(deps: EngineDeps): RunRlm {
|
|
|
320
348
|
liveContext = input.context ?? [];
|
|
321
349
|
contextPin = await pinContext(liveContext);
|
|
322
350
|
await sandbox.loadContextPinned(contextPin);
|
|
351
|
+
|
|
352
|
+
// rootSampling fields win; smartReasoning is the default reasoning when not overridden.
|
|
353
|
+
// Loop-invariant — built once here; finalize() applies the same merge to its own turn.
|
|
354
|
+
const rootSampling: Sampling = {
|
|
355
|
+
reasoning: deps.config.smartReasoning,
|
|
356
|
+
...deps.config.rootSampling,
|
|
357
|
+
};
|
|
323
358
|
for (let i = 0; i < deps.config.maxIterations; i++) {
|
|
324
359
|
limits.checkTimeout();
|
|
325
360
|
if (selfReportId) emitter.emitSubcallUpdated({ id: selfReportId, detail: `turn ${i + 1}/${deps.config.maxIterations}` });
|
|
@@ -339,7 +374,11 @@ export function createEngine(deps: EngineDeps): RunRlm {
|
|
|
339
374
|
signal: deps.signal,
|
|
340
375
|
};
|
|
341
376
|
if (shouldCompact(history, compactionDeps)) {
|
|
342
|
-
|
|
377
|
+
// Workstream A: with Σ active, rebase structurally — [P, Σ_t, window(O)] — and
|
|
378
|
+
// the summarizer call disappears entirely; degraded runs keep compactHistory.
|
|
379
|
+
history = runStateMode.kind === "active"
|
|
380
|
+
? rebaseWithState(history, runStateMode.state, ++compactions)
|
|
381
|
+
: await compactHistory(history, compactionDeps, ++compactions, (u) => limits.addUsage(u));
|
|
343
382
|
}
|
|
344
383
|
}
|
|
345
384
|
|
|
@@ -357,28 +396,31 @@ export function createEngine(deps: EngineDeps): RunRlm {
|
|
|
357
396
|
);
|
|
358
397
|
}
|
|
359
398
|
|
|
360
|
-
// v5 [ledger] blackboard
|
|
399
|
+
// v5 [ledger] blackboard — silent ("") when it has nothing to say.
|
|
361
400
|
const ledgerBlock = deps.config.enableLedger ? runLedger.injectBlock() : "";
|
|
362
|
-
const memoryBlock = rootMemory !== undefined ? rootMemory.injectBlock(input.rootPrompt) : "";
|
|
363
401
|
// H3: after two retrieval-free turns, inject the coach nudge exactly once, for one turn.
|
|
364
402
|
const nudgeNow = i >= 2 && !sawRetrieval && !retrievalNudged;
|
|
365
403
|
if (nudgeNow) retrievalNudged = true;
|
|
366
404
|
const notes =
|
|
367
405
|
[
|
|
368
406
|
i === softNoteTurn ? WRAP_UP_BUDGET : undefined,
|
|
407
|
+
// Workstream A: from iteration 3 the run conditions on Σ (A_t = (P, Σ_t, O_t)) and
|
|
408
|
+
// the state fence is requested — exploratory cold-start stays as-built (§12.2).
|
|
409
|
+
runStateMode.kind === "active" && i >= 2 ? runStateTurnBlock(runStateMode.state) : undefined,
|
|
369
410
|
ledgerBlock === "" ? undefined : ledgerBlock,
|
|
370
|
-
memoryBlock === "" ? undefined : memoryBlock,
|
|
371
411
|
nudgeNow ? RETRIEVAL_NUDGE : undefined,
|
|
412
|
+
verificationNudgePending ? VERIFICATION_NUDGE : undefined,
|
|
413
|
+
// One-shot (turn 0 only): thinking tokens share the completion budget — mirror of
|
|
414
|
+
// the bench's doubling rule. Advisory; never fatal, never repeated.
|
|
415
|
+
i === 0 && rootSampling.reasoning !== undefined && (rootSampling.maxTokens ?? 16_384) < 8_192
|
|
416
|
+
? REASONING_BUDGET_HINT
|
|
417
|
+
: undefined,
|
|
372
418
|
]
|
|
373
419
|
.filter((s): s is string => s !== undefined)
|
|
374
420
|
.join("\n\n") || undefined;
|
|
421
|
+
verificationNudgePending = false;
|
|
375
422
|
appendUserMessage(history, buildTurnPrompt(i, deps.config.maxIterations, notes));
|
|
376
423
|
|
|
377
|
-
// rootSampling fields win; smartReasoning is the default reasoning when not overridden.
|
|
378
|
-
const rootSampling: Sampling = {
|
|
379
|
-
reasoning: deps.config.smartReasoning,
|
|
380
|
-
...deps.config.rootSampling,
|
|
381
|
-
};
|
|
382
424
|
const turn = await runTurn(history, sandbox, {
|
|
383
425
|
model: model,
|
|
384
426
|
registry: deps.registry,
|
|
@@ -414,15 +456,40 @@ export function createEngine(deps: EngineDeps): RunRlm {
|
|
|
414
456
|
completedTurns = i + 1;
|
|
415
457
|
const final = finalAnswerOf(turn.results);
|
|
416
458
|
if (final != null) {
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
459
|
+
// Verification-discipline nudge (enableVerificationNudge, default OFF): an early
|
|
460
|
+
// finalize whose answer is a bare number / short label is the confident-wrong shape
|
|
461
|
+
// that dominated bench failures. ONE coached redo, then the answer is accepted.
|
|
462
|
+
if (deps.config.enableVerificationNudge === true && !verificationNudged
|
|
463
|
+
&& completedTurns < VERIFICATION_NUDGE_TURN_CAP && isBareAnswer(final)) {
|
|
464
|
+
verificationNudged = true;
|
|
465
|
+
verificationNudgePending = true;
|
|
466
|
+
} else {
|
|
467
|
+
const done = result(final, i + 1, limits);
|
|
468
|
+
lastAnswer = done.answer;
|
|
469
|
+
return done;
|
|
470
|
+
}
|
|
421
471
|
}
|
|
422
472
|
|
|
423
473
|
limits.observe(turnHadError(turn.results));
|
|
424
474
|
history.push({ role: "assistant", content: turn.response });
|
|
425
475
|
pendingReplOutputs = formatReplOutputs(turn.results, turn.skippedBlocks);
|
|
476
|
+
// ── Workstream A: apply ΔΣ_t AFTER the environment reply — Algorithm 1 ordering:
|
|
477
|
+
// state reflects intended effects; feedback arrives as the next O_t. Rejections roll
|
|
478
|
+
// back and lead the next observation (error-as-observation retry); retries exhausted
|
|
479
|
+
// ⇒ degrade to as-built for the rest of the run.
|
|
480
|
+
if (runStateMode.kind === "active") {
|
|
481
|
+
const applied = applyStatePatches(
|
|
482
|
+
runStateMode,
|
|
483
|
+
findStatePatches(turn.response),
|
|
484
|
+
i + 1,
|
|
485
|
+
deps.config,
|
|
486
|
+
i >= 2, // the fence was requested this turn → empty turns count as idle (bench rec #2)
|
|
487
|
+
);
|
|
488
|
+
runStateMode = applied.mode;
|
|
489
|
+
if (applied.observation !== undefined) {
|
|
490
|
+
pendingReplOutputs = `${applied.observation}\n\n${pendingReplOutputs}`;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
426
493
|
|
|
427
494
|
// ── v5 budget cascade ─────────────────────────────────────────────────────
|
|
428
495
|
// Content control lives here; wall-clock timeouts stay hang backstops. Whole-tree
|
|
@@ -440,19 +507,37 @@ export function createEngine(deps: EngineDeps): RunRlm {
|
|
|
440
507
|
}
|
|
441
508
|
if (bstate === "hard") {
|
|
442
509
|
if (budget.canContinue()) {
|
|
443
|
-
// Distill
|
|
444
|
-
//
|
|
445
|
-
|
|
510
|
+
// Distill and chain a fresh run with a fresh spend window — the v4 "finalize
|
|
511
|
+
// NOW" flaw fix: never abort mid-task, restructure-and-resume. Workstream A:
|
|
512
|
+
// with Σ active the handoff IS the state (compactJSON — lossless where it
|
|
513
|
+
// matters); the prose walk stays only for degraded runs.
|
|
514
|
+
const handoff = runStateMode.kind === "active"
|
|
515
|
+
? stateHandoff(runStateMode.state, input.rootPrompt, deps.config.budgetHandoffChars)
|
|
516
|
+
: distillTrajectory(history, input.rootPrompt, deps.config.budgetHandoffChars);
|
|
446
517
|
const cont = budget.nextContinuation();
|
|
518
|
+
// Workstream F (MAS2 Eq. 5): one deterministic local fix for the continuation.
|
|
519
|
+
const fix = rectify({
|
|
520
|
+
state: runStateMode.kind === "active" ? runStateMode.state : undefined,
|
|
521
|
+
config: deps.config,
|
|
522
|
+
});
|
|
447
523
|
if (selfReportId) {
|
|
448
|
-
emitter.emitSubcallUpdated({
|
|
524
|
+
emitter.emitSubcallUpdated({
|
|
525
|
+
id: selfReportId,
|
|
526
|
+
detail:
|
|
527
|
+
`budget hard → continuation ${cont.continuations}` +
|
|
528
|
+
(fix.kind === "none" ? "" : ` · rectify: ${rectifyLabel(fix)}`),
|
|
529
|
+
});
|
|
449
530
|
}
|
|
450
531
|
const inner = await run({
|
|
451
532
|
...input,
|
|
452
|
-
rootPrompt: continuationPrompt(cont.continuations, handoff)
|
|
533
|
+
rootPrompt: continuationPrompt(cont.continuations, handoff)
|
|
534
|
+
+ (fix.kind === "narrow-paths"
|
|
535
|
+
? `\n\n[rectify] narrow child spawns: rlm_query(task, paths=${JSON.stringify(fix.paths)})`
|
|
536
|
+
: ""),
|
|
453
537
|
context: liveContext, // H9: sources added mid-run reach the leaf
|
|
454
538
|
budget: cont,
|
|
455
539
|
remainingTimeoutMs: limits.remainingTimeoutMs(),
|
|
540
|
+
...(fix.kind === "none" ? {} : { rectification: fix }),
|
|
456
541
|
});
|
|
457
542
|
// H9: report the CHAIN's spend, not just the leaf's fresh guard.
|
|
458
543
|
const u = limits.usage();
|
|
@@ -463,12 +548,8 @@ export function createEngine(deps: EngineDeps): RunRlm {
|
|
|
463
548
|
outputTokens: inner.outputTokens + u.outputTokens,
|
|
464
549
|
costUsd: inner.costUsd + u.costUsd,
|
|
465
550
|
};
|
|
466
|
-
//
|
|
467
|
-
// the next identical prompt must replay the full result, not miss.
|
|
468
|
-
// R2: lastAnswer must be set before return — `finally` emitAnswer reads it,
|
|
469
|
-
// and persist must store the CHAIN totals, not just the parent window.
|
|
551
|
+
// R2: lastAnswer must be set before return — `finally` emitAnswer reads it.
|
|
470
552
|
lastAnswer = chained.answer;
|
|
471
|
-
persistRoot(chained.answer, chained);
|
|
472
553
|
return chained;
|
|
473
554
|
}
|
|
474
555
|
// Chain cap reached — finalize with the best partial (a budget never throws).
|
|
@@ -478,7 +559,6 @@ export function createEngine(deps: EngineDeps): RunRlm {
|
|
|
478
559
|
}
|
|
479
560
|
if (pendingReplOutputs) appendUserMessage(history, pendingReplOutputs);
|
|
480
561
|
const finalized = result(await finalize(history, model, deps, limits, sandbox), deps.config.maxIterations, limits);
|
|
481
|
-
persistRoot(finalized.answer);
|
|
482
562
|
lastAnswer = finalized.answer;
|
|
483
563
|
return finalized;
|
|
484
564
|
} catch (err) {
|
|
@@ -497,6 +577,8 @@ export function createEngine(deps: EngineDeps): RunRlm {
|
|
|
497
577
|
nodeStatus = "error";
|
|
498
578
|
throw err;
|
|
499
579
|
} finally {
|
|
580
|
+
// Workstream B Hook 1: Σ → SkillState notes at run end (all return paths; never throws).
|
|
581
|
+
if (skillStore !== undefined && deps.config.enableSkillState) await harvestSkillNotes();
|
|
500
582
|
if (deps.config.enableLedger) runLedger.endRun();
|
|
501
583
|
if (selfReportId) {
|
|
502
584
|
emitter.emitSubcallUpdated({
|
|
@@ -535,6 +617,13 @@ function contextWindowOrFallback(model: Model<Api>, registry: ModelContextRegist
|
|
|
535
617
|
return registry.limitFor(`${model.provider}/${model.id}`);
|
|
536
618
|
}
|
|
537
619
|
|
|
620
|
+
/** Bare number / short label — the early-confident answer shape the verification nudge
|
|
621
|
+
* targets (28/33 bench failures were early confident wrong answers). */
|
|
622
|
+
function isBareAnswer(answer: string): boolean {
|
|
623
|
+
const t = answer.trim();
|
|
624
|
+
return t.length <= 12 || /^[-+$(€£¥]?\d+(?:[.,]\d+)*\s*%?$/.test(t);
|
|
625
|
+
}
|
|
626
|
+
|
|
538
627
|
/** Out of turns: ask the model for its best final answer. FINALIZE_PROMPT asks for a fenced
|
|
539
628
|
* ```repl``` block, so execute it like any turn and prefer the captured answer (H2) — a raw
|
|
540
629
|
* fence echoed verbatim must never become the run answer. Plain text stays the fallback. */
|
|
@@ -548,10 +637,19 @@ async function finalize(
|
|
|
548
637
|
const finalHistory = [...history];
|
|
549
638
|
appendUserMessage(finalHistory, FINALIZE_PROMPT);
|
|
550
639
|
const complete = deps.complete ?? modelComplete;
|
|
640
|
+
// Same merge rule as the main loop (the `rootSampling` construction in run()): rootSampling
|
|
641
|
+
// wins, smartReasoning is the reasoning default. Finalize is a root turn — it must obey the
|
|
642
|
+
// user's sampling too, or the last turn of every run silently reverts to provider defaults.
|
|
643
|
+
const rootSampling: Sampling = {
|
|
644
|
+
reasoning: deps.config.smartReasoning,
|
|
645
|
+
...deps.config.rootSampling,
|
|
646
|
+
};
|
|
551
647
|
const { text, usage } = await complete(finalHistory, {
|
|
552
648
|
model,
|
|
553
649
|
registry: deps.registry,
|
|
554
|
-
|
|
650
|
+
maxTokens: rootSampling.maxTokens,
|
|
651
|
+
temperature: rootSampling.temperature,
|
|
652
|
+
reasoning: rootSampling.reasoning,
|
|
555
653
|
signal: deps.signal,
|
|
556
654
|
});
|
|
557
655
|
limits.addUsage(usage);
|
package/src/core/iteration.ts
CHANGED
|
@@ -25,7 +25,7 @@ export interface Turn {
|
|
|
25
25
|
|
|
26
26
|
export type CompleteFn = (messages: readonly ChatMsg[], opts: CompleteOptions) => Promise<CompleteResult>;
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
interface TurnDeps {
|
|
29
29
|
readonly model: Model<Api>;
|
|
30
30
|
readonly registry: ModelRegistry;
|
|
31
31
|
readonly sampling?: Sampling;
|
package/src/core/ledger.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { createHash } from "node:crypto";
|
|
15
|
+
import { isRecord } from "../util/type-guards.ts";
|
|
15
16
|
|
|
16
17
|
const NOISE = /\b(no edits?|do not edit|analysis[- ]only|do not change)\.?/gi;
|
|
17
18
|
const TOK = /[a-z0-9_]{2,}/g;
|
|
@@ -23,13 +24,13 @@ const DONE_LINES = 6;
|
|
|
23
24
|
const PROMPT_PREVIEW = 80;
|
|
24
25
|
/** v5 wait() parity (audit H1): a coalescing twin never parks forever. Generous default —
|
|
25
26
|
* a twin can legitimately wait out a full child engine run. */
|
|
26
|
-
|
|
27
|
+
const WAIT_TIMEOUT_MS = 600_000;
|
|
27
28
|
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
type ClaimKind = "llm" | "rlm";
|
|
30
|
+
type ClaimStatus = "pending" | "running" | "done" | "error";
|
|
30
31
|
|
|
31
32
|
/** All-readonly (project rule): transitions replace the map entry with a new frozen Claim. */
|
|
32
|
-
|
|
33
|
+
interface Claim {
|
|
33
34
|
readonly key: string;
|
|
34
35
|
readonly kind: ClaimKind;
|
|
35
36
|
readonly prompt: string;
|
|
@@ -39,7 +40,7 @@ export interface Claim {
|
|
|
39
40
|
readonly result: string | null;
|
|
40
41
|
}
|
|
41
42
|
|
|
42
|
-
|
|
43
|
+
interface ClaimRequest {
|
|
43
44
|
readonly kind: ClaimKind;
|
|
44
45
|
readonly prompt: string;
|
|
45
46
|
readonly paths: readonly string[];
|
|
@@ -47,12 +48,12 @@ export interface ClaimRequest {
|
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
/** Result of `tryClaim` — a discriminated union, never an exception. */
|
|
50
|
-
|
|
51
|
+
type ClaimDecision =
|
|
51
52
|
| { readonly type: "run"; readonly key: string }
|
|
52
53
|
| { readonly type: "coalesce"; readonly key: string; readonly done: boolean }
|
|
53
54
|
| { readonly type: "echo" };
|
|
54
55
|
|
|
55
|
-
|
|
56
|
+
interface LedgerHits {
|
|
56
57
|
readonly exact: number;
|
|
57
58
|
readonly echo: number;
|
|
58
59
|
readonly near: number;
|
|
@@ -98,11 +99,6 @@ function sha256Hex(text: string): string {
|
|
|
98
99
|
return createHash("sha256").update(text).digest("hex");
|
|
99
100
|
}
|
|
100
101
|
|
|
101
|
-
/** Type guard (project rule: no `as` narrowing) — used by contextSig over unknown payloads. */
|
|
102
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
103
|
-
return typeof value === "object" && value !== null;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
102
|
/** v5 `context_sig`: fingerprint a packed context so same-question/different-haystack never collide. */
|
|
107
103
|
export function contextSig(context: unknown): string {
|
|
108
104
|
if (context === undefined || context === null) return "";
|
|
@@ -124,7 +120,8 @@ export function contextSig(context: unknown): string {
|
|
|
124
120
|
}
|
|
125
121
|
return h.digest("hex").slice(0, 16);
|
|
126
122
|
}
|
|
127
|
-
|
|
123
|
+
// JSON (not String()): String(obj) collapsed EVERY object context to "[object Object]".
|
|
124
|
+
return sha256Hex(JSON.stringify(context)).slice(0, 16);
|
|
128
125
|
}
|
|
129
126
|
|
|
130
127
|
export function taskKey(
|
package/src/core/limits.ts
CHANGED
|
@@ -24,7 +24,7 @@ export function limitsFromConfig(config: Limits): Limits {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
/** Point-in-time totals for a run. */
|
|
27
|
-
|
|
27
|
+
interface UsageSnapshot {
|
|
28
28
|
readonly inputTokens: number;
|
|
29
29
|
readonly outputTokens: number;
|
|
30
30
|
readonly costUsd: number;
|
|
@@ -30,7 +30,7 @@ interface CacheEntry {
|
|
|
30
30
|
}
|
|
31
31
|
type CacheFile = Readonly<Record<string, CacheEntry>>;
|
|
32
32
|
|
|
33
|
-
/** `<root>/.rlm/models_cache.json` — the single cache path helper
|
|
33
|
+
/** `<root>/.rlm/models_cache.json` — the single cache path helper. */
|
|
34
34
|
export function modelsCachePath(root: string): string {
|
|
35
35
|
return `${root.replace(/\/+$/, "")}/.rlm/models_cache.json`;
|
|
36
36
|
}
|