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 +4 -4
- data/CHANGELOG.md +14 -0
- data/README.md +1 -1
- data/lib/claude_agent_sdk/errors.rb +3 -0
- data/lib/claude_agent_sdk/query.rb +33 -7
- data/lib/claude_agent_sdk/subprocess_cli_transport.rb +54 -15
- data/lib/claude_agent_sdk/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8934de13efa22ba9e3e679393dfb3ef39e8eed194a9db4af8ef5981c8fa66570
|
|
4
|
+
data.tar.gz: 79bfbe222fb677a49be4249e00bdc5148a43e9e0a9bac8e9e4e7fa7d3a34abd7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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
|
|
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(
|
|
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 =
|
|
34
|
-
|
|
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
|
-
|
|
332
|
-
|
|
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
|
|
339
|
-
#
|
|
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
|
|
344
|
-
#
|
|
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
|
|
349
|
-
#
|
|
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
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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
|
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.
|
|
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-
|
|
10
|
+
date: 2026-02-12 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: async
|