# car-ui-agent

In-process UI-improvement agent for the adaptive A2UI rendering loop. Subscribes to `a2ui.render_report` telemetry (#180) and returns a `Decision` the runtime routes through `car-a2ui`'s surface store.

## Loop

```
renderer ─► a2ui.render_report ──► car-server-core handle_a2ui_render_report
                                            │
                                            ▼
                              UIImprovementAgent::on_render_report
                                            │
                                            ▼
                                       Decision::Patch
                                            │
                                            ▼
                              apply_a2ui_envelope (existing path)
                                            │
                                            ▼
                              surface mutated, broadcast as a2ui.event
```

## Decision contract

```rust
pub enum Decision {
    Patch {
        envelope: car_a2ui::PatchComponents,
        strategy_id: String,
        patch_hash: u64,         // for runtime-side oscillation detection
        elapsed_ns: u128,         // observability budget
    },
    StableNoChange,
    HardStop { reason: String }, // placeholder_count > 0
}
```

The agent is **pure decision logic** — no I/O, no async, no persistence handles. Persistence (memgine ingest, durable budgets) is the caller's responsibility because lock-flavor (`std::sync` vs `tokio::sync`) belongs to the runtime.

## Termination

The agent stops proposing when any of:

1. **`placeholder_count > 0`** — invariant violation (`Decision::HardStop`)
2. **`sequence - signature_stable_since >= stability_window`** — signature unchanged for N renders (default `DEFAULT_STABILITY_WINDOW = 3`)
3. **Already decided at this report** — dedup token chain: `sequence` → `report_id` → signature-content hash
4. **No strategy proposed** — first strategy with a non-empty proposal wins; none matched

## Strategy contract

```rust
pub trait RenderingStrategy: Send + Sync {
    fn id(&self) -> &'static str;
    fn proposes(
        &self,
        report: &RenderReport,
        signature: &IntentSignature,
        surface: &A2uiSurface,
    ) -> Option<Value>;
}
```

Every strategy **MUST** emit a leading `test` precondition op guarding its assumption. Debug-asserted; in release builds a violation logs `warn!` and applies anyway, but the contract is "test or it's a TOCTOU bug waiting to happen."

## Integration

Held on `ServerState.ui_agent` as `Arc<UIImprovementAgent>`. Invoked from `handler.rs:handle_a2ui_render_report` after the report broadcast:

```rust
if let Some(surface) = state.a2ui.get(&report.surface_id).await {
    match state.ui_agent.on_render_report(&report, &surface) {
        Decision::Patch { envelope, .. } => {
            let env = A2uiEnvelope { patch_components: Some(envelope), ..Default::default() };
            apply_a2ui_envelope(state, env, None, None).await?;
        }
        Decision::StableNoChange => {}
        Decision::HardStop { reason } => tracing::error!(..., "halted"),
    }
}
```

## Builtin strategies

- `TagSmallListDomain` — adds `x-renderer.domain = "small_list"` to small lists (cardinality 2-5) lacking a domain hint
- `CompactDenseColumn` — adds `x-renderer.attention_budget = "scan"` to compact Columns on mobile viewports

Both are demo strategies — they tag x-renderer hints rather than restructuring the component tree. Real strategies (planned in #36/#37) will swap component types based on intent signature.

## Tests

- `cargo test -p car-ui-agent` — 9 unit tests covering decision branches, dedup, hard-stop, stability window, malformed signature, missing root
- `cargo test -p car-server-core --test embed_smoke ui_agent` — 2 integration tests exercising the full handler.rs path through `run_dispatch`

## Memgine seed data (deferred)

The agent currently uses static `Box<dyn RenderingStrategy>` entries — strategy retrieval is `O(n)` linear over the in-memory vec, not memgine-backed. Seeding the skill graph with strategy manifests at startup would be useful when the agent learns to *query* memgine (ranked retrieval, spreading-activation over related-surface history), but that's a v2 — adding seed data without a consumer would be storing manifests no code reads.

The memgine integration that *does* land in v1: `car-server-core` ingests a Conversation node tagged `ui-agent/<surface>` after each successful `Decision::Patch + apply`. That gives spreading activation real trace data to surface on future renders without needing pre-seeded strategy descriptors.

## Runtime convergence machinery (`car-server-core::ui_agent_loop`)

Two guards live outside the agent, in the runtime where workqueue-shaped state belongs:

- `OscillationDetector` — per-surface ring buffer of recent `patch_hash` values. A proposal that repeats one in the window is suppressed; the loop waits for the next signature change.
- `IterationBudget` — per-surface counter, capped at `DEFAULT_MAX_ITERATIONS = 20`. Surfaces that hit the cap skip the agent entirely until explicitly reset.

The agent itself stays stateless about loop history (per neo's review: "controllers use workqueue backoff; reconcilers stay stateless").

## Reference Swift implementation

`a2ui-shader/Sources/A2UIShaderAgent` — same Decision shape, same loop semantics, used in the renderer-side demo while car wasn't reachable. Loop converges identically on both sides.
