claude-agent-sdk 0.33.1 → 0.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +93 -0
- data/README.md +54 -19
- data/docs/cli-installer.md +40 -9
- data/docs/client.md +27 -18
- data/docs/configuration.md +5 -5
- data/docs/errors.md +4 -3
- data/docs/mcp-servers.md +22 -0
- data/docs/observability.md +6 -0
- data/docs/rails.md +92 -51
- data/docs/sessions.md +66 -15
- data/lib/claude_agent_sdk/cli_installer.rb +34 -18
- data/lib/claude_agent_sdk/command_builder.rb +11 -3
- data/lib/claude_agent_sdk/configuration.rb +54 -2
- data/lib/claude_agent_sdk/errors.rb +11 -3
- data/lib/claude_agent_sdk/fiber_boundary.rb +42 -3
- data/lib/claude_agent_sdk/instrumentation/otel.rb +21 -2
- data/lib/claude_agent_sdk/query.rb +140 -59
- data/lib/claude_agent_sdk/railtie.rb +94 -0
- data/lib/claude_agent_sdk/sdk_mcp_server.rb +46 -4
- data/lib/claude_agent_sdk/session_mutations.rb +39 -12
- data/lib/claude_agent_sdk/session_resume.rb +112 -39
- data/lib/claude_agent_sdk/session_store.rb +19 -3
- data/lib/claude_agent_sdk/session_summary.rb +5 -5
- data/lib/claude_agent_sdk/sessions.rb +123 -55
- data/lib/claude_agent_sdk/streaming.rb +0 -8
- data/lib/claude_agent_sdk/subprocess_cli_transport.rb +319 -54
- data/lib/claude_agent_sdk/tasks/claude_agent_sdk.rake +30 -0
- data/lib/claude_agent_sdk/tasks.rb +13 -0
- data/lib/claude_agent_sdk/testing/session_store_conformance.rb +13 -3
- data/lib/claude_agent_sdk/transcript_mirror_batcher.rb +77 -18
- data/lib/claude_agent_sdk/types.rb +349 -39
- data/lib/claude_agent_sdk/version.rb +1 -1
- data/lib/claude_agent_sdk.rb +53 -44
- 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 +29 -13
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a2976dae089e4ccc32b7974b646cb9e543c4dcdfd6bfb35eeccf6b3c4ef5e6ed
|
|
4
|
+
data.tar.gz: 89ec51407ce6f25c962982078b3fe8301bfda9be4e57bf4a03593e9818d5913a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4784c6381acbfedd9a921a852329dfdd64257aa60b25bf51b0f4278b5fe6ce4913b1f719941a0fece9ac818342e5129c0ffd481159bf731ee6930ecf4902b632
|
|
7
|
+
data.tar.gz: cee7e96d3ef502138189a114716db3862d10d2ff10411e04bf0db8bc654511c547397164c7640c590eed166d11556c26b5d4e6de744f3a6ac396cdb15e40bad3
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,99 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.35.0] - 2026-09-23
|
|
11
|
+
|
|
12
|
+
First-class Rails integration and a first-impressions pass. **Rails users:** if your initializer uses the previously documented `->(inv) { Rails.application.executor.wrap { inv.call } }` callback wrapper, switch to `ClaudeAgentSDK::Railtie.callback_wrapper` — the bare form can deadlock in development (see **Fixed**).
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
- **Rails integration: `ClaudeAgentSDK::Railtie`**, loaded only when Rails is (`require_relative 'claude_agent_sdk/railtie' if defined?(Rails::Railtie)`, which Bundler.require satisfies in a Rails app); non-Rails processes load nothing new. It contributes a rake task and installs nothing into callback dispatch.
|
|
16
|
+
- **`bin/rails generate claude_agent_sdk:install`** — writes `config/initializers/claude_agent_sdk.rb` (commented `model` / `permission_mode` / `cli_path` / OpenTelemetry defaults, `callback_wrapper: ClaudeAgentSDK::Railtie.callback_wrapper` enabled), appends `/vendor/claude/` to `.gitignore` once (any existing spelling counts), and prints the next steps.
|
|
17
|
+
- **`claude_agent_sdk:install_cli` rake task** — `CLIInstaller.install_pinned` into `Rails.root/vendor/claude`, or `install(version:)` with `CLAUDE_CLI_VERSION=x.y.z|stable|latest`; prints the installed path. It does not boot the app, so it runs in a Docker build step. Outside Rails, `require 'claude_agent_sdk/tasks'` in a Rakefile provides the same task (loading only `CLIInstaller`), installing under the working directory.
|
|
18
|
+
- **`ClaudeAgentSDK::Railtie.callback_wrapper`** — a `callback_wrapper` that runs SDK callbacks in `Rails.application.executor` (so ActiveRecord connections check back in), except where that deadlocks: with code reloading enabled or `config.allow_concurrency = false` it calls the callback outside the executor and releases the thread's ActiveRecord connections itself; when the executor is already active on the callback's context (`:inline` scheduling) it calls straight through. Supports Rails 7.1+.
|
|
19
|
+
- CI: a `rails` job runs the Rails integration specs (`spec/rails`, in their own process via `rspec --options spec/rails/.rspec`) against Rails 7.1 on Ruby 3.2 and the latest Rails 8 on Ruby 3.4 (`gemfiles/rails_7_1.gemfile`, `gemfiles/rails_8.gemfile`). The default `bundle exec rspec` run excludes `spec/rails`.
|
|
20
|
+
- **Every SDK type now prints its fields.** `Type#inspect` lists the non-nil attributes (`#<ClaudeAgentSDK::ResultMessage subtype="success" num_turns=3 total_cost_usd=0.012 ...>`) instead of a bare object address, and `#to_s` falls back to it, so the README's `puts message` is readable for every message type. The output is bounded for logging: Strings past 80 characters are truncated with a count of what was cut, Arrays and Hashes show their first five entries plus a count of the rest, nesting past two levels (and any reference cycle) collapses to a placeholder, and objects that only have `Kernel#inspect` (SDK MCP server instances, store adapters, observers) show as `#<ClassName>` rather than dumping their state. Callbacks (`can_use_tool`, hooks, `callback_wrapper`, ...) render from their source location, e.g. `#<Proc(lambda) permissions.rb:17>`, never through their own `#inspect`, so a raising or oversized override cannot break or flood a log line.
|
|
21
|
+
- **One-line `to_s` for results and system messages.** `ResultMessage#to_s` prints `[result: success, 3 turns, 4.2s, $0.0120]` (missing fields omitted; an error result appends its `errors`), `SystemMessage#to_s` prints `[system: init]`, and `TextBlock#to_s` returns its text. `UserMessage` / `AssistantMessage` keep printing their text.
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
- `docs/rails.md` opens with a getting-started path (gem → generator → `install_cli` → first job), and its ActionCable, session-resumption and background-job examples use `ClaudeAgentSDK::Client.open` instead of hand-rolled `Async { connect … ensure disconnect }.wait`. README and gemspec description lead with the Rails integration.
|
|
25
|
+
- **`#inspect` filters credential-bearing attributes** to `"[FILTERED]"` (Hash keys stay visible): `ClaudeAgentOptions#env` (usually carries `ANTHROPIC_API_KEY`), `McpStdioServerConfig#env`, and `McpHttpServerConfig` / `McpSSEServerConfig#headers`, since these objects end up in logs. The objects are not modified. Type subclasses declare such attributes with `inspect_filtered :name`. Typed `SystemMessage` subclasses (`InitMessage`, ...) leave the raw `@data` frame out of `#inspect`, since it repeats their attributes; a bare `SystemMessage` keeps it. Nothing sent to the CLI changes: wire output still goes through `#to_h`.
|
|
26
|
+
- The README's `Client` section and the basic example in `docs/client.md` now lead with `Client.open`, which creates the reactor and always disconnects, instead of the `Async do … begin … ensure client.disconnect end.wait` boilerplate. The manual `connect` / `disconnect` form is still shown for code already running inside an `Async` reactor. No API changes.
|
|
27
|
+
- The bundled `claude-agent-ruby` skill recommends `Client.open` and documents the install generator, the `install_cli` task and `Railtie.callback_wrapper`.
|
|
28
|
+
- **Gem metadata names its maintainer** (`authors: ["ya-luotao"]`, with a contact email) instead of "Community Contributors". The stale `IMPLEMENTATION.md` is removed, and the past audit reports move from the repository root to `docs/history/`, which is not packaged with the gem.
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
- **The Rails `callback_wrapper` previously recommended in docs/rails.md, `->(inv) { Rails.application.executor.wrap { inv.call } }`, can deadlock in development.** With code reloading enabled, the request or job calling the SDK holds a share of the reload interlock while it waits for a callback running on its own thread (the default `:thread` scheduling); if a reload is requested meanwhile — e.g. after the agent edits an app file — the reloader queues for the exclusive lock and the callback's `executor.wrap` queues behind it, forever. With `config.allow_concurrency = false` the same wrapper blocks on the executor's monitor every time. The guide (and the `callback_wrapper` API docs and skill reference) now recommend `ClaudeAgentSDK::Railtie.callback_wrapper`; replace the bare lambda with it in existing initializers.
|
|
32
|
+
|
|
33
|
+
## [0.34.0] - 2026-09-23
|
|
34
|
+
|
|
35
|
+
The September 2026 audit campaign: 17 fixes from the final audit pass plus the 28 AUDIT-2026-09-22 issues (#66–#93). A few fixes tighten behaviour that was silently wrong — read **Changed** before upgrading.
|
|
36
|
+
|
|
37
|
+
### Added
|
|
38
|
+
- **`CLIInstaller::PINNED_CLI_VERSION`** (`'2.1.280'`, the CLI Python SDK 0.2.158 bundles) — the CLI version this gem release is developed and tested against; the Ruby equivalent of the Python SDK's bundled-CLI pin (`_cli_version.py`), except nothing is shipped inside the gem. Single source of truth: this constant is the only place the pin lives — docs and the transport's guidance reference it rather than repeating the literal.
|
|
39
|
+
- **`CLIInstaller.install_pinned(dir: nil)`** — installs exactly `PINNED_CLI_VERSION`. The Dockerfile / `bin/setup` form of "pin the tested pair": a deploy that calls it gets the SDK+CLI combination this release was tested with, and a Dependabot bump of the gem carries the CLI forward with it — no version literal in the caller to keep in sync. `install`'s default is unchanged (`'stable'` dist-tag), and explicit `version:` pins behave exactly as before.
|
|
40
|
+
- **`.github/workflows/cli-pin-bump.yml`** — scheduled (and manually dispatchable) workflow that reads the Python SDK's `_cli_version.py` on `main` and opens a PR moving `PINNED_CLI_VERSION` when it changes. It touches only that one line; cutting the follow-up patch release stays a human decision.
|
|
41
|
+
|
|
42
|
+
### Changed
|
|
43
|
+
- **Runtime dependency floors now name versions that actually work: `async >= 2.10, < 3` and `mcp >= 0.22, < 2`** (were `~> 2.0` / `>= 0.20`). A new CI leg runs the suite against exactly these floors: async 2.0.x cannot run on Ruby 3.2+ at all, releases before 2.6.4 break hook timeouts and control-request error delivery, and 2.10 is the first with `Task#defer_stop`, which `Query#close` now relies on. `mcp` 0.20/0.21 fail every `tools/call` of a tool whose schema uses `$ref` into `$defs`. Only bundles pinned below these floors are affected. (#87)
|
|
44
|
+
- **`config.default_options = {...}` stores a frozen deep copy.** Change defaults by assigning a new Hash; in-place mutation of the stored defaults (`config.default_options[:model] = 'x'`, `merge!`, including on the initial empty Hash) now raises `FrozenError`, and later changes to the Hash you passed have no effect. The snapshot's Strings are frozen too: a session built from configured defaults shares them, so reassign (`options.model = 'opus'`) rather than mutate in place. Your own Hash and objects, and identity-bearing values such as SDK MCP server instances, are never frozen. (#92)
|
|
45
|
+
- **`rename_session_via_store` / `tag_session_via_store` raise `Errno::ENOENT` for a session the store has never seen** (`#load` returns nil or `[]`), like their disk counterparts and `fork_session_via_store`, instead of appending — and so creating — a phantom session that could never be deleted on append-only stores. Deliberately stricter than the Python SDK's store helpers. (#85)
|
|
46
|
+
- **`exit`, `Interrupt` and signal exceptions raised inside an SDK MCP tool handler are reported in-band** as an `isError: true` result, so the `tools/call` control response is always written; previously they escaped dispatch (and in the default `:thread` scheduling `exit` tore down the session). Cancellation still propagates. A `callback_wrapper` now observes a `RuntimeError` whose `#cause` is the original exception. Hooks and `can_use_tool` are tracked in #119. (#77)
|
|
47
|
+
- **Transcript mirror `eager` mode no longer guarantees one `SessionStore#append` per frame.** At most one background drain runs at a time; frames arriving while an append is in flight are coalesced, in order, into the next one (see Fixed, #84). Adapters must not assume one frame per call.
|
|
48
|
+
- **Subagent readers only accept `agent_id`s matching `[A-Za-z0-9._-]+`** (never `.` or `..`); anything else returns `[]` / `nil` without calling the adapter, so a path- or prefix-keyed `SessionStore` can't be re-routed through the synthesized `subagents/agent-<agent_id>` subpath (now documented in the adapter contract). (#79)
|
|
49
|
+
- **Blank session metadata reads the same on disk and in a store:** whitespace-only `git_branch` / `tag` are `nil` and a blank `cwd` falls back to the project path on the disk path too. (#67)
|
|
50
|
+
- **S3 reference adapter (`examples/session_stores/s3_session_store.rb`)** reserves each part number through a per-transcript `.sequence` object with conditional writes, so appends stay ordered across adapter instances and clock skew. Requires an S3-compatible endpoint with strong consistency and conditional `PUT`; stop old writers before upgrading (mixed old/new writers are unsupported), and quiesce writers before deleting sessions. Each uncontended append costs two extra requests.
|
|
51
|
+
- **`CLIInstaller`'s `VERSION` file records the target platform** (OS, architecture, libc) alongside version and checksum, and the offline shortcut requires all three to match, so a cache copied from another platform is reinstalled instead of trusted. Older one- or two-line metadata needs one online reinstall, then works offline again.
|
|
52
|
+
- **SDK MCP servers reject a tool whose input schema declares a top-level `server_context` property** at registration (the `mcp` gem would silently overwrite it with its own context), and a call that passes one to a composed/referenced/free-form schema gets an actionable `isError` result without invoking the handler.
|
|
53
|
+
- The transport's "Claude Code not found" guidance, `docs/cli-installer.md` and the skill references now point at `install_pinned` (interpolating the constant) instead of a hardcoded example version that went stale with every CLI release.
|
|
54
|
+
- Docs, examples and the bundled `claude-agent-ruby` skill now use current model IDs (`claude-opus-5`, `claude-sonnet-5`, `claude-haiku-4-5`) and recommend adaptive thinking plus `effort:`; `ThinkingConfigEnabled(budget_tokens:)` is described as the older-model path. `examples/extended_thinking_example.rb` no longer teaches the deprecated `max_thinking_tokens`. No API changes.
|
|
55
|
+
|
|
56
|
+
### Fixed
|
|
57
|
+
|
|
58
|
+
**Subprocess transport and teardown**
|
|
59
|
+
- **A stdin write cancelled mid-frame now poisons the transport instead of corrupting the session.** An `Async::Stop`, inline cooperative timeout or deadline landing while a large frame was parked on a full pipe left `ready?` true, so the next frame was appended to the partial one and every later message was invalid JSON. The original cancellation still propagates; every later write raises `CLIConnectionError` ("possible partial frame"). Recovery is a new session. (#80)
|
|
60
|
+
- **`close` and `end_input` no longer hang or strand a writer parked on a full stdin pipe.** A reactor fiber parked in the write is woken with the documented `CLIConnectionError`, and on Ruby 3.3+ a reactor-side close no longer hangs the whole reactor while a FiberBoundary worker thread is parked in the write. (#80)
|
|
61
|
+
- **`read_messages` no longer waits forever for a CLI that closes stdout and then hangs.** After stdout EOF the SDK waits 5 s, then TERM, then KILL, and raises `ProcessError` ("did not exit within 5s of closing stdout", negative-signal `exit_code`). (#73)
|
|
62
|
+
- **A truncated final stdout frame is reported** as `CLIJSONDecodeError` (`line` holds the partial frame) instead of the stream silently ending without its `ResultMessage`. Whitespace-only tails and reads cut short by `close` stay silent. (#89)
|
|
63
|
+
- **`Client#disconnect` called from inside an inline control-request callback, or from a streaming-input enumerator, now finishes the teardown before the caller unwinds.** The callback's task belongs to the tree the close stops, and the cascading `Async::Stop` used to interrupt `Query#close` before the transport and the close watcher were closed (a bare `Query#close` leaked the CLI process). The stop is now deferred through the teardown; the caller then unwinds with `Async::Stop` (use `ensure`, not `rescue StandardError`). A teardown error superseded by that Stop is warned on stderr. `:thread` mode, message blocks and observers are unchanged: `disconnect` returns normally there. (#81)
|
|
64
|
+
- **An outer `Async` timeout during `close` no longer makes the transport forget a still-live CLI process**: the timeout propagates and the TERM/KILL fallback keeps ownership. A cancelled close's background fallback releases the process from the at-exit registry only once it has actually been reaped.
|
|
65
|
+
- **Control requests fail promptly when the stream ends**: a clean read EOF broadcasts a terminal error to every waiting control request (they used to sit out the 1200 s timeout), and requests issued after EOF are rejected before writing. A control request's serialization, write and wait now share one deadline and one cleanup scope, so a failed or cancelled send never leaks its waiter and backpressure can't block past the configured timeout.
|
|
66
|
+
- **CLI stderr is scrubbed to valid UTF-8** for the `stderr` callback, `debug_stderr` and `ProcessError#stderr`, like stdout frames. (#90)
|
|
67
|
+
- **CLI discovery no longer accepts a non-executable file at a well-known install location** — a stray 0644 `~/.claude/local/claude` now ends in `CLINotFoundError` with install instructions instead of a raw `Errno::EACCES` at spawn. (#72)
|
|
68
|
+
- **Store-backed resume and CLI discovery work on hosts without a usable home directory** (`HOME` unset with no passwd entry, e.g. `docker --user` in a minimal image, or an empty/relative `HOME`): home-relative auth seeds and install locations are skipped instead of raising `ArgumentError`. `Sessions.config_dir` / `projects_dir` are tracked in #120. (#82)
|
|
69
|
+
- **Sandbox settings merging resolves a relative settings file against the CLI's `cwd`**, not the Ruby process's working directory.
|
|
70
|
+
- **`FiberBoundary` worker threads disable `report_on_exception` before the callback runs**, so a fast-failing callback is no longer also dumped to stderr. (#93)
|
|
71
|
+
|
|
72
|
+
**Options and configuration**
|
|
73
|
+
- **Typed option values are no longer shared by reference between sessions.** `SandboxSettings` (and its network/filesystem configs), `SystemPromptPreset` / `Custom` / `File`, `ToolsPreset`, `AgentDefinition`, `HookMatcher`, thinking configs, `TaskBudget`, `Mcp*ServerConfig` and `SdkPluginConfig` placed in `ClaudeAgentSDK.configure` defaults, or copied by `ClaudeAgentOptions#dup_with` (including values nested in `agents`, `hooks`, `mcp_servers`, `plugins`), are copied per instance via the new `Type#dup_for_options` hook — a per-session change to sandbox rules or a system prompt can no longer silently change what every other session sends to the CLI. Unfrozen Strings are copied too. Procs, observers and factories, SDK MCP server instances, session-store adapters and `callback_wrapper` keep their identity; CLI arguments are unchanged. (#69, #70)
|
|
74
|
+
- **`Configuration#default_options` is no longer a live Hash read without synchronization**: request-time merges read a private frozen snapshot, so a concurrent in-place write can no longer raise `can't add a new key into hash during iteration` or tear a merge. (#92)
|
|
75
|
+
- **String-keyed SDK MCP server configs are recognized** (`{ 'tools' => { 'type' => 'sdk', 'instance' => server } }`, and `type: :sdk`): the instance is registered in-process, stdin stays open for its tool calls, and it is never serialized into `--mcp-config`. (#68)
|
|
76
|
+
- **`Client` normalizes input like `query()`**: a bare Hash prompt is rejected before anything is spawned (it used to stream Ruby inspection strings), and `nil` / empty hook lists are accepted instead of crashing `connect`.
|
|
77
|
+
- **Class-valued shorthand tool schemas work**: the documented `{ id: Integer }` advertises and accepts an integer (it advertised a string and rejected integers); `Float`, `TrueClass` and `FalseClass` likewise.
|
|
78
|
+
- **`ResultError`'s message and `#api_error_status` share one Integer narrowing**, so a non-Integer status (e.g. `"500"`) no longer appears as `API error (HTTP 500)` while the accessor returns `nil`. (#76)
|
|
79
|
+
|
|
80
|
+
**Sessions and SessionStore**
|
|
81
|
+
- **A single unserializable store entry no longer aborts a store-backed resume.** Entries (and subagent metadata sidecars) that JSON can't encode — NaN/Infinity, invalid UTF-8, circular or over-deep nesting — are skipped with a warning naming the entry's `uuid`; well-formed entries are written byte-for-byte as before. A session with no usable entries behaves like an empty one, and `--continue` classifies sidechains from the first surviving entry. (#83)
|
|
82
|
+
- **Non-String subkeys from a `SessionStore` are rejected** like any other unsafe subkey instead of raising `NoMethodError` and aborting the resume. (#91)
|
|
83
|
+
- **Store-backed listings coerce adapter `mtime` values** (ISO-8601 or numeric strings, e.g. SQL timestamps through JSON): they list newest first — they came back oldest first, so `limit:` cut off the newest sessions — and mixed Integer/String mtimes no longer raise. (#66)
|
|
84
|
+
- **Session listings break equal-mtime ties by `session_id`**, so `offset:` / `limit:` pages are stable across calls and never skip or repeat sessions, and disk and store listings order identical input identically. (#78)
|
|
85
|
+
- **Disk and store paths agree on sidechain classification and blank summaries**: a corrupt, blank, non-object or invalidly encoded first transcript line no longer makes one path list a session the other hides (the disk reader now classifies from the first parseable entry, as the store path did), and whitespace-only (or invalidly encoded) summaries/titles are blank on both. (#67, #75)
|
|
86
|
+
- **Session read APIs validate ids at the boundary**: a non-String or invalidly encoded `session_id` / `agent_id` gets the same `nil` / `[]` as a malformed id (and `import_session_to_store` raises `ArgumentError`) instead of a deep `NoMethodError` / `ArgumentError`. (#74)
|
|
87
|
+
- **The transcript mirror no longer piles up background tasks when the `SessionStore` is slower than the CLI's frame rate**: at most one drain task runs, and later frames are buffered and coalesced in order, without ever slowing the read loop. (#84)
|
|
88
|
+
- **Rename and tag no longer corrupt a transcript whose last record lacks a trailing newline**: the metadata record is written on its own line.
|
|
89
|
+
- **Forked sessions are published atomically**: the fork is written to a private temporary file and hard-linked into place, so a failed fork never leaves a partial session under its final id (filesystems without hard links fail safely).
|
|
90
|
+
- **An explicitly cleared custom or AI title at the end of a long transcript stays cleared** on the disk path (it resurrected an older title from the file head).
|
|
91
|
+
- **`InMemorySessionStore` deep-copies entries and summaries** across `append` / `load` / summary boundaries, so callers can no longer mutate stored transcripts through the objects they passed in or got back.
|
|
92
|
+
- **The SessionStore conformance kit checks summary content** (latest title, first timestamp, first prompt, sidechain classification across appends and refolds), so an adapter returning empty or stale summaries now fails it.
|
|
93
|
+
- Docs and the bundled skill describe what store-backed resume really seeds (`settings.json` / `cowork_settings.json` included since 0.31). (#86)
|
|
94
|
+
|
|
95
|
+
**Observability**
|
|
96
|
+
- **`OTelObserver` records per-turn cost increments** for `gen_ai.usage.cost` / `llm.cost.total`; the CLI reports cumulative `total_cost_usd`, so summing spans used to double-count earlier turns.
|
|
97
|
+
- **OpenTelemetry parent context is preserved across the SDK's fibers and threads** (`query()` / `Client.open` reactors, Query's background tasks, FiberBoundary worker dispatch), so observer and callback spans attach to the caller's trace instead of starting detached.
|
|
98
|
+
|
|
99
|
+
**Examples and tooling**
|
|
100
|
+
- `examples/error_handling_example.rb`'s retry helper now retries what the SDK raises: `ResultError` only for transient API statuses (429, 529, other 5xx), then bare `ProcessError` with bounded backoff. (#88)
|
|
101
|
+
- CI runs the suite against the newest allowed majors (json 3.x, mcp 1.x) and the declared dependency floors. The git-less gemspec fallback packages the same files git would; `docs/errors.md` lists `ResultError`; flaky ordering sleeps in specs are gone. (#87, #93)
|
|
102
|
+
|
|
10
103
|
## [0.33.1] - 2026-09-21
|
|
11
104
|
|
|
12
105
|
Compatibility with `json` 3.x and `mcp` 1.x. Upgrade if your bundle resolves `json` 3.x — `rename_session` / `tag_session` raise on 0.33.0.
|
data/README.md
CHANGED
|
@@ -8,26 +8,28 @@
|
|
|
8
8
|
[](https://rubydoc.info/gems/claude-agent-sdk)
|
|
9
9
|
[](LICENSE)
|
|
10
10
|
|
|
11
|
-
A Ruby SDK for the [Claude Code](https://docs.claude.com/en/docs/claude-code-overview) agent runtime
|
|
11
|
+
A Ruby SDK for the [Claude Code](https://docs.claude.com/en/docs/claude-code-overview) agent runtime, built for running agents in production Ruby and Rails apps. It has the same capabilities as the official [TypeScript](https://github.com/anthropics/claude-agent-sdk-typescript) and [Python](https://github.com/anthropics/claude-agent-sdk-python) SDKs, plus what a Rails deploy needs around them: a generator and CLI-vendoring rake task, callbacks that are safe to touch ActiveRecord from, a pinned CLI binary, built-in OpenTelemetry tracing, and transcript mirroring to your own storage.
|
|
12
12
|
|
|
13
13
|
> **Unofficial and community-maintained.** This project is not affiliated with or supported by Anthropic. It tracks the official SDKs release by release; see the [CHANGELOG](CHANGELOG.md) for the currently synced version.
|
|
14
14
|
|
|
15
15
|
## Highlights
|
|
16
16
|
|
|
17
|
+
- **Rails integration.** `bin/rails generate claude_agent_sdk:install` writes the initializer and `bin/rails claude_agent_sdk:install_cli` vendors the CLI; [docs/rails.md](docs/rails.md) covers jobs, ActionCable streaming, session resumption, and solid_queue fiber workers (`callback_scheduling: :inline`).
|
|
18
|
+
- **Callbacks that are safe around ActiveRecord.** Tool handlers, hooks, permission callbacks, and message blocks run on a plain thread by default, outside the SDK's fiber scheduler, so thread-keyed libraries (ActiveRecord, `pg`, per-thread caches) behave as they do everywhere else in your app. `ClaudeAgentSDK::Railtie.callback_wrapper` runs them in the Rails executor so connections go back to the pool, without deadlocking development code reloading.
|
|
19
|
+
- **Hermetic deploys.** `CLIInstaller` vendors a checksum-verified CLI binary, pinned to the version each gem release is tested with, so production never depends on a global `npm install`.
|
|
20
|
+
- **Built-in OpenTelemetry observer** with Langfuse support; no third-party instrumentation library required.
|
|
21
|
+
- **Transcript mirroring.** A `SessionStore` adapter mirrors session transcripts to your own storage (reference adapters for Postgres, Redis, and S3, plus a conformance suite), and sessions can be resumed from it on another host.
|
|
17
22
|
- **Same wire protocol as the official SDKs.** Spawns the `claude` CLI as a subprocess and speaks stream-JSON over stdin/stdout, so every feature of the runtime is available: sessions, subagents, sandboxing, structured output, file checkpointing and rewind.
|
|
18
23
|
- **`query()` for one-shot calls, `Client` for bidirectional sessions** with interrupts, mid-session model switching, and streaming input from any `Enumerator`.
|
|
19
24
|
- **In-process custom tools.** Define tools as Ruby blocks; they run inside your process with direct access to your app state (SDK MCP servers), with JSON-Schema-validated arguments.
|
|
20
25
|
- **All 27 hook events and permission callbacks** with typed inputs, so you can gate, audit, or rewrite every tool call.
|
|
21
|
-
- **Rails-ready.** Fiber-safe callback dispatch, an initializer-style `configure` block, ActionCable streaming, background-job session resumption, and a `callback_scheduling: :inline` mode for fiber workers.
|
|
22
|
-
- **Built-in OpenTelemetry observer** with Langfuse support; no third-party instrumentation library required.
|
|
23
26
|
- **Pluggable transport** to run the CLI somewhere else (an E2B microVM, a container, over SSH).
|
|
24
|
-
- **Hermetic deploys.** `CLIInstaller` vendors a checksum-verified, pinned CLI binary into your project so production never depends on a global `npm install`.
|
|
25
27
|
|
|
26
28
|
## Installation
|
|
27
29
|
|
|
28
30
|
```ruby
|
|
29
31
|
# Gemfile
|
|
30
|
-
gem 'claude-agent-sdk', '~> 0.
|
|
32
|
+
gem 'claude-agent-sdk', '~> 0.35.0'
|
|
31
33
|
```
|
|
32
34
|
|
|
33
35
|
Then `bundle install`, or install directly with `gem install claude-agent-sdk`. To track unreleased changes, point the Gemfile at GitHub: `gem 'claude-agent-sdk', github: 'ya-luotao/claude-agent-sdk-ruby'`.
|
|
@@ -44,6 +46,30 @@ ClaudeAgentSDK::CLIInstaller.install(version: '2.1.220') # => "/app/vendor/clau
|
|
|
44
46
|
|
|
45
47
|
The vendored binary is found ahead of `PATH`, installs are idempotent and concurrency-safe, and a failed upgrade never breaks a working install. See [docs/cli-installer.md](docs/cli-installer.md) for the full behaviour, supported platforms, and the CLI discovery order.
|
|
46
48
|
|
|
49
|
+
### Rails in a minute
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
bundle add claude-agent-sdk
|
|
53
|
+
bin/rails generate claude_agent_sdk:install # config/initializers/claude_agent_sdk.rb + .gitignore entry
|
|
54
|
+
bin/rails claude_agent_sdk:install_cli # the tested CLI into vendor/claude (also a Docker build step)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
```ruby
|
|
58
|
+
# app/jobs/summarize_ticket_job.rb
|
|
59
|
+
class SummarizeTicketJob < ApplicationJob
|
|
60
|
+
def perform(ticket)
|
|
61
|
+
options = ClaudeAgentSDK::ClaudeAgentOptions.new(tools: [], max_turns: 1)
|
|
62
|
+
prompt = "Summarize this support ticket in two sentences:\n\n#{ticket.body}"
|
|
63
|
+
|
|
64
|
+
ClaudeAgentSDK.query(prompt: prompt, options: options) do |message|
|
|
65
|
+
ticket.update!(summary: message.result) if message.is_a?(ClaudeAgentSDK::ResultMessage)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The block runs on a plain thread, so ActiveRecord calls inside it just work. [docs/rails.md](docs/rails.md) continues with multi-turn sessions, ActionCable streaming, and fiber workers.
|
|
72
|
+
|
|
47
73
|
## Quick Start
|
|
48
74
|
|
|
49
75
|
```ruby
|
|
@@ -84,23 +110,31 @@ end
|
|
|
84
110
|
|
|
85
111
|
### `Client` — bidirectional sessions
|
|
86
112
|
|
|
87
|
-
`Client` keeps a session open so you can send follow-up queries, interrupt, switch models, and use hooks, permission callbacks, and custom tools.
|
|
113
|
+
`Client` keeps a session open so you can send follow-up queries, interrupt, switch models, and use hooks, permission callbacks, and custom tools. `Client.open` connects, yields the client, and always disconnects when the block exits, even on an exception. It returns the block's value.
|
|
88
114
|
|
|
89
115
|
```ruby
|
|
90
116
|
require 'claude_agent_sdk'
|
|
91
|
-
require 'async'
|
|
92
117
|
|
|
93
|
-
|
|
94
|
-
client
|
|
118
|
+
ClaudeAgentSDK::Client.open do |client|
|
|
119
|
+
client.query("What is the capital of France?")
|
|
120
|
+
client.receive_response { |msg| puts msg }
|
|
95
121
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
122
|
+
client.query("And of Germany?")
|
|
123
|
+
client.receive_response { |msg| puts msg }
|
|
124
|
+
end
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
`Client.open` creates an [`async`](https://github.com/socketry/async) reactor when there isn't one; blocking calls yield automatically, no `await` needed. Called outside a reactor, `break` inside the block raises `LocalJumpError` (the client still disconnects), so return a value instead. Code that is already running inside an `Async` reactor can also manage the lifecycle by hand:
|
|
128
|
+
|
|
129
|
+
```ruby
|
|
130
|
+
client = ClaudeAgentSDK::Client.new
|
|
131
|
+
begin
|
|
132
|
+
client.connect
|
|
133
|
+
client.query("What is the capital of France?")
|
|
134
|
+
client.receive_response { |msg| puts msg }
|
|
135
|
+
ensure
|
|
136
|
+
client.disconnect
|
|
137
|
+
end
|
|
104
138
|
```
|
|
105
139
|
|
|
106
140
|
See [docs/client.md](docs/client.md) for `interrupt`, mid-session model and permission switching, MCP status, and custom transports.
|
|
@@ -148,7 +182,7 @@ See [docs/hooks-and-permissions.md](docs/hooks-and-permissions.md) for the full
|
|
|
148
182
|
| Session listing, reading, renaming, tagging, forking, resume-at-message | [docs/sessions.md](docs/sessions.md) |
|
|
149
183
|
| Subagent capabilities, event contracts, and minimal example | [docs/subagents.md](docs/subagents.md) |
|
|
150
184
|
| OpenTelemetry tracing, Langfuse, custom observers | [docs/observability.md](docs/observability.md) |
|
|
151
|
-
| Rails: fiber safety, solid_queue fiber workers, ActionCable, jobs
|
|
185
|
+
| Rails: generator, `install_cli` task, callback wrapper, fiber safety, solid_queue fiber workers, ActionCable, jobs | [docs/rails.md](docs/rails.md) |
|
|
152
186
|
| Vendoring a pinned CLI binary and CLI discovery order | [docs/cli-installer.md](docs/cli-installer.md) |
|
|
153
187
|
| Message, content block, and configuration type reference | [docs/types.md](docs/types.md) |
|
|
154
188
|
| Error handling, exception hierarchy, timeouts | [docs/errors.md](docs/errors.md) |
|
|
@@ -210,9 +244,10 @@ bundle install
|
|
|
210
244
|
bundle exec rspec # unit suite
|
|
211
245
|
bundle exec rubocop # lint
|
|
212
246
|
RUN_INTEGRATION=1 bundle exec rspec # also run the real-CLI integration suite (needs `claude` and ANTHROPIC_API_KEY)
|
|
247
|
+
BUNDLE_GEMFILE=gemfiles/rails_8.gemfile bundle exec rspec --options spec/rails/.rspec # Rails integration specs
|
|
213
248
|
```
|
|
214
249
|
|
|
215
|
-
CI runs the suite and RuboCop on Ruby 3.2, 3.3, and 3.4. See [spec/README.md](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/spec/README.md) for the test layout.
|
|
250
|
+
CI runs the suite and RuboCop on Ruby 3.2, 3.3, and 3.4, and the Rails specs against Rails 7.1 and 8. See [spec/README.md](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/spec/README.md) for the test layout.
|
|
216
251
|
|
|
217
252
|
## Contributing
|
|
218
253
|
|
data/docs/cli-installer.md
CHANGED
|
@@ -7,11 +7,18 @@ The SDK runs the `claude` CLI as a subprocess, so a deploy is only reproducible
|
|
|
7
7
|
```ruby
|
|
8
8
|
require 'claude_agent_sdk'
|
|
9
9
|
|
|
10
|
-
#
|
|
11
|
-
|
|
10
|
+
# Install the CLI version this gem release was tested against
|
|
11
|
+
# (CLIInstaller::PINNED_CLI_VERSION) — recommended: bumping the gem
|
|
12
|
+
# then carries the CLI forward with it, e.g. via Dependabot.
|
|
13
|
+
ClaudeAgentSDK::CLIInstaller.install_pinned
|
|
12
14
|
# => "/app/vendor/claude/claude"
|
|
13
15
|
|
|
14
|
-
|
|
16
|
+
# Or pin a concrete version of your own:
|
|
17
|
+
ClaudeAgentSDK::CLIInstaller.install(version: 'x.y.z', dir: '/opt/claude')
|
|
18
|
+
|
|
19
|
+
# 'stable' (the default) and 'latest' are floating dist-tags, not pins:
|
|
20
|
+
# they re-resolve on every call, so a rebuild may install a newer CLI.
|
|
21
|
+
ClaudeAgentSDK::CLIInstaller.install(version: 'stable')
|
|
15
22
|
|
|
16
23
|
# nil unless a binary is already installed there
|
|
17
24
|
ClaudeAgentSDK::CLIInstaller.installed_path
|
|
@@ -19,7 +26,7 @@ ClaudeAgentSDK::CLIInstaller.installed_path
|
|
|
19
26
|
|
|
20
27
|
`install` is idempotent and safe to run concurrently, so it fits `bin/setup`, a cached Docker layer, and every process of a multi-process boot:
|
|
21
28
|
|
|
22
|
-
- The install directory's `VERSION` file records the installed version
|
|
29
|
+
- The install directory's `VERSION` file records the installed version, verified SHA-256, and target platform (OS, architecture, libc). The shortcut re-hashes the vendored binary (~0.1s for the real 245MB binary) and only skips the download when all three match — a truncated binary or a cache copied from another platform is reinstalled instead of trusted. It makes **no network request**, so same-platform repeat boots work offline — with a pinned concrete version; `'stable'`/`'latest'` must always re-resolve through the endpoint, which is one more reason to pin in production. Older one- or two-line metadata lacks a platform and requires one online reinstall to migrate; subsequent pinned installs work offline again.
|
|
23
30
|
- An exclusive `flock` on `<dir>/.install.lock` covers the whole check → download → place → record sequence, so parallel installs into one directory don't race; the loser simply observes the finished install.
|
|
24
31
|
|
|
25
32
|
Failures (unsupported platform, invalid version, HTTP error, response-size cap, oversized download, checksum mismatch, filesystem errors) raise `ClaudeAgentSDK::CLIInstallError`.
|
|
@@ -31,18 +38,42 @@ Failures (unsupported platform, invalid version, HTTP error, response-size cap,
|
|
|
31
38
|
## Docker and `bin/setup`
|
|
32
39
|
|
|
33
40
|
```dockerfile
|
|
34
|
-
# Dockerfile —
|
|
41
|
+
# Dockerfile — the gem's pinned CLI version in its own cached layer
|
|
35
42
|
RUN bundle exec ruby -e "require 'claude_agent_sdk'; \
|
|
36
|
-
ClaudeAgentSDK::CLIInstaller.
|
|
43
|
+
ClaudeAgentSDK::CLIInstaller.install_pinned"
|
|
37
44
|
```
|
|
38
45
|
|
|
39
46
|
```ruby
|
|
40
47
|
#!/usr/bin/env ruby
|
|
41
|
-
# bin/setup
|
|
48
|
+
# bin/setup — gem's pin by default, overridable per developer
|
|
42
49
|
require 'claude_agent_sdk'
|
|
43
|
-
|
|
50
|
+
version = ENV.fetch('CLAUDE_CLI_VERSION', ClaudeAgentSDK::CLIInstaller::PINNED_CLI_VERSION)
|
|
51
|
+
puts ClaudeAgentSDK::CLIInstaller.install(version: version)
|
|
44
52
|
```
|
|
45
53
|
|
|
54
|
+
## Rake task
|
|
55
|
+
|
|
56
|
+
Rails apps get `claude_agent_sdk:install_cli` from the gem's Railtie; any other project can load it from its `Rakefile`:
|
|
57
|
+
|
|
58
|
+
```ruby
|
|
59
|
+
# Rakefile (non-Rails)
|
|
60
|
+
require 'claude_agent_sdk/tasks' # loads only CLIInstaller, not the whole SDK
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
bin/rails claude_agent_sdk:install_cli # Rails: installs PINNED_CLI_VERSION into Rails.root/vendor/claude
|
|
65
|
+
rake claude_agent_sdk:install_cli # elsewhere: into vendor/claude under the working directory
|
|
66
|
+
rake claude_agent_sdk:install_cli CLAUDE_CLI_VERSION=x.y.z # a version of your own, or 'stable' / 'latest'
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The task calls `install_pinned` (or `install(version:)` when `CLAUDE_CLI_VERSION` is set — the same variable the `bin/setup` example above reads), prints the installed path, and doesn't boot the Rails app, so it runs in a Docker build without a database or credentials:
|
|
70
|
+
|
|
71
|
+
```dockerfile
|
|
72
|
+
RUN bin/rails claude_agent_sdk:install_cli
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The variable is deliberately not rake's conventional `VERSION`, which Rails' `db:migrate` uses and build environments often export for an app version or git SHA. An empty `CLAUDE_CLI_VERSION` means the gem's pin.
|
|
76
|
+
|
|
46
77
|
## Supported platforms
|
|
47
78
|
|
|
48
79
|
`darwin-arm64`, `darwin-x64` (Rosetta 2 gets the arm64 build), `linux-x64`, `linux-arm64`, and the `-musl` variants. Windows is not supported.
|
|
@@ -54,4 +85,4 @@ With no explicit `cli_path:` in `ClaudeAgentOptions`, the transport probes in th
|
|
|
54
85
|
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:`)
|
|
55
86
|
2. The vendored binary (`CLIInstaller.installed_path`) — deliberately ahead of `PATH`, so a pinned install beats whatever is installed globally
|
|
56
87
|
3. `which claude`
|
|
57
|
-
4. Common install locations (`~/.claude/local/claude`, `/usr/local/bin/claude`, …)
|
|
88
|
+
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
|
@@ -4,29 +4,38 @@
|
|
|
4
4
|
|
|
5
5
|
## Basic Usage
|
|
6
6
|
|
|
7
|
+
`Client.open` connects, yields the client, and always disconnects when the block exits (exceptions propagate after the disconnect). It returns the block's value, and creates an `async` reactor if it isn't already running inside one.
|
|
8
|
+
|
|
7
9
|
```ruby
|
|
8
10
|
require 'claude_agent_sdk'
|
|
9
|
-
require 'async'
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
client
|
|
12
|
+
ClaudeAgentSDK::Client.open do |client|
|
|
13
|
+
client.query("What is the capital of France?")
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
when ClaudeAgentSDK::AssistantMessage
|
|
21
|
-
puts msg.text
|
|
22
|
-
when ClaudeAgentSDK::ResultMessage
|
|
23
|
-
puts "Cost: $#{msg.total_cost_usd}" if msg.total_cost_usd
|
|
24
|
-
end
|
|
15
|
+
client.receive_response do |msg|
|
|
16
|
+
case msg
|
|
17
|
+
when ClaudeAgentSDK::AssistantMessage
|
|
18
|
+
puts msg.text
|
|
19
|
+
when ClaudeAgentSDK::ResultMessage
|
|
20
|
+
puts "Cost: $#{msg.total_cost_usd}" if msg.total_cost_usd
|
|
25
21
|
end
|
|
26
|
-
ensure
|
|
27
|
-
client.disconnect
|
|
28
22
|
end
|
|
29
|
-
end
|
|
23
|
+
end
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Called outside a reactor, `break` inside the `Client.open` block raises `LocalJumpError` (the client still disconnects), so return a value from the block instead. `break` inside `receive_response` / `receive_messages` is fine. It stops the iteration.
|
|
27
|
+
|
|
28
|
+
If your code already runs inside an `Async` reactor and you want to manage the connection yourself, call `connect` and `disconnect` directly:
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
client = ClaudeAgentSDK::Client.new
|
|
32
|
+
begin
|
|
33
|
+
client.connect
|
|
34
|
+
client.query("What is the capital of France?")
|
|
35
|
+
client.receive_response { |msg| puts msg }
|
|
36
|
+
ensure
|
|
37
|
+
client.disconnect
|
|
38
|
+
end
|
|
30
39
|
```
|
|
31
40
|
|
|
32
41
|
## Advanced Features
|
|
@@ -38,7 +47,7 @@ Async do
|
|
|
38
47
|
|
|
39
48
|
client.interrupt # Send interrupt signal
|
|
40
49
|
client.set_permission_mode('acceptEdits') # Change permission mode mid-conversation
|
|
41
|
-
client.set_model('claude-sonnet-
|
|
50
|
+
client.set_model('claude-sonnet-5') # Switch model mid-conversation
|
|
42
51
|
status = client.get_mcp_status # Inspect MCP server status
|
|
43
52
|
info = client.get_server_info # Inspect server init info
|
|
44
53
|
client.reconnect_mcp_server('my-server') # Reconnect a failed MCP server
|
data/docs/configuration.md
CHANGED
|
@@ -58,7 +58,7 @@ Use the `effort` option to control the model's effort level:
|
|
|
58
58
|
options = ClaudeAgentSDK::ClaudeAgentOptions.new(effort: 'xhigh')
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
-
Valid levels live in `ClaudeAgentSDK::EFFORT_LEVELS` (`low`, `medium`, `high`, `xhigh`, `max`). The set of *supported* levels is model-dependent — `xhigh` is available on Opus 4.7 and the CLI falls back to the highest supported level at or below the one you set (e.g. `xhigh` → `high` on Opus 4.6). When `effort` is `nil`, the CLI picks a model-native default (Opus 4.7 → `xhigh`).
|
|
61
|
+
Valid levels live in `ClaudeAgentSDK::EFFORT_LEVELS` (`low`, `medium`, `high`, `xhigh`, `max`). The set of *supported* levels is model-dependent — `xhigh` is available on Opus 4.7 and later models, and the CLI falls back to the highest supported level at or below the one you set (e.g. `xhigh` → `high` on Opus 4.6). When `effort` is `nil`, the CLI picks a model-native default (e.g. Opus 4.7 → `xhigh`).
|
|
62
62
|
|
|
63
63
|
> **Note:** When `system_prompt` is `nil` (the default), the SDK passes `--system-prompt ""` to the CLI, which suppresses the default Claude Code system prompt. To use the default system prompt, use a `SystemPromptPreset`.
|
|
64
64
|
|
|
@@ -119,8 +119,8 @@ See [examples/budget_control_example.rb](https://github.com/ya-luotao/claude-age
|
|
|
119
119
|
|
|
120
120
|
```ruby
|
|
121
121
|
options = ClaudeAgentSDK::ClaudeAgentOptions.new(
|
|
122
|
-
model: 'claude-sonnet-
|
|
123
|
-
fallback_model: 'claude-
|
|
122
|
+
model: 'claude-sonnet-5',
|
|
123
|
+
fallback_model: 'claude-haiku-4-5'
|
|
124
124
|
)
|
|
125
125
|
```
|
|
126
126
|
|
|
@@ -135,8 +135,8 @@ receives the full conversation.
|
|
|
135
135
|
|
|
136
136
|
```ruby
|
|
137
137
|
options = ClaudeAgentSDK::ClaudeAgentOptions.new(
|
|
138
|
-
model: 'haiku',
|
|
139
|
-
advisor_model: 'opus' #
|
|
138
|
+
model: 'claude-haiku-4-5',
|
|
139
|
+
advisor_model: 'claude-opus-5' # full model ID, or an alias such as 'opus'
|
|
140
140
|
)
|
|
141
141
|
```
|
|
142
142
|
|
data/docs/errors.md
CHANGED
|
@@ -138,11 +138,12 @@ end
|
|
|
138
138
|
| Error | Description |
|
|
139
139
|
|-------|-------------|
|
|
140
140
|
| `ClaudeSDKError` | Base error for all SDK errors |
|
|
141
|
-
| `CLIConnectionError` | Connection issues |
|
|
141
|
+
| `CLIConnectionError` | Connection issues — including every write after a stdin write was cancelled mid-frame (the connection is unusable from then on — reconnect) |
|
|
142
142
|
| `ControlRequestTimeoutError` | Control protocol timeout (configurable via env var) |
|
|
143
143
|
| `CLINotFoundError` | Claude Code not installed |
|
|
144
|
-
| `ProcessError` | Process failed (includes `exit_code` and `stderr`) |
|
|
145
|
-
| `
|
|
144
|
+
| `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
|
+
| `ResultError` | Run ended on a terminal error result (subclasses `ProcessError`; adds `subtype`, `errors`, `api_error_status`, `terminal_reason`, ...) — rescue it first |
|
|
146
|
+
| `CLIJSONDecodeError` | JSON parsing issues — including stdout ending mid-frame (a truncated final message; `line` holds the partial frame) |
|
|
146
147
|
| `MessageParseError` | Message parsing issues |
|
|
147
148
|
|
|
148
149
|
See [lib/claude_agent_sdk/errors.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/lib/claude_agent_sdk/errors.rb) for all error types.
|
data/docs/mcp-servers.md
CHANGED
|
@@ -41,6 +41,17 @@ end.wait
|
|
|
41
41
|
|
|
42
42
|
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:
|
|
43
43
|
|
|
44
|
+
**Reserved argument name:** do not use `server_context` as a top-level tool
|
|
45
|
+
argument. The underlying MCP gem uses that keyword for injected request context
|
|
46
|
+
and would overwrite the user value before your handler runs. Registering a tool
|
|
47
|
+
whose root `properties` declare it raises `ArgumentError`, for both shorthand and
|
|
48
|
+
pre-built schemas (including directly constructed `SdkMcpTool` objects). For
|
|
49
|
+
composed, referenced, or free-form schemas, actual MCP calls containing this
|
|
50
|
+
top-level key return an actionable `isError: true` tool result before the handler
|
|
51
|
+
runs. This runtime guard remains active even if schema validation falls back to
|
|
52
|
+
a permissive schema. Rename the argument to something like `request_context`.
|
|
53
|
+
Nested object properties named `server_context` are safe and remain supported.
|
|
54
|
+
|
|
44
55
|
```ruby
|
|
45
56
|
# Symbol keys (standard Ruby)
|
|
46
57
|
ClaudeAgentSDK.create_tool('save', 'Save a fact', {
|
|
@@ -92,6 +103,12 @@ options = ClaudeAgentSDK::ClaudeAgentOptions.new(
|
|
|
92
103
|
)
|
|
93
104
|
```
|
|
94
105
|
|
|
106
|
+
An exception raised inside a handler is returned to the model as an
|
|
107
|
+
`isError: true` result carrying the exception message, so it can self-correct.
|
|
108
|
+
This includes `exit`, `Interrupt` and other signal exceptions: they are reported
|
|
109
|
+
the same way instead of escaping the session and leaving the CLI waiting on the
|
|
110
|
+
tool call. Cancellation of the tool call itself still propagates.
|
|
111
|
+
|
|
95
112
|
## Mixed Server Support
|
|
96
113
|
|
|
97
114
|
You can use both SDK and external MCP servers together:
|
|
@@ -108,6 +125,11 @@ options = ClaudeAgentSDK::ClaudeAgentOptions.new(
|
|
|
108
125
|
)
|
|
109
126
|
```
|
|
110
127
|
|
|
128
|
+
A hand-written SDK entry may use String or Symbol keys:
|
|
129
|
+
`{ 'type' => 'sdk', 'name' => 'calc', 'instance' => server }` behaves exactly like
|
|
130
|
+
the Hash `create_sdk_mcp_server` returns. The instance is registered in-process
|
|
131
|
+
and is never sent to the CLI.
|
|
132
|
+
|
|
111
133
|
## MCP Resources and Prompts
|
|
112
134
|
|
|
113
135
|
SDK MCP servers can also expose **resources** (data sources) and **prompts** (reusable templates):
|
data/docs/observability.md
CHANGED
|
@@ -6,6 +6,8 @@ The SDK includes a built-in **observer interface** and an **OpenTelemetry observ
|
|
|
6
6
|
|
|
7
7
|
When `connect` spawns the CLI and there is an active OTel span, the SDK injects `TRACEPARENT`/`TRACESTATE` (and any other propagator carrier keys, e.g. `BAGGAGE` — which may carry user-defined key/values — uppercased) into the subprocess environment so CLI-side telemetry (`CLAUDE_CODE_ENABLE_TELEMETRY=1`) joins the caller's distributed trace. This requires the `opentelemetry` gem to be loaded with a configured propagator — there is no hard dependency, and it is a no-op otherwise. Explicit `ClaudeAgentOptions#env` keys always win; stale inherited `TRACEPARENT`/`TRACESTATE` is replaced (or unset) only when an active span supersedes it. This works independently of `OTelObserver`: the CLI parents under the caller's surrounding span, not under `claude_agent.session` (which starts at InitMessage, after spawn).
|
|
8
8
|
|
|
9
|
+
The SDK also carries the active OTel context (including baggage) into the reactor fibers created by `query()` and standalone `Client.open`, their background input/control tasks, and callback worker-thread hops. `OTelObserver` session spans therefore keep the surrounding caller span as their parent in both default `:thread` and opt-in `:inline` mode. For `Client`, keep the intended parent active while connecting and receiving messages: background control callbacks inherit the connection's context, while message observers use the receiving operation's context. Context is captured per operation/task, not when an observer is constructed, so sequential observer reuse does not retain an earlier caller's parent. This uses OTel's scoped context API only when loaded; it does not copy application thread-local state or change callback scheduling.
|
|
10
|
+
|
|
9
11
|
## How It Works
|
|
10
12
|
|
|
11
13
|
Register observers via `ClaudeAgentOptions`. The SDK calls `on_user_prompt` when a prompt is sent — the verbatim string for String prompts (`query()` / `Client#query`), and once per `type: 'user'` message with extractable text for Enumerator/streaming input (`query()` stream path and `Client#connect` with an initial enumerable). It calls `on_message` for every parsed message, `on_error` once per error that surfaces to your code (before `on_close` where both fire), and `on_close` when the session ends. Observer errors are silently rescued so they never crash your application.
|
|
@@ -116,6 +118,10 @@ The OTel observer sets attributes using both `gen_ai.*` (OTel GenAI) and OpenInf
|
|
|
116
118
|
| `claude_agent.generation` | `generation` | `gen_ai.response.model`, `gen_ai.usage.input_tokens`, `gen_ai.usage.output_tokens`, `gen_ai.usage.cache_creation_input_tokens`, `gen_ai.usage.cache_read_input_tokens`, `output.value` |
|
|
117
119
|
| `claude_agent.tool.*` | `tool` | `tool.name`, `input.value`, `output.value` |
|
|
118
120
|
|
|
121
|
+
`gen_ai.usage.cost` and `llm.cost.total` record the **increase** since the last observed `ResultMessage.total_cost_usd` in the connected session, rather than repeating that cumulative total on every turn's span. The baseline survives per-turn span resets and resets on close, a changed session ID (such as `/clear`), or a decreased counter. The original `ResultMessage` is unchanged.
|
|
122
|
+
|
|
123
|
+
Missing costs omit both attributes without discarding the last known total; the next reported increment can therefore include an unreported or interrupted turn. The first observation uses the CLI's reported total. If a newer CLI restores historical spend on resume, that first span also includes it: a fresh observer cannot separate spend it never observed. These are CLI cost estimates, not billing records.
|
|
124
|
+
|
|
119
125
|
Events (`api_retry`, `rate_limit`, `tool_progress`) are recorded on the root span.
|
|
120
126
|
|
|
121
127
|
The `langfuse.observation.type` attribute is set on each span (`agent`/`generation`/`tool`) to enable Langfuse's **trace flow diagram** (DAG graph visualization).
|