import type { AgentToolResult } from "@earendil-works/pi-coding-agent"; import { runSafely } from "#src/debug"; import type { ParentSnapshot } from "#src/lifecycle/parent-snapshot"; import type { AgentSpawnConfig } from "#src/lifecycle/subagent-manager"; import { buildDetails, formatLifetimeTokens, getStatusNote, textResult, } from "#src/tools/helpers"; import type { ResolvedSpawnConfig } from "#src/tools/spawn-config"; import type { ParentSessionInfo, Subagent } from "#src/types"; import { type AgentDetails, describeActivity, formatModelThinking, formatMs, SPINNER, } from "#src/ui/display"; /** Narrow manager interface for the foreground runner. */ export interface ForegroundManagerDeps { spawnAndWait( snapshot: ParentSnapshot, type: string, prompt: string, opts: Omit, ): Promise; } /** All values the foreground runner needs beyond the resolved config. */ export interface ForegroundParams { config: ResolvedSpawnConfig; snapshot: ParentSnapshot; parentSession: ParentSessionInfo; } /** * Run an agent synchronously in the foreground, streaming spinner updates. * Owns: spinner interval, streaming onUpdate callbacks, cleanup, and result formatting. */ export async function runForeground( manager: ForegroundManagerDeps, params: ForegroundParams, signal: AbortSignal | undefined, onUpdate: ((update: AgentToolResult) => void) | undefined, ) { const { identity, execution, presentation } = params.config; let spinnerFrame = 0; const startedAt = Date.now(); let recordRef: Subagent | undefined; const streamUpdate = (): void => { runSafely("foreground progress update", () => { const toolUses = recordRef?.toolUses ?? 0; const details: AgentDetails = { ...presentation.detailBase, toolUses, tokens: recordRef ? formatLifetimeTokens(recordRef) : "", // Read activity off the record; fall back to safe defaults before onSessionCreated fires turnCount: recordRef?.turnCount ?? 1, maxTurns: recordRef?.maxTurns ?? execution.effectiveMaxTurns, durationMs: Date.now() - startedAt, status: "running", activity: describeActivity( recordRef?.activeTools ?? new Map(), recordRef?.responseText ?? "", ), spinnerFrame: spinnerFrame % SPINNER.length, }; onUpdate?.({ content: [{ type: "text", text: `${toolUses} tool uses...` }], // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- Pi SDK ToolCallUpdate details type is not exported details: details as any, }); }); }; // Each update traverses the parent TUI tree; avoid doing that at animation // speed while the foreground tool already owns the progress surface. const spinnerInterval = setInterval(() => { spinnerFrame++; streamUpdate(); }, 500); let record: Subagent; try { streamUpdate(); record = await manager.spawnAndWait( params.snapshot, identity.subagentType, execution.prompt, { description: execution.description, model: execution.model, maxTurns: execution.effectiveMaxTurns, inheritContext: execution.inheritContext, thinkingLevel: execution.effectiveThinkingLevel, origin: "tool", invocation: execution.agentInvocation, signal, parentSession: params.parentSession, observer: { onSessionCreated: (agent) => { recordRef = agent; }, }, }, ); } catch (err) { // Only the spawn boundary converts to an error-text result. Post-spawn // formatting is internal code; its defects stay thrown tool errors so Pi // reports isError rather than a success-shaped result. return textResult(err instanceof Error ? err.message : String(err)); } finally { clearInterval(spinnerInterval); } // Foreground delivery returns the complete outcome inline, so retention can // use the shorter consumed-session window. record.markConsumed(); const tokenText = formatLifetimeTokens(record); const details = buildDetails(presentation.detailBase, record, { tokens: tokenText }); const fallbackNote = identity.fellBack ? `Note: Unknown agent type "${identity.rawType}" — using ${identity.subagentType}.\n\n` : ""; if (record.status === "error") { const partial = record.result?.trim(); return textResult( `${fallbackNote}Model: ${formatModelThinking(record.modelLabel, record.effectiveThinkingLevel)}\nRuntime: ${formatMs(details.durationMs)}\nAgent failed: ${record.error}` + (partial ? `\n\nPartial output before the failure:\n${partial}` : ""), details, ); } const durationMs = (record.completedAt ?? Date.now()) - record.startedAt; const statsParts = [`${record.toolUses} tool uses`]; if (tokenText) statsParts.push(tokenText); return textResult( `${fallbackNote}Model: ${formatModelThinking(record.modelLabel, record.effectiveThinkingLevel)}\nRuntime: ${formatMs(durationMs)}\nAgent completed (${statsParts.join(", ")})${getStatusNote(record.status)}.\n\n` + (record.result?.trim() ?? "No output."), details, ); }