claude_agent 0.1.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 +7 -0
- data/.claude/commands/spec/complete.md +105 -0
- data/.claude/commands/spec/update.md +95 -0
- data/.claude/rules/conventions.md +622 -0
- data/.claude/rules/git.md +86 -0
- data/.claude/rules/pull-requests.md +31 -0
- data/.claude/rules/releases.md +177 -0
- data/.claude/rules/testing.md +267 -0
- data/.claude/settings.json +49 -0
- data/CHANGELOG.md +13 -0
- data/CLAUDE.md +94 -0
- data/LICENSE.txt +21 -0
- data/README.md +679 -0
- data/Rakefile +63 -0
- data/SPEC.md +558 -0
- data/lib/claude_agent/abort_controller.rb +113 -0
- data/lib/claude_agent/client.rb +298 -0
- data/lib/claude_agent/content_blocks.rb +163 -0
- data/lib/claude_agent/control_protocol.rb +717 -0
- data/lib/claude_agent/errors.rb +103 -0
- data/lib/claude_agent/hooks.rb +228 -0
- data/lib/claude_agent/mcp/server.rb +166 -0
- data/lib/claude_agent/mcp/tool.rb +137 -0
- data/lib/claude_agent/message_parser.rb +262 -0
- data/lib/claude_agent/messages.rb +421 -0
- data/lib/claude_agent/options.rb +264 -0
- data/lib/claude_agent/permissions.rb +164 -0
- data/lib/claude_agent/query.rb +90 -0
- data/lib/claude_agent/sandbox_settings.rb +139 -0
- data/lib/claude_agent/spawn.rb +235 -0
- data/lib/claude_agent/transport/base.rb +61 -0
- data/lib/claude_agent/transport/subprocess.rb +432 -0
- data/lib/claude_agent/types.rb +193 -0
- data/lib/claude_agent/version.rb +5 -0
- data/lib/claude_agent.rb +28 -0
- data/sig/claude_agent.rbs +912 -0
- data/sig/manifest.yaml +5 -0
- metadata +97 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ClaudeAgent
|
|
4
|
+
# Controller for aborting operations (TypeScript SDK parity)
|
|
5
|
+
#
|
|
6
|
+
# Provides a Ruby-idiomatic way to cancel ongoing SDK operations.
|
|
7
|
+
# Similar to JavaScript's AbortController pattern.
|
|
8
|
+
#
|
|
9
|
+
# @example Basic usage
|
|
10
|
+
# controller = AbortController.new
|
|
11
|
+
#
|
|
12
|
+
# Thread.new { sleep(5); controller.abort("Timeout") }
|
|
13
|
+
#
|
|
14
|
+
# ClaudeAgent.query(
|
|
15
|
+
# prompt: "Long running task",
|
|
16
|
+
# options: Options.new(abort_controller: controller)
|
|
17
|
+
# )
|
|
18
|
+
#
|
|
19
|
+
# @example With abort reason
|
|
20
|
+
# controller.abort("User cancelled")
|
|
21
|
+
# controller.signal.aborted? # => true
|
|
22
|
+
# controller.signal.reason # => "User cancelled"
|
|
23
|
+
#
|
|
24
|
+
class AbortController
|
|
25
|
+
attr_reader :signal
|
|
26
|
+
|
|
27
|
+
def initialize
|
|
28
|
+
@signal = AbortSignal.new
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Abort the operation
|
|
32
|
+
# @param reason [String, nil] Reason for aborting
|
|
33
|
+
# @return [void]
|
|
34
|
+
def abort(reason = nil)
|
|
35
|
+
@signal.abort!(reason)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Signal object that tracks abort state (TypeScript SDK parity)
|
|
40
|
+
#
|
|
41
|
+
# Thread-safe signal that can be checked by multiple consumers
|
|
42
|
+
# and triggers callbacks when aborted.
|
|
43
|
+
#
|
|
44
|
+
class AbortSignal
|
|
45
|
+
def initialize
|
|
46
|
+
@aborted = false
|
|
47
|
+
@reason = nil
|
|
48
|
+
@mutex = Mutex.new
|
|
49
|
+
@condition = ConditionVariable.new
|
|
50
|
+
@callbacks = []
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Check if signal has been aborted
|
|
54
|
+
# @return [Boolean]
|
|
55
|
+
def aborted?
|
|
56
|
+
@mutex.synchronize { @aborted }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Get the abort reason
|
|
60
|
+
# @return [String, nil]
|
|
61
|
+
def reason
|
|
62
|
+
@mutex.synchronize { @reason }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Register a callback for when abort is triggered
|
|
66
|
+
# @yield [reason] Called when abort occurs
|
|
67
|
+
# @return [void]
|
|
68
|
+
def on_abort(&block)
|
|
69
|
+
@mutex.synchronize do
|
|
70
|
+
if @aborted
|
|
71
|
+
block.call(@reason)
|
|
72
|
+
else
|
|
73
|
+
@callbacks << block
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Wait until aborted (with optional timeout)
|
|
79
|
+
# @param timeout [Numeric, nil] Timeout in seconds
|
|
80
|
+
# @return [Boolean] True if aborted, false if timed out
|
|
81
|
+
def wait(timeout: nil)
|
|
82
|
+
@mutex.synchronize do
|
|
83
|
+
return true if @aborted
|
|
84
|
+
|
|
85
|
+
@condition.wait(@mutex, timeout)
|
|
86
|
+
@aborted
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Raise AbortError if aborted (for checking in loops)
|
|
91
|
+
# @raise [AbortError] If signal has been aborted
|
|
92
|
+
def check!
|
|
93
|
+
raise AbortError, reason if aborted?
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# @api private
|
|
97
|
+
# Trigger the abort
|
|
98
|
+
# @param reason [String, nil] Reason for aborting
|
|
99
|
+
def abort!(reason = nil)
|
|
100
|
+
callbacks_to_call = []
|
|
101
|
+
@mutex.synchronize do
|
|
102
|
+
return if @aborted
|
|
103
|
+
|
|
104
|
+
@aborted = true
|
|
105
|
+
@reason = reason || "Operation was aborted"
|
|
106
|
+
callbacks_to_call = @callbacks.dup
|
|
107
|
+
@callbacks.clear
|
|
108
|
+
@condition.broadcast
|
|
109
|
+
end
|
|
110
|
+
callbacks_to_call.each { |cb| cb.call(@reason) }
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ClaudeAgent
|
|
4
|
+
# Interactive, bidirectional client for Claude Code CLI
|
|
5
|
+
#
|
|
6
|
+
# Unlike {ClaudeAgent.query}, the Client provides:
|
|
7
|
+
# - Multiple conversation turns
|
|
8
|
+
# - Streaming responses
|
|
9
|
+
# - Ability to interrupt operations
|
|
10
|
+
# - Dynamic permission and model changes
|
|
11
|
+
# - File checkpointing and rewind
|
|
12
|
+
#
|
|
13
|
+
# @example Basic usage
|
|
14
|
+
# client = ClaudeAgent::Client.new
|
|
15
|
+
# client.connect
|
|
16
|
+
# client.send_message("Hello!")
|
|
17
|
+
# client.receive_response.each { |msg| puts msg }
|
|
18
|
+
# client.disconnect
|
|
19
|
+
#
|
|
20
|
+
# @example With block (auto-disconnect)
|
|
21
|
+
# ClaudeAgent::Client.open do |client|
|
|
22
|
+
# client.send_message("Help me write a function")
|
|
23
|
+
# client.receive_response.each { |msg| puts msg }
|
|
24
|
+
#
|
|
25
|
+
# client.send_message("Now add tests")
|
|
26
|
+
# client.receive_response.each { |msg| puts msg }
|
|
27
|
+
# end
|
|
28
|
+
#
|
|
29
|
+
# @example With initial prompt
|
|
30
|
+
# ClaudeAgent::Client.open(prompt: "You are a helpful coding assistant") do |client|
|
|
31
|
+
# client.receive_response.each { |msg| puts msg }
|
|
32
|
+
# end
|
|
33
|
+
#
|
|
34
|
+
class Client
|
|
35
|
+
attr_reader :options, :transport, :server_info
|
|
36
|
+
|
|
37
|
+
# Open a client with automatic cleanup
|
|
38
|
+
#
|
|
39
|
+
# @param options [Options, nil] Configuration options
|
|
40
|
+
# @param transport [Transport::Base, nil] Custom transport
|
|
41
|
+
# @param prompt [String, nil] Initial prompt
|
|
42
|
+
# @yield [Client] Connected client
|
|
43
|
+
# @return [Object] Result of block
|
|
44
|
+
def self.open(options: nil, transport: nil, prompt: nil)
|
|
45
|
+
client = new(options: options, transport: transport)
|
|
46
|
+
begin
|
|
47
|
+
client.connect(prompt: prompt)
|
|
48
|
+
yield client
|
|
49
|
+
ensure
|
|
50
|
+
client.disconnect
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Create a new client
|
|
55
|
+
#
|
|
56
|
+
# @param options [Options, nil] Configuration options
|
|
57
|
+
# @param transport [Transport::Base, nil] Custom transport (default: Subprocess)
|
|
58
|
+
def initialize(options: nil, transport: nil)
|
|
59
|
+
@options = options || Options.new
|
|
60
|
+
@transport = transport || Transport::Subprocess.new(options: @options)
|
|
61
|
+
@protocol = nil
|
|
62
|
+
@server_info = nil
|
|
63
|
+
@connected = false
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Connect to the CLI
|
|
67
|
+
#
|
|
68
|
+
# @param prompt [String, nil] Initial prompt to send
|
|
69
|
+
# @return [void]
|
|
70
|
+
def connect(prompt: nil)
|
|
71
|
+
raise CLIConnectionError, "Already connected" if @connected
|
|
72
|
+
|
|
73
|
+
ENV["CLAUDE_CODE_ENTRYPOINT"] = "sdk-rb-client"
|
|
74
|
+
|
|
75
|
+
@protocol = ControlProtocol.new(transport: @transport, options: @options)
|
|
76
|
+
@server_info = @protocol.start(streaming: true)
|
|
77
|
+
@connected = true
|
|
78
|
+
|
|
79
|
+
send_message(prompt) if prompt
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Disconnect from the CLI
|
|
83
|
+
#
|
|
84
|
+
# @return [void]
|
|
85
|
+
def disconnect
|
|
86
|
+
return unless @connected
|
|
87
|
+
|
|
88
|
+
@protocol&.stop
|
|
89
|
+
@protocol = nil
|
|
90
|
+
@connected = false
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Check if client is connected
|
|
94
|
+
#
|
|
95
|
+
# @return [Boolean]
|
|
96
|
+
def connected?
|
|
97
|
+
@connected
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Send a message to Claude
|
|
101
|
+
#
|
|
102
|
+
# @param content [String, Array] Message content
|
|
103
|
+
# @param session_id [String] Session ID (for multi-session support)
|
|
104
|
+
# @param uuid [String, nil] Message UUID for file checkpointing
|
|
105
|
+
# @return [void]
|
|
106
|
+
def send_message(content, session_id: "default", uuid: nil)
|
|
107
|
+
require_connection!
|
|
108
|
+
@protocol.send_user_message(content, session_id: session_id, uuid: uuid)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Alias for send_message
|
|
112
|
+
alias_method :query, :send_message
|
|
113
|
+
|
|
114
|
+
# Receive all messages (blocks until connection closes)
|
|
115
|
+
#
|
|
116
|
+
# @yield [Message] Received messages
|
|
117
|
+
# @return [Enumerator<Message>] If no block given
|
|
118
|
+
def receive_messages(&block)
|
|
119
|
+
require_connection!
|
|
120
|
+
|
|
121
|
+
@protocol.each_message(&block)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Receive messages until a ResultMessage is received
|
|
125
|
+
#
|
|
126
|
+
# @yield [Message] Received messages
|
|
127
|
+
# @return [Enumerator<Message>] If no block given
|
|
128
|
+
def receive_response(&block)
|
|
129
|
+
require_connection!
|
|
130
|
+
|
|
131
|
+
@protocol.receive_response(&block)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Stream user input from an enumerable (TypeScript SDK parity)
|
|
135
|
+
#
|
|
136
|
+
# Sends each message from the input stream to Claude. When a block is given,
|
|
137
|
+
# messages are sent in a background thread while responses are yielded.
|
|
138
|
+
#
|
|
139
|
+
# @param stream [Enumerable] Input stream of messages (strings, hashes, or UserMessage)
|
|
140
|
+
# @param session_id [String] Default session ID for messages
|
|
141
|
+
# @yield [Message] Received messages (if block given)
|
|
142
|
+
# @return [void]
|
|
143
|
+
# @raise [CLIConnectionError] If not connected
|
|
144
|
+
# @raise [AbortError] If abort signal is triggered
|
|
145
|
+
#
|
|
146
|
+
# @example Without block (just send messages)
|
|
147
|
+
# client.stream_input(["Hello", "How are you?"])
|
|
148
|
+
# client.receive_response.each { |msg| puts msg }
|
|
149
|
+
#
|
|
150
|
+
# @example With block (concurrent send/receive)
|
|
151
|
+
# client.stream_input(["Hello", "Follow up"]) do |msg|
|
|
152
|
+
# case msg
|
|
153
|
+
# when ClaudeAgent::AssistantMessage
|
|
154
|
+
# puts msg.text
|
|
155
|
+
# when ClaudeAgent::ResultMessage
|
|
156
|
+
# puts "Done!"
|
|
157
|
+
# end
|
|
158
|
+
# end
|
|
159
|
+
#
|
|
160
|
+
def stream_input(stream, session_id: "default", &block)
|
|
161
|
+
require_connection!
|
|
162
|
+
|
|
163
|
+
if block_given?
|
|
164
|
+
@protocol.stream_conversation(stream, session_id: session_id, &block)
|
|
165
|
+
else
|
|
166
|
+
@protocol.stream_input(stream, session_id: session_id)
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Interrupt the current operation
|
|
171
|
+
#
|
|
172
|
+
# @return [void]
|
|
173
|
+
def interrupt
|
|
174
|
+
require_connection!
|
|
175
|
+
|
|
176
|
+
@protocol.interrupt
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Abort all pending operations (TypeScript SDK parity)
|
|
180
|
+
#
|
|
181
|
+
# This method:
|
|
182
|
+
# 1. Triggers the abort controller (if configured)
|
|
183
|
+
# 2. Aborts the protocol and terminates the transport
|
|
184
|
+
#
|
|
185
|
+
# @param reason [String, nil] Reason for aborting
|
|
186
|
+
# @return [void]
|
|
187
|
+
def abort!(reason = nil)
|
|
188
|
+
return unless @connected
|
|
189
|
+
|
|
190
|
+
@options.abort_controller&.abort(reason)
|
|
191
|
+
@protocol&.abort!
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Change the permission mode
|
|
195
|
+
#
|
|
196
|
+
# @param mode [String] New permission mode
|
|
197
|
+
# @return [Hash] Response
|
|
198
|
+
def set_permission_mode(mode)
|
|
199
|
+
require_connection!
|
|
200
|
+
|
|
201
|
+
@protocol.set_permission_mode(mode)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Change the model
|
|
205
|
+
#
|
|
206
|
+
# @param model [String, nil] New model name (nil to use default)
|
|
207
|
+
# @return [Hash] Response
|
|
208
|
+
def set_model(model)
|
|
209
|
+
require_connection!
|
|
210
|
+
|
|
211
|
+
@protocol.set_model(model)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Rewind files to the state at a specific user message
|
|
215
|
+
#
|
|
216
|
+
# @param user_message_id [String] UUID of the user message to rewind to
|
|
217
|
+
# @param dry_run [Boolean] If true, preview changes without modifying files
|
|
218
|
+
# @return [RewindFilesResult] Result with rewind information
|
|
219
|
+
def rewind_files(user_message_id, dry_run: false)
|
|
220
|
+
require_connection!
|
|
221
|
+
|
|
222
|
+
@protocol.rewind_files(user_message_id, dry_run: dry_run)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Set maximum thinking tokens (TypeScript SDK parity)
|
|
226
|
+
#
|
|
227
|
+
# @param tokens [Integer, nil] Max thinking tokens (nil to reset)
|
|
228
|
+
# @return [Hash] Response
|
|
229
|
+
def set_max_thinking_tokens(tokens)
|
|
230
|
+
require_connection!
|
|
231
|
+
|
|
232
|
+
@protocol.set_max_thinking_tokens(tokens)
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Get available slash commands (TypeScript SDK parity)
|
|
236
|
+
#
|
|
237
|
+
# @return [Array<SlashCommand>]
|
|
238
|
+
def supported_commands
|
|
239
|
+
require_connection!
|
|
240
|
+
|
|
241
|
+
@protocol.supported_commands
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Get available models (TypeScript SDK parity)
|
|
245
|
+
#
|
|
246
|
+
# @return [Array<ModelInfo>]
|
|
247
|
+
def supported_models
|
|
248
|
+
require_connection!
|
|
249
|
+
|
|
250
|
+
@protocol.supported_models
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Get MCP server status (TypeScript SDK parity)
|
|
254
|
+
#
|
|
255
|
+
# @return [Array<McpServerStatus>]
|
|
256
|
+
def mcp_server_status
|
|
257
|
+
require_connection!
|
|
258
|
+
|
|
259
|
+
@protocol.mcp_server_status
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# Get account information (TypeScript SDK parity)
|
|
263
|
+
#
|
|
264
|
+
# @return [AccountInfo]
|
|
265
|
+
def account_info
|
|
266
|
+
require_connection!
|
|
267
|
+
|
|
268
|
+
@protocol.account_info
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# Dynamically set MCP servers for this session (TypeScript SDK parity)
|
|
272
|
+
#
|
|
273
|
+
# This replaces the current set of dynamically-added MCP servers.
|
|
274
|
+
# Servers that are removed will be disconnected, and new servers will be connected.
|
|
275
|
+
#
|
|
276
|
+
# @param servers [Hash] Map of server name to configuration
|
|
277
|
+
# @return [McpSetServersResult] Result with added, removed, and errors
|
|
278
|
+
#
|
|
279
|
+
# @example
|
|
280
|
+
# result = client.set_mcp_servers({
|
|
281
|
+
# "my-server" => { type: "stdio", command: "node", args: ["server.js"] }
|
|
282
|
+
# })
|
|
283
|
+
# puts "Added: #{result.added}"
|
|
284
|
+
# puts "Removed: #{result.removed}"
|
|
285
|
+
#
|
|
286
|
+
def set_mcp_servers(servers)
|
|
287
|
+
require_connection!
|
|
288
|
+
|
|
289
|
+
@protocol.set_mcp_servers(servers)
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
private
|
|
293
|
+
|
|
294
|
+
def require_connection!
|
|
295
|
+
raise CLIConnectionError, "Not connected" unless @connected
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
end
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ClaudeAgent
|
|
4
|
+
# Text content block
|
|
5
|
+
#
|
|
6
|
+
# @example
|
|
7
|
+
# block = TextBlock.new(text: "Hello, world!")
|
|
8
|
+
# block.text # => "Hello, world!"
|
|
9
|
+
#
|
|
10
|
+
TextBlock = Data.define(:text) do
|
|
11
|
+
def type
|
|
12
|
+
:text
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def to_h
|
|
16
|
+
{ type: "text", text: text }
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Extended thinking content block
|
|
21
|
+
#
|
|
22
|
+
# @example
|
|
23
|
+
# block = ThinkingBlock.new(thinking: "Let me consider...", signature: "abc123")
|
|
24
|
+
# block.thinking # => "Let me consider..."
|
|
25
|
+
#
|
|
26
|
+
ThinkingBlock = Data.define(:thinking, :signature) do
|
|
27
|
+
def type
|
|
28
|
+
:thinking
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def to_h
|
|
32
|
+
{ type: "thinking", thinking: thinking, signature: signature }
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Tool use request block
|
|
37
|
+
#
|
|
38
|
+
# @example
|
|
39
|
+
# block = ToolUseBlock.new(id: "tool_123", name: "Read", input: {file_path: "/tmp/file"})
|
|
40
|
+
# block.name # => "Read"
|
|
41
|
+
#
|
|
42
|
+
ToolUseBlock = Data.define(:id, :name, :input) do
|
|
43
|
+
def type
|
|
44
|
+
:tool_use
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def to_h
|
|
48
|
+
{ type: "tool_use", id: id, name: name, input: input }
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Tool result block
|
|
53
|
+
#
|
|
54
|
+
# @example
|
|
55
|
+
# block = ToolResultBlock.new(tool_use_id: "tool_123", content: "file contents", is_error: false)
|
|
56
|
+
#
|
|
57
|
+
ToolResultBlock = Data.define(:tool_use_id, :content, :is_error) do
|
|
58
|
+
def initialize(tool_use_id:, content: nil, is_error: nil)
|
|
59
|
+
super
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def type
|
|
63
|
+
:tool_result
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def to_h
|
|
67
|
+
h = { type: "tool_result", tool_use_id: tool_use_id }
|
|
68
|
+
h[:content] = content unless content.nil?
|
|
69
|
+
h[:is_error] = is_error unless is_error.nil?
|
|
70
|
+
h
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Server tool use block (for MCP servers)
|
|
75
|
+
#
|
|
76
|
+
ServerToolUseBlock = Data.define(:id, :name, :input, :server_name) do
|
|
77
|
+
def type
|
|
78
|
+
:server_tool_use
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def to_h
|
|
82
|
+
{ type: "server_tool_use", id: id, name: name, input: input, server_name: server_name }
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Server tool result block
|
|
87
|
+
#
|
|
88
|
+
ServerToolResultBlock = Data.define(:tool_use_id, :content, :is_error, :server_name) do
|
|
89
|
+
def initialize(tool_use_id:, server_name:, content: nil, is_error: nil)
|
|
90
|
+
super
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def type
|
|
94
|
+
:server_tool_result
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def to_h
|
|
98
|
+
h = { type: "server_tool_result", tool_use_id: tool_use_id, server_name: server_name }
|
|
99
|
+
h[:content] = content unless content.nil?
|
|
100
|
+
h[:is_error] = is_error unless is_error.nil?
|
|
101
|
+
h
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Image content block (TypeScript SDK parity)
|
|
106
|
+
#
|
|
107
|
+
# Supports both base64-encoded image data and URL sources.
|
|
108
|
+
#
|
|
109
|
+
# @example Base64 image
|
|
110
|
+
# block = ImageContentBlock.new(
|
|
111
|
+
# source: { type: "base64", media_type: "image/png", data: "..." }
|
|
112
|
+
# )
|
|
113
|
+
#
|
|
114
|
+
# @example URL image
|
|
115
|
+
# block = ImageContentBlock.new(
|
|
116
|
+
# source: { type: "url", url: "https://example.com/image.png" }
|
|
117
|
+
# )
|
|
118
|
+
#
|
|
119
|
+
ImageContentBlock = Data.define(:source) do
|
|
120
|
+
def type
|
|
121
|
+
:image
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Get the media type if available
|
|
125
|
+
# @return [String, nil]
|
|
126
|
+
def media_type
|
|
127
|
+
source.is_a?(Hash) ? (source[:media_type] || source["media_type"]) : nil
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Get the base64 data if available
|
|
131
|
+
# @return [String, nil]
|
|
132
|
+
def data
|
|
133
|
+
source.is_a?(Hash) ? (source[:data] || source["data"]) : nil
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Get the URL if this is a URL-sourced image
|
|
137
|
+
# @return [String, nil]
|
|
138
|
+
def url
|
|
139
|
+
source.is_a?(Hash) ? (source[:url] || source["url"]) : nil
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Get the source type (base64 or url)
|
|
143
|
+
# @return [String, nil]
|
|
144
|
+
def source_type
|
|
145
|
+
source.is_a?(Hash) ? (source[:type] || source["type"]) : nil
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def to_h
|
|
149
|
+
{ type: "image", source: source }
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# All content block types
|
|
154
|
+
CONTENT_BLOCK_TYPES = [
|
|
155
|
+
TextBlock,
|
|
156
|
+
ThinkingBlock,
|
|
157
|
+
ToolUseBlock,
|
|
158
|
+
ToolResultBlock,
|
|
159
|
+
ServerToolUseBlock,
|
|
160
|
+
ServerToolResultBlock,
|
|
161
|
+
ImageContentBlock
|
|
162
|
+
].freeze
|
|
163
|
+
end
|