데이터 흐름
이 문서는 Elftia 내 레이어 간 데이터 흐름을 설명합니다. 메시지 전송, 도구 호출, 상태 관리, 캐싱 전략을 포함합니다.
전체 메시지 전송 흐름
사용자가 입력창에 타이핑한 시점부터 AI 응답이 완전히 렌더링될 때까지의 전체 데이터 흐름:
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
핵심 단계 설명
- 사용자 입력 —
UnifiedInput컴포넌트가 입력, 첨부파일, 모델 선택을 캡처합니다 - UnifiedChatContext —
user메시지 객체를 생성하고 로컬 상태를 낙관적으로 업데이트합니다 - IPC 호출 — Preload를 통해 노출된
window.api.completion.chatInSession()을 통해 메인 프로세스로 전달됩니다 - CompletionRouter — 토큰을 검증하고 파라미터를 파싱하여
CompletionService를 호출합니다 - EngineDispatcher — Agent의
engineType에 따라 적절한 엔진으로 라우팅합니다 - LLM API — 엔진이 HTTP 요청을 보내고 SSE 스트리밍 응답을 받습니다
- 스트리밍 푸시 — 각 델타가 IPC 이벤트를 통해 프런트엔드로 전달되며, Zustand 스토어가 실시간으로 업데이트됩니다
- 영속화 — 완료 후 백엔드가 어시스턴트 메시지를 데이터베이스에 씁니다
- 상태 동기화 — 프런트엔드가 스트리밍 상태를 초기화하고 최종 메시지를 표시합니다
Agent 도구 호출 흐름
LLM이 도구 호출(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
}
상태 관리 계층 구조
프런트엔드 상태 관리는 세 레이어로 나뉘며, 각각 명확하게 정의된 책임을 가집니다:
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
레이어별 책임
| 레이어 | 기술 | 특성 | 사용 사례 |
|---|---|---|---|
| 레이어 1 | Zustand | 고빈도 업데이트, 정밀 구독, Provider 중첩 없음 | 스트리밍 메시지, 브랜치 전환, 세션 목록 |
| 레이어 2 | React Context | 중빈도 업데이트, API 메서드 제공, 의존성 주입 | 채팅 작업(전송/재생성), 인증, 테마 |
| 레이어 3 | React Context | 저빈도 업데이트, 기능 격리 | 멀티탭, 키워드 하이라이트, 메시지 선택 |
상태 마이그레이션 방향
ChatDataContext (deprecated) ──migrating──> chatStore (Zustand)
│
v
UnifiedChatContext (unified API layer)
ChatDataContext는 Zustand 스토어로 마이그레이션 중인 레거시 Context 래퍼입니다. 새 코드는 chatStore 또는 UnifiedChatContext를 직접 사용해야 합니다.
메시지 브랜칭
채팅 메시지는 브랜칭을 지원하기 위해 트리 구조를 사용합니다(재생성/편집 시 새 브랜치 생성):
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;
}
브랜치 전환 로직
- 사용자가 브랜치 탐색 화살표를 클릭합니다
switchBranch(parentMessageId, newIndex)가BranchInfo.currentIndex를 업데이트합니다getActivePath()가 루트에서 리프까지 활성 메시지 경로를 재계산합니다- 메시지 목록이 다시 렌더링됩니다
캐싱 전략
프런트엔드 캐시
| 캐시 | 기술 | TTL | 목적 |
|---|---|---|---|
| 메시지 캐시 | Zustand messageCache | 세션 수명 | 메시지 재로딩 방지 |
| UI 상태 | IndexedDB (frontendCache) | 영속 | 초안, 접힘 상태, 스크롤 위치 |
| 세션 목록 | Zustand sessions | 새로고침 시 업데이트 | 사이드바 세션 목록 |
| 프로바이더 목록 | Zustand providers | 새로고침 시 업데이트 | 모델 선택기 |
백엔드 캐시
| 캐시 | 위치 | TTL | 목적 |
|---|---|---|---|
| MCP 도구 목록 | CacheService | 5분 | MCP 도구 반복 조회 방지 |
| 트랜스포머 체인 | TransformerService | 10분 | 컴파일된 변환 체인 |
| 프로바이더 인덱스 | LLMConfigService | O(1) Map | ID로 프로바이더 빠른 조회 |
| API 키 쿨다운 | ApiKeyPoolService | 60초~15분 지수 백오프 | 429/529 오류 후 쿨다운 |
| PromptGuardian 결과 | PromptGuardian | SHA-256 키 | 검토된 프롬프트의 캐시된 결과 |
| GuardianAgent 결과 | GuardianAgent | SHA-256 키 | 검토된 도구 호출의 캐시된 결과 |
세션 보호
활성 대화 중 WebSocket 프로젝트 업데이트로 인해 사이드바가 새로고침되거나 채팅 메시지가 지워지는 것을 방지합니다:
{/* 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
}
다중 키 라운드로빈 (ApiKeyPoolService)
백엔드는 LLM 프로바이더당 여러 API 키 구성을 지원하며, 세션 친화성을 가진 가중 라운드로빈을 사용합니다:
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;
}
관련 파일
| 파일 | 설명 |
|---|---|
packages/renderer/src/shared/state/chatStore.ts | Zustand 채팅 상태 스토어 + 통합 채팅 API (전송/재생성/편집, 데이터 캐시; 기존 UnifiedChatContext / ChatDataContext가 여기에 통합됨) |
packages/renderer/src/features/chat/hooks/useSessionProtection.ts | 세션 보호 |
packages/renderer/src/shared/utils/frontendCache.ts | 프런트엔드 IndexedDB 캐시 |
packages/desktop/app/main/services/capabilities/llm/completion/CompletionService.ts | LLM 완성 서비스 |
packages/desktop/app/main/services/capabilities/llm/completion/ApiKeyPoolService.ts | 다중 키 라운드로빈 |
packages/desktop/app/main/services/agent-core/engine/EngineDispatcher.ts | 엔진 디스패치 |
packages/desktop/app/main/services/routers/CompletionRouter.ts | 완성 IPC 라우터 |
packages/desktop/app/main/services/platform/security/ExecutionFirewall.ts | 경로 방화벽 |
packages/desktop/app/main/services/platform/security/GuardianAgent.ts | AI 도구 검토 |
packages/desktop/app/main/services/infra/cache/CacheService.ts | 백엔드 캐시 서비스 |