@hicaru/pi-rlm 0.2.1 → 0.2.2

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.
Files changed (61) hide show
  1. package/README.md +12 -35
  2. package/README.ru.md +18 -23
  3. package/README.zh-CN.md +17 -28
  4. package/package.json +1 -1
  5. package/src/bridge/library.ts +61 -26
  6. package/src/bridge/subcall-handlers.ts +63 -17
  7. package/src/commands/rlm-config.ts +47 -18
  8. package/src/commands/rlm.ts +3 -152
  9. package/src/config/defaults.ts +6 -17
  10. package/src/config/settings.ts +8 -32
  11. package/src/context/library-context.ts +90 -17
  12. package/src/core/engine.ts +55 -335
  13. package/src/core/history.ts +1 -1
  14. package/src/core/limits.ts +5 -12
  15. package/src/core/resource-limits.ts +0 -2
  16. package/src/core/types.ts +3 -36
  17. package/src/index.ts +23 -12
  18. package/src/mode/llm-model.ts +54 -0
  19. package/src/mode/rlm-mode.ts +26 -57
  20. package/src/prompts/glossary.ts +287 -0
  21. package/src/prompts/native.ts +127 -0
  22. package/src/prompts/system.ts +14 -407
  23. package/src/sandbox/context-file.ts +154 -0
  24. package/src/sandbox/interrupts.ts +145 -0
  25. package/src/sandbox/protocol.ts +8 -69
  26. package/src/sandbox/py/guards.py +150 -0
  27. package/src/sandbox/py/retrieval.py +265 -0
  28. package/src/sandbox/py/tasks.py +116 -0
  29. package/src/sandbox/{worker.py → py/worker.py} +76 -696
  30. package/src/sandbox/sandbox-manager.ts +13 -0
  31. package/src/sandbox/sandbox.ts +99 -193
  32. package/src/text/tokens.ts +29 -3
  33. package/src/tool/repl-details.ts +2 -2
  34. package/src/tool/repl-render.ts +58 -0
  35. package/src/tool/repl-result.ts +70 -0
  36. package/src/tool/repl-tool.ts +37 -159
  37. package/src/tool/rlm-aggregator.ts +2 -10
  38. package/src/tool/rlm-details.ts +0 -2
  39. package/src/tool/rlm-events.ts +0 -14
  40. package/src/tool/rlm-tool.ts +1 -12
  41. package/src/ui/config-panel.ts +4 -16
  42. package/src/ui/intro.ts +1 -2
  43. package/src/ui/model-picker.ts +34 -10
  44. package/src/ui/status.ts +3 -7
  45. package/src/util/concurrency.ts +9 -5
  46. package/src/bridge/fallback-todo.ts +0 -148
  47. package/src/bridge/interactive.ts +0 -65
  48. package/src/bridge/pi-interactive.ts +0 -41
  49. package/src/core/artifacts.ts +0 -89
  50. package/src/core/critique.ts +0 -92
  51. package/src/core/gates.ts +0 -301
  52. package/src/core/pipeline-handlers.ts +0 -319
  53. package/src/core/pipeline.ts +0 -268
  54. package/src/prompts/phases.ts +0 -104
  55. package/src/state/index.ts +0 -24
  56. package/src/state/internal.ts +0 -46
  57. package/src/state/paths.ts +0 -44
  58. package/src/state/reads.ts +0 -133
  59. package/src/state/resume.ts +0 -173
  60. package/src/state/rows.ts +0 -123
  61. package/src/state/writes.ts +0 -58
