ai-agents 0.1.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.
@@ -0,0 +1,233 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Agents
4
+ # The execution engine that orchestrates conversations between users and agents.
5
+ # Runner manages the conversation flow, handles tool execution through RubyLLM,
6
+ # coordinates handoffs between agents, and ensures thread-safe operation.
7
+ #
8
+ # The Runner follows a turn-based execution model where each turn consists of:
9
+ # 1. Sending a message to the LLM with current context
10
+ # 2. Receiving a response that may include tool calls
11
+ # 3. Executing tools and getting results (handled by RubyLLM)
12
+ # 4. Checking for agent handoffs
13
+ # 5. Continuing until no more tools are called
14
+ #
15
+ # ## Thread Safety
16
+ # The Runner ensures thread safety by:
17
+ # - Creating new context wrappers for each execution
18
+ # - Using tool wrappers that pass context through parameters
19
+ # - Never storing execution state in shared variables
20
+ #
21
+ # ## Integration with RubyLLM
22
+ # We leverage RubyLLM for LLM communication and tool execution while
23
+ # maintaining our own context management and handoff logic.
24
+ #
25
+ # @example Simple conversation
26
+ # agent = Agents::Agent.new(
27
+ # name: "Assistant",
28
+ # instructions: "You are a helpful assistant",
29
+ # tools: [weather_tool]
30
+ # )
31
+ #
32
+ # result = Agents::Runner.run(agent, "What's the weather?")
33
+ # puts result.output
34
+ # # => "Let me check the weather for you..."
35
+ #
36
+ # @example Conversation with context
37
+ # result = Agents::Runner.run(
38
+ # support_agent,
39
+ # "I need help with my order",
40
+ # context: { user_id: 123, order_id: 456 }
41
+ # )
42
+ #
43
+ # @example Multi-agent handoff
44
+ # triage = Agents::Agent.new(
45
+ # name: "Triage",
46
+ # instructions: "Route users to the right specialist",
47
+ # handoff_agents: [billing_agent, tech_agent]
48
+ # )
49
+ #
50
+ # result = Agents::Runner.run(triage, "I can't pay my bill")
51
+ # # Triage agent will handoff to billing_agent
52
+ class Runner
53
+ DEFAULT_MAX_TURNS = 10
54
+
55
+ class MaxTurnsExceeded < StandardError; end
56
+
57
+ # Convenience class method for running agents
58
+ def self.run(agent, input, context: {}, max_turns: DEFAULT_MAX_TURNS)
59
+ new.run(agent, input, context: context, max_turns: max_turns)
60
+ end
61
+
62
+ # Execute an agent with the given input and context
63
+ #
64
+ # @param starting_agent [Agents::Agent] The initial agent to run
65
+ # @param input [String] The user's input message
66
+ # @param context [Hash] Shared context data accessible to all tools
67
+ # @param max_turns [Integer] Maximum conversation turns before stopping
68
+ # @return [RunResult] The result containing output, messages, and usage
69
+ def run(starting_agent, input, context: {}, max_turns: DEFAULT_MAX_TURNS)
70
+ # Determine current agent from context or use starting agent
71
+ current_agent = context[:current_agent] || starting_agent
72
+
73
+ # Create context wrapper with deep copy for thread safety
74
+ context_copy = deep_copy_context(context)
75
+ context_wrapper = RunContext.new(context_copy)
76
+ current_turn = 0
77
+
78
+ # Create chat and restore conversation history
79
+ chat = create_chat(current_agent, context_wrapper)
80
+ restore_conversation_history(chat, context_wrapper)
81
+
82
+ loop do
83
+ current_turn += 1
84
+ raise MaxTurnsExceeded, "Exceeded maximum turns: #{max_turns}" if current_turn > max_turns
85
+
86
+ # Get response from LLM (RubyLLM handles tool execution)
87
+ response = if current_turn == 1
88
+ chat.ask(input)
89
+ else
90
+ chat.complete
91
+ end
92
+
93
+ # Update usage
94
+ context_wrapper.usage.add(response.usage) if response.respond_to?(:usage) && response.usage
95
+
96
+ # Check for handoff via context (set by HandoffTool)
97
+ if context_wrapper.context[:pending_handoff]
98
+ next_agent = context_wrapper.context[:pending_handoff]
99
+ puts "[Agents] Handoff from #{current_agent.name} to #{next_agent.name}"
100
+
101
+ # Save current conversation state before switching
102
+ save_conversation_state(chat, context_wrapper, current_agent)
103
+
104
+ # Switch to new agent
105
+ current_agent = next_agent
106
+ context_wrapper.context[:current_agent] = next_agent
107
+ context_wrapper.context.delete(:pending_handoff)
108
+
109
+ # Create new chat for new agent with restored history
110
+ chat = create_chat(current_agent, context_wrapper)
111
+ restore_conversation_history(chat, context_wrapper)
112
+ next
113
+ end
114
+
115
+ # If no tools were called, we have our final response
116
+ next if response.tool_call?
117
+
118
+ # Save final state before returning
119
+ save_conversation_state(chat, context_wrapper, current_agent)
120
+
121
+ return RunResult.new(
122
+ output: response.content,
123
+ messages: extract_messages(chat),
124
+ usage: context_wrapper.usage,
125
+ context: context_wrapper.context
126
+ )
127
+ end
128
+ rescue MaxTurnsExceeded => e
129
+ # Save state even on error
130
+ save_conversation_state(chat, context_wrapper, current_agent) if chat
131
+
132
+ RunResult.new(
133
+ output: "Conversation ended: #{e.message}",
134
+ messages: chat ? extract_messages(chat) : [],
135
+ usage: context_wrapper.usage,
136
+ error: e,
137
+ context: context_wrapper.context
138
+ )
139
+ rescue StandardError => e
140
+ # Save state even on error
141
+ save_conversation_state(chat, context_wrapper, current_agent) if chat
142
+
143
+ RunResult.new(
144
+ output: nil,
145
+ messages: chat ? extract_messages(chat) : [],
146
+ usage: context_wrapper.usage,
147
+ error: e,
148
+ context: context_wrapper.context
149
+ )
150
+ end
151
+
152
+ private
153
+
154
+ def deep_copy_context(context)
155
+ # Handle deep copying for thread safety
156
+ context.dup.tap do |copied|
157
+ copied[:conversation_history] = context[:conversation_history]&.map(&:dup) || []
158
+ # Don't copy agents - they're immutable
159
+ copied[:current_agent] = context[:current_agent]
160
+ copied[:turn_count] = context[:turn_count] || 0
161
+ end
162
+ end
163
+
164
+ def restore_conversation_history(chat, context_wrapper)
165
+ history = context_wrapper.context[:conversation_history] || []
166
+
167
+ history.each do |msg|
168
+ # Only restore user and assistant messages with content
169
+ next unless %i[user assistant].include?(msg[:role])
170
+ next unless msg[:content] && !msg[:content].strip.empty?
171
+
172
+ chat.add_message(
173
+ role: msg[:role].to_sym,
174
+ content: msg[:content]
175
+ )
176
+ rescue StandardError => e
177
+ # Continue with partial history on error
178
+ puts "[Agents] Failed to restore message: #{e.message}"
179
+ end
180
+ rescue StandardError => e
181
+ # If history restoration completely fails, continue with empty history
182
+ puts "[Agents] Failed to restore conversation history: #{e.message}"
183
+ context_wrapper.context[:conversation_history] = []
184
+ end
185
+
186
+ def save_conversation_state(chat, context_wrapper, current_agent)
187
+ # Extract messages from chat
188
+ messages = extract_messages(chat)
189
+
190
+ # Update context with latest state
191
+ context_wrapper.context[:conversation_history] = messages
192
+ context_wrapper.context[:current_agent] = current_agent
193
+ context_wrapper.context[:turn_count] = (context_wrapper.context[:turn_count] || 0) + 1
194
+ context_wrapper.context[:last_updated] = Time.now
195
+
196
+ # Clean up temporary handoff state
197
+ context_wrapper.context.delete(:pending_handoff)
198
+ rescue StandardError => e
199
+ puts "[Agents] Failed to save conversation state: #{e.message}"
200
+ end
201
+
202
+ def create_chat(agent, context_wrapper)
203
+ # Get system prompt (may be dynamic)
204
+ system_prompt = agent.get_system_prompt(context_wrapper)
205
+
206
+ # Wrap tools with context for thread-safe execution
207
+ wrapped_tools = agent.all_tools.map do |tool|
208
+ ToolWrapper.new(tool, context_wrapper)
209
+ end
210
+
211
+ # Create chat with proper RubyLLM API
212
+ chat = RubyLLM.chat(model: agent.model)
213
+ chat.with_instructions(system_prompt) if system_prompt
214
+ chat.with_tools(*wrapped_tools) if wrapped_tools.any?
215
+ chat
216
+ end
217
+
218
+ def extract_messages(chat)
219
+ return [] unless chat.respond_to?(:messages)
220
+
221
+ chat.messages.filter_map do |msg|
222
+ # Only include user and assistant messages with content
223
+ next unless %i[user assistant].include?(msg.role)
224
+ next unless msg.content && !msg.content.strip.empty?
225
+
226
+ {
227
+ role: msg.role,
228
+ content: msg.content
229
+ }
230
+ end
231
+ end
232
+ end
233
+ end
@@ -0,0 +1,153 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Tool is the base class for all agent tools, providing a thread-safe interface for
4
+ # agents to interact with external systems and perform actions. Tools extend RubyLLM::Tool
5
+ # while adding critical thread-safety guarantees and enhanced error handling.
6
+ #
7
+ # ## Thread-Safe Design Principles
8
+ # Tools extend RubyLLM::Tool but maintain thread safety by:
9
+ # 1. **No execution state in instance variables** - Only configuration
10
+ # 2. **All state passed through parameters** - ToolContext as first param
11
+ # 3. **Immutable tool instances** - Create once, use everywhere
12
+ # 4. **Stateless perform methods** - Pure functions with context input
13
+ #
14
+ # ## Why Thread Safety Matters
15
+ # In a multi-agent system, the same tool instance may be used concurrently by different
16
+ # agents running in separate threads or fibers. Storing execution state in instance
17
+ # variables would cause race conditions and data corruption.
18
+ #
19
+ # @example Defining a thread-safe tool
20
+ # class WeatherTool < Agents::Tool
21
+ # name "get_weather"
22
+ # description "Get current weather for a location"
23
+ # param :location, type: "string", desc: "City name or coordinates"
24
+ #
25
+ # def perform(tool_context, location:)
26
+ # # All state comes from parameters - no instance variables!
27
+ # api_key = tool_context.context[:weather_api_key]
28
+ # cache_duration = tool_context.context[:cache_duration] || 300
29
+ #
30
+ # begin
31
+ # # Make API call...
32
+ # "Sunny, 72°F in #{location}"
33
+ # rescue => e
34
+ # "Weather service unavailable: #{e.message}"
35
+ # end
36
+ # end
37
+ # end
38
+ #
39
+ # @example Using the functional tool definition
40
+ # # Define a calculator tool
41
+ # calculator = Agents::Tool.tool(
42
+ # "calculate",
43
+ # description: "Perform mathematical calculations"
44
+ # ) do |tool_context, expression:|
45
+ # begin
46
+ # result = eval(expression)
47
+ # result.to_s
48
+ # rescue => e
49
+ # "Calculation error: #{e.message}"
50
+ # end
51
+ # end
52
+ #
53
+ # # Use the tool in an agent
54
+ # agent = Agents::Agent.new(
55
+ # name: "Math Assistant",
56
+ # instructions: "You are a helpful math assistant",
57
+ # tools: [calculator]
58
+ # )
59
+ #
60
+ # # During execution, the runner would call it like this:
61
+ # run_context = Agents::RunContext.new({ user_id: 123 })
62
+ # tool_context = Agents::ToolContext.new(run_context: run_context)
63
+ #
64
+ # result = calculator.execute(tool_context, expression: "2 + 2 * 3")
65
+ # # => "8"
66
+ module Agents
67
+ class Tool < RubyLLM::Tool
68
+ class << self
69
+ def param(name, type = String, desc = nil, required: true, **options)
70
+ # Convert Ruby types to JSON schema types
71
+ json_type = case type
72
+ when String, "string" then "string"
73
+ when Integer, "integer" then "integer"
74
+ when Float, "number" then "number"
75
+ when TrueClass, FalseClass, "boolean" then "boolean"
76
+ when Array, "array" then "array"
77
+ when Hash, "object" then "object"
78
+ else "string"
79
+ end
80
+
81
+ # Call parent param method
82
+ super(name, type: json_type, desc: desc, required: required, **options)
83
+ end
84
+ end
85
+ # Execute the tool with context injection.
86
+ # This method is called by the runner and handles the thread-safe
87
+ # execution pattern by passing all state through parameters.
88
+ #
89
+ # @param tool_context [Agents::ToolContext] The execution context containing shared state and usage tracking
90
+ # @param params [Hash] Tool-specific parameters as defined by the tool's param declarations
91
+ # @return [String] The tool's result
92
+ def execute(tool_context, **params)
93
+ perform(tool_context, **params)
94
+ end
95
+
96
+ # Perform the tool's action. Subclasses must implement this method.
97
+ # This is where the actual tool logic lives. The method receives all
98
+ # execution state through parameters, ensuring thread safety.
99
+ #
100
+ # @param tool_context [Agents::ToolContext] The execution context
101
+ # @param params [Hash] Tool-specific parameters
102
+ # @return [String] The tool's result
103
+ # @raise [NotImplementedError] If not implemented by subclass
104
+ # @example Implementing perform in a subclass
105
+ # class SearchTool < Agents::Tool
106
+ # def perform(tool_context, query:, max_results: 10)
107
+ # api_key = tool_context.context[:search_api_key]
108
+ # results = SearchAPI.search(query, api_key: api_key, limit: max_results)
109
+ # results.map(&:title).join("\n")
110
+ # end
111
+ # end
112
+ def perform(tool_context, **params)
113
+ raise NotImplementedError, "Tools must implement #perform(tool_context, **params)"
114
+ end
115
+
116
+ # Create a tool instance using a functional style definition.
117
+ # This is an alternative to creating a full class for simple tools.
118
+ # The block becomes the tool's perform method.
119
+ #
120
+ # @param name [String] The tool's name (used in function calling)
121
+ # @param description [String] Brief description of what the tool does
122
+ # @yield [tool_context, **params] The block that implements the tool's logic
123
+ # @return [Agents::Tool] A new tool instance
124
+ # @example Creating a simple tool functionally
125
+ # math_tool = Agents::Tool.tool(
126
+ # "add_numbers",
127
+ # description: "Add two numbers together"
128
+ # ) do |tool_context, a:, b:|
129
+ # (a + b).to_s
130
+ # end
131
+ #
132
+ # @example Tool accessing context with error handling
133
+ # greeting_tool = Agents::Tool.tool("greet", description: "Greet a user") do |tool_context, name:|
134
+ # language = tool_context.context[:language] || "en"
135
+ # case language
136
+ # when "es" then "¡Hola, #{name}!"
137
+ # when "fr" then "Bonjour, #{name}!"
138
+ # else "Hello, #{name}!"
139
+ # end
140
+ # rescue => e
141
+ # "Sorry, I couldn't greet you: #{e.message}"
142
+ # end
143
+ def self.tool(name, description: "", &block)
144
+ # Create anonymous class that extends Tool
145
+ Class.new(Tool) do
146
+ self.name = name
147
+ self.description = description
148
+
149
+ define_method :perform, &block
150
+ end.new
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ # ToolContext provides tools with controlled access to execution state during their invocation.
4
+ # It wraps the RunContext and adds tool-specific metadata like retry count. This is a critical
5
+ # component of the thread-safe design - tools receive all their execution state through this
6
+ # context object rather than storing it in instance variables.
7
+ #
8
+ # @example Tool receiving context during execution
9
+ # class CalculatorTool < Agents::Tool
10
+ # def perform(tool_context, expression:)
11
+ # # Access shared context data
12
+ # precision = tool_context.context[:precision] || 2
13
+ #
14
+ # # Track retry attempts
15
+ # if tool_context.retry_count > 0
16
+ # logger.warn "Retrying calculation (attempt #{tool_context.retry_count + 1})"
17
+ # end
18
+ #
19
+ # result = eval(expression)
20
+ # result.round(precision)
21
+ # end
22
+ # end
23
+ #
24
+ # @example Runner creating ToolContext for execution
25
+ # run_context = Agents::RunContext.new({ user: current_user })
26
+ # tool_context = Agents::ToolContext.new(
27
+ # run_context: run_context,
28
+ # retry_count: 0
29
+ # )
30
+ #
31
+ # result = tool.execute(tool_context, expression: "2 + 2")
32
+ #
33
+ # ## How Thread Safety is Ensured
34
+ #
35
+ # ToolContext enables thread-safe tool execution by enforcing a critical principle:
36
+ # all execution state must flow through method parameters, never through instance variables.
37
+ #
38
+ # Here's how it works:
39
+ #
40
+ # 1. **Tool instances are stateless** - They only store configuration (name, description),
41
+ # never execution data like current user, session, or request details.
42
+ #
43
+ # 2. **State flows through parameters** - When a tool is executed, it receives a ToolContext
44
+ # parameter that contains all the execution-specific state it needs.
45
+ #
46
+ # 3. **Each execution is isolated** - Multiple threads can call the same tool instance
47
+ # simultaneously, but each call gets its own ToolContext with its own state.
48
+ #
49
+ # This is similar to how web frameworks handle concurrent requests - the controller
50
+ # instance might be shared, but each request gets its own params and session objects.
51
+ # The ToolContext serves the same purpose, providing isolation without requiring
52
+ # new object creation for each execution.
53
+ module Agents
54
+ class ToolContext
55
+ attr_reader :run_context, :retry_count
56
+
57
+ # Initialize a new ToolContext wrapping a RunContext
58
+ #
59
+ # @param run_context [Agents::RunContext] The run context containing shared execution state
60
+ # @param retry_count [Integer] Number of times this tool execution has been retried (default: 0)
61
+ def initialize(run_context:, retry_count: 0)
62
+ @run_context = run_context
63
+ @retry_count = retry_count
64
+ end
65
+
66
+ # Convenient access to the shared context hash from the RunContext.
67
+ # This delegation makes it easier for tools to access context data without
68
+ # having to navigate through run_context.context.
69
+ #
70
+ # @return [Hash] The shared context hash from the RunContext
71
+ # @example Accessing context data in a tool
72
+ # def perform(tool_context, **params)
73
+ # user_id = tool_context.context[:user_id]
74
+ # session = tool_context.context[:session]
75
+ # # ...
76
+ # end
77
+ def context
78
+ @run_context.context
79
+ end
80
+
81
+ # Convenient access to the usage tracking object from the RunContext.
82
+ # Tools can use this to add their own usage metrics if they make
83
+ # additional LLM calls.
84
+ #
85
+ # @return [Agents::RunContext::Usage] The usage tracking object
86
+ # @example Tool tracking additional LLM usage
87
+ # def perform(tool_context, **params)
88
+ # # Tool makes its own LLM call
89
+ # response = llm.complete("Analyze: #{params[:data]}")
90
+ # tool_context.usage.add(response.usage)
91
+ #
92
+ # response.content
93
+ # end
94
+ def usage
95
+ @run_context.usage
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Agents
4
+ # A thread-safe wrapper that bridges RubyLLM's tool execution with our context injection pattern.
5
+ # This wrapper solves a critical problem: RubyLLM calls tools with just the LLM-provided
6
+ # parameters, but our tools need access to the execution context for shared state.
7
+ #
8
+ # ## The Thread Safety Problem
9
+ # Without this wrapper, we'd need to store context in tool instance variables, which would
10
+ # cause race conditions when the same tool is used by multiple concurrent requests:
11
+ #
12
+ # # UNSAFE - Don't do this!
13
+ # class BadTool < Agents::Tool
14
+ # def set_context(ctx)
15
+ # @context = ctx # Race condition!
16
+ # end
17
+ # end
18
+ #
19
+ # ## The Solution
20
+ # Each Runner creates new wrapper instances for each execution, capturing the context
21
+ # in the wrapper's closure. When RubyLLM calls execute(), the wrapper injects the
22
+ # context before calling the actual tool:
23
+ #
24
+ # # Runner creates wrapper
25
+ # wrapped = ToolWrapper.new(my_tool, context_wrapper)
26
+ #
27
+ # # RubyLLM calls wrapper
28
+ # wrapped.execute(city: "NYC") # No context parameter
29
+ #
30
+ # # Wrapper injects context
31
+ # tool_context = ToolContext.new(run_context: context_wrapper)
32
+ # my_tool.execute(tool_context, city: "NYC") # Context injected!
33
+ #
34
+ # This ensures each execution has its own context without any shared mutable state.
35
+ class ToolWrapper
36
+ def initialize(tool, context_wrapper)
37
+ @tool = tool
38
+ @context_wrapper = context_wrapper
39
+
40
+ # Copy tool metadata for RubyLLM
41
+ @name = tool.name
42
+ @description = tool.description
43
+ @params = tool.class.params if tool.class.respond_to?(:params)
44
+ end
45
+
46
+ # RubyLLM calls this method (follows RubyLLM::Tool pattern)
47
+ def call(args)
48
+ tool_context = ToolContext.new(run_context: @context_wrapper)
49
+ @tool.execute(tool_context, **args.transform_keys(&:to_sym))
50
+ end
51
+
52
+ # Delegate metadata methods to the tool
53
+ def name
54
+ @name || @tool.name
55
+ end
56
+
57
+ def description
58
+ @description || @tool.description
59
+ end
60
+
61
+ # RubyLLM calls this to get parameter definitions
62
+ def parameters
63
+ @tool.parameters
64
+ end
65
+
66
+ # Make this work with RubyLLM's tool calling
67
+ def to_s
68
+ name
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Agents
4
+ VERSION = "0.1.0"
5
+ end
data/lib/agents.rb ADDED
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Main entry point for the Ruby AI Agents SDK
4
+ # This file sets up the core Agents module namespace and provides global configuration
5
+ # for the multi-agent system including LLM provider setup, API keys, and system defaults.
6
+ # It serves as the central configuration hub that other components depend on.
7
+
8
+ require "ruby_llm"
9
+ require_relative "agents/version"
10
+
11
+ module Agents
12
+ class Error < StandardError; end
13
+
14
+ class << self
15
+ # Logger for debugging (can be set by users)
16
+ attr_accessor :logger
17
+
18
+ # Configure both Agents and RubyLLM in one block
19
+ def configure
20
+ yield(configuration) if block_given?
21
+ configure_ruby_llm!
22
+ configuration
23
+ end
24
+
25
+ def configuration
26
+ @configuration ||= Configuration.new
27
+ end
28
+
29
+ private
30
+
31
+ def configure_ruby_llm!
32
+ RubyLLM.configure do |config|
33
+ config.openai_api_key = configuration.openai_api_key if configuration.openai_api_key
34
+ config.anthropic_api_key = configuration.anthropic_api_key if configuration.anthropic_api_key
35
+ config.gemini_api_key = configuration.gemini_api_key if configuration.gemini_api_key
36
+ config.default_model = configuration.default_model
37
+ config.log_level = configuration.debug == true ? :debug : :info
38
+ config.request_timeout = configuration.request_timeout if configuration.request_timeout
39
+ end
40
+ end
41
+ end
42
+
43
+ class Configuration
44
+ attr_accessor :openai_api_key, :anthropic_api_key, :gemini_api_key, :request_timeout, :default_model, :debug
45
+
46
+ def initialize
47
+ @default_model = "gpt-4o-mini"
48
+ @request_timeout = 120
49
+ @debug = false
50
+ end
51
+
52
+ # Check if at least one provider is configured
53
+ # @return [Boolean] True if any provider has an API key
54
+ def configured?
55
+ @openai_api_key || @anthropic_api_key || @gemini_api_key
56
+ end
57
+ end
58
+ end
59
+
60
+ # Core components
61
+ require_relative "agents/result"
62
+ require_relative "agents/run_context"
63
+ require_relative "agents/tool_context"
64
+ require_relative "agents/tool"
65
+ require_relative "agents/handoff"
66
+ require_relative "agents/agent"
67
+
68
+ # Execution components
69
+ require_relative "agents/tool_wrapper"
70
+ require_relative "agents/runner"
@@ -0,0 +1,6 @@
1
+ module Ruby
2
+ module Agents
3
+ VERSION: String
4
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
5
+ end
6
+ end