ai-agents 0.1.2 → 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/README.md +29 -106
- data/docs/Gemfile +14 -0
- data/docs/Gemfile.lock +183 -0
- data/docs/_config.yml +53 -0
- data/docs/_sass/color_schemes/ruby.scss +72 -0
- data/docs/_sass/custom/custom.scss +93 -0
- data/docs/architecture.md +353 -0
- data/docs/assets/fonts/InterVariable.woff2 +0 -0
- data/docs/concepts/agent-tool.md +166 -0
- data/docs/concepts/agents.md +43 -0
- data/docs/concepts/callbacks.md +42 -0
- data/docs/concepts/context.md +110 -0
- data/docs/concepts/handoffs.md +81 -0
- data/docs/concepts/runner.md +87 -0
- data/docs/concepts/tools.md +62 -0
- data/docs/concepts.md +22 -0
- data/docs/guides/agent-as-tool-pattern.md +242 -0
- data/docs/guides/multi-agent-systems.md +261 -0
- data/docs/guides/rails-integration.md +440 -0
- data/docs/guides/state-persistence.md +451 -0
- data/docs/guides.md +18 -0
- data/docs/index.md +97 -0
- data/examples/collaborative-copilot/README.md +169 -0
- data/examples/collaborative-copilot/agents/analysis_agent.rb +48 -0
- data/examples/collaborative-copilot/agents/answer_suggestion_agent.rb +50 -0
- data/examples/collaborative-copilot/agents/copilot_orchestrator.rb +85 -0
- data/examples/collaborative-copilot/agents/integrations_agent.rb +58 -0
- data/examples/collaborative-copilot/agents/research_agent.rb +52 -0
- data/examples/collaborative-copilot/data/contacts.json +47 -0
- data/examples/collaborative-copilot/data/conversations.json +170 -0
- data/examples/collaborative-copilot/data/knowledge_base.json +58 -0
- data/examples/collaborative-copilot/data/linear_issues.json +83 -0
- data/examples/collaborative-copilot/data/stripe_billing.json +71 -0
- data/examples/collaborative-copilot/interactive.rb +90 -0
- data/examples/collaborative-copilot/tools/create_linear_ticket_tool.rb +58 -0
- data/examples/collaborative-copilot/tools/get_article_tool.rb +41 -0
- data/examples/collaborative-copilot/tools/get_contact_tool.rb +51 -0
- data/examples/collaborative-copilot/tools/get_conversation_tool.rb +53 -0
- data/examples/collaborative-copilot/tools/get_stripe_billing_tool.rb +44 -0
- data/examples/collaborative-copilot/tools/search_contacts_tool.rb +57 -0
- data/examples/collaborative-copilot/tools/search_conversations_tool.rb +54 -0
- data/examples/collaborative-copilot/tools/search_knowledge_base_tool.rb +55 -0
- data/examples/collaborative-copilot/tools/search_linear_issues_tool.rb +60 -0
- data/examples/isp-support/interactive.rb +43 -4
- data/lib/agents/agent.rb +34 -0
- data/lib/agents/agent_runner.rb +66 -1
- data/lib/agents/agent_tool.rb +113 -0
- data/lib/agents/callback_manager.rb +54 -0
- data/lib/agents/handoff.rb +8 -34
- 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 +3 -0
- metadata +48 -1
@@ -0,0 +1,166 @@
|
|
1
|
+
---
|
2
|
+
layout: default
|
3
|
+
title: AgentTool
|
4
|
+
parent: Concepts
|
5
|
+
nav_order: 6
|
6
|
+
---
|
7
|
+
|
8
|
+
# AgentTool
|
9
|
+
|
10
|
+
The `AgentTool` class enables **agent-to-agent collaboration** by wrapping agents as callable tools. This pattern allows specialized agents to work behind the scenes to help each other without conversation handoffs.
|
11
|
+
|
12
|
+
## Key Concept
|
13
|
+
|
14
|
+
Unlike handoffs where control transfers between agents with full conversation context, AgentTool creates **constrained execution environments** where wrapped agents:
|
15
|
+
|
16
|
+
- Cannot perform handoffs (empty registry)
|
17
|
+
- Have limited turn counts to prevent infinite loops
|
18
|
+
- Only receive shared state, not conversation history
|
19
|
+
- Always return control to the calling agent
|
20
|
+
|
21
|
+
## Architecture
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
# Specialized agents as tools
|
25
|
+
research_agent = Agent.new(
|
26
|
+
name: "Research Agent",
|
27
|
+
instructions: "Research topics and extract key information"
|
28
|
+
)
|
29
|
+
|
30
|
+
analysis_agent = Agent.new(
|
31
|
+
name: "Analysis Agent",
|
32
|
+
instructions: "Analyze data and provide insights"
|
33
|
+
)
|
34
|
+
|
35
|
+
# Main orchestrator uses other agents as tools
|
36
|
+
orchestrator = Agent.new(
|
37
|
+
name: "Orchestrator",
|
38
|
+
instructions: "Coordinate research and analysis",
|
39
|
+
tools: [
|
40
|
+
research_agent.as_tool(
|
41
|
+
name: "research_topic",
|
42
|
+
description: "Research a specific topic"
|
43
|
+
),
|
44
|
+
analysis_agent.as_tool(
|
45
|
+
name: "analyze_data",
|
46
|
+
description: "Analyze research findings"
|
47
|
+
)
|
48
|
+
]
|
49
|
+
)
|
50
|
+
```
|
51
|
+
|
52
|
+
## Implementation Details
|
53
|
+
|
54
|
+
### Constraints
|
55
|
+
|
56
|
+
AgentTool implements several safety constraints:
|
57
|
+
|
58
|
+
1. **No Handoffs**: Wrapped agents receive an empty registry, preventing handoff calls
|
59
|
+
2. **Limited Turns**: Maximum 3 turns to prevent infinite loops
|
60
|
+
3. **Context Isolation**: Only shared state is passed, not conversation history
|
61
|
+
4. **Return Control**: Always returns to the calling agent
|
62
|
+
|
63
|
+
### Context Isolation
|
64
|
+
|
65
|
+
```ruby
|
66
|
+
# Parent context (full conversation state)
|
67
|
+
parent_context = {
|
68
|
+
state: { user_id: 123, session: "abc" },
|
69
|
+
conversation_history: [...],
|
70
|
+
current_agent: "MainAgent",
|
71
|
+
turn_count: 5
|
72
|
+
}
|
73
|
+
|
74
|
+
# Isolated context (only state)
|
75
|
+
isolated_context = {
|
76
|
+
state: { user_id: 123, session: "abc" }
|
77
|
+
}
|
78
|
+
```
|
79
|
+
|
80
|
+
### Error Handling
|
81
|
+
|
82
|
+
AgentTool provides robust error handling:
|
83
|
+
|
84
|
+
- Execution failures return descriptive error messages
|
85
|
+
- Runtime exceptions are caught and reported
|
86
|
+
- Missing output is handled gracefully
|
87
|
+
|
88
|
+
## Usage Patterns
|
89
|
+
|
90
|
+
### Customer Support Copilot
|
91
|
+
|
92
|
+
```ruby
|
93
|
+
# Specialized agents for different domains
|
94
|
+
conversation_analyzer = Agent.new(
|
95
|
+
name: "ConversationAnalyzer",
|
96
|
+
instructions: "Extract order IDs and customer intent"
|
97
|
+
)
|
98
|
+
|
99
|
+
shopify_agent = Agent.new(
|
100
|
+
name: "ShopifyAgent",
|
101
|
+
instructions: "Perform Shopify operations",
|
102
|
+
tools: [shopify_refund_tool, shopify_lookup_tool]
|
103
|
+
)
|
104
|
+
|
105
|
+
# Main copilot coordinates specialized agents
|
106
|
+
copilot = Agent.new(
|
107
|
+
name: "SupportCopilot",
|
108
|
+
instructions: "Help support agents with customer requests",
|
109
|
+
tools: [
|
110
|
+
conversation_analyzer.as_tool(
|
111
|
+
name: "analyze_conversation",
|
112
|
+
description: "Extract key information from conversation"
|
113
|
+
),
|
114
|
+
shopify_agent.as_tool(
|
115
|
+
name: "shopify_action",
|
116
|
+
description: "Perform Shopify operations"
|
117
|
+
)
|
118
|
+
]
|
119
|
+
)
|
120
|
+
```
|
121
|
+
|
122
|
+
## Output Transformation
|
123
|
+
|
124
|
+
AgentTool supports custom output extractors for transforming results:
|
125
|
+
|
126
|
+
```ruby
|
127
|
+
# Custom output extraction
|
128
|
+
summarizer = Agent.new(
|
129
|
+
name: "Summarizer",
|
130
|
+
instructions: "Summarize long content"
|
131
|
+
)
|
132
|
+
|
133
|
+
summarizer_tool = summarizer.as_tool(
|
134
|
+
name: "summarize",
|
135
|
+
description: "Create a summary",
|
136
|
+
output_extractor: ->(result) {
|
137
|
+
"SUMMARY: #{result.output&.first(200)}..."
|
138
|
+
}
|
139
|
+
)
|
140
|
+
```
|
141
|
+
|
142
|
+
## Best Practices
|
143
|
+
|
144
|
+
1. **Keep it Simple**: Use AgentTool for focused, single-purpose tasks
|
145
|
+
2. **Avoid Deep Nesting**: Don't create agents that use agents that use agents
|
146
|
+
3. **State Management**: Only share necessary state between agents
|
147
|
+
4. **Error Handling**: Always handle potential execution failures
|
148
|
+
5. **Performance**: Consider the overhead of multiple agent calls
|
149
|
+
|
150
|
+
## Comparison with Handoffs
|
151
|
+
|
152
|
+
| Aspect | AgentTool | Handoff |
|
153
|
+
|--------|-----------|---------|
|
154
|
+
| **Purpose** | Behind-the-scenes collaboration | User-facing conversation transfer |
|
155
|
+
| **Context** | Isolated (state only) | Full conversation history |
|
156
|
+
| **Control** | Returns to caller | Transfers to target |
|
157
|
+
| **Constraints** | Limited turns, no handoffs | Full agent capabilities |
|
158
|
+
| **Use Case** | Internal processing | Conversation routing |
|
159
|
+
|
160
|
+
## See Also
|
161
|
+
|
162
|
+
<!-- - [Agent-as-Tool Pattern Guide](../guides/agent-as-tool-pattern.html)
|
163
|
+
- [Multi-Agent Systems](../guides/multi-agent-systems.html) -->
|
164
|
+
|
165
|
+
- [Tools Concept](tools.html)
|
166
|
+
- [Handoffs Concept](handoffs.html)
|
@@ -0,0 +1,43 @@
|
|
1
|
+
---
|
2
|
+
layout: default
|
3
|
+
title: Agents
|
4
|
+
parent: Concepts
|
5
|
+
nav_order: 1
|
6
|
+
---
|
7
|
+
|
8
|
+
# Agents
|
9
|
+
|
10
|
+
An **Agent** is the fundamental building block of the library. It represents an AI assistant with a specific set of capabilities, defined by its instructions, tools, and the underlying language model it uses.
|
11
|
+
|
12
|
+
Agents are immutable and thread-safe by design. Once created, their configuration cannot be changed, ensuring safe sharing across multiple threads without race conditions. Agents can have dynamic instructions using Proc objects that receive runtime context.
|
13
|
+
|
14
|
+
{: .note }
|
15
|
+
This project is in early development. While thread safety is a core design goal and the architecture is built around it, complete thread safety is not yet guaranteed. We're actively working toward this goal.
|
16
|
+
|
17
|
+
### Key Attributes of an Agent
|
18
|
+
|
19
|
+
* **`name`**: A unique name for the agent, used for identification and in handoffs.
|
20
|
+
* **`instructions`**: The system prompt that guides the agent's behavior. This can be a static string or a `Proc` that dynamically generates instructions based on the current context.
|
21
|
+
* **`model`**: The language model the agent will use (e.g., `"gpt-4.1-mini"`).
|
22
|
+
* **`tools`**: An array of `Agents::Tool` instances that the agent can use to perform actions.
|
23
|
+
* **`handoff_agents`**: An array of other agents that this agent can hand off conversations to.
|
24
|
+
|
25
|
+
### Example
|
26
|
+
|
27
|
+
```ruby
|
28
|
+
# Create a simple agent
|
29
|
+
assistant_agent = Agents::Agent.new(
|
30
|
+
name: "Assistant",
|
31
|
+
instructions: "You are a helpful assistant.",
|
32
|
+
model: "gpt-4.1-mini",
|
33
|
+
tools: [CalculatorTool.new]
|
34
|
+
)
|
35
|
+
|
36
|
+
# Create a specialized agent by cloning the base agent
|
37
|
+
specialized_agent = assistant_agent.clone(
|
38
|
+
instructions: "You are a specialized assistant for financial calculations.",
|
39
|
+
tools: assistant_agent.tools + [FinancialDataTool.new]
|
40
|
+
)
|
41
|
+
```
|
42
|
+
|
43
|
+
In this example, we create a base `assistant_agent` and then create a `specialized_agent` by cloning it and adding a new tool. This approach allows for easy composition and reuse of agent configurations.
|
@@ -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.
|
@@ -0,0 +1,110 @@
|
|
1
|
+
---
|
2
|
+
layout: default
|
3
|
+
title: Context
|
4
|
+
parent: Concepts
|
5
|
+
nav_order: 3
|
6
|
+
---
|
7
|
+
|
8
|
+
# Context
|
9
|
+
|
10
|
+
**Context** is the serializable state management system that preserves information across agent interactions, tool executions, and handoffs. Context enables conversation continuity and cross-session persistence through a simple hash-based structure.
|
11
|
+
|
12
|
+
## Context Architecture
|
13
|
+
|
14
|
+
Context flows through multiple abstraction layers:
|
15
|
+
|
16
|
+
### User Context (Serializable)
|
17
|
+
The main context hash that persists across sessions:
|
18
|
+
```ruby
|
19
|
+
context = {
|
20
|
+
user_id: 123,
|
21
|
+
conversation_history: [...],
|
22
|
+
current_agent_name: "Billing",
|
23
|
+
state: { customer_tier: "premium" } # Tools use this nested hash for persistent data
|
24
|
+
}
|
25
|
+
```
|
26
|
+
|
27
|
+
### RunContext (Execution Wrapper)
|
28
|
+
Wraps user context with execution-specific features:
|
29
|
+
- **Context Hash**: Shared data accessible to all tools and agents
|
30
|
+
- **Thread Safety**: Deep copying ensures execution isolation
|
31
|
+
|
32
|
+
### ToolContext (Tool-Specific View)
|
33
|
+
Provides tools with controlled access to execution state:
|
34
|
+
- **Context Access**: Read/write access to shared context hash
|
35
|
+
- **State Management**: Dedicated space for persistent tool state
|
36
|
+
|
37
|
+
## The `ToolContext`
|
38
|
+
|
39
|
+
The `ToolContext` is a wrapper around the `RunContext` that is passed to each tool when it is executed. It provides the tool with controlled access to the execution state, including the shared context hash.
|
40
|
+
|
41
|
+
By passing the context through the `ToolContext`, we ensure that tools can remain stateless and thread-safe, as they do not need to store any execution-specific state in their instance variables.
|
42
|
+
|
43
|
+
## Context Serialization
|
44
|
+
|
45
|
+
Context is fully serializable for persistence across process boundaries:
|
46
|
+
|
47
|
+
```ruby
|
48
|
+
# Run conversation
|
49
|
+
result = runner.run("Hello, I'm John")
|
50
|
+
|
51
|
+
# Serialize for storage
|
52
|
+
context_json = result.context.to_json
|
53
|
+
# Store in database, file, session, etc.
|
54
|
+
|
55
|
+
# Later: restore and continue
|
56
|
+
restored_context = JSON.parse(context_json, symbolize_names: true)
|
57
|
+
next_result = runner.run("What's my name?", context: restored_context)
|
58
|
+
# => "Your name is John"
|
59
|
+
```
|
60
|
+
|
61
|
+
## Conversation Continuity
|
62
|
+
|
63
|
+
The AgentRunner automatically manages conversation continuity through context:
|
64
|
+
|
65
|
+
```ruby
|
66
|
+
# Create runner
|
67
|
+
runner = Agents::Runner.with_agents(triage_agent, billing_agent)
|
68
|
+
|
69
|
+
# First interaction
|
70
|
+
result1 = runner.run("I need billing help")
|
71
|
+
# Triage agent hands off to billing agent
|
72
|
+
# Context includes: current_agent_name: "Billing"
|
73
|
+
|
74
|
+
# Continue conversation
|
75
|
+
result2 = runner.run("What payment methods do you accept?", context: result1.context)
|
76
|
+
# AgentRunner detects billing agent should continue based on context
|
77
|
+
```
|
78
|
+
|
79
|
+
## State Management
|
80
|
+
|
81
|
+
Tools can use the context for persistent state:
|
82
|
+
|
83
|
+
```ruby
|
84
|
+
class CustomerLookupTool < Agents::Tool
|
85
|
+
def perform(tool_context, customer_id:)
|
86
|
+
customer = Customer.find(customer_id)
|
87
|
+
|
88
|
+
# Store in shared state for other tools
|
89
|
+
tool_context.state[:customer_id] = customer_id
|
90
|
+
tool_context.state[:customer_name] = customer.name
|
91
|
+
tool_context.state[:account_type] = customer.account_type
|
92
|
+
|
93
|
+
"Found customer: #{customer.name}"
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
class BillingTool < Agents::Tool
|
98
|
+
def perform(tool_context)
|
99
|
+
# Access state from previous tool
|
100
|
+
customer_id = tool_context.state[:customer_id]
|
101
|
+
account_type = tool_context.state[:account_type]
|
102
|
+
|
103
|
+
return "No customer found" unless customer_id
|
104
|
+
|
105
|
+
# Use customer info for billing operations
|
106
|
+
billing_info = get_billing_info(customer_id, account_type)
|
107
|
+
billing_info.to_s
|
108
|
+
end
|
109
|
+
end
|
110
|
+
```
|
@@ -0,0 +1,81 @@
|
|
1
|
+
---
|
2
|
+
layout: default
|
3
|
+
title: Handoffs
|
4
|
+
parent: Concepts
|
5
|
+
nav_order: 4
|
6
|
+
---
|
7
|
+
|
8
|
+
# Handoffs
|
9
|
+
|
10
|
+
**Handoffs** are a powerful feature of the Ruby Agents library that allow you to build sophisticated multi-agent systems. A handoff is the process of transferring a conversation from one agent to another, more specialized agent.
|
11
|
+
|
12
|
+
This is particularly useful when you have a general-purpose agent that can handle a wide range of queries, but you also have specialized agents that are better equipped to handle specific tasks. For example, you might have a triage agent that routes users to a billing agent or a technical support agent.
|
13
|
+
|
14
|
+
## How Handoffs Work
|
15
|
+
|
16
|
+
Handoffs are implemented as a special type of tool called a `HandoffTool`. When you configure an agent with `handoff_agents`, the library automatically creates a `HandoffTool` for each of the specified agents.
|
17
|
+
|
18
|
+
Here's how the handoff process works:
|
19
|
+
|
20
|
+
1. **The user sends a message:** The user sends a message that indicates they need help with a specific task (e.g., "I have a problem with my bill").
|
21
|
+
2. **The LLM decides to hand off:** The current agent's language model determines that the query is best handled by another agent and decides to call the corresponding `HandoffTool`.
|
22
|
+
3. **The `HandoffTool` signals the handoff:** The `HandoffTool` sets a `pending_handoff` flag in the `RunContext`, indicating which agent to hand off to.
|
23
|
+
4. **The Runner switches agents:** The `Runner` detects the `pending_handoff` flag and switches the `current_agent` to the new agent.
|
24
|
+
5. **The conversation continues:** The conversation continues with the new agent, which now has access to the full conversation history.
|
25
|
+
|
26
|
+
### Loop Prevention
|
27
|
+
|
28
|
+
To prevent infinite handoff loops, the library automatically processes only the first handoff tool call in any LLM response. If multiple handoff tools are called in a single response, only the first one is executed and subsequent calls are ignored. This prevents conflicting handoff states and ensures clean agent transitions.
|
29
|
+
|
30
|
+
## Why Use Tools for Handoffs?
|
31
|
+
|
32
|
+
Using tools for handoffs has several advantages over simply instructing the LLM to hand off the conversation:
|
33
|
+
|
34
|
+
* **Reliability:** LLMs are very good at using tools when they are available. By representing handoffs as tools, we can be more confident that the LLM will use them when appropriate.
|
35
|
+
* **Clarity:** The tool's schema clearly defines when each handoff is suitable, making it easier for the LLM to make the right decision.
|
36
|
+
* **Simplicity:** We don't need to parse free-text responses from the LLM to determine if a handoff is needed.
|
37
|
+
* **Consistency:** This approach works consistently across different LLM providers.
|
38
|
+
|
39
|
+
## Example
|
40
|
+
|
41
|
+
```ruby
|
42
|
+
# Create the specialized agents
|
43
|
+
billing_agent = Agents::Agent.new(name: "Billing", instructions: "Handle billing and payment issues.")
|
44
|
+
support_agent = Agents::Agent.new(name: "Support", instructions: "Provide technical support.")
|
45
|
+
|
46
|
+
# Create the triage agent with handoff agents
|
47
|
+
triage_agent = Agents::Agent.new(
|
48
|
+
name: "Triage",
|
49
|
+
instructions: "You are a triage agent. Your job is to route users to the correct department.",
|
50
|
+
handoff_agents: [billing_agent, support_agent]
|
51
|
+
)
|
52
|
+
|
53
|
+
# Run the triage agent
|
54
|
+
result = Agents::Runner.run(triage_agent, "I have a problem with my bill.")
|
55
|
+
|
56
|
+
# The runner will automatically hand off to the billing agent
|
57
|
+
```
|
58
|
+
|
59
|
+
In this example, the `triage_agent` will automatically hand off the conversation to the `billing_agent` when the user asks a question about their bill. This allows you to create a seamless user experience where the user is always talking to the most qualified agent for their needs.
|
60
|
+
|
61
|
+
## Troubleshooting Handoffs
|
62
|
+
|
63
|
+
### Infinite Handoff Loops
|
64
|
+
|
65
|
+
**Problem:** Agents keep handing off to each other in an endless loop.
|
66
|
+
|
67
|
+
**Common Causes:**
|
68
|
+
- Agent instructions that conflict with each other
|
69
|
+
- Agents configured to hand off for overlapping scenarios
|
70
|
+
- Poor instruction clarity about when to hand off vs. when to handle directly
|
71
|
+
|
72
|
+
**Solutions:**
|
73
|
+
1. **Review agent instructions:** Ensure each agent has a clear, distinct responsibility
|
74
|
+
2. **Use hub-and-spoke pattern:** Have specialized agents only hand off back to a central triage agent
|
75
|
+
3. **Add specific scenarios:** Include examples in instructions of when to handle vs. hand off
|
76
|
+
4. **Enable debug logging:** Use `ENV["RUBYLLM_DEBUG"] = "true"` to see handoff decisions
|
77
|
+
|
78
|
+
|
79
|
+
### Multiple Handoffs in One Response
|
80
|
+
|
81
|
+
The library automatically handles cases where an LLM tries to call multiple handoff tools in a single response. Only the first handoff will be processed, and subsequent calls will be ignored. This is normal behavior and prevents conflicting handoff states.
|
@@ -0,0 +1,87 @@
|
|
1
|
+
---
|
2
|
+
layout: default
|
3
|
+
title: Runner
|
4
|
+
parent: Concepts
|
5
|
+
nav_order: 5
|
6
|
+
---
|
7
|
+
|
8
|
+
# AgentRunner
|
9
|
+
|
10
|
+
The **AgentRunner** is the thread-safe execution manager that provides the main API for multi-agent conversations. It separates agent registry management from execution, enabling safe concurrent use across multiple threads while maintaining conversation continuity.
|
11
|
+
|
12
|
+
## Two-Tier Architecture
|
13
|
+
|
14
|
+
The library uses a two-tier design separating long-lived configuration from short-lived execution:
|
15
|
+
|
16
|
+
### AgentRunner (Thread-Safe Manager)
|
17
|
+
- Created once at application startup
|
18
|
+
- Maintains immutable agent registry
|
19
|
+
- Determines conversation continuity from history
|
20
|
+
- Thread-safe for concurrent conversations
|
21
|
+
|
22
|
+
### Runner (Internal Execution Engine)
|
23
|
+
- Created per conversation turn
|
24
|
+
- Handles LLM communication and tool execution
|
25
|
+
- Manages context state during execution
|
26
|
+
- Stateless and garbage-collected after use
|
27
|
+
|
28
|
+
## Conversation Flow
|
29
|
+
|
30
|
+
Each conversation follows this flow:
|
31
|
+
|
32
|
+
1. **Agent Selection**: AgentRunner determines current agent from conversation history
|
33
|
+
2. **Context Isolation**: Creates deep copy of context for thread safety
|
34
|
+
3. **LLM Communication**: Sends message with context to language model
|
35
|
+
4. **Tool Execution**: Executes any requested tools through RubyLLM
|
36
|
+
5. **Handoff Detection**: Checks for agent handoffs and switches if needed
|
37
|
+
6. **State Persistence**: Updates context with conversation state
|
38
|
+
|
39
|
+
## Thread Safety
|
40
|
+
|
41
|
+
The AgentRunner ensures thread safety through several key mechanisms:
|
42
|
+
|
43
|
+
* **Immutable Registry**: Agent registry is frozen after initialization, preventing runtime modifications
|
44
|
+
* **Context Isolation**: Each execution receives a deep copy of context to prevent cross-contamination
|
45
|
+
* **Stateless Execution**: Internal Runner instances store no execution-specific state
|
46
|
+
* **Tool Wrapping**: ToolWrapper injects context through parameters, keeping tools stateless
|
47
|
+
|
48
|
+
## Usage Pattern
|
49
|
+
|
50
|
+
Create an AgentRunner once and reuse it for multiple conversations:
|
51
|
+
|
52
|
+
```ruby
|
53
|
+
# Create agents
|
54
|
+
triage_agent = Agents::Agent.new(
|
55
|
+
name: "Triage",
|
56
|
+
instructions: "Route users to appropriate specialists"
|
57
|
+
)
|
58
|
+
billing_agent = Agents::Agent.new(
|
59
|
+
name: "Billing",
|
60
|
+
instructions: "Handle billing inquiries"
|
61
|
+
)
|
62
|
+
|
63
|
+
# Register handoffs
|
64
|
+
triage_agent.register_handoffs(billing_agent)
|
65
|
+
|
66
|
+
# Create runner once (thread-safe)
|
67
|
+
runner = Agents::Runner.with_agents(triage_agent, billing_agent)
|
68
|
+
|
69
|
+
# Use from multiple threads safely
|
70
|
+
result1 = runner.run("I have a billing question")
|
71
|
+
result2 = runner.run("Follow up", context: result1.context)
|
72
|
+
```
|
73
|
+
|
74
|
+
## Conversation Continuity
|
75
|
+
|
76
|
+
The AgentRunner automatically maintains conversation continuity by analyzing message history to determine which agent should handle each turn:
|
77
|
+
|
78
|
+
```ruby
|
79
|
+
# First message -> Uses triage agent (default entry point)
|
80
|
+
result1 = runner.run("I need help with my bill")
|
81
|
+
|
82
|
+
# Triage hands off to billing agent
|
83
|
+
# Next message -> AgentRunner detects billing agent should continue
|
84
|
+
result2 = runner.run("What payment methods do you accept?", context: result1.context)
|
85
|
+
```
|
86
|
+
|
87
|
+
The `run` method returns a `RunResult` with output, conversation history, usage metrics, and updated context.
|
@@ -0,0 +1,62 @@
|
|
1
|
+
---
|
2
|
+
layout: default
|
3
|
+
title: Tools
|
4
|
+
parent: Concepts
|
5
|
+
nav_order: 2
|
6
|
+
---
|
7
|
+
|
8
|
+
# Tools
|
9
|
+
|
10
|
+
**Tools** are the components that allow agents to interact with the outside world. They are the primary way to extend an agent's capabilities beyond what the language model can do on its own. A tool can be anything from a simple calculator to a complex integration with an external API.
|
11
|
+
|
12
|
+
In Ruby Agents, tools are designed to be thread-safe and stateless. This is a critical design principle that ensures the stability and reliability of your agent system, especially in concurrent environments.
|
13
|
+
|
14
|
+
## Thread-Safe Design
|
15
|
+
|
16
|
+
The key to the thread-safe design of tools is that they do not store any execution-specific state in their instance variables. All the data a tool needs to perform its action is passed to it through the `perform` method, which receives a `ToolContext` object.
|
17
|
+
|
18
|
+
### The `ToolContext`
|
19
|
+
|
20
|
+
The `ToolContext` provides access to the current execution context, including:
|
21
|
+
|
22
|
+
* **Shared context data:** A hash of data that is shared across all tools and agents in a given run.
|
23
|
+
* **Usage tracking:** An object that tracks token usage for the current run.
|
24
|
+
* **Retry count:** The number of times the current tool execution has been retried.
|
25
|
+
|
26
|
+
By passing all the necessary data through the `ToolContext`, we ensure that tool instances can be safely shared across multiple threads without the risk of data corruption.
|
27
|
+
|
28
|
+
## Creating a Tool
|
29
|
+
|
30
|
+
You can create a tool in two ways:
|
31
|
+
|
32
|
+
1. **Creating a Tool Class:** For more complex tools, you can create a class that inherits from `Agents::Tool` and implements the `perform` method.
|
33
|
+
|
34
|
+
```ruby
|
35
|
+
class WeatherTool < Agents::Tool
|
36
|
+
name "get_weather"
|
37
|
+
description "Get the current weather for a location."
|
38
|
+
param :location, type: "string", desc: "The city and state, e.g., San Francisco, CA"
|
39
|
+
|
40
|
+
def perform(tool_context, location:)
|
41
|
+
# Access the API key from the shared context
|
42
|
+
api_key = tool_context.context[:weather_api_key]
|
43
|
+
|
44
|
+
# Call the weather API and return the result
|
45
|
+
WeatherApi.get(location, api_key)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
```
|
49
|
+
|
50
|
+
2. **Using the Functional Tool Definition:** For simpler tools, you can use the `Agents::Tool.tool` helper to define a tool with a block.
|
51
|
+
|
52
|
+
```ruby
|
53
|
+
calculator_tool = Agents::Tool.tool(
|
54
|
+
"calculate",
|
55
|
+
description: "Perform a mathematical calculation."
|
56
|
+
) do |tool_context, expression:|
|
57
|
+
# Perform the calculation and return the result
|
58
|
+
eval(expression).to_s
|
59
|
+
end
|
60
|
+
```
|
61
|
+
|
62
|
+
In both cases, the `perform` method receives the `tool_context` and the tool's parameters as arguments. This design ensures that your tools are always thread-safe and easy to test.
|
data/docs/concepts.md
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
---
|
2
|
+
layout: default
|
3
|
+
title: Concepts
|
4
|
+
nav_order: 2
|
5
|
+
has_children: true
|
6
|
+
---
|
7
|
+
|
8
|
+
# Concepts
|
9
|
+
|
10
|
+
This section covers the core concepts of the AI Agents library. Understanding these concepts is essential for building robust and scalable AI agent systems.
|
11
|
+
|
12
|
+
## Overview
|
13
|
+
|
14
|
+
The AI Agents library is built around several key concepts that work together to provide a powerful framework for multi-agent AI workflows:
|
15
|
+
|
16
|
+
- **[Agents](concepts/agents.html)** - Immutable, thread-safe AI assistants with specific roles and capabilities
|
17
|
+
- **[AgentRunner](concepts/runner.html)** - Thread-safe execution manager for multi-agent conversations
|
18
|
+
- **[Context](concepts/context.html)** - Serializable state management that persists across agent interactions
|
19
|
+
- **[Handoffs](concepts/handoffs.html)** - Tool-based mechanism for seamless agent transitions
|
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
|
22
|
+
- **[AgentTool](concepts/agent-tool.html)** - Agent-to-agent collaboration without conversation handoffs
|