Skip to content

Repository files navigation

Tusk

An AI-native task management platform where a Large Language Model operates the application through a secure, modular Agent Harness. Instead of suggesting actions, the AI selects tools, validates parameters, performs CRUD operations, updates application state, and confirms completed work — autonomously.

Status: Vertical-slice MVP. The core architecture, the LangGraph agent service, the Supabase edge-function streaming proxy, task CRUD, chat UI, analytics, Kanban, Calendar, observability, and bulk operations are fully implemented. The TS harness in packages/shared/ai is retained as the reference implementation and is kept at byte-level SSE parity with the Python agent via golden-file parity tests.

Stack

  • Monorepo: pnpm workspaces + Turborepo (TS/JS) + uv workspace (Python)
  • Frontend: React 19, Vite, TypeScript, TailwindCSS, shadcn/ui, Radix UI, Zustand (5 stores), TanStack Query, React Hook Form, React Router, Framer Motion, Lucide, react-markdown
  • Agent service: Python 3.11, FastAPI, LangGraph, LangChain, sse-starlette, Pydantic v2, pydantic-settings, PyJWT, httpx, supabase-py
  • Edge function: Deno + @supabase/supabase-js (thin JWT-verifying SSE proxy)
  • Database / Auth: Supabase (Postgres + RLS + JWT auth)
  • AI: OpenRouter (GPT-4o-mini default; GPT-5 mini, Claude 3.7 Sonnet allowlisted) via a provider-agnostic LLMClient adapter with a deterministic MockLLM for tests (no API key required)
  • Testing: Vitest (TS unit) + pytest (Python unit + parity) + Playwright (e2e)

Architecture in one diagram

Browser (React)
  │
  │  CRUD (direct, RLS-scoped via anon key + JWT)
  ├──────────────────────────────► Supabase (Postgres + RLS)
  │
  │  SSE (text/event-stream)
  ▼
Supabase Edge Function (agent-run)        ── verifies JWT, forwards
  │
  │  SSE (text/event-stream) + x-tusk-jwt / x-tusk-user-id headers
  ▼
Python Agent Service (FastAPI + LangGraph) ── re-verifies JWT via JWKS (defense in depth)
  │
  ├── LangGraph StateGraph (init → think → call_model → execute_tools → finalize → suggestions)
  ├── 24 tools (CRUD, lifecycle, bulk, analysis) bound via LangChain `bind_tools`
  ├── Memory / ContextBuilder / Validator / Executor / ErrorRecovery / Formatter
  ├── LLMClient ──► OpenRouter  [or MockLLM in tests]
  └── TaskService ──► Supabase (service-role key, bypasses RLS, scopes by userId)

The browser never sees the OpenRouter API key or the service-role key. CRUD goes directly to Supabase via the anon key + user JWT (RLS enforces per-user isolation). AI agent requests go to the edge function, which verifies the JWT and proxies the SSE stream to the Python agent; the agent re-verifies the JWT via Supabase's JWKS (defense in depth) and runs the LangGraph harness with the service-role key, streaming text deltas, thinking steps, tool-call cards, a final structured result, and follow-up suggestion chips back via SSE.

