Skip to content

Commit ed3672b

Browse files
committed
feat: enhance chat API and client-stream integration
- Updated POST handler in route.ts to dynamically fetch available agents and validate agentId. - Improved error handling for missing or invalid messages and agentId. - Refactored createAgentStreamResponse in client-stream-to-ai-sdk.ts to streamline response creation and added support for new options. - Deprecated createMastraStreamResponse in favor of createAgentStreamResponse for better clarity and usage. - Updated mastra-client.ts to export new types and functions for better integration.
1 parent 52403d2 commit ed3672b

3 files changed

Lines changed: 196 additions & 66 deletions

File tree

app/api/chat/route.ts

Lines changed: 62 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,64 @@
1-
import { mastra } from "../../../src/mastra";
2-
import { UIMessage, convertToModelMessages } from 'ai';
3-
import { createUIMessageStream, createUIMessageStreamResponse } from "ai";
4-
import { toAISdkFormat } from "@mastra/ai-sdk";
5-
import type { ChunkType, MastraModelOutput } from "@mastra/core/stream";
6-
import { RuntimeContext } from "@mastra/core/runtime-context";
7-
export const maxDuration = 30;
1+
import { mastra } from "@/src/mastra";
2+
import { createAgentStreamResponse } from "@/lib/client-stream-to-ai-sdk";
3+
import type { UIMessage } from "ai";
4+
5+
export const maxDuration = 60;
6+
7+
interface ChatRequestBody {
8+
messages: UIMessage[];
9+
agentId?: string;
10+
threadId?: string;
11+
resourceId?: string;
12+
memory?: {
13+
thread?: string | { id: string; resourceId?: string };
14+
resource?: string;
15+
options?: {
16+
lastMessages?: number;
17+
semanticRecall?: boolean;
18+
workingMemory?: { enabled?: boolean };
19+
};
20+
};
21+
maxSteps?: number;
22+
}
23+
824
export async function POST(req: Request) {
9-
const { messages }: {
10-
messages: UIMessage[];
11-
} = await req.json();
12-
const myAgent = mastra.getAgent("weatherAgent");
13-
const stream = await myAgent.stream(messages, { });
14-
const uiMessageStream = createUIMessageStream({
15-
16-
execute: async ({ writer }) => {
17-
const formatted = toAISdkFormat(stream, { from: "agent" })!;
18-
const runtimeContext = new RuntimeContext();
19-
// If the returned object is an async iterable, use for-await
20-
if (Symbol.asyncIterator in formatted) {
21-
for await (const part of formatted as AsyncIterable<any>) {
22-
writer.write(part);
23-
}
24-
} else if (typeof (formatted as any).getReader === "function") {
25-
// If it's a ReadableStream (browser), read via getReader()
26-
const reader = (formatted as ReadableStream<any>).getReader();
27-
try {
28-
while (true) {
29-
const { done, value } = await reader.read();
30-
if (done) break;
31-
writer.write(value);
32-
}
33-
} finally {
34-
reader.releaseLock?.();
35-
}
36-
}
37-
},
38-
});
39-
return createUIMessageStreamResponse({
40-
stream: uiMessageStream,
41-
});
25+
const body: ChatRequestBody = await req.json();
26+
27+
// Get available agents dynamically from mastra
28+
const agentsMap = await mastra.getAgents();
29+
const availableAgents = Object.keys(agentsMap);
30+
31+
// Use first available agent if none specified
32+
const agentId = body.agentId || availableAgents[0];
33+
34+
if (!agentId || !availableAgents.includes(agentId)) {
35+
return Response.json(
36+
{ error: `Invalid or missing agentId. Available: ${availableAgents.join(", ")}` },
37+
{ status: 400 }
38+
);
39+
}
40+
41+
if (!body.messages?.length) {
42+
return Response.json({ error: "messages required" }, { status: 400 });
43+
}
44+
45+
try {
46+
return await createAgentStreamResponse(mastra as Parameters<typeof createAgentStreamResponse>[0], agentId, body.messages, {
47+
threadId: body.threadId,
48+
resourceId: body.resourceId,
49+
memory: body.memory,
50+
maxSteps: body.maxSteps ?? 50,
51+
});
52+
} catch (error) {
53+
return Response.json(
54+
{ error: error instanceof Error ? error.message : "Stream failed" },
55+
{ status: 500 }
56+
);
57+
}
58+
}
59+
60+
export async function GET() {
61+
const agentsMap = await mastra.getAgents();
62+
const availableAgents = Object.keys(agentsMap);
63+
return Response.json({ agents: availableAgents, count: availableAgents.length });
4264
}

lib/client-stream-to-ai-sdk.ts

