@pi-unipi/unipi 2.4.0 → 2.4.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/CHANGELOG.md +43 -0
- package/package.json +26 -22
- package/packages/ask-user/package.json +2 -2
- package/packages/autocomplete/package.json +4 -2
- package/packages/btw/package.json +2 -2
- package/packages/cocoindex/package.json +2 -2
- package/packages/cocoindex/tools.ts +0 -8
- package/packages/compactor/README.md +18 -22
- package/packages/compactor/package.json +3 -3
- package/packages/compactor/src/compaction/auto-trigger.ts +0 -0
- package/packages/compactor/src/compaction/build-sections.ts +2 -2
- package/packages/compactor/src/compaction/format.ts +1 -1
- package/packages/compactor/src/config/presets.ts +19 -30
- package/packages/compactor/src/config/schema.ts +0 -0
- package/packages/compactor/src/index.ts +39 -56
- package/packages/compactor/src/session/resume-inject.ts +27 -0
- package/packages/compactor/src/tools/ctx-batch-execute.ts +4 -2
- package/packages/compactor/src/tools/ctx-execute-file.ts +4 -2
- package/packages/compactor/src/tools/ctx-execute.ts +4 -2
- package/packages/compactor/src/tools/register.ts +22 -4
- package/packages/compactor/src/tui/settings-overlay.ts +9 -22
- package/packages/compactor/src/types.ts +24 -2
- package/packages/core/events.ts +13 -13
- package/packages/core/package.json +3 -2
- package/packages/footer/README.md +2 -2
- package/packages/footer/package.json +3 -3
- package/packages/footer/src/events.ts +0 -10
- package/packages/footer/src/segments/core.ts +0 -9
- package/packages/image/package.json +2 -2
- package/packages/info-screen/core-groups.ts +0 -14
- package/packages/info-screen/package.json +2 -2
- package/packages/input-shortcuts/package.json +2 -2
- package/packages/kanboard/commands.ts +0 -1
- package/packages/kanboard/package.json +2 -2
- package/packages/mcp/package.json +2 -2
- package/packages/memory/package.json +3 -3
- package/packages/memory/storage.ts +0 -18
- package/packages/memory/tools.ts +0 -1
- package/packages/milestone/hooks.ts +9 -15
- package/packages/milestone/package.json +2 -2
- package/packages/notify/README.md +2 -0
- package/packages/notify/events.ts +23 -13
- package/packages/notify/package.json +2 -2
- package/packages/notify/tools.ts +17 -8
- package/packages/notify/types.ts +5 -1
- package/packages/ralph/index.ts +10 -5
- package/packages/ralph/package.json +3 -3
- package/packages/ralph/ralph-loop.ts +10 -0
- package/packages/subagents/README.md +3 -1
- package/packages/subagents/package.json +10 -6
- package/packages/subagents/src/agent-manager.ts +46 -11
- package/packages/subagents/src/core-compat.ts +38 -0
- package/packages/subagents/src/global.d.ts +7 -0
- package/packages/subagents/src/index.ts +6 -3
- package/packages/subagents/src/widget.ts +0 -7
- package/packages/unipi/bundled.js +371 -438
- package/packages/updater/package.json +4 -3
- package/packages/utility/README.md +1 -1
- package/packages/utility/package.json +7 -4
- package/packages/utility/src/commands.ts +2 -10
- package/packages/utility/src/diff/theme.ts +0 -3
- package/packages/utility/src/tui/name-badge.ts +0 -9
- package/packages/web-api/package.json +2 -2
- package/packages/web-api/src/engine/extract.ts +0 -26
- package/packages/web-api/src/tools.ts +0 -13
- package/packages/workflow/commands.ts +13 -1
- package/packages/workflow/index.ts +12 -1
- package/packages/workflow/lifecycle.ts +43 -0
- package/packages/workflow/package.json +3 -2
- package/packages/autocomplete/index.ts +0 -7
- package/packages/autocomplete/src/__tests__/command-registry.audit.test.ts +0 -120
- package/packages/autocomplete/src/__tests__/provider.sorting.test.ts +0 -423
- package/packages/compactor/src/compaction/recall-scope.ts +0 -32
- package/packages/compactor/src/compaction/sections.ts +0 -15
- package/packages/compactor/src/display/bash-display.ts +0 -28
- package/packages/compactor/src/display/diff-presentation.ts +0 -25
- package/packages/compactor/src/display/pending-diff-preview.ts +0 -51
- package/packages/compactor/src/display/user-message-box.ts +0 -16
- package/packages/compactor/src/session/analytics.ts +0 -205
- package/packages/image/index.ts +0 -1
- package/packages/input-shortcuts/index.ts +0 -5
- package/packages/input-shortcuts/src/status.ts +0 -35
- package/packages/kanboard/tui/kanboard-overlay.ts +0 -300
- package/packages/mcp/index.ts +0 -1
- package/packages/memory/search.ts +0 -134
- package/packages/milestone/coexist.ts +0 -114
- package/packages/notify/src/__tests__/ask-user-prompt-message.test.ts +0 -159
- package/packages/notify/src/__tests__/event-bus.test.ts +0 -119
- package/packages/notify/src/__tests__/permission-prompt-message.test.ts +0 -172
- package/packages/subagents/src/__tests__/badge-generation.test.ts +0 -315
- package/packages/subagents/src/__tests__/config.test.ts +0 -240
- package/packages/subagents/src/__tests__/esc-propagation.test.ts +0 -162
- package/packages/subagents/src/__tests__/file-lock.test.ts +0 -244
- package/packages/subagents/src/__tests__/shutdown-stale-ctx.test.ts +0 -185
- package/packages/subagents/src/__tests__/workflow-integration.test.ts +0 -334
- package/packages/subagents/src/prompts.ts +0 -39
- package/packages/utility/index.ts +0 -1
- package/packages/web-api/src/tui/progress.ts +0 -168
- package/packages/web-api/src/tui/result.ts +0 -173
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import * as fs from "node:fs";
|
|
9
9
|
import * as path from "node:path";
|
|
10
10
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
11
|
-
import { MILESTONE_DIRS, safeMtimeMs, tryRead } from "@pi-unipi/core";
|
|
11
|
+
import { MILESTONE_DIRS, UNIPI_EVENTS, safeMtimeMs, tryRead } from "@pi-unipi/core";
|
|
12
12
|
import { parseMilestones, getProgressSummary, updateItemStatus } from "./milestone.js";
|
|
13
13
|
|
|
14
14
|
/** Track when the session started for diffing modified files */
|
|
@@ -167,19 +167,7 @@ export function registerSessionEndHook(pi: ExtensionAPI): void {
|
|
|
167
167
|
}
|
|
168
168
|
});
|
|
169
169
|
|
|
170
|
-
|
|
171
|
-
pi.on("input", (event) => {
|
|
172
|
-
// Check if this is a unipi event emission for WORKFLOW_END
|
|
173
|
-
// The input event fires for tool calls; we need to detect when
|
|
174
|
-
// the workflow ends. We'll use the events system instead.
|
|
175
|
-
return undefined;
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
// Use tool_result to detect workflow end
|
|
179
|
-
// Actually, we should listen for the UNIPI_EVENTS.WORKFLOW_END via pi.events
|
|
180
|
-
// But the ExtensionAPI doesn't expose pi.events.on() directly.
|
|
181
|
-
// Instead, we'll hook into session_shutdown to do a final sync.
|
|
182
|
-
pi.on("session_shutdown", () => {
|
|
170
|
+
const syncModifiedDocs = () => {
|
|
183
171
|
const cwd = process.cwd();
|
|
184
172
|
const milestonesPath = path.join(cwd, MILESTONE_DIRS.MILESTONES);
|
|
185
173
|
|
|
@@ -204,5 +192,11 @@ export function registerSessionEndHook(pi: ExtensionAPI): void {
|
|
|
204
192
|
updateItemStatus(milestonesPath, phase, text, true);
|
|
205
193
|
}
|
|
206
194
|
}
|
|
207
|
-
}
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
// Workflow emits this once its follow-up agent loop drains.
|
|
198
|
+
pi.events.on(UNIPI_EVENTS.WORKFLOW_END, syncModifiedDocs);
|
|
199
|
+
|
|
200
|
+
// Final fallback for changes made outside a workflow or before event wiring.
|
|
201
|
+
pi.on("session_shutdown", syncModifiedDocs);
|
|
208
202
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/milestone",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.1",
|
|
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",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"access": "public"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@pi-unipi/core": "2.4.
|
|
32
|
+
"@pi-unipi/core": "2.4.1"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
35
|
"@earendil-works/pi-coding-agent": "^0.80.0",
|
|
@@ -47,6 +47,8 @@ notify_user({
|
|
|
47
47
|
})
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
+
An explicit semantic priority overrides configured urgency for that dispatch on platforms that support it: `low`/`normal`/`high` map to Gotify `2`/`5`/`8` and ntfy `2`/`3`/`5`. Native and Telegram have no priority input and ignore it. When omitted, Gotify and ntfy retain their configured numeric priorities.
|
|
51
|
+
|
|
50
52
|
## Platforms
|
|
51
53
|
|
|
52
54
|
### Native OS
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
9
9
|
import { UNIPI_EVENTS, emitEvent } from "@pi-unipi/core";
|
|
10
|
-
import type { NotifyConfig, NotifyPlatform, NotifyDispatchResult } from "./types.js";
|
|
10
|
+
import type { NotifyConfig, NotifyPlatform, NotifyDispatchResult, NotifyPriority } from "./types.js";
|
|
11
11
|
import { loadNtfyConfig } from "./ntfy-config.js";
|
|
12
12
|
import { sendNativeNotification, SuppressedError } from "./platforms/native.js";
|
|
13
13
|
import { sendGotifyNotification } from "./platforms/gotify.js";
|
|
@@ -170,7 +170,8 @@ export async function dispatchNotification(
|
|
|
170
170
|
eventPlatforms: NotifyPlatform[],
|
|
171
171
|
eventType: string,
|
|
172
172
|
config: NotifyConfig,
|
|
173
|
-
cwd: string
|
|
173
|
+
cwd: string,
|
|
174
|
+
priority?: NotifyPriority,
|
|
174
175
|
): Promise<NotifyDispatchResult> {
|
|
175
176
|
// Resolve ntfy config from project/global ntfy.json
|
|
176
177
|
const ntfyConfig = loadNtfyConfig(cwd);
|
|
@@ -194,8 +195,8 @@ export async function dispatchNotification(
|
|
|
194
195
|
const results = await Promise.all(
|
|
195
196
|
enabledPlatforms.map(async (platform) => {
|
|
196
197
|
try {
|
|
197
|
-
await sendToPlatform(platform, title, message, config, cwd);
|
|
198
|
-
return { platform, success: true };
|
|
198
|
+
const effectivePriority = await sendToPlatform(platform, title, message, config, cwd, priority);
|
|
199
|
+
return { platform, success: true, ...(effectivePriority === undefined ? {} : { priority: effectivePriority }) };
|
|
199
200
|
} catch (err) {
|
|
200
201
|
// SuppressedError is intentional, not a failure
|
|
201
202
|
if (err instanceof SuppressedError) {
|
|
@@ -230,20 +231,29 @@ export async function dispatchNotification(
|
|
|
230
231
|
}
|
|
231
232
|
|
|
232
233
|
/** Send to a single platform */
|
|
234
|
+
export function mapNotifyPriority(platform: "gotify" | "ntfy", priority: NotifyPriority): number {
|
|
235
|
+
const mappings = {
|
|
236
|
+
gotify: { low: 2, normal: 5, high: 8 },
|
|
237
|
+
ntfy: { low: 2, normal: 3, high: 5 },
|
|
238
|
+
} as const;
|
|
239
|
+
return mappings[platform][priority];
|
|
240
|
+
}
|
|
241
|
+
|
|
233
242
|
async function sendToPlatform(
|
|
234
243
|
platform: NotifyPlatform,
|
|
235
244
|
title: string,
|
|
236
245
|
message: string,
|
|
237
246
|
config: NotifyConfig,
|
|
238
|
-
cwd: string
|
|
239
|
-
|
|
247
|
+
cwd: string,
|
|
248
|
+
priority?: NotifyPriority,
|
|
249
|
+
): Promise<number | undefined> {
|
|
240
250
|
switch (platform) {
|
|
241
251
|
case "native":
|
|
242
252
|
await sendNativeNotification(title, message, {
|
|
243
253
|
windowsAppId: config.native.windowsAppId,
|
|
244
254
|
suppressWhenFocused: config.native.suppressWhenFocused,
|
|
245
255
|
});
|
|
246
|
-
|
|
256
|
+
return undefined;
|
|
247
257
|
case "gotify":
|
|
248
258
|
if (!config.gotify.serverUrl || !config.gotify.appToken) {
|
|
249
259
|
throw new Error("Gotify: serverUrl and appToken are required");
|
|
@@ -253,9 +263,9 @@ async function sendToPlatform(
|
|
|
253
263
|
config.gotify.appToken,
|
|
254
264
|
title,
|
|
255
265
|
message,
|
|
256
|
-
config.gotify.priority
|
|
266
|
+
priority ? mapNotifyPriority("gotify", priority) : config.gotify.priority
|
|
257
267
|
);
|
|
258
|
-
|
|
268
|
+
return priority ? mapNotifyPriority("gotify", priority) : config.gotify.priority;
|
|
259
269
|
case "telegram":
|
|
260
270
|
if (!config.telegram.botToken || !config.telegram.chatId) {
|
|
261
271
|
throw new Error("Telegram: botToken and chatId are required");
|
|
@@ -266,10 +276,10 @@ async function sendToPlatform(
|
|
|
266
276
|
title,
|
|
267
277
|
message
|
|
268
278
|
);
|
|
269
|
-
|
|
279
|
+
return undefined;
|
|
270
280
|
case "ntfy": {
|
|
271
281
|
const ntfyConfig = loadNtfyConfig(cwd);
|
|
272
|
-
if (!ntfyConfig.enabled) return;
|
|
282
|
+
if (!ntfyConfig.enabled) return undefined;
|
|
273
283
|
if (!ntfyConfig.serverUrl || !ntfyConfig.topic) {
|
|
274
284
|
throw new Error("ntfy: serverUrl and topic are required");
|
|
275
285
|
}
|
|
@@ -278,10 +288,10 @@ async function sendToPlatform(
|
|
|
278
288
|
ntfyConfig.topic,
|
|
279
289
|
title,
|
|
280
290
|
message,
|
|
281
|
-
ntfyConfig.priority,
|
|
291
|
+
priority ? mapNotifyPriority("ntfy", priority) : ntfyConfig.priority,
|
|
282
292
|
ntfyConfig.token
|
|
283
293
|
);
|
|
284
|
-
|
|
294
|
+
return priority ? mapNotifyPriority("ntfy", priority) : ntfyConfig.priority;
|
|
285
295
|
}
|
|
286
296
|
}
|
|
287
297
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/notify",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.1",
|
|
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.4.
|
|
37
|
+
"@pi-unipi/core": "2.4.1",
|
|
38
38
|
"node-notifier": "^10.0.1"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
package/packages/notify/tools.ts
CHANGED
|
@@ -8,7 +8,8 @@ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-a
|
|
|
8
8
|
import { Type } from "typebox";
|
|
9
9
|
import { NOTIFY_TOOLS } from "@pi-unipi/core";
|
|
10
10
|
import { loadConfig } from "./settings.js";
|
|
11
|
-
import { dispatchNotification } from "./events.js";
|
|
11
|
+
import { dispatchNotification, mapNotifyPriority } from "./events.js";
|
|
12
|
+
import type { NotifyPriority } from "./types.js";
|
|
12
13
|
|
|
13
14
|
/** Schema for notify_user tool parameters */
|
|
14
15
|
const NotifyUserSchema = Type.Object({
|
|
@@ -19,7 +20,6 @@ const NotifyUserSchema = Type.Object({
|
|
|
19
20
|
priority: Type.Optional(
|
|
20
21
|
Type.String({
|
|
21
22
|
enum: ["low", "normal", "high"],
|
|
22
|
-
default: "normal",
|
|
23
23
|
description: "Priority level",
|
|
24
24
|
})
|
|
25
25
|
),
|
|
@@ -46,12 +46,12 @@ export function registerNotifyTools(pi: ExtensionAPI): void {
|
|
|
46
46
|
const {
|
|
47
47
|
message,
|
|
48
48
|
title,
|
|
49
|
-
priority
|
|
49
|
+
priority,
|
|
50
50
|
platforms,
|
|
51
51
|
} = params as {
|
|
52
52
|
message: string;
|
|
53
53
|
title?: string;
|
|
54
|
-
priority?:
|
|
54
|
+
priority?: NotifyPriority;
|
|
55
55
|
platforms?: Array<"native" | "gotify" | "telegram" | "ntfy">;
|
|
56
56
|
};
|
|
57
57
|
|
|
@@ -65,18 +65,27 @@ export function registerNotifyTools(pi: ExtensionAPI): void {
|
|
|
65
65
|
|
|
66
66
|
// Fire-and-forget: dispatch in background so the tool doesn't block the agent
|
|
67
67
|
const cwd = process.cwd();
|
|
68
|
-
dispatchNotification(
|
|
68
|
+
const dispatchPromise = dispatchNotification(
|
|
69
69
|
pi,
|
|
70
70
|
notifTitle,
|
|
71
71
|
message,
|
|
72
72
|
notifPlatforms,
|
|
73
73
|
"agent_tool",
|
|
74
74
|
config,
|
|
75
|
-
cwd
|
|
76
|
-
|
|
75
|
+
cwd,
|
|
76
|
+
priority,
|
|
77
|
+
);
|
|
78
|
+
void dispatchPromise.catch(() => {
|
|
77
79
|
// Silently ignore — background dispatch failure is non-blocking.
|
|
78
80
|
});
|
|
79
81
|
|
|
82
|
+
const priorityByPlatform = priority ? Object.fromEntries(
|
|
83
|
+
notifPlatforms.flatMap((platform) => {
|
|
84
|
+
if (platform === "gotify" || platform === "ntfy") return [[platform, mapNotifyPriority(platform, priority)]];
|
|
85
|
+
return [];
|
|
86
|
+
}),
|
|
87
|
+
) : undefined;
|
|
88
|
+
|
|
80
89
|
return {
|
|
81
90
|
content: [
|
|
82
91
|
{
|
|
@@ -84,7 +93,7 @@ export function registerNotifyTools(pi: ExtensionAPI): void {
|
|
|
84
93
|
text: `Notification sending to ${notifPlatforms.length} platform(s): ${notifPlatforms.join(", ")}`,
|
|
85
94
|
},
|
|
86
95
|
],
|
|
87
|
-
details: { platforms: notifPlatforms },
|
|
96
|
+
details: { platforms: notifPlatforms, priority, priorityByPlatform },
|
|
88
97
|
};
|
|
89
98
|
},
|
|
90
99
|
});
|
package/packages/notify/types.ts
CHANGED
|
@@ -90,13 +90,15 @@ export interface NotifyConfig {
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
/** Parameters for the notify_user agent tool */
|
|
93
|
+
export type NotifyPriority = "low" | "normal" | "high";
|
|
94
|
+
|
|
93
95
|
export interface NotifyUserParams {
|
|
94
96
|
/** Notification message body */
|
|
95
97
|
message: string;
|
|
96
98
|
/** Notification title (default: "Pi Notification") */
|
|
97
99
|
title?: string;
|
|
98
100
|
/** Priority level */
|
|
99
|
-
priority?:
|
|
101
|
+
priority?: NotifyPriority;
|
|
100
102
|
/** Override platforms for this notification */
|
|
101
103
|
platforms?: NotifyPlatform[];
|
|
102
104
|
}
|
|
@@ -109,6 +111,8 @@ export interface NotifyResult {
|
|
|
109
111
|
success: boolean;
|
|
110
112
|
/** True when the notification was intentionally suppressed (e.g. window focused) */
|
|
111
113
|
suppressed?: boolean;
|
|
114
|
+
/** Effective numeric priority for platforms that support it. */
|
|
115
|
+
priority?: number;
|
|
112
116
|
/** Error message if failed */
|
|
113
117
|
error?: string;
|
|
114
118
|
}
|
package/packages/ralph/index.ts
CHANGED
|
@@ -145,8 +145,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
145
145
|
}
|
|
146
146
|
});
|
|
147
147
|
|
|
148
|
-
// Inject ralph instructions when loop is active
|
|
149
|
-
|
|
148
|
+
// Inject ralph instructions when loop is active.
|
|
149
|
+
// NOTE: injected as a hidden tail message (not the system prompt) so the
|
|
150
|
+
// cacheable prefix (system prompt + prior history) stays byte-stable across
|
|
151
|
+
// turns. The iteration number changes every turn and must never touch the prefix.
|
|
152
|
+
pi.on("before_agent_start", async (_event, ctx) => {
|
|
150
153
|
const mgr = getManager(ctx, pi);
|
|
151
154
|
const currentLoop = mgr.getCurrentLoop();
|
|
152
155
|
if (!currentLoop) return;
|
|
@@ -165,9 +168,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
165
168
|
instructions += `- Otherwise, call ralph_done tool to proceed to next iteration`;
|
|
166
169
|
|
|
167
170
|
return {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
+
message: {
|
|
172
|
+
customType: "unipi-ralph-loop-reminder",
|
|
173
|
+
content: `[RALPH LOOP - ${state.name} - Iteration ${iterStr}]\n\n${instructions}`,
|
|
174
|
+
display: false,
|
|
175
|
+
},
|
|
171
176
|
};
|
|
172
177
|
});
|
|
173
178
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/ralph",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.2",
|
|
4
4
|
"description": "Long-running iterative development loops for Pi coding agent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -27,8 +27,8 @@
|
|
|
27
27
|
"access": "public"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@pi-unipi/core": "2.4.
|
|
31
|
-
"@pi-unipi/info-screen": "2.4.
|
|
30
|
+
"@pi-unipi/core": "2.4.1",
|
|
31
|
+
"@pi-unipi/info-screen": "2.4.1"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
34
|
"@earendil-works/pi-ai": "^0.80.0",
|
|
@@ -374,6 +374,8 @@ export class RalphLoopManager {
|
|
|
374
374
|
return null;
|
|
375
375
|
}
|
|
376
376
|
|
|
377
|
+
this.emitIterationDone(state);
|
|
378
|
+
|
|
377
379
|
const needsReflection =
|
|
378
380
|
state.reflectEvery > 0 && (state.iteration - 1) % state.reflectEvery === 0;
|
|
379
381
|
if (needsReflection) state.lastReflectionAt = state.iteration;
|
|
@@ -384,6 +386,14 @@ export class RalphLoopManager {
|
|
|
384
386
|
return { state, needsReflection };
|
|
385
387
|
}
|
|
386
388
|
|
|
389
|
+
private emitIterationDone(state: LoopState): void {
|
|
390
|
+
this.emitFn(UNIPI_EVENTS.RALPH_ITERATION_DONE, {
|
|
391
|
+
name: state.name,
|
|
392
|
+
iteration: state.iteration - 1,
|
|
393
|
+
nextIteration: state.iteration,
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
|
|
387
397
|
archiveLoop(name: string): boolean {
|
|
388
398
|
const state = this.loadState(name);
|
|
389
399
|
if (!state) return false;
|
|
@@ -99,7 +99,9 @@ Create markdown files defining agent behavior:
|
|
|
99
99
|
|---------|---------|--------------|
|
|
100
100
|
| `enabled` | true | Enable/disable subagents |
|
|
101
101
|
| `maxConcurrent` | 3 | Max agents running at once |
|
|
102
|
-
| `types.{name}.enabled` | true |
|
|
102
|
+
| `types.{name}.enabled` | true | Disable built-in or custom agent types. Disabled types are omitted from tool guidance and rejected before queueing. |
|
|
103
|
+
|
|
104
|
+
For custom agents, JSON configuration and Markdown frontmatter are both enforced: the type can run only when neither source sets `enabled: false`. Configuration is loaded when the extension starts, so reload the session after changing enablement.
|
|
103
105
|
|
|
104
106
|
## License
|
|
105
107
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/subagents",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.1",
|
|
4
4
|
"description": "Subagents for UniPi — parallel execution, file locking, workflow integration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -12,14 +12,12 @@
|
|
|
12
12
|
}
|
|
13
13
|
},
|
|
14
14
|
"scripts": {
|
|
15
|
-
"build": "tsc",
|
|
15
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json && node scripts/verify-build.mjs",
|
|
16
|
+
"prepack": "npm run build",
|
|
16
17
|
"dev": "tsc --watch",
|
|
17
18
|
"test": "npx tsx --test src/__tests__/*.test.ts"
|
|
18
19
|
},
|
|
19
20
|
"dependencies": {
|
|
20
|
-
"@pi-unipi/core": "2.4.0",
|
|
21
|
-
"@pi-unipi/workflow": "2.4.0",
|
|
22
|
-
"@pi-unipi/info-screen": "2.4.0",
|
|
23
21
|
"@earendil-works/pi-agent-core": "^0.80.0"
|
|
24
22
|
},
|
|
25
23
|
"devDependencies": {
|
|
@@ -30,8 +28,14 @@
|
|
|
30
28
|
"@earendil-works/pi-tui": "^0.80.0",
|
|
31
29
|
"typebox": "^1.1.38"
|
|
32
30
|
},
|
|
31
|
+
"files": [
|
|
32
|
+
"dist/**",
|
|
33
|
+
"README.md"
|
|
34
|
+
],
|
|
33
35
|
"pi": {
|
|
34
|
-
"extensions": [
|
|
36
|
+
"extensions": [
|
|
37
|
+
"./dist/index.js"
|
|
38
|
+
],
|
|
35
39
|
"skills": [],
|
|
36
40
|
"prompts": [],
|
|
37
41
|
"themes": []
|
|
@@ -10,7 +10,7 @@ import type { Model } from "@earendil-works/pi-ai";
|
|
|
10
10
|
import type { AgentSession, ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
11
11
|
import { runAgent, type ToolActivity } from "./agent-runner.js";
|
|
12
12
|
import { resolveModel, type ModelRegistry } from "./model-resolver.js";
|
|
13
|
-
import type { AgentRecord, AgentConfig, AgentType, ThinkingLevel } from "./types.js";
|
|
13
|
+
import type { AgentRecord, AgentConfig, AgentType, ThinkingLevel, SubagentsConfig } from "./types.js";
|
|
14
14
|
import { BUILTIN_CONFIGS } from "./types.js";
|
|
15
15
|
import { loadCustomAgents } from "./custom-agents.js";
|
|
16
16
|
import { FileLock } from "./file-lock.js";
|
|
@@ -52,6 +52,7 @@ export class AgentManager {
|
|
|
52
52
|
private onStart?: OnAgentStart;
|
|
53
53
|
private maxConcurrent: number;
|
|
54
54
|
private customAgents: Map<string, AgentConfig>;
|
|
55
|
+
private typeSettings: SubagentsConfig["types"];
|
|
55
56
|
|
|
56
57
|
/** Per-file transparent locking for write agents. */
|
|
57
58
|
readonly fileLock = new FileLock();
|
|
@@ -61,11 +62,18 @@ export class AgentManager {
|
|
|
61
62
|
/** Number of currently running background agents. */
|
|
62
63
|
private runningBackground = 0;
|
|
63
64
|
|
|
64
|
-
constructor(
|
|
65
|
+
constructor(
|
|
66
|
+
onComplete?: OnAgentComplete,
|
|
67
|
+
maxConcurrent = DEFAULT_MAX_CONCURRENT,
|
|
68
|
+
onStart?: OnAgentStart,
|
|
69
|
+
typeSettings: SubagentsConfig["types"] = {},
|
|
70
|
+
agentsCwd = process.cwd(),
|
|
71
|
+
) {
|
|
65
72
|
this.onComplete = onComplete;
|
|
66
73
|
this.onStart = onStart;
|
|
67
74
|
this.maxConcurrent = maxConcurrent;
|
|
68
|
-
this.
|
|
75
|
+
this.typeSettings = typeSettings;
|
|
76
|
+
this.customAgents = loadCustomAgents(agentsCwd);
|
|
69
77
|
this.cleanupInterval = setInterval(() => this.cleanup(), 60_000);
|
|
70
78
|
}
|
|
71
79
|
|
|
@@ -74,6 +82,27 @@ export class AgentManager {
|
|
|
74
82
|
return this.customAgents.get(type) ?? BUILTIN_CONFIGS[type];
|
|
75
83
|
}
|
|
76
84
|
|
|
85
|
+
/** Public agent types known from built-ins, custom files, or JSON config. */
|
|
86
|
+
getKnownTypes(): string[] {
|
|
87
|
+
return [...new Set([
|
|
88
|
+
...Object.keys(BUILTIN_CONFIGS).filter((type) => type !== "name-gen"),
|
|
89
|
+
...this.customAgents.keys(),
|
|
90
|
+
...Object.keys(this.typeSettings),
|
|
91
|
+
])];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** A type is enabled only when both JSON config and agent frontmatter allow it. */
|
|
95
|
+
isTypeEnabled(type: AgentType): boolean {
|
|
96
|
+
const agentConfig = this.getAgentConfig(type);
|
|
97
|
+
return this.typeSettings[type]?.enabled !== false && agentConfig?.enabled !== false;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private assertTypeEnabled(type: AgentType): void {
|
|
101
|
+
if (!this.isTypeEnabled(type)) {
|
|
102
|
+
throw new Error(`Agent type "${type}" is disabled by configuration.`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
77
106
|
setMaxConcurrent(n: number) {
|
|
78
107
|
this.maxConcurrent = Math.max(1, n);
|
|
79
108
|
this.drainQueue();
|
|
@@ -93,6 +122,10 @@ export class AgentManager {
|
|
|
93
122
|
prompt: string,
|
|
94
123
|
options: SpawnOptions,
|
|
95
124
|
): string {
|
|
125
|
+
// Reject before allocating a record or queue entry. This applies equally to
|
|
126
|
+
// foreground and background spawns and avoids ghost disabled agents.
|
|
127
|
+
this.assertTypeEnabled(type);
|
|
128
|
+
|
|
96
129
|
const id = randomUUID().slice(0, 17);
|
|
97
130
|
const abortController = new AbortController();
|
|
98
131
|
const record: AgentRecord = {
|
|
@@ -125,10 +158,13 @@ export class AgentManager {
|
|
|
125
158
|
if (options.isBackground) this.runningBackground++;
|
|
126
159
|
this.onStart?.(record);
|
|
127
160
|
|
|
128
|
-
|
|
161
|
+
const agentConfig = this.getAgentConfig(type);
|
|
162
|
+
|
|
163
|
+
// Resolve model: explicit input > per-agent config > parent model.
|
|
129
164
|
let model = options.model;
|
|
130
|
-
|
|
131
|
-
|
|
165
|
+
const modelInput = options.modelInput ?? agentConfig?.model;
|
|
166
|
+
if (!model && modelInput && options.modelRegistry) {
|
|
167
|
+
const resolved = resolveModel(modelInput, options.modelRegistry);
|
|
132
168
|
if (typeof resolved === "string") {
|
|
133
169
|
// Error message — return early with error
|
|
134
170
|
record.status = "error";
|
|
@@ -143,15 +179,14 @@ export class AgentManager {
|
|
|
143
179
|
model = resolved;
|
|
144
180
|
}
|
|
145
181
|
|
|
146
|
-
const agentConfig = this.getAgentConfig(type);
|
|
147
182
|
const promise = runAgent(ctx, type, prompt, {
|
|
148
183
|
pi,
|
|
149
184
|
model,
|
|
150
185
|
agentConfig,
|
|
151
|
-
maxTurns: options.maxTurns,
|
|
152
|
-
isolated: options.isolated,
|
|
153
|
-
inheritContext: options.inheritContext,
|
|
154
|
-
thinkingLevel: options.thinkingLevel,
|
|
186
|
+
maxTurns: options.maxTurns ?? agentConfig?.maxTurns,
|
|
187
|
+
isolated: options.isolated ?? agentConfig?.isolated,
|
|
188
|
+
inheritContext: options.inheritContext ?? agentConfig?.inheritContext,
|
|
189
|
+
thinkingLevel: options.thinkingLevel ?? agentConfig?.thinking,
|
|
155
190
|
signal: record.abortController!.signal,
|
|
156
191
|
onToolActivity: (activity) => {
|
|
157
192
|
if (activity.type === "end") record.toolUses++;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
export const MODULES = { SUBAGENTS: "subagents" } as const;
|
|
4
|
+
|
|
5
|
+
export const UNIPI_EVENTS = {
|
|
6
|
+
MODULE_READY: "unipi:module:ready",
|
|
7
|
+
BADGE_GENERATE_REQUEST: "unipi:badge:generate:request",
|
|
8
|
+
} as const;
|
|
9
|
+
|
|
10
|
+
export interface UnipiBadgeGenerateRequestEvent {
|
|
11
|
+
conversationSummary?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function emitEvent(
|
|
15
|
+
pi: Pick<ExtensionAPI, "events">,
|
|
16
|
+
eventName: string,
|
|
17
|
+
payload: unknown,
|
|
18
|
+
): boolean {
|
|
19
|
+
try {
|
|
20
|
+
pi.events.emit(eventName, payload);
|
|
21
|
+
return true;
|
|
22
|
+
} catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function withHerdrBlocked<T>(
|
|
28
|
+
pi: Pick<ExtensionAPI, "events">,
|
|
29
|
+
label: string,
|
|
30
|
+
fn: () => Promise<T>,
|
|
31
|
+
): Promise<T> {
|
|
32
|
+
emitEvent(pi, "herdr:blocked", { active: true, label });
|
|
33
|
+
try {
|
|
34
|
+
return await fn();
|
|
35
|
+
} finally {
|
|
36
|
+
emitEvent(pi, "herdr:blocked", { active: false, label });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -12,7 +12,7 @@ import { Type } from "typebox";
|
|
|
12
12
|
import { existsSync, readdirSync } from "node:fs";
|
|
13
13
|
import { join } from "node:path";
|
|
14
14
|
import { homedir } from "node:os";
|
|
15
|
-
import { emitEvent, MODULES, UNIPI_EVENTS, withHerdrBlocked, type UnipiBadgeGenerateRequestEvent } from "
|
|
15
|
+
import { emitEvent, MODULES, UNIPI_EVENTS, withHerdrBlocked, type UnipiBadgeGenerateRequestEvent } from "./core-compat.js";
|
|
16
16
|
import { AgentManager } from "./agent-manager.js";
|
|
17
17
|
import { initConfig } from "./config.js";
|
|
18
18
|
import { type AgentActivity, type NotificationDetails, BUILTIN_TYPES } from "./types.js";
|
|
@@ -246,6 +246,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
246
246
|
description: record.description,
|
|
247
247
|
});
|
|
248
248
|
},
|
|
249
|
+
config.types,
|
|
249
250
|
);
|
|
250
251
|
|
|
251
252
|
// Build notification details for the message renderer
|
|
@@ -494,6 +495,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
494
495
|
// ---- Agent tool ----
|
|
495
496
|
|
|
496
497
|
const builtinTypes = BUILTIN_TYPES.join(", ");
|
|
498
|
+
const enabledTypes = manager.getKnownTypes().filter((type) => manager.isTypeEnabled(type));
|
|
499
|
+
const availableTypes = enabledTypes.join(", ") || "none";
|
|
497
500
|
|
|
498
501
|
pi.registerTool(
|
|
499
502
|
defineTool({
|
|
@@ -501,7 +504,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
501
504
|
label: "Spawn Helper",
|
|
502
505
|
description: `Launch a sub-agent for parallel work.
|
|
503
506
|
|
|
504
|
-
Available agent types: ${
|
|
507
|
+
Available agent types: ${availableTypes}
|
|
505
508
|
Custom types can be defined in:
|
|
506
509
|
- ~/.unipi/config/agents/<name>.md (global)
|
|
507
510
|
- <workspace>/.unipi/config/agents/<name>.md (project)
|
|
@@ -514,7 +517,7 @@ Guidelines:
|
|
|
514
517
|
- Agents inherit the parent model by default`,
|
|
515
518
|
parameters: Type.Object({
|
|
516
519
|
type: Type.String({
|
|
517
|
-
description: `
|
|
520
|
+
description: `Enabled agent type: ${availableTypes}`,
|
|
518
521
|
}),
|
|
519
522
|
prompt: Type.String({
|
|
520
523
|
description: "The task for the agent to perform.",
|
|
@@ -51,13 +51,6 @@ function formatTurns(turn: number, max?: number): string {
|
|
|
51
51
|
return max != null ? `⟳${turn}≤${max}` : `⟳${turn}`;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
/** Format token count compactly: "33.8k token", "1.2M token". */
|
|
55
|
-
function formatTokens(count: number): string {
|
|
56
|
-
if (count >= 1_000_000) return `${(count / 1_000_000).toFixed(1)}M token`;
|
|
57
|
-
if (count >= 1_000) return `${(count / 1_000).toFixed(1)}k token`;
|
|
58
|
-
return `${count} token`;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
54
|
/**
|
|
62
55
|
* Build a human-readable activity string from currently-running tools.
|
|
63
56
|
* Groups by tool type with counts: "reading 3 files, searching 2 patterns".
|