claude-agent-sdk 0.34.0 → 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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +23 -0
- data/README.md +54 -19
- data/docs/cli-installer.md +23 -0
- data/docs/client.md +26 -17
- data/docs/rails.md +90 -51
- data/lib/claude_agent_sdk/railtie.rb +94 -0
- data/lib/claude_agent_sdk/tasks/claude_agent_sdk.rake +30 -0
- data/lib/claude_agent_sdk/tasks.rb +13 -0
- data/lib/claude_agent_sdk/types.rb +219 -3
- data/lib/claude_agent_sdk/version.rb +1 -1
- data/lib/claude_agent_sdk.rb +4 -0
- data/lib/generators/claude_agent_sdk/install/install_generator.rb +63 -0
- data/lib/generators/claude_agent_sdk/install/templates/claude_agent_sdk.rb.tt +35 -0
- metadata +16 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a2976dae089e4ccc32b7974b646cb9e543c4dcdfd6bfb35eeccf6b3c4ef5e6ed
|
|
4
|
+
data.tar.gz: 89ec51407ce6f25c962982078b3fe8301bfda9be4e57bf4a03593e9818d5913a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4784c6381acbfedd9a921a852329dfdd64257aa60b25bf51b0f4278b5fe6ce4913b1f719941a0fece9ac818342e5129c0ffd481159bf731ee6930ecf4902b632
|
|
7
|
+
data.tar.gz: cee7e96d3ef502138189a114716db3862d10d2ff10411e04bf0db8bc654511c547397164c7640c590eed166d11556c26b5d4e6de744f3a6ac396cdb15e40bad3
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.35.0] - 2026-09-23
|
|
11
|
+
|
|
12
|
+
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**).
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
- **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.
|
|
16
|
+
- **`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.
|
|
17
|
+
- **`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.
|
|
18
|
+
- **`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+.
|
|
19
|
+
- 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`.
|
|
20
|
+
- **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.
|
|
21
|
+
- **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.
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
- `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.
|
|
25
|
+
- **`#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`.
|
|
26
|
+
- 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.
|
|
27
|
+
- The bundled `claude-agent-ruby` skill recommends `Client.open` and documents the install generator, the `install_cli` task and `Railtie.callback_wrapper`.
|
|
28
|
+
- **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.
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
- **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.
|
|
32
|
+
|
|
10
33
|
## [0.34.0] - 2026-09-23
|
|
11
34
|
|
|
12
35
|
The September 2026 audit campaign: 17 fixes from the final audit pass plus the 28 AUDIT-2026-09-22 issues (#66–#93). A few fixes tighten behaviour that was silently wrong — read **Changed** before upgrading.
|
data/README.md
CHANGED
|
@@ -8,26 +8,28 @@
|
|
|
8
8
|
[](https://rubydoc.info/gems/claude-agent-sdk)
|
|
9
9
|
[](LICENSE)
|
|
10
10
|
|
|
11
|
-
A Ruby SDK for the [Claude Code](https://docs.claude.com/en/docs/claude-code-overview) agent runtime
|
|
11
|
+
A Ruby SDK for the [Claude Code](https://docs.claude.com/en/docs/claude-code-overview) agent runtime, built for running agents in production Ruby and Rails apps. It has the same capabilities as the official [TypeScript](https://github.com/anthropics/claude-agent-sdk-typescript) and [Python](https://github.com/anthropics/claude-agent-sdk-python) SDKs, plus what a Rails deploy needs around them: a generator and CLI-vendoring rake task, callbacks that are safe to touch ActiveRecord from, a pinned CLI binary, built-in OpenTelemetry tracing, and transcript mirroring to your own storage.
|
|
12
12
|
|
|
13
13
|
> **Unofficial and community-maintained.** This project is not affiliated with or supported by Anthropic. It tracks the official SDKs release by release; see the [CHANGELOG](CHANGELOG.md) for the currently synced version.
|
|
14
14
|
|
|
15
15
|
## Highlights
|
|
16
16
|
|
|
17
|
+
- **Rails integration.** `bin/rails generate claude_agent_sdk:install` writes the initializer and `bin/rails claude_agent_sdk:install_cli` vendors the CLI; [docs/rails.md](docs/rails.md) covers jobs, ActionCable streaming, session resumption, and solid_queue fiber workers (`callback_scheduling: :inline`).
|
|
18
|
+
- **Callbacks that are safe around ActiveRecord.** Tool handlers, hooks, permission callbacks, and message blocks run on a plain thread by default, outside the SDK's fiber scheduler, so thread-keyed libraries (ActiveRecord, `pg`, per-thread caches) behave as they do everywhere else in your app. `ClaudeAgentSDK::Railtie.callback_wrapper` runs them in the Rails executor so connections go back to the pool, without deadlocking development code reloading.
|
|
19
|
+
- **Hermetic deploys.** `CLIInstaller` vendors a checksum-verified CLI binary, pinned to the version each gem release is tested with, so production never depends on a global `npm install`.
|
|
20
|
+
- **Built-in OpenTelemetry observer** with Langfuse support; no third-party instrumentation library required.
|
|
21
|
+
- **Transcript mirroring.** A `SessionStore` adapter mirrors session transcripts to your own storage (reference adapters for Postgres, Redis, and S3, plus a conformance suite), and sessions can be resumed from it on another host.
|
|
17
22
|
- **Same wire protocol as the official SDKs.** Spawns the `claude` CLI as a subprocess and speaks stream-JSON over stdin/stdout, so every feature of the runtime is available: sessions, subagents, sandboxing, structured output, file checkpointing and rewind.
|
|
18
23
|
- **`query()` for one-shot calls, `Client` for bidirectional sessions** with interrupts, mid-session model switching, and streaming input from any `Enumerator`.
|
|
19
24
|
- **In-process custom tools.** Define tools as Ruby blocks; they run inside your process with direct access to your app state (SDK MCP servers), with JSON-Schema-validated arguments.
|
|
20
25
|
- **All 27 hook events and permission callbacks** with typed inputs, so you can gate, audit, or rewrite every tool call.
|
|
21
|
-
- **Rails-ready.** Fiber-safe callback dispatch, an initializer-style `configure` block, ActionCable streaming, background-job session resumption, and a `callback_scheduling: :inline` mode for fiber workers.
|
|
22
|
-
- **Built-in OpenTelemetry observer** with Langfuse support; no third-party instrumentation library required.
|
|
23
26
|
- **Pluggable transport** to run the CLI somewhere else (an E2B microVM, a container, over SSH).
|
|
24
|
-
- **Hermetic deploys.** `CLIInstaller` vendors a checksum-verified, pinned CLI binary into your project so production never depends on a global `npm install`.
|
|
25
27
|
|
|
26
28
|
## Installation
|
|
27
29
|
|
|
28
30
|
```ruby
|
|
29
31
|
# Gemfile
|
|
30
|
-
gem 'claude-agent-sdk', '~> 0.
|
|
32
|
+
gem 'claude-agent-sdk', '~> 0.35.0'
|
|
31
33
|
```
|
|
32
34
|
|
|
33
35
|
Then `bundle install`, or install directly with `gem install claude-agent-sdk`. To track unreleased changes, point the Gemfile at GitHub: `gem 'claude-agent-sdk', github: 'ya-luotao/claude-agent-sdk-ruby'`.
|
|
@@ -44,6 +46,30 @@ ClaudeAgentSDK::CLIInstaller.install(version: '2.1.220') # => "/app/vendor/clau
|
|
|
44
46
|
|
|
45
47
|
The vendored binary is found ahead of `PATH`, installs are idempotent and concurrency-safe, and a failed upgrade never breaks a working install. See [docs/cli-installer.md](docs/cli-installer.md) for the full behaviour, supported platforms, and the CLI discovery order.
|
|
46
48
|
|
|
49
|
+
### Rails in a minute
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
bundle add claude-agent-sdk
|
|
53
|
+
bin/rails generate claude_agent_sdk:install # config/initializers/claude_agent_sdk.rb + .gitignore entry
|
|
54
|
+
bin/rails claude_agent_sdk:install_cli # the tested CLI into vendor/claude (also a Docker build step)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
```ruby
|
|
58
|
+
# app/jobs/summarize_ticket_job.rb
|
|
59
|
+
class SummarizeTicketJob < ApplicationJob
|
|
60
|
+
def perform(ticket)
|
|
61
|
+
options = ClaudeAgentSDK::ClaudeAgentOptions.new(tools: [], max_turns: 1)
|
|
62
|
+
prompt = "Summarize this support ticket in two sentences:\n\n#{ticket.body}"
|
|
63
|
+
|
|
64
|
+
ClaudeAgentSDK.query(prompt: prompt, options: options) do |message|
|
|
65
|
+
ticket.update!(summary: message.result) if message.is_a?(ClaudeAgentSDK::ResultMessage)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The block runs on a plain thread, so ActiveRecord calls inside it just work. [docs/rails.md](docs/rails.md) continues with multi-turn sessions, ActionCable streaming, and fiber workers.
|
|
72
|
+
|
|
47
73
|
## Quick Start
|
|
48
74
|
|
|
49
75
|
```ruby
|
|
@@ -84,23 +110,31 @@ end
|
|
|
84
110
|
|
|
85
111
|
### `Client` — bidirectional sessions
|
|
86
112
|
|
|
87
|
-
`Client` keeps a session open so you can send follow-up queries, interrupt, switch models, and use hooks, permission callbacks, and custom tools.
|
|
113
|
+
`Client` keeps a session open so you can send follow-up queries, interrupt, switch models, and use hooks, permission callbacks, and custom tools. `Client.open` connects, yields the client, and always disconnects when the block exits, even on an exception. It returns the block's value.
|
|
88
114
|
|
|
89
115
|
```ruby
|
|
90
116
|
require 'claude_agent_sdk'
|
|
91
|
-
require 'async'
|
|
92
117
|
|
|
93
|
-
|
|
94
|
-
client
|
|
118
|
+
ClaudeAgentSDK::Client.open do |client|
|
|
119
|
+
client.query("What is the capital of France?")
|
|
120
|
+
client.receive_response { |msg| puts msg }
|
|
95
121
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
122
|
+
client.query("And of Germany?")
|
|
123
|
+
client.receive_response { |msg| puts msg }
|
|
124
|
+
end
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
`Client.open` creates an [`async`](https://github.com/socketry/async) reactor when there isn't one; blocking calls yield automatically, no `await` needed. Called outside a reactor, `break` inside the block raises `LocalJumpError` (the client still disconnects), so return a value instead. Code that is already running inside an `Async` reactor can also manage the lifecycle by hand:
|
|
128
|
+
|
|
129
|
+
```ruby
|
|
130
|
+
client = ClaudeAgentSDK::Client.new
|
|
131
|
+
begin
|
|
132
|
+
client.connect
|
|
133
|
+
client.query("What is the capital of France?")
|
|
134
|
+
client.receive_response { |msg| puts msg }
|
|
135
|
+
ensure
|
|
136
|
+
client.disconnect
|
|
137
|
+
end
|
|
104
138
|
```
|
|
105
139
|
|
|
106
140
|
See [docs/client.md](docs/client.md) for `interrupt`, mid-session model and permission switching, MCP status, and custom transports.
|
|
@@ -148,7 +182,7 @@ See [docs/hooks-and-permissions.md](docs/hooks-and-permissions.md) for the full
|
|
|
148
182
|
| Session listing, reading, renaming, tagging, forking, resume-at-message | [docs/sessions.md](docs/sessions.md) |
|
|
149
183
|
| Subagent capabilities, event contracts, and minimal example | [docs/subagents.md](docs/subagents.md) |
|
|
150
184
|
| OpenTelemetry tracing, Langfuse, custom observers | [docs/observability.md](docs/observability.md) |
|
|
151
|
-
| Rails: fiber safety, solid_queue fiber workers, ActionCable, jobs
|
|
185
|
+
| Rails: generator, `install_cli` task, callback wrapper, fiber safety, solid_queue fiber workers, ActionCable, jobs | [docs/rails.md](docs/rails.md) |
|
|
152
186
|
| Vendoring a pinned CLI binary and CLI discovery order | [docs/cli-installer.md](docs/cli-installer.md) |
|
|
153
187
|
| Message, content block, and configuration type reference | [docs/types.md](docs/types.md) |
|
|
154
188
|
| Error handling, exception hierarchy, timeouts | [docs/errors.md](docs/errors.md) |
|
|
@@ -210,9 +244,10 @@ bundle install
|
|
|
210
244
|
bundle exec rspec # unit suite
|
|
211
245
|
bundle exec rubocop # lint
|
|
212
246
|
RUN_INTEGRATION=1 bundle exec rspec # also run the real-CLI integration suite (needs `claude` and ANTHROPIC_API_KEY)
|
|
247
|
+
BUNDLE_GEMFILE=gemfiles/rails_8.gemfile bundle exec rspec --options spec/rails/.rspec # Rails integration specs
|
|
213
248
|
```
|
|
214
249
|
|
|
215
|
-
CI runs the suite and RuboCop on Ruby 3.2, 3.3, and 3.4. See [spec/README.md](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/spec/README.md) for the test layout.
|
|
250
|
+
CI runs the suite and RuboCop on Ruby 3.2, 3.3, and 3.4, and the Rails specs against Rails 7.1 and 8. See [spec/README.md](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/spec/README.md) for the test layout.
|
|
216
251
|
|
|
217
252
|
## Contributing
|
|
218
253
|
|
data/docs/cli-installer.md
CHANGED
|
@@ -51,6 +51,29 @@ version = ENV.fetch('CLAUDE_CLI_VERSION', ClaudeAgentSDK::CLIInstaller::PINNED_C
|
|
|
51
51
|
puts ClaudeAgentSDK::CLIInstaller.install(version: version)
|
|
52
52
|
```
|
|
53
53
|
|
|
54
|
+
## Rake task
|
|
55
|
+
|
|
56
|
+
Rails apps get `claude_agent_sdk:install_cli` from the gem's Railtie; any other project can load it from its `Rakefile`:
|
|
57
|
+
|
|
58
|
+
```ruby
|
|
59
|
+
# Rakefile (non-Rails)
|
|
60
|
+
require 'claude_agent_sdk/tasks' # loads only CLIInstaller, not the whole SDK
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
bin/rails claude_agent_sdk:install_cli # Rails: installs PINNED_CLI_VERSION into Rails.root/vendor/claude
|
|
65
|
+
rake claude_agent_sdk:install_cli # elsewhere: into vendor/claude under the working directory
|
|
66
|
+
rake claude_agent_sdk:install_cli CLAUDE_CLI_VERSION=x.y.z # a version of your own, or 'stable' / 'latest'
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The task calls `install_pinned` (or `install(version:)` when `CLAUDE_CLI_VERSION` is set — the same variable the `bin/setup` example above reads), prints the installed path, and doesn't boot the Rails app, so it runs in a Docker build without a database or credentials:
|
|
70
|
+
|
|
71
|
+
```dockerfile
|
|
72
|
+
RUN bin/rails claude_agent_sdk:install_cli
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The variable is deliberately not rake's conventional `VERSION`, which Rails' `db:migrate` uses and build environments often export for an app version or git SHA. An empty `CLAUDE_CLI_VERSION` means the gem's pin.
|
|
76
|
+
|
|
54
77
|
## Supported platforms
|
|
55
78
|
|
|
56
79
|
`darwin-arm64`, `darwin-x64` (Rosetta 2 gets the arm64 build), `linux-x64`, `linux-arm64`, and the `-musl` variants. Windows is not supported.
|
data/docs/client.md
CHANGED
|
@@ -4,29 +4,38 @@
|
|
|
4
4
|
|
|
5
5
|
## Basic Usage
|
|
6
6
|
|
|
7
|
+
`Client.open` connects, yields the client, and always disconnects when the block exits (exceptions propagate after the disconnect). It returns the block's value, and creates an `async` reactor if it isn't already running inside one.
|
|
8
|
+
|
|
7
9
|
```ruby
|
|
8
10
|
require 'claude_agent_sdk'
|
|
9
|
-
require 'async'
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
client
|
|
12
|
+
ClaudeAgentSDK::Client.open do |client|
|
|
13
|
+
client.query("What is the capital of France?")
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
when ClaudeAgentSDK::AssistantMessage
|
|
21
|
-
puts msg.text
|
|
22
|
-
when ClaudeAgentSDK::ResultMessage
|
|
23
|
-
puts "Cost: $#{msg.total_cost_usd}" if msg.total_cost_usd
|
|
24
|
-
end
|
|
15
|
+
client.receive_response do |msg|
|
|
16
|
+
case msg
|
|
17
|
+
when ClaudeAgentSDK::AssistantMessage
|
|
18
|
+
puts msg.text
|
|
19
|
+
when ClaudeAgentSDK::ResultMessage
|
|
20
|
+
puts "Cost: $#{msg.total_cost_usd}" if msg.total_cost_usd
|
|
25
21
|
end
|
|
26
|
-
ensure
|
|
27
|
-
client.disconnect
|
|
28
22
|
end
|
|
29
|
-
end
|
|
23
|
+
end
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Called outside a reactor, `break` inside the `Client.open` block raises `LocalJumpError` (the client still disconnects), so return a value from the block instead. `break` inside `receive_response` / `receive_messages` is fine. It stops the iteration.
|
|
27
|
+
|
|
28
|
+
If your code already runs inside an `Async` reactor and you want to manage the connection yourself, call `connect` and `disconnect` directly:
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
client = ClaudeAgentSDK::Client.new
|
|
32
|
+
begin
|
|
33
|
+
client.connect
|
|
34
|
+
client.query("What is the capital of France?")
|
|
35
|
+
client.receive_response { |msg| puts msg }
|
|
36
|
+
ensure
|
|
37
|
+
client.disconnect
|
|
38
|
+
end
|
|
30
39
|
```
|
|
31
40
|
|
|
32
41
|
## Advanced Features
|
data/docs/rails.md
CHANGED
|
@@ -1,6 +1,49 @@
|
|
|
1
1
|
# Rails Integration
|
|
2
2
|
|
|
3
|
-
The SDK
|
|
3
|
+
The gem ships a Railtie, an install generator and a rake task for vendoring the CLI; the rest of this page covers how SDK callbacks interact with Rails' threading, executor and fiber workers, and the common job / ActionCable patterns.
|
|
4
|
+
|
|
5
|
+
## Getting started
|
|
6
|
+
|
|
7
|
+
1. Add the gem:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
bundle add claude-agent-sdk
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
2. Generate the initializer:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
bin/rails generate claude_agent_sdk:install
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
This writes `config/initializers/claude_agent_sdk.rb` — a `ClaudeAgentSDK.configure` block with commented defaults (model, permission mode, CLI path, OpenTelemetry) and the Rails callback wrapper [described below](#rails-executor-around-callbacks-callback_wrapper) switched on — and adds `/vendor/claude/` to `.gitignore`.
|
|
20
|
+
|
|
21
|
+
3. Vendor the Claude Code CLI:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
bin/rails claude_agent_sdk:install_cli # the version this gem release is tested with
|
|
25
|
+
bin/rails claude_agent_sdk:install_cli CLAUDE_CLI_VERSION=x.y.z # or a version of your own ('stable' / 'latest' float)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The binary lands in `Rails.root/vendor/claude`, where the SDK finds it ahead of any `claude` on `PATH` whenever the process runs from the app root, as `bin/rails`, Puma and most job runners do (otherwise set `cli_path:`; the initializer has it commented). The task does not boot the app (no database or credentials needed), so the same line works as a cached Docker build step: `RUN bin/rails claude_agent_sdk:install_cli`. Installs are checksum-verified and idempotent — see [docs/cli-installer.md](cli-installer.md). The CLI authenticates from the environment, e.g. `ANTHROPIC_API_KEY`.
|
|
29
|
+
|
|
30
|
+
4. Run an agent from a job:
|
|
31
|
+
|
|
32
|
+
```ruby
|
|
33
|
+
# app/jobs/summarize_ticket_job.rb
|
|
34
|
+
class SummarizeTicketJob < ApplicationJob
|
|
35
|
+
def perform(ticket)
|
|
36
|
+
options = ClaudeAgentSDK::ClaudeAgentOptions.new(tools: [], max_turns: 1) # text only, no built-in tools
|
|
37
|
+
prompt = "Summarize this support ticket in two sentences:\n\n#{ticket.body}"
|
|
38
|
+
|
|
39
|
+
ClaudeAgentSDK.query(prompt: prompt, options: options) do |message|
|
|
40
|
+
ticket.update!(summary: message.result) if message.is_a?(ClaudeAgentSDK::ResultMessage)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The block runs on a plain thread (see the next section), so ActiveRecord calls inside it just work. For multi-turn sessions, hooks, custom tools and interrupts use `ClaudeAgentSDK::Client.open` — see [ActionCable streaming](#actioncable-streaming) below.
|
|
4
47
|
|
|
5
48
|
## Thread-keyed libraries are safe inside SDK callbacks
|
|
6
49
|
|
|
@@ -23,17 +66,25 @@ The trade-off: because callbacks run on a plain thread rather than inside an `As
|
|
|
23
66
|
|
|
24
67
|
### Rails executor around callbacks: `callback_wrapper`
|
|
25
68
|
|
|
26
|
-
One consequence of the thread hop: an ActiveRecord connection implicitly checked out inside a callback belongs to that throwaway thread and stays stranded until the pool reaper reclaims it. Rails' own answer to "code running on a thread Rails didn't create" is the executor — and `callback_wrapper` lets you install it around every user-callback dispatch:
|
|
69
|
+
One consequence of the thread hop: an ActiveRecord connection implicitly checked out inside a callback belongs to that throwaway thread and stays stranded until the pool reaper reclaims it. Rails' own answer to "code running on a thread Rails didn't create" is the executor — and `callback_wrapper` lets you install it around every user-callback dispatch. Use the SDK's Rails-aware wrapper (the generated initializer already does):
|
|
27
70
|
|
|
28
71
|
```ruby
|
|
29
72
|
ClaudeAgentSDK.configure do |config|
|
|
30
73
|
config.default_options = {
|
|
31
|
-
callback_wrapper:
|
|
74
|
+
callback_wrapper: ClaudeAgentSDK::Railtie.callback_wrapper
|
|
32
75
|
}
|
|
33
76
|
end
|
|
34
77
|
```
|
|
35
78
|
|
|
36
|
-
|
|
79
|
+
It runs each callback inside `Rails.application.executor.wrap` — except where that would deadlock, which is why it replaces the bare `->(invocation) { Rails.application.executor.wrap { invocation.call } }` this guide used to recommend:
|
|
80
|
+
|
|
81
|
+
- **Development (code reloading enabled).** Every executor then holds a share of the code-reload interlock. The request or job calling the SDK is already inside the executor, and in `:thread` mode it waits for the callback's thread. If a reload is requested meanwhile (say, another request arrives after the agent edited an app file), the reloader queues for the exclusive unload lock, and a callback thread entering `executor.wrap` queues behind it for a fresh share — which the reloader can never let through while the waiting caller holds its own. Everything hangs. The helper instead runs the callback without entering the executor (the caller's share still keeps code from being unloaded under it) and returns the thread's ActiveRecord connections to the pool when the callback finishes. The executor's other per-run hooks (query cache, `CurrentAttributes` reset) do not run for callbacks in this case.
|
|
82
|
+
- **`config.allow_concurrency = false`.** The executor holds a process-wide monitor that the calling thread already owns, so a callback thread's `executor.wrap` would block every time; same treatment.
|
|
83
|
+
- **Already inside the executor** (`:inline` scheduling on a job's own fiber): the callback runs straight through, leaving cleanup to the enclosing executor.
|
|
84
|
+
|
|
85
|
+
Everywhere else — production, with no reloading — it is exactly `executor.wrap`. The configuration is read per call, so one initializer is correct in every environment.
|
|
86
|
+
|
|
87
|
+
Writing your own wrapper: it is a callable receiving a zero-arg `invocation`; it must call it and return its value. It runs on the **same execution context as the callback** — inside the worker thread in `:thread` mode, which is the whole point: `executor.wrap` runs on the thread that touches ActiveRecord, so connections check back in when the callback ends. Exceptions from the callback propagate through the wrapper unchanged (don't rescue them); `ensure`-based wrappers like `executor.wrap` are safe, including around a `break` from a message block. Beyond the executor, this is a generic hook for APM span propagation, `CurrentAttributes`/logging context, etc. — to combine one with the Rails wrapper, call it from yours: `rails = ClaudeAgentSDK::Railtie.callback_wrapper` then `->(inv) { MyApm.trace { rails.call(inv) } }`.
|
|
37
88
|
|
|
38
89
|
The wrapper also composes around every timeout-bounded `SessionStore` adapter call (mirror-batcher appends, resume-materialization loads and listings), inside the timeout bound — so an ActiveRecord-backed store adapter gets the same connection hygiene as your callbacks.
|
|
39
90
|
|
|
@@ -96,38 +147,33 @@ class ChatAgentJob < ApplicationJob
|
|
|
96
147
|
queue_as :claude_agents
|
|
97
148
|
|
|
98
149
|
def perform(chat_id, message_content)
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
)
|
|
150
|
+
options = ClaudeAgentSDK::ClaudeAgentOptions.new(
|
|
151
|
+
system_prompt: { type: 'preset', preset: 'claude_code' },
|
|
152
|
+
permission_mode: 'bypassPermissions'
|
|
153
|
+
)
|
|
104
154
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
content: message.result,
|
|
119
|
-
cost: message.total_cost_usd
|
|
120
|
-
})
|
|
121
|
-
end
|
|
155
|
+
ClaudeAgentSDK::Client.open(options: options) do |client|
|
|
156
|
+
client.query(message_content)
|
|
157
|
+
|
|
158
|
+
client.receive_response do |message|
|
|
159
|
+
case message
|
|
160
|
+
when ClaudeAgentSDK::AssistantMessage
|
|
161
|
+
ChatChannel.broadcast_to(chat_id, { type: 'chunk', content: message.text })
|
|
162
|
+
when ClaudeAgentSDK::ResultMessage
|
|
163
|
+
ChatChannel.broadcast_to(chat_id, {
|
|
164
|
+
type: 'complete',
|
|
165
|
+
content: message.result,
|
|
166
|
+
cost: message.total_cost_usd
|
|
167
|
+
})
|
|
122
168
|
end
|
|
123
|
-
ensure
|
|
124
|
-
client.disconnect
|
|
125
169
|
end
|
|
126
|
-
end
|
|
170
|
+
end
|
|
127
171
|
end
|
|
128
172
|
end
|
|
129
173
|
```
|
|
130
174
|
|
|
175
|
+
`Client.open` connects, yields the client, and always disconnects — also when the block raises, so the job's error handling sees the original exception. It runs inside an existing reactor or starts its own, so a job needs no `Async { }.wait` wrapper. Its return value is the block's; to leave the block early use `next`, not `break` (outside an `Async` block, `break` raises `LocalJumpError`, though the session is still torn down). `break` inside `receive_response` itself is fine.
|
|
176
|
+
|
|
131
177
|
## Session Resumption
|
|
132
178
|
|
|
133
179
|
Persist Claude sessions for multi-turn conversations:
|
|
@@ -138,19 +184,13 @@ class ChatSession < ApplicationRecord
|
|
|
138
184
|
# Columns: id, claude_session_id, user_id, created_at, updated_at
|
|
139
185
|
|
|
140
186
|
def send_message(content)
|
|
141
|
-
options
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
Async do
|
|
145
|
-
client.connect
|
|
146
|
-
client.query(content, session_id: claude_session_id ? nil : generate_session_id)
|
|
187
|
+
ClaudeAgentSDK::Client.open(options: build_options) do |client|
|
|
188
|
+
client.query(content)
|
|
147
189
|
|
|
148
190
|
client.receive_response do |message|
|
|
149
191
|
update!(claude_session_id: message.session_id) if message.is_a?(ClaudeAgentSDK::ResultMessage)
|
|
150
192
|
end
|
|
151
|
-
|
|
152
|
-
client.disconnect
|
|
153
|
-
end.wait
|
|
193
|
+
end
|
|
154
194
|
end
|
|
155
195
|
|
|
156
196
|
private
|
|
@@ -160,13 +200,11 @@ class ChatSession < ApplicationRecord
|
|
|
160
200
|
opts[:resume] = claude_session_id if claude_session_id.present?
|
|
161
201
|
ClaudeAgentSDK::ClaudeAgentOptions.new(**opts)
|
|
162
202
|
end
|
|
163
|
-
|
|
164
|
-
def generate_session_id
|
|
165
|
-
"chat_#{id}_#{Time.current.to_i}"
|
|
166
|
-
end
|
|
167
203
|
end
|
|
168
204
|
```
|
|
169
205
|
|
|
206
|
+
The first message starts a new session; every later one resumes it by the ID the previous `ResultMessage` reported.
|
|
207
|
+
|
|
170
208
|
## Background Jobs with Error Handling
|
|
171
209
|
|
|
172
210
|
```ruby
|
|
@@ -176,17 +214,17 @@ class ClaudeAgentJob < ApplicationJob
|
|
|
176
214
|
|
|
177
215
|
def perform(task_id)
|
|
178
216
|
task = Task.find(task_id)
|
|
179
|
-
|
|
217
|
+
|
|
218
|
+
ClaudeAgentSDK::Client.open(options: ClaudeAgentSDK::ClaudeAgentOptions.new(max_turns: 10)) do |client|
|
|
219
|
+
client.query(task.prompt)
|
|
220
|
+
client.receive_response do |message|
|
|
221
|
+
task.update!(status: 'done', result: message.result) if message.is_a?(ClaudeAgentSDK::ResultMessage)
|
|
222
|
+
end
|
|
223
|
+
end
|
|
180
224
|
rescue ClaudeAgentSDK::CLINotFoundError
|
|
181
|
-
task.update!(status: 'failed', error: 'Claude CLI not installed')
|
|
225
|
+
task.update!(status: 'failed', error: 'Claude CLI not installed (bin/rails claude_agent_sdk:install_cli)')
|
|
182
226
|
raise
|
|
183
227
|
end
|
|
184
|
-
|
|
185
|
-
private
|
|
186
|
-
|
|
187
|
-
def execute_agent(task)
|
|
188
|
-
# ... agent execution
|
|
189
|
-
end
|
|
190
228
|
end
|
|
191
229
|
```
|
|
192
230
|
|
|
@@ -250,7 +288,8 @@ ClaudeAgentSDK.configure do |config|
|
|
|
250
288
|
# Use a lambda so each query gets a fresh observer instance (thread-safe).
|
|
251
289
|
# A single shared instance would have its span state clobbered by concurrent requests.
|
|
252
290
|
-> { ClaudeAgentSDK::Instrumentation::OTelObserver.new }
|
|
253
|
-
] : []
|
|
291
|
+
] : [],
|
|
292
|
+
callback_wrapper: ClaudeAgentSDK::Railtie.callback_wrapper
|
|
254
293
|
}
|
|
255
294
|
end
|
|
256
295
|
```
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ClaudeAgentSDK
|
|
4
|
+
# Rails integration. Loaded by lib/claude_agent_sdk.rb only when
|
|
5
|
+
# Rails::Railtie is already defined (Bundler.require runs after
|
|
6
|
+
# `require 'rails'`), so non-Rails processes never see it.
|
|
7
|
+
#
|
|
8
|
+
# Deliberately minimal: it contributes the `claude_agent_sdk:*` rake tasks
|
|
9
|
+
# and nothing else. It installs nothing into callback dispatch — the
|
|
10
|
+
# generated initializer (`bin/rails g claude_agent_sdk:install`) opts in
|
|
11
|
+
# to {.callback_wrapper} explicitly, where it is visible and removable.
|
|
12
|
+
class Railtie < ::Rails::Railtie
|
|
13
|
+
rake_tasks do
|
|
14
|
+
load File.expand_path('tasks/claude_agent_sdk.rake', __dir__)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# A `callback_wrapper` (see ClaudeAgentOptions#callback_wrapper) that
|
|
18
|
+
# gives SDK callbacks Rails' connection hygiene without deadlocking
|
|
19
|
+
# development code reloading.
|
|
20
|
+
#
|
|
21
|
+
# The obvious wrapper, `->(inv) { Rails.application.executor.wrap { inv.call } }`,
|
|
22
|
+
# deadlocks whenever the executor carries a process-wide lock:
|
|
23
|
+
#
|
|
24
|
+
# - With code reloading enabled, every executor holds a share of the
|
|
25
|
+
# ActiveSupport::Dependencies interlock. In the default `:thread`
|
|
26
|
+
# scheduling the request/job thread (already inside the executor, so
|
|
27
|
+
# already holding a share) blocks on the FiberBoundary thread running
|
|
28
|
+
# the callback. If a reload starts meanwhile (another request after the
|
|
29
|
+
# agent edited an app file), the reloader queues for the exclusive
|
|
30
|
+
# unload lock, and the callback thread's own `executor.wrap` then waits
|
|
31
|
+
# behind it for a new share — which the reloader can never get while
|
|
32
|
+
# the parent's share is held. Three-way deadlock.
|
|
33
|
+
# - With `config.allow_concurrency = false`, the executor holds a
|
|
34
|
+
# process-wide monitor that the parent thread already owns, so the
|
|
35
|
+
# callback thread's `executor.wrap` blocks every time.
|
|
36
|
+
#
|
|
37
|
+
# So, per invocation:
|
|
38
|
+
#
|
|
39
|
+
# 1. Executor already active on this execution context (`:inline`
|
|
40
|
+
# scheduling, or a callback running on the caller's own thread) —
|
|
41
|
+
# call straight through; the enclosing executor already owns cleanup.
|
|
42
|
+
# 2. Executor carries a lock (the two cases above, mirroring railties'
|
|
43
|
+
# `configure_executor_for_concurrency`) — call WITHOUT entering the
|
|
44
|
+
# executor, then return this thread's ActiveRecord connections to the
|
|
45
|
+
# pool. The caller's executor still covers the callback: its share of
|
|
46
|
+
# the interlock keeps a reload from unloading code under it until the
|
|
47
|
+
# whole SDK call returns.
|
|
48
|
+
# 3. Otherwise (production: no reloading, concurrency allowed) — run the
|
|
49
|
+
# callback inside `Rails.application.executor.wrap`.
|
|
50
|
+
#
|
|
51
|
+
# The configuration is read on every call, so the same wrapper is correct
|
|
52
|
+
# in every environment.
|
|
53
|
+
#
|
|
54
|
+
# @return [Proc] a callable suitable for `callback_wrapper:`
|
|
55
|
+
# @example config/initializers/claude_agent_sdk.rb
|
|
56
|
+
# ClaudeAgentSDK.configure do |config|
|
|
57
|
+
# config.default_options = { callback_wrapper: ClaudeAgentSDK::Railtie.callback_wrapper }
|
|
58
|
+
# end
|
|
59
|
+
def self.callback_wrapper
|
|
60
|
+
lambda do |invocation|
|
|
61
|
+
app = ::Rails.application
|
|
62
|
+
next invocation.call if app.nil? || app.executor.active?
|
|
63
|
+
next app.executor.wrap { invocation.call } unless executor_locks?(app.config)
|
|
64
|
+
|
|
65
|
+
begin
|
|
66
|
+
invocation.call
|
|
67
|
+
ensure
|
|
68
|
+
release_active_record_connections
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Whether railties registered a process-wide lock hook on the executor
|
|
74
|
+
# (Rails::Application::Finisher, initializer
|
|
75
|
+
# :configure_executor_for_concurrency).
|
|
76
|
+
def self.executor_locks?(config)
|
|
77
|
+
return true if config.allow_concurrency == false
|
|
78
|
+
return false if config.allow_concurrency == :unsafe
|
|
79
|
+
|
|
80
|
+
config.reloading_enabled?
|
|
81
|
+
end
|
|
82
|
+
private_class_method :executor_locks?
|
|
83
|
+
|
|
84
|
+
# What the executor's ActiveRecord completion hook does for a callback
|
|
85
|
+
# that ran on a thread of its own. Explicit :all — the no-argument form
|
|
86
|
+
# is deprecated on Rails 7.1.
|
|
87
|
+
def self.release_active_record_connections
|
|
88
|
+
return unless defined?(::ActiveRecord::Base)
|
|
89
|
+
|
|
90
|
+
::ActiveRecord::Base.connection_handler.clear_active_connections!(:all)
|
|
91
|
+
end
|
|
92
|
+
private_class_method :release_active_record_connections
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# claude_agent_sdk:* tasks. Loaded by ClaudeAgentSDK::Railtie in Rails apps
|
|
4
|
+
# and by `require 'claude_agent_sdk/tasks'` from a plain Rakefile. Guarded
|
|
5
|
+
# because Rake appends the actions of a task defined twice — an app that
|
|
6
|
+
# does both would otherwise install twice.
|
|
7
|
+
unless Rake::Task.task_defined?('claude_agent_sdk:install_cli')
|
|
8
|
+
namespace :claude_agent_sdk do
|
|
9
|
+
desc 'Install the Claude Code CLI into vendor/claude: the version this gem is tested with, ' \
|
|
10
|
+
'or CLAUDE_CLI_VERSION=x.y.z / stable / latest'
|
|
11
|
+
task :install_cli do
|
|
12
|
+
# Under Rails, anchor to the app root instead of the process cwd (the
|
|
13
|
+
# app is not booted: no :environment dependency, so this runs in a
|
|
14
|
+
# Docker build without credentials). Resolved when the task runs.
|
|
15
|
+
root = Rails.root if defined?(Rails) && Rails.respond_to?(:root)
|
|
16
|
+
dir = root&.join('vendor', 'claude')&.to_s
|
|
17
|
+
# Not the conventional rake `VERSION`: Rails' own db:migrate uses it and
|
|
18
|
+
# build environments often export it for the app's version or git SHA.
|
|
19
|
+
version = ENV.fetch('CLAUDE_CLI_VERSION', '').strip
|
|
20
|
+
|
|
21
|
+
path = if version.empty?
|
|
22
|
+
ClaudeAgentSDK::CLIInstaller.install_pinned(dir: dir)
|
|
23
|
+
else
|
|
24
|
+
ClaudeAgentSDK::CLIInstaller.install(version: version, dir: dir)
|
|
25
|
+
end
|
|
26
|
+
label = version.empty? ? ClaudeAgentSDK::CLIInstaller::PINNED_CLI_VERSION : version
|
|
27
|
+
puts "Claude Code CLI (#{label}) installed at #{path}"
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Rake tasks for apps that don't use Rails (where the Railtie loads them).
|
|
4
|
+
# From a plain Rakefile:
|
|
5
|
+
#
|
|
6
|
+
# require 'claude_agent_sdk/tasks'
|
|
7
|
+
#
|
|
8
|
+
# then `rake claude_agent_sdk:install_cli` (e.g. in a Dockerfile). Only the
|
|
9
|
+
# stdlib-only CLIInstaller is loaded, not the whole SDK.
|
|
10
|
+
require 'rake'
|
|
11
|
+
require_relative 'cli_installer'
|
|
12
|
+
|
|
13
|
+
load File.expand_path('tasks/claude_agent_sdk.rake', __dir__)
|
|
@@ -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, &render)
|
|
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(&render)
|
|
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) {
|
|
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
|
|
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
|
|
data/lib/claude_agent_sdk.rb
CHANGED
|
@@ -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
|
|
|
@@ -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,10 +1,10 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: claude-agent-sdk
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.35.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
|
-
-
|
|
7
|
+
- ya-luotao
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
@@ -106,9 +106,14 @@ dependencies:
|
|
|
106
106
|
- - "~>"
|
|
107
107
|
- !ruby/object:Gem::Version
|
|
108
108
|
version: 1.87.0
|
|
109
|
-
description: Unofficial Ruby SDK for
|
|
110
|
-
|
|
111
|
-
|
|
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
|
|
112
117
|
executables: []
|
|
113
118
|
extensions: []
|
|
114
119
|
extra_rdoc_files: []
|
|
@@ -141,6 +146,7 @@ files:
|
|
|
141
146
|
- lib/claude_agent_sdk/observer.rb
|
|
142
147
|
- lib/claude_agent_sdk/option_warnings.rb
|
|
143
148
|
- lib/claude_agent_sdk/query.rb
|
|
149
|
+
- lib/claude_agent_sdk/railtie.rb
|
|
144
150
|
- lib/claude_agent_sdk/sdk_mcp_server.rb
|
|
145
151
|
- lib/claude_agent_sdk/session_mutations.rb
|
|
146
152
|
- lib/claude_agent_sdk/session_resume.rb
|
|
@@ -149,11 +155,15 @@ files:
|
|
|
149
155
|
- lib/claude_agent_sdk/sessions.rb
|
|
150
156
|
- lib/claude_agent_sdk/streaming.rb
|
|
151
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
|
|
152
160
|
- lib/claude_agent_sdk/testing/session_store_conformance.rb
|
|
153
161
|
- lib/claude_agent_sdk/transcript_mirror_batcher.rb
|
|
154
162
|
- lib/claude_agent_sdk/transport.rb
|
|
155
163
|
- lib/claude_agent_sdk/types.rb
|
|
156
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
|
|
157
167
|
homepage: https://github.com/ya-luotao/claude-agent-sdk-ruby
|
|
158
168
|
licenses:
|
|
159
169
|
- MIT
|
|
@@ -181,5 +191,5 @@ requirements: []
|
|
|
181
191
|
rubygems_version: 3.4.19
|
|
182
192
|
signing_key:
|
|
183
193
|
specification_version: 4
|
|
184
|
-
summary: Unofficial Ruby SDK for Claude Agent
|
|
194
|
+
summary: Unofficial Ruby SDK for Claude Agent, with Rails integration
|
|
185
195
|
test_files: []
|