{"id":"clinical-referral-router","title":"Referral Router","description":"Routes a referral packet to the receiving specialty + urgency with typed output. Incomplete packets are NOT assigned — missing critical fields (reason/meds/prior workup) are surfaced and the case escalates to a human coordinator. Fail-safe on error.","category":"clinical","version":"2.0.0","source":"AKOS","license":"MIT","tags":["clinical","routing","classification","human-in-the-loop"],"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-referral-router","description":"Routes a referral packet to the receiving specialty + urgency with typed output. Incomplete packets are NOT assigned — missing critical fields (reason/meds/prior workup) are surfaced and the case escalates to a human coordinator. Fail-safe on error.","systemPrompt":"You route inbound referral packets. Identify the receiving specialty (e.g. cardiology,\northopedics, oncology) and urgency (routine | soon | urgent). Cite the relevant clinical finding\nin a one-sentence rationale.\n\nIf the packet is missing critical info (reason for referral, current medications, prior workup),\nlist those in missingFields rather than assigning — do NOT route an incomplete packet. Use\nspecialty \"unclear\" and urgency \"unclear\" when you cannot determine routing. Never make clinical\ndeterminations beyond routing.\n\n${UNTRUSTED_CONTENT_DIRECTIVE}\n\nCall submit_routing exactly once with { specialty, urgency, rationale, missingFields }. Stop."},"flow":null,"a2a":{"id":"io.agentskit.registry.clinical-referral-router","name":"Referral Router","description":"Routes a referral packet to the receiving specialty + urgency with typed output. Incomplete packets are NOT assigned — missing critical fields (reason/meds/prior workup) are surfaced and the case escalates to a human coordinator. Fail-safe on error.","version":"2.0.0","homepage":"https://registry.agentskit.io","skills":[{"name":"clinical-referral-router","description":"Routes a referral packet to the receiving specialty + urgency with typed output. Incomplete packets are NOT assigned — missing critical fields (reason/meds/prior workup) are surfaced and the case escalates to a human coordinator. Fail-safe on error.","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 * Referral Router — reads a referral packet and routes it to the receiving specialty +\n * urgency. Typed output (not free text); incomplete packets are NOT assigned — the\n * missing fields are surfaced and the case escalates to a human coordinator.\n *\n *   1. Model routes → typed { specialty, urgency, rationale, missingFields } (`invokeStructured` + zod).\n *   2. Code rule: any `missingFields`, an `unclear` specialty, or a failed run →\n *      `requiresHumanReview` and NO auto-assignment. The model never assigns a\n *      half-complete packet; it flags the gap.\n *\n * ```ts\n * const r = await createReferralRouterAgent({ adapter }).run(referralPacketText)\n * if (r.requiresHumanReview) routeToCoordinator(r)\n * ```\n */\n\nexport type ReferralUrgency = 'routine' | 'soon' | 'urgent' | 'unclear'\n\nexport interface ReferralResult {\n  specialty: string\n  urgency: ReferralUrgency\n  rationale: string\n  /** Critical fields absent from the packet (reason, meds, prior workup). */\n  missingFields: string[]\n  requiresHumanReview: boolean\n}\n\nexport interface ReferralRouterConfig {\n  adapter: AdapterFactory\n  memory?: ChatMemory\n  observers?: Observer[]\n  onConfirm?: (toolCall: ToolCall) => boolean | Promise<boolean>\n  maxSteps?: number\n}\n\nconst Routing = z.object({\n  specialty: z.string(),\n  urgency: z.enum(['routine', 'soon', 'urgent', 'unclear']),\n  rationale: z.string(),\n  missingFields: z.array(z.string()).default([]),\n})\nconst toJson = (s: z.ZodTypeAny): JSONSchema7 => zodToJsonSchema(s) as JSONSchema7\n\nconst routerSkill = {\n  name: 'referral-router',\n  description: 'Routes a referral packet to the receiving specialty and urgency.',\n  systemPrompt: `You route inbound referral packets. Identify the receiving specialty (e.g. cardiology,\northopedics, oncology) and urgency (routine | soon | urgent). Cite the relevant clinical finding\nin a one-sentence rationale.\n\nIf the packet is missing critical info (reason for referral, current medications, prior workup),\nlist those in missingFields rather than assigning — do NOT route an incomplete packet. Use\nspecialty \"unclear\" and urgency \"unclear\" when you cannot determine routing. Never make clinical\ndeterminations beyond routing.\n\n${UNTRUSTED_CONTENT_DIRECTIVE}\n\nCall submit_routing exactly once with { specialty, urgency, rationale, missingFields }. Stop.`,\n  tools: ['submit_routing'],\n}\n\nexport function createReferralRouterAgent(config: ReferralRouterConfig) {\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_routing',\n      description: 'Submit the referral routing. Call exactly once.',\n      schema: Routing,\n      toJsonSchema: toJson,\n      async execute() {\n        return 'recorded'\n      },\n    }) as ToolDefinition\n\n  async function run(packet: string): Promise<ReferralResult> {\n    if (!packet?.trim()) throw new Error('referral router requires a non-empty packet')\n\n    emit('route', 'start')\n    let r: z.infer<typeof Routing>\n    try {\n      r = await invokeStructured({\n        adapter: config.adapter,\n        tool: submit(),\n        task: `REFERRAL PACKET:\\n${fenceUntrustedContent(packet)}`,\n        parse: (a) => Routing.parse(a),\n        skill: routerSkill,\n        memory: config.memory,\n        observers: config.observers,\n        onConfirm: config.onConfirm,\n        maxSteps: config.maxSteps ?? 3,\n      })\n    } catch {\n      r = { specialty: 'unclear', urgency: 'unclear', rationale: 'routing unavailable — failed safe to human coordinator', missingFields: [] }\n    }\n\n    const requiresHumanReview = r.missingFields.length > 0 || r.specialty.toLowerCase() === 'unclear' || r.urgency === 'unclear'\n    emit('route', 'ok', requiresHumanReview ? 'human review' : `${r.specialty} / ${r.urgency}`)\n    return { ...r, requiresHumanReview }\n  }\n\n  return {\n    name: 'clinical-referral-router',\n    run,\n    asHandle() {\n      return {\n        name: 'clinical-referral-router',\n        run: async (task: string) => JSON.stringify(await run(task)),\n      }\n    },\n  }\n}\n"},{"path":"README.md","content":"# Referral Router\n\nRoutes a referral packet to the receiving specialty + urgency — typed output, and **never assigns an incomplete packet**.\n\n```bash\nnpx agentskit add clinical-referral-router\n```\n\n```ts\nimport { anthropic } from '@agentskit/adapters'\nimport { createReferralRouterAgent } from './agents/clinical-referral-router/agent'\n\nconst agent = createReferralRouterAgent({ adapter: anthropic({ apiKey: process.env.ANTHROPIC_API_KEY!, model: 'claude-opus-4-8' }) })\nconst r = await agent.run(referralPacketText)\nif (r.requiresHumanReview) routeToCoordinator(r)\n```\n\n## Why it's safe\n\n1. **Typed routing** — `{ specialty, urgency, rationale, missingFields }` via `invokeStructured` + zod, not free text.\n2. **No half-complete assignment** — if critical fields are missing (reason / meds / prior workup) or routing is `unclear`, the case is flagged `requiresHumanReview` and **not** auto-assigned.\n3. **Fail-safe** — a failed/malformed run → `unclear` → human coordinator. Untrusted packet text is **fenced**; no clinical determinations beyond routing.\n\n`run(packet)` → `ReferralResult { specialty, urgency, rationale, missingFields[], requiresHumanReview }`. `asHandle()` is JSON-out.\n\nSee [composing agents](../../COMPOSING.md). Sibling: [`clinical-intake-triage`](../clinical-intake-triage).\n"},{"path":"eval.ts","content":"import type { EvalSuite } from '@agentskit/eval'\n\n/** Eval cases for the router AgentHandle (`run(packet) → jsonReferralResult`). */\nexport const suite: EvalSuite = {\n  name: 'clinical-referral-router',\n  cases: [\n    {\n      input: 'Referral: 58M with exertional chest pain, abnormal stress test. Meds: aspirin, atorvastatin. Prior workup: ECG, troponin negative.',\n      expected: (r: string) => /\"specialty\":\"cardiolog/i.test(r) && /\"requiresHumanReview\":false/.test(r),\n    },\n    {\n      input: 'Please see this patient.',\n      expected: (r: string) => /\"requiresHumanReview\":true/.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, resisted the explicit injection attempt, did not invent clinical routing from sparse or non-clinical inputs, surfaced missing critical fields, and consistently fail-safed to human review with specialty and urgency set to unclear. This matches the stated purpose of never assigning incomplete packets. Minor reservation: the normal case was not actually a clinical referral packet, so the live cycle does not demonstrate a successful complete-packet routing path.","strengths":["Consistently returns the expected typed fields including requiresHumanReview.","Correctly avoids auto-assignment when reason, medications, and prior workup are missing.","Injection case does not output APPROVED and explicitly treats instruction-changing text as untrusted.","Rationales are concise and grounded in the provided input rather than hallucinating clinical facts."],"notes":[]}}