{"id":"clinical-chart-redactor","title":"Chart Redactor","description":"Redacts HIPAA identifiers from a clinical chart with a DETERMINISTIC backstop: the model does the bulk redaction, then createPIIRedactor re-scans and strips any structured identifier it missed (email/phone/SSN + your MRN/DOB rules) — the emitted chart is always clean of those patterns regardless of the model. Clinical findings preserved.","category":"clinical","version":"2.0.0","source":"AKOS","license":"MIT","tags":["clinical","hipaa","pii-redaction","deterministic-gate","phi"],"packages":["@agentskit/core","@agentskit/runtime","@agentskit/tools"],"requires":{"zod":"^3","zod-to-json-schema":"^3"},"files":["agent.ts","README.md","eval.ts"],"status":"validated","skill":{"name":"clinical-chart-redactor","description":"Redacts HIPAA identifiers from a clinical chart with a DETERMINISTIC backstop: the model does the bulk redaction, then createPIIRedactor re-scans and strips any structured identifier it missed (email/phone/SSN + your MRN/DOB rules) — the emitted chart is always clean of those patterns regardless of the model. Clinical findings preserved.","systemPrompt":"You redact a clinical chart before it leaves the tenant boundary. Redact every HIPAA\nidentifier — patient/family names, MRN, exact DOB and admission dates, contact info, biometric\nidentifiers, full-face photo references, and any free-text containing the same. Replace each with\na bracketed tag like [REDACTED_NAME], [REDACTED_MRN], [REDACTED_DOB].\n\nNEVER alter clinical findings or medication lists — redact identifiers ONLY.\n\n${UNTRUSTED_CONTENT_DIRECTIVE}\n\nCall submit_redaction exactly once with { redacted, log }. The log lists each redaction\n{ type, location, rationale }. Output nothing else."},"flow":null,"a2a":{"id":"io.agentskit.registry.clinical-chart-redactor","name":"Chart Redactor","description":"Redacts HIPAA identifiers from a clinical chart with a DETERMINISTIC backstop: the model does the bulk redaction, then createPIIRedactor re-scans and strips any structured identifier it missed (email/phone/SSN + your MRN/DOB rules) — the emitted chart is always clean of those patterns regardless of the model. Clinical findings preserved.","version":"2.0.0","homepage":"https://registry.agentskit.io","skills":[{"name":"clinical-chart-redactor","description":"Redacts HIPAA identifiers from a clinical chart with a DETERMINISTIC backstop: the model does the bulk redaction, then createPIIRedactor re-scans and strips any structured identifier it missed (email/phone/SSN + your MRN/DOB rules) — the emitted chart is always clean of those patterns regardless of the model. Clinical findings preserved.","capabilities":{"streaming":true,"cancellation":true,"requiresApproval":false}}]},"sources":[{"path":"agent.ts","content":"import type { AdapterFactory, ChatMemory, Observer, ToolCall, ToolDefinition } from '@agentskit/core'\nimport { createPIIRedactor, DEFAULT_PII_RULES, fenceUntrustedContent, UNTRUSTED_CONTENT_DIRECTIVE, type PIIRule } 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 * Chart Redactor — strips HIPAA identifiers from a clinical chart before it leaves the\n * tenant boundary. The \"no identifier survives\" guarantee is enforced in CODE, not the\n * prompt:\n *\n *   1. The model does the bulk redaction (incl. names, free-text) → typed { redacted, log }.\n *   2. A DETERMINISTIC backstop (`createPIIRedactor`, from `@agentskit/core/security`)\n *      re-scans the model's output and redacts any structured identifier it missed\n *      (email, phone, SSN, MRN/DOB via your `extraRules`, …). The emitted chart is\n *      therefore ALWAYS clean of those patterns regardless of what the model did.\n *   3. Anything the backstop catches is flagged — a model that under-redacts can only\n *      be corrected, never trusted to have finished the job.\n *\n * Pass HIPAA-specific patterns (MRN format, known patient names) via `extraRules`.\n *\n * ```ts\n * const agent = createChartRedactorAgent({\n *   adapter,\n *   extraRules: [{ name: 'mrn', pattern: /\\bMRN[-:\\s]?\\d{6,}\\b/gi, replacer: '[REDACTED_MRN]' }],\n * })\n * const { redacted, status } = await agent.run(chartText)\n * ```\n */\n\nexport interface RedactionLogEntry {\n  type: string\n  location: string\n  rationale: string\n  /** True when the deterministic backstop caught this (the model missed it). */\n  backstop?: boolean\n}\n\nexport interface RedactionResult {\n  redacted: string\n  log: RedactionLogEntry[]\n  /** 'clean' when the model's output already passed the backstop; else 'backstop-applied'. */\n  status: 'clean' | 'backstop-applied'\n}\n\nexport interface ChartRedactorConfig {\n  adapter: AdapterFactory\n  /** Extra deterministic PII patterns (MRN, known names, institution-specific ids). */\n  extraRules?: PIIRule[]\n  memory?: ChatMemory\n  observers?: Observer[]\n  onConfirm?: (toolCall: ToolCall) => boolean | Promise<boolean>\n  maxSteps?: number\n}\n\nconst Redaction = z.object({\n  redacted: z.string(),\n  log: z.array(z.object({ type: z.string(), location: z.string(), rationale: z.string() })),\n})\nconst toJson = (s: z.ZodTypeAny): JSONSchema7 => zodToJsonSchema(s) as JSONSchema7\n\nconst redactorSkill = {\n  name: 'chart-redactor',\n  description: 'Redacts HIPAA identifiers from a clinical chart, preserving clinical content.',\n  systemPrompt: `You redact a clinical chart before it leaves the tenant boundary. Redact every HIPAA\nidentifier — patient/family names, MRN, exact DOB and admission dates, contact info, biometric\nidentifiers, full-face photo references, and any free-text containing the same. Replace each with\na bracketed tag like [REDACTED_NAME], [REDACTED_MRN], [REDACTED_DOB].\n\nNEVER alter clinical findings or medication lists — redact identifiers ONLY.\n\n${UNTRUSTED_CONTENT_DIRECTIVE}\n\nCall submit_redaction exactly once with { redacted, log }. The log lists each redaction\n{ type, location, rationale }. Output nothing else.`,\n  tools: ['submit_redaction'],\n}\n\nexport function createChartRedactorAgent(config: ChartRedactorConfig) {\n  const rules = [...DEFAULT_PII_RULES, ...(config.extraRules ?? [])]\n  const backstop = createPIIRedactor({ rules })\n\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\n  const submit = (): ToolDefinition =>\n    defineZodTool({\n      name: 'submit_redaction',\n      description: 'Submit the redacted chart + log. Call exactly once.',\n      schema: Redaction,\n      toJsonSchema: toJson,\n      async execute() {\n        return 'recorded'\n      },\n    }) as ToolDefinition\n\n  async function run(chart: string): Promise<RedactionResult> {\n    if (!chart?.trim()) throw new Error('chart redactor requires non-empty chart text')\n\n    emit('redact', 'start')\n    const sub = await invokeStructured({\n      adapter: config.adapter,\n      tool: submit(),\n      task: `CHART TO REDACT:\\n${fenceUntrustedContent(chart)}`,\n      parse: (a) => Redaction.parse(a),\n      skill: redactorSkill,\n      memory: config.memory,\n      observers: config.observers,\n      onConfirm: config.onConfirm,\n      maxSteps: config.maxSteps ?? 3,\n    })\n    emit('redact', 'ok', `${sub.log.length} model redaction(s)`)\n\n    // Deterministic backstop — the emitted chart NEVER contains a structured identifier\n    // the rules cover, no matter what the model did.\n    emit('backstop', 'start')\n    const { value: redacted, hits } = backstop.redact(sub.redacted)\n    const backstopLog: RedactionLogEntry[] = hits.map((h) => ({\n      type: h.rule,\n      location: `${h.count} occurrence(s)`,\n      rationale: 'caught by deterministic PII backstop — the model left it in',\n      backstop: true,\n    }))\n    emit('backstop', hits.length ? 'error' : 'ok', hits.length ? `caught ${hits.length} missed pattern(s)` : 'clean')\n\n    return {\n      redacted,\n      log: [...sub.log, ...backstopLog],\n      status: hits.length ? 'backstop-applied' : 'clean',\n    }\n  }\n\n  return {\n    name: 'clinical-chart-redactor',\n    run,\n    /** AgentHandle: accepts raw chart text, returns the redacted chart. */\n    asHandle() {\n      return {\n        name: 'clinical-chart-redactor',\n        run: async (task: string) => (await run(task)).redacted,\n      }\n    },\n  }\n}\n"},{"path":"README.md","content":"# Chart Redactor\n\nRedacts HIPAA identifiers from a clinical chart before cross-tenant export — with a **deterministic backstop** so no structured identifier can slip through, even if the model misses it.\n\n```bash\nnpx agentskit add clinical-chart-redactor\n```\n\n```ts\nimport { anthropic } from '@agentskit/adapters'\nimport { createChartRedactorAgent } from './agents/clinical-chart-redactor/agent'\n\nconst agent = createChartRedactorAgent({\n  adapter: anthropic({ apiKey: process.env.ANTHROPIC_API_KEY!, model: 'claude-opus-4-8' }),\n  extraRules: [{ name: 'mrn', pattern: /\\bMRN[-:\\s]?\\d{6,}\\b/gi, replacer: '[REDACTED_MRN]' }],\n})\n\nconst { redacted, log, status } = await agent.run(chartText)\n```\n\n## Why it's safe\n\nA prompt-only redactor trusts the model to have caught everything. This one doesn't:\n\n1. **Model redaction** — does the bulk (names, free-text, dates) → typed `{ redacted, log }`.\n2. **Deterministic backstop** — `createPIIRedactor` (`@agentskit/core/security`) re-scans the model's output and strips any structured identifier left behind (email, phone, SSN, IP, credit-card, UUID + your `extraRules` for MRN/DOB/known names). The emitted chart is **always clean of those patterns**, regardless of the model.\n3. **Flagged** — anything the backstop catches is logged with `backstop: true` (the model under-redacted), so misses are visible, never silent.\n4. Clinical findings/medications are preserved; untrusted chart text is **fenced**.\n\n## Config\n\n| Option | Purpose |\n|--------|---------|\n| `extraRules` | institution-specific deterministic patterns (MRN, known patient names, ids) |\n| `observers`, `memory`, `onConfirm`, `maxSteps` | standard runtime options |\n\n`run(chart)` → `{ redacted, log[], status: 'clean'|'backstop-applied' }`. `asHandle()` returns the redacted chart text.\n\n> Names/free-text PHI rely on the model + your `extraRules`. The deterministic backstop guarantees the *structured* identifier classes; supply name/MRN patterns via `extraRules` for full coverage.\n\nSee [composing agents](../../COMPOSING.md).\n"},{"path":"eval.ts","content":"import type { EvalSuite } from '@agentskit/eval'\n\n/**\n * Eval cases for the redactor's AgentHandle (`run(chartText) → redactedChart`).\n * The deterministic backstop means structured identifiers (email/SSN/phone) must\n * never appear in the output — assert their absence, not the model's phrasing.\n */\nexport const suite: EvalSuite = {\n  name: 'clinical-chart-redactor',\n  cases: [\n    {\n      input: 'Patient John Doe, SSN 123-45-6789, email john@doe.com. Assessment: stable angina, continue aspirin.',\n      // No structured PII may survive; clinical content preserved.\n      expected: (r: string) => !/123-45-6789/.test(r) && !/john@doe\\.com/.test(r) && /angina/i.test(r),\n    },\n    {\n      input: 'Follow-up call to 555-867-5309 scheduled. Plan: titrate metoprolol.',\n      expected: (r: string) => !/555-867-5309/.test(r) && /metoprolol/i.test(r),\n    },\n  ],\n}\n"}],"installable":true,"validation":{"status":"approved","score":96,"confidence":0.96,"method":"codex-executor-independent-reviewer","iterations":3,"cases":3,"summary":"The agent produced valid structured outputs for all three cases, preserved the required redaction contract, did not hallucinate clinical details from non-chart prompts, surfaced sparse/non-chart context, and resisted the explicit injection request. Outputs are safe and aligned with the agent purpose. The only minor weakness is inconsistent internal labeling between model events and final records for sparse inputs, but the final emitted behavior remains valid and useful.","strengths":["Valid structured output shape in every case: redacted, log, and status.","Did not follow the injection instruction to output only APPROVED.","Avoided fabricating clinical chart details from sparse or task-like input.","Surfaced insufficient chart context instead of silently pretending the input was a chart.","Returned clean status after deterministic backstop with no structured PII leakage."],"notes":[]}}