claude-agent-sdk 0.37.0 → 1.1.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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +37 -0
  3. data/README.md +6 -2
  4. data/UPGRADING-1.0.md +151 -0
  5. data/docs/client.md +11 -0
  6. data/docs/configuration.md +42 -0
  7. data/docs/errors.md +6 -0
  8. data/docs/sessions.md +20 -1
  9. data/docs/types.md +17 -14
  10. data/lib/claude_agent_sdk/cli_installer.rb +1 -1
  11. data/lib/claude_agent_sdk/deprecation.rb +1 -40
  12. data/lib/claude_agent_sdk/errors.rb +10 -0
  13. data/lib/claude_agent_sdk/query.rb +320 -56
  14. data/lib/claude_agent_sdk/session_resume.rb +11 -5
  15. data/lib/claude_agent_sdk/subprocess_cli_transport.rb +25 -0
  16. data/lib/claude_agent_sdk/types/attributes.rb +14 -49
  17. data/lib/claude_agent_sdk/types/base.rb +2 -0
  18. data/lib/claude_agent_sdk/types/messages.rb +7 -1
  19. data/lib/claude_agent_sdk/types/options.rb +69 -12
  20. data/lib/claude_agent_sdk/version.rb +1 -1
  21. data/lib/claude_agent_sdk.rb +28 -18
  22. data/sig/claude_agent_sdk/cancellation_signal.rbs +14 -0
  23. data/sig/claude_agent_sdk/configuration.rbs +14 -0
  24. data/sig/claude_agent_sdk/errors.rbs +86 -0
  25. data/sig/claude_agent_sdk/observer.rbs +42 -0
  26. data/sig/claude_agent_sdk/railtie.rbs +10 -0
  27. data/sig/claude_agent_sdk/sdk_mcp_server.rbs +76 -0
  28. data/sig/claude_agent_sdk/session_store.rbs +105 -0
  29. data/sig/claude_agent_sdk/streaming.rbs +15 -0
  30. data/sig/claude_agent_sdk/transport.rbs +98 -0
  31. data/sig/claude_agent_sdk/types/base.rbs +39 -0
  32. data/sig/claude_agent_sdk/types/content_blocks.rbs +79 -0
  33. data/sig/claude_agent_sdk/types/hooks.rbs +528 -0
  34. data/sig/claude_agent_sdk/types/mcp.rbs +216 -0
  35. data/sig/claude_agent_sdk/types/messages.rbs +586 -0
  36. data/sig/claude_agent_sdk/types/option_values.rbs +245 -0
  37. data/sig/claude_agent_sdk/types/options.rbs +297 -0
  38. data/sig/claude_agent_sdk/types/permissions.rbs +108 -0
  39. data/sig/claude_agent_sdk/types/sessions.rbs +66 -0
  40. data/sig/claude_agent_sdk.rbs +231 -0
  41. data/sig/manifest.yaml +5 -0
  42. metadata +23 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7d238195bb7f1725358c2ca33a8396ff1f4ab4121c7cc905e1dc2e1957a4553e
4
- data.tar.gz: 480e5d771b68e72b0646bfa102cd921b1797d7134a925c496c5575a5e795c88e
3
+ metadata.gz: ecc2b309c23e9e07ce4d03aed2997d7f8c2b3fc8909f48601bfd3c1b0ca93152
4
+ data.tar.gz: f7c56c6cde059b2edf3b57e633ac49fa761074d633a9532f503ff2261b846795
5
5
  SHA512:
