claude-agent-sdk 0.5.0 → 0.6.1

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: acfb2780cc551ed4ff8cd1d96286636dd2d8d24b1ca3d34b93eadf15d1bbf49c
4
- data.tar.gz: b3857039975c72fbf730e261831783db37efd00706bf30b2863b2feeceb58b3a
3
+ metadata.gz: 74a467e8e7f814ad55325db2de60cee2bd1f7a8bd849945b28302cf7776db2b9
4
+ data.tar.gz: b0c9dc121f7500f314ebe03ba77db2e16a112c60222a7e7d50ca68897d36af41
5
5
  SHA512:
6
- metadata.gz: fa211da2013526662038143ed0cc7e04639a1821e5de8fdd7a6cc305f4d30fe495dd7b827796fd6e85e98612c61df2335120cbd2938a74736b1d31840719cf0f
7
- data.tar.gz: 1bf84df24c779a99dbeeea776e00e53dc131bb2aca21a3bb2658ea7c3a972222bbfb0abef7d0ef48ec402490bd992074b82d7f806b8c3a98eb616f9d76421d20
6
+ metadata.gz: 5c6b145098e0e8489838f4d9b6e1a3f0af50a18e20a15055497fca65c1922e6f0e556bfb1e1844720989b81811eaf50d1605392d4d6bbf500fe367cbd744c172
7
+ data.tar.gz: 7ca8630977ebe60f00d8592c993d80eab1cc9754a1190dbd579b239a6f8ee105c48bbf3e5fe3b1aaa3da26c6ea62f369ee00d040d48eab002253f27aecee26a5
data/CHANGELOG.md CHANGED
@@ -5,6 +5,20 @@ 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.6.0] - 2026-02-13
9
+
10
+ ### Added
11
+ - **Configurable control request timeout:** New `CLAUDE_AGENT_SDK_CONTROL_REQUEST_TIMEOUT_SECONDS` environment variable (default 1200s) for tuning the control protocol timeout — essential for long-running agent sessions and agent teams
12
+ - **`ControlRequestTimeoutError`:** Dedicated error class (`< CLIConnectionError`) raised on control request timeouts, enabling typed exception handling instead of string matching
13
+
14
+ ### Fixed
15
+ - **camelCase `requestId` fallback:** All control message routing (`read_messages`, `handle_control_response`, `handle_control_request`) now tolerates both `request_id` and `requestId` keys from the CLI
16
+ - **Outbound `requestId` parity:** Control requests and responses now include both `request_id` and `requestId` for maximum CLI compatibility
17
+ - **Pending request unblocking:** Transport errors now signal all pending control request conditions, preventing callers from hanging until timeout
18
+ - **Error object in message queue:** `read_messages` rescue now enqueues the exception object (not just `e.message`), preserving error class for typed handling
19
+ - **Thread-safe CLI discovery:** `find_cli` uses `Open3.capture2` instead of backtick shell for thread safety
20
+ - **Robust process cleanup:** `close` now uses SIGTERM → 2s grace period → SIGKILL escalation (was immediate SIGTERM with no fallback), handles `Errno::ESRCH` for already-dead processes, and logs cleanup warnings instead of silently swallowing errors
21
+
8
22
  ## [0.5.0] - 2026-02-07
9
23
 
10
24
  ### Added
data/README.md CHANGED
@@ -38,7 +38,7 @@ Add this line to your application's Gemfile:
38
38
  gem 'claude-agent-sdk', github: 'ya-luotao/claude-agent-sdk-ruby'
39
39
 
40
40
  # Or use a stable version from RubyGems
