Spaces:
Sleeping
Sleeping
Commit ·
cb55577
1
Parent(s): bce1d4b
Prepare OpenCortex hackathon submission
Browse files- README.md +350 -0
- app.py +2 -4
- docs/assets/.gitkeep +1 -0
- docs/superpowers/plans/2026-06-15-context-collapse-retry.md +40 -0
- open_cortex/runtime/client.py +101 -3
- open_cortex/runtime/events.py +8 -1
- open_cortex/ui/app.py +518 -102
- open_cortex/ui/assets/open_cortex.css +1410 -0
- open_cortex/ui/assets/open_cortex.js +738 -0
- pyproject.toml +2 -0
- requirements.txt +4 -0
- tests/runtime/test_client.py +45 -0
- tests/ui/test_app.py +317 -0
- uv.lock +4 -0
README.md
CHANGED
|
@@ -0,0 +1,350 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: OpenCortex
|
| 3 |
+
emoji: 🧠
|
| 4 |
+
colorFrom: cyan
|
| 5 |
+
colorTo: blue
|
| 6 |
+
sdk: gradio
|
| 7 |
+
sdk_version: 6.18.0
|
| 8 |
+
app_file: app.py
|
| 9 |
+
pinned: false
|
| 10 |
+
license: mit
|
| 11 |
+
tags:
|
| 12 |
+
- build-small-hackathon
|
| 13 |
+
- backyard-ai
|
| 14 |
+
- off-brand
|
| 15 |
+
- tiny-titan
|
| 16 |
+
- best-demo
|
| 17 |
+
- codex
|
| 18 |
+
- llama-cpp
|
| 19 |
+
- local-ai
|
| 20 |
+
- ai-infra
|
| 21 |
+
- observability
|
| 22 |
+
---
|
| 23 |
+
|
| 24 |
+
# OpenCortex
|
| 25 |
+
|
| 26 |
+
**Making LLM inference visible.**
|
| 27 |
+
|
| 28 |
+
OpenCortex is a real-time observatory for local LLM inference. It pairs a chat
|
| 29 |
+
interface with a living view of the runtime behind the answer: working memory,
|
| 30 |
+
context pressure, token flow, and engine health.
|
| 31 |
+
|
| 32 |
+
Most chat interfaces show only two things:
|
| 33 |
+
|
| 34 |
+
```text
|
| 35 |
+
user input -> assistant output
|
| 36 |
+
```
|
| 37 |
+
|
| 38 |
+
OpenCortex shows the machine in the middle.
|
| 39 |
+
|
| 40 |
+
```text
|
| 41 |
+
prompt prefill -> KV/cache pressure -> decode rhythm -> context boundary -> answer behavior
|
| 42 |
+
```
|
| 43 |
+
|
| 44 |
+
It is built for learners, local AI users, and AI infrastructure engineers who
|
| 45 |
+
want to understand why an LLM slows down, loops, forgets, or runs out of
|
| 46 |
+
context.
|
| 47 |
+
|
| 48 |
+

|
| 49 |
+
|
| 50 |
+
> Screenshot TODO: replace `docs/assets/opencortex-hero.png` with the main
|
| 51 |
+
> 16:9 demo screenshot before submission.
|
| 52 |
+
|
| 53 |
+
## Why this exists
|
| 54 |
+
|
| 55 |
+
Small local models make AI personal again. But local inference is still a black
|
| 56 |
+
box for most users. When a response becomes slow or repetitive, the user rarely
|
| 57 |
+
knows whether the model is thinking, overloaded, stuck in a loop, or simply
|
| 58 |
+
running out of context.
|
| 59 |
+
|
| 60 |
+
OpenCortex turns runtime signals into a product experience:
|
| 61 |
+
|
| 62 |
+
| Runtime signal | Human-readable concept | Visual feedback |
|
| 63 |
+
| --- | --- | --- |
|
| 64 |
+
| KV/cache usage proxy | Working Memory | block pressure and fragmentation |
|
| 65 |
+
| Context tokens | Context Window | filling chamber and active boundary |
|
| 66 |
+
| Decode throughput | Token Stream | flowing, broken, or stalled token river |
|
| 67 |
+
| TTFT and queue evidence | Engine State | pulse, charging, recovery, hazard state |
|
| 68 |
+
| Repeated generated text | Thought Loop | red loop warning and irregular core pulse |
|
| 69 |
+
|
| 70 |
+
The goal is not to build another metrics dashboard. The goal is to let people
|
| 71 |
+
feel the hidden mechanics of inference while they chat.
|
| 72 |
+
|
| 73 |
+
## Demo links
|
| 74 |
+
|
| 75 |
+
- **Hugging Face Space:** TODO: add Space URL
|
| 76 |
+
- **Demo video:** TODO: add YouTube / Space video URL
|
| 77 |
+
- **Social post:** TODO: add X / LinkedIn / HF post URL
|
| 78 |
+
|
| 79 |
+
The Build Small Hackathon requires a deployed Gradio Space, a demo video, a
|
| 80 |
+
social post, and README tags for tracks and badges. This README is structured
|
| 81 |
+
for that submission flow.
|
| 82 |
+
|
| 83 |
+
The hosted Space runs in **simulated runtime mode** so judges can open the demo
|
| 84 |
+
without a private llama.cpp server. The local path below connects to live
|
| 85 |
+
llama.cpp metrics.
|
| 86 |
+
|
| 87 |
+
## What you can try
|
| 88 |
+
|
| 89 |
+
OpenCortex has one live mode and four built-in runtime experiments.
|
| 90 |
+
|
| 91 |
+
### 1. Live local chat
|
| 92 |
+
|
| 93 |
+
Ask the local model a question and watch the observatory react while the answer
|
| 94 |
+
streams. The UI listens to llama.cpp OpenAI-compatible streaming events,
|
| 95 |
+
timings, `/metrics`, and `/slots`.
|
| 96 |
+
|
| 97 |
+
### 2. Long context stress
|
| 98 |
+
|
| 99 |
+
Shows how prompt growth increases prefill work before generation begins.
|
| 100 |
+
|
| 101 |
+
### 3. Memory pressure
|
| 102 |
+
|
| 103 |
+
Shows working memory blocks fragmenting and reallocating as the runtime becomes
|
| 104 |
+
strained.
|
| 105 |
+
|
| 106 |
+
### 4. Slow decode
|
| 107 |
+
|
| 108 |
+
Shows token flow breaking into a stop-burst-stop rhythm when generation slows.
|
| 109 |
+
|
| 110 |
+
### 5. Context collapse
|
| 111 |
+
|
| 112 |
+
Shows the moment earlier turns leave the active context window. The chat history
|
| 113 |
+
stays visible, but OpenCortex marks the active context boundary so the user can
|
| 114 |
+
see what the model can no longer reliably use.
|
| 115 |
+
|
| 116 |
+

|
| 117 |
+
|
| 118 |
+
> Screenshot TODO: capture the active context boundary and context window full
|
| 119 |
+
> state.
|
| 120 |
+
|
| 121 |
+
### 6. Thought loop detection
|
| 122 |
+
|
| 123 |
+
If generation begins repeating the same pattern, OpenCortex marks it as a
|
| 124 |
+
thought loop. The Token Stream and Cortex Core switch into a red hazard state.
|
| 125 |
+
|
| 126 |
+

