claude_agent 0.7.7 → 0.7.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README.md CHANGED
@@ -29,9 +29,26 @@ gem install claude_agent
29
29
 
30
30
  ## Quick Start
31
31
 
32
+ ### Conversation (Recommended)
33
+
34
+ The simplest way to have multi-turn conversations:
35
+
36
+ ```ruby
37
+ require "claude_agent"
38
+
39
+ ClaudeAgent::Conversation.open(
40
+ permission_mode: "acceptEdits",
41
+ on_stream: ->(text) { print text }
42
+ ) do |c|
43
+ c.say("Fix the bug in auth.rb")
44
+ c.say("Now add tests for the fix")
45
+ puts "\nTotal cost: $#{c.total_cost}"
46
+ end
47
+ ```
48
+
32
49
  ### One-Shot Query
33
50
 
34
- The simplest way to use Claude:
51
+ For single questions:
35
52
 
36
53
  ```ruby
37
54
  require "claude_agent"
@@ -46,21 +63,33 @@ ClaudeAgent.query(prompt: "What is the capital of France?").each do |message|
46
63
  end
47
64
  ```
48
65
 
66
+ ### One-Shot with TurnResult
67
+
68
+ Get a structured result without writing `case` statements:
69
+
70
+ ```ruby
71
+ require "claude_agent"
72
+
73
+ turn = ClaudeAgent.query_turn(prompt: "What is the capital of France?")
74
+ puts turn.text
75
+ puts "Cost: $#{turn.cost}"
76
+ puts "Model: #{turn.model}"
77
+ ```
78
+
49
79
  ### Interactive Client
50
80
 
51
- For multi-turn conversations:
81
+ For fine-grained control over the conversation:
52
82
 
53
83
  ```ruby
54
84
  require "claude_agent"
55
85
 
56
86
  ClaudeAgent::Client.open do |client|
57
- client.query("Remember the number 42")
58
- client.receive_response.each { |msg| } # Process first response
87
+ client.on_text { |text| print text }
59
88
 
60
- client.query("What number did I ask you to remember?")
61
- client.receive_response.each do |msg|
62
- puts msg.text if msg.is_a?(ClaudeAgent::AssistantMessage)
63
- end
89
+ turn = client.send_and_receive("Remember the number 42")
90
+ turn = client.send_and_receive("What number did I ask you to remember?")
91
+
92
+ puts "\nAnswer: #{turn.text}"
64
93
  end
65
94
  ```
66
95
 
@@ -84,6 +113,98 @@ ClaudeAgent.run_setup(trigger: :init, options: options)
84
113
  ClaudeAgent.run_setup(trigger: :maintenance)
