claude-agent-sdk 0.6.2 → 0.7.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8044b53b9c1c89e75c789b94a1ca000bb4a4a6e94d380bef3ded209585517b29
4
- data.tar.gz: a3effbef7a3cbf1faed47cecb6f69df37e17266eabae7e56a556855b85707c4f
3
+ metadata.gz: de5807b36fd822ee89e4a793a536b4ba8804c168259d17cc1a03974425f128ea
4
+ data.tar.gz: c40d5dad151728b8b2e131ad96da616bd03b4972f6d5807d5375ea24d499ceb6
5
5
  SHA512:
6
- metadata.gz: 3c9360caa6e3fc1ffa7d359e4d2195aecf003e1fc37655bfe15355efc2dce3cfd896f30a0b7bdc504a88d90056bab05b7ad2a90c33f0ad03867abcaf0ccb16a7
7
- data.tar.gz: 68059199d80d3f10ff03b18b6fbb505348070f8a1a53d8d00cd56550fdfe7c337a4b53d39d7e41d0238c954dd2106656c6f8eaf6e39b5ac516278ac4c7c57ff4
6
+ metadata.gz: f34dc590db10f0f4ebe1360a5318617fd0b7c6299fcf83d45438d1bb1a9a106734e06268f7d4361bbdf575d660afa63b97586c56235fa9d617a6b891a0e1e158
7
+ data.tar.gz: e6548661fbcd10c96b5b64b4efc6676d5ad47f28435a23e4b8045a590df107bedea2ee8d089a230c32aaa84dafdbb0239bbeffb1b7b7aba255196d2ded16903e
data/CHANGELOG.md CHANGED
@@ -5,6 +5,49 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.7.0] - 2026-02-20
9
+
10
+ ### Added
11
+
12
+ #### Thinking Configuration
13
+ - `ThinkingConfigAdaptive`, `ThinkingConfigEnabled`, `ThinkingConfigDisabled` classes for structured thinking control
14
+ - `thinking` option on `ClaudeAgentOptions` — takes precedence over deprecated `max_thinking_tokens`
15
+ - `ThinkingConfigAdaptive` → 32,000 token default budget
16
+ - `ThinkingConfigEnabled(budget_tokens:)` → explicit budget
17
+ - `ThinkingConfigDisabled` → 0 tokens (thinking off)
18
+ - `effort` option on `ClaudeAgentOptions` — maps to `--effort` CLI flag (`'low'`, `'medium'`, `'high'`)
19
+
20
+ #### Tool Annotations
21
+ - `annotations` attribute on `SdkMcpTool` for MCP tool annotations (e.g., `readOnlyHint`, `title`)
22
+ - `annotations:` keyword on `ClaudeAgentSDK.create_tool`
23
+ - Annotations included in `SdkMcpServer#list_tools` responses
24
+
25
+ #### Hook Enhancements
26
+ - `tool_use_id` attribute on `PreToolUseHookInput` and `PostToolUseHookInput`
27
+ - `additional_context` attribute on `PreToolUseHookSpecificOutput`
28
+
29
+ #### Message Enhancements
30
+ - `tool_use_result` attribute on `UserMessage` for tool response data
31
+ - `MessageParser` populates `tool_use_result` from CLI output
32
+
33
+ ### Changed
34
+
35
+ #### Architecture: Always Streaming Mode (BREAKING for internal API)
36
+ - **`SubprocessCLITransport`** now always uses `--input-format stream-json` — removed `--print` mode and `--agents` CLI flag
37
+ - **`SubprocessCLITransport.new`** still accepts `(prompt, options)` for compatibility but ignores the prompt argument (always uses streaming mode)
38
+ - **`query()`** now uses the full control protocol internally (Query handler + initialize handshake), matching the Python SDK
39
+ - **Agents** are sent via the `initialize` control request over stdin instead of CLI `--agents` flag, avoiding OS ARG_MAX limits
40
+ - **`query()`** now supports SDK MCP servers and `can_use_tool` callbacks (previously Client-only)
41
+
42
+ #### Empty System Prompt
43
+ - When `system_prompt` is `nil`, passes `--system-prompt ""` to CLI for predictable behavior without the default Claude Code system prompt
44
+
45
+ ## [0.6.3] - 2026-02-18
46
+
47
+ ### Fixed
48
+ - **ProcessError stderr:** Real stderr output is now included in `ProcessError` exceptions (was always "No stderr output captured")
49
+ - **Rate limit events:** Added `RateLimitEvent` type and `rate_limit_event` message parsing support
50
+
8
51
  ## [0.6.2] - 2026-02-17
9
52
 
10
53
  ### Fixed
