Skip to content

Commit df99723

Browse files
authored
Merge pull request #18 from ya-luotao/fix-thinking-flag-and-exclude-dynamic-sections
Fix --thinking flag and add exclude_dynamic_sections
2 parents 4ecb37a + 6156d38 commit df99723

12 files changed

Lines changed: 169 additions & 31 deletions

File tree

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.14.1] - 2026-04-09
9+
10+
### Fixed
11+
- **Thinking configuration**: Use `--thinking adaptive` / `--thinking disabled` CLI flags instead of mapping to `--max-thinking-tokens`. Previously, `ThinkingConfigAdaptive` was mapped to `--max-thinking-tokens 32000` (fixed budget) and `ThinkingConfigDisabled` to `--max-thinking-tokens 0`, which put the CLI into the wrong mode. Only `ThinkingConfigEnabled` now uses `--max-thinking-tokens`. (Parity with [Python SDK #796](https://github.com/anthropics/claude-agent-sdk-python/pull/796))
12+
13+
### Added
14+
- **`exclude_dynamic_sections`** on `SystemPromptPreset`: When set to `true`, the CLI strips per-user dynamic sections (working directory, auto-memory, git status) from the preset system prompt and re-injects them into the first user message. This makes the system prompt byte-identical across users, enabling cross-user prompt-caching hits. Sent via `excludeDynamicSections` in the initialize control message; older CLIs silently ignore it. (Parity with [Python SDK #797](https://github.com/anthropics/claude-agent-sdk-python/pull/797))
15+
816
## [0.14.0] - 2026-04-08 — Python SDK v0.1.51–0.1.56 Parity
917

1018
### Added

README.md

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -724,12 +724,12 @@ For complete examples, see [examples/structured_output_example.rb](examples/stru
724724
Control extended thinking behavior with typed configuration objects. The `thinking` option takes precedence over the deprecated `max_thinking_tokens`.
725725

726726
```ruby
727-
# Adaptive thinking — uses a default budget of 32,000 tokens
727+
# Adaptive thinking — CLI dynamically adjusts budget based on task complexity
728728
options = ClaudeAgentSDK::ClaudeAgentOptions.new(
729729
thinking: ClaudeAgentSDK::ThinkingConfigAdaptive.new
730730
)
731731

732-
# Enabled thinking with custom budget
732+
# Enabled thinking with explicit token budget
733733
options = ClaudeAgentSDK::ClaudeAgentOptions.new(
734734
thinking: ClaudeAgentSDK::ThinkingConfigEnabled.new(budget_tokens: 50_000)
735735
)
@@ -750,6 +750,22 @@ options = ClaudeAgentSDK::ClaudeAgentOptions.new(
750750

751751
> **Note:** When `system_prompt` is `nil` (the default), the SDK passes `--system-prompt ""` to the CLI, which suppresses the default Claude Code system prompt. To use the default system prompt, use a `SystemPromptPreset`.
752752
753+
### Cross-User Prompt Caching
754+
755+
When running a multi-user fleet with shared preset prompts, enable `exclude_dynamic_sections` to make the system prompt byte-identical across users for prompt-caching hits:
756+
757+
```ruby
758+
options = ClaudeAgentSDK::ClaudeAgentOptions.new(
759+
system_prompt: ClaudeAgentSDK::SystemPromptPreset.new(
760+
preset: 'claude_code',
761+
append: '...your shared domain instructions...',
762+
exclude_dynamic_sections: true
763+
)
764+
)
765+
```
766+
767+
When set, the CLI strips per-user dynamic sections (working directory, auto-memory, git status) from the system prompt and re-injects them into the first user message instead. Older CLIs silently ignore this option.
768+
753769
## Budget Control
754770

755771
Use `max_budget_usd` to set a spending cap for your queries:
@@ -1565,9 +1581,9 @@ end
15651581
| `PermissionResultAllow` | Permission callback result to allow tool use |
15661582
| `PermissionResultDeny` | Permission callback result to deny tool use |
15671583
| `AgentDefinition` | Agent definition with description, prompt, tools, model, skills, memory, mcp_servers |
1568-
| `ThinkingConfigAdaptive` | Adaptive thinking mode (32,000 token default budget) |
1584+
| `ThinkingConfigAdaptive` | Adaptive thinking mode (CLI dynamically adjusts budget) |
15691585
| `ThinkingConfigEnabled` | Enabled thinking with explicit `budget_tokens` |
1570-
| `ThinkingConfigDisabled` | Disabled thinking (0 tokens) |
1586+
| `ThinkingConfigDisabled` | Disabled thinking |
15711587
| `SdkMcpTool` | SDK MCP tool definition with name, description, input_schema, handler, annotations |
15721588
| `McpStdioServerConfig` | MCP server config for stdio transport |
15731589
| `McpSSEServerConfig` | MCP server config for SSE transport |

lib/claude_agent_sdk.rb

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -340,14 +340,19 @@ def connect(prompt = nil)
340340
# Convert hooks to internal format
341341
hooks = convert_hooks_to_internal_format(configured_options.hooks) if configured_options.hooks
342342

343+
# Extract exclude_dynamic_sections from preset system prompt for the
344+
# initialize request (older CLIs ignore unknown initialize fields)
345+
exclude_dynamic_sections = extract_exclude_dynamic_sections(configured_options.system_prompt)
346+
343347
# Create Query handler
344348
@query_handler = Query.new(
345349
transport: @transport,
346350
is_streaming_mode: true,
347351
can_use_tool: configured_options.can_use_tool,
348352
hooks: hooks,
349353
sdk_mcp_servers: sdk_mcp_servers,
350-
agents: configured_options.agents
354+
agents: configured_options.agents,
355+
exclude_dynamic_sections: exclude_dynamic_sections
351356
)
352357

353358
# Start query handler and initialize
@@ -527,5 +532,19 @@ def convert_hooks_to_internal_format(hooks)
527532
end
528533
internal_hooks
529534
end
535+
536+
def extract_exclude_dynamic_sections(system_prompt)
537+
if system_prompt.is_a?(SystemPromptPreset)
538+
eds = system_prompt.exclude_dynamic_sections
539+
return eds if [true, false].include?(eds)
540+
elsif system_prompt.is_a?(Hash)
541+
type = system_prompt[:type] || system_prompt['type']
542+
if type == 'preset'
543+
eds = system_prompt.fetch(:exclude_dynamic_sections) { system_prompt['exclude_dynamic_sections'] }
544+
return eds if [true, false].include?(eds)
545+
end
546+
end
547+
nil
548+
end
530549
end
531550
end

lib/claude_agent_sdk/query.rb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,15 @@ class Query
2525
STREAM_CLOSE_TIMEOUT_ENV_VAR = 'CLAUDE_CODE_STREAM_CLOSE_TIMEOUT'
2626
DEFAULT_STREAM_CLOSE_TIMEOUT_SECONDS = 60.0
2727

28-
def initialize(transport:, is_streaming_mode:, can_use_tool: nil, hooks: nil, sdk_mcp_servers: nil, agents: nil)
28+
def initialize(transport:, is_streaming_mode:, can_use_tool: nil, hooks: nil, sdk_mcp_servers: nil, agents: nil,
29+
exclude_dynamic_sections: nil)
2930
@transport = transport
3031
@is_streaming_mode = is_streaming_mode
3132
@can_use_tool = can_use_tool
3233
@hooks = hooks || {}
3334
@sdk_mcp_servers = sdk_mcp_servers || {}
3435
@agents = agents
36+
@exclude_dynamic_sections = exclude_dynamic_sections
3537

3638
# Control protocol state
3739
@pending_control_responses = {}
@@ -109,6 +111,7 @@ def initialize_protocol
109111
hooks: hooks_config.empty? ? nil : hooks_config,
110112
agents: agents_dict
111113
}
114+
request[:excludeDynamicSections] = @exclude_dynamic_sections unless @exclude_dynamic_sections.nil?
112115

113116
response = send_control_request(request)
114117
@initialized = true

lib/claude_agent_sdk/subprocess_cli_transport.rb

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -120,8 +120,7 @@ def build_command
120120
end
121121

122122
# Thinking configuration (takes precedence over deprecated max_thinking_tokens)
123-
thinking_tokens = resolve_thinking_tokens
124-
cmd.concat(['--max-thinking-tokens', thinking_tokens.to_s]) unless thinking_tokens.nil?
123+
build_thinking_args(cmd)
125124

126125
# Effort level (valid values: low, medium, high, max)
127126
cmd.concat(['--effort', @options.effort.to_s]) if @options.effort
@@ -494,8 +493,6 @@ def ready?
494493
@ready
495494
end
496495

497-
DEFAULT_ADAPTIVE_THINKING_TOKENS = 32_000
498-
499496
private
500497

501498
def build_settings_args(cmd)
@@ -599,18 +596,18 @@ def load_settings_file(path)
599596
JSON.parse(File.read(path))
600597
end
601598

602-
def resolve_thinking_tokens
599+
def build_thinking_args(cmd)
603600
if @options.thinking
604601
case @options.thinking
605602
when ThinkingConfigAdaptive
606-
DEFAULT_ADAPTIVE_THINKING_TOKENS
603+
cmd.concat(['--thinking', 'adaptive'])
607604
when ThinkingConfigEnabled
608-
@options.thinking.budget_tokens
605+
cmd.concat(['--max-thinking-tokens', @options.thinking.budget_tokens.to_s])
609606
when ThinkingConfigDisabled
610-
0
607+
cmd.concat(['--thinking', 'disabled'])
611608
end
612609
elsif @options.max_thinking_tokens
613-
@options.max_thinking_tokens
610+
cmd.concat(['--max-thinking-tokens', @options.max_thinking_tokens.to_s])
614611
end
615612
end
616613
end

lib/claude_agent_sdk/types.rb

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1735,17 +1735,19 @@ def to_h
17351735

17361736
# System prompt preset configuration
17371737
class SystemPromptPreset
1738-
attr_accessor :type, :preset, :append
1738+
attr_accessor :type, :preset, :append, :exclude_dynamic_sections
17391739

1740-
def initialize(preset:, append: nil)
1740+
def initialize(preset:, append: nil, exclude_dynamic_sections: nil)
17411741
@type = 'preset'
17421742
@preset = preset
17431743
@append = append
1744+
@exclude_dynamic_sections = exclude_dynamic_sections
17441745
end
17451746

17461747
def to_h
17471748
result = { type: @type, preset: @preset }
17481749
result[:append] = @append if @append
1750+
result[:exclude_dynamic_sections] = @exclude_dynamic_sections unless @exclude_dynamic_sections.nil?
17491751
result
17501752
end
17511753
end

lib/claude_agent_sdk/version.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# frozen_string_literal: true
22

33
module ClaudeAgentSDK
4-
VERSION = '0.14.0'
4+
VERSION = '0.14.1'
55
end

plugins/claude-agent-ruby/skills/claude-agent-ruby/references/options.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,14 @@ Notes:
2222

2323
## Core knobs
2424

25-
- `system_prompt`: Set an overall instruction as a string, or use `ClaudeAgentSDK::SystemPromptPreset.new(preset: 'claude_code', append: '...')` to extend a preset prompt.
25+
- `system_prompt`: Set an overall instruction as a string, use `ClaudeAgentSDK::SystemPromptPreset.new(preset: 'claude_code', append: '...', exclude_dynamic_sections: true)` to extend a preset (with optional cross-user caching), or use `ClaudeAgentSDK::SystemPromptFile.new(path: '/path/to/prompt.txt')` to load from a file.
2626
- `model`: Select the model.
2727
- `fallback_model`: Use when the primary model is unavailable.
2828
- `max_turns`: Cap the number of turns.
2929
- `max_budget_usd`: Cap total spend (USD).
3030
- `include_partial_messages`: Include partial assistant messages in the stream when supported.
3131
- `cwd`: Run Claude Code in a specific working directory.
32-
- `max_thinking_tokens`: Stored for API parity, but not currently passed through to Claude CLI.
32+
- `max_thinking_tokens`: Deprecated — use `thinking:` instead (`ThinkingConfigAdaptive`, `ThinkingConfigEnabled`, or `ThinkingConfigDisabled`). Falls back to `--max-thinking-tokens` when `thinking` is unset.
3333

3434
## Tools and permissions
3535

skills/references/options.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ Notes:
2222

2323
## Core knobs
2424

25-
- `system_prompt`: Set an overall instruction as a string, use `ClaudeAgentSDK::SystemPromptPreset.new(preset: 'claude_code', append: '...')` to extend a preset, or use `ClaudeAgentSDK::SystemPromptFile.new(path: '/path/to/prompt.txt')` to load from a file.
25+
- `system_prompt`: Set an overall instruction as a string, use `ClaudeAgentSDK::SystemPromptPreset.new(preset: 'claude_code', append: '...', exclude_dynamic_sections: true)` to extend a preset (with optional cross-user caching), or use `ClaudeAgentSDK::SystemPromptFile.new(path: '/path/to/prompt.txt')` to load from a file.
2626
- `model`: Select the model.
2727
- `fallback_model`: Use when the primary model is unavailable.
2828
- `max_turns`: Cap the number of turns.
@@ -31,7 +31,7 @@ Notes:
3131
- `session_id`: Specify a custom session ID upfront (string).
3232
- `include_partial_messages`: Include partial assistant messages in the stream when supported.
3333
- `cwd`: Run Claude Code in a specific working directory.
34-
- `max_thinking_tokens`: Stored for API parity, but not currently passed through to Claude CLI.
34+
- `max_thinking_tokens`: Deprecated — use `thinking:` instead (`ThinkingConfigAdaptive`, `ThinkingConfigEnabled`, or `ThinkingConfigDisabled`). Falls back to `--max-thinking-tokens` when `thinking` is unset.
3535

3636
## Tools and permissions
3737

spec/unit/client_spec.rb

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,76 @@ def build_transport_class(&on_initialize)
276276
end
277277
end
278278

279+
context 'with exclude_dynamic_sections' do
280+
let(:transport) { instance_double(ClaudeAgentSDK::SubprocessCLITransport, connect: true, write: nil) }
281+
let(:query_handler) { instance_double(ClaudeAgentSDK::Query, start: true, initialize_protocol: true) }
282+
283+
before do
284+
allow(ClaudeAgentSDK::SubprocessCLITransport).to receive(:new).and_return(transport)
285+
end
286+
287+
it 'passes exclude_dynamic_sections from SystemPromptPreset to Query' do
288+
received_kwargs = nil
289+
allow(ClaudeAgentSDK::Query).to receive(:new) do |**kwargs|
290+
received_kwargs = kwargs
291+
query_handler
292+
end
293+
294+
preset = ClaudeAgentSDK::SystemPromptPreset.new(preset: 'claude_code', exclude_dynamic_sections: true)
295+
options = ClaudeAgentSDK::ClaudeAgentOptions.new(system_prompt: preset)
296+
client = described_class.new(options: options)
297+
client.connect
298+
299+
expect(received_kwargs[:exclude_dynamic_sections]).to eq(true)
300+
end
301+
302+
it 'passes exclude_dynamic_sections from Hash with symbol keys to Query' do
303+
received_kwargs = nil
304+
allow(ClaudeAgentSDK::Query).to receive(:new) do |**kwargs|
305+
received_kwargs = kwargs
306+
query_handler
307+
end
308+
309+
options = ClaudeAgentSDK::ClaudeAgentOptions.new(
310+
system_prompt: { type: 'preset', preset: 'claude_code', exclude_dynamic_sections: true }
311+
)
312+
client = described_class.new(options: options)
313+
client.connect
314+
315+
expect(received_kwargs[:exclude_dynamic_sections]).to eq(true)
316+
end
317+
318+
it 'handles false correctly from Hash with symbol keys' do
319+
received_kwargs = nil
320+
allow(ClaudeAgentSDK::Query).to receive(:new) do |**kwargs|
321+
received_kwargs = kwargs
322+
query_handler
323+
end
324+
325+
options = ClaudeAgentSDK::ClaudeAgentOptions.new(
326+
system_prompt: { type: 'preset', preset: 'claude_code', exclude_dynamic_sections: false }
327+
)
328+
client = described_class.new(options: options)
329+
client.connect
330+
331+
expect(received_kwargs[:exclude_dynamic_sections]).to eq(false)
332+
end
333+
334+
it 'passes nil when system_prompt is a plain string' do
335+
received_kwargs = nil
336+
allow(ClaudeAgentSDK::Query).to receive(:new) do |**kwargs|
337+
received_kwargs = kwargs
338+
query_handler
339+
end
340+
341+
options = ClaudeAgentSDK::ClaudeAgentOptions.new(system_prompt: 'You are a helper')
342+
client = described_class.new(options: options)
343+
client.connect
344+
345+
expect(received_kwargs[:exclude_dynamic_sections]).to be_nil
346+
end
347+
end
348+
279349
context 'with default configuration' do
280350
after { ClaudeAgentSDK.reset_configuration }
281351

0 commit comments

Comments
 (0)