@pi-unipi/unipi 2.5.0 → 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/CHANGELOG.md +24 -0
- package/README.md +2 -0
- package/docs/prefix-cache-architecture.md +89 -0
- package/package.json +22 -21
- package/packages/ask-user/package.json +2 -2
- package/packages/autocomplete/package.json +1 -1
- package/packages/autocomplete/src/constants.ts +2 -0
- package/packages/btw/package.json +2 -2
- package/packages/cocoindex/README.md +2 -1
- package/packages/cocoindex/index.ts +6 -13
- package/packages/cocoindex/package.json +4 -3
- package/packages/cocoindex/tools.ts +45 -11
- package/packages/compactor/README.md +4 -2
- package/packages/compactor/package.json +3 -3
- package/packages/compactor/src/session/snapshot.ts +3 -2
- package/packages/compactor/src/tools/register.ts +6 -2
- package/packages/compactor/src/tools/vcc-recall.ts +18 -3
- package/packages/core/bounded-output.ts +106 -0
- package/packages/core/constants.ts +2 -0
- package/packages/core/index.ts +1 -0
- package/packages/core/package.json +1 -1
- package/packages/footer/package.json +2 -2
- package/packages/image/package.json +2 -2
- package/packages/info-screen/package.json +2 -2
- package/packages/input-shortcuts/package.json +2 -2
- package/packages/kanboard/package.json +2 -2
- package/packages/mcp/README.md +4 -0
- package/packages/mcp/package.json +2 -2
- package/packages/mcp/src/bridge/translator.ts +19 -12
- package/packages/memory/index.ts +56 -34
- package/packages/memory/package.json +3 -3
- package/packages/milestone/hooks.ts +1 -1
- package/packages/milestone/package.json +2 -2
- package/packages/notify/package.json +2 -2
- package/packages/ralph/index.ts +12 -18
- package/packages/ralph/package.json +6 -3
- package/packages/ralph/reminder.ts +40 -0
- package/packages/ralph/tools.ts +5 -1
- package/packages/subagents/README.md +2 -0
- package/packages/subagents/package.json +1 -1
- package/packages/subagents/src/agent-manager.ts +5 -1
- package/packages/subagents/src/agent-runner.ts +2 -2
- package/packages/subagents/src/core-compat.ts +73 -0
- package/packages/subagents/src/custom-agents.ts +10 -2
- package/packages/subagents/src/index.ts +14 -3
- package/packages/subagents/src/types.ts +2 -0
- package/packages/unipi/bundled.js +786 -312
- package/packages/updater/package.json +2 -2
- package/packages/utility/README.md +9 -0
- package/packages/utility/package.json +2 -2
- package/packages/utility/src/index.ts +48 -0
- package/packages/utility/src/lifecycle/cleanup.ts +29 -0
- package/packages/utility/src/prefix-cache.ts +263 -0
- package/packages/utility/src/types.ts +1 -1
- package/packages/web-api/package.json +2 -2
- package/packages/workflow/index.ts +2 -2
- package/packages/workflow/package.json +2 -2
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { chmodSync, lstatSync, mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
|
|
6
|
+
export const DEFAULT_MODEL_OUTPUT_BYTES = 64 * 1024;
|
|
7
|
+
export const MAX_RAW_ARTIFACT_BYTES = 16 * 1024 * 1024;
|
|
8
|
+
|
|
9
|
+
export interface BoundedOutput {
|
|
10
|
+
text: string;
|
|
11
|
+
truncated: boolean;
|
|
12
|
+
originalBytes: number;
|
|
13
|
+
visibleBytes: number;
|
|
14
|
+
artifactPath?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface BoundOutputOptions {
|
|
18
|
+
maxBytes?: number;
|
|
19
|
+
artifactPrefix?: string;
|
|
20
|
+
artifactDir?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function byteSlice(text: string, start: number, end?: number): string {
|
|
24
|
+
return Buffer.from(text, "utf8").subarray(start, end).toString("utf8").replace(/\uFFFD+$/u, "");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function secureArtifactDir(customDir?: string): string {
|
|
28
|
+
const dir = customDir ?? join(homedir(), ".unipi", "tool-results");
|
|
29
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
30
|
+
let stat = lstatSync(dir);
|
|
31
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
32
|
+
throw new Error(`Refusing unsafe tool-result directory: ${dir}`);
|
|
33
|
+
}
|
|
34
|
+
if ((stat.mode & 0o077) !== 0) {
|
|
35
|
+
chmodSync(dir, 0o700);
|
|
36
|
+
stat = lstatSync(dir);
|
|
37
|
+
if ((stat.mode & 0o077) !== 0) {
|
|
38
|
+
throw new Error(`Refusing non-private tool-result directory: ${dir}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return dir;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function safePrefix(prefix: string): string {
|
|
45
|
+
const safe = prefix.replace(/[^a-zA-Z0-9_-]/g, "-").replace(/-+/g, "-").slice(0, 48);
|
|
46
|
+
return safe || "tool-result";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Bound model-visible UTF-8 output and, up to the raw artifact safety cap,
|
|
51
|
+
* preserve the complete text in a private local artifact.
|
|
52
|
+
*
|
|
53
|
+
* Artifacts use random names and mode 0600 beneath a mode-0700 directory. The
|
|
54
|
+
* returned path is intentionally explicit so the agent can retrieve it with
|
|
55
|
+
* the ordinary read tool only when full output is actually needed.
|
|
56
|
+
*/
|
|
57
|
+
export function boundModelOutput(text: string, options: BoundOutputOptions = {}): BoundedOutput {
|
|
58
|
+
const maxBytes = Math.max(1024, Math.floor(options.maxBytes ?? DEFAULT_MODEL_OUTPUT_BYTES));
|
|
59
|
+
const originalBytes = Buffer.byteLength(text, "utf8");
|
|
60
|
+
if (originalBytes <= maxBytes) {
|
|
61
|
+
return { text, truncated: false, originalBytes, visibleBytes: originalBytes };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let artifactPath: string | undefined;
|
|
65
|
+
let artifactWarning: string | undefined;
|
|
66
|
+
if (originalBytes <= MAX_RAW_ARTIFACT_BYTES) {
|
|
67
|
+
try {
|
|
68
|
+
const dir = secureArtifactDir(options.artifactDir);
|
|
69
|
+
artifactPath = join(dir, `${safePrefix(options.artifactPrefix ?? "tool-result")}-${randomUUID()}.txt`);
|
|
70
|
+
writeFileSync(artifactPath, text, { encoding: "utf8", mode: 0o600, flag: "wx" });
|
|
71
|
+
} catch (error) {
|
|
72
|
+
artifactWarning = `Full-output artifact unavailable: ${error instanceof Error ? error.message : String(error)}`;
|
|
73
|
+
artifactPath = undefined;
|
|
74
|
+
}
|
|
75
|
+
} else {
|
|
76
|
+
artifactWarning = `Full output exceeded the ${MAX_RAW_ARTIFACT_BYTES}-byte local artifact safety cap and was not retained.`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const marker = [
|
|
80
|
+
"",
|
|
81
|
+
"--- output bounded by UniPi ---",
|
|
82
|
+
artifactPath ? `Full output: ${artifactPath}` : artifactWarning!,
|
|
83
|
+
`Original size: ${originalBytes} bytes; model-visible ceiling: ${maxBytes} bytes.`,
|
|
84
|
+
...(artifactPath ? ["Use the read tool with offset/limit to inspect only the needed region."] : []),
|
|
85
|
+
].join("\n");
|
|
86
|
+
const omissionReserve = 80;
|
|
87
|
+
const markerBytes = Buffer.byteLength(marker, "utf8");
|
|
88
|
+
const contentBudget = Math.max(1, maxBytes - markerBytes - omissionReserve);
|
|
89
|
+
const headBytes = Math.ceil(contentBudget * 0.75);
|
|
90
|
+
const tailBytes = Math.max(0, contentBudget - headBytes);
|
|
91
|
+
const head = byteSlice(text, 0, headBytes);
|
|
92
|
+
const tail = tailBytes > 0 ? byteSlice(text, originalBytes - tailBytes) : "";
|
|
93
|
+
const omission = `\n… ${Math.max(0, originalBytes - contentBudget)} bytes omitted …\n`;
|
|
94
|
+
let bounded = `${head}${omission}${tail}${marker}`;
|
|
95
|
+
if (Buffer.byteLength(bounded, "utf8") > maxBytes) {
|
|
96
|
+
bounded = byteSlice(bounded, 0, maxBytes);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
text: bounded,
|
|
101
|
+
truncated: true,
|
|
102
|
+
originalBytes,
|
|
103
|
+
visibleBytes: Buffer.byteLength(bounded, "utf8"),
|
|
104
|
+
artifactPath,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
@@ -168,6 +168,7 @@ export const UTILITY_COMMANDS = {
|
|
|
168
168
|
BADGE_TOGGLE: "badge-toggle",
|
|
169
169
|
BADGE_SETTINGS: "badge-settings",
|
|
170
170
|
UTIL_SETTINGS: "util-settings",
|
|
171
|
+
PREFIX_CACHE: "prefix-cache",
|
|
171
172
|
} as const;
|
|
172
173
|
|
|
173
174
|
/** Utility tool names */
|
|
@@ -232,6 +233,7 @@ export const MCP_DEFAULTS = {
|
|
|
232
233
|
STARTUP_TIMEOUT_MS: 10000,
|
|
233
234
|
MAX_SERVERS: 20,
|
|
234
235
|
TOOL_NAME_SEPARATOR: "__",
|
|
236
|
+
MAX_MODEL_OUTPUT_BYTES: 64 * 1024,
|
|
235
237
|
} as const;
|
|
236
238
|
|
|
237
239
|
/** Compactor sentinel — when passed as customInstructions to ctx.compact(),
|
package/packages/core/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/footer",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.0",
|
|
4
4
|
"description": "Persistent status bar for Unipi — subscribes to UNIPI_EVENTS and renders key stats from all unipi packages",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"access": "public"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@pi-unipi/core": "2.
|
|
35
|
+
"@pi-unipi/core": "2.6.0"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
38
|
"@earendil-works/pi-coding-agent": "^0.80.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/image",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.0",
|
|
4
4
|
"description": "Image generation and image recognition tools for the Pi coding agent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"access": "public"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@pi-unipi/core": "2.
|
|
37
|
+
"@pi-unipi/core": "2.6.0"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
40
40
|
"@earendil-works/pi-ai": "^0.80.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/info-screen",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.0",
|
|
4
4
|
"description": "Dashboard and module registry for Unipi — configurable info overlay with tabbed groups",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"access": "public"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@pi-unipi/core": "2.
|
|
36
|
+
"@pi-unipi/core": "2.6.0"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
39
|
"@earendil-works/pi-coding-agent": "^0.80.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/input-shortcuts",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.0",
|
|
4
4
|
"description": "Keyboard shortcuts for stash/restore, undo/redo, clipboard, and thinking toggle — chord-based overlay system",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"access": "public"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@pi-unipi/core": "2.
|
|
36
|
+
"@pi-unipi/core": "2.6.0"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
39
|
"@earendil-works/pi-coding-agent": "^0.80.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/kanboard",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.0",
|
|
4
4
|
"description": "Visualization layer for unipi workflow — HTTP server with htmx/Alpine.js UI, modular parsers, TUI overlay, and kanban board",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"access": "public"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@pi-unipi/core": "2.
|
|
42
|
+
"@pi-unipi/core": "2.6.0"
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
|
45
45
|
"@earendil-works/pi-coding-agent": "^0.80.0",
|
package/packages/mcp/README.md
CHANGED
|
@@ -39,6 +39,10 @@ MCP input properties are cloned and recursively canonicalized before registratio
|
|
|
39
39
|
|
|
40
40
|
Pi 0.80 cannot remove dynamically registered tools. Enabling, disabling, deleting, or changing MCP servers is therefore applied on the next Pi restart rather than mutating the tool list mid-session. This prevents stale schemas and makes the restart an explicit cache-epoch boundary.
|
|
41
41
|
|
|
42
|
+
### Bounded Results
|
|
43
|
+
|
|
44
|
+
MCP text results are model-visible up to a hard 64 KiB ceiling. A larger result keeps a bounded head/tail preview and, when the raw result is at most 16 MiB, writes the complete text to a private mode-0600 artifact under `~/.unipi/tool-results/`. Existing result directories are tightened to mode 0700. The returned result includes the path and directs the agent to use `read` with offset/limit. Results above the raw safety cap or filesystem write failures still return a bounded preview with an explicit non-retention warning. MCP image bytes are not written by this text bridge; image blocks remain represented by MIME metadata as before.
|
|
45
|
+
|
|
42
46
|
Example tool calls:
|
|
43
47
|
```
|
|
44
48
|
github__search_code({ query: "authentication middleware" })
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.0",
|
|
4
4
|
"description": "MCP server management extension for Pi coding agent — browse, add, configure, and use MCP servers",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"test": "npx tsx --test tests/**/*.test.ts"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@pi-unipi/core": "2.
|
|
33
|
+
"@pi-unipi/core": "2.6.0"
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
36
36
|
"@earendil-works/pi-coding-agent": "^0.80.0",
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Naming convention: {serverName}__{toolName}
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { MCP_DEFAULTS } from "@pi-unipi/core";
|
|
8
|
+
import { MCP_DEFAULTS, boundModelOutput } from "@pi-unipi/core";
|
|
9
9
|
import type { McpTool, McpToolResult } from "../types.js";
|
|
10
10
|
import type { McpClient } from "./client.js";
|
|
11
11
|
|
|
@@ -170,21 +170,28 @@ export function translateMcpTool(
|
|
|
170
170
|
}
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
-
if (result.isError) {
|
|
174
|
-
const errorText = blocks.map((b) => b.text).join("\n") || "Unknown error";
|
|
175
|
-
return {
|
|
176
|
-
content: [{ type: "text", text: `MCP tool error from ${serverName}: ${errorText}` }],
|
|
177
|
-
details: { error: true, server: serverName, tool: mcpTool.name },
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
|
-
|
|
181
173
|
if (blocks.length === 0) {
|
|
182
|
-
blocks.push({ type: "text", text: "(no output)" });
|
|
174
|
+
blocks.push({ type: "text", text: result.isError ? "Unknown error" : "(no output)" });
|
|
183
175
|
}
|
|
184
176
|
|
|
177
|
+
const rawText = blocks.map((block) => block.text).join("\n");
|
|
178
|
+
const wrapper = result.isError ? `MCP tool error from ${serverName}: ` : "";
|
|
179
|
+
const output = boundModelOutput(rawText, {
|
|
180
|
+
maxBytes: Math.max(1024, MCP_DEFAULTS.MAX_MODEL_OUTPUT_BYTES - Buffer.byteLength(wrapper, "utf8")),
|
|
181
|
+
artifactPrefix: `mcp-${serverName}-${mcpTool.name}`,
|
|
182
|
+
});
|
|
183
|
+
const visibleText = `${wrapper}${output.text}`;
|
|
184
|
+
|
|
185
185
|
return {
|
|
186
|
-
content:
|
|
187
|
-
details: {
|
|
186
|
+
content: [{ type: "text", text: visibleText }],
|
|
187
|
+
details: {
|
|
188
|
+
error: result.isError || undefined,
|
|
189
|
+
server: serverName,
|
|
190
|
+
tool: mcpTool.name,
|
|
191
|
+
truncated: output.truncated,
|
|
192
|
+
originalBytes: output.originalBytes,
|
|
193
|
+
artifactPath: output.artifactPath,
|
|
194
|
+
},
|
|
188
195
|
};
|
|
189
196
|
} catch (err) {
|
|
190
197
|
const message = err instanceof Error ? err.message : String(err);
|
package/packages/memory/index.ts
CHANGED
|
@@ -35,6 +35,54 @@ import { isEmbeddingReady, hasModelChanged } from "./settings.js";
|
|
|
35
35
|
/** Package version */
|
|
36
36
|
const VERSION = getPackageVersion(dirname(fileURLToPath(import.meta.url)));
|
|
37
37
|
|
|
38
|
+
interface MemoryReminderInput {
|
|
39
|
+
projectName: string;
|
|
40
|
+
memories: Array<{ title: string }>;
|
|
41
|
+
canSearch: boolean;
|
|
42
|
+
canStore: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Build the deterministic, model-visible first-turn memory reminder.
|
|
47
|
+
* Exported so provider-payload regressions can exercise the exact production
|
|
48
|
+
* content without initializing a storage backend.
|
|
49
|
+
*/
|
|
50
|
+
export function buildMemoryRecallReminder(input: MemoryReminderInput): string {
|
|
51
|
+
const lines = [
|
|
52
|
+
"## 🧠 Memory System Active",
|
|
53
|
+
"",
|
|
54
|
+
`You have ${input.memories.length} memories stored for project "${input.projectName}".`,
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
if (input.canSearch && input.memories.length > 0) {
|
|
58
|
+
const titleList = input.memories.slice(0, 20).map((memory) => `- ${memory.title}`).join("\n");
|
|
59
|
+
const extra = input.memories.length > 20
|
|
60
|
+
? `\n... and ${input.memories.length - 20} more`
|
|
61
|
+
: "";
|
|
62
|
+
lines.push(
|
|
63
|
+
"**BEFORE starting work**, call `memory_search` with relevant keywords to check for existing context.",
|
|
64
|
+
"",
|
|
65
|
+
"Available memories:",
|
|
66
|
+
titleList + extra,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (input.canStore) {
|
|
71
|
+
lines.push(
|
|
72
|
+
"",
|
|
73
|
+
"**AFTER completing the task**, if you learned something non-obvious,",
|
|
74
|
+
"call `memory_store` to save it for future sessions.",
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
lines.push(
|
|
79
|
+
"",
|
|
80
|
+
"Guardrails: read max 10 memory results per search. Update existing memories instead of creating duplicates.",
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
return lines.join("\n");
|
|
84
|
+
}
|
|
85
|
+
|
|
38
86
|
/** Storage instance for current project */
|
|
39
87
|
let projectStorage: MemoryStorage | null = null;
|
|
40
88
|
|
|
@@ -251,44 +299,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
251
299
|
return;
|
|
252
300
|
}
|
|
253
301
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
"",
|
|
257
|
-
`You have ${projectMemories.length} memories stored for project "${projectName}".`,
|
|
258
|
-
];
|
|
259
|
-
|
|
260
|
-
if (canSearch && projectMemories.length > 0) {
|
|
261
|
-
const titleList = projectMemories.slice(0, 20).map(m => `- ${m.title}`).join("\n");
|
|
262
|
-
const extra = projectMemories.length > 20 ? `\n... and ${projectMemories.length - 20} more` : "";
|
|
263
|
-
lines.push(
|
|
264
|
-
"**BEFORE starting work**, call `memory_search` with relevant keywords to check for existing context.",
|
|
265
|
-
"",
|
|
266
|
-
"Available memories:",
|
|
267
|
-
titleList + extra,
|
|
268
|
-
);
|
|
269
|
-
} else {
|
|
270
|
-
recallDone = true;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
if (canStore) {
|
|
274
|
-
lines.push(
|
|
275
|
-
"",
|
|
276
|
-
"**AFTER completing the task**, if you learned something non-obvious,",
|
|
277
|
-
"call `memory_store` to save it for future sessions.",
|
|
278
|
-
);
|
|
279
|
-
} else {
|
|
280
|
-
storeDone = true;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
lines.push(
|
|
284
|
-
"",
|
|
285
|
-
"Guardrails: read max 10 memory results per search. Update existing memories instead of creating duplicates.",
|
|
286
|
-
);
|
|
302
|
+
if (!canSearch || projectMemories.length === 0) recallDone = true;
|
|
303
|
+
if (!canStore) storeDone = true;
|
|
287
304
|
|
|
288
305
|
return {
|
|
289
306
|
message: {
|
|
290
307
|
customType: "unipi-memory-recall-reminder",
|
|
291
|
-
content:
|
|
308
|
+
content: buildMemoryRecallReminder({
|
|
309
|
+
projectName,
|
|
310
|
+
memories: projectMemories,
|
|
311
|
+
canSearch,
|
|
312
|
+
canStore,
|
|
313
|
+
}),
|
|
292
314
|
display: false,
|
|
293
315
|
},
|
|
294
316
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/memory",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.0",
|
|
4
4
|
"description": "Persistent cross-session memory with MemPalace backend (auto-installed) and SQLite fallback for Pi coding agent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -43,8 +43,8 @@
|
|
|
43
43
|
"better-sqlite3": "^12.9.0",
|
|
44
44
|
"sqlite-vec": "^0.1.9",
|
|
45
45
|
"js-yaml": "^4.1.0",
|
|
46
|
-
"@pi-unipi/core": "2.
|
|
47
|
-
"@pi-unipi/info-screen": "2.
|
|
46
|
+
"@pi-unipi/core": "2.6.0",
|
|
47
|
+
"@pi-unipi/info-screen": "2.6.0"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
|
50
50
|
"@earendil-works/pi-coding-agent": "^0.80.0",
|
|
@@ -51,7 +51,7 @@ function formatMilestoneContext(filePath: string): string | null {
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
/** Build an append-only snapshot that explicitly invalidates earlier snapshots. */
|
|
54
|
-
function formatMilestoneSnapshot(workspace: string, context: string | null): string {
|
|
54
|
+
export function formatMilestoneSnapshot(workspace: string, context: string | null): string {
|
|
55
55
|
return [
|
|
56
56
|
"# UniPi Milestone Snapshot",
|
|
57
57
|
"This snapshot supersedes all prior UniPi milestone snapshots; use only this snapshot for milestone status.",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/milestone",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.0",
|
|
4
4
|
"description": "Lifecycle layer for project-level goals — MILESTONES.md tracking, session hooks, auto-sync",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"access": "public"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@pi-unipi/core": "2.
|
|
35
|
+
"@pi-unipi/core": "2.6.0"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
38
|
"@earendil-works/pi-coding-agent": "^0.80.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/notify",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.0",
|
|
4
4
|
"description": "Cross-platform notification extension for Pi — native OS, Gotify, and Telegram notifications for agent lifecycle events",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"access": "public"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@pi-unipi/core": "2.
|
|
37
|
+
"@pi-unipi/core": "2.6.0",
|
|
38
38
|
"node-notifier": "^10.0.1"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
package/packages/ralph/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
|
/**
|
|
@@ -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",
|
|
@@ -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/packages/ralph/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." }],
|
|
@@ -69,6 +69,8 @@ spawn_helper(
|
|
|
69
69
|
get_helper_result(agent_id: "helper_abc123")
|
|
70
70
|
```
|
|
71
71
|
|
|
72
|
+
Foreground and retrieved background results have a hard 64 KiB model-visible ceiling. For raw results up to 16 MiB, larger output includes a bounded head/tail preview and a path to the complete private mode-0600 artifact under a mode-0700 `~/.unipi/tool-results/` directory. Use `read` with offset/limit to inspect only the needed region. Repeated retrieval reuses the same artifact. Results above the safety cap or artifact-write failures still return a preview with an explicit non-retention warning.
|
|
73
|
+
|
|
72
74
|
## Custom Agent Types
|
|
73
75
|
|
|
74
76
|
Create markdown files defining agent behavior:
|