claude-agent-sdk 0.16.4 → 0.16.6

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: 98b00605830d86d1dc5e632ef9887574b255d2ace8fdc815451a53983ca0ea2e
4
- data.tar.gz: d28cb3256fa5944f966e21afa753d082559a6d537d527dfc2abb5aaa962a450b
3
+ metadata.gz: 5e77eea013fead986f47ff000864ddb9e52a3408186cf09bf3bb8d5214edec28
4
+ data.tar.gz: 49f94edca00072ec403cb26eb79165979d8684e655336bce6ca53f3c1a31eb81
5
5
  SHA512:
6
- metadata.gz: 807595177836563a86f0dc72a3a70b297860d07f76d19c90d70f97dafae7a1bdd69a52583a8c005172148f6742b59fbd80e78f79a3d10c47ad33ba24d7ade89c
7
- data.tar.gz: af94cf3bbd037da6b1a7998a41438f10227d731657c2050e4a0f1d57551e682d4dd9dcfef9d98c67f6ead150472804468093cc226622392b8b3ecdb25cae4d18
6
+ metadata.gz: '08544dd09bb2d8c1127364517e1cc3527aaa0321d8c2d957550f078c5ce1d20796f5622f987ddd9f128377cad697d314a63a31031cd8eeb2e823bbfef282ee17'
7
+ data.tar.gz: e7149a4d3d9546631449d71878a6480d3d8758c2d990b3836cdc1dde65e93d09a8d19658c781c4f2cf5655bcb3c910dac84bc7d0076f635af4957f78a2e91fe9
data/CHANGELOG.md CHANGED
@@ -7,6 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.16.6] - 2026-04-29
11
+
12
+ ### Added
13
+ - Type classes now accept hash construction with mixed key shapes — symbols or strings, snake_case or camelCase — and support bracket access (`obj[:field]`, `obj['field']`). `UserMessage.new({"sessionId" => "abc"})` works the same as `UserMessage.new(session_id: "abc")`. `Type.from_hash(nil)` and `Type.wrap(nil)` are nil-safe.
14
+ - `ClaudeAgentOptions.new(nil)` is accepted and yields an options object populated with configured defaults.
15
+
16
+ ### Changed
17
+ - Discriminator fields on type classes (`type` on `SystemPromptFile`/`McpStdioServerConfig`/etc., `behavior` on `PermissionResultAllow`/`Deny`, `hook_event_name` on every hook input/output) are now `attr_reader`-only, set authoritatively in `initialize`. They cannot be overwritten externally.
18
+ - `ClaudeAgentOptions` continues to raise `ArgumentError` on unknown keys (constructor, `dup_with`, and `[]=`); other type subclasses silently drop unknown keys for forward-compatibility with newer CLI output.
19
+
20
+ ### Internal
21
+ - Unified ~50 type classes (messages, hook inputs/outputs, MCP configs, system prompt configs, sandbox settings) under a shared `Type` base class. `lib/claude_agent_sdk/types.rb` shrank from 1510 to 588 lines; `MessageParser`'s system-message dispatch went from 215 lines to a 14-entry lookup table.
22
+
23
+ ## [0.16.5] - 2026-04-24
24
+
25
+ ### Added
26
+ - `display:` option on `ThinkingConfigAdaptive` and `ThinkingConfigEnabled`, forwarded to the CLI as `--thinking-display <summarized|omitted>`. Opus 4.7 defaults thinking display to `"omitted"` (empty `thinking` field, signature only), so pass `ThinkingConfigAdaptive.new(display: "summarized")` to receive plaintext summarized thinking text. Invalid values raise `ArgumentError` at construction. See [adaptive thinking docs](https://docs.claude.com/en/docs/build-with-claude/adaptive-thinking).
27
+
28
+ ### Internal
29
+ - Extracted private `writeln`/`write` helpers in `Client` and `Query` to consolidate the `@transport.write(json + "\n")` pattern across five call sites. Pure refactor; same bytes on the wire.
30
+
10
31
  ## [0.16.4] - 2026-04-23
11
32
 
12
33
  ### Fixed
data/README.md CHANGED
@@ -108,7 +108,7 @@ Add this line to your application's Gemfile:
108
108
  gem 'claude-agent-sdk', github: 'ya-luotao/claude-agent-sdk-ruby'
109
109
 
110
110
  # Or use a stable version from RubyGems
111
- gem 'claude-agent-sdk', '~> 0.16.4'
111
+ gem 'claude-agent-sdk', '~> 0.16.6'
112
112
  ```
113
113
 
114
114
  And then execute:
@@ -158,8 +158,10 @@ module ClaudeAgentSDK
158
158
  case @options.thinking
159
159
  when ThinkingConfigAdaptive
160
160
  cmd.push("--thinking", "adaptive")
161
+ append_thinking_display(cmd, @options.thinking.display)
161
162
  when ThinkingConfigEnabled
162
163
  cmd.push("--max-thinking-tokens", @options.thinking.budget_tokens.to_s)
164
+ append_thinking_display(cmd, @options.thinking.display)
163
165
  when ThinkingConfigDisabled
164
166
  cmd.push("--thinking", "disabled")
165
167
  end
@@ -168,6 +170,15 @@ module ClaudeAgentSDK
168
170
  end
169
171
  end
170
172
 
173
+ # `--thinking-display` toggles between `"summarized"` (visible thinking
174
+ # text) and `"omitted"` (empty thinking, signature only). Opus 4.7 defaults
175
+ # to `"omitted"`, so pass `display: "summarized"` to see reasoning.
176
+ def append_thinking_display(cmd, display)
177
+ return if display.nil?
178
+
179
+ cmd.push("--thinking-display", display.to_s)
180
+ end
181
+
171
182
  # The set of supported levels is model-dependent; the CLI falls back to
172
183
  # the highest supported level at or below the one requested
173
184
  # (e.g. `xhigh` → `high` on Opus 4.6).
@@ -78,214 +78,57 @@ module ClaudeAgentSDK
78
78
  )
79
79
  end
80
80
 
81
+ # Typed SystemMessage subclasses inherit from `Type` and accept the raw
82
+ # CLI hash directly — camelCase and snake_case keys are normalized by the
83
+ # base class, and the full hash is captured as `#data`.
84
+ SYSTEM_MESSAGE_CLASSES = {
85
+ 'init' => InitMessage,
86
+ 'compact_boundary' => CompactBoundaryMessage,
87
+ 'status' => StatusMessage,
88
+ 'api_retry' => APIRetryMessage,
89
+ 'local_command_output' => LocalCommandOutputMessage,
90
+ 'hook_started' => HookStartedMessage,
91
+ 'hook_progress' => HookProgressMessage,
92
+ 'hook_response' => HookResponseMessage,
93
+ 'session_state_changed' => SessionStateChangedMessage,
94
+ 'files_persisted' => FilesPersistedMessage,
95
+ 'elicitation_complete' => ElicitationCompleteMessage,
96
+ 'task_started' => TaskStartedMessage,
97
+ 'task_progress' => TaskProgressMessage,
98
+ 'task_notification' => TaskNotificationMessage
99
+ }.freeze
100
+
81
101
  def self.parse_system_message(data)
82
- case data[:subtype]
83
- when 'init'
84
- InitMessage.new(
85
- subtype: data[:subtype], data: data,
86
- uuid: data[:uuid], session_id: data[:session_id],
87
- agents: data[:agents], api_key_source: data[:apiKeySource] || data[:api_key_source],
88
- betas: data[:betas], claude_code_version: data[:claude_code_version],
89
- cwd: data[:cwd], tools: data[:tools], mcp_servers: data[:mcp_servers],
90
- model: data[:model], permission_mode: data[:permissionMode] || data[:permission_mode],
91
- slash_commands: data[:slash_commands], output_style: data[:output_style],
92
- skills: data[:skills], plugins: data[:plugins],
93
- fast_mode_state: data[:fastModeState] || data[:fast_mode_state]
94
- )
95
- when 'compact_boundary'
96
- raw_metadata = data[:compact_metadata]
97
- CompactBoundaryMessage.new(
98
- subtype: data[:subtype], data: data,
99
- uuid: data[:uuid], session_id: data[:session_id],
100
- compact_metadata: CompactMetadata.from_hash(raw_metadata)
101
- )
102
- when 'status'
103
- StatusMessage.new(
104
- subtype: data[:subtype], data: data,
105
- uuid: data[:uuid], session_id: data[:session_id],
106
- status: data[:status],
107
- permission_mode: data[:permissionMode] || data[:permission_mode]
108
- )
109
- when 'api_retry'
110
- APIRetryMessage.new(
111
- subtype: data[:subtype], data: data,
112
- uuid: data[:uuid], session_id: data[:session_id],
113
- attempt: data[:attempt], max_retries: data[:maxRetries] || data[:max_retries],
114
- retry_delay_ms: data[:retryDelayMs] || data[:retry_delay_ms],
115
- error_status: data[:errorStatus] || data[:error_status],
116
- error: data[:error]
117
- )
118
- when 'local_command_output'
119
- LocalCommandOutputMessage.new(
120
- subtype: data[:subtype], data: data,
121
- uuid: data[:uuid], session_id: data[:session_id],
122
- content: data[:content]
123
- )
124
- when 'hook_started'
125
- HookStartedMessage.new(
126
- subtype: data[:subtype], data: data,
127
- uuid: data[:uuid], session_id: data[:session_id],
128
- hook_id: data[:hookId] || data[:hook_id],
129
- hook_name: data[:hookName] || data[:hook_name],
130
- hook_event: data[:hookEvent] || data[:hook_event]
131
- )
132
- when 'hook_progress'
133
- HookProgressMessage.new(
134
- subtype: data[:subtype], data: data,
135
- uuid: data[:uuid], session_id: data[:session_id],
136
- hook_id: data[:hookId] || data[:hook_id],
137
- hook_name: data[:hookName] || data[:hook_name],
138
- hook_event: data[:hookEvent] || data[:hook_event],
139
- stdout: data[:stdout], stderr: data[:stderr], output: data[:output]
140
- )
141
- when 'hook_response'
142
- HookResponseMessage.new(
143
- subtype: data[:subtype], data: data,
144
- uuid: data[:uuid], session_id: data[:session_id],
145
- hook_id: data[:hookId] || data[:hook_id],
146
- hook_name: data[:hookName] || data[:hook_name],
147
- hook_event: data[:hookEvent] || data[:hook_event],
148
- output: data[:output], stdout: data[:stdout], stderr: data[:stderr],
149
- exit_code: data[:exitCode] || data[:exit_code],
150
- outcome: data[:outcome]
151
- )
152
- when 'session_state_changed'
153
- SessionStateChangedMessage.new(
154
- subtype: data[:subtype], data: data,
155
- uuid: data[:uuid], session_id: data[:session_id],
156
- state: data[:state]
157
- )
158
- when 'files_persisted'
159
- FilesPersistedMessage.new(
160
- subtype: data[:subtype], data: data,
161
- uuid: data[:uuid], session_id: data[:session_id],
162
- files: data[:files], failed: data[:failed],
163
- processed_at: data[:processedAt] || data[:processed_at]
164
- )
165
- when 'elicitation_complete'
166
- ElicitationCompleteMessage.new(
167
- subtype: data[:subtype], data: data,
168
- uuid: data[:uuid], session_id: data[:session_id],
169
- mcp_server_name: data[:mcpServerName] || data[:mcp_server_name],
170
- elicitation_id: data[:elicitationId] || data[:elicitation_id]
171
- )
172
- when 'task_started'
173
- TaskStartedMessage.new(
174
- subtype: data[:subtype], data: data,
175
- task_id: data[:task_id], description: data[:description],
176
- uuid: data[:uuid], session_id: data[:session_id],
177
- tool_use_id: data[:tool_use_id], task_type: data[:task_type],
178
- workflow_name: data[:workflowName] || data[:workflow_name],
179
- prompt: data[:prompt]
180
- )
181
- when 'task_progress'
182
- TaskProgressMessage.new(
183
- subtype: data[:subtype], data: data,
184
- task_id: data[:task_id], description: data[:description],
185
- usage: data[:usage], uuid: data[:uuid], session_id: data[:session_id],
186
- tool_use_id: data[:tool_use_id], last_tool_name: data[:last_tool_name],
187
- summary: data[:summary]
188
- )
189
- when 'task_notification'
190
- TaskNotificationMessage.new(
191
- subtype: data[:subtype], data: data,
192
- task_id: data[:task_id], status: data[:status],
193
- output_file: data[:output_file], summary: data[:summary],
194
- uuid: data[:uuid], session_id: data[:session_id],
195
- tool_use_id: data[:tool_use_id], usage: data[:usage]
196
- )
197
- else
198
- SystemMessage.new(subtype: data[:subtype], data: data)
199
- end
102
+ klass = SYSTEM_MESSAGE_CLASSES[data[:subtype]] || SystemMessage
103
+ klass.new(data)
200
104
  end
201
105
 
202
106
  def self.parse_result_message(data)
203
- ResultMessage.new(
204
- subtype: data[:subtype],
205
- duration_ms: data[:duration_ms],
206
- duration_api_ms: data[:duration_api_ms],
207
- is_error: data[:is_error],
208
- num_turns: data[:num_turns],
209
- session_id: data[:session_id],
210
- stop_reason: data[:stop_reason],
211
- total_cost_usd: data[:total_cost_usd],
212
- usage: data[:usage],
213
- result: data[:result],
214
- structured_output: data[:structured_output],
215
- model_usage: data[:modelUsage] || data[:model_usage],
216
- permission_denials: data[:permission_denials],
217
- errors: data[:errors],
218
- uuid: data[:uuid],
219
- fast_mode_state: data[:fastModeState] || data[:fast_mode_state]
220
- )
107
+ ResultMessage.new(data)
221
108
  end
222
109
 
223
110
  def self.parse_stream_event(data)
224
- StreamEvent.new(
225
- uuid: data[:uuid],
226
- session_id: data[:session_id],
227
- event: data[:event],
228
- parent_tool_use_id: data[:parent_tool_use_id]
229
- )
111
+ StreamEvent.new(data)
230
112
  end
231
113
 
232
114
  def self.parse_rate_limit_event(data)
233
- info = data[:rate_limit_info] || {}
234
- rate_limit_info = RateLimitInfo.new(
235
- status: info[:status],
236
- resets_at: info[:resetsAt],
237
- rate_limit_type: info[:rateLimitType],
238
- utilization: info[:utilization],
239
- overage_status: info[:overageStatus],
240
- overage_resets_at: info[:overageResetsAt],
241
- overage_disabled_reason: info[:overageDisabledReason],
242
- raw: info
243
- )
244
- RateLimitEvent.new(
245
- rate_limit_info: rate_limit_info,
246
- uuid: data[:uuid],
247
- session_id: data[:session_id],
248
- raw_data: data
249
- )
115
+ RateLimitEvent.new(data.merge(raw_data: data))
250
116
  end
251
117
 
252
118
  def self.parse_tool_progress_message(data)
253
- ToolProgressMessage.new(
254
- uuid: data[:uuid],
255
- session_id: data[:session_id],
256
- tool_use_id: data[:toolUseId] || data[:tool_use_id],
257
- tool_name: data[:toolName] || data[:tool_name],
258
- parent_tool_use_id: data[:parentToolUseId] || data[:parent_tool_use_id],
259
- elapsed_time_seconds: data[:elapsedTimeSeconds] || data[:elapsed_time_seconds],
260
- task_id: data[:taskId] || data[:task_id]
261
- )
119
+ ToolProgressMessage.new(data)
262
120
  end
263
121
 
264
122
  def self.parse_auth_status_message(data)
265
- AuthStatusMessage.new(
266
- uuid: data[:uuid],
267
- session_id: data[:session_id],
268
- is_authenticating: data[:isAuthenticating] || data[:is_authenticating],
269
- output: data[:output],
270
- error: data[:error]
271
- )
123
+ AuthStatusMessage.new(data)
272
124
  end
273
125
 
274
126
  def self.parse_tool_use_summary_message(data)
275
- ToolUseSummaryMessage.new(
276
- uuid: data[:uuid],
277
- session_id: data[:session_id],
278
- summary: data[:summary],
279
- preceding_tool_use_ids: data[:precedingToolUseIds] || data[:preceding_tool_use_ids]
280
- )
127
+ ToolUseSummaryMessage.new(data)
281
128
  end
282
129
 
283
130
  def self.parse_prompt_suggestion_message(data)
284
- PromptSuggestionMessage.new(
285
- uuid: data[:uuid],
286
- session_id: data[:session_id],
287
- suggestion: data[:suggestion]
288
- )
131
+ PromptSuggestionMessage.new(data)
289
132
  end
290
133
 
291
134
  # Accepts blocks with either symbol or string keys — live CLI messages
@@ -261,7 +261,7 @@ module ClaudeAgentSDK
261
261
  response: response_data
262
262
  }
