@tintinweb/pi-subagents 0.4.11 → 0.5.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 +14 -9
- package/README.md +23 -8
- package/dist/agent-runner.js +1 -1
- package/dist/cross-extension-rpc.d.ts +19 -3
- package/dist/cross-extension-rpc.js +40 -19
- package/dist/index.js +4 -3
- package/dist/ui/conversation-viewer.js +1 -1
- package/dist/ui/conversation-viewer.test.js +1 -1
- package/package.json +6 -6
- package/src/agent-runner.ts +1 -1
- package/src/cross-extension-rpc.ts +57 -23
- package/src/index.ts +4 -3
- package/src/ui/conversation-viewer.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,22 +5,26 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
-
## [
|
|
8
|
+
## [Unreleased]
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
- **Stale dist in published package** — added `prepublishOnly` hook to build fresh `dist/` on every `npm publish`.
|
|
12
|
-
|
|
13
|
-
## [0.4.10] - 2026-03-18
|
|
14
|
-
|
|
15
|
-
### Changed
|
|
16
|
-
- **Default max turns is now unlimited** — subagents no longer have a 50-turn default cap. The default is unlimited (no turn limit), matching Claude Code's main loop behavior. Users can still set explicit limits per-agent via `max_turns` frontmatter or the Agent tool parameter, or globally via `/agents` → Settings (`0` = unlimited).
|
|
17
|
-
- **Live turn counter** — all agents now show a live turn count in the widget, inline result, and completion notification. With a turn limit: `⟳5≤30` (5 of 30 turns). Without: `⟳5`. Updates in real time as turns progress.
|
|
10
|
+
## [0.5.0] - 2026-03-22
|
|
18
11
|
|
|
19
12
|
### Added
|
|
13
|
+
- **RPC stop handler** — new `subagents:rpc:stop` event bus RPC allows other extensions to stop running subagents by agent ID. Returns structured error ("Agent not found") on failure.
|
|
14
|
+
- **`abort` in `SpawnCapable` interface** — cross-extension RPC consumers can now stop agents, not just spawn them.
|
|
15
|
+
- **Live turn counter** — all agents now show a live turn count in the widget, inline result, and completion notification. With a turn limit: `⟳5≤30` (5 of 30 turns). Without: `⟳5`. Updates in real time as turns progress via `onTurnEnd` callback.
|
|
20
16
|
- **Biome linting** — added [Biome](https://biomejs.dev/) for correctness linting (unused imports, suspicious patterns). Style rules disabled. Run `npm run lint` to check, `npm run lint:fix` to auto-fix.
|
|
21
17
|
- **CI workflow** — GitHub Actions runs lint, typecheck, and tests on push to master and PRs.
|
|
18
|
+
- **Auto-trigger parent turn on background completion** — background agent completion notifications now use `triggerTurn: true`, automatically prompting the parent agent to process results instead of waiting for user input.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
- **Standardized RPC envelope** — cross-extension RPC handlers (`ping`, `spawn`, `stop`) now use a `handleRpc` wrapper that emits structured envelopes (`{ success: true, data }` / `{ success: false, error }`), matching pi-mono's `RpcResponse` convention.
|
|
22
|
+
- **Protocol versioning via ping** — ping reply now includes `{ version: PROTOCOL_VERSION }` (currently v2). Callers can detect version mismatches and warn users to update.
|
|
23
|
+
- **Default max turns is now unlimited** — subagents no longer have a 50-turn default cap. The default is unlimited (no turn limit), matching Claude Code's main loop behavior. Users can still set explicit limits per-agent via `max_turns` frontmatter or the Agent tool parameter, or globally via `/agents` → Settings (`0` = unlimited).
|
|
24
|
+
- **Stale dist in published package** — added `prepublishOnly` hook to build fresh `dist/` on every `npm publish`.
|
|
22
25
|
|
|
23
26
|
### Fixed
|
|
27
|
+
- **Tool name display** — `getAgentConversation` now reads `ToolCall.name` (the correct property) instead of `toolName`, resolving `[Tool: unknown]` in conversation viewer and verbose output.
|
|
24
28
|
- **Env test CI failure** — `detectEnv` test assumed a branch name exists, but CI checks out detached HEAD. Split into separate tests for repo detection and branch detection with a controlled temp repo.
|
|
25
29
|
|
|
26
30
|
## [0.4.9] - 2026-03-18
|
|
@@ -325,6 +329,7 @@ Initial release.
|
|
|
325
329
|
- **Thinking level** — per-agent extended thinking control
|
|
326
330
|
- **`/agent` and `/agents` commands**
|
|
327
331
|
|
|
332
|
+
[0.5.0]: https://github.com/tintinweb/pi-subagents/compare/v0.4.9...v0.5.0
|
|
328
333
|
[0.4.9]: https://github.com/tintinweb/pi-subagents/compare/v0.4.8...v0.4.9
|
|
329
334
|
[0.4.8]: https://github.com/tintinweb/pi-subagents/compare/v0.4.7...v0.4.8
|
|
330
335
|
[0.4.7]: https://github.com/tintinweb/pi-subagents/compare/v0.4.6...v0.4.7
|
package/README.md
CHANGED
|
@@ -29,7 +29,7 @@ https://github.com/user-attachments/assets/8685261b-9338-4fea-8dfe-1c590d5df543
|
|
|
29
29
|
- **Tool denylist** — block specific tools via `disallowed_tools` frontmatter
|
|
30
30
|
- **Styled completion notifications** — background agent results render as themed, compact notification boxes (icon, stats, result preview) instead of raw XML. Expandable to show full output. Group completions render each agent individually
|
|
31
31
|
- **Event bus** — lifecycle events (`subagents:created`, `started`, `completed`, `failed`, `steered`) emitted via `pi.events`, enabling other extensions to react to sub-agent activity
|
|
32
|
-
- **Cross-extension RPC** — other pi extensions can spawn subagents via the `pi.events` event bus (`subagents:rpc:ping`, `subagents:rpc:spawn`). Emits `subagents:ready` on load
|
|
32
|
+
- **Cross-extension RPC** — other pi extensions can spawn and stop subagents via the `pi.events` event bus (`subagents:rpc:ping`, `subagents:rpc:spawn`, `subagents:rpc:stop`). Standardized reply envelopes with protocol versioning. Emits `subagents:ready` on load
|
|
33
33
|
|
|
34
34
|
## Install
|
|
35
35
|
|
|
@@ -289,7 +289,9 @@ Agent lifecycle events are emitted via `pi.events.emit()` so other extensions ca
|
|
|
289
289
|
|
|
290
290
|
## Cross-Extension RPC
|
|
291
291
|
|
|
292
|
-
Other pi extensions can spawn subagents programmatically via the `pi.events` event bus, without importing this package directly.
|
|
292
|
+
Other pi extensions can spawn and stop subagents programmatically via the `pi.events` event bus, without importing this package directly.
|
|
293
|
+
|
|
294
|
+
All RPC replies use a standardized envelope: `{ success: true, data?: T }` on success, `{ success: false, error: string }` on failure.
|
|
293
295
|
|
|
294
296
|
### Discovery
|
|
295
297
|
|
|
@@ -297,19 +299,19 @@ Listen for `subagents:ready` to know when RPC handlers are available:
|
|
|
297
299
|
|
|
298
300
|
```typescript
|
|
299
301
|
pi.events.on("subagents:ready", () => {
|
|
300
|
-
// RPC handlers are registered — safe to call ping/spawn
|
|
302
|
+
// RPC handlers are registered — safe to call ping/spawn/stop
|
|
301
303
|
});
|
|
302
304
|
```
|
|
303
305
|
|
|
304
306
|
### Ping
|
|
305
307
|
|
|
306
|
-
Check if the subagents extension is loaded:
|
|
308
|
+
Check if the subagents extension is loaded and get the protocol version:
|
|
307
309
|
|
|
308
310
|
```typescript
|
|
309
311
|
const requestId = crypto.randomUUID();
|
|
310
|
-
const unsub = pi.events.on(`subagents:rpc:ping:reply:${requestId}`, () => {
|
|
312
|
+
const unsub = pi.events.on(`subagents:rpc:ping:reply:${requestId}`, (reply) => {
|
|
311
313
|
unsub();
|
|
312
|
-
|
|
314
|
+
if (reply.success) console.log("Protocol version:", reply.data.version);
|
|
313
315
|
});
|
|
314
316
|
pi.events.emit("subagents:rpc:ping", { requestId });
|
|
315
317
|
```
|
|
@@ -322,10 +324,10 @@ Spawn a subagent and receive its ID:
|
|
|
322
324
|
const requestId = crypto.randomUUID();
|
|
323
325
|
const unsub = pi.events.on(`subagents:rpc:spawn:reply:${requestId}`, (reply) => {
|
|
324
326
|
unsub();
|
|
325
|
-
if (reply.
|
|
327
|
+
if (!reply.success) {
|
|
326
328
|
console.error("Spawn failed:", reply.error);
|
|
327
329
|
} else {
|
|
328
|
-
console.log("Agent ID:", reply.id);
|
|
330
|
+
console.log("Agent ID:", reply.data.id);
|
|
329
331
|
}
|
|
330
332
|
});
|
|
331
333
|
pi.events.emit("subagents:rpc:spawn", {
|
|
@@ -336,6 +338,19 @@ pi.events.emit("subagents:rpc:spawn", {
|
|
|
336
338
|
});
|
|
337
339
|
```
|
|
338
340
|
|
|
341
|
+
### Stop
|
|
342
|
+
|
|
343
|
+
Stop a running agent by ID:
|
|
344
|
+
|
|
345
|
+
```typescript
|
|
346
|
+
const requestId = crypto.randomUUID();
|
|
347
|
+
const unsub = pi.events.on(`subagents:rpc:stop:reply:${requestId}`, (reply) => {
|
|
348
|
+
unsub();
|
|
349
|
+
if (!reply.success) console.error("Stop failed:", reply.error);
|
|
350
|
+
});
|
|
351
|
+
pi.events.emit("subagents:rpc:stop", { requestId, agentId: "agent-id-here" });
|
|
352
|
+
```
|
|
353
|
+
|
|
339
354
|
Reply channels are scoped per `requestId`, so concurrent requests don't interfere.
|
|
340
355
|
|
|
341
356
|
## Persistent Agent Memory
|
package/dist/agent-runner.js
CHANGED
|
@@ -302,7 +302,7 @@ export function getAgentConversation(session) {
|
|
|
302
302
|
if (c.type === "text" && c.text)
|
|
303
303
|
textParts.push(c.text);
|
|
304
304
|
else if (c.type === "toolCall")
|
|
305
|
-
toolCalls.push(` Tool: ${c.toolName ?? "unknown"}`);
|
|
305
|
+
toolCalls.push(` Tool: ${c.name ?? c.toolName ?? "unknown"}`);
|
|
306
306
|
}
|
|
307
307
|
if (textParts.length > 0)
|
|
308
308
|
parts.push(`[Assistant]: ${textParts.join("\n")}`);
|
|
@@ -1,17 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Cross-extension RPC handlers for the subagents extension.
|
|
3
3
|
*
|
|
4
|
-
* Exposes ping and
|
|
4
|
+
* Exposes ping, spawn, and stop RPCs over the pi.events event bus,
|
|
5
5
|
* using per-request scoped reply channels.
|
|
6
|
+
*
|
|
7
|
+
* Reply envelope follows pi-mono convention:
|
|
8
|
+
* success → { success: true, data?: T }
|
|
9
|
+
* error → { success: false, error: string }
|
|
6
10
|
*/
|
|
7
11
|
/** Minimal event bus interface needed by the RPC handlers. */
|
|
8
12
|
export interface EventBus {
|
|
9
13
|
on(event: string, handler: (data: unknown) => void): () => void;
|
|
10
14
|
emit(event: string, data: unknown): void;
|
|
11
15
|
}
|
|
12
|
-
/**
|
|
16
|
+
/** RPC reply envelope — matches pi-mono's RpcResponse shape. */
|
|
17
|
+
export type RpcReply<T = void> = {
|
|
18
|
+
success: true;
|
|
19
|
+
data?: T;
|
|
20
|
+
} | {
|
|
21
|
+
success: false;
|
|
22
|
+
error: string;
|
|
23
|
+
};
|
|
24
|
+
/** RPC protocol version — bumped when the envelope or method contracts change. */
|
|
25
|
+
export declare const PROTOCOL_VERSION = 2;
|
|
26
|
+
/** Minimal AgentManager interface needed by the spawn/stop RPCs. */
|
|
13
27
|
export interface SpawnCapable {
|
|
14
28
|
spawn(pi: unknown, ctx: unknown, type: string, prompt: string, options: any): string;
|
|
29
|
+
abort(id: string): boolean;
|
|
15
30
|
}
|
|
16
31
|
export interface RpcDeps {
|
|
17
32
|
events: EventBus;
|
|
@@ -22,9 +37,10 @@ export interface RpcDeps {
|
|
|
22
37
|
export interface RpcHandle {
|
|
23
38
|
unsubPing: () => void;
|
|
24
39
|
unsubSpawn: () => void;
|
|
40
|
+
unsubStop: () => void;
|
|
25
41
|
}
|
|
26
42
|
/**
|
|
27
|
-
* Register ping and
|
|
43
|
+
* Register ping, spawn, and stop RPC handlers on the event bus.
|
|
28
44
|
* Returns unsub functions for cleanup.
|
|
29
45
|
*/
|
|
30
46
|
export declare function registerRpcHandlers(deps: RpcDeps): RpcHandle;
|
|
@@ -1,33 +1,54 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Cross-extension RPC handlers for the subagents extension.
|
|
3
3
|
*
|
|
4
|
-
* Exposes ping and
|
|
4
|
+
* Exposes ping, spawn, and stop RPCs over the pi.events event bus,
|
|
5
5
|
* using per-request scoped reply channels.
|
|
6
|
+
*
|
|
7
|
+
* Reply envelope follows pi-mono convention:
|
|
8
|
+
* success → { success: true, data?: T }
|
|
9
|
+
* error → { success: false, error: string }
|
|
6
10
|
*/
|
|
11
|
+
/** RPC protocol version — bumped when the envelope or method contracts change. */
|
|
12
|
+
export const PROTOCOL_VERSION = 2;
|
|
7
13
|
/**
|
|
8
|
-
*
|
|
14
|
+
* Wire a single RPC handler: listen on `channel`, run `fn(params)`,
|
|
15
|
+
* emit the reply envelope on `channel:reply:${requestId}`.
|
|
16
|
+
*/
|
|
17
|
+
function handleRpc(events, channel, fn) {
|
|
18
|
+
return events.on(channel, async (raw) => {
|
|
19
|
+
const params = raw;
|
|
20
|
+
try {
|
|
21
|
+
const data = await fn(params);
|
|
22
|
+
const reply = { success: true };
|
|
23
|
+
if (data !== undefined)
|
|
24
|
+
reply.data = data;
|
|
25
|
+
events.emit(`${channel}:reply:${params.requestId}`, reply);
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
events.emit(`${channel}:reply:${params.requestId}`, {
|
|
29
|
+
success: false, error: err?.message ?? String(err),
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Register ping, spawn, and stop RPC handlers on the event bus.
|
|
9
36
|
* Returns unsub functions for cleanup.
|
|
10
37
|
*/
|
|
11
38
|
export function registerRpcHandlers(deps) {
|
|
12
39
|
const { events, pi, getCtx, manager } = deps;
|
|
13
|
-
const unsubPing = events
|
|
14
|
-
|
|
15
|
-
events.emit(`subagents:rpc:ping:reply:${requestId}`, {});
|
|
40
|
+
const unsubPing = handleRpc(events, "subagents:rpc:ping", () => {
|
|
41
|
+
return { version: PROTOCOL_VERSION };
|
|
16
42
|
});
|
|
17
|
-
const unsubSpawn = events
|
|
18
|
-
const { requestId, type, prompt, options } = raw;
|
|
43
|
+
const unsubSpawn = handleRpc(events, "subagents:rpc:spawn", ({ type, prompt, options }) => {
|
|
19
44
|
const ctx = getCtx();
|
|
20
|
-
if (!ctx)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
28
|
-
catch (err) {
|
|
29
|
-
events.emit(`subagents:rpc:spawn:reply:${requestId}`, { error: err.message });
|
|
30
|
-
}
|
|
45
|
+
if (!ctx)
|
|
46
|
+
throw new Error("No active session");
|
|
47
|
+
return { id: manager.spawn(pi, ctx, type, prompt, options ?? {}) };
|
|
48
|
+
});
|
|
49
|
+
const unsubStop = handleRpc(events, "subagents:rpc:stop", ({ agentId }) => {
|
|
50
|
+
if (!manager.abort(agentId))
|
|
51
|
+
throw new Error("Agent not found");
|
|
31
52
|
});
|
|
32
|
-
return { unsubPing, unsubSpawn };
|
|
53
|
+
return { unsubPing, unsubSpawn, unsubStop };
|
|
33
54
|
}
|
package/dist/index.js
CHANGED
|
@@ -254,7 +254,7 @@ export default function (pi) {
|
|
|
254
254
|
content: notification + footer,
|
|
255
255
|
display: true,
|
|
256
256
|
details: buildNotificationDetails(record, 500, agentActivity.get(record.id)),
|
|
257
|
-
}, { deliverAs: "followUp" });
|
|
257
|
+
}, { deliverAs: "followUp", triggerTurn: true });
|
|
258
258
|
}
|
|
259
259
|
function sendIndividualNudge(record) {
|
|
260
260
|
agentActivity.delete(record.id);
|
|
@@ -290,7 +290,7 @@ export default function (pi) {
|
|
|
290
290
|
content: `Background agent group completed: ${label}\n\n${notifications}\n\nUse get_subagent_result for full output.`,
|
|
291
291
|
display: true,
|
|
292
292
|
details,
|
|
293
|
-
}, { deliverAs: "followUp" });
|
|
293
|
+
}, { deliverAs: "followUp", triggerTurn: true });
|
|
294
294
|
});
|
|
295
295
|
widget.update();
|
|
296
296
|
}, 30_000);
|
|
@@ -383,7 +383,7 @@ export default function (pi) {
|
|
|
383
383
|
manager.clearCompleted(); // preserve existing behavior
|
|
384
384
|
});
|
|
385
385
|
pi.on("session_switch", () => { manager.clearCompleted(); });
|
|
386
|
-
const { unsubPing: unsubPingRpc, unsubSpawn: unsubSpawnRpc } = registerRpcHandlers({
|
|
386
|
+
const { unsubPing: unsubPingRpc, unsubSpawn: unsubSpawnRpc, unsubStop: unsubStopRpc } = registerRpcHandlers({
|
|
387
387
|
events: pi.events,
|
|
388
388
|
pi,
|
|
389
389
|
getCtx: () => currentCtx,
|
|
@@ -395,6 +395,7 @@ export default function (pi) {
|
|
|
395
395
|
// If the session is going down, there's nothing left to consume agent results.
|
|
396
396
|
pi.on("session_shutdown", async () => {
|
|
397
397
|
unsubSpawnRpc();
|
|
398
|
+
unsubStopRpc();
|
|
398
399
|
unsubPingRpc();
|
|
399
400
|
currentCtx = undefined;
|
|
400
401
|
delete globalThis[MANAGER_KEY];
|
|
@@ -179,7 +179,7 @@ export class ConversationViewer {
|
|
|
179
179
|
if (c.type === "text" && c.text)
|
|
180
180
|
textParts.push(c.text);
|
|
181
181
|
else if (c.type === "toolCall") {
|
|
182
|
-
toolCalls.push(c.toolName ?? "unknown");
|
|
182
|
+
toolCalls.push(c.name ?? c.toolName ?? "unknown");
|
|
183
183
|
}
|
|
184
184
|
}
|
|
185
185
|
if (needsSeparator)
|
|
@@ -159,7 +159,7 @@ describe("ConversationViewer", () => {
|
|
|
159
159
|
role: "assistant",
|
|
160
160
|
content: [
|
|
161
161
|
{ type: "text", text: "Let me check that." },
|
|
162
|
-
{ type: "toolCall", toolUseId: "t1",
|
|
162
|
+
{ type: "toolCall", toolUseId: "t1", name: "very_long_tool_name_" + "x".repeat(200), input: {} },
|
|
163
163
|
],
|
|
164
164
|
},
|
|
165
165
|
];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tintinweb/pi-subagents",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "A pi extension extension that brings smart Claude Code-style autonomous sub-agents to pi.",
|
|
5
5
|
"author": "tintinweb",
|
|
6
6
|
"license": "MIT",
|
|
@@ -21,9 +21,9 @@
|
|
|
21
21
|
"autonomous"
|
|
22
22
|
],
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@mariozechner/pi-ai": "^0.
|
|
25
|
-
"@mariozechner/pi-coding-agent": "^0.
|
|
26
|
-
"@mariozechner/pi-tui": "^0.
|
|
24
|
+
"@mariozechner/pi-ai": "^0.61.1",
|
|
25
|
+
"@mariozechner/pi-coding-agent": "^0.61.1",
|
|
26
|
+
"@mariozechner/pi-tui": "^0.61.1",
|
|
27
27
|
"@sinclair/typebox": "latest"
|
|
28
28
|
},
|
|
29
29
|
"scripts": {
|
|
@@ -36,9 +36,9 @@
|
|
|
36
36
|
"lint:fix": "biome check --fix src/ test/"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
|
-
"@types/node": "^20.0.0",
|
|
40
|
-
"typescript": "^5.0.0",
|
|
41
39
|
"@biomejs/biome": "^2.3.5",
|
|
40
|
+
"@types/node": "^25.5.0",
|
|
41
|
+
"typescript": "^5.0.0",
|
|
42
42
|
"vitest": "^4.0.18"
|
|
43
43
|
},
|
|
44
44
|
"pi": {
|
package/src/agent-runner.ts
CHANGED
|
@@ -393,7 +393,7 @@ export function getAgentConversation(session: AgentSession): string {
|
|
|
393
393
|
const toolCalls: string[] = [];
|
|
394
394
|
for (const c of msg.content) {
|
|
395
395
|
if (c.type === "text" && c.text) textParts.push(c.text);
|
|
396
|
-
else if (c.type === "toolCall") toolCalls.push(` Tool: ${(c as any).toolName ?? "unknown"}`);
|
|
396
|
+
else if (c.type === "toolCall") toolCalls.push(` Tool: ${(c as any).name ?? (c as any).toolName ?? "unknown"}`);
|
|
397
397
|
}
|
|
398
398
|
if (textParts.length > 0) parts.push(`[Assistant]: ${textParts.join("\n")}`);
|
|
399
399
|
if (toolCalls.length > 0) parts.push(`[Tool Calls]:\n${toolCalls.join("\n")}`);
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Cross-extension RPC handlers for the subagents extension.
|
|
3
3
|
*
|
|
4
|
-
* Exposes ping and
|
|
4
|
+
* Exposes ping, spawn, and stop RPCs over the pi.events event bus,
|
|
5
5
|
* using per-request scoped reply channels.
|
|
6
|
+
*
|
|
7
|
+
* Reply envelope follows pi-mono convention:
|
|
8
|
+
* success → { success: true, data?: T }
|
|
9
|
+
* error → { success: false, error: string }
|
|
6
10
|
*/
|
|
7
11
|
|
|
8
12
|
/** Minimal event bus interface needed by the RPC handlers. */
|
|
@@ -11,9 +15,18 @@ export interface EventBus {
|
|
|
11
15
|
emit(event: string, data: unknown): void;
|
|
12
16
|
}
|
|
13
17
|
|
|
14
|
-
/**
|
|
18
|
+
/** RPC reply envelope — matches pi-mono's RpcResponse shape. */
|
|
19
|
+
export type RpcReply<T = void> =
|
|
20
|
+
| { success: true; data?: T }
|
|
21
|
+
| { success: false; error: string };
|
|
22
|
+
|
|
23
|
+
/** RPC protocol version — bumped when the envelope or method contracts change. */
|
|
24
|
+
export const PROTOCOL_VERSION = 2;
|
|
25
|
+
|
|
26
|
+
/** Minimal AgentManager interface needed by the spawn/stop RPCs. */
|
|
15
27
|
export interface SpawnCapable {
|
|
16
28
|
spawn(pi: unknown, ctx: unknown, type: string, prompt: string, options: any): string;
|
|
29
|
+
abort(id: string): boolean;
|
|
17
30
|
}
|
|
18
31
|
|
|
19
32
|
export interface RpcDeps {
|
|
@@ -26,36 +39,57 @@ export interface RpcDeps {
|
|
|
26
39
|
export interface RpcHandle {
|
|
27
40
|
unsubPing: () => void;
|
|
28
41
|
unsubSpawn: () => void;
|
|
42
|
+
unsubStop: () => void;
|
|
29
43
|
}
|
|
30
44
|
|
|
31
45
|
/**
|
|
32
|
-
*
|
|
46
|
+
* Wire a single RPC handler: listen on `channel`, run `fn(params)`,
|
|
47
|
+
* emit the reply envelope on `channel:reply:${requestId}`.
|
|
48
|
+
*/
|
|
49
|
+
function handleRpc<P extends { requestId: string }>(
|
|
50
|
+
events: EventBus,
|
|
51
|
+
channel: string,
|
|
52
|
+
fn: (params: P) => unknown | Promise<unknown>,
|
|
53
|
+
): () => void {
|
|
54
|
+
return events.on(channel, async (raw: unknown) => {
|
|
55
|
+
const params = raw as P;
|
|
56
|
+
try {
|
|
57
|
+
const data = await fn(params);
|
|
58
|
+
const reply: { success: true; data?: unknown } = { success: true };
|
|
59
|
+
if (data !== undefined) reply.data = data;
|
|
60
|
+
events.emit(`${channel}:reply:${params.requestId}`, reply);
|
|
61
|
+
} catch (err: any) {
|
|
62
|
+
events.emit(`${channel}:reply:${params.requestId}`, {
|
|
63
|
+
success: false, error: err?.message ?? String(err),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Register ping, spawn, and stop RPC handlers on the event bus.
|
|
33
71
|
* Returns unsub functions for cleanup.
|
|
34
72
|
*/
|
|
35
73
|
export function registerRpcHandlers(deps: RpcDeps): RpcHandle {
|
|
36
74
|
const { events, pi, getCtx, manager } = deps;
|
|
37
75
|
|
|
38
|
-
const unsubPing = events
|
|
39
|
-
|
|
40
|
-
events.emit(`subagents:rpc:ping:reply:${requestId}`, {});
|
|
76
|
+
const unsubPing = handleRpc(events, "subagents:rpc:ping", () => {
|
|
77
|
+
return { version: PROTOCOL_VERSION };
|
|
41
78
|
});
|
|
42
79
|
|
|
43
|
-
const unsubSpawn =
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
events.emit(`subagents:rpc:spawn:reply:${requestId}`, { error: err.message });
|
|
57
|
-
}
|
|
58
|
-
});
|
|
80
|
+
const unsubSpawn = handleRpc<{ requestId: string; type: string; prompt: string; options?: any }>(
|
|
81
|
+
events, "subagents:rpc:spawn", ({ type, prompt, options }) => {
|
|
82
|
+
const ctx = getCtx();
|
|
83
|
+
if (!ctx) throw new Error("No active session");
|
|
84
|
+
return { id: manager.spawn(pi, ctx, type, prompt, options ?? {}) };
|
|
85
|
+
},
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
const unsubStop = handleRpc<{ requestId: string; agentId: string }>(
|
|
89
|
+
events, "subagents:rpc:stop", ({ agentId }) => {
|
|
90
|
+
if (!manager.abort(agentId)) throw new Error("Agent not found");
|
|
91
|
+
},
|
|
92
|
+
);
|
|
59
93
|
|
|
60
|
-
return { unsubPing, unsubSpawn };
|
|
94
|
+
return { unsubPing, unsubSpawn, unsubStop };
|
|
61
95
|
}
|
package/src/index.ts
CHANGED
|
@@ -289,7 +289,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
289
289
|
content: notification + footer,
|
|
290
290
|
display: true,
|
|
291
291
|
details: buildNotificationDetails(record, 500, agentActivity.get(record.id)),
|
|
292
|
-
}, { deliverAs: "followUp" });
|
|
292
|
+
}, { deliverAs: "followUp", triggerTurn: true });
|
|
293
293
|
}
|
|
294
294
|
|
|
295
295
|
function sendIndividualNudge(record: AgentRecord) {
|
|
@@ -326,7 +326,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
326
326
|
content: `Background agent group completed: ${label}\n\n${notifications}\n\nUse get_subagent_result for full output.`,
|
|
327
327
|
display: true,
|
|
328
328
|
details,
|
|
329
|
-
}, { deliverAs: "followUp" });
|
|
329
|
+
}, { deliverAs: "followUp", triggerTurn: true });
|
|
330
330
|
});
|
|
331
331
|
widget.update();
|
|
332
332
|
},
|
|
@@ -431,7 +431,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
431
431
|
|
|
432
432
|
pi.on("session_switch", () => { manager.clearCompleted(); });
|
|
433
433
|
|
|
434
|
-
const { unsubPing: unsubPingRpc, unsubSpawn: unsubSpawnRpc } = registerRpcHandlers({
|
|
434
|
+
const { unsubPing: unsubPingRpc, unsubSpawn: unsubSpawnRpc, unsubStop: unsubStopRpc } = registerRpcHandlers({
|
|
435
435
|
events: pi.events,
|
|
436
436
|
pi,
|
|
437
437
|
getCtx: () => currentCtx,
|
|
@@ -445,6 +445,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
445
445
|
// If the session is going down, there's nothing left to consume agent results.
|
|
446
446
|
pi.on("session_shutdown", async () => {
|
|
447
447
|
unsubSpawnRpc();
|
|
448
|
+
unsubStopRpc();
|
|
448
449
|
unsubPingRpc();
|
|
449
450
|
currentCtx = undefined;
|
|
450
451
|
delete (globalThis as any)[MANAGER_KEY];
|
|
@@ -191,7 +191,7 @@ export class ConversationViewer implements Component {
|
|
|
191
191
|
for (const c of msg.content) {
|
|
192
192
|
if (c.type === "text" && c.text) textParts.push(c.text);
|
|
193
193
|
else if (c.type === "toolCall") {
|
|
194
|
-
toolCalls.push((c as any).toolName ?? "unknown");
|
|
194
|
+
toolCalls.push((c as any).name ?? (c as any).toolName ?? "unknown");
|
|
195
195
|
}
|
|
196
196
|
}
|
|
197
197
|
if (needsSeparator) lines.push(th.fg("dim", "───"));
|