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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +17 -0
- data/README.md +2 -2
- data/docs/client.md +26 -1
- data/docs/hooks-and-permissions.md +5 -3
- data/docs/mcp-servers.md +1 -2
- data/docs/sessions.md +81 -2
- data/docs/types.md +106 -4
- data/lib/claude_agent_sdk/cli_installer.rb +30 -3
- data/lib/claude_agent_sdk/command_builder.rb +109 -98
- data/lib/claude_agent_sdk/deprecation.rb +39 -0
- data/lib/claude_agent_sdk/fiber_boundary.rb +2 -0
- data/lib/claude_agent_sdk/instrumentation/otel.rb +15 -7
- data/lib/claude_agent_sdk/message_parser.rb +23 -9
- data/lib/claude_agent_sdk/observer.rb +2 -1
- data/lib/claude_agent_sdk/option_warnings.rb +2 -0
- data/lib/claude_agent_sdk/query.rb +50 -43
- data/lib/claude_agent_sdk/sdk_mcp_server.rb +22 -13
- data/lib/claude_agent_sdk/session_mutations.rb +20 -8
- data/lib/claude_agent_sdk/session_resume.rb +20 -11
- data/lib/claude_agent_sdk/session_store.rb +7 -3
- data/lib/claude_agent_sdk/session_summary.rb +4 -2
- data/lib/claude_agent_sdk/sessions.rb +8 -6
- data/lib/claude_agent_sdk/streaming.rb +1 -1
- data/lib/claude_agent_sdk/subprocess_cli_transport.rb +72 -40
- data/lib/claude_agent_sdk/testing/session_store_conformance.rb +14 -10
- data/lib/claude_agent_sdk/transcript_mirror_batcher.rb +2 -0
- data/lib/claude_agent_sdk/types/attributes.rb +271 -0
- data/lib/claude_agent_sdk/types/base.rb +320 -0
- data/lib/claude_agent_sdk/types/content_blocks.rb +57 -0
- data/lib/claude_agent_sdk/types/hooks.rb +640 -0
- data/lib/claude_agent_sdk/types/mcp.rb +232 -0
- data/lib/claude_agent_sdk/types/messages.rb +614 -0
- data/lib/claude_agent_sdk/types/option_values.rb +302 -0
- data/lib/claude_agent_sdk/types/options.rb +352 -0
- data/lib/claude_agent_sdk/types/permissions.rb +107 -0
- data/lib/claude_agent_sdk/types/sessions.rb +10 -0
- data/lib/claude_agent_sdk/types.rb +13 -2534
- data/lib/claude_agent_sdk/version.rb +1 -1
- data/lib/claude_agent_sdk.rb +51 -17
- metadata +11 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7d238195bb7f1725358c2ca33a8396ff1f4ab4121c7cc905e1dc2e1957a4553e
|
|
4
|
+
data.tar.gz: 480e5d771b68e72b0646bfa102cd921b1797d7134a925c496c5575a5e795c88e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 91d8f9de96e91aa5297e2342f33a57de100807978ac5c6bb7739927f99e0c41707f335c4e41512c9d843bf19afdc3d30ab97b526319e3cc45d932e5bae2cc761
|
|
7
|
+
data.tar.gz: e7d2504b2dcda1c0ee5d1d4174d9ece35efc1a53b9832b00595829adacc6039f423d4db4999e1139d1fb6ad93c3b532dad119f9aceb51305328d3d25d847e7f6
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,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.
|
|
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
|
-
|
|
|
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
|
|
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
|
-
|
|
22
|
+
`tool_input` (and the `input` a [permission callback](#permission-callbacks) receives) is the CLI's Hash passed through unchanged, so its keys are Symbols spelled as on the wire: `tool_input[:command]`, `input[:file_path]`. See [Hash keys](types.md#hash-keys).
|
|
23
|
+
|
|
24
|
+
All 27 hook events have typed input classes. See [`ClaudeAgentSDK::HOOK_EVENTS`](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/lib/claude_agent_sdk/types/hooks.rb) and [examples/lifecycle_hooks_example.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/lifecycle_hooks_example.rb).
|
|
23
25
|
|
|
24
26
|
`background_tasks` and `session_crons` are optional arrays of raw CLI hashes.
|
|
25
27
|
`nil` means the CLI did not provide a snapshot; `[]` means it provided an empty
|
|
@@ -40,7 +42,7 @@ Async do
|
|
|
40
42
|
return {} unless input.respond_to?(:tool_name) && input.tool_name == 'Bash'
|
|
41
43
|
|
|
42
44
|
tool_input = input.tool_input || {}
|
|
43
|
-
command = tool_input[:command] ||
|
|
45
|
+
command = tool_input[:command] || ''
|
|
44
46
|
block_patterns = ['rm -rf', 'foo.sh']
|
|
45
47
|
|
|
46
48
|
block_patterns.each do |pattern|
|
|
@@ -105,7 +107,7 @@ Async do
|
|
|
105
107
|
return ClaudeAgentSDK::PermissionResultAllow.new if tool_name == 'Read'
|
|
106
108
|
|
|
107
109
|
if tool_name == 'Write'
|
|
108
|
-
file_path = input[:file_path]
|
|
110
|
+
file_path = input[:file_path]
|
|
109
111
|
if file_path && file_path.include?('/etc/')
|
|
110
112
|
return ClaudeAgentSDK::PermissionResultDeny.new(
|
|
111
113
|
message: 'Cannot write to sensitive system files',
|
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`
|
|
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
|
|
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`
|
|
138
|
-
|
|
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` |
|
|
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
|
-
|
|
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
|
-
|
|
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)
|