Skip to content

Commit be31e4c

Browse files
committed
chore: update dependencies and improve code structure
- Updated dependencies in package.json to latest versions for improved performance and security: - "@ai-sdk/google-vertex": "^3.0.86" - "@ai-sdk/openai": "^2.0.77" - "@ai-sdk/react": "^2.0.106" - "@inquirer/prompts": "^8.0.2" - "@mastra/ai-sdk": "^0.3.2" - "@next/mdx": "^16.0.7" - "@openrouter/ai-sdk-provider": "^1.3.0" - "ai": "^5.0.106" - "framer-motion": "^12.23.25" - "jose": "^6.1.3" - "motion": "^12.23.25" - "next": "^16.0.7" - "react": "^19.2.1" - "react-dom": "^19.2.1" - "shiki": "^3.19.0" - "streamdown": "^1.6.10" - "v0-sdk": "^0.15.2" - "@typescript-eslint/eslint-plugin": "^8.48.1" - "@typescript-eslint/parser": "^8.48.1" - "@vitest/coverage-v8": "^4.0.15" - "prettier": "^3.7.4" - "vitest": "^4.0.15" - Refactored client-stream-to-ai-sdk.ts to streamline the response handling and improve readability. - Removed deprecated Google AI model configurations from google.ts to simplify the codebase. - Updated the mastra index.ts to enhance observability configurations and added multiple chat routes for different agents. - Improved the weather-scorer.ts by updating import paths for better modularity. - Added a new tool-part-transform.ts file to robustly convert Mastra `data-tool-*` parts into `DynamicToolUIPart`. - Created a new Getting Started page in the documentation to assist users in setting up AgentStack. - Added comprehensive AI Elements URLs documentation for better navigation and understanding of available components and features.
1 parent 662cca8 commit be31e4c

18 files changed

Lines changed: 5321 additions & 4744 deletions

app/chat/components/agent-tools.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
ToolInput,
88
ToolOutput,
99
} from "@/src/components/ai-elements/tool"
10-
import type { ToolInvocationState } from "@/app/chat/providers/chat-context"
10+
import type { ToolInvocationState } from "../providers/chat-context"
1111
import type { DynamicToolUIPart, ToolUIPart } from "ai"
1212

