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/events.md ADDED
@@ -0,0 +1,225 @@
1
+ # Events
2
+
3
+ The `EventHandler` dispatches typed events as messages flow through a conversation turn, replacing manual `case` statements over raw message types.
4
+
5
+ Three event layers fire for every message, in order:
6
+
7
+ 1. **Catch-all** -- `:message` fires for every message regardless of type.
8
+ 2. **Type-based** -- the message's `type` fires (e.g., `:assistant`, `:result`, `:stream_event`).
9
+ 3. **Decomposed** -- convenience events extracted from rich content (`:text`, `:thinking`, `:tool_use`, `:tool_result`).
10
+
11
+ ## Quick start
12
+
13
+ ### Block DSL with `EventHandler.define`
14
+
15
+ Build a handler in a single expression using the block DSL. The block is evaluated in the context of the new handler, so `on_*` methods are available directly:
16
+
17
+ ```ruby
18
+ handler = ClaudeAgent::EventHandler.define do
19
+ on_text { |text| print text }
20
+ on_result { |r| puts "\nCost: $#{r.total_cost_usd}" }
21
+ on_tool_use { |tool| puts "Tool: #{tool.display_label}" }
22
+ end
23
+ ```
24
+
25
+ Pass the handler to `query_turn`:
26
+
27
+ ```ruby
28
+ turn = ClaudeAgent.query_turn(prompt: "Explain Ruby", events: handler)
29
+ ```
30
+
31
+ ### Traditional construction
32
+
33
+ Create an instance and chain `on_*` calls. Each returns `self`, so calls can be chained:
34
+
35
+ ```ruby
36
+ handler = ClaudeAgent::EventHandler.new
37
+ .on_text { |text| print text }
38
+ .on_thinking { |thought| $stderr.puts "[thinking] #{thought}" }
39
+ .on_tool_use { |tool| puts "Using: #{tool.display_label}" }
40
+ .on_result { |r| puts "\nDone (cost=$#{r.total_cost_usd})" }
41
+ ```
42
+
43
+ Or register handlers one at a time:
44
+
45
+ ```ruby
46
+ handler = ClaudeAgent::EventHandler.new
47
+ handler.on_text { |text| print text }
48
+ handler.on_result { |r| puts "Done!" }
49
+ ```
50
+
51
+ ### Via Conversation
52
+
53
+ `Conversation.new` accepts `on_*` keyword arguments for any event. These are registered on the underlying `Client` and persist across turns:
54
+
55
+ ```ruby
56
+ conversation = ClaudeAgent::Conversation.new(
57
+ model: "claude-sonnet-4-5-20250514",
58
+ on_text: ->(text) { print text },
59
+ on_thinking: ->(thought) { $stderr.puts thought },
60
+ on_tool_use: ->(tool) { puts "Tool: #{tool.display_label}" },
61
+ on_tool_result: ->(result, tool_use) { puts "Result for #{tool_use&.name}" },
62
+ on_result: ->(r) { puts "\nCost: $#{r.total_cost_usd}" },
63
+ on_message: ->(msg) { log(msg) },
64
+ on_stream_event: ->(evt) { handle_stream(evt) },
65
+ on_status: ->(status) { show_status(status) },
66
+ on_tool_progress: ->(prog) { update_spinner(prog) }
67
+ )
68
+
69
+ turn = conversation.say("Fix the bug in auth.rb")
70
+ conversation.close
71
+ ```
72
+
73
+ `on_stream` is an alias for `on_text`:
74
+
75
+ ```ruby
76
+ conversation = ClaudeAgent::Conversation.new(
77
+ on_stream: ->(text) { print text }
78
+ )
79
+ ```
80
+
81
+ ### Via Client
82
+
83
+ Register event handlers directly on a `Client`. Handlers persist across turns and fire automatically during `receive_turn` and `send_and_receive`:
84
+
85
+ ```ruby
86
+ client = ClaudeAgent::Client.new
87
+ client.on_text { |text| print text }
88
+ client.on_tool_use { |tool| puts "Using: #{tool.display_label}" }
89
+ client.on_result { |r| puts "\nDone!" }
90
+
91
+ client.connect
92
+ turn = client.send_and_receive("Fix the bug")
93
+ client.disconnect
94
+ ```
95
+
96
+ The generic `on` method also works:
97
+
98
+ ```ruby
99
+ client.on(:text) { |text| print text }
100
+ client.on(:stream_event) { |evt| handle_stream(evt) }
101
+ ```
102
+
103
+ ### Via one-shot query
104
+
105
+ Pass an `events:` keyword to `ClaudeAgent.query_turn` for one-shot queries:
106
+
107
+ ```ruby
108
+ events = ClaudeAgent::EventHandler.define do
109
+ on_text { |text| print text }
110
+ on_result { |r| puts "\nCost: $#{r.total_cost_usd}" }
111
+ end
112
+
113
+ turn = ClaudeAgent.query_turn(prompt: "Explain concurrency", events: events)
114
+ puts turn.text
115
+ ```
116
+
117
+ The handler's `reset!` is called automatically when the turn completes.
118
+
119
+ ## Standalone usage
120
+
121
+ Create a handler and dispatch messages manually with `handle`:
122
+
123
+ ```ruby
124
+ handler = ClaudeAgent::EventHandler.new
125
+ handler.on_text { |text| print text }
126
+ handler.on_tool_use { |tool| log_tool(tool) }
127
+ handler.on_tool_result { |result, tool_use| log_result(result, tool_use) }
128
+
129
+ client.receive_response.each { |msg| handler.handle(msg) }
130
+ handler.reset!
131
+ ```
132
+
133
+ `handle` fires events in order:
134
+
135
+ 1. `:message` (catch-all)
136
+ 2. `message.type` (type-based)
137
+ 3. Decomposed events extracted from the message content
138
+
139
+ Call `reset!` between turns to clear internal state (pending tool use tracking). When used via `Client` or `query_turn`, this is called automatically.
140
+
141
+ ## Tool use and tool result pairing
142
+
143
+ The handler tracks pending tool uses internally. When a `:tool_result` event fires, it receives both the result block and the original tool use block that triggered it:
144
+
145
+ ```ruby
146
+ handler.on_tool_use do |tool|
147
+ puts "Requested: #{tool.name} (#{tool.id})"
148
+ end
149
+
150
+ handler.on_tool_result do |result, tool_use|
151
+ puts "Result for: #{tool_use&.name}" # tool_use is the matched ToolUseBlock
152
+ puts "Error: #{result.is_error}" if result.is_error
153
+ end
154
+ ```
155
+
156
+ The `tool_use` argument is `nil` if no matching tool use was found (which should not happen in normal operation).
157
+
158
+ ## Utility methods
159
+
160
+ ### `has_handlers?`
161
+
162
+ Returns whether any handlers have been registered:
163
+
164
+ ```ruby
165
+ handler = ClaudeAgent::EventHandler.new
166
+ handler.has_handlers? # => false
167
+
168
+ handler.on_text { |t| print t }
169
+ handler.has_handlers? # => true
170
+ ```
171
+
172
+ ### `reset!`
173
+
174
+ Clears turn-level tracking state (pending tool uses). Does not remove registered handlers:
175
+
176
+ ```ruby
177
+ handler.reset! # Clear pending tool uses between turns
178
+ ```
179
+
180
+ ## Event reference
181
+
182
+ ### Meta events
183
+
184
+ | Event | Receives | Description |
185
+ |------------|--------------------|-------------------------------------|
186
+ | `:message` | Any message object | Fires for every message (catch-all) |
187
+
188
+ ### Type-based events
189
+
190
+ Each fires when a message with the matching `type` is received. The handler receives the full message object.
191
+
192
+ | Event | Description |
193
+ |-------------------------|-------------------------------------------------------|
194
+ | `:user` | User message |
195
+ | `:assistant` | Assistant message (contains text, thinking, tool use) |
196
+ | `:system` | System message (init, session info) |
197
+ | `:result` | End-of-turn result (cost, usage, session ID) |
198
+ | `:stream_event` | Raw stream event |
199
+ | `:compact_boundary` | Context window compaction boundary |
200
+ | `:status` | Status update |
201
+ | `:tool_progress` | Tool execution progress |
202
+ | `:hook_response` | Hook execution response |
203
+ | `:auth_status` | Authentication status |
204
+ | `:task_notification` | Background task notification |
205
+ | `:hook_started` | Hook execution started |
206
+ | `:hook_progress` | Hook execution progress |
207
+ | `:tool_use_summary` | Tool use summary |
208
+ | `:task_started` | Background task started |
209
+ | `:task_progress` | Background task progress |
210
+ | `:rate_limit_event` | Rate limit information |
211
+ | `:prompt_suggestion` | Suggested follow-up prompt |
212
+ | `:files_persisted` | File checkpoint persisted |
213
+ | `:elicitation_complete` | Elicitation completed |
214
+ | `:local_command_output` | Local command output |
215
+
216
+ ### Decomposed events
217
+
218
+ Extracted from the content of assistant and user messages. These fire after the type-based event.
219
+
220
+ | Event | Receives | Description |
221
+ |----------------|--------------------------------------------|-------------------------------------------------------------------------|
222
+ | `:text` | `String` | Concatenated text from an assistant message |
223
+ | `:thinking` | `String` | Concatenated thinking from an assistant message |
224
+ | `:tool_use` | `ToolUseBlock` or `ServerToolUseBlock` | A tool use request from an assistant message |
225
+ | `:tool_result` | `ToolResultBlock`, `ToolUseBlock` or `nil` | A tool result from a user message, paired with its originating tool use |
@@ -0,0 +1,310 @@
1
+ # Getting Started
2
+
3
+ This guide walks you through the ClaudeAgent Ruby SDK from first install to multi-turn conversations. Each section builds on the last, starting with the simplest API.
4
+
5
+ ## Requirements
6
+
7
+ - **Ruby 3.2+** (the SDK uses `Data.define` for immutable types)
8
+ - **Claude Code CLI v2.0.0+** ([install guide](https://code.claude.com/docs/en/getting-started))
9
+
10
+ Verify both are available:
11
+
12
+ ```bash
13
+ ruby -v # >= 3.2.0
14
+ claude -v # >= 2.0.0
15
+ ```
16
+
17
+ ## Installation
18
+
19
+ Add to your Gemfile:
20
+
21
+ ```ruby
22
+ gem "claude_agent"
23
+ ```
24
+
25
+ Then:
26
+
27
+ ```bash
28
+ bundle install
29
+ ```
30
+
31
+ Or install directly:
32
+
33
+ ```bash
34
+ gem install claude_agent
35
+ ```
36
+
37
+ ## Your First Query
38
+
39
+ The fastest way to get a response is `ClaudeAgent.ask`. It sends a single prompt and returns a `TurnResult`:
40
+
41
+ ```ruby
42
+ require "claude_agent"
43
+
44
+ turn = ClaudeAgent.ask("What is the capital of France?")
45
+ puts turn.text
46
+ # => "The capital of France is Paris."
47
+ ```
48
+
49
+ `TurnResult` gives you structured access to everything that happened during the turn:
50
+
51
+ ```ruby
52
+ turn = ClaudeAgent.ask("Explain Ruby's GIL in one sentence.")
53
+
54
+ puts turn.text # Combined text from all assistant messages
55
+ puts turn.cost # Total cost in USD (e.g., 0.003)
56
+ puts turn.duration_ms # Wall-clock time in milliseconds
57
+ puts turn.session_id # Session ID (for resuming later)
58
+ puts turn.model # Model that handled the request
59
+ puts turn.tool_uses.size # Number of tools Claude invoked
60
+ puts turn.success? # true if no errors
61
+ ```
62
+
63
+ ### Passing Options
64
+
65
+ Override defaults with keyword arguments:
66
+
67
+ ```ruby
68
+ turn = ClaudeAgent.ask("Fix the bug in auth.rb",
69
+ model: "opus",
70
+ max_turns: 5,
71
+ permission_mode: "acceptEdits"
72
+ )
73
+ ```
74
+
75
+ ## Streaming
76
+
77
+ Pass a block to `ask` to receive each message as it arrives:
78
+
79
+ ```ruby
80
+ turn = ClaudeAgent.ask("Explain how TCP works") do |message|
81
+ case message
82
+ when ClaudeAgent::AssistantMessage
83
+ print message.text
84
+ when ClaudeAgent::ResultMessage
85
+ puts "\n--- Done (cost: $#{message.total_cost_usd}) ---"
86
+ end
87
+ end
88
+ ```
89
+
90
+ The block receives every message in the protocol stream. The return value is still a `TurnResult` with the full accumulated data.
91
+
92
+ ### Streaming with Callbacks
93
+
94
+ For cleaner streaming, use `on_stream` to receive just the text:
95
+
96
+ ```ruby
97
+ turn = ClaudeAgent.ask("Write a haiku about Ruby",
98
+ on_stream: ->(text) { print text }
99
+ )
100
+ puts "\nCost: $#{turn.cost}"
101
+ ```
102
+
103
+ ## Multi-Turn Conversations
104
+
105
+ Use `ClaudeAgent.chat` for back-and-forth conversations. The block form auto-closes the connection when done:
106
+
107
+ ```ruby
108
+ ClaudeAgent.chat do |c|
109
+ c.say("What files are in the current directory?")
110
+ c.say("Which one is the largest?")
111
+ c.say("Show me the first 10 lines of that file.")
112
+
113
+ puts "Total cost: $#{c.total_cost}"
114
+ end
115
+ ```
116
+
117
+ Each call to `say` returns a `TurnResult`:
118
+
119
+ ```ruby
120
+ ClaudeAgent.chat(model: "opus") do |c|
121
+ turn = c.say("Write a function that reverses a string")
122
+ puts turn.text
123
+
124
+ turn = c.say("Now add error handling")
125
+ puts turn.text
126
+ puts "Tools used: #{turn.tool_uses.map(&:name).join(", ")}"
127
+ end
128
+ ```
129
+
130
+ ### Without a Block
131
+
132
+ If you need the conversation to outlive a block, skip it:
133
+
134
+ ```ruby
135
+ conversation = ClaudeAgent.chat(model: "sonnet")
136
+ conversation.say("Hello")
137
+ conversation.say("Tell me more")
138
+ conversation.close # Always close when done
139
+ ```
140
+
141
+ ### Streaming in Conversations
142
+
143
+ Pass `on_stream` to print text as it arrives:
144
+
145
+ ```ruby
146
+ ClaudeAgent.chat(on_stream: ->(text) { print text }) do |c|
147
+ c.say("What is 2+2?")
148
+ puts # newline after streamed output
149
+ c.say("Now multiply that by 10")
150
+ puts
151
+ end
152
+ ```
153
+
154
+ Or stream per-turn with a block on `say`:
155
+
156
+ ```ruby
157
+ ClaudeAgent.chat do |c|
158
+ c.say("Explain monads") do |message|
159
+ print message.text if message.is_a?(ClaudeAgent::AssistantMessage)
160
+ end
161
+ end
162
+ ```
163
+
164
+ ## Global Configuration
165
+
166
+ Set defaults that apply to every `ask` and `chat` call:
167
+
168
+ ```ruby
169
+ ClaudeAgent.model = "opus"
170
+ ClaudeAgent.max_turns = 10
171
+ ClaudeAgent.permission_mode = "acceptEdits"
172
+ ClaudeAgent.system_prompt = "You are a helpful coding assistant."
173
+ ```
174
+
175
+ Or configure in bulk:
176
+
177
+ ```ruby
178
+ ClaudeAgent.configure do |c|
179
+ c.model = "opus"
180
+ c.max_turns = 10
181
+ c.permission_mode = "acceptEdits"
182
+ c.system_prompt = "You are a helpful coding assistant."
183
+ c.max_budget_usd = 1.0
184
+ c.cwd = "/path/to/project"
185
+ end
186
+ ```
187
+
188
+ Per-request keyword arguments always override global defaults:
189
+
190
+ ```ruby
191
+ ClaudeAgent.model = "sonnet"
192
+
193
+ # This request uses opus despite the global default
194
+ turn = ClaudeAgent.ask("Complex question", model: "opus")
195
+ ```
196
+
197
+ Reset to defaults with:
198
+
199
+ ```ruby
200
+ ClaudeAgent.reset_config!
201
+ ```
202
+
203
+ See [Configuration](configuration.md) for the full list of options.
204
+
205
+ ## The Conversation Class
206
+
207
+ For full control, use `ClaudeAgent::Conversation` directly. It supports callbacks, permission handling, tool tracking, and session management.
208
+
209
+ ```ruby
210
+ conversation = ClaudeAgent::Conversation.open(
211
+ model: "opus",
212
+ max_turns: 10,
213
+ permission_mode: "acceptEdits",
214
+ on_stream: ->(text) { print text },
215
+ on_tool_use: ->(tool) { puts "\nUsing tool: #{tool.name}" },
216
+ on_result: ->(result) { puts "\nTurn cost: $#{result.total_cost_usd}" }
217
+ ) do |c|
218
+ c.say("Read the file config.rb and explain what it does")
219
+ c.say("Refactor it to use keyword arguments")
220
+
221
+ puts "Session: #{c.session_id}"
222
+ puts "Total cost: $#{c.total_cost}"
223
+ puts "Turns: #{c.turns.size}"
224
+ end
225
+ ```
226
+
227
+ ### Available Callbacks
228
+
229
+ | Callback | Receives | When |
230
+ |------------------|---------------------------------|-------------------------------------------|
231
+ | `on_stream` | `String` | Each text chunk as it streams |
232
+ | `on_text` | `String` | Full text from each assistant message |
233
+ | `on_thinking` | `String` | Thinking/reasoning content |
234
+ | `on_tool_use` | `ToolUseBlock` | Claude requests a tool |
235
+ | `on_tool_result` | `ToolResultBlock, ToolUseBlock` | Tool result arrives (paired with request) |
236
+ | `on_result` | `ResultMessage` | Turn completes |
237
+ | `on_message` | `Message` | Every message (catch-all) |
238
+
239
+ ### Permission Handling
240
+
241
+ By default, Conversation queues permission requests for you to handle. You can also set a mode or provide a custom callback:
242
+
243
+ ```ruby
244
+ # Use a CLI permission mode
245
+ ClaudeAgent::Conversation.open(on_permission: :accept_edits) do |c|
246
+ c.say("Fix the typo in README.md")
247
+ end
248
+
249
+ # Or provide a lambda
250
+ ClaudeAgent::Conversation.open(
251
+ on_permission: ->(tool_name, input, ctx) {
252
+ ClaudeAgent::PermissionResultAllow.new
253
+ }
254
+ ) do |c|
255
+ c.say("Update the config file")
256
+ end
257
+ ```
258
+
259
+ For declarative permission rules, see [Permissions](permissions.md).
260
+
261
+ ## Resuming Sessions
262
+
263
+ Every turn returns a `session_id`. Save it to resume the conversation later:
264
+
265
+ ```ruby
266
+ # First session
267
+ session_id = nil
268
+ ClaudeAgent.chat do |c|
269
+ c.say("Let's work on the authentication module")
270
+ session_id = c.session_id
271
+ end
272
+
273
+ # Later — resume where you left off
274
+ ClaudeAgent.resume_conversation(session_id) do |c|
275
+ c.say("Continue with the tests we discussed")
276
+ end
277
+ ```
278
+
279
+ `resume_conversation` returns a `Conversation`, so it supports the same block form and callbacks.
280
+
281
+ You can also resume via `Conversation.resume` directly:
282
+
283
+ ```ruby
284
+ conversation = ClaudeAgent::Conversation.resume(session_id,
285
+ on_stream: ->(text) { print text }
286
+ )
287
+ conversation.say("Pick up where we left off")
288
+ conversation.close
289
+ ```
290
+
291
+ ### Listing Past Sessions
292
+
293
+ Browse previous sessions without spawning a CLI process:
294
+
295
+ ```ruby
296
+ sessions = ClaudeAgent.list_sessions(dir: "/path/to/project", limit: 10)
297
+ sessions.each do |session|
298
+ puts "#{session.session_id}: #{session.title} (#{session.updated_at})"
299
+ end
300
+ ```
301
+
302
+ ## What's Next
303
+
304
+ - [Configuration](configuration.md) -- full list of options, permission modes, and sandbox settings
305
+ - [Conversations](conversations.md) -- advanced multi-turn patterns, tool tracking, and abort handling
306
+ - [Permissions](permissions.md) -- declarative permission policies and the `can_use_tool` callback
307
+ - [Hooks](hooks.md) -- lifecycle hooks for tool use, session events, and more
308
+ - [Messages](messages.md) -- all message types, content blocks, and the event system
309
+ - [MCP Servers](mcp-servers.md) -- integrating external tools via Model Context Protocol
310
+ - [Sessions](sessions.md) -- listing, reading, forking, and managing past sessions