activeagent 1.8.1 → 1.9.1

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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1058 -0
  3. data/README.md +9 -1
  4. data/lib/active_agent/base.rb +22 -1
  5. data/lib/active_agent/concerns/delegation.rb +4 -0
  6. data/lib/active_agent/concerns/input_requests.rb +178 -0
  7. data/lib/active_agent/concerns/parameterized.rb +7 -4
  8. data/lib/active_agent/concerns/provider.rb +1 -0
  9. data/lib/active_agent/concerns/tooling.rb +7 -2
  10. data/lib/active_agent/delegation/runner.rb +31 -0
  11. data/lib/active_agent/evals/catalog.rb +305 -0
  12. data/lib/active_agent/evals/ruby_llm.rb +411 -0
  13. data/lib/active_agent/evals/scenario.rb +10 -3
  14. data/lib/active_agent/evals/scenario_parser.rb +38 -10
  15. data/lib/active_agent/evals.rb +1 -0
  16. data/lib/active_agent/generation.rb +62 -2
  17. data/lib/active_agent/generation_job.rb +14 -3
  18. data/lib/active_agent/input_request/resume.rb +199 -0
  19. data/lib/active_agent/input_request.rb +279 -0
  20. data/lib/active_agent/providers/_base_provider.rb +153 -31
  21. data/lib/active_agent/providers/anthropic/transforms.rb +57 -28
  22. data/lib/active_agent/providers/anthropic_provider.rb +102 -20
  23. data/lib/active_agent/providers/common/responses/prompt.rb +21 -0
  24. data/lib/active_agent/providers/concerns/input_requests.rb +332 -0
  25. data/lib/active_agent/providers/concerns/mcp_serving.rb +267 -0
  26. data/lib/active_agent/providers/concerns/tool_choice_clearing.rb +18 -3
  27. data/lib/active_agent/providers/deep_seek_provider.rb +6 -0
  28. data/lib/active_agent/providers/deepseek/_types.rb +4 -0
  29. data/lib/active_agent/providers/deepseek/chat/_types.rb +54 -0
  30. data/lib/active_agent/providers/deepseek/chat/request.rb +121 -0
  31. data/lib/active_agent/providers/deepseek/options.rb +53 -0
  32. data/lib/active_agent/providers/deepseek_provider.rb +46 -0
  33. data/lib/active_agent/providers/log_subscriber.rb +13 -0
  34. data/lib/active_agent/providers/mcp_bridge.rb +691 -0
  35. data/lib/active_agent/providers/mcp_tool_cache.rb +230 -0
  36. data/lib/active_agent/providers/open_ai/_base.rb +1 -1
  37. data/lib/active_agent/providers/open_ai/chat_provider.rb +22 -3
  38. data/lib/active_agent/providers/open_ai/options.rb +1 -1
  39. data/lib/active_agent/providers/open_ai/responses_provider.rb +68 -15
  40. data/lib/active_agent/providers/open_router/options.rb +6 -2
  41. data/lib/active_agent/providers/ruby_llm/messages/_types.rb +3 -1
  42. data/lib/active_agent/providers/ruby_llm/options.rb +5 -0
  43. data/lib/active_agent/providers/ruby_llm_provider.rb +127 -29
  44. data/lib/active_agent/schema_tools.rb +12 -0
  45. data/lib/active_agent/telemetry/instrumentation.rb +10 -1
  46. data/lib/active_agent/version.rb +1 -1
  47. data/lib/active_agent.rb +2 -0
  48. data/lib/generators/active_agent/schema_tools/USAGE +11 -0
  49. data/lib/generators/active_agent/schema_tools/schema_tools_generator.rb +39 -4
  50. data/lib/generators/active_agent/schema_tools/templates/schema_tools.rb.tt +13 -0
  51. metadata +31 -2
@@ -42,7 +42,7 @@ module ActiveAgent
42
42
  # provider.complete().
43
43
  #
44
44
  # @param parameters [Hash] serialized request parameters
45
- # @return [Hash, nil] normalized API response hash, or nil for streaming
45
+ # @return [Hash] normalized API response, or metadata for a streamed turn
46
46
  def api_prompt_execute(parameters)
47
47
  @resolved_model_id = parameters[:model] || options.model
48
48
  resolve_ruby_llm_provider!(@resolved_model_id)
@@ -72,16 +72,24 @@ module ActiveAgent
72
72
  end
73
73
  end
