@narumitw/pi-subagents 1.0.2 → 2.0.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/README.md +198 -188
- package/package.json +2 -2
- package/src/agents/built-ins.ts +13 -66
- package/src/agents/catalog.ts +19 -2
- package/src/agents/discovery.ts +31 -15
- package/src/auto-transport.ts +7 -1
- package/src/child-peer-bridge.ts +124 -0
- package/src/child-peer-tools.ts +132 -0
- package/src/completion-delivery.ts +19 -5
- package/src/completion-render.ts +189 -0
- package/src/completion-routing.ts +24 -0
- package/src/config-ui.ts +11 -17
- package/src/consult-registration.ts +3 -2
- package/src/create-stateful-transport.ts +15 -2
- package/src/execution-ui.ts +0 -72
- package/src/in-process-transport.ts +39 -7
- package/src/inspect-tool.ts +3 -1
- package/src/peer-communication.ts +352 -0
- package/src/peer-transport.ts +49 -0
- package/src/persistence.ts +26 -1
- package/src/pi-args.ts +2 -0
- package/src/registry-types.ts +7 -0
- package/src/registry.ts +240 -41
- package/src/result-contract.ts +20 -5
- package/src/rpc-transport.ts +56 -26
- package/src/runner.ts +13 -1
- package/src/spawn-idempotency.ts +2 -0
- package/src/stateful-agent-view.ts +3 -1
- package/src/stateful-guidance.ts +11 -11
- package/src/stateful-safety.ts +0 -45
- package/src/stateful-tool-params.ts +11 -3
- package/src/stateful.ts +119 -47
- package/src/subagents.ts +6 -8
- package/src/subprocess-transport.ts +49 -28
- package/src/task-path.ts +65 -0
- package/src/transport-ui.ts +0 -6
- package/src/transport.ts +2 -1
- package/src/workflow-ui.ts +4 -4
- package/src/automation-contract.ts +0 -709
- package/src/automation-planner.ts +0 -65
- package/src/automation-registration.ts +0 -137
- package/src/automation-tool.ts +0 -40
- package/src/automation.ts +0 -435
- package/src/execution-profiles.ts +0 -95
- package/src/workflow-plan-compiler.ts +0 -618
- package/src/workflow-plan-patch.ts +0 -636
- package/src/workflow-planning-benchmark.ts +0 -95
package/src/stateful.ts
CHANGED
|
@@ -47,18 +47,11 @@ import { createSpawnPromptGuidelines } from "./stateful-guidance.js";
|
|
|
47
47
|
import { assertCurrentSpawn, waitForOwnedSpawn } from "./stateful-lifecycle.js";
|
|
48
48
|
import { resolveStatefulLimits, type StatefulLimits } from "./stateful-limits.js";
|
|
49
49
|
import { createStatefulToolRenderer } from "./stateful-render.js";
|
|
50
|
-
import {
|
|
51
|
-
|
|
52
|
-
assertNoSharedWriteConflict,
|
|
53
|
-
confirmProjectAgent,
|
|
54
|
-
} from "./stateful-safety.js";
|
|
50
|
+
import { confirmProjectAgent } from "./stateful-safety.js";
|
|
51
|
+
import { MAX_TASK_NAME_LENGTH } from "./task-path.js";
|
|
55
52
|
import { MAX_SUBAGENT_TOOL_CALLS, MAX_SUBAGENT_TURNS } from "./turn-budget.js";
|
|
56
53
|
|
|
57
|
-
export {
|
|
58
|
-
assertFollowUpWriteAllowed,
|
|
59
|
-
assertNoSharedWriteConflict,
|
|
60
|
-
isWriteCapable,
|
|
61
|
-
} from "./stateful-safety.js";
|
|
54
|
+
export { isWriteCapable } from "./stateful-safety.js";
|
|
62
55
|
|
|
63
56
|
import {
|
|
64
57
|
MailboxParamsSchema,
|
|
@@ -73,6 +66,7 @@ type StateLifecycleModule = typeof import("./stateful-lifecycle.js");
|
|
|
73
66
|
|
|
74
67
|
type StatefulSessionModules = {
|
|
75
68
|
broker: typeof import("./completion-delivery.js");
|
|
69
|
+
peerCommunication: typeof import("./peer-communication.js");
|
|
76
70
|
context: typeof import("./context.js");
|
|
77
71
|
transport: typeof import("./create-stateful-transport.js");
|
|
78
72
|
cwdPolicy: CwdPolicyModule;
|
|
@@ -100,6 +94,7 @@ let workspaceModule: Promise<typeof import("./workspace.js")> | undefined;
|
|
|
100
94
|
function loadStatefulSessionModules(): Promise<StatefulSessionModules> {
|
|
101
95
|
statefulSessionModules ??= Promise.all([
|
|
102
96
|
import("./completion-delivery.js"),
|
|
97
|
+
import("./peer-communication.js"),
|
|
103
98
|
import("./context.js"),
|
|
104
99
|
import("./create-stateful-transport.js"),
|
|
105
100
|
import("./cwd-policy.js"),
|
|
@@ -107,15 +102,27 @@ function loadStatefulSessionModules(): Promise<StatefulSessionModules> {
|
|
|
107
102
|
import("./registry.js"),
|
|
108
103
|
import("./stateful-lifecycle.js"),
|
|
109
104
|
])
|
|
110
|
-
.then(
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
105
|
+
.then(
|
|
106
|
+
([
|
|
107
|
+
broker,
|
|
108
|
+
peerCommunication,
|
|
109
|
+
context,
|
|
110
|
+
transport,
|
|
111
|
+
cwdPolicy,
|
|
112
|
+
persistence,
|
|
113
|
+
registry,
|
|
114
|
+
lifecycle,
|
|
115
|
+
]) => ({
|
|
116
|
+
broker,
|
|
117
|
+
peerCommunication,
|
|
118
|
+
context,
|
|
119
|
+
transport,
|
|
120
|
+
cwdPolicy,
|
|
121
|
+
persistence,
|
|
122
|
+
registry,
|
|
123
|
+
lifecycle,
|
|
124
|
+
}),
|
|
125
|
+
)
|
|
119
126
|
.catch((error: unknown) => {
|
|
120
127
|
statefulSessionModules = undefined;
|
|
121
128
|
throw error;
|
|
@@ -266,6 +273,7 @@ export function registerStatefulSubagents(
|
|
|
266
273
|
let runtimeLimits = resolveStatefulLimits(settings);
|
|
267
274
|
let agentCatalog = "";
|
|
268
275
|
let completionBroker: CompletionDeliveryBroker | undefined;
|
|
276
|
+
let peerBroker: import("./peer-communication.js").PeerCommunicationBroker | undefined;
|
|
269
277
|
let refreshSpawnToolRegistration: (() => void) | undefined;
|
|
270
278
|
let registry: AgentRegistry | undefined;
|
|
271
279
|
let persistence: AgentPersistence | undefined;
|
|
@@ -361,6 +369,8 @@ export function registerStatefulSubagents(
|
|
|
361
369
|
const generation = ++runtimeGeneration;
|
|
362
370
|
completionBroker?.close();
|
|
363
371
|
completionBroker = undefined;
|
|
372
|
+
const previousPeerBroker = peerBroker;
|
|
373
|
+
peerBroker = undefined;
|
|
364
374
|
if (sweepTimer) clearInterval(sweepTimer);
|
|
365
375
|
sweepTimer = undefined;
|
|
366
376
|
const previousRegistry = registry;
|
|
@@ -370,6 +380,7 @@ export function registerStatefulSubagents(
|
|
|
370
380
|
seenMessageIds.clear();
|
|
371
381
|
pendingIdempotentSpawns.clear();
|
|
372
382
|
const initialize = async () => {
|
|
383
|
+
await previousPeerBroker?.close();
|
|
373
384
|
const currentWorkspaceManager = await getWorkspaceManager();
|
|
374
385
|
const modules = await loadStatefulSessionModules();
|
|
375
386
|
const cleanupErrors = await modules.lifecycle.disposeStatefulRuntime(
|
|
@@ -396,6 +407,7 @@ export function registerStatefulSubagents(
|
|
|
396
407
|
maxStoredAgents: nextLimits.maxStoredAgents,
|
|
397
408
|
});
|
|
398
409
|
let nextRegistry: AgentRegistry;
|
|
410
|
+
let transport: import("./transport.js").SubagentTransport;
|
|
399
411
|
const sessionBroker = new modules.broker.CompletionDeliveryBroker(
|
|
400
412
|
pi,
|
|
401
413
|
ctx,
|
|
@@ -420,12 +432,42 @@ export function registerStatefulSubagents(
|
|
|
420
432
|
},
|
|
421
433
|
},
|
|
422
434
|
);
|
|
423
|
-
const
|
|
435
|
+
const sessionPeerBroker = new modules.peerCommunication.PeerCommunicationBroker({
|
|
436
|
+
getRegistry: () => nextRegistry,
|
|
437
|
+
sendRoot: ({ message, senderPath }) => {
|
|
438
|
+
pi.appendEntry("pi-subagent-peer-message", {
|
|
439
|
+
messageId: message.id,
|
|
440
|
+
senderId: message.senderId,
|
|
441
|
+
senderPath,
|
|
442
|
+
content: modules.context.redactPrivateText(message.content),
|
|
443
|
+
});
|
|
444
|
+
pi.sendMessage(
|
|
445
|
+
{
|
|
446
|
+
customType: "pi-subagent-peer-message",
|
|
447
|
+
content: [
|
|
448
|
+
"Message Type: SUBAGENT_PEER_MESSAGE",
|
|
449
|
+
"Protocol: pi-subagents:v1",
|
|
450
|
+
`Message ID: ${message.id}`,
|
|
451
|
+
`Sender ID: ${message.senderId}`,
|
|
452
|
+
`Sender Path: ${senderPath}`,
|
|
453
|
+
"Payload:",
|
|
454
|
+
message.content,
|
|
455
|
+
].join("\n"),
|
|
456
|
+
display: true,
|
|
457
|
+
details: { ...message, senderPath },
|
|
458
|
+
},
|
|
459
|
+
{ deliverAs: "steer", triggerTurn: false },
|
|
460
|
+
);
|
|
461
|
+
},
|
|
462
|
+
dispatch: (recipient, message) => transport.deliverMessage?.(recipient, message) ?? false,
|
|
463
|
+
});
|
|
464
|
+
transport = modules.transport.createStatefulTransport({
|
|
424
465
|
kind: transportKind,
|
|
425
466
|
modelRegistry: ctx.modelRegistry,
|
|
426
467
|
getParentRuntime: () => ({ ...parentRuntime }),
|
|
427
468
|
getSettings: getCurrentSettings,
|
|
428
469
|
createInProcessSession: dependencies.createInProcessSession,
|
|
470
|
+
peerRuntime: sessionPeerBroker,
|
|
429
471
|
loadTransport: dependencies.loadTransport,
|
|
430
472
|
});
|
|
431
473
|
nextRegistry = new modules.registry.AgentRegistry(transport, {
|
|
@@ -449,11 +491,27 @@ export function registerStatefulSubagents(
|
|
|
449
491
|
recipientId: message.recipientId,
|
|
450
492
|
content: modules.context.redactPrivateText(message.content).slice(0, 160),
|
|
451
493
|
});
|
|
494
|
+
if (
|
|
495
|
+
agent.state === "running" &&
|
|
496
|
+
message.deduplicationKey?.startsWith("completion:")
|
|
497
|
+
) {
|
|
498
|
+
try {
|
|
499
|
+
await transport.deliverMessage?.(agent, {
|
|
500
|
+
...message,
|
|
501
|
+
content: modules.context.redactPrivateText(message.content),
|
|
502
|
+
});
|
|
503
|
+
} catch {
|
|
504
|
+
// The durable parent mailbox remains the retry path for the next turn.
|
|
505
|
+
}
|
|
506
|
+
if (generation !== runtimeGeneration) return;
|
|
507
|
+
}
|
|
452
508
|
}
|
|
453
509
|
}
|
|
454
510
|
},
|
|
455
511
|
onTurnComplete: (completion) => {
|
|
456
|
-
if (generation === runtimeGeneration
|
|
512
|
+
if (generation === runtimeGeneration && completion.recipientId === "root") {
|
|
513
|
+
sessionBroker.enqueue(completion);
|
|
514
|
+
}
|
|
457
515
|
},
|
|
458
516
|
});
|
|
459
517
|
const persisted = sessionPersistence.load();
|
|
@@ -461,6 +519,12 @@ export function registerStatefulSubagents(
|
|
|
461
519
|
persisted,
|
|
462
520
|
currentWorkspaceManager,
|
|
463
521
|
);
|
|
522
|
+
if (generation !== runtimeGeneration) {
|
|
523
|
+
sessionBroker.close();
|
|
524
|
+
await sessionPeerBroker.close();
|
|
525
|
+
await modules.lifecycle.disposeStatefulRuntime(nextRegistry, currentWorkspaceManager);
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
464
528
|
if (ctx.hasUI && orphanCleanupFailures > 0) {
|
|
465
529
|
ctx.ui.notify("Some orphaned subagent worktrees could not be cleaned", "warning");
|
|
466
530
|
}
|
|
@@ -491,14 +555,16 @@ export function registerStatefulSubagents(
|
|
|
491
555
|
nextRegistry.restore(restored);
|
|
492
556
|
if (generation !== runtimeGeneration) {
|
|
493
557
|
sessionBroker.close();
|
|
558
|
+
await sessionPeerBroker.close();
|
|
494
559
|
await modules.lifecycle.disposeStatefulRuntime(nextRegistry, currentWorkspaceManager);
|
|
495
560
|
return;
|
|
496
561
|
}
|
|
497
562
|
registry = nextRegistry;
|
|
498
563
|
persistence = sessionPersistence;
|
|
499
564
|
completionBroker = sessionBroker;
|
|
565
|
+
peerBroker = sessionPeerBroker;
|
|
500
566
|
for (const completion of nextRegistry.listPendingCompletions()) {
|
|
501
|
-
sessionBroker.enqueue(completion);
|
|
567
|
+
if (completion.recipientId === "root") sessionBroker.enqueue(completion);
|
|
502
568
|
}
|
|
503
569
|
runtimeLimits = nextLimits;
|
|
504
570
|
refreshSpawnToolRegistration?.();
|
|
@@ -544,6 +610,8 @@ export function registerStatefulSubagents(
|
|
|
544
610
|
runtimeGeneration++;
|
|
545
611
|
completionBroker?.close();
|
|
546
612
|
completionBroker = undefined;
|
|
613
|
+
const previousPeerBroker = peerBroker;
|
|
614
|
+
peerBroker = undefined;
|
|
547
615
|
if (sweepTimer) clearInterval(sweepTimer);
|
|
548
616
|
sweepTimer = undefined;
|
|
549
617
|
const previousRegistry = registry;
|
|
@@ -553,6 +621,7 @@ export function registerStatefulSubagents(
|
|
|
553
621
|
seenMessageIds.clear();
|
|
554
622
|
pendingIdempotentSpawns.clear();
|
|
555
623
|
const shutdown = async () => {
|
|
624
|
+
await previousPeerBroker?.close();
|
|
556
625
|
const currentWorkspaceManager = await getWorkspaceManager();
|
|
557
626
|
const { disposeStatefulRuntime } = await import("./stateful-lifecycle.js");
|
|
558
627
|
const errors = await disposeStatefulRuntime(previousRegistry, currentWorkspaceManager);
|
|
@@ -566,7 +635,7 @@ export function registerStatefulSubagents(
|
|
|
566
635
|
});
|
|
567
636
|
|
|
568
637
|
const baseSpawnDescription = () =>
|
|
569
|
-
`Start an addressable background subagent with an optional thinking level and execution budgets chosen for the task difficulty, return immediately with an agentId, and receive its completion asynchronously. Detached capacity: ${runtimeLimits.maxAgents} retained agents, ${runtimeLimits.maxActiveTurns} active turns, ${runtimeLimits.maxChildrenPerAgent} direct children per agent, and depth ${runtimeLimits.maxDepth}. Working-directory target policy: ${dependencies.getSettings?.()?.cwdPolicy?.delegation ?? DEFAULT_DELEGATION_CWD_POLICY}. This controls launch targets and protected project resources, not filesystem access or sandboxing.`;
|
|
638
|
+
`Start an addressable background subagent with an opaque agentId and canonical taskPath, plus an optional thinking level and execution budgets chosen for the task difficulty, return immediately with an agentId, and receive its completion asynchronously. Detached capacity: ${runtimeLimits.maxAgents} retained agents, ${runtimeLimits.maxActiveTurns} active turns, ${runtimeLimits.maxChildrenPerAgent} direct children per agent, and depth ${runtimeLimits.maxDepth}. Working-directory target policy: ${dependencies.getSettings?.()?.cwdPolicy?.delegation ?? DEFAULT_DELEGATION_CWD_POLICY}. This controls launch targets and protected project resources, not filesystem access or sandboxing.`;
|
|
570
639
|
const spawnTool = defineTool({
|
|
571
640
|
name: "subagent_spawn",
|
|
572
641
|
label: "Spawn Subagent",
|
|
@@ -575,6 +644,15 @@ export function registerStatefulSubagents(
|
|
|
575
644
|
promptGuidelines: createSpawnPromptGuidelines(completionDelivery, blockingEnabled),
|
|
576
645
|
parameters: Type.Object({
|
|
577
646
|
agent: Type.String({ minLength: 1 }),
|
|
647
|
+
taskName: Type.Optional(
|
|
648
|
+
Type.String({
|
|
649
|
+
minLength: 1,
|
|
650
|
+
maxLength: MAX_TASK_NAME_LENGTH,
|
|
651
|
+
pattern: "^[a-z0-9_]+$",
|
|
652
|
+
description:
|
|
653
|
+
"Canonical path segment for this task; use lowercase letters, digits, and underscores.",
|
|
654
|
+
}),
|
|
655
|
+
),
|
|
578
656
|
task: Type.String({ minLength: 1, maxLength: DEFAULT_MAX_CONTEXT_BYTES }),
|
|
579
657
|
thinkingLevel: Type.Optional(StatefulThinkingLevelSchema),
|
|
580
658
|
timeoutMs: Type.Optional(StatefulTimeoutSchema),
|
|
@@ -586,13 +664,18 @@ export function registerStatefulSubagents(
|
|
|
586
664
|
contextEntryIds: Type.Optional(
|
|
587
665
|
Type.Array(Type.String(), { description: "Optional selected session entry IDs." }),
|
|
588
666
|
),
|
|
589
|
-
parentId: Type.Optional(
|
|
667
|
+
parentId: Type.Optional(
|
|
668
|
+
Type.String({ description: "Optional parent agent ID or canonical task path." }),
|
|
669
|
+
),
|
|
590
670
|
allowConcurrentWrites: Type.Optional(
|
|
591
|
-
Type.Boolean({
|
|
671
|
+
Type.Boolean({
|
|
672
|
+
description:
|
|
673
|
+
"Deprecated compatibility field; shared-workspace concurrency is allowed by default.",
|
|
674
|
+
}),
|
|
592
675
|
),
|
|
593
676
|
workspaceMode: Type.Optional(
|
|
594
677
|
StringEnum(["shared", "worktree"] as const, {
|
|
595
|
-
description: "Use the shared workspace or an opt-in disposable Git worktree.",
|
|
678
|
+
description: "Use the shared workspace (default) or an opt-in disposable Git worktree.",
|
|
596
679
|
}),
|
|
597
680
|
),
|
|
598
681
|
contract: Type.Optional(DelegationContractSchema),
|
|
@@ -684,6 +767,7 @@ export function registerStatefulSubagents(
|
|
|
684
767
|
assertCurrentSpawn(signal, generation, runtimeGeneration);
|
|
685
768
|
const requestHash = modules.spawnIdempotency.hashSpawnRequest({
|
|
686
769
|
agent: params.agent,
|
|
770
|
+
taskName: params.taskName,
|
|
687
771
|
task: params.task,
|
|
688
772
|
cwd,
|
|
689
773
|
agentScope: scope,
|
|
@@ -752,15 +836,6 @@ export function registerStatefulSubagents(
|
|
|
752
836
|
throw new Error("Project-local subagent definitions cannot run in a detached worktree");
|
|
753
837
|
}
|
|
754
838
|
const requestedCwd = cwd;
|
|
755
|
-
if ((params.workspaceMode ?? "shared") === "shared" && !params.allowConcurrentWrites) {
|
|
756
|
-
assertNoSharedWriteConflict(
|
|
757
|
-
ownedRegistry,
|
|
758
|
-
params.agent,
|
|
759
|
-
requestedCwd,
|
|
760
|
-
scope,
|
|
761
|
-
currentSettings,
|
|
762
|
-
);
|
|
763
|
-
}
|
|
764
839
|
const workspaceOwner = `pending-${randomUUID()}`;
|
|
765
840
|
const workspace =
|
|
766
841
|
params.workspaceMode === "worktree"
|
|
@@ -781,6 +856,7 @@ export function registerStatefulSubagents(
|
|
|
781
856
|
);
|
|
782
857
|
agent = await ownedRegistry.spawn({
|
|
783
858
|
agent: params.agent,
|
|
859
|
+
taskName: params.taskName,
|
|
784
860
|
task: params.task,
|
|
785
861
|
cwd: workspace?.path ?? requestedCwd,
|
|
786
862
|
agentScope: scope,
|
|
@@ -819,11 +895,11 @@ export function registerStatefulSubagents(
|
|
|
819
895
|
resolvePending?.(agent);
|
|
820
896
|
const deliveryNote =
|
|
821
897
|
completionDelivery === "auto-resume"
|
|
822
|
-
? "
|
|
823
|
-
: "
|
|
898
|
+
? "Auto-resume will request synthesis after completion."
|
|
899
|
+
: "The current response must not depend on the result because next-turn delivery will not wake an idle root.";
|
|
824
900
|
return result(
|
|
825
901
|
agent,
|
|
826
|
-
`Spawned ${agent.agent} as ${agent.id}.
|
|
902
|
+
`Spawned ${agent.agent} as ${agent.taskPath ?? agent.id} (${agent.id}). Continue the identified non-overlapping local work immediately; do not merely announce the spawn or end while useful local work remains. Only an explicit user-requested specialist model, tool-profile, or isolation exception may lack concurrent local work. ${deliveryNote} Do not poll for progress.`,
|
|
827
903
|
);
|
|
828
904
|
} catch (error) {
|
|
829
905
|
rejectPending?.(error);
|
|
@@ -853,7 +929,7 @@ export function registerStatefulSubagents(
|
|
|
853
929
|
"Send follow-up work to a reusable retained subagent and start a new turn. Semantic resource skew requires explicit revalidation. Use subagent_mailbox for queue-only messages.",
|
|
854
930
|
promptSnippet: "Start a new detached follow-up turn on a retained subagent",
|
|
855
931
|
parameters: Type.Object({
|
|
856
|
-
agentId: Type.String(),
|
|
932
|
+
agentId: Type.String({ description: "Retained agent ID or canonical task path." }),
|
|
857
933
|
task: Type.String({ minLength: 1, maxLength: DEFAULT_MAX_CONTEXT_BYTES }),
|
|
858
934
|
timeoutMs: Type.Optional(
|
|
859
935
|
Type.Integer({
|
|
@@ -871,7 +947,10 @@ export function registerStatefulSubagents(
|
|
|
871
947
|
}),
|
|
872
948
|
),
|
|
873
949
|
allowConcurrentWrites: Type.Optional(
|
|
874
|
-
Type.Boolean({
|
|
950
|
+
Type.Boolean({
|
|
951
|
+
description:
|
|
952
|
+
"Deprecated compatibility field; shared-workspace concurrency is allowed by default.",
|
|
953
|
+
}),
|
|
875
954
|
),
|
|
876
955
|
}),
|
|
877
956
|
...createStatefulToolRenderer("send"),
|
|
@@ -959,13 +1038,6 @@ export function registerStatefulSubagents(
|
|
|
959
1038
|
currentSettings,
|
|
960
1039
|
);
|
|
961
1040
|
assertCurrentSpawn(signal, generation, runtimeGeneration);
|
|
962
|
-
assertFollowUpWriteAllowed(
|
|
963
|
-
ownedRegistry,
|
|
964
|
-
existing,
|
|
965
|
-
params.allowConcurrentWrites ?? false,
|
|
966
|
-
isolatedAgents.has(existing.id),
|
|
967
|
-
currentSettings,
|
|
968
|
-
);
|
|
969
1041
|
const currentGrant = modules.capabilityGrant.issueCapabilityGrant(
|
|
970
1042
|
currentPlan,
|
|
971
1043
|
Date.now(),
|
package/src/subagents.ts
CHANGED
|
@@ -21,11 +21,8 @@ import type {
|
|
|
21
21
|
DelegationCwdPolicy,
|
|
22
22
|
SubagentSettings,
|
|
23
23
|
} from "./agents/types.js";
|
|
24
|
-
import {
|
|
25
|
-
type AutomationRegistrationDependencies,
|
|
26
|
-
registerSubagentAutomation,
|
|
27
|
-
} from "./automation-registration.js";
|
|
28
24
|
import { cachedModuleLoader, throwIfAborted } from "./cached-module-loader.js";
|
|
25
|
+
import { renderCompletionMessage, SUBAGENT_COMPLETION_MESSAGE_TYPE } from "./completion-render.js";
|
|
29
26
|
import {
|
|
30
27
|
type ConfigRegistrationDependencies,
|
|
31
28
|
registerSubagentConfigCommand,
|
|
@@ -60,13 +57,13 @@ type BlockingExecutionModule = Pick<typeof import("./execution.js"), "executeSub
|
|
|
60
57
|
export interface SubagentsDependencies {
|
|
61
58
|
loadBlockingExecution?: () => Promise<BlockingExecutionModule>;
|
|
62
59
|
loadStatefulTransport?: () => Promise<SubagentTransport>;
|
|
63
|
-
automation?: AutomationRegistrationDependencies;
|
|
64
60
|
config?: ConfigRegistrationDependencies;
|
|
65
61
|
consult?: ConsultRegistrationDependencies;
|
|
66
62
|
inspect?: InspectRegistrationDependencies;
|
|
67
63
|
}
|
|
68
64
|
|
|
69
65
|
export default function (pi: ExtensionAPI, dependencies: SubagentsDependencies = {}) {
|
|
66
|
+
pi.registerMessageRenderer(SUBAGENT_COMPLETION_MESSAGE_TYPE, renderCompletionMessage);
|
|
70
67
|
const loadBlockingExecution = cachedModuleLoader(
|
|
71
68
|
dependencies.loadBlockingExecution ?? (() => import("./execution.js")),
|
|
72
69
|
);
|
|
@@ -78,9 +75,6 @@ export default function (pi: ExtensionAPI, dependencies: SubagentsDependencies =
|
|
|
78
75
|
const refreshBlockingCatalog = blockingEnabled
|
|
79
76
|
? registerBlockingSubagent(pi, () => currentSettings, loadBlockingExecution)
|
|
80
77
|
: () => undefined;
|
|
81
|
-
if (blockingEnabled) {
|
|
82
|
-
registerSubagentAutomation(pi, { getSettings: () => currentSettings }, dependencies.automation);
|
|
83
|
-
}
|
|
84
78
|
let refreshStatefulCatalog: (catalog: string) => void = () => undefined;
|
|
85
79
|
let refreshConsultCatalog: (catalog: string) => void = () => undefined;
|
|
86
80
|
|
|
@@ -225,7 +219,11 @@ function registerBlockingSubagent(
|
|
|
225
219
|
].join(" ");
|
|
226
220
|
const promptGuidelines = () => [
|
|
227
221
|
"Use subagent only when delegation fits; the main agent should decide how many subagents to spawn from task shape instead of waiting for the user to specify a count.",
|
|
222
|
+
"The main agent retains overall planning, immediate critical-path work, integration, final verification, and the final answer.",
|
|
228
223
|
"Use no subagent for simple answers, quick targeted edits, latency-sensitive one-step work, tasks requiring frequent user back-and-forth, or critical-path work the main agent can perform directly.",
|
|
224
|
+
"One ordinary implementation worker should not replace work the main agent can perform directly; use a blocking single only when intentional synchronous isolation or a user-requested specialist justifies waiting.",
|
|
225
|
+
"Keep ordinary planning in the main agent, or use explicit workflow mode when a genuine dependency graph requires caller-authored orchestration.",
|
|
226
|
+
"Keep ordinary review in the main agent with a review skill and deterministic checks; reserve panel mode or custom verifier agents for consequential independent verification.",
|
|
229
227
|
"Use the blocking subagent tool only when delegated outputs are required before the main agent's next action and waiting is intentional; the main agent cannot process queued steering until the call returns.",
|
|
230
228
|
"Use a blocking subagent single, parallel, chain, workflow, panel, or fan-in call only when synchronous context or output isolation is worth making the main agent unavailable while it runs.",
|
|
231
229
|
`If a blocking parallel subagent call is genuinely required, keep tasks independent, stay within the configured max ${resolveBlockingMaxParallelTasks(getSettings())}, and avoid write-heavy implementation touching the same files or shared state.`,
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { discoverAgents } from "./agents/discovery.js";
|
|
2
2
|
import type { AgentConfig, SubagentSettings, SubagentThinkingLevel } from "./agents/types.js";
|
|
3
|
+
import {
|
|
4
|
+
CHILD_PEER_TOOL_NAMES,
|
|
5
|
+
childPeerBridgePath,
|
|
6
|
+
type PeerTransportRuntime,
|
|
7
|
+
peerBridgeEnvironment,
|
|
8
|
+
} from "./peer-transport.js";
|
|
3
9
|
import { resolvePiPromptResources } from "./prompt-resources.js";
|
|
4
10
|
import type { ManagedAgent, TurnOutcome } from "./registry.js";
|
|
5
11
|
import { getResultFinalOutput, runSingleAgent, type SubagentDetails } from "./runner.js";
|
|
@@ -17,6 +23,7 @@ export function resolveStatefulSubprocessThinkingLevel(
|
|
|
17
23
|
|
|
18
24
|
export interface SubprocessTransportOptions {
|
|
19
25
|
getSettings?: () => SubagentSettings | undefined;
|
|
26
|
+
peerRuntime?: PeerTransportRuntime;
|
|
20
27
|
}
|
|
21
28
|
|
|
22
29
|
export class SubprocessTransport implements SubagentTransport {
|
|
@@ -54,35 +61,49 @@ export class SubprocessTransport implements SubagentTransport {
|
|
|
54
61
|
projectAgentsDir: discovery.projectAgentsDir,
|
|
55
62
|
results,
|
|
56
63
|
});
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
undefined
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
64
|
+
const credentials = this.options.peerRuntime
|
|
65
|
+
? await this.options.peerRuntime.issueCredentials(
|
|
66
|
+
record.id,
|
|
67
|
+
record.currentTurnGeneration ?? record.turnGeneration ?? 1,
|
|
68
|
+
)
|
|
69
|
+
: undefined;
|
|
70
|
+
let single: Awaited<ReturnType<typeof runSingleAgent>>;
|
|
71
|
+
try {
|
|
72
|
+
single = await runSingleAgent(
|
|
73
|
+
record.cwd,
|
|
74
|
+
discovery.agents,
|
|
75
|
+
record.agent,
|
|
76
|
+
boundedTask.text,
|
|
77
|
+
undefined,
|
|
78
|
+
undefined,
|
|
79
|
+
signal,
|
|
80
|
+
resolveStatefulSubprocessThinkingLevel(discovery.agents, record),
|
|
81
|
+
record.currentTimeoutMs ?? record.timeoutMs ?? resolveStatefulTurnTimeout(agent),
|
|
82
|
+
undefined,
|
|
83
|
+
makeDetails,
|
|
84
|
+
undefined,
|
|
85
|
+
{
|
|
86
|
+
projectTrust,
|
|
87
|
+
...(record.executionPlan ? { tools: record.executionPlan.effectiveTools } : {}),
|
|
88
|
+
appendSystemPromptPaths: promptResources?.appendSystemPromptPaths,
|
|
89
|
+
extensionPaths: credentials ? [childPeerBridgePath()] : undefined,
|
|
90
|
+
additionalTools: credentials ? [...CHILD_PEER_TOOL_NAMES] : undefined,
|
|
91
|
+
env: credentials ? peerBridgeEnvironment(credentials) : undefined,
|
|
92
|
+
timeoutResultFormat: record.resultFormat,
|
|
93
|
+
turnLimits: {
|
|
94
|
+
idleTimeoutMs: record.currentIdleTimeoutMs ?? record.idleTimeoutMs,
|
|
95
|
+
maxTurns: record.currentMaxTurns ?? record.maxTurns,
|
|
96
|
+
maxToolCalls: record.currentMaxToolCalls ?? record.maxToolCalls,
|
|
97
|
+
},
|
|
98
|
+
resultFormat: record.resultFormat,
|
|
99
|
+
contract: record.contract,
|
|
100
|
+
executionPlan: record.executionPlan,
|
|
101
|
+
displayTask: task,
|
|
79
102
|
},
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
},
|
|
85
|
-
);
|
|
103
|
+
);
|
|
104
|
+
} finally {
|
|
105
|
+
this.options.peerRuntime?.revoke(record.id);
|
|
106
|
+
}
|
|
86
107
|
const settledAt = Date.now();
|
|
87
108
|
const telemetry: TransportTelemetry = {
|
|
88
109
|
...starting,
|
package/src/task-path.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
export const ROOT_TASK_PATH = "/root";
|
|
4
|
+
export const MAX_TASK_NAME_LENGTH = 128;
|
|
5
|
+
export const MAX_TASK_PATH_LENGTH = 2_048;
|
|
6
|
+
|
|
7
|
+
const TASK_NAME_PATTERN = /^[a-z0-9_]+$/u;
|
|
8
|
+
|
|
9
|
+
export function validateTaskName(value: string): string {
|
|
10
|
+
if (!value) throw new Error("Subagent taskName must not be empty");
|
|
11
|
+
if (value === "root" || value === "." || value === "..") {
|
|
12
|
+
throw new Error(`Subagent taskName ${JSON.stringify(value)} is reserved`);
|
|
13
|
+
}
|
|
14
|
+
if (value.length > MAX_TASK_NAME_LENGTH) {
|
|
15
|
+
throw new Error(`Subagent taskName cannot exceed ${MAX_TASK_NAME_LENGTH} characters`);
|
|
16
|
+
}
|
|
17
|
+
if (value.includes("/")) throw new Error("Subagent taskName must not contain `/`");
|
|
18
|
+
if (!TASK_NAME_PATTERN.test(value)) {
|
|
19
|
+
throw new Error(
|
|
20
|
+
"Subagent taskName must use only lowercase ASCII letters, digits, and underscores",
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function validateTaskPath(value: string): string {
|
|
27
|
+
if (!value.startsWith(`${ROOT_TASK_PATH}/`) && value !== ROOT_TASK_PATH) {
|
|
28
|
+
throw new Error("Canonical subagent task paths must start with `/root`");
|
|
29
|
+
}
|
|
30
|
+
if (value.length > MAX_TASK_PATH_LENGTH) {
|
|
31
|
+
throw new Error(
|
|
32
|
+
`Canonical subagent task paths cannot exceed ${MAX_TASK_PATH_LENGTH} characters`,
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
if (value.endsWith("/") || value.includes("//")) {
|
|
36
|
+
throw new Error("Canonical subagent task paths must not contain empty segments");
|
|
37
|
+
}
|
|
38
|
+
for (const segment of value.slice(ROOT_TASK_PATH.length + 1).split("/")) {
|
|
39
|
+
if (segment) validateTaskName(segment);
|
|
40
|
+
}
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function joinTaskPath(parentPath: string, taskName: string): string {
|
|
45
|
+
const parent = validateTaskPath(parentPath);
|
|
46
|
+
const name = validateTaskName(taskName);
|
|
47
|
+
return validateTaskPath(`${parent}/${name}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function resolveTaskPath(senderPath: string, reference: string): string {
|
|
51
|
+
if (!reference) throw new Error("Subagent task path reference must not be empty");
|
|
52
|
+
if (reference.startsWith("/")) return validateTaskPath(reference);
|
|
53
|
+
const sender = validateTaskPath(senderPath);
|
|
54
|
+
if (reference.endsWith("/")) {
|
|
55
|
+
throw new Error("Relative subagent task paths must not end with `/`");
|
|
56
|
+
}
|
|
57
|
+
let path = sender;
|
|
58
|
+
for (const segment of reference.split("/")) path = joinTaskPath(path, segment);
|
|
59
|
+
return path;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function deriveTaskName(agentId: string): string {
|
|
63
|
+
const digest = createHash("sha256").update(agentId).digest("hex").slice(0, 32);
|
|
64
|
+
return `agent_${digest}`;
|
|
65
|
+
}
|
package/src/transport-ui.ts
CHANGED
|
@@ -148,12 +148,6 @@ export function responsivenessSetupScreen(runtime: TransportUiRuntime) {
|
|
|
148
148
|
description: "Choose separately whether an idle root resumes for synthesis",
|
|
149
149
|
to: "settings" as const,
|
|
150
150
|
},
|
|
151
|
-
{
|
|
152
|
-
id: "thinking",
|
|
153
|
-
label: "Thinking profiles",
|
|
154
|
-
description: "Preview explicit Fast, Balanced, or Deep per-agent defaults",
|
|
155
|
-
to: "execution-profiles" as const,
|
|
156
|
-
},
|
|
157
151
|
{ id: "back", label: "Back", action: "back" as const },
|
|
158
152
|
],
|
|
159
153
|
hint: "back" as const,
|
package/src/transport.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ManagedAgent, TurnOutcome } from "./registry.js";
|
|
1
|
+
import type { AgentMailboxMessage, ManagedAgent, TurnOutcome } from "./registry.js";
|
|
2
2
|
import type { TransportProgressCallback } from "./transport-types.js";
|
|
3
3
|
|
|
4
4
|
export interface SubagentTransport {
|
|
@@ -9,6 +9,7 @@ export interface SubagentTransport {
|
|
|
9
9
|
signal: AbortSignal,
|
|
10
10
|
onProgress?: TransportProgressCallback,
|
|
11
11
|
): Promise<TurnOutcome>;
|
|
12
|
+
deliverMessage?(agent: ManagedAgent, message: AgentMailboxMessage): Promise<boolean>;
|
|
12
13
|
release?(agent: ManagedAgent): Promise<void>;
|
|
13
14
|
shutdown?(): Promise<void>;
|
|
14
15
|
}
|
package/src/workflow-ui.ts
CHANGED
|
@@ -46,15 +46,15 @@ function workflowEffects(current: DelegationWorkflow, next: DelegationWorkflow):
|
|
|
46
46
|
if (blockingEnabled(current) !== blockingEnabled(next)) {
|
|
47
47
|
effects.push(
|
|
48
48
|
blockingEnabled(next)
|
|
49
|
-
? "Add blocking `subagent
|
|
50
|
-
: "Remove blocking `subagent
|
|
49
|
+
? "Add blocking `subagent` and read-only `subagent_consult`"
|
|
50
|
+
: "Remove blocking `subagent` and read-only `subagent_consult`",
|
|
51
51
|
);
|
|
52
52
|
}
|
|
53
53
|
if (asyncEnabled(current) !== asyncEnabled(next)) {
|
|
54
54
|
effects.push(
|
|
55
55
|
asyncEnabled(next)
|
|
56
|
-
? "Add
|
|
57
|
-
: "Remove
|
|
56
|
+
? "Add async `subagent_spawn`, `subagent_send`, `subagent_manage`, and `subagent_mailbox`"
|
|
57
|
+
: "Remove async `subagent_spawn`, `subagent_send`, `subagent_manage`, and `subagent_mailbox`",
|
|
58
58
|
);
|
|
59
59
|
}
|
|
60
60
|
return effects;
|