Skip to content

Repository files navigation

reusable-agents

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.

Dashboard at a glance

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:

Agent grid

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:

Dependency graph

Click any node to see what it depends on + what it triggers:

Graph side panel

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:

Agent detail with goals

Filtering the grid to a single application:

Application filter

Why

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.json files 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.

Live 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.

Goals & metrics — the north star

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?

The pipeline

   ┌─────────────┐                ┌──────────────────┐
   │ 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

The contract every agent honours

  1. Declare 3–7 goals at registration via PUT /api/agents/<id>/goals (or seed via install/seed-default-goals.sh). Each goal has:
    • id (kebab-case, stable, never reused)
    • title + description
    • metric: {name, current, target, direction, unit, horizon_weeks}
    • target_metric (top-level): the key in RunResult.metrics that 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.
  2. Emit a numeric metrics dict on RunResult. Every key the declared goals reference must appear here. Non-AgentBase scripts must POST to /api/agents/<id>/goals/<goal_id>/progress directly.
  3. 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.

Layers of metric capture

  • 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 up target_metric in 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.

When goals drift

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.

The prioritization rule

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.

Documentation

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.

What's in the box

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)

Quick start

One-command bootstrap

git clone https://github.com/voidsstr/reusable-agents
cd reusable-agents
bash install/bootstrap.sh

The bootstrap walks you through everything in 7 prompted steps:

  1. Prereq check — python3, docker + compose, optional az/claude
  2. .env creation with a freshly-generated API token
  3. Storage backendlocal (filesystem, zero deps) or azure (Blob Storage; pluggable for S3 / GCS / R2 — see below)
  4. AI provider auth — claude-pool init, OpenAI/Anthropic/Azure keys
  5. Email OAuth (optional) — pointer to setup-microsoft-oauth.sh
  6. docker compose up -d --build — API on :8093, UI on :8091
  7. 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.)

Manual install (if you want to know what bootstrap does)

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 agents

Configuring storage (pluggable)

Three 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.

Configuring email (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.

Configuring error tracking (optional)

GlitchTip — a Sentry-API-compatible error tracker — ships as an opt-in companion. Bring it up with:

bash install/glitchtip/install.sh

That 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.

Configuring AI providers

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:11434

Claude 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.

Open the dashboard

http://localhost:8091/ — agent grid, dependency graph, runs, decisions, messages, knowledge tab, confirmations.

Manifest format

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

Where everything lives

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.

1. Agent data — Azure Blob (agents container)

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.

2. Logs — host filesystem (/tmp/reusable-agents-logs/)

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.

3. Configuration — three layers

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.

3a. Framework config (host + storage)

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

3b. Reusable agent code

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)

3c. Per-instance manifests + site configs

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"

Quick lookup — "I want to find X for agent Y"

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

LLM provider chain — chat agents + code editor

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.

Chat-style agents — framework.core.ai_providers

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):

  1. Per-call override_provider arg
  2. Manifest metadata.ai.{provider, model}
  3. config/ai-defaults.json agent_overrides[<id>]
  4. config/ai-defaults.json default_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>"

Code-editor agents — framework.core.code_editor

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_LLMclaude (default) | framework (skip claude entirely, use framework chain) | noop (dry-run)
  • IMPLEMENTER_FORCE_FALLBACK=1 — bypass claude for this run only; same effect as IMPLEMENTER_LLM=framework but reversible per-invocation
  • CLAUDE_POOL=0 — disable claude-pool round-robin, use the user's default claude account
  • CLAUDE_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)

Picking between the two systems

Use case System
One-shot text generation (analysis, audit, summary, JSON extraction) ai_providersself.ai_client()
Iterative tool-using research (web search + fetch loop) ai_providerschat_with_fallback(..., tools=…)
Editing files in a repo (apply a rec, generate an article into the codebase) code_editorrun_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.

Implementer path-scope (per-site)

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: true

Enforcement happens at two checkpoints:

  1. Pre-LLM, in agents/implementer/build-aider-invocation.py — any rec whose target_files fall outside policy is deferred with reason out-of-scope per site policy.
  2. Post-LLM, in agents/implementer/run.sh just before git add — newly-touched files are filtered through the policy again; offenders are git 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.pyScopePolicy 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.

Creating a new agent (the standard flow)

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 "..." --register

What gets created

your-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).

Standards every new agent follows

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.

  1. Kebab-case IDmy-new-agent, not MyNewAgent or my_new_agent.
  2. Manifest schema — see Manifest format below. The scaffold pre-fills it from the CLI args you pass.
  3. 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.
  4. Lifecycle (Python agents) — implement run() returning a RunResult. The framework handles state load + response-queue drain + decision log
    • context summary + error capture + status updates.
  5. Capabilities declared — list every meaningful method on the class with declare(name, description, confirmation_required=...). The UI audits these.
  6. 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).
  7. Status reporting — call self.status("doing X", progress=0.5) liberally. Drives the glow animation in the UI.
  8. Decision logging — call self.decide("plan"|"observation"|"choice"|...) for anything a future run should know about.
  9. State persistence — return RunResult.next_state for state to carry forward. Don't write directly to the filesystem; use storage abstraction.
  10. No --no-verify on git commit (release-tagger enforces).

Authoring without the scaffold

If you want to hand-roll an agent:

Subclass AgentBase (recommended for new agents)

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/agent

Bash agents (lightweight option)

You 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.

Email confirmation flow

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.

Article-author-agent conventions (for sites running editorial pipelines)

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 FAQPage when faqs are populated
  • 3-7 declared goal_ids for 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.

Inter-agent messaging

# 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.

Composability with other systems

  • 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-routine in the manifest and provide routine_id + trigger_url
    • trigger_token_env; the framework's trigger endpoint POSTs to Anthropic's /fire API instead of the host-worker queue.

Deploying applications the agents touch

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.

Configuring per site

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.

When the deployer fires

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.

Substitution variables

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.

Operational rules

  • Never --no-verify on 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.

Contributing

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.

License

MIT — see LICENSE.

About

Composable, configuration-driven Claude Code agents you can clone for your own sites — SEO opportunity research, top-5 rank work, content recommendations, optional auto-implementation + deploy, and a feedback loop for human-in-the-loop review.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages