import { getAgentDir, getMarkdownTheme, getPackageDir, keyHint, type ExtensionAPI, } from "@earendil-works/pi-coding-agent"; import { Container, Markdown, Spacer, Text } from "@earendil-works/pi-tui"; import { join } from "node:path"; import { pathToFileURL } from "node:url"; import { Type } from "typebox"; import { assertPersonaRuntimeReady, cancelPersonaChildren, createConsultProgressTracker, createRoundtableProgressTracker, createAgentScaffold, createDocsIndex, createPersonaInitDraft, createPersonaProjectScaffold, applyPersonaInitFromManifest, buildLegacyRoundtableParams, discoverPersonaProject, extractConsultAnswer, extractRoundtableAnswer, formatAgentScaffoldCreatedMessage, formatConsultBridgeResult, formatDocsIndexReport, formatDoctorReport, formatPersonaInitDraftAuthoringPrompt, formatPersonaInitManifestReport, formatPersonaProjectScaffoldCreatedMessage, formatPersonaList, formatRoundtableBridgeFailure, formatRoundtableBridgeResult, isDirectPersonaCommandName, parsePersonaIndexArgs, parsePersonaInitArgs, parsePersonaOnboardArgs, parsePersonaNewArgs, PI_SUBAGENTS_ROUNDTABLE_MINIMUM_VERSION, planPersonaInitFromManifest, resolveAgentLaunchRequest, resolveConsultLaunchRequest, resolveRoundtableLaunchRequest, resolveRoundtableSelectionRequest, resolveNativeChildTools, resolvePersonaBackend, runNativeRoundtable, runPersonaChild, runDoctor, sendPersonaOutput, runSubagentBridgeRequest, snapshotForkBranch, statusPersonaInitFromManifest, } from "../src/persona/index.js"; const ACTIVE_PERSONA_STATE_TYPE = "pi-persona-active"; const CONSULT_IDLE_TIMEOUT_MS = 180_000; const CONSULT_HEARTBEAT_MS = 10_000; const IS_PI_SUBAGENT_CHILD = process.env.PI_SUBAGENT_CHILD === "1"; export default function registerPiPersona(pi: ExtensionAPI): void { if (IS_PI_SUBAGENT_CHILD) return; let activePersonaName: string | undefined; let pendingRoundtable: { cwd: string; query: string; moderator: string } | undefined; let availableSkills: Array<{ name: string; filePath: string }> = []; const registeredPersonaCommands = new Set(); const updateActivePersonaStatus = (ctx: any) => { ctx.ui?.setStatus?.( "pi-persona-active", activePersonaName ? `persona /${activePersonaName}` : undefined, ); }; const restoreActivePersona = (ctx: any, options: { resetIfMissing?: boolean } = {}) => { const entries = ctx.sessionManager?.getBranch?.() ?? ctx.sessionManager?.getEntries?.() ?? []; for (let index = entries.length - 1; index >= 0; index -= 1) { const entry = entries[index]; if (entry?.type !== "custom" || entry.customType !== ACTIVE_PERSONA_STATE_TYPE) continue; const agentName = entry.data?.agentName; activePersonaName = typeof agentName === "string" && agentName ? agentName : undefined; return; } if (options.resetIfMissing) activePersonaName = undefined; }; const setActivePersona = (ctx: any, agentName: string | undefined) => { activePersonaName = agentName; pi.appendEntry(ACTIVE_PERSONA_STATE_TYPE, { agentName: agentName ?? null }); updateActivePersonaStatus(ctx); }; pi.registerTool({ name: "persona_consult", label: "pi-persona", description: "Ask a known Pi Persona peer for a focused consult through the configured child backend and return the result.", promptSnippet: "Use persona_consult only when the active Pi Persona needs another known persona's expertise. Provide a narrow requester-written summary and synthesize the returned consultant answer.", parameters: Type.Object({ requester: Type.String({ description: "Active Pi Persona requester agent name" }), consultant: Type.String({ description: "Known Pi Persona consultant agent name" }), question: Type.String({ description: "Specific question for the consultant" }), summary: Type.String({ description: "Requester-authored concise context summary" }), constraints: Type.Optional(Type.String({ description: "Constraints the consultant must follow" })), expectedOutput: Type.Optional(Type.String({ description: "Requested answer shape" })), context: Type.Optional(Type.String({ enum: ["fresh", "fork"], description: "fresh by default; fork inherits the current conversation branch", })), }), renderCall(args, theme, context) { const consultant = args.consultant || "consultant"; const question = args.question || "(query pending)"; const contextLine = formatConsultContextLine(args.context); let text = theme.fg("toolTitle", theme.bold(`Consulting ${consultant}`)); text += `\n${theme.fg("muted", `Query: ${context.expanded ? question : truncatePanelText(question, 100)}`)}`; text += `\n${theme.fg("dim", contextLine)}`; if (context.expanded) { text += `\n\n${theme.fg("muted", `Requester: ${args.requester || "(unknown)"}`)}`; if (args.summary) text += `\n\n${theme.fg("muted", "Summary:")}\n${theme.fg("dim", args.summary)}`; if (args.constraints) text += `\n\n${theme.fg("muted", "Constraints:")}\n${theme.fg("dim", args.constraints)}`; if (args.expectedOutput) text += `\n\n${theme.fg("muted", "Expected output:")}\n${theme.fg("dim", args.expectedOutput)}`; } else { text += `\n${theme.fg("dim", keyHint("app.tools.expand", "to expand"))}`; } return new Text(text, 0, 0); }, renderResult(result, { expanded, isPartial }, theme, context = {} as any) { const output = firstToolResultText(result); if (isPartial) { return new Text(theme.fg("toolOutput", stripConsultProgressHeading(output)), 0, 0); } const failed = context.isError; const status = failed ? theme.fg("error", "✗ Consultation failed") : theme.fg("success", "✓ Consultation complete"); if (!expanded) { return new Text(status, 0, 0); } const container = new Container(); container.addChild(new Text(status, 0, 0)); if (output) { container.addChild(new Spacer(1)); container.addChild(new Markdown(output, 0, 0, getMarkdownTheme())); } return container; }, async execute(toolCallId, params, signal, onUpdate, ctx) { let progress: ReturnType | undefined; try { assertNotAborted(signal, "consultation"); restoreActivePersona(ctx); if (!activePersonaName) { throw new Error("persona_consult requires an active persona; run /persona use first"); } if (params.requester !== activePersonaName) { throw new Error(`persona_consult requester must match active persona '${activePersonaName}'`); } const consult = await resolveConsultLaunchRequest(ctx.cwd, params); const backend = await resolvePersonaBackend(ctx.cwd); await assertPersonaRuntimeReady(ctx.cwd, { backend }); progress = createConsultProgressReporter(onUpdate, consult.consultant.name); if (backend === "legacy") { const response = await runSubagentBridgeRequest(pi, ctx, consult.subagentParams, { signal, onUpdate(update: unknown) { progress?.update(update); }, }); const answer = await extractConsultAnswer(response); if (response.isError === true || answer.source === "missing") { throw new Error(formatConsultBridgeResult(consult, answer.text, true)); } return { content: [{ type: "text", text: formatConsultBridgeResult(consult, answer.text) }], details: { backend, requester: consult.requester.name, consultant: consult.consultant.name, context: consult.context, answer }, }; } const request = await createNativeRequest(consult.scope, consult.task, consult.context, toolCallId, ctx, availableSkills, undefined, signal); const result = await runPersonaChild(request, { signal, onUpdate(update: unknown) { progress?.update(update); }, }); return { content: [{ type: "text", text: formatConsultBridgeResult(consult, result.text) }], details: { backend, requester: consult.requester.name, consultant: consult.consultant.name, context: consult.context, model: result.model }, usage: result.usage, }; } finally { progress?.stop(); } }, }); pi.registerTool({ name: "persona_roundtable", label: "pi-persona", description: "Run one blueprint round-table over specialists selected by the active primary generalist.", promptSnippet: "Use persona_roundtable exactly once after /persona-roundtable asks the primary generalist to select a roster. Preserve the user's query, give one concrete reason per specialist, and present the returned moderator synthesis once.", parameters: Type.Object({ query: Type.String({ description: "The user's round-table query, unchanged" }), selections: Type.Array(Type.Object({ name: Type.String({ description: "Selected specialist persona name" }), reason: Type.String({ description: "Concrete reason this specialist is useful" }), }, { additionalProperties: false }), { minItems: 1, maxItems: 5, description: "One to five specialists selected from the project roster", }), context: Type.Optional(Type.String({ enum: ["fresh", "fork"], description: "fresh by default; fork only when full conversation context is deliberately required", })), }), renderCall(args, theme, context) { const selections = Array.isArray(args.selections) ? args.selections : []; let text = theme.fg("toolTitle", theme.bold(`Round-table · ${selections.length || "…"} specialists`)); text += `\n${theme.fg("muted", `Query: ${context.expanded ? args.query || "(query pending)" : truncatePanelText(args.query || "(query pending)", 100)}`)}`; text += `\n${theme.fg("dim", formatRoundtableContextLine(args.context))}`; if (context.expanded) { text += `\n${theme.fg("muted", "Moderator: active primary generalist")}`; text += `\n\n${theme.fg("muted", "Selected panel:")}`; for (const selection of selections) { text += `\n${theme.fg("toolTitle", selection.name || "(unknown)")} ${theme.fg("dim", `— ${selection.reason || "reason pending"}`)}`; } text += `\n\n${theme.fg("muted", "Process:")}`; text += `\n${theme.fg("dim", "1. Independent positions")}`; text += `\n${theme.fg("dim", "2. Peer reveal and revision")}`; text += `\n${theme.fg("dim", "3. Primary-generalist synthesis")}`; } else { const names = selections.map((selection: any) => selection.name).filter(Boolean).join(", "); if (names) text += `\n${theme.fg("dim", `Panel: ${names}`)}`; text += `\n${theme.fg("dim", keyHint("app.tools.expand", "to inspect selection"))}`; } return new Text(text, 0, 0); }, renderResult(result, { expanded, isPartial }, theme, context = {} as any) { const output = firstToolResultText(result); if (isPartial) { return new Text(theme.fg("toolOutput", stripRoundtableProgressHeading(output)), 0, 0); } const failed = context.isError; const status = failed ? theme.fg("error", "✗ Round-table failed") : theme.fg("success", "✓ Round-table complete"); const process = formatRoundtableProcessLine((result.details as { process?: any } | undefined)?.process); if (!expanded) { return new Text(`${status}${process ? `\n${theme.fg("dim", process)}` : ""}`, 0, 0); } const container = new Container(); container.addChild(new Text(`${status}${process ? `\n${theme.fg("dim", process)}` : ""}`, 0, 0)); if (output) { container.addChild(new Spacer(1)); container.addChild(new Markdown(output, 0, 0, getMarkdownTheme())); } return container; }, async execute(toolCallId, params, signal, onUpdate, ctx) { let progress: ReturnType | undefined; try { assertNotAborted(signal, "round-table"); restoreActivePersona(ctx); if (!pendingRoundtable || pendingRoundtable.cwd !== ctx.cwd) { throw new Error("persona_roundtable requires a pending /persona-roundtable request"); } if (params.query.trim() !== pendingRoundtable.query) { throw new Error("persona_roundtable query must match the pending /persona-roundtable request unchanged"); } const roundtable = await resolveRoundtableLaunchRequest(ctx.cwd, params); if (activePersonaName !== roundtable.generalist.name || pendingRoundtable.moderator !== roundtable.generalist.name) { throw new Error(`persona_roundtable requires active primary generalist '${roundtable.generalist.name}'`); } const backend = await resolvePersonaBackend(ctx.cwd); const runtime = await assertPersonaRuntimeReady(ctx.cwd, { backend, minimumPiSubagentsVersion: PI_SUBAGENTS_ROUNDTABLE_MINIMUM_VERSION, }); pendingRoundtable = undefined; progress = createRoundtableProgressReporter(onUpdate, roundtable); if (backend === "legacy") { const response = await runSubagentBridgeRequest(pi, ctx, buildLegacyRoundtableParams(roundtable, runtime.piSubagents?.version), { signal, idleTimeoutMs: false, onUpdate(update: unknown) { progress?.update(update); }, }); const process = createRoundtableProcessDetails(roundtable, response, progress.summary()); if (response.isError === true) throw new Error(formatRoundtableBridgeFailure(roundtable, response)); const answer = await extractRoundtableAnswer(response, roundtable.generalist.name); if (answer.source === "missing") throw new Error(formatRoundtableBridgeFailure(roundtable, response)); return { content: [{ type: "text", text: formatRoundtableBridgeResult(roundtable, answer.text) }], details: { backend, moderator: roundtable.generalist.name, roster: roundtable.roster.map((agent: any) => agent.name), context: roundtable.context, process, answer }, }; } const branch = roundtable.context === "fork" ? snapshotForkBranch(ctx.sessionManager, toolCallId) : []; for (const scope of roundtable.scopes.values()) { assertNotAborted(signal, "round-table"); await createNativeRequest(scope, "preflight", roundtable.context, toolCallId, ctx, availableSkills, branch, signal); } const native = await runNativeRoundtable(roundtable, async ({ scope, task, index, signal: childSignal, onUpdate: childUpdate }: any) => { const request = await createNativeRequest(scope, task, roundtable.context, toolCallId, ctx, availableSkills, branch, childSignal); return runPersonaChild(request, { index, signal: childSignal, idleTimeoutMs: false, onUpdate: childUpdate }); }, { signal, onUpdate: (update: unknown) => progress?.update(update) }); const process = createNativeRoundtableProcessDetails(roundtable, native, progress.summary()); return { content: [{ type: "text", text: formatRoundtableBridgeResult(roundtable, native.text) }], details: { backend, moderator: roundtable.generalist.name, roster: roundtable.roster.map((agent: any) => agent.name), context: roundtable.context, process, }, usage: combineUsage(native.steps.map((step: any) => step.usage)), }; } finally { progress?.stop(); } }, }); const activatePersona = async (agentName: string, args: string, ctx: any) => { try { const launch = await resolveAgentLaunchRequest(ctx.cwd, agentName, { task: normalizeCommandText(args), }); setActivePersona(ctx, launch.agentName); if (!launch.userMessage) { ctx.ui.notify(`Active persona: /${launch.agentName}`, "info"); return; } pi.sendUserMessage( launch.userMessage, ctx.isIdle() ? undefined : { deliverAs: "followUp" }, ); } catch (error) { sendPersonaOutput(pi, ctx, formatPersonaCommandError(agentName, error), "error"); } }; const registerPersonaCommand = (agentName: string) => { if (!isDirectPersonaCommandName(agentName)) return; if (registeredPersonaCommands.has(agentName)) return; pi.registerCommand(agentName, { description: `Activate Pi Persona agent: ${agentName}`, handler: async (args, ctx) => { await activatePersona(agentName, args, ctx); }, }); registeredPersonaCommands.add(agentName); }; registerPersonaCommand("generalist"); const registerProjectCommands = async (cwd: string) => { const project = await discoverPersonaProject(cwd); for (const agent of project.agents) { registerPersonaCommand(agent.name); } return project; }; pi.registerTool({ name: "persona_init", label: "Persona Init", description: "Plan, apply, or inspect a Pi Persona setup manifest during assisted authoring.", promptSnippet: "Use persona_init with action plan before apply. Ask for explicit user approval, then call action apply with confirmed: true. Apply includes persona doctor verification; use status afterward.", parameters: Type.Object({ action: Type.String({ enum: ["plan", "apply", "status"], description: "Manifest action to perform", }), source: Type.String({ description: "Workspace-relative manifest YAML path" }), confirmed: Type.Optional(Type.Boolean({ description: "Required for apply; set true only after explicit user approval", })), }), async execute(_toolCallId, params, _signal, _onUpdate, ctx) { try { let result; let doctor; if (params.action === "plan") { result = await planPersonaInitFromManifest(ctx.cwd, params.source); } else if (params.action === "status") { result = await statusPersonaInitFromManifest(ctx.cwd, params.source); } else { if (params.confirmed !== true) { throw new Error("persona_init apply requires confirmed: true after explicit user approval"); } result = await applyPersonaInitFromManifest(ctx.cwd, params.source); await registerProjectCommands(ctx.cwd); const index = await createDocsIndex(ctx.cwd, { all: true }); const status = await statusPersonaInitFromManifest(ctx.cwd, params.source); doctor = await runDoctor(ctx.cwd); const project = await discoverPersonaProject(ctx.cwd); const primary = project.agents.find((agent: any) => agent.role === "generalist" && agent.primary) ?? project.agents.find((agent: any) => agent.role === "generalist"); if (primary && doctor.status !== "error") setActivePersona(ctx, primary.name); return { content: [{ type: "text", text: [ formatPersonaInitManifestReport(result, { doctorIncluded: true, needsAttention: doctor.status === "error" }), formatDoctorReport(doctor), formatPersonaList(project), doctor.status === "error" ? "Onboarding needs attention; fix the doctor errors above, then run /persona onboard again." : "What would you like help with first?", ].join("\n\n"), }], details: { ...result, index, status, doctor }, }; } return { content: [{ type: "text", text: formatPersonaInitManifestReport(result), }], details: result, }; } catch (error) { throw error instanceof Error ? error : new Error(String(error)); } }, }); pi.on("session_start", async (_event, ctx) => { try { restoreActivePersona(ctx, { resetIfMissing: true }); updateActivePersonaStatus(ctx); await registerProjectCommands(ctx.cwd); } catch (error) { console.error("Failed to register Pi Persona commands:", error); } }); pi.on("session_shutdown", () => { cancelPersonaChildren(); }); pi.on("before_agent_start", async (event, ctx) => { availableSkills = (event.systemPromptOptions?.skills ?? []) .filter((skill: any) => typeof skill?.name === "string" && typeof skill?.filePath === "string") .map((skill: any) => ({ name: skill.name, filePath: skill.filePath })); restoreActivePersona(ctx); if (!activePersonaName) { updateActivePersonaStatus(ctx); return undefined; } updateActivePersonaStatus(ctx); try { const launch = await resolveAgentLaunchRequest(ctx.cwd, activePersonaName); return { systemPrompt: `${event.systemPrompt}\n\n${launch.systemPrompt}`, }; } catch (error) { const failedPersonaName = activePersonaName; const message = error instanceof Error ? error.message : String(error); setActivePersona(ctx, undefined); return { systemPrompt: `${event.systemPrompt}\n\n## Pi Persona\n\nPreviously active persona /${failedPersonaName} is not available in this workspace: ${message}. Answer normally and tell the user to run /persona-list or choose another persona.`, }; } }); pi.registerCommand("persona", { description: "Pi Persona commands. Start with /persona onboard; use /persona quick-start for a minimal scaffold.", handler: async (args, ctx) => { const trimmed = args.trim(); const [subcommand = ""] = trimmed.split(/\s+/, 1); if (subcommand === "use") { const parsed = parsePersonaUseArgs(trimmed.slice("use".length)); if (!parsed) { sendPersonaOutput(pi, ctx, "Usage: /persona use [query]", "error"); return; } await activatePersona(parsed.agentName, parsed.task, ctx); return; } if (subcommand === "status") { restoreActivePersona(ctx); sendPersonaOutput( pi, ctx, activePersonaName ? `Active persona: /${activePersonaName}` : "Active persona: none", "info", ); updateActivePersonaStatus(ctx); return; } if (subcommand === "clear") { setActivePersona(ctx, undefined); sendPersonaOutput(pi, ctx, "Active persona: none", "info"); return; } if (subcommand === "doctor") { const result = await runDoctor(ctx.cwd); const report = formatDoctorReport(result); const level = result.status === "error" ? "error" : result.status === "warning" ? "warning" : "info"; sendPersonaOutput(pi, ctx, report, level); return; } if (subcommand === "onboard" || (subcommand === "init" && trimmed === "init")) { try { const parsed = parsePersonaOnboardArgs(subcommand === "onboard" ? trimmed.slice("onboard".length) : ""); const project = await discoverPersonaProject(ctx.cwd); if (project.agents.length > 0) { let status; try { status = await statusPersonaInitFromManifest(ctx.cwd, parsed.out); } catch (error) { if ((error as any)?.code !== "ENOENT") throw error; } const doctor = await runDoctor(ctx.cwd); sendPersonaOutput(pi, ctx, [ status?.items.every((item: any) => item.state === "done") && doctor.status !== "error" ? "Pi Persona onboarding is complete." : "Pi Persona is already set up in this workspace.", "", formatDoctorReport(doctor), "", formatPersonaList(project), ].join("\n"), doctor.status === "error" ? "error" : "info"); return; } const result = await createPersonaInitDraft(ctx.cwd, parsed.out, { resume: true }); sendPersonaOutput(pi, ctx, formatPersonaInitManifestReport(result), "info"); pi.sendUserMessage( formatPersonaInitDraftAuthoringPrompt(result), ctx.isIdle() ? undefined : { deliverAs: "followUp" }, ); } catch (error) { sendPersonaOutput(pi, ctx, error instanceof Error ? error.message : String(error), "error"); } return; } if (subcommand === "quick-start") { try { const result = await createPersonaProjectScaffold(ctx.cwd); registerPersonaCommand(result.primaryGeneralist); sendPersonaOutput(pi, ctx, formatPersonaProjectScaffoldCreatedMessage(result), "info"); } catch (error) { sendPersonaOutput(pi, ctx, error instanceof Error ? error.message : String(error), "error"); } return; } if (subcommand === "init") { const rawArgs = trimmed.slice("init".length).trim(); try { const parsed = parsePersonaInitArgs(rawArgs); if (parsed.mode === "draft") { const result = await createPersonaInitDraft(ctx.cwd, parsed.out); sendPersonaOutput(pi, ctx, formatPersonaInitManifestReport(result), "info"); pi.sendUserMessage( formatPersonaInitDraftAuthoringPrompt(result), ctx.isIdle() ? undefined : { deliverAs: "followUp" }, ); return; } if (parsed.mode === "plan") { const result = await planPersonaInitFromManifest(ctx.cwd, parsed.from); sendPersonaOutput(pi, ctx, formatPersonaInitManifestReport(result), "info"); return; } if (parsed.mode === "status") { const result = await statusPersonaInitFromManifest(ctx.cwd, parsed.from); sendPersonaOutput(pi, ctx, formatPersonaInitManifestReport(result), "info"); return; } const result = await applyPersonaInitFromManifest(ctx.cwd, parsed.from); await registerProjectCommands(ctx.cwd); sendPersonaOutput(pi, ctx, formatPersonaInitManifestReport(result), "info"); } catch (error) { sendPersonaOutput(pi, ctx, error instanceof Error ? error.message : String(error), "error"); } return; } if (subcommand === "new") { const rawName = trimmed.slice("new".length).trim(); if (!rawName) { ctx.ui.notify(personaUsage(), "error"); return; } try { const parsed = parsePersonaNewArgs(rawName); const result = await createAgentScaffold(ctx.cwd, parsed.rawName, parsed.options); registerPersonaCommand(result.agentName); sendPersonaOutput(pi, ctx, formatAgentScaffoldCreatedMessage(result), "info"); } catch (error) { sendPersonaOutput(pi, ctx, error instanceof Error ? error.message : String(error), "error"); } return; } if (subcommand === "index") { const rawArgs = trimmed.slice("index".length).trim(); try { const parsed = parsePersonaIndexArgs(rawArgs); const result = await createDocsIndex(ctx.cwd, parsed); const hasError = result.results.some((entry: any) => entry.status === "error"); sendPersonaOutput(pi, ctx, formatDocsIndexReport(result), hasError ? "error" : "info"); } catch (error) { sendPersonaOutput(pi, ctx, error instanceof Error ? error.message : String(error), "error"); } return; } sendPersonaOutput(pi, ctx, personaUsage(), "info"); }, }); pi.registerCommand("persona-list", { description: "List available Pi Persona agents", handler: async (_args, ctx) => { try { const project = await registerProjectCommands(ctx.cwd); sendPersonaOutput(pi, ctx, formatPersonaList(project), "info"); } catch (error) { sendPersonaOutput(pi, ctx, error instanceof Error ? error.message : String(error), "error"); } }, }); pi.registerCommand("persona-roundtable", { description: "Run a Pi Persona round-table over selected specialists", handler: async (args, ctx) => { const query = normalizeCommandText(args); if (!query) { ctx.ui.notify('Usage: /persona-roundtable "query"', "error"); return; } try { await registerProjectCommands(ctx.cwd); const selectionRequest = await resolveRoundtableSelectionRequest(ctx.cwd, { query }); await assertPersonaRuntimeReady(ctx.cwd, { minimumPiSubagentsVersion: PI_SUBAGENTS_ROUNDTABLE_MINIMUM_VERSION, }); pendingRoundtable = { cwd: ctx.cwd, query: selectionRequest.query, moderator: selectionRequest.generalist.name, }; await activatePersona(selectionRequest.generalist.name, selectionRequest.userMessage, ctx); } catch (error) { sendPersonaOutput(pi, ctx, error instanceof Error ? error.message : String(error), "error"); } }, }); } function normalizeCommandText(value: string): string { const trimmed = value.trim(); if (trimmed.length >= 2) { const first = trimmed[0]; const last = trimmed[trimmed.length - 1]; if ((first === '"' && last === '"') || (first === "'" && last === "'")) { return trimmed.slice(1, -1).trim(); } } return trimmed; } function formatPersonaCommandError(agentName: string, error: unknown): string { const message = error instanceof Error ? error.message : String(error); if (agentName === "generalist" && message === "Unknown agent: generalist") { return "No persona setup found. Run /persona onboard."; } if (message === `Unknown agent: ${agentName}`) { return `/${agentName} is not available in this workspace. Run /persona-list.`; } return message; } function personaUsage(): string { return [ "Usage: /persona onboard [--out ]", "Usage: /persona quick-start", "Usage: /persona use [query]", "Usage: /persona init (alias for /persona onboard)", "Usage: /persona init draft --out (legacy)", "Usage: /persona init --plan --from ", "Usage: /persona init --from ", "Usage: /persona init status --from ", "Usage: /persona status", "Usage: /persona clear", "Usage: /persona doctor", "Usage: /persona new [--role generalist|specialist] [--description \"...\"] [--docs path[,path]] [--skills native-skill[,native-skill]]", "Usage: /persona index [docs-dir]", "Usage: /persona index --all", ].join("\n"); } function parsePersonaUseArgs(value: string): { agentName: string; task: string } | null { const match = value.trim().match(/^(\S+)(?:\s+([\s\S]*))?$/); if (!match) return null; return { agentName: match[1], task: match[2] ?? "", }; } function formatConsultContextLine(context: unknown): string { return context === "fork" ? "Context: fork · current conversation branch inherited" : "Context: fresh · conversation history not included"; } function formatRoundtableContextLine(context: unknown): string { return context === "fork" ? "Context: fork · current conversation branch inherited" : "Context: fresh · specialists receive only resolved persona context"; } function truncatePanelText(value: unknown, maxLength: number): string { const text = String(value).replace(/\s+/g, " ").trim(); return text.length > maxLength ? `${text.slice(0, maxLength - 1)}…` : text; } function firstToolResultText(result: any): string { const part = result?.content?.find((entry: any) => entry?.type === "text"); return typeof part?.text === "string" ? part.text.trim() : ""; } function stripConsultProgressHeading(value: string): string { return value.replace(/^\[pi-persona\] Consulting [^\n]+\n+/, "").trim(); } function stripRoundtableProgressHeading(value: string): string { return value.replace(/^\[pi-persona\] Round-table\n+/, "").trim(); } function createRoundtableProcessDetails(roundtable: any, response: any, summary: any) { const results = Array.isArray(response?.result?.details?.results) ? response.result.details.results : []; return { specialists: roundtable.roster.length, rounds: 2, expectedSteps: roundtable.roster.length * 2 + 1, completedSteps: results.filter((entry: any) => entry?.exitCode === 0 || entry?.status === "completed").length, failedSteps: results.filter((entry: any) => entry?.exitCode > 0 || entry?.status === "failed").length, ...summary, }; } function createNativeRoundtableProcessDetails(roundtable: any, result: any, summary: any) { return { specialists: roundtable.roster.length, rounds: 2, expectedSteps: roundtable.roster.length * 2 + 1, completedSteps: result.steps.length, failedSteps: 0, ...summary, }; } async function createNativeRequest( scope: any, task: string, context: string, toolCallId: string, ctx: any, loadedSkills: Array<{ name: string; filePath: string }>, frozenBranch?: any[], signal?: AbortSignal, ) { assertNotAborted(signal, "child launch"); let requestedTools; try { requestedTools = resolveNativeChildTools(scope.tools); } catch (error) { throw new Error(`${scope.agent.name}: ${error instanceof Error ? error.message : String(error)}`); } const skillPaths = scope.skills.map((name: string) => { const matches = loadedSkills.filter((skill) => skill.name === name); if (matches.length !== 1) { throw new Error(`Native backend requires exactly one loaded Pi skill named '${name}' for ${scope.agent.name}; found ${matches.length}.`); } return matches[0].filePath; }); const { model, thinkingLevel } = resolveNativeModel(scope.agent.model, ctx); const dynamicProviders = ctx.modelRegistry.getRegisteredProviderIds?.() ?? []; if (dynamicProviders.includes(model.provider)) { throw new Error(`Native backend cannot use extension-registered provider '${model.provider}' because child extensions are disabled.`); } const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model); if (!auth.ok) throw new Error(auth.error); assertNotAborted(signal, "child launch"); return { cwd: ctx.cwd, agentDir: getAgentDir(), sdkEntry: pathToFileURL(join(getPackageDir(), "dist/index.js")).href, authStorageEntry: pathToFileURL(join(getPackageDir(), "dist/core/auth-storage.js")).href, projectTrusted: ctx.isProjectTrusted(), personaName: scope.agent.name, systemPrompt: `${scope.prompt}\n\n## Native Child Boundary\n\nThis is a one-shot leaf session. Use only the tools declared for this persona and return the requested answer directly. Do not attempt delegation.`, task, model: { provider: model.provider, id: model.id }, thinkingLevel, auth, skillNames: scope.skills, skillPaths, tools: requestedTools, context, branch: context === "fork" ? frozenBranch ?? snapshotForkBranch(ctx.sessionManager, toolCallId) : [], }; } function assertNotAborted(signal: AbortSignal | undefined, operation: string) { if (signal?.aborted) throw new Error(`Native Pi Persona ${operation} was cancelled.`); } function resolveNativeModel(value: unknown, ctx: any) { if (typeof value !== "string" || !value.trim()) { if (!ctx.model) throw new Error("Native backend requires an active Pi model."); return { model: ctx.model, thinkingLevel: ctx.thinkingLevel }; } const thinkingMatch = value.trim().match(/:(off|minimal|low|medium|high|xhigh|max)$/); const spec = thinkingMatch ? value.trim().slice(0, -thinkingMatch[0].length) : value.trim(); const separator = spec.indexOf("/"); const provider = separator < 0 ? undefined : spec.slice(0, separator); const id = separator < 0 ? spec : spec.slice(separator + 1); const matches = ctx.modelRegistry.getAll().filter((model: any) => model.id === id && (!provider || model.provider === provider)); if (matches.length !== 1) throw new Error(`Native backend could not resolve persona model '${value}' uniquely; found ${matches.length}.`); return { model: matches[0], thinkingLevel: thinkingMatch?.[1] ?? ctx.thinkingLevel }; } function combineUsage(usages: any[]) { const total: any = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, }; for (const usage of usages) { if (!usage) continue; for (const key of ["input", "output", "cacheRead", "cacheWrite", "totalTokens"]) total[key] += Number(usage[key]) || 0; for (const key of ["input", "output", "cacheRead", "cacheWrite", "total"]) total.cost[key] += Number(usage.cost?.[key]) || 0; } return total; } function formatRoundtableProcessLine(process: any): string { if (!process) return ""; const parts = [ `${process.specialists} specialists`, `${process.rounds} rounds`, `${process.completedSteps}/${process.expectedSteps} steps complete`, `${formatPanelDuration(process.elapsedMs)} elapsed`, ]; if (process.toolCount > 0) parts.push(`${process.toolCount} tools`); if (process.turns > 0) parts.push(`${process.turns} turns`); if (process.categories?.files > 0) parts.push(`${process.categories.files} files`); if (process.sources > 0) parts.push(`${process.sources} external sources`); if (process.recoverableErrors > 0) parts.push(`${process.recoverableErrors} recoverable errors`); if (process.failedSteps > 0) parts.push(`${process.failedSteps} failed steps`); return parts.join(" · "); } function formatPanelDuration(value: unknown): string { const seconds = Math.max(0, Math.floor((Number(value) || 0) / 1_000)); const minutes = Math.floor(seconds / 60); return minutes > 0 ? `${minutes}:${String(seconds % 60).padStart(2, "0")}` : `${seconds}s`; } function createConsultProgressReporter(onUpdate: any, agent: string) { const tracker = createConsultProgressTracker(agent, { idleTimeoutMs: CONSULT_IDLE_TIMEOUT_MS }); let latestUpdate: unknown; let lastPublishedAt = 0; const publish = (force = false) => { if (!onUpdate) return; const now = Date.now(); if (!force && now - lastPublishedAt < 1_000) return; lastPublishedAt = now; onUpdate({ content: [{ type: "text", text: tracker.format(now) }], details: latestUpdate, }); }; const heartbeat = setInterval(() => publish(true), CONSULT_HEARTBEAT_MS); (heartbeat as any).unref?.(); publish(true); return { update(update: unknown) { latestUpdate = update; tracker.update(update); publish(); }, stop() { clearInterval(heartbeat); }, }; } function createRoundtableProgressReporter(onUpdate: any, roundtable: any) { const tracker = createRoundtableProgressTracker(roundtable.roster.map((agent: any) => agent.name), { idleTimeoutMs: false, moderator: roundtable.generalist.name, }); let latestUpdate: unknown; let lastPublishedAt = 0; const publish = (force = false) => { if (!onUpdate) return; const now = Date.now(); if (!force && now - lastPublishedAt < 1_000) return; lastPublishedAt = now; onUpdate({ content: [{ type: "text", text: tracker.format(now) }], details: latestUpdate, }); }; const heartbeat = setInterval(() => publish(true), CONSULT_HEARTBEAT_MS); (heartbeat as any).unref?.(); publish(true); return { update(update: unknown) { const firstUpdate = latestUpdate === undefined; latestUpdate = update; tracker.update(update); publish(firstUpdate); }, summary() { return tracker.snapshot(); }, stop() { clearInterval(heartbeat); }, }; }