Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/claude_agent_sdk/_internal/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
6 changes: 6 additions & 0 deletions src/claude_agent_sdk/_internal/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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
Expand All @@ -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] = {}
Expand Down Expand Up @@ -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(
Expand Down
10 changes: 10 additions & 0 deletions src/claude_agent_sdk/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Expand Down
12 changes: 12 additions & 0 deletions src/claude_agent_sdk/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
34 changes: 34 additions & 0 deletions tests/test_query.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
17 changes: 17 additions & 0 deletions tests/test_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Loading