Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## Unreleased

### Features

Check warning on line 6 in CHANGELOG.md

View check run for this annotation

Claude / Claude Code Review

CHANGELOG references issue number instead of PR number

The Unreleased CHANGELOG entry references (#784), which is the GitHub issue this PR closes, but every other entry in the changelog references the PR number. This PR is #797, so the entry should read (#797) to match the established convention.
Comment thread
claude[bot] marked this conversation as resolved.
Outdated
- Add `exclude_dynamic_sections` to `SystemPromptPreset` for cross-user prompt caching (parity with TypeScript SDK) (#784)

## 0.1.56

### Internal/Other Changes
Expand Down
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