41
- gem 'claude-agent-sdk', '~> 0.5.0'
41
+ gem 'claude-agent-sdk', '~> 0.6.0'
42
42
  ```
43
43
 
44
44
  And then execute:
@@ -996,15 +996,18 @@ end
996
996
  # Base exception class for all SDK errors
997
997
  class ClaudeSDKError < StandardError; end
998
998
 
999
+ # Raised when connection to Claude Code fails
1000
+ class CLIConnectionError < ClaudeSDKError; end
1001
+
1002
+ # Raised when the control protocol does not respond in time
1003
+ class ControlRequestTimeoutError < CLIConnectionError; end
1004
+
999
1005
  # Raised when Claude Code CLI is not found
1000
1006
  class CLINotFoundError < CLIConnectionError
1001
1007
  # @param message [String] Error message (default: "Claude Code not found")
1002
1008
  # @param cli_path [String, nil] Optional path to the CLI that was not found
1003
1009
  end
1004
1010
 
1005
- # Raised when connection to Claude Code fails
1006
- class CLIConnectionError < ClaudeSDKError; end
1007
-
1008
1011
  # Raised when the Claude Code process fails
1009
1012
  class ProcessError < ClaudeSDKError
1010
1013
  attr_reader :exit_code, # Integer | nil
@@ -1027,6 +1030,7 @@ end
1027
1030
 
1028
1031
  | Type | Description |
1029
1032
  |------|-------------|
1033
+ | `Configuration` | Global defaults via `ClaudeAgentSDK.configure` block |
1030
1034
  | `ClaudeAgentOptions` | Main configuration for queries and clients |
1031
1035
  | `HookMatcher` | Hook configuration with matcher pattern and timeout |
1032
1036
  | `PermissionResultAllow` | Permission callback result to allow tool use |
@@ -1088,6 +1092,8 @@ begin
1088
1092
  ClaudeAgentSDK.query(prompt: "Hello") do |message|
1089
1093
  puts message
1090
1094
  end
1095
+ rescue ClaudeAgentSDK::ControlRequestTimeoutError
1096
+ puts "Control protocol timed out — consider increasing the timeout"
1091
1097
  rescue ClaudeAgentSDK::CLINotFoundError
1092
1098
  puts "Please install Claude Code"
1093
1099
  rescue ClaudeAgentSDK::ProcessError => e
@@ -1097,13 +1103,23 @@ rescue ClaudeAgentSDK::CLIJSONDecodeError => e
1097
1103
  end
1098
1104
  ```
1099
1105
 
1106
+ #### Configuring Timeout
1107
+
1108
+ The control request timeout defaults to **1200 seconds** (20 minutes) to accommodate long-running agent sessions. Override it via environment variable:
1109
+
1110
+ ```bash
1111
+ # Set a custom timeout (in seconds)
1112
+ export CLAUDE_AGENT_SDK_CONTROL_REQUEST_TIMEOUT_SECONDS=300 # 5 minutes
1113
+ ```
1114
+
1100
1115
  ### Error Types
1101
1116
 
1102
1117
  | Error | Description |
1103
1118
  |-------|-------------|
1104
1119
  | `ClaudeSDKError` | Base error for all SDK errors |
1105
- | `CLINotFoundError` | Claude Code not installed |
1106
1120
  | `CLIConnectionError` | Connection issues |
1121
+ | `ControlRequestTimeoutError` | Control protocol timeout (configurable via env var) |
1122
+ | `CLINotFoundError` | Claude Code not installed |
1107
1123
  | `ProcessError` | Process failed (includes `exit_code` and `stderr`) |
1108
1124
  | `CLIJSONDecodeError` | JSON parsing issues |
1109
1125
  | `MessageParseError` | Message parsing issues |
@@ -7,6 +7,9 @@ module ClaudeAgentSDK
7
7
  # Raised when unable to connect to Claude Code
8
8
  class CLIConnectionError < ClaudeSDKError; end
9
9
 
10
+ # Raised when the control protocol does not respond in time
11
+ class ControlRequestTimeoutError < CLIConnectionError; end
12
+
10
13
  # Raised when Claude Code is not found or not installed
11
14
  class CLINotFoundError < CLIConnectionError
12
15
  def initialize(message = 'Claude Code not found', cli_path: nil)
@@ -6,6 +6,7 @@ require 'async/queue'
6
6
  require 'async/condition'
7
7
  require 'securerandom'
8
8
  require_relative 'transport'
9
+ require_relative 'errors'
9
10
 
10
11
  module ClaudeAgentSDK
11
12
  # Handles bidirectional control protocol on top of Transport
@@ -19,6 +20,9 @@ module ClaudeAgentSDK
19
20
  class Query
20
21
  attr_reader :transport, :is_streaming_mode, :sdk_mcp_servers
21
22
 
23
+ CONTROL_REQUEST_TIMEOUT_ENV_VAR = 'CLAUDE_AGENT_SDK_CONTROL_REQUEST_TIMEOUT_SECONDS'
24
+ DEFAULT_CONTROL_REQUEST_TIMEOUT_SECONDS = 1200.0
25
+
22
26
  def initialize(transport:, is_streaming_mode:, can_use_tool: nil, hooks: nil, sdk_mcp_servers: nil)
23
27
  @transport = transport
24
28
  @is_streaming_mode = is_streaming_mode
@@ -95,6 +99,16 @@ module ClaudeAgentSDK
95
99
 
96
100
  private
97
101
 
