@hicaru/pi-rlm 0.3.6 → 0.3.9
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 +2 -2
- package/package.json +1 -1
- package/src/bridge/handlers/emitting.ts +5 -23
- package/src/bridge/handlers/index.ts +1 -1
- package/src/bridge/handlers/llm-query.ts +84 -29
- package/src/bridge/handlers/rlm-query.ts +133 -33
- package/src/bridge/handlers/types.ts +12 -0
- package/src/commands/pins.ts +51 -0
- package/src/commands/rlm-config.ts +4 -88
- package/src/commands/rlm-llm.ts +59 -0
- package/src/commands/rlm-rlm.ts +58 -0
- package/src/commands/rlm.ts +2 -2
- package/src/config/defaults.ts +17 -0
- package/src/config/settings.ts +58 -5
- package/src/core/answer.ts +7 -10
- package/src/core/budget.ts +182 -0
- package/src/core/compaction.ts +46 -0
- package/src/core/engine.ts +185 -5
- package/src/core/iteration.ts +5 -0
- package/src/core/ledger.ts +343 -0
- package/src/core/memory.ts +589 -0
- package/src/core/model-registry.ts +88 -0
- package/src/core/types.ts +44 -3
- package/src/index.ts +107 -12
- package/src/mode/rlm-mode.ts +58 -10
- package/src/prompts/glossary.ts +147 -57
- package/src/prompts/native.ts +12 -7
- package/src/prompts/system.ts +22 -7
- package/src/prompts/user.ts +6 -3
- package/src/sandbox/interrupts.ts +24 -0
- package/src/sandbox/protocol.ts +69 -5
- 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/guards.py +11 -6
- package/src/sandbox/py/scaffold.py +615 -0
- package/src/sandbox/py/worker.py +53 -506
- package/src/sandbox/sandbox.ts +21 -3
- package/src/text/repl-output.ts +15 -0
- package/src/tool/repl-render.ts +4 -10
- package/src/tool/repl-result.ts +54 -10
- package/src/tool/repl-tool.ts +50 -3
- package/src/tool/rlm-aggregator.ts +16 -3
- package/src/tool/rlm-details.ts +7 -0
- package/src/tool/rlm-events.ts +17 -1
- package/src/tool/rlm-tool.ts +25 -14
- package/src/tool/subcall-render.ts +14 -129
- package/src/tool/subcall-store.ts +11 -1
- package/src/ui/intro.ts +13 -4
- package/src/ui/modal/agent-modal.ts +104 -0
- package/src/ui/modal/modal-view.ts +132 -0
- package/src/ui/modal/timeline-store.ts +85 -0
- package/src/ui/model-picker/drilldown.ts +173 -0
- package/src/ui/model-picker/grouping.ts +81 -0
- package/src/ui/model-picker/levels.ts +63 -0
- package/src/ui/model-picker.ts +7 -197
- package/src/ui/panel/run-registry.ts +135 -0
- package/src/ui/panel/tree-panel.ts +46 -0
- package/src/ui/status.ts +26 -10
- package/src/ui/theme.ts +0 -4
- package/src/ui/tree/tree-model.ts +221 -0
- package/src/ui/tree/tree-rows.ts +73 -0
- package/src/ui/tree/tree-widget.ts +186 -0
- package/src/util/concurrency.ts +47 -0
package/src/core/engine.ts
CHANGED
|
@@ -16,19 +16,24 @@ import {
|
|
|
16
16
|
createTaskRegistry,
|
|
17
17
|
type Invocation,
|
|
18
18
|
} from "../bridge/handlers/index.ts";
|
|
19
|
+
import { TaskLedger, contextSig, taskKey } from "./ledger.ts";
|
|
20
|
+
import { type MemoryStore, rootContextPaths } from "./memory.ts";
|
|
19
21
|
import { type ChatMsg, modelComplete } from "../bridge/model.ts";
|
|
20
22
|
import { buildRlmSystemPrompt } from "../prompts/system.ts";
|
|
21
23
|
import { buildTurnPrompt, FINALIZE_PROMPT } from "../prompts/user.ts";
|
|
22
24
|
import type { RlmEmitter } from "../tool/rlm-events.ts";
|
|
23
|
-
import {
|
|
25
|
+
import type { SubcallPhase } from "../tool/rlm-details.ts";
|
|
26
|
+
import { PythonSandbox, SANDBOX_WATCHDOG_HEARTBEAT_MS } from "../sandbox/sandbox.ts";
|
|
24
27
|
import { pinContext, type PinnedContext } from "../sandbox/context-file.ts";
|
|
25
28
|
import { previewStdout, previewText } from "../text/preview.ts";
|
|
26
29
|
import { contextLength, contextSizeStats, contextTypeLabel } from "../text/tokens.ts";
|
|
27
30
|
import { finalAnswerOf, formatReplOutputs, latestAnswerContentOf, turnHadError } from "./answer.ts";
|
|
28
|
-
import { compactHistory, shouldCompact } from "./compaction.ts";
|
|
31
|
+
import { compactHistory, elideOldToolPayloads, shouldCompact } from "./compaction.ts";
|
|
29
32
|
import { appendUserMessage } from "./history.ts";
|
|
30
33
|
import { runTurn } from "./iteration.ts";
|
|
31
34
|
import { type Limits, LimitError, LimitGuard } from "./limits.ts";
|
|
35
|
+
import { continuationPrompt, distillTrajectory, resolveBudget, WRAP_UP_BUDGET } from "./budget.ts";
|
|
36
|
+
import { ModelContextRegistry, modelsCachePath } from "./model-registry.ts";
|
|
32
37
|
import type { RlmConfig, RlmInput, RlmResult, RunRlm, Sampling } from "./types.ts";
|
|
33
38
|
import { createSubcallGates, type SubcallGates } from "../util/concurrency.ts";
|
|
34
39
|
|
|
@@ -38,6 +43,9 @@ import { createSubcallGates, type SubcallGates } from "../util/concurrency.ts";
|
|
|
38
43
|
* hold a finished run open on work whose result nobody can receive.
|
|
39
44
|
*/
|
|
40
45
|
const DETACHED_SETTLE_MS = 5_000;
|
|
46
|
+
/** H6 (audit): root episodes snapshot at most this many real files — replay invalidation for
|
|
47
|
+
* the disk-backed slice of the context without hashing an unbounded repository. */
|
|
48
|
+
const ROOT_HASH_MAX = 64;
|
|
41
49
|
|
|
42
50
|
|
|
43
51
|
export interface EngineDeps {
|
|
@@ -55,6 +63,8 @@ export interface EngineDeps {
|
|
|
55
63
|
readonly onUsage?: (usage: Usage, role: "root" | "sub") => void;
|
|
56
64
|
/** Test-only: override model completion (scripted multi-turn responses). */
|
|
57
65
|
readonly complete?: import("./iteration.ts").CompleteFn;
|
|
66
|
+
/** v5: session-wide durable memory store (`.rlm/`); omitted → memory off for this engine. */
|
|
67
|
+
readonly memory?: MemoryStore;
|
|
58
68
|
}
|
|
59
69
|
|
|
60
70
|
/** Build a `runRlm` bound to the given deps. The returned function is reused for recursion. */
|
|
@@ -81,6 +91,17 @@ export function createEngine(deps: EngineDeps): RunRlm {
|
|
|
81
91
|
maxTokens: deps.limits?.maxTokens,
|
|
82
92
|
});
|
|
83
93
|
|
|
94
|
+
// v5 token budget: the primary run-length control. A continuation run carries its own
|
|
95
|
+
// budget in `input.budget`; a fresh run resolves one from the model's context window.
|
|
96
|
+
// ONE registry per run (audit M1): shared by budget resolution, and observed when the
|
|
97
|
+
// model metadata already knows the window so the disk cache populates for other callers.
|
|
98
|
+
const modelCtxRegistry = new ModelContextRegistry(modelsCachePath(runCwd));
|
|
99
|
+
const budget =
|
|
100
|
+
input.budget ??
|
|
101
|
+
(deps.config.enableTokenBudget
|
|
102
|
+
? resolveBudget(contextWindowOrFallback(model, modelCtxRegistry), deps.config)
|
|
103
|
+
: undefined);
|
|
104
|
+
|
|
84
105
|
// One Invocation for the whole run: this engine owns exactly one sandbox at one depth,
|
|
85
106
|
// and its emitter and LimitGuard outlive every sub-call it services — including
|
|
86
107
|
// detached ones, which is why the headless path needs no session registry.
|
|
@@ -99,12 +120,66 @@ export function createEngine(deps: EngineDeps): RunRlm {
|
|
|
99
120
|
},
|
|
100
121
|
},
|
|
101
122
|
};
|
|
123
|
+
// Live activity phase for the tree UI: child engines report on their own subcall
|
|
124
|
+
// node; the root engine has no node, so it reports via the root-phase channel.
|
|
125
|
+
const reportPhase = (phase: SubcallPhase): void => {
|
|
126
|
+
if (selfReportId !== undefined) emitter.emitSubcallUpdated({ id: selfReportId, phase });
|
|
127
|
+
else emitter.emitRootPhase(phase);
|
|
128
|
+
};
|
|
102
129
|
// Detached work must not outlive the sandbox we dispose in `finally`: track it so the
|
|
103
130
|
// run can settle or abort it first (a child engine left running would keep spending).
|
|
104
131
|
let detachedInFlight = 0;
|
|
105
132
|
let detachedIdle: (() => void) | undefined;
|
|
106
133
|
// One registry per run — unawaited task reminders share the same map as await handlers.
|
|
107
134
|
const taskRegistry = createTaskRegistry();
|
|
135
|
+
// v5 TaskLedger: one blackboard per root run; children inherit the same instance via
|
|
136
|
+
// childRun (RlmInput.ledger — the one construction seam, DRY #6).
|
|
137
|
+
const runLedger = input.ledger ?? new TaskLedger();
|
|
138
|
+
if (deps.config.enableLedger) runLedger.beginRun(input.rootPrompt);
|
|
139
|
+
|
|
140
|
+
// v5 durable memory: read-only root replay — an identical prompt over an identical
|
|
141
|
+
// context answers for zero API calls (measured 10,051 → 0 tok in rlm_test).
|
|
142
|
+
const rootMemory =
|
|
143
|
+
deps.memory !== undefined && deps.config.enableMemory ? deps.memory : undefined;
|
|
144
|
+
const modelRefStr = `${model.provider}/${model.id}`;
|
|
145
|
+
const rootKey = taskKey("root", input.rootPrompt, [], modelRefStr, contextSig(input.context));
|
|
146
|
+
if (rootMemory !== undefined && input.depth === 0 && input.budget === undefined) {
|
|
147
|
+
const hit = rootMemory.replay(rootKey);
|
|
148
|
+
if (hit !== undefined) {
|
|
149
|
+
emitter.emitStatus("done");
|
|
150
|
+
return {
|
|
151
|
+
answer: hit.result,
|
|
152
|
+
iterations: 0,
|
|
153
|
+
costUsd: 0,
|
|
154
|
+
inputTokens: 0,
|
|
155
|
+
outputTokens: 0,
|
|
156
|
+
durationMs: 0,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const persistRoot = (
|
|
161
|
+
answer: string,
|
|
162
|
+
spend?: { readonly inputTokens: number; readonly outputTokens: number },
|
|
163
|
+
): void => {
|
|
164
|
+
if (rootMemory === undefined || input.depth !== 0) return;
|
|
165
|
+
// H2 (audit): only clean root runs persist — a continuation leaf carries the ORIGINAL
|
|
166
|
+
// run's key (it persists the chain itself), and stopped/aborted partials must never
|
|
167
|
+
// replay as if they were real answers.
|
|
168
|
+
if (input.budget !== undefined) return;
|
|
169
|
+
if (answer === "" || answer === "(aborted)" || answer.startsWith("(stopped")) return;
|
|
170
|
+
const u = spend ?? limits.usage();
|
|
171
|
+
rootMemory.recordEpisode({
|
|
172
|
+
key: rootKey,
|
|
173
|
+
kind: "root",
|
|
174
|
+
model: modelRefStr,
|
|
175
|
+
prompt: input.rootPrompt,
|
|
176
|
+
paths: rootContextPaths(input.context, ROOT_HASH_MAX),
|
|
177
|
+
result: answer,
|
|
178
|
+
tokensIn: u.inputTokens,
|
|
179
|
+
tokensOut: u.outputTokens,
|
|
180
|
+
});
|
|
181
|
+
};
|
|
182
|
+
|
|
108
183
|
const subcalls = createSubcallHandlers({
|
|
109
184
|
resolve: () => invocation,
|
|
110
185
|
gates: deps.gates
|
|
@@ -119,6 +194,8 @@ export function createEngine(deps: EngineDeps): RunRlm {
|
|
|
119
194
|
// despite being wired before liveContext is assigned — children can only spawn from an
|
|
120
195
|
// interrupt during runTurn, which is strictly after loadContext below.
|
|
121
196
|
getChildContext: () => liveContext,
|
|
197
|
+
ledger: runLedger,
|
|
198
|
+
memory: rootMemory,
|
|
122
199
|
trackDetached: async (task) => {
|
|
123
200
|
detachedInFlight += 1;
|
|
124
201
|
try {
|
|
@@ -139,6 +216,10 @@ export function createEngine(deps: EngineDeps): RunRlm {
|
|
|
139
216
|
detachedIdle = undefined;
|
|
140
217
|
};
|
|
141
218
|
let sandbox: PythonSandbox | undefined;
|
|
219
|
+
const watchdogHeartbeat = setInterval(() => {
|
|
220
|
+
if (detachedInFlight > 0) sandbox?.refreshWatchdog();
|
|
221
|
+
}, SANDBOX_WATCHDOG_HEARTBEAT_MS);
|
|
222
|
+
watchdogHeartbeat.unref?.();
|
|
142
223
|
/**
|
|
143
224
|
* This run's live context: whatever was seeded plus every source added so far. Children
|
|
144
225
|
* inherit it, so it must grow when add_context appends (see the handler's onLoaded below).
|
|
@@ -163,6 +244,9 @@ export function createEngine(deps: EngineDeps): RunRlm {
|
|
|
163
244
|
let compactions = 0;
|
|
164
245
|
let completedTurns = 0;
|
|
165
246
|
let nodeStatus: "done" | "error" = "done";
|
|
247
|
+
// v5 budget cascade state: the wrap-up note fires for exactly ONE turn after crossing soft.
|
|
248
|
+
let softFired = false;
|
|
249
|
+
let softNoteTurn = -1;
|
|
166
250
|
|
|
167
251
|
try {
|
|
168
252
|
const meta = {
|
|
@@ -177,10 +261,13 @@ export function createEngine(deps: EngineDeps): RunRlm {
|
|
|
177
261
|
maxPromptChars: deps.config.maxPromptChars,
|
|
178
262
|
contextLoader: deps.config.contextLoader,
|
|
179
263
|
child: input.depth > 0,
|
|
264
|
+
delegation: input.depth > 0 && deps.config.childSurface === "delegation",
|
|
180
265
|
depth: input.depth,
|
|
181
266
|
});
|
|
182
267
|
|
|
183
|
-
|
|
268
|
+
// v5 (audit M5): a delegation child does not grow the world — add_context stays root-only.
|
|
269
|
+
const contextHandlers =
|
|
270
|
+
deps.config.contextLoader && (input.depth === 0 || deps.config.childSurface !== "delegation")
|
|
184
271
|
? buildAddContextHandler({
|
|
185
272
|
cwd: runCwd,
|
|
186
273
|
emitter,
|
|
@@ -196,8 +283,12 @@ export function createEngine(deps: EngineDeps): RunRlm {
|
|
|
196
283
|
}).handlers
|
|
197
284
|
: {};
|
|
198
285
|
|
|
286
|
+
// v5 doctrine: one condition feeds BOTH the python surface and the memory scope —
|
|
287
|
+
// delegation children keep llm/memory-read/ledger, never repo retrieval or memory.add.
|
|
288
|
+
const surface = input.depth > 0 && deps.config.childSurface === "delegation" ? "child" : "root";
|
|
199
289
|
sandbox = await PythonSandbox.spawn({
|
|
200
290
|
depth: input.depth,
|
|
291
|
+
surface,
|
|
201
292
|
execTimeoutS: deps.config.execTimeoutS,
|
|
202
293
|
requestTimeoutMs: deps.config.requestTimeoutMs,
|
|
203
294
|
python: deps.config.python,
|
|
@@ -205,7 +296,15 @@ export function createEngine(deps: EngineDeps): RunRlm {
|
|
|
205
296
|
initTimeoutMs: deps.config.sandboxInitTimeoutMs,
|
|
206
297
|
maxPromptChars: deps.config.maxPromptChars,
|
|
207
298
|
awaitTimeoutS: Math.round(deps.config.requestTimeoutMs / 1000),
|
|
208
|
-
handlers: {
|
|
299
|
+
handlers: {
|
|
300
|
+
...subcalls,
|
|
301
|
+
...contextHandlers,
|
|
302
|
+
ledgerClaims: () => Promise.resolve(runLedger.listClaims()),
|
|
303
|
+
memoryOp: (op, args) =>
|
|
304
|
+
Promise.resolve(
|
|
305
|
+
rootMemory === undefined ? "memory off" : rootMemory.serviceOp(op, args, surface === "child" ? "child" : "root"),
|
|
306
|
+
),
|
|
307
|
+
},
|
|
209
308
|
});
|
|
210
309
|
|
|
211
310
|
let history: ChatMsg[] = [{ role: "system", content: system }];
|
|
@@ -221,6 +320,8 @@ export function createEngine(deps: EngineDeps): RunRlm {
|
|
|
221
320
|
else emitter.emitTurn(i + 1, deps.config.maxIterations);
|
|
222
321
|
|
|
223
322
|
if (deps.config.compaction) {
|
|
323
|
+
// v5 G1 first: elide old tool payloads head+tail — often avoids the summary entirely.
|
|
324
|
+
history = elideOldToolPayloads(history);
|
|
224
325
|
const compactionDeps = {
|
|
225
326
|
// Summarisation is done by the cheap worker model; the threshold stays on the
|
|
226
327
|
// root model's context window (that is the window the history fills each turn).
|
|
@@ -249,7 +350,18 @@ export function createEngine(deps: EngineDeps): RunRlm {
|
|
|
249
350
|
);
|
|
250
351
|
}
|
|
251
352
|
|
|
252
|
-
|
|
353
|
+
// v5 [ledger] blackboard + [memory] notes — each silent ("") when it has nothing to say.
|
|
354
|
+
const ledgerBlock = deps.config.enableLedger ? runLedger.injectBlock() : "";
|
|
355
|
+
const memoryBlock = rootMemory !== undefined ? rootMemory.injectBlock(input.rootPrompt) : "";
|
|
356
|
+
const notes =
|
|
357
|
+
[
|
|
358
|
+
i === softNoteTurn ? WRAP_UP_BUDGET : undefined,
|
|
359
|
+
ledgerBlock === "" ? undefined : ledgerBlock,
|
|
360
|
+
memoryBlock === "" ? undefined : memoryBlock,
|
|
361
|
+
]
|
|
362
|
+
.filter((s): s is string => s !== undefined)
|
|
363
|
+
.join("\n\n") || undefined;
|
|
364
|
+
appendUserMessage(history, buildTurnPrompt(i, deps.config.maxIterations, notes));
|
|
253
365
|
|
|
254
366
|
// rootSampling fields win; smartReasoning is the default reasoning when not overridden.
|
|
255
367
|
const rootSampling: Sampling = {
|
|
@@ -262,6 +374,7 @@ export function createEngine(deps: EngineDeps): RunRlm {
|
|
|
262
374
|
sampling: rootSampling,
|
|
263
375
|
signal: deps.signal,
|
|
264
376
|
complete: deps.complete,
|
|
377
|
+
onPhase: reportPhase,
|
|
265
378
|
});
|
|
266
379
|
const allBlocks = turn.blocks.length > 0
|
|
267
380
|
? turn.blocks.map((b) => previewText(b, 400)).join("\n")
|
|
@@ -280,6 +393,7 @@ export function createEngine(deps: EngineDeps): RunRlm {
|
|
|
280
393
|
const final = finalAnswerOf(turn.results);
|
|
281
394
|
if (final != null) {
|
|
282
395
|
const done = result(final, i + 1, limits);
|
|
396
|
+
persistRoot(done.answer);
|
|
283
397
|
lastAnswer = done.answer;
|
|
284
398
|
return done;
|
|
285
399
|
}
|
|
@@ -287,9 +401,62 @@ export function createEngine(deps: EngineDeps): RunRlm {
|
|
|
287
401
|
limits.observe(turnHadError(turn.results));
|
|
288
402
|
history.push({ role: "assistant", content: turn.response });
|
|
289
403
|
pendingReplOutputs = formatReplOutputs(turn.results, turn.skippedBlocks);
|
|
404
|
+
|
|
405
|
+
// ── v5 budget cascade ─────────────────────────────────────────────────────
|
|
406
|
+
// Content control lives here; wall-clock timeouts stay hang backstops. Whole-tree
|
|
407
|
+
// tokens (root + sub-LLM) reach `limits` through the invocation's addUsage/addRaw seams.
|
|
408
|
+
if (budget !== undefined) {
|
|
409
|
+
const u = limits.usage();
|
|
410
|
+
budget.observeTotal(u.inputTokens, u.outputTokens);
|
|
411
|
+
const bstate = budget.state();
|
|
412
|
+
if (bstate === "soft" && !softFired) {
|
|
413
|
+
softFired = true;
|
|
414
|
+
softNoteTurn = i + 1;
|
|
415
|
+
if (selfReportId) {
|
|
416
|
+
emitter.emitSubcallUpdated({ id: selfReportId, detail: `budget soft @ ${u.inputTokens + u.outputTokens}/${budget.soft} tok` });
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
if (bstate === "hard") {
|
|
420
|
+
if (budget.canContinue()) {
|
|
421
|
+
// Distill the trajectory and chain a fresh run with a fresh spend window —
|
|
422
|
+
// the v4 "finalize NOW" flaw fix: never abort mid-task, restructure-and-resume.
|
|
423
|
+
const handoff = distillTrajectory(history, input.rootPrompt, deps.config.budgetHandoffChars);
|
|
424
|
+
const cont = budget.nextContinuation();
|
|
425
|
+
if (selfReportId) {
|
|
426
|
+
emitter.emitSubcallUpdated({ id: selfReportId, detail: `budget hard → continuation ${cont.continuations}` });
|
|
427
|
+
}
|
|
428
|
+
const inner = await run({
|
|
429
|
+
...input,
|
|
430
|
+
rootPrompt: continuationPrompt(cont.continuations, handoff),
|
|
431
|
+
context: liveContext, // H9: sources added mid-run reach the leaf
|
|
432
|
+
budget: cont,
|
|
433
|
+
remainingTimeoutMs: limits.remainingTimeoutMs(),
|
|
434
|
+
});
|
|
435
|
+
// H9: report the CHAIN's spend, not just the leaf's fresh guard.
|
|
436
|
+
const u = limits.usage();
|
|
437
|
+
const chained: RlmResult = {
|
|
438
|
+
...inner,
|
|
439
|
+
iterations: inner.iterations + completedTurns,
|
|
440
|
+
inputTokens: inner.inputTokens + u.inputTokens,
|
|
441
|
+
outputTokens: inner.outputTokens + u.outputTokens,
|
|
442
|
+
costUsd: inner.costUsd + u.costUsd,
|
|
443
|
+
};
|
|
444
|
+
// H2: the ORIGINAL run persists the chain's answer under the ORIGINAL key —
|
|
445
|
+
// the next identical prompt must replay the full result, not miss.
|
|
446
|
+
// R2: lastAnswer must be set before return — `finally` emitAnswer reads it,
|
|
447
|
+
// and persist must store the CHAIN totals, not just the parent window.
|
|
448
|
+
lastAnswer = chained.answer;
|
|
449
|
+
persistRoot(chained.answer, chained);
|
|
450
|
+
return chained;
|
|
451
|
+
}
|
|
452
|
+
// Chain cap reached — finalize with the best partial (a budget never throws).
|
|
453
|
+
break;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
290
456
|
}
|
|
291
457
|
if (pendingReplOutputs) appendUserMessage(history, pendingReplOutputs);
|
|
292
458
|
const finalized = result(await finalize(history, model, deps, limits), deps.config.maxIterations, limits);
|
|
459
|
+
persistRoot(finalized.answer);
|
|
293
460
|
lastAnswer = finalized.answer;
|
|
294
461
|
return finalized;
|
|
295
462
|
} catch (err) {
|
|
@@ -308,6 +475,7 @@ export function createEngine(deps: EngineDeps): RunRlm {
|
|
|
308
475
|
nodeStatus = "error";
|
|
309
476
|
throw err;
|
|
310
477
|
} finally {
|
|
478
|
+
if (deps.config.enableLedger) runLedger.endRun();
|
|
311
479
|
if (selfReportId) {
|
|
312
480
|
emitter.emitSubcallUpdated({
|
|
313
481
|
id: selfReportId,
|
|
@@ -319,6 +487,7 @@ export function createEngine(deps: EngineDeps): RunRlm {
|
|
|
319
487
|
if (nodeStatus !== "error" && lastAnswer) emitter.emitAnswer(previewText(lastAnswer));
|
|
320
488
|
emitter.emitStatus(nodeStatus === "error" ? "error" : "done");
|
|
321
489
|
}
|
|
490
|
+
clearInterval(watchdogHeartbeat);
|
|
322
491
|
// Settle detached work FIRST: a child still running may be about to pin this same payload.
|
|
323
492
|
await settleDetached();
|
|
324
493
|
await contextPin?.release();
|
|
@@ -333,6 +502,17 @@ function result(answer: string, iterations: number, limits: LimitGuard): RlmResu
|
|
|
333
502
|
return { answer, iterations, costUsd: u.costUsd, inputTokens: u.inputTokens, outputTokens: u.outputTokens, durationMs: u.durationMs };
|
|
334
503
|
}
|
|
335
504
|
|
|
505
|
+
/** Model metadata window, else the offline registry fallback (disk cache → table → 32k). */
|
|
506
|
+
/** Model metadata window, else the offline registry fallback (disk cache → table → 32k).
|
|
507
|
+
* When metadata provides the window it is observed into the cache (fail-soft, audit M1). */
|
|
508
|
+
function contextWindowOrFallback(model: Model<Api>, registry: ModelContextRegistry): number {
|
|
509
|
+
if (model.contextWindow !== undefined && model.contextWindow > 0) {
|
|
510
|
+
registry.observe(`${model.provider}/${model.id}`, model.contextWindow);
|
|
511
|
+
return model.contextWindow;
|
|
512
|
+
}
|
|
513
|
+
return registry.limitFor(`${model.provider}/${model.id}`);
|
|
514
|
+
}
|
|
515
|
+
|
|
336
516
|
/** Out of turns: ask the model for its best final answer (plain text). */
|
|
337
517
|
async function finalize(history: ChatMsg[], model: Model<Api>, deps: EngineDeps, limits: LimitGuard): Promise<string> {
|
|
338
518
|
const finalHistory = [...history];
|
package/src/core/iteration.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { type ChatMsg, type CompleteOptions, type CompleteResult, modelComplete
|
|
|
10
10
|
import type { ReplResult } from "../sandbox/protocol.ts";
|
|
11
11
|
import type { PythonSandbox } from "../sandbox/sandbox.ts";
|
|
12
12
|
import { findReplBlocks } from "../text/parsing.ts";
|
|
13
|
+
import type { SubcallPhase } from "../tool/rlm-details.ts";
|
|
13
14
|
import type { Sampling } from "./types.ts";
|
|
14
15
|
|
|
15
16
|
export interface Turn {
|
|
@@ -30,10 +31,13 @@ export interface TurnDeps {
|
|
|
30
31
|
readonly signal?: AbortSignal;
|
|
31
32
|
/** Test-only override for model completion (scripted responses). */
|
|
32
33
|
readonly complete?: CompleteFn;
|
|
34
|
+
/** Live activity reporting for the tree UI (thinking → repl/texting per turn). */
|
|
35
|
+
readonly onPhase?: (phase: SubcallPhase) => void;
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
export async function runTurn(history: readonly ChatMsg[], sandbox: PythonSandbox, deps: TurnDeps): Promise<Turn> {
|
|
36
39
|
const complete = deps.complete ?? modelComplete;
|
|
40
|
+
deps.onPhase?.("thinking");
|
|
37
41
|
const { text, usage } = await complete(history, {
|
|
38
42
|
model: deps.model,
|
|
39
43
|
registry: deps.registry,
|
|
@@ -44,6 +48,7 @@ export async function runTurn(history: readonly ChatMsg[], sandbox: PythonSandbo
|
|
|
44
48
|
});
|
|
45
49
|
|
|
46
50
|
const blocks = findReplBlocks(text);
|
|
51
|
+
deps.onPhase?.(blocks.length > 0 ? "repl" : "texting");
|
|
47
52
|
const results = new Array<ReplResult>(blocks.length);
|
|
48
53
|
let executed = 0;
|
|
49
54
|
for (let i = 0; i < blocks.length; i++) {
|