@arhen/pi-core-subagent 1.3.62 → 1.3.64
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 +15 -0
- package/package.json +1 -1
- package/src/format.ts +4 -4
- package/src/index.ts +70 -26
- package/src/manager.ts +3 -0
- package/src/presentation.ts +209 -0
- package/src/schemas.ts +2 -1
package/README.md
CHANGED
|
@@ -317,6 +317,21 @@ Background (default) + intercom — the run returns a runId immediately; you sta
|
|
|
317
317
|
|
|
318
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; completions and aborts queue as follow-ups.
|
|
319
319
|
|
|
320
|
+
## Tool exposure and opt-in codemode
|
|
321
|
+
|
|
322
|
+
Default: **legacy direct calls**, even when the codemode tool is active. Active helpers remain
|
|
323
|
+
model-visible and retain native script-call compatibility. Explicit global `codemode.mode: "only"`
|
|
324
|
+
still applies Pi\'s global script-only policy; this extension does not override it.
|
|
325
|
+
|
|
326
|
+
- `/subagents mode` — show preference and effective profile.
|
|
327
|
+
- `/subagents mode auto` — opt in to script-based routing while codemode is active; direct otherwise.
|
|
328
|
+
- `/subagents mode codemode` — explicitly select script-based routing (direct fallback when unavailable).
|
|
329
|
+
- `/subagents mode direct` — return to legacy direct behavior.
|
|
330
|
+
|
|
331
|
+
Choice is stored per session branch. New sessions/branches without a preference start direct;
|
|
332
|
+
existing explicit choices remain intact. Published 1.3.63 defaulted to `auto`: this compatibility
|
|
333
|
+
correction requires the corrected source/package, not a retroactive change to that npm release.
|
|
334
|
+
|
|
320
335
|
## Commands
|
|
321
336
|
|
|
322
337
|
- `/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.64",
|
|
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/format.ts
CHANGED
|
@@ -248,8 +248,8 @@ export function makeTaskNotice(run: RunSnapshot, task: TaskSnapshot, kind: strin
|
|
|
248
248
|
isStartupFailure(task, kind)
|
|
249
249
|
? "Never started — stop and diagnose before spawning anything else: a config-level error (model, plan, auth, agent file) fails identically on every respawn."
|
|
250
250
|
: kind === "completed"
|
|
251
|
-
? `Use subagent_result(runId: "${run.id}", taskId: "${task.id}") for full output.`
|
|
252
|
-
: `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.`,
|
|
251
|
+
? `Use subagent_result({ runId: "${run.id}", taskId: "${task.id}" }) for full output.`
|
|
252
|
+
: `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
253
|
].join("\n");
|
|
254
254
|
}
|
|
255
255
|
export function makeAskNotice(
|
|
@@ -257,7 +257,7 @@ export function makeAskNotice(
|
|
|
257
257
|
extra: { taskId?: string; agent?: string; question?: string; urgent?: boolean },
|
|
258
258
|
): string {
|
|
259
259
|
const who = extra.agent ? `${extra.agent} (${extra.taskId ?? "task"})` : (extra.taskId ?? "a subagent");
|
|
260
|
-
const reply = `reply_subagent(runId: "${run.id}", taskId: "${extra.taskId ?? ""}", message: ...)`;
|
|
260
|
+
const reply = `reply_subagent({ runId: "${run.id}", taskId: "${extra.taskId ?? ""}", message: ... })`;
|
|
261
261
|
return extra.urgent
|
|
262
262
|
? `[URGENT] Subagent ${who} is blocked and cannot continue until you answer: ${extra.question ?? ""}\nAnswer now, before your next step, with ${reply}.`
|
|
263
263
|
: `[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}.`;
|
|
@@ -270,7 +270,7 @@ export function makeNotice(run: RunSnapshot, kind: string): string {
|
|
|
270
270
|
for (const task of run.tasks) {
|
|
271
271
|
lines.push(`- ${task.agent}: ${task.status}${task.error ? ` — ${truncateText(task.error, 200)}` : ""}`);
|
|
272
272
|
}
|
|
273
|
-
lines.push(`Use subagent_result(runId: "${run.id}") for full output.`);
|
|
273
|
+
lines.push(`Use subagent_result({ runId: "${run.id}" }) for full output.`);
|
|
274
274
|
return lines.join("\n");
|
|
275
275
|
}
|
|
276
276
|
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext, ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { Text, truncateToWidth } from "@earendil-works/pi-tui";
|
|
3
|
+
import type { TSchema } from "typebox";
|
|
3
4
|
import {
|
|
4
5
|
compactLines,
|
|
5
6
|
formatUsage,
|
|
@@ -13,6 +14,7 @@ import { waveNotation } from "./graph.ts";
|
|
|
13
14
|
import { cloneRun, type ParkedMsg, SubagentManager } from "./manager.ts";
|
|
14
15
|
import { listSelectableModels } from "./models.ts";
|
|
15
16
|
import { createPeekPane, type PeekTask } from "./peek.ts";
|
|
17
|
+
import { type AnyToolDefinition, createPresentation, isSubagentMode } from "./presentation.ts";
|
|
16
18
|
import {
|
|
17
19
|
AwaitParam,
|
|
18
20
|
ModelsParam,
|
|
@@ -29,6 +31,13 @@ import { cleanupMerged, ownerAlive, reapDeadWorktrees, repoRoot, sweepStale } fr
|
|
|
29
31
|
|
|
30
32
|
export default function (pi: ExtensionAPI) {
|
|
31
33
|
const manager = new SubagentManager(pi);
|
|
34
|
+
const definitions: AnyToolDefinition[] = [];
|
|
35
|
+
const presentation = createPresentation(pi, definitions);
|
|
36
|
+
const defineTool = <TParams extends TSchema, TDetails = unknown, TState = any>(
|
|
37
|
+
definition: ToolDefinition<TParams, TDetails, TState>,
|
|
38
|
+
): void => {
|
|
39
|
+
definitions.push(definition as AnyToolDefinition);
|
|
40
|
+
};
|
|
32
41
|
|
|
33
42
|
const openPeek = async (ctx: ExtensionContext) => {
|
|
34
43
|
if (!ctx.hasUI) return;
|
|
@@ -65,11 +74,30 @@ export default function (pi: ExtensionAPI) {
|
|
|
65
74
|
};
|
|
66
75
|
pi.registerCommand("subagents", {
|
|
67
76
|
description:
|
|
68
|
-
"List subagent runs. `/subagents peek` opens the browsable pane; `/subagents auto-limit on|off` toggles the 1 h default runtime ceiling (default off = 6 h).",
|
|
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).",
|
|
69
78
|
handler: async (args, ctx) => {
|
|
70
79
|
const arg = String(args ?? "")
|
|
71
80
|
.trim()
|
|
72
81
|
.toLowerCase();
|
|
82
|
+
if (arg === "mode" || arg.startsWith("mode ")) {
|
|
83
|
+
const value = arg.split(/\s+/)[1];
|
|
84
|
+
if (value === undefined) {
|
|
85
|
+
ctx.ui.notify(presentation.describe(), "info");
|
|
86
|
+
} else if (isSubagentMode(value)) {
|
|
87
|
+
presentation.setPreference(value);
|
|
88
|
+
// A switch during a streaming turn or an executing script would rewrite the loadout under
|
|
89
|
+
// a live call; defer it to the next request boundary in that case.
|
|
90
|
+
const idle = typeof ctx.isIdle === "function" ? ctx.isIdle() : true;
|
|
91
|
+
if (idle) presentation.sync();
|
|
92
|
+
ctx.ui.notify(
|
|
93
|
+
`Subagent exposure mode set to ${value}${idle ? "." : " — applies at the next request boundary."} ${presentation.describe()}`,
|
|
94
|
+
"info",
|
|
95
|
+
);
|
|
96
|
+
} else {
|
|
97
|
+
ctx.ui.notify(`Unknown subagent mode "${value}". Use \`/subagents mode auto|direct|codemode\`.`, "warning");
|
|
98
|
+
}
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
73
101
|
if (arg === "peek") return openPeek(ctx);
|
|
74
102
|
if (arg === "auto-limit" || arg.startsWith("auto-limit ")) {
|
|
75
103
|
const value = arg.split(/\s+/)[1];
|
|
@@ -103,8 +131,19 @@ export default function (pi: ExtensionAPI) {
|
|
|
103
131
|
manager.turnActivity = false;
|
|
104
132
|
});
|
|
105
133
|
|
|
134
|
+
pi.on("before_agent_start", () => {
|
|
135
|
+
presentation.sync();
|
|
136
|
+
});
|
|
137
|
+
// Successive model turns of the same run are safe boundaries too: a codemode availability or
|
|
138
|
+
// helper-selection change made between turns must reach the next request without a new prompt.
|
|
139
|
+
pi.on("turn_start", () => {
|
|
140
|
+
presentation.sync();
|
|
141
|
+
});
|
|
142
|
+
|
|
106
143
|
pi.on("session_start", async (_event, ctx) => {
|
|
107
144
|
await manager.restoreFromSidecar(ctx);
|
|
145
|
+
presentation.restore(ctx);
|
|
146
|
+
presentation.sync();
|
|
108
147
|
|
|
109
148
|
const roots = new Set<string>();
|
|
110
149
|
const cwdRoot = repoRoot(ctx.cwd);
|
|
@@ -125,6 +164,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
125
164
|
} catch {}
|
|
126
165
|
}
|
|
127
166
|
});
|
|
167
|
+
pi.on("session_tree", (_event, ctx) => {
|
|
168
|
+
presentation.restore(ctx);
|
|
169
|
+
presentation.sync();
|
|
170
|
+
});
|
|
171
|
+
|
|
128
172
|
pi.on("session_shutdown", async (_event, ctx) => {
|
|
129
173
|
if (ctx?.hasUI) {
|
|
130
174
|
try {
|
|
@@ -134,11 +178,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
134
178
|
manager.clearRuns();
|
|
135
179
|
});
|
|
136
180
|
|
|
137
|
-
|
|
181
|
+
defineTool<typeof ModelsParam, ModelCatalog>({
|
|
138
182
|
name: "subagent_models",
|
|
139
183
|
label: "Subagent Models",
|
|
140
184
|
description:
|
|
141
|
-
"List the models a subagent task may name
|
|
185
|
+
"List the models a subagent task may name: the exact `model` value to pass, the thinking levels the runtime honors, the context window and catalog price. Scoped to this session's models when scoping is configured, else every usable model; ambiguous references are called out. Full reference: `describeNamespace('subagents')`.",
|
|
142
186
|
promptSnippet: "List the models a subagent task can name (reference, thinking levels, context, price).",
|
|
143
187
|
parameters: ModelsParam,
|
|
144
188
|
async execute(_id, _params, _signal, _onUpdate, ctx) {
|
|
@@ -146,26 +190,24 @@ export default function (pi: ExtensionAPI) {
|
|
|
146
190
|
},
|
|
147
191
|
});
|
|
148
192
|
|
|
149
|
-
|
|
193
|
+
defineTool<typeof SubagentParams, RunDetails>({
|
|
150
194
|
name: "subagent",
|
|
151
195
|
label: "Subagent",
|
|
152
196
|
|
|
153
197
|
description:
|
|
154
|
-
"Run isolated subagents (own context
|
|
198
|
+
"Run isolated subagents (own context/session) in the background; returns a runId and completion notifies you. One call = one agent (`agent`+`task`) or many (`tasks`, `chain`, or `needs` edges that gate tasks and prepend upstream output). Write tasks use an isolated git worktree and report a branch; a matching agent file is authoritative (matched by description, body/model win). Full reference: `describeNamespace('subagents')`.",
|
|
155
199
|
promptSnippet: "Define and delegate work to specialized subagents.",
|
|
156
200
|
promptGuidelines: [
|
|
157
201
|
"`model` is optional: omit it to inherit your current session model, or name one (agent-file `model` frontmatter wins) to pin the run. Call subagent_models for the exact references, thinking levels, and prices this session may use.",
|
|
158
202
|
"Use subagent when independent review, testing, research, or parallel analysis improves quality.",
|
|
159
203
|
"Batch every sub-task in ONE call: subagent({ tasks: [...] }) — never multiple parallel subagent calls.",
|
|
160
204
|
"Declare ordering with `needs` edges on the tasks, never by splitting into separate calls; dependents receive upstream outputs automatically — do not restate them. Prefer flat `tasks` (plain parallel); add `needs` only when ordering genuinely matters.",
|
|
205
|
+
"Define each agent inline: invented name, focused system prompt, read-only by default (write:true to edit). Agent files are matched by description/goal, not name; a match is authoritative (body/model), and only per-call tools/write override its tools.",
|
|
206
|
+
"Write agents work in an isolated git worktree; review the branch diff and merge with `git merge --no-ff <branch>` when done.",
|
|
207
|
+
"After spawning, call subagent_status({ runId }) ONCE to confirm the tasks started; fix or respawn a task that died on spawn.",
|
|
161
208
|
"End each task with a runnable check, e.g. 'Verify: bun test'. A subagent's claim of success is not evidence.",
|
|
162
|
-
"
|
|
163
|
-
"
|
|
164
|
-
"Right after spawning, call subagent_status(runId) ONCE before any other work — a child that died on spawn (or never started) is invisible until far later otherwise. If it shows a task failed/never started, fix or respawn immediately.",
|
|
165
|
-
"Never block with nothing to do: if you have no work left after spawning, end your turn — completion notifies you and wakes a fresh turn with the results. await_subagent/autoAwait while idle only burns time and tokens.",
|
|
166
|
-
"A failed task interrupts you immediately as a steering message — handle it in the same turn (resume, swap model, re-dispatch) instead of finishing the plan on a broken intermediate result. Completes and aborts queue as follow-ups.",
|
|
167
|
-
"autoAwait:true only when this SAME turn must consume the result immediately. await_subagent is for syncing with your own parallel work — not the default follow-up to a spawn.",
|
|
168
|
-
"A task that failed mid-work (provider error, rate limit, timeout) keeps its session file and branch: resume_subagent(runId, taskId, model?) revives it with full context — prefer that over respawning. Respawn only when it never started (no session file).",
|
|
209
|
+
"When you have no work left, end your turn — completion notifies you. await_subagent/autoAwait only when this turn must consume the result immediately.",
|
|
210
|
+
"A failed task keeps its session file and branch: resume_subagent({ runId, taskId }) revives it; respawn only when it never started.",
|
|
169
211
|
],
|
|
170
212
|
parameters: SubagentParams,
|
|
171
213
|
executionMode: "parallel",
|
|
@@ -195,7 +237,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
195
237
|
? `\n${asks.length} child(ren) waiting for your answer:\n${asks
|
|
196
238
|
.map(
|
|
197
239
|
(a) =>
|
|
198
|
-
`- ${a.agent} (${a.taskId}): ${a.text}\n reply_subagent(runId: "${run.id}", taskId: "${a.taskId}", message: ...)`,
|
|
240
|
+
`- ${a.agent} (${a.taskId}): ${a.text}\n reply_subagent({ runId: "${run.id}", taskId: "${a.taskId}", message: ... })`,
|
|
199
241
|
)
|
|
200
242
|
.join("\n")}\nAnswer each, then await_subagent again for the result.`
|
|
201
243
|
: "",
|
|
@@ -208,7 +250,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
208
250
|
content: [
|
|
209
251
|
{
|
|
210
252
|
type: "text",
|
|
211
|
-
text: `Background run started: ${details.run.id} (${details.run.mode}, ${details.run.tasks.length} task${details.run.tasks.length > 1 ? "s" : ""}).\nNext: call subagent_status("${details.run.id}") now to confirm the tasks actually started before doing anything else.\nAfter that, completion will notify you — if you have no other work, end your turn instead of waiting.\nOther tools: subagent_result / reply_subagent / steer_subagent / resume_subagent / subagent_cancel.`,
|
|
253
|
+
text: `Background run started: ${details.run.id} (${details.run.mode}, ${details.run.tasks.length} task${details.run.tasks.length > 1 ? "s" : ""}).\nNext: call subagent_status({ runId: "${details.run.id}" }) now to confirm the tasks actually started before doing anything else.\nAfter that, completion will notify you — if you have no other work, end your turn instead of waiting.\nOther tools: subagent_result / reply_subagent / steer_subagent / resume_subagent / subagent_cancel.`,
|
|
212
254
|
},
|
|
213
255
|
],
|
|
214
256
|
details,
|
|
@@ -282,11 +324,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
282
324
|
},
|
|
283
325
|
});
|
|
284
326
|
|
|
285
|
-
|
|
327
|
+
defineTool<typeof RunIdParam, { run?: RunSnapshot }>({
|
|
286
328
|
name: "subagent_status",
|
|
287
329
|
label: "Subagent Status",
|
|
288
330
|
description:
|
|
289
|
-
"Live per-task status of a subagent run (non-blocking), incl. each child's session file path
|
|
331
|
+
"Live per-task status of a subagent run (non-blocking), incl. each child's session file path to `tail -f`. Call once right after spawning to verify children actually started.",
|
|
290
332
|
promptSnippet: "Check progress of a subagent run; use right after spawn as a health check.",
|
|
291
333
|
parameters: RunIdParam,
|
|
292
334
|
async execute(_id, params) {
|
|
@@ -303,7 +345,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
303
345
|
},
|
|
304
346
|
});
|
|
305
347
|
|
|
306
|
-
|
|
348
|
+
defineTool<typeof ResultParam, { run?: RunSnapshot }>({
|
|
307
349
|
name: "subagent_result",
|
|
308
350
|
label: "Subagent Result",
|
|
309
351
|
description: "Full result (finalText + usage) of a run or one task. Non-blocking.",
|
|
@@ -330,11 +372,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
330
372
|
},
|
|
331
373
|
});
|
|
332
374
|
|
|
333
|
-
|
|
375
|
+
defineTool<typeof AwaitParam, { run?: RunSnapshot }>({
|
|
334
376
|
name: "await_subagent",
|
|
335
377
|
label: "Await Subagent",
|
|
336
378
|
description:
|
|
337
|
-
"Block until a run finishes (or timeoutMs elapses). Only when you have your own work to sync
|
|
379
|
+
"Block until a run finishes (or timeoutMs elapses). Only when you have your own work to sync; otherwise end your turn. Child messages that arrive while parked wake the wait and are returned.",
|
|
338
380
|
parameters: AwaitParam,
|
|
339
381
|
async execute(_id, params) {
|
|
340
382
|
const { runId, timeoutMs } = params as { runId: string; timeoutMs?: number };
|
|
@@ -352,7 +394,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
352
394
|
},
|
|
353
395
|
});
|
|
354
396
|
|
|
355
|
-
|
|
397
|
+
defineTool<typeof ReplyParam, { run?: RunSnapshot }>({
|
|
356
398
|
name: "reply_subagent",
|
|
357
399
|
label: "Reply Subagent",
|
|
358
400
|
description: "Answer a child's ask_parent question; resumes its run.",
|
|
@@ -373,7 +415,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
373
415
|
},
|
|
374
416
|
});
|
|
375
417
|
|
|
376
|
-
|
|
418
|
+
defineTool<typeof SteerParam, { steered?: string[] }>({
|
|
377
419
|
name: "steer_subagent",
|
|
378
420
|
label: "Steer Subagent",
|
|
379
421
|
description:
|
|
@@ -400,11 +442,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
400
442
|
},
|
|
401
443
|
});
|
|
402
444
|
|
|
403
|
-
|
|
445
|
+
defineTool<typeof ResumeParam, { run?: RunSnapshot }>({
|
|
404
446
|
name: "resume_subagent",
|
|
405
447
|
label: "Resume Subagent",
|
|
406
448
|
description:
|
|
407
|
-
"Revive a failed/aborted task in its original session (full context + worktree branch preserved). Optional `model` swaps provider
|
|
449
|
+
"Revive a failed/aborted task in its original session (full context + worktree branch preserved). Optional `model` swaps provider after e.g. a rate limit; `thinking` is clamped to the target model; `message` replaces the default recap prompt. Refuses tasks that never started — respawn those.",
|
|
408
450
|
parameters: ResumeParam,
|
|
409
451
|
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
410
452
|
const { runId, taskId, message, model, thinking } = params as {
|
|
@@ -421,7 +463,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
421
463
|
content: [
|
|
422
464
|
{
|
|
423
465
|
type: "text",
|
|
424
|
-
text: `Resumed ${runId}/${taskId} (${res.task.agent})${model ? ` on ${model}` : ""} from ${res.task.sessionFile}${res.task.branch ? `, branch ${res.task.branch}` : ""}.${res.note ? ` Adjusted ${res.note}.` : ""}\nNext: subagent_status("${runId}") to confirm it is running; completion will notify you.`,
|
|
466
|
+
text: `Resumed ${runId}/${taskId} (${res.task.agent})${model ? ` on ${model}` : ""} from ${res.task.sessionFile}${res.task.branch ? `, branch ${res.task.branch}` : ""}.${res.note ? ` Adjusted ${res.note}.` : ""}\nNext: subagent_status({ runId: "${runId}" }) to confirm it is running; completion will notify you.`,
|
|
425
467
|
},
|
|
426
468
|
],
|
|
427
469
|
details: { run: run ? cloneRun(run) : undefined },
|
|
@@ -429,7 +471,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
429
471
|
},
|
|
430
472
|
});
|
|
431
473
|
|
|
432
|
-
|
|
474
|
+
defineTool<typeof RunIdParam, { aborted?: number }>({
|
|
433
475
|
name: "subagent_cancel",
|
|
434
476
|
label: "Subagent Cancel",
|
|
435
477
|
description: "Abort a running/queued subagent run. Children are killed; run becomes aborted.",
|
|
@@ -446,4 +488,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
446
488
|
};
|
|
447
489
|
},
|
|
448
490
|
});
|
|
491
|
+
|
|
492
|
+
presentation.registerInitial();
|
|
449
493
|
}
|
package/src/manager.ts
CHANGED
|
@@ -63,6 +63,9 @@ import {
|
|
|
63
63
|
type Worktree,
|
|
64
64
|
} from "./worktree.ts";
|
|
65
65
|
|
|
66
|
+
/** Legacy callers import the resolver from here; its owner is `models.ts`. */
|
|
67
|
+
export { resolveChildModel };
|
|
68
|
+
|
|
66
69
|
export const DEFAULT_CONCURRENCY = 3;
|
|
67
70
|
export const MAX_CONCURRENCY = 8;
|
|
68
71
|
const DEFAULT_RUNTIME_MS = 3_600_000;
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionAPI,
|
|
3
|
+
ExtensionContext,
|
|
4
|
+
ToolDefinition,
|
|
5
|
+
ToolExposure,
|
|
6
|
+
ToolLoadout,
|
|
7
|
+
ToolLoadoutChanges,
|
|
8
|
+
ToolNamespace,
|
|
9
|
+
} from "@earendil-works/pi-coding-agent";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Presentation preference for the subagent toolset.
|
|
13
|
+
* - auto: native codemode presentation when the codemode tool is active, direct otherwise.
|
|
14
|
+
* - direct (default): legacy native declarations and active-tool script callability.
|
|
15
|
+
* - codemode: subagent tools are callable from codemode scripts but not declared or listed.
|
|
16
|
+
*/
|
|
17
|
+
export type SubagentMode = "auto" | "direct" | "codemode";
|
|
18
|
+
export type EffectiveMode = "direct" | "codemode";
|
|
19
|
+
|
|
20
|
+
/** Branch-scoped custom entry holding the preference, so resume/tree keep it. */
|
|
21
|
+
export const MODE_ENTRY_TYPE = "subagent-mode";
|
|
22
|
+
export const CODEMODE_TOOL_NAME = "codemode";
|
|
23
|
+
|
|
24
|
+
export const SUBAGENT_NAMESPACE: ToolNamespace = {
|
|
25
|
+
name: "subagents",
|
|
26
|
+
description: "Isolated background subagents with a dependency-graph scheduler, git-worktree isolation and intercom.",
|
|
27
|
+
instructions: `Isolated subagents with their own context, session and optional git worktree. Delegate independent review, testing, research or parallel analysis.
|
|
28
|
+
|
|
29
|
+
## Modes
|
|
30
|
+
- direct (default): legacy native tool declarations and active-tool script callability; activating codemode alone does not switch profiles.
|
|
31
|
+
- auto (opt-in): codemode presentation while the \`codemode\` tool is active, direct otherwise.
|
|
32
|
+
- codemode: active tools are callable from scripts but not declared; inactive tools stay model-only and are not callable at all.
|
|
33
|
+
Switch with \`/subagents mode auto|direct|codemode\`. The preference is stored on the session branch and restored on reload, resume and tree navigation.
|
|
34
|
+
|
|
35
|
+
## Operations
|
|
36
|
+
Object arguments, exactly as validated when issued by the model:
|
|
37
|
+
- \`subagent({ agent, task, prompt?, write?, tools?, model?, thinking?, cwd?, maxRuntimeMs?, autoAwait?, notifyPerTask? })\` runs one agent; \`tasks: [...]\`, \`chain: [...]\` with \`{previous}\` and \`needs\` edges gate tasks and prepend upstream output.
|
|
38
|
+
- \`subagent_models()\` lists the models a task may name: the exact \`model\` value to pass, the thinking levels the runtime honors, the context window and pi catalog price per million tokens. The list follows this session's scoped models when scoping is configured, else every usable model; the output states which case applies, and references that would resolve to another model are called out as ambiguous.
|
|
39
|
+
- \`subagent_status({ runId })\` returns live per-task status and session file paths; call once right after spawning.
|
|
40
|
+
- \`subagent_result({ runId, taskId? })\` returns final text, usage and the worktree branch/diff summary.
|
|
41
|
+
- \`await_subagent({ runId, timeoutMs? })\` blocks only when this turn must consume the result.
|
|
42
|
+
- \`reply_subagent({ runId, taskId, message })\` answers a child \`ask_parent\` question and resumes it.
|
|
43
|
+
- \`steer_subagent({ runId, taskId?, message })\` injects a message into running tasks.
|
|
44
|
+
- \`resume_subagent({ runId, taskId, message?, model?, thinking? })\` revives a failed/aborted task with its context and branch.
|
|
45
|
+
- \`subagent_cancel({ runId })\` aborts the run and kills its children.
|
|
46
|
+
|
|
47
|
+
## Agents
|
|
48
|
+
- Define each agent inline: invented name, focused system prompt. Agent files (\`.agents/agents\`, \`.claude/agents\`, \`.pi/agents\`; project dirs, then home) are matched by \`description\` against the goal, never by name. A match is authoritative: its body is the system prompt, its \`model\` frontmatter wins over the inline value, and its \`tools\` apply — only the per-call \`tools\` and \`write\` override them.
|
|
49
|
+
- Agents are read-only by default; \`write: true\` gives an isolated git worktree branch. Review the diff and merge with \`git merge --no-ff <branch>\`.
|
|
50
|
+
- \`model\` is optional: omit it to inherit the session model, or name one from \`subagent_models\` to pin the run.
|
|
51
|
+
|
|
52
|
+
## Rules
|
|
53
|
+
- Batch every sub-task in ONE call with \`tasks\`/\`needs\`; do not split parallel work into separate calls.
|
|
54
|
+
- After spawning, call \`subagent_status({ runId })\` once to confirm the tasks started; fix or respawn a task that died on spawn.
|
|
55
|
+
- A task that failed mid-work keeps its session file and branch: \`resume_subagent({ runId, taskId })\`, model swap included, revives it; respawn only a task that never started.
|
|
56
|
+
- Completion and failures notify you: end the turn when you have no work left; use \`autoAwait\` only when the same turn must consume the result.
|
|
57
|
+
- End each task with a runnable check, e.g. 'Verify: bun test'. A subagent's claim of success is not evidence.
|
|
58
|
+
|
|
59
|
+
## Codemode
|
|
60
|
+
When the codemode profile is explicitly selected (or selected by opt-in auto), these tools are not declared. Call an active operation as \`tools.<name>(args)\`; spawning uses \`await tools.subagent(args)\`. If its signature is unknown, inspect only \`text(await describeTool('subagent'))\` once and reuse it. For another operation, inspect only its exact name when needed. Broad search and tool-list dumps are unnecessary for these known names; this namespace is the optional full reference. Inactive tools remain unavailable. Arguments are validated exactly like model-issued calls.`,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/** Adds the script-call path to the upfront rules, but only in the codemode profile. */
|
|
64
|
+
export const CODEMODE_DISCOVERY_GUIDELINE =
|
|
65
|
+
"Codemode subagents: call active tools as tools.<name>(args); spawn with await tools.subagent(args). If its signature is unknown, inspect only text(await describeTool('subagent')) once, then reuse it. Other helpers: describeTool(exactName) only when needed. No broad search or tool-list dump required; describeNamespace('subagents') is optional full reference.";
|
|
66
|
+
|
|
67
|
+
/** The full-reference pointer only resolves while codemode scripts can reach `describeNamespace`. */
|
|
68
|
+
const NAMESPACE_REFERENCE = / Full reference: `describeNamespace\('subagents'\)`\./;
|
|
69
|
+
|
|
70
|
+
export function isSubagentMode(value: unknown): value is SubagentMode {
|
|
71
|
+
return value === "auto" || value === "direct" || value === "codemode";
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export type AnyToolDefinition = ToolDefinition<any, any, any>;
|
|
75
|
+
|
|
76
|
+
export interface Presentation {
|
|
77
|
+
readonly preference: SubagentMode;
|
|
78
|
+
/** The profile currently registered, or undefined before the first registration. */
|
|
79
|
+
readonly applied: EffectiveMode | undefined;
|
|
80
|
+
/** Effective mode for the given active set, defaulting to the session's live active tools. */
|
|
81
|
+
effective(active?: readonly string[]): EffectiveMode;
|
|
82
|
+
/** Report preference, effective mode and codemode availability. */
|
|
83
|
+
describe(): string;
|
|
84
|
+
setPreference(mode: SubagentMode): void;
|
|
85
|
+
/** Restore the latest preference stored on the session branch. */
|
|
86
|
+
restore(ctx: ExtensionContext): void;
|
|
87
|
+
/** Register the tools for the current effective mode; no-op when already applied. */
|
|
88
|
+
sync(): boolean;
|
|
89
|
+
/** Initial registration during extension load, before any session state exists. */
|
|
90
|
+
registerInitial(): void;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Owns the subagent exposure profile. Tools stay registered while only their exposure changes, so
|
|
95
|
+
* the active selection (and the running manager) is preserved across mode switches.
|
|
96
|
+
*/
|
|
97
|
+
export function createPresentation(pi: ExtensionAPI, definitions: readonly AnyToolDefinition[]): Presentation {
|
|
98
|
+
const ownNames = (): string[] => definitions.map((definition) => definition.name);
|
|
99
|
+
let preference: SubagentMode = "direct";
|
|
100
|
+
let applied: EffectiveMode | undefined;
|
|
101
|
+
let appliedOwn = new Set<string>();
|
|
102
|
+
|
|
103
|
+
const codemodeActive = (active: readonly string[]): boolean => active.includes(CODEMODE_TOOL_NAME);
|
|
104
|
+
const effectiveFrom = (active: readonly string[]): EffectiveMode =>
|
|
105
|
+
preference !== "direct" && codemodeActive(active) ? "codemode" : "direct";
|
|
106
|
+
const sameSet = (a: ReadonlySet<string>, b: ReadonlySet<string>): boolean =>
|
|
107
|
+
a.size === b.size && [...a].every((name) => b.has(name));
|
|
108
|
+
|
|
109
|
+
// Hides only the declarations of active subagent tools; other tools keep their loadout. Runs on
|
|
110
|
+
// every loadout recompute, so it follows the registered profile: a preference that is only
|
|
111
|
+
// pending (mid-stream) must not rewrite a live loadout.
|
|
112
|
+
const hideDeclarations = (loadout: ToolLoadout): ToolLoadoutChanges | undefined => {
|
|
113
|
+
if (applied !== "codemode") return undefined;
|
|
114
|
+
const active = new Set(loadout.declared.map((tool) => tool.name));
|
|
115
|
+
const hidden = definitions.map((definition) => definition.name).filter((name) => active.has(name));
|
|
116
|
+
return hidden.length > 0 ? { hiddenDeclarations: hidden } : undefined;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// Opted-in codemode defers only active own tools; inactive tools stay model-only. Legacy direct
|
|
120
|
+
// callability follows native activation. Re-registering
|
|
121
|
+
// must not reactivate a tool the user deactivated, so `defaultActive: false` replaces the native
|
|
122
|
+
// default once the initial registration has made the tools available by default.
|
|
123
|
+
const register = (mode: EffectiveMode, ownActive: ReadonlySet<string>, initial = false): void => {
|
|
124
|
+
const own = new Set(ownNames());
|
|
125
|
+
applied = mode;
|
|
126
|
+
for (const definition of definitions) {
|
|
127
|
+
const scriptCallable = mode === "codemode" && ownActive.has(definition.name);
|
|
128
|
+
const exposure: ToolExposure = scriptCallable ? "deferred" : mode === "direct" ? "direct" : "model-only";
|
|
129
|
+
const promptGuidelines = [...(definition.promptGuidelines ?? [])];
|
|
130
|
+
if (scriptCallable && !promptGuidelines.includes(CODEMODE_DISCOVERY_GUIDELINE))
|
|
131
|
+
promptGuidelines.push(CODEMODE_DISCOVERY_GUIDELINE);
|
|
132
|
+
// Legacy direct tools stay ungrouped; the namespace reference only belongs to the opted-in profile.
|
|
133
|
+
const description =
|
|
134
|
+
mode === "direct" ? definition.description.replace(NAMESPACE_REFERENCE, "") : definition.description;
|
|
135
|
+
pi.registerTool({
|
|
136
|
+
...definition,
|
|
137
|
+
description,
|
|
138
|
+
exposure,
|
|
139
|
+
namespace: mode === "codemode" ? SUBAGENT_NAMESPACE : undefined,
|
|
140
|
+
promptGuidelines,
|
|
141
|
+
prepareLoadout: hideDeclarations,
|
|
142
|
+
...(initial ? {} : { defaultActive: false }),
|
|
143
|
+
} as AnyToolDefinition);
|
|
144
|
+
}
|
|
145
|
+
// With an explicit --tools/defaultTools allowlist the SDK force-activates every registered
|
|
146
|
+
// declarable tool it names, ignoring `defaultActive`. Restore exactly the own-name membership
|
|
147
|
+
// that was asked for; every unrelated name, including a newly registered one, is preserved.
|
|
148
|
+
if (!initial) {
|
|
149
|
+
const active = pi.getActiveTools();
|
|
150
|
+
if (active.some((name) => own.has(name) && !ownActive.has(name)))
|
|
151
|
+
pi.setActiveTools(active.filter((name) => !own.has(name) || ownActive.has(name)));
|
|
152
|
+
}
|
|
153
|
+
appliedOwn = new Set(ownActive);
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
get preference() {
|
|
158
|
+
return preference;
|
|
159
|
+
},
|
|
160
|
+
get applied() {
|
|
161
|
+
return applied;
|
|
162
|
+
},
|
|
163
|
+
effective: (active = pi.getActiveTools()) => effectiveFrom(active),
|
|
164
|
+
describe() {
|
|
165
|
+
const active = pi.getActiveTools();
|
|
166
|
+
const target = effectiveFrom(active);
|
|
167
|
+
const shown = applied ?? target;
|
|
168
|
+
const available = codemodeActive(active);
|
|
169
|
+
const availability = available ? "codemode is active" : "codemode is not active";
|
|
170
|
+
const fallback = !available && preference === "codemode" ? "; using direct fallback" : "";
|
|
171
|
+
const pending = target !== shown ? `; pending ${target} at the next request boundary` : "";
|
|
172
|
+
return `Subagent exposure: mode ${preference} → effective ${shown} (${availability}${fallback}${pending}). Use /subagents mode auto|direct|codemode.`;
|
|
173
|
+
},
|
|
174
|
+
setPreference(mode) {
|
|
175
|
+
preference = mode;
|
|
176
|
+
pi.appendEntry(MODE_ENTRY_TYPE, { mode });
|
|
177
|
+
},
|
|
178
|
+
restore(ctx) {
|
|
179
|
+
let restored: SubagentMode | undefined;
|
|
180
|
+
for (const entry of ctx.sessionManager.getBranch()) {
|
|
181
|
+
const candidate = entry as { type?: string; customType?: string; data?: { mode?: unknown } };
|
|
182
|
+
if (
|
|
183
|
+
candidate.type === "custom" &&
|
|
184
|
+
candidate.customType === MODE_ENTRY_TYPE &&
|
|
185
|
+
isSubagentMode(candidate.data?.mode)
|
|
186
|
+
)
|
|
187
|
+
restored = candidate.data.mode;
|
|
188
|
+
}
|
|
189
|
+
// Branch-sensitive state: a branch without an entry uses the package default again.
|
|
190
|
+
preference = restored ?? "direct";
|
|
191
|
+
},
|
|
192
|
+
sync() {
|
|
193
|
+
const own = new Set(ownNames());
|
|
194
|
+
const active = pi.getActiveTools();
|
|
195
|
+
const next = effectiveFrom(active);
|
|
196
|
+
const ownActive = new Set(active.filter((name) => own.has(name)));
|
|
197
|
+
// Members inside a profile are part of the registration: a helper activated or deactivated
|
|
198
|
+
// natively must change its exposure (deferred vs model-only) at the next boundary.
|
|
199
|
+
if (next === applied && sameSet(ownActive, appliedOwn)) return false;
|
|
200
|
+
register(next, ownActive);
|
|
201
|
+
return true;
|
|
202
|
+
},
|
|
203
|
+
registerInitial() {
|
|
204
|
+
// Before the session runtime exists `pi.getActiveTools()` is not bound; the optimistic
|
|
205
|
+
// all-active set is corrected by `sync()` once the session starts.
|
|
206
|
+
register("direct", new Set(ownNames()), true);
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
}
|
package/src/schemas.ts
CHANGED
|
@@ -19,7 +19,8 @@ const TaskItem = Type.Object({
|
|
|
19
19
|
prompt: Type.Optional(Type.String({ description: "System prompt defining this agent's behavior" })),
|
|
20
20
|
write: Type.Optional(
|
|
21
21
|
Type.Boolean({
|
|
22
|
-
description:
|
|
22
|
+
description:
|
|
23
|
+
"true = write toolset (adds bash, edit, write); default false = read-only (read, grep, find, ls, codemode)",
|
|
23
24
|
}),
|
|
24
25
|
),
|
|
25
26
|
model: Type.Optional(
|