claude_agent 0.7.10 → 0.7.11

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.
@@ -3,7 +3,13 @@
3
3
  module ClaudeAgent
4
4
  # Dispatches typed events as messages flow through a conversation turn.
5
5
  #
6
- # Register handlers for specific events instead of writing `case` statements
6
+ # Three event layers fire for every message:
7
+ #
8
+ # 1. **Catch-all** — +:message+ fires for every message
9
+ # 2. **Type-based** — +message.type+ fires (e.g. +:assistant+, +:stream_event+, +:status+)
10
+ # 3. **Decomposed** — convenience events for rich content types (+:text+, +:thinking+, etc.)
11
+ #
12
+ # Register handlers for specific events instead of writing +case+ statements
7
13
  # over raw message types. Use standalone or via {Client#on}.
8
14
  #
9
15
  # @example Standalone
@@ -14,6 +20,11 @@ module ClaudeAgent
14
20
  #
15
21
  # client.receive_response.each { |msg| handler.handle(msg) }
16
22
  #
23
+ # @example Type-based events
24
+ # handler.on_stream_event { |evt| handle_stream(evt) }
25
+ # handler.on_tool_progress { |prog| update_spinner(prog) }
26
+ # handler.on_status { |status| show_status(status) }
27
+ #
17
28
  # @example Via Client
18
29
  # client.on_text { |text| print text }
19
30
  # client.on_tool_use { |tool| puts tool.display_label }
@@ -25,13 +36,22 @@ module ClaudeAgent
25
36
  # .on_result { |r| puts "\nDone!" }
26
37
  #
27
38
  class EventHandler
28
- # Events:
29
- # :message — every message (catch-all)
30
- # :text — AssistantMessage text content
31
- # :thinking — AssistantMessage thinking content
32
- # :tool_use — ToolUseBlock or ServerToolUseBlock
33
- # :tool_result — ToolResultBlock or ServerToolResultBlock, paired with original tool_use
34
- # :result — ResultMessage (end of turn)
39
+ # Type-based events — one per message type, auto-dispatched from message.type
40
+ TYPE_EVENTS = %i[
41
+ user assistant system result stream_event compact_boundary
42
+ status tool_progress hook_response auth_status task_notification
43
+ hook_started hook_progress tool_use_summary task_started
44
+ task_progress rate_limit_event prompt_suggestion files_persisted
45
+ ].freeze
46
+
47
+ # Decomposed events — extracted content from rich message types
48
+ DECOMPOSED_EVENTS = %i[text thinking tool_use tool_result].freeze
49
+
50
+ # Meta events — catch-all
51
+ META_EVENTS = %i[message].freeze
52
+
53
+ # All known events
54
+ EVENTS = (META_EVENTS + TYPE_EVENTS + DECOMPOSED_EVENTS).freeze
35
55
 
36
56
  def initialize
37
57
  @handlers = Hash.new { |h, k| h[k] = [] }
@@ -40,7 +60,7 @@ module ClaudeAgent
40
60
 
41
61
  # Register a handler for an event
42
62
  #
43
- # @param event [Symbol] Event name
63
+ # @param event [Symbol] Event name (any symbol, including future/unknown types)
44
64
  # @yield Event-specific arguments
45
65
  # @return [self]
46
66
  def on(event, &block)
@@ -49,53 +69,82 @@ module ClaudeAgent
49
69
  end
50
70
 
51
71
  # @!method on_message(&block)
52
- # Register a handler for every message
72
+ # Register a handler for every message (catch-all)
53
73
  # @yield [message] Any message object
54
74
  # @return [self]
55
75
 
76
+ # @!method on_assistant(&block)
77
+ # Register a handler for AssistantMessage
78
+ # @yield [AssistantMessage] The assistant message
79
+ # @return [self]
80
+
81
+ # @!method on_user(&block)
82
+ # Register a handler for UserMessage
83
+ # @yield [UserMessage] The user message
84
+ # @return [self]
85
+
86
+ # @!method on_result(&block)
87
+ # Register a handler for ResultMessage (end of turn)
88
+ # @yield [ResultMessage] The result
89
+ # @return [self]
90
+
91
+ # @!method on_stream_event(&block)
92
+ # Register a handler for StreamEvent
93
+ # @yield [StreamEvent] The stream event
94
+ # @return [self]
95
+
96
+ # @!method on_status(&block)
97
+ # Register a handler for StatusMessage
98
+ # @yield [StatusMessage] The status message
99
+ # @return [self]
100
+
101
+ # @!method on_tool_progress(&block)
102
+ # Register a handler for ToolProgressMessage
103
+ # @yield [ToolProgressMessage] The tool progress message
104
+ # @return [self]
105
+
56
106
  # @!method on_text(&block)
