The SDK provides five storage options: Key-Value, Vector, Database (SQL), Object (S3), and Stream. KV, Vector, and Stream are built into the runtime and are available as ctx.* in agents and c.var.* in routes. Database and Object storage use Bun's native APIs (sql, s3) instead.
Use ctx.kv / c.var.kv, ctx.vector / c.var.vector, and ctx.stream / c.var.stream for the runtime-managed storage services. Use Bun's sql and s3 directly for database and object storage.
Key-Value Storage
For caching patterns, TTL strategies, and best practices, see Key-Value Storage.
The Key-Value Storage API provides a simple way to store and retrieve data. Use ctx.kv in agents and c.var.kv in routes.
In local development, get(), set(), and delete() use SQLite. Namespace, stats, key listing, and search methods are cloud-backed and throw locally.
get
get(name: string, key: string): Promise<DataResult>
Retrieves a value from the key-value storage.
Parameters
name: The name of the key-value storagekey: The key to retrieve the value for
Return Value
Returns a Promise that resolves to a DataResult<T> object with:
exists: boolean indicating if the value was founddata: the actual value of type T (only present when exists is true)contentType: the content type of the stored valueexpiresAt(optional): ISO 8601 expiration timestamp, undefined if the key does not expire
Example
// Retrieve a value from key-value storage
const result = await ctx.kv.get<{ theme: string }>('user-preferences', 'user-123');
if (result.exists) {
// data is only accessible when exists is true
ctx.logger.info('User preferences:', result.data);
} else {
ctx.logger.info('User preferences not found');
}set
set<T = unknown>(name: string, key: string, value: T, params?: KeyValueStorageSetParams): Promise<void>
Stores a value in the key-value storage.
Parameters
name: The name of the key-value storagekey: The key to store the value undervalue: The value to storeparams(optional): Storage optionsttl: Time-to-live in seconds. Usenullor0for no expirationcontentType: Override the detected content type for the stored value
Return Value
Returns a Promise that resolves when the value has been stored.
Example
// Store a string value
await ctx.kv.set('user-preferences', 'user-123', JSON.stringify({ theme: 'dark' }));
// Store a JSON value
await ctx.kv.set('user-preferences', 'user-123', { theme: 'dark' });
// Store a binary value
const binaryData = new Uint8Array([1, 2, 3, 4]).buffer;
await ctx.kv.set('user-data', 'user-123', binaryData);
// Store a value with TTL (expires after 1 hour)
await ctx.kv.set('session', 'user-123', 'active', { ttl: 3600 });delete
delete(name: string, key: string): Promise<void>
Deletes a value from the key-value storage.
Parameters
name: The name of the key-value storagekey: The key to delete
Return Value
Returns a Promise that resolves when the value has been deleted.
Example
// Delete a value
await ctx.kv.delete('user-preferences', 'user-123');search
search<T>(name: string, keyword: string): Promise<Record<string, KeyValueItemWithMetadata<T>>>
Searches for keys matching a keyword pattern.
Parameters
name: The name of the key-value storagekeyword: The keyword to search for in key names
Return Value
Returns a map of keys to items with metadata:
interface KeyValueItemWithMetadata<T> {
value: T; // The stored value
contentType: string; // MIME type of the value
contentEncoding?: string | null;
size: number; // Size in bytes
expiresAt?: string | null; // ISO 8601 expiration timestamp, null/undefined if no expiry
firstUsed?: number | null; // Unix timestamp (ms) of first access
lastUsed?: number | null; // Unix timestamp (ms) of last access
count?: number | null; // Number of times accessed
}Example
// Search for all keys starting with 'user-'
const matches = await ctx.kv.search<{ theme: string }>('preferences', 'user-');
for (const [key, item] of Object.entries(matches)) {
ctx.logger.info('Found key', {
key,
value: item.value,
size: item.size,
lastUsed: item.lastUsed,
});
}getKeys
getKeys(name: string): Promise<string[]>
Returns all keys in a namespace.
Example
const keys = await ctx.kv.getKeys('cache');
ctx.logger.info(`Found ${keys.length} keys in cache`);getNamespaces
getNamespaces(): Promise<string[]>
Returns all namespace names.
Example
const namespaces = await ctx.kv.getNamespaces();
// ['cache', 'sessions', 'preferences']getStats
getStats(name: string): Promise<KeyValueStats>
Returns statistics for a namespace.
interface KeyValueStats {
sum: number; // Total size in bytes
count: number; // Number of keys
createdAt?: number; // Unix timestamp
lastUsedAt?: number; // Unix timestamp
}Example
const stats = await ctx.kv.getStats('cache');
ctx.logger.info('Cache stats', { keys: stats.count, totalBytes: stats.sum });getAllStats
getAllStats(params?: GetAllStatsParams): Promise<Record<string, KeyValueStats> | KeyValueStatsPaginated>
Returns statistics for all namespaces. Pass params to filter, sort, or paginate the response.
Parameters
params(optional):limit: Max namespaces per page (default 100, max 1000)offset: Namespaces to skipsort: Sort field, one of'name','size','records','created', or'lastUsed'direction:'asc'or'desc'name: Filter namespaces by name substringprojectId,agentId,projectName,agentName: Cloud metadata filters
Return Value
Without params: a flat Record<string, KeyValueStats>.
With params: a paginated KeyValueStatsPaginated object:
interface KeyValueStatsPaginated {
namespaces: Record<string, KeyValueStats>;
total: number;
limit: number;
offset: number;
hasMore: boolean;
}Example
const allStats = await ctx.kv.getAllStats();
for (const [namespace, stats] of Object.entries(allStats)) {
ctx.logger.info(`${namespace}: ${stats.count} keys, ${stats.sum} bytes`);
}
const page = await ctx.kv.getAllStats({ limit: 10, sort: 'records', direction: 'desc' });
if ('hasMore' in page) {
ctx.logger.info(`Showing ${Object.keys(page.namespaces).length} of ${page.total} namespaces`);
}createNamespace
createNamespace(name: string, params?: CreateNamespaceParams): Promise<void>
Creates a new namespace with an optional default TTL.
interface CreateNamespaceParams {
defaultTTLSeconds?: number; // 0 = no expiry; 60-31,536,000 = custom TTL in seconds
}Example
// Create with default server TTL (7 days)
await ctx.kv.createNamespace('tenant-123');
// Create with keys that never expire by default
await ctx.kv.createNamespace('tenant-123', { defaultTTLSeconds: 0 });
// Create with a 30-day default TTL
await ctx.kv.createNamespace('tenant-123', { defaultTTLSeconds: 2592000 });deleteNamespace
deleteNamespace(name: string): Promise<void>
Deletes a namespace and all its keys. This operation cannot be undone.
Example
await ctx.kv.deleteNamespace('old-cache');Vector Storage
For semantic search patterns, RAG examples, and metadata filtering, see Vector Storage.
The Vector Storage API provides a way to store and search for data using vector embeddings. Use ctx.vector in agents and c.var.vector in routes.
upsert
upsert(name: string, ...documents: VectorUpsertParams[]): Promise<VectorUpsertResult[]>
Inserts or updates vectors in the vector storage.
Parameters
name: The name of the vector storagedocuments: One or more documents to upsert. Each document must include a uniquekeyand eitherembeddings(pre-computed numbers) ordocument(text that will be automatically embedded)
Return Value
Returns a Promise that resolves to an array of VectorUpsertResult objects:
interface VectorUpsertResult {
key: string; // The key from the original document
id: string; // The generated vector ID in storage
}Example
// Upsert with automatic text embedding
const results = await ctx.vector.upsert(
'product-descriptions',
{ key: 'chair-001', document: 'Ergonomic office chair with lumbar support', metadata: { category: 'furniture' } },
{ key: 'headphones-001', document: 'Wireless noise-cancelling headphones', metadata: { category: 'electronics' } }
);
for (const r of results) {
ctx.logger.info(`Upserted ${r.key} with vector ID ${r.id}`);
}
// Upsert with pre-computed embeddings
await ctx.vector.upsert(
'product-embeddings',
{ key: 'embed-123', embeddings: [0.1, 0.2, 0.3, 0.4], metadata: { productId: '123' } }
);
// Upsert with TTL (expires after 7 days; null = never expires)
await ctx.vector.upsert(
'product-descriptions',
{ key: 'promo-001', document: 'Limited-time offer', ttl: 604800 }
);search
search(name: string, params: VectorSearchParams): Promise<VectorSearchResult[]>
Searches for vectors in the vector storage.
Parameters
name: The name of the vector storageparams: Search parameters object with the following properties:query(string, required): The text query to search for. This will be converted to embeddings and used to find semantically similar documents.limit(number, optional): Maximum number of search results to return. Must be a positive integer. If not specified, the server default will be used.similarity(number, optional): Minimum similarity threshold for results (0.0-1.0). Only vectors with similarity scores greater than or equal to this value will be returned. 1.0 means exact match, 0.0 means no similarity requirement.metadata(object, optional): Metadata filters to apply to the search. Only vectors whose metadata matches all specified key-value pairs will be included in results. Must be a valid JSON object.
Return Value
Returns a Promise that resolves to an array of search results, each containing an ID, key, metadata, and similarity score.
Examples
// Basic search with query only
const results = await ctx.vector.search('product-descriptions', {
query: 'comfortable office chair'
});
// Search with limit and similarity threshold
const results = await ctx.vector.search('product-descriptions', {
query: 'comfortable office chair',
limit: 5,
similarity: 0.7
});
// Search with metadata filtering
const results = await ctx.vector.search('product-descriptions', {
query: 'comfortable office chair',
limit: 10,
similarity: 0.6,
metadata: { category: 'furniture', inStock: true }
});
// Process search results
for (const result of results) {
ctx.logger.info(`Product ID: ${result.id}, Similarity: ${result.similarity}`);
ctx.logger.info(`Key: ${result.key}`);
ctx.logger.info('Metadata:', result.metadata);
}get
get<T>(name: string, key: string): Promise<VectorResult<T>>
Retrieves a specific vector by key. Returns a discriminated union on exists for type-safe access.
Parameters
name: The name of the vector storagekey: The unique key of the vector to retrieve
Return Value
Returns a VectorResult<T>, a discriminated union:
type VectorResult<T> =
| { exists: true; data: VectorSearchResultWithDocument<T> }
| { exists: false; data: never }
interface VectorSearchResultWithDocument<T> {
id: string;
key: string;
metadata?: T;
similarity: number;
document?: string; // Original text used to create the vector
embeddings?: number[];
expiresAt?: string; // ISO 8601 timestamp, undefined if no expiry
}Example
const result = await ctx.vector.get('product-descriptions', 'chair-001');
if (result.exists) {
ctx.logger.info(`ID: ${result.data.id}`);
ctx.logger.info(`Document: ${result.data.document}`);
ctx.logger.info('Metadata:', result.data.metadata);
} else {
ctx.logger.info('Vector not found');
}delete
delete(name: string, ...keys: string[]): Promise<number>
Deletes one or more vectors from the vector storage.
Parameters
name: The name of the vector storagekeys: One or more keys of the vectors to delete
Return Value
Returns a Promise that resolves to the number of vectors that were deleted.
Examples
// Delete a single vector by key
const deletedCount = await ctx.vector.delete('product-descriptions', 'chair-001');
ctx.logger.info(`Deleted ${deletedCount} vector(s)`);
// Delete multiple vectors in bulk
const deletedCount2 = await ctx.vector.delete('product-descriptions', 'chair-001', 'headphones-001', 'desk-002');
ctx.logger.info(`Deleted ${deletedCount2} vector(s)`);
// Delete with array spread
const keysToDelete = ['chair-001', 'headphones-001', 'desk-002'];
const deletedCount3 = await ctx.vector.delete('product-descriptions', ...keysToDelete);
// Handle cases where some vectors might not exist
const deletedCount4 = await ctx.vector.delete('product-descriptions', 'existing-key', 'non-existent-key');
ctx.logger.info(`Deleted ${deletedCount4} vector(s)`); // May be less than number of keys providedgetMany
getMany<T>(name: string, ...keys: string[]): Promise<Map<string, VectorSearchResultWithDocument<T>>>
Retrieves multiple vectors by key in a single request.
Parameters
name: The name of the vector storagekeys: One or more keys to retrieve
Return Value
Returns a Map of key to vector data. Keys not found in storage are omitted from the map.
Example
const vectors = await ctx.vector.getMany(
'product-descriptions',
'chair-001',
'desk-001',
'lamp-001'
);
for (const [key, data] of vectors) {
ctx.logger.info(`${key}: ${data.document}`);
}
// Check for a specific key
if (vectors.has('chair-001')) {
ctx.logger.info('Chair found:', vectors.get('chair-001')?.metadata);
}exists
exists(name: string): Promise<boolean>
Checks whether a namespace exists and contains at least one vector.
Parameters
name: The name of the vector storage namespace
Example
if (await ctx.vector.exists('product-descriptions')) {
ctx.logger.info('Namespace is populated');
} else {
ctx.logger.info('Namespace is empty or does not exist');
}getStats
getStats(name: string): Promise<VectorNamespaceStatsWithSamples>
Returns statistics for a namespace, including a sample of stored vectors.
Parameters
name: The name of the vector storage namespace
Return Value
interface VectorNamespaceStatsWithSamples {
count: number; // Number of vectors
sum: number; // Total size in bytes
createdAt?: number; // Unix timestamp (ms) when namespace was created
lastUsed?: number; // Unix timestamp (ms) when namespace was last accessed
internal?: boolean; // True if system-managed
sampledResults?: Record<string, VectorItemStats>; // Up to 20 sample vectors
}Example
const stats = await ctx.vector.getStats('product-descriptions');
ctx.logger.info(`${stats.count} vectors, ${stats.sum} bytes`);
if (stats.sampledResults) {
for (const [key, item] of Object.entries(stats.sampledResults)) {
ctx.logger.info(`${key}: ${item.size} bytes`);
}
}getAllStats
getAllStats(params?: VectorGetAllStatsParams): Promise<Record<string, VectorNamespaceStats> | VectorStatsPaginated>
Returns statistics for all namespaces. Accepts optional pagination and filtering parameters.
Parameters
params(optional):limit: Max namespaces per page (default 100, max 1000)offset: Namespaces to skipsort: Sort field —'name','size','records','created', or'lastUsed'direction:'asc'or'desc'name: Filter namespaces by name substring
Return Value
Without params: a flat Record<string, VectorNamespaceStats>.
With params: a paginated VectorStatsPaginated object:
interface VectorStatsPaginated {
namespaces: Record<string, VectorNamespaceStats>;
total: number;
limit: number;
offset: number;
hasMore: boolean;
}Example
// Flat map of all namespaces
const allStats = await ctx.vector.getAllStats();
for (const [ns, stats] of Object.entries(allStats)) {
ctx.logger.info(`${ns}: ${stats.count} vectors`);
}
// Paginated
const page = await ctx.vector.getAllStats({ limit: 10, offset: 0, sort: 'records', direction: 'desc' });
if ('hasMore' in page) {
ctx.logger.info(`Page 1 of ${Math.ceil(page.total / 10)}`);
}getNamespaces
getNamespaces(): Promise<string[]>
Returns the names of all vector namespaces (up to 1000, ordered by creation date, most recent first).
Example
const namespaces = await ctx.vector.getNamespaces();
ctx.logger.info(`Found ${namespaces.length} namespace(s):`, namespaces);deleteNamespace
deleteNamespace(name: string): Promise<void>
Deletes a namespace and all its vectors. This operation cannot be undone.
Example
await ctx.vector.deleteNamespace('old-products');Database (Bun SQL)
Database storage uses Bun's native SQL APIs, not ctx.* or c.var.*. Agentuity auto-injects credentials (DATABASE_URL) for PostgreSQL.
import { sql } from 'bun';Basic Queries
// Query with automatic parameter escaping
const users = await sql`SELECT * FROM users WHERE active = ${true}`;
// Insert data
await sql`INSERT INTO users (name, email) VALUES (${"Alice"}, ${"alice@example.com"})`;
// Update data
await sql`UPDATE users SET active = ${false} WHERE id = ${userId}`;
// Delete data
await sql`DELETE FROM users WHERE id = ${userId}`;Transactions
await sql.begin(async (tx) => {
await tx`UPDATE accounts SET balance = balance - ${amount} WHERE id = ${fromId}`;
await tx`UPDATE accounts SET balance = balance + ${amount} WHERE id = ${toId}`;
await tx`INSERT INTO transfers (from_id, to_id, amount) VALUES (${fromId}, ${toId}, ${amount})`;
});
// Automatically rolls back on errorDynamic Queries
const users = await sql`
SELECT * FROM users
WHERE 1=1
${minAge ? sql`AND age >= ${minAge}` : sql``}
${active !== undefined ? sql`AND active = ${active}` : sql``}
`;Bulk Insert
const newUsers = [
{ name: "Alice", email: "alice@example.com" },
{ name: "Bob", email: "bob@example.com" },
];
await sql`INSERT INTO users ${sql(newUsers)}`;Custom Connections
import { SQL } from "bun";
// PostgreSQL
const postgres = new SQL({
url: process.env.DATABASE_URL,
max: 20,
idleTimeout: 30,
});
// MySQL
const mysql = new SQL("mysql://user:pass@localhost:3306/mydb");
// SQLite
const sqlite = new SQL("sqlite://data/app.db");For Agentuity-specific patterns, see Database. For the complete Bun SQL API, see Bun SQL documentation.
Object Storage (Bun S3)
Object storage uses Bun's native S3 APIs, not ctx.* or c.var.*. To use S3, link a storage bucket to your project: new projects can add one during agentuity project create, and existing projects can follow the Object Storage setup. Credentials (S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, S3_BUCKET, S3_ENDPOINT) are written to .env by the CLI. When deployed to Agentuity Cloud, credentials for linked buckets are available automatically.
import { s3 } from 'bun';Reading Files
const file = s3.file('uploads/profile-123.jpg');
if (await file.exists()) {
const text = await file.text(); // For text files
const json = await file.json(); // For JSON files
const bytes = await file.bytes(); // For binary data
const blob = await file.blob(); // As Blob
}Writing Files
const file = s3.file('documents/readme.txt');
// Write text
await file.write('Hello, world!', { type: 'text/plain' });
// Write JSON
await file.write(JSON.stringify({ name: 'John' }), { type: 'application/json' });
// Write binary data
await file.write(pdfBuffer, { type: 'application/pdf' });Deleting Files
const file = s3.file('uploads/old-file.pdf');
await file.delete();Presigned URLs
Generate time-limited URLs for file access (synchronous, no network required):
// Download URL (1 hour)
const downloadUrl = s3.presign('uploads/document.pdf', {
expiresIn: 3600,
method: 'GET',
});
// Upload URL
const uploadUrl = s3.presign('uploads/new-file.pdf', {
expiresIn: 3600,
method: 'PUT',
});File Metadata
const file = s3.file('uploads/document.pdf');
const stat = await file.stat();
// { etag, lastModified, size, type }Listing Objects
import { S3Client } from 'bun';
const objects = await S3Client.list({
prefix: 'uploads/',
maxKeys: 100,
});Streaming Large Files
const file = s3.file('large-file.zip');
const writer = file.writer({ partSize: 5 * 1024 * 1024 }); // 5MB parts
writer.write(chunk1);
writer.write(chunk2);
await writer.end();For Agentuity-specific patterns, see Object Storage. For the complete Bun S3 API, see Bun S3 documentation.
Stream Storage
For streaming patterns and the dual-stream approach, see Durable Streams.
The Stream Storage API provides first-class support for creating and managing server-side streams. Use ctx.stream in agents and c.var.stream in routes.
create
create(namespace: string, props?: StreamCreateProps): Promise<Stream>
Creates a new writable stream in the given namespace.
Parameters
namespace: A string identifier used to group related streamsprops(optional): Configuration objectmetadata: Key-value pairs for identifying and searching streamscontentType: Content type of the stream (defaults toapplication/octet-stream)compress: Enable automatic gzip compression (defaults tofalse)ttl: Time-to-live in seconds. Usenullor0for no expiration
Return Value
Returns a Promise that resolves to a Stream object:
interface Stream {
id: string; // Unique stream identifier
url: string; // Public URL to access the stream
bytesWritten: number; // Total bytes written (readonly)
compressed: boolean; // Whether compression is enabled (readonly)
write(chunk: string | Uint8Array | ArrayBuffer | object): Promise<void>;
close(): Promise<void>;
getReader(): ReadableStream<Uint8Array>; // Get readable stream from URL
}Stream Characteristics:
- Read-Many: Multiple consumers can read simultaneously
- Re-readable: Can be read multiple times from the beginning
- Resumable: Supports HTTP Range requests
- Persistent: URLs remain accessible until expiration
Example
import { createAgent } from '@agentuity/runtime';
import { z } from 'zod';
const agent = createAgent('UserExporter', {
schema: {
input: z.object({ userId: z.string() }),
output: z.object({ streamId: z.string(), streamUrl: z.string() }),
},
handler: async (ctx, input) => {
// Create a stream with metadata
const stream = await ctx.stream.create('user-export', {
contentType: 'text/csv',
metadata: {
userId: input.userId,
timestamp: Date.now(),
},
});
// Write data in the background
ctx.waitUntil(async () => {
try {
await stream.write('Name,Email\n');
await stream.write('John,john@example.com\n');
} finally {
await stream.close();
}
});
return {
streamId: stream.id,
streamUrl: stream.url,
};
},
});get
get(id: string): Promise<StreamInfo>
Retrieves metadata for a stream by ID.
Parameters
id: The stream ID to retrieve
Return Value
Returns a StreamInfo object:
interface StreamInfo {
id: string; // Unique stream identifier
namespace: string; // Stream namespace
metadata: Record<string, string>; // User-defined metadata
url: string; // Public URL to access the stream
sizeBytes: number; // Size of stream content in bytes
expiresAt: string | null; // ISO 8601 expiration timestamp, null if no expiry
}Example
const info = await ctx.stream.get('stream_0199a52b06e3767dbe2f10afabb5e5e4');
ctx.logger.info('Stream details', {
namespace: info.namespace,
sizeBytes: info.sizeBytes,
url: info.url,
});download
download(id: string): Promise<ReadableStream<Uint8Array>>
Downloads stream content as a readable stream.
Parameters
id: The stream ID to download
Return Value
Returns a ReadableStream<Uint8Array> of the stream content.
Example
const readable = await ctx.stream.download('stream_0199a52b06e3767dbe2f10afabb5e5e4');
// Process the stream
const reader = readable.getReader();
const chunks: Uint8Array[] = [];
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
}
const content = Buffer.concat(chunks).toString('utf-8');list
list(params?: ListStreamsParams): Promise<ListStreamsResponse>
Lists and searches streams with filtering and pagination.
Parameters
params(optional):namespace: Filter by stream namespace. Takes precedence whennameis also providedname: Filter streams by namespace/name substringmetadata: Filter by metadata key-value pairslimit: Maximum streams to return (1-1000, default 100)offset: Number of streams to skipsort: Sort field, one of'name','created','updated','size','count', or'lastUsed'direction:'asc'or'desc'
Return Value
Returns a ListStreamsResponse:
interface ListStreamsResponse {
success: boolean;
message?: string; // Error message if not successful
streams: StreamInfo[]; // Array of stream metadata
total: number; // Total count for pagination
}Example
// List all streams
const result = await ctx.stream.list();
ctx.logger.info(`Found ${result.total} streams`);
// Filter by metadata
const userStreams = await ctx.stream.list({
metadata: { userId: 'user-123' }
});delete
delete(id: string): Promise<void>
Deletes a stream by its ID.
Parameters
id: The stream ID to delete
Example
await ctx.stream.delete(streamId);
ctx.logger.info('Stream deleted successfully');