Widget and input/output channels
🔵 How users and external systems communicate with agents. This is not an "API" in the software sense, but channels — ready-made entry/exit points that an agent receives from configuration.
| Channel | Where it's configured | Nature |
|---|---|---|
| Widget (web) | server.yaml → widget + embedding widget.js |
embeddable chat on a website |
| Telegram bot | agent.yaml → telegram (per agent) |
long-polling, private chats |
| Community VK bot | agent.yaml → vk (per agent) |
Long Poll, community messages |
| Yandex Messenger | agent.yaml → yandex_messenger (per agent) |
polling getUpdates, private and group chats |
| Email (IMAP→SMTP) | agent.yaml → email (per agent) |
incoming emails + reply |
| Storage (S3/WebDAV/FTP) | agent.yaml → storage (per agent) |
new files from bucket/folder → agent |
| HTTP API (SSE) | no config | POST /v1/chat/completions (§8) |
| Incoming webhook | server.yaml → inbound |
POST /v1/inbound/:template (§8.8) |
| Sinks (output) | notify in cron/email/workflow |
log/webhook/telegram/vk/yandex_messenger/messages_db/agent/email/workflow/storage (§11) |
19.1 Widget (web)
An embeddable chat interface that communicates with the agent via
/v1/chat/completions (SSE) and closes the session via POST /v1/sessions/:id.
Embedding
On any page of a website:
<script src="http://127.0.0.1:3000/widget.js"></script>
Served routes: /widget (page), /widget.js (loader), /dist/*
(assets). The /widget page references assets at versioned addresses
(/dist/<версия>/*): the browser caches them forever, and after a deploy it immediately picks up
the new ones. Addresses without a version remain and are revalidated on every request.
Which agent opens
The widget resolves the model in this order (last one wins):
widget.default_model(fromserver.yaml);window.AGENTOS_MODEL— a variable of the parent frame;?model=in the URL (/widget?model=hello-agent).
Configuration in server.yaml:
widget:
default_model: "drom-agent" # агент по умолчанию
The widget takes the base URL from window.AGENTOS_BASE_URL (empty = same origin) —
needed when hosting the widget on a different domain.
Ready-made demo pages
| URL | What |
|---|---|
/widget?model=hello-agent |
widget with a specific agent |
/demo/overlay |
widget over live websites (tabs from config/demo.yaml) |
/demo |
widget demo |
Tabs /demo/overlay are defined in config/demo.yaml
(tabs: [{label, url, model}], hot-reloads on the fly).
Close button
By default, the widget header has a "close" button (×): it sends
agentos:close to the parent frame, and the loader widget.js hides the popup
chat. For inline embedding (the widget is inserted directly into the page rather than opened as
a popup), the button is not needed — visibility is managed by the page itself. It can be disabled
at the template level:
widget:
show_close: false # не показывать кнопку «закрыть» (по умолчанию true)
This is done for felix-agent (page /ext/felix/) and admin (page
/agents/admin), where the chat is embedded inline.
The dialog is preserved
By default, the dialog lives until the user starts a new one with the
"New dialog" button (the pencil in the header). Closing the widget only hides it,
switching demo tabs returns to the dialog of the selected agent, and after
a page reload the completed questions and answers (along with tables,
maps, and charts) are drawn again — they are stored in the browser's localStorage
separately for each agent.
The behavior is configured at the template level:
widget:
persist: true # false — закрытие/перезагрузка начинают новый диалог
history_limit: 40 # сколько завершённых обменов хранить в истории
With persist: false, the widget does not save or restore history: closing
or reloading the page starts the dialog anew. history_limit limits
the size of the saved transcript — the oldest exchanges are evicted.
Links from answers open on the website behind the widget. When the widget occupies most of the screen (phones), the loader closes it so that the opened page is visible; pressing the chat button again continues the dialog.
The dialog cost and the agent name are hidden behind the "$" button in the header.
Suggestion buttons
In a new dialog, below the greeting, you can show ready-made questions as buttons — pressing one sends the question just as if the user had typed it themselves. After the first message, the buttons disappear.
widget:
starters:
- "Подбери игру для компании из шести человек"
- "Что подарить ребёнку 10 лет?"
2–4 short, specific questions that the agent answers confidently work well.
Hosts on their own ChatWidget call the same thing directly:
widget.showSuggestions([...]).
Visual blocks in the widget
Tools can return a visual layer (charts chart, tables table,
text) — the widget renders them inline (Chart.js + tables). The block format and
pagination are common fields.
How to show these blocks is set by widget.tool_visuals (default final):
widget:
tool_visuals: final # final | always | never
final— show only the visuals of the last round of tools (the one that comes right before the answer). Intermediate "introspection" (service tables that the agent looks at along the way) does not make it into the transcript — this way the answer doesn't drown in service tables. The text answer itself does not change.always— show each table/chart immediately, as soon as the tool returns (operator action log, old behavior).never— don't show tool visuals at all, only text.
The rule applies to the widget's SSE stream, so the saved history contains exactly the blocks that the user saw.
Attachments in the widget
When a template has attachments.enabled: true set (§4.3), the widget shows a
📎 button with a source picker: file (upload), screenshot
(getDisplayMedia), photo (camera; on mobile — native capture).
Files go to POST /v1/uploads, and are passed in /v1/chat/completions via the
attachments field — the agent receives a message with their names in the session's scratch directory.
In addition, the agent can itself request an attachment with the
request_attachment tool: the widget
shows a hint card with a button, and after attachment responds via
POST /v1/input-response (§8.2.1).
Sign-in in the widget
If the template has login.enabled: true set (§4.3) and providers are configured
auth.oidc, a "Sign in" button appears in the widget header. Clicking it opens a popup
with OIDC login (GET /v1/login/widget/:provider); after sign-in, the callback issues a
signed x-user-token (HMAC), which the widget caches in localStorage and
sends in the x-user-token header on subsequent requests. The browser-id is then
automatically merged with the OIDC identity (Origin::Oidc in the user-registry) —
one user across all channels. The button changes to "Sign out" (resets the local
token).
login:
enabled: true
providers: [yandex] # пусто = все включённые провайдеры
prompt: "Войти через Яндекс" # текст кнопки (опционально)
Mandatory sign-in (login.required)
login.required: true makes sign-in mandatory: an anonymous user cannot
send a message to the agent. The widget shows a full-screen sign-in screen instead of the
input field, and the server responds with 401 to anonymous POST /v1/chat/completions.
The requirement is satisfied in one of the following ways:
- OIDC — sign-in via providers from
login.providers(empty = any enabled provider, e.g. Yandex); - user database from config — with
login.allow_users: true, sign-in with login/password fromauth.users(POST /v1/login/widget/password).
login:
required: true
providers: [yandex] # вход через Яндекс
allow_users: true # ИЛИ логин/пароль из auth.users
prompt: "Войдите, чтобы продолжить"
| Field | Default | Description |
|---|---|---|
enabled |
false |
show the "Sign in" button (optional sign-in) |
required |
false |
require sign-in; anonymous requests are rejected (401) |
providers |
[] |
OIDC providers (auth.oidc.providers); empty = all |
allow_users |
false |
allow sign-in with login/password from auth.users |
prompt |
empty | text of the button/sign-in screen title |
Sign-in in the Android app
The Android client supports the same sign-in methods. If the selected agent
has login.required, the app shows a sign-in screen instead of the chat:
- OIDC — the "Sign in via
{provider}" button opensGET /v1/login/widget/:providerin an embedded WebView; after sign-in, the callback page calls the native bridgewindow.AndroidAuth.onLogin(...)and the app receives a signedx-user-token; - login/password — with
login.allow_users: true, the sign-in form sendsPOST /v1/login/widget/passwordand stores the resulting pair{user_id, token}.
The resulting token is stored in the app settings (x-user-token) and
sent along with x-user-id; the "Sign out" button in the chat header resets
the identity.
Dynamic sender allowlist
For messenger channels (Telegram / VK / Yandex Messenger / email), the list of
allowed senders (allowed_user_ids / allowed_logins /
allowed_senders) is set in agent.yaml and requires a redeploy. There is a
dynamic alternative — the allowed_senders table in
logs/allowed_senders.db, which is changed via the API without a restart/redeploy.
A record is a channel identity:
tg:<user_id>— Telegram;vk:<user_id>— VK;ym:<login>— Yandex Messenger;email:<address>— email sender (lowercase);user:<canonical_id>— canonical user (any channel).
Records are bound to an agent template (or * — globally). When the table has
records for a template (or global *), the table takes priority: the static
allowlist from agent.yaml is ignored. If the table is empty, the
static allowlist applies as before.
# разрешить tg:12345 и ym:ivan агенту my-agent (без редеплоя)
curl -X POST http://127.0.0.1:3000/v1/allowed-senders/my-agent \
-H "Authorization: Bearer $ADMIN_TOKEN" -H "Content-Type: application/json" \
-d '{"identity":"tg:12345","note":"Pavel"}'
curl -X POST http://127.0.0.1:3000/v1/allowed-senders/my-agent \
-H "Authorization: Bearer $ADMIN_TOKEN" -H "Content-Type: application/json" \
-d '{"identity":"ym:ivan"}'
# список
curl http://127.0.0.1:3000/v1/allowed-senders/my-agent -H "Authorization: Bearer $ADMIN_TOKEN"
# удалить
curl -X DELETE http://127.0.0.1:3000/v1/allowed-senders/my-agent/tg:12345 \
-H "Authorization: Bearer $ADMIN_TOKEN"
The endpoints are in 08-http-api.md (§ "Allowed senders").
19.2 Telegram
One long-polling bot per template; started at server startup if
token_env is set. The token is read from env (never from YAML).
telegram:
token_env: MY_TG_BOT_TOKEN # имя env с токеном бота
allowed_user_ids: [] # пусто = любой; список = только эти Telegram user id
show_typing: true # индикатор «печатает…» во время работы
show_tool_calls: true # компактные строки статуса tool-вызовов
| Field | Default | Description |
|---|---|---|
token_env |
empty | name of the env var with the token; empty = bot disabled |
allowed_user_ids |
[] |
restrict senders by Telegram user id |
show_typing |
true |
show the typing indicator |
show_tool_calls |
true |
echo tool calls to the chat |
Visual blocks on Telegram: chart → PNG (sendPhoto), table → markdown lines.
Incoming photos and documents from the user (with attachments.enabled: true on the
template) are downloaded via getFile and saved to the session scratch; the agent
receives a message with the file names. The agent's
request_attachment request is resolved the same way: the bot sends a
hint "send a file/photo", and the user's reply with media continues the turn.
19.3 VK (community)
A community bot on the Long Poll API — no public HTTPS needed. One bot per
template; started at server startup if token_env is set. The community token
is read from env (never from YAML).
vk:
token_env: VK_GROUP_TOKEN # имя env с group access token
group_id: 123456789 # id сообщества (для longpoll и сообщений от имени группы)
allowed_user_ids: [] # пусто = любой; список = только эти VK user id
show_typing: true # индикатор «печатает…» (messages.setActivity)
show_tool_calls: true # эхо tool-вызовов в чат
poll_wait: 25 # wait для longpoll (сек, 1..25)
| Field | Default | Description |
|---|---|---|
token_env |
empty | name of the env var with the token; empty = bot disabled |
group_id |
0 |
community id; 0 = derive from the token |
allowed_user_ids |
[] |
restrict senders by VK user id |
show_typing |
true |
show the typing indicator |
show_tool_calls |
true |
echo tool calls to the chat |
poll_wait |
25 |
longpoll request wait interval (sec) |
The session is vk:{template}:{peer_id} (for a conversation, peer_id = 2000000000 + chat_id).
Token permissions: messages (and photos/docs for uploading media). Visual
blocks: chart → PNG via photos.getMessagesUploadServer, table → text
lines, file → document via docs.getMessagesUploadServer.
Incoming photos and documents from the user (with attachments.enabled: true in the
template) are downloaded and saved to the session scratch; the agent receives a message
with the file names. The agent request
request_attachment is resolved the same
way: the bot sends a hint, and the user's reply with media continues the turn.
As a result sink (for cron/email/monitor), VK is specified by the type vk:
notify:
- type: vk
peer_id: 41848219 # peer_id получателя (для беседы 2000000000 + chat_id)
# token_env: VK_GROUP_TOKEN # опц.; по умолчанию берётся токен из vk-конфига шаблона
Community setup
For the bot to receive messages, two things must be done in the community:
- Enable Long Poll API (Management → API → Long Poll API → "Enabled").
- Enable the "New messages" event type — by default VK enables Long
Poll, but all event types are disabled (
message_new: 0), so the bot receives nothing. In the UI — the "New messages" checkbox in Long Poll API; or via the API:
curl -X POST "https://api.vk.com/method/groups.setLongPollSettings" \
-d "group_id=<id>&access_token=<token>&v=5.199" \
-d "message_new=1&message_reply=1&message_edit=1&message_allow=1&message_deny=1&message_typing_state=1"
To check the current settings: groups.getLongPollSettings (the
events.message_new field).
19.4 Yandex Messenger (Yandex 360)
A bot on Yandex's Bot API — polling bot/v1/messages/getUpdates/ with the cursor
offset (no public HTTPS needed). One bot per template; it starts when the
server starts, if token_env is set. The bot's OAuth token is read from env (never from
YAML). The bot is created in Yandex 360 for Business ("Bots in Messenger").
yandex_messenger:
token_env: YANDEX_MESSENGER_BOT_TOKEN # имя env с OAuth-токеном бота
allowed_logins: [] # пусто = любой; список = только эти логины
show_typing: true # индикатор «печатает…» (sendTyping)
show_tool_calls: false # эхо tool-вызовов (по умолчанию выключено)
poll_secs: 2 # пауза между опросами getUpdates (сек)
| Field | Default | Description |
|---|---|---|
token_env |
empty | name of the env with the OAuth token; empty = bot disabled |
allowed_logins |
[] |
restrict senders by Yandex login |
show_typing |
true |
show the typing indicator |
show_tool_calls |
false |
echo tool calls (separate messages — noisy, since Messenger can't replace them) |
poll_secs |
2 |
pause between polls getUpdates (sec) |
The session is ym:{template}:user:{login} (personal chat) or
ym:{template}:chat:{chat_id} (group/channel). A reply to a personal chat goes via
login, to a group/channel — via chat_id. Formatting — Yandex's markdown subset
(**жирный**, __курсив__, ~~зачёркнутый~~, [ссылка](url), code);
the agent's full Markdown is re-rendered via comrak. Visual blocks: chart →
PNG (sendImage), table → text lines, file → document (sendFile).
Incoming files and images from the user (with attachments.enabled: true in the
template) are downloaded via getFile and saved to the session scratch; the agent
request request_attachment
is resolved the same way.
As a result sink (for cron/email/monitor), Yandex Messenger is specified by the type
yandex_messenger; the chat_id field accepts either the chat/channel id (the value from
/, e.g. 0/0/4f24b544-…), or the user's login for a personal chat:
notify:
- type: yandex_messenger
chat_id: "0/0/4f24b544-697c-4e18-a9c1-b39432ee9bf9" # или логин пользователя
# token_env: YANDEX_MESSENGER_BOT_TOKEN # опц.; по умолчанию токен из yandex_messenger-конфига
This channel is only about Messenger. Sign-in via Yandex (OAuth login on the web) and Yandex Disk / other service tools are separate integrations.
How to test
-
In Yandex 360 → Bots in Messenger create a bot and copy its OAuth token.
-
Put the token in
.env(see.env.example):YANDEX_MESSENGER_BOT_TOKEN=At... -
Add the
yandex_messengersection to the template'sagent.yaml(an example already exists indrom-agent), restart the server. The log will showyandex messenger bot authenticated(or a warning about a token error). -
Write to the bot in direct messages or add it to a group chat.
A quick token check without starting the server — a smoke test:
cargo test -p agent_web_bot --test yandex_messenger_live -- --nocapture
It calls self/get and getUpdates and prints the bot's login; without a token the test
is silently skipped.
19.4 Email
Polling an IMAP mailbox: each new email starts a one-shot turn and replies via
SMTP. Context between emails — via long-term memory (the memory hook), the key being the
canonical user id of the sender.
email:
imap_host: imap.example.com
imap_port: 993
imap_username: support@example.com
imap_password_env: EMAIL_PASSWORD # пароль из env, не в YAML
allowed_senders: [] # пусто = любой; "@domain" = весь домен
smtp: # reply-настройки (опусти = только обработка)
host: smtp.example.com
username: support@example.com
password_env: SMTP_PASSWORD
from: support@example.com
notify: [] # доп. sink'и (как у cron)
poll_secs: 60
| Field | Default | Description |
|---|---|---|
imap_host |
empty | IMAP host; empty = channel disabled |
imap_port |
993 |
IMAP port (implicit TLS) |
imap_username |
empty | login (usually the mailbox address) |
imap_password_env |
empty | name of the env with the IMAP password |
allowed_senders |
[] |
allowed senders (exact address or @домен) |
smtp |
none | SMTP for replies: host, username, password_env, from, tls, port |
notify |
[] |
additional result sinks |
poll_secs |
60 |
mailbox polling interval |
Email attachments (with attachments.enabled: true in the template) are saved to the
session scratch and passed to the agent (base64/quoted-printable are decoded).
19.4 Storage (S3/WebDAV/FTP)
Polling external storage: each new file under the prefix starts a one-shot agent turn (the file's text goes into the message). Similar to the email channel, but the source is files in a bucket/folder.
storage:
storage: # подключение (как у storage_tool)
backend: s3
endpoint: https://s3.example.com
bucket: inbox
region: us-east-1
access_key_env: S3_ACCESS_KEY
secret_key_env: S3_SECRET_KEY
prefix: inbox/ # папка для опроса
poll_secs: 60 # интервал опроса
include_suffix: ".txt" # опц.: обрабатывать только *.txt
move_after: processed/ # опц.: переместить файл после обработки
# delete_after: true # или: удалить после обработки
| Field | Default | Description |
|---|---|---|
storage |
none | connection (backend: s3/webdav/ftp); empty = channel disabled |
prefix |
"" |
folder/prefix to poll |
poll_secs |
60 |
polling interval |
include_suffix |
"" |
filter by file name suffix (empty = all) |
move_after |
"" |
move the processed file under this prefix |
delete_after |
false |
delete the processed file (mutually exclusive with move_after) |
Dedup — in memory (processed keys); to survive a restart, use
move_after/delete_after so the file isn't processed again.
Storage sink (output)
Delivering the result to storage — the storage sink in notify (like cron/email):
notify:
- type: storage
storage:
backend: s3
endpoint: https://s3.example.com
bucket: reports
region: us-east-1
access_key_env: S3_ACCESS_KEY
secret_key_env: S3_SECRET_KEY
key: "runs/{source}/{ts}.txt" # {source} и {ts} подставляются
19.5 Programmatic channels (pointers)
- HTTP chat (SSE) —
POST /v1/chat/completions, fields and events — §8.2. - Incoming webhook —
POST /v1/inbound/:template(synchronous reply) — §8.8. - Workflow webhook —
POST /v1/workflows/<name>/webhook— §12.5. - Result sinks (where to deliver the reply) —
notifyin cron — §11.