A bottom panel in the UI that shows a flat, chronological timeline of every event in a workflow run — node starts/completions, full LLM request/response content, tool calls, errors — with timestamps and durations. In-memory with JSON export.
LLMCallUpdateAdd to packages/protocol/src/messages.ts:
export interface LLMCallUpdate {
type: "llm_call";
node_id: string;
node_name?: string;
provider: string;
model: string;
messages: Array<{ role: string; content: unknown }>; // full prompt
response: unknown; // full completion
tool_calls?: Array<{ id: string; name: string; args: unknown }>;
tokens_input?: number;
tokens_output?: number;
cost?: number;
duration_ms: number;
error?: string | null;
timestamp: string; // ISO 8601
}
Add LLMCallUpdate to the ProcessingMessage union type.
In packages/runtime/src/providers/base-provider.ts:
generateMessageTraced() — after the LLM call completes (success or error), emit an LLMCallUpdate via ProcessingContext. The context is not currently available in BaseProvider, so:
emitMessage callback to BaseProvider (set by the ProcessingContext when the provider is created/used).generateMessageTraced(), after the span completes, call this.emitMessage({ type: "llm_call", ... }) with the full request messages, response content, token counts, cost, and duration.generateMessagesTraced() (streaming), accumulate chunks into the full response, then emit the LLMCallUpdate after the stream completes.The emitMessage callback is wired up in ProcessingContext.getProvider() — when it returns a provider instance, it sets provider.emitMessage = (msg) => this.postMessage(msg).
New file: web/src/stores/TraceStore.ts
interface TraceEvent {
id: string; // unique ID
timestamp: string; // ISO 8601
relativeMs: number; // ms since run started
type: "node_start" | "node_complete" | "node_error" | "llm_call" | "tool_call" | "tool_result" | "edge_active" | "output";
nodeId?: string;
nodeName?: string;
nodeType?: string;
summary: string; // one-line summary for the list
detail: unknown; // full payload for expansion
}
interface TraceStore {
events: TraceEvent[];
runStartTime: string | null;
isRecording: boolean;
startRun(timestamp: string): void;
append(event: TraceEvent): void;
clear(): void;
exportJSON(): string;
}
Max 10,000 events in memory. Oldest events dropped if exceeded.
In web/src/stores/workflowUpdates.ts, in the handleUpdate function, add a branch that converts each ProcessingMessage into a TraceEvent and appends it to TraceStore:
job_update with status “running” → TraceStore.startRun(timestamp)node_update with status “running” → TraceEvent type “node_start”node_update with status “completed” → TraceEvent type “node_complete”node_update with status “error” → TraceEvent type “node_error”llm_call → TraceEvent type “llm_call”, summary = "${provider}/${model} → ${tokens_output} tokens, ${duration_ms}ms, $${cost}"tool_call → TraceEvent type “tool_call”tool_result → TraceEvent type “tool_result”edge_update with status “active” → TraceEvent type “edge_active”output_update → TraceEvent type “output”job_update with terminal status → stop recordingOther message types (log, notification, progress, chunk) are ignored — they’re noise for the trace view.
New file: web/src/components/panels/TracePanel.tsx
Layout: Bottom panel tab alongside LogPanel. Same panel framework/chrome.
Content: Virtualized flat list of TraceEvents sorted by timestamp.
Each row shows:
detail payload as formatted JSON (or structured content for LLM calls: messages, response, tool_calls)LLM call expanded view:
Toolbar:
trace-{workflowId}-{timestamp}.json)In packages/runtime/src/context.ts, when getProvider() returns a provider, set a message callback:
async getProvider(providerId: string): Promise<BaseProvider> {
const provider = await this._providerResolver(providerId);
provider.setMessageEmitter((msg) => this.postMessage(msg));
return provider;
}
In packages/runtime/src/providers/base-provider.ts, add:
private _emitMessage: ((msg: unknown) => void) | null = null;
setMessageEmitter(fn: (msg: unknown) => void): void {
this._emitMessage = fn;
}
protected emitMessage(msg: unknown): void {
if (this._emitMessage) this._emitMessage(msg);
}
| File | Change |
|---|---|
packages/protocol/src/messages.ts |
Add LLMCallUpdate interface + add to union |
packages/runtime/src/providers/base-provider.ts |
Add setMessageEmitter/emitMessage, emit LLMCallUpdate in traced methods |
packages/runtime/src/context.ts |
Wire setMessageEmitter in getProvider() |
web/src/stores/TraceStore.ts |
New store |
web/src/stores/workflowUpdates.ts |
Feed TraceStore from incoming messages |
web/src/components/panels/TracePanel.tsx |
New panel component |
web/src/components/panels/BottomPanel.tsx (or equivalent) |
Add TracePanel tab |