claude-agent-sdk 0.5.0 → 0.6.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: acfb2780cc551ed4ff8cd1d96286636dd2d8d24b1ca3d34b93eadf15d1bbf49c
4
- data.tar.gz: b3857039975c72fbf730e261831783db37efd00706bf30b2863b2feeceb58b3a
3
+ metadata.gz: 8934de13efa22ba9e3e679393dfb3ef39e8eed194a9db4af8ef5981c8fa66570
4
+ data.tar.gz: 79bfbe222fb677a49be4249e00bdc5148a43e9e0a9bac8e9e4e7fa7d3a34abd7
5
5
  SHA512:
6
- metadata.gz: fa211da2013526662038143ed0cc7e04639a1821e5de8fdd7a6cc305f4d30fe495dd7b827796fd6e85e98612c61df2335120cbd2938a74736b1d31840719cf0f
7
- data.tar.gz: 1bf84df24c779a99dbeeea776e00e53dc131bb2aca21a3bb2658ea7c3a972222bbfb0abef7d0ef48ec402490bd992074b82d7f806b8c3a98eb616f9d76421d20
6
+ metadata.gz: c3c722b1a6ab8ff9ca7484a7b6fd59c1d60660d6fb1e111864c2afe81852bb913c5cc9bf241eb7ad635686b9d1dd8cc6ff849f2a2b6acf2eec3546c09e0e7de2
7
+ data.tar.gz: 2ae9442e915901908b5c48496a348479685273701182a18ace8a20402b1e15c8630b38fd82f2ea480bf97af4e5c25c697711f5ffcc768bf15868081b9b5f24e4
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:
@@ -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 = [
@@ -326,35 +333,67 @@ module ClaudeAgentSDK
326
333
  @ready = false
327
334
  return unless @process
328
335
 
336
+ cleanup_errors = []
337
+
329
338
  # Kill stderr thread
330
339
  if @stderr_task&.alive?
331
- @stderr_task.kill
332
- @stderr_task.join(1) rescue nil
340
+ begin
341
+ @stderr_task.kill
342
+ @stderr_task.join(1)
343
+ rescue StandardError => e
344
+ cleanup_errors << "stderr thread: #{e.message}"
345
+ end
333
346
  end
334
347
 
335
348
  # Close streams
336
349
  begin
337
350
  @stdin&.close
338
- rescue StandardError
339
- # Ignore
351
+ rescue IOError
352
+ # Already closed, ignore
353
+ rescue StandardError => e
354
+ cleanup_errors << "stdin: #{e.message}"
340
355
  end
356
+
341
357
  begin
342
358
  @stdout&.close
343
- rescue StandardError
344
- # Ignore
359
+ rescue IOError
360
+ # Already closed, ignore
361
+ rescue StandardError => e
362
+ cleanup_errors << "stdout: #{e.message}"
345
363
  end
364
+
346
365
  begin
347
366
  @stderr&.close
348
- rescue StandardError
349
- # Ignore
367
+ rescue IOError
368
+ # Already closed, ignore
369
+ rescue StandardError => e
370
+ cleanup_errors << "stderr: #{e.message}"
350
371
  end
351
372
 
352
373
  # Terminate process
353
374
  begin
354
- Process.kill('TERM', @process.pid) if @process.alive?
355
- @process.value
356
- rescue StandardError
357
- # Ignore
375
+ if @process.alive?
376
+ Process.kill('TERM', @process.pid)
377
+ # Wait briefly for graceful shutdown
378
+ Timeout.timeout(2) { @process.value }
379
+ end
380
+ rescue Timeout::Error
381
+ # Force kill if graceful shutdown failed
382
+ begin
383
+ Process.kill('KILL', @process.pid)
384
+ @process.value
385
+ rescue StandardError => e
386
+ cleanup_errors << "force kill: #{e.message}"
387
+ end
388
+ rescue Errno::ESRCH
389
+ # Process already dead, ignore
390
+ rescue StandardError => e
391
+ cleanup_errors << "process termination: #{e.message}"
392
+ end
393
+
394
+ # Log any cleanup errors (non-fatal)
395
+ if cleanup_errors.any?
396
+ warn "Claude SDK: Cleanup warnings: #{cleanup_errors.join(', ')}"
358
397
  end
359
398
 
360
399
  @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.0'
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.0
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-12 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: async