mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-06 22:09:44 +02:00
Threads the existing task_message.created_at column through the full stack (Go protocol -> REST/WS handlers -> TS types -> transcript dialog) so agent run transcripts show per-entry timestamps, helping users spot stalled runs. Additive, no migration.
68 lines
2.1 KiB
TypeScript
68 lines
2.1 KiB
TypeScript
import type { TaskMessagePayload } from "@multica/core/types/events";
|
|
import { redactSecrets } from "./redact";
|
|
|
|
/** A unified timeline entry: tool calls, thinking, text, and errors in chronological order. */
|
|
export interface TimelineItem {
|
|
seq: number;
|
|
type: "tool_use" | "tool_result" | "thinking" | "text" | "error";
|
|
tool?: string;
|
|
content?: string;
|
|
input?: Record<string, unknown>;
|
|
output?: string;
|
|
created_at?: string;
|
|
}
|
|
|
|
function canMergeStreamingText(prev: TimelineItem, next: TimelineItem): boolean {
|
|
return (prev.type === "thinking" || prev.type === "text") && prev.type === next.type;
|
|
}
|
|
|
|
/** Merge adjacent text/thinking fragments that were split only by daemon flush timing. */
|
|
export function coalesceTimelineItems(items: TimelineItem[]): TimelineItem[] {
|
|
const sorted = [...items].sort((a, b) => a.seq - b.seq);
|
|
const out: TimelineItem[] = [];
|
|
|
|
for (const item of sorted) {
|
|
const prev = out[out.length - 1];
|
|
if (prev && canMergeStreamingText(prev, item)) {
|
|
out[out.length - 1] = {
|
|
...prev,
|
|
content: `${prev.content ?? ""}${item.content ?? ""}`,
|
|
created_at: item.created_at ?? prev.created_at,
|
|
};
|
|
continue;
|
|
}
|
|
out.push(item);
|
|
}
|
|
|
|
return out;
|
|
}
|
|
|
|
export function appendTimelineItem(items: TimelineItem[], item: TimelineItem): TimelineItem[] {
|
|
return coalesceTimelineItems([...items, item]);
|
|
}
|
|
|
|
function redactTimelineItems(items: TimelineItem[]): TimelineItem[] {
|
|
return items.map((item) => ({
|
|
...item,
|
|
content: item.content ? redactSecrets(item.content) : item.content,
|
|
output: item.output ? redactSecrets(item.output) : item.output,
|
|
}));
|
|
}
|
|
|
|
/** Build a chronologically ordered timeline from raw task messages. */
|
|
export function buildTimeline(msgs: TaskMessagePayload[]): TimelineItem[] {
|
|
const items: TimelineItem[] = [];
|
|
for (const msg of msgs) {
|
|
items.push({
|
|
seq: msg.seq,
|
|
type: msg.type,
|
|
tool: msg.tool,
|
|
content: msg.content,
|
|
input: msg.input,
|
|
output: msg.output,
|
|
created_at: msg.created_at,
|
|
});
|
|
}
|
|
return redactTimelineItems(coalesceTimelineItems(items));
|
|
}
|