Chatbridge Protocol
Purpose
This document is the canonical machine-facing contract for implementing a Mutiro chat brain over mutiro agent host --mode=bridge.
If you are deciding whether to swap the brain at all, start with Swap the Built-in Brain — it has a minimal working echo brain. This page is the full protocol reference for building a real one. Reference implementations are listed at the end.
Audience:
- coding agents
- external brain implementers
- first-party integrations that must obey the portable host↔brain boundary
Non-goals:
- owner/admin operations
- presence control
- memory APIs
- working memory APIs
- scheduler APIs
The bridge is chat-lane only.
Process Topology
Your brain is the parent process. It spawns
mutiro agent host --mode=bridge as a child and speaks NDJSON over the
child's stdio. The host emits ready first; your brain then drives
session.initialize and subscription.set, and afterwards answers
host → brain requests such as message.observed.
Inbound message normalization, transcription, attachment preprocessing, and observed-turn shaping are host-owned — your brain receives clean, digested envelopes.
Transport
- transport: NDJSON over stdio
- protocol version:
mutiro.agent.bridge.v1 stdout: protocol onlystderr: diagnostics only
Each line is one complete envelope.
Envelope Schema
Rules:
- every request expecting a direct response MUST include
request_id - direct responses use the same
request_id - success response type is
command_result - failure response type is
error conversation_id,message_id, andreply_to_message_idare envelope routing hints; payload fields remain authoritative when present- exception: on
turn.endthe envelope fields are load-bearing (see its entry), and onmessage.sendan envelopeconversation_idback-fills the payload — so directto_usernamesends must omit it
Auth And Identity
- auth model is agent-key-only
- the host is booted with the agent API key
- the brain never receives or sends owner JWTs
- the brain never receives or sends owner usernames as auth material
- the brain cannot impersonate another member
- all outbound operations execute as the booted agent identity
- media upload is host-owned; the brain never talks to storage directly
Capability Model
Host ready
Host role is host.
Host core capabilities:
session.initializesubscription.setconversation.listconversation.getmessage.sendmessage.forwardmessage.reactturn.end
Host optional capabilities:
message.send_voicesignal.emitmedia.uploadrecall.searchrecall.get
Optional capabilities are advertised only when the host has backing services connected.
Brain capabilities
What your brain must handle when the host sends it:
message.observed(required)session.observed(required)task.request(required)session.snapshot(optional)
Your brain does not announce these in the handshake — the host sends
them and expects command_result or error back.
Startup Handshake
Command:
Sequence:
- host starts
- host emits
ready - brain sends
session.initialize - host returns
command_result - brain sends
subscription.set - host begins streaming host→brain bridge requests such as
message.observed
The host allows about 10 seconds for steps 3–5: complete subscription.set
promptly after launch or the host exits.
Wire discipline
- decoding is strict: an unknown or extra field in any envelope is an error
- one malformed line (invalid JSON or invalid envelope) terminates the transport — all pending requests fail; there is no per-line recovery
- lines are capped at 1 MiB
- brain → host commands are processed one at a time, in order; a slow
command (such as
media.upload) queues the commands behind it. Replies to host → brain requests are matched separately and are not blocked.
session.initialize
- direction: brain → host
- payload type:
mutiro.chatbridge.ChatBridgeInitializeCommand - purpose: open the bridge session; must precede
subscription.set
subscription.set
- direction: brain → host
- payload type:
mutiro.chatbridge.ChatBridgeSubscriptionSetCommand - purpose: declare which conversations the brain wants delivered
Rules:
- set
all: true, or provide at least oneconversation_idsentry - rejected before
session.initialize - replay fields (
replay_recent,from_hashes) are accepted but ignored in bridge mode — only the all/ids subscription is stored
Message Catalog
Host → Brain
message.observed
- direction: host → brain
- payload type:
mutiro.chatbridge.ChatBridgeMessageObserved - purpose: deliver one normalized inbound conversation message
Required payload fields:
message.idmessage.conversation_idmessage.from.username
Notes:
- this is the normal inbound turn entry point
- attachment preprocessing happens host-side before delivery
Message parts
The message.parts array contains normalized ChatBridgeMessagePart objects. Each part has a type string discriminator and type-specific fields. The host digests the raw wire format into this clean shape before delivery, so brain implementations only need to care about the fields documented below.
| type | when it arrives | key fields |
|---|---|---|
text |
User sends a normal typed message | text |
audio |
User sends a voice message (host transcribes upstream) | transcript, url, mime_type, metadata.duration_ms |
image |
User shares a photo or screenshot | url, mime_type, metadata.caption, metadata.width, metadata.height |
file |
User shares a document (PDF, etc.) | url, filename, mime_type, metadata.caption |
card |
An agent sends an interactive A2UI card into the chat | a2ui_json, schema_version, card_id |
card_action |
A user clicks or submits on an interactive card | card_id, card_message_id, action_id, data_json |
contact |
User shares another member's contact | metadata.contact_username, metadata.contact_display_name, metadata.contact_member_type |
reaction |
User adds or removes an emoji reaction on a message | reaction (emoji), reaction_operation (added or removed) |
live_call |
A voice call ends — system posts summary to the thread | call_id, summary_text, duration_ms, end_reason, action_items, follow_ups |
Every part also carries an id field (e.g. part-0, part-1). The
payload can additionally carry inline images, a
reply_to_message_preview, and note parts — treat unknown part types
as opaque and skip them.
Attachment downloads and attachment_context
For image and file parts, the host downloads the actual files from storage into {agent_workspace}/Downloads/ before delivering the envelope. The payload includes an attachment_context string with a system notification listing the downloaded files and their local paths:
This means the brain receives both:
- The structured part metadata (type, caption, filename, dimensions, etc.) in
message.parts - The concrete local file paths in
attachment_context, so the brain can read or analyze the files directly from its workspace
The brain should append attachment_context to the message text so the LLM sees both the part descriptions and the file paths.
Example:
Success response:
session.observed
- direction: host → brain
- payload type:
mutiro.chatbridge.ChatBridgeSessionObserved - purpose: deliver non-user-authored session context
task.request
- direction: host → brain
- payload type:
mutiro.chatbridge.ChatBridgeTaskRequest - purpose: delegated live/chat work
Required result payload:
mutiro.chatbridge.ChatBridgeTaskResulttextmust contain the plain-text delegated result
This is the one required live-lane hook.
session.snapshot
- direction: host → brain
- payload type:
mutiro.chatbridge.ChatBridgeSessionSnapshotRequest - purpose: best-effort live bootstrap snapshot
Portable contract:
- snapshot shape is brain-defined
- no memory-specific fields are required by the portable contract
Recommended fields for good live interop:
system_instructionrecent_messagestool_hintsmetadata
Brain → Host
message.send
- direction: brain → host
- payload type:
mutiro.chatbridge.ChatBridgeSendMessageCommand - purpose: canonical portable send operation for authored outbound messages
Destination rules:
- provide exactly one of:
conversation_idto_username
- use
conversation_idfor normal in-thread replies - use
to_usernamefor direct user-targeted sends — and omit the envelopeconversation_idon those sends (an envelope conversation id back-fills the payload and then fails the exactly-one rule)
Content rules:
- provide exactly one of:
textparts
Host translation:
conversation_idmaps to backendSendToConversationto_usernamemaps to backendSendMessage
Supported content:
textparts
Supported parts are whatever the backend mutiro.messaging.MessagePart supports, including:
textaudioimagefilecardcard_interactionlive_callreactioncontact
Replying is expressed by reply_to_message_id.
Example, plain text:
Example, direct send:
Example, parts:
Response type:
- payload type:
mutiro.messaging.SendToConversationResponse - host normalizes backend
SendMessageResponseinto this same response shape for username-targeted sends
message.send_voice
- direction: brain → host
- payload type:
mutiro.chatbridge.ChatBridgeSendVoiceMessageCommand - purpose: send a host-synthesized voice message while keeping TTS privileged on the host side
Destination rules:
- provide exactly one of:
conversation_idto_username
Content rules:
speechis requiredvoice_nameis optionalreply_to_message_idis optional but should be included for in-thread replies
Host behavior:
- the brain does not call audio services directly
- the host performs TTS using its privileged audio client
- the host sends the resulting audio part through messaging
- if the host has no audio client, it must not advertise
message.send_voice
Example in-thread voice reply:
Example direct voice send by username:
message.forward
- direction: brain → host
- payload type:
mutiro.messaging.ForwardMessageRequest - purpose: backend-native forward
Rules:
- use destination
conversation_id - do not emulate forward by copy in the brain
Example:
Response type:
- payload type:
mutiro.messaging.ForwardMessageResponse
message.react
- direction: brain → host
- payload type:
mutiro.messaging.AddReactionRequest - purpose: react to an existing message
Example:
Response type:
- payload type:
mutiro.messaging.AddReactionResponse
media.upload
- direction: brain → host
- payload type:
mutiro.chatbridge.ChatBridgeMediaUploadCommand - purpose: upload local workspace media through the host
Rules:
- v1 is path-first
local_pathis required- the brain does not upload bytes directly to storage
Example upload request:
Example upload response:
Then use the returned media reference inside message.send.parts.image|audio|file.
signal.emit
- direction: brain → host
- payload type:
mutiro.signal.SendSignalRequest - purpose: emit ephemeral conversation-scoped agent signals
- capability: optional
Bound:
- known stable signal semantics come from backend enum
mutiro.signal.SignalType - unknown semantics use
SIGNAL_TYPE_CUSTOM detail_textis required forSIGNAL_TYPE_CUSTOM
Example:
turn.end
- direction: brain → host
- payload type:
mutiro.chatbridge.ChatBridgeTurnEndCommand - purpose: explicit end-of-turn marker
Rules:
- the envelope
conversation_idis required - set the envelope
reply_to_message_idto the message that started the turn — it settles that message's cursor and read receipt; omitting it leaves them pending
Example:
recall.search
- direction: brain → host
- payload type:
mutiro.recall.RecallSearchRequest - capability: optional
recall.get
- direction: brain → host
- payload type:
mutiro.recall.RecallGetRequest - capability: optional
conversation.list
- direction: brain → host
- payload type:
mutiro.chatbridge.ChatBridgeListConversationsCommand - purpose: list visible conversations
conversation.get
- direction: brain → host
- payload type:
mutiro.chatbridge.ChatBridgeGetConversationCommand - purpose: get one conversation by:
conversation_idwith_member_username
Use with_member_username when the brain needs conversation metadata for a direct chat. It is not required for ordinary direct sends because message.send supports to_username.
host.shutdown
- direction: brain → host
- payload: none
- purpose: request a graceful host shutdown
Rules:
- the host acks with
command_resultfor the samerequest_id, then stops the bridge - after the ack, expect no further envelopes
Host Event Stream
The bridge-mode host emits ready once at startup, then only
command_result, error, and host → brain requests such as
message.observed. Message delivery is message.observed — do not wait
on an event stream.
The envelope type space also reserves status, event.message,
event.conversation_update, event.message_status, event.signal,
event.sync_started, and event.sync_complete for host event streaming;
the bridge-mode host does not emit them today.
Error Model
Error envelope:
Codes the bridge-mode host emits:
invalid_json— the line was not valid JSON (fatal: the transport terminates)invalid_envelope— the envelope failed strict decoding (fatal: the transport terminates)command_failed— everything else: validation failures and backend errors both surface here, with the detail inmessage
The type space also reserves invalid_argument, unsupported_envelope,
not_found, permission_denied, and unavailable; do not depend on
receiving them today.
Retry guidance:
retryableis currently never set true; do not blindly replay non-idempotent mutations on failure — parsemessageand decide
Out Of Scope
Do not expect the bridge to provide:
- memory search/get/write
- working memory get/update
- scheduler create/list/cancel
- owner/admin controls
- presence controls
- read receipts
- direct storage credentials
Live Interop
Live is not part of chatbridge.
To interoperate well with live mode, a bridge brain must implement:
task.request
And should implement:
session.snapshot
task.request result must return:
mutiro.chatbridge.ChatBridgeTaskResult- field:
text
That text should be concise plain text suitable for later relay in the live lane.
Reference Implementations
Open-source brains maintained over this same contract — drop-in replacements and templates for building your own:
- pi-brain — small and pedagogical, the closest thing to "minimum viable brain over stdio"; read this first when learning the protocol
- openclaw-brain — external brain implementation over the bridge contract
- claude-agent-brain — wires a coding-agent runtime into a Mutiro agent over the same bridge
The tutorial with a minimal echo brain is Swap the Built-in Brain.