ai-agents 0.11.0 → 0.13.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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +11 -0
  3. data/CLAUDE.md +1 -1
  4. data/README.md +3 -3
  5. data/docs/architecture.md +1 -1
  6. data/docs/concepts/agents.md +16 -0
  7. data/docs/concepts/tools.md +1 -1
  8. data/docs/guides/instrumentation.md +54 -5
  9. data/docs/guides/rails-integration.md +2 -2
  10. data/docs/guides/state-persistence.md +2 -2
  11. data/docs/guides/structured-output.md +3 -3
  12. data/examples/collaborative-copilot/tools/create_linear_ticket_tool.rb +5 -5
  13. data/examples/collaborative-copilot/tools/get_article_tool.rb +1 -1
  14. data/examples/collaborative-copilot/tools/get_contact_tool.rb +1 -1
  15. data/examples/collaborative-copilot/tools/get_conversation_tool.rb +1 -1
  16. data/examples/collaborative-copilot/tools/get_stripe_billing_tool.rb +1 -1
  17. data/examples/collaborative-copilot/tools/search_contacts_tool.rb +3 -2
  18. data/examples/collaborative-copilot/tools/search_conversations_tool.rb +2 -2
  19. data/examples/collaborative-copilot/tools/search_knowledge_base_tool.rb +4 -3
  20. data/examples/collaborative-copilot/tools/search_linear_issues_tool.rb +5 -4
  21. data/examples/isp-support/agents_factory.rb +4 -4
  22. data/examples/isp-support/tools/create_checkout_tool.rb +1 -1
  23. data/examples/isp-support/tools/create_lead_tool.rb +3 -3
  24. data/examples/isp-support/tools/crm_lookup_tool.rb +1 -1
  25. data/examples/isp-support/tools/search_docs_tool.rb +1 -1
  26. data/lib/agents/agent.rb +19 -6
  27. data/lib/agents/agent_tool.rb +1 -1
  28. data/lib/agents/handoff.rb +2 -5
  29. data/lib/agents/helpers/message_extractor.rb +24 -8
  30. data/lib/agents/instrumentation/constants.rb +6 -0
  31. data/lib/agents/instrumentation/tracing_callbacks.rb +153 -32
  32. data/lib/agents/instrumentation.rb +19 -1
  33. data/lib/agents/run_context.rb +5 -4
  34. data/lib/agents/runner.rb +78 -57
  35. data/lib/agents/tool.rb +8 -1
  36. data/lib/agents/tool_wrapper.rb +16 -13
  37. data/lib/agents/version.rb +1 -1
  38. data/lib/agents.rb +4 -2
  39. metadata +3 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '071869beb00d53446a27f489c3817c58b0f8d163f334f0030c95b789a7c4895b'
4
- data.tar.gz: 874227bd4dd05dd2947269bec7da2d593777a8600540ef9ec5a8fb50c346a884
3
+ metadata.gz: 66cc6b306e84c6d62c5bf30b34104de9f683a3ec106eb11de12625000b07795d
4
+ data.tar.gz: dfe243cb0723ef5ef6c79fbb14342cb78a8b5362ccbb4d42356c2f2386d832a5
5
5
  SHA512:
6
- metadata.gz: 7e3814e0e76359be595534eb92ce11d655e19e3997f78aea006be89721aae771c6a1f97ad277ed80581957cdea5a0919506518dfd8e8de1911a03a3641668a8b
7
- data.tar.gz: 351b99abda0942df5253e1735faca522b5eee988fc248ec16564dea700b2bcfcf31a8ec4ba8f9db73f3f57103e5f8b37a0997c6e71f4b1a1872ca796046cad2a
6
+ metadata.gz: 9db32ad036739a09f198995cf98426c477e5afd4a309dae9f33af11a0baf785808268d5d23539b71fe01c0c3f44ff2e7f1a34fcb3d783d44c171bb7175bed00f
7
+ data.tar.gz: a263f704efb3122b60bcd4b245e7d84a04c65ed640eae72befbc7e51c6948e668c01bb4115a2d02351e311bf4933a88a081883947970c522484bed4731cdf0e6
data/CHANGELOG.md CHANGED
@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ### Changed
11
+ - Require RubyLLM 2.0 starting with ai-agents 0.13.0. Applications using RubyLLM 1.x can keep ai-agents 0.12.0.
12
+ - Keep the previous `param :name, desc: "..."` tool declaration syntax working with RubyLLM 2.0.
13
+
14
+ ## [0.12.0] - 2026-06-29
15
+
16
+ ### Added
17
+ - Propagate Langfuse trace metadata and filter attributes from root traces to child agent, tool, and generation spans, while keeping span-specific input/output attributes local to each span.
18
+ - Support optional generation-level attributes via `attribute_provider#generation_attributes(context_wrapper, chat, message)`.
19
+ - Add `gen_ai.request.temperature` to generation spans when an agent temperature is configured.
20
+
10
21
  ## [0.11.0] - 2026-05-27
