@narumitw/pi-subagents 0.54.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.
package/README.md CHANGED
@@ -299,8 +299,8 @@ No benchmark result in this release changes the default delegation policy or mak
299
299
  | --- | --- | --- |
300
300
  | `list_agents` | Optional `agentScope` (default `user`) and `limit` (default 32, maximum 100) | Bounded agent metadata and omission counts |
301
301
  | `get_agent` | Required `agent`; optional `agentScope` | One resolved definition, safe source path, configured tools, and consultation-effective tools; never the system prompt |
302
- | `list_runs` | Optional `includeClosed` and `limit` (default 50, maximum 100) | Metadata-only retained-run summaries and unread counts |
303
- | `get_run` | Required `agentId` | Safe `cwd`, current-task/error summaries, thinking level, context footprint, protocol, effective transport, bounded timing/usage telemetry, structured result when valid, policy, history count, and unread count |
302
+ | `list_runs` | Optional `includeClosed` and `limit` (default 50, maximum 100) | Metadata-only retained-run summaries, turn generation, pending-completion count, and unread counts |
303
+ | `get_run` | Required `agentId` | Safe `cwd`, current run and turn generation, current-task/error summaries, thinking level, context footprint, protocol, effective transport, bounded timing/usage telemetry, structured result when valid, policy, history count, pending-completion count, and unread count |
304
304
  | `list_workflows` | Optional `limit` (default 50, maximum 100) | Metadata-only persisted blocking-workflow summaries for the current session |
305
305
  | `get_workflow` | Required `workflowId` | Bounded task states, generations, dependencies, plan identities, artifact metadata, verification state, and outcome reasons without artifact contents |
306
306
  | `list_models` | Optional `limit` (default 50, maximum 100) | Session-scoped models, or the already-loaded available snapshot |
@@ -310,7 +310,7 @@ No benchmark result in this release changes the default delegation policy or mak
310
310
 
311
311
  The schema rejects fields that do not belong to the selected action. Explicit `project` or `both` scope fails before project-agent discovery unless Pi already trusts the project. Run inspection never returns history output, stored context, or mailbox content; unread counts come from a metadata-only snapshot and do not acknowledge messages. Workflow inspection reads validated, redacted snapshots without quarantining or rewriting invalid files. Paths beneath the Pi agent directory use `~`, project paths are workspace-relative, model objects are projected through an allow-list, and model-facing text is bounded to 50 KiB or 2,000 lines.
312
312
 
313
- Compatibility: `subagent_manage({ "action": "list" })` remains supported with its existing behavior. Prefer `subagent_inspect` when a whole tool must be safe to activate on a read-only surface.
313
+ `subagent_manage` no longer accepts its former compatibility `list` action. Use `subagent_inspect({ "action": "list_runs", "includeClosed": true })` for metadata-only discovery and `get_run` for detail.
314
314
 
315
315
  ## 📖 Read-only consultation
316
316
 
@@ -599,9 +599,9 @@ Legacy v1 and v2 records without acceptance fields retain their prior completed
599
599
 
600
600
  ## 🔁 Stateful agents
601
601
 
602
- Stateful lifecycle tools are available by default. `subagent_spawn` is detached: it schedules work, returns immediately with an opaque `agentId`, and later injects a bounded `pi-subagent-completion` custom message. Completions that settle in the same dispatch window are batched, and the broker allows at most one in-flight root wake until that parent turn starts.
602
+ Stateful lifecycle tools are available by default. `subagent_spawn` is detached: it schedules work, returns immediately with an opaque `agentId`, and later injects a bounded `pi-subagent-completion` custom message. Every turn receives an executor-owned `runId`, monotonically increasing agent-local generation, and unique `completionId`. The terminal completion is persisted before delivery, simultaneous completions are batched, and the broker allows at most one in-flight root wake until that parent turn starts.
603
603
 
