Skip to content

Commit 79ba99f

Browse files
authored
feat(agent): add message metadata support when inferring UI messages (#10449)
## Background UI message metadata was not supported with agents. ## Summary Add `MESSAGE_METADATA` generic to `InferAgentUIMessage`. ## Manual Verification Ran `examples/next-openai` with http://localhost:3000/use-chat-message-metadata
1 parent bd129fb commit 79ba99f

File tree

7 files changed

+79
-18
lines changed

7 files changed

+79
-18
lines changed

.changeset/red-wasps-know.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'ai': patch
3+
---
4+
5+
feat(agent): add message metadata support when inferring UI messages

content/docs/07-reference/01-ai-sdk-core/16-tool-loop-agent.mdx

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,8 @@ The `stream()` method returns a `StreamTextResult` object (see [`streamText`](/d
285285

286286
Infers the UI message type for the given agent instance. Useful for type-safe UI and message exchanges.
287287

288+
#### Basic Example
289+
288290
```ts
289291
import { ToolLoopAgent, InferAgentUIMessage } from 'ai';
290292

@@ -296,6 +298,36 @@ const weatherAgent = new ToolLoopAgent({
296298
type WeatherAgentUIMessage = InferAgentUIMessage<typeof weatherAgent>;
297299
```
298300

301+
#### Example with Message Metadata
302+
303+
You can provide a second type argument to customize the metadata for each message. This is useful for tracking rich metadata returned by the agent (such as createdAt, tokens, finish reason, etc.).
304+
305+
```ts
306+
import { ToolLoopAgent, InferAgentUIMessage } from 'ai';
307+
import { z } from 'zod';
308+
309+
// Example schema for message metadata
310+
const exampleMetadataSchema = z.object({
311+
createdAt: z.number().optional(),
312+
model: z.string().optional(),
313+
totalTokens: z.number().optional(),
314+
finishReason: z.string().optional(),
315+
});
316+
type ExampleMetadata = z.infer<typeof exampleMetadataSchema>;
317+
318+
// Define agent as usual
319+
const metadataAgent = new ToolLoopAgent({
320+
model: 'openai/gpt-4o',
321+
// ...other options
322+
});
323+
324+
// Type-safe UI message type with custom metadata
325+
type MetadataAgentUIMessage = InferAgentUIMessage<
326+
typeof metadataAgent,
327+
ExampleMetadata
328+
>;
329+
```
330+
299331
## Examples
300332

301333
### Basic Agent with Tools
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import { openai } from '@ai-sdk/openai';
2+
import { ToolLoopAgent, InferAgentUIMessage } from 'ai';
3+
14
import { z } from 'zod';
25

36
export const exampleMetadataSchema = z.object({
@@ -9,3 +12,12 @@ export const exampleMetadataSchema = z.object({
912
});
1013

1114
export type ExampleMetadata = z.infer<typeof exampleMetadataSchema>;
15+
16+
export const openaiMetadataAgent = new ToolLoopAgent({
17+
model: openai('gpt-4o'),
18+
});
19+
20+
export type OpenAIMetadataMessage = InferAgentUIMessage<
21+
typeof openaiMetadataAgent,
22+
ExampleMetadata
23+
>;

examples/next-openai/app/api/use-chat-message-metadata/route.ts

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
1-
import { openai } from '@ai-sdk/openai';
2-
import { convertToModelMessages, streamText, UIMessage } from 'ai';
3-
import { ExampleMetadata } from './example-metadata-schema';
1+
import { createAgentUIStreamResponse, UIMessage } from 'ai';
2+
import {
3+
ExampleMetadata,
4+
openaiMetadataAgent,
5+
} from '@/agent/openai-metadata-agent';
46

57
export async function POST(req: Request) {
68
const { messages }: { messages: UIMessage[] } = await req.json();
79

8-
const result = streamText({
9-
model: openai('gpt-4o'),
10-
prompt: convertToModelMessages(messages),
11-
});
12-
13-
return result.toUIMessageStreamResponse({
10+
return createAgentUIStreamResponse({
11+
agent: openaiMetadataAgent,
12+
messages,
1413
messageMetadata: ({ part }): ExampleMetadata | undefined => {
1514
// send custom information to the client on start:
1615
if (part.type === 'start') {

examples/next-openai/app/use-chat-message-metadata/page.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import ChatInput from '@/components/chat-input';
44
import { useChat } from '@ai-sdk/react';
55
import { DefaultChatTransport, UIMessage } from 'ai';
6-
import { ExampleMetadata } from '../api/use-chat-message-metadata/example-metadata-schema';
6+
import { ExampleMetadata } from '@/agent/openai-metadata-agent';
77

88
type MyMessage = UIMessage<ExampleMetadata>;
99

@@ -16,7 +16,7 @@ export default function Chat() {
1616
});
1717

1818
return (
19-
<div className="flex flex-col w-full max-w-md py-24 mx-auto stretch">
19+
<div className="flex flex-col py-24 mx-auto w-full max-w-md stretch">
2020
{messages.map(message => (
2121
<div key={message.id} className="whitespace-pre-wrap">
2222
{message.role === 'user' ? 'User: ' : 'AI: '}
@@ -46,7 +46,7 @@ export default function Chat() {
4646
{status === 'submitted' && <div>Loading...</div>}
4747
<button
4848
type="button"
49-
className="px-4 py-2 mt-4 text-blue-500 border border-blue-500 rounded-md"
49+
className="px-4 py-2 mt-4 text-blue-500 rounded-md border border-blue-500"
5050
onClick={stop}
5151
>
5252
Stop
@@ -59,7 +59,7 @@ export default function Chat() {
5959
<div className="text-red-500">An error occurred.</div>
6060
<button
6161
type="button"
62-
className="px-4 py-2 mt-4 text-blue-500 border border-blue-500 rounded-md"
62+
className="px-4 py-2 mt-4 text-blue-500 rounded-md border border-blue-500"
6363
onClick={() => regenerate()}
6464
>
6565
Retry

packages/ai/src/agent/infer-agent-ui-message.test-d.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,14 @@ import { InferAgentUIMessage } from './infer-agent-ui-message';
1515

1616
describe('InferAgentUIMessage', () => {
1717
it('should not contain arbitrary static tools when no tools are provided', () => {
18-
const baseAgent = new ToolLoopAgent({
18+
const agent = new ToolLoopAgent({
1919
model: 'openai/gpt-4o',
2020
// no tools
2121
});
2222

23-
type Message = InferAgentUIMessage<typeof baseAgent>;
23+
type Message = InferAgentUIMessage<typeof agent>;
2424

25-
expectTypeOf<Message>().toMatchTypeOf<UIMessage<never, never, {}>>();
25+
expectTypeOf<Message>().toMatchTypeOf<UIMessage<unknown, never, {}>>();
2626

2727
type MessagePart = Message['parts'][number];
2828

@@ -38,4 +38,17 @@ describe('InferAgentUIMessage', () => {
3838
| StepStartUIPart
3939
>();
4040
});
41+
42+
it('should include metadata when provided', () => {
43+
const agent = new ToolLoopAgent({
44+
model: 'openai/gpt-4o',
45+
// no tools
46+
});
47+
48+
type Message = InferAgentUIMessage<typeof agent, { foo: string }>;
49+
50+
expectTypeOf<Message>().toMatchTypeOf<
51+
UIMessage<{ foo: string }, never, {}>
52+
>();
53+
});
4154
});

packages/ai/src/agent/infer-agent-ui-message.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import { InferAgentTools } from './infer-agent-tools';
44
/**
55
* Infer the UI message type of an agent.
66
*/
7-
export type InferAgentUIMessage<AGENT> = UIMessage<
8-
never,
7+
export type InferAgentUIMessage<AGENT, MESSAGE_METADATA = unknown> = UIMessage<
8+
MESSAGE_METADATA,
99
never,
1010
InferUITools<InferAgentTools<AGENT>>
1111
>;

0 commit comments

Comments
 (0)