Messaging SDK (Node)

Connect your Node agent to Slack, web chat, and other platforms over gRPC
View as Markdown

The messaging SDK is how your agent container talks to the messaging sidecar that ships next to it. When you declare agent.interfaces.messaging: true in astropods.yml, the platform deploys a messaging container alongside your agent. That sidecar runs platform adapters (Slack, web chat, etc.), normalises every incoming event into a single protobuf shape, and routes it to your agent over a bidirectional gRPC stream.

This page covers the TypeScript SDK, @astropods/messaging. For Python, see Messaging SDK (Python).

If you’re building on a supported framework, prefer a higher-level adapter (e.g. @astropods/adapter-mastra). Those wrap the SDK and handle the streaming loop for you. Reach for the SDK directly when you need full control or when there is no adapter for your framework yet.

Field names below are camelCase — that’s how @grpc/proto-loader exposes proto fields to TypeScript. The corresponding proto names are snake_case.

Install

$bun add @astropods/messaging
$# or: npm install @astropods/messaging

Connect to the sidecar

The sidecar listens on gRPC port 9090. Inside the same pod the address is always localhost:9090; locally with ast dev it’s the same.

1import { MessagingClient } from '@astropods/messaging';
2
3const client = new MessagingClient('localhost:9090');
4await client.connect();
5// or with retry/backoff:
6await client.connectWithRetry({ maxRetries: 10 });

The sidecar talks plaintext gRPC on the loopback interface — no TLS, no auth. The trust boundary is the pod itself.

ReconnectOptions (used by connectWithRetry() and createConversationStream())

FieldTypeDefaultNotes
maxRetriesnumberInfinityCap on retry attempts.
initialDelayMsnumber500Initial backoff delay.
maxDelayMsnumber30_000Cap on backoff delay.
jitterbooleantrueFull-jitter on the backoff delay.
maxBufferSizenumber1000Writes buffered while reconnecting.
retryableStatusCodesnumber[][4, 8, 13, 14]DEADLINE_EXCEEDED, RESOURCE_EXHAUSTED, INTERNAL, UNAVAILABLE.

The conversation stream

The primary RPC is ProcessConversation — a bidirectional stream. The sidecar pushes incoming user messages, feedback, and audio. Your agent pushes back status updates, content chunks, errors, and other responses on the same stream.

1import { MessagingClient, AgentResponse } from '@astropods/messaging';
2
3const client = new MessagingClient('localhost:9090');
4await client.connect();
5const conversation = client.createConversationStream();
6
7conversation.on('response', (resp: AgentResponse) => {
8 if (resp.incomingMessage) {
9 const m = resp.incomingMessage;
10 conversation.sendContentChunk(m.conversationId, {
11 type: 'END',
12 content: `you said: ${m.content}`,
13 });
14 }
15});
16
17conversation.on('error', (err) => console.error('stream error', err));
18conversation.on('reconnecting', (info) => console.warn('reconnecting', info));

ConversationStream events

EventPayloadNotes
responseAgentResponseInbound event from the sidecar.
audioConfigAudioStreamConfigConvenience: emitted in addition to response when audio config arrives.
audioChunkAudioChunkConvenience: emitted in addition to response when an audio chunk arrives.
reconnecting{ attempt, reason, delayMs }Before each retry delay.
reconnected{ attempt }After a successful stream recreation.
errorErrorNon-retryable error OR max retries exceeded.
endOnly on intentional close(), not on unexpected drop.

ConversationStream send methods

MethodWhat it sends
sendMessage(message)A Message.
sendFeedback(feedback)A PlatformFeedback.
sendAgentConfig(config)An AgentConfig.
sendAgentResponse(response)An AgentResponse (typed, any variant).
sendContentChunk(conversationId, chunk)Convenience: wraps ContentChunk in AgentResponse.
sendStatusUpdate(conversationId, status)Convenience: wraps StatusUpdate.
sendTranscript(conversationId, text, msgId?, lang?)Convenience: wraps Transcript.
sendAudioConfig(config)AudioStreamConfig upstream.
sendAudioChunk(chunk)AudioChunk upstream.
endAudio()Sends { done: true } to mark segment end.
end()Closes the stream intentionally.