604
- Detached work follows a non-polling policy. With default `next-turn` delivery, prefer one bounded `subagent_spawn` for related asynchronous research or review only when the current response does not depend on its result. If it does, use blocking `subagent` when registered; in **Async only**, complete required work directly, opt into `auto-resume` when a later synthesis turn is appropriate, or switch workflows. With opt-in `auto-resume`, detached broad work may be final-answer-dependent because completion requests a synthesis turn after the root settles. In either mode, do useful non-overlapping main-agent work immediately, do not poll `subagent_manage` with `action: "list"` or `subagent_mailbox` with `action: "read"`, and do not duplicate delegated work. Add another detached agent only for truly independent work with safe workspace concurrency. Detached lifecycle work intentionally has no `subagent_wait` tool.
604
+ Detached work follows a non-polling policy. With default `next-turn` delivery, prefer one bounded `subagent_spawn` for related asynchronous research or review only when the current response does not depend on its result. If it does, use blocking `subagent` when registered; in **Async only**, complete required work directly, opt into `auto-resume` when a later synthesis turn is appropriate, or switch workflows. With opt-in `auto-resume`, detached broad work may be final-answer-dependent because completion requests a synthesis turn after the root settles. In either mode, do useful non-overlapping main-agent work immediately, do not poll `subagent_inspect` or `subagent_mailbox` with `action: "read"`, and do not duplicate delegated work. Add another detached agent only for truly independent work with safe workspace concurrency. Detached lifecycle work intentionally has no `subagent_wait` tool.
605
605
 
606
606
  A detached agent additionally needs a concrete isolation or specialization benefit such as independent review, bounded context/output, a distinct model/tool profile, or workspace isolation. Simple work that the main agent can perform directly should not be delegated.
607
607
 
@@ -610,9 +610,12 @@ A detached agent additionally needs a concrete isolation or specialization benef
610
610
  - `"next-turn"` (default) preserves the previous behavior: use `deliverAs: "steer"` with `triggerTurn: false`. An active root can consume completion naturally; an idle root is not awakened.
611
611
  - `"auto-resume"` holds completion while the root is active, then requests one synthesis turn after the parent settles when no user or extension messages are already pending. Simultaneous completions share that turn, active work is not interrupted, and pending input suppresses the autonomous wake.
612
612
 
613
- Auto-resume is best-effort because Pi's custom-message API is fire-and-forget. Session-generation checks, shutdown cleanup, batching, and the in-flight wake guard prevent stale or duplicate scheduling pressure, but they do not make completion delivery durable across process exit.
613
+ The bounded persisted completion outbox provides ordered at-least-once delivery across process restart without replaying the child turn. When state must be reduced to its storage bound, persistence drops roots without pending completions first and trims old history rather than discarding an outbox-owned root. A completion is acknowledged only after parent context assembly observes its exact `completionId`; an injection that returns synchronously but never reaches context remains pending for retry. If the process exits after context assembly but before acknowledgement is persisted, the same ID can be delivered again and consumers must deduplicate it. Auto-resume wake admission remains best-effort because Pi's custom-message API is fire-and-forget, but an unacknowledged terminal completion itself remains available for redelivery on the next start of the owning session. Transient terminal-persistence failures retry with bounded exponential backoff and keep the run pending; shutdown cancels retry waits and reports a final persistence failure instead of silently resolving unsaved work.
614
614
 
615
615
  The default `subprocess` transport preserves compatibility: each turn starts a fresh isolated `pi --mode json -p --no-session` child and receives sanitized, bounded history.
616
+ Pi registers every Subagents tool and command during startup, but loads blocking execution, manager UI, inspection work, and the selected detached transport implementation only on first use.
617
+ Session restoration, pending completion delivery, settings validation, and cleanup ownership remain eager.
618
+ A failed first-use code load is reported normally and can be retried.
616
619
  Set `transport` to `in-process` to retain one public Pi SDK `AgentSession` per stateful `agentId`, avoiding repeated process startup while preserving native child history in memory.
