pi-subagents 0.21.4 → 0.22.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.
Files changed (40) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +12 -11
  3. package/agents/context-builder.md +4 -5
  4. package/agents/delegate.md +2 -1
  5. package/agents/oracle.md +5 -8
  6. package/agents/planner.md +2 -3
  7. package/agents/researcher.md +2 -3
  8. package/agents/reviewer.md +4 -21
  9. package/agents/scout.md +2 -3
  10. package/agents/worker.md +7 -7
  11. package/package.json +3 -2
  12. package/prompts/parallel-context-build.md +1 -1
  13. package/prompts/parallel-handoff-plan.md +2 -2
  14. package/skills/pi-subagents/SKILL.md +30 -27
  15. package/src/extension/index.ts +4 -1
  16. package/src/intercom/intercom-bridge.ts +10 -8
  17. package/src/intercom/result-intercom.ts +4 -3
  18. package/src/manager-ui/agent-manager-edit.ts +43 -19
  19. package/src/manager-ui/agent-manager.ts +5 -2
  20. package/src/runs/background/async-execution.ts +16 -10
  21. package/src/runs/background/async-job-tracker.ts +12 -19
  22. package/src/runs/background/async-resume.ts +1 -0
  23. package/src/runs/background/async-status.ts +35 -49
  24. package/src/runs/background/parallel-groups.ts +45 -0
  25. package/src/runs/background/result-watcher.ts +2 -2
  26. package/src/runs/background/run-status.ts +26 -7
  27. package/src/runs/background/stale-run-reconciler.ts +15 -2
  28. package/src/runs/background/subagent-runner.ts +12 -3
  29. package/src/runs/foreground/chain-clarify.ts +35 -30
  30. package/src/runs/foreground/chain-execution.ts +9 -6
  31. package/src/runs/foreground/execution.ts +4 -0
  32. package/src/runs/foreground/subagent-executor.ts +18 -30
  33. package/src/runs/shared/model-fallback.ts +2 -5
  34. package/src/runs/shared/pi-args.ts +20 -0
  35. package/src/shared/model-info.ts +68 -0
  36. package/src/shared/session-identity.ts +10 -0
  37. package/src/shared/types.ts +14 -5
  38. package/src/slash/slash-commands.ts +8 -7
  39. package/src/tui/render.ts +67 -2
  40. package/src/tui/subagents-status.ts +129 -15
@@ -1,10 +1,11 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import type { AgentToolResult } from "@mariozechner/pi-agent-core";
4
- import { formatAsyncRunList, listAsyncRuns } from "./async-status.ts";
5
- import { ASYNC_DIR, RESULTS_DIR, type Details } from "../../shared/types.ts";
4
+ import { formatAsyncRunList, formatAsyncRunProgressLabel, listAsyncRuns } from "./async-status.ts";
5
+ import { ASYNC_DIR, RESULTS_DIR, type AsyncStatus, type Details } from "../../shared/types.ts";
6
6
  import { resolveSubagentIntercomTarget } from "../../intercom/intercom-bridge.ts";
7
7
  import { resolveAsyncRunLocation } from "./async-resume.ts";
8
+ import { flatToLogicalStepIndex, normalizeParallelGroups } from "./parallel-groups.ts";
8
9
  import { reconcileAsyncRun } from "./stale-run-reconciler.ts";
9
10
 
10
11
  interface RunStatusParams {
@@ -31,6 +32,19 @@ function canShowRevive(stepCount: number, sessionFile: unknown): sessionFile is
31
32
  return stepCount === 1 && typeof sessionFile === "string" && fs.existsSync(sessionFile);
32
33
  }
33
34
 
