Learning Paths & Tutorials
AI Agents
Build typed, sandboxed LLM agents.
This tutorial walks you through building a PURISTA AI agent that classifies support tickets, calls business commands as tools, and returns structured, validated output.
AI agents in PURISTA are first-class citizens of the message model. They run as queue-driven workers — they consume a job from a queue bridge, interact with LLM models and tool commands, and return a validated structured result. Like any other service component, they are typed with Zod schemas, sandboxed through the context object, and testable without a running LLM.
What you will build
A triage agent that:
- Receives a support ticket (text)
- Uses an LLM to classify priority (low/normal/high)
- Calls a business command to fetch customer tier
- Returns a structured result with priority and reason
Prerequisites
- PURISTA project with
@purista/coreinstalled @purista/ai-harnessinstalled (required forgetAgentQueueBuildersupport)- An AI provider package installed (e.g.
@purista/harness-openai)
Step 1: Create the service
import { ServiceBuilder } from '@purista/core'
export const supportV1ServiceBuilder = new ServiceBuilder({
serviceName: 'support',
serviceVersion: '1',
serviceDescription: 'Support workflows',
})
Step 2: Define the agent
const triageAgent = supportV1ServiceBuilder
.getAgentQueueBuilder('triage', 'Classify incoming support tickets')
.addPayloadSchema(z.object({
ticketId: z.string(),
text: z.string(),
}))
.addOutputSchema(z.object({
priority: z.enum(['low', 'normal', 'high']),
reason: z.string(),
}))
Step 3: Add a model
.addModel('primary', {
model: 'gpt-4.1-mini',
capabilities: ['object'],
defaults: {
temperature: 0,
maxTokens: 1200,
},
})
Step 4: Add a command tool
Allow the agent to call a business command:
.canInvoke('customer', '1', 'getProfile', {
payloadSchema: z.object({ customerId: z.string() }),
outputSchema: z.object({
tier: z.enum(['standard', 'enterprise']),
openIncidents: z.number(),
}),
})
Step 5: Implement the run function
.setRunFunction(async function(context, payload) {
// Fetch customer profile using the tool
const profile = await context.invoke.tools['customer.1.getProfile'].call({
customerId: payload.customerId,
})
// Ask the model to classify
const result = await context.harness.models.primary.object({
messages: [{
role: 'user',
content: `Customer tier: ${profile.tier}, Open incidents: ${profile.openIncidents}. Ticket: ${payload.text}`,
}],
schema: {
type: 'object',
properties: {
priority: { enum: ['low', 'normal', 'high'] },
reason: { type: 'string' },
},
required: ['priority', 'reason'],
},
}, context.signal)
return result.object
})
Step 6: Add to service and start
Assemble the service from the builder by registering the agent definition, then get a runtime instance by passing the event bridge and configuration:
import { supportV1ServiceBuilder } from './supportV1ServiceBuilder.js'
export const supportV1Service = supportV1ServiceBuilder
.addAgentDefinition(await triageAgent.getDefinition())
const supportService = await supportV1Service.getInstance(eventBridge, {
queueBridge,
ai: {
models: {
primary: {
provider: openaiProvider,
model: 'gpt-4.1-mini',
capabilities: ['object'],
},
},
},
})
Step 7: Test
const result = await context.service.support['1'].triage({
ticketId: 'TICKET-123',
text: 'Cannot log in to the dashboard',
}, {})
console.log(result.priority) // 'high' or 'normal' or 'low'
console.log(result.reason) // 'Customer is enterprise with open incidents'
What you learned
- Agents are queue-driven workflows with typed schemas
- Models are declared with capabilities and bound at runtime
- Tools are allowlisted business commands with typed schemas
context.signalenables cancellation propagation- Outputs are validated against Zod schemas
Next steps
- Add sandboxing for file access
- Use child agents for complex workflows
- Configure long-running execution for slow tasks