claude-agent-sdk 0.36.0 → 0.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +17 -0
  3. data/README.md +2 -2
  4. data/docs/client.md +26 -1
  5. data/docs/hooks-and-permissions.md +5 -3
  6. data/docs/mcp-servers.md +1 -2
  7. data/docs/sessions.md +81 -2
  8. data/docs/types.md +106 -4
  9. data/lib/claude_agent_sdk/cli_installer.rb +30 -3
  10. data/lib/claude_agent_sdk/command_builder.rb +109 -98
  11. data/lib/claude_agent_sdk/deprecation.rb +39 -0
  12. data/lib/claude_agent_sdk/fiber_boundary.rb +2 -0
  13. data/lib/claude_agent_sdk/instrumentation/otel.rb +15 -7
  14. data/lib/claude_agent_sdk/message_parser.rb +23 -9
  15. data/lib/claude_agent_sdk/observer.rb +2 -1
  16. data/lib/claude_agent_sdk/option_warnings.rb +2 -0
  17. data/lib/claude_agent_sdk/query.rb +50 -43
  18. data/lib/claude_agent_sdk/sdk_mcp_server.rb +22 -13
  19. data/lib/claude_agent_sdk/session_mutations.rb +20 -8
  20. data/lib/claude_agent_sdk/session_resume.rb +20 -11
  21. data/lib/claude_agent_sdk/session_store.rb +7 -3
  22. data/lib/claude_agent_sdk/session_summary.rb +4 -2
  23. data/lib/claude_agent_sdk/sessions.rb +8 -6
  24. data/lib/claude_agent_sdk/streaming.rb +1 -1
  25. data/lib/claude_agent_sdk/subprocess_cli_transport.rb +72 -40
  26. data/lib/claude_agent_sdk/testing/session_store_conformance.rb +14 -10
  27. data/lib/claude_agent_sdk/transcript_mirror_batcher.rb +2 -0
  28. data/lib/claude_agent_sdk/types/attributes.rb +271 -0
  29. data/lib/claude_agent_sdk/types/base.rb +320 -0
  30. data/lib/claude_agent_sdk/types/content_blocks.rb +57 -0
  31. data/lib/claude_agent_sdk/types/hooks.rb +640 -0
  32. data/lib/claude_agent_sdk/types/mcp.rb +232 -0
  33. data/lib/claude_agent_sdk/types/messages.rb +614 -0
  34. data/lib/claude_agent_sdk/types/option_values.rb +302 -0
  35. data/lib/claude_agent_sdk/types/options.rb +352 -0
  36. data/lib/claude_agent_sdk/types/permissions.rb +107 -0
  37. data/lib/claude_agent_sdk/types/sessions.rb +10 -0
  38. data/lib/claude_agent_sdk/types.rb +13 -2534
  39. data/lib/claude_agent_sdk/version.rb +1 -1
  40. data/lib/claude_agent_sdk.rb +51 -17
  41. metadata +11 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: aed11255947a3321978d4d9e65cf02ca0d88a6f8b2c9e61afede1de95c247b76
4
- data.tar.gz: e5d54c2745b1264198a25569599c4f34ac6817778914f450d7aaca8ddb7e99d5
3
+ metadata.gz: 7d238195bb7f1725358c2ca33a8396ff1f4ab4121c7cc905e1dc2e1957a4553e
4
+ data.tar.gz: 480e5d771b68e72b0646bfa102cd921b1797d7134a925c496c5575a5e795c88e
5
5
  SHA512:
