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 +4 -4
- data/CHANGELOG.md +14 -0
- data/README.md +21 -5
- 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 +57 -16
- 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: 74a467e8e7f814ad55325db2de60cee2bd1f7a8bd849945b28302cf7776db2b9
|
|
4
|
+
data.tar.gz: b0c9dc121f7500f314ebe03ba77db2e16a112c60222a7e7d50ca68897d36af41
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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
|
|
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 = [
|
|
@@ -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
|
-
|
|
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
|
-
|
|
332
|
-
|
|
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
|
|
339
|
-
#
|
|
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
|
|
344
|
-
#
|
|
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
|
|
349
|
-
#
|
|
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
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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
|
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.1
|
|
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-15 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: async
|