Adapter SDK

TurnStreamer

Incremental text streaming with throttled edits and automatic fallback.

TurnStreamer

TurnStreamer manages a single assistant turn's text output. It accumulates text deltas and streams them to the platform, handling the complexities of rate-limited message editing.

Modes

Native Draft Mode

If PlatformAdapter.supportsNativeStreaming is true, the streamer calls streamDraft() for each delta. The platform shows a typing indicator or live preview.

Edit Fallback Mode

If native streaming is unavailable (most platforms), the streamer:

  1. Sends an initial placeholder message with the first text
  2. Edits the placeholder with accumulated text on a throttled schedule
  3. On finalize, does one final edit with the complete rendered content

Lifecycle

appendDelta("Hello")     → send placeholder "Hello"
appendDelta(" world")    → (throttled) edit → "Hello world"
appendDelta("!")         → (throttled, skipped)
appendDelta(" How")      → edit → "Hello world! How"
finalize(parts)          → final edit with rendered markdown

Throttling

Edits are throttled to maxEditsPerSecond (default: 2). This prevents hitting platform rate limits while still providing responsive streaming feedback.

Key Methods

appendDelta(text)

Appends text to the accumulated buffer. Triggers a throttled edit if enough time has passed since the last one.

getAccumulatedText()

Returns the raw accumulated text (all deltas concatenated). Used by handleMessageComplete to render the final markdown.

finalize(parts?)

Commits the turn.

With parts (non-empty FormattedContent[]):

  • Edits the placeholder with the first part (rendered markdown)
  • Posts additional parts as new messages
  • Returns all platform message IDs

Without parts (empty or omitted):

  • Flushes any unthrottled accumulated text to the placeholder
  • Returns the placeholder message ID
  • Used by handleToolUse — the tool card is posted separately after

Integration with BridgeToPlatform

The orchestrator manages streamers per conversation:

message_start  → create new TurnStreamer
text_delta     → streamer.appendDelta(text)
tool_use       → streamer.finalize() → post tool card
                 (new streamer created for post-tool text)
message_complete → streamer.finalize(rendered accumulated text)

Why accumulated, not e.content?

On message_complete, the SDK renders from streamer.getAccumulatedText() instead of the full e.content. This is critical for turns with tool calls in the middle:

Text: "Let me check..."   → Streamer A (committed)
Tool: Read file.ts         → Tool card posted
Text: "The file shows..."  → Streamer B (committed with accumulated only)

Using e.content would include "Let me check..." again in Streamer B's final edit, duplicating the pre-tool text.

Error Handling

Edit failures are caught and logged but don't crash the adapter. The placeholder message remains with its last successful state.

try {
  await platform.editMessage(containerId, placeholderId, content)
} catch (err) {
  logger.debug('turn-streamer.edit_failed', { error: err.message })
  // Continue — placeholder keeps its last state
}