ai-agents 0.1.3 → 0.2.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/.claude/commands/bump-version.md +44 -0
- data/CHANGELOG.md +35 -0
- data/CLAUDE.md +59 -15
- data/docs/concepts/callbacks.md +42 -0
- data/docs/concepts.md +1 -0
- data/docs/index.md +2 -0
- data/examples/isp-support/interactive.rb +43 -4
- data/lib/agents/agent_runner.rb +66 -1
- data/lib/agents/callback_manager.rb +54 -0
- data/lib/agents/message_extractor.rb +82 -0
- data/lib/agents/run_context.rb +5 -2
- data/lib/agents/runner.rb +16 -27
- data/lib/agents/tool_wrapper.rb +11 -1
- data/lib/agents/version.rb +1 -1
- data/lib/agents.rb +2 -0
- metadata +6 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b2d0dbff300d9f3bd08da74d39e1fbf2eecbfa0b293558af01b5ecfcc765d58c
|
4
|
+
data.tar.gz: 84a94feb4a5faea46ec658159f1266d315ba8175ad8b4c62915745c6e430c741
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7bb4ab502f7c3362a3bc6f3549c30166f5e8e19d5f9aee7c3bf1fe17af334ae765122ecbff8c6b045aa28d5fac6e639dd5f56002ab7b24626ed0b2aae36eba48
|
7
|
+
data.tar.gz: 185d68208135a2f24b5ad8f5bbc075cdf06bcd54bad1b99f7910050224439bb8aacdec7a5c64f9768f7ec9cebdea9efa9e33734db4a650dd97bb16de901a00b5
|
@@ -0,0 +1,44 @@
|
|
1
|
+
---
|
2
|
+
allowed-tools: Bash(git add:*), Bash(git status:*), Bash(git commit:*)
|
3
|
+
description: Bump version, update changelog and commit changes
|
4
|
+
---
|
5
|
+
|
6
|
+
You need to bump the version of the ruby gem and update the CHANGELOG. We need a #$ARGUMENTS version bump.
|
7
|
+
|
8
|
+
The types of version jumps are
|
9
|
+
|
10
|
+
- patch: Increment the third number in the version number.
|
11
|
+
- minor: Increment the second number in the version number.
|
12
|
+
- major: Increment the first number in the version number.
|
13
|
+
|
14
|
+
To bump up the version, follow these steps:
|
15
|
+
|
16
|
+
1. Update the version number in the `lib/agents/version.rb` file.
|
17
|
+
2. Run `bundle install` to ensure the lock file picks up the new version.
|
18
|
+
|
19
|
+
To update the changelog.
|
20
|
+
|
21
|
+
1. Find the changelog file in `CHANGELOG.md`.
|
22
|
+
2. Add a new section for the new version number.
|
23
|
+
3. List the changes made in the new version.
|
24
|
+
|
25
|
+
We follow the `keepachangelog.com` guide for writing these changelogs and follow semantic versioning.
|
26
|
+
|
27
|
+
Here's what makes a good changelog.
|
28
|
+
|
29
|
+
**Guiding Principles**
|
30
|
+
- Changelogs are for humans, not machines.
|
31
|
+
- There should be an entry for every single version.
|
32
|
+
- The same types of changes should be grouped.
|
33
|
+
- Versions and sections should be linkable.
|
34
|
+
- The latest version comes first.
|
35
|
+
|
36
|
+
**Types of changes**
|
37
|
+
`Added` for new features.
|
38
|
+
`Changed` for changes in existing functionality.
|
39
|
+
`Deprecated` for soon-to-be removed features.
|
40
|
+
`Removed` for now removed features.
|
41
|
+
`Fixed` for any bug fixes.
|
42
|
+
`Security` in case of vulnerabilities
|
43
|
+
|
44
|
+
Once done, commit the changes with a message like "chore: bump version to <new-version>".
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
# Changelog
|
2
|
+
|
3
|
+
All notable changes to this project will be documented in this file.
|
4
|
+
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
7
|
+
|
8
|
+
## [0.2.0] - 2025-07-08
|
9
|
+
|
10
|
+
### Added
|
11
|
+
- Real-time callback system for monitoring agent execution
|
12
|
+
- `on_agent_thinking` callback for when agents are processing
|
13
|
+
- `on_tool_start` callback for when tools begin execution
|
14
|
+
- `on_tool_complete` callback for when tools finish execution
|
15
|
+
- `on_agent_handoff` callback for when control transfers between agents
|
16
|
+
- Enhanced conversation history with complete tool call audit trail
|
17
|
+
- Tool calls now captured in assistant messages with arguments
|
18
|
+
- Tool result messages linked to original calls via `tool_call_id`
|
19
|
+
- Full conversation replay capability for debugging
|
20
|
+
- CallbackManager for centralized event handling
|
21
|
+
- MessageExtractor service for clean conversation history processing
|
22
|
+
|
23
|
+
### Changed
|
24
|
+
- RunContext now includes callback management capabilities
|
25
|
+
- Improved thread safety for callback execution
|
26
|
+
- Enhanced error handling for callback failures (non-blocking)
|
27
|
+
|
28
|
+
## [0.1.3] - Previous Release
|
29
|
+
|
30
|
+
### Added
|
31
|
+
- Multi-agent orchestration with seamless handoffs
|
32
|
+
- Thread-safe agent execution architecture
|
33
|
+
- Tool integration system
|
34
|
+
- Shared context management
|
35
|
+
- Provider support for OpenAI, Anthropic, and Gemini
|
data/CLAUDE.md
CHANGED
@@ -9,6 +9,7 @@ This project is a Ruby SDK for building multi-agent AI workflows. It allows deve
|
|
9
9
|
- **Multi-Agent Orchestration**: Defining and managing multiple AI agents with distinct roles.
|
10
10
|
- **Seamless Handoffs**: Transferring conversations between agents without the user's knowledge.
|
11
11
|
- **Tool Integration**: Allowing agents to use custom tools to interact with external systems.
|
12
|
+
- **Real-time Callbacks**: Event-driven system for monitoring agent execution, tool usage, and handoffs.
|
12
13
|
- **Shared Context**: Maintaining state and conversation history across agent interactions with full persistence support.
|
13
14
|
- **Thread-Safe Architecture**: Reusable agent runners that work safely across multiple threads.
|
14
15
|
- **Provider Agnostic**: Supporting various LLM providers like OpenAI, Anthropic, and Gemini.
|
@@ -52,6 +53,7 @@ ruby examples/isp-support/interactive.rb
|
|
52
53
|
|
53
54
|
This will start a command-line interface where you can interact with the multi-agent system. The example demonstrates:
|
54
55
|
- Thread-safe agent runner creation
|
56
|
+
- Real-time callback system with UI feedback
|
55
57
|
- Automatic agent selection based on conversation history
|
56
58
|
- Context persistence that works across process boundaries
|
57
59
|
- Seamless handoffs between triage, sales, and support agents
|
@@ -64,6 +66,7 @@ This will start a command-line interface where you can interact with the multi-a
|
|
64
66
|
- **AgentRunner**: The thread-safe execution manager that coordinates multi-agent conversations and provides the main API.
|
65
67
|
- **Runner**: Internal component that manages individual conversation turns (used by AgentRunner).
|
66
68
|
- **Context**: A shared state object that stores conversation history and agent information, fully serializable for persistence.
|
69
|
+
- **Callbacks**: Event hooks for monitoring agent execution, including agent thinking, tool start/complete, and handoffs.
|
67
70
|
|
68
71
|
## Development Commands
|
69
72
|
|
@@ -111,10 +114,12 @@ ruby examples/isp-support/interactive.rb
|
|
111
114
|
### Core Components
|
112
115
|
|
113
116
|
- **Agents::Agent**: Individual AI agents with specific roles, instructions, and tools
|
117
|
+
- **Agents::AgentRunner**: Thread-safe execution manager with callback support
|
114
118
|
- **Agents::Runner**: Orchestrates multi-agent conversations with automatic handoffs
|
115
119
|
- **Agents::Tool**: Base class for custom tools that agents can execute
|
116
120
|
- **Agents::Context**: Shared state management across agent interactions
|
117
121
|
- **Agents::Handoff**: Manages seamless transfers between agents
|
122
|
+
- **Agents::CallbackManager**: Centralized event handling for real-time monitoring
|
118
123
|
|
119
124
|
### Key Design Principles
|
120
125
|
|
@@ -129,17 +134,19 @@ ruby examples/isp-support/interactive.rb
|
|
129
134
|
|
130
135
|
```
|
131
136
|
lib/agents/
|
132
|
-
├── agent.rb
|
133
|
-
├── agent_runner.rb
|
134
|
-
├── runner.rb
|
135
|
-
├── tool.rb
|
136
|
-
├── handoff.rb
|
137
|
-
├── chat.rb
|
138
|
-
├── result.rb
|
139
|
-
├── run_context.rb
|
140
|
-
├── tool_context.rb
|
141
|
-
├── tool_wrapper.rb
|
142
|
-
|
137
|
+
├── agent.rb # Core agent definition and configuration
|
138
|
+
├── agent_runner.rb # Thread-safe execution manager (main API)
|
139
|
+
├── runner.rb # Internal execution engine for conversation turns
|
140
|
+
├── tool.rb # Base class for custom tools
|
141
|
+
├── handoff.rb # Agent handoff management
|
142
|
+
├── chat.rb # Chat message handling
|
143
|
+
├── result.rb # Result object for agent responses
|
144
|
+
├── run_context.rb # Execution context management
|
145
|
+
├── tool_context.rb # Tool execution context
|
146
|
+
├── tool_wrapper.rb # Thread-safe tool wrapping
|
147
|
+
├── callback_manager.rb # Centralized callback event handling
|
148
|
+
├── message_extractor.rb # Conversation history processing
|
149
|
+
└── version.rb # Gem version
|
143
150
|
```
|
144
151
|
|
145
152
|
### Configuration
|
@@ -167,7 +174,13 @@ support = Agent.new(name: "Support", instructions: "Technical support...")
|
|
167
174
|
triage.register_handoffs(billing, support)
|
168
175
|
|
169
176
|
# Create thread-safe runner (first agent is default entry point)
|
170
|
-
runner = Agents::
|
177
|
+
runner = Agents::AgentRunner.with_agents(triage, billing, support)
|
178
|
+
|
179
|
+
# Add real-time callbacks for monitoring
|
180
|
+
runner.on_agent_thinking { |agent_name, input| puts "🧠 #{agent_name} is thinking..." }
|
181
|
+
runner.on_tool_start { |tool_name, args| puts "🔧 Using #{tool_name}..." }
|
182
|
+
runner.on_tool_complete { |tool_name, result| puts "✅ #{tool_name} completed" }
|
183
|
+
runner.on_agent_handoff { |from, to, reason| puts "🔄 Handoff: #{from} → #{to}" }
|
171
184
|
|
172
185
|
# Use for conversations - automatically handles agent selection and persistence
|
173
186
|
result = runner.run("I have a billing question")
|
@@ -184,10 +197,18 @@ When creating custom tools:
|
|
184
197
|
|
185
198
|
### Testing Strategy
|
186
199
|
|
200
|
+
The codebase follows comprehensive testing patterns with strong emphasis on thread safety and isolation, here's some points to note
|
201
|
+
|
187
202
|
- SimpleCov tracks coverage with 50% minimum overall, 40% per file
|
188
|
-
-
|
189
|
-
-
|
190
|
-
-
|
203
|
+
- RSpec testing framework with descriptive context blocks
|
204
|
+
- Tests organized by component in `spec/agents/` mirroring lib structure
|
205
|
+
- **Instance Doubles**: Extensive use of `instance_double` for clean dependency mocking, never use generic `double` or `stub`
|
206
|
+
- **WebMock**: HTTP call stubbing with network isolation (`WebMock.disable_net_connect!`)
|
207
|
+
- Unit tests for individual components
|
208
|
+
- Integration tests for complex workflows
|
209
|
+
- Thread-safety tests for concurrent scenarios
|
210
|
+
- Clear separation of setup, execution, and assertion phases
|
211
|
+
- Context description should always match /^when\b/, /^with\b/, or /^without\b/.
|
191
212
|
|
192
213
|
### Examples
|
193
214
|
|
@@ -195,3 +216,26 @@ The `examples/` directory contains complete working examples:
|
|
195
216
|
- `isp-support/`: Multi-agent ISP customer support system
|
196
217
|
- Shows hub-and-spoke architecture patterns
|
197
218
|
- Demonstrates tool integration and handoff workflows
|
219
|
+
- Includes real-time callback implementation for UI feedback
|
220
|
+
|
221
|
+
## Callback System
|
222
|
+
|
223
|
+
The SDK includes a comprehensive callback system for monitoring agent execution in real-time. This is particularly useful for:
|
224
|
+
|
225
|
+
- **UI Feedback**: Show users what's happening during agent execution
|
226
|
+
- **Debugging**: Track tool usage and agent handoffs
|
227
|
+
- **Analytics**: Monitor system performance and usage patterns
|
228
|
+
- **Rails Integration**: Stream updates via ActionCable
|
229
|
+
|
230
|
+
### Available Callbacks
|
231
|
+
|
232
|
+
- `on_agent_thinking`: Triggered when an agent starts processing
|
233
|
+
- `on_tool_start`: Triggered when a tool begins execution
|
234
|
+
- `on_tool_complete`: Triggered when a tool finishes execution
|
235
|
+
- `on_agent_handoff`: Triggered when control transfers between agents
|
236
|
+
|
237
|
+
### Callback Integration
|
238
|
+
|
239
|
+
Callbacks are thread-safe and non-blocking. If a callback raises an exception, it won't interrupt agent execution. The system uses a centralized CallbackManager for efficient event handling.
|
240
|
+
|
241
|
+
For detailed callback documentation, see `docs/concepts/callbacks.md`.
|
@@ -0,0 +1,42 @@
|
|
1
|
+
---
|
2
|
+
layout: default
|
3
|
+
title: Callbacks
|
4
|
+
parent: Concepts
|
5
|
+
nav_order: 6
|
6
|
+
---
|
7
|
+
|
8
|
+
# Real-time Callbacks
|
9
|
+
|
10
|
+
The AI Agents SDK provides real-time callbacks that allow you to monitor agent execution as it happens. This is particularly useful for building user interfaces that show live feedback about what agents are doing.
|
11
|
+
|
12
|
+
## Available Callbacks
|
13
|
+
|
14
|
+
The SDK provides four types of callbacks that give you visibility into different stages of agent execution:
|
15
|
+
|
16
|
+
**Agent Thinking** - Triggered when an agent is about to make an LLM call. Useful for showing "thinking" indicators in UIs.
|
17
|
+
|
18
|
+
**Tool Start** - Called when an agent begins executing a tool. Shows which tool is being used and with what arguments.
|
19
|
+
|
20
|
+
**Tool Complete** - Triggered when a tool finishes execution. Provides the tool name and result for status updates.
|
21
|
+
|
22
|
+
**Agent Handoff** - Called when control transfers between agents. Shows the source agent, target agent, and handoff reason.
|
23
|
+
|
24
|
+
## Basic Usage
|
25
|
+
|
26
|
+
Callbacks are registered on the AgentRunner using chainable methods:
|
27
|
+
|
28
|
+
```ruby
|
29
|
+
runner = Agents::AgentRunner.with_agents(triage, support)
|
30
|
+
.on_agent_thinking { |agent, input| puts "#{agent} thinking..." }
|
31
|
+
.on_tool_start { |tool, args| puts "Using #{tool}" }
|
32
|
+
.on_tool_complete { |tool, result| puts "#{tool} completed" }
|
33
|
+
.on_agent_handoff { |from, to, reason| puts "#{from} → #{to}" }
|
34
|
+
```
|
35
|
+
|
36
|
+
## Integration Patterns
|
37
|
+
|
38
|
+
Callbacks work well with real-time web frameworks like Rails ActionCable, allowing you to stream agent status updates directly to browser clients. They're also useful for logging, metrics collection, and building debug interfaces.
|
39
|
+
|
40
|
+
## Thread Safety
|
41
|
+
|
42
|
+
Callbacks execute synchronously in the same thread as agent execution. Exceptions in callbacks are caught and logged as warnings without interrupting agent operation. For heavy operations or external API calls, consider using background jobs triggered by the callbacks.
|
data/docs/concepts.md
CHANGED
@@ -18,4 +18,5 @@ The AI Agents library is built around several key concepts that work together to
|
|
18
18
|
- **[Context](concepts/context.html)** - Serializable state management that persists across agent interactions
|
19
19
|
- **[Handoffs](concepts/handoffs.html)** - Tool-based mechanism for seamless agent transitions
|
20
20
|
- **[Tools](concepts/tools.html)** - Stateless extensions for external system integration
|
21
|
+
- **[Callbacks](concepts/callbacks.html)** - Real-time notifications for agent thinking, tool execution, and handoffs
|
21
22
|
- **[AgentTool](concepts/agent-tool.html)** - Agent-to-agent collaboration without conversation handoffs
|
data/docs/index.md
CHANGED
@@ -26,6 +26,7 @@ AI Agents is a Ruby SDK that enables developers to create sophisticated multi-ag
|
|
26
26
|
- **Multi-Agent Orchestration**: Define and manage multiple AI agents with distinct roles
|
27
27
|
- **Seamless Handoffs**: Transfer conversations between agents without user knowledge
|
28
28
|
- **Tool Integration**: Allow agents to use custom tools to interact with external systems
|
29
|
+
- **Callbacks**: Real-time notifications for agent thinking, tool execution, and handoffs
|
29
30
|
- **Shared Context**: Maintain state and conversation history across agent interactions
|
30
31
|
- **Thread-Safe Architecture**: Reusable agent runners that work safely across multiple threads
|
31
32
|
- **Provider Agnostic**: Support for OpenAI, Anthropic, and Gemini
|
@@ -93,3 +94,4 @@ puts result.output
|
|
93
94
|
- [Working with Tools](concepts/tools.html)
|
94
95
|
- [Agent Handoffs](concepts/handoffs.html)
|
95
96
|
- [Using the Runner](concepts/runner.html)
|
97
|
+
- [Callbacks](concepts/callbacks.html)
|
@@ -14,16 +14,20 @@ class ISPSupportDemo
|
|
14
14
|
end
|
15
15
|
|
16
16
|
# Create agents
|
17
|
-
agents = ISPSupport::AgentsFactory.create_agents
|
17
|
+
@agents = ISPSupport::AgentsFactory.create_agents
|
18
18
|
|
19
19
|
# Create thread-safe runner with all agents (triage first = default entry point)
|
20
20
|
@runner = Agents::Runner.with_agents(
|
21
|
-
agents[:triage],
|
22
|
-
agents[:sales],
|
23
|
-
agents[:support]
|
21
|
+
@agents[:triage],
|
22
|
+
@agents[:sales],
|
23
|
+
@agents[:support]
|
24
24
|
)
|
25
25
|
|
26
|
+
# Setup real-time callbacks for UI feedback
|
27
|
+
setup_callbacks
|
28
|
+
|
26
29
|
@context = {}
|
30
|
+
@current_status = ""
|
27
31
|
|
28
32
|
puts "🏢 Welcome to ISP Customer Support!"
|
29
33
|
puts "Type '/help' for commands or 'exit' to quit."
|
@@ -39,12 +43,18 @@ class ISPSupportDemo
|
|
39
43
|
break if command_result == :exit
|
40
44
|
next if command_result == :handled || user_input.empty?
|
41
45
|
|
46
|
+
# Clear any previous status and show agent is working
|
47
|
+
clear_status_line
|
48
|
+
print "🤖 Processing..."
|
49
|
+
|
42
50
|
# Use the runner - it automatically determines the right agent from context
|
43
51
|
result = @runner.run(user_input, context: @context)
|
44
52
|
|
45
53
|
# Update our context with the returned context from Runner
|
46
54
|
@context = result.context if result.respond_to?(:context) && result.context
|
47
55
|
|
56
|
+
# Clear status and show response
|
57
|
+
clear_status_line
|
48
58
|
puts "🤖 #{result.output || "[No output]"}"
|
49
59
|
|
50
60
|
puts
|
@@ -53,6 +63,35 @@ class ISPSupportDemo
|
|
53
63
|
|
54
64
|
private
|
55
65
|
|
66
|
+
def setup_callbacks
|
67
|
+
@runner.on_agent_thinking do |agent_name, _input|
|
68
|
+
update_status("🧠 #{agent_name} is thinking...")
|
69
|
+
end
|
70
|
+
|
71
|
+
@runner.on_tool_start do |tool_name, _args|
|
72
|
+
update_status("🔧 Using #{tool_name}...")
|
73
|
+
end
|
74
|
+
|
75
|
+
@runner.on_tool_complete do |tool_name, _result|
|
76
|
+
update_status("✅ #{tool_name} completed")
|
77
|
+
end
|
78
|
+
|
79
|
+
@runner.on_agent_handoff do |from_agent, to_agent, _reason|
|
80
|
+
update_status("🔄 Handoff: #{from_agent} → #{to_agent}")
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def update_status(message)
|
85
|
+
clear_status_line
|
86
|
+
print message
|
87
|
+
$stdout.flush
|
88
|
+
end
|
89
|
+
|
90
|
+
def clear_status_line
|
91
|
+
print "\r#{" " * 80}\r" # Clear the current line
|
92
|
+
$stdout.flush
|
93
|
+
end
|
94
|
+
|
56
95
|
def handle_command(input)
|
57
96
|
case input.downcase
|
58
97
|
when "exit", "quit"
|
data/lib/agents/agent_runner.rb
CHANGED
@@ -11,6 +11,8 @@ module Agents
|
|
11
11
|
# ## Usage Pattern
|
12
12
|
# # Create once (typically at application startup)
|
13
13
|
# runner = Agents::Runner.with_agents(triage_agent, billing_agent, support_agent)
|
14
|
+
# .on_tool_start { |tool_name, args| broadcast_event('tool_start', tool_name, args) }
|
15
|
+
# .on_tool_complete { |tool_name, result| broadcast_event('tool_complete', tool_name, result) }
|
14
16
|
#
|
15
17
|
# # Use safely from multiple threads
|
16
18
|
# result = runner.run("I need billing help") # New conversation
|
@@ -22,6 +24,10 @@ module Agents
|
|
22
24
|
# - Each run() call creates independent execution context
|
23
25
|
# - No shared mutable state between concurrent executions
|
24
26
|
#
|
27
|
+
# ## Callback Thread Safety
|
28
|
+
# Callback registration is thread-safe using internal synchronization. Multiple threads
|
29
|
+
# can safely register callbacks concurrently without data races.
|
30
|
+
#
|
25
31
|
class AgentRunner
|
26
32
|
# Initialize with a list of agents. The first agent becomes the default entry point.
|
27
33
|
#
|
@@ -30,10 +36,19 @@ module Agents
|
|
30
36
|
raise ArgumentError, "At least one agent must be provided" if agents.empty?
|
31
37
|
|
32
38
|
@agents = agents.dup.freeze
|
39
|
+
@callbacks_mutex = Mutex.new
|
33
40
|
@default_agent = agents.first
|
34
41
|
|
35
42
|
# Build simple registry from provided agents - developer controls what's available
|
36
43
|
@registry = build_registry(agents).freeze
|
44
|
+
|
45
|
+
# Initialize callback storage - use thread-safe arrays
|
46
|
+
@callbacks = {
|
47
|
+
tool_start: [],
|
48
|
+
tool_complete: [],
|
49
|
+
agent_thinking: [],
|
50
|
+
agent_handoff: []
|
51
|
+
}
|
37
52
|
end
|
38
53
|
|
39
54
|
# Execute a conversation turn with automatic agent selection.
|
@@ -50,15 +65,65 @@ module Agents
|
|
50
65
|
current_agent = determine_conversation_agent(context)
|
51
66
|
|
52
67
|
# Execute using stateless Runner - each execution is independent and thread-safe
|
68
|
+
# Pass callbacks to enable real-time event notifications
|
53
69
|
Runner.new.run(
|
54
70
|
current_agent,
|
55
71
|
input,
|
56
72
|
context: context,
|
57
73
|
registry: @registry,
|
58
|
-
max_turns: max_turns
|
74
|
+
max_turns: max_turns,
|
75
|
+
callbacks: @callbacks
|
59
76
|
)
|
60
77
|
end
|
61
78
|
|
79
|
+
# Register a callback for tool start events.
|
80
|
+
# Called when an agent is about to execute a tool.
|
81
|
+
#
|
82
|
+
# @param block [Proc] Callback block that receives (tool_name, args)
|
83
|
+
# @return [self] For method chaining
|
84
|
+
def on_tool_start(&block)
|
85
|
+
return self unless block
|
86
|
+
|
87
|
+
@callbacks_mutex.synchronize { @callbacks[:tool_start] << block }
|
88
|
+
self
|
89
|
+
end
|
90
|
+
|
91
|
+
# Register a callback for tool completion events.
|
92
|
+
# Called when an agent has finished executing a tool.
|
93
|
+
#
|
94
|
+
# @param block [Proc] Callback block that receives (tool_name, result)
|
95
|
+
# @return [self] For method chaining
|
96
|
+
def on_tool_complete(&block)
|
97
|
+
return self unless block
|
98
|
+
|
99
|
+
@callbacks_mutex.synchronize { @callbacks[:tool_complete] << block }
|
100
|
+
self
|
101
|
+
end
|
102
|
+
|
103
|
+
# Register a callback for agent thinking events.
|
104
|
+
# Called when an agent is about to make an LLM call.
|
105
|
+
#
|
106
|
+
# @param block [Proc] Callback block that receives (agent_name, input)
|
107
|
+
# @return [self] For method chaining
|
108
|
+
def on_agent_thinking(&block)
|
109
|
+
return self unless block
|
110
|
+
|
111
|
+
@callbacks_mutex.synchronize { @callbacks[:agent_thinking] << block }
|
112
|
+
self
|
113
|
+
end
|
114
|
+
|
115
|
+
# Register a callback for agent handoff events.
|
116
|
+
# Called when control is transferred from one agent to another.
|
117
|
+
#
|
118
|
+
# @param block [Proc] Callback block that receives (from_agent, to_agent, reason)
|
119
|
+
# @return [self] For method chaining
|
120
|
+
def on_agent_handoff(&block)
|
121
|
+
return self unless block
|
122
|
+
|
123
|
+
@callbacks_mutex.synchronize { @callbacks[:agent_handoff] << block }
|
124
|
+
self
|
125
|
+
end
|
126
|
+
|
62
127
|
private
|
63
128
|
|
64
129
|
# Build agent registry from provided agents only.
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Agents
|
4
|
+
# Manager for handling and emitting callback events in a thread-safe manner.
|
5
|
+
# Provides both generic emit() method and typed convenience methods.
|
6
|
+
#
|
7
|
+
# @example Using generic emit
|
8
|
+
# manager.emit(:tool_start, tool_name, args)
|
9
|
+
#
|
10
|
+
# @example Using typed methods
|
11
|
+
# manager.emit_tool_start(tool_name, args)
|
12
|
+
# manager.emit_agent_thinking(agent_name, input)
|
13
|
+
class CallbackManager
|
14
|
+
# Supported callback event types
|
15
|
+
EVENT_TYPES = %i[
|
16
|
+
tool_start
|
17
|
+
tool_complete
|
18
|
+
agent_thinking
|
19
|
+
agent_handoff
|
20
|
+
].freeze
|
21
|
+
|
22
|
+
def initialize(callbacks = {})
|
23
|
+
@callbacks = callbacks.dup.freeze
|
24
|
+
end
|
25
|
+
|
26
|
+
# Generic method to emit any callback event type
|
27
|
+
#
|
28
|
+
# @param event_type [Symbol] The type of event to emit
|
29
|
+
# @param args [Array] Arguments to pass to callbacks
|
30
|
+
def emit(event_type, *args)
|
31
|
+
callback_list = @callbacks[event_type] || []
|
32
|
+
|
33
|
+
callback_list.each do |callback|
|
34
|
+
callback.call(*args)
|
35
|
+
rescue StandardError => e
|
36
|
+
# Log callback errors but don't let them crash execution
|
37
|
+
warn "Callback error for #{event_type}: #{e.message}"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Metaprogramming: Create typed emit methods for each event type
|
42
|
+
#
|
43
|
+
# This generates methods like:
|
44
|
+
# emit_tool_start(tool_name, args)
|
45
|
+
# emit_tool_complete(tool_name, result)
|
46
|
+
# emit_agent_thinking(agent_name, input)
|
47
|
+
# emit_agent_handoff(from_agent, to_agent, reason)
|
48
|
+
EVENT_TYPES.each do |event_type|
|
49
|
+
define_method("emit_#{event_type}") do |*args|
|
50
|
+
emit(event_type, *args)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Agents
|
4
|
+
# Service object responsible for extracting and formatting conversation messages
|
5
|
+
# from RubyLLM chat objects into a format suitable for persistence and context restoration.
|
6
|
+
#
|
7
|
+
# Handles different message types:
|
8
|
+
# - User messages: Basic content preservation
|
9
|
+
# - Assistant messages: Includes agent attribution and tool calls
|
10
|
+
# - Tool result messages: Links back to original tool calls
|
11
|
+
#
|
12
|
+
# @example Extract messages from a chat
|
13
|
+
# messages = MessageExtractor.extract_messages(chat, current_agent)
|
14
|
+
# #=> [
|
15
|
+
# { role: :user, content: "Hello" },
|
16
|
+
# { role: :assistant, content: "Hi!", agent_name: "Support", tool_calls: [...] },
|
17
|
+
# { role: :tool, content: "Result", tool_call_id: "call_123" }
|
18
|
+
# ]
|
19
|
+
class MessageExtractor
|
20
|
+
# Extract messages from a chat object for conversation history persistence
|
21
|
+
#
|
22
|
+
# @param chat [Object] Chat object that responds to :messages
|
23
|
+
# @param current_agent [Agent] The agent currently handling the conversation
|
24
|
+
# @return [Array<Hash>] Array of message hashes suitable for persistence
|
25
|
+
def self.extract_messages(chat, current_agent)
|
26
|
+
new(chat, current_agent).extract
|
27
|
+
end
|
28
|
+
|
29
|
+
def initialize(chat, current_agent)
|
30
|
+
@chat = chat
|
31
|
+
@current_agent = current_agent
|
32
|
+
end
|
33
|
+
|
34
|
+
def extract
|
35
|
+
return [] unless @chat.respond_to?(:messages)
|
36
|
+
|
37
|
+
@chat.messages.filter_map do |msg|
|
38
|
+
case msg.role
|
39
|
+
when :user, :assistant
|
40
|
+
extract_user_or_assistant_message(msg)
|
41
|
+
when :tool
|
42
|
+
extract_tool_message(msg)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def extract_user_or_assistant_message(msg)
|
50
|
+
return nil unless msg.content && !msg.content.strip.empty?
|
51
|
+
|
52
|
+
message = {
|
53
|
+
role: msg.role,
|
54
|
+
content: msg.content
|
55
|
+
}
|
56
|
+
|
57
|
+
if msg.role == :assistant
|
58
|
+
# Add agent attribution for conversation continuity
|
59
|
+
message[:agent_name] = @current_agent.name if @current_agent
|
60
|
+
|
61
|
+
# Add tool calls if present
|
62
|
+
if msg.tool_call? && msg.tool_calls
|
63
|
+
# RubyLLM stores tool_calls as Hash with call_id => ToolCall object
|
64
|
+
# Reference: RubyLLM::StreamAccumulator#tool_calls_from_stream
|
65
|
+
message[:tool_calls] = msg.tool_calls.values.map(&:to_h)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
message
|
70
|
+
end
|
71
|
+
|
72
|
+
def extract_tool_message(msg)
|
73
|
+
return nil unless msg.tool_result?
|
74
|
+
|
75
|
+
{
|
76
|
+
role: msg.role,
|
77
|
+
content: msg.content,
|
78
|
+
tool_call_id: msg.tool_call_id
|
79
|
+
}
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
data/lib/agents/run_context.rb
CHANGED
@@ -56,14 +56,17 @@
|
|
56
56
|
# # - No race conditions or data leakage between runs
|
57
57
|
module Agents
|
58
58
|
class RunContext
|
59
|
-
attr_reader :context, :usage
|
59
|
+
attr_reader :context, :usage, :callbacks, :callback_manager
|
60
60
|
|
61
61
|
# Initialize a new RunContext with execution context and usage tracking
|
62
62
|
#
|
63
63
|
# @param context [Hash] The execution context data (will be duplicated for isolation)
|
64
|
-
|
64
|
+
# @param callbacks [Hash] Optional callbacks for real-time event notifications
|
65
|
+
def initialize(context, callbacks: {})
|
65
66
|
@context = context
|
66
67
|
@usage = Usage.new
|
68
|
+
@callbacks = callbacks || {}
|
69
|
+
@callback_manager = CallbackManager.new(@callbacks)
|
67
70
|
end
|
68
71
|
|
69
72
|
# Usage tracks token consumption across all LLM calls within a single run.
|
data/lib/agents/runner.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative "message_extractor"
|
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,
|
@@ -77,14 +79,15 @@ module Agents
|
|
77
79
|
# @param context [Hash] Shared context data accessible to all tools
|
78
80
|
# @param registry [Hash] Registry of agents for handoff resolution
|
79
81
|
# @param max_turns [Integer] Maximum conversation turns before stopping
|
82
|
+
# @param callbacks [Hash] Optional callbacks for real-time event notifications
|
80
83
|
# @return [RunResult] The result containing output, messages, and usage
|
81
|
-
def run(starting_agent, input, context: {}, registry: {}, max_turns: DEFAULT_MAX_TURNS)
|
84
|
+
def run(starting_agent, input, context: {}, registry: {}, max_turns: DEFAULT_MAX_TURNS, callbacks: {})
|
82
85
|
# The starting_agent is already determined by AgentRunner based on conversation history
|
83
86
|
current_agent = starting_agent
|
84
87
|
|
85
88
|
# Create context wrapper with deep copy for thread safety
|
86
89
|
context_copy = deep_copy_context(context)
|
87
|
-
context_wrapper = RunContext.new(context_copy)
|
90
|
+
context_wrapper = RunContext.new(context_copy, callbacks: callbacks)
|
88
91
|
current_turn = 0
|
89
92
|
|
90
93
|
# Create chat and restore conversation history
|
@@ -97,8 +100,12 @@ module Agents
|
|
97
100
|
|
98
101
|
# Get response from LLM (Extended Chat handles tool execution with handoff detection)
|
99
102
|
result = if current_turn == 1
|
103
|
+
# Emit agent thinking event for initial message
|
104
|
+
context_wrapper.callback_manager.emit_agent_thinking(current_agent.name, input)
|
100
105
|
chat.ask(input)
|
101
106
|
else
|
107
|
+
# Emit agent thinking event for continuation
|
108
|
+
context_wrapper.callback_manager.emit_agent_thinking(current_agent.name, "(continuing conversation)")
|
102
109
|
chat.complete
|
103
110
|
end
|
104
111
|
response = result
|
@@ -119,6 +126,9 @@ module Agents
|
|
119
126
|
# Save current conversation state before switching
|
120
127
|
save_conversation_state(chat, context_wrapper, current_agent)
|
121
128
|
|
129
|
+
# Emit agent handoff event
|
130
|
+
context_wrapper.callback_manager.emit_agent_handoff(current_agent.name, next_agent.name, "handoff")
|
131
|
+
|
122
132
|
# Switch to new agent - store agent name for persistence
|
123
133
|
current_agent = next_agent
|
124
134
|
context_wrapper.context[:current_agent] = next_agent.name
|
@@ -143,7 +153,7 @@ module Agents
|
|
143
153
|
|
144
154
|
return RunResult.new(
|
145
155
|
output: response.content,
|
146
|
-
messages: extract_messages(chat, current_agent),
|
156
|
+
messages: MessageExtractor.extract_messages(chat, current_agent),
|
147
157
|
usage: context_wrapper.usage,
|
148
158
|
context: context_wrapper.context
|
149
159
|
)
|
@@ -154,7 +164,7 @@ module Agents
|
|
154
164
|
|
155
165
|
RunResult.new(
|
156
166
|
output: "Conversation ended: #{e.message}",
|
157
|
-
messages: chat ? extract_messages(chat, current_agent) : [],
|
167
|
+
messages: chat ? MessageExtractor.extract_messages(chat, current_agent) : [],
|
158
168
|
usage: context_wrapper.usage,
|
159
169
|
error: e,
|
160
170
|
context: context_wrapper.context
|
@@ -165,7 +175,7 @@ module Agents
|
|
165
175
|
|
166
176
|
RunResult.new(
|
167
177
|
output: nil,
|
168
|
-
messages: chat ? extract_messages(chat, current_agent) : [],
|
178
|
+
messages: chat ? MessageExtractor.extract_messages(chat, current_agent) : [],
|
169
179
|
usage: context_wrapper.usage,
|
170
180
|
error: e,
|
171
181
|
context: context_wrapper.context
|
@@ -208,7 +218,7 @@ module Agents
|
|
208
218
|
|
209
219
|
def save_conversation_state(chat, context_wrapper, current_agent)
|
210
220
|
# Extract messages from chat
|
211
|
-
messages = extract_messages(chat, current_agent)
|
221
|
+
messages = MessageExtractor.extract_messages(chat, current_agent)
|
212
222
|
|
213
223
|
# Update context with latest state
|
214
224
|
context_wrapper.context[:conversation_history] = messages
|
@@ -244,26 +254,5 @@ module Agents
|
|
244
254
|
chat.with_tools(*wrapped_regular_tools) if wrapped_regular_tools.any?
|
245
255
|
chat
|
246
256
|
end
|
247
|
-
|
248
|
-
def extract_messages(chat, current_agent)
|
249
|
-
return [] unless chat.respond_to?(:messages)
|
250
|
-
|
251
|
-
chat.messages.filter_map do |msg|
|
252
|
-
# Only include user and assistant messages with content
|
253
|
-
next unless %i[user assistant].include?(msg.role)
|
254
|
-
next unless msg.content && !msg.content.strip.empty?
|
255
|
-
|
256
|
-
message = {
|
257
|
-
role: msg.role,
|
258
|
-
content: msg.content
|
259
|
-
}
|
260
|
-
|
261
|
-
# Add agent attribution for assistant messages to enable conversation continuity
|
262
|
-
# This allows AgentRunner to determine which agent should continue the conversation
|
263
|
-
message[:agent_name] = current_agent.name if msg.role == :assistant && current_agent
|
264
|
-
|
265
|
-
message
|
266
|
-
end
|
267
|
-
end
|
268
257
|
end
|
269
258
|
end
|
data/lib/agents/tool_wrapper.rb
CHANGED
@@ -46,7 +46,17 @@ module Agents
|
|
46
46
|
# RubyLLM calls this method (follows RubyLLM::Tool pattern)
|
47
47
|
def call(args)
|
48
48
|
tool_context = ToolContext.new(run_context: @context_wrapper)
|
49
|
-
|
49
|
+
|
50
|
+
@context_wrapper.callback_manager.emit_tool_start(@tool.name, args)
|
51
|
+
|
52
|
+
begin
|
53
|
+
result = @tool.execute(tool_context, **args.transform_keys(&:to_sym))
|
54
|
+
@context_wrapper.callback_manager.emit_tool_complete(@tool.name, result)
|
55
|
+
result
|
56
|
+
rescue StandardError => e
|
57
|
+
@context_wrapper.callback_manager.emit_tool_complete(@tool.name, "ERROR: #{e.message}")
|
58
|
+
raise
|
59
|
+
end
|
50
60
|
end
|
51
61
|
|
52
62
|
# Delegate metadata methods to the tool
|
data/lib/agents/version.rb
CHANGED
data/lib/agents.rb
CHANGED
@@ -80,6 +80,8 @@ require_relative "agents/agent"
|
|
80
80
|
# Execution components
|
81
81
|
require_relative "agents/chat"
|
82
82
|
require_relative "agents/tool_wrapper"
|
83
|
+
require_relative "agents/message_extractor"
|
84
|
+
require_relative "agents/callback_manager"
|
83
85
|
require_relative "agents/agent_runner"
|
84
86
|
require_relative "agents/runner"
|
85
87
|
require_relative "agents/agent_tool"
|
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.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Shivam Mishra
|
@@ -31,8 +31,10 @@ executables: []
|
|
31
31
|
extensions: []
|
32
32
|
extra_rdoc_files: []
|
33
33
|
files:
|
34
|
+
- ".claude/commands/bump-version.md"
|
34
35
|
- ".rspec"
|
35
36
|
- ".rubocop.yml"
|
37
|
+
- CHANGELOG.md
|
36
38
|
- CLAUDE.md
|
37
39
|
- LICENSE
|
38
40
|
- README.md
|
@@ -47,6 +49,7 @@ files:
|
|
47
49
|
- docs/concepts.md
|
48
50
|
- docs/concepts/agent-tool.md
|
49
51
|
- docs/concepts/agents.md
|
52
|
+
- docs/concepts/callbacks.md
|
50
53
|
- docs/concepts/context.md
|
51
54
|
- docs/concepts/handoffs.md
|
52
55
|
- docs/concepts/runner.md
|
@@ -94,8 +97,10 @@ files:
|
|
94
97
|
- lib/agents/agent.rb
|
95
98
|
- lib/agents/agent_runner.rb
|
96
99
|
- lib/agents/agent_tool.rb
|
100
|
+
- lib/agents/callback_manager.rb
|
97
101
|
- lib/agents/chat.rb
|
98
102
|
- lib/agents/handoff.rb
|
103
|
+
- lib/agents/message_extractor.rb
|
99
104
|
- lib/agents/result.rb
|
100
105
|
- lib/agents/run_context.rb
|
101
106
|
- lib/agents/runner.rb
|