Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
938c003
chore(test): add shared helpers and integration scaffold
marslavish Apr 27, 2026
0dbeba8
test: refactor unit tests to shared helpers
marslavish Apr 27, 2026
a1fae59
test(agent): add sse parser tests
marslavish Apr 27, 2026
536d30e
docs: add roadmap
marslavish Apr 27, 2026
92f701b
feat(agent): add pausable tools and run store
marslavish Apr 27, 2026
1adfadb
docs: update roadmap for phase 1.1 progress
marslavish Apr 27, 2026
08fc6c1
feat(agent): message-log pause/resume + run handle
marslavish May 4, 2026
9ad2355
docs: align with message-log resume design
marslavish May 4, 2026
75706cf
refactor(agent): export parseSSEStream
marslavish May 4, 2026
fac47ee
fix(openai): prefer native fetch over cross-fetch
marslavish May 4, 2026
3f5edbd
feat(react): add useChat hook package
marslavish May 4, 2026
c6605dc
feat(app): add nextjs chat demo
marslavish May 4, 2026
d02ba4f
docs: roadmap progress for phase 1.3
marslavish May 4, 2026
4b26cbd
chore: drop cross-fetch, expose source exports
marslavish May 4, 2026
a0155e4
feat(agent): maxSteps + lookup decision by id
marslavish May 4, 2026
e482c72
feat(react): lookup pending decision by id
marslavish May 4, 2026
4780a2e
style(react): reformat use-chat
marslavish May 10, 2026
afbe39d
style(demo): reformat imports and indentation
marslavish May 10, 2026
7587403
feat(agentic-kit): add injectDeferralResults helper
marslavish May 11, 2026
fc1a667
feat(react): expand useChat with streaming and tool state
marslavish May 11, 2026
e6b3494
feat(demo): adopt expanded useChat surface
marslavish May 11, 2026
d023c2f
docs: expand package READMEs
marslavish May 12, 2026
5d3a73b
chore: drop roadmap and integration scaffolding
marslavish May 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,52 @@ const message = await complete(model!, {
console.log(message.content);
```

## Consuming from webpack / Next.js

The packages publish ESM with `.js`-suffixed relative imports (e.g.
`from './foo.js'`), which is the correct ESM-with-TS pattern. Webpack does not
auto-rewrite `.js` → `.ts` when reading TypeScript sources directly (e.g. when
linking the workspace from `apps/`), so add an `extensionAlias` to your
`next.config.mjs`:

```js
// next.config.mjs
export default {
transpilePackages: [
'agentic-kit',
'@agentic-kit/agent',
'@agentic-kit/react',
'@agentic-kit/openai',
'@agentic-kit/anthropic',
'@agentic-kit/ollama',
],
webpack: (config) => {
config.resolve.extensionAlias = {
'.js': ['.ts', '.tsx', '.js'],
'.mjs': ['.mts', '.mjs'],
};
return config;
},
};
```

Once a published artifact is installed (`npm install agentic-kit`), the
compiled `dist/` is what resolves and no `extensionAlias` is required — this
workaround only matters when reading TypeScript source through webpack.

Vite, Bun, and esbuild handle `.js` → `.ts` natively. Vite users who want to
consume the workspace TypeScript source via the package `"source"` condition
can opt in with:

```js
// vite.config.ts
export default {
resolve: {
conditions: ['source', 'import', 'module', 'browser', 'default'],
},
};
```

## Contributing

See individual package READMEs for docs and local dev instructions.
Expand Down
9 changes: 9 additions & 0 deletions apps/nextjs-chat-demo/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Either OPENAI_* or LLM_* (the LLM_* convention is shared with the dashboard).
# OPENAI_* takes precedence if both are set.
OPENAI_API_KEY=sk-...
# OPENAI_BASE_URL=https://api.openai.com/v1
# OPENAI_MODEL=gpt-5.4-mini

# LLM_API_KEY=...
# LLM_BASE_URL=https://api.deepseek.com/v1
# LLM_MODEL=deepseek-chat
38 changes: 38 additions & 0 deletions apps/nextjs-chat-demo/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# dependencies
node_modules
.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions

# testing
coverage

# next.js
.next/
out/
build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

# env files
.env
.env*.local

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts
59 changes: 59 additions & 0 deletions apps/nextjs-chat-demo/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# nextjs-chat-demo

A Next.js 15 demo proving `agentic-kit` can replace `@ai-sdk/react` for the
dashboard chatbot. Demonstrates:

- streaming chat via `useChat` from `@agentic-kit/react`
- a plain server tool (`get_current_time`)
- a **pausable** server tool (`send_email`) — model proposes args, the UI shows
Allow / Deny, the answer is fed back in via `respondWithDecision`, and the
agent resumes server-side.

## Run

```bash
# from monorepo root
pnpm install

# point the demo at OpenAI
export OPENAI_API_KEY=sk-...

pnpm --filter nextjs-chat-demo dev
# open http://localhost:3001
```

## AI SDK → agentic-kit migration map

| Dashboard (AI SDK) | This demo (agentic-kit) |
| -------------------------------------------------- | -------------------------------------------------------- |
| `streamText` + `convertToModelMessages` | `Agent.prompt()` / `continue()` + `handle.toResponse()` |
| `tool({ needsApproval: true })` | `AgentTool.decision` JSON Schema |
| `addToolApprovalResponse({ id, approved })` | `respondWithDecision(toolCallId, value)` (auto re-POST) |
| `result.toUIMessageStreamResponse()` | `handle.toResponse()` |
| `useChat` from `@ai-sdk/react` | `useChat` from `@agentic-kit/react` |

## Out of scope

This demo deliberately does not port:

- mentions / @-suggestions
- multi-slot queue (`messageQueue`, `isFullySettled`, `sendAutomaticallyWhen`)
- task queue UI (`plan_tasks`, `complete_task`, `approve_previous_tool`)
- ask vs agent modes, settings menu
- FAB + portal placement
- history dropdown

These are dashboard UI sugar that sits on top of the SDK, not in it.

## Workspace dep wiring

`@agentic-kit/react`, `@agentic-kit/agent`, and `agentic-kit` packages declare
build outputs (`main: index.js`, `module: esm/index.js`) that don't exist on
disk in development. To consume them without a build step the demo combines:

- `tsconfig.json` `paths` map to `../../packages/*/src/index.ts`
- `next.config.mjs` `transpilePackages` so SWC compiles the TS source
- `experimental.externalDir` so Next is happy reading from outside the app dir

See [`PLAN.md`](./PLAN.md) for the full implementation plan and
[`GAPS.md`](./GAPS.md) for everything that felt rough to wire up.
28 changes: 28 additions & 0 deletions apps/nextjs-chat-demo/next.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
transpilePackages: [
'agentic-kit',
'@agentic-kit/agent',
'@agentic-kit/react',
'@agentic-kit/openai',
'@agentic-kit/anthropic',
'@agentic-kit/ollama',
],
experimental: {
externalDir: true,
},
webpack: (config) => {
// The agentic-kit packages are TS source with .js extension imports
// (`from './foo.js'`). webpack doesn't auto-rewrite those to .ts; we
// teach it to fall back to the .ts source.
config.resolve.extensionAlias = {
...(config.resolve.extensionAlias ?? {}),
'.js': ['.ts', '.tsx', '.js'],
'.mjs': ['.mts', '.mjs'],
};
return config;
},
};

export default nextConfig;
30 changes: 30 additions & 0 deletions apps/nextjs-chat-demo/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"name": "nextjs-chat-demo",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "next dev --port 3001",
"start": "next start --port 3001",
"lint": "next lint"
},
"dependencies": {
"@agentic-kit/agent": "workspace:*",
"@agentic-kit/openai": "workspace:*",
"@agentic-kit/react": "workspace:*",
"agentic-kit": "workspace:*",
"clsx": "^2.1.1",
"next": "15.0.4",
"react": "19.0.0",
"react-dom": "19.0.0",
"tailwind-merge": "^3.5.0"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.1.18",
"@types/node": "^22.10.2",
"@types/react": "19.0.0",
"@types/react-dom": "19.0.0",
"tailwindcss": "^4.1.18",
"typescript": "^5.7.2"
}
}
5 changes: 5 additions & 0 deletions apps/nextjs-chat-demo/postcss.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export default {
plugins: {
'@tailwindcss/postcss': {},
},
};
94 changes: 94 additions & 0 deletions apps/nextjs-chat-demo/src/app/api/chat/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { Agent } from '@agentic-kit/agent';
import { OpenAIAdapter } from '@agentic-kit/openai';
import type { Message } from 'agentic-kit';

import { tools } from '@/lib/tools';

export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';

const SYSTEM_PROMPT = [
'You are a friendly assistant in a chat-app demo.',
'You have two tools available:',
'- get_current_time(timezone?): returns the current time in the requested IANA timezone.',
'- send_email(to, subject, body): drafts an email. The user must approve before it is sent.',
'When the user asks for the current time anywhere, call get_current_time.',
'When the user asks you to send an email, call send_email exactly once and wait for the user decision.',
'Keep replies short.',
].join('\n');

interface RequestBody {
messages: Message[];
}

function lastMessageHasPendingDecision(messages: Message[]): boolean {
const last = messages[messages.length - 1];
if (!last || last.role !== 'assistant') return false;
const completedToolCallIds = new Set(
messages
.filter((m): m is Extract<Message, { role: 'toolResult' }> => m.role === 'toolResult')
.map((m) => m.toolCallId)
);
return last.content.some(
(block) =>
block.type === 'toolCall' &&
!completedToolCallIds.has(block.id) &&
'decision' in block &&
block.decision !== undefined
);
}

export async function POST(req: Request): Promise<Response> {
const apiKey = process.env.OPENAI_API_KEY ?? process.env.LLM_API_KEY;
const baseUrl =
process.env.OPENAI_BASE_URL ?? process.env.LLM_BASE_URL ?? 'https://api.openai.com/v1';
const modelId = process.env.OPENAI_MODEL ?? process.env.LLM_MODEL ?? 'gpt-5.4-mini';

if (!apiKey) {
return new Response('OPENAI_API_KEY (or LLM_API_KEY) is not set on the server', {
status: 500,
});
}

let body: RequestBody;
try {
body = (await req.json()) as RequestBody;
} catch {
return new Response('Invalid JSON body', { status: 400 });
}

const messages = Array.isArray(body.messages) ? body.messages : [];
if (messages.length === 0) {
return new Response('Empty messages', { status: 400 });
}

const adapter = new OpenAIAdapter({ apiKey, baseUrl });
const model = adapter.createModel(modelId);

const agent = new Agent({
initialState: { model, tools, systemPrompt: SYSTEM_PROMPT },
streamFn: (m, ctx, opts) => adapter.stream(m, ctx, opts),
maxSteps: 5,
});

const isResume = lastMessageHasPendingDecision(messages);

if (isResume) {
agent.replaceMessages(messages);
try {
const handle = agent.continue();
return handle.toResponse();
} catch (err) {
return new Response(`continue() failed: ${(err as Error).message}`, { status: 400 });
}
}

const last = messages[messages.length - 1];
if (last.role !== 'user') {
return new Response('Last message must be a user message when not resuming', { status: 400 });
}

agent.replaceMessages(messages.slice(0, -1));
const handle = agent.prompt(last);
return handle.toResponse();
}
9 changes: 9 additions & 0 deletions apps/nextjs-chat-demo/src/app/globals.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
@import "tailwindcss";

:root {
color-scheme: light dark;
}

html, body {
height: 100%;
}
18 changes: 18 additions & 0 deletions apps/nextjs-chat-demo/src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import './globals.css';

import type { ReactNode } from 'react';

export const metadata = {
title: 'agentic-kit chat demo',
description: 'Next.js demo proving agentic-kit can replace AI SDK for the dashboard chatbot.',
};

export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="en">
<body className="bg-zinc-50 text-zinc-900 dark:bg-zinc-950 dark:text-zinc-100">
{children}
</body>
</html>
);
}
9 changes: 9 additions & 0 deletions apps/nextjs-chat-demo/src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { ChatPanel } from '@/components/chat-panel';

export default function Page() {
return (
<main className="mx-auto flex h-dvh max-w-3xl flex-col p-4">
<ChatPanel />
</main>
);
}
Loading
Loading