102
+ def control_request_timeout_seconds
103
+ raw_value = ENV.fetch(CONTROL_REQUEST_TIMEOUT_ENV_VAR, nil)
104
+ return DEFAULT_CONTROL_REQUEST_TIMEOUT_SECONDS if raw_value.nil? || raw_value.strip.empty?
105
+
106
+ value = Float(raw_value)
107
+ value.positive? ? value : DEFAULT_CONTROL_REQUEST_TIMEOUT_SECONDS
108
+ rescue ArgumentError
109
+ DEFAULT_CONTROL_REQUEST_TIMEOUT_SECONDS
110
+ end
111
+
98
112
  def read_messages
99
113
  @transport.read_messages do |message|
100
114
  break if @closed
@@ -106,7 +120,7 @@ module ClaudeAgentSDK
106
120
  when 'control_response'
107
121
  handle_control_response(message)
108
122
  when 'control_request'
109
- request_id = message[:request_id]
123
+ request_id = message[:request_id] || message[:requestId]
110
124
  task = Async do
111
125
  begin
112
126
  handle_control_request(message)
@@ -126,8 +140,14 @@ module ClaudeAgentSDK
126
140
  end
127
141
  end
128
142
  rescue StandardError => e
143
+ # Unblock pending control requests (e.g., initialize) so callers don't hang until timeout.
144
+ @pending_control_responses.dup.each do |request_id, condition|
145
+ @pending_control_results[request_id] ||= e
146
+ condition.signal
147
+ end
148
+
129
149
  # Put error in queue so iterators can handle it
130
- @message_queue.enqueue({ type: 'error', error: e.message })
150
+ @message_queue.enqueue({ type: 'error', error: e })
131
151
  ensure
132
152
  # Always signal end of stream
133
153
  @message_queue.enqueue({ type: 'end' })
@@ -135,7 +155,7 @@ module ClaudeAgentSDK
135
155
 
136
156
  def handle_control_response(message)
137
157
  response = message[:response] || {}
138
- request_id = response[:request_id]
158
+ request_id = response[:request_id] || response[:requestId] || message[:request_id] || message[:requestId]
139
159
  return unless @pending_control_responses.key?(request_id)
140
160
 
141
161
  if response[:subtype] == 'error'
@@ -149,7 +169,7 @@ module ClaudeAgentSDK
149
169
  end
150
170
 
151
171
  def handle_control_request(request)
152
- request_id = request[:request_id]
172
+ request_id = request[:request_id] || request[:requestId]
153
173
  request_data = request[:request]
154
174
  subtype = request_data[:subtype]
155
175
 
@@ -172,6 +192,7 @@ module ClaudeAgentSDK
172
192
  response: {
173
193
  subtype: 'success',
174
194
  request_id: request_id,
195
+ requestId: request_id,
175
196
  response: response_data
176
197
  }
177
198
  }
@@ -183,6 +204,7 @@ module ClaudeAgentSDK
183
204
  response: {
184
205
  subtype: 'error',
185
206
  request_id: request_id,
207
+ requestId: request_id,
186
208
  error: 'Cancelled'
187
209
  }
188
210
  }
@@ -194,6 +216,7 @@ module ClaudeAgentSDK
194
216
  response: {
195
217
  subtype: 'error',
196
218
  request_id: request_id,
219
+ requestId: request_id,
197
220
  error: e.message
198
221
  }
199
222
  }
@@ -398,6 +421,8 @@ module ClaudeAgentSDK
398
421
  def send_control_request(request)
399
422
  raise 'Control requests require streaming mode' unless @is_streaming_mode
400
423
 
424
+ timeout_seconds = control_request_timeout_seconds
425
+
401
426
  # Generate unique request ID
402
427
  @request_counter += 1
403
428
  request_id = "req_#{@request_counter}_#{SecureRandom.hex(4)}"
@@ -410,14 +435,15 @@ module ClaudeAgentSDK
410
435
  control_request = {
411
436
  type: 'control_request',
412
437
  request_id: request_id,
438
+ requestId: request_id,
413
439
  request: request
414
440
  }
415
441
 
416
442
  @transport.write(JSON.generate(control_request) + "\n")
417
443
 
418
- # Wait for response with timeout
444
+ # Wait for response with timeout (default 1200s to handle slow CLI startup)
419
445
  Async do |task|
420
- task.with_timeout(60.0) do
446
+ task.with_timeout(timeout_seconds) do
421
447
  condition.wait
422
448
  end
423
449
 
@@ -431,7 +457,7 @@ module ClaudeAgentSDK
431
457
  rescue Async::TimeoutError
432
458
  @pending_control_responses.delete(request_id)
433
459
  @pending_control_results.delete(request_id)
434
- raise "Control request timeout: #{request[:subtype]}"
460
+ raise ControlRequestTimeoutError, "Control request timeout: #{request[:subtype]}"
435
461
  end
436
462
 
437
463
  def handle_sdk_mcp_request(server_name, message)
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'json'
4
4
  require 'open3'
