@ian-pascoe/pi-minimal-subagents 0.4.0 → 0.6.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.
@@ -1,9 +1,15 @@
1
1
  import type { AgentMessage } from "@earendil-works/pi-agent-core";
2
2
  import { contentText, type ImageContent, type TextContent } from "@earendil-works/pi-ai";
3
3
  import { truncateTail } from "@earendil-works/pi-coding-agent";
4
- import type { RecentAgentActivity, SessionContextMode } from "./minimal-subagents-types.js";
4
+ import type {
5
+ ChildAgentTranscriptSnapshot,
6
+ RecentAgentActivity,
7
+ SessionContextMode,
8
+ } from "./minimal-subagents-types.js";
5
9
 
6
10
  const RECENT_AGENT_ACTIVITY_LIMIT = 12;
11
+ const CHILD_AGENT_TRANSCRIPT_MESSAGE_LIMIT = 24;
12
+ const CHILD_AGENT_TRANSCRIPT_PAIR_WINDOW = CHILD_AGENT_TRANSCRIPT_MESSAGE_LIMIT * 2;
7
13
  const RECENT_AGENT_ACTIVITY_MAX_LINES = 20;
8
14
  const RECENT_AGENT_ACTIVITY_MAX_BYTES = 2 * 1024;
9
15
 
@@ -18,17 +24,137 @@ export function snapshotCommittedContext(
18
24
  }
19
25
 
20
26
  function boundedRecentActivityContent(label: string, content: string): RecentAgentActivity {
27
+ const bounded = boundRecentAgentText(content);
28
+ return { label, content: bounded.content, truncated: bounded.truncated };
29
+ }
30
+
31
+ /** Contains bounded Child Agent text and whether truncation removed earlier content. */
32
+ export interface BoundedRecentAgentText {
33
+ readonly content: string;
34
+ readonly truncated: boolean;
35
+ }
36
+
37
+ /** Bound Child Agent transcript fallback text to the Recent Activity line and byte limits. */
38
+ export function boundRecentAgentText(content: string): BoundedRecentAgentText {
21
39
  const bounded = truncateTail(content, {
22
40
  maxLines: RECENT_AGENT_ACTIVITY_MAX_LINES,
23
41
  maxBytes: RECENT_AGENT_ACTIVITY_MAX_BYTES,
24
42
  });
25
- return { label, content: bounded.content, truncated: bounded.truncated };
43
+ return { content: bounded.content, truncated: bounded.truncated };
26
44
  }
27
45
 
28
46
  function visibleMessageContent(content: string | readonly (TextContent | ImageContent)[]): string {
29
47
  return contentText(content, "\n\n") || "(no text content)";
30
48
  }
31
49
 
