If you have the source open and want to change something, start here. AgentPlayground is a
Next.js 15 App Router app running as a single Docker container plus a few sidecar services — not
a microservices platform, so most changes are just editing a file and rebuilding.
| Layer | Tech |
|---|---|
| Framework | Next.js 15, App Router, output: 'standalone' |
| Language | TypeScript (no any types) |
| Database | PostgreSQL 16 + pgvector, Prisma 7 (@prisma/adapter-pg) |
| Auth | NextAuth v5, JWT strategy |
| Styling | Tailwind CSS v4 + CSS custom properties (design tokens) |
| AI | Anthropic Claude SDK, OpenAI SDK (also serves NVIDIA), Ollama over HTTP |
| Infra | Docker Compose, Traefik (reverse proxy + HTTPS in prod), Redis |
This is the core loop — almost everything else in the app is a variation of it.
- Browser sends
POST /api/chat(app/api/chat/route.ts) with the message history, which provider/model is selected, and optionally ateamIdorprojectId. - The route checks the session (
auth()), builds the system prompt — the fullCOORDINATOR_INTROprompt when no team is selected, or a team-scoped prompt (that team's agents + skills) when one is. - It picks a provider adapter (
lib/providers/) based on the selected provider, or falls back through the free-tier chain (Anthropic → NVIDIA → OpenAI → Ollama) if nothing's explicitly configured. - The provider streams a response. Whenever the model calls a tool,
executeTool()inlib/chat-tools.tsruns it (reading/writing the database, the Brain, or the filesystem) and the result goes back into the conversation — up to several iterations of this per turn, so the coordinator can chain several tool calls (like delegating to two teams in one turn) before producing its final answer. - Anything worth remembering gets written to the Brain (
lib/brain/ingest.ts) so future turns — and other teams — have the context. - The final answer streams back to the browser token by token.
Delegated work (a team the coordinator hands a task to) and scheduled/plan work run the same
provider + tool-loop machinery, just from a different entry point — see lib/agents/delegated.ts
and lib/agents/runner.ts rather than the chat route directly.
app/
├── (app)/ authenticated pages — chat, overview, playgrounds, projects, meetings,
│ files (Brain), settings, store, admin-adjacent pages
├── (auth)/ login, register, first-run setup wizard
├── (marketing)/ public site — homepage sections, /docs, /services
├── admin/ admin-only panel (role === "admin")
└── api/ every route handler — chat, playground/project/team CRUD, library install/
export, Brain, MCP, cron, admin
lib/
├── chat-tools.ts every chat tool: definitions + executeTool()
├── providers/ one adapter per LLM provider (Anthropic/OpenAI/Ollama) + the role-based
│ getProvider()/getEmbedProvider() lookup
├── default-skills.ts built-in team/skill definitions seeded on first run
├── seed-defaults.ts the idempotent seeder that writes those into the database
├── brain/ vault + BrainDocument/BrainChunk read/write/search
├── agents/ delegated-task and plan-task runners (the tool loop outside of chat)
├── planner/, council/ multi-step plan building + multi-perspective deliberation
├── docs/ this documentation site's content + registry
└── *-registry.ts, *-catalog.ts the registry pattern used for sidebar, widgets, store, library
prisma/schema.prisma every database model
components/ shared UI — Sidebar, ModelPicker, MarkdownView, docs shell, etc.See Key files map for a task-oriented "want to change X? edit Y" table.
The app runs as a small stack, not just one container:
| Service | What it is |
|---|---|
| dashboard | this Next.js app |
| postgres | PostgreSQL 16 + pgvector (pgvector/pgvector:pg16 image) |
| redis | cache / rate limiting |
| ollama (optional override) | local model runtime, only started via the docker-compose.ollama.yml override |
In production this sits behind Traefik for HTTPS; the desktop/local install just exposes port
3000 directly. See Docker install and
Environment variables for the practical side of running this.
- No Zod — Valibot only for validation.
- Never break the
output: 'standalone'Docker build. - No
anytypes. - Every
[param]route segment at the same URL depth uses the same name (Next.js requirement). - No emojis anywhere in the UI.