@narumitw/pi-subagents 1.0.0 → 1.0.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.
@@ -0,0 +1,64 @@
1
+ import type { ExtensionAPI, ToolDefinition } from "@earendil-works/pi-coding-agent";
2
+ import { cachedModuleLoader, throwIfAborted } from "./cached-module-loader.js";
3
+ import type { SubagentInspectRuntime } from "./inspect.js";
4
+ import { renderInspectCall, renderInspectResult } from "./inspect-render.js";
5
+ import { SubagentInspectParams } from "./inspect-tool.js";
6
+
7
+ interface InspectExecutionModule {
8
+ executeSubagentInspect: typeof import("./inspect.js").executeSubagentInspect;
9
+ }
10
+
11
+ export interface InspectRegistrationDependencies {
12
+ loadExecution?: () => Promise<InspectExecutionModule>;
13
+ }
14
+
15
+ export function registerSubagentInspect(
16
+ pi: ExtensionAPI,
17
+ runtime: SubagentInspectRuntime,
18
+ dependencies: InspectRegistrationDependencies = {},
19
+ ): void {
20
+ const loadExecution = cachedModuleLoader(
21
+ dependencies.loadExecution ?? (() => import("./inspect.js")),
22
+ );
23
+ let lifecycleGeneration = 0;
24
+ pi.on("session_start", () => {
25
+ lifecycleGeneration += 1;
26
+ });
27
+ pi.on("session_shutdown", () => {
28
+ lifecycleGeneration += 1;
29
+ });
30
+ const definition: ToolDefinition<typeof SubagentInspectParams, Record<string, unknown>> = {
31
+ name: "subagent_inspect",
32
+ label: "Inspect Subagents",
33
+ description:
34
+ "Inspect available subagent definitions, models, retained runs, persisted blocking workflows, runtime status, and diagnostics without changing subagent or workspace state. This tool never starts a child, sends or acknowledges messages, interrupts or closes runs, changes settings, or modifies files.",
35
+ promptSnippet: "Inspect subagent metadata and runtime state without changing it",
36
+ parameters: SubagentInspectParams,
37
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
38
+ const ownerGeneration = lifecycleGeneration;
39
+ throwIfAborted(signal, "Subagent inspection loading was cancelled");
40
+ let executionModule: InspectExecutionModule;
41
+ try {
42
+ executionModule = await loadExecution();
43
+ } catch (error) {
44
+ throwIfAborted(signal, "Subagent inspection loading was cancelled");
45
+ if (ownerGeneration !== lifecycleGeneration) {
46
+ throw new DOMException("Subagent inspection session was replaced", "AbortError");
47
+ }
48
+ throw error;
49
+ }
50
+ throwIfAborted(signal, "Subagent inspection loading was cancelled");
51
+ if (ownerGeneration !== lifecycleGeneration) {
52
+ throw new DOMException("Subagent inspection session was replaced", "AbortError");
53
+ }
54
+ return executionModule.executeSubagentInspect(params, ctx, runtime);
55
+ },
56
+ renderCall(args, theme) {
57
+ return renderInspectCall(args, theme);
58
+ },
59
+ renderResult(result, options, theme, context) {
60
+ return renderInspectResult(result, options, theme, context);
61
+ },
62
+ };
63
+ pi.registerTool(definition);
64
+ }
@@ -0,0 +1,43 @@
1
+ import { StringEnum } from "@earendil-works/pi-ai";
2
+ import { type Static, Type } from "typebox";
3
+
4
+ const INSPECT_ACTIONS = [
5
+ "list_agents",
6
+ "get_agent",
7
+ "list_runs",
8
+ "get_run",
9
+ "list_workflows",
10
+ "get_workflow",
11
+ "list_models",
12
+ "preview_context",
13
+ "status",
14
+ "diagnose",
15
+ ] as const;
16
+
17
+ const AgentScopeSchema = StringEnum(["user", "project", "both"] as const, {
18
+ default: "user",
19
+ description: "Agent definition scope. Project scopes require a trusted project.",
20
+ });
21
+ const LimitSchema = Type.Number({ minimum: 1, maximum: 100, multipleOf: 1 });
22
+ const ContextModeSchema = Type.Union([
23
+ StringEnum(["none", "all", "summary"] as const),
24
+ Type.Number({ minimum: 1, multipleOf: 1 }),
25
+ ]);
26
+
27
+ export const SubagentInspectParams = Type.Object(
28
+ {
29
+ action: StringEnum(INSPECT_ACTIONS),
30
+ agent: Type.Optional(Type.String({ minLength: 1 })),
31
+ agentId: Type.Optional(Type.String({ minLength: 1 })),
32
+ workflowId: Type.Optional(Type.String({ minLength: 1, maxLength: 256 })),
33
+ agentScope: Type.Optional(AgentScopeSchema),
34
+ limit: Type.Optional(LimitSchema),
35
+ includeClosed: Type.Optional(Type.Boolean({ default: false })),
36
+ context: Type.Optional(ContextModeSchema),
37
+ contextEntryIds: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
38
+ },
39
+ { additionalProperties: false },
40
+ );
41
+
42
+ export type SubagentInspectParams = Static<typeof SubagentInspectParams>;
43
+ export { INSPECT_ACTIONS };
package/src/inspect.ts CHANGED
@@ -1,11 +1,5 @@
1
1
  import * as path from "node:path";