617
620
  Set it to `rpc` to retain one `pi --mode rpc --no-session --no-extensions` process per active retained agent, preserving native child history with a separate process boundary.
618
621
  Set it to `auto` for deterministic preflight selection: read-only built-in tools use in-process, write-capable built-in tools use RPC, and extension/custom tools use subprocess.
@@ -687,7 +690,7 @@ This avoids lifecycle-driven tool-schema churn and preserves a stable provider p
687
690
  | --- | --- |
688
691
  | `subagent_spawn` | Start detached work with optional task-selected thinking and retained timeout, exact-retry `idempotencyKey`, and `text`, `structured-v1`, or `structured-v2` result format; return an opaque `agentId` immediately and deliver completion asynchronously. |
689
692
  | `subagent_send` | Send follow-up work with an optional one-turn timeout override and trigger a new turn on a reusable agent; semantic skew requires explicit `revalidate: true`, and shared-workspace write conflicts are guarded unless explicitly overridden. |
690
- | `subagent_manage` | Use `action: "list"` to inspect agents, `"interrupt"` to retain an agent after aborting active work, or `"close"` to release it; interrupt/close accept optional `subtree`. |
693
+ | `subagent_manage` | Use `"interrupt"` to retain an agent after aborting active work or `"close"` to release it; both actions accept optional `subtree`. Use `subagent_inspect` for all list and detail operations. |
691
694
  | `subagent_mailbox` | Use `action: "send"` for queue-only messages that do not start a turn, or `"read"` to read and optionally acknowledge unread messages. |
692
695
 
693
696
  The action schemas are flat for provider compatibility and reject parameters that belong to another action. For example:
@@ -721,13 +724,14 @@ The five replaced names are intentionally not registered as aliases. Update expl
721
724
 
722
725
  | Previous call | Fixed-surface call |
723
726
  | --- | --- |
724
- | `subagent_list({ includeClosed })` | `subagent_manage({ action: "list", includeClosed })` |
727
+ | `subagent_list({ includeClosed })` | `subagent_inspect({ action: "list_runs", includeClosed })` |
728
+ | `subagent_manage({ action: "list", includeClosed })` | `subagent_inspect({ action: "list_runs", includeClosed })` |
725
729
  | `subagent_interrupt({ agentId, subtree })` | `subagent_manage({ action: "interrupt", agentId, subtree })` |
726
730
  | `subagent_close({ agentId, subtree })` | `subagent_manage({ action: "close", agentId, subtree })` |
727
731
  | `subagent_message({ agentId, message, ... })` | `subagent_mailbox({ action: "send", agentId, message, ... })` |
728
732
  | `subagent_messages({ agentId, acknowledge, limit })` | `subagent_mailbox({ action: "read", agentId, acknowledge, limit })` |
729
733
 
730
- Persisted agent and mailbox records require no migration. If an explicit prompt in a resumed conversation keeps requesting an old name, update it with the mapping above or start a fresh conversation. To roll back after an upgrade, pin the package version used before the upgrade; for this migration, use `pi install npm:@narumitw/pi-subagents@0.26.0`. The previous release can read the same state directory.
734
+ Persisted agent and mailbox records require no manual migration; older records load with an empty completion outbox and generation zero. If an explicit prompt in a resumed conversation keeps requesting an old name, update it with the mapping above or start a fresh conversation. To roll back after an upgrade, pin the package version used before the upgrade; for this migration, use `pi install npm:@narumitw/pi-subagents@0.26.0`. The previous release can read the same state directory.
731
735
 
732
736
  A spawn can request a thinking level explicitly:
733
737
 
@@ -1048,20 +1052,25 @@ Downgrading is safe: older extension versions ignore this separate state directo
1048
1052
  packages/pi-subagents/
1049
1053
  ├── src/
1050
1054
  │ ├── index.ts # Pi package entrypoint