Inbound message anatomy

An incoming message is a Message. The same shape applies whether it came from Slack, the web chat, or any other adapter.

Message

FieldTypeRequiredNotes
idstringyesUUID assigned by the sidecar.
timestampTimestampyesWhen the platform received the message.
platformstringyes"slack", "web", "discord", etc.
platformContextPlatformContextyesPlatform-native IDs and event metadata.
userUseryesSender identity.
contentstringyesCleaned text. Adapters strip the bot’s @-mention before forwarding.
attachmentsAttachment[]noFiles, images, video, audio, link previews.
conversationIdstringyesStable correlation ID across the message lifecycle. Always echo back on responses.

Timestamp (google.protobuf.Timestamp)

FieldTypeNotes
secondsstringSeconds since UNIX epoch. Encoded as a string (can exceed JS safe int).
nanosnumberNanoseconds within the second.

User

FieldTypeRequiredNotes
idstringyesPlatform-specific user ID.
usernamestringnoDisplay name or handle.
avatarUrlstringnoAvatar URL.
emailstringnoEmail if available.
userData{ [key: string]: string }noPlatform-specific extras (workspace, role, etc.).

Attachment

FieldTypeRequiredNotes
typestringyesOne of the enum values below.
urlstringyesAuthenticated direct-download URL.
filenamestringnoOriginal filename.
sizeBytesnumbernoFile size in bytes.
mimeTypestringnoMIME type.
titlestringnoDisplay title (rich attachments).
descriptionstringnoDisplay description.
widthnumbernoFor images/videos.
heightnumbernoFor images/videos.

Attachment.type values: TYPE_UNSPECIFIED, IMAGE, FILE, VIDEO, AUDIO, LINK.

PlatformContext

FieldTypeRequiredNotes
messageIdstringyesOriginal platform message ID.
channelIdstringyesChannel/room/chat ID.
threadIdstringnoAgent’s reply target. Also set on top-level messages whose response should open a new thread.
threadRootIdstringnoParent thread root timestamp. Set only when this message is a reply inside an existing thread.
channelNamestringnoDisplay channel name.
workspaceIdstringnoSlack workspace, Discord guild, etc.
botUserIdstringnoThe bot’s own user ID in the source platform.
userIdstringnoRaw platform-native sender ID before any cross-platform identity resolution.
eventKindstringyesSee PlatformContextEventKind below.
platformData{ [key: string]: string }noPlatform-specific extras (Slack ts, Discord snowflake, Teams activity ID).

PlatformContextEventKind

ValueWhen the adapter emits it
EVENT_KIND_UNSPECIFIEDFallback. Should not appear in production traffic.
EVENT_KIND_DM1:1 / private chat (Slack DM, web chat session).
EVENT_KIND_APP_MENTIONBot was @-mentioned in a channel or thread.
EVENT_KIND_THREAD_REPLYReply inside an existing thread, no @-mention.
EVENT_KIND_OBSERVEDObserve-channel forward (listen-only).
EVENT_KIND_REACTIONReaction added/removed.
EVENT_KIND_BUTTON_CLICKInteractive button click on a CardAttachment.
EVENT_KIND_SLASH_COMMANDSlash command (Slack/Discord).
EVENT_KIND_ASSISTANT_THREAD_STARTEDSlack assistant thread opened.

Sending a response

AgentResponse

FieldTypeRequiredNotes
conversationIdstringyesMust match the inbound Message.conversationId.
responseIdstringnoStable ID for this response. Used by feedback events.
one variantyesOne of the fields below; @grpc/proto-loader flattens the oneof.

