claude-agent-sdk 0.15.1 → 0.16.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9a037f9cca486fc8d6e94269b9f5f7b686574665ff403457761db764f9f50f07
4
- data.tar.gz: 01bff518e3306d1fb23b8bc1e9b11b72785a98aecf3636a1096c5522f4720444
3
+ metadata.gz: 784f87932778dac72fe69ebbe3236e62a7df31f9671b52150fae392aa3d57f05
4
+ data.tar.gz: cd743d1cabb8bd151457f0a4b18be725f25b5de684ba2430df8a76037900dd98
5
5
  SHA512:
6
- metadata.gz: 253df532bcdc4a6e0b13e0d4438944bcf9c6018b5d1062b4831efc6dac7a332c1759f85137e6de4fc44de0e8b1eb7ccfd0033698392c8faf6ca40f7e237b2e51
7
- data.tar.gz: 143d4ee8780d1a675e1531edc944de1756bff03faa9c42513b9672a3f37ac670dc17929a936cf0f1520d1cfdc45c53042d8f8fa07ad5a559e4b59439e79848f5
6
+ metadata.gz: 68b582ef69fec09c0487d6d5600c76ac5d5a8420264f2b151cc7f52f95c7fb975339d6afa9fe519d4e75b48781c4f6132446fb03b2e9995decc3088ccd66c0f1
7
+ data.tar.gz: d119aaba15cb76997ceb58abbc43176a82df9ad4b697089a6b26ffbe342fb50ea10173890dddb03d5dc13397989abb774b617f140b1f92b3fdb0a8d16e3298d2
data/CHANGELOG.md CHANGED
@@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.16.1] - 2026-04-21
11
+
12
+ ### Fixed
13
+ - `Client#receive_response` no longer raises `LocalJumpError: break from proc-closure` when called inside `Async { }`. The 0.15.1 thread-hop severed `break`'s unwind target; replaced with a flag so the loop exits via the natural `end` marker after `ResultMessage`.
14
+
15
+ ## [0.16.0] - 2026-04-22
16
+
17
+ ### Added
18
+ - **`#text` on every message type that carries content.** No more hand-rolling a `select { TextBlock }.map(&:text).join` in every consumer.
19
+ - `AssistantMessage#text` — joins text across `TextBlock`s in the content array.
20
+ - `UserMessage#text` — handles both String content (plain prompt) and Array-of-blocks content.
21
+ - `SessionMessage#text` — joins text across parsed content blocks from a historical transcript.
22
+ - `#to_s` on each message type is aliased to `#text`, so `puts message` and string interpolation just work.
23
+ - 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.
24
+ - **`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.
25
+
26
+ ### Changed
27
+ - 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`.
28
+
10
29
  ## [0.15.1] - 2026-04-22
11
30
 
12
31
  ### Fixed
data/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # Claude Agent SDK for Ruby
2
2
 
