activeagent 1.0.3 → 1.2.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 +132 -0
- data/README.md +39 -3
- data/lib/active_agent/base.rb +4 -0
- data/lib/active_agent/model_capabilities.rb +89 -0
- data/lib/active_agent/providers/_base_provider.rb +23 -2
- data/lib/active_agent/providers/concerns/exception_handler.rb +12 -1
- data/lib/active_agent/providers/errors.rb +140 -0
- data/lib/active_agent/providers/ollama/chat/transforms.rb +9 -3
- data/lib/active_agent/railtie.rb +5 -0
- data/lib/active_agent/telemetry/configuration.rb +115 -171
- data/lib/active_agent/telemetry/instrumentation.rb +141 -14
- data/lib/active_agent/telemetry/reporter.rb +12 -165
- data/lib/active_agent/telemetry/span.rb +18 -245
- data/lib/active_agent/telemetry/tool_origin.rb +90 -0
- data/lib/active_agent/telemetry/tracer.rb +67 -70
- data/lib/active_agent/telemetry.rb +2 -2
- data/lib/active_agent/version.rb +1 -1
- data/lib/active_agent.rb +1 -1
- metadata +38 -42
- data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/api/traces_controller.rb +0 -117
- data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/application_controller.rb +0 -54
- data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/dashboard_controller.rb +0 -126
- data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/traces_controller.rb +0 -103
- data/lib/active_agent/dashboard/app/jobs/active_agent/dashboard/agent_execution_job.rb +0 -56
- data/lib/active_agent/dashboard/app/jobs/active_agent/dashboard/application_job.rb +0 -14
- data/lib/active_agent/dashboard/app/jobs/active_agent/dashboard/sandbox_cleanup_job.rb +0 -49
- data/lib/active_agent/dashboard/app/jobs/active_agent/dashboard/sandbox_provision_job.rb +0 -65
- data/lib/active_agent/dashboard/app/jobs/active_agent/process_telemetry_traces_job.rb +0 -77
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent.rb +0 -256
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent_run.rb +0 -113
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent_template.rb +0 -208
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent_version.rb +0 -60
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/application_record.rb +0 -46
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/recording_action.rb +0 -125
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/recording_snapshot.rb +0 -83
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/sandbox_run.rb +0 -52
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/sandbox_session.rb +0 -169
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/session_recording.rb +0 -193
- data/lib/active_agent/dashboard/app/models/active_agent/telemetry_trace.rb +0 -198
- data/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/_trace_detail.html.erb +0 -105
- data/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/index.html.erb +0 -135
- data/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/metrics.html.erb +0 -143
- data/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/show.html.erb +0 -36
- data/lib/active_agent/dashboard/app/views/layouts/active_agent/dashboard/application.html.erb +0 -94
- data/lib/active_agent/dashboard/config/routes.rb +0 -78
- data/lib/active_agent/dashboard/engine.rb +0 -39
- data/lib/active_agent/dashboard.rb +0 -151
- data/lib/generators/active_agent/dashboard/install/install_generator.rb +0 -96
- data/lib/generators/active_agent/dashboard/install/templates/initializer.rb +0 -89
- data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agent_runs.rb +0 -42
- data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agent_templates.rb +0 -38
- data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agent_versions.rb +0 -22
- data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agents.rb +0 -53
- data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_sandbox_runs.rb +0 -28
- data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_sandbox_sessions.rb +0 -43
- data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_session_recordings.rb +0 -44
- data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_telemetry_traces.rb +0 -56
- data/lib/generators/active_agent/dashboard/install_generator.rb +0 -64
- data/lib/generators/active_agent/dashboard/templates/active_agent_dashboard.rb.erb +0 -30
- data/lib/generators/active_agent/dashboard/templates/create_active_agent_telemetry_traces.rb.erb +0 -30
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 867cf01603bb86b99c7abdbe6cc9e731264dc60bfdf3e89ca19a4d25fca5ea3d
|
|
4
|
+
data.tar.gz: c8b86f8988ff27931f4a82f8aa71aa04168bceae00f8f9cab6b98507dd3d2b94
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8c81f0537fddda60bbe3049cdb36b0478130d969ba0d35af408f3a5c527e2448d8e733638cd94b6c72c8f4b8d1ec13da32877baae3101e0373cc7d173253d06c
|
|
7
|
+
data.tar.gz: 6af0f6a1879a9e291581a51beb5b6b96ac2ec641a9ebc007cb2345c695e1f746741fbb667bd761f889591d15c7ffeb917e7ecb7c5d1151f4d6687af3f87a3930
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,138 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.2.0] - Unreleased
|
|
9
|
+
|
|
10
|
+
### ⚠️ The dashboard has moved to its own gem
|
|
11
|
+
|
|
12
|
+
The dashboard engine that shipped inside `activeagent` is now a separate
|
|
13
|
+
gem, **`actionagent`**. Nothing is gone — the dashboard is the same
|
|
14
|
+
dashboard, and it gained a great deal in this release — but it comes from a
|
|
15
|
+
different gem now. `activeagent` is the framework alone: it no longer
|
|
16
|
+
defines `ActiveAgent::Dashboard`, and no longer pulls Active Record into
|
|
17
|
+
apps that do not use it.
|
|
18
|
+
|
|
19
|
+
**If you mount the dashboard, add the new gem in the same change that
|
|
20
|
+
upgrades `activeagent`:**
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
gem "activeagent", "~> 1.2"
|
|
24
|
+
gem "actionagent", "~> 1.2" # required if you mount the dashboard
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
This is a minor version, so a `~> 1.0` or `~> 1.1` constraint **will** pick
|
|
28
|
+
it up on the next `bundle update`. If you mount the dashboard and do not add
|
|
29
|
+
`actionagent` at the same time, the app fails at boot with
|
|
30
|
+
`NameError: uninitialized constant ActiveAgent::Dashboard`, raised by your
|
|
31
|
+
own initializer or by the `mount ActiveAgent::Dashboard::Engine` line in
|
|
32
|
+
`config/routes.rb`. Adding the gem is the whole fix — your existing
|
|
33
|
+
configuration keeps working through the compatibility shims below.
|
|
34
|
+
|
|
35
|
+
If you do not mount the dashboard, there is nothing to do: the framework API
|
|
36
|
+
is unchanged, and the gem is 95% smaller.
|
|
37
|
+
|
|
38
|
+
With `actionagent` installed, the old constants keep resolving through
|
|
39
|
+
`ActionAgent::Compatibility` with a deprecation warning:
|
|
40
|
+
|
|
41
|
+
- `ActiveAgent::Dashboard` → `ActionAgent`
|
|
42
|
+
- `ActiveAgent::TelemetryTrace` → `ActionAgent::TelemetryTrace`
|
|
43
|
+
- `ActiveAgent::ProcessTelemetryTracesJob` → `ActionAgent::ProcessTelemetryTracesJob`
|
|
44
|
+
|
|
45
|
+
That last one matters beyond tidiness: Active Job serializes the class name
|
|
46
|
+
into the queue payload, so jobs enqueued before the upgrade still resolve
|
|
47
|
+
after it.
|
|
48
|
+
|
|
49
|
+
Other changes for mounted installs:
|
|
50
|
+
|
|
51
|
+
- **The server-rendered traces console moves from `/traces` to
|
|
52
|
+
`/console/traces`.** `/traces` is now the React traces view — the same
|
|
53
|
+
data, with more of it.
|
|
54
|
+
- **The mount is authenticated everywhere but development and test.** The
|
|
55
|
+
sandbox API, the session-recording capture endpoints and the template
|
|
56
|
+
endpoints previously allowed anonymous access; they no longer do. The
|
|
57
|
+
`GET /api/session_recordings/demo` endpoint is removed.
|
|
58
|
+
- **`current_user_method` / `current_account_method` are superseded by
|
|
59
|
+
`current_user_resolver` / `current_account_resolver`.** The engine's
|
|
60
|
+
controllers are their own base class, so a host app's `current_user`
|
|
61
|
+
helper is not available to them.
|
|
62
|
+
- **An unresolved owner now scopes to nothing rather than to everything.**
|
|
63
|
+
If you configure `user_class` or `account_class`, make sure the matching
|
|
64
|
+
resolver actually returns a record, or the dashboard will show no data.
|
|
65
|
+
- Existing installs upgrading from the in-gem dashboard: re-run
|
|
66
|
+
`rails generate action_agent:install`. It detects the migrations you
|
|
67
|
+
already have and emits only what is missing.
|
|
68
|
+
|
|
69
|
+
## [1.1.0] - 2026-08-12
|
|
70
|
+
|
|
71
|
+
### Dashboard — self-hosted (enterprise) mount readiness
|
|
72
|
+
|
|
73
|
+
The engine can now be mounted in any Rails app as the self-hosted
|
|
74
|
+
observability surface (see `docs/framework/self-hosted-observability.md`):
|
|
75
|
+
|
|
76
|
+
- **One install generator**: the duplicate `active_agent:dashboard:install`
|
|
77
|
+
variant that copied eight migrations (agents, sandboxes, recordings —
|
|
78
|
+
tables for models with no shipped controllers or routes) is removed.
|
|
79
|
+
The surviving generator installs the telemetry traces table only and
|
|
80
|
+
gains `--skip_migrations` / `--skip_routes`; its initializer template now
|
|
81
|
+
covers authentication, `ingest_api_key`, and multi-tenant options.
|
|
82
|
+
- **Canonical mount path is `/activeagents`** (generator, dummy app and
|
|
83
|
+
docs updated). `Telemetry::Configuration#resolved_endpoint` now reports
|
|
84
|
+
the ingest path for wherever the engine is actually mounted — any mount
|
|
85
|
+
path, including `/` on a dedicated subdomain — instead of a hardcoded
|
|
86
|
+
constant, falling back to `LOCAL_ENDPOINT_PATH` when it isn't mounted.
|
|
87
|
+
Note this is informational: `local_storage` capture writes through the
|
|
88
|
+
trace model without HTTP, and remote apps set `endpoint:` explicitly.
|
|
89
|
+
- **`TracesController` honors configuration**: index/metrics/time-series
|
|
90
|
+
queries now go through `ActiveAgent::Dashboard.trace_model` (previously
|
|
91
|
+
only `show` did) and are scoped with `for_account(current_owner)`, so a
|
|
92
|
+
`trace_model_class` override and multi-tenant scoping apply everywhere.
|
|
93
|
+
- **Single-tenant ingest auth**: new `config.ingest_api_key` requires a
|
|
94
|
+
matching Bearer token on `POST <mount>/api/traces` when set. The
|
|
95
|
+
telemetry reporter and ruby_llm_telemetry already send their `api_key`
|
|
96
|
+
as a Bearer header, so remote apps need no changes.
|
|
97
|
+
- **Metrics page no longer 500s with data**: the per-agent stats table
|
|
98
|
+
read a grouped SQL alias through a model method that expected per-trace
|
|
99
|
+
token columns.
|
|
100
|
+
- **Mount detection is route-set based**: the ingest path is resolved by
|
|
101
|
+
locating the mounted engine in the host's routes rather than assuming
|
|
102
|
+
the default `active_agent_path` helper, so `mount ... => "/", as:
|
|
103
|
+
:something_else` and constraint-wrapped (subdomain) mounts resolve
|
|
104
|
+
correctly instead of silently falling back.
|
|
105
|
+
- Deprecated the never-consumed `base_controller_class` config attribute:
|
|
106
|
+
it remains a no-op accessor with its historical default so existing
|
|
107
|
+
initializers keep booting, and will be removed in the next major.
|
|
108
|
+
|
|
109
|
+
### Dashboard & Telemetry — dev console readiness
|
|
110
|
+
|
|
111
|
+
The dashboard engine — Active Agent's local dev console — now works out of
|
|
112
|
+
the box (production observability is the hosted platform product):
|
|
113
|
+
|
|
114
|
+
- **Engine load paths fixed**: `Engine.find_root` now points at the
|
|
115
|
+
dashboard directory, so `ActiveAgent::TelemetryTrace`,
|
|
116
|
+
`ProcessTelemetryTracesJob`, the API controller, views and engine routes
|
|
117
|
+
are auto-discovered in host apps (previously they required manual
|
|
118
|
+
`require`s). The engine is also required eagerly with Rails, since
|
|
119
|
+
engines defined lazily miss initializer collection.
|
|
120
|
+
- **Routes now match shipped controllers**: the engine exposes traces,
|
|
121
|
+
metrics and the ingest API (`<mount>/api/traces`); routes to
|
|
122
|
+
never-shipped controllers (agents, sandboxes, templates, recordings,
|
|
123
|
+
api/v1) were removed. Engine root renders the traces index.
|
|
124
|
+
- **`local_storage` telemetry mode fixed**: tracer payloads are
|
|
125
|
+
symbol-keyed and were silently dropped by the string-keyed ingestion
|
|
126
|
+
normalizer; the reporter now stringifies and honors
|
|
127
|
+
`ActiveAgent::Dashboard.trace_model` overrides.
|
|
128
|
+
- **Token totals no longer double-count**: instrumentation mirrors LLM
|
|
129
|
+
token usage onto the root span; `TelemetryTrace.create_from_payload`
|
|
130
|
+
now counts child spans as the source of truth.
|
|
131
|
+
- **Span waterfall renders real offsets** (was pinned to 0ms), turbo-rails
|
|
132
|
+
is now optional (previously 500s without it), layout route helpers fixed,
|
|
133
|
+
`Agent.for_owner` scope added, synchronous ingest capped at 100
|
|
134
|
+
traces/request.
|
|
135
|
+
- **New docs** (`docs/framework/dashboard.md`, README section) covering
|
|
136
|
+
install, authentication (none by default — see docs), remote ingestion
|
|
137
|
+
and multi-tenant mode; dashboard engine test suite added
|
|
138
|
+
(`test/dashboard/`).
|
|
139
|
+
|
|
8
140
|
## [1.0.0] - 2025-11-21
|
|
9
141
|
|
|
10
142
|
Major refactor with breaking changes. Complete provider rewrite. New modular architecture.
|
data/README.md
CHANGED
|
@@ -25,6 +25,10 @@ Use bundler to add activeagent to your Gemfile and install:
|
|
|
25
25
|
bundle add activeagent
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
+
That is the framework — agents, providers, generation and telemetry
|
|
29
|
+
reporting. The dashboard is a second gem, `actionagent`, added separately
|
|
30
|
+
when you want it; see [Dashboard & Dev Console](#dashboard--dev-console).
|
|
31
|
+
|
|
28
32
|
Add the generation provider gem you want to use:
|
|
29
33
|
|
|
30
34
|
```bash
|
|
@@ -127,6 +131,38 @@ development:
|
|
|
127
131
|
service: "RubyLLM"
|
|
128
132
|
```
|
|
129
133
|
|
|
134
|
+
## Dashboard & Dev Console
|
|
135
|
+
|
|
136
|
+
The dashboard is its own gem, `actionagent`: a mountable Rails engine with
|
|
137
|
+
traces and span waterfalls, token usage and per-agent metrics, plus the agent
|
|
138
|
+
builder, runs, conversations, evaluations, scorecards and cost estimates — so
|
|
139
|
+
you can watch and drive your agents while you build. It ships separately
|
|
140
|
+
because its models are Active Record models and it runs agents through
|
|
141
|
+
[solid_agent](https://github.com/activeagents/solid_agent) — neither of which
|
|
142
|
+
`activeagent` depends on, so an app that only runs agents installs neither.
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
bundle add actionagent
|
|
146
|
+
rails generate action_agent:install
|
|
147
|
+
rails db:migrate
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
```yaml
|
|
151
|
+
# config/active_agent.yml
|
|
152
|
+
telemetry:
|
|
153
|
+
enabled: true
|
|
154
|
+
local_storage: true
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
The generator mounts the engine at `/activeagents` — open it and every
|
|
158
|
+
generation appears as a trace. See
|
|
159
|
+
[docs/framework/dashboard.md](docs/framework/dashboard.md) for
|
|
160
|
+
authentication, remote ingestion, and multi-tenant mode. The hosted
|
|
161
|
+
platform at [activeagents.ai](https://activeagents.ai) runs this same
|
|
162
|
+
engine multi-tenant, adding what a hosted product has to have — accounts,
|
|
163
|
+
plans, billing, quotas and managed sandboxes; every workspace starts with
|
|
164
|
+
a free low-volume trial.
|
|
165
|
+
|
|
130
166
|
## Features
|
|
131
167
|
|
|
132
168
|
- **Agent-Oriented Programming**: Build AI applications using familiar Rails patterns
|
|
@@ -173,12 +209,12 @@ response = prompt.generate_now
|
|
|
173
209
|
|
|
174
210
|
- [Documentation](https://docs.activeagents.ai)
|
|
175
211
|
- [Getting Started Guide](https://docs.activeagents.ai/getting_started)
|
|
176
|
-
- [API Reference](https://docs.activeagents.ai/
|
|
177
|
-
- [Examples](https://docs.activeagents.ai/
|
|
212
|
+
- [API Reference](https://docs.activeagents.ai/framework)
|
|
213
|
+
- [Examples](https://docs.activeagents.ai/agents)
|
|
178
214
|
|
|
179
215
|
## Contributing
|
|
180
216
|
|
|
181
|
-
We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.
|
|
217
|
+
We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.MD) for details.
|
|
182
218
|
|
|
183
219
|
## License
|
|
184
220
|
|
data/lib/active_agent/base.rb
CHANGED
|
@@ -304,6 +304,10 @@ module ActiveAgent
|
|
|
304
304
|
# Render out proc/lamda attributes before rendering templates
|
|
305
305
|
parameters.deep_transform_values! { _1.respond_to?(:call) ? _1.call : _1 }
|
|
306
306
|
|
|
307
|
+
# Strip parameters the target model rejects (e.g. temperature/top_p
|
|
308
|
+
# on thinking-first models) before they reach the provider.
|
|
309
|
+
ModelCapabilities.sanitize!(parameters)
|
|
310
|
+
|
|
307
311
|
# Apply Callbacks
|
|
308
312
|
parameters.merge!(
|
|
309
313
|
trace_id: prompt_options[:trace_id] || SecureRandom.uuid,
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveAgent
|
|
4
|
+
# Per-model capability quirks, applied before a request reaches the
|
|
5
|
+
# provider. Vendors ship models that reject otherwise-standard sampling
|
|
6
|
+
# parameters (thinking-first models steered by prompting/effort instead)
|
|
7
|
+
# with an API 400 — this registry strips those parameters up front so an
|
|
8
|
+
# agent configured with a shared temperature keeps working across model
|
|
9
|
+
# switches.
|
|
10
|
+
#
|
|
11
|
+
# The built-in rules cover the known families; apps can extend the
|
|
12
|
+
# registry for new or self-hosted models:
|
|
13
|
+
#
|
|
14
|
+
# @example Register a custom rule
|
|
15
|
+
# ActiveAgent::ModelCapabilities.register(/\Amy-reasoning-model/, unsupported: [:temperature, :top_p])
|
|
16
|
+
#
|
|
17
|
+
# @example Disable sanitization entirely
|
|
18
|
+
# ActiveAgent::ModelCapabilities.enabled = false
|
|
19
|
+
module ModelCapabilities
|
|
20
|
+
SAMPLING_PARAMS = [ :temperature, :top_p ].freeze
|
|
21
|
+
|
|
22
|
+
# Model families that reject sampling parameters with a 400:
|
|
23
|
+
# - Anthropic thinking-first models (Opus 4.7+, Opus 5, Sonnet 5,
|
|
24
|
+
# Fable 5 / Mythos 5)
|
|
25
|
+
# - OpenAI reasoning models (o-series, GPT-5 family)
|
|
26
|
+
BUILTIN_RULES = [
|
|
27
|
+
{ pattern: /\Aclaude-(opus-5|opus-4-[78]|sonnet-5|fable-5|mythos-5)/, unsupported: SAMPLING_PARAMS },
|
|
28
|
+
{ pattern: /\A(o1|o3|o4)(-|$)/, unsupported: SAMPLING_PARAMS },
|
|
29
|
+
{ pattern: /\Agpt-5/, unsupported: SAMPLING_PARAMS }
|
|
30
|
+
].freeze
|
|
31
|
+
|
|
32
|
+
class << self
|
|
33
|
+
# Master switch; on by default. Set false to send parameters through
|
|
34
|
+
# untouched (the vendor then enforces its own rules).
|
|
35
|
+
attr_writer :enabled
|
|
36
|
+
|
|
37
|
+
def enabled
|
|
38
|
+
return @enabled unless @enabled.nil?
|
|
39
|
+
|
|
40
|
+
true
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Registers an app-defined capability rule ahead of the built-ins.
|
|
44
|
+
#
|
|
45
|
+
# @param pattern [Regexp] matched against the model name
|
|
46
|
+
# @param unsupported [Array<Symbol>] parameter keys the model rejects
|
|
47
|
+
def register(pattern, unsupported:)
|
|
48
|
+
custom_rules << { pattern: pattern, unsupported: unsupported.map(&:to_sym) }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def custom_rules
|
|
52
|
+
@custom_rules ||= []
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def reset!
|
|
56
|
+
@custom_rules = []
|
|
57
|
+
@enabled = nil
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# @return [Array<Symbol>] parameter keys the model rejects
|
|
61
|
+
def unsupported_params(model)
|
|
62
|
+
return [] if model.nil?
|
|
63
|
+
|
|
64
|
+
(custom_rules + BUILTIN_RULES).each do |rule|
|
|
65
|
+
return rule[:unsupported] if model.to_s.match?(rule[:pattern])
|
|
66
|
+
end
|
|
67
|
+
[]
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def sampling_supported?(model)
|
|
71
|
+
(unsupported_params(model) & SAMPLING_PARAMS).empty?
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Strips parameters the model rejects, in place. Returns the removed
|
|
75
|
+
# keys (empty when nothing applied).
|
|
76
|
+
#
|
|
77
|
+
# @param parameters [Hash] prepared prompt parameters (must carry :model)
|
|
78
|
+
# @return [Array<Symbol>] removed parameter keys
|
|
79
|
+
def sanitize!(parameters)
|
|
80
|
+
return [] unless enabled
|
|
81
|
+
return [] unless parameters.is_a?(Hash)
|
|
82
|
+
|
|
83
|
+
removed = unsupported_params(parameters[:model]).select { |key| parameters.key?(key) }
|
|
84
|
+
removed.each { |key| parameters.delete(key) }
|
|
85
|
+
removed
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -55,7 +55,13 @@ module ActiveAgent
|
|
|
55
55
|
:request, :message_stack, # Runtime
|
|
56
56
|
:stream_broadcaster, :streaming, # Callback (Streams)
|
|
57
57
|
:tools_function, # Callback (Tools)
|
|
58
|
-
:usage_stack
|
|
58
|
+
:usage_stack, # Usage Tracking
|
|
59
|
+
:max_tool_turns, :tool_turns # Tool-loop safety
|
|
60
|
+
|
|
61
|
+
# Upper bound on tool-calling round-trips within one generation. A
|
|
62
|
+
# model that keeps emitting tool calls otherwise recurses until the
|
|
63
|
+
# provider stops it — override per agent/prompt with max_tool_turns:.
|
|
64
|
+
DEFAULT_MAX_TOOL_TURNS = 25
|
|
59
65
|
|
|
60
66
|
# @return [String] e.g., "Anthropic", "OpenAI"
|
|
61
67
|
def self.service_name
|
|
@@ -106,6 +112,8 @@ module ActiveAgent
|
|
|
106
112
|
self.stream_broadcaster = kwargs.delete(:stream_broadcaster)
|
|
107
113
|
self.streaming = false
|
|
108
114
|
self.tools_function = kwargs.delete(:tools_function)
|
|
115
|
+
self.max_tool_turns = kwargs.delete(:max_tool_turns) || DEFAULT_MAX_TOOL_TURNS
|
|
116
|
+
self.tool_turns = 0
|
|
109
117
|
self.options = options_klass.new(kwargs.extract!(*options_klass.keys))
|
|
110
118
|
self.context = kwargs
|
|
111
119
|
self.message_stack = []
|
|
@@ -344,7 +352,7 @@ module ActiveAgent
|
|
|
344
352
|
message_stack.push(*api_messages)
|
|
345
353
|
end
|
|
346
354
|
|
|
347
|
-
if (tool_calls = process_prompt_finished_extract_function_calls)&.any?
|
|
355
|
+
if (tool_calls = process_prompt_finished_extract_function_calls)&.any? && tool_turn_allowed?
|
|
348
356
|
process_function_calls(tool_calls)
|
|
349
357
|
resolve_prompt
|
|
350
358
|
else
|
|
@@ -373,6 +381,19 @@ module ActiveAgent
|
|
|
373
381
|
end
|
|
374
382
|
end
|
|
375
383
|
|
|
384
|
+
# Counts a tool round-trip against the per-generation cap. When the
|
|
385
|
+
# cap is hit the loop finishes cleanly with the messages gathered so
|
|
386
|
+
# far (a partial result) instead of recursing indefinitely.
|
|
387
|
+
#
|
|
388
|
+
# @return [Boolean] whether another tool round-trip may run
|
|
389
|
+
def tool_turn_allowed?
|
|
390
|
+
self.tool_turns += 1
|
|
391
|
+
return true if max_tool_turns.nil? || tool_turns <= max_tool_turns
|
|
392
|
+
|
|
393
|
+
instrument("tool_turns_exceeded.active_agent", limit: max_tool_turns)
|
|
394
|
+
false
|
|
395
|
+
end
|
|
396
|
+
|
|
376
397
|
# @abstract
|
|
377
398
|
# @param api_response [Object]
|
|
378
399
|
# @return [Array<Message>, nil]
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../errors"
|
|
4
|
+
|
|
3
5
|
module ActiveAgent
|
|
4
6
|
module Providers
|
|
5
7
|
# Provides exception handling for provider operations.
|
|
@@ -54,7 +56,16 @@ module ActiveAgent
|
|
|
54
56
|
def with_exception_handling(&block)
|
|
55
57
|
yield
|
|
56
58
|
rescue => exception
|
|
57
|
-
|
|
59
|
+
# Vendor API failures are normalized into the framework taxonomy
|
|
60
|
+
# (Errors::RateLimited, Errors::ContextLengthExceeded, ...) so
|
|
61
|
+
# rescue_from policy is portable across providers; the original
|
|
62
|
+
# exception is preserved as #cause. Anything unrecognizable —
|
|
63
|
+
# including ordinary Ruby errors — passes through untouched.
|
|
64
|
+
exception = Errors::Taxonomy.normalize(
|
|
65
|
+
exception,
|
|
66
|
+
provider_tag: (tag_name if respond_to?(:tag_name))
|
|
67
|
+
)
|
|
68
|
+
rescue_with_handler(exception) || raise(exception)
|
|
58
69
|
nil # Discard handler return value to prevent polluting raw_response
|
|
59
70
|
end
|
|
60
71
|
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveAgent
|
|
4
|
+
module Providers
|
|
5
|
+
# Typed provider failures, normalized across vendor SDKs.
|
|
6
|
+
#
|
|
7
|
+
# Every vendor raises its own exception classes for the same underlying
|
|
8
|
+
# conditions (rate limits, context overflows, content filters, outages),
|
|
9
|
+
# which makes retry/backoff/fallback policy impossible to express
|
|
10
|
+
# portably. The taxonomy classifies vendor errors into a small set of
|
|
11
|
+
# framework types — the original exception is preserved as +#cause+, so
|
|
12
|
+
# nothing is lost.
|
|
13
|
+
#
|
|
14
|
+
# @example Portable retry policy
|
|
15
|
+
# rescue_from ActiveAgent::Providers::Errors::RateLimited do |error|
|
|
16
|
+
# retry_job wait: 30.seconds
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
# @example Fallback on outage
|
|
20
|
+
# rescue_from ActiveAgent::Providers::Errors::ServiceUnavailable do |error|
|
|
21
|
+
# FallbackAgent.with(params).ask.generate_later
|
|
22
|
+
# end
|
|
23
|
+
module Errors
|
|
24
|
+
# Base class for normalized provider failures.
|
|
25
|
+
class ProviderError < StandardError
|
|
26
|
+
# @return [Integer, nil] HTTP status from the vendor error, when known
|
|
27
|
+
attr_reader :status
|
|
28
|
+
|
|
29
|
+
# @return [String, nil] provider tag (e.g. "Anthropic", "OpenAI::Chat")
|
|
30
|
+
attr_reader :provider_tag
|
|
31
|
+
|
|
32
|
+
def initialize(message = nil, status: nil, provider_tag: nil)
|
|
33
|
+
super(message)
|
|
34
|
+
@status = status
|
|
35
|
+
@provider_tag = provider_tag
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# 429s / vendor rate & quota limits. Retryable with backoff.
|
|
40
|
+
class RateLimited < ProviderError; end
|
|
41
|
+
|
|
42
|
+
# The prompt exceeded the model's context window. Not retryable
|
|
43
|
+
# without shrinking the input.
|
|
44
|
+
class ContextLengthExceeded < ProviderError; end
|
|
45
|
+
|
|
46
|
+
# Invalid, expired, or unauthorized credentials (401/403).
|
|
47
|
+
class AuthenticationFailed < ProviderError; end
|
|
48
|
+
|
|
49
|
+
# The vendor's safety layer refused the request or response.
|
|
50
|
+
class ContentFiltered < ProviderError; end
|
|
51
|
+
|
|
52
|
+
# Vendor-side failure or overload (5xx, timeouts, connection drops).
|
|
53
|
+
# Retryable; a natural trigger for provider fallback.
|
|
54
|
+
class ServiceUnavailable < ProviderError; end
|
|
55
|
+
|
|
56
|
+
# Malformed or unsupported request the vendor rejected (400/422)
|
|
57
|
+
# that doesn't classify more specifically.
|
|
58
|
+
class InvalidRequest < ProviderError; end
|
|
59
|
+
|
|
60
|
+
# Classifies vendor SDK exceptions into the taxonomy. Unrecognizable
|
|
61
|
+
# exceptions (including ordinary Ruby errors) pass through untouched —
|
|
62
|
+
# only errors that look like vendor API failures are normalized.
|
|
63
|
+
module Taxonomy
|
|
64
|
+
# Vendor SDK class names (demodulized) → taxonomy class. Covers the
|
|
65
|
+
# official anthropic/openai gems and SDKs following their naming.
|
|
66
|
+
NAME_MAP = {
|
|
67
|
+
"RateLimitError" => RateLimited,
|
|
68
|
+
"AuthenticationError" => AuthenticationFailed,
|
|
69
|
+
"PermissionDeniedError" => AuthenticationFailed,
|
|
70
|
+
"ContentFilterError" => ContentFiltered,
|
|
71
|
+
"InternalServerError" => ServiceUnavailable,
|
|
72
|
+
"APIConnectionError" => ServiceUnavailable,
|
|
73
|
+
"APIConnectionTimeoutError" => ServiceUnavailable,
|
|
74
|
+
"APITimeoutError" => ServiceUnavailable,
|
|
75
|
+
"OverloadedError" => ServiceUnavailable,
|
|
76
|
+
"ServiceUnavailableError" => ServiceUnavailable,
|
|
77
|
+
"BadRequestError" => InvalidRequest,
|
|
78
|
+
"UnprocessableEntityError" => InvalidRequest
|
|
79
|
+
}.freeze
|
|
80
|
+
|
|
81
|
+
STATUS_MAP = {
|
|
82
|
+
400 => InvalidRequest,
|
|
83
|
+
401 => AuthenticationFailed,
|
|
84
|
+
403 => AuthenticationFailed,
|
|
85
|
+
408 => ServiceUnavailable,
|
|
86
|
+
422 => InvalidRequest,
|
|
87
|
+
429 => RateLimited,
|
|
88
|
+
529 => ServiceUnavailable # Anthropic "overloaded"
|
|
89
|
+
}.freeze
|
|
90
|
+
|
|
91
|
+
CONTEXT_LENGTH_PATTERN = /context length|context_length|maximum context|context window|too many tokens|prompt is too long|input (?:is )?too long/i
|
|
92
|
+
CONTENT_FILTER_PATTERN = /content (?:filter|policy|management)|filtered due to|blocked by|safety (?:system|filter)/i
|
|
93
|
+
|
|
94
|
+
class << self
|
|
95
|
+
# @param exception [Exception]
|
|
96
|
+
# @param provider_tag [String, nil]
|
|
97
|
+
# @return [Exception] a taxonomy error, or the original exception
|
|
98
|
+
# when it doesn't classify
|
|
99
|
+
def normalize(exception, provider_tag: nil)
|
|
100
|
+
return exception if exception.is_a?(ProviderError)
|
|
101
|
+
|
|
102
|
+
klass = classify(exception)
|
|
103
|
+
return exception unless klass
|
|
104
|
+
|
|
105
|
+
klass.new(exception.message, status: status_of(exception), provider_tag: provider_tag)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# @return [Class, nil]
|
|
109
|
+
def classify(exception)
|
|
110
|
+
name = exception.class.name.to_s.demodulize
|
|
111
|
+
status = status_of(exception)
|
|
112
|
+
api_error = NAME_MAP.key?(name) || !status.nil?
|
|
113
|
+
return nil unless api_error
|
|
114
|
+
|
|
115
|
+
message = exception.message.to_s
|
|
116
|
+
return ContextLengthExceeded if CONTEXT_LENGTH_PATTERN.match?(message)
|
|
117
|
+
return ContentFiltered if CONTENT_FILTER_PATTERN.match?(message)
|
|
118
|
+
|
|
119
|
+
NAME_MAP[name] || STATUS_MAP[status] || (status && status >= 500 ? ServiceUnavailable : nil)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# @return [Integer, nil]
|
|
123
|
+
def status_of(exception)
|
|
124
|
+
[ :status, :status_code, :http_status, :code ].each do |reader|
|
|
125
|
+
next unless exception.respond_to?(reader)
|
|
126
|
+
|
|
127
|
+
value = begin
|
|
128
|
+
exception.public_send(reader)
|
|
129
|
+
rescue StandardError
|
|
130
|
+
nil
|
|
131
|
+
end
|
|
132
|
+
return value if value.is_a?(Integer)
|
|
133
|
+
end
|
|
134
|
+
nil
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
@@ -55,12 +55,18 @@ module ActiveAgent
|
|
|
55
55
|
OpenAI::Chat::Transforms.normalize_messages(messages)
|
|
56
56
|
end
|
|
57
57
|
|
|
58
|
-
# Normalizes instructions using OpenAI transforms
|
|
58
|
+
# Normalizes instructions using OpenAI transforms, then remaps the
|
|
59
|
+
# role: OpenAI's transforms emit the "developer" role, but the chat
|
|
60
|
+
# templates of Ollama-served models (qwen, llama, gemma, …) only
|
|
61
|
+
# know "system" — a "developer" message is silently dropped, so the
|
|
62
|
+
# model never sees its instructions.
|
|
59
63
|
#
|
|
60
64
|
# @param instructions [Array<String>, String]
|
|
61
|
-
# @return [Array<
|
|
65
|
+
# @return [Array<Hash>] system messages
|
|
62
66
|
def normalize_instructions(instructions)
|
|
63
|
-
OpenAI::Chat::Transforms.normalize_instructions(instructions)
|
|
67
|
+
OpenAI::Chat::Transforms.normalize_instructions(instructions).map do |message|
|
|
68
|
+
message.is_a?(Hash) ? message.merge(role: "system") : message
|
|
69
|
+
end
|
|
64
70
|
end
|
|
65
71
|
|
|
66
72
|
# Cleans up serialized request for API submission
|
data/lib/active_agent/railtie.rb
CHANGED
|
@@ -67,6 +67,11 @@ module ActiveAgent
|
|
|
67
67
|
include ActiveAgent::Telemetry::Instrumentation
|
|
68
68
|
instrument_telemetry!
|
|
69
69
|
end
|
|
70
|
+
|
|
71
|
+
# Flush remaining traces when the process exits — without this,
|
|
72
|
+
# short-lived processes (rails runner, jobs, deploys rolling a
|
|
73
|
+
# server) drop whatever was buffered since the last interval flush.
|
|
74
|
+
at_exit { ActiveAgent::Telemetry.shutdown }
|
|
70
75
|
end
|
|
71
76
|
# endregion telemetry_configuration
|
|
72
77
|
|