claude-agent-sdk 0.33.1 → 0.35.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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +93 -0
  3. data/README.md +54 -19
  4. data/docs/cli-installer.md +40 -9
  5. data/docs/client.md +27 -18
  6. data/docs/configuration.md +5 -5
  7. data/docs/errors.md +4 -3
  8. data/docs/mcp-servers.md +22 -0
  9. data/docs/observability.md +6 -0
  10. data/docs/rails.md +92 -51
  11. data/docs/sessions.md +66 -15
  12. data/lib/claude_agent_sdk/cli_installer.rb +34 -18
  13. data/lib/claude_agent_sdk/command_builder.rb +11 -3
  14. data/lib/claude_agent_sdk/configuration.rb +54 -2
  15. data/lib/claude_agent_sdk/errors.rb +11 -3
  16. data/lib/claude_agent_sdk/fiber_boundary.rb +42 -3
  17. data/lib/claude_agent_sdk/instrumentation/otel.rb +21 -2
  18. data/lib/claude_agent_sdk/query.rb +140 -59
  19. data/lib/claude_agent_sdk/railtie.rb +94 -0
  20. data/lib/claude_agent_sdk/sdk_mcp_server.rb +46 -4
  21. data/lib/claude_agent_sdk/session_mutations.rb +39 -12
  22. data/lib/claude_agent_sdk/session_resume.rb +112 -39
  23. data/lib/claude_agent_sdk/session_store.rb +19 -3
  24. data/lib/claude_agent_sdk/session_summary.rb +5 -5
  25. data/lib/claude_agent_sdk/sessions.rb +123 -55
  26. data/lib/claude_agent_sdk/streaming.rb +0 -8
  27. data/lib/claude_agent_sdk/subprocess_cli_transport.rb +319 -54
  28. data/lib/claude_agent_sdk/tasks/claude_agent_sdk.rake +30 -0
  29. data/lib/claude_agent_sdk/tasks.rb +13 -0
  30. data/lib/claude_agent_sdk/testing/session_store_conformance.rb +13 -3
  31. data/lib/claude_agent_sdk/transcript_mirror_batcher.rb +77 -18
  32. data/lib/claude_agent_sdk/types.rb +349 -39
  33. data/lib/claude_agent_sdk/version.rb +1 -1
  34. data/lib/claude_agent_sdk.rb +53 -44
  35. data/lib/generators/claude_agent_sdk/install/install_generator.rb +63 -0
  36. data/lib/generators/claude_agent_sdk/install/templates/claude_agent_sdk.rb.tt +35 -0
  37. metadata +29 -13
@@ -20,6 +20,10 @@ require_relative 'claude_agent_sdk/session_resume'
20
20
  require_relative 'claude_agent_sdk/session_mutations'
21
21
  require_relative 'claude_agent_sdk/fiber_boundary'
22
22
  require_relative 'claude_agent_sdk/option_warnings'
23
+ # Rails apps only: Bundler.require runs after `require 'rails'`, so the
24
+ # Railtie (rake tasks; the generator lives under lib/generators) is picked up
25
+ # there and nowhere else.
26
+ require_relative 'claude_agent_sdk/railtie' if defined?(Rails::Railtie)
23
27
  require 'async'
24
28
  require 'securerandom'
25
29
 
@@ -52,18 +56,43 @@ module ClaudeAgentSDK
52
56
  # Internal: pull live SDK MCP server instances out of an mcp_servers Hash.
53
57
  # Accepts both raw Hash configs and typed Mcp*ServerConfig objects — a
54
58
  # McpSdkServerConfig passed without .to_h previously failed the Hash-only
55
- # guard, so its in-process server was silently never registered.
59
+ # guard, so its in-process server was silently never registered. Hash
60
+ # configs may use String or Symbol keys (and a Symbol :sdk type) — the
61
+ # recognition rule must match CommandBuilder#append_mcp_servers, which
62
+ # strips the instance from exactly these entries.
56
63
  def self.extract_sdk_mcp_servers(mcp_servers)
57
64
  return {} unless mcp_servers.is_a?(Hash)
58
65
 
59
66
  servers = {}
60
67
  mcp_servers.each do |name, config|
61
68
  config = config.to_h if config.is_a?(Type)
62
- servers[name] = config[:instance] if config.is_a?(Hash) && config[:type] == 'sdk'
69
+ next unless config.is_a?(Hash) && (config[:type] || config['type']).to_s == 'sdk'
70
+
71
+ servers[name] = config.key?(:instance) ? config[:instance] : config['instance']
63
72
  end