3
+ ![Claude Agent SDK for Ruby banner](assets/claude-agent-sdk-ruby-banner.png)
4
+
3
5
  [![Gem Version](https://badge.fury.io/rb/claude-agent-sdk.svg?icon=si%3Arubygems)](https://badge.fury.io/rb/claude-agent-sdk)
4
6
 
5
7
  An **unofficial, community-maintained** Ruby SDK for the [Claude Code](https://docs.claude.com/en/docs/claude-code-overview) agent runtime. Not affiliated with or supported by Anthropic.
@@ -106,7 +108,7 @@ Add this line to your application's Gemfile:
106
108
  gem 'claude-agent-sdk', github: 'ya-luotao/claude-agent-sdk-ruby'
107
109
 
108
110
  # Or use a stable version from RubyGems
109
- gem 'claude-agent-sdk', '~> 0.15.1'
111
+ gem 'claude-agent-sdk', '~> 0.16.1'
110
112
  ```
111
113
 
112
114
  And then execute:
@@ -159,11 +161,7 @@ require 'claude_agent_sdk'
159
161
 
160
162
  # Simple query
161
163
  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
164
+ puts message.text if message.is_a?(ClaudeAgentSDK::AssistantMessage)
167
165
  end
168
166
 
169
167
  # With options
@@ -260,11 +258,10 @@ Async do
260
258
 
261
259
  # Receive the response
262
260
  client.receive_response do |msg|
263
- if msg.is_a?(ClaudeAgentSDK::AssistantMessage)
264
- msg.content.each do |block|
265
- puts block.text if block.is_a?(ClaudeAgentSDK::TextBlock)
266
- end
267
- elsif msg.is_a?(ClaudeAgentSDK::ResultMessage)
261
+ case msg
262
+ when ClaudeAgentSDK::AssistantMessage
263
+ puts msg.text
264
+ when ClaudeAgentSDK::ResultMessage
268
265
  puts "Cost: $#{msg.total_cost_usd}" if msg.total_cost_usd
269
266
  end
270
267
  end
@@ -928,10 +925,7 @@ Async do
928
925
  # Capture UUID for rewind capability
929
926
  user_message_uuids << message.uuid if message.uuid
930
927
  when ClaudeAgentSDK::AssistantMessage
931
- # Handle assistant responses
932
- message.content.each do |block|
933
- puts block.text if block.is_a?(ClaudeAgentSDK::TextBlock)
934
- end
928
+ puts message.text
935
929
  when ClaudeAgentSDK::ResultMessage
936
930
  puts "Query completed (cost: $#{message.total_cost_usd})"
937
931
  end
@@ -1140,11 +1134,7 @@ options = ClaudeAgentSDK::ClaudeAgentOptions.new(
1140
1134
  )
1141
1135
 
1142
1136
  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
1137
+ puts msg.text if msg.is_a?(ClaudeAgentSDK::AssistantMessage)
1148
1138
  end
1149
1139
 
1150
1140
  # For long-running apps, flush before exit:
@@ -1258,8 +1248,7 @@ class ChatAgentJob < ApplicationJob
1258
1248
  client.receive_response do |message|
1259
1249
  case message
1260
1250
  when ClaudeAgentSDK::AssistantMessage
1261
- text = extract_text(message)
1262
- ChatChannel.broadcast_to(chat_id, { type: 'chunk', content: text })
1251
+ ChatChannel.broadcast_to(chat_id, { type: 'chunk', content: message.text })
1263
1252
 
1264
1253
  when ClaudeAgentSDK::ResultMessage
1265
1254
  ChatChannel.broadcast_to(chat_id, {
@@ -1274,15 +1263,6 @@ class ChatAgentJob < ApplicationJob
1274
1263
  end
1275
1264
  end.wait
1276
1265
  end
1277
-
1278
- private
1279
-
1280
- def extract_text(message)
1281
- message.content
1282
- .select { |b| b.is_a?(ClaudeAgentSDK::TextBlock) }
1283
- .map(&:text)
1284
- .join("\n\n")
1285
- end
1286
1266
  end
1287
1267
  ```
1288
1268
 
@@ -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
- case block[:type]
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: block[:text])
302
+ TextBlock.new(text: get.call(:text))
295
303
  when 'thinking'
296
- ThinkingBlock.new(thinking: block[:thinking], signature: block[:signature])
304
+ ThinkingBlock.new(thinking: get.call(:thinking), signature: get.call(:signature))
297
305
  when 'tool_use'
298
- ToolUseBlock.new(id: block[:id], name: block[:name], input: block[:input])
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: block[:tool_use_id],
302
- content: block[:content],
303
- is_error: block[: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: block[:type], data: block)
316
+ UnknownBlock.new(type: get.call(:type), data: block)
309
317
  end
310
318
  end
311
319
  end
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ClaudeAgentSDK
4
- VERSION = '0.15.1'
4
+ VERSION = '0.16.1'
5
5
  end
@@ -71,11 +71,7 @@ module ClaudeAgentSDK
71
71
  # permission_mode: 'acceptEdits'
72
72
  # )
73
73
  # ClaudeAgentSDK.query(prompt: "Create a hello.rb file", options: options) do |msg|
74
- # if msg.is_a?(ClaudeAgentSDK::AssistantMessage)
75
- # msg.content.each do |block|
76
- # puts block.text if block.is_a?(ClaudeAgentSDK::TextBlock)
77
- # end
78
- # end
74
+ # puts msg.text if msg.is_a?(ClaudeAgentSDK::AssistantMessage)
79
75
  # end
80
76
  #
81
77
  # @example Streaming input
@@ -422,9 +418,15 @@ module ClaudeAgentSDK
422
418
  def receive_response(&block)
423
419
  return enum_for(:receive_response) unless block
424
420
 
421
+ # Flag-based rather than `break`: `receive_messages` hops this
422
+ # block onto a plain thread via `FiberBoundary.invoke`, which
423
+ # severs break's unwind target.
424
+ result_seen = false
425
425
  receive_messages do |message|
426
+ next if result_seen
427
+
426
428
  block.call(message)
427
- break if message.is_a?(ResultMessage)
429
+ result_seen = true if message.is_a?(ResultMessage)
428
430
  end
429
431
  end
430
432
 
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.1
4
+ version: 0.16.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Community Contributors
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-04-21 00:00:00.000000000 Z
10
+ date: 2026-04-22 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: async