|
4 | 4 |
|
5 | 5 | Localhost web dashboard for real-time agent introspection. Serves at `http://0.0.0.0:8080`, accessible via Tailscale at `http://norisor:8080`. |
6 | 6 |
|
7 | | -Built with `aiohttp` — runs as another coroutine in `asyncio.gather` alongside the cognitive loop, consolidation, and peripherals. Dashboard crash does not kill the agent. |
| 7 | +Built with `aiohttp` — runs as another coroutine in `asyncio.gather`. Dashboard crash does not kill the agent. |
8 | 8 |
|
9 | | -## Architecture |
| 9 | +## Version History |
10 | 10 |
|
11 | | -``` |
12 | | -AgentState (dataclass) |
13 | | - created in main.py |
14 | | - written by cognitive_loop (assigns attention, gut, safety, etc.) |
15 | | - read by dashboard handlers |
16 | | - conversation list is shared (same object reference) |
17 | | - SSE broadcast via asyncio.Queue per subscriber |
18 | | -
|
19 | | -run_dashboard() |
20 | | - aiohttp web.Application |
21 | | - binds 0.0.0.0:8080 |
22 | | - waits on shutdown_event |
23 | | - access_log disabled |
24 | | -``` |
| 11 | +- **v1** (commit d685e49, 1c5c005): 4-panel grid (Live Feed, Status, Context Window, Memory Store). Deployed on norisor. |
| 12 | +- **v2** (commit 427d213): Terminal-style consciousness monitor. Two-column asymmetric layout. LOCAL ONLY, not yet deployed. |
25 | 13 |
|
26 | | -## Data Sharing |
| 14 | +## v2 Architecture (Current Code) |
27 | 15 |
|
28 | | -`AgentState` is created in `main.py` and passed to both `cognitive_loop()` and `run_dashboard()`. |
| 16 | +``` |
| 17 | ++----------------------------------------------------------------------+ |
| 18 | +| [*] Agent Consciousness gut:0.42 boot:5/10 $0.003 esc:0 q:1 | 28px header |
| 19 | ++--------------------------------------------------+-------------------+ |
| 20 | +| | | |
| 21 | +| CONSCIOUS MIND (65%) | ATTENTION (35%) | |
| 22 | +| SSE-driven cycle blocks: | Full text of | |
| 23 | +| - INPUT (what won attention) | all candidates | |
| 24 | +| - SYSTEM PROMPT (collapsible) | | |
| 25 | +| - CONVERSATION (collapsible) +-------------------+ |
| 26 | +| - RESPONSE S1/S2 [model] conf:X | | |
| 27 | +| | MEMORY SEARCH | |
| 28 | +| Auto-scrolls, scroll-lock on manual scroll | [search input] | |
| 29 | +| | semantic results | |
| 30 | ++--------------------------------------------------+-------------------+ |
| 31 | +``` |
29 | 32 |
|
30 | | -- Loop assigns internal objects after creation: `agent_state.attention = attention`, etc. |
31 | | -- Conversation list is shared by reference: `conversation = agent_state.conversation` |
32 | | -- Exchange count synced: `agent_state.exchange_count = exchange_count` |
33 | | -- All reads are safe (single event loop, no thread contention). |
| 33 | +## Data Flow |
34 | 34 |
|
35 | | -## SSE Events |
| 35 | +``` |
| 36 | +cognitive_loop |
| 37 | + -> agent_state.publish_event(cycle_start) # winner + losers + full content |
| 38 | + -> agent_state.publish_event(context_assembled) # full system prompt + conversation |
| 39 | + -> agent_state.publish_event(llm_response) # full reply + model + confidence |
| 40 | + -> agent_state.publish_event(escalation) # triggers + confidence (if S2) |
| 41 | + -> agent_state.publish_event(gate_flush) # persisted + dropped counts |
| 42 | + -> _log_consciousness(...) # persistent NDJSON log |
| 43 | +
|
| 44 | +Dashboard frontend (EventSource) |
| 45 | + -> cycle_start => create cycle block, show INPUT, update Attention Queue |
| 46 | + -> context_assembled => add collapsible SYSTEM PROMPT + CONVERSATION sections |
| 47 | + -> llm_response => add RESPONSE section (green=S1, orange=S2) |
| 48 | + -> escalation => add ESCALATION notice (red) |
| 49 | + -> gate_flush => standalone entry in conscious mind stream |
| 50 | +``` |
36 | 51 |
|
37 | | -The cognitive loop publishes events at 4 points via `agent_state.publish_event()`: |
| 52 | +## SSE Events (5 types) |
38 | 53 |
|
39 | | -| Event | When | Data | |
40 | | -|-------|------|------| |
41 | | -| `cycle_start` | After winner selected | source, content preview, salience, queue_size | |
42 | | -| `llm_response` | After LLM response | reply preview, confidence, escalated | |
43 | | -| `escalation` | Before System 2 call | triggers, confidence | |
44 | | -| `gate_flush` | After periodic flush | persisted count, dropped count | |
| 54 | +| Event | Emitted When | Key Data | |
| 55 | +|-------|-------------|----------| |
| 56 | +| `cycle_start` | After attention selects winner | winner{source, content, salience}, losers[]{source, content, salience}, queue_size | |
| 57 | +| `context_assembled` | After system prompt built | system_prompt (full text), conversation[], identity_tokens, context_shift | |
| 58 | +| `llm_response` | After LLM returns | reply (full text), escalated (bool), model (string), confidence | |
| 59 | +| `escalation` | Before System 2 call | triggers[], confidence | |
| 60 | +| `gate_flush` | After periodic scratch flush | persisted (int), dropped (int) | |
45 | 61 |
|
46 | | -Each browser tab gets its own `asyncio.Queue(maxsize=200)`. Events are fire-and-forget: `QueueFull` silently drops the subscriber. SSE keepalive every 15s. |
| 62 | +All SSE events carry full content — no truncation. |
47 | 63 |
|
48 | | -## API Routes |
| 64 | +## API Routes (v2) |
49 | 65 |
|
50 | 66 | ``` |
51 | | -GET / -> HTML dashboard (inline, dark theme) |
52 | | -GET /events -> SSE stream (real-time cognitive events) |
53 | | -GET /api/status -> JSON agent state snapshot |
54 | | -GET /api/memories -> JSON paginated memory list (?limit=20&offset=0) |
55 | | -GET /api/memory/{id} -> JSON single memory detail |
56 | | -GET /api/attention -> JSON attention queue contents |
57 | | -GET /api/gut -> JSON gut feeling state + delta log |
58 | | -GET /api/conversation -> JSON current conversation window |
59 | | -GET /api/energy -> JSON energy tracker breakdown |
| 67 | +GET / -> HTML dashboard |
| 68 | +GET /events -> SSE stream |
| 69 | +GET /api/status -> JSON header bar data (agent_id, phase, models, gut, bootstrap, energy, escalation) |
| 70 | +GET /api/attention -> JSON attention queue (full text of all candidates) |
| 71 | +GET /api/memories/search -> JSON semantic search (?q=query, uses search_hybrid mutate=False) |
| 72 | +GET /api/memory/{id} -> JSON single memory full detail |
60 | 73 | ``` |
61 | 74 |
|
62 | | -## Memory Browser |
63 | | - |
64 | | -Uses direct `asyncpg pool.fetch()` with SELECT queries. Does NOT go through `MemoryStore` methods to avoid side effects: |
65 | | -- No access count updates |
66 | | -- No retrieval mutation |
67 | | -- The agent doesn't "feel" you browsing its memories |
| 75 | +Removed from v1: `/api/memories` (paginated), `/api/gut`, `/api/conversation`, `/api/energy` |
| 76 | + |
| 77 | +## Memory Search |
| 78 | + |
| 79 | +New endpoint `GET /api/memories/search?q=...`: |
| 80 | +- Empty query: returns latest 10 memories via direct `pool.fetch()` |
| 81 | +- With query: calls `memory.search_hybrid(query=q, top_k=15, mutate=False)` |
| 82 | +- `mutate=False` is critical — no access count updates, no retrieval mutation |
| 83 | +- Frontend: 300ms debounce on input, Enter for immediate search |
| 84 | +- Click memory to expand inline (fetches full detail via `/api/memory/{id}`) |
| 85 | + |
| 86 | +## Consciousness Log |
| 87 | + |
| 88 | +Persistent NDJSON at `~/.agent/logs/consciousness.ndjson`. Each cycle appends: |
| 89 | + |
| 90 | +```json |
| 91 | +{ |
| 92 | + "ts": "2026-02-14T...", |
| 93 | + "source": "external_user", |
| 94 | + "salience": 0.847, |
| 95 | + "input": "full input text", |
| 96 | + "system_prompt_len": 2340, |
| 97 | + "conversation_len": 5, |
| 98 | + "reply": "full LLM response text", |
| 99 | + "escalated": false, |
| 100 | + "confidence": 0.72, |
| 101 | + "context_shift": 0.45, |
| 102 | + "queue_size_after": 0 |
| 103 | +} |
| 104 | +``` |
68 | 105 |
|
69 | | -## Frontend |
| 106 | +Fire-and-forget — logging errors never block the cognitive loop. |
70 | 107 |
|
71 | | -Single inline HTML/CSS/JS string (`DASHBOARD_HTML`). Dark theme, vanilla JS with `EventSource`. |
| 108 | +## AgentState Dataclass |
72 | 109 |
|
73 | | -4 panels: |
74 | | -1. **Live Feed** - scrolling SSE events (attention wins, LLM responses, gate flushes, escalations) |
75 | | -2. **Agent Status** - refreshes every 5s (phase, model, memory count, bootstrap, gut, energy cost) |
76 | | -3. **Context Window** - refreshes every 3s (current conversation messages) |
77 | | -4. **Memory Store** - paginated table with click-to-expand modal (refreshes every 10s) |
| 110 | +Created in `main.py`, passed to both `cognitive_loop()` and `run_dashboard()`. |
78 | 111 |
|
79 | | -All dynamic content uses `textContent` and DOM construction (no `innerHTML` with API data) to prevent XSS. |
| 112 | +```python |
| 113 | +@dataclass |
| 114 | +class AgentState: |
| 115 | + config, layers, memory # Set by main.py |
| 116 | + attention, gut, safety # Set by cognitive_loop after init |
| 117 | + outcome_tracker, bootstrap # Set by cognitive_loop after init |
| 118 | + conversation: list # Shared by reference with loop |
| 119 | + exchange_count: int # Synced by loop |
| 120 | + escalation_stats: dict # Points to loop's _escalation_stats |
| 121 | + _sse_subscribers: list # Per-browser asyncio.Queue(maxsize=200) |
| 122 | +``` |
80 | 123 |
|
81 | 124 | ## Modules |
82 | 125 |
|
83 | | -### `src/dashboard.py` (~900 lines) |
| 126 | +### `src/dashboard.py` (~1035 lines) |
84 | 127 |
|
85 | 128 | - `AgentState` dataclass with SSE broadcast |
86 | | -- Route handlers for all API endpoints |
| 129 | +- Route handlers: index, sse, api_status, api_attention, api_memories_search, api_memory_detail |
87 | 130 | - `run_dashboard()` coroutine |
88 | | -- Inline HTML/CSS/JS |
| 131 | +- `_row_to_memory()` helper |
| 132 | +- Inline HTML/CSS/JS (~680 lines) |
89 | 133 |
|
90 | 134 | ### `src/loop.py` (modified) |
91 | 135 |
|
92 | 136 | - Signature: `cognitive_loop(..., agent_state=None)` |
93 | | -- Assigns objects to `agent_state` after creation |
94 | | -- Uses shared conversation: `agent_state.conversation if agent_state else []` |
95 | | -- Publishes 4 SSE events at key points |
96 | | -- All agent_state operations guarded by `if agent_state:` (no-op without dashboard) |
| 137 | +- Assigns objects to agent_state after creation |
| 138 | +- Publishes 5 SSE events at key points (cycle_start, context_assembled, llm_response, escalation, gate_flush) |
| 139 | +- Writes to consciousness log each cycle |
| 140 | +- Shares conversation list and escalation_stats by reference |
97 | 141 |
|
98 | 142 | ### `src/main.py` (modified) |
99 | 143 |
|
100 | 144 | - Creates `AgentState(config=config, layers=layers, memory=memory)` |
101 | 145 | - Passes `agent_state` to `cognitive_loop()` |
102 | 146 | - Adds `run_dashboard(agent_state, shutdown_event)` to tasks |
103 | 147 |
|
104 | | -## Security |
| 148 | +## Frontend Details |
105 | 149 |
|
106 | | -- Binds to 0.0.0.0 but only accessible via Tailscale (not exposed to public internet) |
107 | | -- Read-only: no mutation endpoints, no write operations |
108 | | -- No authentication (Tailscale provides network-level auth) |
109 | | -- XSS prevented: all dynamic content via textContent/DOM construction |
| 150 | +- Dark theme, monospace font (SF Mono/Fira Code/Cascadia/Consolas) |
| 151 | +- All dynamic content via `textContent` + DOM construction (XSS-safe) |
| 152 | +- Auto-scroll with scroll-lock: pauses when user scrolls up, resumes at bottom |
| 153 | +- Collapsible sections: click label to toggle (triangle indicator) |
| 154 | +- Color coding: blue=external_user, purple=internal_dmn, green=S1/winner, orange=S2, red=escalation |
| 155 | +- Memory depth bars: green >70%, orange >40%, dim otherwise |
| 156 | +- Header polls `/api/status` every 5s |
| 157 | +- Attention polls `/api/attention` every 5s (between SSE cycles) |
110 | 158 |
|
111 | 159 | ## Port |
112 | 160 |
|
113 | 161 | | Service | Port | |
114 | 162 | |---------|------| |
115 | 163 | | Dashboard | 8080 (mapped in docker-compose.yml) | |
116 | 164 |
|
117 | | -## Resilience |
| 165 | +## Deployment State |
118 | 166 |
|
119 | | -- `run_dashboard()` catches exceptions internally |
120 | | -- Dashboard crash logs error and exits without calling `shutdown_event.set()` |
121 | | -- Agent continues operating via Telegram/stdin without dashboard |
122 | | -- `agent_state=None` default means loop works identically without dashboard |
| 167 | +- **Norisor currently runs**: v1 (commit 1c5c005) |
| 168 | +- **Local has**: v2 (commit 427d213) |
| 169 | +- **To deploy v2**: `git push origin main` then SSH pull+restart |
| 170 | +- **User instruction**: "don't deploy until I tell you" |
0 commit comments