claude-agent-sdk 0.15.0 → 0.15.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 +8 -0
- data/README.md +40 -1
- data/lib/claude_agent_sdk/fiber_boundary.rb +39 -0
- data/lib/claude_agent_sdk/query.rb +17 -15
- data/lib/claude_agent_sdk/sdk_mcp_server.rb +6 -4
- data/lib/claude_agent_sdk/version.rb +1 -1
- data/lib/claude_agent_sdk.rb +10 -4
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9a037f9cca486fc8d6e94269b9f5f7b686574665ff403457761db764f9f50f07
|
|
4
|
+
data.tar.gz: 01bff518e3306d1fb23b8bc1e9b11b72785a98aecf3636a1096c5522f4720444
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 253df532bcdc4a6e0b13e0d4438944bcf9c6018b5d1062b4831efc6dac7a332c1759f85137e6de4fc44de0e8b1eb7ccfd0033698392c8faf6ca40f7e237b2e51
|
|
7
|
+
data.tar.gz: 143d4ee8780d1a675e1531edc944de1756bff03faa9c42513b9672a3f37ac670dc17929a936cf0f1520d1cfdc45c53042d8f8fa07ad5a559e4b59439e79848f5
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.15.1] - 2026-04-22
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **Thread-keyed libraries are now safe inside SDK callbacks.** The SDK internally hops to a plain thread at every user-callback boundary — blocks passed to `ClaudeAgentSDK.query` / `Client#receive_messages`, SDK MCP tool handlers, hooks, permission callbacks, and observer methods — so the `async` gem's Fiber scheduler is no longer visible to user code. Previously, any library that keys state on `Thread.current` (ActiveRecord and every DB driver keyed by thread — `pg`, `mysql2`, `sqlite3` — plus per-thread HTTP/cache pools, request stores, etc.) could be corrupted by the scheduler interleaving two fibers onto one checked-out connection. Rails/Sidekiq/Kamal consumers no longer need a caller-side wrapper to avoid this. See the "Thread-keyed libraries are safe inside SDK callbacks" subsection under Rails Integration in the README.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- **Callbacks run on a plain thread, not inside `Async::Task`.** Fiber-specific primitives (e.g. `Async::Task.current.sleep`, `Async::Task.current.async { ... }`) are no longer available inside tool handlers, hooks, permission callbacks, message blocks, or observers. Callbacks that want cooperative concurrency can open their own `Async { }` block. In practice callbacks do ordinary Ruby work and return a value, so this rarely affects real code.
|
|
17
|
+
|
|
10
18
|
## [0.15.0] - 2026-04-17
|
|
11
19
|
|
|
12
20
|
### Fixed
|
data/README.md
CHANGED
|
@@ -106,7 +106,7 @@ Add this line to your application's Gemfile:
|
|
|
106
106
|
gem 'claude-agent-sdk', github: 'ya-luotao/claude-agent-sdk-ruby'
|
|
107
107
|
|
|
108
108
|
# Or use a stable version from RubyGems
|
|
109
|
-
gem 'claude-agent-sdk', '~> 0.15.
|
|
109
|
+
gem 'claude-agent-sdk', '~> 0.15.1'
|
|
110
110
|
```
|
|
111
111
|
|
|
112
112
|
And then execute:
|
|
@@ -1194,6 +1194,45 @@ For a complete multi-tool example, see [examples/otel_langfuse_example.rb](examp
|
|
|
1194
1194
|
|
|
1195
1195
|
The SDK integrates well with Rails applications. Here are common patterns:
|
|
1196
1196
|
|
|
1197
|
+
### Thread-keyed libraries are safe inside SDK callbacks
|
|
1198
|
+
|
|
1199
|
+
The SDK depends on [`async`](https://github.com/socketry/async), which installs
|
|
1200
|
+
a Fiber scheduler that multiplexes fibers onto a single OS thread and
|
|
1201
|
+
intercepts IO so blocking calls yield to siblings. Most mature Ruby libraries
|
|
1202
|
+
are thread-safe but not fiber-safe — they key state (checked-out DB
|
|
1203
|
+
connections, per-thread caches, request stores) on `Thread.current`. When the
|
|
1204
|
+
scheduler interleaves two fibers on one thread, those fibers share the same
|
|
1205
|
+
state slot, and interleaved IO on a shared connection silently corrupts wire
|
|
1206
|
+
protocols. This affects every DB driver keyed by thread (`pg`, `mysql2`,
|
|
1207
|
+
`sqlite3`), ActiveRecord's connection pool, and HTTP/cache clients pooled per
|
|
1208
|
+
thread.
|
|
1209
|
+
|
|
1210
|
+
You do **not** need to think about this. The SDK hops to a plain thread at
|
|
1211
|
+
every user-callback boundary — message blocks given to `query` / `Client`, SDK
|
|
1212
|
+
MCP tool handlers, hooks, permission callbacks, and observer methods — so
|
|
1213
|
+
your code runs with no Fiber scheduler active and inherits the ordinary
|
|
1214
|
+
thread-keyed assumptions every Rails / Sidekiq / Kamal app already makes:
|
|
1215
|
+
|
|
1216
|
+
```ruby
|
|
1217
|
+
tool = ClaudeAgentSDK.create_tool('lookup_user', 'Look up a user', { id: Integer }) do |args|
|
|
1218
|
+
user = User.find(args[:id]) # just works
|
|
1219
|
+
{ content: [{ type: 'text', text: user.name }] }
|
|
1220
|
+
end
|
|
1221
|
+
|
|
1222
|
+
ClaudeAgentSDK.query(prompt: '...') do |message|
|
|
1223
|
+
Message.create!(role: 'assistant', body: message.to_s) # just works
|
|
1224
|
+
end
|
|
1225
|
+
```
|
|
1226
|
+
|
|
1227
|
+
The trade-off: because callbacks run on a plain thread rather than inside
|
|
1228
|
+
an `Async::Task`, fiber-specific primitives aren't available to them —
|
|
1229
|
+
`Async::Task.current` will raise "No async task available". If a callback
|
|
1230
|
+
wants cooperative concurrency it should open its own `Async { }` block. In
|
|
1231
|
+
practice, callbacks typically do some Ruby work, call external services, and
|
|
1232
|
+
return — so this rarely matters. If you wrap your own call site in an outer
|
|
1233
|
+
`Async { }` block, the scheduler is visible to your code again; you've opted
|
|
1234
|
+
in, and whatever fiber-safety rules your app uses apply there.
|
|
1235
|
+
|
|
1197
1236
|
### ActionCable Streaming
|
|
1198
1237
|
|
|
1199
1238
|
Stream Claude responses to the frontend in real-time:
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ClaudeAgentSDK
|
|
4
|
+
# Internal. Consumers of the SDK should never need this directly.
|
|
5
|
+
#
|
|
6
|
+
# The SDK depends on `async`, which installs a Fiber scheduler whenever an
|
|
7
|
+
# `Async { }` block is active. That scheduler multiplexes fibers onto a
|
|
8
|
+
# single OS thread and intercepts IO so blocking calls yield to siblings.
|
|
9
|
+
#
|
|
10
|
+
# Most mature Ruby libraries are thread-safe but not fiber-safe: they key
|
|
11
|
+
# state (checked-out DB connections, per-thread caches, request stores)
|
|
12
|
+
# on `Thread.current`. When the scheduler interleaves two fibers on one
|
|
13
|
+
# thread, those fibers share one state slot — and interleaved IO on a
|
|
14
|
+
# shared connection silently corrupts wire protocols. This bites every
|
|
15
|
+
# DB driver keyed by thread (pg, mysql2, sqlite3), ActiveRecord's
|
|
16
|
+
# connection pool, and any HTTP/cache client pooled per-thread.
|
|
17
|
+
#
|
|
18
|
+
# The SDK invokes user-supplied callbacks (tool handlers, hooks,
|
|
19
|
+
# permission callbacks, message blocks, observer methods) from inside
|
|
20
|
+
# its reactor. `FiberBoundary.invoke` hops those calls to a plain
|
|
21
|
+
# Ruby thread so user code runs on a fiber-scheduler-free thread and
|
|
22
|
+
# inherits the same thread-keyed state assumptions the rest of the
|
|
23
|
+
# user's app makes.
|
|
24
|
+
#
|
|
25
|
+
# No-op when no scheduler is active, so it's cheap to use unconditionally.
|
|
26
|
+
module FiberBoundary
|
|
27
|
+
module_function
|
|
28
|
+
|
|
29
|
+
# Run the given block on a plain thread when a Fiber scheduler is active.
|
|
30
|
+
# Returns the block's value. Exceptions propagate to the caller.
|
|
31
|
+
def invoke(&block)
|
|
32
|
+
return block.call unless Fiber.scheduler
|
|
33
|
+
|
|
34
|
+
thread = Thread.new(&block)
|
|
35
|
+
thread.report_on_exception = false
|
|
36
|
+
thread.value
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -300,11 +300,11 @@ module ClaudeAgentSDK
|
|
|
300
300
|
agent_id: request_data[:agent_id]
|
|
301
301
|
)
|
|
302
302
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
context
|
|
307
|
-
|
|
303
|
+
# User-supplied permission callback runs on a plain thread, not the
|
|
304
|
+
# Async reactor, so AR/PG calls inside it aren't intercepted.
|
|
305
|
+
response = FiberBoundary.invoke do
|
|
306
|
+
@can_use_tool.call(request_data[:tool_name], request_data[:input], context)
|
|
307
|
+
end
|
|
308
308
|
|
|
309
309
|
# Convert PermissionResult to expected format
|
|
310
310
|
case response
|
|
@@ -338,19 +338,21 @@ module ClaudeAgentSDK
|
|
|
338
338
|
# Create typed HookContext
|
|
339
339
|
context = HookContext.new(signal: nil)
|
|
340
340
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
341
|
+
# Hop off the Fiber scheduler before invoking user hook code. The
|
|
342
|
+
# Async-side timeout still wraps the hop; if it fires, .value returns
|
|
343
|
+
# early with an exception and the worker thread is left to finish on
|
|
344
|
+
# its own (matches prior best-effort cancellation semantics).
|
|
345
|
+
unless @hook_callback_timeouts[callback_id]
|
|
346
|
+
hook_output = FiberBoundary.invoke do
|
|
347
|
+
callback.call(hook_input, request_data[:tool_use_id], context)
|
|
348
|
+
end
|
|
349
|
+
end
|
|
346
350
|
|
|
347
351
|
if (timeout = @hook_callback_timeouts[callback_id])
|
|
348
352
|
hook_output = Async::Task.current.with_timeout(timeout) do
|
|
349
|
-
|
|
350
|
-
hook_input,
|
|
351
|
-
|
|
352
|
-
context
|
|
353
|
-
)
|
|
353
|
+
FiberBoundary.invoke do
|
|
354
|
+
callback.call(hook_input, request_data[:tool_use_id], context)
|
|
355
|
+
end
|
|
354
356
|
end
|
|
355
357
|
end
|
|
356
358
|
|
|
@@ -79,8 +79,9 @@ module ClaudeAgentSDK
|
|
|
79
79
|
tool = @tools.find { |t| t.name == name }
|
|
80
80
|
raise "Tool '#{name}' not found" unless tool
|
|
81
81
|
|
|
82
|
-
# Call the tool's handler
|
|
83
|
-
|
|
82
|
+
# Call the tool's handler on a plain thread so the async gem's
|
|
83
|
+
# Fiber scheduler is not visible to user code (which may hit AR/PG).
|
|
84
|
+
result = FiberBoundary.invoke { tool.handler.call(arguments) }
|
|
84
85
|
|
|
85
86
|
# Ensure result has the expected format
|
|
86
87
|
unless result.is_a?(Hash) && result[:content]
|
|
@@ -180,8 +181,9 @@ module ClaudeAgentSDK
|
|
|
180
181
|
end
|
|
181
182
|
|
|
182
183
|
def call(server_context: nil, **args)
|
|
183
|
-
# Filter out server_context and pass remaining args to handler
|
|
184
|
-
|
|
184
|
+
# Filter out server_context and pass remaining args to handler.
|
|
185
|
+
# Hop to a plain thread so user handlers don't see the Fiber scheduler.
|
|
186
|
+
result = FiberBoundary.invoke { @tool_def.handler.call(args) }
|
|
185
187
|
|
|
186
188
|
content = ClaudeAgentSDK.flexible_fetch(result, 'content', 'content')
|
|
187
189
|
unless result.is_a?(Hash) && content
|
data/lib/claude_agent_sdk.rb
CHANGED
|
@@ -13,6 +13,7 @@ require_relative 'claude_agent_sdk/sdk_mcp_server'
|
|
|
13
13
|
require_relative 'claude_agent_sdk/streaming'
|
|
14
14
|
require_relative 'claude_agent_sdk/sessions'
|
|
15
15
|
require_relative 'claude_agent_sdk/session_mutations'
|
|
16
|
+
require_relative 'claude_agent_sdk/fiber_boundary'
|
|
16
17
|
require 'async'
|
|
17
18
|
require 'securerandom'
|
|
18
19
|
|
|
@@ -28,9 +29,12 @@ module ClaudeAgentSDK
|
|
|
28
29
|
end
|
|
29
30
|
|
|
30
31
|
# Safely call a method on each observer, suppressing any errors.
|
|
32
|
+
# Each observer is invoked through FiberBoundary so that user code runs
|
|
33
|
+
# on a plain thread (no Fiber scheduler) even when called from inside
|
|
34
|
+
# the SDK's Async reactor.
|
|
31
35
|
def self.notify_observers(observers, method, *args)
|
|
32
36
|
observers.each do |obs|
|
|
33
|
-
obs.send(method, *args)
|
|
37
|
+
FiberBoundary.invoke { obs.send(method, *args) }
|
|
34
38
|
rescue StandardError
|
|
35
39
|
nil
|
|
36
40
|
end
|
|
@@ -230,12 +234,14 @@ module ClaudeAgentSDK
|
|
|
230
234
|
end
|
|
231
235
|
end
|
|
232
236
|
|
|
233
|
-
# Read and yield messages from the query handler (filters out control messages)
|
|
237
|
+
# Read and yield messages from the query handler (filters out control messages).
|
|
238
|
+
# User block is invoked through FiberBoundary so ActiveRecord / PG calls
|
|
239
|
+
# inside it don't see the async gem's Fiber scheduler.
|
|
234
240
|
query_handler.receive_messages do |data|
|
|
235
241
|
message = MessageParser.parse(data)
|
|
236
242
|
if message
|
|
237
243
|
ClaudeAgentSDK.notify_observers(resolved_observers, :on_message, message)
|
|
238
|
-
block.call(message)
|
|
244
|
+
FiberBoundary.invoke { block.call(message) }
|
|
239
245
|
end
|
|
240
246
|
end
|
|
241
247
|
ensure
|
|
@@ -406,7 +412,7 @@ module ClaudeAgentSDK
|
|
|
406
412
|
message = MessageParser.parse(data)
|
|
407
413
|
if message
|
|
408
414
|
ClaudeAgentSDK.notify_observers(@resolved_observers, :on_message, message)
|
|
409
|
-
block.call(message)
|
|
415
|
+
FiberBoundary.invoke { block.call(message) }
|
|
410
416
|
end
|
|
411
417
|
end
|
|
412
418
|
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.15.
|
|
4
|
+
version: 0.15.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Community Contributors
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-04-
|
|
10
|
+
date: 2026-04-21 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: async
|
|
@@ -106,6 +106,7 @@ files:
|
|
|
106
106
|
- lib/claude_agent_sdk.rb
|
|
107
107
|
- lib/claude_agent_sdk/configuration.rb
|
|
108
108
|
- lib/claude_agent_sdk/errors.rb
|
|
109
|
+
- lib/claude_agent_sdk/fiber_boundary.rb
|
|
109
110
|
- lib/claude_agent_sdk/instrumentation.rb
|
|
110
111
|
- lib/claude_agent_sdk/instrumentation/otel.rb
|
|
111
112
|
- lib/claude_agent_sdk/message_parser.rb
|