@pi-unipi/ralph 2.4.2 → 2.6.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.
- package/index.ts +12 -18
- package/package.json +6 -3
- package/reminder.test.ts +66 -0
- package/reminder.ts +40 -0
- package/tools.ts +5 -1
package/index.ts
CHANGED
|
@@ -30,6 +30,7 @@ const VERSION = getPackageVersion(dirname(fileURLToPath(import.meta.url)));
|
|
|
30
30
|
/** Current loop manager instance (recreated on session reload) */
|
|
31
31
|
let manager: RalphLoopManager | null = null;
|
|
32
32
|
|
|
33
|
+
|
|
33
34
|
/**
|
|
34
35
|
* Get or create the loop manager for the current context.
|
|
35
36
|
*/
|
|
@@ -42,9 +43,14 @@ function getManager(ctx: ExtensionContext, pi: ExtensionAPI): RalphLoopManager {
|
|
|
42
43
|
return manager;
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
export { buildRalphLoopReminder } from "./reminder.js";
|
|
47
|
+
import { buildRalphLoopReminder, latestRalphReminder, RALPH_REMINDER_TYPE } from "./reminder.js";
|
|
48
|
+
|
|
45
49
|
export default function (pi: ExtensionAPI) {
|
|
46
|
-
// Register
|
|
47
|
-
//
|
|
50
|
+
// Register static tool definitions at extension load. Their executors resolve
|
|
51
|
+
// the session-scoped manager lazily, so schemas never arrive late or reorder
|
|
52
|
+
// the provider tool array during session_start.
|
|
53
|
+
registerRalphTools(pi, (ctx) => getManager(ctx, pi));
|
|
48
54
|
|
|
49
55
|
// Register commands
|
|
50
56
|
registerCommands(pi);
|
|
@@ -157,20 +163,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
157
163
|
const state = mgr.loadState(currentLoop);
|
|
158
164
|
if (!state || state.status !== "active") return;
|
|
159
165
|
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
let instructions = `You are in a Ralph loop working on: ${state.taskFile}\n`;
|
|
163
|
-
if (state.itemsPerIteration > 0) {
|
|
164
|
-
instructions += `- Work on ~${state.itemsPerIteration} items this iteration\n`;
|
|
165
|
-
}
|
|
166
|
-
instructions += `- Update the task file as you progress\n`;
|
|
167
|
-
instructions += `- When FULLY COMPLETE: ${RALPH_COMPLETE_MARKER}\n`;
|
|
168
|
-
instructions += `- Otherwise, call ralph_done tool to proceed to next iteration`;
|
|
166
|
+
const content = buildRalphLoopReminder(state);
|
|
167
|
+
if (latestRalphReminder(ctx) === content) return;
|
|
169
168
|
|
|
170
169
|
return {
|
|
171
170
|
message: {
|
|
172
|
-
customType:
|
|
173
|
-
content
|
|
171
|
+
customType: RALPH_REMINDER_TYPE,
|
|
172
|
+
content,
|
|
174
173
|
display: false,
|
|
175
174
|
},
|
|
176
175
|
};
|
|
@@ -187,11 +186,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
187
186
|
manager = null;
|
|
188
187
|
});
|
|
189
188
|
|
|
190
|
-
// Register tools after manager setup
|
|
191
|
-
pi.on("session_start", async (_event, ctx) => {
|
|
192
|
-
const mgr = getManager(ctx, pi);
|
|
193
|
-
registerRalphTools(pi, mgr);
|
|
194
|
-
});
|
|
195
189
|
}
|
|
196
190
|
|
|
197
191
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/ralph",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.0",
|
|
4
4
|
"description": "Long-running iterative development loops for Pi coding agent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -26,9 +26,12 @@
|
|
|
26
26
|
"publishConfig": {
|
|
27
27
|
"access": "public"
|
|
28
28
|
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"test": "npx tsx --test reminder.test.ts"
|
|
31
|
+
},
|
|
29
32
|
"dependencies": {
|
|
30
|
-
"@pi-unipi/core": "2.
|
|
31
|
-
"@pi-unipi/info-screen": "2.
|
|
33
|
+
"@pi-unipi/core": "2.6.0",
|
|
34
|
+
"@pi-unipi/info-screen": "2.6.0"
|
|
32
35
|
},
|
|
33
36
|
"peerDependencies": {
|
|
34
37
|
"@earendil-works/pi-ai": "^0.80.0",
|
package/reminder.test.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import {
|
|
4
|
+
buildRalphLoopReminder,
|
|
5
|
+
latestRalphReminder,
|
|
6
|
+
RALPH_REMINDER_TYPE,
|
|
7
|
+
} from "./reminder.ts";
|
|
8
|
+
|
|
9
|
+
const state = {
|
|
10
|
+
name: "cache-rollout",
|
|
11
|
+
iteration: 3,
|
|
12
|
+
maxIterations: 10,
|
|
13
|
+
taskFile: ".unipi/ralph/cache-rollout.md",
|
|
14
|
+
itemsPerIteration: 2,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function context(branch: any[]) {
|
|
18
|
+
return { sessionManager: { getBranch: () => branch } } as any;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function custom(content: string) {
|
|
22
|
+
return {
|
|
23
|
+
type: "custom_message",
|
|
24
|
+
customType: RALPH_REMINDER_TYPE,
|
|
25
|
+
content,
|
|
26
|
+
id: "r1",
|
|
27
|
+
parentId: null,
|
|
28
|
+
timestamp: "2026-08-14T00:00:00.000Z",
|
|
29
|
+
display: false,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe("Ralph append-only reminder snapshots", () => {
|
|
34
|
+
it("is deterministic and explicitly supersedes older reminders", () => {
|
|
35
|
+
const first = buildRalphLoopReminder(state);
|
|
36
|
+
const second = buildRalphLoopReminder({ ...state });
|
|
37
|
+
assert.equal(first, second);
|
|
38
|
+
assert.match(first, /supersedes all earlier Ralph loop reminders/);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("finds an unchanged retained snapshot for deduplication", () => {
|
|
42
|
+
const content = buildRalphLoopReminder(state);
|
|
43
|
+
assert.equal(latestRalphReminder(context([custom(content)])), content);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("returns the newest retained snapshot when state changes", () => {
|
|
47
|
+
const old = buildRalphLoopReminder({ ...state, iteration: 2 });
|
|
48
|
+
const current = buildRalphLoopReminder(state);
|
|
49
|
+
assert.equal(latestRalphReminder(context([custom(old), { ...custom(current), id: "r2" }])), current);
|
|
50
|
+
assert.notEqual(old, current);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("stops at compaction so current state is reinjected once in the new epoch", () => {
|
|
54
|
+
const old = buildRalphLoopReminder({ ...state, iteration: 2 });
|
|
55
|
+
const compaction = {
|
|
56
|
+
type: "compaction",
|
|
57
|
+
id: "c1",
|
|
58
|
+
parentId: "r1",
|
|
59
|
+
timestamp: "2026-08-14T00:00:01.000Z",
|
|
60
|
+
summary: `folded: ${old}`,
|
|
61
|
+
firstKeptEntryId: "r1",
|
|
62
|
+
tokensBefore: 1000,
|
|
63
|
+
};
|
|
64
|
+
assert.equal(latestRalphReminder(context([custom(old), compaction])), null);
|
|
65
|
+
});
|
|
66
|
+
});
|
package/reminder.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { ExtensionContext, SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { RALPH_COMPLETE_MARKER } from "@pi-unipi/core";
|
|
3
|
+
|
|
4
|
+
export const RALPH_REMINDER_TYPE = "unipi-ralph-loop-reminder";
|
|
5
|
+
|
|
6
|
+
export interface RalphReminderInput {
|
|
7
|
+
name: string;
|
|
8
|
+
iteration: number;
|
|
9
|
+
maxIterations: number;
|
|
10
|
+
taskFile: string;
|
|
11
|
+
itemsPerIteration: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Build the exact deterministic hidden reminder used by the live hook. */
|
|
15
|
+
export function buildRalphLoopReminder(state: RalphReminderInput): string {
|
|
16
|
+
const iterStr = `${state.iteration}${state.maxIterations > 0 ? `/${state.maxIterations}` : ""}`;
|
|
17
|
+
let instructions = "This snapshot supersedes all earlier Ralph loop reminders.\n";
|
|
18
|
+
instructions += `You are in a Ralph loop working on: ${state.taskFile}\n`;
|
|
19
|
+
if (state.itemsPerIteration > 0) {
|
|
20
|
+
instructions += `- Work on ~${state.itemsPerIteration} items this iteration\n`;
|
|
21
|
+
}
|
|
22
|
+
instructions += "- Update the task file as you progress\n";
|
|
23
|
+
instructions += `- When FULLY COMPLETE: ${RALPH_COMPLETE_MARKER}\n`;
|
|
24
|
+
instructions += "- Otherwise, call ralph_done tool to proceed to next iteration";
|
|
25
|
+
return `[RALPH LOOP - ${state.name} - Iteration ${iterStr}]\n\n${instructions}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function latestRalphReminder(ctx: Pick<ExtensionContext, "sessionManager">): string | null {
|
|
29
|
+
const branch = ctx.sessionManager.getBranch() as SessionEntry[];
|
|
30
|
+
for (let index = branch.length - 1; index >= 0; index--) {
|
|
31
|
+
const entry = branch[index];
|
|
32
|
+
if (entry.type === "custom_message" && entry.customType === RALPH_REMINDER_TYPE) {
|
|
33
|
+
return typeof entry.content === "string" ? entry.content : null;
|
|
34
|
+
}
|
|
35
|
+
// A compacted summary may contain the old reminder but no longer retains a
|
|
36
|
+
// dedicated custom entry. Inject the current snapshot once in the new epoch.
|
|
37
|
+
if (entry.type === "compaction") break;
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
package/tools.ts
CHANGED
|
@@ -9,10 +9,12 @@ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-a
|
|
|
9
9
|
import { RALPH_COMPLETE_MARKER, RALPH_DEFAULTS, RALPH_TOOLS } from "@pi-unipi/core";
|
|
10
10
|
import { RalphLoopManager, DEFAULT_REFLECT_INSTRUCTIONS } from "./ralph-loop.js";
|
|
11
11
|
|
|
12
|
+
type ManagerProvider = (ctx: ExtensionContext) => RalphLoopManager;
|
|
13
|
+
|
|
12
14
|
/**
|
|
13
15
|
* Register ralph_start and ralph_done tools.
|
|
14
16
|
*/
|
|
15
|
-
export function registerRalphTools(pi: ExtensionAPI,
|
|
17
|
+
export function registerRalphTools(pi: ExtensionAPI, getManager: ManagerProvider): void {
|
|
16
18
|
// --- ralph_start tool ---
|
|
17
19
|
pi.registerTool({
|
|
18
20
|
name: RALPH_TOOLS.START,
|
|
@@ -44,6 +46,7 @@ export function registerRalphTools(pi: ExtensionAPI, manager: RalphLoopManager):
|
|
|
44
46
|
),
|
|
45
47
|
}),
|
|
46
48
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
49
|
+
const manager = getManager(ctx);
|
|
47
50
|
const taskFile = `.unipi/ralph/${params.name.replace(/[^a-zA-Z0-9_-]/g, "_")}.md`;
|
|
48
51
|
|
|
49
52
|
if (manager.loadState(params.name)?.status === "active") {
|
|
@@ -92,6 +95,7 @@ export function registerRalphTools(pi: ExtensionAPI, manager: RalphLoopManager):
|
|
|
92
95
|
],
|
|
93
96
|
parameters: Type.Object({}),
|
|
94
97
|
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
|
|
98
|
+
const manager = getManager(ctx);
|
|
95
99
|
if (!manager.getCurrentLoop()) {
|
|
96
100
|
return {
|
|
97
101
|
content: [{ type: "text", text: "No active Ralph loop." }],
|