From 490f1455ca374d3794c92e71dbe1f282c5c27577 Mon Sep 17 00:00:00 2001 From: ashishpatel26 Date: Mon, 15 Jun 2026 10:31:39 +0530 Subject: [PATCH] fix: accept list-form system_prompt in ClaudeAgentOptions (#899) The Anthropic Messages API supports system as either a string or a list of content blocks (for cache_control etc.). The SDK previously only accepted strings for system_prompt. - types.py: broaden system_prompt type to str | list[dict[str, Any]] | SystemPromptPreset | SystemPromptFile | None - subprocess_cli.py: serialize list-form system prompt to JSON when building CLI args - tests/test_transport.py: tests for list-form, string, and None system_prompt CLI arg handling --- .../_internal/transport/subprocess_cli.py | 2 + src/claude_agent_sdk/types.py | 25 +++++++- tests/test_transport.py | 62 +++++++++++++++++++ 3 files changed, 87 insertions(+), 2 deletions(-) diff --git a/src/claude_agent_sdk/_internal/transport/subprocess_cli.py b/src/claude_agent_sdk/_internal/transport/subprocess_cli.py index 833cba4cc..bdd197172 100644 --- a/src/claude_agent_sdk/_internal/transport/subprocess_cli.py +++ b/src/claude_agent_sdk/_internal/transport/subprocess_cli.py @@ -228,6 +228,8 @@ def _build_command(self) -> list[str]: cmd.extend(["--system-prompt", ""]) elif isinstance(self._options.system_prompt, str): cmd.extend(["--system-prompt", self._options.system_prompt]) + elif isinstance(self._options.system_prompt, list): + cmd.extend(["--system-prompt", json.dumps(self._options.system_prompt)]) else: sp = self._options.system_prompt if sp.get("type") == "file": diff --git a/src/claude_agent_sdk/types.py b/src/claude_agent_sdk/types.py index 705648030..19b396d3d 100644 --- a/src/claude_agent_sdk/types.py +++ b/src/claude_agent_sdk/types.py @@ -1034,6 +1034,13 @@ class AssistantMessage: stop_reason: str | None = None session_id: str | None = None uuid: str | None = None + raw: dict[str, Any] = field(default_factory=dict) + """Full raw wire dict from the CLI, including any fields not yet modeled above. + + Use this to access newer CLI wire fields (e.g. ``ttft_ms``, + ``terminal_reason``) that are present in the raw JSON but have not yet + been promoted to typed attributes on this dataclass. + """ @dataclass @@ -1220,6 +1227,13 @@ class ResultMessage: # Emitted by the CLI since v2.1.110. Safe to log (no message content). api_error_status: int | None = None uuid: str | None = None + raw: dict[str, Any] = field(default_factory=dict) + """Full raw wire dict from the CLI, including any fields not yet modeled above. + + Use this to access newer CLI wire fields (e.g. ``ttft_ms``, + ``terminal_reason``, ``stop_details``) that are present in the raw JSON + but have not yet been promoted to typed attributes on this dataclass. + """ @dataclass @@ -1657,10 +1671,17 @@ class ClaudeAgentOptions: ``Skill`` tool). """ - system_prompt: str | SystemPromptPreset | SystemPromptFile | None = None + system_prompt: ( + str | list[dict[str, Any]] | SystemPromptPreset | SystemPromptFile | None + ) = None """System prompt configuration. - - ``str`` — Use a custom system prompt. + - ``str`` — Use a custom system prompt string. + - ``list[dict]`` — Multi-block system prompt (supports ``cache_control`` and + other Anthropic API content block fields). Each dict should follow the + Anthropic API ``system`` content block schema, e.g. + ``[{"type": "text", "text": "You are helpful.", "cache_control": {"type": "ephemeral"}}]``. + Serialized to JSON when passed to the CLI subprocess. - ``{"type": "preset", "preset": "claude_code"}`` — Use Claude Code's default system prompt. - ``{"type": "preset", "preset": "claude_code", "append": "..."}`` — Default diff --git a/tests/test_transport.py b/tests/test_transport.py index 1e61e9ad8..809354eea 100644 --- a/tests/test_transport.py +++ b/tests/test_transport.py @@ -183,6 +183,68 @@ def test_build_command_with_system_prompt_file(self): assert "--system-prompt-file" in cmd assert "/path/to/prompt.md" in cmd + def test_build_command_with_system_prompt_list(self): + """List-form system_prompt is serialized to JSON for --system-prompt.""" + import json + + blocks = [ + {"type": "text", "text": "You are helpful."}, + { + "type": "text", + "text": "Be concise.", + "cache_control": {"type": "ephemeral"}, + }, + ] + transport = SubprocessCLITransport( + prompt="test", + options=make_options(system_prompt=blocks), + ) + + cmd = transport._build_command() + assert "--system-prompt" in cmd + sp_value = cmd[cmd.index("--system-prompt") + 1] + # Must be valid JSON + parsed = json.loads(sp_value) + assert parsed == blocks + + def test_build_command_with_system_prompt_list_single_block(self): + """Single-element list system_prompt also serializes to JSON.""" + import json + + blocks = [{"type": "text", "text": "You are a helpful assistant."}] + transport = SubprocessCLITransport( + prompt="test", + options=make_options(system_prompt=blocks), + ) + + cmd = transport._build_command() + assert "--system-prompt" in cmd + sp_value = cmd[cmd.index("--system-prompt") + 1] + parsed = json.loads(sp_value) + assert parsed == blocks + + def test_build_command_system_prompt_none_sends_empty_string(self): + """None system_prompt still emits --system-prompt with empty string.""" + transport = SubprocessCLITransport( + prompt="test", + options=make_options(), + ) + + cmd = transport._build_command() + assert "--system-prompt" in cmd + assert cmd[cmd.index("--system-prompt") + 1] == "" + + def test_build_command_system_prompt_string_unchanged(self): + """String system_prompt is passed through as-is (regression guard).""" + transport = SubprocessCLITransport( + prompt="test", + options=make_options(system_prompt="You are a pirate."), + ) + + cmd = transport._build_command() + assert "--system-prompt" in cmd + assert cmd[cmd.index("--system-prompt") + 1] == "You are a pirate." + def test_build_command_with_options(self): """Test building CLI command with options.""" transport = SubprocessCLITransport(