activeagent 1.8.1 → 1.9.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/CHANGELOG.md +1044 -0
- data/README.md +9 -1
- data/lib/active_agent/base.rb +22 -1
- data/lib/active_agent/concerns/delegation.rb +4 -0
- data/lib/active_agent/concerns/input_requests.rb +178 -0
- data/lib/active_agent/concerns/parameterized.rb +7 -4
- data/lib/active_agent/concerns/provider.rb +1 -0
- data/lib/active_agent/concerns/tooling.rb +7 -2
- data/lib/active_agent/delegation/runner.rb +31 -0
- data/lib/active_agent/evals/catalog.rb +305 -0
- data/lib/active_agent/evals/ruby_llm.rb +411 -0
- data/lib/active_agent/evals/scenario.rb +10 -3
- data/lib/active_agent/evals/scenario_parser.rb +38 -10
- data/lib/active_agent/evals.rb +1 -0
- data/lib/active_agent/generation.rb +62 -2
- data/lib/active_agent/generation_job.rb +14 -3
- data/lib/active_agent/input_request/resume.rb +199 -0
- data/lib/active_agent/input_request.rb +279 -0
- data/lib/active_agent/providers/_base_provider.rb +153 -31
- data/lib/active_agent/providers/anthropic/transforms.rb +57 -28
- data/lib/active_agent/providers/anthropic_provider.rb +102 -20
- data/lib/active_agent/providers/common/responses/prompt.rb +21 -0
- data/lib/active_agent/providers/concerns/input_requests.rb +332 -0
- data/lib/active_agent/providers/concerns/mcp_serving.rb +267 -0
- data/lib/active_agent/providers/concerns/tool_choice_clearing.rb +18 -3
- data/lib/active_agent/providers/deep_seek_provider.rb +6 -0
- data/lib/active_agent/providers/deepseek/_types.rb +4 -0
- data/lib/active_agent/providers/deepseek/chat/_types.rb +54 -0
- data/lib/active_agent/providers/deepseek/chat/request.rb +121 -0
- data/lib/active_agent/providers/deepseek/options.rb +53 -0
- data/lib/active_agent/providers/deepseek_provider.rb +46 -0
- data/lib/active_agent/providers/log_subscriber.rb +13 -0
- data/lib/active_agent/providers/mcp_bridge.rb +691 -0
- data/lib/active_agent/providers/mcp_tool_cache.rb +230 -0
- data/lib/active_agent/providers/open_ai/_base.rb +1 -1
- data/lib/active_agent/providers/open_ai/chat_provider.rb +22 -3
- data/lib/active_agent/providers/open_ai/options.rb +1 -1
- data/lib/active_agent/providers/open_ai/responses_provider.rb +68 -15
- data/lib/active_agent/providers/open_router/options.rb +6 -2
- data/lib/active_agent/providers/ruby_llm/messages/_types.rb +3 -1
- data/lib/active_agent/providers/ruby_llm/options.rb +5 -0
- data/lib/active_agent/providers/ruby_llm_provider.rb +127 -29
- data/lib/active_agent/schema_tools.rb +12 -0
- data/lib/active_agent/telemetry/instrumentation.rb +10 -1
- data/lib/active_agent/version.rb +1 -1
- data/lib/active_agent.rb +2 -0
- data/lib/generators/active_agent/schema_tools/USAGE +11 -0
- data/lib/generators/active_agent/schema_tools/schema_tools_generator.rb +39 -4
- data/lib/generators/active_agent/schema_tools/templates/schema_tools.rb.tt +13 -0
- 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
|
|
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
|
-
|
|
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
|
-
#
|
|
149
|
-
#
|
|
150
|
-
|
|
151
|
-
|
|
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] +=
|
|
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:
|
|
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
|
-
#
|
|
170
|
-
#
|
|
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
|
|
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
|
|
250
|
-
|
|
251
|
-
|
|
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
|
|
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
|
-
|
|
410
|
-
|
|
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
|
-
|
|
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] =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
data/lib/active_agent/version.rb
CHANGED
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
|
|
18
|
-
|
|
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.
|
|
4
|
+
version: 1.9.0
|
|
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-
|
|
11
|
+
date: 2026-10-15 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
|