Troubleshooting
24 known issues with Symptom / Cause / Fix.
docs/TROUBLESHOOTING.mdExhaustive fix list for AgentKit. If you hit an error, search this doc for the message (Cmd/Ctrl+F) — every entry follows Symptom -> Cause -> Fix.
AgentKit is built on Next.js 16.2.4, React 19.2.4, Tailwind 4, Vitest 4, and AI SDK 6. A lot of the behaviour below is specific to that stack — advice from older Next.js / React tutorials will not always apply here.
Table of contents
- Install & setup
- Runtime & dev server
- AI SDK & chat route
- Build & deploy
- Components & TypeScript
- Styling & theming
- Tests
- Still stuck?
1. Install & setup
1.1 Node version error on npm install or npm run dev
Symptom
error: You are using Node.js 18.x. For Next.js, Node.js version >= 20.x is required.
or a silent SyntaxError: Unexpected token when running the dev server.
Cause Next.js 16 requires Node 20.9+ and we recommend Node 22 LTS. AgentKit uses modern syntax that older Node does not parse.
Fix
# Install nvm if you do not have it
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
# Install and use Node 22
nvm install 22
nvm use 22
node -v # should print v22.x
# Clean reinstall
rm -rf node_modules package-lock.json
npm install
If you use Volta or asdf, pin the same way: volta pin node@22 or asdf local nodejs 22.11.0.
1.2 npm install fails on Apple Silicon (M1 / M2 / M3 / M4)
Symptom
npm error gyp ERR! build error
npm error ... lightningcss.darwin-arm64.node ... not found
or
Error: Cannot find module '../sharp-darwin-arm64/sharp.node'
Cause
Transitive native deps (lightningcss, occasionally sharp via Next's image optimiser) publish pre-built ARM binaries. A stale node_modules from another machine — or an install done under Rosetta — leaves x64 binaries behind.
Fix
# Make sure your shell is native arm64 (not Rosetta)
arch # should print: arm64
# Force a clean native rebuild
rm -rf node_modules package-lock.json .next
npm install
npm rebuild lightningcss sharp
If you still see lightningcss errors, install the optional peer explicitly:
npm install lightningcss --save-optional
1.3 Port 3000 already in use
Symptom
Error: listen EADDRINUSE: address already in use :::3000
Cause
Another next dev or unrelated process is holding the port.
Fix
Option A — use a different port:
PORT=3001 npm run dev
Option B — kill the holder:
lsof -i :3000
# note the PID in the second column, then:
kill -9 <PID>
On Windows PowerShell:
netstat -ano | findstr :3000
taskkill /PID <PID> /F
1.4 npm test fails with ResizeObserver is not defined or scrollIntoView is not a function
Symptom
ReferenceError: ResizeObserver is not defined
TypeError: element.scrollIntoView is not a function
Cause
jsdom (the browser-like environment that Vitest uses) does not implement layout APIs. Radix Tooltip, Popover, Select, and some of our scroll-managing components depend on them.
Fix
The polyfill pattern is already in tests/tooltip.test.tsx and tests/select.test.tsx. Copy it into any new test that mounts a Radix primitive:
import { beforeAll } from 'vitest'
beforeAll(() => {
if (typeof globalThis.ResizeObserver === 'undefined') {
class RO {
observe() {}
unobserve() {}
disconnect() {}
}
globalThis.ResizeObserver = RO as unknown as typeof ResizeObserver
}
// For components that call scrollIntoView on refs:
if (!Element.prototype.scrollIntoView) {
Element.prototype.scrollIntoView = function () {}
}
})
Do not globalise these in tests/setup.ts — they are opt-in on purpose, and individual tests sometimes want to assert against the real (missing) API.
2. Runtime & dev server
2.1 Changes do not appear in the browser
Symptom You edit a file, save, Fast Refresh runs, but the browser shows the old output.
Cause
Turbopack's on-disk cache is stale. This most often happens after switching git branches, editing next.config.ts, or renaming a file the cache has indexed.
Fix
# Stop the dev server (Ctrl+C), then:
rm -rf .next
npm run dev
If it keeps recurring, also clear the Turbopack cache for the current user:
rm -rf .next node_modules/.cache
2.2 Fast Refresh lost my component state
Symptom
After saving a file the component re-mounts and local useState / form input resets.
Cause Expected. Fast Refresh preserves state only when it can statically determine a component's identity. Changing a file's exports, top-level const, or the component's name forces a full reload.
Fix No action needed — just re-trigger the state. If it happens on every save for one component, check that the file exports exactly one component with a stable name (default export of an anonymous arrow function loses identity).
2.3 Hydration mismatch warning involving timestamps
Symptom
Hydration failed because the server rendered HTML didn't match the client.
... "10:42:07" vs "10:42:08"
Cause
Date.now(), new Date(), or Math.random() called during render produces different output on the server and on the client. AgentKit demo components (trace viewer, timeline, cost tracker) accept timestamps as props for exactly this reason.
Fix
Do not call new Date() inside render of a server component that is passed into a client component. Either:
// Option 1 — compute a stable ISO string at module scope or in props:
const startedAt = '2026-04-20T09:00:00.000Z'
<AgentTimeline startedAt={startedAt} />
// Option 2 — compute inside useEffect so it only runs client-side:
'use client'
const [now, setNow] = useState<string | null>(null)
useEffect(() => { setNow(new Date().toISOString()) }, [])
Our timestamp-accepting props all allow Date | string — pass ISO strings for SSR stability.
2.4 Theme flashes default-dark on refresh
Symptom
You set the theme to bright or cool-blue, refresh the page, and see a split-second flash of the dark theme before it switches back.
Cause
/public/theme-init.js is not loading, or is loading too late. That script runs synchronously in <head> and sets data-theme before first paint — if it is missing or 404s, the :root defaults win until React hydrates.
Fix
- Open DevTools -> Network tab, reload, filter for
theme-init.js. It should 200 with typeapplication/javascript. - If it 404s: confirm
public/theme-init.jsexists (ls public/theme-init.js) and that your deploy is serving thepublic/folder. On Vercel this is automatic; on other hosts, check your static asset config. - If it 200s but the flash persists: confirm
src/app/layout.tsxstill has<script src="/theme-init.js" />inside<head>(not<body>), and that it is NOT markeddeferorasync.
2.5 Styling looks off — Tailwind arbitrary CSS-var syntax not applied
Symptom
Text is default black/white instead of the brand colour, borders are missing, or classes like text-[var(--color-text-dim)] render as if the class were not there.
Cause
Tailwind 4 parses [var(--color-x)] syntax natively — but only if the CSS var is defined by the time the class is evaluated. Two common mistakes break this:
- You added a new CSS var but did not add it under every
[data-theme="…"]block inglobals.css. - You mixed Tailwind's short-form
text-brand(which expects a--color-brandtoken registered via@theme) with arbitrary-var syntax in the same element and one form is undefined.
Fix
- Define every new token in all three theme blocks:
:root/[data-theme="editorial-dark"],[data-theme="bright"], and[data-theme="cool-blue"]. - Pick one convention per file. AgentKit components use the arbitrary
[var(--color-x)]form throughout — stay consistent within a component. - If you want
text-brandto work, register the token in the@themeblock (not just in the theme scopes).
3. AI SDK & chat route
3.1 Chat hero shows the scripted demo even though OPENAI_API_KEY is set
Symptom
The hero animates through scripted exchanges (Tokyo weather, Osaka time, etc.) instead of streaming real GPT responses. DevTools shows the chat endpoint returning 503 with body:
{"error":"OPENAI_API_KEY is not configured..."}
Cause
process.env.OPENAI_API_KEY is empty at runtime. Possible reasons:
- The file is named
.envor.env.developmentinstead of.env.local. - You created
.env.localafter startingnext dev— env vars are read once at boot. - The key line has inline comments or quotes:
OPENAI_API_KEY="sk-..." # my keybreaks parsing in some shells. - On Vercel / other hosts, the variable is in
.env.localonly and was never added to the dashboard.
Fix
# In the project root:
cp .env.example .env.local
# Edit .env.local and paste the real key (no quotes, no trailing comment):
# OPENAI_API_KEY=sk-proj-xxxxxxxx
# Restart the dev server fully (Ctrl+C, then):
npm run dev
Verify inside src/app/api/chat/route.ts by temporarily adding console.log(!!process.env.OPENAI_API_KEY) at the top of POST — it should log true.
For production, add OPENAI_API_KEY in the Vercel dashboard (Project -> Settings -> Environment Variables) and redeploy; existing deployments do not pick up new env vars.
3.2 convertToModelMessages returns a Promise
Symptom
TypeError: messages is not iterable
or the model receives a [object Promise] as its first message.
Cause
In AI SDK v6 (what AgentKit ships with), convertToModelMessages is async. Earlier versions returned a synchronous value and a lot of tutorials reflect the old signature.
Fix
Always await it:
messages: await convertToModelMessages(messages),
See src/app/api/chat/route.ts for the canonical usage.
3.3 Edge runtime error: fs, crypto, path, or Buffer not available
Symptom
Error: The edge runtime does not support Node.js 'fs' module.
or 500s from /api/chat after adding a library that reads from disk or uses Node's crypto.
Cause
export const runtime = 'edge' means the route runs on the Edge Runtime, which exposes Web-standard APIs only. fs, path, crypto (the Node one), Buffer, and native addons are all unavailable.
Fix Choose one:
// Option A — stay on edge, use Web APIs
const hash = await crypto.subtle.digest('SHA-256', new TextEncoder().encode('hello'))
// Option B — switch the route to Node runtime
export const runtime = 'nodejs' // replace 'edge'
export const maxDuration = 30
Node runtime loses low-latency cold starts, so only switch if you truly need Node APIs.
3.4 429 Too Many Requests from OpenAI
Symptom
The chat streams a partial reply then dies, or the hero never streams. Network tab shows 429 with body mentioning rate_limit_exceeded.
Cause You hit OpenAI's per-minute or per-day request/token quota. Free-tier and brand-new projects have very low limits.
Fix
- Wait 60 seconds and retry (RPM windows roll).
- Upgrade your OpenAI usage tier (billing -> usage limits).
- Implement exponential backoff in your own fetch wrapper if you route through the chat API in bulk.
- For the landing page specifically: the hero falls back to the scripted demo in
src/lib/demo-agent.tswhen the API errors, so visitors are never blocked. Confirm that fallback fires if 429s are common.
3.5 A tool is defined but the model never calls it
Symptom
The assistant answers in prose and ignores tools like get_weather even for obvious prompts.
Cause (in likely order)
- Tool
descriptionis vague — the model picks tools by description, not by name. inputSchemais not a Zod object, or is a plain JSON Schema. AI SDK v6 expects Zod.- The model is one that does not support tool calls (e.g.
gpt-3.5-turbo-instruct). AgentKit defaults togpt-4o-mini, which supports them. - You are calling
tool({ parameters })(old API) instead oftool({ inputSchema }).
Fix
Match the canonical shape from src/app/api/chat/route.ts:
import { tool } from 'ai'
import { z } from 'zod'
get_weather: tool({
description: 'Get current weather for a location',
inputSchema: z.object({
location: z.string().describe('City and country, e.g. "Tokyo, Japan"'),
units: z.enum(['celsius', 'fahrenheit']).default('celsius'),
}),
execute: async ({ location, units }) => ({ /* … */ }),
}),
If you migrated from v4/v5 and see parameters is not a valid option, rename to inputSchema.
4. Build & deploy
4.1 Vercel build succeeds but runtime throws "env var undefined"
Symptom
Deploy logs are green. Visiting the site, the chat hero returns 503 and logs show process.env.OPENAI_API_KEY is undefined.
Cause
.env.local is in .gitignore (as it should be) — it never reaches Vercel. You must also add the var in the dashboard.
Fix
- Vercel dashboard -> your project -> Settings -> Environment Variables.
- Add
OPENAI_API_KEYwith the same value. Scope it to Production, Preview, and Development as appropriate. - Redeploy (Deployments -> latest -> "..." -> Redeploy). Existing deployments cache env at build time; a fresh deploy is mandatory.
4.2 Edge function timeout on cold start
Symptom The first chat request after a deploy hangs for 8–15 seconds before streaming. Subsequent requests are snappy.
Cause Edge functions cold-start the runtime on first hit per region. Expected behaviour — not a bug.
Fix Options:
- Accept the trade-off. Cold starts are rare for an active landing page.
- Keep the function warm with a cron ping (
vercel.json->crons) that calls/api/chatwith a minimal payload every 5 minutes. - If cold starts are intolerable, switch
runtime: 'nodejs'(Node functions keep hotter on Vercel) at the cost of higher per-invocation latency.
4.3 React 19 / Next 16 incompatibility with a third-party library
Symptom
npm warn ERESOLVE overriding peer dependency
...
Error: Invalid hook call. Hooks can only be called inside the body of a function component.
or runtime crashes inside a library wrapper.
Cause
Some libraries still declare peer deps of react@^18. A handful have internal assumptions that break under React 19's new behaviour (automatic batching changes, stricter useInsertionEffect, removed forwardRef default export).
Fix
- Check the library's GitHub issues for "React 19" — most have an alpha/rc branch.
- If a maintained fork exists, switch. Otherwise override the peer:
// package.json "overrides": { "react": "19.2.4", "react-dom": "19.2.4" } - As a last resort, wrap the library in a dynamic import with
ssr: falseso it only runs on the client.
Known-good libraries in AgentKit: every @radix-ui/*, framer-motion@11, lucide-react@0.469+, @ai-sdk/*@2, zod@3.
4.4 Netlify / Cloudflare Pages deploy — chat route does not stream
Symptom Build passes. The chat route returns 200 but with empty body, or streams in one chunk at the end instead of token-by-token.
Cause
Vercel-specific streaming primitives are not automatically enabled on other hosts. The AI SDK's toUIMessageStreamResponse() relies on Web streams, which need the right adapter at each provider.
Fix
Netlify:
npm install -D @netlify/plugin-nextjs
Add to netlify.toml:
[[plugins]]
package = "@netlify/plugin-nextjs"
Redeploy.
Cloudflare Pages:
npm install -D @cloudflare/next-on-pages
Add to package.json:
"scripts": {
"pages:build": "npx @cloudflare/next-on-pages"
}
Build command in Cloudflare dashboard: npm run pages:build. Output directory: .vercel/output/static.
For either, confirm edge compatibility for any Node APIs you add to the chat route (see 3.3).
5. Components & TypeScript
5.1 Cannot find module '@/components/ai' or similar import error
Symptom
Module not found: Can't resolve '@/components/ai'
Cause
Either the @/* path alias is not configured in your editor, or you are using a relative path from a file that moved.
Fix
tsconfig.jsonalready declares"paths": { "@/*": ["./src/*"] }. Restart the TS server in your editor (VS Code: Cmd+Shift+P -> "TypeScript: Restart TS Server").- Import from the barrel files to avoid deep paths:
import { MessageBubble, ChatInput } from '@/components/ai' import { Button, Dialog } from '@/components/ui' - If you still see the error after restart, check that
vitest.config.tsalso has the alias (it does) — otherwise tests will fail with the same message.
5.2 Type 'Date' is not assignable to type 'string'
Symptom
Type 'Date' is not assignable to type 'string'.
on prop `timestamp` of <MessageBubble />
Cause
Most AgentKit timestamp props accept Date | string but a handful are string-only for SSR stability (serialising a Date across the server/client boundary loses timezone nuances).
Fix Always pass ISO strings:
<MessageBubble timestamp={new Date().toISOString()} />
// or, for stable server rendering:
<MessageBubble timestamp="2026-04-20T09:00:00.000Z" />
If you need a Date inside the component, the component parses it for you.
5.3 Radix Tooltip / Popover / Select does not appear in tests
Symptom
expect(screen.getByText('Label text')).toBeInTheDocument() fails even though the code clearly renders the tooltip.
Cause
jsdom does not ship ResizeObserver, IntersectionObserver, or scrollIntoView. Radix uses these internally to position floating UI — without them, the content never mounts.
Fix
Copy the beforeAll polyfill block from tests/tooltip.test.tsx (reproduced in 1.4) into the offending test.
5.4 Functions cannot be passed directly to Client Components
Symptom
Error: Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with "use server".
Cause
A server component is passing a callback (onClick, onSubmit, render, formatter, etc.) into a client component. Next.js 16 enforces this strictly — functions are not serialisable.
Fix
Mark the parent component as a client component by adding 'use client' at the top of the file:
'use client'
import { MessageThread } from '@/components/ai'
export default function Page() {
return <MessageThread formatter={(m) => m.text.toUpperCase()} />
}
If the parent must stay server-rendered (for metadata, data fetching, etc.), wrap the client-only piece in its own 'use client' file and import that from the server component.
6. Styling & theming
6.1 Theme switcher does not persist across refreshes
Symptom You pick "Bright" theme, refresh, and land back on "Editorial dark".
Cause (in likely order)
- localStorage is disabled (private browsing mode, third-party cookie blocker).
- The localStorage key is wrong —
src/components/theme-switcher.tsxwrites toagentkit-themeand/public/theme-init.jsreads the same key. A fork that changed one without the other breaks persistence. /public/theme-init.jsis not loading (see 2.4).
Fix
- Confirm in DevTools -> Application -> Local Storage that
agentkit-themehas the expected value after clicking. - If the value is written but not applied: open Network tab, reload, and confirm
theme-init.jsreturns 200. - If you forked and renamed the key, update both files (
theme-switcher.tsxandtheme-init.js) consistently.
6.2 Custom font flashes (FOUT) on first load
Symptom Headings render in the fallback system font for 200–500 ms, then snap to Instrument Serif / Inter / JetBrains Mono.
Cause
@fontsource/* CSS imports declare the font, but the browser still has to fetch the woff2 over the network before it can substitute. AgentKit preloads the woff2 explicitly for this reason — if the preloads are missing, the flash returns.
Fix
Open src/app/layout.tsx and confirm the four <link rel="preload"> tags are present in <head>:
<link rel="preload" href={interLatin} as="font" type="font/woff2" crossOrigin="anonymous" fetchPriority="high" />
<link rel="preload" href={instrumentItalic} as="font" type="font/woff2" crossOrigin="anonymous" fetchPriority="high" />
<link rel="preload" href={instrumentRegular} as="font" type="font/woff2" crossOrigin="anonymous" fetchPriority="high" />
<link rel="preload" href={jetbrainsLatin} as="font" type="font/woff2" crossOrigin="anonymous" fetchPriority="high" />
The ?url import form is required — it resolves to the hashed asset URL Next uses in production.
Also confirm the corresponding @import "@fontsource-variable/…" lines are in src/app/globals.css. Both the CSS import and the preload are needed: the CSS registers the @font-face; the preload ensures the byte download beats the first paint.
6.3 Ambient gradient / grain blocks clicks on the page
Symptom Clicking anywhere does nothing. Hovering shows no cursor change. The page looks correct.
Cause
The .ambient-bg div in layout.tsx covers the full viewport. It has pointer-events: none set in globals.css — if that rule is missing, or a descendant has pointer-events: auto, the overlay eats all clicks.
Fix
- In DevTools, inspect any element. If
.ambient-bgis at the top of the hit-testing tree and haspointer-events: auto, that is the culprit. - Confirm
globals.cssstill contains:.ambient-bg { pointer-events: none; } - Check for any other element with a higher
z-indexthan 10 (the content wrapper usesz-10). If a decorative layer hasz-20or higher withoutpointer-events: none, it will block clicks.
7. Tests
7.1 Newly added tests are flaky — pass locally, fail on CI (or vice versa)
Symptom Tests involving Toast, Dialog, Popover, or any animated Radix component intermittently assert against "not yet rendered" / "already unmounted" state.
Cause
Radix animations rely on setTimeout / requestAnimationFrame. When you mix user.click() with real timers, the test races the animation.
Fix
Use userEvent.setup() with fake timers, advance them explicitly, and clean up:
import { afterEach, beforeEach, vi } from 'vitest'
beforeEach(() => vi.useFakeTimers())
afterEach(() => {
vi.useRealTimers()
vi.clearAllTimers()
})
it('opens the dialog', async () => {
const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime })
render(<Sample />)
await user.click(screen.getByRole('button', { name: 'Open' }))
expect(await screen.findByRole('dialog')).toBeInTheDocument()
})
Always afterEach(() => cleanup()) — @testing-library/react v16 does not auto-clean.
7.2 Vitest cannot find a component I just added
Symptom
Error: Failed to resolve import "@/components/ai/my-new-component" from "tests/my-new-component.test.tsx"
Cause (in likely order)
- Vitest was started before the file existed, and file-watching did not pick up the new path.
- The file is outside
src/— the@/*alias only resolves under./src/. - You exported the component but did not add it to
src/components/ai/index.tsand are importing from the barrel.
Fix
- Stop Vitest (
q), restart (npm run test:watch). - Confirm the file is under
src/components/ai/orsrc/components/ui/. - If importing from the barrel, add a re-export:
Otherwise, import the file directly in the test:// src/components/ai/index.ts export { MyNewComponent } from './my-new-component'import { MyNewComponent } from '@/components/ai/my-new-component'
Still stuck?
If none of the above matches your symptom:
- Search this file again — the exact error message is often in the Symptom block verbatim.
- Check
docs/GETTING-STARTED.mdfor install basics,docs/DEPLOY.mdfor host-specific notes, anddocs/CUSTOMIZE.mdfor theming and component wiring. - Reply to your purchase receipt with:
- Node version (
node -v) - Exact error message (copy-paste, not a screenshot of a screenshot)
- The file and line where it fires
- What you changed just before it started
- Node version (
We usually reply within one business day.