claude-agent-sdk 0.16.1 → 0.16.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +10 -0
- data/README.md +1 -1
- data/lib/claude_agent_sdk/command_builder.rb +291 -0
- data/lib/claude_agent_sdk/subprocess_cli_transport.rb +12 -244
- data/lib/claude_agent_sdk/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b2c37eb821ab8e4fe91cc69b4d0d683b364617b321d74e642f330c7c560850dd
|
|
4
|
+
data.tar.gz: 62c00c3973e9bfabda1ebf5f95b4ce4ca79bd6dc4c36e8f0bcf0038ede7c588f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: af171dc45b39c86eee1dacd829f062e230b3369225ef394df0df43b9a05d2ecec087a6566dd00cb7a4a01d1af1f3a5677f08f9ae04ecaa9c46d9490c75acaf46
|
|
7
|
+
data.tar.gz: f96ce5a7ee822ef161a541e29fa6677fb28c77c5e316965aa15477a782e087a60b15ea706665488e09f102356f91fa43491be82f526910c2d4afa4f08b3256fd
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.16.3] - 2026-04-23
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- Internal: extracted `SubprocessCLITransport#record_bounded_stderr` helper to deduplicate the recent-stderr ring-buffer append/trim logic shared by `handle_stderr` and `drain_stderr_with_accumulation`, and replaced the inlined `20` cap with a named `RECENT_STDERR_LINES_LIMIT` constant. No public behavior change.
|
|
14
|
+
|
|
15
|
+
## [0.16.2] - 2026-04-23
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
- Extracted `ClaudeAgentSDK::CommandBuilder` from `SubprocessCLITransport`. CLI argv assembly now lives in its own class and can be exercised in isolation — `CommandBuilder.new(cli_path, options).build` returns the argv array without booting a transport. No public behavior change; `SubprocessCLITransport#build_command` still works and now delegates to `CommandBuilder`.
|
|
19
|
+
|
|
10
20
|
## [0.16.1] - 2026-04-21
|
|
11
21
|
|
|
12
22
|
### Fixed
|
data/README.md
CHANGED
|
@@ -108,7 +108,7 @@ Add this line to your application's Gemfile:
|
|
|
108
108
|
gem 'claude-agent-sdk', github: 'ya-luotao/claude-agent-sdk-ruby'
|
|
109
109
|
|
|
110
110
|
# Or use a stable version from RubyGems
|
|
111
|
-
gem 'claude-agent-sdk', '~> 0.16.
|
|
111
|
+
gem 'claude-agent-sdk', '~> 0.16.3'
|
|
112
112
|
```
|
|
113
113
|
|
|
114
114
|
And then execute:
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require_relative "errors"
|
|
5
|
+
require_relative "types"
|
|
6
|
+
|
|
7
|
+
module ClaudeAgentSDK
|
|
8
|
+
# Builds the CLI argv array from a ClaudeAgentOptions instance.
|
|
9
|
+
class CommandBuilder
|
|
10
|
+
EXTRA_ARG_FLAG_REGEXP = /\A[a-z0-9][a-z0-9-]*\z/
|
|
11
|
+
|
|
12
|
+
def initialize(cli_path, options)
|
|
13
|
+
@cli_path = cli_path
|
|
14
|
+
@options = options
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def build
|
|
18
|
+
cmd = [@cli_path, "--output-format", "stream-json", "--verbose"]
|
|
19
|
+
|
|
20
|
+
append_system_prompt(cmd)
|
|
21
|
+
append_tools(cmd)
|
|
22
|
+
append_allowed_tools(cmd)
|
|
23
|
+
append_disallowed_tools(cmd)
|
|
24
|
+
append_max_turns(cmd)
|
|
25
|
+
append_model(cmd)
|
|
26
|
+
append_permission(cmd)
|
|
27
|
+
append_session(cmd)
|
|
28
|
+
append_settings(cmd)
|
|
29
|
+
append_budget(cmd)
|
|
30
|
+
append_thinking(cmd)
|
|
31
|
+
append_effort(cmd)
|
|
32
|
+
append_betas(cmd)
|
|
33
|
+
append_append_allowed_tools(cmd)
|
|
34
|
+
append_output_format(cmd)
|
|
35
|
+
append_additional_dirs(cmd)
|
|
36
|
+
append_mcp_servers(cmd)
|
|
37
|
+
append_boolean_flags(cmd)
|
|
38
|
+
append_plugins(cmd)
|
|
39
|
+
append_setting_sources(cmd)
|
|
40
|
+
append_extra_args(cmd)
|
|
41
|
+
|
|
42
|
+
# Always use streaming mode for bidirectional control protocol.
|
|
43
|
+
# Prompts and agents are sent via stdin (initialize + user messages),
|
|
44
|
+
# which avoids OS ARG_MAX limits for large prompts and agent configurations.
|
|
45
|
+
cmd.push("--input-format", "stream-json")
|
|
46
|
+
|
|
47
|
+
cmd
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def append_system_prompt(cmd)
|
|
53
|
+
case @options.system_prompt
|
|
54
|
+
when nil
|
|
55
|
+
# When nil, pass empty string to ensure predictable behavior without default Claude Code system prompt
|
|
56
|
+
cmd.push("--system-prompt", "")
|
|
57
|
+
when String
|
|
58
|
+
cmd.push("--system-prompt", @options.system_prompt)
|
|
59
|
+
when SystemPromptFile
|
|
60
|
+
cmd.push("--system-prompt-file", @options.system_prompt.path)
|
|
61
|
+
when SystemPromptPreset
|
|
62
|
+
# Preset activates the default Claude Code system prompt by not passing --system-prompt ""
|
|
63
|
+
# Only --append-system-prompt is passed if append text is provided
|
|
64
|
+
cmd.push("--append-system-prompt", @options.system_prompt.append) if @options.system_prompt.append
|
|
65
|
+
when Hash
|
|
66
|
+
append_hash_system_prompt(cmd, @options.system_prompt)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def append_hash_system_prompt(cmd, prompt_hash)
|
|
71
|
+
prompt_type = prompt_hash[:type] || prompt_hash["type"]
|
|
72
|
+
case prompt_type
|
|
73
|
+
when "file"
|
|
74
|
+
prompt_path = prompt_hash[:path] || prompt_hash["path"]
|
|
75
|
+
cmd.push("--system-prompt-file", prompt_path) if prompt_path
|
|
76
|
+
when "preset"
|
|
77
|
+
append = prompt_hash[:append] || prompt_hash["append"]
|
|
78
|
+
# Preset activates the default Claude Code system prompt by not passing --system-prompt ""
|
|
79
|
+
cmd.push("--append-system-prompt", append) if append
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def append_allowed_tools(cmd)
|
|
84
|
+
cmd.push("--allowedTools", @options.allowed_tools.join(",")) unless @options.allowed_tools.empty?
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def append_disallowed_tools(cmd)
|
|
88
|
+
cmd.push("--disallowedTools", @options.disallowed_tools.join(",")) unless @options.disallowed_tools.empty?
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def append_max_turns(cmd)
|
|
92
|
+
cmd.push("--max-turns", @options.max_turns.to_s) if @options.max_turns
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def append_model(cmd)
|
|
96
|
+
cmd.push("--model", @options.model) if @options.model
|
|
97
|
+
cmd.push("--fallback-model", @options.fallback_model) if @options.fallback_model
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def append_permission(cmd)
|
|
101
|
+
cmd.push("--permission-prompt-tool", @options.permission_prompt_tool_name) if @options.permission_prompt_tool_name
|
|
102
|
+
cmd.push("--permission-mode", @options.permission_mode) if @options.permission_mode
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def append_session(cmd)
|
|
106
|
+
cmd.push("--continue") if @options.continue_conversation
|
|
107
|
+
cmd.push("--resume", @options.resume) if @options.resume
|
|
108
|
+
cmd.push("--session-id", @options.session_id) if @options.session_id
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def append_settings(cmd)
|
|
112
|
+
return unless @options.settings || @options.sandbox
|
|
113
|
+
|
|
114
|
+
settings_hash = {}
|
|
115
|
+
settings_is_path = false
|
|
116
|
+
|
|
117
|
+
if @options.settings
|
|
118
|
+
if @options.settings.is_a?(String)
|
|
119
|
+
begin
|
|
120
|
+
settings_hash = JSON.parse(@options.settings)
|
|
121
|
+
rescue JSON::ParserError
|
|
122
|
+
if @options.sandbox
|
|
123
|
+
settings_hash = load_settings_file(@options.settings)
|
|
124
|
+
else
|
|
125
|
+
settings_is_path = true
|
|
126
|
+
cmd.push("--settings", @options.settings)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
elsif @options.settings.is_a?(Hash)
|
|
130
|
+
settings_hash = @options.settings.dup
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
if !settings_is_path && @options.sandbox
|
|
135
|
+
sandbox_hash = @options.sandbox.is_a?(SandboxSettings) ? @options.sandbox.to_h : @options.sandbox
|
|
136
|
+
settings_hash[:sandbox] = sandbox_hash unless sandbox_hash.empty?
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
cmd.push("--settings", JSON.generate(settings_hash)) if !settings_is_path && !settings_hash.empty?
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def append_budget(cmd)
|
|
143
|
+
cmd.push("--max-budget-usd", @options.max_budget_usd.to_s) if @options.max_budget_usd
|
|
144
|
+
|
|
145
|
+
return unless @options.task_budget
|
|
146
|
+
|
|
147
|
+
total = if @options.task_budget.is_a?(TaskBudget)
|
|
148
|
+
@options.task_budget.total
|
|
149
|
+
else
|
|
150
|
+
@options.task_budget[:total] || @options.task_budget["total"]
|
|
151
|
+
end
|
|
152
|
+
cmd.push("--task-budget", total.to_s) if total
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Thinking configuration takes precedence over deprecated max_thinking_tokens
|
|
156
|
+
def append_thinking(cmd)
|
|
157
|
+
if @options.thinking
|
|
158
|
+
case @options.thinking
|
|
159
|
+
when ThinkingConfigAdaptive
|
|
160
|
+
cmd.push("--thinking", "adaptive")
|
|
161
|
+
when ThinkingConfigEnabled
|
|
162
|
+
cmd.push("--max-thinking-tokens", @options.thinking.budget_tokens.to_s)
|
|
163
|
+
when ThinkingConfigDisabled
|
|
164
|
+
cmd.push("--thinking", "disabled")
|
|
165
|
+
end
|
|
166
|
+
elsif @options.max_thinking_tokens
|
|
167
|
+
cmd.push("--max-thinking-tokens", @options.max_thinking_tokens.to_s)
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# The set of supported levels is model-dependent; the CLI falls back to
|
|
172
|
+
# the highest supported level at or below the one requested
|
|
173
|
+
# (e.g. `xhigh` → `high` on Opus 4.6).
|
|
174
|
+
def append_effort(cmd)
|
|
175
|
+
cmd.push("--effort", @options.effort.to_s) if @options.effort
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def append_betas(cmd)
|
|
179
|
+
return unless @options.betas && !@options.betas.empty?
|
|
180
|
+
|
|
181
|
+
cmd.push("--betas", @options.betas.join(","))
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def append_tools(cmd)
|
|
185
|
+
return if @options.tools.nil?
|
|
186
|
+
|
|
187
|
+
case @options.tools
|
|
188
|
+
when Array
|
|
189
|
+
tools_value = @options.tools.empty? ? "" : @options.tools.join(",")
|
|
190
|
+
cmd.push("--tools", tools_value)
|
|
191
|
+
when ToolsPreset
|
|
192
|
+
cmd.push("--tools", "default")
|
|
193
|
+
when Hash
|
|
194
|
+
if (@options.tools[:type] || @options.tools["type"]) == "preset"
|
|
195
|
+
cmd.push("--tools", "default")
|
|
196
|
+
else
|
|
197
|
+
cmd.push("--tools", JSON.generate(@options.tools))
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def append_append_allowed_tools(cmd)
|
|
203
|
+
return unless @options.append_allowed_tools && !@options.append_allowed_tools.empty?
|
|
204
|
+
|
|
205
|
+
cmd.push("--append-allowed-tools", @options.append_allowed_tools.join(","))
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def append_output_format(cmd)
|
|
209
|
+
return unless @options.output_format
|
|
210
|
+
|
|
211
|
+
schema = if @options.output_format.is_a?(Hash) && @options.output_format[:type] == "json_schema"
|
|
212
|
+
@options.output_format[:schema]
|
|
213
|
+
elsif @options.output_format.is_a?(Hash) && @options.output_format["type"] == "json_schema"
|
|
214
|
+
@options.output_format["schema"]
|
|
215
|
+
else
|
|
216
|
+
@options.output_format
|
|
217
|
+
end
|
|
218
|
+
schema_json = schema.is_a?(String) ? schema : JSON.generate(schema)
|
|
219
|
+
cmd.push("--json-schema", schema_json)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def append_additional_dirs(cmd)
|
|
223
|
+
@options.add_dirs.each { |dir| cmd.push("--add-dir", dir.to_s) }
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def append_mcp_servers(cmd)
|
|
227
|
+
return unless @options.mcp_servers && !@options.mcp_servers.empty?
|
|
228
|
+
|
|
229
|
+
if @options.mcp_servers.is_a?(Hash)
|
|
230
|
+
servers_for_cli = {}
|
|
231
|
+
@options.mcp_servers.each do |name, config|
|
|
232
|
+
servers_for_cli[name] = if config.is_a?(Hash) && config[:type] == "sdk"
|
|
233
|
+
config.except(:instance)
|
|
234
|
+
else
|
|
235
|
+
config
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
cmd.push("--mcp-config", JSON.generate({ mcpServers: servers_for_cli })) unless servers_for_cli.empty?
|
|
239
|
+
else
|
|
240
|
+
cmd.push("--mcp-config", @options.mcp_servers.to_s)
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# NOTE: agents are sent via the initialize control request (not CLI args)
|
|
245
|
+
# to avoid OS ARG_MAX limits with large agent configurations.
|
|
246
|
+
def append_boolean_flags(cmd)
|
|
247
|
+
cmd.push("--include-partial-messages") if @options.include_partial_messages
|
|
248
|
+
cmd.push("--fork-session") if @options.fork_session
|
|
249
|
+
cmd.push("--bare") if @options.bare
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def append_plugins(cmd)
|
|
253
|
+
return unless @options.plugins && !@options.plugins.empty?
|
|
254
|
+
|
|
255
|
+
@options.plugins.each do |plugin|
|
|
256
|
+
plugin_config = plugin.is_a?(SdkPluginConfig) ? plugin.to_h : plugin
|
|
257
|
+
plugin_type = plugin_config[:type] || plugin_config["type"]
|
|
258
|
+
plugin_path = plugin_config[:path] || plugin_config["path"]
|
|
259
|
+
|
|
260
|
+
raise ArgumentError, "Unsupported plugin type: #{plugin_type.inspect}" unless %w[local plugin].include?(plugin_type)
|
|
261
|
+
next unless plugin_path
|
|
262
|
+
|
|
263
|
+
cmd.push("--plugin-dir", plugin_path)
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def append_setting_sources(cmd)
|
|
268
|
+
return unless @options.setting_sources
|
|
269
|
+
|
|
270
|
+
cmd.push("--setting-sources", @options.setting_sources.join(","))
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def append_extra_args(cmd)
|
|
274
|
+
@options.extra_args.each do |flag, value|
|
|
275
|
+
raise ArgumentError, "Invalid extra_args flag name: #{flag.inspect} (expected lowercase kebab-case)" unless EXTRA_ARG_FLAG_REGEXP.match?(flag)
|
|
276
|
+
|
|
277
|
+
if value.nil?
|
|
278
|
+
cmd.push("--#{flag}")
|
|
279
|
+
else
|
|
280
|
+
cmd.push("--#{flag}", value.to_s)
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def load_settings_file(path)
|
|
286
|
+
raise CLIConnectionError, "Settings file not found: #{path}" unless File.file?(path)
|
|
287
|
+
|
|
288
|
+
JSON.parse(File.read(path))
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
end
|
|
@@ -6,13 +6,14 @@ require 'timeout'
|
|
|
6
6
|
require_relative 'transport'
|
|
7
7
|
require_relative 'errors'
|
|
8
8
|
require_relative 'version'
|
|
9
|
+
require_relative 'command_builder'
|
|
9
10
|
|
|
10
11
|
module ClaudeAgentSDK
|
|
11
12
|
# Subprocess transport using Claude Code CLI
|
|
12
13
|
class SubprocessCLITransport < Transport
|
|
13
14
|
DEFAULT_MAX_BUFFER_SIZE = 1024 * 1024 # 1MB buffer limit
|
|
14
15
|
MINIMUM_CLAUDE_CODE_VERSION = '2.0.0'
|
|
15
|
-
|
|
16
|
+
RECENT_STDERR_LINES_LIMIT = 20
|
|
16
17
|
|
|
17
18
|
def initialize(options_or_prompt = nil, options = nil)
|
|
18
19
|
# Support both new single-arg form and legacy two-arg form
|
|
@@ -67,126 +68,7 @@ module ClaudeAgentSDK
|
|
|
67
68
|
end
|
|
68
69
|
|
|
69
70
|
def build_command
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
# System prompt handling
|
|
73
|
-
# When nil, pass empty string to ensure predictable behavior without default Claude Code system prompt
|
|
74
|
-
if @options.system_prompt.nil?
|
|
75
|
-
cmd.concat(['--system-prompt', ''])
|
|
76
|
-
elsif @options.system_prompt.is_a?(String)
|
|
77
|
-
cmd.concat(['--system-prompt', @options.system_prompt])
|
|
78
|
-
elsif @options.system_prompt.is_a?(SystemPromptFile)
|
|
79
|
-
cmd.concat(['--system-prompt-file', @options.system_prompt.path])
|
|
80
|
-
elsif @options.system_prompt.is_a?(SystemPromptPreset)
|
|
81
|
-
# Preset activates the default Claude Code system prompt by not passing --system-prompt ""
|
|
82
|
-
# Only --append-system-prompt is passed if append text is provided
|
|
83
|
-
cmd.concat(['--append-system-prompt', @options.system_prompt.append]) if @options.system_prompt.append
|
|
84
|
-
elsif @options.system_prompt.is_a?(Hash)
|
|
85
|
-
prompt_type = @options.system_prompt[:type] || @options.system_prompt['type']
|
|
86
|
-
if prompt_type == 'file'
|
|
87
|
-
prompt_path = @options.system_prompt[:path] || @options.system_prompt['path']
|
|
88
|
-
cmd.concat(['--system-prompt-file', prompt_path]) if prompt_path
|
|
89
|
-
elsif prompt_type == 'preset'
|
|
90
|
-
append = @options.system_prompt[:append] || @options.system_prompt['append']
|
|
91
|
-
# Preset activates the default Claude Code system prompt by not passing --system-prompt ""
|
|
92
|
-
cmd.concat(['--append-system-prompt', append]) if append
|
|
93
|
-
end
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
cmd.concat(['--allowedTools', @options.allowed_tools.join(',')]) unless @options.allowed_tools.empty?
|
|
97
|
-
cmd.concat(['--max-turns', @options.max_turns.to_s]) if @options.max_turns
|
|
98
|
-
cmd.concat(['--disallowedTools', @options.disallowed_tools.join(',')]) unless @options.disallowed_tools.empty?
|
|
99
|
-
cmd.concat(['--model', @options.model]) if @options.model
|
|
100
|
-
cmd.concat(['--fallback-model', @options.fallback_model]) if @options.fallback_model
|
|
101
|
-
cmd.concat(['--permission-prompt-tool', @options.permission_prompt_tool_name]) if @options.permission_prompt_tool_name
|
|
102
|
-
cmd.concat(['--permission-mode', @options.permission_mode]) if @options.permission_mode
|
|
103
|
-
cmd << '--continue' if @options.continue_conversation
|
|
104
|
-
cmd.concat(['--resume', @options.resume]) if @options.resume
|
|
105
|
-
cmd.concat(['--session-id', @options.session_id]) if @options.session_id
|
|
106
|
-
|
|
107
|
-
# Settings handling with sandbox merge
|
|
108
|
-
build_settings_args(cmd)
|
|
109
|
-
|
|
110
|
-
# Budget limit option
|
|
111
|
-
cmd.concat(['--max-budget-usd', @options.max_budget_usd.to_s]) if @options.max_budget_usd
|
|
112
|
-
|
|
113
|
-
# Task budget (API-side token budget)
|
|
114
|
-
if @options.task_budget
|
|
115
|
-
total = if @options.task_budget.is_a?(TaskBudget)
|
|
116
|
-
@options.task_budget.total
|
|
117
|
-
else
|
|
118
|
-
@options.task_budget[:total] || @options.task_budget['total']
|
|
119
|
-
end
|
|
120
|
-
cmd.concat(['--task-budget', total.to_s]) if total
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
# Thinking configuration (takes precedence over deprecated max_thinking_tokens)
|
|
124
|
-
build_thinking_args(cmd)
|
|
125
|
-
|
|
126
|
-
# Effort level — see ClaudeAgentSDK::EFFORT_LEVELS. The set of supported
|
|
127
|
-
# levels is model-dependent; the CLI falls back to the highest supported
|
|
128
|
-
# level at or below the one requested (e.g. `xhigh` → `high` on Opus 4.6).
|
|
129
|
-
cmd.concat(['--effort', @options.effort.to_s]) if @options.effort
|
|
130
|
-
|
|
131
|
-
# Betas option for enabling experimental features
|
|
132
|
-
if @options.betas && !@options.betas.empty?
|
|
133
|
-
cmd.concat(['--betas', @options.betas.join(',')])
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
# Tools option for base tools selection
|
|
137
|
-
build_tools_args(cmd)
|
|
138
|
-
|
|
139
|
-
# Append allowed tools option
|
|
140
|
-
if @options.append_allowed_tools && !@options.append_allowed_tools.empty?
|
|
141
|
-
cmd.concat(['--append-allowed-tools', @options.append_allowed_tools.join(',')])
|
|
142
|
-
end
|
|
143
|
-
|
|
144
|
-
# JSON schema for structured output
|
|
145
|
-
build_output_format_args(cmd)
|
|
146
|
-
|
|
147
|
-
# Add directories
|
|
148
|
-
@options.add_dirs.each do |dir|
|
|
149
|
-
cmd.concat(['--add-dir', dir.to_s])
|
|
150
|
-
end
|
|
151
|
-
|
|
152
|
-
# MCP servers
|
|
153
|
-
build_mcp_servers_args(cmd)
|
|
154
|
-
|
|
155
|
-
cmd << '--include-partial-messages' if @options.include_partial_messages
|
|
156
|
-
cmd << '--fork-session' if @options.fork_session
|
|
157
|
-
cmd << '--bare' if @options.bare
|
|
158
|
-
|
|
159
|
-
# Note: agents are now sent via the initialize control request (not CLI args)
|
|
160
|
-
# to avoid OS ARG_MAX limits with large agent configurations.
|
|
161
|
-
|
|
162
|
-
# Plugins
|
|
163
|
-
build_plugins_args(cmd)
|
|
164
|
-
|
|
165
|
-
# Setting sources
|
|
166
|
-
if @options.setting_sources
|
|
167
|
-
cmd.concat(['--setting-sources', @options.setting_sources.join(',')])
|
|
168
|
-
end
|
|
169
|
-
|
|
170
|
-
# Extra args
|
|
171
|
-
@options.extra_args.each do |flag, value|
|
|
172
|
-
flag_str = flag.to_s
|
|
173
|
-
unless EXTRA_ARG_FLAG_RE.match?(flag_str)
|
|
174
|
-
raise ArgumentError, "Invalid extra_args flag name: #{flag.inspect} (expected lowercase kebab-case)"
|
|
175
|
-
end
|
|
176
|
-
|
|
177
|
-
if value.nil?
|
|
178
|
-
cmd << "--#{flag_str}"
|
|
179
|
-
else
|
|
180
|
-
cmd.concat(["--#{flag_str}", value.to_s])
|
|
181
|
-
end
|
|
182
|
-
end
|
|
183
|
-
|
|
184
|
-
# Always use streaming mode for bidirectional control protocol.
|
|
185
|
-
# Prompts and agents are sent via stdin (initialize + user messages),
|
|
186
|
-
# which avoids OS ARG_MAX limits for large prompts and agent configurations.
|
|
187
|
-
cmd.concat(['--input-format', 'stream-json'])
|
|
188
|
-
|
|
189
|
-
cmd
|
|
71
|
+
CommandBuilder.new(@cli_path, @options).build
|
|
190
72
|
end
|
|
191
73
|
|
|
192
74
|
def connect
|
|
@@ -264,11 +146,7 @@ module ClaudeAgentSDK
|
|
|
264
146
|
line_str = line.chomp
|
|
265
147
|
next if line_str.empty?
|
|
266
148
|
|
|
267
|
-
|
|
268
|
-
@recent_stderr_mutex.synchronize do
|
|
269
|
-
@recent_stderr << line_str
|
|
270
|
-
@recent_stderr.shift if @recent_stderr.size > 20
|
|
271
|
-
end
|
|
149
|
+
record_bounded_stderr(line_str)
|
|
272
150
|
|
|
273
151
|
# Call stderr callback if provided
|
|
274
152
|
@options.stderr&.call(line_str)
|
|
@@ -293,10 +171,7 @@ module ClaudeAgentSDK
|
|
|
293
171
|
line_str = line.chomp
|
|
294
172
|
next if line_str.empty?
|
|
295
173
|
|
|
296
|
-
|
|
297
|
-
@recent_stderr << line_str
|
|
298
|
-
@recent_stderr.shift if @recent_stderr.size > 20
|
|
299
|
-
end
|
|
174
|
+
record_bounded_stderr(line_str)
|
|
300
175
|
end
|
|
301
176
|
end
|
|
302
177
|
|
|
@@ -401,7 +276,6 @@ module ClaudeAgentSDK
|
|
|
401
276
|
def write(data)
|
|
402
277
|
raise CLIConnectionError, 'ProcessTransport is not ready for writing' unless @ready && @stdin
|
|
403
278
|
raise CLIConnectionError, "Cannot write to terminated process" if @process && !@process.alive?
|
|
404
|
-
|
|
405
279
|
raise CLIConnectionError, "Cannot write to process that exited with error: #{@exit_error}" if @exit_error
|
|
406
280
|
|
|
407
281
|
begin
|
|
@@ -517,119 +391,13 @@ module ClaudeAgentSDK
|
|
|
517
391
|
|
|
518
392
|
private
|
|
519
393
|
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
if @options.settings.is_a?(String)
|
|
528
|
-
begin
|
|
529
|
-
settings_hash = JSON.parse(@options.settings)
|
|
530
|
-
rescue JSON::ParserError
|
|
531
|
-
if @options.sandbox
|
|
532
|
-
settings_hash = load_settings_file(@options.settings)
|
|
533
|
-
else
|
|
534
|
-
settings_is_path = true
|
|
535
|
-
cmd.concat(['--settings', @options.settings])
|
|
536
|
-
end
|
|
537
|
-
end
|
|
538
|
-
elsif @options.settings.is_a?(Hash)
|
|
539
|
-
settings_hash = @options.settings.dup
|
|
540
|
-
end
|
|
541
|
-
end
|
|
542
|
-
|
|
543
|
-
if !settings_is_path && @options.sandbox
|
|
544
|
-
sandbox_hash = @options.sandbox.is_a?(SandboxSettings) ? @options.sandbox.to_h : @options.sandbox
|
|
545
|
-
settings_hash[:sandbox] = sandbox_hash unless sandbox_hash.empty?
|
|
546
|
-
end
|
|
547
|
-
|
|
548
|
-
cmd.concat(['--settings', JSON.generate(settings_hash)]) if !settings_is_path && !settings_hash.empty?
|
|
549
|
-
end
|
|
550
|
-
|
|
551
|
-
def build_tools_args(cmd)
|
|
552
|
-
return if @options.tools.nil?
|
|
553
|
-
|
|
554
|
-
if @options.tools.is_a?(Array)
|
|
555
|
-
tools_value = @options.tools.empty? ? '' : @options.tools.join(',')
|
|
556
|
-
cmd.concat(['--tools', tools_value])
|
|
557
|
-
elsif @options.tools.is_a?(ToolsPreset)
|
|
558
|
-
cmd.concat(['--tools', 'default'])
|
|
559
|
-
elsif @options.tools.is_a?(Hash)
|
|
560
|
-
if (@options.tools[:type] || @options.tools['type']) == 'preset'
|
|
561
|
-
cmd.concat(['--tools', 'default'])
|
|
562
|
-
else
|
|
563
|
-
cmd.concat(['--tools', JSON.generate(@options.tools)])
|
|
564
|
-
end
|
|
565
|
-
end
|
|
566
|
-
end
|
|
567
|
-
|
|
568
|
-
def build_output_format_args(cmd)
|
|
569
|
-
return unless @options.output_format
|
|
570
|
-
|
|
571
|
-
schema = if @options.output_format.is_a?(Hash) && @options.output_format[:type] == 'json_schema'
|
|
572
|
-
@options.output_format[:schema]
|
|
573
|
-
elsif @options.output_format.is_a?(Hash) && @options.output_format['type'] == 'json_schema'
|
|
574
|
-
@options.output_format['schema']
|
|
575
|
-
else
|
|
576
|
-
@options.output_format
|
|
577
|
-
end
|
|
578
|
-
schema_json = schema.is_a?(String) ? schema : JSON.generate(schema)
|
|
579
|
-
cmd.concat(['--json-schema', schema_json])
|
|
580
|
-
end
|
|
581
|
-
|
|
582
|
-
def build_mcp_servers_args(cmd)
|
|
583
|
-
return unless @options.mcp_servers && !@options.mcp_servers.empty?
|
|
584
|
-
|
|
585
|
-
if @options.mcp_servers.is_a?(Hash)
|
|
586
|
-
servers_for_cli = {}
|
|
587
|
-
@options.mcp_servers.each do |name, config|
|
|
588
|
-
servers_for_cli[name] = if config.is_a?(Hash) && config[:type] == 'sdk'
|
|
589
|
-
config.reject { |k, _| k == :instance }
|
|
590
|
-
else
|
|
591
|
-
config
|
|
592
|
-
end
|
|
593
|
-
end
|
|
594
|
-
cmd.concat(['--mcp-config', JSON.generate({ mcpServers: servers_for_cli })]) unless servers_for_cli.empty?
|
|
595
|
-
else
|
|
596
|
-
cmd.concat(['--mcp-config', @options.mcp_servers.to_s])
|
|
597
|
-
end
|
|
598
|
-
end
|
|
599
|
-
|
|
600
|
-
def build_plugins_args(cmd)
|
|
601
|
-
return unless @options.plugins && !@options.plugins.empty?
|
|
602
|
-
|
|
603
|
-
@options.plugins.each do |plugin|
|
|
604
|
-
plugin_config = plugin.is_a?(SdkPluginConfig) ? plugin.to_h : plugin
|
|
605
|
-
plugin_type = plugin_config[:type] || plugin_config['type']
|
|
606
|
-
plugin_path = plugin_config[:path] || plugin_config['path']
|
|
607
|
-
|
|
608
|
-
raise ArgumentError, "Unsupported plugin type: #{plugin_type.inspect}" unless %w[local plugin].include?(plugin_type)
|
|
609
|
-
next unless plugin_path
|
|
610
|
-
|
|
611
|
-
cmd.concat(['--plugin-dir', plugin_path])
|
|
612
|
-
end
|
|
613
|
-
end
|
|
614
|
-
|
|
615
|
-
def load_settings_file(path)
|
|
616
|
-
raise CLIConnectionError, "Settings file not found: #{path}" unless File.file?(path)
|
|
617
|
-
|
|
618
|
-
JSON.parse(File.read(path))
|
|
619
|
-
end
|
|
620
|
-
|
|
621
|
-
def build_thinking_args(cmd)
|
|
622
|
-
if @options.thinking
|
|
623
|
-
case @options.thinking
|
|
624
|
-
when ThinkingConfigAdaptive
|
|
625
|
-
cmd.concat(['--thinking', 'adaptive'])
|
|
626
|
-
when ThinkingConfigEnabled
|
|
627
|
-
cmd.concat(['--max-thinking-tokens', @options.thinking.budget_tokens.to_s])
|
|
628
|
-
when ThinkingConfigDisabled
|
|
629
|
-
cmd.concat(['--thinking', 'disabled'])
|
|
630
|
-
end
|
|
631
|
-
elsif @options.max_thinking_tokens
|
|
632
|
-
cmd.concat(['--max-thinking-tokens', @options.max_thinking_tokens.to_s])
|
|
394
|
+
# Append a stderr line to the recent-stderr ring, dropping the oldest
|
|
395
|
+
# entry once the buffer exceeds RECENT_STDERR_LINES_LIMIT. Used to surface the
|
|
396
|
+
# last few lines in ProcessError when the CLI exits non-zero.
|
|
397
|
+
def record_bounded_stderr(line)
|
|
398
|
+
@recent_stderr_mutex.synchronize do
|
|
399
|
+
@recent_stderr << line
|
|
400
|
+
@recent_stderr.shift if @recent_stderr.size > RECENT_STDERR_LINES_LIMIT
|
|
633
401
|
end
|
|
634
402
|
end
|
|
635
403
|
end
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: claude-agent-sdk
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.16.
|
|
4
|
+
version: 0.16.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Community Contributors
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-04-
|
|
10
|
+
date: 2026-04-23 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: async
|
|
@@ -104,6 +104,7 @@ files:
|
|
|
104
104
|
- LICENSE
|
|
105
105
|
- README.md
|
|
106
106
|
- lib/claude_agent_sdk.rb
|
|
107
|
+
- lib/claude_agent_sdk/command_builder.rb
|
|
107
108
|
- lib/claude_agent_sdk/configuration.rb
|
|
108
109
|
- lib/claude_agent_sdk/errors.rb
|
|
109
110
|
- lib/claude_agent_sdk/fiber_boundary.rb
|