Message Flow
July 31, 2026 · View on GitHub
A single turn from continue_chat to AgentMessage.
Turn lifecycle
caller OVOSMessagebusChatAgent OVOS bus
| | |
| continue_chat(messages, sid) --->| |
| | look up sess from |
| | SessionManager.sessions[sid] |
| | (create if absent) |
| | apply lang/units overrides |
| | SessionManager.update(sess) |
| | |
| | emit recognizer_loop:utterance ->
| | context.session = sess.serialize()
| | |
| | (pipeline runs)
| | |
| |<--- speak (utt #1, session) |
| | query.responses.append |
| | _extend_timeout = True |
| | |
| |<--- speak (utt #2, session) |
| | query.responses.append |
| | |
| |<--- ovos.utterance.handled |
| | query.handled.set() |
| | |
|<--- AgentMessage(ASSISTANT, ...) | |
Cross-turn Session reuse
The interesting story is what happens between turns.
Turn 1 (session_id="kitchen"):
- Agent reads
SessionManager.sessions["kitchen"]. It is absent, so the agent createsSession(session_id="kitchen")and registers it. - Emits utterance with that Session attached.
- A skill (say, "weather") activates itself and calls
get_response. It mutates the Session: adds itself toactive_skills, enters response-mode, writes entity context. - Every reply message the skill emits carries the mutated Session in
context.session. - The agent's
_on_speakcallback callsSessionManager.get(message), which callsSession.from_messageand writes the updated Session back intoSessionManager.sessions["kitchen"].
Turn 2 (session_id="kitchen"):
- Agent reads
SessionManager.sessions["kitchen"]: the mutated Session from turn 1. - Emits the new utterance with that Session attached.
- The "weather" skill sees its own
active_skillsentry and itsresponse-modeflag, treats the new utterance as a follow-up, and resolves the conversation.
This is the whole reason OVOSMessagebusChatAgent is a ChatEngine and not a
single-shot solver. The agent owns none of this state, SessionManager does.
By looking up by session_id instead of minting a fresh UUID, the plugin
lets every multi-turn OVOS feature work transparently.
End-of-turn detection
The OVOS pipeline emits exactly one ovos.utterance.handled per utterance,
after every skill that wanted to respond has spoken. The agent uses that as
the turn-complete signal.
The rolling timeout handles multi-speak responses:
speak -> _extend_timeout = True
speak -> _extend_timeout = True
... pause ...
(timeout slice elapses with _extend_timeout=True → wait once more)
ovos.utterance.handled -> _extend_timeout = False, handled.set()
The wait loop only exits when either handled is set or a full timeout
slice elapses with no further speak activity. This means a skill that
streams sentences one at a time over several seconds still completes cleanly.
Streaming
stream_sentences yields each speak content as it arrives instead of
joining at the end. It uses the same end-of-turn detection and the same
rolling timeout. The yield ordering matches the OVOS bus emission order,
which is the order skills emitted their speak calls.
What does not flow through this plugin
- Binary payloads (audio data): a different OVOS subsystem entirely.
- Wake-word activation, STT, TTS: the agent's input is already text and its output is text.
- Pipeline routing decisions (which skill handles what): owned by OVOS.
mycroft.session.updatedirect events: already consumed bySessionManagerbefore the agent sees them.