claude-agent-sdk 0.36.0 → 1.0.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 (64) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +34 -0
  3. data/README.md +7 -3
  4. data/UPGRADING-1.0.md +151 -0
  5. data/docs/client.md +26 -1
  6. data/docs/errors.md +6 -0
  7. data/docs/hooks-and-permissions.md +5 -3
  8. data/docs/mcp-servers.md +1 -2
  9. data/docs/sessions.md +101 -3
  10. data/docs/types.md +109 -4
  11. data/lib/claude_agent_sdk/cli_installer.rb +30 -3
  12. data/lib/claude_agent_sdk/command_builder.rb +109 -98
  13. data/lib/claude_agent_sdk/deprecation.rb +1 -1
  14. data/lib/claude_agent_sdk/errors.rb +10 -0
  15. data/lib/claude_agent_sdk/fiber_boundary.rb +2 -0
  16. data/lib/claude_agent_sdk/instrumentation/otel.rb +15 -7
  17. data/lib/claude_agent_sdk/message_parser.rb +23 -9
  18. data/lib/claude_agent_sdk/observer.rb +2 -1
  19. data/lib/claude_agent_sdk/option_warnings.rb +2 -0
  20. data/lib/claude_agent_sdk/query.rb +50 -43
  21. data/lib/claude_agent_sdk/sdk_mcp_server.rb +22 -13
  22. data/lib/claude_agent_sdk/session_mutations.rb +20 -8
  23. data/lib/claude_agent_sdk/session_resume.rb +31 -16
  24. data/lib/claude_agent_sdk/session_store.rb +7 -3
  25. data/lib/claude_agent_sdk/session_summary.rb +4 -2
  26. data/lib/claude_agent_sdk/sessions.rb +8 -6
  27. data/lib/claude_agent_sdk/streaming.rb +1 -1
  28. data/lib/claude_agent_sdk/subprocess_cli_transport.rb +72 -40
  29. data/lib/claude_agent_sdk/testing/session_store_conformance.rb +14 -10
  30. data/lib/claude_agent_sdk/transcript_mirror_batcher.rb +2 -0
  31. data/lib/claude_agent_sdk/types/attributes.rb +236 -0
  32. data/lib/claude_agent_sdk/types/base.rb +322 -0
  33. data/lib/claude_agent_sdk/types/content_blocks.rb +57 -0
  34. data/lib/claude_agent_sdk/types/hooks.rb +640 -0
  35. data/lib/claude_agent_sdk/types/mcp.rb +232 -0
  36. data/lib/claude_agent_sdk/types/messages.rb +614 -0
  37. data/lib/claude_agent_sdk/types/option_values.rb +302 -0
  38. data/lib/claude_agent_sdk/types/options.rb +352 -0
  39. data/lib/claude_agent_sdk/types/permissions.rb +107 -0
  40. data/lib/claude_agent_sdk/types/sessions.rb +10 -0
  41. data/lib/claude_agent_sdk/types.rb +13 -2534
  42. data/lib/claude_agent_sdk/version.rb +1 -1
  43. data/lib/claude_agent_sdk.rb +62 -28
  44. data/sig/claude_agent_sdk/cancellation_signal.rbs +14 -0
  45. data/sig/claude_agent_sdk/configuration.rbs +14 -0
  46. data/sig/claude_agent_sdk/errors.rbs +86 -0
  47. data/sig/claude_agent_sdk/observer.rbs +42 -0
  48. data/sig/claude_agent_sdk/railtie.rbs +10 -0
  49. data/sig/claude_agent_sdk/sdk_mcp_server.rbs +76 -0
  50. data/sig/claude_agent_sdk/session_store.rbs +105 -0
  51. data/sig/claude_agent_sdk/streaming.rbs +15 -0
  52. data/sig/claude_agent_sdk/transport.rbs +98 -0
  53. data/sig/claude_agent_sdk/types/base.rbs +39 -0
  54. data/sig/claude_agent_sdk/types/content_blocks.rbs +79 -0
  55. data/sig/claude_agent_sdk/types/hooks.rbs +528 -0
  56. data/sig/claude_agent_sdk/types/mcp.rbs +216 -0
  57. data/sig/claude_agent_sdk/types/messages.rbs +586 -0
  58. data/sig/claude_agent_sdk/types/option_values.rbs +245 -0
  59. data/sig/claude_agent_sdk/types/options.rbs +288 -0
  60. data/sig/claude_agent_sdk/types/permissions.rbs +108 -0
  61. data/sig/claude_agent_sdk/types/sessions.rbs +66 -0
  62. data/sig/claude_agent_sdk.rbs +231 -0
  63. data/sig/manifest.yaml +5 -0
  64. metadata +32 -1
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 (`to_h`, `freeze`, ...) counts as undefined —
51
+ 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 so do the value types you build and
67
+ pass in — 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,35 @@ 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` 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
+
385
+ ```ruby
386
+ ClaudeAgentSDK::HookMatcher.new(matchr: 'Bash', hooks: [check])
387
+ # ArgumentError: ClaudeAgentSDK::HookMatcher: unknown attribute :matchr (known: hooks, matcher, timeout)
388
+ ```
389
+
390
+ This covers `.new` and `#[]=` on:
391
+
392
+ - option values: `AgentDefinition`, `SandboxSettings`, `SandboxNetworkConfig`, `SandboxFilesystemConfig`, `ThinkingConfigAdaptive` / `Enabled` / `Disabled`, `TaskBudget`, `SystemPromptPreset` / `Custom` / `File`, `ToolsPreset`, `SdkPluginConfig`, `McpStdioServerConfig`, `McpSSEServerConfig`, `McpHttpServerConfig`, `McpSdkServerConfig`
393
+ - `HookMatcher` and hook outputs: `SyncHookJSONOutput`, `AsyncHookJSONOutput`, every `*HookSpecificOutput`
394
+ - `PermissionResultAllow`, `PermissionResultDeny`, `PermissionUpdate`, `PermissionRuleValue`
395
+
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).)
401
+
402
+ ### Attributes Only
403
+
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.
405
+
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.
407
+
408
+ (Through 0.37 these accessors reached any public method, with a one-time warning in 0.37.)
409
+
305
410
  ## Constants
306
411
 
307
412
  | 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)