activeagent 1.9.7 → 1.9.8
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 +177 -1
- data/README.md +3 -0
- data/lib/active_agent/cli/eval_commands.rb +47 -4
- data/lib/active_agent/concerns/tooling.rb +4 -2
- data/lib/active_agent/evals/capture.rb +14 -3
- data/lib/active_agent/model_capabilities.rb +1 -1
- data/lib/active_agent/providers/_base_provider.rb +1 -1
- data/lib/active_agent/providers/anthropic_provider.rb +10 -0
- data/lib/active_agent/providers/bedrock/bearer_client.rb +2 -1
- data/lib/active_agent/providers/common/usage.rb +6 -1
- data/lib/active_agent/providers/open_ai/responses_provider.rb +7 -0
- data/lib/active_agent/providers/requesty_provider.rb +8 -0
- data/lib/active_agent/providers/ruby_llm/tool_proxy.rb +15 -0
- data/lib/active_agent/providers/ruby_llm_provider.rb +41 -5
- data/lib/active_agent/providers/runcrate/_types.rb +17 -0
- data/lib/active_agent/providers/runcrate/options.rb +39 -0
- data/lib/active_agent/providers/runcrate_provider.rb +72 -0
- data/lib/active_agent/version.rb +1 -1
- metadata +13 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 560139e6515a8b3568aabc383ec0ae4219764a2d8ca91b14b4defc4fdec3439d
|
|
4
|
+
data.tar.gz: b1d8651cb0a058b8922e579374720efd42c3babcadfddc3c123c5b7c6c2afd8b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 722dcbfe0e8bc2254871264f42ddf5394eb4f26acfc47f53ce17f2c9b382db627248d90ded1c9164a30bd176f908ee4510db11332e26bd6e25c91d485b251fde
|
|
7
|
+
data.tar.gz: e97465f43ad9dfee0eecfa0e3b3207b7c3a67dc620a1fa656c451800b9e49ca89259a75dd7ce7ad7066f05bfeece5d719fdf869e30a3cf89ae11f6e6f9de42dd
|
data/CHANGELOG.md
CHANGED
|
@@ -5,7 +5,183 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
-
## [
|
|
8
|
+
## [1.9.8] - 2026-11-12
|
|
9
|
+
|
|
10
|
+
The dashboard holds a run to its catalog's pass bars: a product's and a
|
|
11
|
+
set's `pass` blocks and a product's named criteria lists survive import, a
|
|
12
|
+
run records the bars it started under and shows each group's gate beside
|
|
13
|
+
its pass rate, and `evals run` exits 4 when a gated group misses, so CI
|
|
14
|
+
fails on the group that slipped rather than on the whole-run average. A
|
|
15
|
+
catalog that declares a `threshold` now scores at it instead of the fixed
|
|
16
|
+
0.7. Around that: the empty agent list shows the ways an agent arrives,
|
|
17
|
+
telemetry first; the RubyLLM provider works with ruby_llm 2.2 and the
|
|
18
|
+
Anthropic provider with the `anthropic` gem 2.x; Runcrate joins the
|
|
19
|
+
OpenAI-compatible providers; and smaller fixes to Anthropic usage counts,
|
|
20
|
+
Requesty embeddings, `top_k` for thinking-first models, late cancels,
|
|
21
|
+
`tool_observer` under Capture and Responses streaming errors.
|
|
22
|
+
|
|
23
|
+
Upgrading: two dashboard migrations, 034 `add_catalog_product_criteria`
|
|
24
|
+
(the `criteria` column on scenario products) and 035
|
|
25
|
+
`add_catalog_pass_bars` (the `pass` column on scenario products and sets):
|
|
26
|
+
run `bin/rails generate action_agent:install --skip` and `bin/rails
|
|
27
|
+
db:migrate`. Until they run, catalog imports keep working but still drop
|
|
28
|
+
named criteria lists and pass bars, so dashboard runs stay ungated; a
|
|
29
|
+
catalog imported before the upgrade needs importing again to gain them.
|
|
30
|
+
033 is left free for a later release. A finished run that carries gates
|
|
31
|
+
now fails `evals run --wait` with exit 4 when any gated group missed its
|
|
32
|
+
bar; a run without gates is reported as before. ruby_llm 2.2.0 (2026-11-10)
|
|
33
|
+
breaks the RubyLLM provider in 1.9.7 and earlier (every tool call reads as
|
|
34
|
+
`nil`); upgrade, or hold `ruby_llm < 2.2` until you do.
|
|
35
|
+
|
|
36
|
+
### Added
|
|
37
|
+
|
|
38
|
+
- **The dashboard shows a run's per-group pass gates** (`actionagent`). A
|
|
39
|
+
published report's `pass_thresholds` are kept with the run, so the
|
|
40
|
+
rebuilt report, the export and the run page gate the stored results as
|
|
41
|
+
the application's report did, and a run started from the dashboard on a
|
|
42
|
+
catalog set is held to the set's and product's `pass` bars, recorded as
|
|
43
|
+
the run starts so a later catalog change does not regrade it. The run
|
|
44
|
+
page adds a **Pass rate by group** table, and the run's badge reads
|
|
45
|
+
`gate missed` when a gate was not met even if the whole-run pass rate
|
|
46
|
+
looks fine. The run's JSON adds `gates` and `gates_met`; a run without
|
|
47
|
+
bars reads as before (#678).
|
|
48
|
+
- **An empty agent list shows how agents arrive** (`actionagent`). With no
|
|
49
|
+
agents, the Agents page offered only Browse Templates and Create From
|
|
50
|
+
Scratch. It now shows three ways in, telemetry first:
|
|
51
|
+
- **Connect your app** asks what runs the agents. For ActiveAgent it shows
|
|
52
|
+
the `config/active_agent.yml` telemetry block. For RubyLLM it shows the
|
|
53
|
+
`activeagents-telemetry-ruby_llm` Gemfile line and the initializer that
|
|
54
|
+
subscribes it. Both point at this dashboard's endpoint and read the key
|
|
55
|
+
from `ACTIVEAGENTS_API_KEY`. The first trace registers the agent, and
|
|
56
|
+
**Check for agents** reloads the list.
|
|
57
|
+
- **Link a GitHub repository** opens New project.
|
|
58
|
+
- **Build one here** keeps the template library and the builder.
|
|
59
|
+
- **Runcrate as an OpenAI-compatible provider** (`activeagent`).
|
|
60
|
+
`service: "Runcrate"` generates and embeds through
|
|
61
|
+
`https://api.runcrate.ai/v1` (open-weight models such as
|
|
62
|
+
`deepseek-ai/DeepSeek-V4.1-Flash` and `Qwen/Qwen3-Embedding-8B`), with the
|
|
63
|
+
key from `RUNCRATE_API_KEY`; a thin subclass of the OpenAI provider, like
|
|
64
|
+
Cheaper Inference.
|
|
65
|
+
|
|
66
|
+
### Fixed
|
|
67
|
+
|
|
68
|
+
- **The RubyLLM provider works with ruby_llm 2.2** (`activeagent`). RubyLLM
|
|
69
|
+
2.2.0 (2026-11-10) changed three things the provider leaned on. A
|
|
70
|
+
message's `tool_calls` became a `ToolCall::Collection` that yields the
|
|
71
|
+
calls rather than ID-and-call pairs, so every tool call read as `nil`; a
|
|
72
|
+
streamed delta now carries the provider's call ID in `provider_call_id`
|
|
73
|
+
and leaves `id` empty, so every fragment joined the latest call and
|
|
74
|
+
arguments doubled up; and a chat asks each tool for its `namespace`,
|
|
75
|
+
`qualified_name` and `deferred?`, which ActiveAgent's tool proxy did not
|
|
76
|
+
answer. The provider now iterates either shape, keys streamed fragments
|
|
77
|
+
by the provider's ID, and the proxy answers the three. ruby_llm 1.16
|
|
78
|
+
through 2.1 behave as before. The evals conversation check also learned
|
|
79
|
+
2.2's `acts_as_chat` modules, columns and reworded unavailable-tool error.
|
|
80
|
+
- **`evals run` fails CI on a missed group gate** (`activeagent`). A run
|
|
81
|
+
whose critical group slipped to 50% passed `evals run --min-pass-rate`
|
|
82
|
+
whenever the other groups lifted the whole-run pass rate over the bar.
|
|
83
|
+
A finished run that the dashboard holds to per-group pass gates now fails
|
|
84
|
+
(exit 4) when any gated group missed its bar, with `--wait` alone or with
|
|
85
|
+
`--min-pass-rate`, which stays the whole-run bar; each missed gate prints
|
|
86
|
+
as one line, and `--json` adds `gates`. A run without gates is gated as
|
|
87
|
+
before (#677).
|
|
88
|
+
- **Responses streaming failures retain their error details** (`activeagent`).
|
|
89
|
+
`error` and `response.failed` events raise a provider error with the API
|
|
90
|
+
code and message instead of an unexpected chunk type error.
|
|
91
|
+
- **The dashboard keeps a catalog's pass bars** (`actionagent`). A
|
|
92
|
+
product's and a set's `pass` blocks (`min_pass_rate`, `threshold`,
|
|
93
|
+
`gated`) parsed in `ActiveAgent::Evals::Catalog`, but neither
|
|
94
|
+
`ScenarioProduct` nor `ScenarioSet` kept them: an import dropped the
|
|
95
|
+
bars, the export left them out, the stored catalog's `pass_thresholds`
|
|
96
|
+
fell back to the defaults (every group gated at 100%), and the import
|
|
97
|
+
preview reported the product and its sets as `update` on every re-import
|
|
98
|
+
of an unchanged file. The bars are now stored and exported as imported,
|
|
99
|
+
and both import modes take the document's. Existing installs add the
|
|
100
|
+
columns with `bin/rails generate action_agent:install --skip` and
|
|
101
|
+
`bin/rails db:migrate` (`add_catalog_pass_bars`); imports keep working,
|
|
102
|
+
without the bars, until it runs. Bars dropped by an earlier version come
|
|
103
|
+
back when the original document is imported again.
|
|
104
|
+
- **The dashboard keeps a catalog product's named criteria lists**
|
|
105
|
+
(`actionagent`). A product's `criteria: { shared: [...], strict: [...] }`
|
|
106
|
+
parsed and round-tripped in `ActiveAgent::Evals::Catalog`, but
|
|
107
|
+
`ScenarioProduct` had nowhere to keep it: an import dropped the lists, the
|
|
108
|
+
export left them out, the stored digest never matched the records'
|
|
109
|
+
export, and the import preview reported the product as `update` on every
|
|
110
|
+
re-import of an unchanged file. The lists are now stored on the product
|
|
111
|
+
and exported as imported, so the documents and digests match and an
|
|
112
|
+
unchanged file previews as unchanged. Both import modes take the
|
|
113
|
+
document's lists, as they take a set's `criteria`. A set still runs on
|
|
114
|
+
its own `criteria`; which product lists a run uses is left for later.
|
|
115
|
+
Existing installs add the column with
|
|
116
|
+
`bin/rails generate action_agent:install --skip` and `bin/rails db:migrate`
|
|
117
|
+
(`add_catalog_product_criteria`); imports keep working, without the lists,
|
|
118
|
+
until it runs. Lists dropped by an earlier version come back when the
|
|
119
|
+
original document is imported again (#687).
|
|
120
|
+
- **A self-hosted dashboard shows its own trace endpoint** (`actionagent`).
|
|
121
|
+
The Organization page's Telemetry card showed the platform's `/v1/traces`.
|
|
122
|
+
A self-hosted engine takes traces at `<mount>/api/traces`, and the card
|
|
123
|
+
now shows that. Multi-tenant installs are unchanged.
|
|
124
|
+
- **The agent list's filter arrows sit inside the select** (`actionagent`).
|
|
125
|
+
The browser's own arrow ignores padding and sat against the right border.
|
|
126
|
+
The three filters now draw an inset chevron (`aa-select`).
|
|
127
|
+
- **The Anthropic provider loads the `anthropic` gem 2.x** (`activeagent`).
|
|
128
|
+
The provider pinned its client gem to `~> 1.12`, so a bundle holding
|
|
129
|
+
anthropic 2.0 or later raised `LoadError` the moment the provider loaded,
|
|
130
|
+
although the provider builds its requests as hashes and the 2.0 SDK
|
|
131
|
+
(role-union `MessageParam`, one-value enums as constants, Text Completions
|
|
132
|
+
removed) breaks none of them. The provider and the gemspec's development
|
|
133
|
+
dependency now accept anthropic 1.12 through 2.x, and CI runs the provider
|
|
134
|
+
tests against 2.3 beside the 1.12, 1.14 and 1.55 legs. Two 2.0 changes do
|
|
135
|
+
reach the code: Text Completions went away with
|
|
136
|
+
`Anthropic::Resources::Completions`, which the Bedrock bearer client now
|
|
137
|
+
sets up only when it exists, and the client sends its key as
|
|
138
|
+
`Authorization: Bearer` instead of `x-api-key`. The 2.0 SDK also dropped
|
|
139
|
+
the `?beta=true` query string from its beta Messages requests, so the test
|
|
140
|
+
suite matches recorded cassettes with or without it.
|
|
141
|
+
- **A streamed Anthropic response reports its token usage**
|
|
142
|
+
(`activeagent`). With `stream: true`, `response.usage` was nil, so a
|
|
143
|
+
streamed generation counted 0 tokens and cost nothing. The provider now
|
|
144
|
+
records the usage `message_start` reports and the cumulative counts
|
|
145
|
+
`message_delta` carries, as the OpenAI providers do from their chunks.
|
|
146
|
+
- **Anthropic's `total_tokens` counts cached tokens** (`activeagent`).
|
|
147
|
+
Anthropic's `input_tokens` leaves out cache reads and writes, which it
|
|
148
|
+
reports separately, so `total_tokens` undercounted any prompt that used
|
|
149
|
+
the cache. It is now input + output + cache read + cache creation. The
|
|
150
|
+
other counts are unchanged, and so is cost: pricing still reads
|
|
151
|
+
`input_tokens` and `output_tokens` only.
|
|
152
|
+
- **Added usage keeps its duration** (`activeagent`). `Usage#+` dropped
|
|
153
|
+
`duration_ms`, so the usage of a tool-calling loop (Ollama reports a
|
|
154
|
+
duration) came back without one. It is now summed like the other
|
|
155
|
+
optional counts.
|
|
156
|
+
- **Requesty embeddings work** (`activeagent`). The Requesty provider
|
|
157
|
+
declared an embedding request but never sent one, so `embed` raised
|
|
158
|
+
`NotImplementedError`. It now calls Requesty's OpenAI-compatible
|
|
159
|
+
`/v1/embeddings`.
|
|
160
|
+
- **`top_k` is stripped for thinking-first models** (`activeagent`).
|
|
161
|
+
`ModelCapabilities` removed `temperature` and `top_p` for the models that
|
|
162
|
+
reject sampling parameters, but not `top_k`, which they reject too, so an
|
|
163
|
+
agent configured with it still got a 400.
|
|
164
|
+
- **A late cancel leaves a finished run as it finished** (`actionagent`).
|
|
165
|
+
`AgentRun#cancel!` checked the status of its own copy of the run and
|
|
166
|
+
then wrote `cancelled` unconditionally. A cancel request that loaded the
|
|
167
|
+
run while it was running, and wrote after the worker had recorded its
|
|
168
|
+
result, turned a complete run into a cancelled one that still carried
|
|
169
|
+
its output, and replaced a failed run's error with "Cancelled by user".
|
|
170
|
+
The cancel now moves the run to `cancelled` in one conditional update,
|
|
171
|
+
as the execution job claims a pending run, and only from pending,
|
|
172
|
+
running or awaiting input. A run that finished first keeps its status,
|
|
173
|
+
output, error and completion time; `cancel!` returns false and reloads
|
|
174
|
+
the copy, so the API answers with the status the run ended in. A cancel
|
|
175
|
+
is broadcast once, where the callback and the method each sent it
|
|
176
|
+
before. Cancelling still does not stop a provider call already in
|
|
177
|
+
flight; it keeps that call's result from being recorded.
|
|
178
|
+
- **Capture keeps an agent's own `tool_observer`** (`activeagent`). Since
|
|
179
|
+
1.9.7, `Evals::Capture` records client-side MCP calls through the
|
|
180
|
+
agent's `tool_observer`, and it replaced an observer the agent defined
|
|
181
|
+
itself, so that observer stopped running whenever the agent was
|
|
182
|
+
captured. Capture now runs the agent's observer around the call it
|
|
183
|
+
records: the server is still called once, both see the same result or
|
|
184
|
+
exception, and a call the observer withholds is not recorded as made.
|
|
9
185
|
|
|
10
186
|
## [1.9.7] - 2026-11-04
|
|
11
187
|
|
data/README.md
CHANGED
|
@@ -86,8 +86,8 @@ module ActiveAgent
|
|
|
86
86
|
p.on("--keys LIST", "Replay only these scenario keys, comma-separated") { |v| arguments[:keys] = list_option(v) }
|
|
87
87
|
p.on("--group GROUP", "Replay only this scenario group") { |v| arguments[:group] = v }
|
|
88
88
|
p.on("--sandbox ID", "Replay against this ready checkout sandbox") { |v| arguments[:sandbox_id] = v }
|
|
89
|
-
p.on("--wait", "Wait for the run to finish") { wait = true }
|
|
90
|
-
p.on("--min-pass-rate RATE", "
|
|
89
|
+
p.on("--wait", "Wait for the run to finish; fail (exit #{EXIT_GATE}) when it failed or missed a pass gate") { wait = true }
|
|
90
|
+
p.on("--min-pass-rate RATE", "Also fail below this whole-run pass rate: 0.8 or 80%; implies --wait") do |v|
|
|
91
91
|
min_pass_rate = pass_rate_option(v)
|
|
92
92
|
wait = true
|
|
93
93
|
end
|
|
@@ -199,18 +199,28 @@ module ActiveAgent
|
|
|
199
199
|
end
|
|
200
200
|
end
|
|
201
201
|
|
|
202
|
+
# A finished run fails the gate when it failed, when its whole pass
|
|
203
|
+
# rate is under --min-pass-rate, or when it was held to per-group pass
|
|
204
|
+
# gates (the run's "gates", from the suite's or catalog set's `pass`
|
|
205
|
+
# bars) and the dashboard says it did not meet every one. A run
|
|
206
|
+
# without gates is gated on its status and rate alone, as it was.
|
|
202
207
|
def gate(payload, min_pass_rate)
|
|
203
208
|
run = payload.fetch("run", {})
|
|
204
209
|
rate = pass_rate(run)
|
|
205
210
|
failed = run["status"] == "failed"
|
|
206
211
|
below = min_pass_rate && (rate.nil? || rate < min_pass_rate)
|
|
207
|
-
|
|
212
|
+
gates = run_gates(run)
|
|
213
|
+
missed = !gates.nil? && run["gates_met"] != true
|
|
214
|
+
verdict = failed || below || missed ? "fail" : "pass"
|
|
208
215
|
failures = payload.fetch("results", [])
|
|
216
|
+
summary = { "pass_rate" => rate, "min_pass_rate" => min_pass_rate }
|
|
217
|
+
summary["gates"] = gates if gates
|
|
209
218
|
|
|
210
|
-
emit(payload.merge("
|
|
219
|
+
emit(payload.merge(summary).merge("verdict" => verdict)) do
|
|
211
220
|
@out.puts "Run #{run["id"]} #{run["status"]}: #{passed(run) || "no results"}" \
|
|
212
221
|
"#{rate ? format(" (%.1f%%)", rate * 100) : ""}#{min_pass_rate ? format(", threshold %.1f%%", min_pass_rate * 100) : ""}."
|
|
213
222
|
@out.puts "Error: #{run["error_message"]}" if run["error_message"]
|
|
223
|
+
print_gates(gates, missed) if gates
|
|
214
224
|
unless failures.empty?
|
|
215
225
|
@out.puts "Failed results:"
|
|
216
226
|
table(failures, {
|
|
@@ -227,6 +237,39 @@ module ActiveAgent
|
|
|
227
237
|
verdict == "pass" ? EXIT_SUCCESS : EXIT_GATE
|
|
228
238
|
end
|
|
229
239
|
|
|
240
|
+
# The run's per-group pass gates as the dashboard worked them out
|
|
241
|
+
# (EvaluationRun#gates), or nil for a run held to none.
|
|
242
|
+
def run_gates(run)
|
|
243
|
+
rows = run["gates"]
|
|
244
|
+
rows.select { |row| row.is_a?(Hash) } if rows.is_a?(Array)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def print_gates(gates, missed)
|
|
248
|
+
unmet = gates.reject { |gate| gate["met"] == true }
|
|
249
|
+
unless missed
|
|
250
|
+
@out.puts "Gates: every gate met (#{gates.size})."
|
|
251
|
+
return
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
@out.puts(unmet.empty? ? "Gates: no gate was reached." : "Gates not met:")
|
|
255
|
+
unmet.each { |gate| @out.puts " #{gate_sentence(gate)}" }
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# A gate the run missed, as ActiveAgent::Evals::Report::Gate#to_s
|
|
259
|
+
# reads it: "Orders · gpt-5-mini: 1/2 (50%) passed, below 100%".
|
|
260
|
+
def gate_sentence(gate)
|
|
261
|
+
passed = gate["passed"].to_i
|
|
262
|
+
total = gate["total"].to_i
|
|
263
|
+
passes = total.positive? ? "#{passed}/#{total} (#{whole_percent(passed.to_f / total)})" : "no results"
|
|
264
|
+
label = cell(gate["label"]) unless gate["label"].to_s.empty?
|
|
265
|
+
"#{cell(gate["name"] || gate["group"] || "All")}#{label ? " · #{label}" : ""}: #{passes} passed, below #{whole_percent(gate["min_pass_rate"])}"
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def whole_percent(fraction)
|
|
269
|
+
value = Float(fraction, exception: false)
|
|
270
|
+
value ? "#{(value * 100).round}%" : "?"
|
|
271
|
+
end
|
|
272
|
+
|
|
230
273
|
def pass_rate(run)
|
|
231
274
|
evaluated = run["samples_evaluated"].to_i
|
|
232
275
|
evaluated.positive? ? run["samples_passed"].to_f / evaluated : nil
|
|
@@ -30,8 +30,10 @@ module ActiveAgent
|
|
|
30
30
|
# A callback the provider runs the calls it serves from an MCP server
|
|
31
31
|
# around, which never reach {#tools_function}. It receives the tool name,
|
|
32
32
|
# its arguments and a block that makes the call, and returns the block's
|
|
33
|
-
# result. Nil unless
|
|
34
|
-
#
|
|
33
|
+
# result. Nil unless the agent defines one, to audit its MCP calls say,
|
|
34
|
+
# or a host that records a generation's tool calls sets it on the
|
|
35
|
+
# instance. Evals::Capture keeps an agent's own observer and records the
|
|
36
|
+
# call that observer makes.
|
|
35
37
|
#
|
|
36
38
|
# @return [Proc, nil]
|
|
37
39
|
def tool_observer = nil
|
|
@@ -19,8 +19,10 @@ module ActiveAgent
|
|
|
19
19
|
# (`mcp_strategy: :client`, or a provider with no MCP of its own) is
|
|
20
20
|
# dispatched before that proc, so the provider reports it through the
|
|
21
21
|
# agent's `tool_observer` instead, with a failure the server reported
|
|
22
|
-
# marked as one.
|
|
23
|
-
#
|
|
22
|
+
# marked as one. An observer the agent defines itself still runs, around
|
|
23
|
+
# the call that is recorded. A tool a provider runs on its own side
|
|
24
|
+
# (Anthropic's or OpenAI's hosted MCP) never reaches this process and is
|
|
25
|
+
# not recorded.
|
|
24
26
|
#
|
|
25
27
|
# `model:` and `provider:` replay the same action under another model,
|
|
26
28
|
# the way a comparison run does. A model is an option on the agent
|
|
@@ -190,9 +192,15 @@ module ActiveAgent
|
|
|
190
192
|
# the provider runs that call inside the agent's `tool_observer`, which
|
|
191
193
|
# records it the same way. The two paths are disjoint, so a call is
|
|
192
194
|
# recorded once, and the observer sees the call without making it again.
|
|
195
|
+
#
|
|
196
|
+
# An observer the agent already has is kept, as tools_function's `base`
|
|
197
|
+
# is: it runs around the call this records, so it sees the same name,
|
|
198
|
+
# arguments, result and exception, and a call it does not make is not
|
|
199
|
+
# recorded as made.
|
|
193
200
|
def record_tool_calls(agent)
|
|
194
201
|
calls = @tool_calls
|
|
195
202
|
base = agent.tools_function
|
|
203
|
+
observer = agent.tool_observer
|
|
196
204
|
capture = self.class
|
|
197
205
|
|
|
198
206
|
agent.define_singleton_method(:tools_function) do
|
|
@@ -203,7 +211,10 @@ module ActiveAgent
|
|
|
203
211
|
end
|
|
204
212
|
|
|
205
213
|
agent.define_singleton_method(:tool_observer) do
|
|
206
|
-
proc { |name, arguments, &call| capture.record_call(calls, name, arguments.presence, &call) }
|
|
214
|
+
record = proc { |name, arguments, &call| capture.record_call(calls, name, arguments.presence, &call) }
|
|
215
|
+
next record unless observer
|
|
216
|
+
|
|
217
|
+
proc { |name, arguments, &call| observer.call(name, arguments) { record.call(name, arguments, &call) } }
|
|
207
218
|
end
|
|
208
219
|
end
|
|
209
220
|
|
|
@@ -17,7 +17,7 @@ module ActiveAgent
|
|
|
17
17
|
# @example Disable sanitization entirely
|
|
18
18
|
# ActiveAgent::ModelCapabilities.enabled = false
|
|
19
19
|
module ModelCapabilities
|
|
20
|
-
SAMPLING_PARAMS = [ :temperature, :top_p ].freeze
|
|
20
|
+
SAMPLING_PARAMS = [ :temperature, :top_p, :top_k ].freeze
|
|
21
21
|
|
|
22
22
|
# Model families that reject sampling parameters with a 400:
|
|
23
23
|
# - Anthropic thinking-first models (Opus 4.7+, Opus 5, Sonnet 5,
|
|
@@ -10,7 +10,7 @@ require_relative "concerns/tool_choice_clearing"
|
|
|
10
10
|
|
|
11
11
|
# @private
|
|
12
12
|
GEM_LOADERS = {
|
|
13
|
-
anthropic: [ "anthropic", "
|
|
13
|
+
anthropic: [ "anthropic", [ ">= 1.12", "< 3" ], "anthropic" ],
|
|
14
14
|
openai: [ "openai", "~> 0.34", "openai" ],
|
|
15
15
|
# Keep a tested floor: 1.2 lacks the provider APIs this adapter uses.
|
|
16
16
|
ruby_llm: [ "ruby_llm", [ ">= 1.16", "< 3" ], "ruby_llm" ]
|
|
@@ -197,6 +197,7 @@ module ActiveAgent
|
|
|
197
197
|
when :message_start
|
|
198
198
|
api_message = Anthropic::Transforms.gem_to_hash(api_response_chunk.message)
|
|
199
199
|
self.message_stack.push(api_message)
|
|
200
|
+
record_stream_usage(api_message[:usage])
|
|
200
201
|
broadcast_stream_update(message_stack.last)
|
|
201
202
|
|
|
202
203
|
# -> Content Block Create
|
|
@@ -239,6 +240,15 @@ module ActiveAgent
|
|
|
239
240
|
self.message_stack.last.merge!(delta)
|
|
240
241
|
self.stream_finish_reason = delta[:stop_reason] if delta[:stop_reason]
|
|
241
242
|
|
|
243
|
+
# message_delta carries the cumulative output tokens (and, on newer
|
|
244
|
+
# API versions, refreshed input counts); layer them over the usage
|
|
245
|
+
# message_start reported so the turn keeps a single running total.
|
|
246
|
+
delta_usage = Anthropic::Transforms.gem_to_hash(api_response_chunk.usage)&.compact
|
|
247
|
+
if delta_usage.present?
|
|
248
|
+
message_stack.last[:usage] = (message_stack.last[:usage] || {}).merge(delta_usage)
|
|
249
|
+
record_stream_usage(message_stack.last[:usage])
|
|
250
|
+
end
|
|
251
|
+
|
|
242
252
|
# Message Completed [Full Message]
|
|
243
253
|
when :message_stop
|
|
244
254
|
api_message = Anthropic::Transforms.gem_to_hash(api_response_chunk.message)
|
|
@@ -51,7 +51,8 @@ module ActiveAgent
|
|
|
51
51
|
)
|
|
52
52
|
|
|
53
53
|
@messages = ::Anthropic::Resources::Messages.new(client: self)
|
|
54
|
-
|
|
54
|
+
# The anthropic gem 2.0 removed Text Completions along with this resource.
|
|
55
|
+
@completions = ::Anthropic::Resources::Completions.new(client: self) if defined?(::Anthropic::Resources::Completions)
|
|
55
56
|
@beta = ::Anthropic::Resources::Beta.new(client: self)
|
|
56
57
|
end
|
|
57
58
|
|
|
@@ -148,7 +148,8 @@ module ActiveAgent
|
|
|
148
148
|
cached_tokens: sum_optional(self.cached_tokens, other.cached_tokens),
|
|
149
149
|
cache_creation_tokens: sum_optional(self.cache_creation_tokens, other.cache_creation_tokens),
|
|
150
150
|
reasoning_tokens: sum_optional(self.reasoning_tokens, other.reasoning_tokens),
|
|
151
|
-
audio_tokens: sum_optional(self.audio_tokens, other.audio_tokens)
|
|
151
|
+
audio_tokens: sum_optional(self.audio_tokens, other.audio_tokens),
|
|
152
|
+
duration_ms: sum_optional(self.duration_ms, other.duration_ms)
|
|
152
153
|
)
|
|
153
154
|
end
|
|
154
155
|
|
|
@@ -263,6 +264,10 @@ module ActiveAgent
|
|
|
263
264
|
**usage.slice(:input_tokens, :output_tokens, :service_tier),
|
|
264
265
|
input_tokens: usage[:input_tokens] || 0,
|
|
265
266
|
output_tokens: usage[:output_tokens] || 0,
|
|
267
|
+
# Anthropic's input_tokens excludes cache reads and writes, which are
|
|
268
|
+
# reported separately, so they are added to get every token processed.
|
|
269
|
+
total_tokens: (usage[:input_tokens] || 0) + (usage[:output_tokens] || 0) +
|
|
270
|
+
(usage[:cache_read_input_tokens] || 0) + (usage[:cache_creation_input_tokens] || 0),
|
|
266
271
|
cached_tokens: usage[:cache_read_input_tokens],
|
|
267
272
|
cache_creation_tokens: usage[:cache_creation_input_tokens],
|
|
268
273
|
provider_details: usage.slice(:cache_creation, :server_tool_use).compact
|
|
@@ -110,6 +110,7 @@ module ActiveAgent
|
|
|
110
110
|
# reasoning progress, kept from the reasoning item once it is done
|
|
111
111
|
# - `:"response.content_part.done"` - content part completed
|
|
112
112
|
# - `:"response.output_item.done"` - message, reasoning or function call completed
|
|
113
|
+
# - `:error`, `:"response.failed"` - provider failure
|
|
113
114
|
# - `:"response.completed"` - response finished
|
|
114
115
|
# - `:"response.incomplete"` - response cut short (e.g. max_output_tokens)
|
|
115
116
|
#
|
|
@@ -174,6 +175,12 @@ module ActiveAgent
|
|
|
174
175
|
when :"response.output_item.done"
|
|
175
176
|
process_stream_output_item_done(api_response_event)
|
|
176
177
|
|
|
178
|
+
# Errors arrive as events even though the HTTP request succeeded.
|
|
179
|
+
when :error, :"response.failed"
|
|
180
|
+
error = api_response_event.type == :error ? api_response_event : api_response_event.response.error
|
|
181
|
+
message = [ error&.code, error&.message ].compact.join(": ")
|
|
182
|
+
raise Errors::ProviderError.new(message.presence || "OpenAI response failed", provider_tag: tag_name)
|
|
183
|
+
|
|
177
184
|
# Response Completed, or cut short (e.g. by max_output_tokens) with
|
|
178
185
|
# the output it has so far
|
|
179
186
|
when :"response.completed", :"response.incomplete"
|
|
@@ -50,6 +50,14 @@ module ActiveAgent
|
|
|
50
50
|
|
|
51
51
|
protected
|
|
52
52
|
|
|
53
|
+
# Executes an embedding request via Requesty's OpenAI-compatible API.
|
|
54
|
+
#
|
|
55
|
+
# @param parameters [Hash]
|
|
56
|
+
# @return [Hash] symbolized API response
|
|
57
|
+
def api_embed_execute(parameters)
|
|
58
|
+
client.embeddings.create(**parameters).as_json.deep_symbolize_keys
|
|
59
|
+
end
|
|
60
|
+
|
|
53
61
|
# @see BaseProvider#api_response_normalize
|
|
54
62
|
# @param api_response [OpenAI::Models::ChatCompletion]
|
|
55
63
|
# @return [Hash] normalized response hash
|
|
@@ -32,6 +32,21 @@ module ActiveAgent
|
|
|
32
32
|
alias_method :declared_parameters, :parameters
|
|
33
33
|
alias_method :provider_options, :provider_params
|
|
34
34
|
|
|
35
|
+
# RubyLLM 2.2 registers a tool under its qualified name (namespace
|
|
36
|
+
# plus name) and asks each tool whether it defers. ActiveAgent's
|
|
37
|
+
# tools have no namespace and run when called.
|
|
38
|
+
def namespace
|
|
39
|
+
nil
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def qualified_name
|
|
43
|
+
name
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def deferred?
|
|
47
|
+
false
|
|
48
|
+
end
|
|
49
|
+
|
|
35
50
|
private
|
|
36
51
|
|
|
37
52
|
def deep_stringify(obj)
|
|
@@ -152,15 +152,16 @@ module ActiveAgent
|
|
|
152
152
|
# Handle tool calls in chunk
|
|
153
153
|
if chunk.tool_calls&.any?
|
|
154
154
|
message[:tool_calls] ||= []
|
|
155
|
-
chunk.tool_calls
|
|
155
|
+
each_tool_call(chunk.tool_calls) do |key, tool_call|
|
|
156
156
|
# Only the first delta of a call has its ID and name; the rest
|
|
157
157
|
# are fragments of its arguments. A fragment belongs to the call
|
|
158
158
|
# that shares its stream key (the index RubyLLM 2 numbers calls
|
|
159
159
|
# by), or, without one as RubyLLM 1.16 leaves it for OpenAI, to
|
|
160
160
|
# the latest call. RubyLLM 1.16 cannot preserve interleaved
|
|
161
161
|
# OpenAI calls: its parser drops their indices and fragments.
|
|
162
|
-
|
|
163
|
-
|
|
162
|
+
call_id = streamed_tool_call_id(tool_call)
|
|
163
|
+
existing = if call_id
|
|
164
|
+
message[:tool_calls].find { |tc| tc[:id] == call_id }
|
|
164
165
|
else
|
|
165
166
|
@stream_tool_calls[key] || message[:tool_calls].last
|
|
166
167
|
end
|
|
@@ -169,7 +170,7 @@ module ActiveAgent
|
|
|
169
170
|
existing[:function][:arguments] += fragment
|
|
170
171
|
else
|
|
171
172
|
existing = {
|
|
172
|
-
id:
|
|
173
|
+
id: call_id,
|
|
173
174
|
type: "function",
|
|
174
175
|
function: {
|
|
175
176
|
name: tool_call.name,
|
|
@@ -492,7 +493,7 @@ module ActiveAgent
|
|
|
492
493
|
|
|
493
494
|
# Handle tool calls
|
|
494
495
|
if response.tool_calls&.any?
|
|
495
|
-
hash[:tool_calls] = response.tool_calls.map do |id, tc|
|
|
496
|
+
hash[:tool_calls] = each_tool_call(response.tool_calls).map do |id, tc|
|
|
496
497
|
{
|
|
497
498
|
id: id,
|
|
498
499
|
type: "function",
|
|
@@ -523,6 +524,41 @@ module ActiveAgent
|
|
|
523
524
|
hash
|
|
524
525
|
end
|
|
525
526
|
|
|
527
|
+
# Yields each of a message's tool calls with its key. RubyLLM 1.x and
|
|
528
|
+
# 2.0 to 2.1 keep them in a Hash keyed by call ID (or, on a streamed
|
|
529
|
+
# chunk, by stream index), so iterating yields pairs. RubyLLM 2.2's
|
|
530
|
+
# `ToolCall::Collection` yields the calls themselves and keeps the
|
|
531
|
+
# keys behind `each_key`, in the same order.
|
|
532
|
+
#
|
|
533
|
+
# @param calls [Hash, #each_key, nil]
|
|
534
|
+
# @return [Enumerator, Array] key and call pairs
|
|
535
|
+
def each_tool_call(calls, &block)
|
|
536
|
+
pairs = if calls.nil?
|
|
537
|
+
[]
|
|
538
|
+
elsif calls.is_a?(Hash)
|
|
539
|
+
calls.to_a
|
|
540
|
+
elsif calls.respond_to?(:each_key)
|
|
541
|
+
calls.each_key.to_a.zip(calls.each.to_a)
|
|
542
|
+
else
|
|
543
|
+
calls.each.to_a.map { |call| [ call.id, call ] }
|
|
544
|
+
end
|
|
545
|
+
block ? pairs.each(&block) : pairs
|
|
546
|
+
end
|
|
547
|
+
|
|
548
|
+
# The provider's ID for a streamed tool call, present on its first
|
|
549
|
+
# delta and nil on the fragments. RubyLLM 2.2 keeps it in
|
|
550
|
+
# `provider_call_id` and leaves `id` (its own identifier, assigned when
|
|
551
|
+
# the message completes) nil on a chunk; earlier releases put the
|
|
552
|
+
# provider's ID in `id`.
|
|
553
|
+
#
|
|
554
|
+
# @param tool_call [RubyLLM::ToolCall]
|
|
555
|
+
# @return [String, nil]
|
|
556
|
+
def streamed_tool_call_id(tool_call)
|
|
557
|
+
provider_id = tool_call.provider_call_id if tool_call.respond_to?(:provider_call_id)
|
|
558
|
+
provider_id = nil if provider_id.to_s.empty?
|
|
559
|
+
provider_id || tool_call.id
|
|
560
|
+
end
|
|
561
|
+
|
|
526
562
|
# A streamed tool call's arguments as the text they add to the JSON
|
|
527
563
|
# string the call builds up. Most APIs stream that string; Anthropic
|
|
528
564
|
# opens a call with an empty Hash for its input, and Gemini sends the
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../open_ai/chat/_types"
|
|
4
|
+
require_relative "options"
|
|
5
|
+
|
|
6
|
+
module ActiveAgent
|
|
7
|
+
module Providers
|
|
8
|
+
module Runcrate
|
|
9
|
+
# ActiveModel type for casting and serializing Runcrate requests.
|
|
10
|
+
#
|
|
11
|
+
# Runcrate is OpenAI-compatible, so requests use the same shape as the
|
|
12
|
+
# OpenAI Chat API. This delegates entirely to
|
|
13
|
+
# OpenAI::Chat::RequestType.
|
|
14
|
+
RequestType = ActiveAgent::Providers::OpenAI::Chat::RequestType
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../open_ai/options"
|
|
4
|
+
|
|
5
|
+
module ActiveAgent
|
|
6
|
+
module Providers
|
|
7
|
+
module Runcrate
|
|
8
|
+
# Configuration options for the Runcrate provider.
|
|
9
|
+
#
|
|
10
|
+
# Extends OpenAI::Options, overriding the base URL to point at
|
|
11
|
+
# Runcrate's OpenAI-compatible inference API and resolving the API key
|
|
12
|
+
# from RUNCRATE_API_KEY. Runcrate does not use organization or project
|
|
13
|
+
# identifiers.
|
|
14
|
+
#
|
|
15
|
+
# @example Basic configuration
|
|
16
|
+
# options = Options.new(api_key: ENV["RUNCRATE_API_KEY"])
|
|
17
|
+
#
|
|
18
|
+
# @see https://runcrate.ai/docs/models/overview
|
|
19
|
+
# @see https://runcrate.ai/dashboard/api-keys Runcrate API Keys
|
|
20
|
+
class Options < ActiveAgent::Providers::OpenAI::Options
|
|
21
|
+
# @!attribute base_url
|
|
22
|
+
# @return [String] API endpoint (default: "https://api.runcrate.ai/v1")
|
|
23
|
+
attribute :base_url, :string, as: "https://api.runcrate.ai/v1"
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def resolve_api_key(kwargs)
|
|
28
|
+
kwargs[:api_key] ||
|
|
29
|
+
kwargs[:access_token] ||
|
|
30
|
+
ENV["RUNCRATE_API_KEY"]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Not used as part of Runcrate
|
|
34
|
+
def resolve_organization_id(kwargs) = nil
|
|
35
|
+
def resolve_project_id(kwargs) = nil
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
require_relative "_base_provider"
|
|
2
|
+
|
|
3
|
+
require_gem!(:openai, __FILE__)
|
|
4
|
+
|
|
5
|
+
require_relative "open_ai_provider"
|
|
6
|
+
require_relative "runcrate/_types"
|
|
7
|
+
|
|
8
|
+
module ActiveAgent
|
|
9
|
+
module Providers
|
|
10
|
+
# Provides access to Runcrate's OpenAI-compatible inference API for
|
|
11
|
+
# open-weight models.
|
|
12
|
+
#
|
|
13
|
+
# Extends the OpenAI provider to work with Runcrate's OpenAI-compatible
|
|
14
|
+
# API. Model ids are the vendor-prefixed names Runcrate lists at
|
|
15
|
+
# +/v1/models+ (e.g. +deepseek-ai/DeepSeek-V4.1-Flash+).
|
|
16
|
+
#
|
|
17
|
+
# Runcrate is a plain OpenAI-compatible API: requests, responses and
|
|
18
|
+
# transforms are identical to the OpenAI Chat API, so this provider
|
|
19
|
+
# reuses OpenAI::Chat::RequestType and OpenAI::Chat::Transforms directly.
|
|
20
|
+
# The only Runcrate-specific configuration is the base URL and API key,
|
|
21
|
+
# which live in Runcrate::Options.
|
|
22
|
+
#
|
|
23
|
+
# @example Configuration in active_agent.yml
|
|
24
|
+
# runcrate:
|
|
25
|
+
# service: "Runcrate"
|
|
26
|
+
# api_key: <%= ENV["RUNCRATE_API_KEY"] %>
|
|
27
|
+
# model: "deepseek-ai/DeepSeek-V4.1-Flash"
|
|
28
|
+
#
|
|
29
|
+
# @see OpenAI::ChatProvider
|
|
30
|
+
# @see https://runcrate.ai/docs/models/overview
|
|
31
|
+
class RuncrateProvider < OpenAI::ChatProvider
|
|
32
|
+
# @return [String]
|
|
33
|
+
def self.service_name
|
|
34
|
+
"Runcrate"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# @return [Class]
|
|
38
|
+
def self.options_klass
|
|
39
|
+
Runcrate::Options
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# @return [ActiveModel::Type::Value]
|
|
43
|
+
def self.prompt_request_type
|
|
44
|
+
OpenAI::Chat::RequestType.new
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# @return [ActiveModel::Type::Value]
|
|
48
|
+
def self.embed_request_type
|
|
49
|
+
OpenAI::Embedding::RequestType.new
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
protected
|
|
53
|
+
|
|
54
|
+
# Executes an embedding request via Runcrate's OpenAI-compatible API.
|
|
55
|
+
#
|
|
56
|
+
# @param parameters [Hash]
|
|
57
|
+
# @return [Hash] symbolized API response
|
|
58
|
+
def api_embed_execute(parameters)
|
|
59
|
+
client.embeddings.create(**parameters).as_json.deep_symbolize_keys
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# @see BaseProvider#api_response_normalize
|
|
63
|
+
# @param api_response [OpenAI::Models::ChatCompletion]
|
|
64
|
+
# @return [Hash] normalized response hash
|
|
65
|
+
def api_response_normalize(api_response)
|
|
66
|
+
return api_response unless api_response
|
|
67
|
+
|
|
68
|
+
OpenAI::Chat::Transforms.gem_to_hash(api_response)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
data/lib/active_agent/version.rb
CHANGED
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.9.
|
|
4
|
+
version: 1.9.8
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Justin Bowen
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-11-
|
|
11
|
+
date: 2026-11-12 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: actionpack
|
|
@@ -162,16 +162,22 @@ dependencies:
|
|
|
162
162
|
name: anthropic
|
|
163
163
|
requirement: !ruby/object:Gem::Requirement
|
|
164
164
|
requirements:
|
|
165
|
-
- - "
|
|
165
|
+
- - ">="
|
|
166
166
|
- !ruby/object:Gem::Version
|
|
167
167
|
version: '1.12'
|
|
168
|
+
- - "<"
|
|
169
|
+
- !ruby/object:Gem::Version
|
|
170
|
+
version: '3'
|
|
168
171
|
type: :development
|
|
169
172
|
prerelease: false
|
|
170
173
|
version_requirements: !ruby/object:Gem::Requirement
|
|
171
174
|
requirements:
|
|
172
|
-
- - "
|
|
175
|
+
- - ">="
|
|
173
176
|
- !ruby/object:Gem::Version
|
|
174
177
|
version: '1.12'
|
|
178
|
+
- - "<"
|
|
179
|
+
- !ruby/object:Gem::Version
|
|
180
|
+
version: '3'
|
|
175
181
|
- !ruby/object:Gem::Dependency
|
|
176
182
|
name: aws-sdk-bedrockruntime
|
|
177
183
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -630,6 +636,9 @@ files:
|
|
|
630
636
|
- lib/active_agent/providers/ruby_llm/tool_proxy.rb
|
|
631
637
|
- lib/active_agent/providers/ruby_llm_provider.rb
|
|
632
638
|
- lib/active_agent/providers/rubyllm_provider.rb
|
|
639
|
+
- lib/active_agent/providers/runcrate/_types.rb
|
|
640
|
+
- lib/active_agent/providers/runcrate/options.rb
|
|
641
|
+
- lib/active_agent/providers/runcrate_provider.rb
|
|
633
642
|
- lib/active_agent/railtie.rb
|
|
634
643
|
- lib/active_agent/railtie/schema_generator_extension.rb
|
|
635
644
|
- lib/active_agent/schema_generator.rb
|