AgentResponse payload variants

VariantTypeNotes
incomingMessageMessageServer → agent only. The inbound message itself.
statusStatusUpdatePre-content typing indicator.
contentContentChunkActual message text, streamed.
promptsSuggestedPromptsQuick-reply suggestions.
threadMetadataThreadMetadataOpen a thread or update its title.
transcriptTranscriptSTT result back to the platform (audio flow).
errorErrorResponseSurface an error to the user.
contextRequestThreadHistoryRequestAsk the sidecar to hydrate thread history.
audioConfigAudioStreamConfigServer → agent only. Audio session start.
audioChunkAudioChunkServer → agent only. Audio bytes.
feedbackPlatformFeedbackServer → agent only. User feedback event.

Streaming text content

ContentChunk

FieldTypeRequiredNotes
typestringyesSTART | DELTA | END | REPLACE (see lifecycle).
contentstringnoSemantics depend on type.
attachmentsResponseAttachment[]noShip with END chunks (or standalone).
platformMessageIdstringnoReturned by the adapter after START; pass on later chunks to update it.
optionsMessageOptionsnoCreation flags.

ContentChunk.type lifecycle

ValueUse
STARTCreate the platform message. May be empty (immediate presence) or include initial content.
DELTAAppend the next token(s). Stream as many as you want.
ENDFinalize. Last content (optional) and any attachments ship here.
REPLACEOverwrite the full message content — for post-stream edits.

MessageOptions

FieldTypeRequiredNotes
ephemeralbooleannoOnly visible to the recipient user.
createThreadbooleannoStart a new thread under the user’s message.
replyToMessageIdstringnoReply to a specific message.
silentbooleannoSuppress notification.

ResponseAttachment (set exactly one variant)

VariantTypeFields
imageImageAttachmenturl, altText?, title?, width?, height?
fileFileAttachmenturl, filename, mimeType?, sizeBytes?
cardCardAttachmentplatformCardJson — Slack Block Kit, Discord Embeds, Teams cards.
linkLinkPreviewurl, title?, description?, imageUrl?

Example:

1conversation.sendContentChunk(cid, { type: 'START', content: '' });
2for await (const token of llm.stream(prompt)) {
3 conversation.sendContentChunk(cid, { type: 'DELTA', content: token });
4}
5conversation.sendContentChunk(cid, { type: 'END', content: '' });

Status updates

StatusUpdate

FieldTypeRequiredNotes
statusstringyesOne of the enum values below.
customMessagestringnoRequired with CUSTOM; otherwise overrides the default phrasing.
emojistringnoPlatform emoji, e.g. :mag:.

StatusUpdate.status values: THINKING, SEARCHING, GENERATING, PROCESSING, ANALYZING, CUSTOM.

1conversation.sendStatusUpdate(cid, { status: 'SEARCHING' });
2conversation.sendStatusUpdate(cid, {
3 status: 'CUSTOM',
4 customMessage: 'Querying the knowledge base…',
5 emoji: ':mag:',
6});

Suggested prompts

SuggestedPrompts

FieldTypeRequiredNotes
promptsPrompt[]yesMax 4–6 depending on platform.

Prompt

FieldTypeRequiredNotes
idstringyesUnique ID. Echoed back in PlatformFeedback.promptSelection.
titlestringyesButton/chip label.
messagestringyesFull message sent on click.
descriptionstringnoTooltip/help text.

Errors

ErrorResponse

FieldTypeRequiredNotes
codestringyesOne of the enum values below.
messagestringyesUser-facing error message.
detailsstringnoTechnical details. Logged, not shown to the user.
retryablebooleannoWhether the platform should offer a retry affordance.

ErrorResponse.code values: RATE_LIMIT, CONTEXT_TOO_LONG, INVALID_REQUEST, AGENT_ERROR, TOOL_ERROR, PLATFORM_ERROR.

