This commit adds 6 new documentation files to fill critical gaps: - CONTRIBUTING.md: Developer onboarding guide with setup, workflow, code style, testing, and adding features - TROUBLESHOOTING.md: Common issues and solutions for errors, model issues, tool issues, channel issues, gateway issues, configuration issues, and memory/database issues - docs/api/PROTOCOL.md: Gateway JSON-RPC protocol documentation with connection, authentication, message format, methods, events, error codes, and example client implementation - docs/api/TOOLS.md: Tools API documentation covering tool interface, input schema format, result format, tool patterns, tool registration, tool policy, execution flow, and builtin tools reference - docs/deployment/PRODUCTION.md: Production deployment guide covering Docker deployment, systemd service, security, configuration, monitoring, backup & recovery, and performance tuning - docs/performance/TUNING.md: Performance optimization guide covering context management, model routing, tool execution, memory & embeddings, session management, database performance, gateway performance, and resource usage These files complement the existing excellent documentation (README.md, AGENTS.md, ARCHITECTURE.md, STRUCTURE.md, CONVENTIONS.md) to provide complete coverage for users, developers, and operators.
16 KiB
Gateway API Protocol
Flynn's gateway exposes a WebSocket-based JSON-RPC protocol for real-time communication with the AI agent. This document describes the protocol in detail.
Table of Contents
Overview
The gateway provides:
- WebSocket Server: Real-time bidirectional communication
- JSON-RPC 2.0: Structured request/response protocol
- Streaming Events: Real-time updates during agent processing
- HTTP Server: Serves static dashboard and handles webhook endpoints
Base URL
- WebSocket:
ws://localhost:18800(orwss://if using TLS) - HTTP:
http://localhost:18800(orhttps://if using TLS) - Health check:
GET /health
Default Ports
- Gateway:
18800 - Tailscale Serve (if enabled):
443(HTTPS)
Connection
WebSocket Handshake
// Browser
const ws = new WebSocket('ws://localhost:18800');
// Node.js (ws library)
const WebSocket = require('ws');
const ws = new WebSocket('ws://localhost:18800');
Connection Lifecycle
- Client connects to WebSocket endpoint
- Server validates authentication (if configured)
- Connection assigned unique ID
- Client can start sending requests
- Server sends responses and events asynchronously
Disconnection
ws.onclose = (event) => {
console.log('Disconnected:', event.code, event.reason);
};
Common close codes:
1000: Normal closure1001: Endpoint going away1006: Abnormal closure (network issue)
Authentication
Bearer Token Auth
If gateway.auth.token is configured, all WebSocket connections must provide authentication:
const ws = new WebSocket('ws://localhost:18800', {
headers: {
'Authorization': 'Bearer your-secret-token'
}
});
Tailscale Identity
If gateway.auth.trustTailscaleIdentity is enabled, connections from Tailscale are trusted based on the Tailscale-User-Login header.
// Automatic when connecting via Tailscale
// No additional auth required if trustTailscaleIdentity: true
HTTP Auth
If gateway.auth.applyToHttp is true (default when token is set), HTTP requests also require bearer token:
fetch('http://localhost:18800/api/health', {
headers: {
'Authorization': 'Bearer your-secret-token'
}
});
Message Format
Request (Client → Server)
interface GatewayRequest {
id: number; // Unique request ID (integer)
method: string; // Method name (e.g., 'agent.send', 'system.info')
params?: Record<string, unknown>; // Method parameters (optional)
}
Example:
{
"id": 1,
"method": "agent.send",
"params": {
"message": "Hello, Flynn!",
"session": "telegram:123456"
}
}
Response (Server → Client)
interface GatewayResponse {
id: number; // Request ID this responds to
result: unknown; // Method result
}
Example:
{
"id": 1,
"result": {
"sessionId": "telegram:123456",
"response": "Hello! How can I help you today?"
}
}
Error (Server → Client)
interface GatewayError {
id: number;
error: {
code: ErrorCode; // Error code (integer)
message: string; // Human-readable error message
};
}
Example:
{
"id": 1,
"error": {
"code": 4,
"message": "Request cancelled by client"
}
}
Event (Server → Client)
interface GatewayEvent {
id: number; // Request ID this relates to
event: EventType; // Event type string
data: unknown; // Event-specific data
}
Example:
{
"id": 1,
"event": "content",
"data": {
"text": "Hello! How can I help you today?"
}
}
Methods
System Methods
system.info
Get gateway information.
Request:
{
"id": 1,
"method": "system.info"
}
Response:
{
"id": 1,
"result": {
"version": "0.1.0",
"uptime": 12345,
"connections": 2
}
}
system.disconnect
Close the connection gracefully.
Request:
{
"id": 2,
"method": "system.disconnect"
}
Response:
{
"id": 2,
"result": {
"success": true
}
}
Session Methods
sessions.list
List all sessions.
Request:
{
"id": 3,
"method": "sessions.list"
}
Response:
{
"id": 3,
"result": {
"sessions": [
{
"id": "telegram:123456",
"createdAt": "2025-02-13T10:00:00Z",
"lastActiveAt": "2025-02-13T12:00:00Z",
"messageCount": 42,
"connectionCount": 1
}
]
}
}
sessions.get
Get session details.
Request:
{
"id": 4,
"method": "sessions.get",
"params": {
"sessionId": "telegram:123456"
}
}
Response:
{
"id": 4,
"result": {
"id": "telegram:123456",
"createdAt": "2025-02-13T10:00:00Z",
"lastActiveAt": "2025-02-13T12:00:00Z",
"history": [
{
"role": "user",
"content": "Hello"
},
{
"role": "assistant",
"content": "Hi there!"
}
]
}
}
sessions.create
Create or resume a session.
Request:
{
"id": 5,
"method": "sessions.create",
"params": {
"sessionId": "telegram:123456"
}
}
Response:
{
"id": 5,
"result": {
"sessionId": "telegram:123456",
"created": true
}
}
sessions.delete
Delete a session.
Request:
{
"id": 6,
"method": "sessions.delete",
"params": {
"sessionId": "telegram:123456"
}
}
Response:
{
"id": 6,
"result": {
"success": true
}
}
Agent Methods
agent.send
Send a message to the agent and stream response.
Request:
{
"id": 7,
"method": "agent.send",
"params": {
"message": "What's the weather?",
"sessionId": "telegram:123456",
"attachments": [
{
"mimeType": "image/jpeg",
"data": "base64encodedimage..."
}
]
}
}
Response (final):
{
"id": 7,
"result": {
"content": "I can't check the weather without access to weather APIs. Would you like me to help you with something else?",
"usage": {
"inputTokens": 25,
"outputTokens": 30
}
}
}
Events (streamed during processing):
content event:
{
"id": 7,
"event": "content",
"data": {
"text": "I can't check the weather"
}
}
tool_start event:
{
"id": 7,
"event": "tool_start",
"data": {
"tool": "shell.exec",
"args": {
"command": "echo 'Hello'"
}
}
}
tool_end event:
{
"id": 7,
"event": "tool_end",
"data": {
"tool": "shell.exec",
"result": {
"success": true,
"output": "Hello\n",
"error": null
}
}
}
attachment event:
{
"id": 7,
"event": "attachment",
"data": {
"mimeType": "image/png",
"data": "base64encoded..."
}
}
done event:
{
"id": 7,
"event": "done",
"data": {
"content": "Complete response here..."
}
}
agent.cancel
Cancel the current agent operation.
Request:
{
"id": 8,
"method": "agent.cancel",
"params": {
"sessionId": "telegram:123456"
}
}
Response:
{
"id": 8,
"result": {
"cancelled": true
}
}
agent.setToolUseCallback
Set callback for tool use events (for confirmation UI).
Request:
{
"id": 9,
"method": "agent.setToolUseCallback",
"params": {
"sessionId": "telegram:123456",
"enabled": true
}
}
Response:
{
"id": 9,
"result": {
"success": true
}
}
Tool Methods
tools.list
List available tools.
Request:
{
"id": 10,
"method": "tools.list",
"params": {
"sessionId": "telegram:123456"
}
}
Response:
{
"id": 10,
"result": {
"tools": [
{
"name": "shell.exec",
"description": "Execute a shell command...",
"inputSchema": {
"type": "object",
"properties": {
"command": { "type": "string" }
}
}
}
]
}
}
tools.execute
Execute a tool directly (bypass agent).
Request:
{
"id": 11,
"method": "tools.execute",
"params": {
"sessionId": "telegram:123456",
"tool": "shell.exec",
"args": {
"command": "echo 'Hello'"
}
}
}
Response:
{
"id": 11,
"result": {
"success": true,
"output": "Hello\n"
}
}
Config Methods
config.get
Get current configuration (secrets redacted).
Request:
{
"id": 12,
"method": "config.get"
}
Response:
{
"id": 12,
"result": {
"models": {
"default": {
"anthropic": {
"apiKey": "***"
}
}
}
}
}
config.reload
Reload configuration from file.
Request:
{
"id": 13,
"method": "config.reload"
}
Response:
{
"id": 13,
"result": {
"success": true
}
}
Pairing Methods
pairing.generate
Generate a pairing code for unknown senders.
Request:
{
"id": 14,
"method": "pairing.generate",
"params": {
"channel": "telegram"
}
}
Response:
{
"id": 14,
"result": {
"code": "ABC123",
"expiresAt": "2025-02-13T12:05:00Z"
}
}
pairing.list
List active pairing codes.
Request:
{
"id": 15,
"method": "pairing.list"
}
Response:
{
"id": 15,
"result": {
"codes": [
{
"code": "ABC123",
"channel": "telegram",
"createdAt": "2025-02-13T12:00:00Z",
"expiresAt": "2025-02-13T12:05:00Z"
}
]
}
}
Events
Event Types
content
Streamed text content from the agent.
{
"id": 1,
"event": "content",
"data": {
"text": "Partial response..."
}
}
tool_start
Tool execution started.
{
"id": 1,
"event": "tool_start",
"data": {
"tool": "shell.exec",
"args": {
"command": "echo 'test'"
}
}
}
tool_end
Tool execution completed.
{
"id": 1,
"event": "tool_end",
"data": {
"tool": "shell.exec",
"result": {
"success": true,
"output": "test\n"
}
}
}
attachment
Outbound attachment (image, audio, file).
{
"id": 1,
"event": "attachment",
"data": {
"mimeType": "image/jpeg",
"data": "base64encoded...",
"filename": "output.jpg"
}
}
done
Agent processing complete (final response).
{
"id": 1,
"event": "done",
"data": {
"content": "Complete final response..."
}
}
error
Error occurred during processing.
{
"id": 1,
"event": "error",
"data": {
"code": 5,
"message": "Internal error: ..."
}
}
Error Codes
| Code | Name | Description |
|---|---|---|
| -1 | ParseError |
Invalid JSON in request |
| -2 | InvalidRequest |
Missing required fields |
| -3 | MethodNotFound |
Unknown method name |
| -4 | AuthRequired |
Authentication required but not provided |
| -5 | AuthFailed |
Authentication failed |
| 1 | SessionNotFound |
Session ID doesn't exist |
| 2 | ToolNotFound |
Tool name doesn't exist |
| 3 | AgentBusy |
Agent is processing another request |
| 4 | RequestCancelled |
Request was cancelled by client |
| 5 | InternalError |
Unexpected server error |
Example Client
Browser Client
class FlynnClient {
constructor(url, token) {
this.url = url;
this.token = token;
this.requestId = 0;
this.pending = new Map();
}
connect() {
const headers = {};
if (this.token) {
headers['Authorization'] = `Bearer ${this.token}`;
}
this.ws = new WebSocket(this.url, { headers });
this.ws.onopen = () => {
console.log('Connected to Flynn gateway');
};
this.ws.onmessage = (event) => {
const message = JSON.parse(event.data);
this.handleMessage(message);
};
this.ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
this.ws.onclose = () => {
console.log('Disconnected from Flynn gateway');
};
}
sendRequest(method, params = {}) {
return new Promise((resolve, reject) => {
const id = ++this.requestId;
const request = {
id,
method,
params
};
this.ws.send(JSON.stringify(request));
// Store promise for response
this.pending.set(id, { resolve, reject });
// Set timeout
setTimeout(() => {
if (this.pending.has(id)) {
this.pending.delete(id);
reject(new Error('Request timeout'));
}
}, 30000);
});
}
handleMessage(message) {
// Response
if ('result' in message) {
const { id, result } = message;
const pending = this.pending.get(id);
if (pending) {
this.pending.delete(id);
pending.resolve(result);
}
}
// Error
else if ('error' in message) {
const { id, error } = message;
const pending = this.pending.get(id);
if (pending) {
this.pending.delete(id);
const err = new Error(error.message);
err.code = error.code;
pending.reject(err);
}
}
// Event
else if ('event' in message) {
this.handleEvent(message);
}
}
handleEvent(event) {
const { id, event: eventType, data } = event;
switch (eventType) {
case 'content':
console.log('Content:', data.text);
break;
case 'tool_start':
console.log('Tool started:', data.tool, data.args);
break;
case 'tool_end':
console.log('Tool completed:', data.tool, data.result);
break;
case 'attachment':
console.log('Attachment received:', data.mimeType);
break;
case 'done':
console.log('Done:', data.content);
break;
case 'error':
console.error('Error:', data.code, data.message);
break;
}
}
// Convenience methods
async systemInfo() {
return this.sendRequest('system.info');
}
async listSessions() {
return this.sendRequest('sessions.list');
}
async sendMessage(message, sessionId, attachments = []) {
return this.sendRequest('agent.send', {
message,
sessionId,
attachments
});
}
async listTools(sessionId) {
return this.sendRequest('tools.list', { sessionId });
}
}
// Usage
const client = new FlynnClient('ws://localhost:18800', 'your-token');
client.connect();
client.onopen = async () => {
const info = await client.systemInfo();
console.log('Gateway info:', info);
const response = await client.sendMessage('Hello, Flynn!', 'telegram:123456');
console.log('Response:', response);
};
Node.js Client
const WebSocket = require('ws');
class FlynnNodeClient extends FlynnClient {
connect() {
const options = {};
if (this.token) {
options.headers = { 'Authorization': `Bearer ${this.token}` };
}
this.ws = new WebSocket(this.url, options);
// ... same as browser client
}
}
// Usage
const client = new FlynnNodeClient('ws://localhost:18800', 'your-token');
client.connect();
HTTP Fetch Example
// Health check
async function checkHealth() {
const response = await fetch('http://localhost:18800/health', {
headers: {
'Authorization': 'Bearer your-token'
}
});
const status = await response.json();
console.log('Health:', status);
}
checkHealth();
For more implementation details, see:
- Protocol types:
src/gateway/protocol.ts - Handlers:
src/gateway/handlers/ - Gateway server:
src/gateway/server.ts