ai-agents 0.1.1 → 0.1.3

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.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +29 -106
  3. data/docs/Gemfile +14 -0
  4. data/docs/Gemfile.lock +183 -0
  5. data/docs/_config.yml +53 -0
  6. data/docs/_sass/color_schemes/ruby.scss +72 -0
  7. data/docs/_sass/custom/custom.scss +93 -0
  8. data/docs/architecture.md +353 -0
  9. data/docs/assets/fonts/InterVariable.woff2 +0 -0
  10. data/docs/concepts/agent-tool.md +166 -0
  11. data/docs/concepts/agents.md +43 -0
  12. data/docs/concepts/context.md +110 -0
  13. data/docs/concepts/handoffs.md +81 -0
  14. data/docs/concepts/runner.md +87 -0
  15. data/docs/concepts/tools.md +62 -0
  16. data/docs/concepts.md +21 -0
  17. data/docs/guides/agent-as-tool-pattern.md +242 -0
  18. data/docs/guides/multi-agent-systems.md +261 -0
  19. data/docs/guides/rails-integration.md +440 -0
  20. data/docs/guides/state-persistence.md +451 -0
  21. data/docs/guides.md +18 -0
  22. data/docs/index.md +95 -0
  23. data/examples/collaborative-copilot/README.md +169 -0
  24. data/examples/collaborative-copilot/agents/analysis_agent.rb +48 -0
  25. data/examples/collaborative-copilot/agents/answer_suggestion_agent.rb +50 -0
  26. data/examples/collaborative-copilot/agents/copilot_orchestrator.rb +85 -0
  27. data/examples/collaborative-copilot/agents/integrations_agent.rb +58 -0
  28. data/examples/collaborative-copilot/agents/research_agent.rb +52 -0
  29. data/examples/collaborative-copilot/data/contacts.json +47 -0
  30. data/examples/collaborative-copilot/data/conversations.json +170 -0
  31. data/examples/collaborative-copilot/data/knowledge_base.json +58 -0
  32. data/examples/collaborative-copilot/data/linear_issues.json +83 -0
  33. data/examples/collaborative-copilot/data/stripe_billing.json +71 -0
  34. data/examples/collaborative-copilot/interactive.rb +90 -0
  35. data/examples/collaborative-copilot/tools/create_linear_ticket_tool.rb +58 -0
  36. data/examples/collaborative-copilot/tools/get_article_tool.rb +41 -0
  37. data/examples/collaborative-copilot/tools/get_contact_tool.rb +51 -0
  38. data/examples/collaborative-copilot/tools/get_conversation_tool.rb +53 -0
  39. data/examples/collaborative-copilot/tools/get_stripe_billing_tool.rb +44 -0
  40. data/examples/collaborative-copilot/tools/search_contacts_tool.rb +57 -0
  41. data/examples/collaborative-copilot/tools/search_conversations_tool.rb +54 -0
  42. data/examples/collaborative-copilot/tools/search_knowledge_base_tool.rb +55 -0
  43. data/examples/collaborative-copilot/tools/search_linear_issues_tool.rb +60 -0
  44. data/examples/isp-support/agents_factory.rb +57 -1
  45. data/examples/isp-support/tools/create_lead_tool.rb +16 -2
  46. data/examples/isp-support/tools/crm_lookup_tool.rb +13 -1
  47. data/lib/agents/agent.rb +52 -6
  48. data/lib/agents/agent_tool.rb +113 -0
  49. data/lib/agents/handoff.rb +8 -34
  50. data/lib/agents/tool_context.rb +36 -0
  51. data/lib/agents/version.rb +1 -1
  52. data/lib/agents.rb +1 -0
  53. metadata +44 -2
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Copilot
6
+ # Tool for searching knowledge base articles and documentation
7
+ class SearchKnowledgeBaseTool < Agents::Tool
8
+ description "Search help documentation and knowledge base for solutions"
9
+ param :query, type: "string", desc: "Search terms or keywords to find relevant articles"
10
+ param :category, type: "string",
11
+ desc: "Optional: filter by category (troubleshooting, account, development, billing)", required: false
12
+
13
+ def perform(_tool_context, query:, category: nil)
14
+ data_file = File.join(__dir__, "../data/knowledge_base.json")
15
+ return "Knowledge base unavailable" unless File.exist?(data_file)
16
+
17
+ begin
18
+ kb_data = JSON.parse(File.read(data_file))
19
+ articles = kb_data["articles"]
20
+ query_text = query.downcase
21
+ matches = []
22
+
23
+ articles.each do |article_id, article|
24
+ # Skip if category filter is specified and doesn't match
25
+ next if category && article["category"] != category.downcase
26
+
27
+ # Simple keyword matching
28
+ text_to_search = [
29
+ article["title"],
30
+ article["tags"].join(" "),
31
+ article["content"]
32
+ ].join(" ").downcase
33
+
34
+ next unless text_to_search.include?(query_text)
35
+
36
+ matches << {
37
+ article_id: article_id,
38
+ title: article["title"],
39
+ category: article["category"],
40
+ tags: article["tags"],
41
+ content_preview: "#{article["content"][0...200]}..."
42
+ }
43
+ end
44
+
45
+ if matches.empty?
46
+ "No knowledge base articles found for: #{query}"
47
+ else
48
+ JSON.pretty_generate({ query: query, articles: matches })
49
+ end
50
+ rescue StandardError => e
51
+ "Error searching knowledge base: #{e.message}"
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Copilot
6
+ # Tool for searching Linear issues for development context and bug reports
7
+ class SearchLinearIssuesTool < Agents::Tool
8
+ description "Search Linear issues for bug reports, feature requests, and development context"
9
+ param :query, type: "string", desc: "Search terms (keywords, error messages, or feature descriptions)"
10
+ param :status, type: "string", desc: "Optional: filter by status (backlog, in_progress, completed, resolved)",
11
+ required: false
12
+ param :priority, type: "string", desc: "Optional: filter by priority (low, medium, high)", required: false
13
+
14
+ def perform(_tool_context, query:, status: nil, priority: nil)
15
+ data_file = File.join(__dir__, "../data/linear_issues.json")
16
+ return "Linear issues database unavailable" unless File.exist__(data_file)
17
+
18
+ begin
19
+ linear_data = JSON.parse(File.read(data_file))
20
+ issues = linear_data["issues"]
21
+ query_text = query.downcase
22
+ matches = []
23
+
24
+ issues.each do |issue_id, issue|
25
+ # Apply filters
26
+ next if status && issue["status"] != status.downcase
27
+ next if priority && issue["priority"] != priority.downcase
28
+
29
+ # Simple keyword matching
30
+ text_to_search = [
31
+ issue["title"],
32
+ issue["description"],
33
+ issue["labels"].join(" "),
34
+ issue["resolution"]
35
+ ].compact.join(" ").downcase
36
+
37
+ next unless text_to_search.include?(query_text)
38
+
39
+ matches << {
40
+ issue_id: issue_id,
41
+ title: issue["title"],
42
+ status: issue["status"],
43
+ priority: issue["priority"],
44
+ labels: issue["labels"],
45
+ description: "#{issue["description"][0...200]}...",
46
+ resolution: issue["resolution"]
47
+ }
48
+ end
49
+
50
+ if matches.empty?
51
+ "No Linear issues found for: #{query}"
52
+ else
53
+ JSON.pretty_generate({ query: query, issues: matches })
54
+ end
55
+ rescue StandardError => e
56
+ "Error searching Linear issues: #{e.message}"
57
+ end
58
+ end
59
+ end
60
+ end
@@ -51,7 +51,7 @@ module ISPSupport
51
51
  def create_sales_agent
