claude-agent-sdk 0.15.0 → 0.16.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 +22 -0
- data/README.md +48 -31
- data/lib/claude_agent_sdk/fiber_boundary.rb +39 -0
- data/lib/claude_agent_sdk/message_parser.rb +16 -8
- 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/sessions.rb +30 -0
- data/lib/claude_agent_sdk/types.rb +21 -0
- data/lib/claude_agent_sdk/version.rb +1 -1
- data/lib/claude_agent_sdk.rb +11 -9
- 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: 9c6e7dfb9b25404169db87bff2a8688c9bd1103bb04ebc980bd8206091b33f87
|
|
4
|
+
data.tar.gz: 1c921d464b1b8272742f8de36800635e5f777fb88a42304ac9a5a1d7f9027beb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5b7b2c4e0503db55b36d761bec12bdc43619e16688a3b3e38c79d50a34af92d68e6ccea29e523eaaa0279f577a296cb25ba0c447cafb388c2127bc870b3cc703
|
|
7
|
+
data.tar.gz: bd05dac1831ce8b77462019f026f88b2618ca6df4be31bc84c86c145fbc59fb54738530a8b6f188c654d588171acfe28efd216a8c12b846d4d3e5ae1cfe2d37f
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.16.0] - 2026-04-22
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **`#text` on every message type that carries content.** No more hand-rolling a `select { TextBlock }.map(&:text).join` in every consumer.
|
|
14
|
+
- `AssistantMessage#text` — joins text across `TextBlock`s in the content array.
|
|
15
|
+
- `UserMessage#text` — handles both String content (plain prompt) and Array-of-blocks content.
|
|
16
|
+
- `SessionMessage#text` — joins text across parsed content blocks from a historical transcript.
|
|
17
|
+
- `#to_s` on each message type is aliased to `#text`, so `puts message` and string interpolation just work.
|
|
18
|
+
- Non-text blocks (`ToolUseBlock`, `ThinkingBlock`, `ToolResultBlock`, `UnknownBlock`) intentionally do **not** answer `#text` — only `TextBlock` is textual. The message helpers use `Array#grep(TextBlock)` to select text blocks.
|
|
19
|
+
- **`SessionMessage#content_blocks`** returns typed block objects (`TextBlock`, `ThinkingBlock`, `ToolUseBlock`, `ToolResultBlock`, `UnknownBlock`) instead of the raw hash blocks from the JSONL transcript. Unknown block types become `UnknownBlock` for forward compatibility with newer CLI versions.
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
- Rails Integration / Quick Start / Observability / File Checkpointing README examples dropped the `content.select { is_a?(TextBlock) }.map(&:text).join` dance in favor of `message.text`.
|
|
23
|
+
|
|
24
|
+
## [0.15.1] - 2026-04-22
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
- **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.
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
- **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.
|
|
31
|
+
|
|
10
32
|
## [0.15.0] - 2026-04-17
|
|
11
33
|
|
|
12
34
|
### 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.
|
|
109
|
+
gem 'claude-agent-sdk', '~> 0.16.0'
|
|
110
110
|
```
|
|
111
111
|
|
|
112
112
|
And then execute:
|
|
@@ -159,11 +159,7 @@ require 'claude_agent_sdk'
|
|
|
159
159
|
|
|
160
160
|
# Simple query
|
|
161
161
|
ClaudeAgentSDK.query(prompt: "Hello Claude") do |message|
|
|
162
|
-
if message.is_a?(ClaudeAgentSDK::AssistantMessage)
|
|
163
|
-
message.content.each do |block|
|
|
164
|
-
puts block.text if block.is_a?(ClaudeAgentSDK::TextBlock)
|
|
165
|
-
end
|
|
166
|
-
end
|
|
162
|
+
puts message.text if message.is_a?(ClaudeAgentSDK::AssistantMessage)
|
|
167
163
|
end
|
|
168
164
|
|
|
169
165
|
# With options
|
|
@@ -260,11 +256,10 @@ Async do
|
|
|
260
256
|
|
|
261
257
|
# Receive the response
|
|
262
258
|
client.receive_response do |msg|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
elsif msg.is_a?(ClaudeAgentSDK::ResultMessage)
|
|
259
|
+
case msg
|
|
260
|
+
when ClaudeAgentSDK::AssistantMessage
|
|
261
|
+
puts msg.text
|
|
262
|
+
when ClaudeAgentSDK::ResultMessage
|
|
268
263
|
puts "Cost: $#{msg.total_cost_usd}" if msg.total_cost_usd
|
|
269
264
|
end
|
|
270
265
|
end
|
|
@@ -928,10 +923,7 @@ Async do
|
|
|
928
923
|
# Capture UUID for rewind capability
|
|
929
924
|
user_message_uuids << message.uuid if message.uuid
|
|
930
925
|
when ClaudeAgentSDK::AssistantMessage
|
|
931
|
-
|
|
932
|
-
message.content.each do |block|
|
|
933
|
-
puts block.text if block.is_a?(ClaudeAgentSDK::TextBlock)
|
|
934
|
-
end
|
|
926
|
+
puts message.text
|
|
935
927
|
when ClaudeAgentSDK::ResultMessage
|
|
936
928
|
puts "Query completed (cost: $#{message.total_cost_usd})"
|
|
937
929
|
end
|
|
@@ -1140,11 +1132,7 @@ options = ClaudeAgentSDK::ClaudeAgentOptions.new(
|
|
|
1140
1132
|
)
|
|
1141
1133
|
|
|
1142
1134
|
ClaudeAgentSDK.query(prompt: "List files in /tmp", options: options) do |msg|
|
|
1143
|
-
if msg.is_a?(ClaudeAgentSDK::AssistantMessage)
|
|
1144
|
-
msg.content.each do |block|
|
|
1145
|
-
puts block.text if block.is_a?(ClaudeAgentSDK::TextBlock)
|
|
1146
|
-
end
|
|
1147
|
-
end
|
|
1135
|
+
puts msg.text if msg.is_a?(ClaudeAgentSDK::AssistantMessage)
|
|
1148
1136
|
end
|
|
1149
1137
|
|
|
1150
1138
|
# For long-running apps, flush before exit:
|
|
@@ -1194,6 +1182,45 @@ For a complete multi-tool example, see [examples/otel_langfuse_example.rb](examp
|
|
|
1194
1182
|
|
|
1195
1183
|
The SDK integrates well with Rails applications. Here are common patterns:
|
|
1196
1184
|
|
|
1185
|
+
### Thread-keyed libraries are safe inside SDK callbacks
|
|
1186
|
+
|
|
1187
|
+
The SDK depends on [`async`](https://github.com/socketry/async), which installs
|
|
1188
|
+
a Fiber scheduler that multiplexes fibers onto a single OS thread and
|
|
1189
|
+
intercepts IO so blocking calls yield to siblings. Most mature Ruby libraries
|
|
1190
|
+
are thread-safe but not fiber-safe — they key state (checked-out DB
|
|
1191
|
+
connections, per-thread caches, request stores) on `Thread.current`. When the
|
|
1192
|
+
scheduler interleaves two fibers on one thread, those fibers share the same
|
|
1193
|
+
state slot, and interleaved IO on a shared connection silently corrupts wire
|
|
1194
|
+
protocols. This affects every DB driver keyed by thread (`pg`, `mysql2`,
|
|
1195
|
+
`sqlite3`), ActiveRecord's connection pool, and HTTP/cache clients pooled per
|
|
1196
|
+
thread.
|
|
1197
|
+
|
|
1198
|
+
You do **not** need to think about this. The SDK hops to a plain thread at
|
|
1199
|
+
every user-callback boundary — message blocks given to `query` / `Client`, SDK
|
|
1200
|
+
MCP tool handlers, hooks, permission callbacks, and observer methods — so
|
|
1201
|
+
your code runs with no Fiber scheduler active and inherits the ordinary
|
|
1202
|
+
thread-keyed assumptions every Rails / Sidekiq / Kamal app already makes:
|
|
1203
|
+
|
|
1204
|
+
```ruby
|
|
1205
|
+
tool = ClaudeAgentSDK.create_tool('lookup_user', 'Look up a user', { id: Integer }) do |args|
|
|
1206
|
+
user = User.find(args[:id]) # just works
|
|
1207
|
+
{ content: [{ type: 'text', text: user.name }] }
|
|
1208
|
+
end
|
|
1209
|
+
|
|
1210
|
+
ClaudeAgentSDK.query(prompt: '...') do |message|
|
|
1211
|
+
Message.create!(role: 'assistant', body: message.to_s) # just works
|
|
1212
|
+
end
|
|
1213
|
+
```
|
|
1214
|
+
|
|
1215
|
+
The trade-off: because callbacks run on a plain thread rather than inside
|
|
1216
|
+
an `Async::Task`, fiber-specific primitives aren't available to them —
|
|
1217
|
+
`Async::Task.current` will raise "No async task available". If a callback
|
|
1218
|
+
wants cooperative concurrency it should open its own `Async { }` block. In
|
|
1219
|
+
practice, callbacks typically do some Ruby work, call external services, and
|
|
1220
|
+
return — so this rarely matters. If you wrap your own call site in an outer
|
|
1221
|
+
`Async { }` block, the scheduler is visible to your code again; you've opted
|
|
1222
|
+
in, and whatever fiber-safety rules your app uses apply there.
|
|
1223
|
+
|
|
1197
1224
|
### ActionCable Streaming
|
|
1198
1225
|
|
|
1199
1226
|
Stream Claude responses to the frontend in real-time:
|
|
@@ -1219,8 +1246,7 @@ class ChatAgentJob < ApplicationJob
|
|
|
1219
1246
|
client.receive_response do |message|
|
|
1220
1247
|
case message
|
|
1221
1248
|
when ClaudeAgentSDK::AssistantMessage
|
|
1222
|
-
|
|
1223
|
-
ChatChannel.broadcast_to(chat_id, { type: 'chunk', content: text })
|
|
1249
|
+
ChatChannel.broadcast_to(chat_id, { type: 'chunk', content: message.text })
|
|
1224
1250
|
|
|
1225
1251
|
when ClaudeAgentSDK::ResultMessage
|
|
1226
1252
|
ChatChannel.broadcast_to(chat_id, {
|
|
@@ -1235,15 +1261,6 @@ class ChatAgentJob < ApplicationJob
|
|
|
1235
1261
|
end
|
|
1236
1262
|
end.wait
|
|
1237
1263
|
end
|
|
1238
|
-
|
|
1239
|
-
private
|
|
1240
|
-
|
|
1241
|
-
def extract_text(message)
|
|
1242
|
-
message.content
|
|
1243
|
-
.select { |b| b.is_a?(ClaudeAgentSDK::TextBlock) }
|
|
1244
|
-
.map(&:text)
|
|
1245
|
-
.join("\n\n")
|
|
1246
|
-
end
|
|
1247
1264
|
end
|
|
1248
1265
|
```
|
|
1249
1266
|
|
|
@@ -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
|
|
@@ -288,24 +288,32 @@ module ClaudeAgentSDK
|
|
|
288
288
|
)
|
|
289
289
|
end
|
|
290
290
|
|
|
291
|
+
# Accepts blocks with either symbol or string keys — live CLI messages
|
|
292
|
+
# arrive symbol-keyed (parsed via `symbolize_names: true`), session
|
|
293
|
+
# transcripts arrive string-keyed (parsed via `symbolize_names: false`).
|
|
294
|
+
# Uses a nil-aware fallback so `is_error: false` survives.
|
|
291
295
|
def self.parse_content_block(block)
|
|
292
|
-
|
|
296
|
+
get = lambda do |key|
|
|
297
|
+
v = block[key]
|
|
298
|
+
v.nil? ? block[key.to_s] : v
|
|
299
|
+
end
|
|
300
|
+
case get.call(:type)
|
|
293
301
|
when 'text'
|
|
294
|
-
TextBlock.new(text:
|
|
302
|
+
TextBlock.new(text: get.call(:text))
|
|
295
303
|
when 'thinking'
|
|
296
|
-
ThinkingBlock.new(thinking:
|
|
304
|
+
ThinkingBlock.new(thinking: get.call(:thinking), signature: get.call(:signature))
|
|
297
305
|
when 'tool_use'
|
|
298
|
-
ToolUseBlock.new(id:
|
|
306
|
+
ToolUseBlock.new(id: get.call(:id), name: get.call(:name), input: get.call(:input))
|
|
299
307
|
when 'tool_result'
|
|
300
308
|
ToolResultBlock.new(
|
|
301
|
-
tool_use_id:
|
|
302
|
-
content:
|
|
303
|
-
is_error:
|
|
309
|
+
tool_use_id: get.call(:tool_use_id),
|
|
310
|
+
content: get.call(:content),
|
|
311
|
+
is_error: get.call(:is_error)
|
|
304
312
|
)
|
|
305
313
|
else
|
|
306
314
|
# Forward-compatible: preserve unrecognized content block types (e.g., "document", "image")
|
|
307
315
|
# so newer CLI versions don't crash older SDK versions.
|
|
308
|
-
UnknownBlock.new(type:
|
|
316
|
+
UnknownBlock.new(type: get.call(:type), data: block)
|
|
309
317
|
end
|
|
310
318
|
end
|
|
311
319
|
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
|
|
@@ -38,6 +38,36 @@ module ClaudeAgentSDK
|
|
|
38
38
|
@message = message
|
|
39
39
|
@parent_tool_use_id = parent_tool_use_id
|
|
40
40
|
end
|
|
41
|
+
|
|
42
|
+
# Concatenated text across every TextBlock in this message.
|
|
43
|
+
# Returns "" when the message has no text content (nil message,
|
|
44
|
+
# non-Hash message, empty content, or only non-text blocks).
|
|
45
|
+
def text
|
|
46
|
+
raw = @message.is_a?(Hash) ? (@message['content'] || @message[:content]) : nil
|
|
47
|
+
case raw
|
|
48
|
+
when String then raw
|
|
49
|
+
when Array then content_blocks.grep(TextBlock).map(&:text).join("\n\n")
|
|
50
|
+
else ''
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
alias to_s text
|
|
55
|
+
|
|
56
|
+
# Typed content blocks for this message. Each entry is one of
|
|
57
|
+
# TextBlock, ThinkingBlock, ToolUseBlock, ToolResultBlock, or
|
|
58
|
+
# UnknownBlock (for forward-compatibility with newer CLI block types).
|
|
59
|
+
# Returns [] when the message has no array-of-blocks content (nil
|
|
60
|
+
# message, non-Hash message, String content, missing content).
|
|
61
|
+
def content_blocks
|
|
62
|
+
return [] unless @message.is_a?(Hash)
|
|
63
|
+
|
|
64
|
+
raw = @message['content'] || @message[:content]
|
|
65
|
+
return [] unless raw.is_a?(Array)
|
|
66
|
+
|
|
67
|
+
raw.filter_map do |block|
|
|
68
|
+
MessageParser.parse_content_block(block) if block.is_a?(Hash)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
41
71
|
end
|
|
42
72
|
|
|
43
73
|
# Session browsing functions
|
|
@@ -123,6 +123,19 @@ module ClaudeAgentSDK
|
|
|
123
123
|
@parent_tool_use_id = parent_tool_use_id
|
|
124
124
|
@tool_use_result = tool_use_result # Tool result data when message is a tool response
|
|
125
125
|
end
|
|
126
|
+
|
|
127
|
+
# Concatenated text of this message. Handles both String content
|
|
128
|
+
# (plain-text user prompt) and Array-of-blocks content (typed content).
|
|
129
|
+
# Returns "" when there is no text.
|
|
130
|
+
def text
|
|
131
|
+
case @content
|
|
132
|
+
when String then @content
|
|
133
|
+
when Array then @content.grep(TextBlock).map(&:text).join("\n\n")
|
|
134
|
+
else ''
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
alias to_s text
|
|
126
139
|
end
|
|
127
140
|
|
|
128
141
|
# Assistant message with content blocks
|
|
@@ -142,6 +155,14 @@ module ClaudeAgentSDK
|
|
|
142
155
|
@session_id = session_id # Session the message belongs to
|
|
143
156
|
@uuid = uuid # Unique message UUID in the session transcript
|
|
144
157
|
end
|
|
158
|
+
|
|
159
|
+
# Concatenated text across every TextBlock in this message's content.
|
|
160
|
+
# Returns "" when the message has no text (e.g., a pure tool_use turn).
|
|
161
|
+
def text
|
|
162
|
+
Array(@content).grep(TextBlock).map(&:text).join("\n\n")
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
alias to_s text
|
|
145
166
|
end
|
|
146
167
|
|
|
147
168
|
# System message with metadata
|
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
|
|
@@ -67,11 +71,7 @@ module ClaudeAgentSDK
|
|
|
67
71
|
# permission_mode: 'acceptEdits'
|
|
68
72
|
# )
|
|
69
73
|
# ClaudeAgentSDK.query(prompt: "Create a hello.rb file", options: options) do |msg|
|
|
70
|
-
# if msg.is_a?(ClaudeAgentSDK::AssistantMessage)
|
|
71
|
-
# msg.content.each do |block|
|
|
72
|
-
# puts block.text if block.is_a?(ClaudeAgentSDK::TextBlock)
|
|
73
|
-
# end
|
|
74
|
-
# end
|
|
74
|
+
# puts msg.text if msg.is_a?(ClaudeAgentSDK::AssistantMessage)
|
|
75
75
|
# end
|
|
76
76
|
#
|
|
77
77
|
# @example Streaming input
|
|
@@ -230,12 +230,14 @@ module ClaudeAgentSDK
|
|
|
230
230
|
end
|
|
231
231
|
end
|
|
232
232
|
|
|
233
|
-
# Read and yield messages from the query handler (filters out control messages)
|
|
233
|
+
# Read and yield messages from the query handler (filters out control messages).
|
|
234
|
+
# User block is invoked through FiberBoundary so ActiveRecord / PG calls
|
|
235
|
+
# inside it don't see the async gem's Fiber scheduler.
|
|
234
236
|
query_handler.receive_messages do |data|
|
|
235
237
|
message = MessageParser.parse(data)
|
|
236
238
|
if message
|
|
237
239
|
ClaudeAgentSDK.notify_observers(resolved_observers, :on_message, message)
|
|
238
|
-
block.call(message)
|
|
240
|
+
FiberBoundary.invoke { block.call(message) }
|
|
239
241
|
end
|
|
240
242
|
end
|
|
241
243
|
ensure
|
|
@@ -406,7 +408,7 @@ module ClaudeAgentSDK
|
|
|
406
408
|
message = MessageParser.parse(data)
|
|
407
409
|
if message
|
|
408
410
|
ClaudeAgentSDK.notify_observers(@resolved_observers, :on_message, message)
|
|
409
|
-
block.call(message)
|
|
411
|
+
FiberBoundary.invoke { block.call(message) }
|
|
410
412
|
end
|
|
411
413
|
end
|
|
412
414
|
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.
|
|
4
|
+
version: 0.16.0
|
|
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
|