claude-agent-sdk 0.34.0 → 0.36.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.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +80 -0
  3. data/README.md +68 -24
  4. data/docs/cli-installer.md +38 -1
  5. data/docs/client.md +44 -20
  6. data/docs/errors.md +15 -1
  7. data/docs/hooks-and-permissions.md +22 -0
  8. data/docs/mcp-servers.md +37 -7
  9. data/docs/rails.md +92 -54
  10. data/docs/sessions.md +69 -33
  11. data/lib/claude_agent_sdk/cancellation_signal.rb +2 -1
  12. data/lib/claude_agent_sdk/cli_installer.rb +38 -8
  13. data/lib/claude_agent_sdk/deprecation.rb +51 -0
  14. data/lib/claude_agent_sdk/errors.rb +8 -0
  15. data/lib/claude_agent_sdk/fiber_boundary.rb +111 -2
  16. data/lib/claude_agent_sdk/option_warnings.rb +0 -2
  17. data/lib/claude_agent_sdk/query.rb +49 -8
  18. data/lib/claude_agent_sdk/railtie.rb +105 -0
  19. data/lib/claude_agent_sdk/sdk_mcp_server.rb +36 -25
  20. data/lib/claude_agent_sdk/session_mutations.rb +10 -10
  21. data/lib/claude_agent_sdk/session_resume.rb +19 -24
  22. data/lib/claude_agent_sdk/session_store.rb +28 -18
  23. data/lib/claude_agent_sdk/session_summary.rb +8 -3
  24. data/lib/claude_agent_sdk/sessions.rb +104 -18
  25. data/lib/claude_agent_sdk/subprocess_cli_transport.rb +4 -13
  26. data/lib/claude_agent_sdk/tasks/claude_agent_sdk.rake +37 -0
  27. data/lib/claude_agent_sdk/tasks.rb +13 -0
  28. data/lib/claude_agent_sdk/testing/session_store_conformance.rb +1 -1
  29. data/lib/claude_agent_sdk/transcript_mirror_batcher.rb +17 -1
  30. data/lib/claude_agent_sdk/types.rb +219 -3
  31. data/lib/claude_agent_sdk/version.rb +1 -1
  32. data/lib/claude_agent_sdk.rb +261 -56
  33. data/lib/generators/claude_agent_sdk/install/install_generator.rb +63 -0
  34. data/lib/generators/claude_agent_sdk/install/templates/claude_agent_sdk.rb.tt +35 -0
  35. metadata +17 -6
@@ -157,8 +157,178 @@ module ClaudeAgentSDK
157
157
  end
158
158
  private_class_method :option_hash_key
159
159
 
160
+ # Bounded, human-oriented #inspect listing the non-nil instance variables
161
+ # in definition order:
162
+ #
163
+ # #<ClaudeAgentSDK::ResultMessage subtype="success" num_turns=3 ...>
164
+ #
165
+ # Messages carry whole transcripts, tool payloads and usage maps, so the
166
+ # output is bounded rather than faithful: long Strings are truncated,
167
+ # long Arrays/Hashes abbreviated, and nesting past INSPECT_MAX_DEPTH (or
168
+ # a reference cycle) collapses to a placeholder. Other objects keep their
169
+ # own #inspect (truncated) unless they only have Kernel#inspect, which
170
+ # dumps every ivar recursively — those (SDK MCP server instances, store
171
+ # adapters, observers) show as `#<ClassName>`. For display only: nothing
172
+ # sent to the CLI goes through #inspect or #to_s (wire output uses #to_h).
173
+ def inspect
174
+ inspect_with(0, {}.compare_by_identity)
175
+ end
176
+
177
+ # Object#to_s ignores instance variables, so `puts message` would print
178
+ # only a class name and an address. Types with a natural textual form
179
+ # (UserMessage, AssistantMessage, TextBlock, ResultMessage, SystemMessage)
180
+ # override this.
181
+ def to_s
182
+ inspect
183
+ end
184
+
185
+ # Declares attributes that carry credentials (env vars, auth headers).
186
+ # Objects get logged, so #inspect shows them filtered; #to_h and
187
+ # everything sent to the CLI are unaffected. Inherited by subclasses.
188
+ def self.inspect_filtered(*names)
189
+ @inspect_filtered_attributes = (inspect_filtered_attributes + names.map(&:to_s)).uniq.freeze
190
+ end
191
+
192
+ def self.inspect_filtered_attributes
193
+ @inspect_filtered_attributes || (superclass <= Type ? superclass.inspect_filtered_attributes : [].freeze)
194
+ end
195
+
196
+ INSPECT_MAX_STRING = 80
197
+ INSPECT_MAX_ITEMS = 5
198
+ INSPECT_MAX_DEPTH = 2
199
+ private_constant :INSPECT_MAX_STRING, :INSPECT_MAX_ITEMS, :INSPECT_MAX_DEPTH
200
+
201
+ protected
202
+
203
+ # `seen` holds the Types/containers on the current rendering path (not
204
+ # every one rendered so far), so a shared-but-acyclic value still renders
205
+ # in full wherever it appears.
206
+ def inspect_with(depth, seen)
207
+ return "#<#{inspect_class_name} …>" if depth > INSPECT_MAX_DEPTH || seen.key?(self)
208
+
209
+ seen[self] = true
210
+ begin
211
+ attributes = inspect_attributes.map do |name, value|
212
+ " #{name}=#{inspect_bounded(value, depth + 1, seen)}"
213
+ end
214
+ "#<#{inspect_class_name}#{attributes.join}>"
215
+ ensure
216
+ seen.delete(self)
217
+ end
218
+ end
219
+
160
220
  private