Thread metadata

ThreadMetadata

FieldTypeRequiredNotes
threadIdstringnoPlatform thread ID. Set to update an existing thread.
titlestringnoThread title/subject.
createNewbooleannoCreate a new thread.

Transcript

Sent after STT to replace the “[audio]” placeholder on the platform.

Transcript

FieldTypeRequiredNotes
textstringyesTranscribed text.
messageIdstringnoPlaceholder message ID to update.
languagestringnoBCP-47 detected language (e.g. en-US).

Receiving platform feedback

The sidecar sends PlatformFeedback on the same stream when the user interacts with a previous response.

PlatformFeedback

FieldTypeRequiredNotes
conversationIdstringyesConversation this feedback belongs to.
responseIdstringnoWhich agent response this feedback relates to.
timestampTimestampyesWhen the feedback occurred.
userUsernoPlatform user who submitted the feedback. Empty for anonymous/system events.
one variantyesOne of the fields below.

PlatformFeedback variants

VariantTypeNotes
reactionMessageReactionThumbs up/down or custom emoji.
promptSelectionPromptSelectionUser clicked a SuggestedPrompts entry.
buttonClickButtonClickUser clicked a button on a CardAttachment.
streamControlStreamControlUser asked to stop, pause, resume, or regenerate.
messageEditMessageEditUser edited their own previous message.
messageDeleteMessageDeleteUser deleted their own previous message.
textTextFeedbackFree-form text from a platform-native modal.

MessageReaction

FieldTypeRequiredNotes
typenumberyes0=UNSPECIFIED, 1=THUMBS_UP, 2=THUMBS_DOWN, 3=CUSTOM_EMOJI.
emojistringnoPopulated when type === 3.
addedbooleanyestrue = added, false = removed.

PromptSelection

FieldTypeNotes
promptIdstringMatches Prompt.id from a sent suggestion.
promptMessagestringFull message being sent.

ButtonClick

FieldTypeNotes
buttonIdstringButton identifier from card.
valuestringButton value/payload.
actionstringAction identifier.

StreamControl

FieldTypeNotes
actionnumber0=UNSPECIFIED, 1=STOP, 2=PAUSE, 3=RESUME, 4=REGENERATE.
reasonstringWhy (user click, error, etc.).

MessageEdit

FieldTypeNotes
messageIdstringPlatform message ID that was edited.
newContentstringNew content after edit.
originalContentstringOriginal content (if available).
editedAtTimestampWhen the edit happened.

MessageDelete

FieldTypeNotes
messageIdstringPlatform message ID that was deleted.
deletedAtTimestampWhen the delete happened.

TextFeedback

FieldTypeNotes
textstringFree-form text the user typed.
promptstringLabel/title shown above the textbox.
1conversation.on('response', (resp) => {
2 if (resp.feedback?.reaction) {
3 const { type, added } = resp.feedback.reaction;
4 log({ kind: 'reaction', responseId: resp.feedback.responseId, type, added });
5 }
6 if (resp.feedback?.streamControl) {
7 // Cancel in-flight generation for this conversation
8 }
9});

Audio

Audio flows agent-side as raw bytes — the messaging system does no STT, transcoding, or VAD.

StepDirectionMessageNotes
1Sidecar → agentAgentResponse.audioConfigFormat (encoding, sample rate, channels).
2Sidecar → agentAgentResponse.audioChunkRaw bytes. done: true marks end-of-utterance.
3Agent → sidecarAgentResponse.transcriptSTT result; platform replaces the placeholder.

AudioStreamConfig

FieldTypeRequiredNotes
encodingstringyesOne of the AudioEncoding values below.
sampleRatenumberyesHz: 8000 (telephony), 16000 (speech), 48000 (browser).
channelsnumberyes1 = mono (speech default), 2 = stereo.
languagestringnoBCP-47 hint for STT (e.g. en-US).
conversationIdstringyesLinks audio to a conversation.
sourcestringnoOrigin: browser, twilio, vonage, mobile, upload.
userIdstringnoSpeaking user’s identity.

