claude-agent-sdk 0.35.0 → 0.37.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 +74 -0
- data/README.md +17 -8
- data/docs/cli-installer.md +16 -2
- data/docs/client.md +44 -4
- data/docs/errors.md +15 -1
- data/docs/hooks-and-permissions.md +27 -3
- data/docs/mcp-servers.md +36 -7
- data/docs/rails.md +3 -4
- data/docs/sessions.md +149 -34
- data/docs/types.md +106 -4
- data/lib/claude_agent_sdk/cancellation_signal.rb +2 -1
- data/lib/claude_agent_sdk/cli_installer.rb +68 -11
- data/lib/claude_agent_sdk/command_builder.rb +109 -98
- data/lib/claude_agent_sdk/deprecation.rb +90 -0
- data/lib/claude_agent_sdk/errors.rb +8 -0
- data/lib/claude_agent_sdk/fiber_boundary.rb +113 -2
- data/lib/claude_agent_sdk/instrumentation/otel.rb +15 -7
- data/lib/claude_agent_sdk/message_parser.rb +23 -9
- data/lib/claude_agent_sdk/observer.rb +2 -1
- data/lib/claude_agent_sdk/option_warnings.rb +2 -2
- data/lib/claude_agent_sdk/query.rb +99 -51
- data/lib/claude_agent_sdk/railtie.rb +14 -3
- data/lib/claude_agent_sdk/sdk_mcp_server.rb +58 -38
- data/lib/claude_agent_sdk/session_mutations.rb +28 -16
- data/lib/claude_agent_sdk/session_resume.rb +39 -35
- data/lib/claude_agent_sdk/session_store.rb +35 -21
- data/lib/claude_agent_sdk/session_summary.rb +12 -5
- data/lib/claude_agent_sdk/sessions.rb +112 -24
- data/lib/claude_agent_sdk/streaming.rb +1 -1
- data/lib/claude_agent_sdk/subprocess_cli_transport.rb +75 -52
- data/lib/claude_agent_sdk/tasks/claude_agent_sdk.rake +12 -5
- data/lib/claude_agent_sdk/testing/session_store_conformance.rb +15 -11
- data/lib/claude_agent_sdk/transcript_mirror_batcher.rb +19 -1
- data/lib/claude_agent_sdk/types/attributes.rb +271 -0
- data/lib/claude_agent_sdk/types/base.rb +320 -0
- data/lib/claude_agent_sdk/types/content_blocks.rb +57 -0
- data/lib/claude_agent_sdk/types/hooks.rb +640 -0
- data/lib/claude_agent_sdk/types/mcp.rb +232 -0
- data/lib/claude_agent_sdk/types/messages.rb +614 -0
- data/lib/claude_agent_sdk/types/option_values.rb +302 -0
- data/lib/claude_agent_sdk/types/options.rb +352 -0
- data/lib/claude_agent_sdk/types/permissions.rb +107 -0
- data/lib/claude_agent_sdk/types/sessions.rb +10 -0
- data/lib/claude_agent_sdk/types.rb +13 -2534
- data/lib/claude_agent_sdk/version.rb +1 -1
- data/lib/claude_agent_sdk.rb +308 -73
- data/lib/generators/claude_agent_sdk/install/templates/claude_agent_sdk.rb.tt +3 -3
- metadata +12 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7d238195bb7f1725358c2ca33a8396ff1f4ab4121c7cc905e1dc2e1957a4553e
|
|
4
|
+
data.tar.gz: 480e5d771b68e72b0646bfa102cd921b1797d7134a925c496c5575a5e795c88e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 91d8f9de96e91aa5297e2342f33a57de100807978ac5c6bb7739927f99e0c41707f335c4e41512c9d843bf19afdc3d30ab97b526319e3cc45d932e5bae2cc761
|
|
7
|
+
data.tar.gz: e7d2504b2dcda1c0ee5d1d4174d9ece35efc1a53b9832b00595829adacc6039f423d4db4999e1139d1fb6ad93c3b532dad119f9aceb51305328d3d25d847e7f6
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,80 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.37.0] - 2026-09-23
|
|
11
|
+
|
|
12
|
+
The last 0.x release before 1.0 ([roadmap](https://github.com/ya-luotao/claude-agent-sdk-ruby/issues/126)). No runtime behaviour changes — only new warnings for things 1.0 will reject. **Run your suite on 0.37 with warnings visible before moving to 1.0:**
|
|
13
|
+
- A misspelled key on a value type you build (`HookMatcher`, `AgentDefinition`, `SandboxSettings`, MCP server configs, permission results, hook outputs, …) now warns once, pointing at your call; 1.0 raises `ArgumentError`.
|
|
14
|
+
- `msg[:name]` / camelCase readers reaching a non-attribute method (e.g. `msg[:to_h]`) warn once; in 1.0 they only reach attributes.
|
|
15
|
+
- The 1.0 SemVer surface is now visible: SDK internals are tagged `@api private` and hidden from the API docs, and the documented contracts (Hash-key rule, `mcp_status` / `context_usage` return Hashes, `Type#[]`) are written down.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
- **Docs: pre-1.0 contracts written down** ([#126](https://github.com/ya-luotao/claude-agent-sdk-ruby/issues/126)). No code changes. `Client#mcp_status` / `#get_mcp_status` and `#context_usage` / `#get_context_usage` return the CLI's raw Hash with camelCase Symbol keys. `docs/types.md` had described `McpStatusResponse` as the response type; it is the optional typed view, via `McpStatusResponse.parse(client.mcp_status)`, and there is no typed context-usage class (`docs/client.md`). A new "Hash keys" section in `docs/types.md` states one rule: Hashes passed through from the live CLI stream (`origin`, `usage`, `model_usage`, hook `tool_input`, `can_use_tool` input, SDK MCP tool args, control responses) have Symbol keys spelled as on the wire, and Hashes read from transcripts or a `SessionStore` (`SessionMessage#message`, `get_subagent_metadata`, store keys and entries) have String keys. The docs, examples and bundled skill drop their `h[:key] || h['key']` fallbacks in favour of the one correct form. `Type#[]`, `#[]=` and the camelCase readers are documented as public API, including that `#[]=` changes the object you received. `docs/sessions.md` documents `ClaudeAgentSDK.project_key_for_directory` and `ClaudeAgentSDK.fold_session_summary` for adapter authors, and `import_session_to_store`'s batching (`batch_size:` defaults to 500 entries, and a batch also ends at about 1 MiB).
|
|
19
|
+
- **SDK internals are now tagged `@api private` and hidden from the YARD docs** ([#126](https://github.com/ya-luotao/claude-agent-sdk-ruby/issues/126)). The tagged namespaces are `Query`, `MessageParser`, `TranscriptMirrorBatcher`, `Sessions`, `SessionMutations`, `SessionResume`, `MaterializedResume`, `SessionSummary`, `SessionStores`, `OptionWarnings` and `FiberBoundary`, including everything nested in them. Some public classes also have internal members, and those are tagged individually: `SubprocessCLITransport`'s methods outside the `Transport` interface and its constants; `CommandBuilder`'s constants (`.new` and `#build` stay public); `CLIInstaller::Http`, `Platform`, `Release` and `Metadata` and the installer's constants other than `PINNED_CLI_VERSION`; `SdkMcpServer#handle_json`, `#handle_message` and `SdkMcpServer::ToolInputSchema`; `Client#query_handler`; and `ClaudeAgentSDK::OBSERVER_INTERFACE`. Their public faces are unchanged: `ClaudeAgentSDK.offload`, `fold_session_summary`, the session functions and `SDKSessionInfo` / `SessionMessage` remain public. Nothing changes at runtime. No method or constant was renamed, removed or made private, so every existing call keeps working. From 1.0, SemVer covers `docs/` and the YARD API without `@api private`; objects tagged `@api private` are not covered and may change in any release. `CONTRIBUTING.md` has a new "What is public API" section.
|
|
20
|
+
- `lib/claude_agent_sdk/types.rb` is split into one file per area under `lib/claude_agent_sdk/types/` (messages, hooks, options, ...). The code moved without edits and `types.rb` still loads every part, so `require 'claude_agent_sdk/types'` and every class and constant are unchanged; no API change ([#126](https://github.com/ya-luotao/claude-agent-sdk-ruby/issues/126)).
|
|
21
|
+
- `Type`'s internal machinery is tagged `@api private` and leaves the rendered docs: `Type#dup_for_options`, `Type.deep_dup_for_options`, `Type::OptionValue`, `Type.inspect_filtered`, `Type.inspect_filtered_attributes`, and the new `Type.strict_attributes` / `.strict_attributes?` / `.declare_attributes` / `.attribute?` / `.attribute_names`. They are not part of the SemVer surface. `Type#[]`, `#[]=`, the camelCase readers, `#to_h`, `.wrap` and `.from_hash` stay public ([#126](https://github.com/ya-luotao/claude-agent-sdk-ruby/issues/126)).
|
|
22
|
+
|
|
23
|
+
### Deprecated
|
|
24
|
+
- **Unknown keys on user-constructed types.** The value types you build and pass in (option values such as `AgentDefinition`, `SandboxSettings`, the thinking configs and the MCP server configs; `HookMatcher`; hook outputs; `PermissionResultAllow` / `PermissionResultDeny`; `PermissionUpdate`; `PermissionRuleValue`) silently dropped a misspelled key, so `HookMatcher.new(matchr: 'Bash')` quietly built a matcher with no `matcher` set. `.new` and `#[]=` now warn once per class and key, at the caller's line: `ClaudeAgentSDK::HookMatcher: unknown attribute :matchr ignored; this will raise ArgumentError in 1.0 (known: hooks, matcher, timeout)`. **In 1.0 they raise `ArgumentError`**, as `ClaudeAgentOptions` already does. camelCase and String keys and a type's own discriminator (`type`, `hook_event_name`, `behavior`) are accepted, so `klass.new(value.to_h)` round-trips silently. Types parsed from CLI output (messages, content blocks, hook inputs) and every construction through `.from_hash` / `.wrap` stay lenient, so a newer CLI's extra fields never warn; permission suggestions from the CLI are now hydrated through `PermissionUpdate.wrap` for that reason. Full list in `docs/types.md` ([#126](https://github.com/ya-luotao/claude-agent-sdk-ruby/issues/126)).
|
|
25
|
+
- **`Type#[]`, `#[]=` and camelCase methods reaching something other than an attribute.** They are public API for a type's attributes (its `attr_*` fields, the backward-compatible `RateLimitEvent#data`, predicates such as `options.forkSession?`, and any method your own code adds to a subclass, mixin or instance), but they resolved any public method: `msg[:to_h]` returned a Hash, `msg['freeze']` froze the message, `msg.toH` called `#to_h`. Such a call still works but warns once per class and name, e.g. `ClaudeAgentSDK::ResultMessage#[]: :to_h is not an attribute; Type#[] will only read attributes in 1.0`. **In 1.0 a name that is not an attribute behaves like an undefined one:** `#[]` returns `nil`, `#[]=` ignores it (raises on the types above), a camelCase call raises `NoMethodError`, and `respond_to?` says `false`. Names that are not methods at all keep today's behaviour and do not warn. `UserMessage#text` / `AssistantMessage#text` are convenience methods, not attributes ([#126](https://github.com/ya-luotao/claude-agent-sdk-ruby/issues/126)).
|
|
26
|
+
|
|
27
|
+
## [0.36.0] - 2026-09-23
|
|
28
|
+
|
|
29
|
+
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:**
|
|
30
|
+
- 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.
|
|
31
|
+
- The `*_from_store` / `*_via_store` session functions print a one-time deprecation warning; switch to `list_sessions(session_store: store)` etc. (table under **Deprecated**).
|
|
32
|
+
- 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 `''`).
|
|
33
|
+
|
|
34
|
+
### Added
|
|
35
|
+
- **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.
|
|
36
|
+
- **`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.
|
|
37
|
+
- **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.
|
|
38
|
+
- **`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.
|
|
39
|
+
- **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.
|
|
40
|
+
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.
|
|
41
|
+
- **`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.
|
|
42
|
+
- **`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).
|
|
43
|
+
- 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.
|
|
44
|
+
- `CONTRIBUTING.md`, `SECURITY.md`, and issue and pull request templates.
|
|
45
|
+
|
|
46
|
+
### Changed
|
|
47
|
+
- **`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)
|
|
48
|
+
- **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.
|
|
49
|
+
- `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`.
|
|
50
|
+
- RuboCop targets Ruby 3.2, the gemspec floor (was 3.0). The resulting autocorrections (anonymous block forwarding, dropping `require 'set'`) change no behavior.
|
|
51
|
+
|
|
52
|
+
### Deprecated
|
|
53
|
+
- **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.
|
|
54
|
+
|
|
55
|
+
| Deprecated | Replacement |
|
|
56
|
+
|---|---|
|
|
57
|
+
| `list_sessions_from_store(session_store: s, ...)` | `list_sessions(session_store: s, ...)` |
|
|
58
|
+
| `get_session_info_from_store(session_store: s, ...)` | `get_session_info(session_store: s, ...)` |
|
|
59
|
+
| `get_session_messages_from_store(session_store: s, ...)` | `get_session_messages(session_store: s, ...)` |
|
|
60
|
+
| `list_subagents_from_store(session_store: s, ...)` | `list_subagents(session_store: s, ...)` |
|
|
61
|
+
| `get_subagent_metadata_from_store(session_store: s, ...)` | `get_subagent_metadata(session_store: s, ...)` |
|
|
62
|
+
| `get_subagent_messages_from_store(session_store: s, ...)` | `get_subagent_messages(session_store: s, ...)` |
|
|
63
|
+
| `rename_session_via_store(session_store: s, ...)` | `rename_session(session_store: s, ...)` |
|
|
64
|
+
| `tag_session_via_store(session_store: s, ...)` | `tag_session(session_store: s, ...)` |
|
|
65
|
+
| `delete_session_via_store(session_store: s, ...)` | `delete_session(session_store: s, ...)` |
|
|
66
|
+
| `fork_session_via_store(session_store: s, ...)` | `fork_session(session_store: s, ...)` |
|
|
67
|
+
|
|
68
|
+
All other arguments carry over unchanged. `import_session_to_store` is not affected.
|
|
69
|
+
|
|
70
|
+
### Fixed
|
|
71
|
+
- **`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)
|
|
72
|
+
- **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`:
|
|
73
|
+
- 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;
|
|
74
|
+
- 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.
|
|
75
|
+
- **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.
|
|
76
|
+
- **Remaining disk/store session read inconsistencies (#121):**
|
|
77
|
+
- 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.
|
|
78
|
+
- `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.
|
|
79
|
+
- `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.
|
|
80
|
+
- `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.
|
|
81
|
+
- `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`.
|
|
82
|
+
- `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.
|
|
83
|
+
|
|
10
84
|
## [0.35.0] - 2026-09-23
|
|
11
85
|
|
|
12
86
|
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**).
|
data/README.md
CHANGED
|
@@ -29,7 +29,7 @@ A Ruby SDK for the [Claude Code](https://docs.claude.com/en/docs/claude-code-ove
|
|
|
29
29
|
|
|
30
30
|
```ruby
|
|
31
31
|
# Gemfile
|
|
32
|
-
gem 'claude-agent-sdk', '~> 0.
|
|
32
|
+
gem 'claude-agent-sdk', '~> 0.37.0'
|
|
33
33
|
```
|
|
34
34
|
|
|
35
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'`.
|
|
@@ -75,14 +75,23 @@ The block runs on a plain thread, so ActiveRecord calls inside it just work. [do
|
|
|
75
75
|
```ruby
|
|
76
76
|
require 'claude_agent_sdk'
|
|
77
77
|
|
|
78
|
-
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|
|
|
79
85
|
puts message.text if message.is_a?(ClaudeAgentSDK::AssistantMessage)
|
|
80
86
|
end
|
|
87
|
+
puts result
|
|
81
88
|
```
|
|
82
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
|
+
|
|
83
92
|
### `query()` — one-shot and streaming
|
|
84
93
|
|
|
85
|
-
`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`.
|
|
86
95
|
|
|
87
96
|
```ruby
|
|
88
97
|
options = ClaudeAgentSDK::ClaudeAgentOptions.new(
|
|
@@ -145,7 +154,7 @@ Tools are Ruby blocks that run in-process, with no subprocess or IPC between Cla
|
|
|
145
154
|
|
|
146
155
|
```ruby
|
|
147
156
|
greet = ClaudeAgentSDK.create_tool('greet', 'Greet a user', { name: :string }) do |args|
|
|
148
|
-
|
|
157
|
+
"Hello, #{args[:name]}!"
|
|
149
158
|
end
|
|
150
159
|
|
|
151
160
|
server = ClaudeAgentSDK.create_sdk_mcp_server(name: 'my-tools', tools: [greet])
|
|
@@ -156,7 +165,7 @@ options = ClaudeAgentSDK::ClaudeAgentOptions.new(
|
|
|
156
165
|
)
|
|
157
166
|
```
|
|
158
167
|
|
|
159
|
-
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.
|
|
160
169
|
|
|
161
170
|
### Hooks and permission callbacks
|
|
162
171
|
|
|
@@ -184,7 +193,7 @@ See [docs/hooks-and-permissions.md](docs/hooks-and-permissions.md) for the full
|
|
|
184
193
|
| OpenTelemetry tracing, Langfuse, custom observers | [docs/observability.md](docs/observability.md) |
|
|
185
194
|
| Rails: generator, `install_cli` task, callback wrapper, fiber safety, solid_queue fiber workers, ActionCable, jobs | [docs/rails.md](docs/rails.md) |
|
|
186
195
|
| Vendoring a pinned CLI binary and CLI discovery order | [docs/cli-installer.md](docs/cli-installer.md) |
|
|
187
|
-
|
|
|
196
|
+
| Hash-key rule, attribute access, and the message, content block, and configuration type reference | [docs/types.md](docs/types.md) |
|
|
188
197
|
| Error handling, exception hierarchy, timeouts | [docs/errors.md](docs/errors.md) |
|
|
189
198
|
|
|
190
199
|
API reference: [rubydoc.info/gems/claude-agent-sdk](https://rubydoc.info/gems/claude-agent-sdk). Available built-in tools: [Claude Code documentation](https://docs.anthropic.com/en/docs/claude-code/settings#tools-available-to-claude).
|
|
@@ -247,11 +256,11 @@ RUN_INTEGRATION=1 bundle exec rspec # also run the real-CLI integration suite (
|
|
|
247
256
|
BUNDLE_GEMFILE=gemfiles/rails_8.gemfile bundle exec rspec --options spec/rails/.rspec # Rails integration specs
|
|
248
257
|
```
|
|
249
258
|
|
|
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.
|
|
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.
|
|
251
260
|
|
|
252
261
|
## Contributing
|
|
253
262
|
|
|
254
|
-
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).
|
|
255
264
|
|
|
256
265
|
## License
|
|
257
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`
|
|
@@ -62,7 +76,7 @@ require 'claude_agent_sdk/tasks' # loads only CLIInstaller, not the whole SDK
|
|
|
62
76
|
|
|
63
77
|
```bash
|
|
64
78
|
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
|
|
79
|
+
rake claude_agent_sdk:install_cli # elsewhere: into CLIInstaller.default_dir (vendor/claude under the working directory unless root is set)
|
|
66
80
|
rake claude_agent_sdk:install_cli CLAUDE_CLI_VERSION=x.y.z # a version of your own, or 'stable' / 'latest'
|
|
67
81
|
```
|
|
68
82
|
|
|
@@ -83,6 +97,6 @@ The variable is deliberately not rake's conventional `VERSION`, which Rails' `db
|
|
|
83
97
|
With no explicit `cli_path:` in `ClaudeAgentOptions`, the transport probes in this order:
|
|
84
98
|
|
|
85
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:`)
|
|
86
|
-
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
|
|
87
101
|
3. `which claude`
|
|
88
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,6 +2,8 @@
|
|
|
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
|
|
|
7
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.
|
|
@@ -46,9 +48,10 @@ Async do
|
|
|
46
48
|
client.connect
|
|
47
49
|
|
|
48
50
|
client.interrupt # Send interrupt signal
|
|
49
|
-
client.
|
|
50
|
-
client.
|
|
51
|
-
|
|
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
|
|
52
55
|
info = client.get_server_info # Inspect server init info
|
|
53
56
|
client.reconnect_mcp_server('my-server') # Reconnect a failed MCP server
|
|
54
57
|
client.toggle_mcp_server('my-server', false) # Enable/disable an MCP server
|
|
@@ -62,6 +65,43 @@ Async do
|
|
|
62
65
|
end.wait
|
|
63
66
|
```
|
|
64
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
|
+
|
|
80
|
+
### MCP status and context usage return Hashes
|
|
81
|
+
|
|
82
|
+
`mcp_status` / `get_mcp_status` and `context_usage` / `get_context_usage` return the CLI's control response payload as a plain Hash, unchanged: Symbol keys spelled as on the wire, which for these payloads is camelCase (`:mcpServers`, `:serverInfo`, `:totalTokens`), at every level of nesting (see [Hash keys](types.md#hash-keys)). The SDK does not model or filter the payload, so fields added by newer CLI versions come through. A response without a payload reads as `{}`.
|
|
83
|
+
|
|
84
|
+
```ruby
|
|
85
|
+
status = client.mcp_status
|
|
86
|
+
status[:mcpServers].each { |s| puts "#{s[:name]}: #{s[:status]}" }
|
|
87
|
+
status.dig(:mcpServers, 0, :serverInfo, :version)
|
|
88
|
+
|
|
89
|
+
usage = client.context_usage
|
|
90
|
+
puts "#{usage[:totalTokens]} / #{usage[:maxTokens]} tokens"
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
For a typed view of the MCP status, parse the Hash yourself:
|
|
94
|
+
|
|
95
|
+
```ruby
|
|
96
|
+
typed = ClaudeAgentSDK::McpStatusResponse.parse(client.mcp_status)
|
|
97
|
+
typed.mcp_servers.each do |server| # McpServerStatus
|
|
98
|
+
puts "#{server.name} #{server.status} #{server.server_info&.version}"
|
|
99
|
+
server.tools&.each { |tool| puts " #{tool.name} read_only=#{tool.annotations&.read_only}" }
|
|
100
|
+
end
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`McpServerStatus#config` is an `McpSdkServerConfigStatus` or `McpClaudeAIProxyServerConfig` for `sdk` and `claudeai-proxy` servers, and the raw Hash for every other server type. There is no typed class for context usage; read the Hash.
|
|
104
|
+
|
|
65
105
|
## Custom Transport
|
|
66
106
|
|
|
67
107
|
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).
|
|
@@ -72,7 +112,7 @@ A transport must implement six methods:
|
|
|
72
112
|
|---|---|
|
|
73
113
|
| `connect` | Establish the connection / spawn the remote CLI |
|
|
74
114
|
| `write(data)` | Send raw JSON-line bytes to stdin |
|
|
75
|
-
| `read_messages { \|hash\| ... }` | Yield parsed JSON
|
|
115
|
+
| `read_messages { \|hash\| ... }` | Yield each stdout line as a Hash parsed with `JSON.parse(line, symbolize_names: true)` (the SDK reads Symbol keys; see [Hash keys](types.md#hash-keys)); block until the stream closes |
|
|
76
116
|
| `end_input` | Signal EOF on stdin |
|
|
77
117
|
| `close` | Terminate and clean up |
|
|
78
118
|
| `ready?` | Report whether the transport can accept I/O |
|
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) |
|
|
@@ -19,7 +19,9 @@ All hook input objects include common fields like `session_id`, `transcript_path
|
|
|
19
19
|
- `SubagentStart` → `SubagentStartHookInput` (`agent_id`, `agent_type`)
|
|
20
20
|
- `PermissionRequest` → `PermissionRequestHookInput` (`tool_name`, `tool_input`, `permission_suggestions`)
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
`tool_input` (and the `input` a [permission callback](#permission-callbacks) receives) is the CLI's Hash passed through unchanged, so its keys are Symbols spelled as on the wire: `tool_input[:command]`, `input[:file_path]`. See [Hash keys](types.md#hash-keys).
|
|
23
|
+
|
|
24
|
+
All 27 hook events have typed input classes. See [`ClaudeAgentSDK::HOOK_EVENTS`](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/lib/claude_agent_sdk/types/hooks.rb) and [examples/lifecycle_hooks_example.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/lifecycle_hooks_example.rb).
|
|
23
25
|
|
|
24
26
|
`background_tasks` and `session_crons` are optional arrays of raw CLI hashes.
|
|
25
27
|
`nil` means the CLI did not provide a snapshot; `[]` means it provided an empty
|
|
@@ -40,7 +42,7 @@ Async do
|
|
|
40
42
|
return {} unless input.respond_to?(:tool_name) && input.tool_name == 'Bash'
|
|
41
43
|
|
|
42
44
|
tool_input = input.tool_input || {}
|
|
43
|
-
command = tool_input[:command] ||
|
|
45
|
+
command = tool_input[:command] || ''
|
|
44
46
|
block_patterns = ['rm -rf', 'foo.sh']
|
|
45
47
|
|
|
46
48
|
block_patterns.each do |pattern|
|
|
@@ -105,7 +107,7 @@ Async do
|
|
|
105
107
|
return ClaudeAgentSDK::PermissionResultAllow.new if tool_name == 'Read'
|
|
106
108
|
|
|
107
109
|
if tool_name == 'Write'
|
|
108
|
-
file_path = input[:file_path]
|
|
110
|
+
file_path = input[:file_path]
|
|
109
111
|
if file_path && file_path.include?('/etc/')
|
|
110
112
|
return ClaudeAgentSDK::PermissionResultDeny.new(
|
|
111
113
|
message: 'Cannot write to sensitive system files',
|
|
@@ -193,3 +195,25 @@ never raises — shadowing can be intentional, e.g. a callback used solely for
|
|
|
193
195
|
tools outside `allowed_tools`. To gate every tool call including
|
|
194
196
|
auto-approved ones, use a `PreToolUse` hook instead (note that a `PreToolUse`
|
|
195
197
|
hook returning an allow decision also skips this callback).
|
|
198
|
+
|
|
199
|
+
## When a callback raises
|
|
200
|
+
|
|
201
|
+
An exception raised inside a hook or a `can_use_tool` callback fails that
|
|
202
|
+
control request: the CLI receives an error response carrying the exception
|
|
203
|
+
message, and the session carries on with later requests. The request's
|
|
204
|
+
cancellation signal is invalidated, as for any other callback failure.
|
|
205
|
+
|
|
206
|
+
`exit`, `Interrupt` and other signal exceptions are never swallowed. If one
|
|
207
|
+
is raised while a callback runs (by the callback itself, or a real Ctrl-C /
|
|
208
|
+
`SIGTERM` arriving while an `:inline` callback runs on the main thread), the
|
|
209
|
+
CLI first gets the same error response, naming the exception class
|
|
210
|
+
(`"SystemExit: exit"`, `"Interrupt"`), and then the exception propagates as
|
|
211
|
+
Ruby normally would: `exit 3` ends the process with status 3, and Ctrl-C
|
|
212
|
+
interrupts it. This holds in both `:thread` and `:inline` scheduling, and also
|
|
213
|
+
when a `:thread` hook calls `exit` after its `HookMatcher#timeout` has already
|
|
214
|
+
expired. A `callback_wrapper` sees the exception wrapped in an internal
|
|
215
|
+
`StandardError` whose `#cause` is the original, so ensure-based wrappers (such
|
|
216
|
+
as `Rails.application.executor.wrap`) still clean up. The original is raised
|
|
217
|
+
again after the wrapper returns, even if the wrapper swallows the error.
|
|
218
|
+
Cancellation (`control_cancel_request`, `HookMatcher#timeout`, disconnect)
|
|
219
|
+
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,12 @@ 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` simply lets such exceptions propagate. Cancellation of the tool call itself still propagates.
|
|
111
134
|
|
|
112
135
|
## Mixed Server Support
|
|
113
136
|
|
|
@@ -172,4 +195,10 @@ server = ClaudeAgentSDK.create_sdk_mcp_server(
|
|
|
172
195
|
)
|
|
173
196
|
```
|
|
174
197
|
|
|
198
|
+
An exception raised inside a resource reader or prompt generator is answered
|
|
199
|
+
with a JSON-RPC internal error (`-32603`) carrying the exception message. For
|
|
200
|
+
`exit`, `Interrupt` and other signal exceptions the CLI gets that error first,
|
|
201
|
+
naming the exception class, and then the exception propagates as Ruby normally
|
|
202
|
+
would.
|
|
203
|
+
|
|
175
204
|
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.
|
data/docs/rails.md
CHANGED
|
@@ -25,7 +25,7 @@ The gem ships a Railtie, an install generator and a rake task for vendoring the
|
|
|
25
25
|
bin/rails claude_agent_sdk:install_cli CLAUDE_CLI_VERSION=x.y.z # or a version of your own ('stable' / 'latest' float)
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
-
The binary lands in `Rails.root/vendor/claude`, where the SDK finds it ahead of any `claude` on `PATH
|
|
28
|
+
The binary lands in `Rails.root/vendor/claude`, where the SDK finds it ahead of any `claude` on `PATH`. That holds whatever the process's working directory is (a daemonized worker, a job runner started elsewhere): the Railtie points `ClaudeAgentSDK::CLIInstaller.root` at `Rails.root` during boot, before `config/initializers` run, so an initializer can still set a different root, and a root already set in `config/application.rb` is kept. 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`. For the same reason the task never sees a root set in `config/initializers`; if you move the CLI elsewhere, set `CLIInstaller.root` in `config/application.rb`, which both the task and discovery honour. 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
29
|
|
|
30
30
|
4. Run an agent from a job:
|
|
31
31
|
|
|
@@ -53,8 +53,7 @@ You do **not** need to think about this. By default (`callback_scheduling: :thre
|
|
|
53
53
|
|
|
54
54
|
```ruby
|
|
55
55
|
tool = ClaudeAgentSDK.create_tool('lookup_user', 'Look up a user', { id: Integer }) do |args|
|
|
56
|
-
|
|
57
|
-
{ content: [{ type: 'text', text: user.name }] }
|
|
56
|
+
User.find(args[:id]).name # just works
|
|
58
57
|
end
|
|
59
58
|
|
|
60
59
|
ClaudeAgentSDK.query(prompt: '...') do |message|
|
|
@@ -127,7 +126,7 @@ The one real risk: **scheduler-opaque blocking stalls the whole reactor.** CPU-b
|
|
|
127
126
|
```ruby
|
|
128
127
|
tool = ClaudeAgentSDK.create_tool('lookup', 'Query legacy DB', { id: String }) do |args|
|
|
129
128
|
row = ClaudeAgentSDK.offload { legacy_client.fetch(args[:id]) } # plain thread
|
|
130
|
-
|
|
129
|
+
row.to_json
|
|
131
130
|
end
|
|
132
131
|
```
|
|
133
132
|
|