メインコンテンツまでスキップ

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 行のフラットフィールド(providerIdcliBackenduseExtendedContext、生の model)に基づいて異なる戦略を採用します。

アルゴリズムのステップ

  1. providerId なし → 空オブジェクトを返す;SDK は process.env を使用
  2. code-cli + cliBackend='claude-code'(サブスクリプション OAuth)buildClaudeCodePassThroughResult
    • passThrough: trueAgentProxyServer を起動
    • SDK 自体の Bearer ヘッダーを api.anthropic.com にパススルー
    • レスポンスヘッダー anthropic-ratelimit-unified-{5h,7d}-* を収集し、SubscriptionUsageService をトリガーしてフロントエンドの 5h/7d バッジを更新
    • Elftia 管理の OAuth トークン(TokensService.getValidClaudeAccessToken())をオプションで注入してシステム認証情報を上書き
    • isOfficialProvider: false としてマーク
  3. その他のパスllmConfig.getProvider(providerId) + resolveApiFormat(provider)
  4. Anthropic 公式プロバイダーprovider.id === 'anthropic'):
    • ApiKeyPool または provider.api_key からキーを取得
    • ANTHROPIC_API_KEY を設定
    • カスタムベース URL がある場合は ANTHROPIC_BASE_URL を設定(末尾の /v1 を除去)
    • isOfficialProvider: true としてマーク
  5. その他のプロバイダー
    • 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.handlePassThroughRequestupstreamHeadersreq.headers からコピー+正規化)
isOfficialProvider 直接パスブランチfetchWithRetry の前の AgentProxyServergetProviderHeaders(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 プロバイダーが次の理由でプロキシを経由します:

  1. URL 正規化api_base_url/v1 が含まれている場合、SDK が /v1/v1/messages と重複して結合してしまう
  2. Auth ヘッダーの処理 — プロバイダーによって認証ヘッダーの形式が異なる(x-api-key vs Bearer)
  3. フォーマット変換 — 非 Anthropic プロバイダーはリクエスト/レスポンスのフォーマット変換が必要
  4. リトライコールバック — 429/529 エラーをコールバックを通じてフロントエンドに通知
  5. バックグラウンドモデルルーティング — 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.runSessionsdkOptions.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 を参照してください。

関連ファイル

ファイルパス説明
ClaudeSdkEngineagent-core/engine/ClaudeSdkEngine.tsIEngine 実装+buildProviderEnvWithProxy(パススルーブランチ+extendedContext パススルーを含む)
AgentServiceagent-core/agent/AgentService.tsSDK セッションのライフサイクル;AgentSessionOptionscliBackenduseExtendedContext を保持し、DB の model カラムが生の SDK id を保存;runSessionsdkOptions.sessionStore を注入;resumeSession がフォールバック確認を実施
AgentProxyServeragent-core/agent/AgentProxyServer.ts3つのモードを持つ HTTP プロキシサーバー:passThrough / isOfficialProvider 直接 / デフォルトトランスフォーマーチェーン
1M-context beta インジェクションヘルパーagent-core/agent/anthropicBetaInject.tsinjectExtendedContextBeta(body, model, useExtendedContext)、3つの出口で共有
SqliteSessionStoreagent-core/agent/SqliteSessionStore.tsSDK 0.3.x SessionStore インターフェースの SQLite 実装(append/load/delete/listSubkeys)
sdkPathEncoding ヘルパーagent-core/agent/sdkPathEncoding.tsencodeProjectPath(= replace(/[^A-Za-z0-9]/g, '-'))+ hasResumableSdkJsonl(ファイルが存在し user レコードを含む)
sdkSessionStore ワーカー DAOworkers/db/sdkSessionStore.tssdk_session_store テーブルへの CRUD、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.tsWindows Git 設定

すべてのパスは packages/desktop/app/main/services/ からの相対パスです;shared/contracts/code-cli-types.tspackages/desktop/app/ にあり、workers/db/sdkSessionStore.tspackages/desktop/app/main/ にあります。

ユーザーインタラクションチャンネル(permission + ask_user_question)

Claude SDK エンジンにはバックエンドからレンダラーへの2つの対称的なダイアログチャンネルがあり、どちらも AgentServicependingXxx: Map<requestId, resolver> + 5分タイムアウト + active.sender.send 経由の IPC プッシュで管理します。

チャンネルトリガーMain → Renderer IPCRenderer → Main IPC
PermissionSDK canUseTool コールバック(ツール使用認可)agent:permissionRequestagent:respondPermission
AskUserQuestionAgent が mcp__elftia-ask__ask ツールを積極的に呼び出すagent:askUserQuestionagent:respondAskUserQuestion

Permission:bypassPermissions モードでのフォールバックコールバック

AgentService.runSession常に onPermissionRequest を登録し、lib/claude-sdk.tsmapCliOptionsToSDK常にそれを 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-1086isSkipPermissions の短絡を削除)

AskUserQuestion:SDK in-process MCP でビルトインを置き換える

Claude Code プリセットにバンドルされている AskUserQuestion ツールはターミナルプロンプトを起動しますが、elftia のメインプロセスはこれをサーフェスできません。修正アプローチ(方案 A):

  1. ビルトインを無効化sdkOptions.toolsSettings.disallowedTools'AskUserQuestion' を追加
  2. 代替 MCP を登録AskUserQuestionMcp.tscreateSdkMcpServertool() を使って 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 を pendingQuestions マップに保存 → IPC agent:askUserQuestion → レンダラーの AskUserQuestionDialog がステッパー UI を表示 → ユーザーが Submit → IPC agent: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.tspendingQuestions マップ+askUserQuestion()respondToAskUserQuestion()、注入ロジックは runSession の末尾)
  • packages/desktop/app/main/services/routers/AgentRouter.tsagent:respondAskUserQuestion IPC+zod スキーマ)
  • packages/renderer/src/features/chat/components/agent/AskUserQuestionDialog.tsx(ステッパー UI:現在の質問を展開し+回答済みの質問を折りたたんだサマリー行はクリックで戻れる+常に存在する「Other」テキスト入力+全部回答完了後に Submit が有効化)

拡張ポイント

  • 新しいプロキシフォーマット変換AgentProxyServer に新しい API フォーマットアダプターを追加
  • カスタムリトライ戦略RetryCallback パラメータを通じてリトライ動作をカスタマイズ
  • バックグラウンドモデル設定agentDefaults.background を通じてバックグラウンドタスクのモデルを設定

関連モジュール

モジュールパス関係
EngineDispatcheragent-core/engine/EngineDispatcher.tsエンジン登録
LLMConfigServicecapabilities/llm/config-service/プロバイダー設定
MagiServiceagent-core/magi/MagiService.ts高レベルのオーケストレーション(assembleMagiMcps がレジストリを走査し+Object.assigncustomAgentOptions.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 パイロットで elftia_toolkit 内の media ツールキットとして移設済み(08_builtin_mcp_and_toolkits.md §3 参照)。詳細は capabilities/tools/mcp-builtin/README.md
Skill Toolkit Registrycapabilities/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.tsmergeMcpAssembly(sdkOptions, options, dbSessionId) は非 Clawia SDK セッションのレジストリエントリポイント;setMcpAssembler を通じて main/index.ts で接続。セッションごとのクリーンアップは自動的に onSessionEnd にチェーン