# AI Agents

Build typed, sandboxed LLM agents with models, tools, and explicit execution policies.

---
Canonical: /handbook/learn/ai-agent-tutorial/
Source: web/src/content/handbook-cards/learn/ai-agent-tutorial.mdx
Format: Markdown for 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:

1. Receives a support ticket (text)
2. Uses an LLM to classify priority (low/normal/high)
3. Calls a business command to fetch customer tier
4. Returns a structured result with priority and reason

## Prerequisites

- PURISTA project with `@purista/core` installed
- `@purista/core` installed; add a model provider package such as `@purista/harness-openai` when running against a live provider
- An AI provider package installed (e.g. `@purista/harness-openai`)

## Step 1: Create the service

```typescript [supportV1ServiceBuilder.ts]
import { ServiceBuilder } from '@purista/core'

export const supportV1ServiceBuilder = new ServiceBuilder({
  serviceName: 'support',
  serviceVersion: '1',
  serviceDescription: 'Support workflows',
})
```

## Step 2: Define the agent

```typescript [triageAgent.ts]
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

```typescript [model.ts]
.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:

```typescript [tool.ts]
.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

```typescript [run.ts]
.setRunFunction(async context => {
  // Fetch customer profile using the tool
  const profile = await context.invoke.tools['customer.1.getProfile'].call({
    customerId: context.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: ${context.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:

```typescript [service.ts]
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

```typescript [test.ts]
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.signal` enables cancellation propagation
- Outputs are validated against Zod schemas

## Next steps

- Add [sandboxing](/handbook/patterns/agent-patterns/) for file access
- Use [child agents](/handbook/patterns/agent-patterns/) for complex workflows
- Configure [long-running execution](/handbook/patterns/agent-patterns/) for slow tasks
