claude-agent-sdk 0.29.0 → 0.31.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 +36 -0
- data/README.md +54 -4
- data/docs/configuration.md +17 -0
- data/docs/errors.md +53 -0
- data/docs/sessions.md +42 -0
- data/docs/types.md +71 -3
- data/lib/claude_agent_sdk/cli_installer.rb +459 -0
- data/lib/claude_agent_sdk/command_builder.rb +24 -0
- data/lib/claude_agent_sdk/errors.rb +152 -0
- data/lib/claude_agent_sdk/message_parser.rb +38 -3
- data/lib/claude_agent_sdk/query.rb +69 -29
- data/lib/claude_agent_sdk/session_resume.rb +223 -33
- data/lib/claude_agent_sdk/sessions.rb +112 -19
- data/lib/claude_agent_sdk/subprocess_cli_transport.rb +36 -1
- data/lib/claude_agent_sdk/types.rb +180 -0
- data/lib/claude_agent_sdk/version.rb +1 -1
- data/lib/claude_agent_sdk.rb +33 -27
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ec4f48758138c2a833ba6adc419753f8b8c1b7b2555824c5b2a497d6ab191c8a
|
|
4
|
+
data.tar.gz: 48787d59ffcbd3fa6c90b953234064065aad8cfc87ddc85d77f042d87a9bb250
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: dce3bf872a4bfe12c72248a9f036d2c706600679e9724200eef80da47a093c4e1d7be3e9ec7b895105a35f56f07dce26d8516b03c26c040fd60f8a53b594a785
|
|
7
|
+
data.tar.gz: 4a51034acf467515e897a8739b3f0dc0c85392bd88d88fe90b21251af98f78b29a7fc5877305e7136c7fa5cf9a0e5b64edd718d20794e90fbb14614619ff6246
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,42 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.31.0] - 2026-08-28
|
|
11
|
+
|
|
12
|
+
Syncs the gem with Python SDK **0.2.147** (previously 0.2.134). The intervening Python releases 0.2.135/136/138/139/141–147 only bump the CLI binary Python bundles; this gem does not vendor a CLI, so they carry no Ruby-side change.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
- **`ConversationResetMessage`** (port of Python [#1196](https://github.com/anthropics/claude-agent-sdk-python/pull/1196), v0.2.137). The CLI announces a mid-session transcript discard (`/clear`, and any other flow that replaces the conversation without ending the connection) with a top-level `conversation_reset` frame. It was previously dropped by the parser's forward-compatibility fallthrough, so applications never saw resets — including ones they did not initiate. Carries `new_conversation_id`, `uuid` and `session_id`. A reset also **zeroes the running totals** on subsequent `ResultMessage`s (`total_cost_usd` and friends), so snapshot them when this arrives. `new_conversation_id` is not the next `session_id`; read that from the following message.
|
|
16
|
+
- **Compatibility:** this widens the `Message` union with a frame that was previously dropped silently. Code that raises on an unrecognized message class (`case msg ... else raise`) will now see it — on the first `/clear` of a long-lived session.
|
|
17
|
+
- **`origin` on `UserMessage` and `ResultMessage`** (port of Python [#1199](https://github.com/anthropics/claude-agent-sdk-python/pull/1199), v0.2.137). In streaming/`Client` mode one connection interleaves the turns the application sends with turns the session injects on its own — background-task notifications, fired scheduled-task prompts, MCP channel messages, messages relayed from peer sessions. `origin` distinguishes them, so a consumer can tell "this result answers my prompt" from a task-notification follow-up. The CLI's object is passed through **verbatim** — including keys this version does not model, so newer origin kinds stay visible — and anything that is not an object with a String `kind` reads as `nil`. **Keys are Symbols and non-`kind` keys keep the CLI's camelCase spelling** (`origin[:fromSession]`), unlike the Python SDK's string-keyed equivalent. Prompts sent through `query()` / `Client#query` arrive unattributed unless the host stamps `origin: { kind: 'human' }` itself.
|
|
18
|
+
- **`ClaudeAgentOptions#forward_subagent_text`** (port of Python [#1206](https://github.com/anthropics/claude-agent-sdk-python/pull/1206), v0.2.140). Forwards a subagent's text and thinking blocks as messages in the stream, not just its `tool_use` / `tool_result` blocks, so consumers can render the full nested transcript. Negotiated on the control-protocol handshake, so it applies to both `query()` and `Client`. Matches the TypeScript SDK's `forwardSubagentText`.
|
|
19
|
+
- **`ClaudeAgentOptions#resume_drops_turn`** (port of Python [#1198](https://github.com/anthropics/claude-agent-sdk-python/pull/1198), v0.2.137). Completes the truncating-resume pair alongside the existing `resume_session_at`: names the user prompt whose turn a truncating resume intends to discard, and the CLI refuses the resume if anything past the fork point is not attributable to that turn — so a caller can rewind to "before my last prompt" without silently dropping a queued message or task notification the session absorbed mid-turn and the caller never observed. A refusal surfaces as a `ResultError` whose message contains `Resume rejected by --resume-drops-turn:`; treat it as deterministic rather than retrying. Forwarded in equals form, and an empty string is forwarded rather than dropped so the CLI rejects it as malformed instead of the SDK silently disarming a guard the caller believes is armed. As in the TypeScript and Python SDKs, no SDK-side validation of the option combination is applied.
|
|
20
|
+
- **`ClaudeAgentSDK::ResultError`** (port of Python [#1205](https://github.com/anthropics/claude-agent-sdk-python/pull/1205), v0.2.140). When a run fails, the CLI emits a `result` with `is_error: true` and then exits non-zero on purpose, for shell-script consumers; the trailing failure carried nothing beyond "exit code 1". The SDK now raises a typed `ResultError` carrying `subtype`, `errors`, `result`, `api_error_status`, `terminal_reason`, `session_id`, the raw `data`, and `original_error` (the bare exit failure it replaced), so callers can branch on *why* a run failed without string matching. It **subclasses `ProcessError`**, so existing `rescue ProcessError` handlers keep working — rescue `ResultError` first to reach the structured fields.
|
|
21
|
+
- **`SessionMessage#parent_agent_id`**, and `parent_tool_use_id` is now populated for subagent transcripts (port of Python [#1207](https://github.com/anthropics/claude-agent-sdk-python/pull/1207), v0.2.140). `get_subagent_messages` and `get_subagent_messages_from_store` previously returned `parent_tool_use_id: nil` for every message, losing the link to the Agent `tool_use` block in the parent session that spawned the subagent. Both are now recovered from the `agent-<id>.meta.json` sidecar (or the `agent_metadata` entry in a `SessionStore`), and stay `nil` when it is missing or unusable.
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
- **A refused or failed resume no longer reports a bare "exit code 1" to in-flight control requests** (port of Python [#1198](https://github.com/anthropics/claude-agent-sdk-python/pull/1198)). The CLI reports a rejected resume as an error result on stdout and then exits **before** answering the SDK's `initialize`. The read loop already replaced the generic process failure with the CLI's own error text for the message stream, but pending control requests — including that in-flight handshake — were signalled with the raw exception first, so callers saw `Command failed with exit code 1` with the actual reason discarded. Pending requests now receive the same enriched `ResultError`. This also improves resuming a nonexistent session, which takes the same path.
|
|
25
|
+
- **`can_use_tool` no longer has its permission requests cut off when it is the only bidirectional feature in use.** The check deciding whether the CLI may still send control requests needing a reply considered only SDK MCP servers and hooks, so an `Enumerator` prompt using `can_use_tool` with neither of those closed stdin as soon as the input ended; any later permission `control_request` then failed CLI-side with "Stream closed". This was a live defect on an already-supported path, independent of the string-prompt change below.
|
|
26
|
+
- **Store-backed resume now seeds the caller's `settings.json` and `cowork_settings.json`** into the temporary config directory (port of Python [#1197](https://github.com/anthropics/claude-agent-sdk-python/pull/1197), v0.2.137). Only `.credentials.json` and `.claude.json` were copied, leaving behind `apiKeyHelper` — a fourth authentication mechanism alongside the credentials file, the macOS Keychain and environment variables — plus the user's `env`, `hooks` and `permissions`. A host authenticating solely via `apiKeyHelper` therefore failed with **"Not logged in"** the moment it resumed from a store, with nothing in the error pointing at why. Both files pass through a transform dropping `enabledPlugins` / `extraKnownMarketplaces` (which would reconcile against the always-empty temporary plugin cache and network-install every declared marketplace on each resume) and `env.CLAUDE_CONFIG_DIR` (which would point the subprocess's config reads back out of the temporary directory). A UTF-8 BOM is tolerated, content that is not a JSON object is copied through byte-for-byte, and the result is written `0600`.
|
|
27
|
+
- **A seed file that cannot be read no longer aborts an otherwise-valid resume.** These files are best-effort enrichment of the temporary config directory: a failure other than "not found" is now logged and skipped (removing any partial destination) rather than propagating. Readers also check for a regular file before opening, so a directory or a FIFO in place of a config file is skipped instead of raising — or, for a FIFO, hanging the resume forever.
|
|
28
|
+
- **An unusable subagent metadata sidecar no longer breaks every later resume.** `JSON.parse` is lenient about illegal bytes inside an otherwise well-formed UTF-8-tagged document, returning a Hash holding invalidly-encoded values that only fail later, at generate time. Session import persisted such a Hash as an `agent_metadata` entry, and every subsequent resume through that store then died re-serializing it with an opaque `JSON::GeneratorError` — permanently, until the store entry was repaired externally. Unusable bytes are now treated as an absent sidecar, matching the helper's documented contract. The same regular-file guard prevents a FIFO sidecar from hanging `get_subagent_messages`.
|
|
29
|
+
- **`settings.json` containing a lone surrogate escape is no longer copied through unstripped.** Ruby's `JSON.parse` rejects lone surrogates outright, which fell back to the byte-for-byte passthrough — so `enabledPlugins` survived and the resumed CLI went on network-installing marketplaces on every resume, the exact misbehavior the seeding transform exists to prevent.
|
|
30
|
+
- **Redacting `.credentials.json` no longer aborts a resume on a serialization failure**, and its comment now describes what the code actually does; the rescue covered only parse failures, so a generate failure propagated and took down a resume that every other seed-file path is designed to survive.
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
- **`can_use_tool` now works with a String prompt** (port of Python [#1204](https://github.com/anthropics/claude-agent-sdk-python/pull/1204), v0.2.140). The callback previously required an `Enumerator` prompt and raised `ArgumentError` otherwise. The SDK is always streaming internally, so the restriction was unnecessary once stdin is held open for the permission round-trip. Validation of the `can_use_tool` / `permission_prompt_tool_name` conflict and the shadowing advisory are now shared by `query()` and `Client#connect` instead of duplicated.
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
## [0.30.0] - 2026-08-09
|
|
37
|
+
|
|
38
|
+
### Added
|
|
39
|
+
- **`ClaudeAgentSDK::CLIInstaller`** — downloads a pinned `claude` CLI binary into a project-local directory (`vendor/claude` by default) for hermetic deploys, so a Docker image or CI job runs a known CLI version instead of whatever `npm install -g` last put on `PATH`. `CLIInstaller.install(version: '2.1.220', dir: nil)` resolves `stable`/`latest` dist-tags through the official release endpoint, verifies the platform's SHA-256 from the release manifest, streams the ~280MB binary to an unpredictable sibling temp file (opened `O_EXCL`, so a pre-planted path or symlink is never written through) and renames it into place atomically (`0755`), then records the version **and** the verified checksum in a sibling `VERSION` file. `CLIInstaller.installed_path` returns the vendored binary or `nil`. Stdlib only — no new runtime dependency, and no binary shipped inside the gem. Supports macOS and Linux (glibc + musl, x86_64 + arm64, with a Rosetta 2 override so an x86_64 Ruby on Apple Silicon gets the native build); every failure raises the new `ClaudeAgentSDK::CLIInstallError`, filesystem errors included (wrapped, with `cause` preserved).
|
|
40
|
+
- **Idempotent without trusting the recorded version alone**: the shortcut re-hashes the vendored binary and skips the download only when version *and* checksum match, so a truncated or swapped binary is reinstalled rather than used. It makes no network request — repeat boots work offline.
|
|
41
|
+
- **Concurrency-safe**: an exclusive `flock` on `<dir>/.install.lock` covers dist-tag resolution and the whole check → download → record → publish sequence (last resolver wins, so a slow installer cannot downgrade a newer version published while it waited), and the `VERSION` file is written atomically (temp + rename), so parallel installs into one directory can never observe or produce a half-installed state. Temp files abandoned by an install that was killed before it could clean up are swept on the next run. Discovery (`installed_path`, `find_cli`) stays lock-free: the binary only ever changes by an atomic rename of a fully verified file, so a reader sees the intact old binary or the intact new one.
|
|
42
|
+
- **A failed install never breaks a working one**: the binary is downloaded, verified and *recorded* before the rename that publishes it, and nothing can fail after that rename. A failed upgrade therefore leaves the previously installed binary intact and runnable, with the next `install` redoing it cleanly; a failed first install leaves nothing behind.
|
|
43
|
+
- Text responses are size-capped (1KB for the version endpoint, 5MB for the manifest), and the binary download is bounded by the manifest's declared `size` when present — an over-long stream is aborted instead of filling the disk. The binary streams to disk and is never buffered in memory.
|
|
44
|
+
- **CLI discovery now honors `CLAUDE_CLI_PATH` and the vendored binary.** `SubprocessCLITransport#find_cli` probes `CLAUDE_CLI_PATH` (when it points at an executable), then `CLIInstaller.installed_path`, then the existing `which claude` and common-location logic. The vendored copy deliberately outranks `PATH` — that is what makes a pinned install hermetic. `CLINotFoundError` now also mentions both escape hatches.
|
|
45
|
+
|
|
10
46
|
## [0.29.0] - 2026-08-09
|
|
11
47
|
|
|
12
48
|
### Fixed
|
data/README.md
CHANGED
|
@@ -25,7 +25,7 @@ All three SDKs share the same underlying mechanism: they spawn the `claude` CLI
|
|
|
25
25
|
| Hooks (all 27 events) | ✅ | ✅ | ✅ |
|
|
26
26
|
| Permission callbacks | ✅ | ✅ | ✅ |
|
|
27
27
|
| Structured output | ✅ | ✅ | ✅ |
|
|
28
|
-
| All
|
|
28
|
+
| All 25 message types | ✅ | partial | ✅ |
|
|
29
29
|
| [Sandbox](https://github.com/anthropic-experimental/sandbox-runtime) settings | ✅ | partial | ✅ |
|
|
30
30
|
| Bare mode (`--bare`) | ✅ | ✅ | ✅ |
|
|
31
31
|
| File checkpointing & rewind | ✅ | ✅ | ✅ |
|
|
@@ -36,7 +36,7 @@ All three SDKs share the same underlying mechanism: they spawn the `claude` CLI
|
|
|
36
36
|
| Custom transport (pluggable I/O) | — | — | ✅ |
|
|
37
37
|
| Rails integration | — | — | ✅ |
|
|
38
38
|
|
|
39
|
-
**Where Ruby goes further:** Built-in OpenTelemetry observer with Langfuse flow diagram support — no third-party instrumentation library needed. Custom transport support lets you swap the subprocess for any I/O layer (e.g., connect to a remote Claude Code instance over SSH or a container). Rails integration provides a `configure` block for initializers with thread-safe observer factories, and plays well with ActionCable for real-time streaming. Full typed coverage for all
|
|
39
|
+
**Where Ruby goes further:** Built-in OpenTelemetry observer with Langfuse flow diagram support — no third-party instrumentation library needed. Custom transport support lets you swap the subprocess for any I/O layer (e.g., connect to a remote Claude Code instance over SSH or a container). Rails integration provides a `configure` block for initializers with thread-safe observer factories, and plays well with ActionCable for real-time streaming. Full typed coverage for all 25 CLI message types and all 27 hook events.
|
|
40
40
|
|
|
41
41
|
**What's missing:** The Ruby gem does not bundle the `claude` CLI binary (`npm install -g @anthropic-ai/claude-code`).
|
|
42
42
|
|
|
@@ -68,7 +68,7 @@ Add this line to your application's Gemfile:
|
|
|
68
68
|
gem 'claude-agent-sdk', github: 'ya-luotao/claude-agent-sdk-ruby'
|
|
69
69
|
|
|
70
70
|
# Or use a stable version from RubyGems
|
|
71
|
-
gem 'claude-agent-sdk', '~> 0.
|
|
71
|
+
gem 'claude-agent-sdk', '~> 0.31.0'
|
|
72
72
|
```
|
|
73
73
|
|
|
74
74
|
Then `bundle install`, or install directly: `gem install claude-agent-sdk`.
|
|
@@ -78,6 +78,56 @@ Then `bundle install`, or install directly: `gem install claude-agent-sdk`.
|
|
|
78
78
|
- Node.js
|
|
79
79
|
- Claude Code 2.0.0+: `npm install -g @anthropic-ai/claude-code`
|
|
80
80
|
|
|
81
|
+
### Vendoring the CLI (hermetic deploys)
|
|
82
|
+
|
|
83
|
+
The SDK runs the `claude` CLI as a subprocess, so a deploy is only reproducible if the CLI version is pinned with it. `CLIInstaller` downloads a pinned binary from the official release endpoint into a project-local directory (`vendor/claude` by default) — checksum-verified, no npm/Node at runtime, and nothing extra shipped inside the gem.
|
|
84
|
+
|
|
85
|
+
```ruby
|
|
86
|
+
require 'claude_agent_sdk'
|
|
87
|
+
|
|
88
|
+
# 'stable' (default), 'latest', or a concrete version — pin it in production.
|
|
89
|
+
ClaudeAgentSDK::CLIInstaller.install(version: '2.1.220')
|
|
90
|
+
# => "/app/vendor/claude/claude"
|
|
91
|
+
|
|
92
|
+
ClaudeAgentSDK::CLIInstaller.install(version: '2.1.220', dir: '/opt/claude')
|
|
93
|
+
|
|
94
|
+
# nil unless a binary is already installed there
|
|
95
|
+
ClaudeAgentSDK::CLIInstaller.installed_path
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`install` is idempotent and safe to run concurrently, so it fits `bin/setup`, a cached Docker layer, and every process of a multi-process boot:
|
|
99
|
+
|
|
100
|
+
- The install directory's `VERSION` file records the installed version **and** the SHA-256 that was verified at download time. The shortcut re-hashes the vendored binary (~0.1s for the real 245MB binary) and only skips the download when both match — a truncated, swapped or half-written binary is reinstalled instead of trusted. It makes **no network request**, so repeat boots work offline — with a pinned concrete version; `'stable'`/`'latest'` must always re-resolve through the endpoint, which is one more reason to pin in production.
|
|
101
|
+
- An exclusive `flock` on `<dir>/.install.lock` covers the whole check → download → place → record sequence, so parallel installs into one directory don't race; the loser simply observes the finished install.
|
|
102
|
+
|
|
103
|
+
Failures (unsupported platform, invalid version, HTTP error, response-size cap, oversized download, checksum mismatch, filesystem errors) raise `ClaudeAgentSDK::CLIInstallError`.
|
|
104
|
+
|
|
105
|
+
**A failed install never breaks a working one.** The new binary is downloaded to a temp file, checksum-verified and recorded, and only then renamed into place — the rename is the last step, and nothing can fail after it. So a failed upgrade leaves the previously installed binary intact and runnable (the SDK keeps working), and the next `install` redoes it cleanly. A first install that fails leaves nothing behind at all.
|
|
106
|
+
|
|
107
|
+
> The vendored directory is trusted input: anything that can write to it can replace the binary the SDK executes. Keep it inside your deploy artifact, owned by the deploy user and not world-writable, exactly as you would treat `bin/`.
|
|
108
|
+
|
|
109
|
+
```dockerfile
|
|
110
|
+
# Dockerfile — pin the CLI in its own cached layer
|
|
111
|
+
RUN bundle exec ruby -e "require 'claude_agent_sdk'; \
|
|
112
|
+
ClaudeAgentSDK::CLIInstaller.install(version: '2.1.220')"
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
```ruby
|
|
116
|
+
#!/usr/bin/env ruby
|
|
117
|
+
# bin/setup
|
|
118
|
+
require 'claude_agent_sdk'
|
|
119
|
+
puts ClaudeAgentSDK::CLIInstaller.install(version: ENV.fetch('CLAUDE_CLI_VERSION', 'stable'))
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Supported platforms: `darwin-arm64`, `darwin-x64` (Rosetta 2 gets the arm64 build), `linux-x64`, `linux-arm64`, and the `-musl` variants. Windows is not supported.
|
|
123
|
+
|
|
124
|
+
**CLI discovery order.** With no explicit `cli_path:` in `ClaudeAgentOptions`, the transport probes in this order:
|
|
125
|
+
|
|
126
|
+
1. `CLAUDE_CLI_PATH` — an explicit path to an executable, no discovery at all (a relative value is resolved against the process's working directory, not `cwd:`)
|
|
127
|
+
2. The vendored binary (`CLIInstaller.installed_path`) — deliberately ahead of `PATH`, so a pinned install beats whatever is installed globally
|
|
128
|
+
3. `which claude`
|
|
129
|
+
4. Common install locations (`~/.claude/local/claude`, `/usr/local/bin/claude`, …)
|
|
130
|
+
|
|
81
131
|
### Agentic Coding Skill
|
|
82
132
|
|
|
83
133
|
If you're using [Claude Code](https://claude.ai/claude-code), this repo is a Claude Code plugin marketplace. Add it once, then install the skill:
|
|
@@ -222,7 +272,7 @@ options = ClaudeAgentSDK::ClaudeAgentOptions.new(
|
|
|
222
272
|
|---------|-------------|
|
|
223
273
|
| [quick_start.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/quick_start.rb) | Basic `query()` usage with options |
|
|
224
274
|
| [client_example.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/client_example.rb) | Interactive Client usage |
|
|
225
|
-
| [message_types_example.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/message_types_example.rb) | Handling all
|
|
275
|
+
| [message_types_example.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/message_types_example.rb) | Handling all 25 SDK message types |
|
|
226
276
|
| [streaming_input_example.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/streaming_input_example.rb) | Streaming input for multi-turn conversations |
|
|
227
277
|
| [session_resumption_example.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/session_resumption_example.rb) | Multi-turn conversations with session persistence |
|
|
228
278
|
| [structured_output_example.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/structured_output_example.rb) | JSON schema structured output |
|
data/docs/configuration.md
CHANGED
|
@@ -223,6 +223,23 @@ options = ClaudeAgentSDK::ClaudeAgentOptions.new(
|
|
|
223
223
|
|
|
224
224
|
See [examples/bare_mode_example.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/bare_mode_example.rb).
|
|
225
225
|
|
|
226
|
+
## Forwarding Subagent Text
|
|
227
|
+
|
|
228
|
+
By default only `tool_use` / `tool_result` blocks from subagents (spawned via
|
|
229
|
+
the Agent tool) reach the message stream, as `AssistantMessage` /
|
|
230
|
+
`UserMessage` objects whose `parent_tool_use_id` is the spawning Agent
|
|
231
|
+
`tool_use` id — enough for a progress heartbeat. Set `forward_subagent_text`
|
|
232
|
+
to forward the subagent's **text and thinking** blocks the same way, so you
|
|
233
|
+
can render the full nested transcript:
|
|
234
|
+
|
|
235
|
+
```ruby
|
|
236
|
+
options = ClaudeAgentSDK::ClaudeAgentOptions.new(forward_subagent_text: true)
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Matches the TypeScript SDK's `forwardSubagentText`. The capability is
|
|
240
|
+
negotiated on the control-protocol handshake, so it applies to both
|
|
241
|
+
`ClaudeAgentSDK.query` and `Client`.
|
|
242
|
+
|
|
226
243
|
## File Checkpointing & Rewind
|
|
227
244
|
|
|
228
245
|
Enable file checkpointing to revert file changes to a previous state:
|
data/docs/errors.md
CHANGED
|
@@ -31,6 +31,12 @@ rescue ClaudeAgentSDK::ControlRequestTimeoutError
|
|
|
31
31
|
puts "Control protocol timed out — consider increasing the timeout"
|
|
32
32
|
rescue ClaudeAgentSDK::CLINotFoundError
|
|
33
33
|
puts "Please install Claude Code"
|
|
34
|
+
rescue ClaudeAgentSDK::ResultError => e
|
|
35
|
+
# More specific than ProcessError — must be rescued first.
|
|
36
|
+
case e.terminal_reason
|
|
37
|
+
when 'api_error' then puts "API failed (HTTP #{e.api_error_status}): #{e.result}"
|
|
38
|
+
else puts "Run failed (#{e.subtype}): #{e.errors.join('; ')}"
|
|
39
|
+
end
|
|
34
40
|
rescue ClaudeAgentSDK::ProcessError => e
|
|
35
41
|
puts "Process failed with exit code: #{e.exit_code}"
|
|
36
42
|
rescue ClaudeAgentSDK::CLIJSONDecodeError => e
|
|
@@ -38,6 +44,40 @@ rescue ClaudeAgentSDK::CLIJSONDecodeError => e
|
|
|
38
44
|
end
|
|
39
45
|
```
|
|
40
46
|
|
|
47
|
+
## Terminal Error Results
|
|
48
|
+
|
|
49
|
+
When a run fails, the CLI emits a `result` message with `is_error: true` (which
|
|
50
|
+
you still receive as a `ResultMessage`) and *then* exits non-zero on purpose,
|
|
51
|
+
for shell-script consumers. That trailing process failure carries nothing
|
|
52
|
+
beyond "exit code 1", so the SDK replaces it with a `ResultError` carrying the
|
|
53
|
+
payload the CLI already reported — you can branch on *why* the run failed
|
|
54
|
+
without matching on strings:
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
begin
|
|
58
|
+
ClaudeAgentSDK.query(prompt: "...") { |m| handle(m) }
|
|
59
|
+
rescue ClaudeAgentSDK::ResultError => e
|
|
60
|
+
retry_later if e.terminal_reason == 'api_error' # overloaded / timeout
|
|
61
|
+
widen_budget if e.subtype == 'error_max_turns'
|
|
62
|
+
raise
|
|
63
|
+
end
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`ResultError` subclasses `ProcessError`, so existing `rescue ProcessError`
|
|
67
|
+
handlers keep working unchanged — but rescue `ResultError` **first** if you
|
|
68
|
+
want the structured fields.
|
|
69
|
+
|
|
70
|
+
The exception message prefers, in order: the CLI's `errors[]`, then `result`,
|
|
71
|
+
then a non-`success` `subtype`, then the HTTP status. A run that ends on an API
|
|
72
|
+
failure arrives as `subtype: "success"` with `is_error: true` and the prose in
|
|
73
|
+
`result`, which is why `subtype` alone is not used as the message.
|
|
74
|
+
|
|
75
|
+
A refused resume (a nonexistent session, or a `resume_drops_turn` guard
|
|
76
|
+
failure) reaches you the same way — including on a control request such as the
|
|
77
|
+
initial handshake that was still in flight when the CLI exited. Match on
|
|
78
|
+
`Resume rejected by --resume-drops-turn:` in the message and treat it as
|
|
79
|
+
deterministic: clear the fork target and resume plainly rather than retrying.
|
|
80
|
+
|
|
41
81
|
## Configuring Timeout
|
|
42
82
|
|
|
43
83
|
The control request timeout defaults to **1200 seconds** (20 minutes) to accommodate long-running agent sessions. Override it via environment variable:
|
|
@@ -70,6 +110,19 @@ class ProcessError < ClaudeSDKError
|
|
|
70
110
|
:stderr # String | nil
|
|
71
111
|
end
|
|
72
112
|
|
|
113
|
+
# Raised when the CLI exits after reporting a terminal error result.
|
|
114
|
+
# Subclasses ProcessError, so existing `rescue ProcessError` keeps working.
|
|
115
|
+
class ResultError < ProcessError
|
|
116
|
+
attr_reader :subtype, # String | nil ('error_max_turns', 'error_during_execution', ...)
|
|
117
|
+
:errors, # Array<String> - error strings from the CLI (may be empty)
|
|
118
|
+
:result, # String | nil - result text; holds the "API Error: ..." prose
|
|
119
|
+
:api_error_status, # Integer | nil - HTTP status of the failing API call
|
|
120
|
+
:terminal_reason, # String | nil - why the run ended ('api_error', 'max_turns', ...)
|
|
121
|
+
:session_id, # String | nil
|
|
122
|
+
:data, # Hash - raw `result` payload as emitted by the CLI
|
|
123
|
+
:original_error # ProcessError | nil - the bare exit error this replaced
|
|
124
|
+
end
|
|
125
|
+
|
|
73
126
|
# Raised when JSON parsing fails
|
|
74
127
|
class CLIJSONDecodeError < ClaudeSDKError
|
|
75
128
|
attr_reader :line, # String - The line that failed to parse
|
data/docs/sessions.md
CHANGED
|
@@ -49,6 +49,8 @@ messages = ClaudeAgentSDK.get_subagent_messages(session_id: "uuid-here", agent_i
|
|
|
49
49
|
|
|
50
50
|
With `directory:` given, only that project and its git worktrees are searched (no global fallback). Store-backed counterparts: `list_subagents_from_store` / `get_subagent_messages_from_store`.
|
|
51
51
|
|
|
52
|
+
> Each returned `SessionMessage` carries `parent_tool_use_id` — the id of the Agent `tool_use` block in the parent session that spawned this subagent — and `parent_agent_id`, the spawning subagent's id for nested subagents. Both are read from the `agent-<id>.meta.json` sidecar beside the transcript (or the `agent_metadata` entry in a `SessionStore`), and are `nil` when it is missing or unusable.
|
|
53
|
+
|
|
52
54
|
## Renaming a Session
|
|
53
55
|
|
|
54
56
|
```ruby
|
|
@@ -113,6 +115,46 @@ ClaudeAgentSDK.query(
|
|
|
113
115
|
|
|
114
116
|
`resume_session_at` requires `resume`; the SDK raises `ArgumentError` from `CommandBuilder` when this constraint is violated, matching the underlying CLI's validation but surfacing it synchronously in the caller's stack.
|
|
115
117
|
|
|
118
|
+
### Validating what the truncation discards
|
|
119
|
+
|
|
120
|
+
A bare `resume_session_at` silently drops everything after the fork point —
|
|
121
|
+
including a queued user message or a task notification the session absorbed
|
|
122
|
+
mid-turn that you never observed. `resume_drops_turn` names the user prompt
|
|
123
|
+
whose turn you *intend* to discard, and the CLI refuses the resume if anything
|
|
124
|
+
past the fork point is not attributable to that turn:
|
|
125
|
+
|
|
126
|
+
```ruby
|
|
127
|
+
ClaudeAgentSDK.query(
|
|
128
|
+
prompt: 'try a different approach',
|
|
129
|
+
options: ClaudeAgentSDK::ClaudeAgentOptions.new(
|
|
130
|
+
resume: session_id,
|
|
131
|
+
resume_session_at: last_kept_entry_uuid,
|
|
132
|
+
resume_drops_turn: discarded_prompt_uuid
|
|
133
|
+
)
|
|
134
|
+
) { |m| handle(m) }
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Rule of thumb: set `resume_session_at` to the **last transcript entry of the
|
|
138
|
+
turn you are keeping** (whatever its type), and `resume_drops_turn` to the
|
|
139
|
+
prompt UUID of the turn immediately after it — the next `SessionMessage` with
|
|
140
|
+
`type == 'user'` from `get_session_messages`, or the `uuid` you supplied on a
|
|
141
|
+
streamed user message.
|
|
142
|
+
|
|
143
|
+
With structured output (`output_format`) or end-turn MCP tools, a kept turn
|
|
144
|
+
ends on entries *after* its last assistant message, so forking at the assistant
|
|
145
|
+
UUID is refused by design.
|
|
146
|
+
|
|
147
|
+
A refusal surfaces as a `ResultError` whose message contains
|
|
148
|
+
`Resume rejected by --resume-drops-turn:`. Treat it as **deterministic** —
|
|
149
|
+
clear the pending fork target and resume plainly; do not retry the same
|
|
150
|
+
request. Leave `resume_drops_turn` unset to keep the unvalidated behavior.
|
|
151
|
+
|
|
152
|
+
Unlike `resume_session_at`, the SDK applies no combination validation to
|
|
153
|
+
`resume_drops_turn` and defers entirely to the CLI. An empty string is
|
|
154
|
+
forwarded rather than dropped, so the CLI rejects it as a malformed
|
|
155
|
+
declaration instead of the SDK silently disarming a guard you believe is
|
|
156
|
+
armed.
|
|
157
|
+
|
|
116
158
|
## Mirroring to a `SessionStore`
|
|
117
159
|
|
|
118
160
|
By default Claude Code writes session transcripts to local disk under
|
data/docs/types.md
CHANGED
|
@@ -6,7 +6,8 @@ See [lib/claude_agent_sdk/types.rb](https://github.com/ya-luotao/claude-agent-sd
|
|
|
6
6
|
|
|
7
7
|
```ruby
|
|
8
8
|
# Union type of all possible messages
|
|
9
|
-
Message = UserMessage | AssistantMessage | SystemMessage | ResultMessage
|
|
9
|
+
Message = UserMessage | AssistantMessage | SystemMessage | ResultMessage |
|
|
10
|
+
StreamEvent | RateLimitEvent | ConversationResetMessage
|
|
10
11
|
```
|
|
11
12
|
|
|
12
13
|
### UserMessage
|
|
@@ -18,7 +19,8 @@ class UserMessage
|
|
|
18
19
|
attr_accessor :content, # String | Array<ContentBlock>
|
|
19
20
|
:uuid, # String | nil - Unique ID for rewind support
|
|
20
21
|
:parent_tool_use_id, # String | nil
|
|
21
|
-
:tool_use_result
|
|
22
|
+
:tool_use_result, # Hash | nil - Tool result data when message is a tool response
|
|
23
|
+
:origin # Hash | nil - message provenance (see Message Origin below)
|
|
22
24
|
end
|
|
23
25
|
```
|
|
24
26
|
|
|
@@ -91,7 +93,8 @@ class ResultMessage
|
|
|
91
93
|
:uuid, # String | nil
|
|
92
94
|
:fast_mode_state, # String | nil ('off', 'cooldown', 'on')
|
|
93
95
|
:api_error_status, # Integer | nil (HTTP status on api_error subtype)
|
|
94
|
-
:terminal_reason
|
|
96
|
+
:terminal_reason, # String | nil (see below)
|
|
97
|
+
:origin # Hash | nil - origin of the triggering user message (see below)
|
|
95
98
|
end
|
|
96
99
|
```
|
|
97
100
|
|
|
@@ -109,6 +112,71 @@ optional `canonicalModel` (canonical id used for the pricing lookup, which can
|
|
|
109
112
|
differ from the raw model-string key for provider-specific ids/aliases) and
|
|
110
113
|
`provider` (`'firstParty'`, `'bedrock'`, `'vertex'`, ...).
|
|
111
114
|
|
|
115
|
+
## Message Origin
|
|
116
|
+
|
|
117
|
+
`UserMessage#origin` and `ResultMessage#origin` carry the provenance of a
|
|
118
|
+
user-role turn. In streaming/`Client` mode one connection interleaves the turns
|
|
119
|
+
your application sends with turns the session injects on its own — background
|
|
120
|
+
task notifications, fired scheduled-task prompts, MCP channel messages,
|
|
121
|
+
messages relayed from peer sessions. `origin` tells them apart:
|
|
122
|
+
|
|
123
|
+
```ruby
|
|
124
|
+
if result.origin.nil? || result.origin[:kind] == 'human'
|
|
125
|
+
# a turn this application submitted
|
|
126
|
+
elsif result.origin[:kind] == 'task-notification'
|
|
127
|
+
# follow-up turn driven by a background task
|
|
128
|
+
end
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
The Hash is passed through from the CLI **verbatim**, so:
|
|
132
|
+
|
|
133
|
+
- **Keys are Symbols**, and non-`kind` keys keep the CLI's camelCase spelling —
|
|
134
|
+
`origin[:fromSession]`, `origin[:senderTaskId]`, `origin[:verifiedPeerPid]`.
|
|
135
|
+
(The Python SDK's equivalent is string-keyed; do not port `origin["kind"]`
|
|
136
|
+
literally.)
|
|
137
|
+
- Keys this SDK version does not model still reach you, so newer CLI origin
|
|
138
|
+
kinds stay visible.
|
|
139
|
+
- Anything that is not an object with a String `kind` reads as `nil`.
|
|
140
|
+
|
|
141
|
+
Only `kind` is always present. Known kinds — treat anything unrecognized as
|
|
142
|
+
"not human":
|
|
143
|
+
|
|
144
|
+
`human`, `channel`, `peer`, `task-notification`, `coordinator`,
|
|
145
|
+
`unclassified`, `observer`, `auto-continuation`, `observer-activity`
|
|
146
|
+
|
|
147
|
+
For `kind == 'task-notification'`, `origin[:subkind]` may be
|
|
148
|
+
`scheduled-trigger` (a scheduled task's prompt fired) or `peer-send-message`
|
|
149
|
+
(a message from another of your sessions); it is absent for ordinary
|
|
150
|
+
background-task notifications.
|
|
151
|
+
|
|
152
|
+
`nil` means the CLI did not attribute the message. Prompts you send through
|
|
153
|
+
`ClaudeAgentSDK.query` or `Client#query` arrive that way unless you stamp
|
|
154
|
+
`origin: { kind: 'human' }` on the message Hash yourself — only the `human`
|
|
155
|
+
kind is honored from an SDK host. Tool-result messages never carry an origin.
|
|
156
|
+
|
|
157
|
+
### ConversationResetMessage
|
|
158
|
+
|
|
159
|
+
Emitted when the session's conversation is replaced without ending the
|
|
160
|
+
connection — after `/clear`, or any other flow that discards the transcript
|
|
161
|
+
mid-session.
|
|
162
|
+
|
|
163
|
+
```ruby
|
|
164
|
+
class ConversationResetMessage
|
|
165
|
+
attr_accessor :new_conversation_id, # String - id of the fresh conversation
|
|
166
|
+
:uuid, # String - unique ID of this message
|
|
167
|
+
:session_id # String - the session that was reset
|
|
168
|
+
end
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
A reset clears the conversation history **and zeroes the running totals**
|
|
172
|
+
reported on subsequent `ResultMessage` objects (`total_cost_usd`, and the
|
|
173
|
+
rest). If you accumulate those across a long-lived session, snapshot them when
|
|
174
|
+
this message arrives.
|
|
175
|
+
|
|
176
|
+
`new_conversation_id` is **not** the `session_id` of subsequent messages — it
|
|
177
|
+
is an opaque id for keying an empty transcript in a UI (and for discarding a
|
|
178
|
+
cached session title). Read the new session id from the next message.
|
|
179
|
+
|
|
112
180
|
## Content Block Types
|
|
113
181
|
|
|
114
182
|
```ruby
|