diff --git a/docs/README.md b/docs/README.md
new file mode 100644
index 0000000..9663b05
--- /dev/null
+++ b/docs/README.md
@@ -0,0 +1,25 @@
+# Open Generative UI Documentation
+
+Open Generative UI is a showcase and template for building AI agents with [CopilotKit](https://copilotkit.ai) and [LangGraph](https://langchain-ai.github.io/langgraph/). It demonstrates agent-driven UI where an AI agent and users collaboratively manipulate shared application state.
+
+## Prerequisites
+
+- Node.js 22+
+- Python 3.12+
+- [pnpm](https://pnpm.io/) 9+
+- [uv](https://docs.astral.sh/uv/) (Python package manager)
+- An OpenAI API key
+
+## Documentation
+
+| Guide | Description |
+|-------|-------------|
+| [Getting Started](getting-started.md) | Install, configure, and run the project |
+| [Architecture](architecture.md) | How the monorepo and request flow are structured |
+| [Agent State](agent-state.md) | Bidirectional state sync between agent and frontend |
+| [Generative UI](generative-ui.md) | Register React components the agent can render |
+| [Agent Tools](agent-tools.md) | Create Python tools that read and update state |
+| [Human in the Loop](human-in-the-loop.md) | Pause the agent to collect user input |
+| [MCP Integration](mcp-integration.md) | Optional Model Context Protocol server |
+| [Deployment](deployment.md) | Deploy to Render or other platforms |
+| [Bring to Your App](bring-to-your-app.md) | Adopt these patterns in your own project |
diff --git a/docs/agent-state.md b/docs/agent-state.md
new file mode 100644
index 0000000..6124bc9
--- /dev/null
+++ b/docs/agent-state.md
@@ -0,0 +1,154 @@
+# Agent State
+
+The core pattern in this project is **CopilotKit v2's agent state** — state lives in the LangGraph agent and syncs bidirectionally with the React frontend. Both the user and agent can read and write the same state.
+
+## Define State in the Agent
+
+State is defined as a Python `TypedDict` that extends `BaseAgentState`:
+
+```python
+# apps/agent/src/todos.py
+
+from langchain.agents import AgentState as BaseAgentState
+from typing import TypedDict, Literal
+
+class Todo(TypedDict):
+ id: str
+ title: str
+ description: str
+ emoji: str
+ status: Literal["pending", "completed"]
+
+class AgentState(BaseAgentState):
+ todos: list[Todo]
+```
+
+The state schema is passed to the agent via `context_schema`:
+
+```python
+# apps/agent/main.py
+
+agent = create_deep_agent(
+ model=ChatOpenAI(model="gpt-5.4-2026-03-05"),
+ tools=[...],
+ middleware=[CopilotKitMiddleware()],
+ context_schema=AgentState, # ← state schema
+ ...
+)
+```
+
+## Read State in React
+
+Use the `useAgent()` hook to access agent state:
+
+```tsx
+import { useAgent } from "@copilotkit/react-core/v2";
+
+function MyComponent() {
+ const { agent } = useAgent();
+ const todos = agent.state?.todos || [];
+
+ return (
+
+ {todos.map(todo => (
+ - {todo.emoji} {todo.title}
+ ))}
+
+ );
+}
+```
+
+## Write State from React
+
+Call `agent.setState()` to update state from the frontend:
+
+```tsx
+const toggleTodo = (todoId: string) => {
+ const updated = todos.map(t =>
+ t.id === todoId
+ ? { ...t, status: t.status === "completed" ? "pending" : "completed" }
+ : t
+ );
+ agent.setState({ todos: updated });
+};
+
+const addTodo = () => {
+ const newTodo = {
+ id: crypto.randomUUID(),
+ title: "New task",
+ description: "",
+ emoji: "📝",
+ status: "pending",
+ };
+ agent.setState({ todos: [...todos, newTodo] });
+};
+```
+
+## Write State from Agent Tools
+
+Tools update state by returning a `Command` with an `update` dict:
+
+```python
+# apps/agent/src/todos.py
+
+from langgraph.types import Command
+from langchain.tools import tool, ToolRuntime
+from langchain.messages import ToolMessage
+
+@tool
+def manage_todos(todos: list[Todo], runtime: ToolRuntime) -> Command:
+ """Manage the current todos."""
+ for todo in todos:
+ if "id" not in todo or not todo["id"]:
+ todo["id"] = str(uuid.uuid4())
+
+ return Command(update={
+ "todos": todos,
+ "messages": [
+ ToolMessage(
+ content="Successfully updated todos",
+ tool_call_id=runtime.tool_call_id
+ )
+ ]
+ })
+```
+
+## Read State in Agent Tools
+
+Use `runtime.state` to read current state:
+
+```python
+@tool
+def get_todos(runtime: ToolRuntime):
+ """Get the current todos."""
+ return runtime.state.get("todos", [])
+```
+
+## How State Flows
+
+1. **User edits a todo** → `agent.setState({ todos: [...] })`
+2. **CopilotKit syncs** the change to the agent backend
+3. **Agent observes** the updated state via `runtime.state`
+4. **Agent calls a tool** → `Command(update={ "todos": [...] })`
+5. **CopilotKit syncs** the update back to the frontend
+6. **React re-renders** because `agent.state.todos` changed
+
+The key insight: there is no separate frontend state management. State lives in the agent, and CopilotKit handles the sync.
+
+## Adding New State Fields
+
+To add a new field to agent state:
+
+1. Add the field to `AgentState` in Python:
+ ```python
+ class AgentState(BaseAgentState):
+ todos: list[Todo]
+ tags: list[str] # new field
+ ```
+
+2. Read it in React:
+ ```tsx
+ const tags = agent.state?.tags || [];
+ ```
+
+3. Write it from React or tools the same way as above.
diff --git a/docs/agent-tools.md b/docs/agent-tools.md
new file mode 100644
index 0000000..dbe2217
--- /dev/null
+++ b/docs/agent-tools.md
@@ -0,0 +1,141 @@
+# Agent Tools
+
+Tools are Python functions that the LangGraph agent can call. They can read and update agent state, fetch data, and perform any server-side logic.
+
+## Creating a Tool
+
+Use the `@tool` decorator from LangChain:
+
+```python
+from langchain.tools import tool, ToolRuntime
+
+@tool
+def my_tool(arg1: str, arg2: int, runtime: ToolRuntime):
+ """Description of what this tool does. The agent reads this to decide when to call it."""
+ return f"Result: {arg1} x {arg2}"
+```
+
+- The docstring tells the agent when and how to use the tool
+- `runtime: ToolRuntime` gives access to agent state (optional parameter)
+- Return value is sent back to the agent as the tool result
+
+## Reading State
+
+Access current agent state via `runtime.state`:
+
+```python
+# apps/agent/src/todos.py
+
+@tool
+def get_todos(runtime: ToolRuntime):
+ """Get the current todos."""
+ return runtime.state.get("todos", [])
+```
+
+## Updating State
+
+Return a `Command` with an `update` dict to modify agent state:
+
+```python
+from langgraph.types import Command
+from langchain.messages import ToolMessage
+
+@tool
+def manage_todos(todos: list[Todo], runtime: ToolRuntime) -> Command:
+ """Manage the current todos."""
+ # Ensure all todos have unique IDs
+ for todo in todos:
+ if "id" not in todo or not todo["id"]:
+ todo["id"] = str(uuid.uuid4())
+
+ return Command(update={
+ "todos": todos,
+ "messages": [
+ ToolMessage(
+ content="Successfully updated todos",
+ tool_call_id=runtime.tool_call_id
+ )
+ ]
+ })
+```
+
+Key points:
+- `Command(update={...})` merges the update into agent state
+- Include a `ToolMessage` in the `messages` list to acknowledge the tool call
+- Use `runtime.tool_call_id` for the message's `tool_call_id`
+
+## Returning Data (No State Update)
+
+For tools that just return data without modifying state, return the value directly:
+
+```python
+# apps/agent/src/query.py
+
+import csv
+from pathlib import Path
+from langchain.tools import tool
+
+# Load data at module init
+_data = []
+with open(Path(__file__).parent / "db.csv") as f:
+ _data = list(csv.DictReader(f))
+
+@tool
+def query_data(query: str):
+ """Query the financial transactions database. Call this before creating charts."""
+ return _data
+```
+
+## Registering Tools with the Agent
+
+Add tools to the agent's `tools` list in `apps/agent/main.py`:
+
+```python
+from src.todos import todo_tools # [manage_todos, get_todos]
+from src.query import query_data
+from src.plan import plan_visualization
+
+agent = create_deep_agent(
+ model=ChatOpenAI(model="gpt-5.4-2026-03-05"),
+ tools=[query_data, plan_visualization, *todo_tools],
+ middleware=[CopilotKitMiddleware()],
+ context_schema=AgentState,
+ ...
+)
+```
+
+You can pass individual tools or spread a list of tools.
+
+## Example: Adding a New Tool
+
+Say you want to add a tool that fetches weather data:
+
+**1. Create the tool** (`apps/agent/src/weather.py`):
+
+```python
+from langchain.tools import tool
+
+@tool
+def get_weather(city: str):
+ """Get the current weather for a city."""
+ # Your implementation here
+ return {"city": city, "temp": 72, "condition": "sunny"}
+```
+
+**2. Register it** in `apps/agent/main.py`:
+
+```python
+from src.weather import get_weather
+
+agent = create_deep_agent(
+ tools=[query_data, plan_visualization, *todo_tools, get_weather],
+ ...
+)
+```
+
+The agent can now call `get_weather` when a user asks about weather. If you want a custom UI for the result, register a `useRenderTool` on the frontend (see [Generative UI](generative-ui.md)).
+
+## Next Steps
+
+- [Agent State](agent-state.md) — How state sync works between tools and the frontend
+- [Generative UI](generative-ui.md) — Render custom UI for tool results
diff --git a/docs/architecture.md b/docs/architecture.md
new file mode 100644
index 0000000..fdcdcde
--- /dev/null
+++ b/docs/architecture.md
@@ -0,0 +1,90 @@
+# Architecture
+
+## Monorepo Structure
+
+This is a Turborepo monorepo with three apps:
+
+```
+apps/
+├── app/ Next.js 16 frontend (React 19, TailwindCSS 4)
+├── agent/ Python LangGraph agent (FastAPI, OpenAI)
+└── mcp/ Model Context Protocol server (optional)
+```
+
+The frontend and MCP server are managed by pnpm workspaces. The Python agent is managed separately with `uv`.
+
+## Request Flow
+
+```
+Browser
+ │
+ ▼
+Next.js App (:3000)
+ │
+ ├── /api/copilotkit ← CopilotKit API route
+ │ │
+ │ ▼
+ │ CopilotRuntime
+ │ │
+ │ ▼
+ │ LangGraphHttpAgent ──→ FastAPI Agent (:8123)
+ │ │
+ │ ├── LangGraph tools
+ │ ├── CopilotKitMiddleware
+ │ └── State management
+ │
+ └── React UI
+ │
+ ├── useAgent() ← Read/write agent state
+ ├── useComponent() ← Register generative UI
+ ├── useFrontendTool() ← Agent-callable frontend actions
+ └── useHumanInTheLoop() ← Interactive prompts
+```
+
+## Key Files
+
+### Frontend (`apps/app/`)
+
+| File | Purpose |
+|------|---------|
+| `src/app/layout.tsx` | Wraps the app with `` and `` |
+| `src/app/page.tsx` | Main page — chat UI, demo gallery, background |
+| `src/app/api/copilotkit/route.ts` | Connects CopilotKit runtime to the LangGraph agent |
+| `src/hooks/use-generative-ui-examples.tsx` | Registers all generative UI components |
+| `src/hooks/use-example-suggestions.tsx` | Chat suggestion prompts |
+| `src/components/generative-ui/` | Chart, widget, and interactive components |
+
+### Agent (`apps/agent/`)
+
+| File | Purpose |
+|------|---------|
+| `main.py` | Agent definition, FastAPI app, system prompt |
+| `src/todos.py` | `AgentState` schema and todo tools |
+| `src/query.py` | Sample data query tool |
+| `src/plan.py` | Visualization planning tool |
+| `src/form.py` | Form generation tool (AG-UI) |
+| `src/bounded_memory_saver.py` | Memory-capped checkpointer |
+| `skills/` | Skill documents loaded at startup |
+
+### MCP (`apps/mcp/`)
+
+| File | Purpose |
+|------|---------|
+| `src/server.ts` | MCP resources, prompts, and tools |
+| `src/renderer.ts` | HTML document assembly with design system |
+| `src/skills.ts` | Skill file loader |
+| `src/index.ts` | HTTP server |
+| `src/stdio.ts` | Stdio transport for Claude Desktop |
+
+## State Sync
+
+State flows bidirectionally between the agent and frontend via CopilotKit:
+
+```
+Frontend Agent
+──────── ─────
+agent.state.todos ◄──────── AgentState.todos
+agent.setState(...) ────────► Command(update={...})
+```
+
+Both the user (via React UI) and the agent (via tools) can modify the same state. CopilotKit handles synchronization automatically. See [Agent State](agent-state.md) for details.
diff --git a/docs/bring-to-your-app.md b/docs/bring-to-your-app.md
new file mode 100644
index 0000000..e681ec8
--- /dev/null
+++ b/docs/bring-to-your-app.md
@@ -0,0 +1,256 @@
+# Bring to Your App
+
+This guide walks through adopting the CopilotKit + LangGraph patterns from this project into your own application.
+
+## What You Need
+
+- A React frontend (Next.js recommended)
+- A Python backend (or willingness to add one)
+- An OpenAI API key (or another LLM provider)
+
+## Step 1: Install Frontend Packages
+
+```bash
+npm install @copilotkit/react-core @copilotkit/runtime zod
+```
+
+## Step 2: Add the CopilotKit Provider
+
+Wrap your app with the `` provider:
+
+```tsx
+// app/layout.tsx (Next.js App Router)
+import { CopilotKit } from "@copilotkit/react-core";
+
+export default function RootLayout({ children }) {
+ return (
+
+
+
+ {children}
+
+
+
+ );
+}
+```
+
+## Step 3: Create the API Route
+
+```typescript
+// app/api/copilotkit/route.ts
+import {
+ CopilotRuntime,
+ ExperimentalEmptyAdapter,
+ copilotRuntimeNextJSAppRouterEndpoint,
+} from "@copilotkit/runtime";
+import { LangGraphHttpAgent } from "@copilotkit/runtime/langgraph";
+import { NextRequest } from "next/server";
+
+const agent = new LangGraphHttpAgent({
+ url: process.env.LANGGRAPH_DEPLOYMENT_URL || "http://localhost:8123",
+});
+
+export const POST = async (req: NextRequest) => {
+ const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({
+ endpoint: "/api/copilotkit",
+ serviceAdapter: new ExperimentalEmptyAdapter(),
+ runtime: new CopilotRuntime({
+ agents: { default: agent },
+ }),
+ });
+ return handleRequest(req);
+};
+```
+
+## Step 4: Set Up the Python Agent
+
+Install dependencies:
+
+```bash
+pip install copilotkit langgraph langchain langchain-openai fastapi uvicorn ag-ui-langgraph deepagents
+```
+
+Create the agent:
+
+```python
+# agent/main.py
+import os
+from dotenv import load_dotenv
+from fastapi import FastAPI
+from copilotkit import CopilotKitMiddleware, LangGraphAGUIAgent
+from ag_ui_langgraph import add_langgraph_fastapi_endpoint
+from deepagents import create_deep_agent
+from langchain_openai import ChatOpenAI
+
+load_dotenv()
+
+agent = create_deep_agent(
+ model=ChatOpenAI(model=os.environ.get("LLM_MODEL", "gpt-5.4-2026-03-05")),
+ tools=[], # add your tools here
+ middleware=[CopilotKitMiddleware()],
+ system_prompt="You are a helpful assistant.",
+)
+
+app = FastAPI()
+
+@app.get("/health")
+def health():
+ return {"status": "ok"}
+
+add_langgraph_fastapi_endpoint(
+ app=app,
+ agent=LangGraphAGUIAgent(
+ name="my_agent",
+ description="My agent",
+ graph=agent,
+ ),
+ path="/",
+)
+
+if __name__ == "__main__":
+ import uvicorn
+ uvicorn.run("main:app", host="0.0.0.0", port=8123, reload=True)
+```
+
+## Step 5: Define Your State
+
+```python
+# agent/state.py
+from langchain.agents import AgentState as BaseAgentState
+from typing import TypedDict
+
+class Item(TypedDict):
+ id: str
+ name: str
+ done: bool
+
+class AgentState(BaseAgentState):
+ items: list[Item]
+```
+
+Pass it to the agent:
+
+```python
+from state import AgentState
+
+agent = create_deep_agent(
+ ...
+ context_schema=AgentState,
+)
+```
+
+## Step 6: Create Tools
+
+```python
+# agent/tools.py
+from langchain.tools import tool, ToolRuntime
+from langchain.messages import ToolMessage
+from langgraph.types import Command
+
+@tool
+def update_items(items: list, runtime: ToolRuntime) -> Command:
+ """Update the items list."""
+ return Command(update={
+ "items": items,
+ "messages": [ToolMessage(content="Updated", tool_call_id=runtime.tool_call_id)]
+ })
+
+@tool
+def get_items(runtime: ToolRuntime):
+ """Get the current items."""
+ return runtime.state.get("items", [])
+```
+
+Register them:
+
+```python
+from tools import update_items, get_items
+
+agent = create_deep_agent(
+ tools=[update_items, get_items],
+ ...
+)
+```
+
+## Step 7: Use Agent State in React
+
+```tsx
+import { useAgent, CopilotChat } from "@copilotkit/react-core/v2";
+
+function MyPage() {
+ const { agent } = useAgent();
+ const items = agent.state?.items || [];
+
+ return (
+
+ );
+}
+```
+
+## Step 8: Add Generative UI (Optional)
+
+Register components the agent can render in the chat:
+
+```tsx
+import { useComponent } from "@copilotkit/react-core/v2";
+import { z } from "zod";
+
+useComponent({
+ name: "statusCard",
+ description: "Show a status card with a title and message.",
+ parameters: z.object({
+ title: z.string(),
+ message: z.string(),
+ type: z.enum(["info", "success", "error"]),
+ }),
+ render: ({ title, message, type }) => (
+
+ ),
+});
+```
+
+## What to Keep vs. What's Demo-Specific
+
+**Keep (core patterns):**
+- CopilotKit provider + API route
+- Agent state schema + `CopilotKitMiddleware`
+- Tool pattern with `Command(update={...})`
+- `useAgent()` for reading/writing state
+- `useComponent()` / `useFrontendTool()` / `useHumanInTheLoop()`
+
+**Demo-specific (replace with your own):**
+- Todo state schema and tools
+- Demo gallery and explainer cards
+- Widget renderer and chart components
+- Sample data (`db.csv`)
+- Skills documents
+- Animated background and glassmorphism styling
+
+## Next Steps
+
+- [Agent State](agent-state.md) — Deep dive into state sync
+- [Generative UI](generative-ui.md) — All the hooks for rendering UI
+- [Agent Tools](agent-tools.md) — Building backend tools
diff --git a/docs/deployment.md b/docs/deployment.md
new file mode 100644
index 0000000..9d19ea4
--- /dev/null
+++ b/docs/deployment.md
@@ -0,0 +1,98 @@
+# Deployment
+
+## Render
+
+The project includes a `render.yaml` for one-click deployment to [Render](https://render.com/).
+
+### Services
+
+**Agent** (Python):
+- Runtime: Python 3.12.6
+- Build: `pip install uv && uv sync`
+- Start: `uv run uvicorn main:app --host 0.0.0.0 --port $PORT`
+- Health check: `GET /health`
+- Root directory: `apps/agent`
+
+**Frontend** (Node):
+- Runtime: Node 22
+- Build: `corepack enable && pnpm install --no-frozen-lockfile && pnpm --filter @repo/app build`
+- Start: `pnpm --filter @repo/app start`
+- Health check: `GET /api/health`
+- Root directory: (repo root)
+
+### Environment Variables
+
+| Variable | Service | Required | Notes |
+|----------|---------|----------|-------|
+| `OPENAI_API_KEY` | Agent | Yes | Your OpenAI API key |
+| `LLM_MODEL` | Agent | No | Defaults to `gpt-5.4-2026-03-05` |
+| `LANGSMITH_API_KEY` | Agent | No | For LangSmith tracing |
+| `LANGGRAPH_DEPLOYMENT_URL` | Frontend | Auto | Injected from agent service via `fromService` |
+| `SKIP_INSTALL_DEPS` | Frontend | No | Set to `true` to skip redundant installs |
+
+### Auto-Scaling
+
+Both services are configured with:
+- Min instances: 1
+- Max instances: 3
+- Memory target: 80%
+- CPU target: 70%
+
+### Deploy
+
+1. Fork the repository
+2. Create a new **Blueprint** on Render
+3. Connect your forked repo
+4. Add `OPENAI_API_KEY` as a secret
+5. Deploy
+
+Render reads `render.yaml` and creates both services. The frontend automatically gets the agent URL via service discovery.
+
+## General Deployment
+
+For other platforms, you need to deploy two services:
+
+### 1. Agent (Python)
+
+```bash
+cd apps/agent
+pip install uv
+uv sync
+uv run uvicorn main:app --host 0.0.0.0 --port 8123
+```
+
+Requirements:
+- Python 3.12+
+- `OPENAI_API_KEY` environment variable
+- Port exposed for the frontend to reach
+
+### 2. Frontend (Node)
+
+```bash
+# From repo root
+corepack enable
+pnpm install
+pnpm --filter @repo/app build
+LANGGRAPH_DEPLOYMENT_URL=http://your-agent-host:8123 pnpm --filter @repo/app start
+```
+
+Requirements:
+- Node 22+
+- `LANGGRAPH_DEPLOYMENT_URL` pointing to the agent service
+- Port 3000 exposed
+
+### Health Checks
+
+| Service | Endpoint | Expected |
+|---------|----------|----------|
+| Agent | `GET /health` | `{"status": "ok"}` |
+| Frontend | `GET /api/health` | 200 OK |
+
+## Docker
+
+A Dockerfile for the frontend is available at `docker/Dockerfile.app`. The agent can be containerized with a standard Python Dockerfile using `uv`.
+
+## Next Steps
+
+- [Getting Started](getting-started.md) — Local development setup
+- [Architecture](architecture.md) — Understand the service topology
diff --git a/docs/generative-ui.md b/docs/generative-ui.md
new file mode 100644
index 0000000..5ae2f2f
--- /dev/null
+++ b/docs/generative-ui.md
@@ -0,0 +1,169 @@
+# Generative UI
+
+Generative UI lets the agent render React components directly in the chat. Instead of responding with text, the agent can produce charts, interactive widgets, visualizations, and custom UI.
+
+All generative UI in this project is registered in `apps/app/src/hooks/use-generative-ui-examples.tsx`.
+
+## Hooks Overview
+
+| Hook | Purpose |
+|------|---------|
+| `useComponent` | Register a named React component the agent can render with parameters |
+| `useFrontendTool` | Register a tool the agent can call that runs in the browser |
+| `useRenderTool` | Custom renderer for a specific backend tool |
+| `useDefaultRenderTool` | Fallback renderer for any tool without a custom renderer |
+| `useHumanInTheLoop` | Interactive component that pauses the agent for user input |
+
+All hooks are imported from `@copilotkit/react-core/v2`.
+
+## useComponent — Agent-Rendered Components
+
+Register a React component with a name, description, Zod schema, and render function. The agent decides when to use it based on the description.
+
+### Chart Example
+
+```tsx
+import { z } from "zod";
+import { useComponent } from "@copilotkit/react-core/v2";
+
+const PieChartProps = z.object({
+ title: z.string(),
+ description: z.string(),
+ data: z.array(z.object({
+ label: z.string(),
+ value: z.number(),
+ })),
+});
+
+useComponent({
+ name: "pieChart",
+ description: "Displays data as a pie chart.",
+ parameters: PieChartProps,
+ render: PieChart, // your React component
+});
+```
+
+### Widget Renderer (Sandboxed HTML)
+
+The `widgetRenderer` component renders arbitrary HTML/SVG in a sandboxed iframe. This is the most flexible generative UI — the agent writes HTML and it gets rendered with full interactivity.
+
+```tsx
+useComponent({
+ name: "widgetRenderer",
+ description:
+ "Renders interactive HTML/SVG visualizations in a sandboxed iframe. " +
+ "Use for algorithm visualizations, diagrams, interactive widgets, " +
+ "simulations, math plots, and any visual explanation.",
+ parameters: WidgetRendererProps, // { title, description, html }
+ render: WidgetRenderer,
+});
+```
+
+The iframe environment includes:
+
+**ES Module Libraries** (use `