@arhen/pi-core-subagent 1.3.64 → 1.3.66
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 +14 -11
- package/package.json +1 -1
- package/src/child.ts +61 -10
- package/src/config.ts +60 -0
- package/src/format.ts +19 -5
- package/src/index.ts +46 -14
- package/src/manager.ts +723 -65
- package/src/notification-state.ts +30 -0
- package/src/presentation.ts +30 -28
- package/src/types.ts +10 -0
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# @arhen/pi-core-subagent
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@arhen/pi-core-subagent)
|
|
4
|
-
[](https://github.com/arhen/pi-extensions/actions/workflows/pi-core-subagent.yml)
|
|
5
5
|
[](./LICENSE)
|
|
6
6
|
[](https://github.com/earendil-works/pi)
|
|
7
7
|
|
|
@@ -82,7 +82,7 @@ The dotted arrows are the whole point: a child may burn 200k tokens reading file
|
|
|
82
82
|
|
|
83
83
|
## Usage — the leader invents the agents
|
|
84
84
|
|
|
85
|
-
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.
|
|
85
|
+
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. Any model that is not the session model is preflight-probed; if the provider rejects it, the task falls back to the session model and the result carries a `Model:` note.
|
|
86
86
|
|
|
87
87
|
```json
|
|
88
88
|
{
|
|
@@ -133,7 +133,7 @@ You are a strict API reviewer. Check auth, rate limiting, and error handling. Ci
|
|
|
133
133
|
1. `.agents/agents/` then `.claude/agents/` then `.pi/agents/` in each directory from the task `cwd` up to the filesystem root (nearest ancestor wins).
|
|
134
134
|
2. Home: `~/.agents/agents/` (single source) → `~/.claude/agents/` → `~/.pi/agents/`.
|
|
135
135
|
|
|
136
|
-
Within a directory the file with the highest description-overlap score wins (≥2 shared meaningful tokens). A file `model` is validated against the pi model registry (unknown model fails the task with a catalog message). Files without a `description` frontmatter never match.
|
|
136
|
+
Within a directory the file with the highest description-overlap score wins (≥2 shared meaningful tokens and ≥40% coverage of the shorter token set). A file `model` is validated against the pi model registry (unknown model fails the task with a catalog message). File bodies are capped at 64,000 characters. Files without a `description` frontmatter never match.
|
|
137
137
|
|
|
138
138
|
## Worktree isolation (write agents)
|
|
139
139
|
|
|
@@ -184,7 +184,6 @@ The call line renders the graph in §2 notation as the model types it:
|
|
|
184
184
|
|
|
185
185
|
```
|
|
186
186
|
subagent graph 3
|
|
187
|
-
6 at a time
|
|
188
187
|
wave1[api ∥ db] → gate → wave2[doc]
|
|
189
188
|
api api-mapper Map every route in src/api/
|
|
190
189
|
db db-mapper Map the schema in src/db/
|
|
@@ -292,7 +291,7 @@ Background (default) + intercom — the run returns a runId immediately; you sta
|
|
|
292
291
|
|---|---|
|
|
293
292
|
| `subagent_models` | list the models a subagent task may name, each with the exact `model` value, thinking levels the runtime honors, context window, and catalog price; scoped to the session's enabled set when scoping is configured, else the full available catalogue (the output says which) |
|
|
294
293
|
| `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; children always carry talk tools (ask/notify/mailbox); `notifyPerTask` (default true) wakes you as each task completes |
|
|
295
|
-
| `subagent_status` | live per-task snapshot (non-blocking), including each child's session file path |
|
|
294
|
+
| `subagent_status` | live per-task snapshot (non-blocking), including each child's session file path; call it once right after spawning — a child that died on spawn is invisible until far later otherwise |
|
|
296
295
|
| `subagent_result` | full output of a run or one task |
|
|
297
296
|
| `await_subagent` | block until a run finishes (optional `timeoutMs`) |
|
|
298
297
|
| `reply_subagent` | answer a child's `ask_parent` question |
|
|
@@ -302,20 +301,20 @@ Background (default) + intercom — the run returns a runId immediately; you sta
|
|
|
302
301
|
|
|
303
302
|
### Per-task fields
|
|
304
303
|
|
|
305
|
-
`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`; omitted → the leader's current session model, a matched agent file's frontmatter wins), `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`, `concurrency
|
|
304
|
+
`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`; omitted → the leader's current session model, a matched agent file's frontmatter wins), `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`, `concurrency` (default 3, max 8). A call carries at most 16 tasks.
|
|
306
305
|
|
|
307
306
|
### Child talk tools (always on)
|
|
308
307
|
|
|
309
308
|
| Tool | Meaning |
|
|
310
309
|
|---|---|
|
|
311
310
|
| `ask_parent` | blocking question to the leader; delivered mid-turn as a **steering** message labelled `[URGENT]` or `[not urgent]`, parent answers via `reply_subagent` |
|
|
312
|
-
| `notify_parent` | one-way message to the leader |
|
|
311
|
+
| `notify_parent` | one-way message to the leader; identical updates coalesce, `final: true` declares the complete report |
|
|
313
312
|
| `send_agent_message` | message to a sibling subagent's mailbox (`to` = its task id, or `"leader"`) |
|
|
314
313
|
| `poll_agent_messages` | drain this subagent's mailbox |
|
|
315
314
|
|
|
316
315
|
> **Intercom anti-deadlock:** children are told to never block indefinitely on intercom replies — an unanswered `ask_parent` times out after 10 minutes (the child is told to proceed with best judgment), and sibling polls are capped (~5 tries) with the same fallback. Gated siblings (later waves) may not be running yet — waiting on them is the top stall cause, so children are instructed not to.
|
|
317
316
|
|
|
318
|
-
> **Ask urgency:** `ask_parent` takes `urgent` (default `false`). Both variants steer into the leader's current turn so the question is never deferred to the end of a long turn. `[URGENT]` tells the leader to answer before its next step; `[not urgent]` tells it that the child keeps waiting, so it may finish its current step first. Failures steer for the same reason
|
|
317
|
+
> **Ask urgency:** `ask_parent` takes `urgent` (default `false`). Both variants steer into the leader's current turn so the question is never deferred to the end of a long turn. `[URGENT]` tells the leader to answer before its next step; `[not urgent]` tells it that the child keeps waiting, so it may finish its current step first. Failures steer for the same reason. Completions, aborts, and informational updates are held in an extension outbox and delivered as follow-ups when the leader settles; a receipt confirms against the finalized leader transcript, so a report the leader already consumed (or an awaited result that already rendered the outcome) suppresses its redundant queued notice. See [docs/notification-policy.md](docs/notification-policy.md).
|
|
319
318
|
|
|
320
319
|
## Tool exposure and opt-in codemode
|
|
321
320
|
|
|
@@ -328,9 +327,12 @@ still applies Pi\'s global script-only policy; this extension does not override
|
|
|
328
327
|
- `/subagents mode codemode` — explicitly select script-based routing (direct fallback when unavailable).
|
|
329
328
|
- `/subagents mode direct` — return to legacy direct behavior.
|
|
330
329
|
|
|
331
|
-
Choice is stored
|
|
332
|
-
|
|
333
|
-
|
|
330
|
+
Choice is stored globally in `~/.pi/agent/subagents-config.json` (the same file as `auto-limit`,
|
|
331
|
+
written with a merge, never a clobber) and applies to every session — reload, resume, fork and tree
|
|
332
|
+
navigation included. Sessions that chose a mode before the preference moved into that file keep their
|
|
333
|
+
branch entry only while the config has no `mode`; the built-in default is still `direct`. Published
|
|
334
|
+
1.3.63 defaulted to `auto`: this compatibility correction requires the corrected source/package, not
|
|
335
|
+
a retroactive change to that npm release.
|
|
334
336
|
|
|
335
337
|
## Commands
|
|
336
338
|
|
|
@@ -375,6 +377,7 @@ The extension has no multiplexer integration and does not want one: it exposes t
|
|
|
375
377
|
## Context budget
|
|
376
378
|
|
|
377
379
|
- Parent tools: 9 schemas with short descriptions. **No catalog, no context hook** — nothing injected per request.
|
|
380
|
+
- The widget shows live work only: settled runs are pruned from it and stay reachable through `subagent_status` / `subagent_result`.
|
|
378
381
|
- Background completion: 3-line notice. Full text only via `subagent_result`.
|
|
379
382
|
- Children: isolated sessions; talk tools always injected; each child's prompt states its own task id and its siblings' so mailbox addressing works. Model resolution: explicit `provider/model-id` or bare id via the pi model registry → the parent's current model → settings default. Thinking levels validated against the resolved model's `thinkingLevelMap`; `subagent_models` lists the levels the runtime honors when you need a safe set.
|
|
380
383
|
|
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.66",
|
|
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/child.ts
CHANGED
|
@@ -5,10 +5,17 @@ import type { MailboxMessage } from "./mailbox.ts";
|
|
|
5
5
|
|
|
6
6
|
export const CHILD_TALK_TOOLS = ["ask_parent", "notify_parent", "send_agent_message", "poll_agent_messages"] as const;
|
|
7
7
|
|
|
8
|
+
type ParentNotifyHandler = (
|
|
9
|
+
taskId: string,
|
|
10
|
+
message: string,
|
|
11
|
+
level: "info" | "warning" | "error",
|
|
12
|
+
final?: boolean,
|
|
13
|
+
) => boolean;
|
|
14
|
+
|
|
8
15
|
export interface ChildHandlers {
|
|
9
16
|
onAskParent(taskId: string, question: string, urgent: boolean): Promise<string>;
|
|
10
|
-
onNotifyParent
|
|
11
|
-
onSendMessage(taskId: string, to: string, text: string): boolean;
|
|
17
|
+
onNotifyParent: ParentNotifyHandler | ((...args: Parameters<ParentNotifyHandler>) => void);
|
|
18
|
+
onSendMessage(taskId: string, to: string, text: string, final?: boolean): boolean;
|
|
12
19
|
onPollMailbox(taskId: string): MailboxMessage[];
|
|
13
20
|
}
|
|
14
21
|
|
|
@@ -45,16 +52,37 @@ export function createChildTools(taskId: string, handlers: ChildHandlers): ToolD
|
|
|
45
52
|
name: "notify_parent",
|
|
46
53
|
label: "Notify Parent",
|
|
47
54
|
description:
|
|
48
|
-
"Send a non-blocking
|
|
55
|
+
"Send a non-blocking update to the parent. Identical updates send once per task attempt. Set final:true only for your complete final report, as your last tool call; repeat that exact report as your final answer. A received final report suppresses redundant success follow-ups, not new results or failures.",
|
|
49
56
|
promptSnippet: "Send the parent a non-blocking update or finding.",
|
|
50
57
|
parameters: Type.Object({
|
|
51
58
|
message: Type.String({ description: "The message content for the parent" }),
|
|
52
59
|
level: Type.Optional(StringEnum(["info", "warning", "error"] as const, { default: "info" })),
|
|
60
|
+
final: Type.Optional(
|
|
61
|
+
Type.Boolean({
|
|
62
|
+
description: "Complete final report; no further tool work. Repeat this exact message as your final answer.",
|
|
63
|
+
default: false,
|
|
64
|
+
}),
|
|
65
|
+
),
|
|
53
66
|
}),
|
|
54
67
|
async execute(_toolCallId, params) {
|
|
55
|
-
const { message, level } = params as {
|
|
56
|
-
|
|
57
|
-
|
|
68
|
+
const { message, level, final } = params as {
|
|
69
|
+
message: string;
|
|
70
|
+
level?: "info" | "warning" | "error";
|
|
71
|
+
final?: boolean;
|
|
72
|
+
};
|
|
73
|
+
const submitted = handlers.onNotifyParent(taskId, message, level ?? "info", final === true) !== false;
|
|
74
|
+
return {
|
|
75
|
+
content: [
|
|
76
|
+
{
|
|
77
|
+
type: "text" as const,
|
|
78
|
+
text: submitted
|
|
79
|
+
? "Submitted to parent (or already submitted). Receipt is tracked separately."
|
|
80
|
+
: "Parent notification could not be submitted; retry or include it in your final answer.",
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
isError: !submitted,
|
|
84
|
+
details: {},
|
|
85
|
+
};
|
|
58
86
|
},
|
|
59
87
|
},
|
|
60
88
|
{
|
|
@@ -68,19 +96,42 @@ export function createChildTools(taskId: string, handlers: ChildHandlers): ToolD
|
|
|
68
96
|
description: "Target task id of another subagent in this run (e.g. task_2), or 'leader' for the parent agent",
|
|
69
97
|
}),
|
|
70
98
|
message: Type.String({ description: "Short message content (keep under ~500 chars)" }),
|
|
99
|
+
final: Type.Optional(
|
|
100
|
+
Type.Boolean({
|
|
101
|
+
description: "For to:leader only: complete final report, same contract as notify_parent final:true.",
|
|
102
|
+
default: false,
|
|
103
|
+
}),
|
|
104
|
+
),
|
|
71
105
|
}),
|
|
72
106
|
async execute(_toolCallId, params) {
|
|
73
|
-
const { to, message } = params as { to: string; message: string };
|
|
74
|
-
if (!handlers.onSendMessage(taskId, to, message)) {
|
|
107
|
+
const { to, message, final } = params as { to: string; message: string; final?: boolean };
|
|
108
|
+
if (!handlers.onSendMessage(taskId, to, message, final === true)) {
|
|
75
109
|
return {
|
|
76
110
|
content: [
|
|
77
|
-
{
|
|
111
|
+
{
|
|
112
|
+
type: "text" as const,
|
|
113
|
+
text:
|
|
114
|
+
to === "leader"
|
|
115
|
+
? "Parent notification could not be submitted; retry or include it in your final answer."
|
|
116
|
+
: `Unknown target '${to}'. Use a sibling task id in this run or 'leader'.`,
|
|
117
|
+
},
|
|
78
118
|
],
|
|
79
119
|
isError: true,
|
|
80
120
|
details: {},
|
|
81
121
|
};
|
|
82
122
|
}
|
|
83
|
-
return {
|
|
123
|
+
return {
|
|
124
|
+
content: [
|
|
125
|
+
{
|
|
126
|
+
type: "text" as const,
|
|
127
|
+
text:
|
|
128
|
+
to === "leader"
|
|
129
|
+
? "Submitted to leader (or already submitted). Receipt is tracked separately."
|
|
130
|
+
: "Sent.",
|
|
131
|
+
},
|
|
132
|
+
],
|
|
133
|
+
details: {},
|
|
134
|
+
};
|
|
84
135
|
},
|
|
85
136
|
},
|
|
86
137
|
{
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { writeFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Shared user config for the extension: exposure mode plus the auto-limit ceiling. One file, so a
|
|
8
|
+
* write of one key must never drop the other — every writer merges through `writeSubagentConfig`.
|
|
9
|
+
*/
|
|
10
|
+
export const SUBAGENT_CONFIG_FILENAME = "subagents-config.json";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Presentation preference for the subagent toolset, stored globally in `subagents-config.json`.
|
|
14
|
+
* - auto: native codemode presentation when the codemode tool is active, direct otherwise.
|
|
15
|
+
* - direct (default): legacy native declarations and active-tool script callability.
|
|
16
|
+
* - codemode: subagent tools are callable from codemode scripts but not declared or listed.
|
|
17
|
+
*/
|
|
18
|
+
export type SubagentMode = "auto" | "direct" | "codemode";
|
|
19
|
+
|
|
20
|
+
export const DEFAULT_SUBAGENT_MODE: SubagentMode = "direct";
|
|
21
|
+
|
|
22
|
+
export function isSubagentMode(value: unknown): value is SubagentMode {
|
|
23
|
+
return value === "auto" || value === "direct" || value === "codemode";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Resolved per call, so a test or `PI_CODING_AGENT_DIR` override applies to the running process. */
|
|
27
|
+
export function subagentConfigPath(): string {
|
|
28
|
+
return join(getAgentDir(), SUBAGENT_CONFIG_FILENAME);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Tolerant read: a missing, unreadable or non-object file degrades to an empty object, so a config
|
|
33
|
+
* typo can neither break delegation nor hide the built-in defaults.
|
|
34
|
+
*/
|
|
35
|
+
export function readSubagentConfig(path = subagentConfigPath()): Record<string, unknown> {
|
|
36
|
+
try {
|
|
37
|
+
const raw: unknown = JSON.parse(readFileSync(path, "utf8"));
|
|
38
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return {};
|
|
39
|
+
return raw as Record<string, unknown>;
|
|
40
|
+
} catch {
|
|
41
|
+
return {};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The stored exposure mode, or undefined when the file has no usable one. */
|
|
46
|
+
export function readStoredMode(path = subagentConfigPath()): SubagentMode | undefined {
|
|
47
|
+
const mode = readSubagentConfig(path).mode;
|
|
48
|
+
return isSubagentMode(mode) ? mode : undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let writeQueue: Promise<unknown> = Promise.resolve();
|
|
52
|
+
|
|
53
|
+
/** Read-modify-write on a single queue, so concurrent writers of different keys cannot clobber each other. */
|
|
54
|
+
export function writeSubagentConfig(patch: Record<string, unknown>, path = subagentConfigPath()): Promise<void> {
|
|
55
|
+
const write = writeQueue.then(async () => {
|
|
56
|
+
await writeFile(path, `${JSON.stringify({ ...readSubagentConfig(path), ...patch }, null, 2)}\n`);
|
|
57
|
+
});
|
|
58
|
+
writeQueue = write.catch(() => {});
|
|
59
|
+
return write;
|
|
60
|
+
}
|
package/src/format.ts
CHANGED
|
@@ -208,6 +208,9 @@ function worktreeLine(task: TaskSnapshot, siblings?: TaskSnapshot[]): string {
|
|
|
208
208
|
}
|
|
209
209
|
|
|
210
210
|
export function makeSummary(run: RunSnapshot): string {
|
|
211
|
+
return makeSummaryWithCoverage(run).text;
|
|
212
|
+
}
|
|
213
|
+
export function makeSummaryWithCoverage(run: RunSnapshot): { text: string; coveredTaskIds: Set<string> } {
|
|
211
214
|
const succeeded = run.tasks.filter((t) => t.status === "completed").length;
|
|
212
215
|
const failed = run.tasks.filter((t) => t.status === "failed").length;
|
|
213
216
|
const aborted = run.tasks.filter((t) => t.status === "aborted").length;
|
|
@@ -217,6 +220,7 @@ export function makeSummary(run: RunSnapshot): string {
|
|
|
217
220
|
];
|
|
218
221
|
const usage = formatUsage(run.aggregateUsage);
|
|
219
222
|
if (usage) lines.push(`Usage: ${usage}`);
|
|
223
|
+
const taskEnds = new Map<string, number>();
|
|
220
224
|
for (const task of run.tasks) {
|
|
221
225
|
const edge = task.needs?.length ? ` (${task.id}, needs ${task.needs.join(", ")})` : ` (${task.id})`;
|
|
222
226
|
const fileNote = task.agentFile ? ` [${task.agentFile}]` : "";
|
|
@@ -225,9 +229,14 @@ export function makeSummary(run: RunSnapshot): string {
|
|
|
225
229
|
lines.push(
|
|
226
230
|
`\n## ${task.agent}${edge}${fileNote} ${statusIcon(task.status)}${swap}${tools}${task.error ? `\nError: ${task.error}` : `\n${truncateText(task.finalText || "(no output)")}`}${worktreeLine(task, run.tasks)}`,
|
|
227
231
|
);
|
|
232
|
+
taskEnds.set(task.id, lines.join("\n").length);
|
|
228
233
|
}
|
|
229
234
|
|
|
230
|
-
|
|
235
|
+
const fullText = lines.join("\n");
|
|
236
|
+
const text = truncateText(fullText);
|
|
237
|
+
let prefix = 0;
|
|
238
|
+
while (prefix < fullText.length && prefix < text.length && fullText[prefix] === text[prefix]) prefix++;
|
|
239
|
+
return { text, coveredTaskIds: new Set([...taskEnds].filter(([, end]) => end <= prefix).map(([id]) => id)) };
|
|
231
240
|
}
|
|
232
241
|
export function isStartupFailure(task: TaskSnapshot, kind: string): boolean {
|
|
233
242
|
return kind === "failed" && !task.finalText?.trim();
|
|
@@ -244,7 +253,7 @@ export function makeTaskNotice(run: RunSnapshot, task: TaskSnapshot, kind: strin
|
|
|
244
253
|
const tools = task.toolsNote ? `\nTools: ${task.toolsNote}` : "";
|
|
245
254
|
return [
|
|
246
255
|
`Task ${task.agent} (${task.id}) ${kind} in run ${run.id}: ${detail}${wt}`,
|
|
247
|
-
`Goal: ${goal}${src}${swap}${tools}`,
|
|
256
|
+
`Goal: ${goal}${src}${swap}${tools}${task.worktreeError ? `\nWorktree: ${task.worktreeError}` : ""}`,
|
|
248
257
|
isStartupFailure(task, kind)
|
|
249
258
|
? "Never started — stop and diagnose before spawning anything else: a config-level error (model, plan, auth, agent file) fails identically on every respawn."
|
|
250
259
|
: kind === "completed"
|
|
@@ -252,6 +261,9 @@ export function makeTaskNotice(run: RunSnapshot, task: TaskSnapshot, kind: strin
|
|
|
252
261
|
: `Session file kept — resume_subagent({ runId: "${run.id}", taskId: "${task.id}", model?: ... }) revives it with full context. subagent_result for what it produced so far.`,
|
|
253
262
|
].join("\n");
|
|
254
263
|
}
|
|
264
|
+
export function makeTaskArtifactNotice(run: RunSnapshot, task: TaskSnapshot): string {
|
|
265
|
+
return `Task ${task.agent} (${task.id}) completed in run ${run.id}; result already delivered.${worktreeLine(task, run.tasks)}\nUse subagent_result({ runId: "${run.id}", taskId: "${task.id}" }) for artifacts.`;
|
|
266
|
+
}
|
|
255
267
|
export function makeAskNotice(
|
|
256
268
|
run: RunSnapshot,
|
|
257
269
|
extra: { taskId?: string; agent?: string; question?: string; urgent?: boolean },
|
|
@@ -263,12 +275,14 @@ export function makeAskNotice(
|
|
|
263
275
|
: `[not urgent] Subagent ${who} asks: ${extra.question ?? ""}\nIt waits while you keep working — finish your current step first if you want, then answer with ${reply}.`;
|
|
264
276
|
}
|
|
265
277
|
|
|
266
|
-
export function makeNotice(run: RunSnapshot, kind: string): string {
|
|
278
|
+
export function makeNotice(run: RunSnapshot, kind: string, tasks = run.tasks): string {
|
|
267
279
|
const lines = [
|
|
268
280
|
`Background subagent run ${run.id} ${kind}: ${run.tasks.filter((t) => t.status === "completed").length}/${run.tasks.length} succeeded.`,
|
|
269
281
|
];
|
|
270
|
-
for (const task of
|
|
271
|
-
lines.push(
|
|
282
|
+
for (const task of tasks) {
|
|
283
|
+
lines.push(
|
|
284
|
+
`- ${task.agent}: ${task.status}${task.error ? ` — ${truncateText(task.error, 200)}` : ""}${worktreeLine(task, run.tasks)}`,
|
|
285
|
+
);
|
|
272
286
|
}
|
|
273
287
|
lines.push(`Use subagent_result({ runId: "${run.id}" }) for full output.`);
|
|
274
288
|
return lines.join("\n");
|
package/src/index.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type { TSchema } from "typebox";
|
|
|
4
4
|
import {
|
|
5
5
|
compactLines,
|
|
6
6
|
formatUsage,
|
|
7
|
-
|
|
7
|
+
makeSummaryWithCoverage,
|
|
8
8
|
renderModelCatalog,
|
|
9
9
|
statusIcon,
|
|
10
10
|
taskLine,
|
|
@@ -29,6 +29,24 @@ import {
|
|
|
29
29
|
import { type ModelCatalog, type RunDetails, type RunSnapshot, TERMINAL } from "./types.ts";
|
|
30
30
|
import { cleanupMerged, ownerAlive, reapDeadWorktrees, repoRoot, sweepStale } from "./worktree.ts";
|
|
31
31
|
|
|
32
|
+
function unreportedIntercom(run: RunSnapshot, messages: ParkedMsg[], covered: ReadonlySet<string>): ParkedMsg[] {
|
|
33
|
+
return messages.filter((message) => {
|
|
34
|
+
const task = run.tasks.find((task) => task.id === message.taskId);
|
|
35
|
+
return (
|
|
36
|
+
!covered.has(message.taskId) ||
|
|
37
|
+
task?.status !== "completed" ||
|
|
38
|
+
!(
|
|
39
|
+
message.kind === "done" ||
|
|
40
|
+
(message.kind === "notify" && message.final && task.finalText?.trim() === message.text.trim())
|
|
41
|
+
)
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function intercomLine(message: ParkedMsg): string {
|
|
47
|
+
return `- [${message.kind === "notify" && message.final ? "final" : message.kind}] ${message.agent} (${message.taskId}): ${truncateText(message.text)}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
32
50
|
export default function (pi: ExtensionAPI) {
|
|
33
51
|
const manager = new SubagentManager(pi);
|
|
34
52
|
const definitions: AnyToolDefinition[] = [];
|
|
@@ -74,7 +92,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
74
92
|
};
|
|
75
93
|
pi.registerCommand("subagents", {
|
|
76
94
|
description:
|
|
77
|
-
"List subagent runs. `/subagents peek` opens the browsable pane; `/subagents mode [auto|direct|codemode]` reports or switches the tool exposure profile; `/subagents auto-limit on|off` toggles the 1 h default runtime ceiling (default off = 6 h).",
|
|
95
|
+
"List subagent runs. `/subagents peek` opens the browsable pane; `/subagents mode [auto|direct|codemode]` reports or switches the globally stored tool exposure profile; `/subagents auto-limit on|off` toggles the 1 h default runtime ceiling (default off = 6 h).",
|
|
78
96
|
handler: async (args, ctx) => {
|
|
79
97
|
const arg = String(args ?? "")
|
|
80
98
|
.trim()
|
|
@@ -127,10 +145,24 @@ export default function (pi: ExtensionAPI) {
|
|
|
127
145
|
pi.registerShortcut("ctrl+shift+a", { description: "Peek at running subagents", handler: openPeek });
|
|
128
146
|
|
|
129
147
|
pi.on("agent_start", (_event, ctx) => {
|
|
148
|
+
manager.noteAgentStart();
|
|
130
149
|
if (!manager.turnActivity && !manager.hasActiveRun()) manager.clearWidget(ctx);
|
|
131
150
|
manager.turnActivity = false;
|
|
132
151
|
});
|
|
133
152
|
|
|
153
|
+
pi.on("context", (_event, ctx) => {
|
|
154
|
+
manager.confirmLeaderMessages(ctx);
|
|
155
|
+
});
|
|
156
|
+
pi.on("agent_end", (_event, ctx) => {
|
|
157
|
+
manager.confirmLeaderMessages(ctx);
|
|
158
|
+
manager.scheduleNotificationFlush(ctx);
|
|
159
|
+
});
|
|
160
|
+
// Settlement is the last automatic boundary: anything still held (a busy flush stopped earlier)
|
|
161
|
+
// must be released here, and receipts confirmed against the finalized projection.
|
|
162
|
+
pi.on("agent_settled", (_event, ctx) => {
|
|
163
|
+
manager.confirmLeaderMessages(ctx);
|
|
164
|
+
manager.flushPendingNotifications(ctx);
|
|
165
|
+
});
|
|
134
166
|
pi.on("before_agent_start", () => {
|
|
135
167
|
presentation.sync();
|
|
136
168
|
});
|
|
@@ -226,13 +258,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
226
258
|
if (awaited.intercom.some((m) => m.kind === "ask")) break;
|
|
227
259
|
}
|
|
228
260
|
|
|
261
|
+
const summary = makeSummaryWithCoverage(run);
|
|
262
|
+
manager.markAwaitCoverage(run, summary.coveredTaskIds);
|
|
229
263
|
const asks = intercom.filter((m) => m.kind === "ask");
|
|
230
|
-
const heard =
|
|
264
|
+
const heard = unreportedIntercom(run, intercom, summary.coveredTaskIds).filter(
|
|
265
|
+
(message) => message.kind !== "ask",
|
|
266
|
+
);
|
|
231
267
|
const text = [
|
|
232
|
-
|
|
233
|
-
heard.length > 0
|
|
234
|
-
? `\nIntercom while waiting:\n${heard.map((m) => `- [${m.kind}] ${m.agent} (${m.taskId}): ${truncateText(m.text)}`).join("\n")}`
|
|
235
|
-
: "",
|
|
268
|
+
summary.text,
|
|
269
|
+
heard.length > 0 ? `\nIntercom while waiting:\n${heard.map(intercomLine).join("\n")}` : "",
|
|
236
270
|
asks.length > 0
|
|
237
271
|
? `\n${asks.length} child(ren) waiting for your answer:\n${asks
|
|
238
272
|
.map(
|
|
@@ -384,13 +418,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
384
418
|
if (!awaited) return { content: [{ type: "text", text: `Unknown runId: ${runId}` }], isError: true, details: {} };
|
|
385
419
|
const { run, intercom } = awaited;
|
|
386
420
|
if (!run) return { content: [{ type: "text", text: `Unknown runId: ${runId}` }], isError: true, details: {} };
|
|
387
|
-
const
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
: "";
|
|
393
|
-
return { content: [{ type: "text", text: makeSummary(run) + intercomText }], details: { run } };
|
|
421
|
+
const summary = makeSummaryWithCoverage(run);
|
|
422
|
+
manager.markAwaitCoverage(run, summary.coveredTaskIds);
|
|
423
|
+
const heard = unreportedIntercom(run, intercom, summary.coveredTaskIds);
|
|
424
|
+
const intercomText = heard.length > 0 ? `\n\nIntercom while waiting:\n${heard.map(intercomLine).join("\n")}` : "";
|
|
425
|
+
return { content: [{ type: "text", text: summary.text + intercomText }], details: { run } };
|
|
394
426
|
},
|
|
395
427
|
});
|
|
396
428
|
|