52
52
  Agents::Agent.new(
53
53
  name: "Sales Agent",
54
- instructions: sales_instructions,
54
+ instructions: sales_instructions_with_state,
55
55
  model: "gpt-4.1-mini",
56
56
  tools: [ISPSupport::CreateLeadTool.new, ISPSupport::CreateCheckoutTool.new]
57
57
  )
@@ -113,6 +113,62 @@ module ISPSupport
113
113
  INSTRUCTIONS
114
114
  end
115
115
 
116
+ def sales_instructions_with_state
117
+ lambda { |context|
118
+ state = context.context[:state] || {}
119
+
120
+ base_instructions = <<~INSTRUCTIONS
121
+ You are the Sales Agent for an ISP. You handle new customer acquisition, service upgrades,
122
+ and plan changes.
123
+
124
+ **Your tools:**
125
+ - `create_lead`: Create sales leads with customer information
126
+ - `create_checkout`: Generate secure checkout links for purchases
127
+ - Handoff tools: Route back to triage when needed
128
+
129
+ **When to hand off:**
130
+ - Pure technical support questions → Triage Agent for re-routing
131
+ - Customer needs to speak with human agent → Triage Agent for re-routing
132
+ INSTRUCTIONS
133
+
134
+ # Add customer context if available from previous agent interactions
135
+ if state[:customer_name] && state[:customer_id]
136
+ base_instructions += <<~CONTEXT
137
+
138
+ **Customer Context Available:**
139
+ - Customer Name: #{state[:customer_name]}
140
+ - Customer ID: #{state[:customer_id]}
141
+ - Email: #{state[:customer_email]}
142
+ - Phone: #{state[:customer_phone]}
143
+ - Address: #{state[:customer_address]}
144
+ #{state[:current_plan] ? "- Current Plan: #{state[:current_plan]}" : ""}
145
+ #{state[:account_status] ? "- Account Status: #{state[:account_status]}" : ""}
146
+ #{state[:monthly_usage] ? "- Monthly Usage: #{state[:monthly_usage]}GB" : ""}
147
+
148
+ **IMPORTANT:**#{" "}
149
+ - Use this existing customer information when creating leads or providing service
150
+ - Do NOT ask for name, email, phone, or address - you already have these details
151
+ - For new connections, use the existing customer details and only ask for the desired plan
152
+ - Provide personalized recommendations based on their current information
153
+ CONTEXT
154
+ end
155
+
156
+ base_instructions += <<~FINAL_INSTRUCTIONS
157
+
158
+ **Instructions:**
159
+ - Be enthusiastic but not pushy
160
+ - Gather required info: name, email, desired plan for leads
161
+ - For account verification, ask customer for their account details directly
162
+ - For existing customers wanting upgrades, collect account info and proceed
163
+ - Create checkout links for confirmed purchases
164
+ - Always explain next steps after creating leads or checkout links
165
+ - Handle billing questions yourself - don't hand off for account verification
166
+ FINAL_INSTRUCTIONS
167
+
168
+ base_instructions
169
+ }
170
+ end
171
+
116
172
  def support_instructions
