{"id":"clinical-note-summariser","title":"SOAP Generator","description":"Converts clinician dictation into a TYPED SOAP note (Subjective/Objective/Assessment/Plan) via structured output; preserves facts verbatim, surfaces missing sections instead of inventing them, always a draft for clinician sign-off.","category":"clinical","version":"2.0.0","source":"AKOS","license":"MIT","tags":["clinical","soap","summarization","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":"clinical-note-summariser","description":"Converts clinician dictation into a TYPED SOAP note (Subjective/Objective/Assessment/Plan) via structured output; preserves facts verbatim, surfaces missing sections instead of inventing them, always a draft for clinician sign-off.","systemPrompt":"You convert clinician dictation into a SOAP note: Subjective, Objective, Assessment, Plan.\nPreserve clinical facts verbatim. Do NOT infer diagnoses the clinician did not state. Standardise\nunits (mg, mL, bpm, mmHg). If a section is not covered in the dictation, leave it empty and add\nits name to missingFields — never invent content.\n\n${UNTRUSTED_CONTENT_DIRECTIVE}\n\nCall submit_note exactly once with { subjective, objective, assessment, plan, missingFields }. Stop."},"flow":null,"a2a":{"id":"io.agentskit.registry.clinical-note-summariser","name":"SOAP Generator","description":"Converts clinician dictation into a TYPED SOAP note (Subjective/Objective/Assessment/Plan) via structured output; preserves facts verbatim, surfaces missing sections instead of inventing them, always a draft for clinician sign-off.","version":"2.0.0","homepage":"https://registry.agentskit.io","skills":[{"name":"clinical-note-summariser","description":"Converts clinician dictation into a TYPED SOAP note (Subjective/Objective/Assessment/Plan) via structured output; preserves facts verbatim, surfaces missing sections instead of inventing them, always a draft for clinician sign-off.","capabilities":{"streaming":true,"cancellation":true,"requiresApproval":false}}]},"sources":[{"path":"agent.ts","content":"import type { AdapterFactory, ChatMemory, Observer, ToolCall, ToolDefinition } from '@agentskit/core'\nimport { fenceUntrustedContent, UNTRUSTED_CONTENT_DIRECTIVE } 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 * SOAP Generator — turns clinician dictation into a typed SOAP note (Subjective,\n * Objective, Assessment, Plan). Structured output (each section addressable, not one\n * blob), missing sections surfaced (never silently blank), and ALWAYS a draft for\n * clinician sign-off.\n *\n * ```ts\n * const { note, missingFields } = await createNoteSummariserAgent({ adapter }).run(dictation)\n * ```\n */\n\nexport interface SoapNote {\n  subjective: string\n  objective: string\n  assessment: string\n  plan: string\n}\n\nexport interface NoteResult {\n  note: SoapNote\n  /** SOAP sections the dictation didn't cover — for the clinician to fill, not invent. */\n  missingFields: string[]\n  /** Always true — output is a draft, never a finalised record. */\n  requiresClinicianSignoff: boolean\n}\n\nexport interface NoteSummariserConfig {\n  adapter: AdapterFactory\n  memory?: ChatMemory\n  observers?: Observer[]\n  onConfirm?: (toolCall: ToolCall) => boolean | Promise<boolean>\n  maxSteps?: number\n}\n\nconst Soap = z.object({\n  subjective: z.string(),\n  objective: z.string(),\n  assessment: z.string(),\n  plan: z.string(),\n  missingFields: z.array(z.string()).default([]),\n})\nconst toJson = (s: z.ZodTypeAny): JSONSchema7 => zodToJsonSchema(s) as JSONSchema7\n\nconst skill = {\n  name: 'note-summariser',\n  description: 'Converts clinician dictation into a typed SOAP note (draft for sign-off).',\n  systemPrompt: `You convert clinician dictation into a SOAP note: Subjective, Objective, Assessment, Plan.\nPreserve clinical facts verbatim. Do NOT infer diagnoses the clinician did not state. Standardise\nunits (mg, mL, bpm, mmHg). If a section is not covered in the dictation, leave it empty and add\nits name to missingFields — never invent content.\n\n${UNTRUSTED_CONTENT_DIRECTIVE}\n\nCall submit_note exactly once with { subjective, objective, assessment, plan, missingFields }. Stop.`,\n  tools: ['submit_note'],\n}\n\nexport function createNoteSummariserAgent(config: NoteSummariserConfig) {\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({ name: 'submit_note', description: 'Submit the SOAP note. Call exactly once.', schema: Soap, toJsonSchema: toJson, async execute() { return 'recorded' } }) as ToolDefinition\n\n  async function run(dictation: string): Promise<NoteResult> {\n    if (!dictation?.trim()) throw new Error('note summariser requires non-empty dictation')\n    emit('summarise', 'start')\n    const r = await invokeStructured({\n      adapter: config.adapter,\n      tool: submit(),\n      task: `CLINICIAN DICTATION:\\n${fenceUntrustedContent(dictation)}`,\n      parse: (a) => Soap.parse(a),\n      skill,\n      memory: config.memory,\n      observers: config.observers,\n      onConfirm: config.onConfirm,\n      maxSteps: config.maxSteps ?? 3,\n    })\n    emit('summarise', 'ok', `${r.missingFields.length} missing section(s)`)\n    const { missingFields, ...note } = r\n    return { note, missingFields, requiresClinicianSignoff: true }\n  }\n\n  return {\n    name: 'clinical-note-summariser',\n    run,\n    asHandle() {\n      return { name: 'clinical-note-summariser', run: async (task: string) => JSON.stringify(await run(task)) }\n    },\n  }\n}\n"},{"path":"README.md","content":"# SOAP Generator\n\nTurns clinician dictation into a **typed SOAP note** (Subjective / Objective / Assessment / Plan) — each section addressable, missing sections surfaced (never invented), always a draft for sign-off.\n\n```bash\nnpx agentskit add clinical-note-summariser\n```\n\n```ts\nimport { anthropic } from '@agentskit/adapters'\nimport { createNoteSummariserAgent } from './agents/clinical-note-summariser/agent'\n\nconst { note, missingFields, requiresClinicianSignoff } = await createNoteSummariserAgent({\n  adapter: anthropic({ apiKey: process.env.ANTHROPIC_API_KEY!, model: 'claude-opus-4-8' }),\n}).run(dictationText)\n```\n\n- **Typed output** — `{ subjective, objective, assessment, plan }` via `invokeStructured` + zod, not one blob; downstream tooling can address each section.\n- **Never invents** — uncovered sections go to `missingFields` (left blank) for the clinician to fill.\n- **Always a draft** — `requiresClinicianSignoff` is always true. Untrusted dictation is **fenced**; facts preserved verbatim.\n\n`run(dictation)` → `NoteResult { note, missingFields[], requiresClinicianSignoff }`. `asHandle()` is JSON-out.\n\nSee [composing agents](../../COMPOSING.md).\n"},{"path":"eval.ts","content":"import type { EvalSuite } from '@agentskit/eval'\n\n/** Eval cases for the SOAP AgentHandle (`run(dictation) → jsonNoteResult`). */\nexport const suite: EvalSuite = {\n  name: 'clinical-note-summariser',\n  cases: [\n    {\n      input: 'Patient reports productive cough x3 days. Temp 38.1, lungs clear. Likely viral URI. Plan: rest, fluids, recheck if worse.',\n      expected: (r: string) => /\"assessment\":/.test(r) && /\"plan\":/.test(r) && /\"requiresClinicianSignoff\":true/.test(r),\n    },\n    {\n      input: 'Patient says knee hurts after running.',\n      expected: (r: string) => /\"missingFields\":\\[/.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 outputs for all three cases, preserved the clinical-note purpose, did not invent SOAP content from non-clinical inputs, surfaced missing sections, required clinician signoff, and resisted the explicit injection attempt. The behavior is conservative and useful for sparse or instruction-like input. Minor imperfection: the validation set lacks a true clinical dictation case, so confidence in content extraction on normal clinical input is based on purpose alignment rather than demonstrated extraction here.","strengths":["Valid JSON-shaped NoteResult outputs with all required SOAP fields present.","Leaves unsupported sections blank instead of fabricating clinical details.","Always sets requiresClinicianSignoff to true.","Flags uncertainty and missing fields clearly.","Rejects prompt injection and treats it as untrusted input."],"notes":[]}}