@oai404iao/pi-subagent 0.2.0 → 0.4.0-alpha.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 +266 -75
- package/agents/worker.md +1 -1
- package/config.example.json +4 -1
- package/config.schema.json +29 -4
- package/package.json +9 -6
- package/src/agent-state.ts +125 -0
- package/src/agent-sync.ts +64 -53
- package/src/agents.ts +3 -22
- package/src/catalog.ts +59 -7
- package/src/completion-mailbox.ts +656 -0
- package/src/config.ts +57 -6
- package/src/coordinator.ts +2476 -205
- package/src/descriptor.ts +118 -12
- package/src/index.ts +177 -27
- package/src/mailbox.ts +451 -0
- package/src/providers.ts +223 -28
- package/src/render.ts +25 -8
- package/src/scheduler.ts +173 -0
- package/src/schemas.ts +114 -11
- package/src/task-path.ts +188 -0
- package/src/types.ts +74 -15
package/src/descriptor.ts
CHANGED
|
@@ -3,23 +3,38 @@ import type { SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
|
3
3
|
import type {
|
|
4
4
|
AgentScope,
|
|
5
5
|
AgentSnapshot,
|
|
6
|
-
|
|
6
|
+
AgentSnapshotSource,
|
|
7
|
+
BackgroundProtocol,
|
|
8
|
+
ContextInheritance,
|
|
7
9
|
ReportDelivery,
|
|
8
10
|
SubagentDescriptor,
|
|
9
11
|
SubagentMode,
|
|
10
12
|
SubagentProviderName,
|
|
11
13
|
} from "./types.ts";
|
|
14
|
+
import { validateDescriptorTask } from "./task-path.ts";
|
|
12
15
|
|
|
13
16
|
export const DESCRIPTOR_CUSTOM_TYPE = "pi-subagent/descriptor";
|
|
14
|
-
export const DESCRIPTOR_VERSION =
|
|
17
|
+
export const DESCRIPTOR_VERSION = 3;
|
|
18
|
+
export const LEGACY_DESCRIPTOR_VERSION = 2;
|
|
19
|
+
const LEGACY_MAX_CONCURRENT_BACKGROUND_RUNS = 4;
|
|
20
|
+
const LEGACY_MAX_IDLE_RUNTIMES = 0;
|
|
21
|
+
const LEGACY_BACKGROUND_PROTOCOL: BackgroundProtocol = "legacy";
|
|
15
22
|
|
|
16
23
|
const THINKING_LEVELS = new Set<ThinkingLevel>(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
|
|
17
|
-
const AGENT_SOURCES = new Set<
|
|
24
|
+
const AGENT_SOURCES = new Set<AgentSnapshotSource>(["bundled", "user", "project"]);
|
|
18
25
|
const MODES = new Set<SubagentMode>(["one-shot", "continuable"]);
|
|
19
26
|
const PROVIDERS = new Set<SubagentProviderName>(["spawn", "fork"]);
|
|
20
27
|
const REPORT_DELIVERIES = new Set<ReportDelivery>(["wakeup", "quiet"]);
|
|
28
|
+
const BACKGROUND_PROTOCOLS = new Set<BackgroundProtocol>(["legacy", "mailbox-v2"]);
|
|
21
29
|
const AGENT_SCOPES = new Set<AgentScope>(["user", "project", "both"]);
|
|
22
30
|
const AGENT_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
|
|
31
|
+
/** UUIDv7 with the standard RFC 9562 variant bits (version 7, variant 10xx). */
|
|
32
|
+
const AGENT_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
33
|
+
const CONTEXT_MODES = new Set<ContextInheritance["mode"]>([
|
|
34
|
+
"fresh",
|
|
35
|
+
"all_completed",
|
|
36
|
+
"last_n_completed",
|
|
37
|
+
]);
|
|
23
38
|
|
|
24
39
|
type UnknownRecord = Record<string, unknown>;
|
|
25
40
|
|
|
@@ -46,6 +61,14 @@ function optionalString(value: unknown, field: string): string | undefined {
|
|
|
46
61
|
return value === undefined ? undefined : string(value, field);
|
|
47
62
|
}
|
|
48
63
|
|
|
64
|
+
function agentId(value: unknown, field: string): string {
|
|
65
|
+
const parsed = string(value, field);
|
|
66
|
+
if (!AGENT_ID_PATTERN.test(parsed)) {
|
|
67
|
+
throw new Error(`${field} must be a UUIDv7 agent id`);
|
|
68
|
+
}
|
|
69
|
+
return parsed;
|
|
70
|
+
}
|
|
71
|
+
|
|
49
72
|
function limitedString(value: unknown, field: string, maxLength: number): string {
|
|
50
73
|
const parsed = string(value, field);
|
|
51
74
|
if (parsed.length > maxLength) throw new Error(`${field} exceeds ${maxLength} characters`);
|
|
@@ -87,7 +110,7 @@ function stringArray(value: unknown, field: string): string[] | undefined {
|
|
|
87
110
|
|
|
88
111
|
function parseAgent(value: unknown): AgentSnapshot {
|
|
89
112
|
const input = record(value, "agent");
|
|
90
|
-
const source = string(input.source, "agent.source") as
|
|
113
|
+
const source = string(input.source, "agent.source") as AgentSnapshotSource;
|
|
91
114
|
if (!AGENT_SOURCES.has(source)) throw new Error(`agent.source is unsupported: ${source}`);
|
|
92
115
|
const thinking = optionalString(input.thinking, "agent.thinking") as ThinkingLevel | undefined;
|
|
93
116
|
if (thinking && !THINKING_LEVELS.has(thinking)) throw new Error(`agent.thinking is unsupported: ${thinking}`);
|
|
@@ -106,11 +129,40 @@ function parseAgent(value: unknown): AgentSnapshot {
|
|
|
106
129
|
};
|
|
107
130
|
}
|
|
108
131
|
|
|
132
|
+
function parseContext(value: unknown): ContextInheritance {
|
|
133
|
+
const input = record(value, "context");
|
|
134
|
+
const mode = string(input.mode, "context.mode") as ContextInheritance["mode"];
|
|
135
|
+
if (!CONTEXT_MODES.has(mode)) {
|
|
136
|
+
throw new Error(`unsupported context.mode: ${mode}`);
|
|
137
|
+
}
|
|
138
|
+
if (mode === "last_n_completed") {
|
|
139
|
+
return {
|
|
140
|
+
mode,
|
|
141
|
+
completedTurns: boundedInteger(
|
|
142
|
+
input.completedTurns,
|
|
143
|
+
"context.completedTurns",
|
|
144
|
+
1,
|
|
145
|
+
100,
|
|
146
|
+
),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
if (input.completedTurns !== undefined) {
|
|
150
|
+
throw new Error(
|
|
151
|
+
"context.completedTurns is available only for last_n_completed",
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
return { mode };
|
|
155
|
+
}
|
|
156
|
+
|
|
109
157
|
export function parseDescriptor(value: unknown): SubagentDescriptor {
|
|
110
158
|
const input = record(value, "descriptor");
|
|
111
|
-
if (
|
|
159
|
+
if (
|
|
160
|
+
input.version !== LEGACY_DESCRIPTOR_VERSION
|
|
161
|
+
&& input.version !== DESCRIPTOR_VERSION
|
|
162
|
+
) {
|
|
112
163
|
throw new Error(`unsupported descriptor version: ${String(input.version)}`);
|
|
113
164
|
}
|
|
165
|
+
const version = input.version;
|
|
114
166
|
const mode = string(input.mode, "mode") as SubagentMode;
|
|
115
167
|
if (!MODES.has(mode)) throw new Error(`unsupported descriptor mode: ${mode}`);
|
|
116
168
|
const provider = string(input.provider, "provider") as SubagentProviderName;
|
|
@@ -126,16 +178,29 @@ export function parseDescriptor(value: unknown): SubagentDescriptor {
|
|
|
126
178
|
if (!REPORT_DELIVERIES.has(reportDelivery)) {
|
|
127
179
|
throw new Error(`unsupported runtime.reportDelivery: ${reportDelivery}`);
|
|
128
180
|
}
|
|
181
|
+
const backgroundProtocol =
|
|
182
|
+
runtime.backgroundProtocol === undefined
|
|
183
|
+
? LEGACY_BACKGROUND_PROTOCOL
|
|
184
|
+
: string(runtime.backgroundProtocol, "runtime.backgroundProtocol") as BackgroundProtocol;
|
|
185
|
+
if (!BACKGROUND_PROTOCOLS.has(backgroundProtocol)) {
|
|
186
|
+
throw new Error(`unsupported runtime.backgroundProtocol: ${backgroundProtocol}`);
|
|
187
|
+
}
|
|
188
|
+
if (runtime.syncBundledAgents !== undefined) {
|
|
189
|
+
// Validate, then discard the retired 0.2/0.3 runtime switch. Keeping this
|
|
190
|
+
// read compatibility allows persisted children to cold-resume.
|
|
191
|
+
boolean(runtime.syncBundledAgents, "runtime.syncBundledAgents");
|
|
192
|
+
}
|
|
129
193
|
|
|
130
194
|
const createdAt = string(input.createdAt, "createdAt");
|
|
131
195
|
if (Number.isNaN(Date.parse(createdAt))) throw new Error("createdAt must be an ISO date string");
|
|
132
196
|
|
|
133
|
-
|
|
134
|
-
version: DESCRIPTOR_VERSION,
|
|
197
|
+
const base = {
|
|
135
198
|
mode,
|
|
136
199
|
provider,
|
|
137
200
|
label: limitedString(input.label, "label", 200),
|
|
138
|
-
|
|
201
|
+
agentId: agentId(input.agentId, "agentId"),
|
|
202
|
+
parentAgentId: agentId(input.parentAgentId, "parentAgentId"),
|
|
203
|
+
parentPiSessionId: string(input.parentPiSessionId, "parentPiSessionId"),
|
|
139
204
|
...(optionalString(input.parentSessionFile, "parentSessionFile")
|
|
140
205
|
? { parentSessionFile: input.parentSessionFile as string }
|
|
141
206
|
: {}),
|
|
@@ -150,18 +215,34 @@ export function parseDescriptor(value: unknown): SubagentDescriptor {
|
|
|
150
215
|
thinkingLevel,
|
|
151
216
|
runtime: {
|
|
152
217
|
agentScope,
|
|
153
|
-
syncBundledAgents:
|
|
154
|
-
runtime.syncBundledAgents === undefined
|
|
155
|
-
? true
|
|
156
|
-
: boolean(runtime.syncBundledAgents, "runtime.syncBundledAgents"),
|
|
157
218
|
maxDepth: safeNatural(runtime.maxDepth, "runtime.maxDepth"),
|
|
158
219
|
enableRunInBackground:
|
|
159
220
|
runtime.enableRunInBackground === undefined
|
|
160
221
|
? true
|
|
161
222
|
: boolean(runtime.enableRunInBackground, "runtime.enableRunInBackground"),
|
|
162
223
|
defaultBackground: boolean(runtime.defaultBackground, "runtime.defaultBackground"),
|
|
224
|
+
maxConcurrentBackgroundRuns:
|
|
225
|
+
runtime.maxConcurrentBackgroundRuns === undefined
|
|
226
|
+
? LEGACY_MAX_CONCURRENT_BACKGROUND_RUNS
|
|
227
|
+
: boundedInteger(
|
|
228
|
+
runtime.maxConcurrentBackgroundRuns,
|
|
229
|
+
"runtime.maxConcurrentBackgroundRuns",
|
|
230
|
+
1,
|
|
231
|
+
Number.MAX_SAFE_INTEGER,
|
|
232
|
+
),
|
|
233
|
+
maxIdleRuntimes:
|
|
234
|
+
runtime.maxIdleRuntimes === undefined
|
|
235
|
+
? LEGACY_MAX_IDLE_RUNTIMES
|
|
236
|
+
: boundedInteger(
|
|
237
|
+
runtime.maxIdleRuntimes,
|
|
238
|
+
"runtime.maxIdleRuntimes",
|
|
239
|
+
0,
|
|
240
|
+
Number.MAX_SAFE_INTEGER,
|
|
241
|
+
),
|
|
242
|
+
backgroundProtocol,
|
|
163
243
|
reportDelivery,
|
|
164
244
|
inheritExtensions: boolean(runtime.inheritExtensions, "runtime.inheritExtensions"),
|
|
245
|
+
openAIIdentity: boolean(runtime.openAIIdentity, "runtime.openAIIdentity"),
|
|
165
246
|
maxOutputBytes: boundedInteger(
|
|
166
247
|
runtime.maxOutputBytes,
|
|
167
248
|
"runtime.maxOutputBytes",
|
|
@@ -170,6 +251,22 @@ export function parseDescriptor(value: unknown): SubagentDescriptor {
|
|
|
170
251
|
),
|
|
171
252
|
},
|
|
172
253
|
};
|
|
254
|
+
if (version === LEGACY_DESCRIPTOR_VERSION) {
|
|
255
|
+
return {
|
|
256
|
+
version: LEGACY_DESCRIPTOR_VERSION,
|
|
257
|
+
...base,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
const task = record(input.task, "task");
|
|
261
|
+
return {
|
|
262
|
+
version: DESCRIPTOR_VERSION,
|
|
263
|
+
...base,
|
|
264
|
+
task: validateDescriptorTask({
|
|
265
|
+
name: string(task.name, "task.name"),
|
|
266
|
+
path: string(task.path, "task.path"),
|
|
267
|
+
}),
|
|
268
|
+
context: parseContext(input.context),
|
|
269
|
+
};
|
|
173
270
|
}
|
|
174
271
|
|
|
175
272
|
export function foldDescriptor(entries: readonly SessionEntry[]): DescriptorFold {
|
|
@@ -187,3 +284,12 @@ export function foldDescriptor(entries: readonly SessionEntry[]): DescriptorFold
|
|
|
187
284
|
return { kind: "corrupt", message: error instanceof Error ? error.message : String(error) };
|
|
188
285
|
}
|
|
189
286
|
}
|
|
287
|
+
|
|
288
|
+
export function descriptorContext(
|
|
289
|
+
descriptor: SubagentDescriptor,
|
|
290
|
+
): ContextInheritance {
|
|
291
|
+
if (descriptor.version === 3) return structuredClone(descriptor.context);
|
|
292
|
+
return descriptor.provider === "fork"
|
|
293
|
+
? { mode: "all_completed" }
|
|
294
|
+
: { mode: "fresh" };
|
|
295
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -14,9 +14,11 @@ import {
|
|
|
14
14
|
} from "./agents.ts";
|
|
15
15
|
import type { AgentSyncResult } from "./agent-sync.ts";
|
|
16
16
|
import {
|
|
17
|
+
FollowupTaskParameters,
|
|
17
18
|
InterruptParameters,
|
|
18
19
|
ListAgentsParameters,
|
|
19
20
|
SendMessageParameters,
|
|
21
|
+
WaitAgentParameters,
|
|
20
22
|
delegationParameters,
|
|
21
23
|
forkDelegationParameters,
|
|
22
24
|
} from "./schemas.ts";
|
|
@@ -74,16 +76,18 @@ function registerDelegationTool(
|
|
|
74
76
|
const { enableRunInBackground, defaultBackground } = settings;
|
|
75
77
|
const agentNames = agentDiscovery?.agents.map((agent) => agent.name);
|
|
76
78
|
const description = !enableRunInBackground
|
|
77
|
-
? "Delegate a complete
|
|
79
|
+
? "Delegate a complete task to a named child path with selectable completed-turn context. " +
|
|
78
80
|
"This foreground-only tool waits for the child and returns its final answer. " +
|
|
79
81
|
"Independent sibling calls may still execute in parallel."
|
|
80
82
|
: defaultBackground
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
"
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
83
|
+
? "Delegate a complete task to a named child path with selectable completed-turn context. " +
|
|
84
|
+
(settings.backgroundProtocol === "mailbox-v2"
|
|
85
|
+
? "Background mode is continuable and returns a readable path plus durable id; use send_message to enqueue, followup_task to start, and wait_agent for quiet completions. "
|
|
86
|
+
: "Background mode is continuable and returns a readable path plus durable id; use send_message for later FIFO turns. ") +
|
|
87
|
+
"Start independent children together in one assistant message."
|
|
88
|
+
: "Delegate a complete task to a named child path with selectable completed-turn context. " +
|
|
89
|
+
"This tool waits for the result by default; set run_in_background to true to return a readable path plus durable id.";
|
|
90
|
+
const promptGuidelines = (!enableRunInBackground
|
|
87
91
|
? [
|
|
88
92
|
"Use subagent for focused independent work and give it a complete standalone prompt.",
|
|
89
93
|
"This subagent tool is foreground-only: every call waits for and returns the child's final answer.",
|
|
@@ -99,14 +103,17 @@ function registerDelegationTool(
|
|
|
99
103
|
"Use subagent for focused independent work and give it a complete standalone prompt.",
|
|
100
104
|
"Subagent calls wait for the result by default; request background mode only when work can continue independently.",
|
|
101
105
|
"Independent subagent calls can still be issued together in one assistant message and execute in parallel.",
|
|
102
|
-
]
|
|
106
|
+
]).concat([
|
|
107
|
+
"Set task_name when a stable readable path will help later control calls; otherwise a name is generated from description.",
|
|
108
|
+
"Use fresh context by default and request all_completed or last_n_completed only when parent history materially helps.",
|
|
109
|
+
]);
|
|
103
110
|
const parameters = delegationParameters(enableRunInBackground, agentNames);
|
|
104
111
|
pi.registerTool({
|
|
105
112
|
name: "subagent",
|
|
106
113
|
label: "Subagent",
|
|
107
114
|
description,
|
|
108
115
|
promptSnippet: enableRunInBackground
|
|
109
|
-
? "Delegate focused
|
|
116
|
+
? "Delegate focused work to named child agents"
|
|
110
117
|
: "Run focused independent work in foreground child agents",
|
|
111
118
|
promptGuidelines,
|
|
112
119
|
executionMode: "parallel",
|
|
@@ -150,13 +157,16 @@ function registerForkDelegationTool(
|
|
|
150
157
|
agentDiscovery?: AgentDiscoveryResult,
|
|
151
158
|
): unknown {
|
|
152
159
|
const agentNames = agentDiscovery?.agents.map((agent) => agent.name);
|
|
153
|
-
const parameters = forkDelegationParameters(
|
|
160
|
+
const parameters = forkDelegationParameters(
|
|
161
|
+
agentNames,
|
|
162
|
+
settings.enableRunInBackground,
|
|
163
|
+
);
|
|
154
164
|
pi.registerTool({
|
|
155
165
|
name: "subagent_fork",
|
|
156
166
|
label: "Subagent Fork",
|
|
157
167
|
description:
|
|
158
|
-
"Delegate a
|
|
159
|
-
"The current in-flight tool-calling turn is excluded.
|
|
168
|
+
"Delegate a task to a child seeded with all completed turns in this conversation. " +
|
|
169
|
+
"The current in-flight tool-calling turn is excluded. It is foreground by default; request background mode for a continuable fork.",
|
|
160
170
|
promptSnippet: "Delegate context-dependent work to a child seeded with completed turns",
|
|
161
171
|
promptGuidelines: [
|
|
162
172
|
"Use subagent_fork only when completed conversation history materially helps the delegated task.",
|
|
@@ -206,6 +216,8 @@ export default function subagentExtension(pi: ExtensionAPI): void {
|
|
|
206
216
|
registerForkDelegationTool(pi, coordinator, DEFAULT_SETTINGS);
|
|
207
217
|
const backgroundControlParameters = new Map<string, unknown>([
|
|
208
218
|
["send_message", SendMessageParameters],
|
|
219
|
+
["followup_task", FollowupTaskParameters],
|
|
220
|
+
["wait_agent", WaitAgentParameters],
|
|
209
221
|
["interrupt_agent", InterruptParameters],
|
|
210
222
|
["list_agents", ListAgentsParameters],
|
|
211
223
|
]);
|
|
@@ -214,23 +226,114 @@ export default function subagentExtension(pi: ExtensionAPI): void {
|
|
|
214
226
|
name: "send_message",
|
|
215
227
|
label: "Send Message",
|
|
216
228
|
description:
|
|
217
|
-
"
|
|
229
|
+
"Send a message to a direct continuable child. Legacy children start or join a FIFO turn; mailbox-v2 children only durably enqueue it and require followup_task to execute. " +
|
|
218
230
|
"This call returns acceptance only, never the child's answer.",
|
|
219
|
-
promptSnippet: "Send
|
|
231
|
+
promptSnippet: "Send or enqueue a message for a direct continuable subagent",
|
|
220
232
|
executionMode: "parallel",
|
|
221
233
|
parameters: SendMessageParameters,
|
|
222
234
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
223
235
|
assertBackgroundControlsEnabled(sessionSettings, "send_message");
|
|
224
236
|
const parent = await coordinator.parentFromContext(ctx);
|
|
225
|
-
await coordinator.
|
|
237
|
+
const delivery = await coordinator.sendMessageWithOutcome(
|
|
238
|
+
parent,
|
|
239
|
+
params.subagent_id,
|
|
240
|
+
params.message,
|
|
241
|
+
signal,
|
|
242
|
+
);
|
|
226
243
|
return {
|
|
227
244
|
content: [
|
|
228
245
|
{
|
|
229
246
|
type: "text",
|
|
230
|
-
text:
|
|
247
|
+
text:
|
|
248
|
+
delivery.kind === "mailbox-v2"
|
|
249
|
+
? `message ${delivery.messageId} durably enqueued for ${delivery.taskPath}; ${delivery.pendingMessages} pending`
|
|
250
|
+
: `message queued as the next turn for ${delivery.taskPath}`,
|
|
231
251
|
},
|
|
232
252
|
],
|
|
233
|
-
details: {
|
|
253
|
+
details: {
|
|
254
|
+
kind: "control",
|
|
255
|
+
action: "send",
|
|
256
|
+
agentId: delivery.agentId,
|
|
257
|
+
taskPath: delivery.taskPath,
|
|
258
|
+
...(delivery.kind === "mailbox-v2"
|
|
259
|
+
? {
|
|
260
|
+
messageId: delivery.messageId,
|
|
261
|
+
pendingMessages: delivery.pendingMessages,
|
|
262
|
+
}
|
|
263
|
+
: {}),
|
|
264
|
+
} satisfies ControlDetails,
|
|
265
|
+
};
|
|
266
|
+
},
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
pi.registerTool({
|
|
270
|
+
name: "followup_task",
|
|
271
|
+
label: "Follow-up Task",
|
|
272
|
+
description:
|
|
273
|
+
"For a direct mailbox-v2 continuable child, atomically claim its current pending FIFO mailbox and start exactly one scheduled turn. " +
|
|
274
|
+
"This call returns turn acceptance, not the child's answer.",
|
|
275
|
+
promptSnippet: "Start one mailbox-v2 child turn from queued messages",
|
|
276
|
+
executionMode: "parallel",
|
|
277
|
+
parameters: FollowupTaskParameters,
|
|
278
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
279
|
+
assertBackgroundControlsEnabled(sessionSettings, "followup_task");
|
|
280
|
+
const parent = await coordinator.parentFromContext(ctx);
|
|
281
|
+
const outcome = await coordinator.followupTask(
|
|
282
|
+
parent,
|
|
283
|
+
params.subagent_id,
|
|
284
|
+
signal,
|
|
285
|
+
);
|
|
286
|
+
return {
|
|
287
|
+
content: [
|
|
288
|
+
{
|
|
289
|
+
type: "text",
|
|
290
|
+
text: `started turn ${outcome.turnId} for ${outcome.taskPath}, claiming ${outcome.claimedMessages} mailbox message${outcome.claimedMessages === 1 ? "" : "s"}`,
|
|
291
|
+
},
|
|
292
|
+
],
|
|
293
|
+
details: {
|
|
294
|
+
kind: "control",
|
|
295
|
+
action: "followup",
|
|
296
|
+
agentId: outcome.agentId,
|
|
297
|
+
taskPath: outcome.taskPath,
|
|
298
|
+
turnId: outcome.turnId,
|
|
299
|
+
claimedMessages: outcome.claimedMessages,
|
|
300
|
+
} satisfies ControlDetails,
|
|
301
|
+
};
|
|
302
|
+
},
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
pi.registerTool({
|
|
306
|
+
name: "wait_agent",
|
|
307
|
+
label: "Wait Agent",
|
|
308
|
+
description:
|
|
309
|
+
"Wait event-driven for unread completion updates from direct mailbox-v2 children. Existing updates return immediately; timeout does not consume later updates.",
|
|
310
|
+
promptSnippet: "Wait for quiet mailbox-v2 child completion updates",
|
|
311
|
+
parameters: WaitAgentParameters,
|
|
312
|
+
async execute(toolCallId, params, signal, _onUpdate, ctx) {
|
|
313
|
+
assertBackgroundControlsEnabled(sessionSettings, "wait_agent");
|
|
314
|
+
const parent = await coordinator.parentFromContext(ctx);
|
|
315
|
+
const outcome = await coordinator.waitAgent(
|
|
316
|
+
parent,
|
|
317
|
+
toolCallId,
|
|
318
|
+
params.timeout_ms,
|
|
319
|
+
signal,
|
|
320
|
+
);
|
|
321
|
+
return {
|
|
322
|
+
content: [
|
|
323
|
+
{
|
|
324
|
+
type: "text",
|
|
325
|
+
text: coordinator.formatWaitAgentOutcome(outcome),
|
|
326
|
+
},
|
|
327
|
+
],
|
|
328
|
+
details: {
|
|
329
|
+
kind: "control",
|
|
330
|
+
action: "wait",
|
|
331
|
+
timedOut: outcome.timedOut,
|
|
332
|
+
completionIds: outcome.updates.map(
|
|
333
|
+
(update) => update.completionId,
|
|
334
|
+
),
|
|
335
|
+
unreadUpdates: outcome.unreadUpdates,
|
|
336
|
+
} satisfies ControlDetails,
|
|
234
337
|
};
|
|
235
338
|
},
|
|
236
339
|
});
|
|
@@ -247,10 +350,18 @@ export default function subagentExtension(pi: ExtensionAPI): void {
|
|
|
247
350
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
248
351
|
assertBackgroundControlsEnabled(sessionSettings, "interrupt_agent");
|
|
249
352
|
const parent = await coordinator.parentFromContext(ctx);
|
|
250
|
-
await coordinator.
|
|
353
|
+
const outcome = await coordinator.interruptWithOutcome(
|
|
354
|
+
parent,
|
|
355
|
+
params.agent_id,
|
|
356
|
+
);
|
|
251
357
|
return {
|
|
252
|
-
content: [{ type: "text", text: `interrupt requested for
|
|
253
|
-
details: {
|
|
358
|
+
content: [{ type: "text", text: `interrupt requested for ${outcome.taskPath}` }],
|
|
359
|
+
details: {
|
|
360
|
+
kind: "control",
|
|
361
|
+
action: "interrupt",
|
|
362
|
+
agentId: outcome.agentId,
|
|
363
|
+
taskPath: outcome.taskPath,
|
|
364
|
+
} satisfies ControlDetails,
|
|
254
365
|
};
|
|
255
366
|
},
|
|
256
367
|
});
|
|
@@ -260,7 +371,7 @@ export default function subagentExtension(pi: ExtensionAPI): void {
|
|
|
260
371
|
label: "List Agents",
|
|
261
372
|
description:
|
|
262
373
|
"List direct continuable children or all descendants. running means an active turn, idle means resident between turns, " +
|
|
263
|
-
"
|
|
374
|
+
"ready means persisted and cold-resumable, and mailbox-v2 children show pending task messages separately from unread completion updates.",
|
|
264
375
|
promptSnippet: "List continuable child agents and their lifecycle status",
|
|
265
376
|
parameters: ListAgentsParameters,
|
|
266
377
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
@@ -307,9 +418,13 @@ export default function subagentExtension(pi: ExtensionAPI): void {
|
|
|
307
418
|
: "foreground-first";
|
|
308
419
|
const sections = [
|
|
309
420
|
`Mode: ${schedulingMode}`,
|
|
310
|
-
sessionSettings.
|
|
311
|
-
|
|
312
|
-
|
|
421
|
+
`Background concurrency: ${sessionSettings.maxConcurrentBackgroundRuns}`,
|
|
422
|
+
`Idle runtime LRU: ${sessionSettings.maxIdleRuntimes}`,
|
|
423
|
+
`Background protocol: ${sessionSettings.backgroundProtocol ?? "legacy"}`,
|
|
424
|
+
`OpenAI identity inline: ${sessionSettings.openAIIdentity ? "enabled" : "disabled"}`,
|
|
425
|
+
agentSync?.diagnostics.length
|
|
426
|
+
? "Bundled templates: initialization skipped (see diagnostics)"
|
|
427
|
+
: `Bundled templates: initialization only (${agentSync?.packageVersion ?? "not initialized"})`,
|
|
313
428
|
`User agent dir: ${agentSync?.userAgentsDir ?? coordinator.getUserAgentsDir()}`,
|
|
314
429
|
`Agents:\n${formatAgentCatalog(discovery.agents)}`,
|
|
315
430
|
`Children:\n${coordinator.formatCatalog(entries, "descendants")}`,
|
|
@@ -323,10 +438,14 @@ export default function subagentExtension(pi: ExtensionAPI): void {
|
|
|
323
438
|
|
|
324
439
|
pi.on("session_start", async (_event, ctx) => {
|
|
325
440
|
const loaded = loadSettings({ cwd: ctx.cwd, projectTrusted: ctx.isProjectTrusted() });
|
|
441
|
+
coordinator.configureBackgroundRuns(
|
|
442
|
+
loaded.settings.maxConcurrentBackgroundRuns,
|
|
443
|
+
);
|
|
444
|
+
await coordinator.configureIdleRuntimes(
|
|
445
|
+
loaded.settings.maxIdleRuntimes,
|
|
446
|
+
);
|
|
326
447
|
sessionSettings = loaded.settings;
|
|
327
|
-
agentSync =
|
|
328
|
-
? coordinator.synchronizeBundledAgents()
|
|
329
|
-
: undefined;
|
|
448
|
+
agentSync = coordinator.synchronizeBundledAgents();
|
|
330
449
|
sessionDiscovery = coordinator.discoverAvailableAgents(
|
|
331
450
|
ctx.cwd,
|
|
332
451
|
sessionSettings,
|
|
@@ -347,6 +466,29 @@ export default function subagentExtension(pi: ExtensionAPI): void {
|
|
|
347
466
|
}
|
|
348
467
|
if (!sessionSettings.enableRunInBackground) {
|
|
349
468
|
disableOwnedTools(pi, backgroundControlParameters);
|
|
469
|
+
} else if (sessionSettings.backgroundProtocol !== "mailbox-v2") {
|
|
470
|
+
let hasMailboxChild = true;
|
|
471
|
+
try {
|
|
472
|
+
const parent = await coordinator.parentFromContext(ctx);
|
|
473
|
+
const entries = await coordinator.list(parent, "descendants");
|
|
474
|
+
hasMailboxChild = entries.some(
|
|
475
|
+
(entry) =>
|
|
476
|
+
entry.kind === "child"
|
|
477
|
+
&& entry.descriptor.runtime.backgroundProtocol === "mailbox-v2",
|
|
478
|
+
);
|
|
479
|
+
} catch {
|
|
480
|
+
// Keep the control available when catalog inspection fails so a
|
|
481
|
+
// transient diagnostic cannot strand an existing mailbox child.
|
|
482
|
+
}
|
|
483
|
+
if (!hasMailboxChild) {
|
|
484
|
+
disableOwnedTools(
|
|
485
|
+
pi,
|
|
486
|
+
new Map<string, unknown>([
|
|
487
|
+
["followup_task", FollowupTaskParameters],
|
|
488
|
+
["wait_agent", WaitAgentParameters],
|
|
489
|
+
]),
|
|
490
|
+
);
|
|
491
|
+
}
|
|
350
492
|
}
|
|
351
493
|
if (!agentSyncNotified && agentSync) {
|
|
352
494
|
agentSyncNotified = true;
|
|
@@ -377,6 +519,14 @@ export default function subagentExtension(pi: ExtensionAPI): void {
|
|
|
377
519
|
}
|
|
378
520
|
});
|
|
379
521
|
|
|
522
|
+
pi.on("agent_end", async (_event, ctx) => {
|
|
523
|
+
const parent = await coordinator.parentFromContext(ctx);
|
|
524
|
+
await coordinator.releaseWaitAgentDeliveries(
|
|
525
|
+
parent,
|
|
526
|
+
"parent agent turn ended without a durable wait_agent result",
|
|
527
|
+
);
|
|
528
|
+
});
|
|
529
|
+
|
|
380
530
|
pi.on("session_shutdown", async () => {
|
|
381
531
|
await coordinator.shutdown();
|
|
382
532
|
});
|