import { completionResultForOutcome, type CompletionFailureCategory, type CompletionOutcome, type CompletionResult, } from "./completion.ts"; import { freezeRuntimePlan, type ResolvedRuntimePlan, } from "./runtime-routing.ts"; import type { OperationArtifacts } from "./operation-artifacts.ts"; import type { CompletionSidecarResult, CompletionSidecarRuntime, } from "./completion-sidecar.ts"; import { formatElapsed } from "./format.ts"; /** Immutable parent-side data needed to complete one Completion handoff. */ export interface CompletionHandoffContext { readonly operationId: string; readonly name: string; readonly task: string; readonly agent?: string; readonly artifacts?: OperationArtifacts; readonly startedAt: number; readonly runtimePlan?: ResolvedRuntimePlan; } /** Result enriched for parent-facing foreground or background delivery. */ export interface CompletionHandoffResult { name: string; task: string; summary: string; exitCode: number; elapsed: number; error?: string; errorMessage?: string; failureCategory?: CompletionFailureCategory; runtimePlan?: ResolvedRuntimePlan; /** Actual child runtime facts when no complete parent runtime plan exists. */ runtime?: CompletionSidecarRuntime; } export type SubagentToolResult = { content: Array<{ type: "text"; text: string }>; details: Record; }; export type CompletionSteerMessage = { customType: "subagent_result"; content: string; display: true; details: Record; }; function fallbackCompletionSummary(result: Pick): string { return result.errorMessage ? `Subagent error: ${result.errorMessage}` : result.exitCode !== 0 ? `Sub-agent exited with code ${result.exitCode}` : "Sub-agent exited without output"; } function snapshotCompletionHandoffContext( context: CompletionHandoffContext, ): CompletionHandoffContext { const runtimePlan = freezeRuntimePlan(context.runtimePlan); return Object.freeze({ ...context, ...(runtimePlan ? { runtimePlan } : {}), }); } function mergeObservedRuntimePlan( runtimePlan: ResolvedRuntimePlan | undefined, runtime: CompletionSidecarRuntime | undefined, ): ResolvedRuntimePlan | undefined { if (!runtimePlan || !runtime) return runtimePlan; const observedModel = runtime.provider && runtime.modelId ? `${runtime.provider}/${runtime.modelId}` : undefined; const mismatch = observedModel && observedModel !== runtimePlan.model ? `Resolved model ${runtimePlan.model} but child reported ${observedModel}` : undefined; if (!observedModel && !runtime.thinking) return runtimePlan; return { ...runtimePlan, ...(runtime.thinking ? { thinking: runtime.thinking } : {}), observed: { ...runtimePlan.observed, ...(observedModel ? { model: observedModel } : {}), ...(runtime.thinking ? { thinking: runtime.thinking } : {}), }, ...(mismatch ? { runtimeMismatch: mismatch } : {}), }; } function extractStructuredCompletionResult( context: CompletionHandoffContext, result: CompletionSidecarResult, ): { summary: string; runtimePlan?: ResolvedRuntimePlan; runtime?: CompletionSidecarRuntime } { const runtime = result.runtime ? Object.freeze({ ...result.runtime }) : undefined; return { summary: result.summary, runtimePlan: mergeObservedRuntimePlan(context.runtimePlan, runtime), ...(runtime ? { runtime } : {}), }; } /** * Turn authoritative Completion outcome into one immutable handoff result. * The Completion sidecar is the only source of child result data. When an * older sidecar omits its optional result, the outcome still remains valid and * receives a deterministic summary without consulting a transcript. */ export function prepareCompletionHandoff( input: CompletionHandoffContext, outcome: CompletionOutcome, elapsed: number, ): CompletionHandoffResult { const context = snapshotCompletionHandoffContext(input); const completion = completionResultForOutcome(outcome); const extracted = outcome.result ? extractStructuredCompletionResult(context, outcome.result) : { summary: fallbackCompletionSummary(completion), runtimePlan: context.runtimePlan }; return Object.freeze({ name: context.name, task: context.task, summary: extracted.summary, exitCode: completion.exitCode, elapsed, ...(completion.errorMessage ? { errorMessage: completion.errorMessage } : {}), ...(completion.failureCategory ? { failureCategory: completion.failureCategory } : {}), ...(extracted.runtimePlan ? { runtimePlan: freezeRuntimePlan(extracted.runtimePlan) } : {}), ...(extracted.runtime ? { runtime: extracted.runtime } : {}), }); } export function cancelledCompletionHandoff( input: CompletionHandoffContext, elapsed: number, runtimePlan = input.runtimePlan, ): CompletionHandoffResult { const context = snapshotCompletionHandoffContext(input); return Object.freeze({ name: context.name, task: context.task, summary: "Subagent cancelled.", exitCode: 1, elapsed, error: "cancelled", ...(runtimePlan ? { runtimePlan: freezeRuntimePlan(runtimePlan) } : {}), }); } /** * Build the parent-facing result for a failed Completion watch, e.g. when * Completion supervision throws before an outcome exists. Deliberately sets * `error` rather than `errorMessage`: the presentation layer reserves the * errorMessage branch for child-reported provider/agent errors. */ export function failedCompletionHandoff( input: CompletionHandoffContext, failure: { errorMessage: string; failureCategory: CompletionFailureCategory }, elapsed: number, ): CompletionHandoffResult { const context = snapshotCompletionHandoffContext(input); return Object.freeze({ name: context.name, task: context.task, summary: `Subagent error: ${failure.errorMessage}`, exitCode: 1, elapsed, error: failure.errorMessage, failureCategory: failure.failureCategory, ...(context.runtimePlan ? { runtimePlan: freezeRuntimePlan(context.runtimePlan) } : {}), }); } function resolveResultPresentation( result: Pick, name: string, ): string { if (result.errorMessage) { return ( `Sub-agent "${name}" failed after ${formatElapsed(result.elapsed)} ` + `(provider/agent error — auto-retry exhausted).\n\n` + `Error: ${result.errorMessage}\n\n` + `The subagent did not produce a result. If the work is still needed, start a new ` + `Fresh Subagent with a new task prompt.` ); } const freshAttemptGuidance = "\n\nIf the work is still needed, start a new Fresh Subagent with a new task prompt."; return result.exitCode !== 0 ? `Sub-agent "${name}" failed (exit code ${result.exitCode}).\n\n${result.summary}${freshAttemptGuidance}` : `Sub-agent "${name}" completed (${formatElapsed(result.elapsed)}).\n\n${result.summary}`; } export function buildForegroundSubagentResult( context: CompletionHandoffContext, result: CompletionHandoffResult, summary = result.summary, ): SubagentToolResult { const runtimePlan = freezeRuntimePlan(result.runtimePlan ?? context.runtimePlan); const presentation = result.error === "cancelled" ? `Sub-agent "${context.name}" cancelled after ${formatElapsed(result.elapsed)}.\n\n${summary}` : resolveResultPresentation({ ...result, summary }, context.name); const withRuntimeWarning = runtimePlan?.runtimeMismatch ? `${presentation}\n\nRuntime warning: ${runtimePlan.runtimeMismatch}` : presentation; return { content: [{ type: "text", text: withRuntimeWarning }], details: { id: context.operationId, name: context.name, task: context.task, ...(context.agent ? { agent: context.agent } : {}), summary, exitCode: result.exitCode, elapsed: result.elapsed, status: result.error === "cancelled" ? "cancelled" : result.exitCode === 0 ? "completed" : "failed", ...(result.errorMessage ? { errorMessage: result.errorMessage } : {}), ...(result.failureCategory ? { failureCategory: result.failureCategory } : {}), ...(result.error ? { error: result.error } : {}), ...(runtimePlan ? { runtimePlan } : {}), ...(result.runtime ? { runtime: result.runtime } : {}), operation: { operationId: context.operationId, }, }, }; } /** Turn a handoff into a parent-conversation steer; delivery stays with the watch arc. */ export function buildCompletionSteerMessage( context: CompletionHandoffContext, result: CompletionHandoffResult, ): CompletionSteerMessage { const handoff = buildForegroundSubagentResult(context, result); return { customType: "subagent_result", content: handoff.content[0]?.text ?? "", display: true, details: handoff.details, }; }