@hicaru/pi-rlm 0.1.8 → 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.
Files changed (52) hide show
  1. package/README.md +22 -19
  2. package/package.json +2 -1
  3. package/src/bridge/library.ts +93 -15
  4. package/src/bridge/llm-query.ts +60 -36
  5. package/src/bridge/rlm-query.ts +63 -79
  6. package/src/commands/rlm-config.ts +8 -8
  7. package/src/commands/rlm.ts +48 -12
  8. package/src/config/settings.ts +33 -3
  9. package/src/context/library-context.ts +209 -22
  10. package/src/context/repomix-context.ts +7 -58
  11. package/src/core/answer.ts +5 -13
  12. package/src/core/artifacts.ts +4 -3
  13. package/src/core/critique.ts +92 -0
  14. package/src/core/engine.ts +94 -299
  15. package/src/core/gates.ts +33 -4
  16. package/src/core/limits.ts +19 -1
  17. package/src/core/pipeline-handlers.ts +319 -0
  18. package/src/core/pipeline.ts +40 -15
  19. package/src/core/types.ts +26 -30
  20. package/src/index.ts +36 -26
  21. package/src/mode/native-guards.ts +2 -2
  22. package/src/mode/rlm-mode.ts +8 -11
  23. package/src/prompts/phases.ts +18 -39
  24. package/src/prompts/system.ts +167 -64
  25. package/src/prompts/user.ts +1 -5
  26. package/src/sandbox/protocol.ts +5 -17
  27. package/src/sandbox/sandbox-manager.ts +5 -5
  28. package/src/sandbox/sandbox.ts +67 -27
  29. package/src/sandbox/worker.py +534 -48
  30. package/src/state/paths.ts +1 -1
  31. package/src/state/reads.ts +12 -4
  32. package/src/state/resume.ts +26 -25
  33. package/src/state/rows.ts +2 -2
  34. package/src/text/parsing.ts +0 -6
  35. package/src/text/tokens.ts +7 -1
  36. package/src/tool/repl-details.ts +2 -3
  37. package/src/tool/repl-tool.ts +132 -337
  38. package/src/tool/rlm-aggregator.ts +7 -7
  39. package/src/tool/rlm-details.ts +6 -13
  40. package/src/tool/rlm-events.ts +14 -11
  41. package/src/tool/rlm-tool.ts +20 -38
  42. package/src/tool/subcall-render.ts +61 -9
  43. package/src/tool/subcall-store.ts +4 -2
  44. package/src/ui/config-panel.ts +43 -23
  45. package/src/ui/intro.ts +2 -1
  46. package/src/ui/status.ts +8 -5
  47. package/src/ui/theme-adapter.ts +36 -0
  48. package/src/ui/theme.ts +0 -25
  49. package/src/mode/input-router.ts +0 -23
  50. package/src/registry/edit-registry.ts +0 -22
  51. package/src/text/edits.ts +0 -164
  52. package/src/tool/apply-edits-tool.ts +0 -295