50
+ function omitAgentMessageImages(message: AgentMessage): AgentMessage {
51
+ if (message.role === "user" || message.role === "custom") {
52
+ return {
53
+ ...structuredClone(message),
54
+ content: Array.isArray(message.content)
55
+ ? message.content
56
+ .filter((content) => content.type !== "image")
57
+ .map((content) => structuredClone(content))
58
+ : message.content,
59
+ };
60
+ }
61
+ if (message.role === "toolResult") {
62
+ return {
63
+ ...structuredClone(message),
64
+ content: message.content
65
+ .filter((content) => content.type !== "image")
66
+ .map((content) => structuredClone(content)),
67
+ };
68
+ }
69
+ return structuredClone(message);
70
+ }
71
+
72
+ interface IndexedTranscriptMessage {
73
+ message: AgentMessage;
74
+ originalIndex: number;
75
+ streaming: boolean;
76
+ }
77
+
78
+ /** Select at most 48 recent raw messages, retaining cross-cutoff tool pairs and omitting images. */
79
+ export function selectChildAgentTranscript(
80
+ messages: readonly AgentMessage[],
81
+ streamingAssistantMessage?: AgentMessage,
82
+ ): ChildAgentTranscriptSnapshot {
83
+ const pairWindow = messages.slice(-CHILD_AGENT_TRANSCRIPT_PAIR_WINDOW);
84
+ const firstWindowIndex = messages.length - pairWindow.length;
85
+ const indexed: IndexedTranscriptMessage[] = [
86
+ ...pairWindow.map((message, windowIndex) => ({
87
+ message,
88
+ originalIndex: firstWindowIndex + windowIndex,
89
+ streaming: false,
90
+ })),
91
+ ...(streamingAssistantMessage
92
+ ? [
93
+ {
94
+ message: streamingAssistantMessage,
95
+ originalIndex: messages.length,
96
+ streaming: true,
97
+ },
98
+ ]
99
+ : []),
100
+ ];
101
+ const tail = indexed.slice(-CHILD_AGENT_TRANSCRIPT_MESSAGE_LIMIT).map((item) => ({
102
+ ...item,
103
+ message: omitAgentMessageImages(item.message),
104
+ }));
105
+ const selectedIndexes = new Set(tail.map(({ originalIndex }) => originalIndex));
106
+ const calls = new Map<
107
+ string,
108
+ { originalIndex: number; assistant: Extract<AgentMessage, { role: "assistant" }> }
109
+ >();
110
+ for (const item of indexed) {
111
+ if (item.message.role !== "assistant") continue;
112
+ for (const content of item.message.content) {
113
+ if (content.type === "toolCall") {
114
+ calls.set(content.id, { originalIndex: item.originalIndex, assistant: item.message });
115
+ }
116
+ }
117
+ }
118
+
119
+ const retained = tail.filter(
120
+ (item) => item.message.role !== "toolResult" || calls.has(item.message.toolCallId),
121
+ );
122
+ const missingCalls = new Map<
123
+ number,
124
+ { assistant: Extract<AgentMessage, { role: "assistant" }>; callIds: Set<string> }
125
+ >();
126
+ for (const item of retained) {
127
+ if (item.message.role !== "toolResult") continue;
128
+ const call = calls.get(item.message.toolCallId);
129
+ if (!call || selectedIndexes.has(call.originalIndex)) continue;
130
+ const missing = missingCalls.get(call.originalIndex) ?? {
131
+ assistant: call.assistant,
132
+ callIds: new Set<string>(),
133
+ };
134
+ missing.callIds.add(item.message.toolCallId);
135
+ missingCalls.set(call.originalIndex, missing);
136
+ }
137
+ const prefixes: IndexedTranscriptMessage[] = [...missingCalls.entries()]
138
+ .sort(([left], [right]) => left - right)
139
+ .map(([originalIndex, missing]) => ({
140
+ message: {
141
+ ...structuredClone(missing.assistant),
142
+ content: missing.assistant.content
143
+ .filter((content) => content.type === "toolCall" && missing.callIds.has(content.id))
144
+ .map((content) => structuredClone(content)),
145
+ },
146
+ originalIndex,
147
+ streaming: false,
148
+ }));
149
+ const selected = [...prefixes, ...retained];
150
+ const streamingAssistantIndex = selected.findIndex(({ streaming }) => streaming);
151
+ return {
152
+ messages: selected.map(({ message }) => message),
153
+ streamingAssistantIndex: streamingAssistantIndex >= 0 ? streamingAssistantIndex : undefined,
154
+ toolDefinitions: [],
155
+ };
156
+ }
157
+
32
158
  /** Build a bounded recent activity tail from message text, reasoning, and tool work. */