161
221
 
222
+ # [name, value] pairs rendered by #inspect. Subclasses override to hide
223
+ # redundant state or redact secrets — never by mutating the object.
224
+ def inspect_attributes
225
+ filtered = self.class.inspect_filtered_attributes
226
+ instance_variables.filter_map do |ivar|
227
+ value = instance_variable_get(ivar)
228
+ next if value.nil?
229
+
230
+ name = ivar.to_s.delete_prefix('@')
231
+ [name, filtered.include?(name) ? inspect_filter(value) : value]
232
+ end
233
+ end
234
+
235
+ # A credential-bearing Hash keeps its keys (useful when debugging which
236
+ # variables are set) with every value replaced; anything else is replaced
237
+ # outright. Builds a new Hash; the object itself is never touched.
238
+ def inspect_filter(value)
239
+ value.respond_to?(:each_key) ? value.each_key.to_h { |key| [key, '[FILTERED]'] } : '[FILTERED]'
240
+ end
241
+
242
+ def inspect_class_name
243
+ self.class.name || self.class.inspect
244
+ end
245
+
246
+ def inspect_bounded(value, depth, seen)
247
+ case value
248
+ when Type then value.inspect_with(depth, seen)
249
+ when String then inspect_truncated(value)
250
+ when Array then inspect_container(value, '[', ']', depth, seen) { |item| inspect_bounded(item, depth + 1, seen) }
251
+ when Hash
252
+ inspect_container(value, '{', '}', depth, seen) do |key, item|
253
+ "#{inspect_hash_key(key, depth + 1, seen)}#{inspect_bounded(item, depth + 1, seen)}"
254
+ end
255
+ when Proc, Method, UnboundMethod then inspect_callable(value)
256
+ else inspect_leaf(value)
257
+ end
258
+ end
259
+
260
+ def inspect_container(value, open, close, depth, seen, &)
261
+ return "#{open}#{close}" if value.empty?
262
+ return "#{open}…(#{value.size})#{close}" if depth > INSPECT_MAX_DEPTH || seen.key?(value)
263
+
264
+ seen[value] = true
265
+ begin
266
+ parts = value.first(INSPECT_MAX_ITEMS).map(&)
267
+ parts << "…(+#{value.size - INSPECT_MAX_ITEMS} more)" if value.size > INSPECT_MAX_ITEMS
268
+ "#{open}#{parts.join(', ')}#{close}"
269
+ ensure
270
+ seen.delete(value)
271
+ end
272
+ end
273
+
274
+ # Rendered by hand rather than via Hash#inspect, whose format differs
275
+ # between Ruby 3.3 (`{:a=>1}`) and 3.4 (`{a: 1}`).
276
+ def inspect_hash_key(key, depth, seen)
277
+ return "#{key.name}: " if key.is_a?(Symbol) && key.inspect.match?(/\A:\w+[?!]?\z/)
278
+
279
+ "#{inspect_bounded(key, depth, seen)} => "
280
+ end
281
+
282
+ def inspect_truncated(string)
283
+ return string.inspect if string.length <= INSPECT_MAX_STRING
284
+
285
+ "#{string[0, INSPECT_MAX_STRING].inspect}…(+#{string.length - INSPECT_MAX_STRING} chars)"
286
+ end
287
+
288
+ # Callbacks (can_use_tool, hooks, callback_wrapper, ...) are user-supplied:
289
+ # render them from source_location rather than their own #inspect, which
290
+ # a subclass may override (and raise from) and which embeds an absolute
291
+ # path — `#<Proc(lambda) permissions.rb:17>`, `#<Method Policy#call>`.
292
+ def inspect_callable(value)
293
+ label = if value.is_a?(Proc)
294
+ value.lambda? ? 'Proc(lambda)' : 'Proc'
295
+ else
296
+ "#{value.class.name} #{value.owner.name || value.owner.inspect}##{value.name}"
297
+ end
298
+ file, line = value.source_location
299
+ rendered = file ? "#<#{label} #{File.basename(file)}:#{line}>" : "#<#{label}>"
300
+ inspect_truncated_text(rendered)
301
+ rescue StandardError
302
+ inspect_leaf(value)
303
+ end
304
+
305
+ def inspect_truncated_text(rendered)
306
+ return rendered if rendered.length <= INSPECT_MAX_STRING
307
+
308
+ "#{rendered[0, INSPECT_MAX_STRING]}…(+#{rendered.length - INSPECT_MAX_STRING} chars)"
309
+ end
310
+
311
+ # Printing must never raise (it runs inside loggers and `puts`), so an
312
+ # object whose #inspect raises, or a BasicObject without one, falls back
313
+ # to a placeholder.
314
+ def inspect_leaf(value)
315
+ return "#<#{value.class}>" if kernel_inspect_only?(value)
316
+
317
+ inspect_truncated_text(value.inspect)
318
+ rescue StandardError
319
+ begin
320
+ "#<#{value.class}>"
321
+ rescue StandardError
322
+ '#<?>'
323
+ end
324
+ end
325
+
326
+ def kernel_inspect_only?(value)
327
+ Kernel.instance_method(:method).bind_call(value, :inspect).owner == Kernel
328
+ rescue TypeError # not a Kernel object: BasicObject, Delegator
329
+ false
330
+ end
331
+
162
332
  # Allow camelCase attribute access
