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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +65 -0
- data/README.md +551 -37
- data/SPEC.md +70 -30
- data/lib/claude_agent/client.rb +197 -7
- data/lib/claude_agent/content_blocks.rb +193 -5
- data/lib/claude_agent/control_protocol.rb +111 -11
- data/lib/claude_agent/conversation.rb +248 -0
- data/lib/claude_agent/cumulative_usage.rb +106 -0
- data/lib/claude_agent/event_handler.rb +152 -0
- data/lib/claude_agent/hooks.rb +106 -225
- data/lib/claude_agent/list_sessions.rb +508 -0
- data/lib/claude_agent/mcp/server.rb +3 -3
- data/lib/claude_agent/mcp/tool.rb +4 -4
- data/lib/claude_agent/message_parser.rb +201 -185
- data/lib/claude_agent/messages.rb +86 -13
- data/lib/claude_agent/options.rb +5 -4
- data/lib/claude_agent/permission_queue.rb +87 -0
- data/lib/claude_agent/permission_request.rb +151 -0
- data/lib/claude_agent/permissions.rb +4 -2
- data/lib/claude_agent/query.rb +34 -0
- data/lib/claude_agent/tool_activity.rb +78 -0
- data/lib/claude_agent/turn_result.rb +239 -0
- data/lib/claude_agent/types.rb +29 -0
- data/lib/claude_agent/version.rb +1 -1
- data/lib/claude_agent.rb +39 -1
- data/sig/claude_agent.rbs +285 -4
- metadata +9 -1
|
@@ -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
|
data/lib/claude_agent/types.rb
CHANGED
|
@@ -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,
|
data/lib/claude_agent/version.rb
CHANGED
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
|
-
|
|
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
|