85
114
  ```
86
115
 
116
+ ## Conversation API
117
+
118
+ The `Conversation` class manages the full lifecycle: auto-connects on first message, tracks multi-turn history, accumulates usage, and builds a unified tool activity timeline.
119
+
120
+ ### Basic Usage
121
+
122
+ ```ruby
123
+ conversation = ClaudeAgent.conversation(
124
+ model: "claude-sonnet-4-5-20250514",
125
+ permission_mode: "acceptEdits",
126
+ on_text: ->(text) { print text }
127
+ )
128
+
129
+ turn = conversation.say("Help me refactor this module")
130
+ puts turn.tool_uses.map(&:display_label) # ["Read lib/foo.rb", "Edit lib/foo.rb"]
131
+
132
+ turn = conversation.say("Now update the tests")
133
+ puts "Session cost: $#{conversation.total_cost}"
134
+
135
+ conversation.close
136
+ ```
137
+
138
+ ### Block Form
139
+
140
+ Automatically cleans up when the block exits:
141
+
142
+ ```ruby
143
+ ClaudeAgent::Conversation.open(
144
+ max_turns: 10,
145
+ on_tool_use: ->(tool) { puts " Using: #{tool.display_label}" }
146
+ ) do |c|
147
+ c.say("Implement the feature described in SPEC.md")
148
+ c.say("Run the tests and fix any failures")
149
+
150
+ puts "Tools used: #{c.tool_activity.size}"
151
+ puts "Total cost: $#{c.total_cost}"
152
+ end
153
+ ```
154
+
155
+ ### Resume a Previous Session
156
+
157
+ ```ruby
158
+ conversation = ClaudeAgent.resume_conversation("session-abc-123")
159
+ turn = conversation.say("Continue where we left off")
160
+ conversation.close
161
+ ```
162
+
163
+ ### Callbacks
164
+
165
+ Register callbacks for real-time event handling:
166
+
167
+ ```ruby
168
+ conversation = ClaudeAgent.conversation(
169
+ on_text: ->(text) { print text },
170
+ on_stream: ->(text) { print text }, # Alias for on_text
171
+ on_thinking: ->(thought) { puts "Thinking: #{thought}" },
172
+ on_tool_use: ->(tool) { puts "Tool: #{tool.display_label}" },
173
+ on_tool_result: ->(result) { puts "Result: #{result.content&.slice(0, 80)}" },
174
+ on_result: ->(result) { puts "Done! Cost: $#{result.total_cost_usd}" },
175
+ on_message: ->(msg) { log(msg) } # Catch-all
176
+ )
177
+ ```
178
+
179
+ ### Tool Activity Timeline
180
+
181
+ Track all tool executions across turns with timing:
182
+
183
+ ```ruby
184
+ ClaudeAgent::Conversation.open(permission_mode: "acceptEdits") do |c|
185
+ c.say("Refactor the auth module")
186
+
187
+ c.tool_activity.each do |activity|
188
+ puts "#{activity.display_label} (turn #{activity.turn_index})"
189
+ puts " Duration: #{activity.duration&.round(2)}s" if activity.duration
190
+ puts " Error!" if activity.error?
191
+ end
192
+ end
193
+ ```
194
+
195
+ ### Conversation Accessors
196
+
197
+ ```ruby
198
+ conversation.turns # Array of TurnResult objects
199
+ conversation.messages # All messages across all turns
200
+ conversation.tool_activity # Array of ToolActivity objects
201
+ conversation.total_cost # Total cost in USD
202
+ conversation.session_id # Session ID from most recent turn
203
+ conversation.usage # CumulativeUsage stats
204
+ conversation.open? # Whether conversation is connected
205
+ conversation.closed? # Whether conversation has been closed
206
+ ```
207
+
87
208
  ## Configuration
88
209
 
89
210
  Use `ClaudeAgent::Options` to customize behavior:
@@ -97,7 +218,13 @@ options = ClaudeAgent::Options.new(
97
218
  # Conversation limits
98
219
  max_turns: 10,
99
220
  max_budget_usd: 1.0,
100
- max_thinking_tokens: 10000,
221
+
222
+ # Extended thinking
223
+ thinking: { type: "enabled", budgetTokens: 10000 }, # or "adaptive" or "disabled"
224
+ # max_thinking_tokens: 10000, # Shorthand (when thinking not set)
225
+
226
+ # Response effort
227
+ effort: "high", # "low", "medium", "high", "max"
101
228
 
102
229
  # System prompt
103
230
  system_prompt: "You are a helpful coding assistant.",
@@ -108,8 +235,9 @@ options = ClaudeAgent::Options.new(
108
235
  allowed_tools: ["Read"],
109
236
  disallowed_tools: ["Write"],
110
237
 
111
- # Permission modes: "default", "acceptEdits", "plan", "delegate", "dontAsk", "bypassPermissions"
238
+ # Permission modes: "default", "acceptEdits", "plan", "dontAsk", "bypassPermissions"
112
239
  permission_mode: "acceptEdits",
240
+ permission_queue: true, # Enable queue-based permissions (see Permissions section)
113
241
 
114
242
  # Working directory for file operations
115
243
  cwd: "/path/to/project",
@@ -120,6 +248,7 @@ options = ClaudeAgent::Options.new(
120
248
 
121
249
  # Session management
122
250
  resume: "session-id",
251
+ session_id: "custom-uuid", # Custom conversation UUID
123
252
  continue_conversation: true,
124
253
  fork_session: true,
125
254
  persist_session: true, # Default: true
@@ -128,7 +257,14 @@ options = ClaudeAgent::Options.new(
128
257
  output_format: {
129
258
  type: "object",
130
259
  properties: { answer: { type: "string" } }
131
- }
260
+ },
261
+
262
+ # Prompt suggestions
263
+ prompt_suggestions: true,
264
+
265
+ # Debug logging (CLI-level)
266
+ debug: true,
267
+ debug_file: "/path/to/debug.log"
132
268
  )
133
269
 
134
270
  ClaudeAgent.query(prompt: "Help me refactor this code", options: options)
@@ -161,7 +297,13 @@ sandbox = ClaudeAgent::SandboxSettings.new(
161
297
  excluded_commands: ["docker", "kubectl"],
162
298
  network: ClaudeAgent::SandboxNetworkConfig.new(
163
299
  allowed_domains: ["api.example.com"],
164
- allow_local_binding: true
300
+ allow_local_binding: true,
301
+ allow_managed_domains_only: false
302
+ ),
303
+ filesystem: ClaudeAgent::SandboxFilesystemConfig.new(
304
+ allow_write: ["/tmp/*"],
305
+ deny_write: ["/etc/*"],
306
+ deny_read: ["/secrets/*"]
165
307
  ),
166
308
  ripgrep: ClaudeAgent::SandboxRipgrepConfig.new(
167
309
  command: "/usr/local/bin/rg"
@@ -190,9 +332,106 @@ agents = {
190
332
  options = ClaudeAgent::Options.new(agents: agents)
191
333
  ```
