ai-agents 0.6.0 → 0.7.0
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 +16 -0
- data/docs/concepts/callbacks.md +36 -2
- data/lib/agents/agent_runner.rb +39 -0
- data/lib/agents/callback_manager.rb +3 -0
- data/lib/agents/helpers/headers.rb +22 -18
- data/lib/agents/helpers/message_extractor.rb +62 -58
- data/lib/agents/runner.rb +43 -6
- data/lib/agents/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c143345c7d0dd3a91ff483e0db22a7d23f6c85393275727eddcaf288a1341901
|
4
|
+
data.tar.gz: 0f1abfe692571706be41b85a1da924a717f980b8721b21ece7dd62ad4e995fbf
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 74c96799a138a5e725b3e889eba22c45e9581e7606a0cd235cecd50b8bdde1c6f10281ec1516fde77f48373c39022d2f72172be8c29489260df79fb575d92de8
|
7
|
+
data.tar.gz: 257fd42a178107182a53eb737848ae1a023c28712ac450c4603b12cb60e2ad39dbe7b957a324bd6c24a3a175af8b7c373ec30e25e36425e81642a8f160da7ceb
|
data/CHANGELOG.md
CHANGED
@@ -5,6 +5,22 @@ All notable changes to this project will be documented in this file.
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
7
7
|
|
8
|
+
## [0.7.0] - 2025-10-16
|
9
|
+
|
10
|
+
### Added
|
11
|
+
- **Lifecycle Callback Hooks**: New callbacks for complete execution visibility and observability integration
|
12
|
+
- Added `on_run_start` callback triggered before agent execution begins with agent name, input, and run context
|
13
|
+
- Added `on_run_complete` callback triggered after execution ends (success or failure) with agent name, result, and run context
|
14
|
+
- Added `on_agent_complete` callback triggered after each agent turn with agent name, result, error (if any), and run context
|
15
|
+
- Run context parameter enables storing and retrieving custom data (e.g., span context, trace IDs) throughout execution
|
16
|
+
- Designed for integration with observability platforms (OpenTelemetry, Datadog, New Relic, etc.)
|
17
|
+
- All callbacks are thread-safe and non-blocking with proper error handling
|
18
|
+
- Updated callback documentation with integration patterns for UI feedback, logging, and metrics
|
19
|
+
|
20
|
+
### Changed
|
21
|
+
- CallbackManager now supports 7 event types (previously 4)
|
22
|
+
- Enhanced callback system to provide complete lifecycle coverage for monitoring and tracing
|
23
|
+
|
8
24
|
## [0.6.0] - 2025-10-16
|
9
25
|
|
10
26
|
### Added
|
data/docs/concepts/callbacks.md
CHANGED
@@ -11,7 +11,13 @@ The AI Agents SDK provides real-time callbacks that allow you to monitor agent e
|
|
11
11
|
|
12
12
|
## Available Callbacks
|
13
13
|
|
14
|
-
The SDK provides
|
14
|
+
The SDK provides seven types of callbacks that give you visibility into different stages of agent execution:
|
15
|
+
|
16
|
+
**Run Start** - Triggered before agent execution begins. Receives the agent name, input message, and run context.
|
17
|
+
|
18
|
+
**Run Complete** - Called after agent execution ends (whether successful or failed). Receives the agent name, result object, and run context.
|
19
|
+
|
20
|
+
**Agent Complete** - Triggered after each agent turn finishes. Receives the agent name, result, error (if any), and run context.
|
15
21
|
|
16
22
|
**Agent Thinking** - Triggered when an agent is about to make an LLM call. Useful for showing "thinking" indicators in UIs.
|
17
23
|
|
@@ -27,6 +33,9 @@ Callbacks are registered on the AgentRunner using chainable methods:
|
|
27
33
|
|
28
34
|
```ruby
|
29
35
|
runner = Agents::Runner.with_agents(triage, support)
|
36
|
+
.on_run_start { |agent, input, ctx| puts "Starting: #{agent}" }
|
37
|
+
.on_run_complete { |agent, result, ctx| puts "Completed: #{agent}" }
|
38
|
+
.on_agent_complete { |agent, result, error, ctx| puts "Agent done: #{agent}" }
|
30
39
|
.on_agent_thinking { |agent, input| puts "#{agent} thinking..." }
|
31
40
|
.on_tool_start { |tool, args| puts "Using #{tool}" }
|
32
41
|
.on_tool_complete { |tool, result| puts "#{tool} completed" }
|
@@ -35,7 +44,32 @@ runner = Agents::Runner.with_agents(triage, support)
|
|
35
44
|
|
36
45
|
## Integration Patterns
|
37
46
|
|
38
|
-
|
47
|
+
### UI Feedback
|
48
|
+
|
49
|
+
Callbacks work well with real-time web frameworks like Rails ActionCable, allowing you to stream agent status updates directly to browser clients:
|
50
|
+
|
51
|
+
```ruby
|
52
|
+
runner = Agents::Runner.with_agents(agent)
|
53
|
+
.on_agent_thinking { |agent, input|
|
54
|
+
ActionCable.server.broadcast("agent_#{user_id}", { type: 'thinking', agent: agent })
|
55
|
+
}
|
56
|
+
.on_tool_start { |tool, args|
|
57
|
+
ActionCable.server.broadcast("agent_#{user_id}", { type: 'tool', name: tool })
|
58
|
+
}
|
59
|
+
```
|
60
|
+
|
61
|
+
### Logging & Metrics
|
62
|
+
|
63
|
+
Callbacks are also useful for structured logging and metrics collection:
|
64
|
+
|
65
|
+
```ruby
|
66
|
+
runner = Agents::Runner.with_agents(agent)
|
67
|
+
.on_run_start { |agent, input, ctx| logger.info("Run started", agent: agent) }
|
68
|
+
.on_tool_start { |tool, args| metrics.increment("tool.calls", tags: ["tool:#{tool}"]) }
|
69
|
+
.on_agent_complete do |agent, result, error, ctx|
|
70
|
+
logger.error("Agent failed", agent: agent, error: error) if error
|
71
|
+
end
|
72
|
+
```
|
39
73
|
|
40
74
|
## Thread Safety
|
41
75
|
|
data/lib/agents/agent_runner.rb
CHANGED
@@ -44,6 +44,9 @@ module Agents
|
|
44
44
|
|
45
45
|
# Initialize callback storage - use thread-safe arrays
|
46
46
|
@callbacks = {
|
47
|
+
run_start: [],
|
48
|
+
run_complete: [],
|
49
|
+
agent_complete: [],
|
47
50
|
tool_start: [],
|
48
51
|
tool_complete: [],
|
49
52
|
agent_thinking: [],
|
@@ -125,6 +128,42 @@ module Agents
|
|
125
128
|
self
|
126
129
|
end
|
127
130
|
|
131
|
+
# Register a callback for run start events.
|
132
|
+
# Called before agent execution begins.
|
133
|
+
#
|
134
|
+
# @param block [Proc] Callback block that receives (agent, input, run_context)
|
135
|
+
# @return [self] For method chaining
|
136
|
+
def on_run_start(&block)
|
137
|
+
return self unless block
|
138
|
+
|
139
|
+
@callbacks_mutex.synchronize { @callbacks[:run_start] << block }
|
140
|
+
self
|
141
|
+
end
|
142
|
+
|
143
|
+
# Register a callback for run complete events.
|
144
|
+
# Called after agent execution ends (success or error).
|
145
|
+
#
|
146
|
+
# @param block [Proc] Callback block that receives (agent, result, run_context)
|
147
|
+
# @return [self] For method chaining
|
148
|
+
def on_run_complete(&block)
|
149
|
+
return self unless block
|
150
|
+
|
151
|
+
@callbacks_mutex.synchronize { @callbacks[:run_complete] << block }
|
152
|
+
self
|
153
|
+
end
|
154
|
+
|
155
|
+
# Register a callback for agent complete events.
|
156
|
+
# Called after each agent turn finishes.
|
157
|
+
#
|
158
|
+
# @param block [Proc] Callback block that receives (agent_name, result, error, run_context)
|
159
|
+
# @return [self] For method chaining
|
160
|
+
def on_agent_complete(&block)
|
161
|
+
return self unless block
|
162
|
+
|
163
|
+
@callbacks_mutex.synchronize { @callbacks[:agent_complete] << block }
|
164
|
+
self
|
165
|
+
end
|
166
|
+
|
128
167
|
private
|
129
168
|
|
130
169
|
# Build agent registry from provided agents only.
|
@@ -1,29 +1,33 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
module Agents
|
4
|
-
|
3
|
+
module Agents
|
4
|
+
module Helpers
|
5
|
+
module Headers
|
6
|
+
module_function
|
5
7
|
|
6
|
-
|
7
|
-
|
8
|
+
def normalize(headers, freeze_result: false)
|
9
|
+
return freeze_result ? {}.freeze : {} if headers.nil? || (headers.respond_to?(:empty?) && headers.empty?)
|
8
10
|
|
9
|
-
|
10
|
-
|
11
|
+
hash = headers.respond_to?(:to_h) ? headers.to_h : headers
|
12
|
+
raise ArgumentError, "headers must be a Hash or respond to #to_h" unless hash.is_a?(Hash)
|
11
13
|
|
12
|
-
|
13
|
-
|
14
|
-
|
14
|
+
result = symbolize_keys(hash)
|
15
|
+
freeze_result ? result.freeze : result
|
16
|
+
end
|
15
17
|
|
16
|
-
|
17
|
-
|
18
|
-
|
18
|
+
def merge(agent_headers, runtime_headers)
|
19
|
+
return runtime_headers if agent_headers.empty?
|
20
|
+
return agent_headers if runtime_headers.empty?
|
19
21
|
|
20
|
-
|
21
|
-
|
22
|
+
agent_headers.merge(runtime_headers) { |_key, _agent_value, runtime_value| runtime_value }
|
23
|
+
end
|
22
24
|
|
23
|
-
|
24
|
-
|
25
|
-
|
25
|
+
def symbolize_keys(hash)
|
26
|
+
hash.transform_keys do |key|
|
27
|
+
key.is_a?(Symbol) ? key : key.to_sym
|
28
|
+
end
|
29
|
+
end
|
30
|
+
private_class_method :symbolize_keys
|
26
31
|
end
|
27
32
|
end
|
28
|
-
private_class_method :symbolize_keys
|
29
33
|
end
|
@@ -15,74 +15,78 @@
|
|
15
15
|
# { role: :assistant, content: "Hi!", agent_name: "Support", tool_calls: [...] },
|
16
16
|
# { role: :tool, content: "Result", tool_call_id: "call_123" }
|
17
17
|
# ]
|
18
|
-
module Agents
|
19
|
-
|
18
|
+
module Agents
|
19
|
+
module Helpers
|
20
|
+
module MessageExtractor
|
21
|
+
module_function
|
20
22
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
23
|
+
# Check if content is considered empty (handles both String and Hash content)
|
24
|
+
#
|
25
|
+
# @param content [String, Hash, nil] The content to check
|
26
|
+
# @return [Boolean] true if content is empty, false otherwise
|
27
|
+
def content_empty?(content)
|
28
|
+
case content
|
29
|
+
when String
|
30
|
+
content.strip.empty?
|
31
|
+
when Hash
|
32
|
+
content.empty?
|
33
|
+
else
|
34
|
+
content.nil?
|
35
|
+
end
|
36
|
+
end
|
35
37
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
38
|
+
# Extract messages from a chat object for conversation history persistence
|
39
|
+
#
|
40
|
+
# @param chat [Object] Chat object that responds to :messages
|
41
|
+
# @param current_agent [Agent] The agent currently handling the conversation
|
42
|
+
# @return [Array<Hash>] Array of message hashes suitable for persistence
|
43
|
+
def extract_messages(chat, current_agent)
|
44
|
+
return [] unless chat.respond_to?(:messages)
|
43
45
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
46
|
+
chat.messages.filter_map do |msg|
|
47
|
+
case msg.role
|
48
|
+
when :user, :assistant
|
49
|
+
extract_user_or_assistant_message(msg, current_agent)
|
50
|
+
when :tool
|
51
|
+
extract_tool_message(msg)
|
52
|
+
end
|
53
|
+
end
|
50
54
|
end
|
51
|
-
end
|
52
|
-
end
|
53
55
|
|
54
|
-
|
55
|
-
|
56
|
+
def extract_user_or_assistant_message(msg, current_agent)
|
57
|
+
return nil unless msg.content && !content_empty?(msg.content)
|
56
58
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
59
|
+
message = {
|
60
|
+
role: msg.role,
|
61
|
+
content: msg.content
|
62
|
+
}
|
61
63
|
|
62
|
-
|
63
|
-
|
64
|
-
|
64
|
+
if msg.role == :assistant
|
65
|
+
# Add agent attribution for conversation continuity
|
66
|
+
message[:agent_name] = current_agent.name if current_agent
|
65
67
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
68
|
+
# Add tool calls if present
|
69
|
+
if msg.tool_call? && msg.tool_calls
|
70
|
+
# RubyLLM stores tool_calls as Hash with call_id => ToolCall object
|
71
|
+
# Reference: RubyLLM::StreamAccumulator#tool_calls_from_stream
|
72
|
+
message[:tool_calls] = msg.tool_calls.values.map(&:to_h)
|
73
|
+
end
|
74
|
+
end
|
73
75
|
|
74
|
-
|
75
|
-
|
76
|
-
|
76
|
+
message
|
77
|
+
end
|
78
|
+
private_class_method :extract_user_or_assistant_message
|
77
79
|
|
78
|
-
|
79
|
-
|
80
|
+
def extract_tool_message(msg)
|
81
|
+
return nil unless msg.tool_result?
|
80
82
|
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
83
|
+
{
|
84
|
+
role: msg.role,
|
85
|
+
content: msg.content,
|
86
|
+
tool_call_id: msg.tool_call_id
|
87
|
+
}
|
88
|
+
end
|
89
|
+
private_class_method :extract_tool_message
|
90
|
+
end
|
86
91
|
end
|
87
|
-
private_class_method :extract_tool_message
|
88
92
|
end
|
data/lib/agents/runner.rb
CHANGED
@@ -90,6 +90,9 @@ module Agents
|
|
90
90
|
context_wrapper = RunContext.new(context_copy, callbacks: callbacks)
|
91
91
|
current_turn = 0
|
92
92
|
|
93
|
+
# Emit run start event
|
94
|
+
context_wrapper.callback_manager.emit_run_start(current_agent.name, input, context_wrapper)
|
95
|
+
|
93
96
|
runtime_headers = Helpers::Headers.normalize(headers)
|
94
97
|
agent_headers = Helpers::Headers.normalize(current_agent.headers)
|
95
98
|
|
@@ -100,7 +103,6 @@ module Agents
|
|
100
103
|
configure_chat_for_agent(chat, current_agent, context_wrapper, replace: false)
|
101
104
|
restore_conversation_history(chat, context_wrapper)
|
102
105
|
|
103
|
-
|
104
106
|
loop do
|
105
107
|
current_turn += 1
|
106
108
|
raise MaxTurnsExceeded, "Exceeded maximum turns: #{max_turns}" if current_turn > max_turns
|
@@ -127,18 +129,28 @@ module Agents
|
|
127
129
|
unless registry[next_agent.name]
|
128
130
|
save_conversation_state(chat, context_wrapper, current_agent)
|
129
131
|
error = AgentNotFoundError.new("Handoff failed: Agent '#{next_agent.name}' not found in registry")
|
130
|
-
|
132
|
+
|
133
|
+
result = RunResult.new(
|
131
134
|
output: nil,
|
132
135
|
messages: Helpers::MessageExtractor.extract_messages(chat, current_agent),
|
133
136
|
usage: context_wrapper.usage,
|
134
137
|
context: context_wrapper.context,
|
135
138
|
error: error
|
136
139
|
)
|
140
|
+
|
141
|
+
# Emit agent complete and run complete events with error
|
142
|
+
context_wrapper.callback_manager.emit_agent_complete(current_agent.name, result, error, context_wrapper)
|
143
|
+
context_wrapper.callback_manager.emit_run_complete(current_agent.name, result, context_wrapper)
|
144
|
+
|
145
|
+
return result
|
137
146
|
end
|
138
147
|
|
139
148
|
# Save current conversation state before switching
|
140
149
|
save_conversation_state(chat, context_wrapper, current_agent)
|
141
150
|
|
151
|
+
# Emit agent complete event before handoff
|
152
|
+
context_wrapper.callback_manager.emit_agent_complete(current_agent.name, nil, nil, context_wrapper)
|
153
|
+
|
142
154
|
# Emit agent handoff event
|
143
155
|
context_wrapper.callback_manager.emit_agent_handoff(current_agent.name, next_agent.name, "handoff")
|
144
156
|
|
@@ -161,12 +173,19 @@ module Agents
|
|
161
173
|
# Handle non-handoff halts - return the halt content as final response
|
162
174
|
if response.is_a?(RubyLLM::Tool::Halt)
|
163
175
|
save_conversation_state(chat, context_wrapper, current_agent)
|
164
|
-
|
176
|
+
|
177
|
+
result = RunResult.new(
|
165
178
|
output: response.content,
|
166
179
|
messages: Helpers::MessageExtractor.extract_messages(chat, current_agent),
|
167
180
|
usage: context_wrapper.usage,
|
168
181
|
context: context_wrapper.context
|
169
182
|
)
|
183
|
+
|
184
|
+
# Emit agent complete and run complete events
|
185
|
+
context_wrapper.callback_manager.emit_agent_complete(current_agent.name, result, nil, context_wrapper)
|
186
|
+
context_wrapper.callback_manager.emit_run_complete(current_agent.name, result, context_wrapper)
|
187
|
+
|
188
|
+
return result
|
170
189
|
end
|
171
190
|
|
172
191
|
# If tools were called, continue the loop to let them execute
|
@@ -177,35 +196,53 @@ module Agents
|
|
177
196
|
# Save final state before returning
|
178
197
|
save_conversation_state(chat, context_wrapper, current_agent)
|
179
198
|
|
180
|
-
|
199
|
+
result = RunResult.new(
|
181
200
|
output: response.content,
|
182
201
|
messages: Helpers::MessageExtractor.extract_messages(chat, current_agent),
|
183
202
|
usage: context_wrapper.usage,
|
184
203
|
context: context_wrapper.context
|
185
204
|
)
|
205
|
+
|
206
|
+
# Emit agent complete and run complete events
|
207
|
+
context_wrapper.callback_manager.emit_agent_complete(current_agent.name, result, nil, context_wrapper)
|
208
|
+
context_wrapper.callback_manager.emit_run_complete(current_agent.name, result, context_wrapper)
|
209
|
+
|
210
|
+
return result
|
186
211
|
end
|
187
212
|
rescue MaxTurnsExceeded => e
|
188
213
|
# Save state even on error
|
189
214
|
save_conversation_state(chat, context_wrapper, current_agent) if chat
|
190
215
|
|
191
|
-
RunResult.new(
|
216
|
+
result = RunResult.new(
|
192
217
|
output: "Conversation ended: #{e.message}",
|
193
218
|
messages: chat ? Helpers::MessageExtractor.extract_messages(chat, current_agent) : [],
|
194
219
|
usage: context_wrapper.usage,
|
195
220
|
error: e,
|
196
221
|
context: context_wrapper.context
|
197
222
|
)
|
223
|
+
|
224
|
+
# Emit agent complete and run complete events with error
|
225
|
+
context_wrapper.callback_manager.emit_agent_complete(current_agent.name, result, e, context_wrapper)
|
226
|
+
context_wrapper.callback_manager.emit_run_complete(current_agent.name, result, context_wrapper)
|
227
|
+
|
228
|
+
result
|
198
229
|
rescue StandardError => e
|
199
230
|
# Save state even on error
|
200
231
|
save_conversation_state(chat, context_wrapper, current_agent) if chat
|
201
232
|
|
202
|
-
RunResult.new(
|
233
|
+
result = RunResult.new(
|
203
234
|
output: nil,
|
204
235
|
messages: chat ? Helpers::MessageExtractor.extract_messages(chat, current_agent) : [],
|
205
236
|
usage: context_wrapper.usage,
|
206
237
|
error: e,
|
207
238
|
context: context_wrapper.context
|
208
239
|
)
|
240
|
+
|
241
|
+
# Emit agent complete and run complete events with error
|
242
|
+
context_wrapper.callback_manager.emit_agent_complete(current_agent.name, result, e, context_wrapper)
|
243
|
+
context_wrapper.callback_manager.emit_run_complete(current_agent.name, result, context_wrapper)
|
244
|
+
|
245
|
+
result
|
209
246
|
end
|
210
247
|
|
211
248
|
private
|
data/lib/agents/version.rb
CHANGED