1051
- │ ├── subagents.ts # Extension registration and blocking tool schema
1052
- │ ├── automation.ts # Explicit autonomous planning tool and lifecycle owner
1055
+ │ ├── subagents.ts # Lightweight extension composition and blocking registration
1056
+ │ ├── cached-module-loader.ts # Retryable first-use code-module cache
1057
+ │ ├── automation-registration.ts # Lightweight autonomous tool registration
1058
+ │ ├── automation.ts # First-use autonomous planning execution
1053
1059
  │ ├── automation-contract.ts # Strict request, proposal, and graph-patch contracts
1054
1060
  │ ├── automation-planner.ts # Bounded read-only planner prompt and resource policy
1055
1061
  │ ├── workflow-plan-compiler.ts # Deterministic admission, routing, and workflow compilation
1056
1062
  │ ├── workflow-plan-patch.ts # Generation-safe revisions and atomic plan persistence
1057
1063
  │ ├── workflow-planning-benchmark.ts # Frozen matched offline evaluation protocol
1058
- │ ├── inspect.ts # Side-effect-free metadata inspection tool
1059
- │ ├── consult.ts # Synchronous read-only consultation tool
1064
+ │ ├── inspect-registration.ts # Lightweight inspection tool registration
1065
+ │ ├── inspect.ts # First-use side-effect-free metadata inspection
1066
+ │ ├── consult-registration.ts # Lightweight consultation tool registration
1067
+ │ ├── consult.ts # First-use synchronous read-only consultation
1060
1068
  │ ├── consult-policy.ts # Enforced read-only tool intersection
1061
1069
  │ ├── cwd-policy.ts # Canonical target and saved-trust resolution
1062
1070
  │ ├── prompt-resources.ts # Core-selected SYSTEM and APPEND_SYSTEM resources
1063
1071
  │ ├── safe-text.ts # Shared byte/line/path sanitization
1064
1072
  │ ├── stateful.ts # Detached lifecycle registration and dispatch
1073
+ │ ├── create-stateful-transport.ts # First-turn selected transport loader
1065
1074
  │ ├── rpc-transport.ts # Persistent strict-JSONL Pi RPC child transport
1066
1075
  │ ├── rpc-timeout-finalization.ts # RPC abort-settle-summary recovery
1067
1076
  │ ├── rpc-transport-metadata.ts # RPC result policy and bounded metadata helpers
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@narumitw/pi-subagents",
3
- "version": "0.54.0",
3
+ "version": "1.0.1",
4
4
  "description": "Pi extension for delegating work to specialized isolated subagents.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -0,0 +1,137 @@
