@howarewoo/pi-subagents 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/README.md +265 -94
- package/docs/advanced-policies.md +4 -2
- package/docs/delegation-contract.md +57 -28
- package/docs/images/subagent-hub.svg +1 -1
- package/docs/images/subagent-profiles.svg +1 -1
- package/docs/images/subagent-widget.svg +1 -1
- package/docs/releases.md +111 -23
- package/docs/verification.md +1 -1
- package/package.json +4 -4
- package/src/child-execution.ts +459 -0
- package/src/coordinator.ts +608 -664
- package/src/delegation-types.ts +111 -0
- package/src/hub.ts +38 -267
- package/src/pi-extended-subagents.ts +83 -189
- package/src/profile-viewer.ts +109 -38
- package/src/scheduling.ts +1 -1
- package/src/subagent-widget.ts +115 -0
- package/src/supported-host.ts +26 -0
- package/src/task-input.ts +3 -2
- package/src/task-presentation.ts +165 -0
- package/src/tool-schemas.ts +165 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { requireSupportedHost } from "./supported-host.ts";
|
|
1
2
|
import { createProfileViewer, resolvedProfiles, profileToolPolicy } from "./profile-viewer.ts";
|
|
2
3
|
import { Text, truncateToWidth } from "@earendil-works/pi-tui";
|
|
3
4
|
import { EFFORTS, sameSelection } from "./model-policy.ts";
|
|
@@ -5,7 +6,7 @@ import { clippedText } from "./outcomes.ts";
|
|
|
5
6
|
import { readTaskInput } from "./task-input.ts";
|
|
6
7
|
import { resolveWorkspace } from "./workspace.ts";
|
|
7
8
|
import { join } from "node:path";
|
|
8
|
-
import {
|
|
9
|
+
import { parameters, coordinationParameters, compatibleParameters } from "./tool-schemas.ts";
|
|
9
10
|
import {
|
|
10
11
|
truncateHead,
|
|
11
12
|
type ExtensionToolContext,
|
|
@@ -21,14 +22,11 @@ import {
|
|
|
21
22
|
type ChildSnapshot,
|
|
22
23
|
type StartTask,
|
|
23
24
|
} from "./coordinator.ts";
|
|
24
|
-
import {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
intersectProfileGrants,
|
|
30
|
-
} from "./profiles.ts";
|
|
31
|
-
import { createChildWidget, createHub, safe, taskCard } from "./hub.ts";
|
|
25
|
+
import { READ_TOOLS, resolveProfile, bundledProfiles, intersectProfileGrants } from "./profiles.ts";
|
|
26
|
+
import { createHub } from "./hub.ts";
|
|
27
|
+
import { createChildWidget } from "./subagent-widget.ts";
|
|
28
|
+
import { taskCard } from "./task-presentation.ts";
|
|
29
|
+
import { safe } from "./thread.ts";
|
|
32
30
|
|
|
33
31
|
export default function delegation(pi: ExtensionAPI) {
|
|
34
32
|
let coordinator: DelegationCoordinator;
|
|
@@ -157,64 +155,13 @@ export default function delegation(pi: ExtensionAPI) {
|
|
|
157
155
|
throw new Error(`Delegation requires an unmodified built-in parent tool: ${name}`);
|
|
158
156
|
}
|
|
159
157
|
};
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
]);
|
|
158
|
+
const codemodeEnabled = () =>
|
|
159
|
+
pi.getActiveTools().includes("codemode") &&
|
|
160
|
+
pi
|
|
161
|
+
.getAllTools()
|
|
162
|
+
.some(
|
|
163
|
+
(tool) => tool.name === "codemode" && tool.exposure === "model-only" && !!tool.reachesTools,
|
|
164
|
+
);
|
|
218
165
|
const taskTool = (
|
|
219
166
|
parent?: ChildRef,
|
|
220
167
|
rootContext?: ExtensionToolContext,
|
|
@@ -248,7 +195,7 @@ export default function delegation(pi: ExtensionAPI) {
|
|
|
248
195
|
invalidate() {},
|
|
249
196
|
};
|
|
250
197
|
},
|
|
251
|
-
renderResult(result, options, theme) {
|
|
198
|
+
renderResult(result, options, theme, context) {
|
|
252
199
|
const stored = result.details as ChildSnapshot | { children: ChildSnapshot[] } | undefined;
|
|
253
200
|
const children =
|
|
254
201
|
stored && "children" in stored ? stored.children : stored && "id" in stored ? [stored] : [];
|
|
@@ -272,6 +219,8 @@ export default function delegation(pi: ExtensionAPI) {
|
|
|
272
219
|
.join("\n"),
|
|
273
220
|
),
|
|
274
221
|
];
|
|
222
|
+
if (children.length && !options.expanded)
|
|
223
|
+
lines.push(context.keyHint("app.tools.expand", "to expand"));
|
|
275
224
|
return new Text(lines.map((line) => theme.fg("text", line)).join("\n"), 0, 0).render(
|
|
276
225
|
width,
|
|
277
226
|
);
|
|
@@ -280,6 +229,7 @@ export default function delegation(pi: ExtensionAPI) {
|
|
|
280
229
|
};
|
|
281
230
|
},
|
|
282
231
|
async execute(callerId, args, signal, onUpdate, localContext) {
|
|
232
|
+
requireSupportedHost();
|
|
283
233
|
const ctx = rootContext ?? localContext;
|
|
284
234
|
const owner = coordinator;
|
|
285
235
|
if (parent && args.background)
|
|
@@ -321,6 +271,7 @@ export default function delegation(pi: ExtensionAPI) {
|
|
|
321
271
|
throw new Error("Request cannot mint model grants");
|
|
322
272
|
profile = { ...profile, models: task.models };
|
|
323
273
|
}
|
|
274
|
+
signal?.throwIfAborted();
|
|
324
275
|
let text = task.assignment;
|
|
325
276
|
if (task.taskFile !== undefined) {
|
|
326
277
|
assertTools(["read"]);
|
|
@@ -328,7 +279,16 @@ export default function delegation(pi: ExtensionAPI) {
|
|
|
328
279
|
task.taskFile,
|
|
329
280
|
ctx,
|
|
330
281
|
signal,
|
|
331
|
-
parent
|
|
282
|
+
parent
|
|
283
|
+
? async () => {
|
|
284
|
+
await owner.authorizeSpawn(parent, profile, task.model, {
|
|
285
|
+
context: ctx,
|
|
286
|
+
toolMetadata: () => pi.getAllTools(),
|
|
287
|
+
cwd,
|
|
288
|
+
});
|
|
289
|
+
await owner.taskInputRead(parent, signal);
|
|
290
|
+
}
|
|
291
|
+
: undefined,
|
|
332
292
|
cwd,
|
|
333
293
|
);
|
|
334
294
|
}
|
|
@@ -342,6 +302,7 @@ export default function delegation(pi: ExtensionAPI) {
|
|
|
342
302
|
context: ctx,
|
|
343
303
|
assertTools,
|
|
344
304
|
toolMetadata: () => pi.getAllTools(),
|
|
305
|
+
codemodeEnabled,
|
|
345
306
|
availableSkills,
|
|
346
307
|
callerId: rootCallerId ?? callerId,
|
|
347
308
|
parent,
|
|
@@ -351,8 +312,11 @@ export default function delegation(pi: ExtensionAPI) {
|
|
|
351
312
|
});
|
|
352
313
|
}
|
|
353
314
|
signal?.throwIfAborted();
|
|
354
|
-
const children = owner.admit(inputs);
|
|
355
|
-
const stop = () =>
|
|
315
|
+
const children = await owner.admit(inputs, undefined, signal);
|
|
316
|
+
const stop = () =>
|
|
317
|
+
parent && ["pausing", "paused"].includes(owner.snapshot(parent)!.status)
|
|
318
|
+
? Promise.resolve()
|
|
319
|
+
: owner.stopMany(children);
|
|
356
320
|
const abort = () => {
|
|
357
321
|
void stop();
|
|
358
322
|
};
|
|
@@ -400,66 +364,25 @@ export default function delegation(pi: ExtensionAPI) {
|
|
|
400
364
|
}
|
|
401
365
|
},
|
|
402
366
|
});
|
|
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
367
|
const coordinationTool = (
|
|
447
368
|
owner?: DelegationCoordinator,
|
|
448
369
|
actor?: ChildRef,
|
|
370
|
+
wake = false,
|
|
449
371
|
): ToolDefinition<typeof coordinationParameters> => ({
|
|
450
372
|
name: "delegate_coordinate",
|
|
451
373
|
label: "Coordinate delegation",
|
|
452
374
|
exposure: "model-only",
|
|
453
375
|
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.",
|
|
376
|
+
"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, paused resume (assignment is the appended continuation), and idle followup/parked revive. Resume retains identity and full context, resets only the deadline window, and requires explicit child resumes after parent resume. 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
377
|
parameters: coordinationParameters,
|
|
456
378
|
async execute(callerId, args, signal, _update, ctx) {
|
|
379
|
+
if (["resume", "followup", "revive"].includes(args.action)) requireSupportedHost();
|
|
457
380
|
const registry = owner ?? coordinator;
|
|
458
381
|
signal?.throwIfAborted();
|
|
459
382
|
let value: unknown;
|
|
460
383
|
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);
|
|
384
|
+
value = await registry.message(actor, args.target, args.text, args.mode, signal);
|
|
385
|
+
else if (args.action === "inbox") value = await registry.inbox(actor, signal);
|
|
463
386
|
else if (args.action === "wait")
|
|
464
387
|
value = await registry.coordinateWait(
|
|
465
388
|
actor,
|
|
@@ -482,7 +405,15 @@ export default function delegation(pi: ExtensionAPI) {
|
|
|
482
405
|
generations: registry.generations(args.target.id),
|
|
483
406
|
agents: registry.agents(),
|
|
484
407
|
};
|
|
485
|
-
else if (args.action === "
|
|
408
|
+
else if (args.action === "resume") {
|
|
409
|
+
const next = await registry.resume(args.target, args.assignment);
|
|
410
|
+
value = registry.snapshot(next);
|
|
411
|
+
// Each continuation owns exactly one notification for its new window.
|
|
412
|
+
void registry.wait(next).then((state) => {
|
|
413
|
+
if (!closing && coordinator === registry)
|
|
414
|
+
show(wake ? compatible(state) : state, state.transcript, wake);
|
|
415
|
+
});
|
|
416
|
+
} else if (args.action === "followup" || args.action === "revive") {
|
|
486
417
|
const presence = registry
|
|
487
418
|
.agents()
|
|
488
419
|
.find(
|
|
@@ -523,21 +454,26 @@ export default function delegation(pi: ExtensionAPI) {
|
|
|
523
454
|
: { id: original.parentId, generation: original.parentGeneration };
|
|
524
455
|
}
|
|
525
456
|
signal?.throwIfAborted();
|
|
526
|
-
const next = registry.renew(
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
457
|
+
const next = await registry.renew(
|
|
458
|
+
args.target,
|
|
459
|
+
{
|
|
460
|
+
cwd: state.cwd,
|
|
461
|
+
assignment: args.assignment,
|
|
462
|
+
resultSchema: args.resultSchema,
|
|
463
|
+
profile,
|
|
464
|
+
skills,
|
|
465
|
+
context: ctx,
|
|
466
|
+
assertTools,
|
|
467
|
+
toolMetadata: () => pi.getAllTools(),
|
|
468
|
+
codemodeEnabled,
|
|
469
|
+
availableSkills,
|
|
470
|
+
callerId,
|
|
471
|
+
parent,
|
|
472
|
+
coordinationTools: (ref) => [coordinationTool(registry, ref)],
|
|
473
|
+
nestedTool: (ref) => taskTool(ref, ctx, callerId),
|
|
474
|
+
},
|
|
475
|
+
signal,
|
|
476
|
+
);
|
|
541
477
|
value = registry.snapshot(next);
|
|
542
478
|
}
|
|
543
479
|
}
|
|
@@ -548,56 +484,6 @@ export default function delegation(pi: ExtensionAPI) {
|
|
|
548
484
|
const taskDefinition = taskTool();
|
|
549
485
|
pi.registerTool(taskDefinition);
|
|
550
486
|
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
487
|
pi.registerTool({
|
|
602
488
|
name: "subagent",
|
|
603
489
|
label: "Subagent",
|
|
@@ -607,12 +493,14 @@ export default function delegation(pi: ExtensionAPI) {
|
|
|
607
493
|
promptGuidelines: [
|
|
608
494
|
"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
495
|
"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
|
|
496
|
+
"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 continues a paused conversation with its full context and cumulative budgets; completed idle/parked work starts a fresh generation.",
|
|
611
497
|
],
|
|
612
498
|
description:
|
|
613
499
|
"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
500
|
parameters: compatibleParameters,
|
|
615
501
|
async execute(callerId, args, signal, onUpdate, ctx) {
|
|
502
|
+
if (!("action" in args) || args.action === "resume" || args.action === "steer")
|
|
503
|
+
requireSupportedHost();
|
|
616
504
|
const owner = coordinator;
|
|
617
505
|
if (!("action" in args)) {
|
|
618
506
|
const items = "tasks" in args ? args.tasks : [args];
|
|
@@ -681,10 +569,15 @@ export default function delegation(pi: ExtensionAPI) {
|
|
|
681
569
|
.find(
|
|
682
570
|
(state) => state.id === target.id && state.generation === target.generation,
|
|
683
571
|
)?.presence;
|
|
684
|
-
const response = await coordinationTool().execute(
|
|
572
|
+
const response = await coordinationTool(undefined, undefined, true).execute(
|
|
685
573
|
callerId,
|
|
686
574
|
{
|
|
687
|
-
action:
|
|
575
|
+
action:
|
|
576
|
+
owner.snapshot(target)?.status === "paused"
|
|
577
|
+
? "resume"
|
|
578
|
+
: presence === "parked"
|
|
579
|
+
? "revive"
|
|
580
|
+
: "followup",
|
|
688
581
|
target,
|
|
689
582
|
assignment: args.message,
|
|
690
583
|
},
|
|
@@ -693,9 +586,10 @@ export default function delegation(pi: ExtensionAPI) {
|
|
|
693
586
|
ctx,
|
|
694
587
|
);
|
|
695
588
|
const next = response.details as ChildSnapshot;
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
589
|
+
if (next.generation !== target.generation)
|
|
590
|
+
void owner.wait(next).then((state) => {
|
|
591
|
+
if (!closing && coordinator === owner) show(compatible(state), state.transcript, true);
|
|
592
|
+
});
|
|
699
593
|
return result(compatible(next));
|
|
700
594
|
}
|
|
701
595
|
return result(compatible(owner.snapshot(target)!));
|
package/src/profile-viewer.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { Skill, Theme } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import {
|
|
3
|
+
colorToOklch,
|
|
4
|
+
oklchColor,
|
|
3
5
|
matchesKey,
|
|
4
6
|
truncateToWidth,
|
|
5
7
|
visibleWidth,
|
|
@@ -28,7 +30,9 @@ export async function resolvedProfiles(
|
|
|
28
30
|
);
|
|
29
31
|
}
|
|
30
32
|
|
|
31
|
-
|
|
33
|
+
type DetailLine = { text: string; heading?: boolean };
|
|
34
|
+
|
|
35
|
+
function details(profile: Profile): DetailLine[] {
|
|
32
36
|
const list = (items: readonly string[] | undefined) =>
|
|
33
37
|
items?.length ? items.join(", ") : "none";
|
|
34
38
|
const known = new Set([
|
|
@@ -44,30 +48,60 @@ function details(profile: Profile): string[] {
|
|
|
44
48
|
"model",
|
|
45
49
|
"prewalk",
|
|
46
50
|
]);
|
|
51
|
+
const heading = (text: string): DetailLine => ({ text, heading: true });
|
|
52
|
+
const value = (text: string): DetailLine => ({ text });
|
|
53
|
+
const structured = (label: string, item: unknown, indent = ""): DetailLine[] => {
|
|
54
|
+
if (Array.isArray(item))
|
|
55
|
+
return [
|
|
56
|
+
value(`${indent}${label}:${item.length ? "" : " none (empty grant)"}`),
|
|
57
|
+
...item.flatMap((entry, index) =>
|
|
58
|
+
structured(`Selection ${index + 1}`, entry, indent + " "),
|
|
59
|
+
),
|
|
60
|
+
];
|
|
61
|
+
if (item && typeof item === "object")
|
|
62
|
+
return [
|
|
63
|
+
value(`${indent}${label}:`),
|
|
64
|
+
...Object.entries(item).flatMap(([key, entry]) => structured(key, entry, indent + " ")),
|
|
65
|
+
];
|
|
66
|
+
return [value(`${indent}${label}: ${String(item)}`)];
|
|
67
|
+
};
|
|
47
68
|
return [
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
"Prompt",
|
|
52
|
-
profile.prompt,
|
|
53
|
-
|
|
54
|
-
"
|
|
55
|
-
`
|
|
69
|
+
heading("Description"),
|
|
70
|
+
value(profile.description),
|
|
71
|
+
value(""),
|
|
72
|
+
heading("Prompt"),
|
|
73
|
+
value(profile.prompt),
|
|
74
|
+
value(""),
|
|
75
|
+
heading("Tools and permissions"),
|
|
76
|
+
value(`Tools ceiling: ${list(profile.tools)}`),
|
|
77
|
+
value("Callable tools: unknown in command context; inspect admitted task tools."),
|
|
56
78
|
...(profile.name === "general"
|
|
57
79
|
? [
|
|
58
|
-
|
|
80
|
+
value(
|
|
81
|
+
"Inheritance: available parent skills and callable builtins; no parent instructions/history.",
|
|
82
|
+
),
|
|
59
83
|
]
|
|
60
84
|
: []),
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
`
|
|
65
|
-
`
|
|
66
|
-
|
|
85
|
+
value(
|
|
86
|
+
`Reviewed leaf ceiling: ${list(profile.leafTools)} (writer lease; trusted code, not sandboxed)`,
|
|
87
|
+
),
|
|
88
|
+
value(`Spawn ceiling: ${list(profile.spawn)}`),
|
|
89
|
+
value(`Coordination: ${list(profile.coordination)}`),
|
|
90
|
+
value(""),
|
|
91
|
+
heading("Skills (exact paths)"),
|
|
92
|
+
...(profile.skills.length ? profile.skills.map(value) : [value("none")]),
|
|
93
|
+
value(""),
|
|
94
|
+
heading("Model policy"),
|
|
95
|
+
...structured("Model default", profile.model ?? "selected parent model"),
|
|
96
|
+
...structured("Models grant", profile.models ?? "no explicit override grant"),
|
|
97
|
+
...structured("Prewalk", profile.prewalk ?? "off"),
|
|
67
98
|
...Object.entries(profile)
|
|
68
99
|
.filter(([key]) => !known.has(key))
|
|
69
|
-
.map(([key,
|
|
70
|
-
"
|
|
100
|
+
.map(([key, item]) => value(`${key}: ${JSON.stringify(item)}`)),
|
|
101
|
+
value(""),
|
|
102
|
+
value(
|
|
103
|
+
"Admission also applies parent/ancestry, model availability, capacity and writer checks. This viewer grants nothing.",
|
|
104
|
+
),
|
|
71
105
|
];
|
|
72
106
|
}
|
|
73
107
|
|
|
@@ -93,17 +127,33 @@ export function createProfileViewer(
|
|
|
93
127
|
width = Math.max(0, width);
|
|
94
128
|
const height = Math.max(1, Math.floor(availableRows()));
|
|
95
129
|
const close = keys.getKeys("tui.select.cancel").join("/") || "Esc";
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
130
|
+
const panelBg = oklchColor(colorToOklch(theme.colors.customMessageBg).l, 0, 0);
|
|
131
|
+
// Match the Hub: foreground helpers may reset both colors inside a painted line.
|
|
132
|
+
const paint = (text: string) => {
|
|
133
|
+
const padded = cell(text, width);
|
|
134
|
+
const restoreBg = theme.style("", { bg: panelBg }).replace("\x1b[49m", "");
|
|
135
|
+
return padded
|
|
136
|
+
.split("\x1b[0m")
|
|
137
|
+
.map((part) => theme.style(part.replaceAll("\x1b[49m", restoreBg), { bg: panelBg }))
|
|
138
|
+
.join("\x1b[0m");
|
|
139
|
+
};
|
|
140
|
+
const fallback = () =>
|
|
141
|
+
[`Profiles · ${safe(close)}: close`, "Resize for details"].slice(0, height).map(paint);
|
|
142
|
+
if (width < 24) return fallback();
|
|
100
143
|
const inner = width - 4;
|
|
101
144
|
const wide = width >= 88;
|
|
102
145
|
const leftWidth = wide ? Math.min(28, Math.floor(inner / 3)) : inner;
|
|
103
146
|
const rightWidth = wide ? inner - leftWidth - 3 : inner;
|
|
104
|
-
const
|
|
147
|
+
const footer = [
|
|
148
|
+
"↑↓ select · j/k scroll · PgUp/Dn · Home/End",
|
|
149
|
+
`${safe(close)}: close · callable tools unknown here`,
|
|
150
|
+
].flatMap((line) => wrapTextWithAnsi(line, inner));
|
|
151
|
+
// Frames, divider and hints are reserved before assigning any viewport rows.
|
|
152
|
+
const bodyHeight = height - 3 - footer.length;
|
|
105
153
|
const listHeight = wide ? bodyHeight : Math.min(3, Math.max(1, Math.floor(bodyHeight / 3)));
|
|
106
|
-
|
|
154
|
+
const detailHeight = wide ? bodyHeight : bodyHeight - listHeight - 1;
|
|
155
|
+
if (detailHeight < 4) return fallback();
|
|
156
|
+
viewport = detailHeight - 3;
|
|
107
157
|
const start = Math.max(0, selected - listHeight + 1);
|
|
108
158
|
const roster = profiles
|
|
109
159
|
.slice(start, start + listHeight)
|
|
@@ -113,16 +163,33 @@ export function createProfileViewer(
|
|
|
113
163
|
`${start + index === selected ? ">" : " "} ${safe(profile.name).replaceAll("\n", " ")}`,
|
|
114
164
|
),
|
|
115
165
|
);
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
166
|
+
const profile = profiles[selected];
|
|
167
|
+
const name = profile ? safe(profile.name).replaceAll("\n", " ") : "No selection";
|
|
168
|
+
const content = profile ? details(profile) : [{ text: "No resolved profiles" }];
|
|
169
|
+
// A clipped pinned name still has a complete, scrollable representation.
|
|
170
|
+
if (profile && (visibleWidth(name) > rightWidth || profile.name.includes("\n")))
|
|
171
|
+
content.unshift({ text: profile.name });
|
|
172
|
+
const lines = content.flatMap(({ text, heading }) =>
|
|
119
173
|
safe(text)
|
|
120
174
|
.split("\n")
|
|
121
|
-
.flatMap((line) =>
|
|
175
|
+
.flatMap((line) =>
|
|
176
|
+
wrapTextWithAnsi(line, rightWidth).map((wrapped) =>
|
|
177
|
+
theme.fg(heading ? "accent" : "text", wrapped),
|
|
178
|
+
),
|
|
179
|
+
),
|
|
122
180
|
);
|
|
123
181
|
maximum = Math.max(0, lines.length - viewport);
|
|
124
182
|
offset = Math.min(maximum, offset);
|
|
125
|
-
const detail =
|
|
183
|
+
const detail = [
|
|
184
|
+
theme.fg("accent", name),
|
|
185
|
+
theme.fg("muted", "resolved · read-only"),
|
|
186
|
+
...lines.slice(offset, offset + viewport),
|
|
187
|
+
...Array(Math.max(0, viewport - (lines.length - offset))).fill(""),
|
|
188
|
+
theme.fg(
|
|
189
|
+
"muted",
|
|
190
|
+
`${profiles.length ? selected + 1 : 0}/${profiles.length} · detail ${offset + 1}–${Math.min(lines.length, offset + viewport)}/${lines.length}`,
|
|
191
|
+
),
|
|
192
|
+
];
|
|
126
193
|
const body = wide
|
|
127
194
|
? Array.from(
|
|
128
195
|
{ length: bodyHeight },
|
|
@@ -134,20 +201,24 @@ export function createProfileViewer(
|
|
|
134
201
|
: [
|
|
135
202
|
...roster,
|
|
136
203
|
...Array(Math.max(0, listHeight - roster.length)).fill(""),
|
|
137
|
-
"─".repeat(inner),
|
|
204
|
+
theme.fg("border", "─".repeat(inner)),
|
|
138
205
|
...detail,
|
|
139
206
|
];
|
|
140
207
|
const row = (text: string) =>
|
|
141
|
-
theme.
|
|
208
|
+
paint(theme.fg("border", "│") + ` ${cell(text, inner)} ` + theme.fg("border", "│"));
|
|
209
|
+
const frame = (left: string, title: string, right: string) =>
|
|
210
|
+
paint(
|
|
211
|
+
theme.fg("border", left) +
|
|
212
|
+
theme.fg("accent", title) +
|
|
213
|
+
theme.fg("border", "─".repeat(Math.max(0, width - 2 - visibleWidth(title))) + right),
|
|
214
|
+
);
|
|
142
215
|
return [
|
|
143
|
-
|
|
216
|
+
frame("╭", " Subagent Profiles ", "╮"),
|
|
144
217
|
...body.map(row),
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
),
|
|
148
|
-
|
|
149
|
-
row(`${close}: close · callable tools unknown here`),
|
|
150
|
-
].slice(0, height);
|
|
218
|
+
frame("├", "", "┤"),
|
|
219
|
+
...footer.map((line) => row(theme.fg("muted", line))),
|
|
220
|
+
frame("╰", "", "╯"),
|
|
221
|
+
];
|
|
151
222
|
},
|
|
152
223
|
handleInput(data) {
|
|
153
224
|
if (keys.matches(data, "tui.select.cancel")) {
|