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.
- checksums.yaml +4 -4
- data/.claude/rules/conventions.md +66 -16
- data/CHANGELOG.md +20 -0
- data/CLAUDE.md +24 -4
- data/README.md +52 -1529
- data/SPEC.md +56 -29
- data/docs/architecture.md +339 -0
- data/docs/client.md +526 -0
- data/docs/configuration.md +571 -0
- data/docs/conversations.md +461 -0
- data/docs/errors.md +127 -0
- data/docs/events.md +225 -0
- data/docs/getting-started.md +310 -0
- data/docs/hooks.md +380 -0
- data/docs/logging.md +96 -0
- data/docs/mcp.md +308 -0
- data/docs/messages.md +871 -0
- data/docs/permissions.md +611 -0
- data/docs/queries.md +227 -0
- data/docs/sessions.md +335 -0
- data/lib/claude_agent/abort_controller.rb +24 -0
- data/lib/claude_agent/client/commands.rb +32 -0
- data/lib/claude_agent/client.rb +10 -4
- data/lib/claude_agent/configuration.rb +129 -0
- data/lib/claude_agent/control_protocol/commands.rb +28 -0
- data/lib/claude_agent/conversation.rb +37 -4
- data/lib/claude_agent/errors.rb +21 -4
- data/lib/claude_agent/event_handler.rb +14 -0
- data/lib/claude_agent/fork_session.rb +117 -0
- data/lib/claude_agent/hook_registry.rb +110 -0
- data/lib/claude_agent/hooks.rb +4 -0
- data/lib/claude_agent/mcp/server.rb +22 -0
- data/lib/claude_agent/mcp/tool.rb +24 -3
- data/lib/claude_agent/message.rb +93 -0
- data/lib/claude_agent/messages/streaming.rb +37 -0
- data/lib/claude_agent/options.rb +10 -0
- data/lib/claude_agent/permission_policy.rb +174 -0
- data/lib/claude_agent/permission_request.rb +17 -0
- data/lib/claude_agent/session.rb +100 -11
- data/lib/claude_agent/session_paths.rb +5 -2
- data/lib/claude_agent/turn_result.rb +20 -2
- data/lib/claude_agent/types/sessions.rb +8 -0
- data/lib/claude_agent/version.rb +1 -1
- data/lib/claude_agent.rb +187 -0
- data/sig/claude_agent.rbs +38 -1
- 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.
|