@ian-pascoe/pi-minimal-subagents 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -1
- package/package.json +7 -1
- package/skills/pi-minimal-subagents/SKILL.md +7 -6
- package/src/minimal-subagents-access.ts +150 -0
- package/src/minimal-subagents-child-resources.ts +102 -0
- package/src/minimal-subagents-command.ts +63 -0
- package/src/minimal-subagents-config.ts +44 -1
- package/src/minimal-subagents-context.ts +128 -2
- package/src/minimal-subagents-coordinator.ts +25 -2
- package/src/minimal-subagents-extension.ts +222 -3
- package/src/minimal-subagents-fork-lifecycle.ts +1 -1
- package/src/minimal-subagents-paths.ts +8 -0
- package/src/minimal-subagents-sessions.ts +144 -104
- package/src/minimal-subagents-settings-writer.ts +307 -0
- package/src/minimal-subagents-status-panel.ts +483 -0
- package/src/minimal-subagents-tool-schemas.ts +4 -1
- package/src/minimal-subagents-types.ts +16 -0
|
@@ -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 {
|
|
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 {
|
|
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
|
|
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 {
|
|
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.
|
|
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-
|
|
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
|
+
}
|