Перейти к основному содержимому

Подробный разбор цикла агента 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Список запросов на вызов инструментов
finishReasonstop / tool_use / max_tokens / error

Когда вызовов инструментов нет:

  1. Удалить блоки <think>...</think> (формат рассуждений DeepSeek)
  2. Отправить событие прогресса text_delta
  3. Проверить наличие ожидающих фоновых результатов суб-агентов
  4. Если фоновые результаты есть и максимальное число раундов слива (3) не достигнуто — вставить результаты и продолжить цикл
  5. Иначе вернуть финальный результат

Шаг 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)"]

Подробные шаги конвейера безопасности:

  1. Проверка Abort — если signal.aborted равно true, немедленно вернуть результат
  2. Пропуск нативного поиска — если nativeSearchEnabled и инструмент — нативный поиск, пропустить локальное выполнение
  3. Проверка роли в канале — блокировать все инструменты при channelUserPermissions.canUseTool === false
  4. Хук PreToolUse — выполнить зарегистрированные хуки, заблокировать при behavior === 'deny'
  5. ExecutionFirewall — проверить, не затрагивают ли пути к файлам и команды запрещённые зоны
  6. GuardianAgent — оценка безопасности с помощью LLM (если включено)
  7. Callback разрешения — чувствительные инструменты требуют подтверждения пользователя
  8. Выполнение — исполнение инструмента с таймаутом (Promise.race против Promise с таймаутом)
  9. Хук PostToolUse — асинхронный запуск, не блокирующий (fire-and-forget)

Шаг 4: Обработка результата

  1. Усечение вывода — результаты, превышающие TOOL_RESULT_MAX_CHARS (50 КБ), усекаются
  2. Проверка YieldSignal — если инструмент выбрасывает YieldSignal, цикл немедленно завершается
  3. Callback завершения итерацииawait onIterationComplete() сохраняет сообщения в БД
  4. Добавление в сообщения — результаты инструментов добавляются как сообщения с role: 'tool'
  5. Слив фоновых результатов — завершённые результаты фоновых суб-агентов вставляются в список сообщений

Шаг 5: Управление циклом

Возврат к шагу 1 до выполнения одного из условий выхода:

Условие выходаfinishReason
LLM возвращает обычный текст (без вызовов инструментов)stop
Достигнуто максимальное число итерацийmax_iterations
Сработал AbortSignalinterrupted
LLM вернул ошибкуerror
Сработал YieldSignalstop

Определение констант

КонстантаЗначениеОписание
MAX_ITERATIONS40Максимальное число итераций цикла
TEMPERATURE0.1Параметр temperature для LLM
MAX_TOKENS8192Максимальное число токенов вывода LLM
TOOL_RESULT_MAX_CHARS50 000Максимальное число символов результата инструмента
MAX_LLM_RETRIES2Число повторных попыток вызова LLM
LLM_RETRY_DELAY_MS1 000Базовая задержка между попытками (мс)
DEFAULT_TOOL_EXECUTION_TIMEOUT_MS300 000Таймаут выполнения одного инструмента (5 мин)
PERMISSION_TIMEOUT300 000Таймаут подтверждения разрешения (5 мин)
MAX_BACKGROUND_DRAIN_ROUNDS3Максимальное число последовательных раундов слива фоновых результатов
DEFAULT_MAX_HISTORY100Максимальное число загружаемых сообщений истории
VERSION0.1.0Версия TinyElf

Интеграция фоновых суб-агентов

TinyElf поддерживает параллельное выполнение фоновых суб-агентов. Основной цикл взаимодействует с фоновыми задачами через следующий механизм:

Вставка результатов

pushBackgroundResult(result: BackgroundAgentResult): void

После завершения фонового суб-агента результаты помещаются в очередь основного цикла через SubagentManager.setBackgroundCompleteCallback.

Стратегия слива

  • В конце каждой итерации проверяется очередь фоновых результатов
  • При наличии новых результатов они вставляются в формате [Background agent "label" (runId) outcome]
  • Если LLM вернул обычный текст, но есть ожидающие фоновые результаты, цикл продолжается (максимум 3 раунда)
  • Фактические вызовы инструментов сбрасывают счётчик раундов слива

Сохранение сообщений

По завершении каждой итерации сообщения сохраняются через callback onIterationComplete:

  1. Формируется MessageBlock[] (блоки thinking + tool_use)
  2. Сохраняется сообщение ассистента (с информацией о вызовах инструментов)
  3. Сохраняется каждое сообщение с результатом инструмента (с блоком tool_result)
  4. Через 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 Looptinyelf/TinyElfAgentLoop.tsОсновная логика цикла
Движокtinyelf/TinyElfEngine.tsРеализация IEngine
Session Runnertinyelf/TinyElfSessionRunner.tsОркестрация выполнения сессии
LLM Adaptertinyelf/TinyElfLLMAdapter.tsАдаптер вызовов LLM
Prompt Buildertinyelf/TinyElfPromptBuilder.tsПостроение сообщений
Типыtinyelf/types.tsОпределения типов и констант

Все пути относительно packages/desktop/app/main/services/agent-core/engine/.

Точки расширения

  • Кастомный таймаут инструментаTinyElfEngineConfig.toolExecutionTimeout
  • Кастомный лимит итерацийTinyElfEngineConfig.maxIterations или maxTurns
  • Кастомный temperatureTinyElfEngineConfig.temperature
  • Уровень мышленияTinyElfEngineConfig.thinkingLevel (none/low/medium/high)
  • Расширение хуками — регистрация хуков PreToolUse / PostToolUse / SessionStart / SessionEnd через HookExecutor

Связанные модули

МодульПутьСвязь
ToolRegistryBuildertinyelf/TinyElfToolRegistryBuilder.tsПостроение реестра инструментов
SubagentManagertinyelf/tools/SpawnTool.tsУправление суб-агентами
ExecutionFirewallplatform/security/ExecutionFirewall.tsУровень безопасности 1
GuardianAgentplatform/security/GuardianAgent.tsУровень безопасности 2
TinyElfPermissionstinyelf/TinyElfPermissions.tsУровень безопасности 3