@oai404iao/pi-subagent 0.3.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 +250 -75
- package/agents/worker.md +1 -1
- package/config.example.json +3 -1
- package/config.schema.json +24 -4
- package/package.json +7 -7
- package/src/agent-state.ts +125 -0
- package/src/agent-sync.ts +64 -53
- package/src/agents.ts +3 -22
- package/src/catalog.ts +49 -0
- package/src/completion-mailbox.ts +656 -0
- package/src/config.ts +51 -6
- package/src/coordinator.ts +2385 -231
- package/src/descriptor.ts +104 -11
- package/src/index.ts +175 -26
- package/src/mailbox.ts +451 -0
- package/src/providers.ts +221 -28
- package/src/render.ts +25 -8
- package/src/scheduler.ts +173 -0
- package/src/schemas.ts +111 -11
- package/src/task-path.ts +188 -0
- package/src/types.ts +56 -7
package/src/descriptor.ts
CHANGED
|
@@ -3,25 +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}$/;
|
|
23
31
|
/** UUIDv7 with the standard RFC 9562 variant bits (version 7, variant 10xx). */
|
|
24
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
|
+
]);
|
|
25
38
|
|
|
26
39
|
type UnknownRecord = Record<string, unknown>;
|
|
27
40
|
|
|
@@ -97,7 +110,7 @@ function stringArray(value: unknown, field: string): string[] | undefined {
|
|
|
97
110
|
|
|
98
111
|
function parseAgent(value: unknown): AgentSnapshot {
|
|
99
112
|
const input = record(value, "agent");
|
|
100
|
-
const source = string(input.source, "agent.source") as
|
|
113
|
+
const source = string(input.source, "agent.source") as AgentSnapshotSource;
|
|
101
114
|
if (!AGENT_SOURCES.has(source)) throw new Error(`agent.source is unsupported: ${source}`);
|
|
102
115
|
const thinking = optionalString(input.thinking, "agent.thinking") as ThinkingLevel | undefined;
|
|
103
116
|
if (thinking && !THINKING_LEVELS.has(thinking)) throw new Error(`agent.thinking is unsupported: ${thinking}`);
|
|
@@ -116,11 +129,40 @@ function parseAgent(value: unknown): AgentSnapshot {
|
|
|
116
129
|
};
|
|
117
130
|
}
|
|
118
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
|
+
|
|
119
157
|
export function parseDescriptor(value: unknown): SubagentDescriptor {
|
|
120
158
|
const input = record(value, "descriptor");
|
|
121
|
-
if (
|
|
159
|
+
if (
|
|
160
|
+
input.version !== LEGACY_DESCRIPTOR_VERSION
|
|
161
|
+
&& input.version !== DESCRIPTOR_VERSION
|
|
162
|
+
) {
|
|
122
163
|
throw new Error(`unsupported descriptor version: ${String(input.version)}`);
|
|
123
164
|
}
|
|
165
|
+
const version = input.version;
|
|
124
166
|
const mode = string(input.mode, "mode") as SubagentMode;
|
|
125
167
|
if (!MODES.has(mode)) throw new Error(`unsupported descriptor mode: ${mode}`);
|
|
126
168
|
const provider = string(input.provider, "provider") as SubagentProviderName;
|
|
@@ -136,12 +178,23 @@ export function parseDescriptor(value: unknown): SubagentDescriptor {
|
|
|
136
178
|
if (!REPORT_DELIVERIES.has(reportDelivery)) {
|
|
137
179
|
throw new Error(`unsupported runtime.reportDelivery: ${reportDelivery}`);
|
|
138
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
|
+
}
|
|
139
193
|
|
|
140
194
|
const createdAt = string(input.createdAt, "createdAt");
|
|
141
195
|
if (Number.isNaN(Date.parse(createdAt))) throw new Error("createdAt must be an ISO date string");
|
|
142
196
|
|
|
143
|
-
|
|
144
|
-
version: DESCRIPTOR_VERSION,
|
|
197
|
+
const base = {
|
|
145
198
|
mode,
|
|
146
199
|
provider,
|
|
147
200
|
label: limitedString(input.label, "label", 200),
|
|
@@ -162,16 +215,31 @@ export function parseDescriptor(value: unknown): SubagentDescriptor {
|
|
|
162
215
|
thinkingLevel,
|
|
163
216
|
runtime: {
|
|
164
217
|
agentScope,
|
|
165
|
-
syncBundledAgents:
|
|
166
|
-
runtime.syncBundledAgents === undefined
|
|
167
|
-
? true
|
|
168
|
-
: boolean(runtime.syncBundledAgents, "runtime.syncBundledAgents"),
|
|
169
218
|
maxDepth: safeNatural(runtime.maxDepth, "runtime.maxDepth"),
|
|
170
219
|
enableRunInBackground:
|
|
171
220
|
runtime.enableRunInBackground === undefined
|
|
172
221
|
? true
|
|
173
222
|
: boolean(runtime.enableRunInBackground, "runtime.enableRunInBackground"),
|
|
174
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,
|
|
175
243
|
reportDelivery,
|
|
176
244
|
inheritExtensions: boolean(runtime.inheritExtensions, "runtime.inheritExtensions"),
|
|
177
245
|
openAIIdentity: boolean(runtime.openAIIdentity, "runtime.openAIIdentity"),
|
|
@@ -183,6 +251,22 @@ export function parseDescriptor(value: unknown): SubagentDescriptor {
|
|
|
183
251
|
),
|
|
184
252
|
},
|
|
185
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
|
+
};
|
|
186
270
|
}
|
|
187
271
|
|
|
188
272
|
export function foldDescriptor(entries: readonly SessionEntry[]): DescriptorFold {
|
|
@@ -200,3 +284,12 @@ export function foldDescriptor(entries: readonly SessionEntry[]): DescriptorFold
|
|
|
200
284
|
return { kind: "corrupt", message: error instanceof Error ? error.message : String(error) };
|
|
201
285
|
}
|
|
202
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
|
-
? "Delegate a complete
|
|
82
|
-
|
|
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. ") +
|
|
83
87
|
"Start independent children together in one assistant message."
|
|
84
|
-
: "Delegate a complete
|
|
85
|
-
"This tool waits for the result by default; set run_in_background to true to return a durable
|
|
86
|
-
const promptGuidelines = !enableRunInBackground
|
|
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,10 +418,13 @@ export default function subagentExtension(pi: ExtensionAPI): void {
|
|
|
307
418
|
: "foreground-first";
|
|
308
419
|
const sections = [
|
|
309
420
|
`Mode: ${schedulingMode}`,
|
|
421
|
+
`Background concurrency: ${sessionSettings.maxConcurrentBackgroundRuns}`,
|
|
422
|
+
`Idle runtime LRU: ${sessionSettings.maxIdleRuntimes}`,
|
|
423
|
+
`Background protocol: ${sessionSettings.backgroundProtocol ?? "legacy"}`,
|
|
310
424
|
`OpenAI identity inline: ${sessionSettings.openAIIdentity ? "enabled" : "disabled"}`,
|
|
311
|
-
|
|
312
|
-
?
|
|
313
|
-
:
|
|
425
|
+
agentSync?.diagnostics.length
|
|
426
|
+
? "Bundled templates: initialization skipped (see diagnostics)"
|
|
427
|
+
: `Bundled templates: initialization only (${agentSync?.packageVersion ?? "not initialized"})`,
|
|
314
428
|
`User agent dir: ${agentSync?.userAgentsDir ?? coordinator.getUserAgentsDir()}`,
|
|
315
429
|
`Agents:\n${formatAgentCatalog(discovery.agents)}`,
|
|
316
430
|
`Children:\n${coordinator.formatCatalog(entries, "descendants")}`,
|
|
@@ -324,10 +438,14 @@ export default function subagentExtension(pi: ExtensionAPI): void {
|
|
|
324
438
|
|
|
325
439
|
pi.on("session_start", async (_event, ctx) => {
|
|
326
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
|
+
);
|
|
327
447
|
sessionSettings = loaded.settings;
|
|
328
|
-
agentSync =
|
|
329
|
-
? coordinator.synchronizeBundledAgents()
|
|
330
|
-
: undefined;
|
|
448
|
+
agentSync = coordinator.synchronizeBundledAgents();
|
|
331
449
|
sessionDiscovery = coordinator.discoverAvailableAgents(
|
|
332
450
|
ctx.cwd,
|
|
333
451
|
sessionSettings,
|
|
@@ -348,6 +466,29 @@ export default function subagentExtension(pi: ExtensionAPI): void {
|
|
|
348
466
|
}
|
|
349
467
|
if (!sessionSettings.enableRunInBackground) {
|
|
350
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
|
+
}
|
|
351
492
|
}
|
|
352
493
|
if (!agentSyncNotified && agentSync) {
|
|
353
494
|
agentSyncNotified = true;
|
|
@@ -378,6 +519,14 @@ export default function subagentExtension(pi: ExtensionAPI): void {
|
|
|
378
519
|
}
|
|
379
520
|
});
|
|
380
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
|
+
|
|
381
530
|
pi.on("session_shutdown", async () => {
|
|
382
531
|
await coordinator.shutdown();
|
|
383
532
|
});
|