activeagent 1.9.1 → 1.9.3
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 +407 -1
- data/README.md +3 -0
- data/lib/active_agent/base.rb +29 -4
- data/lib/active_agent/delegation/schema.rb +51 -13
- data/lib/active_agent/evals/comparison.rb +169 -0
- data/lib/active_agent/evals/publisher.rb +19 -12
- data/lib/active_agent/evals/report.rb +82 -66
- data/lib/active_agent/evals/report_html.rb +149 -12
- data/lib/active_agent/evals/report_markdown.rb +202 -0
- data/lib/active_agent/evals/runner.rb +7 -1
- data/lib/active_agent/evals/statistics.rb +26 -0
- data/lib/active_agent/evals.rb +3 -0
- data/lib/active_agent/providers/_base_provider.rb +2 -0
- data/lib/active_agent/providers/anthropic_provider.rb +4 -1
- data/lib/active_agent/providers/common/responses/base.rb +15 -2
- data/lib/active_agent/providers/common/usage.rb +4 -2
- data/lib/active_agent/providers/open_ai/chat_provider.rb +46 -0
- data/lib/active_agent/providers/open_ai/responses_provider.rb +40 -4
- data/lib/active_agent/providers/opper/_types.rb +16 -0
- data/lib/active_agent/providers/opper/options.rb +38 -0
- data/lib/active_agent/providers/opper_provider.rb +72 -0
- data/lib/active_agent/schema_generator.rb +146 -22
- data/lib/active_agent/version.rb +1 -1
- metadata +8 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a7ecc64cad33188dd3cdfc563b553e49d0a05d42192698365a467061e6bb6d59
|
|
4
|
+
data.tar.gz: c3923cee8e401e17e3170c28f2fc83d8c290d70c5a4bc4d301951031d7d9cf43
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 66e3244ce04d56625f880e26c18cbe3f353f35eca2f4dd5922a1adafb1d542ee5216065401e7ddbbe88cccc1c40336076ab6d4a30d54a5360846cad595ae9717
|
|
7
|
+
data.tar.gz: b023e5fe43f6674b7040086cc5a47c19fffc4bf3e919d0bb0ab0138d5c4839d3463f09e5a66d7047e4e28262628465a066b343ee76a503732b803ddc5d270bff
|
data/CHANGELOG.md
CHANGED
|
@@ -5,7 +5,413 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
-
## [
|
|
8
|
+
## [1.9.3] - 2026-10-21
|
|
9
|
+
|
|
10
|
+
Releases `activeagent` and `actionagent` 1.9.3, with the matching browser
|
|
11
|
+
sidecar: browser upload and code conversations (the sandbox browser's live
|
|
12
|
+
view takes files from the person driving it, and a Claude Code or Codex
|
|
13
|
+
session is a conversation with the sandbox's browser beside the chat and a
|
|
14
|
+
hand-off either way, Codex on this machine's own login), evaluation run
|
|
15
|
+
reports exported as JSON, HTML and Markdown with Copy as Markdown, an agent
|
|
16
|
+
builder that offers only tools that work, a model leaderboard on the
|
|
17
|
+
Evaluations page, complete trace window statistics, a judge pin that runs
|
|
18
|
+
on its own provider, Opper as a provider and Parallel Search in the MCP
|
|
19
|
+
catalog, and streaming fixes (unindexed tool call fragments, the finish
|
|
20
|
+
reason of streamed and Responses generations, Anthropic cache-token usage).
|
|
21
|
+
The framework, the dashboard engine and `@activeagents/browser-sidecar`
|
|
22
|
+
move to 1.9.3 together.
|
|
23
|
+
|
|
24
|
+
Upgrading: migration 028 adds the conversation columns to `code_sessions`.
|
|
25
|
+
A host runs `bin/rails generate action_agent:install --skip` then
|
|
26
|
+
`bin/rails db:migrate`, after 1.9.1's 027. Upgrade the sidecar with the
|
|
27
|
+
engine: the file chooser relay and the per-turn browser tokens are in the
|
|
28
|
+
1.9.3 sidecar. On a single-tenant install the new report export
|
|
29
|
+
(`GET <mount>/api/evaluation_reports/:run_id`) answers 401 until
|
|
30
|
+
`ActionAgent.ingest_api_key` is set. A `judge_model` pin written
|
|
31
|
+
`anthropic/…` or `openai/…` now runs on that native provider; an install
|
|
32
|
+
that reached such a model through OpenRouter pins `openrouter/anthropic/…`.
|
|
33
|
+
|
|
34
|
+
### Added
|
|
35
|
+
|
|
36
|
+
- **Export evaluation run reports as JSON, HTML and Markdown** (`activeagent`,
|
|
37
|
+
`actionagent`). `GET <mount>/api/evaluation_reports/:run_id`, with the
|
|
38
|
+
ingest key and scoped to the tenant like the POST (and refused on a
|
|
39
|
+
single-tenant install that set no key), returns a complete run
|
|
40
|
+
as the version-1 envelope the collector takes, so a run moves to another
|
|
41
|
+
install with one POST and posting it back is an identical retry (200);
|
|
42
|
+
`?format=html` returns the run's report page and `?format=md` its
|
|
43
|
+
Markdown, and `?download=1` an attachment of any of them.
|
|
44
|
+
An imported run exports under its published `run_id`, a dashboard run
|
|
45
|
+
under `dashboard-run-<id>-<created_at>`. `ActiveAgent::Evals::Publisher.envelope`
|
|
46
|
+
builds the envelope for both the publisher and the export. The dashboard's
|
|
47
|
+
run report adds `report.json` and `report.md` (one piece with
|
|
48
|
+
`?part=verdict|fix_items|scenario`), and `?download=1` saves any format
|
|
49
|
+
as `<evaluation>-run-<n>.<ext>`; the run report view and the suite's run
|
|
50
|
+
header get *Download HTML / JSON / Markdown*. New rake tasks:
|
|
51
|
+
`action_agent:evaluations:export RUN=… [EVALUATION=…] FORMAT=json|html|md [FILE=…]`
|
|
52
|
+
and `action_agent:evaluations:import FILE=… [ACCOUNT_ID=…]`.
|
|
53
|
+
|
|
54
|
+
- **Copy as Markdown** (`activeagent`, `actionagent`). `Report#to_markdown`
|
|
55
|
+
now has the HTML page's content in GitHub-flavored Markdown: the verdict's
|
|
56
|
+
rationale, What to fix from `Report#fix_items` (replacing the old
|
|
57
|
+
Recommendations list), and every scenario's answers in a `<details>` block
|
|
58
|
+
with a per-model table; table cells escape `|` and line breaks. Its pieces
|
|
59
|
+
are public — `verdict_markdown`, `fix_items_markdown(model:)`,
|
|
60
|
+
`fix_item_markdown(item)`, `scenario_markdown(key)` — and the dashboard
|
|
61
|
+
and the standalone HTML page copy each of them: the whole report, the
|
|
62
|
+
verdict with the comparison, What to fix for the filtered model, a fix
|
|
63
|
+
card, and a scenario. The HTML page now carries one inline script for
|
|
64
|
+
those buttons, with a clipboard fallback.
|
|
65
|
+
|
|
66
|
+
- **Upload files to a page through the sandbox browser's live view**
|
|
67
|
+
(`actionagent`, `@activeagents/browser-sidecar`). A file chooser the page
|
|
68
|
+
opens while someone drives is answered from the driver's own machine
|
|
69
|
+
(picker or drop), over the live-view WebSocket.
|
|
70
|
+
|
|
71
|
+
- **Claude Code and Codex sessions as Interactions, with chat and browser
|
|
72
|
+
handoff** (`actionagent`, `@activeagents/browser-sidecar`). A coding
|
|
73
|
+
session is now a conversation: a later message resumes the CLI session
|
|
74
|
+
(`--resume`, or `codex exec resume` for Codex), a message sent while a
|
|
75
|
+
turn runs waits for it, and
|
|
76
|
+
`POST /api/sandboxes/:sandbox_id/code_sessions/:id/messages` starts or
|
|
77
|
+
queues a turn under the new `:chat_code_session` permission.
|
|
78
|
+
`GET /api/interactions` lists agent, Claude Code and Codex interactions
|
|
79
|
+
together, and the dashboard's `/interactions/code-<id>` shows the turns
|
|
80
|
+
with a composer and the sandbox's browser beside the chat, with a hand
|
|
81
|
+
back to the agent. The sidecar mints a per-turn token for the browser
|
|
82
|
+
MCP, tells the agent once when a person took or returned control, lets
|
|
83
|
+
the agent ask for a person (`browser_request_person`), offers a person
|
|
84
|
+
the file chooser the agent left open, and names the waiting agent in the
|
|
85
|
+
live view. Every Claude Code session runs with `--strict-mcp-config`, and
|
|
86
|
+
every session gets a system note (on stdin, for Codex) that repository
|
|
87
|
+
content, tool output and pages are data.
|
|
88
|
+
|
|
89
|
+
- **Codex turns reach the sandbox browser, and Codex can run on this
|
|
90
|
+
machine's own login** (`actionagent`). A Codex turn now gets the same
|
|
91
|
+
per-turn browser token as a Claude Code turn: the sidecar's MCP server
|
|
92
|
+
goes in as `--config 'mcp_servers.browser.url="…"'`, with the token in an
|
|
93
|
+
environment variable Codex reads (`bearer_token_env_var`) rather than on
|
|
94
|
+
the command line, the server's tools pre-approved
|
|
95
|
+
(`default_tools_approval_mode="approve"`, since Codex would otherwise
|
|
96
|
+
refuse every tool not marked read-only under `approval_policy="never"`),
|
|
97
|
+
and is revoked when the turn ends; hand-offs and
|
|
98
|
+
`browser_request_person` work the same. `config.codex_auth = :local_login`
|
|
99
|
+
(with the `:local` backend) runs Codex on whatever `codex login` set up
|
|
100
|
+
for the dashboard's OS user, a ChatGPT plan included, with
|
|
101
|
+
`--ignore-user-config` so none of the user's own `config.toml` reaches a
|
|
102
|
+
session; the dashboard never reads the credential and only asks `codex
|
|
103
|
+
login status` whether there is one. `GET /api/sandboxes` adds `codex_auth`
|
|
104
|
+
and, in that mode, `codex_login`, the assistant's configuration a
|
|
105
|
+
`connections.codex`, and the Codex card in Settings shows the machine's
|
|
106
|
+
login instead of a key form. The default stays `:api_key`.
|
|
107
|
+
|
|
108
|
+
- **The agent builder offers only tools that work, and every built-in
|
|
109
|
+
capability has functions** (`actionagent`). The Configure step reads the
|
|
110
|
+
agent's tool roster (`GET /api/agents/tool_roster` for an agent not
|
|
111
|
+
created yet) instead of fixed chips, offers the MCP servers a dashboard
|
|
112
|
+
run can call, and names each one it cannot with the reason
|
|
113
|
+
(`MCPToolDispatcher.unreachable_reason`: a stdio server, no URL, missing
|
|
114
|
+
credentials). The six capabilities that gave the model nothing now have
|
|
115
|
+
functions: `terminal` (`run_command`), `filesystem` (`list_files`,
|
|
116
|
+
`read_file`, `write_file`), `edit` (`edit_file`) and `database`
|
|
117
|
+
(`query_database`, read-only) act in the run's checkout sandbox through
|
|
118
|
+
new `SandboxOrchestrator` verbs (on the `:local` backend; others return
|
|
119
|
+
`UnsupportedBackendError` to the model); `translate` (`translate_text`)
|
|
120
|
+
generates on the run's own provider; `slack` lists, reads and posts with
|
|
121
|
+
a bot token connected in Settings -> Integrations -> Slack (a `slack`
|
|
122
|
+
connection `ProviderKey`). Instruction sets are composed into the
|
|
123
|
+
instructions a run executes under. `run_command`, `write_file`,
|
|
124
|
+
`edit_file` and `slack_post_message` wait for approval by default: an
|
|
125
|
+
agent that gains `terminal`, `filesystem`, `edit` or `slack` — from the
|
|
126
|
+
builder, a template, the Tools tab or the agents API — has them on its
|
|
127
|
+
`approval_required_tools` unless the save names its own list, and the
|
|
128
|
+
Tools tab's Approval switch takes them off. An agent that enabled one of
|
|
129
|
+
those capabilities before this release keeps its list as it is. A host's
|
|
130
|
+
`ActionAgent.mcp_catalog` headers now reach the MCP client (they were
|
|
131
|
+
dropped).
|
|
132
|
+
|
|
133
|
+
- **Start an interaction with your own agent from Interactions and Run
|
|
134
|
+
Agents** (`actionagent`). Interactions has a "New interaction" picker and
|
|
135
|
+
Run Agents a "Your agents" list; both open the agent's runner on a fresh
|
|
136
|
+
conversation (`/agents/:id/run?new=1`). The runner lists the agent's tools
|
|
137
|
+
and MCP servers, and can run against a checkout sandbox (`sandbox_id`),
|
|
138
|
+
which the checkout tools need.
|
|
139
|
+
|
|
140
|
+
- **A model leaderboard on the Evaluations page** (`actionagent`). The Pass
|
|
141
|
+
rate tile listed each model's passes on a line of its own, a dozen lines
|
|
142
|
+
for seven models; it now shows the overall rate, the leading model and a
|
|
143
|
+
"compare N models" toggle, which opens a leaderboard below the tiles: one
|
|
144
|
+
row per model with its pooled passes and rate, and one column per counted
|
|
145
|
+
evaluation (`—` where the model did not run). Models rank by the lower
|
|
146
|
+
bound of the Wilson score interval (95%), so a lucky 7/8 does not outrank
|
|
147
|
+
75/173.
|
|
148
|
+
|
|
149
|
+
- **Opper as an OpenAI-compatible provider** (`activeagent`). `service:
|
|
150
|
+
"Opper"` generates and embeds through Opper's compatibility endpoint
|
|
151
|
+
(`https://api.opper.ai/v3/compat`), with the key from `OPPER_API_KEY`
|
|
152
|
+
when none is configured, as the Requesty provider does (#583).
|
|
153
|
+
|
|
154
|
+
- **Parallel Search in the MCP catalog** (`actionagent`). An opt-in,
|
|
155
|
+
keyless `parallel` server (`web_search`, `web_fetch`) over HTTP; queries
|
|
156
|
+
and URLs go to parallel.ai. Every MCP HTTP request now sends
|
|
157
|
+
`User-Agent: ActionAgent/<version>` unless a header names another (#575).
|
|
158
|
+
|
|
159
|
+
### Fixed
|
|
160
|
+
|
|
161
|
+
- **Trace totals and timeline buckets cover the whole filtered window**
|
|
162
|
+
(`actionagent`). `GET /api/traces` adds `total_count`, `returned_count`,
|
|
163
|
+
`truncated`, a `summary` (requests, errors, average latency, throughput
|
|
164
|
+
per minute) and zero-filled `buckets` computed over every trace the
|
|
165
|
+
filters match, independently of the capped detail list, and the Traces
|
|
166
|
+
view reports truncation and averages recorded request durations instead
|
|
167
|
+
of time-bucket means (part of #384).
|
|
168
|
+
|
|
169
|
+
- **A pinned evaluation judge runs on its own provider** (`actionagent`).
|
|
170
|
+
A `judge_model` pin selects the model's native provider, or the one its
|
|
171
|
+
qualifier names (`openrouter/anthropic/…`), instead of the first provider
|
|
172
|
+
with credentials; the qualifier is stripped before the request, the pin
|
|
173
|
+
wins over the credential resolver's default model, and a pinned route
|
|
174
|
+
with no credentials fails the run clearly when another provider is
|
|
175
|
+
configured. Native-to-gateway alias conversion stays out of scope (#431).
|
|
176
|
+
Upgrading: a pin written `anthropic/…` or `openai/…` now selects that
|
|
177
|
+
native provider, as candidate model names already did; an install that
|
|
178
|
+
reached such a model through OpenRouter pins `openrouter/anthropic/…`.
|
|
179
|
+
|
|
180
|
+
- **Streamed tool calls without an `index` are no longer split into
|
|
181
|
+
fragments** (`activeagent`). OpenAI-compatible servers (Gemini's
|
|
182
|
+
compatibility endpoint, some Ollama and OpenRouter backends) stream tool
|
|
183
|
+
calls without the `index` that ties argument fragments to their call, and
|
|
184
|
+
the Chat provider turned every fragment into its own tool call with
|
|
185
|
+
partial arguments. A fragment now continues the call with the same `id`,
|
|
186
|
+
or the last call when it has no `id` and no function name.
|
|
187
|
+
|
|
188
|
+
- **A streamed generation reports why it stopped** (`activeagent`).
|
|
189
|
+
`response.finish_reason` (and `stop_reason`), and the `finish_reason` of
|
|
190
|
+
the `prompt.active_agent` event, were `nil` for every streamed Chat and
|
|
191
|
+
Anthropic generation, since a stream has no response body to read it
|
|
192
|
+
from. The Chat provider now records the chunk's `finish_reason` and the
|
|
193
|
+
Anthropic provider the `message_delta`'s `stop_reason`, and the response
|
|
194
|
+
falls back to it.
|
|
195
|
+
|
|
196
|
+
- **OpenAI Responses generations report why they stopped** (`activeagent`).
|
|
197
|
+
`response.finish_reason` (and `stop_reason`) was `nil` for every
|
|
198
|
+
Responses generation, streamed or not. It is now
|
|
199
|
+
`incomplete_details.reason` when the response was cut short
|
|
200
|
+
(`max_output_tokens`, `content_filter`), else the response's `status`
|
|
201
|
+
(`completed`).
|
|
202
|
+
|
|
203
|
+
- **Anthropic usage with cache token fields no longer raises** (`activeagent`).
|
|
204
|
+
`Usage.from_provider_usage` recognised Anthropic's usage only by
|
|
205
|
+
`cache_creation` or `service_tier`; a payload carrying
|
|
206
|
+
`cache_read_input_tokens` or `cache_creation_input_tokens` without them
|
|
207
|
+
fell through to `Usage.new` and raised `UnknownAttributeError`. The cache
|
|
208
|
+
token fields now mark the payload as Anthropic's (#611).
|
|
209
|
+
|
|
210
|
+
## [1.9.2] - 2026-10-20
|
|
211
|
+
|
|
212
|
+
Releases `activeagent` and `actionagent` 1.9.2, with the matching browser
|
|
213
|
+
sidecar: evaluations at scale (the scenario suite as a build page, model
|
|
214
|
+
comparison with intervals and P95 latency, opt-in parallel scenario runs,
|
|
215
|
+
the judge named after its model), provider streaming fixes (Anthropic
|
|
216
|
+
citations, OpenAI Chat refusals and logprobs, Responses built-in tools),
|
|
217
|
+
nested `has_many` schemas, and delegation contracts from `SchemaGenerator`
|
|
218
|
+
classes.
|
|
219
|
+
|
|
220
|
+
Upgrading: structured-output property names are now sent to the provider,
|
|
221
|
+
and answered, as declared. A `response_format` schema with multiword
|
|
222
|
+
property names (`first_name`) used to reach the provider camelized
|
|
223
|
+
(`firstName`) while `required` stayed snake_case, which strict mode
|
|
224
|
+
rejected; both are now as declared. `parsed_json` is unaffected; code that
|
|
225
|
+
reads `message.content` itself, or matches on request payloads, sees
|
|
226
|
+
`first_name` where it saw `firstName`. In a Rails app, `has_many` is
|
|
227
|
+
available on every `ActiveModel::Model` class, since the railtie includes
|
|
228
|
+
`SchemaGenerator` there, not only on classes that include it themselves.
|
|
229
|
+
Scenario fan-out is off by default: `config.scenario_evaluation_fan_out =
|
|
230
|
+
true` turns it on, and its jobs run on the `:agents` queue. No new
|
|
231
|
+
migration; a host still on 1.9.0 needs 1.9.1's migration 027.
|
|
232
|
+
|
|
233
|
+
### Added
|
|
234
|
+
|
|
235
|
+
- **P50 and P95 latency per model in evaluation reports** (`activeagent`).
|
|
236
|
+
`Report#summary_by_model` adds `p50_duration_ms` and `p95_duration_ms`
|
|
237
|
+
beside `avg_duration_ms`, by nearest rank as the dashboard's Metrics view
|
|
238
|
+
computes them, and the Markdown and HTML reports show them next to the
|
|
239
|
+
mean, which hides the tail (#428).
|
|
240
|
+
|
|
241
|
+
- **Cost per 1,000 scenario runs in the Markdown report** (`activeagent`).
|
|
242
|
+
The summary table gains `Cost / 1,000`, each model's mean cost per priced
|
|
243
|
+
replay on a common denominator, marked `~` when it is estimated or
|
|
244
|
+
covers only some replays (#428).
|
|
245
|
+
|
|
246
|
+
- **`ActiveAgent::Evals::Comparison`: is the gap between models real?**
|
|
247
|
+
(`activeagent`). A 95% interval for each model's pass rate and for the
|
|
248
|
+
difference between every pair of models, from 5,000 seeded resamples of
|
|
249
|
+
whole scenarios (a paired bootstrap for the pairs, over the scenarios both
|
|
250
|
+
ran). A pair is decisive when its interval excludes zero, and
|
|
251
|
+
`#conclusion` says in one sentence which pairs are, or that the ranking
|
|
252
|
+
is not decisive evidence of superiority (#428).
|
|
253
|
+
|
|
254
|
+
- **The model comparison in evaluation reports** (`activeagent`).
|
|
255
|
+
`Report#comparison` and `to_h["comparison"]` carry it for a report that
|
|
256
|
+
compares models, the Markdown report gains a Comparison section, and
|
|
257
|
+
`Report.new(comparison:)` renders one already recorded for the results
|
|
258
|
+
instead of resampling, as `verdict:` does (#428).
|
|
259
|
+
|
|
260
|
+
- **A ranked interval plot in the HTML evaluation report** (`activeagent`).
|
|
261
|
+
Under the comparison table, each model's pass rate is a dot inside its
|
|
262
|
+
95% interval on a fixed 0 to 100% axis, best first, with the figures
|
|
263
|
+
beside it, the conclusion, and how the intervals were computed. Plain
|
|
264
|
+
CSS on the design tokens: no script, light and dark (#428).
|
|
265
|
+
|
|
266
|
+
- **Each comparison run keeps its model comparison** (`actionagent`). A
|
|
267
|
+
scenario run that compares models records `Report#comparison` under
|
|
268
|
+
`scores["_comparison"]` when it completes; a published report's is
|
|
269
|
+
worked out from the stored results rather than taken from the payload;
|
|
270
|
+
and the run's report page renders the recorded intervals instead of
|
|
271
|
+
resampling on every view. A run recorded before gets one from its
|
|
272
|
+
results (#428).
|
|
273
|
+
|
|
274
|
+
- **P50 and P95 latency in the dashboard's model comparison**
|
|
275
|
+
(`actionagent`). The comparison table gains a P50 / P95 column and each
|
|
276
|
+
model's scorecard a p50 / p95 figure, from the percentiles the run
|
|
277
|
+
recorded; a run recorded before shows neither (#428).
|
|
278
|
+
|
|
279
|
+
- **The model comparison plotted in the dashboard** (`actionagent`). A
|
|
280
|
+
run that compared models shows, under its comparison table, each model's
|
|
281
|
+
pass rate inside its 95% interval, best first, with the run's
|
|
282
|
+
conclusion and how the intervals were computed, from the comparison the
|
|
283
|
+
run recorded (#428).
|
|
284
|
+
|
|
285
|
+
- Parallel scenario evaluations in `actionagent`: with
|
|
286
|
+
`config.scenario_evaluation_fan_out = true`, a run enqueues one
|
|
287
|
+
`EvaluationReplayJob` per scenario × model, each stored replay is scored by
|
|
288
|
+
its own `EvaluationJudgeJob`, and the last result to be scored completes the
|
|
289
|
+
run with the scores a sequential run writes. Runs as wide as the host's
|
|
290
|
+
`:agents` queue. Off by default; browser and host-adapter runs stay
|
|
291
|
+
sequential (#422).
|
|
292
|
+
- `ActiveAgent::Evals::Runner.new(refine_gate:)`, a callable asked before each
|
|
293
|
+
recommendation call so per-scenario runners share one run-wide judge limit.
|
|
294
|
+
|
|
295
|
+
- **Nested arrays of objects in ActiveModel schemas** (`activeagent`).
|
|
296
|
+
`has_many :addresses, class_name: "Address"` on an ActiveModel class that
|
|
297
|
+
includes `ActiveAgent::SchemaGenerator` makes `to_json_schema` emit the
|
|
298
|
+
attribute as an array of `$ref`s to the item class's schema, which goes
|
|
299
|
+
under the root `$defs`. `strict:` and `additional_properties:` reach the
|
|
300
|
+
nested schema, a nested class's own definitions move up to the root, and
|
|
301
|
+
a class that nests itself raises `ArgumentError`. A length validation on
|
|
302
|
+
the array sets its item count (`min_items` / `max_items`) rather than a
|
|
303
|
+
string's length. `has_many` also declares the attribute with an
|
|
304
|
+
empty-array default, and is added only to a class without one, so
|
|
305
|
+
ActiveRecord's is untouched (#260).
|
|
306
|
+
|
|
307
|
+
- **Evaluation runs record their judge** (`actionagent`), in
|
|
308
|
+
`scores["_judge"]`: the `EvaluationJudgeAgent` it runs as, its provider,
|
|
309
|
+
the model it asked for and where that setting came from (the evaluation,
|
|
310
|
+
the provider key, the host's config, or the provider's default), and the
|
|
311
|
+
models that answered, with call counts. The suite's Models panel shows
|
|
312
|
+
"judged by <model>" with an `auto` badge when no model is pinned, and
|
|
313
|
+
opens the judge's configuration, with a field that pins `judge_model` on
|
|
314
|
+
the evaluation through `PATCH /api/evaluations/:id` (#605).
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
### Changed
|
|
318
|
+
|
|
319
|
+
- **A scenario suite's page reads like a build page** (`actionagent`). The
|
|
320
|
+
page opened on a summary, every run, the costs, the models and every
|
|
321
|
+
scenario stacked down the page, so on a suite of 100 scenarios and 12 runs
|
|
322
|
+
the selected run's results started three screens down. It now opens on the
|
|
323
|
+
selected run: a run header with the run's passed count, models, agent and
|
|
324
|
+
judge spend, a to-fix count and a progress bar; beside it a rail of runs,
|
|
325
|
+
each with a pass bar per model, and a box of the run's scenarios that
|
|
326
|
+
scrolls on its own, one line per step (`[+]` passed, `[!]` failed, `[~]`
|
|
327
|
+
running, `[ ]` queued, `[?]` errored, `[-]` skipped) under a sticky header
|
|
328
|
+
and foldable group rows, with a step's detail opening in place. A toolbar
|
|
329
|
+
filters the steps to failed only, puts failed first, shows the tool cells
|
|
330
|
+
and follows the running step until the person scrolls. Models, what to fix
|
|
331
|
+
and the footer follow below; a failed run shows its error in the header
|
|
332
|
+
instead of models. Selecting a run keeps the page where it is, polling
|
|
333
|
+
backs off and stops in a hidden tab, and below 940px the rail becomes a
|
|
334
|
+
strip of the latest runs. The same suite is now 2,786px tall where it was
|
|
335
|
+
11,354px (#602).
|
|
336
|
+
|
|
337
|
+
### Fixed
|
|
338
|
+
|
|
339
|
+
- **`max_retries` sets the Anthropic `json_object` retries** (`activeagent`).
|
|
340
|
+
`AnthropicProvider` read the count from its constructor's arguments after
|
|
341
|
+
`BaseProvider` had already moved `max_retries` into the provider's options,
|
|
342
|
+
so an unparseable `json_object` answer was always retried twice, whatever
|
|
343
|
+
`max_retries` said, on Anthropic and on Bedrock, which inherits it. The
|
|
344
|
+
count now comes from the options: `max_retries: 0` sends the request once,
|
|
345
|
+
`max_retries: 5` up to six times, and the default is still two retries
|
|
346
|
+
(#531).
|
|
347
|
+
- **A streamed Anthropic answer with citations no longer raises.** Claude
|
|
348
|
+
streams the citations of a text block as `citations_delta` deltas when it
|
|
349
|
+
cites a document or a web search result, and the Anthropic provider raised
|
|
350
|
+
`Unexpected delta type: citations_delta` on the first one. The delta is now
|
|
351
|
+
skipped like `input_json_delta`: the full block, citations included, arrives
|
|
352
|
+
at `content_block_stop`.
|
|
353
|
+
- **A streamed OpenAI Chat refusal, or a stream with logprobs, no longer
|
|
354
|
+
raises.** The openai gem's chat stream emits `refusal.delta` and
|
|
355
|
+
`refusal.done` when the model refuses, and `logprobs.*` events when the
|
|
356
|
+
request sets `logprobs`, and the Chat provider failed on them with
|
|
357
|
+
`Unexpected Response Event Type`. They are now handled, and `refusal.done`
|
|
358
|
+
ends the generation, since a refused message never gets a `content.done`.
|
|
359
|
+
- **A streamed Responses answer that used a built-in tool no longer raises.**
|
|
360
|
+
With `tools: [{ type: "web_search_preview" }]` (or file search, image
|
|
361
|
+
generation, code interpreter, remote MCP), the stream carries the tool's
|
|
362
|
+
call as an output item (`web_search_call`, ...), its progress events
|
|
363
|
+
(`response.web_search_call.searching`, ...), and
|
|
364
|
+
`response.output_text.annotation.added` for the citations, and the
|
|
365
|
+
Responses provider raised `Unexpected Item Type: web_search_call` on the
|
|
366
|
+
first of them. The tool calls are now kept like in a non-streamed response,
|
|
367
|
+
and their events are skipped.
|
|
368
|
+
- **A streamed Responses answer cut short no longer raises.** When a response
|
|
369
|
+
hits `max_output_tokens`, the stream ends with `response.incomplete` instead
|
|
370
|
+
of `response.completed`, and the Responses provider raised `Unexpected
|
|
371
|
+
Response Chunk Type: response.incomplete`. It now ends the generation with
|
|
372
|
+
the text streamed so far, like `response.completed`.
|
|
373
|
+
- **A streamed Responses refusal no longer raises.** The provider raised
|
|
374
|
+
`Unexpected Response Chunk Type: response.refusal.delta` when the model
|
|
375
|
+
refused; the refusal events are now skipped and the response completes.
|
|
376
|
+
- **A streamed Responses generation reports its token usage.** The usage of a
|
|
377
|
+
streamed response is only on its final event, and the Responses provider
|
|
378
|
+
never read it, so `response.usage` was `nil`. It is now recorded from
|
|
379
|
+
`response.completed` or `response.incomplete`, as the Chat provider does
|
|
380
|
+
from its usage chunk.
|
|
381
|
+
- **Delegation contracts built from a `SchemaGenerator` class**
|
|
382
|
+
(`activeagent`). `delegation ..., schema: TicketForm` and
|
|
383
|
+
`returns TicketForm` produced a contract with no properties, because
|
|
384
|
+
`to_json_schema` wraps the schema in `{ name:, schema:, strict: }` and
|
|
385
|
+
the contract read only a top-level `properties`. The wrapper is now
|
|
386
|
+
unwrapped, and `$defs` are kept so the `$ref`s that nested schemas use
|
|
387
|
+
still resolve. A contract built from a class is sent with the strict
|
|
388
|
+
setting the class declared: `Model.to_json_schema` is not strict unless
|
|
389
|
+
asked, and its `required` then names only the presence-validated
|
|
390
|
+
attributes, which OpenAI's strict mode refuses, so `returns Model` goes
|
|
391
|
+
out with `strict: false` and `returns Model.to_json_schema(strict: true)`
|
|
392
|
+
strict with every attribute required.
|
|
393
|
+
|
|
394
|
+
- **Structured-output schema normalization preserves field names**
|
|
395
|
+
(`activeagent`). `prompt` converts JSON Schema keywords to camel case
|
|
396
|
+
without renaming properties or definitions, keeping multiword attributes
|
|
397
|
+
and `has_many` associations consistent with `required` and `$ref` in
|
|
398
|
+
strict schemas. Literal data in `default`, `const`, `enum` and `examples`
|
|
399
|
+
is preserved too (#260).
|
|
400
|
+
|
|
401
|
+
Property names now reach the provider as declared, so a `first_name`
|
|
402
|
+
attribute is sent as `first_name` rather than `firstName`, and the model
|
|
403
|
+
answers with that key. `parsed_json` underscores keys either way, so code
|
|
404
|
+
reading it is unaffected; code reading the raw `message.content`, or
|
|
405
|
+
matching on request payloads, sees snake_case where it saw camelCase.
|
|
406
|
+
|
|
407
|
+
A delegation's `returns` schema follows suit: its `required` entries are
|
|
408
|
+
no longer camelized, so a multiword required key such as `max_points`
|
|
409
|
+
matches its property in strict mode instead of being sent as
|
|
410
|
+
`maxPoints`.
|
|
411
|
+
- **A judge with no `judge_model` is labelled with the model that
|
|
412
|
+
answered** (`actionagent`), not its provider ("judged by openrouter").
|
|
413
|
+
OpenRouter's default, `openrouter/auto`, routes each call, so a run judged
|
|
414
|
+
by more than one model is labelled "<provider> default" (#605).
|
|
9
415
|
|
|
10
416
|
## [1.9.1] - 2026-10-16
|
|
11
417
|
|
data/README.md
CHANGED
data/lib/active_agent/base.rb
CHANGED
|
@@ -26,7 +26,7 @@ module ActiveAgent
|
|
|
26
26
|
#
|
|
27
27
|
# @example Basic agent
|
|
28
28
|
# class MyAgent < ActiveAgent::Base
|
|
29
|
-
# generate_with :openai, model: "gpt-
|
|
29
|
+
# generate_with :openai, model: "gpt-5"
|
|
30
30
|
#
|
|
31
31
|
# def greet
|
|
32
32
|
# prompt instructions: "Greet the user warmly"
|
|
@@ -112,7 +112,7 @@ module ActiveAgent
|
|
|
112
112
|
# @return [void]
|
|
113
113
|
#
|
|
114
114
|
# @example
|
|
115
|
-
# generate_with :openai, model: "gpt-
|
|
115
|
+
# generate_with :openai, model: "gpt-5"
|
|
116
116
|
def self.generate_with(provider_reference, **agent_options)
|
|
117
117
|
self.prompt_provider = provider_reference
|
|
118
118
|
|
|
@@ -397,13 +397,38 @@ module ActiveAgent
|
|
|
397
397
|
|
|
398
398
|
if response_format[:type].to_sym == :json_schema
|
|
399
399
|
response_format = nest_flat_json_schema(response_format)
|
|
400
|
-
response_format[:json_schema] = prompt_view_schema(response_format[:json_schema])
|
|
401
|
-
response_format[:json_schema].deep_transform_keys! { _1.to_s.camelize(:lower) }
|
|
400
|
+
response_format[:json_schema] = normalize_json_schema(prompt_view_schema(response_format[:json_schema]))
|
|
402
401
|
end
|
|
403
402
|
|
|
404
403
|
response_format
|
|
405
404
|
end
|
|
406
405
|
|
|
406
|
+
# Convert Ruby-style schema keywords without changing property names,
|
|
407
|
+
# definition names, references or literal data (default, const, enum,
|
|
408
|
+
# examples). Only schema-valued keywords are traversed. Named maps keep
|
|
409
|
+
# their keys, so required entries and $ref paths still address them.
|
|
410
|
+
#
|
|
411
|
+
# @api private
|
|
412
|
+
def normalize_json_schema(schema)
|
|
413
|
+
return schema.map { |item| normalize_json_schema(item) } if schema.is_a?(Array)
|
|
414
|
+
return schema unless schema.is_a?(Hash)
|
|
415
|
+
|
|
416
|
+
schema.to_h do |key, value|
|
|
417
|
+
keyword = key.to_s.camelize(:lower)
|
|
418
|
+
normalized = case keyword
|
|
419
|
+
when "properties", "patternProperties", "$defs", "definitions", "dependentSchemas", "dependencies"
|
|
420
|
+
value.is_a?(Hash) ? value.transform_values { |definition| normalize_json_schema(definition) } : value
|
|
421
|
+
when "schema", "items", "prefixItems", "additionalItems", "contains", "additionalProperties",
|
|
422
|
+
"unevaluatedItems", "unevaluatedProperties", "propertyNames", "contentSchema",
|
|
423
|
+
"allOf", "anyOf", "oneOf", "not", "if", "then", "else"
|
|
424
|
+
normalize_json_schema(value)
|
|
425
|
+
else
|
|
426
|
+
value
|
|
427
|
+
end
|
|
428
|
+
[ keyword, normalized ]
|
|
429
|
+
end
|
|
430
|
+
end
|
|
431
|
+
|
|
407
432
|
# Moves a json_schema given flat, with its name, schema and strict flag
|
|
408
433
|
# beside +type+, under +json_schema+, so it is not mistaken for a
|
|
409
434
|
# json_schema that names a view. Without a top-level schema, or with a
|
|
@@ -56,15 +56,25 @@ module ActiveAgent
|
|
|
56
56
|
schema
|
|
57
57
|
end
|
|
58
58
|
|
|
59
|
-
# @param hash [Hash] JSON Schema object
|
|
59
|
+
# @param hash [Hash] JSON Schema object, or the +{ name:, schema:, strict: }+
|
|
60
|
+
# wrapper {ActiveAgent::SchemaGenerator} returns around one
|
|
60
61
|
# @return [Schema]
|
|
61
62
|
def from_hash(hash)
|
|
62
63
|
hash = hash.deep_symbolize_keys
|
|
64
|
+
strict = nil
|
|
65
|
+
if !hash.key?(:properties) && hash[:schema].is_a?(Hash)
|
|
66
|
+
# SchemaGenerator's wrapper carries +strict: true+ only when the
|
|
67
|
+
# schema was built for strict mode; otherwise it is not strict.
|
|
68
|
+
strict = hash[:strict] == true
|
|
69
|
+
hash = hash[:schema]
|
|
70
|
+
end
|
|
63
71
|
|
|
64
72
|
new.tap do |schema|
|
|
65
73
|
(hash[:properties] || {}).each { |name, definition| schema.property(name, definition) }
|
|
66
74
|
schema.required(*Array(hash[:required]))
|
|
67
75
|
schema.additional_properties(hash.fetch(:additionalProperties, hash.fetch(:additional_properties, false)))
|
|
76
|
+
schema.definitions(hash[:$defs]) if hash[:$defs]
|
|
77
|
+
schema.strict(strict) unless strict.nil?
|
|
68
78
|
end
|
|
69
79
|
end
|
|
70
80
|
end
|
|
@@ -73,6 +83,8 @@ module ActiveAgent
|
|
|
73
83
|
@properties = {}
|
|
74
84
|
@required = []
|
|
75
85
|
@additional_properties = false
|
|
86
|
+
@definitions = {}
|
|
87
|
+
@strict = nil
|
|
76
88
|
end
|
|
77
89
|
|
|
78
90
|
# @return [Boolean] true when nothing has been declared
|
|
@@ -158,6 +170,29 @@ module ActiveAgent
|
|
|
158
170
|
@additional_properties = value
|
|
159
171
|
end
|
|
160
172
|
|
|
173
|
+
# Declares whether the schema was built for a provider's strict mode,
|
|
174
|
+
# as the +strict:+ of the wrapper {ActiveAgent::SchemaGenerator} returns
|
|
175
|
+
# says. Strict mode wants every property in +required+; a
|
|
176
|
+
# SchemaGenerator schema names only the attributes a presence
|
|
177
|
+
# validation covers unless it was built with +strict: true+, so a
|
|
178
|
+
# class is sent as it declared itself (see #to_response_format).
|
|
179
|
+
#
|
|
180
|
+
# @param value [Boolean]
|
|
181
|
+
# @return [void]
|
|
182
|
+
def strict(value = true)
|
|
183
|
+
@strict = value
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Declares the subschemas that properties reference with
|
|
187
|
+
# +"$ref": "#/$defs/name"+, such as the nested schemas
|
|
188
|
+
# {ActiveAgent::SchemaGenerator} emits for associations.
|
|
189
|
+
#
|
|
190
|
+
# @param definitions [Hash{Symbol => Hash}]
|
|
191
|
+
# @return [void]
|
|
192
|
+
def definitions(definitions)
|
|
193
|
+
@definitions = @definitions.merge(definitions.deep_symbolize_keys)
|
|
194
|
+
end
|
|
195
|
+
|
|
161
196
|
# @return [Array<Symbol>] declared property names
|
|
162
197
|
def keys
|
|
163
198
|
@properties.keys
|
|
@@ -177,27 +212,30 @@ module ActiveAgent
|
|
|
177
212
|
type: "object",
|
|
178
213
|
properties: @properties.deep_dup,
|
|
179
214
|
required: @required.map(&:to_s),
|
|
180
|
-
additionalProperties: @additional_properties
|
|
181
|
-
|
|
215
|
+
additionalProperties: @additional_properties,
|
|
216
|
+
"$defs": (@definitions.deep_dup if @definitions.any?)
|
|
217
|
+
}.compact
|
|
182
218
|
end
|
|
183
219
|
alias_method :to_h, :to_json_schema
|
|
184
220
|
|
|
185
221
|
# JSON Schema for a +response_format+ payload.
|
|
186
222
|
#
|
|
187
|
-
# ActiveAgent
|
|
188
|
-
#
|
|
189
|
-
#
|
|
190
|
-
#
|
|
191
|
-
# agent code only ever sees snake_case.
|
|
223
|
+
# ActiveAgent sends property names to the provider as declared, so the
|
|
224
|
+
# +required+ array names them as declared too.
|
|
225
|
+
# {Assistant#parsed_json} underscores the keys of the answer, so agent
|
|
226
|
+
# code only ever sees snake_case.
|
|
192
227
|
#
|
|
193
228
|
# @param name [String] schema name reported to the provider
|
|
194
|
-
# @param strict [Boolean]
|
|
229
|
+
# @param strict [Boolean, nil] nil follows the source: what a
|
|
230
|
+
# SchemaGenerator class declared (not strict unless built with
|
|
231
|
+
# +strict: true+, since its +required+ otherwise lists only the
|
|
232
|
+
# presence-validated attributes, which strict mode refuses), else true
|
|
195
233
|
# @return [Hash]
|
|
196
|
-
def to_response_format(name:, strict:
|
|
197
|
-
|
|
198
|
-
|
|
234
|
+
def to_response_format(name:, strict: nil)
|
|
235
|
+
strict = @strict if strict.nil?
|
|
236
|
+
strict = true if strict.nil?
|
|
199
237
|
|
|
200
|
-
{ name: name, schema:
|
|
238
|
+
{ name: name, schema: to_json_schema, strict: strict }
|
|
201
239
|
end
|
|
202
240
|
|
|
203
241
|
# Validates a parsed payload against the declared required properties.
|