claude_agent 0.7.7 → 0.7.9

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.
data/SPEC.md CHANGED
@@ -3,11 +3,11 @@
3
3
  This document provides a comprehensive specification of the Claude Agent SDK, comparing feature parity across the official TypeScript and Python SDKs with this Ruby implementation.
4
4
 
5
5
  **Reference Versions:**
6
- - TypeScript SDK: v0.2.49 (npm package)
7
- - Python SDK: v0.1.39 from GitHub (commit 146e3d6)
6
+ - TypeScript SDK: v0.2.56 (npm package)
7
+ - Python SDK: v0.1.43 from GitHub (commit 9d758dd)
8
8
  - Ruby SDK: This repository
9
9
 
10
- **Last Updated:** 2026-02-20
10
+ **Last Updated:** 2026-02-25
11
11
 
12
12
  ---
13
13
 
@@ -111,6 +111,7 @@ Messages exchanged between SDK and CLI.
111
111
  | `TaskNotificationMessage` | ✅ | ❌ | ✅ | Background task completion |
112
112
  | `ToolUseSummaryMessage` | ✅ | ❌ | ✅ | Summary of tool use (collapsed) |
113
113
  | `TaskStartedMessage` | ✅ | ❌ | ✅ | Subagent task registered (v0.2.45) |
114
+ | `TaskProgressMessage` | ✅ | ❌ | ✅ | Background task progress (v0.2.51) |
114
115
  | `RateLimitEvent` | ✅ | ❌ | ✅ | Rate limit status changes |
115
116
  | `PromptSuggestionMessage` | ✅ | ❌ | ✅ | Suggested next prompt (v0.2.47) |
116
117
  | `FilesPersistedEvent` | ✅ | ❌ | ✅ | File persistence confirmation |
@@ -232,9 +233,12 @@ Bidirectional control protocol for SDK-CLI communication.
232
233
  | `mcp_reconnect` | ✅ | ❌ | ✅ | Reconnect to MCP server |
233
234
  | `mcp_toggle` | ✅ | ❌ | ✅ | Enable/disable MCP server |
234
235
  | `stop_task` | ✅ | ❌ | ✅ | Stop a running background task |
236
+ | `mcp_authenticate` | ✅ | ❌ | ✅ | Authenticate MCP server (v0.2.52) |
237
+ | `mcp_clear_auth` | ✅ | ❌ | ✅ | Clear MCP server auth (v0.2.52) |
235
238
  | `supported_commands` | ✅ | ❌ | ✅ | Get available slash commands |
236
239
  | `supported_models` | ✅ | ❌ | ✅ | Get available models |
237
240
  | `account_info` | ✅ | ❌ | ✅ | Get account information |
241
+ | `apply_flag_settings` | ✅ | ❌ | ✅ | Merge settings into flag layer |
238
242
 
239
243
  ### Return Types
240
244
 
@@ -274,6 +278,8 @@ Event hooks for intercepting and modifying SDK behavior.
274
278
  | `TeammateIdle` | ✅ | ❌ | ✅ | Teammate idle (v0.2.33) |
275
279
  | `TaskCompleted` | ✅ | ❌ | ✅ | Task completed (v0.2.33) |
276
280
  | `ConfigChange` | ✅ | ❌ | ✅ | Config file changed (v0.2.49) |
281
+ | `WorktreeCreate` | ✅ | ❌ | ✅ | Worktree creation (v0.2.50) |
282
+ | `WorktreeRemove` | ✅ | ❌ | ✅ | Worktree removal (v0.2.50) |
277
283
 
278
284
  ### Hook Input Types
279
285
 
@@ -295,6 +301,8 @@ Event hooks for intercepting and modifying SDK behavior.
295
301
  | `TeammateIdleHookInput` | ✅ | ❌ | ✅ |
296
302
  | `TaskCompletedHookInput` | ✅ | ❌ | ✅ |
297
303
  | `ConfigChangeHookInput` | ✅ | ❌ | ✅ |
304
+ | `WorktreeCreateHookInput` | ✅ | ❌ | ✅ |
305
+ | `WorktreeRemoveHookInput` | ✅ | ❌ | ✅ |
298
306
 
299
307
  ### Hook Output Types
300
308
 
