@arhen/pi-core-subagent 1.3.57 → 1.3.59
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 +3 -1
- package/package.json +1 -1
- package/src/child.ts +12 -4
- package/src/format.ts +18 -0
- package/src/index.ts +2 -1
- package/src/manager.ts +49 -31
package/README.md
CHANGED
|
@@ -305,13 +305,15 @@ Background (default) + intercom — the run returns a runId immediately; you sta
|
|
|
305
305
|
|
|
306
306
|
| Tool | Meaning |
|
|
307
307
|
|---|---|
|
|
308
|
-
| `ask_parent` | blocking question to the leader; parent answers via `reply_subagent` |
|
|
308
|
+
| `ask_parent` | blocking question to the leader; delivered mid-turn as a **steering** message labelled `[URGENT]` or `[not urgent]`, parent answers via `reply_subagent` |
|
|
309
309
|
| `notify_parent` | one-way message to the leader |
|
|
310
310
|
| `send_agent_message` | message to a sibling subagent's mailbox (`to` = its task id, or `"leader"`) |
|
|
311
311
|
| `poll_agent_messages` | drain this subagent's mailbox |
|
|
312
312
|
|
|
313
313
|
> **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.
|
|
314
314
|
|
|
315
|
+
> **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 and aborts queue as follow-ups.
|
|
316
|
+
|
|
315
317
|
## Commands
|
|
316
318
|
|
|
317
319
|
- `/subagents` — list runs; `/subagents peek` (or `ctrl+shift+a`) — browsable pane
|
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.59",
|
|
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
|
@@ -6,7 +6,7 @@ import type { MailboxMessage } from "./mailbox.ts";
|
|
|
6
6
|
export const CHILD_TALK_TOOLS = ["ask_parent", "notify_parent", "send_agent_message", "poll_agent_messages"] as const;
|
|
7
7
|
|
|
8
8
|
export interface ChildHandlers {
|
|
9
|
-
onAskParent(taskId: string, question: string): Promise<string>;
|
|
9
|
+
onAskParent(taskId: string, question: string, urgent: boolean): Promise<string>;
|
|
10
10
|
onNotifyParent(taskId: string, message: string, level: "info" | "warning" | "error"): void;
|
|
11
11
|
onSendMessage(taskId: string, to: string, text: string): boolean;
|
|
12
12
|
onPollMailbox(taskId: string): MailboxMessage[];
|
|
@@ -18,18 +18,26 @@ export function createChildTools(taskId: string, handlers: ChildHandlers): ToolD
|
|
|
18
18
|
name: "ask_parent",
|
|
19
19
|
label: "Ask Parent",
|
|
20
20
|
description:
|
|
21
|
-
"Ask the parent agent a clarifying question and BLOCK until it replies (10 min cap — then proceed with best judgment). Use sparingly — only when you truly cannot proceed without information only the parent has. Prefer figuring it out yourself.",
|
|
21
|
+
"Ask the parent agent a clarifying question and BLOCK until it replies (10 min cap — then proceed with best judgment). Use sparingly — only when you truly cannot proceed without information only the parent has. Prefer figuring it out yourself. The parent sees the question mid-turn: set urgent when you cannot continue until it answers, leave it unset when the parent may finish its current step first.",
|
|
22
22
|
promptSnippet: "Ask the parent agent a question when truly blocked.",
|
|
23
23
|
promptGuidelines: [
|
|
24
24
|
"Use ask_parent only as a last resort when blocked on information only the parent has.",
|
|
25
25
|
"Ask one focused question at a time. The parent's reply resumes your work.",
|
|
26
|
+
"Set urgent: true only when you cannot keep working while waiting; otherwise the parent is told it may answer after its current step.",
|
|
26
27
|
],
|
|
27
28
|
parameters: Type.Object({
|
|
28
29
|
question: Type.String({ description: "A single, focused question for the parent agent" }),
|
|
30
|
+
urgent: Type.Optional(
|
|
31
|
+
Type.Boolean({
|
|
32
|
+
description:
|
|
33
|
+
"True when nothing else can proceed until the parent answers — it is told to stop and reply now",
|
|
34
|
+
default: false,
|
|
35
|
+
}),
|
|
36
|
+
),
|
|
29
37
|
}),
|
|
30
38
|
async execute(_toolCallId, params) {
|
|
31
|
-
const { question } = params as { question: string };
|
|
32
|
-
const answer = await handlers.onAskParent(taskId, question);
|
|
39
|
+
const { question, urgent } = params as { question: string; urgent?: boolean };
|
|
40
|
+
const answer = await handlers.onAskParent(taskId, question, urgent === true);
|
|
33
41
|
return { content: [{ type: "text" as const, text: answer || "(parent gave no answer)" }], details: {} };
|
|
34
42
|
},
|
|
35
43
|
},
|
package/src/format.ts
CHANGED
|
@@ -111,10 +111,17 @@ export function isTalking(task: TaskSnapshot): boolean {
|
|
|
111
111
|
return TALK_TOOLS.some((t) => a.startsWith(t));
|
|
112
112
|
}
|
|
113
113
|
let pulsePhase = 0;
|
|
114
|
+
const NOTE_CAP = 240;
|
|
115
|
+
function noteSnippet(note: string): string {
|
|
116
|
+
return note.length > NOTE_CAP ? `${note.slice(0, NOTE_CAP)}…` : note;
|
|
117
|
+
}
|
|
114
118
|
export function compactLines(run: RunSnapshot): string[] {
|
|
115
119
|
const lines: string[] = [];
|
|
116
120
|
for (const task of run.tasks.slice(0, MAX_TASKS)) {
|
|
117
121
|
lines.push(taskLine(task));
|
|
122
|
+
// a swapped model or toolset changes what the task did, so it cannot stay out of the status view
|
|
123
|
+
if (task.modelNote) lines.push(` ↳ Model: ${noteSnippet(task.modelNote)}`);
|
|
124
|
+
if (task.toolsNote) lines.push(` ↳ Tools: ${noteSnippet(task.toolsNote)}`);
|
|
118
125
|
}
|
|
119
126
|
if (run.tasks.length > MAX_TASKS) lines.push(`… +${run.tasks.length - MAX_TASKS} more`);
|
|
120
127
|
return lines;
|
|
@@ -243,6 +250,17 @@ export function makeTaskNotice(run: RunSnapshot, task: TaskSnapshot, kind: strin
|
|
|
243
250
|
: `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.`,
|
|
244
251
|
].join("\n");
|
|
245
252
|
}
|
|
253
|
+
export function makeAskNotice(
|
|
254
|
+
run: RunSnapshot,
|
|
255
|
+
extra: { taskId?: string; agent?: string; question?: string; urgent?: boolean },
|
|
256
|
+
): string {
|
|
257
|
+
const who = extra.agent ? `${extra.agent} (${extra.taskId ?? "task"})` : (extra.taskId ?? "a subagent");
|
|
258
|
+
const reply = `reply_subagent(runId: "${run.id}", taskId: "${extra.taskId ?? ""}", message: ...)`;
|
|
259
|
+
return extra.urgent
|
|
260
|
+
? `[URGENT] Subagent ${who} is blocked and cannot continue until you answer: ${extra.question ?? ""}\nAnswer now, before your next step, with ${reply}.`
|
|
261
|
+
: `[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}.`;
|
|
262
|
+
}
|
|
263
|
+
|
|
246
264
|
export function makeNotice(run: RunSnapshot, kind: string): string {
|
|
247
265
|
const lines = [
|
|
248
266
|
`Background subagent run ${run.id} ${kind}: ${run.tasks.filter((t) => t.status === "completed").length}/${run.tasks.length} succeeded.`,
|
package/src/index.ts
CHANGED
|
@@ -299,7 +299,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
299
299
|
? `\nApplied IN PLACE (no branch) — ${t.isolationReason ?? "worktree unavailable"}. The changes are already in your working tree.`
|
|
300
300
|
: "";
|
|
301
301
|
const wtErr = t.worktreeError ? `\nWorktree: ${t.worktreeError}` : "";
|
|
302
|
-
|
|
302
|
+
const modelNote = t.modelNote ? `\nModel: ${t.modelNote}` : "";
|
|
303
|
+
return `\n## ${t.agent} ${statusIcon(t.status)}\nGoal: ${truncateText(t.task, 300)}\n${t.error ? `Error: ${t.error}` : t.finalText || "(no output yet)"}${wt}${wtErr}${modelNote}\n${formatUsage(t.usage)}`;
|
|
303
304
|
}),
|
|
304
305
|
].join("\n");
|
|
305
306
|
return { content: [{ type: "text", text: truncateText(text) }], details: { run: cloneRun(run) } };
|
package/src/manager.ts
CHANGED
|
@@ -2,7 +2,13 @@ import { existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, rmSync
|
|
|
2
2
|
import { rename, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import { basename, dirname, join, relative, sep } from "node:path";
|
|
4
4
|
import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
5
|
-
import
|
|
5
|
+
import {
|
|
6
|
+
type Api,
|
|
7
|
+
type AssistantMessage,
|
|
8
|
+
clampThinkingLevel,
|
|
9
|
+
getSupportedThinkingLevels,
|
|
10
|
+
type Model,
|
|
11
|
+
} from "@earendil-works/pi-ai";
|
|
6
12
|
import {
|
|
7
13
|
type AgentSessionEvent,
|
|
8
14
|
createAgentSession,
|
|
@@ -23,6 +29,7 @@ import {
|
|
|
23
29
|
getFirstText,
|
|
24
30
|
isStartupFailure,
|
|
25
31
|
isTalking,
|
|
32
|
+
makeAskNotice,
|
|
26
33
|
makeNotice,
|
|
27
34
|
makeTaskNotice,
|
|
28
35
|
SubagentsWidget,
|
|
@@ -160,21 +167,19 @@ export function resolveChildModel(ctx: ExtensionContext, explicit: string | unde
|
|
|
160
167
|
const PROBE_THINKING_LEVELS: ThinkingLevel[] = ["low", "minimal", "medium", "high", "xhigh", "max"];
|
|
161
168
|
|
|
162
169
|
/**
|
|
163
|
-
* Thinking level for the usability probe:
|
|
164
|
-
* the model accepts
|
|
165
|
-
*
|
|
166
|
-
* make every preflight fail and fall
|
|
170
|
+
* Thinking level for the usability probe: exactly what the child session will send — the clamped
|
|
171
|
+
* requested level, or the cheapest the model accepts when none was requested. Probing without a
|
|
172
|
+
* level makes adaptive-thinking providers reject the request (9router claude models answer
|
|
173
|
+
* "thinking.type.disabled is not supported"), which used to make every preflight fail and fall
|
|
174
|
+
* back to the session model.
|
|
167
175
|
*/
|
|
168
176
|
function probeThinking(model: Model<Api>, thinking?: string): ThinkingLevel | undefined {
|
|
169
|
-
if (thinking && thinking !== "off") return thinking as ThinkingLevel;
|
|
170
177
|
if (!model.reasoning) return undefined;
|
|
171
|
-
const
|
|
172
|
-
if (
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
}
|
|
177
|
-
return undefined;
|
|
178
|
+
const supported = getSupportedThinkingLevels(model);
|
|
179
|
+
if (thinking && thinking !== "off") return clampThinkingLevel(model, thinking as ThinkingLevel);
|
|
180
|
+
// the child clamps an unsupported "off" up to its cheapest level, so probe that instead
|
|
181
|
+
if (thinking === "off") return supported.includes("off") ? undefined : supported[0];
|
|
182
|
+
return PROBE_THINKING_LEVELS.find((level) => supported.includes(level));
|
|
178
183
|
}
|
|
179
184
|
|
|
180
185
|
async function probeModel(
|
|
@@ -325,10 +330,10 @@ export class SubagentManager {
|
|
|
325
330
|
return false;
|
|
326
331
|
}
|
|
327
332
|
|
|
328
|
-
clearWidget(ctx
|
|
333
|
+
clearWidget(ctx?: ExtensionContext): void {
|
|
329
334
|
this.widgetRuns = [];
|
|
330
335
|
this.widgetTui = null;
|
|
331
|
-
if (ctx
|
|
336
|
+
if (ctx?.hasUI) {
|
|
332
337
|
try {
|
|
333
338
|
ctx.ui.setWidget("subagents", undefined);
|
|
334
339
|
} catch {}
|
|
@@ -490,24 +495,32 @@ export class SubagentManager {
|
|
|
490
495
|
private notifyParent(
|
|
491
496
|
run: RunSnapshot,
|
|
492
497
|
kind: "completed" | "failed" | "aborted" | "asked",
|
|
493
|
-
extra?: { taskId?: string; question?: string },
|
|
498
|
+
extra?: { taskId?: string; agent?: string; question?: string; urgent?: boolean },
|
|
494
499
|
): void {
|
|
495
500
|
if (kind !== "asked" && run.awaited) return;
|
|
496
501
|
// single-task completed run: the task notice already said everything (failure paths may not have notified per-task)
|
|
497
502
|
if (kind === "completed" && run.tasks.length === 1 && run.notifyPerTask) return;
|
|
498
|
-
const body =
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
503
|
+
const body = kind === "asked" ? makeAskNotice(run, extra ?? {}) : makeNotice(run, kind);
|
|
504
|
+
// asks steer: the leader sees the question during its turn and the urgent flag tells it whether to
|
|
505
|
+
// answer now or after the current step. Failures steer for the same reason — a broken result must
|
|
506
|
+
// not be consumed. Completions and aborts queue as follow-ups.
|
|
502
507
|
try {
|
|
503
|
-
this.pi.sendUserMessage(body, {
|
|
508
|
+
this.pi.sendUserMessage(body, {
|
|
509
|
+
deliverAs: kind === "completed" || kind === "aborted" ? "followUp" : "steer",
|
|
510
|
+
});
|
|
504
511
|
} catch {}
|
|
505
|
-
this.emit("subagent:notification", { runId: run.id, kind, body });
|
|
512
|
+
this.emit("subagent:notification", { runId: run.id, taskId: extra?.taskId, kind, body });
|
|
506
513
|
}
|
|
507
514
|
|
|
508
515
|
private widgetTui: TUI | null = null;
|
|
516
|
+
|
|
517
|
+
/** Settled runs are history (subagent_status/result still reach them) — the widget shows live work only. */
|
|
518
|
+
private pruneSettledWidget(): void {
|
|
519
|
+
this.widgetRuns = this.widgetRuns.filter((r) => !TERMINAL.includes(r.status));
|
|
520
|
+
}
|
|
509
521
|
private upsertWidgetRun(run: RunSnapshot | undefined): void {
|
|
510
|
-
|
|
522
|
+
this.pruneSettledWidget();
|
|
523
|
+
if (!run || TERMINAL.includes(run.status)) return;
|
|
511
524
|
const idx = this.widgetRuns.findIndex((r) => r.id === run.id);
|
|
512
525
|
if (idx >= 0) this.widgetRuns[idx] = run;
|
|
513
526
|
else this.widgetRuns.push(run);
|
|
@@ -547,12 +560,17 @@ export class SubagentManager {
|
|
|
547
560
|
this.widgetTimers.delete(run.id);
|
|
548
561
|
}
|
|
549
562
|
}
|
|
550
|
-
|
|
551
|
-
if (
|
|
552
|
-
this.
|
|
553
|
-
|
|
563
|
+
this.pruneSettledWidget();
|
|
564
|
+
if (this.widgetRuns.length === 0) {
|
|
565
|
+
if (this.widgetTui) this.clearWidget(ctx);
|
|
566
|
+
} else {
|
|
567
|
+
if (ctx?.hasUI) {
|
|
568
|
+
this.ensureWidget(ctx);
|
|
569
|
+
this.widgetTui?.requestRender();
|
|
570
|
+
}
|
|
571
|
+
this.maybePulse(ctx);
|
|
554
572
|
}
|
|
555
|
-
|
|
573
|
+
if (!run) return;
|
|
556
574
|
|
|
557
575
|
onUpdate?.({
|
|
558
576
|
content: [
|
|
@@ -599,14 +617,14 @@ export class SubagentManager {
|
|
|
599
617
|
|
|
600
618
|
private makeChildHandlers(run: RunSnapshot, task: TaskSnapshot, ctx: ExtensionContext): ChildHandlers {
|
|
601
619
|
return {
|
|
602
|
-
onAskParent: async (_taskId, question) => {
|
|
620
|
+
onAskParent: async (_taskId, question, urgent) => {
|
|
603
621
|
if (TERMINAL.includes(task.status)) {
|
|
604
622
|
return "(your task has already ended — stop work and return immediately)";
|
|
605
623
|
}
|
|
606
624
|
this.updateTask(run, task, { status: "awaiting_parent" }, ctx);
|
|
607
625
|
|
|
608
626
|
if (!this.collectParked(run.id, { kind: "ask", taskId: task.id, agent: task.agent, text: question })) {
|
|
609
|
-
this.notifyParent(run, "asked", { taskId: task.id, question });
|
|
627
|
+
this.notifyParent(run, "asked", { taskId: task.id, agent: task.agent, question, urgent });
|
|
610
628
|
}
|
|
611
629
|
|
|
612
630
|
const reply = await this.awaitParentReply(run.id, task.id, PARENT_REPLY_TIMEOUT_MS);
|
|
@@ -876,7 +894,7 @@ export class SubagentManager {
|
|
|
876
894
|
const worktreeNote = wt
|
|
877
895
|
? ` You work in an isolated git worktree (branch ${wt.branch})${task.stackedOn ? `, stacked on ${task.stackedOn} (its changes are already in your tree)` : ""}. Never run git commands that switch branches, create branches, or move the worktree (git switch/checkout/branch/worktree). The extension commits your changes when you finish. git status/diff are fine for inspecting your own changes. node_modules is a SHARED symlink to the main checkout: never install, upgrade, or delete dependencies (no npm/bun/yarn/pnpm install, no \`rm -rf node_modules\`) — those writes escape your worktree and damage the user's project. If the task truly needs a dependency change, edit the manifest only and say so in your answer.`
|
|
878
896
|
: "";
|
|
879
|
-
const subagentInstruction = `You are running as a subagent. Your bash tool already executes in the project working directory — never prefix commands with \`cd\`. Do not call subagent/delegation tools unless the parent explicitly asks. Return a concise final answer. You MAY use ask_parent only when truly blocked on information only the parent has; notify_parent for one-way updates; send_agent_message/poll_agent_messages to coordinate with siblings. Your mailbox address and siblings: ${task.roster ?? "(none)"}. Use the exact task ids (e.g. task_2) as send_agent_message targets. Siblings run independently and may start late or finish early — never block indefinitely on their replies: poll at most 5 times, then proceed with your best judgment. A gated sibling (marked ↳ waits in the graph) may not be running yet; do not wait for it. An unanswered ask_parent times out after 10 minutes — proceed with your best judgment then. When your work is done, call notify_parent ONCE with a concise result summary — key findings, verdicts, file:line evidence — so the leader can start consuming your output before the run finishes.${worktreeNote}`;
|
|
897
|
+
const subagentInstruction = `You are running as a subagent. Your bash tool already executes in the project working directory — never prefix commands with \`cd\`. Do not call subagent/delegation tools unless the parent explicitly asks. Return a concise final answer. You MAY use ask_parent only when truly blocked on information only the parent has (set urgent: true only when nothing else can proceed while you wait); notify_parent for one-way updates; send_agent_message/poll_agent_messages to coordinate with siblings. Your mailbox address and siblings: ${task.roster ?? "(none)"}. Use the exact task ids (e.g. task_2) as send_agent_message targets. Siblings run independently and may start late or finish early — never block indefinitely on their replies: poll at most 5 times, then proceed with your best judgment. A gated sibling (marked ↳ waits in the graph) may not be running yet; do not wait for it. An unanswered ask_parent times out after 10 minutes — proceed with your best judgment then. When your work is done, call notify_parent ONCE with a concise result summary — key findings, verdicts, file:line evidence — so the leader can start consuming your output before the run finishes.${worktreeNote}`;
|
|
880
898
|
|
|
881
899
|
const loader = new DefaultResourceLoader({
|
|
882
900
|
cwd: childCwd,
|