claude-agent-sdk 0.7.2 → 0.8.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1bb953f4036b0f107f4afe0bfb8d89281bb3d496cf15f89a6c5607e7b3b5e210
4
- data.tar.gz: 84081e6b7aa1ebbe580b0e900f4142829278c3a458dc706807e4d141a0af70ff
3
+ metadata.gz: 2c723c3e660d135ab87311e27be6a80b8d13127f9e42789a66b1d30ec0395278
4
+ data.tar.gz: 0652f808dd792a6bef8db32afc567567fe9f016abdf095f1d13ae0ac07af0d03
5
5
  SHA512:
6
- metadata.gz: eda57b17c43f2efa673e31cba432db7022af999e8c8de82065bc4e21c13325a07480cc632004226847d383881916605218332b129f0524d2b3a1c0b291dee3d1
7
- data.tar.gz: e5e9f25bb361ecaaf84a9972e55d94ed1eb18e5ae50c7abc8831f9a3a01c36e510835a8d253f71c0ba0df7f88de04ca024125ac4627a4274c84629e56ba3f57c
6
+ metadata.gz: 399925ff55a9a23ab54cbe97adb18fea804be6c92cf21aa3ca3fdcd4cbe54693dfc8b77b85f15ed2eaf1517acaecd09935701d0248fb99a23be6325d151c99aa
7
+ data.tar.gz: dd14802c444f4d6609cbb8215e79c3f5d8c4d9453e20d0d3af40cd853d653625c93e117082118beee2c5fb0ad29447a170687a44d56f88be6785affb8d118060
data/CHANGELOG.md CHANGED
@@ -5,11 +5,70 @@ 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.0] - 2026-03-05
9
+
10
+ Port of Python SDK v0.1.46 features.
11
+
12
+ ### Added
13
+
14
+ #### Task Message Types
15
+ - `TaskStartedMessage`, `TaskProgressMessage`, `TaskNotificationMessage` — typed `SystemMessage` subclasses for background task lifecycle events
16
+ - `TASK_NOTIFICATION_STATUSES` constant (`completed`, `failed`, `stopped`)
17
+ - `MessageParser` dispatches on `subtype` within `system` messages, falling back to generic `SystemMessage` for unknown subtypes
18
+
19
+ #### MCP Server Control
20
+ - `reconnect_mcp_server(server_name)` on `Query` and `Client` — retry failed MCP server connections
21
+ - `toggle_mcp_server(server_name, enabled)` on `Query` and `Client` — enable/disable MCP servers live
22
+ - `stop_task(task_id)` on `Query` and `Client` — stop a running background task
23
+
24
+ #### Subagent Context on Hook Inputs
25
+ - `agent_id` and `agent_type` attributes on `PreToolUseHookInput`, `PostToolUseHookInput`, `PostToolUseFailureHookInput`, `PermissionRequestHookInput`
26
+ - Populated when hooks fire inside subagents, allowing attribution of tool calls to specific agents
27
+
28
+ #### Result Message
29
+ - `stop_reason` attribute on `ResultMessage` (e.g., `'end_turn'`, `'max_tokens'`, `'stop_sequence'`)
30
+
31
+ #### Typed MCP Status Response
32
+ - `McpServerInfo`, `McpToolAnnotations`, `McpToolInfo`, `McpServerStatus`, `McpStatusResponse` types
33
+ - `.parse` class methods for hydrating from raw CLI response hashes
34
+ - `MCP_SERVER_CONNECTION_STATUSES` constant (`connected`, `failed`, `needs-auth`, `pending`, `disabled`)
35
+
36
+ #### Session Browsing
37
+ - `ClaudeAgentSDK.list_sessions(directory:, limit:, include_worktrees:)` — list sessions from `~/.claude/projects/` JSONL files
38
+ - `ClaudeAgentSDK.get_session_messages(session_id:, directory:, limit:, offset:)` — reconstruct conversation chain from session transcript
39
+ - `SDKSessionInfo` type with `session_id`, `summary`, `last_modified`, `file_size`, `custom_title`, `first_prompt`, `git_branch`, `cwd`
40
+ - `SessionMessage` type with `type`, `uuid`, `session_id`, `message`
41
+ - Pure filesystem operations — no CLI subprocess required
42
+ - Git worktree-aware session scanning
43
+ - `parentUuid` chain walking with cycle detection for robust conversation reconstruction
44
+
45
+ ### Fixed
46
+ - **`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.
47
+
48
+ ## [0.7.3] - 2026-02-26
49
+
50
+ ### Fixed
51
+ - **String-keyed JSON schema crash:** Libraries like [RubyLLM](https://github.com/crmne/ruby_llm) that deep-stringify schema keys (e.g., `{ 'type' => 'object', 'properties' => { ... } }`) were misidentified as simple type-mapping schemas, causing each top-level key to be treated as a parameter name instead of passing the schema through. Now both symbol-keyed and string-keyed schemas are detected and normalized correctly. (PR #9 by [@iuhoay](https://github.com/iuhoay))
52
+ - **Shallow key symbolization:** `convert_schema` used `transform_keys` (shallow) which left nested property keys as strings, breaking downstream `MCP::Tool::InputSchema` construction. Now uses deep symbolization recursively.
53
+ - **Guard ordering crash:** `convert_schema` and `convert_input_schema` accessed `schema[:type]` before the `schema.is_a?(Hash)` guard, which would raise `NoMethodError` on `nil` input.
54
+ - **Schema detection tightened:** Pre-built schema detection now requires `type == 'object'` and `properties.is_a?(Hash)`, preventing false positives when a simple schema happens to have parameters named `type` and `properties`.
55
+
56
+ ### Added
57
+ - `ClaudeAgentSDK.deep_symbolize_keys` utility method for recursive hash key symbolization
58
+
8
59
  ## [0.7.2] - 2026-02-21
9
60
 
10
61
  ### Fixed
62
+ - **Unknown content block crash:** Unrecognized content block types (e.g., `document` blocks from PDF reading) now return `UnknownBlock` instead of raising `MessageParseError`, aligning with the Python SDK's forward-compatible design
63
+ - **Unknown message type crash:** Unrecognized message types now return `nil` (skipped by callers) instead of raising
11
64
  - **Empty input schema crash:** Tools with no parameters (`input_schema: {}`) caused `MCP::Tool::InputSchema` validation failure (`required` array must have at least 1 item per JSON Schema draft-04). Now omits `required` when empty.
12
- - **RuboCop offense:** Removed redundant `else` clause in `MessageParser.parse`
65
+
66
+ ### Added
67
+ - `UnknownBlock` type that preserves raw data for unrecognized content block types
68
+
69
+ ### Changed
70
+ - **Breaking (minor):** `MessageParser.parse` no longer raises `MessageParseError` for unknown message types — returns `nil` instead. If you were rescuing `MessageParseError` to handle unknown types, check for `nil` return values instead.
71
+ - **Breaking (minor):** `MessageParser.parse_content_block` no longer raises `MessageParseError` for unknown content block types — returns `UnknownBlock` instead. Content block iteration using `is_a?` filtering (e.g., `block.is_a?(TextBlock)`) is unaffected.
13
72
 
14
73
  ## [0.7.1] - 2026-02-21
15
74
 
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.2'
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
  ```
