/** * CEO synthesis pass β€” runs after all specialists have finished. * * Given the per-agent outputs, this asks the CEO model to produce the final * markdown report (βœ… μ™„λ£Œ / πŸš€ λ‹€μŒ / πŸ’‘ μΈμ‚¬μ΄νŠΈ) that the user actually * reads. The function deliberately doesn't try to *parse* the response β€” * we trust the prompt to keep the structure and surface the text as-is. * * Failure mode: when the CEO call errors out we still return whatever raw * text we managed to collect (typically empty). The dispatcher then * concatenates the per-agent outputs into a fallback report so the user * never sees a blank screen. */ import { IAIService } from '../../core/services'; import { logError } from '../../utils'; import { getCompanyAgent } from './agents'; import { applyPromptVars, CEO_REPORT_PROMPT } from './promptAssets'; import { AgentTurnOutput, CompanyState, CompanyTaskPlan } from './types'; /** Max characters of per-agent output to feed back into the CEO synthesis. */ const PER_AGENT_REPORT_BUDGET = 2000; export interface ReportResult { /** Generated markdown. Empty string on hard failure. */ report: string; /** True when the LLM call succeeded with non-empty content. */ ok: boolean; } /** * Build the user-message payload the CEO sees: the brief, plus each agent's * task + output, lightly trimmed so the planner-model's context window * doesn't blow up on a verbose specialist. */ function _buildReportUserMessage( plan: CompanyTaskPlan, outputs: AgentTurnOutput[], ): string { const lines: string[] = []; if (plan.brief) { lines.push('## 이번 μž‘μ—… λΈŒλ¦¬ν”„'); lines.push(plan.brief); lines.push(''); } lines.push('## μ—μ΄μ „νŠΈλ³„ μ‚°μΆœλ¬Ό'); if (outputs.length === 0) { lines.push('_(no agent dispatched this turn β€” produce a brief acknowledgement instead)_'); } else { for (const out of outputs) { const def = getCompanyAgent(out.agentId); const head = def ? `### ${def.emoji} ${def.name}` : `### ${out.agentId}`; lines.push(''); lines.push(head); lines.push(`**Task:** ${out.task}`); if (out.error) { lines.push(`**Note:** dispatch failed β€” \`${out.error}\`. μ‚¬μš© κ°€λŠ₯ν•œ λΆ€λΆ„λ§Œ μΈμš©ν•΄μ„œ 보고.`); } lines.push(''); const body = out.response.length > PER_AGENT_REPORT_BUDGET ? out.response.slice(0, PER_AGENT_REPORT_BUDGET) + '\n…(truncated)' : out.response; lines.push(body); } } return lines.join('\n'); } /** Build a fallback report by concatenating agent outputs verbatim. Used when the LLM synthesis fails. */ export function buildFallbackReport( plan: CompanyTaskPlan, outputs: AgentTurnOutput[], ): string { const parts: string[] = ['## βœ… μ™„λ£Œλœ μž‘μ—…']; if (outputs.length === 0) { parts.push('- _(no agents ran this turn)_'); } else { for (const out of outputs) { const def = getCompanyAgent(out.agentId); const head = def ? `**${def.emoji} ${def.name}**` : `**${out.agentId}**`; const firstLine = (out.response.split(/\n/).find((l) => l.trim()) || out.task).trim(); parts.push(`- ${head} β€” ${firstLine.slice(0, 120)}`); } } parts.push(''); parts.push('## πŸš€ λ‹€μŒ μ•‘μ…˜'); parts.push('_(CEO ν•©μ„± μ‹€νŒ¨ β€” μœ„ μ‚°μΆœλ¬Όμ„ 직접 ν™•μΈν•˜μ„Έμš”)_'); parts.push(''); parts.push('## πŸ’‘ μΈμ‚¬μ΄νŠΈ'); parts.push(`- 이번 턴은 ${outputs.length}λͺ…μ˜ μ—μ΄μ „νŠΈκ°€ μž‘μ—…ν–ˆμŠ΅λ‹ˆλ‹€.`); if (plan.brief) parts.push(`- λΈŒλ¦¬ν”„: ${plan.brief}`); return parts.join('\n'); } /** End-to-end synthesis call. Never throws β€” returns `{ ok: false, … }` on error. */ export async function runCeoReporter( ai: IAIService, plan: CompanyTaskPlan, outputs: AgentTurnOutput[], state: CompanyState, options: { model?: string; timeoutMs?: number } = {}, ): Promise { const system = applyPromptVars(CEO_REPORT_PROMPT, { company: state.companyName }); const user = _buildReportUserMessage(plan, outputs); try { const result = await ai.chat({ system, user, model: options.model, timeoutMs: options.timeoutMs, }); const text = (result.content || '').trim(); if (!text) { return { report: buildFallbackReport(plan, outputs), ok: false }; } return { report: text, ok: true }; } catch (e: any) { logError('ceoReporter: AI call failed.', { error: e?.message ?? String(e) }); return { report: buildFallbackReport(plan, outputs), ok: false }; } }