@rol1n/subagent 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,35 @@
1
- # Temporary Holding Version
1
+ # subagent
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Delegate tasks to independent, full-powered Pi workers. The `subagent` tool returns after launch; completion or questions are delivered back to the parent session asynchronously.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ pi install npm:@rol1n/subagent
9
+ ```
10
+
11
+ Also included in `@rol1n/pi-kit`. The parent and workers require the `pi` executable on PATH and their normal Pi authentication.
12
+
13
+ ## Use
14
+
15
+ ```json
16
+ {"action":"spawn","name":"reviewer","cwd":"/existing/project","task":"Review the changed files and report findings."}
17
+ {"action":"send","target":"reviewer","task":"Also inspect the error handling."}
18
+ {"action":"stop","target":"reviewer"}
19
+ ```
20
+
21
+ `model` optionally selects a worker model. `/subagent` shows active workers. Each worker inherits its own normal Pi capabilities; this is a delegation runtime, not a restricted execution environment.
22
+
23
+ ## Backends
24
+
25
+ | Variable | Behavior |
26
+ |---|---|
27
+ | `PI_SUBAGENT_BACKEND=process` | Run silent background processes |
28
+ | `PI_SUBAGENT_MUX=tmux` | Prefer tmux when running inside an available tmux session |
29
+ | `PI_SUBAGENT_MUX=zellij` | Prefer Zellij when running inside an available Zellij session |
30
+
31
+ Without a usable multiplexer, the worker backend falls back to a background process. Native Windows supports PowerShell launch commands and Zellij panes; tmux is for POSIX environments. Session transcripts and task artifacts are stored under `~/.pi/agent/`.
32
+
33
+ ## Other delegation packages
34
+
35
+ Use [multi-harness](https://github.com/RolinShmily/pi-kit/tree/main/extensions/multi-harness) for Codex, Claude, or OpenCode peers. That package shares multiplexer helpers but provides a separate tool and does not automatically enable this extension's entrypoint. The bundled subagent skill file is reference material and is not registered by the package manifest.
@@ -0,0 +1,35 @@
1
+ # subagent
2
+
3
+ Delegate tasks to independent, full-powered Pi workers. The `subagent` tool returns after launch; completion or questions are delivered back to the parent session asynchronously.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ pi install npm:@rol1n/subagent
9
+ ```
10
+
11
+ Also included in `@rol1n/pi-kit`. The parent and workers require the `pi` executable on PATH and their normal Pi authentication.
12
+
13
+ ## Use
14
+
15
+ ```json
16
+ {"action":"spawn","name":"reviewer","cwd":"/existing/project","task":"Review the changed files and report findings."}
17
+ {"action":"send","target":"reviewer","task":"Also inspect the error handling."}
18
+ {"action":"stop","target":"reviewer"}
19
+ ```
20
+
21
+ `model` optionally selects a worker model. `/subagent` shows active workers. Each worker inherits its own normal Pi capabilities; this is a delegation runtime, not a restricted execution environment.
22
+
23
+ ## Backends
24
+
25
+ | Variable | Behavior |
26
+ |---|---|
27
+ | `PI_SUBAGENT_BACKEND=process` | Run silent background processes |
28
+ | `PI_SUBAGENT_MUX=tmux` | Prefer tmux when running inside an available tmux session |
29
+ | `PI_SUBAGENT_MUX=zellij` | Prefer Zellij when running inside an available Zellij session |
30
+
31
+ Without a usable multiplexer, the worker backend falls back to a background process. Native Windows supports PowerShell launch commands and Zellij panes; tmux is for POSIX environments. Session transcripts and task artifacts are stored under `~/.pi/agent/`.
32
+
33
+ ## Other delegation packages
34
+
35
+ Use [multi-harness](https://github.com/RolinShmily/pi-kit/tree/main/extensions/multi-harness) for Codex, Claude, or OpenCode peers. That package shares multiplexer helpers but provides a separate tool and does not automatically enable this extension's entrypoint. The bundled subagent skill file is reference material and is not registered by the package manifest.
@@ -0,0 +1,171 @@
1
+ /**
2
+ * subagent/bridge.ts — Subagent Worker Bridge
3
+ *
4
+ * Loaded exclusively inside the spawned worker Pi process via `-e <bridgePath>`.
5
+ * - Grants the worker an `ask_question` tool to consult the parent orchestrator.
6
+ * - Handles auto-exit on task completion (calling ctx.shutdown() so the pane closes cleanly).
7
+ * - Suppresses auto-exit while waiting for the parent's answer to `ask_question`.
8
+ * - Prevents recursive subagent spawning.
9
+ */
10
+
11
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
12
+ import { Box, Text } from "@earendil-works/pi-tui";
13
+ import { Type } from "typebox";
14
+ import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
15
+
16
+ export default function subagentBridge(pi: ExtensionAPI) {
17
+ if (process.env.PI_SUBAGENT_ACTIVE !== "1") {
18
+ return;
19
+ }
20
+
21
+ const workerName = process.env.PI_SUBAGENT_NAME || "worker";
22
+ const sessionFile = process.env.PI_SUBAGENT_SESSION;
23
+
24
+ let awaitingAnswer = false;
25
+
26
+ // Render a minimal indicator in the child worker's terminal
27
+ pi.on("session_start", (_event, ctx) => {
28
+ ctx.ui.setWidget(
29
+ "subagent-worker-banner",
30
+ (_tui, theme) => {
31
+ const box = new Box(1, 0, (text: string) => {
32
+ try {
33
+ return theme.bg("toolSuccessBg", text);
34
+ } catch {
35
+ return text;
36
+ }
37
+ });
38
+ const badge = theme.bold(theme.fg("accent", `[Worker: ${workerName}]`));
39
+ const hint = theme.fg("dim", " · Full Pi worker · ask_question available");
40
+ box.addChild(new Text(`${badge}${hint}`, 0, 0));
41
+ return box;
42
+ },
43
+ { placement: "aboveEditor" },
44
+ );
45
+ });
46
+
47
+ // When input arrives (e.g. parent reply or manual steer), answer is received
48
+ pi.on("input", () => {
49
+ awaitingAnswer = false;
50
+ });
51
+
52
+ pi.on("agent_start", () => {
53
+ awaitingAnswer = false;
54
+ });
55
+
56
+ pi.on("agent_end", (event, ctx) => {
57
+ if (awaitingAnswer) {
58
+ // Park in waiting state, do not exit
59
+ return;
60
+ }
61
+
62
+ const messages = (event as any).messages as any[] | undefined;
63
+ let errorMessage: string | undefined;
64
+
65
+ if (messages && messages.length > 0) {
66
+ for (let i = messages.length - 1; i >= 0; i--) {
67
+ const msg = messages[i];
68
+ if (msg?.role === "assistant") {
69
+ if (msg.stopReason === "error") {
70
+ errorMessage = typeof msg.errorMessage === "string" ? msg.errorMessage : "Agent ended with error";
71
+ }
72
+ break;
73
+ }
74
+ }
75
+ }
76
+
77
+ if (sessionFile) {
78
+ try {
79
+ writeFileSync(
80
+ `${sessionFile}.exit`,
81
+ JSON.stringify({
82
+ type: errorMessage ? "error" : "done",
83
+ ...(errorMessage ? { errorMessage } : {}),
84
+ }),
85
+ );
86
+ } catch {
87
+ // best effort
88
+ }
89
+ }
90
+
91
+ ctx.shutdown();
92
+ });
93
+
94
+ // Register `ask_question` tool
95
+ pi.registerTool({
96
+ name: "ask_question",
97
+ label: "Ask Orchestrator",
98
+ description:
99
+ "Ask the orchestrator (the parent agent that spawned you) a clarifying question and pause until they reply. " +
100
+ "Use when requirements are ambiguous, a critical decision needs confirmation, or you are blocked. " +
101
+ "Your session stays open while you wait — the answer will arrive as your next turn message.",
102
+ parameters: Type.Object({
103
+ question: Type.String({
104
+ description: "The question for the parent orchestrator. Include enough context for them to decide.",
105
+ }),
106
+ }),
107
+ async execute(_toolCallId, params, signal) {
108
+ if (!sessionFile) {
109
+ throw new Error("ask_question is only available within a managed subagent session.");
110
+ }
111
+
112
+ awaitingAnswer = true;
113
+ const askFile = `${sessionFile}.ask`;
114
+ const replyFile = `${sessionFile}.reply`;
115
+
116
+ const askData = {
117
+ name: workerName,
118
+ question: params.question,
119
+ timestamp: new Date().toISOString(),
120
+ };
121
+
122
+ writeFileSync(askFile, JSON.stringify(askData));
123
+
124
+ try {
125
+ while (!existsSync(replyFile)) {
126
+ if (signal?.aborted) {
127
+ throw new Error("Cancelled while waiting for parent reply");
128
+ }
129
+ await new Promise<void>((resolve, reject) => {
130
+ const timer = setTimeout(resolve, 200);
131
+ if (signal) {
132
+ const onAbort = () => {
133
+ clearTimeout(timer);
134
+ signal.removeEventListener("abort", onAbort);
135
+ reject(new Error("Cancelled while waiting for parent reply"));
136
+ };
137
+ signal.addEventListener("abort", onAbort, { once: true });
138
+ }
139
+ });
140
+ }
141
+
142
+ const reply = readFileSync(replyFile, "utf-8");
143
+ try {
144
+ unlinkSync(replyFile);
145
+ } catch {}
146
+ awaitingAnswer = false;
147
+
148
+ return {
149
+ content: [
150
+ {
151
+ type: "text",
152
+ text: `Orchestrator answer:\n\n${reply}`,
153
+ },
154
+ ],
155
+ details: { question: params.question, reply },
156
+ };
157
+ } finally {
158
+ awaitingAnswer = false;
159
+ try {
160
+ if (existsSync(askFile)) unlinkSync(askFile);
161
+ } catch {}
162
+ }
163
+ },
164
+ renderCall(args, theme) {
165
+ const text =
166
+ theme.fg("toolTitle", theme.bold("ask_question ")) +
167
+ theme.fg("muted", String((args as any).question ?? ""));
168
+ return new Text(text, 0, 0);
169
+ },
170
+ });
171
+ }