claude_agent 0.7.14 → 0.7.16

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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/rules/conventions.md +66 -16
  3. data/CHANGELOG.md +20 -0
  4. data/CLAUDE.md +24 -4
  5. data/README.md +52 -1529
  6. data/SPEC.md +56 -29
  7. data/docs/architecture.md +339 -0
  8. data/docs/client.md +526 -0
  9. data/docs/configuration.md +571 -0
  10. data/docs/conversations.md +461 -0
  11. data/docs/errors.md +127 -0
  12. data/docs/events.md +225 -0
  13. data/docs/getting-started.md +310 -0
  14. data/docs/hooks.md +380 -0
  15. data/docs/logging.md +96 -0
  16. data/docs/mcp.md +308 -0
  17. data/docs/messages.md +871 -0
  18. data/docs/permissions.md +611 -0
  19. data/docs/queries.md +227 -0
  20. data/docs/sessions.md +335 -0
  21. data/lib/claude_agent/abort_controller.rb +24 -0
  22. data/lib/claude_agent/client/commands.rb +32 -0
  23. data/lib/claude_agent/client.rb +10 -4
  24. data/lib/claude_agent/configuration.rb +129 -0
  25. data/lib/claude_agent/control_protocol/commands.rb +28 -0
  26. data/lib/claude_agent/conversation.rb +37 -4
  27. data/lib/claude_agent/errors.rb +21 -4
  28. data/lib/claude_agent/event_handler.rb +14 -0
  29. data/lib/claude_agent/fork_session.rb +117 -0
  30. data/lib/claude_agent/hook_registry.rb +110 -0
  31. data/lib/claude_agent/hooks.rb +4 -0
  32. data/lib/claude_agent/mcp/server.rb +22 -0
  33. data/lib/claude_agent/mcp/tool.rb +24 -3
  34. data/lib/claude_agent/message.rb +93 -0
  35. data/lib/claude_agent/messages/streaming.rb +37 -0
  36. data/lib/claude_agent/options.rb +10 -0
  37. data/lib/claude_agent/permission_policy.rb +174 -0
  38. data/lib/claude_agent/permission_request.rb +17 -0
  39. data/lib/claude_agent/session.rb +100 -11
  40. data/lib/claude_agent/session_paths.rb +5 -2
  41. data/lib/claude_agent/turn_result.rb +20 -2
  42. data/lib/claude_agent/types/sessions.rb +8 -0
  43. data/lib/claude_agent/version.rb +1 -1
  44. data/lib/claude_agent.rb +187 -0
  45. data/sig/claude_agent.rbs +38 -1
  46. metadata +20 -1