Prerequisites

  • Node.js >= 20 and pnpm 11.x (npm i -g pnpm or corepack enable)
  • Python >= 3.11 and uv (pip install uv or curl -LsSf https://astral.sh/uv/install.sh | sh)
  • A Supabase account (free tier is fine) — supabase.com
  • An OpenRouter API key (optional for tests, required for real AI) — openrouter.ai/keys

Supabase setup

You need a Supabase project for the database (Postgres + RLS), auth (JWT), and the edge function that proxies agent requests.

1. Create a project

  • Go to supabase.com → New Project.
  • Pick a name (e.g. Tusk), choose a region close to you, set a database password. Wait ~2 min for it to provision.
  • Once active, note the Project URL and the anon + service-role keys from Project Settings → API.

2. Apply the database migrations

Run these three SQL files in order against your project. You can do this via the Supabase Dashboard (SQL Editor → paste → Run), the Supabase CLI (supabase db push after mirroring them into supabase/migrations/), or the Supabase MCP server:

  1. packages/db/migrations/0001_initial_schema.sql — tables, enums, indexes
  2. packages/db/migrations/0002_rls.sql — Row Level Security policies
  3. packages/db/migrations/0003_user_id_defaults.sqluser_id defaults from auth.uid()

3. Enable email auth

The frontend uses Supabase email/password auth. In the Supabase Dashboard:

  • Authentication → Providers → Email: make sure Email is enabled.
  • Authentication → URL Configuration: set the Site URL to http://localhost:5173 for local dev (add your production URL later).
  • (Optional) Disable "Confirm email" under Authentication → Providers → Email for faster local dev sign-up, or use the user that Supabase creates automatically.

4. Deploy the edge function (optional for local dev)

The agent-run edge function is only needed if you want to test the production request path (Browser → Edge Function → Python agent). For local dev you can skip this and use the direct-to-agent bypass (see below).

To deploy it:

supabase link --project-ref <your-project-ref>
supabase secrets set AGENT_SERVICE_URL=http://localhost:8000   # or your deployed agent URL
supabase functions deploy agent-run --no-verify-jwt

Environment setup

The repo uses three env files (all gitignored — never commit secrets):

.env (repo root) — shared config + OpenRouter

cp .env.example .env

Fill in:

Var Value
SUPABASE_URL https://<ref>.supabase.co
SUPABASE_SERVICE_ROLE_KEY from Supabase → Settings → API
SUPABASE_ANON_KEY from Supabase → Settings → API
OPENROUTER_API_KEY your OpenRouter key (optional for tests)
OPENROUTER_MODEL openai/gpt-4o-mini (default)

apps/agent/.env — Python agent service

cp apps/agent/.env.example apps/agent/.env

Fill in:

Var Value
TUSK_SUPABASE_URL https://<ref>.supabase.co
TUSK_SUPABASE_SERVICE_ROLE_KEY service-role key (server-side only)
TUSK_SUPABASE_JWT_ISSUER https://<ref>.supabase.co/auth/v1
TUSK_OPENROUTER_API_KEY your OpenRouter key (optional for tests)
TUSK_OPENROUTER_MODEL openai/gpt-4o-mini
TUSK_AGENT_TRUST_EDGE 0 for local dev, 1 for production
TUSK_MAX_TURNS 10

apps/web/.env.local — frontend (Vite)

Create this file (Vite only reads VITE_-prefixed vars):

VITE_SUPABASE_URL=https://<ref>.supabase.co
VITE_SUPABASE_ANON_KEY=<your-anon-key>
VITE_AGENT_URL=http://localhost:8000

VITE_AGENT_URL is the local-dev bypass: when set, the web app calls the Python agent directly at http://localhost:8000, skipping the edge function. This is the easiest way to develop locally. Unset it in production so requests route through the Supabase edge function.

Running locally

You need two terminals — one for the web app, one for the Python agent.

# Terminal 1 — install deps (one-time)
pnpm install
cd apps/agent && uv sync --extra dev && cd ../..

# Terminal 1 — web app (Vite dev server on :5173)
pnpm dev

# Terminal 2 — Python agent (uvicorn on :8000)
make -C apps/agent agent-dev
# or: cd apps/agent && uv run uvicorn agent.main:app --reload --port 8000

Open http://localhost:5173. You'll see the login page — sign up with email/password (Supabase auth), then you're in.

How the local request flow works

With VITE_AGENT_URL=http://localhost:8000 set:

Browser → http://localhost:8000/agent/run   (direct to Python agent)

Without VITE_AGENT_URL (or in production):

Browser → Supabase Edge Function (agent-run) → Python agent

Running without an OpenRouter key (tests only)

Both the TS Vitest suite and the Python pytest suite use mock LLM clients (MockLLMClient / MockLLM) and in-memory task services, so they run with zero external dependencies:

pnpm test              # TS unit tests
pnpm agent:test        # Python unit + parity tests

At runtime the agent needs a real TUSK_OPENROUTER_API_KEY — without it, _build_llm falls back to MockLLM, which only matches a few hardcoded intents and is not useful for real work.

Scripts

Script Description
pnpm dev Start the web (Vite) dev server
pnpm build Build all TS workspaces (Turborepo, cached)
pnpm build:edge Bundle @tusk/shared into the edge function bundle
pnpm typecheck TypeScript typecheck (zero implicit any)
pnpm lint Lint all TS workspaces (ESLint)
pnpm test Run Vitest unit tests
pnpm test:e2e Run Playwright e2e tests (requires dev server)
pnpm format Format the TS codebase with Prettier
pnpm agent:test Run the Python agent pytest suite
pnpm agent:lint Ruff check + format check on the Python agent
pnpm agent:typecheck mypy --strict on the Python agent
make -C apps/agent agent-dev Start the Python agent (uvicorn :8000)

Workspace layout

tusk/
├── apps/
│   ├── agent/                  # Python LangGraph agent service (FastAPI + SSE)
│   │   ├── agent/              # graph, tools, schemas, auth, sse_emitter, ...
│   │   ├── tests/              # pytest unit + parity/golden tests
│   │   ├── Dockerfile          # multi-stage production image (port 8000)
│   │   └── pyproject.toml     # uv-managed deps
│   └── web/                    # React 19 frontend (ChatGPT-style shell + task views)
│       └── src/
│           ├── features/       # tasks, chat, analytics, activity, observability, settings, auth
│           ├── store/          # 5 Zustand stores
│           ├── hooks/          # use-tasks, use-agent, use-observability, use-keyboard
│           └── lib/            # agent-client (SSE), supabase-client, env, utils
├── packages/
│   ├── shared/                 # Framework-agnostic core (TS)
│   │   └── src/
│   │       ├── schemas/        # Zod domain schemas
│   │       ├── ai/             # The reference TS Agent Harness (kept at parity)
│   │       ├── services/       # SupabaseTaskService (production)
│   │       ├── testing/        # InMemoryTaskService (tests only)
│   │       └── constants.ts    # priorities, models, token budgets, retry policy
│   └── db/                     # Supabase SQL migrations (0001 schema, 0002 RLS, 0003 defaults)
├── supabase/
│   └── functions/agent-run/    # Deno edge function (JWT verify + SSE proxy)
└── docs/                       # design specs + implementation plans

Two harnesses, one contract

The codebase contains two implementations of the same agent loop:

  • packages/shared/ai (TypeScript) — the original reference harness (Dispatcher, ToolRegistry, StreamingManager, …). It is framework-agnostic (no React/DOM/Node imports) and is exercised by the Vitest suite. It is not wired into the production request path; it is kept as the source of truth for the SSE wire contract.
  • apps/agent (Python / LangGraph) — the production harness. A StateGraph mirrors the TS Dispatcher.dispatch loop 1:1 (init → think → call_model → execute_tools → finalize → suggestions).

The two are kept in lockstep by parity tests (apps/agent/tests/parity/test_parity.py): golden SSE event sequences captured from the TS harness are replayed through the Python run_graph and the event type sequence, terminal turns, and (normalised) assistantText are asserted to match. This is the core regression gate for any change to either harness.

See ARCHITECTURE.md for the full graph topology, module responsibilities, the SSE event contract, the tool catalogue, and extension points. See AGENTS.md for build/test/lint commands.

About

An AI-native task management platform driven autonomously by a Large Language Model via a secure, modular Agent Harness that handles tool selection, parameter validation, CRUD actions, state updates, and completion checks without human intervention.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages