> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.astropods.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.astropods.com/_mcp/server.

# Worked examples

Each example is a full loop: connect, handle inbound messages, stream a reply, and react to feedback.

## Slack

The Slack adapter forwards five flavors of event:

| Event kind                                                                               | Source                                                              |
| ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
| `EVENT_KIND_DM`                                                                          | Direct message to the bot.                                          |
| `EVENT_KIND_APP_MENTION`                                                                 | `@bot` in a channel or thread.                                      |
| `EVENT_KIND_THREAD_REPLY`                                                                | Reply inside a thread the bot is already in.                        |
| `EVENT_KIND_OBSERVED`                                                                    | Channel the bot is observing without being mentioned (listen-only). |
| `EVENT_KIND_REACTION` / `_BUTTON_CLICK` / `_SLASH_COMMAND` / `_ASSISTANT_THREAD_STARTED` | Interactive events.                                                 |

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

#### Node

```typescript
import { MessagingClient, AgentResponse, Message } from '@astropods/messaging';

const client = new MessagingClient('localhost:9090');
await client.connectWithRetry();
const conversation = client.createConversationStream();

async function handleSlackMessage(m: Message) {
  const cid = m.conversationId;
  const kind = m.platformContext?.eventKind;

  if (kind === 'EVENT_KIND_OBSERVED') return; // listen-only

  conversation.sendStatusUpdate(cid, { status: 'THINKING' });

  const openThread = kind === 'EVENT_KIND_APP_MENTION';
  conversation.sendContentChunk(cid, {
    type: 'START',
    content: '',
    options: openThread ? { createThread: true } : undefined,
  });

  for await (const token of streamLLM(m.content)) {
    conversation.sendContentChunk(cid, { type: 'DELTA', content: token });
  }
  conversation.sendContentChunk(cid, { type: 'END', content: '' });

  conversation.sendAgentResponse({
    conversationId: cid,
    prompts: {
      prompts: [
        { id: 'p1', title: 'Show example', message: 'Show me an example' },
        { id: 'p2', title: 'Go deeper',    message: 'Can you explain more?' },
      ],
    },
  });
}

conversation.on('response', (resp: AgentResponse) => {
  if (resp.incomingMessage?.platform === 'slack') {
    handleSlackMessage(resp.incomingMessage);
  }

  if (resp.feedback?.reaction) {
    const { type, added } = resp.feedback.reaction;
    recordFeedback({
      responseId: resp.feedback.responseId,
      kind: type === 1 ? 'up' : type === 2 ? 'down' : 'emoji',
      added,
      userId: resp.feedback.user?.id,
    });
  }

  if (resp.feedback?.promptSelection) {
    const text = (resp.feedback.promptSelection as any).promptMessage;
    handleSlackMessage({
      platform: 'slack',
      content: text,
      conversationId: resp.feedback.conversationId,
      user: resp.feedback.user!,
    } as Message);
  }
});
```

A Block Kit card with an action button:

```typescript
conversation.sendAgentResponse({
  conversationId: cid,
  content: {
    type: 'END',
    content: 'Deploy status:',
    attachments: [{
      card: {
        platformCardJson: JSON.stringify({
          blocks: [
            { type: 'header', text: { type: 'plain_text', text: 'Deploy #4837' } },
            { type: 'section', fields: [
              { type: 'mrkdwn', text: '*Status:*\n:white_check_mark: Green' },
              { type: 'mrkdwn', text: '*Region:*\nus-east-1' },
            ]},
            { type: 'actions', elements: [
              { type: 'button', text: { type: 'plain_text', text: 'View logs' },
                action_id: 'view_logs', value: 'deploy-4837' },
            ]},
          ],
        }),
      },
    }],
  },
});
```

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

#### Python