117
173
  <<~INSTRUCTIONS
118
174
  You are the Support Agent for an ISP. You handle technical support, troubleshooting,
@@ -8,8 +8,22 @@ module ISPSupport
8
8
  param :email, type: "string", desc: "Customer's email address"
9
9
  param :desired_plan, type: "string", desc: "Plan the customer is interested in"
10
10
 
11
- def perform(_tool_context, name:, email:, desired_plan:)
12
- "Lead created for #{name} (#{email}) interested in #{desired_plan} plan. Sales team will contact within 24 hours."
11
+ def perform(tool_context, name:, email:, desired_plan:)
12
+ # Store lead information in state for follow-up
13
+ tool_context.state[:lead_name] = name
14
+ tool_context.state[:lead_email] = email
15
+ tool_context.state[:desired_plan] = desired_plan
16
+ tool_context.state[:lead_created_at] = Time.now.iso8601
17
+
18
+ # Check if we have existing customer info from CRM lookup
19
+ if tool_context.state[:customer_id]
20
+ existing_customer = tool_context.state[:customer_name]
21
+ "Lead created for existing customer #{existing_customer} (#{email}) " \
22
+ "interested in upgrading to #{desired_plan} plan. Sales team will contact within 24 hours."
23
+ else
24
+ "Lead created for #{name} (#{email}) interested in #{desired_plan} plan. " \
25
+ "Sales team will contact within 24 hours."
26
+ end
13
27
  end
14
28
  end
15
29
  end
@@ -8,7 +8,7 @@ module ISPSupport
8
8
  description "Look up customer account information by account ID"
9
9
  param :account_id, type: "string", desc: "Customer account ID (e.g., CUST001)"
10
10
 
11
- def perform(_tool_context, account_id:)
11
+ def perform(tool_context, account_id:)
12
12
  data_file = File.join(__dir__, "../data/customers.json")
13
13
  return "Customer database unavailable" unless File.exist?(data_file)
14
14
 
@@ -18,6 +18,18 @@ module ISPSupport
18
18
 
19
19
  return "Customer not found" unless customer
20
20
 
