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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e041e8f2080b82e5b01648a83edfcfb7efa66cdfdb44fe814c4cca03e268816a
4
- data.tar.gz: d6bcf308c34a8eb7522137275beaa286571fdc82c13cb91a0d8de9fa4609313f
3
+ metadata.gz: 7401cdff6a7895238a082383f2ef32683b743946445e11dacbf377470bf31071
4
+ data.tar.gz: b0f847d04eedf620c7f5320950d65e5ffc712296ef7db2b7ea6be25f3bc433b7
5
5
  SHA512:
6
- metadata.gz: a9010a8e0f091c79757f282fd4f233b19468bfeb979fdecd4f00ad3b239bd60c3e39d159e17187444d56804fadde65ad748dd84fd7fb25fde9ecdfe3b081d257
7
- data.tar.gz: 17c642804ffd9dc781bbb7a55fc5f68207fbb44ad68e836509aa3657d81767fee8d0138a29e53193fde58926e2c889dfa77461ba31372e95e5782f55adfb4b29
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