1313
export interface AgentToolsProps {

app/chat/components/chat-messages.tsx

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ import {
5050
isToolOrDynamicToolUIPart,
5151
isFileUIPart,
5252
} from "ai"
53+
import { mapDataToolPartToDynamicToolPart } from "../helpers/tool-part-transform"
5354
import type { BundledLanguage } from "shiki"
5455
import { Button } from "@/ui/button"
5556

@@ -157,9 +158,27 @@ function MessageItem({
157158
}, [rawContent, isAssistant, showArtifacts])
158159

159160
const messageReasoning = message.parts?.find(isReasoningUIPart)
160-
const messageTools = message.parts?.filter(isToolOrDynamicToolUIPart) as
161-
| ToolInvocationState[]
162-
| undefined
161+
const messageTools = useMemo(() => {
162+
if (!message.parts || message.parts.length === 0) return undefined
163+
const tools: ToolInvocationState[] = []
164+
165+
for (const p of message.parts) {
166+
if (!p) continue
167+
if (isToolOrDynamicToolUIPart(p)) {
168+
tools.push(p as ToolInvocationState)
169+
continue
170+
}
171+
172+
if (typeof p.type === "string" && p.type.startsWith("data-tool-")) {
173+
const converted = mapDataToolPartToDynamicToolPart(p)
174+
if (converted) {
175+
tools.push(converted as ToolInvocationState)
176+
}
177+
}
178+
}
179+
180+
return tools.length > 0 ? tools : undefined
181+
}, [message.parts])
163182

164183
const fileParts = message.parts?.filter(isFileUIPart) as FileUIPart[] | undefined
165184
const imageParts = fileParts?.filter((f) => f.mediaType?.startsWith("image/"))
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
import type { DynamicToolUIPart } from "ai";
2+
3+
/**
4+
* Robustly convert a Mastra `data-tool-*` part into a `DynamicToolUIPart`.
5+
*
6+
* This is a best-effort mapper that tries to handle several shapes Mastra
7+
* might emit for nested tool parts. It extracts values from common fields,
8+
* supports nested payloads, and normalizes the state into the AI SDK `ToolUIPart` states.
9+
*
10+
* If the part does not look like a Mastra `data-tool-*` part, returns null.
11+
*/
12+
export function mapDataToolPartToDynamicToolPart(part: any): DynamicToolUIPart | null {
13+
if (!part || typeof part !== "object") return null;
14+
if (typeof part.type !== "string" || !part.type.startsWith("data-tool")) {
15+
// Not a Mastra data-tool part
16+
return null;
17+
}
18+
19+
// Utility helpers
20+
const first = <T>(...xs: (T | undefined | null)[]) => xs.find((x) => x !== undefined && x !== null) as T | undefined;
21+
22+
const toStringIfExists = (v: any) => (v === undefined || v === null ? undefined : String(v));
23+
24+
const isObject = (v: any) => v && typeof v === "object" && !Array.isArray(v);
25+
26+
// Extract the primary payload object to inspect - Mastra sometimes nests
27+
// the payload in different properties (data, payload, body, call, invocation, etc.)
28+
const candidates: any[] = [
29+
part.data,
30+
(part as any).payload,
31+
(part as any).body,
32+
part,
33+
].filter(Boolean);
34+
35+
// If payload has nested "data" and/or "call" shapes, prefer them
36+
let payload: any = undefined;
37+
for (const candidate of candidates) {
38+
if (isObject(candidate?.call)) {
39+
payload = candidate.call;
40+
break;
41+
}
42+
if (isObject(candidate?.invocation)) {
43+
payload = candidate.invocation;
44+
break;
45+
}
46+
if (isObject(candidate?.toolInvocation)) {
47+
payload = candidate.toolInvocation;
48+
break;
49+
}
50+
if (isObject(candidate?.data)) {
51+
// If candidate.data looks like call/args/out, prefer it
52+
if (isObject(candidate.data?.input) || isObject(candidate.data?.output) || candidate.data.id) {
53+
payload = candidate.data;
54+
break;
55+
}
56+
}
57+
}
58+
59+
// fallback to first object candidate
60+
payload = payload ?? candidates[0] ?? {};
61+
62+
// Some nested shapes: payload may itself contain another "payload" or "data"
63+
let inner = payload;
64+
if (!inner || !isObject(inner)) inner = {};
65+
if (isObject(inner.payload)) inner = inner.payload;
66+
if (isObject(inner.data) && (Object.keys(inner.data).length > 0)) inner = inner.data;
67+
68+
// Attempt to find a "call" or "execution" sub-object if present
69+
if (isObject(inner.call)) inner = inner.call;
70+
else if (isObject(inner.exec)) inner = inner.exec;
71+
else if (isObject(inner.execution)) inner = inner.execution;
72+
else if (isObject(inner.run)) inner = inner.run;
73+
74+
// Now, pick the fields from different possible shapes
75+
const toolCallId =
76+
(first<string>(
77+
inner?.toolCallId,
78+
inner?.callId,
79+
inner?.id,
80+
inner?.tool_call_id,
81+
inner?.requestId,
82+
inner?.runId
83+
) ?? first<string>(
84+
payload?.toolCallId,
85+
payload?.id,
86+
payload?.callId,
87+
payload?.tool_call_id
88+
) ?? `toolcall-${Date.now()}`) as string;
89+
90+
const toolName =
91+
(first<string>(
92+
inner?.toolName,
93+
inner?.name,
94+
inner?.tool,
95+
inner?.toolId,
96+
inner?.tool_id
97+
) ??
98+
first<string>(payload?.toolName, payload?.name, payload?.tool, payload?.toolId)) as string | undefined;
99+
100+
// Input detection: args / parameters / input / params
101+
const input =
102+
inner?.input ??
103+
inner?.args ??
104+
inner?.parameters ??
105+
inner?.params ??
106+
payload?.input ??
107+
payload?.args ??
108+
payload?.params ??
109+
undefined;
110+
111+
// Output detection: output / result / value / return
112+
const output =
113+
inner?.output ??
114+
inner?.result ??
115+
inner?.value ??
116+
inner?.return ??
117+
payload?.output ??
118+
payload?.result ??
119+
payload?.value ??
120+
undefined;
121+
122+
// Error detection
123+
const errorText =
124+
first<string | undefined>(
125+
inner?.errorText ?? inner?.error ?? inner?.err,
126+
payload?.errorText ?? payload?.error ?? payload?.err
127+
) ?? undefined;
128+
129+
// Map any status-like property to AI SDK tool state
130+
const rawState =
131+
(inner?.state ?? inner?.status ?? payload?.state ?? payload?.status ?? "").toString() ?? "";
132+
133+
// Convert a free-form status to tool states defined by the SDK
134+
function mapToToolState(s: string): DynamicToolUIPart["state"] {
135+
const st = String(s ?? "").toLowerCase();
136+
137+
if (!st) {
138+
// if output exists, consider it output-available; otherwise input-available.
139+
if (output !== undefined && output !== null) {
140+
return "output-available";
141+
}
142+
return "input-available";
143+
}
144+
145+
if (st.includes("stream") || st.includes("pending") || st.includes("started")) {
146+
return "input-streaming";
147+
}
148+
if (st.includes("run") || st.includes("running") || st.includes("in-flight") || st.includes("started")) {
149+
return "input-available";
150+
}
151+
if (st.includes("approval") || st.includes("approve-request") || st.includes("approval-requested")) {
152+
return "approval-requested" as DynamicToolUIPart["state"];
153+
}
154+
if (st.includes("approved") || st.includes("approval-responded")) {
155+
return "approval-responded" as DynamicToolUIPart["state"];
156+
}
157+
if (st.includes("success") || st.includes("done") || st.includes("finished") || st.includes("output-available") || st.includes("completed")) {
158+
return "output-available";
159+
}
160+
if (st.includes("deny") || st.includes("denied") || st.includes("rejected")) {
161+
return "output-denied" as DynamicToolUIPart["state"];
162+
}
163+
if (st.includes("error") || st.includes("failed") || st.includes("exception")) {
164+
return "output-error";
165+
}
166+
167+
// fallback: if an output is present consider finished, otherwise input available
168+
if (output !== undefined && output !== null) return "output-available";
169+
return "input-available";
170+
}
171+
172+
const state = mapToToolState(rawState);
173+
174+
// Finally construct the DynamicToolUIPart. We only fill the fields we can derive.
175+
const dynamic: DynamicToolUIPart = {
176+
type: "dynamic-tool",
177+
toolCallId: String(toolCallId),
178+
toolName: toolName ?? (typeof inner?.tool === "string" ? inner.tool : undefined) ?? `tool-${(toolCallId ?? "").slice(0, 8)}`,
179+
input: input ?? undefined,
180+
output: output ?? undefined,
181+
errorText: errorText ?? undefined,
182+
state,
183+
} as DynamicToolUIPart;
184+
185+
// Attach any useful debug/metadata (opaque) — non-standard; client code can ignore it.
186+
try {
187+
(dynamic as any).__mastra = {
188+
sourceType: part.type,
189+
original: part,
190+
};
191+
} catch {
192+
/* ignore: best-effort debug metadata attach */
193+
}
194+
195+
return dynamic;
196+
}

app/chat/providers/chat-context.tsx

Lines changed: 58 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import { useChat } from "@ai-sdk/react"
44
import { DefaultChatTransport } from "ai"
5-
import { getAgentConfig, AGENT_CONFIGS } from "@/app/chat/config/agents"
5+
import { getAgentConfig, AGENT_CONFIGS } from "../config/agents"
66
import type { UIMessage, DynamicToolUIPart, TextUIPart, ReasoningUIPart, ToolUIPart } from "ai"
77
import {
88
createContext,
@@ -14,6 +14,7 @@ import {
1414
useState,
1515
type ReactNode,
1616
} from "react"
17+
import { mapDataToolPartToDynamicToolPart } from "../helpers/tool-part-transform";
1718

1819
export interface Source {
1920
url: string
@@ -179,6 +180,7 @@ export function ChatProvider({
179180
agentId: selectedAgent,
180181
resourceId: resourceId,
181182
},
183+
182184
body: {
183185
messages,
184186
resourceId: resourceId,
@@ -223,14 +225,28 @@ export function ChatProvider({
223225

224226
const toolInvocations = useMemo((): ToolInvocationState[] => {
225227
const lastMessage = messages[messages.length - 1]
226-
if (lastMessage?.role === "assistant") {
227-
return (
228-
lastMessage.parts?.filter(
229-
(p): p is DynamicToolUIPart => p.type === "dynamic-tool"
230-
) ?? []
231-
)
228+
if (!lastMessage || lastMessage.role !== "assistant" || !lastMessage.parts) return []
229+
230+
// Iterate parts in order and collect both `dynamic-tool` and
231+
// Mastra-native `data-tool-*` parts (converted to dynamic-tool).
232+
const {parts} = lastMessage
233+
const result: ToolInvocationState[] = []
234+
235+
for (const p of parts) {
236+
if (p.type === "dynamic-tool") {
237+
result.push(p as ToolInvocationState)
238+
continue
239+
}
240+
241+
if (typeof p.type === "string" && p.type.startsWith("data-tool-")) {
242+
const converted = mapDataToolPartToDynamicToolPart(p)
243+
if (converted) {
244+
result.push(converted as ToolInvocationState)
245+
}
246+
}
232247
}
233-
return []
248+
249+
return result
234250
}, [messages])
235251

236252
// Extract sources from source-url parts
@@ -258,32 +274,46 @@ export function ChatProvider({
258274
if (lastMessage?.role === "assistant" && lastMessage.parts) {
259275
for (const part of lastMessage.parts) {
260276
// Check for generated HTML/React code in tool outputs
277+
let output: Record<string, unknown> | undefined
278+
279+
// Extract output from standard dynamic-tool parts
261280
if (part.type === "dynamic-tool") {
262281
const toolPart = part as DynamicToolUIPart
263-
if (toolPart.output && typeof toolPart.output === "object") {
264-
const output = toolPart.output as Record<string, unknown>
282+
output = toolPart.output as Record<string, unknown> | undefined
265283

266-
// Check for preview URL or generated code
267-
if (output.previewUrl && typeof output.previewUrl === "string") {
284+
// Or convert a Mastra `data-tool-*` part into a DynamicTool shape, then check its output
285+
} else if (typeof part.type === "string" && part.type.startsWith("data-tool-")) {
286+
const converted = mapDataToolPartToDynamicToolPart(part)
287+
output = converted?.output as Record<string, unknown> | undefined
288+
289+
// Not a tool part we care about
290+
} else {
291+
continue
292+
}
293+
294+
if (output && typeof output === "object") {
295+
const out = output as Record<string, unknown>
296+
297+
// Check for preview URL or generated code
298+
if (out.previewUrl && typeof out.previewUrl === "string") {
299+
setWebPreviewState({
300+
id: `preview-${Date.now()}`,
301+
url: out.previewUrl,
302+
title: (out.title as string) || "Generated Preview",
303+
})
304+
} else if (out.code && typeof out.code === "string") {
305+
// For code generation (like Recharts), create a data URL or sandbox
306+
const language = (out.language as string) || "tsx"
307+
if (language === "html" || (out as any).html) {
308+
const htmlContent = (out as any).html || out.code
309+
const dataUrl = `data:text/html;charset=utf-8,${encodeURIComponent(htmlContent)}`
268310
setWebPreviewState({
269311
id: `preview-${Date.now()}`,
270-
url: output.previewUrl,
271-
title: (output.title as string) || "Generated Preview",
312+
url: dataUrl,
313+
title: (out.title as string) || "Generated UI",
314+
code: out.code,
315+
language,
272316
})
273-
} else if (output.code && typeof output.code === "string") {
274-
// For code generation (like Recharts), create a data URL or sandbox
275-
const language = (output.language as string) || "tsx"
276-
if (language === "html" || output.html) {
277-
const htmlContent = (output.html as string) || output.code
278-
const dataUrl = `data:text/html;charset=utf-8,${encodeURIComponent(htmlContent)}`
279-
setWebPreviewState({
280-
id: `preview-${Date.now()}`,
281-
url: dataUrl,
282-
title: (output.title as string) || "Generated UI",
283-
code: output.code,
284-
language,
285-
})
286-
}
287317
}
288318
}
289319
}

app/networks/providers/network-context.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,9 +129,12 @@ export function NetworkProvider({
129129
const lastMessage = messages[messages.length - 1]
130130
if (lastMessage?.role === "assistant" && networkConfig) {
131131
const dataParts = lastMessage.parts?.filter(
132-
(p) => p.type === "data-network" || p.type === "dynamic-tool"
132+
(p) =>
133+
p.type === "data-network" ||
134+
p.type === "dynamic-tool" ||
135+
(typeof p.type === "string" && p.type.startsWith("data-tool-"))
133136
)
134-
137+
135138
if (dataParts && dataParts.length > 0) {
136139
const steps: RoutingStep[] = networkConfig.agents.map((agent, index) => ({
137140
agentId: agent.id,

0 commit comments

Comments
 (0)