2
- import { StringEnum } from "@earendil-works/pi-ai";
3
- import {
4
- CONFIG_DIR_NAME,
5
- type ExtensionAPI,
6
- type ExtensionContext,
7
- } from "@earendil-works/pi-coding-agent";
8
- import { type Static, Type } from "typebox";
2
+ import { CONFIG_DIR_NAME, type ExtensionContext } from "@earendil-works/pi-coding-agent";
9
3
  import { discoverAgents } from "./agents/discovery.js";
10
4
  import type {
11
5
  AgentConfig,
@@ -16,7 +10,7 @@ import type {
16
10
  import { projectCapabilityManifest } from "./capabilities.js";
17
11
  import { resolveConsultTools } from "./consult-policy.js";
18
12
  import { buildContextSnapshot, type ContextMode } from "./context.js";
19
- import { renderInspectCall, renderInspectResult } from "./inspect-render.js";
13
+ import { INSPECT_ACTIONS } from "./inspect-tool.js";
20
14
  import { DEFAULT_MAX_CONTEXT_BYTES } from "./limits.js";
21
15
  import { resolvePiInvocation } from "./pi-invocation.js";
22
16
  import type { AgentRunInspectionDetail, AgentRunInspectionSummary } from "./registry.js";
@@ -36,47 +30,10 @@ import type { StatefulSubagentRuntimeStatus } from "./stateful.js";
36
30
  import type { WorkItemLedgerSnapshot } from "./work-item-ledger.js";
37
31
  import { inspectSessionWorkflows } from "./work-item-persistence.js";
38
32
 
39
- const INSPECT_ACTIONS = [
40
- "list_agents",
41
- "get_agent",
42
- "list_runs",
43
- "get_run",
44
- "list_workflows",
45
- "get_workflow",
46
- "list_models",
47
- "preview_context",
48
- "status",
49
- "diagnose",
50
- ] as const;
51
-
52
- const AgentScopeSchema = StringEnum(["user", "project", "both"] as const, {
53
- default: "user",
54
- description: "Agent definition scope. Project scopes require a trusted project.",
55
- });
56
-
57
- const LimitSchema = Type.Number({ minimum: 1, maximum: 100, multipleOf: 1 });
58
- const ContextModeSchema = Type.Union([
59
- StringEnum(["none", "all", "summary"] as const),
60
- Type.Number({ minimum: 1, multipleOf: 1 }),
61
- ]);
62
33
  const MAX_DETAILS_LIST_BYTES = 40 * 1024;
63
34
 
64
- export const SubagentInspectParams = Type.Object(
65
- {
66
- action: StringEnum(INSPECT_ACTIONS),
67
- agent: Type.Optional(Type.String({ minLength: 1 })),
68
- agentId: Type.Optional(Type.String({ minLength: 1 })),
69
- workflowId: Type.Optional(Type.String({ minLength: 1, maxLength: 256 })),
70
- agentScope: Type.Optional(AgentScopeSchema),
71
- limit: Type.Optional(LimitSchema),
72
- includeClosed: Type.Optional(Type.Boolean({ default: false })),
73
- context: Type.Optional(ContextModeSchema),
74
- contextEntryIds: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
75
- },
76
- { additionalProperties: false },
77
- );
78
-
79
- export type SubagentInspectParams = Static<typeof SubagentInspectParams>;
35
+ export { registerSubagentInspect } from "./inspect-registration.js";
36
+ export { SubagentInspectParams } from "./inspect-tool.js";
80
37
 
81
38
  export interface SubagentInspectRuntime {
82
39
  getBlockingEnabled(): boolean;
@@ -106,26 +63,6 @@ type ValidatedInspectOperation =
106
63
  | { action: "status" }
107
64
  | { action: "diagnose" };
108
65
 
109
- export function registerSubagentInspect(pi: ExtensionAPI, runtime: SubagentInspectRuntime): void {
110
- pi.registerTool({
111
- name: "subagent_inspect",
112
- label: "Inspect Subagents",
113
- description:
114
- "Inspect available subagent definitions, models, retained runs, persisted blocking workflows, runtime status, and diagnostics without changing subagent or workspace state. This tool never starts a child, sends or acknowledges messages, interrupts or closes runs, changes settings, or modifies files.",
115
- promptSnippet: "Inspect subagent metadata and runtime state without changing it",
116
- parameters: SubagentInspectParams,
117
- async execute(_toolCallId, params, _signal, _onUpdate, ctx): Promise<InspectToolResult> {
118
- return executeSubagentInspect(validateInspectParams(params), ctx, runtime);
119
- },
120
- renderCall(args, theme) {
121
- return renderInspectCall(args, theme);
122
- },
123
- renderResult(result, options, theme, context) {
124
- return renderInspectResult(result, options, theme, context);
125
- },
126
- });
127
- }
128
-
129
66
  export function validateInspectParams(params: unknown): ValidatedInspectOperation {
130
67
  const values = parameterRecord(params);
131
68
  const rawAction = values.action;
@@ -195,11 +132,12 @@ export function validateInspectParams(params: unknown): ValidatedInspectOperatio
195
132
  return { action };
196
133
  }
197
134
 
198
- async function executeSubagentInspect(
199
- operation: ValidatedInspectOperation,
135
+ export async function executeSubagentInspect(
136
+ params: unknown,
200
137
  ctx: ExtensionContext,
201
138
  runtime: SubagentInspectRuntime,
202
139
  ): Promise<InspectToolResult> {
140
+ const operation = validateInspectParams(params);
203
141
  if (operation.action === "list_agents" || operation.action === "get_agent") {
204
142
  assertTrustedScope(operation.agentScope, ctx);
205
143
  const settings = inspectSubagentSettings().settings;
package/src/params.ts CHANGED
@@ -6,7 +6,7 @@ import { MAX_CONFIGURABLE_PARALLEL_TASKS, MAX_SUBAGENT_TIMEOUT_MS } from "./limi
6
6
  import { PANEL_PRESETS } from "./panel-planning.js";
7
7
  import { SUBAGENT_RESULT_FORMATS } from "./result-contract.js";
8
8
  import { MAX_SUBAGENT_TOOL_CALLS, MAX_SUBAGENT_TURNS } from "./turn-budget.js";
9
- import { VerifiedExecutionContractSchema } from "./verified-execution-contract.js";
9
+ import { VerifiedExecutionContractSchema } from "./verified-execution-schema.js";
10
10
 
11
11
  const TimeoutMs = Type.Number({
12
12
  description:
package/src/pi-args.ts ADDED
@@ -0,0 +1,41 @@
1
+ import type { SubagentThinkingLevel } from "./agents/types.js";
2
+
3
+ export interface PiArgsOptions {
4
+ model?: string;
5
+ thinkingLevel?: SubagentThinkingLevel;
6
+ tools?: string[];
7
+ disableExtensions?: boolean;
8
+ disableSkills?: boolean;
9
+ disablePromptTemplates?: boolean;
10
+ disableContextFiles?: boolean;
11
+ projectTrust?: boolean;
12
+ baseSystemPromptPath?: string;
13
+ appendSystemPromptPaths?: string[];
14
+ /** Existing single append prompt path retained for compatibility. */
15
+ systemPromptPath?: string;
16
+ task: string;
17
+ }
18
+
19
+ export function buildPiArgs(options: PiArgsOptions): string[] {
20
+ const args: string[] = ["--mode", "json", "-p", "--no-session"];
21
+ if (options.model) args.push("--model", options.model);
22
+ if (options.thinkingLevel) args.push("--thinking", options.thinkingLevel);
23
+ if (options.disableExtensions) args.push("--no-extensions");
24
+ if (options.disableSkills) args.push("--no-skills");
25
+ if (options.disablePromptTemplates) args.push("--no-prompt-templates");
26
+ if (options.disableContextFiles) args.push("--no-context-files");
27
+ if (options.projectTrust !== undefined) {
28
+ args.push(options.projectTrust ? "--approve" : "--no-approve");
29
+ }
30
+ if (Array.isArray(options.tools)) {
31
+ if (options.tools.length > 0) args.push("--tools", options.tools.join(","));
32
+ else args.push("--no-tools");
33
+ }
34
+ if (options.baseSystemPromptPath) args.push("--system-prompt", options.baseSystemPromptPath);
35
+ for (const promptPath of options.appendSystemPromptPaths ?? []) {
36
+ args.push("--append-system-prompt", promptPath);
37
+ }
38
+ if (options.systemPromptPath) args.push("--append-system-prompt", options.systemPromptPath);
39
+ args.push(`Task: ${options.task}`);
40
+ return args;
41
+ }
package/src/render.ts CHANGED
@@ -11,12 +11,8 @@ import type { AgentScope, SubagentThinkingLevel } from "./agents/types.js";
11
11
  import { renderPanelCall, renderPanelResult } from "./panel-render.js";
12
12
  import { hasUsableAggregator, type SubagentParams } from "./params.js";
13
13
  import { expansionHint, formatToolActivity, safeBlock, safeLine } from "./render-common.js";
14
- import {
15
- getResultFinalOutput,
16
- isResultError,
17
- type SingleResult,
18
- type SubagentDetails,
19
- } from "./runner.js";
14
+ import type { SingleResult, SubagentDetails } from "./runner.js";
15
+ import { getResultFinalOutput, isResultError } from "./runner-outcome.js";
20
16
 
21
17
  const COLLAPSED_ITEM_COUNT = 5;
22
18
 
@@ -0,0 +1,31 @@
1
+ import type { SingleResult } from "./runner.js";
2
+ import {
3
+ formatResultFailure as formatBaseResultFailure,
4
+ getResultFinalOutput as getBaseResultFinalOutput,
5
+ isResultError as isBaseResultError,
6
+ } from "./runner-result.js";
7
+
8
+ export function getResultFinalOutput(result: SingleResult): string {
9
+ return getBaseResultFinalOutput(result);
10
+ }
11
+
12
+ export function isResultError(result: SingleResult): boolean {
13
+ return (
14
+ isBaseResultError(result) ||
15
+ result.resultContractInvalid === true ||
16
+ (result.outcome !== undefined &&
17
+ result.outcome.status !== "completed" &&
18
+ result.outcome.status !== "partial")
19
+ );
20
+ }
21
+
22
+ export function formatResultFailure(result: SingleResult): string {
23
+ const contractError = result.resultContractInvalid
24
+ ? `Subagent returned an invalid ${result.resultFormat ?? "structured"} result contract`
25
+ : result.outcome && !["completed", "partial"].includes(result.outcome.status)
26
+ ? `Subagent outcome ${result.outcome.status}${result.outcome.reasonCode ? ` (${result.outcome.reasonCode})` : ""}; recovery: ${result.outcome.recoveryActions.join(", ") || "none"}`
27
+ : undefined;
28
+ return contractError
29
+ ? formatBaseResultFailure({ ...result, errorMessage: contractError })
30
+ : formatBaseResultFailure(result);
31
+ }
package/src/runner.ts CHANGED
@@ -31,6 +31,7 @@ import type { PanelSynthesis } from "./panel-contract.js";
31
31
  import type { PanelEvidenceArtifact } from "./panel-evidence.js";
32
32
  import type { PanelFailure } from "./panel-failure.js";
33
33
  import type { PanelPhaseBudgets, PanelPreset } from "./panel-planning.js";
34
+ import { buildPiArgs } from "./pi-args.js";
34
35
  import { resolvePiInvocation } from "./pi-invocation.js";
35
36
  import { JsonLineDecoder } from "./protocol.js";
36
37
  import {
@@ -38,12 +39,8 @@ import {
38
39
  parseAnyStructuredSubagentResult,
39
40
  type SubagentResultFormat,
40
41
  } from "./result-contract.js";
41
- import {
42
- formatResultFailure as formatBaseResultFailure,
43
- getResultFinalOutput as getBaseResultFinalOutput,
44
- getFinalOutput,
45
- isResultError as isBaseResultError,
46
- } from "./runner-result.js";
42
+ import { formatResultFailure, getResultFinalOutput, isResultError } from "./runner-outcome.js";
43
+ import { getFinalOutput } from "./runner-result.js";
47
44
  import {
48
45
  addUsageValue,
49
46
  mergeUsageStats,
@@ -68,6 +65,13 @@ import type { WorkItemLedgerSnapshot } from "./work-item-ledger.js";
68
65
 
69
66
  export const KILL_GRACE_MS = 5000;
70
67
 
68
+ export type { PiArgsOptions } from "./pi-args.js";
69
+ export { buildPiArgs } from "./pi-args.js";
70
+ export {
71
+ formatResultFailure,
72
+ getResultFinalOutput,
73
+ isResultError,
74
+ } from "./runner-outcome.js";
71
75
  export type { UsageStats } from "./runner-usage.js";
72
76
  export type RecentActivityItem =
73
77
  | { type: "text"; text: string }
@@ -154,31 +158,6 @@ export interface SubagentDetails {
154
158
  isError?: boolean;
155
159
  }
156
160
 
157
- export function getResultFinalOutput(result: SingleResult): string {
158
- return getBaseResultFinalOutput(result);
159
- }
160
-
161
- export function isResultError(result: SingleResult): boolean {
162
- return (
163
- isBaseResultError(result) ||
164
- result.resultContractInvalid === true ||
165
- (result.outcome !== undefined &&
166
- result.outcome.status !== "completed" &&
167
- result.outcome.status !== "partial")
168
- );
169
- }
170
-
171
- export function formatResultFailure(result: SingleResult): string {
172
- const contractError = result.resultContractInvalid
173
- ? `Subagent returned an invalid ${result.resultFormat ?? "structured"} result contract`
174
- : result.outcome && !["completed", "partial"].includes(result.outcome.status)
175
- ? `Subagent outcome ${result.outcome.status}${result.outcome.reasonCode ? ` (${result.outcome.reasonCode})` : ""}; recovery: ${result.outcome.recoveryActions.join(", ") || "none"}`
176
- : undefined;
177
- return contractError
178
- ? formatBaseResultFailure({ ...result, errorMessage: contractError })
179
- : formatBaseResultFailure(result);
180
- }
181
-
182
161
  function boundMessageText(
183
162
  message: Message,
184
163
  maxBytes: number,
@@ -359,46 +338,6 @@ async function writePromptToTempFile(
359
338
  return { dir: tmpDir, filePath };
360
339
  }
361
340
 
362
- export interface PiArgsOptions {
363
- model?: string;
364
- thinkingLevel?: SubagentThinkingLevel;
365
- tools?: string[];
366
- disableExtensions?: boolean;
367
- disableSkills?: boolean;
368
- disablePromptTemplates?: boolean;
369
- disableContextFiles?: boolean;
370
- projectTrust?: boolean;
371
- baseSystemPromptPath?: string;
372
- appendSystemPromptPaths?: string[];
373
- /** Existing single append prompt path retained for compatibility. */
374
- systemPromptPath?: string;
375
- task: string;
376
- }
377
-
378
- export function buildPiArgs(options: PiArgsOptions): string[] {
379
- const args: string[] = ["--mode", "json", "-p", "--no-session"];
380
- if (options.model) args.push("--model", options.model);
381
- if (options.thinkingLevel) args.push("--thinking", options.thinkingLevel);
382
- if (options.disableExtensions) args.push("--no-extensions");
383
- if (options.disableSkills) args.push("--no-skills");
384
- if (options.disablePromptTemplates) args.push("--no-prompt-templates");
385
- if (options.disableContextFiles) args.push("--no-context-files");
386
- if (options.projectTrust !== undefined) {
387
- args.push(options.projectTrust ? "--approve" : "--no-approve");
388
- }
389
- if (Array.isArray(options.tools)) {
390
- if (options.tools.length > 0) args.push("--tools", options.tools.join(","));
391
- else args.push("--no-tools");
392
- }
393
- if (options.baseSystemPromptPath) args.push("--system-prompt", options.baseSystemPromptPath);
394
- for (const promptPath of options.appendSystemPromptPaths ?? []) {
395
- args.push("--append-system-prompt", promptPath);
396
- }
397
- if (options.systemPromptPath) args.push("--append-system-prompt", options.systemPromptPath);
398
- args.push(`Task: ${options.task}`);
399
- return args;
400
- }
401
-
402
341
  function signalProcess(proc: ReturnType<typeof spawn>, signal: NodeJS.Signals): void {
403
342
  if (process.platform !== "win32" && proc.pid) {
404
343
  try {
@@ -857,16 +796,16 @@ export async function runSingleAgent(
857
796
  onExceeded: stopForBudget,
858
797
  });
859
798
 
860
- timeout = setTimeout(() => {
861
- stopForBudget({
862
- reason: launchPolicy?.workTimeoutReason ?? "work_timeout",
863
- limit: launchPolicy?.workTimeoutReportLimit ?? timeoutMs,
864
- });
865
- }, timeoutMs);
866
- timeout.unref();
867
-
868
799
  proc.once("spawn", () => {
869
800
  currentResult.processStarted = true;
801
+ if (settled || budgetStop || wasAborted) return;
802
+ timeout = setTimeout(() => {
803
+ stopForBudget({
804
+ reason: launchPolicy?.workTimeoutReason ?? "work_timeout",
805
+ limit: launchPolicy?.workTimeoutReportLimit ?? timeoutMs,
806
+ });
807
+ }, timeoutMs);
808
+ timeout.unref();
870
809
  });
871
810
  proc.stdout?.on("data", (data) => decoder.push(data));
872
811
  proc.stderr?.on("data", (data) => {
package/src/stateful.ts CHANGED
@@ -20,7 +20,10 @@ import {
20
20
  import { issueCapabilityGrant } from "./capability-grant.js";
21
21
  import { CompletionDeliveryBroker } from "./completion-delivery.js";
22
22
  import { buildContextSnapshot, type ContextMode, redactPrivateText } from "./context.js";
23
- import { createStatefulTransport } from "./create-stateful-transport.js";
23
+ import {
24
+ type CreateStatefulTransportOptions,
25
+ createStatefulTransport,
26
+ } from "./create-stateful-transport.js";
24
27
  import {
25
28
  assertDelegationTargetAllowed,
26
29
  resolveSubagentTarget,
@@ -133,6 +136,7 @@ export interface StatefulSubagentDependencies {
133
136
  workspaceManager?: WorkspaceManager;
134
137
  settings?: SubagentRuntimeSettings;
135
138
  getSettings?: () => SubagentSettings | undefined;
139
+ loadTransport?: CreateStatefulTransportOptions["loadTransport"];
136
140
  }
137
141
 
138
142
  export interface StatefulSubagentRuntimeStatus {
@@ -319,6 +323,7 @@ export function registerStatefulSubagents(
319
323
  getParentRuntime: () => ({ ...parentRuntime }),
320
324
  getSettings: getCurrentSettings,
321
325
  createInProcessSession: dependencies.createInProcessSession,
326
+ loadTransport: dependencies.loadTransport,
322
327
  });
323
328
  nextRegistry = new AgentRegistry(transport, {
324
329
  maxAgents: nextLimits.maxAgents,
package/src/subagents.ts CHANGED
@@ -21,11 +21,24 @@ import type {
21
21
  DelegationCwdPolicy,
22
22
  SubagentSettings,
23
23
  } from "./agents/types.js";
24
- import { registerSubagentAutomation } from "./automation.js";
25
- import { registerSubagentConfigCommand, registerSubagentConfigLifecycle } from "./config-ui.js";
26
- import { registerSubagentConsult } from "./consult.js";
27
- import { executeSubagent } from "./execution.js";
28
- import { registerSubagentInspect } from "./inspect.js";
24
+ import {
25
+ type AutomationRegistrationDependencies,
26
+ registerSubagentAutomation,
27
+ } from "./automation-registration.js";
28
+ import { cachedModuleLoader, throwIfAborted } from "./cached-module-loader.js";
29
+ import {
30
+ type ConfigRegistrationDependencies,
31
+ registerSubagentConfigCommand,
32
+ registerSubagentConfigLifecycle,
33
+ } from "./config-registration.js";
34
+ import {
35
+ type ConsultRegistrationDependencies,
36
+ registerSubagentConsult,
37
+ } from "./consult-registration.js";
38
+ import {
39
+ type InspectRegistrationDependencies,
40
+ registerSubagentInspect,
41
+ } from "./inspect-registration.js";
29
42
  import { MAX_BLOCKING_PARALLEL_CONCURRENCY } from "./limits.js";
30
43
  import { SubagentParams } from "./params.js";
31
44
  import { renderSubagentCall, renderSubagentResult } from "./render.js";
@@ -40,17 +53,34 @@ import {
40
53
  resolveBlockingMaxParallelTasks,
41
54
  } from "./settings.js";
42
55
  import { registerStatefulSubagents } from "./stateful.js";
56
+ import type { SubagentTransport } from "./transport.js";
57
+
58
+ type BlockingExecutionModule = Pick<typeof import("./execution.js"), "executeSubagent">;
59
+
60
+ export interface SubagentsDependencies {
61
+ loadBlockingExecution?: () => Promise<BlockingExecutionModule>;
62
+ loadStatefulTransport?: () => Promise<SubagentTransport>;
63
+ automation?: AutomationRegistrationDependencies;
64
+ config?: ConfigRegistrationDependencies;
65
+ consult?: ConsultRegistrationDependencies;
66
+ inspect?: InspectRegistrationDependencies;
67
+ }
43
68
 
44
- export default function (pi: ExtensionAPI) {
69
+ export default function (pi: ExtensionAPI, dependencies: SubagentsDependencies = {}) {
70
+ const loadBlockingExecution = cachedModuleLoader(
71
+ dependencies.loadBlockingExecution ?? (() => import("./execution.js")),
72
+ );
45
73
  const configOwner = registerSubagentConfigLifecycle(pi);
46
74
  const settings = readSubagentSettings();
47
75
  let currentSettings: SubagentSettings | undefined = settings;
48
76
  let currentCatalog = "";
49
77
  const blockingEnabled = settings?.blocking?.enabled !== false;
50
78
  const refreshBlockingCatalog = blockingEnabled
51
- ? registerBlockingSubagent(pi, () => currentSettings)
79
+ ? registerBlockingSubagent(pi, () => currentSettings, loadBlockingExecution)
52
80
  : () => undefined;
53
- if (blockingEnabled) registerSubagentAutomation(pi, { getSettings: () => currentSettings });
81
+ if (blockingEnabled) {
82
+ registerSubagentAutomation(pi, { getSettings: () => currentSettings }, dependencies.automation);
83
+ }
54
84
  let refreshStatefulCatalog: (catalog: string) => void = () => undefined;
55
85
  let refreshConsultCatalog: (catalog: string) => void = () => undefined;
56
86
 
@@ -78,6 +108,7 @@ export default function (pi: ExtensionAPI) {
78
108
  blockingEnabled,
79
109
  settings: settings?.stateful,
80
110
  getSettings: () => currentSettings,
111
+ loadTransport: dependencies.loadStatefulTransport,
81
112
  });
82
113
  refreshStatefulCatalog = statefulRuntime.setAgentCatalog;
83
114
  const getBlockingEnabled = () => blockingEnabled;
@@ -88,18 +119,24 @@ export default function (pi: ExtensionAPI) {
88
119
  currentSettings?.cwdPolicy?.consultation ?? DEFAULT_CONSULTATION_CWD_POLICY;
89
120
  const getDelegationCwdPolicy = () =>
90
121
  currentSettings?.cwdPolicy?.delegation ?? DEFAULT_DELEGATION_CWD_POLICY;
91
- registerSubagentInspect(pi, {
92
- ...statefulRuntime,
93
- getBlockingEnabled,
94
- getMaxParallelTasks,
95
- getConsultResourcePolicy,
96
- getConsultationCwdPolicy,
97
- getDelegationCwdPolicy,
98
- });
122
+ registerSubagentInspect(
123
+ pi,
124
+ {
125
+ ...statefulRuntime,
126
+ getBlockingEnabled,
127
+ getMaxParallelTasks,
128
+ getConsultResourcePolicy,
129
+ getConsultationCwdPolicy,
130
+ getDelegationCwdPolicy,
131
+ },
132
+ dependencies.inspect,
133
+ );
99
134
  if (blockingEnabled) {
100
- refreshConsultCatalog = registerSubagentConsult(pi, {
101
- getSettings: () => currentSettings,
102
- });
135
+ refreshConsultCatalog = registerSubagentConsult(
136
+ pi,
137
+ { getSettings: () => currentSettings },
138
+ dependencies.consult,
139
+ );
103
140
  }
104
141
  registerSubagentConfigCommand(
105
142
  pi,
@@ -155,12 +192,14 @@ export default function (pi: ExtensionAPI) {
155
192
  },
156
193
  },
157
194
  configOwner,
195
+ dependencies.config,
158
196
  );
159
197
  }
160
198
 
161
199
  function registerBlockingSubagent(
162
200
  pi: ExtensionAPI,
163
201
  getSettings: () => SubagentSettings | undefined,
202
+ loadExecution: () => Promise<BlockingExecutionModule>,
164
203
  ): (catalog: string) => void {
165
204
  let catalog = "";
166
205
  const activeControllers = new Set<AbortController>();
@@ -212,14 +251,28 @@ function registerBlockingSubagent(
212
251
  const effectiveSignal = signal
213
252
  ? AbortSignal.any([signal, lifecycleController.signal])
214
253
  : lifecycleController.signal;
215
- const work = executeSubagent(
216
- toolCallId,
217
- params,
218
- effectiveSignal,
219
- onUpdate,
220
- ctx,
221
- getSettings(),
222
- );
254
+ const work = (async () => {
255
+ throwIfAborted(effectiveSignal, "Blocking subagent execution was cancelled");
256
+ let executionModule: BlockingExecutionModule;
257
+ try {
258
+ executionModule = await loadExecution();
259
+ } catch (error) {
260
+ throwIfAborted(
261
+ effectiveSignal,
262
+ "Blocking subagent execution was cancelled while loading",
263
+ );
264
+ throw error;
265
+ }
266
+ throwIfAborted(effectiveSignal, "Blocking subagent execution was cancelled while loading");
267
+ return executionModule.executeSubagent(
268
+ toolCallId,
269
+ params,
270
+ effectiveSignal,
271
+ onUpdate,
272
+ ctx,
273
+ getSettings(),
274
+ );
275
+ })();
223
276
  activeWork.add(work);
224
277
  try {
225
278
  return await work;
@@ -256,8 +309,8 @@ function appendAgentCatalog(baseDescription: string, catalog: string): string {
256
309
  }
257
310
 
258
311
  export { parsePositiveInteger } from "./execution/runtime-policy.js";
312
+ export { buildPiArgs } from "./pi-args.js";
259
313
  export { formatTokens, formatUsageStats } from "./render.js";
260
- export { buildPiArgs } from "./runner.js";
261
314
  export {
262
315
  DEFAULT_CONSULT_RESOURCE_POLICY,
263
316
  DEFAULT_CONSULTATION_CWD_POLICY,