import { derived } from 'svelte/store' import { messages, type ToolCallResult } from './chat' import { planSteps, currentTask } from './workspace' export { type ToolCallResult } export interface ActivityEntry { id: string type: 'goal' | 'plan' | 'step_running' | 'step_done' | 'step_failed' | 'tool_running' | 'tool_done' | 'tool_error' | 'knowledge' | 'complete' | 'question' | 'error' description: string detail?: string timestamp: number toolName?: string status: 'running' | 'done' | 'failed' } export const activityLog = derived([messages, planSteps, currentTask], ([$msgs, $steps, $task]) => { const entries: ActivityEntry[] = [] const now = Date.now() // Goal if ($task?.goal) { entries.push({ id: 'goal', type: 'goal', description: $task.goal, timestamp: 0, status: 'done' }) } // Plan steps for (const s of $steps) { if (s.status === 'pending') continue const stepLabel = s.title || `Step ${s.seq}` entries.push({ id: s.id, type: s.status === 'running' ? 'step_running' : s.status === 'done' ? 'step_done' : 'step_failed', description: `Step ${s.seq}: ${stepLabel}`, detail: s.detail || undefined, timestamp: s.started_at ? new Date(s.started_at).getTime() : now, status: s.status === 'running' ? 'running' : s.status === 'done' ? 'done' : 'failed' }) } // Tool calls (from messages) let entryIdx = 0 for (let mi = 0; mi < $msgs.length; mi++) { for (const t of $msgs[mi].tools) { const label = toolActivityLabel(t) if (t.type === 'tool_use') { entries.push({ id: t.id ?? `tool_${mi}_${entryIdx++}`, type: 'tool_running', description: label, timestamp: now - ($msgs.length - mi) * 1000, toolName: t.name, status: 'running' }) } else if (t.type === 'tool_result') { // Find and update matching tool_use entry const running = entries.find((e) => e.type === 'tool_running' && e.id === t.id && e.status === 'running' ) if (running && t.error) { running.type = 'tool_error' running.status = 'failed' running.description = `${t.name}: ${t.error.slice(0, 80)}` } else if (running) { running.type = 'tool_done' running.status = 'done' running.detail = typeof t.result === 'string' ? t.result.slice(0, 200) : JSON.stringify(t.result ?? '').slice(0, 200) } else { entries.push({ id: t.id ?? `tool_${mi}_${entryIdx++}`, type: t.error ? 'tool_error' : 'tool_done', description: t.error ? `${t.name}: ${t.error.slice(0, 80)}` : t.name, detail: !t.error ? (typeof t.result === 'string' ? t.result.slice(0, 200) : '') : undefined, timestamp: now - ($msgs.length - mi) * 1000, toolName: t.name, status: t.error ? 'failed' : 'done' }) } } } } // Knowledge recorded — detect from upsert_knowledge tool results for (let mi = 0; mi < $msgs.length; mi++) { for (const t of $msgs[mi].tools) { if (t.type === 'tool_result' && t.name === 'upsert_knowledge' && !t.error) { const title = t.args?.title ?? '' entries.push({ id: `knowledge_${mi}`, type: 'knowledge', description: title ? `Recorded: ${title.slice(0, 60)}` : 'Recorded knowledge', timestamp: now - ($msgs.length - mi) * 1000, status: 'done' }) } } } // Task completion if ($task?.outcome) { entries.push({ id: 'complete', type: 'complete', description: $task.summary || `Task ${$task.outcome}`, timestamp: now, status: $task.outcome === 'failure' ? 'failed' : 'done' }) } // Sort newest first entries.sort((a, b) => b.timestamp - a.timestamp) return entries }) function toolActivityLabel(t: ToolCallResult): string { const args = t.args ?? {} switch (t.name) { case 'set_goal': return 'Set goal' case 'propose_plan': return 'Proposed plan' case 'search_knowledge': return `Research: ${args.query || ''}` case 'get_entity': return `Lookup: ${args.slug_or_id || ''}` case 'get_entity_knowledge': return 'Check prior knowledge' case 'get_relations': return 'Check relationships' case 'list_lxcs': return 'List containers' case 'list_entities': return 'List entities' case 'get_health_summary': return 'Fleet health' case 'get_state_snapshot': return 'State snapshot' case 'run': { const purpose = args.purpose as string || '' const target = (args.target as string) || '' if (purpose) return purpose if (target) return `Run on ${target}` return 'Run command' } case 'get_execution_status': return 'Check execution' case 'update_plan_step': return 'Update plan' case 'upsert_knowledge': return 'Record knowledge' case 'complete_task': return 'Complete task' case 'ping_service': return 'Check service' case 'ask_operator': return 'Ask operator' default: return t.name } }