import type { AgentActivityTraceItem } from "./agent-activity-trace.ts"; import type { AgentActivityView } from "./chat-message-view.ts"; import type { ConsultationTimelineRow } from "./consultation-run-timeline.ts"; import type { CompletedActivityReceiptView } from "./rectification-activity-receipt.ts"; import { PUBLIC_RECTIFICATION_METHOD_LABELS } from "./rectification-varga-sentence.ts"; export const RECTIFICATION_TIMELINE_SOURCE_LIMIT = 8; export const RECTIFICATION_TIMELINE_LIVE_ID = "activity-live"; const FAILED_SUFFIX = "未完成"; export type RectificationTimelineInput = Readonly<{ trace: readonly AgentActivityTraceItem[] | undefined; receipt: CompletedActivityReceiptView | undefined; activity: AgentActivityView | undefined; settled: boolean; }>; function methodChips(receipt: CompletedActivityReceiptView | undefined): string[] { if (!receipt) return []; const seen = new Set(); const chips: string[] = []; for (const method of receipt.methods) { const label = PUBLIC_RECTIFICATION_METHOD_LABELS[method]; if (!label || seen.has(label)) continue; seen.add(label); chips.push(label); if (chips.length >= RECTIFICATION_TIMELINE_SOURCE_LIMIT) break; } return chips; } /** Progress labels are the only ones that name an action still under way. */ function isProgressLabel(label: string): boolean { return /^正在/.test(label.trim()); } /** * Project the rectification agent's tool trace and receipt onto the same * timeline rows the consultation surface renders, so both sessions share one * step list, one live marker and one settled summary. Thinking text never * reaches this surface: the public stream drops it on the server. */ export function rectificationTimelineRows(input: RectificationTimelineInput): ConsultationTimelineRow[] { const failedTool = input.receipt?.failedTool; const rows: ConsultationTimelineRow[] = (input.trace ?? []).map((item) => { if (item.kind === "think") { return { id: item.id, kind: "think", status: item.status, label: item.label, ...(item.text?.trim() ? { thinkingText: item.text } : {}), }; } const failed = item.status === "done" && failedTool !== undefined && item.tool === failedTool; return { id: item.id, kind: "calculate", status: item.status, label: failed ? `${item.label}${FAILED_SUFFIX}` : item.label, }; }); const chips = methodChips(input.receipt); if (chips.length > 0) { for (let index = rows.length - 1; index >= 0; index -= 1) { const row = rows[index]; if (!row || row.kind !== "calculate" || row.status !== "done" || row.label.endsWith(FAILED_SUFFIX)) continue; rows[index] = { ...row, sources: chips }; break; } } const hasLiveRow = rows.some((row) => row.status === "live"); const label = input.activity?.label ?? ""; if (!input.settled && !hasLiveRow && isProgressLabel(label)) { rows.push({ id: RECTIFICATION_TIMELINE_LIVE_ID, kind: input.activity?.phase === "answer-composition" ? "write" : "calculate", status: "live", label, }); } return rows; }