ai-agents 0.6.0 → 0.8.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 +29 -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 +70 -57
- data/lib/agents/run_context.rb +13 -7
- data/lib/agents/runner.rb +134 -16
- data/lib/agents/tool_wrapper.rb +9 -0
- data/lib/agents/version.rb +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6391e30443ff9e226e6b3bf3f629f3cd996cbb7fe4479c7a7dc437c82544ae2f
|
|
4
|
+
data.tar.gz: 22fc6ee4f3130006c1dd1f6c8fb704a176457d35602a434088d25cea5dd3c949
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ae3866cfbec885088c5b41b0e91bbc8532fc3115ed981c3428f56c09b40c1a75bc9bb1277ed1991f19cee4c43118c3f9a5d9ea8d6ecc07d29ad03117a77666e6
|
|
7
|
+
data.tar.gz: b7de2e98dc3ce52b4c80b7b1bcfa4fb1f07f62176d861ab61b9a1092d2de4273657d54edfa18d6afd83acccee21c89e465f18539b8966c5f9645a673b4e5a3c4
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,35 @@ 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
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.8.0] - 2026-01-07
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **Provider Smoke Tests**: Comprehensive smoke tests for validating against the latest RubyLLM version
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- **RubyLLM Update**: Updated to latest RubyLLM version with improved API integration and tool call handling
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
- **Tool Message Restoration**: Fixed conversation history restoration to properly handle tool calls and results
|
|
20
|
+
|
|
21
|
+
## [0.7.0] - 2025-10-16
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
- **Lifecycle Callback Hooks**: New callbacks for complete execution visibility and observability integration
|
|
25
|
+
- Added `on_run_start` callback triggered before agent execution begins with agent name, input, and run context
|
|
26
|
+
- Added `on_run_complete` callback triggered after execution ends (success or failure) with agent name, result, and run context
|
|
27
|
+
- Added `on_agent_complete` callback triggered after each agent turn with agent name, result, error (if any), and run context
|
|
28
|
+
- Run context parameter enables storing and retrieving custom data (e.g., span context, trace IDs) throughout execution
|
|
29
|
+
- Designed for integration with observability platforms (OpenTelemetry, Datadog, New Relic, etc.)
|
|
30
|
+
- All callbacks are thread-safe and non-blocking with proper error handling
|
|
31
|
+
- Updated callback documentation with integration patterns for UI feedback, logging, and metrics
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
- CallbackManager now supports 7 event types (previously 4)
|
|
35
|
+
- Enhanced callback system to provide complete lifecycle coverage for monitoring and tracing
|
|
36
|
+
|
|
8
37
|
## [0.6.0] - 2025-10-16
|
|
9
38
|
|
|
10
39
|
### 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,87 @@
|
|
|
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
|
+
content_present = message_content?(msg)
|
|
58
|
+
tool_calls_present = assistant_tool_calls?(msg)
|
|
59
|
+
return nil unless content_present || tool_calls_present
|
|
60
|
+
|
|
61
|
+
message = {
|
|
62
|
+
role: msg.role,
|
|
63
|
+
content: content_present ? msg.content : ""
|
|
64
|
+
}
|
|
56
65
|
|
|
57
|
-
|
|
58
|
-
role: msg.role,
|
|
59
|
-
content: msg.content
|
|
60
|
-
}
|
|
66
|
+
return message unless msg.role == :assistant
|
|
61
67
|
|
|
62
|
-
|
|
63
|
-
# Add agent attribution for conversation continuity
|
|
64
|
-
message[:agent_name] = current_agent.name if current_agent
|
|
68
|
+
message[:agent_name] = current_agent.name if current_agent
|
|
65
69
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
70
|
+
if tool_calls_present
|
|
71
|
+
# RubyLLM stores tool_calls as Hash with call_id => ToolCall object
|
|
72
|
+
# Reference: RubyLLM::StreamAccumulator#tool_calls_from_stream
|
|
73
|
+
message[:tool_calls] = msg.tool_calls.values.map(&:to_h)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
message
|
|
71
77
|
end
|
|
72
|
-
end
|
|
73
78
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
79
|
+
def message_content?(msg)
|
|
80
|
+
msg.content && !content_empty?(msg.content)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def assistant_tool_calls?(msg)
|
|
84
|
+
msg.role == :assistant && msg.tool_call? && msg.tool_calls && !msg.tool_calls.empty?
|
|
85
|
+
end
|
|
77
86
|
|
|
78
|
-
|
|
79
|
-
|
|
87
|
+
def extract_tool_message(msg)
|
|
88
|
+
return nil unless msg.tool_result?
|
|
80
89
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
90
|
+
{
|
|
91
|
+
role: msg.role,
|
|
92
|
+
content: msg.content,
|
|
93
|
+
tool_call_id: msg.tool_call_id
|
|
94
|
+
}
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
private_class_method :extract_user_or_assistant_message, :message_content?, :assistant_tool_calls?,
|
|
98
|
+
:extract_tool_message
|
|
99
|
+
end
|
|
86
100
|
end
|
|
87
|
-
private_class_method :extract_tool_message
|
|
88
101
|
end
|
data/lib/agents/run_context.rb
CHANGED
|
@@ -94,15 +94,21 @@ module Agents
|
|
|
94
94
|
end
|
|
95
95
|
|
|
96
96
|
# Add usage metrics from an LLM response to the running totals.
|
|
97
|
-
#
|
|
97
|
+
# Only tracks usage for responses that have token data (e.g., RubyLLM::Message).
|
|
98
|
+
# Safely skips responses without token methods (e.g., RubyLLM::Tool::Halt).
|
|
98
99
|
#
|
|
99
|
-
# @param
|
|
100
|
+
# @param response [RubyLLM::Message] A RubyLLM::Message object with token usage data
|
|
100
101
|
# @example Adding usage from an LLM response
|
|
101
|
-
# usage.add(llm_response
|
|
102
|
-
def add(
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
102
|
+
# usage.add(llm_response)
|
|
103
|
+
def add(response)
|
|
104
|
+
return unless response.respond_to?(:input_tokens)
|
|
105
|
+
|
|
106
|
+
input = response.input_tokens || 0
|
|
107
|
+
output = response.output_tokens || 0
|
|
108
|
+
|
|
109
|
+
@input_tokens += input
|
|
110
|
+
@output_tokens += output
|
|
111
|
+
@total_tokens += input + output
|
|
106
112
|
end
|
|
107
113
|
end
|
|
108
114
|
end
|
data/lib/agents/runner.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "set"
|
|
4
|
+
|
|
3
5
|
module Agents
|
|
4
6
|
# The execution engine that orchestrates conversations between users and agents.
|
|
5
7
|
# Runner manages the conversation flow, handles tool execution through RubyLLM,
|
|
@@ -90,6 +92,9 @@ module Agents
|
|
|
90
92
|
context_wrapper = RunContext.new(context_copy, callbacks: callbacks)
|
|
91
93
|
current_turn = 0
|
|
92
94
|
|
|
95
|
+
# Emit run start event
|
|
96
|
+
context_wrapper.callback_manager.emit_run_start(current_agent.name, input, context_wrapper)
|
|
97
|
+
|
|
93
98
|
runtime_headers = Helpers::Headers.normalize(headers)
|
|
94
99
|
agent_headers = Helpers::Headers.normalize(current_agent.headers)
|
|
95
100
|
|
|
@@ -100,7 +105,6 @@ module Agents
|
|
|
100
105
|
configure_chat_for_agent(chat, current_agent, context_wrapper, replace: false)
|
|
101
106
|
restore_conversation_history(chat, context_wrapper)
|
|
102
107
|
|
|
103
|
-
|
|
104
108
|
loop do
|
|
105
109
|
current_turn += 1
|
|
106
110
|
raise MaxTurnsExceeded, "Exceeded maximum turns: #{max_turns}" if current_turn > max_turns
|
|
@@ -116,6 +120,7 @@ module Agents
|
|
|
116
120
|
chat.complete
|
|
117
121
|
end
|
|
118
122
|
response = result
|
|
123
|
+
track_usage(response, context_wrapper)
|
|
119
124
|
|
|
120
125
|
# Check for handoff via RubyLLM's halt mechanism
|
|
121
126
|
if response.is_a?(RubyLLM::Tool::Halt) && context_wrapper.context[:pending_handoff]
|
|
@@ -127,18 +132,28 @@ module Agents
|
|
|
127
132
|
unless registry[next_agent.name]
|
|
128
133
|
save_conversation_state(chat, context_wrapper, current_agent)
|
|
129
134
|
error = AgentNotFoundError.new("Handoff failed: Agent '#{next_agent.name}' not found in registry")
|
|
130
|
-
|
|
135
|
+
|
|
136
|
+
result = RunResult.new(
|
|
131
137
|
output: nil,
|
|
132
138
|
messages: Helpers::MessageExtractor.extract_messages(chat, current_agent),
|
|
133
139
|
usage: context_wrapper.usage,
|
|
134
140
|
context: context_wrapper.context,
|
|
135
141
|
error: error
|
|
136
142
|
)
|
|
143
|
+
|
|
144
|
+
# Emit agent complete and run complete events with error
|
|
145
|
+
context_wrapper.callback_manager.emit_agent_complete(current_agent.name, result, error, context_wrapper)
|
|
146
|
+
context_wrapper.callback_manager.emit_run_complete(current_agent.name, result, context_wrapper)
|
|
147
|
+
|
|
148
|
+
return result
|
|
137
149
|
end
|
|
138
150
|
|
|
139
151
|
# Save current conversation state before switching
|
|
140
152
|
save_conversation_state(chat, context_wrapper, current_agent)
|
|
141
153
|
|
|
154
|
+
# Emit agent complete event before handoff
|
|
155
|
+
context_wrapper.callback_manager.emit_agent_complete(current_agent.name, nil, nil, context_wrapper)
|
|
156
|
+
|
|
142
157
|
# Emit agent handoff event
|
|
143
158
|
context_wrapper.callback_manager.emit_agent_handoff(current_agent.name, next_agent.name, "handoff")
|
|
144
159
|
|
|
@@ -161,12 +176,19 @@ module Agents
|
|
|
161
176
|
# Handle non-handoff halts - return the halt content as final response
|
|
162
177
|
if response.is_a?(RubyLLM::Tool::Halt)
|
|
163
178
|
save_conversation_state(chat, context_wrapper, current_agent)
|
|
164
|
-
|
|
179
|
+
|
|
180
|
+
result = RunResult.new(
|
|
165
181
|
output: response.content,
|
|
166
182
|
messages: Helpers::MessageExtractor.extract_messages(chat, current_agent),
|
|
167
183
|
usage: context_wrapper.usage,
|
|
168
184
|
context: context_wrapper.context
|
|
169
185
|
)
|
|
186
|
+
|
|
187
|
+
# Emit agent complete and run complete events
|
|
188
|
+
context_wrapper.callback_manager.emit_agent_complete(current_agent.name, result, nil, context_wrapper)
|
|
189
|
+
context_wrapper.callback_manager.emit_run_complete(current_agent.name, result, context_wrapper)
|
|
190
|
+
|
|
191
|
+
return result
|
|
170
192
|
end
|
|
171
193
|
|
|
172
194
|
# If tools were called, continue the loop to let them execute
|
|
@@ -177,35 +199,53 @@ module Agents
|
|
|
177
199
|
# Save final state before returning
|
|
178
200
|
save_conversation_state(chat, context_wrapper, current_agent)
|
|
179
201
|
|
|
180
|
-
|
|
202
|
+
result = RunResult.new(
|
|
181
203
|
output: response.content,
|
|
182
204
|
messages: Helpers::MessageExtractor.extract_messages(chat, current_agent),
|
|
183
205
|
usage: context_wrapper.usage,
|
|
184
206
|
context: context_wrapper.context
|
|
185
207
|
)
|
|
208
|
+
|
|
209
|
+
# Emit agent complete and run complete events
|
|
210
|
+
context_wrapper.callback_manager.emit_agent_complete(current_agent.name, result, nil, context_wrapper)
|
|
211
|
+
context_wrapper.callback_manager.emit_run_complete(current_agent.name, result, context_wrapper)
|
|
212
|
+
|
|
213
|
+
return result
|
|
186
214
|
end
|
|
187
215
|
rescue MaxTurnsExceeded => e
|
|
188
216
|
# Save state even on error
|
|
189
217
|
save_conversation_state(chat, context_wrapper, current_agent) if chat
|
|
190
218
|
|
|
191
|
-
RunResult.new(
|
|
219
|
+
result = RunResult.new(
|
|
192
220
|
output: "Conversation ended: #{e.message}",
|
|
193
221
|
messages: chat ? Helpers::MessageExtractor.extract_messages(chat, current_agent) : [],
|
|
194
222
|
usage: context_wrapper.usage,
|
|
195
223
|
error: e,
|
|
196
224
|
context: context_wrapper.context
|
|
197
225
|
)
|
|
226
|
+
|
|
227
|
+
# Emit agent complete and run complete events with error
|
|
228
|
+
context_wrapper.callback_manager.emit_agent_complete(current_agent.name, result, e, context_wrapper)
|
|
229
|
+
context_wrapper.callback_manager.emit_run_complete(current_agent.name, result, context_wrapper)
|
|
230
|
+
|
|
231
|
+
result
|
|
198
232
|
rescue StandardError => e
|
|
199
233
|
# Save state even on error
|
|
200
234
|
save_conversation_state(chat, context_wrapper, current_agent) if chat
|
|
201
235
|
|
|
202
|
-
RunResult.new(
|
|
236
|
+
result = RunResult.new(
|
|
203
237
|
output: nil,
|
|
204
238
|
messages: chat ? Helpers::MessageExtractor.extract_messages(chat, current_agent) : [],
|
|
205
239
|
usage: context_wrapper.usage,
|
|
206
240
|
error: e,
|
|
207
241
|
context: context_wrapper.context
|
|
208
242
|
)
|
|
243
|
+
|
|
244
|
+
# Emit agent complete and run complete events with error
|
|
245
|
+
context_wrapper.callback_manager.emit_agent_complete(current_agent.name, result, e, context_wrapper)
|
|
246
|
+
context_wrapper.callback_manager.emit_run_complete(current_agent.name, result, context_wrapper)
|
|
247
|
+
|
|
248
|
+
result
|
|
209
249
|
end
|
|
210
250
|
|
|
211
251
|
private
|
|
@@ -227,26 +267,98 @@ module Agents
|
|
|
227
267
|
|
|
228
268
|
# Restores conversation history from context into RubyLLM chat.
|
|
229
269
|
# Converts stored message hashes back into RubyLLM::Message objects with proper content handling.
|
|
270
|
+
# Supports user, assistant, and tool role messages for complete conversation continuity.
|
|
230
271
|
#
|
|
231
272
|
# @param chat [RubyLLM::Chat] The chat instance to restore history into
|
|
232
273
|
# @param context_wrapper [RunContext] Context containing conversation history
|
|
233
274
|
def restore_conversation_history(chat, context_wrapper)
|
|
234
275
|
history = context_wrapper.context[:conversation_history] || []
|
|
276
|
+
valid_tool_call_ids = Set.new
|
|
235
277
|
|
|
236
278
|
history.each do |msg|
|
|
237
|
-
|
|
238
|
-
next unless %i[user assistant].include?(msg[:role].to_sym)
|
|
239
|
-
next unless msg[:content] && !Helpers::MessageExtractor.content_empty?(msg[:content])
|
|
279
|
+
next unless restorable_message?(msg)
|
|
240
280
|
|
|
241
|
-
|
|
242
|
-
|
|
281
|
+
if msg[:role].to_sym == :tool &&
|
|
282
|
+
msg[:tool_call_id] &&
|
|
283
|
+
!valid_tool_call_ids.include?(msg[:tool_call_id])
|
|
284
|
+
Agents.logger&.warn("Skipping tool message without matching assistant tool_call_id #{msg[:tool_call_id]}")
|
|
285
|
+
next
|
|
286
|
+
end
|
|
243
287
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
)
|
|
288
|
+
message_params = build_message_params(msg)
|
|
289
|
+
next unless message_params # Skip invalid messages
|
|
290
|
+
|
|
291
|
+
message = RubyLLM::Message.new(**message_params)
|
|
249
292
|
chat.add_message(message)
|
|
293
|
+
|
|
294
|
+
if message.role == :assistant && message_params[:tool_calls]
|
|
295
|
+
valid_tool_call_ids.merge(message_params[:tool_calls].keys)
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
# Check if a message should be restored
|
|
301
|
+
def restorable_message?(msg)
|
|
302
|
+
role = msg[:role].to_sym
|
|
303
|
+
return false unless %i[user assistant tool].include?(role)
|
|
304
|
+
|
|
305
|
+
# Allow assistant messages that only contain tool calls (no text content)
|
|
306
|
+
tool_calls_present = role == :assistant && msg[:tool_calls] && !msg[:tool_calls].empty?
|
|
307
|
+
return false if role != :tool && !tool_calls_present &&
|
|
308
|
+
Helpers::MessageExtractor.content_empty?(msg[:content])
|
|
309
|
+
|
|
310
|
+
true
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
# Build message parameters for restoration
|
|
314
|
+
def build_message_params(msg)
|
|
315
|
+
role = msg[:role].to_sym
|
|
316
|
+
|
|
317
|
+
content_value = msg[:content]
|
|
318
|
+
# Assistant tool-call messages may have empty text, but still need placeholder content
|
|
319
|
+
content_value = "" if content_value.nil? && role == :assistant && msg[:tool_calls]&.any?
|
|
320
|
+
|
|
321
|
+
params = {
|
|
322
|
+
role: role,
|
|
323
|
+
content: RubyLLM::Content.new(content_value)
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
# Handle tool-specific parameters (Tool Results)
|
|
327
|
+
if role == :tool
|
|
328
|
+
return nil unless valid_tool_message?(msg)
|
|
329
|
+
|
|
330
|
+
params[:tool_call_id] = msg[:tool_call_id]
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
# FIX: Restore tool_calls on assistant messages
|
|
334
|
+
# This is required by OpenAI/Anthropic API contracts to link
|
|
335
|
+
# subsequent tool result messages back to this request.
|
|
336
|
+
if role == :assistant && msg[:tool_calls] && !msg[:tool_calls].empty?
|
|
337
|
+
# Convert stored array of hashes back into the Hash format RubyLLM expects
|
|
338
|
+
# RubyLLM stores tool_calls as: { call_id => ToolCall_object, ... }
|
|
339
|
+
# Reference: openai/tools.rb:35 uses hash iteration |_, tc|
|
|
340
|
+
params[:tool_calls] = msg[:tool_calls].each_with_object({}) do |tc, hash|
|
|
341
|
+
tool_call_id = tc[:id] || tc["id"]
|
|
342
|
+
next unless tool_call_id
|
|
343
|
+
|
|
344
|
+
hash[tool_call_id] = RubyLLM::ToolCall.new(
|
|
345
|
+
id: tool_call_id,
|
|
346
|
+
name: tc[:name] || tc["name"],
|
|
347
|
+
arguments: tc[:arguments] || tc["arguments"] || {}
|
|
348
|
+
)
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
params
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
# Validate tool message has required tool_call_id
|
|
356
|
+
def valid_tool_message?(msg)
|
|
357
|
+
if msg[:tool_call_id]
|
|
358
|
+
true
|
|
359
|
+
else
|
|
360
|
+
Agents.logger&.warn("Skipping tool message without tool_call_id in conversation history")
|
|
361
|
+
false
|
|
250
362
|
end
|
|
251
363
|
end
|
|
252
364
|
|
|
@@ -303,6 +415,12 @@ module Agents
|
|
|
303
415
|
chat.with_headers(**headers)
|
|
304
416
|
end
|
|
305
417
|
|
|
418
|
+
def track_usage(response, context_wrapper)
|
|
419
|
+
return unless context_wrapper&.usage
|
|
420
|
+
|
|
421
|
+
context_wrapper.usage.add(response)
|
|
422
|
+
end
|
|
423
|
+
|
|
306
424
|
# Builds thread-safe tool wrappers for an agent's tools and handoff tools.
|
|
307
425
|
#
|
|
308
426
|
# @param agent [Agents::Agent] The agent whose tools to wrap
|
data/lib/agents/tool_wrapper.rb
CHANGED
|
@@ -73,6 +73,15 @@ module Agents
|
|
|
73
73
|
@tool.parameters
|
|
74
74
|
end
|
|
75
75
|
|
|
76
|
+
# Expose params schema for RubyLLM providers that expect it
|
|
77
|
+
def params_schema
|
|
78
|
+
@tool.respond_to?(:params_schema) ? @tool.params_schema : nil
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def provider_params
|
|
82
|
+
@tool.respond_to?(:provider_params) ? @tool.provider_params : {}
|
|
83
|
+
end
|
|
84
|
+
|
|
76
85
|
# Make this work with RubyLLM's tool calling
|
|
77
86
|
def to_s
|
|
78
87
|
name
|
data/lib/agents/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ai-agents
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Shivam Mishra
|
|
@@ -15,14 +15,14 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - "~>"
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: 1.
|
|
18
|
+
version: 1.9.1
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - "~>"
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: 1.
|
|
25
|
+
version: 1.9.1
|
|
26
26
|
description: Ruby AI Agents SDK enables creating complex AI workflows with multi-agent
|
|
27
27
|
orchestration, tool execution, safety guardrails, and provider-agnostic LLM integration.
|
|
28
28
|
email:
|