Spaces:
Running on Zero
Running on Zero
| # Observer Pattern β Decoupled Rendering | |
| ## Design contract | |
| The observer is the camera crew. It has exactly one rule: **never append events**. | |
| ```mermaid | |
| flowchart LR | |
| Conductor --> Agents | |
| Agents -->|append| Ledger[("Ledger")] | |
| Ledger -->|read only| Obs["Observer.consume()"] | |
| Obs --> Diff["ViewDiff β the delta"] | |
| Diff --> Out["UI callback Β· SSE Β· WebSocket"] | |
| ``` | |
| The cognitive loop (left) writes; the observer (right) only reads. The world runs | |
| identically whether or not any observer is attached. | |
| The cognitive loop runs identically whether or not any observer is attached. | |
| The world is not "observed into existence" β it runs, and the observer watches. | |
| --- | |
| ## What this buys you | |
| ### 1. Multiple renderers off one ledger | |
| ```python | |
| stage_observer = Observer() # renders the Gradio stage markdown | |
| graph_observer = Observer() # renders the cognition graph | |
| chatlog_observer = Observer() # renders a plain chat log | |
| # All three subscribe to the same ledger; all three update independently | |
| conductor = Conductor(scenario, observer=stage_observer) | |
| ``` | |
| ### 2. Record and replay | |
| Because the ledger is the only source of truth: | |
| ```python | |
| # Record a run by keeping the SQLiteLedger file. | |
| # Replay by feeding saved events to a fresh observer: | |
| for event in saved_ledger.events: | |
| replay_observer.consume(event) | |
| ``` | |
| The UI can replay any past run, pause it, or fast-forward to a specific turn. | |
| ### 3. Testable rendering | |
| The stage is a pure function of the ledger: | |
| ```python | |
| # Projections (and therefore observer views) are deterministically testable | |
| events = (world_observed_event, judge_verdict_event) | |
| obs = Observer() | |
| for e in events: | |
| obs.consume(e) | |
| assert obs.view.judge_notes == ["keep it"] | |
| ``` | |
| --- | |
| ## ViewDiff β streaming the delta | |
| Instead of re-rendering the full state each turn, the observer computes a diff: | |
| ```python | |
| @dataclass | |
| class ViewDiff: | |
| scene_changed: bool | |
| new_scene: str | |
| new_agent_notes: list[str] | |
| new_judge_notes: list[str] | |
| new_user_artifacts: list[str] | |
| @property | |
| def has_changes(self) -> bool: ... | |
| ``` | |
| Only the diff is sent to the client β the right shape for SSE/WebSocket streaming. | |
| --- | |
| ## Gradio integration (current) | |
| Currently the Gradio UI re-renders by calling `conductor.projection` after each step. | |
| This works but does a full rebuild from the ledger on every render. | |
| The upgrade path (Phase 3): | |
| 1. Attach an observer to the conductor at startup. | |
| 2. Use `gr.State` + `gr.update()` from the observer callbacks. | |
| 3. For real-time streaming, wire a generator-based Gradio output to the observer callback queue. | |
| ```python | |
| # Future pattern (Phase 3) | |
| obs = Observer() | |
| conductor = Conductor(scenario, observer=obs) | |
| def stream_step(): | |
| conductor.step() | |
| # obs.consume() was called inside conductor._append() | |
| # retrieve the latest diff from obs | |
| diff = obs.latest_diff() | |
| if diff.scene_changed: | |
| yield gr.update(value=render_stage(obs.view)) | |
| ``` | |
| This upgrade is realized by the **Fishbowl UI** β see | |
| [ADR-0021](../adr/0021-fishbowl-ui-gradio-presenter.md) and the plan in | |
| [next-steps/fishbowl-ui.md](next-steps/fishbowl-ui.md) β where the Show is a `gr.HTML` | |
| stage re-rendered by a `gr.Timer` over a presenter that derives per-agent state from | |
| the ledger (a new, pure projection alongside `rebuild_stage`). | |
| --- | |
| ## Callback safety | |
| Callbacks are synchronous and called inline during `consume()`. | |
| They must not: | |
| - Append events to the ledger (breaks the read/write separation) | |
| - Block for more than a few milliseconds (blocks the conductor loop) | |
| - Raise exceptions unchecked (propagates into the conductor) | |
| The `Observer` class does not catch callback exceptions β the caller is responsible | |
| for wrapping callbacks in try/except if they can fail. | |