Storage APIs — Agentuity Documentation

Storage APIs

KV, Vector, Database, Object, and Stream storage reference

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 storage
  • key: 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 found
  • data: 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 storage
  • key: The key to store the value under
  • value: 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 storage
  • key: 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<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 storage
  • keyword: 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 storage
  • documents: One or more documents to upsert. Each document must include a unique key and either embeddings (pre-computed numbers) or document (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 storage
  • params: 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 storage
  • key: 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 storage
  • keys: 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 provided

getMany

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 storage
  • keys: 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 skip
    • sort: 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 error

Dynamic 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 stream
  • props (optional): Configuration object
    • metadata: Key-value pairs for identifying and searching streams
    • contentType: Content type of the stream (defaults to application/octet-stream)
    • compress: Enable automatic gzip compression (defaults to false)

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 name
    • metadata: Filter by metadata key-value pairs
    • limit: 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');