@hicaru/pi-rlm 0.1.7 → 0.1.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.
Files changed (43) hide show
  1. package/README.md +41 -4
  2. package/package.json +2 -1
  3. package/src/bridge/library.ts +155 -0
  4. package/src/bridge/llm-query.ts +1 -0
  5. package/src/bridge/rlm-query.ts +56 -12
  6. package/src/config/defaults.ts +2 -0
  7. package/src/config/settings.ts +4 -0
  8. package/src/context/library-context.ts +266 -0
  9. package/src/context/repomix-context.ts +2 -48
  10. package/src/core/answer.ts +1 -10
  11. package/src/core/artifacts.ts +88 -0
  12. package/src/core/critique.ts +92 -0
  13. package/src/core/engine.ts +446 -53
  14. package/src/core/gates.ts +301 -0
  15. package/src/core/iteration.ts +7 -2
  16. package/src/core/pipeline.ts +196 -28
  17. package/src/core/types.ts +5 -3
  18. package/src/index.ts +3 -6
  19. package/src/mode/native-guards.ts +2 -2
  20. package/src/prompts/phases.ts +104 -0
  21. package/src/prompts/system.ts +59 -16
  22. package/src/prompts/user.ts +12 -4
  23. package/src/sandbox/protocol.ts +29 -11
  24. package/src/sandbox/sandbox.ts +77 -2
  25. package/src/sandbox/worker.py +215 -46
  26. package/src/state/index.ts +2 -1
  27. package/src/state/paths.ts +4 -2
  28. package/src/state/reads.ts +31 -2
  29. package/src/state/resume.ts +31 -6
  30. package/src/state/rows.ts +8 -2
  31. package/src/state/writes.ts +5 -3
  32. package/src/text/tokens.ts +7 -1
  33. package/src/tool/repl-details.ts +2 -3
  34. package/src/tool/repl-tool.ts +52 -57
  35. package/src/tool/rlm-aggregator.ts +7 -7
  36. package/src/tool/rlm-details.ts +6 -3
  37. package/src/tool/rlm-events.ts +14 -11
  38. package/src/tool/rlm-tool.ts +2 -8
  39. package/src/tool/subcall-store.ts +2 -0
  40. package/src/ui/config-panel.ts +8 -1
  41. package/src/registry/edit-registry.ts +0 -22
  42. package/src/text/edits.ts +0 -16
  43. package/src/tool/apply-edits-tool.ts +0 -288
