Agents are the core building blocks of an Agentuity application. Use createAgent() to define typed handlers with schema validation, lifecycle hooks, and route middleware.
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 listeners
See Event System for available events and usage examples.
validator(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
},
});Agent Handler
The agent handler is the core function that processes inputs and produces outputs.
Handler Signature
The handler receives a context object and the validated input:
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
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,
};
},
});For concepts, LLM integration, and progressive examples, see Creating Agents.
Streaming Responses
Set schema.stream: true to declare that the handler returns a ReadableStream instead of a plain object. The runtime skips output validation for streaming agents and the AgentRunner gains a stream: true property.
import { createAgent } from '@agentuity/runtime';
import { s } from '@agentuity/schema';
import { streamText } from 'ai';
import { anthropic } from '@ai-sdk/anthropic';
export default createAgent('chat-stream', {
schema: {
input: s.object({ message: s.string() }),
// No output schema needed — the stream is the output
stream: true,
},
handler: async (ctx, input) => {
const { textStream } = streamText({
model: anthropic('claude-sonnet-4-5'),
prompt: input.message,
});
// textStream is a ReadableStream<string> — return it directly
return textStream;
},
});Handler return type: When stream: true, return any ReadableStream<string> or ReadableStream<Uint8Array>. AI SDK's textStream (from streamText) satisfies this directly.
For streaming patterns with routes, see HTTP Routes.
Route setup
Streaming agents require the stream() middleware in the route handler. Without it the response may be buffered:
import { createRouter, stream } from '@agentuity/runtime';
import chatAgent from '@agent/chat-stream';
const router = createRouter();
router.post('/chat', chatAgent.validator(), stream(async (c) => {
const body = c.req.valid('json');
return chatAgent.run(body);
}));
export default router;Consuming a stream
const response = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: 'Hello' }),
});
const reader = response.body?.getReader();
const decoder = new TextDecoder();
while (reader) {
const { done, value } = await reader.read();
if (done) break;
process.stdout.write(decoder.decode(value));
}For streaming patterns, chunked responses, and AI SDK integration, see Streaming Responses.