@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
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
import type { SubcallKind } from "../tool/rlm-details.ts";
|
|
2
|
-
import type { SubcallStart } from "../state/events.ts";
|
|
3
|
-
import { Dispatcher, type DispatcherSink } from "./dispatcher.ts";
|
|
4
|
-
import {
|
|
5
|
-
MlflowTracer,
|
|
6
|
-
msToNs,
|
|
7
|
-
SpanStatusCode,
|
|
8
|
-
SpanType,
|
|
9
|
-
type MlflowConfig,
|
|
10
|
-
type SpanTracer,
|
|
11
|
-
} from "./mlflow.ts";
|
|
12
|
-
import type { TelemetrySink } from "./sink.ts";
|
|
13
|
-
|
|
14
|
-
// @mlflow/core keeps process-global trace state. RLM telemetry uses one shared
|
|
15
|
-
// tracer per process and should not be combined with another MLflow init path in
|
|
16
|
-
// the same host process.
|
|
17
|
-
let sharedTracer: MlflowTracer | undefined;
|
|
18
|
-
function tracerFor(config: MlflowConfig): MlflowTracer {
|
|
19
|
-
sharedTracer ??= new MlflowTracer(config);
|
|
20
|
-
return sharedTracer;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const SPAN_TYPE: Readonly<Record<SubcallKind, SpanType>> = Object.freeze({
|
|
24
|
-
root: SpanType.AGENT,
|
|
25
|
-
rlm: SpanType.AGENT,
|
|
26
|
-
llm: SpanType.CHAT_MODEL,
|
|
27
|
-
batch: SpanType.CHAT_MODEL,
|
|
28
|
-
tool: SpanType.TOOL,
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
type SpanOp =
|
|
32
|
-
| { readonly kind: "start"; readonly id: string; readonly info: SubcallStart; readonly tsMs: number }
|
|
33
|
-
| { readonly kind: "usage"; readonly id: string; readonly costUsd: number; readonly tokens: number }
|
|
34
|
-
| { readonly kind: "end"; readonly id: string; readonly error?: string; readonly resultPreview?: string; readonly tsMs: number };
|
|
35
|
-
|
|
36
|
-
class SpanApplier<S> implements DispatcherSink<SpanOp> {
|
|
37
|
-
readonly name = "rlm-mlflow";
|
|
38
|
-
private readonly spans = new Map<string, S>();
|
|
39
|
-
private readonly usage = new Map<string, { cost: number; tokens: number }>();
|
|
40
|
-
|
|
41
|
-
constructor(private readonly tracer: SpanTracer<S>) {}
|
|
42
|
-
|
|
43
|
-
async handle(op: SpanOp): Promise<void> {
|
|
44
|
-
if (op.kind === "start") {
|
|
45
|
-
const parent = op.info.parentId ? this.spans.get(op.info.parentId) : undefined;
|
|
46
|
-
const span = this.tracer.startSpan({
|
|
47
|
-
name: op.info.label,
|
|
48
|
-
spanType: SPAN_TYPE[op.info.kind],
|
|
49
|
-
parent,
|
|
50
|
-
inputs: {
|
|
51
|
-
...(op.info.model ? { model: op.info.model } : {}),
|
|
52
|
-
...(op.info.detail ? { detail: op.info.detail } : {}),
|
|
53
|
-
},
|
|
54
|
-
attributes: {
|
|
55
|
-
"rlm.kind": op.info.kind,
|
|
56
|
-
"rlm.depth": op.info.depth,
|
|
57
|
-
...(op.info.model ? { "rlm.model": op.info.model } : {}),
|
|
58
|
-
...(op.info.runId ? { "rlm.runId": op.info.runId } : {}),
|
|
59
|
-
...(op.info.resume ? { "rlm.resume": true } : {}),
|
|
60
|
-
},
|
|
61
|
-
startTimeNs: msToNs(op.tsMs),
|
|
62
|
-
});
|
|
63
|
-
if (span !== undefined) this.spans.set(op.id, span);
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (op.kind === "usage") {
|
|
68
|
-
const usage = this.usage.get(op.id) ?? { cost: 0, tokens: 0 };
|
|
69
|
-
this.usage.set(op.id, { cost: usage.cost + op.costUsd, tokens: usage.tokens + op.tokens });
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
this.closeSpan(op.id, {
|
|
74
|
-
outputs: op.resultPreview ? { result: op.resultPreview } : undefined,
|
|
75
|
-
status: op.error ? SpanStatusCode.ERROR : undefined,
|
|
76
|
-
endTimeNs: msToNs(op.tsMs),
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
async flush(): Promise<void> {
|
|
81
|
-
await this.tracer.flush();
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
async shutdown(): Promise<void> {
|
|
85
|
-
for (const id of [...this.spans.keys()]) this.closeSpan(id, { status: SpanStatusCode.ERROR });
|
|
86
|
-
await this.tracer.flush();
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
private closeSpan(
|
|
90
|
-
id: string,
|
|
91
|
-
opts: { readonly outputs?: Readonly<Record<string, unknown>>; readonly status?: SpanStatusCode; readonly endTimeNs?: number },
|
|
92
|
-
): void {
|
|
93
|
-
const span = this.spans.get(id);
|
|
94
|
-
if (span === undefined) return;
|
|
95
|
-
const usage = this.usage.get(id);
|
|
96
|
-
this.tracer.endSpan(span, {
|
|
97
|
-
...(usage ? { attributes: { "rlm.usage.cost_usd": usage.cost, "rlm.usage.tokens": usage.tokens } } : {}),
|
|
98
|
-
...opts,
|
|
99
|
-
});
|
|
100
|
-
this.spans.delete(id);
|
|
101
|
-
this.usage.delete(id);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
export class MlflowSink implements TelemetrySink {
|
|
106
|
-
private readonly dispatcher: Dispatcher<SpanOp>;
|
|
107
|
-
|
|
108
|
-
constructor(config: MlflowConfig, maxQueueSize = 100, tracer: SpanTracer = tracerFor(config)) {
|
|
109
|
-
this.dispatcher = new Dispatcher<SpanOp>({ maxQueueSize });
|
|
110
|
-
this.dispatcher.registerSink(new SpanApplier(tracer));
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
start(id: string, info: SubcallStart): void {
|
|
114
|
-
if (id) this.dispatcher.dispatch({ kind: "start", id, info, tsMs: Date.now() });
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
usage(id: string, costUsd: number, tokens: number): void {
|
|
118
|
-
if (id) this.dispatcher.dispatch({ kind: "usage", id, costUsd, tokens });
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
end(id: string, opts?: { readonly error?: string; readonly resultPreview?: string }): void {
|
|
122
|
-
if (id) {
|
|
123
|
-
this.dispatcher.dispatch({
|
|
124
|
-
kind: "end",
|
|
125
|
-
id,
|
|
126
|
-
error: opts?.error,
|
|
127
|
-
resultPreview: opts?.resultPreview,
|
|
128
|
-
tsMs: Date.now(),
|
|
129
|
-
});
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
shutdown(): Promise<void> {
|
|
134
|
-
return this.dispatcher.shutdown();
|
|
135
|
-
}
|
|
136
|
-
}
|
package/src/telemetry/mlflow.ts
DELETED
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
import { flushTraces, init, type LiveSpan, startSpan as mlflowStartSpan, SpanStatusCode, SpanType } from "@mlflow/core";
|
|
2
|
-
import { resolveMlflowConfig, type MlflowConfig } from "./mlflow-config.ts";
|
|
3
|
-
|
|
4
|
-
export { type LiveSpan, type MlflowConfig, SpanStatusCode, SpanType };
|
|
5
|
-
|
|
6
|
-
export function msToNs(ms: number): number {
|
|
7
|
-
return ms * 1_000_000;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export interface StartSpanOptions<S> {
|
|
11
|
-
readonly name: string;
|
|
12
|
-
readonly spanType: SpanType;
|
|
13
|
-
readonly parent?: S;
|
|
14
|
-
readonly inputs?: Readonly<Record<string, unknown>>;
|
|
15
|
-
readonly attributes?: Readonly<Record<string, unknown>>;
|
|
16
|
-
readonly startTimeNs?: number;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export interface EndSpanOptions {
|
|
20
|
-
readonly attributes?: Readonly<Record<string, unknown>>;
|
|
21
|
-
readonly outputs?: Readonly<Record<string, unknown>>;
|
|
22
|
-
readonly status?: SpanStatusCode;
|
|
23
|
-
readonly endTimeNs?: number;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface SpanTracer<S = unknown> {
|
|
27
|
-
startSpan(options: StartSpanOptions<S>): S | undefined;
|
|
28
|
-
endSpan(span: S, options?: EndSpanOptions): void;
|
|
29
|
-
flush(): Promise<void>;
|
|
30
|
-
shutdown(): Promise<void>;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export class MlflowTracer implements SpanTracer<LiveSpan> {
|
|
34
|
-
private initialized = false;
|
|
35
|
-
private initAttempted = false;
|
|
36
|
-
|
|
37
|
-
constructor(private readonly config: MlflowConfig) {}
|
|
38
|
-
|
|
39
|
-
startSpan(options: StartSpanOptions<LiveSpan>): LiveSpan | undefined {
|
|
40
|
-
if (!this.ensureInit()) return undefined;
|
|
41
|
-
const span = mlflowStartSpan({
|
|
42
|
-
name: options.name,
|
|
43
|
-
spanType: options.spanType,
|
|
44
|
-
...(options.parent ? { parent: options.parent } : {}),
|
|
45
|
-
...(options.inputs ? { inputs: options.inputs } : {}),
|
|
46
|
-
...(options.startTimeNs !== undefined ? { startTimeNs: options.startTimeNs } : {}),
|
|
47
|
-
});
|
|
48
|
-
if (options.attributes) {
|
|
49
|
-
for (const [key, value] of Object.entries(options.attributes)) span.setAttribute(key, value);
|
|
50
|
-
}
|
|
51
|
-
return span;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
endSpan(span: LiveSpan, options: EndSpanOptions = {}): void {
|
|
55
|
-
if (options.attributes) {
|
|
56
|
-
for (const [key, value] of Object.entries(options.attributes)) span.setAttribute(key, value);
|
|
57
|
-
}
|
|
58
|
-
span.end({
|
|
59
|
-
...(options.outputs ? { outputs: options.outputs } : {}),
|
|
60
|
-
...(options.status !== undefined ? { status: options.status } : {}),
|
|
61
|
-
...(options.endTimeNs !== undefined ? { endTimeNs: options.endTimeNs } : {}),
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
async flush(): Promise<void> {
|
|
66
|
-
if (!this.initialized) return;
|
|
67
|
-
try {
|
|
68
|
-
await flushTraces();
|
|
69
|
-
} catch (err) {
|
|
70
|
-
console.warn(`[rlm-telemetry] mlflow flush failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
async shutdown(): Promise<void> {
|
|
75
|
-
await this.flush();
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
private ensureInit(): boolean {
|
|
79
|
-
if (this.initialized || this.initAttempted) return this.initialized;
|
|
80
|
-
this.initAttempted = true;
|
|
81
|
-
const resolved = resolveMlflowConfig(this.config);
|
|
82
|
-
if (!resolved.trackingUri) {
|
|
83
|
-
console.warn("[rlm-telemetry] MLFLOW_TRACKING_URI unset — spans dropped");
|
|
84
|
-
return false;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
try {
|
|
88
|
-
init({
|
|
89
|
-
trackingUri: resolved.trackingUri,
|
|
90
|
-
experimentId: resolved.experimentId ?? "0",
|
|
91
|
-
...(resolved.trackingToken ? { trackingServerToken: resolved.trackingToken } : {}),
|
|
92
|
-
});
|
|
93
|
-
this.initialized = true;
|
|
94
|
-
} catch (err) {
|
|
95
|
-
console.warn(`[rlm-telemetry] mlflow init failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
96
|
-
}
|
|
97
|
-
return this.initialized;
|
|
98
|
-
}
|
|
99
|
-
}
|
package/src/telemetry/sink.ts
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import type { SubcallStart } from "../state/events.ts";
|
|
2
|
-
|
|
3
|
-
export interface TelemetrySink {
|
|
4
|
-
start(id: string, info: SubcallStart): void;
|
|
5
|
-
usage(id: string, costUsd: number, tokens: number): void;
|
|
6
|
-
end(id: string, opts?: { readonly error?: string; readonly resultPreview?: string }): void;
|
|
7
|
-
shutdown(): Promise<void>;
|
|
8
|
-
}
|
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `apply_diff` — the main-agent-facing editing tool.
|
|
3
|
-
*
|
|
4
|
-
* Lightweight by design: the root model produces a complete unified diff
|
|
5
|
-
* directly and this tool validates its header then writes to disk via `applyEdits()`.
|
|
6
|
-
* Unlike the old `propose_edits`,
|
|
7
|
-
* there is NO inner RLM engine turn (no generate→validate→revise loop) — the
|
|
8
|
-
* model is trusted to emit a correct diff.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { Type } from "typebox";
|
|
12
|
-
import { Text } from "@earendil-works/pi-tui";
|
|
13
|
-
import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
14
|
-
import { applyEdits } from "../patch/index.ts";
|
|
15
|
-
import { validateToolParams } from "./tool-utils.ts";
|
|
16
|
-
import { errorMessage } from "../util/errors.ts";
|
|
17
|
-
import { headlineStatusGlyph } from "./subcall-render.ts";
|
|
18
|
-
import type { ProposedDiffEdit } from "../sandbox/protocol.ts";
|
|
19
|
-
|
|
20
|
-
// ── Parameter schema ──────────────────────────────────────────────────────
|
|
21
|
-
|
|
22
|
-
const ApplyDiffParams = Object.freeze(Type.Object({
|
|
23
|
-
diff: Type.String({
|
|
24
|
-
description: "Full unified diff string. Must include --- a/<path> / +++ b/<path> header and @@ hunk markers.",
|
|
25
|
-
}),
|
|
26
|
-
}));
|
|
27
|
-
|
|
28
|
-
// ── Details (no inner engine → no sub-call tree) ──────────────────────────
|
|
29
|
-
|
|
30
|
-
export interface ApplyDiffDetails {
|
|
31
|
-
readonly status: "done" | "error";
|
|
32
|
-
readonly path: string;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const errorDetails = (): ApplyDiffDetails => ({ status: "error", path: "" });
|
|
36
|
-
|
|
37
|
-
// ── Helpers ───────────────────────────────────────────────────────────────
|
|
38
|
-
|
|
39
|
-
/** True when the diff carries a real `--- a/<path>` header (not a bare `---`). */
|
|
40
|
-
function hasValidHeader(diff: string): boolean {
|
|
41
|
-
return /^--- \S/m.test(diff);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/** Extract the first target file path from a `--- a/<path>` header line. */
|
|
45
|
-
function firstDiffPath(diff: string): string {
|
|
46
|
-
const match = /^--- a\/(.+)$/m.exec(diff);
|
|
47
|
-
return match?.[1] ?? "(unknown path)";
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/** First ~60 chars of the diff body, starting at the first hunk marker. */
|
|
51
|
-
function bodyPreview(diff: string): string {
|
|
52
|
-
const start = diff.indexOf("@@");
|
|
53
|
-
const body = start >= 0 ? diff.slice(start) : diff;
|
|
54
|
-
return body.replace(/\n/g, " ").slice(0, 60);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// ── Tool factory ──────────────────────────────────────────────────────────
|
|
58
|
-
|
|
59
|
-
export function createApplyDiffTool(): ToolDefinition<typeof ApplyDiffParams, ApplyDiffDetails> {
|
|
60
|
-
return {
|
|
61
|
-
name: "apply_diff",
|
|
62
|
-
label: "Apply Diff",
|
|
63
|
-
description: [
|
|
64
|
-
"Apply a complete unified diff directly to disk.",
|
|
65
|
-
"The diff MUST include a --- a/<path> / +++ b/<path> header and @@ hunk markers.",
|
|
66
|
-
].join(" "),
|
|
67
|
-
parameters: ApplyDiffParams,
|
|
68
|
-
|
|
69
|
-
async execute(_toolCallId, rawParams, _signal, _onUpdate, ctx) {
|
|
70
|
-
const validation = validateToolParams(ApplyDiffParams, rawParams, "apply_diff", errorDetails);
|
|
71
|
-
if (!validation.ok) return validation.error;
|
|
72
|
-
const { diff } = validation.value;
|
|
73
|
-
|
|
74
|
-
if (!diff.trim()) {
|
|
75
|
-
return {
|
|
76
|
-
content: [{ type: "text", text: "apply_diff requires a non-empty diff." }],
|
|
77
|
-
details: errorDetails(),
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// No `path` param → the model MUST include a complete header. Reject with a
|
|
82
|
-
// clear message instead of a silent fallback so a malformed diff is surfaced.
|
|
83
|
-
if (!hasValidHeader(diff)) {
|
|
84
|
-
return {
|
|
85
|
-
content: [{
|
|
86
|
-
type: "text",
|
|
87
|
-
text: "apply_diff requires a complete unified diff with --- a/<path> / +++ b/<path> header and @@ hunk markers.",
|
|
88
|
-
}],
|
|
89
|
-
details: errorDetails(),
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const path = firstDiffPath(diff);
|
|
94
|
-
const diffEdit: ProposedDiffEdit = { diff };
|
|
95
|
-
try {
|
|
96
|
-
await applyEdits([], [diffEdit], ctx);
|
|
97
|
-
} catch (e) {
|
|
98
|
-
return {
|
|
99
|
-
content: [{ type: "text", text: `apply_diff failed: ${errorMessage(e)}` }],
|
|
100
|
-
details: { status: "error", path },
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
return {
|
|
105
|
-
content: [{ type: "text", text: `Applied diff to ${path}.` }],
|
|
106
|
-
details: { status: "done", path },
|
|
107
|
-
};
|
|
108
|
-
},
|
|
109
|
-
|
|
110
|
-
renderCall(args, theme) {
|
|
111
|
-
const path = firstDiffPath(args.diff);
|
|
112
|
-
return new Text(
|
|
113
|
-
[theme.fg("toolTitle", theme.bold("apply_diff ")), theme.fg("dim", `${path}: ${bodyPreview(args.diff)}`)].join(""),
|
|
114
|
-
0, 0,
|
|
115
|
-
);
|
|
116
|
-
},
|
|
117
|
-
|
|
118
|
-
renderResult(result, _options, theme) {
|
|
119
|
-
const details = result.details as ApplyDiffDetails | undefined;
|
|
120
|
-
const path = details?.path ? details.path : "(unknown path)";
|
|
121
|
-
const glyph = headlineStatusGlyph(details?.status === "error" ? "error" : "done", theme);
|
|
122
|
-
return new Text(`${glyph} ${theme.fg("toolTitle", theme.bold("apply_diff"))} · ${path}`, 0, 0);
|
|
123
|
-
},
|
|
124
|
-
};
|
|
125
|
-
}
|