Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion extensions/pi-subagents/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# pi-subagents

Minimal recursive child-Pi spawner for Pi.
Minimal recursive child-Pi spawner for Pi 0.80.4 or newer.

This extension intentionally does **not** define roles, agent types, chains, or parallel task lists. The parent agent supplies the full prompt and instructions for each child.

Expand All @@ -22,6 +22,7 @@ spawn_subagent({
- No wait-for-completion mode exists; calls always return immediately with an ID and resultPath.
- You will be notified when the subagent completes.
- The returned ID is also the child Pi session ID and can be used with Pi session lookup/resume behavior.
- `model` is an explicit override; omitting it inherits the active parent provider/model.
- `timeout` is optional (default 600s = 10 minutes) and measured in seconds. When reached, the parent is informed that the child is still running; the child is **not killed**.
- Give `timeout` a healthy margin above expected runtime because child execution time can be wildly unpredictable.
- Subagents always start with fresh session history. Put any desired context explicitly in `task`.
Expand Down
10 changes: 5 additions & 5 deletions extensions/pi-subagents/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
"peerDependencies": {
"@earendil-works/pi-agent-core": "*",
"@earendil-works/pi-ai": "*",
"@earendil-works/pi-coding-agent": "*",
"@earendil-works/pi-coding-agent": ">=0.80.4",
"@earendil-works/pi-tui": "*"
},
"peerDependenciesMeta": {
Expand All @@ -73,9 +73,9 @@
"typebox": "^1.1.24"
},
"devDependencies": {
"@earendil-works/pi-agent-core": "^0.74.0",
"@earendil-works/pi-ai": "^0.74.0",
"@earendil-works/pi-coding-agent": "^0.74.0",
"@earendil-works/pi-tui": "^0.74.0"
"@earendil-works/pi-agent-core": "^0.80.4",
"@earendil-works/pi-ai": "^0.80.4",
"@earendil-works/pi-coding-agent": "^0.80.4",
"@earendil-works/pi-tui": "^0.80.4"
}
}
2 changes: 2 additions & 0 deletions extensions/pi-subagents/skills/pi-subagents/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Use this tool to launch unrestricted child Pi sessions. The caller must include

- `spawn_subagent({ task, timeout?, cwd?, model? })`
- Calls return immediately; the parent will be notified when the subagent completes.
- `model` is an explicit override; omitting it inherits the active parent provider/model.
- `timeout` is optional; default is `600` seconds (10 minutes).
- Timeout is only a notification threshold: the parent is informed that the child is still running; the child is not killed.
- The returned subagent id is also the child Pi session id.
Expand All @@ -37,6 +38,7 @@ Calls return immediately; the parent will be notified when each subagent complet
- No wait-for-completion mode exists.
- No subagent types exist.
- No chain or parallel-list mode exists.
- `model` is an explicit override; omitting it inherits the active parent provider/model.
- `timeout` is optional and measured in seconds; omitted timeout defaults to 10 minutes.
- When `timeout` expires, the parent is informed that the subagent is still running; the child is not killed.
- Do not kill subagents autonomously to enforce `timeout`.
Expand Down
12 changes: 9 additions & 3 deletions extensions/pi-subagents/src/extension/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ interface ToolDetails {
timedOut?: boolean;
timeoutAt?: number;
timeoutMessage?: string;
model?: string;
subagents?: Array<{ id: string; running: boolean }>;
}

Expand Down Expand Up @@ -792,14 +793,18 @@ function makeRecord(
const id = randomUUID();
const parentId = parentSessionId(ctx);
const dir = childDir(parentId, id);
const inheritedModel = ctx.model
? `${ctx.model.provider}/${ctx.model.id}`
: undefined;
const model = params.model || inheritedModel;
fs.mkdirSync(dir, { recursive: true });
return {
id,
parentSessionId: parentId,
cwd: params.cwd ? path.resolve(ctx.cwd, params.cwd) : ctx.cwd,
taskPreview: params.task.slice(0, 500),
timeout: params.timeout ?? DEFAULT_TIMEOUT_SECONDS,
...(params.model ? { model: params.model } : {}),
...(model ? { model } : {}),
running: false,
sessionDir: dir,
outputFile: path.join(dir, "result.log"),
Expand Down Expand Up @@ -1281,7 +1286,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
name: "spawn_subagent",
label: "Spawn subagent",
description:
"Spawn a child Pi subagent for one task. timeout is optional and measured in seconds (default 600 = 10 minutes). This returns immediately, allowing the parent to spawn multiple concurrent subagents by calling spawn_subagent multiple times. Do not kill subagents autonomously to enforce timeout; the parent will be informed when timeout expires. Give a healthy timeout margin above expected runtime because subagent execution may be wildly unpredictable.",
"Spawn a child Pi subagent for one task. model is an explicit override; omitting it inherits the active parent provider/model. timeout is optional and measured in seconds (default 600 = 10 minutes). This returns immediately, allowing the parent to spawn multiple concurrent subagents by calling spawn_subagent multiple times. Do not kill subagents autonomously to enforce timeout; the parent will be informed when timeout expires. Give a healthy timeout margin above expected runtime because subagent execution may be wildly unpredictable.",
parameters: SpawnSubagentParams,
async execute(
id,
Expand Down Expand Up @@ -1336,13 +1341,14 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
content: [
{
type: "text",
text: `Spawned subagent ${started.record.id}. Result will be at: ${started.record.outputFile}. You will be notified when this subagent completes. Do not poll for result. Do not sleep for result. Continue with whatever other work you may have.`,
text: `Spawned subagent ${started.record.id} using ${started.record.model ?? "Pi's selected model"}. Result will be at: ${started.record.outputFile}. You will be notified when this subagent completes. Do not poll for result. Do not sleep for result. Continue with whatever other work you may have.`,
},
],
details: {
id: started.record.id,
running: started.record.running,
resultPath: started.record.outputFile,
...(started.record.model ? { model: started.record.model } : {}),
},
};
},
Expand Down
5 changes: 4 additions & 1 deletion extensions/pi-subagents/src/extension/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ export const SpawnSubagentParams = Type.Object(
}),
),
model: Type.Optional(
Type.String({ description: "Optional model override for the child Pi." }),
Type.String({
description:
"Explicit model override for the child Pi. Omitting it inherits the active parent provider/model.",
}),
),
},
{ additionalProperties: false },
Expand Down
25 changes: 25 additions & 0 deletions extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,31 @@ function setupLifelineWatcher(): void {
setupLifelineWatcher();

export default function registerSubagentPromptRuntime(pi: ExtensionAPI): void {
let settledProviderError: string | undefined;
let exitingForProviderError = false;

pi.on("message_end", (event) => {
if (event.message.role !== "assistant") return;
if (event.message.stopReason !== "error") {
settledProviderError = undefined;
return;
}
settledProviderError =
event.message.errorMessage?.trim() ||
`Provider/model request failed for ${event.message.provider}/${event.message.model}.`;
});

pi.on("agent_settled", () => {
if (!settledProviderError || exitingForProviderError) return;
exitingForProviderError = true;
process.exitCode = 1;
process.stderr.write(`${settledProviderError}\n`);
// message_end has been persisted and agent_settled guarantees no retry,
// compaction, or queued continuation remains. Force exit because the
// dedicated parent lifeline intentionally keeps the event loop alive.
setImmediate(() => process.exit(1));
});

pi.on("tool_call", (event) => {
if (!FILE_TOOL_NAMES.has(event.toolName)) return;
const resultPath = process.env[SUBAGENT_RESULT_PATH_ENV]?.trim();
Expand Down
Loading
Loading