1
+ import type { ExtensionAPI, ToolDefinition } from "@earendil-works/pi-coding-agent";
2
+ import { Text } from "@earendil-works/pi-tui";
3
+ import type { AutomationExecutionOptions } from "./automation.js";
4
+ import { type AutomationDetails, SubagentAutomationParams } from "./automation-tool.js";
5
+ import { cachedModuleLoader, throwIfAborted } from "./cached-module-loader.js";
6
+ import { renderFallbackResult, safeLine, toolHeader } from "./render-common.js";
7
+
8
+ interface AutomationExecutionModule {
9
+ executeAutomationRequest: typeof import("./automation.js").executeAutomationRequest;
10
+ }
11
+
12
+ export interface AutomationRegistrationDependencies {
13
+ loadExecution?: () => Promise<AutomationExecutionModule>;
14
+ }
15
+
16
+ export function registerSubagentAutomation(
17
+ pi: ExtensionAPI,
18
+ options: AutomationExecutionOptions,
19
+ dependencies: AutomationRegistrationDependencies = {},
20
+ ): void {
21
+ const loadExecution = cachedModuleLoader(
22
+ dependencies.loadExecution ?? (() => import("./automation.js")),
23
+ );
24
+ let generation = 0;
25
+ const activeControllers = new Set<AbortController>();
26
+ const activeWork = new Set<Promise<unknown>>();
27
+ const cancelAndWait = async (reason: string) => {
28
+ generation++;
29
+ for (const controller of activeControllers) {
30
+ controller.abort(new DOMException(reason, "AbortError"));
31
+ }
32
+ await Promise.allSettled([...activeWork]);
33
+ };
34
+ pi.on("session_start", () => cancelAndWait("Autonomous workflow session replaced"));
35
+ pi.on("session_shutdown", () => cancelAndWait("Autonomous workflow session shut down"));
36
+ const definition: ToolDefinition<typeof SubagentAutomationParams, AutomationDetails> = {
37
+ name: "subagent_auto",
38
+ label: "Autonomous Subagent Workflow",
39
+ description: [
40
+ "Explicitly opt in to one bounded read-only planning turn that compiles a high-level objective into the smallest justified existing workflow.",
41
+ "The deterministic compiler may return parent-owned work, request missing input, or reject without launching execution workers.",
42
+ "Mutating workflows require an authoritative integration path and an independent verifier, allow at most two concurrent mutating workers, and never allow workflow grandchildren.",
43
+ "The first version routes only built-in and user-scoped agents; use caller-authored workflow mode for project-local agents.",
44
+ ].join(" "),
45
+ promptSnippet:
46
+ "Explicitly compile one high-level objective into a bounded capability-matched workflow",
47
+ promptGuidelines: [
48
+ "Use subagent_auto only when the caller explicitly opts into autonomous workflow planning.",
49
+ "Provide a complete authority ceiling and aggregate budget; parent-owned and insufficient-evidence results launch no execution workers.",
50
+ "Use caller-authored subagent workflow mode as the compatibility fallback when deterministic task control is required.",
51
+ ],
52
+ parameters: SubagentAutomationParams,
53
+ async execute(toolCallId, params, signal, onUpdate, ctx) {
54
+ const ownerGeneration = generation;
55
+ const controller = new AbortController();
56
+ activeControllers.add(controller);
57
+ const combined = combineSignals(signal, controller.signal);
58
+ const work = (async () => {
59
+ throwIfAborted(combined.signal, "Autonomous workflow loading was cancelled");
60
+ let executionModule: AutomationExecutionModule;
61
+ try {
62
+ executionModule = await loadExecution();
63
+ } catch (error) {
64
+ throwIfAborted(combined.signal, "Autonomous workflow loading was cancelled");
65
+ throw error;
66
+ }
67
+ throwIfAborted(combined.signal, "Autonomous workflow loading was cancelled");
68
+ if (ownerGeneration !== generation) {
69
+ throw new DOMException("Autonomous workflow owner was replaced", "AbortError");
70
+ }
71
+ return executionModule.executeAutomationRequest(
72
+ toolCallId,
73
+ params,
74
+ combined.signal,
75
+ onUpdate,
76
+ ctx,
77
+ options,
78
+ () => ownerGeneration === generation,
79
+ );
80
+ })();
81
+ activeWork.add(work);
82
+ try {
83
+ return await work;
84
+ } finally {
85
+ combined.dispose();
86
+ activeControllers.delete(controller);
87
+ activeWork.delete(work);
88
+ }
89
+ },
90
+ renderCall(args, theme) {
91
+ const request = (args as { request?: { objective?: string; version?: string } }).request;
92
+ return new Text(
93
+ toolHeader(theme, "subagent_auto", request?.objective, [request?.version ?? "request"]),
94
+ 0,
95
+ 0,
96
+ );
97
+ },
98
+ renderResult(result, renderOptions, theme) {
99
+ const status = safeLine(result.details?.status, "completed", 128);
100
+ return renderFallbackResult(
101
+ result,
102
+ renderOptions,
103
+ theme,
104
+ result.details?.isError === true ||
105
+ status.endsWith("failed") ||
106
+ status.endsWith("rejected"),
107
+ );
108
+ },
109
+ };
110
+ pi.registerTool<typeof SubagentAutomationParams, AutomationDetails>(definition);
111
+ pi.on("tool_result", (event) => {
112
+ if (event.toolName !== "subagent_auto") return;
113
+ if ((event.details as AutomationDetails | undefined)?.isError) return { isError: true };
114
+ });
115
+ }
116
+
117
+ function combineSignals(
118
+ external: AbortSignal | undefined,
119
+ owned: AbortSignal,
120
+ ): { signal: AbortSignal; dispose(): void } {
121
+ const controller = new AbortController();
122
+ const signals = [external, owned].filter((value): value is AbortSignal => value !== undefined);
123
+ const listeners = signals.map((source) => {
124
+ const listener = () => {
125
+ if (!controller.signal.aborted) controller.abort(source.reason);
126
+ };
127
+ if (source.aborted) listener();
128
+ else source.addEventListener("abort", listener, { once: true });
129
+ return { source, listener };
130
+ });
131
+ return {
132
+ signal: controller.signal,
133
+ dispose() {
134
+ for (const { source, listener } of listeners) source.removeEventListener("abort", listener);
135
+ },
136
+ };
137
+ }
@@ -0,0 +1,40 @@
1
+ import { type Static, Type } from "typebox";
2
+ import { AutomationRequestSchema } from "./automation-contract.js";
3
+ import type { SubagentDetails } from "./runner.js";
4
+ import type { CompiledWorkflowPlan } from "./workflow-plan-compiler.js";
5
+
6
+ export const SubagentAutomationParams = Type.Object(
7
+ { request: AutomationRequestSchema },
8
+ { additionalProperties: false },
9
+ );
10
+ export type SubagentAutomationParams = Static<typeof SubagentAutomationParams>;
11
+
12
+ export interface AutomationDetails {
13
+ status:
14
+ | "planning"
15
+ | "planner-failed"
16
+ | "parent-owned"
17
+ | "needs-input"
18
+ | "compiler-rejected"
19
+ | "executed";
20
+ requestVersion: string;
21
+ planVersion?: string;
22
+ planId?: string;
23
+ workflowGeneration?: number;
24
+ revision?: number;
25
+ childCount: number;
26
+ reasonCodes: string[];
27
+ missingInputs?: string[];
28
+ planner?: {
29
+ agent: string;
30
+ tools: string[];
31
+ resources: "project-context" | "none";
32
+ timeoutMs: number;
33
+ maxTurns: number;
34
+ maxToolCalls: number;
35
+ failed?: boolean;
36
+ };
37
+ compiled?: CompiledWorkflowPlan;
38
+ execution?: SubagentDetails;
39
+ isError?: boolean;
40
+ }
package/src/automation.ts CHANGED
@@ -1,22 +1,11 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import * as path from "node:path";
3
3
  import type { AgentToolResult, AgentToolUpdateCallback } from "@earendil-works/pi-agent-core";