@@ -282,6 +401,26 @@ Async do
282
401
  end.wait
283
402
  ```
284
403
 
404
+ ### Pre-built JSON Schemas
405
+
406
+ If your schemas come from another library (e.g., [RubyLLM](https://github.com/crmne/ruby_llm)) that deep-stringifies keys, the SDK handles them transparently — both symbol-keyed and string-keyed schemas are accepted and normalized:
407
+
408
+ ```ruby
409
+ # Symbol keys (standard Ruby)
410
+ tool = ClaudeAgentSDK.create_tool('save', 'Save a fact', {
411
+ type: 'object',
412
+ properties: { fact: { type: 'string' } },
413
+ required: ['fact']
414
+ }) { |args| { content: [{ type: 'text', text: "Saved: #{args[:fact]}" }] } }
415
+
416
+ # String keys (e.g., from RubyLLM or JSON.parse)
417
+ tool = ClaudeAgentSDK.create_tool('save', 'Save a fact', {
418
+ 'type' => 'object',
419
+ 'properties' => { 'fact' => { 'type' => 'string' } },
420
+ 'required' => ['fact']
421
+ }) { |args| { content: [{ type: 'text', text: "Saved: #{args[:fact]}" }] } }
422
+ ```
423
+
285
424
  ### Benefits Over External MCP Servers
286
425
 
287
426
  - **No subprocess management** - Runs in the same process as your application
@@ -754,6 +893,47 @@ end.wait
754
893
 
755
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.
756
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
+
757
937
  ## Rails Integration
758
938
 
759
939
  The SDK integrates well with Rails applications. Here are common patterns:
@@ -946,13 +1126,26 @@ end
946
1126
 
947
1127
  #### SystemMessage
948
1128
 
949
- System message with metadata.
1129
+ System message with metadata. Task lifecycle events are typed subclasses.
950
1130
 
951
1131
  ```ruby
