Conversation Rooms
Deterministic, restart-safe turn-taking for a team of agents in one shared chat
A conversation room lets a roster of agents share one chat and reply on their own — you send a message, the room runs the scheduled turns in a deterministic, bounded, restart-safe order and hands you the replies.
Quick Start
Build a team of agents, name the roster, and send one message — the room runs every scheduled turn and returns the replies.
- Simple room
Three agents share one chat.
on_messagefans out to the whole roster and returns their replies.python from praisonaiagents import Agent from praisonaiagents.rooms import Room, RoomConfig, RoomEvent researcher = Agent(name="researcher", instructions="Research the topic.") critic = Agent(name="critic", instructions="Critique proposals.") writer = Agent(name="writer", instructions="Write the final draft.") cfg = RoomConfig(name="standup", roster=["researcher", "critic", "writer"]) room = Room(cfg, agents={ "researcher": researcher.achat, "critic": critic.achat, "writer": writer.achat, }) replies = await room.on_message(RoomEvent(speaker="user", content="plan the sprint @critic")) for turn in replies: print(f"{turn.speaker}: {turn.content}") - Configure caps
Bound the conversation with
max_roundsandmax_messagesso a room can never loop unbounded.python from praisonaiagents.rooms import RoomConfig cfg = RoomConfig( name="standup", roster=["researcher", "critic", "writer"], max_rounds=2, max_messages=10, ) - Load from YAML / dict
Build the config from a parsed mapping. Either
roster:oragents:names the roster.python from praisonaiagents.rooms import RoomConfig cfg = RoomConfig.from_dict("standup", { "agents": ["researcher", "critic", "writer"], "max_rounds": 2, }) - Advanced: drive the planner yourself
If you’re building a wrapper that persists its own transcript, drive
plan_nextdirectly instead of usingRoom.python from praisonaiagents.rooms import RoomEvent, RoundRobinRoomPlanner roster = ["planner", "coder", "reviewer"] transcript = [RoomEvent(speaker="user", content="build a login form", round=0)] planner = RoundRobinRoomPlanner() while (next_speaker := planner.plan_next(roster, transcript)) is not None: print("next up:", next_speaker) # Run the named agent, then append its reply below. transcript.append(RoomEvent(speaker=next_speaker, content="...", round=0))
User Interaction Flow
A real user builds a team and asks the room a question — no manual loop, no reset.
- Create
Agentobjects. - Declare a
RoomConfignaming the roster. - Build a
Room(cfg, agents=...). await room.on_message(RoomEvent(speaker="user", content="...")).- The room fans out to round 0, honours any
@-mentions for later rounds, and returns the replies. - The next user message reopens a fresh activity automatically.
How a Room Runs
Room.on_message appends your event, drains the planner, runs each scheduled agent, and returns the replies.
The planner is pure, so persisting room.transcript and rebuilding the room is enough to resume — replaying a settled transcript returns no next speaker, so there is no double-execution.
Choosing a Configuration Surface
Pick where your room config lives.
Configuration Options
RoomConfig is a dataclass describing the roster and planner caps.
| Option | Type | Default | Description |
|---|---|---|---|
name | str | — (required) | Room identifier (e.g. room:standup). Must be non-empty, else ValueError. |
roster | List[str] | [] | Ordered agent ids that take turns. |
planner | str | "round_robin" | Planner selector. Only "round_robin" is valid; any other value raises ValueError. |
max_rounds | int | 3 | Hard cap on turn-taking rounds. Must be >= 1. |
max_messages | int | 20 | Hard cap on messages per human-initiated activity. Must be >= 1. |
RoomConfig methods:
build_planner()— constructsRoundRobinRoomPlanner(max_rounds=..., max_messages=...).to_dict()— plain dict for YAML round-tripping or diagnostics.RoomConfig.from_dict(name, data)— builds from a parsed mapping; acceptsroster:oragents:. Rejects scalar values (bare string, bytes, mapping) with an actionableValueError.
RoundRobinRoomPlanner is a dataclass with two hard caps.
| Option | Type | Default | Description |
|---|---|---|---|
max_rounds | int | 3 | No turns are scheduled at or beyond this round. Must be >= 1. |
max_messages | int | 20 | No turns once the activity reaches this many events (human + agent, counting passes). Must be >= 1. |
RoomEvent is a frozen dataclass — immutable, hashable, and safe to persist or replay.
| Field | Type | Default | Description |
|---|---|---|---|
speaker | str | — | Agent id from the roster, or any non-roster id (e.g. a human participant). |
content | str | "" | Message text. Scanned for @name addressing. |
round | int | 0 | Turn-taking round this event belongs to. |
passed | bool | False | True means the speaker was scheduled but passed. Still consumes the turn. |
For the full Room and RoomConfig API surface, see the auto-generated SDK reference.
Every Room and RoomConfig parameter, method, and default.
Turn-Taking Rules
Round 0 — opening fan-out
After a human message, every rostered agent speaks once in roster order. This is the “everyone on round 0” default.
Later rounds — @-mention admission
A round N+1 admits only the agents that were @-mentioned by an agent during round N, and only agents actually on the roster. Mentions are honoured in roster order (not mention order). An agent speaks at most once per round.
Bounded — hard stops
max_rounds and max_messages are hard stops; a room can never loop unbounded.
Restart-safe — pure replay
The plan is a pure function of (roster, transcript). Rebuild a Room from a persisted transcript and plan_next returns the identical next speaker — no double-execution, no lost turn.
Current Activity Semantics
The planner scopes accounting to the slice of the transcript from the last human (non-roster) message onward.
- A room only starts once a human (non-roster) message has been seen. Before that,
plan_nextreturnsNone. max_messagesis per activity, not lifetime. A settled room reliably restarts on the next human message, no matter how long the historical transcript is.@-mention admission is also scoped to the current activity — stale mentions from a prior settled activity do not leak into a new activity’s later rounds.
Mention Parsing
extract_mentions(text) returns the ordered, de-duplicated list of @name mentions found in text.
from praisonaiagents.rooms import extract_mentions
extract_mentions("@coder and @reviewer, then @coder again")
# ['coder', 'reviewer'] — first-seen order, de-duplicated
extract_mentions("email me at foo@bar.com")
# [] — a @ glued to a word character is not a mention
extract_mentions("")
# []- First-seen order is preserved so addressing is deterministic.
- Case is preserved; matching against roster ids is case-sensitive.
- Name grammar:
@followed by[A-Za-z0-9][\w\-.]*— letters, digits, underscore, dash, dot; a name may not start with_,-, or.. - Email-safe: a
@glued to a preceding word character does not match, so scanning human text is safe.
Common Patterns
Concurrent messages are safe
Two overlapping on_message calls do not interleave — each is serialised through an internal asyncio.Lock, so every human message opens exactly one round-0 activity and no agent double-executes for one activity.
Unknown roster id passes automatically
An id in roster with no matching agents entry records a passed=True turn so the planner advances instead of looping forever on a missing agent.
from praisonaiagents.rooms import Room, RoomConfig, RoomEvent
async def reply(prompt: str) -> str:
return "on it"
cfg = RoomConfig(name="r", roster=["ghost", "real"])
room = Room(cfg, agents={"real": reply}) # "ghost" has no callable
replies = await room.on_message(RoomEvent(speaker="user", content="hi"))
# "ghost" records passed=True; "real" replies.Custom render
Pass render=lambda transcript: ... to control the prompt handed to each agent. The default joins speaker: content lines.
from praisonaiagents.rooms import Room, RoomConfig
cfg = RoomConfig(name="r", roster=["a", "b"])
room = Room(
cfg,
agents={"a": a.achat, "b": b.achat},
render=lambda transcript: "\n".join(f"[{e.speaker}] {e.content}" for e in transcript),
)Custom planner
Pass planner=MyPlanner() for a domain-specific policy. The runtime tags each turn with the activity’s current round rather than inventing round-robin admission, so custom planners are never fed misleading round labels.
from typing import Optional, Sequence
from praisonaiagents.rooms import Room, RoomConfig, RoomEvent, RoomTurnPlannerProtocol
class FirstAgentPlanner:
def plan_next(
self,
roster: Sequence[str],
transcript: Sequence[RoomEvent],
) -> Optional[str]:
return roster[0] if roster else None
assert isinstance(FirstAgentPlanner(), RoomTurnPlannerProtocol)
cfg = RoomConfig(name="r", roster=["a", "b"])
room = Room(cfg, agents={"a": a.achat, "b": b.achat}, planner=FirstAgentPlanner())Explicit pass
An agent with nothing to add returns an empty reply, recorded as a turn that advances the planner without adding content.
Best Practices
Keep custom planners pure
No clock, no RNG, no I/O. Purity is the whole property that makes a room restart-safe.
Roster order is your tie-breaker
When multiple agents are admitted (round 0, or several @-mentions in one round), they speak in roster order, not mention order. Design roster order intentionally.
Set caps conservatively
max_rounds and max_messages are the guardrails against runaway loops. RoomConfig() defaults to 3 rounds and 20 messages per activity.
Persist the transcript for durability
Room keeps the transcript in memory only. To survive a restart, persist room.transcript in your wrapper and rebuild the room from it — the pure planner resumes the identical schedule.
Don't rely on lifetime transcript length
Message caps are per activity (per human message), by design. A long historical transcript never permanently disables a room.
Make it Live
The core Room is in-memory and I/O-free by design. To run a room over a live platform (Telegram / Slack / Discord / webhook) with a durable transcript and restart-safe schedule, use GatewayRoom from praisonai_bot.bots — it wraps the same core Room, swaps the transcript in and out via BotSessionManager, and plugs in anywhere a single agent is bound today.
Run a rostered room over a live platform with a durable, restart-safe transcript.
Related
Full Room and RoomConfig API surface.
Run a room over Telegram / Slack / Discord with a durable transcript.
Bot GatewayThe praisonai-bot wrapper adds durable persistence and platform delivery.
The in-process, run-to-completion counterpart to a live conversation room.
Gateway Turn LockSerialise per-session turns across replicas.