activeagent 1.9.6 → 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 +612 -28
- data/README.md +9 -0
- data/lib/active_agent/base.rb +1 -0
- data/lib/active_agent/cli/eval_commands.rb +50 -7
- data/lib/active_agent/concerns/tooling.rb +11 -0
- data/lib/active_agent/evals/capture.rb +286 -0
- data/lib/active_agent/evals/catalog.rb +150 -19
- data/lib/active_agent/evals/gate_check.rb +79 -0
- data/lib/active_agent/evals/pass_thresholds.rb +137 -0
- data/lib/active_agent/evals/patterns.rb +80 -0
- data/lib/active_agent/evals/publisher.rb +124 -8
- data/lib/active_agent/evals/report.rb +165 -20
- data/lib/active_agent/evals/report_html.rb +91 -17
- data/lib/active_agent/evals/report_markdown.rb +63 -6
- data/lib/active_agent/evals/result.rb +10 -2
- data/lib/active_agent/evals/rspec.rb +87 -0
- data/lib/active_agent/evals/ruby_llm.rb +105 -3
- data/lib/active_agent/evals/runner.rb +59 -16
- data/lib/active_agent/evals/scenario.rb +12 -4
- data/lib/active_agent/evals/scenario_parser.rb +9 -1
- data/lib/active_agent/evals/scorer.rb +53 -23
- data/lib/active_agent/evals/suite.rb +117 -6
- data/lib/active_agent/evals/test_helper.rb +286 -0
- data/lib/active_agent/evals.rb +9 -1
- data/lib/active_agent/model_capabilities.rb +1 -1
- data/lib/active_agent/providers/_base_provider.rb +18 -2
- data/lib/active_agent/providers/anthropic_provider.rb +10 -0
- data/lib/active_agent/providers/atlas_cloud/options.rb +35 -0
- data/lib/active_agent/providers/atlas_cloud_provider.rb +45 -0
- data/lib/active_agent/providers/bedrock/bearer_client.rb +2 -1
- data/lib/active_agent/providers/cheaper_inference/_types.rb +17 -0
- data/lib/active_agent/providers/cheaper_inference/options.rb +39 -0
- data/lib/active_agent/providers/cheaper_inference_provider.rb +72 -0
- data/lib/active_agent/providers/common/usage.rb +6 -1
- data/lib/active_agent/providers/ollama/_types.rb +1 -0
- data/lib/active_agent/providers/ollama/catalog.rb +221 -0
- data/lib/active_agent/providers/ollama/cloud.rb +109 -0
- data/lib/active_agent/providers/ollama/options.rb +66 -2
- data/lib/active_agent/providers/ollama_provider.rb +55 -3
- 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/railtie.rb +4 -0
- data/lib/active_agent/version.rb +1 -1
- data/lib/tasks/active_agent/evals.rake +28 -0
- metadata +27 -5
- data/lib/tasks/activeagent_tasks.rake +0 -4
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,589 @@ 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
|
+
## [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.
|
|
185
|
+
|
|
186
|
+
## [1.9.7] - 2026-11-04
|
|
187
|
+
|
|
188
|
+
Evaluations an application runs in its own test suite, and the dashboard
|
|
189
|
+
pieces around them: a suite declares the pass rate each group of questions
|
|
190
|
+
must reach (`pass`), and `assert_meets_pass_gates` or RSpec's
|
|
191
|
+
`meet_its_pass_gates` holds a run to it; scenarios replay a set number of
|
|
192
|
+
trials; the judge can skip an answer that already failed its expectations;
|
|
193
|
+
`Patterns` builds the regex for a count or a share a seeded scenario
|
|
194
|
+
expects; scenario keys select strictly; named criteria lists layer per
|
|
195
|
+
audience; `ActiveAgent::Evals::RubyLLM.verify!` checks every model's
|
|
196
|
+
credentials before a run; and the publisher builds itself from the
|
|
197
|
+
environment and returns links that open. Alongside them: Ollama Cloud,
|
|
198
|
+
scoring leases, catalog scenario extensions kept, evaluation catalogs
|
|
199
|
+
without Rails, ranked comparison tables, and users' own remote MCP servers.
|
|
200
|
+
|
|
201
|
+
Upgrading: three dashboard migrations, 030 `add_evaluation_scoring_leases`
|
|
202
|
+
(the scoring lease columns on `evaluation_scenario_results`), 031
|
|
203
|
+
`add_catalog_scenario_metadata` (the `metadata` column on
|
|
204
|
+
`catalog_scenarios`) and 032 `create_active_agent_mcp_server_configs` (the
|
|
205
|
+
table behind a user's own MCP servers): run `bin/rails generate
|
|
206
|
+
action_agent:install --skip` and `bin/rails db:migrate`. Until they run,
|
|
207
|
+
fanned-out runs keep scoring with the claim 1.9.3 uses, catalog imports
|
|
208
|
+
keep working but still drop scenario extensions (extensions discarded
|
|
209
|
+
before the upgrade need the original document imported again), and
|
|
210
|
+
the MCP Services page cannot list or save servers of your own.
|
|
211
|
+
|
|
212
|
+
The pricing tables changed too: a trace recorded under Ollama that priced
|
|
213
|
+
at the default rate before prices at Ollama Cloud's listed rate now, and a
|
|
214
|
+
finished evaluation run's cached figures are worked out again.
|
|
215
|
+
|
|
216
|
+
A suite's `pass` block is read and checked as the suite loads: a key other
|
|
217
|
+
than `threshold` and `min_pass_rate` (and, on a group, `min_pass_rate` and
|
|
218
|
+
`gated`) raises `ArgumentError` naming it, and in a catalog a product's or
|
|
219
|
+
set's `pass` raises `Catalog::InvalidDocument`. 1.9.6 ignored a suite's
|
|
220
|
+
`pass` and kept a product's as metadata, so a document that already carried
|
|
221
|
+
one under its own keys needs them renamed.
|
|
222
|
+
|
|
223
|
+
### Added
|
|
224
|
+
|
|
225
|
+
- **Ollama Cloud** (`activeagent`). `generate_with :ollama, cloud: true,
|
|
226
|
+
model: "gpt-oss:120b"` sends requests to `https://ollama.com/v1` with the
|
|
227
|
+
key from `OLLAMA_API_KEY` (or `api_key:`), and a request to the cloud
|
|
228
|
+
without a key fails before it is sent, naming what to set. The host can
|
|
229
|
+
also be given as `host:` or through `OLLAMA_HOST`, read the way the
|
|
230
|
+
`ollama` command reads it: a bare origin is completed to its `/v1`
|
|
231
|
+
endpoint, and a scheme-less `host[:port]` is http on 11434. A local server
|
|
232
|
+
signed in to the cloud needs none of this — `model: "gpt-oss:120b-cloud"`
|
|
233
|
+
works as before — and the provider knows that spelling names the cloud's
|
|
234
|
+
model (`ActiveAgent::Providers::Ollama::Cloud`).
|
|
235
|
+
- **Ollama model lookup** (`activeagent`). `provider.models` lists the
|
|
236
|
+
models the configured host serves and `provider.model_info(name)`
|
|
237
|
+
describes one — its details, capabilities, context length, and whether
|
|
238
|
+
the cloud serves it — from the host's own API (`/api/tags`, `/api/show`),
|
|
239
|
+
local server or Ollama Cloud alike; a host with only the OpenAI-compatible
|
|
240
|
+
API is listed through `/v1/models` (`ActiveAgent::Providers::Ollama::Catalog`).
|
|
241
|
+
- **Ollama Cloud rates on traces and evaluations** (`actionagent`). A model
|
|
242
|
+
run under Ollama is priced from Ollama Cloud's listing by the name the
|
|
243
|
+
cloud uses: RubyLLM's registry carries it under its `ollama_cloud`
|
|
244
|
+
provider, and an install without RubyLLM has the same rows bundled
|
|
245
|
+
(`ModelPricing::OLLAMA_CLOUD_RATES`), each a `catalog` rate rather than a
|
|
246
|
+
guess at the family's. `gpt-oss:120b-cloud` through a local server and
|
|
247
|
+
`gpt-oss:120b` through the cloud price alike, and `deepseek-v4-flash`
|
|
248
|
+
under Ollama is Ollama's price, not DeepSeek's.
|
|
249
|
+
- **The model lookup says which host is the cloud** (`actionagent`).
|
|
250
|
+
`POST /api/provider_keys/test` and `GET /api/provider_models?provider=ollama`
|
|
251
|
+
carry `cloud` and, per listed model a price list names, `rates`
|
|
252
|
+
(`{ input, output, source }` in $ per million tokens); Settings → Provider
|
|
253
|
+
API Keys shows both with the connection test.
|
|
254
|
+
|
|
255
|
+
- **Atlas Cloud as an OpenAI-compatible provider** (`activeagent`).
|
|
256
|
+
`service: "AtlasCloud"` generates through Atlas Cloud's compatibility
|
|
257
|
+
endpoint, with the key from `ATLASCLOUD_API_KEY` and an overridable
|
|
258
|
+
`base_url`; chat only, no embeddings (docs/providers/atlas_cloud.md).
|
|
259
|
+
Contributed in #352.
|
|
260
|
+
|
|
261
|
+
- **Cheaper Inference as an OpenAI-compatible provider** (`activeagent`).
|
|
262
|
+
`service: "CheaperInference"` generates and embeds through
|
|
263
|
+
`https://api.cheaperinference.com/v1`, with the key from
|
|
264
|
+
`CHEAPER_INFERENCE_API_KEY`; a thin subclass of the OpenAI provider, like
|
|
265
|
+
Opper. Contributed in #629 and landed in #666.
|
|
266
|
+
|
|
267
|
+
- **Your own MCP servers** (`actionagent`). A dashboard user adds a remote
|
|
268
|
+
MCP server by URL on the MCP Services page (*Your MCP servers*), with
|
|
269
|
+
optional request headers, tests it (`tools/list`, recording the tools it
|
|
270
|
+
serves) and enables it on agents from the Tools tab, where it is listed
|
|
271
|
+
beside the catalog as `custom:<id>`. Agent runs and scenario evaluation runs
|
|
272
|
+
call its tools, and evaluation fix items attribute them to it. Each server
|
|
273
|
+
belongs to an owner like a provider key (`MCPServerConfig`, install
|
|
274
|
+
migration 032), and every lookup, including a run's, is scoped to that
|
|
275
|
+
owner. Header values are encrypted at rest, never returned to the browser
|
|
276
|
+
(names with masked values; a blank value on update keeps the stored one),
|
|
277
|
+
and scrubbed from recorded output. Writes and tests ask for
|
|
278
|
+
`:manage_credentials`. A URL that reaches a loopback, private or
|
|
279
|
+
link-local address is refused when saved and when called (`MCPHostGuard`),
|
|
280
|
+
unless the host sets `ActionAgent.allow_private_mcp_hosts`, e.g. for local
|
|
281
|
+
development. API: `/api/mcp_server_configs` with a `test` member action.
|
|
282
|
+
|
|
283
|
+
- **Evals in the test suite** (`activeagent`). An eval is an ordinary test
|
|
284
|
+
under `test/evals`, collected by `rails test` like any other, whose
|
|
285
|
+
`ActiveAgent::Evals::TestCase` skips it unless `EVALS` is on, so the
|
|
286
|
+
suite never calls a model by accident. `bin/rails test:evals` switches
|
|
287
|
+
evals on and runs them through the same runner (`TEST=path` narrows,
|
|
288
|
+
`TESTOPTS` passes through); `EVALS=1 bin/rails test ...` runs one file,
|
|
289
|
+
one line, or the whole suite with the evals in. The same switch gates an
|
|
290
|
+
RSpec suite (`filter_run_excluding eval: true unless
|
|
291
|
+
ActiveAgent::Evals::TestHelper.enabled?`, with `expect_with :minitest`
|
|
292
|
+
for the assertions). `require "active_agent/evals/test_helper"`
|
|
293
|
+
after `rails/test_help` gives `ActiveAgent::Evals::TestCase` and the
|
|
294
|
+
`TestHelper` module: `run_eval MyAgent.with(...).action` returns an
|
|
295
|
+
`ActiveAgent::Evals::Replay` — the answer, every tool call with its
|
|
296
|
+
arguments, result or error and timing, duration and tokens — and
|
|
297
|
+
`assert_answer_present`, `assert_output_contains` /
|
|
298
|
+
`refute_output_contains`, `assert_tool_called` (with an argument subset)
|
|
299
|
+
/ `refute_tool_called` and `assert_tools_succeeded` read it, failing with
|
|
300
|
+
what the agent did. `assert_eval_passes` (and `score_eval`) scores the
|
|
301
|
+
replay as a dashboard run does and fails with the fault and the fix;
|
|
302
|
+
`run_eval_suite` replays a YAML suite across models and returns the
|
|
303
|
+
`Report`, which `assert_pass_rate` gates and `to_html` writes.
|
|
304
|
+
`ActiveAgent::Evals::Capture` is the assertion-free core: it runs a
|
|
305
|
+
generation into a `Replay`, capturing tool calls on the agent's
|
|
306
|
+
`tools_function` (where the telemetry tool spans are; tools served by a
|
|
307
|
+
client-side MCP server are recorded through the agent's `tool_observer`),
|
|
308
|
+
replays under another `model:` or `provider:` the way a comparison run
|
|
309
|
+
does, and takes a block for an agent run some other way. The framework's railtie now
|
|
310
|
+
loads the gem's rake tasks; the empty `activeagent_tasks.rake`
|
|
311
|
+
placeholder is gone.
|
|
312
|
+
|
|
313
|
+
- **A suite declares its pass gates** (`activeagent`). A suite's `pass`
|
|
314
|
+
block sets the score every answer must reach (`threshold`) and the share
|
|
315
|
+
of a model's answers that must pass (`min_pass_rate`), for the suite and
|
|
316
|
+
for any group with its own bar; `gated: false` keeps a group in the report
|
|
317
|
+
and out of `Report#passed?`. A catalog's products and sets take the
|
|
318
|
+
same block, and later documents layer it key by key. `Suite#pass_thresholds`
|
|
319
|
+
reads it as a `PassThresholds`, `Runner.new(pass_thresholds:)` takes it in
|
|
320
|
+
place of `threshold:` and hands it to the report, and `Report#gates` is
|
|
321
|
+
then one row per group × model with its pass rate against its bar, plus an
|
|
322
|
+
`All` row per model over its gated results; `Report#passed?` is every gate
|
|
323
|
+
met, and a report with no gate (a selection that matched nothing, or every
|
|
324
|
+
scenario dropped as `production_only`) does not pass;
|
|
325
|
+
`Report#failures(group:)` lists the results that did not pass. The
|
|
326
|
+
Markdown report adds a *Pass rate by group* table (`gates_markdown`;
|
|
327
|
+
`failures_markdown(group:)` lists the failures for a test message), the
|
|
328
|
+
HTML page tones each group's pass count by its gate, and `to_h` gains
|
|
329
|
+
`pass_thresholds`, `groups` and `gates`. A report built without pass
|
|
330
|
+
thresholds renders as it did. The command line's
|
|
331
|
+
`evals run --min-pass-rate` stays a gate on the whole run.
|
|
332
|
+
- **The pass-gate assertion, for Minitest and RSpec** (`activeagent`).
|
|
333
|
+
`TestHelper#assert_meets_pass_gates(report)` passes when every gate of
|
|
334
|
+
the report is met, each group held to the bar the suite declares for it
|
|
335
|
+
and ungated groups left out, where `assert_pass_rate` holds every model
|
|
336
|
+
to one rate the test names; it takes a `PassThresholds` to hold the
|
|
337
|
+
report to another bar, and returns the check. `run_eval_suite` hands a
|
|
338
|
+
suite's bars to the runner when it declares any (a `threshold` other
|
|
339
|
+
than 0.7, a `min_pass_rate` below 1, or any key in a group's `pass`), so
|
|
340
|
+
the report it returns carries them; a suite with no `pass` block, or
|
|
341
|
+
only a suite-level one that restates the defaults, runs under the
|
|
342
|
+
defaults and its report carries no bars, and a `pass_thresholds:` or
|
|
343
|
+
`threshold:` given to `run_eval_suite` wins over the suite's. In RSpec the
|
|
344
|
+
assertion works through `expect_with :minitest` like the rest of the
|
|
345
|
+
helper, and `require "active_agent/evals/rspec"`, with
|
|
346
|
+
`ActiveAgent::Evals::RSpec` included beside the helper, adds the first
|
|
347
|
+
eval matcher, `expect(report).to meet_its_pass_gates`; that file loads
|
|
348
|
+
nothing from rspec, and `require "active_agent/evals"` loads neither it
|
|
349
|
+
nor the helper. Both fail with one message, `GateCheck#message`: how many
|
|
350
|
+
of the gates were not met, each unmet gate as its sentence, then the
|
|
351
|
+
failures to read under a heading per group with an unmet gate (one
|
|
352
|
+
`Report#failure_markdown(result)` line each, the line `failures_markdown`
|
|
353
|
+
is built from) — and, for an `All` row that is short, the failures it
|
|
354
|
+
counts: under `Failures:` when no group row is short with it, else the
|
|
355
|
+
ones outside the groups already listed under `Other failures:`; an
|
|
356
|
+
ungated group's failures are never listed. `GateCheck.new(report)`
|
|
357
|
+
answers `met?`, `unmet` and `gates` for a script that reports on its own.
|
|
358
|
+
- **Pattern builders for counts and shares** (`activeagent`).
|
|
359
|
+
`ActiveAgent::Evals::Patterns.number(count)` and
|
|
360
|
+
`Patterns.share(count, total)` return a regex string for a scenario's
|
|
361
|
+
`contains` or `not_contains` entry: `number(10)` matches the figure on its
|
|
362
|
+
own and not the `10` in `2026-10-01`, `10/1`, `10:00`, `1–10`, `Oct 10`,
|
|
363
|
+
`10 October`, `10.5`, `110`, `10,000` or `10%`; `share(3, 4)` matches
|
|
364
|
+
`75%`, `75 %`, `75.0%` and `75.00%` and not `175%` or `7.5%`.
|
|
365
|
+
`Scorer.matches_pattern?` tries an entry as a substring and then as a
|
|
366
|
+
regex, so the string goes into a suite document or a scenario hash as it
|
|
367
|
+
is, and a scenario seeded with known data can expect the figure that data
|
|
368
|
+
implies. A fractional or negative count, a total of zero or a count above
|
|
369
|
+
the total raise `ArgumentError`.
|
|
370
|
+
- **Strict scenario selection by key** (`activeagent`). `Suite#scenarios`
|
|
371
|
+
and `Catalog#scenarios` take `strict: true` with `keys:`: every key must
|
|
372
|
+
name a scenario the suite or catalog holds, or the call raises
|
|
373
|
+
`Suite::NotFound` listing the missing keys in the order given, so a
|
|
374
|
+
misspelled key from an environment variable or a command line fails the
|
|
375
|
+
run, named in the error. A key the suite holds outside the chosen
|
|
376
|
+
`groups:` returns an empty selection. `Suite.select` is the same check
|
|
377
|
+
over any scenarios, with `known:` the wider set the keys must come from,
|
|
378
|
+
for a runner that takes keys across several suites. Without `strict:` the
|
|
379
|
+
selection narrows as before: the scenario a misspelled key meant is left
|
|
380
|
+
out without notice, and only when every key misses does a run held to
|
|
381
|
+
`Report#passed?` (or `assert_meets_pass_gates`) fail, without naming the
|
|
382
|
+
key.
|
|
383
|
+
- **Skip the judge for a replay that already failed its expectations**
|
|
384
|
+
(`activeagent`). `Runner.new(judge_failed_expectations: false)` asks the
|
|
385
|
+
judge about an answered replay only when every expectation its scenario
|
|
386
|
+
declared was met. A replay that called none of its expected tools, printed
|
|
387
|
+
a forbidden phrase, lacks expected content or had a tool call fail is a
|
|
388
|
+
failed result whatever a judge would say — `Diagnosis` assigns one of its
|
|
389
|
+
evidence faults (`tool_error`, `missing_capability`,
|
|
390
|
+
`expected_tool_not_called`, `ungrounded_answer`, `forbidden_content` or
|
|
391
|
+
`missing_content`) before it looks at quality — so it fails on whichever
|
|
392
|
+
of those it assigns first, with no `task_completion` score, its
|
|
393
|
+
`llm_judge` criteria unscored and no judge-written recommendation (a
|
|
394
|
+
refusal or invented figure the judge skipped loses the judge's suggested
|
|
395
|
+
tool too), and the calls saved stay out of the `judge_limit` budget.
|
|
396
|
+
`Scorer.new(judge_failed_expectations:)` and
|
|
397
|
+
`Scorer.expectations_met?(scores)` are the pieces underneath, and the
|
|
398
|
+
scores hash keeps its key order either way. The default (`true`) grades
|
|
399
|
+
every answered replay as it did; a blank answer is never graded, and a
|
|
400
|
+
blank or errored replay is never refined under the default
|
|
401
|
+
`refine_faults`, with or without the flag.
|
|
402
|
+
- **Replay each scenario several times** (`activeagent`, `actionagent`).
|
|
403
|
+
`Runner.new(trials: 3)` replays every scenario under every model that
|
|
404
|
+
many times, scenario by scenario and model by model, so a flaky scenario
|
|
405
|
+
gets a pass share instead of one coin flip. Scenario keys stay as they
|
|
406
|
+
are; `Result#trial` tells the repeats apart, reads 1 on a result built
|
|
407
|
+
without one, and lands in `to_h` as `"trial"`. A model's `"scenarios"`
|
|
408
|
+
count and pass rate cover every replay, with `"unique_scenarios"` beside
|
|
409
|
+
them, and the verdict then reads "Passed 5 of 6 replays of 2 scenarios";
|
|
410
|
+
a matrix cell shows one mark per trial with the mean score and the most
|
|
411
|
+
frequent fault; the Answers section lists every trial; a failure line
|
|
412
|
+
(`failure_markdown`, `failures_markdown`, the pass-gate message) names
|
|
413
|
+
the trial (`gpt-5-mini` · trial 2) when its scenario × model was
|
|
414
|
+
replayed more than once; the Markdown, JSON and HTML reports state the
|
|
415
|
+
trial count only when it is above 1, so a one-trial Markdown or HTML
|
|
416
|
+
report is exactly what it was, and its JSON differs only by `"trial"` on
|
|
417
|
+
each result and `"unique_scenarios"` on each model. `Comparison` pools a
|
|
418
|
+
scenario's replays into its pass share, so its intervals still resample
|
|
419
|
+
scenarios. The collector accepts the repeats of one scenario × model when
|
|
420
|
+
each carries its `trial`, stores every one, and rebuilds them into the
|
|
421
|
+
run's report; the dashboard's own runs replay once.
|
|
422
|
+
- **The evaluation publisher reads `run_id` from the report, returns an
|
|
423
|
+
absolute receipt `url` and builds itself from `ENV`** (`activeagent`).
|
|
424
|
+
`ActiveAgent::Evals::Publisher#call` and `#progress` take `run_id:` as
|
|
425
|
+
optional and read it from the report's `metadata.run_id`, where
|
|
426
|
+
`Correlation#with_run` records it, from a `Report` or its saved JSON hash;
|
|
427
|
+
an explicit `run_id:` wins, and a report with neither raises
|
|
428
|
+
`ArgumentError` before anything is sent. The receipt's `url` comes back
|
|
429
|
+
absolute: the path a collector sends
|
|
430
|
+
(`<mount>/evaluations/:evaluation_id/runs/:id` from the engine) is joined
|
|
431
|
+
onto the publisher's `dashboard_url`, by default the endpoint's origin
|
|
432
|
+
(`https://activeagents.ai` for the hosted collector on its API host), or
|
|
433
|
+
the HTTP(S) URL `Publisher.new(dashboard_url:)` is given, so the link opens
|
|
434
|
+
in a browser; an absolute `url` and a receipt without one are left as they
|
|
435
|
+
are. `Publisher.from_env` builds the publisher from
|
|
436
|
+
`ACTIVEAGENTS_EVALUATIONS_ENDPOINT` (the hosted default when blank),
|
|
437
|
+
`ACTIVEAGENTS_API_KEY` (required) and the new, optional
|
|
438
|
+
`ACTIVEAGENTS_DASHBOARD_URL`, for a dashboard people reach at another
|
|
439
|
+
address than the collector's:
|
|
440
|
+
`Publisher.from_env.call(report: report, source: "support-app", agent_name: "SupportBot", suite: "orders")`.
|
|
441
|
+
- **Verify RubyLLM models and the judge before a run** (`activeagent`).
|
|
442
|
+
`ActiveAgent::Evals::RubyLLM.verify!(models, context:)` takes a run's
|
|
443
|
+
`ModelSpec`s (or one) and, for the first that cannot run, raises
|
|
444
|
+
`RubyLLM::ConfigurationError` saying why: a provider RubyLLM does not know
|
|
445
|
+
(the message lists the ones it does), one outside `providers:` (the names
|
|
446
|
+
a host has keys for), or one its context cannot build —
|
|
447
|
+
`claude-sonnet-5 runs on anthropic, which has no API key. Set
|
|
448
|
+
ANTHROPIC_API_KEY.`, naming the provider's `configuration_requirements`
|
|
449
|
+
the context leaves unset (`which is not configured. Set OLLAMA_API_BASE.`
|
|
450
|
+
when they are not keys), or carrying RubyLLM's own message when those are
|
|
451
|
+
all set and it refused for another reason, with RubyLLM's error as the
|
|
452
|
+
cause. It builds each chat with `assume_model_exists: true`, which is
|
|
453
|
+
where RubyLLM checks its keys, and sends nothing. `judge(..., verify: true)`
|
|
454
|
+
runs the same check on the judge's own model when the judge is built,
|
|
455
|
+
with the same `providers:` when it is given them, so a judge whose
|
|
456
|
+
provider has no key refuses before the first scenario is replayed rather
|
|
457
|
+
than at its first grade; the default stays lazy.
|
|
458
|
+
- **Named criteria lists on a suite** (`activeagent`). A suite document
|
|
459
|
+
carries a top-level `criteria` beside `groups`: a list of the
|
|
460
|
+
`{ key, type, config }` hashes `Runner.new(criteria:)` takes, which reads
|
|
461
|
+
as the one list named `default`, or a mapping of name → list, one per
|
|
462
|
+
audience the same questions are graded for. `Suite#criteria(*names)`
|
|
463
|
+
joins the named lists in the order given, a later list's criterion
|
|
464
|
+
replacing an earlier one with the same key, ready for the runner;
|
|
465
|
+
with no names it returns every list in document order, and a name no
|
|
466
|
+
document declares contributes nothing, so a deployment asks for `shared`
|
|
467
|
+
plus an audience that may have no list of its own. `Suite#criteria_names`
|
|
468
|
+
lists the names. Later documents layer each list by criterion key, as they
|
|
469
|
+
layer scenarios: a key already present is replaced in place, a new key
|
|
470
|
+
appended. A catalog product carries the same mapping, which `suite_for`
|
|
471
|
+
hands its suite and `to_yaml` writes back. A `criteria` value that is
|
|
472
|
+
neither a list nor a mapping of lists, or a criterion that is not a
|
|
473
|
+
mapping or has no key, is refused: `ArgumentError` from a suite,
|
|
474
|
+
`Catalog::InvalidDocument` from a catalog product (a set keeps a criterion
|
|
475
|
+
without a key; see Changed) and `ScenarioParser::ParseError` from a pasted
|
|
476
|
+
suite. Earlier releases ignored a suite document's top-level `criteria`;
|
|
477
|
+
a document that carries one in another shape is refused.
|
|
478
|
+
|
|
479
|
+
### Changed
|
|
480
|
+
|
|
481
|
+
- **Every trace cost is priced under its provider** (`actionagent`). The
|
|
482
|
+
trace list, Metrics, the agent scorecard, the interactions list and an
|
|
483
|
+
evaluation result priced from its trace all hand `ModelPricing` the
|
|
484
|
+
provider the llm span recorded, so `gpt-5.5` through OpenRouter is
|
|
485
|
+
OpenRouter's rate, and a trace's `OpenAI::Chat` tag reads as `openai`
|
|
486
|
+
(`ModelPricing.provider_slug`). `TelemetryTrace.pluck_with_llm_model_and_provider`
|
|
487
|
+
plucks the provider beside the model for a caller that prices tokens.
|
|
488
|
+
|
|
489
|
+
- **Ranked comparison tables and value pills** (`actionagent`). Wherever the
|
|
490
|
+
dashboard compares rows, they now read through one pattern: a
|
|
491
|
+
`ComparisonTable` with two-line mono headers that sort on click and a
|
|
492
|
+
rank-tinted `ValuePill` in every numeric cell, banded by tercile in the
|
|
493
|
+
column's better direction or by fixed thresholds where the domain has them
|
|
494
|
+
(pass rates at 85 and 50), with missing values unranked and sorted last,
|
|
495
|
+
and a visually hidden rank word so colour is never the only signal. Applied
|
|
496
|
+
to a run's model comparison table and the Evaluations leaderboard, the
|
|
497
|
+
scenario matrix (the key leads each step with the prompt under it, one
|
|
498
|
+
score pill per model, cost and time pills per scenario, the tools level
|
|
499
|
+
stacking tool call, time, tokens and cost pills), each opened result's
|
|
500
|
+
score, time, tokens and cost strip, the Metrics overview's change pills and
|
|
501
|
+
the home page's Top Agents list. Under 600px a table stacks each row to
|
|
502
|
+
labelled pills.
|
|
503
|
+
|
|
504
|
+
- **A set's `criteria` layer across catalog documents** (`activeagent`). A
|
|
505
|
+
later document's criterion replaces the one with its key in place and a
|
|
506
|
+
new key is appended, as a product's named lists and a suite's scenarios
|
|
507
|
+
layer; a later document used to replace the set's whole list. A criterion
|
|
508
|
+
without a key is still accepted and appended as it is, once: a later
|
|
509
|
+
document that restates it adds nothing. A criterion's `key` is written as
|
|
510
|
+
a string, as scenario keys are, so a set criterion keyed by a number reads
|
|
511
|
+
back with a quoted key and the catalog's digest changes with it.
|
|
512
|
+
|
|
513
|
+
### Fixed
|
|
514
|
+
|
|
515
|
+
- **The dashboard engine loads in a host without Action Cable**
|
|
516
|
+
(`actionagent`). The engine's class body camelizes its inflection
|
|
517
|
+
overrides with `Zeitwerk::Inflector` and did not require Zeitwerk itself.
|
|
518
|
+
Rails requires it only once the application sets up its autoloaders,
|
|
519
|
+
after `Bundler.require` has loaded the gem; until then the constant
|
|
520
|
+
exists only because Action Cable's engine loads it as a side effect. An
|
|
521
|
+
app generated with `--skip-action-cable`, or any API-only host, died at
|
|
522
|
+
boot with `uninitialized constant ActionAgent::Engine::Zeitwerk` (seen on
|
|
523
|
+
Rails 8.1.4; the require order is the same on every Rails since 7.0). The
|
|
524
|
+
engine requires Zeitwerk, a dependency of Active Support, before reading
|
|
525
|
+
it, and a test loads the engine in a fresh process right after `rails`,
|
|
526
|
+
with nothing in between.
|
|
527
|
+
|
|
528
|
+
- **An evaluation judge job delivered twice scores its result once**
|
|
529
|
+
(`actionagent`). The claim on a pending result was a conditional write on
|
|
530
|
+
its timestamp, which turned away a stale reader but not a duplicate
|
|
531
|
+
delivered after the claim: that one read the new timestamp and asked the
|
|
532
|
+
judge to score the result again. A claim now takes a 30-minute lease
|
|
533
|
+
(`scoring_token`, `scoring_expires_at`) that a fresh reader cannot take
|
|
534
|
+
while it is live, and a worker that finishes after its lease was
|
|
535
|
+
reclaimed cannot overwrite its successor's result. A delivery that finds
|
|
536
|
+
a live lease, such as the queue's redelivery after a worker died
|
|
537
|
+
mid-judge, enqueues the job again for when the lease expires, so the run
|
|
538
|
+
still completes. Without migration 030 the judge job keeps the timestamp
|
|
539
|
+
claim instead of failing on the missing columns.
|
|
540
|
+
|
|
541
|
+
- **Keep scenario catalog extension fields** (`activeagent`, `actionagent`).
|
|
542
|
+
Opaque scenario fields survive parsing, YAML/JSON export, database reload,
|
|
543
|
+
standard-field edits and storage restore. Their changes affect import
|
|
544
|
+
previews and catalog digests, including when only an extension changes.
|
|
545
|
+
Empty values and literal `metadata` keys are preserved; canonical format
|
|
546
|
+
fields cannot be replaced through the metadata container.
|
|
547
|
+
|
|
548
|
+
- **Bound trace console list limits** (`actionagent`). The console defaults
|
|
549
|
+
to 50 recent matching traces, clamps integer limits to 1–1000, and uses
|
|
550
|
+
the default for malformed values. Its documentation describes the bounded
|
|
551
|
+
recent list rather than claiming pagination.
|
|
552
|
+
|
|
553
|
+
- **Select trace time windows in dark mode** (`actionagent`). The dark
|
|
554
|
+
Traces header includes the shared time-window selector and zoom controls,
|
|
555
|
+
including when embedded on an agent page or when the window is empty.
|
|
556
|
+
|
|
557
|
+
- **Show tool usage in both dashboard themes** (`actionagent`). The Traces
|
|
558
|
+
page renders the same tool counts, durations, failures and empty state
|
|
559
|
+
in dark and light mode, using the shared telemetry colors.
|
|
560
|
+
|
|
561
|
+
- **Load evaluation catalogs without Rails** (`activeagent`). The standalone
|
|
562
|
+
`active_agent/evals` entry point loads the ActiveSupport array extension
|
|
563
|
+
used to construct, parse and load catalogs, without loading the framework.
|
|
564
|
+
|
|
565
|
+
- **Client-side MCP tool calls appear in an eval replay** (`activeagent`).
|
|
566
|
+
`Evals::Capture` recorded the tools that reach the agent's
|
|
567
|
+
`tools_function`, and a tool served by a client-side MCP server is
|
|
568
|
+
dispatched before it, so a replay of an agent with `mcps:` showed none of
|
|
569
|
+
those calls and `assert_tool_called` could not find them. The provider
|
|
570
|
+
now runs such a call inside the agent's `tool_observer`, which Capture
|
|
571
|
+
sets, so it is recorded once with its arguments, result preview and
|
|
572
|
+
timing. A result the server marks as an error, which the model receives
|
|
573
|
+
as a result and not an exception, is recorded as a failed call, so
|
|
574
|
+
`failed_tool_calls` and `assert_tools_succeeded` see it. A server the
|
|
575
|
+
provider runs itself (Anthropic's or OpenAI's hosted MCP) executes on the
|
|
576
|
+
provider's side and is still not recorded.
|
|
577
|
+
|
|
578
|
+
- **An agent run is executed by one job** (`actionagent`). The execution
|
|
579
|
+
job read a run's status, then wrote `running`, as two steps, and only
|
|
580
|
+
turned away a finished or paused run. A job delivered twice, or two
|
|
581
|
+
workers that both loaded the run as pending, each started a generation
|
|
582
|
+
for it, and a duplicate that arrived while the run was running started
|
|
583
|
+
another. The job now moves the run from pending to running in one
|
|
584
|
+
conditional update, as `CodeSessionJob` claims a session, and starts no
|
|
585
|
+
generation unless it was the one to do so. The database decides at the
|
|
586
|
+
write, so this holds on SQLite under Rails 7.2 too, where a row-lock read
|
|
587
|
+
followed by a write is refused once another worker has committed. Nothing
|
|
588
|
+
is held over the generation, so a cancel does not wait for one. A run
|
|
589
|
+
whose worker died after the claim stays running and is not run again.
|
|
590
|
+
|
|
8
591
|
## [1.9.6] - 2026-10-29
|
|
9
592
|
|
|
10
593
|
The dashboard on a phone, and the small finished pieces: the sidebar as a
|
|
@@ -1386,6 +1969,35 @@ finished must handle. Session Replay is now **Sessions** in the sidebar.
|
|
|
1386
1969
|
do for `generate_later`. `InputRequest#arguments` holds the arguments of the
|
|
1387
1970
|
paused call.
|
|
1388
1971
|
|
|
1972
|
+
- **`ActiveAgent::Evals::RubyLLM`** — the RubyLLM side of an evaluation,
|
|
1973
|
+
which hosts driving RubyLLM conversations had been writing for themselves.
|
|
1974
|
+
`require "active_agent/evals/ruby_llm"` (it requires `ruby_llm`;
|
|
1975
|
+
`require "active_agent/evals"` alone still does not) gives two helpers, for
|
|
1976
|
+
RubyLLM 1.16 and later and for 2.x:
|
|
1977
|
+
- `RubyLLM.judge(label:, model:, provider:, context:, correlation:)`, a
|
|
1978
|
+
`Judge` that answers from `context.chat(...)` and, with a `Correlation`,
|
|
1979
|
+
traces each call under the kind it serves. `temperature:` and a
|
|
1980
|
+
`configure:` hook set the chat up; any other keyword must be one
|
|
1981
|
+
`RubyLLM::Chat.new` takes, and anything else raises when the judge is
|
|
1982
|
+
built rather than failing every call. `on_usage:` reports each call's
|
|
1983
|
+
model, tokens and cost.
|
|
1984
|
+
- `RubyLLM.replay(messages)`, a `Replay` from a conversation's messages.
|
|
1985
|
+
`acts_as_chat` records of either RubyLLM generation are read through
|
|
1986
|
+
their own `to_llm`, so a 2.x table, or one between 2.x's upgrade and its
|
|
1987
|
+
cleanup, replays the same as `RubyLLM::Message` values. Tool calls keep
|
|
1988
|
+
the order the model made them in, each paired with the result that
|
|
1989
|
+
answers it; one is errored when that result reports an error at its top
|
|
1990
|
+
level — JSON whose `"error"` is a non-empty string, object or array, or
|
|
1991
|
+
`true`; an MCP result with `"isError": true`; or the inspected
|
|
1992
|
+
`{ error: "..." }` RubyLLM 1.x stores for a tool's error Hash with a
|
|
1993
|
+
String error — with the error (JSON-encoded, at most 1,200 bytes) as its
|
|
1994
|
+
detail. `input_tokens` counts the whole prompt, cache reads and writes
|
|
1995
|
+
included. Cost is RubyLLM's own price, left nil when any message is
|
|
1996
|
+
unpriced. The answer is the last reply after the last user message; a
|
|
1997
|
+
conversation that stopped at a tool call or on the user's own message
|
|
1998
|
+
has no answer and says so in its `error` (on RubyLLM 1.x, pass `answer:`
|
|
1999
|
+
for a tool that ends the turn with `halt`).
|
|
2000
|
+
|
|
1389
2001
|
### Changed
|
|
1390
2002
|
|
|
1391
2003
|
- **Confirm destructive actions in the dashboard's own dialog**
|
|
@@ -2100,34 +2712,6 @@ output in the shape ruby_llm reads. No migrations.
|
|
|
2100
2712
|
`json_object`, which ruby_llm has no mode for, and a `json_schema`
|
|
2101
2713
|
without a schema now raise `ArgumentError` instead of sending a request
|
|
2102
2714
|
the API rejects (#501).
|
|
2103
|
-
- **`ActiveAgent::Evals::RubyLLM`** — the RubyLLM side of an evaluation,
|
|
2104
|
-
which hosts driving RubyLLM conversations had been writing for themselves.
|
|
2105
|
-
`require "active_agent/evals/ruby_llm"` (it requires `ruby_llm`;
|
|
2106
|
-
`require "active_agent/evals"` alone still does not) gives two helpers, for
|
|
2107
|
-
RubyLLM 1.16 and later and for 2.x:
|
|
2108
|
-
- `RubyLLM.judge(label:, model:, provider:, context:, correlation:)`, a
|
|
2109
|
-
`Judge` that answers from `context.chat(...)` and, with a `Correlation`,
|
|
2110
|
-
traces each call under the kind it serves. `temperature:` and a
|
|
2111
|
-
`configure:` hook set the chat up; any other keyword must be one
|
|
2112
|
-
`RubyLLM::Chat.new` takes, and anything else raises when the judge is
|
|
2113
|
-
built rather than failing every call. `on_usage:` reports each call's
|
|
2114
|
-
model, tokens and cost.
|
|
2115
|
-
- `RubyLLM.replay(messages)`, a `Replay` from a conversation's messages.
|
|
2116
|
-
`acts_as_chat` records of either RubyLLM generation are read through
|
|
2117
|
-
their own `to_llm`, so a 2.x table, or one between 2.x's upgrade and its
|
|
2118
|
-
cleanup, replays the same as `RubyLLM::Message` values. Tool calls keep
|
|
2119
|
-
the order the model made them in, each paired with the result that
|
|
2120
|
-
answers it; one is errored when that result reports an error at its top
|
|
2121
|
-
level — JSON whose `"error"` is a non-empty string, object or array, or
|
|
2122
|
-
`true`; an MCP result with `"isError": true`; or the inspected
|
|
2123
|
-
`{ error: "..." }` RubyLLM 1.x stores for a tool's error Hash with a
|
|
2124
|
-
String error — with the error (JSON-encoded, at most 1,200 bytes) as its
|
|
2125
|
-
detail. `input_tokens` counts the whole prompt, cache reads and writes
|
|
2126
|
-
included. Cost is RubyLLM's own price, left nil when any message is
|
|
2127
|
-
unpriced. The answer is the last reply after the last user message; a
|
|
2128
|
-
conversation that stopped at a tool call or on the user's own message
|
|
2129
|
-
has no answer and says so in its `error` (on RubyLLM 1.x, pass `answer:`
|
|
2130
|
-
for a tool that ends the turn with `halt`).
|
|
2131
2715
|
- **`ActionAgent::ProviderKey.credentials_for(owner)` and
|
|
2132
2716
|
`.apply_to(config, owner:)`** (`actionagent`) hand an owner's API keys to
|
|
2133
2717
|
code outside the engine — `{ "openai" => "sk-..." }`, or written through
|