64
73
  servers
65
74
  end
66
75
 
76
+ # Internal: normalize hook lists for the control protocol. An absent or
77
+ # disabled event must not become an empty registration in initialize.
78
+ def self.convert_hooks_to_internal_format(hooks)
79
+ return nil unless hooks
80
+
81
+ internal_hooks = {}
82
+ hooks.each do |event, matchers|
83
+ next if matchers.nil? || matchers.empty?
84
+
85
+ entries = []
86
+ matchers.each do |matcher|
87
+ config = { matcher: matcher.matcher, hooks: matcher.hooks }
88
+ config[:timeout] = matcher.timeout if matcher.timeout
89
+ entries << config
90
+ end
91
+ internal_hooks[event.to_s] = entries unless entries.empty?
92
+ end
93
+ internal_hooks.empty? ? nil : internal_hooks
94
+ end
95
+
67
96
  # Internal: validate can_use_tool and route permission prompts over stdio.
68
97
  #
69
98
  # Shared by query() and Client#connect so both entry points enforce the
@@ -422,6 +451,7 @@ module ClaudeAgentSDK
422
451
  # rename_session). Appends a custom-title entry carrying a fresh uuid +
423
452
  # timestamp via SessionStore#append.
424
453
  # @raise [ArgumentError] if session_id is invalid or title is empty
454
+ # @raise [Errno::ENOENT] if the session is not found in the store (nothing is appended)
425
455
  def self.rename_session_via_store(session_store:, session_id:, title:, directory: nil)
426
456
  SessionMutations.rename_session_via_store(session_store: session_store, session_id: session_id,
427
457
  title: title, directory: directory)
@@ -430,6 +460,7 @@ module ClaudeAgentSDK
430
460
  # Tag a session in a SessionStore (store-backed counterpart to tag_session).
431
461
  # Pass nil to clear the tag.
432
462
  # @raise [ArgumentError] if session_id is invalid or tag is empty after sanitization
463
+ # @raise [Errno::ENOENT] if the session is not found in the store (nothing is appended)
433
464
  def self.tag_session_via_store(session_store:, session_id:, tag:, directory: nil)
434
465
  SessionMutations.tag_session_via_store(session_store: session_store, session_id: session_id,
435
466
  tag: tag, directory: directory)
@@ -528,7 +559,7 @@ module ClaudeAgentSDK
528
559
 
529
560
  raise ArgumentError, 'transport must respond to #connect (see ClaudeAgentSDK::Transport)' if transport && !transport.respond_to?(:connect)
530
561
 
