Troubleshooting

Troubleshooting

24 known issues with Symptom / Cause / Fix.

Sourcedocs/TROUBLESHOOTING.md

Exhaustive 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

  1. Install & setup
  2. Runtime & dev server
  3. AI SDK & chat route
  4. Build & deploy
  5. Components & TypeScript
  6. Styling & theming
  7. Tests
  8. 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

  1. Open DevTools -> Network tab, reload, filter for theme-init.js. It should 200 with type application/javascript.
  2. If it 404s: confirm public/theme-init.js exists (ls public/theme-init.js) and that your deploy is serving the public/ folder. On Vercel this is automatic; on other hosts, check your static asset config.
  3. If it 200s but the flash persists: confirm src/app/layout.tsx still has <script src="/theme-init.js" /> inside <head> (not <body>), and that it is NOT marked defer or async.

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:

  1. You added a new CSS var but did not add it under every [data-theme="…"] block in globals.css.
  2. You mixed Tailwind's short-form text-brand (which expects a --color-brand token 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-brand to work, register the token in the @theme block (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:

  1. The file is named .env or .env.development instead of .env.local.
  2. You created .env.local after starting next dev — env vars are read once at boot.
  3. The key line has inline comments or quotes: OPENAI_API_KEY="sk-..." # my key breaks parsing in some shells.
  4. On Vercel / other hosts, the variable is in .env.local only 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.ts when 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)

  1. Tool description is vague — the model picks tools by description, not by name.
  2. inputSchema is not a Zod object, or is a plain JSON Schema. AI SDK v6 expects Zod.
  3. The model is one that does not support tool calls (e.g. gpt-3.5-turbo-instruct). AgentKit defaults to gpt-4o-mini, which supports them.
  4. You are calling tool({ parameters }) (old API) instead of tool({ 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

  1. Vercel dashboard -> your project -> Settings -> Environment Variables.
  2. Add OPENAI_API_KEY with the same value. Scope it to Production, Preview, and Development as appropriate.
  3. 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/chat with 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

  1. Check the library's GitHub issues for "React 19" — most have an alpha/rc branch.
  2. If a maintained fork exists, switch. Otherwise override the peer:
    // package.json
    "overrides": {
      "react": "19.2.4",
      "react-dom": "19.2.4"
    }
    
  3. As a last resort, wrap the library in a dynamic import with ssr: false so 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.json already 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.ts also 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)

  1. localStorage is disabled (private browsing mode, third-party cookie blocker).
  2. The localStorage key is wrong — src/components/theme-switcher.tsx writes to agentkit-theme and /public/theme-init.js reads the same key. A fork that changed one without the other breaks persistence.
  3. /public/theme-init.js is not loading (see 2.4).

Fix

  1. Confirm in DevTools -> Application -> Local Storage that agentkit-theme has the expected value after clicking.
  2. If the value is written but not applied: open Network tab, reload, and confirm theme-init.js returns 200.
  3. If you forked and renamed the key, update both files (theme-switcher.tsx and theme-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

  1. In DevTools, inspect any element. If .ambient-bg is at the top of the hit-testing tree and has pointer-events: auto, that is the culprit.
  2. Confirm globals.css still contains:
    .ambient-bg { pointer-events: none; }
    
  3. Check for any other element with a higher z-index than 10 (the content wrapper uses z-10). If a decorative layer has z-20 or higher without pointer-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)

  1. Vitest was started before the file existed, and file-watching did not pick up the new path.
  2. The file is outside src/ — the @/* alias only resolves under ./src/.
  3. You exported the component but did not add it to src/components/ai/index.ts and are importing from the barrel.

Fix

  1. Stop Vitest (q), restart (npm run test:watch).
  2. Confirm the file is under src/components/ai/ or src/components/ui/.
  3. If importing from the barrel, add a re-export:
    // src/components/ai/index.ts
    export { MyNewComponent } from './my-new-component'
    
    Otherwise, import the file directly in the test:
    import { MyNewComponent } from '@/components/ai/my-new-component'
    

Still stuck?

If none of the above matches your symptom:

  1. Search this file again — the exact error message is often in the Symptom block verbatim.
  2. Check docs/GETTING-STARTED.md for install basics, docs/DEPLOY.md for host-specific notes, and docs/CUSTOMIZE.md for theming and component wiring.
  3. 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

We usually reply within one business day.