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
@@ -0,0 +1,461 @@
1
+ # Conversations
2
+
3
+ The `Conversation` class is the primary interface for multi-turn interactions with Claude. It manages the full lifecycle of a conversation: connecting to the CLI, sending messages, tracking turns, accumulating tool activity, and cleaning up resources.
4
+
5
+ Under the hood, `Conversation` wraps a `Client` and composes `TurnResult`, `EventHandler`, `CumulativeUsage`, and `PermissionQueue` into a single stateful object. It auto-connects on the first call to `say`, tracks multi-turn history, and builds a unified tool activity timeline across all turns.
6
+
7
+ ## Creating a Conversation
8
+
9
+ There are several ways to create a conversation, depending on how much control you need over its lifecycle.
10
+
11
+ ### `ClaudeAgent.chat`
12
+
13
+ The top-level entry point. Merges global configuration defaults automatically.
14
+
15
+ ```ruby
16
+ # Block form -- auto-closes when the block exits
17
+ ClaudeAgent.chat(model: "claude-sonnet-4-5-20250514") do |c|
18
+ c.say("Hello")
19
+ c.say("Goodbye")
20
+ end
21
+
22
+ # Without a block -- caller is responsible for closing
23
+ c = ClaudeAgent.chat(model: "claude-sonnet-4-5-20250514")
24
+ c.say("Hello")
25
+ c.close
26
+ ```
27
+
28
+ ### `ClaudeAgent.conversation`
29
+
30
+ Creates a `Conversation` without merging global configuration defaults. Accepts the same keyword arguments as `Conversation.new`.
31
+
32
+ ```ruby
33
+ c = ClaudeAgent.conversation(max_turns: 5)
34
+ c.say("Help me debug this")
35
+ c.close
36
+ ```
37
+
38
+ ### `Conversation.new`
39
+
40
+ Direct instantiation. Accepts all `Options` keyword arguments plus conversation-level callbacks (any `on_*` keyword).
41
+
42
+ ```ruby
43
+ conversation = ClaudeAgent::Conversation.new(
44
+ model: "claude-sonnet-4-5-20250514",
45
+ max_turns: 10,
46
+ on_text: ->(text) { print text },
47
+ on_result: ->(result) { puts "\nCost: $#{result.total_cost_usd}" }
48
+ )
49
+ turn = conversation.say("Fix the bug in auth.rb")
50
+ conversation.close
51
+ ```
52
+
53
+ ### `Conversation.open`
54
+
55
+ Block form with automatic cleanup. The conversation is closed when the block exits, even if an exception is raised.
56
+
57
+ ```ruby
58
+ ClaudeAgent::Conversation.open(permission_mode: "default") do |c|
59
+ c.say("Help me write a function")
60
+ c.say("Now add tests")
61
+ puts "Total cost: $#{c.total_cost}"
62
+ end
63
+ ```
64
+
65
+ ## Sending Messages
66
+
67
+ Use `say` to send a message and receive the complete turn result. The conversation auto-connects on the first call.
68
+
69
+ ```ruby
70
+ conversation = ClaudeAgent::Conversation.new(max_turns: 5)
71
+
72
+ turn = conversation.say("Fix the bug in auth.rb")
73
+ puts turn.text
74
+ puts "Tools used: #{turn.tool_uses.size}"
75
+ ```
76
+
77
+ ### Block Form for Streaming
78
+
79
+ Pass a block to `say` to receive each message as it streams in.
80
+
81
+ ```ruby
82
+ conversation.say("Explain how authentication works") do |message|
83
+ case message
84
+ when ClaudeAgent::AssistantMessage
85
+ print message.text
86
+ when ClaudeAgent::ResultMessage
87
+ puts "\nDone! Cost: $#{message.total_cost_usd}"
88
+ end
89
+ end
90
+ ```
91
+
92
+ ### Multiple Turns
93
+
94
+ Each call to `say` is a new turn. Context is preserved across the conversation.
95
+
96
+ ```ruby
97
+ ClaudeAgent::Conversation.open(max_turns: 3) do |c|
98
+ c.say("Remember the secret word: PINEAPPLE")
99
+ turn = c.say("What was the secret word?")
100
+ puts turn.text # => "PINEAPPLE"
101
+ puts c.turns.size # => 2
102
+ end
103
+ ```
104
+
105
+ ## TurnResult
106
+
107
+ Every call to `say` returns a `TurnResult` -- an accumulation of all messages received during that turn. It provides convenient accessors so you never need to write `case` statements over raw message types.
108
+
109
+ ### Text and Thinking
110
+
111
+ | Method | Return Type | Description |
112
+ |------------|-------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------|
113
+ | `text` | `String` | All text content concatenated across assistant messages. Falls back to accumulated streaming deltas if the turn was aborted before an `AssistantMessage` arrived. |
114
+ | `thinking` | `String` | All thinking content concatenated across assistant messages. |
115
+
116
+ ```ruby
117
+ turn = conversation.say("Explain Ruby blocks")
118
+ puts turn.text
119
+ puts "Thinking: #{turn.thinking}" unless turn.thinking.empty?
120
+ ```
121
+
122
+ ### Tool Use
123
+
124
+ | Method | Return Type | Description |
125
+ |-------------------|-------------------------------------------------|------------------------------------------------------------------------------------------|
126
+ | `tool_uses` | `Array<ToolUseBlock, ServerToolUseBlock>` | All tool use blocks across all assistant messages. |
127
+ | `tool_results` | `Array<ToolResultBlock, ServerToolResultBlock>` | All tool result blocks from user messages (system-generated tool responses). |
128
+ | `tool_executions` | `Array<Hash>` | Tool use/result pairs matched by ID. Each entry has `:tool_use` and `:tool_result` keys. |
129
+
130
+ ```ruby
131
+ turn = conversation.say("Read the config file and fix the typo")
132
+
133
+ turn.tool_uses.each do |tool|
134
+ puts "Used: #{tool.display_label}"
135
+ end
136
+
137
+ turn.tool_executions.each do |exec|
138
+ puts "#{exec[:tool_use].name}: #{exec[:tool_result]&.content&.to_s&.slice(0, 80)}"
139
+ end
140
+ ```
141
+
142
+ ### Result Accessors
143
+
144
+ These delegate to the underlying `ResultMessage`. They return `nil` if the turn is still in progress.
145
+
146
+ | Method | Return Type | Description |
147
+ |---------------------|----------------|-----------------------------------------------|
148
+ | `cost` | `Float, nil` | Total cost in USD for this turn. |
149
+ | `duration_ms` | `Integer, nil` | Wall-clock duration in milliseconds. |
150
+ | `duration_api_ms` | `Integer, nil` | API-only duration in milliseconds. |
151
+ | `session_id` | `String, nil` | Session ID for resumption. |
152
+ | `model` | `String, nil` | Model used (from first assistant message). |
153
+ | `stop_reason` | `String, nil` | Why the model stopped generating. |
154
+ | `usage` | `Hash, nil` | Token usage breakdown. |
155
+ | `model_usage` | `Hash, nil` | Per-model usage breakdown. |
156
+ | `structured_output` | `Object, nil` | Structured output (if requested via options). |
157
+ | `num_turns` | `Integer, nil` | Number of turns in the session. |
158
+
159
+ ```ruby
160
+ turn = conversation.say("What is 2+2?")
161
+ puts "Model: #{turn.model}"
162
+ puts "Cost: $#{turn.cost}"
163
+ puts "Duration: #{turn.duration_ms}ms"
164
+ puts "Session: #{turn.session_id}"
165
+ ```
166
+
167
+ ### Status
168
+
169
+ | Method | Return Type | Description |
170
+ |----------------------|------------------------------|--------------------------------------------------------------|
171
+ | `complete?` | `Boolean` | Whether a `ResultMessage` has been received. |
172
+ | `success?` | `Boolean` | Whether the turn completed successfully. |
173
+ | `error?` | `Boolean` | Whether the turn ended with an error. |
174
+ | `subtype` | `String, nil` | The result subtype (e.g., `"success"`, `"error_max_turns"`). |
175
+ | `errors` | `Array<String>` | Errors from the result. |
176
+ | `permission_denials` | `Array<SDKPermissionDenial>` | Tools that were denied by permission callbacks. |
177
+
178
+ ```ruby
179
+ turn = conversation.say("Deploy to production")
180
+ if turn.success?
181
+ puts "Deployed successfully"
182
+ elsif turn.error?
183
+ puts "Errors: #{turn.errors.join(", ")}"
184
+ end
185
+ ```
186
+
187
+ ### Filtered Message Access
188
+
189
+ | Method | Return Type | Description |
190
+ |----------------------|------------------------------------------------------|-----------------------------------------------------------------------|
191
+ | `messages` | `Array<Message>` | All messages received during this turn. |
192
+ | `assistant_messages` | `Array<AssistantMessage>` | Only assistant messages. |
193
+ | `user_messages` | `Array<UserMessage, UserMessageReplay>` | Only user messages (including system-generated tool result messages). |
194
+ | `stream_events` | `Array<StreamEvent>` | Only stream events. |
195
+ | `content_blocks` | `Array<TextBlock, ThinkingBlock, ToolUseBlock, ...>` | All content blocks across all assistant messages. |
196
+
197
+ ## Callbacks
198
+
199
+ Register callbacks when creating the conversation to react to events as they happen. Callbacks persist across turns.
200
+
201
+ | Callback | Arguments | Description |
202
+ |--------------------|----------------------------------|---------------------------------------------------------------------------------------------------------|
203
+ | `on_text` | `(text)` | Fires when the assistant produces text content. |
204
+ | `on_stream` | `(text)` | Alias for `on_text`. |
205
+ | `on_thinking` | `(thinking)` | Fires when the assistant produces thinking content. |
206
+ | `on_tool_use` | `(tool_use)` | Fires when the assistant requests a tool use. The argument is a `ToolUseBlock` or `ServerToolUseBlock`. |
207
+ | `on_tool_result` | `(tool_result, tool_use)` | Fires when a tool result is received. The second argument is the matched `ToolUseBlock` (or `nil`). |
208
+ | `on_result` | `(result)` | Fires when the turn completes. The argument is a `ResultMessage`. |
209
+ | `on_message` | `(message)` | Fires for every message (catch-all). |
210
+ | `on_stream_event` | `(stream_event)` | Fires for raw stream events. |
211
+ | `on_status` | `(status_message)` | Fires for status messages (e.g., compacting). |
212
+ | `on_tool_progress` | `(tool_progress_message)` | Fires for tool progress updates. |
213
+ | `on_permission` | `Symbol, Proc, PermissionPolicy` | Controls permission handling. See below. |
214
+
215
+ ```ruby
216
+ conversation = ClaudeAgent::Conversation.new(
217
+ on_text: ->(text) { print text },
218
+ on_thinking: ->(thinking) { $stderr.puts "[thinking] #{thinking}" },
219
+ on_tool_use: ->(tool) { puts "\nUsing tool: #{tool.display_label}" },
220
+ on_tool_result: ->(result, _tool_use) { puts " Result: #{result.content.to_s.slice(0, 60)}" },
221
+ on_result: ->(r) { puts "\nCost: $#{r.total_cost_usd}" },
222
+ on_message: ->(msg) { $stderr.puts "[#{msg.type}]" }
223
+ )
224
+ ```
225
+
226
+ ### Permission Handling
227
+
228
+ The `on_permission` parameter controls how tool permission requests are handled:
229
+
230
+ - **`:queue`** (default) -- Permission requests are queued. Poll with `conversation.pending_permission`.
231
+ - **`:default`**, **`:accept_edits`**, **`:plan`**, **`:bypass_permissions`**, **`:dont_ask`** -- Maps to CLI permission modes.
232
+ - **A callable** (Proc/Lambda) -- Used as `can_use_tool` callback.
233
+ - **A `PermissionPolicy`** -- Compiled to a `can_use_tool` callback.
234
+
235
+ ```ruby
236
+ # Queue mode (default) -- poll for permissions
237
+ conversation = ClaudeAgent::Conversation.new
238
+ # ... in a UI loop:
239
+ if request = conversation.pending_permission
240
+ show_dialog(request)
241
+ end
242
+
243
+ # Callable mode
244
+ conversation = ClaudeAgent::Conversation.new(
245
+ on_permission: ->(name, input, context) {
246
+ ClaudeAgent::PermissionResultAllow.new
247
+ }
248
+ )
249
+
250
+ # Symbol mode
251
+ conversation = ClaudeAgent::Conversation.new(on_permission: :accept_edits)
252
+ ```
253
+
254
+ ## Tool Activity Timeline
255
+
256
+ After each turn, the conversation builds `ToolActivity` entries from tool use/result pairs. These form a unified timeline across all turns with timing information.
257
+
258
+ ```ruby
259
+ ClaudeAgent::Conversation.open(max_turns: 10) do |c|
260
+ c.say("Read the config, fix the bug, and write tests")
261
+
262
+ c.tool_activity.each do |activity|
263
+ status = activity.error? ? "FAILED" : "OK"
264
+ duration = activity.duration ? "#{activity.duration.round(2)}s" : "n/a"
265
+ puts "#{activity.display_label} [#{status}] (#{duration}) -- turn #{activity.turn_index}"
266
+ end
267
+ end
268
+ ```
269
+
270
+ ### ToolActivity Accessors
271
+
272
+ Each `ToolActivity` is an immutable `Data.define` object built after a turn completes.
273
+
274
+ | Method | Return Type | Description |
275
+ |-----------------|------------------------|---------------------------------------------------------------|
276
+ | `name` | `String` | Tool name (e.g., `"Read"`, `"Write"`, `"Bash"`). |
277
+ | `display_label` | `String` | Human-readable label. |
278
+ | `summary(max:)` | `String` | Detailed summary, truncated to `max` characters (default 60). |
279
+ | `file_path` | `String, nil` | File path if this is a file-based tool. |
280
+ | `id` | `String` | Tool use ID. |
281
+ | `tool_use` | `ToolUseBlock` | The original tool use block. |
282
+ | `tool_result` | `ToolResultBlock, nil` | The matching result block (nil if not yet received). |
283
+ | `turn_index` | `Integer` | Which turn this tool was used in (zero-indexed). |
284
+ | `started_at` | `Time, nil` | When the tool use was detected. |
285
+ | `completed_at` | `Time, nil` | When the tool result was received. |
286
+ | `duration` | `Float, nil` | Duration in seconds (nil if timing not available). |
287
+ | `error?` | `Boolean` | Whether the tool produced an error result. |
288
+ | `complete?` | `Boolean` | Whether the tool execution is complete (has a result). |
289
+
290
+ ## Live Tool Tracking
291
+
292
+ For real-time UIs that need to show tool progress as it happens (spinners, progress bars, status indicators), enable live tracking with `track_tools: true`.
293
+
294
+ ```ruby
295
+ conversation = ClaudeAgent::Conversation.new(track_tools: true)
296
+ tracker = conversation.tool_tracker
297
+ ```
298
+
299
+ The `ToolActivityTracker` is an `Enumerable` collection of `LiveToolActivity` entries that updates in real time as tools start, progress, and complete. It resets at the start of each call to `say`.
300
+
301
+ ### Registering Tracker Callbacks
302
+
303
+ ```ruby
304
+ tracker = conversation.tool_tracker
305
+
306
+ tracker.on_start do |entry|
307
+ puts "Started: #{entry.display_label}"
308
+ end
309
+
310
+ tracker.on_progress do |entry|
311
+ puts " #{entry.name}: #{entry.elapsed&.round(1)}s elapsed..."
312
+ end
313
+
314
+ tracker.on_complete do |entry|
315
+ status = entry.error? ? "FAILED" : "done"
316
+ puts "Finished: #{entry.display_label} (#{status})"
317
+ end
318
+
319
+ # Catch-all -- fires in addition to specific callbacks
320
+ tracker.on_change do |event, entry|
321
+ # event is :started, :completed, or :progress
322
+ log("#{event}: #{entry.id}")
323
+ end
324
+ ```
325
+
326
+ ### Querying Tracker State
327
+
328
+ ```ruby
329
+ tracker.running # => Array<LiveToolActivity> currently in progress
330
+ tracker.done # => Array<LiveToolActivity> completed successfully
331
+ tracker.errored # => Array<LiveToolActivity> completed with errors
332
+
333
+ tracker.size # => Integer total count
334
+ tracker.empty? # => Boolean
335
+
336
+ tracker.find_by_id("tool_123") # => LiveToolActivity or nil
337
+ tracker["tool_123"] # => same as find_by_id
338
+
339
+ tracker.each { |entry| render(entry) }
340
+ ```
341
+
342
+ ### LiveToolActivity
343
+
344
+ Unlike `ToolActivity` (immutable, built after a turn completes), `LiveToolActivity` is mutable and tracks status changes as they happen.
345
+
346
+ | Method | Return Type | Description |
347
+ |-----------------|------------------------|---------------------------------------------------------|
348
+ | `id` | `String` | Tool use ID. |
349
+ | `name` | `String` | Tool name. |
350
+ | `input` | `Hash` | Tool input parameters. |
351
+ | `display_label` | `String` | Human-readable label. |
352
+ | `summary(max:)` | `String` | Detailed summary. |
353
+ | `file_path` | `String, nil` | File path if applicable. |
354
+ | `tool_use` | `ToolUseBlock` | The tool use block. |
355
+ | `tool_result` | `ToolResultBlock, nil` | The tool result (nil while running). |
356
+ | `status` | `Symbol` | `:running`, `:done`, or `:error`. |
357
+ | `started_at` | `Time` | When the tool started. |
358
+ | `elapsed` | `Float, nil` | Elapsed time in seconds (updated by progress events). |
359
+ | `running?` | `Boolean` | Whether the tool is currently running. |
360
+ | `done?` | `Boolean` | Whether the tool completed successfully. |
361
+ | `error?` | `Boolean` | Whether the tool completed with an error. |
362
+ | `complete?` | `Boolean` | Whether the tool execution is complete (done or error). |
363
+
364
+ ## Conversation Accessors
365
+
366
+ These accessors are available on the `Conversation` object itself.
367
+
368
+ | Method | Return Type | Description |
369
+ |------------------------|----------------------------|------------------------------------------------------------|
370
+ | `turns` | `Array<TurnResult>` | All completed turns. |
371
+ | `messages` | `Array<Message>` | All messages across all turns. |
372
+ | `tool_activity` | `Array<ToolActivity>` | Unified tool timeline across all turns. |
373
+ | `tool_tracker` | `ToolActivityTracker, nil` | Live tool tracker (nil unless `track_tools: true`). |
374
+ | `total_cost` | `Float` | Total cost across all turns (session-cumulative from CLI). |
375
+ | `session_id` | `String, nil` | Session ID from the most recent turn. |
376
+ | `usage` | `CumulativeUsage` | Cumulative usage stats. |
377
+ | `open?` | `Boolean` | Whether the conversation is open (client connected). |
378
+ | `closed?` | `Boolean` | Whether the conversation has been closed. |
379
+ | `pending_permission` | `PermissionRequest, nil` | Next pending permission request (non-blocking poll). |
380
+ | `pending_permissions?` | `Boolean` | Whether any permission requests are pending. |
381
+
382
+ ```ruby
383
+ ClaudeAgent::Conversation.open(max_turns: 10) do |c|
384
+ c.say("Refactor the auth module")
385
+ c.say("Now add integration tests")
386
+
387
+ puts "Turns: #{c.turns.size}"
388
+ puts "Messages: #{c.messages.size}"
389
+ puts "Tools: #{c.tool_activity.size}"
390
+ puts "Session: #{c.session_id}"
391
+ puts "Total cost: $#{c.total_cost}"
392
+ puts "Input tokens: #{c.usage.input_tokens}"
393
+ puts "Output tokens: #{c.usage.output_tokens}"
394
+ end
395
+ ```
396
+
397
+ ## Resuming a Conversation
398
+
399
+ Resume a previous conversation by session ID. The CLI restores the conversation context from the session transcript.
400
+
401
+ ### `Conversation.resume`
402
+
403
+ ```ruby
404
+ conversation = ClaudeAgent::Conversation.resume("session-abc-123")
405
+ turn = conversation.say("Continue where we left off")
406
+ puts turn.text
407
+ conversation.close
408
+ ```
409
+
410
+ ### `ClaudeAgent.resume_conversation`
411
+
412
+ Module-level convenience that delegates to `Conversation.resume`.
413
+
414
+ ```ruby
415
+ conversation = ClaudeAgent.resume_conversation("session-abc-123",
416
+ max_turns: 5,
417
+ on_text: ->(text) { print text }
418
+ )
419
+ turn = conversation.say("What did we discuss last time?")
420
+ conversation.close
421
+ ```
422
+
423
+ Both methods accept the same keyword arguments as `Conversation.new` for callbacks and options.
424
+
425
+ ## Cumulative Usage
426
+
427
+ The `CumulativeUsage` object tracks token counts, cost, and duration across all turns in a conversation.
428
+
429
+ Access it via `conversation.usage`:
430
+
431
+ ```ruby
432
+ ClaudeAgent::Conversation.open do |c|
433
+ c.say("Hello")
434
+ c.say("Follow up question")
435
+
436
+ usage = c.usage
437
+ puts "Input tokens: #{usage.input_tokens}"
438
+ puts "Output tokens: #{usage.output_tokens}"
439
+ puts "Cache read: #{usage.cache_read_input_tokens}"
440
+ puts "Cache create: #{usage.cache_creation_input_tokens}"
441
+ puts "Total cost: $#{usage.total_cost_usd}"
442
+ puts "Turns: #{usage.num_turns}"
443
+ puts "Duration: #{usage.duration_ms}ms"
444
+ puts "API duration: #{usage.duration_api_ms}ms"
445
+ end
446
+ ```
447
+
448
+ ### CumulativeUsage Fields
449
+
450
+ | Field | Type | Description |
451
+ |-------------------------------|-----------|--------------------------------------------------------------------------------|
452
+ | `input_tokens` | `Integer` | Sum of input tokens across all turns. |
453
+ | `output_tokens` | `Integer` | Sum of output tokens across all turns. |
454
+ | `cache_read_input_tokens` | `Integer` | Sum of cache-read input tokens across all turns. |
455
+ | `cache_creation_input_tokens` | `Integer` | Sum of cache-creation input tokens across all turns. |
456
+ | `total_cost_usd` | `Float` | Session-cumulative cost from the CLI (not summed -- replaced each turn). |
457
+ | `num_turns` | `Integer` | Session-cumulative turn count from the CLI (not summed -- replaced each turn). |
458
+ | `duration_ms` | `Integer` | Sum of wall-clock duration across all turns. |
459
+ | `duration_api_ms` | `Integer` | Sum of API-only duration across all turns. |
460
+
461
+ Token counts are summed across turns because the CLI reports per-turn values. Cost and turn count are session-cumulative values from the CLI and are replaced (not summed) on each result.
data/docs/errors.md ADDED
@@ -0,0 +1,127 @@
1
+ # Error Handling
2
+
3
+ All errors inherit from `ClaudeAgent::Error`, which inherits from `StandardError`. You can rescue the base class to catch any SDK error, or rescue specific subclasses for targeted handling.
4
+
5
+ ## Error Hierarchy
6
+
7
+ ```
8
+ StandardError
9
+ ClaudeAgent::Error
10
+ CLINotFoundError — Claude Code CLI binary not found
11
+ CLIVersionError — CLI version below MINIMUM_VERSION ("2.0.0")
12
+ CLIConnectionError — connection to CLI process failed
13
+ ProcessError — CLI process exited with error
14
+ JSONDecodeError — JSON parsing failed
15
+ MessageParseError — message structure could not be parsed
16
+ TimeoutError — control protocol request timed out
17
+ ConfigurationError — invalid option provided
18
+ NotFoundError — resource not found (e.g., Session.retrieve)
19
+ AbortError — operation aborted/cancelled
20
+ ```
21
+
22
+ ## Error Details
23
+
24
+ | Error | Attributes | Raised When |
25
+ |----------------------|---------------------------------|---------------------------------------------------------------------------------|
26
+ | `CLINotFoundError` | -- | CLI binary not at expected path |
27
+ | `CLIVersionError` | -- | `claude -v` returns < 2.0.0 |
28
+ | `CLIConnectionError` | -- | Pipe broken, stdin/stdout closed, already/not connected |
29
+ | `ProcessError` | `exit_code`, `stderr` | CLI process exits non-zero |
30
+ | `JSONDecodeError` | `raw_content` | Malformed JSON from CLI, buffer overflow |
31
+ | `MessageParseError` | `raw_message` | Message structure unrecognizable |
32
+ | `TimeoutError` | `request_id`, `timeout_seconds` | Control protocol request exceeds deadline |
33
+ | `ConfigurationError` | -- | Invalid `Options` values (bad permission_mode, non-callable can_use_tool, etc.) |
34
+ | `NotFoundError` | -- | `Session.retrieve` or `Session.info` for nonexistent session |
35
+ | `AbortError` | `partial_turn` | `AbortController#abort` called, or session closed mid-turn |
36
+
37
+ ## Handling Pattern
38
+
39
+ ```ruby
40
+ require "claude_agent"
41
+
42
+ begin
43
+ turn = ClaudeAgent.ask("Fix the failing tests")
44
+ puts turn.text
45
+
46
+ rescue ClaudeAgent::CLINotFoundError
47
+ # Install or update PATH
48
+ abort "Claude Code CLI is not installed."
49
+
50
+ rescue ClaudeAgent::CLIVersionError
51
+ # Upgrade CLI
52
+ abort "Claude Code CLI is too old. Run: claude update"
53
+
54
+ rescue ClaudeAgent::CLIConnectionError => e
55
+ # Pipe broken, process died, etc.
56
+ $stderr.puts "Connection lost: #{e.message}"
57
+
58
+ rescue ClaudeAgent::ProcessError => e
59
+ # CLI exited with an error code
60
+ $stderr.puts "CLI failed (exit #{e.exit_code}): #{e.stderr}"
61
+
62
+ rescue ClaudeAgent::JSONDecodeError => e
63
+ # Corrupted output from CLI
64
+ $stderr.puts "Bad JSON: #{e.raw_content&.slice(0, 200)}"
65
+
66
+ rescue ClaudeAgent::MessageParseError => e
67
+ # Unexpected message structure
68
+ $stderr.puts "Unparseable message: #{e.raw_message.inspect}"
69
+
70
+ rescue ClaudeAgent::TimeoutError => e
71
+ # Control protocol deadline exceeded
72
+ $stderr.puts "Timed out after #{e.timeout_seconds}s (request: #{e.request_id})"
73
+
74
+ rescue ClaudeAgent::ConfigurationError => e
75
+ # Bad options (caught at Options construction time)
76
+ abort "Invalid config: #{e.message}"
77
+
78
+ rescue ClaudeAgent::NotFoundError => e
79
+ # Session.retrieve for a session that does not exist
80
+ $stderr.puts "Not found: #{e.message}"
81
+
82
+ rescue ClaudeAgent::AbortError => e
83
+ # Operation cancelled -- see next section for partial results
84
+ $stderr.puts "Aborted: #{e.message}"
85
+
86
+ rescue ClaudeAgent::Error => e
87
+ # Catch-all for any SDK error not handled above
88
+ $stderr.puts "ClaudeAgent error: #{e.message}"
89
+ end
90
+ ```
91
+
92
+ ## AbortError and Partial Results
93
+
94
+ When a turn is aborted mid-flight (via `AbortController` or session close), the `AbortError` carries a `partial_turn` -- a `TurnResult` containing whatever was accumulated before cancellation.
95
+
96
+ ```ruby
97
+ controller = ClaudeAgent::AbortController.new
98
+
99
+ # Abort after 5 seconds
100
+ Thread.new { sleep 5; controller.abort("Taking too long") }
101
+
102
+ begin
103
+ conversation = ClaudeAgent::Conversation.new(
104
+ options: ClaudeAgent::Options.new(abort_controller: controller)
105
+ )
106
+ turn = conversation.say("Refactor the entire codebase")
107
+ rescue ClaudeAgent::AbortError => e
108
+ turn = e.partial_turn
109
+
110
+ if turn
111
+ # Text accumulated from assistant messages (or streamed fragments)
112
+ puts "Partial text: #{turn.text}" unless turn.text.empty?
113
+
114
+ # Tools that were invoked before the abort
115
+ turn.tool_uses.each do |tool|
116
+ puts "Tool called: #{tool.name}"
117
+ end
118
+
119
+ # All raw messages received so far
120
+ puts "Messages received: #{turn.messages.size}"
121
+ else
122
+ puts "Aborted before any messages were received."
123
+ end
124
+ end
125
+ ```
126
+
127
+ `partial_turn` is `nil` if the abort happened before any messages arrived. Always check before accessing its fields.