@@ -514,6 +522,25 @@ Session management and resumption.
514
522
  | Persist session | ✅ | ❌ | ✅ | `persistSession` option |
515
523
  | Continue most recent | ✅ | ✅ | ✅ | `continue` option |
516
524
 
525
+ ### Session Discovery
526
+
527
+ | Feature | TypeScript | Python | Ruby | Notes |
528
+ |------------------|:----------:|:------:|:----:|--------------------------------------------|
529
+ | `listSessions()` | ✅ | ❌ | ✅ | List past sessions with metadata (v0.2.53) |
530
+
531
+ #### SDKSessionInfo Fields
532
+
533
+ | Field | TypeScript | Python | Ruby | Notes |
534
+ |----------------|:----------:|:------:|:----:|-------------------------------------|
535
+ | `sessionId` | ✅ | ❌ | ✅ | Session UUID |
536
+ | `summary` | ✅ | ❌ | ✅ | Display title/summary |
537
+ | `lastModified` | ✅ | ❌ | ✅ | Last modified time (ms since epoch) |
538
+ | `fileSize` | ✅ | ❌ | ✅ | Session file size in bytes |
539
+ | `customTitle` | ✅ | ❌ | ✅ | User-set title via /rename |
540
+ | `firstPrompt` | ✅ | ❌ | ✅ | First meaningful user prompt |
541
+ | `gitBranch` | ✅ | ❌ | ✅ | Git branch at end of session |
542
+ | `cwd` | ✅ | ❌ | ✅ | Working directory for session |
543
+
517
544
  ### V2 Session API (Unstable)
518
545
 
519
546
  | Feature | TypeScript | Python | Ruby | Notes |
@@ -593,11 +620,11 @@ Error types and hierarchy.
593
620
  |----------------------|:----------:|:------:|:----:|--------------------------------|
594
621
  | Base Error | ✅ | ✅ | ✅ | `Error` / `ClaudeAgent::Error` |
595
622
  | `AbortError` | ✅ | ❌ | ✅ | Operation cancelled |
596
- | `CLINotFoundError` | ❌ | | ✅ | CLI not found |
623
+ | `CLINotFoundError` | ❌ | | ✅ | CLI not found |
597
624
  | `CLIVersionError` | ❌ | ❌ | ✅ | CLI version too old |
598
- | `CLIConnectionError` | ❌ | | ✅ | Connection failed |
599
- | `ProcessError` | ❌ | | ✅ | CLI process failed |
600
- | `JSONDecodeError` | ❌ | | ✅ | JSON parsing failed |
625
+ | `CLIConnectionError` | ❌ | | ✅ | Connection failed |
626
+ | `ProcessError` | ❌ | | ✅ | CLI process failed |
627
+ | `JSONDecodeError` | ❌ | | ✅ | JSON parsing failed |
601
628
  | `MessageParseError` | ❌ | ❌ | ✅ | Message parsing failed |
602
629
  | `TimeoutError` | ❌ | ❌ | ✅ | Control request timeout |
603
630
  | `ConfigurationError` | ❌ | ❌ | ✅ | Invalid configuration |
@@ -620,6 +647,12 @@ Error types and hierarchy.
620
647
 
621
648
  Public API surface for SDK clients.
622
649
 
650
+ ### Standalone Functions
651
+
652
+ | Feature | TypeScript | Python | Ruby | Notes |
653
+ |------------------|:----------------:|:------:|:----:|--------------------------------------------|
654
+ | `listSessions()` | ✅ `listSessions` | ❌ | ✅ | List past sessions with metadata (v0.2.53) |
655
+
623
656
  ### Query Interface
624
657
 
625
658
  | Feature | TypeScript | Python | Ruby | Notes |
@@ -629,24 +662,24 @@ Public API surface for SDK clients.
629
662
 
630
663
  ### Query Control Methods
631
664
 
