Adapter SDK

Formatter & Semantic Blocks

Platform-agnostic message format and the Formatter interface for rendering.

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

BlockDescription
textProse text, optionally with inlineFormat: 'md' for markdown
headingSection heading (level 1-3)
codeFenced code block with optional language
quoteBlock quote, optionally expandable
listOrdered or unordered list
hrHorizontal rule
linkHyperlink with label

Cards

BlockDescription
tool_cardTool invocation with name, input JSON, and optional result
task_cardBackground task notification
user_message_cardMulti-user conversation buffer display
result_cardFinal turn result with usage stats and cost
error_cardSession error with exit code

Special

BlockDescription
session_markerVisual separator (e.g., "Session started")
attachment_refReference to an attachment for inline display
thinkingClaude'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' }
  }
}