192
334
 
335
+ ## TurnResult
336
+
337
+ A `TurnResult` accumulates all messages from sending a prompt to receiving the final `ResultMessage`. It eliminates the need for `case` statements over raw message types.
338
+
339
+ ### Getting a TurnResult
340
+
341
+ ```ruby
342
+ # Via Conversation
343
+ turn = conversation.say("Fix the bug")
344
+
345
+ # Via Client
346
+ turn = client.send_and_receive("Fix the bug")
347
+
348
+ # Via one-shot query
349
+ turn = ClaudeAgent.query_turn(prompt: "Fix the bug")
350
+ ```
351
+
352
+ ### Accessors
353
+
354
+ ```ruby
355
+ # Text and thinking
356
+ turn.text # All text content concatenated
357
+ turn.thinking # All thinking content concatenated
358
+
359
+ # Tool usage
360
+ turn.tool_uses # Array of ToolUseBlock / ServerToolUseBlock
361
+ turn.tool_results # Array of ToolResultBlock / ServerToolResultBlock
362
+ turn.tool_executions # Array of { tool_use:, tool_result: } pairs
363
+
364
+ # Result data
365
+ turn.cost # Total cost in USD
366
+ turn.duration_ms # Wall-clock duration
367
+ turn.session_id # Session ID for resumption
368
+ turn.model # Model name
369
+ turn.stop_reason # Why the model stopped ("end_turn", "tool_use", etc.)
370
+ turn.usage # Token usage hash
371
+ turn.model_usage # Per-model usage breakdown
372
+ turn.structured_output # Structured output (if requested)
373
+ turn.num_turns # Number of turns in session
374
+
375
+ # Status
376
+ turn.success? # Whether turn completed successfully
377
+ turn.error? # Whether turn ended with error
378
+ turn.complete? # Whether a ResultMessage was received
379
+ turn.errors # Array of error strings
380
+ turn.permission_denials # Array of SDKPermissionDenial
381
+
382
+ # Filtered message access
383
+ turn.assistant_messages # All AssistantMessages
384
+ turn.user_messages # All UserMessages / UserMessageReplays
385
+ turn.stream_events # All StreamEvents
386
+ turn.content_blocks # All content blocks across assistant messages
387
+ ```
388
+
389
+ ## Event Handlers
390
+
391
+ Register typed callbacks instead of writing `case` statements. Works with `Client`, `Conversation`, or standalone.
392
+
393
+ ### Via Client
394
+
395
+ ```ruby
396
+ ClaudeAgent::Client.open do |client|
397
+ client.on_text { |text| print text }
398
+ client.on_tool_use { |tool| puts "\nUsing: #{tool.display_label}" }
399
+ client.on_tool_result { |result, tool_use| puts "Done: #{tool_use&.name}" }
400
+ client.on_result { |result| puts "\nCost: $#{result.total_cost_usd}" }
401
+
402
+ client.send_and_receive("Fix the bug in auth.rb")
403
+ end
404
+ ```
405
+
406
+ ### Via One-Shot Query
407
+
408
+ ```ruby
409
+ events = ClaudeAgent::EventHandler.new
410
+ .on_text { |text| print text }
411
+ .on_result { |r| puts "\nDone!" }
412
+
413
+ ClaudeAgent.query_turn(prompt: "Explain this code", events: events)
414
+ ```
415
+
416
+ ### Standalone
417
+
418
+ ```ruby
419
+ handler = ClaudeAgent::EventHandler.new
420
+ handler.on(:text) { |text| print text }
421
+ handler.on(:thinking) { |thought| puts "Thinking: #{thought}" }
422
+ handler.on(:tool_use) { |tool| puts "Tool: #{tool.display_label}" }
423
+ handler.on(:tool_result) { |result, tool_use| puts "Result for #{tool_use&.name}" }
424
+ handler.on(:result) { |result| puts "Cost: $#{result.total_cost_usd}" }
425
+ handler.on(:message) { |msg| log(msg) } # Catch-all
426
+
427
+ # Dispatch manually
428
+ client.receive_response.each { |msg| handler.handle(msg) }
429
+ handler.reset! # Clear turn state between turns
430
+ ```
431
+
193
432
  ## Message Types
194
433
 
195
- The SDK provides strongly-typed message classes:
434
+ The SDK provides strongly-typed message classes for all protocol messages.
196
435
 
197
436
  ### AssistantMessage
198
437
 
@@ -224,6 +463,7 @@ result.success? # Convenience method
224
463
  result.error? # Convenience method
225
464
  result.errors # Array of error messages (if any)
226
465
  result.permission_denials # Array of SDKPermissionDenial (if any)
