renamed -> caller_ping to ask_question, questions don't end subagent sessions anymore, subagent sessions stay open when waiting for their own subagents to finish
This commit is contained in:
@@ -20,8 +20,7 @@ export type SubagentActivityEvent =
|
||||
| "tool_execution_update"
|
||||
| "tool_result"
|
||||
| "tool_execution_end"
|
||||
| "caller_ping"
|
||||
| "subagent_done"
|
||||
| "ask_question"
|
||||
| "session_shutdown";
|
||||
|
||||
export interface SubagentActivityState {
|
||||
@@ -70,8 +69,7 @@ export interface SubagentActivityRecorder {
|
||||
toolExecutionUpdate(toolCallId?: string, toolName?: string): void;
|
||||
toolResult(toolCallId?: string, toolName?: string): void;
|
||||
toolExecutionEnd(toolCallId?: string, toolName?: string): void;
|
||||
callerPing(): void;
|
||||
subagentDone(): void;
|
||||
askQuestion(): void;
|
||||
sessionShutdown(reason: SubagentShutdownReason): void;
|
||||
}
|
||||
|
||||
@@ -95,8 +93,7 @@ const KNOWN_EVENTS = new Set<SubagentActivityEvent>([
|
||||
"tool_execution_update",
|
||||
"tool_result",
|
||||
"tool_execution_end",
|
||||
"caller_ping",
|
||||
"subagent_done",
|
||||
"ask_question",
|
||||
"session_shutdown",
|
||||
]);
|
||||
const MAX_ACTIVITY_STRING_LENGTH = 200;
|
||||
@@ -239,8 +236,7 @@ function createNoopRecorder(): SubagentActivityRecorder {
|
||||
toolExecutionUpdate() {},
|
||||
toolResult() {},
|
||||
toolExecutionEnd() {},
|
||||
callerPing() {},
|
||||
subagentDone() {},
|
||||
askQuestion() {},
|
||||
sessionShutdown() {},
|
||||
};
|
||||
}
|
||||
@@ -497,11 +493,15 @@ export function createSubagentActivityRecorder(params: {
|
||||
refreshActiveScope(current);
|
||||
}, "immediate");
|
||||
},
|
||||
callerPing() {
|
||||
markDone("caller_ping");
|
||||
},
|
||||
subagentDone() {
|
||||
markDone("subagent_done");
|
||||
askQuestion() {
|
||||
// The subagent paused to ask the orchestrator a question. Park it in the
|
||||
// "waiting" phase (do NOT disable the recorder) so the status widget shows
|
||||
// it as waiting and recording resumes when the answer arrives.
|
||||
record("ask_question", (current, observedAt) => {
|
||||
clearActiveState(current);
|
||||
current.phase = "waiting";
|
||||
current.waitingSince = observedAt;
|
||||
}, "immediate");
|
||||
},
|
||||
sessionShutdown(reason) {
|
||||
if (reason === "quit") markDone("session_shutdown");
|
||||
|
||||
@@ -1256,28 +1256,23 @@ export function closeSurface(surface: string): void {
|
||||
|
||||
export interface PollResult {
|
||||
/** How the subagent exited */
|
||||
reason: "done" | "ping" | "sentinel" | "error";
|
||||
reason: "done" | "sentinel" | "error";
|
||||
/** Shell exit code (from sentinel). 0 for file-based exits. */
|
||||
exitCode: number;
|
||||
/** Ping data if reason is "ping" */
|
||||
ping?: { name: string; message: string };
|
||||
/** Error message if reason is "error" (auto-retry exhausted, provider overload, etc.) */
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interpret an `.exit` sidecar payload (written by subagent_done / caller_ping /
|
||||
* the error path in subagent-done.ts). Centralized so both the fast and slow
|
||||
* paths in pollForExit decode the payload the same way.
|
||||
* Interpret an `.exit` sidecar payload (written by the error path in
|
||||
* subagent-done.ts). Centralized so both the fast and slow paths in
|
||||
* pollForExit decode the payload the same way. Clean completions write no
|
||||
* sidecar and are detected via the terminal sentinel instead.
|
||||
*
|
||||
* Note: ask_question does NOT write a `.exit` sidecar — it keeps the session
|
||||
* open and signals the parent via a separate `.ask` file (see deliverPendingQuestion).
|
||||
*/
|
||||
function interpretExitSidecar(data: any): PollResult {
|
||||
if (data?.type === "ping") {
|
||||
return {
|
||||
reason: "ping",
|
||||
exitCode: 0,
|
||||
ping: { name: data.name, message: data.message },
|
||||
};
|
||||
}
|
||||
if (data?.type === "error") {
|
||||
const errorMessage =
|
||||
typeof data.errorMessage === "string" && data.errorMessage.trim() !== ""
|
||||
@@ -1292,8 +1287,8 @@ export const __pollForExitTest__ = { interpretExitSidecar };
|
||||
|
||||
/**
|
||||
* Poll until the subagent exits. Checks for a `.exit` sidecar file first
|
||||
* (written by subagent_done / caller_ping), falling back to the terminal
|
||||
* sentinel for crash detection.
|
||||
* (written by the error path), falling back to the terminal sentinel for
|
||||
* clean-completion and crash detection.
|
||||
*/
|
||||
export async function pollForExit(
|
||||
surface: string,
|
||||
@@ -1312,7 +1307,7 @@ export async function pollForExit(
|
||||
throw new Error("Aborted while waiting for subagent to finish");
|
||||
}
|
||||
|
||||
// Fast path: check for .exit sidecar file (written by subagent_done / caller_ping)
|
||||
// Fast path: check for .exit sidecar file (written by the error path)
|
||||
if (options.sessionFile) {
|
||||
try {
|
||||
const exitFile = `${options.sessionFile}.exit`;
|
||||
|
||||
@@ -548,7 +548,6 @@ interface SubagentResult {
|
||||
error?: string;
|
||||
/** Provider/agent error message when auto-retry exhausted (overload, rate limit, etc.). */
|
||||
errorMessage?: string;
|
||||
ping?: { name: string; message: string };
|
||||
/** Aggregate usage/model/tool stats parsed from the completed session file. */
|
||||
stats?: SessionStats;
|
||||
}
|
||||
@@ -588,10 +587,21 @@ interface RunningSubagent {
|
||||
/** All currently running subagents, keyed by id. */
|
||||
const runningSubagents = new Map<string, RunningSubagent>();
|
||||
|
||||
// When this extension is loaded inside a subagent that itself spawns children
|
||||
// (e.g. a worker delegating to scout/researcher), `subagent-done.ts` runs in the
|
||||
// same process and needs to know whether this session still has children in
|
||||
// flight — so it can suppress auto-exit and keep the session open until they all
|
||||
// report back. Expose a live count through a process-global symbol that both
|
||||
// modules share. (subagent-done.ts reads it; if absent it assumes zero.)
|
||||
const RUNNING_CHILDREN_COUNT_KEY = Symbol.for("pi-subagents/running-children-count");
|
||||
(globalThis as any)[RUNNING_CHILDREN_COUNT_KEY] = () => runningSubagents.size;
|
||||
|
||||
// ── Widget management ──
|
||||
|
||||
/** Latest ExtensionContext from session_start, used for widget updates. */
|
||||
let latestCtx: ExtensionContext | null = null;
|
||||
/** Latest ExtensionAPI, used to deliver ask_question notifications from the watcher. */
|
||||
let latestPi: ExtensionAPI | null = null;
|
||||
|
||||
/** Interval timer for widget re-renders. */
|
||||
let widgetInterval: ReturnType<typeof setInterval> | null = null;
|
||||
@@ -731,7 +741,7 @@ function updateWidget() {
|
||||
* first positional message so that /skill: args land in messages[1..] and arrive
|
||||
* as standalone prompts in the child session.
|
||||
*/
|
||||
const SUBAGENT_CONTROL_TOOLS = ["caller_ping", "subagent_done"] as const;
|
||||
const SUBAGENT_CONTROL_TOOLS = ["ask_question"] as const;
|
||||
|
||||
/**
|
||||
* Build the child --tools allowlist.
|
||||
@@ -739,7 +749,7 @@ const SUBAGENT_CONTROL_TOOLS = ["caller_ping", "subagent_done"] as const;
|
||||
* Pi 0.70+ applies --tools to built-in, extension, and custom tools. If a
|
||||
* subagent definition restricts tools to e.g. "read,bash,write", the child
|
||||
* control tools from subagent-done.ts would otherwise be hidden, leaving a
|
||||
* manually resumed or user-touched subagent unable to call subagent_done.
|
||||
* manually resumed or user-touched subagent unable to call ask_question.
|
||||
*/
|
||||
function buildSubagentToolAllowlist(
|
||||
effectiveTools?: string,
|
||||
@@ -1113,11 +1123,11 @@ async function launchSubagent(
|
||||
// Only full-context fork mode inherits prior conversation state.
|
||||
// Blank-session modes need the wrapper instructions and artifact-backed handoff.
|
||||
const modeHint = agentDefs?.autoExit
|
||||
? "Complete your task autonomously."
|
||||
: "Complete your task. When finished, call the subagent_done tool. The user can interact with you at any time.";
|
||||
? "Complete your task autonomously. When you are finished, simply stop — your session ends automatically."
|
||||
: "Complete your task. The user can interact with you at any time, and the session ends when the user exits the pane.";
|
||||
const summaryInstruction = agentDefs?.autoExit
|
||||
? "Your FINAL assistant message should summarize what you accomplished."
|
||||
: "Your FINAL assistant message (before calling subagent_done or before the user exits) should summarize what you accomplished.";
|
||||
: "Your FINAL assistant message (before the user exits) should summarize what you accomplished.";
|
||||
// An agent with a non-empty subagent_agents list is granted the spawning
|
||||
// toolset and may only spawn the listed agents (enforced via PI_SUBAGENT_ALLOWED).
|
||||
const grantSpawning = !!(agentDefs?.subagentAgents && agentDefs.subagentAgents.length > 0);
|
||||
@@ -1377,6 +1387,50 @@ function copyClaudeSession(sentinelFile: string): string | null {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect an `ask_question` signal from a still-running subagent and notify the
|
||||
* orchestrator without ending the subagent. Each subagent has its own
|
||||
* `${sessionFile}.ask` file and its own watcher, so parallel questions from
|
||||
* multiple subagents are delivered independently. The file is deleted after
|
||||
* delivery so it fires once per question (a subagent may ask again later).
|
||||
*/
|
||||
function deliverPendingQuestion(running: RunningSubagent): void {
|
||||
const askFile = `${running.sessionFile}.ask`;
|
||||
let payload: any = null;
|
||||
try {
|
||||
if (!existsSync(askFile)) return;
|
||||
payload = JSON.parse(readFileSync(askFile, "utf-8"));
|
||||
} catch {
|
||||
// Malformed/partway-written file — drop it and move on.
|
||||
}
|
||||
try {
|
||||
unlinkSync(askFile);
|
||||
} catch {}
|
||||
if (!payload?.question) return;
|
||||
|
||||
const name = running.name; // unique (deduped at spawn) — used to target the reply
|
||||
const sessionId = existsSync(running.sessionFile) ? getSessionId(running.sessionFile) : null;
|
||||
const elapsed = Math.floor((Date.now() - running.startTime) / 1000);
|
||||
const replyHint = sessionId
|
||||
? `\n\nReply with subagent_message({ name: "${name}", message: "…" }) while it runs (or sessionId: "${sessionId}" after it exits). It stays open until you reply.`
|
||||
: `\n\nReply with subagent_message({ name: "${name}", message: "…" }). It stays open until you reply.`;
|
||||
|
||||
latestPi?.sendMessage(
|
||||
{
|
||||
customType: "subagent_question",
|
||||
content: `Sub-agent "${name}" asks (${formatElapsed(elapsed)}):\n\n${payload.question}${replyHint}`,
|
||||
display: true,
|
||||
details: {
|
||||
name,
|
||||
agent: running.agent,
|
||||
question: payload.question,
|
||||
...(sessionId ? { sessionId } : {}),
|
||||
},
|
||||
},
|
||||
{ triggerTurn: true, deliverAs: "steer" },
|
||||
);
|
||||
}
|
||||
|
||||
async function watchSubagent(
|
||||
running: RunningSubagent,
|
||||
signal: AbortSignal,
|
||||
@@ -1390,6 +1444,7 @@ async function watchSubagent(
|
||||
sentinelFile: running.sentinelFile,
|
||||
onTick() {
|
||||
observeRunningSubagent(running);
|
||||
deliverPendingQuestion(running);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1464,7 +1519,6 @@ async function watchSubagent(
|
||||
...(subagentSessionId ? { sessionId: subagentSessionId } : {}),
|
||||
exitCode: result.exitCode,
|
||||
elapsed,
|
||||
ping: result.ping,
|
||||
...(result.errorMessage ? { errorMessage: result.errorMessage } : {}),
|
||||
...(stats ? { stats } : {}),
|
||||
};
|
||||
@@ -1497,6 +1551,7 @@ async function watchSubagent(
|
||||
}
|
||||
|
||||
export default function subagentsExtension(pi: ExtensionAPI) {
|
||||
latestPi = pi;
|
||||
// Capture the UI context for widget updates
|
||||
pi.on("session_start", (_event, ctx) => {
|
||||
latestCtx = ctx;
|
||||
@@ -1666,30 +1721,6 @@ export default function subagentsExtension(pi: ExtensionAPI) {
|
||||
.then((result) => {
|
||||
updateWidget(); // reflect removal from Map immediately
|
||||
|
||||
if (result.ping) {
|
||||
// Subagent is requesting help — steer a ping with its session id so
|
||||
// the orchestrator can reply via subagent_message.
|
||||
const sessionRef = result.sessionId
|
||||
? `\n\nSession id: ${result.sessionId}\nReply with subagent_message({ name: "${result.ping.name}", message: "…" }) while it runs, or sessionId after it exits.`
|
||||
: "";
|
||||
pi.sendMessage(
|
||||
{
|
||||
customType: "subagent_ping",
|
||||
content: `Sub-agent "${result.ping.name}" needs help (${formatElapsed(result.elapsed)}):\n\n${result.ping.message}${sessionRef}`,
|
||||
display: true,
|
||||
details: {
|
||||
name: result.ping.name,
|
||||
message: result.ping.message,
|
||||
agent: running.agent,
|
||||
sessionFile: result.sessionFile,
|
||||
...(result.sessionId ? { sessionId: result.sessionId } : {}),
|
||||
},
|
||||
},
|
||||
{ triggerTurn: true, deliverAs: "steer" },
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const presentation = resolveResultPresentation(result, running.name);
|
||||
|
||||
pi.sendMessage(
|
||||
@@ -2091,25 +2122,6 @@ export default function subagentsExtension(pi: ExtensionAPI) {
|
||||
.then((result) => {
|
||||
updateWidget();
|
||||
|
||||
if (result.ping) {
|
||||
const sessionRef = `\n\nSession id: ${resumedSessionId}\nReply with subagent_message({ name: "${result.ping.name}", message: "…" }) while it runs, or sessionId after it exits.`;
|
||||
pi.sendMessage(
|
||||
{
|
||||
customType: "subagent_ping",
|
||||
content: `Sub-agent "${result.ping.name}" needs help (${formatElapsed(result.elapsed)}):\n\n${result.ping.message}${sessionRef}`,
|
||||
display: true,
|
||||
details: {
|
||||
name: result.ping.name,
|
||||
message: result.ping.message,
|
||||
sessionFile: sessionPath,
|
||||
sessionId: resumedSessionId,
|
||||
},
|
||||
},
|
||||
{ triggerTurn: true, deliverAs: "steer" },
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const allEntries = getNewEntries(sessionPath, entryCountBefore);
|
||||
const summary = findLastAssistantMessage(allEntries) ??
|
||||
(result.errorMessage
|
||||
@@ -2338,8 +2350,8 @@ export default function subagentsExtension(pi: ExtensionAPI) {
|
||||
};
|
||||
});
|
||||
|
||||
// ── subagent_ping message renderer ──
|
||||
pi.registerMessageRenderer("subagent_ping", (message, options, theme) => {
|
||||
// ── subagent_question message renderer ──
|
||||
pi.registerMessageRenderer("subagent_question", (message, options, theme) => {
|
||||
const details = message.details as any;
|
||||
if (!details) return undefined;
|
||||
|
||||
@@ -2350,19 +2362,19 @@ export default function subagentsExtension(pi: ExtensionAPI) {
|
||||
const bgFn = (text: string) => theme.bg("toolSuccessBg", text);
|
||||
|
||||
const icon = theme.fg("accent", "?");
|
||||
const header = `${icon} ${theme.fg("toolTitle", theme.bold(name))}${agentTag} ${theme.fg("dim", "— needs help")}`;
|
||||
const header = `${icon} ${theme.fg("toolTitle", theme.bold(name))}${agentTag} ${theme.fg("dim", "— asks a question")}`;
|
||||
|
||||
const contentLines = [header];
|
||||
|
||||
if (options.expanded) {
|
||||
contentLines.push("");
|
||||
contentLines.push(details.message ?? "");
|
||||
if (details.sessionFile) {
|
||||
contentLines.push(details.question ?? "");
|
||||
if (details.sessionId) {
|
||||
contentLines.push("");
|
||||
contentLines.push(theme.fg("dim", `Session: ${details.sessionFile}`));
|
||||
contentLines.push(theme.fg("dim", `Session id: ${details.sessionId}`));
|
||||
}
|
||||
} else {
|
||||
const preview = (details.message ?? "").split("\n")[0].slice(0, width - 10);
|
||||
const preview = (details.question ?? "").split("\n")[0].slice(0, width - 10);
|
||||
contentLines.push(theme.fg("dim", preview));
|
||||
contentLines.push(theme.fg("muted", keyHint("app.tools.expand", "to expand")));
|
||||
}
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
/**
|
||||
* Extension loaded into sub-agents.
|
||||
* - Shows agent identity + available tools as a styled widget above the editor (toggle with Ctrl+Alt+O)
|
||||
* - Provides a `subagent_done` tool for autonomous agents to self-terminate
|
||||
* - Provides an `ask_question` tool for asking the parent orchestrator a question
|
||||
*
|
||||
* Subagents do NOT self-terminate via a tool. Auto-exit agents shut down
|
||||
* automatically when their agent loop ends (see the `agent_end` handler);
|
||||
* interactive agents end when the human exits the pane.
|
||||
*
|
||||
* `ask_question` keeps the session OPEN: it writes a `${sessionFile}.ask`
|
||||
* signal the parent's watcher picks up, parks the session in a "waiting" state
|
||||
* (auto-exit is suppressed for that turn via `awaitingAnswer`), and the parent
|
||||
* replies with subagent_message — which lands as the subagent's next turn.
|
||||
*/
|
||||
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
||||
import { Box, Text } from "@mariozechner/pi-tui";
|
||||
@@ -13,6 +22,32 @@ export function shouldMarkUserTookOver(agentStarted: boolean): boolean {
|
||||
return agentStarted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Number of child subagents this session itself still has in flight.
|
||||
*
|
||||
* When this extension is loaded inside a subagent that can spawn its own
|
||||
* children (e.g. a worker delegating to scout/researcher), `index.ts` runs in
|
||||
* the same process and publishes a live count through a shared process-global
|
||||
* symbol. A subagent that spawns children and then writes a "waiting for
|
||||
* results" message would otherwise auto-exit the instant that turn ends —
|
||||
* killing the session before its children report back. Reading this count lets
|
||||
* `agent_end` keep the session open until every child has finished and its
|
||||
* result has been delivered.
|
||||
*
|
||||
* Returns 0 when the spawning tools aren't loaded (scout/researcher, or a
|
||||
* standalone session), so those agents auto-exit exactly as before.
|
||||
*/
|
||||
export function runningChildrenCount(): number {
|
||||
const fn = (globalThis as any)[Symbol.for("pi-subagents/running-children-count")];
|
||||
if (typeof fn !== "function") return 0;
|
||||
try {
|
||||
const n = fn();
|
||||
return typeof n === "number" && n > 0 ? n : 0;
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
export function shouldAutoExitOnAgentEnd(
|
||||
_userTookOver: boolean,
|
||||
messages: any[] | undefined,
|
||||
@@ -143,6 +178,10 @@ export default function (pi: ExtensionAPI) {
|
||||
|
||||
let userTookOver = false;
|
||||
let agentStarted = false;
|
||||
// Set when ask_question is called; suppresses auto-exit for that turn so the
|
||||
// session stays open while it waits for the orchestrator's reply. Cleared at
|
||||
// the start of the next turn (when the reply lands).
|
||||
let awaitingAnswer = false;
|
||||
|
||||
// Show widget + status bar on session start
|
||||
pi.on("session_start", (_event, ctx) => {
|
||||
@@ -168,12 +207,27 @@ export default function (pi: ExtensionAPI) {
|
||||
|
||||
pi.on("agent_start", () => {
|
||||
agentStarted = true;
|
||||
// A new turn is starting — any pending ask_question has now been answered
|
||||
// (or superseded), so let auto-exit resume normally when this turn ends.
|
||||
awaitingAnswer = false;
|
||||
recorder.agentStart();
|
||||
});
|
||||
|
||||
pi.on("agent_end", (event, ctx) => {
|
||||
const messages = (event as any).messages as any[] | undefined;
|
||||
const shouldExit = autoExit && shouldAutoExitOnAgentEnd(userTookOver, messages);
|
||||
// Never shut down while this session still has work in flight:
|
||||
// - awaitingAnswer: an ask_question is pending the orchestrator's reply.
|
||||
// - runningChildrenCount(): this subagent spawned its own children and is
|
||||
// waiting for their results (delivered as steered turns). Exiting now
|
||||
// would strand those children and drop their results.
|
||||
// In both cases the session parks as `waiting` and resumes when the next
|
||||
// turn lands.
|
||||
const hasPendingChildren = runningChildrenCount() > 0;
|
||||
const shouldExit =
|
||||
!awaitingAnswer &&
|
||||
!hasPendingChildren &&
|
||||
autoExit &&
|
||||
shouldAutoExitOnAgentEnd(userTookOver, messages);
|
||||
|
||||
if (shouldExit) {
|
||||
// Surface stopReason: "error" turns (auto-retry exhausted, provider
|
||||
@@ -266,59 +320,70 @@ export default function (pi: ExtensionAPI) {
|
||||
});
|
||||
|
||||
pi.registerTool({
|
||||
name: "caller_ping",
|
||||
label: "Caller Ping",
|
||||
name: "ask_question",
|
||||
label: "ask_question",
|
||||
description:
|
||||
"Send a help request to the parent agent and exit this session. " +
|
||||
"The parent will be notified with your message and can resume this session with a response. " +
|
||||
"Use when you're stuck, need clarification, or need the parent to take action.",
|
||||
"Ask the orchestrator (the parent agent that spawned you) a single question and pause until they reply. " +
|
||||
"Use this when requirements are ambiguous, a decision would materially affect your work, you're blocked, " +
|
||||
"or you need information or confirmation only the orchestrator has. Prefer asking over guessing. " +
|
||||
"Your session stays open while you wait — the answer arrives as your next message, then you continue. " +
|
||||
"Ask exactly one question per call; make separate calls for unrelated questions.",
|
||||
promptSnippet:
|
||||
"Use this tool to ask the orchestrator one clarifying, missing-requirement, preference, or decision question before continuing — instead of guessing.",
|
||||
promptGuidelines: [
|
||||
"Ask exactly one question per tool call.",
|
||||
"If you need answers to multiple things, make separate ask_question calls instead of bundling them.",
|
||||
"Prefer this tool over guessing when requirements, preferences, or implementation choices are unclear.",
|
||||
"Use it when multiple valid paths exist and the right one depends on the orchestrator's intent.",
|
||||
"Give enough context in the question that the orchestrator can answer without re-reading your whole task.",
|
||||
"After asking, stop and wait — the reply will arrive as your next message.",
|
||||
],
|
||||
parameters: Type.Object({
|
||||
message: Type.String({ description: "What you need help with" }),
|
||||
question: Type.String({
|
||||
description:
|
||||
"The single freeform question to ask the orchestrator. Include enough context to answer it directly.",
|
||||
}),
|
||||
}),
|
||||
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
||||
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
||||
const sessionFile = process.env.PI_SUBAGENT_SESSION;
|
||||
if (!sessionFile) {
|
||||
throw new Error(
|
||||
"caller_ping is only available in subagent contexts. " +
|
||||
"ask_question is only available in subagent contexts. " +
|
||||
"PI_SUBAGENT_SESSION environment variable is not set.",
|
||||
);
|
||||
}
|
||||
|
||||
recorder.callerPing();
|
||||
const exitData = {
|
||||
type: "ping" as const,
|
||||
// Keep the session open: suppress auto-exit for this turn and park in the
|
||||
// "waiting" phase. The parent's watcher picks up the `.ask` signal and
|
||||
// notifies the orchestrator, who replies via subagent_message.
|
||||
awaitingAnswer = true;
|
||||
recorder.askQuestion();
|
||||
const askData = {
|
||||
name: process.env.PI_SUBAGENT_NAME ?? "subagent",
|
||||
message: params.message,
|
||||
agent: process.env.PI_SUBAGENT_AGENT ?? "",
|
||||
question: params.question,
|
||||
};
|
||||
writeFileSync(`${sessionFile}.exit`, JSON.stringify(exitData));
|
||||
writeFileSync(`${sessionFile}.ask`, JSON.stringify(askData));
|
||||
|
||||
ctx.shutdown();
|
||||
return {
|
||||
content: [{ type: "text", text: "Ping sent. Session will exit and parent will be notified." }],
|
||||
details: {},
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text:
|
||||
"Question sent to the orchestrator. Stop here and wait — do not continue working or " +
|
||||
"assume an answer. Their reply will arrive as your next message.",
|
||||
},
|
||||
],
|
||||
details: { question: params.question },
|
||||
};
|
||||
},
|
||||
|
||||
renderCall(args, theme) {
|
||||
const text =
|
||||
theme.fg("toolTitle", theme.bold("ask_question ")) +
|
||||
theme.fg("muted", String((args as any).question ?? ""));
|
||||
return new Text(text, 0, 0);
|
||||
},
|
||||
});
|
||||
|
||||
pi.registerTool({
|
||||
name: "subagent_done",
|
||||
label: "Subagent Done",
|
||||
description:
|
||||
"Call this tool when you have completed your task. " +
|
||||
"It will close this session and return your results to the main session. " +
|
||||
"Your LAST assistant message before calling this becomes the summary returned to the caller.",
|
||||
parameters: Type.Object({}),
|
||||
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
|
||||
const sessionFile = process.env.PI_SUBAGENT_SESSION;
|
||||
recorder.subagentDone();
|
||||
if (sessionFile) {
|
||||
writeFileSync(`${sessionFile}.exit`, JSON.stringify({ type: "done" }));
|
||||
}
|
||||
ctx.shutdown();
|
||||
return {
|
||||
content: [{ type: "text", text: "Shutting down subagent session." }],
|
||||
details: {},
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user