# Channel Adapter Pattern

> Use when an agent's response/formatting logic is (or will be) reachable from more than one delivery channel (chat, SMS, email, push, web). Triggers — before adding a second channel to a single-channel agent, before writing/editing a shared respond()/format_reply()/persona-selection helper, or when a multi-channel agent's voice output sounds like it was written for text (spelled digits, markdown symbols read aloud).

- Skill: `oimiragieo/channel-adapter-pattern` (Agent Skill)
- Install (CLI): `npx skillmds@latest add oimiragieo/channel-adapter-pattern`
- Raw SKILL.md: https://api.skillmd.com/api/skills/oimiragieo/channel-adapter-pattern/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: oimiragieo (https://skillmd.com/u/oimiragieo)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/oimiragieo/channel-adapter-pattern

---


# Channel-Adapter Pattern

## Overview

Generate the reply in ONE canonical form; let a thin per-channel adapter at the edge shape it. A shared `respond()` helper that can't see which channel it's rendering for silently reuses whichever channel was coded first — so a voice channel inherits a chat channel's markdown and digits, and reads `*asterisks*` and "2:30" aloud.

## When to use

- Adding a second delivery channel to an agent built for one.
- Writing/editing any shared `respond()` / `format_reply()` / persona-selection helper.
- Voice/phone output sounds text-shaped: spelled numbers, markdown symbols, URLs read aloud.

## The core rule

**Thread an explicit `medium`/`channel` param through every shared response helper, and branch ONLY in a thin edge adapter — never in core agent/tool logic.**

```python
# core logic is channel-agnostic; the persona/format is chosen by medium
reply = await agent.respond(user_text, medium=medium)   # "voice" | "sms" | "web" | ...
# a per-channel adapter (send_outbound) does the final wire-format transform
```

- Canonical format in, per-channel transform out. The agent produces text; the chat adapter returns it as-is; the voice adapter strips markdown + speaks numbers as words.
- Adapter contract per channel: `parse_inbound` + `send_outbound` translating to/from a common envelope; keep formatting hints in the envelope metadata, not in the shared logic.
- Per-channel latency/model-tier routing is an adapter decision, not a core-logic branch.

## The grep-for-the-leak diagnostic

Enumerate EVERY caller of the shared helper. **Any caller on a different channel that doesn't pass `medium=` is a latent leak.** This is the root cause seen in production: a background job that called the shared response helper with no medium → defaulted to the text persona. Test the SAME helper across every channel adapter before shipping a new channel.

## Common mistakes

- Building the shared helper for channel #1, adding channel #2 later without a medium param → #2 inherits #1's shape.
- Hardcoding `medium="voice"` inside a helper that is ALSO reached by the text gateway (verify the caller set first — shared classes must thread, not hardcode).
- Branching core tool logic on channel instead of at the edge adapter.

## Real-world impact

A support bot serving both a web widget and SMS shipped this exact bug: an SMS-side caller invoked the shared `respond()` helper with no `medium=` set, so it silently defaulted to the web persona — SMS output came out markdown-shaped with literal asterisks and unescaped links. The fix threaded `medium="sms"` explicitly into every SMS-side caller. This is the design-time partner to `deliver-format-fallback` (send-time 4xx recovery) and `deliver-durably` (crash-survivable delivery) — use all three together. Exa-confirmed against the 2026 "channel adapter pattern" (CallSphere, getwidget conversational-AI architecture).

