본문으로 건너뛰기

데이터 흐름

이 문서는 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

핵심 단계 설명

  1. 사용자 입력UnifiedInput 컴포넌트가 입력, 첨부파일, 모델 선택을 캡처합니다
  2. UnifiedChatContextuser 메시지 객체를 생성하고 로컬 상태를 낙관적으로 업데이트합니다
  3. IPC 호출 — Preload를 통해 노출된 window.api.completion.chatInSession()을 통해 메인 프로세스로 전달됩니다
  4. CompletionRouter — 토큰을 검증하고 파라미터를 파싱하여 CompletionService를 호출합니다
  5. EngineDispatcher — Agent의 engineType에 따라 적절한 엔진으로 라우팅합니다
  6. LLM API — 엔진이 HTTP 요청을 보내고 SSE 스트리밍 응답을 받습니다
  7. 스트리밍 푸시 — 각 델타가 IPC 이벤트를 통해 프런트엔드로 전달되며, Zustand 스토어가 실시간으로 업데이트됩니다
  8. 영속화 — 완료 후 백엔드가 어시스턴트 메시지를 데이터베이스에 씁니다
  9. 상태 동기화 — 프런트엔드가 스트리밍 상태를 초기화하고 최종 메시지를 표시합니다

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

레이어별 책임

레이어기술특성사용 사례
레이어 1Zustand고빈도 업데이트, 정밀 구독, Provider 중첩 없음스트리밍 메시지, 브랜치 전환, 세션 목록
레이어 2React Context중빈도 업데이트, API 메서드 제공, 의존성 주입채팅 작업(전송/재생성), 인증, 테마
레이어 3React 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;
}

브랜치 전환 로직

  1. 사용자가 브랜치 탐색 화살표를 클릭합니다
  2. switchBranch(parentMessageId, newIndex)BranchInfo.currentIndex를 업데이트합니다
  3. getActivePath()가 루트에서 리프까지 활성 메시지 경로를 재계산합니다
  4. 메시지 목록이 다시 렌더링됩니다

캐싱 전략

프런트엔드 캐시

캐시기술TTL목적
메시지 캐시Zustand messageCache세션 수명메시지 재로딩 방지
UI 상태IndexedDB (frontendCache)영속초안, 접힘 상태, 스크롤 위치
세션 목록Zustand sessions새로고침 시 업데이트사이드바 세션 목록
프로바이더 목록Zustand providers새로고침 시 업데이트모델 선택기

백엔드 캐시

캐시위치TTL목적
MCP 도구 목록CacheService5분MCP 도구 반복 조회 방지
트랜스포머 체인TransformerService10분컴파일된 변환 체인
프로바이더 인덱스LLMConfigServiceO(1) MapID로 프로바이더 빠른 조회
API 키 쿨다운ApiKeyPoolService60초~15분 지수 백오프429/529 오류 후 쿨다운
PromptGuardian 결과PromptGuardianSHA-256 키검토된 프롬프트의 캐시된 결과
GuardianAgent 결과GuardianAgentSHA-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.tsZustand 채팅 상태 스토어 + 통합 채팅 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.tsLLM 완성 서비스
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.tsAI 도구 검토
packages/desktop/app/main/services/infra/cache/CacheService.ts백엔드 캐시 서비스