Интеграция ClaudeSdkEngine
ClaudeSdkEngine — это обёртка IEngine вокруг @anthropic-ai/claude-agent-sdk. Она обрабатывает настройку прокси-сервера провайдера, разрешение API Key, балансировку нагрузки с несколькими ключами и автоматическую настройку Git/Bash на Windows.
Архитектурная диаграмма
graph TB
Router["AgentRouter / MagiService"] --> Engine["ClaudeSdkEngine"]
Engine --> EnvBuild["buildProviderEnvWithProxy()"]
Engine --> AgentSvc["AgentService"]
Engine --> GitRt["GitRuntimeService<br/>(Windows)"]
EnvBuild --> Decision{"Route?"}
Decision -->|"cliBackend='claude-code'<br/>(Subscription OAuth)"| PassThrough["AgentProxyServer<br/>passThrough mode"]
Decision -->|"id='anthropic'<br/>and no streaming callback"| Direct["Direct API Key pass<br/>env: ANTHROPIC_API_KEY"]
Decision -->|"Other providers"| ProxySvc["AgentProxyServer<br/>transformer chain / direct pass"]
PassThrough --> Anthropic["api.anthropic.com<br/>passthrough SDK native Bearer<br/>collect 5h/7d quota headers"]
ProxySvc --> URLNorm["URL normalization<br/>remove duplicate /v1"]
ProxySvc --> AuthH["Auth header conversion<br/>x-api-key → Bearer"]
ProxySvc --> FmtConv["Format conversion<br/>OpenAI ↔ Anthropic"]
ProxySvc --> Retry["Retry callback<br/>429/529 → IPC notify"]
ProxySvc --> BgModel["Background model routing<br/>background task model replacement"]
Direct --> AgentSvc
ProxySvc -->|"env: ANTHROPIC_BASE_URL=localhost:PORT"| AgentSvc
PassThrough -->|"env: ANTHROPIC_BASE_URL=localhost:PORT"| AgentSvc
AgentSvc --> SDK["Claude Agent SDK<br/>subprocess"]
Основная логика: buildProviderEnvWithProxy
Эта функция подготавливает переменные среды для Claude Agent SDK. Она применяет разные стратегии на основе плоских полей строки chat_sessions (providerId, cliBackend, useExtendedContext и сырого model).
Шаги алгоритма
- Нет providerId → вернуть пустой объект; SDK использует
process.env code-cli+cliBackend='claude-code'(Subscription OAuth) →buildClaudeCodePassThroughResult:- Запустить
AgentProxyServerсpassThrough: true - Прозрачно передать нативный Bearer-заголовок SDK на
api.anthropic.com - Собрать заголовки ответа
anthropic-ratelimit-unified-{5h,7d}-*, запускаяSubscriptionUsageServiceдля обновления значков 5h/7d на фронтенде - Опциональное внедрение OAuth Token под управлением Elftia (
TokensService.getValidClaudeAccessToken()) для переопределения системных учётных данных - Пометить
isOfficialProvider: false
- Запустить
- Другие пути →
llmConfig.getProvider(providerId)+resolveApiFormat(provider) - Официальный провайдер Anthropic (
provider.id === 'anthropic'):- Получить Key из ApiKeyPool или provider.api_key
- Установить
ANTHROPIC_API_KEY - Если есть пользовательский base URL, установить
ANTHROPIC_BASE_URL(убрать завершающий/v1) - Пометить
isOfficialProvider: true
- Другие провайдеры:
- Получить Key из ApiKeyPool или provider.api_key
- Запустить
AgentProxyServer(локальный HTTP-прокси) - Установить
ANTHROPIC_BASE_URL = proxy.getBaseUrl() - Для провайдеров в формате Anthropic передать реальный Key (включает серверные инструменты)
- Для провайдеров не в формате Anthropic передать
'proxy-mode'(Key обрабатывается прокси) - Вернуть callback очистки
onSessionEnd(остановить прокси)
Сигнатура функции дополнена параметром
schemaFields?: { cliBackend, useExtendedContext }(v89+), передаваемым вызывающей стороной из строкиchat_sessions.cliBackend === 'claude-code'активирует ветку passthrough;useExtendedContext === trueактивирует внедрение beta для 1M-контекста (см. ниже).
Внедрение 1M-context beta (v89+)
Когда chat_sessions.useExtendedContext = 1 и model находится в белом списке с поддержкой 1M (claude-opus-4-7 / claude-opus-4-6 / claude-sonnet-4-6), функция injectExtendedContextBeta(headers, model, useExtendedContext) добавляет 'context-1m-2025-08-07' в HTTP-заголовок anthropic-beta исходящего запроса (список через запятую).
Не поле тела запроса. Ранние реализации помещали флаг в массив
body.anthropic_beta, что отвергалось эндпоинтом/v1/messagesс ошибкой 400"anthropic_beta: Extra inputs are not permitted". Правильный путь — HTTP-заголовок.
Точки внедрения (три места используют один идемпотентный помощник без учёта регистра):
| Маршрут | Расположение | Объект headers, в который производится внедрение |
|---|---|---|
| Режим passthrough (claude-code OAuth) | AgentProxyServer.handlePassThroughRequest до fetch | upstreamHeaders (скопированы + нормализованы из req.headers) |
| Ветка прямой передачи isOfficialProvider | AgentProxyServer до fetchWithRetry | Объект, возвращённый getProviderHeaders(provider, apiKey) |
| Маршрут через цепочку трансформеров | Выход TransformerChainExecutor.executeRequestChain | config.headers (объединяется в заголовки fetch) |
Помощник сохраняет другие beta, уже записанные SDK (например, prompt-caching-2024-07-31), и совместим с вариантами регистра (Anthropic-Beta также распознаётся и перезаписывается в канонический нижний регистр anthropic-beta).
Claude Agent SDK не добавляет 1M-context beta автоматически (в исходном коде 0 вхождений
context-1m). Если маркер UI[1m]не внедрён в HTTP-заголовки через этот помощник, контекст 1M не вступит в силу и запросы будут работать в режиме 200K.
Роль прокси-сервера
Все провайдеры, не являющиеся встроенным Anthropic, маршрутизируются через прокси по следующим причинам:
- Нормализация URL —
api_base_urlможет содержать/v1, SDK продублирует его до/v1/v1/messages - Обработка заголовков аутентификации — разные провайдеры используют разные форматы заголовков (x-api-key и Bearer)
- Преобразование форматов — провайдеры не в формате Anthropic требуют трансформации форматов запроса/ответа
- Callback повторных попыток — ошибки 429/529 уведомляют фронтенд через callback
- Маршрутизация фоновых моделей — фоновые запросы SDK могут направляться к другим моделям
Разрешение API Key
function resolveApiKey(apiKey: string): string {
if (apiKey.startsWith('$')) {
return process.env[apiKey.slice(1)] || '';
}
return apiKey;
}
Поддерживает ссылку на переменные среды через префикс $, например $ANTHROPIC_API_KEY.
Балансировка нагрузки с несколькими ключами
Реализована через ApiKeyPoolService:
setApiKeyPool(pool: ApiKeyPoolService): void;
После установки buildProviderEnvWithProxy отдаёт приоритет получению Key из Pool:
let apiKey = '';
if (apiKeyPool && sessionId) {
apiKey = await apiKeyPool.getKeyForSession(provider.id, sessionId);
}
if (!apiKey) {
apiKey = resolveApiKey(provider.api_key);
}
Pool использует стратегию взвешенного кругового перебора + session affinity для распределения Key; при ошибках 429/529 автоматически активируется режим cooldown.
Настройка Git на Windows
setGitRuntime(runtime: GitRuntimeService): void;
Дочерний процесс Claude Agent SDK требует наличия Git и Bash. На Windows GitRuntimeService отвечает за:
- Определение пути установки Git for Windows
- Добавление пути git-bash в
PATH - Обеспечение нормальной работы дочернего процесса SDK
Автоматически вызывает ensureGitForSdk() перед каждым startSession и resumeSession.
Два пути вызова
Самостоятельный маршрут (AgentRouter)
AgentRouter вызывает ClaudeSdkEngine напрямую без передачи providerEnv:
// ctx.providerEnv пуст
engine.startSession(ctx);
// → внутренне вызывает buildProviderEnvWithProxy() для самостоятельного построения
Маршрут с предварительным построением Magi
MagiService предварительно строит providerEnv и передаёт его:
// ctx.providerEnv уже построен MagiService
engine.startSession(ctx);
// → напрямую использует ctx.providerEnv, пропускает самостоятельное построение
// → использует startWithExistingSession (DB-сессия уже существует)
Логика принятия решений
if (ctx.providerEnv && dbSessionId) {
await this.agent.startWithExistingSession(sender, dbSessionId, sessionOpts);
} else {
await this.agent.createSession(sender, sessionOpts);
}
Валидация API Key
Проверяет ключи перед запуском сессии, чтобы предотвратить сбои дочернего процесса CLI из-за отсутствующих ключей:
if (!env?.ANTHROPIC_API_KEY && !process.env.ANTHROPIC_API_KEY) {
throw new Error(`API key not configured for provider "${providerId}"`);
}
IPC-события
Помимо стандартных событий agent:event, ClaudeSdkEngine также отправляет уведомления о повторных попытках:
| Событие | Payload | Описание |
|---|---|---|
agent:event type=retry | { attempt, maxAttempts, delayMs, error } | Уведомление о повторной попытке API-запроса |
SessionStore (SDK 0.3.x, с 2026-05-19)
SDK 0.3.x предоставляет официальный интерфейс SessionStore, зеркалирующий транскрипты сессий в любое внешнее хранилище. AgentService.runSession внедряет его через sdkOptions.sessionStore = new SqliteSessionStore(db):
SDK subprocess writes disk JSONL → SDK also calls sessionStore.append(key, entries)
→ SqliteSessionStore → sdkSessionStore:append IPC → DB worker
→ INSERT OR IGNORE to sdk_session_store table (idempotent by entryUuid)
On Resume: SDK calls sessionStore.load(key) → returns entries[] | null
→ SDK materializes entries to temp JSONL → subprocess --resume
→ can recover even if disk JSONL is lost (as long as sdk_session_store has data)
Проверки безопасности возобновления (AgentService.resumeSession): перед диспетчеризацией проверяется одновременно hasResumableSdkJsonl(projectPath, sdkSessionId) (файл на диске существует + содержит запись type:'user') и sdkSessionStore:countBySessionId > 0; если оба пусты → очистить sdkSessionId и позволить SDK начать новый разговор (история чата в DB сохраняется).
Устаревший путь: старая таблица sdk_records + путь JsonlBuilder.reconstruct() (IPC schema ≠ disk schema, SDK отклоняет) полностью выведены из эксплуатации в migration v97. См. docs/dev/66_sdk_records/01_schema_divergence_investigation.md.
Ключевые файлы
| Файл | Путь | Описание |
|---|---|---|
| ClaudeSdkEngine | agent-core/engine/ClaudeSdkEngine.ts | Реализация IEngine + buildProviderEnvWithProxy (включая ветку passthrough + передачу extendedContext) |
| AgentService | agent-core/agent/AgentService.ts | Жизненный цикл SDK-сессии; AgentSessionOptions содержит cliBackend + useExtendedContext, столбец DB model хранит сырой SDK id; runSession внедряет sdkOptions.sessionStore; resumeSession проходит fallback-проверки |
| AgentProxyServer | agent-core/agent/AgentProxyServer.ts | HTTP-прокси-сервер с тремя режимами: passThrough / isOfficialProvider direct / default transformer chain |
| Помощник внедрения 1M-context beta | agent-core/agent/anthropicBetaInject.ts | injectExtendedContextBeta(body, model, useExtendedContext), общий для трёх точек выхода |
| SqliteSessionStore | agent-core/agent/SqliteSessionStore.ts | SQLite-реализация интерфейса SessionStore SDK 0.3.x (append/load/delete/listSubkeys) |
| Помощник sdkPathEncoding | agent-core/agent/sdkPathEncoding.ts | encodeProjectPath (= replace(/[^A-Za-z0-9]/g, '-')) + hasResumableSdkJsonl (файл существует + содержит запись пользователя) |
| DAO worker sdkSessionStore | workers/db/sdkSessionStore.ts | CRUD таблицы sdk_session_store, 6 IPC (append/load/delete/listSubkeys/listSessions/countBySessionId) |
| Типы Code CLI + протокол разделения | shared/contracts/code-cli-types.ts | splitModelReference() — единственный парсер строкового протокола на IPC-точке входа |
| ApiKeyPoolService | capabilities/llm/completion/ApiKeyPoolService.ts | Балансировка нагрузки с несколькими ключами |
| GitRuntimeService | platform/runtime/GitRuntimeService.ts | Настройка Git на Windows |
Все пути относительны packages/desktop/app/main/services/; shared/contracts/code-cli-types.ts находится в packages/desktop/app/, а workers/db/sdkSessionStore.ts — в packages/desktop/app/main/.
Каналы взаимодействия с пользователем (permission + ask_user_question)
Движок Claude SDK имеет два симметричных диалоговых канала от бэкенда к рендереру, оба поддерживаемых AgentService с pendingXxx: Map<requestId, resolver> + 5-минутный таймаут + IPC-отправка через active.sender.send.
| Канал | Вызывается | Main → Renderer IPC | Renderer → Main IPC |
|---|---|---|---|
| Permission | Callback SDK canUseTool (авторизация использования инструментов) | agent:permissionRequest | agent:respondPermission |
| AskUserQuestion | Agent активно вызывает инструмент mcp__elftia-ask__ask | agent:askUserQuestion | agent:respondAskUserQuestion |
Permission: Резервный callback в режиме bypassPermissions
AgentService.runSession всегда регистрирует onPermissionRequest, а mapCliOptionsToSDK в lib/claude-sdk.ts всегда транслирует его в canUseTool SDK. В режиме bypass callback внутренне применяет behavior: 'allow' и выводит диагностические поля agentID / blockedPath / decisionReason.
Предыдущий код замыкал callback на обоих уровнях через if (skipPermissions) skip, что вызывало молчаливую ошибку отказа: когда главный агент работает в режиме bypassPermissions, но подагент использует tools: [Read, Glob] для сужения белого списка инструментов, SDK всё равно проходит через canUseTool gate для внеплановых вызовов подагента, но callback не зарегистрирован → молчаливый отказ → пользователь видит «permission denied», но elftia не показывает диалог.
Ключевые места исправления:
packages/desktop/app/main/lib/claude-sdk.ts:262-339(оба уровня callback всегда зарегистрированы)packages/desktop/app/main/services/agent-core/agent/AgentService.ts:1064-1086(удалено замыканиеisSkipPermissions)
AskUserQuestion: Замена встроенного инструмента MCP в процессе SDK
Инструмент AskUserQuestion, входящий в пресет Claude Code, открывает терминальные подсказки, которые главный процесс elftia не может отобразить. Подход к исправлению (вариант A):
- Отключить встроенный: добавить
'AskUserQuestion'вsdkOptions.toolsSettings.disallowedTools - Зарегистрировать замещающий MCP:
AskUserQuestionMcp.tsиспользуетcreateSdkMcpServer+tool()для предоставленияmcp__elftia-ask__askсо схемой, идентичной встроенному (questions: Array<{question, header(≤12 chars), multiSelect, options[2-4]}>) - Направить модель:
appendSystemPromptдобавляет инструкцию вызыватьmcp__elftia-ask__askвместо встроенного при возникновении сценариев с вопросами - Поток обработчика: вызов
AgentService.askUserQuestion(dbSessionId, questions)→ создание Promise + сохранение resolver в MappendingQuestions→ IPCagent:askUserQuestion→AskUserQuestionDialogна стороне рендерера отображает stepper UI → пользователь нажимает Submit → IPCagent:respondAskUserQuestion→ resolver сериализует ответы как JSON для tool_result. Отмена проходит веткуisError: true, сообщая модели об отмене пользователем.
Экземпляр MCP-сервера создаётся на каждую сессию (замыкание над dbSessionId), обеспечивая попадание вопросов в правильный рендерер при работе в нескольких вкладках.
Связанный код:
packages/desktop/app/main/services/agent-core/agent/AskUserQuestionMcp.ts(конструктор MCP-сервера +ELFTIA_ASK_MCP_NAME)packages/desktop/app/main/services/agent-core/agent/AgentService.ts(MappendingQuestions+askUserQuestion()+respondToAskUserQuestion(), логика внедрения в концеrunSession)packages/desktop/app/main/services/routers/AgentRouter.ts(IPCagent:respondAskUserQuestion+ zod schema)packages/renderer/src/features/chat/components/agent/AskUserQuestionDialog.tsx(stepper UI: текущий вопрос развёрнут + отвеченные вопросы в свёрнутой сводной строке с возможностью возврата + постоянное текстовое поле «Other» + Submit активируется только после ответа на все вопросы)
Точки расширения
- Новое преобразование формата прокси: добавить новые адаптеры формата API в
AgentProxyServer - Пользовательская стратегия повторных попыток: настроить поведение повторных попыток через параметр
RetryCallback - Конфигурация фоновой модели: задать модель для фоновых задач через
agentDefaults.background
Связанные модули
| Модуль | Путь | Связь |
|---|---|---|
| EngineDispatcher | agent-core/engine/EngineDispatcher.ts | Регистрация движка |
| LLMConfigService | capabilities/llm/config-service/ | Конфигурация провайдера |
| MagiService | agent-core/magi/MagiService.ts | Высокоуровневая оркестрация (включает assembleMagiMcps с обходом реестра + слияние Object.assign в customAgentOptions.additionalMcpServers; buildTinyElfDirectMcpServers — точка входа TinyElf) |
| MagiSdkOptionsBuilder | agent-core/magi/MagiSdkOptionsBuilder.ts | Построение промпта (V1-V4) + внедрение пользовательских MCP (setUserMcpServers / getMcpServers(allowedUserMcpNames)) + setChannelMcpProbe. Сборка встроенных MCP перенесена из Phase 5.9 в services/capabilities/tools/mcp-builtin/; исходное название сохранено, поскольку модуль по-прежнему обрабатывает пользовательскую часть MCP |
| McpProviderRegistry | capabilities/tools/mcp-builtin/ | 11 статически зарегистрированных встроенных MCP Provider + фабрика динамических ScriptPluginProviders; единая точка входа assembleMcpForSession(ctx). Вызовы в SDK-маршруте: MagiService.assembleMagiMcps (Clawia), AgentService.mergeMcpAssembly (остальные). Исходный media-tools MCP перенесён в пилоте Tier C как toolkit media внутри elftia_toolkit (см. 08_builtin_mcp_and_toolkits.md §3). См. capabilities/tools/mcp-builtin/README.md |
| Skill Toolkit Registry | capabilities/tools/skill-toolkit/ | Диспетчер функций в процессе внутри MCP elftia_toolkit. Предоставляет 4 мета-инструмента: list_toolkits / read_toolkit / read_toolkit_reference / skill_invoke. Два встроенных toolkit: chrome-use (28 функций CDP) + media (10 функций генерации медиа/речи). Progressive disclosure: SKILL.md — индекс, глубокая документация на уровне провайдера/операции в map references toolkit, получаемая по запросу через read_toolkit_reference |
| Внедрение MCP AgentService | agent-core/agent/AgentService.ts | mergeMcpAssembly(sdkOptions, options, dbSessionId) — точка входа реестра для SDK-сессий не Clawia; внедряется в main/index.ts через setMcpAssembler. Очистка на уровне сессии автоматически привязывается к onSessionEnd |