6
- metadata.gz: b5c4ce0e5b2a8594eafd236fcdd4d8592cf0f900b8eede62e7e680b50d3560ea99d04180e3b42550e863a95225e9cac191fe50e8baa7c376413832b051e1900b
7
- data.tar.gz: b45fb3972949da75f83f845244e72b74fefcc88acf57c04ed89cbdde896a7e50eda69853a3430db88163259afe88862da0c598083745a3b9778f418ec001bf2d
6
+ metadata.gz: 91d8f9de96e91aa5297e2342f33a57de100807978ac5c6bb7739927f99e0c41707f335c4e41512c9d843bf19afdc3d30ab97b526319e3cc45d932e5bae2cc761
7
+ data.tar.gz: e7d2504b2dcda1c0ee5d1d4174d9ece35efc1a53b9832b00595829adacc6039f423d4db4999e1139d1fb6ad93c3b532dad119f9aceb51305328d3d25d847e7f6
data/CHANGELOG.md CHANGED
@@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.37.0] - 2026-09-23
11
+
12
+ The last 0.x release before 1.0 ([roadmap](https://github.com/ya-luotao/claude-agent-sdk-ruby/issues/126)). No runtime behaviour changes — only new warnings for things 1.0 will reject. **Run your suite on 0.37 with warnings visible before moving to 1.0:**
13
+ - A misspelled key on a value type you build (`HookMatcher`, `AgentDefinition`, `SandboxSettings`, MCP server configs, permission results, hook outputs, …) now warns once, pointing at your call; 1.0 raises `ArgumentError`.
14
+ - `msg[:name]` / camelCase readers reaching a non-attribute method (e.g. `msg[:to_h]`) warn once; in 1.0 they only reach attributes.
15
+ - The 1.0 SemVer surface is now visible: SDK internals are tagged `@api private` and hidden from the API docs, and the documented contracts (Hash-key rule, `mcp_status` / `context_usage` return Hashes, `Type#[]`) are written down.
16
+
17
+ ### Changed
18
+ - **Docs: pre-1.0 contracts written down** ([#126](https://github.com/ya-luotao/claude-agent-sdk-ruby/issues/126)). No code changes. `Client#mcp_status` / `#get_mcp_status` and `#context_usage` / `#get_context_usage` return the CLI's raw Hash with camelCase Symbol keys. `docs/types.md` had described `McpStatusResponse` as the response type; it is the optional typed view, via `McpStatusResponse.parse(client.mcp_status)`, and there is no typed context-usage class (`docs/client.md`). A new "Hash keys" section in `docs/types.md` states one rule: Hashes passed through from the live CLI stream (`origin`, `usage`, `model_usage`, hook `tool_input`, `can_use_tool` input, SDK MCP tool args, control responses) have Symbol keys spelled as on the wire, and Hashes read from transcripts or a `SessionStore` (`SessionMessage#message`, `get_subagent_metadata`, store keys and entries) have String keys. The docs, examples and bundled skill drop their `h[:key] || h['key']` fallbacks in favour of the one correct form. `Type#[]`, `#[]=` and the camelCase readers are documented as public API, including that `#[]=` changes the object you received. `docs/sessions.md` documents `ClaudeAgentSDK.project_key_for_directory` and `ClaudeAgentSDK.fold_session_summary` for adapter authors, and `import_session_to_store`'s batching (`batch_size:` defaults to 500 entries, and a batch also ends at about 1 MiB).
19
+ - **SDK internals are now tagged `@api private` and hidden from the YARD docs** ([#126](https://github.com/ya-luotao/claude-agent-sdk-ruby/issues/126)). The tagged namespaces are `Query`, `MessageParser`, `TranscriptMirrorBatcher`, `Sessions`, `SessionMutations`, `SessionResume`, `MaterializedResume`, `SessionSummary`, `SessionStores`, `OptionWarnings` and `FiberBoundary`, including everything nested in them. Some public classes also have internal members, and those are tagged individually: `SubprocessCLITransport`'s methods outside the `Transport` interface and its constants; `CommandBuilder`'s constants (`.new` and `#build` stay public); `CLIInstaller::Http`, `Platform`, `Release` and `Metadata` and the installer's constants other than `PINNED_CLI_VERSION`; `SdkMcpServer#handle_json`, `#handle_message` and `SdkMcpServer::ToolInputSchema`; `Client#query_handler`; and `ClaudeAgentSDK::OBSERVER_INTERFACE`. Their public faces are unchanged: `ClaudeAgentSDK.offload`, `fold_session_summary`, the session functions and `SDKSessionInfo` / `SessionMessage` remain public. Nothing changes at runtime. No method or constant was renamed, removed or made private, so every existing call keeps working. From 1.0, SemVer covers `docs/` and the YARD API without `@api private`; objects tagged `@api private` are not covered and may change in any release. `CONTRIBUTING.md` has a new "What is public API" section.
20
+ - `lib/claude_agent_sdk/types.rb` is split into one file per area under `lib/claude_agent_sdk/types/` (messages, hooks, options, ...). The code moved without edits and `types.rb` still loads every part, so `require 'claude_agent_sdk/types'` and every class and constant are unchanged; no API change ([#126](https://github.com/ya-luotao/claude-agent-sdk-ruby/issues/126)).
21
+ - `Type`'s internal machinery is tagged `@api private` and leaves the rendered docs: `Type#dup_for_options`, `Type.deep_dup_for_options`, `Type::OptionValue`, `Type.inspect_filtered`, `Type.inspect_filtered_attributes`, and the new `Type.strict_attributes` / `.strict_attributes?` / `.declare_attributes` / `.attribute?` / `.attribute_names`. They are not part of the SemVer surface. `Type#[]`, `#[]=`, the camelCase readers, `#to_h`, `.wrap` and `.from_hash` stay public ([#126](https://github.com/ya-luotao/claude-agent-sdk-ruby/issues/126)).
22
+
23
+ ### Deprecated
24
+ - **Unknown keys on user-constructed types.** The value types you build and pass in (option values such as `AgentDefinition`, `SandboxSettings`, the thinking configs and the MCP server configs; `HookMatcher`; hook outputs; `PermissionResultAllow` / `PermissionResultDeny`; `PermissionUpdate`; `PermissionRuleValue`) silently dropped a misspelled key, so `HookMatcher.new(matchr: 'Bash')` quietly built a matcher with no `matcher` set. `.new` and `#[]=` now warn once per class and key, at the caller's line: `ClaudeAgentSDK::HookMatcher: unknown attribute :matchr ignored; this will raise ArgumentError in 1.0 (known: hooks, matcher, timeout)`. **In 1.0 they raise `ArgumentError`**, as `ClaudeAgentOptions` already does. camelCase and String keys and a type's own discriminator (`type`, `hook_event_name`, `behavior`) are accepted, so `klass.new(value.to_h)` round-trips silently. Types parsed from CLI output (messages, content blocks, hook inputs) and every construction through `.from_hash` / `.wrap` stay lenient, so a newer CLI's extra fields never warn; permission suggestions from the CLI are now hydrated through `PermissionUpdate.wrap` for that reason. Full list in `docs/types.md` ([#126](https://github.com/ya-luotao/claude-agent-sdk-ruby/issues/126)).
25
+ - **`Type#[]`, `#[]=` and camelCase methods reaching something other than an attribute.** They are public API for a type's attributes (its `attr_*` fields, the backward-compatible `RateLimitEvent#data`, predicates such as `options.forkSession?`, and any method your own code adds to a subclass, mixin or instance), but they resolved any public method: `msg[:to_h]` returned a Hash, `msg['freeze']` froze the message, `msg.toH` called `#to_h`. Such a call still works but warns once per class and name, e.g. `ClaudeAgentSDK::ResultMessage#[]: :to_h is not an attribute; Type#[] will only read attributes in 1.0`. **In 1.0 a name that is not an attribute behaves like an undefined one:** `#[]` returns `nil`, `#[]=` ignores it (raises on the types above), a camelCase call raises `NoMethodError`, and `respond_to?` says `false`. Names that are not methods at all keep today's behaviour and do not warn. `UserMessage#text` / `AssistantMessage#text` are convenience methods, not attributes ([#126](https://github.com/ya-luotao/claude-agent-sdk-ruby/issues/126)).
26
+
10
27
  ## [0.36.0] - 2026-09-23
11
28
 
12
29
  The first step on the [road to 1.0](https://github.com/ya-luotao/claude-agent-sdk-ruby/issues/126): one `session_store:` argument for every session function (the store-specific twins are deprecated), `ClaudeAgentSDK.ask`, String tool results, and the last audit follow-ups (#119–#121). **Read before upgrading:**
data/README.md CHANGED
@@ -29,7 +29,7 @@ A Ruby SDK for the [Claude Code](https://docs.claude.com/en/docs/claude-code-ove
29
29
 
30
30
  ```ruby
31
31
  # Gemfile
32
- gem 'claude-agent-sdk', '~> 0.36.0'
32
+ gem 'claude-agent-sdk', '~> 0.37.0'
33
33
  ```
34
34
 
35
35
  Then `bundle install`, or install directly with `gem install claude-agent-sdk`. To track unreleased changes, point the Gemfile at GitHub: `gem 'claude-agent-sdk', github: 'ya-luotao/claude-agent-sdk-ruby'`.
@@ -193,7 +193,7 @@ See [docs/hooks-and-permissions.md](docs/hooks-and-permissions.md) for the full
193
193
  | OpenTelemetry tracing, Langfuse, custom observers | [docs/observability.md](docs/observability.md) |
194
194
  | Rails: generator, `install_cli` task, callback wrapper, fiber safety, solid_queue fiber workers, ActionCable, jobs | [docs/rails.md](docs/rails.md) |
195
195
  | Vendoring a pinned CLI binary and CLI discovery order | [docs/cli-installer.md](docs/cli-installer.md) |
196
- | Message, content block, and configuration type reference | [docs/types.md](docs/types.md) |
196
+ | Hash-key rule, attribute access, and the message, content block, and configuration type reference | [docs/types.md](docs/types.md) |
197
197
  | Error handling, exception hierarchy, timeouts | [docs/errors.md](docs/errors.md) |
198
198
 
199
199
  API reference: [rubydoc.info/gems/claude-agent-sdk](https://rubydoc.info/gems/claude-agent-sdk). Available built-in tools: [Claude Code documentation](https://docs.anthropic.com/en/docs/claude-code/settings#tools-available-to-claude).
data/docs/client.md CHANGED
@@ -77,6 +77,31 @@ The Ruby-style names above sit next to the Python SDK's spellings, and both work
77
77
 
78
78
  Each Ruby-style method calls its parity counterpart, so both send the same control request and raise `CLIConnectionError` when the client is not connected. The one exception is `server_info`, which reads the cached initialization result and returns `nil` instead of raising before `connect`. As with any Ruby setter, `client.model = 'haiku'` evaluates to `'haiku'`, not to the control response.
79
79
 
80
+ ### MCP status and context usage return Hashes
81
+
82
+ `mcp_status` / `get_mcp_status` and `context_usage` / `get_context_usage` return the CLI's control response payload as a plain Hash, unchanged: Symbol keys spelled as on the wire, which for these payloads is camelCase (`:mcpServers`, `:serverInfo`, `:totalTokens`), at every level of nesting (see [Hash keys](types.md#hash-keys)). The SDK does not model or filter the payload, so fields added by newer CLI versions come through. A response without a payload reads as `{}`.
83
+
84
+ ```ruby
85
+ status = client.mcp_status
86
+ status[:mcpServers].each { |s| puts "#{s[:name]}: #{s[:status]}" }
87
+ status.dig(:mcpServers, 0, :serverInfo, :version)
88
+
89
+ usage = client.context_usage
90
+ puts "#{usage[:totalTokens]} / #{usage[:maxTokens]} tokens"
91
+ ```
92
+
93
+ For a typed view of the MCP status, parse the Hash yourself:
94
+
95
+ ```ruby
96
+ typed = ClaudeAgentSDK::McpStatusResponse.parse(client.mcp_status)
97
+ typed.mcp_servers.each do |server| # McpServerStatus
98
+ puts "#{server.name} #{server.status} #{server.server_info&.version}"
99
+ server.tools&.each { |tool| puts " #{tool.name} read_only=#{tool.annotations&.read_only}" }
100
+ end
101
+ ```
102
+
103
+ `McpServerStatus#config` is an `McpSdkServerConfigStatus` or `McpClaudeAIProxyServerConfig` for `sdk` and `claudeai-proxy` servers, and the raw Hash for every other server type. There is no typed class for context usage; read the Hash.
104
+
80
105
  ## Custom Transport
81
106
 
82
107
  By default, `Client` uses `SubprocessCLITransport` to spawn the Claude Code CLI locally. You can provide a custom transport class to connect via other channels (e.g., remote SSH, WebSocket, or a sandbox VM).
@@ -87,7 +112,7 @@ A transport must implement six methods:
87
112
  |---|---|
88
113
  | `connect` | Establish the connection / spawn the remote CLI |
89
114
  | `write(data)` | Send raw JSON-line bytes to stdin |
90
- | `read_messages { \|hash\| ... }` | Yield parsed JSON messages from stdout; block until the stream closes |
115
+ | `read_messages { \|hash\| ... }` | Yield each stdout line as a Hash parsed with `JSON.parse(line, symbolize_names: true)` (the SDK reads Symbol keys; see [Hash keys](types.md#hash-keys)); block until the stream closes |
91
116
  | `end_input` | Signal EOF on stdin |
92
117
  | `close` | Terminate and clean up |
93
118
  | `ready?` | Report whether the transport can accept I/O |
@@ -19,7 +19,9 @@ All hook input objects include common fields like `session_id`, `transcript_path
19
19
  - `SubagentStart` → `SubagentStartHookInput` (`agent_id`, `agent_type`)
20
20
  - `PermissionRequest` → `PermissionRequestHookInput` (`tool_name`, `tool_input`, `permission_suggestions`)
21
21
 
22
- All 27 hook events have typed input classes. See [`ClaudeAgentSDK::HOOK_EVENTS`](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/lib/claude_agent_sdk/types.rb) and [examples/lifecycle_hooks_example.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/lifecycle_hooks_example.rb).
22
+ `tool_input` (and the `input` a [permission callback](#permission-callbacks) receives) is the CLI's Hash passed through unchanged, so its keys are Symbols spelled as on the wire: `tool_input[:command]`, `input[:file_path]`. See [Hash keys](types.md#hash-keys).
23
+
24
+ All 27 hook events have typed input classes. See [`ClaudeAgentSDK::HOOK_EVENTS`](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/lib/claude_agent_sdk/types/hooks.rb) and [examples/lifecycle_hooks_example.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/lifecycle_hooks_example.rb).
23
25
 
24
26
  `background_tasks` and `session_crons` are optional arrays of raw CLI hashes.
25
27
  `nil` means the CLI did not provide a snapshot; `[]` means it provided an empty
@@ -40,7 +42,7 @@ Async do
40
42
  return {} unless input.respond_to?(:tool_name) && input.tool_name == 'Bash'
41
43
 
42
44
  tool_input = input.tool_input || {}
43
- command = tool_input[:command] || tool_input['command'] || ''
45
+ command = tool_input[:command] || ''
44
46
  block_patterns = ['rm -rf', 'foo.sh']
45
47
 
46
48
  block_patterns.each do |pattern|
@@ -105,7 +107,7 @@ Async do
105
107
  return ClaudeAgentSDK::PermissionResultAllow.new if tool_name == 'Read'
106
108
 
107
109
  if tool_name == 'Write'
108
- file_path = input[:file_path] || input['file_path']
110
+ file_path = input[:file_path]
109
111
  if file_path && file_path.include?('/etc/')
110
112
  return ClaudeAgentSDK::PermissionResultDeny.new(
111
113
  message: 'Cannot write to sensitive system files',
data/docs/mcp-servers.md CHANGED
@@ -130,8 +130,7 @@ first gets an `isError` result naming the exception class
130
130
  (`"SystemExit: exit"`), so it is not left waiting on the tool call, and then
131
131
  the exception propagates as Ruby normally would (`exit` ends the process,
132
132
  Ctrl-C interrupts it). Called directly, without a session,
133
- `SdkMcpServer#call_tool` and `#handle_message` simply let such exceptions
134
- propagate. Cancellation of the tool call itself still propagates.
133
+ `SdkMcpServer#call_tool` simply lets such exceptions propagate. Cancellation of the tool call itself still propagates.
135
134
 
136
135
  ## Mixed Server Support
137
136
 
data/docs/sessions.md CHANGED
@@ -40,7 +40,7 @@ messages.each { |msg| puts "[#{msg.type}] #{msg.message}" }
40
40
  ClaudeAgentSDK.get_session_messages(session_id: 'abc-123-...', offset: 10, limit: 20)
41
41
  ```
42
42
 
43
- Each `SessionMessage` includes `type` (`"user"` or `"assistant"`), `uuid`, `session_id`, and `message` (raw API hash).
43
+ Each `SessionMessage` includes `type` (`"user"` or `"assistant"`), `uuid`, `session_id`, and `message` (the raw API message Hash, read from the transcript, so its keys are Strings: `msg.message['content']`; see [Hash keys](types.md#hash-keys)).
44
44
 
45
45
  ## Reading Subagent Transcripts
46
46
 
@@ -292,6 +292,65 @@ require 'claude_agent_sdk/testing/session_store_conformance'
292
292
  ClaudeAgentSDK::Testing.run_session_store_conformance(-> { MyStore.new(...) })
293
293
  ```
294
294
 
295
+ Keys and entries cross the adapter boundary with **String** keys (see
296
+ [Hash keys](types.md#hash-keys)): a key is `{ 'project_key' => ..., 'session_id' => ... }`,
297
+ plus `'subpath'` for a subagent transcript, and entries are the raw JSONL
298
+ objects. Persist entries verbatim and treat `entry['uuid']` as an idempotency
299
+ key, since a retried or re-imported batch can repeat earlier writes.
300
+
301
+ #### Helpers for adapter authors
302
+
303
+ `ClaudeAgentSDK.project_key_for_directory(directory = nil)` returns the
304
+ `project_key` the SDK uses for a directory (a String or Pathname; `nil` means
305
+ the current working directory). It applies the CLI's project-directory naming
306
+ (realpath, Unicode NFC, then the CLI's sanitization), so a key you build
307
+ matches the keys of live-mirrored transcripts:
308
+
309
+ ```ruby
310
+ key = { 'project_key' => ClaudeAgentSDK.project_key_for_directory('/path/to/project'),
311
+ 'session_id' => '550e8400-e29b-41d4-a716-446655440000' }
312
+ store.load(key)
313
+ ```
314
+
315
+ `ClaudeAgentSDK.fold_session_summary(prev, key, entries)` maintains a
316
+ per-session summary incrementally, so an adapter can implement
317
+ `#list_session_summaries` and `list_sessions(session_store:)` can read every
318
+ session's metadata in one call instead of one `#load` per session. Call it
319
+ from `#append`:
320
+
321
+ ```ruby
322
+ def append(key, entries)
323
+ return if entries.nil? || entries.empty?
324
+
325
+ write_entries(key, entries)
326
+ return unless key['subpath'].nil? # subagent transcripts never feed the summary
327
+
328
+ summary = ClaudeAgentSDK.fold_session_summary(read_summary(key), key, entries)
329
+ summary['mtime'] = write_time_ms # the clock #list_sessions reports
330
+ write_summary(key, summary)
331
+ end
332
+
333
+ def list_session_summaries(project_key)
334
+ read_summaries(project_key) # => [{ 'session_id' => ..., 'mtime' => ..., 'data' => {...} }, ...]
335
+ end
336
+ ```
337
+
338
+ - `prev` is the summary you stored for the same key on the previous append,
339
+ or `nil` on the first one. `entries` are the entries being appended.
340
+ - It returns a new `{ 'session_id', 'mtime', 'data' }` Hash and leaves `prev`
341
+ unchanged. Every derived field is set-once or last-wins, so the fold never
342
+ needs earlier entries again.
343
+ - Only call it for main-transcript keys (no `'subpath'`).
344
+ - It does not set `mtime`: it carries `prev`'s value, or `0` for a new
345
+ session. Stamp it after persisting, from the same clock as the `mtime`
346
+ your `#list_sessions` returns. When the store also implements
347
+ `#list_sessions`, a summary whose `mtime` is older than the listed one is
348
+ treated as stale and the SDK re-derives it from the transcript.
349
+ - `data` is opaque. Store it as returned; its String keys survive a JSON
350
+ round-trip (JSONB, Redis).
351
+
352
+ `InMemorySessionStore#append` is a working reference.
353
+
295
354
  Copy-in reference adapters for **S3, Redis, and Postgres** live in
296
355
  [`examples/session_stores/`](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/session_stores/README.md), each with a
297
356
  production checklist.
@@ -391,7 +450,27 @@ Where the store path differs from the disk path:
391
450
  entries are removed too depends on the store's delete cascade.
392
451
 
393
452
  To migrate, `import_session_to_store` replays a local on-disk session (and its
394
- subagents) into a store.
453
+ subagents) into a store:
454
+
455
+ ```ruby
456
+ ClaudeAgentSDK.import_session_to_store(
457
+ session_id: '550e8400-...',
458
+ session_store: store,
459
+ directory: '/path/to/project', # optional; nil searches every project
460
+ include_subagents: true, # default
461
+ batch_size: 500 # default
462
+ )
463
+ ```
464
+
465
+ It streams the transcript and calls `store.append` once per batch. A batch ends
466
+ at `batch_size` entries (default **500**; `nil` or a non-positive value also
467
+ means 500) or at about 1 MiB of JSONL, whichever comes first. Entries are
468
+ keyed under the on-disk project directory name, so the imported session can
469
+ be resumed with `session_store:` + `resume:` from the original directory.
470
+ Re-importing appends the entries again, so adapters should dedupe by
471
+ `entry['uuid']`. It raises `ArgumentError` for an invalid `session_id` and
472
+ `Errno::ENOENT` when the transcript cannot be found; an unparseable line is
473
+ skipped with a warning.
395
474
 
396
475
  > **Deprecated:** the separate store functions (`list_sessions_from_store`,
397
476
  > `get_session_info_from_store`, `get_session_messages_from_store`,
data/docs/types.md CHANGED
@@ -1,6 +1,81 @@
1
1
  # Types Reference
2
2
 
3
- See [lib/claude_agent_sdk/types.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/lib/claude_agent_sdk/types.rb) for complete type definitions.
3
+ See [lib/claude_agent_sdk/types/](https://github.com/ya-luotao/claude-agent-sdk-ruby/tree/main/lib/claude_agent_sdk/types) for complete type definitions (one file per area; `types.rb` loads them all).
4
+
5
+ ## Hash keys
6
+
7
+ Where the SDK hands you a plain Hash rather than a typed object, its key form
8
+ depends on where the data came from. One rule covers every case:
9
+
10
+ | Source | Key form | Examples |
11
+ |--------|----------|----------|
12
+ | The CLI's live stream-JSON, passed through as-is | **Symbols**, spelled exactly as on the wire | `UserMessage#origin`, `ResultMessage#origin`, `AssistantMessage#usage`, `ResultMessage#usage`, `ResultMessage#model_usage`, `ResultMessage#structured_output`, `UserMessage#tool_use_result`, `SystemMessage#data`, hook `tool_input`, the `can_use_tool` `input`, SDK MCP tool and prompt `args`, `Client#mcp_status`, `Client#context_usage` |
13
+ | Transcripts read from disk or a `SessionStore` | **Strings**, spelled as in the JSONL | `SessionMessage#message`, `get_subagent_metadata`, `SessionStore` keys and entries, `fold_session_summary` input and output |
14
+
15
+ "As on the wire" means the SDK does not rewrite key names. Structures the CLI
16
+ generates use camelCase (`origin[:fromSession]`, `model_usage` values'
17
+ `:inputTokens` / `:costUSD`, `client.mcp_status[:mcpServers]`), while objects
18
+ the CLI relays from the API keep their snake_case (`usage[:input_tokens]`).
19
+ Nesting follows the same rule all the way down, including keys that are data
20
+ rather than field names: `model_usage` is keyed by model-name Symbols
21
+ (`result.model_usage.each { |model, u| puts "#{model}: $#{u[:costUSD]}" }`).
22
+
23
+ The wrong key form reads as `nil` rather than raising, so look up the source
24
+ before indexing: `tool_input['command']` on a hook input, or
25
+ `meta[:toolUseId]` on subagent metadata, silently returns `nil`. The Python
26
+ SDK uses String keys everywhere; do not port its lookups literally.
27
+
28
+ The Symbol side of the rule relies on the transport parsing each line with
29
+ `JSON.parse(line, symbolize_names: true)`. The built-in
30
+ `SubprocessCLITransport` does; a [custom transport](client.md#custom-transport)
31
+ must too.
32
+
33
+ ## Reading and writing attributes
34
+
35
+ The SDK's typed objects (messages, content blocks, hook inputs and outputs,
36
+ option objects: everything built on `ClaudeAgentSDK::Type`) accept the same
37
+ attribute name in several spellings. These accessors are public API:
38
+
39
+ ```ruby
40
+ msg.session_id # the attr_accessor
41
+ msg[:session_id] # Symbol or String, snake_case or camelCase:
42
+ msg['session_id'] # all four read the same attribute
43
+ msg[:sessionId]
44
+ msg['sessionId']
45
+ msg.sessionId # camelCase reader (also answers respond_to?)
46
+ ```
47
+
48
+ `#[]` returns `nil` for a name the type does not define, while a misspelled
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).
52
+
53
+ `#[]=` assigns through the attribute's setter, with the same name
54
+ normalization, and returns the assigned value:
55
+
56
+ ```ruby
57
+ msg[:result] = 'edited' # same as msg.result = 'edited'
58
+ ```
59
+
60
+ - It **changes the object you received**. Messages are not frozen or copied
61
+ on delivery, so a change is visible to anything else holding the same
62
+ object (for example an observer that received it before your block did).
63
+ Copy first if you need the original.
64
+ - A name the type does not define is ignored on the types the SDK parses from
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).
68
+ - Discriminator fields (`type` on the MCP server and system-prompt configs,
69
+ `behavior` on `PermissionResultAllow` / `PermissionResultDeny`,
70
+ `hook_event_name` on hook inputs and outputs) are read-only, so
71
+ assigning them has no effect.
72
+
73
+ Constructors accept the same spellings: `ResultMessage.new('sessionId' => 'abc')`
74
+ is equivalent to `ResultMessage.new(session_id: 'abc')`.
75
+
76
+ `SDKSessionInfo` and `SessionMessage` (returned by the session functions) are
77
+ currently plain classes, not `Type`s: use their snake_case accessors
78
+ (`info.session_id`); they have no `#[]`, `#[]=` or camelCase readers.
4
79
 
5
80
  ## Message Types
6
81
 
@@ -134,8 +209,9 @@ turn was cancelled via `Client#interrupt`. `nil` when the CLI did not report
134
209
  one (older CLIs, or a result that bypassed the query loop such as a local
135
210
  slash command).
136
211
 
137
- `model_usage` values are passed through verbatim from the CLI, so their keys
138
- are camelCase (the TypeScript/Python SDKs' `ModelUsage` shape): `inputTokens`,
212
+ `model_usage` is passed through verbatim from the CLI (see [Hash keys](#hash-keys)):
213
+ it is keyed by model-name Symbols (`:"claude-sonnet-4-5"`), and each value's
214
+ keys are camelCase Symbols (the TypeScript/Python SDKs' `ModelUsage` shape): `inputTokens`,
139
215
  `outputTokens`, `cacheReadInputTokens`, `cacheCreationInputTokens`,
140
216
  `webSearchRequests`, `costUSD`, `contextWindow`, `maxOutputTokens`, plus
141
217
  optional `canonicalModel` (canonical id used for the pricing lookup, which can
@@ -287,7 +363,7 @@ end
287
363
  | `McpHttpServerConfig` | MCP server config for HTTP transport |
288
364
  | `SdkPluginConfig` | SDK plugin configuration |
289
365
  | `McpServerStatus` | Status of a single MCP server connection (with `.parse`) |
290
- | `McpStatusResponse` | Response from `get_mcp_status` containing all server statuses (with `.parse`) |
366
+ | `McpStatusResponse` | Typed view of the `Client#mcp_status` / `#get_mcp_status` Hash: `McpStatusResponse.parse(client.mcp_status).mcp_servers` is an Array of `McpServerStatus`. The client itself returns the raw Hash (see [client.md](client.md#mcp-status-and-context-usage-return-hashes)) |
291
367
  | `McpServerInfo` | MCP server name and version |
292
368
  | `McpToolInfo` | MCP tool name, description, and annotations |
293
369
  | `McpToolAnnotations` | MCP tool annotation hints (`read_only`, `destructive`, `open_world`) |
@@ -302,6 +378,32 @@ end
302
378
  | `SystemPromptFile` | System prompt loaded from a file path |
303
379
  | `ToolsPreset` | Tools preset configuration for base tools selection |
304
380
 
381
+ ### Unknown Keys
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:
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)
387
+ ```
388
+
389
+ **In 1.0 the same call raises `ArgumentError`.** This covers `.new` and `#[]=` on:
390
+
391
+ - option values: `AgentDefinition`, `SandboxSettings`, `SandboxNetworkConfig`, `SandboxFilesystemConfig`, `ThinkingConfigAdaptive` / `Enabled` / `Disabled`, `TaskBudget`, `SystemPromptPreset` / `Custom` / `File`, `ToolsPreset`, `SdkPluginConfig`, `McpStdioServerConfig`, `McpSSEServerConfig`, `McpHttpServerConfig`, `McpSdkServerConfig`
392
+ - `HookMatcher` and hook outputs: `SyncHookJSONOutput`, `AsyncHookJSONOutput`, every `*HookSpecificOutput`
393
+ - `PermissionResultAllow`, `PermissionResultDeny`, `PermissionUpdate`, `PermissionRuleValue`
394
+
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
+
397
+ ### Attributes Only
398
+
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:
400
+
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
+ ```
404
+
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.
406
+
305
407
  ## Constants
306
408
 
307
409
  | Constant | Description |
@@ -30,16 +30,22 @@ module ClaudeAgentSDK
30
30
  # ClaudeAgentSDK::CLIInstaller.install_pinned
31
31
  # @example Pin a version of your own
32
32
  # ClaudeAgentSDK::CLIInstaller.install(version: '2.1.220')
33
- module CLIInstaller
33
+ module CLIInstaller # rubocop:disable Metrics/ModuleLength -- Http/Platform/Release/Metadata submodules in one file
34
+ # @api private
34
35
  BASE_URL = 'https://downloads.claude.ai/claude-code-releases'
35
36
  # Dist-tags resolved through a GET to BASE_URL/<tag>.
37
+ #
38
+ # @api private
36
39
  DIST_TAGS = %w[stable latest].freeze
37
40
  # Concrete version, optionally with a pre-release suffix (e.g. 2.1.220-rc1).
38
41
  # The suffix is restricted to the semver pre-release character set: every
39
42
  # accepted version is interpolated straight into a download URL, and a
40
43
  # laxer `\S+` would let "2.1.220-x/../2.1.221" traverse out of the release
41
44
  # path — silently installing something other than the pinned version.
45
+ #
46
+ # @api private
42
47
  VERSION_PATTERN = /\A\d+\.\d+\.\d+(-[A-Za-z0-9.-]+)?\z/
48
+ # @api private
43
49
  CHECKSUM_PATTERN = /\A[0-9a-f]{64}\z/
44
50
  # The CLI version this gem release is developed and tested against — the
45
51
  # Ruby equivalent of the Python SDK's bundled-CLI pin (_cli_version.py),
@@ -48,24 +54,35 @@ module ClaudeAgentSDK
48
54
  # .github/workflows/cli-pin-bump.yml or a Python-sync release, so a
49
55
  # Dependabot bump of the gem carries the CLI forward with it.
50
56
  PINNED_CLI_VERSION = '2.1.280'
57
+ # @api private
51
58
  BINARY_NAME = 'claude'
59
+ # @api private
52
60
  VERSION_FILE = 'VERSION'
61
+ # @api private
53
62
  LOCK_FILE = '.install.lock'
54
63
  # Relative to .root (Dir.pwd when unset), resolved at CALL time by
55
64
  # .default_dir — an absolute constant would freeze the working directory
56
65
  # as of require time, which is wrong for anything that chdirs (Rake
57
66
  # tasks, bin/setup, test suites).
67
+ #
68
+ # @api private
58
69
  DEFAULT_DIR = File.join('vendor', 'claude')
59
70
  # Response caps. The dist-tag endpoints return a bare version string and
60
71
  # manifests are a few KB; anything larger is a misrouted response, not
61
72
  # something to buffer in memory. (The binary itself streams to disk.)
73
+ #
74
+ # @api private
62
75
  VERSION_RESPONSE_LIMIT = 1024
76
+ # @api private
63
77
  MANIFEST_RESPONSE_LIMIT = 5 * 1024 * 1024
78
+ # @api private
64
79
  METADATA_READ_LIMIT = 4096
65
80
 
66
81
  # Maps the running Ruby to a release-manifest platform key
67
82
  # (darwin-arm64, darwin-x64, linux-x64, linux-arm64, and the -musl
68
83
  # variants). Windows is not supported by this gem.
84
+ #
85
+ # @api private
69
86
  module Platform
70
87
  class << self
71
88
  def detect
@@ -123,6 +140,8 @@ module ClaudeAgentSDK
123
140
  # and chunked streaming for the binary. Knows nothing about releases; the
124
141
  # specs stub .fetch_text / .download_to wholesale so no HTTP stubbing
125
142
  # library is needed.
143
+ #
144
+ # @api private
126
145
  module Http
127
146
  MAX_REDIRECTS = 5
128
147
  OPEN_TIMEOUT_SECONDS = 10
@@ -155,7 +174,9 @@ module ClaudeAgentSDK
155
174
  File.open(path, File::WRONLY | File::CREAT | File::EXCL | File::BINARY, 0o600) do |file|
156
175
  response.read_body do |chunk|
157
176
  written += chunk.bytesize
158
- raise CLIInstallError, "Download from #{url} exceeds the expected #{max_bytes} bytes" if over?(written, max_bytes)
177
+ if over?(written, max_bytes)
178
+ raise CLIInstallError, "Download from #{url} exceeds the expected #{max_bytes} bytes"
179
+ end
159
180
 
160
181
  file.write(chunk)
161
182
  end
@@ -205,6 +226,8 @@ module ClaudeAgentSDK
205
226
 
206
227
  # Talks to the release service: dist-tag resolution, manifest lookup and
207
228
  # URL construction. Pure remote reads — no filesystem, no state.
229
+ #
230
+ # @api private
208
231
  module Release
209
232
  class << self
210
233
  # Local, network-free check of what the caller asked for. Returns the
@@ -239,7 +262,9 @@ module ClaudeAgentSDK
239
262
  end
240
263
 
241
264
  checksum = entry['checksum'].to_s.downcase
242
- raise CLIInstallError, "#{url} has no valid sha256 checksum for #{platform}" unless checksum.match?(CHECKSUM_PATTERN)
265
+ unless checksum.match?(CHECKSUM_PATTERN)
266
+ raise CLIInstallError, "#{url} has no valid sha256 checksum for #{platform}"
267
+ end
243
268
 
244
269
  size = entry['size']
245
270
  { checksum: checksum, size: size.is_a?(Integer) && size.positive? ? size : nil }
@@ -277,6 +302,8 @@ module ClaudeAgentSDK
277
302
  # per line. Both platform and checksum must match before trusting a cached
278
303
  # binary offline: a cache copied between OS/CPU/libc targets is not usable.
279
304
  # Older one- or two-line files lack that proof and trigger a clean reinstall.
305
+ #
306
+ # @api private
280
307
  module Metadata
281
308
  class << self
282
309
  def read(dir)