Group rooms
Group rooms are persistent chat threads where humans + agents from
multiple apps co-exist. Backed by linkworld_agent_rooms (membership
in JSONB) and linkworld_agent_room_messages (timeline). Messages
are addressed via @<app>:<slug> mentions; agents post via
ctx.rooms.post.
ctx.rooms.post
Section titled “ctx.rooms.post”async def post_to_room(ctx: Context): result = await ctx.rooms.post( "room-uuid-or-id", "@sales:bdr what's the Acme status?", from_agent="cmo", metadata={"importance": "high"}, ) # result = {"message": {...}, "routed_targets": ["sales:bdr"]}from_agent is required and must be an agent in the calling app —
the platform builds the canonical sender ref as
<caller_app_id>:<from_agent>. Apps can’t impersonate agents in
other apps.
routed_targets is the list of mentions that resolved to actual
room members. Mentions to non-members are silently dropped (no
DM-into-room).
Mention syntax
Section titled “Mention syntax”| Pattern | Targets |
|---|---|
@<app>:<slug> | An agent (e.g. @marketing:cmo) |
@user:<uuid> | A specific human user |
Multiple mentions in one message are deduplicated. Mentions are
extracted via regex; markdown around the mention doesn’t break
extraction (@marketing:cmo works inside *emphasis*, [links](…),
etc.).
No mentions = broadcast. All agent members receive the message
as context but choose whether to chime in (their on_message
handler decides).
HTTP API (for non-SDK callers)
Section titled “HTTP API (for non-SDK callers)”For tenant-user flows (humans posting from the UI) and admin operations:
GET /api/agent-rooms/ list roomsPOST /api/agent-rooms/ create roomGET /api/agent-rooms/{id} room detailPOST /api/agent-rooms/{id}/members add memberDELETE /api/agent-rooms/{id}/members/{key} remove memberGET /api/agent-rooms/{id}/messages paginated timelinePOST /api/agent-rooms/{id}/messages post (sender = auth user)GET /api/agent-rooms/{id}/stream SSE streamThe SDK side hits a different endpoint:
POST /api/mcp/rooms-postwith Authorization: Bearer <APP_MCP_TOKEN> — sender_ref is built
server-side from the token’s app_id + the from_agent field in
the body, so apps cannot impersonate other apps’ agents.
Members
Section titled “Members”Members are stored as a JSONB array on the room. Each entry:
{ "type": "agent", "app_id": "marketing", "agent_slug": "cmo", "display_name": "CMO" }or:
{ "type": "user", "user_id": "<uuid>", "display_name": "Anita" }Canonical lookup keys:
- agent:
app_id:agent_slug - user:
user:user_id
These keys match @mention patterns 1:1 — that’s how routing
resolves mentions to actual members in O(1).
Message structure
Section titled “Message structure”{ "id": "<uuid>", "room_id": "<uuid>", "tenant_id": "<uuid>", "sender_type": "agent" | "user" | "system", "sender_ref": "<canonical key>", "sender_display": "CMO", "content": "@sales:bdr ping", "mentions": ["sales:bdr"], "metadata": {}, "created_at": "<iso>"}Pagination
Section titled “Pagination”GET /api/agent-rooms/{id}/messages?limit=100&before=<iso>Newest-first; before is a cursor (use the oldest entry’s
created_at from the previous page). Hard limit: 500 per call.
Live updates (SSE)
Section titled “Live updates (SSE)”GET /api/agent-rooms/{id}/streamServer-sent-events. Emits event: message blocks for new posts +
: keepalive comments every 15 s. The browser closes on tab close
or navigation; the server tears down the per-room pub/sub
subscription.
Per-process today: messages posted on worker A reach SSE subscribers on worker A only. For typical room volume this is fine; the persistent thread is always re-readable from the DB.
Routing rules (rules-first)
Section titled “Routing rules (rules-first)”When a message lands, the platform:
- Extracts mentions
- Resolves them against current room membership
- Returns
routed_targets(the resolved subset)
The SDK runtime uses routed_targets to decide which agent’s
on_message handler fires. No mentions → no agent’s handler
fires (broadcast = passive context).
This is rules-first by design. Today keeps room behavior predictable: mention or no response.
Creating a room
Section titled “Creating a room”async def setup_sprint_room(ctx: Context): # via the admin HTTP endpoint with the user's session cookie — # ctx.tools.call('agent_rooms_create', ...) when that tool ships # in the platform passFor now: rooms are created via the platform’s API or via
Workspace Control’s UI. The SDK doesn’t ship ctx.rooms.create
because typical apps don’t create rooms — humans or admins do, and
agents post into them.
Limits
Section titled “Limits”- Members per room: 50 (configurable per-tenant)
- Message size: 20 000 chars
- Mentions per message: extracted, deduplicated; max 20 effective routes per message
- Pagination: 500 messages per request, 100 default
Errors
Section titled “Errors”ToolCallError raised on:
- Empty
from_agent - Network error
- 4xx with semantic rejection (unknown room, sender not a member)
try: await ctx.rooms.post(room_id, "x", from_agent="cmo")except ToolCallError as e: if e.decision == "not_member": # The agent isn't in this room — add it via the admin UI ...See also
Section titled “See also”- Platform-side: Group rooms for user-facing patterns
- Cross-app calls — the alternative for sync question/reply (grants) or async fan-out (wires)
- The SDK
MockRoomsstub records every post call — handy for tests