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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d17d03fa867e2779de155d6fba8a86fbc9edbc5662157ca3e21606016c81fb17
4
- data.tar.gz: 949787994cd58eb5be72b8834a519a704697c61ecbfff9ca1d6f4e1fe921b18f
3
+ metadata.gz: b625dde6e1777c93be6e4bcfb6c9565087d798d414a03078f8de4184c39dfa7f
4
+ data.tar.gz: 30117f8eac89f94aabae5c37fdc12c012ed779a0f1c58062d70e9b727ee5ce3c
5
5
  SHA512:
6
- metadata.gz: a99fdb7b2dcab7e0286763abfb1f6d0277423a7ef2198a040ef8c76a8959907009ebc21f6523c35ecff1409a66fd8c636648d1f594aaa481119ed3598eb1b06e
7
- data.tar.gz: 8e3e7e26487dcfe993534d3d1b7f695ba5009ad18e9d173c9e1e0f1353ec54565905f3e74e81a39eb5789aa2df008536a2a5a191095e7d8604f36abd27bb6583
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
  [![Gem Version](https://badge.fury.io/rb/claude-agent-sdk.svg?icon=si%3Arubygems)](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.7.3'
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
- SystemMessage.new(
70
- subtype: data[:subtype],
71
- data: data
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] # Structured output when output_format is specified
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 = ->(key) { input_data[key] || input_data[key.to_s] }
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
- userMessageUuid: user_message_uuid
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
- @transport.write(JSON.generate(message) + "\n")
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)