The dashboard is its own gem. activeagent is the framework — agents,
providers, generation, telemetry reporting — and actionagent is a mountable
Rails engine that adds the dashboard on top of it: every agent generation
recorded as a trace with a span waterfall, a metrics overview, and the
agent builder, interactions and evaluations alongside them — running
inside your app against your own database while you build. The hosted
activeagents.ai platform mounts the same engine,
so what you see locally in development is what the platform shows (plus the
accounts, plans, billing and managed sandbox infrastructure a hosted
product has to have) once you point telemetry at it. Every platform
workspace starts with a free low-volume trial.
![Dashboard: traces list with expandable span timelines]
The dashboard's models are Active Record models and its runs persist
conversations, so actionagent adds activerecord and
solid_agent on top of what the framework already pulls in —
neither of which activeagent itself requires. Add both gems:
# Gemfile
gem "activeagent"
gem "actionagent"bundle install
rails generate action_agent:install
rails db:migrateThe generator:
- copies three migrations —
active_agent_telemetry_traces(the trace store),active_agent_dashboard_tables(agents, runs, versions, conversations, evaluations, sandboxes, recordings, keys) andactive_agent_evaluation_scenarios(scenario suites and their per-model results; re-run the generator on an existing install to get it); pass--traces_onlyfor a trace sink alone, - mounts the engine at
/activeagents, - writes
config/initializers/action_agent.rb.
API keys and provider credentials are encrypted at rest, so run
rails db:encryption:init before creating any (or set
ActionAgent.encrypt_credentials = false to store them in plain
text — a deliberate downgrade, not a default).
Deploying this beyond your laptop — for a team, or as the trace sink for a fleet of apps? See Self-Hosted Observability.
Then enable telemetry with local storage in config/active_agent.yml:
telemetry:
enabled: true
local_storage: trueThat's it. Run any agent and open /activeagents — each generation
appears as a trace with prompt/LLM/tool spans, timing, token usage
(input / output / thinking), provider and model. The dashboard's React
bundle ships prebuilt in the actionagent gem, so mounting it doesn't ask
your app to run a JavaScript build.
local_storage: true writes traces through the engine's trace model, so it
only works in the app that mounts the engine. Without actionagent
installed, telemetry logs that it has nowhere to write — apps that only run
agents point telemetry at an endpoint: instead (see below).
| Page | Path | Contents |
|---|---|---|
| Ask ActiveAgents | /activeagents/assistant |
Ask about recorded evaluations and prepare an agent draft for review (development and test only — see below) |
| Agents | /activeagents |
Your agents with per-agent request, token and error stats; build, edit, version them, and test them as a user in the Run Agent workbench (see below) |
| Traces | /activeagents/traces |
Every generation: agent + action, status, duration, tokens; expandable span timeline; All/Errors filter; 30s auto-refresh |
| Metrics | /activeagents/metrics |
The service overview: golden signals, six time series over 1h/24h/7d, and the top agents, models, actions, tools and error types (see below) |
| Interactions | /activeagents/interactions |
The conversations behind the traces: messages, tool calls, generations |
| Evaluations | /activeagents/evaluations |
Scored agent outputs, and scenario suites replayed across models (see below) |
| Console | /activeagents/console/traces |
The same traces and metrics server-rendered, without JavaScript; span waterfall per trace at /activeagents/console/traces/:id |
| Ingest API | POST /activeagents/api/traces |
JSON trace ingestion from other apps and SDKs (local_storage writes through the model instead, no HTTP) |
Every agent page has a Run Agent button. It opens a workbench that works the agent the way a user would — a conversation, not a one-shot prompt box — so you can watch what the model is given, change it, and run again.
Add user messages. Type a message and press Run (or ⌘/Ctrl+Enter). The
run executes under the agent's instructions and the selected action, and the
reply streams into the conversation with the same LLM/tool activity feed the
Interactions view shows. The conversation is pinned: the next message is
sent with every user and assistant turn before it, so "and which one grew
fastest?" means what it would mean to a person. Each run is still its own
AgentRun with its own trace, which is how Traces and Interactions keep
showing exactly what the model saw.
The workbench follows the dashboard theme:
Modify the context. The conversation on the page is the persisted solid_agent context, and it is editable: hover a turn to edit or delete it, use Add message to seed a user or assistant turn without running anything, and New conversation to start from an empty context. The system row shows the composed instructions the run executes under (edit those on the Instructions tab). Editing a previous question and asking a follow-up is the quickest way to see how an agent handles a changed history.
Attach files. Files attached to a message upload with the run through
Active Storage (AgentRun has_many_attached :attachments), and reach the
model according to their kind: images as vision input, PDFs as documents,
and text-like files — CSV, Markdown, JSON, plain text — inlined into the
message, with the filename and size in a header the model can cite. The
persisted user message keeps an attachment manifest, so the conversation
shows the thumbnails afterwards. A host app without Active Storage keeps
everything else and answers attachment uploads with a clear 422.
Generative UI. An assistant reply can carry UI instead of, or alongside, prose: cards, stats, tables, charts, lists, progress bars, forms, choice buttons, images, callouts and code. Three ways in, all rendered by the same component:
- a fenced
```uiblock in a markdown reply whose body is JSON — an array of blocks, or{ "blocks": [...] }; - a JSON reply whose top level is
{ "ui": [...] }(aresponse_formatagent, say) — any other JSON object renders as a key/value block; - the Generative UI tool (
render_ui) — enable it on the agent's Tools tab and the model can call it with{ "blocks": [...] }.
Forms and choices are live: submitting a form or clicking a choice posts the answer back into the conversation as the next user message, so a model can ask for input and continue.
While a run executes, the LLM and tool calls stream into the conversation
as they happen — here a calculate call answered mid-run:
[
{ "type": "stats", "items": [
{ "label": "Revenue", "value": "$3.36M", "delta": "+11%", "tone": "positive" },
{ "label": "Deals", "value": "113" }
]},
{ "type": "chart", "chart": "bar", "title": "Revenue by region",
"x": "region", "series": ["revenue"],
"data": [ { "region": "EMEA", "revenue": 1240000 }, { "region": "APAC", "revenue": 710000 } ] },
{ "type": "form", "title": "Book a follow-up", "submit": "Book call",
"fields": [
{ "name": "date", "label": "Date", "type": "text", "required": true },
{ "name": "time", "label": "Time", "type": "select", "options": ["09:00", "11:00", "15:00"] }
]}
]Block fields: card {title, body, image_url, footer}, stat {label, value, delta, tone}, stats {items}, table {columns, rows}, chart {chart: bar|line|area|pie, title, x, series, data}, list {title, items, ordered},
progress {label, value}, form {title, submit, fields[{name, label, type: text|textarea|number|select|checkbox, options, placeholder, required}]},
choices {prompt, options}, image {url, alt, caption}, callout {tone, title, body}, code {language, code}.
Blocks are rendered as React elements only, never as HTML. An image (or a
card's image_url) is displayed straight away when it is a data:image/…
URL or one of your own app's — an Active Storage blob, say. A URL on any
other host is shown as a button naming that host instead: fetching an image
is a request to whoever serves it, and the model chose the address, so the
person reading the reply decides whether to make it.
Time-series charts on the console's metrics page use the optional groupdate gem when present and degrade gracefully without it; the React metrics page reads buckets the API already aggregated and needs nothing extra.
A tool for developing and CI-ing agents, not a production surface. Answering a
question means sending recorded prompts, outputs and evaluation report excerpts
to a model provider, so the page and its API are available in development and
test only. Where it is off there is no nav item, no route and no endpoint —
both /activeagents/api/dashboard_assistant actions answer 403. Turn it on
somewhere else deliberately, or off everywhere:
# config/initializers/action_agent.rb
ActionAgent.configure do |config|
config.assistant_enabled = true # or false to remove it in development too
endChoose a provider and model, then allow that provider to process your message, recent conversation history and authorized report excerpts. Configure a provider credential in Settings first. The assistant uses the host's authentication, agent scope, execution policy and quota hooks.
Ask which demo questions passed, why an evaluation failed, or describe an agent to build. Report cards link to recorded evidence and disclose weak checks and missing provenance. Historical passes cannot establish that current main works. Compact report references remain available when earlier excerpts are replaced. Raw recorded exceptions are withheld from assistant evidence because they may contain credentials; open the authorized report to inspect those details. Agent drafts open in the builder for review; they are not saved or run by chat.
Conversation state resets on reload. Assistant generations disable framework traces and provider notifications, and message/history parameters are filtered before Rails request logging. Provider retention and any host middleware that records raw HTTP bodies still follow the host's policies. Repository connections, COI execution, Claude Code sessions and PR checks remain planned work.
/activeagents/metrics answers the question an on-call tab is open for:
is this service healthy right now, and if not, since when. It is laid out
the way an APM overview is — golden signals across the top, one grid of
time series under them, top-N lists down the right — because the point is
to see a change and then find what moved, not to read a table.
Pick a window with the 1h / 24h / 7d control. The window fixes the
bucket size the whole page is drawn at — 60 buckets of a minute, 96 of 15
minutes, 84 of two hours — so the charts stay the same shape whatever the
traffic, and the indicator beside the control says which (live · 15 min buckets, refreshed every 60 seconds).
Five golden signals lead: Requests (with requests per minute), Latency (p50, with p95 and p99 under it), Error rate (with the error count and how many were rate-limited), Tokens (input and output) and Cost (with cost per request). Each carries a 24-point sparkline of its own series and a delta against the period of the same length just before the window — more traffic and falling latency read as success, a rising error rate as error, volume and spend stay neutral, because a bigger number is not by itself good or bad.
Six panels plot the window:
| Panel | What it shows |
|---|---|
| Requests | Bars per bucket, stacked by agent, with deploy markers |
| Latency | p50 / p95 / p99 lines, with the incident marker |
| Errors | Bars stacked by error class, with the incident marker |
| Tokens | Input and output lines |
| Cost | Estimated spend per bucket |
| Tool calls | Calls per bucket, errored calls stacked on top |
The right rail ranks what is behind them: Agents by requests (with
p95, error rate and cost), Models by tokens, Slowest actions by
p95, Tools by calls (with average duration and error rate) and
Errors by type — 429 rate limit, timeout, tool error,
provider 5xx, other, always all five so the shape of a spike is
readable at a glance.
Filter to one agent from the select, or by clicking its row in the Agents
rail; clicking it again clears the filter. Everything narrows together —
signals, charts and rails — so the page never shows a filtered chart next
to an unfiltered tile. The rail keeps listing every agent while a filter
is on, with the active one highlighted, so it stays the way to hop
between them. An env chip names the environment most of the window's
traces report.
Two kinds of marker sit on the plots. A deploy is an agent version
saved inside the window (v4 · SupportAgent, or instructions v4 · SupportAgent when that version changed the instructions — the deploy a
latency or error shift most often traces back to); at most the three most
recent are drawn, because more than that is a picket fence. An
incident is the bucket with the most errors when it is a real spike —
at least five errors and at least twice the window's error rate — labelled
by its dominant error class and the agent that errored most in it.
Empty windows say so (No traffic yet — run an agent, or point your app's ActiveAgent telemetry at this workspace) rather than drawing five flat
lines.
GET /api/metrics is what the page reads, and what to point your own
alerting or reporting at:
| Param | Meaning |
|---|---|
range |
1h, 24h (default) or 7d — the window and its bucket size |
hours |
A custom window instead, bucketed to about 96 points (range then reads custom) |
agent |
An agent_class; every key in the response is scoped to it |
sort |
Ranks the per-agent table: popular, longest, cost, tokens, errors |
The response carries range, bucket_seconds, window_minutes, agent
and environment, then totals (requests, requests per minute, p50 / p95
/ p99 in ms, errors, error rate, tokens in / out / total, cost, cost per
request, tool calls, tool errors, tool error rate), deltas against the
previous period (requests_pct, p50_pct, error_rate_pt, tokens_pct,
cost_pct; null when there is nothing to compare against), series (one
entry per bucket, oldest first, zero-filled, each with ts, requests,
requests_by_agent, the three percentiles, errors, errors_by_type,
tokens_in, tokens_out, cost, tool_calls, tool_errors), the
agents, models, actions and tools rails, errors_by_type and
markers. The earlier keys — summary, hourly_requests, by_agent,
window_hours, sorts, sort — are still there and still mean what they
did, so anything already reading them keeps working.
Percentiles are nearest-rank over each trace's total duration and are
computed in Ruby from one pass over the window, so PostgreSQL and SQLite
return the same numbers; cost is ModelPricing's estimate per trace from
the model on its first LLM span. ActionAgent::MetricsReport is the whole
of it if you would rather call it directly.
An evaluation scores an agent one of two ways. Without scenarios it samples the agent's recent recorded generations and scores them against rule, telemetry and LLM-judge criteria. With scenarios it replays a list of user messages you paste in — one fresh run per scenario, per candidate model — and reports which tasks the agent completes, what the failures have in common, and what to change. That is how to answer "can this agent do these new tasks with the tools it has?" and "how does a frontier model compare with the latest open-weights model on my workload?" without waiting for traffic.
Paste scenarios into the Scenarios field of the New Evaluation form, or
through the API (scenarios_text, or a scenarios array). One message per
line; # Heading lines group related tasks so a group can be run on its own;
options after | set expectations:
# Find records
Which gynecologists in Charlotte have scheduling enabled? | tools: find_records
Show me all providers with no license on file
# Blame
Who changed the biography for Dr. AbdelRazek? | contains: AbdelRazek
| Option | Meaning |
|---|---|
tools: a, b |
A passing answer calls at least one of these tools |
contains: x, y |
The answer must contain each pattern (substring or regex) |
not_contains: x |
The answer must not contain the pattern |
key: k |
A stable key, so results line up across re-imports |
group: g |
Overrides the heading for this line |
Compare models takes the candidates as a comma-separated list. A bare
name infers its provider from the family (claude-* → Anthropic, gpt-* →
OpenAI, name:tag → Ollama); prefix it to be explicit
(ollama/qwen3:8b, openrouter/meta-llama/llama-3.3-70b-instruct). Each
candidate needs credentials the same way an agent run does — the owner's
provider key or the host app's config/active_agent.yml.
A run is queued (EvaluationRunJob) and its results land as each replay
finishes. Each replay runs as the evaluation's owner when agents are owned
per user, so a tool scoped to its caller sees that user's rows; a
multi-tenant install replays unattributed unless a host adapter
(ActionAgent.scenario_evaluation_adapter_resolver) runs the suite itself. The suite card opens onto the three questions asked of a run, in
that order — is it getting better, which model, what do I fix — and then
the evidence behind them.
Runs lists the suite's history, newest first, numbered #n from the
oldest so a number keeps naming the same run once the list is capped. A row
carries when the run finished, a pass bar per model, and its delta against
the previous complete run — +3 passed vs #7, green when it moved up, red
when it moved down. A run over a different number of scenarios or models
reads partial run instead: those two totals are not comparable and a
delta would lie about it. Selecting an older run re-derives everything below —
models, what to fix, the matrix, the drill-downs — so the whole card
describes the run you are reading, while the collapsed header keeps
reporting the latest.
Models gives each candidate its pass bar and k/n, mean score, mean
latency, input and output tokens, estimated cost, and the faults it hit as
badges (or [+] no faults). The model that did best carries an
info-toned judge's pick badge — never a green winner, because losing a
comparison by one scenario is not a failing grade — and the verdict
line under the panel is the rationale for the pick. The panel says who made
it: the judge model, or rules when the run was scored without one and the
ranking is pass rate alone.
What to fix turns the faults into work. One card per fault, plus one
per instruction change the judge proposed, each naming the scope it speaks
for (3 scenarios · both models), the fix it calls for, and the tools
involved — deduplicated to one chip per tool, whatever the number of
scenarios that hit it: the missing tools a scenario expected, the tools
that errored, or the tools the judge suggested adding. When every missing
tool resolves to the same MCP server the card names it (served by Playwright) and says whether this agent has it enabled or merely has it
available, which is usually the whole diagnosis. The action follows from
that: Enable server for Agent deep-links to MCP Services, failing
or suggested tools to Tools, an instruction change to the agent's
instructions — in-app, with the run still open behind it.
Scenarios is the matrix: one row per scenario, one column per model,
filtered by group chips or [ ] failed only. Each row shows the tools the
scenario expects as chips, and each cell the [+]/[!] glyph, the score,
the fault, and the tools that model actually called — coloured against the
expectation, so a call that satisfies it reads green, one that errored red
with ✗, and anything else stays muted (no tools called when there were
none). Group rows carry k/n passed per model. Opening a row drills into
it: each model's answer, its tool calls, its timing, tokens and cost, and
the diagnosis behind its fault, with re-run scenario -> to replay that
one on its own and a [x] enabled toggle to keep it out of later runs.
The footer states the run's terms — the judge, the criteria it scored on,
what it cost — and links to run report ->: the same self-contained page
Report#to_html writes for a CLI run, framed in the dashboard's own theme
so it does not flash white inside a dark console. Open standalone opens
the unframed page, which is the copy to archive next to a CI run.
Delete suite, on the right, takes the suite and its runs with it.
A scenario passes when the run completed, met its expectations, and scored at least 0.7 across the evaluation's criteria. Anything else carries exactly one fault, assigned from the evidence in this order:
| Fault | Meaning | Typical fix |
|---|---|---|
run_error |
The replay raised, or the model returned nothing | Credentials, model name, throttling |
tool_error |
A tool the agent called returned an error | Fix the tool, or its parameter descriptions |
missing_capability |
The agent said no tool covers the task | Add the tool the recommendation names |
expected_tool_not_called |
The scenario expects a tool the agent did not call | Enable the tool, or sharpen its description / the instructions |
ungrounded_answer |
The agent had tools, called none, and still stated specifics — a count, an id, a date — nothing supplied | Instruct it to answer only from tool results; add the tool that returns this data |
forbidden_content / missing_content |
A content expectation failed | Instructions, or the tool's output |
low_quality |
Criteria scored the answer below 0.7 | Read the answer against the weakest criterion |
missing_capability, expected_tool_not_called and ungrounded_answer are
the faults that turn a pasted list of new tasks into a backlog: they say which
tasks the current toolset cannot reach and what to build. The last two also
tell an honest gap from an invented answer: expected_tool_not_called carries
ungrounded: true in its evidence when the answer stated specifics no tool
supplied, and ungrounded_answer is the same finding for a scenario that
names no expected tool.
The parsing, scoring, diagnosis and report are the framework's
ActiveAgent::Evals; the engine adds the
persistence, the job, the API and the UI. An app can run the same
evaluations against its own agent from Ruby with that module alone.
The API: POST /api/evaluations with scenarios_text;
POST /api/evaluations/:id/run with group, keys[], scenario_ids[]
and models[]; GET /api/evaluations/:id/runs/:run_id for the results,
which carry the same fix_items the What-to-fix cards are built from,
server resolution included; GET/PUT /api/evaluations/:id/scenarios to
read or replace the suite; and
GET /api/evaluations/:id/runs/:run_id/report for the HTML report, with
?theme=dark or ?theme=light to pin its palette.
The dashboard is itself an MCP server: POST <mount>/mcp speaks Streamable
HTTP JSON-RPC, authenticated with a dashboard API key (Settings → API Keys)
as a Bearer token. Connect a client with:
{ "type": "http", "url": "https://example.com/activeagents/mcp",
"headers": { "Authorization": "Bearer aa_..." } }tools/list offers two kinds of tool:
| Tool | What a call does |
|---|---|
run_<slug> (one per agent the key can reach) |
Runs that agent with { message } and returns its answer; a named action marked expose as tool is run_<slug>__<action> |
find_<records>, count_<records>, get_<record> (one set per discovered schema tools class) |
Reads the host's records directly, with the tool's own parameter schema, so a client that only needs the rows does not have to ask an agent for them |
Every call runs as the key's caller — the key's owner, or whatever
ActionAgent.agent_actor_resolver returns for the request — so a schema
tool's scope sees the same actor it would inside an agent run, and an
agent's own authorization callbacks decide against the same person. A
boundary violation (an undeclared filter, an id the caller cannot see) comes
back as a tool result with isError, the shape an agent's model would get;
a refusal raised by the host's scope or by an agent answers as a JSON-RPC
error (-32003), never as an empty, confident result. Direct reads run no
generation, so neither execution_enabled nor the execution quota applies to
them. Set ActionAgent.mcp_schema_tools = false to keep schema tools
reachable only through agents. agent://<slug> resources return each
agent's live scorecard.
The dashboard has no authentication by default. Anyone who can reach the route can read your traces. Before deploying anywhere non-local, set an authentication method in the initializer:
ActionAgent.configure do |config|
# Any proc that authenticates the request — Devise, Rails 8 sessions, basic auth…
config.authentication_method = ->(controller) do
controller.authenticate_admin!
end
endOr constrain the mount in config/routes.rb:
authenticate :user, ->(u) { u.admin? } do
mount ActionAgent::Engine => "/activeagents"
endThe local ingest endpoint accepts unauthenticated posts by default (it
receives traces from your own app process on your own machine). If the
mount is reachable from other machines, set config.ingest_api_key to
require a Bearer token — see
Self-Hosted Observability. In
multi-tenant mode ingest always authenticates per-account keys (see
below).
Point telemetry at any compatible receiver — including the hosted platform — instead of (or in addition to) local storage:
telemetry:
enabled: true
endpoint: https://api.activeagents.ai/v1/traces
api_key: <%= ENV["ACTIVEAGENTS_API_KEY"] %>The wire format is documented in telemetry.md under "self-hosting endpoint requirements" — anything that speaks it can feed or receive these traces.
The engine also supports account-scoped deployments — this is exactly how the hosted platform runs it:
ActionAgent.configure do |config|
config.multi_tenant = true
config.account_class = "Account" # must have a telemetry_api_key column
config.trace_model_class = "TelemetryTrace" # optional model override
endIn multi-tenant mode the ingest API authenticates with
Authorization: Bearer <account.telemetry_api_key> and processes traces
asynchronously through ActionAgent::ProcessTelemetryTracesJob
(idempotent per trace_id, capped at 100 traces per request). Add an
increment_telemetry_usage! method to your account model to hook usage
tracking or rate limiting.
| This engine | activeagents.ai (production) | |
|---|---|---|
| Intended use | Development, or your own production mount | Managed production |
| Traces + span waterfall | ✓ | ✓ |
| Metrics + per-agent stats | ✓ | ✓ |
| Trace ingest API | ✓ (single tenant, local) | ✓ (multi-tenant, quotas) |
| Agent builder, runs, versions | ✓ | ✓ |
| Conversations, evaluations, scorecards, cost estimates | ✓ built in | ✓ |
| Accounts, plans, billing, managed sandboxes | Yours to operate | ✓ |
One gem, two contexts: it shows your traces while you develop, and the platform runs the same engine multi-tenant with managed infrastructure. What the platform adds is the business around it — accounts, plans, billing, quotas and cloud sandboxes — not a bigger feature set. To run it as a shared production surface of your own, see Self-Hosted Dashboard.
actionagent depends on
solid_agent, so it is already
in your bundle — the Interactions view is built on the contexts, messages and
generations SolidAgent::HasContext records, and dashboard runs persist
through it. The concern resolves those by name to solid_agent's own
AgentContext, AgentMessage and AgentGeneration models, so run its
installer once as well:
rails generate solid_agent:install
rails db:migrateInclude the same concern in your own agents to persist their conversations
alongside traces; generation records carry the same trace_id for
correlation:
class ApplicationAgent < ActiveAgent::Base
include SolidAgent::HasContext
has_context contextual: :user
endSee Persistence (SolidAgent) for the rest of what that gem records — the tool exchange, long-term memory, runs and cost.