11
22
 
12
23
  ### Added
data/CLAUDE.md CHANGED
@@ -256,7 +256,7 @@ The SDK includes optional OpenTelemetry instrumentation (`lib/agents/instrumenta
256
256
 
257
257
  6. **Don't use `.delete` for shared tracing state**: If a value in `context[:__otel_tracing]` needs to be read by multiple callbacks, use `tracing[:key]` not `tracing.delete(:key)`. Delete is a destructive side-effect that breaks subsequent reads.
258
258
 
259
- 7. **Per-call LLM spans via `on_end_message`**: Individual GENERATION spans are created by hooking into RubyLLM's `chat.on_end_message` (registered in `on_chat_created`). Each span is created and immediately finished. There is no `current_llm_span` in tracing state — only `current_tool_span` needs single-slot tracking.
259
+ 7. **Per-call LLM spans via `after_message`**: Individual GENERATION spans are created by hooking into RubyLLM's `chat.after_message` (registered in `on_chat_created`). Each span is created and immediately finished. There is no `current_llm_span` in tracing state — only `current_tool_span` needs single-slot tracking.
260
260
 
261
261
  8. **Conversation history deduplication**: `Runner#last_message_matches?` checks if the last restored message already matches the current input. If so, uses `chat.complete` instead of `chat.ask(input)` to avoid sending the user message twice.
262
262
 
data/README.md CHANGED
@@ -128,9 +128,9 @@ agent.register_handoffs(technical_support, billing)
128
128
  ```ruby
129
129
  class EmailTool < Agents::Tool
130
130
  description "Send emails to customers"
131
- param :to, type: "string", desc: "Email address"
132
- param :subject, type: "string", desc: "Email subject"
133
- param :body, type: "string", desc: "Email body"
131
+ parameter :to, type: "string", description: "Email address"
132
+ parameter :subject, type: "string", description: "Email subject"
133
+ parameter :body, type: "string", description: "Email body"
134
134
 
135
135
  def perform(tool_context, to:, subject:, body:)
136
136
  # Send email logic here
data/docs/architecture.md CHANGED
@@ -301,7 +301,7 @@ The tool system is fully extensible:
301
301
  class CustomTool < Agents::Tool
302
302
  name "custom_action"
303
303
  description "Perform custom business logic"
304
- param :input, type: "string"
304
+ parameter :input, type: "string"
305
305
 
306
306
  def perform(tool_context, input:)
307
307
  # Access context for state
@@ -22,6 +22,8 @@ This project is in early development. While thread safety is a core design goal
22
22
  * **`tools`**: An array of `Agents::Tool` instances that the agent can use to perform actions.
23
23
  * **`handoff_agents`**: An array of other agents that this agent can hand off conversations to.
24
24
  * **`temperature`**: Controls randomness in responses (0.0 = deterministic, 1.0 = very random, default: 0.7)
25
+ * **`protocol`**: Optional RubyLLM provider protocol, such as `:responses` or `:chat_completions`.
26
+ * **`thinking`**: Optional RubyLLM thinking options, such as `effort:` and `display:`.
25
27
 
26
28
  ### Example
27
29
 
@@ -43,3 +45,17 @@ specialized_agent = assistant_agent.clone(
43
45
  ```
44
46
 
45
47
  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.
48
+
49
+ For an OpenAI reasoning model, select Responses for that agent and leave temperature unset:
50
+
51
+ ```ruby
52
+ reasoning_agent = Agents::Agent.new(
53
+ name: "Reasoning",
54
+ model: "your-reasoning-model",
55
+ protocol: :responses,
56
+ temperature: nil,
57
+ thinking: { effort: :medium, display: :summarized }
58
+ )
59
+ ```
60
+
61
+ Other agents can use `protocol: :chat_completions`. The runner applies each agent's protocol and thinking settings when it starts or hands off a conversation.
@@ -33,7 +33,7 @@ You create tools by creating a class that inherits from `Agents::Tool` and imple
33
33
  class WeatherTool < Agents::Tool
34
34
  name "get_weather"
35
35
  description "Get the current weather for a location."
36
- param :location, type: "string", desc: "The city and state, e.g., San Francisco, CA"
36
+ parameter :location, type: "string", description: "The city and state, e.g., San Francisco, CA"
37
37
 
38
38
  def perform(tool_context, location:)
39
39
  # Access the API key from the shared context
@@ -18,8 +18,41 @@ The `Agents::Instrumentation` module produces OTel spans that give you full visi
18
18
  - **Agent container spans** grouping related LLM and tool calls
19
19
  - **Handoff events** recording agent-to-agent transfers
20
20
 
21
+ Generation inputs include MIME types for RubyLLM message attachments. Their URLs, local paths, and image data are omitted. Structured image inputs also omit the image URL.
22
+
21
23
  Spans follow the [GenAI semantic conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/) and include Langfuse-specific attributes for rich rendering in the Langfuse dashboard.
22
24
 
25
+ ### RubyLLM 2.0 events and Langfuse
26
+
27
+ RubyLLM 2.0 emits `chat.ruby_llm`, `tool_call.ruby_llm`, and `usage.ruby_llm` events. In Rails, these use `ActiveSupport::Notifications`. Outside Rails, configure a RubyLLM instrumenter to receive them. The events are useful for logging and per-attempt usage, but RubyLLM does not turn them into this runner's Langfuse trace. Keep `Agents::Instrumentation` when you need the following mapping:
28
+
29
+ | Runner span or event | Data in Langfuse | RubyLLM event alone |
30
+ |----------------------|------------------|---------------------|
31
+ | Root span | Overall input and output, session, user, tags, and Chatwoot metadata | No runner-level input, output, or Chatwoot context |
32
+ | Agent span | Agent name and the generations and tools it owns | No agent handoff boundary |
33
+ | Generation span | Request messages, response, model, temperature, and response tokens | `chat.ruby_llm` has the model and response, but needs an OTLP and Langfuse attribute adapter |
34
+ | Tool span | Tool arguments, result, and error status | `tool_call.ruby_llm` has the call, but needs the same adapter |
35
+ | Handoff event | Source agent, target agent, and reason | No RubyLLM handoff event for this runner |
36
+
37
+ RubyLLM's `usage.ruby_llm` event reports each provider attempt, including failed and cancelled attempts. The generation spans here use tokens from completed messages. Use a separate `usage.ruby_llm` subscriber for attempt-level accounting; do not add its tokens to the same generation spans or costs may be counted twice. See the [RubyLLM instrumentation guide](https://rubyllm.com/instrumentation/) for event payloads.
38
+
39
+ For an app outside Rails, add `activesupport` and configure RubyLLM before creating a chat:
40
+
41
+ ```ruby
42
+ require "active_support"
43
+ require "active_support/notifications"
44
+
45
+ RubyLLM.configure do |config|
46
+ config.instrumenter = ActiveSupport::Notifications
47
+ end
48
+
49
+ ActiveSupport::Notifications.subscribe("usage.ruby_llm") do |event|
50
+ puts event.payload.inspect # Replace with your per-attempt usage collector.
51
+ end
52
+ ```
53
+
54
+ This enables RubyLLM's native events. The runner spans still require `Agents::Instrumentation.install` and an OTel exporter, as shown below.
55
+
23
56
  ## Setup
24
57
 
25
58
  ### 1. Install dependencies
@@ -144,7 +177,7 @@ OpenTelemetry::SDK.configure do |c|
144
177
  OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new(
145
178
  OpenTelemetry::Exporter::OTLP::Exporter.new(
146
179
  endpoint: "#{langfuse_host}/api/public/otel/v1/traces",
147
- headers: { "Authorization" => "Basic #{auth_token}" }
180
+ headers: { "Authorization" => "Basic #{auth_token}", "x-langfuse-ingestion-version" => "4" }
148
181
  )
149
182
  )
150
183
  )