57
- # Register a handler for assistant text content
107
+ # Register a handler for assistant text content (decomposed)
58
108
  # @yield [String] Text from the AssistantMessage
59
109
  # @return [self]
60
110
 
61
111
  # @!method on_thinking(&block)
62
- # Register a handler for assistant thinking content
112
+ # Register a handler for assistant thinking content (decomposed)
63
113
  # @yield [String] Thinking from the AssistantMessage
64
114
  # @return [self]
65
115
 
66
116
  # @!method on_tool_use(&block)
67
- # Register a handler for tool use requests
117
+ # Register a handler for tool use requests (decomposed)
68
118
  # @yield [ToolUseBlock, ServerToolUseBlock] The tool use block
69
119
  # @return [self]
70
120
 
71
121
  # @!method on_tool_result(&block)
72
- # Register a handler for tool results, paired with the original request
122
+ # Register a handler for tool results, paired with the original request (decomposed)
73
123
  # @yield [ToolResultBlock, ToolUseBlock|nil] Result block and matched tool use
74
124
  # @return [self]
75
125
 
76
- # @!method on_result(&block)
77
- # Register a handler for the final ResultMessage
78
- # @yield [ResultMessage] The result
79
- # @return [self]
80
-
81
- %i[message text thinking tool_use tool_result result].each do |event|
126
+ EVENTS.each do |event|
82
127
  define_method(:"on_#{event}") { |&block| on(event, &block) }
83
128
  end
84
129
 
85
130
  # Dispatch a message to registered handlers
86
131
  #
132
+ # Fires events in order:
133
+ # 1. +:message+ (catch-all)
134
+ # 2. +message.type+ (type-based, e.g. +:assistant+, +:stream_event+)
135
+ # 3. Decomposed events (+:text+, +:thinking+, +:tool_use+, +:tool_result+)
136
+ #
87
137
  # @param message [Message] Any SDK message
88
138
  # @return [void]
89
139
  def handle(message)
90
140
  emit(:message, message)
141
+ emit(message.type, message)
91
142
 
92
143
  case message
93
144
  when AssistantMessage
94
145
  handle_assistant(message)
95
146
  when UserMessage, UserMessageReplay
96
147
  handle_user(message)
97
- when ResultMessage
98
- emit(:result, message)
99
148
  end
100
149
  end
101
150
 
@@ -167,13 +167,13 @@ module ClaudeAgent
167
167
  required: [ :reason ]
168
168
 
169
169
  BaseHookInput.define_input "Stop",
170
- optional: { stop_hook_active: false }
170
+ optional: { stop_hook_active: false, last_assistant_message: nil }
171
171
 
172
172
  BaseHookInput.define_input "SubagentStart",
173
173
  required: [ :agent_id, :agent_type ]
174
174
 
175
175
  BaseHookInput.define_input "SubagentStop",
176
- optional: { stop_hook_active: false, agent_id: nil, agent_transcript_path: nil }
176
+ optional: { stop_hook_active: false, agent_id: nil, agent_transcript_path: nil, agent_type: nil, last_assistant_message: nil }
177
177
 
178
178
  BaseHookInput.define_input "PreCompact",
