@arhen/pi-core-subagent 1.2.1 → 1.3.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 +28 -2
- package/package.json +1 -1
- package/src/index.ts +36 -3
package/README.md
CHANGED
|
@@ -90,10 +90,19 @@ Chain — `{previous}` is replaced with the prior agent's output:
|
|
|
90
90
|
}
|
|
91
91
|
```
|
|
92
92
|
|
|
93
|
+
The call line renders the graph in §2 notation as the model types it:
|
|
94
|
+
|
|
93
95
|
```
|
|
94
|
-
|
|
96
|
+
subagent graph 3
|
|
97
|
+
6 at a time
|
|
98
|
+
wave1[api ∥ db] → gate → wave2[doc]
|
|
99
|
+
api api-mapper Map every route in src/api/
|
|
100
|
+
db db-mapper Map the schema in src/db/
|
|
101
|
+
doc writer ✎ ← api, db Write ARCHITECTURE.md from the maps above. Verify: test -s ARCHI…
|
|
95
102
|
```
|
|
96
103
|
|
|
104
|
+
`✎` marks a write-toolset task; `←` lists its edges. With no `needs` anywhere the wave line is omitted entirely.
|
|
105
|
+
|
|
97
106
|
What the edge does:
|
|
98
107
|
|
|
99
108
|
- **Gates** — `doc` starts only after both `api` and `db` finish.
|
|
@@ -124,7 +133,7 @@ Background + intercom:
|
|
|
124
133
|
| Tool | Purpose |
|
|
125
134
|
|---|---|
|
|
126
135
|
| `subagent` | single / `tasks` (parallel or graph via `needs`) / `chain` (`{previous}`); `background:true` fire-and-forget; `allowIntercom:true` enables child talk tools; `notifyPerTask: true` wakes you as each task completes (default off) |
|
|
127
|
-
| `subagent_status` | live per-task snapshot (non-blocking) |
|
|
136
|
+
| `subagent_status` | live per-task snapshot (non-blocking), including each child's session file path |
|
|
128
137
|
| `subagent_result` | full output of a run or one task |
|
|
129
138
|
| `await_subagent` | block until a run finishes (optional `timeoutMs`) |
|
|
130
139
|
| `reply_subagent` | answer a child's `ask_parent` question |
|
|
@@ -152,6 +161,23 @@ Read-only pane over the session's subagents:
|
|
|
152
161
|
- `x` then `y` — abort ONE subagent (only mutation; `n`/any other key cancels)
|
|
153
162
|
- `esc` — close
|
|
154
163
|
|
|
164
|
+
## Watching a child from outside
|
|
165
|
+
|
|
166
|
+
`subagent_status` returns each running child's session file (JSONL). Children are `AgentSession`s in this process — they have no TTY — but their transcript is a real file, so any external viewer can follow one:
|
|
167
|
+
|
|
168
|
+
```sh
|
|
169
|
+
tail -f /path/from/subagent_status.jsonl
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
In a terminal multiplexer, that is a pane per agent — e.g. with [Herdr](https://herdr.dev):
|
|
173
|
+
|
|
174
|
+
```sh
|
|
175
|
+
herdr pane split --current --direction right
|
|
176
|
+
herdr pane run w1:p2 "tail -f /path/from/subagent_status.jsonl"
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
The extension has no multiplexer integration and does not want one: it exposes the path, your agent already knows how to drive its own terminal. For an in-pi view of the same stream, use [`/subagents peek`](#peek--subagents-peek-or-ctrlshifta).
|
|
180
|
+
|
|
155
181
|
## Context budget
|
|
156
182
|
|
|
157
183
|
- Parent tools: 6 schemas with short descriptions. **No catalog, no context hook** — nothing injected per request.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arhen/pi-core-subagent",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "pi extension: fast in-process subagents with a dependency-graph scheduler (needs edges gate tasks and carry upstream output into dependent prompts), plus background runs, intercom and agent-to-agent mailbox. Leader defines agents inline.",
|
|
6
6
|
"license": "MIT",
|
package/src/index.ts
CHANGED
|
@@ -472,6 +472,33 @@ export function resolveNeeds(inputs: { id?: string; needs?: string[] }[], mode:
|
|
|
472
472
|
return edges;
|
|
473
473
|
}
|
|
474
474
|
|
|
475
|
+
/**
|
|
476
|
+
* Graph Protocol §2 notation: `wave1[api ∥ db] → gate → wave2[doc]`.
|
|
477
|
+
*
|
|
478
|
+
* Tolerates half-streamed args: a need pointing at an id that has not arrived yet
|
|
479
|
+
* keeps its task out of the ready set, so the layout settles as the model types.
|
|
480
|
+
* Returns "" when there are no edges — flat fan-out gets no graph vocabulary.
|
|
481
|
+
*/
|
|
482
|
+
export function waveNotation(tasks: { id?: string; needs?: string[] }[]): string {
|
|
483
|
+
if (!tasks.some((t) => t.needs?.length)) return "";
|
|
484
|
+
const ids = tasks.map((t, i) => t.id ?? `task_${i + 1}`);
|
|
485
|
+
const settled = new Set<string>();
|
|
486
|
+
let remaining = tasks.map((t, i) => ({ id: ids[i] as string, needs: t.needs ?? [] }));
|
|
487
|
+
const waves: string[][] = [];
|
|
488
|
+
while (remaining.length > 0) {
|
|
489
|
+
const ready = remaining.filter((t) => t.needs.every((n) => settled.has(n)));
|
|
490
|
+
if (ready.length === 0) break; // cycle, or an upstream id not typed yet
|
|
491
|
+
waves.push(ready.map((t) => t.id));
|
|
492
|
+
for (const t of ready) settled.add(t.id);
|
|
493
|
+
remaining = remaining.filter((t) => !settled.has(t.id));
|
|
494
|
+
}
|
|
495
|
+
if (remaining.length > 0) waves.push(remaining.map((t) => t.id)); // show them rather than drop them
|
|
496
|
+
if (waves.length < 2) return "";
|
|
497
|
+
const full = waves.map((w, i) => `wave${i + 1}[${w.join(" ∥ ")}]`).join(" → gate → ");
|
|
498
|
+
// Long graphs: keep the shape, drop the names.
|
|
499
|
+
return full.length <= 100 ? full : waves.map((w, i) => `wave${i + 1}[${w.length}]`).join(" → gate → ");
|
|
500
|
+
}
|
|
501
|
+
|
|
475
502
|
/**
|
|
476
503
|
* Graph Protocol §6: the edge carries the upstream output, not just ordering.
|
|
477
504
|
* Upstream results are prepended verbatim; `{previous}` stays supported so old
|
|
@@ -1402,6 +1429,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
1402
1429
|
if (args.concurrency) parts.push(`${args.concurrency} at a time`);
|
|
1403
1430
|
if (args.maxRuntimeMs) parts.push(`${Math.round(args.maxRuntimeMs / 60000)}m limit`);
|
|
1404
1431
|
const params = parts.length > 0 ? `\n ${theme.fg("dim", parts.join(" · "))}` : "";
|
|
1432
|
+
const notation = waveNotation(tasks);
|
|
1433
|
+
const graphLine = notation ? `\n ${theme.fg("muted", notation)}` : "";
|
|
1405
1434
|
// The plan the model actually wrote: ids, edges, toolset. Streams in as args arrive,
|
|
1406
1435
|
// so a graph is visible before the first child spawns.
|
|
1407
1436
|
const plan = tasks
|
|
@@ -1416,7 +1445,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1416
1445
|
return `\n ${theme.fg("muted", id)} ${theme.fg("accent", t.agent ?? "…")}${mark}${edge}${what}`;
|
|
1417
1446
|
})
|
|
1418
1447
|
.join("");
|
|
1419
|
-
return new Text(`${theme.fg("toolTitle", theme.bold("subagent"))} ${theme.fg("accent", mode)}${flags ? ` ${theme.fg("muted", `[${flags}]`)}` : ""}${params}${plan}`, 0, 0);
|
|
1448
|
+
return new Text(`${theme.fg("toolTitle", theme.bold("subagent"))} ${theme.fg("accent", mode)}${flags ? ` ${theme.fg("muted", `[${flags}]`)}` : ""}${params}${graphLine}${plan}`, 0, 0);
|
|
1420
1449
|
},
|
|
1421
1450
|
renderResult(result, { expanded }, theme) {
|
|
1422
1451
|
const run = result.details?.run;
|
|
@@ -1444,14 +1473,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
1444
1473
|
pi.registerTool<typeof RunIdParam, { run?: RunSnapshot }>({
|
|
1445
1474
|
name: "subagent_status",
|
|
1446
1475
|
label: "Subagent Status",
|
|
1447
|
-
description: "Live status of a subagent run (non-blocking): per-task state.",
|
|
1476
|
+
description: "Live status of a subagent run (non-blocking): per-task state, plus each child's session file path (JSONL) so you can tail it from outside — e.g. in a terminal multiplexer pane.",
|
|
1448
1477
|
promptSnippet: "Check progress of a subagent run.",
|
|
1449
1478
|
parameters: RunIdParam,
|
|
1450
1479
|
async execute(_id, params) {
|
|
1451
1480
|
const { runId } = params as { runId: string };
|
|
1452
1481
|
const run = manager.getRun(runId);
|
|
1453
1482
|
if (!run) return { content: [{ type: "text", text: `Unknown runId: ${runId}` }], isError: true, details: {} };
|
|
1454
|
-
|
|
1483
|
+
// Session file paths are the one primitive an outside tool needs: `tail -f` it in a
|
|
1484
|
+
// multiplexer pane, a log viewer, anything. Cheaper than owning a pane integration.
|
|
1485
|
+
const files = run.tasks.filter((t) => t.sessionFile).map((t) => `${t.id} (${t.agent}): ${t.sessionFile}`);
|
|
1486
|
+
const text = [compactLines(run).join("\n"), ...(files.length > 0 ? ["", "Live session files (tail -f to watch):", ...files] : [])].join("\n");
|
|
1487
|
+
return { content: [{ type: "text", text }], details: { run: cloneRun(run) } };
|
|
1455
1488
|
},
|
|
1456
1489
|
});
|
|
1457
1490
|
|