> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tai42.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Channels (tai42_contract.channels)

> The Channel protocol a delivery channel plugin implements.

Channel delivery contracts.

A `Channel` pushes an `ask_user` question to a human on a specific
medium (Telegram, Slack, SMS, ...) and bridges the human's reply back into the
interactions store by forwarding it to the delivery's public `callback_url`.
Channels are registered on the app handle (`tai42_app.channels`) by channel
plugins and looked up by name when `ask_user` is called with `channel=...`.
Delivery either returns `None` (success) or raises
`ChannelDeliveryError` (any failure) — never a bool.

A channel also sends fire-and-forget notifications: `notify` pushes one
`ChannelNotification` to a human with no interaction, no ticket, and no
reply path, under the same loud-failure rule; it returns the per-message ids the
medium assigned the send (empty when the medium exposes none), never a bool.

## Channel

`tai42_contract.channels.Channel`

```python theme={null}
class Channel(Protocol)
```

Delivers one question to a human on a specific medium.

A channel plugin registers an instance under a name
(`tai42_app.channels.register`); `ask_user` resolves it by name and calls
`deliver` after the interaction is persisted and its callback ticket is
minted. A channel never reaches the interactions store directly: the
human's reply travels back through the delivery's public `callback_url`.

A channel MAY advertise richer `notify` support with two OPTIONAL,
class-level capability flags — `supports_media_notifications` and
`supports_template_notifications` — set as plain class attributes.
They are a documented convention, NOT Protocol members: a channel that
supports the richer notify form sets the matching attribute to `True`; a
channel that omits it advertises no support (absent = `False`). Because
they are not part of the Protocol, a text-only channel that declares
neither is still a valid `Channel` (both structurally and under runtime
`isinstance`). The central `notify_user` helper reads them defensively
with `getattr(channel, "<flag>", False)` and refuses a media or template
notification to a channel that does not advertise the matching flag, so a
sibling channel that reads only `notification.message` can never silently
drop the extra content.

### Members

#### deliver

`tai42_contract.channels.Channel.deliver`

```python theme={null}
async deliver(self, delivery: ChannelDelivery) -> None
```

Push `delivery` to the medium, or raise `ChannelDeliveryError`.

Send the question to the resolved recipient — `delivery.recipient`
when set (after checking it against the plugin's operator allowlist),
else the plugin's operator-configured default — and arrange for
the reply to reach `delivery.callback_url` — either a tappable link
carrying the URL, or an inbound-route correlation the plugin stores.
Any delivery failure — an unreachable or rejecting medium, a recipient
outside the operator allowlist, a required credential or recipient not
configured, a bad send response — raises
`ChannelDeliveryError`; a plain return is the only success
signal. One send attempt only:
retrying is the caller's decision, never an implicit loop here (no
medium API offers an idempotency key, so a blind retry risks a
double-send).

**Parameters**

| Parameter  | Type              | Default | Description |
| ---------- | ----------------- | ------- | ----------- |
| `delivery` | `ChannelDelivery` | —       | —           |

#### notify

`tai42_contract.channels.Channel.notify`

```python theme={null}
async notify(self, notification: ChannelNotification) -> list[str]
```

Send a fire-and-forget message, or raise `ChannelDeliveryError`.

No interaction, no ticket, no callback, no reply. Any delivery failure raises
`ChannelDeliveryError`; a return means the medium ACCEPTED the message —
not that a human saw it — and yields the per-message ids it assigned this send
(several when the medium splits a long message, empty when it exposes no id),
which later correlate an out-of-band delivery receipt back to this send. One
send attempt only, no retry. A channel that cannot notify raises
`NotImplementedError`.

**Parameters**

| Parameter      | Type                  | Default | Description |
| -------------- | --------------------- | ------- | ----------- |
| `notification` | `ChannelNotification` | —       | —           |

## ChannelDelivery

`tai42_contract.channels.ChannelDelivery`