163
333
  def method_missing(method_name, ...)
164
334
  normalized = normalize_name(method_name)
@@ -230,6 +400,10 @@ module ClaudeAgentSDK
230
400
  # Text content block
231
401
  class TextBlock < Type
232
402
  attr_accessor :text
403
+
404
+ def to_s
405
+ text.to_s
406
+ end
233
407
  end
234
408
 
235
409
  # Thinking content block
@@ -375,6 +549,22 @@ module ClaudeAgentSDK
375
549
  super
376
550
  @data ||= attributes if attributes.is_a?(Hash)
377
551
  end
552
+
553
+ def to_s
554
+ subtype.nil? ? '[system]' : "[system: #{subtype}]"
555
+ end
556
+
557
+ private
558
+
559
+ # A typed subclass (InitMessage, TaskStartedMessage, ...) already exposes
560
+ # the fields of its raw frame as attributes; repeating @data would double
561
+ # the output. A bare SystemMessage (unrecognized subtype) keeps it, since
562
+ # @data is the only place its payload lives.
563
+ def inspect_attributes
564
+ return super if instance_of?(SystemMessage)
565
+
566
+ super.reject { |pair| pair.first == 'data' }
567
+ end
378
568
  end
379
569
 
380
570
  # Init system message (emitted at session start and after /clear)
@@ -755,6 +945,19 @@ module ClaudeAgentSDK
755
945
  # @return [Hash{Symbol => Object}, nil]
756
946
  # @see UserMessage#origin
757
947
  attr_accessor :origin