data/docs/queries.md ADDED
@@ -0,0 +1,227 @@
1
+ # One-Shot Queries
2
+
3
+ One-shot queries send a single prompt to the Claude Code CLI and return the response. No persistent connection or multi-turn state is maintained. For multi-turn conversations, see `Conversation`.
4
+
5
+ The SDK provides three query methods at increasing levels of control:
6
+
7
+ | Method | Returns | Config integration | Streaming | Best for |
8
+ |--------------------------|-----------------------|----------------------------|----------------------|--------------------------------------|
9
+ | `ClaudeAgent.ask` | `TurnResult` | Yes (merges global config) | Block form | Most applications |
10
+ | `ClaudeAgent.query_turn` | `TurnResult` | No (explicit Options) | Block + EventHandler | Custom transports, event dispatch |
11
+ | `ClaudeAgent.query` | `Enumerator<Message>` | No (explicit Options) | Enumerator | Full control over message processing |
12
+
13
+ ## ClaudeAgent.ask
14
+
15
+ The primary entry point. Merges per-request keyword arguments with the global `Configuration`, builds an `Options` instance, and delegates to `query_turn`.
16
+
17
+ ```ruby
18
+ turn = ClaudeAgent.ask("What is 2+2?")
19
+ puts turn.text # => "4"
20
+ puts turn.cost # => 0.002
21
+ ```
22
+
23
+ ### With configuration overrides
24
+
25
+ Keyword arguments override global config for this request only:
26
+
27
+ ```ruby
28
+ turn = ClaudeAgent.ask("Fix the bug in auth.rb",
29
+ model: "opus",
30
+ max_turns: 5,
31
+ permission_mode: "acceptEdits"
32
+ )
33
+ ```
34
+
35
+ ### With callbacks
36
+
37
+ Pass `on_*` lambdas to receive events as they stream in. The method still returns a `TurnResult` after the turn completes.
38
+
39
+ ```ruby
40
+ turn = ClaudeAgent.ask("Explain Ruby GC",
41
+ on_text: ->(text) { print text },
42
+ on_tool_use: ->(tool) { puts "\nUsing: #{tool.name}" },
43
+ on_result: ->(result) { puts "\nCost: $#{result.total_cost_usd}" }
44
+ )
45
+ ```
46
+
47
+ Available callback keys correspond to `EventHandler` events: `on_text`, `on_thinking`, `on_tool_use`, `on_tool_result`, `on_result`, `on_assistant`, `on_stream_event`, `on_message` (catch-all), and others. See `EventHandler::EVENTS` for the full list.
48
+
49
+ ### With streaming block
50
+
51
+ The block receives each raw message as it arrives:
52
+
53
+ ```ruby
54
+ turn = ClaudeAgent.ask("Explain Ruby GC") do |msg|
55
+ case msg
56
+ when ClaudeAgent::AssistantMessage
57
+ print msg.text
58
+ when ClaudeAgent::ResultMessage
59
+ puts "\nDone in #{msg.duration_ms}ms"
60
+ end
61
+ end
62
+ ```
63
+
64
+ ### With explicit Options
65
+
66
+ Pass a pre-built `Options` to bypass the global `Configuration` entirely:
67
+
68
+ ```ruby
69
+ opts = ClaudeAgent::Options.new(
70
+ model: "claude-sonnet-4-5-20250514",
71
+ max_turns: 3,
72
+ permission_mode: "acceptEdits",
73
+ tools: ["Read", "Bash"]
74
+ )
75
+
76
+ turn = ClaudeAgent.ask("List files in /tmp", options: opts)
77
+ ```
78
+
79
+ ### With global configuration
80
+
81
+ Set defaults once, then call `ask` without repeating them:
82
+
83
+ ```ruby
84
+ ClaudeAgent.configure do |c|
85
+ c.model = "opus"
86
+ c.max_turns = 10
87
+ c.permission_mode = "acceptEdits"
88
+ end
89
+
90
+ # These calls inherit the global config
91
+ turn = ClaudeAgent.ask("Fix the failing test")
92
+ turn = ClaudeAgent.ask("Now update the docs", max_turns: 3) # override max_turns
93
+ ```
94
+
95
+ ## ClaudeAgent.query_turn
96
+
97
+ Wraps `query` and accumulates all messages into a `TurnResult`. Use this when you need to pass an explicit `Options` or `EventHandler` without going through `Configuration`.
98
+
99
+ ```ruby
100
+ def query_turn(prompt:, options: nil, transport: nil, events: nil, &block)
101
+ ```
102
+
103
+ ### Basic usage
104
+
105
+ ```ruby
106
+ turn = ClaudeAgent.query_turn(prompt: "What is 2+2?")
107
+ puts turn.text
108
+ puts turn.cost
109
+ ```
110
+
111
+ ### With EventHandler
112
+
113
+ Build an `EventHandler` for typed event dispatch:
114
+
115
+ ```ruby
116
+ events = ClaudeAgent::EventHandler.new
117
+ .on_text { |text| print text }
118
+ .on_tool_use { |tool| puts "Tool: #{tool.name}" }
119
+ .on_result { |r| puts "\nCost: $#{r.total_cost_usd}" }
120
+
121
+ turn = ClaudeAgent.query_turn(
122
+ prompt: "Refactor the parser",
123
+ options: ClaudeAgent::Options.new(model: "opus", max_turns: 5),
124
+ events: events
125
+ )
126
+ ```
127
+
128
+ ### With block
129
+
130
+ The block receives each message, just like the block form of `ask`:
131
+
132
+ ```ruby
133
+ turn = ClaudeAgent.query_turn(prompt: "Explain closures") do |msg|
134
+ print msg.text if msg.is_a?(ClaudeAgent::AssistantMessage)
135
+ end
136
+
137
+ puts turn.session_id
138
+ ```
139
+
140
+ ### With custom transport
141
+
142
+ Inject a transport for testing or custom subprocess management:
143
+
144
+ ```ruby
145
+ transport = ClaudeAgent::Transport::Subprocess.new(options: opts)
146
+ turn = ClaudeAgent.query_turn(prompt: "Hello", options: opts, transport: transport)
147
+ ```
148
+
149
+ ## ClaudeAgent.query
150
+
151
+ The lowest-level one-shot interface. Returns an `Enumerator` that yields each `Message` as it arrives from the CLI. You are responsible for iterating and interpreting message types.
152
+
153
+ ```ruby
154
+ def query(prompt:, options: nil, transport: nil)
155
+ ```
156
+
157
+ ### Basic usage with case statement
158
+
159
+ ```ruby
160
+ ClaudeAgent.query(prompt: "What is 2+2?").each do |message|
161
+ case message
162
+ when ClaudeAgent::SystemMessage
163
+ # Init message with session metadata
164
+ when ClaudeAgent::AssistantMessage
165
+ print message.text
166
+ when ClaudeAgent::UserMessage
167
+ # Tool results (system-generated)
168
+ when ClaudeAgent::StreamEvent
169
+ # Streaming deltas
170
+ when ClaudeAgent::ResultMessage
171
+ puts "\nCost: $#{message.total_cost_usd}"
172
+ puts "Duration: #{message.duration_ms}ms"
173
+ puts "Session: #{message.session_id}"
174
+ end
175
+ end
176
+ ```
177
+
178
+ ### Collecting all messages
179
+
180
+ ```ruby
181
+ messages = ClaudeAgent.query(prompt: "Hello").to_a
182
+ result = messages.find { |m| m.is_a?(ClaudeAgent::ResultMessage) }
183
+ puts result.total_cost_usd
184
+ ```
185
+
186
+ ### With custom options
187
+
188
+ ```ruby
189
+ options = ClaudeAgent::Options.new(
190
+ model: "claude-sonnet-4-5-20250514",
191
+ max_turns: 5,
192
+ permission_mode: "acceptEdits"
193
+ )
194
+
195
+ ClaudeAgent.query(prompt: "Fix the bug", options: options).each do |message|
196
+ # ...
197
+ end
198
+ ```
199
+
200
+ ## TurnResult
201
+
202
+ All three methods ultimately produce a `TurnResult` (for `query`, you build one yourself or use `query_turn`). Key accessors:
203
+
204
+ | Accessor | Type | Description |
205
+ |-------------------|-----------------|---------------------------------------------|
206
+ | `text` | `String` | All assistant text concatenated |
207
+ | `thinking` | `String` | All thinking content concatenated |
208
+ | `tool_uses` | `Array` | Tool use blocks from assistant messages |
209
+ | `tool_results` | `Array` | Tool result blocks from user messages |
210
+ | `tool_executions` | `Array<Hash>` | Matched `{ tool_use:, tool_result: }` pairs |
211
+ | `result` | `ResultMessage` | Final result message (nil if incomplete) |
212
+ | `cost` | `Float` | Total cost in USD |
213
+ | `usage` | `Hash` | Token usage breakdown |
214
+ | `duration_ms` | `Integer` | Wall-clock duration |
215
+ | `session_id` | `String` | Session ID for resumption |
216
+ | `model` | `String` | Model used |
217
+ | `success?` | `Boolean` | Whether the turn completed without error |
218
+ | `error?` | `Boolean` | Whether the turn ended with an error |
219
+ | `messages` | `Array` | All raw messages received |
220
+
221
+ ## Choosing the Right Method
222
+
223
+ **Use `ask`** when you want the simplest path with global configuration support. This is the right choice for most applications.
224
+
225
+ **Use `query_turn`** when you need explicit `Options` or `EventHandler` without going through `Configuration`, or when injecting a custom transport.
226
+
227
+ **Use `query`** when you need full control over message iteration -- for example, to build custom accumulators, forward messages to another system, or handle message types that `TurnResult` does not expose.
data/docs/sessions.md ADDED
@@ -0,0 +1,335 @@
1
+ # Sessions
2
+
3
+ Claude Code CLI persists every conversation as a session on disk. The SDK can find, inspect, mutate, fork, and resume these sessions without spawning a CLI subprocess -- all operations read and write the session JSONL files directly.
4
+
5
+ ## Session Resource
6
+
7
+ The `Session` class wraps `SessionInfo` with a rich, Rails-like API for discovering and working with past sessions.
8
+
9
+ ### Finding a Session
10
+
11
+ Use `Session.find` for a safe lookup that returns `nil` when the session does not exist, or `Session.retrieve` when you want an exception on missing sessions.
12
+
13
+ ```ruby
14
+ # Returns Session or nil (targeted lookup by ID, not a full scan)
15
+ session = ClaudeAgent::Session.find("abc-123-def-456")
16
+
17
+ # Returns Session or raises ClaudeAgent::NotFoundError
18
+ session = ClaudeAgent::Session.retrieve("abc-123-def-456")
19
+ ```
20
+
21
+ Both accept an optional `dir:` keyword to scope the search to a specific project directory:
22
+
23
+ ```ruby
24
+ session = ClaudeAgent::Session.find("abc-123-def-456", dir: "/path/to/project")
25
+ ```
26
+
27
+ ### Listing Sessions
28
+
29
+ ```ruby
30
+ # All sessions across all projects, sorted by last modified (most recent first)
31
+ sessions = ClaudeAgent::Session.all
32
+
33
+ # With optional filters
34
+ sessions = ClaudeAgent::Session.where(dir: "/path/to/project", limit: 10)
35
+ ```
36
+
37
+ ### Fields
38
+
39
+ Every `Session` exposes the following attributes:
40
+
41
+ | Field | Type | Description |
42
+ |-----------------|------------------|-----------------------------------------------------------------------------------|
43
+ | `session_id` | `String` | UUID of the session. |
44
+ | `summary` | `String` | Display summary: custom title, last auto-summary, first prompt, or `"(session)"`. |
45
+ | `last_modified` | `Integer` | Last modification time as epoch milliseconds. |
46
+ | `file_size` | `Integer` | Size of the session JSONL file in bytes. |
47
+ | `custom_title` | `String`, `nil` | User-assigned title, if any. |
48
+ | `first_prompt` | `String`, `nil` | First meaningful user prompt (truncated to 200 chars). |
49
+ | `git_branch` | `String`, `nil` | Git branch active during the session. |
50
+ | `cwd` | `String`, `nil` | Working directory the session was started in. |
51
+ | `tag` | `String`, `nil` | User-assigned tag, if any. |
52
+ | `created_at` | `Integer`, `nil` | Creation timestamp (epoch milliseconds), if available. |
53
+
54
+ ```ruby
55
+ session = ClaudeAgent::Session.retrieve("abc-123-def-456")
56
+
57
+ puts session.summary # => "Fix login bug"
58
+ puts session.git_branch # => "fix/login"
59
+ puts session.custom_title # => nil (no custom title set)
60
+ puts session.cwd # => "/Users/dev/myapp"
61
+ ```
62
+
63
+ ### Messages
64
+
65
+ `session.messages` returns a chainable, `Enumerable` `SessionMessageRelation`. Messages are loaded lazily on first access.
66
+
67
+ ```ruby
68
+ session = ClaudeAgent::Session.retrieve("abc-123-def-456")
69
+
70
+ # All messages
71
+ session.messages.each { |m| puts "#{m.type}: #{m.uuid}" }
72
+
73
+ # Pagination via .where
74
+ session.messages.where(limit: 10).to_a
75
+ session.messages.where(limit: 10, offset: 5).to_a
76
+
77
+ # Enumerable methods work directly
78
+ session.messages.first
79
+ session.messages.count
80
+ session.messages.select { |m| m.type == "assistant" }
81
+ session.messages.map(&:uuid)
82
+ ```
83
+
84
+ Each message in the relation is a `SessionMessage` with these fields:
85
+
86
+ | Field | Type | Description |
87
+ |----------------------|-----------------|---------------------------------------------------|
88
+ | `type` | `String` | `"user"` or `"assistant"`. |
89
+ | `uuid` | `String` | Message UUID. |
90
+ | `session_id` | `String` | Session UUID this message belongs to. |
91
+ | `message` | `Hash` | Raw message payload (role, content blocks, etc.). |
92
+ | `parent_tool_use_id` | `String`, `nil` | Parent tool use ID for tool result messages. |
93
+
94
+ ### Mutations
95
+
96
+ #### Renaming
97
+
98
+ ```ruby
99
+ session.rename("My descriptive title")
100
+ session.custom_title # => "My descriptive title"
101
+ ```
102
+
103
+ Appends a `custom-title` JSONL entry to the session file. The `custom_title` attribute is updated in place.
104
+
105
+ #### Tagging
106
+
107
+ ```ruby
108
+ session.tag_session("important")
109
+ session.tag # => "important"
110
+
111
+ # Clear the tag
112
+ session.tag_session(nil)
113
+ session.tag # => nil
114
+ ```
115
+
116
+ Appends a `tag` JSONL entry. Unicode zero-width and directional characters are automatically stripped from tag values.
117
+
118
+ Both mutations return `self` for chaining:
119
+
120
+ ```ruby
121
+ session.rename("Refactored auth module").tag_session("refactor")
122
+ ```
123
+
124
+ ### Forking
125
+
126
+ Create a new session by copying an existing one. All UUIDs in the forked session are remapped to fresh values.
127
+
128
+ ```ruby
129
+ # Fork the entire session
130
+ forked = session.fork
131
+ forked.session_id # => new UUID
132
+
133
+ # Fork up to a specific message (inclusive)
134
+ forked = session.fork(up_to: "message-uuid-here")
135
+
136
+ # Fork with a custom title
137
+ forked = session.fork(title: "Branch: try alternative approach")
138
+ ```
139
+
140
+ The returned value is a new `Session` instance pointing to the forked session file.
141
+
142
+ ### Reloading
143
+
144
+ Re-read session metadata from disk to pick up external changes:
145
+
146
+ ```ruby
147
+ session.reload
148
+ session.summary # reflects current file state
149
+ ```
150
+
151
+ Raises `ClaudeAgent::NotFoundError` if the session file no longer exists.
152
+
153
+ ### Resuming
154
+
155
+ Open a `Conversation` that continues from this session. The CLI restores full conversation context from the session transcript.
156
+
157
+ ```ruby
158
+ # Block form -- auto-closes when the block exits
159
+ session.resume(model: "opus") do |c|
160
+ turn = c.say("Continue where we left off")
161
+ puts turn.text
162
+ end
163
+
164
+ # Without a block -- caller is responsible for closing
165
+ conversation = session.resume(max_turns: 5)
166
+ conversation.say("What did we discuss last time?")
167
+ conversation.close
168
+ ```
169
+
170
+ Accepts the same keyword arguments as `Conversation.new`.
171
+
172
+ ## Functional API
173
+
174
+ The module-level methods provide direct access to session operations without wrapping results in `Session` objects. These return the underlying data types (`SessionInfo`, `SessionMessage`, `ForkSessionResult`) and are useful when you need lower-level control.
175
+
176
+ ### `ClaudeAgent.list_sessions`
177
+
178
+ ```ruby
179
+ # All sessions
180
+ sessions = ClaudeAgent.list_sessions
181
+ # => Array<SessionInfo>
182
+
183
+ # Scoped to a directory with pagination
184
+ sessions = ClaudeAgent.list_sessions(
185
+ dir: "/path/to/project",
186
+ limit: 20,
187
+ offset: 10,
188
+ include_worktrees: true # default: true
189
+ )
190
+ ```
191
+
192
+ When `dir` is inside a git repository and `include_worktrees` is `true`, sessions from all git worktree paths are included automatically.
193
+
194
+ ### `ClaudeAgent.get_session_info`
195
+
196
+ Targeted lookup of a single session by UUID. Returns `SessionInfo` or `nil`.
197
+
198
+ ```ruby
199
+ info = ClaudeAgent.get_session_info("abc-123-def-456")
200
+ info = ClaudeAgent.get_session_info("abc-123-def-456", dir: "/path/to/project")
201
+ ```
202
+
203
+ ### `ClaudeAgent.get_session_messages`
204
+
205
+ Read the conversation transcript for a session. Returns user and assistant messages in chronological order, reconstructing the main conversation thread from branches and forks.
206
+
207
+ ```ruby
208
+ messages = ClaudeAgent.get_session_messages("abc-123-def-456")
209
+ # => Array<SessionMessage>
210
+
211
+ messages = ClaudeAgent.get_session_messages("abc-123-def-456",
212
+ dir: "/path/to/project",
213
+ limit: 10,
214
+ offset: 5
215
+ )
216
+ ```
217
+
218
+ ### `ClaudeAgent.rename_session`
219
+
220
+ ```ruby
221
+ ClaudeAgent.rename_session("abc-123-def-456", "New title")
222
+ ClaudeAgent.rename_session("abc-123-def-456", "New title", dir: "/path/to/project")
223
+ ```
224
+
225
+ Raises `ArgumentError` if the title is empty, `ClaudeAgent::Error` if the session is not found.
226
+
227
+ ### `ClaudeAgent.tag_session`
228
+
229
+ ```ruby
230
+ ClaudeAgent.tag_session("abc-123-def-456", "important")
231
+ ClaudeAgent.tag_session("abc-123-def-456", nil) # clear tag
232
+ ClaudeAgent.tag_session("abc-123-def-456", "v2", dir: "/path/to/project")
233
+ ```
234
+
235
+ Raises `ClaudeAgent::Error` if the session is not found.
236
+
237
+ ### `ClaudeAgent.fork_session`
238
+
239
+ ```ruby
240
+ result = ClaudeAgent.fork_session("abc-123-def-456")
241
+ # => ForkSessionResult
242
+
243
+ result.session_id # => new UUID
244
+
245
+ result = ClaudeAgent.fork_session("abc-123-def-456",
246
+ up_to_message_id: "msg-uuid",
247
+ title: "Forked conversation",
248
+ dir: "/path/to/project"
249
+ )
250
+ ```
251
+
252
+ Raises `ArgumentError` if `up_to_message_id` is provided but not found in the session transcript.
253
+
254
+ ## V2 Session API (Unstable)
255
+
256
+ > **Warning:** The V2 Session API is unstable and may change without notice in any release. It is marked `@alpha` in the source and should not be used in production.
257
+
258
+ The V2 API provides a lower-level, multi-turn session interface that maps directly to the TypeScript SDK's `SDKSession` pattern. Unlike `Conversation`, it gives you explicit control over send/stream cycles.
259
+
260
+ ### Creating a Session
261
+
262
+ ```ruby
263
+ session = ClaudeAgent.unstable_v2_create_session(
264
+ model: "claude-sonnet-4-5-20250929",
265
+ permission_mode: "acceptEdits"
266
+ )
267
+ ```
268
+
269
+ ### Sending and Streaming
270
+
271
+ ```ruby
272
+ session.send("Hello, Claude!")
273
+
274
+ session.stream.each do |msg|
275
+ case msg
276
+ when ClaudeAgent::AssistantMessage
277
+ print msg.text
278
+ when ClaudeAgent::ResultMessage
279
+ puts "\nDone!"
280
+ end
281
+ end
282
+ ```
283
+
284
+ ### Resuming
285
+
286
+ ```ruby
287
+ session = ClaudeAgent.unstable_v2_resume_session(
288
+ "session-abc-123",
289
+ model: "claude-sonnet-4-5-20250929"
290
+ )
291
+ session.send("Continue our conversation")
292
+ session.stream.each { |msg| puts msg.inspect }
293
+ session.close
294
+ ```
295
+
296
+ ### One-Shot Prompt
297
+
298
+ ```ruby
299
+ result = ClaudeAgent.unstable_v2_prompt(
300
+ "What files are in this directory?",
301
+ model: "claude-sonnet-4-5-20250929"
302
+ )
303
+ puts result.text
304
+ ```
305
+
306
+ ### SessionOptions
307
+
308
+ `SessionOptions` is a `Data.define` type with the following fields:
309
+
310
+ | Field | Type | Description |
311
+ |----------------------------------|-----------------|----------------------------------------------------|
312
+ | `model` | `String` | Model identifier (required). |
313
+ | `path_to_claude_code_executable` | `String`, `nil` | Custom path to the Claude Code CLI binary. |
314
+ | `env` | `Hash`, `nil` | Environment variables to pass to the CLI process. |
315
+ | `allowed_tools` | `Array`, `nil` | Tools the agent is allowed to use. |
316
+ | `disallowed_tools` | `Array`, `nil` | Tools the agent is not allowed to use. |
317
+ | `can_use_tool` | `Proc`, `nil` | Callback for dynamic tool permission decisions. |
318
+ | `hooks` | `Hash`, `nil` | Hook configuration. |
319
+ | `permission_mode` | `String`, `nil` | Permission mode (e.g., `"acceptEdits"`, `"plan"`). |
320
+
321
+ ### Lifecycle
322
+
323
+ Always close V2 sessions when done to clean up the underlying CLI subprocess:
324
+
325
+ ```ruby
326
+ session = ClaudeAgent.unstable_v2_create_session(model: "claude-sonnet-4-5-20250929")
327
+ begin
328
+ session.send("Do something")
329
+ session.stream.each { |msg| process(msg) }
330
+ ensure
331
+ session.close
332
+ end
333
+
334
+ session.closed? # => true
335
+ ```
@@ -34,6 +34,17 @@ module ClaudeAgent
34
34
  def abort(reason = nil)
