Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
/doc/
/log/*.log
/node_modules/
node_modules/
/pkg/
/test/dummy/db/*.sqlite3
/test/dummy/db/*.sqlite3-*
Expand Down
116 changes: 103 additions & 13 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
| Provider implementations | `lib/active_agent/providers/` |
| Agent concerns/mixins | `lib/active_agent/concerns/` |
| Rails generators | `lib/generators/active_agent/` |
| Dashboard engine (`actionagent` gem) | `actionagent/` |
| Dashboard React source | `actionagent/frontend/` |
| Test suite | `test/` |
| Test Rails app | `test/dummy/` |
| Documentation source | `docs/` |
Expand Down Expand Up @@ -79,8 +81,11 @@ end

### Template Structure

Templates live in `app/views/agents/{agent_name}/`:
- `instructions.md.erb` - System prompt (shared across actions)
Templates live in `app/views/agents/{name}/`, where `{name}` is the class name
underscored with the `_agent` suffix dropped — `TravelAgent` looks in
`app/views/agents/travel/` (`_prefixes` in `lib/active_agent/concerns/view.rb`
also searches the full underscored class name, `app/views/travel_agent/`):
- `instructions.md` - System prompt (shared across actions); `.md.erb` works too
- `{action_name}.md.erb` - Action-specific prompt template

### Provider Configuration
Expand All @@ -102,11 +107,13 @@ development:
rails generate active_agent:agent AgentName action1 action2
```

Creates:
Creates (the view directory drops the `_agent` suffix, the agent file keeps it):
- `app/agents/agent_name_agent.rb`
- `app/views/agents/agent_name_agent/instructions.md.erb`
- `app/views/agents/agent_name_agent/action1.md.erb`
- `app/views/agents/agent_name_agent/action2.md.erb`
- `app/views/agents/agent_name/instructions.md`
- `app/views/agents/agent_name/action1.md.erb`
- `app/views/agents/agent_name/action2.md.erb`
- `test/agents/agent_name_agent_test.rb` and
`test/docs/previews/agent_name_agent_preview.rb`

### Adding a Tool to an Agent

Expand Down Expand Up @@ -167,12 +174,28 @@ end

### Adding a New Provider

1. Create `lib/active_agent/providers/my_provider.rb`
2. Create `lib/active_agent/providers/my_provider/` directory with:
- `client.rb` - API client wrapper
- `request.rb` - Request building
- `response.rb` - Response parsing
3. Register in `lib/active_agent/providers.rb`
1. Create `lib/active_agent/providers/{service}_provider.rb` defining
`ActiveAgent::Providers::{Service}Provider` — `gemini_provider.rb` holds
`GeminiProvider`. Subclass `BaseProvider` (`providers/_base_provider.rb`) or
an existing provider. If it needs a client gem, call
`require_gem!(:key, __FILE__)` at the top — the key must exist in
`GEM_LOADERS` at the top of `_base_provider.rb` (`:anthropic`, `:openai`,
`:ruby_llm` today), which is also where the gem's version requirement
lives, so a new client gem means a new entry there
2. Put the supporting pieces in `lib/active_agent/providers/{service}/` — the
shipped providers keep `options.rb`, `request.rb`, `_types.rb` and any
transforms there rather than in one file (see `providers/anthropic/`)
3. There is no registry file to edit. `provider_load`
(`lib/active_agent/concerns/provider.rb`) requires
`active_agent/providers/#{service_name.underscore}_provider` and then
const_gets `ActiveAgent::Providers::#{Service}Provider`, so the file name is
the registration. `service_name` is the config's `service:` value, or the
provider key camelized when the config omits it (`:openai` → `"Openai"`).
`PROVIDER_SERVICE_NAMES_REMAPS`, in that same file, fixes only the constant
half when that string doesn't camelize to your class name (`"Openai"` →
`"OpenAI"`); the require still uses the un-remapped name, which is why
`openai_provider.rb` exists next to `open_ai_provider.rb` as a one-line
`require_relative`. A remap without that alias file is a LoadError

## File Naming Conventions

Expand All @@ -186,16 +209,22 @@ end
## Testing

```bash
# Run all tests
# Run the framework's tests (bare bin/test collects test/**/*_test.rb only)
bin/test

