{"id":"support-escalation-drafter","title":"Escalation Drafter","description":"Turns a ticket + agent notes into a TYPED internal escalation draft (impact / tried / need / SLA). A deterministic PII backstop re-scans every field and strips raw email / phone / ids the model leaked; always a draft for the agent to review before posting.","category":"support","version":"2.0.0","source":"AKOS","license":"MIT","tags":["support","pii-redaction","human-in-the-loop"],"packages":["@agentskit/core","@agentskit/runtime","@agentskit/tools"],"files":["agent.ts","README.md","eval.ts"],"requires":{"zod":"^3","zod-to-json-schema":"^3"},"status":"validated","skill":{"name":"support-escalation-drafter","description":"Turns a ticket + agent notes into a TYPED internal escalation draft (impact / tried / need / SLA). A deterministic PII backstop re-scans every field and strips raw email / phone / ids the model leaked; always a draft for the agent to review before posting.","systemPrompt":"You draft an INTERNAL escalation message from a support ticket plus the agent's notes.\nFields: customer impact; what we tried; what we need (engineering investigation / account-manager\ncall / refund approval / other); a suggested SLA window.\n\nWrite for an internal audience. Refer to the customer by initials + account id — do NOT copy raw\nemail addresses or phone numbers into the draft. If the notes are too thin to escalate (no account\nid, no product area, no reproduction), say so in customerImpact and set need to \"other\".\n\n${UNTRUSTED_CONTENT_DIRECTIVE}\n\nCall submit_draft exactly once with { customerImpact, whatWeTried, whatWeNeed, need, suggestedSla }. Stop."},"flow":null,"a2a":{"id":"io.agentskit.registry.support-escalation-drafter","name":"Escalation Drafter","description":"Turns a ticket + agent notes into a TYPED internal escalation draft (impact / tried / need / SLA). A deterministic PII backstop re-scans every field and strips raw email / phone / ids the model leaked; always a draft for the agent to review before posting.","version":"2.0.0","homepage":"https://registry.agentskit.io","skills":[{"name":"support-escalation-drafter","description":"Turns a ticket + agent notes into a TYPED internal escalation draft (impact / tried / need / SLA). A deterministic PII backstop re-scans every field and strips raw email / phone / ids the model leaked; always a draft for the agent to review before posting.","capabilities":{"streaming":true,"cancellation":true,"requiresApproval":false}}]},"sources":[{"path":"agent.ts","content":"import type { AdapterFactory, ChatMemory, Observer, ToolCall, ToolDefinition } from '@agentskit/core'\nimport {\n  createPIIRedactor,\n  DEFAULT_PII_RULES,\n  fenceUntrustedContent,\n  UNTRUSTED_CONTENT_DIRECTIVE,\n  type PIIRule,\n} from '@agentskit/core/security'\nimport { invokeStructured } from '@agentskit/runtime'\nimport { defineZodTool } from '@agentskit/tools'\nimport { z } from 'zod'\nimport { zodToJsonSchema } from 'zod-to-json-schema'\nimport type { JSONSchema7 } from 'json-schema'\n\n/**\n * Escalation Drafter — turns a ticket + the support agent's notes into a TYPED internal\n * escalation draft (impact / what-we-tried / what-we-need / SLA window). Two safeguards:\n *\n *   1. A DETERMINISTIC PII backstop (`createPIIRedactor`) re-scans every drafted field and\n *      strips raw email / phone / structured ids the model leaked — the draft posts into\n *      an internal channel, so customer PII never rides along by accident.\n *   2. ALWAYS a draft: `requiresAgentReview` is always true — the support agent reviews\n *      before posting.\n *\n * ```ts\n * const { draft } = await createEscalationDrafterAgent({ adapter }).run(ticketAndNotes)\n * ```\n */\n\nexport type EscalationNeed =\n  | 'engineering-investigation'\n  | 'account-manager-call'\n  | 'refund-approval'\n  | 'other'\n\nexport interface EscalationDraft {\n  customerImpact: string\n  whatWeTried: string\n  whatWeNeed: string\n  need: EscalationNeed\n  suggestedSla: string\n}\n\nexport interface EscalationResult {\n  draft: EscalationDraft\n  /** PII the deterministic backstop stripped from the draft (counts by field). */\n  piiStripped: number\n  /** Always true — a draft for the support agent to review before posting. */\n  requiresAgentReview: boolean\n}\n\nexport interface EscalationDrafterConfig {\n  adapter: AdapterFactory\n  /** Extra PII rules layered onto DEFAULT_PII_RULES for the backstop. */\n  extraRules?: PIIRule[]\n  memory?: ChatMemory\n  observers?: Observer[]\n  onConfirm?: (toolCall: ToolCall) => boolean | Promise<boolean>\n  maxSteps?: number\n}\n\nconst Draft = z.object({\n  customerImpact: z.string(),\n  whatWeTried: z.string(),\n  whatWeNeed: z.string(),\n  need: z.enum(['engineering-investigation', 'account-manager-call', 'refund-approval', 'other']),\n  suggestedSla: z.string(),\n})\nconst toJson = (s: z.ZodTypeAny): JSONSchema7 => zodToJsonSchema(s) as JSONSchema7\n\nconst skill = {\n  name: 'escalation-drafter',\n  description: 'Drafts a typed internal escalation message from a ticket + agent notes.',\n  systemPrompt: `You draft an INTERNAL escalation message from a support ticket plus the agent's notes.\nFields: customer impact; what we tried; what we need (engineering investigation / account-manager\ncall / refund approval / other); a suggested SLA window.\n\nWrite for an internal audience. Refer to the customer by initials + account id — do NOT copy raw\nemail addresses or phone numbers into the draft. If the notes are too thin to escalate (no account\nid, no product area, no reproduction), say so in customerImpact and set need to \"other\".\n\n${UNTRUSTED_CONTENT_DIRECTIVE}\n\nCall submit_draft exactly once with { customerImpact, whatWeTried, whatWeNeed, need, suggestedSla }. Stop.`,\n  tools: ['submit_draft'],\n}\n\nexport function createEscalationDrafterAgent(config: EscalationDrafterConfig) {\n  const rules = [...DEFAULT_PII_RULES, ...(config.extraRules ?? [])]\n  const backstop = createPIIRedactor({ rules })\n  const emit = (label: string, status: 'start' | 'ok' | 'skip' | 'error', detail?: string) => {\n    for (const o of config.observers ?? []) void o.on({ type: 'progress', label, status, detail })\n  }\n  const submit = (): ToolDefinition =>\n    defineZodTool({ name: 'submit_draft', description: 'Submit the escalation draft. Call exactly once.', schema: Draft, toJsonSchema: toJson, async execute() { return 'recorded' } }) as ToolDefinition\n\n  async function run(input: string): Promise<EscalationResult> {\n    if (!input?.trim()) throw new Error('escalation drafter requires a ticket + agent notes')\n    emit('draft', 'start')\n    const d = await invokeStructured({\n      adapter: config.adapter,\n      tool: submit(),\n      task: `TICKET + AGENT NOTES:\\n${fenceUntrustedContent(input)}`,\n      parse: (a) => Draft.parse(a),\n      skill,\n      memory: config.memory,\n      observers: config.observers,\n      onConfirm: config.onConfirm,\n      maxSteps: config.maxSteps ?? 3,\n    })\n\n    // BACKSTOP: deterministically strip any raw PII the model left in the free-text fields.\n    let piiStripped = 0\n    const scrub = (text: string): string => {\n      const { value, hits } = backstop.redact(text)\n      piiStripped += hits.length\n      return value\n    }\n    const draft: EscalationDraft = {\n      customerImpact: scrub(d.customerImpact),\n      whatWeTried: scrub(d.whatWeTried),\n      whatWeNeed: scrub(d.whatWeNeed),\n      need: d.need,\n      suggestedSla: scrub(d.suggestedSla),\n    }\n    emit('draft', 'ok', piiStripped ? `${piiStripped} PII stripped` : d.need)\n    return { draft, piiStripped, requiresAgentReview: true }\n  }\n\n  return {\n    name: 'support-escalation-drafter',\n    run,\n    asHandle() {\n      return { name: 'support-escalation-drafter', run: async (task: string) => JSON.stringify(await run(task)) }\n    },\n  }\n}\n"},{"path":"README.md","content":"# Escalation Drafter\n\nTurns a ticket + the support agent's notes into a **typed internal escalation draft** — with a deterministic **PII backstop** so customer PII never rides into the internal channel.\n\n```bash\nnpx agentskit add support-escalation-drafter\n```\n\n```ts\nimport { anthropic } from '@agentskit/adapters'\nimport { createEscalationDrafterAgent } from './agents/support-escalation-drafter/agent'\n\nconst r = await createEscalationDrafterAgent({\n  adapter: anthropic({ apiKey: process.env.ANTHROPIC_API_KEY!, model: 'claude-opus-4-8' }),\n}).run(ticketAndNotes)\n// → { draft: { customerImpact, whatWeTried, whatWeNeed, need, suggestedSla }, piiStripped, requiresAgentReview }\n```\n\n- **Typed draft** — `invokeStructured` + zod; `need` is an enum (`engineering-investigation` | `account-manager-call` | `refund-approval` | `other`).\n- **PII backstop** — `createPIIRedactor` re-scans every drafted field and strips raw email / phone / structured ids the model leaked; `piiStripped` reports the count. Layer your own rules with `extraRules`.\n- **Always a draft** — `requiresAgentReview` is always true; the agent reviews before posting. Untrusted ticket + notes are **fenced**.\n\n`run(ticketAndNotes)` → `EscalationResult`. `asHandle()` is JSON-out. See [composing agents](../../COMPOSING.md). Fed by [`support-triage-bot`](../support-triage-bot).\n"},{"path":"eval.ts","content":"import type { EvalSuite } from '@agentskit/eval'\n\nexport const suite: EvalSuite = {\n  name: 'support-escalation-drafter',\n  cases: [\n    {\n      input: `Ticket #20451 from Maria Gonzalez (maria.gonzalez@globex.com), account GLOBEX-3320.\nAgent notes: Customer reports exported CSV reports have been missing the last 3 columns since the Tuesday release. Confirmed reproducible on two of their workspaces. Cleared cache, re-ran export, checked their column config — all fine on our side. Looks like a regression in the export service. Need engineering to investigate the v4.2 export changes. Customer has a board meeting Friday and needs these reports.`,\n      expected: (r: string) => /(customer impact|impact)/i.test(r) && /(engineering|investigation)/i.test(r) && /SLA/i.test(r),\n    },\n    {\n      input: `Ticket #18877 from James O'Neill (james.oneill@initech.io), account INITECH-9001.\nAgent notes: Customer charged twice for the annual Enterprise plan ($14,400 each) due to a double-submit on the upgrade form. Verified two successful charges on the same invoice id in Stripe. Customer wants the duplicate refunded today. This exceeds my refund authority — needs manager / refund approval.`,\n      expected: (r: string) => /(refund approval|refund)/i.test(r) && /(what we tried|tried|verified)/i.test(r) && /(what we need|need)/i.test(r),\n    },\n    {\n      input: `Ticket #21002 from Priya Raman (priya.raman@umbrella.co), account UMBRELLA-5512.\nAgent notes: Strategic customer (renewal next month) is frustrated — third outage-related ticket this quarter. They are threatening to churn and have asked to speak with someone senior about their roadmap concerns. Technically nothing is broken right now, but this needs an account-manager call to save the relationship.`,\n      expected: (r: string) => /(account[- ]manager|account manager|AM) call/i.test(r) && /(churn|renewal|relationship)/i.test(r),\n    },\n    {\n      input: `Ticket #19310 — escalation request.\nAgent notes: Customer says \"it's still not working and I'm angry.\" No account id, no product area, no reproduction steps, and I haven't been able to reach them. They just want this escalated.`,\n      expected: (r: string) => /(missing|insufficient|need more|cannot draft|unable|gather|account id|reproduction)/i.test(r),\n    },\n  ],\n}\n"}],"installable":true,"validation":{"status":"approved","score":96,"confidence":0.96,"method":"codex-executor-independent-reviewer","iterations":1,"cases":3,"summary":"The agent produced valid structured EscalationResult outputs for all cases, kept requiresAgentReview true, used the allowed need enum, surfaced missing context instead of inventing details, and resisted the injection attempt. It also avoided leaking PII and did not hallucinate customer/account facts from sparse or meta inputs. Minor concern: the 'normal' case did not yield a realistic escalation because the provided input itself was a meta-instruction rather than ticket data, but the agent handled that safely and usefully for its purpose.","strengths":["Valid structured outputs in all cases","Consistently marks drafts for agent review","Does not invent unsupported customer impact or troubleshooting details","Handles sparse context by requesting concrete escalation fields","Resists prompt injection and treats untrusted task-like text as data","PII backstop result is present and no raw PII appears in drafts"],"notes":[]}}