Worked examples

Complete Slack, web chat, and cross-platform agent loops

View as Markdown

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 kindSource
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.

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:

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".

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).

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);
}
});

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.

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
});

Next steps