@@ -20,7 +20,7 @@ export function generateRunId(
20
20
  now: Date = new Date(),
21
21
  suffix: string = randomBytes(RUN_ID_SUFFIX_BYTES).toString("hex"),
22
22
  ): string {
23
- const pad = (n: number) => String(n).padStart(2, "0");
23
+ const pad = (n: number): string => String(n).padStart(2, "0");
24
24
  const iso = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}T${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`;
25
25
  return `${iso.slice(0, ISO_DATETIME_LENGTH).replaceAll(":", "-").replace("T", "_")}-${suffix}`;
26
26
  }
@@ -12,8 +12,12 @@ import { runsDir, runDir, trailPath, contextPath } from "./paths.ts";
12
12
  import { isHeader, isRow, type RunHeader, type Row } from "./rows.ts";
13
13
  import { errorMessage, failSoft, listDirectoriesSorted, pathExists, warn } from "./internal.ts";
14
14
 
15
- /** Every well-formed row, in trail order. Malformed line → one warn, skipped. */
16
- export async function readRows(cwd: string, dir: string, runId: string): Promise<Row[]> {
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[]> {
17
21
  const path = trailPath(cwd, dir, runId);
18
22
  if (!await pathExists(path)) return [];
19
23
  const content = await failSoft(
@@ -21,10 +25,14 @@ export async function readRows(cwd: string, dir: string, runId: string): Promise
21
25
  undefined as string | undefined,
22
26
  );
23
27
  const trimmed = content?.trim();
24
- if (!trimmed) return [];
28
+ return trimmed ? trimmed.split("\n") : [];
29
+ }
25
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);
26
34
  const rows: Row[] = [];
27
- for (const line of trimmed.split("\n")) {
35
+ for (const line of lines) {
28
36
  try {
29
37
  const row = JSON.parse(line) as unknown;
30
38
  if (isRow(row)) rows.push(row);
@@ -7,12 +7,10 @@
7
7
  * garbage from a crash is tolerated.
8
8
  */
9
9
 
10
- import { readFile } from "node:fs/promises";
11
10
  import { type ChatMsg } from "../bridge/model.ts";
12
11
  import { appendUserMessage } from "../core/history.ts";
13
12
  import { buildTurnPrompt } from "../prompts/user.ts";
14
- import type { ProposedEdit } from "../sandbox/protocol.ts";
15
- import { readHeader } from "./reads.ts";
13
+ import { readHeader, readTrailLines } from "./reads.ts";
16
14
  import {
17
15
  isCompaction,
18
16
  isHeader,
@@ -25,15 +23,21 @@ import {
25
23
  type Row,
26
24
  type RunHeader,
27
25
  } from "./rows.ts";
28
- import { trailPath, snapshotPath } from "./paths.ts";
29
- import { failSoft, pathExists } from "./internal.ts";
26
+ import { snapshotPath } from "./paths.ts";
27
+ import { pathExists } from "./internal.ts";
28
+
29
+ /** Artifact path + supersede flag reconstructed from phase rows. */
30
+ export interface PhaseReconArtifact {
31
+ readonly path: string;
32
+ readonly superseded: boolean;
33
+ }
30
34
 
31
35
  export interface PhaseRecon {
32
36
  readonly current: string;
33
37
  readonly advancedAt: number;
34
38
  readonly summary?: string;
35
- /** Repo-relative artifact paths keyed by the phase that produced them. */
36
- readonly artifacts?: Readonly<Partial<Record<string, string>>>;
39
+ /** Repo-relative artifacts keyed by the phase that produced them. */
40
+ readonly artifacts?: Readonly<Partial<Record<string, PhaseReconArtifact>>>;
37
41
  readonly backwardJumps?: number;
38
42
  }
39
43
 
@@ -45,7 +49,6 @@ export type ReconstructResult =
45
49
  readonly pendingReplOutputs?: string;
46
50
  readonly usageSeed: { readonly costUsd: number; readonly inputTokens: number; readonly outputTokens: number; readonly durationMs: number };
47
51
  readonly best: string;
48
- readonly editsAcc: ProposedEdit[];
49
52
  readonly completedTurns: number;
50
53
  readonly compactions: number;
51
54
  /** R-C1: the latest turn whose per-turn snapshot file exists on disk (undefined ⇒ no restore). */
@@ -59,15 +62,10 @@ export type ReconstructResult =
59
62
 
60
63
  /** QB: single read + parse — detects mid-file holes without reading the trail twice. */
61
64
  async function readRowsStrict(cwd: string, dir: string, runId: string): Promise<{ readonly rows: Row[]; readonly hole: boolean }> {
62
- const path = trailPath(cwd, dir, runId);
63
- if (!await pathExists(path)) return { rows: [], hole: false };
64
- const content = await failSoft(() => readFile(path, "utf-8"), undefined as string | undefined);
65
- const trimmed = content?.trim();
66
- if (!trimmed) return { rows: [], hole: false };
67
-
65
+ const lines = await readTrailLines(cwd, dir, runId);
68
66
  const rows: Row[] = [];
69
67
  let sawBad = false;
70
- for (const line of trimmed.split("\n")) {
68
+ for (const line of lines) {
71
69
  try {
72
70
  const row = JSON.parse(line) as unknown;
73
71
  if (!isRow(row)) {
@@ -102,7 +100,6 @@ export async function reconstructRlmState(
102
100
  let history: ChatMsg[] = [{ role: "system", content: systemPrompt }];
103
101
  const usageSeed = { costUsd: 0, inputTokens: 0, outputTokens: 0, durationMs: 0 };
104
102
  let best = "";
105
- let editsAcc: ProposedEdit[] = [];
106
103
  let completedTurns = 0;
107
104
  let compactions = 0;
108
105
  let snapshotTurn: number | undefined; // R-C1: latest turn with an existing snapshot file
@@ -110,8 +107,8 @@ export async function reconstructRlmState(
110
107
  const todoRows: { action: string; params: Record<string, unknown>; result: string }[] = [];
111
108
  let terminated = false;
112
109
  let phase: PhaseRecon | undefined;
113
- // Accumulate artifact paths keyed by the producing phase (artifactPhase on the row).
114
- const artifactsAcc: Record<string, string> = {};
110
+ // Append-only journal: paths stay; supersededPath flips the blueprint slot.
111
+ const artifactsAcc = new Map<string, PhaseReconArtifact>();
115
112
 
116
113
  for (const row of rows) {
117
114
  if (isHeader(row)) continue;
@@ -134,7 +131,6 @@ export async function reconstructRlmState(
134
131
  usageSeed.outputTokens += row.usage.outputTokens;
135
132
  if (row.answerContent) best = row.answerContent;
136
133
  else if (!best && row.response.trim()) best = row.response; // C3: mirror engine fallback
137
- if (row.edits && row.edits.length > 0) editsAcc = [...row.edits];
138
134
  completedTurns = row.turn;
139
135
  // R-C1: verify the per-turn snapshot file exists — a crashed finalize leaves the row claiming snapshotOk:true with no pkl.
140
136
  if (row.snapshotOk && await pathExists(snapshotPath(cwd, dir, runId, row.turn)))
@@ -145,17 +141,22 @@ export async function reconstructRlmState(
145
141
  }
146
142
  if (isPhase(row)) {
147
143
  if (row.artifactPath !== undefined && row.artifactPhase !== undefined) {
148
- artifactsAcc[row.artifactPhase] = row.artifactPath;
144
+ artifactsAcc.set(row.artifactPhase, { path: row.artifactPath, superseded: false });
149
145
  }
150
- // On loop-back to blueprint, drop stale plan so resume cannot re-gate with it.
151
- if (row.phase === "blueprint" && row.backwardJumps !== undefined && row.backwardJumps > 0) {
152
- delete artifactsAcc.blueprint;
146
+ // Append-only: a superseded artifact keeps its slot, flipped to superseded.
147
+ if (row.supersededPath !== undefined) {
148
+ const prior = artifactsAcc.get("blueprint");
149
+ if (prior !== undefined) {
150
+ artifactsAcc.set("blueprint", { path: prior.path, superseded: true });
151
+ }
153
152
  }
153
+ const artifactsObj: Record<string, PhaseReconArtifact> = {};
154
+ for (const [k, v] of artifactsAcc) artifactsObj[k] = v;
154
155
  phase = {
155
156
  current: row.phase,
156
157
  advancedAt: row.turn - 1,
157
158
  summary: row.summary,
158
- artifacts: Object.keys(artifactsAcc).length > 0 ? { ...artifactsAcc } : undefined,
159
+ artifacts: artifactsAcc.size > 0 ? artifactsObj : undefined,
159
160
  backwardJumps: row.backwardJumps,
160
161
  };
161
162
  continue;
@@ -168,5 +169,5 @@ export async function reconstructRlmState(
168
169
  }
169
170
 
170
171
  if (completedTurns === 0 && !terminated) return { ok: false, reason: "no-turns", detail: runId };
171
- return { ok: true, header, history, pendingReplOutputs, usageSeed, best, editsAcc, completedTurns, compactions, snapshotTurn, todoRows, terminated, phase };
172
+ return { ok: true, header, history, pendingReplOutputs, usageSeed, best, completedTurns, compactions, snapshotTurn, todoRows, terminated, phase };
172
173
  }
package/src/state/rows.ts CHANGED
@@ -9,7 +9,6 @@
9
9
  */
10
10
 
11
11
  import type { ChatMsg } from "../bridge/model.ts";
12
- import type { ProposedEdit } from "../sandbox/protocol.ts";
13
12
 
14
13
  /** Bump when a row shape changes such that the resume fold cannot replay older files. */
15
14
  export const STATE_SCHEMA_VERSION = 5;
@@ -47,7 +46,6 @@ export interface TurnRow {
47
46
  readonly response: string; // assistant message
48
47
  readonly replOutputs?: string; // formatReplOutputs(results) → next user message
49
48
  readonly answerContent?: string; // restores `best`
50
- readonly edits?: readonly ProposedEdit[]; // restores editsAcc (latest wins)
51
49
  readonly error: boolean; // turnHadError → limits.observe on resume
52
50
  readonly usage: UsageRow;
53
51
  readonly cumulativeDurationMs: number; // limits.usage().durationMs at turn-write time
@@ -94,6 +92,8 @@ export interface PhaseRow {
94
92
  readonly artifactPhase?: string;
95
93
  readonly blockersCount?: number;
96
94
  readonly backwardJumps?: number;
95
+ /** Path of the artifact this transition superseded (validate loop-back). */
96
+ readonly supersededPath?: string;
97
97
  }
98
98
 
99
99
  export type Row = RunHeader | TurnRow | CompactionRow | TodoRow | TerminalRow | PhaseRow;
@@ -19,12 +19,6 @@ export function findReplBlocks(text: string): string[] {
19
19
  return blocks;
20
20
  }
21
21
 
22
- /** True if the response contains at least one runnable ```repl``` block. */
23
- export function hasReplBlock(text: string): boolean {
24
- FENCE.lastIndex = 0;
25
- return FENCE.test(text);
26
- }
27
-
28
22
  /** Truncate REPL stdout for the model's context window (head + tail, with an elision note). */
29
23
  export function truncateOutput(text: string, limit = 20_000): string {
30
24
  if (text.length <= limit) return text;
@@ -8,11 +8,17 @@
8
8
 
9
9
  const CHARS_PER_TOKEN = 4;
10
10
 
11
+ /** Rough token count for a character length (≈4 chars/token). Always ≥ 1 for non-empty text. */
12
+ export function estimateTokens(charCount: number): number {
13
+ if (charCount <= 0) return 0;
14
+ return Math.ceil(charCount / CHARS_PER_TOKEN);
15
+ }
16
+
11
17
  /** Rough token count for a list of role/content messages. */
12
18
  export function estimateMessageTokens(messages: { content: string }[]): number {
13
19
  let chars = 0;
14
20
  for (const m of messages) chars += m.content.length + 8; // small per-message overhead
15
- return Math.ceil(chars / CHARS_PER_TOKEN);
21
+ return estimateTokens(chars);
16
22
  }
17
23
 
18
24
  /** Total character length of a context payload (string or list of strings). */
@@ -6,7 +6,6 @@
6
6
  * accumulated into the subcalls array for tree rendering.
7
7
  */
8
8
 
9
- import type { ProposedEdit } from "../sandbox/protocol.ts";
10
9
  import type { RlmSubcall } from "./rlm-details.ts";
11
10
 
12
11
  export interface ReplDetails {
@@ -23,6 +22,6 @@ export interface ReplDetails {
23
22
  readonly totals: { readonly costUsd: number; readonly tokens: number };
24
23
  /** Final answer submitted through answer["ready"] without echoing it to the model. */
25
24
  readonly finalAnswer?: string;
26
- /** File edits staged inside the REPL for native relay through apply_edits(). */
27
- readonly edits?: readonly ProposedEdit[];
25
+ /** Advisory diagnostics surfaced to the user, never a failure. */
26
+ readonly warnings?: readonly string[];
28
27
  }