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

1"""SDK adapter for Claude Agent SDK.

2

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

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

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

6

7 Design 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

13 from __future__ import annotations

14

15 from typing import TYPE_CHECKING, cast

16

17 if TYPE_CHECKING:

18 from src.core.protocols import SDKClientProtocol

19

20

21 class 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]