Skip to main content

Build a Channel from Scratch

A channel connects Wolffish to an external communication platform. The desktop UI, Telegram, and WhatsApp are all channels — they receive messages from different sources but route them through the same brain pipeline. This guide walks through implementing a new channel from scratch.

What is a Channel?

A channel is a TurnSink implementation. It receives segments (streaming text, tool calls, results) and renders them to the user in whatever format the platform supports. The TurnRunner handles the agent pipeline — your channel just needs to:
  1. Accept incoming messages from your platform
  2. Send them into the agent pipeline
  3. Render the agent’s response back to the user
  4. Handle approval requests (for dangerous tool calls)

The TurnSink Interface

Methods Explained

Segment Types

  • text: Streaming deltas — accumulate them to build the full response
  • tool_call: The agent is invoking a tool (show a status indicator)
  • tool_result: The tool finished (show output or a summary)
  • turn_end: The turn is complete with a stop reason (end_turn, tool_use, max_tokens)

Step-by-Step Implementation

Step 1: Create the Channel File

Step 2: Implement TurnSink

Step 3: Handle Incoming Messages

When your platform receives a message, load or create a conversation and send it through the TurnRunner:

Step 4: Register the Channel

Add your channel to the startup flow in src/main/index.ts:
The TurnRunner is shared across all channels. It serializes turns per conversation — a conversation’s own turns queue behind each other, but different conversations run in parallel regardless of which channel initiated them.

Per-Conversation Serialization

The TurnRunner keeps one queue per conversation (turns with no conversation id get a private lane keyed by their turn id). A second message in the same conversation waits for that conversation’s current turn to finish — preserving transcript order and preventing races in its memory and context assembly — while a message for a different conversation runs immediately, in parallel.
A long-running turn only blocks its own conversation’s next message — never other conversations or other channels. A channel can still preempt its own conversation’s in-flight turn by aborting the previous controller before dispatching a new one.

Conversation Mapping

Your channel decides how external threads map to Wolffish conversations:
The Telegram channel uses Option C — one active conversation that persists until the user sends /new.

Reference: Telegram Channel Pattern

The Telegram channel demonstrates the full pattern:

Checklist for New Channels

Before shipping your channel:
  • Implements all TurnSink methods
  • Handles approval requests with user-facing prompts
  • Maps external threads to conversations consistently
  • Cleans up responseBuffer on onDone and onError
  • Handles onCredentialBlocked with a helpful message
  • Registered in main process startup
  • Tested with multi-turn conversations
  • Tested with tool calls (including failures and cancellation)
  • Tested with concurrent messages from multiple channels