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.
@@ -0,0 +1,239 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClaudeAgent
4
+ # Represents a complete agent turn — everything from sending a prompt
5
+ # to receiving the final ResultMessage.
6
+ #
7
+ # Accumulates messages as they flow through and provides convenient
8
+ # accessors for text, tool use, usage data, and more. Eliminates the
9
+ # 5+ message type `case` statement that every app rewrites.
10
+ #
11
+ # @example Via Client
12
+ # turn = client.send_and_receive("Fix the bug in auth.rb")
13
+ # puts turn.text
14
+ # puts "Cost: $#{turn.cost}"
15
+ # puts "Tools used: #{turn.tool_uses.map(&:display_label).join(", ")}"
16
+ #
17
+ # @example With streaming block
18
+ # turn = client.send_and_receive("Fix the bug") do |msg|
19
+ # case msg
20
+ # when ClaudeAgent::AssistantMessage
21
+ # print msg.text
22
+ # end
23
+ # end
24
+ # puts "\nDone! #{turn.tool_uses.size} tools used"
25
+ #
26
+ # @example Via one-shot query
27
+ # turn = ClaudeAgent.query_turn(prompt: "What is 2+2?")
28
+ # puts turn.text
29
+ #
30
+ class TurnResult
31
+ # All messages received during this turn
32
+ # @return [Array<Message>]
33
+ attr_reader :messages
34
+
35
+ def initialize
36
+ @messages = []
37
+ @result = nil
38
+ end
39
+
40
+ # Append a message to this turn
41
+ #
42
+ # @param message [Message] Any SDK message
43
+ # @return [self]
44
+ def <<(message)
45
+ @messages << message
46
+ @result = message if message.is_a?(ResultMessage)
47
+ self
48
+ end
49
+
50
+ # The final ResultMessage, or nil if the turn is still in progress
51
+ # @return [ResultMessage, nil]
52
+ def result
53
+ @result
54
+ end
55
+
56
+ # Whether a ResultMessage has been received
57
+ # @return [Boolean]
58
+ def complete?
59
+ !@result.nil?
60
+ end
61
+
62
+ # --- Text & Thinking ---
63
+
64
+ # All text content concatenated across assistant messages
65
+ # @return [String]
66
+ def text
67
+ assistant_messages.map(&:text).join
68
+ end
69
+
70
+ # All thinking content concatenated across assistant messages
71
+ # @return [String]
72
+ def thinking
73
+ assistant_messages.map(&:thinking).join
74
+ end
75
+
76
+ # --- Tool Use ---
77
+
78
+ # All tool use blocks across all assistant messages
79
+ # @return [Array<ToolUseBlock, ServerToolUseBlock>]
80
+ def tool_uses
81
+ assistant_messages.flat_map { |m| m.content.select { |b| b.is_a?(ToolUseBlock) || b.is_a?(ServerToolUseBlock) } }
82
+ end
83
+
84
+ # All tool result blocks from user messages (system-generated tool responses)
85
+ # @return [Array<ToolResultBlock, ServerToolResultBlock>]
86
+ def tool_results
87
+ user_messages
88
+ .select { |m| m.content.is_a?(Array) }
89
+ .flat_map { |m| m.content.select { |b| b.is_a?(ToolResultBlock) || b.is_a?(ServerToolResultBlock) } }
90
+ end
91
+
92
+ # Tool use/result pairs matched by ID
93
+ #
94
+ # Each entry is a Hash with:
95
+ # - `:tool_use` — the ToolUseBlock or ServerToolUseBlock
96
+ # - `:tool_result` — the matching ToolResultBlock/ServerToolResultBlock (nil if not yet received)
97
+ #
98
+ # @return [Array<Hash>]
99
+ def tool_executions
100
+ results_by_id = tool_results.each_with_object({}) { |r, h| h[r.tool_use_id] = r }
101
+ tool_uses.map do |tool_use|
102
+ { tool_use: tool_use, tool_result: results_by_id[tool_use.id] }
103
+ end
104
+ end
105
+
106
+ # --- Result Accessors ---
107
+
108
+ # Token usage from the ResultMessage
109
+ # @return [Hash, nil]
110
+ def usage
111
+ @result&.usage
112
+ end
113
+
114
+ # Total cost in USD
115
+ # @return [Float, nil]
116
+ def cost
117
+ @result&.total_cost_usd
118
+ end
119
+
120
+ # Wall-clock duration in milliseconds
121
+ # @return [Integer, nil]
122
+ def duration_ms
123
+ @result&.duration_ms
124
+ end
125
+
126
+ # API-only duration in milliseconds
127
+ # @return [Integer, nil]
128
+ def duration_api_ms
129
+ @result&.duration_api_ms
130
+ end
131
+
132
+ # Session ID for resumption
133
+ # @return [String, nil]
134
+ def session_id
135
+ @result&.session_id
136
+ end
137
+
138
+ # Number of turns in the session
139
+ # @return [Integer, nil]
140
+ def num_turns
141
+ @result&.num_turns
142
+ end
143
+
144
+ # Why the model stopped generating
145
+ # @return [String, nil]
146
+ def stop_reason
147
+ @result&.stop_reason
148
+ end
149
+
150
+ # Model used for this turn (from first assistant message)
151
+ # @return [String, nil]
152
+ def model
153
+ assistant_messages.first&.model
154
+ end
155
+
156
+ # Per-model usage breakdown
157
+ # @return [Hash, nil]
158
+ def model_usage
159
+ @result&.model_usage
160
+ end
161
+
162
+ # Structured output (if requested via options)
163
+ # @return [Object, nil]
164
+ def structured_output
165
+ @result&.structured_output
166
+ end
167
+
168
+ # Tools that were denied by permission callbacks
169
+ # @return [Array<SDKPermissionDenial>]
170
+ def permission_denials
171
+ @result&.permission_denials || []
172
+ end
173
+
174
+ # Errors from the result
175
+ # @return [Array<String>]
176
+ def errors
177
+ @result&.errors || []
178
+ end
179
+
180
+ # --- Status ---
181
+
182
+ # Whether the turn completed successfully
183
+ # @return [Boolean]
184
+ def success?
185
+ @result&.success? || false
186
+ end
187
+
188
+ # Whether the turn ended with an error
189
+ # @return [Boolean]
190
+ def error?
191
+ @result&.error? || false
192
+ end
193
+
194
+ # The result subtype (e.g. "success", "error_max_turns")
195
+ # @return [String, nil]
196
+ def subtype
197
+ @result&.subtype
198
+ end
199
+
200
+ # --- Filtered Message Access ---
201
+
202
+ # All AssistantMessages in this turn
203
+ # @return [Array<AssistantMessage>]
204
+ def assistant_messages
205
+ @messages.select { |m| m.is_a?(AssistantMessage) }
206
+ end
207
+
208
+ # All UserMessages in this turn (including system-generated tool result messages)
209
+ # @return [Array<UserMessage, UserMessageReplay>]
210
+ def user_messages
211
+ @messages.select { |m| m.is_a?(UserMessage) || m.is_a?(UserMessageReplay) }
212
+ end
213
+
214
+ # All StreamEvents in this turn
215
+ # @return [Array<StreamEvent>]
216
+ def stream_events
217
+ @messages.select { |m| m.is_a?(StreamEvent) }
218
+ end
219
+
220
+ # All content blocks across all assistant messages
221
+ # @return [Array<TextBlock, ThinkingBlock, ToolUseBlock, ...>]
222
+ def content_blocks
223
+ assistant_messages.flat_map(&:content)
224
+ end
225
+
226
+ # --- Inspection ---
227
+
228
+ def inspect
229
+ status = complete? ? (success? ? "success" : "error") : "in_progress"
230
+ parts = [ "#<#{self.class} status=#{status}" ]
231
+ parts << "messages=#{@messages.size}"
232
+ parts << "text=#{text.length}chars" unless text.empty?
233
+ parts << "tools=#{tool_uses.size}" unless tool_uses.empty?
234
+ parts << "cost=$#{cost}" if cost
235
+ parts << "duration=#{duration_ms}ms" if duration_ms
236
+ "#{parts.join(" ")}>"
237
+ end
238
+ end
239
+ end
@@ -184,6 +184,35 @@ module ClaudeAgent
184
184
  # max_turns: 10
