ConversationManager
(unify/conversation_manager/conversation_manager.py)
is the live “front office”: a singleton that holds in-memory conversation
state, consumes events, runs the reasoning loop, and delegates outbound
I/O. Its package README describes it as the layer that interfaces with
users while delegating complex reasoning to the Actor.
The event model
Everything the runtime reacts to is a typed dataclass extendingEvent
(unify/conversation_manager/events.py).
Subclasses auto-register via __init_subclass__, serialize with
to_json()/from_dict(), and carry behavioral flags — loggable
(persist to transcripts), prominent, and suppress_slow_brain_wake
(don’t schedule a brain turn for this event).
The catalogue is broad and symmetric: for most channels there’s a
*Received and a *Sent class (SMSReceived/SMSSent,
EmailReceived/EmailSent, UnifyMessageReceived/UnifyMessageSent, and —
for the org-installed Teams app — separate DM and channel pairs
MsTeamsBotMessageReceived/MsTeamsBotMessageSent and
MsTeamsBotChannelMessageReceived/MsTeamsBotChannelMessageSent…),
plus call-lifecycle events (PhoneCallReceived, PhoneCallAnswered,
PhoneCallEnded, UnifyMeetStarted…), per-utterance voice events
(InboundPhoneUtterance, OutboundUnifyMeetUtterance…), reactions,
voice-control events (FastBrainNotification, VoiceInterrupt), and
lifecycle events (StartupEvent, TaskDue, PreHireMessage).
Two mapping layers connect the world to handlers:
- Thread → event class.
CommsManager.events_mapincomms_manager.pyturns an envelope’sthreadkey into an event instance, resolving the sender to a contact viaMEDIUM_TO_CONTACT_FIELDand publishing to the in-process broker onapp:comms:*topics. - Event class → handler.
EventHandlerindomains/event_handlers.pyis a registry: handlers are declared with@EventHandler.register(EventClass)and dispatched byEventHandler.handle_event(event, cm). A typical text-message handler logs to transcripts, updates the liveContactIndex, pushes aNotificationBarentry, and requests a brain run.
Mediums and modes
cm_types/medium.py
defines the Medium enum — the single source of truth for channel types
(UNIFY_MESSAGE, EMAIL, SMS_MESSAGE, WHATSAPP_MESSAGE,
WHATSAPP_CALL, PHONE_CALL, UNIFY_MEET, GOOGLE_MEET, TEAMS_MEET,
API_MESSAGE, Slack/Discord variants, and both the delegated-Graph
(TEAMS_MESSAGE, TEAMS_CHANNEL_MESSAGE) and org-installed bot
(MS_TEAMS_BOT_MESSAGE, MS_TEAMS_BOT_CHANNEL_MESSAGE) Teams mediums…).
The two org-installed bot send tools are gated behind the assistant’s
has_ms_teams_bot capability. Each medium registers a MediumInfo with a
mode: TEXT, CALL, or MEET
(cm_types/mode.py).
Mode is what gates behavior — voice tools like guide_voice_agent only
exist in voice sessions, and transcript rows pick their exchange from call
state in voice modes. MEDIUM_TO_CONTACT_FIELD maps external mediums to
the contact field used for identity resolution (phone_number,
whatsapp_number, email_address, slack_user_id…).
The event loop and slow-brain scheduling
ConversationManager.wait_for_events() subscribes to the in-memory broker
(patterns app:comms:*, app:actor:*, and friends), deserializes each
message back into an Event, and dispatches through EventHandler.
Handlers never call the LLM directly — they call request_llm_run(),
which queues a request; flush_llm_requests() coalesces the queue and
submits through a Debouncer
(domains/utils.py)
with queue-of-2 semantics: at most one running turn plus one pending, with
user-origin requests outranking background ones.
A single turn (_run_llm) is assembled from pure parts:
Renderer.render_state()(domains/renderer.py) snapshots the conversation into prompt text (and computes incremental diffs used when steering in-flight work);build_brain_spec()(domains/brain.py) materializes the full inputs for one run — deliberately a small explicit structure so execution strategy can change without touching prompt construction;- the tool surface merges read-only inspection tools
(
ConversationManagerBrainToolsindomains/brain_tools.py), action tools (ConversationManagerBrainActionToolsindomains/brain_action_tools.py— everysend_*,make_call,start_unify_meet,act,wait,guide_voice_agent…), and dynamic steering tools generated per in-flight action handle (task_actions.py:ask/stop/interject/pause/resumeper handle).
wait ends the
turn silently, which is how the assistant’s default “don’t spam” behavior
is implemented.
The domains package
Thedomains/ directory is an ownership map — each module owns one
concern:
Outbound: CommsPrimitives
unify/comms/primitives.py
is the single implementation of assistant-owned outbound
communication — the same CommsPrimitives class serves both the live
brain’s action tools and primitives.comms.* inside Actor plans and
offline task runs.
Every send follows the same pipeline (using send_sms as the example):
_check_outbound_allowed(contact)— the hardshould_respondgate. A contact withshould_respond=Falsecannot be messaged, period; the method returns an explanatory error string the brain must surface.- Detail resolution — attach an inline phone/email to the contact if one was provided.
- HTTP to the gateway — via
comms_utils.py(e.g.POST {COMMS_URL}/phone/send-text). - Delivery proof — on success, a
*Sentevent (SMSSent) is published; its handler writes the transcript row. This is why the prompt-level rule “an outbound message isn’t real until its transcript row appears” holds — the row is the proof.
suppress_slow_brain_wake so the assistant isn’t re-woken by its own
confirmations.
The API message round trip
The developer API channel is the one send path that doesn’t originate a conversation — it completes a caller that is already waiting, so it runs request/response rather than fire-and-forget:- Inbound. Orchestra’s
POST /v0/messagespersists anApiMessagerow and dispatches it to the adapters, which publish anapi_messageenvelope.CommsManager.events_mapmaps that thread toApiMessageReceived, whose handler parks theapi_message_idand the caller’s tags on theConversationManageras_pending_api_message_id/_pending_api_message_tags. - Outbound.
send_api_responsereads that pending id, so it is only meaningful while a call is in flight — with nothing pending it returns{"status": "ok", "note": "no pending api message"}rather than opening a new outbound conversation. It uploads any attachments, thencomms_utils.complete_api_message()issuesPUT {ORCHESTRA_URL}/messages/{id}/complete, which is what flips the caller’s poll fromprocessingtocompleted. - Record. An
api_message_sentevent writes the transcript row, anchored to the boss contact — the API exchange lands in the same durable history as every other medium (Medium.API_MESSAGE).
Transcripts
unify/transcript_manager/transcript_manager.py
owns the durable record: Message rows (medium, sender, receivers,
content, attachments) grouped into Exchange threads
(types/message.py,
types/exchange.py).
The runtime writes through managers_utils.log_message(), which infers
medium and role from the event class, resolves the exchange (from call
state for voice), and calls TranscriptManager.log_messages(). Voice
utterances are stored with call-relative timestamps. These are the same
rows the Console’s
Transcripts pane renders.
Steering and interruptions
Communication interacts with the steering machinery in two directions. Downward: every in-flightact(...) is a steerable
handle the brain can ask, interject, pause, resume, or stop via
dynamically generated tools. Upward: when a user speaks while the slow
brain is mid-turn, the input does not cancel the running turn. The
Debouncer
(domains/utils.py)
holds one running turn plus at most one pending turn — a new submission
replaces whatever was pending and starts when the current turn finishes.
Preemption used to exist (a SpeechUrgencyEvaluator that classified the
utterance and cancelled the running turn) and was removed: the queue of two
is the whole mechanism. What makes interruption feel immediate on a call is
the fast brain — barge-in and the interim turn — not turn cancellation; see
Voice calls.