74
74
 
75
+ # Blank means unset, as from an ERB value whose variable is unset.
76
+ if (protocol = options.protocol.presence)
77
+ raise ArgumentError, "The protocol option needs ruby_llm 2.0 or newer, and #{::RubyLLM::VERSION} is loaded" unless ruby_llm_v2?
78
+
79
+ kwargs[:protocol] = protocol.to_sym
80
+ end
81
+
75
82
  if parameters[:stream]
76
83
  stream_proc = parameters[:stream]
77
84
  @stream_tool_calls = {}
85
+ @stream_response = { model: @resolved_model_id, stop_reason: "end_turn" }
78
86
 
79
87
  # For streaming, pass a block that forwards chunks
80
88
  @ruby_llm_provider.complete(messages, **kwargs) do |chunk|
81
89
  stream_proc.call(chunk)
82
90
  end
83
91
 
84
- nil
92
+ @stream_response
85
93
  else
86
94
  response = @ruby_llm_provider.complete(messages, **kwargs)
87
95
  normalize_ruby_llm_response(response, @resolved_model_id)
@@ -145,37 +153,63 @@ module ActiveAgent
145
153
  if chunk.tool_calls&.any?
146
154
  message[:tool_calls] ||= []
147
155
  chunk.tool_calls.each do |key, tool_call|
148
- # RubyLLM 2 keys OpenAI deltas by index; only the first delta
149
- # includes the call ID and name. Keep each index tied to its call.
150
- existing = message[:tool_calls].find { |tc| tool_call.id && tc[:id] == tool_call.id }
151
- existing ||= @stream_tool_calls[key]
156
+ # Only the first delta of a call has its ID and name; the rest
157
+ # are fragments of its arguments. A fragment belongs to the call
158
+ # that shares its stream key (the index RubyLLM 2 numbers calls
159
+ # by), or, without one as RubyLLM 1.16 leaves it for OpenAI, to
160
+ # the latest call. RubyLLM 1.16 cannot preserve interleaved
161
+ # OpenAI calls: its parser drops their indices and fragments.
162
+ existing = if tool_call.id
163
+ message[:tool_calls].find { |tc| tc[:id] == tool_call.id }
164
+ else
165
+ @stream_tool_calls[key] || message[:tool_calls].last
166
+ end
167
+ fragment = streamed_tool_arguments(tool_call.arguments)
152
168
  if existing
153
- existing[:function][:arguments] += tool_call.arguments.to_s if tool_call.arguments
169
+ existing[:function][:arguments] += fragment
154
170
  else
155
171
  existing = {
156
172
  id: tool_call.id,
157
173
  type: "function",
158
174
  function: {
159
175
  name: tool_call.name,
160
- arguments: tool_call.arguments.to_s
176
+ arguments: fragment
161
177
  }
162
178
  }
163
179
  message[:tool_calls] << existing
164
180
  end
165
- @stream_tool_calls[key] = existing
181
+ @stream_tool_calls[key] = existing unless key.nil?
166
182
  end
167
183
  end
168
184
 
169
- # Stream completion is handled by the base provider after
170
- # api_prompt_execute returns nil. No action needed here.
185
+ # Counts arrive in separate chunks (Anthropic sends input at the start
186
+ # and output at the end). They are cumulative for this turn, so merge
187
+ # reported fields rather than adding each chunk's counts.
188
+ if (usage = ruby_llm_usage(chunk))
189
+ (@stream_response[:usage] ||= {}).merge!(usage) do |field, previous, current|
190
+ # Chat Completions synthesizes zero when cache writes are omitted.
191
+ # That cannot erase writes already counted in this turn.
192
+ field == :cache_creation_tokens && current == 0 ? previous : current
193
+ end
194
+ end
195
+ if chunk.respond_to?(:finish_reason) && chunk.finish_reason
196
+ @stream_response[:stop_reason] = ruby_llm_stop_reason(chunk, tool_calls: message[:tool_calls])
197
+ elsif message[:tool_calls]&.any? && @stream_response[:stop_reason] == "end_turn"
198
+ @stream_response[:stop_reason] = "tool_use"
199
+ end
200
+
201
+ # The base provider finishes the turn after api_prompt_execute returns
202
+ # its metadata, once every chunk has been consumed.
171
203
  end
172
204
 
173
205
  # Extracts messages from the completed API response.
174
206
  #
