import type { BackendTask, ProcessArtifact, ProcessEvent, ProcessRun, TaskTimelineCard, TaskTimelineCardType, } from '@/types'; export type BuildTaskTimelineCardsInput = { task: BackendTask; processRuns?: ProcessRun[]; processEvents?: ProcessEvent[]; processArtifacts?: ProcessArtifact[]; }; const TIMELINE_CARD_TYPES = new Set([ 'task_created', 'plan', 'skill', 'tool_call', 'tool_result', 'next_step', 'agent_team', 'agent_progress', 'agent_handoff', 'artifact', 'error', 'result', 'acceptance', ]); const RESULT_STATUSES = new Set(['awaiting_acceptance', 'closed', 'abandoned', 'cancelled', 'error']); function isTimelineCardType(value: unknown): value is TaskTimelineCardType { return typeof value === 'string' && TIMELINE_CARD_TYPES.has(value as TaskTimelineCardType); } function toTime(value: string): number { const parsed = new Date(value).getTime(); return Number.isFinite(parsed) ? parsed : 0; } function firstString(...values: unknown[]): string | undefined { for (const value of values) { if (typeof value !== 'string') continue; const trimmed = value.trim(); if (trimmed) return trimmed; } return undefined; } function stringList(value: unknown): string[] { if (Array.isArray(value)) { return value.filter((item): item is string => typeof item === 'string' && item.trim().length > 0); } if (typeof value === 'string' && value.trim()) { return [value.trim()]; } return []; } function normalizeSkillNames(metadata: Record | undefined): string[] | undefined { if (!metadata || (!('skill_names' in metadata) && !('selected_skill_names' in metadata))) { return undefined; } const names = [ ...stringList(metadata.skill_names), ...stringList(metadata.selected_skill_names), ]; return Array.from(new Set(names)); } function cardTypeForEvent(event: ProcessEvent): TaskTimelineCardType | null { if (isTimelineCardType(event.metadata?.timeline_type)) { return event.metadata.timeline_type; } if (event.status === 'error') { return 'error'; } switch (String(event.kind)) { case 'task_planned': case 'run_started': return 'plan'; case 'skill_selected': return 'skill'; case 'tool_call_started': return 'tool_call'; case 'tool_call_finished': return 'tool_result'; case 'agent_team_created': return 'agent_team'; case 'agent_handoff': return 'agent_handoff'; case 'run_progress': case 'run_finished': return 'agent_progress'; case 'task_result_ready': return 'result'; case 'task_acceptance_recorded': return 'acceptance'; case 'task_error': return 'error'; default: return null; } } function titleForCard(type: TaskTimelineCardType, actorName?: string): string { switch (type) { case 'task_created': return '任务已创建'; case 'plan': return '执行计划'; case 'skill': return '选择 Skill'; case 'tool_call': return actorName ? `调用工具:${actorName}` : '调用工具'; case 'tool_result': return actorName ? `工具结果:${actorName}` : '工具结果'; case 'next_step': return '下一步'; case 'agent_team': return '启动 Agent Team'; case 'agent_progress': return actorName || 'Agent 进展'; case 'agent_handoff': return 'Agent 交接'; case 'artifact': return '生成产物'; case 'error': return '执行遇到问题'; case 'result': return '本轮结果'; case 'acceptance': return '任务验收'; } } function summaryForEvent(event: ProcessEvent): string | undefined { return firstString( event.metadata?.result_summary, event.metadata?.reason, event.metadata?.action_summary, event.text, ); } function detailsForEvent(event: ProcessEvent): Record | undefined { const skillNames = normalizeSkillNames(event.metadata); if (!event.metadata && !skillNames) { return undefined; } return { ...(event.metadata ?? {}), ...(skillNames ? { skill_names: skillNames } : {}), }; } function feedbackCreatedAt(feedback: Record, task: BackendTask): string { return firstString(feedback.created_at, task.updated_at, task.created_at) ?? task.created_at; } function feedbackSummary(feedback: Record): string | undefined { return firstString(feedback.comment, feedback.summary, feedback.acceptance_type); } function resultSummary(task: BackendTask): string | undefined { return firstString( task.metadata?.result_summary, task.metadata?.summary, task.close_reason, task.validation_result?.summary, ); } function buildRunMap(processRuns: ProcessRun[]): Map { const map = new Map(); for (const run of processRuns) { map.set(run.run_id, run); } return map; } export function buildTaskTimelineCards(input: BuildTaskTimelineCardsInput): TaskTimelineCard[] { const { task } = input; const processRuns = input.processRuns ?? task.process_runs ?? []; const processEvents = input.processEvents ?? task.process_events ?? []; const processArtifacts = input.processArtifacts ?? task.process_artifacts ?? []; const runsById = buildRunMap(processRuns); const cards: TaskTimelineCard[] = [ { id: `${task.task_id}:created`, taskId: task.task_id, type: 'task_created', title: titleForCard('task_created'), summary: firstString(task.short_title, task.description, task.goal), actorName: task.creator, status: task.status, createdAt: task.created_at, details: task.metadata, }, ]; for (const event of processEvents) { const type = cardTypeForEvent(event); if (!type) continue; cards.push({ id: event.event_id, taskId: task.task_id, runId: event.run_id, parentRunId: event.parent_run_id, type, title: titleForCard(type, event.actor_name), summary: summaryForEvent(event), actorName: event.actor_name, status: event.status, createdAt: event.created_at, details: detailsForEvent(event), }); } for (const run of processRuns) { if (!run.parent_run_id) continue; cards.push({ id: `${run.run_id}:fallback-progress`, taskId: task.task_id, runId: run.run_id, parentRunId: run.parent_run_id, type: 'agent_progress', title: titleForCard('agent_progress', run.actor_name), summary: firstString(run.summary, run.title), actorName: run.actor_name, status: run.status, createdAt: run.started_at, details: run.metadata, }); } for (const artifact of processArtifacts) { const run = runsById.get(artifact.run_id); cards.push({ id: artifact.artifact_id, taskId: task.task_id, runId: artifact.run_id, parentRunId: run?.parent_run_id, type: 'artifact', title: titleForCard('artifact'), summary: firstString(artifact.title), actorName: artifact.actor_name, createdAt: artifact.created_at, relatedArtifactIds: [artifact.artifact_id], details: { ...(artifact.metadata ?? {}), artifact_type: artifact.artifact_type, title: artifact.title, }, }); } if (RESULT_STATUSES.has(task.status)) { cards.push({ id: `${task.task_id}:result`, taskId: task.task_id, runId: task.run_ids.at(-1) ?? null, type: 'result', title: titleForCard('result'), summary: resultSummary(task), status: task.status, createdAt: task.closed_at ?? task.updated_at ?? task.created_at, details: task.validation_result ?? undefined, }); } for (const [index, feedback] of task.feedback.entries()) { cards.push({ id: `${task.task_id}:acceptance:${index}`, taskId: task.task_id, runId: firstString(feedback.run_id) ?? null, type: 'acceptance', title: titleForCard('acceptance'), summary: feedbackSummary(feedback), status: firstString(feedback.acceptance_type), createdAt: feedbackCreatedAt(feedback, task), details: feedback, }); } return cards .map((card, index) => ({ card, index })) .sort((a, b) => toTime(a.card.createdAt) - toTime(b.card.createdAt) || a.index - b.index) .map(({ card }) => card); }