@hicaru/pi-rlm 0.1.0 → 0.1.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.
- package/README.md +58 -131
- package/README.ru.md +6 -8
- package/README.zh-CN.md +6 -12
- package/package.json +10 -12
- package/src/bridge/pi-interactive.ts +3 -48
- package/src/commands/rlm-config.ts +10 -3
- package/src/commands/rlm.ts +0 -15
- package/src/config/settings.ts +1 -24
- package/src/core/answer.ts +1 -17
- package/src/core/engine.ts +9 -14
- package/src/core/types.ts +1 -13
- package/src/index.ts +52 -35
- package/src/prompts/system.ts +19 -37
- package/src/sandbox/protocol.ts +0 -6
- package/src/sandbox/sandbox-manager.ts +7 -5
- package/src/sandbox/sandbox.ts +4 -1
- package/src/sandbox/worker.py +12 -11
- package/src/tool/repl-details.ts +3 -0
- package/src/tool/repl-tool.ts +34 -5
- package/src/tool/rlm-events.ts +2 -41
- package/src/tool/rlm-tool.ts +0 -13
- package/src/ui/model-picker.ts +12 -4
- package/src/patch/apply.ts +0 -148
- package/src/patch/index.ts +0 -37
- package/src/state/events.ts +0 -22
- package/src/telemetry/dispatcher.ts +0 -116
- package/src/telemetry/index.ts +0 -14
- package/src/telemetry/mlflow-config.ts +0 -15
- package/src/telemetry/mlflow-sink.ts +0 -136
- package/src/telemetry/mlflow.ts +0 -99
- package/src/telemetry/sink.ts +0 -8
- package/src/tool/apply-diff-tool.ts +0 -125
package/src/tool/repl-tool.ts
CHANGED
|
@@ -24,10 +24,10 @@ import { previewText } from "../text/preview.ts";
|
|
|
24
24
|
import { mapPool } from "../util/concurrency.ts";
|
|
25
25
|
import { LimitGuard } from "../core/limits.ts";
|
|
26
26
|
import { checkResourceLimits } from "../core/resource-limits.ts";
|
|
27
|
-
import type { RlmConfig, Sampling } from "../core/types.ts";
|
|
27
|
+
import type { InteractiveDeps, RlmConfig, Sampling } from "../core/types.ts";
|
|
28
28
|
import { SandboxManager } from "../sandbox/sandbox-manager.ts";
|
|
29
29
|
import type { SubLlmHandlers } from "../sandbox/sandbox.ts";
|
|
30
|
-
import type { ReplResult } from "../sandbox/protocol.ts";
|
|
30
|
+
import type { ProposedEdit, ReplResult } from "../sandbox/protocol.ts";
|
|
31
31
|
import { RlmEmitter } from "./rlm-events.ts";
|
|
32
32
|
import { SubcallStore } from "./subcall-store.ts";
|
|
33
33
|
import type { ReplDetails } from "./repl-details.ts";
|
|
@@ -47,6 +47,10 @@ export const ReplToolParams = Object.freeze(Type.Object({
|
|
|
47
47
|
code: Type.String({ description: "Python code to execute in the persistent REPL sandbox" }),
|
|
48
48
|
}));
|
|
49
49
|
|
|
50
|
+
export function surfaceReplEdits(edits: readonly ProposedEdit[], raised: boolean): readonly ProposedEdit[] | undefined {
|
|
51
|
+
return edits.length > 0 && !raised ? edits : undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
50
54
|
// ── Mutable bridge state (handler indirection) ──
|
|
51
55
|
|
|
52
56
|
/**
|
|
@@ -60,12 +64,14 @@ class NativeBridgeState {
|
|
|
60
64
|
currentParentId: string | undefined;
|
|
61
65
|
currentDepth = 0;
|
|
62
66
|
currentLimits: LimitGuard | null = null;
|
|
67
|
+
currentInteractive: InteractiveDeps | null = null;
|
|
63
68
|
|
|
64
|
-
swap(inv: { emitter: RlmEmitter; parentId?: string; depth: number; limits: LimitGuard }): void {
|
|
69
|
+
swap(inv: { emitter: RlmEmitter; parentId?: string; depth: number; limits: LimitGuard; interactive: InteractiveDeps }): void {
|
|
65
70
|
this.currentEmitter = inv.emitter;
|
|
66
71
|
this.currentParentId = inv.parentId;
|
|
67
72
|
this.currentDepth = inv.depth;
|
|
68
73
|
this.currentLimits = inv.limits;
|
|
74
|
+
this.currentInteractive = inv.interactive;
|
|
69
75
|
}
|
|
70
76
|
|
|
71
77
|
buildLlmHandlers(deps: {
|
|
@@ -220,6 +226,8 @@ class NativeBridgeState {
|
|
|
220
226
|
maxTokens: deps.config.maxTokens,
|
|
221
227
|
maxErrors: deps.config.maxErrors,
|
|
222
228
|
},
|
|
229
|
+
onTodo: state.currentInteractive?.onTodo,
|
|
230
|
+
onAskUserQuestion: state.currentInteractive?.onAskUserQuestion,
|
|
223
231
|
});
|
|
224
232
|
|
|
225
233
|
try {
|
|
@@ -273,6 +281,7 @@ export interface ReplToolDeps {
|
|
|
273
281
|
readonly config: RlmConfig;
|
|
274
282
|
readonly signal?: AbortSignal;
|
|
275
283
|
readonly onUsage?: (usage: Usage, role: "sub") => void;
|
|
284
|
+
readonly ensureContext?: () => Promise<void>;
|
|
276
285
|
}
|
|
277
286
|
|
|
278
287
|
export function createReplTool(deps: ReplToolDeps): ToolDefinition<typeof ReplToolParams, ReplDetails> {
|
|
@@ -374,6 +383,7 @@ export function createReplTool(deps: ReplToolDeps): ToolDefinition<typeof ReplTo
|
|
|
374
383
|
parentId: undefined,
|
|
375
384
|
});
|
|
376
385
|
|
|
386
|
+
await deps.ensureContext?.();
|
|
377
387
|
await sandboxManager.getOrCreate({
|
|
378
388
|
...llmHandlers,
|
|
379
389
|
...rlmHandlers,
|
|
@@ -395,7 +405,7 @@ export function createReplTool(deps: ReplToolDeps): ToolDefinition<typeof ReplTo
|
|
|
395
405
|
// Wire per-invocation mutable state only after the serialized exec slot
|
|
396
406
|
// is active. Swapping earlier would let queued repl() calls overwrite
|
|
397
407
|
// emitter/limits for the currently running REPL execution.
|
|
398
|
-
bridgeState.swap({ emitter, parentId: undefined, depth: 0, limits });
|
|
408
|
+
bridgeState.swap({ emitter, parentId: undefined, depth: 0, limits, interactive });
|
|
399
409
|
});
|
|
400
410
|
const elapsed = Date.now() - start;
|
|
401
411
|
capturedStdout = result.stdout;
|
|
@@ -411,6 +421,12 @@ export function createReplTool(deps: ReplToolDeps): ToolDefinition<typeof ReplTo
|
|
|
411
421
|
|
|
412
422
|
if (queuedId) emitter.emitSubcallUpdated({ id: queuedId, status: "done" });
|
|
413
423
|
|
|
424
|
+
const baseText = result.stdout || result.answerContent || "(no output)";
|
|
425
|
+
const surfacedEdits = surfaceReplEdits(result.edits, result.raised);
|
|
426
|
+
const editsBlock = surfacedEdits
|
|
427
|
+
? `\n\nSTAGED_EDITS:\n${JSON.stringify(surfacedEdits)}`
|
|
428
|
+
: "";
|
|
429
|
+
|
|
414
430
|
const details: ReplDetails = {
|
|
415
431
|
status: "done",
|
|
416
432
|
output: result.stdout,
|
|
@@ -418,10 +434,11 @@ export function createReplTool(deps: ReplToolDeps): ToolDefinition<typeof ReplTo
|
|
|
418
434
|
executionTimeMs: elapsed,
|
|
419
435
|
subcalls: store.getSubcalls(),
|
|
420
436
|
totals: store.getTotals(),
|
|
437
|
+
edits: surfacedEdits,
|
|
421
438
|
};
|
|
422
439
|
// Final progressive update
|
|
423
440
|
onUpdate?.({ content: [{ type: "text", text: result.stdout.slice(0, 500) || "(no output)" }], details });
|
|
424
|
-
return { content: [{ type: "text", text:
|
|
441
|
+
return { content: [{ type: "text", text: baseText + editsBlock }], details };
|
|
425
442
|
} catch (e) {
|
|
426
443
|
progressStatus = "error";
|
|
427
444
|
const msg = errorMessage(e);
|
|
@@ -477,6 +494,9 @@ function renderReplCollapsed(details: ReplDetails, theme: Theme): Text {
|
|
|
477
494
|
parts.push(formatCost(details.totals.costUsd));
|
|
478
495
|
if (details.totals.tokens > 0) parts.push(`${formatTokens(details.totals.tokens)} tok`);
|
|
479
496
|
if (details.executionTimeMs > 0) parts.push(`${details.executionTimeMs}ms`);
|
|
497
|
+
if (details.edits && details.edits.length > 0) {
|
|
498
|
+
parts.push(theme.fg("success", `${details.edits.length} staged`));
|
|
499
|
+
}
|
|
480
500
|
const stats = parts.length > 0 ? ` ${theme.fg("dim", parts.join(" · "))}` : "";
|
|
481
501
|
|
|
482
502
|
const header = `${glyph} ${theme.fg("toolTitle", theme.bold("REPL"))}${stats}`;
|
|
@@ -511,6 +531,15 @@ function renderReplExpanded(details: ReplDetails, theme: Theme): Container {
|
|
|
511
531
|
container.addChild(new Text(out, 0, 0));
|
|
512
532
|
}
|
|
513
533
|
|
|
534
|
+
if (details.edits && details.edits.length > 0) {
|
|
535
|
+
const editFiles = new Set<string>();
|
|
536
|
+
for (const edit of details.edits) editFiles.add(edit.path);
|
|
537
|
+
container.addChild(new Spacer(1));
|
|
538
|
+
container.addChild(new Text(theme.fg("success",
|
|
539
|
+
`${details.edits.length} edit${details.edits.length > 1 ? "s" : ""} staged across ${editFiles.size} file${editFiles.size > 1 ? "s" : ""}`,
|
|
540
|
+
), 0, 0));
|
|
541
|
+
}
|
|
542
|
+
|
|
514
543
|
// Stderr
|
|
515
544
|
if (details.stderr) {
|
|
516
545
|
container.addChild(new Spacer(1));
|
package/src/tool/rlm-events.ts
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* RlmEmitter — typed EventEmitter wrapper for RLM lifecycle events.
|
|
3
3
|
*
|
|
4
4
|
* Replaces RlmToolBridge mutation methods. The engine and bridges emit typed
|
|
5
|
-
* events instead of calling bridge.addSubcall/updateSubcall/etc.
|
|
6
|
-
*
|
|
5
|
+
* events instead of calling bridge.addSubcall/updateSubcall/etc. The
|
|
6
|
+
* RlmEventAggregator subscribes to build derived state.
|
|
7
7
|
*
|
|
8
8
|
* Node.js EventEmitter is synchronous — listeners run in registration order
|
|
9
9
|
* during emit. No backpressure needed: engine events are sequential, one at
|
|
@@ -11,7 +11,6 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { EventEmitter } from "node:events";
|
|
14
|
-
import type { TelemetrySink } from "../telemetry/sink.ts";
|
|
15
14
|
import type { SubcallKind, SubcallStatus, RlmRunStatus } from "./rlm-details.ts";
|
|
16
15
|
import type { ProposedEdit } from "../sandbox/protocol.ts";
|
|
17
16
|
|
|
@@ -76,7 +75,6 @@ export interface RootPromptEvent {
|
|
|
76
75
|
*
|
|
77
76
|
* Auto-generates monotonic subcall IDs (`s1`, `s2`, …) via `emitSubcallCreated()`.
|
|
78
77
|
* Provides typed `on*` methods that return unsubscribe functions.
|
|
79
|
-
* `attachSink()` wires all events to a TelemetrySink and returns a detach function.
|
|
80
78
|
*/
|
|
81
79
|
export class RlmEmitter {
|
|
82
80
|
private readonly ee = new EventEmitter();
|
|
@@ -169,43 +167,6 @@ export class RlmEmitter {
|
|
|
169
167
|
return () => { this.ee.off("root-prompt", handler); };
|
|
170
168
|
}
|
|
171
169
|
|
|
172
|
-
// ── Sink integration ──
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Wire all lifecycle events to a TelemetrySink.
|
|
176
|
-
* Returns a detach function that unsubscribes all sink listeners.
|
|
177
|
-
* The caller is responsible for calling `sink.shutdown()` after detaching.
|
|
178
|
-
*/
|
|
179
|
-
attachSink(sink: TelemetrySink): () => void {
|
|
180
|
-
const unsubs: (() => void)[] = [];
|
|
181
|
-
|
|
182
|
-
unsubs.push(this.onSubcallCreated((event) => {
|
|
183
|
-
sink.start(event.id, {
|
|
184
|
-
kind: event.kind,
|
|
185
|
-
depth: event.depth,
|
|
186
|
-
parentId: event.parentId,
|
|
187
|
-
model: event.model,
|
|
188
|
-
label: event.label,
|
|
189
|
-
detail: event.detail,
|
|
190
|
-
args: event.args,
|
|
191
|
-
});
|
|
192
|
-
}));
|
|
193
|
-
|
|
194
|
-
unsubs.push(this.onSubcallUpdated((event) => {
|
|
195
|
-
if (event.costUsd !== undefined || event.tokens !== undefined) {
|
|
196
|
-
sink.usage(event.id, event.costUsd ?? 0, event.tokens ?? 0);
|
|
197
|
-
}
|
|
198
|
-
if (event.status !== undefined && event.status !== "running") {
|
|
199
|
-
sink.end(event.id, {
|
|
200
|
-
error: event.status === "error" ? (event.detail ?? "error") : undefined,
|
|
201
|
-
resultPreview: event.resultPreview,
|
|
202
|
-
});
|
|
203
|
-
}
|
|
204
|
-
}));
|
|
205
|
-
|
|
206
|
-
return () => { unsubs.forEach((fn) => fn()); };
|
|
207
|
-
}
|
|
208
|
-
|
|
209
170
|
// ── Lifecycle ──
|
|
210
171
|
|
|
211
172
|
/** Remove all listeners. Call after the run completes to prevent leaks. */
|
package/src/tool/rlm-tool.ts
CHANGED
|
@@ -11,7 +11,6 @@ import { Container, Markdown, Spacer, Text, type Component } from "@earendil-wor
|
|
|
11
11
|
import { Type } from "typebox";
|
|
12
12
|
import { createPiInteractiveDeps } from "../bridge/pi-interactive.ts";
|
|
13
13
|
import type { RlmController, StartInput } from "../mode/rlm-mode.ts";
|
|
14
|
-
import { createTelemetrySink } from "../telemetry/index.ts";
|
|
15
14
|
import { formatCost, formatTokens, spinnerFrame } from "../ui/theme.ts";
|
|
16
15
|
import { errorMessage } from "../util/errors.ts";
|
|
17
16
|
import { type RlmDetails } from "./rlm-details.ts";
|
|
@@ -23,8 +22,6 @@ import {
|
|
|
23
22
|
renderExpandedSubcallTree,
|
|
24
23
|
} from "./subcall-render.ts";
|
|
25
24
|
import { createProgressNotifier, validateToolParams } from "./tool-utils.ts";
|
|
26
|
-
import { applyEdits } from "../patch/index.ts";
|
|
27
|
-
import { tryExtractDiff } from "../core/answer.ts";
|
|
28
25
|
|
|
29
26
|
// ── Parameter schema ──
|
|
30
27
|
|
|
@@ -63,11 +60,8 @@ export function createRlmTool(controller: RlmController): ToolDefinition<typeof
|
|
|
63
60
|
if (!validation.ok) return validation.error;
|
|
64
61
|
const params = validation.value;
|
|
65
62
|
|
|
66
|
-
const sink = await createTelemetrySink(controller.config.telemetry);
|
|
67
63
|
const emitter = new RlmEmitter();
|
|
68
64
|
const aggregator = new RlmEventAggregator(emitter, onUpdate ?? (() => {}));
|
|
69
|
-
let detachSink: (() => void) | undefined;
|
|
70
|
-
if (sink) detachSink = emitter.attachSink(sink);
|
|
71
65
|
emitter.emitRootPrompt(params.prompt);
|
|
72
66
|
|
|
73
67
|
// Wire abort signal to controller
|
|
@@ -98,10 +92,6 @@ export function createRlmTool(controller: RlmController): ToolDefinition<typeof
|
|
|
98
92
|
const result = await done;
|
|
99
93
|
|
|
100
94
|
emitter.emitAnswer(result.answer);
|
|
101
|
-
const proposedEdits = result.edits ?? [];
|
|
102
|
-
const proposedDiffs = result.diffs?.length ? result.diffs : tryExtractDiff(result.answer);
|
|
103
|
-
if (proposedEdits.length > 0) emitter.emitEdits(proposedEdits);
|
|
104
|
-
await applyEdits(proposedEdits, proposedDiffs, ctx);
|
|
105
95
|
|
|
106
96
|
return {
|
|
107
97
|
content: [{ type: "text", text: result.answer }],
|
|
@@ -116,11 +106,8 @@ export function createRlmTool(controller: RlmController): ToolDefinition<typeof
|
|
|
116
106
|
};
|
|
117
107
|
} finally {
|
|
118
108
|
progress.stop();
|
|
119
|
-
detachSink?.();
|
|
120
109
|
aggregator.dispose();
|
|
121
110
|
emitter.shutdown();
|
|
122
|
-
try { await sink?.shutdown(); }
|
|
123
|
-
catch (err) { console.warn(`[rlm] telemetry shutdown failed: ${errorMessage(err)}`); }
|
|
124
111
|
}
|
|
125
112
|
},
|
|
126
113
|
|
package/src/ui/model-picker.ts
CHANGED
|
@@ -14,12 +14,19 @@ export interface ModelSelection {
|
|
|
14
14
|
const LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"] as const;
|
|
15
15
|
type SelectableThinkingLevel = (typeof LEVELS)[number];
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
const CHEAPEST_VALUE = "__rlm_cheapest__";
|
|
18
|
+
|
|
19
|
+
function items(models: Model<Api>[], includeCheapest = false): SelectItem[] {
|
|
20
|
+
const modelItems = models.map((m) => ({
|
|
19
21
|
value: `${m.provider}/${m.id}`,
|
|
20
22
|
label: `${m.provider}/${m.id}`,
|
|
21
23
|
description: `in ${formatCost(m.cost.input)}/Mtok · out ${formatCost(m.cost.output)}/Mtok${m.reasoning ? " · reasoning" : ""}`,
|
|
22
24
|
}));
|
|
25
|
+
if (!includeCheapest) return modelItems;
|
|
26
|
+
return [
|
|
27
|
+
{ value: CHEAPEST_VALUE, label: "⟳ cheapest (auto)", description: "Always use the cheapest available model" },
|
|
28
|
+
...modelItems,
|
|
29
|
+
];
|
|
23
30
|
}
|
|
24
31
|
|
|
25
32
|
function supportedThinkingLevels(model: Model<Api>): SelectableThinkingLevel[] {
|
|
@@ -75,7 +82,7 @@ export async function selectModel(
|
|
|
75
82
|
models: Model<Api>[],
|
|
76
83
|
current?: Model<Api>,
|
|
77
84
|
currentThinking?: ThinkingLevel,
|
|
78
|
-
): Promise<ModelSelection | undefined> {
|
|
85
|
+
): Promise<ModelSelection | null | undefined> {
|
|
79
86
|
if (models.length === 0) {
|
|
80
87
|
ctx.ui.notify("RLM: no models with configured auth", "warning");
|
|
81
88
|
return undefined;
|
|
@@ -96,7 +103,7 @@ export async function selectModel(
|
|
|
96
103
|
render: (w) => [truncateToWidth(theme.fg("dim", `Filter: ${query || "type to filter…"}`), w)],
|
|
97
104
|
invalidate: () => {},
|
|
98
105
|
};
|
|
99
|
-
const list = new SelectList(items(models), Math.min(models.length,
|
|
106
|
+
const list = new SelectList(items(models, true), Math.min(models.length + 1, 13), {
|
|
100
107
|
selectedPrefix: (t) => theme.fg("accent", t),
|
|
101
108
|
selectedText: (t) => theme.fg("accent", t),
|
|
102
109
|
description: (t) => theme.fg("muted", t),
|
|
@@ -133,6 +140,7 @@ export async function selectModel(
|
|
|
133
140
|
};
|
|
134
141
|
});
|
|
135
142
|
|
|
143
|
+
if (chosen === CHEAPEST_VALUE) return null;
|
|
136
144
|
const model = chosen ? models.find((m) => `${m.provider}/${m.id}` === chosen) : undefined;
|
|
137
145
|
if (!model) return undefined;
|
|
138
146
|
return { model, thinkingLevel: await selectThinkingLevel(ctx, model, currentThinking) };
|
package/src/patch/apply.ts
DELETED
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Applies RLM-proposed edits to disk.
|
|
3
|
-
*
|
|
4
|
-
* Two edit kinds from the sandbox protocol:
|
|
5
|
-
* ProposedEdit — oldText / newText anchor replacement
|
|
6
|
-
* ProposedDiffEdit — unified diff string (applied via `diff.applyPatch`)
|
|
7
|
-
*
|
|
8
|
-
* Returns a Result. Caller decides whether to show errors in UI.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { readFile, writeFile } from "node:fs/promises";
|
|
12
|
-
import { resolve } from "node:path";
|
|
13
|
-
import * as Diff from "diff";
|
|
14
|
-
import type { ProposedDiffEdit, ProposedEdit } from "../sandbox/protocol.ts";
|
|
15
|
-
import { err, ok, type Result } from "../util/errors.ts";
|
|
16
|
-
|
|
17
|
-
// ── Shared private helpers ─────────────────────────────────────────────────
|
|
18
|
-
|
|
19
|
-
/** Normalise to LF so string-replace is CRLF-safe. */
|
|
20
|
-
function toLF(s: string): string {
|
|
21
|
-
return s.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/** Restore original line endings after replacement. */
|
|
25
|
-
function restoreEndings(s: string, crlf: boolean): string {
|
|
26
|
-
return crlf ? s.replace(/\n/g, "\r\n") : s;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function hasCRLF(s: string): boolean {
|
|
30
|
-
return s.includes("\r\n");
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// ── ApplyResult ─────────────────────────────────────────────────────────────
|
|
34
|
-
|
|
35
|
-
export interface ApplySuccess {
|
|
36
|
-
readonly applied: number;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export interface ApplyFailure {
|
|
40
|
-
readonly failures: ReadonlyArray<{ readonly path: string; readonly reason: string }>;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export type ApplyResult = Result<ApplySuccess, ApplyFailure>;
|
|
44
|
-
|
|
45
|
-
// ── Generic accumulator (shared by anchor + diff apply) ─────────────────────
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Runs `applyOne` for every item, tallying successes and collecting failures.
|
|
49
|
-
* The only thing that differs between anchor and diff apply is the per-item
|
|
50
|
-
* helper and the failure-path key — both passed in, so the loop is written once.
|
|
51
|
-
*/
|
|
52
|
-
async function applyAll<T>(
|
|
53
|
-
items: readonly T[],
|
|
54
|
-
applyOne: (item: T, cwd: string) => Promise<Result<void, string>>,
|
|
55
|
-
getKey: (item: T) => string,
|
|
56
|
-
cwd: string,
|
|
57
|
-
): Promise<ApplyResult> {
|
|
58
|
-
const failures: Array<{ readonly path: string; readonly reason: string }> = [];
|
|
59
|
-
let applied = 0;
|
|
60
|
-
for (const item of items) {
|
|
61
|
-
const r = await applyOne(item, cwd);
|
|
62
|
-
if (r.ok) {
|
|
63
|
-
applied++;
|
|
64
|
-
} else {
|
|
65
|
-
failures.push({ path: getKey(item), reason: r.error });
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
return failures.length === 0 ? ok({ applied }) : err({ failures });
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// ── ProposedEdit (oldText / newText) ────────────────────────────────────────
|
|
72
|
-
|
|
73
|
-
async function applySingleAnchor(
|
|
74
|
-
edit: ProposedEdit,
|
|
75
|
-
cwd: string,
|
|
76
|
-
): Promise<Result<void, string>> {
|
|
77
|
-
const abs = resolve(cwd, edit.path);
|
|
78
|
-
let raw: string;
|
|
79
|
-
try {
|
|
80
|
-
raw = await readFile(abs, "utf8");
|
|
81
|
-
} catch (e) {
|
|
82
|
-
return err(`read error: ${e instanceof Error ? e.message : String(e)}`);
|
|
83
|
-
}
|
|
84
|
-
const crlf = hasCRLF(raw);
|
|
85
|
-
const content = toLF(raw);
|
|
86
|
-
const needle = toLF(edit.oldText);
|
|
87
|
-
if (!content.includes(needle)) {
|
|
88
|
-
return err(`oldText not found in ${edit.path}`);
|
|
89
|
-
}
|
|
90
|
-
const replaced = content.replace(needle, toLF(edit.newText));
|
|
91
|
-
try {
|
|
92
|
-
await writeFile(abs, restoreEndings(replaced, crlf), "utf8");
|
|
93
|
-
return ok(undefined);
|
|
94
|
-
} catch (e) {
|
|
95
|
-
return err(`write error: ${e instanceof Error ? e.message : String(e)}`);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export function applyAnchorEdits(
|
|
100
|
-
edits: readonly ProposedEdit[],
|
|
101
|
-
cwd: string,
|
|
102
|
-
): Promise<ApplyResult> {
|
|
103
|
-
return applyAll(edits, applySingleAnchor, (e) => e.path, cwd);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// ── ProposedDiffEdit (unified diff string) ──────────────────────────────────
|
|
107
|
-
|
|
108
|
-
async function applySingleDiff(
|
|
109
|
-
diffEdit: ProposedDiffEdit,
|
|
110
|
-
cwd: string,
|
|
111
|
-
): Promise<Result<void, string>> {
|
|
112
|
-
// Extract file path from diff header: "--- a/path" or "--- path"
|
|
113
|
-
const match = /^--- (?:a\/)?(.+)$/m.exec(diffEdit.diff);
|
|
114
|
-
const relPath = match?.[1]?.trim();
|
|
115
|
-
if (relPath === undefined) {
|
|
116
|
-
return err("diff has no '---' header; cannot determine target file");
|
|
117
|
-
}
|
|
118
|
-
const abs = resolve(cwd, relPath);
|
|
119
|
-
let raw: string;
|
|
120
|
-
try {
|
|
121
|
-
raw = await readFile(abs, "utf8");
|
|
122
|
-
} catch (e) {
|
|
123
|
-
return err(`read error: ${e instanceof Error ? e.message : String(e)}`);
|
|
124
|
-
}
|
|
125
|
-
const crlf = hasCRLF(raw);
|
|
126
|
-
let patched: string | false;
|
|
127
|
-
try {
|
|
128
|
-
patched = Diff.applyPatch(toLF(raw), diffEdit.diff);
|
|
129
|
-
} catch (e) {
|
|
130
|
-
return err(`invalid diff — ${e instanceof Error ? e.message : String(e)}`);
|
|
131
|
-
}
|
|
132
|
-
if (patched === false) {
|
|
133
|
-
return err(`patch does not apply cleanly to ${relPath}`);
|
|
134
|
-
}
|
|
135
|
-
try {
|
|
136
|
-
await writeFile(abs, restoreEndings(patched, crlf), "utf8");
|
|
137
|
-
return ok(undefined);
|
|
138
|
-
} catch (e) {
|
|
139
|
-
return err(`write error: ${e instanceof Error ? e.message : String(e)}`);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
export function applyDiffEdits(
|
|
144
|
-
diffs: readonly ProposedDiffEdit[],
|
|
145
|
-
cwd: string,
|
|
146
|
-
): Promise<ApplyResult> {
|
|
147
|
-
return applyAll(diffs, applySingleDiff, (d) => d.diff.slice(0, 40), cwd);
|
|
148
|
-
}
|
package/src/patch/index.ts
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* applyEdits — THE single call site for applying proposed edits/diffs.
|
|
3
|
-
*
|
|
4
|
-
* Both rlm-tool.ts and rlm.ts must call this and nothing else.
|
|
5
|
-
* No duplication allowed.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
9
|
-
import type { ProposedDiffEdit, ProposedEdit } from "../sandbox/protocol.ts";
|
|
10
|
-
import { applyAnchorEdits, applyDiffEdits, type ApplyResult } from "./apply.ts";
|
|
11
|
-
|
|
12
|
-
/** Surface an apply outcome through a single notify — used for both edit kinds. */
|
|
13
|
-
function notifyApplyResult(r: ApplyResult, label: string, ctx: ExtensionContext): void {
|
|
14
|
-
if (!r.ok) {
|
|
15
|
-
const lines = r.error.failures.map((f) => `• ${f.path}: ${f.reason}`);
|
|
16
|
-
ctx.ui.notify(`Some ${label}s failed:\n${lines.join("\n")}`, "error");
|
|
17
|
-
} else {
|
|
18
|
-
ctx.ui.notify(`Applied ${r.value.applied} ${label}${r.value.applied !== 1 ? "s" : ""}.`, "info");
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export async function applyEdits(
|
|
23
|
-
edits: readonly ProposedEdit[],
|
|
24
|
-
diffs: readonly ProposedDiffEdit[],
|
|
25
|
-
ctx: ExtensionContext,
|
|
26
|
-
): Promise<void> {
|
|
27
|
-
const hasEdits = edits.length > 0 || diffs.length > 0;
|
|
28
|
-
if (!hasEdits) return;
|
|
29
|
-
const cwd = ctx.cwd ?? process.cwd();
|
|
30
|
-
|
|
31
|
-
if (edits.length > 0) {
|
|
32
|
-
notifyApplyResult(await applyAnchorEdits(edits, cwd), "edit", ctx);
|
|
33
|
-
}
|
|
34
|
-
if (diffs.length > 0) {
|
|
35
|
-
notifyApplyResult(await applyDiffEdits(diffs, cwd), "diff", ctx);
|
|
36
|
-
}
|
|
37
|
-
}
|
package/src/state/events.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SubcallStart — parameter object carried forward for telemetry compatibility.
|
|
3
|
-
*
|
|
4
|
-
* The SubcallObserver interface, treeObserver(), observerWith(), and NOOP_OBSERVER
|
|
5
|
-
* have been removed. The engine and bridges now call RlmToolBridge directly.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import type { SubcallKind } from "../tool/rlm-details.ts";
|
|
9
|
-
|
|
10
|
-
export interface SubcallStart {
|
|
11
|
-
readonly kind: SubcallKind;
|
|
12
|
-
readonly depth: number;
|
|
13
|
-
readonly parentId?: string;
|
|
14
|
-
readonly model?: string;
|
|
15
|
-
readonly label: string;
|
|
16
|
-
readonly detail?: string;
|
|
17
|
-
readonly args?: string;
|
|
18
|
-
/** Run ID for the root node — lets MLflow correlate a resumed trace with the original. */
|
|
19
|
-
readonly runId?: string;
|
|
20
|
-
/** True when this is a resumed root node (not a fresh start). */
|
|
21
|
-
readonly resume?: boolean;
|
|
22
|
-
}
|
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
export interface DispatcherSink<E> {
|
|
2
|
-
readonly name: string;
|
|
3
|
-
handle(event: E): Promise<void>;
|
|
4
|
-
flush(): Promise<void>;
|
|
5
|
-
shutdown(): Promise<void>;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export interface DispatcherOptions {
|
|
9
|
-
readonly maxQueueSize: number;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/** Bounded FIFO async dispatcher with drop-oldest backpressure. */
|
|
13
|
-
export class Dispatcher<E> {
|
|
14
|
-
private readonly sinks: DispatcherSink<E>[] = [];
|
|
15
|
-
private queue: E[] = [];
|
|
16
|
-
private flushing = false;
|
|
17
|
-
private inFlight: Promise<void> = Promise.resolve();
|
|
18
|
-
private shuttingDown = false;
|
|
19
|
-
private backpressureActive = false;
|
|
20
|
-
private readonly failed = new Set<string>();
|
|
21
|
-
|
|
22
|
-
constructor(private readonly options: DispatcherOptions) {}
|
|
23
|
-
|
|
24
|
-
registerSink(sink: DispatcherSink<E>): () => void {
|
|
25
|
-
this.sinks.push(sink);
|
|
26
|
-
return () => {
|
|
27
|
-
const idx = this.sinks.indexOf(sink);
|
|
28
|
-
if (idx >= 0) this.sinks.splice(idx, 1);
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
dispatch(event: E): void {
|
|
33
|
-
if (this.shuttingDown || this.sinks.length === 0) return;
|
|
34
|
-
const cap = this.options.maxQueueSize;
|
|
35
|
-
if (this.queue.length >= cap) {
|
|
36
|
-
this.queue.shift();
|
|
37
|
-
if (!this.backpressureActive) {
|
|
38
|
-
this.backpressureActive = true;
|
|
39
|
-
console.warn(`[rlm-telemetry] backpressure: queue saturated at ${cap}; dropping oldest events`);
|
|
40
|
-
}
|
|
41
|
-
} else if (this.backpressureActive && this.queue.length < cap - 1) {
|
|
42
|
-
this.backpressureActive = false;
|
|
43
|
-
console.warn("[rlm-telemetry] backpressure recovered: queue back under capacity");
|
|
44
|
-
}
|
|
45
|
-
this.queue.push(event);
|
|
46
|
-
this.scheduleFlush();
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
async shutdown(): Promise<void> {
|
|
50
|
-
this.shuttingDown = true;
|
|
51
|
-
const remaining = this.queue;
|
|
52
|
-
this.queue = [];
|
|
53
|
-
this.flushing = false;
|
|
54
|
-
|
|
55
|
-
await this.inFlight;
|
|
56
|
-
|
|
57
|
-
for (const event of remaining) await this.broadcast(event);
|
|
58
|
-
const sinks = [...this.sinks];
|
|
59
|
-
await Promise.allSettled(sinks.map((sink) => sink.flush()));
|
|
60
|
-
await Promise.allSettled(sinks.map((sink) => sink.shutdown()));
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
reset(): void {
|
|
64
|
-
this.sinks.length = 0;
|
|
65
|
-
this.queue = [];
|
|
66
|
-
this.flushing = false;
|
|
67
|
-
this.inFlight = Promise.resolve();
|
|
68
|
-
this.shuttingDown = false;
|
|
69
|
-
this.backpressureActive = false;
|
|
70
|
-
this.failed.clear();
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
private scheduleFlush(): void {
|
|
74
|
-
if (this.flushing) return;
|
|
75
|
-
this.flushing = true;
|
|
76
|
-
this.drain();
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
private drain(): void {
|
|
80
|
-
if (this.queue.length === 0) {
|
|
81
|
-
this.flushing = false;
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
const batch = this.queue;
|
|
85
|
-
this.queue = [];
|
|
86
|
-
|
|
87
|
-
this.inFlight = this.inFlight.then(async () => {
|
|
88
|
-
for (const event of batch) await this.broadcast(event);
|
|
89
|
-
if (this.queue.length > 0) {
|
|
90
|
-
const handle = setImmediate(() => this.drain());
|
|
91
|
-
handle.unref?.();
|
|
92
|
-
} else {
|
|
93
|
-
this.flushing = false;
|
|
94
|
-
}
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
private async broadcast(event: E): Promise<void> {
|
|
99
|
-
const sinks = [...this.sinks];
|
|
100
|
-
const results = await Promise.allSettled(sinks.map((sink) => sink.handle(event)));
|
|
101
|
-
results.forEach((result, idx) => {
|
|
102
|
-
const name = sinks[idx]?.name;
|
|
103
|
-
if (!name) return;
|
|
104
|
-
if (result.status === "rejected") {
|
|
105
|
-
if (!this.failed.has(name)) {
|
|
106
|
-
this.failed.add(name);
|
|
107
|
-
const reason = result.reason instanceof Error ? result.reason.message : String(result.reason);
|
|
108
|
-
console.warn(`[rlm-telemetry] sink ${name} rejected event: ${reason}`);
|
|
109
|
-
}
|
|
110
|
-
} else if (this.failed.has(name)) {
|
|
111
|
-
this.failed.delete(name);
|
|
112
|
-
console.warn(`[rlm-telemetry] sink ${name} recovered`);
|
|
113
|
-
}
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
|
-
}
|
package/src/telemetry/index.ts
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import type { TelemetryConfig } from "../core/types.ts";
|
|
2
|
-
import { resolveMlflowConfig } from "./mlflow-config.ts";
|
|
3
|
-
import type { TelemetrySink } from "./sink.ts";
|
|
4
|
-
|
|
5
|
-
export type { TelemetrySink };
|
|
6
|
-
|
|
7
|
-
export async function createTelemetrySink(config: TelemetryConfig | undefined): Promise<TelemetrySink | undefined> {
|
|
8
|
-
if (!config) return undefined;
|
|
9
|
-
const resolved = resolveMlflowConfig({ trackingUri: config.trackingUri, experimentId: config.experimentId });
|
|
10
|
-
const enabled = config.enabled ?? Boolean(resolved.trackingUri);
|
|
11
|
-
if (!enabled || !resolved.trackingUri) return undefined;
|
|
12
|
-
const { MlflowSink } = await import("./mlflow-sink.ts");
|
|
13
|
-
return new MlflowSink(resolved, config.maxQueueSize);
|
|
14
|
-
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
export interface MlflowConfig {
|
|
2
|
-
readonly trackingUri?: string;
|
|
3
|
-
readonly experimentId?: string;
|
|
4
|
-
readonly trackingToken?: string;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
const readEnv = (key: string): string | undefined => process.env[key]?.trim() || undefined;
|
|
8
|
-
|
|
9
|
-
export function resolveMlflowConfig(config: Pick<MlflowConfig, "trackingUri" | "experimentId">): MlflowConfig {
|
|
10
|
-
return {
|
|
11
|
-
trackingUri: readEnv("MLFLOW_TRACKING_URI") || config.trackingUri,
|
|
12
|
-
experimentId: readEnv("MLFLOW_EXPERIMENT_ID") || config.experimentId,
|
|
13
|
-
trackingToken: readEnv("MLFLOW_TRACKING_TOKEN"),
|
|
14
|
-
};
|
|
15
|
-
}
|