185
185
  # )
186
186
  #
187
+ # Session metadata returned by list_sessions (TypeScript SDK parity: SDKSessionInfo)
188
+ #
189
+ # @example
190
+ # session = SessionInfo.new(
191
+ # session_id: "abc-123",
192
+ # summary: "Fix login bug",
193
+ # last_modified: 1706000000000,
194
+ # file_size: 4096,
195
+ # custom_title: "Login fix",
196
+ # first_prompt: "Help me fix the login page",
197
+ # git_branch: "fix/login",
198
+ # cwd: "/Users/dev/myapp"
199
+ # )
200
+ #
201
+ SessionInfo = Data.define(
202
+ :session_id,
203
+ :summary,
204
+ :last_modified,
205
+ :file_size,
206
+ :custom_title,
207
+ :first_prompt,
208
+ :git_branch,
209
+ :cwd
210
+ ) do
211
+ def initialize(session_id:, summary:, last_modified:, file_size:, custom_title: nil, first_prompt: nil, git_branch: nil, cwd: nil)
212
+ super
213
+ end
214
+ end
215
+
187
216
  AgentDefinition = Data.define(
188
217
  :description,
189
218
  :prompt,
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ClaudeAgent
4
- VERSION = "0.7.7"
4
+ VERSION = "0.7.9"
5
5
  end
data/lib/claude_agent.rb CHANGED
@@ -16,15 +16,53 @@ require_relative "claude_agent/messages"
16
16
  require_relative "claude_agent/message_parser"
17
17
  require_relative "claude_agent/hooks"
18
18
  require_relative "claude_agent/permissions"
19
+ require_relative "claude_agent/permission_request"
20
+ require_relative "claude_agent/permission_queue"
19
21
  require_relative "claude_agent/control_protocol"
20
22
  require_relative "claude_agent/transport/base"
21
23
  require_relative "claude_agent/transport/subprocess"
22
24
  require_relative "claude_agent/mcp/tool"
23
25
  require_relative "claude_agent/mcp/server"
26
+ require_relative "claude_agent/cumulative_usage"
27
+ require_relative "claude_agent/event_handler"
28
+ require_relative "claude_agent/turn_result"
29
+ require_relative "claude_agent/tool_activity"
24
30
  require_relative "claude_agent/query"
25
31
  require_relative "claude_agent/client"
32
+ require_relative "claude_agent/conversation"
33
+ require_relative "claude_agent/list_sessions" # Session discovery (TypeScript SDK v0.2.53 parity)
26
34
  require_relative "claude_agent/session" # V2 Session API (unstable)
27
35
 
28
36
  module ClaudeAgent
29
- # Re-export key classes at module level for convenience
37
+ class << self
38
+ # Create a new Conversation
39
+ #
40
+ # @see Conversation#initialize
41
+ # @return [Conversation]
42
+ def conversation(**kwargs)
43
+ Conversation.new(**kwargs)
44
+ end
45
+
46
+ # List past sessions with metadata
47
+ #
48
+ # Reads session metadata directly from disk without spawning a CLI subprocess.
49
+ # Returns SessionInfo objects sorted by last modified time (most recent first).
50
+ #
51
+ # @param dir [String, nil] Directory to scope sessions to (includes git worktrees).
52
+ # When nil, returns sessions from all projects.
53
+ # @param limit [Integer, nil] Maximum number of sessions to return.
54
+ # @return [Array<SessionInfo>]
55
+ def list_sessions(dir: nil, limit: nil)
56
+ ListSessions.call(dir: dir, limit: limit)
57
+ end
58
+
59
+ # Resume a previous Conversation by session ID
60
+ #
61
+ # @param session_id [String] Session ID to resume
62
+ # @see Conversation.resume
63
+ # @return [Conversation]
64
+ def resume_conversation(session_id, **kwargs)
65
+ Conversation.resume(session_id, **kwargs)
66
+ end
67
+ end
30
68
  end