Skip to content

v0.35.0

Choose a tag to compare

@ya-luotao ya-luotao released this 23 Sep 10:45
· 393 commits to main since this release

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 — writes config/initializers/claude_agent_sdk.rb (commented model / permission_mode / cli_path / OpenTelemetry defaults, callback_wrapper: ClaudeAgentSDK::Railtie.callback_wrapper enabled), appends /vendor/claude/ to .gitignore once (any existing spelling counts), and prints the next steps.
  • claude_agent_sdk:install_cli rake task — CLIInstaller.install_pinned into Rails.root/vendor/claude, or install(version:) with CLAUDE_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 only CLIInstaller), installing under the working directory.
  • ClaudeAgentSDK::Railtie.callback_wrapper — a callback_wrapper that runs SDK callbacks in Rails.application.executor (so ActiveRecord connections check back in), except where that deadlocks: with code reloading enabled or config.allow_concurrency = false it 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 (:inline scheduling) it calls straight through. Supports Rails 7.1+.
  • CI: a rails job runs the Rails integration specs (spec/rails, in their own process via rspec --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 default bundle exec rspec run excludes spec/rails.
  • Every SDK type now prints its fields. Type#inspect lists the non-nil attributes (#<ClaudeAgentSDK::ResultMessage subtype="success" num_turns=3 total_cost_usd=0.012 ...>) instead of a bare object address, and #to_s falls back to it, so the README's puts message is 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 have Kernel#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_s for results and system messages. ResultMessage#to_s prints [result: success, 3 turns, 4.2s, $0.0120] (missing fields omitted; an error result appends its errors), SystemMessage#to_s prints [system: init], and TextBlock#to_s returns its text. UserMessage / AssistantMessage keep printing their text.

Changed

  • docs/rails.md opens with a getting-started path (gem → generator → install_cli → first job), and its ActionCable, session-resumption and background-job examples use ClaudeAgentSDK::Client.open instead of hand-rolled Async { connect … ensure disconnect }.wait. README and gemspec description lead with the Rails integration.
  • #inspect filters credential-bearing attributes to "[FILTERED]" (Hash keys stay visible): ClaudeAgentOptions#env (usually carries ANTHROPIC_API_KEY), McpStdioServerConfig#env, and McpHttpServerConfig / McpSSEServerConfig#headers, since these objects end up in logs. The objects are not modified. Type subclasses declare such attributes with inspect_filtered :name. Typed SystemMessage subclasses (InitMessage, ...) leave the raw @data frame out of #inspect, since it repeats their attributes; a bare SystemMessage keeps it. Nothing sent to the CLI changes: wire output still goes through #to_h.
  • The README's Client section and the basic example in docs/client.md now lead with Client.open, which creates the reactor and always disconnects, instead of the Async do … begin … ensure client.disconnect end.wait boilerplate. The manual connect / disconnect form is still shown for code already running inside an Async reactor. No API changes.
  • The bundled claude-agent-ruby skill recommends Client.open and documents the install generator, the install_cli task and Railtie.callback_wrapper.
  • Gem metadata names its maintainer (authors: ["ya-luotao"], with a contact email) instead of "Community Contributors". The stale IMPLEMENTATION.md is removed, and the past audit reports move from the repository root to docs/history/, which is not packaged with the gem.

Fixed

  • The Rails callback_wrapper previously 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 :thread scheduling); 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's executor.wrap queues behind it, forever. With config.allow_concurrency = false the same wrapper blocks on the executor's monitor every time. The guide (and the callback_wrapper API docs and skill reference) now recommend ClaudeAgentSDK::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]
end

What'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