data/README.md CHANGED
@@ -16,6 +16,7 @@
16
16
  - [Hooks](#hooks)
17
17
  - [Permission Callbacks](#permission-callbacks)
18
18
  - [Structured Output](#structured-output)
19
+ - [Thinking Configuration](#thinking-configuration)
19
20
  - [Budget Control](#budget-control)
20
21
  - [Fallback Model](#fallback-model)
21
22
  - [Beta Features](#beta-features)
@@ -38,7 +39,7 @@ Add this line to your application's Gemfile:
38
39
  gem 'claude-agent-sdk', github: 'ya-luotao/claude-agent-sdk-ruby'
39
40
 
40
41
  # Or use a stable version from RubyGems
41
- gem 'claude-agent-sdk', '~> 0.6.0'
42
+ gem 'claude-agent-sdk', '~> 0.7.0'
42
43
  ```
43
44
 
44
45
  And then execute:
@@ -249,8 +250,11 @@ Custom tools are implemented as in-process MCP servers that run directly within
249
250
  require 'claude_agent_sdk'
250
251
  require 'async'
251
252
 
252
- # Define a tool using create_tool
253
- greet_tool = ClaudeAgentSDK.create_tool('greet', 'Greet a user', { name: :string }) do |args|
253
+ # Define a tool using create_tool (with optional annotations)
254
+ greet_tool = ClaudeAgentSDK.create_tool(
255
+ 'greet', 'Greet a user', { name: :string },
256
+ annotations: { title: 'Greeter', readOnlyHint: true }
257
+ ) do |args|
254
258
  { content: [{ type: 'text', text: "Hello, #{args[:name]}!" }] }
255
259
  end
256
260
 
@@ -392,8 +396,8 @@ A **hook** is a Ruby proc/lambda that the Claude Code *application* (*not* Claud
392
396
 
393
397
  All hook input objects include common fields like `session_id`, `transcript_path`, `cwd`, and `permission_mode`.
394
398
 
395
- - `PreToolUse` → `PreToolUseHookInput` (`tool_name`, `tool_input`)
396
- - `PostToolUse` → `PostToolUseHookInput` (`tool_name`, `tool_input`, `tool_response`)
399
+ - `PreToolUse` → `PreToolUseHookInput` (`tool_name`, `tool_input`, `tool_use_id`)
400
+ - `PostToolUse` → `PostToolUseHookInput` (`tool_name`, `tool_input`, `tool_response`, `tool_use_id`)
397
401
  - `PostToolUseFailure` → `PostToolUseFailureHookInput` (`tool_name`, `tool_input`, `tool_use_id`, `error`, `is_interrupt`)
398
402
  - `UserPromptSubmit` → `UserPromptSubmitHookInput` (`prompt`)
399
403
  - `Stop` → `StopHookInput` (`stop_hook_active`)
@@ -566,6 +570,37 @@ end
566
570
 
567
571
  For complete examples, see [examples/structured_output_example.rb](examples/structured_output_example.rb).
568
572
 
573
+ ## Thinking Configuration
574
+
575
+ Control extended thinking behavior with typed configuration objects. The `thinking` option takes precedence over the deprecated `max_thinking_tokens`.
576
+
577
+ ```ruby
578
+ # Adaptive thinking — uses a default budget of 32,000 tokens
579
+ options = ClaudeAgentSDK::ClaudeAgentOptions.new(
580
+ thinking: ClaudeAgentSDK::ThinkingConfigAdaptive.new
581
+ )
582
+
583
+ # Enabled thinking with custom budget
584
+ options = ClaudeAgentSDK::ClaudeAgentOptions.new(
585
+ thinking: ClaudeAgentSDK::ThinkingConfigEnabled.new(budget_tokens: 50_000)
586
+ )
587
+
588
+ # Explicitly disabled thinking
589
+ options = ClaudeAgentSDK::ClaudeAgentOptions.new(
590
+ thinking: ClaudeAgentSDK::ThinkingConfigDisabled.new
591
+ )
592
+ ```
593
+
594
+ Use the `effort` option to control the model's effort level:
595
+
596
+ ```ruby
597
+ options = ClaudeAgentSDK::ClaudeAgentOptions.new(
598
+ effort: 'high' # 'low', 'medium', or 'high'
599
+ )
600
+ ```
601
+
602
+ > **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`.
603
+
569
604
  ## Budget Control
570
605
 
571
606
  Use `max_budget_usd` to set a spending cap for your queries:
@@ -891,7 +926,8 @@ User input message.
891
926
  class UserMessage
892
927
  attr_accessor :content, # String | Array<ContentBlock>
893
928
  :uuid, # String | nil - Unique ID for rewind support
894
- :parent_tool_use_id # String | nil
929
+ :parent_tool_use_id, # String | nil
930
+ :tool_use_result # Hash | nil - Tool result data when message is a tool response
895
931
  end
896
932
  ```
897
933
 
@@ -1036,6 +1072,10 @@ end
1036
1072
  | `PermissionResultAllow` | Permission callback result to allow tool use |
1037
1073
  | `PermissionResultDeny` | Permission callback result to deny tool use |
1038
1074
  | `AgentDefinition` | Agent definition with description, prompt, tools, model |
1075
+ | `ThinkingConfigAdaptive` | Adaptive thinking mode (32,000 token default budget) |
1076
+ | `ThinkingConfigEnabled` | Enabled thinking with explicit `budget_tokens` |
1077
+ | `ThinkingConfigDisabled` | Disabled thinking (0 tokens) |
1078
+ | `SdkMcpTool` | SDK MCP tool definition with name, description, input_schema, handler, annotations |
1039
1079
  | `McpStdioServerConfig` | MCP server config for stdio transport |
1040
1080
  | `McpSSEServerConfig` | MCP server config for SSE transport |
1041
1081
  | `McpHttpServerConfig` | MCP server config for HTTP transport |
@@ -23,6 +23,8 @@ module ClaudeAgentSDK
23
23
  parse_result_message(data)
24
24
  when 'stream_event'
25
25
  parse_stream_event(data)
26
+ when 'rate_limit_event'
27
+ parse_rate_limit_event(data)
26
28
  else
27
29
  raise MessageParseError.new("Unknown message type: #{message_type}", data: data)
28
30
  end
@@ -33,6 +35,7 @@ module ClaudeAgentSDK
33
35
  def self.parse_user_message(data)
34
36
  parent_tool_use_id = data[:parent_tool_use_id]
35
37
  uuid = data[:uuid] # UUID for rewind support
38
+ tool_use_result = data[:tool_use_result]
36
39
  message_data = data[:message]
37
40
  raise MessageParseError.new("Missing message field in user message", data: data) unless message_data
38
41
 
@@ -41,9 +44,11 @@ module ClaudeAgentSDK
41
44
 
42
45
  if content.is_a?(Array)
43
46
  content_blocks = content.map { |block| parse_content_block(block) }
44
- UserMessage.new(content: content_blocks, uuid: uuid, parent_tool_use_id: parent_tool_use_id)
47
+ UserMessage.new(content: content_blocks, uuid: uuid, parent_tool_use_id: parent_tool_use_id,
48
+ tool_use_result: tool_use_result)
45
49
  else
46
- UserMessage.new(content: content, uuid: uuid, parent_tool_use_id: parent_tool_use_id)
50
+ UserMessage.new(content: content, uuid: uuid, parent_tool_use_id: parent_tool_use_id,
51
+ tool_use_result: tool_use_result)
47
52
  end
48
53
  end
49
54
 
@@ -91,6 +96,10 @@ module ClaudeAgentSDK
91
96
  )
92
97
  end
93
98
 
99
+ def self.parse_rate_limit_event(data)
100
+ RateLimitEvent.new(data: data)
101
+ end
102
+
94
103
  def self.parse_content_block(block)
95
104
  case block[:type]
96
105
  when 'text'
@@ -23,12 +23,13 @@ module ClaudeAgentSDK
23
23
  CONTROL_REQUEST_TIMEOUT_ENV_VAR = 'CLAUDE_AGENT_SDK_CONTROL_REQUEST_TIMEOUT_SECONDS'
24
24
  DEFAULT_CONTROL_REQUEST_TIMEOUT_SECONDS = 1200.0
25
25
 
26
- def initialize(transport:, is_streaming_mode:, can_use_tool: nil, hooks: nil, sdk_mcp_servers: nil)
26
+ def initialize(transport:, is_streaming_mode:, can_use_tool: nil, hooks: nil, sdk_mcp_servers: nil, agents: nil)
27
27
  @transport = transport
28
28
  @is_streaming_mode = is_streaming_mode
29
29
  @can_use_tool = can_use_tool
30
30
  @hooks = hooks || {}
31
31
  @sdk_mcp_servers = sdk_mcp_servers || {}
32
+ @agents = agents
32
33
 
33
34
  # Control protocol state
34
35
  @pending_control_responses = {}
@@ -76,10 +77,24 @@ module ClaudeAgentSDK
76
77
  end
77
78
  end
78
79
 
80
+ # Build agents dict for initialization
81
+ agents_dict = nil
82
+ if @agents
83
+ agents_dict = @agents.transform_values do |agent_def|
84
+ {
85
+ description: agent_def.description,
86
+ prompt: agent_def.prompt,
87
+ tools: agent_def.tools,
88
+ model: agent_def.model
89
+ }.compact
90
+ end
91
+ end
92
+
79
93
  # Send initialize request
80
94
  request = {
81
95
  subtype: 'initialize',
82
- hooks: hooks_config.empty? ? nil : hooks_config
96
+ hooks: hooks_config.empty? ? nil : hooks_config,
97
+ agents: agents_dict
83
98
  }
84
99
 
85
100
  response = send_control_request(request)
@@ -306,6 +321,7 @@ module ClaudeAgentSDK
306
321
  PreToolUseHookInput.new(
307
322
  tool_name: fetch.call(:tool_name),
308
323
  tool_input: fetch.call(:tool_input),
324
+ tool_use_id: fetch.call(:tool_use_id),
309
325
  **base_args
310
326
  )
311
327
  when 'PostToolUse'
@@ -313,6 +329,7 @@ module ClaudeAgentSDK
313
329
  tool_name: fetch.call(:tool_name),
314
330
  tool_input: fetch.call(:tool_input),
315
331
  tool_response: fetch.call(:tool_response),
332
+ tool_use_id: fetch.call(:tool_use_id),
316
333
  **base_args
317
334
  )
318
335
  when 'PostToolUseFailure'
@@ -51,11 +51,13 @@ module ClaudeAgentSDK
51
51
  # @return [Array<Hash>] Array of tool definitions
52
52
  def list_tools
53
53
  @tools.map do |tool|
54
- {
54
+ entry = {
55
55
  name: tool.name,
56
56
  description: tool.description,
57
57
  inputSchema: convert_input_schema(tool.input_schema)
58
58
  }
59
+ entry[:annotations] = tool.annotations if tool.annotations
60
+ entry
59
61
  end
60
62
  end
61
63
 
@@ -387,14 +389,15 @@ module ClaudeAgentSDK
387
389
  # { content: [{ type: 'text', text: "Result: #{result}" }] }
388
390
  # end
389
391
  # end
390
- def self.create_tool(name, description, input_schema, &handler)
392
+ def self.create_tool(name, description, input_schema, annotations: nil, &handler)
391
393
  raise ArgumentError, 'Block required for tool handler' unless handler
392
394
 
393
395
  SdkMcpTool.new(
394
396
  name: name,
395
397
  description: description,
396
398
  input_schema: input_schema,
397
- handler: handler
399
+ handler: handler,
400
+ annotations: annotations
398
401
  )
399
402
  end
400
403
 
@@ -13,15 +13,9 @@ module ClaudeAgentSDK
13
13
  DEFAULT_MAX_BUFFER_SIZE = 1024 * 1024 # 1MB buffer limit
14
14
  MINIMUM_CLAUDE_CODE_VERSION = '2.0.0'
15
15
 
16
- # Prompts larger than this are piped via stdin instead of CLI args
17
- # to avoid Errno::E2BIG (ARG_MAX is typically 1MB on macOS/Linux,
18
- # shared with environment variables).
19
- PROMPT_STDIN_THRESHOLD = 200 * 1024 # 200KB
20
-
21
- def initialize(prompt, options)
22
- @prompt = prompt
23
- @is_streaming = !prompt.is_a?(String)
24
- @options = options
16
+ def initialize(options_or_prompt = nil, options = nil)
17
+ # Support both new single-arg form and legacy two-arg form
18
+ @options = options.nil? ? options_or_prompt : options
25
19
  @cli_path = options.cli_path || find_cli
26
20
  @cwd = options.cwd
27
21
  @process = nil
@@ -32,7 +26,8 @@ module ClaudeAgentSDK
32
26
  @exit_error = nil
33
27
  @max_buffer_size = options.max_buffer_size || DEFAULT_MAX_BUFFER_SIZE
34
28
  @stderr_task = nil
35
- @pipe_prompt_via_stdin = false
29
+ @recent_stderr = []
30
+ @recent_stderr_mutex = Mutex.new
36
31
  end
37
32
 
38
33
  def find_cli
@@ -74,20 +69,21 @@ module ClaudeAgentSDK
74
69
  cmd = [@cli_path, '--output-format', 'stream-json', '--verbose']
75
70
 
76
71
  # System prompt handling
77
- if @options.system_prompt
78
- if @options.system_prompt.is_a?(String)
79
- cmd.concat(['--system-prompt', @options.system_prompt])
80
- elsif @options.system_prompt.is_a?(SystemPromptPreset)
81
- cmd.concat(['--system-prompt-preset', @options.system_prompt.preset]) if @options.system_prompt.preset
82
- cmd.concat(['--append-system-prompt', @options.system_prompt.append]) if @options.system_prompt.append
83
- elsif @options.system_prompt.is_a?(Hash)
84
- prompt_type = @options.system_prompt[:type] || @options.system_prompt['type']
85
- if prompt_type == 'preset'
86
- preset = @options.system_prompt[:preset] || @options.system_prompt['preset']
87
- append = @options.system_prompt[:append] || @options.system_prompt['append']
88
- cmd.concat(['--system-prompt-preset', preset]) if preset
89
- cmd.concat(['--append-system-prompt', append]) if append
90
- end
72
+ # When nil, pass empty string to ensure predictable behavior without default Claude Code system prompt
73
+ if @options.system_prompt.nil?
74
+ cmd.concat(['--system-prompt', ''])
75
+ elsif @options.system_prompt.is_a?(String)
76
+ cmd.concat(['--system-prompt', @options.system_prompt])
77
+ elsif @options.system_prompt.is_a?(SystemPromptPreset)
78
+ cmd.concat(['--system-prompt-preset', @options.system_prompt.preset]) if @options.system_prompt.preset
79
+ cmd.concat(['--append-system-prompt', @options.system_prompt.append]) if @options.system_prompt.append
80
+ elsif @options.system_prompt.is_a?(Hash)
81
+ prompt_type = @options.system_prompt[:type] || @options.system_prompt['type']
82
+ if prompt_type == 'preset'
83
+ preset = @options.system_prompt[:preset] || @options.system_prompt['preset']
84
+ append = @options.system_prompt[:append] || @options.system_prompt['append']
85
+ cmd.concat(['--system-prompt-preset', preset]) if preset
86
+ cmd.concat(['--append-system-prompt', append]) if append
91
87
  end
92
88
  end
93
89
 
@@ -145,7 +141,13 @@ module ClaudeAgentSDK
145
141
 
146
142
  # Budget limit option
147
143
  cmd.concat(['--max-budget-usd', @options.max_budget_usd.to_s]) if @options.max_budget_usd
148
- # Note: max_thinking_tokens is stored in options but not yet supported by Claude CLI
144
+
145
+ # Thinking configuration (takes precedence over deprecated max_thinking_tokens)
146
+ thinking_tokens = resolve_thinking_tokens
147
+ cmd.concat(['--max-thinking-tokens', thinking_tokens.to_s]) unless thinking_tokens.nil?
148
+
149
+ # Effort level
150
+ cmd.concat(['--effort', @options.effort.to_s]) if @options.effort
149
151
 
150
152
  # Betas option for enabling experimental features
151
153
  if @options.betas && !@options.betas.empty?
@@ -214,18 +216,8 @@ module ClaudeAgentSDK
214
216
  cmd << '--include-partial-messages' if @options.include_partial_messages
215
217
  cmd << '--fork-session' if @options.fork_session
216
218
 
217
- # Agents
218
- if @options.agents
219
- agents_dict = @options.agents.transform_values do |agent_def|
220
- {
221
- description: agent_def.description,
222
- prompt: agent_def.prompt,
223
- tools: agent_def.tools,
224
- model: agent_def.model
225
- }.compact
226
- end
227
- cmd.concat(['--agents', JSON.generate(agents_dict)])
228
- end
219
+ # Note: agents are now sent via the initialize control request (not CLI args)
220
+ # to avoid OS ARG_MAX limits with large agent configurations.
229
221
 
230
222
  # Plugins
231
223
  if @options.plugins && !@options.plugins.empty?
@@ -248,17 +240,10 @@ module ClaudeAgentSDK
248
240
  end
249
241
  end
250
242
 
251
- # Prompt handling
252
- if @is_streaming
253
- cmd.concat(['--input-format', 'stream-json'])
254
- elsif @prompt.to_s.bytesize > PROMPT_STDIN_THRESHOLD
255
- # Large prompts are piped via stdin to avoid OS argument size limits.
256
- # Claude CLI reads from stdin when --print is used without a trailing argument.
257
- cmd << '--print'
258
- @pipe_prompt_via_stdin = true
259
- else
260
- cmd.concat(['--print', '--', @prompt.to_s])
261
- end
243
+ # Always use streaming mode for bidirectional control protocol.
244
+ # Prompts and agents are sent via stdin (initialize + user messages),
245
+ # which avoids OS ARG_MAX limits for large prompts and agent configurations.
246
+ cmd.concat(['--input-format', 'stream-json'])
262
247
 
263
248
  cmd
264
249
  end
@@ -273,9 +258,11 @@ module ClaudeAgentSDK
273
258
  # Build environment
274
259
  # Convert symbol keys to strings for spawn compatibility
275
260
  custom_env = @options.env.transform_keys { |k| k.to_s }
276
- # Strip CLAUDECODE to prevent "nested session" detection when the SDK
277
- # launches Claude Code from within an existing Claude Code terminal
278
- process_env = ENV.to_h.except('CLAUDECODE').merge('CLAUDE_AGENT_SDK_VERSION' => VERSION).merge(custom_env)
261
+ # Explicitly unset CLAUDECODE to prevent "nested session" detection when the SDK
262
+ # launches Claude Code from within an existing Claude Code terminal.
263
+ # NOTE: Must set to nil (not just omit the key) — Ruby's spawn only overlays
264
+ # the env hash on top of the parent environment; a nil value actively unsets.
265
+ process_env = ENV.to_h.merge('CLAUDECODE' => nil, 'CLAUDE_AGENT_SDK_VERSION' => VERSION).merge(custom_env)
279
266
  process_env['CLAUDE_CODE_ENTRYPOINT'] ||= 'sdk-rb'
280
267
  process_env['PWD'] = @cwd.to_s if @cwd
281
268
 
@@ -292,32 +279,24 @@ module ClaudeAgentSDK
292
279
  # Without this, --verbose output fills the OS pipe buffer (~64KB),
293
280
  # the subprocess blocks on write, and all pipes stall → EPIPE.
294
281
  if @stderr
295
- if should_pipe_stderr
282
+ if should_pipe_stderr # rubocop:disable Style/ConditionalAssignment
296
283
  @stderr_task = Thread.new do
297
284
  handle_stderr
298
285
  rescue StandardError
299
286
  # Ignore errors during stderr reading
300
287
  end
301
288
  else
302
- # Silently drain stderr so the subprocess never blocks
289
+ # Silently drain stderr so the subprocess never blocks,
290
+ # but still accumulate recent lines for error reporting.
303
291
  @stderr_task = Thread.new do
304
- @stderr.each_line { |_| } # discard
292
+ drain_stderr_with_accumulation
305
293
  rescue StandardError
306
294
  # Ignore — process may have already exited
307
295
  end
308
296
  end
309
297
  end
310
298
 
311
- # For large prompts, pipe the prompt text to stdin before closing.
312
- # This avoids Errno::E2BIG when the prompt exceeds ARG_MAX.
313
- if @pipe_prompt_via_stdin && @stdin
314
- @stdin.write(@prompt.to_s)
315
- end
316
-
317
- # Close stdin for non-streaming mode
318
- @stdin.close unless @is_streaming
319
- @stdin = nil unless @is_streaming
320
-
299
+ # Always keep stdin open streaming mode uses it for the control protocol
321
300
  @ready = true
322
301
  rescue Errno::ENOENT => e
323
302
  # Check if error is from cwd or CLI
@@ -343,6 +322,12 @@ module ClaudeAgentSDK
343
322
  line_str = line.chomp
344
323
  next if line_str.empty?
345
324
 
325
+ # Accumulate recent lines for inclusion in ProcessError
326
+ @recent_stderr_mutex.synchronize do
327
+ @recent_stderr << line_str
328
+ @recent_stderr.shift if @recent_stderr.size > 20
329
+ end
330
+
346
331
  # Call stderr callback if provided
347
332
  @options.stderr&.call(line_str)
348
333
 
@@ -359,6 +344,20 @@ module ClaudeAgentSDK
359
344
  # Ignore errors during stderr reading
360
345
  end
361
346
 
347
+ def drain_stderr_with_accumulation
348
+ return unless @stderr
349
+
350
+ @stderr.each_line do |line|
351
+ line_str = line.chomp
352
+ next if line_str.empty?
353
+
354
+ @recent_stderr_mutex.synchronize do
355
+ @recent_stderr << line_str
356
+ @recent_stderr.shift if @recent_stderr.size > 20
357
+ end
358
+ end
359
+ end
360
+
362
361
  def close
363
362
  @ready = false
364
363
  return unless @process
@@ -511,10 +510,16 @@ module ClaudeAgentSDK
511
510
  returncode = status.exitstatus
512
511
 
513
512
  if returncode && returncode != 0
513
+ # Wait briefly for stderr thread to finish draining
514
+ @stderr_task&.join(1)
515
+
516
+ stderr_text = @recent_stderr_mutex.synchronize { @recent_stderr.last(10).join("\n") }
517
+ stderr_text = 'No stderr output captured' if stderr_text.empty?
518
+
514
519
  @exit_error = ProcessError.new(
515
520
  "Command failed with exit code #{returncode}",
516
521
  exit_code: returncode,
517
- stderr: 'Check stderr output for details'
522
+ stderr: stderr_text
518
523
  )
519
524
  raise @exit_error
520
525
  end
@@ -544,5 +549,24 @@ module ClaudeAgentSDK
544
549
  def ready?
545
550
  @ready
546
551
  end
552
+
553
+ DEFAULT_ADAPTIVE_THINKING_TOKENS = 32_000
554
+
555
+ private
556
+
557
+ def resolve_thinking_tokens
558
+ if @options.thinking
559
+ case @options.thinking
560
+ when ThinkingConfigAdaptive
561
+ DEFAULT_ADAPTIVE_THINKING_TOKENS
562
+ when ThinkingConfigEnabled
563
+ @options.thinking.budget_tokens
564
+ when ThinkingConfigDisabled
565
+ 0
566
+ end
567
+ elsif @options.max_thinking_tokens
568
+ @options.max_thinking_tokens
569
+ end
570
+ end
547
571
  end
548
572
  end
@@ -81,12 +81,13 @@ module ClaudeAgentSDK
81
81
 
82
82
  # User message
83
83
  class UserMessage
84
- attr_accessor :content, :uuid, :parent_tool_use_id
84
+ attr_accessor :content, :uuid, :parent_tool_use_id, :tool_use_result
85
85
 
86
- def initialize(content:, uuid: nil, parent_tool_use_id: nil)
86
+ def initialize(content:, uuid: nil, parent_tool_use_id: nil, tool_use_result: nil)
87
87
  @content = content
88
88
  @uuid = uuid # Unique identifier for rewind support
89
89
  @parent_tool_use_id = parent_tool_use_id
90
+ @tool_use_result = tool_use_result # Tool result data when message is a tool response
90
91
  end
91
92
  end
92
93
 
@@ -144,6 +145,45 @@ module ClaudeAgentSDK
144
145
  end
145
146
  end
146
147
 
148
+ # Rate limit event emitted by Claude Code CLI when API rate limits are hit
149
+ class RateLimitEvent
150
+ attr_accessor :data
151
+
152
+ def initialize(data:)
153
+ @data = data
154
+ end
155
+ end
156
+
157
+ # Thinking configuration types
158
+
159
+ # Adaptive thinking: uses a default budget of 32000 tokens
160
+ class ThinkingConfigAdaptive
161
+ attr_accessor :type
162
+
163
+ def initialize
164
+ @type = 'adaptive'
165
+ end
166
+ end
167
+
168
+ # Enabled thinking: uses a user-specified budget
169
+ class ThinkingConfigEnabled
170
+ attr_accessor :type, :budget_tokens
171
+
172
+ def initialize(budget_tokens:)
173
+ @type = 'enabled'
174
+ @budget_tokens = budget_tokens
175
+ end
176
+ end
177
+
178
+ # Disabled thinking: sets thinking tokens to 0
179
+ class ThinkingConfigDisabled
180
+ attr_accessor :type
181
+
182
+ def initialize
183
+ @type = 'disabled'
184
+ end
185
+ end
186
+
147
187
  # Agent definition configuration
148
188
  class AgentDefinition
149
189
  attr_accessor :description, :prompt, :tools, :model
@@ -269,26 +309,29 @@ module ClaudeAgentSDK
269
309
 
270
310
  # PreToolUse hook input
271
311
  class PreToolUseHookInput < BaseHookInput
272
- attr_accessor :hook_event_name, :tool_name, :tool_input
312
+ attr_accessor :hook_event_name, :tool_name, :tool_input, :tool_use_id
273
313
 
274
- def initialize(hook_event_name: 'PreToolUse', tool_name: nil, tool_input: nil, **base_args)
314
+ def initialize(hook_event_name: 'PreToolUse', tool_name: nil, tool_input: nil, tool_use_id: nil, **base_args)
275
315
  super(**base_args)
276
316
  @hook_event_name = hook_event_name
277
317
  @tool_name = tool_name
278
318
  @tool_input = tool_input
319
+ @tool_use_id = tool_use_id
279
320
  end
280
321
  end
281
322
 
282
323
  # PostToolUse hook input
283
324
  class PostToolUseHookInput < BaseHookInput
284
- attr_accessor :hook_event_name, :tool_name, :tool_input, :tool_response
325
+ attr_accessor :hook_event_name, :tool_name, :tool_input, :tool_response, :tool_use_id
285
326
 
286
- def initialize(hook_event_name: 'PostToolUse', tool_name: nil, tool_input: nil, tool_response: nil, **base_args)
327
+ def initialize(hook_event_name: 'PostToolUse', tool_name: nil, tool_input: nil, tool_response: nil,
328
+ tool_use_id: nil, **base_args)
287
329
  super(**base_args)
288
330
  @hook_event_name = hook_event_name
289
331
  @tool_name = tool_name
290
332
  @tool_input = tool_input
291
333
  @tool_response = tool_response
334
+ @tool_use_id = tool_use_id
292
335
  end
293
336
  end
294
337
 
@@ -398,13 +441,16 @@ module ClaudeAgentSDK
398
441
 
399
442
  # PreToolUse hook specific output
400
443
  class PreToolUseHookSpecificOutput
401
- attr_accessor :hook_event_name, :permission_decision, :permission_decision_reason, :updated_input
444
+ attr_accessor :hook_event_name, :permission_decision, :permission_decision_reason,
445
+ :updated_input, :additional_context
402
446
 
403
- def initialize(permission_decision: nil, permission_decision_reason: nil, updated_input: nil)
447
+ def initialize(permission_decision: nil, permission_decision_reason: nil, updated_input: nil,
448
+ additional_context: nil)
404
449
  @hook_event_name = 'PreToolUse'
405
450
  @permission_decision = permission_decision # 'allow', 'deny', or 'ask'
406
451
  @permission_decision_reason = permission_decision_reason
407
452
  @updated_input = updated_input
453
+ @additional_context = additional_context
408
454
  end
409
455
 
410
456
  def to_h
@@ -412,6 +458,7 @@ module ClaudeAgentSDK
412
458
  result[:permissionDecision] = @permission_decision if @permission_decision
413
459
  result[:permissionDecisionReason] = @permission_decision_reason if @permission_decision_reason
414
460
  result[:updatedInput] = @updated_input if @updated_input
461
+ result[:additionalContext] = @additional_context if @additional_context
415
462
  result
416
463
  end
417
464
  end
@@ -782,7 +829,8 @@ module ClaudeAgentSDK
782
829
  :fork_session, :agents, :setting_sources,
783
830
  :output_format, :max_budget_usd, :max_thinking_tokens,
784
831
  :fallback_model, :plugins, :debug_stderr,
785
- :betas, :tools, :sandbox, :enable_file_checkpointing, :append_allowed_tools
832
+ :betas, :tools, :sandbox, :enable_file_checkpointing, :append_allowed_tools,
833
+ :thinking, :effort
786
834
 
787
835
  # Non-nil defaults for options that need them.
788
836
  # Keys absent from here default to nil.
@@ -844,13 +892,14 @@ module ClaudeAgentSDK
844
892
 
845
893
  # SDK MCP Tool definition
846
894
  class SdkMcpTool
847
- attr_accessor :name, :description, :input_schema, :handler
895
+ attr_accessor :name, :description, :input_schema, :handler, :annotations
848
896
 
849
- def initialize(name:, description:, input_schema:, handler:)
897
+ def initialize(name:, description:, input_schema:, handler:, annotations: nil)
850
898
  @name = name
851
899
  @description = description
852
900
  @input_schema = input_schema
853
901
  @handler = handler
902
+ @annotations = annotations # MCP tool annotations (e.g., { title: '...', readOnlyHint: true })
854
903
  end
855
904
  end
856
905
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ClaudeAgentSDK
4
- VERSION = '0.6.2'
4
+ VERSION = '0.7.0'
5
5
  end
@@ -65,12 +65,46 @@ module ClaudeAgentSDK
65
65
  options = options.dup_with(env: (options.env || {}).merge('CLAUDE_CODE_ENTRYPOINT' => 'sdk-rb'))
66
66
 
67
67
  Async do
68
- transport = SubprocessCLITransport.new(prompt, options)
68
+ # Always use streaming mode with control protocol (matches Python SDK).
69
+ # This sends agents via initialize request instead of CLI args,
70
+ # avoiding OS ARG_MAX limits.
71
+ transport = SubprocessCLITransport.new(options)
69
72
  begin
70
73
  transport.connect
71
74
 
72
- # If prompt is an Enumerator, write each message to stdin
73
- if prompt.is_a?(Enumerator) || prompt.respond_to?(:each)
75
+ # Extract SDK MCP servers
76
+ sdk_mcp_servers = {}
77
+ if options.mcp_servers.is_a?(Hash)
78
+ options.mcp_servers.each do |name, config|
79
+ sdk_mcp_servers[name] = config[:instance] if config.is_a?(Hash) && config[:type] == 'sdk'
80
+ end
81
+ end
82
+
83
+ # Create Query handler for control protocol
84
+ query_handler = Query.new(
85
+ transport: transport,
86
+ is_streaming_mode: true,
87
+ agents: options.agents,
88
+ sdk_mcp_servers: sdk_mcp_servers
89
+ )
90
+
91
+ # Start reading messages in background
92
+ query_handler.start
93
+
94
+ # Initialize the control protocol (sends agents)
95
+ query_handler.initialize_protocol
96
+
97
+ # Send prompt(s) as user messages, then close stdin
98
+ if prompt.is_a?(String)
99
+ message = {
100
+ type: 'user',
101
+ message: { role: 'user', content: prompt },
102
+ parent_tool_use_id: nil,
103
+ session_id: 'default'
104
+ }
105
+ transport.write(JSON.generate(message) + "\n")
106
+ transport.end_input
107
+ elsif prompt.is_a?(Enumerator) || prompt.respond_to?(:each)
74
108
  Async do
75
109
  begin
76
110
  prompt.each do |message_json|
@@ -82,13 +116,18 @@ module ClaudeAgentSDK
82
116
  end
83
117
  end
84
118
 
85
- # Read and yield messages
86
- transport.read_messages do |data|
119
+ # Read and yield messages from the query handler (filters out control messages)
120
+ query_handler.receive_messages do |data|
87
121
  message = MessageParser.parse(data)
88
122
  block.call(message)
89
123
  end
90
124
  ensure
91
- transport.close
125
+ # query_handler.close stops the background read task and closes the transport
126
+ if query_handler
127
+ query_handler.close
128
+ else
129
+ transport.close
130
+ end
92
131
  end
93
132
  end.wait
94
133
  end
@@ -167,7 +206,7 @@ module ClaudeAgentSDK
167
206
  )
168
207
 
169
208
  # Client always uses streaming mode; keep stdin open for bidirectional communication.
170
- @transport = SubprocessCLITransport.new([].to_enum, configured_options)
209
+ @transport = SubprocessCLITransport.new(configured_options)
171
210
  @transport.connect
172
211
 
173
212
  # Extract SDK MCP servers
@@ -187,7 +226,8 @@ module ClaudeAgentSDK
187
226
  is_streaming_mode: true,
188
227
  can_use_tool: configured_options.can_use_tool,
189
228
  hooks: hooks,
190
- sdk_mcp_servers: sdk_mcp_servers
229
+ sdk_mcp_servers: sdk_mcp_servers,
230
+ agents: configured_options.agents
191
231
  )
192
232
 
193
233
  # Start query handler and initialize
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.6.2
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Community Contributors
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-02-16 00:00:00.000000000 Z
10
+ date: 2026-02-20 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: async