263
263
  }
264
- @transport.write(JSON.generate(success_response) + "\n")
264
+ writeln(JSON.generate(success_response))
265
265
  rescue Async::Stop
266
266
  # Cancellation requested; respond with an error so the CLI can unblock.
267
267
  cancelled_response = {
@@ -273,7 +273,7 @@ module ClaudeAgentSDK
273
273
  error: 'Cancelled'
274
274
  }
275
275
  }
276
- @transport.write(JSON.generate(cancelled_response) + "\n")
276
+ writeln(JSON.generate(cancelled_response))
277
277
  rescue StandardError => e
278
278
  # Send error response
279
279
  error_response = {
@@ -285,7 +285,7 @@ module ClaudeAgentSDK
285
285
  error: e.message
286
286
  }
287
287
  }
288
- @transport.write(JSON.generate(error_response) + "\n")
288
+ writeln(JSON.generate(error_response))
289
289
  end
290
290
 
291
291
  def handle_permission_request(request_data)
@@ -642,7 +642,7 @@ module ClaudeAgentSDK
642
642
  request: request
643
643
  }
644
644
 
645
- @transport.write(JSON.generate(control_request) + "\n")
645
+ writeln(JSON.generate(control_request))
646
646
 
647
647
  # Wait for response with timeout (default 1200s to handle slow CLI startup)
648
648
  Async do |task|
@@ -913,13 +913,8 @@ module ClaudeAgentSDK
913
913
  def stream_input(stream)
914
914
  stream.each do |message|
915
915
  break if @closed
916
- serialized = if message.is_a?(Hash)
917
- JSON.generate(message) + "\n"
918
- else
919
- message.to_s
920
- end
921
- serialized += "\n" unless serialized.end_with?("\n")
922
- @transport.write(serialized)
916
+ serialized = message.is_a?(Hash) ? JSON.generate(message) : message.to_s
917
+ writeln(serialized)
923
918
  end
924
919
  rescue StandardError => e
925
920
  # Log error but don't raise
@@ -928,6 +923,14 @@ module ClaudeAgentSDK
928
923
  wait_for_result_and_end_input
929
924
  end
930
925
 
926
+ def writeln(string)
927
+ write string.end_with?("\n") ? string : "#{string}\n"
928
+ end
929
+
930
+ def write(string)
931
+ @transport.write(string)
932
+ end
933
+
931
934
  # Receive SDK messages (not control messages)
932
935
  def receive_messages(&block)
933
936
  return enum_for(:receive_messages) unless block