```python
import queue, threading, grpc
from astropods_messaging import (
    AgentMessagingStub, ConversationRequest, AgentResponse,
    ContentChunk, StatusUpdate, SuggestedPrompts,
)
from astropods_messaging.astro.messaging.v1.message_pb2 import PlatformContext

channel = grpc.insecure_channel("localhost:9090")
stub = AgentMessagingStub(channel)
outbound: queue.Queue = queue.Queue()

def send(resp: AgentResponse):
    outbound.put(ConversationRequest(agent_response=resp))

def handle_slack_message(m):
    cid = m.conversation_id
    kind = m.platform_context.event_kind

    if kind == PlatformContext.EVENT_KIND_OBSERVED:
        return  # listen-only

    send(AgentResponse(
        conversation_id=cid,
        status=StatusUpdate(status=StatusUpdate.THINKING),
    ))

    open_thread = kind == PlatformContext.EVENT_KIND_APP_MENTION
    send(AgentResponse(
        conversation_id=cid,
        content=ContentChunk(type=ContentChunk.START, content=""),
    ))

    for token in stream_llm(m.content):
        send(AgentResponse(
            conversation_id=cid,
            content=ContentChunk(type=ContentChunk.DELTA, content=token),
        ))

    send(AgentResponse(
        conversation_id=cid,
        content=ContentChunk(type=ContentChunk.END, content=""),
    ))

    send(AgentResponse(conversation_id=cid, prompts=SuggestedPrompts(prompts=[
        SuggestedPrompts.Prompt(id="p1", title="Show example",
                                message="Show me an example"),
        SuggestedPrompts.Prompt(id="p2", title="Go deeper",
                                message="Can you explain more?"),
    ])))

def requests():
    while True:
        yield outbound.get()

for resp in stub.ProcessConversation(requests()):
    payload = resp.WhichOneof("payload")
    if payload == "incoming_message" and resp.incoming_message.platform == "slack":
        threading.Thread(target=handle_slack_message,
                         args=(resp.incoming_message,)).start()
    elif payload == "feedback" and resp.feedback.HasField("reaction"):
        r = resp.feedback.reaction
        record_feedback(resp.feedback.response_id, r.type, r.added)
```

A Block Kit card with an action button:

```python
import json
from astropods_messaging.astro.messaging.v1.response_pb2 import (
    ResponseAttachment, CardAttachment,
)

block_kit = {
    "blocks": [
        {"type": "header", "text": {"type": "plain_text", "text": "Deploy #4837"}},
        {"type": "section", "fields": [
            {"type": "mrkdwn", "text": "*Status:*\n:white_check_mark: Green"},
            {"type": "mrkdwn", "text": "*Region:*\nus-east-1"},
        ]},
        {"type": "actions", "elements": [
            {"type": "button",
             "text": {"type": "plain_text", "text": "View logs"},
             "action_id": "view_logs", "value": "deploy-4837"},
        ]},
    ],
}

send(AgentResponse(
    conversation_id=cid,
    content=ContentChunk(
        type=ContentChunk.END,
        content="Deploy status:",
        attachments=[ResponseAttachment(
            card=CardAttachment(platform_card_json=json.dumps(block_kit)),
        )],
    ),
))
```

When the user clicks **View logs**, you'll get a `PlatformFeedback.button_click` event with `button_id="view_logs"` and `value="deploy-4837"`.

## Web chat

Every message arrives from the `web` platform with a DM event kind. The two web-specific concerns are **audio input** and **session-scoped conversations** (one conversation ID per browser tab).

#### Node

```typescript
import {
  MessagingClient, AgentResponse, Message,
  AudioStreamConfig, audioEncodingToFiletype,
} from '@astropods/messaging';

const client = new MessagingClient('localhost:9090');
await client.connectWithRetry();
const conversation = client.createConversationStream();

async function handleWebMessage(m: Message) {
  const cid = m.conversationId;
  conversation.sendStatusUpdate(cid, { status: 'GENERATING' });
  conversation.sendContentChunk(cid, { type: 'START', content: '' });

  for await (const token of streamLLM(m.content)) {
    conversation.sendContentChunk(cid, { type: 'DELTA', content: token });
  }
  conversation.sendContentChunk(cid, { type: 'END', content: '' });
}

conversation.on('audioConfig', async (config: AudioStreamConfig) => {
  const audioStream = conversation.audioAsReadable();
  const filetype = audioEncodingToFiletype(config.encoding);
  const transcript = await agent.voice.listen(audioStream, { filetype });

  conversation.sendTranscript(config.conversationId, transcript);

  handleWebMessage({
    platform: 'web',
    content: transcript,
    conversationId: config.conversationId,
    user: { id: config.userId ?? 'unknown' },
  } as Message);
});

conversation.on('response', (resp: AgentResponse) => {
  if (resp.incomingMessage?.platform === 'web') {
    handleWebMessage(resp.incomingMessage);
  }

  if (resp.feedback?.streamControl) {
    const action = (resp.feedback.streamControl as any).action;
    if (action === 1 /* STOP */) cancelGeneration(resp.feedback.conversationId);
  }
});
```

