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: CreateAgentConfig): 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. Return agent-specific config forctx.configshutdown(optional): Async cleanup function called on app shutdown with the setup result
Return Value
Returns an AgentRunner object with the following properties:
metadata: Agent metadata (id,agentId,filename,version,name,description)run(input): Execute the agent. Input is required when the agent has an input schema, omitted otherwise.createEval(name, config): Deprecated evaluation hook kept for older appsaddEventListener(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/agent'; 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 { s } from '@agentuity/schema';
const agent = createAgent('CachedProcessor', {
schema: {
input: s.object({ key: s.string() }),
output: s.object({ value: s.string() }),
},
// Called once when the app starts, return value is available via ctx.config
setup: async () => {
const cache = new Map<string, string>();
const client = await initializeExternalService();
return { cache, client };
},
// Called when the 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 runs once when the app starts and returns agent-specific configuration available via ctx.config. This is useful for:
- Agent-specific caches or connection pools
- Pre-computed data or models
- External service clients
Agent setup() and shutdown() are current and useful. The type system still includes app-level state plumbing, but ctx.app defaults to {}. Prefer module-scoped shared dependencies plus registerShutdownHook() instead of new examples built around ctx.app.
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
For streaming agents, call agent.validator() normally. The runtime skips output validation when schema.stream: true is set. Use stream: true only with standalone validator() routes that return a stream without an agent.
Example
import { Hono } from 'hono';
import { z } from 'zod';
import type { Env } from '@agentuity/runtime';
import agent from './agent';
const router = new Hono<Env>();
// 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.unknown() }),
},
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 a 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 can be returned directly from a validated route with c.body(...):
import { Hono } from 'hono';
import type { Env } from '@agentuity/runtime';
import chatAgent from '@agent/chat-stream/agent';
const router = new Hono<Env>();
router.post('/chat', chatAgent.validator(), async (c) => {
const body = c.req.valid('json');
return c.body(await chatAgent.run(body));
});
export default router;Use stream() when the route builds the ReadableStream itself. For validated non-agent streams, use the standalone validator({ input, stream: true }) helper from @agentuity/runtime.
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.