activeagent 1.3.1 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +421 -0
- data/lib/active_agent/evals/design_tokens.rb +130 -0
- data/lib/active_agent/evals/diagnosis.rb +258 -0
- data/lib/active_agent/evals/judge.rb +212 -0
- data/lib/active_agent/evals/model_spec.rb +80 -0
- data/lib/active_agent/evals/publisher.rb +76 -0
- data/lib/active_agent/evals/replay.rb +63 -0
- data/lib/active_agent/evals/report.rb +450 -0
- data/lib/active_agent/evals/report_html.rb +634 -0
- data/lib/active_agent/evals/result.rb +78 -0
- data/lib/active_agent/evals/runner.rb +204 -0
- data/lib/active_agent/evals/scenario.rb +68 -0
- data/lib/active_agent/evals/scenario_parser.rb +265 -0
- data/lib/active_agent/evals/scorer.rb +118 -0
- data/lib/active_agent/evals/suite.rb +99 -0
- data/lib/active_agent/evals.rb +61 -0
- data/lib/active_agent/providers/_base_provider.rb +5 -1
- data/lib/active_agent/providers/mock/messages/base.rb +4 -2
- data/lib/active_agent/providers/open_ai/responses/transforms.rb +42 -11
- data/lib/active_agent/providers/ruby_llm/options.rb +4 -0
- data/lib/active_agent/providers/ruby_llm_provider.rb +14 -1
- data/lib/active_agent/providers/rubyllm_provider.rb +1 -0
- data/lib/active_agent/telemetry/configuration.rb +11 -0
- data/lib/active_agent/telemetry/instrumentation.rb +29 -6
- data/lib/active_agent/version.rb +1 -1
- data/lib/active_agent.rb +1 -0
- metadata +19 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7401cdff6a7895238a082383f2ef32683b743946445e11dacbf377470bf31071
|
|
4
|
+
data.tar.gz: b0f847d04eedf620c7f5320950d65e5ffc712296ef7db2b7ea6be25f3bc433b7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0a29cb99c3139f9c264eb6e86677d47ed592fb165c39489be2efc8ea2bb2dd1614bc8802643b812b04319b7356bc37177943bc9c2e0174de276555ba811ba426
|
|
7
|
+
data.tar.gz: 6fb392e7cb2f4381d79ae16567edca55d62c585b06f04a6a3da6b31cd0bb7e6149b2e67ceb7e5e143b04bc41123dd1d5bba14f38b33be6e21813e9923308d524
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,427 @@ 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
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [1.5.0] - 2026-09-10
|
|
11
|
+
|
|
12
|
+
Releases `activeagent` 1.5.0 and `actionagent` 1.5.0 from one tag.
|
|
13
|
+
|
|
14
|
+
`actionagent` goes from 1.3.0 to 1.5.0, skipping 1.4: the two gems are
|
|
15
|
+
released together from this repository and from one tag, and carrying one
|
|
16
|
+
version number across both is less confusing than explaining which
|
|
17
|
+
dashboard version pairs with which framework. `actionagent` 1.4 does not
|
|
18
|
+
exist and never will. The engine's floor on the framework
|
|
19
|
+
(`activeagent >= 1.4`) is unchanged and still correct.
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- **`ActiveAgent::Evals::Publisher` delivers a finished report to a
|
|
24
|
+
collector.** A run that already happened — in CI, in a host app's own
|
|
25
|
+
runtime, anywhere the evaluation core runs — can be sent to an
|
|
26
|
+
ActiveAgents-compatible collector without replaying the agent:
|
|
27
|
+
`Publisher.new(api_key:, endpoint:).call(report:, run_id:, source:,
|
|
28
|
+
agent_name:, suite:)` posts a version-1 envelope wrapping `Report#to_h`
|
|
29
|
+
(or the saved JSON hash of an earlier run) and returns the collector's
|
|
30
|
+
receipt. Delivery is synchronous, requires HTTPS outside loopback, does
|
|
31
|
+
not follow a redirect carrying the bearer credential, caps a request at
|
|
32
|
+
2 MiB, and raises `Publisher::Error` on anything but a receipt naming the
|
|
33
|
+
same `run_id` — so a retry with that same `run_id` and the saved report
|
|
34
|
+
re-delivers rather than re-runs. Publication is strictly opt-in and
|
|
35
|
+
happens only where an application writes the call: no configuration flag,
|
|
36
|
+
no callback, no default credential, and `api_key:` supplied explicitly at
|
|
37
|
+
the call site. That is deliberate, because the payload is the report
|
|
38
|
+
itself — every scenario's prompt, the agent's answers, and the tool calls
|
|
39
|
+
and their results — and whether that may leave the application is the
|
|
40
|
+
application's decision to make. Installing the gem sends nothing
|
|
41
|
+
anywhere. `docs/evals/publication.md` documents the envelope, the receipt
|
|
42
|
+
and the retry rules. (#414)
|
|
43
|
+
|
|
44
|
+
- **`Runner` takes `around_evaluation:` and `require_judge_scores:`.**
|
|
45
|
+
`around_evaluation:` is called with `(scenario, spec)` and a block, and
|
|
46
|
+
wraps the whole evaluation — the replay, the scoring, the judge calls
|
|
47
|
+
behind a recommendation — so a host can establish one trace context
|
|
48
|
+
across all of it and correlate a replay with the judging it triggered. It
|
|
49
|
+
must return the block's result; `on_result` runs after it returns, an
|
|
50
|
+
error it raises propagates to the caller, and `#evaluate` called directly
|
|
51
|
+
bypasses it, for a host doing its own scheduling. `require_judge_scores:`
|
|
52
|
+
(default `false`) settles what an unusable judge means. A judge that
|
|
53
|
+
raises or answers unscorably is skipped, and the scenario is then decided
|
|
54
|
+
on its rule scores alone — which reads as "the agent passed" when the
|
|
55
|
+
truth is "nobody graded the answer". Set it, and an otherwise passing
|
|
56
|
+
result whose `task_completion` or declared `llm_judge` criterion has no
|
|
57
|
+
usable score fails instead, with the new `judge_unavailable` fault naming
|
|
58
|
+
the unscored criteria and pointing at the judge's credentials, model and
|
|
59
|
+
JSON reply. A run with no judged criteria is unaffected. (#414)
|
|
60
|
+
|
|
61
|
+
- **A grouped suite imports as YAML or JSON, whole.** `ScenarioParser` read
|
|
62
|
+
a pasted list or a JSON array of scenarios; it now also reads the grouped
|
|
63
|
+
document `Suite` loads — `groups:` with per-group keys and display names,
|
|
64
|
+
scenarios carrying `key`, `prompt`, `notes`, `expect` and
|
|
65
|
+
`production_only` — from YAML or JSON, keeping every part of it.
|
|
66
|
+
`ScenarioParser.parse` and `.scenarios` gain `include_production_only:`,
|
|
67
|
+
which defaults to `true` to match `Suite`. The dashboard defaults it the
|
|
68
|
+
other way: post the document as `scenarios_text` and the production-only
|
|
69
|
+
questions stay out unless `include_production_only` is sent alongside it,
|
|
70
|
+
because those prompts run against a live agent. The choice is made at
|
|
71
|
+
import — the engine stores the scenarios it selected, not the source
|
|
72
|
+
document — so changing it means importing the document again. (#414)
|
|
73
|
+
|
|
74
|
+
- **`actionagent`: `ActionAgent.scenario_evaluation_adapter_resolver`.** A
|
|
75
|
+
host application with its own agent runtime can now run an evaluation
|
|
76
|
+
itself while keeping the dashboard's catalog, selection, jobs, result
|
|
77
|
+
persistence and report pages. The resolver is called with the persisted
|
|
78
|
+
evaluation and returns `nil` for the engine's normal `Agent#test_execute`
|
|
79
|
+
path, or a callable — `evaluation:`, `owner:`, `scenarios:`, `models:`,
|
|
80
|
+
`on_result:` — that runs the host's own agent and judge, yields every
|
|
81
|
+
result as it lands, and returns an `ActiveAgent::Evals::Report`. The
|
|
82
|
+
engine holds it to that contract: anything other than a `Report`, or a
|
|
83
|
+
report that omits or duplicates one of the selected scenario × model
|
|
84
|
+
pairs, fails the run rather than completing it with rows missing, and an
|
|
85
|
+
exception leaves the results already written in place. Dashboard
|
|
86
|
+
authentication, execution enablement and the host's execution quota still
|
|
87
|
+
apply. (#414)
|
|
88
|
+
|
|
89
|
+
- **`actionagent`: the Run Agent page is a conversation workbench.** Testing
|
|
90
|
+
an agent used to mean one prompt in, one output out, with no way to see —
|
|
91
|
+
or shape — what the model was given. The page now works the way a user
|
|
92
|
+
would work the agent: it pins a persisted conversation (a solid_agent
|
|
93
|
+
context) and every run sends that conversation's user and assistant turns
|
|
94
|
+
ahead of the new message, so follow-up questions actually follow up. The
|
|
95
|
+
context is editable in place — edit or delete a turn, seed a user or
|
|
96
|
+
assistant message without running, start a new conversation — and every
|
|
97
|
+
run is a fresh `AgentRun` with its own trace, so Traces and Interactions
|
|
98
|
+
see exactly what the model saw. Files attach to a message and ride along
|
|
99
|
+
through Active Storage (`AgentRun has_many_attached :attachments`, guarded
|
|
100
|
+
for hosts without it): images reach the model as vision input, PDFs as
|
|
101
|
+
documents, and text-like files (CSV, Markdown, JSON, plain text) are
|
|
102
|
+
inlined into the message; the persisted user message keeps an attachment
|
|
103
|
+
manifest so the conversation shows thumbnails afterwards. Assistant replies
|
|
104
|
+
can render **generative UI** — cards, stats, tables, charts, lists,
|
|
105
|
+
progress, forms, choice buttons, images, callouts and code — from a fenced
|
|
106
|
+
```` ```ui ```` JSON block in prose, a JSON reply whose top level is
|
|
107
|
+
`ui`/`blocks`, or the new `render_ui` tool (enable the **Generative UI**
|
|
108
|
+
tool on the agent). Forms and choices post their answer back into the
|
|
109
|
+
conversation as the next user message. New engine API: `GET/POST
|
|
110
|
+
/api/agents/:id/conversations`, message create/update/delete under
|
|
111
|
+
`/api/interactions/:id/messages`, multipart `POST /api/agents/:id/execute`
|
|
112
|
+
with `attachments[]` and `params[context_id]`, and attachment metadata on
|
|
113
|
+
run and message JSON. The reference host (`test/dummy`) gained the Active
|
|
114
|
+
Storage tables so the attachment path is exercised by the engine's tests.
|
|
115
|
+
Two notes for anyone driving that API directly: `execute`/`test` now answer
|
|
116
|
+
422 unless the request carries a prompt or a file, and per-run overrides in
|
|
117
|
+
`params` can no longer name `attachments` or `action` — those stay the
|
|
118
|
+
controller's to set. Model-supplied images in generative UI load on sight
|
|
119
|
+
only when they are inline data or this app's own URL; any other host is
|
|
120
|
+
offered as a click-to-load, since fetching one tells that host whatever the
|
|
121
|
+
model put in the URL.
|
|
122
|
+
|
|
123
|
+
### Fixed
|
|
124
|
+
|
|
125
|
+
- **A scenario passes only if it completed the task.** A scenario's verdict
|
|
126
|
+
was the mean of everything scored for it, and the judge's
|
|
127
|
+
`task_completion` grade was one number in that mean: an answer that
|
|
128
|
+
called the expected tool and contained the expected string could carry a
|
|
129
|
+
task grade of 0.2 to a mean of 0.73 and pass at the default threshold of
|
|
130
|
+
0.7. `task_completion` is a gate now — it has to reach `threshold` on its
|
|
131
|
+
own, and no number of passing tool and content checks can lift it — and
|
|
132
|
+
the fault names the number that failed: "Task completion scored 0.2
|
|
133
|
+
against a pass threshold of 0.7". An evaluation that configures its own
|
|
134
|
+
`llm_judge` criteria rather than relying on the implicit grade — which is
|
|
135
|
+
what the dashboard does — is gated the same way, on the mean of those
|
|
136
|
+
grades, so one soft dimension among strong ones still passes while an
|
|
137
|
+
answer the judge marked down cannot be carried by its mechanics. A
|
|
138
|
+
scenario the judge could not grade at all is unchanged, still falling
|
|
139
|
+
back to the rule scores. `score` and `avg_score` still mean the aggregate
|
|
140
|
+
they always did, and each model's
|
|
141
|
+
summary gains `avg_task_completion` so the judge's grade reads separately
|
|
142
|
+
from it. **This can turn a suite that passed on 1.4.0 red; see the note
|
|
143
|
+
on upgrading below.** (#414)
|
|
144
|
+
|
|
145
|
+
- **A judge's score is read as a JSON number.** The score was pulled out of
|
|
146
|
+
the judge's reply by regular expression, matching the first run of digits
|
|
147
|
+
after `"score":`. It read `{"score": 9e-2}` — 0.09 — as 9, clamped to a
|
|
148
|
+
perfect 1.0; it read the string `{"score": "0.9"}` and the truncated
|
|
149
|
+
`{"score": 0.9oops}` as a confident 0.9 rather than as unusable. The
|
|
150
|
+
score now comes from the parsed JSON object and has to be a finite
|
|
151
|
+
number, so exponent notation is read as written and a string, a boolean,
|
|
152
|
+
`null`, `NaN` or `1e999` is unscorable — which the runner already knows
|
|
153
|
+
how to handle. Fenced ```` ```json ```` replies still parse. The
|
|
154
|
+
dashboard's generation-sampling evaluations score through the engine's own
|
|
155
|
+
judge rather than the framework's, and read a score by the same rule now,
|
|
156
|
+
so the two halves of the dashboard no longer disagree about the same
|
|
157
|
+
reply. (#414)
|
|
158
|
+
|
|
159
|
+
- **A judge that answers with the wrong types cannot put junk in the fix
|
|
160
|
+
list.** `suggested_tool` and `instruction_change` were coerced rather
|
|
161
|
+
than checked, so a reply of `"suggested_tool": {"name": true}` added a
|
|
162
|
+
tool literally named `true` to the report's suggested tools, and
|
|
163
|
+
`"instruction_change": ["invalid"]` became a fix card asking someone to
|
|
164
|
+
add `["invalid"]` to the agent's instructions. Both fields must now be
|
|
165
|
+
nonempty strings and are dropped when they are not, so a malformed reply
|
|
166
|
+
loses only the malformed part: the judge's recommendation still reaches
|
|
167
|
+
the result, the report and every rendering of it. (#414)
|
|
168
|
+
|
|
169
|
+
- **A grouped suite pasted into the dashboard keeps its keys and
|
|
170
|
+
expectations.** Only a pasted list or JSON was recognised, so a YAML suite
|
|
171
|
+
went to the line parser and was read as prose: a document describing three
|
|
172
|
+
scenarios became eighteen, with prompts like `tools: [lookup_order]` and
|
|
173
|
+
`production_only: true`, groups named `expect`, generated keys in place of
|
|
174
|
+
the document's own, and every expectation dropped — a suite that looked
|
|
175
|
+
imported and scored nothing real. Such a document is now parsed as the
|
|
176
|
+
suite it is, and one that is not valid, or a selection that matches no
|
|
177
|
+
scenarios, returns an import error (`ScenarioParser::ParseError`, HTTP
|
|
178
|
+
422) instead of a suite of nonsense or a sampling evaluation nobody asked
|
|
179
|
+
for. (#414)
|
|
180
|
+
|
|
181
|
+
- **`actionagent`: run and result metadata survive persistence.** A run
|
|
182
|
+
rebuilt from the database was rebuilt without it: `Report#metadata` came
|
|
183
|
+
back holding only the four keys the engine writes itself, and each
|
|
184
|
+
result's replay metadata was gone entirely, so a host's own run and result
|
|
185
|
+
IDs, response trace IDs and judge trace IDs did not survive the round trip
|
|
186
|
+
and its reports could not be joined to its telemetry. Run metadata is now
|
|
187
|
+
kept in `scores["_metadata"]` and per-result metadata in
|
|
188
|
+
`diagnosis["_replay_metadata"]`, restored by `EvaluationRun#to_report` and
|
|
189
|
+
served as `metadata` on result JSON. Both are reserved storage keys that
|
|
190
|
+
the public diagnosis excludes, so nothing migrates and `diagnosis` still
|
|
191
|
+
means what it did. (#414)
|
|
192
|
+
|
|
193
|
+
- **`actionagent`: refreshing a catalog does not rewrite what an earlier run
|
|
194
|
+
asked.** A saved run rendered its scenarios from the catalog rows as they
|
|
195
|
+
are now, so rewording a question, retagging its group or changing its
|
|
196
|
+
expectations silently rewrote history — last month's report showed this
|
|
197
|
+
month's prompt above last month's answers, and judged them against
|
|
198
|
+
expectations that were not in force when they were given. Each result now
|
|
199
|
+
records the scenario it was actually evaluated against in
|
|
200
|
+
`diagnosis["_scenario_snapshot"]`, and the report, the API and the
|
|
201
|
+
scenario matrix read that snapshot, in the order the run itself used, with
|
|
202
|
+
the dashboard noting on a scenario whose catalog entry has since changed
|
|
203
|
+
that re-running uses the current one. A run records its judge the same
|
|
204
|
+
way, in `scores["_judge_label"]`: `Report#to_h` and `#to_markdown` now
|
|
205
|
+
name the judge a rebuilt run was given instead of reporting "No judge" for
|
|
206
|
+
every run reconstructed from the database. Results saved before this
|
|
207
|
+
release carry no snapshot and still render from the current catalog, and a
|
|
208
|
+
link to a saved report (`?evaluation=:id&run=:run_id`) now opens its
|
|
209
|
+
evaluation even when it is no longer on the first page of the index.
|
|
210
|
+
(#414)
|
|
211
|
+
|
|
212
|
+
- **`actionagent`: an observed agent cannot be made executable.** An agent
|
|
213
|
+
discovered from telemetry has no configuration to run, and `execute` and
|
|
214
|
+
`test` refused one — but `update` and `restore` did not, so an observed
|
|
215
|
+
record could be flipped to `active`, given instructions and then run; and
|
|
216
|
+
a run queued against an agent that became observed afterwards still
|
|
217
|
+
reached a provider when its job came up. The refusal now covers `update`
|
|
218
|
+
and `restore` as well, and it is enforced under the API rather than only
|
|
219
|
+
in front of it: `Agent#execute`, `#test_execute` and
|
|
220
|
+
`AgentExecutionService#call` raise
|
|
221
|
+
`ActionAgent::Agent::ObservedAgentError`, so that queued job fails its run
|
|
222
|
+
without a provider call or a trace. Duplicating the agent still gives you
|
|
223
|
+
an executable copy, and an evaluation whose host explicitly resolves an
|
|
224
|
+
adapter for it remains the one path that replays an observed agent's
|
|
225
|
+
scenarios. (#414)
|
|
226
|
+
|
|
227
|
+
- **The OpenAI Responses API keeps images and documents on a message with a
|
|
228
|
+
role.** `{ role: "user", text: "…", image: "…" }` — the shorthand the Chat
|
|
229
|
+
API and Anthropic transforms accept, and the only provider-neutral way to
|
|
230
|
+
send history followed by a multimodal turn — lost its `image:` or
|
|
231
|
+
`document:` on the provider the framework defaults to, because the
|
|
232
|
+
Responses transform kept only `content` from a role-bearing hash. It now
|
|
233
|
+
builds `input_text` / `input_image` / `input_file` parts for it, and a
|
|
234
|
+
media-only `{ role: "user", image: "…" }` becomes a message with one part.
|
|
235
|
+
The shorthand keys always come off the message, so a hash that carries
|
|
236
|
+
`content` *and* `image:` no longer sends `image` as an unknown parameter,
|
|
237
|
+
and a blank `image:`/`document:` contributes no part rather than an empty
|
|
238
|
+
one. A nil `document:` alongside a role was the unknown-parameter case; the
|
|
239
|
+
crash needed the role-less `{ document: nil }` inside a content array, which
|
|
240
|
+
called `start_with?` on nil.
|
|
241
|
+
|
|
242
|
+
- **`actionagent`: a dashboard run's trace is attributed to the agent that
|
|
243
|
+
ran it.** Every locally stored run used to register an "observed" twin of
|
|
244
|
+
its own agent, because a run's class and action match no authored record.
|
|
245
|
+
The service that ran the agent now names it when it records the trace.
|
|
246
|
+
It is named by that caller and never read from the payload: resource
|
|
247
|
+
attributes are whatever the reporter sent, and single-tenant ingest is
|
|
248
|
+
unauthenticated unless `ActionAgent.ingest_api_key` is set, so an id taken
|
|
249
|
+
from there would let any reporter bind its traces to any authored agent by
|
|
250
|
+
guessing a primary key. A host that swaps in its own `trace_model` should
|
|
251
|
+
add the `agent:` keyword to its `create_from_payload`; without it the
|
|
252
|
+
dashboard logs the error and records no trace for its own runs. (#405)
|
|
253
|
+
|
|
254
|
+
- **`actionagent`: an observed agent's history cannot be authored.** The
|
|
255
|
+
runner's conversation workbench writes an agent's history without running
|
|
256
|
+
it, and those two endpoints — starting a conversation, and seeding, editing
|
|
257
|
+
or deleting a turn — did not answer to the read-only rule execution does.
|
|
258
|
+
A turn typed into a telemetry mirror would be a fabrication attributed to
|
|
259
|
+
an agent whose whole point is that it only reports what really happened.
|
|
260
|
+
Both refuse an observed agent now, with the same message and status
|
|
261
|
+
`execute` gives. Reading that history is unchanged. (#405)
|
|
262
|
+
|
|
263
|
+
### Note on upgrading from 1.4.0
|
|
264
|
+
|
|
265
|
+
A scenario suite that passed on 1.4.0 can fail on this release with nothing
|
|
266
|
+
about your agent, your models or your suite having changed. Nothing has
|
|
267
|
+
regressed: the numbers those runs passed on were wrong, and this release
|
|
268
|
+
stops averaging them away.
|
|
269
|
+
|
|
270
|
+
A scenario's score was the mean of every criterion scored for it, and the
|
|
271
|
+
judge's `task_completion` grade — its answer to "did this actually do what
|
|
272
|
+
was asked" — was one term in that mean, alongside the rule checks. An answer
|
|
273
|
+
that called the expected tool, called it successfully, and contained the
|
|
274
|
+
expected string scored 1.0, 1.0 and 0.2 for a mean of 0.73, and passed at
|
|
275
|
+
the default threshold of 0.7: the mechanics carried the answer. That is the
|
|
276
|
+
wrong answer to the question an evaluation exists to ask. The agent called
|
|
277
|
+
`lookup_order`, said "ABC-123", and still never told the customer where the
|
|
278
|
+
order was — and the suite went green.
|
|
279
|
+
|
|
280
|
+
From this release `task_completion` has to clear `threshold` on its own.
|
|
281
|
+
Expect the first run after upgrading to show fewer passes than the run
|
|
282
|
+
before it, concentrated in the scenarios whose answers were thin, evasive or
|
|
283
|
+
wrong while their mechanics were right. Each of those now carries the
|
|
284
|
+
`low_quality` fault with a summary naming the grade that failed — "Task
|
|
285
|
+
completion scored 0.2 against a pass threshold of 0.7" — and the judge's
|
|
286
|
+
recommendation for it, and each model's summary reports
|
|
287
|
+
`avg_task_completion` beside `avg_score`, so a drop in pass rate can be read
|
|
288
|
+
against the grade that caused it. Nothing else about scoring moved: a
|
|
289
|
+
scenario the judge could not grade still falls back to its rule scores
|
|
290
|
+
unless you opt into `require_judge_scores: true`, and a suite meant to be
|
|
291
|
+
scored on mechanics alone can run without a judge or at a lower `threshold`.
|
|
292
|
+
Read that first run as a new baseline rather than a regression — it is
|
|
293
|
+
measuring something the runs before it were not.
|
|
294
|
+
|
|
295
|
+
## [1.4.0] - 2026-09-09
|
|
296
|
+
|
|
297
|
+
Releases `activeagent` 1.4.0 and `actionagent` 1.3.0 from one tag.
|
|
298
|
+
|
|
299
|
+
### Added
|
|
300
|
+
|
|
301
|
+
- **`ActiveAgent::Evals`, the evaluation core, in the framework.** Pasted-list
|
|
302
|
+
and YAML suite parsing, model resolution, rule and expectation scoring, the
|
|
303
|
+
fault taxonomy with its recommendations, the optional judge, and the
|
|
304
|
+
per-model report live in `lib/active_agent/evals`, loadable on their own
|
|
305
|
+
with `require "active_agent/evals"`. Any app can replay a list of tasks
|
|
306
|
+
across models against its own agent through one `replay` callable and get
|
|
307
|
+
the same faults, recommendations and verdict the dashboard shows;
|
|
308
|
+
`actionagent` keeps only what the dashboard adds — persistence, the job,
|
|
309
|
+
the API and the UI.
|
|
310
|
+
- **Scenario evaluations in the dashboard.** An evaluation can now carry a
|
|
311
|
+
suite of scenarios — a pasted list of user messages, grouped with
|
|
312
|
+
`# Heading` lines and annotated with the tool each should call — and a run
|
|
313
|
+
replays every selected scenario through the agent once per candidate model
|
|
314
|
+
(`compare_models`, or a per-run `models` selection) instead of sampling
|
|
315
|
+
recorded generations. Each scenario × model result records the answer, the
|
|
316
|
+
tools called, its score, and, when it falls short, one fault
|
|
317
|
+
(`run_error`, `tool_error`, `missing_capability`,
|
|
318
|
+
`expected_tool_not_called`, `forbidden_content`, `missing_content`,
|
|
319
|
+
`low_quality`) with a recommendation; a configured judge model refines the
|
|
320
|
+
recommendation with the tool to add or the instruction to change. Runs can
|
|
321
|
+
be narrowed to a group or to single scenarios, and the run summary ranks
|
|
322
|
+
the models by pass rate with a verdict. New tables
|
|
323
|
+
`evaluation_scenarios` and `evaluation_scenario_results` ship in
|
|
324
|
+
`create_active_agent_evaluation_scenarios`, which
|
|
325
|
+
`rails generate action_agent:install` emits for new and existing installs.
|
|
326
|
+
- **`ActionAgent.mcp_catalog`.** A host app registers the MCP servers it
|
|
327
|
+
serves or connects itself — `[{ key:, name:, tool_hints: [...] }, …]` —
|
|
328
|
+
and they join the built-in catalog: listed in the MCP Services view, with
|
|
329
|
+
telemetry traffic for their bare tool names attributed to them.
|
|
330
|
+
`MCPCatalog.keys` lists built-ins and registrations together;
|
|
331
|
+
`MCPCatalog::BY_KEY` still holds the built-ins alone.
|
|
332
|
+
- **Suite results rebuilt around what to do next.** An expanded scenario
|
|
333
|
+
suite used to be a summary and a matrix; it now opens on the three
|
|
334
|
+
questions a run is actually asked. **Runs** numbers every run of the suite
|
|
335
|
+
from the oldest and scores it against the one before — `+3 passed vs #7`,
|
|
336
|
+
or `partial run` when the two covered different scenarios or models and the
|
|
337
|
+
numbers do not compare — so progress is legible without reading two runs
|
|
338
|
+
side by side; selecting an older run re-derives the models, the fix list,
|
|
339
|
+
the matrix and every drill-down, and the collapsed header keeps reporting
|
|
340
|
+
the latest. **Models** marks the best candidate with an info-toned
|
|
341
|
+
`judge's pick` badge and the verdict that justifies it, rather than a green
|
|
342
|
+
*winner* — losing a comparison by one scenario is not a failing grade.
|
|
343
|
+
**What to fix** turns each fault into a card with the tools involved
|
|
344
|
+
(deduplicated to one chip each), the MCP server that serves them, whether
|
|
345
|
+
this agent has it enabled, and a button that deep-links to MCP Services,
|
|
346
|
+
Tools or the agent's instructions: the fix, not just the finding. The
|
|
347
|
+
scenario × model matrix shows the tools each model actually called against
|
|
348
|
+
the tools the scenario expected, coloured by whether they match, and a row
|
|
349
|
+
opens onto every model's answer, timing, cost and diagnosis.
|
|
350
|
+
- **The evaluation report is a designed page.** `Report#to_html(theme:)`
|
|
351
|
+
renders a run on the dashboard's design system — stat tiles, a panel per
|
|
352
|
+
model with the judge's pick and verdict, the what-to-fix cards, the
|
|
353
|
+
scenario × model matrix and a disclosure per scenario — still one
|
|
354
|
+
self-contained file with inline styles and no external assets, so it
|
|
355
|
+
archives next to a CI run. `theme:` pins `"light"` or `"dark"`; without it
|
|
356
|
+
the page follows the viewer's `prefers-color-scheme`, and the dashboard
|
|
357
|
+
passes its own theme through when it frames the report at
|
|
358
|
+
`/api/evaluations/:id/runs/:run_id/report`. The new `Report#fix_items`
|
|
359
|
+
builds the what-to-fix list — faults grouped with the tools each implicates
|
|
360
|
+
and the action that addresses it — for the page, the engine's API and any
|
|
361
|
+
app that wants the backlog as JSON; `tool_resolver:`, `agent_name:` and
|
|
362
|
+
`links:` on `Report.new` let a host name the MCP server behind a tool, the
|
|
363
|
+
agent, and the routes an action should point at, so a CI job gets the same
|
|
364
|
+
cards the dashboard shows.
|
|
365
|
+
- **An APM-style service overview on the Metrics page.** The page answered
|
|
366
|
+
"how much traffic in the last 24 hours"; it now answers "is this healthy
|
|
367
|
+
right now, and since when". A `1h` / `24h` / `7d` range fixes the bucket
|
|
368
|
+
size the whole page is drawn at (60 × 1 min, 96 × 15 min, 84 × 2 h); five
|
|
369
|
+
golden signals — requests, latency, error rate, tokens, cost — carry a
|
|
370
|
+
sparkline and a delta against the period just before the window; six panels
|
|
371
|
+
plot requests stacked by agent, latency percentiles, errors by class,
|
|
372
|
+
tokens, spend and tool calls, with markers for the agent versions deployed
|
|
373
|
+
inside the window and for an error spike when one stands out; and a rail
|
|
374
|
+
ranks the agents, models, slowest actions, tools and error classes behind
|
|
375
|
+
them. Filtering to an agent — from the select, or by clicking its rail
|
|
376
|
+
row — narrows every one of those together. `GET /api/metrics` gains
|
|
377
|
+
`range` and `agent` params and the keys that feed it (`totals`, `deltas`,
|
|
378
|
+
`series`, `agents`, `models`, `actions`, `tools`, `errors_by_type`,
|
|
379
|
+
`markers`) from the new `ActionAgent::MetricsReport`: one pass over the
|
|
380
|
+
window, with bucketing, nearest-rank percentiles and error classification
|
|
381
|
+
done in Ruby so PostgreSQL and SQLite report the same numbers. Every
|
|
382
|
+
earlier key and param still means what it did.
|
|
383
|
+
- **A design token layer under the dashboard.** Colors, fonts and the type
|
|
384
|
+
scale live in `actionagent/frontend/tokens.css` as CSS variables scoped to
|
|
385
|
+
the mounted dashboard (`.aa-dashboard`, with the dark palette under
|
|
386
|
+
`.theme-dark`), and the views draw from a set of shared primitives —
|
|
387
|
+
badges, chips, panels, cards, pass bars, stat tiles, segmented controls —
|
|
388
|
+
instead of each restating the same hex codes and paddings. Dark mode is
|
|
389
|
+
then one class rather than a conditional at every call site, and a host
|
|
390
|
+
app's own stylesheet cannot bleed into the engine's. The framework carries
|
|
391
|
+
the same values in `ActiveAgent::Evals::DesignTokens` so the standalone
|
|
392
|
+
HTML report matches the dashboard it came from, with a test that fails when
|
|
393
|
+
the two drift apart.
|
|
394
|
+
|
|
395
|
+
- **RubyLLM backend pinning via `platform:`.** RubyLLM resolves which of its
|
|
396
|
+
providers serves a request from the model ID, and a model served by more
|
|
397
|
+
than one — `gemini-2.5-flash` exists on both the Gemini API and Vertex
|
|
398
|
+
AI — lands on whichever RubyLLM's registry prefers, with no way to say
|
|
399
|
+
otherwise from ActiveAgent. The new `platform:` option
|
|
400
|
+
(`generate_with :ruby_llm, model: "gemini-2.5-flash", platform: :vertexai`)
|
|
401
|
+
forwards to RubyLLM's `provider:` and pins the backend, for embeddings as
|
|
402
|
+
well as prompts. It is not named `provider:` because a provider reference
|
|
403
|
+
is already the first argument to `generate_with`. Omitting it keeps
|
|
404
|
+
model-based routing unchanged. (#373)
|
|
405
|
+
|
|
406
|
+
### Fixed
|
|
407
|
+
|
|
408
|
+
- **A run report is readable in the dashboard.** The report was framed at a
|
|
409
|
+
fixed viewport height, so everything past the first screen — including
|
|
410
|
+
every fix item — sat behind a nested scrollbar. The frame is sized to the
|
|
411
|
+
report's own content, and a fix action targets the top window so it
|
|
412
|
+
navigates the dashboard instead of loading it into the frame. (#410, #411)
|
|
413
|
+
|
|
414
|
+
- **Provider credentials store on a host that skipped `db:encryption:init`.**
|
|
415
|
+
Encryption keys derived from `secret_key_base` were installed after Rails
|
|
416
|
+
had already configured `ActiveRecord::Encryption`, so the config read back
|
|
417
|
+
correct while every credential write raised `Errors::Configuration` — in
|
|
418
|
+
the dashboard, the Settings API Keys tab failed to render and provider
|
|
419
|
+
keys failed to save. (#412)
|
|
420
|
+
|
|
421
|
+
- **`service: "RubyLLM"` loads when the ruby_llm railtie has run.** The
|
|
422
|
+
ruby_llm gem registers `RubyLLM` as an inflector acronym in Rails apps,
|
|
423
|
+
which turns `"RubyLLM".underscore` into `rubyllm` — so provider loading
|
|
424
|
+
required a nonexistent `rubyllm_provider.rb` and failed with
|
|
425
|
+
`cannot load such file`. An alias file now covers that require path, the
|
|
426
|
+
same fix `openai_provider.rb` applies for `OpenAI`. (#371, fixed in #372
|
|
427
|
+
by @aoki-ryusei; regression tests in #374)
|
|
428
|
+
|
|
8
429
|
## [1.3.1] - 2026-08-19
|
|
9
430
|
|
|
10
431
|
### Fixed
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveAgent
|
|
4
|
+
module Evals
|
|
5
|
+
# The dashboard's design tokens as Ruby, so the self-contained HTML report
|
|
6
|
+
# paints with the same palette as the mounted dashboard without loading
|
|
7
|
+
# its stylesheet.
|
|
8
|
+
#
|
|
9
|
+
# Mirrors actionagent/frontend/tokens.css: LIGHT is the `.aa-dashboard`
|
|
10
|
+
# block, DARK the overrides in `.aa-dashboard.theme-dark`. A test asserts
|
|
11
|
+
# the two files stay in step — change a value in both places.
|
|
12
|
+
module DesignTokens
|
|
13
|
+
FONT_TEXT = '"Inter Variable", Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'
|
|
14
|
+
FONT_MONO = '"JetBrains Mono", "SF Mono", "Fira Code", Menlo, Consolas, "Courier New", monospace'
|
|
15
|
+
|
|
16
|
+
LIGHT = {
|
|
17
|
+
# Brand
|
|
18
|
+
"--color-accent" => "#FA343B",
|
|
19
|
+
"--color-accent-hover" => "#E02D33",
|
|
20
|
+
"--color-accent-b" => "#FA343B29",
|
|
21
|
+
"--color-accent-b-hover" => "#FA343B33",
|
|
22
|
+
"--color-on-accent" => "#ffffff",
|
|
23
|
+
"--color-accent-ui" => "#ef4444",
|
|
24
|
+
"--color-accent-ui-hover" => "#dc2626",
|
|
25
|
+
"--color-accent-ui-muted" => "rgba(239, 68, 68, 0.15)",
|
|
26
|
+
"--color-accent-ui-tint" => "#fef2f2",
|
|
27
|
+
|
|
28
|
+
# Surfaces
|
|
29
|
+
"--color-background" => "#f9fafb",
|
|
30
|
+
"--color-background-page" => "#ffffff",
|
|
31
|
+
"--color-surface" => "#ffffff",
|
|
32
|
+
"--color-card" => "#ffffff",
|
|
33
|
+
"--color-muted" => "#f3f4f6",
|
|
34
|
+
"--color-hover" => "#f3f4f6",
|
|
35
|
+
"--color-background-blur" => "rgba(255,255,255,0.9)",
|
|
36
|
+
|
|
37
|
+
# Borders
|
|
38
|
+
"--color-border" => "#e5e7eb",
|
|
39
|
+
"--color-border-light" => "#f3f4f6",
|
|
40
|
+
"--color-border-strong" => "#d1d5db",
|
|
41
|
+
|
|
42
|
+
# Text
|
|
43
|
+
"--color-text-primary" => "#111827",
|
|
44
|
+
"--color-text-secondary" => "#6b7280",
|
|
45
|
+
"--color-text-muted" => "#9ca3af",
|
|
46
|
+
"--color-text-cell" => "#4b5563",
|
|
47
|
+
|
|
48
|
+
# Semantic status
|
|
49
|
+
"--color-success" => "#16a34a",
|
|
50
|
+
"--color-success-soft" => "#dcfce7",
|
|
51
|
+
"--color-success-text" => "#166534",
|
|
52
|
+
"--color-warning" => "#eab308",
|
|
53
|
+
"--color-warning-soft" => "#fef9c3",
|
|
54
|
+
"--color-warning-text" => "#854d0e",
|
|
55
|
+
"--color-error" => "#dc2626",
|
|
56
|
+
"--color-error-soft" => "#fee2e2",
|
|
57
|
+
"--color-error-text" => "#991b1b",
|
|
58
|
+
"--color-info" => "#3b82f6",
|
|
59
|
+
"--color-info-soft" => "#dbeafe",
|
|
60
|
+
"--color-info-text" => "#1e40af",
|
|
61
|
+
|
|
62
|
+
# Trace span colors (observability)
|
|
63
|
+
"--span-root" => "#9ca3af",
|
|
64
|
+
"--span-prompt" => "#60a5fa",
|
|
65
|
+
"--span-generate" => "#a855f7",
|
|
66
|
+
"--span-llm" => "#ef4444",
|
|
67
|
+
"--span-thinking" => "#fbbf24",
|
|
68
|
+
"--span-tool" => "#22c55e",
|
|
69
|
+
"--span-response" => "#2dd4bf",
|
|
70
|
+
|
|
71
|
+
# Token flow colors
|
|
72
|
+
"--color-token-in" => "#2563eb",
|
|
73
|
+
"--color-token-out" => "#7c3aed",
|
|
74
|
+
|
|
75
|
+
# Chart / agent palette
|
|
76
|
+
"--chart-1" => "#6366f1",
|
|
77
|
+
"--chart-2" => "#10b981",
|
|
78
|
+
"--chart-3" => "#f59e0b",
|
|
79
|
+
"--chart-4" => "#ec4899",
|
|
80
|
+
"--chart-5" => "#3b82f6",
|
|
81
|
+
|
|
82
|
+
# Type
|
|
83
|
+
"--font-text" => FONT_TEXT,
|
|
84
|
+
"--font-mono" => FONT_MONO
|
|
85
|
+
}.freeze
|
|
86
|
+
|
|
87
|
+
DARK = {
|
|
88
|
+
"--color-accent-ui-tint" => "rgba(239, 68, 68, 0.15)",
|
|
89
|
+
|
|
90
|
+
"--color-background" => "#0f0f0f",
|
|
91
|
+
"--color-background-page" => "#0f0f0f",
|
|
92
|
+
"--color-surface" => "#1a1a1a",
|
|
93
|
+
"--color-card" => "rgba(255,255,255,0.05)",
|
|
94
|
+
"--color-muted" => "rgba(255,255,255,0.05)",
|
|
95
|
+
"--color-hover" => "#252525",
|
|
96
|
+
"--color-background-blur" => "rgba(15,15,15,0.9)",
|
|
97
|
+
|
|
98
|
+
"--color-border" => "rgba(255,255,255,0.1)",
|
|
99
|
+
"--color-border-light" => "rgba(255,255,255,0.05)",
|
|
100
|
+
"--color-border-strong" => "rgba(255,255,255,0.2)",
|
|
101
|
+
|
|
102
|
+
"--color-text-primary" => "#ffffff",
|
|
103
|
+
"--color-text-secondary" => "rgba(255,255,255,0.6)",
|
|
104
|
+
"--color-text-muted" => "rgba(255,255,255,0.4)",
|
|
105
|
+
"--color-text-cell" => "rgba(255,255,255,0.7)",
|
|
106
|
+
|
|
107
|
+
"--color-success-soft" => "rgba(22,163,74,0.15)",
|
|
108
|
+
"--color-success-text" => "#4ade80",
|
|
109
|
+
"--color-warning-soft" => "rgba(234,179,8,0.15)",
|
|
110
|
+
"--color-warning-text" => "#facc15",
|
|
111
|
+
"--color-error-soft" => "rgba(220,38,38,0.15)",
|
|
112
|
+
"--color-error-text" => "#f87171",
|
|
113
|
+
"--color-info-soft" => "rgba(59,130,246,0.15)",
|
|
114
|
+
"--color-info-text" => "#93c5fd"
|
|
115
|
+
}.freeze
|
|
116
|
+
|
|
117
|
+
# One CSS rule declaring `tokens` as custom properties on `scope`, with
|
|
118
|
+
# an optional `color-scheme` so form controls and scrollbars follow:
|
|
119
|
+
#
|
|
120
|
+
# DesignTokens.css(scope: ":root") # light
|
|
121
|
+
# DesignTokens.css(scope: ":root.theme-dark", tokens: DesignTokens::DARK,
|
|
122
|
+
# color_scheme: "dark") # dark overrides
|
|
123
|
+
def self.css(scope:, tokens: LIGHT, color_scheme: nil)
|
|
124
|
+
declarations = tokens.map { |name, value| " #{name}: #{value};" }
|
|
125
|
+
declarations.unshift(" color-scheme: #{color_scheme};") if color_scheme
|
|
126
|
+
"#{scope} {\n#{declarations.join("\n")}\n}"
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|