121 lines
4.7 KiB
TypeScript
121 lines
4.7 KiB
TypeScript
/**
|
|
* 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<ReportResult> {
|
|
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 };
|
|
}
|
|
}
|