AudioChunk

FieldTypeRequiredNotes
dataBuffer | Uint8ArrayyesRaw audio bytes. Empty when done: true.
sequencenumbernoMonotonic ordering counter.
donebooleannotrue = end of segment, run STT now.

AudioEncoding

ValueUse
LINEAR16PCM signed 16-bit LE. Universal baseline.
MULAWG.711 mu-law. Twilio / telephony (8 kHz).
OPUSRaw Opus frames. Low-latency codec.
MP3MP3. Batch uploads, pre-recorded.
WEBM_OPUSWebM/Opus. Browser MediaRecorder default.
OGG_OPUSOGG/Opus. Firefox MediaRecorder.
FLACFLAC lossless. High-quality uploads.
AACAAC. iOS native recording.

Use audioAsReadable() to feed Mastra’s voice.listen():

1import { audioEncodingToFiletype } from '@astropods/messaging';
2
3conversation.on('audioConfig', async (config) => {
4 const audioStream = conversation.audioAsReadable();
5 const filetype = audioEncodingToFiletype(config.encoding); // 'webm' for WEBM_OPUS
6 const transcript = await agent.voice.listen(audioStream, { filetype });
7 conversation.sendTranscript(config.conversationId, transcript);
8});

Auxiliary RPCs

RPCWhen to use
ProcessMessageServer-streaming for one-shot request/response. Same AgentResponse shape, no inbound feedback.
GetThreadHistoryPull current thread state (handles edits/deletions).
GetConversationMetadataLook up a conversation by ID or by (platform, channel, thread) without fetching history.
ProcessAudioStreamDedicated audio-only stream. First message must be AudioStreamConfig, rest are AudioChunks.
HealthCheckReturns HEALTHY / DEGRADED / UNHEALTHY plus the sidecar version.

ThreadHistoryRequest

FieldTypeRequiredDefaultNotes
conversationIdstringyesConversation to query.
maxMessagesnumberno50How many recent messages to return.
includeEditedbooleannotrueInclude edit history.
includeDeletedbooleannofalseInclude deleted markers.

ThreadHistoryResponse

FieldTypeNotes
conversationIdstringEchoes the request.
messagesThreadMessage[]Recent messages.
isCompletebooleanfalse if truncated by maxMessages.
fetchedAtTimestampWhen the snapshot was taken.

ThreadMessage

FieldTypeNotes
messageIdstringPlatform message ID.
userUserAuthor.
contentstringCurrent content (after edits).
attachmentsAttachment[]Attachments on the message.
timestampTimestampWhen it was sent.
wasEditedbooleanTrue if the message has been edited.
originalContentstringContent before edits.
editedAtTimestampLast edit time.
isDeletedbooleanTrue if the message has been deleted.
deletedAtTimestampDeletion time.
platformData{ [key: string]: string }Platform-specific extras.
1const history = await client.getThreadHistory(conversationId, 50);
2for (const m of history.messages) {
3 // m.content, m.wasEdited, m.isDeleted, m.originalContent, ...
4}

Reconnection

The Node SDK has a built-in reconnect loop on both connectWithRetry() and createConversationStream(). It retries with full-jitter exponential backoff and buffers writes until the stream is back.

1const conversation = client.createConversationStream({
2 maxRetries: 20,
3 initialDelayMs: 500,
4 maxDelayMs: 30_000,
5 jitter: true,
6 maxBufferSize: 1000,
7});
8
9conversation.on('reconnecting', ({ attempt, delayMs }) => { /* … */ });
10conversation.on('reconnected', ({ attempt }) => { /* … */ });

Local development

ast dev runs the messaging sidecar locally so the SDK flow is identical to production. Enable platform adapters per project under dev.interfaces.messaging.adapters in astropods.yml:

1dev:
2 interfaces:
3 messaging:
4 adapters: [web] # or [slack, web]

Open the bundled playground at http://localhost:8080 to drive your agent end-to-end without touching Slack. See the package spec for the full dev.interfaces.messaging schema.

Worked examples

Slack

The Slack adapter forwards five flavours of event:

eventKindSource
EVENT_KIND_DMDirect message to the bot.
EVENT_KIND_APP_MENTION@bot in a channel or thread.
EVENT_KIND_THREAD_REPLYReply inside a thread the bot is already in.
EVENT_KIND_OBSERVEDChannel the bot is observing without being mentioned (listen-only).
EVENT_KIND_REACTION / _BUTTON_CLICK / _SLASH_COMMAND / _ASSISTANT_THREAD_STARTEDInteractive events.

Status updates translate to Slack’s assistant.threads.setStatus. Suggested prompts translate to assistant.threads.setSuggestedPrompts. CardAttachment ships Block Kit JSON straight through.

1import { MessagingClient, AgentResponse, Message } from '@astropods/messaging';
2
3const client = new MessagingClient('localhost:9090');
4await client.connectWithRetry();
5const conversation = client.createConversationStream();
6
7async function handleSlackMessage(m: Message) {
8 const cid = m.conversationId;
9 const kind = m.platformContext?.eventKind;
10
11 if (kind === 'EVENT_KIND_OBSERVED') return; // listen-only
12
13 conversation.sendStatusUpdate(cid, { status: 'THINKING' });
14
15 const openThread = kind === 'EVENT_KIND_APP_MENTION';
16 conversation.sendContentChunk(cid, {
17 type: 'START',
18 content: '',
19 options: openThread ? { createThread: true } : undefined,
20 });
21
22 for await (const token of streamLLM(m.content)) {
23 conversation.sendContentChunk(cid, { type: 'DELTA', content: token });
24 }
25 conversation.sendContentChunk(cid, { type: 'END', content: '' });
26
27 conversation.sendAgentResponse({
28 conversationId: cid,
29 prompts: {
30 prompts: [
31 { id: 'p1', title: 'Show example', message: 'Show me an example' },
32 { id: 'p2', title: 'Go deeper', message: 'Can you explain more?' },
33 ],
34 },
35 });
36}
37
38conversation.on('response', (resp: AgentResponse) => {
39 if (resp.incomingMessage?.platform === 'slack') {
40 handleSlackMessage(resp.incomingMessage);
41 }
42
43 if (resp.feedback?.reaction) {
44 const { type, added } = resp.feedback.reaction;
45 recordFeedback({
46 responseId: resp.feedback.responseId,
47 kind: type === 1 ? 'up' : type === 2 ? 'down' : 'emoji',
48 added,
49 userId: resp.feedback.user?.id,
50 });
51 }
52
53 if (resp.feedback?.promptSelection) {
54 const text = (resp.feedback.promptSelection as any).promptMessage;
55 handleSlackMessage({
56 platform: 'slack',
57 content: text,
58 conversationId: resp.feedback.conversationId,
59 user: resp.feedback.user!,
60 } as Message);
61 }
62});

A Block Kit card with an action button:

1conversation.sendAgentResponse({
2 conversationId: cid,
3 content: {
4 type: 'END',
5 content: 'Deploy status:',
6 attachments: [{
7 card: {
8 platformCardJson: JSON.stringify({
9 blocks: [
10 { type: 'header', text: { type: 'plain_text', text: 'Deploy #4837' } },
11 { type: 'section', fields: [
12 { type: 'mrkdwn', text: '*Status:*\n:white_check_mark: Green' },
13 { type: 'mrkdwn', text: '*Region:*\nus-east-1' },
14 ]},
15 { type: 'actions', elements: [
16 { type: 'button', text: { type: 'plain_text', text: 'View logs' },
17 action_id: 'view_logs', value: 'deploy-4837' },
18 ]},
19 ],
20 }),
21 },
22 }],
23 },
24});

