Formatter & Semantic Blocks
Formatter & Semantic Blocks
The SDK uses Semantic Blocks as a platform-agnostic intermediate representation for all messages flowing from bridge to platform. Your Formatter implementation renders these blocks into platform-native format.
Formatter Interface
interface Formatter {
render(blocks: SemanticBlock[]): FormattedContent[]
renderStreaming?(text: string): FormattedContent
}
render(blocks)
Converts an array of semantic blocks into one or more FormattedContent payloads. Return multiple parts when content exceeds the platform's message size limit.
renderStreaming(text) — optional
Lightweight rendering for live streaming text. Called by TurnStreamer during incremental edits. If not provided, the streamer uses plain text.
Semantic Block Types
type SemanticBlock =
| { kind: 'text'; text: string; inlineFormat?: 'md' }
| { kind: 'heading'; level: 1 | 2 | 3; text: string }
| { kind: 'code'; lang?: string; text: string }
| { kind: 'quote'; text: string; expandable?: boolean }
| { kind: 'list'; ordered: boolean; items: string[] }
| { kind: 'hr' }
| { kind: 'link'; href: string; label: string }
| { kind: 'tool_card'; name: string; summary: string;
inputJson: string; result?: { content: string; isError?: boolean } }
| { kind: 'task_card'; taskId: string; status: string; summary: string }
| { kind: 'user_message_card';
entries: Array<{ sender: string; time: string; content: string }> }
| { kind: 'result_card'; usage?: UsageSummary;
costUsd?: number | null; durationMs?: number | null; isError?: boolean }
| { kind: 'error_card'; error: string; exitCode?: number }
| { kind: 'session_marker'; label: string }
| { kind: 'attachment_ref'; attachment: MessageAttachment; caption?: string }
| { kind: 'thinking'; text: string }
Text & Formatting
| Block | Description |
|---|---|
text | Prose text, optionally with inlineFormat: 'md' for markdown |
heading | Section heading (level 1-3) |
code | Fenced code block with optional language |
quote | Block quote, optionally expandable |
list | Ordered or unordered list |
hr | Horizontal rule |
link | Hyperlink with label |
Cards
| Block | Description |
|---|---|
tool_card | Tool invocation with name, input JSON, and optional result |
task_card | Background task notification |
user_message_card | Multi-user conversation buffer display |
result_card | Final turn result with usage stats and cost |
error_card | Session error with exit code |
Special
| Block | Description |
|---|---|
session_marker | Visual separator (e.g., "Session started") |
attachment_ref | Reference to an attachment for inline display |
thinking | Claude's thinking/reasoning block |
Conversion Functions
The SDK provides functions that convert bridge events into semantic blocks:
import {
streamUserMessageToBlocks,
streamToolCallToBlocks,
streamResultToBlocks,
sessionErrorToBlocks,
bridgeMessageToBlocks,
toolSummary,
} from '@oro.ad/bridge-adapter-sdk'
These are used internally by the orchestrators. You typically don't call them directly, but they're available if you need custom rendering.
FormattedContent
The output of your Formatter, ready to be passed to postMessage() or editMessage().
interface FormattedContent {
kind: 'text' | 'rich'
text?: string
parseMode?: 'plain' | 'html' | 'markdown' | 'markdownv2' | 'mrkdwn'
rich?: unknown // platform-specific payload (e.g., Slack blocks)
}
Example: Telegram Formatter
class TelegramFormatter implements Formatter {
render(blocks: SemanticBlock[]): FormattedContent[] {
let html = ''
for (const block of blocks) {
switch (block.kind) {
case 'text':
html += block.inlineFormat === 'md'
? renderMarkdownToHtml(block.text)
: escapeHtml(block.text)
break
case 'code':
html += `<pre><code class="language-${block.lang || ''}">`
html += escapeHtml(block.text)
html += '</code></pre>\n'
break
case 'tool_card':
html += `<b>🔧 ${escapeHtml(block.name)}</b>\n`
html += `<i>${escapeHtml(block.summary)}</i>\n`
break
// ... other block types
}
}
return chunkText(html, 4000).map(chunk => ({
kind: 'text',
text: chunk,
parseMode: 'html',
}))
}
renderStreaming(text: string): FormattedContent {
return { kind: 'text', text, parseMode: 'plain' }
}
}