peninsula123 commited on
Commit
828386c
·
1 Parent(s): b7280c1

feat(main): stream metrics fetch

Browse files
.gitignore CHANGED
@@ -5,6 +5,8 @@ bin
5
  tmp
6
  .venv
7
  plans
 
 
8
  notes
9
 
10
  .superpowers
 
5
  tmp
6
  .venv
7
  plans
8
+ !docs/superpowers/plans/
9
+ !docs/superpowers/plans/*.md
10
  notes
11
 
12
  .superpowers
docs/superpowers/plans/2026-06-10-open-cortex-mvp.md ADDED
@@ -0,0 +1,1654 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # OpenCortex MVP 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:** Build a polished bilingual Gradio observatory that streams chat and truthful llama.cpp runtime evidence, distinguishes five experiments, and deploys locally or through Hugging Face Space with an optional Modal backend.
6
+
7
+ **Architecture:** Gradio owns queuing and per-session state, while custom HTML/CSS and a framework-free browser controller render the approved observatory interface. A backend-neutral event protocol keeps simulated and live inference interchangeable; the live adapter uses llama.cpp `/apply-template`, `/tokenize`, `/completion`, and `/metrics`, with all semantic states derived from measured values and a benchmark profile.
8
+
9
+ **Tech Stack:** Python 3.12, uv 0.11, Gradio Blocks, httpx, prometheus-client, pytest, pytest-httpx, Node.js 24 built-in test runner, llama.cpp `llama-server`, Hugging Face Spaces, optional Modal.
10
+
11
+ ---
12
+
13
+ ## File Map
14
+
15
+ Create or modify the following focused units:
16
+
17
+ ```text
18
+ app.py # Hugging Face and local entry point
19
+ pyproject.toml # Python package, runtime, test, lint config
20
+ .gitignore # Track deployment files; ignore generated artifacts
21
+ README.md # Setup, architecture, truthfulness, deployment
22
+ src/open_cortex/config.py # Environment-backed immutable settings
23
+ src/open_cortex/controller.py # One-turn orchestration and session state
24
+ src/open_cortex/backends/base.py # Inference backend protocol
25
+ src/open_cortex/backends/simulated.py # Deterministic development backend
26
+ src/open_cortex/backends/llama_cpp.py # llama.cpp HTTP and SSE adapter
27
+ src/open_cortex/backends/metrics.py # Prometheus snapshot parsing
28
+ src/open_cortex/backends/load.py # Dedicated-demo controlled load
29
+ src/open_cortex/runtime/models.py # State, event, message, experiment types
30
+ src/open_cortex/runtime/state_engine.py # Metrics-to-semantic-state mapping
31
+ src/open_cortex/runtime/context.py # Chat template, token budget, forgetting
32
+ src/open_cortex/runtime/benchmark.py # Benchmark profile and CLI
33
+ src/open_cortex/ui/app.py # Gradio Blocks construction and callbacks
34
+ src/open_cortex/ui/render.py # Escaped chat and state bridge HTML
35
+ src/open_cortex/ui/localization.py # English and Chinese product copy
36
+ src/open_cortex/ui/assets/open_cortex.css # Product surface and organ visuals
37
+ src/open_cortex/ui/assets/open_cortex.js # State application, splitter, drawer, locale
38
+ tests/ # Unit, contract, UI, and integration tests
39
+ deploy/huggingface/README.md # Space configuration
40
+ deploy/modal/modal_app.py # Optional remote llama-server deployment
41
+ ```
42
+
43
+ The implementation does not add React, a frontend build step, a database, a
44
+ metrics database, or a Python llama.cpp binding.
45
+
46
+ ### Task 1: Replace the Scaffold With an Installable Application
47
+
48
+ **Files:**
49
+ - Delete: `main.py`
50
+ - Create: `app.py`
51
+ - Create: `src/open_cortex/__init__.py`
52
+ - Create: `src/open_cortex/config.py`
53
+ - Create: `tests/test_config.py`
54
+ - Modify: `pyproject.toml`
55
+ - Modify: `.gitignore`
56
+
57
+ - [ ] **Step 1: Add the failing settings test**
58
+
59
+ ```python
60
+ # tests/test_config.py
61
+ from open_cortex.config import Settings
62
+
63
+
64
+ def test_settings_default_to_simulator(monkeypatch):
65
+ monkeypatch.delenv("OPEN_CORTEX_BACKEND", raising=False)
66
+ settings = Settings.from_env()
67
+
68
+ assert settings.backend == "simulated"
69
+ assert settings.llama_base_url == "http://127.0.0.1:8080"
70
+ assert settings.context_size == 4096
71
+ assert settings.enable_load_experiments is False
72
+ ```
73
+
74
+ - [ ] **Step 2: Install and lock the project dependencies**
75
+
76
+ Run:
77
+
78
+ ```bash
79
+ uv add gradio httpx prometheus-client
80
+ uv add --dev pytest pytest-httpx pytest-cov ruff
81
+ ```
82
+
83
+ Expected: `pyproject.toml` and `uv.lock` contain the resolved packages.
84
+
85
+ - [ ] **Step 3: Configure packaging and quality commands**
86
+
87
+ Use this project configuration in `pyproject.toml`, retaining the versions
88
+ resolved by `uv add`:
89
+
90
+ ```toml
91
+ [project]
92
+ name = "open-cortex"
93
+ version = "0.1.0"
94
+ description = "A real-time observatory for local LLM inference"
95
+ readme = "README.md"
96
+ requires-python = ">=3.12"
97
+ dependencies = [
98
+ "gradio",
99
+ "httpx",
100
+ "prometheus-client",
101
+ ]
102
+
103
+ [project.scripts]
104
+ open-cortex = "open_cortex.ui.app:main"
105
+ open-cortex-benchmark = "open_cortex.runtime.benchmark:main"
106
+
107
+ [dependency-groups]
108
+ dev = [
109
+ "pytest",
110
+ "pytest-cov",
111
+ "pytest-httpx",
112
+ "ruff",
113
+ ]
114
+
115
+ [build-system]
116
+ requires = ["hatchling"]
117
+ build-backend = "hatchling.build"
118
+
119
+ [tool.pytest.ini_options]
120
+ addopts = "-q"
121
+ testpaths = ["tests"]
122
+
123
+ [tool.ruff]
124
+ line-length = 100
125
+ target-version = "py312"
126
+ ```
127
+
128
+ - [ ] **Step 4: Implement immutable environment settings**
129
+
130
+ ```python
131
+ # src/open_cortex/config.py
132
+ from __future__ import annotations
133
+
134
+ import os
135
+ from dataclasses import dataclass
136
+
137
+
138
+ def _as_bool(value: str | None, default: bool = False) -> bool:
139
+ if value is None:
140
+ return default
141
+ return value.lower() in {"1", "true", "yes", "on"}
142
+
143
+
144
+ @dataclass(frozen=True)
145
+ class Settings:
146
+ backend: str
147
+ llama_base_url: str
148
+ model_name: str
149
+ context_size: int
150
+ request_timeout_seconds: float
151
+ enable_load_experiments: bool
152
+ remote_backend: bool
153
+
154
+ @classmethod
155
+ def from_env(cls) -> "Settings":
156
+ base_url = os.getenv("OPEN_CORTEX_LLAMA_BASE_URL", "http://127.0.0.1:8080")
157
+ return cls(
158
+ backend=os.getenv("OPEN_CORTEX_BACKEND", "simulated"),
159
+ llama_base_url=base_url.rstrip("/"),
160
+ model_name=os.getenv("OPEN_CORTEX_MODEL_NAME", "Llama 3.2 3B Instruct"),
161
+ context_size=int(os.getenv("OPEN_CORTEX_CONTEXT_SIZE", "4096")),
162
+ request_timeout_seconds=float(
163
+ os.getenv("OPEN_CORTEX_REQUEST_TIMEOUT_SECONDS", "120")
164
+ ),
165
+ enable_load_experiments=_as_bool(
166
+ os.getenv("OPEN_CORTEX_ENABLE_LOAD_EXPERIMENTS")
167
+ ),
168
+ remote_backend=_as_bool(os.getenv("OPEN_CORTEX_REMOTE_BACKEND")),
169
+ )
170
+ ```
171
+
172
+ - [ ] **Step 5: Add the Space entry point and clean ignore rules**
173
+
174
+ ```python
175
+ # app.py
176
+ from open_cortex.ui.app import build_app
177
+
178
+ demo = build_app()
179
+
180
+ if __name__ == "__main__":
181
+ demo.launch()
182
+ ```
183
+
184
+ Delete `main.py`. Remove the `deploy` line from `.gitignore`; add
185
+ `.pytest_cache/`, `.ruff_cache/`, `htmlcov/`, `*.gguf`, and `models/`.
186
+
187
+ - [ ] **Step 6: Verify and commit**
188
+
189
+ Run:
190
+
191
+ ```bash
192
+ uv run pytest tests/test_config.py -v
193
+ uv run ruff check src tests app.py
194
+ ```
195
+
196
+ Expected: all tests pass and Ruff reports no errors.
197
+
198
+ ```bash
199
+ git add .gitignore app.py pyproject.toml uv.lock src/open_cortex tests/test_config.py
200
+ git commit -m "build: scaffold OpenCortex application"
201
+ ```
202
+
203
+ ### Task 2: Define the Backend-Neutral Runtime Protocol
204
+
205
+ **Files:**
206
+ - Create: `src/open_cortex/runtime/__init__.py`
207
+ - Create: `src/open_cortex/runtime/models.py`
208
+ - Create: `src/open_cortex/backends/__init__.py`
209
+ - Create: `src/open_cortex/backends/base.py`
210
+ - Create: `tests/runtime/test_models.py`
211
+
212
+ - [ ] **Step 1: Write serialization and ordering tests**
213
+
214
+ ```python
215
+ # tests/runtime/test_models.py
216
+ from open_cortex.runtime.models import (
217
+ ChatMessage,
218
+ ExperimentScenario,
219
+ RuntimeEvent,
220
+ RuntimePhase,
221
+ RuntimeState,
222
+ )
223
+
224
+
225
+ def test_runtime_event_serializes_enum_values(healthy_runtime_state):
226
+ event = RuntimeEvent(
227
+ request_id="req-1",
228
+ sequence=2,
229
+ kind="token",
230
+ state=healthy_runtime_state,
231
+ text_delta="hello",
232
+ )
233
+
234
+ payload = event.to_dict()
235
+
236
+ assert payload["sequence"] == 2
237
+ assert payload["state"]["phase"] == "decode"
238
+ assert payload["text_delta"] == "hello"
239
+
240
+
241
+ def test_scenarios_have_stable_wire_values():
242
+ assert [scenario.value for scenario in ExperimentScenario] == [
243
+ "normal",
244
+ "long_context",
245
+ "memory_pressure",
246
+ "slow_decode",
247
+ "context_collapse",
248
+ ]
249
+ ```
250
+
251
+ Add `tests/conftest.py` with a complete `healthy_runtime_state` fixture using
252
+ the dataclasses below.
253
+
254
+ - [ ] **Step 2: Run the test and confirm the missing module failure**
255
+
256
+ Run: `uv run pytest tests/runtime/test_models.py -v`
257
+
258
+ Expected: FAIL with `ModuleNotFoundError: open_cortex.runtime.models`.
259
+
260
+ - [ ] **Step 3: Implement the exact public types**
261
+
262
+ `runtime/models.py` must define:
263
+
264
+ ```python
265
+ class RuntimePhase(StrEnum):
266
+ IDLE = "idle"
267
+ PREFILL = "prefill"
268
+ DECODE = "decode"
269
+ RECOVERY = "recovery"
270
+ ERROR = "error"
271
+
272
+
273
+ class Severity(StrEnum):
274
+ QUIET = "quiet"
275
+ HEALTHY = "healthy"
276
+ BUSY = "busy"
277
+ STRAINED = "strained"
278
+ DEGRADED = "degraded"
279
+
280
+
281
+ class ExperimentScenario(StrEnum):
282
+ NORMAL = "normal"
283
+ LONG_CONTEXT = "long_context"
284
+ MEMORY_PRESSURE = "memory_pressure"
285
+ SLOW_DECODE = "slow_decode"
286
+ CONTEXT_COLLAPSE = "context_collapse"
287
+
288
+
289
+ @dataclass(frozen=True)
290
+ class ChatMessage:
291
+ id: str
292
+ role: Literal["system", "user", "assistant"]
293
+ content: str
294
+
295
+
296
+ @dataclass(frozen=True)
297
+ class GenerationConfig:
298
+ max_tokens: int = 384
299
+ temperature: float = 0.7
300
+ top_p: float = 0.9
301
+
302
+
303
+ @dataclass(frozen=True)
304
+ class RawMetrics:
305
+ prompt_tokens: int | None = None
306
+ generated_tokens: int = 0
307
+ kv_usage_ratio: float | None = None
308
+ kv_cache_tokens: int | None = None
309
+ prompt_tokens_per_second: float | None = None
310
+ tokens_per_second: float | None = None
311
+ ttft_ms: float | None = None
312
+ last_token_interval_ms: float | None = None
313
+ cache_evictions: int | None = None
314
+ requests_processing: int | None = None
315
+ requests_deferred: int | None = None
316
+ network_ms: float | None = None
317
+ ```
318
+
319
+ Also implement the four organ dataclasses, `RuntimeState`, and `RuntimeEvent`
320
+ exactly as specified in
321
+ `docs/superpowers/specs/2026-06-09-open-cortex-mvp-design.md`.
322
+ Add `network_ms: float | None = None` to `RuntimeEvent` so remote transport
323
+ latency remains event evidence rather than being misrepresented as an engine
324
+ organ metric.
325
+ Implement `to_dict()` with `dataclasses.asdict()` followed by recursive enum
326
+ conversion. `RuntimeEvent.kind` is a `Literal` of the seven event names in the
327
+ specification.
328
+
329
+ - [ ] **Step 4: Define the backend protocol**
330
+
331
+ ```python
332
+ # src/open_cortex/backends/base.py
333
+ from collections.abc import Iterator, Sequence
334
+ from typing import Protocol
335
+
336
+ from open_cortex.runtime.models import (
337
+ ChatMessage,
338
+ ExperimentScenario,
339
+ GenerationConfig,
340
+ RuntimeEvent,
341
+ )
342
+
343
+
344
+ class InferenceBackend(Protocol):
345
+ def stream_chat(
346
+ self,
347
+ messages: Sequence[ChatMessage],
348
+ config: GenerationConfig,
349
+ experiment: ExperimentScenario,
350
+ ) -> Iterator[RuntimeEvent]:
351
+ raise NotImplementedError
352
+
353
+ def supported_experiments(self) -> dict[ExperimentScenario, str | None]:
354
+ raise NotImplementedError
355
+ ```
356
+
357
+ - [ ] **Step 5: Verify and commit**
358
+
359
+ Run: `uv run pytest tests/runtime/test_models.py -v`
360
+
361
+ Expected: PASS.
362
+
363
+ ```bash
364
+ git add src/open_cortex/runtime src/open_cortex/backends tests
365
+ git commit -m "feat: define runtime event protocol"
366
+ ```
367
+
368
+ ### Task 3: Derive Semantic State From Metrics and Benchmarks
369
+
370
+ **Files:**
371
+ - Create: `src/open_cortex/runtime/thresholds.py`
372
+ - Create: `src/open_cortex/runtime/state_engine.py`
373
+ - Create: `tests/runtime/test_state_engine.py`
374
+
375
+ - [ ] **Step 1: Write table-driven state tests**
376
+
377
+ Cover these exact cases:
378
+
379
+ ```python
380
+ @pytest.mark.parametrize(
381
+ ("ratio", "severity"),
382
+ [(0.59, Severity.HEALTHY), (0.60, Severity.BUSY),
383
+ (0.80, Severity.STRAINED), (0.95, Severity.DEGRADED)],
384
+ )
385
+ def test_kv_thresholds(ratio, severity, state_engine):
386
+ state = state_engine.derive(
387
+ sequence=1,
388
+ phase=RuntimePhase.DECODE,
389
+ metrics=RawMetrics(kv_usage_ratio=ratio),
390
+ forgotten_message_ids=(),
391
+ )
392
+ assert state.working_memory.severity is severity
393
+
394
+
395
+ def test_context_loss_is_degraded_even_after_usage_recovers(state_engine):
396
+ state = state_engine.derive(
397
+ sequence=3,
398
+ phase=RuntimePhase.RECOVERY,
399
+ metrics=RawMetrics(prompt_tokens=1800),
400
+ forgotten_message_ids=("message-1",),
401
+ )
402
+ assert state.context_window.severity is Severity.DEGRADED
403
+ assert state.context_window.used_tokens == 1800
404
+ ```
405
+
406
+ Add tests proving decode severity is relative to a benchmark baseline and that
407
+ missing values stay `None`.
408
+
409
+ - [ ] **Step 2: Run tests to verify failure**
410
+
411
+ Run: `uv run pytest tests/runtime/test_state_engine.py -v`
412
+
413
+ Expected: FAIL because `RuntimeStateEngine` is undefined.
414
+
415
+ - [ ] **Step 3: Implement thresholds and profile types**
416
+
417
+ ```python
418
+ # src/open_cortex/runtime/thresholds.py
419
+ from dataclasses import dataclass
420
+
421
+
422
+ @dataclass(frozen=True)
423
+ class BenchmarkProfile:
424
+ model_name: str
425
+ context_size: int
426
+ median_ttft_ms: float
427
+ median_tokens_per_second: float
428
+ median_prompt_tokens_per_second: float
429
+
430
+
431
+ @dataclass(frozen=True)
432
+ class Thresholds:
433
+ kv_busy: float = 0.60
434
+ kv_strained: float = 0.80
435
+ kv_degraded: float = 0.95
436
+ context_busy: float = 0.60
437
+ context_strained: float = 0.80
438
+ context_near_limit: float = 0.95
439
+ slow_decode_ratio: float = 0.50
440
+ strained_ttft_ratio: float = 2.0
441
+ ```
442
+
443
+ - [ ] **Step 4: Implement `RuntimeStateEngine.derive`**
444
+
445
+ The constructor accepts `model_name`, `context_size`, `Thresholds`, and an
446
+ optional `BenchmarkProfile`. The mapping must obey:
447
+
448
+ ```python
449
+ kv severity = ratio bands from Thresholds
450
+ context degraded = bool(forgotten_message_ids)
451
+ context severity otherwise = prompt_tokens / context_size bands
452
+ token stream strained = measured tps < baseline * slow_decode_ratio
453
+ engine strained = measured ttft > baseline * strained_ttft_ratio
454
+ ```
455
+
456
+ Labels are stable semantic keys, not translated display strings:
457
+ `memory_holding`, `memory_busy`, `memory_fragmenting`, `memory_overloaded`,
458
+ `context_open`, `context_filling`, `context_compressed`, `context_forgotten`,
459
+ `flow_smooth`, `flow_slowing`, `flow_stalled`, `engine_idle`,
460
+ `engine_prefill`, `engine_decoding`, `engine_strained`, and `engine_error`.
461
+
462
+ - [ ] **Step 5: Verify and commit**
463
+
464
+ Run:
465
+
466
+ ```bash
467
+ uv run pytest tests/runtime/test_state_engine.py -v
468
+ uv run pytest tests/runtime -v
469
+ ```
470
+
471
+ Expected: PASS.
472
+
473
+ ```bash
474
+ git add src/open_cortex/runtime tests/runtime
475
+ git commit -m "feat: derive cognitive runtime states"
476
+ ```
477
+
478
+ ### Task 4: Build the Deterministic Simulator Contract
479
+
480
+ **Files:**
481
+ - Create: `src/open_cortex/backends/simulated.py`
482
+ - Create: `tests/backends/test_simulated.py`
483
+
484
+ - [ ] **Step 1: Write one contract test per experiment**
485
+
486
+ Assert the event sequence and the primary affected signal:
487
+
488
+ ```python
489
+ EXPECTED_NON_TOKEN_KINDS = [
490
+ "request_started",
491
+ "prefill_progress",
492
+ "first_token",
493
+ "request_completed",
494
+ ]
495
+
496
+
497
+ def test_slow_decode_only_degrades_token_cadence(simulated_backend):
498
+ events = list(
499
+ simulated_backend.stream_chat(
500
+ [ChatMessage("m1", "user", "Explain KV cache.")],
501
+ GenerationConfig(max_tokens=24),
502
+ ExperimentScenario.SLOW_DECODE,
503
+ )
504
+ )
505
+
506
+ assert [
507
+ event.kind for event in events if event.kind != "token"
508
+ ] == EXPECTED_NON_TOKEN_KINDS
509
+ first = next(event for event in events if event.kind == "first_token")
510
+ assert first.state.token_stream.severity is Severity.STRAINED
511
+ assert first.state.working_memory.severity is Severity.HEALTHY
512
+ assert first.state.context_window.severity is Severity.HEALTHY
513
+ ```
514
+
515
+ For context collapse, require a `context_forgotten` event, non-empty forgotten
516
+ IDs, and recovered context usage. For memory pressure, require memory strain but
517
+ healthy token/context organs. For long context, require high context usage and
518
+ long TTFT but normal decode speed.
519
+
520
+ - [ ] **Step 2: Run tests to verify failure**
521
+
522
+ Run: `uv run pytest tests/backends/test_simulated.py -v`
523
+
524
+ Expected: FAIL because `SimulatedBackend` is undefined.
525
+
526
+ - [ ] **Step 3: Implement deterministic fixture timelines**
527
+
528
+ Use fixed metrics and response fragments, with no random values or sleeps.
529
+ Every emitted event passes through `RuntimeStateEngine.derive`. Use these
530
+ scenario signatures:
531
+
532
+ | Scenario | Prompt tokens | KV | TTFT | TPS | Forgotten |
533
+ | --- | ---: | ---: | ---: | ---: | --- |
534
+ | Normal | 820 | 0.42 | 380 ms | 31 | none |
535
+ | Long context | 3500 | 0.68 | 1900 ms | 29 | none |
536
+ | Memory pressure | 1500 | 0.87 | 620 ms | 28 | none |
537
+ | Slow decode | 1100 | 0.48 | 480 ms | 6 | none |
538
+ | Context collapse | 1900 after release | 0.55 | 650 ms | 25 | oldest ID |
539
+
540
+ The assistant text demonstrates the effect instead of narrating UI controls.
541
+ For example, context collapse says that the recent thread is available but
542
+ cannot correctly recall the first user request.
543
+
544
+ - [ ] **Step 4: Verify and commit**
545
+
546
+ Run: `uv run pytest tests/backends/test_simulated.py -v`
547
+
548
+ Expected: PASS.
549
+
550
+ ```bash
551
+ git add src/open_cortex/backends/simulated.py tests/backends
552
+ git commit -m "feat: add deterministic runtime simulator"
553
+ ```
554
+
555
+ ### Task 5: Implement Safe Rendering and Localization
556
+
557
+ **Files:**
558
+ - Create: `src/open_cortex/ui/__init__.py`
559
+ - Create: `src/open_cortex/ui/localization.py`
560
+ - Create: `src/open_cortex/ui/render.py`
561
+ - Create: `tests/ui/test_render.py`
562
+ - Create: `tests/ui/test_localization.py`
563
+
564
+ - [ ] **Step 1: Write escaping, boundary, and translation tests**
565
+
566
+ ```python
567
+ def test_chat_renderer_escapes_user_html():
568
+ html = render_chat(
569
+ [ChatMessage("m1", "user", "<script>alert(1)</script>")],
570
+ forgotten_message_ids=(),
571
+ locale="en",
572
+ )
573
+ assert "<script>" not in html
574
+ assert "&lt;script&gt;" in html
575
+
576
+
577
+ def test_boundary_precedes_first_active_message():
578
+ html = render_chat(
579
+ [
580
+ ChatMessage("old", "user", "old"),
581
+ ChatMessage("active", "assistant", "active"),
582
+ ],
583
+ forgotten_message_ids=("old",),
584
+ locale="en",
585
+ )
586
+ assert html.index("outside-active-context") < html.index("active-context-boundary")
587
+ assert html.index("active-context-boundary") < html.index('data-message-id="active"')
588
+
589
+
590
+ def test_chinese_copy_contains_semantic_state():
591
+ assert translate("memory_fragmenting", "zh") == "工作记忆正在碎片化"
592
+ ```
593
+
594
+ - [ ] **Step 2: Run tests to verify failure**
595
+
596
+ Run: `uv run pytest tests/ui/test_render.py tests/ui/test_localization.py -v`
597
+
598
+ Expected: FAIL because the UI modules do not exist.
599
+
600
+ - [ ] **Step 3: Implement a closed translation catalog**
601
+
602
+ `localization.py` must contain English and Chinese strings for all state labels,
603
+ five experiments, header controls, `Active Context Boundary`,
604
+ `outside active context`, metric units, disconnected state, retry, and
605
+ simulator disclosure. `translate(key, locale)` falls back to English and raises
606
+ `KeyError` for an unknown key during tests.
607
+
608
+ - [ ] **Step 4: Implement render functions**
609
+
610
+ ```python
611
+ def render_chat(
612
+ messages: Sequence[ChatMessage],
613
+ forgotten_message_ids: Sequence[str],
614
+ locale: str,
615
+ incomplete_message_id: str | None = None,
616
+ ) -> str:
617
+ forgotten = set(forgotten_message_ids)
618
+ chunks = ['<div class="chat-thread">']
619
+ boundary_written = False
620
+ for message in messages:
621
+ if message.id not in forgotten and forgotten and not boundary_written:
622
+ chunks.append(
623
+ '<div class="active-context-boundary">'
624
+ f'<span>{html.escape(translate("active_context_boundary", locale))}</span>'
625
+ "</div>"
626
+ )
627
+ boundary_written = True
628
+ classes = ["chat-message", f"role-{message.role}"]
629
+ if message.id in forgotten:
630
+ classes.append("outside-active-context")
631
+ if message.id == incomplete_message_id:
632
+ classes.append("is-incomplete")
633
+ chunks.append(
634
+ f'<article class="{" ".join(classes)}" '
635
+ f'data-message-id="{html.escape(message.id, quote=True)}">'
636
+ f"<p>{html.escape(message.content)}</p></article>"
637
+ )
638
+ chunks.append("</div>")
639
+ return "".join(chunks)
640
+
641
+
642
+ def render_state_payload(
643
+ event: RuntimeEvent,
644
+ locale: str,
645
+ backend_mode: str,
646
+ network_ms: float | None,
647
+ ) -> str:
648
+ payload = {
649
+ "requestId": event.request_id,
650
+ "sequence": event.sequence,
651
+ "kind": event.kind,
652
+ "locale": locale,
653
+ "backendMode": backend_mode,
654
+ "networkMs": network_ms,
655
+ "state": event.to_dict()["state"],
656
+ }
657
+ encoded = html.escape(json.dumps(payload, ensure_ascii=False), quote=True)
658
+ return f'<div class="runtime-state-payload" data-state="{encoded}"></div>'
659
+ ```
660
+
661
+ `render_chat` places forgotten messages inside
662
+ `.outside-active-context`, inserts one `.active-context-boundary` before the
663
+ first active message, and never emits unescaped message content.
664
+
665
+ - [ ] **Step 5: Verify and commit**
666
+
667
+ Run: `uv run pytest tests/ui -v`
668
+
669
+ Expected: PASS.
670
+
671
+ ```bash
672
+ git add src/open_cortex/ui tests/ui
673
+ git commit -m "feat: render bilingual runtime state safely"
674
+ ```
675
+
676
+ ### Task 6: Rebuild the Approved Product Surface as Formal Assets
677
+
678
+ **Files:**
679
+ - Create: `src/open_cortex/ui/assets/open_cortex.css`
680
+ - Create: `src/open_cortex/ui/assets/open_cortex.js`
681
+ - Create: `tests/ui/test_assets.py`
682
+ - Create: `tests/js/open_cortex.test.js`
683
+
684
+ - [ ] **Step 1: Write structural CSS and JavaScript tests**
685
+
686
+ The Python test reads both asset files and requires selectors/IDs for:
687
+ `#conversation-panel`, `#runtime-panel`, `#workspace-divider`,
688
+ `#conversation-restore`, `#cortex-core`, `#working-memory`,
689
+ `#context-window`, `#token-stream`, `#engine-state`, and
690
+ `#runtime-state-bridge`.
691
+
692
+ The Node test requires the browser script and checks pure functions:
693
+
694
+ ```javascript
695
+ const test = require("node:test");
696
+ const assert = require("node:assert/strict");
697
+ require("../../src/open_cortex/ui/assets/open_cortex.js");
698
+
699
+ const controller = globalThis.OpenCortexController;
700
+
701
+ test("splitter collapses below threshold", () => {
702
+ assert.deepEqual(
703
+ controller.resolveSplit(148, { min: 280, max: 680, collapse: 220 }),
704
+ { collapsed: true, width: 0 }
705
+ );
706
+ });
707
+
708
+ test("stale state is rejected", () => {
709
+ assert.equal(controller.isNewer({ requestId: "r", sequence: 4 }, {
710
+ requestId: "r", sequence: 3
711
+ }), false);
712
+ });
713
+ ```
714
+
715
+ - [ ] **Step 2: Run tests to verify failure**
716
+
717
+ Run:
718
+
719
+ ```bash
720
+ uv run pytest tests/ui/test_assets.py -v
721
+ node --test tests/js/open_cortex.test.js
722
+ ```
723
+
724
+ Expected: FAIL because the assets do not exist.
725
+
726
+ - [ ] **Step 3: Implement the formal visual system**
727
+
728
+ The CSS must use these restrained tokens:
729
+
730
+ ```css
731
+ :root {
732
+ --oc-bg: #070b12;
733
+ --oc-panel: rgba(13, 19, 29, 0.82);
734
+ --oc-panel-raised: rgba(17, 25, 38, 0.94);
735
+ --oc-border: rgba(148, 163, 184, 0.16);
736
+ --oc-text: #e8eef7;
737
+ --oc-muted: #8290a3;
738
+ --oc-cyan: #5cd6e8;
739
+ --oc-teal: #38bfa7;
740
+ --oc-orange: #f2a65a;
741
+ --oc-red: #ff5f73;
742
+ --oc-magenta: #df6cff;
743
+ --conversation-width: 42%;
744
+ }
745
+ ```
746
+
747
+ Implement a near-black radial background, thin borders, no global glow, and
748
+ unique organ structures:
749
+
750
+ - Working Memory: a grid of KV cells; `.is-fragmenting` offsets, dims, and
751
+ reorders cells with an irregular animation.
752
+ - Context Window: a horizontal memory tape with a fill mask and released oldest
753
+ segments; `.is-forgotten` uses one red boundary, not a red panel.
754
+ - Token Stream: conduit particles whose CSS duration and opacity are set by JS;
755
+ `.is-slowing` uses stop-burst-stop keyframes.
756
+ - Engine State: compact rhythm trace and TTFT evidence.
757
+ - Cortex Core: layered rings and four named connections. Prefill charges the
758
+ outer ring, decode pulses regularly, memory pressure disturbs only the memory
759
+ connection, slow decode breaks only the token connection, and context loss
760
+ dims only the context connection.
761
+
762
+ The conversation panel uses modern product spacing and sentence case. Reserve
763
+ uppercase for short machine labels. Hide duplicate descriptions and secondary
764
+ metrics at widths below 1180 px.
765
+
766
+ - [ ] **Step 4: Implement the browser controller**
767
+
768
+ Expose these pure functions on `globalThis.OpenCortexController`:
769
+
770
+ ```javascript
771
+ resolveSplit(candidate, limits)
772
+ isNewer(next, current)
773
+ stateClasses(payload)
774
+ formatMetric(value, unit, digits)
775
+ ```
776
+
777
+ On `DOMContentLoaded`, the controller:
778
+
779
+ 1. Observes `#runtime-state-bridge` with `MutationObserver`.
780
+ 2. Parses `.runtime-state-payload.dataset.state`.
781
+ 3. Rejects lower sequence values for the same request.
782
+ 4. Updates text, CSS custom properties, severity classes, phase classes, and
783
+ only the affected core connection.
784
+ 5. Handles pointer drag on `#workspace-divider`.
785
+ 6. Collapses at 220 px, restores to the last width, and toggles on double click.
786
+ 7. Stores width and locale in `localStorage`.
787
+ 8. Honors `prefers-reduced-motion`.
788
+
789
+ Guard browser initialization with `if (typeof document !== "undefined")` so
790
+ Node can test the pure functions without a DOM.
791
+
792
+ - [ ] **Step 5: Verify and commit**
793
+
794
+ Run:
795
+
796
+ ```bash
797
+ uv run pytest tests/ui/test_assets.py -v
798
+ node --test tests/js/open_cortex.test.js
799
+ ```
800
+
801
+ Expected: PASS.
802
+
803
+ ```bash
804
+ git add src/open_cortex/ui/assets tests/ui/test_assets.py tests/js
805
+ git commit -m "feat: add living observatory visual system"
806
+ ```
807
+
808
+ ### Task 7: Wire a Complete Simulator-Driven Gradio Product
809
+
810
+ **Files:**
811
+ - Create: `src/open_cortex/controller.py`
812
+ - Create: `src/open_cortex/ui/app.py`
813
+ - Create: `tests/test_controller.py`
814
+ - Create: `tests/ui/test_app.py`
815
+
816
+ - [ ] **Step 1: Write orchestration tests**
817
+
818
+ Test that one submitted turn:
819
+
820
+ 1. Adds the user message immediately.
821
+ 2. Appends assistant deltas to one assistant message.
822
+ 3. Yields chat HTML and runtime payload from the same event.
823
+ 4. Keeps monotonically increasing sequence values.
824
+ 5. Preserves partial output on `request_failed`.
825
+
826
+ Also test `build_app(Settings.from_env())` returns a `gr.Blocks`.
827
+
828
+ - [ ] **Step 2: Run tests to verify failure**
829
+
830
+ Run: `uv run pytest tests/test_controller.py tests/ui/test_app.py -v`
831
+
832
+ Expected: FAIL because the controller and app are undefined.
833
+
834
+ - [ ] **Step 3: Implement session orchestration**
835
+
836
+ ```python
837
+ @dataclass
838
+ class SessionState:
839
+ messages: list[ChatMessage] = field(default_factory=list)
840
+ forgotten_message_ids: tuple[str, ...] = ()
841
+ locale: str = "en"
842
+ experiment: ExperimentScenario = ExperimentScenario.NORMAL
843
+ incomplete_message_id: str | None = None
844
+
845
+
846
+ class ChatController:
847
+ def __init__(self, backend: InferenceBackend, backend_mode: str):
848
+ self.backend = backend
849
+ self.backend_mode = backend_mode
850
+
851
+ def stream_turn(
852
+ self,
853
+ prompt: str,
854
+ session: SessionState,
855
+ ) -> Iterator[tuple[str, str, SessionState, str]]:
856
+ prompt = prompt.strip()
857
+ if not prompt:
858
+ return
859
+ session.messages.append(ChatMessage(uuid.uuid4().hex, "user", prompt))
860
+ assistant = ChatMessage(uuid.uuid4().hex, "assistant", "")
861
+ session.messages.append(assistant)
862
+ session.incomplete_message_id = assistant.id
863
+ for event in self.backend.stream_chat(
864
+ session.messages[:-1],
865
+ GenerationConfig(),
866
+ session.experiment,
867
+ ):
868
+ if event.text_delta:
869
+ assistant = replace(
870
+ assistant,
871
+ content=assistant.content + event.text_delta,
872
+ )
873
+ session.messages[-1] = assistant
874
+ if event.forgotten_message_ids:
875
+ session.forgotten_message_ids = event.forgotten_message_ids
876
+ if event.kind == "request_completed":
877
+ session.incomplete_message_id = None
878
+ yield (
879
+ render_chat(
880
+ session.messages,
881
+ session.forgotten_message_ids,
882
+ session.locale,
883
+ session.incomplete_message_id,
884
+ ),
885
+ render_state_payload(
886
+ event,
887
+ session.locale,
888
+ self.backend_mode,
889
+ event.network_ms,
890
+ ),
891
+ session,
892
+ "",
893
+ )
894
+ ```
895
+
896
+ The tuple is `chat_html`, `state_payload_html`, updated session, and cleared
897
+ composer value. Generate message/request IDs with `uuid.uuid4().hex`. Do not
898
+ sleep in the controller; simulator timing belongs in the simulator and live
899
+ timing comes from SSE arrival.
900
+
901
+ - [ ] **Step 4: Build the Gradio layout**
902
+
903
+ Use `gr.Blocks(fill_height=True, css_paths=[asset_dir / "open_cortex.css"],
904
+ js=asset_text)` with:
905
+
906
+ - Compact header: identity, backend status, language button.
907
+ - A `gr.Row` workspace.
908
+ - Left `gr.Column(elem_id="conversation-panel")` containing experiment label,
909
+ `gr.HTML(elem_id="chat-transcript")`, and composer controls.
910
+ - `gr.HTML` divider and restore control.
911
+ - Right `gr.Column(elem_id="runtime-panel")` containing model/phase controls,
912
+ static observatory HTML, and compact evidence strip.
913
+ - A visually hidden, but DOM-mounted,
914
+ `gr.HTML(elem_id="runtime-state-bridge")`.
915
+ - `gr.State(SessionState())`.
916
+
917
+ The static observatory HTML contains all IDs required in Task 6; only the bridge
918
+ is replaced while streaming so core animations are not restarted per token.
919
+
920
+ - [ ] **Step 5: Wire events**
921
+
922
+ `Textbox.submit` and Send button call the same generator. Scenario buttons set
923
+ `session.experiment`. The language button toggles `en`/`zh`, rerenders the chat,
924
+ and sends a bridge payload that updates static labels. Call `.queue()` so
925
+ streaming generators work on Spaces.
926
+
927
+ - [ ] **Step 6: Run the simulator and inspect all scenarios**
928
+
929
+ Run: `uv run python app.py`
930
+
931
+ Expected: the page opens in simulator mode; all five scenarios are visibly
932
+ different, context collapse adds the boundary, and splitter collapse/restore
933
+ works.
934
+
935
+ - [ ] **Step 7: Verify and commit**
936
+
937
+ Run:
938
+
939
+ ```bash
940
+ uv run pytest -v
941
+ node --test tests/js/open_cortex.test.js
942
+ uv run ruff check .
943
+ ```
944
+
945
+ Expected: PASS.
946
+
947
+ ```bash
948
+ git add app.py src/open_cortex/controller.py src/open_cortex/ui tests
949
+ git commit -m "feat: deliver simulator-driven OpenCortex UI"
950
+ ```
951
+
952
+ ### Task 8: Parse llama.cpp Metrics Without Inventing Values
953
+
954
+ **Files:**
955
+ - Create: `src/open_cortex/backends/metrics.py`
956
+ - Create: `tests/backends/test_metrics.py`
957
+ - Create: `tests/fixtures/llama_metrics.txt`
958
+
959
+ - [ ] **Step 1: Add a representative Prometheus fixture and failing tests**
960
+
961
+ Fixture metrics:
962
+
963
+ ```text
964
+ # TYPE llamacpp:prompt_tokens_total counter
965
+ llamacpp:prompt_tokens_total 4120
966
+ llamacpp:tokens_predicted_total 830
967
+ llamacpp:prompt_tokens_seconds 117.5
968
+ llamacpp:predicted_tokens_seconds 29.7
969
+ llamacpp:kv_cache_usage_ratio 0.63
970
+ llamacpp:kv_cache_tokens 2580
971
+ llamacpp:requests_processing 1
972
+ llamacpp:requests_deferred 0
973
+ ```
974
+
975
+ Assert exact mapping into `RawMetrics`, plus a second test where absent metrics
976
+ remain `None`.
977
+
978
+ - [ ] **Step 2: Run tests to verify failure**
979
+
980
+ Run: `uv run pytest tests/backends/test_metrics.py -v`
981
+
982
+ Expected: FAIL because `parse_llama_metrics` is undefined.
983
+
984
+ - [ ] **Step 3: Implement the parser**
985
+
986
+ Use `prometheus_client.parser.text_string_to_metric_families`. Match exact
987
+ metric names, read the first unlabeled aggregate sample, convert the KV ratio
988
+ to a ratio rather than a percentage, and never default an absent metric to
989
+ zero.
990
+
991
+ - [ ] **Step 4: Verify and commit**
992
+
993
+ Run: `uv run pytest tests/backends/test_metrics.py -v`
994
+
995
+ Expected: PASS.
996
+
997
+ ```bash
998
+ git add src/open_cortex/backends/metrics.py tests/backends tests/fixtures
999
+ git commit -m "feat: parse llama.cpp runtime telemetry"
1000
+ ```
1001
+
1002
+ ### Task 9: Implement Exact Prompt Formatting, Tokenization, and Forgetting
1003
+
1004
+ **Files:**
1005
+ - Create: `src/open_cortex/runtime/context.py`
1006
+ - Create: `tests/runtime/test_context.py`
1007
+
1008
+ - [ ] **Step 1: Write HTTP-backed context policy tests**
1009
+
1010
+ Mock `/apply-template` to return the model-formatted prompt and `/tokenize` to
1011
+ return token IDs. Assert:
1012
+
1013
+ - Messages stay intact when formatted prompt tokens fit the budget.
1014
+ - Oldest non-system turns are removed as user/assistant pairs.
1015
+ - The latest user message is never removed.
1016
+ - Returned forgotten IDs exactly match removed visible messages.
1017
+ - The final formatted prompt token count is at or below
1018
+ `context_size - max_generation_tokens`.
1019
+
1020
+ - [ ] **Step 2: Run tests to verify failure**
1021
+
1022
+ Run: `uv run pytest tests/runtime/test_context.py -v`
1023
+
1024
+ Expected: FAIL because `LlamaContextPolicy` is undefined.
1025
+
1026
+ - [ ] **Step 3: Implement the server-backed policy**
1027
+
1028
+ ```python
1029
+ @dataclass(frozen=True)
1030
+ class PreparedPrompt:
1031
+ prompt: str
1032
+ prompt_tokens: int
1033
+ active_messages: tuple[ChatMessage, ...]
1034
+ forgotten_message_ids: tuple[str, ...]
1035
+
1036
+
1037
+ class ContextBudgetError(ValueError):
1038
+ pass
1039
+
1040
+
1041
+ class LlamaContextPolicy:
1042
+ def __init__(self, client: httpx.Client, context_size: int):
1043
+ self.client = client
1044
+ self.context_size = context_size
1045
+
1046
+ def prepare(
1047
+ self,
1048
+ messages: Sequence[ChatMessage],
1049
+ max_generation_tokens: int,
1050
+ ) -> PreparedPrompt:
1051
+ active = list(messages)
1052
+ forgotten: list[str] = []
1053
+ budget = self.context_size - max_generation_tokens
1054
+ while True:
1055
+ response = self.client.post(
1056
+ "/apply-template",
1057
+ json={
1058
+ "messages": [
1059
+ {"role": message.role, "content": message.content}
1060
+ for message in active
1061
+ ]
1062
+ },
1063
+ )
1064
+ response.raise_for_status()
1065
+ prompt = response.json()["prompt"]
1066
+ token_response = self.client.post(
1067
+ "/tokenize",
1068
+ json={"content": prompt, "add_special": False},
1069
+ )
1070
+ token_response.raise_for_status()
1071
+ prompt_tokens = len(token_response.json()["tokens"])
1072
+ if prompt_tokens <= budget:
1073
+ return PreparedPrompt(
1074
+ prompt=prompt,
1075
+ prompt_tokens=prompt_tokens,
1076
+ active_messages=tuple(active),
1077
+ forgotten_message_ids=tuple(forgotten),
1078
+ )
1079
+ removable = next(
1080
+ (
1081
+ index
1082
+ for index, message in enumerate(active[:-1])
1083
+ if message.role != "system"
1084
+ ),
1085
+ None,
1086
+ )
1087
+ if removable is None:
1088
+ raise ContextBudgetError(
1089
+ "latest message cannot fit within the configured context"
1090
+ )
1091
+ removed = active.pop(removable)
1092
+ forgotten.append(removed.id)
1093
+ if (
1094
+ removed.role == "user"
1095
+ and removable < len(active) - 1
1096
+ and active[removable].role == "assistant"
1097
+ ):
1098
+ forgotten.append(active.pop(removable).id)
1099
+ ```
1100
+
1101
+ For each candidate history, POST message dictionaries to `/apply-template`,
1102
+ then POST the returned string to `/tokenize` with `add_special=False`. Remove
1103
+ the oldest complete conversational turn until it fits. This uses the active
1104
+ model's real chat template and tokenizer; no character-count estimate is
1105
+ allowed.
1106
+
1107
+ - [ ] **Step 4: Verify and commit**
1108
+
1109
+ Run: `uv run pytest tests/runtime/test_context.py -v`
1110
+
1111
+ Expected: PASS.
1112
+
1113
+ ```bash
1114
+ git add src/open_cortex/runtime/context.py tests/runtime/test_context.py
1115
+ git commit -m "feat: enforce model-accurate context boundaries"
1116
+ ```
1117
+
1118
+ ### Task 10: Stream Live llama.cpp Events
1119
+
1120
+ **Files:**
1121
+ - Create: `src/open_cortex/backends/llama_cpp.py`
1122
+ - Create: `tests/backends/test_llama_cpp.py`
1123
+ - Create: `tests/fixtures/completion_stream.sse`
1124
+
1125
+ - [ ] **Step 1: Add an SSE fixture and event contract tests**
1126
+
1127
+ The fixture must contain prompt progress events, content events, and a final
1128
+ timings object. Test:
1129
+
1130
+ - `request_started` precedes `prefill_progress`.
1131
+ - `first_token` is emitted exactly once.
1132
+ - token text is preserved exactly.
1133
+ - TTFT uses `time.monotonic()` at the request boundary.
1134
+ - rolling TPS and last-token interval use actual event arrival times.
1135
+ - `/metrics` failure leaves KV evidence `None` while chat continues.
1136
+ - interrupted SSE emits `request_failed` with partial text retained.
1137
+ - context-policy forgotten IDs produce `context_forgotten`.
1138
+
1139
+ - [ ] **Step 2: Run tests to verify failure**
1140
+
1141
+ Run: `uv run pytest tests/backends/test_llama_cpp.py -v`
1142
+
1143
+ Expected: FAIL because `LlamaCppBackend` is undefined.
1144
+
1145
+ - [ ] **Step 3: Implement the client around llama.cpp native endpoints**
1146
+
1147
+ `LlamaCppBackend` owns one `httpx.Client`, `LlamaContextPolicy`,
1148
+ `RuntimeStateEngine`, and a monotonic clock dependency. For generation:
1149
+
1150
+ 1. Prepare the exact prompt with `/apply-template` and `/tokenize`.
1151
+ 2. Snapshot `/metrics`.
1152
+ 3. POST `/completion` with:
1153
+
1154
+ ```json
1155
+ {
1156
+ "prompt": "<formatted prompt>",
1157
+ "stream": true,
1158
+ "return_progress": true,
1159
+ "cache_prompt": true,
1160
+ "n_predict": 384,
1161
+ "temperature": 0.7,
1162
+ "top_p": 0.9
1163
+ }
1164
+ ```
1165
+
1166
+ 4. Parse `data:` SSE lines and ignore `[DONE]`.
1167
+ 5. Emit prefill progress from `prompt_progress.total`,
1168
+ `prompt_progress.processed`, and `prompt_progress.time_ms`.
1169
+ 6. Throttle `/metrics` sampling to at most once every 250 ms.
1170
+ 7. Measure TTFT and token intervals at the OpenCortex boundary.
1171
+ 8. Derive each state through `RuntimeStateEngine`.
1172
+ 9. Emit completion or failure without fabricating unsupported evidence.
1173
+
1174
+ Scenario preparation must be explicit:
1175
+
1176
+ - Long context adds a deterministic hidden reference document and uses
1177
+ `/apply-template` plus `/tokenize` to size the actual foreground prompt to
1178
+ 80-90% of the configured context. The document remains in the generated
1179
+ request, so high prefill and TTFT are real.
1180
+ - Context collapse first applies a deterministic overflow document to identify
1181
+ which oldest visible turns no longer fit. It then removes the overflow
1182
+ document and those forgotten turns before generation. The generation request
1183
+ therefore has a recovered context level while the UI accurately marks the
1184
+ visible turns excluded from the active prompt.
1185
+ - Memory pressure and slow decode do not pad the foreground context.
1186
+
1187
+ The base URL is used identically for local and remote modes. If remote mode is
1188
+ enabled, separately capture HTTP connect/round-trip time in `network_ms`; never
1189
+ label it inference latency.
1190
+
1191
+ - [ ] **Step 4: Expose support status**
1192
+
1193
+ Normal, long-context, and context-collapse are always supported. Memory
1194
+ pressure and slow decode return a concise disabled reason unless
1195
+ `enable_load_experiments` is true.
1196
+
1197
+ - [ ] **Step 5: Verify and commit**
1198
+
1199
+ Run:
1200
+
1201
+ ```bash
1202
+ uv run pytest tests/backends/test_llama_cpp.py -v
1203
+ uv run pytest tests/backends -v
1204
+ ```
1205
+
1206
+ Expected: PASS.
1207
+
1208
+ ```bash
1209
+ git add src/open_cortex/backends/llama_cpp.py tests/backends tests/fixtures
1210
+ git commit -m "feat: stream truthful llama.cpp runtime events"
1211
+ ```
1212
+
1213
+ ### Task 11: Add Controlled, Isolated Experiment Load
1214
+
1215
+ **Files:**
1216
+ - Create: `src/open_cortex/backends/load.py`
1217
+ - Create: `tests/backends/test_load.py`
1218
+ - Modify: `src/open_cortex/backends/llama_cpp.py`
1219
+
1220
+ - [ ] **Step 1: Write lifecycle and scenario-isolation tests**
1221
+
1222
+ Using `pytest-httpx`, assert:
1223
+
1224
+ - Normal and long-context scenarios create no background requests.
1225
+ - Memory pressure starts long-prompt, cache-retaining requests.
1226
+ - Slow decode starts several short-prompt generation requests.
1227
+ - Every background request is cancelled/joined after foreground completion.
1228
+ - One session cannot stop another session's load handle.
1229
+ - Disabled settings prevent all load.
1230
+
1231
+ - [ ] **Step 2: Run tests to verify failure**
1232
+
1233
+ Run: `uv run pytest tests/backends/test_load.py -v`
1234
+
1235
+ Expected: FAIL because `ControlledLoadGenerator` is undefined.
1236
+
1237
+ - [ ] **Step 3: Implement bounded load handles**
1238
+
1239
+ ```python
1240
+ @dataclass
1241
+ class LoadHandle:
1242
+ request_id: str
1243
+ stop_event: threading.Event
1244
+ futures: list[Future[None]]
1245
+
1246
+ def close(self) -> None:
1247
+ self.stop_event.set()
1248
+ for future in self.futures:
1249
+ future.cancel()
1250
+ wait(self.futures, timeout=5)
1251
+
1252
+
1253
+ class ControlledLoadGenerator:
1254
+ def __init__(self, base_url: str, timeout: float, max_workers: int = 4):
1255
+ self.base_url = base_url
1256
+ self.timeout = timeout
1257
+ self.executor = ThreadPoolExecutor(max_workers=max_workers)
1258
+
1259
+ def start(
1260
+ self,
1261
+ request_id: str,
1262
+ scenario: ExperimentScenario,
1263
+ ) -> LoadHandle | None:
1264
+ if scenario is ExperimentScenario.MEMORY_PRESSURE:
1265
+ jobs = [(MEMORY_PRESSURE_PROMPT, 96)] * 2
1266
+ elif scenario is ExperimentScenario.SLOW_DECODE:
1267
+ jobs = [(SLOW_DECODE_PROMPT, 512)] * 4
1268
+ else:
1269
+ return None
1270
+ stop_event = threading.Event()
1271
+ futures = [
1272
+ self.executor.submit(
1273
+ run_load_request,
1274
+ self.base_url,
1275
+ self.timeout,
1276
+ prompt,
1277
+ n_predict,
1278
+ stop_event,
1279
+ )
1280
+ for prompt, n_predict in jobs
1281
+ ]
1282
+ return LoadHandle(request_id, stop_event, futures)
1283
+ ```
1284
+
1285
+ Memory pressure uses two long prompts with `cache_prompt=true` and bounded
1286
+ `n_predict`. Slow decode uses four short-context requests with larger
1287
+ `n_predict`. Use a dedicated `ThreadPoolExecutor(max_workers=4)`, explicit
1288
+ timeouts, and per-request stop events. No load runs for unsupported scenarios.
1289
+ Define the worker and prompts in the same module:
1290
+
1291
+ ```python
1292
+ MEMORY_PRESSURE_PROMPT = (
1293
+ "Retain the following numbered facts and wait for a recall question. "
1294
+ + " ".join(f"fact-{index}={index * 17}" for index in range(1200))
1295
+ )
1296
+ SLOW_DECODE_PROMPT = "Write a detailed but concise explanation of KV cache reuse."
1297
+
1298
+
1299
+ def run_load_request(
1300
+ base_url: str,
1301
+ timeout: float,
1302
+ prompt: str,
1303
+ n_predict: int,
1304
+ stop_event: threading.Event,
1305
+ ) -> None:
1306
+ with httpx.Client(base_url=base_url, timeout=timeout) as client:
1307
+ with client.stream(
1308
+ "POST",
1309
+ "/completion",
1310
+ json={
1311
+ "prompt": prompt,
1312
+ "stream": True,
1313
+ "cache_prompt": True,
1314
+ "n_predict": n_predict,
1315
+ },
1316
+ ) as response:
1317
+ response.raise_for_status()
1318
+ for _line in response.iter_lines():
1319
+ if stop_event.is_set():
1320
+ return
1321
+ ```
1322
+
1323
+ - [ ] **Step 4: Integrate cleanup with live streaming**
1324
+
1325
+ Start load immediately before the foreground `/completion`. Close it in a
1326
+ `finally` block for success, cancellation, timeout, and parse errors. Display
1327
+ only resulting llama.cpp metrics and foreground cadence; do not display the
1328
+ requested worker count as runtime evidence.
1329
+
1330
+ - [ ] **Step 5: Verify and commit**
1331
+
1332
+ Run: `uv run pytest tests/backends/test_load.py tests/backends/test_llama_cpp.py -v`
1333
+
1334
+ Expected: PASS and no worker threads remain alive.
1335
+
1336
+ ```bash
1337
+ git add src/open_cortex/backends/load.py src/open_cortex/backends/llama_cpp.py tests
1338
+ git commit -m "feat: add isolated runtime stress experiments"
1339
+ ```
1340
+
1341
+ ### Task 12: Select Backends and Complete Live UI Behavior
1342
+
1343
+ **Files:**
1344
+ - Modify: `src/open_cortex/ui/app.py`
1345
+ - Modify: `src/open_cortex/controller.py`
1346
+ - Modify: `src/open_cortex/config.py`
1347
+ - Create: `tests/ui/test_backend_selection.py`
1348
+ - Create: `tests/ui/test_experiment_behavior.py`
1349
+
1350
+ - [ ] **Step 1: Write backend selection and experiment behavior tests**
1351
+
1352
+ Assert `simulated` selects `SimulatedBackend`, `llama_cpp` selects
1353
+ `LlamaCppBackend`, and unknown values raise a startup error. For each scenario,
1354
+ feed contract events through `ChatController` and assert:
1355
+
1356
+ | Scenario | Required visible effect |
1357
+ | --- | --- |
1358
+ | Normal | stable core and smooth token flow |
1359
+ | Long context | filled tape and longer prefill; decode can recover |
1360
+ | Memory pressure | fragmenting KV cells only |
1361
+ | Slow decode | broken token conduit and naturally delayed text only |
1362
+ | Context collapse | old messages dimmed, boundary visible, context tape released |
1363
+
1364
+ - [ ] **Step 2: Run tests to verify failure**
1365
+
1366
+ Run:
1367
+
1368
+ ```bash
1369
+ uv run pytest tests/ui/test_backend_selection.py \
1370
+ tests/ui/test_experiment_behavior.py -v
1371
+ ```
1372
+
1373
+ Expected: FAIL until backend selection and scenario flags are wired.
1374
+
1375
+ - [ ] **Step 3: Implement backend factory and disabled controls**
1376
+
1377
+ Add `create_backend(settings)` to `ui/app.py`. Render disabled experiment
1378
+ buttons with the exact reason returned by `supported_experiments()`. The header
1379
+ must show `SIMULATED` in simulator mode and `LIVE` in llama.cpp mode.
1380
+
1381
+ - [ ] **Step 4: Make model behavior expressive**
1382
+
1383
+ Inject one short scenario system instruction only in live mode:
1384
+
1385
+ - Memory pressure: preserve recent details but admit uncertainty when asked
1386
+ about early details.
1387
+ - Slow decode: no explanatory instruction; actual streaming timing is enough.
1388
+ - Context collapse: answer only from active messages and do not claim access to
1389
+ forgotten turns.
1390
+ - Long context: answer normally; do not narrate context pressure.
1391
+
1392
+ Do not tell the model to describe UI organs, colors, metrics, or experiments.
1393
+
1394
+ - [ ] **Step 5: Verify live behavior manually**
1395
+
1396
+ Start llama.cpp:
1397
+
1398
+ ```bash
1399
+ llama-server -m "$MODEL_GGUF" -c 4096 --metrics --host 127.0.0.1 --port 8080
1400
+ ```
1401
+
1402
+ Start OpenCortex:
1403
+
1404
+ ```bash
1405
+ OPEN_CORTEX_BACKEND=llama_cpp uv run python app.py
1406
+ ```
1407
+
1408
+ Expected: all supported experiments affect the intended organ and assistant
1409
+ experience; metrics disappear rather than becoming synthetic when `/metrics`
1410
+ is disabled.
1411
+
1412
+ - [ ] **Step 6: Verify and commit**
1413
+
1414
+ Run:
1415
+
1416
+ ```bash
1417
+ uv run pytest -v
1418
+ node --test tests/js/open_cortex.test.js
1419
+ uv run ruff check .
1420
+ ```
1421
+
1422
+ Expected: PASS.
1423
+
1424
+ ```bash
1425
+ git add src/open_cortex tests
1426
+ git commit -m "feat: connect live runtime to observatory"
1427
+ ```
1428
+
1429
+ ### Task 13: Benchmark the Model and Choose Context From Evidence
1430
+
1431
+ **Files:**
1432
+ - Create: `src/open_cortex/runtime/benchmark.py`
1433
+ - Create: `tests/runtime/test_benchmark.py`
1434
+ - Create: `benchmarks/.gitkeep`
1435
+ - Modify: `.gitignore`
1436
+
1437
+ - [ ] **Step 1: Write aggregation and selection tests**
1438
+
1439
+ Test median aggregation for TTFT, prompt TPS, decode TPS, and peak KV usage.
1440
+ Test selection across 2048, 4096, and 8192 contexts using these rules:
1441
+
1442
+ 1. Bilingual prompts complete without error.
1443
+ 2. Peak KV usage remains below 80% under normal chat.
1444
+ 3. Median TTFT remains below the configured demo ceiling.
1445
+ 4. Highest context satisfying the first three wins.
1446
+
1447
+ - [ ] **Step 2: Run tests to verify failure**
1448
+
1449
+ Run: `uv run pytest tests/runtime/test_benchmark.py -v`
1450
+
1451
+ Expected: FAIL because benchmark functions are undefined.
1452
+
1453
+ - [ ] **Step 3: Implement the CLI**
1454
+
1455
+ `open-cortex-benchmark` accepts:
1456
+
1457
+ ```text
1458
+ --base-url
1459
+ --model-name
1460
+ --contexts 2048 4096 8192
1461
+ --runs 3
1462
+ --ttft-ceiling-ms 2500
1463
+ --output benchmarks/<name>.json
1464
+ ```
1465
+
1466
+ For each context configuration, run one English prompt, one Chinese prompt,
1467
+ one long-context prompt, and one 128-token decode. Store raw runs plus a
1468
+ `BenchmarkProfile`. The CLI does not restart llama-server; print the exact
1469
+ `llama-server -c <size>` command required for each pass and accept one context
1470
+ per invocation when automated restart is unavailable.
1471
+
1472
+ - [ ] **Step 4: Document and perform the benchmark**
1473
+
1474
+ Run once for 2K, 4K, and 8K against the target hardware. Commit the resulting
1475
+ small JSON profiles, not model files or raw prompts containing user data.
1476
+ Update `OPEN_CORTEX_CONTEXT_SIZE` only after comparing the profiles.
1477
+
1478
+ - [ ] **Step 5: Verify and commit**
1479
+
1480
+ Run: `uv run pytest tests/runtime/test_benchmark.py -v`
1481
+
1482
+ Expected: PASS.
1483
+
1484
+ ```bash
1485
+ git add src/open_cortex/runtime/benchmark.py tests/runtime/test_benchmark.py \
1486
+ benchmarks .gitignore
1487
+ git commit -m "feat: benchmark runtime and select context size"
1488
+ ```
1489
+
1490
+ ### Task 14: Package Hugging Face and Optional Modal Deployment
1491
+
1492
+ **Files:**
1493
+ - Create: `deploy/huggingface/README.md`
1494
+ - Create: `deploy/modal/modal_app.py`
1495
+ - Create: `tests/deploy/test_modal_config.py`
1496
+ - Modify: `README.md`
1497
+
1498
+ - [ ] **Step 1: Write a deployment configuration test**
1499
+
1500
+ Import `deploy/modal/modal_app.py` without starting Modal and assert its
1501
+ llama-server command includes `--metrics`, `--host 0.0.0.0`, the configured
1502
+ context, and no public `/slots` requirement. Keep command construction in a
1503
+ pure `build_llama_command()` function so the test does not need Modal
1504
+ credentials.
1505
+
1506
+ - [ ] **Step 2: Run tests to verify failure**
1507
+
1508
+ Run: `uv run pytest tests/deploy/test_modal_config.py -v`
1509
+
1510
+ Expected: FAIL because the deployment module is absent.
1511
+
1512
+ - [ ] **Step 3: Add the self-contained Space instructions**
1513
+
1514
+ Document:
1515
+
1516
+ - Space SDK `gradio`.
1517
+ - Required environment variables.
1518
+ - CPU command that launches llama-server before `python app.py`.
1519
+ - Model license and download procedure.
1520
+ - Why ZeroGPU is not assumed for llama.cpp.
1521
+ - Health checks for `/health` and `/metrics`.
1522
+ - Simulator mode for screenshots only, clearly labeled.
1523
+
1524
+ - [ ] **Step 4: Add the optional Modal backend**
1525
+
1526
+ `modal_app.py` must:
1527
+
1528
+ - Read model repo/file and context from secrets/environment.
1529
+ - Cache the GGUF in a Modal Volume.
1530
+ - Start `llama-server` with `--metrics`, bounded parallel slots, and the
1531
+ benchmark-selected context.
1532
+ - Expose one authenticated or unguessable HTTPS endpoint to the Space.
1533
+ - Keep `/slots` disabled publicly.
1534
+ - Build the server command in a pure function tested in Step 1.
1535
+
1536
+ The Gradio application remains unchanged; only
1537
+ `OPEN_CORTEX_LLAMA_BASE_URL` and `OPEN_CORTEX_REMOTE_BACKEND=true` differ.
1538
+
1539
+ - [ ] **Step 5: Complete the repository README**
1540
+
1541
+ Include:
1542
+
1543
+ - Product statement and screenshot location.
1544
+ - `mise install`, `uv sync`, and simulator quick start.
1545
+ - Local llama.cpp command.
1546
+ - Live-mode environment table.
1547
+ - Benchmark command.
1548
+ - Five experiment semantics.
1549
+ - Data truthfulness statement.
1550
+ - Local, Space, and Space-plus-Modal deployment choices.
1551
+ - Test commands.
1552
+
1553
+ - [ ] **Step 6: Verify and commit**
1554
+
1555
+ Run:
1556
+
1557
+ ```bash
1558
+ uv run pytest -v
1559
+ node --test tests/js/open_cortex.test.js
1560
+ uv run ruff check .
1561
+ ```
1562
+
1563
+ Expected: PASS.
1564
+
1565
+ ```bash
1566
+ git add README.md deploy tests/deploy
1567
+ git commit -m "docs: package OpenCortex demo deployment"
1568
+ ```
1569
+
1570
+ ### Task 15: Perform Acceptance Verification and Demo Rehearsal
1571
+
1572
+ **Files:**
1573
+ - Create: `docs/demo-script.md`
1574
+ - Create: `docs/verification.md`
1575
+
1576
+ - [ ] **Step 1: Run the complete automated suite**
1577
+
1578
+ Run:
1579
+
1580
+ ```bash
1581
+ uv run pytest --cov=open_cortex --cov-report=term-missing
1582
+ node --test tests/js/open_cortex.test.js
1583
+ uv run ruff check .
1584
+ ```
1585
+
1586
+ Expected: all tests pass, no uncovered critical branch in state derivation,
1587
+ context forgetting, SSE failure, or load cleanup.
1588
+
1589
+ - [ ] **Step 2: Verify simulator acceptance**
1590
+
1591
+ Run: `OPEN_CORTEX_BACKEND=simulated uv run python app.py`
1592
+
1593
+ Check and record:
1594
+
1595
+ - First screen reads as an inference observatory.
1596
+ - All five experiments differ without reading metric labels.
1597
+ - Context collapse retains old messages and shows the active boundary.
1598
+ - Splitter collapses below threshold, restores, and supports double click.
1599
+ - English/Chinese switch updates static and semantic copy.
1600
+ - Reduced-motion mode remains understandable.
1601
+
1602
+ - [ ] **Step 3: Verify live local acceptance**
1603
+
1604
+ Run the selected GGUF and benchmark context with `--metrics`. Check:
1605
+
1606
+ - Prefill and decode are distinct.
1607
+ - Output timing follows actual SSE arrivals.
1608
+ - KV, context, TPS, and TTFT values match source evidence.
1609
+ - `/metrics` outage hides evidence.
1610
+ - Stream interruption preserves partial text.
1611
+ - Load experiment workers stop after each request.
1612
+
1613
+ - [ ] **Step 4: Verify remote acceptance**
1614
+
1615
+ Point the same UI at the Modal URL. Confirm backend network time is displayed
1616
+ separately from TTFT and no code or visual changes are needed.
1617
+
1618
+ - [ ] **Step 5: Write the 3-5 minute demo script**
1619
+
1620
+ Use this order:
1621
+
1622
+ 1. Normal chat establishes the core pulse and real evidence.
1623
+ 2. Long context visibly fills the tape and lengthens prefill.
1624
+ 3. Memory pressure fragments KV cells without slowing the token conduit.
1625
+ 4. Slow decode produces stop-burst-stop output with healthy memory.
1626
+ 5. Context collapse marks forgotten history and shows failed early recall.
1627
+ 6. Collapse the chat drawer to finish on the full observatory view.
1628
+
1629
+ - [ ] **Step 6: Record final evidence and commit**
1630
+
1631
+ `docs/verification.md` records date, model, quantization, hardware, selected
1632
+ context, benchmark profile, test commands, Space URL, backend mode, and known
1633
+ limitations.
1634
+
1635
+ ```bash
1636
+ git add docs/demo-script.md docs/verification.md
1637
+ git commit -m "test: verify OpenCortex MVP acceptance"
1638
+ ```
1639
+
1640
+ ## Completion Gate
1641
+
1642
+ Do not call the MVP complete until all conditions hold:
1643
+
1644
+ - Simulator and live backends emit the same event contract.
1645
+ - No live metric is estimated or copied from simulator values.
1646
+ - The five experiments have distinct causes, visual effects, and assistant
1647
+ behavior.
1648
+ - Context forgetting uses the model's own chat template and tokenizer.
1649
+ - Memory pressure and slow decode load are bounded, isolated, and disabled
1650
+ unless explicitly enabled.
1651
+ - The UI works in English and Chinese, with a draggable/collapsible conversation
1652
+ panel.
1653
+ - Local and remote llama.cpp use the same backend adapter.
1654
+ - Automated tests, manual acceptance, and deployment documentation are complete.
makefile ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ llama-server:
2
+ llama-server \
3
+ -hf Qwen/Qwen2.5-1.5B-Instruct-GGUF:Q4_K_M \
4
+ -c 2048 -t 8 -tb 8 \
5
+ --metrics \
6
+ --host 127.0.0.1 --port 8080
7
+
8
+ llama-cli:
9
+ llama-cli \
10
+ -hf Qwen/Qwen2.5-1.5B-Instruct-GGUF:Q4_K_M \
11
+ -c 2048 -t 8 \
12
+ -n 128 -cnv
open_cortex/__init__.py ADDED
File without changes
open_cortex/runtime/__init__.py ADDED
File without changes
open_cortex/runtime/client.py ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import time
3
+ from collections.abc import Iterator
4
+ import httpx
5
+
6
+ from open_cortex.runtime.events import RuntimeEvent
7
+ from open_cortex.runtime.metrics import fetch_runtime_snapshot
8
+ from open_cortex.runtime.messages import ChatMessage, to_llama_messages
9
+
10
+
11
+ CHAT_URL = "http://127.0.0.1:8080/v1/chat/completions"
12
+ 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),
19
+ "temperature": 0.2,
20
+ "max_tokens": 100,
21
+ "stream": True,
22
+ "stream_options": {"include_usage": True},
23
+ "timings_per_token": True,
24
+ }
25
+
26
+ request_started = time.perf_counter()
27
+ first_token_seen = False
28
+ final_stats = None
29
+
30
+ yield RuntimeEvent(
31
+ kind="request_started",
32
+ text_delta="",
33
+ ttft_ms=None,
34
+ snapshot=None,
35
+ )
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
+ response.raise_for_status()
40
+
41
+ for line in response.iter_lines():
42
+ if not line.startswith("data: "):
43
+ continue
44
+
45
+ data = line.removeprefix("data: ")
46
+
47
+ if data == "[DONE]":
48
+ break
49
+
50
+ event = json.loads(data)
51
+ choices = event.get("choices", [])
52
+
53
+ if choices:
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 = time.perf_counter()
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"):
81
+ final_stats = event
82
+
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",
89
+ text_delta="",
90
+ ttft_ms=None,
91
+ snapshot=None,
92
+ prompt_tokens=usage["prompt_tokens"],
93
+ completion_tokens=usage["completion_tokens"],
94
+ prompt_tps=timings["prompt_per_second"],
95
+ decode_tps=timings["predicted_per_second"],
96
+ )
open_cortex/runtime/events.py ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dataclasses import dataclass
2
+ from typing import Literal
3
+
4
+ from open_cortex.runtime.metrics import RuntimeSnapshot
5
+
6
+
7
+ RuntimeEventKind = Literal[
8
+ "request_started",
9
+ "first_token",
10
+ "token",
11
+ "request_completed",
12
+ ]
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class RuntimeEvent:
17
+ kind: RuntimeEventKind
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
open_cortex/runtime/messages.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dataclasses import dataclass
2
+ from typing import Literal
3
+
4
+
5
+ Role = Literal["system", "user", "assistant"]
6
+
7
+
8
+ @dataclass(frozen=True)
9
+ class ChatMessage:
10
+ role: Role
11
+ content: str
12
+
13
+
14
+ def to_llama_messages(messages: list[ChatMessage]) -> list[dict[str, str]]:
15
+ return [
16
+ {
17
+ "role": message.role,
18
+ "content": message.content,
19
+ }
20
+ for message in messages
21
+ ]
open_cortex/runtime/metrics.py ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dataclasses import dataclass
2
+ import httpx
3
+
4
+
5
+ @dataclass(frozen=True)
6
+ class RuntimeSnapshot:
7
+ prompt_tps: float | None
8
+ decode_tps: float | None
9
+ requests_processing: int | None
10
+ requests_deferred: int | None
11
+ active_slots: int
12
+ slot_context_tokens: tuple[int, ...]
13
+ slot_context_size: int | None
14
+
15
+
16
+ def parse_prometheus_value(text: str, name: str) -> float | None:
17
+ prefix = f"{name} "
18
+ for line in text.splitlines():
19
+ if line.startswith(prefix):
20
+ return float(line.removeprefix(prefix))
21
+ return None
22
+
23
+ def fetch_runtime_snapshot(
24
+ client: httpx.Client,
25
+ metrics_url: str,
26
+ slots_url: str,
27
+ ) -> RuntimeSnapshot:
28
+ metrics_response = client.get(metrics_url)
29
+ metrics_response.raise_for_status()
30
+
31
+ slots_response = client.get(slots_url)
32
+ slots_response.raise_for_status()
33
+
34
+ metrics_text = metrics_response.text
35
+ slots = slots_response.json()
36
+
37
+ active_slots = [
38
+ slot
39
+ for slot in slots
40
+ if slot.get("is_processing") is True
41
+ ]
42
+
43
+ requests_processing = parse_prometheus_value(
44
+ metrics_text,
45
+ "llamacpp:requests_processing",
46
+ )
47
+ requests_deferred = parse_prometheus_value(
48
+ metrics_text,
49
+ "llamacpp:requests_deferred",
50
+ )
51
+
52
+ return RuntimeSnapshot(
53
+ prompt_tps=parse_prometheus_value(
54
+ metrics_text,
55
+ "llamacpp:prompt_tokens_seconds",
56
+ ),
57
+ decode_tps=parse_prometheus_value(
58
+ metrics_text,
59
+ "llamacpp:predicted_tokens_seconds",
60
+ ),
61
+ requests_processing=(
62
+ int(requests_processing)
63
+ if requests_processing is not None
64
+ else None
65
+ ),
66
+ requests_deferred=(
67
+ int(requests_deferred)
68
+ if requests_deferred is not None
69
+ else None
70
+ ),
71
+ active_slots=len(active_slots),
72
+ slot_context_tokens=tuple(
73
+ int(slot["n_prompt_tokens"])
74
+ for slot in active_slots
75
+ if slot.get("n_prompt_tokens") is not None
76
+ ),
77
+ slot_context_size=(
78
+ int(slots[0]["n_ctx"])
79
+ if slots and slots[0].get("n_ctx") is not None
80
+ else None
81
+ ),
82
+ )
pyproject.toml CHANGED
@@ -4,4 +4,15 @@ version = "0.1.0"
4
  description = "Add your description here"
5
  readme = "README.md"
6
  requires-python = ">=3.12"
7
- dependencies = []
 
 
 
 
 
 
 
 
 
 
 
 
4
  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
+ [[tool.uv.index]]
13
+ url = "https://pypi.tuna.tsinghua.edu.cn/simple"
14
+ default = true
15
+
16
+ [build-system]
17
+ requires = ["hatchling"]
18
+ build-backend = "hatchling.build"
scripts/debug_events.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+
3
+ from open_cortex.runtime.client import stream_chat_events
4
+ from open_cortex.runtime.messages import ChatMessage
5
+
6
+
7
+ messages = [
8
+ ChatMessage(
9
+ role="system",
10
+ content="你是一名严谨的机器学习系统教师。",
11
+ ),
12
+ ChatMessage(
13
+ role="user",
14
+ content="用三句话解释大语言模型推理。",
15
+ ),
16
+ ]
17
+
18
+ for event in stream_chat_events(messages):
19
+ if event.text_delta:
20
+ print(event.text_delta, end="", flush=True)
21
+
22
+ if event.kind in {"first_token", "request_completed"}:
23
+ print(f"\n\nEVENT: {event}\n", file=sys.stderr)
scripts/gradio_chat.py ADDED
File without changes
scripts/runtime_snapshot.py ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import sys
3
+ import time
4
+
5
+ import httpx
6
+
7
+ from open_cortex.runtime.metrics import fetch_runtime_snapshot
8
+
9
+ URL = "http://127.0.0.1:8080/v1/chat/completions"
10
+ METRICS_URL = "http://127.0.0.1:8080/metrics"
11
+ SLOTS_URL = "http://127.0.0.1:8080/slots"
12
+
13
+ def parse_prometheus_value(text: str, name: str) -> float | None:
14
+ prefix = f"{name}"
15
+ for line in text.splitlines():
16
+ if line.startswith(prefix):
17
+ return float(line.removeprefix(prefix))
18
+
19
+ return None
20
+
21
+ request_body = {
22
+ "messages": [
23
+ {
24
+ "role": "user",
25
+ "content": "用三句话解释大语言模型的推理过程。",
26
+ }
27
+ ],
28
+ "temperature": 0.2,
29
+ "max_tokens": 100,
30
+ "stream": True,
31
+ "stream_options": {"include_usage": True},
32
+ "timings_per_token": True,
33
+ }
34
+
35
+ request_started = time.perf_counter()
36
+ first_token_at = None
37
+ final_stats = None
38
+
39
+ with httpx.Client(timeout=120.0) as client:
40
+ with client.stream("POST", URL, json=request_body) as response:
41
+ response.raise_for_status()
42
+
43
+ for line in response.iter_lines():
44
+ if not line.startswith("data: "):
45
+ continue
46
+
47
+ data = line.removeprefix("data: ")
48
+
49
+ if data == "[DONE]":
50
+ break
51
+
52
+ event = json.loads(data)
53
+ choices = event.get("choices", [])
54
+
55
+ if choices:
56
+ content = choices[0].get("delta", {}).get("content")
57
+
58
+ if content:
59
+ if first_token_at is None:
60
+ first_token_at = time.perf_counter()
61
+ snapshot = fetch_runtime_snapshot(client,METRICS_URL,SLOTS_URL)
62
+ print(f"\n\nFIRST TOKEN SNAPSHOT: {snapshot}\n", file = sys.stderr)
63
+ print(content, end="", flush=True)
64
+
65
+ if event.get("usage"):
66
+ final_stats = event
67
+
68
+ print()
69
+
70
+ if first_token_at is not None:
71
+ ttft_ms = (first_token_at - request_started) * 1000
72
+ print(f"TTFT: {ttft_ms:.1f} ms", file=sys.stderr)
73
+
74
+ if final_stats is not None:
75
+ usage = final_stats["usage"]
76
+ timings = final_stats["timings"]
77
+
78
+ print(f"Prompt tokens: {usage['prompt_tokens']}", file=sys.stderr)
79
+ print(f"Output tokens: {usage['completion_tokens']}", file=sys.stderr)
80
+ print(
81
+ f"Prefill: {timings['prompt_per_second']:.1f} tok/s",
82
+ file=sys.stderr,
83
+ )
84
+ print(
85
+ f"Decode: {timings['predicted_per_second']:.1f} tok/s",
86
+ file=sys.stderr,
87
+ )
scripts/stream_chat.py ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import sys
3
+ import time
4
+
5
+ import httpx
6
+
7
+
8
+ URL = "http://127.0.0.1:8080/v1/chat/completions"
9
+
10
+ request_body = {
11
+ "messages": [
12
+ {
13
+ "role": "user",
14
+ "content": "用三句话解释大语言模型的推理过程。",
15
+ }
16
+ ],
17
+ "temperature": 0.2,
18
+ "max_tokens": 100,
19
+ "stream": True,
20
+ "stream_options": {"include_usage": True},
21
+ "timings_per_token": True,
22
+ }
23
+
24
+ request_started = time.perf_counter()
25
+ first_token_at = None
26
+ final_stats = None
27
+
28
+ with httpx.Client(timeout=120.0) as client:
29
+ with client.stream("POST", URL, json=request_body) as response:
30
+ response.raise_for_status()
31
+
32
+ for line in response.iter_lines():
33
+ if not line.startswith("data: "):
34
+ continue
35
+
36
+ data = line.removeprefix("data: ")
37
+
38
+ if data == "[DONE]":
39
+ break
40
+
41
+ event = json.loads(data)
42
+ choices = event.get("choices", [])
43
+
44
+ if choices:
45
+ content = choices[0].get("delta", {}).get("content")
46
+
47
+ if content:
48
+ if first_token_at is None:
49
+ first_token_at = time.perf_counter()
50
+
51
+ print(content, end="", flush=True)
52
+
53
+ if event.get("usage"):
54
+ final_stats = event
55
+
56
+ print()
57
+
58
+ if first_token_at is not None:
59
+ ttft_ms = (first_token_at - request_started) * 1000
60
+ print(f"TTFT: {ttft_ms:.1f} ms", file=sys.stderr)
61
+
62
+ if final_stats is not None:
63
+ usage = final_stats["usage"]
64
+ timings = final_stats["timings"]
65
+
66
+ print(f"Prompt tokens: {usage['prompt_tokens']}", file=sys.stderr)
67
+ print(f"Output tokens: {usage['completion_tokens']}", file=sys.stderr)
68
+ print(
69
+ f"Prefill: {timings['prompt_per_second']:.1f} tok/s",
70
+ file=sys.stderr,
71
+ )
72
+ print(
73
+ f"Decode: {timings['predicted_per_second']:.1f} tok/s",
74
+ file=sys.stderr,
75
+ )
uv.lock ADDED
The diff for this file is too large to render. See raw diff