Skip to main content

Data Flow

This document describes how data flows between layers in Elftia, including message sending, tool calls, state management, and caching strategies.

Full Message Sending Flow

The complete data flow from the user typing in the input box to the AI response being fully rendered:

sequenceDiagram
participant U as User Input
participant UI as UnifiedInput
participant UCC as UnifiedChatContext
participant CBC as ChatBackendContext
participant IPC as IPC Layer
participant CR as CompletionRouter
participant CS as CompletionService
participant ED as EngineDispatcher
participant Engine as Engine (Chat/SDK/TinyElf/CLI)
participant LLM as LLM API
participant DB as DbClient (Worker)
participant Store as chatStore (Zustand)
participant Render as MessageRenderer

U->>UI: Type message, press Enter
UI->>UCC: sendMessage(content, attachments)
UCC->>UCC: Create user message, update local state
UCC->>CBC: Send message via IPC
CBC->>IPC: window.api.completion.chatInSession(params)
IPC->>CR: secureHandle validates token
CR->>CS: completion.chatInSession(sessionId, messages, config)
CS->>ED: engineDispatcher.chat(session)
ED->>Engine: Route to appropriate engine
Engine->>LLM: HTTP request (SSE stream)

loop SSE streaming response
LLM-->>Engine: data chunk
Engine-->>CR: IPC event (stream:delta)
CR-->>IPC: mainWindow.send('stream:delta', chunk)
IPC-->>CBC: onStreamDelta callback
CBC-->>Store: setStreamingState({content})
Store-->>Render: Re-render StreamingMessage
end

Engine-->>CR: stream complete
CR->>DB: Persist assistant message
CR-->>IPC: stream:end event
IPC-->>CBC: onStreamEnd callback
CBC-->>UCC: Update message list
UCC-->>Store: clearStreamingState, addMessage
Store-->>Render: Render final message

Key Step Descriptions

  1. User inputUnifiedInput component captures input, attachments, and model selection
  2. UnifiedChatContext — Creates a user message object, optimistically updates local state
  3. IPC call — Sent to the main process via window.api.completion.chatInSession() exposed through Preload
  4. CompletionRouter — Validates token, parses parameters, calls CompletionService
  5. EngineDispatcher — Routes to the appropriate engine based on the Agent's engineType
  6. LLM API — The engine makes an HTTP request and receives an SSE streaming response
  7. Streaming push — Each delta is pushed to the frontend via an IPC event; Zustand store updates in real time
  8. Persistence — After completion, the backend writes the assistant message to the database
  9. State sync — Frontend clears streaming state, displays final message

Agent Tool Call Flow

The processing flow when the LLM returns a tool call (tool_use):

flowchart TB
LLM[LLM returns tool_use] --> Parse[Parse tool_call]
Parse --> FW{ExecutionFirewall<br/>path check}

FW -->|deny| Block[Return denied result to LLM]
FW -->|pass| Guardian{GuardianAgent<br/>AI safety review}

Guardian -->|risk: high/critical| PermGate{ChannelPermissionGate<br/>human confirmation}
Guardian -->|risk: low/none| Execute[Execute tool]
Guardian -->|monitor mode| LogOnly[Log and execute]

PermGate -->|user denies| Block
PermGate -->|user approves| Execute

Execute --> Result[Tool execution result]
Result --> Audit[AuditLogger records]
Result --> BackToLLM[Result returned to LLM]
BackToLLM --> LLM

LogOnly --> Execute
{/* Tool call pipeline in TinyElf Agent Loop */}
interface ToolCallPipeline {
firewall: ExecutionFirewall; // 1. Deterministic check (zero LLM overhead)
guardian: GuardianAgent; // 2. AI review (mode-dependent)
permissionGate: ChannelPermissionGate; // 3. Human confirmation (Channel sources only)
executor: ToolExecutor; // 4. Execute
auditLogger: AuditLogger; // 5. Audit
}

State Management Hierarchy

Frontend state management is split into three layers, each with clearly defined responsibilities:

graph TB
subgraph "Layer 1: Zustand Store (core state)"
CS[chatStore — sessions/messages/streaming/branches]
SS[settingsStore — settings state]
end

subgraph "Layer 2: React Context (domain state)"
CDC[ChatDataContext — data cache wrapper]
UCC2[UnifiedChatContext — unified chat API]
TC[ThemeContext — theme]
AC[AuthContext — authentication]
EC[ElfiContext — Elfi assistant]
end

subgraph "Layer 3: Feature Context (feature state)"
CTC[ChatTabsContext — multi-tab]
WIC[WorldInfoHighlightContext — WI highlights]
MSC[MessageSelectionContext — multi-select]
end

CS --> CDC
CDC --> UCC2
UCC2 --> CTC

Layer Responsibilities