35
35
  @signal.abort!(reason)
36
36
  end
37
+
38
+ # Reset the controller so it can be reused for another turn.
39
+ #
40
+ # After calling {#abort}, the controller is in an aborted state
41
+ # and cannot be reused without resetting. Call this before the
42
+ # next operation, or use {Conversation} which auto-resets.
43
+ #
44
+ # @return [void]
45
+ def reset!
46
+ @signal.reset!
47
+ end
37
48
  end
38
49
 
39
50
  # Signal object that tracks abort state (TypeScript SDK parity)
@@ -93,6 +104,19 @@ module ClaudeAgent
93
104
  raise AbortError, reason if aborted?
94
105
  end
95
106
 
107
+ # Reset the signal so the controller can be reused.
108
+ #
109
+ # No-op if the signal has not been aborted.
110
+ #
111
+ # @return [void]
112
+ def reset!
113
+ @mutex.synchronize do
114
+ return unless @aborted
115
+ @aborted = false
116
+ @reason = nil
117
+ end
118
+ end
119
+
96
120
  # @api private
97
121
  # Trigger the abort
98
122
  # @param reason [String, nil] Reason for aborting
@@ -220,6 +220,38 @@ module ClaudeAgent
220
220
 
221
221
  @protocol.mcp_clear_auth(server_name)
