activeagent 1.6.4 → 1.7.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 +217 -0
- data/lib/active_agent/concerns/view.rb +23 -0
- data/lib/active_agent/evals/publisher.rb +110 -14
- data/lib/active_agent/generation_job.rb +15 -1
- data/lib/active_agent/providers/anthropic/transforms.rb +57 -4
- data/lib/active_agent/providers/open_ai/responses/transforms.rb +8 -0
- data/lib/active_agent/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4aee77fd4f67a50824b7153b66f31aa5f19293bad4397e032ec0a9e674a759d0
|
|
4
|
+
data.tar.gz: e261e8ca9d221edbc849791ed28c67bc491ccc31e1e18f5c4cbf7a9a96a806c5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f5779f6207e07b1aec0ef14f72a5b010075b953c9025a12f6231ba164522c8b785cd9ca1bb297361abedc42aa535c468ef642978ec9fb439c79c9c0fe71ebd79
|
|
7
|
+
data.tar.gz: 1d2720a47e5e8c2a6c17a74cff893f1da2e89dd430b295bdcda4b9bdfb966afa8e75fbf95c0f063f5bca0bc8406d782e08b99fa6bed99b20230fb793ac3e518b
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,223 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.7.0] - 2026-09-24
|
|
11
|
+
|
|
12
|
+
Releases `activeagent` and `actionagent` 1.7.0 from one tag. A minor release:
|
|
13
|
+
a mounted engine collects the evaluation reports applications publish with
|
|
14
|
+
`ActiveAgent::Evals::Publisher`, whose failures now say what the collector
|
|
15
|
+
refused and whether to retry; observed agents read their own traces in
|
|
16
|
+
evaluation criteria, the Tools and Traces tabs and deploy markers; host apps
|
|
17
|
+
extend the engine's models and controllers through concerns and mirror their
|
|
18
|
+
agent classes into the dashboard; and the Evaluations page is rebuilt around
|
|
19
|
+
runs. Run the install generator after upgrading (see Upgrading below).
|
|
20
|
+
|
|
21
|
+
Upgrading: the install generator emits two new migrations, both guarded
|
|
22
|
+
column by column: `ensure_agent_release_columns`, which adds the agent release
|
|
23
|
+
columns an install generated fresh on 1.6.2-1.6.4 never got (and those on
|
|
24
|
+
tables with a custom `table_name_prefix`), and `add_evaluation_report_identity`
|
|
25
|
+
for the evaluation report collector. Re-run
|
|
26
|
+
`bin/rails generate action_agent:install --skip` (`--skip` keeps your
|
|
27
|
+
initializer) and `bin/rails db:migrate`. A traces-only install needs neither;
|
|
28
|
+
if you re-run the generator there, pass `--traces_only` again, or it emits the
|
|
29
|
+
whole dashboard schema. Nothing else changes until an application publishes a
|
|
30
|
+
report to the mount.
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
|
|
34
|
+
- **Host concerns for the engine's models and controllers** (`actionagent`).
|
|
35
|
+
`ActionAgent.model_concerns` is included into
|
|
36
|
+
`ActionAgent::ApplicationRecord` as it loads, and so into every engine
|
|
37
|
+
model; `ActionAgent.controller_concerns` into
|
|
38
|
+
`ActionAgent::ApplicationController`, ahead of its own callbacks, and so
|
|
39
|
+
into every dashboard controller. (The ingest endpoint,
|
|
40
|
+
`Api::TracesController`, inherits `ActionController::API` and keeps its
|
|
41
|
+
own bearer-token authentication; it is not touched.) Entries are modules
|
|
42
|
+
or their names, resolved
|
|
43
|
+
when the class loads. A host that pins the engine's tables to one database
|
|
44
|
+
connection, or carries its session helpers onto the dashboard's
|
|
45
|
+
controllers, configures that here instead of reopening the classes from a
|
|
46
|
+
`to_prepare` block.
|
|
47
|
+
- **The Evaluations page is rebuilt around runs** (`actionagent`). Evaluations
|
|
48
|
+
are the top level; every run is kept and listed with its movement against
|
|
49
|
+
the run before it (`+3 passed vs #2`, `partial run`, `#1 failed`), and a
|
|
50
|
+
sampling evaluation's run opens to a page of its own at
|
|
51
|
+
`<mount>/evaluations/:id/runs/:run_id` — a scorecard per model cohort, the
|
|
52
|
+
judge's verdict, the criteria × models matrix and what the run asks to fix.
|
|
53
|
+
A scenario suite's runs are the same full-width list; a row selects the run
|
|
54
|
+
the suite's model scorecards, fix items and scenario matrix show.
|
|
55
|
+
- **What a run cost is two figures, not one.** The agent's spend — what the
|
|
56
|
+
replayed or sampled interactions cost to serve, with a `per_interaction`
|
|
57
|
+
rate, the operating cost a per-conversation budget is set against — is
|
|
58
|
+
reported apart from the judge's, the judge model's own calls, which run
|
|
59
|
+
agent-to-agent and offline. Every judge call is metered under what it was
|
|
60
|
+
for (`scores["_judge_usage"]`: calls, tokens, estimated cost and how many
|
|
61
|
+
calls scored, recommended, ruled or authored KPIs), and `EvaluationRun#usage`
|
|
62
|
+
carries both sides. The page shows them on every run row, on the run, on a
|
|
63
|
+
page tile and in the footer, so the cost of operating an agent is never
|
|
64
|
+
inflated by the cost of checking it.
|
|
65
|
+
- A generation-sampling run records `scores["_cohorts"]`: per model, how many
|
|
66
|
+
generations were sampled, how many cleared every criterion, their latency
|
|
67
|
+
and tokens, and what those interactions cost to serve.
|
|
68
|
+
- `GET /api/evaluations` carries `run_count` and a `previous_run` summary per
|
|
69
|
+
evaluation, and every serialized run its `number` in the evaluation's
|
|
70
|
+
history, oldest first.
|
|
71
|
+
- The dashboard's object lists hold their metric columns in place: a trace,
|
|
72
|
+
interaction or evaluation run with nothing in a column prints a dash there
|
|
73
|
+
rather than sliding its neighbours over (`MetaStrip`).
|
|
74
|
+
- `ActiveAgent::Base.rendered_instructions` renders an agent's instructions
|
|
75
|
+
outside a generation, for a dashboard mirroring the class and for tests
|
|
76
|
+
asserting what a model is told. Both otherwise reached a private renderer
|
|
77
|
+
through `send`.
|
|
78
|
+
- `ActionAgent::AgentSync` mirrors host agent classes into dashboard `Agent`
|
|
79
|
+
records, setting the `agent_class_name` that `AgentRelease` already expects a
|
|
80
|
+
host to have written. The code owns what an agent is (name, description,
|
|
81
|
+
instructions, tools — rewritten every sync); the operator owns how it runs
|
|
82
|
+
(provider, model, status — set on create and preserved), so a model chosen in
|
|
83
|
+
the dashboard survives the next deploy.
|
|
84
|
+
- `ActionAgent.run_host_agent_classes` (default `false`) runs an agent that
|
|
85
|
+
mirrors a host class as that class, rather than as one rebuilt from the
|
|
86
|
+
record's `tools` and `instructions` columns. Dashboard-authored agents, which
|
|
87
|
+
name no class, keep using the dynamic runtime either way; a class name that no
|
|
88
|
+
longer resolves falls back to it rather than failing the run.
|
|
89
|
+
- MCP servers take `allowed_tools` and `require_approval` in the common
|
|
90
|
+
format. OpenAI's Responses API receives both as given; Anthropic receives
|
|
91
|
+
`allowed_tools` as an `mcp_toolset` entry in `tools` (every other tool of
|
|
92
|
+
the server disabled), beside any tools the request already declares
|
|
93
|
+
(#328, by @dark-panda).
|
|
94
|
+
- **A mounted engine collects published evaluation reports** (`actionagent`).
|
|
95
|
+
An application that runs its agents itself and evaluates them in-process
|
|
96
|
+
publishes the finished report with `ActiveAgent::Evals::Publisher`; until
|
|
97
|
+
now a self-hosted install had nowhere to receive it. On a full install (not
|
|
98
|
+
one generated with `--traces_only`, which answers 501),
|
|
99
|
+
`POST <mount>/api/evaluation_reports` takes the version-1 envelope and
|
|
100
|
+
returns the receipt the publisher checks, and
|
|
101
|
+
`ActionAgent::EvaluationReportImport` stores it as the engine's own rows:
|
|
102
|
+
the observed agent for the report's `source` and `agent_name`, an evaluation
|
|
103
|
+
named for its suite and scope (`orders (eu, support)`), its scenarios, and a
|
|
104
|
+
complete run with a result per scenario and model. The Evaluations page
|
|
105
|
+
shows it the way it shows a run the dashboard executed, with the summary
|
|
106
|
+
recomputed from the stored results. The endpoint authenticates exactly as
|
|
107
|
+
trace ingest does (`ingest_api_key`, or the tenant's key in multi-tenant
|
|
108
|
+
mode), takes only `application/json`, and places a report's agent wherever
|
|
109
|
+
`trace_owner_resolver` puts that tenant's traced agents. A `run_id` is
|
|
110
|
+
stored once per tenant, or once per install, and compared exactly: 201 for a
|
|
111
|
+
new report, 200 for an identical retry, 409 for different content. Invalid
|
|
112
|
+
reports, and an evaluation name the report does not own, are 422; a cap an
|
|
113
|
+
operator has to lift (observed agents per owner, 100 evaluations per agent,
|
|
114
|
+
2,000 scenarios per evaluation) is 403; a new report over the new
|
|
115
|
+
`:evaluation_report` quota kind or past 30 new reports a minute from a key
|
|
116
|
+
is 429. An identical retry is never refused by the quota or the rate limit.
|
|
117
|
+
Bodies over 2 MiB are 413, and Rails never parses the body into params, so
|
|
118
|
+
nothing past the limit is read. `usage_recorder` is told
|
|
119
|
+
`:evaluation_report` for each stored report. Evaluation runs gain
|
|
120
|
+
`external_tenant`, `external_run_id` and `external_report_digest`, unique on
|
|
121
|
+
the first two (binary on MySQL); see Upgrading above.
|
|
122
|
+
`docs/evals/publication.md` documents the endpoint.
|
|
123
|
+
|
|
124
|
+
### Changed
|
|
125
|
+
|
|
126
|
+
- The engine's judge blocks take `ActiveAgent::Evals::Judge`'s `kind:`, so a
|
|
127
|
+
scenario run's score, recommendation and verdict calls are metered apart.
|
|
128
|
+
- `ActionAgent::TelemetryTrace` inherits `ActionAgent::ApplicationRecord`
|
|
129
|
+
like every other engine model (`actionagent`), so it carries the model
|
|
130
|
+
concerns above, `AdapterAware` and the ownership API (`owner_association`,
|
|
131
|
+
`for_owner`) from the same place. Its table name is unchanged.
|
|
132
|
+
- `Api::TracesController`'s bearer authentication and its 429 quota body
|
|
133
|
+
live in `ActionAgent::Api::IngestAuthentication` (`actionagent`), which the
|
|
134
|
+
evaluation report collector shares. A host subclass that overrides
|
|
135
|
+
`authenticate_api_key!` is unaffected. The tenant's
|
|
136
|
+
`increment_telemetry_usage!` is still called for each trace ingest request,
|
|
137
|
+
and not for a report post.
|
|
138
|
+
- `add_agent_releases` reads `ActionAgent.table_name_prefix` for the tables
|
|
139
|
+
it alters (`actionagent`), so a newly generated copy works on an install
|
|
140
|
+
with a custom prefix. The trace table keeps its fixed name.
|
|
141
|
+
- A collector's rejection of `ActiveAgent::Evals::Publisher` says what it
|
|
142
|
+
refused and whether to retry. The message carries the `error` string of a
|
|
143
|
+
JSON object response body beside the HTTP status — control characters and
|
|
144
|
+
runs of whitespace collapsed to one space, the API key replaced with
|
|
145
|
+
`[FILTERED]`, cut to 200 characters; nothing else from the body — and what
|
|
146
|
+
to do next: never retry the report under the same `run_id` on a 409,
|
|
147
|
+
publish a smaller selection on a 413, correct the report on a 422, retry
|
|
148
|
+
later on a 408, 429 or 5xx, and resolve the cause first on anything else,
|
|
149
|
+
such as a 401 or 403. `Publisher::Error` carries `status`, `detail` and
|
|
150
|
+
`retryable?`.
|
|
151
|
+
|
|
152
|
+
### Deprecated
|
|
153
|
+
|
|
154
|
+
- Assigning `ActionAgent.base_controller_class`, which has never been
|
|
155
|
+
consumed, warns through `ActionAgent.deprecator` and points at
|
|
156
|
+
`controller_concerns`. The accessor is removed in 2.0.
|
|
157
|
+
|
|
158
|
+
### Fixed
|
|
159
|
+
|
|
160
|
+
- Telemetry criteria (`trace_error_rate`, `trace_latency`) score an observed
|
|
161
|
+
agent from its own traces (`actionagent`). They selected traces by
|
|
162
|
+
`Agent#telemetry_agent_class`, which appends `Agent` to a class name
|
|
163
|
+
lacking it, so an agent observed from an application reporting `SupportBot`
|
|
164
|
+
found no traces and scored nothing, and observed agents of one class ending
|
|
165
|
+
in `Agent` read each other's actions. `Agent#telemetry_traces` selects the
|
|
166
|
+
traces `AgentRegistrar` attributed to the agent, plus unattributed ones with
|
|
167
|
+
its service, class and action. Deleting an observed agent leaves its traces
|
|
168
|
+
unattributed, so the agent registered again for them still reads them. The
|
|
169
|
+
agent's Traces tab, its Tools tab usage
|
|
170
|
+
columns and the Interactions list filtered to it use the same selection. The
|
|
171
|
+
Traces tab asks for it with `GET /api/traces?agent_id=`, which answers 404
|
|
172
|
+
for an agent the caller cannot see; `agent=` still filters by class. On the
|
|
173
|
+
Metrics page filtered to a class, an observed agent's deploy markers now
|
|
174
|
+
show under the class its traces report (`Agent#reported_agent_class`).
|
|
175
|
+
Authored and mirrored agents read the traces they did before.
|
|
176
|
+
- `Agent.prompt(...).generate_later` and `Agent.embed(...).embed_later` run
|
|
177
|
+
their job instead of raising `ArgumentError: unknown keywords` in the
|
|
178
|
+
worker (#346).
|
|
179
|
+
- The agent builder and editor can reach every model a provider serves: the
|
|
180
|
+
OpenRouter catalog is no longer cut to its first 100 ids, and the model
|
|
181
|
+
field is a type-ahead over the catalog that also takes an unlisted id
|
|
182
|
+
(`actionagent`, #427).
|
|
183
|
+
- A rejected Create Agent shows its validation errors on the builder — a
|
|
184
|
+
summary and a message under each field — instead of leaving the form
|
|
185
|
+
silently in place. `POST`/`PATCH /api/agents` 422s carry `field_errors`
|
|
186
|
+
beside `errors` (`actionagent`, #426).
|
|
187
|
+
- An engine agent is refused a provider whose client gem the host has not
|
|
188
|
+
installed (`openai` for OpenAI, Ollama and OpenRouter; `anthropic` for
|
|
189
|
+
Anthropic) when the provider is chosen, with a validation error naming
|
|
190
|
+
the gem, instead of failing on its first run (`actionagent`, #416).
|
|
191
|
+
- A fresh `action_agent:install` creates the agent release columns with the
|
|
192
|
+
dashboard tables (`actionagent`): `release_digest` on agents,
|
|
193
|
+
`release_digest` and `revision` on agent versions, and `agent_version_id`
|
|
194
|
+
on agent runs and evaluation runs. The generator emits `add_agent_releases`
|
|
195
|
+
before the create-table migration, so on a fresh install it found none of
|
|
196
|
+
those tables and added nothing, and creating an agent run or an evaluation
|
|
197
|
+
run raised `NoMethodError` on `agent_version_id`. An install generated
|
|
198
|
+
that way on 1.6.2-1.6.4 gets the columns from the new
|
|
199
|
+
`ensure_agent_release_columns` migration (see Upgrading above).
|
|
200
|
+
- Every failure of `ActiveAgent::Evals::Publisher` to deliver a report
|
|
201
|
+
raises `Publisher::Error`. A malformed response (`Net::HTTPBadResponse`,
|
|
202
|
+
`Net::HTTPHeaderSyntaxError`, or a `Zlib::Error` from corrupt compression)
|
|
203
|
+
escaped as its own class and is now a retryable `Publisher::Error`. A
|
|
204
|
+
report that cannot be encoded as JSON (invalid UTF-8, `NaN`, nesting too
|
|
205
|
+
deep) is now a non-retryable one raised before anything is sent: it escaped
|
|
206
|
+
as `JSON::GeneratorError`, or was blamed on the collector as invalid JSON.
|
|
207
|
+
Only a network failure keeps its underlying error as `cause`, so a response
|
|
208
|
+
body or report content never reaches a log through the exception chain.
|
|
209
|
+
Invalid arguments raise `ArgumentError`, now also for a `report` that does
|
|
210
|
+
not convert to a hash (a string raised `NoMethodError` and `nil` published
|
|
211
|
+
an empty report) and a `nil` timeout (`TypeError`).
|
|
212
|
+
- The publisher strips whitespace around its API key, so the key it sends is
|
|
213
|
+
the one it filters from a collector's explanation, and refuses a key with
|
|
214
|
+
characters other than visible ASCII.
|
|
215
|
+
|
|
216
|
+
### Security
|
|
217
|
+
|
|
218
|
+
- The dashboard's JSON API verifies the CSRF token (`actionagent`, #461). It
|
|
219
|
+
authenticates with the host's session cookie but had opted out of forgery
|
|
220
|
+
protection. The dashboard now sends the page's token with every mutating
|
|
221
|
+
request from one fetch shim; the MCP facade and trace ingest, which
|
|
222
|
+
authenticate by bearer token, stay exempt. A rejected request answers
|
|
223
|
+
`422` with `code: "invalid_csrf_token"`. Hosts that re-enabled protection
|
|
224
|
+
themselves (`ActionAgent::Api::BaseController.protect_from_forgery`) can
|
|
225
|
+
drop that line.
|
|
226
|
+
|
|
10
227
|
## [1.6.4] - 2026-09-22
|
|
11
228
|
|
|
12
229
|
Releases `activeagent` and `actionagent` 1.6.4 from one tag. A patch on 1.6.3
|
|
@@ -12,6 +12,29 @@ module ActiveAgent
|
|
|
12
12
|
include ActionView::Layouts
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
+
class_methods do
|
|
16
|
+
# The agent's rendered instructions, outside a generation.
|
|
17
|
+
#
|
|
18
|
+
# Two surfaces need the text an agent would run on without running it: a
|
|
19
|
+
# dashboard that mirrors the class (ActionAgent::AgentSync) and a test
|
|
20
|
+
# asserting what the model is told. Both otherwise reach a private
|
|
21
|
+
# renderer through `send`, which couples them to internals that can move
|
|
22
|
+
# without notice.
|
|
23
|
+
#
|
|
24
|
+
# TicketAgent.rendered_instructions # => "You are the Ticket agent..."
|
|
25
|
+
#
|
|
26
|
+
# TicketAgent.rendered_instructions(topic: "tickets")
|
|
27
|
+
#
|
|
28
|
+
# @param template [String] template name, default "instructions"
|
|
29
|
+
# @param assigns [Hash] instance variables the template reads
|
|
30
|
+
# @return [String, nil] nil when the agent has no such template
|
|
31
|
+
def rendered_instructions(template: "instructions", **assigns)
|
|
32
|
+
agent = new
|
|
33
|
+
assigns.each { |name, value| agent.instance_variable_set(:"@#{name}", value) }
|
|
34
|
+
agent.send(:view_render_template, template)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
15
38
|
# Builds template lookup paths supporting both flat and nested directory structures.
|
|
16
39
|
#
|
|
17
40
|
# Templates are searched in priority order:
|
|
@@ -4,6 +4,7 @@ require "json"
|
|
|
4
4
|
require "net/http"
|
|
5
5
|
require "openssl"
|
|
6
6
|
require "uri"
|
|
7
|
+
require "zlib"
|
|
7
8
|
|
|
8
9
|
module ActiveAgent
|
|
9
10
|
module Evals
|
|
@@ -11,11 +12,51 @@ module ActiveAgent
|
|
|
11
12
|
# retain run_id when retrying: compatible collectors treat that identity as
|
|
12
13
|
# immutable within the authenticated account. Delivery is blocking and does
|
|
13
14
|
# not follow redirects with the account's bearer credential.
|
|
15
|
+
#
|
|
16
|
+
# Every failure to deliver raises Error. Invalid arguments raise
|
|
17
|
+
# ArgumentError before anything is sent.
|
|
14
18
|
class Publisher
|
|
15
19
|
DEFAULT_ENDPOINT = "https://api.activeagents.ai/v1/evaluations"
|
|
16
20
|
MAX_BYTES = 2 * 1024 * 1024
|
|
17
|
-
|
|
21
|
+
DETAIL_LIMIT = 200
|
|
18
22
|
|
|
23
|
+
# Raised for every failed delivery. Only a network failure keeps the
|
|
24
|
+
# underlying error as its +cause+, so neither the response nor the
|
|
25
|
+
# report reaches a log through the exception chain.
|
|
26
|
+
#
|
|
27
|
+
# @!attribute [r] status
|
|
28
|
+
# @return [Integer, nil] the collector's HTTP status for a rejection, nil otherwise
|
|
29
|
+
# @!attribute [r] detail
|
|
30
|
+
# @return [String, nil] the collector's sanitized explanation of a rejection, if it gave one
|
|
31
|
+
class Error < StandardError
|
|
32
|
+
attr_reader :status, :detail
|
|
33
|
+
|
|
34
|
+
def initialize(message = nil, status: nil, detail: nil, retryable: false)
|
|
35
|
+
super(message)
|
|
36
|
+
@status = status
|
|
37
|
+
@detail = detail
|
|
38
|
+
@retryable = retryable
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Returns true when delivering the same report under the same run_id
|
|
42
|
+
# again may succeed.
|
|
43
|
+
def retryable?
|
|
44
|
+
@retryable
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Whether each rejection status is retryable, and what the caller should
|
|
49
|
+
# do about it. Other statuses fall back to the rules in +rejection+.
|
|
50
|
+
REJECTIONS = {
|
|
51
|
+
409 => [ false, "the collector already holds a different report under this run_id; never retry this report with the same run_id" ],
|
|
52
|
+
413 => [ false, "the report exceeds the collector's size limit; publish a smaller selection" ],
|
|
53
|
+
422 => [ false, "correct what the collector refused before retrying" ],
|
|
54
|
+
429 => [ true, "the account is over its quota or rate limit; retain the report and run_id and retry later" ]
|
|
55
|
+
}.freeze
|
|
56
|
+
|
|
57
|
+
# The key is sent as a bearer token and filtered from the collector's
|
|
58
|
+
# explanation, so it is limited to visible ASCII: the sanitizer in
|
|
59
|
+
# +collector_detail+ never alters it, and an echo of it always matches.
|
|
19
60
|
def initialize(api_key:, endpoint: DEFAULT_ENDPOINT, timeout: 10, open_timeout: 10)
|
|
20
61
|
@uri = URI.parse(endpoint.to_s)
|
|
21
62
|
unless @uri.is_a?(URI::HTTP) && @uri.host && !@uri.userinfo && !@uri.query && !@uri.fragment
|
|
@@ -24,12 +65,16 @@ module ActiveAgent
|
|
|
24
65
|
unless @uri.scheme == "https" || %w[localhost 127.0.0.1 ::1].include?(@uri.hostname)
|
|
25
66
|
raise ArgumentError, "Evaluation endpoint requires HTTPS except on loopback hosts"
|
|
26
67
|
end
|
|
27
|
-
raise ArgumentError, "Evaluation API key is required" if api_key.to_s.strip.empty?
|
|
28
68
|
|
|
29
|
-
@api_key = api_key.to_s
|
|
30
|
-
|
|
31
|
-
@
|
|
32
|
-
|
|
69
|
+
@api_key = api_key.to_s.strip
|
|
70
|
+
raise ArgumentError, "Evaluation API key is required" if @api_key.empty?
|
|
71
|
+
unless @api_key.match?(/\A[\x21-\x7E]+\z/)
|
|
72
|
+
raise ArgumentError, "Evaluation API key must contain only visible ASCII characters"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
@timeout = Float(timeout, exception: false)
|
|
76
|
+
@open_timeout = Float(open_timeout, exception: false)
|
|
77
|
+
unless [ @timeout, @open_timeout ].all? { |value| value&.finite? && value.positive? }
|
|
33
78
|
raise ArgumentError, "Evaluation delivery timeouts must be positive and finite"
|
|
34
79
|
end
|
|
35
80
|
rescue URI::InvalidURIError
|
|
@@ -43,7 +88,7 @@ module ActiveAgent
|
|
|
43
88
|
identities.each do |key, value|
|
|
44
89
|
raise ArgumentError, "#{key} must be a nonempty string" unless value.is_a?(String) && !value.strip.empty?
|
|
45
90
|
end
|
|
46
|
-
body =
|
|
91
|
+
body = encode(identities.merge("version" => 1, "report" => report_hash(report)))
|
|
47
92
|
raise Error, "Evaluation report exceeds the 2 MiB delivery limit; publish a smaller selection" if body.bytesize > MAX_BYTES
|
|
48
93
|
|
|
49
94
|
http = Net::HTTP.new(@uri.hostname, @uri.port)
|
|
@@ -57,19 +102,70 @@ module ActiveAgent
|
|
|
57
102
|
request["Accept"] = "application/json"
|
|
58
103
|
request.body = body
|
|
59
104
|
response = http.request(request)
|
|
60
|
-
unless %w[200 201].include?(response.code)
|
|
61
|
-
raise Error, "Evaluation delivery rejected (HTTP #{response.code}); retain the report and run_id for retry"
|
|
62
|
-
end
|
|
105
|
+
raise rejection(response) unless %w[200 201].include?(response.code)
|
|
63
106
|
|
|
64
|
-
receipt = JSON.parse(response.body)
|
|
107
|
+
receipt = JSON.parse(response.body.to_s)
|
|
65
108
|
unless receipt.is_a?(Hash) && receipt["run_id"] == run_id && receipt["status"] == "complete" && receipt["id"] && receipt["evaluation_id"]
|
|
66
|
-
raise Error
|
|
109
|
+
raise Error.new("Evaluation collector returned an invalid completion receipt; retain the report and run_id for retry", retryable: true)
|
|
67
110
|
end
|
|
68
111
|
receipt
|
|
69
112
|
rescue JSON::ParserError
|
|
70
|
-
|
|
113
|
+
# The parser's message quotes the body.
|
|
114
|
+
raise Error.new("Evaluation collector returned invalid JSON; retain the report and run_id for retry", retryable: true), cause: nil
|
|
115
|
+
rescue Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, Zlib::Error => e
|
|
116
|
+
# These messages can quote the response's status line, headers or body.
|
|
117
|
+
raise Error.new("Evaluation collector returned a malformed response (#{e.class}); retain the report and run_id for retry", retryable: true), cause: nil
|
|
71
118
|
rescue IOError, SocketError, SystemCallError, Timeout::Error, OpenSSL::SSL::SSLError => e
|
|
72
|
-
raise Error
|
|
119
|
+
raise Error.new("Evaluation delivery failed (#{e.class}); retain the report and run_id for retry", retryable: true)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
private
|
|
123
|
+
|
|
124
|
+
def report_hash(report)
|
|
125
|
+
hash = report.to_h if report.respond_to?(:to_h) && !report.nil? && !report.is_a?(Array)
|
|
126
|
+
raise ArgumentError, "report must be a Report or its saved JSON hash" unless hash.is_a?(Hash)
|
|
127
|
+
|
|
128
|
+
hash
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Returns the envelope as JSON. A report holding invalid UTF-8, NaN,
|
|
132
|
+
# Infinity or nesting over JSON's depth limit raises a non-retryable
|
|
133
|
+
# Error, without the generator's message, which can quote the report.
|
|
134
|
+
def encode(envelope)
|
|
135
|
+
JSON.generate(envelope)
|
|
136
|
+
rescue JSON::JSONError, EncodingError => e
|
|
137
|
+
raise Error.new("Evaluation report cannot be encoded as JSON (#{e.class}); correct the report before publishing"), cause: nil
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def rejection(response)
|
|
141
|
+
status = response.code.to_i
|
|
142
|
+
retryable, guidance = REJECTIONS.fetch(status) do
|
|
143
|
+
if status == 408 || (500..599).cover?(status)
|
|
144
|
+
[ true, "retain the report and run_id for retry" ]
|
|
145
|
+
else
|
|
146
|
+
[ false, "retain the report and run_id, and resolve the rejection before retrying" ]
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
detail = collector_detail(response.body)
|
|
150
|
+
reason = detail ? "HTTP #{status}: #{detail}" : "HTTP #{status}"
|
|
151
|
+
Error.new("Evaluation delivery rejected (#{reason}); #{guidance}", status: status, detail: detail, retryable: retryable)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Returns the +error+ string of a JSON object body, or nil for any other
|
|
155
|
+
# body. Control characters and runs of whitespace become one space, the
|
|
156
|
+
# API key becomes [FILTERED], and the result is cut to DETAIL_LIMIT
|
|
157
|
+
# characters: `"answer\n\tis required"` → `"answer is required"`.
|
|
158
|
+
def collector_detail(body)
|
|
159
|
+
parsed = JSON.parse(body.to_s)
|
|
160
|
+
message = parsed["error"] if parsed.is_a?(Hash)
|
|
161
|
+
return unless message.is_a?(String)
|
|
162
|
+
|
|
163
|
+
detail = message.scrub.gsub(/[[:space:]\p{C}]+/, " ").strip.gsub(@api_key, "[FILTERED]")
|
|
164
|
+
return if detail.empty?
|
|
165
|
+
|
|
166
|
+
detail.length > DETAIL_LIMIT ? "#{detail[0, DETAIL_LIMIT - 1].rstrip}…" : detail
|
|
167
|
+
rescue JSON::ParserError, EncodingError
|
|
168
|
+
nil
|
|
73
169
|
end
|
|
74
170
|
end
|
|
75
171
|
end
|
|
@@ -23,7 +23,21 @@ module ActiveAgent
|
|
|
23
23
|
# argument, so a record arrives as the same record the caller passed and
|
|
24
24
|
# the agent's authorization callbacks decide against a real user rather
|
|
25
25
|
# than against nil.
|
|
26
|
-
|
|
26
|
+
#
|
|
27
|
+
# +direct_generation_type+ marks a job enqueued by Agent.prompt(...) or
|
|
28
|
+
# Agent.embed(...), which have no action to call: +agent_method+ is then
|
|
29
|
+
# the synthetic +__direct_*__+ name, so the generation is rebuilt from
|
|
30
|
+
# +direct_args+ and +direct_options+ instead.
|
|
31
|
+
def perform(agent, agent_method, generation_method, args:, kwargs: nil, params: nil, actor: nil,
|
|
32
|
+
direct_generation_type: nil, direct_args: nil, direct_options: nil)
|
|
33
|
+
if direct_generation_type
|
|
34
|
+
generation = ActiveAgent::Parameterized::DirectGeneration.new(
|
|
35
|
+
agent.constantize, direct_generation_type.to_sym, params || {},
|
|
36
|
+
*Array(direct_args), **(direct_options || {}).symbolize_keys
|
|
37
|
+
)
|
|
38
|
+
return generation.public_send(generation_method)
|
|
39
|
+
end
|
|
40
|
+
|
|
27
41
|
agent_class = params ? agent.constantize.with(params) : agent.constantize
|
|
28
42
|
agent_class = agent_class.as(actor) if actor
|
|
29
43
|
prompt = if kwargs
|
|
@@ -38,10 +38,20 @@ module ActiveAgent
|
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
# Handle mcps parameter (common format) -> transforms to mcp_servers (provider format)
|
|
41
|
-
if params[:mcps]
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
if params[:mcps] || params[:mcp_servers]
|
|
42
|
+
mcps = if params[:mcps]
|
|
43
|
+
params.delete(:mcps)
|
|
44
|
+
else
|
|
45
|
+
params[:mcp_servers]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
params[:mcp_servers] = normalize_mcp_servers(mcps)
|
|
49
|
+
|
|
50
|
+
# A server's allowed_tools become an mcp_toolset entry beside the
|
|
51
|
+
# request's own tools. Added only when there is one: an empty
|
|
52
|
+
# list would send `"tools": null` on every MCP request.
|
|
53
|
+
mcp_tools = normalize_mcp_tools(mcps)
|
|
54
|
+
params[:tools] = Array(params[:tools]) + mcp_tools if mcp_tools.present?
|
|
45
55
|
end
|
|
46
56
|
|
|
47
57
|
params
|
|
@@ -111,6 +121,49 @@ module ActiveAgent
|
|
|
111
121
|
end
|
|
112
122
|
end
|
|
113
123
|
|
|
124
|
+
# Builds Anthropic mcp_toolset tool entries from MCP server allowed_tools.
|
|
125
|
+
#
|
|
126
|
+
# Accepts allowed_tools entries as tool-name strings/symbols or hashes
|
|
127
|
+
# with a :name key; entries in any other format are ignored.
|
|
128
|
+
#
|
|
129
|
+
# @param mcp_servers [Array<Hash>]
|
|
130
|
+
# @return [Array<Hash>, nil] toolset entries, or nil when none were extracted
|
|
131
|
+
def normalize_mcp_tools(mcp_servers)
|
|
132
|
+
return nil unless mcp_servers.is_a?(Array)
|
|
133
|
+
|
|
134
|
+
result = mcp_servers.filter_map do |server|
|
|
135
|
+
next unless server.is_a?(Hash)
|
|
136
|
+
|
|
137
|
+
server_hash = server.deep_symbolize_keys
|
|
138
|
+
allowed_tools = server_hash[:allowed_tools]
|
|
139
|
+
next unless allowed_tools.is_a?(Array)
|
|
140
|
+
|
|
141
|
+
configs = allowed_tools.filter_map { |tool|
|
|
142
|
+
name = case tool
|
|
143
|
+
when String, Symbol
|
|
144
|
+
tool.to_s
|
|
145
|
+
when Hash
|
|
146
|
+
(tool[:name] || tool["name"]).to_s
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
[ name, { enabled: true } ] if name.present?
|
|
150
|
+
}.to_h
|
|
151
|
+
|
|
152
|
+
next if configs.empty?
|
|
153
|
+
|
|
154
|
+
{
|
|
155
|
+
type: "mcp_toolset",
|
|
156
|
+
mcp_server_name: server_hash[:name],
|
|
157
|
+
default_config: {
|
|
158
|
+
enabled: false
|
|
159
|
+
},
|
|
160
|
+
configs: configs
|
|
161
|
+
}
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
result.presence
|
|
165
|
+
end
|
|
166
|
+
|
|
114
167
|
# Normalizes tool_choice from common format to Anthropic gem model objects.
|
|
115
168
|
#
|
|
116
169
|
# The Anthropic gem expects tool_choice to be a model object (ToolChoiceAuto,
|
|
@@ -96,6 +96,14 @@ module ActiveAgent
|
|
|
96
96
|
server_url: server_hash[:url] || server_hash[:server_url]
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
if server_hash[:require_approval]
|
|
100
|
+
result[:require_approval] = server_hash[:require_approval]
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
if server_hash[:allowed_tools]
|
|
104
|
+
result[:allowed_tools] = server_hash[:allowed_tools]
|
|
105
|
+
end
|
|
106
|
+
|
|
99
107
|
# Keep authorization field (OpenAI uses 'authorization', not 'authorization_token')
|
|
100
108
|
if server_hash[:authorization]
|
|
101
109
|
result[:authorization] = server_hash[:authorization]
|
data/lib/active_agent/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: activeagent
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Justin Bowen
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-09-
|
|
11
|
+
date: 2026-09-24 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: actionpack
|