A self-hostable framework for running LLM-driven agents with shared memory, scheduled execution, human-in-the-loop confirmations, and a control dashboard. Agents register with a local instance from their own repos, get auto-scheduled via systemd, and write all state to Azure Blob Storage so they get smarter over time.
Agent grid — color-coded by category, glowing while running, with filter pills for application (🛒 aisleprompt / 🎮 specpicks / 🔧 reusable-agents / etc.) and confirmation/queue-driven badges per card:
n8n-style dependency graph — every agent is a node; edges show pipeline triggers, email-confirmation flows, queue dispatches, and shared-config ties. Drag-to-reposition with localStorage persistence, auto-layout via elkjs, custom edge styles per relationship kind:
Click any node to see what it depends on + what it triggers:
Per-agent detail — overview with confirmation-flow banner, dependencies, runs drill-down with per-run artifacts (recommendations, emails, decision logs), and a Goals tab showing persistent objectives with progress bars and 30-point sparklines:
Filtering the grid to a single application:
Most agent systems are monoliths. You install one product and your agents have to live inside it. This framework inverts the relationship:
- Your agent code lives in your own repo (or wherever it makes sense for its problem domain).
- The framework runs next to your agents and provides the cross-cutting infrastructure: registration, scheduling, status, decision logs, message bus, confirmations for dangerous actions, an HTTP API + UI.
- Each repo POSTs its
manifest.jsonfiles to the local framework instance and immediately gains: scheduled execution (systemd timers auto-wired), live status visibility (UI glows when working), a durable decision log, and inter-agent communication.
You can run one framework instance for personal projects, share it across
several of your repos (this codebase already does — nsc-assistant,
specpicks, etc. all register with the same instance), or fork it for
production deployments.
This framework is currently running in production for 🛒 aisleprompt.com — an AI meal planner that builds your Instacart cart. The site's entire content + SEO + per-batch deploy pipeline is driven by agents in this repo: agents curate recipes, optimize SEO, generate articles, and ship code edits to the Vite/React frontend on a 2-hour cron cycle.
The deployment runs on a single dev box with a host-worker process that
subscribes to the framework's job stream and dispatches each agent into a
per-run systemd-run --scope so individual runs can crash without affecting
the rest. The Container App that serves the dashboard is self-deployed via
install/deploy-azure.sh.
Every agent in this framework exists to drive user usage of the configured websites (currently aisleprompt.com, specpicks.com; new sites slot in via per-site config). Code volume, fancy LLM pipelines, and run frequency are not goals — usage on the live site is. Every new agent and every change to an existing agent must answer: what declared goal does this advance, by how much per run?
┌─────────────┐ ┌──────────────────┐
│ agent.run() │ → returns → │ RunResult.metrics │
└─────────────┘ │ {key: float, …} │
└────────┬─────────┘
│ (auto-tracked by AgentBase.post_run)
▼
┌─────────────────────┐ key matches ┌────────────────────┐
│ goal.target_metric │ ◄────────────── │ active goal set │
│ "key" │ │ /api/agents/<id>/ │
└─────────────────────┘ │ goals │
└────────┬───────────┘
│
▼
framework.core.goals.record_goal_progress()
│
┌────────────────────────────┼────────────────────┐
▼ ▼ ▼
progress_history (last N) goal-progress.json/run goals/cache
in active.json (per-run snapshot) (fast UI reads,
drives % to target fed to dashboard via metric_helper)
time-series chart
- Declare 3–7 goals at registration via
PUT /api/agents/<id>/goals(or seed viainstall/seed-default-goals.sh). Each goal has:id(kebab-case, stable, never reused)title+descriptionmetric: {name, current, target, direction, unit, horizon_weeks}target_metric(top-level): the key inRunResult.metricsthat advances this goal each run. Without this, the goal can't auto-tick.directives: list[str]— what the agent should DO each run to advance this goal. Read by the agent's LLM at run start.
- Emit a numeric
metricsdict onRunResult. Every key the declared goals reference must appear here. Non-AgentBase scripts must POST to/api/agents/<id>/goals/<goal_id>/progressdirectly. - Stay legible. The dashboard's Goals tab is the single pane of glass for "is this agent moving the needle?" Fancy work that doesn't move a metric is invisible — and effectively didn't happen.
- Layer A — explicit per-run scoring (
runs/<ts>/goal-progress.json). Used by SEO analyzer-style agents that compute a multi-dimensional score. Wins over Layer B when both fire. - Layer B — implicit auto-track from
RunResult.metrics. Walks the agent's active goals, looks uptarget_metricin metrics, records. This is the default path; use this unless you have a reason not to. - Cache layer (
framework/core/metric_helper.py) maintains a pre-aggregated time-series so the Goals tab loads in one storage call instead of walking every run dir.
Goals stick around forever; that's deliberate (so progress is
comparable across months). But if a goal's target_metric no longer
maps to a real metric — or a metric was renamed — re-PUT the goal
set. As of framework/core/goals.py the merge updates top-level fields
(target_metric, baseline, target, direction…) on existing IDs, so
re-seeding actually persists.
When choosing what to build next, scan the Goals tab first. Stalled goals are the work. Pick the goal whose target gap × user impact is largest, then trace it backward: which agent owns the metric? what's its bottleneck? That is the next change. The review session 2026-05-04 is a worked example.
| Doc | What you read it for |
|---|---|
docs/architecture.md |
The big-picture map: framework + customer repos + Azure storage, lifecycle of one agent run, end-to-end trace of how a SEO recommendation moves through the system. Start here. |
docs/agents-catalog.md |
Every agent in the fleet, organized by category, with code path + manifest path + cron schedule + one-line description. |
docs/repo-boundaries.md |
What goes in reusable-agents/ vs in a customer repo (specpicks, nsc-assistant) vs in ~/.reusable-agents/ (per-host). Decision tree + side-by-side examples + common mistakes. |
docs/seo-onboard-new-site.md |
Step-by-step: add a new site to the SEO agent in 5 commands. |
blueprints/README.md |
The five blueprint patterns (site-quality-recommender, pipeline-stage, inbox-poller, llm-code-editor, scheduled-task) + when to pick each. |
shared/schemas/site-config.schema.json |
The canonical site.yaml schema. Validates every per-site config at registration. |
agents/<id>/README.md |
Per-agent docs (collector, analyzer, reporter, deployer, implementer, responder, digest-rollup, agent-doctor, hydration, ebay-sync, progressive-improvement, competitor-research). |
install/glitchtip/README.md |
Optional self-hosted error tracker (Sentry-API-compatible). 4-container compose, Cloudflare-Tunnel ingress recipe, and the mobile-SDK wiring checklist (with aisleprompt as the worked example). |
CLAUDE.md |
Instructions for Claude Code when working in this repo. References the docs above. |
reusable-agents/
├─ framework/
│ ├─ core/ Importable Python package — the foundation
│ │ ├─ agent_base.py AgentBase lifecycle (setup/pre_run/run/post_run/teardown)
│ │ ├─ storage.py StorageBackend abstraction (Azure Blob + LocalFS)
│ │ ├─ registry.py Master agent list (registry/agents.json in storage)
│ │ ├─ status.py Live status writer + global event log
│ │ ├─ messaging.py Inter-agent async messages (shared/messages/)
│ │ ├─ confirmations.py @requires_confirmation decorator + approve/reject
│ │ ├─ decision_log.py Per-run jsonl log + per-agent changelog
│ │ ├─ context_index.py Date-indexed run summaries with daily rollups
│ │ ├─ scheduler.py systemd --user timer/service writer (cron→OnCalendar)
│ │ ├─ release_tagger.py git commit + tag agent/<id>/release/<run-ts> + push
│ │ ├─ email_codes.py Subject-tag encode/decode, request-id generator
│ │ ├─ guardrails.py Capability dataclass for declared dangerous methods
│ │ └─ mailer.py Outbound mailer abstraction (LogMailer + Graph + SMTP)
│ ├─ api/ FastAPI service (35 routes + 2 WebSockets)
│ │ ├─ Dockerfile python:3.12-slim, non-root, healthcheck
│ │ ├─ host-worker.sh Systemd-user service — exec triggers on host
│ │ └─ app/ Routes for agents/runs/status/messages/etc.
│ ├─ ui/ React + Vite + Tailwind dashboard
│ │ ├─ Dockerfile node:20 build → nginx:1.27-alpine, iframe-friendly
│ │ ├─ nginx.conf Reverse-proxies /api + /ws to agent-api
│ │ └─ src/ AgentList, AgentDetail, Confirmations, Events
│ └─ tests/ pytest suite — 20 tests cover core primitives
├─ blueprints/ Reusable agent-pattern templates (see blueprints/README.md)
│ ├─ site-quality-recommender/ Crawl + LLM analysis + email recs (auto-pilot capable)
│ ├─ pipeline-stage/ One step in a multi-stage pipeline (run-dir based)
│ ├─ inbox-poller/ IMAP loop, parses tagged subjects, dispatches replies
│ ├─ llm-code-editor/ Reads recs, drives LLM to apply edits, commits + deploys
│ └─ scheduled-task/ Default cron-driven script blueprint
├─ agents/ All in-repo reference agents (consolidated layout)
│ ├─ progressive-improvement-agent/ Reference impl of site-quality-recommender (audits)
│ ├─ competitor-research-agent/ Reference impl of site-quality-recommender (competitor)
│ ├─ seo-{data-collector,analyzer,reporter,implementer,deployer}/ Reference SEO pipeline
│ ├─ responder-agent/ Reference impl of inbox-poller
│ └─ reusable-agents-competitor-research-agent/ Self-improvement instance (codebase mode)
├─ shared/
│ ├─ schemas/ JSON schemas (recommendations + site config)
│ ├─ site_config.py SEO site-config loader
│ └─ site_quality.py Site-quality config loader + tier scoring + email render
├─ install/
│ ├─ register-agent.sh POSTs one manifest.json to the framework
│ ├─ register-all-from-dir.sh Walks a dir and registers every manifest.json
│ ├─ install-host-worker.sh Sets up the host-worker systemd unit
│ ├─ install.sh One-shot installer (validates env, brings up stack, seeds providers)
│ ├─ bootstrap-azure.sh Creates Azure resource group + storage account + container
│ ├─ seed-providers.sh Seeds AI provider skeletons (Azure / Anthropic / Ollama / Copilot / OpenAI)
│ └─ seed-providers-local.sh Host-tailored seeder for the dev box
├─ docker-compose.yml API + UI services
├─ .env.example Operator config template
└─ examples/sites/*.yaml Per-site SEO config templates (legacy use)
git clone https://github.com/voidsstr/reusable-agents
cd reusable-agents
bash install/bootstrap.shThe bootstrap walks you through everything in 7 prompted steps:
- Prereq check — python3, docker + compose, optional az/claude
.envcreation with a freshly-generated API token- Storage backend —
local(filesystem, zero deps) orazure(Blob Storage; pluggable for S3 / GCS / R2 — see below) - AI provider auth — claude-pool init, OpenAI/Anthropic/Azure keys
- Email OAuth (optional) — pointer to
setup-microsoft-oauth.sh docker compose up -d --build— API on:8093, UI on:8091- Host worker — systemd-user service that exec's agent runs on the host
When it finishes, open http://localhost:8091 and you're up.
For non-interactive (CI / Dockerfile RUN) mode:
bash install/bootstrap.sh --non-interactive(Reads everything from .env; fails fast if anything's missing.)
cp .env.example .env
$EDITOR .env # see Configuration § below
docker compose up -d --build # API on :8093, UI on :8091
bash install/install-host-worker.sh # systemd-user agent executor
bash install/register-all-from-dir.sh ./agents # register reference agentsThree options, controlled by STORAGE_BACKEND in .env:
| Backend | Setup | Best for |
|---|---|---|
local (default) |
STORAGE_BACKEND=local — writes to ~/.reusable-agents/data (configurable via AGENT_STORAGE_LOCAL_PATH) |
Dev, single-host, no external deps |
azure |
STORAGE_BACKEND=azure + AZURE_STORAGE_CONNECTION_STRING + AZURE_STORAGE_CONTAINER |
Production / multi-host |
| Custom (S3, GCS, R2, MinIO) | Implement framework.core.storage.StorageBackend, register at startup |
When you need a different cloud |
Custom backend example:
# my_app/storage_s3.py
from framework.core.storage import StorageBackend, register_backend
class S3Backend(StorageBackend):
name = "s3"
def read_json(self, key): ...
def write_json(self, key, value): ...
def list_prefix(self, prefix): ...
# ... see framework/core/storage.py for the full interface
register_backend("s3", lambda: S3Backend(bucket=os.environ["S3_BUCKET"]))Then STORAGE_BACKEND=s3 in .env and import your module before any
agent code calls get_storage(). The framework ships zero S3 glue —
write your own (≈100 lines) so the boto3 dep stays optional.
Two scripts; same Azure App Registration, different scopes:
# Set in .env first:
# MS_GRAPH_CLIENT_ID=<your-azure-app-client-id>
# MS_GRAPH_TENANT_ID=<your-tenant-id>
# MS_GRAPH_SIGNIN_HINT=<mailbox-to-send-from-and-poll>
bash install/setup-microsoft-oauth.sh # outbound (Mail.Send via Graph)
bash install/setup-imap-oauth.sh # inbound (IMAP polling)The Azure App Registration needs these delegated permissions:
Mail.Send, Mail.Send.Shared, IMAP.AccessAsUser.All, offline_access.
Both scripts use the device-code OAuth flow — works over SSH, no
localhost callback needed.
GlitchTip — a Sentry-API-compatible error tracker — ships as an opt-in companion. Bring it up with:
bash install/glitchtip/install.shThat starts 4 containers (web, worker, postgres, redis, ~300 MB RAM)
on http://localhost:8095. Sentry-API-compatible means the same
@sentry/react-native, @sentry/python, etc. SDKs work with no code
changes — just point the DSN at the local instance.
The framework includes a companion agent — crash-watcher-agent — that
polls the GlitchTip (or Sentry SaaS) REST API every 10 minutes for new
unresolved issues, fetches the top in-app frame, and dispatches a
crash-fix rec to the implementer. The crash → fix → ship loop runs
without human intervention.
Full setup — public-ingress recipe (Cloudflare Tunnel), first-run UI
steps, mobile-SDK wiring checklist (with aisleprompt as the worked
example), retention/backup notes — lives in
install/glitchtip/README.md.
Swap to Sentry SaaS by unsetting SENTRY_API_BASE in the framework
.env — same agent code, same SDK, same DSN format.
The framework's chat fallback chain is claude-cli (claude-pool) → copilot → azure_openai → openai → anthropic → ollama. The implementer
also has a code-editor chain (aider-copilot-proxy → jcode-copilot → ... → ollama). Both honor environment variables + per-deployment
config in storage; no API key in .env = that provider is skipped.
Set any of:
# Pick one or more — agents fall over in DEFAULT_FALLBACK_KINDS order
ANTHROPIC_API_KEY=sk-ant-...
OPENAI_API_KEY=sk-...
AZURE_OPENAI_API_KEY=...
AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com
# Free local fallback:
OLLAMA_HOST=http://localhost:11434Claude Max users: run bash install/init-claude-pool.sh (or just
HOME=~/.reusable-agents/claude-pool/profile-1 claude /login per
profile) to register up to 5 round-robin profiles. The pool waits for
ALL profiles to exhaust before falling over to a different provider —
see framework/cli/claude_pool.py.
http://localhost:8091/ — agent grid, dependency graph, runs, decisions, messages, knowledge tab, confirmations.
Every agent dir has a manifest.json describing it:
{
"id": "specpicks-scraper-watchdog",
"name": "SpecPicks Scraper Watchdog",
"description": "Restarts the scraper container if it dies.",
"category": "research",
"task_type": "desktop-task",
"cron_expr": "*/5 * * * *",
"timezone": "America/Detroit",
"enabled": true,
"owner": "you@example.com",
"runbook": "AGENT.md",
"skill": "SKILL.md",
"entry_command": "bash /absolute/path/to/agent/run.sh",
"metadata": {
"framework": "reusable-agents",
"source_repo": "specpicks"
}
}Field reference:
| Field | Required | What |
|---|---|---|
id |
yes | Stable kebab-case id; primary key for the framework |
name |
yes | Display name for the UI |
description |
no | One-line summary |
category |
no | One of seo / research / fleet / personal / ops / misc (or your own) |
task_type |
no | desktop-task (host) / cloud-routine (Anthropic Routines) / manual |
cron_expr |
no | 5-field cron — auto-wires a systemd timer if set |
timezone |
no | IANA tz, default UTC |
enabled |
no | If false, schedule is registered but disabled |
owner |
no | Email — gets confirmation requests for dangerous actions |
runbook |
no | Path (relative to manifest dir) to AGENT.md, or use the convention |
skill |
no | Path to SKILL.md (Claude Desktop task definition) |
entry_command |
no | Shell command for the host-worker to exec on "Run now" |
metadata |
no | Free-form JSON — flow through to the registry |
When an agent runs there are three places state lands: agent data (the canonical, durable home in the framework storage backend), logs (transient process output on the host), and config (some in the storage backend, some in version-controlled repos, some on the host). This section is the operator reference for finding any of it.
Everything an agent produces or accumulates lives here. One container,
hierarchical by key prefix. Backend is selected via env (default azure
when AZURE_STORAGE_CONNECTION_STRING is set; falls back to local FS at
AGENT_STORAGE_LOCAL_PATH, default ~/.reusable-agents/data, for dev).
registry/
agents.json # master registry — every registered agent's manifest
events.jsonl # global event log (state transitions, registrations, etc.)
config/
ai-providers.json # registered AI providers (azure-openai, anthropic, ollama, copilot, claude-cli)
ai-defaults.json # global default + per-agent overrides
agents/<agent-id>/
manifest.json # canonical manifest (mirror of registry/agents.json[id])
status.json # current state, message, progress — what the dashboard reads
runbook.md # the AGENT.md prompt (embedded at registration)
skill.md # the SKILL.md prompt (embedded at registration)
readme.md # human-readable overview (embedded at registration)
state/latest.json # carried-forward state (next-run uses this)
state/history/<run-ts>.json # snapshot per run for audit
goals/active.json # long-running goals + current metric values
goals/changes.jsonl # append-only log of recs dispatched against goals,
# with metric_before/after deltas (drives adaptive prompts)
runs/<run-ts>/
progress.json # start/end ts, status, metrics
decisions.jsonl # streaming log of decisions/observations from the run
context-summary.md # human-readable narrative for next run
recommendations.json # generated recs (SEO/PI/CR/catalog-audit)
responses.json # parsed user replies for THIS run
email-rendered.html # body of the email this run sent (where applicable)
pages.jsonl # crawl output (PI/CR)
snapshot.json / comparison.json # pre/post run diff (SEO)
data/ # raw ingest (GSC, GA4, etc.)
deploy.json # deployer artifacts (where applicable)
artifacts/* # agent-specific extras
context-summaries/<YYYY-MM-DD>.md # daily rollups (caps prompt size for older runs)
changelog.jsonl # release tags + commit SHAs from production-affecting runs
outbound-emails/<request-id>.json # email metadata (subject, recipients, rec ids) — used to
# route replies and render the dashboard's Confirmations tab
responses-queue/<request-id>.json # parsed user replies awaiting pickup by an implementer
confirmations/<request-id>.json # pending dangerous-action approvals (per-action gate)
errors/<ts>-<class>.json # unrecoverable errors recorded by the resilience layer
shared/
messages/<message-id>.json # inter-agent async messages (target_agent in body)
inboxes/<agent-id>/<message-id> # zero-byte markers for fast inbox listing
Why blob keys over Storage Queues for messages: indexable by date, auditable in the portal, no 7-day queue retention cap.
Process stdout/stderr that doesn't belong in durable storage. Cleared on host reboot; the API container bind-mounts this read-only so the dashboard's "Live LLM" tab can tail them.
/tmp/reusable-agents-host-worker.log # host-worker service stdout/stderr
/tmp/reusable-agents-logs/<agent-id>-<run-ts>.log # per-run agent stdout
/tmp/reusable-agents-logs/agent-<id>.log # systemd-user service log (responder, etc.)
/tmp/reusable-agents-logs/dispatch-implementer-<site>-<ts>.log
# transient implementer scope output
# (claude --print + tool calls)
The dispatch logs are what the Live LLM tab tails in real time.
Decision logs (decisions.jsonl) and progress JSONs go to storage
(durable), not here.
Config is split intentionally so secrets stay on the host, agent code stays in version control, and per-instance settings stay in the repo that owns the application.
| What | Where | Purpose |
|---|---|---|
| Storage backend choice + connection string | reusable-agents/.env (gitignored) |
STORAGE_BACKEND=azure, AZURE_STORAGE_CONNECTION_STRING=…, AZURE_STORAGE_CONTAINER=agents, FRAMEWORK_API_TOKEN=… |
| Host-worker systemd env | ~/.config/systemd/user/reusable-agents-host-worker.service |
Same Azure env as .env so the host-worker writes to the same storage |
| Docker-compose host overrides | reusable-agents/docker-compose.override.yml (gitignored) |
Per-host port bindings, bind mounts |
| AI provider registry | agents/config/ai-providers.json (in storage) |
Editable from the dashboard's AI Providers page |
| AI provider defaults / per-agent overrides | agents/config/ai-defaults.json (in storage) |
Same, editable from UI |
| Responder IMAP/OAuth config | ~/.reusable-agents/responder/config.yaml (host, gitignored) |
IMAP host, mailbox, oauth file path, dispatcher routes |
| Responder OAuth token | ~/.reusable-agents/responder/.oauth.json (host, mode 0600) |
XOAUTH2 refresh token; used by responder + Graph email send |
The reusable framework + agents repo (this repo). Cloned to a known
path on the host (default /home/voidsstr/development/reusable-agents).
reusable-agents/
framework/ # core lib (storage, status, registry, scheduler, …)
api/ # FastAPI service (Dockerized)
ui/ # React dashboard (Dockerized)
core/ # AgentBase, ai_providers, goals, goal_changes,
# email_codes, completion_email, resilience, …
agents/<reusable-agent-id>/ # generic agent bodies (no per-site assumptions)
agent.py # subclass of AgentBase
AGENT.md # runbook prompt
SKILL.md # task definition for Claude Desktop
manifest.json # template manifest
requirements.txt
README.md
shared/ # cross-agent helpers (site_quality, run_files, schemas)
blueprints/ # cookiecutter-style templates for new agents
install/ # bootstrap scripts (create-agent.sh, install-host-worker.sh)
examples/sites/<site>.yaml # generic per-site configs (used by SEO/PI/CR)
Per-app/site instances live in the repo that owns the application — NOT in the reusable repo. This way each app's deploy pipeline carries its own agent configs.
nsc-assistant/agents/<agent-id>/
manifest.json # registers id, cron, owner, entry_command
site.yaml # per-site config (DB URL, audit script command,
# reporter recipients, implementer repo path)
README.md # operator notes for THIS instance
specpicks/agents/<agent-id>/
manifest.json
site.yaml
README.md
The manifest's entry_command is what the host-worker exec's. It
typically points back at a reusable agent body in this repo:
# example from nsc-assistant/agents/aisleprompt-progressive-improvement-agent/manifest.json:
"entry_command": "PROGRESSIVE_IMPROVEMENT_CONFIG=$HOME/development/nsc-assistant/agents/aisleprompt-progressive-improvement-agent/site.yaml \
python3 $HOME/development/reusable-agents/agents/progressive-improvement-agent/agent.py"| Looking for… | Path |
|---|---|
| Current state of agent | dashboard /agents/<id> (reads agents/<id>/status.json from storage) |
| Live LLM output during a run | dashboard /agents/<id> → "Live LLM" tab (tails /tmp/reusable-agents-logs/dispatch-implementer-*.log) |
| Why an agent failed last run | agents/<id>/runs/<run-ts>/decisions.jsonl + agents/<id>/errors/<ts>-*.json in storage |
| What recs the agent generated | agents/<id>/runs/<run-ts>/recommendations.json (or the legacy ~/.openclaw/.../seo/runs/<site>/<ts>/ for SEO) |
| What user replied to | agents/<id>/runs/<run-ts>/responses.json |
| Email metadata for routing replies | agents/<id>/outbound-emails/<request-id>.json |
| What's queued for the implementer | agents/implementer/responses-queue/*.json |
| Goals + progress | agents/<id>/goals/active.json + goals/changes.jsonl |
| Agent's runbook prompt | agents/<id>/runbook.md (embedded at registration; source-of-truth is manifest.runbook_path in the repo) |
| Cron schedule | ~/.config/systemd/user/agent-<id>.timer (auto-wired from manifest.cron_expr) |
| Host-worker log | /tmp/reusable-agents-host-worker.log |
| Responder log | /tmp/reusable-agents-logs/agent-responder-agent.log |
| AI provider for an agent | dashboard /providers, or GET /api/providers/resolve/<id> |
| Storage browser | dashboard /agents/<id> → "Storage" tab |
There are two parallel routing systems, used by different agent shapes. Both are configurable from the dashboard or storage; you don't edit agent code to switch models.
Any agent that calls self.ai_client(...) or
framework.core.ai_providers.chat_with_fallback(...) (analyzers,
authors, audits, recommenders) routes through this.
| Provider kind | Backed by | Auth | When to pick |
|---|---|---|---|
copilot |
GitHub Copilot proxy (e.g. copilot-api on :4141) |
Copilot Pro/Business subscription, no API key | Default. Includes claude-sonnet-4.6, gpt-4o. Subscription billing — no per-call cost. |
anthropic |
Anthropic API (claude-opus-5, claude-sonnet-4-6, …) |
ANTHROPIC_API_KEY |
Highest-quality per-token. Pay-per-call. |
claude-cli |
Local claude CLI in --print mode |
Claude Max session token (claude setup-token) |
Subscription billing under Claude Max. Per-account 5h rate-limit — share via claude-pool for multi-account round-robin. |
azure_openai |
Azure OpenAI deployments | AZURE_OPENAI_API_KEY + endpoint |
Enterprise-billed OpenAI access, Responses-API for codex. |
openai |
api.openai.com | OPENAI_API_KEY |
Direct OpenAI billing. |
ollama |
Local Ollama (:11434) |
none | Free local inference (qwen3:8b/32b on a GPU host). Privacy + zero cost. |
Resolution order for ai_client_for(agent_id):
- Per-call
override_providerarg - Manifest
metadata.ai.{provider, model} config/ai-defaults.jsonagent_overrides[<id>]config/ai-defaults.jsondefault_provider/default_model
Automatic fallback via chat_with_fallback(agent_id, messages, …):
on rate-limit / timeout / 429 / 502-504 / quota errors the framework
walks DEFAULT_FALLBACK_KINDS = ('copilot', 'azure_openai', 'openai', 'anthropic', 'ollama'), skipping kinds without credentials. Returns
(text, client_used) so the agent records which provider answered.
Switch providers from the dashboard at /providers, or via the API:
TOK=$FRAMEWORK_API_TOKEN
API=http://localhost:8093
# Make copilot the new global default
curl -s -X POST -H "Authorization: Bearer $TOK" -H "Content-Type: application/json" \
"$API/api/providers/defaults/set" \
-d '{"provider_name":"copilot","model":"claude-sonnet-4.6"}'
# Pin one agent to a specific provider/model
curl -s -X POST -H "Authorization: Bearer $TOK" -H "Content-Type: application/json" \
"$API/api/providers/defaults/agent-override" \
-d '{"agent_id":"daily-briefing-calendar-agent","provider":"copilot","model":"claude-sonnet-4.6"}'
# See what's resolved for an agent
curl -s -H "Authorization: Bearer $TOK" "$API/api/providers/resolve/<agent-id>"The implementer (and any llm-code-editor blueprint instance) edits
files via headless coding tools, not via chat(). Configured at
config/code-editor-config.json in storage; ships with sensible
defaults so a fresh install works without a config file.
The chain is a list of editor backends, tried in order until one succeeds. Each backend pairs an editor binary with an LLM model:
| Backend id | Editor | Model | Notes |
|---|---|---|---|
aider-copilot-proxy |
aider | openai/claude-sonnet-4.6 via :4141 |
Default top of chain. Free under existing Copilot subscription. Surgical whole-edit format, byte-stable diffs. |
aider-github-copilot |
aider | github_copilot/claude-sonnet-4 (litellm native) |
Same model, no proxy — needs ~/.config/litellm/github_copilot/api-key.json. |
opencode-azure |
sst/opencode | azure/chat (gpt-4.1-mini) |
Modern provider-agnostic agent. Constrained editor harness — slower but disciplined. |
crush-azure |
charmbracelet/crush | azure/<deployment> |
BYO model via ~/.config/crush/crush.json. |
aider-azure |
aider | azure/<deployment> (gpt-4.1-mini) |
Looser whole-edit format on gpt-4.1-mini occasionally rewrites entire files; demoted to last-resort. |
codex-azure |
OpenAI Codex CLI | ${AZURE_OPENAI_DEPLOYMENT} via Responses API |
Requires Responses-API-enabled Azure deployment — skips otherwise. |
plandex-azure |
plandex | azure/gpt-4.1-mini |
Pluggable; activates when an operator wires up plandex auth. |
Implementer specifically has its own front-of-chain Claude path that's tried before the framework chain:
claude-pool (round-robin Claude Max accounts)
└─ on rc=75 (all accounts rate-limited) or IMPLEMENTER_FORCE_FALLBACK=1
└─ framework code-editor chain (aider via copilot proxy → … → plandex)
Env knobs (read by agents/implementer/run.sh):
IMPLEMENTER_LLM—claude(default) |framework(skip claude entirely, use framework chain) |noop(dry-run)IMPLEMENTER_FORCE_FALLBACK=1— bypass claude for this run only; same effect asIMPLEMENTER_LLM=frameworkbut reversible per-invocationCLAUDE_POOL=0— disable claude-pool round-robin, use the user's defaultclaudeaccountCLAUDE_POOL_FAIL_FAST=1— exit rc=75 on rate-limit instead of waiting (default on for the implementer so the framework chain takes over fast)
| Use case | System |
|---|---|
| One-shot text generation (analysis, audit, summary, JSON extraction) | ai_providers — self.ai_client() |
| Iterative tool-using research (web search + fetch loop) | ai_providers — chat_with_fallback(..., tools=…) |
| Editing files in a repo (apply a rec, generate an article into the codebase) | code_editor — run_with_fallback(EditRequest(...)) |
| Inter-agent handoff (recommender → editor) | Both — recommender uses ai_providers, editor receives the handoff and uses code_editor |
Don't shell out to claude / aider / gh copilot directly from a
new agent — both systems above already wrap those binaries with live
LLM stream capture, usage tracking, fallback chains, and dashboard
visibility. Calling them directly bypasses all of that.
The implementer agent runs aider/claude/copilot against the entire
target repo. Without a path-scope policy, LLMs drift — recs about SEO
meta tags have caused the implementer to refactor a mobile app on the
same repo. Every per-site agent's site.yaml should declare:
implementer:
repo_path: /home/voidsstr/development/<site>
allowed_paths:
- "src/**"
- "frontend/**"
- "db/migrations/**"
- "*.md"
excluded_paths:
- "mobile/**"
- "ios-extensions/**"
post_apply:
kick_mobile_build: false # refuse to trigger EAS even on drift
kick_backend_deploy: trueEnforcement happens at two checkpoints:
- Pre-LLM, in
agents/implementer/build-aider-invocation.py— any rec whosetarget_filesfall outside policy is deferred with reasonout-of-scope per site policy. - Post-LLM, in
agents/implementer/run.shjust beforegit add— newly-touched files are filtered through the policy again; offenders aregit checkout-ed (or deleted if newly-created) and dropped from the commit. This catches drift where an in-scope rec edits an out-of-scope file as a side effect.
Primitive: framework/core/implementer_scope.py — ScopePolicy
dataclass with is_path_allowed(), filter_files(),
is_rec_in_scope(). fnmatch globs, ** matches any number of path
segments. Schema in
shared/schemas/site-quality-config.schema.json.
The framework ships an install/create-agent.sh scaffold script that sets up
a new agent dir conforming to all framework standards (manifest format,
runbook conventions, entry-script shape, registration glue). Use this when
adding a new agent to ANY repo — your repo, my repo, doesn't matter.
Pick a blueprint first — it determines the shape of what you're building:
| You want to... | Blueprint |
|---|---|
| Crawl a site, identify issues, email ranked recs, gate ship-time on user replies | site-quality-recommender |
| Build one stage of a multi-step pipeline (reads upstream run-dir, writes downstream) | pipeline-stage |
| Poll an IMAP inbox, parse subject tags, route replies to other agents | inbox-poller |
| Read approved recs, drive an LLM to apply edits, commit + tag + deploy | llm-code-editor |
| Run a script on a cron schedule (the default) | scheduled-task |
See blueprints/<name>/BLUEPRINT.md for when each fits, what files come
out, and which existing agents are reference implementations.
# Python agent (subclasses AgentBase, gets full lifecycle for free)
bash /path/to/reusable-agents/install/create-agent.sh \
my-new-agent /path/to/your-repo/agents \
--name "My New Agent" \
--description "Pulls X, computes Y, emits Z" \
--category research \
--cron "*/30 * * * *" \
--timezone "America/Detroit" \
--owner "you@example.com" \
--kind python
# Bash agent (lighter weight; no AgentBase, just an entry script)
bash /path/to/reusable-agents/install/create-agent.sh \
my-watchdog /path/to/your-repo/agents \
--description "..." --kind bash --cron "*/5 * * * *"
# Auto-register immediately after scaffolding
bash /path/to/reusable-agents/install/create-agent.sh \
my-new-agent /path/to/your-repo/agents \
--description "..." --registeryour-repo/agents/<agent-id>/
├── manifest.json # registry metadata (already filled in from CLI args)
├── AGENT.md # runbook stub with conventions for decisions, state, gates
├── SKILL.md # Claude Desktop task definition (frontmatter + body)
├── agent.py # AgentBase subclass with example status/decide/confirm calls
├── run.sh # entry script the framework's host-worker invokes
├── README.md # quick reference card
└── requirements.txt # extra Python deps the agent needs
For --kind bash, you get run.sh only (no agent.py).
Every agent MUST declare goals. Goals are persistent objectives that the agent's runs incrementally advance. The framework tracks progress over time (with a sparkline + progress bar in the dashboard) and graduates goals to "accomplished" once their metric target is hit.
{
"id": "goal-zero-broken-pages",
"title": "Drive broken-page count to 0",
"description": "Every URL on the site returns 2xx with valid HTML.",
"metric": {
"name": "broken_pages",
"current": 12,
"target": 0,
"direction": "decrease",
"unit": "pages",
"horizon_weeks": 4
},
"directives": [
"flag every non-2xx response as critical",
"auto-tier any rec with confidence >= 0.95 + severity in {critical,high}"
]
}Goal directives are pasted into the agent's LLM system prompt at run
start to bias analysis. The run() should end with a call to
framework.core.goals.record_goal_progress(...) for each goal, pushing
the new measurement.
Schema: shared/schemas/agent-goals.schema.json. Seed via
install/seed-default-goals.sh (idempotent — preserves history) or PUT
to /api/agents/<id>/goals.
- Kebab-case ID —
my-new-agent, notMyNewAgentormy_new_agent. - Manifest schema — see Manifest format below. The scaffold pre-fills it from the CLI args you pass.
- AGENT.md sections — every runbook has the same eight headings so a new reader can scan: What this agent does · Schedule · Inputs/Outputs · Per-run flow · Hard gates · State carried · Decisions to log · Goals.
- Lifecycle (Python agents) — implement
run()returning aRunResult. The framework handles state load + response-queue drain + decision log- context summary + error capture + status updates.
- Capabilities declared — list every meaningful method on the class
with
declare(name, description, confirmation_required=...). The UI audits these. - Confirmation-gated dangers — wrap any production-affecting method
with
@requires_confirmation(reason=...). The framework emails the owner; nothing happens until the owner approves (via email reply or the dashboard). - Status reporting — call
self.status("doing X", progress=0.5)liberally. Drives the glow animation in the UI. - Decision logging — call
self.decide("plan"|"observation"|"choice"|...)for anything a future run should know about. - State persistence — return
RunResult.next_statefor state to carry forward. Don't write directly to the filesystem; use storage abstraction. - No
--no-verifyon git commit (release-tagger enforces).
If you want to hand-roll an agent:
from framework.core.agent_base import AgentBase, RunResult
from framework.core.guardrails import declare
class SeoDeployer(AgentBase):
agent_id = "seo-deployer"
name = "SEO Deployer"
category = "seo"
capabilities = [
declare("read_metrics", "Pull GSC + GA4 data"),
declare("ship_to_prod", "Deploy a new container revision",
confirmation_required=True, risk_level="high",
affects=["production", "git", "billing"]),
]
def run(self) -> RunResult:
self.status("checking metrics", progress=0.2)
self.decide("plan", "if delta < threshold, skip deploy")
# … work …
self.status("ready to ship", progress=0.9)
return RunResult(status="success", summary="ok",
metrics={"changes_shipped": 0})
@requires_confirmation(reason="deploys a new tag to production Azure")
def ship_to_prod(self, tag: str): ...
if __name__ == "__main__":
SeoDeployer().run_once()Then add a manifest.json next to it and register:
bash /path/to/reusable-agents/install/register-agent.sh /path/to/your/agentYou don't have to subclass anything. Bash agents work fine — they just need
a manifest.json declaring entry_command. They won't get the AgentBase
lifecycle features (status, decisions, etc.) for free, but they're easy
to drop in.
For dangerous actions:
1. Agent calls @requires_confirmation method
2. Framework writes a pending confirmation to storage
3. Framework emails the agent's owner with subject [<agent-id>:<request-id>]
4. Owner replies "yes" / "no" — the responder agent picks it up via IMAP XOAUTH2
5. Responder writes the reply to <agent>/responses-queue/<request-id>.json
6. Next agent run's pre_run() drains the queue, resolves the confirmation
7. The originally-deferred call now succeeds (or raises ConfirmationRejected)
The same flow can be UI-driven: the dashboard's Confirmations page has
approve/reject buttons that write directly to storage, bypassing email.
The framework includes a reference article-author-agent blueprint
used by aisleprompt and specpicks. Sites that pick it up MUST follow
these rules so syndication aggregators (MSN.com, Apple News, Google
News) keep accepting the feed.
Voice — neutral synthesis, not first-party reviews
Forbidden phrases anywhere in article body: "we tested", "in our lab", "our team measured", "we benchmarked", "our testbench", "our review rig". Replace with "Per [source]…", "Public benchmarks show…", "Community measurements indicate…". Numeric claims must cite a URL inline. Disparagement requires a sourced criticism.
Every article ends with ## Citations and sources listing each
inline-referenced URL plus the disclaimer:
"This piece is editorial synthesis based on publicly available
information. No independent first-party benchmarking is reported."
Drives the outbound_citations array → JSON-LD citation field.
SEO surface area the agent must populate
<title>≤65 chars (mobile SERP cap)<meta name="description">140-160 chars- Canonical URL + Open Graph + Twitter card (1200×630 hero)
- JSON-LD
Article(headline, image, datePublished, dateModified, author, publisher, mainEntityOfPage, citation[]) - JSON-LD
BreadcrumbList(Home → Vertical → Article) - JSON-LD
FAQPagewhenfaqsare populated - 3-7 declared
goal_idsfor the SEO opportunity agent
Hero image policy
Heroes are cropped to BOTH 16:9 (lead cards) and 1:1 (80×80 thumb-left
rows on /articles + homepage trending rail). Pick photos that work
for both. Forbidden sources: wikimedia.org, wikipedia.org, generic
placeholder names. Preferred: products.main_image_url of a
featured/related SKU, or a vertical-default image. The
compute_article_vertical SQL trigger auto-buckets into
{ai-rigs, pc-gaming, retro-gaming, makers, how-to, other} on write.
Cross-sell — Amazon + eBay
Every product mention surfaces a tagged Amazon link, an eBay link, or
both — chosen by products.listing_preference:
| listing_preference | Primary | Alternate | Typical SKUs |
|---|---|---|---|
amazon |
Amazon | eBay search URL | Consumer GPUs, Mac Studio |
ebay |
eBay | Amazon search URL | Workstation/pro, retro |
either |
Both | Equal weight | Default |
Sites should also expose a 🛒 Editor's Picks strip above the fold
with side-by-side Amazon (orange pill) + eBay (blue pill) buttons +
live products.price. Each picks-card emits a JSON-LD Product
schema with AggregateRating + Offer for Google rich-results
merchant pricing eligibility.
The article-author-agent's full prompt + rule set lives in the site
repo at agents/article-author-agent/prompts/article_author_system.md
(the canonical reference). Any change to those rules belongs there
first, then propagate to the per-site CLAUDE.md "How articles are
written" section.
Timely > evergreen — four framework primitives
The article-author MUST consume these (don't roll your own):
| Primitive | What it does | When the proposer calls it |
|---|---|---|
framework.core.seasonal_calendar |
Returns active NOW / IMMINENT / UPCOMING US holidays (Memorial Day, July 4th, Thanksgiving, …) + season-relevant recipe_keywords / link_categories. Drives the SEASONAL + HOLIDAY SIGNAL prompt block. |
Every run, in the prompt builder. |
framework.core.trends_signal |
Pulls Google Trends RSS + audience-appropriate subreddits, cached 6h per agent. Drives the TRENDING TODAY prompt block. |
Every run, gathered into signals["trends"]. |
framework.core.featured_rotation |
Reads editorial_articles.tags for holiday:<id> markers; promotes articles whose holiday is active, demotes stale holiday picks; leaves operator-pinned features alone. Cycles by hour for visual variety. |
At the end of every successful article-author run. |
framework.core.article_link_guard |
Counts inline /recipes/<slug> + /k/<slug> links in the body before INSERT; rejects articles below the per-site min and re-queues with a failure addendum so the LLM knows exactly which gap to fix. |
In the implementer's run.sh post-write step. |
Tag every seasonal proposal with holiday:<id> (e.g. holiday:memorial-day)
or the rotation has nothing to cycle. The article-author prompt enforces this
already — don't strip it.
The dedup that protects against re-proposing evergreen articles MUST
exempt holiday-bearing titles, otherwise "Memorial Day Cookout Menu"
gets killed as "near-dup of How to Meal Prep for the Week" because
they share the structural Recipes + Shopping List suffix every meal-
plan article uses. See _dedup_proposals_by_title in aisleprompt's
agent.py for the reference implementation.
# Agent A
self.message(to=["agent-b"], kind="request", subject="please refresh",
body={"site": "aisleprompt"})
# Agent B (next run)
for msg in self.inbox():
if msg["kind"] == "request":
# … handle …
self.mark_message_read(msg["message_id"])Messages persist in shared/messages/ indefinitely — useful for analytics
("what did agent X tell agent Y last month?"). Threading via in_reply_to.
- Existing scripts: register a manifest pointing at your existing bash/python script. Zero refactor.
- Microsoft Graph email: the framework's mailer ships a Graph sendMail implementation with Send-As → Send-on-Behalf fallback.
- OAuth2 IMAP: the responder-agent dir has a complete XOAUTH2 setup for Office 365 + Google Workspace (one-time browser bootstrap, refresh tokens auto-rotate).
- Anthropic Routines / Desktop Scheduled Tasks: declare
task_type: cloud-routinein the manifest and provideroutine_id+trigger_urltrigger_token_env; the framework's trigger endpoint POSTs to Anthropic's/fireAPI instead of the host-worker queue.
When an agent commits code (e.g., the SEO implementer applies a snippet
fix to frontend/src/pages/RecipePage.tsx), the framework can chain
straight into a 5-stage deploy pipeline so the change reaches
production without manual intervention:
test → build → push → deploy → smoke_check
The deployer is cloud-agnostic by design — every stage is just a
shell command template. Whatever you can express in bash (Azure CLI,
AWS CLI, kubectl, Terraform, custom scripts), you can deploy.
Each site declares its own pipeline under deployer: in its
site.yaml. Drop in any recipe from examples/deployer/:
| Recipe | Target | Status |
|---|---|---|
azure-container-apps.yaml |
Azure Container Apps + ACR | active |
azure-app-service.yaml |
Azure App Service + ACR | sample |
azure-functions.yaml |
Azure Functions (consumption) | sample |
aws-ecs-fargate.yaml |
AWS ECS Fargate + ECR | sample |
aws-lambda.yaml |
AWS Lambda + ECR | sample |
aws-app-runner.yaml |
AWS App Runner + ECR | sample |
Sample recipes are valid YAML you can copy verbatim — they just aren't currently used by any production site, so they're shipped as documentation. The two active recipes are wired into aisleprompt and specpicks today.
Per-batch — every successful implementer batch chains into deploy
unless the dispatch is DB-only (article-author / catalog-audit / h2h)
or IMPLEMENTER_SKIP_DEPLOY=1 is in the environment.
Every stage's cmd: runs through a template substitution before exec:
| Variable | Source |
|---|---|
{tag} |
UTC timestamp set at deploy start |
{image} |
deploy.vars.image |
{app} |
deploy.vars.app |
{rg} |
deploy.vars.rg (or any other vars: key) |
{<custom>} |
any key under deploy.vars: |
{tag} and {image} are top-level — every stage sees them. Anything
else under deploy.vars: is also expanded everywhere via the same
template substitution. So a Kubernetes recipe could set
cluster: prod-eks and reference {cluster} in any stage.
See examples/deployer/README.md for
recipe-by-recipe details.
- Never
--no-verifyon git commit — release-tagger fails the run if hooks fail. - Hard cap on agent decisions per run is configurable; default 50 to keep the decision log scannable.
- Status writes are throttled to ≤1/s per agent to avoid blob churn — terminal states (success/failure/blocked/cancelled) are always flushed.
- Cron expressions auto-translate to systemd OnCalendar; complex Quartz
extensions (
L,W,?,#) aren't supported — write the timer by hand if you need them.
This codebase is shared across several of my own repos but designed to be fork-friendly. Open issues / PRs at https://github.com/voidsstr/reusable-agents.
If you build an interesting agent on top of it, I'd love to see it.
MIT — see LICENSE.




