@howarewoo/pi-subagents 0.0.0-stage → 0.1.1
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 +31 -0
- package/LICENSE +21 -0
- package/README.md +328 -2
- package/THIRD_PARTY_NOTICES.md +25 -0
- package/docs/advanced-policies.md +76 -0
- package/docs/delegation-contract.md +268 -0
- package/docs/images/subagent-hub.svg +1 -0
- package/docs/images/subagent-profiles.svg +1 -0
- package/docs/images/subagent-widget.svg +1 -0
- package/docs/parity-inventory.md +330 -0
- package/docs/provenance.md +13 -0
- package/docs/releases.md +148 -0
- package/docs/verification.md +430 -0
- package/package.json +64 -4
- package/src/capabilities.ts +39 -0
- package/src/coordinator.ts +1432 -0
- package/src/hub.ts +841 -0
- package/src/model-adapter.ts +77 -0
- package/src/model-policy.ts +100 -0
- package/src/outcomes.ts +110 -0
- package/src/pi-extended-subagents.ts +779 -0
- package/src/profile-viewer.ts +175 -0
- package/src/profiles.ts +225 -0
- package/src/scheduling.ts +73 -0
- package/src/task-input.ts +73 -0
- package/src/thread.ts +268 -0
- package/src/workspace.ts +46 -0
|
@@ -0,0 +1,779 @@
|
|
|
1
|
+
import { createProfileViewer, resolvedProfiles, profileToolPolicy } from "./profile-viewer.ts";
|
|
2
|
+
import { Text, truncateToWidth } from "@earendil-works/pi-tui";
|
|
3
|
+
import { EFFORTS, sameSelection } from "./model-policy.ts";
|
|
4
|
+
import { clippedText } from "./outcomes.ts";
|
|
5
|
+
import { readTaskInput } from "./task-input.ts";
|
|
6
|
+
import { resolveWorkspace } from "./workspace.ts";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { Type } from "typebox";
|
|
9
|
+
import {
|
|
10
|
+
truncateHead,
|
|
11
|
+
type ExtensionToolContext,
|
|
12
|
+
type ToolDefinition,
|
|
13
|
+
type ToolRenderers,
|
|
14
|
+
type ExtensionAPI,
|
|
15
|
+
type BuildSystemPromptOptions,
|
|
16
|
+
} from "@earendil-works/pi-coding-agent";
|
|
17
|
+
import { resolveScheduling } from "./scheduling.ts";
|
|
18
|
+
import {
|
|
19
|
+
DelegationCoordinator,
|
|
20
|
+
type ChildRef,
|
|
21
|
+
type ChildSnapshot,
|
|
22
|
+
type StartTask,
|
|
23
|
+
} from "./coordinator.ts";
|
|
24
|
+
import {
|
|
25
|
+
READ_TOOLS,
|
|
26
|
+
resolveProfile,
|
|
27
|
+
bundledProfiles,
|
|
28
|
+
COORDINATION_ACTIONS,
|
|
29
|
+
intersectProfileGrants,
|
|
30
|
+
} from "./profiles.ts";
|
|
31
|
+
import { createChildWidget, createHub, safe, taskCard } from "./hub.ts";
|
|
32
|
+
|
|
33
|
+
export default function delegation(pi: ExtensionAPI) {
|
|
34
|
+
let coordinator: DelegationCoordinator;
|
|
35
|
+
let closing = false;
|
|
36
|
+
let parentPromptOptions: BuildSystemPromptOptions | undefined;
|
|
37
|
+
const availableSkills = () => parentPromptOptions?.skills ?? [];
|
|
38
|
+
pi.on("before_agent_start", (event) => {
|
|
39
|
+
parentPromptOptions = event.systemPromptOptions;
|
|
40
|
+
});
|
|
41
|
+
let renderWidget: (() => void) | undefined;
|
|
42
|
+
let unsubscribeWidget: (() => void) | undefined;
|
|
43
|
+
const ref = (args: string): ChildRef => {
|
|
44
|
+
const [id, generation] = args.trim().split(/\s+/);
|
|
45
|
+
if (!id || !Number.isSafeInteger(Number(generation)))
|
|
46
|
+
throw new Error("Expected <task-id> <generation>");
|
|
47
|
+
return { id, generation: Number(generation) };
|
|
48
|
+
};
|
|
49
|
+
const preview = (value: unknown, transcript?: string) => {
|
|
50
|
+
const snapshots = (Array.isArray(value) ? value : [value]).filter(
|
|
51
|
+
(item): item is ChildSnapshot =>
|
|
52
|
+
!!item &&
|
|
53
|
+
typeof item === "object" &&
|
|
54
|
+
"id" in item &&
|
|
55
|
+
"generation" in item &&
|
|
56
|
+
"status" in item,
|
|
57
|
+
);
|
|
58
|
+
const references = snapshots.map((state) => ({
|
|
59
|
+
id: state.id,
|
|
60
|
+
generation: state.generation,
|
|
61
|
+
status: state.status,
|
|
62
|
+
transcript: state.transcript,
|
|
63
|
+
artifacts: state.artifacts,
|
|
64
|
+
}));
|
|
65
|
+
const text = JSON.stringify(
|
|
66
|
+
value,
|
|
67
|
+
(key, item) => {
|
|
68
|
+
if (
|
|
69
|
+
["result", "assignment", "activity", "progress", "error"].includes(key) &&
|
|
70
|
+
typeof item === "string"
|
|
71
|
+
)
|
|
72
|
+
return clippedText(item);
|
|
73
|
+
if (key === "value") return clippedText(JSON.stringify(item));
|
|
74
|
+
return item;
|
|
75
|
+
},
|
|
76
|
+
2,
|
|
77
|
+
);
|
|
78
|
+
const clipped = truncateHead(
|
|
79
|
+
(references.length
|
|
80
|
+
? `References: ${JSON.stringify(references)}\n`
|
|
81
|
+
: transcript
|
|
82
|
+
? `Transcript: ${transcript}\n`
|
|
83
|
+
: "") + text,
|
|
84
|
+
);
|
|
85
|
+
return `${clipped.content}${clipped.truncated ? "\n[Preview truncated; inspect full artifact/transcript references or subagent-status for one identity]" : ""}`;
|
|
86
|
+
};
|
|
87
|
+
const compatible = (state: ChildSnapshot) => ({
|
|
88
|
+
...state,
|
|
89
|
+
taskId: state.id,
|
|
90
|
+
id: `${state.id}@${state.generation}`,
|
|
91
|
+
});
|
|
92
|
+
const show = (value: unknown, transcript?: string, wake = false) =>
|
|
93
|
+
pi.sendMessage(
|
|
94
|
+
{ customType: "delegation", content: preview(value, transcript), display: true },
|
|
95
|
+
wake ? { triggerTurn: true, deliverAs: "followUp" } : undefined,
|
|
96
|
+
);
|
|
97
|
+
pi.on("session_start", (_event, ctx) => {
|
|
98
|
+
coordinator = new DelegationCoordinator(
|
|
99
|
+
join(ctx.sessionManager.getSessionDir(), "delegation", ctx.sessionManager.getSessionId()),
|
|
100
|
+
ctx.sessionManager.getSessionId(),
|
|
101
|
+
);
|
|
102
|
+
unsubscribeWidget = coordinator.subscribe((event) => {
|
|
103
|
+
renderWidget?.();
|
|
104
|
+
pi.events.emit("delegation:registry", event);
|
|
105
|
+
});
|
|
106
|
+
if (ctx.mode !== "tui") return;
|
|
107
|
+
ctx.ui.setWidget(
|
|
108
|
+
"delegation",
|
|
109
|
+
(tui, theme) => {
|
|
110
|
+
renderWidget = () => tui.requestRender();
|
|
111
|
+
return createChildWidget(
|
|
112
|
+
() => coordinator.list(),
|
|
113
|
+
theme,
|
|
114
|
+
() => tui.terminal.rows,
|
|
115
|
+
);
|
|
116
|
+
},
|
|
117
|
+
{ placement: "aboveEditor" },
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
pi.on("session_shutdown", async (_event, ctx) => {
|
|
121
|
+
closing = true;
|
|
122
|
+
renderWidget = undefined;
|
|
123
|
+
if (ctx.mode === "tui") ctx.ui.setWidget("delegation", undefined);
|
|
124
|
+
await coordinator?.shutdown();
|
|
125
|
+
unsubscribeWidget?.();
|
|
126
|
+
});
|
|
127
|
+
pi.on("tool_call", (event) => {
|
|
128
|
+
try {
|
|
129
|
+
const builtinRead =
|
|
130
|
+
READ_TOOLS.includes(event.toolName as (typeof READ_TOOLS)[number]) &&
|
|
131
|
+
pi
|
|
132
|
+
.getAllTools()
|
|
133
|
+
.some(
|
|
134
|
+
(tool) =>
|
|
135
|
+
tool.name === event.toolName && tool.sourceInfo.path === `builtin:${event.toolName}`,
|
|
136
|
+
);
|
|
137
|
+
const tools = pi.getAllTools();
|
|
138
|
+
const ownSource = tools.find((tool) => tool.name === "delegate_task")?.sourceInfo.path;
|
|
139
|
+
const ownCompatibilityCall =
|
|
140
|
+
event.toolName === "subagent" &&
|
|
141
|
+
ownSource !== undefined &&
|
|
142
|
+
tools.some((tool) => tool.name === "subagent" && tool.sourceInfo.path === ownSource);
|
|
143
|
+
coordinator.parentToolStart(
|
|
144
|
+
event.toolCallId,
|
|
145
|
+
event.toolName,
|
|
146
|
+
builtinRead || ownCompatibilityCall,
|
|
147
|
+
);
|
|
148
|
+
} catch (error) {
|
|
149
|
+
return { block: true, reason: String(error) };
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
pi.on("tool_execution_end", (event) => coordinator.parentToolEnd(event.toolCallId));
|
|
153
|
+
const assertTools = (names: readonly string[]) => {
|
|
154
|
+
for (const name of names) {
|
|
155
|
+
const tool = pi.getAllTools().find((tool) => tool.name === name);
|
|
156
|
+
if (!tool || tool.sourceInfo.path !== `builtin:${name}`)
|
|
157
|
+
throw new Error(`Delegation requires an unmodified built-in parent tool: ${name}`);
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
const role = Type.Union([
|
|
161
|
+
Type.Literal("general"),
|
|
162
|
+
Type.Literal("scout"),
|
|
163
|
+
Type.Literal("review"),
|
|
164
|
+
Type.Literal("implementation"),
|
|
165
|
+
]);
|
|
166
|
+
const limits = Type.Object(
|
|
167
|
+
{
|
|
168
|
+
depth: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
169
|
+
spawns: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
170
|
+
deadlineMs: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
171
|
+
requests: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
172
|
+
tools: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
173
|
+
},
|
|
174
|
+
{ additionalProperties: false },
|
|
175
|
+
);
|
|
176
|
+
const modelSelection = Type.Object(
|
|
177
|
+
{
|
|
178
|
+
provider: Type.String({ minLength: 1 }),
|
|
179
|
+
id: Type.String({ minLength: 1 }),
|
|
180
|
+
effort: Type.Enum(EFFORTS),
|
|
181
|
+
},
|
|
182
|
+
{ additionalProperties: false },
|
|
183
|
+
);
|
|
184
|
+
const assignment = {
|
|
185
|
+
cwd: Type.Optional(
|
|
186
|
+
Type.String({
|
|
187
|
+
minLength: 1,
|
|
188
|
+
description:
|
|
189
|
+
"Existing workspace; absolute or relative to root parent cwd. No new permissions or sandbox.",
|
|
190
|
+
}),
|
|
191
|
+
),
|
|
192
|
+
models: Type.Optional(Type.Array(modelSelection, { maxItems: 64 })),
|
|
193
|
+
model: Type.Optional(modelSelection),
|
|
194
|
+
assignment: Type.Optional(Type.String({ minLength: 1 })),
|
|
195
|
+
taskFile: Type.Optional(Type.String({ minLength: 1 })),
|
|
196
|
+
resultSchema: Type.Optional(Type.Unknown()),
|
|
197
|
+
profile: role,
|
|
198
|
+
spawn: Type.Optional(Type.Array(role)),
|
|
199
|
+
coordination: Type.Optional(
|
|
200
|
+
Type.Array(Type.Union(COORDINATION_ACTIONS.map((action) => Type.Literal(action)))),
|
|
201
|
+
),
|
|
202
|
+
limits: Type.Optional(limits),
|
|
203
|
+
};
|
|
204
|
+
const item = Type.Object(assignment, { additionalProperties: false });
|
|
205
|
+
const parameters = Type.Union([
|
|
206
|
+
Type.Object(
|
|
207
|
+
{ ...assignment, background: Type.Optional(Type.Boolean()) },
|
|
208
|
+
{ additionalProperties: false },
|
|
209
|
+
),
|
|
210
|
+
Type.Object(
|
|
211
|
+
{
|
|
212
|
+
tasks: Type.Array(item, { minItems: 1, maxItems: 64 }),
|
|
213
|
+
background: Type.Optional(Type.Boolean()),
|
|
214
|
+
},
|
|
215
|
+
{ additionalProperties: false },
|
|
216
|
+
),
|
|
217
|
+
]);
|
|
218
|
+
const taskTool = (
|
|
219
|
+
parent?: ChildRef,
|
|
220
|
+
rootContext?: ExtensionToolContext,
|
|
221
|
+
rootCallerId?: string,
|
|
222
|
+
compatibility = false,
|
|
223
|
+
): ToolDefinition<typeof parameters> => ({
|
|
224
|
+
name: "delegate_task",
|
|
225
|
+
label: "Delegate task",
|
|
226
|
+
exposure: "model-only",
|
|
227
|
+
promptSnippet: "Delegate coding, review or research to native subagents in supplied workspaces",
|
|
228
|
+
promptGuidelines: [
|
|
229
|
+
"Use delegate_task for authorized native delegation with explicit profiles. When subagent is available, prefer subagent for orchestration task owners and its async completion wake-ups. Supply the existing task workspace cwd; do not substitute inline implementation when a workflow requires a worker.",
|
|
230
|
+
],
|
|
231
|
+
description:
|
|
232
|
+
"Delegate one assignment or a fully preflighted ordered tasks batch on the selected parent model by default. Optional exact model {provider,id,effort} requires an operator profile models grant and stays exact, suppressing inherited role prewalk; models may only narrow that grant list. No aliases/fallback/coercion. Configured role-default prewalk switches once after first edit/write, not a repository scan. Coding workers are supported through implementation; advisor/probes/speculation/history/executable resource inheritance remain unavailable. Optional cwd selects an existing workspace: root hooks see explicitly resolved builtin paths and a cwd-prefixed shell command; this is not sandboxing or transparent hook-cwd rebinding. Role tools are ceilings intersected with callable parent builtins; inactive tools stay inactive. Snapshots report effective tools. Bounded FIFO capacity; effective implementation writers serialize. Manual !/!! and external edits are uncoordinated: never conflict with child writes. Foreground waits; background delivers one final batch message. Nested foreground only, explicit spawn grants and available capacity required. Limits may only narrow operator policy. Supply exactly one inline assignment or taskFile (explicit non-hidden workspace-relative UTF-8 file, at most 50000 bytes, checked through parent read hooks). Optional resultSchema requires child submit_result; prose is never validated output. Previews are bounded; full result artifacts and transcripts retained after drain.",
|
|
233
|
+
parameters,
|
|
234
|
+
renderCall(args, theme) {
|
|
235
|
+
return {
|
|
236
|
+
render: (width) =>
|
|
237
|
+
[
|
|
238
|
+
theme.fg(
|
|
239
|
+
"toolTitle",
|
|
240
|
+
`Delegate · ${"tasks" in args ? `batch ${args.tasks?.length ?? 0}` : safe(args.profile ?? "profile pending")} · ${args.background ? "background" : "foreground"}`,
|
|
241
|
+
),
|
|
242
|
+
safe(
|
|
243
|
+
"tasks" in args
|
|
244
|
+
? "Ordered batch · expand results for each identity"
|
|
245
|
+
: (args.assignment ?? args.taskFile ?? "Receiving assignment…"),
|
|
246
|
+
).replaceAll("\n", " "),
|
|
247
|
+
].map((line) => truncateToWidth(line, Math.max(0, width))),
|
|
248
|
+
invalidate() {},
|
|
249
|
+
};
|
|
250
|
+
},
|
|
251
|
+
renderResult(result, options, theme) {
|
|
252
|
+
const stored = result.details as ChildSnapshot | { children: ChildSnapshot[] } | undefined;
|
|
253
|
+
const children =
|
|
254
|
+
stored && "children" in stored ? stored.children : stored && "id" in stored ? [stored] : [];
|
|
255
|
+
return {
|
|
256
|
+
render(width) {
|
|
257
|
+
const lines = children.length
|
|
258
|
+
? children.flatMap((snapshot) => {
|
|
259
|
+
let child = snapshot;
|
|
260
|
+
try {
|
|
261
|
+
child = coordinator?.snapshot(snapshot) ?? snapshot;
|
|
262
|
+
} catch {
|
|
263
|
+
/* delivered snapshots survive runtime replacement */
|
|
264
|
+
}
|
|
265
|
+
return taskCard(child, options.expanded);
|
|
266
|
+
})
|
|
267
|
+
: [
|
|
268
|
+
safe(
|
|
269
|
+
result.content
|
|
270
|
+
.filter((block) => block.type === "text")
|
|
271
|
+
.map((block) => block.text)
|
|
272
|
+
.join("\n"),
|
|
273
|
+
),
|
|
274
|
+
];
|
|
275
|
+
return new Text(lines.map((line) => theme.fg("text", line)).join("\n"), 0, 0).render(
|
|
276
|
+
width,
|
|
277
|
+
);
|
|
278
|
+
},
|
|
279
|
+
invalidate() {},
|
|
280
|
+
};
|
|
281
|
+
},
|
|
282
|
+
async execute(callerId, args, signal, onUpdate, localContext) {
|
|
283
|
+
const ctx = rootContext ?? localContext;
|
|
284
|
+
const owner = coordinator;
|
|
285
|
+
if (parent && args.background)
|
|
286
|
+
throw new Error("Nested background delegation is unsupported; use foreground");
|
|
287
|
+
const policy = await resolveScheduling(ctx.cwd, ctx.isProjectTrusted());
|
|
288
|
+
owner.configure(policy);
|
|
289
|
+
const items = "tasks" in args ? args.tasks : [args];
|
|
290
|
+
const inputs: StartTask[] = [];
|
|
291
|
+
for (const task of items) {
|
|
292
|
+
if ((task.assignment === undefined) === (task.taskFile === undefined))
|
|
293
|
+
throw new Error("Supply exactly one of assignment or taskFile");
|
|
294
|
+
const cwd = resolveWorkspace(
|
|
295
|
+
task.cwd ?? (parent ? owner.snapshot(parent)?.cwd : undefined),
|
|
296
|
+
ctx.cwd,
|
|
297
|
+
);
|
|
298
|
+
let { profile, skills } = await resolveProfile(
|
|
299
|
+
task.profile,
|
|
300
|
+
ctx.cwd,
|
|
301
|
+
ctx.isProjectTrusted(),
|
|
302
|
+
undefined,
|
|
303
|
+
availableSkills(),
|
|
304
|
+
);
|
|
305
|
+
if (task.spawn) {
|
|
306
|
+
if (task.spawn.some((role) => !profile.spawn?.includes(role)))
|
|
307
|
+
throw new Error("Request cannot mint spawn grants");
|
|
308
|
+
profile = { ...profile, spawn: task.spawn };
|
|
309
|
+
}
|
|
310
|
+
if (task.coordination) {
|
|
311
|
+
if (task.coordination.some((action) => !profile.coordination?.includes(action)))
|
|
312
|
+
throw new Error("Request cannot mint coordination grants");
|
|
313
|
+
profile = { ...profile, coordination: task.coordination };
|
|
314
|
+
}
|
|
315
|
+
if (task.models) {
|
|
316
|
+
if (
|
|
317
|
+
task.models.some(
|
|
318
|
+
(selection) => !profile.models?.some((grant) => sameSelection(grant, selection)),
|
|
319
|
+
)
|
|
320
|
+
)
|
|
321
|
+
throw new Error("Request cannot mint model grants");
|
|
322
|
+
profile = { ...profile, models: task.models };
|
|
323
|
+
}
|
|
324
|
+
let text = task.assignment;
|
|
325
|
+
if (task.taskFile !== undefined) {
|
|
326
|
+
assertTools(["read"]);
|
|
327
|
+
text = await readTaskInput(
|
|
328
|
+
task.taskFile,
|
|
329
|
+
ctx,
|
|
330
|
+
signal,
|
|
331
|
+
parent ? () => owner.taskInputRead(parent) : undefined,
|
|
332
|
+
cwd,
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
inputs.push({
|
|
336
|
+
cwd,
|
|
337
|
+
model: task.model,
|
|
338
|
+
assignment: text!,
|
|
339
|
+
resultSchema: task.resultSchema,
|
|
340
|
+
profile,
|
|
341
|
+
skills,
|
|
342
|
+
context: ctx,
|
|
343
|
+
assertTools,
|
|
344
|
+
toolMetadata: () => pi.getAllTools(),
|
|
345
|
+
availableSkills,
|
|
346
|
+
callerId: rootCallerId ?? callerId,
|
|
347
|
+
parent,
|
|
348
|
+
limits: task.limits,
|
|
349
|
+
coordinationTools: (actor) => [coordinationTool(owner, actor)],
|
|
350
|
+
nestedTool: (ref: ChildRef) => taskTool(ref, ctx, rootCallerId ?? callerId),
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
signal?.throwIfAborted();
|
|
354
|
+
const children = owner.admit(inputs);
|
|
355
|
+
const stop = () => owner.stopMany(children);
|
|
356
|
+
const abort = () => {
|
|
357
|
+
void stop();
|
|
358
|
+
};
|
|
359
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
360
|
+
if (signal?.aborted) await stop();
|
|
361
|
+
const states = () => children.map((child) => owner.snapshot(child)!);
|
|
362
|
+
const result = (snapshots: ChildSnapshot[]) => {
|
|
363
|
+
if (compatibility) snapshots = snapshots.map(compatible);
|
|
364
|
+
const value = "tasks" in args ? snapshots : snapshots[0];
|
|
365
|
+
return {
|
|
366
|
+
content: [
|
|
367
|
+
{
|
|
368
|
+
type: "text" as const,
|
|
369
|
+
text: preview(value, snapshots.length === 1 ? snapshots[0].transcript : undefined),
|
|
370
|
+
},
|
|
371
|
+
],
|
|
372
|
+
details: "tasks" in args ? { children: snapshots } : snapshots[0],
|
|
373
|
+
isError: snapshots.some(
|
|
374
|
+
(state) => state.status === "failed" || state.status === "cancelled",
|
|
375
|
+
),
|
|
376
|
+
};
|
|
377
|
+
};
|
|
378
|
+
const completed = Promise.all(children.map((child) => owner.wait(child)));
|
|
379
|
+
if (args.background) {
|
|
380
|
+
signal?.removeEventListener("abort", abort);
|
|
381
|
+
void completed.then((snapshots) => {
|
|
382
|
+
if (compatibility) snapshots = snapshots.map(compatible);
|
|
383
|
+
if (!closing && coordinator === owner)
|
|
384
|
+
show(
|
|
385
|
+
"tasks" in args ? snapshots : snapshots[0],
|
|
386
|
+
snapshots.length === 1 ? snapshots[0].transcript : undefined,
|
|
387
|
+
compatibility,
|
|
388
|
+
);
|
|
389
|
+
});
|
|
390
|
+
return result(states());
|
|
391
|
+
}
|
|
392
|
+
const unsubscribe = owner.subscribe((event) => {
|
|
393
|
+
if (children.some((child) => child.id === event.child.id)) onUpdate?.(result(states()));
|
|
394
|
+
});
|
|
395
|
+
try {
|
|
396
|
+
return result(await completed);
|
|
397
|
+
} finally {
|
|
398
|
+
unsubscribe();
|
|
399
|
+
signal?.removeEventListener("abort", abort);
|
|
400
|
+
}
|
|
401
|
+
},
|
|
402
|
+
});
|
|
403
|
+
const identity = Type.Object(
|
|
404
|
+
{ id: Type.String({ minLength: 1 }), generation: Type.Integer({ minimum: 1 }) },
|
|
405
|
+
{ additionalProperties: false },
|
|
406
|
+
);
|
|
407
|
+
const coordinationParameters = Type.Union([
|
|
408
|
+
Type.Object(
|
|
409
|
+
{
|
|
410
|
+
action: Type.Literal("message"),
|
|
411
|
+
target: Type.Optional(identity),
|
|
412
|
+
text: Type.String({ minLength: 1, maxLength: 4096 }),
|
|
413
|
+
mode: Type.Optional(
|
|
414
|
+
Type.Union([Type.Literal("inbox"), Type.Literal("steer"), Type.Literal("followUp")]),
|
|
415
|
+
),
|
|
416
|
+
},
|
|
417
|
+
{ additionalProperties: false },
|
|
418
|
+
),
|
|
419
|
+
Type.Object({ action: Type.Literal("inbox") }, { additionalProperties: false }),
|
|
420
|
+
Type.Object(
|
|
421
|
+
{
|
|
422
|
+
action: Type.Literal("wait"),
|
|
423
|
+
target: Type.Optional(identity),
|
|
424
|
+
messageId: Type.Optional(Type.String({ minLength: 1 })),
|
|
425
|
+
timeoutMs: Type.Integer({ minimum: 1, maximum: 60000 }),
|
|
426
|
+
},
|
|
427
|
+
{ additionalProperties: false },
|
|
428
|
+
),
|
|
429
|
+
Type.Object(
|
|
430
|
+
{
|
|
431
|
+
action: Type.Union([Type.Literal("park"), Type.Literal("stop"), Type.Literal("status")]),
|
|
432
|
+
target: identity,
|
|
433
|
+
},
|
|
434
|
+
{ additionalProperties: false },
|
|
435
|
+
),
|
|
436
|
+
Type.Object(
|
|
437
|
+
{
|
|
438
|
+
action: Type.Union([Type.Literal("followup"), Type.Literal("revive")]),
|
|
439
|
+
target: identity,
|
|
440
|
+
assignment: Type.String({ minLength: 1 }),
|
|
441
|
+
resultSchema: Type.Optional(Type.Unknown()),
|
|
442
|
+
},
|
|
443
|
+
{ additionalProperties: false },
|
|
444
|
+
),
|
|
445
|
+
]);
|
|
446
|
+
const coordinationTool = (
|
|
447
|
+
owner?: DelegationCoordinator,
|
|
448
|
+
actor?: ChildRef,
|
|
449
|
+
): ToolDefinition<typeof coordinationParameters> => ({
|
|
450
|
+
name: "delegate_coordinate",
|
|
451
|
+
label: "Coordinate delegation",
|
|
452
|
+
exposure: "model-only",
|
|
453
|
+
description:
|
|
454
|
+
"Live-parent identity-pinned coordination. message targets a running family agent (omit target for root inbox, direct children only); inbox reads own bounded mailbox; wait targets an outcome or own inbox with required 1–60000ms timeout, retaining capacity. No transcript access. Child actions require explicit operator grants. Root additionally has status/stop/park and idle followup/parked revive. Followup/revive create a fresh generation/session with current model/auth and narrowed current grants, no old tool replay or historical recovery. Message accepted/queued is NOT model acknowledgment; delivered means transcript/inbox visibility only. 4096-byte messages, 64 mailbox/waiters, 512 messages and 64 generations per live parent.",
|
|
455
|
+
parameters: coordinationParameters,
|
|
456
|
+
async execute(callerId, args, signal, _update, ctx) {
|
|
457
|
+
const registry = owner ?? coordinator;
|
|
458
|
+
signal?.throwIfAborted();
|
|
459
|
+
let value: unknown;
|
|
460
|
+
if (args.action === "message")
|
|
461
|
+
value = await registry.message(actor, args.target, args.text, args.mode);
|
|
462
|
+
else if (args.action === "inbox") value = registry.inbox(actor);
|
|
463
|
+
else if (args.action === "wait")
|
|
464
|
+
value = await registry.coordinateWait(
|
|
465
|
+
actor,
|
|
466
|
+
args.target,
|
|
467
|
+
args.timeoutMs,
|
|
468
|
+
signal,
|
|
469
|
+
args.messageId,
|
|
470
|
+
);
|
|
471
|
+
else {
|
|
472
|
+
if (actor) throw new Error("Root-only coordination action");
|
|
473
|
+
if (args.action === "stop") {
|
|
474
|
+
await registry.stop(args.target);
|
|
475
|
+
value = registry.snapshot(args.target);
|
|
476
|
+
} else if (args.action === "park") {
|
|
477
|
+
registry.park(args.target);
|
|
478
|
+
value = registry.agents();
|
|
479
|
+
} else if (args.action === "status")
|
|
480
|
+
value = {
|
|
481
|
+
snapshot: registry.snapshot(args.target),
|
|
482
|
+
generations: registry.generations(args.target.id),
|
|
483
|
+
agents: registry.agents(),
|
|
484
|
+
};
|
|
485
|
+
else if (args.action === "followup" || args.action === "revive") {
|
|
486
|
+
const presence = registry
|
|
487
|
+
.agents()
|
|
488
|
+
.find(
|
|
489
|
+
(state) => state.id === args.target.id && state.generation === args.target.generation,
|
|
490
|
+
)?.presence;
|
|
491
|
+
if (presence !== (args.action === "revive" ? "parked" : "idle"))
|
|
492
|
+
throw new Error("Followup requires idle; revive requires parked current identity");
|
|
493
|
+
const state = registry.snapshot(args.target)!;
|
|
494
|
+
const parent = registry.renewalParent(args.target);
|
|
495
|
+
registry.configure(await resolveScheduling(ctx.cwd, ctx.isProjectTrusted()));
|
|
496
|
+
let { profile, skills } = await resolveProfile(
|
|
497
|
+
state.profile,
|
|
498
|
+
ctx.cwd,
|
|
499
|
+
ctx.isProjectTrusted(),
|
|
500
|
+
undefined,
|
|
501
|
+
availableSkills(),
|
|
502
|
+
);
|
|
503
|
+
let ancestor = parent;
|
|
504
|
+
let descendantRole = state.profile;
|
|
505
|
+
while (ancestor) {
|
|
506
|
+
const original = registry.snapshot(ancestor)!;
|
|
507
|
+
const current = (
|
|
508
|
+
await resolveProfile(
|
|
509
|
+
original.profile,
|
|
510
|
+
ctx.cwd,
|
|
511
|
+
ctx.isProjectTrusted(),
|
|
512
|
+
undefined,
|
|
513
|
+
availableSkills(),
|
|
514
|
+
)
|
|
515
|
+
).profile;
|
|
516
|
+
if (!current.spawn?.includes(descendantRole))
|
|
517
|
+
throw new Error("Current ancestor spawn role denied");
|
|
518
|
+
profile = intersectProfileGrants(profile, current);
|
|
519
|
+
descendantRole = original.profile;
|
|
520
|
+
ancestor =
|
|
521
|
+
original.parentGeneration === undefined
|
|
522
|
+
? undefined
|
|
523
|
+
: { id: original.parentId, generation: original.parentGeneration };
|
|
524
|
+
}
|
|
525
|
+
signal?.throwIfAborted();
|
|
526
|
+
const next = registry.renew(args.target, {
|
|
527
|
+
cwd: state.cwd,
|
|
528
|
+
assignment: args.assignment,
|
|
529
|
+
resultSchema: args.resultSchema,
|
|
530
|
+
profile,
|
|
531
|
+
skills,
|
|
532
|
+
context: ctx,
|
|
533
|
+
assertTools,
|
|
534
|
+
toolMetadata: () => pi.getAllTools(),
|
|
535
|
+
availableSkills,
|
|
536
|
+
callerId,
|
|
537
|
+
parent,
|
|
538
|
+
coordinationTools: (ref) => [coordinationTool(registry, ref)],
|
|
539
|
+
nestedTool: (ref) => taskTool(ref, ctx, callerId),
|
|
540
|
+
});
|
|
541
|
+
value = registry.snapshot(next);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
return { content: [{ type: "text", text: preview(value) }], details: value };
|
|
545
|
+
},
|
|
546
|
+
});
|
|
547
|
+
pi.registerTool(coordinationTool());
|
|
548
|
+
const taskDefinition = taskTool();
|
|
549
|
+
pi.registerTool(taskDefinition);
|
|
550
|
+
const compatibleTaskDefinition = taskTool(undefined, undefined, undefined, true);
|
|
551
|
+
const worker = Type.Union([role, Type.Literal("worker")]);
|
|
552
|
+
const compatibleItem = Type.Object(
|
|
553
|
+
{
|
|
554
|
+
agent: worker,
|
|
555
|
+
task: Type.String({ minLength: 1 }),
|
|
556
|
+
cwd: Type.Optional(
|
|
557
|
+
Type.String({
|
|
558
|
+
minLength: 1,
|
|
559
|
+
description:
|
|
560
|
+
"Existing workspace; absolute or relative to root parent cwd. Omit to inherit the call default.",
|
|
561
|
+
}),
|
|
562
|
+
),
|
|
563
|
+
model: Type.Optional(Type.Union([modelSelection, Type.String({ minLength: 1 })])),
|
|
564
|
+
limits: Type.Optional(limits),
|
|
565
|
+
},
|
|
566
|
+
{ additionalProperties: false },
|
|
567
|
+
);
|
|
568
|
+
const compatibleParameters = Type.Union([
|
|
569
|
+
Type.Object(
|
|
570
|
+
{ ...compatibleItem.properties, async: Type.Optional(Type.Boolean()) },
|
|
571
|
+
{ additionalProperties: false },
|
|
572
|
+
),
|
|
573
|
+
Type.Object(
|
|
574
|
+
{
|
|
575
|
+
tasks: Type.Array(compatibleItem, { minItems: 1, maxItems: 64 }),
|
|
576
|
+
async: Type.Optional(Type.Boolean()),
|
|
577
|
+
cwd: Type.Optional(Type.String({ minLength: 1 })),
|
|
578
|
+
},
|
|
579
|
+
{ additionalProperties: false },
|
|
580
|
+
),
|
|
581
|
+
Type.Object(
|
|
582
|
+
{ action: Type.Union([Type.Literal("list"), Type.Literal("agents")]) },
|
|
583
|
+
{ additionalProperties: false },
|
|
584
|
+
),
|
|
585
|
+
Type.Object(
|
|
586
|
+
{
|
|
587
|
+
action: Type.Union([Type.Literal("status"), Type.Literal("stop")]),
|
|
588
|
+
id: Type.String({ minLength: 1 }),
|
|
589
|
+
},
|
|
590
|
+
{ additionalProperties: false },
|
|
591
|
+
),
|
|
592
|
+
Type.Object(
|
|
593
|
+
{
|
|
594
|
+
action: Type.Union([Type.Literal("steer"), Type.Literal("resume")]),
|
|
595
|
+
id: Type.String({ minLength: 1 }),
|
|
596
|
+
message: Type.String({ minLength: 1 }),
|
|
597
|
+
},
|
|
598
|
+
{ additionalProperties: false },
|
|
599
|
+
),
|
|
600
|
+
]);
|
|
601
|
+
pi.registerTool({
|
|
602
|
+
name: "subagent",
|
|
603
|
+
label: "Subagent",
|
|
604
|
+
exposure: "model-only",
|
|
605
|
+
promptSnippet:
|
|
606
|
+
"Native coding workers: launch, coordinate and resume work in existing task workspaces",
|
|
607
|
+
promptGuidelines: [
|
|
608
|
+
"Use subagent for native worker delegation requested by the user or an active workflow; no extension-specific user instruction or enable call is needed. Use agent worker for implementation, scout for research, review for review, or general for restricted inherited capabilities.",
|
|
609
|
+
"Pass subagent the approved task handoff and existing workspace cwd. Preserve selected model routing; no model override is needed to inherit the parent. Pass accessible skill/reference paths in the task; general inherits available skills and callable builtins, plus operator-reviewed leaf tools only; parent instructions/history and project trust are not inherited.",
|
|
610
|
+
"subagent launches are async by default and wake this session after actual drain. Continue independent work or yield; do not poll or sleep. Use the exact returned id (UUID@generation) for status, steer, stop or resume. Resume starts a fresh conversation in the same workspace; include needed context.",
|
|
611
|
+
],
|
|
612
|
+
description:
|
|
613
|
+
"Native delegation for orchestration and coding task owners. Launch {agent,task,cwd?,model?,async?} or an ordered {tasks:[...],async?,cwd?} batch. worker maps explicitly to the implementation profile; unknown/custom agent names reject. Model strings require exact provider/id[:effort] and operator grants. Actions: agents, list, status/stop with exact returned id, steer/resume with id and message. Uses the same registry, parent permissions, limits and global writer lease as delegate_task. Existing workspaces only: file paths and shell cwd are explicitly adapted through root builtin hooks. No automatic worktree creation/commits/apply/deletion, detached survival, script workflows or blanket competing API parity. Do not load another provider of the subagent tool alongside this extension.",
|
|
614
|
+
parameters: compatibleParameters,
|
|
615
|
+
async execute(callerId, args, signal, onUpdate, ctx) {
|
|
616
|
+
const owner = coordinator;
|
|
617
|
+
if (!("action" in args)) {
|
|
618
|
+
const items = "tasks" in args ? args.tasks : [args];
|
|
619
|
+
const tasks = items.map((task) => {
|
|
620
|
+
let model = typeof task.model === "object" ? task.model : undefined;
|
|
621
|
+
if (typeof task.model === "string") {
|
|
622
|
+
const slash = task.model.indexOf("/");
|
|
623
|
+
if (slash < 1 || slash === task.model.length - 1)
|
|
624
|
+
throw new Error("Model strings require exact provider/id[:effort]");
|
|
625
|
+
let id = task.model.slice(slash + 1);
|
|
626
|
+
const colon = id.lastIndexOf(":");
|
|
627
|
+
const effort =
|
|
628
|
+
colon >= 0 ? EFFORTS.find((level) => level === id.slice(colon + 1)) : undefined;
|
|
629
|
+
if (effort) id = id.slice(0, colon);
|
|
630
|
+
model = {
|
|
631
|
+
provider: task.model.slice(0, slash),
|
|
632
|
+
id,
|
|
633
|
+
effort: effort ?? ctx.thinkingLevel ?? "off",
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
return {
|
|
637
|
+
assignment: task.task,
|
|
638
|
+
profile: task.agent === "worker" ? ("implementation" as const) : task.agent,
|
|
639
|
+
cwd: task.cwd ?? args.cwd,
|
|
640
|
+
model,
|
|
641
|
+
limits: task.limits,
|
|
642
|
+
};
|
|
643
|
+
});
|
|
644
|
+
// Internal reuse: the outer subagent call has its own public validation/hooks;
|
|
645
|
+
// every executed builtin still traverses the original root dispatcher.
|
|
646
|
+
return compatibleTaskDefinition.execute(
|
|
647
|
+
callerId,
|
|
648
|
+
"tasks" in args
|
|
649
|
+
? { tasks, background: args.async ?? true }
|
|
650
|
+
: { ...tasks[0], background: args.async ?? true },
|
|
651
|
+
signal,
|
|
652
|
+
onUpdate,
|
|
653
|
+
ctx,
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
const result = (value: unknown) => ({
|
|
657
|
+
content: [{ type: "text" as const, text: preview(value) }],
|
|
658
|
+
details: value,
|
|
659
|
+
});
|
|
660
|
+
if (!("id" in args))
|
|
661
|
+
return result(
|
|
662
|
+
args.action === "agents"
|
|
663
|
+
? { agents: [...bundledProfiles(), { name: "worker", profile: "implementation" }] }
|
|
664
|
+
: owner.list().map(compatible),
|
|
665
|
+
);
|
|
666
|
+
const match = /^([0-9a-f-]{36})@([1-9][0-9]*)$/.exec(args.id);
|
|
667
|
+
if (!match || !Number.isSafeInteger(Number(match[2])))
|
|
668
|
+
throw new Error("Use the exact subagent id returned by launch: UUID@generation");
|
|
669
|
+
const target = { id: match[1], generation: Number(match[2]) };
|
|
670
|
+
if (args.action === "status")
|
|
671
|
+
return result({
|
|
672
|
+
snapshot: compatible(owner.snapshot(target)!),
|
|
673
|
+
generations: owner.generations(target.id).map(compatible),
|
|
674
|
+
});
|
|
675
|
+
signal?.throwIfAborted();
|
|
676
|
+
if (args.action === "stop") await owner.stop(target);
|
|
677
|
+
else if (args.action === "steer") await owner.steer(target, args.message);
|
|
678
|
+
else if ("message" in args) {
|
|
679
|
+
const presence = owner
|
|
680
|
+
.agents()
|
|
681
|
+
.find(
|
|
682
|
+
(state) => state.id === target.id && state.generation === target.generation,
|
|
683
|
+
)?.presence;
|
|
684
|
+
const response = await coordinationTool().execute(
|
|
685
|
+
callerId,
|
|
686
|
+
{
|
|
687
|
+
action: presence === "parked" ? "revive" : "followup",
|
|
688
|
+
target,
|
|
689
|
+
assignment: args.message,
|
|
690
|
+
},
|
|
691
|
+
signal,
|
|
692
|
+
undefined,
|
|
693
|
+
ctx,
|
|
694
|
+
);
|
|
695
|
+
const next = response.details as ChildSnapshot;
|
|
696
|
+
void owner.wait(next).then((state) => {
|
|
697
|
+
if (!closing && coordinator === owner) show(compatible(state), state.transcript, true);
|
|
698
|
+
});
|
|
699
|
+
return result(compatible(next));
|
|
700
|
+
}
|
|
701
|
+
return result(compatible(owner.snapshot(target)!));
|
|
702
|
+
},
|
|
703
|
+
});
|
|
704
|
+
pi.registerCommand("subagent-profiles", {
|
|
705
|
+
description: "Subagent Profiles: inspect resolved workspace profiles (read-only)",
|
|
706
|
+
handler: async (_args, ctx) => {
|
|
707
|
+
const profiles = await resolvedProfiles(
|
|
708
|
+
ctx.cwd,
|
|
709
|
+
ctx.isProjectTrusted(),
|
|
710
|
+
undefined,
|
|
711
|
+
ctx.getSystemPromptOptions().skills ?? [],
|
|
712
|
+
);
|
|
713
|
+
if (ctx.mode !== "tui") {
|
|
714
|
+
show({ toolPolicy: profileToolPolicy, profiles });
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
await ctx.ui.custom<void>(
|
|
718
|
+
(tui, theme, keys, done) =>
|
|
719
|
+
createProfileViewer(
|
|
720
|
+
profiles,
|
|
721
|
+
theme,
|
|
722
|
+
keys,
|
|
723
|
+
() => tui.requestRender(),
|
|
724
|
+
() => done(),
|
|
725
|
+
() => Math.max(1, Math.floor(tui.terminal.rows * 0.9)),
|
|
726
|
+
),
|
|
727
|
+
{ overlay: true, overlayOptions: { width: "90%", maxHeight: "90%", anchor: "center" } },
|
|
728
|
+
);
|
|
729
|
+
},
|
|
730
|
+
});
|
|
731
|
+
pi.registerCommand("subagent-status", {
|
|
732
|
+
description: "List delegation identities, states and final results",
|
|
733
|
+
handler: async (args) =>
|
|
734
|
+
show(args.trim() ? coordinator.snapshot(ref(args)) : coordinator.list()),
|
|
735
|
+
});
|
|
736
|
+
pi.registerCommand("subagent-stop", {
|
|
737
|
+
description: "Stop <task-id> <generation> and drain it",
|
|
738
|
+
handler: async (args) => {
|
|
739
|
+
await coordinator.stop(ref(args));
|
|
740
|
+
},
|
|
741
|
+
});
|
|
742
|
+
pi.registerCommand("subagent-steer", {
|
|
743
|
+
description: "Steer <task-id> <generation> <text>",
|
|
744
|
+
handler: async (args) => {
|
|
745
|
+
const [id, generation, ...text] = args.trim().split(/\s+/);
|
|
746
|
+
await coordinator.steer(ref(`${id} ${generation}`), text.join(" "));
|
|
747
|
+
},
|
|
748
|
+
});
|
|
749
|
+
pi.registerCommand("subagent-hub", {
|
|
750
|
+
description:
|
|
751
|
+
"Subagent Hub: subagent roster, generation history, transcripts and pinned controls (TUI only)",
|
|
752
|
+
handler: async (_args, ctx) => {
|
|
753
|
+
if (ctx.mode !== "tui") return;
|
|
754
|
+
await ctx.ui.custom<void>(
|
|
755
|
+
(tui, theme, keys, done) =>
|
|
756
|
+
createHub(
|
|
757
|
+
coordinator,
|
|
758
|
+
theme,
|
|
759
|
+
keys,
|
|
760
|
+
() => tui.requestRender(),
|
|
761
|
+
() => done(),
|
|
762
|
+
{
|
|
763
|
+
tui,
|
|
764
|
+
cwd: ctx.cwd,
|
|
765
|
+
toolRenderers: {
|
|
766
|
+
delegate_task: {
|
|
767
|
+
renderShell: taskDefinition.renderShell,
|
|
768
|
+
renderCall: taskDefinition.renderCall,
|
|
769
|
+
renderResult: taskDefinition.renderResult,
|
|
770
|
+
} as ToolRenderers,
|
|
771
|
+
},
|
|
772
|
+
},
|
|
773
|
+
() => Math.max(1, Math.floor(tui.terminal.rows * 0.9)),
|
|
774
|
+
),
|
|
775
|
+
{ overlay: true, overlayOptions: { width: "90%", maxHeight: "90%", anchor: "center" } },
|
|
776
|
+
);
|
|
777
|
+
},
|
|
778
|
+
});
|
|
779
|
+
}
|