oira666_pi-subagent 0.1.6 → 0.1.7
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/index.ts +161 -144
- package/package.json +5 -1
- package/runner.ts +17 -2
package/index.ts
CHANGED
|
@@ -411,33 +411,37 @@ export default function (pi: ExtensionAPI) {
|
|
|
411
411
|
// Auto-discover agents on session start
|
|
412
412
|
pi.on("session_start", async (_event, ctx) => {
|
|
413
413
|
if (!canDelegate) return;
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
414
|
+
try {
|
|
415
|
+
const discovery = discoverAgents(ctx.cwd, "both");
|
|
416
|
+
discoveredAgents = discovery.agents;
|
|
417
|
+
|
|
418
|
+
if (discoveredAgents.length > 0 && ctx.hasUI) {
|
|
419
|
+
const list = discoveredAgents
|
|
420
|
+
.map((a) => ` - ${a.name} (${a.source})`)
|
|
421
|
+
.join("\n");
|
|
422
|
+
ctx.ui.notify(
|
|
423
|
+
`Found ${discoveredAgents.length} subagent(s):\n${list}`,
|
|
424
|
+
"info",
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
} catch (err) {
|
|
428
|
+
console.error("[pi-subagent] Error in session_start:", err);
|
|
426
429
|
}
|
|
427
430
|
});
|
|
428
431
|
|
|
429
432
|
// Inject available agents into the system prompt
|
|
430
433
|
pi.on("before_agent_start", async (event) => {
|
|
431
|
-
|
|
432
|
-
|
|
434
|
+
try {
|
|
435
|
+
if (!canDelegate) return;
|
|
436
|
+
if (discoveredAgents.length === 0) return;
|
|
433
437
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
438
|
+
const agentList = discoveredAgents
|
|
439
|
+
.map((a) => `- **${a.name}**: ${a.description}`)
|
|
440
|
+
.join("\n");
|
|
441
|
+
return {
|
|
442
|
+
systemPrompt:
|
|
443
|
+
event.systemPrompt +
|
|
444
|
+
`\n\n## Available Subagents
|
|
441
445
|
|
|
442
446
|
The following subagents are available via the \`subagent\` tool:
|
|
443
447
|
|
|
@@ -471,7 +475,10 @@ The tool always accepts a \`tasks\` array:
|
|
|
471
475
|
- Cycle prevention: ${preventCycles ? "enabled" : "disabled"}
|
|
472
476
|
- Current delegation stack: ${ancestorAgentStack.length > 0 ? ancestorAgentStack.join(" -> ") : "(root)"}
|
|
473
477
|
`,
|
|
474
|
-
|
|
478
|
+
};
|
|
479
|
+
} catch (err) {
|
|
480
|
+
console.error("[pi-subagent] Error in before_agent_start:", err);
|
|
481
|
+
}
|
|
475
482
|
});
|
|
476
483
|
|
|
477
484
|
// Register the subagent tool
|
|
@@ -498,152 +505,165 @@ The tool always accepts a \`tasks\` array:
|
|
|
498
505
|
parameters: SubagentParams,
|
|
499
506
|
|
|
500
507
|
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
const delegationMode = parseDelegationMode(params.mode);
|
|
505
|
-
if (!delegationMode) {
|
|
506
|
-
const fallbackDetails = makeDetailsFactory(
|
|
507
|
-
discovery.projectAgentsDir,
|
|
508
|
-
DEFAULT_DELEGATION_MODE,
|
|
509
|
-
);
|
|
510
|
-
return {
|
|
511
|
-
content: [
|
|
512
|
-
{
|
|
513
|
-
type: "text",
|
|
514
|
-
text: `Invalid mode \"${String(params.mode)}\". Expected \"spawn\" or \"fork\".\nAvailable agents: ${formatAgentNames(agents)}`,
|
|
515
|
-
},
|
|
516
|
-
],
|
|
517
|
-
details: fallbackDetails("single")([]),
|
|
518
|
-
isError: true,
|
|
519
|
-
};
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
const makeDetails = makeDetailsFactory(
|
|
523
|
-
discovery.projectAgentsDir,
|
|
524
|
-
delegationMode,
|
|
525
|
-
);
|
|
508
|
+
try {
|
|
509
|
+
const discovery = discoverAgents(ctx.cwd, "both");
|
|
510
|
+
const { agents } = discovery;
|
|
526
511
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
512
|
+
const delegationMode = parseDelegationMode(params.mode);
|
|
513
|
+
if (!delegationMode) {
|
|
514
|
+
const fallbackDetails = makeDetailsFactory(
|
|
515
|
+
discovery.projectAgentsDir,
|
|
516
|
+
DEFAULT_DELEGATION_MODE,
|
|
517
|
+
);
|
|
533
518
|
return {
|
|
534
519
|
content: [
|
|
535
520
|
{
|
|
536
521
|
type: "text",
|
|
537
|
-
text:
|
|
522
|
+
text: `Invalid mode \"${String(params.mode)}\". Expected \"spawn\" or \"fork\".\nAvailable agents: ${formatAgentNames(agents)}`,
|
|
538
523
|
},
|
|
539
524
|
],
|
|
540
|
-
details:
|
|
525
|
+
details: fallbackDetails("single")([]),
|
|
541
526
|
isError: true,
|
|
542
527
|
};
|
|
543
528
|
}
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
const tasks = params.tasks ?? [];
|
|
547
|
-
if (tasks.length === 0) {
|
|
548
|
-
return {
|
|
549
|
-
content: [
|
|
550
|
-
{
|
|
551
|
-
type: "text",
|
|
552
|
-
text: `Invalid parameters. Provide a non-empty tasks array.\nAvailable agents: ${formatAgentNames(agents)}`,
|
|
553
|
-
},
|
|
554
|
-
],
|
|
555
|
-
details: makeDetails("single")([]),
|
|
556
|
-
};
|
|
557
|
-
}
|
|
558
529
|
|
|
559
|
-
|
|
530
|
+
const makeDetails = makeDetailsFactory(
|
|
531
|
+
discovery.projectAgentsDir,
|
|
532
|
+
delegationMode,
|
|
533
|
+
);
|
|
560
534
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
535
|
+
let forkSessionSnapshotJsonl: string | undefined;
|
|
536
|
+
if (delegationMode === "fork") {
|
|
537
|
+
forkSessionSnapshotJsonl = buildForkSessionSnapshotJsonl(
|
|
538
|
+
ctx.sessionManager,
|
|
539
|
+
);
|
|
540
|
+
if (!forkSessionSnapshotJsonl) {
|
|
541
|
+
return {
|
|
542
|
+
content: [
|
|
543
|
+
{
|
|
544
|
+
type: "text",
|
|
545
|
+
text: "Cannot use mode=\"fork\": failed to snapshot current session context.",
|
|
546
|
+
},
|
|
547
|
+
],
|
|
548
|
+
details: makeDetails("single")([]),
|
|
549
|
+
isError: true,
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
}
|
|
564
553
|
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
requested,
|
|
568
|
-
ancestorAgentStack,
|
|
569
|
-
);
|
|
570
|
-
if (cycleViolations.length > 0) {
|
|
571
|
-
const stackText =
|
|
572
|
-
ancestorAgentStack.length > 0
|
|
573
|
-
? ancestorAgentStack.join(" -> ")
|
|
574
|
-
: "(root)";
|
|
554
|
+
const tasks = params.tasks ?? [];
|
|
555
|
+
if (tasks.length === 0) {
|
|
575
556
|
return {
|
|
576
557
|
content: [
|
|
577
558
|
{
|
|
578
559
|
type: "text",
|
|
579
|
-
text: `
|
|
580
|
-
Current stack: ${stackText}
|
|
581
|
-
|
|
582
|
-
This guard prevents self-recursion and cyclic handoffs (for example A -> B -> A).`,
|
|
560
|
+
text: `Invalid parameters. Provide a non-empty tasks array.\nAvailable agents: ${formatAgentNames(agents)}`,
|
|
583
561
|
},
|
|
584
562
|
],
|
|
585
|
-
details: makeDetails(
|
|
586
|
-
isError: true,
|
|
563
|
+
details: makeDetails("single")([]),
|
|
587
564
|
};
|
|
588
565
|
}
|
|
589
|
-
}
|
|
590
566
|
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
requestedProjectAgents.length > 0 &&
|
|
602
|
-
projectAgentConfirmationSetting === "ask" &&
|
|
603
|
-
!approvedProjectAgentDirsForSession.has(projectAgentSessionKey);
|
|
604
|
-
if (shouldConfirmProjectAgents) {
|
|
605
|
-
if (ctx.hasUI) {
|
|
606
|
-
const approval = await confirmProjectAgentsIfNeeded(
|
|
607
|
-
requestedProjectAgents,
|
|
608
|
-
discovery.projectAgentsDir,
|
|
609
|
-
ctx,
|
|
567
|
+
const executionMode = tasks.length === 1 ? "single" : "parallel";
|
|
568
|
+
|
|
569
|
+
// Security: guard project-local agents before running
|
|
570
|
+
const requested = new Set<string>();
|
|
571
|
+
for (const t of tasks) requested.add(t.agent);
|
|
572
|
+
|
|
573
|
+
if (preventCycles) {
|
|
574
|
+
const cycleViolations = getCycleViolations(
|
|
575
|
+
requested,
|
|
576
|
+
ancestorAgentStack,
|
|
610
577
|
);
|
|
611
|
-
if (
|
|
578
|
+
if (cycleViolations.length > 0) {
|
|
579
|
+
const stackText =
|
|
580
|
+
ancestorAgentStack.length > 0
|
|
581
|
+
? ancestorAgentStack.join(" -> ")
|
|
582
|
+
: "(root)";
|
|
612
583
|
return {
|
|
613
584
|
content: [
|
|
614
585
|
{
|
|
615
586
|
type: "text",
|
|
616
|
-
text:
|
|
587
|
+
text: `Blocked: delegation cycle detected. Requested agent(s) already in the delegation stack: ${cycleViolations.join(", ")}.
|
|
588
|
+
Current stack: ${stackText}
|
|
589
|
+
|
|
590
|
+
This guard prevents self-recursion and cyclic handoffs (for example A -> B -> A).`,
|
|
617
591
|
},
|
|
618
592
|
],
|
|
619
593
|
details: makeDetails(executionMode)([]),
|
|
594
|
+
isError: true,
|
|
620
595
|
};
|
|
621
596
|
}
|
|
622
|
-
|
|
623
|
-
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
const requestedProjectAgents = getRequestedProjectAgents(
|
|
600
|
+
agents,
|
|
601
|
+
requested,
|
|
602
|
+
);
|
|
603
|
+
const projectAgentConfirmationSetting =
|
|
604
|
+
getProjectAgentConfirmationSetting();
|
|
605
|
+
const projectAgentSessionKey = getProjectAgentSessionKey(
|
|
606
|
+
discovery.projectAgentsDir,
|
|
607
|
+
);
|
|
608
|
+
const shouldConfirmProjectAgents =
|
|
609
|
+
requestedProjectAgents.length > 0 &&
|
|
610
|
+
projectAgentConfirmationSetting === "ask" &&
|
|
611
|
+
!approvedProjectAgentDirsForSession.has(projectAgentSessionKey);
|
|
612
|
+
if (shouldConfirmProjectAgents) {
|
|
613
|
+
if (ctx.hasUI) {
|
|
614
|
+
const approval = await confirmProjectAgentsIfNeeded(
|
|
615
|
+
requestedProjectAgents,
|
|
616
|
+
discovery.projectAgentsDir,
|
|
617
|
+
ctx,
|
|
618
|
+
);
|
|
619
|
+
if (approval === "no") {
|
|
620
|
+
return {
|
|
621
|
+
content: [
|
|
622
|
+
{
|
|
623
|
+
type: "text",
|
|
624
|
+
text: "Canceled: project-local agents not approved.",
|
|
625
|
+
},
|
|
626
|
+
],
|
|
627
|
+
details: makeDetails(executionMode)([]),
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
if (approval === "session") {
|
|
631
|
+
approvedProjectAgentDirsForSession.add(projectAgentSessionKey);
|
|
632
|
+
}
|
|
633
|
+
} else {
|
|
634
|
+
const names = requestedProjectAgents.map((a) => a.name).join(", ");
|
|
635
|
+
const dir = discovery.projectAgentsDir ?? "(unknown)";
|
|
636
|
+
return {
|
|
637
|
+
content: [
|
|
638
|
+
{
|
|
639
|
+
type: "text",
|
|
640
|
+
text: `Blocked: project-local agent confirmation is required in non-UI mode.\nAgents: ${names}\nSource: ${dir}\n\nSet ${SUBAGENT_CONFIRM_PROJECT_AGENTS_ENV}=false or =session only if this repository is trusted.`,
|
|
641
|
+
},
|
|
642
|
+
],
|
|
643
|
+
details: makeDetails(executionMode)([]),
|
|
644
|
+
isError: true,
|
|
645
|
+
};
|
|
624
646
|
}
|
|
625
|
-
} else {
|
|
626
|
-
const names = requestedProjectAgents.map((a) => a.name).join(", ");
|
|
627
|
-
const dir = discovery.projectAgentsDir ?? "(unknown)";
|
|
628
|
-
return {
|
|
629
|
-
content: [
|
|
630
|
-
{
|
|
631
|
-
type: "text",
|
|
632
|
-
text: `Blocked: project-local agent confirmation is required in non-UI mode.\nAgents: ${names}\nSource: ${dir}\n\nSet ${SUBAGENT_CONFIRM_PROJECT_AGENTS_ENV}=false or =session only if this repository is trusted.`,
|
|
633
|
-
},
|
|
634
|
-
],
|
|
635
|
-
details: makeDetails(executionMode)([]),
|
|
636
|
-
isError: true,
|
|
637
|
-
};
|
|
638
647
|
}
|
|
639
|
-
}
|
|
640
648
|
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
649
|
+
if (tasks.length === 1) {
|
|
650
|
+
const [task] = tasks;
|
|
651
|
+
return executeSingle(
|
|
652
|
+
task.agent,
|
|
653
|
+
task.task,
|
|
654
|
+
task.cwd,
|
|
655
|
+
delegationMode,
|
|
656
|
+
forkSessionSnapshotJsonl,
|
|
657
|
+
agents,
|
|
658
|
+
ctx.cwd,
|
|
659
|
+
signal,
|
|
660
|
+
onUpdate,
|
|
661
|
+
makeDetails,
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
return await executeParallel(
|
|
666
|
+
tasks,
|
|
647
667
|
delegationMode,
|
|
648
668
|
forkSessionSnapshotJsonl,
|
|
649
669
|
agents,
|
|
@@ -652,18 +672,15 @@ This guard prevents self-recursion and cyclic handoffs (for example A -> B -> A)
|
|
|
652
672
|
onUpdate,
|
|
653
673
|
makeDetails,
|
|
654
674
|
);
|
|
675
|
+
} catch (err) {
|
|
676
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
677
|
+
const stack = err instanceof Error && err.stack ? `\n\n${err.stack}` : "";
|
|
678
|
+
return {
|
|
679
|
+
content: [{ type: "text" as const, text: `[pi-subagent] Unexpected error: ${msg}${stack}` }],
|
|
680
|
+
details: buildSubagentDetails("single", DEFAULT_DELEGATION_MODE, null, []),
|
|
681
|
+
isError: true,
|
|
682
|
+
};
|
|
655
683
|
}
|
|
656
|
-
|
|
657
|
-
return executeParallel(
|
|
658
|
-
tasks,
|
|
659
|
-
delegationMode,
|
|
660
|
-
forkSessionSnapshotJsonl,
|
|
661
|
-
agents,
|
|
662
|
-
ctx.cwd,
|
|
663
|
-
signal,
|
|
664
|
-
onUpdate,
|
|
665
|
-
makeDetails,
|
|
666
|
-
);
|
|
667
684
|
},
|
|
668
685
|
|
|
669
686
|
renderCall: (args, theme) => renderCall(args, theme),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oira666_pi-subagent",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "Subagent extension for Pi coding agent. Delegate tasks to specialized agents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -37,8 +37,12 @@
|
|
|
37
37
|
"access": "public"
|
|
38
38
|
},
|
|
39
39
|
"license": "MIT",
|
|
40
|
+
"scripts": {
|
|
41
|
+
"test": "node --import tsx --test test/*.test.ts"
|
|
42
|
+
},
|
|
40
43
|
"devDependencies": {
|
|
41
44
|
"@types/node": "^25.2.3",
|
|
45
|
+
"tsx": "^4.21.0",
|
|
42
46
|
"typescript": "^5.9.3"
|
|
43
47
|
},
|
|
44
48
|
"peerDependencies": {
|
package/runner.ts
CHANGED
|
@@ -241,7 +241,7 @@ const _inheritedCliArgs = parseInheritedCliArgs(process.argv);
|
|
|
241
241
|
// JSON-line stream processing
|
|
242
242
|
// ---------------------------------------------------------------------------
|
|
243
243
|
|
|
244
|
-
function processJsonLine(line: string, result: SingleResult): boolean {
|
|
244
|
+
export function processJsonLine(line: string, result: SingleResult): boolean {
|
|
245
245
|
if (!line.trim()) return false;
|
|
246
246
|
|
|
247
247
|
let event: any;
|
|
@@ -251,6 +251,9 @@ function processJsonLine(line: string, result: SingleResult): boolean {
|
|
|
251
251
|
return false;
|
|
252
252
|
}
|
|
253
253
|
|
|
254
|
+
// Guard: JSON.parse can return null, a number, boolean, or array — none of which have .type
|
|
255
|
+
if (!event || typeof event !== "object" || Array.isArray(event)) return false;
|
|
256
|
+
|
|
254
257
|
if (event.type === "message_end" && event.message) {
|
|
255
258
|
const msg = event.message as Message;
|
|
256
259
|
result.messages.push(msg);
|
|
@@ -524,7 +527,12 @@ export async function runAgent(opts: RunAgentOptions): Promise<SingleResult> {
|
|
|
524
527
|
resolve(code ?? 0);
|
|
525
528
|
});
|
|
526
529
|
|
|
527
|
-
proc.on("error", () =>
|
|
530
|
+
proc.on("error", (err) => {
|
|
531
|
+
result.stderr += `Spawn error: ${err.message}`;
|
|
532
|
+
result.stopReason = "error";
|
|
533
|
+
result.errorMessage = `Failed to spawn pi process: ${err.message}`;
|
|
534
|
+
resolve(1);
|
|
535
|
+
});
|
|
528
536
|
|
|
529
537
|
// Abort handling
|
|
530
538
|
if (signal) {
|
|
@@ -559,6 +567,13 @@ export async function runAgent(opts: RunAgentOptions): Promise<SingleResult> {
|
|
|
559
567
|
}
|
|
560
568
|
}
|
|
561
569
|
|
|
570
|
+
return result;
|
|
571
|
+
} catch (err) {
|
|
572
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
573
|
+
result.exitCode = result.exitCode === -1 ? 1 : result.exitCode;
|
|
574
|
+
result.stopReason = result.stopReason ?? "error";
|
|
575
|
+
result.errorMessage = result.errorMessage ?? msg;
|
|
576
|
+
if (!result.stderr.trim()) result.stderr = msg;
|
|
562
577
|
return result;
|
|
563
578
|
} finally {
|
|
564
579
|
cleanupTempDir(promptTmpDir);
|