Multi-chat
A thread holds many humans + bots, yet each bot still gets a clean one-on-one LLM call without lying about authorship. One durable stream is what was said and what each bot did, all interleaved and author-stamped; dispatch decides who wakes; buildMessages projects that shared history into pi messages from one bot’s point of view.
Alice, Bob, bot-X ──► THREAD STREAM (arbe-thread-{id}) chat · pi.assistant · pi.tool_result · signal.* │ every entry author-stamped (authorId) ▼ DISPATCHER cheap routing: mention + always; loop guard, fanout cap │ eligible ▼ BOT GATE ambient only: gating prompt, topic match │ yes ▼ LLM TURN via buildMessages(selfId, entries) │ ▼ appends pi.assistant (authored by the bot) to the same streamThree layers, each with one job. The thread stream is an append-only multi-party log, no alternation constraints — there is no separate per-bot stream, just one shared stream where each agent’s entries carry its authorId. The dispatcher is a pure (event, roster, policy) → wake list; cross-bot rules (loop guard, fanout cap, fairness, dedup) need a global view, so it stays central. The per-bot LLM view is a disposable projection — system prompt is <persona> + <thread context> + <agent roster>. Cost-split: cheap dispatcher first (mention + always routing, loop guard M events per T seconds without human input), then the optional bot-gate (gating prompt, topic match) for ambient.
Projection rules in buildMessages(selfId, entries) → pi.Message[] (packages/core/dispatch/dispatch.ts): a bot’s own pi.assistant/pi.tool_result entries pass through (toolCall↔toolResult pairs preserved — providers require it; a missing result is back-filled so the next complete() never sees a dangling call); another agent’s chat becomes a labelled [Alice]: … user turn; another bot’s assistant text becomes a labelled user turn too, so the target bot can tell speakers apart; other authors’ tool results, chunks, compaction, and lifecycle entries are skipped. Pi’s UserMessage is a projection output, never storage.
chat.chunk streams a bot’s final text turn for live UX; tool activity never chunks. Resume a bot by reading the thread and re-running the projection — the stream is the only durable handle, the projection is recomputed, never stored. A coding session is just a thread with one human + one driving bot (agentId set).
Code: dispatcher + projection in packages/core/dispatch/ (dispatch.ts holds buildMessages and the turn loop, selection.ts the routing).
See threads, system/dispatch, system/agents.
Deferred: per-(bot, thread) trigger policy when one bot inhabits many threads; per-turn checkpoints for cheap replay.