Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
420 changes: 420 additions & 0 deletions ISSUES_ONEPAGER.md

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions demo-0jmi15/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Environment variables
# Copy this file to .env and fill in your values

# OpenAI API key (https://platform.openai.com/api-keys)
OPENAI_API_KEY=sk-...

23 changes: 23 additions & 0 deletions demo-0jmi15/agents/assistant.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { agent } from "veryfront/agent";
import { promptRegistry } from "veryfront/prompt";

function getSystemPrompt(): string {
const prompt = promptRegistry.get("assistant");
if (prompt) {
const content = prompt.getContent();
return typeof content === "string" ? content : "";
}
return "You are a helpful AI assistant.";
}

export default agent({
id: "assistant",
model: "openai/gpt-4o",
system: getSystemPrompt,

// Use all discovered tools from tools/
// To select specific tools, change to: tools: { toolName: true, anotherTool: true }
tools: true,

maxSteps: 10,
});
147 changes: 147 additions & 0 deletions demo-0jmi15/app/api/chat/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import { z } from "zod";
import { getAgent } from "veryfront/agent";

// AI SDK v5 UIMessage format with parts array
// Supports text, tool-call, tool-result, and dynamic tool-* parts
const textPartSchema = z.object({
type: z.literal("text"),
text: z.string().max(10000),
state: z.string().optional(),
});

const toolCallPartSchema = z.object({
type: z.literal("tool-call"),
toolCallId: z.string(),
toolName: z.string(),
args: z.unknown(),
});

const toolResultPartSchema = z.object({
type: z.literal("tool-result"),
toolCallId: z.string(),
result: z.unknown(),
});

// Dynamic tool part (e.g., tool-calculator, tool-search)
// These are UI-specific parts that include tool state
const dynamicToolPartSchema = z.object({
type: z.string().startsWith("tool-"),

Copilot AI Jan 20, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Zod schema uses z.string().startsWith("tool-") which is not a valid Zod method. To validate that a string starts with a specific prefix, use z.string().refine((s) => s.startsWith("tool-"), { message: "Type must start with 'tool-'" }) or z.string().regex(/^tool-/) instead.

Suggested change
type: z.string().startsWith("tool-"),
type: z.string().refine((s) => s.startsWith("tool-"), {
message: "type must start with 'tool-'",
}),

Copilot uses AI. Check for mistakes.
toolCallId: z.string(),
toolName: z.string(),
state: z.string().optional(),
input: z.unknown().optional(),
output: z.unknown().optional(),
}).passthrough();

// Union of all supported part types
const partSchema = z.union([
textPartSchema,
toolCallPartSchema,
toolResultPartSchema,
dynamicToolPartSchema,
]);

const messageSchema = z.object({
id: z.string().optional(),
role: z.enum(["user", "assistant", "system", "tool"]),
parts: z.array(partSchema).min(1),
});

const chatRequestSchema = z.object({
messages: z.array(messageSchema).min(1).max(100),
});

type ParsedMessage = z.infer<typeof messageSchema>;

/**
* Transform UI messages to agent-compatible format.
* AI SDK v5 UI bundles tool results in assistant message parts (output field),
* but the agent runtime expects separate tool role messages.
*/
function transformUIMessages(messages: ParsedMessage[]): ParsedMessage[] {
const result: ParsedMessage[] = [];

for (const msg of messages) {
if (msg.role === "assistant") {
// Check for tool parts with output (completed tool calls)
const toolPartsWithOutput = msg.parts.filter(
(p): p is { type: string; toolCallId: string; toolName: string; output: unknown } =>
typeof p === "object" &&
p !== null &&
"type" in p &&
typeof p.type === "string" &&
p.type.startsWith("tool-") &&
p.type !== "tool-result" &&
"output" in p &&
p.output !== undefined
);

if (toolPartsWithOutput.length > 0) {
// Add the assistant message (keep tool parts for args extraction)
result.push(msg);

// Add tool result messages for each completed tool call
for (const toolPart of toolPartsWithOutput) {
result.push({
id: `tool_${toolPart.toolCallId}`,
role: "tool",
parts: [{
type: "tool-result",
toolCallId: toolPart.toolCallId,
result: toolPart.output,
}],
});
}
} else {
result.push(msg);
}
} else {
result.push(msg);
}
}

return result;
}

export async function POST(request: Request) {
try {
const body = await request.json();
const { messages: rawMessages } = chatRequestSchema.parse(body);

// Transform UI format to agent-compatible format
// AI SDK v5 UI bundles tool results in assistant parts (output field),
// but the agent runtime expects separate tool role messages
const messages = transformUIMessages(rawMessages);

const agent = getAgent("assistant");
if (!agent) {
return Response.json({ error: "Agent not found" }, { status: 404 });
}

// Clear server-side memory before each request
// The client (useChat) manages full conversation history
await agent.clearMemory();

// In production, extract userId from session/cookie
// For development, we use a default user
const userId = "current-user";

// Pass transformed messages to the agent
const result = await agent.stream({
messages,
context: { userId },
});
return result.toDataStreamResponse();
} catch (error) {
if (error instanceof z.ZodError) {
return Response.json(
{ error: "Invalid request", details: error.errors },
{ status: 400 }
);
}
return Response.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}
12 changes: 12 additions & 0 deletions demo-0jmi15/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
<meta charSet="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>AI Chat</title>
</head>
<body>{children}</body>
</html>
);
}
22 changes: 22 additions & 0 deletions demo-0jmi15/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
'use client'

import { Chat } from 'veryfront/components/ai'
import { useChat } from 'veryfront/agent/react'

export default function ChatPage() {
const chat = useChat({ api: '/api/chat' })

return (
<div className="flex flex-col h-screen bg-white dark:bg-neutral-900">
{/* Header - sticky at top, full width */}
<header className="sticky top-0 z-10 flex-shrink-0 border-b border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900">
<div className="px-4 py-3 flex items-center justify-between">
<h1 className="font-medium text-neutral-900 dark:text-white">AI Assistant</h1>
</div>
</header>

{/* Chat - fills remaining space with scrollable content */}
<Chat {...chat} className="flex-1 min-h-0" placeholder="Message" />
</div>
)
}
25 changes: 25 additions & 0 deletions demo-0jmi15/tools/calculator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { tool } from "veryfront/tool";
import { z } from "zod";

export default tool({
id: "calculator",
description: "Perform basic arithmetic operations",
parameters: z.object({
operation: z.enum(["add", "subtract", "multiply", "divide"]),
a: z.number(),
b: z.number(),
}),
execute: async ({ operation, a, b }) => {
switch (operation) {
case "add":
return { result: a + b };
case "subtract":
return { result: a - b };
case "multiply":
return { result: a * b };
case "divide":
if (b === 0) throw new Error("Cannot divide by zero");
return { result: a / b };
}
},
});
16 changes: 16 additions & 0 deletions demo-0jmi15/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"jsx": "react-jsx",
"skipLibCheck": true,
"esModuleInterop": true,
"paths": {
"@/*": ["./*"]
}
},
"include": ["**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}
13 changes: 13 additions & 0 deletions demo-0jmi15/veryfront.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { VeryfrontConfig } from "veryfront";

const config: VeryfrontConfig = {
projectSlug: "demo-0jmi15-37z9kb",
router: "app",

// Development
dev: {
open: true,
},
};

export default config;
1 change: 1 addition & 0 deletions deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@
"#veryfront/rendering": "./src/rendering/index.ts",
"#veryfront/resource": "./src/resource/index.ts",
"#veryfront/routing": "./src/routing/index.ts",
"#veryfront/issues": "./src/issues/index.ts",
"#veryfront/security": "./src/security/index.ts",
"#veryfront/server": "./src/server/index.ts",
"#veryfront/testing": "./src/testing/index.ts",
Expand Down
Loading
Loading