diff --git a/src/claude_agent_sdk/_internal/client.py b/src/claude_agent_sdk/_internal/client.py index 6127ba56..76323323 100644 --- a/src/claude_agent_sdk/_internal/client.py +++ b/src/claude_agent_sdk/_internal/client.py @@ -91,6 +91,15 @@ async def process_query( if isinstance(config, dict) and config.get("type") == "sdk": sdk_mcp_servers[name] = config["instance"] # type: ignore[typeddict-item] + # Extract exclude_dynamic_sections from preset system prompt for the + # initialize request (older CLIs ignore unknown initialize fields). + exclude_dynamic_sections: bool | None = None + sp = configured_options.system_prompt + if isinstance(sp, dict) and sp.get("type") == "preset": + eds = sp.get("exclude_dynamic_sections") + if isinstance(eds, bool): + exclude_dynamic_sections = eds + # Convert agents to dict format for initialize request agents_dict = None if configured_options.agents: @@ -118,6 +127,7 @@ async def process_query( sdk_mcp_servers=sdk_mcp_servers, initialize_timeout=initialize_timeout, agents=agents_dict, + exclude_dynamic_sections=exclude_dynamic_sections, ) try: diff --git a/src/claude_agent_sdk/_internal/query.py b/src/claude_agent_sdk/_internal/query.py index 81eca07f..80b6d93c 100644 --- a/src/claude_agent_sdk/_internal/query.py +++ b/src/claude_agent_sdk/_internal/query.py @@ -76,6 +76,7 @@ def __init__( sdk_mcp_servers: dict[str, "McpServer"] | None = None, initialize_timeout: float = 60.0, agents: dict[str, dict[str, Any]] | None = None, + exclude_dynamic_sections: bool | None = None, ): """Initialize Query with transport and callbacks. @@ -87,6 +88,8 @@ def __init__( sdk_mcp_servers: Optional SDK MCP server instances initialize_timeout: Timeout in seconds for the initialize request agents: Optional agent definitions to send via initialize + exclude_dynamic_sections: Optional preset-prompt flag to send via + initialize (see ``SystemPromptPreset``) """ self._initialize_timeout = initialize_timeout self.transport = transport @@ -95,6 +98,7 @@ def __init__( self.hooks = hooks or {} self.sdk_mcp_servers = sdk_mcp_servers or {} self._agents = agents + self._exclude_dynamic_sections = exclude_dynamic_sections # Control protocol state self.pending_control_responses: dict[str, anyio.Event] = {} @@ -154,6 +158,8 @@ async def initialize(self) -> dict[str, Any] | None: } if self._agents: request["agents"] = self._agents + if self._exclude_dynamic_sections is not None: + request["excludeDynamicSections"] = self._exclude_dynamic_sections # Use longer timeout for initialize since MCP servers may take time to start response = await self._send_control_request( diff --git a/src/claude_agent_sdk/client.py b/src/claude_agent_sdk/client.py index aaa9ab67..64f845fb 100644 --- a/src/claude_agent_sdk/client.py +++ b/src/claude_agent_sdk/client.py @@ -157,6 +157,15 @@ async def _empty_stream() -> AsyncIterator[dict[str, Any]]: ) initialize_timeout = max(initialize_timeout_ms / 1000.0, 60.0) + # Extract exclude_dynamic_sections from preset system prompt for the + # initialize request (older CLIs ignore unknown initialize fields). + exclude_dynamic_sections: bool | None = None + sp = self.options.system_prompt + if isinstance(sp, dict) and sp.get("type") == "preset": + eds = sp.get("exclude_dynamic_sections") + if isinstance(eds, bool): + exclude_dynamic_sections = eds + # Convert agents to dict format for initialize request agents_dict: dict[str, dict[str, Any]] | None = None if self.options.agents: @@ -176,6 +185,7 @@ async def _empty_stream() -> AsyncIterator[dict[str, Any]]: sdk_mcp_servers=sdk_mcp_servers, initialize_timeout=initialize_timeout, agents=agents_dict, + exclude_dynamic_sections=exclude_dynamic_sections, ) # Start reading messages and initialize diff --git a/src/claude_agent_sdk/types.py b/src/claude_agent_sdk/types.py index fca39dcd..a82a8b9b 100644 --- a/src/claude_agent_sdk/types.py +++ b/src/claude_agent_sdk/types.py @@ -38,6 +38,18 @@ class SystemPromptPreset(TypedDict): type: Literal["preset"] preset: Literal["claude_code"] append: NotRequired[str] + exclude_dynamic_sections: NotRequired[bool] + """Strip per-user dynamic sections (working directory, auto-memory, git + status) from the system prompt so it stays static and cacheable across + users. The stripped content is re-injected into the first user message + so the model still has access to it. + + Use this when many users share the same preset system prompt and you + want the prompt-caching prefix to hit cross-user. + + Requires a Claude Code CLI version that supports this option; older + CLIs silently ignore it. + """ class SystemPromptFile(TypedDict): diff --git a/tests/test_query.py b/tests/test_query.py index d0cece38..30e51c09 100644 --- a/tests/test_query.py +++ b/tests/test_query.py @@ -25,6 +25,40 @@ from claude_agent_sdk.types import HookMatcher +def _capture_initialize_request(**query_kwargs): + """Run Query.initialize() with a stubbed control channel and return the request dict.""" + captured: dict = {} + + async def _run(): + transport = AsyncMock() + transport.is_ready = Mock(return_value=True) + q = Query(transport=transport, is_streaming_mode=True, **query_kwargs) + + async def fake_send(request, timeout): + captured.update(request) + return {"commands": []} + + with patch.object(q, "_send_control_request", side_effect=fake_send): + await q.initialize() + + anyio.run(_run) + return captured + + +def test_initialize_sends_exclude_dynamic_sections(): + """Query.initialize() includes excludeDynamicSections in the control request.""" + sent = _capture_initialize_request(exclude_dynamic_sections=True) + assert sent["subtype"] == "initialize" + assert sent["excludeDynamicSections"] is True + + +def test_initialize_omits_exclude_dynamic_sections_when_unset(): + """excludeDynamicSections is absent from initialize when not configured.""" + sent = _capture_initialize_request() + assert sent["subtype"] == "initialize" + assert "excludeDynamicSections" not in sent + + def _make_mock_transport(messages, control_requests=None): """Create a mock transport that yields messages and optionally sends control requests. diff --git a/tests/test_types.py b/tests/test_types.py index 47d3584f..fbd07509 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -150,6 +150,23 @@ def test_claude_code_options_with_system_prompt_preset_and_append(self): "append": "Be concise.", } + def test_claude_code_options_with_system_prompt_preset_exclude_dynamic_sections( + self, + ): + """Test Options with system prompt preset and exclude_dynamic_sections.""" + options = ClaudeAgentOptions( + system_prompt={ + "type": "preset", + "preset": "claude_code", + "exclude_dynamic_sections": True, + }, + ) + assert options.system_prompt == { + "type": "preset", + "preset": "claude_code", + "exclude_dynamic_sections": True, + } + def test_claude_code_options_with_system_prompt_file(self): """Test Options with system prompt file.""" options = ClaudeAgentOptions(