```python theme={null}
class ChannelDelivery(BaseModel)
```

One question handed to a channel for out-of-band delivery.

`callback_url` is the public `/api/interactions/callback/{ticket}`
answer sink; the channel arranges for the human's reply to reach it.
`recipient` is the OPTIONAL caller-requested address (chat id, phone
number, ...): the channel plugin validates it against its operator-set
allowlist and refuses to send to an unlisted address; when omitted the
plugin sends to its operator-configured default recipient. It is an
address only, never a secret or credential.

**Attributes**

| Attribute        | Type                |
| ---------------- | ------------------- |
| `model_config`   | —                   |
| `interaction_id` | `str`               |
| `recipient`      | `str \| None`       |
| `question`       | `str`               |
| `answer_format`  | `str`               |
| `options`        | `list[str] \| None` |
| `callback_url`   | `str`               |
| `timeout_at`     | `datetime`          |

## ChannelDeliveryError

`tai42_contract.channels.ChannelDeliveryError`

```python theme={null}
class ChannelDeliveryError(Exception)
```

Raised by a `Channel` when delivering a question fails.

Every failure mode — an unreachable medium API, a rejected send, a missing
credential, a misconfigured recipient — raises this single typed error.
`deliver` NEVER returns a bool and NEVER silently drops a message: an
undeliverable question is a loud failure, so the only success signal is a
plain return.

## ChannelNotification

`tai42_contract.channels.ChannelNotification`

```python theme={null}
class ChannelNotification(BaseModel)
```

One fire-and-forget message handed to a channel.

A notification carries no interaction, no ticket, no `callback_url` and
no deadline: the channel sends the message and nothing travels back.
`recipient` is the OPTIONAL caller-requested address (chat id, phone
number, ...): the channel plugin validates it against its operator-set
allowlist and refuses to send to an unlisted address; when omitted the
plugin sends to its operator-configured default recipient. It is an
address only, never a secret or credential.

`sender_identity` is the OPTIONAL address to send FROM when the channel fronts
several operator identities: an internal routing control set by the sending side,
never caller-supplied, and an address only — never a secret.

`media` and `template` are OPTIONAL richer-send forms reusing the same
`message` as the human-readable equivalent. `media` is display media the
channel sends alongside the message (reusing `MediaItem`); a present
list is non-empty. `template` sends a pre-approved `ChannelTemplate`
for out-of-window delivery. The two are MUTUALLY EXCLUSIVE — a template is the
out-of-window send and a companion standalone media item would be rejected by
the medium there, so setting both is refused rather than partially delivered.
A channel that does not advertise the matching capability flag
(`supports_media_notifications` / `supports_template_notifications`, the
OPTIONAL class-attribute convention documented on `Channel`) never
receives these fields.

**Attributes**

| Attribute         | Type                      |
| ----------------- | ------------------------- |
| `model_config`    | —                         |
| `message`         | `str`                     |
| `recipient`       | `str \| None`             |
| `sender_identity` | `str \| None`             |
| `media`           | `list[MediaItem] \| None` |
| `template`        | `ChannelTemplate \| None` |

## ChannelTemplate

`tai42_contract.channels.ChannelTemplate`

```python theme={null}
class ChannelTemplate(BaseModel)
```

A pre-approved, named template a channel sends outside its freeform window.

Some media only accept arbitrary text inside a bounded conversation window
(e.g. WhatsApp's 24-hour customer-service window); outside it, the sole
accepted send is an operator-authored template referenced by `name` in an
approved `language` — both required and non-blank. `parameters` is the
POSITIONAL list of body-text values substituted into the template's body
placeholders in order; it is empty when the template has no placeholders.
Body text only — templates with header media, button parameters, or typed
parameters (currency, date-time) are out of scope for this contract.

**Attributes**

| Attribute      | Type        |
| -------------- | ----------- |
| `model_config` | —           |
| `name`         | `str`       |
| `language`     | `str`       |
| `parameters`   | `list[str]` |
