claude-agent-sdk 0.34.0 → 0.36.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +80 -0
- data/README.md +68 -24
- data/docs/cli-installer.md +38 -1
- data/docs/client.md +44 -20
- data/docs/errors.md +15 -1
- data/docs/hooks-and-permissions.md +22 -0
- data/docs/mcp-servers.md +37 -7
- data/docs/rails.md +92 -54
- data/docs/sessions.md +69 -33
- data/lib/claude_agent_sdk/cancellation_signal.rb +2 -1
- data/lib/claude_agent_sdk/cli_installer.rb +38 -8
- data/lib/claude_agent_sdk/deprecation.rb +51 -0
- data/lib/claude_agent_sdk/errors.rb +8 -0
- data/lib/claude_agent_sdk/fiber_boundary.rb +111 -2
- data/lib/claude_agent_sdk/option_warnings.rb +0 -2
- data/lib/claude_agent_sdk/query.rb +49 -8
- data/lib/claude_agent_sdk/railtie.rb +105 -0
- data/lib/claude_agent_sdk/sdk_mcp_server.rb +36 -25
- data/lib/claude_agent_sdk/session_mutations.rb +10 -10
- data/lib/claude_agent_sdk/session_resume.rb +19 -24
- data/lib/claude_agent_sdk/session_store.rb +28 -18
- data/lib/claude_agent_sdk/session_summary.rb +8 -3
- data/lib/claude_agent_sdk/sessions.rb +104 -18
- data/lib/claude_agent_sdk/subprocess_cli_transport.rb +4 -13
- data/lib/claude_agent_sdk/tasks/claude_agent_sdk.rake +37 -0
- data/lib/claude_agent_sdk/tasks.rb +13 -0
- data/lib/claude_agent_sdk/testing/session_store_conformance.rb +1 -1
- data/lib/claude_agent_sdk/transcript_mirror_batcher.rb +17 -1
- data/lib/claude_agent_sdk/types.rb +219 -3
- data/lib/claude_agent_sdk/version.rb +1 -1
- data/lib/claude_agent_sdk.rb +261 -56
- 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 +17 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: aed11255947a3321978d4d9e65cf02ca0d88a6f8b2c9e61afede1de95c247b76
|
|
4
|
+
data.tar.gz: e5d54c2745b1264198a25569599c4f34ac6817778914f450d7aaca8ddb7e99d5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b5c4ce0e5b2a8594eafd236fcdd4d8592cf0f900b8eede62e7e680b50d3560ea99d04180e3b42550e863a95225e9cac191fe50e8baa7c376413832b051e1900b
|
|
7
|
+
data.tar.gz: b45fb3972949da75f83f845244e72b74fefcc88acf57c04ed89cbdde896a7e50eda69853a3430db88163259afe88862da0c598083745a3b9778f418ec001bf2d
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,86 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.36.0] - 2026-09-23
|
|
11
|
+
|
|
12
|
+
The first step on the [road to 1.0](https://github.com/ya-luotao/claude-agent-sdk-ruby/issues/126): one `session_store:` argument for every session function (the store-specific twins are deprecated), `ClaudeAgentSDK.ask`, String tool results, and the last audit follow-ups (#119–#121). **Read before upgrading:**
|
|
13
|
+
- An `exit`, `Interrupt` or signal raised inside a hook, `can_use_tool` or SDK MCP handler now **ends the process after the CLI gets its error response**. Since 0.34.0 a tool handler's `exit` was turned into an `isError` result and the process kept running, which also swallowed a real Ctrl-C or SIGTERM in `:inline` mode.
|
|
14
|
+
- The `*_from_store` / `*_via_store` session functions print a one-time deprecation warning; switch to `list_sessions(session_store: store)` etc. (table under **Deprecated**).
|
|
15
|
+
- Local-disk session APIs raise `ConfigDirError` (not `ArgumentError`) on hosts without a home directory; a session with no prompt has `first_prompt` `nil` on the disk path too (was `''`).
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- **SDK MCP tool handlers may return a String.** `create_tool('greet', ...) { |args| "Hello, #{args[:name]}!" }` now sends Claude a single text block, the same as returning `{ content: [{ type: 'text', text: "Hello, ..." }] }`. Both dispatch paths (`tools/call` through the MCP server and the direct `SdkMcpServer#call_tool`) accept it. Hash returns behave exactly as before and remain the form for `is_error`, `structured_content`, images and several blocks; any other non-Hash return still produces the in-band "must return a hash with :content key" error. The `create_tool` YARD examples, README and `docs/mcp-servers.md` (new "Handler Return Values" section) lead with the String form.
|
|
19
|
+
- **`ClaudeAgentSDK.ask(prompt, options: nil)`** — runs `query` to completion and returns the final `ResultMessage`, so `ClaudeAgentSDK.ask("What is 2 + 2?").result` is the answer text, with cost, usage and `session_id` on the same object. It is `query` underneath: same prompt types (String or Enumerable), same `options:` and `transport:`, same errors (a terminal error exit still raises `ResultError`; an `is_error` result that is not followed by an error exit is returned like any other). An optional block receives every message as it arrives, so callers can stream progress and still get the result. It consumes the whole stream and returns the last `ResultMessage`; if the stream ends without one it raises `CLIConnectionError`. The README Quick Start now leads with it.
|
|
20
|
+
- **Ruby-style `Client` method names next to the Python-parity ones**: `client.model = 'haiku'` (`set_model`), `client.permission_mode = 'plan'` (`set_permission_mode`), `client.context_usage` (`get_context_usage`) and `client.mcp_status` (`get_mcp_status`). Each delegates to its parity counterpart, so both spellings send the same control request, raise the same `CLIConnectionError` before `connect`, and an override of the parity method covers both. The parity names stay, so code ported from the Python docs keeps working. (`server_info` / `get_server_info` already existed as a pair.) `docs/client.md` lists both spellings.
|
|
21
|
+
- **`CLIInstaller.root` — a configurable root for the vendored CLI.** `CLIInstaller.default_dir` (and so `install`, `install_pinned`, `installed_path` without `dir:`, and the transport's discovery of the vendored binary) is `vendor/claude` under `CLIInstaller.root` when it is set, and under the working directory at call time when it is `nil`, the default. A process whose cwd is not the project root, such as a daemonized worker, can now find the vendored binary instead of falling through to `PATH`. The setter takes a String or Pathname, absolutizes it once, and rejects an empty path; `nil` restores the working-directory default. Unset, nothing changes, and an explicit `dir:` still wins.
|
|
22
|
+
- **The Railtie anchors CLI discovery to `Rails.root`**: an initializer sets `CLIInstaller.root ||= Rails.root` before `config/initializers` run, so an app initializer can override it and a root set in `config/application.rb` is kept. The `claude_agent_sdk:install_cli` task, which does not boot the app, installs under an explicitly set `CLIInstaller.root` if there is one, and under `Rails.root/vendor/claude` as before otherwise. The generated initializer's commented `cli_path:` line no longer describes a cwd workaround. `docs/cli-installer.md` gains a "Where `vendor/claude` is" section.
|
|
23
|
+
Groundwork for 1.0 ([#126](https://github.com/ya-luotao/claude-agent-sdk-ruby/issues/126)): the sessions API collapses to one function per operation, and the store-specific twins are deprecated. Nothing is removed; every existing call keeps working.
|
|
24
|
+
- **`session_store:` on every session function.** `list_sessions`, `get_session_info`, `get_session_messages`, `list_subagents`, `get_subagent_metadata`, `get_subagent_messages`, `rename_session`, `tag_session`, `delete_session` and `fork_session` take an optional `session_store:`. Omitted or `nil`, they work on local disk exactly as before; given a store, they run the same code the `*_from_store` / `*_via_store` function did, with the same arguments. Two differences between the paths are documented in `docs/sessions.md`: `directory: nil` means every project on disk but the current working directory with a store (a store cannot enumerate projects), and `include_worktrees:` is disk-only: with `session_store:`, `list_sessions` accepts only the default `true` and raises `ArgumentError` for `false` or `nil` instead of silently ignoring the filter.
|
|
25
|
+
- **`ClaudeAgentSDK::ConfigDirError`** (a `ClaudeSDKError`), raised by the local-disk session APIs when the Claude config directory cannot be located: `CLAUDE_CONFIG_DIR` is unset and there is no usable home directory for the default `~/.claude`. Its message says to set `CLAUDE_CONFIG_DIR` (#120).
|
|
26
|
+
- CI: a macOS leg (Ruby 3.4) for the main suite; simplecov coverage (`COVERAGE=1 bundle exec rspec`) on the Linux Ruby 3.4 leg, with line/branch totals in the job summary and the HTML report as an artifact; and a weekly real-CLI integration run (`.github/workflows/integration.yml`) against `CLIInstaller::PINNED_CLI_VERSION`, also triggered by PRs that touch the installer. Dependabot keeps the workflows' actions current.
|
|
27
|
+
- `CONTRIBUTING.md`, `SECURITY.md`, and issue and pull request templates.
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
- **`exit` / `Interrupt` / signal exceptions from an SDK MCP tool handler terminate the process again, after the CLI gets its `isError` result.** Since #77 (0.34.0) they were converted into an `isError` result and the process kept running. That also swallowed a real Ctrl-C or `SIGTERM` landing in an `:inline` handler. The `isError` text now names the exception class (`"SystemExit: exit"` rather than `"exit"`). `SdkMcpServer#call_tool` and `#handle_message`, called directly without a session, let these exceptions propagate instead of returning an `isError` result. (#119)
|
|
31
|
+
- **Root-module plumbing is tagged `@api private`** and no longer appears in the generated YARD docs (`.yardopts` gains `--hide-api private`; `--no-private` alone never hid `@api private` objects): `resolve_observers`, `extract_sdk_mcp_servers`, `convert_hooks_to_internal_format`, `configure_can_use_tool`, `extract_exclude_dynamic_sections`, `extract_system_prompt_snapshot`, `notify_observers`, `check_inline_isolation`, `extract_user_prompt_text`, `prompt_text_from_content`, `observing_prompt_stream`, `flexible_fetch`, `normalize_tool_result`, and the tool-schema helpers `deep_symbolize_keys`, `deep_normalize_schema`, `prebuilt_json_schema?`, `normalize_tool_schema`, `ruby_type_to_json_schema`. They still work, but they are not part of the public API and move under `ClaudeAgentSDK::Internal` in 1.0. `fold_session_summary` stays public, since SessionStore adapters call it from `#append`. The existing `@api private` tags (`FiberBoundary` internals, `CancellationSignal#cancel`) are hidden too; `#cancel`'s tag is moved to its own line, where YARD recognizes it.
|
|
32
|
+
- `examples/rails_actioncable_example.rb` and `examples/rails_background_job_example.rb` use `ClaudeAgentSDK::Client.open` instead of hand-rolled `Async { connect … ensure disconnect }.wait`, matching `docs/rails.md`.
|
|
33
|
+
- RuboCop targets Ruby 3.2, the gemspec floor (was 3.0). The resulting autocorrections (anonymous block forwarding, dropping `require 'set'`) change no behavior.
|
|
34
|
+
|
|
35
|
+
### Deprecated
|
|
36
|
+
- **The ten store-specific session functions**, removed in 1.0. Each still returns exactly what it did before (a `nil` `session_store:` still fails rather than falling back to disk), and prints a one-time warning per method per process naming its replacement and the calling line, e.g. `app/jobs/sync.rb:12: warning: ClaudeAgentSDK.list_sessions_from_store is deprecated and will be removed in 1.0; use ClaudeAgentSDK.list_sessions(session_store: store)`. The warning uses plain `Kernel#warn`, not `category: :deprecated`, because Ruby hides that category unless `Warning[:deprecated]` is enabled; `-W0` / `$VERBOSE = nil` silences it.
|
|
37
|
+
|
|
38
|
+
| Deprecated | Replacement |
|
|
39
|
+
|---|---|
|
|
40
|
+
| `list_sessions_from_store(session_store: s, ...)` | `list_sessions(session_store: s, ...)` |
|
|
41
|
+
| `get_session_info_from_store(session_store: s, ...)` | `get_session_info(session_store: s, ...)` |
|
|
42
|
+
| `get_session_messages_from_store(session_store: s, ...)` | `get_session_messages(session_store: s, ...)` |
|
|
43
|
+
| `list_subagents_from_store(session_store: s, ...)` | `list_subagents(session_store: s, ...)` |
|
|
44
|
+
| `get_subagent_metadata_from_store(session_store: s, ...)` | `get_subagent_metadata(session_store: s, ...)` |
|
|
45
|
+
| `get_subagent_messages_from_store(session_store: s, ...)` | `get_subagent_messages(session_store: s, ...)` |
|
|
46
|
+
| `rename_session_via_store(session_store: s, ...)` | `rename_session(session_store: s, ...)` |
|
|
47
|
+
| `tag_session_via_store(session_store: s, ...)` | `tag_session(session_store: s, ...)` |
|
|
48
|
+
| `delete_session_via_store(session_store: s, ...)` | `delete_session(session_store: s, ...)` |
|
|
49
|
+
| `fork_session_via_store(session_store: s, ...)` | `fork_session(session_store: s, ...)` |
|
|
50
|
+
|
|
51
|
+
All other arguments carry over unchanged. `import_session_to_store` is not affected.
|
|
52
|
+
|
|
53
|
+
### Fixed
|
|
54
|
+
- **`exit`, `Interrupt` and signal exceptions raised while a hook, `can_use_tool`, or an SDK MCP resource reader / prompt generator runs no longer leave the CLI without an answer.** The SDK now responds first, then re-raises. The CLI gets exactly the response an ordinary exception from that callback produces, naming the exception class (`"SystemExit: exit"`, `"Interrupt"`, `"SignalException: SIGTERM"`). For hooks and `can_use_tool` that is the error control response; for `resources/read` / `prompts/get` it is a JSON-RPC `-32603` error. The transport flushes that response, and then the original exception propagates, so the process terminates as plain Ruby would: `exit 3` exits with status 3, and Ctrl-C interrupts. Previously no response was written and the reactor stopped. In the default `:thread` scheduling, Ruby also re-raised the worker thread's `SystemExit` on the main thread, which answered with a misleading `Cancelled`. This covers every dispatch site: `can_use_tool`, hooks with and without a `HookMatcher#timeout` (both variants), resources, prompts and tool handlers. It also covers a real Ctrl-C / `SIGTERM` that MRI delivers to the main thread while an `:inline` callback runs there, and a `:thread` hook that calls `exit` after its timeout expired (only `exit` is carried out of such an abandoned worker: an `Interrupt` or signal exception it raises ends that thread alone, as in plain Ruby). Cancellation (`Async::Stop`, hook timeouts) still propagates unchanged. A `callback_wrapper` sees an internal `StandardError` carrier whose `#cause` is the original, so ensure-based wrappers still clean up; the original is re-raised even if the wrapper swallows the carrier. Observers and message blocks are unchanged. (#119)
|
|
55
|
+
- **Session APIs on hosts without a home directory (#120).** With `CLAUDE_CONFIG_DIR` unset and `HOME` unset with no passwd entry (`docker --user` in a minimal image) or an empty/relative `HOME`:
|
|
56
|
+
- the local-disk session APIs (`list_sessions`, `get_session_*`, `list_subagents`, `rename_session` / `tag_session` / `delete_session` / `fork_session`, `import_session_to_store`) raise `ConfigDirError` instead of a bare `ArgumentError` from `~` expansion;
|
|
57
|
+
- a fresh session with a `session_store` no longer fails at connect. The transcript mirror cannot map the CLI's transcript files to store keys without a projects dir, so each unmappable batch is reported as a `MirrorErrorMessage` (with a `nil` key) and counted as dropped, while the session itself runs normally. `SessionStores.projects_dir` returns `nil` in this case instead of raising.
|
|
58
|
+
- **Store-backed resume seeds auth and settings from the home the CLI subprocess will use (#120).** When `options.env` sets `HOME`, `.credentials.json`, `settings.json` / `cowork_settings.json` and `.claude.json` are now read from under that home, as `CLAUDE_CONFIG_DIR` already was, instead of the parent process's home. An empty or relative `HOME` there, or `HOME => nil`, counts as no home, so those files are skipped. The transcript mirror resolves the subprocess's default `~/.claude/projects` the same way, so a `HOME` override no longer sends every mirror frame down the "not under projects dir" drop path.
|
|
59
|
+
- **Remaining disk/store session read inconsistencies (#121):**
|
|
60
|
+
- Store listings report `SDKSessionInfo#last_modified` as Integer epoch milliseconds, as documented, whatever shape the adapter's `mtime` has (ISO-8601 String, numeric String, Float, or now `Time`, e.g. an ActiveRecord `updated_at`). Previously only the ordering was coerced and the raw value leaked through. An unusable mtime (including non-finite numbers) reads as `0`, the value it already sorted by.
|
|
61
|
+
- `continue_conversation` with a `session_store` breaks equal-mtime ties by `session_id`, like the listings, instead of resuming whichever session the adapter listed first.
|
|
62
|
+
- `first_prompt` is `nil` on the disk path too (it was `''`) when a session has no prompt, matching the store path and the Python SDK.
|
|
63
|
+
- `cwd`: both paths take the first non-blank top-level `cwd`. The disk path took the first `cwd` even when empty (then fell back to the project path) and also matched `cwd` keys nested in tool inputs; the store fold now also skips whitespace-only values, so a sidecar no longer locks on one.
|
|
64
|
+
- `rename_session` / `tag_session` / `delete_session` / `fork_session` and their `_via_store` counterparts validate `session_id` (and `up_to_message_id`) at the boundary like the readers: a non-String id raises `ArgumentError` ("Invalid session_id") instead of `NoMethodError`.
|
|
65
|
+
- `list_sessions` deduplicates a session found in several project directories deterministically: newest `last_modified`, then the larger file, then the project directory that sorts first (the global scan now walks project directories in name order). Equal mtimes used to keep whichever copy the filesystem listed first.
|
|
66
|
+
|
|
67
|
+
## [0.35.0] - 2026-09-23
|
|
68
|
+
|
|
69
|
+
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**).
|
|
70
|
+
|
|
71
|
+
### Added
|
|
72
|
+
- **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.
|
|
73
|
+
- **`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.
|
|
74
|
+
- **`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.
|
|
75
|
+
- **`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+.
|
|
76
|
+
- 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`.
|
|
77
|
+
- **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.
|
|
78
|
+
- **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.
|
|
79
|
+
|
|
80
|
+
### Changed
|
|
81
|
+
- `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.
|
|
82
|
+
- **`#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`.
|
|
83
|
+
- 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.
|
|
84
|
+
- The bundled `claude-agent-ruby` skill recommends `Client.open` and documents the install generator, the `install_cli` task and `Railtie.callback_wrapper`.
|
|
85
|
+
- **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.
|
|
86
|
+
|
|
87
|
+
### Fixed
|
|
88
|
+
- **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.
|
|
89
|
+
|
|
10
90
|
## [0.34.0] - 2026-09-23
|
|
11
91
|
|
|
12
92
|
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.36.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,19 +46,52 @@ 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
|
|
50
76
|
require 'claude_agent_sdk'
|
|
51
77
|
|
|
52
|
-
ClaudeAgentSDK.
|
|
78
|
+
puts ClaudeAgentSDK.ask("What is 2 + 2?").result
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
`ask` runs the whole conversation and returns the final `ResultMessage`: `#result` is the answer, and the same object carries `total_cost_usd`, `usage`, `session_id` and `structured_output` (`puts` on it prints a summary such as `[result: success, 1 turn, 2.1s, $0.0031]`). It takes the same prompt and `options:` as `query()`, and given a block it also yields every message as it arrives:
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
result = ClaudeAgentSDK.ask("Explain Ruby's GVL in three sentences") do |message|
|
|
53
85
|
puts message.text if message.is_a?(ClaudeAgentSDK::AssistantMessage)
|
|
54
86
|
end
|
|
87
|
+
puts result
|
|
55
88
|
```
|
|
56
89
|
|
|
90
|
+
It raises the same errors as `query()` (see [docs/errors.md](docs/errors.md)), plus `CLIConnectionError` if the stream ends without a result.
|
|
91
|
+
|
|
57
92
|
### `query()` — one-shot and streaming
|
|
58
93
|
|
|
59
|
-
`query()` runs a single conversation and yields each response message to the block.
|
|
94
|
+
`query()` runs a single conversation and yields each response message to the block. Reach for it over `ask` when you handle the messages yourself, or want to stop early with `break`.
|
|
60
95
|
|
|
61
96
|
```ruby
|
|
62
97
|
options = ClaudeAgentSDK::ClaudeAgentOptions.new(
|
|
@@ -84,23 +119,31 @@ end
|
|
|
84
119
|
|
|
85
120
|
### `Client` — bidirectional sessions
|
|
86
121
|
|
|
87
|
-
`Client` keeps a session open so you can send follow-up queries, interrupt, switch models, and use hooks, permission callbacks, and custom tools.
|
|
122
|
+
`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
123
|
|
|
89
124
|
```ruby
|
|
90
125
|
require 'claude_agent_sdk'
|
|
91
|
-
require 'async'
|
|
92
126
|
|
|
93
|
-
|
|
94
|
-
client
|
|
127
|
+
ClaudeAgentSDK::Client.open do |client|
|
|
128
|
+
client.query("What is the capital of France?")
|
|
129
|
+
client.receive_response { |msg| puts msg }
|
|
95
130
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
131
|
+
client.query("And of Germany?")
|
|
132
|
+
client.receive_response { |msg| puts msg }
|
|
133
|
+
end
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
`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:
|
|
137
|
+
|
|
138
|
+
```ruby
|
|
139
|
+
client = ClaudeAgentSDK::Client.new
|
|
140
|
+
begin
|
|
141
|
+
client.connect
|
|
142
|
+
client.query("What is the capital of France?")
|
|
143
|
+
client.receive_response { |msg| puts msg }
|
|
144
|
+
ensure
|
|
145
|
+
client.disconnect
|
|
146
|
+
end
|
|
104
147
|
```
|
|
105
148
|
|
|
106
149
|
See [docs/client.md](docs/client.md) for `interrupt`, mid-session model and permission switching, MCP status, and custom transports.
|
|
@@ -111,7 +154,7 @@ Tools are Ruby blocks that run in-process, with no subprocess or IPC between Cla
|
|
|
111
154
|
|
|
112
155
|
```ruby
|
|
113
156
|
greet = ClaudeAgentSDK.create_tool('greet', 'Greet a user', { name: :string }) do |args|
|
|
114
|
-
|
|
157
|
+
"Hello, #{args[:name]}!"
|
|
115
158
|
end
|
|
116
159
|
|
|
117
160
|
server = ClaudeAgentSDK.create_sdk_mcp_server(name: 'my-tools', tools: [greet])
|
|
@@ -122,7 +165,7 @@ options = ClaudeAgentSDK::ClaudeAgentOptions.new(
|
|
|
122
165
|
)
|
|
123
166
|
```
|
|
124
167
|
|
|
125
|
-
Arguments are validated against the tool's JSON Schema before your handler runs, and handler exceptions are reported back to the model in-band so it can self-correct. See [docs/mcp-servers.md](docs/mcp-servers.md) for resources, prompts, mixed SDK + external servers, and schema details.
|
|
168
|
+
A String return is sent to Claude as a single text block. Return a Hash instead (`{ content: [...], is_error: true }`) to flag an error, attach `structured_content:`, or send several content blocks or images. Arguments are validated against the tool's JSON Schema before your handler runs, and handler exceptions are reported back to the model in-band so it can self-correct. See [docs/mcp-servers.md](docs/mcp-servers.md) for resources, prompts, mixed SDK + external servers, and schema details.
|
|
126
169
|
|
|
127
170
|
### Hooks and permission callbacks
|
|
128
171
|
|
|
@@ -148,7 +191,7 @@ See [docs/hooks-and-permissions.md](docs/hooks-and-permissions.md) for the full
|
|
|
148
191
|
| Session listing, reading, renaming, tagging, forking, resume-at-message | [docs/sessions.md](docs/sessions.md) |
|
|
149
192
|
| Subagent capabilities, event contracts, and minimal example | [docs/subagents.md](docs/subagents.md) |
|
|
150
193
|
| OpenTelemetry tracing, Langfuse, custom observers | [docs/observability.md](docs/observability.md) |
|
|
151
|
-
| Rails: fiber safety, solid_queue fiber workers, ActionCable, jobs
|
|
194
|
+
| Rails: generator, `install_cli` task, callback wrapper, fiber safety, solid_queue fiber workers, ActionCable, jobs | [docs/rails.md](docs/rails.md) |
|
|
152
195
|
| Vendoring a pinned CLI binary and CLI discovery order | [docs/cli-installer.md](docs/cli-installer.md) |
|
|
153
196
|
| Message, content block, and configuration type reference | [docs/types.md](docs/types.md) |
|
|
154
197
|
| Error handling, exception hierarchy, timeouts | [docs/errors.md](docs/errors.md) |
|
|
@@ -210,13 +253,14 @@ bundle install
|
|
|
210
253
|
bundle exec rspec # unit suite
|
|
211
254
|
bundle exec rubocop # lint
|
|
212
255
|
RUN_INTEGRATION=1 bundle exec rspec # also run the real-CLI integration suite (needs `claude` and ANTHROPIC_API_KEY)
|
|
256
|
+
BUNDLE_GEMFILE=gemfiles/rails_8.gemfile bundle exec rspec --options spec/rails/.rspec # Rails integration specs
|
|
213
257
|
```
|
|
214
258
|
|
|
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.
|
|
259
|
+
CI runs the suite and RuboCop on Ruby 3.2, 3.3, and 3.4 on Linux, the suite on macOS, and the Rails specs against Rails 7.1 and 8; a weekly job runs the integration suite against the pinned CLI. See [CONTRIBUTING.md](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/CONTRIBUTING.md) for the development setup and [spec/README.md](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/spec/README.md) for the test layout.
|
|
216
260
|
|
|
217
261
|
## Contributing
|
|
218
262
|
|
|
219
|
-
Bug reports and pull requests are welcome on [GitHub](https://github.com/ya-luotao/claude-agent-sdk-ruby/issues). Please include a failing spec with bug reports where possible, and keep pull requests focused on one change. Releases follow [Semantic Versioning](https://semver.org/) and are recorded in the [CHANGELOG](CHANGELOG.md).
|
|
263
|
+
Bug reports and pull requests are welcome on [GitHub](https://github.com/ya-luotao/claude-agent-sdk-ruby/issues). Please include a failing spec with bug reports where possible, and keep pull requests focused on one change; [CONTRIBUTING.md](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/CONTRIBUTING.md) has the details. Report security vulnerabilities privately, as described in [SECURITY.md](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/SECURITY.md). Releases follow [Semantic Versioning](https://semver.org/) and are recorded in the [CHANGELOG](CHANGELOG.md).
|
|
220
264
|
|
|
221
265
|
## License
|
|
222
266
|
|
data/docs/cli-installer.md
CHANGED
|
@@ -33,6 +33,20 @@ Failures (unsupported platform, invalid version, HTTP error, response-size cap,
|
|
|
33
33
|
|
|
34
34
|
**A failed install never breaks a working one.** The new binary is downloaded to a temp file, checksum-verified and recorded, and only then renamed into place — the rename is the last step, and nothing can fail after it. So a failed upgrade leaves the previously installed binary intact and runnable (the SDK keeps working), and the next `install` redoes it cleanly. A first install that fails leaves nothing behind at all.
|
|
35
35
|
|
|
36
|
+
## Where `vendor/claude` is
|
|
37
|
+
|
|
38
|
+
With no `dir:`, `install`, `install_pinned` and `installed_path` use `CLIInstaller.default_dir`: `vendor/claude` under `CLIInstaller.root`, or under the process's working directory at call time while `root` is unset (the default). Transport discovery uses the same directory, so installing and finding the binary agree.
|
|
39
|
+
|
|
40
|
+
Set `root` when a process that runs agents does not start in the project root — a daemonized worker, a job runner launched from `/`, a systemd unit without `WorkingDirectory=`. Otherwise that process looks for `vendor/claude` under its own working directory, misses the vendored binary, and falls through to whatever `claude` is on `PATH`:
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
# early in boot, before the first query
|
|
44
|
+
ClaudeAgentSDK::CLIInstaller.root = '/srv/myapp' # a String or a Pathname
|
|
45
|
+
ClaudeAgentSDK::CLIInstaller.default_dir # => "/srv/myapp/vendor/claude"
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
A relative path is resolved against the working directory once, when you set it. `nil` restores the working-directory default. In a Rails app you don't need this line: the Railtie sets `root` to `Rails.root` during boot (see [docs/rails.md](rails.md)). An explicit `dir:` argument always wins over `root`.
|
|
49
|
+
|
|
36
50
|
> The vendored directory is trusted input: anything that can write to it can replace the binary the SDK executes. Keep it inside your deploy artifact, owned by the deploy user and not world-writable, exactly as you would treat `bin/`.
|
|
37
51
|
|
|
38
52
|
## Docker and `bin/setup`
|
|
@@ -51,6 +65,29 @@ version = ENV.fetch('CLAUDE_CLI_VERSION', ClaudeAgentSDK::CLIInstaller::PINNED_C
|
|
|
51
65
|
puts ClaudeAgentSDK::CLIInstaller.install(version: version)
|
|
52
66
|
```
|
|
53
67
|
|
|
68
|
+
## Rake task
|
|
69
|
+
|
|
70
|
+
Rails apps get `claude_agent_sdk:install_cli` from the gem's Railtie; any other project can load it from its `Rakefile`:
|
|
71
|
+
|
|
72
|
+
```ruby
|
|
73
|
+
# Rakefile (non-Rails)
|
|
74
|
+
require 'claude_agent_sdk/tasks' # loads only CLIInstaller, not the whole SDK
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
bin/rails claude_agent_sdk:install_cli # Rails: installs PINNED_CLI_VERSION into Rails.root/vendor/claude
|
|
79
|
+
rake claude_agent_sdk:install_cli # elsewhere: into CLIInstaller.default_dir (vendor/claude under the working directory unless root is set)
|
|
80
|
+
rake claude_agent_sdk:install_cli CLAUDE_CLI_VERSION=x.y.z # a version of your own, or 'stable' / 'latest'
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
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:
|
|
84
|
+
|
|
85
|
+
```dockerfile
|
|
86
|
+
RUN bin/rails claude_agent_sdk:install_cli
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
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.
|
|
90
|
+
|
|
54
91
|
## Supported platforms
|
|
55
92
|
|
|
56
93
|
`darwin-arm64`, `darwin-x64` (Rosetta 2 gets the arm64 build), `linux-x64`, `linux-arm64`, and the `-musl` variants. Windows is not supported.
|
|
@@ -60,6 +97,6 @@ puts ClaudeAgentSDK::CLIInstaller.install(version: version)
|
|
|
60
97
|
With no explicit `cli_path:` in `ClaudeAgentOptions`, the transport probes in this order:
|
|
61
98
|
|
|
62
99
|
1. `CLAUDE_CLI_PATH` — an explicit path to an executable, no discovery at all (a relative value is resolved against the process's working directory, not `cwd:`)
|
|
63
|
-
2. The vendored binary (`CLIInstaller.installed_path`) — deliberately ahead of `PATH`, so a pinned install beats whatever is installed globally
|
|
100
|
+
2. The vendored binary (`CLIInstaller.installed_path`, i.e. `vendor/claude` under `CLIInstaller.root` or the working directory — see [above](#where-vendorclaude-is)) — deliberately ahead of `PATH`, so a pinned install beats whatever is installed globally
|
|
64
101
|
3. `which claude`
|
|
65
102
|
4. Common install locations (`~/.claude/local/claude`, `/usr/local/bin/claude`, …) — only an executable regular file counts, and the `~` ones are skipped when there is no usable home directory (HOME unset with no passwd entry, or a non-absolute HOME)
|
data/docs/client.md
CHANGED
|
@@ -2,31 +2,42 @@
|
|
|
2
2
|
|
|
3
3
|
`ClaudeAgentSDK::Client` supports bidirectional, interactive conversations with Claude Code. Unlike `query()`, `Client` enables **custom tools**, **hooks**, and **permission callbacks**, all of which can be defined as Ruby procs/lambdas. The Client class automatically uses streaming mode for bidirectional communication, allowing you to send multiple queries dynamically during a single session without closing the connection.
|
|
4
4
|
|
|
5
|
+
For a single question you don't need a session: `ClaudeAgentSDK.ask(prompt, options:)` runs `query()` to completion and returns the final `ResultMessage` (`#result` is the answer text), optionally yielding each message to a block on the way. See the README's [Quick Start](../README.md#quick-start).
|
|
6
|
+
|
|
5
7
|
## Basic Usage
|
|
6
8
|
|
|
9
|
+
`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.
|
|
10
|
+
|
|
7
11
|
```ruby
|
|
8
12
|
require 'claude_agent_sdk'
|
|
9
|
-
require 'async'
|
|
10
|
-
|
|
11
|
-
Async do
|
|
12
|
-
client = ClaudeAgentSDK::Client.new
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
client.query("What is the capital of France?")
|
|
14
|
+
ClaudeAgentSDK::Client.open do |client|
|
|
15
|
+
client.query("What is the capital of France?")
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
end
|
|
17
|
+
client.receive_response do |msg|
|
|
18
|
+
case msg
|
|
19
|
+
when ClaudeAgentSDK::AssistantMessage
|
|
20
|
+
puts msg.text
|
|
21
|
+
when ClaudeAgentSDK::ResultMessage
|
|
22
|
+
puts "Cost: $#{msg.total_cost_usd}" if msg.total_cost_usd
|
|
25
23
|
end
|
|
26
|
-
ensure
|
|
27
|
-
client.disconnect
|
|
28
24
|
end
|
|
29
|
-
end
|
|
25
|
+
end
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
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.
|
|
29
|
+
|
|
30
|
+
If your code already runs inside an `Async` reactor and you want to manage the connection yourself, call `connect` and `disconnect` directly:
|
|
31
|
+
|
|
32
|
+
```ruby
|
|
33
|
+
client = ClaudeAgentSDK::Client.new
|
|
34
|
+
begin
|
|
35
|
+
client.connect
|
|
36
|
+
client.query("What is the capital of France?")
|
|
37
|
+
client.receive_response { |msg| puts msg }
|
|
38
|
+
ensure
|
|
39
|
+
client.disconnect
|
|
40
|
+
end
|
|
30
41
|
```
|
|
31
42
|
|
|
32
43
|
## Advanced Features
|
|
@@ -37,9 +48,10 @@ Async do
|
|
|
37
48
|
client.connect
|
|
38
49
|
|
|
39
50
|
client.interrupt # Send interrupt signal
|
|
40
|
-
client.
|
|
41
|
-
client.
|
|
42
|
-
|
|
51
|
+
client.permission_mode = 'acceptEdits' # Change permission mode mid-conversation
|
|
52
|
+
client.model = 'claude-sonnet-5' # Switch model mid-conversation (nil = default)
|
|
53
|
+
usage = client.context_usage # Context window usage by category
|
|
54
|
+
status = client.mcp_status # Inspect MCP server status
|
|
43
55
|
info = client.get_server_info # Inspect server init info
|
|
44
56
|
client.reconnect_mcp_server('my-server') # Reconnect a failed MCP server
|
|
45
57
|
client.toggle_mcp_server('my-server', false) # Enable/disable an MCP server
|
|
@@ -53,6 +65,18 @@ Async do
|
|
|
53
65
|
end.wait
|
|
54
66
|
```
|
|
55
67
|
|
|
68
|
+
The Ruby-style names above sit next to the Python SDK's spellings, and both work, so code ported from the Python docs runs unchanged:
|
|
69
|
+
|
|
70
|
+
| Ruby style | Python-parity name |
|
|
71
|
+
|------------|--------------------|
|
|
72
|
+
| `client.model = 'haiku'` | `client.set_model('haiku')` |
|
|
73
|
+
| `client.permission_mode = 'plan'` | `client.set_permission_mode('plan')` |
|
|
74
|
+
| `client.context_usage` | `client.get_context_usage` |
|
|
75
|
+
| `client.mcp_status` | `client.get_mcp_status` |
|
|
76
|
+
| `client.server_info` | `client.get_server_info` |
|
|
77
|
+
|
|
78
|
+
Each Ruby-style method calls its parity counterpart, so both send the same control request and raise `CLIConnectionError` when the client is not connected. The one exception is `server_info`, which reads the cached initialization result and returns `nil` instead of raising before `connect`. As with any Ruby setter, `client.model = 'haiku'` evaluates to `'haiku'`, not to the control response.
|
|
79
|
+
|
|
56
80
|
## Custom Transport
|
|
57
81
|
|
|
58
82
|
By default, `Client` uses `SubprocessCLITransport` to spawn the Claude Code CLI locally. You can provide a custom transport class to connect via other channels (e.g., remote SSH, WebSocket, or a sandbox VM).
|
data/docs/errors.md
CHANGED
|
@@ -44,6 +44,15 @@ rescue ClaudeAgentSDK::CLIJSONDecodeError => e
|
|
|
44
44
|
end
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
+
An ordinary exception raised inside your own callbacks (hooks, `can_use_tool`,
|
|
48
|
+
SDK MCP tool handlers, resource readers and prompt generators) is not raised
|
|
49
|
+
to the code above. It is reported to the CLI as a failed callback, and the
|
|
50
|
+
session keeps running. `exit`, `Interrupt` and other signal exceptions are the
|
|
51
|
+
exception: the CLI gets the error response first, and then they propagate as
|
|
52
|
+
Ruby normally would. See
|
|
53
|
+
[Hooks & Permission Callbacks](hooks-and-permissions.md#when-a-callback-raises)
|
|
54
|
+
and [MCP Servers](mcp-servers.md).
|
|
55
|
+
|
|
47
56
|
## Terminal Error Results
|
|
48
57
|
|
|
49
58
|
When a run fails, the CLI emits a `result` message with `is_error: true` (which
|
|
@@ -104,6 +113,10 @@ class CLINotFoundError < CLIConnectionError
|
|
|
104
113
|
# @param cli_path [String, nil] Optional path to the CLI that was not found
|
|
105
114
|
end
|
|
106
115
|
|
|
116
|
+
# Raised by the local-disk session APIs when CLAUDE_CONFIG_DIR is unset and
|
|
117
|
+
# no usable home directory exists for the default ~/.claude
|
|
118
|
+
class ConfigDirError < ClaudeSDKError; end
|
|
119
|
+
|
|
107
120
|
# Raised when the Claude Code process fails
|
|
108
121
|
class ProcessError < ClaudeSDKError
|
|
109
122
|
attr_reader :exit_code, # Integer | nil
|
|
@@ -138,9 +151,10 @@ end
|
|
|
138
151
|
| Error | Description |
|
|
139
152
|
|-------|-------------|
|
|
140
153
|
| `ClaudeSDKError` | Base error for all SDK errors |
|
|
141
|
-
| `CLIConnectionError` | Connection issues — including every write after a stdin write was cancelled mid-frame (the connection is unusable from then on — reconnect) |
|
|
154
|
+
| `CLIConnectionError` | Connection issues — including every write after a stdin write was cancelled mid-frame (the connection is unusable from then on — reconnect), and `ClaudeAgentSDK.ask` when the stream ends without a `ResultMessage` |
|
|
142
155
|
| `ControlRequestTimeoutError` | Control protocol timeout (configurable via env var) |
|
|
143
156
|
| `CLINotFoundError` | Claude Code not installed |
|
|
157
|
+
| `ConfigDirError` | A local-disk session API (`list_sessions`, `get_session_*`, `rename_session`, ...) could not locate the Claude config directory: `CLAUDE_CONFIG_DIR` is unset and there is no usable home directory (`HOME` unset with no passwd entry, as under `docker --user` in a minimal image, or an empty/relative `HOME`). Set `CLAUDE_CONFIG_DIR` |
|
|
144
158
|
| `ProcessError` | Process failed (includes `exit_code` and `stderr`) — also raised when the CLI is still running 5s after closing stdout and the SDK had to terminate it |
|
|
145
159
|
| `ResultError` | Run ended on a terminal error result (subclasses `ProcessError`; adds `subtype`, `errors`, `api_error_status`, `terminal_reason`, ...) — rescue it first |
|
|
146
160
|
| `CLIJSONDecodeError` | JSON parsing issues — including stdout ending mid-frame (a truncated final message; `line` holds the partial frame) |
|
|
@@ -193,3 +193,25 @@ never raises — shadowing can be intentional, e.g. a callback used solely for
|
|
|
193
193
|
tools outside `allowed_tools`. To gate every tool call including
|
|
194
194
|
auto-approved ones, use a `PreToolUse` hook instead (note that a `PreToolUse`
|
|
195
195
|
hook returning an allow decision also skips this callback).
|
|
196
|
+
|
|
197
|
+
## When a callback raises
|
|
198
|
+
|
|
199
|
+
An exception raised inside a hook or a `can_use_tool` callback fails that
|
|
200
|
+
control request: the CLI receives an error response carrying the exception
|
|
201
|
+
message, and the session carries on with later requests. The request's
|
|
202
|
+
cancellation signal is invalidated, as for any other callback failure.
|
|
203
|
+
|
|
204
|
+
`exit`, `Interrupt` and other signal exceptions are never swallowed. If one
|
|
205
|
+
is raised while a callback runs (by the callback itself, or a real Ctrl-C /
|
|
206
|
+
`SIGTERM` arriving while an `:inline` callback runs on the main thread), the
|
|
207
|
+
CLI first gets the same error response, naming the exception class
|
|
208
|
+
(`"SystemExit: exit"`, `"Interrupt"`), and then the exception propagates as
|
|
209
|
+
Ruby normally would: `exit 3` ends the process with status 3, and Ctrl-C
|
|
210
|
+
interrupts it. This holds in both `:thread` and `:inline` scheduling, and also
|
|
211
|
+
when a `:thread` hook calls `exit` after its `HookMatcher#timeout` has already
|
|
212
|
+
expired. A `callback_wrapper` sees the exception wrapped in an internal
|
|
213
|
+
`StandardError` whose `#cause` is the original, so ensure-based wrappers (such
|
|
214
|
+
as `Rails.application.executor.wrap`) still clean up. The original is raised
|
|
215
|
+
again after the wrapper returns, even if the wrapper swallows the error.
|
|
216
|
+
Cancellation (`control_cancel_request`, `HookMatcher#timeout`, disconnect)
|
|
217
|
+
still propagates as before.
|
data/docs/mcp-servers.md
CHANGED
|
@@ -14,7 +14,7 @@ greet_tool = ClaudeAgentSDK.create_tool(
|
|
|
14
14
|
'greet', 'Greet a user', { name: :string },
|
|
15
15
|
annotations: { title: 'Greeter', readOnlyHint: true }
|
|
16
16
|
) do |args|
|
|
17
|
-
|
|
17
|
+
"Hello, #{args[:name]}!"
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
server = ClaudeAgentSDK.create_sdk_mcp_server(
|
|
@@ -37,6 +37,27 @@ Async do
|
|
|
37
37
|
end.wait
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
+
## Handler Return Values
|
|
41
|
+
|
|
42
|
+
A handler returns either a String or a Hash:
|
|
43
|
+
|
|
44
|
+
- **A String** is sent to Claude as a single text block. `"Hello, Alice!"` is shorthand for `{ content: [{ type: 'text', text: "Hello, Alice!" }] }`.
|
|
45
|
+
- **A Hash** gives full control over the MCP result. `:content` (required) is an Array of MCP content blocks, so a tool can return several text blocks, images (`{ type: 'image', data: base64, mimeType: 'image/png' }`) and so on. Set `is_error: true` to tell Claude the call failed, and `structured_content:` to attach machine-readable output. Keys may be Symbols or Strings, and camelCase `isError` / `structuredContent` work too.
|
|
46
|
+
|
|
47
|
+
```ruby
|
|
48
|
+
ClaudeAgentSDK.create_tool('lookup_order', 'Look up an order', { id: :string }) do |args|
|
|
49
|
+
order = Order.find_by(number: args[:id])
|
|
50
|
+
next { content: [{ type: 'text', text: "No order #{args[:id]}" }], is_error: true } unless order
|
|
51
|
+
|
|
52
|
+
{
|
|
53
|
+
content: [{ type: 'text', text: "Order #{order.number}: #{order.status}" }],
|
|
54
|
+
structured_content: { number: order.number, status: order.status }
|
|
55
|
+
}
|
|
56
|
+
end
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Any other return value (`nil`, an Integer, an Array, ...) is reported to Claude as an `isError: true` result saying the tool must return a hash with a `:content` key.
|
|
60
|
+
|
|
40
61
|
## Pre-built JSON Schemas
|
|
41
62
|
|
|
42
63
|
If your schemas come from another library (e.g., [RubyLLM](https://github.com/crmne/ruby_llm)) that deep-stringifies keys, the SDK handles them transparently — both symbol-keyed and string-keyed schemas are accepted and normalized:
|
|
@@ -80,15 +101,14 @@ ClaudeAgentSDK.create_tool('save', 'Save a fact', {
|
|
|
80
101
|
|
|
81
102
|
```ruby
|
|
82
103
|
add_tool = ClaudeAgentSDK.create_tool('add', 'Add two numbers', { a: :number, b: :number }) do |args|
|
|
83
|
-
|
|
84
|
-
{ content: [{ type: 'text', text: "#{args[:a]} + #{args[:b]} = #{result}" }] }
|
|
104
|
+
"#{args[:a]} + #{args[:b]} = #{args[:a] + args[:b]}"
|
|
85
105
|
end
|
|
86
106
|
|
|
87
107
|
divide_tool = ClaudeAgentSDK.create_tool('divide', 'Divide numbers', { a: :number, b: :number }) do |args|
|
|
88
108
|
if args[:b] == 0
|
|
89
109
|
{ content: [{ type: 'text', text: 'Error: Division by zero' }], is_error: true }
|
|
90
110
|
else
|
|
91
|
-
|
|
111
|
+
"Result: #{args[:a] / args[:b]}"
|
|
92
112
|
end
|
|
93
113
|
end
|
|
94
114
|
|
|
@@ -105,9 +125,13 @@ options = ClaudeAgentSDK::ClaudeAgentOptions.new(
|
|
|
105
125
|
|
|
106
126
|
An exception raised inside a handler is returned to the model as an
|
|
107
127
|
`isError: true` result carrying the exception message, so it can self-correct.
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
128
|
+
`exit`, `Interrupt` and other signal exceptions are never swallowed: the CLI
|
|
129
|
+
first gets an `isError` result naming the exception class
|
|
130
|
+
(`"SystemExit: exit"`), so it is not left waiting on the tool call, and then
|
|
131
|
+
the exception propagates as Ruby normally would (`exit` ends the process,
|
|
132
|
+
Ctrl-C interrupts it). Called directly, without a session,
|
|
133
|
+
`SdkMcpServer#call_tool` and `#handle_message` simply let such exceptions
|
|
134
|
+
propagate. Cancellation of the tool call itself still propagates.
|
|
111
135
|
|
|
112
136
|
## Mixed Server Support
|
|
113
137
|
|
|
@@ -172,4 +196,10 @@ server = ClaudeAgentSDK.create_sdk_mcp_server(
|
|
|
172
196
|
)
|
|
173
197
|
```
|
|
174
198
|
|
|
199
|
+
An exception raised inside a resource reader or prompt generator is answered
|
|
200
|
+
with a JSON-RPC internal error (`-32603`) carrying the exception message. For
|
|
201
|
+
`exit`, `Interrupt` and other signal exceptions the CLI gets that error first,
|
|
202
|
+
naming the exception class, and then the exception propagates as Ruby normally
|
|
203
|
+
would.
|
|
204
|
+
|
|
175
205
|
See [examples/mcp_calculator.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/mcp_calculator.rb) and [examples/mcp_resources_prompts_example.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/mcp_resources_prompts_example.rb) for complete examples.
|