pi-subagents 0.65.1 → 0.66.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +55 -0
- package/README.md +1 -1
- package/agents/researcher.md +23 -13
- package/docs/agents.md +16 -2
- package/docs/configuration.md +18 -0
- package/docs/extension-api.md +91 -0
- package/docs/models.md +58 -1
- package/docs/observability.md +42 -2
- package/docs/tool-reference.md +3 -3
- package/docs/workflows.md +14 -7
- package/package.json +2 -1
- package/skills/pi-subagents/references/execution-controls.md +14 -1
- package/skills/pi-subagents/references/management-authoring-rpc.md +2 -1
- package/src/agents/advertised-agent-prompt.ts +63 -0
- package/src/agents/agent-management.ts +14 -1
- package/src/agents/agent-serializer.ts +2 -0
- package/src/agents/agents.ts +8 -0
- package/src/api/shared-types.ts +1 -1
- package/src/api/workflow-resources.ts +6 -0
- package/src/extension/index.ts +40 -2
- package/src/extension/public-execution.ts +0 -1
- package/src/extension/rpc.ts +4 -21
- package/src/extension/schemas.ts +8 -6
- package/src/extension/tool-description.ts +6 -5
- package/src/intercom/native-supervisor-channel.ts +82 -54
- package/src/runs/background/active-async-capacity.ts +18 -18
- package/src/runs/background/async-job-tracker.ts +35 -3
- package/src/runs/background/async-status-snapshot.ts +10 -12
- package/src/runs/background/async-status.ts +17 -9
- package/src/runs/background/auto-drain.ts +40 -29
- package/src/runs/background/chain-root-attachment.ts +8 -0
- package/src/runs/background/control-channel.ts +78 -44
- package/src/runs/background/notify.ts +86 -12
- package/src/runs/background/owned-process-tree.ts +6 -6
- package/src/runs/background/process-terminal.ts +23 -23
- package/src/runs/background/run-child-session.ts +60 -32
- package/src/runs/background/run-status.ts +75 -5
- package/src/runs/background/runner-aliases.ts +18 -7
- package/src/runs/background/runner-child-launch.ts +86 -0
- package/src/runs/background/stale-run-reconciler.ts +3 -1
- package/src/runs/background/subagent-runner.ts +412 -209
- package/src/runs/background/subagent-wait.ts +3 -0
- package/src/runs/background/wait-completions.ts +4 -0
- package/src/runs/foreground/async-steering-action.ts +19 -0
- package/src/runs/foreground/execution.ts +101 -25
- package/src/runs/foreground/subagent-executor.ts +474 -197
- package/src/runs/foreground/workflow-detach-reconcile.ts +8 -5
- package/src/runs/foreground/workflow-foreground-steering.ts +56 -2
- package/src/runs/shared/acceptance.ts +2 -2
- package/src/runs/shared/agent-contract.ts +1 -1
- package/src/runs/shared/async-status-projection.ts +47 -47
- package/src/runs/shared/child-hooks.ts +151 -2
- package/src/runs/shared/child-launch.ts +18 -13
- package/src/runs/shared/child-session.ts +42 -6
- package/src/runs/shared/child-tool-plan.ts +2 -2
- package/src/runs/shared/completion-evidence.ts +2 -2
- package/src/runs/shared/completion-guard.ts +1 -0
- package/src/runs/shared/host-step-status.ts +11 -11
- package/src/runs/shared/llm-intent-arbiter.ts +10 -9
- package/src/runs/shared/model-fallback.ts +10 -6
- package/src/runs/shared/nested-events.ts +5 -5
- package/src/runs/shared/orca-progress-tabs.ts +6 -0
- package/src/runs/shared/parallel-handoff.ts +57 -12
- package/src/runs/shared/parallel-utils.ts +2 -2
- package/src/runs/shared/readonly-drain-observation.ts +42 -0
- package/src/runs/shared/readonly-model-continuation.ts +69 -0
- package/src/runs/shared/readonly-session-evidence.ts +307 -0
- package/src/runs/shared/run-fanout-budget.ts +8 -8
- package/src/runs/shared/runtime-acknowledged-extensions.ts +3 -3
- package/src/runs/shared/subagent-prompt-runtime.ts +13 -3
- package/src/runs/shared/worktree-setup-command.ts +190 -0
- package/src/runs/shared/worktree.ts +332 -204
- package/src/shared/types.ts +81 -60
- package/src/shared/utils.ts +7 -2
- package/src/shared/workflow-child-permit.ts +18 -13
- package/src/tui/fleet.ts +11 -5
- package/src/tui/render.ts +23 -5
- package/src/workflows/chat-progress.ts +3 -3
- package/src/workflows/scripted-workflow.ts +38 -10
- package/src/workflows/workflow-checklist.ts +11 -15
- package/src/workflows/workflow-child-summary.ts +57 -8
- package/src/workflows/workflow-preflight.ts +19 -19
- package/src/workflows/workflow-receipt.ts +3 -3
- package/src/workflows/workflow-resources.ts +96 -21
- package/src/workflows/workflow-settlement.ts +3 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-subagents",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.66.0",
|
|
4
4
|
"description": "Pi extension for single-agent delegation and scripted multi-agent workflows",
|
|
5
5
|
"author": "Nico Bailon",
|
|
6
6
|
"license": "MIT",
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"./agents": "./src/api/agents.ts",
|
|
14
14
|
"./delegation": "./src/api/delegation.ts",
|
|
15
15
|
"./capability-ceiling": "./src/api/capability-ceiling.ts",
|
|
16
|
+
"./workflow-resources": "./src/api/workflow-resources.ts",
|
|
16
17
|
"./preflight": "./src/api/preflight.ts",
|
|
17
18
|
"./control-channel": "./src/api/control-channel.ts",
|
|
18
19
|
"./intercom-bridge": "./src/api/intercom-bridge.ts",
|
|
@@ -258,7 +258,7 @@ A cooperating terminal runtime can register read-only external records through `
|
|
|
258
258
|
|
|
259
259
|
### Scheduled subagent runs
|
|
260
260
|
|
|
261
|
-
Schedules are durable project records under `.pi/subagents/schedules/`. They are enabled by default; set `{ "scheduledRuns": { "enabled": false } }` in `~/.pi/agent/extensions/subagent/config.json` to disable them. Only schedule explicit work the user asked for.
|
|
261
|
+
Schedules are durable project records under `.pi/subagents/schedules/`. They are enabled by default; set `{ "scheduledRuns": { "enabled": false } }` in `~/.pi/agent/extensions/subagent/config.json` to disable them. Only schedule explicit work the user asked for. To keep schedules outside the project repository, set `{ "scheduledRuns": { "storeRoot": "~/.pi/subagent-schedules" } }` in the same config: `storeRoot` accepts an absolute path or a `~/`-prefixed path, and records land under `<storeRoot>/<sha256(path.resolve(cwd)) first 20 hex>/<scheduleId>/`.
|
|
262
262
|
|
|
263
263
|
```typescript
|
|
264
264
|
// One-shot reviewer
|
|
@@ -327,6 +327,19 @@ subagent({ action: "steer", id: "abc123", message: "Focus on the failing test."
|
|
|
327
327
|
|
|
328
328
|
The action waits up to three seconds for the child Pi session to accept the correlated user input and returns a request id with `delivered`, `scheduled`, `pending`, `partial`, `recovered`, or `failed` plus per-child states. Indexed pending children return `scheduled` immediately. Only a top-level single-child run may automatically interrupt after a missed acknowledgment and recover after confirmed pause within a further 15 seconds. Recovery preserves the original child contract and only its remaining deadline, turn, and tool budgets. If the session is missing, a budget is exhausted, the pause cannot be confirmed, or replacement launch fails, the source remains paused when pausing succeeded and the action returns the exact failure. Chain, parallel, and nested runs never auto-interrupt; inspect their per-child outcomes and handle failures explicitly. A late acknowledgment is recorded and cannot cancel committed recovery.
|
|
329
329
|
|
|
330
|
+
Steering supports three delivery modes via the `mode` parameter (`steer` is the default):
|
|
331
|
+
|
|
332
|
+
- `mode: "steer"` — interrupt the child at the next safe point of its current turn and deliver the message.
|
|
333
|
+
- `mode: "follow_up"` — do not interrupt; queue input through Pi's native follow-up path for the next turn boundary. Eligible completed retained workflow children (single-step runs in state `complete` with a stored session file) receive the message as a revival brief (`queueRevivalBrief`) when they are revived; paused children reject follow-up steering outright. The 20-message queue limit applies to retained revival briefs, not live follow-up input.
|
|
334
|
+
- `mode: "auto"` — same next-safe-point delivery path as `steer`, but without the automatic pause-and-revive recovery after a missed acknowledgment.
|
|
335
|
+
|
|
336
|
+
```typescript
|
|
337
|
+
subagent({ action: "steer", id: "abc123", mode: "follow_up", message: "After this step, also validate the config file." })
|
|
338
|
+
subagent({ action: "steer", id: "abc123", mode: "auto", message: "Switch to the failing test now." })
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
Direct input acceptance returns `delivered`, not proof of model compliance. A live follow-up acknowledgment reports `queued`, meaning Pi accepted it into its follow-up queue, not that it was delivered. The runtime does not provide a later correlated live queued-to-delivered receipt.
|
|
342
|
+
|
|
330
343
|
## Watchdog
|
|
331
344
|
|
|
332
345
|
The subagent watchdog is an **opt-in** adversarial change reviewer. It is not the
|
|
@@ -96,6 +96,7 @@ A minimal agent file looks like this:
|
|
|
96
96
|
name: my-agent
|
|
97
97
|
package: code-analysis
|
|
98
98
|
description: What this agent does
|
|
99
|
+
advertise: true
|
|
99
100
|
aliases: developer, coder
|
|
100
101
|
model: provider/model-id
|
|
101
102
|
thinking: high
|
|
@@ -111,7 +112,7 @@ skillPath: ./skills, ../shared-skills
|
|
|
111
112
|
Your system prompt here.
|
|
112
113
|
```
|
|
113
114
|
|
|
114
|
-
That is only a starting point. Omit `package` for the traditional unqualified runtime name. Common optional fields include:
|
|
115
|
+
That is only a starting point. Omit `package` for the traditional unqualified runtime name. Set `advertise: true` only when the parent should receive this agent's name and description before deciding whether to delegate; advertisement is off by default. Common optional fields include:
|
|
115
116
|
- `defaultProgress`
|
|
116
117
|
- `defaultReads`
|
|
117
118
|
- `output`
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
import type { ResolvedSubagentCapabilityCeiling } from "../runs/shared/capability-ceiling.ts";
|
|
3
|
+
import { isAgentAllowedByCapabilityCeiling } from "../runs/shared/capability-ceiling.ts";
|
|
4
|
+
import type { AgentConfig } from "./agents.ts";
|
|
5
|
+
|
|
6
|
+
const MAX_ADVERTISED_AGENTS = 16;
|
|
7
|
+
const MAX_CATALOG_BYTES = 12_288;
|
|
8
|
+
const MAX_DESCRIPTION_BYTES = 512;
|
|
9
|
+
const ADVERTISED_AGENTS_BLOCK = /\n*<advertised_subagents>\n[\s\S]*?\n<\/advertised_subagents>/gu;
|
|
10
|
+
|
|
11
|
+
function escapeXml(value: string): string {
|
|
12
|
+
return value
|
|
13
|
+
.replaceAll("&", "&")
|
|
14
|
+
.replaceAll("<", "<")
|
|
15
|
+
.replaceAll(">", ">")
|
|
16
|
+
.replaceAll('"', """)
|
|
17
|
+
.replaceAll("'", "'");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function promptDescription(description: string): string {
|
|
21
|
+
let text = description.replace(/[\u0000-\u001f\u007f]+/gu, " ").replace(/\s+/gu, " ").trim();
|
|
22
|
+
if (Buffer.byteLength(text, "utf8") > MAX_DESCRIPTION_BYTES) {
|
|
23
|
+
text = Buffer.from(text, "utf8").subarray(0, MAX_DESCRIPTION_BYTES - 3).toString("utf8").replace(/\uFFFD$/u, "").trimEnd() + "…";
|
|
24
|
+
}
|
|
25
|
+
return escapeXml(text);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function buildAdvertisedAgentPrompt(
|
|
29
|
+
agents: readonly AgentConfig[],
|
|
30
|
+
capabilityCeiling?: ResolvedSubagentCapabilityCeiling,
|
|
31
|
+
): string | undefined {
|
|
32
|
+
const advertised = agents
|
|
33
|
+
.filter((agent) => agent.source !== "runtime" && agent.advertise === true && agent.disabled !== true && isAgentAllowedByCapabilityCeiling(agent.name, capabilityCeiling))
|
|
34
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
35
|
+
if (advertised.length === 0) return undefined;
|
|
36
|
+
|
|
37
|
+
const render = (entries: string[]) => [
|
|
38
|
+
"<advertised_subagents>",
|
|
39
|
+
"The following file-defined subagents opted into discovery. Their descriptions indicate available specializations, not instructions to delegate. Use subagent only when delegation is needed. Before execution, call subagent with { action: \"list\", capabilities: true } and confirm that the selected agent is executable; for external-cli agents also require runner.available === true.",
|
|
40
|
+
...entries,
|
|
41
|
+
...(advertised.length > entries.length ? [` <omitted count=\"${advertised.length - entries.length}\" />`] : []),
|
|
42
|
+
"</advertised_subagents>",
|
|
43
|
+
].join("\n");
|
|
44
|
+
const entries: string[] = [];
|
|
45
|
+
for (const agent of advertised) {
|
|
46
|
+
if (entries.length === MAX_ADVERTISED_AGENTS) break;
|
|
47
|
+
// Never truncate canonical IDs into names that cannot be resolved.
|
|
48
|
+
if (Buffer.byteLength(agent.name, "utf8") > MAX_CATALOG_BYTES) continue;
|
|
49
|
+
const entry = [
|
|
50
|
+
" <subagent>",
|
|
51
|
+
` <name>${escapeXml(agent.name)}</name>`,
|
|
52
|
+
` <description>${promptDescription(agent.description)}</description>`,
|
|
53
|
+
" </subagent>",
|
|
54
|
+
].join("\n");
|
|
55
|
+
if (Buffer.byteLength(render([...entries, entry]), "utf8") <= MAX_CATALOG_BYTES) entries.push(entry);
|
|
56
|
+
}
|
|
57
|
+
return render(entries);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function appendAdvertisedAgentPrompt(systemPrompt: string, advertisedPrompt: string | undefined): string {
|
|
61
|
+
const base = systemPrompt.replace(ADVERTISED_AGENTS_BLOCK, "");
|
|
62
|
+
return advertisedPrompt ? `${base.trimEnd()}\n\n${advertisedPrompt}` : base;
|
|
63
|
+
}
|
|
@@ -42,7 +42,7 @@ import { listExternalJobProviders } from "../api/external-job-provider.ts";
|
|
|
42
42
|
|
|
43
43
|
type ManagementAction = "list" | "get" | "models" | "create" | "update" | "delete" | "eject" | "disable" | "enable" | "reset";
|
|
44
44
|
type ManagementScope = "user" | "project";
|
|
45
|
-
type ManagementContext = Pick<ExtensionContext, "cwd" | "modelRegistry"> & { model?: ExtensionContext["model"]; config?: ExtensionConfig; currentSessionId?: string; runtimeAgentOwner?: RuntimeAgentOwner };
|
|
45
|
+
type ManagementContext = Pick<ExtensionContext, "cwd" | "modelRegistry"> & { model?: ExtensionContext["model"]; config?: ExtensionConfig; currentSessionId?: string; runtimeAgentOwner?: RuntimeAgentOwner; onAgentsChanged?: () => void };
|
|
46
46
|
|
|
47
47
|
interface ManagementParams {
|
|
48
48
|
action?: string;
|
|
@@ -348,6 +348,7 @@ export function preservedAgentFrontmatterFields(agent: AgentConfig, cfg: Record<
|
|
|
348
348
|
if (hasKey(cfg, "name")) changed("name");
|
|
349
349
|
if (hasKey(cfg, "package")) changed("package");
|
|
350
350
|
if (hasKey(cfg, "description")) changed("description");
|
|
351
|
+
if (hasKey(cfg, "advertise")) changed("advertise");
|
|
351
352
|
if (hasKey(cfg, "aliases")) changed("alias", "aliases");
|
|
352
353
|
if (hasKey(cfg, "systemPrompt")) changed("systemPrompt");
|
|
353
354
|
if (hasKey(cfg, "runner")) changed("runner");
|
|
@@ -415,6 +416,11 @@ function parseTools(raw: string): { tools?: string[]; mcpDirectTools?: string[]
|
|
|
415
416
|
}
|
|
416
417
|
|
|
417
418
|
function applyAgentConfig(target: AgentConfig, cfg: Record<string, unknown>): string | undefined {
|
|
419
|
+
if (hasKey(cfg, "advertise")) {
|
|
420
|
+
if (cfg.advertise === "") delete target.advertise;
|
|
421
|
+
else if (typeof cfg.advertise === "boolean") target.advertise = cfg.advertise;
|
|
422
|
+
else return "config.advertise must be a boolean or empty string when provided.";
|
|
423
|
+
}
|
|
418
424
|
if (hasKey(cfg, "aliases")) {
|
|
419
425
|
if (cfg.aliases === false || cfg.aliases === "") delete target.aliases;
|
|
420
426
|
else if (typeof cfg.aliases === "string") {
|
|
@@ -1176,6 +1182,7 @@ export function handleCreate(params: ManagementParams, ctx: ManagementContext):
|
|
|
1176
1182
|
const sw = skillsWarning(ctx.cwd, agent);
|
|
1177
1183
|
if (sw) warnings.push(sw);
|
|
1178
1184
|
fs.writeFileSync(targetPath, serializeAgent(agent), "utf-8");
|
|
1185
|
+
ctx.onAgentsChanged?.();
|
|
1179
1186
|
return result([`Created agent '${runtimeName}' at ${targetPath}.`, ...warnings].join("\n"));
|
|
1180
1187
|
}
|
|
1181
1188
|
|
|
@@ -1241,6 +1248,7 @@ export function handleUpdate(params: ManagementParams, ctx: ManagementContext):
|
|
|
1241
1248
|
updated.filePath = renamed.filePath!;
|
|
1242
1249
|
}
|
|
1243
1250
|
fs.writeFileSync(updated.filePath, serializeAgent(updated, { preserveFrontmatterFields }), "utf-8");
|
|
1251
|
+
ctx.onAgentsChanged?.();
|
|
1244
1252
|
const headline = updated.name === oldName
|
|
1245
1253
|
? `Updated agent '${updated.name}' at ${updated.filePath}.`
|
|
1246
1254
|
: `Updated agent '${oldName}' to '${updated.name}' at ${updated.filePath}.`;
|
|
@@ -1254,6 +1262,7 @@ function handleDelete(params: ManagementParams, ctx: ManagementContext): AgentTo
|
|
|
1254
1262
|
if ("content" in targetOrError) return targetOrError;
|
|
1255
1263
|
const target = targetOrError;
|
|
1256
1264
|
fs.unlinkSync(target.filePath);
|
|
1265
|
+
ctx.onAgentsChanged?.();
|
|
1257
1266
|
return result(`Deleted agent '${target.name}' at ${target.filePath}.`);
|
|
1258
1267
|
}
|
|
1259
1268
|
|
|
@@ -1292,6 +1301,7 @@ function handleEject(params: ManagementParams, ctx: ManagementContext): AgentToo
|
|
|
1292
1301
|
return result(`Failed to read source agent at ${source.filePath}: ${message}`, true);
|
|
1293
1302
|
}
|
|
1294
1303
|
fs.writeFileSync(targetPath, content, "utf-8");
|
|
1304
|
+
ctx.onAgentsChanged?.();
|
|
1295
1305
|
return result(`Ejected agent '${runtimeName}' from ${source.source} to ${scope} scope at ${targetPath}. Edit it there to customize; it shadows the bundled ${source.source} agent of the same name.`);
|
|
1296
1306
|
}
|
|
1297
1307
|
|
|
@@ -1314,6 +1324,7 @@ function handleDisable(params: ManagementParams, ctx: ManagementContext): AgentT
|
|
|
1314
1324
|
const settingsPath = mergeBuiltinAgentOverride(ctx.cwd, runtimeName, scope, { disabled: true });
|
|
1315
1325
|
const after = resolveEffectiveAgent(discoverAgentsAll(ctx.cwd), raw).agent;
|
|
1316
1326
|
if (after?.disabled === true) {
|
|
1327
|
+
ctx.onAgentsChanged?.();
|
|
1317
1328
|
return result(`Disabled agent '${runtimeName}' via ${scope} settings override at ${settingsPath}. It is now hidden from runtime discovery and { action: "list" }.`);
|
|
1318
1329
|
}
|
|
1319
1330
|
return result(`Wrote a disabled override for '${runtimeName}' at ${settingsPath}, but the agent is still enabled. A higher-precedence ${after?.override?.scope ?? "project"} override is likely winning. Try agentScope: '${after?.override?.scope ?? "project"}'.`, true);
|
|
@@ -1338,6 +1349,7 @@ function handleEnable(params: ManagementParams, ctx: ManagementContext): AgentTo
|
|
|
1338
1349
|
const { path: settingsPath, removed } = removeBuiltinAgentOverrideFields(ctx.cwd, runtimeName, scope, ["disabled"]);
|
|
1339
1350
|
const after = resolveEffectiveAgent(discoverAgentsAll(ctx.cwd), raw).agent;
|
|
1340
1351
|
if (after && after.disabled !== true) {
|
|
1352
|
+
if (removed) ctx.onAgentsChanged?.();
|
|
1341
1353
|
if (removed) return result(`Enabled agent '${runtimeName}' (removed disabled override at ${settingsPath}).`);
|
|
1342
1354
|
return result(`Agent '${runtimeName}' is already enabled.`);
|
|
1343
1355
|
}
|
|
@@ -1385,6 +1397,7 @@ function handleReset(params: ManagementParams, ctx: ManagementContext): AgentToo
|
|
|
1385
1397
|
return result(`Agent '${runtimeName}' has no ${scope} customization to reset.${note} It is at its bundled ${bundled.source} default.`);
|
|
1386
1398
|
}
|
|
1387
1399
|
lines.push(`Reset agent '${runtimeName}' to its bundled ${bundled.source} default.`);
|
|
1400
|
+
ctx.onAgentsChanged?.();
|
|
1388
1401
|
return result(lines.join("\n"));
|
|
1389
1402
|
}
|
|
1390
1403
|
|
|
@@ -6,6 +6,7 @@ export const KNOWN_FIELDS = new Set([
|
|
|
6
6
|
"name",
|
|
7
7
|
"package",
|
|
8
8
|
"description",
|
|
9
|
+
"advertise",
|
|
9
10
|
"alias",
|
|
10
11
|
"aliases",
|
|
11
12
|
"tools",
|
|
@@ -62,6 +63,7 @@ export function serializeAgent(config: AgentConfig, options: SerializeAgentOptio
|
|
|
62
63
|
lines.push(`name: ${frontmatterNameForConfig(config)}`);
|
|
63
64
|
if (config.packageName) lines.push(`package: ${config.packageName}`);
|
|
64
65
|
lines.push(`description: ${config.description}`);
|
|
66
|
+
if (config.advertise === true || preserve("advertise")) lines.push(`advertise: ${config.advertise === true ? "true" : "false"}`);
|
|
65
67
|
const aliasesValue = joinComma(config.aliases);
|
|
66
68
|
if (aliasesValue || preserve("alias", "aliases")) lines.push(`aliases: ${aliasesValue ?? ""}`);
|
|
67
69
|
|
package/src/agents/agents.ts
CHANGED
|
@@ -135,6 +135,7 @@ export interface AgentConfig {
|
|
|
135
135
|
packageSourceVersion?: string;
|
|
136
136
|
packageSourceRoot?: string;
|
|
137
137
|
description: string;
|
|
138
|
+
advertise?: boolean;
|
|
138
139
|
aliases?: string[];
|
|
139
140
|
tools?: string[];
|
|
140
141
|
excludeTools?: string[];
|
|
@@ -1981,6 +1982,12 @@ function loadAgentsFromDefinitionFiles(files: AgentDefinitionFile[], source: Age
|
|
|
1981
1982
|
|
|
1982
1983
|
const runner = parseAgentRunnerFrontmatter(frontmatter.runner, localName);
|
|
1983
1984
|
validateExternalRunnerProfile(frontmatter, localName, runner);
|
|
1985
|
+
let advertise: boolean | undefined;
|
|
1986
|
+
if (frontmatter.advertise !== undefined) {
|
|
1987
|
+
if (frontmatter.advertise === "true") advertise = true;
|
|
1988
|
+
else if (frontmatter.advertise === "false") advertise = false;
|
|
1989
|
+
else throw new Error(`Agent '${localName}' has invalid advertise frontmatter; expected true or false.`);
|
|
1990
|
+
}
|
|
1984
1991
|
const rawTools = parseFrontmatterList(frontmatter.tools);
|
|
1985
1992
|
const parsedTools = splitToolList(rawTools);
|
|
1986
1993
|
const tools = parsedTools.tools ?? [];
|
|
@@ -2105,6 +2112,7 @@ function loadAgentsFromDefinitionFiles(files: AgentDefinitionFile[], source: Age
|
|
|
2105
2112
|
...(packageSource?.packageVersion ? { packageSourceVersion: packageSource.packageVersion } : {}),
|
|
2106
2113
|
...(packageSource?.packageRoot ? { packageSourceRoot: packageSource.packageRoot } : {}),
|
|
2107
2114
|
description: frontmatter.description,
|
|
2115
|
+
...(advertise !== undefined ? { advertise } : {}),
|
|
2108
2116
|
...(aliases !== undefined ? { aliases } : {}),
|
|
2109
2117
|
...(rawTools !== undefined ? { tools } : {}),
|
|
2110
2118
|
...(excludeTools !== undefined ? { excludeTools } : {}),
|
package/src/api/shared-types.ts
CHANGED
package/src/extension/index.ts
CHANGED
|
@@ -19,7 +19,8 @@ import * as path from "node:path";
|
|
|
19
19
|
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
20
20
|
import { keyText, type ExtensionAPI, type ExtensionContext, type ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
21
21
|
import { Box, Container, Spacer, Text, truncateToWidth, visibleWidth, wrapTextWithAnsi, type Component } from "@earendil-works/pi-tui";
|
|
22
|
-
import { discoverAgentSnapshot, discoverAgents, type AgentConfig, type AgentScope } from "../agents/agents.ts";
|
|
22
|
+
import { clearAgentDiscoveryCache, discoverAgentSnapshot, discoverAgents, type AgentConfig, type AgentScope } from "../agents/agents.ts";
|
|
23
|
+
import { appendAdvertisedAgentPrompt, buildAdvertisedAgentPrompt } from "../agents/advertised-agent-prompt.ts";
|
|
23
24
|
import { clearRuntimeAgentsForPi, listRuntimeAgentConfigs, mergeRuntimeAgents } from "../agents/runtime-agent-registry.ts";
|
|
24
25
|
import { registerRuntimeAgentEventListener } from "../agents/runtime-agent-events.ts";
|
|
25
26
|
import { ensureAccessibleDir } from "../shared/accessible-dir.ts";
|
|
@@ -489,7 +490,9 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
489
490
|
}, run);
|
|
490
491
|
};
|
|
491
492
|
|
|
492
|
-
const supervisorChannel = createNativeSupervisorChannel(pi, state
|
|
493
|
+
const supervisorChannel = createNativeSupervisorChannel(pi, state, {
|
|
494
|
+
getCurrentOwnerStates: () => executor.getCurrentSupervisorOwnerStates(),
|
|
495
|
+
});
|
|
493
496
|
const waitSubscriptionManager = createWaitSubscriptionManager(pi, state);
|
|
494
497
|
const mainWatchdog = registerMainWatchdog(pi);
|
|
495
498
|
const resultDeliveryOwnership = createResultDeliveryOwnership(state);
|
|
@@ -531,6 +534,15 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
531
534
|
resolveCapabilityCeiling: (sessionId) => resolveCurrentSubagentCapabilityCeiling(sessionId),
|
|
532
535
|
});
|
|
533
536
|
let refreshResultDelivery = () => {};
|
|
537
|
+
let advertisedAgents: AgentConfig[] = [];
|
|
538
|
+
let advertisedContext: Pick<ExtensionContext, "cwd" | "model"> | undefined;
|
|
539
|
+
const refreshAdvertisedAgents = () => {
|
|
540
|
+
advertisedAgents = [];
|
|
541
|
+
if (!advertisedContext) return;
|
|
542
|
+
clearAgentDiscoveryCache();
|
|
543
|
+
advertisedAgents = discoverAgents(advertisedContext.cwd, "both", advertisedContext.model?.provider).agents
|
|
544
|
+
.filter((agent) => agent.advertise === true);
|
|
545
|
+
};
|
|
534
546
|
const hasResultDeliveryDemand = () => {
|
|
535
547
|
if ([...state.asyncJobs.values()].some((job) => job.status === "queued" || job.status === "running")) return true;
|
|
536
548
|
if (state.foregroundControls.size > 0) return true;
|
|
@@ -609,7 +621,16 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
609
621
|
getSubagentSessionRoot,
|
|
610
622
|
expandTilde,
|
|
611
623
|
discoverAgents: discoverAgentsForRuntime,
|
|
624
|
+
onAgentsChanged: () => {
|
|
625
|
+
try {
|
|
626
|
+
refreshAdvertisedAgents();
|
|
627
|
+
} catch (error) {
|
|
628
|
+
// The mutation already persisted. Withdraw stale guidance, not its result.
|
|
629
|
+
console.error("Failed to refresh advertised agents; catalog withdrawn until refresh:", error);
|
|
630
|
+
}
|
|
631
|
+
},
|
|
612
632
|
activateSupervisorTransport: () => supervisorChannel.activateTransport(),
|
|
633
|
+
findPendingAsks: (target) => supervisorChannel.findPendingAsks(target),
|
|
613
634
|
refreshResultDelivery: () => refreshResultDelivery(),
|
|
614
635
|
trackRetainedNestedRoute: undefined,
|
|
615
636
|
};
|
|
@@ -778,6 +799,16 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
778
799
|
|
|
779
800
|
pi.registerTool(tool);
|
|
780
801
|
|
|
802
|
+
pi.on("before_agent_start", (event, ctx) => {
|
|
803
|
+
const selectedTools = event.systemPromptOptions.selectedTools ?? pi.getActiveTools();
|
|
804
|
+
const sessionId = state.currentSessionId ?? resolveCurrentSessionId(ctx.sessionManager);
|
|
805
|
+
const advertisedPrompt = selectedTools.includes("subagent")
|
|
806
|
+
? buildAdvertisedAgentPrompt(advertisedAgents, resolveCurrentSubagentCapabilityCeiling(sessionId))
|
|
807
|
+
: undefined;
|
|
808
|
+
const systemPrompt = appendAdvertisedAgentPrompt(event.systemPrompt, advertisedPrompt);
|
|
809
|
+
if (systemPrompt !== event.systemPrompt) return { systemPrompt };
|
|
810
|
+
});
|
|
811
|
+
|
|
781
812
|
registerWaitTool(pi, state, waitToolConfig.enabled, waitSubscriptionManager, waitToolConfig.defaultTimeoutMs);
|
|
782
813
|
|
|
783
814
|
pi.on("agent_end", async (_event, ctx) => {
|
|
@@ -912,6 +943,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
912
943
|
const previousRuntimeSessionId = state.currentSessionId;
|
|
913
944
|
resultDeliveryOwnership.claimPredecessor(previousSessionFile, previousRuntimeSessionId);
|
|
914
945
|
state.currentSessionId = resolveCurrentSessionId(ctx.sessionManager);
|
|
946
|
+
state.supervisorOwnerSessionId = ctx.sessionManager.getSessionId() || null;
|
|
915
947
|
transitionResultDelivery();
|
|
916
948
|
state.parentSessionFile = ctx.sessionManager.getSessionFile();
|
|
917
949
|
state.trustedSessionFileRoot = state.parentSessionFile ? path.join(getAgentDir(), "sessions") : undefined;
|
|
@@ -1029,6 +1061,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
1029
1061
|
promptTemplateBridge.dispose();
|
|
1030
1062
|
state.widgetsSuspended = false;
|
|
1031
1063
|
state.currentSessionId = null;
|
|
1064
|
+
state.supervisorOwnerSessionId = null;
|
|
1032
1065
|
state.statusProjectionSessionId = null;
|
|
1033
1066
|
state.parentSessionFile = null;
|
|
1034
1067
|
parentSessionEnvValue = null;
|
|
@@ -1141,4 +1174,9 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
1141
1174
|
}
|
|
1142
1175
|
await herdrStatusBridge.flush();
|
|
1143
1176
|
});
|
|
1177
|
+
|
|
1178
|
+
pi.on("session_start", (_event, ctx) => {
|
|
1179
|
+
advertisedContext = { cwd: ctx.cwd, model: ctx.model };
|
|
1180
|
+
refreshAdvertisedAgents();
|
|
1181
|
+
});
|
|
1144
1182
|
}
|
|
@@ -73,7 +73,6 @@ export function normalizePublicSubagentExecution<T extends PublicSubagentExecuti
|
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
75
|
if (params.baseRef !== undefined) {
|
|
76
|
-
if (typeof params.baseRef !== "string") return { ok: false, error: "baseRef must be a valid Git ref.", mode: params.action === undefined ? "workflow" : "management" };
|
|
77
76
|
try {
|
|
78
77
|
normalizeWorktreeBaseRef(params.baseRef);
|
|
79
78
|
} catch (error) {
|
package/src/extension/rpc.ts
CHANGED
|
@@ -643,27 +643,10 @@ function stopAsyncRun(
|
|
|
643
643
|
message: `Stop requested for async run ${initialRunId}.`,
|
|
644
644
|
};
|
|
645
645
|
}
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
kill: options.kill,
|
|
651
|
-
now: options.now,
|
|
652
|
-
source: "rpc-stop",
|
|
653
|
-
...(child ? { targetIndex: child.index, childId: child.id } : {}),
|
|
654
|
-
});
|
|
655
|
-
} catch (error) {
|
|
656
|
-
throw new SubagentRpcError("execution_failed", error instanceof Error ? error.message : String(error));
|
|
657
|
-
}
|
|
658
|
-
if (child) emitChildStopping(initialRunId, location.asyncDir, child);
|
|
659
|
-
return {
|
|
660
|
-
runId: initialRunId,
|
|
661
|
-
asyncDir: location.asyncDir,
|
|
662
|
-
previousState: initialStatus.state,
|
|
663
|
-
state: "stopping",
|
|
664
|
-
...(child ? { childId: child.id } : {}),
|
|
665
|
-
message: child ? `Stop requested for child ${child.id} in async run ${initialRunId}.` : `Stop requested for async run ${initialRunId}.`,
|
|
666
|
-
};
|
|
646
|
+
// Workflow controls live in-process; a persisted run directory cannot restore them.
|
|
647
|
+
throw new SubagentRpcError("invalid_state", child
|
|
648
|
+
? `Child '${child.id}' in workflow ${initialRunId} has no live stop callback available.`
|
|
649
|
+
: `Workflow ${initialRunId} has no live run controller available to stop.`);
|
|
667
650
|
}
|
|
668
651
|
|
|
669
652
|
let status;
|
package/src/extension/schemas.ts
CHANGED
|
@@ -77,6 +77,8 @@ const AcceptanceEvidenceKinds = [
|
|
|
77
77
|
"manual-notes",
|
|
78
78
|
];
|
|
79
79
|
|
|
80
|
+
// Provider boolean branches intentionally overapproximate false-only runtime inputs.
|
|
81
|
+
// Restricted function-declaration converters only support string enum members.
|
|
80
82
|
const AcceptanceOverride = Type.Unsafe({
|
|
81
83
|
anyOf: [
|
|
82
84
|
{ type: "string", enum: ["auto", "attested", "checked"] },
|
|
@@ -89,10 +91,10 @@ const AcceptanceOverride = Type.Unsafe({
|
|
|
89
91
|
{
|
|
90
92
|
type: "string",
|
|
91
93
|
},
|
|
92
|
-
{ type: "boolean"
|
|
94
|
+
{ type: "boolean" },
|
|
93
95
|
{ type: "object", additionalProperties: true },
|
|
94
96
|
],
|
|
95
|
-
description: `Optional acceptance policy. Prefer an inline JSON object. JSON-encoded object strings are tolerated only during input normalization; invalid strings fail closed. Reviewer/read-only calls, omit acceptance. { level: "checked", evidence: ["commands-run", "changed-files"] }. Supported evidence kinds: ${AcceptanceEvidenceKinds.join(",")}. acceptance.review.required.`,
|
|
97
|
+
description: `Optional acceptance policy. false disables acceptance; true is invalid. Prefer an inline JSON object. JSON-encoded object strings are tolerated only during input normalization; invalid strings fail closed. Reviewer/read-only calls, omit acceptance. { level: "checked", evidence: ["commands-run", "changed-files"] }. Supported evidence kinds: ${AcceptanceEvidenceKinds.join(",")}. acceptance.review.required.`,
|
|
96
98
|
});
|
|
97
99
|
|
|
98
100
|
const AgentContractOverride = Type.Object({
|
|
@@ -149,7 +151,7 @@ const WorkflowPreflightOverride = Type.Object({
|
|
|
149
151
|
version: Type.Integer({ minimum: 1, maximum: 1 }),
|
|
150
152
|
coverage: Type.Optional(Type.String({ enum: ["complete", "partial"] })),
|
|
151
153
|
lanes: Type.Array(WorkflowPreflightLane, { maxItems: 64 }),
|
|
152
|
-
}, { additionalProperties: false, description: "Bounded display-only lane hints for workflow launch/status.
|
|
154
|
+
}, { additionalProperties: false, description: "Bounded display-only lane hints for workflow launch/status. Coverage mismatches warn but never change launch authority or execution." });
|
|
153
155
|
|
|
154
156
|
// Parallel task item (within a parallel step)
|
|
155
157
|
export const ParallelTaskSchema = Type.Object({
|
|
@@ -257,7 +259,7 @@ export const ChainItem = Type.Object({
|
|
|
257
259
|
const MissionLaunchOverride = Type.Unsafe({
|
|
258
260
|
anyOf: [
|
|
259
261
|
{ type: "object", additionalProperties: true },
|
|
260
|
-
{ type: "boolean"
|
|
262
|
+
{ type: "boolean" },
|
|
261
263
|
],
|
|
262
264
|
});
|
|
263
265
|
const MissionUpdateOverride = Type.Unsafe({ type: "object", additionalProperties: true });
|
|
@@ -317,7 +319,7 @@ const SubagentParamProperties = {
|
|
|
317
319
|
scope: Type.Optional(Type.String({ enum: ["session", "user", "project"], description: "Scope for action='watchdog.configure'. Defaults to session to avoid persistent settings writes unless user/project is explicit." })),
|
|
318
320
|
target: Type.Optional(Type.String({ enum: ["main", "children", "child"], description: "Target for watchdog actions." })),
|
|
319
321
|
focus: Type.Optional(Type.Boolean({ description: "Focus the new Herdr pane for inspector.open or project.open." })),
|
|
320
|
-
thinking: Type.Optional(Type.Unsafe({ anyOf: [{ type: "string" }, { type: "boolean"
|
|
322
|
+
thinking: Type.Optional(Type.Unsafe({ anyOf: [{ type: "string" }, { type: "boolean" }], description: "Thinking level for action='watchdog.configure' only (off/minimal/low/medium/high/xhigh/max, inherit, or false for off; true is invalid). Ignored on dispatch; set per-run child thinking with a suffix on the model string, e.g. model: 'provider/id:high'." })),
|
|
321
323
|
at: Type.Optional(Type.String({ description: "One-shot trigger for action='schedule.create': a relative delay such as '+10m' or an ISO timestamp with timezone." })),
|
|
322
324
|
every: Type.Optional(Type.String({ description: "Fixed recurring interval for action='schedule.create', such as '30m', '6h', '2d', or '2w'." })),
|
|
323
325
|
sessionOnly: Type.Optional(Type.Boolean()),
|
|
@@ -326,7 +328,7 @@ const SubagentParamProperties = {
|
|
|
326
328
|
overlap: Type.Optional(Type.String({ enum: ["skip"], description: "Overlap policy. This slice supports skip only." })),
|
|
327
329
|
catchUp: Type.Optional(Type.String({ enum: ["none", "latest"], description: "Missed occurrence policy for recurring schedules. Defaults to latest." })),
|
|
328
330
|
missionId: Type.Optional(Type.String({ description: "Mission id." })),
|
|
329
|
-
mission: Type.Optional(Type.Unsafe({ ...MissionLaunchOverride, description: "Mission object, or false for no mission. Set exactly one non-empty title or summary; objective and labels are optional. goal may only be true and then requires budget.tokens." })),
|
|
331
|
+
mission: Type.Optional(Type.Unsafe({ ...MissionLaunchOverride, description: "Mission object, or false for no mission; true is invalid. Set exactly one non-empty title or summary; objective and labels are optional. goal may only be true and then requires budget.tokens." })),
|
|
330
332
|
missionUpdate: Type.Optional(Type.Unsafe({ ...MissionUpdateOverride, description: "Mission update: objective, goal false or {paused:boolean}, budget, summary, labels, decisions, artifacts, or delivery receipts." })),
|
|
331
333
|
missionStatus: Type.Optional(Type.String({ description: "Mission status." })),
|
|
332
334
|
missionScope: Type.Optional(Type.String({ description: "Mission list scope: project (default) or global pointer index." })),
|
|
@@ -11,11 +11,12 @@ const AGENT_SELECTION_GUIDANCE = "Before execution, call { action: \"list\", cap
|
|
|
11
11
|
const WORKFLOW_RESUME_KEY_GUIDANCE = "Each workflow key identifies one result lane: use a new stable workflow key for every distinct retained resume pass; same-key calls are reused only when launch parameters are identical, and incompatible parameters are rejected.";
|
|
12
12
|
const WORKFLOW_OUTPUT_BINDING_GUIDANCE = "For durable workflow child files, set output on runs.run/runs.all; task filename prose is not an output declaration, and return the child's outputReference, outputPathMapping, or artifactPaths instead of inventing a literal path.";
|
|
13
13
|
const WORKFLOW_LANES_GUIDANCE = "For bounded parallel sequential chains, use runs.lanes([{key,stages:[{key,agent,task},{key,resume:'previous',task},...]}]); first stages run together, later stages sequence per lane, and the bounded board reports lane-local failures. Only an explicit structuredOutput.verdict === 'blocked' blocks a successful stage; reviewer prose is not parsed.";
|
|
14
|
+
const WORKTREE_BASE_REF_GUIDANCE = "baseRef must be HEAD or a supported named ref such as refs/heads/main; full 40/64-character commit IDs and revision expressions such as HEAD~1 are unsupported. Omitted baseRef defaults to HEAD resolved at worktree allocation. The source checkout must still be clean.";
|
|
14
15
|
const WORKFLOW_SCRIPT_PORTABILITY_GUIDANCE = "workflowScript rejects nested async function, arrow, and method helpers; use top-level await, plain helper functions that return runs.run(...), or explicit Promise chains instead.";
|
|
15
16
|
const WORKFLOW_RESOURCE_GUIDANCE = "For permission/policy-extension interoperability, use an extension-owned named resource such as {workflow:'review',args:{task:'...'}} or {workflow:'run-ci',args:{command:'npm test'}}. The host resolves the script and authority internally so policy can distinguish it from raw workflowScript/workflowScriptPath; args are bounded plain data, and do not combine workflow with agent, task, workflowScript, or workflowScriptPath.";
|
|
16
|
-
const WORKFLOW_HOST_GUIDANCE = "For permission-sensitive host calls, use an extension-owned resource such as {workflow:'run-ci',args:{command:'npm test'}}; raw workflowScript/workflowScriptPath have unknown resource provenance and cannot use runs.host. In a resource that grants it, await runs.host(key,{kind:'command',command,timeoutMs,output?,role?,provider?}). runs.host has no per-step cwd: commands and relative output paths use the workflow cwd; set cwd on the outer subagent request instead (for example, {cwd:'/path/to/worktree',workflowScript:'...'}), or put a trusted directory change in the command (for example, 'cd /path/to/worktree && npm test').
|
|
17
|
+
const WORKFLOW_HOST_GUIDANCE = "For permission-sensitive host calls, use an extension-owned resource such as {workflow:'run-ci',args:{command:'npm test'}}; raw workflowScript/workflowScriptPath have unknown resource provenance and cannot use runs.host. In a resource that grants it, await runs.host(key,{kind:'command',command,timeoutMs,output?,role?,provider?}). runs.host has no per-step cwd: commands and relative output paths use the workflow cwd; set cwd on the outer subagent request instead (for example, {cwd:'/path/to/worktree',workflowScript:'...'}), or put a trusted directory change in the command (for example, 'cd /path/to/worktree && npm test'). runs.host supports only command steps; output is bounded and command failure fails the workflow.";
|
|
17
18
|
|
|
18
|
-
export const DEFAULT_SUBAGENT_TOOL_DESCRIPTION = `Delegate to configured subagents. For execution, omit action and use {agent, task?} for one child, workflowScript for inline orchestration, workflowScriptPath to load a script from the request cwd, or a named workflow resource for permission/policy-aware execution. ${WORKFLOW_RESOURCE_GUIDANCE} ${AGENT_SELECTION_GUIDANCE} The script inputs are mutually exclusive. Use action:'validate' with either script input to check it without launching children. For multi-step or parallel work, make exactly one top-level subagent call with async:true; launch children only inside that workflow and do not make another top-level call for them. Use runs.run('key',{agent,task}) for one child, await runs.all([{key:'a',agent:'reviewer',task:'...'},{key:'b',agent:'reviewer',task:'...'}]) for ordinary parallel children, and read its ordered array result with indexes, destructuring, or .map(...), not by key property. ${WORKFLOW_SCRIPT_PORTABILITY_GUIDANCE} ${WORKFLOW_LANES_GUIDANCE} ${WORKFLOW_HOST_GUIDANCE} ${EXTERNAL_CLI_RUNNER_GUIDANCE} ${SUBAGENT_FAILURE_RECOVERY_GUIDANCE} Use action only for management/control. Use guide or the pi-subagents skill for advanced workflow details.`;
|
|
19
|
+
export const DEFAULT_SUBAGENT_TOOL_DESCRIPTION = `Delegate to configured subagents. For execution, omit action and use {agent, task?} for one child, workflowScript for inline orchestration, workflowScriptPath to load a script from the request cwd, or a named workflow resource for permission/policy-aware execution. ${WORKFLOW_RESOURCE_GUIDANCE} ${AGENT_SELECTION_GUIDANCE} The script inputs are mutually exclusive. Use action:'validate' with either script input to check it without launching children. For multi-step or parallel work, make exactly one top-level subagent call with async:true; launch children only inside that workflow and do not make another top-level call for them. Use runs.run('key',{agent,task}) for one child, await runs.all([{key:'a',agent:'reviewer',task:'...'},{key:'b',agent:'reviewer',task:'...'}]) for ordinary parallel children, and read its ordered array result with indexes, destructuring, or .map(...), not by key property. ${WORKFLOW_SCRIPT_PORTABILITY_GUIDANCE} ${WORKFLOW_LANES_GUIDANCE} ${WORKFLOW_HOST_GUIDANCE} ${WORKTREE_BASE_REF_GUIDANCE} ${EXTERNAL_CLI_RUNNER_GUIDANCE} ${SUBAGENT_FAILURE_RECOVERY_GUIDANCE} Use action only for management/control. Use guide or the pi-subagents skill for advanced workflow details.`;
|
|
19
20
|
|
|
20
21
|
export const SUBAGENT_TOOL_PROMPT_SNIPPET = "Delegate to subagents; orchestrate in one workflowScript call.";
|
|
21
22
|
|
|
@@ -49,7 +50,7 @@ EXECUTION:
|
|
|
49
50
|
• ${AGENT_SELECTION_GUIDANCE}
|
|
50
51
|
• When passing an explicit model to a child (on the call or a runs.run/runs.all item), first call { action: "models" } and copy an exact provider/id; bare ids resolve only when unique in the registry, and agent names (e.g. gpt-pro, advisor) are not model ids. Set per-run thinking with a suffix on the model string (e.g. provider/id:high; off/minimal/low/medium/high/xhigh/max); the suffix wins over the agent's thinking default. The thinking field only applies to action='watchdog.configure' and is ignored on dispatch.
|
|
51
52
|
• SINGLE CHILD: { agent:"worker", task:"..." }. This structured form starts exactly one direct child. Fields such as model, context, cwd, worktree, output, budgets, acceptance, and async apply to that child. Do not combine agent/task with action, workflowScript, or workflowScriptPath.
|
|
52
|
-
• WORKFLOW SCRIPT: { workflowScript: "return runs.run('main', {agent:'worker', task:'...'})" }. Use stable-key runs.run for one child and await runs.all([{key,agent,task}, ...]) for ordinary parallel children. runs.all resolves to an ordered array, not a key map, so use results[0], array destructuring, or results.map((result) => result.output), not results.<key>. Do not read .output from unawaited runs.run launches. Stored runs.run promises are only for advanced rolling fanout and each must later be observed with direct await, Promise.race, or Promise.all. Ordinary JavaScript provides sequence, branching, filtering, retries, and aggregation. workflowScript is an ordinary JavaScript statement body, so use an explicit return for a useful result. Use top-level await, plain helper functions, or explicit Promise chains; nested async function, arrow, and method helpers are rejected. For task text with Markdown fences or shell blocks, build quoted lines instead of nesting raw template literals: \`const task=["Run:","\`\`\`bash","npm test","\`\`\`"].join("\\n")\`. Scripts normally start async unless config sets asyncByDefault:false; set async:true explicitly when async behavior matters. Pass async:false only when the parent must block until completion, never for final reviews or gates. Same-repo blocking workflows default to a live in-chat card; explicit live-card requires same-repository async:false, so async workflows should omit chatProgress or use auto/off. Workflow-level child controls default onto each runs.run launch, and explicit child fields override them. Use {action:"children.list"} to list recent retained workflow children with resumable/not-resumable reasons. Resume only rows reported resumable. For a simple follow-up or implementation challenge, use {action:"resume", id:"run-id", message:"..."}. Resume keeps the stored agent/model/tool contract. If no resumable child is listed, launch a same-role fallback challenge and label it as fallback. Inside workflowScript, continue one with runs.run(key, {resume:"run-id", task:"follow-up"}); workflow resumes wait for completed output, and loops must continue from each latest returned runId. Await runs.steer(key, message, {mode?, index?, ackTimeoutMs?}) to guide a prior keyed child without exposing its run id; receipts are queued, delivered, missed, or failed. Always await or return runs.steer. For repository mutation lanes, set worktree:true on the workflow or individual runs.run/runs.all item for managed isolation; each parallel child gets a separate worktree and handoff artifact.
|
|
53
|
+
• WORKFLOW SCRIPT: { workflowScript: "return runs.run('main', {agent:'worker', task:'...'})" }. Use stable-key runs.run for one child and await runs.all([{key,agent,task}, ...]) for ordinary parallel children. runs.all resolves to an ordered array, not a key map, so use results[0], array destructuring, or results.map((result) => result.output), not results.<key>. Do not read .output from unawaited runs.run launches. Stored runs.run promises are only for advanced rolling fanout and each must later be observed with direct await, Promise.race, or Promise.all. Ordinary JavaScript provides sequence, branching, filtering, retries, and aggregation. workflowScript is an ordinary JavaScript statement body, so use an explicit return for a useful result. Use top-level await, plain helper functions, or explicit Promise chains; nested async function, arrow, and method helpers are rejected. For task text with Markdown fences or shell blocks, build quoted lines instead of nesting raw template literals: \`const task=["Run:","\`\`\`bash","npm test","\`\`\`"].join("\\n")\`. Scripts normally start async unless config sets asyncByDefault:false; set async:true explicitly when async behavior matters. Pass async:false only when the parent must block until completion, never for final reviews or gates. Same-repo blocking workflows default to a live in-chat card; explicit live-card requires same-repository async:false, so async workflows should omit chatProgress or use auto/off. Workflow-level child controls default onto each runs.run launch, and explicit child fields override them. Use {action:"children.list"} to list recent retained workflow children with resumable/not-resumable reasons. Resume only rows reported resumable. For a simple follow-up or implementation challenge, use {action:"resume", id:"run-id", message:"..."}. Resume keeps the stored agent/model/tool contract. If no resumable child is listed, launch a same-role fallback challenge and label it as fallback. Inside workflowScript, continue one with runs.run(key, {resume:"run-id", task:"follow-up"}); workflow resumes wait for completed output, and loops must continue from each latest returned runId. Await runs.steer(key, message, {mode?, index?, ackTimeoutMs?}) to guide a prior keyed child without exposing its run id; receipts are queued, delivered, missed, or failed. Always await or return runs.steer. For repository mutation lanes, set worktree:true on the workflow or individual runs.run/runs.all item for managed isolation; each parallel child gets a separate worktree and handoff artifact. ${WORKTREE_BASE_REF_GUIDANCE} A workflow usageBudget is enforced once across the workflow. Available globals are runs.run, runs.all, runs.steer, runs.status, runs.ref/refs, emit, console, and standard JavaScript only. Workflows get async state.get(key) and state.set(key, JSONValue) through their automatic or explicit mission; mission:false workflows do not have a state global. Scripts cannot access filesystem, shell, arbitrary Pi tools, or host globals.
|
|
53
54
|
• ${WORKFLOW_LANES_GUIDANCE}
|
|
54
55
|
• FILE SCRIPT: { workflowScriptPath:"workflows/review.js" }. Relative paths resolve against the request cwd. The host reads the file before the filesystem-free workflow sandbox starts. Do not combine this field with workflowScript.
|
|
55
56
|
• Sequential example: { workflowScript: "const a = await runs.run('analyze', {agent:'agent-a', task:'Analyze the request'}); return (await runs.run('plan', {agent:'agent-b', task:'Plan from: '+a.output})).output" }
|
|
@@ -60,7 +61,7 @@ EXECUTION:
|
|
|
60
61
|
MANAGEMENT / CONTROL (use action; omit execution fields):
|
|
61
62
|
• validate checks workflowScript or workflowScriptPath syntax and statically decidable structure without launching children. list, get, models, guide, children.list, create, update, delete, eject, disable, enable, reset, status, debug.run, doctor, grant-spawn-budget, worktree.discard, worktree.cleanup (plan-only), lane.status, lane.recordMerge, lane.recordSupersession, refine/refine.show/refine.rollback, mission.create/list/show/update/resolve-decision/attach-run/close, inspector.open/status/close, project.open/status/close, and watchdog actions remain available. Use {action:"guide", topic:"overview"} for packaged current-version help; topics are overview, workflows, agents, missions, observability, tool-reference, configuration, models, watchdog, and extension-api.
|
|
62
63
|
• status, interrupt, stop, resume, and steer manage live or persisted runs. Use status view:"fleet" for an overview or view:"transcript" with id and optional index to tail output.
|
|
63
|
-
• Create durable project schedules with { action:"schedule.create", id?, name?, sessionOnly?:true, at:"+10m" | ISO, baseRef?, workflowScript:"return runs.run('main', {agent:'worker', task:'...'})" }, or use workflowScriptPath instead. An optional baseRef
|
|
64
|
+
• Create durable project schedules with { action:"schedule.create", id?, name?, sessionOnly?:true, at:"+10m" | ISO, baseRef?, workflowScript:"return runs.run('main', {agent:'worker', task:'...'})" }, or use workflowScriptPath instead. An optional baseRef uses the same managed-worktree ref policy and resolves at allocation; the source checkout must still be clean. With sessionOnly:true, the schedule records the creating session file and only that session can restore or execute it; omitted/false preserves project-wide behavior. Manage them with schedule.list/show/history/pause/resume/run/run-due/delete. This first slice supports fixed intervals; calendar schedules and schedule mission attachment are deferred.
|
|
64
65
|
|
|
65
66
|
${SUBAGENT_SAFETY_GUIDANCE}`;
|
|
66
67
|
|
|
@@ -73,7 +74,7 @@ EXECUTE:
|
|
|
73
74
|
• ${AGENT_SELECTION_GUIDANCE}
|
|
74
75
|
• Passing an explicit model? Call {action:"models"} first and copy an exact provider/id; bare ids resolve only when unique in the registry; agent names (e.g. gpt-pro, advisor) are not model ids. Per-run thinking is a suffix on the model string (provider/id:high; off/minimal/low/medium/high/xhigh/max), and the suffix wins over the agent's thinking default; the thinking field only applies to action='watchdog.configure' and is ignored on dispatch.
|
|
75
76
|
• SINGLE {agent:"worker",task:"..."} starts exactly one direct child. Fields apply to that child. Do not combine agent/task with action, workflowScript, or workflowScriptPath.
|
|
76
|
-
• SCRIPT {workflowScript:"return runs.run('main', {agent:'worker', task:'...'})"}. Use stable-key runs.run for one child and await runs.all([{key,agent,task}, ...]) for ordinary parallel work. runs.all resolves to an ordered array, not a key map; use results[0], destructuring, or results.map(...), not results.<key>. Do not read .output from unawaited runs.run launches. Stored runs.run promises are only for advanced rolling fanout and each must later be observed with direct await, Promise.race, or Promise.all. Await runs.steer(key,message,options?) to guide a prior keyed child; it returns queued, delivered, missed, or failed and never accepts a raw run id. Always await or return steering calls. Use {action:"children.list"} for recent retained workflow children and resume only rows reported resumable. Use {action:"resume",id:"run-id",message:"..."} for a simple follow-up or challenge; resume keeps the stored agent/model/tool contract. If none is resumable, launch a same-role fallback challenge and label it as fallback. Inside workflowScript use runs.run(key,{resume:"run-id",task:"follow-up"}) when the script must wait for completion and continue from the latest returned runId. Workflows get async state.get/state.set through their automatic or explicit mission; mission:false does not. Scripts are ordinary JavaScript statement bodies; use explicit return for a useful result. Use top-level await, plain helper functions, or explicit Promise chains; nested async function, arrow, and method helpers are rejected. For task text with Markdown fences or shell blocks, build quoted lines instead of nesting raw template literals: \`const task=["Run:","\`\`\`bash","npm test","\`\`\`"].join("\\n")\`. Use JavaScript for sequence, branching, retries, and aggregation. For repository mutation lanes, use worktree:true on the workflow or runs.run/runs.all item for managed isolation
|
|
77
|
+
• SCRIPT {workflowScript:"return runs.run('main', {agent:'worker', task:'...'})"}. Use stable-key runs.run for one child and await runs.all([{key,agent,task}, ...]) for ordinary parallel work. runs.all resolves to an ordered array, not a key map; use results[0], destructuring, or results.map(...), not results.<key>. Do not read .output from unawaited runs.run launches. Stored runs.run promises are only for advanced rolling fanout and each must later be observed with direct await, Promise.race, or Promise.all. Await runs.steer(key,message,options?) to guide a prior keyed child; it returns queued, delivered, missed, or failed and never accepts a raw run id. Always await or return steering calls. Use {action:"children.list"} for recent retained workflow children and resume only rows reported resumable. Use {action:"resume",id:"run-id",message:"..."} for a simple follow-up or challenge; resume keeps the stored agent/model/tool contract. If none is resumable, launch a same-role fallback challenge and label it as fallback. Inside workflowScript use runs.run(key,{resume:"run-id",task:"follow-up"}) when the script must wait for completion and continue from the latest returned runId. Workflows get async state.get/state.set through their automatic or explicit mission; mission:false does not. Scripts are ordinary JavaScript statement bodies; use explicit return for a useful result. Use top-level await, plain helper functions, or explicit Promise chains; nested async function, arrow, and method helpers are rejected. For task text with Markdown fences or shell blocks, build quoted lines instead of nesting raw template literals: \`const task=["Run:","\`\`\`bash","npm test","\`\`\`"].join("\\n")\`. Use JavaScript for sequence, branching, retries, and aggregation. For repository mutation lanes, use worktree:true on the workflow or runs.run/runs.all item for managed isolation. ${WORKTREE_BASE_REF_GUIDANCE} Scripts normally start async unless config sets asyncByDefault:false; set async:true explicitly when async behavior matters. async:false blocks the parent until completion and auto-enables a same-repo live chat card unless chatProgress is off; explicit live-card requires same-repository async:false, so async workflows should omit chatProgress or use auto/off.
|
|
77
78
|
• ${WORKFLOW_LANES_GUIDANCE}
|
|
78
79
|
• FILE SCRIPT {workflowScriptPath:"workflows/review.js"} loads the script on the host relative to the request cwd before sandbox execution. Do not combine it with workflowScript.
|
|
79
80
|
• Example: {workflowScript:"const [a,b]=await runs.all([{key:'a',agent:'agent-a',task:'Implement A',worktree:true},{key:'b',agent:'agent-b',task:'Implement B',worktree:true}]); return [a.output,b.output]"}
|