Skip to content
Open
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
11 changes: 10 additions & 1 deletion src/coder_eval/agents/claude_code_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
# CLI). If this import breaks on an SDK upgrade, the threaded watchdog loses
# its kill target and timeouts will no longer be enforced at the agent layer.
from claude_agent_sdk._internal.transport.subprocess_cli import SubprocessCLITransport
from claude_agent_sdk.types import SystemPromptPreset

from coder_eval.agent import Agent, AgentState
from coder_eval.agents._logging import PrefixedAdapter, log_raw_sdk_event
Expand Down Expand Up @@ -1173,6 +1174,14 @@ def _build_claude_query(
if "ToolSearch" not in disallowed_tools:
disallowed_tools.append("ToolSearch")

# A plain-string system_prompt would REPLACE Claude Code's default system
# prompt, dropping its behavioral guidance (parallel tool-call batching,
# conciseness). Always keep the default via the SDK preset and append the
# configured prompt after it.
system_prompt: SystemPromptPreset | None = None
if self.config.system_prompt is not None:
system_prompt = SystemPromptPreset(type="preset", preset="claude_code", append=self.config.system_prompt)

# as_posix(), not str(): bash on Windows strips backslashes from unquoted
# paths, so a redirect like `> D:\foo\bar` ends up writing to "Dfoobar".
options = ClaudeAgentOptions(
Expand All @@ -1192,7 +1201,7 @@ def _build_claude_query(
# summing per-message values undercounts by 10x+. Without this flag
# StreamEvents are suppressed by the SDK.
include_partial_messages=True,
system_prompt=self.config.system_prompt,
system_prompt=system_prompt,
setting_sources=self.config.setting_sources if self.config.setting_sources is not None else ["project"],
resume=self._session_id,
settings=json.dumps(self.config.claude_settings)
Expand Down
3 changes: 2 additions & 1 deletion src/coder_eval/models/agent_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,8 @@ class BaseAgentConfig(BaseModel):
system_prompt: str | None = Field(
default=None,
description=(
"Custom system prompt. Replaces the default system prompt. "
"Custom system prompt, appended to the agent's default system prompt "
"(claude-code: the SDK 'claude_code' preset with append). "
"Supports inline text or multi-line YAML strings. "
"Mutually exclusive with system_prompt_file."
),
Expand Down
26 changes: 26 additions & 0 deletions tests/test_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,32 @@ async def test_claude_settings_none_default():
assert captured_options[0].settings is None


@pytest.mark.asyncio
async def test_system_prompt_appends_to_claude_code_preset():
"""system_prompt keeps the Claude Code default prompt and appends via the SDK preset."""
config = parse_agent_config(type=AgentKind.CLAUDE_CODE, system_prompt="You are a coding agent.")
agent = ClaudeCodeAgent(config)

captured_options = await _capture_sdk_options(agent)

assert captured_options[0].system_prompt == {
"type": "preset",
"preset": "claude_code",
"append": "You are a coding agent.",
}


@pytest.mark.asyncio
async def test_system_prompt_none_leaves_sdk_default():
"""No system_prompt -> ClaudeAgentOptions.system_prompt stays None (SDK default prompt)."""
config = parse_agent_config(type=AgentKind.CLAUDE_CODE)
agent = ClaudeCodeAgent(config)

captured_options = await _capture_sdk_options(agent)

assert captured_options[0].system_prompt is None


@pytest.mark.asyncio
async def test_sdk_options_forwarded_to_sdk():
"""An sdk_options key (e.g. effort) is splatted into ClaudeAgentOptions."""
Expand Down
Loading