diff --git a/implementation-plans/_index.md b/implementation-plans/_index.md
index 41cab49..450a838 100644
--- a/implementation-plans/_index.md
+++ b/implementation-plans/_index.md
@@ -18,3 +18,4 @@
| #14 | Preset configurations | COMPLETED | [issue-014.md](issue-014.md) |
| #15 | Agent lineage visualization | COMPLETED | [issue-015.md](issue-015.md) |
| #16 | Memory candidates viewer | COMPLETED | [issue-016.md](issue-016.md) |
+| #17 | Audit/activity log view | COMPLETED | [issue-017.md](issue-017.md) |
diff --git a/implementation-plans/issue-017.md b/implementation-plans/issue-017.md
new file mode 100644
index 0000000..f63ddeb
--- /dev/null
+++ b/implementation-plans/issue-017.md
@@ -0,0 +1,56 @@
+# Issue #17 — Audit/Activity Log View
+
+**Status: COMPLETED**
+
+## Overview
+
+Timeline view of all orchestration events for a session — state transitions, tool invocations, errors. Populated from the streamed responses captured during the session.
+
+## Implementation Steps
+
+### 1. Audit Event Store (`src/lib/stores/audit.svelte.ts`) — COMPLETED
+
+- Created `AuditEvent` type with `id`, `sessionId`, `timestamp`, `eventType`, `details`, `state?`
+- Event types: `state_change`, `tool_invocation`, `error`, `message`
+- Store events grouped by session using `SvelteMap`, persisted to `localStorage`
+- Functions: `addEvent()`, `getEventsBySession()`, `getAllSessions()`, `clearSession()`
+- Sample data seeded for demo sessions
+
+### 2. AuditTimeline Component (`src/lib/components/AuditTimeline.svelte`) — COMPLETED
+
+- Vertical timeline with left-aligned time markers and event cards
+- Color-coded type badges: state change=blue, tool=green, error=red, message=gray
+- Each event shows timestamp, type badge, state label (when applicable), and detail text
+- Date headers when events span multiple days
+- Hover shadow effect on event cards
+
+### 3. Audit Route (`src/routes/audit/+page.svelte`) — COMPLETED
+
+- Dedicated `/audit` route with session selector dropdown and timeline
+- Filter controls for event type (all, state changes, tool invocations, errors, messages)
+- URL query param `?session=id` for deep linking to specific session
+- Empty states when no events or no session selected
+- Header with back-to-chat link and "Sample Data" badge
+
+### 4. Audit Route Config (`src/routes/audit/+page.ts`) — COMPLETED
+
+- `export const prerender = false` and `export const ssr = false`
+
+### 5. Homepage Navigation (`src/routes/+page.svelte`) — COMPLETED
+
+- Added "Audit Log" navigation link alongside existing Chat, Lineage, Memory links
+
+### 6. Chat Page Integration (`src/routes/chat/+page.svelte`) — COMPLETED
+
+- Added "Audit" link in the header navigation bar
+- Capture orchestration state changes during streaming into the audit store
+- Capture errors into the audit store
+
+## Files Changed
+
+- `src/lib/stores/audit.svelte.ts` (new)
+- `src/lib/components/AuditTimeline.svelte` (new)
+- `src/routes/audit/+page.svelte` (new)
+- `src/routes/audit/+page.ts` (new)
+- `src/routes/+page.svelte` (modified)
+- `src/routes/chat/+page.svelte` (modified)
diff --git a/src/lib/components/AuditTimeline.svelte b/src/lib/components/AuditTimeline.svelte
new file mode 100644
index 0000000..176473f
--- /dev/null
+++ b/src/lib/components/AuditTimeline.svelte
@@ -0,0 +1,110 @@
+
+
+{#if events.length === 0}
+
+
📄
+
No events for this session
+
Events will appear here as orchestration runs.
+
+{:else}
+
+
+
+
+
+ {#each events as event, i (event.id)}
+ {@const showDate = i === 0 || formatDate(event.timestamp) !== formatDate(events[i - 1].timestamp)}
+
+ {#if showDate}
+ -
+ {formatDate(event.timestamp)}
+
+ {/if}
+
+ -
+
+
+
+
+
+
+ {formatTimestamp(event.timestamp)}
+
+ {typeLabel(event.eventType)}
+
+ {#if event.state}
+
+ {event.state}
+
+ {/if}
+
+
{event.details}
+
+
+ {/each}
+
+
+{/if}
diff --git a/src/lib/stores/audit.svelte.ts b/src/lib/stores/audit.svelte.ts
new file mode 100644
index 0000000..662191f
--- /dev/null
+++ b/src/lib/stores/audit.svelte.ts
@@ -0,0 +1,247 @@
+import { SvelteMap } from 'svelte/reactivity';
+
+export type AuditEventType = 'state_change' | 'tool_invocation' | 'error' | 'message';
+
+export interface AuditEvent {
+ id: string;
+ sessionId: string;
+ timestamp: Date;
+ eventType: AuditEventType;
+ details: string;
+ state?: string;
+}
+
+export interface SessionAuditLog {
+ sessionId: string;
+ events: AuditEvent[];
+}
+
+const STORAGE_KEY = 'llm-multiverse-audit-events';
+
+function loadEvents(): SvelteMap {
+ if (typeof localStorage === 'undefined') return new SvelteMap();
+ try {
+ const raw = localStorage.getItem(STORAGE_KEY);
+ if (!raw) return new SvelteMap();
+ const arr: [string, AuditEvent[]][] = JSON.parse(raw);
+ return new SvelteMap(
+ arr.map(([id, events]) => [
+ id,
+ events.map((e) => ({ ...e, timestamp: new Date(e.timestamp) }))
+ ])
+ );
+ } catch {
+ return new SvelteMap();
+ }
+}
+
+function saveEvents(events: SvelteMap) {
+ if (typeof localStorage === 'undefined') return;
+ localStorage.setItem(STORAGE_KEY, JSON.stringify([...events.entries()]));
+}
+
+function getSampleData(): SvelteMap {
+ const samples = new SvelteMap();
+ const now = Date.now();
+
+ samples.set('session-demo-alpha', [
+ {
+ id: 'evt-001',
+ sessionId: 'session-demo-alpha',
+ timestamp: new Date(now - 300_000),
+ eventType: 'state_change',
+ details: 'Orchestration started — decomposing user request',
+ state: 'DECOMPOSING'
+ },
+ {
+ id: 'evt-002',
+ sessionId: 'session-demo-alpha',
+ timestamp: new Date(now - 280_000),
+ eventType: 'tool_invocation',
+ details: 'Invoked web_search tool for "SvelteKit best practices"',
+ state: 'DECOMPOSING'
+ },
+ {
+ id: 'evt-003',
+ sessionId: 'session-demo-alpha',
+ timestamp: new Date(now - 260_000),
+ eventType: 'state_change',
+ details: 'Dispatching subtasks to subagents',
+ state: 'DISPATCHING'
+ },
+ {
+ id: 'evt-004',
+ sessionId: 'session-demo-alpha',
+ timestamp: new Date(now - 240_000),
+ eventType: 'message',
+ details: 'Researcher agent assigned to subtask "Gather TypeScript patterns"'
+ },
+ {
+ id: 'evt-005',
+ sessionId: 'session-demo-alpha',
+ timestamp: new Date(now - 220_000),
+ eventType: 'state_change',
+ details: 'Subagents executing tasks',
+ state: 'EXECUTING'
+ },
+ {
+ id: 'evt-006',
+ sessionId: 'session-demo-alpha',
+ timestamp: new Date(now - 200_000),
+ eventType: 'tool_invocation',
+ details: 'Invoked code_analysis tool on project source files'
+ },
+ {
+ id: 'evt-007',
+ sessionId: 'session-demo-alpha',
+ timestamp: new Date(now - 180_000),
+ eventType: 'error',
+ details: 'Timeout waiting for code_analysis response (retrying)'
+ },
+ {
+ id: 'evt-008',
+ sessionId: 'session-demo-alpha',
+ timestamp: new Date(now - 160_000),
+ eventType: 'tool_invocation',
+ details: 'Retry: code_analysis tool succeeded'
+ },
+ {
+ id: 'evt-009',
+ sessionId: 'session-demo-alpha',
+ timestamp: new Date(now - 140_000),
+ eventType: 'state_change',
+ details: 'Compacting results from subagents',
+ state: 'COMPACTING'
+ },
+ {
+ id: 'evt-010',
+ sessionId: 'session-demo-alpha',
+ timestamp: new Date(now - 120_000),
+ eventType: 'state_change',
+ details: 'Orchestration complete',
+ state: 'COMPLETE'
+ }
+ ]);
+
+ samples.set('session-demo-beta', [
+ {
+ id: 'evt-011',
+ sessionId: 'session-demo-beta',
+ timestamp: new Date(now - 600_000),
+ eventType: 'state_change',
+ details: 'Orchestration started — decomposing deployment request',
+ state: 'DECOMPOSING'
+ },
+ {
+ id: 'evt-012',
+ sessionId: 'session-demo-beta',
+ timestamp: new Date(now - 580_000),
+ eventType: 'state_change',
+ details: 'Dispatching to sysadmin agent',
+ state: 'DISPATCHING'
+ },
+ {
+ id: 'evt-013',
+ sessionId: 'session-demo-beta',
+ timestamp: new Date(now - 560_000),
+ eventType: 'state_change',
+ details: 'Executing deployment analysis',
+ state: 'EXECUTING'
+ },
+ {
+ id: 'evt-014',
+ sessionId: 'session-demo-beta',
+ timestamp: new Date(now - 540_000),
+ eventType: 'tool_invocation',
+ details: 'Invoked kubectl_check tool for cluster status'
+ },
+ {
+ id: 'evt-015',
+ sessionId: 'session-demo-beta',
+ timestamp: new Date(now - 520_000),
+ eventType: 'error',
+ details: 'Permission denied: kubectl access not granted for this session'
+ },
+ {
+ id: 'evt-016',
+ sessionId: 'session-demo-beta',
+ timestamp: new Date(now - 500_000),
+ eventType: 'message',
+ details: 'Agent requested elevated permissions for cluster access'
+ },
+ {
+ id: 'evt-017',
+ sessionId: 'session-demo-beta',
+ timestamp: new Date(now - 480_000),
+ eventType: 'state_change',
+ details: 'Compacting partial results',
+ state: 'COMPACTING'
+ },
+ {
+ id: 'evt-018',
+ sessionId: 'session-demo-beta',
+ timestamp: new Date(now - 460_000),
+ eventType: 'state_change',
+ details: 'Orchestration complete with warnings',
+ state: 'COMPLETE'
+ }
+ ]);
+
+ return samples;
+}
+
+function createAuditStore() {
+ const events = $state>(loadEvents());
+
+ // Seed sample data if store is empty
+ if (events.size === 0) {
+ const samples = getSampleData();
+ for (const [id, evts] of samples) {
+ events.set(id, evts);
+ }
+ saveEvents(events);
+ }
+
+ function addEvent(
+ sessionId: string,
+ event: Omit
+ ) {
+ const auditEvent: AuditEvent = {
+ id: crypto.randomUUID(),
+ sessionId,
+ timestamp: new Date(),
+ ...event
+ };
+ const existing = events.get(sessionId) ?? [];
+ events.set(sessionId, [...existing, auditEvent]);
+ saveEvents(events);
+ }
+
+ function getEventsBySession(sessionId: string): AuditEvent[] {
+ return events.get(sessionId) ?? [];
+ }
+
+ function getAllSessions(): SessionAuditLog[] {
+ return [...events.entries()]
+ .map(([sessionId, evts]) => ({ sessionId, events: evts }))
+ .sort((a, b) => {
+ const aLatest = a.events.length > 0 ? a.events[a.events.length - 1].timestamp.getTime() : 0;
+ const bLatest = b.events.length > 0 ? b.events[b.events.length - 1].timestamp.getTime() : 0;
+ return bLatest - aLatest;
+ });
+ }
+
+ function clearSession(sessionId: string) {
+ events.delete(sessionId);
+ saveEvents(events);
+ }
+
+ return {
+ addEvent,
+ getEventsBySession,
+ getAllSessions,
+ clearSession
+ };
+}
+
+export const auditStore = createAuditStore();
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
index d06f761..7e9179a 100644
--- a/src/routes/+page.svelte
+++ b/src/routes/+page.svelte
@@ -4,6 +4,7 @@
const chatHref = resolveRoute('/chat');
const lineageHref = resolveRoute('/lineage');
const memoryHref = resolveRoute('/memory');
+ const auditHref = resolveRoute('/audit');
LLM Multiverse UI
@@ -15,4 +16,6 @@
Agent Lineage
Memory Candidates
+
+ Audit Log
diff --git a/src/routes/audit/+page.svelte b/src/routes/audit/+page.svelte
new file mode 100644
index 0000000..f6ccfc8
--- /dev/null
+++ b/src/routes/audit/+page.svelte
@@ -0,0 +1,128 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {#if selectedSessionId}
+
+
+
+
+
+
+ {totalEvents} event{totalEvents !== 1 ? 's' : ''}
+
+ {/if}
+
+
+
+
+
+ {#if !selectedSessionId}
+ {#if allSessions.length === 0}
+
+
📋
+
No audit events recorded
+
+ Events will appear here as orchestration sessions run.
+
+
+ {:else}
+
+
🔍
+
Select a session to view its audit log
+
+ Choose a session from the dropdown above.
+
+
+ {/if}
+ {:else}
+
+ {/if}
+
+
diff --git a/src/routes/audit/+page.ts b/src/routes/audit/+page.ts
new file mode 100644
index 0000000..ae88a27
--- /dev/null
+++ b/src/routes/audit/+page.ts
@@ -0,0 +1,2 @@
+export const prerender = false;
+export const ssr = false;
diff --git a/src/routes/chat/+page.svelte b/src/routes/chat/+page.svelte
index 06899e1..40b7ee5 100644
--- a/src/routes/chat/+page.svelte
+++ b/src/routes/chat/+page.svelte
@@ -19,6 +19,7 @@
import { OrchestrationState } from '$lib/proto/llm_multiverse/v1/orchestrator_pb';
import { sessionStore } from '$lib/stores/sessions.svelte';
import { memoryStore } from '$lib/stores/memory.svelte';
+ import { auditStore } from '$lib/stores/audit.svelte';
let messages: ChatMessage[] = $state([]);
let isStreaming = $state(false);
@@ -32,6 +33,7 @@
let showConfig = $state(false);
const lineageHref = resolveRoute('/lineage');
const memoryHref = resolveRoute('/memory');
+ const auditHref = resolveRoute('/audit');
const isNonDefaultConfig = $derived(
sessionConfig.overrideLevel !== OverrideLevel.NONE ||
@@ -80,6 +82,7 @@
finalResult = null;
const sessionId = sessionStore.activeSessionId!;
+ let lastAuditState = OrchestrationState.UNSPECIFIED;
const userMessage: ChatMessage = {
id: crypto.randomUUID(),
@@ -103,6 +106,18 @@
try {
for await (const response of processRequest(sessionId, content, sessionConfig)) {
orchestrationState = response.state;
+
+ // Capture state changes to the audit log
+ if (response.state !== lastAuditState) {
+ const stateLabel = OrchestrationState[response.state] ?? String(response.state);
+ auditStore.addEvent(sessionId, {
+ eventType: 'state_change',
+ details: response.message || `State changed to ${stateLabel}`,
+ state: stateLabel
+ });
+ lastAuditState = response.state;
+ }
+
if (response.intermediateResult) {
intermediateResult = response.intermediateResult;
}
@@ -131,6 +146,10 @@
? `Error (${err.code}): ${err.message}`
: 'An unexpected error occurred';
error = msg;
+ auditStore.addEvent(sessionId, {
+ eventType: 'error',
+ details: msg
+ });
const idx = messages.length - 1;
messages[idx] = {
...messages[idx],
@@ -163,6 +182,12 @@
>
Memory
+
+ Audit
+