The SDK provides five storage options: Key-Value, Vector, Database (SQL), Object (S3), and Stream. Built-in services (KV, Vector, Stream) are accessed through the agent context (ctx.*), while Database and Object storage use Bun's native APIs (sql, s3).
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. It is accessed through the ctx.kv object.
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 value
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(name: string, key: string, value: ArrayBuffer | string | Json, ttl?: number): 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 store (can be an ArrayBuffer, string, or JSON object)ttl(optional): Time-to-live in seconds (minimum 60 seconds)
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
size: number; // Size in bytes
created_at: string; // ISO timestamp
updated_at: string; // ISO timestamp
}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,
updatedAt: item.updated_at,
});
}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(): Promise<Record<string, KeyValueStats>>
Returns statistics for all namespaces.
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`);
}createNamespace
createNamespace(name: string): Promise<void>
Creates a new namespace.
Example
await ctx.kv.createNamespace('tenant-123');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. It is accessed through the ctx.vector object.
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. 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.POSTGRES_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. 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. Streams are accessible via the ctx.stream object.
create
create(name: string, props?: StreamCreateProps): Promise<Stream>
Creates a new, named, writable stream.
Parameters
name: A string identifier for the streamprops(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)
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
name: string; // Stream name
metadata: Record<string, string>; // User-defined metadata
url: string; // Public URL to access the stream
sizeBytes: number; // Size of stream content in bytes
}Example
const info = await ctx.stream.get('stream_0199a52b06e3767dbe2f10afabb5e5e4');
ctx.logger.info('Stream details', {
name: info.name,
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):name: Filter by stream namemetadata: Filter by metadata key-value pairslimit: Maximum streams to return (1-1000, default 100)offset: Number of streams to skip
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');