|
| 127 |
+
|
| 128 |
+
> Screenshot TODO: capture a repeated story response with `THOUGHT LOOP
|
| 129 |
+
> DETECTED` visible.
|
| 130 |
+
|
| 131 |
+
## How it works
|
| 132 |
+
|
| 133 |
+
OpenCortex is intentionally lightweight:
|
| 134 |
+
|
| 135 |
+
```text
|
| 136 |
+
Browser UI
|
| 137 |
+
|
|
| 138 |
+
| POST /api/chat
|
| 139 |
+
v
|
| 140 |
+
FastAPI / Space app
|
| 141 |
+
|
|
| 142 |
+
| OpenAI-compatible streaming request
|
| 143 |
+
v
|
| 144 |
+
llama.cpp llama-server
|
| 145 |
+
|
|
| 146 |
+
| /v1/chat/completions
|
| 147 |
+
| /metrics
|
| 148 |
+
| /slots
|
| 149 |
+
v
|
| 150 |
+
Runtime events -> semantic state -> visual organs
|
| 151 |
+
```
|
| 152 |
+
|
| 153 |
+
The backend converts low-level runtime evidence into normalized events:
|
| 154 |
+
|
| 155 |
+
- `request_started`
|
| 156 |
+
- `first_token`
|
| 157 |
+
- `token`
|
| 158 |
+
- `context_collapse`
|
| 159 |
+
- `request_completed`
|
| 160 |
+
- `error`
|
| 161 |
+
|
| 162 |
+
The frontend consumes those events and updates the chat and observatory in the
|
| 163 |
+
same stream, so visual state and language output stay aligned.
|
| 164 |
+
|
| 165 |
+
## Runtime evidence
|
| 166 |
+
|
| 167 |
+
The MVP uses real llama.cpp evidence where available:
|
| 168 |
+
|
| 169 |
+
| Evidence | Source | Used for |
|
| 170 |
+
| --- | --- | --- |
|
| 171 |
+
| Time to first token | measured in Python client | Engine State |
|
| 172 |
+
| Prompt tokens | llama.cpp usage/timings | Context Window |
|
| 173 |
+
| Completion tokens | llama.cpp usage/timings | Token Stream |
|
| 174 |
+
| Prompt throughput | llama.cpp timings | Prefill evidence |
|
| 175 |
+
| Decode throughput | llama.cpp timings and metrics | Token Stream |
|
| 176 |
+
| Active slot context | llama.cpp `/slots` | Context and memory proxy |
|
| 177 |
+
| Processing/deferred requests | llama.cpp `/metrics` | Engine State |
|
| 178 |
+
| Repetition pattern | generated text heuristic | Thought loop hazard |
|
| 179 |
+
|
| 180 |
+
Working Memory is labeled as a **context-derived proxy** in this MVP. llama.cpp
|
| 181 |
+
does not expose a simple per-request KV cache percentage through the HTTP API we
|
| 182 |
+
use, so OpenCortex derives a truthful pressure estimate from active slot context
|
| 183 |
+
tokens and context size.
|
| 184 |
+
|
| 185 |
+
## Model
|
| 186 |
+
|
| 187 |
+
The current local demo uses:
|
| 188 |
+
|
| 189 |
+
```text
|
| 190 |
+
Qwen/Qwen2.5-1.5B-Instruct-GGUF
|
| 191 |
+
quantization: Q4_K_M
|
| 192 |
+
context: 2048 tokens in the local demo
|
| 193 |
+
runtime: llama.cpp llama-server
|
| 194 |
+
```
|
| 195 |
+
|
| 196 |
+
This keeps the submission inside the Build Small model limit and qualifies the
|
| 197 |
+
project for tiny-model storytelling. The interface is backend-neutral enough to
|
| 198 |
+
evolve toward Ollama, vLLM, or Modal-hosted llama.cpp later.
|
| 199 |
+
|
| 200 |
+
On Hugging Face Spaces, OpenCortex defaults to `OPEN_CORTEX_BACKEND=simulated`
|
| 201 |
+
when `SPACE_ID` is present. Set `OPEN_CORTEX_BACKEND=llama_cpp` only when the
|
| 202 |
+
Space can reach a running llama.cpp backend.
|
| 203 |
+
|
| 204 |
+
## Run locally
|
| 205 |
+
|
| 206 |
+
### 1. Install project dependencies
|
| 207 |
+
|
| 208 |
+
```bash
|
| 209 |
+
uv sync
|
| 210 |
+
```
|
| 211 |
+
|
| 212 |
+
### 2. Start llama.cpp
|
| 213 |
+
|
| 214 |
+
From your llama.cpp checkout:
|
| 215 |
+
|
| 216 |
+
```bash
|
| 217 |
+
llama-server \
|
| 218 |
+
-hf Qwen/Qwen2.5-1.5B-Instruct-GGUF:Q4_K_M \
|
| 219 |
+
-c 2048 -t 8 -tb 8 \
|
| 220 |
+
--metrics \
|
| 221 |
+
--host 127.0.0.1 --port 8080
|
| 222 |
+
```
|
| 223 |
+
|
| 224 |
+
Check that the server is alive:
|
| 225 |
+
|
| 226 |
+
```bash
|
| 227 |
+
curl --noproxy '*' http://127.0.0.1:8080/health
|
| 228 |
+
```
|
| 229 |
+
|
| 230 |
+
Expected:
|
| 231 |
+
|
| 232 |
+
```json
|
| 233 |
+
{"status":"ok"}
|
| 234 |
+
```
|
| 235 |
+
|
| 236 |
+
### 3. Start OpenCortex
|
| 237 |
+
|
| 238 |
+
```bash
|
| 239 |
+
uv run open-cortex
|
| 240 |
+
```
|
| 241 |
+
|
| 242 |
+
Then open:
|
| 243 |
+
|
| 244 |
+
```text
|
| 245 |
+
http://127.0.0.1:7860
|
| 246 |
+
```
|
| 247 |
+
|
| 248 |
+
## Suggested demo script
|
| 249 |
+
|
| 250 |
+
Use this for the video:
|
| 251 |
+
|
| 252 |
+
1. Open the app and send a normal question.
|
| 253 |
+
2. Point out prefill, first-token latency, context usage, and token flow.
|
| 254 |
+
3. Run **Slow decode** to show token flow breaking.
|
| 255 |
+
4. Ask for a long story and show **Thought Loop Detected** when repetition
|
| 256 |
+
appears.
|
| 257 |
+
5. Fill the context window and show **Context Window Full**.
|
| 258 |
+
6. Send one more message and show the earliest message leaving the active
|
| 259 |
+
context boundary.
|
| 260 |
+
7. Close with the core claim: OpenCortex turns runtime internals into something
|
| 261 |
+
users can see and reason about.
|
| 262 |
+
|
| 263 |
+
## Screenshots needed before final submission
|
| 264 |
+
|
| 265 |
+
Place these files under `docs/assets/`:
|
| 266 |
+
|
| 267 |
+
```text
|
| 268 |
+
docs/assets/opencortex-hero.png
|
| 269 |
+
docs/assets/opencortex-thought-loop.png
|
| 270 |
+
docs/assets/opencortex-context-collapse.png
|
| 271 |
+
docs/assets/opencortex-slow-decode.png
|
| 272 |
+
```
|
| 273 |
+
|
| 274 |
+
Capture recommendations:
|
| 275 |
+
|
| 276 |
+
- `opencortex-hero.png`: full UI during live decode, 16:9, both chat and
|
| 277 |
+
observatory visible.
|
| 278 |
+
- `opencortex-thought-loop.png`: repeated generation with `THOUGHT LOOP
|
| 279 |
+
DETECTED` visible.
|
| 280 |
+
- `opencortex-context-collapse.png`: active context boundary visible on the
|
| 281 |
+
left, context organ visible on the right.
|
| 282 |
+
- `opencortex-slow-decode.png`: Slow decode experiment with broken token flow.
|
| 283 |
+
|
| 284 |
+
## Hackathon fit
|
| 285 |
+
|
| 286 |
+
OpenCortex targets the **Backyard AI** track: it is a practical tool for
|
| 287 |
+
learning and debugging local AI behavior on hardware you own.
|
| 288 |
+
|
| 289 |
+
It also targets these badges:
|
| 290 |
+
|
| 291 |
+
- **Off Brand**: custom runtime cockpit UI, not default Gradio components.
|
| 292 |
+
- **Tiny Titan**: built around a 1.5B local model.
|
| 293 |
+
- **Best Demo**: the experience is designed around a clear visual story.
|
| 294 |
+
- **Best Use of Codex**: the project was developed with Codex assistance and
|
| 295 |
+
should be submitted with Codex-attributed commits in the connected repository.
|
| 296 |
+
|
| 297 |
+
Modal credits were used/planned for development exploration, but the current MVP
|
| 298 |
+
does not require Modal at runtime.
|
| 299 |
+
|
| 300 |
+
## Truthfulness and limitations
|
| 301 |
+
|
| 302 |
+
OpenCortex is a visualization layer over real runtime evidence, but it does not
|
| 303 |
+
claim to expose every internal tensor or true biological cognition.
|
| 304 |
+
|
| 305 |
+
Current limitations:
|
| 306 |
+
|
| 307 |
+
- KV cache usage is approximated from active context pressure.
|
| 308 |
+
- Thought loop detection is heuristic pattern detection over generated text.
|
| 309 |
+
- Context collapse is handled at the application layer by trimming oldest turns.
|
| 310 |
+
- The current demo is optimized for a single local runtime, not multi-user
|
| 311 |
+
production serving.
|
| 312 |
+
- vLLM integration is a planned evolution, not part of v0.1.
|
| 313 |
+
|
| 314 |
+
These constraints are visible by design. The product shows both semantic states
|
| 315 |
+
and metric evidence so users can tell which parts are measured and which parts
|
| 316 |
+
are interpreted.
|
| 317 |
+
|
| 318 |
+
## Project structure
|
| 319 |
+
|
| 320 |
+
```text
|
| 321 |
+
app.py Space/local entry point
|
| 322 |
+
open_cortex/ui/app.py HTTP app, event streaming, HTML shell
|
| 323 |
+
open_cortex/ui/assets/ Product UI CSS and browser controller
|
| 324 |
+
open_cortex/runtime/client.py llama.cpp streaming client
|
| 325 |
+
open_cortex/runtime/metrics.py llama.cpp metrics and slots parser
|
| 326 |
+
open_cortex/runtime/events.py Runtime event model
|
| 327 |
+
tests/ Runtime and UI regression tests
|
| 328 |
+
```
|
| 329 |
+
|
| 330 |
+
## Development checks
|
| 331 |
+
|
| 332 |
+
```bash
|
| 333 |
+
uv run pytest -q
|
| 334 |
+
mise exec -- node --check open_cortex/ui/assets/open_cortex.js
|
| 335 |
+
```
|
| 336 |
+
|
| 337 |
+
## Submission checklist
|
| 338 |
+
|
| 339 |
+
- [ ] Deploy Space inside `build-small-hackathon/`.
|
| 340 |
+
- [ ] Confirm the Space launches as a Gradio-compatible submission.
|
| 341 |
+
- [ ] Add final Space URL to this README.
|
| 342 |
+
- [ ] Add demo video URL to this README.
|
| 343 |
+
- [ ] Add social post URL to this README.
|
| 344 |
+
- [ ] Replace screenshot placeholders in `docs/assets/`.
|
| 345 |
+
- [ ] Run the Build Small README validator.
|
| 346 |
+
- [ ] Confirm the model is under 32B parameters.
|
| 347 |
+
|
| 348 |
+
## License
|
| 349 |
+
|
| 350 |
+
MIT
|
app.py
CHANGED
|
@@ -1,7 +1,5 @@
|
|
| 1 |
-
from open_cortex.ui.app import
|
| 2 |
|
| 3 |
-
demo = build_app()
|
| 4 |
|
| 5 |
if __name__ == "__main__":
|
| 6 |
-
|
| 7 |
-
demo.launch()
|
|
|
|
| 1 |
+
from open_cortex.ui.app import app, main
|
| 2 |
|
|
|
|
| 3 |
|
| 4 |
if __name__ == "__main__":
|
| 5 |
+
main()
|
|
|
docs/assets/.gitkeep
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
|
docs/superpowers/plans/2026-06-15-context-collapse-retry.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Context Collapse Retry Implementation Plan
|
| 2 |
+
|
| 3 |
+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
| 4 |
+
|
| 5 |
+
**Goal:** Turn llama.cpp context overflow into a visible Context Collapse event, then trim oldest chat history and retry once.
|
| 6 |
+
|
| 7 |
+
**Architecture:** Keep llama.cpp client behavior simple: it raises the HTTP 400 from llama.cpp. The UI service layer catches the overflow, emits a `context_collapse` NDJSON event, trims oldest non-system messages, and retries once. The browser renders the collapse boundary and continues streaming the retried response.
|
| 8 |
+
|
| 9 |
+
**Tech Stack:** Python FastAPI streaming NDJSON, httpx, vanilla JS, CSS.
|
| 10 |
+
|
| 11 |
+
---
|
| 12 |
+
|
| 13 |
+
### Task 1: Backend Collapse Event And Retry
|
| 14 |
+
|
| 15 |
+
**Files:**
|
| 16 |
+
- Modify: `open_cortex/ui/app.py`
|
| 17 |
+
- Test: `tests/ui/test_app.py`
|
| 18 |
+
|
| 19 |
+
- [ ] Write a failing test that monkeypatches `stream_chat_events` to raise an HTTP 400 overflow on the first call and stream a normal response on the second call.
|
| 20 |
+
- [ ] Implement `_is_context_overflow`, `_trim_for_context_collapse`, and retry-once logic in `_stream_events`.
|
| 21 |
+
- [ ] Verify the stream contains `request_started`, `context_collapse`, then retried response events.
|
| 22 |
+
|
| 23 |
+
### Task 2: Frontend Collapse Boundary
|
| 24 |
+
|
| 25 |
+
**Files:**
|
| 26 |
+
- Modify: `open_cortex/ui/assets/open_cortex.js`
|
| 27 |
+
- Modify: `open_cortex/ui/assets/open_cortex.css`
|
| 28 |
+
|
| 29 |
+
- [ ] Add `applyContextCollapse(event)` to mark old messages as `historical-context`, insert `active-context-boundary`, and switch the right panel to collapse visuals.
|
| 30 |
+
- [ ] Ensure the next retried stream reuses the same assistant bubble.
|
| 31 |
+
- [ ] Run `mise exec -- node --check open_cortex/ui/assets/open_cortex.js`.
|
| 32 |
+
|
| 33 |
+
### Task 3: Verification
|
| 34 |
+
|
| 35 |
+
**Files:**
|
| 36 |
+
- Test: all tests
|
| 37 |
+
|
| 38 |
+
- [ ] Run `uv run pytest -q`.
|
| 39 |
+
- [ ] Run `uv run python -m py_compile open_cortex/ui/app.py open_cortex/runtime/client.py`.
|
| 40 |
+
- [ ] Restart `uv run python app.py` and manually trigger a long-context continuation.
|
open_cortex/runtime/client.py
CHANGED
|
@@ -13,6 +13,39 @@ METRICS_URL = "http://127.0.0.1:8080/metrics"
|
|
| 13 |
SLOTS_URL = "http://127.0.0.1:8080/slots"
|
| 14 |
|
| 15 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
def stream_chat_events(message: list[ChatMessage]) -> Iterator[RuntimeEvent]:
|
| 17 |
request_body = {
|
| 18 |
"messages": to_llama_messages(message),
|
|
@@ -25,7 +58,12 @@ def stream_chat_events(message: list[ChatMessage]) -> Iterator[RuntimeEvent]:
|
|
| 25 |
|
| 26 |
request_started = time.perf_counter()
|
| 27 |
first_token_seen = False
|
|
|
|
|
|
|
|
|
|
| 28 |
final_stats = None
|
|
|
|
|
|
|
| 29 |
|
| 30 |
yield RuntimeEvent(
|
| 31 |
kind="request_started",
|
|
@@ -36,7 +74,7 @@ def stream_chat_events(message: list[ChatMessage]) -> Iterator[RuntimeEvent]:
|
|
| 36 |
|
| 37 |
with httpx.Client(timeout=120.0, trust_env=False) as client:
|
| 38 |
with client.stream("POST", CHAT_URL, json=request_body) as response:
|
| 39 |
-
|
| 40 |
|
| 41 |
for line in response.iter_lines():
|
| 42 |
if not line.startswith("data: "):
|
|
@@ -54,27 +92,78 @@ def stream_chat_events(message: list[ChatMessage]) -> Iterator[RuntimeEvent]:
|
|
| 54 |
content = choices[0].get("delta", {}).get("content")
|
| 55 |
|
| 56 |
if content:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
if not first_token_seen:
|
| 58 |
first_token_seen = True
|
| 59 |
-
first_token_at =
|
| 60 |
ttft_ms = (first_token_at - request_started) * 1000
|
| 61 |
snapshot = fetch_runtime_snapshot(
|
| 62 |
client,
|
| 63 |
METRICS_URL,
|
| 64 |
SLOTS_URL,
|
| 65 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
yield RuntimeEvent(
|
| 67 |
kind="first_token",
|
| 68 |
text_delta=content,
|
| 69 |
ttft_ms=ttft_ms,
|
| 70 |
snapshot=snapshot,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
)
|
| 72 |
else:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
yield RuntimeEvent(
|
| 74 |
kind="token",
|
| 75 |
text_delta=content,
|
| 76 |
ttft_ms=None,
|
| 77 |
snapshot=None,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
)
|
| 79 |
|
| 80 |
if event.get("usage"):
|
|
@@ -83,6 +172,7 @@ def stream_chat_events(message: list[ChatMessage]) -> Iterator[RuntimeEvent]:
|
|
| 83 |
if final_stats is not None:
|
| 84 |
usage = final_stats["usage"]
|
| 85 |
timings = final_stats["timings"]
|
|
|
|
| 86 |
|
| 87 |
yield RuntimeEvent(
|
| 88 |
kind="request_completed",
|
|
@@ -93,4 +183,12 @@ def stream_chat_events(message: list[ChatMessage]) -> Iterator[RuntimeEvent]:
|
|
| 93 |
completion_tokens=usage["completion_tokens"],
|
| 94 |
prompt_tps=timings["prompt_per_second"],
|
| 95 |
decode_tps=timings["predicted_per_second"],
|
| 96 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
SLOTS_URL = "http://127.0.0.1:8080/slots"
|
| 14 |
|
| 15 |
|
| 16 |
+
def _raise_for_status_with_body(response: httpx.Response) -> None:
|
| 17 |
+
if response.is_error:
|
| 18 |
+
try:
|
| 19 |
+
response.read()
|
| 20 |
+
except httpx.HTTPError:
|
| 21 |
+
pass
|
| 22 |
+
response.raise_for_status()
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def _working_memory_percent(
|
| 26 |
+
context_tokens: int | None,
|
| 27 |
+
context_size: int | None,
|
| 28 |
+
) -> float | None:
|
| 29 |
+
if context_tokens is None or not context_size:
|
| 30 |
+
return None
|
| 31 |
+
return round(min(100.0, context_tokens / context_size * 100), 1)
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def _detect_repetition(text: str) -> bool:
|
| 35 |
+
normalized = " ".join(text.split())
|
| 36 |
+
if len(normalized) < 120:
|
| 37 |
+
return False
|
| 38 |
+
|
| 39 |
+
for window in (96, 72, 48, 32):
|
| 40 |
+
if len(normalized) <= window * 2:
|
| 41 |
+
continue
|
| 42 |
+
tail = normalized[-window:]
|
| 43 |
+
if normalized[:-window].count(tail) >= 1:
|
| 44 |
+
return True
|
| 45 |
+
|
| 46 |
+
return False
|
| 47 |
+
|
| 48 |
+
|
| 49 |
def stream_chat_events(message: list[ChatMessage]) -> Iterator[RuntimeEvent]:
|
| 50 |
request_body = {
|
| 51 |
"messages": to_llama_messages(message),
|
|
|
|
| 58 |
|
| 59 |
request_started = time.perf_counter()
|
| 60 |
first_token_seen = False
|
| 61 |
+
first_token_at = None
|
| 62 |
+
generated_tokens = 0
|
| 63 |
+
generated_text = ""
|
| 64 |
final_stats = None
|
| 65 |
+
base_context_tokens = None
|
| 66 |
+
context_size = None
|
| 67 |
|
| 68 |
yield RuntimeEvent(
|
| 69 |
kind="request_started",
|
|
|
|
| 74 |
|
| 75 |
with httpx.Client(timeout=120.0, trust_env=False) as client:
|
| 76 |
with client.stream("POST", CHAT_URL, json=request_body) as response:
|
| 77 |
+
_raise_for_status_with_body(response)
|
| 78 |
|
| 79 |
for line in response.iter_lines():
|
| 80 |
if not line.startswith("data: "):
|
|
|
|
| 92 |
content = choices[0].get("delta", {}).get("content")
|
| 93 |
|
| 94 |
if content:
|
| 95 |
+
now = time.perf_counter()
|
| 96 |
+
generated_tokens += 1
|
| 97 |
+
generated_text += content
|
| 98 |
+
elapsed_ms = (
|
| 99 |
+
(now - first_token_at) * 1000
|
| 100 |
+
if first_token_at is not None
|
| 101 |
+
else 0.0
|
| 102 |
+
)
|
| 103 |
+
live_tps = (
|
| 104 |
+
generated_tokens / (elapsed_ms / 1000)
|
| 105 |
+
if elapsed_ms > 0
|
| 106 |
+
else None
|
| 107 |
+
)
|
| 108 |
+
repetition_detected = _detect_repetition(generated_text)
|
| 109 |
+
|
| 110 |
if not first_token_seen:
|
| 111 |
first_token_seen = True
|
| 112 |
+
first_token_at = now
|
| 113 |
ttft_ms = (first_token_at - request_started) * 1000
|
| 114 |
snapshot = fetch_runtime_snapshot(
|
| 115 |
client,
|
| 116 |
METRICS_URL,
|
| 117 |
SLOTS_URL,
|
| 118 |
)
|
| 119 |
+
base_context_tokens = (
|
| 120 |
+
snapshot.slot_context_tokens[0]
|
| 121 |
+
if snapshot.slot_context_tokens
|
| 122 |
+
else None
|
| 123 |
+
)
|
| 124 |
+
context_size = snapshot.slot_context_size
|
| 125 |
+
context_tokens = (
|
| 126 |
+
base_context_tokens + generated_tokens
|
| 127 |
+
if base_context_tokens is not None
|
| 128 |
+
else None
|
| 129 |
+
)
|
| 130 |
yield RuntimeEvent(
|
| 131 |
kind="first_token",
|
| 132 |
text_delta=content,
|
| 133 |
ttft_ms=ttft_ms,
|
| 134 |
snapshot=snapshot,
|
| 135 |
+
generated_tokens=generated_tokens,
|
| 136 |
+
elapsed_ms=0.0,
|
| 137 |
+
live_tps=None,
|
| 138 |
+
repetition_detected=repetition_detected,
|
| 139 |
+
context_tokens=context_tokens,
|
| 140 |
+
context_size=context_size,
|
| 141 |
+
working_memory_percent=_working_memory_percent(
|
| 142 |
+
context_tokens,
|
| 143 |
+
context_size,
|
| 144 |
+
),
|
| 145 |
)
|
| 146 |
else:
|
| 147 |
+
context_tokens = (
|
| 148 |
+
base_context_tokens + generated_tokens
|
| 149 |
+
if base_context_tokens is not None
|
| 150 |
+
else None
|
| 151 |
+
)
|
| 152 |
yield RuntimeEvent(
|
| 153 |
kind="token",
|
| 154 |
text_delta=content,
|
| 155 |
ttft_ms=None,
|
| 156 |
snapshot=None,
|
| 157 |
+
generated_tokens=generated_tokens,
|
| 158 |
+
elapsed_ms=elapsed_ms,
|
| 159 |
+
live_tps=live_tps,
|
| 160 |
+
repetition_detected=repetition_detected,
|
| 161 |
+
context_tokens=context_tokens,
|
| 162 |
+
context_size=context_size,
|
| 163 |
+
working_memory_percent=_working_memory_percent(
|
| 164 |
+
context_tokens,
|
| 165 |
+
context_size,
|
| 166 |
+
),
|
| 167 |
)
|
| 168 |
|
| 169 |
if event.get("usage"):
|
|
|
|
| 172 |
if final_stats is not None:
|
| 173 |
usage = final_stats["usage"]
|
| 174 |
timings = final_stats["timings"]
|
| 175 |
+
context_tokens = usage["prompt_tokens"] + usage["completion_tokens"]
|
| 176 |
|
| 177 |
yield RuntimeEvent(
|
| 178 |
kind="request_completed",
|
|
|
|
| 183 |
completion_tokens=usage["completion_tokens"],
|
| 184 |
prompt_tps=timings["prompt_per_second"],
|
| 185 |
decode_tps=timings["predicted_per_second"],
|
| 186 |
+
generated_tokens=usage["completion_tokens"],
|
| 187 |
+
repetition_detected=_detect_repetition(generated_text),
|
| 188 |
+
context_tokens=context_tokens,
|
| 189 |
+
context_size=context_size,
|
| 190 |
+
working_memory_percent=_working_memory_percent(
|
| 191 |
+
context_tokens,
|
| 192 |
+
context_size,
|
| 193 |
+
),
|
| 194 |
+
)
|
open_cortex/runtime/events.py
CHANGED
|
@@ -18,7 +18,14 @@ class RuntimeEvent:
|
|
| 18 |
text_delta: str
|
| 19 |
ttft_ms: float | None
|
| 20 |
snapshot: RuntimeSnapshot | None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
prompt_tokens: int | None = None
|
| 22 |
completion_tokens: int | None = None
|
| 23 |
prompt_tps: float | None = None
|
| 24 |
-
decode_tps: float | None = None
|
|
|
|
| 18 |
text_delta: str
|
| 19 |
ttft_ms: float | None
|
| 20 |
snapshot: RuntimeSnapshot | None
|
| 21 |
+
generated_tokens: int = 0
|
| 22 |
+
elapsed_ms: float | None = None
|
| 23 |
+
live_tps: float | None = None
|
| 24 |
+
repetition_detected: bool = False
|
| 25 |
+
context_tokens: int | None = None
|
| 26 |
+
context_size: int | None = None
|
| 27 |
+
working_memory_percent: float | None = None
|
| 28 |
prompt_tokens: int | None = None
|
| 29 |
completion_tokens: int | None = None
|
| 30 |
prompt_tps: float | None = None
|
| 31 |
+
decode_tps: float | None = None
|
open_cortex/ui/app.py
CHANGED
|
@@ -1,138 +1,554 @@
|
|
| 1 |
-
|
| 2 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
from open_cortex.runtime.client import stream_chat_events
|
|
|
|
|
|
|
|
|
|
| 4 |
|
| 5 |
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
-
if event.kind == "first_token" and event.snapshot is not None:
|
| 11 |
-
snapshot = event.snapshot
|
| 12 |
-
context_tokens = (
|
| 13 |
-
snapshot.slot_context_tokens[0]
|
| 14 |
-
if snapshot.slot_context_tokens
|
| 15 |
-
else None
|
| 16 |
-
)
|
| 17 |
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
"Engine: "
|
| 26 |
-
f"processing={snapshot.requests_processing} "
|
| 27 |
-
f"deferred={snapshot.requests_deferred}"
|
| 28 |
-
),
|
| 29 |
-
]
|
| 30 |
-
)
|
| 31 |
|
| 32 |
-
if event.kind == "request_completed":
|
| 33 |
-
return "\n".join(
|
| 34 |
-
[
|
| 35 |
-
"Phase: completed",
|
| 36 |
-
f"Prompt tokens: {event.prompt_tokens}",
|
| 37 |
-
f"Output tokens: {event.completion_tokens}",
|
| 38 |
-
f"Prefill: {event.prompt_tps:.1f} tok/s",
|
| 39 |
-
f"Decode: {event.decode_tps:.1f} tok/s",
|
| 40 |
-
]
|
| 41 |
-
)
|
| 42 |
|
| 43 |
-
|
|
|
|
| 44 |
|
| 45 |
|
|
|
|
|
|
|
| 46 |
|
| 47 |
-
def user(user_message: str, history: list[dict]) -> tuple[str, list[dict]]:
|
| 48 |
-
if not user_message.strip():
|
| 49 |
-
return "", history
|
| 50 |
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
]
|
| 57 |
|
| 58 |
-
def bot(history: list):
|
| 59 |
-
messages = history_to_chat_messages(history)
|
| 60 |
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
)
|
| 67 |
|
| 68 |
-
runtime_text = "Phase: idle"
|
| 69 |
|
| 70 |
-
|
| 71 |
-
|
|
|
|
|
|
|
|
|
|
| 72 |
|
| 73 |
-
if event.text_delta:
|
| 74 |
-
history[-1]["content"] += event.text_delta
|
| 75 |
|
| 76 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
|
|
|
|
|
|
|
| 78 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
|
| 80 |
-
def build_app() -> gr.Blocks:
|
| 81 |
-
with gr.Blocks() as demo:
|
| 82 |
-
gr.Markdown("# OpenCortex Minimal Chat")
|
| 83 |
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
height=480,
|
| 88 |
-
)
|
| 89 |
-
msg = gr.Textbox(
|
| 90 |
-
placeholder="Ask the local model...",
|
| 91 |
-
show_label=False,
|
| 92 |
-
)
|
| 93 |
-
clear = gr.Button("Clear")
|
| 94 |
-
|
| 95 |
-
with gr.Column(scale=1):
|
| 96 |
-
runtime = gr.Textbox(
|
| 97 |
-
label="Runtime",
|
| 98 |
-
value="Phase: idle",
|
| 99 |
-
lines=10,
|
| 100 |
-
interactive=False,
|
| 101 |
-
)
|
| 102 |
|
| 103 |
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
chatbot,
|
| 112 |
-
[chatbot, runtime],
|
| 113 |
-
)
|
| 114 |
|
| 115 |
-
|
| 116 |
-
lambda: ([], "Phase: idle"),
|
| 117 |
-
None,
|
| 118 |
-
[chatbot, runtime],
|
| 119 |
-
queue=False,
|
| 120 |
-
)
|
| 121 |
-
return demo
|
| 122 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
|
| 130 |
|
| 131 |
-
|
| 132 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 133 |
|
|
|
|
| 134 |
|
| 135 |
|
|
|
|
| 136 |
|
| 137 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
import os
|
| 5 |
+
import re
|
| 6 |
+
import time
|
| 7 |
+
from collections.abc import Iterator
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
from typing import Any
|
| 10 |
+
|
| 11 |
+
import httpx
|
| 12 |
+
import uvicorn
|
| 13 |
+
from fastapi import FastAPI, Request
|
| 14 |
+
from fastapi.responses import HTMLResponse, StreamingResponse
|
| 15 |
+
from fastapi.staticfiles import StaticFiles
|
| 16 |
+
|
| 17 |
from open_cortex.runtime.client import stream_chat_events
|
| 18 |
+
from open_cortex.runtime.events import RuntimeEvent
|
| 19 |
+
from open_cortex.runtime.messages import ChatMessage
|
| 20 |
+
from open_cortex.runtime.metrics import RuntimeSnapshot
|
| 21 |
|
| 22 |
|
| 23 |
+
ASSET_DIR = Path(__file__).with_name("assets")
|
| 24 |
+
CSS_FILE = ASSET_DIR / "open_cortex.css"
|
| 25 |
+
JS_FILE = ASSET_DIR / "open_cortex.js"
|
| 26 |
+
MAX_COLLAPSE_RETAINED_CHARS = 3000
|
| 27 |
+
COLLAPSED_MESSAGE_CHARS = 1200
|
| 28 |
+
DEFAULT_CONTEXT_SIZE = 2048
|
| 29 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
|
| 31 |
+
def _backend_mode() -> str:
|
| 32 |
+
explicit = os.getenv("OPEN_CORTEX_BACKEND")
|
| 33 |
+
if explicit:
|
| 34 |
+
return explicit
|
| 35 |
+
if os.getenv("SPACE_ID"):
|
| 36 |
+
return "simulated"
|
| 37 |
+
return "llama_cpp"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
|
| 40 |
+
def _simulator_delay_seconds() -> float:
|
| 41 |
+
return float(os.getenv("OPEN_CORTEX_SIMULATOR_DELAY_SECONDS", "0.025"))
|
| 42 |
|
| 43 |
|
| 44 |
+
def _asset_version() -> int:
|
| 45 |
+
return int(max(CSS_FILE.stat().st_mtime, JS_FILE.stat().st_mtime))
|
| 46 |
|
|
|
|
|
|
|
|
|
|
| 47 |
|
| 48 |
+
def _memory_blocks() -> str:
|
| 49 |
+
return "\n".join(
|
| 50 |
+
'<i class="filled"></i>' if index < 1 else "<i></i>"
|
| 51 |
+
for index in range(12)
|
| 52 |
+
)
|
|
|
|
| 53 |
|
|
|
|
|
|
|
| 54 |
|
| 55 |
+
def _token_particles() -> str:
|
| 56 |
+
return "\n".join('<i class="particle"></i>' for _ in range(9))
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def _latency_trace() -> str:
|
| 60 |
+
return "\n".join("<i></i>" for _ in range(8))
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def render_index() -> str:
|
| 64 |
+
asset_version = _asset_version()
|
| 65 |
+
return f"""<!doctype html>
|
| 66 |
+
<html lang="en">
|
| 67 |
+
<head>
|
| 68 |
+
<meta charset="UTF-8">
|
| 69 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 70 |
+
<title>OpenCortex Runtime Observatory</title>
|
| 71 |
+
<link rel="stylesheet" href="/assets/open_cortex.css?v={asset_version}">
|
| 72 |
+
</head>
|
| 73 |
+
<body>
|
| 74 |
+
<main class="app phase-idle" id="app">
|
| 75 |
+
<header class="topbar">
|
| 76 |
+
<div class="brand">
|
| 77 |
+
<div class="mark"></div>
|
| 78 |
+
<div class="brand-name">OpenCortex</div>
|
| 79 |
+
</div>
|
| 80 |
+
<div class="top-actions">
|
| 81 |
+
<div class="runtime-connection"><span class="live-dot"></span><span>llama.cpp · local</span></div>
|
| 82 |
+
</div>
|
| 83 |
+
</header>
|
| 84 |
+
|
| 85 |
+
<div class="workspace">
|
| 86 |
+
<section class="surface conversation">
|
| 87 |
+
<div class="section-head">
|
| 88 |
+
<div class="panel-title">Conversation</div>
|
| 89 |
+
<div class="scenario" id="scenario-label">Live local chat</div>
|
| 90 |
+
</div>
|
| 91 |
+
|
| 92 |
+
<div class="messages" id="messages"></div>
|
| 93 |
+
|
| 94 |
+
<div class="composer-wrap">
|
| 95 |
+
<div class="composer">
|
| 96 |
+
<textarea id="prompt" placeholder="Ask the local model, then watch inference state change..."></textarea>
|
| 97 |
+
<div class="composer-actions">
|
| 98 |
+
<span class="hint">↵ SEND · SHIFT + ↵ NEW LINE</span>
|
| 99 |
+
<button class="send" id="send" type="button"><span>Send</span><span>↗</span></button>
|
| 100 |
+
</div>
|
| 101 |
+
</div>
|
| 102 |
+
</div>
|
| 103 |
+
</section>
|
| 104 |
+
|
| 105 |
+
<div class="resizer" id="resizer" role="separator" aria-label="Resize conversation panel" aria-orientation="vertical">
|
| 106 |
+
<span class="resize-grip"></span>
|
| 107 |
+
<button class="drawer-toggle" id="drawer-toggle" type="button" aria-label="Show conversation">›</button>
|
| 108 |
+
</div>
|
| 109 |
+
|
| 110 |
+
<section class="surface observatory">
|
| 111 |
+
<div class="observatory-head">
|
| 112 |
+
<div class="panel-title">Runtime Observatory</div>
|
| 113 |
+
<div class="model-state">
|
| 114 |
+
<div class="model-name">Qwen2.5 1.5B · Q4_K_M</div>
|
| 115 |
+
<div class="phase-pill" id="phase">IDLE</div>
|
| 116 |
+
<div class="controls">
|
| 117 |
+
<select id="experiment" aria-label="Runtime experiment">
|
| 118 |
+
<option value="live">Live detected</option>
|
| 119 |
+
<option value="long-context">Sim · Long context stress</option>
|
| 120 |
+
<option value="memory-pressure">Sim · Memory pressure</option>
|
| 121 |
+
<option value="slow-decode">Sim · Slow decode</option>
|
| 122 |
+
<option value="context-collapse">Sim · Context collapse</option>
|
| 123 |
+
</select>
|
| 124 |
+
<button class="run-experiment" id="run-experiment" type="button">Run experiment</button>
|
| 125 |
+
</div>
|
| 126 |
+
</div>
|
| 127 |
+
</div>
|
| 128 |
+
|
| 129 |
+
<div class="observatory-stage">
|
| 130 |
+
<div class="runtime-event" id="runtime-event">READY</div>
|
| 131 |
+
<span class="conduit c1"></span>
|
| 132 |
+
<span class="conduit c2"></span>
|
| 133 |
+
<span class="conduit c3"></span>
|
| 134 |
+
<span class="conduit c4"></span>
|
| 135 |
+
|
| 136 |
+
<article class="organ memory active" id="memory-organ">
|
| 137 |
+
<div class="organ-head">
|
| 138 |
+
<div class="organ-name">Working Memory</div>
|
| 139 |
+
<span class="organ-state" id="memory-state">Memory quiet</span>
|
| 140 |
+
</div>
|
| 141 |
+
<div class="metric-row">
|
| 142 |
+
<div><span class="metric-value" id="kv">—</span><span class="metric-unit">%</span></div>
|
| 143 |
+
</div>
|
| 144 |
+
<div class="memory-blocks" id="memory-blocks">
|
| 145 |
+
{_memory_blocks()}
|
| 146 |
+
</div>
|
| 147 |
+
</article>
|
| 148 |
+
|
| 149 |
+
<article class="organ context active" id="context-organ">
|
| 150 |
+
<div class="organ-head">
|
| 151 |
+
<div class="organ-name">Context Window</div>
|
| 152 |
+
<span class="organ-state" id="context-state">Context resting</span>
|
| 153 |
+
</div>
|
| 154 |
+
<div class="metric-row">
|
| 155 |
+
<div><span class="metric-value" id="context-used">0</span><span class="metric-unit" id="context-unit">/ —</span></div>
|
| 156 |
+
</div>
|
| 157 |
+
<div class="context-vessel">
|
| 158 |
+
<div class="context-fill" id="context-fill"></div>
|
| 159 |
+
<div class="memory-tape" aria-hidden="true">
|
| 160 |
+
<i class="memory-segment old"></i><i class="memory-segment old"></i>
|
| 161 |
+
<i class="memory-segment"></i><i class="memory-segment"></i>
|
| 162 |
+
<i class="memory-segment"></i><i class="memory-segment"></i>
|
| 163 |
+
<i class="memory-segment recent"></i><i class="memory-segment recent"></i>
|
| 164 |
+
</div>
|
| 165 |
+
<div class="context-ticks"></div>
|
| 166 |
+
<span class="tape-direction">→</span>
|
| 167 |
+
</div>
|
| 168 |
+
</article>
|
| 169 |
+
|
| 170 |
+
<article class="organ tokens active" id="tokens-organ">
|
| 171 |
+
<div class="organ-head">
|
| 172 |
+
<div class="organ-name">Token Stream</div>
|
| 173 |
+
<span class="organ-state" id="token-state">Stream dormant</span>
|
| 174 |
+
</div>
|
| 175 |
+
<div class="metric-row">
|
| 176 |
+
<div><span class="metric-value" id="tps">0.0</span><span class="metric-unit">tok/s</span></div>
|
| 177 |
+
</div>
|
| 178 |
+
<div class="token-river">
|
| 179 |
+
{_token_particles()}
|
| 180 |
+
</div>
|
| 181 |
+
</article>
|
| 182 |
+
|
| 183 |
+
<article class="organ health active" id="engine-organ">
|
| 184 |
+
<div class="organ-head">
|
| 185 |
+
<div class="organ-name">Engine State</div>
|
| 186 |
+
<span class="organ-state" id="health-state">Engine healthy</span>
|
| 187 |
+
</div>
|
| 188 |
+
<div class="metric-row">
|
| 189 |
+
<div><span class="metric-value" id="ttft">—</span><span class="metric-unit">ms</span></div>
|
| 190 |
+
</div>
|
| 191 |
+
<div class="latency-trace">{_latency_trace()}</div>
|
| 192 |
+
</article>
|
| 193 |
+
|
| 194 |
+
<div class="core-wrap">
|
| 195 |
+
<div class="core-rings"></div>
|
| 196 |
+
<div class="core"></div>
|
| 197 |
+
</div>
|
| 198 |
+
<div class="core-copy">
|
| 199 |
+
<div class="core-label">Cortex Core</div>
|
| 200 |
+
<div class="core-state" id="core-state">Ready for input</div>
|
| 201 |
+
<div class="core-detail" id="core-detail">ENGINE IDLE · SLOT AVAILABLE</div>
|
| 202 |
+
</div>
|
| 203 |
+
</div>
|
| 204 |
+
|
| 205 |
+
<div class="evidence">
|
| 206 |
+
<div class="evidence-cell">
|
| 207 |
+
<div class="evidence-head"><span class="measured-dot"></span><span>Measured live by llama.cpp</span></div>
|
| 208 |
+
</div>
|
| 209 |
+
<div class="evidence-cell">
|
| 210 |
+
<div class="evidence-label">Prompt eval</div>
|
| 211 |
+
<div class="evidence-value" id="prompt-eval">measuring</div>
|
| 212 |
+
</div>
|
| 213 |
+
<div class="evidence-cell">
|
| 214 |
+
<div class="evidence-label" id="evidence-3-label">KV evidence</div>
|
| 215 |
+
<div class="evidence-value" id="kv-retained">unavailable</div>
|
| 216 |
+
</div>
|
| 217 |
+
</div>
|
| 218 |
+
</section>
|
| 219 |
+
</div>
|
| 220 |
+
</main>
|
| 221 |
+
<script type="module" src="/assets/open_cortex.js?v={asset_version}"></script>
|
| 222 |
+
</body>
|
| 223 |
+
</html>
|
| 224 |
+
"""
|
| 225 |
+
|
| 226 |
+
|
| 227 |
+
def _snapshot_to_payload(snapshot: RuntimeSnapshot | None) -> dict[str, Any] | None:
|
| 228 |
+
if snapshot is None:
|
| 229 |
+
return None
|
| 230 |
+
return {
|
| 231 |
+
"prompt_tps": snapshot.prompt_tps,
|
| 232 |
+
"decode_tps": snapshot.decode_tps,
|
| 233 |
+
"requests_processing": snapshot.requests_processing,
|
| 234 |
+
"requests_deferred": snapshot.requests_deferred,
|
| 235 |
+
"active_slots": snapshot.active_slots,
|
| 236 |
+
"slot_context_tokens": list(snapshot.slot_context_tokens),
|
| 237 |
+
"slot_context_size": snapshot.slot_context_size,
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
|
| 241 |
+
def event_to_payload(event: RuntimeEvent) -> dict[str, Any]:
|
| 242 |
+
return {
|
| 243 |
+
"kind": event.kind,
|
| 244 |
+
"text_delta": event.text_delta,
|
| 245 |
+
"ttft_ms": event.ttft_ms,
|
| 246 |
+
"snapshot": _snapshot_to_payload(event.snapshot),
|
| 247 |
+
"generated_tokens": event.generated_tokens,
|
| 248 |
+
"elapsed_ms": event.elapsed_ms,
|
| 249 |
+
"live_tps": event.live_tps,
|
| 250 |
+
"repetition_detected": event.repetition_detected,
|
| 251 |
+
"context_tokens": event.context_tokens,
|
| 252 |
+
"context_size": event.context_size,
|
| 253 |
+
"working_memory_percent": event.working_memory_percent,
|
| 254 |
+
"prompt_tokens": event.prompt_tokens,
|
| 255 |
+
"completion_tokens": event.completion_tokens,
|
| 256 |
+
"prompt_tps": event.prompt_tps,
|
| 257 |
+
"decode_tps": event.decode_tps,
|
| 258 |
+
}
|
| 259 |
+
|
| 260 |
+
|
| 261 |
+
def _http_error_text(exc: httpx.HTTPStatusError) -> str:
|
| 262 |
+
try:
|
| 263 |
+
return exc.response.text
|
| 264 |
+
except httpx.ResponseNotRead:
|
| 265 |
+
try:
|
| 266 |
+
exc.response.read()
|
| 267 |
+
except (httpx.HTTPError, httpx.StreamError):
|
| 268 |
+
return ""
|
| 269 |
+
return exc.response.text
|
| 270 |
+
|
| 271 |
+
|
| 272 |
+
def _is_context_overflow(exc: httpx.HTTPStatusError) -> bool:
|
| 273 |
+
return (
|
| 274 |
+
exc.response.status_code == 400
|
| 275 |
+
and "exceeds the available context size" in _http_error_text(exc)
|
| 276 |
)
|
| 277 |
|
|
|
|
| 278 |
|
| 279 |
+
def _context_size_from_error(exc: httpx.HTTPStatusError) -> int | None:
|
| 280 |
+
match = re.search(r"context size \((\d+) tokens\)", _http_error_text(exc))
|
| 281 |
+
if match is None:
|
| 282 |
+
return None
|
| 283 |
+
return int(match.group(1))
|
| 284 |
|
|
|
|
|
|
|
| 285 |
|
| 286 |
+
def _trim_for_context_collapse(
|
| 287 |
+
messages: list[ChatMessage],
|
| 288 |
+
) -> tuple[list[ChatMessage], int]:
|
| 289 |
+
system_messages = [message for message in messages if message.role == "system"]
|
| 290 |
+
conversation = [message for message in messages if message.role != "system"]
|
| 291 |
|
| 292 |
+
if not conversation:
|
| 293 |
+
return messages, 0
|
| 294 |
|
| 295 |
+
latest_user_index = next(
|
| 296 |
+
(
|
| 297 |
+
index
|
| 298 |
+
for index in range(len(conversation) - 1, -1, -1)
|
| 299 |
+
if conversation[index].role == "user"
|
| 300 |
+
),
|
| 301 |
+
len(conversation) - 1,
|
| 302 |
+
)
|
| 303 |
+
latest_user = conversation[latest_user_index]
|
| 304 |
+
recent_start = max(1, len(conversation) - 3)
|
| 305 |
+
recent = conversation[recent_start:]
|
| 306 |
+
|
| 307 |
+
retained: list[ChatMessage] = []
|
| 308 |
+
for message in recent:
|
| 309 |
+
if message is latest_user:
|
| 310 |
+
retained.append(message)
|
| 311 |
+
continue
|
| 312 |
+
if len(message.content) > MAX_COLLAPSE_RETAINED_CHARS:
|
| 313 |
+
retained.append(
|
| 314 |
+
ChatMessage(
|
| 315 |
+
role=message.role,
|
| 316 |
+
content=(
|
| 317 |
+
"[Earlier content collapsed; recent tail retained.]\n"
|
| 318 |
+
+ message.content[-COLLAPSED_MESSAGE_CHARS:]
|
| 319 |
+
),
|
| 320 |
+
)
|
| 321 |
+
)
|
| 322 |
+
else:
|
| 323 |
+
retained.append(message)
|
| 324 |
+
|
| 325 |
+
if latest_user not in retained:
|
| 326 |
+
retained.append(latest_user)
|
| 327 |
+
|
| 328 |
+
retained = list(dict.fromkeys(retained))
|
| 329 |
+
dropped = len(conversation) - len(retained)
|
| 330 |
+
return [*system_messages, *retained], max(0, dropped)
|
| 331 |
+
|
| 332 |
+
|
| 333 |
+
def _parse_messages(raw_messages: Any) -> list[ChatMessage]:
|
| 334 |
+
if not isinstance(raw_messages, list):
|
| 335 |
+
return []
|
| 336 |
+
|
| 337 |
+
messages: list[ChatMessage] = []
|
| 338 |
+
for raw_message in raw_messages:
|
| 339 |
+
if not isinstance(raw_message, dict):
|
| 340 |
+
continue
|
| 341 |
+
role = raw_message.get("role")
|
| 342 |
+
content = raw_message.get("content")
|
| 343 |
+
if role not in {"system", "user", "assistant"}:
|
| 344 |
+
continue
|
| 345 |
+
if not isinstance(content, str) or not content.strip():
|
| 346 |
+
continue
|
| 347 |
+
messages.append(ChatMessage(role=role, content=content.strip()))
|
| 348 |
+
|
| 349 |
+
return messages
|
| 350 |
+
|
| 351 |
+
|
| 352 |
+
def _latest_user_text(messages: list[ChatMessage]) -> str:
|
| 353 |
+
for message in reversed(messages):
|
| 354 |
+
if message.role == "user":
|
| 355 |
+
return message.content
|
| 356 |
+
return ""
|
| 357 |
+
|
| 358 |
+
|
| 359 |
+
def _simulated_answer(messages: list[ChatMessage]) -> str:
|
| 360 |
+
latest = _latest_user_text(messages).lower()
|
| 361 |
+
if any(marker in latest for marker in ("story", "故事", "repeat", "loop")):
|
| 362 |
+
phrase = (
|
| 363 |
+
"A tiny local model begins exploring a hidden runtime chamber. "
|
| 364 |
+
"It finds the same sentence again, and the same sentence again. "
|
| 365 |
+
)
|
| 366 |
+
return (
|
| 367 |
+
"I can feel the decode rhythm becoming unstable.\n\n"
|
| 368 |
+
+ phrase * 8
|
| 369 |
+
+ "\n\nOpenCortex marks this as a thought loop because recent generated "
|
| 370 |
+
"text is repeating instead of moving forward."
|
| 371 |
+
)
|
| 372 |
+
if any(marker in latest for marker in ("context", "window", "memory", "上下文", "记忆")):
|
| 373 |
+
return (
|
| 374 |
+
"OpenCortex treats the context window as the model's active workspace. "
|
| 375 |
+
"As the conversation grows, more tokens must be prefetched before decode "
|
| 376 |
+
"can begin. When the active window fills, older turns are still visible in "
|
| 377 |
+
"the chat log, but they fall outside what the model can reliably use."
|
| 378 |
+
)
|
| 379 |
+
return (
|
| 380 |
+
"OpenCortex is running in Space demo mode. It simulates the same runtime "
|
| 381 |
+
"event stream used by the local llama.cpp integration: prefill, first token, "
|
| 382 |
+
"decode throughput, context pressure, and completion. Run it locally with "
|
| 383 |
+
"llama.cpp to replace this simulated stream with live engine evidence."
|
| 384 |
+
)
|
| 385 |
|
|
|
|
|
|
|
|
|
|
| 386 |
|
| 387 |
+
def _token_chunks(text: str) -> list[str]:
|
| 388 |
+
chunks = re.findall(r"\S+\s*", text)
|
| 389 |
+
return chunks or [text]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 390 |
|
| 391 |
|
| 392 |
+
def _stream_simulated_events(messages: list[ChatMessage]) -> Iterator[RuntimeEvent]:
|
| 393 |
+
answer = _simulated_answer(messages)
|
| 394 |
+
chunks = _token_chunks(answer)
|
| 395 |
+
context_size = DEFAULT_CONTEXT_SIZE
|
| 396 |
+
prompt_tokens = max(24, sum(len(message.content) for message in messages) // 3)
|
| 397 |
+
base_context_tokens = min(context_size, prompt_tokens + 96)
|
| 398 |
+
delay_seconds = _simulator_delay_seconds()
|
|
|
|
|
|
|
|
|
|
| 399 |
|
| 400 |
+
yield RuntimeEvent(kind="request_started", text_delta="", ttft_ms=None, snapshot=None)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 401 |
|
| 402 |
+
snapshot = RuntimeSnapshot(
|
| 403 |
+
prompt_tps=72.4,
|
| 404 |
+
decode_tps=24.6,
|
| 405 |
+
requests_processing=1,
|
| 406 |
+
requests_deferred=0,
|
| 407 |
+
active_slots=1,
|
| 408 |
+
slot_context_tokens=(base_context_tokens,),
|
| 409 |
+
slot_context_size=context_size,
|
| 410 |
+
)
|
| 411 |
|
| 412 |
+
generated_text = ""
|
| 413 |
+
started_at = time.perf_counter()
|
| 414 |
+
for index, chunk in enumerate(chunks, start=1):
|
| 415 |
+
if delay_seconds:
|
| 416 |
+
time.sleep(delay_seconds)
|
| 417 |
+
generated_text += chunk
|
| 418 |
+
context_tokens = min(context_size, base_context_tokens + index)
|
| 419 |
+
event_kind = "first_token" if index == 1 else "token"
|
| 420 |
+
elapsed_ms = max(0.0, (time.perf_counter() - started_at) * 1000)
|
| 421 |
+
live_tps = None if index == 1 or elapsed_ms == 0 else index / (elapsed_ms / 1000)
|
| 422 |
+
repetition_detected = "same sentence again" in generated_text and index > 18
|
| 423 |
+
|
| 424 |
+
yield RuntimeEvent(
|
| 425 |
+
kind=event_kind,
|
| 426 |
+
text_delta=chunk,
|
| 427 |
+
ttft_ms=420.0 if index == 1 else None,
|
| 428 |
+
snapshot=snapshot if index == 1 else None,
|
| 429 |
+
generated_tokens=index,
|
| 430 |
+
elapsed_ms=0.0 if index == 1 else elapsed_ms,
|
| 431 |
+
live_tps=live_tps,
|
| 432 |
+
repetition_detected=repetition_detected,
|
| 433 |
+
context_tokens=context_tokens,
|
| 434 |
+
context_size=context_size,
|
| 435 |
+
working_memory_percent=round(min(100.0, context_tokens / context_size * 100), 1),
|
| 436 |
+
)
|
| 437 |
|
| 438 |
+
final_context_tokens = min(context_size, base_context_tokens + len(chunks))
|
| 439 |
+
yield RuntimeEvent(
|
| 440 |
+
kind="request_completed",
|
| 441 |
+
text_delta="",
|
| 442 |
+
ttft_ms=None,
|
| 443 |
+
snapshot=None,
|
| 444 |
+
generated_tokens=len(chunks),
|
| 445 |
+
repetition_detected="same sentence again" in generated_text,
|
| 446 |
+
context_tokens=final_context_tokens,
|
| 447 |
+
context_size=context_size,
|
| 448 |
+
working_memory_percent=round(min(100.0, final_context_tokens / context_size * 100), 1),
|
| 449 |
+
prompt_tokens=prompt_tokens,
|
| 450 |
+
completion_tokens=len(chunks),
|
| 451 |
+
prompt_tps=72.4,
|
| 452 |
+
decode_tps=24.6,
|
| 453 |
+
)
|
| 454 |
|
| 455 |
|
| 456 |
+
def _stream_events(messages: list[ChatMessage]) -> Iterator[str]:
|
| 457 |
+
if _backend_mode() == "simulated":
|
| 458 |
+
for event in _stream_simulated_events(messages):
|
| 459 |
+
yield json.dumps(event_to_payload(event), ensure_ascii=False) + "\n"
|
| 460 |
+
return
|
| 461 |
+
|
| 462 |
+
try:
|
| 463 |
+
for event in stream_chat_events(messages):
|
| 464 |
+
yield json.dumps(event_to_payload(event), ensure_ascii=False) + "\n"
|
| 465 |
+
except httpx.HTTPStatusError as exc:
|
| 466 |
+
if _is_context_overflow(exc):
|
| 467 |
+
trimmed_messages, dropped_messages = _trim_for_context_collapse(messages)
|
| 468 |
+
yield json.dumps(
|
| 469 |
+
{
|
| 470 |
+
"kind": "context_collapse",
|
| 471 |
+
"message": (
|
| 472 |
+
"The earliest turns fell outside the active context. "
|
| 473 |
+
"OpenCortex trimmed old history and retried with recent memory."
|
| 474 |
+
),
|
| 475 |
+
"dropped_messages": dropped_messages,
|
| 476 |
+
"retained_messages": len(trimmed_messages),
|
| 477 |
+
"context_size": _context_size_from_error(exc),
|
| 478 |
+
},
|
| 479 |
+
ensure_ascii=False,
|
| 480 |
+
) + "\n"
|
| 481 |
+
try:
|
| 482 |
+
for event in stream_chat_events(trimmed_messages):
|
| 483 |
+
yield json.dumps(event_to_payload(event), ensure_ascii=False) + "\n"
|
| 484 |
+
return
|
| 485 |
+
except httpx.HTTPStatusError as retry_exc:
|
| 486 |
+
if _is_context_overflow(retry_exc):
|
| 487 |
+
payload = {
|
| 488 |
+
"kind": "error",
|
| 489 |
+
"code": "context_overflow",
|
| 490 |
+
"message": (
|
| 491 |
+
"The conversation still exceeds llama.cpp's context window "
|
| 492 |
+
"after collapse. Start a new run or restart llama-server with "
|
| 493 |
+
"a larger -c value."
|
| 494 |
+
),
|
| 495 |
+
}
|
| 496 |
+
else:
|
| 497 |
+
payload = {
|
| 498 |
+
"kind": "error",
|
| 499 |
+
"code": "backend_http_error",
|
| 500 |
+
"message": f"{type(retry_exc).__name__}: {retry_exc}",
|
| 501 |
+
}
|
| 502 |
+
else:
|
| 503 |
+
payload = {
|
| 504 |
+
"kind": "error",
|
| 505 |
+
"code": "backend_http_error",
|
| 506 |
+
"message": f"{type(exc).__name__}: {exc}",
|
| 507 |
+
}
|
| 508 |
+
yield json.dumps(payload, ensure_ascii=False) + "\n"
|
| 509 |
+
except Exception as exc:
|
| 510 |
+
yield json.dumps(
|
| 511 |
+
{
|
| 512 |
+
"kind": "error",
|
| 513 |
+
"code": "backend_error",
|
| 514 |
+
"message": f"{type(exc).__name__}: {exc}",
|
| 515 |
+
},
|
| 516 |
+
ensure_ascii=False,
|
| 517 |
+
) + "\n"
|
| 518 |
+
|
| 519 |
+
|
| 520 |
+
def create_app() -> FastAPI:
|
| 521 |
+
app = FastAPI(title="OpenCortex")
|
| 522 |
+
app.mount("/assets", StaticFiles(directory=ASSET_DIR), name="assets")
|
| 523 |
+
|
| 524 |
+
@app.get("/", response_class=HTMLResponse)
|
| 525 |
+
def index() -> str:
|
| 526 |
+
return render_index()
|
| 527 |
+
|
| 528 |
+
@app.post("/api/chat")
|
| 529 |
+
async def chat(request: Request) -> StreamingResponse:
|
| 530 |
+
payload = await request.json()
|
| 531 |
+
messages = _parse_messages(payload.get("messages"))
|
| 532 |
+
return StreamingResponse(
|
| 533 |
+
_stream_events(messages),
|
| 534 |
+
media_type="application/x-ndjson",
|
| 535 |
+
)
|
| 536 |
|
| 537 |
+
return app
|
| 538 |
|
| 539 |
|
| 540 |
+
app = create_app()
|
| 541 |
|
| 542 |
|
| 543 |
+
def main() -> None:
|
| 544 |
+
host = os.getenv("OPEN_CORTEX_HOST", "0.0.0.0" if os.getenv("SPACE_ID") else "127.0.0.1")
|
| 545 |
+
uvicorn.run(
|
| 546 |
+
"open_cortex.ui.app:app",
|
| 547 |
+
host=host,
|
| 548 |
+
port=7860,
|
| 549 |
+
reload=False,
|
| 550 |
+
)
|
| 551 |
|
| 552 |
+
|
| 553 |
+
if __name__ == "__main__":
|
| 554 |
+
main()
|
open_cortex/ui/assets/open_cortex.css
ADDED
|
@@ -0,0 +1,1410 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
:root {
|
| 2 |
+
--bg: #06080d;
|
| 3 |
+
--panel: rgba(12, 16, 24, .82);
|
| 4 |
+
--panel-2: rgba(15, 21, 31, .76);
|
| 5 |
+
--line: rgba(151, 176, 211, .13);
|
| 6 |
+
--line-hot: rgba(84, 216, 255, .38);
|
| 7 |
+
--text: #e8eef7;
|
| 8 |
+
--muted: #7f8b9d;
|
| 9 |
+
--quiet: #536071;
|
| 10 |
+
--cyan: #53d7f7;
|
| 11 |
+
--cyan-2: #3aa7d2;
|
| 12 |
+
--teal: #64e8c2;
|
| 13 |
+
--yellow: #e7cf72;
|
| 14 |
+
--orange: #f2a35d;
|
| 15 |
+
--red: #f06b79;
|
| 16 |
+
--mono: "SFMono-Regular", "Cascadia Code", "Roboto Mono", Consolas, monospace;
|
| 17 |
+
--sans: Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
* { box-sizing: border-box; }
|
| 21 |
+
|
| 22 |
+
html, body {
|
| 23 |
+
margin: 0;
|
| 24 |
+
min-width: 1180px;
|
| 25 |
+
width: 100%;
|
| 26 |
+
height: 100%;
|
| 27 |
+
min-height: 100dvh;
|
| 28 |
+
color: var(--text);
|
| 29 |
+
background:
|
| 30 |
+
radial-gradient(circle at 72% 38%, rgba(26, 111, 142, .075), transparent 34%),
|
| 31 |
+
var(--bg);
|
| 32 |
+
font-family: var(--sans);
|
| 33 |
+
overflow: hidden;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
body::before {
|
| 37 |
+
content: "";
|
| 38 |
+
position: fixed;
|
| 39 |
+
inset: 0;
|
| 40 |
+
pointer-events: none;
|
| 41 |
+
opacity: .18;
|
| 42 |
+
background-image:
|
| 43 |
+
linear-gradient(rgba(132, 164, 204, .025) 1px, transparent 1px),
|
| 44 |
+
linear-gradient(90deg, rgba(132, 164, 204, .025) 1px, transparent 1px);
|
| 45 |
+
background-size: 32px 32px;
|
| 46 |
+
mask-image: linear-gradient(to bottom, black, transparent 88%);
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
button, select, textarea { font: inherit; }
|
| 50 |
+
button { color: inherit; }
|
| 51 |
+
|
| 52 |
+
.app {
|
| 53 |
+
--chat-width: 40%;
|
| 54 |
+
position: relative;
|
| 55 |
+
height: 100vh;
|
| 56 |
+
height: 100dvh;
|
| 57 |
+
min-height: 100dvh;
|
| 58 |
+
padding: 12px;
|
| 59 |
+
display: grid;
|
| 60 |
+
grid-template-rows: 44px minmax(0, 1fr);
|
| 61 |
+
gap: 8px;
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
.topbar {
|
| 65 |
+
grid-row: 1;
|
| 66 |
+
position: relative;
|
| 67 |
+
z-index: 50;
|
| 68 |
+
min-height: 44px;
|
| 69 |
+
display: flex;
|
| 70 |
+
align-items: center;
|
| 71 |
+
justify-content: space-between;
|
| 72 |
+
padding: 0 13px;
|
| 73 |
+
border: 1px solid var(--line);
|
| 74 |
+
border-radius: 12px;
|
| 75 |
+
background: rgba(9, 12, 18, .74);
|
| 76 |
+
backdrop-filter: blur(22px);
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
.brand, .top-actions, .runtime-connection, .scenario-meta, .metric-row,
|
| 80 |
+
.composer-actions, .model-state, .organ-head, .evidence-head {
|
| 81 |
+
display: flex;
|
| 82 |
+
align-items: center;
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
.brand { gap: 11px; }
|
| 86 |
+
.mark {
|
| 87 |
+
position: relative;
|
| 88 |
+
width: 25px;
|
| 89 |
+
height: 25px;
|
| 90 |
+
border: 1px solid rgba(91, 219, 250, .55);
|
| 91 |
+
border-radius: 8px;
|
| 92 |
+
background: radial-gradient(circle, rgba(83, 215, 247, .28), transparent 68%);
|
| 93 |
+
}
|
| 94 |
+
.mark::before, .mark::after {
|
| 95 |
+
content: "";
|
| 96 |
+
position: absolute;
|
| 97 |
+
inset: 5px;
|
| 98 |
+
border: 1px solid rgba(91, 219, 250, .5);
|
| 99 |
+
border-radius: 50%;
|
| 100 |
+
}
|
| 101 |
+
.mark::after {
|
| 102 |
+
inset: 10px;
|
| 103 |
+
border: 0;
|
| 104 |
+
background: var(--cyan);
|
| 105 |
+
box-shadow: 0 0 12px var(--cyan);
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
.brand-name { font-weight: 650; letter-spacing: -.02em; }
|
| 109 |
+
.top-actions { gap: 8px; }
|
| 110 |
+
.runtime-connection {
|
| 111 |
+
gap: 7px;
|
| 112 |
+
color: #79879a;
|
| 113 |
+
font: 9px var(--mono);
|
| 114 |
+
}
|
| 115 |
+
.live-dot {
|
| 116 |
+
width: 6px;
|
| 117 |
+
height: 6px;
|
| 118 |
+
border-radius: 50%;
|
| 119 |
+
background: var(--teal);
|
| 120 |
+
box-shadow: 0 0 8px var(--teal);
|
| 121 |
+
}
|
| 122 |
+
.icon-btn {
|
| 123 |
+
height: 28px;
|
| 124 |
+
padding: 0 10px;
|
| 125 |
+
color: #9ca8b8;
|
| 126 |
+
border: 1px solid var(--line);
|
| 127 |
+
border-radius: 7px;
|
| 128 |
+
background: rgba(255, 255, 255, .018);
|
| 129 |
+
cursor: pointer;
|
| 130 |
+
}
|
| 131 |
+
.icon-btn:hover { color: white; border-color: rgba(151, 176, 211, .28); }
|
| 132 |
+
|
| 133 |
+
.workspace {
|
| 134 |
+
grid-row: 2;
|
| 135 |
+
height: 100%;
|
| 136 |
+
min-height: 0;
|
| 137 |
+
display: grid;
|
| 138 |
+
grid-template-columns: var(--chat-width) 8px minmax(0, 1fr);
|
| 139 |
+
gap: 0;
|
| 140 |
+
transition: grid-template-columns .24s ease;
|
| 141 |
+
}
|
| 142 |
+
.workspace:has(.resizer.dragging) {
|
| 143 |
+
transition: none;
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
.surface {
|
| 147 |
+
height: 100%;
|
| 148 |
+
min-height: 0;
|
| 149 |
+
border: 1px solid var(--line);
|
| 150 |
+
border-radius: 14px;
|
| 151 |
+
background: var(--panel);
|
| 152 |
+
backdrop-filter: blur(20px);
|
| 153 |
+
overflow: hidden;
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
.conversation {
|
| 157 |
+
display: grid;
|
| 158 |
+
grid-template-rows: auto minmax(0, 1fr) auto;
|
| 159 |
+
transition: opacity .18s ease, transform .24s ease, border-color .18s ease;
|
| 160 |
+
}
|
| 161 |
+
.app.chat-collapsed {
|
| 162 |
+
--chat-width: 0px;
|
| 163 |
+
}
|
| 164 |
+
.app.chat-collapsed .conversation {
|
| 165 |
+
opacity: 0;
|
| 166 |
+
transform: translateX(-18px);
|
| 167 |
+
pointer-events: none;
|
| 168 |
+
border-color: transparent;
|
| 169 |
+
}
|
| 170 |
+
.app.collapse-preview .conversation {
|
| 171 |
+
opacity: .28;
|
| 172 |
+
transform: translateX(-8px);
|
| 173 |
+
}
|
| 174 |
+
.resizer {
|
| 175 |
+
position: relative;
|
| 176 |
+
z-index: 20;
|
| 177 |
+
display: flex;
|
| 178 |
+
align-items: center;
|
| 179 |
+
justify-content: center;
|
| 180 |
+
cursor: col-resize;
|
| 181 |
+
touch-action: none;
|
| 182 |
+
}
|
| 183 |
+
.resizer::before {
|
| 184 |
+
content: "";
|
| 185 |
+
width: 1px;
|
| 186 |
+
height: calc(100% - 24px);
|
| 187 |
+
background: rgba(151,176,211,.12);
|
| 188 |
+
transition: width .15s, background .15s;
|
| 189 |
+
}
|
| 190 |
+
.resizer:hover::before,
|
| 191 |
+
.resizer.dragging::before {
|
| 192 |
+
width: 2px;
|
| 193 |
+
background: rgba(83,215,247,.5);
|
| 194 |
+
}
|
| 195 |
+
.resize-grip {
|
| 196 |
+
position: absolute;
|
| 197 |
+
width: 4px;
|
| 198 |
+
height: 34px;
|
| 199 |
+
border-radius: 99px;
|
| 200 |
+
background: #202a38;
|
| 201 |
+
box-shadow: 0 0 0 1px rgba(151,176,211,.08);
|
| 202 |
+
}
|
| 203 |
+
.drawer-toggle {
|
| 204 |
+
position: absolute;
|
| 205 |
+
top: 14px;
|
| 206 |
+
width: 22px;
|
| 207 |
+
height: 22px;
|
| 208 |
+
padding: 0;
|
| 209 |
+
border: 1px solid rgba(151,176,211,.16);
|
| 210 |
+
border-radius: 6px;
|
| 211 |
+
color: #7f8c9d;
|
| 212 |
+
background: #0b1018;
|
| 213 |
+
cursor: pointer;
|
| 214 |
+
opacity: 0;
|
| 215 |
+
transition: opacity .15s, color .15s;
|
| 216 |
+
}
|
| 217 |
+
.app.chat-collapsed .drawer-toggle {
|
| 218 |
+
opacity: 1;
|
| 219 |
+
color: var(--cyan);
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
.section-head {
|
| 223 |
+
min-height: 52px;
|
| 224 |
+
padding: 0 15px;
|
| 225 |
+
display: flex;
|
| 226 |
+
align-items: center;
|
| 227 |
+
justify-content: space-between;
|
| 228 |
+
border-bottom: 1px solid var(--line);
|
| 229 |
+
}
|
| 230 |
+
.eyebrow {
|
| 231 |
+
color: #708094;
|
| 232 |
+
font: 9px var(--mono);
|
| 233 |
+
letter-spacing: .14em;
|
| 234 |
+
text-transform: uppercase;
|
| 235 |
+
}
|
| 236 |
+
.panel-title {
|
| 237 |
+
font-size: 13px;
|
| 238 |
+
font-weight: 590;
|
| 239 |
+
letter-spacing: -.01em;
|
| 240 |
+
}
|
| 241 |
+
.scenario {
|
| 242 |
+
display: inline-flex;
|
| 243 |
+
align-items: center;
|
| 244 |
+
gap: 7px;
|
| 245 |
+
padding: 6px 8px;
|
| 246 |
+
color: #aeb9c7;
|
| 247 |
+
border: 1px solid rgba(151, 176, 211, .13);
|
| 248 |
+
border-radius: 6px;
|
| 249 |
+
background: rgba(255, 255, 255, .018);
|
| 250 |
+
font-size: 11px;
|
| 251 |
+
}
|
| 252 |
+
.scenario::before {
|
| 253 |
+
content: "";
|
| 254 |
+
width: 5px;
|
| 255 |
+
height: 5px;
|
| 256 |
+
border-radius: 50%;
|
| 257 |
+
background: var(--cyan);
|
| 258 |
+
box-shadow: 0 0 7px rgba(83, 215, 247, .45);
|
| 259 |
+
}
|
| 260 |
+
|
| 261 |
+
.messages {
|
| 262 |
+
overflow-y: auto;
|
| 263 |
+
padding: 18px 18px 10px;
|
| 264 |
+
scrollbar-width: thin;
|
| 265 |
+
scrollbar-color: #263244 transparent;
|
| 266 |
+
}
|
| 267 |
+
.message {
|
| 268 |
+
max-width: 88%;
|
| 269 |
+
margin-bottom: 17px;
|
| 270 |
+
}
|
| 271 |
+
.message.user { margin-left: auto; }
|
| 272 |
+
.message-meta {
|
| 273 |
+
margin-bottom: 6px;
|
| 274 |
+
color: #667386;
|
| 275 |
+
font: 9px var(--mono);
|
| 276 |
+
letter-spacing: .08em;
|
| 277 |
+
text-transform: uppercase;
|
| 278 |
+
}
|
| 279 |
+
.user .message-meta { text-align: right; }
|
| 280 |
+
.bubble {
|
| 281 |
+
padding: 11px 13px;
|
| 282 |
+
border: 1px solid var(--line);
|
| 283 |
+
border-radius: 11px;
|
| 284 |
+
color: #cbd5e2;
|
| 285 |
+
background: rgba(255, 255, 255, .025);
|
| 286 |
+
font-size: 12px;
|
| 287 |
+
line-height: 1.6;
|
| 288 |
+
}
|
| 289 |
+
.user .bubble {
|
| 290 |
+
color: #dcf7ff;
|
| 291 |
+
border-color: rgba(83, 215, 247, .18);
|
| 292 |
+
background: rgba(40, 128, 158, .1);
|
| 293 |
+
}
|
| 294 |
+
.assistant-live .bubble {
|
| 295 |
+
border-color: rgba(83, 215, 247, .2);
|
| 296 |
+
background: linear-gradient(135deg, rgba(42, 108, 137, .08), rgba(255,255,255,.018));
|
| 297 |
+
}
|
| 298 |
+
.markdown-body p,
|
| 299 |
+
.markdown-body ul,
|
| 300 |
+
.markdown-body pre,
|
| 301 |
+
.markdown-body h1,
|
| 302 |
+
.markdown-body h2,
|
| 303 |
+
.markdown-body h3 {
|
| 304 |
+
margin: 0 0 10px;
|
| 305 |
+
}
|
| 306 |
+
.markdown-body :last-child { margin-bottom: 0; }
|
| 307 |
+
.markdown-body h1,
|
| 308 |
+
.markdown-body h2,
|
| 309 |
+
.markdown-body h3 {
|
| 310 |
+
color: #e8eef7;
|
| 311 |
+
font-weight: 650;
|
| 312 |
+
letter-spacing: -.01em;
|
| 313 |
+
line-height: 1.35;
|
| 314 |
+
}
|
| 315 |
+
.markdown-body h1 { font-size: 17px; }
|
| 316 |
+
.markdown-body h2 { font-size: 15px; }
|
| 317 |
+
.markdown-body h3 { font-size: 13px; }
|
| 318 |
+
.markdown-body ul {
|
| 319 |
+
padding-left: 17px;
|
| 320 |
+
}
|
| 321 |
+
.markdown-body li {
|
| 322 |
+
margin: 4px 0;
|
| 323 |
+
}
|
| 324 |
+
.markdown-body code {
|
| 325 |
+
padding: 1px 5px;
|
| 326 |
+
border: 1px solid rgba(151,176,211,.14);
|
| 327 |
+
border-radius: 5px;
|
| 328 |
+
color: #d9f8ff;
|
| 329 |
+
background: rgba(3, 8, 14, .5);
|
| 330 |
+
font: 11px var(--mono);
|
| 331 |
+
}
|
| 332 |
+
.markdown-body pre {
|
| 333 |
+
overflow-x: auto;
|
| 334 |
+
padding: 10px 11px;
|
| 335 |
+
border: 1px solid rgba(151,176,211,.12);
|
| 336 |
+
border-radius: 9px;
|
| 337 |
+
background: rgba(3, 8, 14, .62);
|
| 338 |
+
}
|
| 339 |
+
.markdown-body pre code {
|
| 340 |
+
padding: 0;
|
| 341 |
+
border: 0;
|
| 342 |
+
background: transparent;
|
| 343 |
+
white-space: pre;
|
| 344 |
+
}
|
| 345 |
+
.cursor {
|
| 346 |
+
display: inline-block;
|
| 347 |
+
width: 5px;
|
| 348 |
+
height: 13px;
|
| 349 |
+
margin-left: 3px;
|
| 350 |
+
vertical-align: -2px;
|
| 351 |
+
background: var(--cyan);
|
| 352 |
+
animation: blink .82s step-end infinite;
|
| 353 |
+
}
|
| 354 |
+
@keyframes blink { 50% { opacity: 0; } }
|
| 355 |
+
|
| 356 |
+
.composer-wrap {
|
| 357 |
+
padding: 10px;
|
| 358 |
+
border-top: 1px solid var(--line);
|
| 359 |
+
background: rgba(7, 10, 16, .66);
|
| 360 |
+
}
|
| 361 |
+
.composer {
|
| 362 |
+
padding: 10px;
|
| 363 |
+
border: 1px solid rgba(151, 176, 211, .18);
|
| 364 |
+
border-radius: 10px;
|
| 365 |
+
background: rgba(16, 22, 32, .72);
|
| 366 |
+
}
|
| 367 |
+
textarea {
|
| 368 |
+
display: block;
|
| 369 |
+
width: 100%;
|
| 370 |
+
height: 42px;
|
| 371 |
+
resize: none;
|
| 372 |
+
border: 0;
|
| 373 |
+
outline: 0;
|
| 374 |
+
color: #dce5ef;
|
| 375 |
+
background: transparent;
|
| 376 |
+
font-size: 12px;
|
| 377 |
+
}
|
| 378 |
+
textarea::placeholder { color: #596577; }
|
| 379 |
+
.composer-actions { justify-content: space-between; }
|
| 380 |
+
.hint { color: #566273; font: 9px var(--mono); }
|
| 381 |
+
.send {
|
| 382 |
+
display: inline-flex;
|
| 383 |
+
align-items: center;
|
| 384 |
+
gap: 7px;
|
| 385 |
+
height: 29px;
|
| 386 |
+
padding: 0 11px;
|
| 387 |
+
border: 1px solid rgba(91, 219, 250, .38);
|
| 388 |
+
border-radius: 7px;
|
| 389 |
+
color: #dcf8ff;
|
| 390 |
+
background: rgba(46, 155, 190, .13);
|
| 391 |
+
cursor: pointer;
|
| 392 |
+
}
|
| 393 |
+
.send:hover { background: rgba(46, 155, 190, .2); }
|
| 394 |
+
|
| 395 |
+
.observatory {
|
| 396 |
+
display: grid;
|
| 397 |
+
grid-template-rows: auto minmax(0, 1fr) auto;
|
| 398 |
+
background:
|
| 399 |
+
radial-gradient(circle at 49% 47%, rgba(32, 132, 168, .09), transparent 39%),
|
| 400 |
+
rgba(8, 12, 19, .84);
|
| 401 |
+
}
|
| 402 |
+
.observatory-head {
|
| 403 |
+
min-height: 52px;
|
| 404 |
+
padding: 0 14px;
|
| 405 |
+
display: flex;
|
| 406 |
+
justify-content: space-between;
|
| 407 |
+
align-items: center;
|
| 408 |
+
border-bottom: 1px solid var(--line);
|
| 409 |
+
}
|
| 410 |
+
.model-state { gap: 8px; }
|
| 411 |
+
.model-name {
|
| 412 |
+
color: #aab5c4;
|
| 413 |
+
font: 10px var(--mono);
|
| 414 |
+
}
|
| 415 |
+
.phase-pill {
|
| 416 |
+
min-width: 82px;
|
| 417 |
+
padding: 5px 8px;
|
| 418 |
+
border: 1px solid rgba(83, 215, 247, .25);
|
| 419 |
+
border-radius: 99px;
|
| 420 |
+
color: var(--cyan);
|
| 421 |
+
background: rgba(83, 215, 247, .06);
|
| 422 |
+
font: 9px var(--mono);
|
| 423 |
+
text-align: center;
|
| 424 |
+
letter-spacing: .11em;
|
| 425 |
+
text-transform: uppercase;
|
| 426 |
+
}
|
| 427 |
+
|
| 428 |
+
.observatory-stage {
|
| 429 |
+
position: relative;
|
| 430 |
+
min-height: 0;
|
| 431 |
+
overflow: hidden;
|
| 432 |
+
}
|
| 433 |
+
.observatory-stage::before,
|
| 434 |
+
.observatory-stage::after {
|
| 435 |
+
content: "";
|
| 436 |
+
position: absolute;
|
| 437 |
+
pointer-events: none;
|
| 438 |
+
}
|
| 439 |
+
.observatory-stage::before {
|
| 440 |
+
inset: 9% 18%;
|
| 441 |
+
border: 1px solid rgba(91, 219, 250, .06);
|
| 442 |
+
border-radius: 50%;
|
| 443 |
+
}
|
| 444 |
+
.observatory-stage::after {
|
| 445 |
+
inset: 23% 32%;
|
| 446 |
+
border: 1px dashed rgba(91, 219, 250, .09);
|
| 447 |
+
border-radius: 50%;
|
| 448 |
+
animation: rotate 28s linear infinite;
|
| 449 |
+
}
|
| 450 |
+
@keyframes rotate { to { transform: rotate(360deg); } }
|
| 451 |
+
|
| 452 |
+
.conduit {
|
| 453 |
+
position: absolute;
|
| 454 |
+
z-index: 1;
|
| 455 |
+
height: 1px;
|
| 456 |
+
transform-origin: left center;
|
| 457 |
+
background: linear-gradient(90deg, rgba(83,215,247,.05), rgba(83,215,247,.28), rgba(83,215,247,.05));
|
| 458 |
+
}
|
| 459 |
+
.conduit::after {
|
| 460 |
+
content: "";
|
| 461 |
+
position: absolute;
|
| 462 |
+
top: -1px;
|
| 463 |
+
left: 0;
|
| 464 |
+
width: 34px;
|
| 465 |
+
height: 3px;
|
| 466 |
+
background: linear-gradient(90deg, transparent, var(--cyan), transparent);
|
| 467 |
+
filter: blur(.2px);
|
| 468 |
+
animation: signal 2.2s linear infinite;
|
| 469 |
+
}
|
| 470 |
+
@keyframes signal { from { transform: translateX(0); } to { transform: translateX(190px); } }
|
| 471 |
+
.c1 { left: 30%; top: 28%; width: 190px; transform: rotate(25deg); }
|
| 472 |
+
.c2 { left: 54%; top: 45%; width: 180px; transform: rotate(-27deg); }
|
| 473 |
+
.c3 { left: 30%; top: 70%; width: 190px; transform: rotate(-23deg); }
|
| 474 |
+
.c4 { left: 54%; top: 53%; width: 175px; transform: rotate(28deg); }
|
| 475 |
+
|
| 476 |
+
.core-wrap {
|
| 477 |
+
position: absolute;
|
| 478 |
+
z-index: 3;
|
| 479 |
+
left: 50%;
|
| 480 |
+
top: 50%;
|
| 481 |
+
width: 188px;
|
| 482 |
+
height: 188px;
|
| 483 |
+
transform: translate(-50%, -50%);
|
| 484 |
+
}
|
| 485 |
+
.core-rings, .core, .core::before, .core::after {
|
| 486 |
+
position: absolute;
|
| 487 |
+
border-radius: 50%;
|
| 488 |
+
}
|
| 489 |
+
.core-rings {
|
| 490 |
+
inset: 0;
|
| 491 |
+
border: 1px solid rgba(83, 215, 247, .13);
|
| 492 |
+
box-shadow: inset 0 0 42px rgba(83, 215, 247, .04);
|
| 493 |
+
animation: breathe 3s ease-in-out infinite;
|
| 494 |
+
}
|
| 495 |
+
.core-rings::before, .core-rings::after {
|
| 496 |
+
content: "";
|
| 497 |
+
position: absolute;
|
| 498 |
+
border-radius: 50%;
|
| 499 |
+
border: 1px dashed rgba(83, 215, 247, .2);
|
| 500 |
+
}
|
| 501 |
+
.core-rings::before { inset: 13px; animation: rotate 16s linear infinite; }
|
| 502 |
+
.core-rings::after { inset: 31px; animation: rotate 11s linear infinite reverse; }
|
| 503 |
+
.core {
|
| 504 |
+
inset: 43px;
|
| 505 |
+
background:
|
| 506 |
+
radial-gradient(circle at 36% 28%, rgba(220, 251, 255, .92), rgba(92, 219, 247, .5) 11%, rgba(27, 119, 153, .4) 29%, rgba(6, 18, 27, .9) 68%);
|
| 507 |
+
box-shadow:
|
| 508 |
+
0 0 25px rgba(83, 215, 247, .35),
|
| 509 |
+
0 0 70px rgba(23, 130, 168, .16),
|
| 510 |
+
inset -9px -13px 25px rgba(0, 0, 0, .6);
|
| 511 |
+
animation: core-pulse 1.35s ease-in-out infinite;
|
| 512 |
+
}
|
| 513 |
+
.core::before {
|
| 514 |
+
content: "";
|
| 515 |
+
inset: 9px;
|
| 516 |
+
border: 1px solid rgba(186, 245, 255, .23);
|
| 517 |
+
}
|
| 518 |
+
.core::after {
|
| 519 |
+
content: "";
|
| 520 |
+
left: 24px;
|
| 521 |
+
top: 18px;
|
| 522 |
+
width: 17px;
|
| 523 |
+
height: 9px;
|
| 524 |
+
background: rgba(225, 253, 255, .55);
|
| 525 |
+
filter: blur(5px);
|
| 526 |
+
transform: rotate(-28deg);
|
| 527 |
+
}
|
| 528 |
+
@keyframes breathe {
|
| 529 |
+
50% { transform: scale(1.025); border-color: rgba(83, 215, 247, .24); }
|
| 530 |
+
}
|
| 531 |
+
@keyframes core-pulse {
|
| 532 |
+
50% {
|
| 533 |
+
transform: scale(1.075);
|
| 534 |
+
box-shadow: 0 0 36px rgba(83, 215, 247, .48), 0 0 90px rgba(23, 130, 168, .22), inset -9px -13px 25px rgba(0,0,0,.6);
|
| 535 |
+
}
|
| 536 |
+
}
|
| 537 |
+
.core-copy {
|
| 538 |
+
position: absolute;
|
| 539 |
+
z-index: 4;
|
| 540 |
+
left: 50%;
|
| 541 |
+
top: calc(50% + 105px);
|
| 542 |
+
transform: translateX(-50%);
|
| 543 |
+
text-align: center;
|
| 544 |
+
white-space: nowrap;
|
| 545 |
+
}
|
| 546 |
+
.core-label {
|
| 547 |
+
color: #718195;
|
| 548 |
+
font: 9px var(--mono);
|
| 549 |
+
letter-spacing: .15em;
|
| 550 |
+
text-transform: uppercase;
|
| 551 |
+
}
|
| 552 |
+
.core-state {
|
| 553 |
+
margin-top: 5px;
|
| 554 |
+
color: #dffaff;
|
| 555 |
+
font-size: 15px;
|
| 556 |
+
font-weight: 590;
|
| 557 |
+
}
|
| 558 |
+
.core-detail {
|
| 559 |
+
margin-top: 4px;
|
| 560 |
+
color: #688195;
|
| 561 |
+
font: 9px var(--mono);
|
| 562 |
+
}
|
| 563 |
+
|
| 564 |
+
.organ {
|
| 565 |
+
position: absolute;
|
| 566 |
+
z-index: 5;
|
| 567 |
+
width: 218px;
|
| 568 |
+
min-height: 112px;
|
| 569 |
+
padding: 12px;
|
| 570 |
+
border: 1px solid rgba(151, 176, 211, .14);
|
| 571 |
+
border-radius: 12px;
|
| 572 |
+
background: rgba(11, 16, 24, .78);
|
| 573 |
+
backdrop-filter: blur(16px);
|
| 574 |
+
transition: border-color .3s, transform .3s, box-shadow .3s;
|
| 575 |
+
}
|
| 576 |
+
.organ.active {
|
| 577 |
+
border-color: rgba(151, 176, 211, .17);
|
| 578 |
+
box-shadow: inset 0 0 25px rgba(83, 215, 247, .012);
|
| 579 |
+
}
|
| 580 |
+
.memory { left: 3.5%; top: 8%; }
|
| 581 |
+
.context { right: 3.5%; top: 8%; }
|
| 582 |
+
.tokens { left: 3.5%; bottom: 7%; }
|
| 583 |
+
.health { right: 3.5%; bottom: 7%; }
|
| 584 |
+
.organ-head { justify-content: space-between; }
|
| 585 |
+
.organ-name {
|
| 586 |
+
color: #8f9db0;
|
| 587 |
+
font-size: 10px;
|
| 588 |
+
font-weight: 600;
|
| 589 |
+
letter-spacing: .025em;
|
| 590 |
+
}
|
| 591 |
+
.organ-state {
|
| 592 |
+
color: #8198a8;
|
| 593 |
+
font: 9px var(--mono);
|
| 594 |
+
letter-spacing: .03em;
|
| 595 |
+
}
|
| 596 |
+
.metric-row {
|
| 597 |
+
justify-content: space-between;
|
| 598 |
+
margin-top: 12px;
|
| 599 |
+
}
|
| 600 |
+
.metric-value {
|
| 601 |
+
font: 18px var(--mono);
|
| 602 |
+
letter-spacing: -.04em;
|
| 603 |
+
}
|
| 604 |
+
.metric-unit {
|
| 605 |
+
margin-left: 3px;
|
| 606 |
+
color: #637084;
|
| 607 |
+
font: 9px var(--mono);
|
| 608 |
+
}
|
| 609 |
+
|
| 610 |
+
.meter {
|
| 611 |
+
position: relative;
|
| 612 |
+
height: 5px;
|
| 613 |
+
margin-top: 11px;
|
| 614 |
+
overflow: hidden;
|
| 615 |
+
border-radius: 99px;
|
| 616 |
+
background: #1a2330;
|
| 617 |
+
}
|
| 618 |
+
.meter-fill {
|
| 619 |
+
width: 63%;
|
| 620 |
+
height: 100%;
|
| 621 |
+
border-radius: inherit;
|
| 622 |
+
background: linear-gradient(90deg, #277f9d, var(--cyan));
|
| 623 |
+
box-shadow: 0 0 9px rgba(83, 215, 247, .35);
|
| 624 |
+
transition: width .45s ease;
|
| 625 |
+
}
|
| 626 |
+
.memory-blocks {
|
| 627 |
+
display: grid;
|
| 628 |
+
grid-template-columns: repeat(6, 1fr);
|
| 629 |
+
grid-template-rows: repeat(2, 1fr);
|
| 630 |
+
gap: 3px;
|
| 631 |
+
height: 30px;
|
| 632 |
+
margin-top: 9px;
|
| 633 |
+
padding: 3px;
|
| 634 |
+
border: 1px solid rgba(151, 176, 211, .12);
|
| 635 |
+
border-radius: 6px;
|
| 636 |
+
background: rgba(3, 8, 14, .72);
|
| 637 |
+
}
|
| 638 |
+
.memory-blocks i {
|
| 639 |
+
position: relative;
|
| 640 |
+
border: 1px solid rgba(83, 215, 247, .18);
|
| 641 |
+
border-radius: 2px;
|
| 642 |
+
background: rgba(83, 215, 247, .08);
|
| 643 |
+
transition: opacity .35s, transform .35s, background .35s;
|
| 644 |
+
}
|
| 645 |
+
.memory-blocks i::after {
|
| 646 |
+
content: "";
|
| 647 |
+
position: absolute;
|
| 648 |
+
left: 3px;
|
| 649 |
+
right: 3px;
|
| 650 |
+
bottom: 2px;
|
| 651 |
+
height: 1px;
|
| 652 |
+
background: rgba(255,255,255,.1);
|
| 653 |
+
}
|
| 654 |
+
.memory-blocks i.filled {
|
| 655 |
+
background: rgba(83, 215, 247, .48);
|
| 656 |
+
box-shadow: 0 0 5px rgba(83, 215, 247, .18);
|
| 657 |
+
}
|
| 658 |
+
.context-vessel {
|
| 659 |
+
position: relative;
|
| 660 |
+
height: 34px;
|
| 661 |
+
margin-top: 10px;
|
| 662 |
+
overflow: hidden;
|
| 663 |
+
border: 1px solid rgba(151, 176, 211, .14);
|
| 664 |
+
border-radius: 7px;
|
| 665 |
+
background: rgba(3, 8, 14, .8);
|
| 666 |
+
}
|
| 667 |
+
.context-fill {
|
| 668 |
+
position: absolute;
|
| 669 |
+
z-index: 1;
|
| 670 |
+
left: 0;
|
| 671 |
+
top: 0;
|
| 672 |
+
width: 59%;
|
| 673 |
+
height: 100%;
|
| 674 |
+
background: linear-gradient(90deg, rgba(48,148,181,.08), rgba(48,148,181,.22));
|
| 675 |
+
border-right: 1px solid rgba(83, 215, 247, .48);
|
| 676 |
+
transition: width .45s ease;
|
| 677 |
+
}
|
| 678 |
+
.context-ticks {
|
| 679 |
+
position: absolute;
|
| 680 |
+
z-index: 3;
|
| 681 |
+
inset: 0;
|
| 682 |
+
background: repeating-linear-gradient(90deg, transparent 0 17px, rgba(255,255,255,.055) 18px);
|
| 683 |
+
}
|
| 684 |
+
.memory-tape {
|
| 685 |
+
position: absolute;
|
| 686 |
+
z-index: 2;
|
| 687 |
+
inset: 4px;
|
| 688 |
+
display: flex;
|
| 689 |
+
gap: 3px;
|
| 690 |
+
align-items: stretch;
|
| 691 |
+
}
|
| 692 |
+
.memory-segment {
|
| 693 |
+
flex: 1;
|
| 694 |
+
border-radius: 2px;
|
| 695 |
+
background: rgba(104, 205, 232, .16);
|
| 696 |
+
border: 1px solid rgba(104, 205, 232, .08);
|
| 697 |
+
transition: opacity .7s, transform .7s, filter .7s;
|
| 698 |
+
}
|
| 699 |
+
.memory-segment.old { opacity: .45; }
|
| 700 |
+
.memory-segment.recent {
|
| 701 |
+
background: rgba(83, 215, 247, .38);
|
| 702 |
+
box-shadow: 0 0 6px rgba(83,215,247,.15);
|
| 703 |
+
}
|
| 704 |
+
.context-forgetting .memory-segment.old {
|
| 705 |
+
opacity: 0;
|
| 706 |
+
transform: translateX(-18px) scaleX(.2);
|
| 707 |
+
filter: blur(3px);
|
| 708 |
+
}
|
| 709 |
+
.tape-direction {
|
| 710 |
+
position: absolute;
|
| 711 |
+
z-index: 4;
|
| 712 |
+
right: 5px;
|
| 713 |
+
top: 50%;
|
| 714 |
+
transform: translateY(-50%);
|
| 715 |
+
color: rgba(196, 242, 252, .6);
|
| 716 |
+
font: 9px var(--mono);
|
| 717 |
+
}
|
| 718 |
+
|
| 719 |
+
.token-river {
|
| 720 |
+
position: relative;
|
| 721 |
+
display: flex;
|
| 722 |
+
align-items: center;
|
| 723 |
+
gap: 5px;
|
| 724 |
+
height: 31px;
|
| 725 |
+
margin-top: 10px;
|
| 726 |
+
overflow: hidden;
|
| 727 |
+
padding: 0 7px;
|
| 728 |
+
border: 1px solid rgba(151,176,211,.13);
|
| 729 |
+
border-radius: 99px;
|
| 730 |
+
background: linear-gradient(180deg, rgba(83,215,247,.04), rgba(3,8,14,.84));
|
| 731 |
+
}
|
| 732 |
+
.token-river::before,
|
| 733 |
+
.token-river::after {
|
| 734 |
+
content: "";
|
| 735 |
+
position: absolute;
|
| 736 |
+
left: 5px;
|
| 737 |
+
right: 5px;
|
| 738 |
+
height: 1px;
|
| 739 |
+
background: linear-gradient(90deg, transparent, rgba(83,215,247,.24), transparent);
|
| 740 |
+
}
|
| 741 |
+
.token-river::before { top: 5px; }
|
| 742 |
+
.token-river::after { bottom: 5px; }
|
| 743 |
+
.particle {
|
| 744 |
+
flex: 0 0 auto;
|
| 745 |
+
width: 17px;
|
| 746 |
+
height: 6px;
|
| 747 |
+
border-radius: 2px;
|
| 748 |
+
background: linear-gradient(90deg, rgba(83,215,247,.17), rgba(83,215,247,.92));
|
| 749 |
+
box-shadow: 0 0 7px rgba(83,215,247,.2);
|
| 750 |
+
animation: flow 1.35s linear infinite;
|
| 751 |
+
}
|
| 752 |
+
.particle:nth-child(2n) { width: 8px; opacity: .72; animation-delay: -.5s; }
|
| 753 |
+
.particle:nth-child(3n) { width: 12px; opacity: .45; animation-delay: -.9s; }
|
| 754 |
+
@keyframes flow {
|
| 755 |
+
from { transform: translateX(-26px); opacity: .15; }
|
| 756 |
+
25% { opacity: 1; }
|
| 757 |
+
to { transform: translateX(48px); opacity: .15; }
|
| 758 |
+
}
|
| 759 |
+
|
| 760 |
+
.latency-trace {
|
| 761 |
+
position: relative;
|
| 762 |
+
display: flex;
|
| 763 |
+
align-items: center;
|
| 764 |
+
gap: 3px;
|
| 765 |
+
height: 30px;
|
| 766 |
+
margin-top: 9px;
|
| 767 |
+
padding: 0 4px;
|
| 768 |
+
border-left: 1px solid rgba(151,176,211,.15);
|
| 769 |
+
border-right: 1px solid rgba(151,176,211,.15);
|
| 770 |
+
background: linear-gradient(180deg, transparent 48%, rgba(151,176,211,.1) 49%, rgba(151,176,211,.1) 51%, transparent 52%);
|
| 771 |
+
}
|
| 772 |
+
.latency-trace i {
|
| 773 |
+
flex: 1;
|
| 774 |
+
height: 12%;
|
| 775 |
+
border-radius: 2px;
|
| 776 |
+
background: rgba(83, 215, 247, .42);
|
| 777 |
+
animation: trace 1.6s ease-in-out infinite;
|
| 778 |
+
}
|
| 779 |
+
.latency-trace i:nth-child(2) { height: 24%; animation-delay: -.2s; }
|
| 780 |
+
.latency-trace i:nth-child(3) { height: 52%; animation-delay: -.4s; }
|
| 781 |
+
.latency-trace i:nth-child(4) { height: 92%; animation-delay: -.6s; }
|
| 782 |
+
.latency-trace i:nth-child(5) { height: 28%; animation-delay: -.8s; }
|
| 783 |
+
.latency-trace i:nth-child(6) { height: 18%; animation-delay: -1s; }
|
| 784 |
+
.latency-trace i:nth-child(7) { height: 38%; animation-delay: -1.2s; }
|
| 785 |
+
.latency-trace i:nth-child(8) { height: 14%; animation-delay: -1.4s; }
|
| 786 |
+
@keyframes trace { 50% { opacity: .35; transform: scaleY(.75); } }
|
| 787 |
+
|
| 788 |
+
.evidence {
|
| 789 |
+
min-height: 38px;
|
| 790 |
+
display: flex;
|
| 791 |
+
align-items: center;
|
| 792 |
+
gap: 18px;
|
| 793 |
+
padding: 0 14px;
|
| 794 |
+
border-top: 1px solid var(--line);
|
| 795 |
+
background: rgba(7, 10, 15, .74);
|
| 796 |
+
}
|
| 797 |
+
.evidence-cell {
|
| 798 |
+
display: flex;
|
| 799 |
+
align-items: center;
|
| 800 |
+
gap: 6px;
|
| 801 |
+
min-width: 0;
|
| 802 |
+
padding: 0;
|
| 803 |
+
border: 0;
|
| 804 |
+
}
|
| 805 |
+
.evidence-head { gap: 7px; color: #78879a; font-size: 9px; }
|
| 806 |
+
.measured-dot {
|
| 807 |
+
width: 5px;
|
| 808 |
+
height: 5px;
|
| 809 |
+
border-radius: 50%;
|
| 810 |
+
background: var(--cyan);
|
| 811 |
+
box-shadow: 0 0 6px var(--cyan);
|
| 812 |
+
}
|
| 813 |
+
.evidence-label {
|
| 814 |
+
color: #5f6d80;
|
| 815 |
+
font: 8px var(--mono);
|
| 816 |
+
letter-spacing: .07em;
|
| 817 |
+
text-transform: uppercase;
|
| 818 |
+
}
|
| 819 |
+
.evidence-value {
|
| 820 |
+
margin-top: 0;
|
| 821 |
+
overflow: hidden;
|
| 822 |
+
color: #b9c6d5;
|
| 823 |
+
font: 11px var(--mono);
|
| 824 |
+
text-overflow: ellipsis;
|
| 825 |
+
white-space: nowrap;
|
| 826 |
+
}
|
| 827 |
+
|
| 828 |
+
.controls {
|
| 829 |
+
display: flex;
|
| 830 |
+
align-items: center;
|
| 831 |
+
gap: 7px;
|
| 832 |
+
}
|
| 833 |
+
select {
|
| 834 |
+
height: 27px;
|
| 835 |
+
padding: 0 25px 0 8px;
|
| 836 |
+
color: #9ba8b8;
|
| 837 |
+
border: 1px solid var(--line);
|
| 838 |
+
border-radius: 7px;
|
| 839 |
+
outline: 0;
|
| 840 |
+
background: #0c1119;
|
| 841 |
+
font-size: 10px;
|
| 842 |
+
}
|
| 843 |
+
.run-experiment {
|
| 844 |
+
height: 27px;
|
| 845 |
+
padding: 0 10px;
|
| 846 |
+
color: #d9f8ff;
|
| 847 |
+
border: 1px solid rgba(83, 215, 247, .3);
|
| 848 |
+
border-radius: 7px;
|
| 849 |
+
background: rgba(83, 215, 247, .075);
|
| 850 |
+
font-size: 10px;
|
| 851 |
+
cursor: pointer;
|
| 852 |
+
}
|
| 853 |
+
|
| 854 |
+
.app.phase-idle .core {
|
| 855 |
+
opacity: .42;
|
| 856 |
+
animation: none;
|
| 857 |
+
transform: scale(.86);
|
| 858 |
+
}
|
| 859 |
+
.app.phase-idle .core-rings,
|
| 860 |
+
.app.phase-idle .conduit::after,
|
| 861 |
+
.app.phase-idle .particle {
|
| 862 |
+
animation-play-state: paused;
|
| 863 |
+
opacity: .2;
|
| 864 |
+
}
|
| 865 |
+
.app.phase-prefill .core {
|
| 866 |
+
animation: charge .72s ease-in-out infinite alternate;
|
| 867 |
+
}
|
| 868 |
+
.app.phase-prefill .core-rings::before { animation-duration: 2.2s; }
|
| 869 |
+
.app.phase-prefill .core-rings::after { animation-duration: 1.4s; }
|
| 870 |
+
.app.phase-prefill .tokens .particle { animation-play-state: paused; opacity: .14; }
|
| 871 |
+
.app.phase-prefill .memory,
|
| 872 |
+
.app.phase-prefill .context {
|
| 873 |
+
border-color: rgba(83, 215, 247, .4);
|
| 874 |
+
box-shadow: inset 0 0 24px rgba(83, 215, 247, .05);
|
| 875 |
+
}
|
| 876 |
+
@keyframes charge {
|
| 877 |
+
from { transform: scale(.75); filter: brightness(.7); }
|
| 878 |
+
to { transform: scale(1.12); filter: brightness(1.35); }
|
| 879 |
+
}
|
| 880 |
+
.app.phase-decode .core { animation: core-pulse 1.35s ease-in-out infinite; }
|
| 881 |
+
.app.core-slow .core { animation-duration: 2.9s; }
|
| 882 |
+
.assistant-live.slow-stream .cursor { animation-duration: 1.8s; }
|
| 883 |
+
.app.phase-recovery .core {
|
| 884 |
+
animation: recovery 1.1s ease-out infinite;
|
| 885 |
+
background: radial-gradient(circle at 36% 28%, #fff2e4, rgba(242,163,93,.7) 12%, rgba(145,73,39,.42) 35%, rgba(12,9,8,.94) 72%);
|
| 886 |
+
}
|
| 887 |
+
|
| 888 |
+
.memory.memory-pressure {
|
| 889 |
+
border-color: rgba(242, 163, 93, .28);
|
| 890 |
+
box-shadow: inset 0 0 25px rgba(242,163,93,.04);
|
| 891 |
+
}
|
| 892 |
+
.memory.memory-pressure .organ-state { color: var(--orange); }
|
| 893 |
+
.memory.memory-pressure .memory-blocks i.filled {
|
| 894 |
+
background: rgba(242,163,93,.5);
|
| 895 |
+
border-color: rgba(242,163,93,.4);
|
| 896 |
+
}
|
| 897 |
+
.memory.memory-pressure .memory-blocks i:nth-child(3n) {
|
| 898 |
+
animation: fragment .66s ease-in-out infinite alternate;
|
| 899 |
+
}
|
| 900 |
+
.memory.memory-pressure .memory-blocks i:nth-child(4n) {
|
| 901 |
+
animation: evict 1.8s ease-in-out infinite;
|
| 902 |
+
}
|
| 903 |
+
.memory.memory-overloaded {
|
| 904 |
+
border-color: rgba(240,107,121,.4);
|
| 905 |
+
animation: shake .28s linear infinite;
|
| 906 |
+
}
|
| 907 |
+
.memory.memory-overloaded .organ-state { color: var(--red); }
|
| 908 |
+
.memory.memory-overloaded .memory-blocks i.filled {
|
| 909 |
+
background: rgba(240,107,121,.56);
|
| 910 |
+
border-color: rgba(240,107,121,.45);
|
| 911 |
+
}
|
| 912 |
+
|
| 913 |
+
.context.context-pressure {
|
| 914 |
+
border-color: rgba(242, 163, 93, .3);
|
| 915 |
+
}
|
| 916 |
+
.context.context-pressure .organ-state { color: var(--orange); }
|
| 917 |
+
.context.context-pressure .context-vessel {
|
| 918 |
+
animation: pressure 1.35s ease-in-out infinite;
|
| 919 |
+
}
|
| 920 |
+
.context.context-pressure .context-fill {
|
| 921 |
+
background: rgba(242,163,93,.18);
|
| 922 |
+
border-color: rgba(242,163,93,.62);
|
| 923 |
+
}
|
| 924 |
+
.tokens.tokens-slow {
|
| 925 |
+
border-color: rgba(242,163,93,.34);
|
| 926 |
+
}
|
| 927 |
+
.tokens.tokens-slow .organ-state { color: var(--orange); }
|
| 928 |
+
.tokens.tokens-slow .particle {
|
| 929 |
+
animation: broken-flow 3.2s steps(7, end) infinite;
|
| 930 |
+
background: linear-gradient(90deg, rgba(242,163,93,.12), rgba(242,163,93,.9));
|
| 931 |
+
}
|
| 932 |
+
.tokens.tokens-stalled {
|
| 933 |
+
border-color: rgba(240,107,121,.4);
|
| 934 |
+
}
|
| 935 |
+
.tokens.tokens-stalled .organ-state { color: var(--red); }
|
| 936 |
+
.tokens.tokens-stalled .particle {
|
| 937 |
+
animation: stalled-flow 5s steps(3, end) infinite;
|
| 938 |
+
background: rgba(240,107,121,.55);
|
| 939 |
+
}
|
| 940 |
+
|
| 941 |
+
.health.engine-strained {
|
| 942 |
+
border-color: rgba(242,163,93,.3);
|
| 943 |
+
}
|
| 944 |
+
.health.engine-strained .organ-state { color: var(--orange); }
|
| 945 |
+
.health.engine-strained .latency-trace i {
|
| 946 |
+
background: rgba(242,163,93,.55);
|
| 947 |
+
animation-duration: .7s;
|
| 948 |
+
}
|
| 949 |
+
.health.engine-recovery {
|
| 950 |
+
border-color: rgba(240,107,121,.34);
|
| 951 |
+
}
|
| 952 |
+
.health.engine-recovery .organ-state { color: var(--red); }
|
| 953 |
+
.health.engine-recovery .latency-trace i {
|
| 954 |
+
background: rgba(240,107,121,.58);
|
| 955 |
+
animation: recovery-bars .52s steps(3, end) infinite;
|
| 956 |
+
}
|
| 957 |
+
|
| 958 |
+
/* Scenario-specific signal paths into the Cortex Core. */
|
| 959 |
+
.app.scenario-memory .c1 {
|
| 960 |
+
height: 2px;
|
| 961 |
+
background: repeating-linear-gradient(90deg, rgba(242,163,93,.08) 0 8px, rgba(242,163,93,.72) 8px 15px, transparent 15px 24px);
|
| 962 |
+
animation: connection-fault .38s steps(2, end) infinite;
|
| 963 |
+
}
|
| 964 |
+
.app.scenario-memory .c1::after {
|
| 965 |
+
background: linear-gradient(90deg, transparent, var(--orange), transparent);
|
| 966 |
+
animation: signal-stutter 1.1s steps(4, end) infinite;
|
| 967 |
+
}
|
| 968 |
+
.app.scenario-memory .core {
|
| 969 |
+
animation: memory-core-strain .82s steps(5, end) infinite;
|
| 970 |
+
}
|
| 971 |
+
.app.scenario-memory .core-rings::before {
|
| 972 |
+
border-left-color: rgba(242,163,93,.85);
|
| 973 |
+
border-top-color: rgba(242,163,93,.36);
|
| 974 |
+
animation-duration: 2.1s;
|
| 975 |
+
}
|
| 976 |
+
|
| 977 |
+
.app.scenario-slow .c3 {
|
| 978 |
+
height: 2px;
|
| 979 |
+
background: repeating-linear-gradient(90deg, rgba(242,163,93,.65) 0 13px, transparent 13px 31px);
|
| 980 |
+
animation: conduit-blackout 3.4s steps(5, end) infinite;
|
| 981 |
+
}
|
| 982 |
+
.app.scenario-slow .c3::after {
|
| 983 |
+
background: linear-gradient(90deg, transparent, var(--orange), transparent);
|
| 984 |
+
animation: signal-burst 3.4s steps(7, end) infinite;
|
| 985 |
+
}
|
| 986 |
+
.app.scenario-slow .core {
|
| 987 |
+
animation: slow-core-beat 3.4s steps(7, end) infinite;
|
| 988 |
+
}
|
| 989 |
+
|
| 990 |
+
.app.scenario-collapse .c2 {
|
| 991 |
+
height: 2px;
|
| 992 |
+
opacity: .24;
|
| 993 |
+
background: repeating-linear-gradient(90deg, rgba(240,107,121,.55) 0 7px, transparent 7px 20px);
|
| 994 |
+
animation: context-disconnect .7s steps(2, end) infinite;
|
| 995 |
+
}
|
| 996 |
+
.app.scenario-collapse .c2::after { display: none; }
|
| 997 |
+
.app.scenario-collapse .core {
|
| 998 |
+
animation: context-core-loss 1.15s steps(5, end) infinite;
|
| 999 |
+
}
|
| 1000 |
+
.app.scenario-collapse .core-rings::after {
|
| 1001 |
+
border-right-color: rgba(240,107,121,.72);
|
| 1002 |
+
opacity: .42;
|
| 1003 |
+
}
|
| 1004 |
+
|
| 1005 |
+
.app.scenario-loop .c3 {
|
| 1006 |
+
height: 2px;
|
| 1007 |
+
background: repeating-linear-gradient(90deg, rgba(240,107,121,.7) 0 9px, transparent 9px 18px);
|
| 1008 |
+
animation: loop-conduit 1.15s steps(4, end) infinite;
|
| 1009 |
+
}
|
| 1010 |
+
.app.scenario-loop .c3::after {
|
| 1011 |
+
background: linear-gradient(90deg, transparent, var(--red), transparent);
|
| 1012 |
+
animation: loop-signal 1.15s steps(5, end) infinite;
|
| 1013 |
+
}
|
| 1014 |
+
.app.scenario-loop .tokens {
|
| 1015 |
+
border-color: rgba(240,107,121,.44);
|
| 1016 |
+
box-shadow: 0 0 30px rgba(240,107,121,.14);
|
| 1017 |
+
}
|
| 1018 |
+
.app.scenario-loop .tokens .particle {
|
| 1019 |
+
background: var(--red);
|
| 1020 |
+
box-shadow: 0 0 10px rgba(240,107,121,.7);
|
| 1021 |
+
animation: loop-token 1.15s steps(5, end) infinite;
|
| 1022 |
+
}
|
| 1023 |
+
.app.scenario-loop .tokens .particle:nth-child(2n) { animation-delay: -.08s; }
|
| 1024 |
+
.app.scenario-loop .tokens .particle:nth-child(3n) { animation-delay: -.17s; }
|
| 1025 |
+
.app.scenario-loop .core {
|
| 1026 |
+
animation: thought-loop-core 1.15s steps(5, end) infinite;
|
| 1027 |
+
}
|
| 1028 |
+
.app.scenario-loop .core-rings::before {
|
| 1029 |
+
border-bottom-color: rgba(240,107,121,.84);
|
| 1030 |
+
border-left-color: rgba(240,107,121,.42);
|
| 1031 |
+
animation-duration: 1.15s;
|
| 1032 |
+
}
|
| 1033 |
+
|
| 1034 |
+
.app.hazard-context-full .observatory-stage::before,
|
| 1035 |
+
.app.hazard-loop .observatory-stage::before {
|
| 1036 |
+
border-color: rgba(240,107,121,.16);
|
| 1037 |
+
box-shadow:
|
| 1038 |
+
inset 0 0 80px rgba(240,107,121,.045),
|
| 1039 |
+
0 0 120px rgba(240,107,121,.045);
|
| 1040 |
+
animation: danger-atmosphere 1.8s ease-in-out infinite;
|
| 1041 |
+
}
|
| 1042 |
+
.app.hazard-context-full .runtime-event {
|
| 1043 |
+
border-color: rgba(242,163,93,.9);
|
| 1044 |
+
color: #fff1dc;
|
| 1045 |
+
background:
|
| 1046 |
+
linear-gradient(90deg, rgba(49,22,8,.94), rgba(76,31,10,.94), rgba(49,22,8,.94));
|
| 1047 |
+
box-shadow:
|
| 1048 |
+
0 0 0 1px rgba(242,163,93,.18),
|
| 1049 |
+
0 0 38px rgba(242,163,93,.24),
|
| 1050 |
+
0 0 92px rgba(240,107,121,.12);
|
| 1051 |
+
}
|
| 1052 |
+
.app.hazard-loop .runtime-event {
|
| 1053 |
+
border-color: rgba(240,107,121,.9);
|
| 1054 |
+
color: #ffe3e7;
|
| 1055 |
+
background:
|
| 1056 |
+
linear-gradient(90deg, rgba(45,7,17,.95), rgba(76,12,26,.94), rgba(45,7,17,.95));
|
| 1057 |
+
box-shadow:
|
| 1058 |
+
0 0 0 1px rgba(240,107,121,.2),
|
| 1059 |
+
0 0 42px rgba(240,107,121,.3),
|
| 1060 |
+
0 0 110px rgba(240,107,121,.16);
|
| 1061 |
+
}
|
| 1062 |
+
.app.hazard-context-full .context {
|
| 1063 |
+
border-color: rgba(242,163,93,.62);
|
| 1064 |
+
box-shadow: 0 0 36px rgba(242,163,93,.12), inset 0 0 28px rgba(242,163,93,.05);
|
| 1065 |
+
}
|
| 1066 |
+
.app.hazard-context-full .context .context-vessel {
|
| 1067 |
+
animation: context-full-shock .74s steps(3, end) infinite;
|
| 1068 |
+
}
|
| 1069 |
+
.app.hazard-context-full .context .context-fill {
|
| 1070 |
+
background: rgba(242,163,93,.24);
|
| 1071 |
+
border-color: rgba(255,211,137,.9);
|
| 1072 |
+
box-shadow: inset -9px 0 16px rgba(242,163,93,.24);
|
| 1073 |
+
}
|
| 1074 |
+
.app.hazard-context-full .core-rings {
|
| 1075 |
+
border-color: rgba(242,163,93,.32);
|
| 1076 |
+
box-shadow: 0 0 46px rgba(242,163,93,.08), inset 0 0 52px rgba(242,163,93,.06);
|
| 1077 |
+
}
|
| 1078 |
+
.app.hazard-context-full .core-rings::after {
|
| 1079 |
+
border-color: rgba(242,163,93,.42);
|
| 1080 |
+
animation: rotate 3.8s linear infinite reverse;
|
| 1081 |
+
}
|
| 1082 |
+
.app.hazard-loop .core-rings {
|
| 1083 |
+
border-color: rgba(240,107,121,.34);
|
| 1084 |
+
box-shadow: 0 0 52px rgba(240,107,121,.1), inset 0 0 52px rgba(240,107,121,.06);
|
| 1085 |
+
}
|
| 1086 |
+
.app.hazard-loop .core-rings::after {
|
| 1087 |
+
border-color: rgba(240,107,121,.46);
|
| 1088 |
+
animation: rotate 2.2s steps(9, end) infinite;
|
| 1089 |
+
}
|
| 1090 |
+
|
| 1091 |
+
/* Memory pressure is relocation, eviction, and refill rather than a color change. */
|
| 1092 |
+
.memory.memory-pressure .memory-blocks {
|
| 1093 |
+
animation: bank-jolt 1.8s steps(4, end) infinite;
|
| 1094 |
+
}
|
| 1095 |
+
.memory.memory-pressure .memory-blocks i:nth-child(1) {
|
| 1096 |
+
animation: relocate-a 1.8s steps(4, end) infinite;
|
| 1097 |
+
}
|
| 1098 |
+
.memory.memory-pressure .memory-blocks i:nth-child(2) {
|
| 1099 |
+
animation: relocate-b 1.8s steps(4, end) infinite;
|
| 1100 |
+
}
|
| 1101 |
+
.memory.memory-pressure .memory-blocks i:nth-child(5) {
|
| 1102 |
+
animation: relocate-c 1.8s steps(4, end) infinite;
|
| 1103 |
+
}
|
| 1104 |
+
.memory.memory-pressure .memory-blocks i:nth-child(8) {
|
| 1105 |
+
animation: relocate-d 1.8s steps(4, end) infinite;
|
| 1106 |
+
}
|
| 1107 |
+
.memory.memory-pressure .memory-blocks i:nth-child(4),
|
| 1108 |
+
.memory.memory-pressure .memory-blocks i:nth-child(10) {
|
| 1109 |
+
animation: cell-blackout 1.8s steps(4, end) infinite;
|
| 1110 |
+
}
|
| 1111 |
+
|
| 1112 |
+
/* Slow decode is an unmistakable stop-burst-stop rhythm. */
|
| 1113 |
+
.tokens.tokens-slow .particle {
|
| 1114 |
+
animation: token-burst 3.4s steps(8, end) infinite;
|
| 1115 |
+
}
|
| 1116 |
+
.tokens.tokens-slow .particle:nth-child(2n) { animation-delay: -.12s; }
|
| 1117 |
+
.tokens.tokens-slow .particle:nth-child(3n) { animation-delay: -.24s; }
|
| 1118 |
+
|
| 1119 |
+
.message.context-fading { opacity: .4; transition: opacity 1s; }
|
| 1120 |
+
.message.outside-context {
|
| 1121 |
+
opacity: .23;
|
| 1122 |
+
filter: grayscale(.8);
|
| 1123 |
+
}
|
| 1124 |
+
.message.context-ejected {
|
| 1125 |
+
animation: context-pop-out 1.05s cubic-bezier(.2, .7, .2, 1) both;
|
| 1126 |
+
}
|
| 1127 |
+
.message.outside-context .bubble {
|
| 1128 |
+
border-style: dashed;
|
| 1129 |
+
}
|
| 1130 |
+
.context-badge {
|
| 1131 |
+
display: none;
|
| 1132 |
+
margin-top: 5px;
|
| 1133 |
+
color: var(--red);
|
| 1134 |
+
font: 8px var(--mono);
|
| 1135 |
+
letter-spacing: .08em;
|
| 1136 |
+
text-transform: uppercase;
|
| 1137 |
+
}
|
| 1138 |
+
.message.outside-context .context-badge { display: block; }
|
| 1139 |
+
.active-context-boundary {
|
| 1140 |
+
display: none;
|
| 1141 |
+
align-items: center;
|
| 1142 |
+
gap: 9px;
|
| 1143 |
+
margin: -2px 0 17px;
|
| 1144 |
+
color: rgba(240, 107, 121, .88);
|
| 1145 |
+
font: 8px var(--mono);
|
| 1146 |
+
letter-spacing: .1em;
|
| 1147 |
+
text-transform: uppercase;
|
| 1148 |
+
}
|
| 1149 |
+
.active-context-boundary::before,
|
| 1150 |
+
.active-context-boundary::after {
|
| 1151 |
+
content: "";
|
| 1152 |
+
height: 1px;
|
| 1153 |
+
flex: 1;
|
| 1154 |
+
background: linear-gradient(90deg, transparent, rgba(240,107,121,.72));
|
| 1155 |
+
}
|
| 1156 |
+
.active-context-boundary::after {
|
| 1157 |
+
background: linear-gradient(90deg, rgba(240,107,121,.72), transparent);
|
| 1158 |
+
}
|
| 1159 |
+
.messages.context-limited .active-context-boundary {
|
| 1160 |
+
display: flex;
|
| 1161 |
+
animation: boundary-arrive .7s ease-out;
|
| 1162 |
+
}
|
| 1163 |
+
.messages.context-limited .historical-context {
|
| 1164 |
+
opacity: .18;
|
| 1165 |
+
filter: grayscale(.9) blur(.25px);
|
| 1166 |
+
transform: scale(.985);
|
| 1167 |
+
transition: opacity .9s, filter .9s, transform .9s;
|
| 1168 |
+
}
|
| 1169 |
+
.messages.context-limited .historical-context .bubble {
|
| 1170 |
+
border-style: dashed;
|
| 1171 |
+
}
|
| 1172 |
+
|
| 1173 |
+
.runtime-event {
|
| 1174 |
+
position: absolute;
|
| 1175 |
+
z-index: 10;
|
| 1176 |
+
left: 50%;
|
| 1177 |
+
top: 50%;
|
| 1178 |
+
min-width: 220px;
|
| 1179 |
+
padding: 12px 16px;
|
| 1180 |
+
transform: translate(-50%, -50%) scale(.9);
|
| 1181 |
+
border: 1px solid rgba(240,107,121,.5);
|
| 1182 |
+
border-radius: 10px;
|
| 1183 |
+
color: #ffdce2;
|
| 1184 |
+
background: rgba(30,8,16,.9);
|
| 1185 |
+
box-shadow: 0 0 42px rgba(240,107,121,.14);
|
| 1186 |
+
font: 10px var(--mono);
|
| 1187 |
+
letter-spacing: .08em;
|
| 1188 |
+
text-align: center;
|
| 1189 |
+
opacity: 0;
|
| 1190 |
+
pointer-events: none;
|
| 1191 |
+
overflow: hidden;
|
| 1192 |
+
}
|
| 1193 |
+
.runtime-event.hazard {
|
| 1194 |
+
min-width: 276px;
|
| 1195 |
+
padding: 14px 20px;
|
| 1196 |
+
font-size: 11px;
|
| 1197 |
+
letter-spacing: .11em;
|
| 1198 |
+
}
|
| 1199 |
+
.runtime-event.critical {
|
| 1200 |
+
transform: translate(-50%, -50%) scale(1.04);
|
| 1201 |
+
}
|
| 1202 |
+
.runtime-event.hazard::before,
|
| 1203 |
+
.runtime-event.hazard::after {
|
| 1204 |
+
content: "";
|
| 1205 |
+
position: absolute;
|
| 1206 |
+
inset: 0;
|
| 1207 |
+
pointer-events: none;
|
| 1208 |
+
}
|
| 1209 |
+
.runtime-event.hazard::before {
|
| 1210 |
+
border-radius: inherit;
|
| 1211 |
+
box-shadow: inset 0 0 20px rgba(255,255,255,.05);
|
| 1212 |
+
}
|
| 1213 |
+
.runtime-event.hazard::after {
|
| 1214 |
+
width: 42%;
|
| 1215 |
+
background: linear-gradient(90deg, transparent, rgba(255,255,255,.18), transparent);
|
| 1216 |
+
transform: skewX(-18deg) translateX(-140%);
|
| 1217 |
+
animation: loop-warning-scan 1.25s linear infinite;
|
| 1218 |
+
}
|
| 1219 |
+
.runtime-event.visible {
|
| 1220 |
+
animation: event-pop 1.55s ease-in-out forwards;
|
| 1221 |
+
}
|
| 1222 |
+
.runtime-event.sticky {
|
| 1223 |
+
opacity: 1;
|
| 1224 |
+
transform: translate(-50%, -50%) scale(1);
|
| 1225 |
+
animation: none;
|
| 1226 |
+
}
|
| 1227 |
+
|
| 1228 |
+
@keyframes fragment {
|
| 1229 |
+
to { transform: translateY(2px) rotate(3deg); opacity: .55; }
|
| 1230 |
+
}
|
| 1231 |
+
@keyframes evict {
|
| 1232 |
+
55% { transform: translateY(-7px) scale(.8); opacity: .08; }
|
| 1233 |
+
72% { transform: translateY(2px) scale(.7); opacity: 0; }
|
| 1234 |
+
100% { transform: none; opacity: 1; }
|
| 1235 |
+
}
|
| 1236 |
+
@keyframes broken-flow {
|
| 1237 |
+
0% { transform: translateX(-25px); opacity: .1; }
|
| 1238 |
+
18%, 42% { transform: translateX(5px); opacity: 1; }
|
| 1239 |
+
43%, 70% { transform: translateX(5px); opacity: .2; }
|
| 1240 |
+
100% { transform: translateX(48px); opacity: .35; }
|
| 1241 |
+
}
|
| 1242 |
+
@keyframes boundary-arrive {
|
| 1243 |
+
from { opacity: 0; transform: scaleX(.72); }
|
| 1244 |
+
to { opacity: 1; transform: scaleX(1); }
|
| 1245 |
+
}
|
| 1246 |
+
@keyframes context-pop-out {
|
| 1247 |
+
0% { opacity: 1; transform: translateX(0) scale(1); filter: none; }
|
| 1248 |
+
42% { opacity: .72; transform: translateX(-10px) scale(.99); filter: grayscale(.2); }
|
| 1249 |
+
100% { opacity: .23; transform: translateX(-20px) scale(.97); filter: grayscale(.8) blur(.25px); }
|
| 1250 |
+
}
|
| 1251 |
+
@keyframes connection-fault {
|
| 1252 |
+
50% { opacity: .28; filter: brightness(1.8); }
|
| 1253 |
+
}
|
| 1254 |
+
@keyframes signal-stutter {
|
| 1255 |
+
0%, 22% { transform: translateX(0); opacity: .1; }
|
| 1256 |
+
35% { transform: translateX(55px); opacity: 1; }
|
| 1257 |
+
36%, 58% { transform: translateX(55px); opacity: .12; }
|
| 1258 |
+
100% { transform: translateX(175px); opacity: .8; }
|
| 1259 |
+
}
|
| 1260 |
+
@keyframes signal-burst {
|
| 1261 |
+
0%, 57% { transform: translateX(0); opacity: 0; }
|
| 1262 |
+
63% { opacity: 1; }
|
| 1263 |
+
78% { transform: translateX(165px); opacity: 1; }
|
| 1264 |
+
79%, 100% { transform: translateX(165px); opacity: 0; }
|
| 1265 |
+
}
|
| 1266 |
+
@keyframes conduit-blackout {
|
| 1267 |
+
0%, 55% { opacity: .12; }
|
| 1268 |
+
60%, 79% { opacity: 1; filter: brightness(1.6); }
|
| 1269 |
+
80%, 100% { opacity: .18; }
|
| 1270 |
+
}
|
| 1271 |
+
@keyframes context-disconnect {
|
| 1272 |
+
50% { opacity: .06; transform: rotate(-27deg) translateX(3px); }
|
| 1273 |
+
}
|
| 1274 |
+
@keyframes memory-core-strain {
|
| 1275 |
+
0%, 100% {
|
| 1276 |
+
transform: translate(-2px, -2px) scale(.92);
|
| 1277 |
+
box-shadow: -18px -15px 34px rgba(242,163,93,.35), 0 0 45px rgba(83,215,247,.18), inset -9px -13px 25px rgba(0,0,0,.6);
|
| 1278 |
+
}
|
| 1279 |
+
24% { transform: translate(3px, -1px) scale(1.1); }
|
| 1280 |
+
42% { transform: translate(-4px, 2px) scale(.86); filter: brightness(.7); }
|
| 1281 |
+
70% { transform: translate(1px, 1px) scale(1.04); filter: brightness(1.25); }
|
| 1282 |
+
}
|
| 1283 |
+
@keyframes slow-core-beat {
|
| 1284 |
+
0%, 55% { transform: scale(.86); opacity: .58; }
|
| 1285 |
+
60% { transform: scale(1.15); opacity: 1; }
|
| 1286 |
+
72% { transform: scale(.94); opacity: .75; }
|
| 1287 |
+
100% { transform: scale(.86); opacity: .58; }
|
| 1288 |
+
}
|
| 1289 |
+
@keyframes context-core-loss {
|
| 1290 |
+
0%, 100% {
|
| 1291 |
+
transform: translate(2px, -2px) scale(.94);
|
| 1292 |
+
box-shadow: 18px -14px 34px rgba(240,107,121,.27), 0 0 38px rgba(83,215,247,.13), inset -9px -13px 25px rgba(0,0,0,.6);
|
| 1293 |
+
}
|
| 1294 |
+
42% { transform: translate(-1px, 2px) scale(.8); opacity: .48; }
|
| 1295 |
+
55% { transform: translate(3px, -3px) scale(1.06); opacity: .9; }
|
| 1296 |
+
}
|
| 1297 |
+
@keyframes bank-jolt {
|
| 1298 |
+
0%, 100% { transform: translateX(0); }
|
| 1299 |
+
28% { transform: translateX(-3px); }
|
| 1300 |
+
32% { transform: translateX(4px); }
|
| 1301 |
+
38% { transform: translateX(-2px); }
|
| 1302 |
+
}
|
| 1303 |
+
@keyframes relocate-a {
|
| 1304 |
+
0%, 16%, 100% { transform: translate(0, 0); opacity: 1; }
|
| 1305 |
+
35%, 58% { transform: translate(34px, 15px) rotate(5deg); opacity: .78; }
|
| 1306 |
+
72% { transform: translate(0, 0); opacity: .35; }
|
| 1307 |
+
}
|
| 1308 |
+
@keyframes relocate-b {
|
| 1309 |
+
0%, 18%, 100% { transform: translate(0, 0); }
|
| 1310 |
+
36%, 62% { transform: translate(68px, 0) rotate(-4deg); opacity: .65; }
|
| 1311 |
+
}
|
| 1312 |
+
@keyframes relocate-c {
|
| 1313 |
+
0%, 22%, 100% { transform: translate(0, 0); }
|
| 1314 |
+
38%, 68% { transform: translate(-67px, 15px) rotate(7deg); opacity: .75; }
|
| 1315 |
+
}
|
| 1316 |
+
@keyframes relocate-d {
|
| 1317 |
+
0%, 20%, 100% { transform: translate(0, 0); }
|
| 1318 |
+
42%, 66% { transform: translate(-35px, -15px) rotate(-6deg); opacity: .7; }
|
| 1319 |
+
}
|
| 1320 |
+
@keyframes cell-blackout {
|
| 1321 |
+
0%, 23%, 100% { opacity: 1; transform: scale(1); }
|
| 1322 |
+
34%, 69% { opacity: .04; transform: scale(.62); background: rgba(3,8,14,.8); }
|
| 1323 |
+
82% { opacity: .55; transform: scale(1.08); }
|
| 1324 |
+
}
|
| 1325 |
+
@keyframes token-burst {
|
| 1326 |
+
0%, 52% { transform: translateX(-18px); opacity: .05; }
|
| 1327 |
+
58% { transform: translateX(-8px); opacity: .9; }
|
| 1328 |
+
72% { transform: translateX(52px); opacity: 1; }
|
| 1329 |
+
76%, 100% { transform: translateX(58px); opacity: .04; }
|
| 1330 |
+
}
|
| 1331 |
+
@keyframes stalled-flow {
|
| 1332 |
+
0%, 70% { transform: translateX(-15px); opacity: .08; }
|
| 1333 |
+
82% { transform: translateX(12px); opacity: .8; }
|
| 1334 |
+
100% { transform: translateX(18px); opacity: .1; }
|
| 1335 |
+
}
|
| 1336 |
+
@keyframes thought-loop-core {
|
| 1337 |
+
0%, 100% {
|
| 1338 |
+
transform: scale(.9);
|
| 1339 |
+
box-shadow: 0 0 32px rgba(240,107,121,.2), inset -9px -13px 25px rgba(0,0,0,.6);
|
| 1340 |
+
}
|
| 1341 |
+
28% { transform: scale(1.12) rotate(3deg); filter: brightness(1.3); }
|
| 1342 |
+
45% { transform: scale(.78) rotate(-4deg); filter: brightness(.65); }
|
| 1343 |
+
64% { transform: scale(1.04) rotate(2deg); }
|
| 1344 |
+
}
|
| 1345 |
+
@keyframes loop-conduit {
|
| 1346 |
+
0%, 100% { opacity: .26; filter: brightness(.8); }
|
| 1347 |
+
30% { opacity: 1; filter: brightness(1.7); }
|
| 1348 |
+
54% { opacity: .08; }
|
| 1349 |
+
}
|
| 1350 |
+
@keyframes loop-signal {
|
| 1351 |
+
0% { transform: translateX(0); opacity: 0; }
|
| 1352 |
+
20% { opacity: 1; }
|
| 1353 |
+
42% { transform: translateX(78px); opacity: 1; }
|
| 1354 |
+
43%, 64% { transform: translateX(78px); opacity: .12; }
|
| 1355 |
+
88% { transform: translateX(18px); opacity: .9; }
|
| 1356 |
+
100% { transform: translateX(0); opacity: .1; }
|
| 1357 |
+
}
|
| 1358 |
+
@keyframes loop-token {
|
| 1359 |
+
0%, 45% { transform: translateX(-16px); opacity: .08; }
|
| 1360 |
+
55% { transform: translateX(46px); opacity: 1; }
|
| 1361 |
+
70% { transform: translateX(20px); opacity: .55; }
|
| 1362 |
+
100% { transform: translateX(46px); opacity: .16; }
|
| 1363 |
+
}
|
| 1364 |
+
@keyframes recovery {
|
| 1365 |
+
0% { transform: scale(.75); opacity: .45; }
|
| 1366 |
+
55% { transform: scale(1.08); opacity: 1; }
|
| 1367 |
+
100% { transform: scale(.92); opacity: .75; }
|
| 1368 |
+
}
|
| 1369 |
+
@keyframes recovery-bars {
|
| 1370 |
+
50% { height: 15%; opacity: .28; }
|
| 1371 |
+
}
|
| 1372 |
+
@keyframes event-pop {
|
| 1373 |
+
0% { opacity: 0; transform: translate(-50%, -50%) scale(.8); }
|
| 1374 |
+
18%, 72% { opacity: 1; transform: translate(-50%, -50%) scale(1); }
|
| 1375 |
+
100% { opacity: 0; transform: translate(-50%, -50%) scale(1.04); }
|
| 1376 |
+
}
|
| 1377 |
+
@keyframes danger-atmosphere {
|
| 1378 |
+
0%, 100% {
|
| 1379 |
+
opacity: 1;
|
| 1380 |
+
transform: scale(1);
|
| 1381 |
+
}
|
| 1382 |
+
50% {
|
| 1383 |
+
opacity: .58;
|
| 1384 |
+
transform: scale(.985);
|
| 1385 |
+
}
|
| 1386 |
+
}
|
| 1387 |
+
@keyframes context-full-shock {
|
| 1388 |
+
0%, 100% {
|
| 1389 |
+
transform: scaleX(1);
|
| 1390 |
+
filter: brightness(1);
|
| 1391 |
+
}
|
| 1392 |
+
36% {
|
| 1393 |
+
transform: scaleX(.955) translateX(-2px);
|
| 1394 |
+
filter: brightness(1.35);
|
| 1395 |
+
}
|
| 1396 |
+
62% {
|
| 1397 |
+
transform: scaleX(1.015) translateX(1px);
|
| 1398 |
+
filter: brightness(.9);
|
| 1399 |
+
}
|
| 1400 |
+
}
|
| 1401 |
+
@keyframes loop-warning-scan {
|
| 1402 |
+
to { transform: skewX(-18deg) translateX(330%); }
|
| 1403 |
+
}
|
| 1404 |
+
@keyframes pressure {
|
| 1405 |
+
50% { transform: scaleX(.97); border-color: rgba(242, 163, 93, .42); }
|
| 1406 |
+
}
|
| 1407 |
+
@keyframes shake {
|
| 1408 |
+
25% { transform: translateX(-1px); }
|
| 1409 |
+
75% { transform: translateX(1px); }
|
| 1410 |
+
}
|
open_cortex/ui/assets/open_cortex.js
ADDED
|
@@ -0,0 +1,738 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const app = document.getElementById("app");
|
| 2 |
+
const phase = document.getElementById("phase");
|
| 3 |
+
const coreState = document.getElementById("core-state");
|
| 4 |
+
const coreDetail = document.getElementById("core-detail");
|
| 5 |
+
const kv = document.getElementById("kv");
|
| 6 |
+
const contextUsed = document.getElementById("context-used");
|
| 7 |
+
const contextUnit = document.getElementById("context-unit");
|
| 8 |
+
const contextFill = document.getElementById("context-fill");
|
| 9 |
+
const tps = document.getElementById("tps");
|
| 10 |
+
const ttft = document.getElementById("ttft");
|
| 11 |
+
const memoryState = document.getElementById("memory-state");
|
| 12 |
+
const contextState = document.getElementById("context-state");
|
| 13 |
+
const tokenState = document.getElementById("token-state");
|
| 14 |
+
const healthState = document.getElementById("health-state");
|
| 15 |
+
const scenarioLabel = document.getElementById("scenario-label");
|
| 16 |
+
const memoryBlocks = [...document.querySelectorAll("#memory-blocks i")];
|
| 17 |
+
const messages = document.getElementById("messages");
|
| 18 |
+
const runtimeEvent = document.getElementById("runtime-event");
|
| 19 |
+
const evidence3Label = document.getElementById("evidence-3-label");
|
| 20 |
+
const evidence3Value = document.getElementById("kv-retained");
|
| 21 |
+
const promptEval = document.getElementById("prompt-eval");
|
| 22 |
+
const prompt = document.getElementById("prompt");
|
| 23 |
+
const send = document.getElementById("send");
|
| 24 |
+
const experiment = document.getElementById("experiment");
|
| 25 |
+
const runExperimentButton = document.getElementById("run-experiment");
|
| 26 |
+
const workspace = document.querySelector(".workspace");
|
| 27 |
+
const resizer = document.getElementById("resizer");
|
| 28 |
+
const drawerToggle = document.getElementById("drawer-toggle");
|
| 29 |
+
const memoryOrgan = document.getElementById("memory-organ");
|
| 30 |
+
const contextOrgan = document.getElementById("context-organ");
|
| 31 |
+
const tokensOrgan = document.getElementById("tokens-organ");
|
| 32 |
+
const engineOrgan = document.getElementById("engine-organ");
|
| 33 |
+
|
| 34 |
+
let chatHistory = [];
|
| 35 |
+
let activeAssistant = null;
|
| 36 |
+
let previousChatWidth = workspace.getBoundingClientRect().width * 0.4;
|
| 37 |
+
let pendingChatWidth = previousChatWidth;
|
| 38 |
+
let collapseHoldUntil = 0;
|
| 39 |
+
let contextWindowFullPending = false;
|
| 40 |
+
const collapseThreshold = 230;
|
| 41 |
+
|
| 42 |
+
function finishActiveAssistant() {
|
| 43 |
+
if (activeAssistant) {
|
| 44 |
+
activeAssistant.isStreaming = false;
|
| 45 |
+
}
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
function clearHazardState() {
|
| 49 |
+
app.classList.remove("hazard-loop", "hazard-context-full");
|
| 50 |
+
runtimeEvent.classList.remove("hazard", "critical");
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
function setHazardState(kind, critical = false) {
|
| 54 |
+
clearHazardState();
|
| 55 |
+
app.classList.add(kind);
|
| 56 |
+
runtimeEvent.classList.add("hazard");
|
| 57 |
+
runtimeEvent.classList.toggle("critical", critical);
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
function showRuntimeEvent(label, sticky = false) {
|
| 61 |
+
runtimeEvent.textContent = label;
|
| 62 |
+
runtimeEvent.classList.remove("visible", "sticky");
|
| 63 |
+
void runtimeEvent.offsetWidth;
|
| 64 |
+
runtimeEvent.classList.add("visible");
|
| 65 |
+
runtimeEvent.classList.toggle("sticky", sticky);
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
function escapeHtml(value) {
|
| 69 |
+
return value
|
| 70 |
+
.replaceAll("&", "&")
|
| 71 |
+
.replaceAll("<", "<")
|
| 72 |
+
.replaceAll(">", ">")
|
| 73 |
+
.replaceAll('"', """)
|
| 74 |
+
.replaceAll("'", "'");
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
function renderInlineMarkdown(value) {
|
| 78 |
+
return escapeHtml(value)
|
| 79 |
+
.replace(/`([^`]+)`/g, "<code>$1</code>")
|
| 80 |
+
.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
function renderMarkdown(value) {
|
| 84 |
+
const lines = value.replace(/\r\n/g, "\n").split("\n");
|
| 85 |
+
const html = [];
|
| 86 |
+
let paragraph = [];
|
| 87 |
+
let list = [];
|
| 88 |
+
let inCode = false;
|
| 89 |
+
let codeLines = [];
|
| 90 |
+
let codeLang = "";
|
| 91 |
+
|
| 92 |
+
function flushParagraph() {
|
| 93 |
+
if (!paragraph.length) return;
|
| 94 |
+
html.push(`<p>${paragraph.map(renderInlineMarkdown).join("<br>")}</p>`);
|
| 95 |
+
paragraph = [];
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
function flushList() {
|
| 99 |
+
if (!list.length) return;
|
| 100 |
+
html.push(`<ul>${list.map((item) => `<li>${renderInlineMarkdown(item)}</li>`).join("")}</ul>`);
|
| 101 |
+
list = [];
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
for (const line of lines) {
|
| 105 |
+
const codeFence = line.match(/^```(\w+)?\s*$/);
|
| 106 |
+
if (codeFence) {
|
| 107 |
+
if (inCode) {
|
| 108 |
+
html.push(`<pre><code${codeLang ? ` data-lang="${escapeHtml(codeLang)}"` : ""}>${escapeHtml(codeLines.join("\n"))}</code></pre>`);
|
| 109 |
+
inCode = false;
|
| 110 |
+
codeLines = [];
|
| 111 |
+
codeLang = "";
|
| 112 |
+
} else {
|
| 113 |
+
flushParagraph();
|
| 114 |
+
flushList();
|
| 115 |
+
inCode = true;
|
| 116 |
+
codeLang = codeFence[1] || "";
|
| 117 |
+
}
|
| 118 |
+
continue;
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
if (inCode) {
|
| 122 |
+
codeLines.push(line);
|
| 123 |
+
continue;
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
if (!line.trim()) {
|
| 127 |
+
flushParagraph();
|
| 128 |
+
flushList();
|
| 129 |
+
continue;
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
const heading = line.match(/^(#{1,3})\s+(.+)$/);
|
| 133 |
+
if (heading) {
|
| 134 |
+
flushParagraph();
|
| 135 |
+
flushList();
|
| 136 |
+
const level = heading[1].length;
|
| 137 |
+
html.push(`<h${level}>${renderInlineMarkdown(heading[2])}</h${level}>`);
|
| 138 |
+
continue;
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
const bullet = line.match(/^\s*[-*]\s+(.+)$/);
|
| 142 |
+
if (bullet) {
|
| 143 |
+
flushParagraph();
|
| 144 |
+
list.push(bullet[1]);
|
| 145 |
+
continue;
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
paragraph.push(line);
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
if (inCode) {
|
| 152 |
+
html.push(`<pre><code${codeLang ? ` data-lang="${escapeHtml(codeLang)}"` : ""}>${escapeHtml(codeLines.join("\n"))}</code></pre>`);
|
| 153 |
+
}
|
| 154 |
+
flushParagraph();
|
| 155 |
+
flushList();
|
| 156 |
+
return html.join("");
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
function renderMessages() {
|
| 160 |
+
if (!chatHistory.length) {
|
| 161 |
+
messages.classList.remove("context-limited");
|
| 162 |
+
messages.innerHTML = `
|
| 163 |
+
<div class="message">
|
| 164 |
+
<div class="message-meta">Cortex</div>
|
| 165 |
+
<div class="bubble">Send a message to begin observing local inference.</div>
|
| 166 |
+
</div>
|
| 167 |
+
`;
|
| 168 |
+
return;
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
const hasCollapsedContext = chatHistory.some((message) => message.outsideContext);
|
| 172 |
+
messages.classList.toggle("context-limited", hasCollapsedContext);
|
| 173 |
+
let boundaryInserted = false;
|
| 174 |
+
|
| 175 |
+
messages.innerHTML = chatHistory.map((message, index) => {
|
| 176 |
+
const role = message.role === "user" ? "user" : "";
|
| 177 |
+
const live = message.role === "assistant" && message.isStreaming ? " assistant-live" : "";
|
| 178 |
+
const outside = message.outsideContext ? " historical-context outside-context" : "";
|
| 179 |
+
const ejected = message.contextEjected ? " context-ejected" : "";
|
| 180 |
+
const author = message.role === "user" ? "You" : "Cortex";
|
| 181 |
+
const cursor = live ? '<span class="cursor"></span>' : "";
|
| 182 |
+
const boundary = !boundaryInserted && hasCollapsedContext && !message.outsideContext
|
| 183 |
+
? '<div class="active-context-boundary"><span>Active context boundary · outside active context above</span></div>'
|
| 184 |
+
: "";
|
| 185 |
+
if (boundary) boundaryInserted = true;
|
| 186 |
+
const body = message.role === "assistant" ? renderMarkdown(message.content) : escapeHtml(message.content);
|
| 187 |
+
return `
|
| 188 |
+
${boundary}
|
| 189 |
+
<div class="message ${role}${live}${outside}${ejected}">
|
| 190 |
+
<div class="message-meta">${author}</div>
|
| 191 |
+
<div class="bubble markdown-body">${body}${cursor}</div>
|
| 192 |
+
${message.outsideContext ? '<div class="context-badge">outside active context</div>' : ""}
|
| 193 |
+
</div>
|
| 194 |
+
`;
|
| 195 |
+
}).join("");
|
| 196 |
+
messages.scrollTop = messages.scrollHeight;
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
function setPhase(name, label, core, detail) {
|
| 200 |
+
const preserveCollapse = app.classList.contains("scenario-collapse") && Date.now() < collapseHoldUntil;
|
| 201 |
+
const removableClasses = [
|
| 202 |
+
"phase-idle",
|
| 203 |
+
"phase-prefill",
|
| 204 |
+
"phase-decode",
|
| 205 |
+
"phase-recovery",
|
| 206 |
+
"scenario-loop",
|
| 207 |
+
"scenario-memory",
|
| 208 |
+
"scenario-slow",
|
| 209 |
+
"scenario-collapse",
|
| 210 |
+
"hazard-loop",
|
| 211 |
+
"hazard-context-full"
|
| 212 |
+
].filter((className) => !(preserveCollapse && className === "scenario-collapse"));
|
| 213 |
+
|
| 214 |
+
app.classList.remove(...removableClasses);
|
| 215 |
+
app.classList.add("phase-" + name);
|
| 216 |
+
phase.textContent = label;
|
| 217 |
+
coreState.textContent = core;
|
| 218 |
+
coreDetail.textContent = detail;
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
function resetRuntimeVisuals() {
|
| 222 |
+
app.classList.remove(
|
| 223 |
+
"scenario-loop",
|
| 224 |
+
"scenario-memory",
|
| 225 |
+
"scenario-slow",
|
| 226 |
+
"scenario-collapse",
|
| 227 |
+
"core-slow",
|
| 228 |
+
"hazard-loop",
|
| 229 |
+
"hazard-context-full"
|
| 230 |
+
);
|
| 231 |
+
clearHazardState();
|
| 232 |
+
collapseHoldUntil = 0;
|
| 233 |
+
contextWindowFullPending = false;
|
| 234 |
+
memoryOrgan.className = "organ memory active";
|
| 235 |
+
contextOrgan.className = "organ context active";
|
| 236 |
+
tokensOrgan.className = "organ tokens active";
|
| 237 |
+
engineOrgan.className = "organ health active";
|
| 238 |
+
runtimeEvent.classList.remove("visible", "sticky");
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
function setMetrics({ contextTokens, contextSize, speed, firstToken, promptRate }) {
|
| 242 |
+
const used = contextTokens == null ? "—" : Number(contextTokens).toLocaleString();
|
| 243 |
+
const size = contextSize == null ? "—" : Number(contextSize).toLocaleString();
|
| 244 |
+
contextUsed.textContent = used;
|
| 245 |
+
contextUnit.textContent = `/ ${size}`;
|
| 246 |
+
const fill = contextTokens && contextSize ? Math.min(100, Math.round(contextTokens / contextSize * 100)) : 0;
|
| 247 |
+
contextFill.style.width = fill + "%";
|
| 248 |
+
kv.textContent = contextTokens && contextSize ? String(fill) : "—";
|
| 249 |
+
tps.textContent = speed == null ? "—" : Number(speed).toFixed(1);
|
| 250 |
+
ttft.textContent = firstToken == null ? "—" : Number(firstToken).toFixed(0);
|
| 251 |
+
promptEval.textContent = promptRate == null ? "measuring" : `${Number(promptRate).toFixed(1)} tok/s`;
|
| 252 |
+
|
| 253 |
+
const filled = Math.max(1, Math.min(12, Math.round(fill / 100 * memoryBlocks.length)));
|
| 254 |
+
memoryBlocks.forEach((block, index) => block.classList.toggle("filled", index < filled));
|
| 255 |
+
|
| 256 |
+
memoryOrgan.classList.toggle("memory-pressure", fill >= 70 && fill < 85);
|
| 257 |
+
memoryOrgan.classList.toggle("memory-overloaded", fill >= 85);
|
| 258 |
+
contextOrgan.classList.toggle("context-pressure", fill >= 70);
|
| 259 |
+
|
| 260 |
+
if (fill >= 85) {
|
| 261 |
+
memoryState.textContent = "Memory overloaded";
|
| 262 |
+
contextState.textContent = "Near context limit";
|
| 263 |
+
} else if (fill >= 70) {
|
| 264 |
+
memoryState.textContent = "Memory strained";
|
| 265 |
+
contextState.textContent = "Context pressure";
|
| 266 |
+
} else if (fill >= 45) {
|
| 267 |
+
memoryState.textContent = "Memory busy";
|
| 268 |
+
contextState.textContent = "Context growing";
|
| 269 |
+
}
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
function setRuntimeIdle() {
|
| 273 |
+
resetRuntimeVisuals();
|
| 274 |
+
scenarioLabel.textContent = "Live local chat";
|
| 275 |
+
setPhase("idle", "IDLE", "Ready for input", "ENGINE IDLE · SLOT AVAILABLE");
|
| 276 |
+
runtimeEvent.textContent = "READY";
|
| 277 |
+
memoryState.textContent = "Memory quiet";
|
| 278 |
+
contextState.textContent = "Context resting";
|
| 279 |
+
tokenState.textContent = "Stream dormant";
|
| 280 |
+
healthState.textContent = "Engine healthy";
|
| 281 |
+
kv.textContent = "—";
|
| 282 |
+
setMetrics({ contextTokens: 0, contextSize: null, speed: 0, firstToken: null, promptRate: null });
|
| 283 |
+
evidence3Label.textContent = "KV evidence";
|
| 284 |
+
evidence3Value.textContent = "context proxy";
|
| 285 |
+
}
|
| 286 |
+
|
| 287 |
+
function applyRuntimeEvent(event) {
|
| 288 |
+
if (event.kind === "context_collapse") {
|
| 289 |
+
applyContextCollapse(event);
|
| 290 |
+
return;
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
if (event.kind === "request_started") {
|
| 294 |
+
clearHazardState();
|
| 295 |
+
scenarioLabel.textContent = "Live local chat";
|
| 296 |
+
setPhase("prefill", "PREFILL", "Waiting for first token", "PREFILL · PROCESSING PROMPT");
|
| 297 |
+
showRuntimeEvent("PREFILL STARTED");
|
| 298 |
+
memoryState.textContent = "Memory allocating";
|
| 299 |
+
contextState.textContent = "Context pending";
|
| 300 |
+
tokenState.textContent = "Stream waiting";
|
| 301 |
+
healthState.textContent = "Engine charging";
|
| 302 |
+
kv.textContent = "—";
|
| 303 |
+
evidence3Label.textContent = "KV proxy";
|
| 304 |
+
evidence3Value.textContent = "waiting for context";
|
| 305 |
+
return;
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
if (event.kind === "first_token") {
|
| 309 |
+
const snapshot = event.snapshot || {};
|
| 310 |
+
const contextTokens = event.context_tokens ?? snapshot.slot_context_tokens?.[0] ?? null;
|
| 311 |
+
const contextSize = event.context_size ?? snapshot.slot_context_size ?? null;
|
| 312 |
+
setPhase("decode", "DECODE", "Generating response", `TTFT ${Number(event.ttft_ms || 0).toFixed(1)} ms · LIVE`);
|
| 313 |
+
showRuntimeEvent("FIRST TOKEN");
|
| 314 |
+
memoryState.textContent = "Memory holding";
|
| 315 |
+
contextState.textContent = "Context active";
|
| 316 |
+
tokenState.textContent = "Decode flowing";
|
| 317 |
+
healthState.textContent = `processing=${snapshot.requests_processing ?? "—"} deferred=${snapshot.requests_deferred ?? "—"}`;
|
| 318 |
+
setMetrics({
|
| 319 |
+
contextTokens,
|
| 320 |
+
contextSize,
|
| 321 |
+
speed: snapshot.decode_tps,
|
| 322 |
+
firstToken: event.ttft_ms,
|
| 323 |
+
promptRate: snapshot.prompt_tps
|
| 324 |
+
});
|
| 325 |
+
evidence3Label.textContent = "KV proxy";
|
| 326 |
+
evidence3Value.textContent = event.working_memory_percent == null ? "context-derived" : `${event.working_memory_percent.toFixed(1)}% context-derived`;
|
| 327 |
+
return;
|
| 328 |
+
}
|
| 329 |
+
|
| 330 |
+
if (event.kind === "token") {
|
| 331 |
+
if (event.repetition_detected) {
|
| 332 |
+
app.classList.add("scenario-loop");
|
| 333 |
+
setHazardState("hazard-loop", true);
|
| 334 |
+
showRuntimeEvent("THOUGHT LOOP DETECTED", true);
|
| 335 |
+
coreState.textContent = "Thought loop emerging";
|
| 336 |
+
tokenState.textContent = `Thought loop · ${event.live_tps ? event.live_tps.toFixed(1) : "—"} tok/s`;
|
| 337 |
+
healthState.textContent = "Repetition detected";
|
| 338 |
+
evidence3Label.textContent = "Generation pattern";
|
| 339 |
+
evidence3Value.textContent = "thought loop detected";
|
| 340 |
+
} else {
|
| 341 |
+
runtimeEvent.textContent = "TOKEN STREAM";
|
| 342 |
+
coreState.textContent = "Generating response";
|
| 343 |
+
tokenState.textContent = `Decode flowing · ${event.live_tps ? event.live_tps.toFixed(1) : "—"} tok/s`;
|
| 344 |
+
healthState.textContent = "Engine active";
|
| 345 |
+
evidence3Label.textContent = "Generation pattern";
|
| 346 |
+
evidence3Value.textContent = "no repetition detected";
|
| 347 |
+
}
|
| 348 |
+
phase.textContent = "DECODE";
|
| 349 |
+
coreDetail.textContent = `DECODE TOKEN ${event.generated_tokens || "—"} · LIVE`;
|
| 350 |
+
setMetrics({
|
| 351 |
+
contextTokens: event.context_tokens,
|
| 352 |
+
contextSize: event.context_size,
|
| 353 |
+
speed: event.live_tps,
|
| 354 |
+
firstToken: Number(ttft.textContent) || null,
|
| 355 |
+
promptRate: null
|
| 356 |
+
});
|
| 357 |
+
promptEval.textContent = `${event.generated_tokens || 0} generated tokens`;
|
| 358 |
+
if (event.working_memory_percent != null) {
|
| 359 |
+
evidence3Label.textContent = "KV proxy";
|
| 360 |
+
evidence3Value.textContent = `${event.working_memory_percent.toFixed(1)}% context-derived`;
|
| 361 |
+
}
|
| 362 |
+
return;
|
| 363 |
+
}
|
| 364 |
+
|
| 365 |
+
if (event.kind === "request_completed") {
|
| 366 |
+
finishActiveAssistant();
|
| 367 |
+
const contextFull = event.context_size && event.context_tokens >= event.context_size;
|
| 368 |
+
phase.textContent = "DONE";
|
| 369 |
+
showRuntimeEvent(
|
| 370 |
+
contextFull ? "CONTEXT WINDOW FULL" : (event.repetition_detected ? "THOUGHT LOOP DETECTED" : "REQUEST COMPLETE"),
|
| 371 |
+
contextFull || event.repetition_detected
|
| 372 |
+
);
|
| 373 |
+
coreState.textContent = contextFull
|
| 374 |
+
? "Context full; next turn will evict earliest memory"
|
| 375 |
+
: (event.repetition_detected ? "Generation ended in a loop" : "Generation complete");
|
| 376 |
+
coreDetail.textContent = "FINAL TIMINGS RECEIVED";
|
| 377 |
+
setMetrics({
|
| 378 |
+
contextTokens: event.context_tokens,
|
| 379 |
+
contextSize: event.context_size,
|
| 380 |
+
speed: event.decode_tps,
|
| 381 |
+
firstToken: Number(ttft.textContent) || null,
|
| 382 |
+
promptRate: event.prompt_tps
|
| 383 |
+
});
|
| 384 |
+
tokenState.textContent = `Decode ${event.decode_tps ? event.decode_tps.toFixed(1) : "—"} tok/s`;
|
| 385 |
+
healthState.textContent = "Engine settled";
|
| 386 |
+
promptEval.textContent = `Prefill ${event.prompt_tps ? event.prompt_tps.toFixed(1) : "—"} tok/s`;
|
| 387 |
+
evidence3Label.textContent = "Output tokens";
|
| 388 |
+
evidence3Value.textContent = event.completion_tokens ?? "—";
|
| 389 |
+
if (contextFull) {
|
| 390 |
+
applyContextFullWarning();
|
| 391 |
+
} else {
|
| 392 |
+
contextState.textContent = event.repetition_detected ? "Context saturated by loop" : "Context retained";
|
| 393 |
+
}
|
| 394 |
+
renderMessages();
|
| 395 |
+
}
|
| 396 |
+
}
|
| 397 |
+
|
| 398 |
+
function applyContextFullWarning() {
|
| 399 |
+
contextWindowFullPending = true;
|
| 400 |
+
collapseHoldUntil = Date.now() + 2500;
|
| 401 |
+
scenarioLabel.textContent = "Context full";
|
| 402 |
+
setHazardState("hazard-context-full", true);
|
| 403 |
+
app.classList.add("scenario-collapse");
|
| 404 |
+
contextOrgan.classList.add("context-pressure");
|
| 405 |
+
memoryOrgan.classList.add("memory-overloaded");
|
| 406 |
+
engineOrgan.classList.add("engine-strained");
|
| 407 |
+
runtimeEvent.textContent = "CONTEXT WINDOW FULL";
|
| 408 |
+
runtimeEvent.classList.add("visible", "sticky");
|
| 409 |
+
memoryState.textContent = "Memory overloaded";
|
| 410 |
+
contextState.textContent = "Next turn will evict earliest message";
|
| 411 |
+
tokenState.textContent = "Stream complete";
|
| 412 |
+
healthState.textContent = "Awaiting next turn";
|
| 413 |
+
evidence3Label.textContent = "Next action";
|
| 414 |
+
evidence3Value.textContent = "pop earliest turn";
|
| 415 |
+
}
|
| 416 |
+
|
| 417 |
+
function applyDeferredContextEjection() {
|
| 418 |
+
if (!contextWindowFullPending) return false;
|
| 419 |
+
|
| 420 |
+
const target = chatHistory.find((message) => !message.outsideContext);
|
| 421 |
+
if (!target) return false;
|
| 422 |
+
|
| 423 |
+
target.outsideContext = true;
|
| 424 |
+
target.contextEjected = true;
|
| 425 |
+
contextWindowFullPending = false;
|
| 426 |
+
clearHazardState();
|
| 427 |
+
collapseHoldUntil = Date.now() + 2500;
|
| 428 |
+
scenarioLabel.textContent = "Context collapse";
|
| 429 |
+
setPhase("decode", "COLLAPSE", "Context boundary shifted", "EARLIEST TURN OUTSIDE ACTIVE MEMORY");
|
| 430 |
+
app.classList.add("scenario-collapse");
|
| 431 |
+
showRuntimeEvent("CONTEXT COLLAPSE", true);
|
| 432 |
+
memoryState.textContent = "Memory rebalanced";
|
| 433 |
+
contextState.textContent = "Earliest message ejected";
|
| 434 |
+
tokenState.textContent = "Sending recent context";
|
| 435 |
+
healthState.textContent = "Engine recovering";
|
| 436 |
+
evidence3Label.textContent = "Dropped turns";
|
| 437 |
+
evidence3Value.textContent = "1";
|
| 438 |
+
return true;
|
| 439 |
+
}
|
| 440 |
+
|
| 441 |
+
function applyContextCollapse(event) {
|
| 442 |
+
const dropped = Math.max(1, Number(event.dropped_messages || 1));
|
| 443 |
+
let marked = 0;
|
| 444 |
+
for (const message of chatHistory) {
|
| 445 |
+
if (message === activeAssistant) continue;
|
| 446 |
+
if (marked >= dropped) break;
|
| 447 |
+
message.outsideContext = true;
|
| 448 |
+
message.contextEjected = marked === 0;
|
| 449 |
+
marked += 1;
|
| 450 |
+
}
|
| 451 |
+
|
| 452 |
+
collapseHoldUntil = Date.now() + 2500;
|
| 453 |
+
clearHazardState();
|
| 454 |
+
scenarioLabel.textContent = "Context collapse";
|
| 455 |
+
setPhase("decode", "COLLAPSE", "Context boundary shifted", "EARLIEST TURNS OUTSIDE ACTIVE MEMORY");
|
| 456 |
+
app.classList.add("scenario-collapse");
|
| 457 |
+
showRuntimeEvent("CONTEXT COLLAPSE", true);
|
| 458 |
+
memoryState.textContent = "Memory rebalanced";
|
| 459 |
+
contextState.textContent = "Earliest turns forgotten";
|
| 460 |
+
tokenState.textContent = "Retrying with recent context";
|
| 461 |
+
healthState.textContent = "Recovered locally";
|
| 462 |
+
contextUsed.textContent = "recent";
|
| 463 |
+
contextUnit.textContent = event.context_size ? `/ ${event.context_size}` : "/ active";
|
| 464 |
+
contextFill.style.width = "58%";
|
| 465 |
+
kv.textContent = "58";
|
| 466 |
+
evidence3Label.textContent = "Dropped turns";
|
| 467 |
+
evidence3Value.textContent = String(event.dropped_messages ?? marked);
|
| 468 |
+
|
| 469 |
+
if (activeAssistant && !activeAssistant.content) {
|
| 470 |
+
activeAssistant.content = "Earlier turns fell outside the active context.\n\n";
|
| 471 |
+
}
|
| 472 |
+
renderMessages();
|
| 473 |
+
}
|
| 474 |
+
|
| 475 |
+
function seedExperimentMessages({ outside = false } = {}) {
|
| 476 |
+
chatHistory = [
|
| 477 |
+
{
|
| 478 |
+
role: "user",
|
| 479 |
+
content: "Why does a long conversation make the model feel slower?",
|
| 480 |
+
outsideContext: outside
|
| 481 |
+
},
|
| 482 |
+
{
|
| 483 |
+
role: "assistant",
|
| 484 |
+
content: "Each new turn carries more history back into the model, increasing prefill work before decoding begins.",
|
| 485 |
+
outsideContext: outside
|
| 486 |
+
},
|
| 487 |
+
{
|
| 488 |
+
role: "user",
|
| 489 |
+
content: "Can you show me what changes inside the runtime?"
|
| 490 |
+
},
|
| 491 |
+
{
|
| 492 |
+
role: "assistant",
|
| 493 |
+
content: "Watch the runtime organs: context fills, working memory holds KV blocks, and token flow reveals decode health."
|
| 494 |
+
}
|
| 495 |
+
];
|
| 496 |
+
activeAssistant = chatHistory.at(-1);
|
| 497 |
+
renderMessages();
|
| 498 |
+
}
|
| 499 |
+
|
| 500 |
+
function runLongContextExperiment() {
|
| 501 |
+
resetRuntimeVisuals();
|
| 502 |
+
seedExperimentMessages();
|
| 503 |
+
scenarioLabel.textContent = "Long context stress";
|
| 504 |
+
setPhase("prefill", "PREFILL", "Reading long context", "TTFT RISING · CONTEXT PRESSURE");
|
| 505 |
+
showRuntimeEvent("LONG CONTEXT STRESS", true);
|
| 506 |
+
contextOrgan.classList.add("context-pressure");
|
| 507 |
+
engineOrgan.classList.add("engine-strained");
|
| 508 |
+
memoryState.textContent = "Memory busy";
|
| 509 |
+
contextState.textContent = "Near context limit";
|
| 510 |
+
tokenState.textContent = "Stream waiting";
|
| 511 |
+
healthState.textContent = "Engine strained by prefill";
|
| 512 |
+
setMetrics({ contextTokens: 1840, contextSize: 2048, speed: 0, firstToken: 1380, promptRate: 38.6 });
|
| 513 |
+
evidence3Label.textContent = "Experiment";
|
| 514 |
+
evidence3Value.textContent = "simulated long prompt";
|
| 515 |
+
}
|
| 516 |
+
|
| 517 |
+
function runMemoryPressureExperiment() {
|
| 518 |
+
resetRuntimeVisuals();
|
| 519 |
+
seedExperimentMessages();
|
| 520 |
+
scenarioLabel.textContent = "Memory pressure";
|
| 521 |
+
app.classList.add("scenario-memory");
|
| 522 |
+
setPhase("decode", "STRAINED", "KV blocks reallocating", "WORKING SET PRESSURE · SIMULATED");
|
| 523 |
+
showRuntimeEvent("MEMORY PRESSURE", true);
|
| 524 |
+
memoryOrgan.classList.add("memory-pressure");
|
| 525 |
+
engineOrgan.classList.add("engine-strained");
|
| 526 |
+
memoryState.textContent = "KV blocks fragmenting";
|
| 527 |
+
contextState.textContent = "Context stable";
|
| 528 |
+
tokenState.textContent = "Decode flowing";
|
| 529 |
+
healthState.textContent = "Engine strained";
|
| 530 |
+
setMetrics({ contextTokens: 1240, contextSize: 2048, speed: 24.8, firstToken: 648, promptRate: 72.4 });
|
| 531 |
+
kv.textContent = "91";
|
| 532 |
+
evidence3Label.textContent = "KV proxy";
|
| 533 |
+
evidence3Value.textContent = "simulated pressure";
|
| 534 |
+
}
|
| 535 |
+
|
| 536 |
+
function runSlowDecodeExperiment() {
|
| 537 |
+
resetRuntimeVisuals();
|
| 538 |
+
seedExperimentMessages();
|
| 539 |
+
scenarioLabel.textContent = "Slow decode";
|
| 540 |
+
app.classList.add("scenario-slow", "core-slow");
|
| 541 |
+
setPhase("decode", "DECODE", "Engine decoding slowly", "TOKEN FLOW INTERRUPTED · SIMULATED");
|
| 542 |
+
showRuntimeEvent("SLOW DECODE", true);
|
| 543 |
+
tokensOrgan.classList.add("tokens-slow");
|
| 544 |
+
engineOrgan.classList.add("engine-strained");
|
| 545 |
+
memoryState.textContent = "Memory healthy";
|
| 546 |
+
contextState.textContent = "Context stable";
|
| 547 |
+
tokenState.textContent = "Flow interrupted";
|
| 548 |
+
healthState.textContent = "Decode scheduler strained";
|
| 549 |
+
setMetrics({ contextTokens: 980, contextSize: 2048, speed: 6.8, firstToken: 510, promptRate: 86.2 });
|
| 550 |
+
evidence3Label.textContent = "Experiment";
|
| 551 |
+
evidence3Value.textContent = "simulated decode delay";
|
| 552 |
+
}
|
| 553 |
+
|
| 554 |
+
function runContextCollapseExperiment() {
|
| 555 |
+
resetRuntimeVisuals();
|
| 556 |
+
seedExperimentMessages({ outside: true });
|
| 557 |
+
scenarioLabel.textContent = "Context collapse";
|
| 558 |
+
setPhase("recovery", "COLLAPSE", "Engine recovering", "EARLIEST MEMORY OUTSIDE ACTIVE WINDOW");
|
| 559 |
+
setHazardState("hazard-context-full", true);
|
| 560 |
+
app.classList.add("scenario-collapse");
|
| 561 |
+
showRuntimeEvent("CONTEXT COLLAPSE", true);
|
| 562 |
+
contextOrgan.classList.add("context-pressure", "context-forgetting");
|
| 563 |
+
engineOrgan.classList.add("engine-recovery");
|
| 564 |
+
memoryState.textContent = "Memory rebalanced";
|
| 565 |
+
contextState.textContent = "Earlier memory forgotten";
|
| 566 |
+
tokenState.textContent = "Retrying recent context";
|
| 567 |
+
healthState.textContent = "Engine recovering";
|
| 568 |
+
setMetrics({ contextTokens: 1180, contextSize: 2048, speed: 29.2, firstToken: 1742, promptRate: 34.8 });
|
| 569 |
+
evidence3Label.textContent = "Dropped turns";
|
| 570 |
+
evidence3Value.textContent = "2";
|
| 571 |
+
}
|
| 572 |
+
|
| 573 |
+
function runExperiment() {
|
| 574 |
+
switch (experiment.value) {
|
| 575 |
+
case "long-context":
|
| 576 |
+
runLongContextExperiment();
|
| 577 |
+
break;
|
| 578 |
+
case "memory-pressure":
|
| 579 |
+
runMemoryPressureExperiment();
|
| 580 |
+
break;
|
| 581 |
+
case "slow-decode":
|
| 582 |
+
runSlowDecodeExperiment();
|
| 583 |
+
break;
|
| 584 |
+
case "context-collapse":
|
| 585 |
+
runContextCollapseExperiment();
|
| 586 |
+
break;
|
| 587 |
+
default:
|
| 588 |
+
setRuntimeIdle();
|
| 589 |
+
prompt.focus();
|
| 590 |
+
}
|
| 591 |
+
}
|
| 592 |
+
|
| 593 |
+
async function streamChat() {
|
| 594 |
+
const value = prompt.value.trim();
|
| 595 |
+
if (!value) return;
|
| 596 |
+
|
| 597 |
+
prompt.value = "";
|
| 598 |
+
send.disabled = true;
|
| 599 |
+
const ejectedBeforeSend = applyDeferredContextEjection();
|
| 600 |
+
chatHistory.push({ role: "user", content: value });
|
| 601 |
+
activeAssistant = { role: "assistant", content: "", isStreaming: true };
|
| 602 |
+
chatHistory.push(activeAssistant);
|
| 603 |
+
if (ejectedBeforeSend) {
|
| 604 |
+
activeAssistant.content = "Earlier turns fell outside the active context.\n\n";
|
| 605 |
+
}
|
| 606 |
+
renderMessages();
|
| 607 |
+
|
| 608 |
+
try {
|
| 609 |
+
const response = await fetch("/api/chat", {
|
| 610 |
+
method: "POST",
|
| 611 |
+
headers: { "Content-Type": "application/json" },
|
| 612 |
+
body: JSON.stringify({
|
| 613 |
+
messages: chatHistory
|
| 614 |
+
.slice(0, -1)
|
| 615 |
+
.filter((message) => !message.outsideContext)
|
| 616 |
+
.map(({ role, content }) => ({ role, content }))
|
| 617 |
+
})
|
| 618 |
+
});
|
| 619 |
+
|
| 620 |
+
if (!response.ok || !response.body) {
|
| 621 |
+
finishActiveAssistant();
|
| 622 |
+
activeAssistant.content += `\n[OpenCortex backend error: ${response.status}]`;
|
| 623 |
+
renderMessages();
|
| 624 |
+
return;
|
| 625 |
+
}
|
| 626 |
+
|
| 627 |
+
const reader = response.body.getReader();
|
| 628 |
+
const decoder = new TextDecoder();
|
| 629 |
+
let buffer = "";
|
| 630 |
+
|
| 631 |
+
while (true) {
|
| 632 |
+
const { value: chunk, done } = await reader.read();
|
| 633 |
+
if (done) break;
|
| 634 |
+
buffer += decoder.decode(chunk, { stream: true });
|
| 635 |
+
const lines = buffer.split("\n");
|
| 636 |
+
buffer = lines.pop() || "";
|
| 637 |
+
|
| 638 |
+
for (const line of lines) {
|
| 639 |
+
if (!line.trim()) continue;
|
| 640 |
+
const event = JSON.parse(line);
|
| 641 |
+
if (event.kind === "error") {
|
| 642 |
+
finishActiveAssistant();
|
| 643 |
+
const isContextOverflow = event.code === "context_overflow";
|
| 644 |
+
showRuntimeEvent(isContextOverflow ? "CONTEXT WINDOW EXCEEDED" : "BACKEND ERROR", true);
|
| 645 |
+
if (isContextOverflow) {
|
| 646 |
+
setPhase("decode", "DEGRADED", "Context window exceeded", "REQUEST REJECTED · CONTEXT FULL");
|
| 647 |
+
memoryState.textContent = "Memory overloaded";
|
| 648 |
+
contextState.textContent = "Context limit reached";
|
| 649 |
+
tokenState.textContent = "Stream blocked";
|
| 650 |
+
healthState.textContent = "Increase -c or start a new run";
|
| 651 |
+
activeAssistant.content += `\n[OpenCortex] ${event.message}`;
|
| 652 |
+
} else {
|
| 653 |
+
activeAssistant.content += `\n[OpenCortex backend error: ${event.message}]`;
|
| 654 |
+
}
|
| 655 |
+
renderMessages();
|
| 656 |
+
continue;
|
| 657 |
+
}
|
| 658 |
+
applyRuntimeEvent(event);
|
| 659 |
+
if (event.text_delta) {
|
| 660 |
+
activeAssistant.content += event.text_delta;
|
| 661 |
+
renderMessages();
|
| 662 |
+
}
|
| 663 |
+
}
|
| 664 |
+
}
|
| 665 |
+
} catch (error) {
|
| 666 |
+
showRuntimeEvent("BACKEND ERROR", true);
|
| 667 |
+
finishActiveAssistant();
|
| 668 |
+
activeAssistant.content += `\n[OpenCortex backend error: ${error.message || error}]`;
|
| 669 |
+
renderMessages();
|
| 670 |
+
} finally {
|
| 671 |
+
finishActiveAssistant();
|
| 672 |
+
renderMessages();
|
| 673 |
+
send.disabled = false;
|
| 674 |
+
}
|
| 675 |
+
}
|
| 676 |
+
|
| 677 |
+
send.addEventListener("click", streamChat);
|
| 678 |
+
prompt.addEventListener("keydown", (event) => {
|
| 679 |
+
if (event.key === "Enter" && !event.shiftKey) {
|
| 680 |
+
event.preventDefault();
|
| 681 |
+
streamChat();
|
| 682 |
+
}
|
| 683 |
+
});
|
| 684 |
+
runExperimentButton.addEventListener("click", runExperiment);
|
| 685 |
+
|
| 686 |
+
function collapseConversation() {
|
| 687 |
+
const current = parseFloat(getComputedStyle(app).getPropertyValue("--chat-width"));
|
| 688 |
+
if (current > collapseThreshold) previousChatWidth = current;
|
| 689 |
+
app.classList.remove("collapse-preview");
|
| 690 |
+
app.classList.add("chat-collapsed");
|
| 691 |
+
app.style.setProperty("--chat-width", "0px");
|
| 692 |
+
}
|
| 693 |
+
|
| 694 |
+
function restoreConversation() {
|
| 695 |
+
app.classList.remove("chat-collapsed", "collapse-preview");
|
| 696 |
+
const width = Math.max(340, previousChatWidth || workspace.clientWidth * 0.4);
|
| 697 |
+
app.style.setProperty("--chat-width", width + "px");
|
| 698 |
+
}
|
| 699 |
+
|
| 700 |
+
resizer.addEventListener("pointerdown", (event) => {
|
| 701 |
+
if (event.target === drawerToggle || app.classList.contains("chat-collapsed")) return;
|
| 702 |
+
event.preventDefault();
|
| 703 |
+
resizer.setPointerCapture(event.pointerId);
|
| 704 |
+
resizer.classList.add("dragging");
|
| 705 |
+
document.body.style.cursor = "col-resize";
|
| 706 |
+
});
|
| 707 |
+
|
| 708 |
+
resizer.addEventListener("pointermove", (event) => {
|
| 709 |
+
if (!resizer.hasPointerCapture(event.pointerId)) return;
|
| 710 |
+
const bounds = workspace.getBoundingClientRect();
|
| 711 |
+
const maximum = bounds.width * 0.62;
|
| 712 |
+
pendingChatWidth = Math.max(72, Math.min(maximum, event.clientX - bounds.left));
|
| 713 |
+
app.style.setProperty("--chat-width", pendingChatWidth + "px");
|
| 714 |
+
app.classList.toggle("collapse-preview", pendingChatWidth < collapseThreshold);
|
| 715 |
+
});
|
| 716 |
+
|
| 717 |
+
resizer.addEventListener("pointerup", (event) => {
|
| 718 |
+
if (!resizer.hasPointerCapture(event.pointerId)) return;
|
| 719 |
+
resizer.releasePointerCapture(event.pointerId);
|
| 720 |
+
resizer.classList.remove("dragging");
|
| 721 |
+
document.body.style.cursor = "";
|
| 722 |
+
if (pendingChatWidth < collapseThreshold) collapseConversation();
|
| 723 |
+
else {
|
| 724 |
+
previousChatWidth = pendingChatWidth;
|
| 725 |
+
app.classList.remove("collapse-preview");
|
| 726 |
+
}
|
| 727 |
+
});
|
| 728 |
+
|
| 729 |
+
resizer.addEventListener("dblclick", () => {
|
| 730 |
+
if (app.classList.contains("chat-collapsed")) restoreConversation();
|
| 731 |
+
else collapseConversation();
|
| 732 |
+
});
|
| 733 |
+
drawerToggle.addEventListener("click", (event) => {
|
| 734 |
+
event.stopPropagation();
|
| 735 |
+
restoreConversation();
|
| 736 |
+
});
|
| 737 |
+
|
| 738 |
+
setRuntimeIdle();
|
pyproject.toml
CHANGED
|
@@ -5,8 +5,10 @@ description = "Add your description here"
|
|
| 5 |
readme = "README.md"
|
| 6 |
requires-python = ">=3.12"
|
| 7 |
dependencies = [
|
|
|
|
| 8 |
"gradio>=6.18.0",
|
| 9 |
"httpx>=0.28.1",
|
|
|
|
| 10 |
]
|
| 11 |
|
| 12 |
[build-system]
|
|
|
|
| 5 |
readme = "README.md"
|
| 6 |
requires-python = ">=3.12"
|
| 7 |
dependencies = [
|
| 8 |
+
"fastapi>=0.137.0",
|
| 9 |
"gradio>=6.18.0",
|
| 10 |
"httpx>=0.28.1",
|
| 11 |
+
"uvicorn>=0.49.0",
|
| 12 |
]
|
| 13 |
|
| 14 |
[build-system]
|
requirements.txt
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi>=0.137.0
|
| 2 |
+
gradio>=6.18.0
|
| 3 |
+
httpx>=0.28.1
|
| 4 |
+
uvicorn>=0.49.0
|
tests/runtime/test_client.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pytest
|
| 2 |
+
import httpx
|
| 3 |
+
|
| 4 |
+
from open_cortex.runtime.client import (
|
| 5 |
+
_detect_repetition,
|
| 6 |
+
_raise_for_status_with_body,
|
| 7 |
+
_working_memory_percent,
|
| 8 |
+
)
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def test_detect_repetition_marks_repeated_generation_loop():
|
| 12 |
+
repeated = (
|
| 13 |
+
"艾丽和卡斯一起探索了整个宇宙,他们发现了一个惊人的事实:"
|
| 14 |
+
"整个宇宙的中心并不是银河系,而是整个宇宙的中心。"
|
| 15 |
+
)
|
| 16 |
+
|
| 17 |
+
assert _detect_repetition(repeated * 3) is True
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def test_detect_repetition_allows_short_normal_text():
|
| 21 |
+
text = "KV Cache stores attention keys and values so decode can reuse prior context."
|
| 22 |
+
|
| 23 |
+
assert _detect_repetition(text) is False
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def test_working_memory_percent_is_context_derived_and_capped():
|
| 27 |
+
assert _working_memory_percent(512, 2048) == 25.0
|
| 28 |
+
assert _working_memory_percent(4096, 2048) == 100.0
|
| 29 |
+
assert _working_memory_percent(None, 2048) is None
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def test_raise_for_status_with_body_preserves_streaming_error_body():
|
| 33 |
+
request = httpx.Request("POST", "http://127.0.0.1:8080/v1/chat/completions")
|
| 34 |
+
response = httpx.Response(
|
| 35 |
+
400,
|
| 36 |
+
request=request,
|
| 37 |
+
stream=httpx.ByteStream(
|
| 38 |
+
b"request (2059 tokens) exceeds the available context size (2048 tokens)"
|
| 39 |
+
),
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
with pytest.raises(httpx.HTTPStatusError) as exc_info:
|
| 43 |
+
_raise_for_status_with_body(response)
|
| 44 |
+
|
| 45 |
+
assert "exceeds the available context size" in exc_info.value.response.text
|
tests/ui/test_app.py
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
|
| 3 |
+
from fastapi import FastAPI
|
| 4 |
+
from fastapi.testclient import TestClient
|
| 5 |
+
import httpx
|
| 6 |
+
|
| 7 |
+
from open_cortex.runtime.events import RuntimeEvent
|
| 8 |
+
from open_cortex.runtime.messages import ChatMessage
|
| 9 |
+
from open_cortex.runtime.metrics import RuntimeSnapshot
|
| 10 |
+
from open_cortex.ui.app import (
|
| 11 |
+
create_app,
|
| 12 |
+
event_to_payload,
|
| 13 |
+
render_index,
|
| 14 |
+
_backend_mode,
|
| 15 |
+
_stream_events,
|
| 16 |
+
_stream_simulated_events,
|
| 17 |
+
_http_error_text,
|
| 18 |
+
_is_context_overflow,
|
| 19 |
+
_trim_for_context_collapse,
|
| 20 |
+
)
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def test_render_index_uses_original_product_shell():
|
| 24 |
+
html = render_index()
|
| 25 |
+
|
| 26 |
+
assert 'id="app"' in html
|
| 27 |
+
assert 'class="topbar"' in html
|
| 28 |
+
assert "OpenCortex" in html
|
| 29 |
+
assert "llama.cpp · local" in html
|
| 30 |
+
assert "EN / 中" not in html
|
| 31 |
+
assert 'id="lang"' not in html
|
| 32 |
+
assert 'id="clear"' not in html
|
| 33 |
+
assert ">Clear<" not in html
|
| 34 |
+
assert ">Send<" in html
|
| 35 |
+
assert "Send message" not in html
|
| 36 |
+
assert 'class="workspace"' in html
|
| 37 |
+
assert 'id="messages"' in html
|
| 38 |
+
assert 'id="prompt"' in html
|
| 39 |
+
assert 'id="send"' in html
|
| 40 |
+
assert 'id="runtime-event"' in html
|
| 41 |
+
assert 'id="experiment"' in html
|
| 42 |
+
assert 'id="run-experiment"' in html
|
| 43 |
+
assert "Live detected" in html
|
| 44 |
+
assert "Sim · Memory pressure" in html
|
| 45 |
+
assert "Sim · Context collapse" in html
|
| 46 |
+
assert 'id="memory-organ"' in html
|
| 47 |
+
assert 'id="context-used"' in html
|
| 48 |
+
assert 'id="core-state"' in html
|
| 49 |
+
assert "/assets/open_cortex.css?v=" in html
|
| 50 |
+
assert "/assets/open_cortex.js?v=" in html
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def test_styles_use_dynamic_viewport_height():
|
| 54 |
+
css = (create_app.__globals__["ASSET_DIR"] / "open_cortex.css").read_text()
|
| 55 |
+
|
| 56 |
+
assert "height: 100dvh" in css
|
| 57 |
+
assert "min-height: 100dvh" in css
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def test_frontend_exposes_experiment_and_engine_state_handlers():
|
| 61 |
+
js = (create_app.__globals__["ASSET_DIR"] / "open_cortex.js").read_text()
|
| 62 |
+
|
| 63 |
+
assert "function renderMarkdown" in js
|
| 64 |
+
assert "message.role === \"assistant\" ? renderMarkdown" in js
|
| 65 |
+
assert "isStreaming" in js
|
| 66 |
+
assert 'message.role === "assistant" && message.isStreaming' in js
|
| 67 |
+
assert "activeAssistant.isStreaming = false" in js
|
| 68 |
+
assert "function runExperiment" in js
|
| 69 |
+
assert "function runMemoryPressureExperiment" in js
|
| 70 |
+
assert "function runSlowDecodeExperiment" in js
|
| 71 |
+
assert "function runContextCollapseExperiment" in js
|
| 72 |
+
assert "collapseHoldUntil" in js
|
| 73 |
+
assert "contextWindowFullPending" in js
|
| 74 |
+
assert "function applyContextFullWarning" in js
|
| 75 |
+
assert "function applyDeferredContextEjection" in js
|
| 76 |
+
assert "CONTEXT WINDOW FULL" in js
|
| 77 |
+
assert "hazard-context-full" in js
|
| 78 |
+
assert "hazard-loop" in js
|
| 79 |
+
assert "runtimeEvent.classList.add(\"hazard\"" in js
|
| 80 |
+
assert "Earlier turns fell outside the active context." in js
|
| 81 |
+
assert "engine-strained" in js
|
| 82 |
+
assert "Engine recovering" in js
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def test_danger_states_have_decisive_visual_treatment():
|
| 86 |
+
css = (create_app.__globals__["ASSET_DIR"] / "open_cortex.css").read_text()
|
| 87 |
+
|
| 88 |
+
assert ".app.hazard-context-full .runtime-event" in css
|
| 89 |
+
assert ".app.hazard-loop .runtime-event" in css
|
| 90 |
+
assert ".runtime-event.hazard" in css
|
| 91 |
+
assert ".runtime-event.critical" in css
|
| 92 |
+
assert "context-full-shock" in css
|
| 93 |
+
assert "loop-warning-scan" in css
|
| 94 |
+
assert "danger-atmosphere" in css
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def test_context_collapse_styles_pop_out_oldest_message():
|
| 98 |
+
css = (create_app.__globals__["ASSET_DIR"] / "open_cortex.css").read_text()
|
| 99 |
+
|
| 100 |
+
assert "context-pop-out" in css
|
| 101 |
+
assert "@keyframes context-pop-out" in css
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def test_create_app_serves_index():
|
| 105 |
+
app = create_app()
|
| 106 |
+
client = TestClient(app)
|
| 107 |
+
|
| 108 |
+
response = client.get("/")
|
| 109 |
+
|
| 110 |
+
assert isinstance(app, FastAPI)
|
| 111 |
+
assert response.status_code == 200
|
| 112 |
+
assert "OpenCortex" in response.text
|
| 113 |
+
assert 'id="send"' in response.text
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
def test_space_defaults_to_simulated_backend(monkeypatch):
|
| 117 |
+
monkeypatch.delenv("OPEN_CORTEX_BACKEND", raising=False)
|
| 118 |
+
monkeypatch.setenv("SPACE_ID", "build-small-hackathon/open-cortex")
|
| 119 |
+
|
| 120 |
+
assert _backend_mode() == "simulated"
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
def test_explicit_backend_overrides_space_default(monkeypatch):
|
| 124 |
+
monkeypatch.setenv("OPEN_CORTEX_BACKEND", "llama_cpp")
|
| 125 |
+
monkeypatch.setenv("SPACE_ID", "build-small-hackathon/open-cortex")
|
| 126 |
+
|
| 127 |
+
assert _backend_mode() == "llama_cpp"
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
def test_stream_events_can_run_without_llama_cpp_in_simulated_mode(monkeypatch):
|
| 131 |
+
monkeypatch.setenv("OPEN_CORTEX_BACKEND", "simulated")
|
| 132 |
+
monkeypatch.setenv("OPEN_CORTEX_SIMULATOR_DELAY_SECONDS", "0")
|
| 133 |
+
|
| 134 |
+
messages = [ChatMessage(role="user", content="What is OpenCortex?")]
|
| 135 |
+
|
| 136 |
+
payloads = [json.loads(line) for line in _stream_events(messages)]
|
| 137 |
+
|
| 138 |
+
assert payloads[0]["kind"] == "request_started"
|
| 139 |
+
assert payloads[1]["kind"] == "first_token"
|
| 140 |
+
assert payloads[-1]["kind"] == "request_completed"
|
| 141 |
+
assert "OpenCortex" in "".join(payload["text_delta"] for payload in payloads)
|
| 142 |
+
assert payloads[-1]["context_size"] == 2048
|
| 143 |
+
assert payloads[-1]["decode_tps"] > 0
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
def test_simulated_story_can_surface_thought_loop(monkeypatch):
|
| 147 |
+
monkeypatch.setenv("OPEN_CORTEX_SIMULATOR_DELAY_SECONDS", "0")
|
| 148 |
+
|
| 149 |
+
messages = [ChatMessage(role="user", content="Write a long repeating story.")]
|
| 150 |
+
|
| 151 |
+
payloads = [
|
| 152 |
+
event_to_payload(event)
|
| 153 |
+
for event in _stream_simulated_events(messages)
|
| 154 |
+
]
|
| 155 |
+
|
| 156 |
+
assert any(payload["repetition_detected"] for payload in payloads)
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
def test_event_to_payload_includes_live_loop_state():
|
| 160 |
+
event = RuntimeEvent(
|
| 161 |
+
kind="token",
|
| 162 |
+
text_delta="银河系",
|
| 163 |
+
ttft_ms=None,
|
| 164 |
+
snapshot=RuntimeSnapshot(
|
| 165 |
+
prompt_tps=88.8,
|
| 166 |
+
decode_tps=21.5,
|
| 167 |
+
requests_processing=1,
|
| 168 |
+
requests_deferred=0,
|
| 169 |
+
active_slots=1,
|
| 170 |
+
slot_context_tokens=(512,),
|
| 171 |
+
slot_context_size=2048,
|
| 172 |
+
),
|
| 173 |
+
generated_tokens=66,
|
| 174 |
+
elapsed_ms=2100.0,
|
| 175 |
+
live_tps=19.7,
|
| 176 |
+
repetition_detected=True,
|
| 177 |
+
context_tokens=578,
|
| 178 |
+
context_size=2048,
|
| 179 |
+
working_memory_percent=28.2,
|
| 180 |
+
)
|
| 181 |
+
|
| 182 |
+
payload = event_to_payload(event)
|
| 183 |
+
|
| 184 |
+
assert payload["kind"] == "token"
|
| 185 |
+
assert payload["text_delta"] == "银河系"
|
| 186 |
+
assert payload["generated_tokens"] == 66
|
| 187 |
+
assert payload["live_tps"] == 19.7
|
| 188 |
+
assert payload["repetition_detected"] is True
|
| 189 |
+
assert payload["context_tokens"] == 578
|
| 190 |
+
assert payload["context_size"] == 2048
|
| 191 |
+
assert payload["working_memory_percent"] == 28.2
|
| 192 |
+
assert payload["snapshot"]["slot_context_tokens"] == [512]
|
| 193 |
+
assert payload["snapshot"]["slot_context_size"] == 2048
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
def test_stream_events_emits_context_collapse_and_retries_trimmed_history(monkeypatch):
|
| 197 |
+
calls: list[list[ChatMessage]] = []
|
| 198 |
+
request = httpx.Request("POST", "http://127.0.0.1:8080/v1/chat/completions")
|
| 199 |
+
overflow = httpx.HTTPStatusError(
|
| 200 |
+
"400 Bad Request",
|
| 201 |
+
request=request,
|
| 202 |
+
response=httpx.Response(
|
| 203 |
+
400,
|
| 204 |
+
request=request,
|
| 205 |
+
text="request (2059 tokens) exceeds the available context size (2048 tokens)",
|
| 206 |
+
),
|
| 207 |
+
)
|
| 208 |
+
|
| 209 |
+
def fake_stream_chat_events(messages: list[ChatMessage]):
|
| 210 |
+
calls.append(messages)
|
| 211 |
+
if len(calls) == 1:
|
| 212 |
+
yield RuntimeEvent(
|
| 213 |
+
kind="request_started",
|
| 214 |
+
text_delta="",
|
| 215 |
+
ttft_ms=None,
|
| 216 |
+
snapshot=None,
|
| 217 |
+
)
|
| 218 |
+
raise overflow
|
| 219 |
+
yield RuntimeEvent(
|
| 220 |
+
kind="first_token",
|
| 221 |
+
text_delta="继续",
|
| 222 |
+
ttft_ms=12.3,
|
| 223 |
+
snapshot=None,
|
| 224 |
+
context_tokens=128,
|
| 225 |
+
context_size=2048,
|
| 226 |
+
working_memory_percent=6.2,
|
| 227 |
+
)
|
| 228 |
+
|
| 229 |
+
monkeypatch.setattr("open_cortex.ui.app.stream_chat_events", fake_stream_chat_events)
|
| 230 |
+
messages = [
|
| 231 |
+
ChatMessage(role="system", content="You are Cortex."),
|
| 232 |
+
ChatMessage(role="user", content="old question"),
|
| 233 |
+
ChatMessage(role="assistant", content="old long answer"),
|
| 234 |
+
ChatMessage(role="user", content="recent question"),
|
| 235 |
+
ChatMessage(role="assistant", content="recent answer"),
|
| 236 |
+
ChatMessage(role="user", content="继续"),
|
| 237 |
+
]
|
| 238 |
+
|
| 239 |
+
payloads = [json.loads(line) for line in _stream_events(messages)]
|
| 240 |
+
|
| 241 |
+
assert [payload["kind"] for payload in payloads] == [
|
| 242 |
+
"request_started",
|
| 243 |
+
"context_collapse",
|
| 244 |
+
"first_token",
|
| 245 |
+
]
|
| 246 |
+
assert payloads[1]["dropped_messages"] == 2
|
| 247 |
+
assert payloads[1]["retained_messages"] == 4
|
| 248 |
+
assert payloads[1]["context_size"] == 2048
|
| 249 |
+
assert [message.content for message in calls[1]] == [
|
| 250 |
+
"You are Cortex.",
|
| 251 |
+
"recent question",
|
| 252 |
+
"recent answer",
|
| 253 |
+
"继续",
|
| 254 |
+
]
|
| 255 |
+
|
| 256 |
+
|
| 257 |
+
def test_context_overflow_detection_reads_streaming_error_body():
|
| 258 |
+
request = httpx.Request("POST", "http://127.0.0.1:8080/v1/chat/completions")
|
| 259 |
+
response = httpx.Response(
|
| 260 |
+
400,
|
| 261 |
+
request=request,
|
| 262 |
+
stream=httpx.ByteStream(
|
| 263 |
+
b"request (2059 tokens) exceeds the available context size (2048 tokens)"
|
| 264 |
+
),
|
| 265 |
+
)
|
| 266 |
+
error = httpx.HTTPStatusError("400 Bad Request", request=request, response=response)
|
| 267 |
+
|
| 268 |
+
assert _is_context_overflow(error) is True
|
| 269 |
+
|
| 270 |
+
|
| 271 |
+
def test_http_error_text_handles_closed_stream_without_crashing():
|
| 272 |
+
request = httpx.Request("POST", "http://127.0.0.1:8080/v1/chat/completions")
|
| 273 |
+
response = httpx.Response(
|
| 274 |
+
400,
|
| 275 |
+
request=request,
|
| 276 |
+
stream=httpx.ByteStream(b"closed"),
|
| 277 |
+
)
|
| 278 |
+
response.close()
|
| 279 |
+
error = httpx.HTTPStatusError("400 Bad Request", request=request, response=response)
|
| 280 |
+
|
| 281 |
+
assert _http_error_text(error) == ""
|
| 282 |
+
|
| 283 |
+
|
| 284 |
+
def test_trim_for_context_collapse_drops_oversized_assistant_history():
|
| 285 |
+
messages = [
|
| 286 |
+
ChatMessage(role="user", content="write a long story"),
|
| 287 |
+
ChatMessage(role="assistant", content="故事" * 4000),
|
| 288 |
+
ChatMessage(role="user", content="what happened next?"),
|
| 289 |
+
ChatMessage(role="assistant", content="recent answer"),
|
| 290 |
+
ChatMessage(role="user", content="继续"),
|
| 291 |
+
]
|
| 292 |
+
|
| 293 |
+
trimmed, dropped = _trim_for_context_collapse(messages)
|
| 294 |
+
|
| 295 |
+
assert dropped == 2
|
| 296 |
+
assert trimmed == [
|
| 297 |
+
ChatMessage(role="user", content="what happened next?"),
|
| 298 |
+
ChatMessage(role="assistant", content="recent answer"),
|
| 299 |
+
ChatMessage(role="user", content="继续"),
|
| 300 |
+
]
|
| 301 |
+
|
| 302 |
+
|
| 303 |
+
def test_trim_for_context_collapse_keeps_recent_tail_of_oversized_answer():
|
| 304 |
+
messages = [
|
| 305 |
+
ChatMessage(role="user", content="write a long story"),
|
| 306 |
+
ChatMessage(role="assistant", content="A" * 5000 + "recent ending"),
|
| 307 |
+
ChatMessage(role="user", content="1"),
|
| 308 |
+
]
|
| 309 |
+
|
| 310 |
+
trimmed, dropped = _trim_for_context_collapse(messages)
|
| 311 |
+
|
| 312 |
+
assert dropped == 1
|
| 313 |
+
assert len(trimmed) == 2
|
| 314 |
+
assert trimmed[0].role == "assistant"
|
| 315 |
+
assert "Earlier content collapsed" in trimmed[0].content
|
| 316 |
+
assert "recent ending" in trimmed[0].content
|
| 317 |
+
assert trimmed[1] == ChatMessage(role="user", content="1")
|
uv.lock
CHANGED
|
@@ -549,8 +549,10 @@ name = "open-cortex"
|
|
| 549 |
version = "0.1.0"
|
| 550 |
source = { editable = "." }
|
| 551 |
dependencies = [
|
|
|
|
| 552 |
{ name = "gradio" },
|
| 553 |
{ name = "httpx" },
|
|
|
|
| 554 |
]
|
| 555 |
|
| 556 |
[package.dev-dependencies]
|
|
@@ -560,8 +562,10 @@ dev = [
|
|
| 560 |
|
| 561 |
[package.metadata]
|
| 562 |
requires-dist = [
|
|
|
|
| 563 |
{ name = "gradio", specifier = ">=6.18.0" },
|
| 564 |
{ name = "httpx", specifier = ">=0.28.1" },
|
|
|
|
| 565 |
]
|
| 566 |
|
| 567 |
[package.metadata.requires-dev]
|
|
|
|
| 549 |
version = "0.1.0"
|
| 550 |
source = { editable = "." }
|
| 551 |
dependencies = [
|
| 552 |
+
{ name = "fastapi" },
|
| 553 |
{ name = "gradio" },
|
| 554 |
{ name = "httpx" },
|
| 555 |
+
{ name = "uvicorn" },
|
| 556 |
]
|
| 557 |
|
| 558 |
[package.dev-dependencies]
|
|
|
|
| 562 |
|
| 563 |
[package.metadata]
|
| 564 |
requires-dist = [
|
| 565 |
+
{ name = "fastapi", specifier = ">=0.137.0" },
|
| 566 |
{ name = "gradio", specifier = ">=6.18.0" },
|
| 567 |
{ name = "httpx", specifier = ">=0.28.1" },
|
| 568 |
+
{ name = "uvicorn", specifier = ">=0.49.0" },
|
| 569 |
]
|
| 570 |
|
| 571 |
[package.metadata.requires-dev]
|