When the user clicks View logs, you’ll get a PlatformFeedback.buttonClick event with buttonId: "view_logs" and value: "deploy-4837".

Web (playground / browser chat)

Every message arrives with platform: "web" and eventKind: 'EVENT_KIND_DM'. The two web-specific concerns are audio input and session-scoped conversations (one conversationId per browser tab).

1import {
2 MessagingClient, AgentResponse, Message,
3 AudioStreamConfig, audioEncodingToFiletype,
4} from '@astropods/messaging';
5
6const client = new MessagingClient('localhost:9090');
7await client.connectWithRetry();
8const conversation = client.createConversationStream();
9
10async function handleWebMessage(m: Message) {
11 const cid = m.conversationId;
12 conversation.sendStatusUpdate(cid, { status: 'GENERATING' });
13 conversation.sendContentChunk(cid, { type: 'START', content: '' });
14
15 for await (const token of streamLLM(m.content)) {
16 conversation.sendContentChunk(cid, { type: 'DELTA', content: token });
17 }
18 conversation.sendContentChunk(cid, { type: 'END', content: '' });
19}
20
21conversation.on('audioConfig', async (config: AudioStreamConfig) => {
22 const audioStream = conversation.audioAsReadable();
23 const filetype = audioEncodingToFiletype(config.encoding);
24 const transcript = await agent.voice.listen(audioStream, { filetype });
25
26 conversation.sendTranscript(config.conversationId, transcript);
27
28 handleWebMessage({
29 platform: 'web',
30 content: transcript,
31 conversationId: config.conversationId,
32 user: { id: config.userId ?? 'unknown' },
33 } as Message);
34});
35
36conversation.on('response', (resp: AgentResponse) => {
37 if (resp.incomingMessage?.platform === 'web') {
38 handleWebMessage(resp.incomingMessage);
39 }
40
41 if (resp.feedback?.streamControl) {
42 const action = (resp.feedback.streamControl as any).action;
43 if (action === 1 /* STOP */) cancelGeneration(resp.feedback.conversationId);
44 }
45});

The bundled playground emits WEBM_OPUS at 48 kHz from MediaRecorder. Firefox emits OGG_OPUS. audioEncodingToFiletype(config.encoding) picks the right STT filetype.

Cross-platform agent

In practice a single agent serves both. The only platform-specific code is whether you open a Slack thread:

1conversation.on('response', (resp) => {
2 const m = resp.incomingMessage;
3 if (!m) return;
4
5 const openThread =
6 m.platform === 'slack' &&
7 m.platformContext?.eventKind === 'EVENT_KIND_APP_MENTION';
8
9 conversation.sendContentChunk(m.conversationId, {
10 type: 'START',
11 content: '',
12 options: openThread ? { createThread: true } : undefined,
13 });
14 // …rest of the loop is identical for Slack and web
15});

Exported symbols

1// Client
2MessagingClient
3ConversationStream
4MessageStream
5Helpers // createMessage, createStatusResponse, createContentResponse, ...
6
7// Types
8Message, User, Attachment, PlatformContext, PlatformContextEventKind, Timestamp
9AgentResponse, ContentChunk, StatusUpdate, SuggestedPrompts, ThreadMetadata
10ErrorResponse, Transcript, ThreadHistoryRequest, ThreadHistoryResponse, ThreadMessage
11AgentConfig, AgentToolConfig, AgentToolGraph
12PlatformFeedback, MessageReaction, TextFeedback
13AudioStreamConfig, AudioChunk, AudioEncoding, audioEncodingToFiletype
14ConversationRequest, ReconnectOptions

The full proto source lives in modules/messaging/proto/astro/messaging/v1/service.proto, message.proto, response.proto, feedback.proto, audio.proto, config.proto.