@narumitw/pi-subagents 1.0.0 → 1.0.2
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 +12 -4
- package/package.json +7 -7
- package/src/automation-registration.ts +137 -0
- package/src/automation-tool.ts +40 -0
- package/src/automation.ts +6 -156
- package/src/cached-module-loader.ts +18 -0
- package/src/config-registration.ts +114 -0
- package/src/config-ui.ts +2 -2
- package/src/consult-registration.ts +132 -0
- package/src/consult-render.ts +1 -1
- package/src/consult-tool.ts +95 -0
- package/src/consult.ts +37 -204
- package/src/create-stateful-transport.ts +106 -18
- package/src/inspect-registration.ts +64 -0
- package/src/inspect-tool.ts +43 -0
- package/src/inspect.ts +7 -69
- package/src/panel-planning.ts +2 -2
- package/src/panel-presets.ts +3 -0
- package/src/params.ts +2 -2
- package/src/pi-args.ts +41 -0
- package/src/render.ts +4 -47
- package/src/runner-outcome.ts +31 -0
- package/src/runner.ts +18 -79
- package/src/settings.ts +4 -1
- package/src/stateful.ts +254 -98
- package/src/subagents.ts +82 -29
- package/src/usage-format.ts +42 -0
- package/src/verified-execution-contract.ts +3 -31
- package/src/verified-execution-schema.ts +32 -0
package/README.md
CHANGED
|
@@ -613,6 +613,9 @@ A detached agent additionally needs a concrete isolation or specialization benef
|
|
|
613
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.
|
|
@@ -1049,20 +1052,25 @@ Downgrading is safe: older extension versions ignore this separate state directo
|
|
|
1049
1052
|
packages/pi-subagents/
|
|
1050
1053
|
├── src/
|
|
1051
1054
|
│ ├── index.ts # Pi package entrypoint
|
|
1052
|
-
│ ├── subagents.ts #
|
|
1053
|
-
│ ├──
|
|
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
|
|
1054
1059
|
│ ├── automation-contract.ts # Strict request, proposal, and graph-patch contracts
|
|
1055
1060
|
│ ├── automation-planner.ts # Bounded read-only planner prompt and resource policy
|
|
1056
1061
|
│ ├── workflow-plan-compiler.ts # Deterministic admission, routing, and workflow compilation
|
|
1057
1062
|
│ ├── workflow-plan-patch.ts # Generation-safe revisions and atomic plan persistence
|
|
1058
1063
|
│ ├── workflow-planning-benchmark.ts # Frozen matched offline evaluation protocol
|
|
1059
|
-
│ ├── inspect.ts
|
|
1060
|
-
│ ├──
|
|
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
|
|
1061
1068
|
│ ├── consult-policy.ts # Enforced read-only tool intersection
|
|
1062
1069
|
│ ├── cwd-policy.ts # Canonical target and saved-trust resolution
|
|
1063
1070
|
│ ├── prompt-resources.ts # Core-selected SYSTEM and APPEND_SYSTEM resources
|
|
1064
1071
|
│ ├── safe-text.ts # Shared byte/line/path sanitization
|
|
1065
1072
|
│ ├── stateful.ts # Detached lifecycle registration and dispatch
|
|
1073
|
+
│ ├── create-stateful-transport.ts # First-turn selected transport loader
|
|
1066
1074
|
│ ├── rpc-transport.ts # Persistent strict-JSONL Pi RPC child transport
|
|
1067
1075
|
│ ├── rpc-timeout-finalization.ts # RPC abort-settle-summary recovery
|
|
1068
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": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Pi extension for delegating work to specialized isolated subagents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -39,12 +39,12 @@
|
|
|
39
39
|
"typebox": "*"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
|
-
"@biomejs/biome": "2.5.
|
|
43
|
-
"@earendil-works/pi-agent-core": "0.84.
|
|
44
|
-
"@earendil-works/pi-ai": "0.84.
|
|
45
|
-
"@earendil-works/pi-coding-agent": "0.84.
|
|
46
|
-
"@earendil-works/pi-tui": "0.84.
|
|
47
|
-
"typebox": "1.3.
|
|
42
|
+
"@biomejs/biome": "2.5.8",
|
|
43
|
+
"@earendil-works/pi-agent-core": "0.84.2",
|
|
44
|
+
"@earendil-works/pi-ai": "0.84.2",
|
|
45
|
+
"@earendil-works/pi-coding-agent": "0.84.2",
|
|
46
|
+
"@earendil-works/pi-tui": "0.84.2",
|
|
47
|
+
"typebox": "1.3.14",
|
|
48
48
|
"typescript": "7.0.2"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
@@ -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
|
|
57
|
-
|
|
58
|
-
|
|
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
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { cachedModuleLoader } from "./cached-module-loader.js";
|
|
3
|
+
import type { SubagentMenuOwner, SubagentSettingsRuntime } from "./config-ui.js";
|
|
4
|
+
|
|
5
|
+
const SUBCOMMANDS = [
|
|
6
|
+
{ value: "settings", label: "settings", description: "Configure subagent user settings" },
|
|
7
|
+
{ value: "status", label: "status", description: "Show effective subagent settings" },
|
|
8
|
+
{ value: "help", label: "help", description: "Show subagent settings help" },
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
type ConfigUiModule = Pick<
|
|
12
|
+
typeof import("./config-ui.js"),
|
|
13
|
+
"showSubagentManager" | "showSubagentSettings"
|
|
14
|
+
>;
|
|
15
|
+
|
|
16
|
+
type ConfigStatusModule = Pick<
|
|
17
|
+
typeof import("./config-status.js"),
|
|
18
|
+
"showSubagentHelp" | "showSubagentStatus"
|
|
19
|
+
>;
|
|
20
|
+
|
|
21
|
+
export interface ConfigRegistrationDependencies {
|
|
22
|
+
loadConfigUi?: () => Promise<ConfigUiModule>;
|
|
23
|
+
loadConfigStatus?: () => Promise<ConfigStatusModule>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function registerSubagentConfigLifecycle(pi: ExtensionAPI): SubagentMenuOwner {
|
|
27
|
+
const owner: SubagentMenuOwner = { generation: 0, controller: new AbortController() };
|
|
28
|
+
pi.on("session_start", () => {
|
|
29
|
+
owner.generation += 1;
|
|
30
|
+
owner.controller.abort(new DOMException("Subagent session replaced", "AbortError"));
|
|
31
|
+
owner.controller = new AbortController();
|
|
32
|
+
});
|
|
33
|
+
pi.on("session_shutdown", () => {
|
|
34
|
+
owner.generation += 1;
|
|
35
|
+
owner.controller.abort(new DOMException("Subagent session shut down", "AbortError"));
|
|
36
|
+
});
|
|
37
|
+
return owner;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function registerSubagentConfigCommand(
|
|
41
|
+
pi: ExtensionAPI,
|
|
42
|
+
runtime: SubagentSettingsRuntime,
|
|
43
|
+
owner = registerSubagentConfigLifecycle(pi),
|
|
44
|
+
dependencies: ConfigRegistrationDependencies = {},
|
|
45
|
+
): void {
|
|
46
|
+
const loadConfigUi = cachedModuleLoader(
|
|
47
|
+
dependencies.loadConfigUi ?? (() => import("./config-ui.js")),
|
|
48
|
+
);
|
|
49
|
+
const loadConfigStatus = cachedModuleLoader<ConfigStatusModule>(
|
|
50
|
+
dependencies.loadConfigStatus ?? (() => import("./config-status.js")),
|
|
51
|
+
);
|
|
52
|
+
pi.registerCommand("subagents", {
|
|
53
|
+
description: "Manage current-session subagents and user settings",
|
|
54
|
+
getArgumentCompletions(prefix: string) {
|
|
55
|
+
const normalized = prefix.trim().toLowerCase();
|
|
56
|
+
const matches = SUBCOMMANDS.filter((item) => item.value.startsWith(normalized));
|
|
57
|
+
return matches.length > 0 ? matches : null;
|
|
58
|
+
},
|
|
59
|
+
async handler(args, ctx) {
|
|
60
|
+
const subcommand = args.trim().toLowerCase();
|
|
61
|
+
const runStatusCommand = async (show: (status: ConfigStatusModule) => void) => {
|
|
62
|
+
const generation = owner.generation;
|
|
63
|
+
const controller = owner.controller;
|
|
64
|
+
const isCurrent = () =>
|
|
65
|
+
generation === owner.generation &&
|
|
66
|
+
controller === owner.controller &&
|
|
67
|
+
!controller.signal.aborted;
|
|
68
|
+
let status: ConfigStatusModule;
|
|
69
|
+
try {
|
|
70
|
+
status = await loadConfigStatus();
|
|
71
|
+
} catch (error) {
|
|
72
|
+
if (!isCurrent()) return;
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
if (!isCurrent()) return;
|
|
76
|
+
show(status);
|
|
77
|
+
};
|
|
78
|
+
if (!subcommand && ctx.mode !== "tui") {
|
|
79
|
+
await runStatusCommand((status) => status.showSubagentStatus(ctx, runtime));
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (subcommand === "status") {
|
|
83
|
+
await runStatusCommand((status) => status.showSubagentStatus(ctx, runtime));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (subcommand === "help") {
|
|
87
|
+
await runStatusCommand((status) => status.showSubagentHelp(ctx, runtime));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (!subcommand || subcommand === "settings") {
|
|
91
|
+
const generation = owner.generation;
|
|
92
|
+
const controller = owner.controller;
|
|
93
|
+
const isCurrent = () =>
|
|
94
|
+
generation === owner.generation &&
|
|
95
|
+
controller === owner.controller &&
|
|
96
|
+
!controller.signal.aborted;
|
|
97
|
+
let configUi: ConfigUiModule;
|
|
98
|
+
try {
|
|
99
|
+
configUi = await loadConfigUi();
|
|
100
|
+
} catch (error) {
|
|
101
|
+
if (!isCurrent()) return;
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
if (!isCurrent()) return;
|
|
105
|
+
if (!subcommand) await configUi.showSubagentManager(pi, ctx, runtime, owner);
|
|
106
|
+
else await configUi.showSubagentSettings(ctx, runtime, owner);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (ctx.mode === "tui" || ctx.hasUI) {
|
|
110
|
+
ctx.ui.notify(`Unknown /subagents subcommand: ${subcommand}`, "warning");
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
}
|
package/src/config-ui.ts
CHANGED
|
@@ -168,7 +168,7 @@ function registerSubagentPrimaryCommand(
|
|
|
168
168
|
});
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
-
async function showSubagentManager(
|
|
171
|
+
export async function showSubagentManager(
|
|
172
172
|
pi: ExtensionAPI,
|
|
173
173
|
ctx: ExtensionCommandContext,
|
|
174
174
|
runtime: SubagentSettingsRuntime,
|
|
@@ -681,7 +681,7 @@ async function showSubagentManager(
|
|
|
681
681
|
});
|
|
682
682
|
}
|
|
683
683
|
|
|
684
|
-
async function showSubagentSettings(
|
|
684
|
+
export async function showSubagentSettings(
|
|
685
685
|
ctx: ExtensionCommandContext,
|
|
686
686
|
runtime: SubagentSettingsRuntime,
|
|
687
687
|
owner: SubagentMenuOwner,
|