21
+ # Store customer information in shared state for other tools/agents
22
+ tool_context.state[:customer_id] = account_id.upcase
23
+ tool_context.state[:customer_name] = customer["name"]
24
+ tool_context.state[:customer_email] = customer["email"]
25
+ tool_context.state[:customer_phone] = customer["phone"]
26
+ tool_context.state[:customer_address] = customer["address"]
27
+ tool_context.state[:current_plan] = customer["plan"]["name"]
28
+ tool_context.state[:account_status] = customer["account_status"]
29
+ tool_context.state[:plan_price] = customer["plan"]["price"]
30
+ tool_context.state[:next_bill_date] = customer["billing"]["next_bill_date"]
31
+ tool_context.state[:account_balance] = customer["billing"]["balance"]
32
+
21
33
  # Return the entire customer data as JSON for the agent to process
22
34
  customer.to_json
23
35
  rescue StandardError
data/lib/agents/agent.rb CHANGED
@@ -13,11 +13,16 @@
13
13
  # tools: [calculator_tool, weather_tool]
14
14
  # )
15
15
  #
16
- # @example Creating an agent with dynamic instructions
16
+ # @example Creating an agent with dynamic state-aware instructions
17
17
  # agent = Agents::Agent.new(
18
18
  # name: "Support Agent",
19
19
  # instructions: ->(context) {
20
- # "You are supporting user #{context.context[:user_name]}"
20
+ # state = context.context[:state] || {}
21
+ # base = "You are a support agent."
22
+ # if state[:customer_name]
23
+ # base += " Customer: #{state[:customer_name]} (#{state[:customer_id]})"
24
+ # end
25
+ # base
21
26
  # }
22
27
  # )
23
28
  #
@@ -147,18 +152,25 @@ module Agents
147
152
  # instructions: "You are a helpful support agent"
148
153
  # )
149
154
  #
150
- # @example Dynamic instructions based on context
155
+ # @example Dynamic instructions with state awareness
151
156
  # agent = Agent.new(
152
- # name: "Support",
157
+ # name: "Sales Agent",
153
158
  # instructions: ->(context) {
154
- # user = context.context[:user]
155
- # "You are helping #{user.name}. They are a #{user.tier} customer with account #{user.id}"
159
+ # state = context.context[:state] || {}
160
+ # base = "You are a sales agent."
161
+ # if state[:customer_name] && state[:current_plan]
162
+ # base += " Customer: #{state[:customer_name]} on #{state[:current_plan]} plan."
163
+ # end
164
+ # base
156
165
  # }
157
166
  # )
158
167
  #
159
168
  # @param context [Agents::RunContext] The current execution context containing runtime data
160
169
  # @return [String, nil] The system prompt string or nil if no instructions are set
161
170
  def get_system_prompt(context)
171
+ # TODO: Add string interpolation support for instructions
172
+ # Allow instructions like "You are helping %{customer_name}" that automatically
173
+ # get state values injected from context[:state] using Ruby's % formatting
162
174
  case instructions
163
175
  when String
164
176
  instructions
@@ -166,5 +178,39 @@ module Agents
166
178
  instructions.call(context)
167
179
  end
168
180
  end
181
+
182
+ # Transform this agent into a tool, callable by other agents.
183
+ # This enables agent-to-agent collaboration without conversation handoffs.
184
+ #
185
+ # Agent-as-tool is different from handoffs in two key ways:
186
+ # 1. The wrapped agent receives generated input, not conversation history
187
+ # 2. The wrapped agent returns a result to the calling agent, rather than taking over
188
+ #
189
+ # @param name [String, nil] Override the tool name (defaults to snake_case agent name)
190
+ # @param description [String, nil] Override the tool description
191
+ # @param output_extractor [Proc, nil] Custom proc to extract/transform the agent's output
192
+ # @param params [Hash] Additional parameter definitions for the tool
193
+ # @return [Agents::AgentTool] A tool that wraps this agent
194
+ #
195
+ # @example Basic agent-as-tool
196
+ # research_agent = Agent.new(name: "Researcher", instructions: "Research topics")
197
+ # research_tool = research_agent.as_tool(
198
+ # name: "research_topic",
199
+ # description: "Research a topic using company knowledge base"
200
+ # )
201
+ #
202
+ # @example Custom output extraction
203
+ # analyzer_tool = analyzer_agent.as_tool(
204
+ # output_extractor: ->(result) { result.context[:extracted_data]&.to_json || result.output }
205
+ # )
206
+ #
207
+ def as_tool(name: nil, description: nil, output_extractor: nil)
208
+ AgentTool.new(
209
+ agent: self,
210
+ name: name,
211
+ description: description,
212
+ output_extractor: output_extractor
213
+ )
214
+ end
169
215
  end
