본문으로 건너뛰기

ClaudeSdkEngine 통합

ClaudeSdkEngine@anthropic-ai/claude-agent-sdk를 감싸는 IEngine 래퍼입니다. 프로바이더 프록시 설정, API Key 해석, 다중 키 로드 밸런싱, 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)를 기반으로 서로 다른 전략을 채택합니다.

알고리즘 단계

  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 Token(TokensService.getValidClaudeAccessToken())의 선택적 주입으로 시스템 자격증명 재정의 가능
    • isOfficialProvider: false 표시
  3. 기타 경로llmConfig.getProvider(providerId) + resolveApiFormat(provider)
  4. 공식 Anthropic 프로바이더(provider.id === 'anthropic'):
    • ApiKeyPool 또는 provider.api_key에서 Key 조회
    • ANTHROPIC_API_KEY 설정
    • 커스텀 base URL이 있으면 ANTHROPIC_BASE_URL 설정 (후행 /v1 제거)
    • isOfficialProvider: true 표시
  5. 기타 프로바이더:
    • ApiKeyPool 또는 provider.api_key에서 Key 조회
    • AgentProxyServer(로컬 HTTP 프록시) 실행
    • ANTHROPIC_BASE_URL = proxy.getBaseUrl() 설정
    • Anthropic 형식 프로바이더에는 실제 Key 전달 (서버 사이드 도구 활성화)
    • 비 Anthropic 형식에는 'proxy-mode' 전달 (Key는 프록시가 처리)
    • onSessionEnd 정리 콜백 반환 (프록시 중지)

함수 시그니처에 schemaFields?: { cliBackend, useExtendedContext } 파라미터가 추가되었습니다 (v89+). 호출자가 chat_sessions 행에서 투명하게 전달합니다. cliBackend === 'claude-code'는 passthrough 분기를 트리거하고, useExtendedContext === true는 1M-context beta 주입을 트리거합니다 (아래 참조).

1M-context beta 주입 (v89+)

chat_sessions.useExtendedContext = 1이고 model이 1M-capable 화이트리스트(claude-opus-4-7 / claude-opus-4-6 / claude-sonnet-4-6)에 있으면, injectExtendedContextBeta(headers, model, useExtendedContext)가 아웃바운드 요청의 anthropic-beta HTTP 헤더에 'context-1m-2025-08-07'을 병합합니다 (쉼표로 구분된 목록).

body 필드가 아닙니다. 초기 구현에서는 플래그를 body.anthropic_beta 배열에 넣었으나, /v1/messages 엔드포인트가 400 "anthropic_beta: Extra inputs are not permitted" 오류로 거부했습니다. 표준 경로는 HTTP 헤더입니다.

주입 지점 (세 곳에서 동일한 멱등 + 대소문자 무관 헬퍼를 공유):

경로위치주입 대상 headers 객체
passthrough 모드 (claude-code OAuth)AgentProxyServer.handlePassThroughRequestfetch 이전upstreamHeaders (req.headers에서 복사 + 정규화)
isOfficialProvider 직접 전달 분기AgentProxyServerfetchWithRetry 이전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). [1m] UI 마커가 이 헬퍼를 통해 HTTP 헤더에 주입되지 않으면 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 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는 Pool에서 Key를 우선적으로 조회합니다:

let apiKey = '';
if (apiKeyPool && sessionId) {
apiKey = await apiKeyPool.getKeyForSession(provider.id, sessionId);
}
if (!apiKey) {
apiKey = resolveApiKey(provider.api_key);
}

Pool은 가중 라운드 로빈 + 세션 어피니티 전략으로 Key를 분배하며, 429/529 오류 발생 시 자동 쿨다운이 트리거됩니다.

Windows Git 설정

setGitRuntime(runtime: GitRuntimeService): void;

Claude Agent SDK 서브프로세스는 Git과 Bash를 필요로 합니다. Windows에서 GitRuntimeService는 다음을 담당합니다:

  • Git for Windows 설치 경로 감지
  • git-bash 경로를 PATH에 추가
  • SDK 서브프로세스가 정상적으로 실행될 수 있도록 보장