179
179
  required: [ :trigger ],
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClaudeAgent
4
+ # Mutable wrapper around a ToolUseBlock for real-time status tracking.
5
+ #
6
+ # Unlike {ToolActivity} (immutable, built after a turn completes),
7
+ # LiveToolActivity tracks status changes as they happen — running,
8
+ # done, or error — making it suitable for live UIs.
9
+ #
10
+ # @example
11
+ # activity = LiveToolActivity.new(tool_use)
12
+ # activity.running? # => true
13
+ # activity.elapsed # => nil
14
+ #
15
+ # activity.update_elapsed(2.5)
16
+ # activity.elapsed # => 2.5
17
+ #
18
+ # activity.complete!(tool_result)
19
+ # activity.done? # => true
20
+ # activity.elapsed # => 2.5 (preserved from progress)
21
+ #
22
+ class LiveToolActivity
23
+ attr_reader :tool_use, :tool_result, :status, :started_at, :elapsed
24
+
25
+ # @param tool_use [ToolUseBlock, ServerToolUseBlock] The tool use block
26
+ def initialize(tool_use)
27
+ @tool_use = tool_use
28
+ @tool_result = nil
29
+ @status = :running
30
+ @started_at = Time.now
31
+ @elapsed = nil
32
+ end
33
+
34
+ # Tool use ID
35
+ # @return [String]
36
+ def id
37
+ tool_use.id
38
+ end
39
+
40
+ # Tool name
41
+ # @return [String]
42
+ def name
43
+ tool_use.name
44
+ end
45
+
46
+ # Tool input
47
+ # @return [Hash]
48
+ def input
49
+ tool_use.input
50
+ end
51
+
52
+ # Human-readable label (delegates to ToolUseBlock#display_label)
53
+ # @return [String]
54
+ def display_label
55
+ tool_use.display_label
56
+ end
57
+
58
+ # Detailed summary (delegates to ToolUseBlock#summary)
59
+ # @param max [Integer] Maximum length
60
+ # @return [String]
61
+ def summary(max: 60)
62
+ tool_use.summary(max: max)
63
+ end
64
+
65
+ # File path if this is a file-based tool
66
+ # @return [String, nil]
67
+ def file_path
68
+ tool_use.file_path
69
+ end
70
+
71
+ # Mark the tool as complete with its result.
72
+ #
73
+ # Transitions status to :done or :error based on the result.
74
+ # Calculates elapsed time from started_at if not already set by progress updates.
75
+ #
76
+ # @param result [ToolResultBlock, ServerToolResultBlock] The tool result
77
+ # @return [void]
78
+ def complete!(result)
79
+ @tool_result = result
80
+ @status = result.is_error ? :error : :done
81
+ @elapsed ||= Time.now - @started_at
82
+ end
83
+
84
+ # Update elapsed time from a ToolProgressMessage.
85
+ #
86
+ # @param seconds [Float] Elapsed time in seconds
87
+ # @return [void]
88
+ def update_elapsed(seconds)
89
+ @elapsed = seconds
90
+ end
91
+
92
+ # Whether the tool is currently running.
93
+ # @return [Boolean]
94
+ def running?
95
+ @status == :running
96
+ end
97
+
98
+ # Whether the tool completed successfully.
99
+ # @return [Boolean]
100
+ def done?
101
+ @status == :done
102
+ end
103
+
104
+ # Whether the tool completed with an error.
105
+ # @return [Boolean]
106
+ def error?
107
+ @status == :error
108
+ end
109
+
110
+ # Whether the tool execution is complete (done or error).
111
+ # @return [Boolean]
112
+ def complete?
113
+ @status == :done || @status == :error
114
+ end
115
+
116
+ # @return [Hash]
117
+ def to_h
118
+ {
119
+ id: id,
120
+ name: name,
121
+ status: @status,
122
+ elapsed: @elapsed,
123
+ started_at: @started_at
124
+ }
125
+ end
126
+
127
+ def inspect
128
+ parts = [ "#<#{self.class}" ]
129
+ parts << "id=#{id}"
130
+ parts << "name=#{name}"
131
+ parts << "status=#{@status}"
132
+ parts << "elapsed=#{@elapsed}s" if @elapsed
133
+ "#{parts.join(" ")}>"
134
+ end
135
+ end
136
+ end
@@ -254,7 +254,8 @@ module ClaudeAgent
254
254
  StatusMessage.new(
255
255
  uuid: raw[:uuid] || "",
256
256
  session_id: raw[:session_id] || "",
257
- status: raw[:status]
257
+ status: raw[:status],
258
+ permission_mode: raw[:permission_mode]
258
259
  )
259
260
  end
260
261
 
@@ -265,7 +266,8 @@ module ClaudeAgent
265
266
  tool_use_id: raw[:tool_use_id] || "",
266
267
  tool_name: raw[:tool_name] || "",
267
268
  parent_tool_use_id: raw[:parent_tool_use_id],
268
- elapsed_time_seconds: raw[:elapsed_time_seconds] || 0
269
+ elapsed_time_seconds: raw[:elapsed_time_seconds] || 0,
270
+ task_id: raw[:task_id]
269
271
  )
270
272
  end
271
273
 
