State belongs to your app. Use cookies, auth sessions, or request headers to identify the current browser or user, then store durable state in KV or a database with keys you control. Nothing is stored automatically.
npm install hono @agentuity/keyvalue zod @agentuity/telemetryChoose the State Boundary
Pick the right store before writing any code. The wrong boundary is the most common source of memory bugs.
| What you need | Where it lives |
|---|---|
| browser session identity | signed cookie or your auth session |
| compact user or conversation memory | Key-Value Storage |
| relational user or entity data | Database |
| generated files, transcripts, or exports | Durable Streams |
| async job status | KV status record plus Queues |
The examples on this page use KV because that data is compact and fetched by exact key.
Store Session Memory
This route creates a signed browser session cookie when one does not exist, then reads and writes typed memory under that session ID.
import { KeyValueClient } from '@agentuity/keyvalue';
import { Hono } from 'hono';
import type { Context } from 'hono';
import { getCookie, setCookie } from 'hono/cookie';
import { z } from 'zod';
const memorySchema = z.object({
displayName: z.string(),
lastIntent: z.string(),
});
type UserMemory = z.infer<typeof memorySchema>;
const kv = new KeyValueClient();
const app = new Hono();
// Returns an existing session ID from the cookie, or creates and sets a new one
function getOrCreateSessionId(c: Context): string {
const existing = getCookie(c, 'app_session');
if (existing) {
return existing;
}
const sessionId = crypto.randomUUID();
setCookie(c, 'app_session', sessionId, {
httpOnly: true, // not readable from JS
sameSite: 'Lax', // safe for most single-domain apps
secure: new URL(c.req.url).protocol === 'https:',
maxAge: 60 * 60 * 24 * 30, // 30 days
});
return sessionId;
}
app.post('/api/memory', async (c) => {
const sessionId = getOrCreateSessionId(c);
const body: unknown = await c.req.json();
const memory = memorySchema.parse(body);
await kv.set<UserMemory>('app-memory', sessionId, memory, {
ttl: 60 * 60 * 24 * 30, // match cookie lifetime
});
return c.json({ ok: true });
});
app.get('/api/memory', async (c) => {
const sessionId = getOrCreateSessionId(c);
const result = await kv.get<UserMemory>('app-memory', sessionId);
return c.json({
memory: result.exists ? result.data : null,
});
});
export default app;KeyValueClient auto-reads AGENTUITY_SDK_KEY from the environment. Run agentuity dev locally and the key is injected automatically.
If you have already added @agentuity/hono, kv is available on c.var after the agentuity() middleware. Replace new KeyValueClient() with c.var.kv inside each handler. See Hono for setup details.
Cookie helpers are framework-specific. Nuxt and h3 expose getCookie/setCookie from h3. Next.js App Router exposes cookies() from next/headers. SvelteKit exposes cookies on the request event. The KV calls are identical across all of them.
Choose the Key
Use the most stable identifier available. Authenticated apps should key memory by user ID. Anonymous apps can fall back to a signed cookie, device token, or conversation ID.
// Prefer authenticated user ID; fall back to anonymous session ID
function memoryKey(userId: string | null, sessionId: string): string {
return userId ? `user:${userId}` : `session:${sessionId}`;
}
const key = memoryKey(userId, sessionId);
await kv.set('app-memory', key, memory, {
ttl: 60 * 60 * 24 * 30,
});Do not key durable user memory by request ID. Request IDs are useful for logs and tracing but do not identify the next request from the same user.
Validate on Read
KV stores raw JSON. Treat every read as a boundary: the value may have been written by older code, a migration script, or a manual test.
import { logger } from '@agentuity/telemetry';
const result = await kv.get<unknown>('app-memory', sessionId);
if (!result.exists) {
return null;
}
// safeParse returns { success: true, data } | { success: false, error: ValidationError }
const parsed = memorySchema.safeParse(result.data);
if (!parsed.success) {
// Log and discard stale or malformed records rather than crashing
logger.warn('Dropping unrecognised memory record', {
sessionId,
issues: parsed.error.issues,
});
return null;
}
return parsed.data;Inside @agentuity/hono route handlers, replace logger with c.var.logger so logs are scoped to the request span.
kv.get<T>() with a concrete type is fine when your app owns every write path. safeParse is the safer choice when the data shape can drift between deploys.
result.data is only accessible when result.exists is true. TypeScript enforces this: accessing result.data without the check is a type error.
Keep Memory Small
Store summaries, preferences, IDs, and pointers. Put large transcripts in a database or durable stream, then store only the reference in KV.
interface ConversationPointer {
readonly summary: string;
readonly streamId: string; // points to the full transcript
readonly updatedAt: string;
}
await kv.set<ConversationPointer>('conversation-memory', sessionId, {
summary: 'User prefers concise answers about billing issues',
streamId: 'stream_01j...',
updatedAt: new Date().toISOString(),
});A KV value holding a summary and a reference ID stays small and fast to read. The full transcript stays in the stream where it belongs.
Retention
Match TTL to the way state is used. Keys with no TTL override inherit the namespace default (7 days for auto-created namespaces). Pass ttl: null or ttl: 0 to store a key that never expires.
| State type | Suggested TTL |
|---|---|
| anonymous browser session | 7 to 30 days |
| signed-in user preference | null (no expiry) or database-backed |
| API cache entry | minutes or hours |
| async job status | long enough for the UI to poll and for support to review failures |
| generated output pointer | no longer than the stream or file it points to |
// Never expires
await kv.set('user-prefs', userId, prefs, { ttl: null });
// Expires after 30 days
await kv.set('app-memory', sessionId, memory, { ttl: 60 * 60 * 24 * 30 });
// Inherits namespace default (7 days if the namespace was auto-created)
await kv.set('cache', cacheKey, value);See Key-Value Storage for namespace-level TTL defaults and key inspection.
Common Gotchas
| Symptom | Check |
|---|---|
| memory never appears on the next request | the cookie domain, sameSite, and secure flags match the deployed host |
| one user sees another user's state | the key includes the authenticated user ID or anonymous session ID |
old values fail safeParse | read with unknown, run safeParse, then migrate or drop stale records |
| chat history grows without bound | store a summary plus recent turns, not the full transcript |
| keys disappear unexpectedly | the per-key or namespace TTL is shorter than expected; check result.expiresAt on a read |
Next Steps
- Agents: read memory before model calls
- Chat and Streaming: persist chat history after streamed responses
- Key-Value Storage: configure TTL, inspect namespaces, and understand sliding expiration
- Migrating from Runtime Apps: map
ctx.*state access from previous SDK versions to explicit app state