466
+ result.stop_reason # Why the model stopped generating (e.g. "end_turn", "tool_use")
227
467
  ```
228
468
 
229
469
  ### UserMessageReplay
@@ -295,11 +535,17 @@ progress.elapsed_time_seconds # Time elapsed
295
535
  Hook execution output:
296
536
 
297
537
  ```ruby
538
+ hook_response.hook_id # Hook identifier
298
539
  hook_response.hook_name # Hook name
299
540
  hook_response.hook_event # Hook event type
300
541
  hook_response.stdout # Hook stdout
301
542
  hook_response.stderr # Hook stderr
543
+ hook_response.output # Combined output
302
544
  hook_response.exit_code # Exit code
545
+ hook_response.outcome # "success", "error", or "cancelled"
546
+ hook_response.success? # Convenience predicate
547
+ hook_response.error? # Convenience predicate
548
+ hook_response.cancelled? # Convenience predicate
303
549
  ```
304
550
 
305
551
  ### HookStartedMessage
@@ -368,6 +614,56 @@ notification.failed? # Convenience predicate
368
614
  notification.stopped? # Convenience predicate
369
615
  ```
370
616
 
617
+ ### TaskStartedMessage
618
+
619
+ Background task (subagent) start notification:
620
+
621
+ ```ruby
622
+ task_started.task_id # Task ID
623
+ task_started.tool_use_id # Associated tool use ID (optional)
624
+ task_started.description # Task description (optional)
625
+ task_started.task_type # Task type (optional)
626
+ ```
627
+
628
+ ### TaskProgressMessage
629
+
630
+ Background task progress reporting:
631
+
632
+ ```ruby
633
+ task_progress.task_id # Task ID
634
+ task_progress.description # What the task is doing
635
+ task_progress.usage # Token usage so far (optional)
636
+ task_progress.last_tool_name # Most recent tool used (optional)
637
+ ```
638
+
639
+ ### RateLimitEvent
640
+
641
+ Rate limit status and utilization:
642
+
643
+ ```ruby
644
+ rate_limit.rate_limit_info # Full rate limit info hash
645
+ rate_limit.status # Rate limit status (e.g. "allowed_warning")
646
+ ```
647
+
648
+ ### PromptSuggestionMessage
649
+
650
+ Suggested follow-up prompts (requires `prompt_suggestions: true`):
651
+
652
+ ```ruby
653
+ suggestion.suggestion # The suggested prompt text
654
+ ```
655
+
656
+ ### GenericMessage
657
+
658
+ Wraps unknown/future message types instead of raising errors:
659
+
660
+ ```ruby
661
+ msg.type # Message type as symbol (e.g. :fancy_new)
662
+ msg[:data] # Dynamic field access via []
663
+ msg.data # Dynamic field access via method_missing
664
+ msg.to_h # Raw data hash
665
+ ```
666
+
371
667
  ## Content Blocks
372
668
 
373
669
  Assistant messages contain content blocks:
@@ -380,19 +676,49 @@ message.content.each do |block|
380
676
  when ClaudeAgent::ThinkingBlock
381
677
  puts "Thinking: #{block.thinking}"
382
678
  when ClaudeAgent::ToolUseBlock
383
- puts "Tool: #{block.name}, ID: #{block.id}, Input: #{block.input}"
679
+ puts "Tool: #{block.display_label}"
680
+ puts " File: #{block.file_path}" if block.file_path
681
+ puts " Summary: #{block.summary}"
384
682
  when ClaudeAgent::ToolResultBlock
385
683
  puts "Result for #{block.tool_use_id}: #{block.content}"
386
684
  when ClaudeAgent::ServerToolUseBlock
387
- puts "MCP Tool: #{block.name} from #{block.server_name}"
685
+ puts "MCP Tool: #{block.display_label}" # "server_name/tool_name"
388
686
  when ClaudeAgent::ServerToolResultBlock
389
687
  puts "MCP Result from #{block.server_name}"
390
688
  when ClaudeAgent::ImageContentBlock
391
689
  puts "Image: #{block.media_type}, source: #{block.source_type}"
690
+ when ClaudeAgent::GenericBlock
691
+ puts "Unknown block: #{block.type}, data: #{block.to_h}"
392
692
  end
393
693
  end