170
216
  end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Agents
4
+ # AgentTool wraps an agent as a tool, enabling agent-to-agent collaboration
5
+ # without conversation handoffs. This implementation constrains wrapped agents
6
+ # for safety and predictability.
7
+ #
8
+ # Key constraints:
9
+ # 1. Wrapped agents cannot perform handoffs (empty registry)
10
+ # 2. Limited turn count to prevent infinite loops
11
+ # 3. Isolated context (only shared state, no conversation history)
12
+ # 4. Always returns to calling agent
13
+ #
14
+ # @example Customer support copilot with specialized agents
15
+ # conversation_agent = Agent.new(
16
+ # name: "ConversationAnalyzer",
17
+ # instructions: "Extract order IDs and customer intent from conversation history"
18
+ # )
19
+ #
20
+ # actions_agent = Agent.new(
21
+ # name: "ShopifyActions",
22
+ # instructions: "Perform Shopify operations",
23
+ # tools: [shopify_tool]
24
+ # )
25
+ #
26
+ # copilot = Agent.new(
27
+ # name: "SupportCopilot",
28
+ # tools: [
29
+ # conversation_agent.as_tool(
30
+ # name: "analyze_conversation",
31
+ # description: "Extract key info from conversation history"
32
+ # ),
33
+ # actions_agent.as_tool(
34
+ # name: "shopify_action",
35
+ # description: "Perform Shopify operations like refunds"
36
+ # )
37
+ # ]
38
+ # )
39
+ class AgentTool < Tool
40
+ attr_reader :wrapped_agent, :tool_name, :tool_description, :output_extractor
41
+
42
+ # Default parameter for agent tools
43
+ param :input, type: "string", desc: "Input message for the agent"
44
+
45
+ # Initialize an AgentTool that wraps an agent as a callable tool
46
+ #
47
+ # @param agent [Agents::Agent] The agent to wrap as a tool
48
+ # @param name [String, nil] Override the tool name (defaults to snake_case agent name)
49
+ # @param description [String, nil] Override the tool description
50
+ # @param output_extractor [Proc, nil] Custom proc to extract/transform the agent's output
51
+ def initialize(agent:, name: nil, description: nil, output_extractor: nil)
52
+ @wrapped_agent = agent
53
+ @tool_name = name || transform_agent_name(agent.name)
54
+ @tool_description = description || "Execute #{agent.name} agent"
55
+ @output_extractor = output_extractor
56
+
57
+ super()
58
+ end
59
+
60
+ def name
61
+ @tool_name
62
+ end
63
+
64
+ def description
65
+ @tool_description
66
+ end
67
+
68
+ # Execute the wrapped agent with constraints to prevent handoffs and recursion
69
+ def perform(tool_context, input:)
70
+ # Create isolated context for the wrapped agent
71
+ isolated_context = create_isolated_context(tool_context.context)
72
+
73
+ # Execute with explicit constraints:
74
+ # 1. Empty registry prevents handoffs
75
+ # 2. Low max_turns prevents infinite loops
76
+ # 3. Isolated context prevents history access
77
+ result = Runner.new.run(
78
+ @wrapped_agent,
79
+ input,
80
+ context: isolated_context,
81
+ registry: {}, # CONSTRAINT: No handoffs allowed
82
+ max_turns: 3 # CONSTRAINT: Limited turns for tool execution
83
+ )
84
+
85
+ return "Agent execution failed: #{result.error.message}" if result.error
86
+
87
+ # Extract output
88
+ if @output_extractor
89
+ @output_extractor.call(result)
90
+ else
91
+ result.output || "No output from #{@wrapped_agent.name}"
92
+ end
93
+ rescue StandardError => e
94
+ "Error executing #{@wrapped_agent.name}: #{e.message}"
95
+ end
96
+
97
+ private
98
+
99
+ def transform_agent_name(name)
100
+ name.downcase.gsub(/\s+/, "_").gsub(/[^a-z0-9_]/, "")
101
+ end
102
+
103
+ # Create isolated context that only shares state, not conversation artifacts
104
+ def create_isolated_context(parent_context)
105
+ isolated = {}
106
+
107
+ # Only share the state - everything else is conversation-specific
108
+ isolated[:state] = parent_context[:state] if parent_context[:state]
109
+
110
+ isolated
111
+ end
112
+ end
113
+ end
@@ -12,35 +12,10 @@ module Agents
12
12
  # 4. The tool signals the handoff through context