632
- | Method | TypeScript | Python | Ruby | Notes |
633
- |--------------------------|:----------:|:------:|:----:|------------------------|
634
- | `interrupt()` | ✅ | ✅ | ✅ | Interrupt execution |
635
- | `setPermissionMode()` | ✅ | ✅ | ✅ | Change permission mode |
636
- | `setModel()` | ✅ | ✅ | ✅ | Change model |
637
- | `setMaxThinkingTokens()` | ✅ | ❌ | ✅ | Set thinking limit |
638
- | `supportedCommands()` | ✅ | ❌ | ✅ | Get slash commands |
639
- | `supportedModels()` | ✅ | ❌ | ✅ | Get available models |
640
- | `mcpServerStatus()` | ✅ | ✅ | ✅ | Get MCP status |
641
- | `accountInfo()` | ✅ | ❌ | ✅ | Get account info |
642
- | `rewindFiles()` | ✅ | ✅ | ✅ | Rewind file changes |
643
- | `setMcpServers()` | ✅ | ❌ | ✅ | Dynamic MCP servers |
644
- | `reconnectMcpServer()` | ✅ | ❌ | ✅ | Reconnect MCP server |
645
- | `toggleMcpServer()` | ✅ | ❌ | ✅ | Enable/disable MCP |
646
- | `stopTask()` | ✅ | ❌ | ✅ | Stop running task |
647
- | `streamInput()` | ✅ | ✅ | ✅ | Stream user input |
648
- | `initializationResult()` | ✅ | | ✅ | Full init response |
649
- | `close()` | ✅ | ✅ | ✅ | Close query/session |
665
+ | Method | TypeScript | Python | Ruby | Notes |
666
+ |--------------------------|:----------:|:------:|:----:|----------------------------------------------|
667
+ | `interrupt()` | ✅ | ✅ | ✅ | Interrupt execution |
668
+ | `setPermissionMode()` | ✅ | ✅ | ✅ | Change permission mode |
669
+ | `setModel()` | ✅ | ✅ | ✅ | Change model |
670
+ | `setMaxThinkingTokens()` | ✅ | ❌ | ✅ | Set thinking limit |
671
+ | `supportedCommands()` | ✅ | ❌ | ✅ | Get slash commands |
672
+ | `supportedModels()` | ✅ | ❌ | ✅ | Get available models |
673
+ | `mcpServerStatus()` | ✅ | ✅ | ✅ | Get MCP status |
674
+ | `accountInfo()` | ✅ | ❌ | ✅ | Get account info |
675
+ | `rewindFiles()` | ✅ | ✅ | ✅ | Rewind file changes |
676
+ | `setMcpServers()` | ✅ | ❌ | ✅ | Dynamic MCP servers |
677
+ | `reconnectMcpServer()` | ✅ | ❌ | ✅ | Reconnect MCP server |
678
+ | `toggleMcpServer()` | ✅ | ❌ | ✅ | Enable/disable MCP |
679
+ | `stopTask()` | ✅ | ❌ | ✅ | Stop running task |
680
+ | `streamInput()` | ✅ | ✅ | ✅ | Stream user input |
681
+ | `initializationResult()` | ✅ | | ✅ | Full init response (Py: `get_server_info()`) |
682
+ | `close()` | ✅ | ✅ | ✅ | Close query/session |
650
683
 
651
684
  ### Client Class
652
685
 
@@ -689,23 +722,30 @@ Public API surface for SDK clients.
689
722
  - `executable`/`executableArgs` are JS-specific (`node`/`bun`/`deno`)
690
723
  - v0.2.45: Added `TaskStartedMessage`, `RateLimitEvent` message types
691
724
  - v0.2.47: Added `promptSuggestions` option and `PromptSuggestionMessage`
692
- - v0.2.49: Added `ConfigChange` hook event, `SandboxFilesystemConfig`
725
+ - v0.2.49: Added `ConfigChange` hook event, `SandboxFilesystemConfig`, ModelInfo capability fields
726
+ - v0.2.50: Added `WorktreeCreate`/`WorktreeRemove` hook events, `apply_flag_settings` control request
727
+ - v0.2.51: Added `TaskProgressMessage` for real-time background agent progress reporting
728
+ - v0.2.52: Added `mcp_authenticate`/`mcp_clear_auth` control requests for MCP server authentication
729
+ - v0.2.53: Added `listSessions()` for discovering and listing past sessions with `SDKSessionInfo` metadata
730
+ - v0.2.54 – v0.2.56: CLI parity updates (no new SDK-facing features)
693
731
 
694
732
  ### Python SDK
695
733
  - Full source available with `Transport` abstract class
