Chat with Conversation History

Store chat history with key-value storage from a framework route

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 zod
typescriptsrc/index.ts
import { 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