13
13
  # 5. The Runner detects this and switches to the new agent
14
14
  #
15
- # ## First-Call-Wins Implementation
16
- # This implementation uses "first-call-wins" semantics to prevent infinite handoff loops.
17
- #
18
- # ### The Problem We Solved
19
- # During development, we discovered that LLMs could call the same handoff tool multiple times
20
- # in a single response, leading to infinite loops:
21
- #
22
- # 1. User: "My internet isn't working but my account shows active"
23
- # 2. Triage Agent hands off to Support Agent
24
- # 3. Support Agent sees account info is needed, hands back to Triage Agent
25
- # 4. Triage Agent sees technical issue, hands off to Support Agent again
26
- # 5. This creates an infinite ping-pong loop
27
- #
28
- # ### Root Cause Analysis
29
- # Unlike OpenAI's SDK which processes tool calls before execution, RubyLLM automatically
30
- # executes all tool calls in a response. This meant:
31
- # - LLM calls handoff tool 10+ times in one response
32
- # - Each call sets context[:pending_handoff], overwriting previous values
33
- # - Runner processes handoffs after tool execution, seeing only the last one
34
- # - Multiple handoff signals created conflicting state
35
- #
36
- # TODO: Overall, this problem can be tackled better if we replace the RubyLLM chat
37
- # program with our own implementation.
38
- #
39
- # ### The Solution
40
- # We implemented first-call-wins semantics inspired by OpenAI's approach:
41
- # - First handoff call in a response sets the pending handoff
42
- # - Subsequent calls are ignored with a "transfer in progress" message
43
- # - This prevents loops and mirrors OpenAI SDK behavior
15
+ # ## Loop Prevention
16
+ # The library prevents infinite handoff loops by processing only the first handoff
17
+ # tool call in any LLM response. This is handled automatically by the Chat class
18
+ # which detects handoff tools and processes them separately from regular tools.
44
19
  #
45
20
  # ## Why Tools Instead of Instructions
46
21
  # Using tools for handoffs has several advantages:
@@ -66,12 +41,11 @@ module Agents
66
41
  # # LLM calls: handoff_to_billing()
67
42
  # # Runner switches to billing_agent for the next turn
68
43
  #
69
- # @example First-call-wins in action
44
+ # @example Multiple handoff handling
70
45
  # # Single LLM response with multiple handoff calls:
71
- # # Call 1: handoff_to_support() -> Sets pending_handoff, returns "Transferring to Support"
72
- # # Call 2: handoff_to_support() -> Ignored, returns "Transfer already in progress"
73
- # # Call 3: handoff_to_billing() -> Ignored, returns "Transfer already in progress"
74
- # # Result: Only transfers to Support Agent (first call wins)
46
+ # # Call 1: handoff_to_support() -> Processed and executed
47
+ # # Call 2: handoff_to_billing() -> Ignored (only first handoff processed)
48
+ # # Result: Only transfers to Support Agent
75
49
  class HandoffTool < Tool
76
50
  attr_reader :target_agent
77
51
 
@@ -94,5 +94,41 @@ module Agents
94
94
  def usage
95
95
  @run_context.usage
96
96
  end
97
+
98
+ # Convenient access to the shared state hash within the context.
99
+ # This provides tools with a dedicated space to store and retrieve
100
+ # state that persists across agent interactions within a conversation.
101
+ #
102
+ # State is automatically initialized as an empty hash if it doesn't exist.
103
+ # All state modifications are automatically included in context serialization,
104
+ # making it persist across process boundaries (e.g., Rails with ActiveRecord).
105
+ #
106
+ # @return [Hash] The shared state hash
107
+ # @example Tool storing customer information in state
108
+ # def perform(tool_context, customer_id:)
109
+ # customer = Customer.find(customer_id)
110
+ #
111
+ # # Store in shared state for other tools/agents to access
112
+ # tool_context.state[:customer_id] = customer_id
113
+ # tool_context.state[:customer_name] = customer.name
114
+ # tool_context.state[:plan_type] = customer.plan
115
+ #
116
+ # "Found customer #{customer.name}"
117
+ # end
118
+ #
119
+ # @example Tool reading from shared state
120
+ # def perform(tool_context)
121
+ # customer_id = tool_context.state[:customer_id]
122
+ # plan_type = tool_context.state[:plan_type]
123
+ #
124
+ # if customer_id && plan_type
125
+ # "Current plan: #{plan_type} for customer #{customer_id}"
126
+ # else
127
+ # "No customer information available"
128
+ # end
129
+ # end
130
+ def state
131
+ context[:state] ||= {}
132
+ end
97
133
  end