696
734
  - Partial control protocol: query and client support interrupt, setPermissionMode, setModel, rewindFiles, mcpStatus
697
- - Missing hooks: SessionStart, SessionEnd, Setup, TeammateIdle, TaskCompleted, ConfigChange
735
+ - Has `CLINotFoundError`, `CLIConnectionError`, `ProcessError`, `CLIJSONDecodeError` error types
736
+ - Missing hooks: SessionStart, SessionEnd, Setup, TeammateIdle, TaskCompleted, ConfigChange, WorktreeCreate, WorktreeRemove
698
737
  - Missing permission modes: `dontAsk`
699
738
  - Missing options: `allowDangerouslySkipPermissions`, `persistSession`, `resumeSessionAt`, `sessionId`, `strictMcpConfig`, `init`/`initOnly`/`maintenance`, `debug`/`debugFile`, `promptSuggestions`
700
739
  - `ToolPermissionContext` missing `blockedPath`, `decisionReason`, `toolUseID`, `agentID`, `description`
701
740
  - Has SDK MCP server support with `tool()` helper and annotations
702
741
  - Added `thinking` config and `effort` option in v0.1.36
703
- - Handles `rate_limit_event` and unknown message types gracefully (v0.1.39)
742
+ - Handles `rate_limit_event` and unknown message types gracefully (v0.1.40)
743
+ - Client has `get_server_info()` for accessing the initialization result (v0.1.31+)
744
+ - v0.1.42 – v0.1.43: CLI parity updates (no new SDK-facing features)
704
745
 
705
746
  ### Ruby SDK (This Repository)
706
- - Feature parity with TypeScript SDK v0.2.42
747
+ - Feature parity with TypeScript SDK v0.2.56
707
748
  - Ruby-idiomatic patterns (Data.define, snake_case)
708
749
  - Complete control protocol, hook, and V2 Session API support
709
750
  - Dedicated Client class for multi-turn conversations
710
751
  - `executable`/`executableArgs` marked N/A (JS runtime options)
711
- - Gaps from TS v0.2.43-v0.2.49: `promptSuggestions`, `TaskStartedMessage`, `RateLimitEvent`, `PromptSuggestionMessage`, `ConfigChange` hook, `SandboxFilesystemConfig`
@@ -32,7 +32,7 @@ module ClaudeAgent
32
32
  # end
33
33
  #
34
34
  class Client
35
- attr_reader :options, :transport, :server_info
35
+ attr_reader :options, :transport, :server_info, :cumulative_usage, :event_handler, :permission_queue
36
36
 
37
37
  # Open a client with automatic cleanup
38
38
  #
@@ -61,6 +61,9 @@ module ClaudeAgent
61
61
  @protocol = nil
62
62
  @server_info = nil
63
63
  @connected = false
64
+ @cumulative_usage = CumulativeUsage.new
65
+ @event_handler = EventHandler.new
66
+ @permission_queue = PermissionQueue.new
64
67
  end
65
68
 
66
69
  # Connect to the CLI
@@ -74,6 +77,7 @@ module ClaudeAgent
74
77
 
75
78
  logger.info("client") { "Connecting" }
76
79
  @protocol = ControlProtocol.new(transport: @transport, options: @options)
80
+ @protocol.permission_queue = @permission_queue
77
81
  @server_info = @protocol.start(streaming: true)
78
82
  @connected = true
79
83
  logger.info("client") { "Connected" }
@@ -88,6 +92,7 @@ module ClaudeAgent
88
92
  return unless @connected
89
93
 
90
94
  logger.info("client") { "Disconnecting" }
95
+ @permission_queue.drain!(reason: "Client disconnected")
91
96
  @protocol&.stop
92
97
  @protocol = nil
93
98
  @connected = false
@@ -119,20 +124,133 @@ module ClaudeAgent
119
124
  #
120
125
  # @yield [Message] Received messages
121
126
  # @return [Enumerator<Message>] If no block given
122
- def receive_messages(&block)
127
+ def receive_messages
123
128
  require_connection!
124
129
 
125
- @protocol.each_message(&block)
130
+ if block_given?
131
+ @protocol.each_message do |message|
132
+ @cumulative_usage.track(message)
133
+ yield message
134
+ end
135
+ else
136
+ enum_for(:receive_messages)
137
+ end
126
138
  end