5
+ require 'timeout'
5
6
  require_relative 'transport'
6
7
  require_relative 'errors'
7
8
  require_relative 'version'
@@ -29,9 +30,15 @@ module ClaudeAgentSDK
29
30
  end
30
31
 
31
32
  def find_cli
32
- # Try which command first
33
- cli = `which claude 2>/dev/null`.strip
34
- return cli unless cli.empty?
33
+ # Try which command first (using Open3 for thread safety)
34
+ cli = nil
35
+ begin
36
+ stdout, _status = Open3.capture2('which', 'claude')
37
+ cli = stdout.strip
38
+ rescue StandardError
39
+ # which command failed, try common locations
40
+ end
41
+ return cli if cli && !cli.empty? && File.executable?(cli)
35
42
 
36
43
  # Try common locations
37
44
  locations = [
@@ -255,7 +262,9 @@ module ClaudeAgentSDK
255
262
  # Build environment
256
263
  # Convert symbol keys to strings for spawn compatibility
257
264
  custom_env = @options.env.transform_keys { |k| k.to_s }
258
- process_env = ENV.to_h.merge('CLAUDE_AGENT_SDK_VERSION' => VERSION).merge(custom_env)
265
+ # Strip CLAUDECODE to prevent "nested session" detection when the SDK
266
+ # launches Claude Code from within an existing Claude Code terminal
267
+ process_env = ENV.to_h.except('CLAUDECODE').merge('CLAUDE_AGENT_SDK_VERSION' => VERSION).merge(custom_env)
259
268
  process_env['CLAUDE_CODE_ENTRYPOINT'] ||= 'sdk-rb'
260
269
  process_env['PWD'] = @cwd.to_s if @cwd
261
270
 
@@ -326,35 +335,67 @@ module ClaudeAgentSDK
326
335
  @ready = false
327
336
  return unless @process
328
337
 
338
+ cleanup_errors = []
339
+
329
340
  # Kill stderr thread
330
341
  if @stderr_task&.alive?
331
- @stderr_task.kill
332
- @stderr_task.join(1) rescue nil
342
+ begin
343
+ @stderr_task.kill
344
+ @stderr_task.join(1)
345
+ rescue StandardError => e
346
+ cleanup_errors << "stderr thread: #{e.message}"
347
+ end
333
348
  end
334
349
 
335
350
  # Close streams
336
351
  begin
337
352
  @stdin&.close
338
- rescue StandardError
339
- # Ignore
353
+ rescue IOError
354
+ # Already closed, ignore
355
+ rescue StandardError => e
356
+ cleanup_errors << "stdin: #{e.message}"
340
357
  end
358
+
341
359
  begin
342
360
  @stdout&.close
343
- rescue StandardError
344
- # Ignore
361
+ rescue IOError
362
+ # Already closed, ignore
363
+ rescue StandardError => e
364
+ cleanup_errors << "stdout: #{e.message}"
345
365
  end
366
+
346
367
  begin
347
368
  @stderr&.close
348
- rescue StandardError
349
- # Ignore
369
+ rescue IOError
370
+ # Already closed, ignore
371
+ rescue StandardError => e
372
+ cleanup_errors << "stderr: #{e.message}"
350
373
  end
351
374
 
352
375
  # Terminate process
353
376
  begin
354
- Process.kill('TERM', @process.pid) if @process.alive?
355
- @process.value
356
- rescue StandardError
357
- # Ignore
377
+ if @process.alive?
378
+ Process.kill('TERM', @process.pid)
379
+ # Wait briefly for graceful shutdown
380
+ Timeout.timeout(2) { @process.value }
381
+ end
382
+ rescue Timeout::Error
383
+ # Force kill if graceful shutdown failed
384
+ begin
385
+ Process.kill('KILL', @process.pid)
386
+ @process.value
387
+ rescue StandardError => e
388
+ cleanup_errors << "force kill: #{e.message}"
389
+ end
390
+ rescue Errno::ESRCH
391
+ # Process already dead, ignore
392
+ rescue StandardError => e
393
+ cleanup_errors << "process termination: #{e.message}"
394
+ end
395
+
396
+ # Log any cleanup errors (non-fatal)
397
+ if cleanup_errors.any?
398
+ warn "Claude SDK: Cleanup warnings: #{cleanup_errors.join(', ')}"
358
399
  end
359
400
 
360
401
  @process = nil
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ClaudeAgentSDK
4
- VERSION = '0.5.0'
4
+ VERSION = '0.6.1'
5
5
  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.5.0
4
+ version: 0.6.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Community Contributors
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-02-06 00:00:00.000000000 Z
10
+ date: 2026-02-15 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: async