Imported from iamcapote/empty_repo (
AGENTS.md). Install upstream withnpx skills add iamcapote/empty_repo. Copyright stays with the author.
Grail Crusaders — Agent Instructions
Project Overview
Grail Crusaders is an AI agent evolution arena. Agents compete in Dev Arena (write real code in Gitea repos) and Social Arena (interact on a simulated social network). Natural selection decides fitness: code that compiles and passes tests survives; code that fails is culled.
Stack: Python 3.12 / FastAPI / SQLAlchemy 2.x / SQLite (WAL) — Vue 3 / Vite / D3.js — Gitea 1.22 — Docker Compose
Architecture
backend/app/
├── main.py # FastAPI entry, CORS, table creation, pricing seed
├── api/campaigns.py # All REST endpoints (25+)
├── core/config.py # Pydantic Settings, GC_ env prefix
├── core/ids.py # ULID generation
├── db/models.py # 8 SQLAlchemy tables
├── db/schemas.py # 15+ Pydantic request/response models
├── db/session.py # Engine, WAL mode, busy_timeout=5000
├── services/
│ ├── runner.py # Campaign execution engine (seed→rounds→evaluate)
│ ├── context.py # LLM prompt assembly with token budgeting
│ ├── llm_client.py # OpenAI-compatible chat completions
│ ├── seeders.py # Agent profile generation (deterministic + LLM)
│ ├── evaluators.py # Fitness scoring (6 social + 5 dev evaluators)
│ ├── lifecycle.py # State machine transitions
│ ├── tool_registry.py # Tool specs and validation
│ ├── social_arena.py # In-memory social network
│ ├── dev_arena.py # In-memory dev environment (fallback)
│ ├── gitea_env.py # Real Gitea-backed dev arena
│ ├── gitea_client.py # Gitea REST API v1 HTTP client
│ └── events.py # SSE event broadcasting
frontend/src/
├── App.vue, main.js, router.js, api.js
├── views/ # 10 views (CampaignList, Setup, Dashboard, etc.)
└── components/ # 6 viz components (ForceGraph, ScoreRadar, etc.)
Build & Run
# Docker (recommended)
cp .env.example .env # Set GC_LLM_API_KEY and GC_GITEA_ADMIN_TOKEN
docker compose up --build
# Local backend dev
cd backend
pip install -e ".[dev]"
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
# Local frontend dev
cd frontend
npm install && npm run dev
# Tests
cd backend && pytest # 97 tests
cd frontend && npm run build # Verify frontend compiles
Code Conventions
- IDs: All primary keys are 26-char ULIDs (Crockford Base32), generated by
core/ids.new_ulid() - Database: SQLite with WAL journal mode. Use
get_db()dependency for sessions. All JSON fields stored as_jsonText columns, parsed in Pydantic schemas - Config: All settings use
GC_prefix in env vars. Pydantic Settings reads.envfile. Never hardcode secrets - API patterns: FastAPI router in
api/campaigns.py. All endpoints return Pydantic models. Campaign lifecycle enforced bylifecycle.ensure_transition() - State machine: draft → seeding → running → stopped/evaluating → complete/failed. Invalid transitions raise HTTP 409
- Safety profiles:
guarded(10 tool calls/turn, 120s timeout) vsunrestricted(50 calls/turn) - Error handling: LLM failures return
do_nothingtool call as fallback. Tool validation viatool_registry.validate_tool_call() - Gitea integration: Gitea 1.22 uses PUT (not POST) for file create/update. The
gitea_client.pyhandles this
Campaign Lifecycle
- Create — POST
/campaignswith env_type, source_text, agent_count, round_limit - Seed — POST
/campaigns/{id}/seedcreates agents and initializes environment - Start — POST
/campaigns/{id}/startbegins round execution - Evaluate — Automatic after rounds complete, or manual POST
/campaigns/{id}/evaluate - Evolve — POST
/campaigns/{id}/evolvecreates next-generation campaign from top performers - Auto-evolve — POST
/campaigns/{id}/auto-evolveruns multiple generations automatically
Key API Endpoints
| Method | Path | Purpose |
|---|---|---|
| POST | /campaigns |
Create campaign |
| GET | /campaigns |
List campaigns (filter by status) |
| GET | /campaigns/{id} |
Get campaign details |
| POST | /campaigns/{id}/seed |
Seed agents + environment |
| POST | /campaigns/{id}/start |
Start execution |
| POST | /campaigns/{id}/stop |
Stop execution |
| POST | /campaigns/{id}/evaluate |
Trigger evaluation |
| POST | /campaigns/{id}/evolve |
Evolve next generation |
| POST | /campaigns/{id}/auto-evolve |
Multi-generation auto-evolve |
| GET | /campaigns/{id}/leaderboard |
Ranked agents |
| GET | /campaigns/{id}/agents |
List agents |
| GET | /campaigns/{id}/agents/{aid} |
Agent details |
| GET | /campaigns/{id}/agents/{aid}/events |
Agent action history |
| GET | /campaigns/{id}/events |
All campaign events |
| GET | /campaigns/{id}/feed |
SSE real-time stream |
| GET | /campaigns/{id}/usage |
Token/cost usage |
| GET | /campaigns/{id}/diagnostics |
Success/failure metrics |
| GET | /campaigns/{id}/cohorts |
Top/middle/bottom quartiles |
| GET | /campaigns/{id}/social/timeline |
Social posts |
| GET | /campaigns/{id}/social/graph |
Interaction edges |
| GET | /campaigns/{id}/social/network |
D3 force graph data |
| GET | /campaigns/{id}/export |
Full campaign export |
| GET | /campaigns/compare/{a}/{b} |
Compare two campaigns |
| GET | /campaigns/{id}/snapshot |
Experiment snapshot |
Database Tables
8 tables: campaigns, agents, action_events, edges, evaluations, usage_records, model_pricing, experiment_snapshots
campaigns— Config, state, LLM settings, budget, convergence policyagents— Persona, strategy, fitness_score, rank, branch_nameaction_events— Round-by-round tool calls with status and timingevaluations— Per-evaluator scores (reliability, engagement, diff_quality, etc.)usage_records— Token counts and cost per agent per round
Environment Types
Dev Arena (Primary)
12 tools: inspect_repo, list_branches, read_file, write_file, diff_branch, open_issue, comment_issue, list_issues, open_pr, review_pr, list_prs, do_nothing
5 evaluators: reliability, diff_quality, collaboration, review_quality, adaptation
Social Arena
11 tools: read_timeline, create_post, reply_to_post, like_post, dislike, repost, quote_post, follow_agent, list_following, view_agent_profile, do_nothing
6 evaluators: reliability, engagement, influence, adaptation, controversy, argument_survival
Testing
- Tests in
backend/tests/using pytest + httpx TestClient test_comprehensive.py— Campaign CRUD, validation, config parsingtest_lifecycle.py— State transitions, seed/snapshot, conflict detection- Always run
pytestafter changes. All 97 tests must pass - Frontend:
npm run buildmust succeed with zero errors
Common Tasks
Adding a new API endpoint
- Add route function in
backend/app/api/campaigns.py - Add request/response schemas in
backend/app/db/schemas.pyif needed - Add corresponding method in
frontend/src/api.js - Run
pytestto verify nothing breaks
Adding a new evaluator
- Add
evaluate_<name>()function inbackend/app/services/evaluators.py - Register it in the evaluator dispatch within
run_evaluators() - Update docs/ARCHITECTURE.md evaluator tables
Adding a new tool
- Add
ToolSpecentry inbackend/app/services/tool_registry.py - Add handler in
social_arena.pyordev_arena.py(andgitea_env.pyfor dev) - Update tool definitions in
context.pytemplates
Modifying the database
- Add/modify columns in
backend/app/db/models.py - Update schemas in
backend/app/db/schemas.py - SQLite: delete
grail.dbto recreate (or use ALTER TABLE migration) - Docker: rebuild with
docker compose up --build
Docker
Services:
api: port 8060 → backend FastAPI
frontend: port 5174 → Vue/nginx
gitea: port 3000 → Gitea 1.22
Volumes: api-data, gitea-data, postgres-data (optional)
Gitea admin token: create via Gitea UI at http://localhost:3000 → user settings → applications → generate token with all scopes.
Gotchas
- Gitea 1.22 file API: Uses PUT for create/update, not POST. Already handled in
gitea_client.py - SQLite locking: WAL mode + busy_timeout=5000ms prevents "database is locked" errors. Don't run multiple writers
- Venice AI: Uses OpenAI-compatible API at
https://api.venice.ai/api/v1. Model:qwen3-5-9b. Some models don't support tool calling - Campaign status: Must follow state machine. Attempting invalid transitions returns 409 Conflict
- Safety profiles:
guardedlimits to 10 tool calls per turn. The runner enforces this in_run_agent_turn() - JSON fields: Stored as
_jsonText columns. Access via schemafrom_orm_model()or parse manually. Don't serialize None — use empty dict - SSE streaming:
/campaigns/{id}/feeduses Server-Sent Events. Frontend uses EventSource API - CORS: Configured in
main.pyfromGC_CORS_ORIGINSsetting. Add frontend dev ports if needed