Jido Messaging

Hex.pmHex DocsCILicenseWebsiteEcosystemDiscord

Messaging and notification system for the Jido ecosystem. Provides a unified interface for building conversational AI agents across multiple channels (Telegram, Discord, Slack, etc.).

Release Status

This package is being prepared for the Jido 1.x messaging release line. jido_messaging is built around Jido.Chat and the Elixir implementation aligned to the Vercel Chat SDK (chat-sdk.dev/docs).

Features

Installation

Add jido_messaging to your list of dependencies in mix.exs:

def deps do
[
{:jido_messaging, "~> 1.1"}
]
end

Quick Start

1. Define Your Messaging Module

defmodule MyApp.Messaging do
use Jido.Messaging,
persistence: Jido.Messaging.Persistence.ETS
end

2. Add to Supervision Tree

# In application.ex
def start(_type, _args) do
children = [
MyApp.Messaging
]
Supervisor.start_link(children, strategy: :one_for_one)
end

3. Use the API

# Create a room
{:ok, room} = MyApp.Messaging.create_room(%{type: :direct, name: "Support Chat"})
# Save a message
{:ok, message} = MyApp.Messaging.save_message(%{
room_id: room.id,
sender_id: "user_123",
role: :user,
content: [%{type: :text, text: "Hello!"}]
})
# List messages
{:ok, messages} = MyApp.Messaging.list_messages(room.id)

Eventful Commands

Use low-level persistence functions such as save_message/1 for imports, migrations, and tests that should not notify realtime consumers. Use eventful commands when a write should emit canonical jido.messaging.* signals:

{:ok, result} =
MyApp.Messaging.post_message(%{
room_id: room.id,
sender_id: "user_123",
role: :user,
content: [%{type: :text, text: "Hello!"}]
})
message = result.record
[%Jido.Signal{type: "jido.messaging.room.message_added"}] = result.signals

Subscribe to the instance Signal Bus for application UI or bridge notifications:

{:ok, subscription_id} = MyApp.Messaging.subscribe_signals("jido.messaging.room.**")
:ok = MyApp.Messaging.unsubscribe_signals(subscription_id)

Durable SQLite Persistence

Use the SQLite adapter when the host app needs durable local messaging state:

defmodule MyApp.Messaging do
use Jido.Messaging,
persistence: Jido.Messaging.Persistence.SQLite,
persistence_opts: [path: "data/my_app_messaging.sqlite3"]
end

The SQLite adapter stores canonical rooms, participants, messages, threads, bridge bindings, routing policies, bridge configs, and ingress subscriptions. room_timeline/2 returns top-level messages, grouped thread replies, and reply counts from the persisted message records.

Presence Signals

Jido.Messaging.Presence bridges transport-specific presence state, such as Phoenix Presence, into canonical messaging participant signals:

defmodule MyApp.Presence do
use Jido.Messaging.Presence,
messaging: MyApp.Messaging,
presence: MyAppWeb.Presence,
topic: "my_app:presence",
source: "my_app.presence"
end

Call touch/3 when a participant is seen online and mark_left/2 when a session disconnects. The helper emits jido.messaging.room.participant_joined, jido.messaging.room.participant_left, and jido.messaging.participant.presence_changed signals.

Adapter Integration (Telegram + Discord)

jido_messaging no longer ships in-package Telegram/Discord handlers.
Use adapter packages directly:

Dependencies

def deps do
[
{:jido_chat, "~> 1.0"},
{:jido_chat_telegram, "~> 1.1"},
{:jido_chat_discord, "~> 1.0"},
{:jido_messaging, "~> 1.1"}
]
end

Runtime Configuration

# Telegram
config :jido_chat_telegram,
telegram_bot_token: System.get_env("TELEGRAM_BOT_TOKEN")
# Discord (Nostrum transport)
config :nostrum,
token: System.get_env("DISCORD_BOT_TOKEN")
# Discord webhook verification (optional, recommended)
config :jido_chat_discord,
discord_public_key: System.get_env("DISCORD_PUBLIC_KEY")

