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
- User input —
UnifiedInputcomponent captures input, attachments, and model selection - UnifiedChatContext — Creates a
usermessage object, optimistically updates local state - IPC call — Sent to the main process via
window.api.completion.chatInSession()exposed through Preload - CompletionRouter — Validates token, parses parameters, calls
CompletionService - EngineDispatcher — Routes to the appropriate engine based on the Agent's
engineType - LLM API — The engine makes an HTTP request and receives an SSE streaming response
- Streaming push — Each delta is pushed to the frontend via an IPC event; Zustand store updates in real time
- Persistence — After completion, the backend writes the assistant message to the database
- 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
| Layer | Technology | Characteristics | Use Cases |
|---|---|---|---|
| Layer 1 | Zustand | High-frequency updates, precise subscriptions, no Provider nesting | Streaming messages, branch switching, session list |
| Layer 2 | React Context | Medium-frequency updates, provides API methods, dependency injection | Chat operations (send/regenerate), auth, theme |
| Layer 3 | React Context | Low-frequency updates, feature isolation | Multi-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
- User clicks branch navigation arrows
switchBranch(parentMessageId, newIndex)updatesBranchInfo.currentIndexgetActivePath()recalculates the active message path from root to leaf- Message list re-renders
Caching Strategies
Frontend Cache
| Cache | Technology | TTL | Purpose |
|---|---|---|---|
| Message cache | Zustand messageCache | Session lifetime | Avoid reloading messages |
| UI state | IndexedDB (frontendCache) | Persistent | Drafts, collapsed state, scroll position |
| Session list | Zustand sessions | Updated on refresh | Sidebar session list |
| Provider list | Zustand providers | Updated on refresh | Model selector |
Backend Cache
| Cache | Location | TTL | Purpose |
|---|---|---|---|
| MCP tool list | CacheService | 5 minutes | Avoid repeatedly listing MCP tools |
| Transformer chain | TransformerService | 10 minutes | Compiled transformation chain |
| Provider index | LLMConfigService | O(1) Map | Fast provider lookup by ID |
| API key cooldown | ApiKeyPoolService | 60s-15min exponential backoff | Cool down after 429/529 errors |
| PromptGuardian results | PromptGuardian | SHA-256 key | Cached results for reviewed prompts |
| GuardianAgent results | GuardianAgent | SHA-256 key | Cached 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;
}
Related Files
| File | Description |
|---|---|
packages/renderer/src/shared/state/chatStore.ts | Zustand 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.ts | Session protection |
packages/renderer/src/shared/utils/frontendCache.ts | Frontend IndexedDB cache |
packages/desktop/app/main/services/capabilities/llm/completion/CompletionService.ts | LLM completion service |
packages/desktop/app/main/services/capabilities/llm/completion/ApiKeyPoolService.ts | Multi-key round-robin |
packages/desktop/app/main/services/agent-core/engine/EngineDispatcher.ts | Engine dispatch |
packages/desktop/app/main/services/routers/CompletionRouter.ts | Completion IPC router |
packages/desktop/app/main/services/platform/security/ExecutionFirewall.ts | Path firewall |
packages/desktop/app/main/services/platform/security/GuardianAgent.ts | AI tool review |
packages/desktop/app/main/services/infra/cache/CacheService.ts | Backend cache service |