Подробный разбор цикла агента TinyElf
TinyElf — встроенный движок агентов Elftia, реализующий классический паттерн «вызвать LLM → выполнить инструмент → получить результат → повторить». В этой статье подробно разбирается полный алгоритм цикла агента (Agent Loop).
Общая архитектура
graph TB
Engine["TinyElfEngine"] --> SessionRunner["TinyElfSessionRunner<br/>runSession()"]
SessionRunner --> ToolBuilder["TinyElfToolRegistryBuilder<br/>buildToolRegistry()"]
SessionRunner --> PromptBuilder["TinyElfPromptBuilder<br/>buildMessages()"]
SessionRunner --> LoopInst["TinyElfAgentLoop<br/>instantiation"]
SessionRunner --> |"run"| Loop["AgentLoop.run()"]
ToolBuilder --> FileTools["FileSystemTools"]
ToolBuilder --> ShellT["ShellTool"]
ToolBuilder --> WebT["WebSearch/WebFetch"]
ToolBuilder --> SkillsT["Skills tools"]
ToolBuilder --> SpawnT["SpawnTool"]
ToolBuilder --> McpT["MCP tools"]
ToolBuilder --> SessionT["Session tools"]
Loop --> LLMCall["callLLMWithRetry()"]
Loop --> ToolExec["executeToolCalls()"]
Loop --> Save["onIterationComplete()"]
Поток выполнения сессии
sequenceDiagram
participant R as AgentRouter
participant E as TinyElfEngine
participant B as ToolRegistryBuilder
participant S as SessionRunner
participant L as AgentLoop
participant LLM as TinyElfLLMAdapter
participant T as ToolRegistry
R->>E: startSession(ctx)
E->>E: saveMessage(user)
E->>E: sender.send(userMessage)
E->>B: buildToolRegistry()
B-->>E: { toolRegistry, subagentManager, ... }
E->>S: runSession(deps, params)
S->>S: new TinyElfLLMAdapter()
S->>S: new ExecutionFirewall()
S->>S: buildGuardian()
S->>L: new TinyElfAgentLoop()
S->>S: buildMessages()
S->>L: run(messages, callbacks)
loop До 40 итераций
L->>LLM: callLLMWithRetry(messages, tools)
LLM-->>L: { content, toolCalls, usage }
alt Нет вызовов инструментов
L->>L: Проверить фоновые результаты
alt Есть фоновые результаты
L->>L: Вставить фоновые результаты в сообщения
L->>L: continue (итерация не расходуется)
else Нет фоновых результатов
L-->>S: AgentLoopResult (finishReason: stop)
end
else Есть вызовы инструментов
L->>L: executeToolCalls(parallel)
Note over L,T: Конвейер безопасности: Firewall → Guardian → Permission
L->>T: execute(tool)
T-->>L: ToolCallResult
L->>S: onIterationComplete(data)
S->>S: saveMessage(assistant + tools)
S->>S: sender.send(assistantMessage)
L->>L: Добавить результаты инструментов в сообщения
L->>L: Слить фоновые результаты
end
end
S->>S: saveFinalResult()
S->>S: sender.send(result + complete)
Алгоритм цикла агента
Шаг 1: Вызов LLM (с повторными попытками)
callLLMWithRetry(messages, tools, signal)
- Максимум
MAX_LLM_RETRIES(2) попыток - Задержка между попытками:
LLM_RETRY_DELAY_MS * (attempt + 1)(1 с, 2 с, 3 с) - Условие повтора: LLM возвращает
finishReason === 'error'без содержимого или выбрасывает исключение - После исчерпания всех попыток возвращается сообщение об ошибке
- Перед каждой повторной попыткой проверяется
AbortSignal
Шаг 2: Разбор ответа
Ответ LLM содержит три части:
| Поле | Описание |
|---|---|
content | Текстовый ответ (может содержать блоки <think>) |
toolCalls | Список запросов на вызов инструментов |
finishReason | stop / tool_use / max_tokens / error |
Когда вызовов инструментов нет:
- Удалить блоки
<think>...</think>(формат рассуждений DeepSeek) - Отправить событие прогресса
text_delta - Проверить наличие ожидающих фоновых результатов суб-агентов
- Если фоновые результаты есть и максимальное число раундов слива (3) не достигнуто — вставить результаты и продолжить цикл
- Иначе вернуть финальный результат
Шаг 3: Выполнение инструментов (параллельное)
Все вызовы инструментов из одного ответа LLM выполняются параллельно (Promise.all). Каждый вызов проходит через следующий конвейер безопасности:
graph LR
TC["Запрос вызова инструмента"] --> Abort{"Проверка Abort"}
Abort -->|Прерван| Err1["Вернуть ошибку"]
Abort -->|Не прерван| NS{"Пропуск нативного поиска?"}
NS -->|Да| Skip["Пропустить (провайдер обрабатывает)"]
NS -->|Нет| Chan{"Проверка роли в канале"}
Chan -->|Отказано| Err2["Нет разрешения"]
Chan -->|Разрешено| Hook{"Хук PreToolUse"}
Hook -->|deny| Err3["Хук отказал"]
Hook -->|allow| FW{"ExecutionFirewall"}
FW -->|blocked| Err4["Firewall заблокировал"]
FW -->|allowed| GA{"GuardianAgent"}
GA -->|blocked| Err5["Guardian заблокировал"]
GA -->|allowed| Perm{"Callback разрешения"}
Perm -->|deny| Err6["Пользователь отказал"]
Perm -->|allow| Exec["Выполнить инструмент<br/>Таймаут 5 минут"]
Exec --> PostHook["Хук PostToolUse<br/>(fire-and-forget)"]
Подробные шаги конвейера безопасности:
- Проверка Abort — если
signal.abortedравноtrue, немедленно вернуть результат - Пропуск нативного поиска — если
nativeSearchEnabledи инструмент — нативный поиск, пропустить локальное выполнение - Проверка роли в канале — блокировать все инструменты при
channelUserPermissions.canUseTool === false - Хук PreToolUse — выполнить зарегистрированные хуки, заблокировать при
behavior === 'deny' - ExecutionFirewall — проверить, не затрагивают ли пути к файлам и команды запрещённые зоны
- GuardianAgent — оценка безопасности с помощью LLM (если включено)
- Callback разрешения — чувствительные инструменты требуют подтверждения пользователя
- Выполнение — исполнение инструмента с таймаутом (
Promise.raceпротив Promise с таймаутом) - Хук PostToolUse — асинхронный запуск, не блокирующий (fire-and-forget)
Шаг 4: Обработка результата
- Усечение вывода — результаты, превышающие
TOOL_RESULT_MAX_CHARS(50 КБ), усекаются - Проверка YieldSignal — если инструмент выбрасывает
YieldSignal, цикл немедленно завершается - Callback завершения итерации —
await onIterationComplete()сохраняет сообщения в БД - Добавление в сообщения — результаты инструментов добавляются как сообщения с
role: 'tool' - Слив фоновых результатов — завершённые результаты фоновых суб-агентов вставляются в список сообщений
Шаг 5: Управление циклом
Возврат к шагу 1 до выполнения одного из условий выхода:
| Условие выхода | finishReason |
|---|---|
| LLM возвращает обычный текст (без вызовов инструментов) | stop |
| Достигнуто максимальное число итераций | max_iterations |
| Сработал AbortSignal | interrupted |
| LLM вернул ошибку | error |
| Сработал YieldSignal | stop |
Определение констант
| Константа | Значение | Описание |
|---|---|---|
MAX_ITERATIONS | 40 | Максимальное число итераций цикла |
TEMPERATURE | 0.1 | Параметр temperature для LLM |
MAX_TOKENS | 8192 | Максимальное число токенов вывода LLM |
TOOL_RESULT_MAX_CHARS | 50 000 | Максимальное число символов результата инструмента |
MAX_LLM_RETRIES | 2 | Число повторных попыток вызова LLM |
LLM_RETRY_DELAY_MS | 1 000 | Базовая задержка между попытками (мс) |
DEFAULT_TOOL_EXECUTION_TIMEOUT_MS | 300 000 | Таймаут выполнения одного инструмента (5 мин) |
PERMISSION_TIMEOUT | 300 000 | Таймаут подтверждения разрешения (5 мин) |
MAX_BACKGROUND_DRAIN_ROUNDS | 3 | Максимальное число последовательных раундов слива фоновых результатов |
DEFAULT_MAX_HISTORY | 100 | Максимальное число загружаемых сообщений истории |
VERSION | 0.1.0 | Версия TinyElf |
Интеграция фоновых суб-агентов
TinyElf поддерживает параллельное выполнение фоновых суб-агентов. Основной цикл взаимодействует с фоновыми задачами через следующий механизм:
Вставка результатов
pushBackgroundResult(result: BackgroundAgentResult): void
После завершения фонового суб-агента результаты помещаются в очередь основного цикла через SubagentManager.setBackgroundCompleteCallback.
Стратегия слива
- В конце каждой итерации проверяется очередь фоновых результатов
- При наличии новых результатов они вставляются в формате
[Background agent "label" (runId) outcome] - Если LLM вернул обычный текст, но есть ожидающие фоновые результаты, цикл продолжается (максимум 3 раунда)
- Фактические вызовы инструментов сбрасывают счётчик раундов слива
Сохранение сообщений
По завершении каждой итерации сообщения сохраняются через callback onIterationComplete:
- Формируется
MessageBlock[](блоки thinking + tool_use) - Сохраняется сообщение ассистента (с информацией о вызовах инструментов)
- Сохраняется каждое сообщение с результатом инструмента (с блоком
tool_result) - Через IPC отправляется событие
assistantMessage
Финальный текстовый ответ сохраняется отдельно через saveFinalResult() и содержит статистику выполнения (inputTokens, outputTokens).
Двойная запись SDK (удалено, 2026-05-19)
Историческая справка: изначально TinyElf записывал каждое сообщение в таблицу
sdk_recordsчерезTinyElfSdkAdapter.convertSingleMessage(), преобразуя в формат SDK JSONL для обеспечения совместимости схем с Claude SDK в целях межмоторной совместимости. Однако весь механизмsdk_records(зеркало IPC на стороне SDK + синтез на стороне TinyElf) был упразднён 2026-05-19 — схема IPC на стороне SDK оказалась несовместима с дисковой схемой JSONL (см.docs/dev/66_sdk_records/01_schema_divergence_investigation.md), а SDK 0.3.x теперь предоставляет официальный интерфейсSessionStore(см.packages/desktop/app/main/services/agent-core/agent/SqliteSessionStore.ts). TinyElf теперь полагается исключительно наchat_messagesдля хранения данных; зеркало SDK больше не требуется.
Поле состояния sdkDualWrite, методы initSdkDualWrite / initSdkDualWriteForResume / writeSdkRecords и файл TinyElfSdkAdapter.ts удалены.
Ключевые файлы
| Файл | Путь | Описание |
|---|---|---|
| Agent Loop | tinyelf/TinyElfAgentLoop.ts | Основная логика цикла |
| Движок | tinyelf/TinyElfEngine.ts | Реализация IEngine |
| Session Runner | tinyelf/TinyElfSessionRunner.ts | Оркестрация выполнения сессии |
| LLM Adapter | tinyelf/TinyElfLLMAdapter.ts | Адаптер вызовов LLM |
| Prompt Builder | tinyelf/TinyElfPromptBuilder.ts | Построение сообщений |
| Типы | tinyelf/types.ts | Определения типов и констант |
Все пути относительно packages/desktop/app/main/services/agent-core/engine/.
Точки расширения
- Кастомный таймаут инструмента —
TinyElfEngineConfig.toolExecutionTimeout - Кастомный лимит итераций —
TinyElfEngineConfig.maxIterationsилиmaxTurns - Кастомный temperature —
TinyElfEngineConfig.temperature - Уровень мышления —
TinyElfEngineConfig.thinkingLevel(none/low/medium/high) - Расширение хуками — регистрация хуков PreToolUse / PostToolUse / SessionStart / SessionEnd через
HookExecutor
Связанные модули
| Модуль | Путь | Связь |
|---|---|---|
| ToolRegistryBuilder | tinyelf/TinyElfToolRegistryBuilder.ts | Построение реестра инструментов |
| SubagentManager | tinyelf/tools/SpawnTool.ts | Управление суб-агентами |
| ExecutionFirewall | platform/security/ExecutionFirewall.ts | Уровень безопасности 1 |
| GuardianAgent | platform/security/GuardianAgent.ts | Уровень безопасности 2 |
| TinyElfPermissions | tinyelf/TinyElfPermissions.ts | Уровень безопасности 3 |