Reference

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

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 object
    • cors: Override default CORS settings
    • services: Override default services (KV, Vector, Stream)
      • useLocal: Use local services for development (default: false)
      • keyvalue: Custom KeyValueStorage implementation
      • vector: Custom VectorStorage implementation
      • stream: Custom StreamStorage implementation
    • setup: Async function called before server starts, returns app state available via ctx.app
    • shutdown: 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 does
    • schema (optional): Object containing input and output schemas
      • input: Schema for validating incoming data (Zod, Valibot, ArkType, or any StandardSchemaV1)
      • output: Schema for validating outgoing data
      • stream: Enable streaming responses (boolean, defaults to false)
    • handler: The agent function that processes inputs and returns outputs
    • setup (optional): Async function called once on app startup, returns agent-specific config accessible via ctx.config
    • shutdown (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 input
  • createEval(name, config): Create quality evaluations for this agent
  • addEventListener(eventName, callback): Attach lifecycle event listeners
  • removeEventListener(eventName, callback): Remove event listeners
  • validator(options?): Route validation middleware (see below)
  • inputSchema (conditional): Present if input schema is defined
  • outputSchema (conditional): Present if output schema is defined
  • stream (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 }): MiddlewareHandler

Options

  • 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 capabilities
  • input: 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:

Aspectv0v1
Parameters(request, response, context)(ctx, input)
Input accessawait request.data.json()Direct input parameter
Return patternreturn response.json(data)return data
ValidationManualAutomatic via schemas
Type safetyManual typesAuto-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 18

Return 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's setup() function. Fully typed based on what setup returns.
  • app: Application-wide state returned from createApp()'s setup() 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 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

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 storage
  • documents: One or more documents to upsert. Each document must include a unique key and 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 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(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 storage
  • key: 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 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

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 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 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');

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 execute

Note: 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.ts files for business logic (no HTTP access)
  • Router Context (Hono): Used in src/api/index.ts for 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:

FeatureRouter Context (Hono)Agent Context
TypeHono ContextAgentContext
Used insrc/api/index.tsagent.ts files
Request accessc.req (Hono Request)Direct input parameter (validated)
ResponseBuilder methods (.json(), .text())Direct returns
Servicesc.var.kv, c.var.logger, etc.ctx.kv, ctx.logger, etc.
Agent callingImport and call: agent.run()Import and call: agent.run()
State managementVia Hono middlewareBuilt-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 endpoint
  • handler: 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 endpoint
  • handler: 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 endpoint
  • handler: 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 run

Binary 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:

  1. Agent handler executes and returns output
  2. Output is validated against schema
  3. Agent emits completed event
  4. All evals attached to the agent run via waitUntil()
  5. Eval results are sent to eval tracking service
  6. 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 later

Complete 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 ExtensionData TypeDescription
.jsonobjectParsed JSON data
.yaml, .ymlobjectParsed YAML data
.tomlobjectParsed TOML data
.sqlstringSQL query content
.txtstringText content
.mdstringMarkdown content
.csvstringCSV data
.xmlstringXML content
.htmlstringHTML content
.png, .jpg, .jpeg, .gif, .svg, .webpstringBase64 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: runIdsessionId, added session, thread, state
  • Package names: @agentuity/sdk@agentuity/runtime (or @agentuity/server)

For complete migration instructions, see the Migration Guide.


Need Help?

Join our DiscordCommunity 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!