@hicaru/pi-rlm 0.3.16 → 0.3.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -4
- package/README.ru.md +56 -66
- package/README.zh-CN.md +61 -65
- package/package.json +5 -5
- package/src/bridge/add-context.ts +1 -1
- package/src/bridge/handlers/await.ts +13 -22
- package/src/bridge/handlers/completion.ts +27 -5
- package/src/bridge/handlers/emitting.ts +2 -2
- package/src/bridge/handlers/llm-query.ts +46 -68
- package/src/bridge/handlers/rlm-query.ts +14 -84
- package/src/bridge/handlers/task-registry.ts +22 -17
- package/src/bridge/handlers/types.ts +8 -6
- package/src/bridge/model.ts +6 -3
- package/src/commands/rlm-llm.ts +1 -10
- package/src/commands/rlm-rlm.ts +1 -8
- package/src/config/defaults.ts +28 -12
- package/src/config/settings.ts +41 -31
- package/src/config/skillstate.ts +465 -0
- package/src/context/md-cache.ts +1 -1
- package/src/context/merge.ts +1 -1
- package/src/context/namespace.ts +2 -2
- package/src/context/refresh.ts +1 -1
- package/src/context/source-dir.ts +21 -11
- package/src/context/source-doc.ts +1 -1
- package/src/context/source-git.ts +3 -15
- package/src/context/source-text.ts +1 -1
- package/src/context/walk.ts +6 -14
- package/src/core/budget.ts +107 -21
- package/src/core/compaction.ts +44 -1
- package/src/core/engine.ts +141 -84
- package/src/core/iteration.ts +1 -1
- package/src/core/ledger.ts +10 -13
- package/src/core/limits.ts +1 -1
- package/src/core/model-registry.ts +1 -1
- package/src/core/resource-limits.ts +1 -1
- package/src/core/root-context.ts +126 -0
- package/src/core/root-digest.ts +213 -0
- package/src/core/root-state.ts +240 -0
- package/src/core/run-state.ts +577 -0
- package/src/core/types.ts +51 -12
- package/src/index.ts +167 -36
- package/src/mode/llm-model.ts +13 -1
- package/src/mode/native-guards.ts +0 -6
- package/src/mode/rlm-mode.ts +34 -11
- package/src/mode/subagent.ts +5 -5
- package/src/prompts/glossary.ts +41 -25
- package/src/prompts/native.ts +1 -3
- package/src/prompts/system.ts +12 -4
- package/src/sandbox/context-file.ts +1 -1
- package/src/sandbox/interrupts.ts +25 -31
- package/src/sandbox/protocol.ts +14 -20
- package/src/sandbox/py/__pycache__/guards.cpython-314.pyc +0 -0
- package/src/sandbox/py/__pycache__/scaffold.cpython-314.pyc +0 -0
- package/src/sandbox/py/__pycache__/worker.cpython-314.pyc +0 -0
- package/src/sandbox/py/guards.py +1 -1
- package/src/sandbox/py/scaffold.py +24 -31
- package/src/sandbox/py/worker.py +3 -1
- package/src/sandbox/sandbox-manager.ts +2 -2
- package/src/sandbox/sandbox.ts +21 -4
- package/src/text/agent-text.ts +58 -0
- package/src/text/parsing.ts +35 -3
- package/src/text/preview.ts +3 -0
- package/src/text/repl-output.ts +1 -1
- package/src/tool/background-tasks.ts +1 -1
- package/src/tool/repl-render.ts +1 -1
- package/src/tool/repl-result.ts +1 -1
- package/src/tool/repl-tool.ts +50 -26
- package/src/tool/rlm-tool.ts +4 -5
- package/src/tool/subcall-render.ts +1 -1
- package/src/tool/subcall-store.ts +2 -2
- package/src/tool/tool-utils.ts +5 -5
- package/src/ui/intro.ts +1 -1
- package/src/ui/modal/timeline-store.ts +1 -1
- package/src/ui/model-picker/drilldown.ts +1 -1
- package/src/ui/model-picker/levels.ts +1 -1
- package/src/ui/panel/run-registry.ts +1 -1
- package/src/ui/tree/tree-rows.ts +1 -1
- package/src/ui/tree/tree-widget.ts +1 -1
- package/src/util/bm25.ts +97 -0
- package/src/util/concurrency.ts +1 -1
- package/src/util/errors.ts +1 -1
- package/src/util/retry.ts +22 -7
- package/src/util/state-merge.ts +34 -0
- package/src/util/throttle.ts +1 -1
- package/src/util/type-guards.ts +6 -0
- package/src/core/memory.ts +0 -589
package/src/core/engine.ts
CHANGED
|
@@ -16,8 +16,7 @@ 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
22
|
import { buildTurnPrompt, FINALIZE_PROMPT, RETRIEVAL_NUDGE, REASONING_BUDGET_HINT, VERIFICATION_NUDGE } from "../prompts/user.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";
|
|
@@ -49,11 +68,6 @@ const DETACHED_SETTLE_MS = 5_000;
|
|
|
49
68
|
/** Verification nudge (enableVerificationNudge): only an EARLY finalize is suspicious — from
|
|
50
69
|
* turn 4 on, a bare answer is just... an answer. "Before iteration ~4", per the bench data. */
|
|
51
70
|
const VERIFICATION_NUDGE_TURN_CAP = 4;
|
|
52
|
-
/** H6 (audit): root episodes snapshot at most this many real files — replay invalidation for
|
|
53
|
-
* the disk-backed slice of the context without hashing an unbounded repository. */
|
|
54
|
-
const ROOT_HASH_MAX = 64;
|
|
55
|
-
|
|
56
|
-
|
|
57
71
|
export interface EngineDeps {
|
|
58
72
|
readonly model: Model<Api>;
|
|
59
73
|
readonly llmModel: Model<Api>;
|
|
@@ -69,8 +83,13 @@ export interface EngineDeps {
|
|
|
69
83
|
readonly onUsage?: (usage: Usage, role: "root" | "sub") => void;
|
|
70
84
|
/** Test-only: override model completion (scripted multi-turn responses). */
|
|
71
85
|
readonly complete?: import("./iteration.ts").CompleteFn;
|
|
72
|
-
/**
|
|
73
|
-
|
|
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;
|
|
74
93
|
}
|
|
75
94
|
|
|
76
95
|
/** Build a `runRlm` bound to the given deps. The returned function is reused for recursion. */
|
|
@@ -87,6 +106,7 @@ export function createEngine(deps: EngineDeps): RunRlm {
|
|
|
87
106
|
}
|
|
88
107
|
|
|
89
108
|
const model = deps.model;
|
|
109
|
+
const skillStore = deps.skillStore;
|
|
90
110
|
|
|
91
111
|
// Create LimitGuard BEFORE the bridge so sub-LLM usage feeds into it.
|
|
92
112
|
// Children inherit the parent's remaining timeout (propagated as remaining amount, not
|
|
@@ -142,54 +162,26 @@ export function createEngine(deps: EngineDeps): RunRlm {
|
|
|
142
162
|
// childRun (RlmInput.ledger — the one construction seam, DRY #6).
|
|
143
163
|
const runLedger = input.ledger ?? new TaskLedger();
|
|
144
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" };
|
|
145
172
|
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
return {
|
|
157
|
-
answer: hit.result,
|
|
158
|
-
iterations: 0,
|
|
159
|
-
costUsd: 0,
|
|
160
|
-
inputTokens: 0,
|
|
161
|
-
outputTokens: 0,
|
|
162
|
-
durationMs: 0,
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
const persistRoot = (
|
|
167
|
-
answer: string,
|
|
168
|
-
spend?: { readonly inputTokens: number; readonly outputTokens: number },
|
|
169
|
-
): void => {
|
|
170
|
-
if (rootMemory === undefined || input.depth !== 0) return;
|
|
171
|
-
// H2 (audit): only clean root runs persist — a continuation leaf carries the ORIGINAL
|
|
172
|
-
// run's key (it persists the chain itself), and stopped/aborted partials must never
|
|
173
|
-
// replay as if they were real answers.
|
|
174
|
-
if (input.budget !== undefined) return;
|
|
175
|
-
if (answer === "" || answer === "(aborted)" || answer.startsWith("(stopped")) return;
|
|
176
|
-
const u = spend ?? limits.usage();
|
|
177
|
-
rootMemory.recordEpisode({
|
|
178
|
-
key: rootKey,
|
|
179
|
-
kind: "root",
|
|
180
|
-
model: modelRefStr,
|
|
181
|
-
prompt: input.rootPrompt,
|
|
182
|
-
paths: rootContextPaths(input.context, ROOT_HASH_MAX),
|
|
183
|
-
result: answer,
|
|
184
|
-
tokensIn: u.inputTokens,
|
|
185
|
-
tokensOut: u.outputTokens,
|
|
186
|
-
});
|
|
187
|
-
};
|
|
188
|
-
|
|
189
|
-
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 = {
|
|
190
183
|
resolve: () => invocation,
|
|
191
|
-
gates
|
|
192
|
-
?? createSubcallGates(deps.config.maxConcurrentSubcalls, deps.config.maxConcurrentChildren),
|
|
184
|
+
gates,
|
|
193
185
|
registry: deps.registry,
|
|
194
186
|
getLlmModel: () => deps.llmModel,
|
|
195
187
|
getModel: () => model,
|
|
@@ -201,7 +193,6 @@ export function createEngine(deps: EngineDeps): RunRlm {
|
|
|
201
193
|
// interrupt during runTurn, which is strictly after loadContext below.
|
|
202
194
|
getChildContext: () => liveContext,
|
|
203
195
|
ledger: runLedger,
|
|
204
|
-
memory: rootMemory,
|
|
205
196
|
trackDetached: async (task) => {
|
|
206
197
|
detachedInFlight += 1;
|
|
207
198
|
try {
|
|
@@ -211,7 +202,39 @@ export function createEngine(deps: EngineDeps): RunRlm {
|
|
|
211
202
|
if (detachedInFlight === 0) detachedIdle?.();
|
|
212
203
|
}
|
|
213
204
|
},
|
|
214
|
-
|
|
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
|
+
};
|
|
215
238
|
/** Wait (bounded) for detached work before the sandbox goes away. */
|
|
216
239
|
const settleDetached = async (): Promise<void> => {
|
|
217
240
|
if (detachedInFlight === 0) return;
|
|
@@ -267,6 +290,7 @@ export function createEngine(deps: EngineDeps): RunRlm {
|
|
|
267
290
|
contextChars: contextLength(input.context),
|
|
268
291
|
contextStats: contextSizeStats(input.context),
|
|
269
292
|
rootPrompt: input.rootPrompt || undefined,
|
|
293
|
+
skillBlock: input.skillBlock,
|
|
270
294
|
};
|
|
271
295
|
const system = buildRlmSystemPrompt(meta, {
|
|
272
296
|
orchestrator: deps.config.orchestrator,
|
|
@@ -274,13 +298,13 @@ export function createEngine(deps: EngineDeps): RunRlm {
|
|
|
274
298
|
maxPromptChars: deps.config.maxPromptChars,
|
|
275
299
|
contextLoader: deps.config.contextLoader,
|
|
276
300
|
child: input.depth > 0,
|
|
277
|
-
delegation: input.depth > 0
|
|
301
|
+
delegation: input.depth > 0,
|
|
278
302
|
depth: input.depth,
|
|
279
303
|
});
|
|
280
304
|
|
|
281
305
|
// v5 (audit M5): a delegation child does not grow the world — add_context stays root-only.
|
|
282
306
|
const contextHandlers =
|
|
283
|
-
deps.config.contextLoader &&
|
|
307
|
+
deps.config.contextLoader && input.depth === 0
|
|
284
308
|
? buildAddContextHandler({
|
|
285
309
|
cwd: runCwd,
|
|
286
310
|
emitter,
|
|
@@ -296,9 +320,8 @@ export function createEngine(deps: EngineDeps): RunRlm {
|
|
|
296
320
|
}).handlers
|
|
297
321
|
: {};
|
|
298
322
|
|
|
299
|
-
// v5 doctrine:
|
|
300
|
-
|
|
301
|
-
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";
|
|
302
325
|
sandbox = await PythonSandbox.spawn({
|
|
303
326
|
depth: input.depth,
|
|
304
327
|
surface,
|
|
@@ -313,10 +336,8 @@ export function createEngine(deps: EngineDeps): RunRlm {
|
|
|
313
336
|
...subcalls,
|
|
314
337
|
...contextHandlers,
|
|
315
338
|
ledgerClaims: () => Promise.resolve(runLedger.listClaims()),
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
rootMemory === undefined ? "memory off" : rootMemory.serviceOp(op, args, surface === "child" ? "child" : "root"),
|
|
319
|
-
),
|
|
339
|
+
// SKILL.state (Workstream E): the model-visible recall surface — one function.
|
|
340
|
+
skillSearch: skillSearchHandler(() => (deps.config.enableSkillState ? skillStore : undefined)),
|
|
320
341
|
},
|
|
321
342
|
});
|
|
322
343
|
|
|
@@ -353,7 +374,11 @@ export function createEngine(deps: EngineDeps): RunRlm {
|
|
|
353
374
|
signal: deps.signal,
|
|
354
375
|
};
|
|
355
376
|
if (shouldCompact(history, compactionDeps)) {
|
|
356
|
-
|
|
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));
|
|
357
382
|
}
|
|
358
383
|
}
|
|
359
384
|
|
|
@@ -371,17 +396,18 @@ export function createEngine(deps: EngineDeps): RunRlm {
|
|
|
371
396
|
);
|
|
372
397
|
}
|
|
373
398
|
|
|
374
|
-
// v5 [ledger] blackboard
|
|
399
|
+
// v5 [ledger] blackboard — silent ("") when it has nothing to say.
|
|
375
400
|
const ledgerBlock = deps.config.enableLedger ? runLedger.injectBlock() : "";
|
|
376
|
-
const memoryBlock = rootMemory !== undefined ? rootMemory.injectBlock(input.rootPrompt) : "";
|
|
377
401
|
// H3: after two retrieval-free turns, inject the coach nudge exactly once, for one turn.
|
|
378
402
|
const nudgeNow = i >= 2 && !sawRetrieval && !retrievalNudged;
|
|
379
403
|
if (nudgeNow) retrievalNudged = true;
|
|
380
404
|
const notes =
|
|
381
405
|
[
|
|
382
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,
|
|
383
410
|
ledgerBlock === "" ? undefined : ledgerBlock,
|
|
384
|
-
memoryBlock === "" ? undefined : memoryBlock,
|
|
385
411
|
nudgeNow ? RETRIEVAL_NUDGE : undefined,
|
|
386
412
|
verificationNudgePending ? VERIFICATION_NUDGE : undefined,
|
|
387
413
|
// One-shot (turn 0 only): thinking tokens share the completion budget — mirror of
|
|
@@ -439,7 +465,6 @@ export function createEngine(deps: EngineDeps): RunRlm {
|
|
|
439
465
|
verificationNudgePending = true;
|
|
440
466
|
} else {
|
|
441
467
|
const done = result(final, i + 1, limits);
|
|
442
|
-
persistRoot(done.answer);
|
|
443
468
|
lastAnswer = done.answer;
|
|
444
469
|
return done;
|
|
445
470
|
}
|
|
@@ -448,6 +473,23 @@ export function createEngine(deps: EngineDeps): RunRlm {
|
|
|
448
473
|
limits.observe(turnHadError(turn.results));
|
|
449
474
|
history.push({ role: "assistant", content: turn.response });
|
|
450
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
|
+
}
|
|
451
493
|
|
|
452
494
|
// ── v5 budget cascade ─────────────────────────────────────────────────────
|
|
453
495
|
// Content control lives here; wall-clock timeouts stay hang backstops. Whole-tree
|
|
@@ -465,19 +507,37 @@ export function createEngine(deps: EngineDeps): RunRlm {
|
|
|
465
507
|
}
|
|
466
508
|
if (bstate === "hard") {
|
|
467
509
|
if (budget.canContinue()) {
|
|
468
|
-
// Distill
|
|
469
|
-
//
|
|
470
|
-
|
|
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);
|
|
471
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
|
+
});
|
|
472
523
|
if (selfReportId) {
|
|
473
|
-
emitter.emitSubcallUpdated({
|
|
524
|
+
emitter.emitSubcallUpdated({
|
|
525
|
+
id: selfReportId,
|
|
526
|
+
detail:
|
|
527
|
+
`budget hard → continuation ${cont.continuations}` +
|
|
528
|
+
(fix.kind === "none" ? "" : ` · rectify: ${rectifyLabel(fix)}`),
|
|
529
|
+
});
|
|
474
530
|
}
|
|
475
531
|
const inner = await run({
|
|
476
532
|
...input,
|
|
477
|
-
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
|
+
: ""),
|
|
478
537
|
context: liveContext, // H9: sources added mid-run reach the leaf
|
|
479
538
|
budget: cont,
|
|
480
539
|
remainingTimeoutMs: limits.remainingTimeoutMs(),
|
|
540
|
+
...(fix.kind === "none" ? {} : { rectification: fix }),
|
|
481
541
|
});
|
|
482
542
|
// H9: report the CHAIN's spend, not just the leaf's fresh guard.
|
|
483
543
|
const u = limits.usage();
|
|
@@ -488,12 +548,8 @@ export function createEngine(deps: EngineDeps): RunRlm {
|
|
|
488
548
|
outputTokens: inner.outputTokens + u.outputTokens,
|
|
489
549
|
costUsd: inner.costUsd + u.costUsd,
|
|
490
550
|
};
|
|
491
|
-
//
|
|
492
|
-
// the next identical prompt must replay the full result, not miss.
|
|
493
|
-
// R2: lastAnswer must be set before return — `finally` emitAnswer reads it,
|
|
494
|
-
// and persist must store the CHAIN totals, not just the parent window.
|
|
551
|
+
// R2: lastAnswer must be set before return — `finally` emitAnswer reads it.
|
|
495
552
|
lastAnswer = chained.answer;
|
|
496
|
-
persistRoot(chained.answer, chained);
|
|
497
553
|
return chained;
|
|
498
554
|
}
|
|
499
555
|
// Chain cap reached — finalize with the best partial (a budget never throws).
|
|
@@ -503,7 +559,6 @@ export function createEngine(deps: EngineDeps): RunRlm {
|
|
|
503
559
|
}
|
|
504
560
|
if (pendingReplOutputs) appendUserMessage(history, pendingReplOutputs);
|
|
505
561
|
const finalized = result(await finalize(history, model, deps, limits, sandbox), deps.config.maxIterations, limits);
|
|
506
|
-
persistRoot(finalized.answer);
|
|
507
562
|
lastAnswer = finalized.answer;
|
|
508
563
|
return finalized;
|
|
509
564
|
} catch (err) {
|
|
@@ -522,6 +577,8 @@ export function createEngine(deps: EngineDeps): RunRlm {
|
|
|
522
577
|
nodeStatus = "error";
|
|
523
578
|
throw err;
|
|
524
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();
|
|
525
582
|
if (deps.config.enableLedger) runLedger.endRun();
|
|
526
583
|
if (selfReportId) {
|
|
527
584
|
emitter.emitSubcallUpdated({
|
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
|
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Root Σ (WS-3) — per-LLM-call A_t assembly for the native Pi session, via the `context`
|
|
3
|
+
* event (which feeds Pi's transformContext → applied on EVERY provider call).
|
|
4
|
+
*
|
|
5
|
+
* Pi hands handlers a structuredClone of the outgoing messages; the LAST returned array
|
|
6
|
+
* wins and the disk transcript is never touched — context is the query channel, the
|
|
7
|
+
* session log stays the archive (LLM-memory-survey thesis). Both transforms mutate the
|
|
8
|
+
* clone in place (new message objects land in the same array slots; splice uses the same
|
|
9
|
+
* array) to honor the zero-extra-allocations rule.
|
|
10
|
+
*
|
|
11
|
+
* - `elideStalePayloads` — discard semantics (paper §5.3): tool payloads older than the
|
|
12
|
+
* keep window become head+tail previews; the full bytes remain in the session log.
|
|
13
|
+
* - `spliceSigmaSnapshot` — the fresh Σ snapshot rides immediately before the last user
|
|
14
|
+
* message, exactly one instance (previous ones are removed — idempotent per call).
|
|
15
|
+
*
|
|
16
|
+
* Both are pure with respect to the transcript and never throw on weird shapes.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { ContextEvent } from "@earendil-works/pi-coding-agent";
|
|
20
|
+
import type { RunState } from "./run-state.ts";
|
|
21
|
+
import { runStateRootBlock } from "./run-state.ts";
|
|
22
|
+
import { truncateOutput } from "../text/parsing.ts";
|
|
23
|
+
import { textContentOf } from "../text/agent-text.ts";
|
|
24
|
+
|
|
25
|
+
/** The message union Pi passes through the context event (indexed — host evolution safe). */
|
|
26
|
+
export type RootMessage = ContextEvent["messages"][number];
|
|
27
|
+
|
|
28
|
+
export interface ElideOptions {
|
|
29
|
+
readonly keepTurns: number;
|
|
30
|
+
readonly elideChars: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const ELIDE_MARK = "chars elided — full result in session log";
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* WS-3a: elide stale tool payloads. The newest `keepTurns` assistant turns and the final
|
|
37
|
+
* user message stay verbatim; older toolResult content over `elideChars` becomes a
|
|
38
|
+
* head+tail preview (same truncation shape as repl stdout). `role:"custom"` messages with
|
|
39
|
+
* `customType: "rlm-sigma"` are immune (WS-3b owns them). Mutates the array in place;
|
|
40
|
+
* returns the number of messages elided (telemetry), for zero-cost counters at the seam.
|
|
41
|
+
*/
|
|
42
|
+
export function elideStalePayloads(messages: RootMessage[], opts: ElideOptions): number {
|
|
43
|
+
const keepTurns = Math.max(0, Math.floor(opts.keepTurns));
|
|
44
|
+
if (keepTurns === 0 || messages.length === 0) return 0;
|
|
45
|
+
|
|
46
|
+
// Index of the assistant message that opens the keepTurns-th-from-last turn — everything
|
|
47
|
+
// from there on is the protected tail (same walk as core/compaction.ts elideOldToolPayloads).
|
|
48
|
+
let tailStart = -1;
|
|
49
|
+
let seen = 0;
|
|
50
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
51
|
+
if (messages[i]?.role === "assistant") {
|
|
52
|
+
seen += 1;
|
|
53
|
+
if (seen >= keepTurns) {
|
|
54
|
+
tailStart = i;
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (tailStart <= 0) return 0; // fewer turns than the window — nothing to elide
|
|
60
|
+
|
|
61
|
+
const lastUser = lastIndexOfRole(messages, "user");
|
|
62
|
+
let elided = 0;
|
|
63
|
+
for (let i = 0; i < tailStart; i++) {
|
|
64
|
+
const m = messages[i];
|
|
65
|
+
if (m === undefined || m.role !== "toolResult") continue;
|
|
66
|
+
if (i === lastUser) continue; // paranoia: the final user message is never touched
|
|
67
|
+
const total = totalTextLength(m);
|
|
68
|
+
if (total <= opts.elideChars) continue;
|
|
69
|
+
messages[i] = {
|
|
70
|
+
...m,
|
|
71
|
+
content: [{ type: "text", text: previewToolText(m, opts.elideChars) }],
|
|
72
|
+
} as RootMessage;
|
|
73
|
+
elided += 1;
|
|
74
|
+
}
|
|
75
|
+
return elided;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** WS-3b: exactly one live Σ snapshot, immediately before the LAST user message. */
|
|
79
|
+
export function spliceSigmaSnapshot(
|
|
80
|
+
messages: RootMessage[],
|
|
81
|
+
state: RunState,
|
|
82
|
+
rectifyHint: string | undefined,
|
|
83
|
+
): void {
|
|
84
|
+
const block = runStateRootBlock(state);
|
|
85
|
+
const text = rectifyHint === undefined ? block : `${block}\n${rectifyHint}`;
|
|
86
|
+
// Remove any previous instance (only one lives at a time — idempotent across calls).
|
|
87
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
88
|
+
const m = messages[i];
|
|
89
|
+
if (isSigmaSnapshot(m)) messages.splice(i, 1);
|
|
90
|
+
}
|
|
91
|
+
const at = lastIndexOfRole(messages, "user");
|
|
92
|
+
const sigma: RootMessage = {
|
|
93
|
+
role: "custom",
|
|
94
|
+
customType: "rlm-sigma",
|
|
95
|
+
content: text,
|
|
96
|
+
display: false,
|
|
97
|
+
details: { kind: "sigma-snapshot", updatedAt: state.updatedAt },
|
|
98
|
+
timestamp: Date.now(),
|
|
99
|
+
} as RootMessage;
|
|
100
|
+
if (at < 0) {
|
|
101
|
+
messages.push(sigma); // no user message (degenerate) — append; still exactly one
|
|
102
|
+
} else {
|
|
103
|
+
messages.splice(at, 0, sigma);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function isSigmaSnapshot(m: RootMessage | undefined): boolean {
|
|
108
|
+
return m !== undefined && m.role === "custom" && (m as { customType?: unknown }).customType === "rlm-sigma";
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function lastIndexOfRole(messages: readonly RootMessage[], role: "user"): number {
|
|
112
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
113
|
+
if (messages[i]?.role === role) return i;
|
|
114
|
+
}
|
|
115
|
+
return -1;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function totalTextLength(message: RootMessage): number {
|
|
119
|
+
return textContentOf((message as { content?: unknown }).content).length;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function previewToolText(message: RootMessage, elideChars: number): string {
|
|
123
|
+
const text = textContentOf((message as { content?: unknown }).content);
|
|
124
|
+
// The preview REPLACES the payload, so `elideChars` budgets the WHOLE head+tail result.
|
|
125
|
+
return truncateOutput(text, Math.max(100, elideChars), ELIDE_MARK);
|
|
126
|
+
}
|