pi-subagentura 1.0.11 → 2.0.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 +72 -2
- package/artifact.ts +150 -0
- package/interactive-tmux.ts +502 -0
- package/package.json +8 -6
- package/subagent-artifact-cli.ts +78 -0
- package/subagent.ts +702 -73
- package/helpers.ts +0 -599
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
> **Note:** The `docs/` folder is managed by the [`pi-docs`](https://github.com/lmn451/pi-docs) package.
|
|
6
6
|
|
|
7
|
-
A public [Pi](https://pi.dev) package that adds in-process sub-agent tools:
|
|
7
|
+
A public [Pi](https://pi.dev) package that adds in-process and attachable sub-agent tools:
|
|
8
8
|
|
|
9
9
|
- `subagent_with_context` — spawn a sub-agent that inherits the full conversation history
|
|
10
10
|
- `subagent_isolated` — spawn a sub-agent with a fresh, empty context window
|
|
@@ -12,7 +12,12 @@ A public [Pi](https://pi.dev) package that adds in-process sub-agent tools:
|
|
|
12
12
|
- `get_subagent_result` — block until an async job completes and return the final output
|
|
13
13
|
- `cancel_subagent` — abort a running async job
|
|
14
14
|
- `prune_subagent_jobs` — remove all completed and failed jobs from the registry
|
|
15
|
-
|
|
15
|
+
- `subagent_interactive` — spawn an attachable tmux-backed Pi session with artifact-based progress
|
|
16
|
+
- `get_interactive_subagent_status` — list attachable sessions with pane/session/artifact metadata
|
|
17
|
+
- `cancel_interactive_subagent` — kill an attachable sub-agent tmux pane
|
|
18
|
+
- `read_subagent_artifact` — read an interactive sub-agent's lifecycle events and output
|
|
19
|
+
- `list_subagent_artifacts` — list all known interactive sub-agents (in-session and on-disk)
|
|
20
|
+
The default sub-agents run inside the current Pi process, stream live progress back to the UI, and inherit the active model by default. Async sub-agents run in the background — the main agent continues immediately while you poll for progress and collect results when ready. Interactive sub-agents run as separate `pi --session ...` processes in tmux panes so you can attach and continue follow-ups directly there, and write structured progress to a per-sub-agent artifact directory on disk.
|
|
16
21
|
|
|
17
22
|
## Why use it?
|
|
18
23
|
|
|
@@ -22,6 +27,7 @@ The sub-agents run inside the current Pi process, stream live progress back to t
|
|
|
22
27
|
- Run sub-agents in the background while continuing the main conversation
|
|
23
28
|
- Poll, collect, or cancel background jobs on demand
|
|
24
29
|
- Get live previews of running sub-agents (current turn, active tool, usage)
|
|
30
|
+
- Attach to interactive sub-agent sessions for direct follow-ups and debugging
|
|
25
31
|
|
|
26
32
|

|
|
27
33
|
|
|
@@ -127,6 +133,69 @@ Parameters:
|
|
|
127
133
|
|
|
128
134
|
Remove all completed and failed subagent jobs from the registry. Running and cancelled jobs are preserved.
|
|
129
135
|
|
|
136
|
+
|
|
137
|
+
### Interactive tmux Tools
|
|
138
|
+
|
|
139
|
+
Use these when observability and manual follow-up matter more than in-process execution. They require running Pi inside tmux. Interactive sub-agents write their progress to a per-sub-agent artifact directory on disk; the pane is for live monitoring, the artifact is the source of truth.
|
|
140
|
+
|
|
141
|
+
#### `subagent_interactive`
|
|
142
|
+
|
|
143
|
+
Starts a separate interactive `pi` process in a tmux window and returns immediately with:
|
|
144
|
+
|
|
145
|
+
- sub-agent id
|
|
146
|
+
- tmux pane id
|
|
147
|
+
- `tmux attach ...` command (works from outside tmux)
|
|
148
|
+
- `tmux select-pane ...` or `tmux select-window ...` command for use inside the same tmux session
|
|
149
|
+
- child Pi session file path
|
|
150
|
+
- artifact directory (events.ndjson + output.md)
|
|
151
|
+
- the tmux window name (in background mode) so you can find it in your window list
|
|
152
|
+
|
|
153
|
+
Parameters:
|
|
154
|
+
|
|
155
|
+
- `task` — required initial task
|
|
156
|
+
- `name` — optional display name for the pane/session
|
|
157
|
+
- `persona` — optional system prompt appended to the child session
|
|
158
|
+
- `model` — optional model override
|
|
159
|
+
- `cwd` — optional working directory
|
|
160
|
+
- `includeContext` — include serialized parent conversation in the child prompt (default: `false`)
|
|
161
|
+
- `background` — spawn in a detached named window (invisible) instead of a visible horizontal split. Default `true` — your tmux layout is undisturbed and you can attach later with the returned `select-window` command. Pass `background: false` for a side-by-side split you can watch in real time.
|
|
162
|
+
|
|
163
|
+
The sub-agent's work is **always** written to the artifact dir as `events.ndjson` (lifecycle log) and `output.md` (clean prose the child writes). The pane is for live monitoring; the artifact is the source of truth. The artifact survives parent restarts, so sub-agents that finish while you're away are picked up on the next poll.
|
|
164
|
+
|
|
165
|
+
#### Sub-agent completion protocol
|
|
166
|
+
|
|
167
|
+
Every interactive sub-agent receives a built-in system prompt that tells it how to signal completion. The child **must** call one of these when it has nothing more to add before waiting for the next user input:
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
$ARTIFACT_DIR/cli.mjs done 0 # success — parent reads $ARTIFACT_DIR/output.md
|
|
171
|
+
$ARTIFACT_DIR/cli.mjs error "msg" # unrecoverable failure
|
|
172
|
+
# 'cancelled' is only set by the parent via cancel_interactive_subagent
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Write the final result to `$ARTIFACT_DIR/output.md` before calling `done`. After `done`, the REPL stays open and the child can be re-prompted via `tmux send-keys` to the pane. The parent gets a pointer notification on `done` / `error` / `cancelled` and reads the result via `read_subagent_artifact`. Tool calls and progress are visible in the TUI widget below the editor; no separate progress event is needed.
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
#### `get_interactive_subagent_status`
|
|
179
|
+
|
|
180
|
+
Lists tracked interactive sub-agents, attach/select commands, and session paths. It intentionally does **not** capture pane output to avoid consuming model context.
|
|
181
|
+
|
|
182
|
+
#### `cancel_interactive_subagent`
|
|
183
|
+
Kills the tmux pane for an interactive sub-agent by id. Writes a `cancelled` event to the artifact before killing the pane so the artifact log is self-describing.
|
|
184
|
+
|
|
185
|
+
#### `list_subagent_artifacts`
|
|
186
|
+
|
|
187
|
+
Lists all known interactive sub-agents: id, name, status, and last-update timestamp. Use this to discover sub-agents that finished while the parent was away.
|
|
188
|
+
|
|
189
|
+
#### `read_subagent_artifact`
|
|
190
|
+
|
|
191
|
+
Reads a sub-agent's artifact by id. Returns the lifecycle event log (pass `since` to fetch only new events) and, by default, the sub-agent's `output.md` content. This is the canonical way to get the sub-agent's work product — the parent agent does not need to read the tmux pane or capture rendered TUI.
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
Parameters:
|
|
195
|
+
- `id` — required sub-agent id
|
|
196
|
+
- `since` — optional unix-ms timestamp; only return events with `ts >= since`
|
|
197
|
+
- `includeOutput` — include `output.md` (default `true`)
|
|
198
|
+
|
|
130
199
|
### `list_available_models`
|
|
131
200
|
|
|
132
201
|
List all available AI models with auth status. Use this to validate model identifiers before passing them to subagent tools — prevents silent fallback to the parent session model.
|
|
@@ -142,6 +211,7 @@ Parameters:
|
|
|
142
211
|
- “Spawn a context-aware sub-agent to continue debugging while we keep planning here.”
|
|
143
212
|
- “Run a sub-agent in the background to run the test suite, then notify me when done.”
|
|
144
213
|
- “Spawn two isolated async sub-agents to review this code from different angles, then collect both results.”
|
|
214
|
+
- “Start an interactive sub-agent in tmux for investigating the auth bug; I’ll attach and guide it.”
|
|
145
215
|
|
|
146
216
|
## Development
|
|
147
217
|
|
package/artifact.ts
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sub-agent artifact storage.
|
|
3
|
+
*
|
|
4
|
+
* Each interactive sub-agent owns a directory under the parent's artifacts root.
|
|
5
|
+
* The directory holds two files:
|
|
6
|
+
*
|
|
7
|
+
* events.ndjson — append-only log of lifecycle and tool_activity events
|
|
8
|
+
|
|
9
|
+
* output.md — clean prose the sub-agent produced; atomically rewritten
|
|
10
|
+
*
|
|
11
|
+
* The parent agent's extension reads these files (via list_subagent_artifacts /
|
|
12
|
+
* read_subagent_artifact) to learn what the sub-agent did. The pane is for live
|
|
13
|
+
* monitoring only; the artifact is the source of truth.
|
|
14
|
+
*
|
|
15
|
+
* Files survive parent-agent restarts, so a sub-agent can complete while the
|
|
16
|
+
* parent is down and the parent can catch up by reading the artifact later.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
|
|
20
|
+
import { join } from "node:path";
|
|
21
|
+
|
|
22
|
+
// ── Types ───────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
export type SubagentStatus = "running" | "done" | "error" | "cancelled";
|
|
25
|
+
|
|
26
|
+
export interface SubagentEvent {
|
|
27
|
+
/** Unix epoch milliseconds */
|
|
28
|
+
ts: number;
|
|
29
|
+
type: "started" | "tool_activity" | "done" | "error" | "cancelled";
|
|
30
|
+
status: SubagentStatus;
|
|
31
|
+
message?: string;
|
|
32
|
+
/** For tool_activity: which tool was called and a short arg summary. */
|
|
33
|
+
tool?: string;
|
|
34
|
+
summary?: string;
|
|
35
|
+
exitCode?: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
export interface SubagentArtifact {
|
|
40
|
+
id: string;
|
|
41
|
+
dir: string;
|
|
42
|
+
statusFile: string;
|
|
43
|
+
outputFile: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ── Paths ───────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
export function artifactPath(rootDir: string, id: string): SubagentArtifact {
|
|
49
|
+
const dir = join(rootDir, id);
|
|
50
|
+
return {
|
|
51
|
+
id,
|
|
52
|
+
dir,
|
|
53
|
+
statusFile: join(dir, "events.ndjson"),
|
|
54
|
+
outputFile: join(dir, "output.md"),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Create the artifact directory with owner-only perms. Idempotent. */
|
|
59
|
+
export function ensureArtifactDir(art: SubagentArtifact): void {
|
|
60
|
+
mkdirSync(art.dir, { recursive: true, mode: 0o700 });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ── Writes ──────────────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
/** Append one event to the NDJSON log. Creates the dir if needed. */
|
|
66
|
+
export function appendEvent(art: SubagentArtifact, event: SubagentEvent): void {
|
|
67
|
+
ensureArtifactDir(art);
|
|
68
|
+
appendFileSync(art.statusFile, JSON.stringify(event) + "\n", { mode: 0o600 });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Atomically replace output.md with `content`. The actual write goes to a
|
|
73
|
+
* sibling .tmp file first; renameSync is atomic within a filesystem, so a
|
|
74
|
+
* concurrent reader sees either the old content or the new — never partial.
|
|
75
|
+
*/
|
|
76
|
+
export function writeOutput(art: SubagentArtifact, content: string): void {
|
|
77
|
+
ensureArtifactDir(art);
|
|
78
|
+
const tmp = art.outputFile + ".tmp";
|
|
79
|
+
writeFileSync(tmp, content, { mode: 0o600 });
|
|
80
|
+
renameSync(tmp, art.outputFile);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ── Reads ───────────────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Read all events for a sub-agent. If `since` is provided, only events with
|
|
87
|
+
* ts >= since are returned. Malformed lines are silently skipped (the
|
|
88
|
+
* sub-agent CLI is the only writer, but a partial write could in theory
|
|
89
|
+
* leave a truncated line).
|
|
90
|
+
*/
|
|
91
|
+
export function readEvents(art: SubagentArtifact, since?: number): SubagentEvent[] {
|
|
92
|
+
if (!existsSync(art.statusFile)) return [];
|
|
93
|
+
let content: string;
|
|
94
|
+
try {
|
|
95
|
+
content = readFileSync(art.statusFile, "utf8");
|
|
96
|
+
} catch {
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
const events: SubagentEvent[] = [];
|
|
100
|
+
for (const line of content.split("\n")) {
|
|
101
|
+
if (!line.trim()) continue;
|
|
102
|
+
try {
|
|
103
|
+
const ev = JSON.parse(line) as SubagentEvent;
|
|
104
|
+
if (since === undefined || ev.ts >= since) events.push(ev);
|
|
105
|
+
} catch {
|
|
106
|
+
// Skip malformed lines (partial write, manual edit, etc.)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return events;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Returns output.md content, or null if it doesn't exist yet. */
|
|
113
|
+
export function readOutput(art: SubagentArtifact): string | null {
|
|
114
|
+
if (!existsSync(art.outputFile)) return null;
|
|
115
|
+
try {
|
|
116
|
+
return readFileSync(art.outputFile, "utf8");
|
|
117
|
+
} catch {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** List all sub-agent artifacts under `rootDir`. Ignores loose files. */
|
|
123
|
+
export function listArtifacts(rootDir: string): SubagentArtifact[] {
|
|
124
|
+
if (!existsSync(rootDir)) return [];
|
|
125
|
+
let entries: string[];
|
|
126
|
+
try {
|
|
127
|
+
entries = readdirSync(rootDir);
|
|
128
|
+
} catch {
|
|
129
|
+
return [];
|
|
130
|
+
}
|
|
131
|
+
const out: SubagentArtifact[] = [];
|
|
132
|
+
for (const name of entries) {
|
|
133
|
+
const full = join(rootDir, name);
|
|
134
|
+
try {
|
|
135
|
+
if (statSync(full).isDirectory()) {
|
|
136
|
+
out.push(artifactPath(rootDir, name));
|
|
137
|
+
}
|
|
138
|
+
} catch {
|
|
139
|
+
// skip unreadable
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return out;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Most recent event, or null if no events yet. */
|
|
146
|
+
export function lastEvent(art: SubagentArtifact): SubagentEvent | null {
|
|
147
|
+
const events = readEvents(art);
|
|
148
|
+
return events.length > 0 ? events[events.length - 1] : null;
|
|
149
|
+
}
|
|
150
|
+
|