claude-agent-sdk 0.7.3 → 0.8.1
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 +70 -0
- data/README.md +190 -7
- data/lib/claude_agent_sdk/message_parser.rb +28 -5
- data/lib/claude_agent_sdk/query.rb +93 -8
- data/lib/claude_agent_sdk/sessions.rb +499 -0
- data/lib/claude_agent_sdk/subprocess_cli_transport.rb +110 -89
- data/lib/claude_agent_sdk/types.rb +180 -13
- data/lib/claude_agent_sdk/version.rb +1 -1
- data/lib/claude_agent_sdk.rb +87 -14
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b625dde6e1777c93be6e4bcfb6c9565087d798d414a03078f8de4184c39dfa7f
|
|
4
|
+
data.tar.gz: 30117f8eac89f94aabae5c37fdc12c012ed779a0f1c58062d70e9b727ee5ce3c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2a36e3ff75415bb4c2f42b27a8280b4828199af2972d20aa5fa4366a24daefd325e5a11c3e39ee9396144561c98e2ea1ddcc94157b80fc7aeef72f58a526e36b
|
|
7
|
+
data.tar.gz: f0fde6a0779679869bd4d6607f7220ea016c021896a97a421be650e169ef53719d5810cf614613c7cb52b24c19ebbf2f3908f3cc49f07bc5b13aa58f377e1a8b
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,76 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.8.1] - 2026-03-08
|
|
9
|
+
|
|
10
|
+
Python SDK parity fixes for one-shot `query()` control protocol and CLI transport.
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
#### One-Shot Query Control Protocol
|
|
15
|
+
- **Hooks and `can_use_tool` in `query()`:** One-shot `query()` now passes `hooks`, `can_use_tool`, and SDK MCP servers through to the `Query` handler, matching the Python SDK (previously these were Client-only)
|
|
16
|
+
- **`can_use_tool` validation:** String prompts with `can_use_tool` now raise `ArgumentError` (streaming mode required); conflicting `permission_prompt_tool_name` also raises early
|
|
17
|
+
- **`session_id` parity:** One-shot queries now send `session_id: ''` (was `'default'`), matching Python SDK behavior
|
|
18
|
+
- **Premature stdin close:** Added `wait_for_result_and_end_input` that holds stdin open until the first result when hooks or SDK MCP servers need control message exchange
|
|
19
|
+
- **`stream_input` stdin leak:** Moved `end_input` to `ensure` block so stdin is always closed even when the stream enumerator raises
|
|
20
|
+
- **`Async::Condition` race:** Added `@first_result_received` flag guard to prevent lost signals when result arrives before `wait` is called
|
|
21
|
+
|
|
22
|
+
#### CLI Transport Parity
|
|
23
|
+
- **File checkpointing:** Moved from deprecated `--enable-file-checkpointing` CLI flag to `CLAUDE_CODE_ENABLE_SDK_FILE_CHECKPOINTING` environment variable
|
|
24
|
+
- **Partial messages:** Now also sets `CLAUDE_CODE_ENABLE_FINE_GRAINED_TOOL_STREAMING=1` environment variable when `include_partial_messages` is enabled
|
|
25
|
+
- **Tools preset:** `ToolsPreset` objects and preset hashes now map to `--tools default` (was `--tools <json>`)
|
|
26
|
+
- **Plugins:** Changed from `--plugins <json>` to `--plugin-dir <path>` per-plugin, matching current CLI interface
|
|
27
|
+
- **Plugin type:** `SdkPluginConfig` now defaults to `type: 'local'` (was `'plugin'`), normalizes legacy `'plugin'` type
|
|
28
|
+
- **Rewind control request:** Changed key from `userMessageUuid` to `user_message_id` for Python SDK parity
|
|
29
|
+
- **Settings file with sandbox:** When sandbox is enabled and settings is a file path, now reads and parses the file to merge sandbox settings (raises on missing/invalid files instead of silently dropping settings)
|
|
30
|
+
|
|
31
|
+
#### Hook Input Parsing
|
|
32
|
+
- **Falsy value preservation:** `parse_hook_input` now uses `key?`-based lookup instead of `||`, correctly preserving `false` and `nil` values (e.g., `stop_hook_active: false`)
|
|
33
|
+
- **Empty hooks normalization:** `query()` now skips empty matcher lists and normalizes hooks to `nil` when no matchers survive, preventing unnecessary 60s close-wait timeout
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
- **`build_command` refactored:** Extracted `build_settings_args`, `build_tools_args`, `build_output_format_args`, `build_mcp_servers_args`, `build_plugins_args` private helpers to reduce method complexity
|
|
37
|
+
|
|
38
|
+
## [0.8.0] - 2026-03-05
|
|
39
|
+
|
|
40
|
+
Port of Python SDK v0.1.46 features.
|
|
41
|
+
|
|
42
|
+
### Added
|
|
43
|
+
|
|
44
|
+
#### Task Message Types
|
|
45
|
+
- `TaskStartedMessage`, `TaskProgressMessage`, `TaskNotificationMessage` — typed `SystemMessage` subclasses for background task lifecycle events
|
|
46
|
+
- `TASK_NOTIFICATION_STATUSES` constant (`completed`, `failed`, `stopped`)
|
|
47
|
+
- `MessageParser` dispatches on `subtype` within `system` messages, falling back to generic `SystemMessage` for unknown subtypes
|
|
48
|
+
|
|
49
|
+
#### MCP Server Control
|
|
50
|
+
- `reconnect_mcp_server(server_name)` on `Query` and `Client` — retry failed MCP server connections
|
|
51
|
+
- `toggle_mcp_server(server_name, enabled)` on `Query` and `Client` — enable/disable MCP servers live
|
|
52
|
+
- `stop_task(task_id)` on `Query` and `Client` — stop a running background task
|
|
53
|
+
|
|
54
|
+
#### Subagent Context on Hook Inputs
|
|
55
|
+
- `agent_id` and `agent_type` attributes on `PreToolUseHookInput`, `PostToolUseHookInput`, `PostToolUseFailureHookInput`, `PermissionRequestHookInput`
|
|
56
|
+
- Populated when hooks fire inside subagents, allowing attribution of tool calls to specific agents
|
|
57
|
+
|
|
58
|
+
#### Result Message
|
|
59
|
+
- `stop_reason` attribute on `ResultMessage` (e.g., `'end_turn'`, `'max_tokens'`, `'stop_sequence'`)
|
|
60
|
+
|
|
61
|
+
#### Typed MCP Status Response
|
|
62
|
+
- `McpServerInfo`, `McpToolAnnotations`, `McpToolInfo`, `McpServerStatus`, `McpStatusResponse` types
|
|
63
|
+
- `.parse` class methods for hydrating from raw CLI response hashes
|
|
64
|
+
- `MCP_SERVER_CONNECTION_STATUSES` constant (`connected`, `failed`, `needs-auth`, `pending`, `disabled`)
|
|
65
|
+
|
|
66
|
+
#### Session Browsing
|
|
67
|
+
- `ClaudeAgentSDK.list_sessions(directory:, limit:, include_worktrees:)` — list sessions from `~/.claude/projects/` JSONL files
|
|
68
|
+
- `ClaudeAgentSDK.get_session_messages(session_id:, directory:, limit:, offset:)` — reconstruct conversation chain from session transcript
|
|
69
|
+
- `SDKSessionInfo` type with `session_id`, `summary`, `last_modified`, `file_size`, `custom_title`, `first_prompt`, `git_branch`, `cwd`
|
|
70
|
+
- `SessionMessage` type with `type`, `uuid`, `session_id`, `message`
|
|
71
|
+
- Pure filesystem operations — no CLI subprocess required
|
|
72
|
+
- Git worktree-aware session scanning
|
|
73
|
+
- `parentUuid` chain walking with cycle detection for robust conversation reconstruction
|
|
74
|
+
|
|
75
|
+
### Fixed
|
|
76
|
+
- **`McpToolAnnotations.parse` losing `false` values:** `readOnly: false` was evaluated as `false || nil → nil` due to `||` short-circuiting. Now uses `.key?` to check presence before falling back to snake_case keys.
|
|
77
|
+
|
|
8
78
|
## [0.7.3] - 2026-02-26
|
|
9
79
|
|
|
10
80
|
### Fixed
|
data/README.md
CHANGED
|
@@ -6,6 +6,119 @@
|
|
|
6
6
|
|
|
7
7
|
[](https://badge.fury.io/rb/claude-agent-sdk)
|
|
8
8
|
|
|
9
|
+
### Feature Parity with Python SDK (v0.1.46)
|
|
10
|
+
|
|
11
|
+
| Feature | Python | Ruby |
|
|
12
|
+
|---------|:------:|:----:|
|
|
13
|
+
| One-shot `query()` | `query()` | `query()` |
|
|
14
|
+
| Bidirectional `Client` | `ClaudeSDKClient` | `Client` |
|
|
15
|
+
| Streaming input | `AsyncIterable` | `Enumerator` |
|
|
16
|
+
| Custom tools (SDK MCP servers) | `@tool` decorator | `create_tool` block |
|
|
17
|
+
| MCP resources & prompts | ✅ | ✅ |
|
|
18
|
+
| Hooks (all 10 events) | ✅ | ✅ |
|
|
19
|
+
| Permission callbacks (`can_use_tool`) | ✅ | ✅ |
|
|
20
|
+
| Structured output | ✅ | ✅ |
|
|
21
|
+
| Thinking config (adaptive/enabled/disabled) | ✅ | ✅ |
|
|
22
|
+
| Effort levels | ✅ | ✅ |
|
|
23
|
+
| Programmatic subagents | ✅ | ✅ |
|
|
24
|
+
| Sandbox settings | ✅ | ✅ |
|
|
25
|
+
| Beta features (1M context) | ✅ | ✅ |
|
|
26
|
+
| File checkpointing & rewind | ✅ | ✅ |
|
|
27
|
+
| Session browsing (`list_sessions`, `get_session_messages`) | ✅ | ✅ |
|
|
28
|
+
| Task message types (started/progress/notification) | ✅ | ✅ |
|
|
29
|
+
| MCP server control (reconnect/toggle/stop) | ✅ | ✅ |
|
|
30
|
+
| Subagent context on hook inputs | ✅ | ✅ |
|
|
31
|
+
| Typed MCP status response | ✅ | ✅ |
|
|
32
|
+
| `stop_reason` on `ResultMessage` | ✅ | ✅ |
|
|
33
|
+
| Fallback model | ✅ | ✅ |
|
|
34
|
+
| Plugin support | ✅ | ✅ |
|
|
35
|
+
| Rails integration (configure block, ActionCable) | — | ✅ |
|
|
36
|
+
| Bundled CLI binary | ✅ | — |
|
|
37
|
+
|
|
38
|
+
<details>
|
|
39
|
+
<summary><strong>Usage & Implementation Differences</strong></summary>
|
|
40
|
+
|
|
41
|
+
#### Async model
|
|
42
|
+
|
|
43
|
+
Python uses `async`/`await` with `anyio` (works with both asyncio and Trio). Ruby uses the [`async`](https://github.com/socketry/async) gem with fibers — no `await` keyword needed, blocking calls yield automatically.
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
# Python
|
|
47
|
+
async with ClaudeSDKClient(options) as client:
|
|
48
|
+
await client.query("Hello")
|
|
49
|
+
async for msg in client.receive_messages():
|
|
50
|
+
print(msg)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
```ruby
|
|
54
|
+
# Ruby
|
|
55
|
+
Async do
|
|
56
|
+
client = ClaudeAgentSDK::Client.new(options: options)
|
|
57
|
+
client.connect
|
|
58
|
+
client.query("Hello")
|
|
59
|
+
client.receive_messages { |msg| puts msg }
|
|
60
|
+
client.disconnect
|
|
61
|
+
end.wait
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
#### Custom tools
|
|
65
|
+
|
|
66
|
+
Python uses a `@tool` decorator. Ruby uses `create_tool` with a block.
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
# Python
|
|
70
|
+
@tool(name="add", description="Add numbers", input_schema={...})
|
|
71
|
+
def add(a: int, b: int) -> str:
|
|
72
|
+
return str(a + b)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
```ruby
|
|
76
|
+
# Ruby
|
|
77
|
+
add = ClaudeAgentSDK.create_tool("add", "Add numbers", { a: :number, b: :number }) do |args|
|
|
78
|
+
{ content: [{ type: "text", text: (args[:a] + args[:b]).to_s }] }
|
|
79
|
+
end
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
#### Streaming input
|
|
83
|
+
|
|
84
|
+
Python uses `AsyncIterable`. Ruby uses `Enumerator` or any `#each`-able.
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
# Python
|
|
88
|
+
async def messages():
|
|
89
|
+
yield {"type": "user", "message": {"role": "user", "content": "Hello"}}
|
|
90
|
+
|
|
91
|
+
async for msg in query(prompt=messages(), options=options):
|
|
92
|
+
print(msg)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
```ruby
|
|
96
|
+
# Ruby
|
|
97
|
+
messages = ClaudeAgentSDK::Streaming.from_array(["Hello", "Follow up"])
|
|
98
|
+
ClaudeAgentSDK.query(prompt: messages) { |msg| puts msg }
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
#### Types
|
|
102
|
+
|
|
103
|
+
Python uses `dataclass` with type annotations and `TypedDict`. Ruby uses plain classes with `attr_accessor` and keyword args — no runtime type checking, but the same structure.
|
|
104
|
+
|
|
105
|
+
#### Configuration defaults
|
|
106
|
+
|
|
107
|
+
Python passes options directly. Ruby adds `ClaudeAgentSDK.configure` for global defaults that merge with per-call options — handy for Rails initializers.
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
# Ruby-only: global defaults
|
|
111
|
+
ClaudeAgentSDK.configure do |config|
|
|
112
|
+
config.default_options = { model: "sonnet", permission_mode: "bypassPermissions" }
|
|
113
|
+
end
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
#### Subprocess transport
|
|
117
|
+
|
|
118
|
+
Both SDKs spawn `claude` CLI as a subprocess with stream-JSON over stdin/stdout. Python uses `anyio.open_process`; Ruby uses `Open3.popen3` with a background `Thread` for stderr. The wire protocol is identical.
|
|
119
|
+
|
|
120
|
+
</details>
|
|
121
|
+
|
|
9
122
|
## Table of Contents
|
|
10
123
|
|
|
11
124
|
- [Installation](#installation)
|
|
@@ -23,6 +136,7 @@
|
|
|
23
136
|
- [Tools Configuration](#tools-configuration)
|
|
24
137
|
- [Sandbox Settings](#sandbox-settings)
|
|
25
138
|
- [File Checkpointing & Rewind](#file-checkpointing--rewind)
|
|
139
|
+
- [Session Browsing](#session-browsing)
|
|
26
140
|
- [Rails Integration](#rails-integration)
|
|
27
141
|
- [Types](#types)
|
|
28
142
|
- [Error Handling](#error-handling)
|
|
@@ -39,7 +153,7 @@ Add this line to your application's Gemfile:
|
|
|
39
153
|
gem 'claude-agent-sdk', github: 'ya-luotao/claude-agent-sdk-ruby'
|
|
40
154
|
|
|
41
155
|
# Or use a stable version from RubyGems
|
|
42
|
-
gem 'claude-agent-sdk', '~> 0.
|
|
156
|
+
gem 'claude-agent-sdk', '~> 0.8.0'
|
|
43
157
|
```
|
|
44
158
|
|
|
45
159
|
And then execute:
|
|
@@ -225,13 +339,18 @@ Async do
|
|
|
225
339
|
puts "MCP status: #{status}"
|
|
226
340
|
|
|
227
341
|
# Get server initialization info
|
|
228
|
-
info = client.server_info
|
|
229
|
-
puts "Available commands: #{info}"
|
|
230
|
-
|
|
231
|
-
# (Parity alias) Get server initialization info
|
|
232
342
|
info = client.get_server_info
|
|
233
343
|
puts "Available commands: #{info}"
|
|
234
344
|
|
|
345
|
+
# Reconnect a failed MCP server
|
|
346
|
+
client.reconnect_mcp_server('my-server')
|
|
347
|
+
|
|
348
|
+
# Enable or disable an MCP server
|
|
349
|
+
client.toggle_mcp_server('my-server', false)
|
|
350
|
+
|
|
351
|
+
# Stop a running background task
|
|
352
|
+
client.stop_task('task_abc123')
|
|
353
|
+
|
|
235
354
|
client.disconnect
|
|
236
355
|
end.wait
|
|
237
356
|
```
|
|
@@ -774,6 +893,47 @@ end.wait
|
|
|
774
893
|
|
|
775
894
|
> **Note:** The `uuid` field on `UserMessage` is populated by the CLI and represents checkpoint identifiers. Rewinding to a UUID restores file state to what it was at that point in the conversation.
|
|
776
895
|
|
|
896
|
+
## Session Browsing
|
|
897
|
+
|
|
898
|
+
Browse and inspect previous Claude Code sessions directly from Ruby — no CLI subprocess required.
|
|
899
|
+
|
|
900
|
+
### Listing Sessions
|
|
901
|
+
|
|
902
|
+
```ruby
|
|
903
|
+
# List all sessions (sorted by most recent first)
|
|
904
|
+
sessions = ClaudeAgentSDK.list_sessions
|
|
905
|
+
sessions.each do |session|
|
|
906
|
+
puts "#{session.session_id}: #{session.summary} (#{session.git_branch})"
|
|
907
|
+
end
|
|
908
|
+
|
|
909
|
+
# List sessions for a specific directory
|
|
910
|
+
sessions = ClaudeAgentSDK.list_sessions(directory: '/path/to/project', limit: 10)
|
|
911
|
+
|
|
912
|
+
# Include git worktree sessions
|
|
913
|
+
sessions = ClaudeAgentSDK.list_sessions(directory: '.', include_worktrees: true)
|
|
914
|
+
```
|
|
915
|
+
|
|
916
|
+
Each `SDKSessionInfo` includes:
|
|
917
|
+
- `session_id`, `summary`, `last_modified`, `file_size`
|
|
918
|
+
- `custom_title`, `first_prompt`, `git_branch`, `cwd`
|
|
919
|
+
|
|
920
|
+
### Reading Session Messages
|
|
921
|
+
|
|
922
|
+
```ruby
|
|
923
|
+
# Get the full conversation from a session
|
|
924
|
+
messages = ClaudeAgentSDK.get_session_messages(session_id: 'abc-123-...')
|
|
925
|
+
messages.each do |msg|
|
|
926
|
+
puts "[#{msg.type}] #{msg.message}"
|
|
927
|
+
end
|
|
928
|
+
|
|
929
|
+
# Paginate through messages
|
|
930
|
+
page = ClaudeAgentSDK.get_session_messages(session_id: 'abc-123-...', offset: 10, limit: 20)
|
|
931
|
+
```
|
|
932
|
+
|
|
933
|
+
Each `SessionMessage` includes `type` (`"user"` or `"assistant"`), `uuid`, `session_id`, and `message` (raw API dict).
|
|
934
|
+
|
|
935
|
+
> **Note:** Session browsing reads `~/.claude/projects/` JSONL files directly. It respects the `CLAUDE_CONFIG_DIR` environment variable and automatically detects git worktrees.
|
|
936
|
+
|
|
777
937
|
## Rails Integration
|
|
778
938
|
|
|
779
939
|
The SDK integrates well with Rails applications. Here are common patterns:
|
|
@@ -966,13 +1126,26 @@ end
|
|
|
966
1126
|
|
|
967
1127
|
#### SystemMessage
|
|
968
1128
|
|
|
969
|
-
System message with metadata.
|
|
1129
|
+
System message with metadata. Task lifecycle events are typed subclasses.
|
|
970
1130
|
|
|
971
1131
|
```ruby
|
|
972
1132
|
class SystemMessage
|
|
973
|
-
attr_accessor :subtype, # String ('init', etc.)
|
|
1133
|
+
attr_accessor :subtype, # String ('init', 'task_started', 'task_progress', 'task_notification', etc.)
|
|
974
1134
|
:data # Hash
|
|
975
1135
|
end
|
|
1136
|
+
|
|
1137
|
+
# Typed subclasses (all inherit from SystemMessage, so is_a?(SystemMessage) still works)
|
|
1138
|
+
class TaskStartedMessage < SystemMessage
|
|
1139
|
+
attr_accessor :task_id, :description, :uuid, :session_id, :tool_use_id, :task_type
|
|
1140
|
+
end
|
|
1141
|
+
|
|
1142
|
+
class TaskProgressMessage < SystemMessage
|
|
1143
|
+
attr_accessor :task_id, :description, :usage, :uuid, :session_id, :tool_use_id, :last_tool_name
|
|
1144
|
+
end
|
|
1145
|
+
|
|
1146
|
+
class TaskNotificationMessage < SystemMessage
|
|
1147
|
+
attr_accessor :task_id, :status, :output_file, :summary, :uuid, :session_id, :tool_use_id, :usage
|
|
1148
|
+
end
|
|
976
1149
|
```
|
|
977
1150
|
|
|
978
1151
|
#### ResultMessage
|
|
@@ -987,6 +1160,7 @@ class ResultMessage
|
|
|
987
1160
|
:is_error, # Boolean
|
|
988
1161
|
:num_turns, # Integer
|
|
989
1162
|
:session_id, # String
|
|
1163
|
+
:stop_reason, # String | nil ('end_turn', 'max_tokens', 'stop_sequence')
|
|
990
1164
|
:total_cost_usd, # Float | nil
|
|
991
1165
|
:usage, # Hash | nil
|
|
992
1166
|
:result, # String | nil (final text result)
|
|
@@ -1111,6 +1285,13 @@ end
|
|
|
1111
1285
|
| `McpSSEServerConfig` | MCP server config for SSE transport |
|
|
1112
1286
|
| `McpHttpServerConfig` | MCP server config for HTTP transport |
|
|
1113
1287
|
| `SdkPluginConfig` | SDK plugin configuration |
|
|
1288
|
+
| `McpServerStatus` | Status of a single MCP server connection (with `.parse`) |
|
|
1289
|
+
| `McpStatusResponse` | Response from `get_mcp_status` containing all server statuses (with `.parse`) |
|
|
1290
|
+
| `McpServerInfo` | MCP server name and version |
|
|
1291
|
+
| `McpToolInfo` | MCP tool name, description, and annotations |
|
|
1292
|
+
| `McpToolAnnotations` | MCP tool annotation hints (`read_only`, `destructive`, `open_world`) |
|
|
1293
|
+
| `SDKSessionInfo` | Session metadata from `list_sessions` |
|
|
1294
|
+
| `SessionMessage` | Single message from `get_session_messages` |
|
|
1114
1295
|
| `SandboxSettings` | Sandbox settings for isolated command execution |
|
|
1115
1296
|
| `SandboxNetworkConfig` | Network configuration for sandbox |
|
|
1116
1297
|
| `SandboxIgnoreViolations` | Configure which sandbox violations to ignore |
|
|
@@ -1126,6 +1307,8 @@ end
|
|
|
1126
1307
|
| `SETTING_SOURCES` | Available setting sources |
|
|
1127
1308
|
| `HOOK_EVENTS` | Available hook events |
|
|
1128
1309
|
| `ASSISTANT_MESSAGE_ERRORS` | Possible error types in AssistantMessage |
|
|
1310
|
+
| `TASK_NOTIFICATION_STATUSES` | Task lifecycle notification statuses (`completed`, `failed`, `stopped`) |
|
|
1311
|
+
| `MCP_SERVER_CONNECTION_STATUSES` | MCP server connection states (`connected`, `failed`, `needs-auth`, `pending`, `disabled`) |
|
|
1129
1312
|
|
|
1130
1313
|
## Error Handling
|
|
1131
1314
|
|
|
@@ -66,10 +66,32 @@ module ClaudeAgentSDK
|
|
|
66
66
|
end
|
|
67
67
|
|
|
68
68
|
def self.parse_system_message(data)
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
69
|
+
case data[:subtype]
|
|
70
|
+
when 'task_started'
|
|
71
|
+
TaskStartedMessage.new(
|
|
72
|
+
subtype: data[:subtype], data: data,
|
|
73
|
+
task_id: data[:task_id], description: data[:description],
|
|
74
|
+
uuid: data[:uuid], session_id: data[:session_id],
|
|
75
|
+
tool_use_id: data[:tool_use_id], task_type: data[:task_type]
|
|
76
|
+
)
|
|
77
|
+
when 'task_progress'
|
|
78
|
+
TaskProgressMessage.new(
|
|
79
|
+
subtype: data[:subtype], data: data,
|
|
80
|
+
task_id: data[:task_id], description: data[:description],
|
|
81
|
+
usage: data[:usage], uuid: data[:uuid], session_id: data[:session_id],
|
|
82
|
+
tool_use_id: data[:tool_use_id], last_tool_name: data[:last_tool_name]
|
|
83
|
+
)
|
|
84
|
+
when 'task_notification'
|
|
85
|
+
TaskNotificationMessage.new(
|
|
86
|
+
subtype: data[:subtype], data: data,
|
|
87
|
+
task_id: data[:task_id], status: data[:status],
|
|
88
|
+
output_file: data[:output_file], summary: data[:summary],
|
|
89
|
+
uuid: data[:uuid], session_id: data[:session_id],
|
|
90
|
+
tool_use_id: data[:tool_use_id], usage: data[:usage]
|
|
91
|
+
)
|
|
92
|
+
else
|
|
93
|
+
SystemMessage.new(subtype: data[:subtype], data: data)
|
|
94
|
+
end
|
|
73
95
|
end
|
|
74
96
|
|
|
75
97
|
def self.parse_result_message(data)
|
|
@@ -80,10 +102,11 @@ module ClaudeAgentSDK
|
|
|
80
102
|
is_error: data[:is_error],
|
|
81
103
|
num_turns: data[:num_turns],
|
|
82
104
|
session_id: data[:session_id],
|
|
105
|
+
stop_reason: data[:stop_reason],
|
|
83
106
|
total_cost_usd: data[:total_cost_usd],
|
|
84
107
|
usage: data[:usage],
|
|
85
108
|
result: data[:result],
|
|
86
|
-
structured_output: data[:structured_output]
|
|
109
|
+
structured_output: data[:structured_output]
|
|
87
110
|
)
|
|
88
111
|
end
|
|
89
112
|
|
|
@@ -22,6 +22,8 @@ module ClaudeAgentSDK
|
|
|
22
22
|
|
|
23
23
|
CONTROL_REQUEST_TIMEOUT_ENV_VAR = 'CLAUDE_AGENT_SDK_CONTROL_REQUEST_TIMEOUT_SECONDS'
|
|
24
24
|
DEFAULT_CONTROL_REQUEST_TIMEOUT_SECONDS = 1200.0
|
|
25
|
+
STREAM_CLOSE_TIMEOUT_ENV_VAR = 'CLAUDE_CODE_STREAM_CLOSE_TIMEOUT'
|
|
26
|
+
DEFAULT_STREAM_CLOSE_TIMEOUT_SECONDS = 60.0
|
|
25
27
|
|
|
26
28
|
def initialize(transport:, is_streaming_mode:, can_use_tool: nil, hooks: nil, sdk_mcp_servers: nil, agents: nil)
|
|
27
29
|
@transport = transport
|
|
@@ -42,6 +44,8 @@ module ClaudeAgentSDK
|
|
|
42
44
|
|
|
43
45
|
# Message stream
|
|
44
46
|
@message_queue = Async::Queue.new
|
|
47
|
+
@first_result_received = false
|
|
48
|
+
@first_result_condition = Async::Condition.new
|
|
45
49
|
@task = nil
|
|
46
50
|
@initialized = false
|
|
47
51
|
@closed = false
|
|
@@ -124,6 +128,16 @@ module ClaudeAgentSDK
|
|
|
124
128
|
DEFAULT_CONTROL_REQUEST_TIMEOUT_SECONDS
|
|
125
129
|
end
|
|
126
130
|
|
|
131
|
+
def stream_close_timeout_seconds
|
|
132
|
+
raw_value = ENV.fetch(STREAM_CLOSE_TIMEOUT_ENV_VAR, nil)
|
|
133
|
+
return DEFAULT_STREAM_CLOSE_TIMEOUT_SECONDS if raw_value.nil? || raw_value.strip.empty?
|
|
134
|
+
|
|
135
|
+
value = Float(raw_value) / 1000.0
|
|
136
|
+
value.positive? ? value : DEFAULT_STREAM_CLOSE_TIMEOUT_SECONDS
|
|
137
|
+
rescue ArgumentError
|
|
138
|
+
DEFAULT_STREAM_CLOSE_TIMEOUT_SECONDS
|
|
139
|
+
end
|
|
140
|
+
|
|
127
141
|
def read_messages
|
|
128
142
|
@transport.read_messages do |message|
|
|
129
143
|
break if @closed
|
|
@@ -150,6 +164,10 @@ module ClaudeAgentSDK
|
|
|
150
164
|
task&.stop
|
|
151
165
|
next
|
|
152
166
|
else
|
|
167
|
+
if message[:type] == 'result' && !@first_result_received
|
|
168
|
+
@first_result_received = true
|
|
169
|
+
@first_result_condition.signal
|
|
170
|
+
end
|
|
153
171
|
# Regular SDK messages go to the queue
|
|
154
172
|
@message_queue.enqueue(message)
|
|
155
173
|
end
|
|
@@ -164,6 +182,10 @@ module ClaudeAgentSDK
|
|
|
164
182
|
# Put error in queue so iterators can handle it
|
|
165
183
|
@message_queue.enqueue({ type: 'error', error: e })
|
|
166
184
|
ensure
|
|
185
|
+
unless @first_result_received
|
|
186
|
+
@first_result_received = true
|
|
187
|
+
@first_result_condition.signal
|
|
188
|
+
end
|
|
167
189
|
# Always signal end of stream
|
|
168
190
|
@message_queue.enqueue({ type: 'end' })
|
|
169
191
|
end
|
|
@@ -308,7 +330,13 @@ module ClaudeAgentSDK
|
|
|
308
330
|
|
|
309
331
|
def parse_hook_input(input_data)
|
|
310
332
|
event_name = input_data[:hook_event_name] || input_data['hook_event_name']
|
|
311
|
-
fetch =
|
|
333
|
+
fetch = lambda do |key|
|
|
334
|
+
if input_data.key?(key)
|
|
335
|
+
input_data[key]
|
|
336
|
+
elsif input_data.key?(key.to_s)
|
|
337
|
+
input_data[key.to_s]
|
|
338
|
+
end
|
|
339
|
+
end
|
|
312
340
|
base_args = {
|
|
313
341
|
session_id: fetch.call(:session_id),
|
|
314
342
|
transcript_path: fetch.call(:transcript_path),
|
|
@@ -316,13 +344,19 @@ module ClaudeAgentSDK
|
|
|
316
344
|
permission_mode: fetch.call(:permission_mode)
|
|
317
345
|
}
|
|
318
346
|
|
|
347
|
+
# Subagent context fields shared by tool-lifecycle hooks
|
|
348
|
+
subagent_args = {
|
|
349
|
+
agent_id: fetch.call(:agent_id),
|
|
350
|
+
agent_type: fetch.call(:agent_type)
|
|
351
|
+
}
|
|
352
|
+
|
|
319
353
|
case event_name
|
|
320
354
|
when 'PreToolUse'
|
|
321
355
|
PreToolUseHookInput.new(
|
|
322
356
|
tool_name: fetch.call(:tool_name),
|
|
323
357
|
tool_input: fetch.call(:tool_input),
|
|
324
358
|
tool_use_id: fetch.call(:tool_use_id),
|
|
325
|
-
**base_args
|
|
359
|
+
**subagent_args, **base_args
|
|
326
360
|
)
|
|
327
361
|
when 'PostToolUse'
|
|
328
362
|
PostToolUseHookInput.new(
|
|
@@ -330,7 +364,7 @@ module ClaudeAgentSDK
|
|
|
330
364
|
tool_input: fetch.call(:tool_input),
|
|
331
365
|
tool_response: fetch.call(:tool_response),
|
|
332
366
|
tool_use_id: fetch.call(:tool_use_id),
|
|
333
|
-
**base_args
|
|
367
|
+
**subagent_args, **base_args
|
|
334
368
|
)
|
|
335
369
|
when 'PostToolUseFailure'
|
|
336
370
|
PostToolUseFailureHookInput.new(
|
|
@@ -339,7 +373,7 @@ module ClaudeAgentSDK
|
|
|
339
373
|
tool_use_id: fetch.call(:tool_use_id),
|
|
340
374
|
error: fetch.call(:error),
|
|
341
375
|
is_interrupt: fetch.call(:is_interrupt),
|
|
342
|
-
**base_args
|
|
376
|
+
**subagent_args, **base_args
|
|
343
377
|
)
|
|
344
378
|
when 'UserPromptSubmit'
|
|
345
379
|
UserPromptSubmitHookInput.new(
|
|
@@ -377,7 +411,7 @@ module ClaudeAgentSDK
|
|
|
377
411
|
tool_name: fetch.call(:tool_name),
|
|
378
412
|
tool_input: fetch.call(:tool_input),
|
|
379
413
|
permission_suggestions: fetch.call(:permission_suggestions),
|
|
380
|
-
**base_args
|
|
414
|
+
**subagent_args, **base_args
|
|
381
415
|
)
|
|
382
416
|
when 'PreCompact'
|
|
383
417
|
PreCompactHookInput.new(
|
|
@@ -662,6 +696,35 @@ module ClaudeAgentSDK
|
|
|
662
696
|
})
|
|
663
697
|
end
|
|
664
698
|
|
|
699
|
+
# Reconnect a failed MCP server
|
|
700
|
+
# @param server_name [String] Name of the MCP server to reconnect
|
|
701
|
+
def reconnect_mcp_server(server_name)
|
|
702
|
+
send_control_request({
|
|
703
|
+
subtype: 'mcp_reconnect',
|
|
704
|
+
serverName: server_name
|
|
705
|
+
})
|
|
706
|
+
end
|
|
707
|
+
|
|
708
|
+
# Enable or disable an MCP server
|
|
709
|
+
# @param server_name [String] Name of the MCP server
|
|
710
|
+
# @param enabled [Boolean] Whether to enable or disable
|
|
711
|
+
def toggle_mcp_server(server_name, enabled)
|
|
712
|
+
send_control_request({
|
|
713
|
+
subtype: 'mcp_toggle',
|
|
714
|
+
serverName: server_name,
|
|
715
|
+
enabled: enabled
|
|
716
|
+
})
|
|
717
|
+
end
|
|
718
|
+
|
|
719
|
+
# Stop a running background task
|
|
720
|
+
# @param task_id [String] The ID of the task to stop
|
|
721
|
+
def stop_task(task_id)
|
|
722
|
+
send_control_request({
|
|
723
|
+
subtype: 'stop_task',
|
|
724
|
+
task_id: task_id
|
|
725
|
+
})
|
|
726
|
+
end
|
|
727
|
+
|
|
665
728
|
# Rewind files to a previous checkpoint (v0.1.15+)
|
|
666
729
|
# Restores file state to what it was at the given user message
|
|
667
730
|
# Requires enable_file_checkpointing to be true in options
|
|
@@ -669,20 +732,42 @@ module ClaudeAgentSDK
|
|
|
669
732
|
def rewind_files(user_message_uuid)
|
|
670
733
|
send_control_request({
|
|
671
734
|
subtype: 'rewind_files',
|
|
672
|
-
|
|
735
|
+
user_message_id: user_message_uuid
|
|
673
736
|
})
|
|
674
737
|
end
|
|
675
738
|
|
|
739
|
+
# Wait for the first result before closing stdin when hooks or SDK MCP
|
|
740
|
+
# servers may still need to exchange control messages with the CLI.
|
|
741
|
+
def wait_for_result_and_end_input
|
|
742
|
+
if !@first_result_received &&
|
|
743
|
+
((@sdk_mcp_servers && !@sdk_mcp_servers.empty?) || (@hooks && !@hooks.empty?))
|
|
744
|
+
Async::Task.current.with_timeout(stream_close_timeout_seconds) do
|
|
745
|
+
@first_result_condition.wait unless @first_result_received
|
|
746
|
+
end
|
|
747
|
+
end
|
|
748
|
+
rescue Async::TimeoutError
|
|
749
|
+
nil
|
|
750
|
+
ensure
|
|
751
|
+
@transport.end_input
|
|
752
|
+
end
|
|
753
|
+
|
|
676
754
|
# Stream input messages to transport
|
|
677
755
|
def stream_input(stream)
|
|
678
756
|
stream.each do |message|
|
|
679
757
|
break if @closed
|
|
680
|
-
|
|
758
|
+
serialized = if message.is_a?(Hash)
|
|
759
|
+
JSON.generate(message) + "\n"
|
|
760
|
+
else
|
|
761
|
+
message.to_s
|
|
762
|
+
end
|
|
763
|
+
serialized += "\n" unless serialized.end_with?("\n")
|
|
764
|
+
@transport.write(serialized)
|
|
681
765
|
end
|
|
682
|
-
@transport.end_input
|
|
683
766
|
rescue StandardError => e
|
|
684
767
|
# Log error but don't raise
|
|
685
768
|
warn "Error streaming input: #{e.message}"
|
|
769
|
+
ensure
|
|
770
|
+
wait_for_result_and_end_input
|
|
686
771
|
end
|
|
687
772
|
|
|
688
773
|
# Receive SDK messages (not control messages)
|