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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 63e05456e057d627b1b3a3f75bfbbf5753b7dc7521fc429376681d6ffe222e54
4
- data.tar.gz: 354528a7d4eae51c5598b41ad7fbfb77ec715e9354469c827074a974cb1e43c4
3
+ metadata.gz: b2d0dbff300d9f3bd08da74d39e1fbf2eecbfa0b293558af01b5ecfcc765d58c
4
+ data.tar.gz: 84a94feb4a5faea46ec658159f1266d315ba8175ad8b4c62915745c6e430c741
5
5
  SHA512:
6
- metadata.gz: 1d6ff99299ba88e2fba47cff1fb13f686e30bf4826014da7bec95b7363609f8b6acc2bb69a86393786d048bd693d4b1efb2697cd6a7376abe0a81f6ea347da0d
7
- data.tar.gz: 43fb8db36d106caff7220bcdc4f6dba063a0b82b208baf757a7ffa578d7dde8dd917cff408cfa897037c0a2c6efbe004685399bbe93dce92958575f1181112f8
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 # Core agent definition and configuration
133
- ├── agent_runner.rb # Thread-safe execution manager (main API)
134
- ├── runner.rb # Internal execution engine for conversation turns
135
- ├── tool.rb # Base class for custom tools
136
- ├── handoff.rb # Agent handoff management
137
- ├── chat.rb # Chat message handling
138
- ├── result.rb # Result object for agent responses
139
- ├── run_context.rb # Execution context management
140
- ├── tool_context.rb # Tool execution context
141
- ├── tool_wrapper.rb # Thread-safe tool wrapping
142
- └── version.rb # Gem version
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::Runner.with_agents(triage, billing, support)
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
- - WebMock is used for HTTP mocking in tests
189
- - RSpec is the testing framework with standard configuration
190
- - Tests are organized by component in `spec/agents/`
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"
@@ -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
@@ -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
- def initialize(context)
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
@@ -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
- @tool.execute(tool_context, **args.transform_keys(&:to_sym))
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Agents
4
- VERSION = "0.1.3"
4
+ VERSION = "0.2.0"
5
5
  end
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.1.3
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