Use key-value storage when a chat route needs to remember recent turns. In v3, keep the route in your framework and store the conversation under an explicit conversationId.
npm install hono @agentuity/aigateway @agentuity/keyvalue zodimport { Hono } from 'hono';
import { AIGatewayClient } from '@agentuity/aigateway';
import { KeyValueClient } from '@agentuity/keyvalue';
import { z } from 'zod';
const chatRequestSchema = z.object({
conversationId: z.string(),
message: z.string(),
model: z.string().min(1).default('deepseek/deepseek-v4-flash'),
});
const messageSchema = z.object({
role: z.enum(['user', 'assistant']),
content: z.string(),
});
const historySchema = z.array(messageSchema);
type ChatMessage = z.infer<typeof messageSchema>;
const HISTORY_NAMESPACE = 'chat-history';
const HISTORY_LIMIT = 20;
const HISTORY_TTL_SECONDS = 60 * 60 * 24 * 30;
const app = new Hono();
const kv = new KeyValueClient();
const gateway = new AIGatewayClient();
async function readHistory(conversationId: string): Promise<readonly ChatMessage[]> {
const stored = await kv.get<unknown>(HISTORY_NAMESPACE, conversationId);
return stored.exists ? historySchema.parse(stored.data) : [];
}
async function saveHistory(
conversationId: string,
messages: readonly ChatMessage[]
): Promise<readonly ChatMessage[]> {
const next = messages.slice(-HISTORY_LIMIT);
await kv.set(HISTORY_NAMESPACE, conversationId, next, {
ttl: HISTORY_TTL_SECONDS,
});
return next;
}
app.post('/api/chat', async (c) => {
const body: unknown = await c.req.json();
const input = chatRequestSchema.parse(body);
const history = await readHistory(input.conversationId);
const userMessage: ChatMessage = {
role: 'user',
content: input.message,
};
const messages = [...history, userMessage];
const result = await gateway.completeText({
model: input.model,
messages: [
{ role: 'system', content: 'You are a concise product support assistant.' },
...messages,
],
});
if (!result.hasText) {
return c.json({ error: 'The model returned no text.' }, 502);
}
const assistantMessage: ChatMessage = {
role: 'assistant',
content: result.text,
};
const nextHistory = await saveHistory(input.conversationId, [
...messages,
assistantMessage,
]);
return c.json({
conversationId: input.conversationId,
message: assistantMessage,
messageCount: nextHistory.length,
model: result.completion.model ?? input.model,
finishReason: result.finishReason ?? 'unknown',
});
});
app.get('/api/chat/:conversationId', async (c) => {
const conversationId = c.req.param('conversationId');
const messages = await readHistory(conversationId);
return c.json({ conversationId, messages });
});
app.delete('/api/chat/:conversationId', async (c) => {
const conversationId = c.req.param('conversationId');
await kv.delete(HISTORY_NAMESPACE, conversationId);
return c.json({ conversationId, messages: [] });
});
export default app;KeyValueClient reads AGENTUITY_SDK_KEY from the environment. AIGatewayClient reads AGENTUITY_AIGATEWAY_KEY, AGENTUITY_SDK_KEY, or AGENTUITY_CLI_KEY. The route accepts a model override but does not require provider keys or provider SDK env. Run this through agentuity dev or a linked Agentuity project when you want managed key-value storage and AI Gateway model routing.
Call the Route
Generate a conversation ID in your app, cookie, or authenticated user session. Pass that ID on each request.
const conversationId = crypto.randomUUID();
const response = await fetch('/api/chat', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
conversationId,
message: 'What changed in the v3 Agentuity starter?',
}),
});
if (!response.ok) {
throw new Error(`Chat failed with ${response.status}`);
}
const result = await response.json();The response includes the assistant message, model name, finish reason, and stored message count.
Reset a Conversation
Delete the key when the user starts over:
await fetch(`/api/chat/${conversationId}`, { method: 'DELETE' });Notes
- use one namespace for chat history and make the key explicit
- store a rolling window or summary instead of unlimited messages
- keep gateway metadata in the response when the UI should show model details
- use key-value storage or a database for new app state, not legacy runtime thread state
Next Steps
- Chat and Streaming: stream assistant text while storing the final transcript
- Key-Value Storage: configure TTLs, search keys, and manage namespaces
- Migrating Runtime Apps to Frameworks: move legacy runtime state into v3 app-owned state