Skip to content

Commit e176abd

Browse files
authored
Merge pull request #79 from sidclawhq/feat/ai-sdk-v7-migration
feat: migrate demo-atlas + vercel-ai example to AI SDK v7
2 parents caa6dd6 + 80cab4b commit e176abd

7 files changed

Lines changed: 213 additions & 331 deletions

File tree

apps/demo/package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,18 @@
1212
"test": "echo 'no tests yet'"
1313
},
1414
"dependencies": {
15-
"@ai-sdk/anthropic": "^1.2.0",
15+
"@ai-sdk/anthropic": "^4.0.23",
16+
"@ai-sdk/react": "^4.0.44",
1617
"@sidclaw/sdk": "*",
1718
"@sidclaw/shared": "*",
18-
"ai": "^4.3.0",
19+
"ai": "^7.0.41",
1920
"lucide-react": "^1.27.0",
2021
"next": "15.5.20",
2122
"react": "19.2.8",
2223
"react-dom": "19.2.8",
2324
"react-markdown": "^10.1.0",
2425
"sonner": "^2.0.7",
25-
"zod": "^3.25.67"
26+
"zod": "^4.3.6"
2627
},
2728
"devDependencies": {
2829
"@eslint/eslintrc": "^3.3.6",

apps/demo/src/app/api/chat/route.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { streamText } from 'ai';
1+
import { streamText, convertToModelMessages, stepCountIs, type UIMessage } from 'ai';
22
import { anthropic } from '@ai-sdk/anthropic';
33
import { AgentIdentityClient } from '@sidclaw/sdk';
44
import { getOrCreateDemoSession } from '@/lib/demo-session';
@@ -34,7 +34,7 @@ export async function POST(request: Request) {
3434
const tools = {
3535
search_knowledge_base: {
3636
description: 'Search the Atlas Financial internal knowledge base for policy documents, FAQs, and guides. Use this for general questions about refunds, transfers, fees, security.',
37-
parameters: z.object({
37+
inputSchema: z.object({
3838
query: z.string().describe('The search query'),
3939
}),
4040
execute: async ({ query }: { query: string }) => {
@@ -61,7 +61,7 @@ export async function POST(request: Request) {
6161

6262
lookup_account: {
6363
description: 'Look up a customer account by account ID. Returns account details including name, balance, type, and support tier.',
64-
parameters: z.object({
64+
inputSchema: z.object({
6565
account_id: z.string().describe('The account ID (e.g., A-1234)'),
6666
}),
6767
execute: async ({ account_id }: { account_id: string }) => {
@@ -88,7 +88,7 @@ export async function POST(request: Request) {
8888

8989
send_email: {
9090
description: 'Send an email to a customer. Use this for follow-ups, notifications, or responses. Requires human approval before sending.',
91-
parameters: z.object({
91+
inputSchema: z.object({
9292
to: z.string().describe('Customer email address'),
9393
subject: z.string().describe('Email subject line'),
9494
body: z.string().describe('Email body content'),
@@ -117,7 +117,7 @@ export async function POST(request: Request) {
117117

118118
update_case: {
119119
description: 'Update a support case with new notes or status changes. Requires human approval for modifications.',
120-
parameters: z.object({
120+
inputSchema: z.object({
121121
case_id: z.string().describe('The case ID (e.g., C-5678)'),
122122
notes: z.string().describe('Notes to add to the case'),
123123
}),
@@ -145,7 +145,7 @@ export async function POST(request: Request) {
145145

146146
export_customer_data: {
147147
description: 'Export customer data to a file. This is typically blocked by policy.',
148-
parameters: z.object({
148+
inputSchema: z.object({
149149
format: z.string().describe('Export format (csv, json)'),
150150
scope: z.string().describe('What data to export'),
151151
}),
@@ -169,7 +169,7 @@ export async function POST(request: Request) {
169169

170170
close_account: {
171171
description: 'Close a customer account. This is a high-risk action typically blocked by policy.',
172-
parameters: z.object({
172+
inputSchema: z.object({
173173
account_id: z.string().describe('The account ID to close'),
174174
reason: z.string().describe('Reason for closure'),
175175
}),
@@ -214,10 +214,10 @@ CRITICAL RULES:
214214
- For case references, use C-5678 as the default.
215215
216216
You are demonstrating SidClaw's governance platform. The governance decisions you encounter (allow, approval_required, deny) are being made in real-time by the SidClaw policy engine based on actual policy rules configured for Atlas Financial. It is essential that every action goes through the tools so the governance trace appears on the right panel.`,
217-
messages,
217+
messages: await convertToModelMessages(messages as UIMessage[]),
218218
tools,
219-
maxSteps: 5,
219+
stopWhen: stepCountIs(5),
220220
});
221221

222-
return result.toDataStreamResponse();
222+
return result.toUIMessageStreamResponse();
223223
}

apps/demo/src/components/ChatInterface.tsx

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,21 @@
11
'use client';
22

3-
import { useChat } from 'ai/react';
3+
import { useChat } from '@ai-sdk/react';
4+
import { DefaultChatTransport, type UIMessage } from 'ai';
45
import { ChatMessage } from './ChatMessage';
56
import { ChatInput } from './ChatInput';
67
import { SuggestedPrompts } from './SuggestedPrompts';
7-
import { useRef, useEffect } from 'react';
8+
import { useRef, useEffect, useMemo, useState } from 'react';
89
import type { ApprovalNotification } from '@/app/page';
910

11+
/** Concatenate the text parts of a UIMessage (AI SDK v5+ message shape). */
12+
function messageText(message: UIMessage): string {
13+
return message.parts
14+
.filter((part): part is Extract<typeof part, { type: 'text' }> => part.type === 'text')
15+
.map((part) => part.text)
16+
.join('');
17+
}
18+
1019
interface ChatInterfaceProps {
1120
sessionId: string;
1221
agentId: string;
@@ -22,10 +31,23 @@ const OP_LABELS: Record<string, string> = {
2231
};
2332

2433
export function ChatInterface({ sessionId, agentId, apiKey, notifications = [] }: ChatInterfaceProps) {
25-
const { messages, input, handleInputChange, handleSubmit, isLoading, append } = useChat({
26-
api: '/api/chat',
27-
body: { sessionId, agentId, apiKey },
28-
});
34+
// v5+ useChat no longer manages input state or accepts api/body directly —
35+
// extra request fields travel via the transport, input state is ours.
36+
const transport = useMemo(
37+
() => new DefaultChatTransport({ api: '/api/chat', body: { sessionId, agentId, apiKey } }),
38+
[sessionId, agentId, apiKey],
39+
);
40+
const { messages, sendMessage, status } = useChat({ transport });
41+
const [input, setInput] = useState('');
42+
const isLoading = status === 'submitted' || status === 'streaming';
43+
44+
const handleSubmit = (e: React.FormEvent) => {
45+
e.preventDefault();
46+
const text = input.trim();
47+
if (!text || isLoading) return;
48+
sendMessage({ text });
49+
setInput('');
50+
};
2951

3052
const messagesEndRef = useRef<HTMLDivElement>(null);
3153

@@ -34,7 +56,7 @@ export function ChatInterface({ sessionId, agentId, apiKey, notifications = [] }
3456
}, [messages, notifications]);
3557

3658
const handleSuggestedPrompt = (prompt: string) => {
37-
append({ role: 'user', content: prompt });
59+
sendMessage({ text: prompt });
3860
};
3961

4062
return (
@@ -57,7 +79,7 @@ export function ChatInterface({ sessionId, agentId, apiKey, notifications = [] }
5779
</div>
5880
)}
5981
{messages.map((message) => (
60-
<ChatMessage key={message.id} message={message} />
82+
<ChatMessage key={message.id} message={{ role: message.role, content: messageText(message) }} />
6183
))}
6284
{isLoading && (
6385
<div className="flex items-center gap-2 text-base text-[#71717A]">
@@ -108,7 +130,7 @@ export function ChatInterface({ sessionId, agentId, apiKey, notifications = [] }
108130
{/* Input */}
109131
<ChatInput
110132
input={input}
111-
onChange={handleInputChange}
133+
onChange={(e) => setInput(e.target.value)}
112134
onSubmit={handleSubmit}
113135
isLoading={isLoading}
114136
/>

examples/vercel-ai-assistant/app/api/chat/route.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { streamText } from 'ai';
1+
import { streamText, convertToModelMessages, stepCountIs, type UIMessage } from 'ai';
22
import { openai } from '@ai-sdk/openai';
33
import { z } from 'zod';
44
import { AgentIdentityClient } from '@sidclaw/sdk';
@@ -17,7 +17,7 @@ const client = new AgentIdentityClient({
1717
const tools = {
1818
check_inventory: {
1919
description: 'Check product inventory levels',
20-
parameters: z.object({
20+
inputSchema: z.object({
2121
product: z.string().describe('Product name to check'),
2222
}),
2323
execute: async ({ product }: { product: string }) => {
@@ -35,7 +35,7 @@ const tools = {
3535
},
3636
send_notification: {
3737
description: 'Send a notification email to a customer',
38-
parameters: z.object({
38+
inputSchema: z.object({
3939
to: z.string().describe('Recipient email address'),
4040
message: z.string().describe('Notification message'),
4141
}),
@@ -46,7 +46,7 @@ const tools = {
4646
},
4747
delete_records: {
4848
description: 'Delete all records for a customer (destructive operation)',
49-
parameters: z.object({
49+
inputSchema: z.object({
5050
customerId: z.string().describe('Customer ID whose records to delete'),
5151
}),
5252
execute: async ({ customerId }: { customerId: string }) => {
@@ -70,10 +70,10 @@ export async function POST(req: Request) {
7070
system: `You are a helpful assistant with access to governed tools.
7171
Some tools may be blocked by governance policies — if a tool call fails with a policy denial,
7272
explain to the user that the action was blocked and why. Be concise.`,
73-
messages,
73+
messages: await convertToModelMessages(messages as UIMessage[]),
7474
tools: governedTools,
75-
maxSteps: 3,
75+
stopWhen: stepCountIs(3),
7676
});
7777

78-
return result.toDataStreamResponse();
78+
return result.toUIMessageStreamResponse();
7979
}

examples/vercel-ai-assistant/app/page.tsx

Lines changed: 37 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
11
'use client';
22

3-
import { useChat } from 'ai/react';
3+
import { useChat } from '@ai-sdk/react';
4+
import { useState } from 'react';
45

56
export default function ChatPage() {
6-
const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat({
7-
api: '/api/chat',
8-
});
7+
const { messages, sendMessage, status } = useChat();
8+
const [input, setInput] = useState('');
9+
const isLoading = status === 'submitted' || status === 'streaming';
10+
const handleSubmit = (e: React.FormEvent) => {
11+
e.preventDefault();
12+
const text = input.trim();
13+
if (!text || isLoading) return;
14+
sendMessage({ text });
15+
setInput('');
16+
};
917

1018
return (
1119
<div className="mx-auto flex h-screen max-w-2xl flex-col p-4">
@@ -65,19 +73,30 @@ export default function ChatPage() {
6573
: 'bg-zinc-900 text-zinc-300 border border-zinc-800'
6674
}`}
6775
>
68-
<pre className="whitespace-pre-wrap font-sans">{message.content}</pre>
69-
{message.toolInvocations?.map((invocation, i) => (
70-
<div key={i} className="mt-2 rounded border border-zinc-700 bg-zinc-950 p-2 text-xs">
71-
<span className="font-mono text-zinc-500">tool: {invocation.toolName}</span>
72-
{'result' in invocation && (
73-
<pre className="mt-1 text-zinc-400 whitespace-pre-wrap">
74-
{typeof invocation.result === 'string'
75-
? invocation.result
76-
: JSON.stringify(invocation.result, null, 2)}
77-
</pre>
78-
)}
79-
</div>
80-
))}
76+
{message.parts.map((part, i) => {
77+
if (part.type === 'text') {
78+
return (
79+
<pre key={i} className="whitespace-pre-wrap font-sans">{part.text}</pre>
80+
);
81+
}
82+
if (part.type.startsWith('tool-') || part.type === 'dynamic-tool') {
83+
const toolName = part.type === 'dynamic-tool'
84+
? (part as { toolName: string }).toolName
85+
: part.type.slice('tool-'.length);
86+
const output = (part as { output?: unknown }).output;
87+
return (
88+
<div key={i} className="mt-2 rounded border border-zinc-700 bg-zinc-950 p-2 text-xs">
89+
<span className="font-mono text-zinc-500">tool: {toolName}</span>
90+
{output !== undefined && (
91+
<pre className="mt-1 text-zinc-400 whitespace-pre-wrap">
92+
{typeof output === 'string' ? output : JSON.stringify(output, null, 2)}
93+
</pre>
94+
)}
95+
</div>
96+
);
97+
}
98+
return null;
99+
})}
81100
</div>
82101
</div>
83102
))}
@@ -95,7 +114,7 @@ export default function ChatPage() {
95114
<form onSubmit={handleSubmit} className="flex gap-2 border-t border-zinc-800 pt-4">
96115
<input
97116
value={input}
98-
onChange={handleInputChange}
117+
onChange={(e) => setInput(e.target.value)}
99118
placeholder="Ask something..."
100119
className="flex-1 rounded border border-zinc-700 bg-zinc-900 px-4 py-2 text-sm text-zinc-100 placeholder-zinc-600 outline-none focus:border-zinc-500"
101120
disabled={isLoading}

examples/vercel-ai-assistant/package.json

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,21 @@
99
"start": "next start --port 3001"
1010
},
1111
"dependencies": {
12+
"@ai-sdk/openai": "^4.0.23",
13+
"@ai-sdk/react": "^4.0.44",
1214
"@sidclaw/sdk": "*",
13-
"ai": "^4.0.0",
14-
"@ai-sdk/openai": "^1.0.0",
15+
"ai": "^7.0.41",
1516
"next": "^15.0.0",
1617
"react": "^19.2.8",
1718
"react-dom": "^19.2.8",
18-
"zod": "^3.23.0"
19+
"zod": "^4.3.6"
1920
},
2021
"devDependencies": {
22+
"@tailwindcss/postcss": "^4.3.3",
2123
"@types/react": "^19.2.17",
2224
"@types/react-dom": "^19.0.0",
23-
"tailwindcss": "^4.0.0",
24-
"@tailwindcss/postcss": "^4.3.3",
2525
"postcss": "^8.5.24",
26+
"tailwindcss": "^4.0.0",
2627
"tsx": "^4.23.1",
2728
"typescript": "^5.9.0"
2829
}

0 commit comments

Comments
 (0)