Web ↔︎ AI interface

Facts verified 2026-07-30 by claude-code/2026.07. Generated from the authored source at docs/llm-wiki/ai-interface.md — the wiki owns these facts (ADR: D1 of docs/plans/20260811_documentation_system_execution.md). Edit that file, then regenerate: python docs/portfolio/_build/generate_wiki_pages.py.


Request path

  • Browser never calls the AI backend directly: useAGUIClient (web/hooks/use-agui-client.ts) → POST /api/ag-ui-chat (web/app/api/ag-ui-chat/route.ts) → AI Gateway ${AI_SERVER_URL}/chat/agui/stream (:8080) → engine.
  • AI_SERVER_URL is the only backend URL web code reads (fallback http://127.0.0.1:8080; http://ai-server:8080 in docker-compose.yml). Engine fan-out happens in the gateway (ai/src/kavai/gateway/app.py, SYSTEM_TO_BACKEND).
  • The proxy relays the backend SSE line-by-line, then appends synthetic RUN_FINISHED + SESSION_METADATA events and persists assistant artifacts to Supabase. maxDuration = 600.

Engines

  • Valid engine ids (SystemId): argus | dataadk | kawa | orion.
  • Verified list via GET /api/systems (proxies gateway /systems, /systems/verified); useVerifiedAIEngines + useSelectedEngine (web/hooks/use-ai-engines.ts) resolve the effective engine (stored default if verified, else dataadk, else first verified).
  • e2e_verified on /systems is not a capability claim. How many product features an engine actually delivers is measured separately by the feature-matrix certification: docs/evaluation/ENGINE_CERTIFICATION.md, generated by pixi run certify-engines. Read the current status from that file, not from here — it is generated by the runner and carries an append-only History table, so prose copies of the numbers go stale within days. Check it before assuming an engine handles a given surface.
  • Capability rows (mandatory: "if-declared" in ai/tests/features/matrix.py): skipped unless an engine declares them in e2e_registry.json under feature_suite.declared_capability_rows, blocking for engines that do. surface-analysis is the first. Which engines declare it is recorded in ai/src/kavai/systems/e2e_registry.json and pinned by test_the_engines_that_declare_the_capability — read those rather than a prose list. dataadk deliberately does not: its port to kavai-image-skills is deferred by owner decision 2026-08-08 and its legacy analyzer path is unchanged. The row asserts IMAGE_ANALYSIS_RESULT (CONTRACT-CDC-001 v2.4 §7.8) plus a form-D report; plan: docs/plans/20260808_image_skills_certification_execution.md.

The AG-UI event contract

  • Normative contract: CONTRACT-CDC-001 — docs/handbooks/content/web/agui-contract.md (generated from the canonical source, lint-enforced). Do not invent event shapes.
  • Event families handled by the client (AgentSubscriber pattern): TEXT_MESSAGE_START/CONTENT/END, TOOL_CALL_START/RESULT, STATE_SNAPSHOT, STATE_DELTA (JSON-Patch), RUN_STARTED/FINISHED/ERROR, and CUSTOM events: IMAGE_GALLERY, DATASET_LIST, THERMAL_ANALYSIS, MARKDOWN_REPORT, THREE_D_OVERVIEW, CDC_PROVENANCE, IMAGE_ANALYSIS_RESULT (v2.4 — per-image findings recorded by web/lib/ag-ui/image-analysis.ts, rendered as annotation overlays in the gallery viewer).
  • Model output is drawn as model output. Findings from IMAGE_ANALYSIS_RESULT, and undecided suggestions read from GET /api/images/{id}/suggestions, carry kind: 'suggestion' into BoundingBoxVisualizer — dashed, one colour outside the category palette, labelled on the box. Stored annotations stay solid. Do not merge the two into one visual treatment: a reviewer accepting a box has to be able to tell which is which, and colour already means category.
  • Analysis can also start from the viewer, and that kind is durable. POST /api/images/{id}/analyze records the attempt, its provenance and its suggestions; the review panel (web/modules/gallery/overlays/components/suggestion-review-panel.tsx) accepts, rejects or recategorizes them through POST /api/annotation-review-decisions, which appends the decision and promotes an accepted suggestion to an annotation in one transaction. An IMAGE_ANALYSIS_RESULT overlay from a chat turn is session-scoped and vanishes on reload; a recorded suggestion does not. Plan: docs/plans/20260809_agent_tool_surface_and_analyze_execution.md.
  • There is no batch analysis endpoint, and that is deliberate. A selected set is analyzed from the gallery by calling the single-image route once per image — sequential, capped at 25, in the browser while the tab is open (web/modules/gallery/core/hooks/use-batch-analysis.ts). Each image records its own attempt, so a selection is indistinguishable downstream from that many separate clicks. Do not add a POST /images/analyze taking a list: it would look like a job and behave like a long HTTP request, and durable collection runs are WP-6’s analysis_runs.
  • Narrative guide (lifecycle, tool-call visibility, state sync): docs/handbooks/content/web/web-ai-interface.md.
  • Server side of the connection: the AI Handbook (docs/handbooks/content/ai/).

What the assistant is, and what it may call

  • Every engine has a charter — docs/ai/charters/<engine>.md: its job, scope, what it must decline, and a “must never” table where each entry names the mechanism enforcing it or is marked as intent only. charters/README.md holds the invariants true of every engine and where the facts come from.
  • dataadk fails three mandatory rows (2026-08-09), and that is expected. It is the parity reference — a thin wrapper over kavai-dataadk, byte-synced with KavApps kavai_server — and it moves at the KavApps team’s pace, not KAP’s. A row KAP has added fails there until it exists upstream and syncs. Read the matrix as distance from upstream, never as a defect list, and never patch dataadk locally: fix upstream and sync, because being the same as upstream is the only property it has. scope in particular is FR-AI-08 dataset-scope narrowing, enforced uniformly at the gateway (kavai.gateway.scope) — not tenant isolation, which is RLS and unaffected.
  • The tool catalog is emitted, not hand-listed: PYTHONPATH=ai/src python ai/scripts/emit_tool_catalog.py writes ai/_output/tools/kavai.tools.json — {name, description, side_effect, parameters} per tool, parameters as JSON Schema 2020-12. Build artifact (_output/ is git-ignored), so run the emitter rather than looking for a committed file.
  • Five tools today, all read: list_datasets, smart_image_search, execute_sql, ask_about_dataset, render_ui_component. execute_sql is read by enforcement, not convention — its _run accepts only SELECT/WITH and rejects INSERT/UPDATE/DELETE/DROP.
  • side_effect is declared, never inferred (read/write/external), and the base class defaults to empty so the emitter refuses a tool that declares nothing. Scope gap to know: engine adapters (systems/*/tools_adapter.py) wrap these and are not separately enumerated — an adapter inventing its own tool would not appear.
  • A tool’s description is product copy. It is the whole instruction a model gets about when to reach for the tool; the emitter rejects an empty one.

Authoritative sources

docs/handbooks/content/web/ai-integration.md · docs/handbooks/content/web/web-ai-interface.md · docs/handbooks/content/web/agui-contract.md · docs/evaluation/ENGINE_CERTIFICATION.md