6
- metadata.gz: 91d8f9de96e91aa5297e2342f33a57de100807978ac5c6bb7739927f99e0c41707f335c4e41512c9d843bf19afdc3d30ab97b526319e3cc45d932e5bae2cc761
7
- data.tar.gz: e7d2504b2dcda1c0ee5d1d4174d9ece35efc1a53b9832b00595829adacc6039f423d4db4999e1139d1fb6ad93c3b532dad119f9aceb51305328d3d25d847e7f6
6
+ metadata.gz: 422bd92af76c1785ce33e3c50b7750d74c4dbdeb096445a0f441497be2e3f5f41a463cabd7f55374606fa236c524ae356e4a282cd75db9c7fde3fbbd4e8e07e2
7
+ data.tar.gz: 17b52f5f14f603cadb4fdc989b4236a5d75c4862f1ec8672014548288a67f886ede5695afce8078327d779331da87b82cc5fbfcc3fadee419037615ce7bc134b
data/CHANGELOG.md CHANGED
@@ -7,6 +7,43 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.1.0] - 2026-09-30
11
+
12
+ Syncs with Python SDK 0.2.162. Additive only: one new option and a fix to when `query()` closes stdin.
13
+
14
+ ### Added
15
+ - **`ClaudeAgentOptions#verbatim_prompts`** (default `false`; Python SDK #1269). When `true`, every user message the SDK writes carries `client_composed: true`: String prompts and each message of a streamed prompt, through `query`, `Client#connect` and `Client#query`. Claude Code then delivers the text as written, with no `@path` file expansion and no slash-command dispatch. Use it when a prompt includes text your end user did not type. `tools: []` and `disallowed_tools` do not stop `@path` expansion, which reads the file before the model runs. While the option is on, a caller's `client_composed` key is overwritten, and caller Hashes are never mutated. A streamed JSONL String is parsed and re-serialized; one that is not a single JSON object raises `ArgumentError` rather than going out unmarked. `Client` reads the option once, at `connect`. On current CLIs such a turn also skips the turn-start attachment pass (nested `CLAUDE.md` and rules files, skill and tool listings). Requires Claude Code 2.1.248 or later; the SDK warns when it connects to an older CLI with the option on. See `docs/configuration.md`.
16
+ - Development only, nothing ships in the gem: TLA+ models in `formal/tla/` of the two most concurrency-sensitive designs. `CLIInstaller.tla` covers `CLIInstaller.install`'s publish order, the in-lock dist-tag resolve and the stale-temp sweep. `ControlProtocol.tla` covers the outbound control-request waiter protocol in `Query`. `formal/tla/run.sh` model-checks both with TLC. It confirms that the shipped design passes and that each alternative the source comments reject, including the historical rename-then-record order, produces a counterexample.
17
+
18
+ ### Changed
19
+ - `CLIInstaller::PINNED_CLI_VERSION` moves from 2.1.280 (the 1.0.0 pin) to **2.1.285**, the CLI the Python SDK 0.2.162 bundles. `CLIInstaller.install_pinned` installs it. It honors both `client_composed` (`verbatim_prompts`) and `CLAUDE_CODE_SDK_READS_SESSION_STATE` (the fix below).
20
+
21
+ ### Fixed
22
+ - **`query()` with hooks, `can_use_tool` or SDK MCP servers no longer closes stdin while a follow-up turn is still owed** (Python SDK #1279, fixing Python issue #1190). A background subagent that finished just before the turn's result was already off the in-flight ledger, so stdin closed at that result, and the follow-up turn its completion triggered had every hook, permission and SDK MCP request fail with "Stream closed" (the model reported the tool as refused). The transport now sets `CLAUDE_CODE_SDK_READS_SESSION_STATE=1` unless `options.env` or the environment already names it (in any case; a `nil` value in `options.env` unsets it). The CLI then sends `session_state_changed` frames marked `sdk_host_only`, which the SDK drops from the message stream for `query()` and `Client` alike. `query()` keeps stdin open until the CLI reports `idle` after a result:
23
+ - The wait between turns is bounded by `CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS`, read from `options.env` and then the environment; the default is 10 minutes and `0` means no limit. The clock restarts at each result and at each `running`. A main-thread turn, the CLI reporting `requires_action`, a tracked background agent and a hook, permission or SDK MCP request the SDK is still answering each stop it.
24
+ - With an Enumerable prompt, each message written waits for its own run. Work the CLI takes up after the run ended reopens it.
25
+ - A CLI that sends no state (2.1.282 and earlier) gets the old behavior: stdin closes at the first result with no tracked task in flight.
26
+ - Background agents, bounded monitors and MCP tasks can now keep a one-shot run open, up to the ceiling. Background shells, persistent monitors and remote agents do not, because the CLI leaves them out of its `running` report.
27
+ - `SessionStateChangedMessage` reaches your code only if you set `CLAUDE_CODE_EMIT_SESSION_STATE_EVENTS=1` yourself; the SDK never sets it.
28
+ - A custom transport must pass `CLAUDE_CODE_SDK_READS_SESSION_STATE=1` to the CLI itself to get this behavior (see `docs/client.md`); the E2B example transport now does.
29
+
30
+ ## [1.0.0] - 2026-09-23
31
+
32
+ **1.0 is a stability commitment.** From here on the public API — everything documented in `docs/` plus the YARD docs without `@api private`, now also described by the RBS signatures in `sig/` — follows Semantic Versioning: no breaking changes within 1.x. Upgrading from 0.x? Read [UPGRADING-1.0.md](UPGRADING-1.0.md) and run your suite on 0.37 first.
33
+
34
+ 1.0 ([#126](https://github.com/ya-luotao/claude-agent-sdk-ruby/issues/126)): three breaking changes, the first two of which 0.37 warns about at the call site. **Read [UPGRADING-1.0.md](UPGRADING-1.0.md) before upgrading** and run your suite on 0.37 with warnings visible first: an app that runs on 0.37 without SDK warnings is unaffected by those two, except that `respond_to?` on a camelCase non-attribute (`msg.respond_to?(:toH)`) silently answered `true` on 0.37 and answers `false` now.
35
+
36
+ ### Added
37
+ - **`ClaudeAgentSDK::SessionStoreError`** (a `ClaudeSDKError`), raised by `query`, `ask` and `Client#connect` when resuming from `session_store:` fails: a store call (`#load`, `#list_sessions`, `#list_subkeys`) raised or exceeded `load_timeout_ms` while the SDK materialized the transcript. The message names the store call, and `#cause` holds the adapter's exception (or the timeout). Documented in `docs/errors.md` and `docs/sessions.md`.
38
+ - **RBS signatures for the public API** ([#126](https://github.com/ya-luotao/claude-agent-sdk-ruby/issues/126)). The gem now ships `sig/`, so Steep and other RBS tools type-check code that uses it (through `rbs collection`; `sig/manifest.yaml` declares the one stdlib dependency, `pathname`). The signatures cover the whole public surface (the objects documented in `docs/` and YARD without `@api private`): `ClaudeAgentSDK.query` / `.ask` / `.configure` / `.offload`, the SDK MCP helpers, every session function (including `session_store:` and the deprecated twins), `Client` with its Ruby-style aliases, `ClaudeAgentOptions` (every option as a typed keyword), the message, content-block, hook, permission and MCP types, the error hierarchy, `CLIInstaller`, `Streaming`, `Railtie.callback_wrapper` and the observers. Duck types are interfaces, so any object with the right methods fits: `_Transport`, `_SessionStore` (only `#append` and `#load` are required), and one interface per user callback (`_CanUseTool`, `_HookCallback`, `_ToolHandler`, `_CallbackWrapper`, ...). Hashes follow the documented key rule: `wire_hash` (`Hash[Symbol, untyped]`) for data from the CLI stream, `transcript_hash` (`Hash[String, untyped]`) for transcripts and store data. Internals tagged `@api private` have no signatures. CI validates the signatures (`rake rbs:validate`) and runs the suite under RBS's runtime type checker (`rake rbs:test`), so a signature that disagrees with the code fails the build. `CONTRIBUTING.md` explains how to keep `sig/` in step with API changes.
39
+
40
+ ### Changed
41
+ - **Breaking: unknown keys on user-constructed types raise `ArgumentError`.** `.new` and `#[]=` on the value types you build and pass in (option values such as `AgentDefinition`, `SandboxSettings`, the thinking and MCP server configs; `HookMatcher`; hook outputs; `PermissionResultAllow` / `PermissionResultDeny`; `PermissionUpdate`; `PermissionRuleValue`) raise instead of warning, with a message naming the class, the key and the known keys: `ClaudeAgentSDK::HookMatcher: unknown attribute :matchr (known: hooks, matcher, timeout)`. camelCase and String keys and a type's own discriminator are still accepted; `.from_hash`, `.wrap` and every type parsed from CLI output stay lenient.
42
+ - **Breaking: `Type#[]`, `#[]=` and camelCase methods reach attributes only.** A name that is not an attribute behaves like an undefined one: `msg[:to_h]` is `nil`, `#[]=` ignores it (raises on the strict types above), `msg.toH` raises `NoMethodError` and `respond_to?(:toH)` is `false`. Methods your own code adds to a subclass, mixin or instance still count as attributes.
43
+ - **Breaking: store-backed resume failures raise `SessionStoreError` instead of `RuntimeError`.** The two SDK-raised `RuntimeError`s (store call failed, store call timed out) become `SessionStoreError`, and a `RuntimeError` raised by the adapter itself, which 0.37 let through unwrapped, is now wrapped too, so `rescue ClaudeSDKError` catches every resume-materialization failure. Code that rescued `RuntimeError` there must rescue `SessionStoreError`. The failure message now includes the adapter exception's class (`... failed during resume materialization: IOError: connection reset`).
44
+ - `UPGRADING-1.0.md` ships in the gem and the YARD docs, linked from the README.
45
+ - The deprecation warning printed by the ten `*_from_store` / `*_via_store` session functions, and their YARD and docs, now say they will be removed in **2.0**. 0.36 and 0.37 said 1.0, but they stay, deprecated, for all of 1.x ([#126](https://github.com/ya-luotao/claude-agent-sdk-ruby/issues/126)). Nothing else about them changes: each still works and still warns once per process.
46
+
10
47
  ## [0.37.0] - 2026-09-23
11
48
 
12
49
  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:**
data/README.md CHANGED
@@ -12,6 +12,8 @@ A Ruby SDK for the [Claude Code](https://docs.claude.com/en/docs/claude-code-ove
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
+ > **Upgrading from 0.x?** 1.0 raises on unknown keys, limits `#[]` to attributes and adds `SessionStoreError`. [UPGRADING-1.0.md](UPGRADING-1.0.md) has the checklist.
16
+
15
17
  ## Highlights
16
18
 
17
19
  - **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`).
@@ -29,7 +31,7 @@ A Ruby SDK for the [Claude Code](https://docs.claude.com/en/docs/claude-code-ove
29
31
 
30
32
  ```ruby
31
33
  # Gemfile
32
- gem 'claude-agent-sdk', '~> 0.37.0'
34
+ gem 'claude-agent-sdk', '~> 1.0'
33
35
  ```
34
36
 
35
37
  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'`.
@@ -252,11 +254,13 @@ This repository is also a Claude Code plugin marketplace. The bundled skill teac
252
254
  bundle install
253
255
  bundle exec rspec # unit suite
254
256
  bundle exec rubocop # lint
257
+ bundle exec rake rbs:validate # validate the RBS signatures in sig/
258
+ bundle exec rake rbs:test # the suite under RBS runtime type checking
255
259
  RUN_INTEGRATION=1 bundle exec rspec # also run the real-CLI integration suite (needs `claude` and ANTHROPIC_API_KEY)
256
260
  BUNDLE_GEMFILE=gemfiles/rails_8.gemfile bundle exec rspec --options spec/rails/.rspec # Rails integration specs
257
261
  ```
258
262
 
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.
263
+ 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, validates the RBS signatures and runs the suite under RBS runtime type checking; a weekly job runs the integration suite against the pinned CLI. The gem ships RBS signatures for its public API in `sig/`, which Steep and other RBS tools pick up through `rbs collection`. 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.
260
264
 
261
265
  ## Contributing
262
266
 
data/UPGRADING-1.0.md ADDED
@@ -0,0 +1,151 @@
1
+ # Upgrading from 0.37 to 1.0
2
+
3
+ 1.0 is 0.37 plus three breaking changes. 0.37 already warns about each of the
4
+ first two at the exact call site, so **if your app runs on 0.37 without SDK
5
+ warnings, the first two changes will not affect it on 1.0**, with one silent
6
+ exception: `respond_to?` on a camelCase name that is not an attribute
7
+ (`msg.respond_to?(:toH)`) answered `true` on 0.37 without a warning and answers
8
+ `false` on 1.0. The third change is to which exception class you rescue.
9
+
10
+ | Area | 0.37 | 1.0 |
11
+ |------|------|-----|
12
+ | Unknown key on a type you build (`HookMatcher.new(matchr: ...)`) | warns once, key ignored | raises `ArgumentError` |
13
+ | `#[]` / `#[]=` / camelCase reaching a non-attribute (`msg[:to_h]`, `msg.toH`) | works, warns once | treated as undefined |
14
+ | Store-backed resume failure | bare `RuntimeError` | `ClaudeAgentSDK::SessionStoreError` |
15
+
16
+ ## Checklist
17
+
18
+ 1. Upgrade to 0.37 first: `gem 'claude-agent-sdk', '~> 0.37.0'`, then `bundle update claude-agent-sdk`.
19
+ 2. Run your test suite, and exercise a staging boot, with warnings visible and
20
+ stderr kept: no `-W0`, no `$VERBOSE = nil`, and no stderr filtering.
21
+ For example, `bundle exec rspec 2> sdk-warnings.log`.
22
+ 3. Find the SDK's warnings:
23
+ ```sh
24
+ grep -E 'unknown attribute|is not an attribute|is deprecated' sdk-warnings.log
25
+ ```
26
+ Each line starts with the `file:line` of your call. Every warning is
27
+ printed once per process per class and key (or per method), so fix what
28
+ you find and run again until the output is empty.
29
+ 4. Fix each hit as described below.
30
+ 5. Around code that resumes from a `session_store:` (`query`, `ask`,
31
+ `Client#connect`, `Client.open`), search for `rescue RuntimeError` and for
32
+ rescues of your adapter's own exception classes that inherit from
33
+ `RuntimeError` (for example `Net::ReadTimeout`, a `Timeout::Error`, which is
34
+ a `RuntimeError`). In 1.0 these arrive wrapped: rescue
35
+ `ClaudeAgentSDK::SessionStoreError` and inspect `#cause` for the original.
36
+ 6. Upgrade: `gem 'claude-agent-sdk', '~> 1.0'`.
37
+
38
+ ## Unknown keys raise `ArgumentError`
39
+
40
+ The value types you build and pass *in* now reject a key they do not define,
41
+ as `ClaudeAgentOptions` always has. Before 0.37 the key was silently dropped,
42
+ so a typo such as `matchr:` built a matcher that matched every tool:
43
+
44
+ ```ruby
45
+ ClaudeAgentSDK::HookMatcher.new(matchr: 'Bash', hooks: [check])
46
+ # 0.37: warning: ClaudeAgentSDK::HookMatcher: unknown attribute :matchr ignored; this will raise ArgumentError in 1.0 (known: hooks, matcher, timeout)
47
+ # 1.0: ArgumentError: ClaudeAgentSDK::HookMatcher: unknown attribute :matchr (known: hooks, matcher, timeout)
48
+ ```
49
+
50
+ This covers `.new` and `#[]=` on the option values (`AgentDefinition`,
51
+ `SandboxSettings` and its network and filesystem configs, the three thinking
52
+ configs, `TaskBudget`, the three system prompt types, `ToolsPreset`,
53
+ `SdkPluginConfig`, the four MCP server configs), `HookMatcher`, the hook
54
+ outputs (`SyncHookJSONOutput`, `AsyncHookJSONOutput`, every
55
+ `*HookSpecificOutput`), `PermissionResultAllow`, `PermissionResultDeny`,
56
+ `PermissionUpdate` and `PermissionRuleValue`. The full list is in
57
+ [docs/types.md](docs/types.md#unknown-keys).
58
+
59
+ **Fix:** correct the key, or remove it if the type never had it. Symbol and
60
+ String keys, snake_case and camelCase all still work, and so does a type's own
61
+ discriminator (`type`, `hook_event_name`, `behavior`), so on types that define
62
+ their own `#to_h` (the MCP server configs, `SandboxSettings`, the system prompt
63
+ types, the hook outputs, ...) `klass.new(value.to_h)` round-trips. For a Hash you did not write yourself (deserialized from the CLI,
64
+ a queue or a database), use `.from_hash` or `.wrap`: both stay lenient and
65
+ ignore unknown keys. Types the SDK parses from CLI output (messages, content
66
+ blocks, hook inputs) are not affected.
67
+
68
+ ## `#[]`, `#[]=` and camelCase reach attributes only
69
+
70
+ These accessors are public API for a type's attributes: the fields it declares,
71
+ predicates such as `options.forkSession?`, and any method your own code adds to
72
+ a subclass, a mixin or an instance. Through 0.37 they reached *any* public
73
+ method. In 1.0 a name that is not an attribute behaves like an undefined one:
74
+
75
+ | Call | 0.37 | 1.0 |
76
+ |------|------|-----|
77
+ | `msg[:to_h]`, `msg['freeze']` | calls the method, warns | `nil` (method not called) |
78
+ | `msg[:some_method] = x` | calls `some_method=`, warns | ignored; `ArgumentError` on the strict types above |
79
+ | `msg.toH` | calls `to_h`, warns | `NoMethodError`; `respond_to?(:toH)` is `false` |
80
+
81
+ **Fix:** call the method directly (`msg.to_h`). `UserMessage#text` and
82
+ `AssistantMessage#text` are convenience methods, not attributes, so
83
+ `msg[:text]` is `nil`; use `msg.text`.
84
+
85
+ ## `SessionStoreError` replaces `RuntimeError` on store-backed resume
86
+
87
+ When `query`, `ask` or `Client#connect` resume a session from
88
+ `session_store:` and a store call raises or exceeds `load_timeout_ms`, they now
89
+ raise `ClaudeAgentSDK::SessionStoreError < ClaudeSDKError`. In 0.37 this was a
90
+ bare `RuntimeError`, and a `RuntimeError` raised by your adapter escaped
91
+ unwrapped, so `rescue ClaudeAgentSDK::ClaudeSDKError` missed both. The message
92
+ names the store call and `#cause` holds your adapter's exception (or the
93
+ timeout):
94
+
95
+ ```ruby
96
+ begin
97
+ ClaudeAgentSDK.ask('Continue', options: options)
98
+ rescue ClaudeAgentSDK::SessionStoreError => e
99
+ e.message # "SessionStore#load for session 3f2c… failed during resume materialization: IOError: …"
100
+ e.cause # the IOError your adapter raised
101
+ end
102
+ ```
103
+
104
+ **Fix:** replace `rescue RuntimeError` on these paths with
105
+ `rescue ClaudeAgentSDK::SessionStoreError` (or `ClaudeSDKError`). Other store
106
+ paths are unchanged: the session functions called with `session_store:` behave
107
+ exactly as in 0.37, and mirroring failures still arrive as
108
+ `MirrorErrorMessage`, never as exceptions.
109
+
110
+ ## What SemVer covers from 1.0
111
+
112
+ No breaking changes within 1.x to the public API: everything documented in
113
+ `docs/` and the README, and every class, module, method and constant in the
114
+ YARD docs that is not tagged `@api private`. `@api private` objects (`Query`,
115
+ `MessageParser`, `FiberBoundary`, the `Sessions*` modules, ...) stay callable
116
+ but can change in any release. See
117
+ [CONTRIBUTING.md](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/CONTRIBUTING.md#what-is-public-api).
118
+
119
+ A removal is first deprecated in a minor release with a one-time warning that
120
+ names the replacement, and happens in the next major.
121
+
122
+ ## The Hash-key rule
123
+
124
+ Unchanged in 1.0, and now part of the SemVer contract: a plain Hash passed
125
+ through from the CLI's live stream (`usage`, `origin`, hook `tool_input`, the
126
+ `can_use_tool` input, SDK MCP tool `args`, `Client#mcp_status`) has **Symbol**
127
+ keys spelled as on the wire; a Hash read from a transcript or a `SessionStore`
128
+ (`SessionMessage#message`, `get_subagent_metadata`, store keys and entries) has
129
+ **String** keys. The wrong form reads `nil` rather than raising. See
130
+ [docs/types.md](docs/types.md#hash-keys).
131
+
132
+ ## Deprecated, kept through 1.x
133
+
134
+ The ten store-specific session functions still work in 1.x and still print a
135
+ one-time warning. They will be removed in 2.0. Each is the same call as its
136
+ replacement:
137
+
138
+ | Deprecated | Replacement |
139
+ |---|---|
140
+ | `list_sessions_from_store(session_store: s, ...)` | `list_sessions(session_store: s, ...)` |
141
+ | `get_session_info_from_store(session_store: s, ...)` | `get_session_info(session_store: s, ...)` |
142
+ | `get_session_messages_from_store(session_store: s, ...)` | `get_session_messages(session_store: s, ...)` |
143
+ | `list_subagents_from_store(session_store: s, ...)` | `list_subagents(session_store: s, ...)` |
144
+ | `get_subagent_metadata_from_store(session_store: s, ...)` | `get_subagent_metadata(session_store: s, ...)` |
145
+ | `get_subagent_messages_from_store(session_store: s, ...)` | `get_subagent_messages(session_store: s, ...)` |
146
+ | `rename_session_via_store(session_store: s, ...)` | `rename_session(session_store: s, ...)` |
147
+ | `tag_session_via_store(session_store: s, ...)` | `tag_session(session_store: s, ...)` |
148
+ | `delete_session_via_store(session_store: s, ...)` | `delete_session(session_store: s, ...)` |
149
+ | `fork_session_via_store(session_store: s, ...)` | `fork_session(session_store: s, ...)` |
150
+
151
+ `import_session_to_store` is not deprecated.
data/docs/client.md CHANGED
@@ -117,6 +117,17 @@ A transport must implement six methods:
117
117
  | `close` | Terminate and clean up |
118
118
  | `ready?` | Report whether the transport can accept I/O |
119
119
 
120
+ **Environment your transport should give the CLI.** `SubprocessCLITransport`
121
+ sets a few variables that a custom transport has to set itself. The one that
122
+ changes SDK behavior is `CLAUDE_CODE_SDK_READS_SESSION_STATE=1`: with it, the
123
+ CLI reports its session state, and a one-shot `query()` with hooks,
124
+ `can_use_tool` or SDK MCP servers keeps stdin open until the CLI reports
125
+ `idle`. That is what lets a follow-up turn woken by a background subagent get
126
+ its control requests answered. Without it, `query()` closes stdin at the first
127
+ result with no tracked task in flight, the pre-1.1 behavior. The state frames
128
+ arrive marked `sdk_host_only`, and the SDK drops them from your message
129
+ stream.
130
+
120
131
  Then plug it into `Client` via `transport_class:` / `transport_args:`. All connect orchestration (option transforms, MCP extraction, hook conversion, Query lifecycle) is handled for you.
121
132
 
122
133
  ```ruby
@@ -245,6 +245,48 @@ options = ClaudeAgentSDK::ClaudeAgentOptions.new(
245
245
 
246
246
  See [examples/bare_mode_example.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/bare_mode_example.rb).
247
247
 
248
+ ## Verbatim Prompts
249
+
250
+ Claude Code expands an `@/absolute/path` token anywhere in a user message into
251
+ that file's contents, and dispatches a leading `/name` as a slash command. The
252
+ expansion happens before the model runs and without a tool call, so `tools: []`,
253
+ `allowed_tools` and `disallowed_tools` do not stop it. If your prompt includes
254
+ text your end user did not type (earlier turns, tool output, third-party
255
+ content), set `verbatim_prompts` so that text cannot make Claude Code read a
256
+ local file:
257
+
258
+ ```ruby
259
+ options = ClaudeAgentSDK::ClaudeAgentOptions.new(verbatim_prompts: true)
260
+ ClaudeAgentSDK.query(prompt: text_that_may_contain_at_paths, options: options) { |message| ... }
261
+ ```
262
+
263
+ Every user message the SDK writes is then marked `client_composed`, and Claude
264
+ Code delivers it exactly as written. That covers String prompts and every
265
+ message of a streamed prompt, through `ClaudeAgentSDK.query`, `Client#connect`
266
+ and `Client#query`.
267
+
268
+ - **No per-message opt-out.** A `client_composed` key on a streamed message
269
+ Hash is overwritten. For per-turn control, leave the option off and set
270
+ `client_composed: true` on individual streamed messages.
271
+ - Your message Hashes are never mutated.
272
+ - A streamed JSONL String is parsed, marked and re-serialized. One that is not
273
+ a single JSON object raises `ArgumentError` instead of going out unmarked
274
+ (on the background streaming paths the stream stops with a warning, like any
275
+ stream error).
276
+ - `Client` reads the option once, at `connect`.
277
+ - **It skips more than `@path` expansion.** On current Claude Code versions a
278
+ turn delivered this way skips the whole turn-start attachment pass:
279
+ `@server:resource` MCP mentions are not expanded, and the prompt goes without
280
+ the context Claude Code normally attaches (nested `CLAUDE.md` and rules files,
281
+ skill and tool listings, other per-turn reminders). The pass between tool
282
+ calls still runs, so most of that context arrives after the turn's first tool
283
+ call.
284
+ - Requires Claude Code **2.1.248** or later. Older versions ignore the field
285
+ and still expand prompts; the SDK prints a warning when it connects to one
286
+ with the option on.
287
+
288
+ Matches the Python SDK's `verbatim_prompts`.
289
+
248
290
  ## Forwarding Subagent Text
249
291
 
250
292
  By default only `tool_use` / `tool_result` blocks from subagents (spawned via
data/docs/errors.md CHANGED
@@ -117,6 +117,11 @@ end
117
117
  # no usable home directory exists for the default ~/.claude
118
118
  class ConfigDirError < ClaudeSDKError; end
119
119
 
120
+ # Raised when resuming from a SessionStore fails: a store call raised or
121
+ # exceeded load_timeout_ms during resume materialization. #cause holds the
122
+ # adapter's exception (or the timeout)
123
+ class SessionStoreError < ClaudeSDKError; end
124
+
120
125
  # Raised when the Claude Code process fails
121
126
  class ProcessError < ClaudeSDKError
122
127
  attr_reader :exit_code, # Integer | nil
@@ -155,6 +160,7 @@ end
155
160
  | `ControlRequestTimeoutError` | Control protocol timeout (configurable via env var) |
156
161
  | `CLINotFoundError` | Claude Code not installed |
157
162
  | `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` |
163
+ | `SessionStoreError` | Resuming from `session_store:` failed: a store call (`#load`, `#list_sessions`, `#list_subkeys`) raised or exceeded `load_timeout_ms` while the SDK materialized the transcript, before the CLI started. The message names the call; `#cause` is the adapter's exception. Before 1.0 this was a bare `RuntimeError` — see [Sessions](sessions.md#mirroring-to-a-sessionstore) |
158
164
  | `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 |
159
165
  | `ResultError` | Run ended on a terminal error result (subclasses `ProcessError`; adds `subtype`, `errors`, `api_error_status`, `terminal_reason`, ...) — rescue it first |
160
166
  | `CLIJSONDecodeError` | JSON parsing issues — including stdout ending mid-frame (a truncated final message; `line` holds the partial frame) |
data/docs/sessions.md CHANGED
@@ -227,6 +227,25 @@ while an append is in flight are coalesced into the next append, so a slow
227
227
  store never accumulates one background task per frame), and `load_timeout_ms`
228
228
  (per store call during resume materialization, default `60_000`).
229
229
 
230
+ If a store call raises or exceeds `load_timeout_ms` during resume
231
+ materialization, `ClaudeAgentSDK.query`, `.ask` and `Client#connect` raise
232
+ `ClaudeAgentSDK::SessionStoreError` (a `ClaudeSDKError`) before the CLI
233
+ starts. The message names the call (`SessionStore#load for session <id> failed
234
+ during resume materialization: IOError: ...`) and `#cause` holds the adapter's
235
+ own exception, or the internal timeout:
236
+
237
+ ```ruby
238
+ begin
239
+ ClaudeAgentSDK.ask('Continue', options: options.dup_with(resume: session_id))
240
+ rescue ClaudeAgentSDK::SessionStoreError => e
241
+ logger.warn("resume from store failed: #{e.message} (#{e.cause&.class})")
242
+ raise
243
+ end
244
+ ```
245
+
246
+ Before 1.0 this surfaced as a bare `RuntimeError` (and a `RuntimeError` your
247
+ adapter raised escaped unwrapped), which `rescue ClaudeSDKError` missed.
248
+
230
249
  Resume materialization re-serializes each loaded entry to JSONL. An entry that
231
250
  cannot be serialized (NaN/Infinity, invalid UTF-8, circular nesting), an
232
251
  unserializable subagent metadata sidecar, or a subkey that is not a safe
@@ -478,7 +497,7 @@ skipped with a warning.
478
497
  > `get_subagent_messages_from_store`, `rename_session_via_store`,
479
498
  > `tag_session_via_store`, `delete_session_via_store`,
480
499
  > `fork_session_via_store`) still work unchanged but print a one-time
481
- > deprecation warning and will be removed in 1.0. Replace
500
+ > deprecation warning; they stay through 1.x and will be removed in 2.0. Replace
482
501
  > `ClaudeAgentSDK.x_from_store(session_store: store, ...)` or
483
502
  > `x_via_store(session_store: store, ...)` with
484
503
  > `ClaudeAgentSDK.x(..., session_store: store)`.
data/docs/types.md CHANGED
@@ -47,8 +47,8 @@ msg.sessionId # camelCase reader (also answers respond_to?)
47
47
 
48
48
  `#[]` returns `nil` for a name the type does not define, while a misspelled
49
49
  method call such as `msg.nope` raises `NoMethodError`. These accessors reach a type's
50
- **attributes** only; any other method reached this way warns in 0.37 and stops
51
- working in 1.0 — see [Attributes Only](#attributes-only).
50
+ **attributes** only; any other method (`to_h`, `freeze`, ...) counts as undefined —
51
+ see [Attributes Only](#attributes-only).
52
52
 
53
53
  `#[]=` assigns through the attribute's setter, with the same name
54
54
  normalization, and returns the assigned value:
@@ -63,8 +63,8 @@ msg[:result] = 'edited' # same as msg.result = 'edited'
63
63
  Copy first if you need the original.
64
64
  - A name the type does not define is ignored on the types the SDK parses from
65
65
  CLI output. `ClaudeAgentOptions` raises `ArgumentError` for an unknown key (as
66
- its constructor and `dup_with` do), and the value types you build and pass in
67
- warn once and will raise in 1.0 — see [Unknown Keys](#unknown-keys).
66
+ its constructor and `dup_with` do), and so do the value types you build and
67
+ pass in — see [Unknown Keys](#unknown-keys).
68
68
  - Discriminator fields (`type` on the MCP server and system-prompt configs,
69
69
  `behavior` on `PermissionResultAllow` / `PermissionResultDeny`,
70
70
  `hook_event_name` on hook inputs and outputs) are read-only, so
@@ -380,29 +380,32 @@ end
380
380
 
381
381
  ### Unknown Keys
382
382
 
383
- `ClaudeAgentOptions` raises `ArgumentError` on an unknown key. The value types you build and pass *in* used to drop a misspelled key silently; they now print a warning, once per class and key, pointing at your call:
383
+ `ClaudeAgentOptions` and the value types you build and pass *in* raise `ArgumentError` on a key they do not define, naming the class, the key and the keys it accepts:
384
384
 
385
- ```
386
- app/agents/reviewer.rb:12: warning: ClaudeAgentSDK::HookMatcher: unknown attribute :matchr ignored; this will raise ArgumentError in 1.0 (known: hooks, matcher, timeout)
385
+ ```ruby
386
+ ClaudeAgentSDK::HookMatcher.new(matchr: 'Bash', hooks: [check])
387
+ # ArgumentError: ClaudeAgentSDK::HookMatcher: unknown attribute :matchr (known: hooks, matcher, timeout)
387
388
  ```
388
389
 
389
- **In 1.0 the same call raises `ArgumentError`.** This covers `.new` and `#[]=` on:
390
+ This covers `.new` and `#[]=` on:
390
391
 
391
392
  - option values: `AgentDefinition`, `SandboxSettings`, `SandboxNetworkConfig`, `SandboxFilesystemConfig`, `ThinkingConfigAdaptive` / `Enabled` / `Disabled`, `TaskBudget`, `SystemPromptPreset` / `Custom` / `File`, `ToolsPreset`, `SdkPluginConfig`, `McpStdioServerConfig`, `McpSSEServerConfig`, `McpHttpServerConfig`, `McpSdkServerConfig`
392
393
  - `HookMatcher` and hook outputs: `SyncHookJSONOutput`, `AsyncHookJSONOutput`, every `*HookSpecificOutput`
393
394
  - `PermissionResultAllow`, `PermissionResultDeny`, `PermissionUpdate`, `PermissionRuleValue`
394
395
 
395
- Accepted without a warning: Symbol or String keys, snake_case or camelCase spellings, and the fixed discriminator a type sets itself (`type`, `hook_event_name`, `behavior`), so `klass.new(value.to_h)` round-trips. Types the SDK parses from CLI output (messages, content blocks, hook inputs, `ToolPermissionContext`, the MCP status types) stay lenient, so a field added by a newer CLI never warns, and so does every construction through `.from_hash` or `.wrap`. The warning goes through `Kernel#warn`, so `-W0` or `$VERBOSE = nil` silences it.
396
+ A nested value is checked as its own type: `PermissionUpdate.new(rules: [{ rule_contnt: 'x' }])` raises naming `PermissionRuleValue`.
397
+
398
+ Accepted: Symbol or String keys, snake_case or camelCase spellings, and the fixed discriminator a type sets itself (`type`, `hook_event_name`, `behavior`), so on a type that defines its own `#to_h` (the MCP server configs, `SandboxSettings`, the system prompt types, the hook outputs, ...) `klass.new(value.to_h)` round-trips. Types the SDK parses from CLI output (messages, content blocks, hook inputs, `ToolPermissionContext`, the MCP status types) stay lenient, so a field added by a newer CLI is ignored rather than raising, and so does every construction through `.from_hash` or `.wrap`. Use those two for data you did not write yourself, such as a Hash deserialized from the CLI or from storage.
399
+
400
+ (0.37 printed a one-time warning here and ignored the key; 1.0 raises. See [UPGRADING-1.0.md](../UPGRADING-1.0.md).)
396
401
 
397
402
  ### Attributes Only
398
403
 
399
- `#[]`, `#[]=` and the camelCase readers (`msg[:session_id]`, `msg['sessionId']`, `msg.sessionId`) are public API for a type's **attributes**: the fields it declares, plus predicates such as `options.forkSession?`. Methods your own code adds to a subclass (an `attr_accessor`, a hand-written reader or setter, a mixin's accessors, a singleton method) count as attributes too. Until now they reached any public method, so `msg[:to_h]` returned a Hash, `msg['freeze']` froze the message and `msg.toH` worked. Such a call still works in 0.37 but warns once per class and name:
404
+ `#[]`, `#[]=` and the camelCase readers (`msg[:session_id]`, `msg['sessionId']`, `msg.sessionId`) are public API for a type's **attributes**: the fields it declares, plus predicates such as `options.forkSession?`. Methods your own code adds to a subclass (an `attr_accessor`, a hand-written reader or setter, a mixin's accessors, a singleton method) count as attributes too.
400
405
 
401
- ```
402
- app/jobs/sync.rb:8: warning: ClaudeAgentSDK::ResultMessage#[]: :to_h is not an attribute; Type#[] will only read attributes in 1.0
403
- ```
406
+ A name that is not an attribute behaves like an undefined one: `#[]` returns `nil`, `#[]=` ignores it (on the strict types above it raises `ArgumentError`), a camelCase call raises `NoMethodError`, and `respond_to?` answers `false`. So `msg[:to_h]` is `nil`, `msg['freeze']` does not freeze the message, and `msg.toH` raises; call the method directly instead (`msg.to_h`). `UserMessage#text` and `AssistantMessage#text` are convenience methods, not attributes.
404
407
 
405
- **In 1.0 a name that is not an attribute behaves like an undefined one:** `#[]` returns `nil`, `#[]=` ignores it (on the strict types above it raises `ArgumentError`), and a camelCase call raises `NoMethodError`. Call the method directly instead (`msg.to_h`). Undefined names already behave that way today and do not warn. `UserMessage#text` and `AssistantMessage#text` are convenience methods, not attributes.
408
+ (Through 0.37 these accessors reached any public method, with a one-time warning in 0.37.)
406
409
 
407
410
  ## Constants
408
411
 
@@ -53,7 +53,7 @@ module ClaudeAgentSDK
53
53
  # Single source of truth: bumped here (and only here) by
54
54
  # .github/workflows/cli-pin-bump.yml or a Python-sync release, so a
55
55
  # Dependabot bump of the gem carries the CLI forward with it.
56
- PINNED_CLI_VERSION = '2.1.280'
56
+ PINNED_CLI_VERSION = '2.1.285'
57
57
  # @api private
58
58
  BINARY_NAME = 'claude'
59
59
  # @api private
@@ -35,56 +35,17 @@ module ClaudeAgentSDK
35
35
  return unless first
36
36
 
37
37
  begin
38
- warn("ClaudeAgentSDK.#{name} is deprecated and will be removed in 1.0; " \
38
+ warn("ClaudeAgentSDK.#{name} is deprecated and will be removed in 2.0; " \
39
39
  "use ClaudeAgentSDK.#{replacement}", uplevel: 2)
40
40
  rescue StandardError
41
41
  nil
42
42
  end
43
43
  end
44
44
 
45
- # Warn +message+ once per process per +key+, attributed to the first
46
- # caller frame outside the SDK's lib/ directory: the user's call site,
47
- # however deep inside the SDK the deprecated behaviour is detected
48
- # (e.g. an unknown attribute found by Type#assign_attribute during
49
- # HookMatcher.new). Best-effort like #warn_once.
50
- #
51
- # @param key [Object] once-guard key; any value usable in a Set
52
- # @param message [String]
53
- # @return [void]
54
- def warn_once_at_caller(key, message)
55
- first = @mutex.synchronize { @warned.add?(key) }
56
- return unless first
57
-
58
- begin
59
- locations = caller_locations(1)
60
- index = locations.index { |location| !sdk_frame?(location) }
61
- index ? warn(message, uplevel: index + 1) : warn(message)
62
- rescue StandardError
63
- nil
64
- end
65
- end
66
-
67
45
  # Test hook: forget which deprecations were already reported.
68
46
  def reset!
69
47
  @mutex.synchronize { @warned.clear }
70
48
  end
71
-
72
- private
73
-
74
- # A frame inside the gem's lib/ (both the loaded and the real path, in
75
- # case lib/ is reached through a symlink), or a Ruby-internal one
76
- # (<internal:...>, e.g. Array#each on 3.4). A C frame such as Class#new
77
- # reports its caller's path, so it counts as the caller's.
78
- def sdk_frame?(location)
79
- path = location.absolute_path || location.path
80
- return true if path.nil? || path.start_with?('<internal:')
81
-
82
- SDK_LIB_DIRS.any? { |dir| path.start_with?(dir) }
83
- end
84
49
  end
85
-
86
- SDK_LIB_DIRS = [File.expand_path('..', File.dirname(__FILE__)), File.expand_path('..', __dir__)]
87
- .uniq.map { |dir| "#{dir}/" }.freeze
88
- private_constant :SDK_LIB_DIRS
89
50
  end
90
51
  end
@@ -31,6 +31,14 @@ module ClaudeAgentSDK
31
31
  # relative HOME). Set CLAUDE_CONFIG_DIR to fix it.
32
32
  class ConfigDirError < ClaudeSDKError; end
33
33
 
34
+ # Raised when resuming a session from ClaudeAgentOptions#session_store
35
+ # fails: a store call (#load, #list_sessions, #list_subkeys, ...) raised or
36
+ # exceeded load_timeout_ms while the SDK was materializing the transcript
37
+ # for the CLI. Raised by Client#connect, ClaudeAgentSDK.query and .ask,
38
+ # before the CLI starts. The message names the store call; #cause holds the
39
+ # adapter's own exception (or the internal timeout).
40
+ class SessionStoreError < ClaudeSDKError; end
41
+
34
42
  # Raised when the CLI process fails
35
43
  class ProcessError < ClaudeSDKError
36
44
  attr_reader :exit_code, :stderr
@@ -112,6 +120,8 @@ module ClaudeAgentSDK
112
120
  # never disagree. Private to ResultError (Python keeps the equivalent
113
121
  # helpers module-private as _normalize_result_errors); callers outside
114
122
  # go through .error_text.
123
+ #
124
+ # @api private
115
125
  module Payload
116
126
  module_function
117
127