175
- # @param api_response [Hash, nil] normalized response hash
207
+ # @param api_response [Hash, nil] normalized response or stream metadata
176
208
  # @return [Array<Hash>, nil]
177
209
  def process_prompt_finished_extract_messages(api_response)
178
210
  return nil unless api_response
211
+ return nil if request.stream
212
+
179
213
  [ api_response ]
180
214
  end
181
215
 
@@ -241,16 +275,20 @@ module ActiveAgent
241
275
  end
242
276
  end
243
277
 
244
- # Executes tool calls and pushes results to message_stack.
278
+ # Executes tool calls and pushes results to message_stack. Pushes
279
+ # nothing when a call paused for the user.
245
280
  #
246
281
  # @param tool_calls [Array<Hash>] with :id, :name, :input keys
247
282
  # @return [void]
248
283
  def process_function_calls(tool_calls)
249
- tool_calls.each do |tool_call|
250
- content = instrument("tool_call.active_agent", tool_name: tool_call[:name]) do
251
- tools_function.call(tool_call[:name], **tool_call[:input])
284
+ results = dispatch_tool_calls(tool_calls) do |tool_call|
285
+ instrument("tool_call.active_agent", tool_name: tool_call[:name]) do
286
+ call_tool_function(tool_call[:name], **tool_call[:input])
252
287
  end
288
+ end
289
+ return unless results
253
290
 
