Coverage for src / infra / sdk_adapter.py: 50%

12 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2026-01-04 04:43 +0000

1"""SDK adapter for Claude Agent SDK. 

2 

3This module provides a factory for creating Claude SDK clients, isolating 

4SDK imports to the infra layer. The pipeline layer uses SDKClientProtocol 

5from core.protocols instead of importing SDK types directly. 

6 

7Design principles: 

8- All SDK imports are local (inside methods, not at module level) 

9- Factory pattern enables dependency injection and testing 

10- TYPE_CHECKING imports for SDK types avoid runtime dependency 

11""" 

12 

13from __future__ import annotations 

14 

15from typing import TYPE_CHECKING, cast 

16 

17if TYPE_CHECKING: 

18 from src.core.protocols import SDKClientProtocol 

19 

20 

21class SDKClientFactory: 

22 """Factory for creating Claude SDK clients. 

23 

24 This factory encapsulates SDK imports and client creation, allowing 

25 the pipeline layer to use SDK clients without importing SDK directly. 

26 

27 Usage: 

28 factory = SDKClientFactory() 

29 client = factory.create(options) 

30 async with client: 

31 await client.query(prompt) 

32 async for msg in client.receive_response(): 

33 ... 

34 """ 

35 

36 def create(self, options: object) -> SDKClientProtocol: 

37 """Create a new SDK client with the given options. 

38 

39 Args: 

40 options: ClaudeAgentOptions (or compatible) for the client. 

41 The type is `object` to avoid requiring SDK import in 

42 the caller's type annotations. 

43 

44 Returns: 

45 SDKClientProtocol wrapping a ClaudeSDKClient. 

46 """ 

47 from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient # noqa: TC002 

48 

49 return cast( 

50 "SDKClientProtocol", 

51 ClaudeSDKClient(options=cast("ClaudeAgentOptions", options)), 

52 ) 

53 

54 def create_options( 

55 self, 

56 *, 

57 cwd: str, 

58 permission_mode: str = "bypassPermissions", 

59 model: str = "opus", 

60 system_prompt: dict[str, str] | None = None, 

61 setting_sources: list[str] | None = None, 

62 mcp_servers: object | None = None, 

63 disallowed_tools: list[str] | None = None, 

64 env: dict[str, str] | None = None, 

65 hooks: dict[str, list[object]] | None = None, 

66 ) -> object: 

67 """Create SDK options without requiring SDK import in caller. 

68 

69 This method encapsulates the ClaudeAgentOptions construction, 

70 allowing callers to build options without SDK imports. 

71 

72 Args: 

73 cwd: Working directory for the agent. 

74 permission_mode: Permission mode (default "bypassPermissions"). 

75 model: Model to use (default "opus"). 

76 system_prompt: System prompt configuration. 

77 setting_sources: List of setting sources. 

78 mcp_servers: List of MCP server configurations. 

79 disallowed_tools: List of tools to disallow. 

80 env: Environment variables for the agent. 

81 hooks: Hook configurations keyed by event type. 

82 

83 Returns: 

84 ClaudeAgentOptions instance. 

85 """ 

86 from claude_agent_sdk import ClaudeAgentOptions 

87 

88 return ClaudeAgentOptions( 

89 cwd=cwd, 

90 permission_mode=permission_mode, # type: ignore[arg-type] 

91 model=model, 

92 system_prompt=system_prompt or {"type": "preset", "preset": "claude_code"}, # type: ignore[arg-type] 

93 setting_sources=setting_sources or ["project", "user"], # type: ignore[arg-type] 

94 mcp_servers=mcp_servers, # type: ignore[arg-type] 

95 disallowed_tools=disallowed_tools, # type: ignore[arg-type] 

96 env=env, # type: ignore[arg-type] 

97 hooks=hooks, # type: ignore[arg-type] 

98 ) 

99 

100 def create_hook_matcher( 

101 self, 

102 matcher: object | None, 

103 hooks: list[object], 

104 ) -> object: 

105 """Create a HookMatcher for SDK hook registration. 

106 

107 Args: 

108 matcher: Optional matcher configuration. 

109 hooks: List of hook callables. 

110 

111 Returns: 

112 HookMatcher instance. 

113 """ 

114 from claude_agent_sdk.types import HookMatcher 

115 

116 return HookMatcher(matcher=matcher, hooks=hooks) # type: ignore[arg-type]