@@ -157,8 +190,8 @@ The instrumentation sets Langfuse-specific attributes that map to the Langfuse U
157
190
 
158
191
  | Attribute | Set On | Langfuse Display |
159
192
  |-----------|--------|-----------------|
160
- | `langfuse.trace.input` | Root span | Trace input (top of page) |
161
- | `langfuse.trace.output` | Root span | Trace output (top of page) |
193
+ | `langfuse.trace.input` | Root span | Legacy trace input for existing evaluators |
194
+ | `langfuse.trace.output` | Root span | Legacy trace output for existing evaluators |
162
195
  | `langfuse.observation.input` | All spans | Observation input (sidebar click) |
163
196
  | `langfuse.observation.output` | All spans | Observation output (sidebar click) |
164
197
  | `langfuse.observation.type` | Tool spans | `"tool"` type indicator |
@@ -168,6 +201,12 @@ The instrumentation sets Langfuse-specific attributes that map to the Langfuse U
168
201
  | `gen_ai.request.model` | Generation spans only | Model name + cost calculation |
169
202
  | `gen_ai.usage.input_tokens` | Generation spans | Token usage |
170
203
  | `gen_ai.usage.output_tokens` | Generation spans | Token usage |
204
+ | `gen_ai.usage.reasoning.output_tokens` | Generation spans, when reported | Reasoning tokens within output tokens |
205
+ | `langfuse.observation.metadata.reasoning_summary` | Responses generation spans with `thinking: { display: :summarized }` | Returned summary, when provided |
206
+
207
+ Langfuse v4 reads overall input and output from `langfuse.observation.input` and `langfuse.observation.output` on the root span. This integration also keeps the deprecated trace input and output attributes for existing consumers. The Langfuse v4 ingestion path needs the `x-langfuse-ingestion-version: 4` exporter header. See the [Langfuse migration guide](https://langfuse.com/integrations/native/opentelemetry/migration-to-v4).
208
+
209
+ Reasoning tokens are already part of output tokens, so do not add the two counts. The summary field records only text returned for a requested Responses summary. Encrypted reasoning signatures stay in conversation history for stateless replay and are not added to Langfuse spans. Treat persisted conversation history as sensitive data.
171
210
 
172
211
  ### EU vs US Cloud
173
212
 
@@ -202,7 +241,7 @@ OpenTelemetry::SDK.configure do |c|
202
241
  OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new(
203
242
  OpenTelemetry::Exporter::OTLP::Exporter.new(
204
243
  endpoint: "#{langfuse_host}/api/public/otel/v1/traces",
205
- headers: { "Authorization" => "Basic #{auth_token}" }
244
+ headers: { "Authorization" => "Basic #{auth_token}", "x-langfuse-ingestion-version" => "4" }
206
245
  )
207
246
  )
208
247
  )