291
+ tool_calls.zip(results).each do |tool_call, content|
254
292
  message_stack.push({
255
293
  role: "tool",
256
294
  tool_call_id: tool_call[:id],
@@ -259,7 +297,8 @@ module ActiveAgent
259
297
  end
260
298
  end
261
299
 
262
- # api_prompt_execute always returns a normalized Hash or nil (streaming),
300
+ # api_prompt_execute always returns a normalized Hash (stream metadata
301
+ # carries the final usage and stop reason without another message),
263
302
  # so no additional normalization is needed for instrumentation.
264
303
  # Inherits default api_response_normalize from BaseProvider.
265
304
 
@@ -393,6 +432,10 @@ module ActiveAgent
393
432
  # complete takes, { name:, schema:, strict: }, filling the name and
394
433
  # strict flag in the way RubyLLM::Chat#with_schema does.
395
434
  #
435
+ # A json_schema takes its name, schema and strict flag nested under
436
+ # +json_schema+ or beside +type+, the two shapes the OpenAI providers
437
+ # accept; a field given beside +type+ wins, as it does there.
438
+ #
396
439
  # @param response_format [Hash, Symbol, String, nil] ActiveAgent common format
397
440
  # @return [Hash, nil] nil when the response is plain text
398
441
  # @raise [ArgumentError] for json_schema without a schema, and for any
@@ -406,10 +449,12 @@ module ActiveAgent
406
449
  when "text"
407
450
  nil
408
451
  when "json_schema"
409
- json_schema = format[:json_schema] || {}
410
- raise ArgumentError, "RubyLLMProvider needs a schema for a json_schema response_format" unless json_schema[:schema]
452
+ nested = format[:json_schema] || {}
453
+ schema = format[:schema] || nested[:schema]
454
+ raise ArgumentError, "RubyLLMProvider needs a schema for a json_schema response_format" unless schema
411
455
 
412
- { name: json_schema[:name] || "response", schema: json_schema[:schema], strict: json_schema[:strict] != false }
456
+ strict = format.fetch(:strict) { nested[:strict] }
457
+ { name: format[:name] || nested[:name] || "response", schema: schema, strict: strict != false }
413
458
  else
414
459
  raise ArgumentError, "RubyLLMProvider supports a json_schema or text response_format, not #{format[:type].inspect}; " \
415
460
  "ruby_llm has no JSON object mode, so give json_schema a schema instead"
@@ -463,27 +508,80 @@ module ActiveAgent
463
508
  if response.respond_to?(:stop_reason) && response.stop_reason
464
509
  hash[:stop_reason] = response.stop_reason
465
510
  elsif response.respond_to?(:finish_reason) && response.finish_reason
466
- hash[:stop_reason] = { stop: "end_turn", tool_calls: "tool_use" }.fetch(response.finish_reason, response.finish_reason.to_s)
511
+ hash[:stop_reason] = ruby_llm_stop_reason(response)
467
512
  elsif response.tool_calls&.any?
468
513
  hash[:stop_reason] = "tool_use"
469
514
  else
470
515
  hash[:stop_reason] = "end_turn"
471
516
  end
472
517
 
473
- # Add usage info if available
518
+ usage = ruby_llm_usage(response)
519
+ hash[:usage] = usage if usage
520
+
521
+ hash[:model] = model_id if model_id
522
+
523
+ hash
524
+ end
525
+
526
+ # A streamed tool call's arguments as the text they add to the JSON
527
+ # string the call builds up. Most APIs stream that string; Anthropic
528
+ # opens a call with an empty Hash for its input, and Gemini sends the
529
+ # arguments whole as a Hash.
530
+ #
531
+ # @param arguments [String, Hash, nil]
532
+ # @return [String]
533
+ def streamed_tool_arguments(arguments)
534
+ case arguments
535
+ when String then arguments
536
+ when Hash then arguments.empty? ? "" : arguments.to_json
537
+ else ""
538
+ end
539
+ end
540
+
541
+ # ruby_llm's normalized finish reason as the stop reason ActiveAgent
542
+ # reports. A response that asks for tools is tool_use however the API
543
+ # ends it: OpenAI's Responses API ends one with :stop. A reason
544
+ # ActiveAgent has no name for, such as Anthropic's :pause_turn, is kept
545
+ # as the provider spelled it.
546
+ #
547
+ # @param response [RubyLLM::Message] carrying a finish_reason
548
+ # @return [String]
549
+ def ruby_llm_stop_reason(response, tool_calls: response.tool_calls)
550
+ return "tool_use" if response.finish_reason == :stop && tool_calls&.any?
551
+
552
+ { stop: "end_turn", tool_calls: "tool_use" }.fetch(response.finish_reason, response.finish_reason.to_s)
553
+ end
554
+
555
+ # The tokens ruby_llm counted for the response, or nil when it counted
556
+ # none: the message has no Tokens then in ruby_llm 1.x. RubyLLM counts
557
+ # cached tokens apart from the input, so input_tokens leaves them out.
558
+ # A count the provider left out is left out here too, and Usage reads it
559
+ # as zero.
560
+ #
561
+ # @param response [RubyLLM::Message]
562
+ # @return [Hash, nil] Common::Usage attributes
563
+ def ruby_llm_usage(response)
474
564
  if response.respond_to?(:tokens)
475
565
  tokens = response.tokens
476
- hash[:usage] = { input_tokens: tokens.input, output_tokens: tokens.output } if tokens.input
566
+ return unless tokens
567
+
568
+ usage = {
569
+ input_tokens: tokens.input,
570
+ output_tokens: tokens.output,
571
+ cached_tokens: tokens.cache_read,
572
+ cache_creation_tokens: tokens.cache_write,
573
+ reasoning_tokens: tokens.thinking
574
+ }.compact
575
+ # ruby_llm's OpenAI parser reports cache_write: 0 for a response or
576
+ # chunk that carries no usage at all, so a zero cache or thinking
577
+ # count is not a count. Input and output are, even at zero.
578
+ usage if usage[:input_tokens] || usage[:output_tokens] || usage.values.any?(&:positive?)
477
579
  elsif response.respond_to?(:input_tokens) && response.input_tokens
478
- hash[:usage] = {
580
+ {
479
581
  input_tokens: response.input_tokens,
480
582
  output_tokens: response.output_tokens
481
- }
583
+ }.compact
482
584
  end
483
-
484
- hash[:model] = model_id if model_id
485
-
486
- hash
487
585
  end
488
586
  end
489
587
  end
@@ -83,6 +83,18 @@ module ActiveAgent
83
83
  "on_or_before" => :lteq, "on_or_after" => :gteq
84
84
  }.freeze
85
85
 
86
+ # Column names that look like they hold a credential. The schema tools
87
+ # generator never suggests one, and the dashboard never lets one be
88
+ # chosen: reading one back would hand a model a credential, and
89
+ # filtering on one leaks it a character at a time through the row
90
+ # counts.
91
+ SECRET_COLUMNS = /password|digest|token|secret|api_key|otp|encrypted|ssn/i
92
+
93
+ # Heads a file `active_agent:schema_tools --managed` wrote. The
94
+ # dashboard rewrites and removes only tools files that carry it, so
95
+ # deleting the line hands the file back to whoever edits it.
96
+ MANAGED_MARKER = "Managed by the ActiveAgent dashboard"
97
+
86
98
  # Raised when a tool call names a column outside the declared allowlists,
87
99
  # or is otherwise outside the declared boundary.
88
100
  class UnpermittedAttribute < ArgumentError; end
@@ -212,6 +212,7 @@ module ActiveAgent
212
212
  if result.respond_to?(:finish_reason) && result.finish_reason.present?
213
213
  llm_span.set_attribute("llm.finish_reason", result.finish_reason.to_s)
214
214
  end
215
+ span.set_attribute("agent.awaiting_input", true) if result.try(:awaiting_input?)
215
216
 
216
217
  llm_span.set_status(:ok)
217
218
  llm_span.finish
@@ -254,7 +255,10 @@ module ActiveAgent
254
255
  capture_bodies = agent.send(:telemetry_capture_bodies?)
255
256
  arguments = kwargs.presence || (args.length == 1 ? args.first : args.presence)
256
257
  if arguments.present? && capture_bodies
257
- tool_span.set_attribute("tool.input.args", agent.send(:telemetry_truncate, JSON.generate(arguments)))
258
+ # The result and any error arrive from `base` with secret
259
+ # answers already scrubbed; the arguments have not been through it.
260
+ recorded = agent.send(:telemetry_scrub_secrets, JSON.generate(arguments))
261
+ tool_span.set_attribute("tool.input.args", agent.send(:telemetry_truncate, recorded))
258
262
  end
259
263
  begin
260
264
  result = base.call(tool_name, *args, **kwargs)
@@ -338,6 +342,11 @@ module ActiveAgent
338
342
  nil
339
343
  end
340
344
 
345
+ # @return [String] `text` without the generation's secret answers
346
+ def telemetry_scrub_secrets(text)
347
+ respond_to?(:scrub_input_request_secrets, true) ? scrub_input_request_secrets(text) : text
348
+ end
349
+
341
350
  def telemetry_truncate(value)
342
351
  text = value.to_s
343
352
  return text if text.length <= TELEMETRY_ATTRIBUTE_MAX_CHARS
@@ -1,3 +1,3 @@
1
1
  module ActiveAgent
2
- VERSION = "1.8.1"
2
+ VERSION = "1.9.1"
3
3
  end
data/lib/active_agent.rb CHANGED
@@ -100,6 +100,8 @@ module ActiveAgent
100
100
  autoload :Streaming, "active_agent/concerns/streaming"
101
101
  autoload :InlinePreviewInterceptor
102
102
  autoload :Generation
103
+ autoload :InputRequest
104
+ autoload :InputRequests, "active_agent/concerns/input_requests"
103
105
  autoload :Queueing, "active_agent/concerns/queueing"
104
106
  autoload :Parameterized, "active_agent/concerns/parameterized"
105
107
  autoload :Preview, "active_agent/concerns/preview"
@@ -20,3 +20,14 @@ Examples:
20
20
 
21
21
  scopes reads through ReservationPolicy::Scope even if it cannot be
22
22
  found from here.
23
+
24
+ `bin/rails generate active_agent:schema_tools Reservation --filterable status --returns status starts_at`
25
+
26
+ declares exactly those columns (after `id`) instead of suggesting them.
27
+ A column that looks like a secret, or that the model does not have, is
28
+ left out.
29
+
30
+ `bin/rails generate active_agent:schema_tools Reservation --managed --returns status`
31
+
32
+ heads the file with a comment saying the ActiveAgent dashboard manages
33
+ it: a project's boots rewrite or remove only files that carry it.
@@ -11,16 +11,25 @@ module ActiveAgent
11
11
  # a judgement about exposure, not a fact about the table (#440). Every
12
12
  # column the model has is listed, commented out, minus the ones that look
13
13
  # like secrets, so the allowlist is a review step rather than a blank page.
14
+ #
15
+ # --filterable and --returns write that review's outcome instead: the
16
+ # columns named are declared, and only those. --managed heads the file
17
+ # with ActiveAgent::SchemaTools::MANAGED_MARKER, which a dashboard
18
+ # project's boots look for before they rewrite or remove it.
14
19
  class SchemaToolsGenerator < ::Rails::Generators::NamedBase
15
20
  source_root File.expand_path("templates", __dir__)
16
21
 
17
- # Columns never suggested, whatever the model: reading one back would
18
- # hand a model a credential, and filtering on one leaks it a character
19
- # at a time through the row counts.
20
- SECRET_COLUMNS = /password|digest|token|secret|api_key|otp|encrypted|ssn/i
22
+ # Columns never suggested or written, whatever the model.
23
+ SECRET_COLUMNS = ActiveAgent::SchemaTools::SECRET_COLUMNS
21
24
 
22
25
  class_option :policy, type: :boolean, default: nil,
23
26
  desc: "Scope every read through <Model>Policy::Scope (default: when that policy exists)"
27
+ class_option :filterable, type: :array, default: nil,
28
+ desc: "Columns an agent may filter on, declared rather than suggested"
29
+ class_option :returns, type: :array, default: nil,
30
+ desc: "Columns an agent may read back, declared rather than suggested"
31
+ class_option :managed, type: :boolean, default: false,
32
+ desc: "Head the file with ActiveAgent::SchemaTools::MANAGED_MARKER, for a dashboard project that rewrites it"
24
33
 
25
34
  check_class_collision suffix: "Tools"
26
35
 
@@ -68,6 +77,32 @@ module ActiveAgent
68
77
  columns.reject { |name, _type| name == "id" || name.match?(SECRET_COLUMNS) }
69
78
  end
70
79
 
80
+ # The columns --filterable or --returns (+option+) declares, after +id+:
81
+ # nil when the option is not given. A column that looks like a secret,
82
+ # or that the model's table does not have when it can be read, is
83
+ # left out with a note.
84
+ def declared_columns(option)
85
+ names = options[option]
86
+ return nil if names.nil?
87
+
88
+ known = columns.map(&:first)
89
+ names.map(&:to_s).uniq.reject { |name| name == "id" }.select do |name|
90
+ if name.match?(SECRET_COLUMNS)
91
+ say_status :skip, "#{name}: looks like a secret, so it is not declared", :yellow
92
+ false
93
+ elsif known.any? && !known.include?(name)
94
+ say_status :skip, "#{name}: #{class_name} has no such column", :yellow
95
+ false
96
+ else
97
+ true
98
+ end
99
+ end
100
+ end
101
+
102
+ def column_list(names)
103
+ [ "id", *names ].map { |name| ":#{name}" }.join(", ")
104
+ end
105
+
71
106
  def secret_columns
72
107
  columns.select { |name, _type| name.match?(SECRET_COLUMNS) }.map(&:first)
73
108
  end
@@ -1,4 +1,9 @@
1
1
  <% module_namespacing do -%>
2
+ <% if options[:managed] -%>
3
+ # <%= ActiveAgent::SchemaTools::MANAGED_MARKER %>: a project's choice of what its
4
+ # App assistant may read rewrites or removes this file on every boot. Delete
5
+ # these three lines to keep your own edits.
6
+ <% end -%>
2
7
  # Bounded, read-only agent tools over <%= class_name %>:
3
8
  # find_<%= collection_name %>, count_<%= collection_name %>, get_<%= resource_name %>.
4
9
  #
@@ -9,19 +14,27 @@
9
14
  class <%= class_name %>Tools < ActiveAgent::SchemaTools
10
15
  model <%= class_name %>
11
16
 
17
+ <% if (declared = declared_columns(:filterable)) -%>
18
+ filterable <%= column_list(declared) %>
19
+ <% else -%>
12
20
  filterable :id
13
21
  <% if suggested_columns.any? -%>
14
22
  # filterable <%= suggested_columns.map { |name, _type| ":#{name}" }.join(", ") %>
15
23
  <% else -%>
16
24
  # filterable :status, :owner_id # columns an agent may filter on
17
25
  <% end -%>
26
+ <% end -%>
18
27
 
28
+ <% if (declared = declared_columns(:returns)) -%>
29
+ returns <%= column_list(declared) %>
30
+ <% else -%>
19
31
  returns :id
20
32
  <% if suggested_columns.any? -%>
21
33
  # returns <%= suggested_columns.map { |name, _type| ":#{name}" }.join(", ") %>
22
34
  <% else -%>
23
35
  # returns :id, :title, :status # columns an agent may read back
24
36
  <% end -%>
37
+ <% end -%>
25
38
  <% if suggested_columns.any? -%>
26
39
  #
27
40
  # Columns and their types:
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activeagent
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.8.1
4
+ version: 1.9.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Justin Bowen
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-10-01 00:00:00.000000000 Z
11
+ date: 2026-10-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: actionpack
@@ -186,6 +186,20 @@ dependencies:
186
186
  - - ">="
187
187
  - !ruby/object:Gem::Version
188
188
  version: '0'
189
+ - !ruby/object:Gem::Dependency
190
+ name: mcp
191
+ requirement: !ruby/object:Gem::Requirement
192
+ requirements:
193
+ - - ">="
194
+ - !ruby/object:Gem::Version
195
+ version: '1.6'
196
+ type: :development
197
+ prerelease: false
198
+ version_requirements: !ruby/object:Gem::Requirement
199
+ requirements:
200
+ - - ">="
201
+ - !ruby/object:Gem::Version
202
+ version: '1.6'
189
203
  - !ruby/object:Gem::Dependency
190
204
  name: openai
191
205
  requirement: !ruby/object:Gem::Requirement
@@ -419,6 +433,7 @@ files:
419
433
  - lib/active_agent/concerns/authorization.rb
420
434
  - lib/active_agent/concerns/callbacks.rb
421
435
  - lib/active_agent/concerns/delegation.rb
436
+ - lib/active_agent/concerns/input_requests.rb
422
437
  - lib/active_agent/concerns/observers.rb
423
438
  - lib/active_agent/concerns/parameterized.rb
424
439
  - lib/active_agent/concerns/preview.rb
@@ -440,6 +455,7 @@ files:
440
455
  - lib/active_agent/delegation/schema.rb
441
456
  - lib/active_agent/deprecator.rb
442
457
  - lib/active_agent/evals.rb
458
+ - lib/active_agent/evals/catalog.rb
443
459
  - lib/active_agent/evals/correlation.rb
444
460
  - lib/active_agent/evals/design_tokens.rb
445
461
  - lib/active_agent/evals/diagnosis.rb
@@ -451,6 +467,7 @@ files:
451
467
  - lib/active_agent/evals/report.rb
452
468
  - lib/active_agent/evals/report_html.rb
453
469
  - lib/active_agent/evals/result.rb
470
+ - lib/active_agent/evals/ruby_llm.rb
454
471
  - lib/active_agent/evals/runner.rb
455
472
  - lib/active_agent/evals/scenario.rb
456
473
  - lib/active_agent/evals/scenario_parser.rb
@@ -459,6 +476,8 @@ files:
459
476
  - lib/active_agent/generation.rb
460
477
  - lib/active_agent/generation_job.rb
461
478
  - lib/active_agent/inline_preview_interceptor.rb
479
+ - lib/active_agent/input_request.rb
480
+ - lib/active_agent/input_request/resume.rb
462
481
  - lib/active_agent/model_capabilities.rb
463
482
  - lib/active_agent/providers/_base_provider.rb
464
483
  - lib/active_agent/providers/anthropic/_types.rb
@@ -492,14 +511,24 @@ files:
492
511
  - lib/active_agent/providers/common/responses/prompt.rb
493
512
  - lib/active_agent/providers/common/usage.rb
494
513
  - lib/active_agent/providers/concerns/exception_handler.rb
514
+ - lib/active_agent/providers/concerns/input_requests.rb
495
515
  - lib/active_agent/providers/concerns/instrumentation.rb
516
+ - lib/active_agent/providers/concerns/mcp_serving.rb
496
517
  - lib/active_agent/providers/concerns/previewable.rb
497
518
  - lib/active_agent/providers/concerns/tool_choice_clearing.rb
519
+ - lib/active_agent/providers/deep_seek_provider.rb
520
+ - lib/active_agent/providers/deepseek/_types.rb
521
+ - lib/active_agent/providers/deepseek/chat/_types.rb
522
+ - lib/active_agent/providers/deepseek/chat/request.rb
523
+ - lib/active_agent/providers/deepseek/options.rb
524
+ - lib/active_agent/providers/deepseek_provider.rb
498
525
  - lib/active_agent/providers/errors.rb
499
526
  - lib/active_agent/providers/gemini/_types.rb
500
527
  - lib/active_agent/providers/gemini/options.rb
501
528
  - lib/active_agent/providers/gemini_provider.rb
502
529
  - lib/active_agent/providers/log_subscriber.rb
530
+ - lib/active_agent/providers/mcp_bridge.rb
531
+ - lib/active_agent/providers/mcp_tool_cache.rb
503
532
  - lib/active_agent/providers/mock/_types.rb
504
533
  - lib/active_agent/providers/mock/embedding_request.rb
505
534
  - lib/active_agent/providers/mock/messages/_types.rb