Потоки данных
В этом документе описано, как данные перемещаются между слоями 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 — передаётся в главный процесс через
window.api.completion.chatInSession(), открытый через Preload - CompletionRouter — проверяет токен, разбирает параметры, вызывает
CompletionService - EngineDispatcher — маршрутизирует запрос к нужному движку на основе
engineTypeAgent - LLM API — движок выполняет HTTP-запрос и получает потоковый SSE-ответ
- Потоковая передача — каждая дельта отправляется на фронтенд через IPC-событие; Zustand store обновляется в реальном времени
- Сохранение — после завершения бэкенд записывает сообщение ассистента в базу данных
- Синхронизация состояния — фронтенд очищает потоковое состояние, отображает итоговое сообщение
Процесс вызова инструментов 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 — это устаревший Context-обёртка, который находится в процессе миграции в Zustand store. Новый код должен напрямую использовать 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.currentIndexgetActivePath()пересчитывает активный путь сообщений от корня до листа- Список сообщений перерисовывается
Стратегии кэширования
Кэш фронтенда
| Кэш | Технология | TTL | Назначение |
|---|---|---|---|
| Кэш сообщений | Zustand messageCache | Время жизни сессии | Избежать повторной загрузки сообщений |
| Состояние UI | IndexedDB (frontendCache) | Постоянный | Черновики, свёрнутые элементы, позиция прокрутки |
| Список сессий | Zustand sessions | Обновляется при обновлении | Список сессий в боковой панели |
| Список провайдеров | Zustand providers | Обновляется при обновлении | Селектор моделей |
Кэш бэкенда
| Кэш | Расположение | TTL | Назначение |
|---|---|---|---|
| Список инструментов MCP | CacheService | 5 минут | Избежать повторного перечисления инструментов MCP |
| Цепочка Transformer | TransformerService | 10 минут | Скомпилированная цепочка преобразований |
| Индекс провайдеров | LLMConfigService | Map O(1) | Быстрый поиск провайдера по 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)
Бэкенд поддерживает настройку нескольких API-ключей для каждого LLM-провайдера с использованием взвешенного циклического перебора и привязки к сессии:
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 | Сервис кэша бэкенда |