127
139
 
128
140
  # Receive messages until a ResultMessage is received
129
141
  #
130
142
  # @yield [Message] Received messages
131
143
  # @return [Enumerator<Message>] If no block given
132
- def receive_response(&block)
144
+ def receive_response
133
145
  require_connection!
134
146
 
135
- @protocol.receive_response(&block)
147
+ if block_given?
148
+ @protocol.receive_response do |message|
149
+ @cumulative_usage.track(message)
150
+ yield message
151
+ end
152
+ else
153
+ enum_for(:receive_response)
154
+ end
155
+ end
156
+
157
+ # Register an event handler
158
+ #
159
+ # Handlers persist across turns and fire automatically during
160
+ # {#receive_turn} and {#send_and_receive}.
161
+ #
162
+ # @param event [Symbol] Event name (:message, :text, :thinking, :tool_use, :tool_result, :result)
163
+ # @yield Event-specific arguments
164
+ # @return [self]
165
+ #
166
+ # @example
167
+ # client.on(:text) { |text| print text }
168
+ # client.on(:tool_use) { |tool| show_spinner(tool) }
169
+ #
170
+ def on(event, &block)
171
+ @event_handler.on(event, &block)
172
+ self
173
+ end
174
+
175
+ # @!method on_text(&block)
176
+ # Register a handler for assistant text content
177
+ # @yield [String] Text from the AssistantMessage
178
+ # @return [self]
179
+
180
+ # @!method on_thinking(&block)
181
+ # Register a handler for assistant thinking content
182
+ # @yield [String] Thinking from the AssistantMessage
183
+ # @return [self]
184
+
185
+ # @!method on_tool_use(&block)
186
+ # Register a handler for tool use requests
187
+ # @yield [ToolUseBlock, ServerToolUseBlock] The tool use block
188
+ # @return [self]
189
+
190
+ # @!method on_tool_result(&block)
191
+ # Register a handler for tool results, paired with the original request
192
+ # @yield [ToolResultBlock, ToolUseBlock|nil] Result block and matched tool use
193
+ # @return [self]
194
+
195
+ # @!method on_result(&block)
196
+ # Register a handler for the final ResultMessage
197
+ # @yield [ResultMessage] The result
198
+ # @return [self]
199
+
200
+ # @!method on_message(&block)
201
+ # Register a handler for every message (catch-all)
202
+ # @yield [message] Any message object
203
+ # @return [self]
204
+
205
+ %i[message text thinking tool_use tool_result result].each do |event|
206
+ define_method(:"on_#{event}") { |&block| on(event, &block) }
207
+ end
208
+
209
+ # Receive messages until a ResultMessage, accumulating into a TurnResult
210
+ #
211
+ # Dispatches events to registered handlers (see {#on}).
212
+ #
213
+ # @yield [Message] Each message as it arrives (optional)
214
+ # @return [TurnResult] The completed turn
215
+ def receive_turn
216
+ require_connection!
217
+
218
+ turn = TurnResult.new
219
+ receive_response do |message|
220
+ turn << message
221
+ @event_handler.handle(message)
222
+ yield message if block_given?
223
+ end
224
+ @event_handler.reset!
225
+ turn
226
+ end
227
+
228
+ # Send a message and receive the complete turn result
229
+ #
230
+ # Combines {#send_message} and {#receive_turn} into a single call.
231
+ #
232
+ # @param content [String, Array] Message content
233
+ # @param session_id [String] Session ID
234
+ # @param uuid [String, nil] Message UUID for file checkpointing
235
+ # @yield [Message] Each message as it arrives (optional)
236
+ # @return [TurnResult] The completed turn
237
+ #
238
+ # @example Simple
239
+ # turn = client.send_and_receive("Fix the bug")
240
+ # puts turn.text
241
+ # puts "Cost: $#{turn.cost}"
242
+ #
243
+ # @example With streaming
244
+ # turn = client.send_and_receive("Fix the bug") do |msg|
245
+ # case msg
246
+ # when ClaudeAgent::AssistantMessage
247
+ # print msg.text
248
+ # end
249
+ # end
250
+ #
251
+ def send_and_receive(content, session_id: "default", uuid: nil, &block)
252
+ send_message(content, session_id: session_id, uuid: uuid)
253
+ receive_turn(&block)
136
254
  end
