// ─── Agent Activity Tree (Mission Control) ────────────────────────── // Full-depth parallel agent activity tracking for the 3-Tier system. // Provides: initActivityTree, addNode, addToolCall, updateToolCall, // finalizeNode, getStats, createMockActivityTree, formatElapsed // Agent metadata — maps agent IDs to display info const AGENT_META: Record = { rose: { emoji: '🌹', name: 'Rose', tier: 1 }, lotus: { emoji: '🪷', name: 'Lotus', tier: 2 }, 'forget-me-not':{ emoji: '🌼', name: 'Forget-me-not', tier: 2 }, sunflower: { emoji: '🌻', name: 'Sunflower', tier: 2 }, iris: { emoji: '⚜️', name: 'Iris', tier: 2 }, ivy: { emoji: '🌿', name: 'Ivy', tier: 2 }, dandelion: { emoji: '🛡', name: 'Dandelion', tier: 2 }, root: { emoji: '🌳', name: 'Root', tier: 2 }, }; // Tier-3 sub-agent detection — tool names that indicate sub-agent work const TIER3_TOOL_NAMES = new Set([ 'search_files', 'read_file', 'write_file', 'terminal', 'browser_navigate', 'browser_snapshot', 'browser_click', 'browser_type', 'browser_press', 'web_search', 'delegate_task', 'execute_code', 'patch', ]); let _nodeCounter = 0; function _nextNodeId(agentId: string): string { return `${agentId}-${++_nodeCounter}`; } // ─── Core Functions ──────────────────────────────────────────────── function initActivityTree(): ActivityTree { const tree: ActivityTree = { version: 1, rootId: 'rose', nodes: { rose: { id: 'rose', parentId: null, agentId: 'rose', agentEmoji: '🌹', agentName: 'Rose', tier: 1, status: 'running', task: 'Orchestrating', toolCalls: [], startedAt: Date.now(), endedAt: null, duration: null, children: [], collapsed: false, metadata: {}, } }, stats: _emptyStats(), }; S.activityTree = tree; S.mcFilter = {}; S.mcSort = 'runtime'; return tree; } function _emptyStats(): MCStats { return { totalAgents: 0, runningAgents: 0, pendingAgents: 0, doneAgents: 0, errorAgents: 0, totalTools: 0, doneTools: 0, runningTools: 0, avgResponseTime: 0, totalElapsed: 0, }; } // ─── Node Operations ─────────────────────────────────────────────── function atAddNode(opts: { parentId?: string; agentId: string; task: string; status?: ActivityNode['status']; tier?: 1|2|3; }): ActivityNode | null { const tree = S.activityTree; if (!tree) return null; const parentId = opts.parentId || tree.rootId; const parent = tree.nodes[parentId]; if (!parent) return null; const meta = AGENT_META[opts.agentId]; const tier = opts.tier || (meta ? meta.tier : 3); const id = _nextNodeId(opts.agentId); const node: ActivityNode = { id, parentId, agentId: opts.agentId, agentEmoji: meta?.emoji || '⚙️', agentName: meta?.name || opts.agentId, tier, status: opts.status || 'pending', task: opts.task, toolCalls: [], startedAt: opts.status === 'running' ? Date.now() : null, endedAt: null, duration: null, children: [], collapsed: false, metadata: {}, }; tree.nodes[id] = node; parent.children.push(id); _recalcStats(tree); return node; } function atUpdateNode(nodeId: string, updates: Partial): void { const tree = S.activityTree; if (!tree) return; const node = tree.nodes[nodeId]; if (!node) return; Object.assign(node, updates); _recalcStats(tree); } function atGetNode(nodeId: string): ActivityNode | null { return S.activityTree?.nodes[nodeId] || null; } function atGetChildren(nodeId: string): ActivityNode[] { const tree = S.activityTree; if (!tree) return []; const node = tree.nodes[nodeId]; if (!node) return []; return node.children.map(id => tree.nodes[id]).filter(Boolean); } function atGetRunningNodes(): ActivityNode[] { const tree = S.activityTree; if (!tree) return []; return Object.values(tree.nodes).filter( n => n.status === 'running' || n.status === 'thinking' ); } // ─── Tool Call Operations ────────────────────────────────────────── function atAddToolCall(nodeId: string, tc: { name: string; args?: Record; status?: ActivityToolCall['status']; }): ActivityToolCall | null { const tree = S.activityTree; if (!tree) return null; const node = tree.nodes[nodeId]; if (!node) return null; // If node was pending, it's now running if (node.status === 'pending') { node.status = 'running'; node.startedAt = node.startedAt || Date.now(); } const toolCall: ActivityToolCall = { id: `tc-${++_nodeCounter}`, name: tc.name, status: tc.status || 'running', args: tc.args || {}, startedAt: Date.now(), endedAt: null, }; node.toolCalls.push(toolCall); _recalcStats(tree); return toolCall; } function atUpdateToolCall(nodeId: string, toolId: string, updates: Partial): void { const tree = S.activityTree; if (!tree) return; const node = tree.nodes[nodeId]; if (!node) return; const tc = node.toolCalls.find(t => t.id === toolId); if (!tc) return; Object.assign(tc, updates); if (updates.status === 'done' || updates.status === 'error') { tc.endedAt = tc.endedAt || Date.now(); tc.duration = tc.startedAt ? (tc.endedAt - tc.startedAt) / 1000 : null; } _recalcStats(tree); } // ─── Finalization ────────────────────────────────────────────────── function atFinalizeNode(nodeId: string, status: ActivityNode['status']): void { const tree = S.activityTree; if (!tree) return; const node = tree.nodes[nodeId]; if (!node) return; node.status = status; node.endedAt = Date.now(); node.duration = node.startedAt ? (node.endedAt - node.startedAt) / 1000 : null; // Finalize any still-running tool calls for (const tc of node.toolCalls) { if (tc.status === 'running' || tc.status === 'pending') { tc.status = status === 'error' ? 'error' : 'done'; tc.endedAt = node.endedAt; tc.duration = tc.startedAt ? (tc.endedAt - tc.startedAt) / 1000 : null; } } // Recursively finalize children that are still active for (const childId of node.children) { const child = tree.nodes[childId]; if (child && (child.status === 'running' || child.status === 'thinking' || child.status === 'pending')) { atFinalizeNode(childId, status); } } _recalcStats(tree); } // ─── Stats ───────────────────────────────────────────────────────── function _recalcStats(tree: ActivityTree): void { const nodes = Object.values(tree.nodes).filter(n => n.id !== tree.rootId); const tools = nodes.flatMap(n => n.toolCalls); const doneDurations = nodes .filter(n => n.duration !== null) .map(n => n.duration!); const runningNodes = nodes.filter(n => n.status === 'running' || n.status === 'thinking'); const maxElapsed = runningNodes.reduce((max, n) => { const elapsed = n.startedAt ? (Date.now() - n.startedAt) / 1000 : 0; return Math.max(max, elapsed); }, 0); tree.stats = { totalAgents: nodes.length, runningAgents: runningNodes.length, pendingAgents: nodes.filter(n => n.status === 'pending').length, doneAgents: nodes.filter(n => n.status === 'done').length, errorAgents: nodes.filter(n => n.status === 'error').length, totalTools: tools.length, doneTools: tools.filter(t => t.status === 'done').length, runningTools: tools.filter(t => t.status === 'running').length, avgResponseTime: doneDurations.length ? doneDurations.reduce((a, b) => a + b, 0) / doneDurations.length : 0, totalElapsed: maxElapsed, }; } function atGetStats(): MCStats { return S.activityTree?.stats || _emptyStats(); } // ─── Reset ───────────────────────────────────────────────────────── function atReset(): void { _nodeCounter = 0; initActivityTree(); } // ─── Formatting ──────────────────────────────────────────────────── function formatElapsed(ms: number): string { if (ms < 1000) return `${Math.round(ms)}ms`; const s = Math.floor(ms / 1000); if (s < 60) return `${s}s`; const m = Math.floor(s / 60); const rs = s % 60; if (m < 60) return `${m}m ${rs}s`; const h = Math.floor(m / 60); const rm = m % 60; return `${h}h ${rm}m`; } function formatDuration(seconds: number | null): string { if (seconds === null) return '—'; if (seconds < 0.01) return '<0.01s'; if (seconds < 1) return `${seconds.toFixed(2)}s`; if (seconds < 60) return `${seconds.toFixed(1)}s`; const m = Math.floor(seconds / 60); const rs = Math.round(seconds % 60); return `${m}m ${rs}s`; } // ─── Mock Data ───────────────────────────────────────────────────── function createMockActivityTree(): ActivityTree { initActivityTree(); const tree = S.activityTree!; const now = Date.now(); // Lotus — running with 2 tool calls const lotus = atAddNode({ agentId: 'lotus', task: 'Analysiere Gesundheitsdaten', status: 'running' })!; lotus.startedAt = now - 12000; atAddToolCall(lotus.id, { name: 'search_files', args: { pattern: 'health*', path: '/data' } }); atUpdateToolCall(lotus.id, lotus.toolCalls[0].id, { status: 'done', result: '12 matches', duration: 0.3 }); lotus.toolCalls[0].startedAt = now - 11000; lotus.toolCalls[0].endedAt = now - 10997; atAddToolCall(lotus.id, { name: 'read_file', args: { path: '/data/health-log.md' } }); // Sunflower — running with 1 done, 1 running tool const sunflower = atAddNode({ agentId: 'sunflower', task: 'Portfolio-Analyse Q1 2026', status: 'running' })!; sunflower.startedAt = now - 8000; atAddToolCall(sunflower.id, { name: 'browser_navigate', args: { url: 'https://finance.example.com' } }); atUpdateToolCall(sunflower.id, sunflower.toolCalls[0].id, { status: 'done', result: 'Page loaded', duration: 2.1 }); sunflower.toolCalls[0].startedAt = now - 7500; sunflower.toolCalls[0].endedAt = now - 5400; atAddToolCall(sunflower.id, { name: 'terminal', args: { command: 'python3 analyse.py' } }); // Dandelion — pending const dandelion = atAddNode({ agentId: 'dandelion', task: 'Triaging unread messages', status: 'pending' })!; // Add a tier-3 sub-agent under Lotus const researcher = atAddNode({ parentId: lotus.id, agentId: 'researcher', task: 'Looking up nutrition data', status: 'running', tier: 3 })!; researcher.agentEmoji = '🔍'; researcher.agentName = 'Researcher'; researcher.startedAt = now - 3000; atAddToolCall(researcher.id, { name: 'web_search', args: { query: 'nutrition database API' } }); _recalcStats(tree); return tree; } // ─── Make core functions global (IIFE-safe) ──────────────────────── window.initActivityTree = initActivityTree; window.createMockActivityTree = createMockActivityTree; window.formatElapsed = formatElapsed; window.formatDuration = formatDuration; window.atAddNode = atAddNode; window.atUpdateNode = atUpdateNode; window.atGetNode = atGetNode; window.atAddToolCall = atAddToolCall; window.atUpdateToolCall = atUpdateToolCall; window.atFinalizeNode = atFinalizeNode; window.atGetStats = atGetStats; window.atReset = atReset; window._atTrackTool = _atTrackTool; window._atTrackToolComplete = _atTrackToolComplete; window._atTrackSubagent = _atTrackSubagent; window._atTrackDone = _atTrackDone; // ─── Export for console testing ───────────────────────────────────── (window as any)._at = { init: initActivityTree, addNode: atAddNode, updateNode: atUpdateNode, getNode: atGetNode, addToolCall: atAddToolCall, updateToolCall: atUpdateToolCall, finalize: atFinalizeNode, stats: atGetStats, reset: atReset, mock: createMockActivityTree, formatElapsed, formatDuration, AGENT_META, trackSubagent: _atTrackSubagent, }; // ─── SSE Event Bridge (called from messages.ts) ──────────────────── // Tracks which agent is currently active based on tool calls. // When delegate_task is detected, creates a new agent node. // Otherwise, attaches tool calls to the current active agent node. let _activeAgentNodeId: string | null = null; // Current agent node receiving tool calls let _lastToolId: string | null = null; // Last tool call ID (for complete matching) function _atTrackTool(d: any): void { if (!S.activityTree) initActivityTree(); const tree = S.activityTree!; // Detect delegation — when tool name is 'delegate_task' if (d.name === 'delegate_task') { // Prefer explicit agent field from SSE payload (set by streaming.py // when it detects delegate_task), then fall back to args lookup. const agentId = d.agent || d.args?.agentId || d.args?.agent || d.args?.name || 'unknown'; const task = d.args?.goal || d.args?.prompt || d.args?.task || d.preview || 'Delegated task'; const node = atAddNode({ agentId, task, status: 'running' }); if (node) { _activeAgentNodeId = node.id; } return; } // Determine which node to attach the tool call to let targetNodeId = _activeAgentNodeId || tree.rootId; // Check if this tool is from a specific agent (from inflight metadata) const inflight = (window as any).INFLIGHT?.[S.session?.session_id]; if (inflight?.thisTurnAgent) { const agentKey = inflight.thisTurnAgent; // Find or create node for this agent const existingNode = Object.values(tree.nodes).find( (n: ActivityNode) => n.agentId === agentKey && (n.status === 'running' || n.status === 'thinking') ); if (existingNode) { targetNodeId = existingNode.id; _activeAgentNodeId = existingNode.id; } else if (AGENT_META[agentKey]) { // New agent appeared — create node const task = inflight.thisTurnModel ? `${inflight.thisTurnModel}` : 'Working...'; const node = atAddNode({ agentId: agentKey, task, status: 'running' }); if (node) { _activeAgentNodeId = node.id; targetNodeId = node.id; } } } const tc = atAddToolCall(targetNodeId, { name: d.name, args: d.args || {}, status: 'running', }); if (tc) { _lastToolId = tc.id; } } // ─── Subagent lifecycle event handler (from SSE 'subagent' events) ──── function _atTrackSubagent(d: any): void { if (!S.activityTree) initActivityTree(); const tree = S.activityTree!; if (d.event_type === 'subagent.start') { // Derive agentId from subagent_id if available, otherwise from goal hint. // subagent_id format: "lotus-1" → agentId = "lotus" let agentId = 'unknown'; if (d.subagent_id) { // Strip numeric suffix e.g. "lotus-1" → "lotus" agentId = d.subagent_id.replace(/-\d+$/, ''); } const task = d.goal || d.preview || `Subagent ${d.task_index ?? ''}`.trim(); const node = atAddNode({ agentId, task, status: 'running' }); if (node) { _activeAgentNodeId = node.id; } return; } if (d.event_type === 'subagent.complete' || d.event_type === 'subagent.done') { // Find the running node for this subagent and finalize it. // Use subagent_id to locate the right node. let targetNodeId: string | null = null; if (d.subagent_id) { const agentId = d.subagent_id.replace(/-\d+$/, ''); const found = Object.values(tree.nodes).find( (n: ActivityNode) => n.agentId === agentId && n.status === 'running' ); if (found) targetNodeId = found.id; } // Fall back to _activeAgentNodeId if no subagent_id match if (!targetNodeId) targetNodeId = _activeAgentNodeId; if (targetNodeId && tree.nodes[targetNodeId]) { const status = d.status === 'timeout' || d.status === 'error' ? 'error' : 'done'; atFinalizeNode(targetNodeId, status); if (_activeAgentNodeId === targetNodeId) { _activeAgentNodeId = null; } } return; } } function _atTrackToolComplete(d: any): void { if (!S.activityTree) return; const tree = S.activityTree!; // Try to find the matching running tool call across all active nodes const activeNodes = Object.values(tree.nodes).filter( (n: ActivityNode) => n.status === 'running' || n.status === 'thinking' ); for (const node of activeNodes) { for (const tc of node.toolCalls) { if (tc.status === 'running' && tc.name === d.name) { atUpdateToolCall(node.id, tc.id, { status: d.is_error ? 'error' : 'done', result: d.preview || d.result, duration: d.duration, }); return; } } } } function _atTrackDone(): void { if (!S.activityTree) return; // Finalize the active agent node if (_activeAgentNodeId) { atFinalizeNode(_activeAgentNodeId, 'done'); _activeAgentNodeId = null; } // Reset root status const root = S.activityTree.nodes[S.activityTree.rootId]; if (root) { root.status = 'running'; } } function _atTrackNewSession(): void { _activeAgentNodeId = null; _lastToolId = null; if (S.activityTree) { atReset(); } }