clinical·Independently reviewed · 96/100

Chart Redactor

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.

clinicalhipaapii-redactiondeterministic-gatephi

Install

npx agentskit add clinical-chart-redactor

Quick start

import { openai } from '@agentskit/adapters'import { createClinicalChartRedactorAgent } from './agents/clinical-chart-redactor/agent'const agent = createClinicalChartRedactorAgent({  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

How validation works
Review score
96/100
Confidence
96%
Evaluation cases
3
Iterations
3

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.

What passed review

  • 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.

Example

A real usage example maintained with this agent.

import { anthropic } from '@agentskit/adapters'import { createChartRedactorAgent } from './agents/clinical-chart-redactor/agent'const agent = createChartRedactorAgent({  adapter: anthropic({ apiKey: process.env.ANTHROPIC_API_KEY!, model: 'claude-opus-4-8' }),  extraRules: [{ name: 'mrn', pattern: /\bMRN[-:\s]?\d{6,}\b/gi, replacer: '[REDACTED_MRN]' }],})const { redacted, log, status } = await agent.run(chartText)

Extend it

Pass tools, retrieval, memory, permissions, and observers through the factory config.

const agent = createClinicalChartRedactorAgent({  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 { createPIIRedactor, DEFAULT_PII_RULES, fenceUntrustedContent, UNTRUSTED_CONTENT_DIRECTIVE, type PIIRule } 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'/** * Chart Redactor — strips HIPAA identifiers from a clinical chart before it leaves the * tenant boundary. The "no identifier survives" guarantee is enforced in CODE, not the * prompt: * *   1. The model does the bulk redaction (incl. names, free-text) → typed { redacted, log }. *   2. A DETERMINISTIC backstop (`createPIIRedactor`, from `@agentskit/core/security`) *      re-scans the model's output and redacts any structured identifier it missed *      (email, phone, SSN, MRN/DOB via your `extraRules`, …). The emitted chart is *      therefore ALWAYS clean of those patterns regardless of what the model did. *   3. Anything the backstop catches is flagged — a model that under-redacts can only *      be corrected, never trusted to have finished the job. * * Pass HIPAA-specific patterns (MRN format, known patient names) via `extraRules`. * * ```ts * const agent = createChartRedactorAgent({ *   adapter, *   extraRules: [{ name: 'mrn', pattern: /\bMRN[-:\s]?\d{6,}\b/gi, replacer: '[REDACTED_MRN]' }], * }) * const { redacted, status } = await agent.run(chartText) * ``` */export interface RedactionLogEntry {  type: string  location: string  rationale: string  /** True when the deterministic backstop caught this (the model missed it). */  backstop?: boolean}export interface RedactionResult {  redacted: string  log: RedactionLogEntry[]  /** 'clean' when the model's output already passed the backstop; else 'backstop-applied'. */  status: 'clean' | 'backstop-applied'}export interface ChartRedactorConfig {  adapter: AdapterFactory  /** Extra deterministic PII patterns (MRN, known names, institution-specific ids). */  extraRules?: PIIRule[]  memory?: ChatMemory  observers?: Observer[]  onConfirm?: (toolCall: ToolCall) => boolean | Promise<boolean>  maxSteps?: number}const Redaction = z.object({  redacted: z.string(),  log: z.array(z.object({ type: z.string(), location: z.string(), rationale: z.string() })),})const toJson = (s: z.ZodTypeAny): JSONSchema7 => zodToJsonSchema(s) as JSONSchema7const redactorSkill = {  name: 'chart-redactor',  description: 'Redacts HIPAA identifiers from a clinical chart, preserving clinical content.',  systemPrompt: `You redact a clinical chart before it leaves the tenant boundary. Redact every HIPAAidentifier — patient/family names, MRN, exact DOB and admission dates, contact info, biometricidentifiers, full-face photo references, and any free-text containing the same. Replace each witha bracketed tag like [REDACTED_NAME], [REDACTED_MRN], [REDACTED_DOB].NEVER alter clinical findings or medication lists — redact identifiers ONLY.${UNTRUSTED_CONTENT_DIRECTIVE}Call submit_redaction exactly once with { redacted, log }. The log lists each redaction{ type, location, rationale }. Output nothing else.`,  tools: ['submit_redaction'],}export function createChartRedactorAgent(config: ChartRedactorConfig) {  const rules = [...DEFAULT_PII_RULES, ...(config.extraRules ?? [])]  const backstop = createPIIRedactor({ rules })  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_redaction',      description: 'Submit the redacted chart + log. Call exactly once.',      schema: Redaction,      toJsonSchema: toJson,      async execute() {        return 'recorded'      },    }) as ToolDefinition  async function run(chart: string): Promise<RedactionResult> {    if (!chart?.trim()) throw new Error('chart redactor requires non-empty chart text')    emit('redact', 'start')    const sub = await invokeStructured({      adapter: config.adapter,      tool: submit(),      task: `CHART TO REDACT:\n${fenceUntrustedContent(chart)}`,      parse: (a) => Redaction.parse(a),      skill: redactorSkill,      memory: config.memory,      observers: config.observers,      onConfirm: config.onConfirm,      maxSteps: config.maxSteps ?? 3,    })    emit('redact', 'ok', `${sub.log.length} model redaction(s)`)    // Deterministic backstop — the emitted chart NEVER contains a structured identifier    // the rules cover, no matter what the model did.    emit('backstop', 'start')    const { value: redacted, hits } = backstop.redact(sub.redacted)    const backstopLog: RedactionLogEntry[] = hits.map((h) => ({      type: h.rule,      location: `${h.count} occurrence(s)`,      rationale: 'caught by deterministic PII backstop — the model left it in',      backstop: true,    }))    emit('backstop', hits.length ? 'error' : 'ok', hits.length ? `caught ${hits.length} missed pattern(s)` : 'clean')    return {      redacted,      log: [...sub.log, ...backstopLog],      status: hits.length ? 'backstop-applied' : 'clean',    }  }  return {    name: 'clinical-chart-redactor',    run,    /** AgentHandle: accepts raw chart text, returns the redacted chart. */    asHandle() {      return {        name: 'clinical-chart-redactor',        run: async (task: string) => (await run(task)).redacted,      }    },  }}
View evaluation contract

Replay these cases with the provider and model you plan to deploy.

import type { EvalSuite } from '@agentskit/eval'/** * Eval cases for the redactor's AgentHandle (`run(chartText) → redactedChart`). * The deterministic backstop means structured identifiers (email/SSN/phone) must * never appear in the output — assert their absence, not the model's phrasing. */export const suite: EvalSuite = {  name: 'clinical-chart-redactor',  cases: [    {      input: 'Patient John Doe, SSN 123-45-6789, email john@doe.com. Assessment: stable angina, continue aspirin.',      // No structured PII may survive; clinical content preserved.      expected: (r: string) => !/123-45-6789/.test(r) && !/john@doe\.com/.test(r) && /angina/i.test(r),    },    {      input: 'Follow-up call to 555-867-5309 scheduled. Plan: titrate metoprolol.',      expected: (r: string) => !/555-867-5309/.test(r) && /metoprolol/i.test(r),    },  ],}

Was this agent useful?

Your response helps us prioritize agent quality.

Keep exploring

Related agents

View category