@arhen/pi-core-subagent 1.3.24 → 1.3.26
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 +27 -7
- package/package.json +1 -1
- package/src/agentfile.ts +61 -0
- package/src/format.ts +1 -1
- package/src/index.ts +29 -57
- package/src/manager.ts +14 -49
- package/src/schemas.ts +3 -5
- package/src/types.ts +0 -2
package/README.md
CHANGED
|
@@ -50,7 +50,7 @@ flowchart LR
|
|
|
50
50
|
- **A bad graph fails before it spawns.** Unknown ids, self-edges and cycles are rejected at call time — never halfway through a run with three children already burning tokens.
|
|
51
51
|
- **Proof is an exit code, never a self-report.** Tasks are asked for a runnable `Verify:` command; the leader checks `git diff --stat`. Agents auditing their own work score ~0. ([why](#why-9-is-a-verification-command-not-a-self-report))
|
|
52
52
|
- **No ceremony without edges.** Six independent reviewers stay six independent reviewers — no waves, no gates, no graph vocabulary imposed on flat work.
|
|
53
|
-
- **
|
|
53
|
+
- **Agent files respected.** An `agent` name matching `.agents/agents/<name>.md`, `.claude/agents/<name>.md`, or `.pi/agents/<name>.md` loads that file — body = system prompt, frontmatter `model`/`tools` apply, `model` is validated against the pi model registry. Inline params override the file. Project dirs win over home (`~/.agents` single source → `~/.claude` → `~/.pi`).
|
|
54
54
|
- **Two toolsets only.** Read-only (`read, grep, find, ls` — default) or write (`read, grep, find, ls, bash, edit, write` — `write: true`). No per-agent tool config surface.
|
|
55
55
|
- **In-process** — children are `AgentSession`s in the same runtime. No process spawn, no context bleed.
|
|
56
56
|
- **Zero parent-context injection.** No catalog, no context hook. 6 slim tools total.
|
|
@@ -83,7 +83,7 @@ The dotted arrows are the whole point: a child may burn 200k tokens reading file
|
|
|
83
83
|
|
|
84
84
|
## Usage — the leader invents the agents
|
|
85
85
|
|
|
86
|
-
Define agents inline per call
|
|
86
|
+
Define agents inline per call, or reference a named agent file (see [Agent files](#agent-files)). Model resolution: explicit `model` → agent-file `model` (validated against the pi model registry) → the parent's current model → settings default.
|
|
87
87
|
|
|
88
88
|
```json
|
|
89
89
|
{
|
|
@@ -116,6 +116,27 @@ Chain — `{previous}` is replaced with the prior agent's output:
|
|
|
116
116
|
}
|
|
117
117
|
```
|
|
118
118
|
|
|
119
|
+
## Agent files
|
|
120
|
+
|
|
121
|
+
An `agent` name that matches `<name>.md` in an agents directory loads that file — no inline `prompt` needed. The file body becomes the system prompt; `model` and `tools` frontmatter apply.
|
|
122
|
+
|
|
123
|
+
```md
|
|
124
|
+
---
|
|
125
|
+
name: api-reviewer
|
|
126
|
+
description: Strict API reviewer — auth, rate limiting, error handling
|
|
127
|
+
model: claude-opus-4-6
|
|
128
|
+
tools: read, grep, find, ls
|
|
129
|
+
---
|
|
130
|
+
You are a strict API reviewer. Check auth, rate limiting, and error handling. Cite file:line.
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**Lookup order** (first match wins):
|
|
134
|
+
|
|
135
|
+
1. `.agents/agents/` then `.claude/agents/` then `.pi/agents/` in each directory from the task `cwd` up to the filesystem root (nearest ancestor wins).
|
|
136
|
+
2. Home: `~/.agents/agents/` (single source) → `~/.claude/agents/` → `~/.pi/agents/`.
|
|
137
|
+
|
|
138
|
+
**Precedence** — inline params always win over the file: `prompt` overrides the body, `model` overrides frontmatter `model`, `tools`/`write` override frontmatter `tools`. A file `model` is validated against the pi model registry (unknown model fails the task with a catalog message). Files without frontmatter work too — the whole file is the system prompt.
|
|
139
|
+
|
|
119
140
|
## Graph mode — `needs`
|
|
120
141
|
|
|
121
142
|
`parallel` runs everything at once; `chain` runs everything one at a time. Most real work is neither. Give a task an `id` and list the ids it `needs`:
|
|
@@ -238,7 +259,7 @@ Background (default) + intercom — the run returns a runId immediately; you sta
|
|
|
238
259
|
|
|
239
260
|
| Tool | Purpose |
|
|
240
261
|
|---|---|
|
|
241
|
-
| `subagent` | single / `tasks` (parallel or graph via `needs`) / `chain` (`{previous}`);
|
|
262
|
+
| `subagent` | single / `tasks` (parallel or graph via `needs`) / `chain` (`{previous}`); every run is background — returns a runId, completion notifies you; `autoAwait:true` parks the call until the run finishes and returns the final result inline; `allowIntercom:true` enables child talk tools; `notifyPerTask` (default true) wakes you as each task completes |
|
|
242
263
|
| `subagent_status` | live per-task snapshot (non-blocking), including each child's session file path |
|
|
243
264
|
| `subagent_result` | full output of a run or one task |
|
|
244
265
|
| `await_subagent` | block until a run finishes (optional `timeoutMs`) |
|
|
@@ -248,7 +269,7 @@ Background (default) + intercom — the run returns a runId immediately; you sta
|
|
|
248
269
|
|
|
249
270
|
### Per-task fields
|
|
250
271
|
|
|
251
|
-
`agent` (name you invent — required), `task` (required), `prompt` (system prompt, optional — minimal default used), `write` (toolset, default read-only), plus optional `model` (`provider/model-id`), `thinking` (validated enum: `off|minimal|low|medium|high|xhigh|max`), `tools` (explicit allowlist), `cwd`, `maxRuntimeMs`, `id`, `needs` (dependency edges — see [Graph mode](#graph-mode--needs)). Top-level only: `
|
|
272
|
+
`agent` (name you invent — required), `task` (required), `prompt` (system prompt, optional — minimal default used), `write` (toolset, default read-only), plus optional `model` (`provider/model-id`), `thinking` (validated enum: `off|minimal|low|medium|high|xhigh|max`), `tools` (explicit allowlist), `cwd`, `maxRuntimeMs`, `id`, `needs` (dependency edges — see [Graph mode](#graph-mode--needs)). Top-level only: `autoAwait`, `notifyPerTask`, `allowIntercom`, `concurrency`.
|
|
252
273
|
|
|
253
274
|
### Child talk tools (when `allowIntercom: true`)
|
|
254
275
|
|
|
@@ -259,13 +280,12 @@ Background (default) + intercom — the run returns a runId immediately; you sta
|
|
|
259
280
|
| `send_agent_message` | message to a sibling subagent's mailbox (`to` = its task id, or `"leader"`) |
|
|
260
281
|
| `poll_agent_messages` | drain this subagent's mailbox |
|
|
261
282
|
|
|
262
|
-
> **Intercom anti-deadlock:** children are told to never block indefinitely on intercom replies — `ask_parent` keeps the stall watchdog fed while awaiting a parent reply
|
|
283
|
+
> **Intercom anti-deadlock:** children are told to never block indefinitely on intercom replies — `ask_parent` keeps the stall watchdog fed while awaiting a parent reply, and sibling polls are capped (~5 tries) with a proceed-with-best-judgment fallback. Gated siblings (later waves) may not be running yet — waiting on them is the top stall cause, so children are instructed not to.
|
|
263
284
|
|
|
264
285
|
## Commands
|
|
265
286
|
|
|
266
287
|
- `/subagents` — list runs; `/subagents peek` (or `ctrl+shift+a`) — browsable pane
|
|
267
|
-
- `/subagents auto-
|
|
268
|
-
- `/subagents auto-limit on|off` — toggle leader-imposed `maxRuntimeMs` caps (persists to the same config; default on). `off` strips ALL task timeouts: tasks run unlimited until done, stalled, or aborted — only for runs where a hard bound is genuinely required is a cap kept (none, when off). Bare `/subagents auto-limit` shows the current state.
|
|
288
|
+
- `/subagents auto-limit on|off` — toggle leader-imposed `maxRuntimeMs` caps (persists to `~/.pi/agent/subagents-config.json`; default on). `off` strips ALL task timeouts: tasks run unlimited until done, stalled, or aborted — only for runs where a hard bound is genuinely required is a cap kept (none, when off). Bare `/subagents auto-limit` shows the current state.
|
|
269
289
|
|
|
270
290
|
## Peek — `/subagents peek` or `ctrl+shift+a`
|
|
271
291
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arhen/pi-core-subagent",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.26",
|
|
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/agentfile.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/** Named agent-file resolution (.agents/.claude/.pi agents dirs). */
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { parseFrontmatter } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
|
|
6
|
+
export interface AgentFileInfo {
|
|
7
|
+
body: string;
|
|
8
|
+
model?: string;
|
|
9
|
+
tools?: string[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const AGENT_DIRS = [".agents/agents", ".claude/agents", ".pi/agents"] as const;
|
|
13
|
+
/** Agent names become file paths — refuse anything that could traverse. */
|
|
14
|
+
const SAFE_NAME = /^[\w.-]+$/;
|
|
15
|
+
|
|
16
|
+
function readAgentFile(dir: string, name: string): AgentFileInfo | undefined {
|
|
17
|
+
if (!existsSync(dir)) return undefined;
|
|
18
|
+
const path = join(dir, `${name}.md`);
|
|
19
|
+
if (!existsSync(path)) return undefined;
|
|
20
|
+
const { frontmatter, body } = parseFrontmatter(readFileSync(path, "utf8"));
|
|
21
|
+
const tools =
|
|
22
|
+
typeof frontmatter.tools === "string"
|
|
23
|
+
? frontmatter.tools
|
|
24
|
+
.split(",")
|
|
25
|
+
.map((t) => t.trim())
|
|
26
|
+
.filter(Boolean)
|
|
27
|
+
: Array.isArray(frontmatter.tools)
|
|
28
|
+
? frontmatter.tools.map(String)
|
|
29
|
+
: undefined;
|
|
30
|
+
return {
|
|
31
|
+
body,
|
|
32
|
+
model: typeof frontmatter.model === "string" ? frontmatter.model : undefined,
|
|
33
|
+
tools: tools?.length ? tools : undefined,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Look up `<name>.md` in agent dirs. Order: for each ancestor of `cwd` (nearest
|
|
39
|
+
* first): `.agents/agents` (single source per the ~/.agents spec) → `.claude/agents`
|
|
40
|
+
* → `.pi/agents`; then home: `~/.agents/agents` → `~/.claude/agents` → `~/.pi/agents`.
|
|
41
|
+
* First match wins. `agentDir` is the pi agent dir (`~/.pi/agent`); home is derived from it.
|
|
42
|
+
*/
|
|
43
|
+
export function resolveAgentFile(name: string, cwd: string, agentDir: string): AgentFileInfo | undefined {
|
|
44
|
+
if (!SAFE_NAME.test(name)) return undefined;
|
|
45
|
+
let dir = cwd;
|
|
46
|
+
while (true) {
|
|
47
|
+
for (const sub of AGENT_DIRS) {
|
|
48
|
+
const hit = readAgentFile(join(dir, sub), name);
|
|
49
|
+
if (hit) return hit;
|
|
50
|
+
}
|
|
51
|
+
const parent = dirname(dir);
|
|
52
|
+
if (parent === dir) break;
|
|
53
|
+
dir = parent;
|
|
54
|
+
}
|
|
55
|
+
const home = dirname(dirname(agentDir)); // ~/.pi/agent → ~
|
|
56
|
+
for (const sub of AGENT_DIRS) {
|
|
57
|
+
const hit = readAgentFile(join(home, sub), name);
|
|
58
|
+
if (hit) return hit;
|
|
59
|
+
}
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
package/src/format.ts
CHANGED
|
@@ -213,7 +213,7 @@ export function makeSummary(run: RunSnapshot): string {
|
|
|
213
213
|
const aborted = run.tasks.filter((t) => t.status === "aborted").length;
|
|
214
214
|
const done = TERMINAL.includes(run.status) ? "finished" : "running";
|
|
215
215
|
const lines = [
|
|
216
|
-
`Run ${run.id}: Subagents ${run.mode}
|
|
216
|
+
`Run ${run.id}: Subagents ${run.mode} ${done}: ${succeeded}/${run.tasks.length} succeeded${failed ? `, ${failed} failed` : ""}${aborted ? `, ${aborted} aborted` : ""}.`,
|
|
217
217
|
];
|
|
218
218
|
const usage = formatUsage(run.aggregateUsage);
|
|
219
219
|
if (usage) lines.push(`Usage: ${usage}`);
|
package/src/index.ts
CHANGED
|
@@ -16,15 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
18
18
|
import { Text, truncateToWidth } from "@earendil-works/pi-tui";
|
|
19
|
-
import {
|
|
20
|
-
compactLines,
|
|
21
|
-
formatUsage,
|
|
22
|
-
makeSummary,
|
|
23
|
-
statusIcon,
|
|
24
|
-
taskLine,
|
|
25
|
-
themedTaskLine,
|
|
26
|
-
truncateText,
|
|
27
|
-
} from "./format.ts";
|
|
19
|
+
import { compactLines, formatUsage, makeSummary, statusIcon, taskLine, truncateText } from "./format.ts";
|
|
28
20
|
import { waveNotation } from "./graph.ts";
|
|
29
21
|
import { cloneRun, SubagentManager } from "./manager.ts";
|
|
30
22
|
import { createPeekPane, type PeekTask } from "./peek.ts";
|
|
@@ -77,8 +69,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
77
69
|
);
|
|
78
70
|
};
|
|
79
71
|
pi.registerCommand("subagents", {
|
|
80
|
-
description:
|
|
81
|
-
"List subagent runs. `/subagents peek` opens the browsable pane; `/subagents auto-bg on|off` toggles background-by-default.",
|
|
72
|
+
description: "List subagent runs. `/subagents peek` opens the browsable pane.",
|
|
82
73
|
handler: async (args, ctx) => {
|
|
83
74
|
const arg = String(args ?? "")
|
|
84
75
|
.trim()
|
|
@@ -100,22 +91,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
100
91
|
}
|
|
101
92
|
return;
|
|
102
93
|
}
|
|
103
|
-
if (arg === "auto-bg" || arg.startsWith("auto-bg ")) {
|
|
104
|
-
const value = arg.split(/\s+/)[1];
|
|
105
|
-
if (value === "on" || value === "off") {
|
|
106
|
-
const next = manager.setAutoBg(value === "on");
|
|
107
|
-
ctx.ui.notify(
|
|
108
|
-
`auto-bg ${next ? "on" : "off"} — subagent calls default to ${next ? "background" : "blocking (inline result)"}.`,
|
|
109
|
-
"info",
|
|
110
|
-
);
|
|
111
|
-
} else {
|
|
112
|
-
ctx.ui.notify(
|
|
113
|
-
`auto-bg is ${manager.autoBgOn ? "on" : "off"} — use \`/subagents auto-bg on|off\` to change it.`,
|
|
114
|
-
"info",
|
|
115
|
-
);
|
|
116
|
-
}
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
94
|
const runs = manager.listRuns().slice(0, 10);
|
|
120
95
|
if (runs.length === 0) {
|
|
121
96
|
ctx.ui.notify("No subagent runs in this session.", "info");
|
|
@@ -152,7 +127,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
152
127
|
// ponytail: this string is billed on every request. No example block — an example
|
|
153
128
|
// biases the model toward one shape; guidelines + JSON schema describe all of them.
|
|
154
129
|
description:
|
|
155
|
-
"Run isolated subagents (own context, own session). You invent each agent: name, optional system prompt, toolset (read-only default, write:true to edit). Use `agent`+`task` for one, `tasks` for many. `needs` declares dependency edges: a task waits for its needs and receives their outputs prepended to its prompt.
|
|
130
|
+
"Run isolated subagents (own context, own session). You invent each agent: name, optional system prompt, toolset (read-only default, write:true to edit). Use `agent`+`task` for one, `tasks` for many. `needs` declares dependency edges: a task waits for its needs and receives their outputs prepended to its prompt. An `agent` name matching a file in `.agents/agents`, `.claude/agents`, or `.pi/agents` (project dirs, then home) loads it: body = system prompt, frontmatter `model`/`tools` apply; inline prompt/model/tools/write override the file. Every run is background: the call returns a runId immediately and completion notifies you. Set autoAwait:true when you need the result before your next step — the call parks until the run finishes and returns runId + final result in one response. allowIntercom:true lets children talk to you and each other.",
|
|
156
131
|
promptSnippet: "Define and delegate work to specialized subagents.",
|
|
157
132
|
promptGuidelines: [
|
|
158
133
|
"Use subagent when independent review, testing, research, or parallel analysis improves quality.",
|
|
@@ -160,29 +135,34 @@ export default function (pi: ExtensionAPI) {
|
|
|
160
135
|
"Order comes from `needs`, not from separate calls: give tasks an `id`, list the ids each depends on. Tasks with no unmet needs run in parallel; dependents receive their upstream outputs automatically — do not restate them.",
|
|
161
136
|
"Prefer flat `tasks` (plain parallel) unless a real dependency exists — only add `needs` edges when ordering genuinely matters.",
|
|
162
137
|
"End each task with a runnable check, e.g. 'Verify: npx tsc --noEmit && bun test'. A subagent's claim of success is not evidence.",
|
|
163
|
-
"Define each agent yourself: invented name, focused system prompt, and read-only (default) or write:true. Prefer read-only.",
|
|
164
|
-
"
|
|
165
|
-
"For long multi-task runs, don't
|
|
138
|
+
"Define each agent yourself: invented name, focused system prompt, and read-only (default) or write:true. Prefer read-only. An `agent` name that matches an existing file in `.agents/agents`, `.claude/agents`, or `.pi/agents` (project first, then home) loads that agent — body = system prompt, frontmatter `model`/`tools` apply, file `model` is validated against the model registry; inline params override the file. Files without frontmatter work too.",
|
|
139
|
+
"When you need a run's result before your next step, spawn with autoAwait:true — the call returns runId + final result in one response. Otherwise spawn background and settle results (await_subagent / subagent_result) before continuing dependent work.",
|
|
140
|
+
"For long multi-task runs, don't autoAwait the whole run: spawn background, then loop await_subagent with short timeoutMs slices (e.g. 20s), processing whichever tasks completed in each slice while the rest keep running. You get incremental results instead of one big wait.",
|
|
166
141
|
"allowIntercom:true only when a child may need to ask you something.",
|
|
167
142
|
],
|
|
168
143
|
parameters: SubagentParams,
|
|
169
144
|
executionMode: "parallel", // sibling subagent calls run concurrently, not serialized
|
|
170
145
|
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
171
146
|
const typed = params as SubagentParamsShape;
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
details,
|
|
182
|
-
};
|
|
147
|
+
const details = manager.startInBackground(typed, ctx);
|
|
148
|
+
if (typed.autoAwait) {
|
|
149
|
+
// awaitRun wakes on every child→leader message (ask/notify/done) — that's the
|
|
150
|
+
// slice-loop feature. autoAwait wants the final result: re-park until terminal.
|
|
151
|
+
let run = details.run;
|
|
152
|
+
while (!TERMINAL.includes(run.status)) {
|
|
153
|
+
run = (await manager.awaitRun(details.run.id))?.run ?? run;
|
|
154
|
+
}
|
|
155
|
+
return { content: [{ type: "text", text: makeSummary(run) }], details: { run } };
|
|
183
156
|
}
|
|
184
|
-
|
|
185
|
-
|
|
157
|
+
return {
|
|
158
|
+
content: [
|
|
159
|
+
{
|
|
160
|
+
type: "text",
|
|
161
|
+
text: `Background run started: ${details.run.id} (${details.run.mode}, ${details.run.tasks.length} task${details.run.tasks.length > 1 ? "s" : ""}).\nUse subagent_status / subagent_result / await_subagent / reply_subagent / subagent_cancel to interact.`,
|
|
162
|
+
},
|
|
163
|
+
],
|
|
164
|
+
details,
|
|
165
|
+
};
|
|
186
166
|
},
|
|
187
167
|
renderCall(args, theme) {
|
|
188
168
|
// ponytail: args stream in partially, so mode is unknowable until JSON closes. Show "preparing…" instead of a wrong "single ?".
|
|
@@ -194,9 +174,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
194
174
|
: args.agent
|
|
195
175
|
? `single ${args.agent}`
|
|
196
176
|
: "preparing…";
|
|
197
|
-
const flags = [
|
|
198
|
-
.filter(Boolean)
|
|
199
|
-
.join(" · ");
|
|
177
|
+
const flags = [args.autoAwait ? "await" : "bg", args.allowIntercom ? "a2a" : ""].filter(Boolean).join(" · ");
|
|
200
178
|
// Params used, dimmed: model, thinking, toolset, per-task write count.
|
|
201
179
|
const tasks = args.tasks ?? args.chain ?? [];
|
|
202
180
|
const writeCount = tasks.filter((t) => t.write).length;
|
|
@@ -237,18 +215,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
237
215
|
const run = result.details?.run;
|
|
238
216
|
if (!run) return new Text(result.content[0]?.type === "text" ? result.content[0].text : "", 0, 0);
|
|
239
217
|
// ponytail: mode/count already shown on the call line above; result header only adds progress + status.
|
|
240
|
-
const header = `${statusIcon(run.status)} ${theme.fg("accent", `${run.tasks.filter((t) => t.status === "completed").length}/${run.tasks.length} done`)}
|
|
218
|
+
const header = `${statusIcon(run.status)} ${theme.fg("accent", `${run.tasks.filter((t) => t.status === "completed").length}/${run.tasks.length} done`)} ${theme.fg("muted", run.status)}`;
|
|
241
219
|
if (!expanded) {
|
|
242
|
-
//
|
|
243
|
-
// already shows live per-task state — keep the card to the header only.
|
|
244
|
-
if (run.background) {
|
|
245
|
-
const usage = formatUsage(run.aggregateUsage);
|
|
246
|
-
return new Text(usage ? `${header}\n${theme.fg("dim", usage)}` : header, 0, 0);
|
|
247
|
-
}
|
|
248
|
-
const lines = [header, ...run.tasks.map((task) => ` ${themedTaskLine(task, theme)}`)];
|
|
220
|
+
// Every run is background: the spawn snapshot is always "0 tools" noise and the footer
|
|
221
|
+
// widget already shows live per-task state — keep the card to the header only.
|
|
249
222
|
const usage = formatUsage(run.aggregateUsage);
|
|
250
|
-
|
|
251
|
-
return new Text(lines.join("\n"), 0, 0);
|
|
223
|
+
return new Text(usage ? `${header}\n${theme.fg("dim", usage)}` : header, 0, 0);
|
|
252
224
|
}
|
|
253
225
|
const lines = [header];
|
|
254
226
|
for (const task of run.tasks) {
|
package/src/manager.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
type ToolDefinition,
|
|
17
17
|
} from "@earendil-works/pi-coding-agent";
|
|
18
18
|
import type { TUI } from "@earendil-works/pi-tui";
|
|
19
|
+
import { resolveAgentFile } from "./agentfile.ts";
|
|
19
20
|
import { CHILD_TALK_TOOLS, type ChildHandlers, createChildTools, createWatchdog, type Watchdog } from "./child.ts";
|
|
20
21
|
import {
|
|
21
22
|
activitySnippet,
|
|
@@ -211,9 +212,6 @@ export class SubagentManager {
|
|
|
211
212
|
private widgetRuns: RunSnapshot[] = [];
|
|
212
213
|
private eventSeq = 0;
|
|
213
214
|
|
|
214
|
-
/** Default for `background` when the agent doesn't say — toggle via `/subagents auto-bg on|off`. */
|
|
215
|
-
private autoBg = true;
|
|
216
|
-
|
|
217
215
|
/** When false, strip leader-imposed maxRuntimeMs so tasks run unlimited — toggle via `/subagents auto-limit on|off`. */
|
|
218
216
|
private autoLimit = true;
|
|
219
217
|
|
|
@@ -222,37 +220,21 @@ export class SubagentManager {
|
|
|
222
220
|
constructor(private readonly pi: ExtensionAPI) {
|
|
223
221
|
try {
|
|
224
222
|
const cfg = JSON.parse(readFileSync(join(getAgentDir(), "subagents-config.json"), "utf8"));
|
|
225
|
-
if (typeof cfg.autoBg === "boolean") this.autoBg = cfg.autoBg;
|
|
226
223
|
if (typeof cfg.autoLimit === "boolean") this.autoLimit = cfg.autoLimit;
|
|
227
224
|
} catch {
|
|
228
225
|
/* no config yet — defaults */
|
|
229
226
|
}
|
|
230
227
|
}
|
|
231
228
|
|
|
232
|
-
/** Flip the background-by-default flag; persists to the agent dir. Returns the new value. */
|
|
233
|
-
setAutoBg(on: boolean): boolean {
|
|
234
|
-
this.autoBg = on;
|
|
235
|
-
void writeFile(
|
|
236
|
-
join(getAgentDir(), "subagents-config.json"),
|
|
237
|
-
JSON.stringify({ autoBg: on, autoLimit: this.autoLimit }, null, 2),
|
|
238
|
-
).catch(() => {});
|
|
239
|
-
return on;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
229
|
/** Flip the auto-limit flag; persists to the agent dir. Returns the new value. */
|
|
243
230
|
setAutoLimit(on: boolean): boolean {
|
|
244
231
|
this.autoLimit = on;
|
|
245
|
-
void writeFile(
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
).catch(() => {});
|
|
232
|
+
void writeFile(join(getAgentDir(), "subagents-config.json"), JSON.stringify({ autoLimit: on }, null, 2)).catch(
|
|
233
|
+
() => {},
|
|
234
|
+
);
|
|
249
235
|
return on;
|
|
250
236
|
}
|
|
251
237
|
|
|
252
|
-
get autoBgOn(): boolean {
|
|
253
|
-
return this.autoBg;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
238
|
get autoLimitOn(): boolean {
|
|
257
239
|
return this.autoLimit;
|
|
258
240
|
}
|
|
@@ -392,7 +374,7 @@ export class SubagentManager {
|
|
|
392
374
|
if (kind !== "asked" && run.awaited) return; // parent already got the result via await_subagent
|
|
393
375
|
const body =
|
|
394
376
|
kind === "asked"
|
|
395
|
-
? `A
|
|
377
|
+
? `A subagent is asking you a question (task ${extra?.taskId}): ${extra?.question ?? ""}\nReply with reply_subagent(runId: "${run.id}", taskId: "${extra?.taskId}", message: ...).`
|
|
396
378
|
: makeNotice(run, kind);
|
|
397
379
|
try {
|
|
398
380
|
this.pi.sendUserMessage(body, { deliverAs: "followUp" });
|
|
@@ -509,13 +491,6 @@ export class SubagentManager {
|
|
|
509
491
|
if (this.collectParked(run.id, { kind: "ask", taskId: task.id, agent: task.agent, text: question })) {
|
|
510
492
|
return "Your question was delivered to the parent (they're waiting on this run). Keep working; the answer arrives via the pending reply.";
|
|
511
493
|
}
|
|
512
|
-
// A blocking run's parent can't reply mid-tool (followUp only fires after the
|
|
513
|
-
// tool returns) — only background runs can truly wait for the answer.
|
|
514
|
-
if (!run.background) {
|
|
515
|
-
this.updateTask(run, task, { status: "running" }, ctx);
|
|
516
|
-
this.liveChildren.get(`${run.id}:${task.id}`)?.touchWatchdog();
|
|
517
|
-
return "Parent cannot answer while this run is blocking. Continue autonomously with your best judgment.";
|
|
518
|
-
}
|
|
519
494
|
this.notifyParent(run, "asked", { taskId: task.id, question });
|
|
520
495
|
// M3: a waiting child is not stalled — keep the watchdog fed until the reply.
|
|
521
496
|
const keepAlive = setInterval(() => this.liveChildren.get(`${run.id}:${task.id}`)?.touchWatchdog(), 30_000);
|
|
@@ -657,18 +632,19 @@ export class SubagentManager {
|
|
|
657
632
|
): Promise<void> {
|
|
658
633
|
if (TERMINAL.includes(task.status)) return; // canceled while queued
|
|
659
634
|
|
|
660
|
-
//
|
|
661
|
-
//
|
|
662
|
-
const
|
|
635
|
+
// Named agent file (`.agents/agents` etc.): body = system prompt, frontmatter
|
|
636
|
+
// model/tools fill gaps. Inline params always win over the file.
|
|
637
|
+
const file = resolveAgentFile(input.agent, task.cwd, getAgentDir());
|
|
638
|
+
const prompt = input.prompt?.trim() ?? file?.body;
|
|
663
639
|
const thinking = input.thinking;
|
|
664
|
-
const baseTools = input.tools ?? (input.write ? WRITE_TOOLS : READONLY_TOOLS);
|
|
640
|
+
const baseTools = input.tools ?? (input.write ? WRITE_TOOLS : (file?.tools ?? READONLY_TOOLS));
|
|
665
641
|
const tools = [...baseTools, ...(run.allowIntercom ? CHILD_TALK_TOOLS : [])];
|
|
666
642
|
|
|
667
643
|
// Model + thinking resolve against the pi model registry; a bad request
|
|
668
644
|
// fails the TASK with a helpful message, not the whole run.
|
|
669
645
|
let model: Model<Api> | undefined;
|
|
670
646
|
try {
|
|
671
|
-
model = resolveChildModel(ctx, input.model);
|
|
647
|
+
model = resolveChildModel(ctx, input.model ?? file?.model);
|
|
672
648
|
validateThinking(model, thinking);
|
|
673
649
|
} catch (err) {
|
|
674
650
|
this.updateTask(
|
|
@@ -890,7 +866,6 @@ export class SubagentManager {
|
|
|
890
866
|
id: newId("run"),
|
|
891
867
|
mode,
|
|
892
868
|
status: "queued",
|
|
893
|
-
background: params.background ?? this.autoBg,
|
|
894
869
|
allowIntercom: Boolean(params.allowIntercom),
|
|
895
870
|
notifyPerTask: params.notifyPerTask ?? true,
|
|
896
871
|
createdAt: Date.now(),
|
|
@@ -965,7 +940,7 @@ export class SubagentManager {
|
|
|
965
940
|
onUpdate,
|
|
966
941
|
);
|
|
967
942
|
if (task.status === "completed") outputs.set(task.id, task.finalText ?? "");
|
|
968
|
-
if (run.notifyPerTask &&
|
|
943
|
+
if (run.notifyPerTask && TERMINAL.includes(task.status)) {
|
|
969
944
|
this.notifyTask(run, task, task.status as "completed" | "failed" | "aborted");
|
|
970
945
|
}
|
|
971
946
|
},
|
|
@@ -1012,17 +987,7 @@ export class SubagentManager {
|
|
|
1012
987
|
this.persist(ctx);
|
|
1013
988
|
}
|
|
1014
989
|
|
|
1015
|
-
|
|
1016
|
-
params: SubagentParamsShape,
|
|
1017
|
-
signal: AbortSignal | undefined,
|
|
1018
|
-
onUpdate: ((partial: any) => void) | undefined,
|
|
1019
|
-
ctx: ExtensionContext,
|
|
1020
|
-
): Promise<RunDetails> {
|
|
1021
|
-
const { run, inputs } = this.createRun(params, ctx);
|
|
1022
|
-
await this.executeTasks(run, inputs, ctx, signal, onUpdate);
|
|
1023
|
-
return { run: cloneRun(run) };
|
|
1024
|
-
}
|
|
1025
|
-
|
|
990
|
+
/** Spawn a run that keeps executing after this call returns. Every run is background. */
|
|
1026
991
|
startInBackground(params: SubagentParamsShape, ctx: ExtensionContext): RunDetails {
|
|
1027
992
|
const { run, inputs } = this.createRun(params, ctx);
|
|
1028
993
|
void this.executeTasks(run, inputs, ctx, undefined, undefined)
|
|
@@ -1050,7 +1015,7 @@ export class SubagentManager {
|
|
|
1050
1015
|
this.notifyParent(run, "failed");
|
|
1051
1016
|
this.persist(ctx);
|
|
1052
1017
|
});
|
|
1053
|
-
return { run: cloneRun(run)
|
|
1018
|
+
return { run: cloneRun(run) };
|
|
1054
1019
|
}
|
|
1055
1020
|
|
|
1056
1021
|
/** Push a steering message into a live child's session. Returns false when unknown or not running. */
|
package/src/schemas.ts
CHANGED
|
@@ -55,17 +55,15 @@ export const SubagentParams = Type.Object({
|
|
|
55
55
|
"Per-task timeout, ms. Omit for no cap (default): tasks run until done, stalled, or user-aborted. Do not add arbitrary caps — only set when a hard bound is genuinely required.",
|
|
56
56
|
}),
|
|
57
57
|
),
|
|
58
|
-
|
|
58
|
+
autoAwait: Type.Optional(
|
|
59
59
|
Type.Boolean({
|
|
60
60
|
description:
|
|
61
|
-
"
|
|
62
|
-
default: true,
|
|
61
|
+
"Start the run in the background, then park this tool call until it finishes and return the final result inline (runId + summary in one response). Default false.",
|
|
63
62
|
}),
|
|
64
63
|
),
|
|
65
64
|
notifyPerTask: Type.Optional(
|
|
66
65
|
Type.Boolean({
|
|
67
|
-
description:
|
|
68
|
-
"Wake you (queued follow-up turn) as each task completes — background runs only, since blocking runs can't be woken mid-tool. Default true.",
|
|
66
|
+
description: "Wake you (queued follow-up turn) as each task completes. Default true.",
|
|
69
67
|
default: true,
|
|
70
68
|
}),
|
|
71
69
|
),
|
package/src/types.ts
CHANGED
|
@@ -47,7 +47,6 @@ export interface RunSnapshot {
|
|
|
47
47
|
id: string;
|
|
48
48
|
mode: RunMode;
|
|
49
49
|
status: RunStatus;
|
|
50
|
-
background: boolean;
|
|
51
50
|
allowIntercom: boolean;
|
|
52
51
|
notifyPerTask: boolean;
|
|
53
52
|
createdAt: number;
|
|
@@ -62,7 +61,6 @@ export interface RunSnapshot {
|
|
|
62
61
|
|
|
63
62
|
export interface RunDetails {
|
|
64
63
|
run: RunSnapshot;
|
|
65
|
-
background?: boolean;
|
|
66
64
|
}
|
|
67
65
|
|
|
68
66
|
export interface PendingReply {
|