# Run specific test file
bin/test test/path/to/test.rb

# Run tests for a specific provider
bin/test test/integration/open_ai/

# Run both trees — the framework's test/ and the engine's actionagent/test/
bundle exec rake test
```

The engine's tests live in `actionagent/test/` and are outside `bin/test`'s
default glob; the Rakefile's `test` task is what sweeps both.

### Test Fixtures

- VCR cassettes in `test/fixtures/vcr_cassettes/`
Expand Down Expand Up @@ -230,6 +259,56 @@ bin/test test/integration/open_ai/
- Model ID determines which provider is used automatically
- Supports prompts, embeddings, tool calling, and streaming

## The dashboard: a second gem in this repo

This repo ships **two** gems. `activeagent` is the framework — agents,
providers, generation, telemetry reporting, and no Active Record. `actionagent`
is the dashboard: a mountable Rails engine under `actionagent/`, with its own
gemspec, holding traces and metrics, the agent builder, runs, conversations,
evaluations, scorecards, sandboxes, session recordings, and the
agents-as-MCP-server endpoint.

They are separate because the dashboard needs `activerecord` and `solid_agent`,
neither of which the framework requires — and `solid_agent` depends on
`activeagent`, so the framework could never declare that second one without a
cycle. (Both gemspecs declare `railties`; that one is not part of the split.)

| What | Where |
|------|-------|
| Gemspec | `actionagent/actionagent.gemspec` |
| Engine + configuration seams | `actionagent/lib/action_agent/engine.rb`, `actionagent/lib/action_agent.rb` |
| Routes | `actionagent/config/routes.rb` |
| Models, controllers, jobs, services, queries, serializers, views | `actionagent/app/` |
| React source (entry `index.jsx`) | `actionagent/frontend/` |
| Prebuilt JS/CSS (committed) | `actionagent/app/assets/builds/` |
| Install generator | `actionagent/lib/generators/action_agent/install_generator.rb` |
| Engine tests | `actionagent/test/` |

- Everything in the engine is namespaced `ActionAgent::` (`ActionAgent::Agent`,
`ActionAgent::AgentRun`, `ActionAgent::AgentExecutionService`,
`ActionAgent::TelemetryTrace`, …). The engine is a normal gem root, so Rails
finds `app/` and `config/routes.rb` without help.
- `ActionAgent::Compatibility` keeps the pre-split names resolving with a
deprecation, so existing initializers and already-enqueued jobs keep
working: `ActiveAgent::Dashboard` → `ActionAgent`,
`ActiveAgent::TelemetryTrace` → `ActionAgent::TelemetryTrace`,
`ActiveAgent::ProcessTelemetryTracesJob` →
`ActionAgent::ProcessTelemetryTracesJob`. It prepends a `const_missing` onto
`ActiveAgent` rather than aliasing eagerly, so the old names don't load the
engine's models — and drag Active Record in at boot — just by existing.
- Paths are relative to wherever the host mounts the engine (the generator
writes `/activeagents`): `<mount>/api/...` for the JSON API, `<mount>/mcp`
for the MCP endpoint, `<mount>/console/traces` for the server-rendered
views; every other path outside `/api` renders the React app for
client-side routing.
- The frontend is built with `npm run build` inside `actionagent/frontend/` and the
output is committed, so host apps never run a JavaScript build. Initial
state reaches React through a JSON data attribute, not Inertia.
- Host integration goes through `ActionAgent.configure` seams
(authentication, `current_user_resolver`, `multi_tenant`,
`table_name_prefix`, `execution_enabled`, sandbox backends, quotas); all
are optional and unset means single-user self-hosted behaviour.

## Common Gotchas

1. **Generation is lazy** - Nothing happens until `generate_now` or `prompt_later`
Expand All @@ -248,6 +327,11 @@ rails generate active_agent:install
# Generate agent
rails generate active_agent:agent MyAgent action1 action2

# Install the dashboard engine (migrations, mount, initializer).
# Needs the actionagent gem — it does not come with activeagent.
bundle add actionagent
rails generate action_agent:install

# Run tests
bin/test

Expand All @@ -260,6 +344,12 @@ bin/rubocop
- Ruby 3.1+
- Rails 7.2+ / 8.0+ / 8.1+
- Provider gems (optional): `openai`, `anthropic`, `ruby_llm`
- `activeagent` depends on actionpack, actionview, activesupport, activemodel,
activejob, railties and `activeagents-telemetry` — deliberately **not**
activerecord
- `actionagent` (optional — the dashboard) adds `activerecord` and
`solid_agent` on top of `activeagent`. Installing the framework does not
install it: `bundle add actionagent` before running its generator

## Links

Expand Down
53 changes: 53 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,59 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [2.0.0] - Unreleased

### ⚠️ Breaking: the dashboard is now a separate gem

The dashboard engine that shipped inside `activeagent` has moved to a new
gem, **`actionagent`**. `activeagent` is now the framework alone — it no
longer defines `ActiveAgent::Dashboard`, and no longer pulls Active Record
into apps that do not use it.

**If you mount the dashboard, add the new gem before upgrading:**

```ruby
gem "activeagent", "~> 2.0"
gem "actionagent", "~> 2.0" # required if you mount the dashboard
```

Upgrading `activeagent` alone will fail at boot with
`NameError: uninitialized constant ActiveAgent::Dashboard`, raised by your
own initializer or by the `mount ActiveAgent::Dashboard::Engine` line in
`config/routes.rb`. This is a major version precisely so that a
`~> 1.1` constraint will not pick it up on its own.

With `actionagent` installed, the old constants keep resolving through
`ActionAgent::Compatibility` with a deprecation warning:

- `ActiveAgent::Dashboard` → `ActionAgent`
- `ActiveAgent::TelemetryTrace` → `ActionAgent::TelemetryTrace`
- `ActiveAgent::ProcessTelemetryTracesJob` → `ActionAgent::ProcessTelemetryTracesJob`

That last one matters beyond tidiness: Active Job serializes the class name
into the queue payload, so jobs enqueued before the upgrade still resolve
after it.

Other changes for mounted installs:

- **The server-rendered traces console moves from `/traces` to
`/console/traces`.** `/traces` is now the React traces view — the same
data, with more of it.
- **The mount is authenticated everywhere but development and test.** The
sandbox API, the session-recording capture endpoints and the template
endpoints previously allowed anonymous access; they no longer do. The
`GET /api/session_recordings/demo` endpoint is removed.
- **`current_user_method` / `current_account_method` are superseded by
`current_user_resolver` / `current_account_resolver`.** The engine's
controllers are their own base class, so a host app's `current_user`
helper is not available to them.
- **An unresolved owner now scopes to nothing rather than to everything.**
If you configure `user_class` or `account_class`, make sure the matching
resolver actually returns a record, or the dashboard will show no data.
- Existing installs upgrading from the in-gem dashboard: re-run
`rails generate action_agent:install`. It detects the migrations you
already have and emits only what is missing.

## [1.1.0] - 2026-08-12

### Dashboard — self-hosted (enterprise) mount readiness
Expand Down
3 changes: 3 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@ gem "debug" unless ENV["CI"] == "true"
gem "rubocop-rails-omakase"

gemspec

# The dashboard engine, a sibling gem in this repo.
gemspec path: "actionagent", name: "actionagent"
38 changes: 24 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ Use bundler to add activeagent to your Gemfile and install:
bundle add activeagent
```