#### Python

```python
import queue, threading, grpc
from astropods_messaging import (
    AgentMessagingStub, ConversationRequest, AgentResponse,
    ContentChunk, StatusUpdate, Transcript,
)

channel = grpc.insecure_channel("localhost:9090")
stub = AgentMessagingStub(channel)
outbound: queue.Queue = queue.Queue()

audio_buffers: dict[str, bytearray] = {}
audio_configs: dict[str, object] = {}

def send(resp: AgentResponse):
    outbound.put(ConversationRequest(agent_response=resp))

def handle_web_message(m):
    cid = m.conversation_id
    send(AgentResponse(
        conversation_id=cid,
        status=StatusUpdate(status=StatusUpdate.GENERATING),
    ))
    send(AgentResponse(
        conversation_id=cid,
        content=ContentChunk(type=ContentChunk.START, content=""),
    ))
    for token in stream_llm(m.content):
        send(AgentResponse(
            conversation_id=cid,
            content=ContentChunk(type=ContentChunk.DELTA, content=token),
        ))
    send(AgentResponse(
        conversation_id=cid,
        content=ContentChunk(type=ContentChunk.END, content=""),
    ))

def requests():
    while True:
        yield outbound.get()

for resp in stub.ProcessConversation(requests()):
    payload = resp.WhichOneof("payload")

    if payload == "incoming_message" and resp.incoming_message.platform == "web":
        threading.Thread(
            target=handle_web_message,
            args=(resp.incoming_message,),
        ).start()

    elif payload == "audio_config":
        cfg = resp.audio_config
        audio_configs[cfg.conversation_id] = cfg
        audio_buffers[cfg.conversation_id] = bytearray()

    elif payload == "audio_chunk":
        chunk = resp.audio_chunk
        # Single-session example — maintain a per-session mapping in real apps
        cid = next(iter(audio_configs))
        audio_buffers[cid].extend(chunk.data)
        if chunk.done:
            cfg = audio_configs.pop(cid)
            audio = audio_buffers.pop(cid)
            text = run_stt(bytes(audio),
                           encoding=cfg.encoding,
                           sample_rate=cfg.sample_rate)
            send(AgentResponse(
                conversation_id=cid,
                transcript=Transcript(text=text),
            ))
            handle_web_message(type("M", (), {
                "conversation_id": cid, "content": text,
            }))
```

The bundled playground emits `WEBM_OPUS` at 48 kHz from `MediaRecorder`. Firefox emits `OGG_OPUS`. Branch on the config's encoding to pick 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.

#### Node

```typescript
conversation.on('response', (resp) => {
  const m = resp.incomingMessage;
  if (!m) return;

  const openThread =
    m.platform === 'slack' &&
    m.platformContext?.eventKind === 'EVENT_KIND_APP_MENTION';

  conversation.sendContentChunk(m.conversationId, {
    type: 'START',
    content: '',
    options: openThread ? { createThread: true } : undefined,
  });
  // …rest of the loop is identical for Slack and web
});
```

#### Python

```python
from astropods_messaging.astro.messaging.v1.response_pb2 import MessageOptions

m = resp.incoming_message
open_thread = (
    m.platform == "slack"
    and m.platform_context.event_kind == PlatformContext.EVENT_KIND_APP_MENTION
)
send(AgentResponse(
    conversation_id=m.conversation_id,
    content=ContentChunk(
        type=ContentChunk.START,
        content="",
        options=MessageOptions(create_thread=True) if open_thread else None,
    ),
))
# …rest of the loop is identical for Slack and web
```

## Next steps

* [Files in chat](/messaging-sdk/files-in-chat): accept uploads and return downloads
* [SDK reference](/messaging-sdk/reference): every exported symbol