pi-subagentura 1.0.12 → 2.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
@@ -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
- The 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.
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
  ![Sub-agent demo](working.png)
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
+
package/helpers.ts CHANGED
@@ -8,12 +8,12 @@
8
8
  import { randomBytes } from "node:crypto";
9
9
  import { appendFileSync, mkdirSync, existsSync } from "node:fs";
10
10
  import { resolve } from "node:path";
11
- import { getModel, getProviders } from "@mariozechner/pi-ai";
12
- import type { Model } from "@mariozechner/pi-ai";
11
+ import { getModel, getProviders } from "@earendil-works/pi-ai";
12
+ import type { Model } from "@earendil-works/pi-ai";
13
13
 
14
14
  // Note: Model<TApi> and AgentToolResult<T> are SDK generics. We use `unknown` as
15
15
  // the type argument to avoid strict generic instantiation issues with tsc.
16
- import type { AgentToolResult } from "@mariozechner/pi-agent-core";
16
+ import type { AgentToolResult } from "@earendil-works/pi-agent-core";
17
17
 
18
18
  import {
19
19
  AuthStorage,
@@ -21,7 +21,7 @@ import {
21
21
  ModelRegistry,
22
22
  SessionManager,
23
23
  type AgentSession,
24
- } from "@mariozechner/pi-coding-agent";
24
+ } from "@earendil-works/pi-coding-agent";
25
25
 
26
26
  // ── Debug Logging ─────────────────────────────────────────────────
27
27
 
@@ -77,7 +77,7 @@ export function extractTextFromContent(content: unknown): string {
77
77
  */
78
78
  export const ACTIVE_TOOL_DEBOUNCE_MS = 150;
79
79
 
80
- // Note: If Pi adds new providers, getProviders() from @mariozechner/pi-ai will
80
+ // Note: If Pi adds new providers, getProviders() from @earendil-works/pi-ai will
81
81
  // return them automatically. We no longer maintain a hardcoded list.
82
82
 
83
83
  // ── Types ───────────────────────────────────────────────────────────