@@ -1,288 +0,0 @@
1
- import { mkdir, readFile, writeFile } from "node:fs/promises";
2
- import { dirname, resolve } from "node:path";
3
- import { createEditToolDefinition, type AgentToolResult, type Theme, type ToolDefinition } from "@earendil-works/pi-coding-agent";
4
- import { Container, Text, type Component } from "@earendil-works/pi-tui";
5
- import { Type } from "typebox";
6
- import type { EditRegistry } from "../registry/edit-registry.ts";
7
- import { countOccurrences } from "../text/edits.ts";
8
- import { errorMessage, formatError } from "../util/errors.ts";
9
-
10
- export const ApplyEditsToolParams = Object.freeze(Type.Object({
11
- ids: Type.Array(Type.String({ description: "A staged edit ID returned by stage_edit()." }), {
12
- description: "Staged edit IDs to apply.",
13
- }),
14
- }));
15
-
16
- export interface ApplyEditsFailure {
17
- readonly id: string;
18
- readonly path: string;
19
- readonly error: string;
20
- }
21
-
22
- export interface ApplyEditsPatch {
23
- readonly oldText: string;
24
- readonly newText: string;
25
- }
26
-
27
- export interface ApplyEditsFileStat {
28
- readonly path: string;
29
- readonly status: "applied" | "failed";
30
- readonly added: number;
31
- readonly removed: number;
32
- readonly edits: readonly ApplyEditsPatch[];
33
- }
34
-
35
- export interface ApplyEditsDetails {
36
- readonly status: "done" | "partial" | "error";
37
- readonly appliedCount: number;
38
- readonly failedCount: number;
39
- readonly errors: readonly ApplyEditsFailure[];
40
- readonly fileStats: readonly ApplyEditsFileStat[];
41
- }
42
-
43
- export interface LineStats {
44
- readonly added: number;
45
- readonly removed: number;
46
- }
47
-
48
- export function countLines(text: string): number {
49
- return text.length === 0 ? 0 : text.split("\n").length;
50
- }
51
-
52
- export function diffStats(before: string, after: string): LineStats {
53
- const beforeLineCount = countLines(before);
54
- const afterLineCount = countLines(after);
55
- return Object.freeze({
56
- added: Math.max(0, afterLineCount - beforeLineCount),
57
- removed: Math.max(0, beforeLineCount - afterLineCount),
58
- });
59
- }
60
-
61
- function statusFor(appliedCount: number, failedCount: number): ApplyEditsDetails["status"] {
62
- if (failedCount === 0 && appliedCount > 0) return "done";
63
- return appliedCount > 0 ? "partial" : "error";
64
- }
65
-
66
- function aggregateLineStats(fileStats: readonly ApplyEditsFileStat[]): LineStats {
67
- let added = 0;
68
- let removed = 0;
69
- for (let i = 0; i < fileStats.length; i++) {
70
- const stat = fileStats[i];
71
- added += stat.added;
72
- removed += stat.removed;
73
- }
74
- return Object.freeze({ added, removed });
75
- }
76
-
77
- function appendPatch(existing: readonly ApplyEditsPatch[], patch: ApplyEditsPatch | undefined): readonly ApplyEditsPatch[] {
78
- if (patch === undefined) return existing;
79
- const patches = new Array<ApplyEditsPatch>(existing.length + 1);
80
- for (let i = 0; i < existing.length; i++) {
81
- patches[i] = existing[i];
82
- }
83
- patches[existing.length] = patch;
84
- return Object.freeze(patches);
85
- }
86
-
87
- function mergeFileStat(
88
- fileStatsByPath: Map<string, ApplyEditsFileStat>,
89
- path: string,
90
- status: ApplyEditsFileStat["status"],
91
- stats: LineStats,
92
- patch?: ApplyEditsPatch,
93
- ): void {
94
- const existing = fileStatsByPath.get(path);
95
- const nextStatus: ApplyEditsFileStat["status"] = existing?.status === "failed" || status === "failed" ? "failed" : "applied";
96
- fileStatsByPath.set(path, {
97
- path,
98
- status: nextStatus,
99
- added: (existing?.added ?? 0) + stats.added,
100
- removed: (existing?.removed ?? 0) + stats.removed,
101
- edits: appendPatch(existing?.edits ?? Object.freeze([]), patch),
102
- });
103
- }
104
-
105
- function renderLineStats(stats: LineStats, theme: Theme): string {
106
- return `${theme.fg("success", `+${stats.added}`)} ${theme.fg("error", `-${stats.removed}`)} lines`;
107
- }
108
-
109
- function formatEditCounts(details: ApplyEditsDetails): string {
110
- return details.failedCount > 0
111
- ? `${details.appliedCount} applied, ${details.failedCount} failed`
112
- : `${details.appliedCount} applied`;
113
- }
114
-
115
- function formatFileCount(fileCount: number): string {
116
- return `${fileCount} file${fileCount === 1 ? "" : "s"}`;
117
- }
118
-
119
- function summarizeFiles(details: ApplyEditsDetails): string {
120
- return `apply_edits: ${formatFileCount(details.fileStats.length)}, ${formatEditCounts(details)}`;
121
- }
122
-
123
- function summarize(details: ApplyEditsDetails): string {
124
- const stats = aggregateLineStats(details.fileStats);
125
- const head = `${summarizeFiles(details)} (+${stats.added} -${stats.removed} lines)`;
126
- if (details.errors.length === 0) return `${head}.`;
127
- const rows = new Array<string>(details.errors.length);
128
- for (let i = 0; i < details.errors.length; i++) {
129
- const error = details.errors[i];
130
- rows[i] = `${error.id}: ${error.error}`;
131
- }
132
- return `${head}.\n${rows.join("\n")}`;
133
- }
134
-
135
- function renderCollapsed(details: ApplyEditsDetails, theme: Theme): Text {
136
- const stats = aggregateLineStats(details.fileStats);
137
- const statusColor = details.status === "error" ? "error" : "success";
138
- return new Text(`${theme.fg(statusColor, summarizeFiles(details))} ${renderLineStats(stats, theme)}`, 0, 0);
139
- }
140
-
141
- function renderFileLine(fileStat: ApplyEditsFileStat, theme: Theme): Text {
142
- const glyph = fileStat.status === "applied" ? theme.fg("success", "✓") : theme.fg("error", "✗");
143
- const stats = renderLineStats(fileStat, theme);
144
- return new Text(`${glyph} ${theme.fg("dim", fileStat.path)} ${stats}`, 0, 0);
145
- }
146
-
147
- function limitPatchText(text: string): string {
148
- const maxChars = 2_000;
149
- return text.length > maxChars ? `${text.slice(0, maxChars)}…` : text;
150
- }
151
-
152
- function renderPatchLines(text: string, prefix: string, color: "error" | "success", theme: Theme): string {
153
- const limitedText = limitPatchText(text);
154
- const lines = limitedText.split("\n");
155
- const rendered = new Array<string>(lines.length);
156
- for (let i = 0; i < lines.length; i++) {
157
- rendered[i] = theme.fg(color, `${prefix}${lines[i]}`);
158
- }
159
- return rendered.join("\n");
160
- }
161
-
162
- function renderPatch(patch: ApplyEditsPatch, theme: Theme): Text {
163
- const oldText = patch.oldText.length === 0 ? "(new file)" : patch.oldText;
164
- const text = [
165
- theme.fg("error", "--- old"),
166
- renderPatchLines(oldText, "- ", "error", theme),
167
- theme.fg("success", "+++ new"),
168
- renderPatchLines(patch.newText, "+ ", "success", theme),
169
- ].join("\n");
170
- return new Text(text, 2, 0);
171
- }
172
-
173
- function renderExpanded(details: ApplyEditsDetails, theme: Theme): Container {
174
- const container = new Container();
175
- const header = summarizeFiles(details);
176
- const headerColor = details.status === "error" ? "error" : "success";
177
- container.addChild(new Text(theme.fg(headerColor, header), 0, 0));
178
- for (let i = 0; i < details.fileStats.length; i++) {
179
- const fileStat = details.fileStats[i];
180
- container.addChild(renderFileLine(fileStat, theme));
181
- for (let editIndex = 0; editIndex < fileStat.edits.length; editIndex++) {
182
- container.addChild(renderPatch(fileStat.edits[editIndex], theme));
183
- }
184
- }
185
- if (details.errors.length > 0) {
186
- const rows = new Array<string>(details.errors.length);
187
- for (let i = 0; i < details.errors.length; i++) {
188
- const error = details.errors[i];
189
- rows[i] = `${error.id}: ${error.error}`;
190
- }
191
- container.addChild(new Text(theme.fg("error", rows.join("\n")), 0, 0));
192
- }
193
- return container;
194
- }
195
-
196
- export function createApplyEditsTool(editRegistry: EditRegistry): ToolDefinition<typeof ApplyEditsToolParams, ApplyEditsDetails> {
197
- return {
198
- name: "apply_edits",
199
- label: "Apply Edits",
200
- description: "Apply staged REPL edits by ID without re-typing file paths or edit bodies.",
201
- parameters: ApplyEditsToolParams,
202
-
203
- async execute(toolCallId, params, signal, _onUpdate, ctx): Promise<AgentToolResult<ApplyEditsDetails>> {
204
- const errors = new Array<ApplyEditsFailure>(params.ids.length);
205
- const fileStatsByPath = new Map<string, ApplyEditsFileStat>();
206
- let appliedCount = 0;
207
- let failedCount = 0;
208
-
209
- const editTool = createEditToolDefinition(ctx.cwd);
210
- for (let i = 0; i < params.ids.length; i++) {
211
- const id = params.ids[i];
212
- const edit = editRegistry.get(id);
213
- if (edit === undefined) {
214
- errors[failedCount] = { id, path: id, error: formatError("unknown edit id") };
215
- mergeFileStat(fileStatsByPath, id, "failed", { added: 0, removed: 0 });
216
- failedCount++;
217
- continue;
218
- }
219
-
220
- try {
221
- const fullPath = resolve(ctx.cwd, edit.path);
222
- if (edit.oldText.length === 0) {
223
- await mkdir(dirname(fullPath), { recursive: true });
224
- await writeFile(fullPath, edit.newText, "utf8");
225
- mergeFileStat(fileStatsByPath, edit.path, "applied", diffStats("", edit.newText), { oldText: edit.oldText, newText: edit.newText });
226
- editRegistry.delete(id);
227
- appliedCount++;
228
- continue;
229
- }
230
-
231
- const content = await readFile(fullPath, "utf8");
232
- const occurrences = countOccurrences(content, edit.oldText);
233
- if (occurrences !== 1) {
234
- errors[failedCount] = { id, path: edit.path, error: formatError(`anchor occurs ${occurrences} times in ${edit.path}`) };
235
- mergeFileStat(fileStatsByPath, edit.path, "failed", { added: 0, removed: 0 }, { oldText: edit.oldText, newText: edit.newText });
236
- failedCount++;
237
- continue;
238
- }
239
-
240
- const after = content.replace(edit.oldText, edit.newText);
241
- await editTool.execute(
242
- toolCallId,
243
- { path: edit.path, edits: [{ oldText: edit.oldText, newText: edit.newText }] },
244
- signal,
245
- undefined,
246
- ctx,
247
- );
248
- mergeFileStat(fileStatsByPath, edit.path, "applied", diffStats(content, after), { oldText: edit.oldText, newText: edit.newText });
249
- editRegistry.delete(id);
250
- appliedCount++;
251
- } catch (error: unknown) {
252
- const path = edit.path;
253
- errors[failedCount] = { id, path, error: formatError(errorMessage(error)) };
254
- mergeFileStat(fileStatsByPath, path, "failed", { added: 0, removed: 0 }, { oldText: edit.oldText, newText: edit.newText });
255
- failedCount++;
256
- }
257
- }
258
-
259
- const fileStats = new Array<ApplyEditsFileStat>(fileStatsByPath.size);
260
- let fileStatIndex = 0;
261
- for (const stat of fileStatsByPath.values()) {
262
- fileStats[fileStatIndex] = stat;
263
- fileStatIndex++;
264
- }
265
-
266
- const details = Object.freeze({
267
- status: statusFor(appliedCount, failedCount),
268
- appliedCount,
269
- failedCount,
270
- errors: Object.freeze(errors.slice(0, failedCount)),
271
- fileStats: Object.freeze(fileStats),
272
- });
273
- return { content: [{ type: "text", text: summarize(details) }], details };
274
- },
275
-
276
- renderCall(args, theme) {
277
- const editCount = args.ids.length;
278
- const summary = `apply_edits: ${editCount} edit${editCount === 1 ? "" : "s"}`;
279
- return new Text(theme.fg("toolTitle", theme.bold(summary)), 0, 0);
280
- },
281
-
282
- renderResult(result, options, theme): Component {
283
- const details = result.details;
284
- if (details === undefined) return new Text("(no apply_edits details)", 0, 0);
285
- return options.expanded ? renderExpanded(details, theme) : renderCollapsed(details, theme);
286
- },
287
- };
288
- }