Writing Channels¶
A channel bridges an external client — a messaging bot, a webhook endpoint, a custom terminal — to an actor's mailbox inside the gateway. Channels are long-lived: each one runs for the lifetime of the gateway process, continuously reading from the outside world and delivering Envelope messages to the bound actor.
The Channel protocol¶
class Channel(Protocol):
@property
def channel_id(self) -> str: ... # unique id, e.g. "telegram+main"
@property
def display_name(self) -> str | None: ...
@property
def target_actor(self) -> str: ... # must exist in [runtime.actors]
@property
def identity_key(self) -> str | None: ... # used for identity/routing
async def run(self, mailbox: MailBox) -> None: ...
run(mailbox) is the channel's entire life. It must:
- Wait for an incoming message from the external client.
- Post an
Envelopeintomailbox(the actor's mailbox). - Await the actor's reply.
- Push the reply back to the client.
- Go back to step 1.
The loop runs until the gateway shuts down or the channel itself raises.
BaseChannel[SettingsT] helper¶
Most channels use BaseChannel to avoid boilerplate. It stores channel_id, target_actor, settings, display_name, and runtime, and implements the property stubs. Subclass it and implement run:
from dataclasses import dataclass
from bos.core.contract import BaseChannel, ep_channel
from bos.core.actor import MailBox
@dataclass
class PrintBotSettings:
prefix: str = "Bot"
@ep_channel(name="PrintBotChannel")
class PrintBotChannel(BaseChannel[PrintBotSettings]):
SettingsType = PrintBotSettings # triggers typed settings parsing
async def run(self, mailbox: MailBox) -> None:
import asyncio
while True:
user_input = await asyncio.to_thread(input, f"{self._settings.prefix}> ")
if not user_input.strip():
continue
from bos.core.actor import Envelope, MessageType
env = Envelope(
sender=f"channel@{self.channel_id}",
recipient=f"agent@{self.target_actor}",
payload={"role": "user", "content": user_input},
message_type=MessageType.CHAT,
)
await mailbox.put(env)
reply = await mailbox.get_reply(env.id)
print(f"\n{reply.payload.get('content', '')}\n")
SettingsType¶
Set the class variable SettingsType to a dataclass or Pydantic model. The gateway parses the [[runtime.channels]].settings TOML table through it and hands a typed instance to __init__ as settings. If SettingsType is None (the default), settings is a raw dict.
Settings flow¶
In config, each channel entry is an element of [[runtime.channels]]:
[[runtime.channels]]
type = "PrintBotChannel" # registered ep_channel name
channel_id = "printbot+main" # unique id
display_name = "Print Bot"
target_actor = "main"
[runtime.channels.settings] # becomes PrintBotSettings(prefix="Agent")
prefix = "Agent"
Keep secrets in environment variables
Reference secrets by env-var name (token_env = "TELEGRAM_BOT_TOKEN") rather than inlining them. The channel reads os.environ[settings.token_env] at startup. See TelegramChannel for a real example.
How the gateway instantiates a channel¶
workspace.resolve_gateway_channelsreads each[[runtime.channels]]block.- It looks up the
ep_channelregistered undertype. - It constructs the channel, passing
channel_id,target_actor,display_name, and parsedsettings. - It binds the channel to the named actor's mailbox.
- It calls
channel.run(mailbox)as a long-running async task.
Each channel is bound to exactly one actor (target_actor). The actor's mailbox address is agent@<actor_name>. The channel's own address is channel@<channel_id>.
Validation rules¶
channel_idmust be unique across all configured channels.target_actormust exist in[runtime.actors].typemust be a registeredep_channelimplementation.HttpChannelis reserved as gateway infrastructure (the control-plane API) and may not be used as a user channel.
Built-in channels¶
| Name | Requires | Notes |
|---|---|---|
TelegramChannel |
pip install bos-ai |
settings.token_env names the env var holding the bot token |
LarkChannel |
pip install 'bos-ai[lark]' |
Lark/Feishu; settings include app_id_env, app_secret_env |
See also¶
- Concepts: runtime — actors, mailboxes, gateway lifecycle
- Configuration —
[[runtime.channels]]and[runtime.actors]reference - Packaging & entry points — ship a channel in an installable package