948
+
949
+ # One human-readable line, e.g. `[result: success, 3 turns, 4.2s, $0.0120]`
950
+ # (parts the CLI did not report are left out). An error result appends its
951
+ # `errors`. Use #inspect for every field.
952
+ def to_s
953
+ parts = [subtype].compact
954
+ parts << "#{num_turns} #{num_turns == 1 ? 'turn' : 'turns'}" unless num_turns.nil?
955
+ parts << format('%.1fs', duration_ms / 1000.0) if duration_ms.is_a?(Numeric)
956
+ parts << format('$%.4f', total_cost_usd) if total_cost_usd.is_a?(Numeric)
957
+ line = parts.empty? ? '[result]' : "[result: #{parts.join(', ')}]"
958
+ line += " - #{Array(errors).join('; ')}" if is_error && !Array(errors).empty?
959
+ line
960
+ end
758
961
  end
759
962
 
760
963
  # Stream event for partial message updates
@@ -1723,6 +1926,8 @@ module ClaudeAgentSDK
1723
1926
  attr_accessor :command, :args, :env
1724
1927
  attr_reader :type
1725
1928
 
1929
+ inspect_filtered :env
1930
+
1726
1931
  def initialize(attributes = {})
1727
1932
  super
1728
1933
  @type = 'stdio'
@@ -1742,6 +1947,8 @@ module ClaudeAgentSDK
1742
1947
  attr_accessor :url, :headers
1743
1948
  attr_reader :type
1744
1949
 
1950
+ inspect_filtered :headers
1951
+
1745
1952
  def initialize(attributes = {})
1746
1953
  super
1747
1954
  @type = 'sse'
@@ -1760,6 +1967,8 @@ module ClaudeAgentSDK
1760
1967
  attr_accessor :url, :headers
1761
1968
  attr_reader :type
1762
1969
 
1970
+ inspect_filtered :headers
1971
+
1763
1972
  def initialize(attributes = {})
1764
1973
  super
1765
1974
  @type = 'http'
@@ -1980,6 +2189,9 @@ module ClaudeAgentSDK
1980
2189
 
1981
2190
  # Claude Agent Options for configuring queries
1982
2191
  class ClaudeAgentOptions < Type
2192
+ # `env` routinely carries credentials (ANTHROPIC_API_KEY, ...).
2193
+ inspect_filtered :env
2194
+
1983
2195
  attr_accessor :allowed_tools, :system_prompt, :mcp_servers, :permission_mode,
1984
2196
  :resume, :resume_session_at, :session_id, :max_turns, :disallowed_tools,
1985
2197
  :model, :permission_prompt_tool_name, :cwd, :cli_path, :settings,
@@ -2226,13 +2438,17 @@ module ClaudeAgentSDK
2226
2438
  # A callable receiving a zero-arg invocation; it MUST call it and
2227
2439
  # return its value:
2228
2440
  #
2229
- # callback_wrapper: ->(invocation) { Rails.application.executor.wrap { invocation.call } }
2441
+ # callback_wrapper: ->(invocation) { MyApm.trace('agent.callback') { invocation.call } }
2230
2442
  #
2231
2443
  # The wrapper runs on the same execution context as the callback —
2232
- # inside the worker thread in :thread mode (so executor.wrap checks AR
2233
- # connections back in when the callback ends), in place on the reactor
2444
+ # inside the worker thread in :thread mode, in place on the reactor
2234
2445
  # fiber in :inline mode. Exceptions propagate through it unchanged; it
2235
2446
  # must not swallow them. Default nil (no wrapping).
2447
+ #
2448
+ # Rails apps: use ClaudeAgentSDK::Railtie.callback_wrapper, which runs
2449
+ # callbacks in the Rails executor (AR connections check back in when the
2450
+ # callback ends). A bare `Rails.application.executor.wrap` deadlocks
2451
+ # under development code reloading in :thread mode.
2236
2452
  def callback_wrapper=(value)
2237
2453
  raise ArgumentError, "callback_wrapper must be a callable or nil (got #{value.inspect})" unless value.nil? || value.respond_to?(:call)
2238
2454
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ClaudeAgentSDK
4
- VERSION = '0.34.0'
4
+ VERSION = '0.36.0'
5
5
  end