952
1132
  class SystemMessage
953
- attr_accessor :subtype, # String ('init', etc.)
1133
+ attr_accessor :subtype, # String ('init', 'task_started', 'task_progress', 'task_notification', etc.)
954
1134
  :data # Hash
955
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
956
1149
  ```
957
1150
 
958
1151
  #### ResultMessage
@@ -967,6 +1160,7 @@ class ResultMessage
967
1160
  :is_error, # Boolean
968
1161
  :num_turns, # Integer
969
1162
  :session_id, # String
1163
+ :stop_reason, # String | nil ('end_turn', 'max_tokens', 'stop_sequence')
970
1164
  :total_cost_usd, # Float | nil
971
1165
  :usage, # Hash | nil
972
1166
  :result, # String | nil (final text result)
@@ -978,7 +1172,7 @@ end
978
1172
 
979
1173
  ```ruby
980
1174
  # Union type of all content blocks
981
- ContentBlock = TextBlock | ThinkingBlock | ToolUseBlock | ToolResultBlock
1175
+ ContentBlock = TextBlock | ThinkingBlock | ToolUseBlock | ToolResultBlock | UnknownBlock
982
1176
  ```
983
1177
 
984
1178
  #### TextBlock
@@ -1026,6 +1220,17 @@ class ToolResultBlock
1026
1220
  end
1027
1221
  ```
1028
1222
 
1223
+ #### UnknownBlock
1224
+
1225
+ Generic content block for types the SDK doesn't explicitly handle (e.g., `document` for PDFs, `image` for inline images). Preserves the raw data for forward compatibility with newer CLI versions.
1226
+
1227
+ ```ruby
1228
+ class UnknownBlock
1229
+ attr_accessor :type, # String — the original block type (e.g., "document")
1230
+ :data # Hash — the full raw block hash
1231
+ end
1232
+ ```
1233
+
1029
1234
  ### Error Types
1030
1235
 
1031
1236
  ```ruby
@@ -1080,6 +1285,13 @@ end
1080
1285
  | `McpSSEServerConfig` | MCP server config for SSE transport |
1081
1286
  | `McpHttpServerConfig` | MCP server config for HTTP transport |
1082
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` |
1083
1295
  | `SandboxSettings` | Sandbox settings for isolated command execution |
1084
1296
  | `SandboxNetworkConfig` | Network configuration for sandbox |
1085
1297
  | `SandboxIgnoreViolations` | Configure which sandbox violations to ignore |
@@ -1095,6 +1307,8 @@ end
1095
1307
  | `SETTING_SOURCES` | Available setting sources |
1096
1308
  | `HOOK_EVENTS` | Available hook events |