394
694
  ```
395
695
 
696
+ ### ToolUseBlock Introspection
697
+
698
+ Tool use blocks provide human-readable labels and summaries:
699
+
700
+ ```ruby
701
+ block.name # "Read", "Write", "Bash", etc.
702
+ block.input # Tool input parameters (symbol-keyed Hash)
703
+ block.file_path # File path for Read/Write/Edit/NotebookEdit (nil otherwise)
704
+ block.display_label # One-line label: "Read lib/foo.rb", "Bash: git status", "Grep: pattern"
705
+ block.summary # Detailed: "Write: /path.rb (3 lines)", "Edit: /path.rb replacing 5 line(s)"
706
+ block.summary(max: 100) # Custom max length
707
+ ```
708
+
709
+ `ServerToolUseBlock` provides the same interface with server context in labels (e.g. `"calculator/add"`).
710
+
711
+ ### GenericBlock
712
+
713
+ Unknown content block types are wrapped instead of returning raw Hashes:
714
+
715
+ ```ruby
716
+ block.type # Block type as symbol
717
+ block[:field] # Dynamic field access via []
718
+ block.field # Dynamic field access via method_missing
719
+ block.to_h # Raw data hash
720
+ ```
721
+
396
722
  ## MCP Tools
397
723
 
398
724
  Create in-process MCP tools that Claude can use:
@@ -404,7 +730,7 @@ calculator = ClaudeAgent::MCP::Tool.new(
404
730
  description: "Add two numbers together",
405
731
  schema: { a: Float, b: Float }
406
732
  ) do |args|
407
- args["a"] + args["b"]
733
+ args[:a] + args[:b]
408
734
  end
409
735
 
410
736
  # Create a server with tools
@@ -426,6 +752,8 @@ ClaudeAgent.query(
426
752
  )
427
753
  ```
428
754
 
755
+ > **Note:** MCP tool handlers receive **symbol-keyed** argument hashes (e.g. `args[:a]` not `args["a"]`).
756
+
429
757
  ### External MCP Servers
430
758
 
431
759
  Configure external MCP servers:
@@ -484,7 +812,7 @@ tool = ClaudeAgent::MCP::Tool.new(
484
812
  openWorldHint: true, # Tool interacts with external systems
485
813
  title: "Web Search" # Human-readable display name
486
814
  }
487
- ) { |args| "Results for #{args['query']}" }
815
+ ) { |args| "Results for #{args[:query]}" }
488
816
 
489
817
  # Or with the convenience method
