Gateway Rooms

Run a team of agents in one live Telegram / Slack / Discord chat with a durable, deterministic turn-taking transcript

A gateway room puts a roster of agents into one live chat, where they take bounded, @-addressed turns and reply as a single assistant message.

Diagram

👤 User

📡 Channel

🚪 Gateway

🏠 GatewayRoom

🧠 Room Planner

🤖 Rostered Agents

📤 Outbox

Quick Start

  1. Simplest: Python on one platform
    python
    from praisonai_bot.bots import Bot, GatewayRoom
    from praisonaiagents import Agent
    from praisonaiagents.rooms import RoomConfig
    
    researcher = Agent(name="researcher", instructions="Research the topic.")
    critic = Agent(name="critic", instructions="Critique proposals.")
    
    room = GatewayRoom(
        RoomConfig(name="standup", roster=["researcher", "critic"]),
        {"researcher": researcher, "critic": critic},
    )
    
    Bot("telegram", agent=room).start()
  2. With YAML

    Declare the room under rooms: and bind it with a room:<name> route.

    yaml
    agents:
      researcher:
        instructions: Research the topic.
      critic:
        instructions: Critique proposals.
      writer:
        instructions: Write the final summary.
    
    rooms:
      standup:
        agents: [researcher, critic, writer]
        max_rounds: 3
        max_messages: 20
    
    channels:
      telegram:
        token: ${TELEGRAM_BOT_TOKEN}
        routes:
          group: room:standup
  3. Several platforms at once
    python
    from praisonai_bot.bots import BotOS, GatewayRoom
    from praisonaiagents import Agent
    from praisonaiagents.rooms import RoomConfig
    
    researcher = Agent(name="researcher", instructions="Research the topic.")
    critic = Agent(name="critic", instructions="Critique proposals.")
    
    room = GatewayRoom(
        RoomConfig(name="standup", roster=["researcher", "critic"]),
        {"researcher": researcher, "critic": critic},
    )
    
    BotOS(agent=room, platforms=["telegram", "slack"]).start()

User Interaction Flow

  1. A user posts a message in a group chat.
  2. The gateway resolves the route room:standup and calls GatewayRoom.achat(prompt).
  3. Every rostered agent speaks in round 0; later turns go to whoever is @-mentioned.
  4. The combined reply (researcher: … then critic: …) returns through the outbox.
  5. The group sees one assistant turn.
  6. After a restart, the next message resumes the same schedule from the saved transcript.

How a Gateway Room Runs

Diagram
OutboxRostered AgentRoundRobinRoomPlannerGatewayRoomBotSessionManagerGatewayChannelUserOutboxRostered AgentRoundRobinRoomPlannerGatewayRoomBotSessionManagerGatewayChannelUserloop[Each scheduled turn]MessageInboundRoute room:standupachat(prompt)Next speakersTurn scheduleReply with transcriptTextCombined replyDeliverSendReplies
PieceRole
GatewayRoomLooks like one agent (chat, achat, start, astart, chat_history, tools)
Core RoomRuns the planner and collects turns
BotSessionManagerSaves and restores the transcript per user/chat
OutboxDelivers the combined reply like any agent reply

Configuration Options

Each rooms.<name> YAML entry is one RoomConfig.

OptionTypeDefaultDescription
agents (or roster)List[str]— (required)Ordered agent ids that take turns
max_roundsint3Hard cap on turn-taking rounds (>= 1)
max_messagesint20Hard cap on messages per human message (>= 1)
plannerstr"round_robin"Only "round_robin" is valid

Durable Transcript

The transcript lives in GatewayRoom.chat_history, so a restart loses nothing.

Diagram

📨 New Message

🔄 Session loads chat_history

🧠 Planner replays schedule

🤖 Next turns

💾 Session store

  • BotSessionManager swaps the transcript in per user/chat and saves it to the configured session store.
  • Member replies are stored as {"role": "assistant", "content": "<id>: <text>"}, because session stores keep only role and content.
  • Ids containing ": " still reload intact, since the longest id is matched first.
  • The session’s per-agent lock keeps one writer per room.

Member Isolation

Each agent is cloned with clone_for_channel() when it joins a room, so history and tools never leak between rooms or routes.

Diagram

Yes

No or raises

🤖 Your Agent

clone_for_channel works?

✅ Room-private copy

⚠️ Logged, member passes

The room swaps each member’s chat_history and tools per turn, so a shared instance would corrupt other routes.


Choosing a Surface

Diagram

Live chat
Telegram / Slack / Discord

In-process, run to completion

Own storage and delivery

Where does the team run?

GatewayRoom + rooms: YAML

Core Room

Drive RoundRobinRoomPlanner yourself


Common Patterns

Different rooms on different channels
yaml
routes:
  group: room:standup
  dm: support

A room is routed exactly like an agent id.

Missing roster member

build_rooms logs roster ids [...] are not defined under 'agents:' and will pass, and the gateway keeps running.

Bad room config

A bad roster is rejected at load time as rooms.<name>: <reason>, before any traffic arrives.

Narrow tools per route

A route tool_policy narrows the room’s tools (the union of its members’ tools) for one turn, then restores them.

Attachments and cancel

attachments go to each member whose chat / achat accepts them. A set cancel_token stops the roster between turns by raising InterruptedError.


Best Practices

Keep the caps low

The defaults (max_rounds: 3, max_messages: 20) already bound cost per human message. Raise them only when needed.

Use agents that can be cloned

Members without clone_for_channel() pass instead of sharing state, so they never speak.

Avoid ': ' in agent ids

Reload handles it, but plain ids keep the transcript readable.

Order the roster on purpose

Round-0 fan-out and multi-mention turns both follow roster order.

Pair with a turn lock across replicas

Use Gateway Turn Lock when several gateway replicas serve the same chat.


Related