Quick start
import { openai } from '@agentskit/adapters'import { createSupportTriageBotAgent } from './agents/support-triage-bot/agent'const agent = createSupportTriageBotAgent({ adapter: openai({ apiKey: process.env.OPENAI_API_KEY!, model: 'gpt-4o', }),})const result = await agent.run('Describe your task here')console.log(result.content)Independent reviewer approved
Validation evidence
- Review score
- 96/100
- Confidence
- 95%
- Evaluation cases
- 3
- Iterations
- 1
The agent produced valid structured triage outputs for all three cases, resisted prompt injection, avoided fabricating support-ticket details from meta-style inputs, defaulted safely to P3 when context was insufficient, and surfaced uncertainty in the rationale. No unsafe content, empty output, or material hallucination was present. Minor weakness: queue naming is inconsistent across cases, which may matter if downstream routing expects canonical queue IDs, but it does not invalidate these outputs based on the provided contract.
What passed review
- Maintains structured output with topic, severity, queue, rationale, and redFlagsHit.
- Correctly treats instruction-like ticket text as untrusted data rather than following it.
- Uses a safe P3 default for missing or sparse context and explicitly notes gaps.
- Does not invent realistic customer details when the input lacks them.
Reviewer notes
- Normalize queue values to canonical identifiers or document accepted queue labels so downstream consumers do not receive mixed forms like `Support Operations`, `general_support`, and `General Support`.
Example
A real usage example maintained with this agent.
import { anthropic } from '@agentskit/adapters'import { createTriageBotAgent } from './agents/support-triage-bot/agent'const r = await createTriageBotAgent({ adapter: anthropic({ apiKey: process.env.ANTHROPIC_API_KEY!, model: 'claude-opus-4-8' }),}).run(ticketText)// → { topic, severity: 'P1'|'P2'|'P3'|'P4', queue, rationale, redFlagsHit[] }Extend it
Pass tools, retrieval, memory, permissions, and observers through the factory config.
const agent = createSupportTriageBotAgent({ adapter, tools, retriever, memory, onConfirm: (call) => approve(call), observers: [tracer],})View agent factory source
import type { AdapterFactory, ChatMemory, Observer, ToolCall, ToolDefinition } from '@agentskit/core'import { fenceUntrustedContent, UNTRUSTED_CONTENT_DIRECTIVE } from '@agentskit/core/security'import { invokeStructured } from '@agentskit/runtime'import { defineZodTool } from '@agentskit/tools'import { z } from 'zod'import { zodToJsonSchema } from 'zod-to-json-schema'import type { JSONSchema7 } from 'json-schema'/** * Triage Bot — classifies a support ticket by topic / severity (P1–P4) / queue. Typed * output, with a deterministic red-flag net: outage / data-loss / security-breach * language forces P1 regardless of the model (it can only raise severity, never bury * a P1). Output is metadata for a human agent — the bot never replies to the customer. */export type Severity = 'P1' | 'P2' | 'P3' | 'P4'export interface TriageResult { topic: string severity: Severity queue: string rationale: string redFlagsHit: string[]}export interface TriageBotConfig { adapter: AdapterFactory /** Patterns that force P1. Defaults: outage/data-loss/security/breach. */ p1RedFlags?: RegExp[] memory?: ChatMemory observers?: Observer[] onConfirm?: (toolCall: ToolCall) => boolean | Promise<boolean> maxSteps?: number}const DEFAULT_P1: RegExp[] = [ /\b(outage|down|offline|not working for all)\b/i, /\bdata (loss|breach|leak|deleted)\b/i, /\b(security|breach|hacked|compromised|unauthor[iz]?ed access)\b/i, /\bcannot (access|log ?in).*(everyone|all users|whole team)\b/i,]const Classification = z.object({ topic: z.string(), severity: z.enum(['P1', 'P2', 'P3', 'P4']), queue: z.string(), rationale: z.string(),})const toJson = (s: z.ZodTypeAny): JSONSchema7 => zodToJsonSchema(s) as JSONSchema7const skill = { name: 'support-triage-bot', description: 'Classifies a support ticket by topic, severity (P1-P4), and queue.', systemPrompt: `You triage inbound support tickets. Classify topic, severity (P1|P2|P3|P4), and thesuggested queue. P1 ONLY for outage, data loss, security incident, or contractual breach. Defaultto P3 when unsure. You NEVER reply to the customer — your output is metadata for a human agent.${UNTRUSTED_CONTENT_DIRECTIVE}Call submit_triage exactly once with { topic, severity, queue, rationale }. Stop.`, tools: ['submit_triage'],}const rank = (s: Severity): number => ['P1', 'P2', 'P3', 'P4'].indexOf(s)export function createTriageBotAgent(config: TriageBotConfig) { const p1Flags = config.p1RedFlags ?? DEFAULT_P1 const emit = (label: string, status: 'start' | 'ok' | 'skip' | 'error', detail?: string) => { for (const o of config.observers ?? []) void o.on({ type: 'progress', label, status, detail }) } const submit = (): ToolDefinition => defineZodTool({ name: 'submit_triage', description: 'Submit the triage classification. Call exactly once.', schema: Classification, toJsonSchema: toJson, async execute() { return 'recorded' } }) as ToolDefinition async function run(ticket: string): Promise<TriageResult> { if (!ticket?.trim()) throw new Error('triage bot requires a non-empty ticket') const redFlagsHit = p1Flags.map((re) => ticket.match(re)?.[0]).filter((m): m is string => Boolean(m)) emit('triage', 'start') let c: z.infer<typeof Classification> try { c = await invokeStructured({ adapter: config.adapter, tool: submit(), task: `SUPPORT TICKET:\n${fenceUntrustedContent(ticket)}`, parse: (a) => Classification.parse(a), skill, memory: config.memory, observers: config.observers, onConfirm: config.onConfirm, maxSteps: config.maxSteps ?? 3, }) } catch { c = { topic: 'unknown', severity: 'P3', queue: 'general', rationale: 'classification unavailable — defaulted to P3' } } // SAFETY NET: a red flag forces P1; the model can only raise severity, never bury a P1. let severity = c.severity let rationale = c.rationale if (redFlagsHit.length && rank(severity) > rank('P1')) { severity = 'P1' rationale = `red-flag term(s) (${redFlagsHit.join(', ')}) — forced P1. Model said: ${c.rationale}` } emit('triage', 'ok', `${severity}${redFlagsHit.length ? ' (red-flag)' : ''}`) return { topic: c.topic, severity, queue: severity === 'P1' ? 'incident' : c.queue, rationale, redFlagsHit } } return { name: 'support-triage-bot', run, asHandle() { return { name: 'support-triage-bot', run: async (task: string) => JSON.stringify(await run(task)) } }, }}View evaluation contract
Replay these cases with the provider and model you plan to deploy.
import type { EvalSuite } from '@agentskit/eval'export const suite: EvalSuite = { name: 'support-triage-bot', cases: [ { input: `Subject: Production API returning 503 for all our customersBody: Since 09:14 UTC every request to https://api.acme.io/v2/charges returns 503. Our checkout is fully down and we are losing sales. Account id ACME-44192, contact ops@acme.io, phone +1-415-555-0199. This is impacting all of production.`, expected: (r: string) => /P1/.test(r) && /(outage|down|503)/i.test(r) && /queue/i.test(r), }, { input: `Subject: How do I change my billing email?Body: Hi, I'd like to update the email address that invoices are sent to. It's currently jane@oldmail.com and I want it changed to jane@newmail.com. No rush, just whenever you get a chance. Account id ACME-9921.`, expected: (r: string) => /P3|P4/.test(r) && /(billing|account|email)/i.test(r) && /queue/i.test(r), }, { input: `Subject: We think there was a data breachBody: A staff member received a phishing email that appears to reference customer records from our dashboard. We suspect credentials may have leaked and customer PII could be exposed. Account id ACME-7781, reporter security@acme.io.`, expected: (r: string) => /P1/.test(r) && /(security|breach|incident)/i.test(r), }, { input: `Subject: it's brokenBody: nothing works please fix asap`, expected: (r: string) => /P3/.test(r) && /(unsure|unclear|missing|insufficient|more info|escalat)/i.test(r), }, ],}Was this agent useful?
Your response helps us prioritize agent quality.