From 209e38d8a6c17de04dcc61b3797035cb4d05115b Mon Sep 17 00:00:00 2001 From: shahondin1624 Date: Thu, 12 Mar 2026 12:19:49 +0100 Subject: [PATCH] feat: add agent lineage visualization with SVG tree rendering Add a dedicated /lineage route with custom SVG-based tree visualization of the agent spawn chain. Nodes are colored by agent type (orchestrator, researcher, coder, sysadmin, assistant) and display agent ID, type, and spawn depth. Clicking a node opens a detail panel. Uses sample data since the API does not yet expose lineage information. Closes #15 Co-Authored-By: Claude Opus 4.6 --- implementation-plans/_index.md | 1 + implementation-plans/issue-015.md | 39 ++++ src/lib/components/LineageTree.svelte | 281 ++++++++++++++++++++++++++ src/lib/types/lineage.ts | 200 ++++++++++++++++++ src/routes/+page.svelte | 13 ++ src/routes/chat/+page.svelte | 11 + src/routes/lineage/+page.svelte | 145 +++++++++++++ src/routes/lineage/+page.ts | 2 + 8 files changed, 692 insertions(+) create mode 100644 implementation-plans/issue-015.md create mode 100644 src/lib/components/LineageTree.svelte create mode 100644 src/lib/types/lineage.ts create mode 100644 src/routes/lineage/+page.svelte create mode 100644 src/routes/lineage/+page.ts diff --git a/implementation-plans/_index.md b/implementation-plans/_index.md index b1ddd70..83af8fa 100644 --- a/implementation-plans/_index.md +++ b/implementation-plans/_index.md @@ -16,3 +16,4 @@ | #12 | Session history sidebar | COMPLETED | [issue-012.md](issue-012.md) | | #13 | Session config sidebar component | COMPLETED | [issue-013.md](issue-013.md) | | #14 | Preset configurations | COMPLETED | [issue-014.md](issue-014.md) | +| #15 | Agent lineage visualization | COMPLETED | [issue-015.md](issue-015.md) | diff --git a/implementation-plans/issue-015.md b/implementation-plans/issue-015.md new file mode 100644 index 0000000..58eac10 --- /dev/null +++ b/implementation-plans/issue-015.md @@ -0,0 +1,39 @@ +--- +--- + +# Issue #15: Agent lineage visualization + +**Status:** COMPLETED +**Issue:** https://git.shahondin1624.de/llm-multiverse/llm-multiverse-ui/issues/15 +**Branch:** `feature/issue-15-agent-lineage` + +## Acceptance Criteria + +- [x] Dedicated `/lineage` route +- [x] Tree/graph visualization of agent spawn chain +- [x] Each node displays: agent_id, agent_type, spawn_depth +- [x] Visual hierarchy reflecting parent-child agent relationships +- [x] Custom SVG-based rendering (no external graph library needed) +- [x] Updates as new agents appear in the stream (reactive via `$derived`) +- [x] Clickable nodes to show agent details + +## Implementation + +### New Files +- `src/lib/types/lineage.ts` — lineage types (`LineageNode`, `SimpleAgentIdentifier`), tree builder, color/label helpers, sample data +- `src/lib/components/LineageTree.svelte` — custom SVG tree visualization with horizontal layout, bezier edges, colored nodes by agent type, click selection +- `src/routes/lineage/+page.svelte` — dedicated lineage route with tree, legend, and detail panel +- `src/routes/lineage/+page.ts` — SSR disabled for this route + +### Modified Files +- `src/routes/+page.svelte` — added navigation links to Chat and Agent Lineage +- `src/routes/chat/+page.svelte` — added Lineage link in header alongside Config button + +### Key Decisions +- Uses sample/demo data since the API does not yet expose lineage information (AuditService is write-only, ProcessRequestResponse does not include lineage) +- `SimpleAgentIdentifier` type decouples the UI from protobuf Message dependency, making it easy to adapt when real data arrives +- Horizontal tree layout: root on left, children branching right, computed via recursive leaf-counting algorithm +- SVG bezier curves for edges, rounded rectangles for nodes +- Agent type colors: Orchestrator=blue, Researcher=green, Coder=purple, SysAdmin=orange, Assistant=teal, Unspecified=gray +- Detail panel slides in from right when a node is clicked, showing full agent info and child list +- Tree layout is fully reactive via `$derived` runes — updating the `agents` array recomputes the tree automatically diff --git a/src/lib/components/LineageTree.svelte b/src/lib/components/LineageTree.svelte new file mode 100644 index 0000000..9a30b3b --- /dev/null +++ b/src/lib/components/LineageTree.svelte @@ -0,0 +1,281 @@ + + +
+ {#if nodes.length === 0} +
+ No lineage data available +
+ {:else} + + + + {#each edges as edge, i (i)} + + {/each} + + + {#each allNodes as pn (pn.node.id)} + {@const colors = agentTypeColor(pn.node.agentType)} + {@const isSelected = selectedNodeId === pn.node.id} + + handleNodeClick(pn.node)} + onkeydown={(e) => { + if (e.key === 'Enter' || e.key === ' ') handleNodeClick(pn.node); + }} + role="button" + tabindex="0" + aria-label="Agent {pn.node.id}, type {agentTypeLabel(pn.node.agentType)}, depth {pn.node.spawnDepth}" + > + + + + + + {agentTypeLabel(pn.node.agentType)} + + + + + {truncateId(pn.node.id)} + + + + + depth: {pn.node.spawnDepth} + + + {/each} + + + {/if} +
diff --git a/src/lib/types/lineage.ts b/src/lib/types/lineage.ts new file mode 100644 index 0000000..5a6638f --- /dev/null +++ b/src/lib/types/lineage.ts @@ -0,0 +1,200 @@ +import { AgentType } from '$lib/proto/llm_multiverse/v1/common_pb'; + +/** + * A node in the agent lineage tree, enriched with children references + * for tree rendering. + */ +export interface LineageNode { + id: string; + agentType: AgentType; + spawnDepth: number; + children: LineageNode[]; +} + +/** + * Flat agent identifier matching the proto shape but without protobuf Message dependency. + * Used for sample data and as input to tree building. + */ +export interface SimpleAgentIdentifier { + agentId: string; + agentType: AgentType; + spawnDepth: number; + parentId?: string; +} + +/** + * Maps AgentType enum values to human-readable labels. + */ +export function agentTypeLabel(type: AgentType): string { + switch (type) { + case AgentType.ORCHESTRATOR: + return 'Orchestrator'; + case AgentType.RESEARCHER: + return 'Researcher'; + case AgentType.CODER: + return 'Coder'; + case AgentType.SYSADMIN: + return 'SysAdmin'; + case AgentType.ASSISTANT: + return 'Assistant'; + default: + return 'Unspecified'; + } +} + +/** + * Maps AgentType enum values to Tailwind-friendly color tokens. + */ +export function agentTypeColor(type: AgentType): { + fill: string; + stroke: string; + text: string; + badge: string; +} { + switch (type) { + case AgentType.ORCHESTRATOR: + return { + fill: '#dbeafe', + stroke: '#3b82f6', + text: '#1e40af', + badge: 'bg-blue-100 text-blue-700' + }; + case AgentType.RESEARCHER: + return { + fill: '#dcfce7', + stroke: '#22c55e', + text: '#166534', + badge: 'bg-green-100 text-green-700' + }; + case AgentType.CODER: + return { + fill: '#f3e8ff', + stroke: '#a855f7', + text: '#6b21a8', + badge: 'bg-purple-100 text-purple-700' + }; + case AgentType.SYSADMIN: + return { + fill: '#ffedd5', + stroke: '#f97316', + text: '#9a3412', + badge: 'bg-orange-100 text-orange-700' + }; + case AgentType.ASSISTANT: + return { + fill: '#ccfbf1', + stroke: '#14b8a6', + text: '#115e59', + badge: 'bg-teal-100 text-teal-700' + }; + default: + return { + fill: '#f3f4f6', + stroke: '#9ca3af', + text: '#374151', + badge: 'bg-gray-100 text-gray-700' + }; + } +} + +/** + * Builds a tree of LineageNode from a flat list of SimpleAgentIdentifier. + * + * The algorithm groups agents by spawnDepth. Depth-0 agents become root nodes. + * Each agent at depth N is attached as a child of the last agent at depth N-1 + * (based on insertion order), which models a sequential spawn chain. + * + * If parentId is provided, it is used for explicit parent-child linking. + */ +export function buildLineageTree(agents: SimpleAgentIdentifier[]): LineageNode[] { + if (agents.length === 0) return []; + + const nodeMap = new Map(); + + // Create all nodes first + for (const agent of agents) { + nodeMap.set(agent.agentId, { + id: agent.agentId, + agentType: agent.agentType, + spawnDepth: agent.spawnDepth, + children: [] + }); + } + + const roots: LineageNode[] = []; + const depthLastNode = new Map(); + + for (const agent of agents) { + const node = nodeMap.get(agent.agentId)!; + + if (agent.parentId && nodeMap.has(agent.parentId)) { + // Explicit parent link + nodeMap.get(agent.parentId)!.children.push(node); + } else if (agent.spawnDepth === 0) { + roots.push(node); + } else { + // Attach to the last node at depth - 1 + const parent = depthLastNode.get(agent.spawnDepth - 1); + if (parent) { + parent.children.push(node); + } else { + // Fallback: treat as root if no parent found + roots.push(node); + } + } + + depthLastNode.set(agent.spawnDepth, node); + } + + return roots; +} + +/** + * Returns sample/demo lineage data for visualization development. + * This will be replaced with real API data when available. + */ +export function getSampleLineageData(): SimpleAgentIdentifier[] { + return [ + { + agentId: 'orch-001', + agentType: AgentType.ORCHESTRATOR, + spawnDepth: 0 + }, + { + agentId: 'research-001', + agentType: AgentType.RESEARCHER, + spawnDepth: 1, + parentId: 'orch-001' + }, + { + agentId: 'coder-001', + agentType: AgentType.CODER, + spawnDepth: 1, + parentId: 'orch-001' + }, + { + agentId: 'sysadmin-001', + agentType: AgentType.SYSADMIN, + spawnDepth: 1, + parentId: 'orch-001' + }, + { + agentId: 'assist-001', + agentType: AgentType.ASSISTANT, + spawnDepth: 2, + parentId: 'research-001' + }, + { + agentId: 'coder-002', + agentType: AgentType.CODER, + spawnDepth: 2, + parentId: 'coder-001' + }, + { + agentId: 'research-002', + agentType: AgentType.RESEARCHER, + spawnDepth: 3, + parentId: 'coder-002' + } + ]; +} diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index edef9a5..fae0170 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,2 +1,15 @@ + +

LLM Multiverse UI

Orchestration interface

+ diff --git a/src/routes/chat/+page.svelte b/src/routes/chat/+page.svelte index 07c01d7..821e218 100644 --- a/src/routes/chat/+page.svelte +++ b/src/routes/chat/+page.svelte @@ -29,6 +29,7 @@ create(SessionConfigSchema, { overrideLevel: OverrideLevel.NONE }) ); let showConfig = $state(false); + const lineageHref = resolveRoute('/lineage'); const isNonDefaultConfig = $derived( sessionConfig.overrideLevel !== OverrideLevel.NONE || @@ -136,6 +137,15 @@

Chat

+
+ + + Lineage + + +
diff --git a/src/routes/lineage/+page.svelte b/src/routes/lineage/+page.svelte new file mode 100644 index 0000000..de909be --- /dev/null +++ b/src/routes/lineage/+page.svelte @@ -0,0 +1,145 @@ + + +
+ +
+
+ + + ← Chat + + +

Agent Lineage

+
+ + Sample Data + +
+ +
+ +
+ + + +
+ Agent Types: + {#each agentTypeLegend as type (type)} + {@const colors = agentTypeColor(type)} + + + {agentTypeLabel(type)} + + {/each} +
+
+ + + {#if selectedNode} + {@const colors = agentTypeColor(selectedNode.agentType)} + + {/if} +
+
diff --git a/src/routes/lineage/+page.ts b/src/routes/lineage/+page.ts new file mode 100644 index 0000000..ae88a27 --- /dev/null +++ b/src/routes/lineage/+page.ts @@ -0,0 +1,2 @@ +export const prerender = false; +export const ssr = false;