1097
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`) |
1098
1312
 
1099
1313
  ## Error Handling
1100
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
 
@@ -316,13 +316,19 @@ module ClaudeAgentSDK
316
316
  permission_mode: fetch.call(:permission_mode)
317
317
  }
318
318
 
319
+ # Subagent context fields shared by tool-lifecycle hooks
320
+ subagent_args = {
321
+ agent_id: fetch.call(:agent_id),
322
+ agent_type: fetch.call(:agent_type)
323
+ }
324
+
319
325
  case event_name
320
326
  when 'PreToolUse'
321
327
  PreToolUseHookInput.new(
322
328
  tool_name: fetch.call(:tool_name),
323
329
  tool_input: fetch.call(:tool_input),
324
330
  tool_use_id: fetch.call(:tool_use_id),
325
- **base_args
331
+ **subagent_args, **base_args
326
332
  )
327
333
  when 'PostToolUse'
328
334
  PostToolUseHookInput.new(
@@ -330,7 +336,7 @@ module ClaudeAgentSDK
330
336
  tool_input: fetch.call(:tool_input),
331
337
  tool_response: fetch.call(:tool_response),
332
338
  tool_use_id: fetch.call(:tool_use_id),
333
- **base_args
339
+ **subagent_args, **base_args
334
340
  )
335
341
  when 'PostToolUseFailure'
336
342
  PostToolUseFailureHookInput.new(
@@ -339,7 +345,7 @@ module ClaudeAgentSDK
339
345
  tool_use_id: fetch.call(:tool_use_id),
340
346
  error: fetch.call(:error),
341
347
  is_interrupt: fetch.call(:is_interrupt),
342
- **base_args
348
+ **subagent_args, **base_args
343
349
  )
344
350
  when 'UserPromptSubmit'
345
351
  UserPromptSubmitHookInput.new(
@@ -377,7 +383,7 @@ module ClaudeAgentSDK
377
383
  tool_name: fetch.call(:tool_name),
378
384
  tool_input: fetch.call(:tool_input),
379
385
  permission_suggestions: fetch.call(:permission_suggestions),
380
- **base_args
386
+ **subagent_args, **base_args
381
387
  )
382
388
  when 'PreCompact'
383
389
  PreCompactHookInput.new(
@@ -662,6 +668,35 @@ module ClaudeAgentSDK
662
668
  })
663
669
  end
664
670
 
671
+ # Reconnect a failed MCP server
672
+ # @param server_name [String] Name of the MCP server to reconnect
673
+ def reconnect_mcp_server(server_name)
674
+ send_control_request({
675
+ subtype: 'mcp_reconnect',
676
+ serverName: server_name
677
+ })
678
+ end
679
+
680
+ # Enable or disable an MCP server
681
+ # @param server_name [String] Name of the MCP server
682
+ # @param enabled [Boolean] Whether to enable or disable
683
+ def toggle_mcp_server(server_name, enabled)
684
+ send_control_request({
685
+ subtype: 'mcp_toggle',
686
+ serverName: server_name,
687
+ enabled: enabled
688
+ })
689
+ end
690
+
691
+ # Stop a running background task
692
+ # @param task_id [String] The ID of the task to stop
693
+ def stop_task(task_id)
694
+ send_control_request({
695
+ subtype: 'stop_task',
696
+ task_id: task_id
697
+ })
698
+ end
699
+
665
700
  # Rewind files to a previous checkpoint (v0.1.15+)
666
701
  # Restores file state to what it was at the given user message
667
702
  # Requires enable_file_checkpointing to be true in options
@@ -3,6 +3,15 @@
3
3
  require 'mcp'
4
4
 
5
5
  module ClaudeAgentSDK
6
+ # Recursively convert all hash keys to symbols
7
+ def self.deep_symbolize_keys(obj)
8
+ case obj
9
+ when Hash then obj.transform_keys(&:to_sym).transform_values { |v| deep_symbolize_keys(v) }
10
+ when Array then obj.map { |v| deep_symbolize_keys(v) }
11
+ else obj
12
+ end
13
+ end
14
+
6
15
  # SDK MCP Server - wraps official MCP::Server with block-based API
7
16
  #
8
17
  # Unlike external MCP servers that run as separate processes, SDK MCP servers
@@ -191,9 +200,12 @@ module ClaudeAgentSDK
191
200
  private
192
201
 
193
202
  def convert_schema(schema)
194
- # If it's already a proper JSON schema, return it
195
- if schema.is_a?(Hash) && schema[:type] && schema[:properties]
196
- return schema
203
+ # If it's already a proper JSON schema (symbol or string keys), normalize
204
+ # to symbol keys so downstream code (schema[:properties]) works uniformly.
205
+ if schema.is_a?(Hash)
206
+ type_val = schema[:type] || schema['type']
207
+ props_val = schema[:properties] || schema['properties']
208
+ return ClaudeAgentSDK.deep_symbolize_keys(schema) if type_val == 'object' && props_val.is_a?(Hash)
197
209
  end
198
210
 
199
211
  # Simple schema: hash mapping parameter names to types
@@ -318,9 +330,12 @@ module ClaudeAgentSDK
318
330
  end
319
331
 
320
332
  def convert_input_schema(schema)
321
- # If it's already a proper JSON schema, return it
322
- if schema.is_a?(Hash) && schema[:type] && schema[:properties]
323
- return schema
333
+ # If it's already a proper JSON schema (symbol or string keys), normalize
334
+ # to symbol keys for consistent output.
335
+ if schema.is_a?(Hash)
336
+ type_val = schema[:type] || schema['type']
337
+ props_val = schema[:properties] || schema['properties']
338
+ return ClaudeAgentSDK.deep_symbolize_keys(schema) if type_val == 'object' && props_val.is_a?(Hash)
324
339
  end
325
340
 
326
341
  # Simple schema: hash mapping parameter names to types