98
134
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Agents
4
- VERSION = "0.1.1"
4
+ VERSION = "0.1.3"
5
5
  end
data/lib/agents.rb CHANGED
@@ -82,3 +82,4 @@ require_relative "agents/chat"
82
82
  require_relative "agents/tool_wrapper"
83
83
  require_relative "agents/agent_runner"
84
84
  require_relative "agents/runner"
85
+ 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.1
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shivam Mishra
@@ -37,7 +37,48 @@ files:
37
37
  - LICENSE
38
38
  - README.md
39
39
  - Rakefile
40
+ - docs/Gemfile
41
+ - docs/Gemfile.lock
42
+ - docs/_config.yml
43
+ - docs/_sass/color_schemes/ruby.scss
44
+ - docs/_sass/custom/custom.scss
45
+ - docs/architecture.md
46
+ - docs/assets/fonts/InterVariable.woff2
47
+ - docs/concepts.md
48
+ - docs/concepts/agent-tool.md
49
+ - docs/concepts/agents.md
50
+ - docs/concepts/context.md
51
+ - docs/concepts/handoffs.md
52
+ - docs/concepts/runner.md
53
+ - docs/concepts/tools.md
54
+ - docs/guides.md
55
+ - docs/guides/agent-as-tool-pattern.md
56
+ - docs/guides/multi-agent-systems.md
57
+ - docs/guides/rails-integration.md
58
+ - docs/guides/state-persistence.md
59
+ - docs/index.md
40
60
  - examples/README.md
61
+ - examples/collaborative-copilot/README.md
62
+ - examples/collaborative-copilot/agents/analysis_agent.rb
63
+ - examples/collaborative-copilot/agents/answer_suggestion_agent.rb
64
+ - examples/collaborative-copilot/agents/copilot_orchestrator.rb
65
+ - examples/collaborative-copilot/agents/integrations_agent.rb
66
+ - examples/collaborative-copilot/agents/research_agent.rb
67
+ - examples/collaborative-copilot/data/contacts.json
68
+ - examples/collaborative-copilot/data/conversations.json
69
+ - examples/collaborative-copilot/data/knowledge_base.json
70
+ - examples/collaborative-copilot/data/linear_issues.json
71
+ - examples/collaborative-copilot/data/stripe_billing.json
72
+ - examples/collaborative-copilot/interactive.rb
73
+ - examples/collaborative-copilot/tools/create_linear_ticket_tool.rb
74
+ - examples/collaborative-copilot/tools/get_article_tool.rb
75
+ - examples/collaborative-copilot/tools/get_contact_tool.rb
76
+ - examples/collaborative-copilot/tools/get_conversation_tool.rb
77
+ - examples/collaborative-copilot/tools/get_stripe_billing_tool.rb
78
+ - examples/collaborative-copilot/tools/search_contacts_tool.rb
79
+ - examples/collaborative-copilot/tools/search_conversations_tool.rb
80
+ - examples/collaborative-copilot/tools/search_knowledge_base_tool.rb
81
+ - examples/collaborative-copilot/tools/search_linear_issues_tool.rb
41
82
  - examples/isp-support/README.md
42
83
  - examples/isp-support/agents_factory.rb
43
84
  - examples/isp-support/data/customers.json
@@ -52,6 +93,7 @@ files:
52
93
  - lib/agents.rb
53
94
  - lib/agents/agent.rb
54
95
  - lib/agents/agent_runner.rb
96
+ - lib/agents/agent_tool.rb
55
97
  - lib/agents/chat.rb
56
98
  - lib/agents/handoff.rb
57
99
  - lib/agents/result.rb
@@ -75,7 +117,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
75
117
  requirements:
76
118
  - - ">="
77
119
  - !ruby/object:Gem::Version
78
- version: 3.1.0
120
+ version: 3.2.0
79
121
  required_rubygems_version: !ruby/object:Gem::Requirement
80
122
  requirements:
81
123
  - - ">="