Lines changed: 131 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,135 @@
1-
import { createUIMessageStream } from "ai";
1+
import {
2+
createUIMessageStream,
3+
createUIMessageStreamResponse,
4+
} from "ai";
25
import { toAISdkFormat } from "@mastra/ai-sdk";
3-
import type { ChunkType, MastraModelOutput } from "@mastra/core/stream";
4-
5-
const response = await agent.stream({ messages: "Tell me a story" });
6-
7-
const chunkStream: ReadableStream<ChunkType> = new ReadableStream<ChunkType>({
8-
start(controller) {
9-
response
10-
.processDataStream({
11-
onChunk: async (chunk) => controller.enqueue(chunk as ChunkType),
12-
})
13-
.finally(() => controller.close());
14-
},
15-
});
16-
17-
const uiMessageStream = createUIMessageStream({
18-
execute: async ({ writer }) => {
19-
for await (const part of toAISdkFormat(
20-
chunkStream as unknown as MastraModelOutput,
21-
{ from: "agent" },
22-
)) {
23-
writer.write(part);
6+
import type { MastraModelOutput } from "@mastra/core/stream";
7+
8+
export interface StreamToAISdkOptions {
9+
agentId: string;
10+
messages: string | Array<{ role: string; content: string }>;
11+
threadId?: string;
12+
resourceId?: string;
13+
}
14+
15+
export interface AgentStreamOptions {
16+
format?: "aisdk" | "mastra";
17+
threadId?: string;
18+
resourceId?: string;
19+
memory?: {
20+
thread?: string | { id: string; resourceId?: string };
21+
resource?: string;
22+
options?: {
23+
lastMessages?: number;
24+
semanticRecall?: boolean;
25+
workingMemory?: { enabled?: boolean };
26+
};
27+
};
28+
maxSteps?: number;
29+
}
30+
31+
type MastraAgent = {
32+
stream: (
33+
messages: unknown,
34+
options?: {
35+
format?: string;
36+
threadId?: string;
37+
resourceId?: string;
38+
memory?: AgentStreamOptions["memory"];
39+
maxSteps?: number;
2440
}
25-
},
26-
});
41+
) => Promise<MastraModelOutput & { toUIMessageStreamResponse?: () => Response }>;
42+
};
43+
44+
type MastraInstance = {
45+
getAgent: (id: string) => MastraAgent;
46+
} & Record<string, any>;
47+
48+
/**
49+
* Creates a streaming Response for Next.js API routes using server-side Mastra agent.
50+
*
51+
* IMPORTANT: This should be used in API routes with the SERVER-SIDE mastra instance,
52+
* not the client SDK. The client SDK (MastraClient) is for frontend use only.
53+
*
54+
* @example
55+
* ```ts
56+
* // app/api/chat/route.ts
57+
* import { mastra } from "@/src/mastra";
58+
* import { createAgentStreamResponse } from "@/lib/client-stream-to-ai-sdk";
59+
*
60+
* export async function POST(req: Request) {
61+
* const { messages, agentId, threadId, resourceId, memory } = await req.json();
62+
* return createAgentStreamResponse(mastra, agentId, messages, {
63+
* threadId,
64+
* resourceId,
65+
* memory,
66+
* });
67+
* }
68+
* ```
69+
*
70+
* @see https://mastra.ai/docs/frameworks/agentic-uis/ai-sdk
71+
*/
72+
export async function createAgentStreamResponse(
73+
mastra: MastraInstance,
74+
agentId: string,
75+
messages: unknown,
76+
options?: AgentStreamOptions
77+
): Promise<Response> {
78+
const agent = mastra.getAgent(agentId);
79+
80+
const streamOptions = {
81+
format: options?.format ?? "aisdk",
82+
threadId: options?.threadId,
83+
resourceId: options?.resourceId,
84+
memory: options?.memory,
85+
maxSteps: options?.maxSteps,
86+
};
87+
88+
// Preferred: Use built-in AI SDK format
89+
if (streamOptions.format === "aisdk") {
90+
const stream = await agent.stream(messages, streamOptions);
91+
if (stream.toUIMessageStreamResponse) {
92+
return stream.toUIMessageStreamResponse();
93+
}
94+
}
95+
96+
// Fallback: Manual transformation with toAISdkFormat
97+
const stream = await agent.stream(messages, {
98+
threadId: options?.threadId,
99+
resourceId: options?.resourceId,
100+
memory: options?.memory,
101+
maxSteps: options?.maxSteps,
102+
});
103+
104+
const uiMessageStream = createUIMessageStream({
105+
execute: async ({ writer }) => {
106+
const aiSdkStream = toAISdkFormat(stream, { from: "agent" });
107+
const reader = aiSdkStream.getReader();
108+
try {
109+
while (true) {
110+
const { done, value } = await reader.read();
111+
if (done) break;
112+
writer.write(value);
113+
}
114+
} finally {
115+
reader.releaseLock();
116+
}
117+
},
118+
});
119+
120+
return createUIMessageStreamResponse({ stream: uiMessageStream });
121+
}
27122

28-
for await (const part of uiMessageStream) {
29-
console.log(part);
123+
/**
124+
* @deprecated Use createAgentStreamResponse with server-side mastra instance instead.
125+
* This export exists for backward compatibility only.
126+
*/
127+
export async function createMastraStreamResponse(
128+
_client: unknown,
129+
_options: StreamToAISdkOptions
130+
): Promise<Response> {
131+
throw new Error(
132+
"createMastraStreamResponse is deprecated. Use createAgentStreamResponse with " +
133+
"the server-side mastra instance instead. See: https://mastra.ai/docs/frameworks/agentic-uis/ai-sdk"
134+
);
30135
}

lib/mastra-client.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,6 @@ export const mastraClient = new MastraClient({
99
headers: {},
1010
credentials: "same-origin",
1111
});
12+
13+
export { createAgentStreamResponse } from "./client-stream-to-ai-sdk";
14+
export type { StreamToAISdkOptions } from "./client-stream-to-ai-sdk";

0 commit comments

Comments
 (0)