531
- Async do
562
+ Async(&FiberBoundary.capture_otel_context do
532
563
  materialized = nil
533
564
  query_handler = nil
534
565
  begin
@@ -556,25 +587,7 @@ module ClaudeAgentSDK
556
587
  # Extract SDK MCP servers
557
588
  sdk_mcp_servers = extract_sdk_mcp_servers(configured_options.mcp_servers)
558
589
 
559
- hooks = nil
560
- if configured_options.hooks
561
- hooks = {}
562
- configured_options.hooks.each do |event, matchers|
563
- next if matchers.nil? || matchers.empty?
564
-
565
- entries = []
566
- matchers.each do |matcher|
567
- config = {
568
- matcher: matcher.matcher,
569
- hooks: matcher.hooks
570
- }
571
- config[:timeout] = matcher.timeout if matcher.timeout
572
- entries << config
573
- end
574
- hooks[event.to_s] = entries unless entries.empty?
575
- end
576
- hooks = nil if hooks.empty?
577
- end
590
+ hooks = convert_hooks_to_internal_format(configured_options.hooks)
578
591
 
579
592
  # Create Query handler for control protocol
580
593
  query_handler = Query.new(
@@ -686,7 +699,7 @@ module ClaudeAgentSDK
686
699
  end
687
700
  end
688
701
  end
689
- end.wait
702
+ end).wait
690
703
  end
691
704
 
692
705
  # Client for bidirectional, interactive conversations with Claude Code
@@ -762,7 +775,7 @@ module ClaudeAgentSDK
762
775
  def self.open(prompt = nil, options: nil, transport_class: SubprocessCLITransport, transport_args: {})
763
776
  raise ArgumentError, 'Client.open requires a block' unless block_given?
764
777
 
765
- Sync do
778
+ Sync(&FiberBoundary.capture_otel_context do
766
779
  client = new(options: options, transport_class: transport_class, transport_args: transport_args)
767
780
  # connect failures self-clean via connect's rescue -> disconnect ->
768
781
  # raise, and disconnect is idempotent — no double-teardown.
@@ -772,7 +785,7 @@ module ClaudeAgentSDK
772
785
  ensure
773
786
  client.disconnect
774
787
  end
775
- end
788
+ end)
776
789
  end
777
790
 
778
791
  # Connect to Claude with optional initial prompt.
@@ -792,6 +805,7 @@ module ClaudeAgentSDK
792
805
  def connect(prompt = nil)
793
806
  return if @connected
794
807
 
808
+ raise ArgumentError, 'prompt must be a String or an Enumerable of message Hashes/JSONL Strings (got Hash)' if prompt.is_a?(Hash)
795
809
  raise ArgumentError, "prompt must be a String, an Enumerator, or nil (got #{prompt.class})" unless prompt.nil? || prompt.is_a?(String) || prompt.respond_to?(:each)
796
810
 
797
811
  # Validate and configure permission settings
@@ -1050,6 +1064,19 @@ module ClaudeAgentSDK
1050
1064
  end
1051
1065
 
1052
1066
  # Disconnect from Claude
1067
+ #
1068
+ # Callable from inside a user callback (tool handler / hook /
1069
+ # can_use_tool). With the default `callback_scheduling: :thread` the
1070
+ # callback runs on a worker thread: the close is marshalled to the
1071
+ # reactor, returns normally once the teardown completed, and the
1072
+ # callback's return value is dropped (its reactor task was stopped). With
1073
+ # `:inline` the callback's task is a child of the read task being
1074
+ # stopped, so once the teardown has completed the deferred Async::Stop
1075
+ # unwinds the callback — disconnect raises rather than returns there;
1076
+ # ensure blocks run, `rescue StandardError` does not see it. The same
1077
+ # holds, in every scheduling mode, for a streaming-input enumerator that
1078
+ # calls disconnect: it is iterated on the reactor inside a task the close
1079
+ # stops, so it unwinds with Async::Stop once the teardown has completed.
1053
1080
  def disconnect
1054
1081
  if @connected
1055
1082
  ClaudeAgentSDK.notify_observers(@resolved_observers || [], :on_close,
@@ -1125,7 +1152,7 @@ module ClaudeAgentSDK
1125
1152
  sdk_mcp_servers = ClaudeAgentSDK.extract_sdk_mcp_servers(configured_options.mcp_servers)
1126
1153
 
1127
1154
  # Convert hooks to internal format
1128
- hooks = convert_hooks_to_internal_format(configured_options.hooks) if configured_options.hooks
1155
+ hooks = ClaudeAgentSDK.convert_hooks_to_internal_format(configured_options.hooks)
1129
1156
 
1130
1157
  # Extract exclude_dynamic_sections and snapshot from the system prompt
1131
1158
  # for the initialize request (older CLIs ignore unknown initialize fields)
@@ -1236,24 +1263,6 @@ module ClaudeAgentSDK
1236
1263
  @query_handler.set_transcript_mirror_batcher(batcher)
1237
1264
  end
1238
1265
 
1239
- def convert_hooks_to_internal_format(hooks)
1240
- return nil unless hooks
1241
-
1242
- internal_hooks = {}
1243
- hooks.each do |event, matchers|
1244
- internal_hooks[event.to_s] = []
1245
- matchers.each do |matcher|
1246
- config = {
1247
- matcher: matcher.matcher,
1248
- hooks: matcher.hooks
1249
- }
1250
- config[:timeout] = matcher.timeout if matcher.timeout
1251
- internal_hooks[event.to_s] << config
1252
- end
1253
- end
1254
- internal_hooks
1255
- end
1256
-
1257
1266
  def writeln(string)
1258
1267
  write string.end_with?("\n") ? string : "#{string}\n"
1259
1268
  end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+
5
+ module ClaudeAgentSDK
6
+ module Generators
7
+ # `bin/rails generate claude_agent_sdk:install` — writes the initializer,
8
+ # ignores the vendored CLI, and prints the next steps. Lives under
9
+ # lib/generators/ so Rails' generator lookup finds it by namespace.
10
+ class InstallGenerator < ::Rails::Generators::Base
11
+ # Explicit: Thor derives the namespace by snake-casing the class path,
12
+ # which turns ClaudeAgentSDK into "claude_agent_s_d_k".
13
+ namespace 'claude_agent_sdk:install'
14
+ source_root File.expand_path('templates', __dir__)
15
+
16
+ desc 'Creates config/initializers/claude_agent_sdk.rb and git-ignores the vendored Claude Code CLI.'
17
+
18
+ GITIGNORE_ENTRY = '/vendor/claude/'
19
+ # Spellings that already ignore the vendored CLI directory.
20
+ GITIGNORE_PATTERN = %r{\A/?vendor/claude/?\z}
21
+
22
+ def create_initializer
23
+ template 'claude_agent_sdk.rb.tt', 'config/initializers/claude_agent_sdk.rb'
24
+ end
25
+
26
+ def ignore_vendored_cli
27
+ path = File.join(destination_root, '.gitignore')
28
+ entry = "# Claude Code CLI vendored by `bin/rails claude_agent_sdk:install_cli`\n#{GITIGNORE_ENTRY}\n"
29
+ return create_file('.gitignore', entry) unless File.exist?(path)
30
+
31
+ content = File.read(path)
32
+ if content.each_line.any? { |line| line.strip.match?(GITIGNORE_PATTERN) }
33
+ say_status :identical, '.gitignore (already ignores vendor/claude)', :blue
34
+ else
35
+ append_to_file '.gitignore', gitignore_separator(content) + entry
36
+ end
37
+ end
38
+
39
+ def show_next_steps
40
+ say <<~MSG
41
+
42
+ Next steps:
43
+ 1. Install the Claude Code CLI this gem is tested with into vendor/claude
44
+ (run it in your Dockerfile / bin/setup as well):
45
+ bin/rails claude_agent_sdk:install_cli
46
+ 2. Provide credentials to the CLI, e.g. ANTHROPIC_API_KEY in the environment.
47
+ 3. Review config/initializers/claude_agent_sdk.rb.
48
+
49
+ Rails guide: https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/docs/rails.md
50
+ MSG
51
+ end
52
+
53
+ private
54
+
55
+ # Start the appended block on its own line, after a blank one.
56
+ def gitignore_separator(content)
57
+ return '' if content.empty?
58
+
59
+ content.end_with?("\n") ? "\n" : "\n\n"
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Claude Agent SDK defaults, merged into every ClaudeAgentSDK.query and
4
+ # ClaudeAgentSDK::Client session; per-call ClaudeAgentOptions override them.
5
+ # Guide: https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/docs/rails.md
6
+
7
+ # Uncomment together with `observers:` below (needs the opentelemetry-sdk gem).
8
+ # require 'claude_agent_sdk/instrumentation'
9
+
10
+ ClaudeAgentSDK.configure do |config|
11
+ config.default_options = {
12
+ # Model: full ID or alias ('opus', 'sonnet', 'haiku'). Unset, the CLI picks.
13
+ # model: 'claude-sonnet-5',
14
+ # model: 'claude-opus-5',
15
+ # model: 'claude-haiku-4-5',
16
+
17
+ # Tool permissions: 'default', 'acceptEdits', 'plan', 'dontAsk',
18
+ # 'bypassPermissions' (unattended jobs in a sandbox only), or 'auto'.
19
+ # permission_mode: 'default',
20
+
21
+ # The CLI is found in vendor/claude relative to the process cwd (Rails.root
22
+ # under bin/rails, Puma and most job runners). Pin it for other launchers:
23
+ # cli_path: Rails.root.join('vendor', 'claude', 'claude').to_s,
24
+
25
+ # OpenTelemetry tracing (Langfuse, Honeycomb, ...). A factory lambda gives
26
+ # every query/session its own observer — safe under Puma and job threads.
27
+ # observers: [-> { ClaudeAgentSDK::Instrumentation::OTelObserver.new }],
28
+
29
+ # Wraps every SDK callback (tool handlers, hooks, permission callbacks,
30
+ # message blocks, observers) so ActiveRecord connections go back to the
31
+ # pool. Not a bare `Rails.application.executor.wrap`: that deadlocks with
32
+ # development code reloading. See docs/rails.md ("callback_wrapper").
33
+ callback_wrapper: ClaudeAgentSDK::Railtie.callback_wrapper
34
+ }
35
+ end
metadata CHANGED
@@ -1,36 +1,42 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: claude-agent-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.33.1
4
+ version: 0.35.0
5
5
  platform: ruby
6
6
  authors:
7
- - Community Contributors
7
+ - ya-luotao
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-09-21 00:00:00.000000000 Z
11
+ date: 2026-09-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: async
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - "~>"
17
+ - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '2.0'
19
+ version: '2.10'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '3'
20
23
  type: :runtime
21
24
  prerelease: false
22
25
  version_requirements: !ruby/object:Gem::Requirement
23
26
  requirements:
24
- - - "~>"
27
+ - - ">="
25
28
  - !ruby/object:Gem::Version
26
- version: '2.0'
29
+ version: '2.10'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '3'
27
33
  - !ruby/object:Gem::Dependency
28
34
  name: mcp
29
35
  requirement: !ruby/object:Gem::Requirement
30
36
  requirements:
31
37
  - - ">="
32
38
  - !ruby/object:Gem::Version
33
- version: '0.20'
39
+ version: '0.22'
34
40
  - - "<"
35
41
  - !ruby/object:Gem::Version
36
42
  version: '2'
@@ -40,7 +46,7 @@ dependencies:
40
46
  requirements:
41
47
  - - ">="
42
48
  - !ruby/object:Gem::Version
43
- version: '0.20'
49
+ version: '0.22'
44
50
  - - "<"
45
51
  - !ruby/object:Gem::Version
46
52
  version: '2'
@@ -100,9 +106,14 @@ dependencies:
100
106
  - - "~>"
101
107
  - !ruby/object:Gem::Version
102
108
  version: 1.87.0
103
- description: Unofficial Ruby SDK for interacting with Claude Code, supporting bidirectional
104
- conversations, custom tools, and hooks. Not officially maintained by Anthropic.
105
- email: []
109
+ description: 'Unofficial Ruby SDK for the Claude Code agent runtime: one-shot queries
110
+ and bidirectional sessions, in-process custom tools, hooks and permission callbacks.
111
+ Includes Rails integration (Railtie, install generator, CLI-vendoring rake task,
112
+ executor-aware callback wrapper), a pinned CLI installer, OpenTelemetry tracing,
113
+ and session transcript mirroring. Not affiliated with or officially maintained by
114
+ Anthropic.'
115
+ email:
116
+ - luotao@hey.com
106
117
  executables: []
107
118
  extensions: []
108
119
  extra_rdoc_files: []
@@ -135,6 +146,7 @@ files:
135
146
  - lib/claude_agent_sdk/observer.rb
136
147
  - lib/claude_agent_sdk/option_warnings.rb
137
148
  - lib/claude_agent_sdk/query.rb
149
+ - lib/claude_agent_sdk/railtie.rb
138
150
  - lib/claude_agent_sdk/sdk_mcp_server.rb
139
151
  - lib/claude_agent_sdk/session_mutations.rb
140
152
  - lib/claude_agent_sdk/session_resume.rb
@@ -143,11 +155,15 @@ files:
143
155
  - lib/claude_agent_sdk/sessions.rb
144
156
  - lib/claude_agent_sdk/streaming.rb
145
157
  - lib/claude_agent_sdk/subprocess_cli_transport.rb
158
+ - lib/claude_agent_sdk/tasks.rb
159
+ - lib/claude_agent_sdk/tasks/claude_agent_sdk.rake
146
160
  - lib/claude_agent_sdk/testing/session_store_conformance.rb
147
161
  - lib/claude_agent_sdk/transcript_mirror_batcher.rb
148
162
  - lib/claude_agent_sdk/transport.rb
149
163
  - lib/claude_agent_sdk/types.rb
150
164
  - lib/claude_agent_sdk/version.rb
165
+ - lib/generators/claude_agent_sdk/install/install_generator.rb
166
+ - lib/generators/claude_agent_sdk/install/templates/claude_agent_sdk.rb.tt
151
167
  homepage: https://github.com/ya-luotao/claude-agent-sdk-ruby
152
168
  licenses:
153
169
  - MIT
@@ -175,5 +191,5 @@ requirements: []
175
191
  rubygems_version: 3.4.19
176
192
  signing_key:
177
193
  specification_version: 4
178
- summary: Unofficial Ruby SDK for Claude Agent
194
+ summary: Unofficial Ruby SDK for Claude Agent, with Rails integration
179
195
  test_files: []