Ingress Wiring Pattern

jido_messaging is now the shared ingress runtime:

  1. Host app receives webhook/gateway payload.
  2. Call MyApp.Messaging.route_webhook_request/4 or route_payload/3.
  3. Runtime resolves bridge config, verifies/parses via adapter callbacks, and routes through Jido.Chat.process_event/4.
  4. Message events are ingested; non-message events return typed envelopes.

Canonical APIs:

For adapter-owned listeners (Telegram polling / Discord gateway), pass a sink MFA that targets:

Host Webhook Endpoint (Generic Plug)

defmodule MyApp.Webhooks.Router do
use Plug.Router
plug :match
plug :dispatch
post "/webhooks/:bridge_id" do
conn =
Jido.Messaging.WebhookPlug.call(
conn,
Jido.Messaging.WebhookPlug.init(
instance_module: MyApp.Messaging,
bridge_id_resolver: fn conn -> conn.params["bridge_id"] end
)
)
conn
end
end

Bridge Config Ingress Modes

# Telegram polling ingress (listener worker owned by bridge runtime)
MyApp.Messaging.put_bridge_config(%{
id: "tg_primary",
adapter_module: Jido.Chat.Telegram.Adapter,
opts: %{
ingress: %{
mode: "polling",
token: System.fetch_env!("TELEGRAM_BOT_TOKEN"),
timeout_s: 30,
poll_interval_ms: 500
}
}
})
# Discord gateway ingress (Nostrum ConsumerGroup source by default)
MyApp.Messaging.put_bridge_config(%{
id: "dc_primary",
adapter_module: Jido.Chat.Discord.Adapter,
opts: %{
ingress: %{
mode: "gateway",
poll_interval_ms: 250
}
}
})

Demo Topology Bootstrap (YAML)

The demo task supports declarative topology bootstrap from YAML:

mix jido.messaging.demo --topology config/demo.topology.example.yaml

Live Telegram + Discord bridge demo (env-driven topology):

scripts/demo_bridge_live.sh

Supported top-level keys:

Use config/demo.topology.example.yaml as the starter template. For live bridge ingress with Telegram polling + Discord gateway, use config/demo.topology.live.yaml with .env values.

Architecture

MyApp.Messaging (Supervisor)
├── Runtime (GenServer) - Manages adapter state
└── (Future) RoomSupervisor, InstanceSupervisor
Message Flow:
1. Host webhook endpoint or adapter listener emits into runtime ingress.
2. `InboundRouter` resolves `BridgeConfig` and adapter module by `bridge_id`.
3. Adapter verifies/parses event; runtime routes through `Jido.Chat.process_event/4`.
4. Message events are ingested (room/participant/message + dedupe/session).
5. Outbound delivery runs through `OutboundRouter`/`OutboundGateway`.

Test Lanes

jido_messaging uses lane-based test execution:

Domain Model

Message (Canonical)

%Jido.Messaging.Message{
id: "msg_abc123",
room_id: "room_xyz",
sender_id: "user_123",
thread_id: "thread_123",
external_thread_id: "platform_thread_123",
delivery_external_room_id: "platform_delivery_target_123",
role: :user | :assistant | :system | :tool,
content: [%Content.Text{text: "Hello"}],
status: :sending | :sent | :delivered | :read | :failed,
metadata: %{}
}

Room

%Jido.Chat.Room{
id: "room_xyz",
type: :direct | :group | :channel | :thread,
name: "Support Chat",
external_bindings: %{telegram: %{"bot_id" => "chat_123"}}
}

Participant

%Jido.Chat.Participant{
id: "part_abc",
type: :human | :agent | :system,
identity: %{username: "john", display_name: "John"},
external_ids: %{telegram: "123456789"}
}

Documentation

Full documentation is available at HexDocs.

License

This project is licensed under the Apache 2.0 License - see the LICENSE file for details.