33
159
  export function buildRecentAgentActivity(messages: readonly AgentMessage[]): RecentAgentActivity[] {
34
160
  const activity: RecentAgentActivity[] = [];
@@ -1,8 +1,10 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import {
3
3
  assembleImportedContext,
4
+ boundRecentAgentText,
4
5
  buildRecentAgentActivity,
5
6
  contextContainsImages,
7
+ selectChildAgentTranscript,
6
8
  } from "./minimal-subagents-context.js";
7
9
  import {
8
10
  canAgentContractSpawn,
@@ -44,6 +46,7 @@ import type {
44
46
  CallerSnapshot,
45
47
  CancelResult,
46
48
  ChildAgentRuntime,
49
+ ChildAgentTranscriptSnapshot,
47
50
  CoordinatorDependencies,
48
51
  CoordinatorMessage,
49
52
  DeleteResult,
@@ -449,6 +452,25 @@ export class MinimalSubagentsCoordinator {
449
452
  };
450
453
  }
451
454
 
455
+ /** Lazily inspect one Child Agent's bounded process-local transcript for trusted UI. */
456
+ inspectTranscript(agentId: string): ChildAgentTranscriptSnapshot {
457
+ const agent = this.requireAgent(agentId);
458
+ const runtime = this.runtimes.get(agentId);
459
+ const liveSnapshot = runtime?.snapshotActivityTranscript?.();
460
+ if (liveSnapshot) return liveSnapshot;
461
+ if (runtime) return selectChildAgentTranscript(runtime.snapshotActivityMessages());
462
+ const fallback =
463
+ agent.unavailable_reason ||
464
+ agent.latest_result?.error ||
465
+ agent.latest_result?.output ||
466
+ "Child Agent runtime is not available.";
467
+ return {
468
+ messages: [],
469
+ toolDefinitions: [],
470
+ fallback: boundRecentAgentText(fallback).content,
471
+ };
472
+ }
473
+
452
474
  /** Report whether root or one explicitly authorized child can create another agent. */
453
475
  canAgentSpawn(callerId: string): boolean {
454
476
  if (callerId === "root") return true;
@@ -1453,7 +1475,8 @@ export class MinimalSubagentsCoordinator {
1453
1475
  const elapsed = agent.active_turn_started_at
1454
1476
  ? Math.max(0, this.now().getTime() - new Date(agent.active_turn_started_at).getTime())
1455
1477
  : undefined;
1456
- const runtimeProfile = this.runtimes.get(agent.agent_id)?.getRuntimeProfile() ?? {
1478
+ const runtime = this.runtimes.get(agent.agent_id);
1479
+ const runtimeProfile = runtime?.getRuntimeProfile() ?? {
1457
1480
  model: agent.launch_contract.model,
1458
1481
  thinking_level: agent.launch_contract.thinking_level,
1459
1482
  };
@@ -1467,7 +1490,7 @@ export class MinimalSubagentsCoordinator {
1467
1490
  ? { turn_id: agent.latest_result.turn_id, status: agent.latest_result.status }
1468
1491
  : undefined,
1469
1492
  ...runtimeProfile,
1470
- tools: [...agent.launch_contract.ordinary_tools],
1493
+ tools: runtime?.getActiveToolNames?.() ?? [...agent.launch_contract.ordinary_tools],
1471
1494
  elapsed_ms: elapsed ?? agent.latest_result?.elapsed_ms,
1472
1495
  latest_activity_at: agent.latest_activity_at ?? agent.created_at,
1473
1496
  task: agent.task,
@@ -6,6 +6,7 @@ import {
6
6
  SessionManager,
7
7
  SettingsManager,
8
8
  type ExtensionAPI,
9
+ type ExtensionCommandContext,
9
10
  type ExtensionContext,
10
11
  type ExtensionFactory,
11
12
  type MessageEndEvent,
@@ -15,8 +16,25 @@ import {
15
16
  type SessionStartEvent,
16
17
  type SessionTreeEvent,
17
18
  } from "@earendil-works/pi-coding-agent";
19
+ import {
20
+ createSubagentAccessBranchRecord,
21
+ reconcileCoordinatorToolAccess,
22
+ replaySubagentAccessBranch,
23
+ resolveSubagentAccessSnapshot,
24
+ SUBAGENT_ACCESS_ENTRY_TYPE,
25
+ type SubagentAccessOverride,
26
+ type SubagentAccessReplayDiagnostic,
27
+ } from "./minimal-subagents-access.js";
28
+ import {
29
+ completeSubagentsCommandArguments,
30
+ parseSubagentsCommandArguments,
31
+ type SubagentsCommand,
32
+ } from "./minimal-subagents-command.js";
18
33
  import { MinimalSubagentsCoordinator } from "./minimal-subagents-coordinator.js";
19
- import { resolveMinimalSubagentsSettings } from "./minimal-subagents-config.js";
34
+ import {
35
+ resolveMinimalSubagentsSettings,
36
+ type ResolvedSubagentAccessSettings,
37
+ } from "./minimal-subagents-config.js";
20
38
  import { snapshotCommittedContext } from "./minimal-subagents-context.js";
21
39
  import {
22
40
  isForkDestinationForSource,
@@ -25,7 +43,6 @@ import {
25
43
  } from "./minimal-subagents-fork-lifecycle.js";
26
44
  import {
27
45
  buildEligibleModelIds,
28
- COORDINATOR_TOOL_NAMES,
29
46
  excludeCoordinatorTools,
30
47
  } from "./minimal-subagents-capabilities.js";
31
48
  import {
@@ -39,9 +56,15 @@ import {
39
56
  findDeliveryEvidence,
40
57
  PiAgentSessionFactory,
41
58
  type PiAgentSessionFactoryOptions,
59
+ type RuntimeToolAdapter,
42
60
  unavailableAgent,
43
61
  } from "./minimal-subagents-sessions.js";
44
62
  import { shutdownMinimalSubagentsSession } from "./minimal-subagents-shutdown.js";
63
+ import { MinimalSubagentsSettingsWriter } from "./minimal-subagents-settings-writer.js";
64
+ import {
65
+ MinimalSubagentsStatusPanelController,
66
+ type MinimalSubagentsStatusAccess,
67
+ } from "./minimal-subagents-status-panel.js";
45
68
  import { createCoordinatorToolDefinitions } from "./minimal-subagents-tools.js";
46
69
  import {
47
70
  renderMinimalSubagentsMessage,
@@ -60,6 +83,43 @@ import type {
60
83
  } from "./minimal-subagents-types.js";
61
84
 
62
85
  const EXTENSION_ENTRYPOINT = fileURLToPath(new URL("./index.ts", import.meta.url));
86
+ const CODEX_CONVERSION_TOOL_ADAPTER_SOURCE = "npm:@howaboua/pi-codex-conversion";
87
+ const CODEX_CONVERSION_TOOL_REPLACEMENTS = [
88
+ { sourceToolNames: ["bash"], runtimeToolNames: ["exec_command", "write_stdin"] },
89
+ { sourceToolNames: ["bash", "edit", "write"], runtimeToolNames: ["apply_patch"] },
90
+ { sourceToolNames: ["bash", "edit", "write"], runtimeToolNames: ["exec", "wait"] },
91
+ { sourceToolNames: ["bash", "edit", "write"], runtimeToolNames: ["notebook"] },
92
+ ] as const;
93
+
94
+ function codexConversionRuntimeToolAdapters(pi: ExtensionAPI): RuntimeToolAdapter[] {
95
+ const extensions = new Map<
96
+ string,
97
+ {
98
+ path: string;
99
+ source: string;
100
+ toolNames: string[];
101
+ }
102
+ >();
103
+ for (const tool of pi.getAllTools()) {
104
+ const { path, source } = tool.sourceInfo;
105
+ if (path.startsWith("<")) continue;
106
+ const extension = extensions.get(path) ?? { path, source, toolNames: [] };
107
+ extension.toolNames.push(tool.name);
108
+ extensions.set(path, extension);
109
+ }
110
+ return [...extensions.values()]
111
+ .filter(
112
+ (extension) =>
113
+ extension.source === CODEX_CONVERSION_TOOL_ADAPTER_SOURCE ||
114
+ extension.source.startsWith(`${CODEX_CONVERSION_TOOL_ADAPTER_SOURCE}@`),
115
+ )
116
+ .map((extension) => ({
117
+ toolNames: extension.toolNames,
118
+ replacements: CODEX_CONVERSION_TOOL_REPLACEMENTS.filter((replacement) =>
119
+ replacement.runtimeToolNames.every((toolName) => extension.toolNames.includes(toolName)),
120
+ ),
121
+ }));
122
+ }
63
123
 
64
124
  function currentConversationMessages(context: ExtensionContext): AgentMessage[] {
65
125
  const entries = context.sessionManager.getEntries();
@@ -294,6 +354,25 @@ async function waitForRootSessionIdle(context: ExtensionContext): Promise<void>
294
354
  }
295
355
  }
296
356
 
357
+ interface ActiveSubagentAccessSession {
358
+ readonly agentDir: string;
359
+ readonly eligibleModelIds: readonly string[];
360
+ readonly context: ExtensionContext;
361
+ settings: ResolvedSubagentAccessSettings;
362
+ branchOverride: SubagentAccessOverride;
363
+ }
364
+
365
+ function reportInvalidSubagentAccessRecords(
366
+ context: ExtensionContext,
367
+ ): (diagnostics: SubagentAccessReplayDiagnostic[]) => void {
368
+ return (diagnostics) => {
369
+ context.ui.notify(
370
+ `Minimal subagents ignored ${diagnostics.length} invalid Subagent Access branch record${diagnostics.length === 1 ? "" : "s"}.`,
371
+ "warning",
372
+ );
373
+ };
374
+ }
375
+
297
376
  /** SDK and runtime construction effects required by the Minimal Subagents lifecycle controller. */
298
377
  export interface MinimalSubagentsLifecycleEffects {
299
378
  /** Resolve Pi's current agent directory without coupling lifecycle tests to process configuration. */
@@ -311,6 +390,8 @@ const productionLifecycleEffects: MinimalSubagentsLifecycleEffects = {
311
390
  export class MinimalSubagentsLifecycleController {
312
391
  private coordinator: MinimalSubagentsCoordinator | undefined;
313
392
  private uiController: MinimalSubagentsUiController | undefined;
393
+ private statusPanelController: MinimalSubagentsStatusPanelController | undefined;
394
+ private accessSession: ActiveSubagentAccessSession | undefined;
314
395
  private preparedFork:
315
396
  | { sourceSessionFile: string; selectedBranchSnapshot: RegistrySnapshot }
316
397
  | undefined;
@@ -325,6 +406,11 @@ export class MinimalSubagentsLifecycleController {
325
406
  register(): void {
326
407
  this.pi.registerMessageRenderer("minimal-subagents.message", renderMinimalSubagentsMessage);
327
408
  this.pi.registerMessageRenderer("minimal-subagents.result", renderMinimalSubagentsResult);
409
+ this.pi.registerCommand("subagents", {
410
+ description: "Control Subagent Access and inspect Child Agents",
411
+ getArgumentCompletions: completeSubagentsCommandArguments,
412
+ handler: (args, context) => this.runSubagentsCommand(args, context),
413
+ });
328
414
 
329
415
  this.pi.on("session_start", (event, context) => this.startSession(event, context));
330
416
  this.pi.on("session_before_fork", (event, context) => this.prepareSessionFork(event, context));
@@ -350,6 +436,10 @@ export class MinimalSubagentsLifecycleController {
350
436
  settingsManager,
351
437
  eligibleModelIds,
352
438
  );
439
+ const branchOverride = replaySubagentAccessBranch(
440
+ context.sessionManager.getBranch(),
441
+ reportInvalidSubagentAccessRecords(context),
442
+ ).override;
353
443
  if (minimalSubagentsConfig.warnings.length > 0) {
354
444
  context.ui.notify(
355
445
  `Minimal subagents configuration warnings:\n- ${minimalSubagentsConfig.warnings.join("\n- ")}`,
@@ -386,6 +476,7 @@ export class MinimalSubagentsLifecycleController {
386
476
  eligibleModelIds,
387
477
  modelScopeRestricted: enabledModelPatterns !== undefined,
388
478
  availableToolNames,
479
+ getRuntimeToolAdapters: () => codexConversionRuntimeToolAdapters(this.pi),
389
480
  projectTrusted: context.isProjectTrusted(),
390
481
  maxSubagentDepth: minimalSubagentsConfig.maxSubagentDepth,
391
482
  onChildSessionActivity: () => activeCoordinator?.scheduleDeliveryReconciliation(),
@@ -488,7 +579,19 @@ export class MinimalSubagentsLifecycleController {
488
579
  onAttention: (message) => context.ui.notify(message, "error"),
489
580
  });
490
581
  for (const tool of rootTools) this.pi.registerTool(tool);
491
- this.pi.setActiveTools([...new Set([...this.pi.getActiveTools(), ...COORDINATOR_TOOL_NAMES])]);
582
+ this.accessSession = {
583
+ agentDir,
584
+ eligibleModelIds,
585
+ context,
586
+ settings: minimalSubagentsConfig.subagentAccess,
587
+ branchOverride,
588
+ };
589
+ this.applySubagentAccess();
590
+ this.statusPanelController = new MinimalSubagentsStatusPanelController(
591
+ activeCoordinator,
592
+ context,
593
+ () => this.currentSubagentStatusAccess(),
594
+ );
492
595
 
493
596
  if (hasHistoricalChildIdentity(context.sessionManager.getBranch())) {
494
597
  context.ui.notify(
@@ -538,9 +641,122 @@ export class MinimalSubagentsLifecycleController {
538
641
  );
539
642
  await this.coordinator.restore(snapshot);
540
643
  this.coordinator.writeCheckpoint();
644
+ const replayedAccess = replaySubagentAccessBranch(
645
+ context.sessionManager.getBranch(),
646
+ reportInvalidSubagentAccessRecords(context),
647
+ );
648
+ if (this.accessSession) {
649
+ this.accessSession.branchOverride = replayedAccess.override;
650
+ this.applySubagentAccess();
651
+ }
541
652
  this.uiController?.refresh();
542
653
  }
543
654
 
655
+ private async runSubagentsCommand(args: string, context: ExtensionCommandContext): Promise<void> {
656
+ const parsed = parseSubagentsCommandArguments(args);
657
+ if (!parsed.ok) {
658
+ context.ui.notify(parsed.message, "error");
659
+ return;
660
+ }
661
+ if (!this.accessSession || !this.coordinator) {
662
+ context.ui.notify("Minimal subagents is not active for this session.", "error");
663
+ return;
664
+ }
665
+ if (parsed.command.action === "status") {
666
+ await this.statusPanelController?.open();
667
+ return;
668
+ }
669
+ await this.changeSubagentAccess(parsed.command, context);
670
+ }
671
+
672
+ private async changeSubagentAccess(
673
+ command: Exclude<SubagentsCommand, { action: "status" }>,
674
+ context: ExtensionCommandContext,
675
+ ): Promise<void> {
676
+ const accessSession = this.accessSession;
677
+ if (!accessSession) return;
678
+ const requestedOverride: SubagentAccessOverride =
679
+ command.action === "reset" ? "inherit" : command.action === "enable" ? "enabled" : "disabled";
680
+
681
+ let persistedScope: "global" | "project" | undefined;
682
+ if (command.scope !== "session") {
683
+ const writeResult = await new MinimalSubagentsSettingsWriter(
684
+ context,
685
+ () => accessSession.agentDir,
686
+ ).writeMinimalSubagentsEnabled(
687
+ command.scope,
688
+ command.action === "reset" ? undefined : command.action === "enable",
689
+ );
690
+ if (!writeResult.ok) {
691
+ context.ui.notify(writeResult.error.message, "error");
692
+ return;
693
+ }
694
+ persistedScope = command.scope;
695
+ const settingsManager = SettingsManager.create(context.cwd, accessSession.agentDir, {
696
+ projectTrusted: context.isProjectTrusted(),
697
+ });
698
+ accessSession.settings = resolveMinimalSubagentsSettings(
699
+ settingsManager,
700
+ accessSession.eligibleModelIds,
701
+ ).subagentAccess;
702
+ }
703
+
704
+ await context.waitForIdle();
705
+ try {
706
+ this.pi.appendEntry(
707
+ SUBAGENT_ACCESS_ENTRY_TYPE,
708
+ createSubagentAccessBranchRecord(requestedOverride),
709
+ );
710
+ accessSession.branchOverride = requestedOverride;
711
+ this.applySubagentAccess();
712
+ } catch (error) {
713
+ const detail = error instanceof Error ? error.message : String(error);
714
+ context.ui.notify(
715
+ persistedScope
716
+ ? `Minimal subagents ${persistedScope} default changed, but the current session did not: ${detail}`
717
+ : `Minimal subagents could not change the current session: ${detail}`,
718
+ "error",
719
+ );
720
+ return;
721
+ }
722
+
723
+ const snapshot = this.currentSubagentAccessSnapshot();
724
+ context.ui.notify(
725
+ `Subagent Access ${snapshot.enabled ? "enabled" : "disabled"} (${command.scope}).`,
726
+ "info",
727
+ );
728
+ }
729
+
730
+ private applySubagentAccess(): void {
731
+ const accessSession = this.accessSession;
732
+ if (!accessSession) return;
733
+ const snapshot = resolveSubagentAccessSnapshot(
734
+ accessSession.settings,
735
+ accessSession.branchOverride,
736
+ this.pi.getActiveTools(),
737
+ );
738
+ this.pi.setActiveTools(
739
+ reconcileCoordinatorToolAccess(this.pi.getActiveTools(), snapshot.enabled),
740
+ );
741
+ }
742
+
743
+ private currentSubagentAccessSnapshot() {
744
+ const accessSession = this.accessSession;
745
+ if (!accessSession) throw new Error("Minimal subagents access: no active session");
746
+ return resolveSubagentAccessSnapshot(
747
+ accessSession.settings,
748
+ accessSession.branchOverride,
749
+ this.pi.getActiveTools(),
750
+ );
751
+ }
752
+
753
+ private currentSubagentStatusAccess(): MinimalSubagentsStatusAccess {
754
+ return {
755
+ ...this.currentSubagentAccessSnapshot(),
756
+ projectTrusted: this.accessSession?.context.isProjectTrusted() ?? false,
757
+ };
758
+ }
759
+
544
760
  private async reconcileMessageDelivery(
545
761
  event: MessageEndEvent,
546
762
  _context: ExtensionContext,
@@ -556,6 +772,8 @@ export class MinimalSubagentsLifecycleController {
556
772
  event: SessionShutdownEvent,
557
773
  context: ExtensionContext,
558
774
  ): Promise<void> {
775
+ this.statusPanelController?.dispose();
776
+ this.statusPanelController = undefined;
559
777
  if (this.coordinator) {
560
778
  if (event.reason === "fork" && this.preparedFork) {
561
779
  await this.coordinator.restore(this.preparedFork.selectedBranchSnapshot);
@@ -571,6 +789,7 @@ export class MinimalSubagentsLifecycleController {
571
789
  this.uiController?.dispose();
572
790
  this.uiController = undefined;
573
791
  this.coordinator = undefined;
792
+ this.accessSession = undefined;
574
793
  this.preparedFork = undefined;
575
794
  }
576
795
  }
@@ -1,5 +1,5 @@
1
1
  import type { ForkSnapshot } from "./minimal-subagents-types.js";
2
- import { canonicalPath } from "./minimal-subagents-sessions.js";
2
+ import { canonicalPath } from "./minimal-subagents-paths.js";
3
3
 
4
4
  declare global {
5
5
  // eslint-disable-next-line no-var -- A process-global handoff must be visible to replacement extension instances.
@@ -0,0 +1,8 @@
1
+ import { existsSync, realpathSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+
4
+ /** Canonicalize one path, resolving symlinks only when the target exists. */
5
+ export function canonicalPath(path: string): string {
6
+ const absolutePath = resolve(path);
7
+ return existsSync(absolutePath) ? realpathSync(absolutePath) : absolutePath;
8
+ }
@@ -181,6 +181,138 @@ const DeleteDetailsSchema = Type.Object({
181
181
  ),
182
182
  });
183
183
 
184
+ const CoordinatorTurnStatusOutputSchema = Type.Union([
185
+ Type.Literal("completed"),
186
+ Type.Literal("failed"),
187
+ Type.Literal("cancelled"),
188
+ Type.Literal("interrupted"),
189
+ ]);
190
+ const CoordinatorTurnOutputSchema = Type.Object({
191
+ agent_id: Type.String(),
192
+ turn_id: Type.String(),
193
+ status: CoordinatorTurnStatusOutputSchema,
194
+ output: Type.String(),
195
+ error: Type.Optional(Type.String()),
196
+ usage: Type.Optional(RenderUsageSchema),
197
+ elapsed_ms: Type.Optional(Type.Number()),
198
+ });
199
+ const CoordinatorAgentSummaryOutputSchema = Type.Object({
200
+ agent_id: Type.String(),
201
+ parent_id: Type.String(),
202
+ state: Type.Union([Type.Literal("running"), Type.Literal("idle")]),
203
+ availability: Type.Union([Type.Literal("available"), Type.Literal("unavailable")]),
204
+ active_turn_id: Type.Optional(Type.String()),
205
+ latest_turn: Type.Optional(
206
+ Type.Object({
207
+ turn_id: Type.String(),
208
+ status: Type.Union([Type.Literal("running"), CoordinatorTurnStatusOutputSchema]),
209
+ }),
210
+ ),
211
+ tools: Type.Array(Type.String()),
212
+ elapsed_ms: Type.Optional(Type.Number()),
213
+ latest_activity_at: Type.Optional(Type.String()),
214
+ task: Type.Optional(Type.String()),
215
+ child_count: Type.Number(),
216
+ children: Type.Array(Type.Unknown()),
217
+ model: Type.String(),
218
+ thinking_level: Type.String(),
219
+ });
220
+ const CoordinatorAgentDetailOutputSchema = Type.Object({
221
+ ...CoordinatorAgentSummaryOutputSchema.properties,
222
+ session_file: Type.Optional(Type.String()),
223
+ launch_contract: Type.Object({
224
+ session_context: Type.String(),
225
+ project_context: Type.String(),
226
+ model: Type.String(),
227
+ thinking_level: Type.String(),
228
+ tools: Type.Optional(
229
+ Type.Union([
230
+ Type.Literal("none"),
231
+ Type.Literal("read"),
232
+ Type.Literal("modify"),
233
+ Type.Array(Type.String()),
234
+ ]),
235
+ ),
236
+ ordinary_tools: Type.Array(Type.String()),
237
+ delegation: Type.Optional(Type.Union([Type.Literal("none"), Type.Literal("fanout")])),
238
+ }),
239
+ capability_ceiling: Type.Array(Type.String()),
240
+ spawn_entry_id: Type.String(),
241
+ recent_messages: Type.Array(
242
+ Type.Object({
243
+ source_agent_id: Type.String(),
244
+ turn_id: Type.String(),
245
+ content: Type.String(),
246
+ }),
247
+ ),
248
+ recent_activity: Type.Array(RenderRecentActivitySchema),
249
+ latest_result: Type.Optional(CoordinatorTurnOutputSchema),
250
+ missing_dependencies: Type.Array(Type.String()),
251
+ unavailable_reason: Type.Optional(Type.String()),
252
+ usage: Type.Optional(RenderUsageSchema),
253
+ });
254
+ const CoordinatorWaitMessageOutputSchema = Type.Object({
255
+ event: Type.Literal("message"),
256
+ agent_id: Type.String(),
257
+ turn_id: Type.String(),
258
+ message_id: Type.String(),
259
+ delivery_id: Type.Optional(Type.String()),
260
+ message: Type.String(),
261
+ });
262
+ const CoordinatorWaitSourceOutputSchema = {
263
+ source_agent_id: Type.String(),
264
+ source_turn_id: Type.String(),
265
+ };
266
+
267
+ /** Final details schemas exposed to CodeMode for the six coordinator tools. */
268
+ export const CoordinatorToolOutputSchemas = {
269
+ subagent: Type.Object({
270
+ agent_id: Type.String(),
271
+ turn_id: Type.String(),
272
+ status: Type.Literal("running"),
273
+ agent: Type.Optional(CoordinatorAgentDetailOutputSchema),
274
+ }),
275
+ agent_message: Type.Object({
276
+ agent_id: Type.String(),
277
+ message_id: Type.String(),
278
+ disposition: Type.Union([
279
+ Type.Literal("delivered-via-wait"),
280
+ Type.Literal("queued"),
281
+ Type.Literal("failed"),
282
+ ]),
283
+ error: Type.Optional(Type.String()),
284
+ }),
285
+ subagent_wait: Type.Union([
286
+ Type.Object({
287
+ ...CoordinatorWaitMessageOutputSchema.properties,
288
+ ...CoordinatorWaitSourceOutputSchema,
289
+ }),
290
+ Type.Object({
291
+ ...CoordinatorTurnOutputSchema.properties,
292
+ event: Type.Literal("turn"),
293
+ messages: Type.Optional(Type.Array(CoordinatorWaitMessageOutputSchema)),
294
+ ...CoordinatorWaitSourceOutputSchema,
295
+ }),
296
+ Type.Object({
297
+ event: Type.Literal("timeout"),
298
+ agent_id: Type.String(),
299
+ turn_id: Type.String(),
300
+ timeout_ms: Type.Number(),
301
+ agent: CoordinatorAgentDetailOutputSchema,
302
+ ...CoordinatorWaitSourceOutputSchema,
303
+ }),
304
+ ]),
305
+ subagent_status: Type.Union([
306
+ Type.Object({
307
+ parent_id: Type.String(),
308
+ agents: Type.Array(CoordinatorAgentSummaryOutputSchema),
309
+ }),
310
+ Type.Object({ agent: CoordinatorAgentDetailOutputSchema }),
311
+ ]),
312
+ subagent_cancel: CancelDetailsSchema,
313
+ subagent_delete: DeleteDetailsSchema,
314
+ } as const;
315
+
184
316
  const MessageRenderDetailsSchema = Type.Union([
185
317
  CurrentMessageDetailsSchema,
186
318
  LegacyMessageDetailsSchema,