claude-agent-sdk 0.2.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +34 -0
- data/README.md +516 -91
- data/lib/claude_agent_sdk/message_parser.rb +7 -4
- data/lib/claude_agent_sdk/query.rb +91 -4
- data/lib/claude_agent_sdk/subprocess_cli_transport.rb +106 -2
- data/lib/claude_agent_sdk/types.rb +397 -10
- data/lib/claude_agent_sdk/version.rb +1 -1
- data/lib/claude_agent_sdk.rb +9 -0
- metadata +2 -2
|
@@ -32,6 +32,7 @@ module ClaudeAgentSDK
|
|
|
32
32
|
|
|
33
33
|
def self.parse_user_message(data)
|
|
34
34
|
parent_tool_use_id = data[:parent_tool_use_id]
|
|
35
|
+
uuid = data[:uuid] # UUID for rewind support
|
|
35
36
|
message_data = data[:message]
|
|
36
37
|
raise MessageParseError.new("Missing message field in user message", data: data) unless message_data
|
|
37
38
|
|
|
@@ -40,9 +41,9 @@ module ClaudeAgentSDK
|
|
|
40
41
|
|
|
41
42
|
if content.is_a?(Array)
|
|
42
43
|
content_blocks = content.map { |block| parse_content_block(block) }
|
|
43
|
-
UserMessage.new(content: content_blocks, parent_tool_use_id: parent_tool_use_id)
|
|
44
|
+
UserMessage.new(content: content_blocks, uuid: uuid, parent_tool_use_id: parent_tool_use_id)
|
|
44
45
|
else
|
|
45
|
-
UserMessage.new(content: content, parent_tool_use_id: parent_tool_use_id)
|
|
46
|
+
UserMessage.new(content: content, uuid: uuid, parent_tool_use_id: parent_tool_use_id)
|
|
46
47
|
end
|
|
47
48
|
end
|
|
48
49
|
|
|
@@ -54,7 +55,8 @@ module ClaudeAgentSDK
|
|
|
54
55
|
AssistantMessage.new(
|
|
55
56
|
content: content_blocks,
|
|
56
57
|
model: data.dig(:message, :model),
|
|
57
|
-
parent_tool_use_id: data[:parent_tool_use_id]
|
|
58
|
+
parent_tool_use_id: data[:parent_tool_use_id],
|
|
59
|
+
error: data[:error] # authentication_failed, billing_error, rate_limit, invalid_request, server_error, unknown
|
|
58
60
|
)
|
|
59
61
|
end
|
|
60
62
|
|
|
@@ -75,7 +77,8 @@ module ClaudeAgentSDK
|
|
|
75
77
|
session_id: data[:session_id],
|
|
76
78
|
total_cost_usd: data[:total_cost_usd],
|
|
77
79
|
usage: data[:usage],
|
|
78
|
-
result: data[:result]
|
|
80
|
+
result: data[:result],
|
|
81
|
+
structured_output: data[:structured_output] # Structured output when output_format is specified
|
|
79
82
|
)
|
|
80
83
|
end
|
|
81
84
|
|
|
@@ -217,16 +217,73 @@ module ClaudeAgentSDK
|
|
|
217
217
|
callback = @hook_callbacks[callback_id]
|
|
218
218
|
raise "No hook callback found for ID: #{callback_id}" unless callback
|
|
219
219
|
|
|
220
|
+
# Parse input data into typed HookInput object
|
|
221
|
+
input_data = request_data[:input] || {}
|
|
222
|
+
hook_input = parse_hook_input(input_data)
|
|
223
|
+
|
|
224
|
+
# Create typed HookContext
|
|
225
|
+
context = HookContext.new(signal: nil)
|
|
226
|
+
|
|
220
227
|
hook_output = callback.call(
|
|
221
|
-
|
|
228
|
+
hook_input,
|
|
222
229
|
request_data[:tool_use_id],
|
|
223
|
-
|
|
230
|
+
context
|
|
224
231
|
)
|
|
225
232
|
|
|
226
233
|
# Convert Ruby-safe field names to CLI-expected names
|
|
227
234
|
convert_hook_output_for_cli(hook_output)
|
|
228
235
|
end
|
|
229
236
|
|
|
237
|
+
def parse_hook_input(input_data)
|
|
238
|
+
event_name = input_data[:hook_event_name] || input_data['hook_event_name']
|
|
239
|
+
base_args = {
|
|
240
|
+
session_id: input_data[:session_id],
|
|
241
|
+
transcript_path: input_data[:transcript_path],
|
|
242
|
+
cwd: input_data[:cwd],
|
|
243
|
+
permission_mode: input_data[:permission_mode]
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
case event_name
|
|
247
|
+
when 'PreToolUse'
|
|
248
|
+
PreToolUseHookInput.new(
|
|
249
|
+
tool_name: input_data[:tool_name],
|
|
250
|
+
tool_input: input_data[:tool_input],
|
|
251
|
+
**base_args
|
|
252
|
+
)
|
|
253
|
+
when 'PostToolUse'
|
|
254
|
+
PostToolUseHookInput.new(
|
|
255
|
+
tool_name: input_data[:tool_name],
|
|
256
|
+
tool_input: input_data[:tool_input],
|
|
257
|
+
tool_response: input_data[:tool_response],
|
|
258
|
+
**base_args
|
|
259
|
+
)
|
|
260
|
+
when 'UserPromptSubmit'
|
|
261
|
+
UserPromptSubmitHookInput.new(
|
|
262
|
+
prompt: input_data[:prompt],
|
|
263
|
+
**base_args
|
|
264
|
+
)
|
|
265
|
+
when 'Stop'
|
|
266
|
+
StopHookInput.new(
|
|
267
|
+
stop_hook_active: input_data[:stop_hook_active],
|
|
268
|
+
**base_args
|
|
269
|
+
)
|
|
270
|
+
when 'SubagentStop'
|
|
271
|
+
SubagentStopHookInput.new(
|
|
272
|
+
stop_hook_active: input_data[:stop_hook_active],
|
|
273
|
+
**base_args
|
|
274
|
+
)
|
|
275
|
+
when 'PreCompact'
|
|
276
|
+
PreCompactHookInput.new(
|
|
277
|
+
trigger: input_data[:trigger],
|
|
278
|
+
custom_instructions: input_data[:custom_instructions],
|
|
279
|
+
**base_args
|
|
280
|
+
)
|
|
281
|
+
else
|
|
282
|
+
# Return base input for unknown event types
|
|
283
|
+
BaseHookInput.new(**base_args)
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
|
|
230
287
|
def handle_mcp_message(request_data)
|
|
231
288
|
server_name = request_data[:server_name]
|
|
232
289
|
mcp_message = request_data[:message]
|
|
@@ -238,6 +295,13 @@ module ClaudeAgentSDK
|
|
|
238
295
|
end
|
|
239
296
|
|
|
240
297
|
def convert_hook_output_for_cli(hook_output)
|
|
298
|
+
# Handle typed output objects
|
|
299
|
+
if hook_output.respond_to?(:to_h) && !hook_output.is_a?(Hash)
|
|
300
|
+
return hook_output.to_h
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
return {} unless hook_output.is_a?(Hash)
|
|
304
|
+
|
|
241
305
|
# Convert Ruby hash with symbol keys to CLI format
|
|
242
306
|
# Handle special keywords that might be Ruby-safe versions
|
|
243
307
|
converted = {}
|
|
@@ -245,9 +309,21 @@ module ClaudeAgentSDK
|
|
|
245
309
|
converted_key = case key
|
|
246
310
|
when :async_, 'async_' then 'async'
|
|
247
311
|
when :continue_, 'continue_' then 'continue'
|
|
248
|
-
|
|
312
|
+
when :hook_specific_output then 'hookSpecificOutput'
|
|
313
|
+
when :suppress_output then 'suppressOutput'
|
|
314
|
+
when :stop_reason then 'stopReason'
|
|
315
|
+
when :system_message then 'systemMessage'
|
|
316
|
+
when :async_timeout then 'asyncTimeout'
|
|
317
|
+
else key.to_s
|
|
249
318
|
end
|
|
250
|
-
|
|
319
|
+
|
|
320
|
+
# Recursively convert nested objects
|
|
321
|
+
converted_value = if value.respond_to?(:to_h) && !value.is_a?(Hash)
|
|
322
|
+
value.to_h
|
|
323
|
+
else
|
|
324
|
+
value
|
|
325
|
+
end
|
|
326
|
+
converted[converted_key] = converted_value
|
|
251
327
|
end
|
|
252
328
|
converted
|
|
253
329
|
end
|
|
@@ -475,6 +551,17 @@ module ClaudeAgentSDK
|
|
|
475
551
|
})
|
|
476
552
|
end
|
|
477
553
|
|
|
554
|
+
# Rewind files to a previous checkpoint (v0.1.15+)
|
|
555
|
+
# Restores file state to what it was at the given user message
|
|
556
|
+
# Requires enable_file_checkpointing to be true in options
|
|
557
|
+
# @param user_message_uuid [String] The UUID of the UserMessage to rewind to
|
|
558
|
+
def rewind_files(user_message_uuid)
|
|
559
|
+
send_control_request({
|
|
560
|
+
subtype: 'rewind_files',
|
|
561
|
+
userMessageUuid: user_message_uuid
|
|
562
|
+
})
|
|
563
|
+
end
|
|
564
|
+
|
|
478
565
|
# Stream input messages to transport
|
|
479
566
|
def stream_input(stream)
|
|
480
567
|
stream.each do |message|
|
|
@@ -75,11 +75,97 @@ module ClaudeAgentSDK
|
|
|
75
75
|
cmd.concat(['--max-turns', @options.max_turns.to_s]) if @options.max_turns
|
|
76
76
|
cmd.concat(['--disallowedTools', @options.disallowed_tools.join(',')]) unless @options.disallowed_tools.empty?
|
|
77
77
|
cmd.concat(['--model', @options.model]) if @options.model
|
|
78
|
+
cmd.concat(['--fallback-model', @options.fallback_model]) if @options.fallback_model
|
|
78
79
|
cmd.concat(['--permission-prompt-tool', @options.permission_prompt_tool_name]) if @options.permission_prompt_tool_name
|
|
79
80
|
cmd.concat(['--permission-mode', @options.permission_mode]) if @options.permission_mode
|
|
80
81
|
cmd << '--continue' if @options.continue_conversation
|
|
81
82
|
cmd.concat(['--resume', @options.resume]) if @options.resume
|
|
82
|
-
|
|
83
|
+
|
|
84
|
+
# Settings handling with sandbox merge
|
|
85
|
+
# Sandbox settings are merged into the main settings JSON
|
|
86
|
+
if @options.settings || @options.sandbox
|
|
87
|
+
settings_hash = {}
|
|
88
|
+
settings_is_path = false
|
|
89
|
+
|
|
90
|
+
# Parse existing settings if provided
|
|
91
|
+
if @options.settings
|
|
92
|
+
if @options.settings.is_a?(String)
|
|
93
|
+
begin
|
|
94
|
+
settings_hash = JSON.parse(@options.settings)
|
|
95
|
+
rescue JSON::ParserError
|
|
96
|
+
# If not valid JSON, treat as file path and pass as-is
|
|
97
|
+
settings_is_path = true
|
|
98
|
+
cmd.concat(['--settings', @options.settings])
|
|
99
|
+
if @options.sandbox
|
|
100
|
+
warn "Warning: Cannot merge sandbox settings when settings is a file path. " \
|
|
101
|
+
"Sandbox settings will be ignored. Use a Hash or JSON string for settings " \
|
|
102
|
+
"to enable sandbox merging."
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
elsif @options.settings.is_a?(Hash)
|
|
106
|
+
settings_hash = @options.settings.dup
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Merge sandbox settings if provided (only when settings is not a file path)
|
|
111
|
+
if !settings_is_path && @options.sandbox
|
|
112
|
+
sandbox_hash = if @options.sandbox.is_a?(SandboxSettings)
|
|
113
|
+
@options.sandbox.to_h
|
|
114
|
+
else
|
|
115
|
+
@options.sandbox
|
|
116
|
+
end
|
|
117
|
+
settings_hash[:sandbox] = sandbox_hash unless sandbox_hash.empty?
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Output merged settings (only when settings is not a file path)
|
|
121
|
+
if !settings_is_path && !settings_hash.empty?
|
|
122
|
+
cmd.concat(['--settings', JSON.generate(settings_hash)])
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Budget limit option
|
|
127
|
+
cmd.concat(['--max-budget-usd', @options.max_budget_usd.to_s]) if @options.max_budget_usd
|
|
128
|
+
# Note: max_thinking_tokens is stored in options but not yet supported by Claude CLI
|
|
129
|
+
|
|
130
|
+
# Betas option for enabling experimental features
|
|
131
|
+
if @options.betas && !@options.betas.empty?
|
|
132
|
+
cmd.concat(['--betas', @options.betas.join(',')])
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Tools option for base tools selection
|
|
136
|
+
if @options.tools
|
|
137
|
+
if @options.tools.is_a?(Array)
|
|
138
|
+
cmd.concat(['--tools', @options.tools.join(',')])
|
|
139
|
+
elsif @options.tools.is_a?(ToolsPreset)
|
|
140
|
+
cmd.concat(['--tools', JSON.generate(@options.tools.to_h)])
|
|
141
|
+
elsif @options.tools.is_a?(Hash)
|
|
142
|
+
cmd.concat(['--tools', JSON.generate(@options.tools)])
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Append allowed tools option
|
|
147
|
+
if @options.append_allowed_tools && !@options.append_allowed_tools.empty?
|
|
148
|
+
cmd.concat(['--append-allowed-tools', @options.append_allowed_tools.join(',')])
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# File checkpointing for rewind support
|
|
152
|
+
cmd << '--enable-file-checkpointing' if @options.enable_file_checkpointing
|
|
153
|
+
|
|
154
|
+
# JSON schema for structured output
|
|
155
|
+
# Accepts either:
|
|
156
|
+
# 1. Direct schema: { type: 'object', properties: {...} }
|
|
157
|
+
# 2. Wrapped format: { type: 'json_schema', schema: {...} }
|
|
158
|
+
if @options.output_format
|
|
159
|
+
schema = if @options.output_format.is_a?(Hash) && @options.output_format[:type] == 'json_schema'
|
|
160
|
+
@options.output_format[:schema]
|
|
161
|
+
elsif @options.output_format.is_a?(Hash) && @options.output_format['type'] == 'json_schema'
|
|
162
|
+
@options.output_format['schema']
|
|
163
|
+
else
|
|
164
|
+
@options.output_format
|
|
165
|
+
end
|
|
166
|
+
schema_json = schema.is_a?(String) ? schema : JSON.generate(schema)
|
|
167
|
+
cmd.concat(['--json-schema', schema_json])
|
|
168
|
+
end
|
|
83
169
|
|
|
84
170
|
# Add directories
|
|
85
171
|
@options.add_dirs.each do |dir|
|
|
@@ -121,6 +207,14 @@ module ClaudeAgentSDK
|
|
|
121
207
|
cmd.concat(['--agents', JSON.generate(agents_dict)])
|
|
122
208
|
end
|
|
123
209
|
|
|
210
|
+
# Plugins
|
|
211
|
+
if @options.plugins && !@options.plugins.empty?
|
|
212
|
+
plugins_config = @options.plugins.map do |plugin|
|
|
213
|
+
plugin.is_a?(SdkPluginConfig) ? plugin.to_h : plugin
|
|
214
|
+
end
|
|
215
|
+
cmd.concat(['--plugins', JSON.generate(plugins_config)])
|
|
216
|
+
end
|
|
217
|
+
|
|
124
218
|
# Setting sources
|
|
125
219
|
sources_value = @options.setting_sources ? @options.setting_sources.join(',') : ''
|
|
126
220
|
cmd.concat(['--setting-sources', sources_value])
|
|
@@ -159,7 +253,7 @@ module ClaudeAgentSDK
|
|
|
159
253
|
process_env['PWD'] = @cwd.to_s if @cwd
|
|
160
254
|
|
|
161
255
|
# Determine stderr handling
|
|
162
|
-
should_pipe_stderr = @options.stderr || @options.extra_args.key?('debug-to-stderr')
|
|
256
|
+
should_pipe_stderr = @options.stderr || @options.debug_stderr || @options.extra_args.key?('debug-to-stderr')
|
|
163
257
|
|
|
164
258
|
begin
|
|
165
259
|
# Start process using Open3
|
|
@@ -205,7 +299,17 @@ module ClaudeAgentSDK
|
|
|
205
299
|
line_str = line.chomp
|
|
206
300
|
next if line_str.empty?
|
|
207
301
|
|
|
302
|
+
# Call stderr callback if provided
|
|
208
303
|
@options.stderr&.call(line_str)
|
|
304
|
+
|
|
305
|
+
# Write to debug_stderr file/IO if provided
|
|
306
|
+
if @options.debug_stderr
|
|
307
|
+
if @options.debug_stderr.respond_to?(:puts)
|
|
308
|
+
@options.debug_stderr.puts(line_str)
|
|
309
|
+
elsif @options.debug_stderr.is_a?(String)
|
|
310
|
+
File.open(@options.debug_stderr, 'a') { |f| f.puts(line_str) }
|
|
311
|
+
end
|
|
312
|
+
end
|
|
209
313
|
end
|
|
210
314
|
rescue StandardError
|
|
211
315
|
# Ignore errors during stderr reading
|