import { SpanStatusCode, trace } from '@opentelemetry/api' import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' import { updateRunStatus } from '@/lib/copilot/async-runs/repository' import { MothershipStreamV1CompletionStatus, MothershipStreamV1EventType, } from '@/lib/copilot/generated/mothership-stream-v1' import { type RequestTraceV1Outcome, RequestTraceV1Outcome as RequestTraceV1OutcomeConst, } from '@/lib/copilot/generated/request-trace-v1' import { CopilotFinalizeOutcome } from '@/lib/copilot/generated/trace-attribute-values-v1' import { TraceAttr } from '@/lib/copilot/generated/trace-attributes-v1' import { TraceSpan } from '@/lib/copilot/generated/trace-spans-v1' import type { StreamWriter } from '@/lib/copilot/request/session' import type { OrchestratorResult } from '@/lib/copilot/request/types' const logger = createLogger('CopilotStreamFinalize') const getTracer = () => trace.getTracer('sim-copilot-finalize', '1.0.0') // Single finalization path. `outcome` is the caller's resolved verdict // so we don't have to re-derive cancel vs error from raw signals. export async function finalizeStream( result: OrchestratorResult, publisher: StreamWriter, runId: string, outcome: RequestTraceV1Outcome, requestId: string ): Promise { const spanOutcome = outcome === RequestTraceV1OutcomeConst.cancelled ? CopilotFinalizeOutcome.Aborted : outcome === RequestTraceV1OutcomeConst.success ? CopilotFinalizeOutcome.Success : CopilotFinalizeOutcome.Error const span = getTracer().startSpan(TraceSpan.CopilotFinalizeStream, { attributes: { [TraceAttr.CopilotFinalizeOutcome]: spanOutcome, [TraceAttr.RunId]: runId, [TraceAttr.RequestId]: requestId, [TraceAttr.CopilotResultToolCalls]: result.toolCalls?.length ?? 0, [TraceAttr.CopilotResultContentBlocks]: result.contentBlocks?.length ?? 0, [TraceAttr.CopilotResultContentLength]: result.content?.length ?? 0, [TraceAttr.CopilotPublisherSawComplete]: publisher.sawComplete, [TraceAttr.CopilotPublisherClientDisconnected]: publisher.clientDisconnected, }, }) try { if (outcome === RequestTraceV1OutcomeConst.cancelled) { await handleAborted(result, publisher, runId, requestId) } else if (outcome === RequestTraceV1OutcomeConst.error) { span.setStatus({ code: SpanStatusCode.ERROR, message: result.error || 'orchestration failed', }) await handleError(result, publisher, runId, requestId) } else { await handleSuccess(publisher, runId, requestId) } // Successful + cancelled paths fall through as status-unset → set // OK so dashboards don't show "incomplete" for normal terminals. if (outcome !== RequestTraceV1OutcomeConst.error) { span.setStatus({ code: SpanStatusCode.OK }) } } catch (error) { span.recordException(toError(error)) span.setStatus({ code: SpanStatusCode.ERROR, message: 'finalize threw' }) throw error } finally { span.end() } } async function handleAborted( result: OrchestratorResult, publisher: StreamWriter, runId: string, requestId: string ): Promise { const partialContentLen = result.content?.length ?? 0 const toolCallCount = result.toolCalls?.length ?? 0 const blockCount = result.contentBlocks?.length ?? 0 logger.info(`[${requestId}] Stream aborted by explicit stop`, { partialContentLen, toolCallCount, blockCount, }) if (!publisher.sawComplete) { const partialContent = result.content || undefined await publisher.publish({ type: MothershipStreamV1EventType.complete, payload: { status: MothershipStreamV1CompletionStatus.cancelled, ...(partialContent ? { partialContent } : {}), ...(partialContentLen ? { partialContentLen } : {}), ...(toolCallCount ? { toolCallCount } : {}), }, }) } await publisher.flush() await loggedRunStatusUpdate(runId, MothershipStreamV1CompletionStatus.cancelled, requestId, { completedAt: new Date(), }) } async function handleError( result: OrchestratorResult, publisher: StreamWriter, runId: string, requestId: string ): Promise { const errorMessage = result.error || result.errors?.[0] || 'An unexpected error occurred while processing the response.' // Persist whatever was generated before the failure, exactly like an abort — // a transient provider error (e.g. overloaded) shouldn't discard the partial // assistant output the user already saw streaming. const partialContent = result.content || undefined const partialContentLen = result.content?.length ?? 0 const toolCallCount = result.toolCalls?.length ?? 0 if (publisher.clientDisconnected) { logger.info(`[${requestId}] Stream failed after client disconnect`, { error: errorMessage }) } logger.error(`[${requestId}] Orchestration returned failure`, { error: errorMessage, partialContentLen, toolCallCount, }) // Surface the real error (Go already classifies provider errors like // "overloaded" into a friendly displayMessage). Don't clobber it with a // generic string. await publisher.publish({ type: MothershipStreamV1EventType.error, payload: { message: errorMessage, error: errorMessage, displayMessage: errorMessage, data: { displayMessage: errorMessage }, }, }) if (!publisher.sawComplete) { await publisher.publish({ type: MothershipStreamV1EventType.complete, payload: { status: MothershipStreamV1CompletionStatus.error, ...(partialContent ? { partialContent } : {}), ...(partialContentLen ? { partialContentLen } : {}), ...(toolCallCount ? { toolCallCount } : {}), }, }) } await publisher.flush() await loggedRunStatusUpdate(runId, MothershipStreamV1CompletionStatus.error, requestId, { completedAt: new Date(), error: errorMessage, }) } async function handleSuccess( publisher: StreamWriter, runId: string, requestId: string ): Promise { if (!publisher.sawComplete) { await publisher.publish({ type: MothershipStreamV1EventType.complete, payload: { status: MothershipStreamV1CompletionStatus.complete }, }) } await publisher.flush() await loggedRunStatusUpdate(runId, MothershipStreamV1CompletionStatus.complete, requestId, { completedAt: new Date(), }) } async function loggedRunStatusUpdate( runId: string, status: Parameters[1], requestId: string, updates: Parameters[2] = {} ): Promise { try { await updateRunStatus(runId, status, updates) } catch (error) { logger.warn(`[${requestId}] Failed to update run status to ${status}`, { runId, error: toError(error).message, }) } }