222
222
  end
223
+
224
+ # Cancel a queued async user message (TypeScript SDK v0.2.76 parity)
225
+ #
226
+ # Drops a previously queued user message before it is processed.
227
+ #
228
+ # @param message_uuid [String] UUID of the message to cancel
229
+ # @return [Hash] Response from the CLI
230
+ #
231
+ # @example
232
+ # client.cancel_async_message("msg-uuid-123")
233
+ #
234
+ def cancel_async_message(message_uuid)
235
+ require_connection!
236
+
237
+ @protocol.cancel_async_message(message_uuid)
238
+ end
239
+
240
+ # Get effective merged settings (TypeScript SDK v0.2.76 parity)
241
+ #
242
+ # Returns the current effective settings after merging all layers.
243
+ #
244
+ # @return [Hash] Merged settings
245
+ #
246
+ # @example
247
+ # settings = client.get_settings
248
+ # puts settings["model"]
249
+ #
250
+ def get_settings
251
+ require_connection!
252
+
253
+ @protocol.get_settings
254
+ end
223
255
  end
224
256
  end
225
257
  end
@@ -189,17 +189,23 @@ module ClaudeAgent
189
189
  # Receive messages until a ResultMessage, accumulating into a TurnResult
190
190
  #
191
191
  # Dispatches events to registered handlers (see {#on}).
192
+ # On abort, raises {AbortError} with the partial {TurnResult} attached.
192
193
  #
193
194
  # @yield [Message] Each message as it arrives (optional)
194
195
  # @return [TurnResult] The completed turn
196
+ # @raise [AbortError] If abort signal is triggered (with partial_turn attached)
195
197
  def receive_turn
196
198
  require_connection!
197
199
 
198
200
  turn = TurnResult.new
199
- receive_response do |message|
200
- turn << message
201
- @event_handler.handle(message)
202
- yield message if block_given?
201
+ begin
202
+ receive_response do |message|
203
+ turn << message
204
+ @event_handler.handle(message)
205
+ yield message if block_given?
206
+ end
207
+ rescue AbortError => e
208
+ raise AbortError.new(e.message, partial_turn: turn)
203
209
  end
204
210
  @event_handler.reset!
205
211
  turn