35
+ function stepLineLabel(status: AsyncStatus, index: number): string {
36
+ const steps = status.steps ?? [];
37
+ if (status.mode === "parallel") return `Agent ${index + 1}/${steps.length || 1}`;
38
+ if (status.mode === "chain") {
39
+ const chainStepCount = status.chainStepCount ?? (steps.length || 1);
40
+ const groups = normalizeParallelGroups(status.parallelGroups, steps.length, chainStepCount);
41
+ const group = groups.find((candidate) => index >= candidate.start && index < candidate.start + candidate.count);
42
+ if (group) return `Step ${group.stepIndex + 1}/${chainStepCount} Agent ${index - group.start + 1}/${group.count}`;
43
+ return `Step ${flatToLogicalStepIndex(index, chainStepCount, groups) + 1}/${chainStepCount}`;
44
+ }
45
+ return `Step ${index + 1}`;
46
+ }
47
+
34
48
  export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDeps = {}): AgentToolResult<Details> {
35
49
  const asyncDirRoot = deps.asyncDirRoot ?? ASYNC_DIR;
36
50
  const resultsDir = deps.resultsDir ?? RESULTS_DIR;
@@ -89,9 +103,14 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
89
103
  const logPath = path.join(asyncDir, `subagent-log-${effectiveRunId}.md`);
90
104
  const eventsPath = path.join(asyncDir, "events.jsonl");
91
105
  if (status) {
92
- const stepsTotal = status.steps?.length ?? 1;
93
- const current = status.currentStep !== undefined ? status.currentStep + 1 : undefined;
94
- const stepLine = current !== undefined ? `Step: ${current}/${stepsTotal}` : `Steps: ${stepsTotal}`;
106
+ const progressLabel = formatAsyncRunProgressLabel({
107
+ mode: status.mode,
108
+ state: status.state,
109
+ currentStep: status.currentStep,
110
+ chainStepCount: status.chainStepCount,
111
+ parallelGroups: status.parallelGroups,
112
+ steps: (status.steps ?? []).map((step, index) => ({ index, agent: step.agent, status: step.status })),
113
+ });
95
114
  const started = new Date(status.startedAt).toISOString();
96
115
  const updated = status.lastUpdate ? new Date(status.lastUpdate).toISOString() : "n/a";
97
116
  const statusActivityText = status.state === "running" ? activityText(status.activityState, status.lastActivityAt) : undefined;
@@ -101,7 +120,7 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
101
120
  `State: ${status.state}`,
102
121
  statusActivityText ? `Activity: ${statusActivityText}` : undefined,
103
122
  `Mode: ${status.mode}`,
104
- stepLine,
123
+ `Progress: ${progressLabel}`,
105
124
  `Started: ${started}`,
106
125
  `Updated: ${updated}`,
107
126
  `Dir: ${asyncDir}`,
@@ -111,7 +130,7 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
111
130
  for (const [index, step] of (status.steps ?? []).entries()) {
112
131
  const stepActivityText = step.status === "running" ? activityText(step.activityState, step.lastActivityAt) : undefined;
113
132
  const errorText = step.error ? `, error: ${step.error}` : "";
114
- lines.push(`Step ${index + 1}: ${step.agent} ${step.status}${stepActivityText ? `, ${stepActivityText}` : ""}${errorText}`);
133
+ lines.push(`${stepLineLabel(status, index)}: ${step.agent} ${step.status}${stepActivityText ? `, ${stepActivityText}` : ""}${errorText}`);
115
134
  if (step.status === "running") {
116
135
  lines.push(` Intercom target: ${resolveSubagentIntercomTarget(status.runId, step.agent, index)} (if registered)`);
117
136
  }
@@ -1,7 +1,8 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { writeAtomicJson } from "../../shared/atomic-json.ts";
4
- import { RESULTS_DIR, type AsyncStatus } from "../../shared/types.ts";
4
+ import { RESULTS_DIR, type AsyncParallelGroupStatus, type AsyncStatus, type SubagentRunMode } from "../../shared/types.ts";
5
+ import { normalizeParallelGroups } from "./parallel-groups.ts";
5
6
 
6
7
  export type PidLiveness = "alive" | "dead" | "unknown";
7
8
 
@@ -10,8 +11,11 @@ type KillFn = (pid: number, signal?: NodeJS.Signals | 0) => boolean;
10
11
  interface StartedRunMetadata {
11
12
  runId: string;
12
13
  pid?: number;
13
- mode?: "single" | "chain";
14
+ sessionId?: string;
15
+ mode?: SubagentRunMode;
14
16
  agents?: string[];
17
+ chainStepCount?: number;
18
+ parallelGroups?: AsyncParallelGroupStatus[];
15
19
  startedAt?: number;
16
20
  sessionFile?: string;
17
21
  }
@@ -133,13 +137,21 @@ function terminalStatusFromResult(status: AsyncStatus, resultPath: string, now:
133
137
  function buildStartedStatus(asyncDir: string, startedRun: StartedRunMetadata, now: number): AsyncStatus {
134
138
  const startedAt = startedRun.startedAt ?? now;
135
139
  const agents = startedRun.agents?.length ? startedRun.agents : ["subagent"];
140
+ const chainStepCount = startedRun.chainStepCount;
141
+ const parallelGroups = chainStepCount !== undefined
142
+ ? normalizeParallelGroups(startedRun.parallelGroups, agents.length, chainStepCount)
143
+ : [];
136
144
  return {
137
145
  runId: startedRun.runId || path.basename(asyncDir),
146
+ ...(startedRun.sessionId ? { sessionId: startedRun.sessionId } : {}),
138
147
  mode: startedRun.mode ?? "single",
139
148
  state: "running",
140
149
  pid: startedRun.pid,
141
150
  startedAt,
142
151
  lastUpdate: now,
152
+ currentStep: 0,
153
+ ...(chainStepCount !== undefined ? { chainStepCount } : {}),
154
+ ...(parallelGroups.length ? { parallelGroups } : {}),
143
155
  steps: agents.map((agent) => ({
144
156
  agent,
145
157
  status: "running" as const,
@@ -197,6 +209,7 @@ function buildFailedRepair(status: AsyncStatus, asyncDir: string, now: number, r
197
209
  timestamp: now,
198
210
  durationMs: Math.max(0, now - status.startedAt),
199
211
  asyncDir,
212
+ sessionId: status.sessionId,
200
213
  sessionFile: status.sessionFile,
201
214
  },
202
215
  };
@@ -16,6 +16,7 @@ import {
16
16
  type AsyncStatus,
17
17
  type ModelAttempt,
18
18
  type ResolvedControlConfig,
19
+ type SubagentRunMode,
19
20
  type Usage,
20
21
  DEFAULT_MAX_OUTPUT,
21
22
  type MaxOutputConfig,
@@ -90,7 +91,7 @@ interface SubagentRunConfig {
90
91
  controlConfig?: ResolvedControlConfig;
91
92
  controlIntercomTarget?: string;
92
93
  childIntercomTargets?: Array<string | undefined>;
93
- resultMode?: "single" | "parallel" | "chain";
94
+ resultMode?: SubagentRunMode;
94
95
  }
95
96
 
96
97
  interface StepResult {
@@ -472,7 +473,7 @@ function writeRunLog(
472
473
  logPath: string,
473
474
  input: {
474
475
  id: string;
475
- mode: "single" | "chain";
476
+ mode: SubagentRunMode;
476
477
  cwd: string;
477
478
  startedAt: number;
478
479
  endedAt: number;
@@ -537,6 +538,7 @@ interface SingleStepContext {
537
538
  piArgv1?: string;
538
539
  registerInterrupt?: (interrupt: (() => void) | undefined) => void;
539
540
  childIntercomTarget?: string;
541
+ orchestratorIntercomTarget?: string;
540
542
  onChildEvent?: (event: ChildEvent) => void;
541
543
  }
542
544
 
@@ -603,6 +605,10 @@ async function runSingleStep(
603
605
  mcpDirectTools: step.mcpDirectTools,
604
606
  promptFileStem: step.agent,
605
607
  intercomSessionName: ctx.childIntercomTarget,
608
+ orchestratorIntercomTarget: ctx.orchestratorIntercomTarget,
609
+ runId: ctx.id,
610
+ childAgentName: step.agent,
611
+ childIndex: ctx.flatIndex,
606
612
  });
607
613
  const run = await runPiStreaming(
608
614
  args,
@@ -877,7 +883,8 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
877
883
  || flatSteps.some((step) => Boolean(step.sessionFile));
878
884
  const statusPayload: RunnerStatusPayload = {
879
885
  runId: id,
880
- mode: flatSteps.length > 1 ? "chain" : "single",
886
+ ...(config.sessionId ? { sessionId: config.sessionId } : {}),
887
+ mode: config.resultMode ?? (flatSteps.length > 1 ? "chain" : "single"),
881
888
  state: "running",
882
889
  lastActivityAt: overallStartTime,
883
890
  startedAt: overallStartTime,
@@ -1297,6 +1304,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1297
1304
  piPackageRoot: config.piPackageRoot,
1298
1305
  piArgv1: config.piArgv1,
1299
1306
  childIntercomTarget: config.childIntercomTargets?.[fi],
1307
+ orchestratorIntercomTarget: config.controlIntercomTarget,
1300
1308
  registerInterrupt: (interrupt) => {
1301
1309
  activeChildInterrupt = interrupt;
1302
1310
  },
@@ -1437,6 +1445,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1437
1445
  piPackageRoot: config.piPackageRoot,
1438
1446
  piArgv1: config.piArgv1,
1439
1447
  childIntercomTarget: config.childIntercomTargets?.[flatIndex],
1448
+ orchestratorIntercomTarget: config.controlIntercomTarget,
1440
1449
  registerInterrupt: (interrupt) => {
1441
1450
  activeChildInterrupt = interrupt;
1442
1451
  },
@@ -18,15 +18,10 @@ import { createEditorState, ensureCursorVisible, getCursorDisplayPos, handleEdit
18
18
  import { updateFrontmatterField } from "../../agents/agent-serializer.ts";
19
19
  import { serializeChain } from "../../agents/chain-serializer.ts";
20
20
  import { resolveModelCandidate, splitThinkingSuffix } from "../shared/model-fallback.ts";
21
+ import { findModelInfo, getSupportedThinkingLevels, type ModelInfo, type ThinkingLevel } from "../../shared/model-info.ts";
21
22
 
22
23
  type ClarifyMode = 'single' | 'parallel' | 'chain';
23
24
 
24
- export interface ModelInfo {
25
- provider: string;
26
- id: string;
27
- fullId: string;
28
- }
29
-
30
25
  export interface BehaviorOverride {
31
26
  output?: string | false;
32
27
  reads?: string[] | false;
@@ -44,9 +39,6 @@ export interface ChainClarifyResult {
44
39
 
45
40
  type EditMode = "template" | "output" | "reads" | "model" | "thinking" | "skills";
46
41
 
47
- const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"] as const;
48
- type ThinkingLevel = typeof THINKING_LEVELS[number];
49
-
50
42
  /**
51
43
  * TUI component for chain clarification.
52
44
  * Factory signature matches ctx.ui.custom: (tui, theme, kb, done) => Component
@@ -602,7 +594,10 @@ export class ChainClarifyComponent implements Component {
602
594
  const selected = this.filteredModels[this.modelSelectedIndex];
603
595
  if (selected) {
604
596
  const { thinkingSuffix } = splitThinkingSuffix(this.getEffectiveModel(this.editingStep!));
605
- this.updateBehavior(this.editingStep!, "model", `${selected.fullId}${thinkingSuffix}`);
597
+ const requestedLevel = thinkingSuffix.slice(1);
598
+ const selectedModel = findModelInfo(selected.fullId, this.availableModels, this.preferredProvider);
599
+ const suffix = getSupportedThinkingLevels(selectedModel).some((level) => level === requestedLevel) ? thinkingSuffix : "";
600
+ this.updateBehavior(this.editingStep!, "model", `${selected.fullId}${suffix}`);
606
601
  }
607
602
  this.exitEditMode();
608
603
  return;
@@ -645,6 +640,10 @@ export class ChainClarifyComponent implements Component {
645
640
  }
646
641
  }
647
642
 
643
+ private getAvailableThinkingLevels(stepIndex: number): ThinkingLevel[] {
644
+ return getSupportedThinkingLevels(findModelInfo(this.getEffectiveModel(stepIndex), this.availableModels, this.preferredProvider));
645
+ }
646
+
648
647
  /** Enter thinking level selector mode */
649
648
  private enterThinkingSelector(): void {
650
649
  if (!this.getEffectiveBehavior(this.selectedStep).model) {
@@ -654,15 +653,11 @@ export class ChainClarifyComponent implements Component {
654
653
  this.editingStep = this.selectedStep;
655
654
  this.editMode = "thinking";
656
655
 
657
- const currentModel = this.getEffectiveModel(this.selectedStep);
658
- const colonIdx = currentModel.lastIndexOf(":");
659
- if (colonIdx !== -1) {
660
- const suffix = currentModel.substring(colonIdx + 1);
661
- const levelIdx = THINKING_LEVELS.indexOf(suffix as ThinkingLevel);
662
- this.thinkingSelectedIndex = levelIdx >= 0 ? levelIdx : 0;
663
- } else {
664
- this.thinkingSelectedIndex = 0;
665
- }
656
+ const levels = this.getAvailableThinkingLevels(this.selectedStep);
657
+ const { thinkingSuffix } = splitThinkingSuffix(this.getEffectiveModel(this.selectedStep));
658
+ const suffix = thinkingSuffix.slice(1);
659
+ const levelIdx = levels.findIndex((level) => level === suffix);
660
+ this.thinkingSelectedIndex = levelIdx >= 0 ? levelIdx : Math.max(0, levels.indexOf("off"));
666
661
 
667
662
  this.tui.requestRender();
668
663
  }
@@ -673,8 +668,11 @@ export class ChainClarifyComponent implements Component {
673
668
  return;
674
669
  }
675
670
 
671
+ const levels = this.getAvailableThinkingLevels(this.editingStep!);
672
+ if (levels.length === 0) return;
673
+
676
674
  if (matchesKey(data, "return")) {
677
- const selectedLevel = THINKING_LEVELS[this.thinkingSelectedIndex];
675
+ const selectedLevel = levels[this.thinkingSelectedIndex] ?? "off";
678
676
  this.applyThinkingLevel(selectedLevel);
679
677
  this.exitEditMode();
680
678
  return;
@@ -682,14 +680,14 @@ export class ChainClarifyComponent implements Component {
682
680
 
683
681
  if (matchesKey(data, "up")) {
684
682
  this.thinkingSelectedIndex = this.thinkingSelectedIndex === 0
685
- ? THINKING_LEVELS.length - 1
683
+ ? levels.length - 1
686
684
  : this.thinkingSelectedIndex - 1;
687
685
  this.tui.requestRender();
688
686
  return;
689
687
  }
690
688
 
691
689
  if (matchesKey(data, "down")) {
692
- this.thinkingSelectedIndex = this.thinkingSelectedIndex === THINKING_LEVELS.length - 1
690
+ this.thinkingSelectedIndex = this.thinkingSelectedIndex === levels.length - 1
693
691
  ? 0
694
692
  : this.thinkingSelectedIndex + 1;
695
693
  this.tui.requestRender();
@@ -1044,13 +1042,18 @@ export class ChainClarifyComponent implements Component {
1044
1042
  "xhigh": "Maximum reasoning (ultrathink)",
1045
1043
  };
1046
1044
 
1047
- for (let i = 0; i < THINKING_LEVELS.length; i++) {
1048
- const level = THINKING_LEVELS[i];
1049
- const isSelected = i === this.thinkingSelectedIndex;
1050
- const prefix = isSelected ? th.fg("accent", "→ ") : " ";
1051
- const levelText = isSelected ? th.fg("accent", level) : level;
1052
- const desc = th.fg("dim", ` - ${levelDescriptions[level]}`);
1053
- lines.push(this.row(` ${prefix}${levelText}${desc}`));
1045
+ const levels = this.getAvailableThinkingLevels(this.editingStep!);
1046
+ if (levels.length === 0) {
1047
+ lines.push(this.row(` ${th.fg("dim", "No supported thinking levels")}`));
1048
+ } else {
1049
+ for (let i = 0; i < levels.length; i++) {
1050
+ const level = levels[i]!;
1051
+ const isSelected = i === this.thinkingSelectedIndex;
1052
+ const prefix = isSelected ? th.fg("accent", "→ ") : " ";
1053
+ const levelText = isSelected ? th.fg("accent", level) : level;
1054
+ const desc = th.fg("dim", ` - ${levelDescriptions[level]}`);
1055
+ lines.push(this.row(` ${prefix}${levelText}${desc}`));
1056
+ }
1054
1057
  }
1055
1058
 
1056
1059
  const contentLines = lines.length;
@@ -1059,7 +1062,9 @@ export class ChainClarifyComponent implements Component {
1059
1062
  lines.push(this.row(""));
1060
1063
  }
1061
1064
 
1062
- const footerText = " [Enter] Select • [Esc] Cancel • ↑↓ Navigate ";
1065
+ const footerText = levels.length === 0
1066
+ ? " [Esc] Cancel "
1067
+ : " [Enter] Select • [Esc] Cancel • ↑↓ Navigate ";
1063
1068
  lines.push(this.renderFooter(footerText));
1064
1069
 
1065
1070
  return lines;
@@ -7,7 +7,8 @@ import * as path from "node:path";
7
7
  import type { AgentToolResult } from "@mariozechner/pi-agent-core";
8
8
  import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
9
9
  import type { AgentConfig } from "../../agents/agents.ts";
10
- import { ChainClarifyComponent, type ChainClarifyResult, type BehaviorOverride, type ModelInfo } from "./chain-clarify.ts";
10
+ import { ChainClarifyComponent, type ChainClarifyResult, type BehaviorOverride } from "./chain-clarify.ts";
11
+ import { toModelInfo, type ModelInfo } from "../../shared/model-info.ts";
11
12
  import {
12
13
  resolveChainTemplates,
13
14
  createChainDir,
@@ -93,6 +94,7 @@ interface ParallelChainRunInput {
93
94
  onControlEvent?: (event: ControlEvent) => void;
94
95
  controlConfig: ResolvedControlConfig;
95
96
  childIntercomTarget?: (agent: string, index: number) => string | undefined;
97
+ orchestratorIntercomTarget?: string;
96
98
  foregroundControl?: {
97
99
  updatedAt: number;
98
100
  currentAgent?: string;
@@ -240,6 +242,7 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
240
242
  controlConfig: input.controlConfig,
241
243
  onControlEvent: input.onControlEvent,
242
244
  intercomSessionName: input.childIntercomTarget?.(task.agent, input.globalTaskIndex + taskIndex),
245
+ orchestratorIntercomTarget: input.orchestratorIntercomTarget,
243
246
  modelOverride: effectiveModel,
244
247
  availableModels: input.availableModels,
245
248
  preferredModelProvider: input.ctx.model?.provider,
@@ -313,6 +316,7 @@ interface ChainExecutionParams {
313
316
  onControlEvent?: (event: ControlEvent) => void;
314
317
  controlConfig: ResolvedControlConfig;
315
318
  childIntercomTarget?: (agent: string, index: number) => string | undefined;
319
+ orchestratorIntercomTarget?: string;
316
320
  foregroundControl?: {
317
321
  updatedAt: number;
318
322
  currentAgent?: string;
@@ -363,6 +367,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
363
367
  onControlEvent,
364
368
  controlConfig,
365
369
  childIntercomTarget,
370
+ orchestratorIntercomTarget,
366
371
  foregroundControl,
367
372
  intercomEvents,
368
373
  chainSkills: chainSkillsParam,
@@ -389,11 +394,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
389
394
  let templates: ResolvedTemplates = resolveChainTemplates(chainSteps);
390
395
  const shouldClarify = clarify !== false && ctx.hasUI && !hasParallelSteps;
391
396
  let tuiBehaviorOverrides: (BehaviorOverride | undefined)[] | undefined;
392
- const availableModels: ModelInfo[] = ctx.modelRegistry.getAvailable().map((m) => ({
393
- provider: m.provider,
394
- id: m.id,
395
- fullId: `${m.provider}/${m.id}`,
396
- }));
397
+ const availableModels: ModelInfo[] = ctx.modelRegistry.getAvailable().map(toModelInfo);
397
398
  const availableSkills = discoverAvailableSkills(cwd ?? ctx.cwd);
398
399
 
399
400
  if (shouldClarify) {
@@ -586,6 +587,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
586
587
  controlConfig,
587
588
  onControlEvent,
588
589
  childIntercomTarget,
590
+ orchestratorIntercomTarget,
589
591
  foregroundControl,
590
592
  worktreeSetup,
591
593
  maxSubagentDepth: params.maxSubagentDepth,
@@ -771,6 +773,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
771
773
  controlConfig,
772
774
  onControlEvent,
773
775
  intercomSessionName: childIntercomTarget?.(seqStep.agent, globalTaskIndex),
776
+ orchestratorIntercomTarget,
774
777
  modelOverride: effectiveModel,
775
778
  availableModels,
776
779
  preferredModelProvider: ctx.model?.provider,
@@ -153,6 +153,10 @@ async function runSingleAttempt(
153
153
  mcpDirectTools: agent.mcpDirectTools,
154
154
  promptFileStem: agent.name,
155
155
  intercomSessionName: options.intercomSessionName,
156
+ orchestratorIntercomTarget: options.orchestratorIntercomTarget,
157
+ runId: options.runId,
158
+ childAgentName: agent.name,
159
+ childIndex: options.index ?? 0,
156
160
  });
157
161
 
158
162
  const result: SingleResult = {
@@ -5,7 +5,8 @@ import type { AgentToolResult } from "@mariozechner/pi-agent-core";
5
5
  import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
6
6
  import { type AgentConfig, type AgentScope } from "../../agents/agents.ts";
7
7
  import { getArtifactsDir } from "../../shared/artifacts.ts";
8
- import { ChainClarifyComponent, type ChainClarifyResult, type ModelInfo } from "./chain-clarify.ts";
8
+ import { ChainClarifyComponent, type ChainClarifyResult } from "./chain-clarify.ts";
9
+ import { toModelInfo, type ModelInfo } from "../../shared/model-info.ts";
9
10
  import { executeChain } from "./chain-execution.ts";
10
11
  import { resolveExecutionAgentScope } from "../../agents/agent-scope.ts";
11
12
  import { handleManagementAction } from "../../agents/agent-management.ts";
@@ -29,6 +30,7 @@ import {
29
30
  import { discoverAvailableSkills, normalizeSkillInput } from "../../agents/skills.ts";
30
31
  import { executeAsyncChain, executeAsyncSingle, isAsyncAvailable } from "../background/async-execution.ts";
31
32
  import { createForkContextResolver } from "../../shared/fork-context.ts";
33
+ import { resolveCurrentSessionId } from "../../shared/session-identity.ts";
32
34
  import { applyIntercomBridgeToAgent, INTERCOM_BRIDGE_MARKER, resolveIntercomBridge, resolveIntercomSessionTarget, resolveSubagentIntercomTarget, type IntercomBridgeState } from "../../intercom/intercom-bridge.ts";
33
35
  import { formatControlIntercomMessage, formatControlNoticeMessage, resolveControlConfig, shouldNotifyControlEvent } from "../shared/subagent-control.ts";
34
36
  import { finalizeSingleOutput, injectSingleOutputInstruction, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.ts";
@@ -65,6 +67,7 @@ import {
65
67
  type MaxOutputConfig,
66
68
  type ResolvedControlConfig,
67
69
  type SingleResult,
70
+ type SubagentRunMode,
68
71
  type SubagentState,
69
72
  DEFAULT_ARTIFACT_CONFIG,
70
73
  SUBAGENT_ACTIONS,
@@ -332,7 +335,7 @@ async function resumeAsyncRun(input: {
332
335
  }
333
336
 
334
337
  const parentSessionFile = input.ctx.sessionManager.getSessionFile() ?? null;
335
- input.deps.state.currentSessionId = parentSessionFile ?? `session-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
338
+ input.deps.state.currentSessionId = resolveCurrentSessionId(input.ctx.sessionManager);
336
339
  const effectiveCwd = target.cwd ?? input.requestCwd;
337
340
  const scope: AgentScope = resolveExecutionAgentScope(input.params.agentScope);
338
341
  const discoveredAgents = input.deps.discoverAgents(effectiveCwd, scope).agents;
@@ -356,11 +359,7 @@ async function resumeAsyncRun(input: {
356
359
 
357
360
  const runId = randomUUID().slice(0, 8);
358
361
  const artifactConfig: ArtifactConfig = { ...DEFAULT_ARTIFACT_CONFIG, enabled: input.params.artifacts !== false };
359
- const availableModels = input.ctx.modelRegistry.getAvailable().map((m) => ({
360
- provider: m.provider,
361
- id: m.id,
362
- fullId: `${m.provider}/${m.id}`,
363
- }));
362
+ const availableModels = input.ctx.modelRegistry.getAvailable().map(toModelInfo);
364
363
  const result = executeAsyncSingle(runId, {
365
364
  agent: target.agent,
366
365
  task: buildRevivedAsyncTask(target, followUp),
@@ -423,7 +422,7 @@ async function emitForegroundResultIntercom(input: {
423
422
  pi: ExtensionAPI;
424
423
  intercomBridge: IntercomBridgeState;
425
424
  runId: string;
426
- mode: "single" | "parallel" | "chain";
425
+ mode: SubagentRunMode;
427
426
  results: SingleResult[];
428
427
  chainSteps?: number;
429
428
  }): Promise<ReturnType<typeof buildSubagentResultIntercomPayload> | null> {
@@ -459,7 +458,7 @@ async function maybeBuildForegroundIntercomReceipt(input: {
459
458
  pi: ExtensionAPI;
460
459
  intercomBridge: IntercomBridgeState;
461
460
  runId: string;
462
- mode: "single" | "parallel" | "chain";
461
+ mode: SubagentRunMode;
463
462
  details: Details;
464
463
  }): Promise<{ text: string; details: Details } | null> {
465
464
  const payload = await emitForegroundResultIntercom({
@@ -780,11 +779,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
780
779
  currentSessionId: deps.state.currentSessionId!,
781
780
  currentModelProvider: ctx.model?.provider,
782
781
  };
783
- const availableModels: ModelInfo[] = ctx.modelRegistry.getAvailable().map((m) => ({
784
- provider: m.provider,
785
- id: m.id,
786
- fullId: `${m.provider}/${m.id}`,
787
- }));
782
+ const availableModels: ModelInfo[] = ctx.modelRegistry.getAvailable().map(toModelInfo);
788
783
  const currentMaxSubagentDepth = resolveCurrentMaxSubagentDepth(deps.config.maxSubagentDepth);
789
784
  const currentProvider = ctx.model?.provider;
790
785
  const controlIntercomTarget = intercomBridge.active ? intercomBridge.orchestratorTarget : undefined;
@@ -949,6 +944,7 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
949
944
  onControlEvent,
950
945
  controlConfig,
951
946
  childIntercomTarget: childIntercomTarget ? (agent, index) => childIntercomTarget(runId, agent, index) : undefined,
947
+ orchestratorIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
952
948
  foregroundControl,
953
949
  chainSkills,
954
950
  chainDir: params.chainDir,
@@ -977,11 +973,7 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
977
973
  chain: asyncChain,
978
974
  agents,
979
975
  ctx: asyncCtx,
980
- availableModels: ctx.modelRegistry.getAvailable().map((m) => ({
981
- provider: m.provider,
982
- id: m.id,
983
- fullId: `${m.provider}/${m.id}`,
984
- })),
976
+ availableModels: ctx.modelRegistry.getAvailable().map(toModelInfo),
985
977
  cwd: effectiveCwd,
986
978
  maxOutput: params.maxOutput,
987
979
  artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
@@ -1043,6 +1035,7 @@ interface ForegroundParallelRunInput {
1043
1035
  controlConfig: ResolvedControlConfig;
1044
1036
  onControlEvent?: (event: ControlEvent) => void;
1045
1037
  childIntercomTarget?: (agent: string, index: number) => string | undefined;
1038
+ orchestratorIntercomTarget?: string;
1046
1039
  foregroundControl?: SubagentState["foregroundControls"] extends Map<string, infer T> ? T : never;
1047
1040
  concurrencyLimit: number;
1048
1041
  liveResults: (SingleResult | undefined)[];
@@ -1201,6 +1194,7 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
1201
1194
  controlConfig: input.controlConfig,
1202
1195
  onControlEvent: input.onControlEvent,
1203
1196
  intercomSessionName: input.childIntercomTarget?.(task.agent, index),
1197
+ orchestratorIntercomTarget: input.orchestratorIntercomTarget,
1204
1198
  modelOverride: input.modelOverrides[index],
1205
1199
  availableModels: input.availableModels,
1206
1200
  preferredModelProvider: input.ctx.model?.provider,
@@ -1305,11 +1299,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
1305
1299
  }
1306
1300
 
1307
1301
  const currentProvider = ctx.model?.provider;
1308
- const availableModels: ModelInfo[] = ctx.modelRegistry.getAvailable().map((m) => ({
1309
- provider: m.provider,
1310
- id: m.id,
1311
- fullId: `${m.provider}/${m.id}`,
1312
- }));
1302
+ const availableModels: ModelInfo[] = ctx.modelRegistry.getAvailable().map(toModelInfo);
1313
1303
  let taskTexts = tasks.map((t) => t.task);
1314
1304
  const skillOverrides: (string[] | false | undefined)[] = tasks.map((t) =>
1315
1305
  normalizeSkillInput(t.skill),
@@ -1482,6 +1472,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
1482
1472
  controlConfig,
1483
1473
  onControlEvent,
1484
1474
  childIntercomTarget: childIntercomTarget ? (agent, index) => childIntercomTarget(runId, agent, index) : undefined,
1475
+ orchestratorIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
1485
1476
  foregroundControl,
1486
1477
  concurrencyLimit: parallelConcurrency,
1487
1478
  maxSubagentDepths,
@@ -1586,11 +1577,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
1586
1577
  }
1587
1578
 
1588
1579
  const currentProvider = ctx.model?.provider;
1589
- const availableModels: ModelInfo[] = ctx.modelRegistry.getAvailable().map((m) => ({
1590
- provider: m.provider,
1591
- id: m.id,
1592
- fullId: `${m.provider}/${m.id}`,
1593
- }));
1580
+ const availableModels: ModelInfo[] = ctx.modelRegistry.getAvailable().map(toModelInfo);
1594
1581
  let task = params.task ?? "";
1595
1582
  let modelOverride: string | undefined = resolveModelCandidate(
1596
1583
  (params.model as string | undefined) ?? agentConfig.model,
@@ -1751,6 +1738,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
1751
1738
  controlConfig,
1752
1739
  onControlEvent,
1753
1740
  intercomSessionName: childIntercomTarget,
1741
+ orchestratorIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
1754
1742
  index: 0,
1755
1743
  modelOverride,
1756
1744
  availableModels,
@@ -1966,7 +1954,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
1966
1954
  const scope: AgentScope = resolveExecutionAgentScope(effectiveParams.agentScope);
1967
1955
  const effectiveCwd = effectiveParams.cwd ?? ctx.cwd;
1968
1956
  const parentSessionFile = ctx.sessionManager.getSessionFile() ?? null;
1969
- deps.state.currentSessionId = parentSessionFile ?? `session-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
1957
+ deps.state.currentSessionId = resolveCurrentSessionId(ctx.sessionManager);
1970
1958
  const discoveredAgents = deps.discoverAgents(effectiveCwd, scope).agents;
1971
1959
  effectiveParams = applyAgentDefaultContext(effectiveParams, discoveredAgents);
1972
1960
  const sessionName = resolveIntercomSessionTarget(deps.pi.getSessionName(), ctx.sessionManager.getSessionId());
@@ -1,10 +1,7 @@
1
+ import type { ModelInfo as AvailableModelInfo } from "../../shared/model-info.ts";
1
2
  import type { Usage } from "../../shared/types.ts";
2
3
 
3
- export interface AvailableModelInfo {
4
- provider: string;
5
- id: string;
6
- fullId: string;
7
- }
4
+ export type { AvailableModelInfo };
8
5
 
9
6
  interface ModelAttemptSummary {
10
7
  model: string;
@@ -7,6 +7,10 @@ const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"];
7
7
  const TASK_ARG_LIMIT = 8000;
8
8
  const PROMPT_RUNTIME_EXTENSION_PATH = path.join(path.dirname(fileURLToPath(import.meta.url)), "subagent-prompt-runtime.ts");
9
9
  export const SUBAGENT_CHILD_ENV = "PI_SUBAGENT_CHILD";
10
+ export const SUBAGENT_ORCHESTRATOR_TARGET_ENV = "PI_SUBAGENT_ORCHESTRATOR_TARGET";
11
+ export const SUBAGENT_RUN_ID_ENV = "PI_SUBAGENT_RUN_ID";
12
+ export const SUBAGENT_CHILD_AGENT_ENV = "PI_SUBAGENT_CHILD_AGENT";
13
+ export const SUBAGENT_CHILD_INDEX_ENV = "PI_SUBAGENT_CHILD_INDEX";
10
14
 
11
15
  interface BuildPiArgsInput {
12
16
  baseArgs: string[];
@@ -25,6 +29,10 @@ interface BuildPiArgsInput {
25
29
  mcpDirectTools?: string[];
26
30
  promptFileStem?: string;
27
31
  intercomSessionName?: string;
32
+ orchestratorIntercomTarget?: string;
33
+ runId?: string;
34
+ childAgentName?: string;
35
+ childIndex?: number;
28
36
  }
29
37
 
30
38
  interface BuildPiArgsResult {
@@ -119,6 +127,18 @@ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
119
127
  if (input.intercomSessionName) {
120
128
  env.PI_SUBAGENT_INTERCOM_SESSION_NAME = input.intercomSessionName;
121
129
  }
130
+ if (input.orchestratorIntercomTarget) {
131
+ env[SUBAGENT_ORCHESTRATOR_TARGET_ENV] = input.orchestratorIntercomTarget;
132
+ }
133
+ if (input.runId) {
134
+ env[SUBAGENT_RUN_ID_ENV] = input.runId;
135
+ }
136
+ if (input.childAgentName) {
137
+ env[SUBAGENT_CHILD_AGENT_ENV] = input.childAgentName;
138
+ }
139
+ if (input.childIndex !== undefined) {
140
+ env[SUBAGENT_CHILD_INDEX_ENV] = String(input.childIndex);
141
+ }
122
142
  if (input.mcpDirectTools?.length) {
123
143
  env.MCP_DIRECT_TOOLS = input.mcpDirectTools.join(",");
124
144
  } else {