@@ -295,13 +297,24 @@ module ClaudeAgent
295
297
  end
296
298
 
297
299
  def parse_task_notification_message(raw)
300
+ usage = raw[:usage]
301
+ parsed_usage = if usage
302
+ TaskUsage.new(
303
+ total_tokens: usage[:total_tokens] || 0,
304
+ tool_uses: usage[:tool_uses] || 0,
305
+ duration_ms: usage[:duration_ms] || 0
306
+ )
307
+ end
308
+
298
309
  TaskNotificationMessage.new(
299
310
  uuid: raw[:uuid] || "",
300
311
  session_id: raw[:session_id] || "",
301
312
  task_id: raw[:task_id] || "",
302
313
  status: raw[:status] || "unknown",
303
314
  output_file: raw[:output_file] || "",
304
- summary: raw[:summary] || ""
315
+ summary: raw[:summary] || "",
316
+ tool_use_id: raw[:tool_use_id],
317
+ usage: parsed_usage
305
318
  )
306
319
  end
307
320
 
@@ -288,7 +288,11 @@ module ClaudeAgent
288
288
  # status: "compacting"
289
289
  # )
290
290
  #
291
- StatusMessage = Data.define(:uuid, :session_id, :status) do
291
+ StatusMessage = Data.define(:uuid, :session_id, :status, :permission_mode) do
292
+ def initialize(uuid:, session_id:, status:, permission_mode: nil)
293
+ super
294
+ end
295
+
292
296
  def type
293
297
  :status
294
298
  end
@@ -313,7 +317,8 @@ module ClaudeAgent
313
317
  :tool_use_id,
314
318
  :tool_name,
315
319
  :parent_tool_use_id,
316
- :elapsed_time_seconds
320
+ :elapsed_time_seconds,
321
+ :task_id
317
322
  ) do
318
323
  def initialize(
319
324
  uuid:,
@@ -321,7 +326,8 @@ module ClaudeAgent
321
326
  tool_use_id:,
322
327
  tool_name:,
323
328
  elapsed_time_seconds:,
324
- parent_tool_use_id: nil
329
+ parent_tool_use_id: nil,
330
+ task_id: nil
325
331
  )
326
332
  super
327
333
  end
@@ -469,7 +475,9 @@ module ClaudeAgent
469
475
  :task_id,
470
476
  :status,
471
477
  :output_file,
472
- :summary
478
+ :summary,
479
+ :tool_use_id,
480
+ :usage
473
481
  ) do
474
482
  def initialize(
475
483
  uuid:,
@@ -477,7 +485,9 @@ module ClaudeAgent
477
485
  task_id:,
478
486
  status:,
479
487
  output_file:,
480
- summary:
488
+ summary:,
489
+ tool_use_id: nil,
490
+ usage: nil
481
491
  )
482
492
  super
483
493
  end
@@ -39,9 +39,6 @@ module ClaudeAgent
39
39
  enable_file_checkpointing: false,
40
40
  persist_session: true,
41
41
  betas: [],
42
- init: false,
43
- init_only: false,
44
- maintenance: false,
45
42
  prompt_suggestions: false,
46
43
  debug: false,
47
44
  debug_file: nil
@@ -60,12 +57,11 @@ module ClaudeAgent
60
57
  continue_conversation resume fork_session resume_session_at session_id
61
58
  max_turns max_budget_usd thinking effort max_thinking_tokens
62
59
  strict_mcp_config mcp_servers hooks
63
- settings sandbox cwd add_dirs env user agent
60
+ sandbox cwd add_dirs env agent
64
61
  cli_path extra_args agents setting_sources plugins
65
62
  include_partial_messages output_format enable_file_checkpointing
66
63
  persist_session prompt_suggestions betas max_buffer_size stderr_callback
67
64
  abort_controller spawn_claude_code_process
68
- init init_only maintenance
69
65
  debug debug_file
70
66
  logger
71
67
  ].freeze
@@ -93,10 +89,9 @@ module ClaudeAgent
93
89
  args.concat(conversation_args)
94
90
  args.concat(limits_args)
95
91
  args.concat(mcp_args)
96
- args.concat(settings_args)
92
+ args.concat(sandbox_args)
97
93
  args.concat(environment_args)
98
94
  args.concat(output_args)
99
- args.concat(setup_hook_args)
100
95
  args.concat(debug_args)
101
96
  args.concat(extra_cli_args)
102
97
  end