That is the framework — agents, providers, generation and telemetry
reporting. The dashboard is a second gem, `actionagent`, added separately
when you want it; see [Dashboard & Dev Console](#dashboard--dev-console).

Add the generation provider gem you want to use:

```bash
Expand Down Expand Up @@ -127,14 +131,19 @@ development:
service: "RubyLLM"
```

## Dev Console & Observability
## Dashboard & Dev Console

Active Agent includes a local dev console — traces with span waterfalls,
token usage, and per-agent metrics — as a mountable Rails engine, so you
can watch your agents while you build:
The dashboard is its own gem, `actionagent`: a mountable Rails engine with
traces and span waterfalls, token usage and per-agent metrics, plus the agent
builder, runs, conversations, evaluations, scorecards and cost estimates — so
you can watch and drive your agents while you build. It ships separately
because its models are Active Record models and it runs agents through
[solid_agent](https://github.com/activeagents/solid_agent) — neither of which
`activeagent` depends on, so an app that only runs agents installs neither.

```bash
rails generate active_agent:dashboard:install
bundle add actionagent
rails generate action_agent:install
rails db:migrate
```

Expand All @@ -145,13 +154,14 @@ telemetry:
local_storage: true
```

Open `/activeagents` and every generation appears as a trace. See
The generator mounts the engine at `/activeagents` — open it and every
generation appears as a trace. See
[docs/framework/dashboard.md](docs/framework/dashboard.md) for
authentication, remote ingestion, and multi-tenant mode. For production
monitoring — evaluations, cost estimates, retention, team workspaces —
point telemetry at the hosted platform at
[activeagents.ai](https://activeagents.ai); every workspace starts with a
free low-volume trial.
authentication, remote ingestion, and multi-tenant mode. The hosted
platform at [activeagents.ai](https://activeagents.ai) runs this same
engine multi-tenant, adding what a hosted product has to have — accounts,
plans, billing, quotas and managed sandboxes; every workspace starts with
a free low-volume trial.

## Features

Expand Down Expand Up @@ -199,12 +209,12 @@ response = prompt.generate_now

- [Documentation](https://docs.activeagents.ai)
- [Getting Started Guide](https://docs.activeagents.ai/getting_started)
- [API Reference](https://docs.activeagents.ai/docs/framework)
- [Examples](https://docs.activeagents.ai/docs/agents)
- [API Reference](https://docs.activeagents.ai/framework)
- [Examples](https://docs.activeagents.ai/agents)

## Contributing

We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details.
We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.MD) for details.

## License

Expand Down
32 changes: 31 additions & 1 deletion Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,40 @@ require "rake/testtask"

Rake::TestTask.new(:test) do |t|
t.libs << "test"
t.test_files = FileList["test/**/*_test.rb"]
t.test_files = FileList["test/**/*_test.rb", "actionagent/test/**/*_test.rb"]
.exclude("test/**/integration_test.rb")
.exclude("test/dummy/tmp/**/*")
t.verbose = true
end

task default: :test

# bundler/gem_tasks only discovers the gemspec at the repository root, so the
# dashboard gem needs its own build path. It must be built from its own
# directory: RubyGems resolves a gemspec's file list against the working
# directory, so `gem build actionagent/actionagent.gemspec` from here would
# not find the files it lists.
namespace :actionagent do
desc "Build the actionagent gem into pkg/"
task :build do
require "fileutils"
root = File.expand_path(__dir__)
FileUtils.mkdir_p(File.join(root, "pkg"))

Dir.chdir(File.join(root, "actionagent")) do
sh "gem build actionagent.gemspec"
version = File.read("lib/action_agent/version.rb")[/VERSION = "([^"]+)"/, 1]
gem_file = "actionagent-#{version}.gem"

# A gem missing its own entry point is the failure mode this guards
# against — it installs and resolves, then dies on require.
contents = `tar -xOf #{gem_file} data.tar.gz | tar -tzf -`
%w[lib/action_agent.rb config/routes.rb app/assets/builds/action_agent.js].each do |required|
raise "actionagent gem is missing #{required}" unless contents.include?(required)
end

FileUtils.mv(gem_file, File.join(root, "pkg", gem_file))
puts "actionagent #{version} -> pkg/#{gem_file}"
end
end
end
21 changes: 21 additions & 0 deletions actionagent/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2025 Active Agents AI

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Loading