Skip to content

Agentic AI Architecture

Runtime model

The agent is a client-side orchestration hook rather than a server process. useVoiceAgent owns lifecycle state and delegates inference to src/lib/llm-runtime.ts. A selected variant maps to one adapter: Gemma custom kernels, LFM custom kernels, Transformers.js, or WebLLM.

flowchart TD
  classDef default fill:#1e293b,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
  classDef highlight fill:#065f46,stroke:#34d399,stroke-width:2px,color:#f0fdf4
  classDef warning fill:#78350f,stroke:#fbbf24,stroke-width:2px,color:#fffbeb
  Request["User request"]:::highlight --> Prompt["System prompt + recent history"]
  Prompt --> Adapter["Selected LLM adapter"]
  Adapter --> Events["text / thinking / tool events"]
  Events --> ToolGate{"Tools enabled?"}
  ToolGate -->|No| Answer["Answer stream"]
  ToolGate -->|Yes| Execute["Validate and execute bounded tools"]
  Execute --> Adapter
  Answer --> Split["Sentence splitter"]
  Split --> Speech["TTS queue"]
  Execute --> Answer
  linkStyle default stroke:#64748b,stroke-width:2px

Streaming and tools

streamLLMWithToolLoop caps tool rounds and calls per round. Models with native tool support receive structured turns; prompt-fallback models receive serialized tool-call and tool-result messages. A nudge prompt handles models that fail to emit a tool call when a request obviously needs time/date or calculation.

Events are normalized into text_delta, thinking_delta, tool_call, tool_result, and done. The UI can therefore render reasoning and tool activity without knowing which engine produced it.

Voice response path

When TTS is enabled, text deltas feed TextSplitterStream. The next sentence is prefetched while the current sentence synthesizes, PCM is enqueued for playback, and the final chunks are combined into a WAV object URL for the message bubble. Abort signals stop both model generation and playback.

State and failure handling

  • Setup phases: selecting, loading, ready.
  • Runtime statuses include listening, recording, transcribing, thinking, synthesizing, speaking, and error.
  • A ref-backed abort controller prevents stale generations from updating the conversation.
  • Barge-in aborts active generation when new speech is detected.
  • Model switching unloads stale engine/model combinations.
  • Tool execution is bounded and validated before dispatch.

There is no durable server checkpoint or cross-device memory. Conversation state is in React memory and is cleared explicitly or on page reload.

Model selection matrix

Engine Adapter Strength Cost
Gemma kernel gemma4-kernel Custom WebGPU path and vision-capable variants High memory and device sensitivity
LFM kernel lfm2-kernel Small custom-kernel models Specialized implementation
Transformers.js transformers-js Hugging Face ONNX model family Browser/WebGPU compatibility
WebLLM webllm MLC-compiled Qwen/Llama choices Model-specific runtime footprint

The adapter boundary is not merely a type abstraction. load, unload, isReady, abort, stream, and getLoadProgress are the lifecycle operations needed to prevent a prior model from consuming memory or emitting events after a switch.

Event semantics

text_delta is user-visible answer content. thinking_delta is optional diagnostic/reasoning UI content. tool_call and tool_result are rendered as activity and also become the next model turn. done marks a generation boundary, while the hook separately waits for TTS completion before returning to ready/listening state.

Tool safety boundary

The built-in registry contains calculator and current-time capabilities. Tool execution is local and bounded by MAX_TOOL_ROUNDS and MAX_TOOL_CALLS_PER_ROUND. Prompt-fallback models receive serialized tool turns because not every small model supports structured tool calls. This compatibility path increases prompt-injection and parser risk, so experimental tools are opt-in.