137
255
 
138
256
  # Stream user input from an enumerable (TypeScript SDK parity)
@@ -161,11 +279,14 @@ module ClaudeAgent
161
279
  # end
162
280
  # end
163
281
  #
164
- def stream_input(stream, session_id: "default", &block)
282
+ def stream_input(stream, session_id: "default")
165
283
  require_connection!
166
284
 
167
285
  if block_given?
168
- @protocol.stream_conversation(stream, session_id: session_id, &block)
286
+ @protocol.stream_conversation(stream, session_id: session_id) do |message|
287
+ @cumulative_usage.track(message)
288
+ yield message
289
+ end
169
290
  else
170
291
  @protocol.stream_input(stream, session_id: session_id)
171
292
  end
@@ -191,6 +312,7 @@ module ClaudeAgent
191
312
  def abort!(reason = nil)
192
313
  return unless @connected
193
314
 
315
+ @permission_queue.drain!(reason: reason || "Operation aborted")
194
316
  @options.abort_controller&.abort(reason)
195
317
  @protocol&.abort!
196
318
  end
@@ -298,6 +420,22 @@ module ClaudeAgent
298
420
  @protocol.stop_task(task_id)
299
421
  end
300
422
 
423
+ # Apply flag settings (TypeScript SDK v0.2.50 parity)
424
+ #
425
+ # Merges the provided settings into the flag settings layer.
426
+ #
427
+ # @param settings [Hash] Settings to merge into the flag layer
428
+ # @return [Hash] Response from the CLI
429
+ #
430
+ # @example
431
+ # client.apply_flag_settings({ "model" => "claude-sonnet-4-5-20250514" })
432
+ #
433
+ def apply_flag_settings(settings)
434
+ require_connection!
435
+
436
+ @protocol.apply_flag_settings(settings)
437
+ end
438
+
301
439
  # Dynamically set MCP servers for this session (TypeScript SDK parity)
302
440
  #
303
441
  # This replaces the current set of dynamically-added MCP servers.
@@ -355,6 +493,58 @@ module ClaudeAgent
355
493
  @protocol.mcp_toggle(server_name, enabled: enabled)
356
494
  end
357
495
 
496
+ # Initiate OAuth authentication for an MCP server (TypeScript SDK v0.2.52 parity)
497
+ #
498
+ # @param server_name [String] Name of the MCP server to authenticate
499
+ # @return [Hash] Response from the CLI
500
+ #
501
+ # @example
502
+ # client.mcp_authenticate("my-remote-server")
503
+ #
504
+ def mcp_authenticate(server_name)
505
+ require_connection!
506
+
507
+ @protocol.mcp_authenticate(server_name)
508
+ end
509
+
510
+ # Clear stored auth credentials for an MCP server (TypeScript SDK v0.2.52 parity)
511
+ #
512
+ # @param server_name [String] Name of the MCP server to clear auth for
513
+ # @return [Hash] Response from the CLI
514
+ #
515
+ # @example
516
+ # client.mcp_clear_auth("my-remote-server")
517
+ #
518
+ def mcp_clear_auth(server_name)
519
+ require_connection!
520
+
521
+ @protocol.mcp_clear_auth(server_name)
522
+ end
523
+
524
+ # Non-blocking poll for the next pending permission request.
525
+ #
526
+ # Returns the next {PermissionRequest} from the queue, or nil if
527
+ # no requests are pending. Call {PermissionRequest#allow!} or
528
+ # {PermissionRequest#deny!} to resolve it.
529
+ #
530
+ # @return [PermissionRequest, nil] The next pending request, or nil
531
+ #
532
+ # @example UI poll loop
533
+ # if request = client.pending_permission
534
+ # show_permission_dialog(request)
535
+ # end
536
+ #
537
+ def pending_permission
538
+ @permission_queue.poll
539
+ end
540
+
541
+ # Check if there are any pending permission requests.
542
+ #
543
+ # @return [Boolean]
544
+ def pending_permissions?
545
+ !@permission_queue.empty?
546
+ end
547
+
358
548
  private
359
549
 
360
550
  def logger