490
818
  tool = ClaudeAgent::MCP.tool(
@@ -502,12 +830,12 @@ Tools can return various formats:
502
830
  ```ruby
503
831
  # Simple string
504
832
  ClaudeAgent::MCP::Tool.new(name: "greet", description: "Greet") do |args|
505
- "Hello, #{args['name']}!"
833
+ "Hello, #{args[:name]}!"
506
834
  end
507
835
 
508
836
  # Number (converted to string)
509
837
  ClaudeAgent::MCP::Tool.new(name: "add", description: "Add") do |args|
510
- args["a"] + args["b"]
838
+ args[:a] + args[:b]
511
839
  end
512
840
 
513
841
  # Custom MCP content
@@ -551,6 +879,8 @@ options = ClaudeAgent::Options.new(
551
879
  )
552
880
  ```
553
881
 
882
+ > **Note:** Hook callbacks receive **symbol-keyed** input hashes (e.g. `input.tool_input[:file_path]`).
883
+
554
884
  ### Hook Events
555
885
 
556
886
  All available hook events:
@@ -568,6 +898,11 @@ All available hook events:
568
898
  - `PreCompact` - Before conversation compaction
569
899
  - `PermissionRequest` - When permission is requested
570
900
  - `Setup` - Initial setup or maintenance (trigger: "init" or "maintenance")
901
+ - `TeammateIdle` - When a teammate agent becomes idle
902
+ - `TaskCompleted` - When a background task completes
903
+ - `ConfigChange` - When configuration changes
904
+ - `WorktreeCreate` - When a git worktree is created
905
+ - `WorktreeRemove` - When a git worktree is removed
571
906
 
572
907
  ### Hook Input Types
573
908
 
@@ -586,6 +921,11 @@ All available hook events:
586
921
  | PreCompact | `PreCompactInput` | trigger, custom_instructions |
587
922
  | PermissionRequest | `PermissionRequestInput` | tool_name, tool_input, permission_suggestions |
588
923
  | Setup | `SetupInput` | trigger (init/maintenance) |
924
+ | TeammateIdle | `TeammateIdleInput` | teammate_name, team_name |
925
+ | TaskCompleted | `TaskCompletedInput` | task_id, task_subject, task_description, teammate_name |
926
+ | ConfigChange | `ConfigChangeInput` | source, file_path |
927
+ | WorktreeCreate | `WorktreeCreateInput` | name |
928
+ | WorktreeRemove | `WorktreeRemoveInput` | worktree_path |
589
929
 
590
930
  All hook inputs inherit from `BaseHookInput` with: `hook_event_name`, `session_id`, `transcript_path`, `cwd`, `permission_mode`.
591
931
 
@@ -596,13 +936,14 @@ Control tool permissions programmatically:
596
936
  ```ruby
597
937
  options = ClaudeAgent::Options.new(
598
938
  can_use_tool: ->(tool_name, tool_input, context) {
599
- # Context includes: permission_suggestions, blocked_path, decision_reason, tool_use_id, agent_id
939
+ # Context includes: permission_suggestions, blocked_path, decision_reason,
940
+ # tool_use_id, agent_id, description
600
941
 
601
942
  # Allow all read operations
602
943
  if tool_name == "Read"
603
944
  ClaudeAgent::PermissionResultAllow.new
604
945
  # Deny writes to sensitive paths
605
- elsif tool_name == "Write" && tool_input["file_path"].include?(".env")
946
+ elsif tool_name == "Write" && tool_input[:file_path]&.include?(".env")
606
947
  ClaudeAgent::PermissionResultDeny.new(
607
948
  message: "Cannot modify .env files",
608
949
  interrupt: true
@@ -614,6 +955,8 @@ options = ClaudeAgent::Options.new(
614
955
  )
615
956
  ```
616
957
 
958
+ > **Note:** `can_use_tool` callbacks receive **symbol-keyed** `tool_input` hashes.
959
+
617
960
  ### Permission Results
618
961
 
619
962
  ```ruby
@@ -630,6 +973,67 @@ ClaudeAgent::PermissionResultDeny.new(
630
973
  )
631
974
  ```
632
975
 
976
+ ### Permission Queue
977
+
978
+ For UI-driven permission handling (e.g. TUI's, desktop apps, web UIs), use queue-based permissions instead of synchronous callbacks:
979
+
980
+ ```ruby
981
+ # Enable via Options
982
+ options = ClaudeAgent::Options.new(permission_queue: true)
983
+
984
+ # Or via Conversation (queue mode is the default)
985
+ conversation = ClaudeAgent.conversation(permission_mode: "default")
986
+ ```
987
+
988
+ Resolve permissions from any thread:
989
+
990
+ ```ruby
991
+ # Non-blocking poll
992
+ if request = client.pending_permission
993
+ puts "Tool: #{request.tool_name}"
994
+ puts "Input: #{request.input}"
995
+ request.allow! # or request.deny!(message: "Not allowed")
996
+ end
997
+
998
+ # Check if any are waiting
999
+ client.pending_permissions?
1000
+
1001
+ # Blocking wait with timeout
1002
+ request = client.permission_queue.pop(timeout: 30)
1003
+ request&.allow!
1004
+
1005
+ # Drain all pending (e.g. during shutdown)
1006
+ client.permission_queue.drain!(reason: "Session ended")
1007
+ ```
1008
+
1009
+ ### Hybrid Mode
1010
+
1011
+ Combine synchronous callbacks with deferred queue resolution:
1012
+
1013
+ ```ruby
1014
+ options = ClaudeAgent::Options.new(
1015
+ can_use_tool: ->(tool_name, tool_input, context) {
1016
+ if tool_name == "Read"
1017
+ ClaudeAgent::PermissionResultAllow.new # Auto-allow reads
1018
+ else
1019
+ context.request.defer! # Defer everything else to the queue
1020
+ end
1021
+ }
1022
+ )
1023
+
1024
+ client = ClaudeAgent::Client.new(options: options)
1025
+ client.connect
1026
+
1027
+ # In another thread: resolve deferred permissions
1028
+ Thread.new do
1029
+ loop do
1030
+ request = client.permission_queue.pop
1031
+ break unless request
1032
+ request.allow! # Or show UI dialog
1033
+ end
1034
+ end
1035
+ ```
1036
+
633
1037
  ### Permission Updates
634
1038
 
635
1039
  ```ruby
@@ -670,6 +1074,33 @@ rescue ClaudeAgent::AbortError => e
670
1074
  end
671
1075
  ```
672
1076
 
1077
+ ## Cumulative Usage
1078
+
1079
+ The `Client` automatically tracks cumulative usage across turns:
1080
+
1081
+ ```ruby
1082
+ ClaudeAgent::Client.open do |client|
1083
+ client.send_and_receive("Hello")
1084
+ client.send_and_receive("Follow up")
1085
+
1086
+ usage = client.cumulative_usage
1087
+ puts "Tokens: #{usage.input_tokens} in / #{usage.output_tokens} out"
1088
+ puts "Cache: #{usage.cache_read_input_tokens} read / #{usage.cache_creation_input_tokens} created"
1089
+ puts "Cost: $#{usage.total_cost_usd}"
1090
+ puts "Turns: #{usage.num_turns}"
1091
+ puts "Duration: #{usage.duration_ms}ms"
1092
+ end
1093
+ ```
1094
+
1095
+ Also available via `Conversation#usage`:
1096
+
1097
+ ```ruby
1098
+ ClaudeAgent::Conversation.open do |c|
1099
+ c.say("Hello")
1100
+ puts c.usage.to_h # => { input_tokens: 100, output_tokens: 50, ... }
1101
+ end
1102
+ ```
1103
+
673
1104
  ## Client API
674
1105
 
675
1106
  For fine-grained control:
@@ -680,18 +1111,36 @@ client = ClaudeAgent::Client.new(options: options)
680
1111
  # Connect to CLI
681
1112
  client.connect
682
1113
 
683
- # Send queries
684
- client.query("First question")
685
- client.receive_response.each { |msg| process(msg) }
1114
+ # Send queries and receive TurnResults
1115
+ turn = client.send_and_receive("First question")
1116
+ puts turn.text
686
1117
 
687
- client.query("Follow-up question")
1118
+ turn = client.send_and_receive("Follow-up question")
1119
+ puts turn.text
1120
+
1121
+ # Or use lower-level send/receive
1122
+ client.send_message("Question")
688
1123
  client.receive_response.each { |msg| process(msg) }
689
1124
 
1125
+ # Or receive as TurnResult without sending
1126
+ client.send_message("Question")
1127
+ turn = client.receive_turn
1128
+
1129
+ # Event handlers (persist across turns)
1130
+ client.on_text { |text| print text }
1131
+ client.on_tool_use { |tool| puts tool.display_label }
1132
+ client.on_tool_result { |result, tool_use| puts "Done: #{tool_use&.name}" }
1133
+ client.on_thinking { |thought| puts thought }
1134
+ client.on_result { |result| puts "Cost: $#{result.total_cost_usd}" }
1135
+ client.on_message { |msg| log(msg) }
1136
+
690
1137
  # Control methods
691
1138
  client.interrupt # Cancel current operation
692
1139
  client.set_model("claude-opus-4-5-20251101") # Change model
693
1140
  client.set_permission_mode("acceptEdits") # Change permissions
694
1141
  client.set_max_thinking_tokens(5000) # Change thinking limit
1142
+ client.stop_task("task-123") # Stop a running background task
1143
+ client.apply_flag_settings({ "model" => "..." }) # Merge settings into flag layer
695
1144
 
696
1145
  # File checkpointing (requires enable_file_checkpointing: true)
697
1146
  result = client.rewind_files("user-message-uuid", dry_run: true)
@@ -704,12 +1153,20 @@ result = client.set_mcp_servers({
704
1153
  })
705
1154
  puts "Added: #{result.added}, Removed: #{result.removed}"
706
1155
 
707
- # Reconnect a disconnected MCP server
708
- client.mcp_reconnect("my-server")
1156
+ # MCP server lifecycle
1157
+ client.mcp_reconnect("my-server") # Reconnect a disconnected server
1158
+ client.mcp_toggle("my-server", enabled: false) # Disable a server
1159
+ client.mcp_toggle("my-server", enabled: true) # Re-enable a server
1160
+ client.mcp_authenticate("my-remote-server") # OAuth authentication
1161
+ client.mcp_clear_auth("my-remote-server") # Clear stored credentials
1162
+
1163
+ # Permission queue access
1164
+ client.pending_permission # Non-blocking poll for next request
1165
+ client.pending_permissions? # Check if any requests waiting
1166
+ client.permission_queue # Direct access to PermissionQueue
709
1167
 
710
- # Enable or disable an MCP server
711
- client.mcp_toggle("my-server", enabled: false) # Disable
712
- client.mcp_toggle("my-server", enabled: true) # Re-enable
1168
+ # Cumulative usage tracking
1169
+ client.cumulative_usage # CumulativeUsage with totals across all turns
713
1170
 
714
1171
  # Query capabilities
715
1172
  client.supported_commands.each { |cmd| puts "#{cmd.name}: #{cmd.description}" }
@@ -721,9 +1178,52 @@ puts client.account_info.email
721
1178
  client.disconnect
722
1179
  ```
723
1180
 
1181
+ ## Session Discovery
1182
+
1183
+ List past Claude Code sessions from disk without spawning a CLI subprocess:
1184
+
1185
+ ```ruby
1186
+ # All sessions (most recent first)
1187
+ sessions = ClaudeAgent.list_sessions
1188
+
1189
+ # Scoped to a project directory (includes git worktree siblings)
1190
+ sessions = ClaudeAgent.list_sessions(dir: "/path/to/project", limit: 10)
1191
+
1192
+ sessions.each do |s|
1193
+ puts "#{s.summary} (#{s.git_branch || 'no branch'})"
1194
+ puts " Session: #{s.session_id}"
1195
+ puts " Modified: #{Time.at(s.last_modified / 1000)}"
1196
+ puts " Prompt: #{s.first_prompt}" if s.first_prompt
1197
+ end
1198
+ ```
1199
+
1200
+ Each session is a `SessionInfo` with these fields:
1201
+
1202
+ | Field | Type | Description |
1203
+ |------------------|-----------------|--------------------------------------------------|
1204
+ | `session_id` | `String` | UUID of the session |
1205
+ | `summary` | `String` | Custom title, last summary, or first prompt |
1206
+ | `last_modified` | `Integer` | Epoch milliseconds of last modification |
1207
+ | `file_size` | `Integer` | Session file size in bytes |
1208
+ | `custom_title` | `String\|nil` | User-set title, if any |
1209
+ | `first_prompt` | `String\|nil` | First meaningful user prompt |
1210
+ | `git_branch` | `String\|nil` | Git branch the session was on |
1211
+ | `cwd` | `String\|nil` | Working directory of the session |
1212
+
1213
+ Use with `Conversation.resume` to pick up where you left off:
1214
+
1215
+ ```ruby
1216
+ sessions = ClaudeAgent.list_sessions(dir: Dir.pwd, limit: 5)
1217
+ session = sessions.first
1218
+
1219
+ conversation = ClaudeAgent.resume_conversation(session.session_id)
1220
+ turn = conversation.say("Continue where we left off")
1221
+ conversation.close
1222
+ ```
1223
+
724
1224
  ## V2 Session API (Unstable)
725
1225
 
726
- > **⚠️ Alpha API**: This API is unstable and may change without notice.
1226
+ > **Warning**: This API is unstable and may change without notice.
727
1227
 
728
1228
  The V2 Session API provides a simpler interface for multi-turn conversations, matching the TypeScript SDK's `SDKSession` interface.
729
1229
 
@@ -809,6 +1309,12 @@ session = ClaudeAgent.unstable_v2_create_session(options)
809
1309
 
810
1310
  | Type | Purpose |
811
1311
  |-----------------------|----------------------------------------------------------------------------------|
1312
+ | `TurnResult` | Complete agent turn with text, tools, usage, and status accessors |
1313
+ | `ToolActivity` | Tool use/result pair with turn index and timing |
1314
+ | `CumulativeUsage` | Running totals of tokens, cost, turns, and duration |
1315
+ | `PermissionRequest` | Deferred permission promise resolvable from any thread |
1316
+ | `PermissionQueue` | Thread-safe queue of pending permission requests |
1317
+ | `EventHandler` | Typed event callback registry |
812
1318
  | `SlashCommand` | Available slash commands (name, description, argument_hint) |
813
1319
  | `ModelInfo` | Available models (value, display_name, description) |
814
1320
  | `McpServerStatus` | MCP server status (name, status, server_info) |
@@ -816,6 +1322,7 @@ session = ClaudeAgent.unstable_v2_create_session(options)
816
1322
  | `ModelUsage` | Per-model usage stats (input_tokens, output_tokens, cost_usd) |
817
1323
  | `McpSetServersResult` | Result of set_mcp_servers (added, removed, errors) |
818
1324
  | `RewindFilesResult` | Result of rewind_files (can_rewind, error, files_changed, insertions, deletions) |
1325
+ | `SessionInfo` | Session metadata from `list_sessions` (session_id, summary, git_branch, cwd) |
819
1326
  | `SDKPermissionDenial` | Permission denial info (tool_name, tool_use_id, tool_input) |
820
1327
 
821
1328
  ## Logging
@@ -866,12 +1373,12 @@ When enabled, the SDK logs events across transport, protocol, parsing, MCP, and
866
1373
 
867
1374
  ### Log Levels
868
1375
 
869
- | Level | What's Logged |
870
- |-------|---------------|
871
- | ERROR | Control request failures, unknown message types |
872
- | WARN | Force kills, JSON parse errors during buffering, unknown MCP tools |
1376
+ | Level | What's Logged |
1377
+ |-------|----------------------------------------------------------------------------------------------------------------|
1378
+ | ERROR | Control request failures, unknown message types |
1379
+ | WARN | Force kills, JSON parse errors during buffering, unknown MCP tools |
873
1380
  | INFO | Process spawn/close, protocol start/stop, permission decisions, tool calls, query start/completion with timing |
874
- | DEBUG | Full commands, message types received, control request/response routing, reader thread lifecycle |
1381
+ | DEBUG | Full commands, message types received, control request/response routing, reader thread lifecycle |
875
1382
 
876
1383
  ## Environment Variables
877
1384
 
@@ -927,7 +1434,14 @@ bin/release 1.2.0 # Release a new version
927
1434
  ## Architecture
928
1435
 
929
1436
  ```
930
- ClaudeAgent.query() / ClaudeAgent::Client
1437
+ ClaudeAgent.conversation() / ClaudeAgent::Conversation
1438
+
1439
+ │ Manages lifecycle, callbacks, turn history, tool activity
1440
+
1441
+
1442
+ ClaudeAgent::Client
1443
+
1444
+ │ Event handlers, cumulative usage, permission queue
931
1445
 
932
1446
 
933
1447
  ┌──────────────────────────┐