@hicaru/pi-rlm 0.1.9 → 0.2.0
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/package.json +1 -1
- package/src/bridge/llm-query.ts +59 -36
- package/src/bridge/rlm-query.ts +63 -77
- package/src/commands/rlm-config.ts +8 -8
- package/src/commands/rlm.ts +48 -12
- package/src/config/settings.ts +33 -3
- package/src/context/repomix-context.ts +5 -10
- package/src/core/answer.ts +4 -3
- package/src/core/artifacts.ts +4 -3
- package/src/core/engine.ts +48 -249
- package/src/core/gates.ts +3 -3
- package/src/core/limits.ts +19 -1
- package/src/core/pipeline-handlers.ts +319 -0
- package/src/core/pipeline.ts +2 -2
- package/src/core/types.ts +25 -27
- package/src/index.ts +35 -17
- package/src/mode/rlm-mode.ts +8 -11
- package/src/prompts/system.ts +143 -52
- package/src/prompts/user.ts +1 -5
- package/src/sandbox/protocol.ts +0 -7
- package/src/sandbox/sandbox-manager.ts +5 -5
- package/src/sandbox/sandbox.ts +25 -19
- package/src/sandbox/worker.py +354 -0
- package/src/state/paths.ts +1 -1
- package/src/state/reads.ts +12 -4
- package/src/state/resume.ts +5 -11
- package/src/text/parsing.ts +0 -6
- package/src/tool/repl-tool.ts +105 -284
- package/src/tool/rlm-details.ts +0 -10
- package/src/tool/rlm-tool.ts +18 -31
- package/src/tool/subcall-render.ts +61 -9
- package/src/tool/subcall-store.ts +2 -2
- package/src/ui/config-panel.ts +41 -21
- package/src/ui/intro.ts +2 -1
- package/src/ui/status.ts +8 -5
- package/src/ui/theme-adapter.ts +36 -0
- package/src/ui/theme.ts +0 -25
- package/src/mode/input-router.ts +0 -23
package/src/core/answer.ts
CHANGED
|
@@ -37,13 +37,14 @@ export function formatReplOutputs(results: readonly ReplResult[], skippedBlocks
|
|
|
37
37
|
return "No ```repl``` block found in your response. Write one to interact with the REPL.";
|
|
38
38
|
}
|
|
39
39
|
const multi = results.length > 1;
|
|
40
|
-
const parts
|
|
40
|
+
const parts = new Array<string>(results.length);
|
|
41
41
|
let hadElision = false;
|
|
42
|
-
for (
|
|
42
|
+
for (let i = 0; i < results.length; i++) {
|
|
43
|
+
const r = results[i];
|
|
43
44
|
const head = multi ? `[block ${i + 1}]\n` : "";
|
|
44
45
|
const { text, elided } = formatStdout(r);
|
|
45
46
|
hadElision ||= elided;
|
|
46
|
-
parts
|
|
47
|
+
parts[i] = `${head}${text}${formatStderr(r)}`;
|
|
47
48
|
}
|
|
48
49
|
const body = parts.join("\n\n");
|
|
49
50
|
const skipNote = skippedBlocks > 0
|
package/src/core/artifacts.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { execFileSync } from "node:child_process";
|
|
|
6
6
|
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
7
|
import { join } from "node:path";
|
|
8
8
|
import type { Result } from "../util/errors.ts";
|
|
9
|
+
import { errorMessage } from "../util/errors.ts";
|
|
9
10
|
|
|
10
11
|
export const ARTIFACTS_DIR = ".rlm/artifacts";
|
|
11
12
|
|
|
@@ -59,7 +60,7 @@ export function captureGoal(cwd: string, brief: string): GoalCaptureResult {
|
|
|
59
60
|
writeFileSync(join(cwd, baselinePath), JSON.stringify({ paths }, null, 2), "utf-8");
|
|
60
61
|
return { ok: true, value: { goalPath, baselinePath } };
|
|
61
62
|
} catch (err) {
|
|
62
|
-
const message =
|
|
63
|
+
const message = errorMessage(err);
|
|
63
64
|
return { ok: false, error: message };
|
|
64
65
|
}
|
|
65
66
|
}
|
|
@@ -72,7 +73,7 @@ export function saveArtifact(cwd: string, dir: string, slug: string, content: st
|
|
|
72
73
|
writeFileSync(join(cwd, rel), content, "utf-8");
|
|
73
74
|
return { ok: true, path: rel };
|
|
74
75
|
} catch (err) {
|
|
75
|
-
const message =
|
|
76
|
+
const message = errorMessage(err);
|
|
76
77
|
return { ok: false, error: message };
|
|
77
78
|
}
|
|
78
79
|
}
|
|
@@ -82,7 +83,7 @@ export function readArtifact(cwd: string, relPath: string): Result<string, strin
|
|
|
82
83
|
try {
|
|
83
84
|
return { ok: true, value: readFileSync(join(cwd, relPath), "utf-8") };
|
|
84
85
|
} catch (err) {
|
|
85
|
-
const message =
|
|
86
|
+
const message = errorMessage(err);
|
|
86
87
|
return { ok: false, error: `could not read artifact ${relPath}: ${message}` };
|
|
87
88
|
}
|
|
88
89
|
}
|
package/src/core/engine.ts
CHANGED
|
@@ -20,31 +20,21 @@ import { mergeLibraryIntoContext } from "../context/library-context.ts";
|
|
|
20
20
|
import { createLlmBridge } from "../bridge/llm-query.ts";
|
|
21
21
|
import { type ChatMsg, modelComplete } from "../bridge/model.ts";
|
|
22
22
|
import { createRlmHandlers } from "../bridge/rlm-query.ts";
|
|
23
|
-
import { resolveModelId } from "../config/settings.ts";
|
|
23
|
+
import { displayModelRef, resolveModelId } from "../config/settings.ts";
|
|
24
24
|
import { buildRlmSystemPrompt } from "../prompts/system.ts";
|
|
25
25
|
import { buildTurnPrompt, FINALIZE_PROMPT } from "../prompts/user.ts";
|
|
26
26
|
import { phaseGuidance } from "../prompts/phases.ts";
|
|
27
27
|
import type { RlmEmitter } from "../tool/rlm-events.ts";
|
|
28
28
|
import { PythonSandbox } from "../sandbox/sandbox.ts";
|
|
29
29
|
import {
|
|
30
|
-
advancePhase as validatePhaseTransition,
|
|
31
|
-
initialPhaseState,
|
|
32
|
-
isPhase,
|
|
33
30
|
phaseGatePrompt,
|
|
34
|
-
PHASES,
|
|
35
|
-
reconcilePhase,
|
|
36
|
-
routeAfterValidate,
|
|
37
|
-
stageForArtifactKind,
|
|
38
|
-
STAGES,
|
|
39
|
-
type ArtifactRef,
|
|
40
31
|
type Phase,
|
|
41
32
|
type PhaseState,
|
|
42
|
-
type SavedArtifact,
|
|
43
33
|
type StageGateData,
|
|
44
34
|
} from "./pipeline.ts";
|
|
45
|
-
import {
|
|
35
|
+
import { PipelineController } from "./pipeline-handlers.ts";
|
|
46
36
|
import type { ValidationGateData } from "./gates.ts";
|
|
47
|
-
import { captureGoal,
|
|
37
|
+
import { captureGoal, type GoalCapture } from "./artifacts.ts";
|
|
48
38
|
import { previewStdout, previewText } from "../text/preview.ts";
|
|
49
39
|
import { contextLength, contextSizeStats, contextTypeLabel } from "../text/tokens.ts";
|
|
50
40
|
import { finalAnswerOf, formatReplOutputs, latestAnswerContentOf, turnHadError } from "./answer.ts";
|
|
@@ -187,29 +177,26 @@ export function createEngine(deps: EngineDeps): RunRlm {
|
|
|
187
177
|
});
|
|
188
178
|
|
|
189
179
|
const llm = createLlmBridge({
|
|
190
|
-
workerModel: deps.workerModel,
|
|
180
|
+
workerModel: () => deps.workerModel,
|
|
191
181
|
registry: deps.registry,
|
|
192
|
-
|
|
193
|
-
maxPromptChars: deps.config.maxPromptChars,
|
|
194
|
-
maxConcurrent: deps.config.maxConcurrentSubcalls,
|
|
195
|
-
sampling: deps.config.subSampling,
|
|
182
|
+
config: () => deps.config,
|
|
196
183
|
signal: deps.signal,
|
|
197
184
|
onUsage: (u) => {
|
|
198
185
|
limits.addUsage(u);
|
|
199
186
|
deps.onUsage?.(u, "sub");
|
|
200
187
|
},
|
|
201
|
-
emitter,
|
|
202
|
-
parentId: selfReportId,
|
|
203
|
-
depth: input.depth,
|
|
188
|
+
emitter: () => emitter,
|
|
189
|
+
parentId: () => selfReportId,
|
|
190
|
+
depth: () => input.depth,
|
|
204
191
|
remainingBudget,
|
|
205
192
|
});
|
|
206
193
|
const rlm = createRlmHandlers({
|
|
207
194
|
run,
|
|
208
195
|
llm,
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
parentNodeId: selfReportId,
|
|
196
|
+
config: () => deps.config,
|
|
197
|
+
modelLabel: (override) => displayModelRef(deps.registry, override, model),
|
|
198
|
+
emitter: () => emitter,
|
|
199
|
+
parentNodeId: () => selfReportId,
|
|
213
200
|
remainingBudget,
|
|
214
201
|
onChildUsage: (costUsd, inputTokens, outputTokens) => {
|
|
215
202
|
limits.addRaw(costUsd, inputTokens, outputTokens);
|
|
@@ -220,18 +207,8 @@ export function createEngine(deps: EngineDeps): RunRlm {
|
|
|
220
207
|
let lastAnswer = "";
|
|
221
208
|
let compactions = 0;
|
|
222
209
|
let completedTurns = 0;
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
* Latest save per phase: path + optional gate memo (single record so they cannot desync).
|
|
226
|
-
* Invalidated by clearLastSaved on phase exit / loop-back.
|
|
227
|
-
*/
|
|
228
|
-
let lastSaved: Partial<Record<Phase, SavedArtifact>> = {};
|
|
229
|
-
/** Advisory warnings accumulated from save_artifact critiques (TUI). */
|
|
230
|
-
let pipelineWarnings: string[] = [];
|
|
231
|
-
/** Serviced ask_user_question rounds in the current phase (session-only; reset on transition). */
|
|
232
|
-
let askRoundsThisPhase = 0;
|
|
233
|
-
let pendingHistoryReset: ChatMsg[] | undefined;
|
|
234
|
-
let goal: GoalCapture | undefined;
|
|
210
|
+
/** Owns all pipeline state (phase, per-phase latest save, ask rounds, pending reset). */
|
|
211
|
+
let pipeline: PipelineController | undefined;
|
|
235
212
|
let nodeStatus: "done" | "error" = "done";
|
|
236
213
|
let persistOn = persist;
|
|
237
214
|
if (persist && deps.runState && !input.resume && runId) {
|
|
@@ -284,13 +261,6 @@ export function createEngine(deps: EngineDeps): RunRlm {
|
|
|
284
261
|
if (!ok) persistOn = false;
|
|
285
262
|
};
|
|
286
263
|
|
|
287
|
-
/** Clear lastSaved so a re-entered stage cannot re-gate with a stale artifact. */
|
|
288
|
-
const clearLastSaved = (...phases: readonly Phase[]): void => {
|
|
289
|
-
const next: Partial<Record<Phase, SavedArtifact>> = { ...lastSaved };
|
|
290
|
-
for (const p of phases) delete next[p];
|
|
291
|
-
lastSaved = next;
|
|
292
|
-
};
|
|
293
|
-
|
|
294
264
|
try {
|
|
295
265
|
const pipelineOn = input.depth === 0 && deps.config.pipeline;
|
|
296
266
|
const meta = {
|
|
@@ -309,111 +279,22 @@ export function createEngine(deps: EngineDeps): RunRlm {
|
|
|
309
279
|
libraryLoader: deps.config.libraryLoader,
|
|
310
280
|
});
|
|
311
281
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
return formatError(
|
|
322
|
-
`artifact kind '${kind}' belongs to phase '${stage.phase}', but the pipeline is in '${current}'`,
|
|
323
|
-
);
|
|
324
|
-
}
|
|
325
|
-
const saved = saveArtifact(runCwd, stage.artifactDir, kind, content);
|
|
326
|
-
if (!saved.ok) return formatError(saved.error);
|
|
327
|
-
// Preflight: run the SAME gate advance_phase will run, now instead of a turn later.
|
|
328
|
-
const critique = critiqueArtifact(stage, content, saved.path, runCwd);
|
|
329
|
-
// One assignment: path + optional gate memo (undefined when gate failed).
|
|
330
|
-
lastSaved = {
|
|
331
|
-
...lastSaved,
|
|
332
|
-
[stage.phase]: Object.freeze({
|
|
333
|
-
path: saved.path,
|
|
334
|
-
gateData: critique.gateData,
|
|
335
|
-
}),
|
|
336
|
-
};
|
|
337
|
-
if (critique.warnings.length > 0) {
|
|
338
|
-
const next = new Array<string>(pipelineWarnings.length + critique.warnings.length);
|
|
339
|
-
for (let i = 0; i < pipelineWarnings.length; i++) next[i] = pipelineWarnings[i];
|
|
340
|
-
for (let i = 0; i < critique.warnings.length; i++) {
|
|
341
|
-
next[pipelineWarnings.length + i] = critique.warnings[i];
|
|
342
|
-
}
|
|
343
|
-
pipelineWarnings = next;
|
|
344
|
-
emitter.emitWarnings(Object.freeze([...pipelineWarnings]));
|
|
345
|
-
}
|
|
346
|
-
return `ok — saved ${saved.path}.\n${formatCritique(critique)}`;
|
|
347
|
-
},
|
|
348
|
-
advancePhase: async (phase: string, summary: string | undefined): Promise<string> => {
|
|
349
|
-
const current = phaseState?.current ?? PHASES[0];
|
|
350
|
-
const outcome = validatePhaseTransition(current, phase);
|
|
351
|
-
if (!outcome.ok) return formatError(outcome.error);
|
|
352
|
-
|
|
353
|
-
// Clarify interview gate: engine counts serviced ask_user_question rounds
|
|
354
|
-
// (un-gameable — the model cannot advance without having actually asked).
|
|
355
|
-
if (current === "clarify" && askRoundsThisPhase === 0) {
|
|
356
|
-
return formatError(
|
|
357
|
-
"clarify requires at least one ask_user_question round — interview the user before advancing",
|
|
358
|
-
);
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
// GATE: measure the CURRENT stage's latest save only (never fall back to
|
|
362
|
-
// phaseState.artifacts — those are completed-channel paths and may be stale
|
|
363
|
-
// across a corrective loop). Prefer the memo filled by save_artifact.
|
|
364
|
-
const stage = STAGES[current];
|
|
365
|
-
const savedEntry = lastSaved[current];
|
|
366
|
-
const artifactPath = savedEntry?.path;
|
|
367
|
-
if (stage.artifactDir !== "" && artifactPath === undefined) {
|
|
368
|
-
return formatError(
|
|
369
|
-
`phase '${current}' has no saved artifact — call save_artifact("${stage.artifactKind}", content) first`,
|
|
370
|
-
);
|
|
371
|
-
}
|
|
372
|
-
let gateData: StageGateData | undefined;
|
|
373
|
-
if (stage.artifactDir !== "" && artifactPath !== undefined) {
|
|
374
|
-
if (savedEntry?.gateData !== undefined) {
|
|
375
|
-
gateData = savedEntry.gateData;
|
|
376
|
-
} else {
|
|
377
|
-
const content = readArtifact(runCwd, artifactPath);
|
|
378
|
-
if (!content.ok) return formatError(content.error);
|
|
379
|
-
const gate = stage.gate(content.value, artifactPath, runCwd);
|
|
380
|
-
if (!gate.ok) return formatError(gate.error);
|
|
381
|
-
gateData = gate.value;
|
|
382
|
-
lastSaved = {
|
|
383
|
-
...lastSaved,
|
|
384
|
-
[current]: Object.freeze({ path: artifactPath, gateData }),
|
|
385
|
-
};
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
// Transition accepted: persist row, schedule root history reset (fresh session).
|
|
390
|
-
const prevArtifacts = phaseState?.artifacts ?? {};
|
|
391
|
-
const nextArtifacts: Partial<Record<Phase, ArtifactRef>> = { ...prevArtifacts };
|
|
392
|
-
if (artifactPath !== undefined) {
|
|
393
|
-
nextArtifacts[current] = Object.freeze({ path: artifactPath, status: "active" });
|
|
394
|
-
}
|
|
395
|
-
phaseState = {
|
|
396
|
-
current: outcome.phase,
|
|
397
|
-
advancedAt: completedTurns,
|
|
398
|
-
summary,
|
|
399
|
-
artifacts: nextArtifacts,
|
|
400
|
-
backwardJumps: phaseState?.backwardJumps ?? 0,
|
|
401
|
-
};
|
|
402
|
-
await persistPhaseRow(phaseState, artifactPath, artifactPath !== undefined ? current : undefined, gateData);
|
|
403
|
-
clearLastSaved(current);
|
|
404
|
-
askRoundsThisPhase = 0;
|
|
405
|
-
pendingHistoryReset = resetHistoryForPhase(system, phaseState, { goal });
|
|
406
|
-
return `ok — phase advanced to '${outcome.phase}' (was '${current}'${summary ? `, summary: ${summary.slice(0, 80)}` : ""})`;
|
|
407
|
-
},
|
|
408
|
-
}
|
|
409
|
-
: {};
|
|
282
|
+
pipeline = new PipelineController({
|
|
283
|
+
runCwd,
|
|
284
|
+
maxBackwardJumps: deps.config.maxBackwardJumps,
|
|
285
|
+
emitter,
|
|
286
|
+
completedTurns: () => completedTurns,
|
|
287
|
+
resetHistoryForPhase: (state, options) => resetHistoryForPhase(system, state, options),
|
|
288
|
+
persistPhaseRow,
|
|
289
|
+
});
|
|
290
|
+
const phaseHandlers = pipelineOn ? pipeline.handlers() : {};
|
|
410
291
|
const baseAsk = deps.config.askUserQuestion ? deps.onAskUserQuestion : undefined;
|
|
411
292
|
const interactiveHandlers = buildInteractiveHandlers({
|
|
412
293
|
onAskUserQuestion: baseAsk
|
|
413
294
|
? async (questions) => {
|
|
414
295
|
const answers = await baseAsk(questions);
|
|
415
296
|
// Count only successfully serviced root-depth rounds (handler already rejects depth>0).
|
|
416
|
-
|
|
297
|
+
pipeline?.noteAskRound();
|
|
417
298
|
return answers;
|
|
418
299
|
}
|
|
419
300
|
: undefined,
|
|
@@ -484,43 +365,14 @@ export function createEngine(deps: EngineDeps): RunRlm {
|
|
|
484
365
|
best = input.resume.best;
|
|
485
366
|
compactions = input.resume.compactions;
|
|
486
367
|
completedTurns = input.resume.completedTurns;
|
|
487
|
-
if (input.resume.phase)
|
|
488
|
-
const resumePhase = input.resume.phase;
|
|
489
|
-
const artifacts: Partial<Record<Phase, ArtifactRef>> = {};
|
|
490
|
-
if (resumePhase.artifacts) {
|
|
491
|
-
for (const [k, v] of Object.entries(resumePhase.artifacts)) {
|
|
492
|
-
if (v !== undefined && isPhase(k)) {
|
|
493
|
-
artifacts[k] = Object.freeze({
|
|
494
|
-
path: v.path,
|
|
495
|
-
status: v.superseded ? "superseded" as const : "active" as const,
|
|
496
|
-
});
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
phaseState = {
|
|
501
|
-
current: reconcilePhase(resumePhase.current),
|
|
502
|
-
advancedAt: resumePhase.advancedAt,
|
|
503
|
-
summary: resumePhase.summary,
|
|
504
|
-
artifacts,
|
|
505
|
-
backwardJumps: resumePhase.backwardJumps ?? 0,
|
|
506
|
-
};
|
|
507
|
-
// lastSaved is session-only: never rehydrate from trail (would re-gate stale
|
|
508
|
-
// plan/validation after loop-back / mid-stage resume without a fresh save).
|
|
509
|
-
lastSaved = {};
|
|
510
|
-
// askRoundsThisPhase is session-only (like lastSaved): a resume mid-clarify
|
|
511
|
-
// restarts the interview count so the model must ask again in this process.
|
|
512
|
-
askRoundsThisPhase = 0;
|
|
513
|
-
}
|
|
368
|
+
if (input.resume.phase) pipeline.seedFromResume(input.resume.phase);
|
|
514
369
|
} else if (pipelineOn) {
|
|
515
370
|
// Goal capture (script, no LLM) + seed phase state + fresh history.
|
|
516
371
|
const captured = captureGoal(runCwd, input.rootPrompt);
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
// Fail-soft: fold into the first reset message (never console — corrupts TUI).
|
|
522
|
-
goalNotice = `Note: goal artifact could not be written (${captured.error}); the brief remains only in the system prompt.`;
|
|
523
|
-
}
|
|
372
|
+
// Fail-soft: fold the failure into the first reset message (never console — corrupts TUI).
|
|
373
|
+
const goalNotice = captured.ok
|
|
374
|
+
? undefined
|
|
375
|
+
: `Note: goal artifact could not be written (${captured.error}); the brief remains only in the system prompt.`;
|
|
524
376
|
// Clarify only when interviews are enabled AND the host wired a callback.
|
|
525
377
|
// Config alone is not enough: without onAskUserQuestion every ask throws and
|
|
526
378
|
// the run would burn maxIterations stuck at clarify (askRounds stays 0).
|
|
@@ -528,8 +380,7 @@ export function createEngine(deps: EngineDeps): RunRlm {
|
|
|
528
380
|
deps.config.askUserQuestion && deps.onAskUserQuestion !== undefined
|
|
529
381
|
? "clarify"
|
|
530
382
|
: "research";
|
|
531
|
-
|
|
532
|
-
history = resetHistoryForPhase(system, phaseState, { goal, notice: goalNotice });
|
|
383
|
+
history = pipeline.seedFresh(startPhase, captured.ok ? captured.value : undefined, goalNotice);
|
|
533
384
|
}
|
|
534
385
|
|
|
535
386
|
// Context: serialize ContextBundle to sandbox-ready JSON array, pass raw strings through.
|
|
@@ -550,9 +401,9 @@ export function createEngine(deps: EngineDeps): RunRlm {
|
|
|
550
401
|
else emitter.emitTurn(i + 1, deps.config.maxIterations);
|
|
551
402
|
|
|
552
403
|
// Apply deferred history reset from a prior advance_phase (fresh session policy).
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
404
|
+
const scheduledReset = pipeline.takePendingReset();
|
|
405
|
+
if (scheduledReset !== undefined) {
|
|
406
|
+
history = scheduledReset;
|
|
556
407
|
pendingReplOutputs = undefined;
|
|
557
408
|
}
|
|
558
409
|
|
|
@@ -588,7 +439,7 @@ export function createEngine(deps: EngineDeps): RunRlm {
|
|
|
588
439
|
pendingReplOutputs = undefined;
|
|
589
440
|
}
|
|
590
441
|
|
|
591
|
-
const gateMsg = deps.config.pipeline ? phaseGatePrompt(
|
|
442
|
+
const gateMsg = deps.config.pipeline ? phaseGatePrompt(pipeline.phase, completedTurns) : undefined;
|
|
592
443
|
const gateUserMsg = gateMsg ? `[${new Date().toISOString()}] ${gateMsg}` : undefined;
|
|
593
444
|
// Phase guidance lives only in resetHistoryForPhase (fresh session) — do not re-inject
|
|
594
445
|
// into every turn prompt (avoids duplication on turn 1 and dead post-transition flags).
|
|
@@ -625,79 +476,27 @@ export function createEngine(deps: EngineDeps): RunRlm {
|
|
|
625
476
|
completedTurns = i + 1;
|
|
626
477
|
const final = finalAnswerOf(turn.results);
|
|
627
478
|
if (final != null) {
|
|
628
|
-
// Validate-phase finalize:
|
|
629
|
-
//
|
|
630
|
-
if (pipelineOn &&
|
|
631
|
-
const
|
|
632
|
-
if (
|
|
633
|
-
// Reject finalize — push error into next turn.
|
|
634
|
-
history.push({ role: "assistant", content: turn.response });
|
|
635
|
-
pendingReplOutputs = formatError(
|
|
636
|
-
"finalize rejected — save the validation artifact first via save_artifact(\"validation\", content) with status: ready, blockers_count, and verdict",
|
|
637
|
-
);
|
|
638
|
-
continue;
|
|
639
|
-
}
|
|
640
|
-
const content = readArtifact(runCwd, vPath);
|
|
641
|
-
if (!content.ok) {
|
|
642
|
-
history.push({ role: "assistant", content: turn.response });
|
|
643
|
-
pendingReplOutputs = formatError(content.error);
|
|
644
|
-
continue;
|
|
645
|
-
}
|
|
646
|
-
const gate = STAGES.validate.gate(content.value, vPath, runCwd);
|
|
647
|
-
if (!gate.ok) {
|
|
648
|
-
history.push({ role: "assistant", content: turn.response });
|
|
649
|
-
pendingReplOutputs = formatError(gate.error);
|
|
650
|
-
continue;
|
|
651
|
-
}
|
|
652
|
-
if (gate.value.kind !== "validation") {
|
|
479
|
+
// Validate-phase finalize is gated: the controller measures THIS turn's validation
|
|
480
|
+
// save only, never phase.artifacts (stale after a prior corrective loop).
|
|
481
|
+
if (pipelineOn && pipeline.phase?.current === "validate") {
|
|
482
|
+
const outcome = await pipeline.finalizeInValidate(final);
|
|
483
|
+
if (outcome.kind === "reject") {
|
|
653
484
|
history.push({ role: "assistant", content: turn.response });
|
|
654
|
-
pendingReplOutputs =
|
|
485
|
+
pendingReplOutputs = outcome.error;
|
|
655
486
|
continue;
|
|
656
487
|
}
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
validation,
|
|
660
|
-
phaseState.backwardJumps,
|
|
661
|
-
deps.config.maxBackwardJumps,
|
|
662
|
-
);
|
|
663
|
-
if (route.kind === "loop-back") {
|
|
664
|
-
// Keep all prior artifacts; mark blueprint as superseded by this validation.
|
|
665
|
-
// lastSaved is still cleared so the gate must see a genuinely FRESH plan.
|
|
666
|
-
const prior = phaseState.artifacts.blueprint;
|
|
667
|
-
const nextArtifacts: Partial<Record<Phase, ArtifactRef>> = {
|
|
668
|
-
...phaseState.artifacts,
|
|
669
|
-
validate: Object.freeze({ path: vPath, status: "active" }),
|
|
670
|
-
};
|
|
671
|
-
if (prior !== undefined) {
|
|
672
|
-
nextArtifacts.blueprint = Object.freeze({
|
|
673
|
-
path: prior.path,
|
|
674
|
-
status: "superseded",
|
|
675
|
-
supersededBy: vPath,
|
|
676
|
-
});
|
|
677
|
-
}
|
|
678
|
-
phaseState = {
|
|
679
|
-
current: "blueprint",
|
|
680
|
-
advancedAt: completedTurns,
|
|
681
|
-
summary: `loop-back: ${validation.blockersCount} blocker(s)`,
|
|
682
|
-
artifacts: nextArtifacts,
|
|
683
|
-
backwardJumps: phaseState.backwardJumps + 1,
|
|
684
|
-
};
|
|
685
|
-
await persistPhaseRow(phaseState, vPath, "validate", gate.value, prior?.path);
|
|
686
|
-
clearLastSaved("blueprint", "validate");
|
|
687
|
-
askRoundsThisPhase = 0;
|
|
688
|
-
history = resetHistoryForPhase(system, phaseState, { goal, validation });
|
|
488
|
+
if (outcome.kind === "loop-back") {
|
|
489
|
+
history = outcome.history;
|
|
689
490
|
pendingReplOutputs = undefined;
|
|
690
|
-
pendingHistoryReset = undefined;
|
|
691
491
|
continue;
|
|
692
492
|
}
|
|
693
|
-
if (
|
|
694
|
-
const
|
|
695
|
-
const halted = result(report, i + 1, limits);
|
|
493
|
+
if (outcome.kind === "halt") {
|
|
494
|
+
const halted = result(outcome.report, i + 1, limits);
|
|
696
495
|
await recordTerminal("completed", halted);
|
|
697
496
|
lastAnswer = halted.answer;
|
|
698
497
|
return halted;
|
|
699
498
|
}
|
|
700
|
-
//
|
|
499
|
+
// outcome.kind === "accept" — take the model's final answer
|
|
701
500
|
}
|
|
702
501
|
const done = result(final, i + 1, limits);
|
|
703
502
|
await recordTerminal("completed", done);
|
|
@@ -709,9 +508,9 @@ export function createEngine(deps: EngineDeps): RunRlm {
|
|
|
709
508
|
// Always capture this turn's REPL outputs for the JSONL trail (fidelity).
|
|
710
509
|
const turnReplOutputs = formatReplOutputs(turn.results, turn.skippedBlocks);
|
|
711
510
|
// If advance_phase scheduled a history reset, apply it now (do not pollute fresh history).
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
511
|
+
const nextReset = pipeline.takePendingReset();
|
|
512
|
+
if (nextReset !== undefined) {
|
|
513
|
+
history = nextReset;
|
|
715
514
|
// Fanout/advance result is already embedded in the reset user message — do not
|
|
716
515
|
// also append raw REPL stdout as a next-turn user message.
|
|
717
516
|
pendingReplOutputs = undefined;
|
package/src/core/gates.ts
CHANGED
|
@@ -135,7 +135,7 @@ export function sectionHasNonEmptyBody(content: string, heading: string): boolea
|
|
|
135
135
|
const headingRe = new RegExp(`^##\\s+${escapeRegExp(heading)}\\s*$`);
|
|
136
136
|
let inSection = false;
|
|
137
137
|
let seen = false; // first match wins — do not re-enter on a later duplicate heading
|
|
138
|
-
let
|
|
138
|
+
let hasBody = false; // only emptiness matters — never accumulate the body itself
|
|
139
139
|
forEachLineOutsideFences(content, (line) => {
|
|
140
140
|
if (/^##\s+/.test(line)) {
|
|
141
141
|
if (inSection) {
|
|
@@ -148,9 +148,9 @@ export function sectionHasNonEmptyBody(content: string, heading: string): boolea
|
|
|
148
148
|
}
|
|
149
149
|
return;
|
|
150
150
|
}
|
|
151
|
-
if (inSection)
|
|
151
|
+
if (inSection && line.trim().length > 0) hasBody = true;
|
|
152
152
|
});
|
|
153
|
-
return
|
|
153
|
+
return hasBody;
|
|
154
154
|
}
|
|
155
155
|
|
|
156
156
|
function escapeRegExp(s: string): string {
|
package/src/core/limits.ts
CHANGED
|
@@ -13,6 +13,24 @@ export interface Limits {
|
|
|
13
13
|
readonly maxErrors?: number;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
/** Pick the limit caps out of a config (`RlmConfig` satisfies this structurally). */
|
|
17
|
+
export function limitsFromConfig(config: Limits): Limits {
|
|
18
|
+
return {
|
|
19
|
+
maxBudgetUsd: config.maxBudgetUsd,
|
|
20
|
+
maxTimeoutMs: config.maxTimeoutMs,
|
|
21
|
+
maxTokens: config.maxTokens,
|
|
22
|
+
maxErrors: config.maxErrors,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Point-in-time totals for a run. */
|
|
27
|
+
export interface UsageSnapshot {
|
|
28
|
+
readonly inputTokens: number;
|
|
29
|
+
readonly outputTokens: number;
|
|
30
|
+
readonly costUsd: number;
|
|
31
|
+
readonly durationMs: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
16
34
|
export class LimitError extends Error {
|
|
17
35
|
constructor(
|
|
18
36
|
public readonly kind: "timeout" | "tokens" | "budget" | "errors",
|
|
@@ -71,7 +89,7 @@ export class LimitGuard {
|
|
|
71
89
|
}
|
|
72
90
|
}
|
|
73
91
|
|
|
74
|
-
usage() {
|
|
92
|
+
usage(): UsageSnapshot {
|
|
75
93
|
return {
|
|
76
94
|
inputTokens: this.inputTokens,
|
|
77
95
|
outputTokens: this.outputTokens,
|