@narumitw/pi-subagents 0.26.0 → 0.27.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/README.md +43 -12
- package/package.json +1 -1
- package/src/stateful-tool-params.ts +202 -0
- package/src/stateful.ts +101 -116
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@narumitw/pi-subagents) [](https://pi.dev) [](./LICENSE)
|
|
4
4
|
|
|
5
|
-
`@narumitw/pi-subagents` is a native [Pi coding agent](https://pi.dev) extension for delegating work to specialized agents. The batch `subagent` tool keeps isolated Pi subprocesses, while
|
|
5
|
+
`@narumitw/pi-subagents` is a native [Pi coding agent](https://pi.dev) extension for delegating work to specialized agents. The blocking batch `subagent` tool keeps isolated Pi subprocesses, while four detached tools run reusable agents either as subprocess-backed logical sessions or as public-SDK in-process child sessions. Together they form a fixed five-tool surface.
|
|
6
6
|
|
|
7
7
|
Use it to split independent research, planning, implementation, and review work across focused workers. Under the default next-turn delivery policy, background delegation is for work the current response does not depend on. Opt-in auto-resume also supports final-answer-dependent background work by requesting a synthesis turn after completion.
|
|
8
8
|
|
|
@@ -19,7 +19,7 @@ Use it to split independent research, planning, implementation, and review work
|
|
|
19
19
|
- Supports per-task `cwd`, hard subprocess `timeoutMs`, task-selected `thinkingLevel`, abort propagation, and streaming progress.
|
|
20
20
|
- Bounds JSON lines, captured messages, stderr, final output, chain substitution, and fan-in context.
|
|
21
21
|
- Enforces a recursion-depth guard and deterministic process-group termination.
|
|
22
|
-
- Provides addressable stateful agents with follow-up, mailbox
|
|
22
|
+
- Provides addressable stateful agents with follow-up, consolidated mailbox/management actions, context selection, and persistence.
|
|
23
23
|
- Publishes transient runtime status through Pi's generic extension status API while subagents are running.
|
|
24
24
|
- Returns complete bounded worker output in tool details and a concise result for the main agent.
|
|
25
25
|
|
|
@@ -211,7 +211,7 @@ Run a chain where each step receives the previous output:
|
|
|
211
211
|
|
|
212
212
|
Stateful lifecycle tools are available by default. `subagent_spawn` is detached: it schedules work, returns immediately with an opaque `agentId`, and later injects a bounded `pi-subagent-completion` custom message. Completions that settle in the same dispatch window are batched, and the broker allows at most one in-flight root wake until that parent turn starts.
|
|
213
213
|
|
|
214
|
-
Detached work follows a non-polling policy. With default `next-turn` delivery, prefer one bounded `subagent_spawn` for related asynchronous research or review only when the current response does not depend on its result; if it does, use blocking `subagent`. With opt-in `auto-resume`, detached broad work may be final-answer-dependent because completion requests a synthesis turn after the root settles. In either mode, do useful non-overlapping main-agent work immediately, do not poll `
|
|
214
|
+
Detached work follows a non-polling policy. With default `next-turn` delivery, prefer one bounded `subagent_spawn` for related asynchronous research or review only when the current response does not depend on its result; if it does, use blocking `subagent`. With opt-in `auto-resume`, detached broad work may be final-answer-dependent because completion requests a synthesis turn after the root settles. In either mode, do useful non-overlapping main-agent work immediately, do not poll `subagent_manage` with `action: "list"` or `subagent_mailbox` with `action: "read"`, and do not duplicate delegated work. Add another detached agent only for truly independent work with safe workspace concurrency. Detached lifecycle work intentionally has no `subagent_wait` tool.
|
|
215
215
|
|
|
216
216
|
A detached agent additionally needs a concrete isolation or specialization benefit such as independent review, bounded context/output, a distinct model/tool profile, or workspace isolation. Simple work that the main agent can perform directly should not be delegated.
|
|
217
217
|
|
|
@@ -246,20 +246,49 @@ The direct routes remain predictable: `/subagents settings` changes user complet
|
|
|
246
246
|
}
|
|
247
247
|
```
|
|
248
248
|
|
|
249
|
-
The settings UI patches the raw JSON atomically and preserves unknown fields; it refuses to overwrite malformed or invalid settings. Set `"enabled": false` to remove all stateful
|
|
249
|
+
The settings UI patches the raw JSON atomically and preserves unknown fields; it refuses to overwrite malformed or invalid settings. Set `"enabled": false` to remove all four stateful tools. Otherwise, the extension keeps the following tool membership fixed across spawn, completion, interrupt, close, and mailbox transitions. This avoids lifecycle-driven tool-schema churn and preserves a stable provider prompt prefix for KV caching.
|
|
250
250
|
|
|
251
251
|
| Tool | Purpose |
|
|
252
252
|
| --- | --- |
|
|
253
253
|
| `subagent_spawn` | Start detached work with an optional task-selected thinking level, return an opaque `agentId` immediately, and deliver completion asynchronously. |
|
|
254
|
-
| `subagent_send` | Send follow-up work
|
|
255
|
-
| `
|
|
256
|
-
| `
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
254
|
+
| `subagent_send` | Send follow-up work and trigger a new turn on a reusable agent; shared-workspace write conflicts are guarded unless explicitly overridden. |
|
|
255
|
+
| `subagent_manage` | Use `action: "list"` to inspect agents, `"interrupt"` to retain an agent after aborting active work, or `"close"` to release it; interrupt/close accept optional `subtree`. |
|
|
256
|
+
| `subagent_mailbox` | Use `action: "send"` for queue-only messages that do not start a turn, or `"read"` to read and optionally acknowledge unread messages. |
|
|
257
|
+
|
|
258
|
+
The action schemas are flat for provider compatibility and reject parameters that belong to another action. For example:
|
|
259
|
+
|
|
260
|
+
```json
|
|
261
|
+
{
|
|
262
|
+
"action": "interrupt",
|
|
263
|
+
"agentId": "sa_example",
|
|
264
|
+
"subtree": true
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
```json
|
|
269
|
+
{
|
|
270
|
+
"action": "send",
|
|
271
|
+
"agentId": "sa_example",
|
|
272
|
+
"message": "Check the API compatibility note before finishing."
|
|
273
|
+
}
|
|
274
|
+
```
|
|
260
275
|
|
|
261
276
|
Use the **Current-session agents** action in `/subagents` to inspect the indented agent tree, lifecycle state, unread count, and available actions, or to confirm clearing retained agents. `/subagents:agents list|clear` remains the compatibility command for the same current-session operations. Active turns are FIFO-limited by `maxActiveTurns`; excess retained work remains in `starting` state until a slot is available. `maxAgents` separately bounds running, queued, and idle records. `parentId` creates a bounded child relationship; subtree interrupt and close operate child-first.
|
|
262
277
|
|
|
278
|
+
### Migrating from the seven-tool lifecycle surface
|
|
279
|
+
|
|
280
|
+
The five replaced names are intentionally not registered as aliases. Update explicit prompts and integrations as follows:
|
|
281
|
+
|
|
282
|
+
| Previous call | Fixed-surface call |
|
|
283
|
+
| --- | --- |
|
|
284
|
+
| `subagent_list({ includeClosed })` | `subagent_manage({ action: "list", includeClosed })` |
|
|
285
|
+
| `subagent_interrupt({ agentId, subtree })` | `subagent_manage({ action: "interrupt", agentId, subtree })` |
|
|
286
|
+
| `subagent_close({ agentId, subtree })` | `subagent_manage({ action: "close", agentId, subtree })` |
|
|
287
|
+
| `subagent_message({ agentId, message, ... })` | `subagent_mailbox({ action: "send", agentId, message, ... })` |
|
|
288
|
+
| `subagent_messages({ agentId, acknowledge, limit })` | `subagent_mailbox({ action: "read", agentId, acknowledge, limit })` |
|
|
289
|
+
|
|
290
|
+
Persisted agent and mailbox records require no migration. If an explicit prompt in a resumed conversation keeps requesting an old name, update it with the mapping above or start a fresh conversation. To roll back after an upgrade, pin the package version used before the upgrade; for this migration, use `pi install npm:@narumitw/pi-subagents@0.26.0`. The previous release can read the same state directory.
|
|
291
|
+
|
|
263
292
|
A spawn can request a thinking level explicitly:
|
|
264
293
|
|
|
265
294
|
```json
|
|
@@ -462,8 +491,10 @@ Stateful records are stored as versioned mode-0600 JSON under `~/.pi/agent/pi-su
|
|
|
462
491
|
```txt
|
|
463
492
|
extensions/pi-subagents/
|
|
464
493
|
├── src/
|
|
465
|
-
│ ├── subagents.ts
|
|
466
|
-
│
|
|
494
|
+
│ ├── subagents.ts # Pi entrypoint and blocking tool schema
|
|
495
|
+
│ ├── stateful.ts # Detached lifecycle registration and dispatch
|
|
496
|
+
│ ├── stateful-tool-params.ts # Consolidated action schemas and validation
|
|
497
|
+
│ └── *.ts # Package-local discovery, execution, rendering, and config modules
|
|
467
498
|
├── README.md
|
|
468
499
|
├── LICENSE
|
|
469
500
|
├── tsconfig.json
|
package/package.json
CHANGED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { StringEnum } from "@earendil-works/pi-ai";
|
|
2
|
+
import { Type } from "typebox";
|
|
3
|
+
|
|
4
|
+
const MANAGE_ACTIONS = ["list", "interrupt", "close"] as const;
|
|
5
|
+
const MAILBOX_ACTIONS = ["send", "read"] as const;
|
|
6
|
+
const MAX_MAILBOX_MESSAGE_LENGTH = 16 * 1024;
|
|
7
|
+
|
|
8
|
+
export const ManageParamsSchema = Type.Object(
|
|
9
|
+
{
|
|
10
|
+
action: StringEnum(MANAGE_ACTIONS, {
|
|
11
|
+
description:
|
|
12
|
+
"Use list to inspect agents, interrupt to stop active work, or close to release agents.",
|
|
13
|
+
}),
|
|
14
|
+
agentId: Type.Optional(
|
|
15
|
+
Type.String({ minLength: 1, description: "Required for interrupt and close." }),
|
|
16
|
+
),
|
|
17
|
+
includeClosed: Type.Optional(
|
|
18
|
+
Type.Boolean({
|
|
19
|
+
default: false,
|
|
20
|
+
description: "List closed records as well as retained agents.",
|
|
21
|
+
}),
|
|
22
|
+
),
|
|
23
|
+
subtree: Type.Optional(
|
|
24
|
+
Type.Boolean({
|
|
25
|
+
default: false,
|
|
26
|
+
description: "Interrupt or close the target and all descendants child-first.",
|
|
27
|
+
}),
|
|
28
|
+
),
|
|
29
|
+
},
|
|
30
|
+
{ additionalProperties: false },
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
export const MailboxParamsSchema = Type.Object(
|
|
34
|
+
{
|
|
35
|
+
action: StringEnum(MAILBOX_ACTIONS, {
|
|
36
|
+
description: "Use send for queue-only delivery or read to inspect unread mailbox messages.",
|
|
37
|
+
}),
|
|
38
|
+
agentId: Type.String({ minLength: 1, description: "Mailbox owner or message recipient." }),
|
|
39
|
+
message: Type.Optional(
|
|
40
|
+
Type.String({
|
|
41
|
+
minLength: 1,
|
|
42
|
+
maxLength: MAX_MAILBOX_MESSAGE_LENGTH,
|
|
43
|
+
description: "Message content required by send; sending does not start a turn.",
|
|
44
|
+
}),
|
|
45
|
+
),
|
|
46
|
+
senderId: Type.Optional(Type.String({ description: "Optional same-tree sender identity." })),
|
|
47
|
+
deduplicationKey: Type.Optional(
|
|
48
|
+
Type.String({ maxLength: 256, description: "Optional idempotency key for send." }),
|
|
49
|
+
),
|
|
50
|
+
acknowledge: Type.Optional(
|
|
51
|
+
Type.Boolean({ default: true, description: "Mark returned read messages as acknowledged." }),
|
|
52
|
+
),
|
|
53
|
+
limit: Type.Optional(
|
|
54
|
+
Type.Number({ minimum: 1, maximum: 20, default: 20, description: "Maximum messages." }),
|
|
55
|
+
),
|
|
56
|
+
},
|
|
57
|
+
{ additionalProperties: false },
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
export type ValidatedManageParams =
|
|
61
|
+
| { action: "list"; includeClosed?: boolean }
|
|
62
|
+
| { action: "interrupt" | "close"; agentId: string; subtree?: boolean };
|
|
63
|
+
|
|
64
|
+
export type ValidatedMailboxParams =
|
|
65
|
+
| {
|
|
66
|
+
action: "send";
|
|
67
|
+
agentId: string;
|
|
68
|
+
message: string;
|
|
69
|
+
senderId?: string;
|
|
70
|
+
deduplicationKey?: string;
|
|
71
|
+
}
|
|
72
|
+
| { action: "read"; agentId: string; acknowledge?: boolean; limit?: number };
|
|
73
|
+
|
|
74
|
+
export function validateManageParams(params: unknown): ValidatedManageParams {
|
|
75
|
+
const values = parameterRecord(params, "subagent_manage");
|
|
76
|
+
const action = values.action;
|
|
77
|
+
if (
|
|
78
|
+
typeof action !== "string" ||
|
|
79
|
+
!MANAGE_ACTIONS.includes(action as (typeof MANAGE_ACTIONS)[number])
|
|
80
|
+
) {
|
|
81
|
+
throw new Error(`subagent_manage action must be one of: ${MANAGE_ACTIONS.join(", ")}`);
|
|
82
|
+
}
|
|
83
|
+
if (action === "list") {
|
|
84
|
+
assertOnlyActionKeys("subagent_manage", action, values, ["action", "includeClosed"]);
|
|
85
|
+
assertOptionalBoolean("subagent_manage", action, values, "includeClosed");
|
|
86
|
+
return values as ValidatedManageParams;
|
|
87
|
+
}
|
|
88
|
+
assertOnlyActionKeys("subagent_manage", action, values, ["action", "agentId", "subtree"]);
|
|
89
|
+
assertRequiredString("subagent_manage", action, values, "agentId");
|
|
90
|
+
assertOptionalBoolean("subagent_manage", action, values, "subtree");
|
|
91
|
+
return values as ValidatedManageParams;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function validateMailboxParams(params: unknown): ValidatedMailboxParams {
|
|
95
|
+
const values = parameterRecord(params, "subagent_mailbox");
|
|
96
|
+
const action = values.action;
|
|
97
|
+
if (
|
|
98
|
+
typeof action !== "string" ||
|
|
99
|
+
!MAILBOX_ACTIONS.includes(action as (typeof MAILBOX_ACTIONS)[number])
|
|
100
|
+
) {
|
|
101
|
+
throw new Error(`subagent_mailbox action must be one of: ${MAILBOX_ACTIONS.join(", ")}`);
|
|
102
|
+
}
|
|
103
|
+
if (action === "send") {
|
|
104
|
+
assertOnlyActionKeys("subagent_mailbox", action, values, [
|
|
105
|
+
"action",
|
|
106
|
+
"agentId",
|
|
107
|
+
"message",
|
|
108
|
+
"senderId",
|
|
109
|
+
"deduplicationKey",
|
|
110
|
+
]);
|
|
111
|
+
assertRequiredString("subagent_mailbox", action, values, "agentId");
|
|
112
|
+
assertRequiredString("subagent_mailbox", action, values, "message");
|
|
113
|
+
const message = values.message as string;
|
|
114
|
+
if (message.length > MAX_MAILBOX_MESSAGE_LENGTH) {
|
|
115
|
+
throw new Error(
|
|
116
|
+
`subagent_mailbox action "send" requires message at most ${MAX_MAILBOX_MESSAGE_LENGTH} characters`,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
assertOptionalString("subagent_mailbox", action, values, "senderId");
|
|
120
|
+
assertOptionalString("subagent_mailbox", action, values, "deduplicationKey");
|
|
121
|
+
if (typeof values.deduplicationKey === "string" && values.deduplicationKey.length > 256) {
|
|
122
|
+
throw new Error(
|
|
123
|
+
'subagent_mailbox action "send" requires deduplicationKey at most 256 characters',
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
return values as ValidatedMailboxParams;
|
|
127
|
+
}
|
|
128
|
+
assertOnlyActionKeys("subagent_mailbox", action, values, [
|
|
129
|
+
"action",
|
|
130
|
+
"agentId",
|
|
131
|
+
"acknowledge",
|
|
132
|
+
"limit",
|
|
133
|
+
]);
|
|
134
|
+
assertRequiredString("subagent_mailbox", action, values, "agentId");
|
|
135
|
+
assertOptionalBoolean("subagent_mailbox", action, values, "acknowledge");
|
|
136
|
+
if (
|
|
137
|
+
values.limit !== undefined &&
|
|
138
|
+
(typeof values.limit !== "number" ||
|
|
139
|
+
!Number.isFinite(values.limit) ||
|
|
140
|
+
values.limit < 1 ||
|
|
141
|
+
values.limit > 20)
|
|
142
|
+
) {
|
|
143
|
+
throw new Error('subagent_mailbox action "read" requires limit between 1 and 20');
|
|
144
|
+
}
|
|
145
|
+
return values as ValidatedMailboxParams;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function parameterRecord(params: unknown, toolName: string): Record<string, unknown> {
|
|
149
|
+
if (!params || typeof params !== "object" || Array.isArray(params)) {
|
|
150
|
+
throw new Error(`${toolName} parameters must be an object`);
|
|
151
|
+
}
|
|
152
|
+
return params as Record<string, unknown>;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function assertOnlyActionKeys(
|
|
156
|
+
toolName: string,
|
|
157
|
+
action: string,
|
|
158
|
+
values: Record<string, unknown>,
|
|
159
|
+
allowed: readonly string[],
|
|
160
|
+
): void {
|
|
161
|
+
const unexpected = Object.keys(values).find(
|
|
162
|
+
(key) => values[key] !== undefined && !allowed.includes(key),
|
|
163
|
+
);
|
|
164
|
+
if (unexpected) {
|
|
165
|
+
throw new Error(`${toolName} action "${action}" does not accept ${unexpected}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function assertRequiredString(
|
|
170
|
+
toolName: string,
|
|
171
|
+
action: string,
|
|
172
|
+
values: Record<string, unknown>,
|
|
173
|
+
key: string,
|
|
174
|
+
): void {
|
|
175
|
+
const value = values[key];
|
|
176
|
+
if (value === undefined) throw new Error(`${toolName} action "${action}" requires ${key}`);
|
|
177
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
178
|
+
throw new Error(`${toolName} action "${action}" requires ${key} to be a non-empty string`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function assertOptionalString(
|
|
183
|
+
toolName: string,
|
|
184
|
+
action: string,
|
|
185
|
+
values: Record<string, unknown>,
|
|
186
|
+
key: string,
|
|
187
|
+
): void {
|
|
188
|
+
if (values[key] !== undefined && typeof values[key] !== "string") {
|
|
189
|
+
throw new Error(`${toolName} action "${action}" requires ${key} to be a string`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function assertOptionalBoolean(
|
|
194
|
+
toolName: string,
|
|
195
|
+
action: string,
|
|
196
|
+
values: Record<string, unknown>,
|
|
197
|
+
key: string,
|
|
198
|
+
): void {
|
|
199
|
+
if (values[key] !== undefined && typeof values[key] !== "boolean") {
|
|
200
|
+
throw new Error(`${toolName} action "${action}" requires ${key} to be a boolean`);
|
|
201
|
+
}
|
|
202
|
+
}
|
package/src/stateful.ts
CHANGED
|
@@ -25,6 +25,12 @@ import { DEFAULT_MAX_CONTEXT_BYTES, truncateUtf8 } from "./limits.js";
|
|
|
25
25
|
import { AgentPersistence } from "./persistence.js";
|
|
26
26
|
import { AgentRegistry, type AgentTurnCompletion, type ManagedAgent } from "./registry.js";
|
|
27
27
|
import { readSubagentSettings } from "./settings.js";
|
|
28
|
+
import {
|
|
29
|
+
MailboxParamsSchema,
|
|
30
|
+
ManageParamsSchema,
|
|
31
|
+
validateMailboxParams,
|
|
32
|
+
validateManageParams,
|
|
33
|
+
} from "./stateful-tool-params.js";
|
|
28
34
|
import { SubprocessTransport } from "./subprocess-transport.js";
|
|
29
35
|
import { WorkspaceManager } from "./workspace.js";
|
|
30
36
|
|
|
@@ -64,8 +70,8 @@ function createSpawnPromptGuidelines(completionDelivery: CompletionDelivery): st
|
|
|
64
70
|
"When subagent_spawn fits the completion-delivery policy, do not choose a blocking parallel subagent merely to keep delegation in the same turn.",
|
|
65
71
|
"Add another subagent_spawn only for truly independent work with safe workspace concurrency.",
|
|
66
72
|
noLocalWorkGuidance,
|
|
67
|
-
|
|
68
|
-
|
|
73
|
+
'Consume and synthesize available subagent_spawn completion messages; use subagent_manage with action "interrupt" or "close" for agents that are no longer needed.',
|
|
74
|
+
'Completion from subagent_spawn is delivered automatically. Do not poll with subagent_manage action "list" or subagent_mailbox action "read", repeatedly check progress, or duplicate the delegated work.',
|
|
69
75
|
];
|
|
70
76
|
}
|
|
71
77
|
|
|
@@ -91,6 +97,11 @@ export interface StatefulSubagentController {
|
|
|
91
97
|
clearAgents(): Promise<number>;
|
|
92
98
|
}
|
|
93
99
|
|
|
100
|
+
interface StatefulActionToolResult {
|
|
101
|
+
content: Array<{ type: "text"; text: string }>;
|
|
102
|
+
details: Record<string, unknown>;
|
|
103
|
+
}
|
|
104
|
+
|
|
94
105
|
export function registerStatefulSubagents(
|
|
95
106
|
pi: ExtensionAPI,
|
|
96
107
|
dependencies: StatefulSubagentDependencies = {},
|
|
@@ -157,6 +168,11 @@ export function registerStatefulSubagents(
|
|
|
157
168
|
if (!registry) throw new Error("Stateful subagents are not initialized for this session");
|
|
158
169
|
return registry;
|
|
159
170
|
};
|
|
171
|
+
const requireAgent = (agentId: string) => {
|
|
172
|
+
const agent = requireRegistry().get(agentId);
|
|
173
|
+
if (!agent) throw new Error(`Unknown subagent: ${agentId}`);
|
|
174
|
+
return agent;
|
|
175
|
+
};
|
|
160
176
|
|
|
161
177
|
pi.on("session_start", async (_event, ctx) => {
|
|
162
178
|
const generation = ++runtimeGeneration;
|
|
@@ -374,7 +390,9 @@ export function registerStatefulSubagents(
|
|
|
374
390
|
pi.registerTool({
|
|
375
391
|
name: "subagent_send",
|
|
376
392
|
label: "Send Subagent Follow-up",
|
|
377
|
-
description:
|
|
393
|
+
description:
|
|
394
|
+
"Send follow-up work to an idle, completed, interrupted, or failed subagent and start a new turn. Use subagent_mailbox for queue-only messages.",
|
|
395
|
+
promptSnippet: "Start a new detached follow-up turn on a retained subagent",
|
|
378
396
|
parameters: Type.Object({
|
|
379
397
|
agentId: Type.String(),
|
|
380
398
|
task: Type.String({ minLength: 1, maxLength: DEFAULT_MAX_CONTEXT_BYTES }),
|
|
@@ -404,141 +422,66 @@ export function registerStatefulSubagents(
|
|
|
404
422
|
});
|
|
405
423
|
|
|
406
424
|
pi.registerTool({
|
|
407
|
-
name: "
|
|
408
|
-
label: "
|
|
409
|
-
description:
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
const message = await requireRegistry().sendMessage(
|
|
418
|
-
params.agentId,
|
|
419
|
-
params.message,
|
|
420
|
-
params.senderId,
|
|
421
|
-
params.deduplicationKey,
|
|
422
|
-
);
|
|
423
|
-
return {
|
|
424
|
-
content: [{ type: "text", text: `Queued ${message.id} for ${message.recipientId}.` }],
|
|
425
|
-
details: { message },
|
|
426
|
-
};
|
|
427
|
-
},
|
|
428
|
-
});
|
|
429
|
-
|
|
430
|
-
pi.registerTool({
|
|
431
|
-
name: "subagent_messages",
|
|
432
|
-
label: "Read Subagent Messages",
|
|
433
|
-
description: "Read unread mailbox messages and optionally acknowledge them.",
|
|
434
|
-
parameters: Type.Object({
|
|
435
|
-
agentId: Type.String(),
|
|
436
|
-
acknowledge: Type.Optional(Type.Boolean({ default: true })),
|
|
437
|
-
limit: Type.Optional(Type.Number({ minimum: 1, maximum: 20, default: 20 })),
|
|
438
|
-
}),
|
|
439
|
-
async execute(_id, params) {
|
|
440
|
-
const messages = await requireRegistry().readMessages(
|
|
441
|
-
params.agentId,
|
|
442
|
-
params.acknowledge,
|
|
443
|
-
params.limit,
|
|
444
|
-
);
|
|
445
|
-
const summaries = messages.map((message) => ({
|
|
446
|
-
...message,
|
|
447
|
-
content: truncateUtf8(message.content, MAX_TOOL_MESSAGE_BYTES).text,
|
|
448
|
-
}));
|
|
449
|
-
const text = summaries.length
|
|
450
|
-
? summaries
|
|
451
|
-
.map((message) => `${message.id} from ${message.senderId}: ${message.content}`)
|
|
452
|
-
.join("\n")
|
|
453
|
-
: "No unread messages.";
|
|
454
|
-
return {
|
|
455
|
-
content: [{ type: "text", text: truncateUtf8(text, DEFAULT_MAX_CONTEXT_BYTES).text }],
|
|
456
|
-
details: { messages: summaries },
|
|
457
|
-
};
|
|
458
|
-
},
|
|
459
|
-
});
|
|
460
|
-
|
|
461
|
-
pi.registerTool({
|
|
462
|
-
name: "subagent_list",
|
|
463
|
-
label: "List Subagents",
|
|
464
|
-
description: "List stateful subagents and lifecycle states.",
|
|
465
|
-
parameters: Type.Object({ includeClosed: Type.Optional(Type.Boolean({ default: false })) }),
|
|
466
|
-
async execute(_id, params) {
|
|
467
|
-
const agents = requireRegistry().list(params.includeClosed);
|
|
468
|
-
return {
|
|
469
|
-
content: [
|
|
470
|
-
{
|
|
471
|
-
type: "text",
|
|
472
|
-
text: agents.length ? agents.map(formatLine).join("\n") : "No stateful subagents.",
|
|
473
|
-
},
|
|
474
|
-
],
|
|
475
|
-
details: { agents: agents.map(summarizeAgent) },
|
|
476
|
-
};
|
|
477
|
-
},
|
|
478
|
-
});
|
|
479
|
-
|
|
480
|
-
pi.registerTool({
|
|
481
|
-
name: "subagent_interrupt",
|
|
482
|
-
label: "Interrupt Subagent",
|
|
483
|
-
description: "Interrupt the current turn while retaining the subagent for follow-up work.",
|
|
484
|
-
parameters: Type.Object({
|
|
485
|
-
agentId: Type.String(),
|
|
486
|
-
subtree: Type.Optional(Type.Boolean({ default: false })),
|
|
487
|
-
}),
|
|
488
|
-
async execute(_id, params) {
|
|
489
|
-
if (params.subtree) {
|
|
490
|
-
const agents = await requireRegistry().interruptTree(params.agentId);
|
|
491
|
-
const root = requireRegistry().get(params.agentId);
|
|
492
|
-
if (!root) throw new Error(`Unknown subagent: ${params.agentId}`);
|
|
425
|
+
name: "subagent_manage",
|
|
426
|
+
label: "Manage Subagents",
|
|
427
|
+
description:
|
|
428
|
+
"List retained subagents, interrupt active work while keeping an agent reusable, or close agents and release their resources.",
|
|
429
|
+
promptSnippet: "List or control retained detached subagents",
|
|
430
|
+
parameters: ManageParamsSchema,
|
|
431
|
+
async execute(_id, params): Promise<StatefulActionToolResult> {
|
|
432
|
+
const operation = validateManageParams(params);
|
|
433
|
+
if (operation.action === "list") {
|
|
434
|
+
const agents = requireRegistry().list(operation.includeClosed);
|
|
493
435
|
return {
|
|
494
|
-
content: [
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
436
|
+
content: [
|
|
437
|
+
{
|
|
438
|
+
type: "text",
|
|
439
|
+
text: agents.length ? agents.map(formatLine).join("\n") : "No stateful subagents.",
|
|
440
|
+
},
|
|
441
|
+
],
|
|
442
|
+
details: { agents: agents.map(summarizeAgent) },
|
|
499
443
|
};
|
|
500
444
|
}
|
|
501
|
-
const
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
445
|
+
const agentId = operation.agentId;
|
|
446
|
+
if (operation.action === "interrupt") {
|
|
447
|
+
if (operation.subtree) {
|
|
448
|
+
const agents = await requireRegistry().interruptTree(agentId);
|
|
449
|
+
return {
|
|
450
|
+
content: [{ type: "text", text: `Interrupted ${agents.length} active agent(s).` }],
|
|
451
|
+
details: {
|
|
452
|
+
agent: summarizeAgent(requireAgent(agentId)),
|
|
453
|
+
agents: agents.map(summarizeAgent),
|
|
454
|
+
},
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
const agent = await requireRegistry().interrupt(agentId);
|
|
458
|
+
return result(agent, `Interrupted ${agent.id}; it remains reusable.`);
|
|
459
|
+
}
|
|
460
|
+
const existing = requireRegistry().get(agentId);
|
|
461
|
+
if (existing?.state === "closed" && !operation.subtree) {
|
|
517
462
|
const pendingOwner = isolatedAgents.get(existing.id);
|
|
518
463
|
if (pendingOwner) await workspaceManager.cleanup(pendingOwner);
|
|
519
464
|
isolatedAgents.delete(existing.id);
|
|
520
465
|
return result(existing, `Closed ${existing.id}.`);
|
|
521
466
|
}
|
|
522
|
-
if (
|
|
467
|
+
if (operation.subtree) {
|
|
523
468
|
let agents: ManagedAgent[];
|
|
524
469
|
try {
|
|
525
|
-
agents = await requireRegistry().closeTree(
|
|
470
|
+
agents = await requireRegistry().closeTree(agentId);
|
|
526
471
|
} finally {
|
|
527
472
|
await cleanupClosedWorkspaces(requireRegistry(), isolatedAgents, workspaceManager);
|
|
528
473
|
}
|
|
529
|
-
const root = requireRegistry().get(params.agentId);
|
|
530
|
-
if (!root) throw new Error(`Unknown subagent: ${params.agentId}`);
|
|
531
474
|
return {
|
|
532
475
|
content: [{ type: "text", text: `Closed ${agents.length} agent(s).` }],
|
|
533
476
|
details: {
|
|
534
|
-
agent: summarizeAgent(
|
|
477
|
+
agent: summarizeAgent(requireAgent(agentId)),
|
|
535
478
|
agents: agents.map(summarizeAgent),
|
|
536
479
|
},
|
|
537
480
|
};
|
|
538
481
|
}
|
|
539
482
|
let agent: ManagedAgent;
|
|
540
483
|
try {
|
|
541
|
-
agent = await requireRegistry().close(
|
|
484
|
+
agent = await requireRegistry().close(agentId);
|
|
542
485
|
} finally {
|
|
543
486
|
await cleanupClosedWorkspaces(requireRegistry(), isolatedAgents, workspaceManager);
|
|
544
487
|
}
|
|
@@ -546,6 +489,48 @@ export function registerStatefulSubagents(
|
|
|
546
489
|
},
|
|
547
490
|
});
|
|
548
491
|
|
|
492
|
+
pi.registerTool({
|
|
493
|
+
name: "subagent_mailbox",
|
|
494
|
+
label: "Subagent Mailbox",
|
|
495
|
+
description:
|
|
496
|
+
"Queue a bounded message without starting a turn, or read unread mailbox messages and optionally acknowledge them.",
|
|
497
|
+
promptSnippet: "Send or read queue-only detached-subagent mailbox messages",
|
|
498
|
+
parameters: MailboxParamsSchema,
|
|
499
|
+
async execute(_id, params): Promise<StatefulActionToolResult> {
|
|
500
|
+
const operation = validateMailboxParams(params);
|
|
501
|
+
if (operation.action === "send") {
|
|
502
|
+
const message = await requireRegistry().sendMessage(
|
|
503
|
+
operation.agentId,
|
|
504
|
+
operation.message,
|
|
505
|
+
operation.senderId,
|
|
506
|
+
operation.deduplicationKey,
|
|
507
|
+
);
|
|
508
|
+
return {
|
|
509
|
+
content: [{ type: "text", text: `Queued ${message.id} for ${message.recipientId}.` }],
|
|
510
|
+
details: { message },
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
const messages = await requireRegistry().readMessages(
|
|
514
|
+
operation.agentId,
|
|
515
|
+
operation.acknowledge,
|
|
516
|
+
operation.limit,
|
|
517
|
+
);
|
|
518
|
+
const summaries = messages.map((message) => ({
|
|
519
|
+
...message,
|
|
520
|
+
content: truncateUtf8(message.content, MAX_TOOL_MESSAGE_BYTES).text,
|
|
521
|
+
}));
|
|
522
|
+
const text = summaries.length
|
|
523
|
+
? summaries
|
|
524
|
+
.map((message) => `${message.id} from ${message.senderId}: ${message.content}`)
|
|
525
|
+
.join("\n")
|
|
526
|
+
: "No unread messages.";
|
|
527
|
+
return {
|
|
528
|
+
content: [{ type: "text", text: truncateUtf8(text, DEFAULT_MAX_CONTEXT_BYTES).text }],
|
|
529
|
+
details: { messages: summaries },
|
|
530
|
+
};
|
|
531
|
+
},
|
|
532
|
+
});
|
|
533
|
+
|
|
549
534
|
pi.registerCommand("subagents:agents", {
|
|
550
535
|
description: "Inspect or clear current-session subagents",
|
|
551
536
|
getArgumentCompletions(prefix: string) {
|