AgentPlayground
Docs
Architecture overview

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.

Stack

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

How a chat request flows end to end

This is the core loop — almost everything else in the app is a variation of it.

  1. Browser sends POST /api/chat (app/api/chat/route.ts) with the message history, which provider/model is selected, and optionally a teamId or projectId.
  2. The route checks the session (auth()), builds the system prompt — the full COORDINATOR_INTRO prompt when no team is selected, or a team-scoped prompt (that team's agents + skills) when one is.
  3. 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.
  4. The provider streams a response. Whenever the model calls a tool, executeTool() in lib/chat-tools.ts runs 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.
  5. Anything worth remembering gets written to the Brain (lib/brain/ingest.ts) so future turns — and other teams — have the context.
  6. 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.

Where things live, at a glance
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.

Docker Compose services

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.

Hard constraints worth knowing before you edit anything
  • No Zod — Valibot only for validation.
  • Never break the output: 'standalone' Docker build.
  • No any types.
  • Every [param] route segment at the same URL depth uses the same name (Next.js requirement).
  • No emojis anywhere in the UI.