261 lines · 9.6 KB · TypeScript
RawDownload
//// Subagents: child pi sessions a parent session's model spawns through the// `subagent` tool, plus the management half it supervises them with —// `subagent_list`, `subagent_status`, `subagent_stop`.//// Each subagent runs headless in this runtime under the reserved runtime key// `subagent:<parent pi session id>:<run id>`, with its own context and its own// canonical session file (so the UI can open it like any other session), and// reports its final text back as the tool result.//// The registry below is keyed by parent pi session id, and that is what keeps// the two safety properties intact as management grows://   * every lookup goes through the caller's own parent bucket, so a session//     can only ever see, inspect, or stop the children it spawned itself;//   * a child's runtime key carries the reserved prefix, so when it asks to//     spawn, findParentRuntime resolves that key and the spawn is refused.// import { randomUUID } from "node:crypto";import { getGlobalSingleton } from "./instances";import { piRuntimeManager } from "./pi-runtime";import { lastAssistantResult, type LastAssistantResult } from "./session-text";import { sessionSubagentLink, setSubagentLink } from "./session-metadata-store"; const NICKNAMES = [  "Euclid",  "Archimedes",  "Hypatia",  "Ptolemy",  "Leibniz",  "Lovelace",  "Boole",  "Turing",  "Hopper",  "Noether",  "Curie",  "Gauss",  "Euler",  "Ramanujan",  "Erdos",  "Franklin",  "Kepler",  "Darwin",  "Fermi",  "Bohr",]; export const MAX_CONCURRENT_PER_PARENT = 4;const MAX_RESULT_CHARS = 8000;const SUBAGENT_SESSION_PREFIX = "subagent:"; /** A run is "running" until exactly one of its terminal states is written: *  "done"/"error" by the run itself, "cancelled" by stopSubagent. */export type SubagentStatus = "running" | "done" | "error" | "cancelled"; export type SubagentRun = {  id: string;  parentPiSessionId: string;  name: string;  task: string;  piSessionId: string | null;  /** Reserved runtime key this child owns — also the no-nesting marker. */  runtimeSessionId: string;  cwd: string;  status: SubagentStatus;  startedAt: string;  finishedAt: string | null;  error?: string;}; type SubagentState = {  /** parent pi session id -> runs, newest last */  byParent: Map<string, SubagentRun[]>;  /** pi session ids that ARE subagents — they may not spawn their own */  childPiSessionIds: Set<string>;}; function state(): SubagentState {  return getGlobalSingleton("subagentRegistry", () => ({    byParent: new Map<string, SubagentRun[]>(),    childPiSessionIds: new Set<string>(),  }));} export function listSubagents(parentPiSessionId: string): SubagentRun[] {  return state().byParent.get(parentPiSessionId) ?? [];} /** Scoped lookup: a run is only reachable through the parent that spawned it. */export function findSubagent(parentPiSessionId: string, runId: string): SubagentRun | null {  const target = runId.trim();  return listSubagents(parentPiSessionId).find((run) => run.id === target) ?? null;} function findParentRuntime(parentPiSessionId: string) {  return piRuntimeManager    .listSessions()    .find(({ session }) => session.status.piSessionId === parentPiSessionId);} /** The child's live runtime entry, if this runtime still holds one. */function findChildRuntime(run: SubagentRun) {  return piRuntimeManager.findSessionForLookup(run.runtimeSessionId, run.piSessionId);} function taskPrompt(name: string, task: string): string {  return [    `You are "${name}", a subagent completing one task for a parent agent session.`,    "Work independently with the tools you have. When finished, end with a clear,",    "self-contained final report — it is the only thing the parent will see.",    "",    task,  ].join("\n");} /** Cap the report the parent sees, but say so rather than cutting silently — *  the full text stays readable in the subagent's own session. */function clampReport(text: string): string {  if (text.length <= MAX_RESULT_CHARS) return text;  const dropped = text.length - MAX_RESULT_CHARS;  return `${text.slice(0, MAX_RESULT_CHARS)}\n\n[truncated — ${dropped} more characters; open the subagent's session for the rest]`;} /** Whatever the child has written to its transcript so far. Readable mid-run, *  so a still-working subagent can be inspected rather than only awaited. */export function subagentReport(run: SubagentRun): LastAssistantResult {  if (!run.piSessionId) return { text: "", error: null };  const result = lastAssistantResult(run.cwd, run.piSessionId);  return { text: clampReport(result.text), error: result.error };} /** True while this runtime still has the child streaming. */export function subagentIsActive(run: SubagentRun): boolean {  return findChildRuntime(run)?.session.status.active === true;} /** Terminal states are write-once: the first writer wins, so a prompt that *  unblocks because stopSubagent aborted it cannot re-report itself as done. */function settle(run: SubagentRun, status: SubagentStatus, error?: string): void {  if (run.status !== "running") return;  run.status = status;  if (error) run.error = error;  run.finishedAt = new Date().toISOString();} /** Record the child's pi session id as soon as it exists — before the prompt, *  not after it returns. That is what makes a running child reachable: the UI *  can open it, and the no-nesting guard sees it in childPiSessionIds. */async function adoptChildSession(run: SubagentRun, piSessionId: string | null): Promise<void> {  if (!piSessionId || run.piSessionId === piSessionId) return;  run.piSessionId = piSessionId;  state().childPiSessionIds.add(piSessionId);  await setSubagentLink(piSessionId, run.parentPiSessionId, run.name).catch(() => undefined);} /** Stop a child this session spawned. Settling the run first frees its slot *  against the concurrency cap immediately, so the parent can spawn a *  replacement without waiting on the child's teardown. */export async function stopSubagent(parentPiSessionId: string, runId: string): Promise<SubagentRun> {  const run = findSubagent(parentPiSessionId, runId);  if (!run) throw new Error(`No subagent "${runId}" was spawned by this session.`);  if (run.status !== "running") return run;  settle(run, "cancelled");  const child = findChildRuntime(run);  if (child) {    await child.session.abort().catch(() => undefined);    await child.session.stop().catch(() => undefined);  }  return run;} function stoppedResult(  run: SubagentRun,  text: string,): { piSessionId: string | null; result: string } {  const partial = text ? `\n\nPartial work so far:\n${text}` : "";  return {    piSessionId: run.piSessionId,    result: `Subagent "${run.name}" was stopped before it reported.${partial}`,  };} export async function runSubagent(input: {  parentPiSessionId: string;  name: string;  task: string;  modelId?: string;}): Promise<{ piSessionId: string | null; result: string }> {  const registry = state();  const { parentPiSessionId } = input;   if (    registry.childPiSessionIds.has(parentPiSessionId) ||    sessionSubagentLink(parentPiSessionId) !== null  ) {    throw new Error("Subagents cannot spawn their own subagents.");  }  const parent = findParentRuntime(parentPiSessionId);  if (!parent) {    throw new Error("No running session found for this conversation.");  }  if (parent.sessionId.startsWith(SUBAGENT_SESSION_PREFIX)) {    throw new Error("Subagents cannot spawn their own subagents.");  }  const running = listSubagents(parentPiSessionId).filter((run) => run.status === "running");  if (running.length >= MAX_CONCURRENT_PER_PARENT) {    throw new Error(      `Too many subagents already running (${running.length}). Wait for one to finish, or stop one with subagent_stop.`,    );  }   const siblingCount = listSubagents(parentPiSessionId).length;  const runId = randomUUID().slice(0, 8);  const cwd = parent.session.status.cwd;  const run: SubagentRun = {    id: runId,    parentPiSessionId,    name: input.name.trim() || NICKNAMES[siblingCount % NICKNAMES.length],    task: input.task,    piSessionId: null,    runtimeSessionId: `${SUBAGENT_SESSION_PREFIX}${parentPiSessionId}:${runId}`,    cwd,    status: "running",    startedAt: new Date().toISOString(),    finishedAt: null,  };  const runs = registry.byParent.get(parentPiSessionId) ?? [];  runs.push(run);  registry.byParent.set(parentPiSessionId, runs);   const modelId = input.modelId?.trim() || parent.session.status.modelId;   try {    const { session } = piRuntimeManager.getSessionForLookup(run.runtimeSessionId, null);    await session.ensureStarted(modelId, cwd || undefined, null, {});    run.cwd = session.status.cwd || cwd;    await adoptChildSession(run, session.status.piSessionId);    await session.prompt(taskPrompt(run.name, input.task), () => {});    const status = session.status;    await adoptChildSession(run, status.piSessionId);    const report = subagentReport(run);    void session.stop().catch(() => undefined);    // A stop that landed mid-prompt already settled the run; report the partial    // work rather than overwriting the cancellation with done/error.    if (run.status === "cancelled") return stoppedResult(run, report.text);    const failure = status.lastError ?? report.error;    if (failure) {      settle(run, "error", failure);      throw new Error(`Subagent "${run.name}" failed: ${failure}`);    }    settle(run, "done");    return {      piSessionId: run.piSessionId,      result: report.text || "(the subagent produced no final text)",    };  } catch (error) {    if (run.status === "cancelled") return stoppedResult(run, subagentReport(run).text);    settle(run, "error", error instanceof Error ? error.message : "Subagent run failed");    throw error;  }}

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t