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.
Files changed (3) hide show
  1. package/index.ts +161 -144
  2. package/package.json +5 -1
  3. 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
- 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
- );
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
- if (!canDelegate) return;
432
- if (discoveredAgents.length === 0) return;
434
+ try {
435
+ if (!canDelegate) return;
436
+ if (discoveredAgents.length === 0) return;
433
437
 
434
- const agentList = discoveredAgents
435
- .map((a) => `- **${a.name}**: ${a.description}`)
436
- .join("\n");
437
- return {
438
- systemPrompt:
439
- event.systemPrompt +
440
- `\n\n## Available Subagents
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
- const discovery = discoverAgents(ctx.cwd, "both");
502
- const { agents } = discovery;
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
- let forkSessionSnapshotJsonl: string | undefined;
528
- if (delegationMode === "fork") {
529
- forkSessionSnapshotJsonl = buildForkSessionSnapshotJsonl(
530
- ctx.sessionManager,
531
- );
532
- if (!forkSessionSnapshotJsonl) {
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: "Cannot use mode=\"fork\": failed to snapshot current session context.",
522
+ text: `Invalid mode \"${String(params.mode)}\". Expected \"spawn\" or \"fork\".\nAvailable agents: ${formatAgentNames(agents)}`,
538
523
  },
539
524
  ],
540
- details: makeDetails("single")([]),
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
- const executionMode = tasks.length === 1 ? "single" : "parallel";
530
+ const makeDetails = makeDetailsFactory(
531
+ discovery.projectAgentsDir,
532
+ delegationMode,
533
+ );
560
534
 
561
- // Security: guard project-local agents before running
562
- const requested = new Set<string>();
563
- for (const t of tasks) requested.add(t.agent);
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
- if (preventCycles) {
566
- const cycleViolations = getCycleViolations(
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: `Blocked: delegation cycle detected. Requested agent(s) already in the delegation stack: ${cycleViolations.join(", ")}.
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(executionMode)([]),
586
- isError: true,
563
+ details: makeDetails("single")([]),
587
564
  };
588
565
  }
589
- }
590
566
 
591
- const requestedProjectAgents = getRequestedProjectAgents(
592
- agents,
593
- requested,
594
- );
595
- const projectAgentConfirmationSetting =
596
- getProjectAgentConfirmationSetting();
597
- const projectAgentSessionKey = getProjectAgentSessionKey(
598
- discovery.projectAgentsDir,
599
- );
600
- const shouldConfirmProjectAgents =
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 (approval === "no") {
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: "Canceled: project-local agents not approved.",
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
- if (approval === "session") {
623
- approvedProjectAgentDirsForSession.add(projectAgentSessionKey);
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
- if (tasks.length === 1) {
642
- const [task] = tasks;
643
- return executeSingle(
644
- task.agent,
645
- task.task,
646
- task.cwd,
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.6",
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", () => resolve(1));
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);