@@ -251,7 +290,7 @@ Langfuse renders empty string attributes as "undefined". The instrumentation gua
251
290
 
252
291
  ### Double-counted costs
253
292
 
254
- If token costs appear inflated, verify that `gen_ai.request.model` is only set on GENERATION spans, not on container or root spans. The built-in instrumentation handles this correctly. If you set custom `span_attributes` that include `gen_ai.request.model`, costs will be double-counted.
293
+ If token costs appear inflated, verify that `gen_ai.request.model` is only set on GENERATION spans, not on container or root spans. `Agents::Instrumentation` handles this by default. If you set custom `span_attributes` that include `gen_ai.request.model`, costs will be double-counted.
255
294
 
256
295
  ### Empty spans / missing data
257
296
 
@@ -266,3 +305,13 @@ If token costs appear inflated, verify that `gen_ai.request.model` is only set o
266
305
  - Check that the Authorization header uses `Basic` (not `Bearer`) with base64-encoded `pk:sk`
267
306
  - Use `BatchSpanProcessor` for production; `SimpleSpanProcessor` can be useful for debugging
268
307
  - **SSL CRL errors on Ruby 3.4+**: The OTLP exporter silently fails when SSL certificate revocation list (CRL) checks fail. The exporter reports SUCCESS but no data arrives. Fix by passing `ssl_verify_mode: OpenSSL::SSL::VERIFY_NONE` to the exporter in development, or ensure your system CA certificates are up to date
308
+
309
+ ## End-to-end check
310
+
311
+ The optional Langfuse check stubs the model response, exports real OTel spans to your Langfuse project, and reads them back through the observations API. It covers a handoff, token usage, session and user attributes, and a failed tool. Set `LANGFUSE_HOST`, `LANGFUSE_PUBLIC_KEY`, and `LANGFUSE_SECRET_KEY`, then run:
312
+
313
+ ```sh
314
+ RUN_LANGFUSE_E2E=1 bundle exec rspec spec/integration/langfuse_e2e_spec.rb
315
+ ```
316
+
317
+ The model call is stubbed, so this check does not require a provider API key. Use the separate live LLM smoke suite when you also need to check provider access.
@@ -264,7 +264,7 @@ Create Rails-specific tools for database operations:
264
264
  class CustomerLookupTool < Agents::Tool
265
265
  name "lookup_customer"
266
266
  description "Look up customer information by email or ID"
267
- param :identifier, type: "string", desc: "Email address or customer ID"
267
+ parameter :identifier, type: "string", description: "Email address or customer ID"
268
268
 
269
269
  def perform(tool_context, identifier:)
270
270
  # Access Rails models safely
@@ -286,7 +286,7 @@ end
286
286
  class BillingTool < Agents::Tool
287
287
  name "get_billing_info"
288
288
  description "Retrieve billing information for a customer"
289
- param :user_id, type: "integer", desc: "Customer user ID"
289
+ parameter :user_id, type: "integer", description: "Customer user ID"
290
290
 
291
291
  def perform(tool_context, user_id:)
292
292
  user = User.find(user_id)
@@ -210,7 +210,7 @@ Tools should be stateless and rely on context for all data:
210
210
  class DatabaseTool < Agents::Tool
211
211
  name "query_database"
212
212
  description "Query the application database"
