ClaudeSdkEngine インテグレーション
ClaudeSdkEngine は @anthropic-ai/claude-agent-sdk を包む IEngine ラッパーです。プロバイダープロキシ(代理サーバー)のセットアップ、API キーの解決、マルチキーロードバランシング、および Windows 環境での Git/Bash 自動設定を担います。
アーキテクチャ図
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'(サブスクリプション OAuth) →buildClaudeCodePassThroughResult:passThrough: trueでAgentProxyServerを起動- SDK 自体の Bearer ヘッダーを
api.anthropic.comにパススルー - レスポンスヘッダー
anthropic-ratelimit-unified-{5h,7d}-*を収集し、SubscriptionUsageServiceをトリガーしてフロントエンドの 5h/7d バッジを更新 - Elftia 管理の OAuth トークン(
TokensService.getValidClaudeAccessToken())をオプションで注入してシステム認証情報を上書き isOfficialProvider: falseとしてマーク
- その他のパス →
llmConfig.getProvider(providerId)+resolveApiFormat(provider) - Anthropic 公式プロバイダー(
provider.id === 'anthropic'):- ApiKeyPool または provider.api_key からキーを取得
ANTHROPIC_API_KEYを設定- カスタムベース URL がある場合は
ANTHROPIC_BASE_URLを設定(末尾の/v1を除去) isOfficialProvider: trueとしてマーク
- その他のプロバイダー:
- ApiKeyPool または provider.api_key からキーを取得
AgentProxyServer(ローカル HTTP プロキシ)を起動ANTHROPIC_BASE_URL = proxy.getBaseUrl()を設定- Anthropic フォーマットのプロバイダーには実際のキーを渡す(サーバーサイドツールを有効化)
- 非 Anthropic フォーマットには
'proxy-mode'を渡す(キーはプロキシが処理) onSessionEndクリーンアップコールバックを返す(プロキシを停止)
関数シグネチャには
schemaFields?: { cliBackend, useExtendedContext }パラメータ(v89+)が追加されており、呼び出し元からchat_sessions行を経由して渡されます。cliBackend === 'claude-code'はパススルーブランチをトリガーし、useExtendedContext === trueは 1M-context beta インジェクションをトリガーします(下記参照)。
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' を送信リクエストの anthropic-beta HTTP ヘッダーにマージします(カンマ区切りリスト)。
body フィールドではありません。初期実装ではフラグを
body.anthropic_beta配列に記述していましたが、/v1/messagesエンドポイントに 400"anthropic_beta: Extra inputs are not permitted"で拒否されました。正規のパスは HTTP ヘッダーです。
インジェクションポイント(3箇所が同じ冪等+大文字小文字非依存のヘルパーを共有):
| ルート | 場所 | ヘッダーオブジェクトの注入先 |
|---|---|---|
| パススルーモード(claude-code OAuth) | fetch の前の AgentProxyServer.handlePassThroughRequest | upstreamHeaders(req.headers からコピー+正規化) |
| isOfficialProvider 直接パスブランチ | fetchWithRetry の前の AgentProxyServer | getProviderHeaders(provider, apiKey) が返すオブジェクト |
| トランスフォーマーチェーンルート | TransformerChainExecutor.executeRequestChain の出口 | config.headers(fetch ヘッダーにマージ) |
このヘルパーは SDK によって書き込まれた他の beta(例:prompt-caching-2024-07-31)を保持し、大文字小文字のバリアント(Anthropic-Beta も認識され、正規の小文字 anthropic-beta として書き直される)と互換性があります。
Claude Agent SDK 自体は 1M-context beta を自動的には追加しません(ソース内に
context-1mの出現は 0 件)。このヘルパーを通じて HTTP ヘッダーに[1m]UI マーカーが注入されない場合、1M コンテキストは有効にならず、リクエストは引き続き 200K モードで処理されます。
プロキシサーバーの役割
すべての非内蔵 Anthropic プロバイダーが次の理由でプロキシを経由します:
- URL 正規化 —
api_base_urlに/v1が含まれている場合、SDK が/v1/v1/messagesと重複して結合してしまう - Auth ヘッダーの処理 — プロバイダーによって認証ヘッダーの形式が異なる(x-api-key vs Bearer)
- フォーマット変換 — 非 Anthropic プロバイダーはリクエスト/レスポンスのフォーマット変換が必要
- リトライコールバック — 429/529 エラーをコールバックを通じてフロントエンドに通知
- バックグラウンドモデルルーティング — SDK のバックグラウンドタスクリクエストを別のモデルにルーティング可能
API キーの解決
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 はプールからキーを優先的に取得します:
let apiKey = '';
if (apiKeyPool && sessionId) {
apiKey = await apiKeyPool.getKeyForSession(provider.id, sessionId);
}
if (!apiKey) {
apiKey = resolveApiKey(provider.api_key);
}
プールは重み付きラウンドロビン+セッションアフィニティ戦略でキーを分配し、429/529 エラー発生時には自動クールダウンを起動します。
Windows Git 設定
setGitRuntime(runtime: GitRuntimeService): void;
Claude Agent SDK のサブプロセスには Git と Bash が必要です。Windows では GitRuntimeService が以下を担当します:
- Git for Windows のインストールパスを検出
- git-bash パスを
PATHに追加 - SDK サブプロセスが正常に実行できることを保証
各 startSession および resumeSession の前に ensureGitForSdk() を自動的に呼び出します。
2つの呼び出しルート
セルフサービスルート(AgentRouter)
AgentRouter は providerEnv を渡さずに ClaudeSdkEngine を直接呼び出します:
// 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 キーのバリデーション
キーが不足したことで 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 はリトライ通知も送信します:
| イベント | ペイロード | 説明 |
|---|---|---|
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 サブプロセスがディスクに JSONL を書き込む → SDK も sessionStore.append(key, entries) を呼び出す
→ SqliteSessionStore → sdkSessionStore:append IPC → DB ワーカー
→ sdk_session_store テーブルへ INSERT OR IGNORE(entryUuid で冪等)
再開時:SDK が sessionStore.load(key) を呼び出す → entries[] | null を返す
→ SDK が entries を一時 JSONL にマテリアライズ → サブプロセス --resume
→ ディスク JSONL が失われても回復可能(sdk_session_store にデータがある限り)
再開時の安全確認(AgentService.resumeSession):ディスパッチ前に hasResumableSdkJsonl(projectPath, sdkSessionId)(ディスクファイルが存在し type:'user' レコードを含む)と sdkSessionStore:countBySessionId > 0 の両方を確認します;両方が空の場合 → sdkSessionId をクリアし SDK に新しい会話を開始させます(DB のチャット履歴は保持)。
非推奨パス:旧 sdk_records テーブル+ JsonlBuilder.reconstruct() パス(IPC スキーマ ≠ ディスクスキーマ、SDK が拒否)は migration v97 で完全に廃止済みです。詳細は docs/dev/66_sdk_records/01_schema_divergence_investigation.md を参照してください。
関連ファイル
| ファイル | パス | 説明 |
|---|---|---|
| ClaudeSdkEngine | agent-core/engine/ClaudeSdkEngine.ts | IEngine 実装+buildProviderEnvWithProxy(パススルーブランチ+extendedContext パススルーを含む) |
| AgentService | agent-core/agent/AgentService.ts | SDK セッションのライフサイクル;AgentSessionOptions が cliBackend+useExtendedContext を保持し、DB の model カラムが生の SDK id を保存;runSession が sdkOptions.sessionStore を注入;resumeSession がフォールバック確認を実施 |
| AgentProxyServer | agent-core/agent/AgentProxyServer.ts | 3つのモードを持つ HTTP プロキシサーバー:passThrough / isOfficialProvider 直接 / デフォルトトランスフォーマーチェーン |
| 1M-context beta インジェクションヘルパー | agent-core/agent/anthropicBetaInject.ts | injectExtendedContextBeta(body, model, useExtendedContext)、3つの出口で共有 |
| SqliteSessionStore | agent-core/agent/SqliteSessionStore.ts | SDK 0.3.x SessionStore インターフェースの SQLite 実装(append/load/delete/listSubkeys) |
| sdkPathEncoding ヘルパー | agent-core/agent/sdkPathEncoding.ts | encodeProjectPath(= replace(/[^A-Za-z0-9]/g, '-'))+ hasResumableSdkJsonl(ファイルが存在し user レコードを含む) |
| sdkSessionStore ワーカー DAO | workers/db/sdkSessionStore.ts | sdk_session_store テーブルへの CRUD、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 | Windows Git 設定 |
すべてのパスは 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 エンジンにはバックエンドからレンダラーへの2つの対称的なダイアログチャンネルがあり、どちらも AgentService が pendingXxx: Map<requestId, resolver> + 5分タイムアウト + active.sender.send 経由の IPC プッシュで管理します。
| チャンネル | トリガー | Main → Renderer IPC | Renderer → Main IPC |
|---|---|---|---|
| Permission | SDK canUseTool コールバック(ツール使用認可) | agent:permissionRequest | agent:respondPermission |
| AskUserQuestion | Agent が mcp__elftia-ask__ask ツールを積極的に呼び出す | agent:askUserQuestion | agent:respondAskUserQuestion |
Permission:bypassPermissions モードでのフォールバックコールバック
AgentService.runSession は常に onPermissionRequest を登録し、lib/claude-sdk.ts の mapCliOptionsToSDK は常にそれを SDK の canUseTool にブリッジします。バイパスモードでは、コールバック内部で behavior: 'allow' が適用され、診断フィールド agentID / blockedPath / decisionReason が出力されます。
以前のコードは両方のレイヤーで if (skipPermissions) skip を使ってコールバックを短絡していました。これによりサイレントな拒否バグが発生しました:メインエージェントが bypassPermissions であっても、サブエージェントが tools: [Read, Glob] でツールホワイトリストを絞り込むと、SDK はサブエージェントのアウトオブバウンドな呼び出しに対して canUseTool ゲートを実行しますが、コールバックが登録されていない → サイレント拒否 → ユーザーには「権限が拒否されました」と表示されるが elftia はダイアログを表示しない、という問題でした。
修正の主要コード:
packages/desktop/app/main/lib/claude-sdk.ts:262-339(両方のコールバックレイヤーを常に登録)packages/desktop/app/main/services/agent-core/agent/AgentService.ts:1064-1086(isSkipPermissionsの短絡を削除)
AskUserQuestion:SDK in-process MCP でビルトインを置き換える
Claude Code プリセットにバンドルされている AskUserQuestion ツールはターミナルプロンプトを起動しますが、elftia のメインプロセスはこれをサーフェスできません。修正アプローチ(方案 A):
- ビルトインを無効化:
sdkOptions.toolsSettings.disallowedToolsに'AskUserQuestion'を追加 - 代替 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 をpendingQuestionsマップに保存 → IPCagent:askUserQuestion→ レンダラーのAskUserQuestionDialogがステッパー UI を表示 → ユーザーが Submit → IPCagent:respondAskUserQuestion→ resolver が answers を 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(pendingQuestionsマップ+askUserQuestion()+respondToAskUserQuestion()、注入ロジックはrunSessionの末尾)packages/desktop/app/main/services/routers/AgentRouter.ts(agent:respondAskUserQuestionIPC+zod スキーマ)packages/renderer/src/features/chat/components/agent/AskUserQuestionDialog.tsx(ステッパー UI:現在の質問を展開し+回答済みの質問を折りたたんだサマリー行はクリックで戻れる+常に存在する「Other」テキスト入力+全部回答完了後に Submit が有効化)
拡張ポイント
- 新しいプロキシフォーマット変換:
AgentProxyServerに新しい API フォーマットアダプターを追加 - カスタムリトライ戦略:
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 パイロットで elftia_toolkit 内の media ツールキットとして移設済み(08_builtin_mcp_and_toolkits.md §3 参照)。詳細は capabilities/tools/mcp-builtin/README.md |
| Skill Toolkit Registry | capabilities/tools/skill-toolkit/ | elftia_toolkit MCP 内の in-process 関数ディスパッチャー。4つのメタツールを公開:list_toolkits / read_toolkit / read_toolkit_reference / skill_invoke。2つのビルトインツールキット:chrome-use(28 CDP 関数)+media(10 個のメディア生成/音声関数)。段階的な開示:SKILL.md がインデックス、プロバイダー/オペレーション別の詳細ドキュメントはツールキットの references マップに保存され、read_toolkit_reference でオンデマンドに取得 |
| AgentService MCP インジェクション | agent-core/agent/AgentService.ts | mergeMcpAssembly(sdkOptions, options, dbSessionId) は非 Clawia SDK セッションのレジストリエントリポイント;setMcpAssembler を通じて main/index.ts で接続。セッションごとのクリーンアップは自動的に onSessionEnd にチェーン |