Repository navigation
v0.35.0
First-class Rails integration and a first-impressions pass. Rails users: if your initializer uses the previously documented ->(inv) { Rails.application.executor.wrap { inv.call } } callback wrapper, switch to ClaudeAgentSDK::Railtie.callback_wrapper — the bare form can deadlock in development (see Fixed).
Added
- Rails integration:
ClaudeAgentSDK::Railtie, loaded only when Rails is (require_relative 'claude_agent_sdk/railtie' if defined?(Rails::Railtie), which Bundler.require satisfies in a Rails app); non-Rails processes load nothing new. It contributes a rake task and installs nothing into callback dispatch. bin/rails generate claude_agent_sdk:install— writesconfig/initializers/claude_agent_sdk.rb(commentedmodel/permission_mode/cli_path/ OpenTelemetry defaults,callback_wrapper: ClaudeAgentSDK::Railtie.callback_wrapperenabled), appends/vendor/claude/to.gitignoreonce (any existing spelling counts), and prints the next steps.claude_agent_sdk:install_clirake task —CLIInstaller.install_pinnedintoRails.root/vendor/claude, orinstall(version:)withCLAUDE_CLI_VERSION=x.y.z|stable|latest; prints the installed path. It does not boot the app, so it runs in a Docker build step. Outside Rails,require 'claude_agent_sdk/tasks'in a Rakefile provides the same task (loading onlyCLIInstaller), installing under the working directory.ClaudeAgentSDK::Railtie.callback_wrapper— acallback_wrapperthat runs SDK callbacks inRails.application.executor(so ActiveRecord connections check back in), except where that deadlocks: with code reloading enabled orconfig.allow_concurrency = falseit calls the callback outside the executor and releases the thread's ActiveRecord connections itself; when the executor is already active on the callback's context (:inlinescheduling) it calls straight through. Supports Rails 7.1+.- CI: a
railsjob runs the Rails integration specs (spec/rails, in their own process viarspec --options spec/rails/.rspec) against Rails 7.1 on Ruby 3.2 and the latest Rails 8 on Ruby 3.4 (gemfiles/rails_7_1.gemfile,gemfiles/rails_8.gemfile). The defaultbundle exec rspecrun excludesspec/rails. - Every SDK type now prints its fields.
Type#inspectlists the non-nil attributes (#<ClaudeAgentSDK::ResultMessage subtype="success" num_turns=3 total_cost_usd=0.012 ...>) instead of a bare object address, and#to_sfalls back to it, so the README'sputs messageis readable for every message type. The output is bounded for logging: Strings past 80 characters are truncated with a count of what was cut, Arrays and Hashes show their first five entries plus a count of the rest, nesting past two levels (and any reference cycle) collapses to a placeholder, and objects that only haveKernel#inspect(SDK MCP server instances, store adapters, observers) show as#<ClassName>rather than dumping their state. Callbacks (can_use_tool, hooks,callback_wrapper, ...) render from their source location, e.g.#<Proc(lambda) permissions.rb:17>, never through their own#inspect, so a raising or oversized override cannot break or flood a log line. - One-line
to_sfor results and system messages.ResultMessage#to_sprints[result: success, 3 turns, 4.2s, $0.0120](missing fields omitted; an error result appends itserrors),SystemMessage#to_sprints[system: init], andTextBlock#to_sreturns its text.UserMessage/AssistantMessagekeep printing their text.
Changed
docs/rails.mdopens with a getting-started path (gem → generator →install_cli→ first job), and its ActionCable, session-resumption and background-job examples useClaudeAgentSDK::Client.openinstead of hand-rolledAsync { connect … ensure disconnect }.wait. README and gemspec description lead with the Rails integration.#inspectfilters credential-bearing attributes to"[FILTERED]"(Hash keys stay visible):ClaudeAgentOptions#env(usually carriesANTHROPIC_API_KEY),McpStdioServerConfig#env, andMcpHttpServerConfig/McpSSEServerConfig#headers, since these objects end up in logs. The objects are not modified. Type subclasses declare such attributes withinspect_filtered :name. TypedSystemMessagesubclasses (InitMessage, ...) leave the raw@dataframe out of#inspect, since it repeats their attributes; a bareSystemMessagekeeps it. Nothing sent to the CLI changes: wire output still goes through#to_h.- The README's
Clientsection and the basic example indocs/client.mdnow lead withClient.open, which creates the reactor and always disconnects, instead of theAsync do … begin … ensure client.disconnect end.waitboilerplate. The manualconnect/disconnectform is still shown for code already running inside anAsyncreactor. No API changes. - The bundled
claude-agent-rubyskill recommendsClient.openand documents the install generator, theinstall_clitask andRailtie.callback_wrapper. - Gem metadata names its maintainer (
authors: ["ya-luotao"], with a contact email) instead of "Community Contributors". The staleIMPLEMENTATION.mdis removed, and the past audit reports move from the repository root todocs/history/, which is not packaged with the gem.
Fixed
- The Rails
callback_wrapperpreviously recommended in docs/rails.md,->(inv) { Rails.application.executor.wrap { inv.call } }, can deadlock in development. With code reloading enabled, the request or job calling the SDK holds a share of the reload interlock while it waits for a callback running on its own thread (the default:threadscheduling); if a reload is requested meanwhile — e.g. after the agent edits an app file — the reloader queues for the exclusive lock and the callback'sexecutor.wrapqueues behind it, forever. Withconfig.allow_concurrency = falsethe same wrapper blocks on the executor's monitor every time. The guide (and thecallback_wrapperAPI docs and skill reference) now recommendClaudeAgentSDK::Railtie.callback_wrapper; replace the bare lambda with it in existing initializers.
Usage
bundle add claude-agent-sdk
bin/rails generate claude_agent_sdk:install # initializer with Railtie.callback_wrapper + .gitignore entry
bin/rails claude_agent_sdk:install_cli # tested CLI into vendor/claude (works as a Docker build step)ClaudeAgentSDK::Client.open(options: options) do |client|
client.query('Summarize this repo')
client.receive_response { |msg| puts msg } # ResultMessage prints as [result: success, 3 turns, 4.2s, $0.0120]
endWhat's changed
- #124 First-impression polish: readable to_s/inspect, Client.open-first docs, maintainer metadata — @ya-luotao
- #125 Rails integration: Railtie, install generator, install_cli task, deadlock-safe callback wrapper — @ya-luotao
Roadmap to 1.0: #126
Full Changelog: v0.34.0...v0.35.0
gem install claude-agent-sdk -v 0.35.0