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
|
@@ -241,7 +241,7 @@ module ClaudeAgent
|
|
|
241
241
|
# Get the event type from the raw event
|
|
242
242
|
# @return [String, nil]
|
|
243
243
|
def event_type
|
|
244
|
-
event[
|
|
244
|
+
event[:type]
|
|
245
245
|
end
|
|
246
246
|
end
|
|
247
247
|
|
|
@@ -267,13 +267,13 @@ module ClaudeAgent
|
|
|
267
267
|
# Get the compaction trigger type
|
|
268
268
|
# @return [String] "manual" or "auto"
|
|
269
269
|
def trigger
|
|
270
|
-
compact_metadata[:trigger]
|
|
270
|
+
compact_metadata[:trigger]
|
|
271
271
|
end
|
|
272
272
|
|
|
273
273
|
# Get the token count before compaction
|
|
274
274
|
# @return [Integer, nil]
|
|
275
275
|
def pre_tokens
|
|
276
|
-
compact_metadata[:pre_tokens]
|
|
276
|
+
compact_metadata[:pre_tokens]
|
|
277
277
|
end
|
|
278
278
|
end
|
|
279
279
|
|
|
@@ -654,6 +654,41 @@ module ClaudeAgent
|
|
|
654
654
|
end
|
|
655
655
|
end
|
|
656
656
|
|
|
657
|
+
# Task progress message (TypeScript SDK v0.2.51 parity)
|
|
658
|
+
#
|
|
659
|
+
# Reports progress during background task (subagent) execution.
|
|
660
|
+
# Contains usage information and description of what the task is doing.
|
|
661
|
+
#
|
|
662
|
+
# @example
|
|
663
|
+
# msg = TaskProgressMessage.new(
|
|
664
|
+
# uuid: "msg-123",
|
|
665
|
+
# session_id: "session-abc",
|
|
666
|
+
# task_id: "task-456",
|
|
667
|
+
# description: "Searching codebase for patterns",
|
|
668
|
+
# usage: { total_tokens: 5000, tool_uses: 3, duration_ms: 2500 }
|
|
669
|
+
# )
|
|
670
|
+
#
|
|
671
|
+
TaskProgressMessage = Data.define(
|
|
672
|
+
:uuid, :session_id, :task_id, :tool_use_id,
|
|
673
|
+
:description, :usage, :last_tool_name
|
|
674
|
+
) do
|
|
675
|
+
def initialize(
|
|
676
|
+
uuid:,
|
|
677
|
+
session_id:,
|
|
678
|
+
task_id:,
|
|
679
|
+
description:,
|
|
680
|
+
usage: nil,
|
|
681
|
+
tool_use_id: nil,
|
|
682
|
+
last_tool_name: nil
|
|
683
|
+
)
|
|
684
|
+
super
|
|
685
|
+
end
|
|
686
|
+
|
|
687
|
+
def type
|
|
688
|
+
:task_progress
|
|
689
|
+
end
|
|
690
|
+
end
|
|
691
|
+
|
|
657
692
|
# Rate limit event (TypeScript SDK v0.2.45 parity)
|
|
658
693
|
#
|
|
659
694
|
# Reports rate limit status and utilization information.
|
|
@@ -663,12 +698,12 @@ module ClaudeAgent
|
|
|
663
698
|
# uuid: "msg-123",
|
|
664
699
|
# session_id: "session-abc",
|
|
665
700
|
# rate_limit_info: {
|
|
666
|
-
#
|
|
667
|
-
#
|
|
668
|
-
#
|
|
669
|
-
#
|
|
670
|
-
#
|
|
671
|
-
#
|
|
701
|
+
# status: "allowed_warning",
|
|
702
|
+
# resetsAt: 1700000000,
|
|
703
|
+
# rateLimitType: "five_hour",
|
|
704
|
+
# utilization: 0.85,
|
|
705
|
+
# isUsingOverage: false,
|
|
706
|
+
# overageStatus: "available"
|
|
672
707
|
# }
|
|
673
708
|
# )
|
|
674
709
|
# msg.status # => "allowed_warning"
|
|
@@ -685,7 +720,7 @@ module ClaudeAgent
|
|
|
685
720
|
# Get the rate limit status
|
|
686
721
|
# @return [String, nil]
|
|
687
722
|
def status
|
|
688
|
-
rate_limit_info[
|
|
723
|
+
rate_limit_info[:status]
|
|
689
724
|
end
|
|
690
725
|
end
|
|
691
726
|
|
|
@@ -719,11 +754,11 @@ module ClaudeAgent
|
|
|
719
754
|
# msg = FilesPersistedEvent.new(
|
|
720
755
|
# uuid: "msg-123",
|
|
721
756
|
# session_id: "session-abc",
|
|
722
|
-
# files: [{
|
|
757
|
+
# files: [{ filename: "test.rb", file_id: "file-456" }],
|
|
723
758
|
# failed: [],
|
|
724
759
|
# processed_at: "2026-01-30T12:00:00Z"
|
|
725
760
|
# )
|
|
726
|
-
# msg.files.first[
|
|
761
|
+
# msg.files.first[:filename] # => "test.rb"
|
|
727
762
|
#
|
|
728
763
|
FilesPersistedEvent = Data.define(
|
|
729
764
|
:uuid,
|
|
@@ -747,6 +782,42 @@ module ClaudeAgent
|
|
|
747
782
|
end
|
|
748
783
|
end
|
|
749
784
|
|
|
785
|
+
# Generic message for unknown/future protocol types
|
|
786
|
+
#
|
|
787
|
+
# Wraps unrecognized top-level message types so they can be inspected
|
|
788
|
+
# without crashing the application. Supports dynamic field access via
|
|
789
|
+
# `[]` and `method_missing`.
|
|
790
|
+
#
|
|
791
|
+
# @example
|
|
792
|
+
# msg = GenericMessage.new(message_type: "fancy_new", raw: { data: "hello" })
|
|
793
|
+
# msg.type # => :fancy_new
|
|
794
|
+
# msg[:data] # => "hello"
|
|
795
|
+
# msg.data # => "hello"
|
|
796
|
+
# msg.to_h # => { data: "hello" }
|
|
797
|
+
#
|
|
798
|
+
GenericMessage = Data.define(:message_type, :raw) do
|
|
799
|
+
def type
|
|
800
|
+
message_type&.to_sym || :unknown
|
|
801
|
+
end
|
|
802
|
+
|
|
803
|
+
def to_h
|
|
804
|
+
raw
|
|
805
|
+
end
|
|
806
|
+
|
|
807
|
+
def [](key)
|
|
808
|
+
raw[key]
|
|
809
|
+
end
|
|
810
|
+
|
|
811
|
+
def respond_to_missing?(name, include_private = false)
|
|
812
|
+
raw.key?(name) || super
|
|
813
|
+
end
|
|
814
|
+
|
|
815
|
+
def method_missing(name, *args)
|
|
816
|
+
return raw[name] if args.empty? && raw.key?(name)
|
|
817
|
+
super
|
|
818
|
+
end
|
|
819
|
+
end
|
|
820
|
+
|
|
750
821
|
# All message types
|
|
751
822
|
MESSAGE_TYPES = [
|
|
752
823
|
UserMessage,
|
|
@@ -766,7 +837,9 @@ module ClaudeAgent
|
|
|
766
837
|
ToolUseSummaryMessage,
|
|
767
838
|
FilesPersistedEvent,
|
|
768
839
|
TaskStartedMessage,
|
|
840
|
+
TaskProgressMessage,
|
|
769
841
|
RateLimitEvent,
|
|
770
|
-
PromptSuggestionMessage
|
|
842
|
+
PromptSuggestionMessage,
|
|
843
|
+
GenericMessage
|
|
771
844
|
].freeze
|
|
772
845
|
end
|
data/lib/claude_agent/options.rb
CHANGED
|
@@ -56,6 +56,7 @@ module ClaudeAgent
|
|
|
56
56
|
system_prompt append_system_prompt
|
|
57
57
|
model fallback_model
|
|
58
58
|
permission_mode permission_prompt_tool_name can_use_tool allow_dangerously_skip_permissions
|
|
59
|
+
permission_queue
|
|
59
60
|
continue_conversation resume fork_session resume_session_at session_id
|
|
60
61
|
max_turns max_budget_usd thinking effort max_thinking_tokens
|
|
61
62
|
strict_mcp_config mcp_servers hooks
|
|
@@ -309,10 +310,10 @@ module ClaudeAgent
|
|
|
309
310
|
raise ConfigurationError, "can_use_tool must be callable (Proc, Lambda, or object responding to #call)"
|
|
310
311
|
end
|
|
311
312
|
|
|
312
|
-
# Auto-set permission_prompt_tool_name to "stdio" when can_use_tool
|
|
313
|
-
#
|
|
314
|
-
# control protocol instead of interactive terminal prompts
|
|
315
|
-
if can_use_tool && !permission_prompt_tool_name
|
|
313
|
+
# Auto-set permission_prompt_tool_name to "stdio" when can_use_tool or
|
|
314
|
+
# permission_queue is configured, so the CLI routes permission prompts
|
|
315
|
+
# through the control protocol instead of interactive terminal prompts
|
|
316
|
+
if (can_use_tool || permission_queue) && !permission_prompt_tool_name
|
|
316
317
|
@permission_prompt_tool_name = "stdio"
|
|
317
318
|
end
|
|
318
319
|
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ClaudeAgent
|
|
4
|
+
# Thread-safe queue of pending permission requests.
|
|
5
|
+
#
|
|
6
|
+
# Provides both blocking and non-blocking access to permission
|
|
7
|
+
# requests that need resolution from the main thread.
|
|
8
|
+
#
|
|
9
|
+
# @example Non-blocking poll in a UI timer
|
|
10
|
+
# while request = client.permission_queue.poll
|
|
11
|
+
# show_dialog(request)
|
|
12
|
+
# end
|
|
13
|
+
#
|
|
14
|
+
# @example Blocking wait
|
|
15
|
+
# request = client.permission_queue.pop # blocks until available
|
|
16
|
+
# request.allow!
|
|
17
|
+
#
|
|
18
|
+
class PermissionQueue
|
|
19
|
+
def initialize
|
|
20
|
+
@queue = Queue.new
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Non-blocking poll for the next pending request.
|
|
24
|
+
#
|
|
25
|
+
# @return [PermissionRequest, nil] The next request, or nil if empty
|
|
26
|
+
def poll
|
|
27
|
+
@queue.pop(true)
|
|
28
|
+
rescue ThreadError
|
|
29
|
+
nil
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Blocking pop — waits for a request to arrive.
|
|
33
|
+
#
|
|
34
|
+
# @param timeout [Numeric, nil] Maximum seconds to wait (nil = wait forever)
|
|
35
|
+
# @return [PermissionRequest, nil] The request, or nil if timed out
|
|
36
|
+
def pop(timeout: nil)
|
|
37
|
+
if timeout
|
|
38
|
+
deadline = Time.now + timeout
|
|
39
|
+
loop do
|
|
40
|
+
request = poll
|
|
41
|
+
return request if request
|
|
42
|
+
remaining = deadline - Time.now
|
|
43
|
+
return nil if remaining <= 0
|
|
44
|
+
sleep([ remaining, 0.05 ].min)
|
|
45
|
+
end
|
|
46
|
+
else
|
|
47
|
+
@queue.pop
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Check if there are pending requests.
|
|
52
|
+
# @return [Boolean]
|
|
53
|
+
def empty?
|
|
54
|
+
@queue.empty?
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Number of pending requests.
|
|
58
|
+
# @return [Integer]
|
|
59
|
+
def size
|
|
60
|
+
@queue.size
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# @api private
|
|
64
|
+
# Push a request onto the queue (called by ControlProtocol).
|
|
65
|
+
#
|
|
66
|
+
# @param request [PermissionRequest]
|
|
67
|
+
# @return [void]
|
|
68
|
+
def push(request)
|
|
69
|
+
@queue.push(request)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Deny all pending requests and drain the queue.
|
|
73
|
+
#
|
|
74
|
+
# Used during abort/disconnect to unblock the reader thread.
|
|
75
|
+
#
|
|
76
|
+
# @param reason [String] Denial reason for all pending requests
|
|
77
|
+
# @return [Array<PermissionRequest>] The drained requests
|
|
78
|
+
def drain!(reason: "Operation aborted")
|
|
79
|
+
requests = []
|
|
80
|
+
while (request = poll)
|
|
81
|
+
request.deny!(message: reason) unless request.resolved?
|
|
82
|
+
requests << request
|
|
83
|
+
end
|
|
84
|
+
requests
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ClaudeAgent
|
|
4
|
+
# A deferred permission request that can be resolved from any thread.
|
|
5
|
+
#
|
|
6
|
+
# When the control protocol receives a can_use_tool request and
|
|
7
|
+
# queue-based permissions are enabled, it creates a PermissionRequest
|
|
8
|
+
# and enqueues it. The main/UI thread resolves it by calling
|
|
9
|
+
# {#allow!} or {#deny!}, which unblocks the reader thread.
|
|
10
|
+
#
|
|
11
|
+
# @example Resolving from a UI thread
|
|
12
|
+
# request = client.pending_permission
|
|
13
|
+
# puts "Tool: #{request.tool_name}, Input: #{request.input}"
|
|
14
|
+
# request.allow!
|
|
15
|
+
#
|
|
16
|
+
# @example Denying with a reason
|
|
17
|
+
# request.deny!(message: "Not allowed in production")
|
|
18
|
+
#
|
|
19
|
+
# @example Hybrid mode — defer from a can_use_tool callback
|
|
20
|
+
# can_use_tool: ->(name, input, context) {
|
|
21
|
+
# if name == "Read"
|
|
22
|
+
# ClaudeAgent::PermissionResultAllow.new # auto-allow reads
|
|
23
|
+
# else
|
|
24
|
+
# context.request.defer! # defer writes/bash to the UI
|
|
25
|
+
# end
|
|
26
|
+
# }
|
|
27
|
+
#
|
|
28
|
+
class PermissionRequest
|
|
29
|
+
attr_reader :tool_name, :input, :context, :request_id, :created_at
|
|
30
|
+
|
|
31
|
+
def initialize(tool_name:, input:, context:, request_id:)
|
|
32
|
+
@tool_name = tool_name
|
|
33
|
+
@input = input
|
|
34
|
+
@context = context
|
|
35
|
+
@request_id = request_id
|
|
36
|
+
@created_at = Time.now
|
|
37
|
+
|
|
38
|
+
@mutex = Mutex.new
|
|
39
|
+
@condition = ConditionVariable.new
|
|
40
|
+
@resolved = false
|
|
41
|
+
@deferred = false
|
|
42
|
+
@result = nil
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Allow the tool to execute.
|
|
46
|
+
#
|
|
47
|
+
# @param updated_input [Hash, nil] Modified input to send to the tool
|
|
48
|
+
# @param updated_permissions [Array<PermissionUpdate>, nil] Permission rule updates
|
|
49
|
+
# @return [void]
|
|
50
|
+
# @raise [Error] If already resolved
|
|
51
|
+
def allow!(updated_input: nil, updated_permissions: nil)
|
|
52
|
+
resolve!(PermissionResultAllow.new(
|
|
53
|
+
updated_input: updated_input,
|
|
54
|
+
updated_permissions: updated_permissions,
|
|
55
|
+
tool_use_id: context&.tool_use_id
|
|
56
|
+
))
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Deny the tool execution.
|
|
60
|
+
#
|
|
61
|
+
# @param message [String] Reason for denial
|
|
62
|
+
# @param interrupt [Boolean] Whether to interrupt the agent
|
|
63
|
+
# @return [void]
|
|
64
|
+
# @raise [Error] If already resolved
|
|
65
|
+
def deny!(message: "", interrupt: false)
|
|
66
|
+
resolve!(PermissionResultDeny.new(
|
|
67
|
+
message: message,
|
|
68
|
+
interrupt: interrupt,
|
|
69
|
+
tool_use_id: context&.tool_use_id
|
|
70
|
+
))
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Mark this request as deferred (enqueue instead of resolving synchronously).
|
|
74
|
+
#
|
|
75
|
+
# Called from within a can_use_tool callback to signal that the
|
|
76
|
+
# callback will not return an answer. The protocol will enqueue
|
|
77
|
+
# the request and wait for {#allow!} or {#deny!} from another thread.
|
|
78
|
+
#
|
|
79
|
+
# @return [self]
|
|
80
|
+
def defer!
|
|
81
|
+
@mutex.synchronize { @deferred = true }
|
|
82
|
+
self
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Check if this request was deferred by a callback.
|
|
86
|
+
# @return [Boolean]
|
|
87
|
+
def deferred?
|
|
88
|
+
@mutex.synchronize { @deferred }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Check if this request has been resolved.
|
|
92
|
+
# @return [Boolean]
|
|
93
|
+
def resolved?
|
|
94
|
+
@mutex.synchronize { @resolved }
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Check if this request is still pending.
|
|
98
|
+
# @return [Boolean]
|
|
99
|
+
def pending?
|
|
100
|
+
!resolved?
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Get the result (nil if not yet resolved).
|
|
104
|
+
# @return [PermissionResultAllow, PermissionResultDeny, nil]
|
|
105
|
+
def result
|
|
106
|
+
@mutex.synchronize { @result }
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# @api private
|
|
110
|
+
# Block until resolved (called by ControlProtocol reader thread).
|
|
111
|
+
#
|
|
112
|
+
# @param timeout [Numeric, nil] Timeout in seconds
|
|
113
|
+
# @return [PermissionResultAllow, PermissionResultDeny]
|
|
114
|
+
# @raise [TimeoutError] If timeout expires before resolution
|
|
115
|
+
def wait(timeout: nil)
|
|
116
|
+
@mutex.synchronize do
|
|
117
|
+
unless @resolved
|
|
118
|
+
deadline = timeout ? Time.now + timeout : nil
|
|
119
|
+
until @resolved
|
|
120
|
+
remaining = deadline ? deadline - Time.now : nil
|
|
121
|
+
if remaining && remaining <= 0
|
|
122
|
+
raise TimeoutError.new(
|
|
123
|
+
"Permission request timed out",
|
|
124
|
+
request_id: request_id,
|
|
125
|
+
timeout_seconds: timeout
|
|
126
|
+
)
|
|
127
|
+
end
|
|
128
|
+
@condition.wait(@mutex, remaining ? [ remaining, 0.5 ].min : 0.5)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
@result
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def inspect
|
|
136
|
+
status = resolved? ? "resolved(#{@result&.behavior})" : "pending"
|
|
137
|
+
"#<#{self.class} tool=#{tool_name} status=#{status} age=#{(Time.now - created_at).round(1)}s>"
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
private
|
|
141
|
+
|
|
142
|
+
def resolve!(result)
|
|
143
|
+
@mutex.synchronize do
|
|
144
|
+
raise Error, "Permission request already resolved" if @resolved
|
|
145
|
+
@result = result
|
|
146
|
+
@resolved = true
|
|
147
|
+
@condition.broadcast
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
@@ -157,7 +157,8 @@ module ClaudeAgent
|
|
|
157
157
|
:tool_use_id,
|
|
158
158
|
:agent_id,
|
|
159
159
|
:signal,
|
|
160
|
-
:description
|
|
160
|
+
:description,
|
|
161
|
+
:request
|
|
161
162
|
) do
|
|
162
163
|
def initialize(
|
|
163
164
|
permission_suggestions: nil,
|
|
@@ -166,7 +167,8 @@ module ClaudeAgent
|
|
|
166
167
|
tool_use_id: nil,
|
|
167
168
|
agent_id: nil,
|
|
168
169
|
signal: nil,
|
|
169
|
-
description: nil
|
|
170
|
+
description: nil,
|
|
171
|
+
request: nil
|
|
170
172
|
)
|
|
171
173
|
super
|
|
172
174
|
end
|
data/lib/claude_agent/query.rb
CHANGED
|
@@ -118,6 +118,40 @@ module ClaudeAgent
|
|
|
118
118
|
end
|
|
119
119
|
end
|
|
120
120
|
|
|
121
|
+
# One-shot query that returns a TurnResult
|
|
122
|
+
#
|
|
123
|
+
# Like {query}, but accumulates all messages into a {TurnResult}
|
|
124
|
+
# for convenient access to text, tool use, usage, and more.
|
|
125
|
+
#
|
|
126
|
+
# @param prompt [String] The prompt to send to Claude
|
|
127
|
+
# @param options [Options, nil] Configuration options
|
|
128
|
+
# @param transport [Transport::Base, nil] Custom transport
|
|
129
|
+
# @param events [EventHandler, nil] Event handler for dispatching events
|
|
130
|
+
# @yield [Message] Each message as it arrives (optional)
|
|
131
|
+
# @return [TurnResult] The completed turn
|
|
132
|
+
#
|
|
133
|
+
# @example Simple
|
|
134
|
+
# turn = ClaudeAgent.query_turn(prompt: "What is 2+2?")
|
|
135
|
+
# puts turn.text
|
|
136
|
+
# puts "Cost: $#{turn.cost}"
|
|
137
|
+
#
|
|
138
|
+
# @example With events
|
|
139
|
+
# events = ClaudeAgent::EventHandler.new
|
|
140
|
+
# .on_text { |text| print text }
|
|
141
|
+
# .on_result { |r| puts "\nCost: $#{r.total_cost_usd}" }
|
|
142
|
+
# turn = ClaudeAgent.query_turn(prompt: "Explain Ruby", events: events)
|
|
143
|
+
#
|
|
144
|
+
def query_turn(prompt:, options: nil, transport: nil, events: nil)
|
|
145
|
+
turn = TurnResult.new
|
|
146
|
+
query(prompt: prompt, options: options, transport: transport).each do |message|
|
|
147
|
+
turn << message
|
|
148
|
+
events&.handle(message)
|
|
149
|
+
yield message if block_given?
|
|
150
|
+
end
|
|
151
|
+
events&.reset!
|
|
152
|
+
turn
|
|
153
|
+
end
|
|
154
|
+
|
|
121
155
|
private
|
|
122
156
|
|
|
123
157
|
# Convert an Options object to a hash for merging
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ClaudeAgent
|
|
4
|
+
# A single tool execution in the conversation timeline.
|
|
5
|
+
#
|
|
6
|
+
# Pairs a ToolUseBlock with its ToolResultBlock and adds context
|
|
7
|
+
# about which turn it occurred in and when.
|
|
8
|
+
#
|
|
9
|
+
# @example
|
|
10
|
+
# conversation.tool_activity.each do |activity|
|
|
11
|
+
# puts activity.display_label
|
|
12
|
+
# puts " Turn: #{activity.turn_index}"
|
|
13
|
+
# puts " Duration: #{activity.duration}s" if activity.duration
|
|
14
|
+
# puts " Error!" if activity.error?
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
ToolActivity = Data.define(
|
|
18
|
+
:tool_use,
|
|
19
|
+
:tool_result,
|
|
20
|
+
:turn_index,
|
|
21
|
+
:started_at,
|
|
22
|
+
:completed_at
|
|
23
|
+
) do
|
|
24
|
+
def initialize(tool_use:, tool_result: nil, turn_index:, started_at: nil, completed_at: nil)
|
|
25
|
+
super
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Tool name
|
|
29
|
+
# @return [String]
|
|
30
|
+
def name
|
|
31
|
+
tool_use.name
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Human-readable label (delegates to ToolUseBlock#display_label)
|
|
35
|
+
# @return [String]
|
|
36
|
+
def display_label
|
|
37
|
+
tool_use.display_label
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Detailed summary (delegates to ToolUseBlock#summary)
|
|
41
|
+
# @param max [Integer] Maximum length
|
|
42
|
+
# @return [String]
|
|
43
|
+
def summary(max: 60)
|
|
44
|
+
tool_use.summary(max: max)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# File path if this is a file-based tool
|
|
48
|
+
# @return [String, nil]
|
|
49
|
+
def file_path
|
|
50
|
+
tool_use.file_path
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Tool use ID
|
|
54
|
+
# @return [String]
|
|
55
|
+
def id
|
|
56
|
+
tool_use.id
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Whether the tool produced an error result
|
|
60
|
+
# @return [Boolean]
|
|
61
|
+
def error?
|
|
62
|
+
tool_result&.is_error == true
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Whether the tool execution is complete (has a result)
|
|
66
|
+
# @return [Boolean]
|
|
67
|
+
def complete?
|
|
68
|
+
!tool_result.nil?
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Duration in seconds (nil if timing not available)
|
|
72
|
+
# @return [Float, nil]
|
|
73
|
+
def duration
|
|
74
|
+
return nil unless started_at && completed_at
|
|
75
|
+
completed_at - started_at
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|