AI-Native Shell¶
The AI-Native shell ("Agent mode") replaces the 5-phase wizard at /app/agent with a chat canvas. The agent drives the design — phase navigation, component selection, link budget, stackup, thermal, export — via a tool-call loop streamed over SSE. The legacy Guided wizard at /app is unchanged and remains the fallback when the flag is OFF or on mobile.
This guide answers "what is it, how do I switch it on, and where does the code live?"
TL;DR¶
- Flag:
VITE_FF_AI_NATIVE_SHELL(read once at module load viasrc/utils/featureFlags.ts → isAiNativeAvailable()) - Route:
/app/agentmountsAgentShell;/appkeeps the Guided wizard - Wire:
POST /api/chat/gemini/stream(fetch + ReadableStream — not EventSource) - Mobile:
<640 pxviewports keep the Guided sidebar regardless of flag state - Source of truth for changes:
docs/planning/ai_native_transformation/ai-native-transformation.md
Architecture¶
Browser /app/agent FastAPI Gemini
───────────────── ───────── ──────
AgentShell │
├─ PhaseNavigation POST /chat/gemini/stream │
└─ ChatCanvas ──────► fetch ──► verify_jwt │
├─ MessageBubble handle_chat_stream │
│ └─ ActionCard agent_loop.run_loop ───►│ send_message
│ └─ <registry> while iter < 16: │
├─ ChatInputBar yield thinking │
└─ agentStream.ts yield tool_call ◄┘
(SSE parser) dispatch_tool ─────►│ function_response
yield tool_result ◄─┘
if no tool_calls: done
Wire format¶
- Transport:
Content-Type: text/event-streamover fetch POST (lets us sendAuthorization: Bearer <Supabase JWT>;EventSourcecannot). - Framing:
event: <name>\ndata: <json>\n\nplus:keepalivecomments every 15 s to defeat proxies. - Event names:
thinking | tool_call | tool_result | text | done. - Parser:
frontend/src/services/chat/agentStream.tsowns cross-chunk buffering, multi-byte UTF-8, multi-linedata:, and keepalive drop.
Tool registry¶
10 tools dispatched by the loop, each verifies args.project_id == context.project_id (JWT-derived) before touching state. Failures encode as {ok: false, error} tool responses so the model can recover without the loop crashing.
| Phase | Tool |
|---|---|
| pre-existing | update_rf_specs, generate_block_diagram, update_stackup_config, run_surrogate_predict, run_stackup_solver |
| AI-native additions | goto_phase, select_components_batch, run_link_budget, run_thermal_analysis, run_pdn_analysis, run_glass_weave_analysis, generate_export, export_bom, load_stackup, load_bom, lookup_component_by_mpn, set_active_component |
run_solveris kept as a deprecated alias forrun_stackup_solverintool_dispatcher.pyso persisted conversations still route.
Registry source: backend/services/agent_loop/loop.py (loop) + backend/services/tool_dispatcher.py (routing) + backend/tools/ (handlers).
Export surface¶
generate_export exports kicad, odb, ads, gerber, ipc2581, hfss, magicon_internal, pdf, pptx. Each format branch delegates to the same generator the Guided HTTP route uses (routes/exports.py, routes/_export_utils.py, services/stackup_pdf_service, services/pptx_generator) — never a reimplementation. The kicad/odb/ads branches live in tools/generate_export.py; the widened branches live in tools/generate_export_formats.py and lazy-import their generators (so importing the tool never pulls in KiCad/pcbnew). Cross-surface byte parity is locked by tests/test_generate_export_parity.py. Per-format gating mirrors the routes (gerber/pdf ungated; ipc2581/hfss behind EXPORT_IPC2581/EXPORT_HFSS; magicon-internal gated in production only). pptx (full design report) derives its BOM/stackup from prior tool results and returns insufficient_design when too sparse. ADS/ODB pass Design-Intelligence/sidecar through when include_sidecar/include_design_intelligence is set (no more hardcoded None).
export_bom serializes the user's BOM (the latest bom_preview, else the applied phased BOM via load_bom) to csv/excel/json through the single-source services/export/bom_export_service.py, which formats the canonical assembler BOM and never re-assembles. The same service backs POST /api/export/bom, which the BomPreviewCard "Download BOM" button calls (services/export/bom/bomFileDownloadService.ts → shared services/export/fileDownload.ts). ExportReadyCard downloads inline base64 files (text fallback for the legacy kicad/ads payloads).
Both the widened formats and export_bom sit behind FF_AGENT_EXPORT_EXTENDED (backend, default on) — an agent-path kill switch: off restores the {kicad, odb, ads}-only surface and hides export_bom. Guided HTTP export routes never read this flag. Source change: OpenSpec agent-export-parity.
Action card registry (frontend)¶
Tool results render via frontend/src/components/Chat/actionCards/registry.tsx. Each tool_result.resultType maps to a card component — StackupInlineCard, BomPreviewCard, LinkBudgetResultCard, ExportReadyCard, KeyValueCard (the generic per-phase PhaseViewCard was retired 2026-06-08 in favor of these dedicated cards). Cards are memoized on result.id; without React.memo a 7-tool turn re-renders 30+ times.
Phase components inline rendering¶
Cards that mount full Phase components (e.g., StackupInlineCard → <StackupDesignView viewMode="inline" />) pass viewMode='inline' so the wizard chrome (step labels, "Back/Next") is suppressed and dense tables condense for the 896 px chat column. Default viewMode='full' keeps Guided behavior identical. The escape hatch for full interactivity is "Open full editor" → <ModeSwitcher> to /app.
Standalone calculator → agent handoff¶
/tools/{impedance,link-budget,stackup} show an "Open in agent" CTA when the flag is ON. The CTA writes a free-text seed to sessionStorage['agentSeed'] and navigates to /app/agent; ChatCanvas reads + clears the key on mount and pre-fills ChatInputBar (does not auto-send).
Public demo (planned — signed-out)¶
The Agent canvas gates behind sign-in (ChatCanvas.tsx → if (!user)), so landing visitors can't watch the agent run. Scripted demo mode was removed (§3.14). The replacement is a public, unauthenticated /demo/agent route that replays a real captured agent run over the production SSE framing and action-card registry — no Gemini quota, deterministic, and honest (it shows only what the real tools produced). Implemented (OpenSpec change add-public-demo-shell) behind FF_PUBLIC_DEMO (backend) / VITE_FF_PUBLIC_DEMO (frontend), both default off pending a reviewed transcript captured via backend/scripts/capture_demo_transcript.py (a grounded seed fixture ships for tests). Backend: routes/demo.py + services/demo/; frontend: components/Demo/DemoAgentShell.tsx + services/chat/demoStream.ts. Closing this is the top funnel fix — see docs/articles/ai-native-positioning.md.
Feature flag¶
- Read once via
isAiNativeAvailable()infrontend/src/utils/featureFlags.ts. All call sites import the helper rather thanimport.meta.envdirectly — keeps the flag fully mockable in tests and gives us a single chokepoint for future flag-source migrations (e.g., GrowthBook). agentStream.tsis dormant until the flag is ON: import graph is gated, verified byfrontend/src/__tests__/featureFlag/dormancy.test.ts(§7.9). If you wire the SSE consumer to a new mount point, add a row to that suite.- After editing root
.env, fully restart Vite (HMR does not re-read env). Quick trick:touch frontend/vite.config.tstriggers a full server restart.
Key files¶
Backend (backend/):
- services/agent_loop/loop.py — the loop itself (iteration cap, tool dispatch, function-response loopback); agent_loop is a package
- services/agent_loop_sse.py — SSE framing + keepalive emitter
- services/tool_dispatcher.py — name → executor registry (raises UnknownToolError for unregistered tools)
- services/conversation_store.py — server-side conversation persistence (Supabase)
- tools/ — per-tool handlers: solver_tool.py, surrogate_tool.py, run_link_budget.py, run_thermal_analysis.py, run_pdn_analysis.py, select_components_batch.py, block_diagram.py, goto_phase.py, stackup_advisor.py, rf_specs.py, generate_export.py (+ generate_export_formats.py), export_bom.py, load_stackup.py, load_bom.py, lookup_component.py, set_active_component.py
- services/export/bom_export_service.py — canonical BOM → csv/excel/json serializer (single source; backs export_bom + POST /api/export/bom)
- routes/chat_gemini.py — POST /api/chat/gemini/stream endpoint (JWT verify, project tenancy assertion)
Frontend (frontend/src/):
- components/AgentShell/AgentShell.tsx — shell composition (PhaseNavigation + ChatCanvas)
- components/Layout/ChatCanvas.tsx — message rendering + seed consumption + scroll
- components/Layout/PhaseNavigation.tsx — thin progress strip (mounted by AgentShell, not the wizard)
- components/Chat/actionCards/registry.tsx — resultType → card component map
- services/chat/agentStream.ts — SSE parser (the wire-format owner)
- hooks/chat/useGeminiChat.ts — chat state + send dispatcher (legacy + agent paths)
- utils/featureFlags.ts — isAiNativeAvailable() reader
- App.tsx — <PhaseProvider> hoisted above <Routes> so <ModeSwitcher> round-trips don't reset state
Tests:
- frontend/src/__tests__/featureFlag/dormancy.test.ts — agentStream.ts does not load when flag OFF
- frontend/src/__tests__/integration/aiNativeParity.test.tsx — flag OFF vs flag ON reach the same final state
- frontend/src/__tests__/App.aiNativeShell.test.tsx — route gating + Guided shell at /app
Switching modes¶
/app(Guided wizard) ⇄/app/agent(chat canvas):ModeSwitcherbutton in the header of each shell.- Switching preserves
PhaseProviderstate because the provider is hoisted above<Routes>(see CLAUDE.md note +App.tsx). <640 pxviewports: the agent shell falls back to the sidebar even when the flag is ON. The canvas needs real estate; mobile users get the Guided sidebar plus chat-as-sidebar. Not a regression — document if you see it.
Common pitfalls¶
These are spelled out in the planning doc's "Prerequisites & Non-Obvious Gotchas" section but worth repeating here because each has bitten contributors:
- The agent loop is the load-bearing change. Every cosmetic edit assumes the loop iterates. Don't ship UI for tools the backend can't dispatch.
- Tool errors must never crash the loop. Wrap dispatch in try/except; encode failures as
{ok: false, error}. Crashes kill the conversation. EventSourceis structurally blocked — can't carry the JWT header. Stay on fetch + ReadableStream.StackupDesignView.tsxis prop-driven. Don't introduce a context dep on it — the inlineActionCardrender path breaks.PhaseProviderMUST be above<Routes>inApp.tsx. Per-shell mounting silently resets state onModeSwitcherround-trips.- Tools write client state via
clientUpdate; backend stays stateless re: UI. SSE consumer appliesapplyAgentUpdate(clientUpdate)and on version skew opensConflictResolverModal. - RLS is the second line of defense. Tools verify project tenancy from JWT context first; RLS catches anything that slips past.
- Memoize cards on
result.idor a 7-tool turn re-renders 30+ times. - Synthetic
local-*conversation ids are a data-loss trap. When hydration fails (classically: a brand-new project whose row hasn't synced toprojectsyet → conversation INSERT hits the FK → 404), the hook falls back to a client-mintedlocal-*id.append_messagecan't persist under it, so the whole session silently evaporates on the next remount. Three layers defend this now:hydrateConversationawaitswaitForCloudSync(projectId)+ retries once; the stream route resolveslocal-*/missing ids to the real(project, user)conversation via_resolve_conversation_id(routes/chat_gemini.py); ChatCanvas shows a loader whileisHydratinginstead of flashing the EmptyState. - Action-card buttons must not yank the user out of the agent shell unless navigation is their stated purpose. "Go to …" / "View …" buttons navigate; "Apply Solution" applies the patch via
applyAgentUpdate, persists withsaveCurrentProject()after the commit (latest-ref effect inSolverInlineCard), and stays put.
Manual smoke¶
.\start-all-servers-windowed.ps1withVITE_FF_AI_NATIVE_SHELL=truein root.env.- Open
http://localhost:5180/app/agent. - Type: "Design a 5.8 GHz 1 W satellite uplink."
- Verify the stream renders:
thinking→ 5+tool_callevents → final action cards (phase view, BOM, link budget, export-ready). - Reload the page. Verify the conversation hydrates from the Supabase store (no re-stream).
- Click "Open full editor" on a stackup card → switches to
/appwith the same phase state.
Where to file changes¶
- Spec changes: update the relevant section of
docs/planning/ai_native_transformation/ai-native-transformation.md(the canonical source). - Tool registry edits: backend
tools/(handlers, routed viaservices/tool_dispatcher.py) + frontendcomponents/Chat/actionCards/registry.tsx. Cards need aresultType-keyed entry; tools need a JSON schema for Gemini function declarations. - Feature-flag dormancy: every new SSE consumer added to a mount point needs a new row in
src/__tests__/featureFlag/dormancy.test.ts. - Mobile fallback: keep
<640 pxon the sidebar path; do not collapse the canvas.