startSessionresumeSession 이전에 ensureGitForSdk()를 자동으로 호출합니다.

두 가지 호출 경로

셀프서비스 경로 (AgentRouter)

AgentRouter는 providerEnv 없이 ClaudeSdkEngine을 직접 호출합니다:

// ctx.providerEnv is empty
engine.startSession(ctx);
// → internally calls buildProviderEnvWithProxy() to construct itself

Magi 사전 빌드 경로

MagiService는 providerEnv를 미리 구성하여 전달합니다:

// ctx.providerEnv already built by MagiService
engine.startSession(ctx);
// → directly uses ctx.providerEnv, skips self-construction
// → uses startWithExistingSession (DB session already exists)

결정 로직

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은 재시도 알림을 전송합니다:

이벤트페이로드설명
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 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 스키마 ≠ 디스크 스키마, SDK 거부)는 migration v97에서 완전히 종료되었습니다. docs/dev/66_sdk_records/01_schema_divergence_investigation.md를 참조하세요.

주요 파일

파일경로설명
ClaudeSdkEngineagent-core/engine/ClaudeSdkEngine.tsIEngine 구현 + buildProviderEnvWithProxy (passThrough 분기 + extendedContext 투명 전달 포함)
AgentServiceagent-core/agent/AgentService.tsSDK 세션 라이프사이클; AgentSessionOptionscliBackend + useExtendedContext 포함, DB model 컬럼에 원시 SDK id 저장; runSession에서 sdkOptions.sessionStore 주입; resumeSession에서 폴백 검사 수행
AgentProxyServeragent-core/agent/AgentProxyServer.tsHTTP 프록시 서버, 세 가지 모드: passThrough / isOfficialProvider 직접 전달 / 기본 트랜스포머 체인
1M-context beta 주입 헬퍼agent-core/agent/anthropicBetaInject.tsinjectExtendedContextBeta(body, model, useExtendedContext), 세 곳의 출구에서 공유
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 worker 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 엔진은 백엔드에서 렌더러로 향하는 두 개의 대칭적인 대화 채널을 가지며, 둘 다 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에 연결합니다. bypass 모드에서는 콜백 내부적으로 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 인-프로세스 MCP로 내장 도구 대체

Claude Code 프리셋에 번들된 AskUserQuestion 도구는 터미널 프롬프트를 표시하는데 Elftia 메인 프로세스가 이를 노출할 수 없습니다. 수정 접근법 (옵션 A):

  1. 내장 비활성화: sdkOptions.toolsSettings.disallowedTools'AskUserQuestion' 추가
  2. 대체 MCP 등록: AskUserQuestionMcp.tscreateSdkMcpServer + 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 생성 + pendingQuestions Map에 resolver 저장 → IPC agent:askUserQuestion → 렌더러 AskUserQuestionDialog가 스테퍼 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 (pendingQuestions Map + askUserQuestion() + respondToAskUserQuestion(), runSession 끝부분의 주입 로직)
  • packages/desktop/app/main/services/routers/AgentRouter.ts (agent: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.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 파일럿에서 마이그레이션됨 elftia_toolkitmedia 툴킷으로 (08_builtin_mcp_and_toolkits.md §3 참조). capabilities/tools/mcp-builtin/README.md 참조
Skill Toolkit Registrycapabilities/tools/skill-toolkit/elftia_toolkit MCP 내 인-프로세스 함수 디스패처. 4개의 메타 도구 노출: list_toolkits / read_toolkit / read_toolkit_reference / skill_invoke. 내장 툴킷 두 개: chrome-use (28개 CDP 함수) + media (10개 미디어 생성/음성 함수). 점진적 공개: SKILL.md가 인덱스, 프로바이더별/작업별 심층 문서는 툴킷의 references 맵에 저장, 필요 시 read_toolkit_reference로 조회
AgentService MCP 주입agent-core/agent/AgentService.tsmergeMcpAssembly(sdkOptions, options, dbSessionId)는 비 Clawia SDK 세션의 레지스트리 진입점; main/index.ts에서 setMcpAssembler를 통해 연결. 세션별 정리 작업이 onSessionEnd에 자동으로 체인됨