Yoke.sh
Conversations

Message Types

messages.body parts schema, message type conventions, relations, and blobs.

Conversation messages store structure in columns and relations, not envelope metadata. The runtime normalizes body on write — only parts and whitelisted meta keys are kept.

messages row

messages
├── type                 string column (message, reply, reaction, system, edit, …)
├── author_participant_id
├── body                 jsonb
├── branch_id            soft fork only
└── created_at

Author is always author_participant_id. Message kind is type.

body shape

{
  "parts": [
    { "type": "text", "text": "hello", "format": "plain" }
  ],
  "meta": {
    "clientMsgId": "optional-dedup-id",
    "sourceConversationId": "forum-id-for-egress-routing",
    "sourceMessageId": "original-forum-message-id",
    "sourceActorId": "forum-author-actor-id"
  }
}

Whitelisted meta keys: clientMsgId, sourceConversationId, sourceMessageId, sourceActorId, plus agent-specific keys like followUp and guardianInject.

Part types (v1)

typeFields
texttext, format (plain | mrkdwn | html)
imageblobId, alt?
audioblobId, transcript?
videoblobId, caption?, thumbBlobId?
documentblobId, filename?, caption?
reasoningtext
refusaltext
tool_callid, name, input
tool_resulttoolCallId, content, isError?

Media parts reference blobs.id — no inline base64 in jsonb.

Message type conventions

typeUse
messageNormal line — agent LLM output, tool traffic, forum traffic
replyThread/reply semantics — not the user-visible signal for agent output
reactionEmoji reaction message
systemJoin/leave notifications
editReplacement content for earlier line

Tool traffic in monologues uses type: message with tool_call / tool_result parts.

Agent LLM outputs are persisted as type: message on the monologue. User visibility is determined by egress: if the message is agent-authored and contains text parts, egress forwards that text to the forum.

message_relations

Post via conversation_post_message with optional relations:

await ctx.conversations.postMessage(conversationId, {
  parts: [{ type: "text", text: "copied", format: "plain" }],
}, {
  type: "message",
  relations: [{ relationType: "mirror", targetMessageId: sourceId }],
});

Threads are chains of reply relations — no parent column on messages.

blobs

Register metadata, upload bytes out of band (S3):

POST /v1/tenants/:tenant_id/blobs
Content-Type: image/png
Body: <bytes>

Requires YOKE_STORAGE=s3 and YOKE_S3_BUCKET.

Three-actor pipeline (message flow)

Forum:     human posts (type: message)
Ingestion: monologue copy (type: message, meta.source*)
Processor: tool_call + tool_result + agent text (type: message)
Egress:    agent text → forum (type: message) — decides user visibility
Bridge:    agent text on forum → chat platform (Slack, North, …)

Egress listens to all monologue conversation.message.posted events. It forwards agent-authored messages that contain text parts — not tool_call-only or tool_result traffic. See Egress.

The processor and inference loop do not decide what reaches the user. They persist monologue messages; egress reads the stream and copies text to the forum.

Inference alignment

InferenceMessage uses the same body.parts schema. Processor converts transcript rows to inference messages before ctx.llm.infer. See Inference.

On this page