API Reference
Comprehensive reference for the Agentuity JavaScript SDK API
This section provides detailed documentation for the Agentuity JavaScript SDK API, including method signatures, parameters, return values, and example usage.
Table of Contents
- Application Entry Point
- Agent Creation
- Schema Validation
- Agent Handler
- Context API
- Router & Routes
- Agent Communication
- Storage APIs
- Logging
- Telemetry
- Session & Thread Management
- Evaluations
- Event System
- Advanced Features
Application Entry Point
Every Agentuity v1 application starts with creating an application instance using the createApp() function. This function initializes your application with the necessary configuration, router, server, and event system.
createApp
createApp(config?: AppConfig): App
Creates and initializes an Agentuity application instance.
Parameters
config(optional): Application configuration objectcors: Override default CORS settingsservices: Override default services (KV, Vector, Stream)useLocal: Use local services for development (default: false)keyvalue: Custom KeyValueStorage implementationvector: Custom VectorStorage implementationstream: Custom StreamStorage implementation
setup: Async function called before server starts, returns app state available viactx.appshutdown: Async cleanup function called when server stops, receives app state
Return Value
Returns an App instance with the following properties:
interface App {
router: Hono; // The main application router
server: Server; // Server instance with .url property
logger: Logger; // Application-level logger
}Note: The App instance also provides addEventListener() and removeEventListener() methods for lifecycle events. See the Event System section for details.
Basic Example
// app.ts
import { createApp } from '@agentuity/runtime';
// Create the application instance (async)
const { server, logger } = await createApp();
// The application will automatically discover and mount agents
// from the agents directory specified in your agentuity.json
// Access application properties
logger.info(`Server running at ${server.url}`);With Configuration
import { createApp } from '@agentuity/runtime';
const { server, logger } = await createApp({
// Custom CORS settings
cors: {
origin: ['https://example.com'],
credentials: true
},
// Use local services for development
services: {
useLocal: true
}
});With Setup and Shutdown
import { createApp } from '@agentuity/runtime';
const { server, logger } = await createApp({
// Initialize shared resources
setup: async () => {
const db = await connectDatabase();
const redis = await connectRedis();
return { db, redis };
},
// Clean up on shutdown
shutdown: async (state) => {
await state.db.close();
await state.redis.quit();
},
});
// In agents, access via ctx.app:
// ctx.app.db.query('SELECT * FROM users')Environment Variables
Agentuity applications access configuration and secrets through standard Node.js environment variables (process.env).
Standard Environment Variables:
AGENTUITY_SDK_KEY- SDK-level API key (used in development to access Agentuity Cloud)AGENTUITY_PROJECT_KEY- Project-level API key (used in production)
Example
// app.ts
import { createApp } from '@agentuity/runtime';
if (!process.env.AGENTUITY_SDK_KEY) {
console.error('Missing AGENTUITY_SDK_KEY environment variable');
process.exit(1);
}
const { server, logger } = await createApp();
// Access other environment variables
const apiEndpoint = process.env.API_ENDPOINT || 'https://api.example.com';
const openaiKey = process.env.OPENAI_API_KEY;
// Optional: you can use the AI Gateway to access OpenAI, Anthropic, etc without needing to set various API keys.Note: Environment variables are typically loaded from a .env file in development and configured in your deployment environment for production.
Agent Creation
Agents are created using the createAgent() function, which provides type-safe agent definitions with built-in schema validation.
createAgent
createAgent(name: string, config: AgentConfig): AgentRunner
Creates a new agent with schema validation and type inference.
Parameters
name: Unique agent name (must be unique within the project)config: Configuration object with the following properties:description(optional): Human-readable description of what the agent doesschema(optional): Object containing input and output schemasinput: Schema for validating incoming data (Zod, Valibot, ArkType, or any StandardSchemaV1)output: Schema for validating outgoing datastream: Enable streaming responses (boolean, defaults to false)
handler: The agent function that processes inputs and returns outputssetup(optional): Async function called once on app startup, returns agent-specific config accessible viactx.configshutdown(optional): Async cleanup function called on app shutdown
Return Value
Returns an AgentRunner object with the following properties:
metadata: Agent metadata (id, identifier, filename, version, name, description)run(input?): Execute the agent with optional inputcreateEval(name, config): Create quality evaluations for this agentaddEventListener(eventName, callback): Attach lifecycle event listenersremoveEventListener(eventName, callback): Remove event listenersvalidator(options?): Route validation middleware (see below)inputSchema(conditional): Present if input schema is definedoutputSchema(conditional): Present if output schema is definedstream(conditional): Present if streaming is enabled
Note: To call agents from other agents, import them directly: import otherAgent from '@agent/other'; otherAgent.run(input) (see Agent Communication).
Agent Setup and Shutdown
Agents can define setup and shutdown functions for initialization and cleanup:
import { createAgent } from '@agentuity/runtime';
import { z } from 'zod';
const agent = createAgent('CachedProcessor', {
schema: {
input: z.object({ key: z.string() }),
output: z.object({ value: z.string() }),
},
// Called once when app starts - return value available via ctx.config
setup: async (app) => {
const cache = new Map<string, string>();
const client = await initializeExternalService();
return { cache, client };
},
// Called when app shuts down
shutdown: async (app, config) => {
await config.client.disconnect();
config.cache.clear();
},
handler: async (ctx, input) => {
// ctx.config is fully typed from setup's return value
const cached = ctx.config.cache.get(input.key);
if (cached) {
return { value: cached };
}
const value = await ctx.config.client.fetch(input.key);
ctx.config.cache.set(input.key, value);
return { value };
},
});
export default agent;The setup function receives the app state (from createApp's setup) and returns agent-specific configuration. This is useful for:
- Agent-specific caches or connection pools
- Pre-computed data or models
- External service clients
agent.validator()
Creates Hono middleware for type-safe request validation using the agent's schema.
Signatures
agent.validator(): MiddlewareHandler
agent.validator(options: { output: Schema }): MiddlewareHandler
agent.validator(options: { input: Schema; output?: Schema }): MiddlewareHandlerOptions
- No options: Uses agent's input/output schemas
{ output: Schema }: Output-only validation (useful for GET routes){ input: Schema, output?: Schema }: Custom schema override
Example
import { createRouter } from '@agentuity/runtime';
import agent from './agent';
const router = createRouter();
// Use agent's schema
router.post('/', agent.validator(), async (c) => {
const data = c.req.valid('json'); // Fully typed
return c.json(data);
});
// Custom schema override
router.post('/custom',
agent.validator({ input: z.object({ custom: z.string() }) }),
async (c) => {
const data = c.req.valid('json');
return c.json(data);
}
);Returns 400 Bad Request with validation error details if input validation fails.
Example
import { createAgent } from '@agentuity/runtime';
import { z } from 'zod';
const agent = createAgent('GreetingAgent', {
description: 'A simple greeting agent that responds to user messages',
schema: {
input: z.object({
message: z.string().min(1),
userId: z.string().optional(),
}),
output: z.object({
response: z.string(),
timestamp: z.number(),
}),
},
handler: async (ctx, input) => {
// Input is automatically validated and typed
ctx.logger.info(`Processing message from user: ${input.userId ?? 'anonymous'}`);
return {
response: `Hello! You said: ${input.message}`,
timestamp: Date.now(),
};
},
});
export default agent;Agent Configuration
The agent configuration object defines the structure and behavior of your agent.
Schema Property
The schema property defines input and output validation using any library that implements StandardSchemaV1:
// Using Zod
import { z } from 'zod';
const agent = createAgent('Greeter', {
schema: {
input: z.object({ name: z.string() }),
output: z.object({ greeting: z.string() }),
},
handler: async (ctx, input) => {
return { greeting: `Hello, ${input.name}!` };
},
});// Using Valibot
import * as v from 'valibot';
const agent = createAgent('Greeter', {
schema: {
input: v.object({ name: v.string() }),
output: v.object({ greeting: v.string() }),
},
handler: async (ctx, input) => {
return { greeting: `Hello, ${input.name}!` };
},
});Metadata Property
Agent metadata provides information about the agent for documentation and tooling:
const agent = createAgent('DataProcessor', {
description: 'Processes and transforms user data',
schema: {
input: inputSchema,
output: outputSchema,
},
handler: async (ctx, input) => {
// Agent logic
},
});Schema Validation
The SDK includes built-in schema validation using the StandardSchemaV1 interface, providing runtime type safety and automatic validation.
StandardSchema Support
The SDK supports any validation library that implements the StandardSchemaV1 interface:
Supported Libraries:
- Zod - Most popular, recommended for new projects
- Valibot - Lightweight alternative with similar API
- ArkType - TypeScript-first validation
- Any library implementing StandardSchemaV1
Why StandardSchema?
StandardSchemaV1 is a common interface that allows different validation libraries to work seamlessly with the SDK, so you're not locked into a single library and can choose the one that best fits your needs. For more details on schema validation patterns and best practices, see the Schema Validation Guide.
Example with Zod:
import { createAgent } from '@agentuity/runtime';
import { z } from 'zod';
const agent = createAgent('UserCreator', {
schema: {
input: z.object({
email: z.string().email(),
age: z.number().min(0).max(120),
preferences: z.object({
newsletter: z.boolean(),
notifications: z.boolean(),
}).optional(),
}),
output: z.object({
userId: z.string().uuid(),
created: z.date(),
}),
},
handler: async (ctx, input) => {
// Input is validated before handler runs
// TypeScript knows exact shape of input
ctx.logger.info(`Creating user: ${input.email}`);
return {
userId: crypto.randomUUID(),
created: new Date(),
};
},
});Validation Behavior:
- Input validation: Automatic before handler execution. If validation fails, an error is thrown and the handler is not called.
- Output validation: Automatic after handler execution. If validation fails, an error is thrown before returning to the caller.
- Error messages: Schema validation errors include detailed information about what failed and why.
Type Inference
TypeScript automatically infers types from your schemas, providing full autocomplete and type checking:
import { createAgent } from '@agentuity/runtime';
import { z } from 'zod';
const agent = createAgent('SearchAgent', {
schema: {
input: z.object({
query: z.string(),
filters: z.object({
category: z.enum(['tech', 'business', 'sports']),
limit: z.number().default(10),
}),
}),
output: z.object({
results: z.array(z.object({
id: z.string(),
title: z.string(),
score: z.number(),
})),
total: z.number(),
}),
},
handler: async (ctx, input) => {
// TypeScript knows:
// - input.query is string
// - input.filters.category is 'tech' | 'business' | 'sports'
// - input.filters.limit is number
// Return type is also validated
return {
results: [
{ id: '1', title: 'Example', score: 0.95 },
],
total: 1,
};
// This would cause a TypeScript error:
// return { invalid: 'structure' };
},
});
// When calling the agent from another agent:
import searchAgent from '@agent/search';
const result = await searchAgent.run({
query: 'agentic AI',
filters: { category: 'tech', limit: 5 },
});
// TypeScript knows result has this shape:
// {
// results: Array<{ id: string; title: string; score: number }>;
// total: number;
// }Benefits of Type Inference:
- Full IDE autocomplete for input and output
- Compile-time type checking catches errors before runtime
- No need to manually define TypeScript interfaces
- Refactoring is safer - changes to schemas update types automatically
Agent Handler
The agent handler is the core function that processes inputs and produces outputs.
Handler Signature
The handler signature has changed significantly from v0:
type AgentHandler<TInput, TOutput> = (
ctx: AgentContext,
input: TInput
) => Promise<TOutput> | TOutput;Parameters
ctx: The agent context providing access to services, logging, and agent capabilitiesinput: The validated input data (typed according to your input schema)
Return Value
The handler should return the output data (typed according to your output schema). The return can be:
- A direct value:
return { result: 'value' } - A Promise:
return Promise.resolve({ result: 'value' }) - An async function automatically returns a Promise
Key Differences from v0:
| Aspect | v0 | v1 |
|---|---|---|
| Parameters | (request, response, context) | (ctx, input) |
| Input access | await request.data.json() | Direct input parameter |
| Return pattern | return response.json(data) | return data |
| Validation | Manual | Automatic via schemas |
| Type safety | Manual types | Auto-inferred from schemas |
Example Comparison:
// v0 pattern
export default async (request, response, context) => {
const { name } = await request.data.json();
context.logger.info(`Hello ${name}`);
return response.json({ greeting: `Hello, ${name}!` });
};// v1 pattern
import { createAgent } from '@agentuity/runtime';
import { z } from 'zod';
export default createAgent('Greeter', {
schema: {
input: z.object({ name: z.string() }),
output: z.object({ greeting: z.string() }),
},
handler: async (ctx, input) => {
ctx.logger.info(`Hello ${input.name}`);
return { greeting: `Hello, ${input.name}!` };
},
});Input Validation
Input validation happens automatically before the handler executes:
const agent = createAgent('UserValidator', {
schema: {
input: z.object({
email: z.string().email(),
age: z.number().min(18),
}),
output: z.object({
success: z.boolean(),
}),
},
handler: async (ctx, input) => {
// This code only runs if:
// - input.email is a valid email format
// - input.age is a number >= 18
ctx.logger.info(`Valid user: ${input.email}, age ${input.age}`);
return { success: true };
},
});Validation Errors:
If validation fails, the handler is not called and an error response is returned:
// Request with invalid input:
// { email: "not-an-email", age: 15 }
// Results in validation error thrown before handler:
// Error: Validation failed
// - email: Invalid email format
// - age: Number must be greater than or equal to 18Return Values
Handlers return data directly rather than using response builder methods:
Simple Returns:
const agent = createAgent('Adder', {
schema: {
input: z.object({ x: z.number(), y: z.number() }),
output: z.object({ sum: z.number() }),
},
handler: async (ctx, input) => {
return { sum: input.x + input.y };
},
});Async Processing:
const agent = createAgent('UserFetcher', {
schema: {
input: z.object({ userId: z.string() }),
output: z.object({
user: z.object({
id: z.string(),
name: z.string(),
}),
}),
},
handler: async (ctx, input) => {
// Await async operations
const userData = await fetchUserFromDatabase(input.userId);
// Return the result
return {
user: {
id: userData.id,
name: userData.name,
},
};
},
});Error Handling:
const agent = createAgent('RiskyOperator', {
schema: {
input: z.object({ id: z.string() }),
output: z.object({ data: z.any() }),
},
handler: async (ctx, input) => {
try {
const data = await riskyOperation(input.id);
return { data };
} catch (error) {
ctx.logger.error('Operation failed', { error });
throw new Error('Failed to process request');
}
},
});Output Validation:
The return value is automatically validated against the output schema:
const agent = createAgent('ValueChecker', {
schema: {
input: z.object({ value: z.number() }),
output: z.object({
result: z.number(),
isPositive: z.boolean(),
}),
},
handler: async (ctx, input) => {
// This would fail output validation:
// return { result: input.value };
// Error: Missing required field 'isPositive'
// This passes validation:
return {
result: input.value,
isPositive: input.value > 0,
};
},
});Context API
The AgentContext provides access to various capabilities and services within your agent handler, including storage APIs, logging, tracing, agent communication, and state management.
Context Properties
The context object passed to your agent handler contains the following properties:
interface AgentContext<TConfig = unknown, TAppState = unknown> {
// Identifiers
sessionId: string; // Unique ID for this agent execution
// Agent Calling (use imports instead)
// import otherAgent from '@agent/other-agent';
// await otherAgent.run(input);
// Configuration
config: TConfig; // Agent-specific config from setup()
app: TAppState; // App-wide state from createApp setup()
// State Management
session: Session; // Session object for cross-request state
thread: Thread; // Thread object for conversation state
state: Map<string, unknown>; // Request-scoped state storage
// Storage Services
kv: KeyValueStorage; // Key-value storage
vector: VectorStorage; // Vector database for embeddings
stream: StreamStorage; // Stream storage
// Observability
logger: Logger; // Structured logging
tracer: Tracer; // OpenTelemetry tracing
// Lifecycle
waitUntil(promise: Promise<void> | (() => void | Promise<void>)): void;
}Key Properties Explained:
Identifiers:
sessionId: Unique identifier for this agent execution. Use for tracking and correlating logs.
Configuration:
config: Agent-specific configuration returned from the agent'ssetup()function. Fully typed based on what setup returns.app: Application-wide state returned fromcreateApp()'ssetup()function. Shared across all agents.
Agent Calling:
- Import agents directly:
import otherAgent from '@agent/other-agent' - Call with:
await otherAgent.run(input)
State Management:
session: Persistent session object that spans multiple agent calls.thread: Thread object for managing conversation state.state: Map for storing request-scoped data that persists throughout the handler execution.
Example Usage:
import { createAgent } from '@agentuity/runtime';
import { z } from 'zod';
const agent = createAgent('QueryProcessor', {
schema: {
input: z.object({ query: z.string() }),
output: z.object({ result: z.string() }),
},
handler: async (ctx, input) => {
// Access session ID
ctx.logger.info(`Session ID: ${ctx.sessionId}`);
// Use storage services
await ctx.kv.set('cache', 'last-query', input.query);
// Store request-scoped data
ctx.state.set('startTime', Date.now());
// Call another agent (import at top of file)
// import enrichmentAgent from '@agent/enrichment';
const enrichedData = await enrichmentAgent.run({ text: input.query });
// Use session state
ctx.session.state.set('queryCount',
(ctx.session.state.get('queryCount') as number || 0) + 1
);
return { result: enrichedData.output };
},
});Background Tasks (waitUntil)
The waitUntil method allows you to execute background tasks that don't need to block the immediate response to the caller. These tasks will be completed after the main response is sent.
waitUntil
waitUntil(callback: Promise<void> | (() => void | Promise<void>)): void
Defers the execution of a background task until after the main response has been sent.
Parameters
callback: A Promise, or a function that returns either void (synchronous) or a Promise (asynchronous), to be executed in the background
Example
import { createAgent } from '@agentuity/runtime';
import { z } from 'zod';
const agent = createAgent('MessageReceiver', {
schema: {
input: z.object({ userId: z.string(), message: z.string() }),
output: z.object({ status: z.string(), timestamp: z.number() }),
},
handler: async (ctx, input) => {
const responseData = {
status: 'received',
timestamp: Date.now(),
};
// Schedule background tasks (async functions)
ctx.waitUntil(async () => {
// Log the message asynchronously
await logMessageToDatabase(input.userId, input.message);
});
ctx.waitUntil(async () => {
// Send notification email in the background
await sendNotificationEmail(input.userId, input.message);
});
// Can also use synchronous functions
ctx.waitUntil(() => {
// Update analytics synchronously
updateAnalyticsSync(input.userId, 'message_received');
});
// Return immediately without waiting for background tasks
return responseData;
},
});Use Cases
- Logging and analytics that don't affect the user experience
- Sending notifications or emails
- Database cleanup or maintenance tasks
- Third-party API calls that don't impact the response
- Background data processing or enrichment
Storage APIs
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
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
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<string[]>
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 either embeddings or text
Return Value
Returns a Promise that resolves to an array of string IDs for the upserted vectors.
Example
// Upsert documents with text
const ids = 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' } }
);
// Upsert documents with embeddings
const ids2 = await ctx.vector.upsert(
'product-embeddings',
{ key: 'embed-123', embeddings: [0.1, 0.2, 0.3, 0.4], metadata: { productId: '123' } },
{ key: 'embed-456', embeddings: [0.5, 0.6, 0.7, 0.8], metadata: { productId: '456' } }
);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(name: string, key: string): Promise<VectorSearchResult | null>
Retrieves a specific vector from the vector storage using its key.
Parameters
name: The name of the vector storagekey: The unique key of the vector to retrieve
Return Value
Returns a Promise that resolves to a VectorSearchResult object if found, or null if the key doesn't exist.
Example
// Retrieve a specific vector by key
const vector = await ctx.vector.get('product-descriptions', 'chair-001');
if (vector) {
ctx.logger.info(`Found vector: ${vector.id}`);
ctx.logger.info(`Key: ${vector.key}`);
ctx.logger.info('Metadata:', vector.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 providedDatabase (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 Storage. For the complete Bun SQL API, see Bun SQL documentation.
Object Storage (Bun S3)
Object storage uses Bun's native S3 APIs. Agentuity auto-injects the required credentials (S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, S3_BUCKET, S3_ENDPOINT).
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
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');Logging
The SDK provides logging functionality through the ctx.logger object.
Logger Interface
The Logger interface defines the following methods:
interface Logger {
trace(message: unknown, ...args: unknown[]): void;
debug(message: unknown, ...args: unknown[]): void;
info(message: unknown, ...args: unknown[]): void;
warn(message: unknown, ...args: unknown[]): void;
error(message: unknown, ...args: unknown[]): void;
fatal(message: unknown, ...args: unknown[]): never;
child(opts: Record<string, unknown>): Logger;
}Logging Methods
trace
trace(message: unknown, ...args: unknown[]): void
Logs a trace message (most verbose logging level).
Parameters
message: The message to log (can be any type)args: Additional arguments to include in the log
Example
ctx.logger.trace('Detailed diagnostic info', { data: complexObject });debug
debug(message: unknown, ...args: unknown[]): void
Logs a debug message.
Parameters
message: The message to log (can be any type)args: Additional arguments to include in the log
Example
ctx.logger.debug('Processing request', { requestId: '123' });info
info(message: unknown, ...args: unknown[]): void
Logs an informational message.
Parameters
message: The message to log (can be any type)args: Additional arguments to include in the log
Example
ctx.logger.info('Request processed successfully', { requestId: '123' });warn
warn(message: unknown, ...args: unknown[]): void
Logs a warning message.
Parameters
message: The message to log (can be any type)args: Additional arguments to include in the log
Example
ctx.logger.warn('Resource not found', { resourceId: '456' });error
error(message: unknown, ...args: unknown[]): void
Logs an error message.
Parameters
message: The message to log (can be any type)args: Additional arguments to include in the log
Example
ctx.logger.error('Failed to process request', error);fatal
fatal(message: unknown, ...args: unknown[]): never
Logs a fatal error message and exits the process.
Parameters
message: The message to log (can be any type)args: Additional arguments to include in the log
Return Value
This method never returns (type never) as it terminates the process.
Example
// Log fatal error and exit
ctx.logger.fatal('Critical system failure', { error, context });
// Code after this line will never executeNote: Use fatal() only for unrecoverable errors that require process termination. For recoverable errors, use error() instead.
Creating Child Loggers
child
child(opts: Record<string, unknown>): Logger
Creates a child logger with additional context.
Parameters
opts: Additional context to include in all logs from the child logger (key-value pairs of any type)
Return Value
Returns a new Logger instance with the additional context.
Example
const requestLogger = ctx.logger.child({ requestId: '123', userId: '456' });
requestLogger.info('Processing request');Telemetry
The SDK integrates with OpenTelemetry for tracing and metrics.
Tracing
The SDK provides access to OpenTelemetry tracing through the ctx.tracer object.
Example
// Create a span
ctx.tracer.startActiveSpan('process-data', async (span) => {
try {
// Add attributes to the span
span.setAttribute('userId', '123');
// Perform some work
const result = await processData();
// Add events to the span
span.addEvent('data-processed', { itemCount: result.length });
return result;
} catch (error) {
// Record the error
span.recordException(error);
span.setStatus({ code: SpanStatusCode.ERROR });
throw error;
} finally {
span.end();
}
});Agent Communication
Agents can communicate with each other through a type-safe registry pattern, enabling complex multi-agent workflows with full TypeScript support.
Agent Registry
Import other agents directly and call them with full type safety.
Key Features:
- Type-safe calls: TypeScript infers input and output types from agent schemas
- Automatic validation: Input and output are validated against schemas
- IDE autocomplete: Full IntelliSense support for agent names and parameters
- Error handling: Type-safe error responses
Basic Pattern:
import otherAgent from '@agent/other';
// Call another agent
const result = await otherAgent.run(input);The @agent/ path alias provides access to all agents in your project with compile-time type checking.
Calling Other Agents
Import agents and call them directly.
Example: Simple Agent Call
// src/agent/processor/agent.ts
import { createAgent } from '@agentuity/runtime';
import { z } from 'zod';
import enrichmentAgent from '@agent/enrichment';
const processor = createAgent('Processor', {
schema: {
input: z.object({ userInput: z.string() }),
output: z.object({
processed: z.boolean(),
analysis: z.object({
sentiment: z.string(),
keywords: z.array(z.string()),
}),
}),
},
handler: async (ctx, input) => {
// Call the enrichment agent
const enriched = await enrichmentAgent.run({
text: input.userInput,
});
// TypeScript knows enriched has the shape from the agent's schema
return {
processed: true,
analysis: {
sentiment: enriched.sentiment,
keywords: enriched.keywords,
},
};
},
});
export default processor;Example: Calling Multiple Agents
import { createAgent } from '@agentuity/runtime';
import { z } from 'zod';
import webSearchAgent from '@agent/web-search';
import databaseAgent from '@agent/database';
import cacheAgent from '@agent/cache';
const coordinator = createAgent('Coordinator', {
schema: {
input: z.object({ query: z.string() }),
output: z.object({
results: z.array(z.any()),
metadata: z.object({
sources: z.number(),
processingTime: z.number(),
}),
}),
},
handler: async (ctx, input) => {
const startTime = Date.now();
// Call multiple agents in parallel
const [webResults, dbResults, cacheResults] = await Promise.all([
webSearchAgent.run({ query: input.query }),
databaseAgent.run({ query: input.query }),
cacheAgent.run({ key: input.query }),
]);
// Combine results
const allResults = [
...webResults.items,
...dbResults.records,
...(cacheResults.found ? [cacheResults.data] : []),
];
return {
results: allResults,
metadata: {
sources: 3,
processingTime: Date.now() - startTime,
},
};
},
});
export default coordinator;Error Handling:
import { createAgent } from '@agentuity/runtime';
import { z } from 'zod';
import externalService from '@agent/external-service';
const robustAgent = createAgent('Robust', {
schema: {
input: z.object({ userId: z.string() }),
output: z.object({ success: z.boolean(), data: z.any() }),
},
handler: async (ctx, input) => {
try {
// Try calling external agent
const result = await externalService.run({
userId: input.userId,
});
return { success: true, data: result };
} catch (error) {
ctx.logger.error('External service failed', { error });
// Fallback to cached data
const cached = await ctx.kv.get('user-cache', input.userId);
if (cached.exists) {
return { success: true, data: cached.data };
}
return { success: false, data: null };
}
},
});
export default robustAgent;Router & Routes
The SDK provides a Hono-based routing system for creating HTTP endpoints. Routes are defined in src/api/index.ts and provide full control over HTTP request handling.
Creating Routes
Routes are created using the createRouter() function from @agentuity/server.
Basic Setup:
// src/api/index.ts
import { createRouter } from '@agentuity/runtime';
const router = createRouter();
// Define routes
router.get('/', (c) => {
return c.json({ message: 'Hello from route' });
});
export default router;Router Context
Router handlers receive a context parameter (typically c) that provides access to the request, response helpers, and Agentuity services. This Hono Context is distinct from the AgentContext type used in agent handlers.
Understanding Context Types
Agentuity uses two distinct context types based on where you're writing code:
- AgentContext: Used in
agent.tsfiles for business logic (no HTTP access) - Router Context (Hono): Used in
src/api/index.tsfor HTTP handling (has HTTP + agent services)
Both commonly use c as the variable name in SDK examples. The distinction is type-based, not name-based.
For a detailed explanation with examples, see the Context Types Guide.
Router Context Interface:
interface RouterContext {
// Request
req: Request; // Hono request object with .param(), .query(), .header(), .json()
// Agentuity Services (via c.var)
var: {
kv: KeyValueStorage; // Key-value storage
vector: VectorStorage; // Vector storage
stream: StreamStorage; // Stream storage
logger: Logger; // Structured logging
tracer: Tracer; // OpenTelemetry tracing
};
// Response Helpers
json(data: any, status?: number): Response;
text(text: string, status?: number): Response;
html(html: string, status?: number): Response;
redirect(url: string, status?: number): Response;
// ... other Hono response methods
}Key Differences Between Context Types:
| Feature | Router Context (Hono) | Agent Context |
|---|---|---|
| Type | Hono Context | AgentContext |
| Used in | src/api/index.ts | agent.ts files |
| Request access | c.req (Hono Request) | Direct input parameter (validated) |
| Response | Builder methods (.json(), .text()) | Direct returns |
| Services | c.var.kv, c.var.logger, etc. | ctx.kv, ctx.logger, etc. |
| Agent calling | Import and call: agent.run() | Import and call: agent.run() |
| State management | Via Hono middleware | Built-in (.state, .session, .thread) |
Example Usage:
import processor from '@agent/processor';
router.post('/process', processor.validator(), async (c) => {
// Access request
const body = c.req.valid('json');
const authHeader = c.req.header('Authorization');
// Use Agentuity services
c.var.logger.info('Processing request', { body });
// Call an agent
const result = await processor.run({ data: body.data });
// Store result
await c.var.kv.set('results', body.id, result);
// Return response
return c.json({ success: true, result });
});Accessing Services:
All Agentuity storage and observability services are available in router handlers:
- Call agents via imports:
import agent from '@agent/name'; agent.run() - Use key-value storage via
c.var.kv - Log with
c.var.logger - Trace with
c.var.tracer - Store objects via
import { s3 } from 'bun' - Search vectors via
c.var.vector
This unified context makes it easy to build HTTP endpoints that leverage the full Agentuity platform.
HTTP Methods
The router supports all standard HTTP methods.
GET Requests:
router.get('/users', (c) => {
return c.json({ users: [] });
});
router.get('/users/:id', (c) => {
const id = c.req.param('id');
return c.json({ userId: id });
});POST Requests:
router.post('/users', async (c) => {
const body = await c.req.json();
return c.json({ created: true, user: body });
});PUT, PATCH, DELETE:
router.put('/users/:id', async (c) => {
const id = c.req.param('id');
const body = await c.req.json();
return c.json({ updated: true, id, data: body });
});
router.patch('/users/:id', async (c) => {
const id = c.req.param('id');
const updates = await c.req.json();
return c.json({ patched: true, id, updates });
});
router.delete('/users/:id', (c) => {
const id = c.req.param('id');
return c.json({ deleted: true, id });
});Calling Agents from Routes:
Import agents and call them directly:
import processorAgent from '@agent/processor';
router.post('/process', processorAgent.validator(), async (c) => {
const input = c.req.valid('json');
// Call the agent
const result = await processorAgent.run({
data: input.data,
});
return c.json(result);
});Specialized Routes
The router provides specialized route handlers for non-HTTP triggers like email, WebSockets, scheduled jobs, and real-time communication.
Email Routes
router.email(address: string, handler: EmailHandler): Router
Handle incoming emails sent to a specific address.
Parameters:
address: Email address to handle (e.g., 'support@example.com')handler: Function that receives the parsed email object and context
Handler Signature:
type EmailHandler = (email: Email, c: Context) => any | Promise<any>;
interface Email {
// Sender information
fromEmail(): string | null;
fromName(): string | null;
// Recipient information
to(): string | null; // All recipients (comma-separated)
toEmail(): string | null; // First recipient email
toName(): string | null; // First recipient name
// Message content
subject(): string | null;
text(): string | null;
html(): string | null;
// Attachments
attachments(): Array<{
filename: string;
contentType: string;
}>;
// Metadata
date(): Date | null;
messageId(): string | null;
headers(): Headers;
}Example:
router.email('support@example.com', async (email, c) => {
c.logger.info('Email received', {
from: email.fromEmail(),
fromName: email.fromName(),
to: email.to(), // All recipients
toEmail: email.toEmail(), // First recipient
subject: email.subject(),
date: email.date(),
messageId: email.messageId(),
hasAttachments: email.attachments().length > 0
});
// Process attachments if present
const attachments = email.attachments();
if (attachments.length > 0) {
c.logger.info('Attachments found', {
count: attachments.length,
files: attachments.map(att => ({
filename: att.filename,
contentType: att.contentType
}))
});
}
// Process email and trigger agent
// import emailProcessor from '@agent/email-processor';
const result = await emailProcessor.run({
sender: email.fromEmail(),
content: email.text() || email.html() || '',
});
return c.json({ processed: true });
});WebSocket Routes
router.websocket(path: string, handler: WebSocketHandler): Router
Create a WebSocket endpoint for real-time bidirectional communication.
Parameters:
path: Route path for the WebSocket endpointhandler: Function that returns a WebSocket connection setup function
Handler Signature:
type WebSocketHandler = (c: Context) => (ws: WebSocketConnection) => void | Promise<void>;
interface WebSocketConnection {
onOpen(handler: (event: any) => void | Promise<void>): void;
onMessage(handler: (event: any) => void | Promise<void>): void;
onClose(handler: (event: any) => void | Promise<void>): void;
send(data: string | ArrayBuffer | Uint8Array): void;
}Example:
import chatAgent from '@agent/chat';
router.websocket('/chat', (c) => (ws) => {
ws.onOpen((event) => {
c.var.logger.info('WebSocket connected');
ws.send(JSON.stringify({ type: 'connected' }));
});
ws.onMessage(async (event) => {
const message = JSON.parse(event.data);
// Process message with agent
const response = await chatAgent.run({
message: message.text,
});
ws.send(JSON.stringify({ type: 'response', data: response }));
});
ws.onClose((event) => {
c.var.logger.info('WebSocket disconnected');
});
});Server-Sent Events (SSE)
router.sse(path: string, handler: SSEHandler): Router
Create a Server-Sent Events endpoint for server-to-client streaming.
Parameters:
path: Route path for the SSE endpointhandler: Function that returns an SSE stream setup function
Handler Signature:
type SSEHandler = (c: Context) => (stream: SSEStream) => void | Promise<void>;
interface SSEStream {
write(data: string | number | boolean | object): Promise<void>;
writeSSE(message: { data?: string; event?: string; id?: string }): Promise<void>;
onAbort(handler: () => void): void;
close?(): void;
}Example:
import longRunningAgent from '@agent/long-running';
router.sse('/updates', (c) => async (stream) => {
// Send initial connection message
await stream.write({ type: 'connected' });
// Stream agent progress updates
const updates = await longRunningAgent.run({ task: 'process' });
for (const update of updates) {
await stream.write({
type: 'progress',
data: update,
});
}
// Clean up on client disconnect
stream.onAbort(() => {
c.var.logger.info('Client disconnected');
});
});Stream Routes
router.stream(path: string, handler: StreamHandler): Router
Create an HTTP streaming endpoint for piping data streams.
Parameters:
path: Route path for the stream endpointhandler: Function that returns a ReadableStream
Handler Signature:
type StreamHandler = (c: Context) => ReadableStream<any> | Promise<ReadableStream<any>>;Example:
import dataGenerator from '@agent/data-generator';
router.stream('/data', async (c) => {
// Create a readable stream
const stream = new ReadableStream({
async start(controller) {
// Stream data chunks
const data = await dataGenerator.run({ query: 'all' });
for (const chunk of data) {
controller.enqueue(new TextEncoder().encode(JSON.stringify(chunk) + '\n'));
}
controller.close();
},
});
return stream;
});Cron Routes
router.cron(schedule: string, handler: CronHandler): Router
Schedule a recurring job using cron syntax.
Parameters:
schedule: Cron schedule expression (e.g., '0 9 * * *' for daily at 9am)handler: Function to execute on schedule
Handler Signature:
type CronHandler = (c: Context) => any | Promise<any>;Cron Schedule Format:
┌───────────── minute (0 - 59)
│ ┌───────────── hour (0 - 23)
│ │ ┌───────────── day of month (1 - 31)
│ │ │ ┌───────────── month (1 - 12)
│ │ │ │ ┌───────────── day of week (0 - 6) (Sunday = 0)
│ │ │ │ │
* * * * *Example:
import reportGenerator from '@agent/report-generator';
import healthCheck from '@agent/health-check';
// Run daily at 9am
router.cron('0 9 * * *', async (c) => {
c.var.logger.info('Running daily report');
const report = await reportGenerator.run({
type: 'daily',
date: new Date().toISOString(),
});
// Send report via email or store
await c.var.kv.set('reports', `daily-${Date.now()}`, report);
return c.json({ success: true });
});
// Run every 5 minutes
router.cron('*/5 * * * *', async (c) => {
await healthCheck.run({});
return c.json({ checked: true });
});SMS Routes
router.sms(params: { number: string }, handler: SMSHandler): Router
Handle incoming SMS messages sent to a specific phone number.
Parameters:
params.number: Phone number to handle (E.164 format, e.g., '+12345678900')handler: Function that processes the SMS
Handler Signature:
type SMSHandler = (c: Context) => any | Promise<any>;Example:
import smsBot from '@agent/sms-bot';
router.sms({ number: '+12345678900' }, async (c) => {
const body = await c.req.json();
c.var.logger.info('SMS received', {
from: body.from,
message: body.text,
});
// Process SMS with agent
const response = await smsBot.run({
sender: body.from,
message: body.text,
});
return c.json({ reply: response });
});Route Parameters
Access route parameters and query strings through the request object.
Path Parameters:
router.get('/posts/:postId/comments/:commentId', (c) => {
const postId = c.req.param('postId');
const commentId = c.req.param('commentId');
return c.json({ postId, commentId });
});Query Parameters:
router.get('/search', (c) => {
const query = c.req.query('q');
const limit = c.req.query('limit') || '10';
const page = c.req.query('page') || '1';
return c.json({
query,
limit: parseInt(limit),
page: parseInt(page)
});
});Request Headers:
router.get('/protected', (c) => {
const authHeader = c.req.header('Authorization');
if (!authHeader) {
return c.json({ error: 'Unauthorized' }, 401);
}
return c.json({ authorized: true });
});Session & Thread Management
The SDK provides session and thread management for stateful agent interactions.
Sessions
Sessions represent a user's interaction with your agents, persisting across multiple requests.
Session Interface:
interface Session {
id: string; // Unique session identifier
thread: Thread; // Reference to the current thread
state: Map<string, unknown>; // Session-scoped persistent state
// Event listeners for session lifecycle (optional)
addEventListener(
eventName: 'completed',
callback: (eventName: 'completed', session: Session) => Promise<void> | void
): void;
removeEventListener(
eventName: 'completed',
callback: (eventName: 'completed', session: Session) => Promise<void> | void
): void;
}Using Session State:
const agent = createAgent('MessageTracker', {
schema: {
input: z.object({ message: z.string() }),
output: z.object({
response: z.string(),
messageCount: z.number(),
}),
},
handler: async (ctx, input) => {
// Get current message count from session
const currentCount = (ctx.session.state.get('messageCount') as number) || 0;
const newCount = currentCount + 1;
// Update session state
ctx.session.state.set('messageCount', newCount);
ctx.session.state.set('lastMessage', input.message);
ctx.session.state.set('lastTimestamp', Date.now());
return {
response: `Message received`,
messageCount: newCount,
};
},
});Threads
Threads represent a specific conversation or workflow within a session.
Thread Interface:
interface Thread {
id: string; // Unique thread identifier
state: Map<string, unknown>; // Thread-scoped state
// Event listeners for thread lifecycle (optional)
addEventListener(
eventName: 'destroyed',
callback: (eventName: 'destroyed', thread: Thread) => Promise<void> | void
): void;
removeEventListener(
eventName: 'destroyed',
callback: (eventName: 'destroyed', thread: Thread) => Promise<void> | void
): void;
destroy(): Promise<void>; // Destroy the thread
}Using Thread State:
const conversationAgent = createAgent('ConversationAgent', {
schema: {
input: z.object({
message: z.string(),
userId: z.string(),
}),
output: z.object({
reply: z.string(),
context: z.array(z.string()),
}),
},
handler: async (ctx, input) => {
// Get conversation history from thread
const history = (ctx.thread.state.get('history') as string[]) || [];
// Add current message to history
history.push(input.message);
ctx.thread.state.set('history', history);
// Store user info
ctx.thread.state.set('userId', input.userId);
return {
reply: `Processed message ${history.length}`,
context: history,
};
},
});State Management
The context provides three levels of state management:
1. Request State (ctx.state):
- Scoped to the current request only
- Cleared after handler completes
- Use for temporary data within a single execution
handler: async (ctx, input) => {
ctx.state.set('startTime', Date.now());
// Do work...
const duration = Date.now() - (ctx.state.get('startTime') as number);
ctx.logger.info(`Request took ${duration}ms`);
}2. Thread State (ctx.thread.state):
- Persists across requests within the same thread
- Useful for conversation context and workflow state
- Destroyed when thread is destroyed
handler: async (ctx, input) => {
const step = (ctx.thread.state.get('currentStep') as number) || 1;
ctx.thread.state.set('currentStep', step + 1);
}3. Session State (ctx.session.state):
- Persists across all threads in a session
- Useful for user preferences and long-term data
- Survives thread destruction
handler: async (ctx, input) => {
const totalRequests = (ctx.session.state.get('totalRequests') as number) || 0;
ctx.session.state.set('totalRequests', totalRequests + 1);
}Complete Example:
const statefulAgent = createAgent('StatefulAgent', {
schema: {
input: z.object({ action: z.string(), data: z.any() }),
output: z.object({
success: z.boolean(),
stats: z.object({
requestDuration: z.number(),
threadStep: z.number(),
sessionTotal: z.number(),
}),
}),
},
handler: async (ctx, input) => {
// Request-scoped state
ctx.state.set('startTime', Date.now());
// Thread-scoped state (conversation flow)
const threadStep = (ctx.thread.state.get('step') as number) || 0;
ctx.thread.state.set('step', threadStep + 1);
ctx.thread.state.set('lastAction', input.action);
// Session-scoped state (user-level)
const sessionTotal = (ctx.session.state.get('total') as number) || 0;
ctx.session.state.set('total', sessionTotal + 1);
ctx.session.state.set('lastSeen', Date.now());
// Process action
await processAction(input.action, input.data);
return {
success: true,
stats: {
requestDuration: Date.now() - (ctx.state.get('startTime') as number),
threadStep: threadStep + 1,
sessionTotal: sessionTotal + 1,
},
};
},
});Evaluations
The SDK includes a built-in evaluation framework for assessing agent quality and performance. Evals run automatically after agent execution to validate outputs and measure quality metrics.
Creating Evals
Evals are created using the createEval() method on an agent.
Eval Configuration:
agent.createEval({
metadata: {
name: string; // Eval name
description?: string; // What this eval checks
filename?: string; // Source file (auto-populated)
},
handler: EvalFunction; // Eval logic
});Basic Example:
import { createAgent } from '@agentuity/runtime';
import { z } from 'zod';
const agent = createAgent('ConfidenceAgent', {
schema: {
input: z.object({ query: z.string() }),
output: z.object({
answer: z.string(),
confidence: z.number(),
}),
},
handler: async (ctx, input) => {
// Agent logic
return {
answer: 'Response to ' + input.query,
confidence: 0.95,
};
},
});
// Add an eval to check confidence threshold
agent.createEval({
metadata: {
name: 'confidence-check',
description: 'Ensures confidence is above minimum threshold',
},
handler: async (ctx, input, output) => {
const passed = output.confidence >= 0.8;
return {
success: true,
passed,
metadata: {
confidence: output.confidence,
threshold: 0.8,
reason: passed ? 'Confidence acceptable' : 'Confidence too low',
},
};
},
});Eval Results
Evals can return different result types depending on the evaluation needs.
Result Types:
type EvalRunResult =
| { success: true; passed: boolean; metadata?: object } // Binary pass/fail
| { success: true; score: number; metadata?: object } // Numeric score (0-1)
| { success: false; error: string }; // Eval failed to runBinary Pass/Fail Eval:
agent.createEval({
metadata: { name: 'output-length-check' },
handler: async (ctx, input, output) => {
const passed = output.answer.length >= 10;
return {
success: true,
passed,
metadata: {
actualLength: output.answer.length,
minimumLength: 10
}
};
},
});Score-Based Eval:
agent.createEval({
metadata: { name: 'quality-score' },
handler: async (ctx, input, output) => {
// Calculate quality score (0-1 range)
let score = 0;
// Check various quality factors
if (output.answer.length > 20) score += 0.3;
if (output.confidence > 0.8) score += 0.4;
if (output.answer.includes(input.query)) score += 0.3;
return {
success: true,
score,
metadata: {
factors: {
length: output.answer.length,
confidence: output.confidence,
relevance: output.answer.includes(input.query)
}
}
};
},
});Error Handling in Evals:
agent.createEval({
metadata: { name: 'external-validation' },
handler: async (ctx, input, output) => {
try {
// Call external validation service
const isValid = await validateWithExternalService(output);
return {
success: true,
passed: isValid,
metadata: { validator: 'external-service' }
};
} catch (error) {
// Eval itself failed
return {
success: false,
error: `Validation service error: ${error.message}`
};
}
},
});Eval Execution
Evals run automatically after agent execution completes, using the waitUntil() mechanism.
Execution Flow:
- Agent handler executes and returns output
- Output is validated against schema
- Agent emits
completedevent - All evals attached to the agent run via
waitUntil() - Eval results are sent to eval tracking service
- Response is returned to caller (without waiting for evals)
Multiple Evals Example:
const comprehensiveAgent = createAgent('TextAnalyzer', {
schema: {
input: z.object({ text: z.string() }),
output: z.object({
summary: z.string(),
sentiment: z.enum(['positive', 'negative', 'neutral']),
keywords: z.array(z.string()),
}),
},
handler: async (ctx, input) => {
// Processing logic
return {
summary: 'Summary of ' + input.text,
sentiment: 'positive',
keywords: ['keyword1', 'keyword2'],
};
},
});
// Eval 1: Check summary length
comprehensiveAgent.createEval({
metadata: { name: 'summary-length' },
handler: async (ctx, input, output) => {
const passed = output.summary.length >= 20 && output.summary.length <= 200;
return { success: true, passed, metadata: { length: output.summary.length } };
},
});
// Eval 2: Check keywords count
comprehensiveAgent.createEval({
metadata: { name: 'keywords-count' },
handler: async (ctx, input, output) => {
const passed = output.keywords.length >= 2 && output.keywords.length <= 10;
return { success: true, passed, metadata: { count: output.keywords.length } };
},
});
// Eval 3: Overall quality score
comprehensiveAgent.createEval({
metadata: { name: 'quality-score' },
handler: async (ctx, input, output) => {
const summaryQuality = output.summary.length >= 50 ? 0.5 : 0.3;
const keywordQuality = output.keywords.length >= 3 ? 0.5 : 0.3;
const score = summaryQuality + keywordQuality;
return {
success: true,
score,
metadata: {
summaryQuality,
keywordQuality
}
};
},
});Accessing Context in Evals:
Evals receive the same context as the agent, enabling access to storage, logging, and other services:
agent.createEval({
metadata: { name: 'logged-eval' },
handler: async (ctx, input, output) => {
// Log eval execution
ctx.logger.info('Running eval', {
sessionId: ctx.sessionId,
input,
output
});
// Store eval results
await ctx.kv.set('eval-results', ctx.sessionId, {
timestamp: Date.now(),
passed: true
});
return { success: true, passed: true };
},
});Event System
The SDK provides a comprehensive event system for hooking into agent, session, and thread lifecycles.
Agent Events
Agents emit events during their lifecycle that you can listen to.
Available Events:
agent.addEventListener('started', (eventName, agent, ctx) => {
// Agent execution has started
});
agent.addEventListener('completed', (eventName, agent, ctx) => {
// Agent execution completed successfully
// Evals run during this event
});
agent.addEventListener('errored', (eventName, agent, ctx, error) => {
// Agent execution failed
});Example: Logging Agent Lifecycle:
import { createAgent } from '@agentuity/runtime';
import { z } from 'zod';
const agent = createAgent('TaskRunner', {
schema: {
input: z.object({ task: z.string() }),
output: z.object({ result: z.string() }),
},
handler: async (ctx, input) => {
return { result: `Completed: ${input.task}` };
},
});
// Log when agent starts
agent.addEventListener('started', (eventName, agent, ctx) => {
ctx.logger.info('Agent started', {
agentName: agent.metadata.name,
sessionId: ctx.sessionId,
});
});
// Log when agent completes
agent.addEventListener('completed', (eventName, agent, ctx) => {
ctx.logger.info('Agent completed', {
agentName: agent.metadata.name,
sessionId: ctx.sessionId,
});
});
// Log when agent errors
agent.addEventListener('errored', (eventName, agent, ctx, error) => {
ctx.logger.error('Agent errored', {
agentName: agent.metadata.name,
sessionId: ctx.sessionId,
error: error.message,
});
});App Events
The application instance emits events for agents, sessions, and threads.
Available App Events:
// Agent lifecycle events
app.addEventListener('agent.started', (eventName, agent, ctx) => {});
app.addEventListener('agent.completed', (eventName, agent, ctx) => {});
app.addEventListener('agent.errored', (eventName, agent, ctx, error) => {});
// Session lifecycle events
app.addEventListener('session.started', (eventName, session) => {});
app.addEventListener('session.completed', (eventName, session) => {});
// Thread lifecycle events
app.addEventListener('thread.created', (eventName, thread) => {});
app.addEventListener('thread.destroyed', (eventName, thread) => {});Example: Application-Wide Analytics:
import { createApp } from '@agentuity/runtime';
const { app, server, logger } = createApp();
// Track agent executions
let agentExecutions = 0;
app.addEventListener('agent.started', (eventName, agent, ctx) => {
agentExecutions++;
logger.info(`Total agent executions: ${agentExecutions}`);
});
// Track errors for monitoring
app.addEventListener('agent.errored', (eventName, agent, ctx, error) => {
logger.error('Agent error detected', {
agentName: agent.metadata.name,
error: error.message,
sessionId: ctx.sessionId
});
// Could send to error tracking service
// await sendToErrorTracker(error, agent, ctx);
});
// Session analytics
app.addEventListener('session.completed', (eventName, session) => {
const messageCount = session.state.get('messageCount') || 0;
logger.info('Session completed', {
sessionId: session.id,
totalMessages: messageCount
});
});
// Thread cleanup
app.addEventListener('thread.destroyed', (eventName, thread) => {
logger.info('Thread destroyed', {
threadId: thread.id
});
});Event Handlers
Event handlers can be added and removed dynamically.
Adding Event Listeners:
// Agent-level
const handler = (eventName, agent, ctx) => {
ctx.logger.info('Agent started');
};
agent.addEventListener('started', handler);
// App-level
app.addEventListener('agent.completed', (eventName, agent, ctx) => {
ctx.logger.info(`${agent.metadata.name} completed`);
});Removing Event Listeners:
// Remove specific handler
agent.removeEventListener('started', handler);
// Note: You must keep a reference to the handler function
// to remove it laterComplete Example with Cleanup:
const agent = createAgent('ActionHandler', {
schema: {
input: z.object({ action: z.string() }),
output: z.object({ success: z.boolean() }),
},
handler: async (ctx, input) => {
return { success: true };
},
});
// Create handlers with references for cleanup
const startedHandler = (eventName, agent, ctx) => {
ctx.logger.info('Started processing');
};
const completedHandler = (eventName, agent, ctx) => {
ctx.logger.info('Completed processing');
};
const erroredHandler = (eventName, agent, ctx, error) => {
ctx.logger.error('Error occurred', { error });
};
// Add listeners
agent.addEventListener('started', startedHandler);
agent.addEventListener('completed', completedHandler);
agent.addEventListener('errored', erroredHandler);
// Later, if needed, remove listeners
// agent.removeEventListener('started', startedHandler);
// agent.removeEventListener('completed', completedHandler);
// agent.removeEventListener('errored', erroredHandler);Advanced Features
File Imports
The Agentuity bundler supports importing various file types directly into your agent code. Files are processed at build time and embedded in your agent bundle, making them immediately available without disk I/O.
Supported File Types:
| File Extension | Data Type | Description |
|---|---|---|
.json | object | Parsed JSON data |
.yaml, .yml | object | Parsed YAML data |
.toml | object | Parsed TOML data |
.sql | string | SQL query content |
.txt | string | Text content |
.md | string | Markdown content |
.csv | string | CSV data |
.xml | string | XML content |
.html | string | HTML content |
.png, .jpg, .jpeg, .gif, .svg, .webp | string | Base64 data URL |
Usage Example:
import { createAgent } from '@agentuity/runtime';
import { z } from 'zod';
// Import various file types
import config from './config.json'; // object
import emailTemplate from './templates/welcome.txt'; // string
import getUserQuery from './queries/getUser.sql'; // string
import logo from './assets/logo.png'; // string (base64 data URL)
const agent = createAgent('ReportGenerator', {
schema: {
input: z.object({ userId: z.string(), format: z.enum(['html', 'text']) }),
output: z.object({ sent: z.boolean(), report: z.string() }),
},
handler: async (ctx, input) => {
// Use JSON config
const apiUrl = config.api.baseUrl;
// Use text template
const message = emailTemplate
.replace('{{userId}}', input.userId)
.replace('{{appName}}', config.appName);
// Use SQL query
const user = await database.query(getUserQuery, [input.userId]);
// Use image in HTML report
let report = 'Text report';
if (input.format === 'html') {
report = `
<html>
<body>
<img src="${logo}" alt="Logo" />
<h1>User Report</h1>
<p>User: ${user.name}</p>
</body>
</html>
`;
}
await sendEmail(input.userId, message);
return { sent: true, report };
},
});Key Features:
- Build-time processing: Files are embedded during build, not loaded at runtime
- No disk I/O: Data is immediately available in memory
- Automatic parsing: JSON and YAML files are automatically parsed into objects
- Type safety: TypeScript infers types for imported data
- Relative paths: Import from current directory, subdirectories, or parent directories
TypeScript Support:
For TypeScript projects, add type declarations:
// src/types/assets.d.ts
declare module '*.json' {
const value: any;
export default value;
}
declare module '*.sql' {
const value: string;
export default value;
}
declare module '*.png' {
const value: string; // Base64 data URL
export default value;
}
declare module '*.txt' {
const value: string;
export default value;
}Best Practices:
- Keep files small - Imported files are bundled with your code, increasing bundle size and deployment time
- Use for static data - Best for configuration, templates, and static assets that don't change frequently
- Consider external storage - For large datasets or frequently changing data, use Object Storage or Vector Storage APIs instead
- Version control - Commit imported files to your repository to keep them in sync with code
Notes:
- All imports are processed at build time, not runtime
- Imported files become part of your agent bundle
- File paths are relative to the importing file
- The bundler automatically handles file types by extension
Migrating from v0
For users upgrading from v0, the key architectural changes include:
- Agent definition: Function exports →
createAgent('Name', { ... })API - Handler signature:
(request, response, context)→(ctx, input) - Returns: Explicit
response.json()→ Direct returns with schema validation - Agent communication:
context.getAgent()→import agent from '@agent/name'; agent.run() - File structure: Single agent file →
src/agent/for agents,src/api/for routes - Context properties:
runId→sessionId, addedsession,thread,state - Package names:
@agentuity/sdk→@agentuity/runtime(or@agentuity/server)
For complete migration instructions, see the Migration Guide.
Need Help?
Join our Community for assistance or just to hang with other humans building agents.
Send us an email at hi@agentuity.com if you'd like to get in touch.
Please Follow us on
If you haven't already, please Signup for your free account now and start building your first agent!