import type { AgentExecutionReceipt, ConsultationAgentPublicEvent } from "./consultation-agent-events.ts"; import { CONSULTATION_CHART_CALCULATION_LABEL, CONSULTATION_COMPOSING_LABEL, CONSULTATION_DONE_CHART_LABEL, CONSULTATION_DONE_SKILL_LABEL, CONSULTATION_LOADING_METHOD_LABEL, consultationWriteLabel, } from "./consultation-activity-labels.ts"; import { consultationDomainDefinition, normalizeConsultationDomain, } from "./consultation-domain-registry.ts"; import type { PublicThinkingSection } from "./consultation-thinking-plan.ts"; export const CONSULTATION_TIMELINE_KINDS = ["method", "calculate", "think", "write"] as const; export type ConsultationTimelineKind = (typeof CONSULTATION_TIMELINE_KINDS)[number]; export type ConsultationTimelineStatus = "live" | "done"; export type ConsultationTimelineRow = Readonly<{ id: string; kind: ConsultationTimelineKind; status: ConsultationTimelineStatus; label: string; queries?: readonly string[]; sources?: readonly string[]; thinkingText?: string; }>; export type ConsultationTimelineState = Readonly<{ rows: readonly ConsultationTimelineRow[]; answer: string; }>; const METHOD_ID = "method"; const CALCULATE_ID = "calculate"; export function emptyConsultationTimeline(): ConsultationTimelineState { return { rows: [], answer: "" }; } export function reduceConsultationTimeline( state: ConsultationTimelineState, event: ConsultationAgentPublicEvent, ): ConsultationTimelineState { if (event.type === "skill.started") { return upsertRow(state, { id: METHOD_ID, kind: "method", status: "live", label: CONSULTATION_LOADING_METHOD_LABEL, }); } if (event.type === "skill.completed") { return completeRow(state, METHOD_ID, CONSULTATION_DONE_SKILL_LABEL); } if (event.type === "tool.started") { return upsertRow(completeLiveThink(state), { id: CALCULATE_ID, kind: "calculate", status: "live", label: event.label || CONSULTATION_CHART_CALCULATION_LABEL, }); } if (event.type === "activity") { if (event.phase === "chart-calculation") { return patchRow(state, CALCULATE_ID, { status: "live", label: event.label, }, { id: CALCULATE_ID, kind: "calculate", status: "live", label: event.label, }); } if (event.phase === "evidence-validation") { return completeRow(state, CALCULATE_ID, CONSULTATION_DONE_CHART_LABEL); } if (event.phase === "answer-composition") { return upsertWrite(completeLiveThink(state), event.label); } return state; } if (event.type === "tool.completed") { return completeRow(state, CALCULATE_ID, CONSULTATION_DONE_CHART_LABEL); } if (event.type === "tool.failed") { return completeRow(state, CALCULATE_ID, CONSULTATION_DONE_CHART_LABEL); } if (event.type === "thinking.section") { const section: PublicThinkingSection = { id: event.id, title: event.title, heading: event.heading, steps: event.steps, }; const withCalc = enrichCalculate(completeRow(state, CALCULATE_ID, CONSULTATION_DONE_CHART_LABEL), section); const withoutOpen = completeLiveThink(withCalc); return upsertRow(withoutOpen, { id: `think-${section.id}`, kind: "think", status: "live", label: consultationThinkTitle(section.title), }); } if (event.type === "thinking.delta") { const think = lastRow(state.rows, (row) => row.kind === "think" && row.status === "live"); if (think) { const thinkingText = `${think.thinkingText ?? ""}${event.text}`.slice(0, 4_000); return upsertRow(state, { ...think, status: "live", thinkingText }); } const thinkCount = state.rows.filter((row) => row.kind === "think").length; return upsertRow(state, { id: `think-open-${thinkCount + 1}`, kind: "think", status: "live", label: "正在分析…", thinkingText: event.text.slice(0, 4_000), }); } if (event.type === "answer.delta") { const next = { ...completeLiveThink(state), answer: `${state.answer}${event.text}` }; return syncWriteRows(next, false); } if (event.type === "run.completed" || event.type === "run.failed") { const next = syncWriteRows(completeLiveThink(state), true); return { ...next, rows: next.rows.map((row) => ( row.status === "done" ? row : { ...row, status: "done" as const, label: doneLabel(row) } )), }; } return state; } export function reduceConsultationTimelineEvents( events: readonly ConsultationAgentPublicEvent[], state = emptyConsultationTimeline(), ): ConsultationTimelineState { return events.reduce(reduceConsultationTimeline, state); } export function consultationTimelineFromSettled(input: { text?: string; thinkingText?: string; thinkingSections?: readonly PublicThinkingSection[]; agentExecutionReceipt?: AgentExecutionReceipt; }): readonly ConsultationTimelineRow[] { const text = input.text?.trim() ?? ""; const sections = input.thinkingSections ?? []; const receipt = input.agentExecutionReceipt; if (!text && !sections.length && !input.thinkingText?.trim() && !receipt) return []; const events: ConsultationAgentPublicEvent[] = [ { type: "skill.started", name: "jyotish-vedic-astrology" }, { type: "skill.completed", name: "jyotish-vedic-astrology" }, ]; const calculated = receipt?.steps.some((step) => step.kind === "tool") || sections.some((section) => section.id.startsWith("domain-")); if (calculated) { events.push({ type: "tool.started", callId: "settled", tool: "run-jyotish-consultation", label: CONSULTATION_CHART_CALCULATION_LABEL, }); events.push({ type: "tool.completed", callId: "settled", tool: "run-jyotish-consultation", status: "ready", durationMs: 0, }); } for (const section of sections) { events.push({ type: "thinking.section", ...section }); } if (input.thinkingText?.trim() && sections.length === 0) { events.push({ type: "thinking.delta", text: input.thinkingText }); } if (text) events.push({ type: "answer.delta", text }); events.push({ type: "run.completed", receipt: receipt ?? { runId: "settled", runtime: "mastra-agentic", skill: { name: "jyotish-vedic-astrology", loaded: true, referenceReads: 0, methodologySections: 0, }, steps: [], workflow: { route: "settled", status: "ready", preciseTiming: "blocked", missingLayers: [] }, }, }); let state = reduceConsultationTimelineEvents(events); if (input.thinkingText?.trim() && sections.length > 0) { const lastThink = [...state.rows].reverse().find((row) => row.kind === "think"); if (lastThink) { state = upsertRow(state, { ...lastThink, thinkingText: input.thinkingText.slice(0, 4_000) }); } } return state.rows; } export function consultationThinkTitle(title: string): string { const cleaned = title.replace(/\s+/g, " ").trim(); if (cleaned.length <= 20) return cleaned || "正在分析"; return cleaned.slice(0, 20); } function answerHeadings(text: string): string[] { return [...text.matchAll(/^##\s+(.+?)\s*$/gm)].map((match) => match[1]?.trim() ?? "").filter(Boolean); } function queriesFromSection(section: PublicThinkingSection): string[] { const domain = section.id.startsWith("domain-") ? normalizeConsultationDomain(section.id.slice("domain-".length)) : null; if (!domain) return []; const definition = consultationDomainDefinition(domain); return uniqueLabels([definition.label, ...definition.evidencePreview]).slice(0, 8); } function sourcesFromSection(section: PublicThinkingSection): string[] { return uniqueLabels(section.steps .map((step) => step.label.replace(/^对照\s+/, "").trim()) .filter((label) => label.length > 0 && label.length <= 24)).slice(0, 8); } function uniqueLabels(values: readonly string[]): string[] { const seen = new Set(); const kept: string[] = []; for (const value of values) { const label = value.replace(/\s+/g, " ").trim(); if (!label || seen.has(label)) continue; seen.add(label); kept.push(label); } return kept; } function lastRow( rows: readonly ConsultationTimelineRow[], match: (row: ConsultationTimelineRow) => boolean, ): ConsultationTimelineRow | undefined { for (let index = rows.length - 1; index >= 0; index -= 1) { const row = rows[index]; if (row && match(row)) return row; } return undefined; } function upsertRow(state: ConsultationTimelineState, row: ConsultationTimelineRow): ConsultationTimelineState { const index = state.rows.findIndex((item) => item.id === row.id); if (index < 0) return { ...state, rows: [...state.rows, row] }; return { ...state, rows: state.rows.map((item, current) => (current === index ? { ...item, ...row } : item)), }; } function patchRow( state: ConsultationTimelineState, id: string, patch: Partial, create?: ConsultationTimelineRow, ): ConsultationTimelineState { const existing = state.rows.find((row) => row.id === id); if (!existing) return create ? upsertRow(state, create) : state; return upsertRow(state, { ...existing, ...patch, id, kind: existing.kind }); } function completeRow( state: ConsultationTimelineState, id: string, label?: string, ): ConsultationTimelineState { const existing = state.rows.find((row) => row.id === id); if (!existing) return state; return upsertRow(state, { ...existing, status: "done", label: label ?? doneLabel(existing), }); } function completeLiveThink(state: ConsultationTimelineState): ConsultationTimelineState { return { ...state, rows: state.rows.map((row) => ( row.kind === "think" && row.status === "live" ? { ...row, status: "done" as const, label: doneLabel(row) } : row )), }; } function doneLabel(row: ConsultationTimelineRow): string { if (row.kind === "method") return CONSULTATION_DONE_SKILL_LABEL; if (row.kind === "calculate") return CONSULTATION_DONE_CHART_LABEL; if (row.kind === "write") { const heading = row.id.startsWith("write-") ? row.id.slice("write-".length) : ""; return heading ? consultationWriteLabel(heading, false) : row.label.replace(/…$/, "").replace(/^正在/, ""); } return row.label.replace(/…$/, "").replace(/^正在/, "") || row.label; } function upsertWrite(state: ConsultationTimelineState, liveLabel: string): ConsultationTimelineState { const headings = answerHeadings(state.answer); const heading = headings.at(-1); if (!heading) { return upsertRow(state, { id: "write-open", kind: "write", status: "live", label: liveLabel || CONSULTATION_COMPOSING_LABEL, }); } return syncWriteRows(state, false); } function syncWriteRows(state: ConsultationTimelineState, settled: boolean): ConsultationTimelineState { const headings = answerHeadings(state.answer); if (headings.length === 0) { if (!state.answer.trim() && !settled) return state; if (!state.answer.trim()) return state; return upsertRow(completeRow(state, "write-open"), { id: "write-open", kind: "write", status: settled ? "done" : "live", label: settled ? "组织回答" : CONSULTATION_COMPOSING_LABEL, }); } let next = completeRow(state, "write-open"); next = { ...next, rows: next.rows.filter((row) => row.id !== "write-open"), }; headings.forEach((heading, index) => { const last = index === headings.length - 1; next = upsertRow(next, { id: `write-${heading}`, kind: "write", status: settled || !last ? "done" : "live", label: consultationWriteLabel(heading, !settled && last), }); }); return next; } function enrichCalculate( state: ConsultationTimelineState, section: PublicThinkingSection, ): ConsultationTimelineState { const calculate = state.rows.find((row) => row.id === CALCULATE_ID); if (!calculate) return state; const queries = uniqueLabels([...(calculate.queries ?? []), ...queriesFromSection(section)]).slice(0, 8); const sources = uniqueLabels([...(calculate.sources ?? []), ...sourcesFromSection(section)]).slice(0, 8); return upsertRow(state, { ...calculate, queries, sources }); }