213
- param :query, type: "string", desc: "SQL query to execute"
213
+ parameter :query, type: "string", description: "SQL query to execute"
214
214
 
215
215
  def perform(tool_context, query:)
216
216
  # Get database connection from context, not instance variables
@@ -238,7 +238,7 @@ Store tool-specific data in context:
238
238
  class FileProcessorTool < Agents::Tool
239
239
  name "process_file"
240
240
  description "Process uploaded files"
241
- param :file_path, type: "string", desc: "Path to file"
241
+ parameter :file_path, type: "string", description: "Path to file"
242
242
 
243
243
  def perform(tool_context, file_path:)
244
244
  # Initialize tool state in context if needed
@@ -41,12 +41,12 @@ result = runner.run("I love the new product features, especially the API and das
41
41
  # }
42
42
  ```
43
43
 
44
- ## RubyLLM::Schema (Recommended)
44
+ ## Schematist::Schema (Recommended)
45
45
 
46
- For more complex schemas, use `RubyLLM::Schema` which provides a cleaner Ruby DSL:
46
+ For more complex schemas, use `Schematist::Schema` which provides a cleaner Ruby DSL:
47
47
 
48
48
  ```ruby
49
- class ContactSchema < RubyLLM::Schema
49
+ class ContactSchema < Schematist::Schema
50
50
  string :name, description: "Full name of the person"
51
51
  string :email, description: "Email address"
52
52
  string :phone, description: "Phone number", required: false
@@ -6,11 +6,11 @@ module Copilot
6
6
  # Tool for creating Linear tickets for engineering issues
7
7
  class CreateLinearTicketTool < Agents::Tool
8
8
  description "Create a Linear ticket for engineering issues or feature requests"
9
- param :title, type: "string", desc: "Title of the issue"
10
- param :description, type: "string", desc: "Detailed description of the issue"
11
- param :priority, type: "string", desc: "Priority level (low, medium, high)"
12
- param :assignee, type: "string", desc: "Optional: email of person to assign to", required: false
13
- param :labels, type: "string", desc: "Comma-separated labels (e.g., bug,api,production)"
9
+ parameter :title, type: "string", description: "Title of the issue"
10
+ parameter :description, type: "string", description: "Detailed description of the issue"
11
+ parameter :priority, type: "string", description: "Priority level (low, medium, high)"
12
+ parameter :assignee, type: "string", description: "Optional: email of person to assign to", required: false
13
+ parameter :labels, type: "string", description: "Comma-separated labels (e.g., bug,api,production)"
14
14
 
15
15
  def perform(tool_context, title:, description:, priority: "medium", assignee: nil, labels: "")
16
16
  # Generate a ticket ID
@@ -6,7 +6,7 @@ module Copilot
6
6
  # Tool for retrieving specific knowledge base articles by ID
7
7
  class GetArticleTool < Agents::Tool
8
8
  description "Get the full content of a specific knowledge base article"
9
- param :article_id, type: "string", desc: "ID of the article to retrieve (e.g., ART-001)"
9
+ parameter :article_id, type: "string", description: "ID of the article to retrieve (e.g., ART-001)"
10
10
 
11
11
  def perform(tool_context, article_id:)
12
12
  data_file = File.join(__dir__, "../data/knowledge_base.json")
@@ -6,7 +6,7 @@ module Copilot
6
6
  # Tool for retrieving contact/customer information
7
7
  class GetContactTool < Agents::Tool
8
8
  description "Get customer profile and contact information"
9
- param :contact_id, type: "string", desc: "ID of the contact to retrieve"
9
+ parameter :contact_id, type: "string", description: "ID of the contact to retrieve"
10
10
 
11
11
  def perform(tool_context, contact_id:)
12
12
  data_file = File.join(__dir__, "../data/contacts.json")
@@ -6,7 +6,7 @@ module Copilot
6
6
  # Tool for retrieving conversation details and context
7
7
  class GetConversationTool < Agents::Tool
8
8
  description "Get conversation details, messages, and context for analysis"
9
- param :conversation_id, type: "string", desc: "ID of the conversation to retrieve"
9
+ parameter :conversation_id, type: "string", description: "ID of the conversation to retrieve"
10
10
 
11
11
  def perform(tool_context, conversation_id:)
12
12
  data_file = File.join(__dir__, "../data/conversations.json")
@@ -6,7 +6,7 @@ module Copilot
6
6
  # Tool for retrieving Stripe billing information
7
7
  class GetStripeBillingTool < Agents::Tool
8
8
  description "Get customer billing information and payment history from Stripe"
9
- param :customer_email, type: "string", desc: "Customer email to look up billing info"
9
+ parameter :customer_email, type: "string", description: "Customer email to look up billing info"
10
10
 
11
11
  def perform(tool_context, customer_email:)
12
12
  data_file = File.join(__dir__, "../data/stripe_billing.json")
@@ -6,8 +6,9 @@ module Copilot
6
6
  # Tool for searching contacts to find patterns and related customers
7
7
  class SearchContactsTool < Agents::Tool
8
8
  description "Search contacts to find patterns, related customers, or specific profiles"
9
- param :query, type: "string", desc: "Search terms (name, email, company, or tags)"
10
- param :plan, type: "string", desc: "Optional: filter by plan type (Basic, Pro, Enterprise)", required: false
9
+ parameter :query, type: "string", description: "Search terms (name, email, company, or tags)"
10
+ parameter :plan, type: "string", description: "Filter by plan: Basic, Pro, or Enterprise",
11
+ required: false
11
12
 
12
13
  def perform(_tool_context, query:, plan: nil)
13
14
  data_file = File.join(__dir__, "../data/contacts.json")
@@ -6,8 +6,8 @@ module Copilot
6
6
  # Tool for searching through conversation history to find similar cases
7
7
  class SearchConversationsTool < Agents::Tool
8
8
  description "Search past conversations for similar cases and resolutions"
9
- param :query, type: "string", desc: "Search terms (keywords, tags, or issue description)"
10
- param :contact_id, type: "string", desc: "Optional: limit search to specific contact", required: false
9
+ parameter :query, type: "string", description: "Search terms (keywords, tags, or issue description)"
10
+ parameter :contact_id, type: "string", description: "Optional: limit search to specific contact", required: false
11
11
 
12
12
  def perform(_tool_context, query:, contact_id: nil)
13
13
  data_file = File.join(__dir__, "../data/conversations.json")
@@ -6,9 +6,10 @@ module Copilot
6
6
  # Tool for searching knowledge base articles and documentation
7
7
  class SearchKnowledgeBaseTool < Agents::Tool
8
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
9
+ parameter :query, type: "string", description: "Search terms or keywords to find relevant articles"
10
+ parameter :category, type: "string",
11
+ description: "Filter by category: troubleshooting, account, development, or billing",
12
+ required: false
12
13
 
13
14
  def perform(_tool_context, query:, category: nil)
14
15
  data_file = File.join(__dir__, "../data/knowledge_base.json")
@@ -6,10 +6,11 @@ module Copilot
6
6
  # Tool for searching Linear issues for development context and bug reports
7
7
  class SearchLinearIssuesTool < Agents::Tool
8
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
9
+ parameter :query, type: "string", description: "Search issue keywords, errors, or feature descriptions"
10
+ parameter :status, type: "string", description: "Filter by status: backlog, in progress, completed, or resolved",
11
+ required: false
12
+ parameter :priority, type: "string", description: "Filter by priority: low, medium, or high",
13
+ required: false
13
14
 
14
15
  def perform(_tool_context, query:, status: nil, priority: nil)
15
16
  data_file = File.join(__dir__, "../data/linear_issues.json")
@@ -6,7 +6,7 @@ require_relative "tools/create_lead_tool"
6
6
  require_relative "tools/create_checkout_tool"
7
7
  require_relative "tools/search_docs_tool"
8
8
  require_relative "tools/escalate_to_human_tool"
9
- require "ruby_llm/schema"
9
+ require "schematist"
10
10
 
11
11
  module ISPSupport
12
12
  # Factory for creating all ISP support agents with proper handoff relationships.
@@ -98,7 +98,7 @@ module ISPSupport
98
98
  end
99
99
 
100
100
  def triage_response_schema
101
- RubyLLM::Schema.create do
101
+ Schematist::Schema.create do
102
102
  string :response, description: "Your response to the customer"
103
103
  string :intent, enum: %w[sales support unclear], description: "The detected intent category"
104
104
  array :sentiment, description: "Customer sentiment indicators" do
@@ -108,7 +108,7 @@ module ISPSupport
108
108
  end
109
109
 
110
110
  def support_response_schema
111
- RubyLLM::Schema.create do
111
+ Schematist::Schema.create do
112
112
  string :response, description: "Your response to the customer"
113
113
  string :intent, enum: %w[support], description: "The intent category (always support)"
114
114
  array :sentiment, description: "Customer sentiment indicators" do
@@ -118,7 +118,7 @@ module ISPSupport
118
118
  end
119
119
 
120
120
  def sales_response_schema
121
- RubyLLM::Schema.create do
121
+ Schematist::Schema.create do
122
122
  string :response, description: "Your response to the customer"
123
123
  string :intent, enum: %w[sales], description: "The intent category (always sales)"
124
124
  array :sentiment, description: "Customer sentiment indicators" do
@@ -6,7 +6,7 @@ module ISPSupport
6
6
  # Tool for creating checkout links for new service subscriptions.
7
7
  class CreateCheckoutTool < Agents::Tool
8
8
  description "Create a secure checkout link for a service plan"
9
- param :plan_name, type: "string", desc: "Name of the plan to purchase"
9
+ parameter :plan_name, type: "string", description: "Name of the plan to purchase"
10
10
 
11
11
  def perform(_tool_context, plan_name:)
12
12
  session_id = SecureRandom.hex(8)
@@ -4,9 +4,9 @@ module ISPSupport
4
4
  # Tool for creating sales leads in the CRM system.
5
5
  class CreateLeadTool < Agents::Tool
6
6
  description "Create a new sales lead with customer information"
7
- param :name, type: "string", desc: "Customer's full name"
8
- param :email, type: "string", desc: "Customer's email address"
9
- param :desired_plan, type: "string", desc: "Plan the customer is interested in"
7
+ parameter :name, type: "string", description: "Customer's full name"
8
+ parameter :email, type: "string", description: "Customer's email address"
9
+ parameter :desired_plan, type: "string", description: "Plan the customer is interested in"
10
10
 
11
11
  def perform(tool_context, name:, email:, desired_plan:)
12
12
  # Store lead information in state for follow-up
@@ -6,7 +6,7 @@ module ISPSupport
6
6
  # Tool for looking up customer information from the CRM system.
7
7
  class CrmLookupTool < Agents::Tool
8
8
  description "Look up customer account information by account ID"
9
- param :account_id, type: "string", desc: "Customer account ID (e.g., CUST001)"
9
+ parameter :account_id, type: "string", description: "Customer account ID (e.g., CUST001)"
10
10
 
11
11
  def perform(tool_context, account_id:)
12
12
  data_file = File.join(__dir__, "../data/customers.json")
@@ -4,7 +4,7 @@ module ISPSupport
4
4
  # Tool for searching the knowledge base documentation.
5
5
  class SearchDocsTool < Agents::Tool
6
6
  description "Search knowledge base for troubleshooting steps and solutions"
7
- param :query, type: "string", desc: "Search terms or description of the issue"
7
+ parameter :query, type: "string", description: "Search terms or description of the issue"
8
8
 
9
9
  def perform(_tool_context, query:)
10
10
  case query.downcase
data/lib/agents/agent.rb CHANGED
@@ -50,8 +50,8 @@ require_relative "helpers/hash_normalizer"
50
50
  # )
51
51
  module Agents
52
52
  class Agent
53
- attr_reader :name, :instructions, :model, :provider, :assume_model_exists, :tools, :handoff_agents, :temperature,
54
- :response_schema, :headers, :params
53
+ attr_reader :name, :instructions, :model, :provider, :protocol, :assume_model_exists, :tools, :handoff_agents,
54
+ :temperature, :thinking, :response_schema, :headers, :params
55
55
 
56
56
  # Initialize a new Agent instance
57
57
  #
@@ -59,23 +59,32 @@ module Agents
59
59
  # @param instructions [String, Proc, nil] Static string or dynamic Proc that returns instructions
60
60
  # @param model [String] The LLM model to use (default: "gpt-4.1-mini")
61
61
  # @param provider [Symbol, String, nil] Optional RubyLLM provider override
62
+ # @param protocol [Symbol, String, nil] Optional provider protocol override (e.g., :responses)
62
63
  # @param assume_model_exists [Boolean] Whether RubyLLM should skip registry validation for custom model IDs
63
64
  # @param tools [Array<Agents::Tool>] Array of tool instances the agent can use
64
65
  # @param handoff_agents [Array<Agents::Agent>] Array of agents this agent can hand off to
65
- # @param temperature [Float] Controls randomness in responses (0.0 = deterministic, 1.0 = very random, default: 0.7)
66
+ # @param temperature [Float, nil] Sampling temperature; nil uses the model default
67
+ # @param thinking [Hash, nil] RubyLLM thinking options (e.g., effort: :medium, display: :summarized)
66
68
  # @param response_schema [Hash, nil] JSON schema for structured output responses
67
69
  # @param headers [Hash, nil] Default HTTP headers applied to LLM requests
68
70
  # @param params [Hash, nil] Default provider-specific parameters applied to LLM requests (e.g., service_tier)
69
- def initialize(name:, instructions: nil, model: "gpt-4.1-mini", provider: nil, assume_model_exists: false,
70
- tools: [], handoff_agents: [], temperature: 0.7, response_schema: nil, headers: nil, params: nil)
71
+ def initialize(name:, instructions: nil, model: "gpt-4.1-mini", provider: nil, protocol: nil,
72
+ assume_model_exists: false, tools: [], handoff_agents: [], temperature: 0.7, thinking: nil,
73
+ response_schema: nil, headers: nil, params: nil)
71
74
  @name = name
72
75
  @instructions = instructions
73
76
  @model = model
74
77
  @provider = provider&.to_sym
78
+ @protocol = protocol&.to_sym
75
79
  @assume_model_exists = assume_model_exists
76
80
  @tools = tools.dup
77
81
  @handoff_agents = []
78
82
  @temperature = temperature
83
+ @thinking = if thinking.nil?
84
+ nil
85
+ else
86
+ Helpers::HashNormalizer.normalize(thinking, label: "thinking", freeze_result: true)
87
+ end
79
88
  @response_schema = response_schema
80
89
  @headers = Helpers::HashNormalizer.normalize(headers, label: "headers", freeze_result: true)
81
90
  @params = Helpers::HashNormalizer.normalize(params, label: "params", freeze_result: true)
@@ -131,7 +140,7 @@ module Agents
131
140
 
132
141
  # Creates a new agent instance with modified attributes while preserving immutability.
133
142
  # The clone method is used when you need to create variations of agents without mutating the original.
134
- # This can be used for runtime agent modifications, say in a multi-tenant environment we can do something like the following:
143
+ # This can be used for runtime agent modifications, such as multi-tenant settings:
135
144
  #
136
145
  # @example Multi-tenant agent customization
137
146
  # def agent_for_tenant(tenant)
@@ -161,10 +170,12 @@ module Agents
161
170
  # @option changes [String, Proc] :instructions New instructions
162
171
  # @option changes [String] :model New model identifier
163
172
  # @option changes [Symbol, String, nil] :provider New provider override
173
+ # @option changes [Symbol, String, nil] :protocol New provider protocol override
164
174
  # @option changes [Boolean] :assume_model_exists Whether to skip model registry validation
165
175
  # @option changes [Array<Agents::Tool>] :tools New tools array (replaces all tools)
166
176
  # @option changes [Array<Agents::Agent>] :handoff_agents New handoff agents
167
177
  # @option changes [Float] :temperature Temperature for LLM responses (0.0-1.0)
178
+ # @option changes [Hash, nil] :thinking RubyLLM thinking options
168
179
  # @option changes [Hash, nil] :response_schema JSON schema for structured output
169
180
  # @return [Agents::Agent] A new frozen agent instance with the specified changes
170
181
  def clone(**changes)
@@ -173,10 +184,12 @@ module Agents
173
184
  instructions: changes.fetch(:instructions, @instructions),
174
185
  model: changes.fetch(:model, @model),
175
186
  provider: changes.fetch(:provider, @provider),
187
+ protocol: changes.fetch(:protocol, @protocol),
176
188
  assume_model_exists: changes.fetch(:assume_model_exists, @assume_model_exists),
177
189
  tools: changes.fetch(:tools, @tools.dup),
178
190
  handoff_agents: changes.fetch(:handoff_agents, @handoff_agents),
179
191
  temperature: changes.fetch(:temperature, @temperature),
192
+ thinking: changes.fetch(:thinking, @thinking),
180
193
  response_schema: changes.fetch(:response_schema, @response_schema),
181
194
  headers: changes.fetch(:headers, @headers),
182
195
  params: changes.fetch(:params, @params)
@@ -40,7 +40,7 @@ module Agents
40
40
  attr_reader :wrapped_agent, :tool_name, :tool_description, :output_extractor
41
41
 
42
42
  # Default parameter for agent tools
43
- param :input, type: "string", desc: "Input message for the agent"
43
+ parameter :input, type: "string", description: "Input message for the agent"
44
44
 
45
45
  # Initialize an AgentTool that wraps an agent as a callable tool
46
46
  #
@@ -69,19 +69,16 @@ module Agents
69
69
  @tool_description
70
70
  end
71
71
 
72
- # Use RubyLLM's halt mechanism to stop continuation after handoff
72
+ # The runner switches agents after this tool records the handoff.
73
73
  # Store handoff info in context for Runner to detect and process
74
74
  def perform(tool_context)
75
75
  # Store handoff information in context for Runner to detect
76
- # TODO: The following is a race condition that needs to be addressed in future versions
77
- # If multiple handoff tools execute concurrently, they overwrite each other's pending_handoff data.
78
76
  tool_context.run_context.context[:pending_handoff] = {
79
77
  target_agent: @target_agent,
80
78
  timestamp: Time.now
81
79
  }
82
80
 
83
- # Return halt to stop LLM continuation
84
- halt("I'll transfer you to #{@target_agent.name} who can better assist you with this.")
81
+ "I'll transfer you to #{@target_agent.name} who can better assist you with this."
85
82
  end
86
83
 
87
84
  # NOTE: RubyLLM will handle schema generation internally when needed