4
- import {
5
- type ExtensionAPI,
6
- type ExtensionContext,
7
- getAgentDir,
8
- type ToolDefinition,
9
- } from "@earendil-works/pi-coding-agent";
10
- import { Text } from "@earendil-works/pi-tui";
11
- import { type Static, Type } from "typebox";
4
+ import { type ExtensionContext, getAgentDir } from "@earendil-works/pi-coding-agent";
12
5
  import { getBuiltInAgent } from "./agents/built-ins.js";
13
6
  import { discoverAgents } from "./agents/discovery.js";
14
7
  import type { SubagentSettings } from "./agents/types.js";
15
- import {
16
- AutomationRequestSchema,
17
- parseAutomationRequest,
18
- type WorkflowPlan,
19
- } from "./automation-contract.js";
8
+ import { parseAutomationRequest, type WorkflowPlan } from "./automation-contract.js";
20
9
  import {
21
10
  AUTOMATION_PLANNER_MAX_TIMEOUT_MS,
22
11
  AUTOMATION_PLANNER_MAX_TOOL_CALLS,
@@ -26,13 +15,13 @@ import {
26
15
  parseAutomationPlannerOutput,
27
16
  resolveAutomationPlannerPolicy,
28
17
  } from "./automation-planner.js";
18
+ import type { AutomationDetails, SubagentAutomationParams } from "./automation-tool.js";
29
19
  import {
30
20
  assertDelegationTargetAllowed,
31
21
  resolveSubagentTarget,
32
22
  targetPolicyAudit,
33
23
  } from "./cwd-policy.js";
34
24
  import { executeSubagent } from "./execution.js";
35
- import { renderFallbackResult, safeLine, toolHeader } from "./render-common.js";
36
25
  import {
37
26
  getResultFinalOutput,
38
27
  isResultError,
@@ -53,41 +42,9 @@ import {
53
42
  import { AutomationPlanPersistence, createWorkflowPlanRecord } from "./workflow-plan-patch.js";
54
43
  import { createBlockingWorkLedger, resolveWorkflowTasks } from "./workflow-planning.js";
55
44
 
56
- export const SubagentAutomationParams = Type.Object(
57
- { request: AutomationRequestSchema },
58
- { additionalProperties: false },
59
- );
60
- export type SubagentAutomationParams = Static<typeof SubagentAutomationParams>;
61
-
62
- export interface AutomationDetails {
63
- status:
64
- | "planning"
65
- | "planner-failed"
66
- | "parent-owned"
67
- | "needs-input"
68
- | "compiler-rejected"
69
- | "executed";
70
- requestVersion: string;
71
- planVersion?: string;
72
- planId?: string;
73
- workflowGeneration?: number;
74
- revision?: number;
75
- childCount: number;
76
- reasonCodes: string[];
77
- missingInputs?: string[];
78
- planner?: {
79
- agent: string;
80
- tools: string[];
81
- resources: "project-context" | "none";
82
- timeoutMs: number;
83
- maxTurns: number;
84
- maxToolCalls: number;
85
- failed?: boolean;
86
- };
87
- compiled?: CompiledWorkflowPlan;
88
- execution?: SubagentDetails;
89
- isError?: boolean;
90
- }
45
+ export { registerSubagentAutomation } from "./automation-registration.js";
46
+ export type { AutomationDetails } from "./automation-tool.js";
47
+ export { SubagentAutomationParams } from "./automation-tool.js";
91
48
 
92
49
  export interface AutomationPlannerRequest {
93
50
  prompt: string;
@@ -110,91 +67,6 @@ export interface AutomationExecutionOptions {
110
67
  persistCompiled?: (compiled: CompiledWorkflowPlan, ctx: ExtensionContext) => Promise<void>;
111
68
  }
112
69
 
113
- export function registerSubagentAutomation(
114
- pi: ExtensionAPI,
115
- options: AutomationExecutionOptions,
116
- ): void {
117
- let generation = 0;
118
- const activeControllers = new Set<AbortController>();
119
- const activeWork = new Set<Promise<unknown>>();
120
- const cancelAndWait = async (reason: string) => {
121
- generation++;
122
- for (const controller of activeControllers) {
123
- controller.abort(new DOMException(reason, "AbortError"));
124
- }
125
- await Promise.allSettled([...activeWork]);
126
- };
127
- pi.on("session_start", () => cancelAndWait("Autonomous workflow session replaced"));
128
- pi.on("session_shutdown", () => cancelAndWait("Autonomous workflow session shut down"));
129
- const description = () =>
130
- [
131
- "Explicitly opt in to one bounded read-only planning turn that compiles a high-level objective into the smallest justified existing workflow.",
132
- "The deterministic compiler may return parent-owned work, request missing input, or reject without launching execution workers.",
133
- "Mutating workflows require an authoritative integration path and an independent verifier, allow at most two concurrent mutating workers, and never allow workflow grandchildren.",
134
- "The first version routes only built-in and user-scoped agents; use caller-authored workflow mode for project-local agents.",
135
- ].join(" ");
136
- const definition: ToolDefinition<typeof SubagentAutomationParams, AutomationDetails> = {
137
- name: "subagent_auto",
138
- label: "Autonomous Subagent Workflow",
139
- description: description(),
140
- promptSnippet:
141
- "Explicitly compile one high-level objective into a bounded capability-matched workflow",
142
- promptGuidelines: [
143
- "Use subagent_auto only when the caller explicitly opts into autonomous workflow planning.",
144
- "Provide a complete authority ceiling and aggregate budget; parent-owned and insufficient-evidence results launch no execution workers.",
145
- "Use caller-authored subagent workflow mode as the compatibility fallback when deterministic task control is required.",
146
- ],
147
- parameters: SubagentAutomationParams,
148
- async execute(toolCallId, params, signal, onUpdate, ctx) {
149
- const ownerGeneration = generation;
150
- const controller = new AbortController();
151
- activeControllers.add(controller);
152
- const combined = combineSignals(signal, controller.signal);
153
- const work = executeAutomationRequest(
154
- toolCallId,
155
- params,
156
- combined.signal,
157
- onUpdate,
158
- ctx,
159
- options,
160
- () => ownerGeneration === generation,
161
- );
162
- activeWork.add(work);
163
- try {
164
- return await work;
165
- } finally {
166
- combined.dispose();
167
- activeControllers.delete(controller);
168
- activeWork.delete(work);
169
- }
170
- },
171
- renderCall(args, theme) {
172
- const request = (args as { request?: { objective?: string; version?: string } }).request;
173
- return new Text(
174
- toolHeader(theme, "subagent_auto", request?.objective, [request?.version ?? "request"]),
175
- 0,
176
- 0,
177
- );
178
- },
179
- renderResult(result, renderOptions, theme) {
180
- const status = safeLine(result.details?.status, "completed", 128);
181
- return renderFallbackResult(
182
- result,
183
- renderOptions,
184
- theme,
185
- result.details?.isError === true ||
186
- status.endsWith("failed") ||
187
- status.endsWith("rejected"),
188
- );
189
- },
190
- };
191
- pi.registerTool<typeof SubagentAutomationParams, AutomationDetails>(definition);
192
- pi.on("tool_result", (event) => {
193
- if (event.toolName !== "subagent_auto") return;
194
- if ((event.details as AutomationDetails | undefined)?.isError) return { isError: true };
195
- });
196
- }
197
-
198
70
  export async function executeAutomationRequest(
199
71
  toolCallId: string,
200
72
  params: SubagentAutomationParams,
@@ -561,25 +433,3 @@ function abortError(message: string): Error {
561
433
  error.name = "AbortError";
562
434
  return error;
563
435
  }
564
-
565
- function combineSignals(
566
- external: AbortSignal | undefined,
567
- owned: AbortSignal,
568
- ): { signal: AbortSignal; dispose(): void } {
569
- const controller = new AbortController();
570
- const signals = [external, owned].filter((value): value is AbortSignal => value !== undefined);
571
- const listeners = signals.map((source) => {
572
- const listener = () => {
573
- if (!controller.signal.aborted) controller.abort(source.reason);
574
- };
575
- if (source.aborted) listener();
576
- else source.addEventListener("abort", listener, { once: true });
577
- return { source, listener };
578
- });
579
- return {
580
- signal: controller.signal,
581
- dispose() {
582
- for (const { source, listener } of listeners) source.removeEventListener("abort", listener);
583
- },
584
- };
585
- }
@@ -0,0 +1,18 @@
1
+ export function cachedModuleLoader<Module>(load: () => Promise<Module>): () => Promise<Module> {
2
+ let pending: Promise<Module> | undefined;
3
+ return () => {
4
+ if (!pending) {
5
+ pending = load().catch((error) => {
6
+ pending = undefined;
7
+ throw error;
8
+ });
9
+ }
10
+ return pending;
11
+ };
12
+ }
13
+
14
+ export function throwIfAborted(signal: AbortSignal | undefined, message: string): void {
15
+ if (!signal?.aborted) return;
16
+ if (signal.reason instanceof Error) throw signal.reason;
17
+ throw new DOMException(message, "AbortError");
18
+ }