LayerTechnologyCharacteristicsUse Cases
Layer 1ZustandHigh-frequency updates, precise subscriptions, no Provider nestingStreaming messages, branch switching, session list
Layer 2React ContextMedium-frequency updates, provides API methods, dependency injectionChat operations (send/regenerate), auth, theme
Layer 3React ContextLow-frequency updates, feature isolationMulti-tab, keyword highlighting, message selection

State Migration Direction

ChatDataContext (deprecated) ──migrating──> chatStore (Zustand)

v
UnifiedChatContext (unified API layer)

ChatDataContext is a legacy Context wrapper in the process of being migrated to the Zustand store. New code should use chatStore or UnifiedChatContext directly.

Message Branching

Chat messages use a tree structure to support branching (regenerate/edit creates new branches):

graph TB
M1[User: Hello] --> M2a[Assistant: Hello! v1]
M1 --> M2b[Assistant: Hi! v2]
M2a --> M3[User: Help me write some code]
M3 --> M4a[Assistant: Sure v1]
M3 --> M4b[Assistant: Of course v2]
{/* Branch data structure */}
interface BranchInfo {
id: string;
parentMessageId: string;
children: string[];
currentIndex: number;
}

{/* Branch operations */}
interface BranchOperations {
switchBranch(messageId: string, index: number): void;
getActivePath(rootId: string): Message[];
regenerate(messageId: string): void;
editMessage(messageId: string, newContent: string): void;
}

Branch Switching Logic

  1. User clicks branch navigation arrows
  2. switchBranch(parentMessageId, newIndex) updates BranchInfo.currentIndex
  3. getActivePath() recalculates the active message path from root to leaf
  4. Message list re-renders

Caching Strategies

Frontend Cache

CacheTechnologyTTLPurpose
Message cacheZustand messageCacheSession lifetimeAvoid reloading messages
UI stateIndexedDB (frontendCache)PersistentDrafts, collapsed state, scroll position
Session listZustand sessionsUpdated on refreshSidebar session list
Provider listZustand providersUpdated on refreshModel selector

Backend Cache

CacheLocationTTLPurpose
MCP tool listCacheService5 minutesAvoid repeatedly listing MCP tools
Transformer chainTransformerService10 minutesCompiled transformation chain
Provider indexLLMConfigServiceO(1) MapFast provider lookup by ID
API key cooldownApiKeyPoolService60s-15min exponential backoffCool down after 429/529 errors
PromptGuardian resultsPromptGuardianSHA-256 keyCached results for reviewed prompts
GuardianAgent resultsGuardianAgentSHA-256 keyCached results for reviewed tool calls

Session Protection

Prevents WebSocket project updates from refreshing the sidebar and clearing chat messages during an active conversation:

{/* Session protection flow */}
interface SessionProtection {
activeSessions: Set<string>;
processingSessions: Set<string>;

markActive(sessionId: string): void; // Mark when user sends a message
shouldSkipRefresh(): boolean; // activeSessions.size > 0
markInactive(sessionId: string): void; // Remove after conversation completes
}

Multi-Key Round-Robin (ApiKeyPoolService)

The backend supports configuring multiple API keys per LLM provider, using weighted round-robin with session affinity:

flowchart LR
Req[Request] --> Check{Session bound?}
Check -->|yes| BoundKey[Use bound key]
Check -->|no| RR[Weighted round-robin selects key]
RR --> Bind[Bind to session]
Bind --> Call[API call]
BoundKey --> Call
Call --> OK{Success?}
OK -->|429/529| Cool[Cool down this key]
Cool --> Retry[Switch to next key and retry]
OK -->|success| Done[Return result]
{/* Simplified ApiKeyPoolService */}
class ApiKeyPoolService {
private sessionBindings: Map<string, string>;
private cooldowns: Map<string, { until: number; backoff: number }>;

resolveApiKeyForRequest(
providerId: string,
sessionId?: string,
): Promise<{ keyId: string; apiKey: string }>;

markKeyError(keyId: string, statusCode: number): void;
}
FileDescription
packages/renderer/src/shared/state/chatStore.tsZustand chat state store + unified chat API (send/regenerate/edit, data cache; the former UnifiedChatContext / ChatDataContext have been merged here)
packages/renderer/src/features/chat/hooks/useSessionProtection.tsSession protection
packages/renderer/src/shared/utils/frontendCache.tsFrontend IndexedDB cache
packages/desktop/app/main/services/capabilities/llm/completion/CompletionService.tsLLM completion service
packages/desktop/app/main/services/capabilities/llm/completion/ApiKeyPoolService.tsMulti-key round-robin
packages/desktop/app/main/services/agent-core/engine/EngineDispatcher.tsEngine dispatch
packages/desktop/app/main/services/routers/CompletionRouter.tsCompletion IPC router
packages/desktop/app/main/services/platform/security/ExecutionFirewall.tsPath firewall
packages/desktop/app/main/services/platform/security/GuardianAgent.tsAI tool review
packages/desktop/app/main/services/infra/cache/CacheService.tsBackend cache service