@@ -1,268 +0,0 @@
1
- /**
2
- * RLM pipeline stage graph — data-driven transitions with deterministic gates.
3
- *
4
- * Stages: clarify → research → blueprint → validate
5
- * (clarify skipped when askUserQuestion is off).
6
- * The root RLM writes artifacts via save_artifact(); advance_phase() is gated by
7
- * TypeScript floors (never LLM judgment). Validate routes on measured
8
- * blockers_count with a bounded corrective loop back to blueprint.
9
- * The pipeline is read-only by design: it produces a validated plan and does not
10
- * write source. Accidental sandbox writes are blocked (open/pathlib/os.open);
11
- * this is steering, not a hard security boundary.
12
- */
13
- import {
14
- checkStatusReady,
15
- clarificationRecord,
16
- type ClarificationGateData,
17
- type GateResult,
18
- planPhaseRecords,
19
- type PlanGateData,
20
- type ValidationGateData,
21
- validationRecord,
22
- verifyCitations,
23
- } from "./gates.ts";
24
-
25
- export type Phase = "clarify" | "research" | "blueprint" | "validate";
26
-
27
- export const PHASES = Object.freeze([
28
- "clarify",
29
- "research",
30
- "blueprint",
31
- "validate",
32
- ] as const satisfies readonly Phase[]);
33
-
34
- const PHASE_SET: ReadonlySet<string> = Object.freeze(new Set<string>(PHASES));
35
-
36
- export function isPhase(value: unknown): value is Phase {
37
- return typeof value === "string" && PHASE_SET.has(value);
38
- }
39
-
40
- /**
41
- * Map a persisted phase name onto the current graph. Trails written before the
42
- * implement phase was retired resume at `blueprint` — the last phase whose
43
- * artifact is still meaningful.
44
- */
45
- export function reconcilePhase(persisted: unknown): Phase {
46
- if (isPhase(persisted)) return persisted;
47
- return "blueprint";
48
- }
49
-
50
- /** Kind string the model passes to save_artifact(kind, content). */
51
- export type ArtifactKind = "clarification" | "research" | "plan" | "validation";
52
-
53
- /** Structured data a stage gate extracts from its artifact (what edges route on). */
54
- export type StageGateData =
55
- | { readonly kind: "clarification"; readonly clarification: ClarificationGateData }
56
- | { readonly kind: "research" }
57
- | { readonly kind: "plan"; readonly plan: PlanGateData }
58
- | { readonly kind: "validation"; readonly validation: ValidationGateData };
59
-
60
- export interface StageDef {
61
- readonly phase: Phase;
62
- /** Subdir under .rlm/artifacts/ ("" = side-effect stage, no artifact). */
63
- readonly artifactDir: string;
64
- /** save_artifact kind, or "" when the stage produces no artifact. */
65
- readonly artifactKind: ArtifactKind | "";
66
- /** Deterministic floor run on the artifact BEFORE leaving this stage. */
67
- readonly gate: (content: string, path: string, cwd: string) => GateResult<StageGateData>;
68
- }
69
-
70
- const clarifyGate: StageDef["gate"] = (content, path) => {
71
- const status = checkStatusReady(content, path);
72
- if (!status.ok) return status;
73
- const rec = clarificationRecord(content, path);
74
- if (!rec.ok) return rec;
75
- return { ok: true, value: { kind: "clarification", clarification: rec.value } };
76
- };
77
-
78
- /** Compose floors: status: ready → citations → stage-specific contract. */
79
- const researchGate: StageDef["gate"] = (content, path, cwd) => {
80
- const status = checkStatusReady(content, path);
81
- if (!status.ok) return status;
82
- const cites = verifyCitations(content, cwd);
83
- if (!cites.ok) return cites;
84
- return { ok: true, value: { kind: "research" } };
85
- };
86
-
87
- const blueprintGate: StageDef["gate"] = (content, path, cwd) => {
88
- const status = checkStatusReady(content, path);
89
- if (!status.ok) return status;
90
- const cites = verifyCitations(content, cwd);
91
- if (!cites.ok) return cites;
92
- const plan = planPhaseRecords(content, path);
93
- if (!plan.ok) return plan;
94
- return { ok: true, value: { kind: "plan", plan: plan.value } };
95
- };
96
-
97
- const validateGate: StageDef["gate"] = (content, path) => {
98
- const status = checkStatusReady(content, path);
99
- if (!status.ok) return status;
100
- const rec = validationRecord(content, path);
101
- if (!rec.ok) return rec;
102
- return { ok: true, value: { kind: "validation", validation: rec.value } };
103
- };
104
-
105
- /** Single source of truth for gates, artifact dirs/kinds, and routing. */
106
- export const STAGES: Readonly<Record<Phase, StageDef>> = Object.freeze({
107
- clarify: { phase: "clarify", artifactDir: "clarifications", artifactKind: "clarification", gate: clarifyGate },
108
- research: { phase: "research", artifactDir: "research", artifactKind: "research", gate: researchGate },
109
- blueprint: { phase: "blueprint", artifactDir: "plans", artifactKind: "plan", gate: blueprintGate },
110
- validate: { phase: "validate", artifactDir: "validations", artifactKind: "validation", gate: validateGate },
111
- });
112
-
113
- /** Lookup stage by save_artifact kind — single map derived from STAGES (DRY). */
114
- const STAGE_BY_KIND: Readonly<Partial<Record<ArtifactKind, StageDef>>> = Object.freeze(
115
- (Object.values(STAGES) as readonly StageDef[]).reduce<Partial<Record<ArtifactKind, StageDef>>>((acc, stage) => {
116
- if (stage.artifactKind !== "") acc[stage.artifactKind] = stage;
117
- return acc;
118
- }, {}),
119
- );
120
-
121
- export function stageForArtifactKind(kind: string): StageDef | undefined {
122
- if (kind === "clarification" || kind === "research" || kind === "plan" || kind === "validation") {
123
- return STAGE_BY_KIND[kind];
124
- }
125
- return undefined;
126
- }
127
-
128
- /** An artifact path plus its lifecycle status — append-only, never deleted. */
129
- export interface ArtifactRef {
130
- readonly path: string;
131
- readonly status: "active" | "superseded";
132
- /** Path of the artifact that superseded this one (the rejecting validation). */
133
- readonly supersededBy?: string;
134
- }
135
-
136
- /** The latest artifact saved for a stage, plus its gate result when it passed. */
137
- export interface SavedArtifact {
138
- readonly path: string;
139
- /** Gate payload when the artifact passed; undefined ⇒ advance_phase must re-gate. */
140
- readonly gateData?: StageGateData;
141
- }
142
-
143
- export interface PhaseState {
144
- readonly current: Phase;
145
- readonly advancedAt: number;
146
- readonly summary?: string;
147
- /** Artifact each completed stage produced — the named channels. */
148
- readonly artifacts: Readonly<Partial<Record<Phase, ArtifactRef>>>;
149
- /** Corrective validate→blueprint re-entries taken so far. */
150
- readonly backwardJumps: number;
151
- }
152
-
153
- /**
154
- * @param start — first phase of the run (`clarify` when interviews are on, else `research`).
155
- */
156
- export function initialPhaseState(advancedAt = 0, start: Phase = "clarify"): PhaseState {
157
- return { current: start, advancedAt, artifacts: Object.freeze({}), backwardJumps: 0 };
158
- }
159
-
160
- export type RouteDecision =
161
- | { readonly kind: "done" }
162
- | { readonly kind: "loop-back"; readonly next: "blueprint" }
163
- | { readonly kind: "halt"; readonly reason: string };
164
-
165
- /**
166
- * Route out of `validate` on MEASURED gate data: blockers_count === 0 → done;
167
- * blockers_count > 0 → loop back to blueprint, bounded by maxBackwardJumps.
168
- */
169
- export function routeAfterValidate(
170
- data: ValidationGateData,
171
- backwardJumps: number,
172
- maxBackwardJumps: number,
173
- ): RouteDecision {
174
- if (data.blockersCount === 0) return { kind: "done" };
175
- if (backwardJumps >= maxBackwardJumps) {
176
- return {
177
- kind: "halt",
178
- reason: `validation reports ${data.blockersCount} blocker(s) after ${backwardJumps} corrective pass(es) — backward-jump limit (${maxBackwardJumps}) reached; surfacing the validation report as the final answer`,
179
- };
180
- }
181
- return { kind: "loop-back", next: "blueprint" };
182
- }
183
-
184
- /** Forward transitions only; corrective loop-back is ENGINE-initiated via routeAfterValidate. */
185
- export function nextForward(current: Phase): Phase | undefined {
186
- const idx = PHASES.indexOf(current);
187
- return idx >= 0 && idx < PHASES.length - 1 ? PHASES[idx + 1] : undefined;
188
- }
189
-
190
- export interface AdvancePhaseResult {
191
- readonly ok: true;
192
- readonly phase: Phase;
193
- }
194
-
195
- export interface AdvancePhaseFailure {
196
- readonly ok: false;
197
- readonly error: string;
198
- readonly phase: Phase;
199
- }
200
-
201
- export type AdvancePhaseOutcome = AdvancePhaseResult | AdvancePhaseFailure;
202
-
203
- /**
204
- * Pure order check: only the immediate next phase is allowed.
205
- * Artifact gates are applied by the engine after this returns ok.
206
- */
207
- export function advancePhase(
208
- current: Phase | undefined,
209
- target: string,
210
- ): AdvancePhaseOutcome {
211
- if (!isPhase(target)) {
212
- return {
213
- ok: false,
214
- error: `unknown phase '${target}'; valid phases: ${PHASES.join(", ")}`,
215
- phase: current ?? PHASES[0],
216
- };
217
- }
218
- const from = current ?? PHASES[0];
219
- const expected = nextForward(from);
220
- if (expected === undefined) {
221
- return {
222
- ok: false,
223
- error: `'${from}' is the terminal phase; finalize via answer["ready"] = True after saving the validation artifact`,
224
- phase: from,
225
- };
226
- }
227
- if (target !== expected) {
228
- return {
229
- ok: false,
230
- error: `cannot advance from '${from}' to '${target}' — the next phase is '${expected}'`,
231
- phase: from,
232
- };
233
- }
234
- return { ok: true, phase: target };
235
- }
236
-
237
- /** Return the current phase (defaults to first phase if undefined). */
238
- export function currentPhase(state: PhaseState | undefined): Phase {
239
- return state?.current ?? PHASES[0];
240
- }
241
-
242
- /** Return the number of turns spent in the current phase. */
243
- export function turnsInPhase(state: PhaseState | undefined, completedTurns: number): number {
244
- return state ? completedTurns - state.advancedAt : completedTurns;
245
- }
246
-
247
- /** PHASE_GATE_TURNS: if the model stays in one phase for this many turns, the engine re-prompts. */
248
- export const PHASE_GATE_TURNS = 4;
249
-
250
- /** Produce a re-prompt message when the model stalls in a phase for too long. */
251
- export function phaseGatePrompt(
252
- state: PhaseState | undefined,
253
- completedTurns: number,
254
- ): string | undefined {
255
- const turns = turnsInPhase(state, completedTurns);
256
- const phase = currentPhase(state);
257
- if (turns >= PHASE_GATE_TURNS && turns % PHASE_GATE_TURNS === 0) {
258
- const next = nextForward(phase);
259
- const hint = next
260
- ? ` Consider calling advance_phase("${next}") if your ${phase} work is complete (after save_artifact when required).`
261
- : "";
262
- return [
263
- `You have spent ${turns} turns in the '${phase}' phase.`,
264
- `If the ${phase} phase is complete, advance to the next phase.${hint}`,
265
- ].join(" ");
266
- }
267
- return undefined;
268
- }
@@ -1,104 +0,0 @@
1
- /**
2
- * Per-phase guidance for the gated RLM pipeline.
3
- * Injected at each phase boundary; the only channel between phases is durable
4
- * artifacts under .rlm/artifacts/.
5
- */
6
- import type { Phase } from "../core/pipeline.ts";
7
-
8
- const CLARIFY_GUIDANCE = Object.freeze([
9
- "## Clarify phase (intake interview)",
10
- "Interview the user until the task is understood. The engine COUNTS ask_user_question",
11
- "rounds it services — you cannot advance without having actually asked at least once.",
12
- "",
13
- "1. Intent first: ask ONE open-ended intent question (what problem, who hits it, what",
14
- " success looks like). Options are answer *shapes* (e.g. end user / maintainer / operator);",
15
- " do NOT mark a Recommended option. The automatic Other / free-text path carries the real framing.",
16
- " Up to 2 more intent rounds if the answer still cannot scope a narrow probe; then proceed",
17
- " with what you have.",
18
- "2. Probe before asking more: ground follow-ups in the REPL (deterministic search over",
19
- " context, a few llm_query reads) so questions cite real code as file:line.",
20
- "3. Confirm inferences, don't record them: batch inferred decisions into one call —",
21
- " \"From the code I inferred <behavior> (file:line). Keep or change?\" The user's answer",
22
- " is the Decision, not your inference.",
23
- "4. Scope / shape / detail rounds, one at a time: recommended option first; architecture",
24
- " (shape) options must state the tradeoff (\"optimizes X, costs Y\") — never one option",
25
- " masquerading as a choice; 2–4 independent detail questions may batch in one call.",
26
- "5. Classify answers: Decision → record; Correction → re-probe that seam, re-ask dependents;",
27
- " Defer → ## Open Questions.",
28
- "6. Terminate on depth, not politeness: stop when every raised branch has a Decision or a",
29
- " Deferral and ## Problem & Intent holds the user's own words verbatim. Do not pad the",
30
- " interview; do not ask a final \"looks good?\" rubber-stamp.",
31
- "7. Then save_artifact(\"clarification\", content) with frontmatter:",
32
- " status: ready",
33
- " decisions_count: N",
34
- " open_questions_count: M",
35
- " and body sections:",
36
- " ## Problem & Intent (user's words VERBATIM)",
37
- " ## Decisions (one '- ' bullet per decision)",
38
- " ## Open Questions (one '- ' bullet per deferral; may be empty with count 0)",
39
- " ## Non-Goals",
40
- ' Then advance_phase("research", summary).',
41
- ].join("\n"));
42
-
43
- const RESEARCH_GUIDANCE = Object.freeze([
44
- "## Research phase",
45
- "Read the clarifications artifact (if present) and honor recorded Decisions; do not silently",
46
- "resolve Open Questions.",
47
- "Probe the repository, delegate long reads via `llm_query_batched` / `llm_query_chunked`.",
48
- "Every factual claim about the code MUST be cited as `path/file.ext:LINE` (or `LINE-LINE`).",
49
- "The engine VERIFIES citations against the working tree — unbacked citations reject advance.",
50
- "When research is complete, write ONE research document with a `## Findings` section and call:",
51
- ' `save_artifact("research", content)` # frontmatter must include `status: ready`',
52
- ' `advance_phase("blueprint", summary)`',
53
- "Do not implement code in this phase.",
54
- ].join("\n"));
55
-
56
- const BLUEPRINT_GUIDANCE = Object.freeze([
57
- "## Blueprint phase",
58
- "Read the clarifications artifact (if present) and plan only within recorded Decisions;",
59
- "Open Questions that would block the design must be re-asked via ask_user_question, not assumed.",
60
- 'Produce ONE plan document and save it with `save_artifact("plan", content)`.',
61
- "This pipeline is read-only: it produces a plan a human or a native-mode agent executes;",
62
- "do not write source files here (sandbox write modes are blocked).",
63
- "Write changes as exact, copy-pasteable before/after code — never as prose instructions.",
64
- "The ENGINE derive-checks the document — these are hard gates, not suggestions:",
65
- "- frontmatter: `status: ready`, `phase_count: N`, `phases: [{n: 1, title: ...}, ...]`",
66
- "- `phases:` / `phase_count` MUST match the `## Phase N:` body headings exactly (fenced examples ignored)",
67
- "- every `file:line` citation MUST resolve against the working tree at this revision",
68
- "Each `## Phase N: <title>` section contains:",
69
- "- `### Changes Required` — per file: path + the exact intended change",
70
- "- `### Success Criteria` — `#### Automated Verification:` (runnable commands) and",
71
- " `#### Manual Verification:` checklists",
72
- "Phases must be independently implementable in order, each leaving the tree working.",
73
- 'When ready: `advance_phase("validate", summary)`.',
74
- ].join("\n"));
75
-
76
- const VALIDATE_GUIDANCE = Object.freeze([
77
- "## Validate phase (adversarial plan review)",
78
- "Nothing has been implemented — you are reviewing the PLAN, not a diff.",
79
- "Read the goal artifact (verbatim brief), the clarifications artifact (recorded Decisions),",
80
- "and the plan. For each plan phase check against the working tree:",
81
- "- every cited `file:line` still resolves and says what the plan claims",
82
- "- every file the phase edits exists (or is created by an earlier phase)",
83
- "- no phase contradicts a recorded Decision or silently resolves an Open Question",
84
- "- the Success Criteria are actually runnable and actually prove the change",
85
- "- phases are independently implementable in the stated order",
86
- "Write ONE review via `save_artifact(\"validation\", content)` with frontmatter:",
87
- "- `status: ready`",
88
- "- `blockers_count: <int >= 0>` # MEASURED — routing key; not prose",
89
- "- `verdict: pass | fail` # pass requires blockers_count === 0",
90
- "Each blocker needs a resolvable `file:line` citation.",
91
- "Then finalize: `answer[\"content\"] = <the reviewed plan + review summary>; answer[\"ready\"] = True`.",
92
- "If `blockers_count > 0`, the engine re-enters blueprint (bounded by maxBackwardJumps).",
93
- ].join("\n"));
94
-
95
- const GUIDANCE: Readonly<Record<Phase, string>> = Object.freeze({
96
- clarify: CLARIFY_GUIDANCE,
97
- research: RESEARCH_GUIDANCE,
98
- blueprint: BLUEPRINT_GUIDANCE,
99
- validate: VALIDATE_GUIDANCE,
100
- });
101
-
102
- export function phaseGuidance(phase: Phase): string {
103
- return GUIDANCE[phase];
104
- }
@@ -1,24 +0,0 @@
1
- /**
2
- * Barrel for the RLM run-state module.
3
- *
4
- * Re-exports every public symbol so `core/engine.ts` and `mode/rlm-mode.ts`
5
- * import from one door. Type-only re-exports use `export type`.
6
- */
7
-
8
- export { generateRunId, runsDir, runDir, trailPath, contextPath, snapshotPath } from "./paths.ts";
9
- export type {
10
- UsageRow,
11
- RunHeader,
12
- TurnRow,
13
- CompactionRow,
14
- TerminalRow,
15
- TodoRow,
16
- PhaseRow,
17
- Row,
18
- } from "./rows.ts";
19
- export { STATE_SCHEMA_VERSION, isHeader, isTurn, isCompaction, isPhase, isTodo, isTerminal, isRow } from "./rows.ts";
20
- export { appendRow, appendTodoRow, pruneRuns, writeContextSidecar } from "./writes.ts";
21
- export { readRows, readHeader, readContextSidecar, readLibrarySidecars, listRunIds, resolveRunId } from "./reads.ts";
22
- export type { LibrarySlot } from "./reads.ts";
23
- export { reconstructRlmState } from "./resume.ts";
24
- export type { PhaseRecon, ReconstructResult } from "./resume.ts";
@@ -1,46 +0,0 @@
1
- /** Internal helpers shared across the RLM run-state module. */
2
-
3
- import { access, readdir } from "node:fs/promises";
4
- import { errorMessage } from "../util/errors.ts";
5
-
6
- export { errorMessage } from "../util/errors.ts";
7
-
8
- export interface FailSoftOptions {
9
- readonly label?: string;
10
- readonly warn?: boolean;
11
- }
12
-
13
- const DEFAULT_FAIL_SOFT_OPTIONS = Object.freeze({});
14
-
15
- export const warn = (e: unknown): void => console.warn(`[rlm-state] ${errorMessage(e)}`);
16
-
17
- export async function failSoft<T>(
18
- fn: () => Promise<T>,
19
- fallback: T,
20
- options: FailSoftOptions = DEFAULT_FAIL_SOFT_OPTIONS,
21
- ): Promise<T> {
22
- try {
23
- return await fn();
24
- } catch (e) {
25
- if (options.warn !== false) warn(options.label ? `${options.label}: ${errorMessage(e)}` : e);
26
- return fallback;
27
- }
28
- }
29
-
30
- export async function listDirectoriesSorted(root: string): Promise<string[]> {
31
- const entries = await readdir(root, { withFileTypes: true });
32
- return entries
33
- .filter((entry) => entry.isDirectory())
34
- .map((entry) => entry.name)
35
- .sort()
36
- .reverse();
37
- }
38
-
39
- export async function pathExists(path: string): Promise<boolean> {
40
- try {
41
- await access(path);
42
- return true;
43
- } catch {
44
- return false;
45
- }
46
- }
@@ -1,44 +0,0 @@
1
- /**
2
- * Pure path/id helpers for the RLM run-state module.
3
- *
4
- * Run-IDs are filename-sortable ISO-like slugs with a random hex suffix
5
- * for sub-second collision safety. All helpers are pure — no I/O.
6
- */
7
-
8
- import { randomBytes } from "node:crypto";
9
- import { isAbsolute, join } from "node:path";
10
-
11
- const RUN_ID_SUFFIX_BYTES = 2;
12
- const ISO_DATETIME_LENGTH = 19;
13
-
14
- /** `YYYY-MM-DD_HH-MM-SS-<4hex>` — filename-sortable, sub-second collision-safe.
15
- *
16
- * Prune ordering in writes.ts:pruneRuns depends on the ISO-slug format producing
17
- * chronologically sortable strings. If the format changes, update pruning logic
18
- * to maintain oldest-first deletion. */
19
- export function generateRunId(
20
- now: Date = new Date(),
21
- suffix: string = randomBytes(RUN_ID_SUFFIX_BYTES).toString("hex"),
22
- ): string {
23
- const pad = (n: number): string => String(n).padStart(2, "0");
24
- const iso = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}T${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`;
25
- return `${iso.slice(0, ISO_DATETIME_LENGTH).replaceAll(":", "-").replace("T", "_")}-${suffix}`;
26
- }
27
-
28
- export const runsDir = (cwd: string, dir: string): string =>
29
- isAbsolute(dir) ? dir : join(cwd, dir);
30
-
31
- export const runDir = (cwd: string, dir: string, runId: string): string => join(runsDir(cwd, dir), runId);
32
-
33
- export const trailPath = (cwd: string, dir: string, runId: string): string => join(runDir(cwd, dir, runId), "trail.jsonl");
34
-
35
- export const contextPath = (cwd: string, dir: string, runId: string, json: boolean, index = 0): string =>
36
- join(runDir(cwd, dir, runId), index === 0
37
- ? (json ? "context.json" : "context.txt")
38
- : `context.${index}.${json ? "json" : "txt"}`);
39
-
40
- /** R-C1: per-turn snapshot files — `sandbox-<turn>.pkl` so resume can fall back to a prior turn if the latest rename failed. */
41
- export function snapshotPath(cwd: string, dir: string, runId: string, turn?: number): string {
42
- const name = turn !== undefined ? `sandbox-${turn}.pkl` : "sandbox.pkl";
43
- return join(runDir(cwd, dir, runId), name);
44
- }
@@ -1,133 +0,0 @@
1
- /**
2
- * Fail-soft JSONL readers for the RLM run-state module.
3
- *
4
- * `readRows` parses each line in its own try/catch so a truncated trailing
5
- * line cannot erase prior rows. `listRunIds` sorts directories newest-first
6
- * by the slug (ISO-like timestamps are self-sorting).
7
- */
8
-
9
- import { open, readdir, readFile } from "node:fs/promises";
10
- import { join } from "node:path";
11
- import { runsDir, runDir, trailPath, contextPath } from "./paths.ts";
12
- import { isHeader, isRow, type RunHeader, type Row } from "./rows.ts";
13
- import { errorMessage, failSoft, listDirectoriesSorted, pathExists, warn } from "./internal.ts";
14
-
15
- /**
16
- * Raw JSONL lines of a run's trail, in order. Missing or unreadable file → [].
17
- * Shared by the fail-soft reader here and the hole-detecting reader in resume.ts, which
18
- * differ only in how they treat a bad line.
19
- */
20
- export async function readTrailLines(cwd: string, dir: string, runId: string): Promise<string[]> {
21
- const path = trailPath(cwd, dir, runId);
22
- if (!await pathExists(path)) return [];
23
- const content = await failSoft(
24
- () => readFile(path, "utf-8"),
25
- undefined as string | undefined,
26
- );
27
- const trimmed = content?.trim();
28
- return trimmed ? trimmed.split("\n") : [];
29
- }
30
-
31
- /** Every well-formed row, in trail order. Malformed line → one warn, skipped. */
32
- export async function readRows(cwd: string, dir: string, runId: string): Promise<Row[]> {
33
- const lines = await readTrailLines(cwd, dir, runId);
34
- const rows: Row[] = [];
35
- for (const line of lines) {
36
- try {
37
- const row = JSON.parse(line) as unknown;
38
- if (isRow(row)) rows.push(row);
39
- else warn("skipping invalid JSONL row shape");
40
- } catch (e) {
41
- warn(`skipping malformed JSONL row — ${errorMessage(e)}`);
42
- }
43
- }
44
- return rows;
45
- }
46
-
47
- /** Read the first line of a trail file without reading the entire file (P1). */
48
- async function readFirstLine(path: string): Promise<string | undefined> {
49
- return await failSoft(async () => {
50
- const file = await open(path, "r");
51
- try {
52
- const stats = await file.stat();
53
- const size = Math.min(stats.size, 65536);
54
- if (size <= 0) return undefined;
55
- const buffer = Buffer.alloc(size);
56
- const { bytesRead } = await file.read(buffer, 0, size, 0);
57
- const content = buffer.toString("utf-8", 0, bytesRead);
58
- const nl = content.indexOf("\n");
59
- return nl >= 0 ? content.slice(0, nl) : content.trim() || undefined;
60
- } finally {
61
- await file.close();
62
- }
63
- }, undefined as string | undefined, { warn: false });
64
- }
65
-
66
- /** First well-formed header row, or undefined. Bounded read — never reads the full trail (P1). */
67
- export async function readHeader(cwd: string, dir: string, runId: string): Promise<RunHeader | undefined> {
68
- const line = await readFirstLine(trailPath(cwd, dir, runId));
69
- if (!line) return undefined;
70
- try {
71
- const row = JSON.parse(line) as unknown;
72
- return isHeader(row) ? row : undefined;
73
- } catch {
74
- return undefined;
75
- }
76
- }
77
-
78
- /** Reload context from a persistent sidecar file. */
79
- export async function readContextSidecar(cwd: string, dir: string, runId: string, json: boolean): Promise<unknown> {
80
- const path = contextPath(cwd, dir, runId, json);
81
- if (!await pathExists(path)) return undefined;
82
- const content = await failSoft(
83
- () => readFile(path, "utf-8"),
84
- undefined as string | undefined,
85
- );
86
- if (content === undefined) return undefined;
87
- try {
88
- return json ? JSON.parse(content) as unknown : content;
89
- } catch (e) {
90
- warn(e);
91
- return undefined;
92
- }
93
- }
94
-
95
- const LIBRARY_SIDECAR = /^context\.(\d+)\.(json|txt)$/;
96
-
97
- export interface LibrarySlot {
98
- readonly index: number;
99
- readonly payload: unknown;
100
- }
101
-
102
- /** Fail-soft lister for load_library resume sidecars (`context.<index>.json|txt`). */
103
- export async function readLibrarySidecars(cwd: string, dir: string, runId: string): Promise<LibrarySlot[]> {
104
- const entries = await failSoft(() => readdir(runDir(cwd, dir, runId)), [] as string[]);
105
- const slots: LibrarySlot[] = [];
106
- for (const name of entries) {
107
- const m = LIBRARY_SIDECAR.exec(name);
108
- if (!m) continue;
109
- const index = Number(m[1]);
110
- const json = m[2] === "json";
111
- const content = await failSoft(
112
- () => readFile(join(runDir(cwd, dir, runId), name), "utf-8"),
113
- undefined as string | undefined,
114
- );
115
- if (content === undefined) continue;
116
- try {
117
- slots.push({ index, payload: json ? JSON.parse(content) as unknown : content });
118
- } catch (e) { warn(e); }
119
- }
120
- return slots.sort((a, b) => a.index - b.index);
121
- }
122
-
123
- /** Enumerate run-ids by directory listing; newest first (slug sorts chronologically). */
124
- export async function listRunIds(cwd: string, dir: string): Promise<string[]> {
125
- return await failSoft(() => listDirectoriesSorted(runsDir(cwd, dir)), [], { warn: false });
126
- }
127
-
128
- /** `@latest` / explicit id resolution. */
129
- export async function resolveRunId(cwd: string, dir: string, ref: string): Promise<string | undefined> {
130
- const ids = await listRunIds(cwd, dir);
131
- if (ref === "@latest") return ids[0];
132
- return ids.includes(ref) ? ref : undefined;
133
- }