Snek RPC Protocol API Specification

retoor retoor@molodetz.nl

Version 1.0 — February 2026


Table of Contents

  1. Protocol Overview
  2. Connection
  3. Request/Response Format
  4. Authentication
  5. Channel Operations
  6. Messaging
  7. Events
  8. Visual Feedback
  9. Message Routing
  10. Streaming
  11. Image Handling
  12. Tool System
  13. Error Handling
  14. Constants Reference

1. Protocol Overview

The Snek RPC protocol is a JSON-based remote procedure call protocol transported over WebSocket. Clients send method invocations as JSON objects and receive corresponding responses matched by a unique call identifier. The server also pushes unsolicited events (messages, typing indicators, joins/leaves) over the same connection.

Key characteristics:

  • Transport: WebSocket (RFC 6455)
  • Encoding: JSON over WebSocket text frames
  • Concurrency model: Multiplexed — multiple outstanding calls share one connection, matched by callId
  • Direction: Bidirectional — client sends requests, server sends responses and pushes events

2. Connection

Endpoint

wss://snek.molodetz.nl/rpc.ws

Plaintext variant (development only):

ws://snek.molodetz.nl/rpc.ws

WebSocket Parameters

Parameter Value Description
Heartbeat interval 30 seconds WebSocket ping/pong keepalive (WS_HEARTBEAT)
Request timeout 190 seconds Maximum wait for an RPC response (DEFAULT_REQUEST_TIMEOUT)
Receive retry delay 1.0 seconds Delay before retrying after a receive error (WS_RECEIVE_RETRY_DELAY)

Connection Lifecycle

1. Open WebSocket to wss://snek.molodetz.nl/rpc.ws
2. Call login(username, password)
3. Call get_user(null) to retrieve authenticated user info
4. Call get_channels() to enumerate available channels
5. Enter receive loop — process events and messages
6. On disconnect — reconnect with exponential backoff

Reconnection Strategy

Parameter Value
Max retries 3 (RECONNECT_MAX_RETRIES)
Initial delay 1.0 seconds (RECONNECT_INITIAL_DELAY)
Backoff factor 2.0x (RECONNECT_BACKOFF_FACTOR)

Delays follow: 1s → 2s → 4s. After exhausting retries the client should re-establish the full connection from step 1.


3. Request/Response Format

Client Request

Every RPC call from client to server uses the following JSON structure:

{
  "method": "<method_name>",
  "args": [<positional_arg_1>, <positional_arg_2>, ...],
  "kwargs": {<keyword_args>},
  "callId": "<unique_identifier>"
}
Field Type Required Description
method string yes RPC method name
args array yes Positional arguments (may be empty [])
kwargs object yes Keyword arguments (may be empty {})
callId string yes Client-generated unique ID to correlate response. Recommended: 16-character hex string (e.g., os.urandom(8).hex()) or UUID v4.

Server Response

The server responds with a JSON object containing the same callId:

{
  "callId": "<matching_call_id>",
  "data": <result_value>
}
Field Type Description
callId string Matches the request callId
data any Return value — object, array, string, or null depending on the method

Additional top-level fields may be present depending on context (e.g., event, message, username).

Fire-and-Forget Requests

Some calls do not require a response. The client sends the request normally but does not wait for a matching callId response. This is a client-side optimization — the server may still send a response.


4. Authentication

login

Authenticate with the Snek platform.

Request:

{
  "method": "login",
  "args": ["<username>", "<password>"],
  "kwargs": {},
  "callId": "<id>"
}

Response:

{
  "callId": "<id>",
  "data": { ... }
}

Must be the first call after establishing the WebSocket connection.

get_user

Retrieve user information.

Request:

{
  "method": "get_user",
  "args": [null],
  "kwargs": {},
  "callId": "<id>"
}

Pass null as the sole argument to retrieve the authenticated user.

Response:

{
  "callId": "<id>",
  "data": {
    "username": "botname",
    "nick": "BotNick"
  }
}
Field Type Description
username string Login username (unique identifier)
nick string Display name

5. Channel Operations

get_channels

