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_URLis the only backend URL web code reads (fallbackhttp://127.0.0.1:8080;http://ai-server:8080indocker-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_METADATAevents 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, elsedataadk, else first verified). e2e_verifiedon/systemsis 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 bypixi 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"inai/tests/features/matrix.py): skipped unless an engine declares them ine2e_registry.jsonunderfeature_suite.declared_capability_rows, blocking for engines that do.surface-analysisis the first. Which engines declare it is recorded inai/src/kavai/systems/e2e_registry.jsonand pinned bytest_the_engines_that_declare_the_capability— read those rather than a prose list. dataadk deliberately does not: its port tokavai-image-skillsis deferred by owner decision 2026-08-08 and its legacy analyzer path is unchanged. The row assertsIMAGE_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, andCUSTOMevents:IMAGE_GALLERY,DATASET_LIST,THERMAL_ANALYSIS,MARKDOWN_REPORT,THREE_D_OVERVIEW,CDC_PROVENANCE,IMAGE_ANALYSIS_RESULT(v2.4 — per-image findings recorded byweb/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 fromGET /api/images/{id}/suggestions, carrykind: 'suggestion'intoBoundingBoxVisualizer— 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}/analyzerecords the attempt, its provenance and its suggestions; the review panel (web/modules/gallery/overlays/components/suggestion-review-panel.tsx) accepts, rejects or recategorizes them throughPOST /api/annotation-review-decisions, which appends the decision and promotes an accepted suggestion to an annotation in one transaction. AnIMAGE_ANALYSIS_RESULToverlay 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 aPOST /images/analyzetaking a list: it would look like a job and behave like a long HTTP request, and durable collection runs are WP-6’sanalysis_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.mdholds the invariants true of every engine and where the facts come from. dataadkfails three mandatory rows (2026-08-09), and that is expected. It is the parity reference — a thin wrapper overkavai-dataadk, byte-synced with KavAppskavai_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.scopein 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.pywritesai/_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_sqlis read by enforcement, not convention — its_runaccepts only SELECT/WITH and rejects INSERT/UPDATE/DELETE/DROP. side_effectis 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
descriptionis product copy. It is the whole instruction a model gets about when to reach for the tool; the emitter rejects an empty one.