@@ -233,9 +228,8 @@ module ClaudeAgent
233
228
  end
234
229
  end
235
230
 
236
- def settings_args
231
+ def sandbox_args
237
232
  [].tap do |args|
238
- args.push("--settings", settings) if settings
239
233
  if sandbox
240
234
  args.push("--sandbox", JSON.generate(sandbox.to_h))
241
235
  end
@@ -244,7 +238,6 @@ module ClaudeAgent
244
238
 
245
239
  def environment_args
246
240
  [].tap do |args|
247
- args.push("--user", user) if user
248
241
  args.push("--agent", agent) if agent
249
242
  add_dirs.each { |dir| args.push("--add-dir", dir.to_s) }
250
243
  args.push("--setting-sources", setting_sources.join(",")) if setting_sources&.any?
@@ -270,14 +263,6 @@ module ClaudeAgent
270
263
  end
271
264
  end
272
265
 
273
- def setup_hook_args
274
- [].tap do |args|
275
- args.push("--init") if init
276
- args.push("--init-only") if init_only
277
- args.push("--maintenance") if maintenance
278
- end
279
- end
280
-
281
266
  def debug_args
282
267
  [].tap do |args|
283
268
  args.push("--debug") if debug
@@ -342,11 +327,6 @@ module ClaudeAgent
342
327
  if session_id && (continue_conversation || resume) && !fork_session
343
328
  raise ConfigurationError, "session_id cannot be used with continue or resume unless fork_session is also set"
344
329
  end
345
-
346
- setup_options = [ init, init_only, maintenance ].count { |opt| opt }
347
- if setup_options > 1
348
- raise ConfigurationError, "Only one of init, init_only, or maintenance can be set at a time"
349
- end
350
330
  end
351
331
  end
352
332
  end
@@ -2,48 +2,6 @@
2
2
 
3
3
  module ClaudeAgent
4
4
  class << self
5
- # Run Setup hooks and exit
6
- #
7
- # This is a convenience method for running Setup hooks without starting
8
- # a conversation. Useful for CI/CD pipelines or scripts that need to
9
- # ensure setup is complete before proceeding.
10
- #
11
- # @param trigger [Symbol] The setup trigger (:init or :maintenance)
12
- # @param options [Options, nil] Additional configuration options
13
- # @return [Array<Message>] All messages received during setup
14
- #
15
- # @example Run init setup
16
- # messages = ClaudeAgent.run_setup
17
- # result = messages.last
18
- # puts "Setup completed" if result.success?
19
- #
20
- # @example Run init setup with custom options
21
- # options = ClaudeAgent::Options.new(cwd: "/my/project")
22
- # ClaudeAgent.run_setup(trigger: :init, options: options)
23
- #
24
- # @note The :maintenance trigger requires --maintenance flag which
25
- # continues into a conversation. For maintenance-only behavior,
26
- # use options with maintenance: true and handle accordingly.
27
- #
28
- def run_setup(trigger: :init, options: nil)
29
- options ||= Options.new
30
-
31
- case trigger
32
- when :init
33
- # Create new options with init_only set
34
- setup_options = Options.new(**options_to_hash(options).merge(init_only: true))
35
- when :maintenance
36
- # Note: There's no --maintenance-only flag, so we use --maintenance
37
- # which will continue into a conversation. The caller should handle this.
38
- setup_options = Options.new(**options_to_hash(options).merge(maintenance: true))
39
- else
40
- raise ArgumentError, "Invalid trigger: #{trigger}. Must be :init or :maintenance"
41
- end
42
-
43
- # Run with an empty prompt - setup hooks run before the prompt is processed
44
- query(prompt: "", options: setup_options).to_a
45
- end
46
-
47
5
  # One-shot query to Claude Code CLI
48
6
  #
49
7
  # This is a simple, stateless interface for sending a single prompt
@@ -151,17 +109,5 @@ module ClaudeAgent
151
109
  events&.reset!
152
110
  turn
153
111
  end
154
-
155
- private
156
-
157
- # Convert an Options object to a hash for merging
158
- # @param options [Options] The options object
159
- # @return [Hash] Hash of option values
160
- def options_to_hash(options)
161
- Options::ATTRIBUTES.each_with_object({}) do |attr, hash|
162
- value = options.send(attr)
163
- hash[attr] = value unless value.nil?
164
- end
165
- end
166
112
  end
167
113
  end