diff --git a/e2e-tests/test_dynamic_control.py b/e2e-tests/test_dynamic_control.py new file mode 100644 index 00000000..779ebea0 --- /dev/null +++ b/e2e-tests/test_dynamic_control.py @@ -0,0 +1,97 @@ +"""End-to-end tests for dynamic control features with real Claude API calls.""" + +import pytest + +from claude_code_sdk import ( + ClaudeAgentOptions, + ClaudeSDKClient, +) + + +@pytest.mark.e2e +@pytest.mark.asyncio +async def test_set_permission_mode(): + """Test that permission mode can be changed dynamically during a session.""" + + options = ClaudeAgentOptions( + permission_mode="default", + ) + + async with ClaudeSDKClient(options=options) as client: + # Change permission mode to acceptEdits + await client.set_permission_mode("acceptEdits") + + # Make a query that would normally require permission + await client.query("What is 2+2? Just respond with the number.") + + async for message in client.receive_response(): + print(f"Got message: {message}") + pass # Just consume messages + + # Change back to default + await client.set_permission_mode("default") + + # Make another query + await client.query("What is 3+3? Just respond with the number.") + + async for message in client.receive_response(): + print(f"Got message: {message}") + pass # Just consume messages + + +@pytest.mark.e2e +@pytest.mark.asyncio +async def test_set_model(): + """Test that model can be changed dynamically during a session.""" + + options = ClaudeAgentOptions() + + async with ClaudeSDKClient(options=options) as client: + # Start with default model + await client.query("What is 1+1? Just the number.") + + async for message in client.receive_response(): + print(f"Default model response: {message}") + pass + + # Switch to Haiku model + await client.set_model("claude-3-5-haiku-20241022") + + await client.query("What is 2+2? Just the number.") + + async for message in client.receive_response(): + print(f"Haiku model response: {message}") + pass + + # Switch back to default (None means default) + await client.set_model(None) + + await client.query("What is 3+3? Just the number.") + + async for message in client.receive_response(): + print(f"Back to default model: {message}") + pass + + +@pytest.mark.e2e +@pytest.mark.asyncio +async def test_interrupt(): + """Test that interrupt can be sent during a session.""" + + options = ClaudeAgentOptions() + + async with ClaudeSDKClient(options=options) as client: + # Start a query + await client.query("Count from 1 to 100 slowly.") + + # Send interrupt (may or may not stop the response depending on timing) + try: + await client.interrupt() + print("Interrupt sent successfully") + except Exception as e: + print(f"Interrupt resulted in: {e}") + + # Consume any remaining messages + async for message in client.receive_response(): + print(f"Got message after interrupt: {message}") + pass diff --git a/src/claude_code_sdk/_internal/query.py b/src/claude_code_sdk/_internal/query.py index d83951e6..affbe181 100644 --- a/src/claude_code_sdk/_internal/query.py +++ b/src/claude_code_sdk/_internal/query.py @@ -469,6 +469,15 @@ async def set_permission_mode(self, mode: str) -> None: } ) + async def set_model(self, model: str | None) -> None: + """Change the AI model.""" + await self._send_control_request( + { + "subtype": "set_model", + "model": model, + } + ) + async def stream_input(self, stream: AsyncIterable[dict[str, Any]]) -> None: """Stream input messages to transport.""" try: diff --git a/src/claude_code_sdk/client.py b/src/claude_code_sdk/client.py index 8c12be19..ba9e3130 100644 --- a/src/claude_code_sdk/client.py +++ b/src/claude_code_sdk/client.py @@ -193,6 +193,54 @@ async def interrupt(self) -> None: raise CLIConnectionError("Not connected. Call connect() first.") await self._query.interrupt() + async def set_permission_mode(self, mode: str) -> None: + """Change permission mode during conversation (only works with streaming mode). + + Args: + mode: The permission mode to set. Valid options: + - 'default': CLI prompts for dangerous tools + - 'acceptEdits': Auto-accept file edits + - 'bypassPermissions': Allow all tools (use with caution) + + Example: + ```python + async with ClaudeSDKClient() as client: + # Start with default permissions + await client.query("Help me analyze this codebase") + + # Review mode done, switch to auto-accept edits + await client.set_permission_mode('acceptEdits') + await client.query("Now implement the fix we discussed") + ``` + """ + if not self._query: + raise CLIConnectionError("Not connected. Call connect() first.") + await self._query.set_permission_mode(mode) + + async def set_model(self, model: str | None = None) -> None: + """Change the AI model during conversation (only works with streaming mode). + + Args: + model: The model to use, or None to use default. Examples: + - 'claude-sonnet-4-20250514' + - 'claude-opus-4-1-20250805' + - 'claude-opus-4-20250514' + + Example: + ```python + async with ClaudeSDKClient() as client: + # Start with default model + await client.query("Help me understand this problem") + + # Switch to a different model for implementation + await client.set_model('claude-3-5-sonnet-20241022') + await client.query("Now implement the solution") + ``` + """ + if not self._query: + raise CLIConnectionError("Not connected. Call connect() first.") + await self._query.set_model(model) + async def get_server_info(self) -> dict[str, Any] | None: """Get server initialization info including available commands and output styles.