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

Интеграция 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).

Шаги алгоритма

  1. Нет providerId → вернуть пустой объект; SDK использует process.env
  2. 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
  3. Другие путиllmConfig.getProvider(providerId) + resolveApiFormat(provider)
  4. Официальный провайдер Anthropic (provider.id === 'anthropic'):
    • Получить Key из ApiKeyPool или provider.api_key
    • Установить ANTHROPIC_API_KEY
    • Если есть пользовательский base URL, установить ANTHROPIC_BASE_URL (убрать завершающий /v1)
    • Пометить isOfficialProvider: true
  5. Другие провайдеры:
    • Получить 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 до fetchupstreamHeaders (скопированы + нормализованы из req.headers)
Ветка прямой передачи isOfficialProviderAgentProxyServer до fetchWithRetryОбъект, возвращённый getProviderHeaders(provider, apiKey)
Маршрут через цепочку трансформеровВыход TransformerChainExecutor.executeRequestChainconfig.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, маршрутизируются через прокси по следующим причинам:

  1. Нормализация URLapi_base_url может содержать /v1, SDK продублирует его до /v1/v1/messages
  2. Обработка заголовков аутентификации — разные провайдеры используют разные форматы заголовков (x-api-key и Bearer)
  3. Преобразование форматов — провайдеры не в формате Anthropic требуют трансформации форматов запроса/ответа
  4. Callback повторных попыток — ошибки 429/529 уведомляют фронтенд через callback
  5. Маршрутизация фоновых моделей — фоновые запросы 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.

Ключевые файлы

ФайлПутьОписание
ClaudeSdkEngineagent-core/engine/ClaudeSdkEngine.tsРеализация IEngine + buildProviderEnvWithProxy (включая ветку passthrough + передачу extendedContext)
AgentServiceagent-core/agent/AgentService.tsЖизненный цикл SDK-сессии; AgentSessionOptions содержит cliBackend + useExtendedContext, столбец DB model хранит сырой SDK id; runSession внедряет sdkOptions.sessionStore; resumeSession проходит fallback-проверки
AgentProxyServeragent-core/agent/AgentProxyServer.tsHTTP-прокси-сервер с тремя режимами: passThrough / isOfficialProvider direct / default transformer chain
Помощник внедрения 1M-context betaagent-core/agent/anthropicBetaInject.tsinjectExtendedContextBeta(body, model, useExtendedContext), общий для трёх точек выхода
SqliteSessionStoreagent-core/agent/SqliteSessionStore.tsSQLite-реализация интерфейса SessionStore SDK 0.3.x (append/load/delete/listSubkeys)
Помощник sdkPathEncodingagent-core/agent/sdkPathEncoding.tsencodeProjectPath (= replace(/[^A-Za-z0-9]/g, '-')) + hasResumableSdkJsonl (файл существует + содержит запись пользователя)
DAO worker sdkSessionStoreworkers/db/sdkSessionStore.tsCRUD таблицы sdk_session_store, 6 IPC (append/load/delete/listSubkeys/listSessions/countBySessionId)
Типы Code CLI + протокол разделенияshared/contracts/code-cli-types.tssplitModelReference() — единственный парсер строкового протокола на IPC-точке входа
ApiKeyPoolServicecapabilities/llm/completion/ApiKeyPoolService.tsБалансировка нагрузки с несколькими ключами
GitRuntimeServiceplatform/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 IPCRenderer → Main IPC
PermissionCallback SDK canUseTool (авторизация использования инструментов)agent:permissionRequestagent:respondPermission
AskUserQuestionAgent активно вызывает инструмент mcp__elftia-ask__askagent:askUserQuestionagent: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):

  1. Отключить встроенный: добавить 'AskUserQuestion' в sdkOptions.toolsSettings.disallowedTools
  2. Зарегистрировать замещающий MCP: AskUserQuestionMcp.ts использует createSdkMcpServer + tool() для предоставления mcp__elftia-ask__ask со схемой, идентичной встроенному (questions: Array<{question, header(≤12 chars), multiSelect, options[2-4]}>)
  3. Направить модель: appendSystemPrompt добавляет инструкцию вызывать mcp__elftia-ask__ask вместо встроенного при возникновении сценариев с вопросами
  4. Поток обработчика: вызов AgentService.askUserQuestion(dbSessionId, questions) → создание Promise + сохранение resolver в Map pendingQuestions → IPC agent:askUserQuestionAskUserQuestionDialog на стороне рендерера отображает stepper UI → пользователь нажимает Submit → IPC agent: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 (Map pendingQuestions + askUserQuestion() + respondToAskUserQuestion(), логика внедрения в конце runSession)
  • packages/desktop/app/main/services/routers/AgentRouter.ts (IPC agent:respondAskUserQuestion + zod schema)
  • packages/renderer/src/features/chat/components/agent/AskUserQuestionDialog.tsx (stepper UI: текущий вопрос развёрнут + отвеченные вопросы в свёрнутой сводной строке с возможностью возврата + постоянное текстовое поле «Other» + Submit активируется только после ответа на все вопросы)

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

  • Новое преобразование формата прокси: добавить новые адаптеры формата API в AgentProxyServer
  • Пользовательская стратегия повторных попыток: настроить поведение повторных попыток через параметр RetryCallback
  • Конфигурация фоновой модели: задать модель для фоновых задач через agentDefaults.background

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

МодульПутьСвязь
EngineDispatcheragent-core/engine/EngineDispatcher.tsРегистрация движка
LLMConfigServicecapabilities/llm/config-service/Конфигурация провайдера
MagiServiceagent-core/magi/MagiService.tsВысокоуровневая оркестрация (включает assembleMagiMcps с обходом реестра + слияние Object.assign в customAgentOptions.additionalMcpServers; buildTinyElfDirectMcpServers — точка входа TinyElf)
MagiSdkOptionsBuilderagent-core/magi/MagiSdkOptionsBuilder.tsПостроение промпта (V1-V4) + внедрение пользовательских MCP (setUserMcpServers / getMcpServers(allowedUserMcpNames)) + setChannelMcpProbe. Сборка встроенных MCP перенесена из Phase 5.9 в services/capabilities/tools/mcp-builtin/; исходное название сохранено, поскольку модуль по-прежнему обрабатывает пользовательскую часть MCP
McpProviderRegistrycapabilities/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 Registrycapabilities/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 AgentServiceagent-core/agent/AgentService.tsmergeMcpAssembly(sdkOptions, options, dbSessionId) — точка входа реестра для SDK-сессий не Clawia; внедряется в main/index.ts через setMcpAssembler. Очистка на уровне сессии автоматически привязывается к onSessionEnd