Retrieve all channels visible to the authenticated user.

Request:

{
  "method": "get_channels",
  "args": [],
  "kwargs": {},
  "callId": "<id>"
}

Response:

{
  "callId": "<id>",
  "data": [
    {
      "uid": "channel_unique_id",
      "name": "Channel Name",
      "tag": "dm"
    },
    ...
  ]
}

Channel Object

Field Type Description
uid string Unique channel identifier. Matches pattern ^[a-zA-Z0-9_-]+$
name string Human-readable channel name
tag string Channel type. "dm" indicates a direct message channel

Channel Types

Tag Behavior
"dm" Direct message — bot always responds to messages
other / none Public channel — bot responds only if explicitly joined or triggered

6. Messaging

send_message

Send a message to a channel.

Request:

{
  "method": "send_message",
  "args": ["<channel_uid>", "<message_text>", <is_final>],
  "kwargs": {},
  "callId": "<id>"
}
Argument Type Required Description
channel_uid string yes Target channel UID
message_text string yes Message content (Markdown supported)
is_final boolean yes true for complete messages, false for streaming partial updates

Response:

{
  "callId": "<id>",
  "data": { ... }
}

Drive URL Rewriting

Relative drive paths are rewritten before processing:

(/drive.bin  →  (https://snek.molodetz.nl/drive.bin

7. Events

The server pushes unsolicited events over the WebSocket connection. Events do not have a callId matching any pending request.

Event Structure

{
  "event": "<event_type>",
  "data": { ... }
}

Message Event

Messages appear as a special case — they carry top-level fields instead of being nested under data:

{
  "event": "message",
  "message": "Hello world",
  "username": "sender_username",
  "user_nick": "SenderNick",
  "channel_uid": "target_channel_uid",
  "is_final": true
}
Field Type Description
event string "message"
message string Message content
username string Sender's username
user_nick string Sender's display name
channel_uid string Channel where the message was sent
is_final boolean true for complete messages, false for streaming updates

Clients should ignore messages where is_final is false unless implementing a streaming display.

set_typing Event

Server-pushed event indicating a user is typing or providing visual feedback.

{
  "event": "set_typing",
  "data": { ... }
}

join / leave Events

{
  "event": "join",
  "data": {
    "channel_uid": "<channel_uid>"
  }
}
{
  "event": "leave",
  "data": {
    "channel_uid": "<channel_uid>"
  }
}

Event Handling Pattern

Events are dispatched to handler methods named on_<event_type>. The data object is unpacked as keyword arguments:

Event: {"event": "join", "data": {"channel_uid": "abc123"}}
Handler: on_join(channel_uid="abc123")

Unknown events are silently ignored.


8. Visual Feedback

set_typing

Set a colored typing indicator for the bot in a channel. Used for visual "thinking" feedback.

Request:

{
  "method": "set_typing",
  "args": ["<channel_uid>", "<html_color_code>"],
  "kwargs": {},
  "callId": "<id>"
}
Argument Type Description
channel_uid string Target channel
html_color_code string HTML hex color code (e.g., "#FF0000")

Color Generation

Random bright colors are generated using HSL with these ranges:

Component Min Max
Hue 0.0 1.0
Saturation 0.7 1.0
Lightness 0.5 0.7

The color must match the pattern ^#[0-9A-Fa-f]{6}$.


9. Message Routing

Incoming messages are classified and routed in the following priority order:

Priority Condition Handler
1 username == self.username on_own_message(channel_uid, message)
2 Message starts with "ping" on_ping(username, user_nick, channel_uid, message_after_ping)
3 Message contains @nick join or @username join on_join(channel_uid)
4 Message contains @nick leave or @username leave on_leave(channel_uid)
5 Message contains @nick or @username on_mention(username, user_nick, channel_uid, message)
6 Default on_message(username, user_nick, channel_uid, message)

Channel Permission Rules

For on_message processing, the bot applies these rules:

  1. DM channels (tag == "dm"): Always respond.
  2. Public channels: Respond only if:
    • The bot has been explicitly joined to the channel, OR
    • The message matches a configured trigger pattern, OR
    • The bot's username appears in the message text.

Ping/Pong

Messages starting with "ping" trigger an automatic "pong" response:

Incoming: "ping hello"
Response: "pong hello"

10. Streaming

The protocol supports incremental message delivery using the is_final field on send_message.

Sending Streaming Messages

1. send_message(channel_uid, "Partial cont...",  false)   ← partial update
2. send_message(channel_uid, "Partial content h...", false) ← partial update
3. send_message(channel_uid, "Partial content here.", true) ← final message

Each partial message replaces the previous one in the UI. The final message (is_final=true) marks the message as complete.

Receiving Streaming Messages

When receiving events, messages with is_final=false represent in-progress content from another user or bot. Standard bot implementations skip non-final messages and only process the final version:

if not data.is_final:
    continue

Streaming Update Interval

The minimum interval between streaming updates is 0.0 seconds (STREAMING_UPDATE_INTERVAL), meaning updates are sent as fast as they are generated.


11. Image Handling

URL Extraction

Image URLs are extracted from message text using the pattern:

https?://\S+\.(?:png|jpg|jpeg|gif|bmp|webp|svg)(?:\?\S*)?

Matched URLs are stripped of trailing ., ', and " characters.

Image Formats

Two encoding formats are supported:

OpenAI Format (default)

Images are sent as separate content blocks in the OpenAI multi-modal message format:

{
  "role": "user",
  "content": [
    {
      "type": "text",
      "text": "message text with URL removed"
    },
    {
      "type": "image_url",
      "image_url": {
        "url": "data:image/png;base64,<base64_encoded_data>"
      }
    }
  ]
}

DeepSeek Format

Images are inlined into the text content as tagged references:

{
  "role": "user",
  "content": [
    {
      "type": "text",
      "text": "message with [image: data:image/png;base64,<base64_data>] inline"
    }
  ]
}

MIME Type Detection

MIME types are resolved in order:

  1. File extension via mimetypes.guess_type()
  2. Magic bytes detection:
Bytes MIME Type
\x89PNG\r\n\x1a\n image/png
\xff\xd8 image/jpeg
GIF87a or GIF89a image/gif
RIFF....WEBP image/webp
  1. Fallback: image/png

12. Tool System

The tool system follows the OpenAI function calling specification. Tools are serialized as function definitions, sent alongside the chat completion request, and executed when the LLM returns tool_calls.

Tool Definition Schema

Each tool is serialized as:

{
  "type": "function",
  "function": {
    "name": "<method_name>",
    "description": "<docstring>",
    "parameters": {
      "type": "object",
      "properties": {
        "<param_name>": {
          "type": "<json_type>",
          "default": "<default_value>"
        }
      },
      "required": ["<param_without_default>"]
    }
  }
}

Type Mapping

Python Type JSON Schema Type
str "string"
int "integer"
bool "boolean"
list "array"
dict "object"
None "null"
(default) "string"

Parameters with default values are optional; parameters without defaults are listed in required.

Tool Call Flow

1. Client sends chat completion request with tools array
2. LLM returns response with tool_calls array
3. Client executes each tool call
4. Client sends tool results back as role:"tool" messages
5. Client sends another chat completion request with updated context
6. Repeat until LLM returns a text response without tool_calls

Tool Call Response (from LLM)

{
  "role": "assistant",
  "content": "",
  "tool_calls": [
    {
      "id": "call_abc123",
      "type": "function",
      "function": {
        "name": "database_find",
        "arguments": "{\"table\": \"users\", \"query\": {\"active\": true}}"
      }
    }
  ]
}

Tool Result Message

{
  "role": "tool",
  "tool_call_id": "call_abc123",
  "content": "<stringified_result>"
}

Tool Call Limits

Parameter Value Description
Max depth 25 Maximum consecutive tool call rounds (MAX_TOOL_CALL_DEPTH)
Max repeated calls 5 Maximum identical tool invocations (MAX_REPEATED_TOOL_CALLS)
Max consecutive errors 3 Abort tool loop after 3 consecutive errors

Method Exclusions

Methods are excluded from tool serialization if:

  • The name starts with _
  • The name is serialize or handle
  • The attribute is not callable

13. Error Handling

Exception Hierarchy

BotError
├── ConnectionError        Connection/WebSocket failures
├── AuthenticationError    Login failures
├── RPCError               RPC call failures, timeouts
├── ToolError              Tool execution failures
├── DatabaseError          Database operation failures
├── ValidationError        Input validation failures
└── CircuitBreakerOpenError  Circuit breaker tripped

RPC Error Conditions

Condition Behavior
WebSocket closed/closing RPCError raised
WebSocket error frame RPCError raised
Response timeout (190s) RPCError raised
Invalid JSON in response Logged as warning, skipped
Connection closed during call RPCError raised

Reconnection on Send Failure

When send_message fails, the client retries with exponential backoff:

Attempt 1: send → fail → wait 1s
Attempt 2: reconnect + send → fail → wait 2s
Attempt 3: reconnect + send → fail → raise ConnectionError

Each reconnection attempt performs a full login sequence on a new WebSocket.

Circuit Breaker

Tool execution is protected by a circuit breaker pattern:

Parameter Value
Failure threshold 5 consecutive failures
Recovery timeout 60 seconds
Half-open max calls 3

States: CLOSED → (threshold exceeded) → OPEN → (timeout elapsed) → HALF_OPEN → (success) → CLOSED

When the circuit breaker is open, tool calls fail immediately with CircuitBreakerOpenError.


14. Constants Reference

Connection

Constant Value Description
DEFAULT_WS_URL wss://snek.molodetz.nl/rpc.ws Default WebSocket endpoint
DEFAULT_OPENAI_URL https://api.openai.com Default LLM API base URL
DEFAULT_SEARCH_API_BASE https://search.molodetz.nl Default search API endpoint
WS_HEARTBEAT 30.0s WebSocket heartbeat interval
DEFAULT_REQUEST_TIMEOUT 190s RPC call timeout
HTTP_CONNECT_TIMEOUT 90.0s HTTP TCP connect timeout

Reconnection

Constant Value Description
RECONNECT_MAX_RETRIES 3 Maximum reconnection attempts
RECONNECT_INITIAL_DELAY 1.0s First retry delay
RECONNECT_BACKOFF_FACTOR 2.0 Delay multiplier per retry
WS_RECEIVE_RETRY_DELAY 1.0s Delay after receive error

Concurrency

Constant Value Description
MAX_CONCURRENT_OPERATIONS 100 WebSocket semaphore limit
MAX_CONCURRENT_REQUESTS 10 HTTP connection pool limit
THREAD_POOL_WORKERS 8 Thread pool for blocking operations
PROCESS_POOL_WORKERS 4 Process pool for CPU-intensive tasks
EXECUTOR_TIMEOUT 30.0s Thread/process pool call timeout

Message Handling

Constant Value Description
MAX_MESSAGE_HISTORY 50 Recent messages retained in memory
DEFAULT_OPENAI_LIMIT 200 Max messages sent to LLM per session
CONTEXT_WINDOW_TOKEN_LIMIT 120000 Token budget for LLM context
STREAMING_UPDATE_INTERVAL 0.0s Min interval between streaming updates

Tool Limits

Constant Value Description
MAX_TOOL_CALL_DEPTH 25 Max consecutive tool call rounds
MAX_REPEATED_TOOL_CALLS 5 Max identical tool invocations
MAX_RETRY_ATTEMPTS 3 General retry limit

Timing

Constant Value Description
TASK_CHECK_INTERVAL 0.5s Service loop tick interval
THINKING_TASK_INTERVAL 1.0s Typing indicator refresh interval
SERVICE_CLEANUP_INTERVAL 3600s Periodic cleanup cycle
SHUTDOWN_TIMEOUT 5.0s Max wait for graceful shutdown
MIN_COUNTER_UPDATE_INTERVAL 240s Debounce for counter updates

Agentic

Constant Value Description
AGENTIC_MAX_RESEARCH_QUERIES 8 Max search queries per research task
AGENTIC_MAX_PLAN_STEPS 10 Max steps in a generated plan
AGENTIC_SCRATCHPAD_SIZE 50 Scratchpad buffer size

Visual

Constant Value Description
COLOR_HUE_MIN 0.0 HSL hue range start
COLOR_HUE_MAX 1.0 HSL hue range end
COLOR_SATURATION_MIN 0.7 HSL saturation range start
COLOR_SATURATION_MAX 1.0 HSL saturation range end
COLOR_LIGHTNESS_MIN 0.5 HSL lightness range start
COLOR_LIGHTNESS_MAX 0.7 HSL lightness range end

Media

Constant Value Description
MEDIA_HUNT_DEDUP_MAX_SIZE 10000 Dedup cache max entries
MEDIA_HUNT_DEDUP_TTL 86400s Dedup cache TTL (24 hours)
MEDIA_HUNT_HEAD_TIMEOUT 5.0s HEAD request timeout
MEDIA_HUNT_FETCH_TIMEOUT 30.0s Full fetch timeout

Bot Name Sanitization

The following bot names are sanitized in outgoing messages to prevent cross-triggering:

snek, grok, snik, lisa, gemma, joanne, ira, thomas

Sanitization inserts a hyphen after the first character (e.g., sneks-nek).


Appendix A: Complete Connection Example

CLIENT  →  WebSocket CONNECT wss://snek.molodetz.nl/rpc.ws
SERVER  ←  101 Switching Protocols

CLIENT  →  {"method":"login","args":["mybot","mypassword"],"kwargs":{},"callId":"a1b2c3d4e5f6g7h8"}
SERVER  ←  {"callId":"a1b2c3d4e5f6g7h8","data":{...}}

CLIENT  →  {"method":"get_user","args":[null],"kwargs":{},"callId":"b2c3d4e5f6g7h8a1"}
SERVER  ←  {"callId":"b2c3d4e5f6g7h8a1","data":{"username":"mybot","nick":"MyBot"}}

CLIENT  →  {"method":"get_channels","args":[],"kwargs":{},"callId":"c3d4e5f6g7h8a1b2"}
SERVER  ←  {"callId":"c3d4e5f6g7h8a1b2","data":[{"uid":"ch1","name":"general","tag":"public"},{"uid":"ch2","name":"DM","tag":"dm"}]}

--- receive loop ---

SERVER  ←  {"event":"message","message":"hello @MyBot","username":"alice","user_nick":"Alice","channel_uid":"ch1","is_final":true}

CLIENT  →  {"method":"send_message","args":["ch1","hey, what's up?",true],"kwargs":{},"callId":"d4e5f6g7h8a1b2c3"}
SERVER  ←  {"callId":"d4e5f6g7h8a1b2c3","data":{...}}

Appendix B: Streaming Example

CLIENT  →  {"method":"set_typing","args":["ch1","#FF6B35"],"kwargs":{},"callId":"e5f6g7h8a1b2c3d4"}

CLIENT  →  {"method":"send_message","args":["ch1","Working on",false],"kwargs":{},"callId":"f6g7h8a1b2c3d4e5"}
CLIENT  →  {"method":"send_message","args":["ch1","Working on it...",false],"kwargs":{},"callId":"g7h8a1b2c3d4e5f6"}
CLIENT  →  {"method":"send_message","args":["ch1","Working on it... done!",true],"kwargs":{},"callId":"h8a1b2c3d4e5f6g7"}

Appendix C: Bot State Machine

INITIALIZING → INITIALIZED → CONNECTING → CONNECTED → RUNNING → SHUTTING_DOWN → SHUTDOWN
                                                         ↓
                                                       ERROR
State Description
INITIALIZING HTTP sessions, signal handlers, and background tasks being set up
INITIALIZED Setup complete, ready to connect
CONNECTING WebSocket connection in progress
CONNECTED Authenticated and channels enumerated
RUNNING Processing messages in the receive loop
SHUTTING_DOWN Graceful shutdown initiated, cancelling background tasks
SHUTDOWN All resources released
ERROR Initialization or fatal runtime failure