39 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
- Node Capability Negotiation: Optional companion-node role/capability registration
Operational note: onboarding (flynn setup / flynn onboard) now runs post-save live readiness checks (model/channel/memory/automation) and prints a guided first-success task flow. Companion CLI now also supports bootstrap-manifest export (flynn companion --export-bootstrap <path|->), release-bundle export (--export-release-bundle <dir> with optional --signing-key/--signing-key-id signature output), release-bundle verification (--verify-release-bundle <dir> with optional --verify-signing-key/--verify-signing-key-id/--require-signature), platform shell-template export (--export-shell-template <dir>), plus richer shell bootstrap flags for status/location/push (--app-version, --latitude/--longitude, --push-token, etc.) for desktop/mobile app packaging without changing JSON-RPC method/event shapes.
Execution Model (Sessions + Per-Session Queue)
Two concepts matter for correct clients:
- connectionId: a single WebSocket connection identity (assigned on connect)
- sessionId: the conversation/session the connection is attached to (defaults to a per-connection session, but can be switched to resume an old session)
The gateway serialises agent work per session, not per WebSocket connection:
- Requests that target the same
sessionIdrun one-at-a-time (FIFO) in a per-session lane. - Requests for different sessions can run in parallel.
- Lane policy is configurable (
collect,followup,steer,steer_backlog,interrupt) with per-channel and per-session overrides. - Session-local overrides can be managed at runtime via
agent.sendcommands:/queue,/queue set ...,/queue reset. - Backend selection for a turn is server-side (
nativeby default, optional external backends per config:claude_code,opencode,codex,gemini,pi_embedded) and does not change JSON-RPC method signatures. - Runtime backend mode overrides are available via
agent.sendcommand fast-path:/runtime status,/runtime activate pi,/runtime deactivate pi,/runtime use config(/backend ...remains a compatibility alias). - The gateway
agent.sendcommand path and channel-router path use the same runtime backend-mode command service;flynn tuiforwards/runtime ...through this gateway path for parity. - Backend routing and fallback outcomes are emitted to audit logs (
backend.route,backend.success,backend.fallback) for rollout evaluation; this telemetry is outside JSON-RPC response payloads. - Session-start memory injection (
user/profile+user/working) is server-side and controlled bymemory.user_namespace; it does not affect protocol payloads. - Multi-turn child agents are exposed through tool calls (
subagent.spawn/send/list/cancel/delete/summary) inside the agent loop; child sessions support per-session queue mode and budget guardrails but do not add new JSON-RPC methods. - Session command fast-path includes
/subagents(list|summary|cancel|delete) for child-session inspection/control without protocol changes.
This is implemented via a per-lane queue (LaneQueue) in the gateway server, and used by agent.send and agent.cancel.
sequenceDiagram
autonumber
participant C as Client
participant G as Gateway (WS JSON-RPC)
participant LQ as LaneQueue (per-session)
participant SB as SessionBridge
participant A as AgentOrchestrator
C->>G: agent.send {connectionId, message}
G->>SB: resolve sessionId for connectionId
SB-->>G: sessionId (laneId)
G->>LQ: enqueue(laneId, work)
alt lane idle
LQ-->>G: starts work immediately
else lane busy
Note over LQ: work queued (FIFO) for this lane
opt mode=interrupt and newer request arrives
LQ->>SB: request cancellation of active lane run
SB->>A: cancel() (best effort)
G-->>C: transient preempt notice (queue.preempt)
end
end
G->>A: process(message) in that session
A-->>G: streaming events (content/tool_start/tool_end/context_warning/run_state)
G-->>C: events + final done
C->>G: agent.cancel {connectionId}
G->>LQ: cancel(laneId) (queued items rejected)
G->>SB: cancel active op (best-effort)
G-->>C: result.cancelled=true/false
interrupt queue mode also requests active-run cancellation when a newer request is enqueued for the same session lane. Cancellation still completes at agent/tool-loop safe points. When this preemption happens, the requester receives a transient content notice and the audit log records queue.preempt.
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 server.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 server.tailscale_identity is enabled, connections from Tailscale are trusted based on the Tailscale-User-Login header.
// Automatic when connecting via Tailscale
// No additional auth required if server.tailscale_identity is enabled
HTTP Auth
If server.auth_http is true (default: true), HTTP requests also require bearer token when server.token is set:
fetch('http://localhost:18800/api/health', {
headers: {
'Authorization': 'Bearer your-secret-token'
}
});
Exceptions (handled by their own trust/auth model and therefore bypass gateway token auth):
POST /webhooks/:name(HMAC-validated when webhook secret is configured)POST /gmail/push(Google Pub/Sub push)POST /teams/events(Microsoft Bot Framework activity callback)POST /google-chat/events(Google Chat event callback, optional webhook token check)POST /bluebubbles/events(BlueBubbles iMessage webhook callback, optional webhook token check)
WebChat PWA push-subscription endpoints (auth-protected):
GET /webchat/push/public-key(returns enabled/configured push metadata)GET /webchat/push/subscriptions(returns current subscription count/cap)POST /webchat/push/subscriptions(registers/updates one browser subscription)DELETE /webchat/push/subscriptions(removes one browser subscription by endpoint)
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"
}
system.presence
Return tracked sender presence snapshots (most recent first).
Online/offline is inferred from inactivity threshold in the daemon.
Request:
{
"id": 3,
"method": "system.presence",
"params": {
"channel": "telegram",
"status": "online",
"limit": 50
}
}
system.sessionAnalytics
Return aggregate session analytics from the SQLite message history.
Useful for operator dashboards and trend checks (sessions/day, message volume, top active sessions).
Request:
{
"id": 10,
"method": "system.sessionAnalytics",
"params": {
"days": 30,
"topLimit": 10
}
}
system.contextUsage
Return per-session estimated context-window budget snapshots.
Useful for proactive compaction monitoring and operator dashboards.
Request:
{
"id": 11,
"method": "system.contextUsage"
}
system.metrics
Return aggregated gateway metrics snapshot (used by the dashboard).
Includes run-state counters, cancel latency samples, and reaction decision counters.
Request:
{
"id": 11,
"method": "system.metrics"
}
Response:
{
"id": 11,
"result": {
"messagesProcessed": 120,
"errors": 2,
"activeRequests": 1,
"uptime": 3600,
"modelCalls": {
"total": 15,
"avgLatency": 420,
"errorRate": 0.07,
"recentCalls": []
},
"runStates": {
"start": 25,
"complete": 22,
"cancel_requested": 1,
"cancelled": 1,
"error": 1
},
"cancelLatencyMs": {
"sampleCount": 4,
"samples": [120, 240, 310, 95]
},
"reactions": {
"matched": 6,
"skipped": 3,
"skipReasons": {
"no_match": 2,
"no_rules": 1
}
},
"queueDepth": 0
}
}
system.localBackends
Return status for user-level local LLM backend daemons (for example ollama.service and llama-server.service).
Request:
{
"id": 12,
"method": "system.localBackends"
}
Response:
{
"id": 12,
"result": {
"backends": [
{
"id": "ollama",
"provider": "ollama",
"name": "Ollama",
"unit": "ollama.service",
"configured": true,
"loadState": "loaded",
"activeState": "active",
"subState": "running",
"unitFileState": "enabled",
"description": "Ollama Service",
"pid": 12345,
"result": "success",
"statusText": "active (running)",
"availableActions": ["restart", "stop"]
}
]
}
}
system.dockerDependencies
Return status for docker-compose managed dependencies discovered from docker-compose.yml (excluding Flynn's own flynn service). Includes profile-scoped services (for example whisper-server, brave-search, searxng) when profiles are defined.
Request:
{
"id": 12,
"method": "system.dockerDependencies"
}
Response:
{
"id": 12,
"result": {
"dependencies": [
{
"id": "whisper",
"name": "Whisper (whisper.cpp)",
"service": "whisper-server",
"configured": true,
"state": "running",
"health": "healthy",
"statusText": "Up 4 minutes (healthy)",
"containerName": "flynn-whisper-server-1",
"availableActions": ["restart", "stop", "update"]
},
{
"id": "brave-search",
"name": "Brave Search",
"service": "brave-search",
"configured": true,
"state": "running",
"health": "healthy",
"statusText": "Up 2 minutes",
"containerName": "brave-search",
"availableActions": ["restart", "stop", "update"]
},
{
"id": "searxng",
"name": "SearXNG",
"service": "searxng",
"configured": true,
"state": "running",
"health": "none",
"statusText": "Up 2 minutes",
"containerName": "searxng",
"availableActions": ["restart", "stop", "update"]
}
]
}
}
system.dockerDependencyControl
Control a docker-compose dependency (start, restart, stop, update).
dependencymust match an ID returned bysystem.dockerDependencies.updatepulls the latest image for that compose service and runsdocker compose up -dto reconcile.
Request:
{
"id": 14,
"method": "system.dockerDependencyControl",
"params": {
"dependency": "whisper",
"action": "restart"
}
}
Response:
{
"id": 14,
"result": {
"dependency": "whisper",
"action": "restart",
"status": {
"id": "whisper",
"state": "running",
"health": "healthy",
"statusText": "running (healthy)"
},
"message": "Restarted whisper-server container."
}
}
system.localBackendControl
Control a local backend daemon (start, restart, stop, update).
updatesemantics:ollama: pulls configured Ollama models (tiers/local providers + embedding/audio models) viaollama pull.llamacpp: performs a safe service restart (model file refresh remains external to Flynn).
Request:
{
"id": 13,
"method": "system.localBackendControl",
"params": {
"backend": "ollama",
"action": "restart"
}
}
system.observabilitySources
Return graph/log-capable observability sources for the dashboard.
Request:
{
"id": 15,
"method": "system.observabilitySources"
}
Response:
{
"id": 15,
"result": {
"sources": [
{
"id": "systemd:flynn",
"name": "Flynn daemon",
"kind": "systemd_system",
"runtime": "systemd_system",
"status": "running",
"graphCapable": true,
"logCapable": true
},
{
"id": "docker:whisper",
"name": "Whisper (whisper.cpp)",
"kind": "docker_dependency",
"runtime": "docker_compose",
"status": "running",
"graphCapable": true,
"logCapable": true
}
]
}
}
system.observabilitySeries
Return sampled service trend points (bounded, in-memory) for dashboard charts.
Request:
{
"id": 16,
"method": "system.observabilitySeries",
"params": {
"windowMinutes": 60,
"bucketSeconds": 30,
"sourceIds": ["systemd:flynn", "docker:whisper"]
}
}
Response:
{
"id": 16,
"result": {
"generatedAt": 1739999999000,
"windowMinutes": 60,
"bucketSeconds": 30,
"series": [
{
"sourceId": "systemd:flynn",
"points": [
{ "ts": 1739999970000, "stateCode": 3, "healthCode": 2, "errorCount": 0, "restartCount": 0 },
{ "ts": 1739999985000, "stateCode": 3, "healthCode": 2, "errorCount": 0, "restartCount": 1 }
]
}
]
}
}
system.serviceLogs
Return recent logs for a discovered observability source. Flynn applies secret masking heuristics to returned lines.
Request:
{
"id": 17,
"method": "system.serviceLogs",
"params": {
"sourceId": "docker:whisper",
"lines": 200,
"sinceSeconds": 900
}
}
Response:
{
"id": 17,
"result": {
"sourceId": "docker:whisper",
"fetchedAt": 1739999999000,
"redacted": false,
"truncated": false,
"lines": [
{
"ts": 1739999990000,
"level": "warn",
"text": "queue depth rising"
}
]
}
}
Response:
{
"id": 13,
"result": {
"backend": "ollama",
"action": "restart",
"status": {
"id": "ollama",
"activeState": "active",
"subState": "running",
"statusText": "active (running)"
}
}
}
Response:
{
"id": 11,
"result": {
"sessions": [
{
"sessionId": "ws:abc-123",
"budget": {
"estimatedTokens": 172000,
"contextWindow": 200000,
"remainingTokens": 28000,
"usagePct": 86,
"thresholdPct": 80,
"thresholdTokens": 160000,
"shouldCompact": true
}
}
]
}
}
Response:
{
"id": 10,
"result": {
"daily": [
{ "day": "2026-02-16", "sessions": 14, "messages": 228 }
],
"topSessions": [
{ "sessionId": "telegram:123456", "messages": 42, "lastActivity": 1739700300 }
],
"topTools": [
{ "toolName": "web.search", "executions": 37 }
],
"topTopics": [
{ "topic": "kubernetes", "occurrences": 22 }
],
"averageMessagesPerSession": 16.29,
"totalSessions": 14,
"totalMessages": 228
}
}
Response:
{
"id": 3,
"result": {
"presence": [
{
"channel": "telegram",
"senderId": "123456",
"senderName": "alice",
"firstSeenAt": 1739700000000,
"lastSeenAt": 1739700300000,
"messageCount": 12,
"status": "online"
}
],
"summary": { "total": 1, "online": 1, "offline": 0 }
}
}
Response:
{
"id": 2,
"result": {
"success": true
}
}
Session Methods
sessions.list
List sessions with optional persisted inclusion, frontend filtering, and paging.
Request:
{
"id": 3,
"method": "sessions.list",
"params": {
"includePersisted": true,
"frontend": "ws",
"limit": 50,
"offset": 0
}
}
Response:
{
"id": 3,
"result": {
"sessions": [
{
"id": "telegram:123456",
"frontend": "telegram",
"userId": "123456",
"messageCount": 42,
"lastMessageAt": 1739448000000,
"config": {
"modelTier": "fast",
"queue": {
"mode": "followup",
"overflow": "drop_old",
"cap": 8,
"debounceMs": 250,
"summarizeOverflow": true
},
"elevation": {
"active": false,
"untilMs": 1739451600000
}
}
}
],
"total": 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..."
}
}
context_warning event:
{
"id": 7,
"event": "context_warning",
"data": {
"level": "checkpoint",
"message": "Context usage is 86.0% (172,000/200,000 estimated tokens). Checkpoint saved to memory namespace `session/checkpoints/ws/abc-123`.",
"budget": {
"estimatedTokens": 172000,
"contextWindow": 200000,
"remainingTokens": 28000,
"usagePct": 86,
"thresholdPct": 80,
"thresholdTokens": 160000,
"shouldCompact": true
},
"actions": {
"checkpointSaved": true,
"autoCompacted": false,
"checkpointNamespace": "session/checkpoints/ws/abc-123"
}
}
}
run_state event:
{
"id": 7,
"event": "run_state",
"data": {
"state": "complete",
"timestamp": 1730140800000
}
}
done event:
{
"id": 7,
"event": "done",
"data": {
"content": "Complete response here..."
}
}
When queue policy rejects/supersedes a request before execution, the server emits an error event with code: 3 (AgentBusy) and includes data.queue metadata (code, laneId, mode, overflow, droppedCount).
agent.cancel
Cancel the current agent operation.
Used by the web dashboard/chat stop button and channel-level /stop / /cancel command fast-paths.
Cancellation is best-effort and stops at the next agent/tool-loop safe point.
Flynn now propagates a run-level abort signal into model/tool execution, so providers/tools that honor AbortSignal typically stop promptly instead of waiting for request/tool timeouts.
Node Methods
node.register
Register node role/capabilities for the current WebSocket connection.
Registration is scoped to the connection. If a companion reconnects it must call node.register again
to restore node identity, capabilities, and access to node.* methods.
CompanionRuntimeClient can optionally replay cached node registration/status/location/push state on reconnect.
Request:
{
"id": 9,
"method": "node.register",
"params": {
"nodeId": "companion-desktop",
"role": "companion",
"protocolVersion": 1,
"capabilities": ["ui.canvas", "notifications"]
}
}
Response:
{
"id": 9,
"result": {
"registered": true,
"node": { "id": "companion-desktop", "role": "companion" },
"protocol": { "serverVersion": 1, "clientVersion": 1, "negotiatedVersion": 1 },
"capabilities": {
"declared": ["ui.canvas", "notifications"],
"enabled": ["ui.canvas", "notifications"]
}
}
}
node.capabilities.get
Return negotiated capabilities for the currently registered node connection.
node.location.set
Update the last-known location for the currently registered node connection.
Requires server.nodes.enabled: true and server.nodes.location.enabled: true.
Request:
{
"id": 10,
"method": "node.location.set",
"params": {
"latitude": 37.7749,
"longitude": -122.4194,
"accuracyMeters": 12.4,
"source": "gps",
"capturedAt": 1763241200000
}
}
Response:
{
"id": 10,
"result": {
"updated": true,
"node": { "id": "companion-desktop", "role": "companion" },
"location": {
"latitude": 37.7749,
"longitude": -122.4194,
"accuracyMeters": 12.4,
"source": "gps",
"capturedAt": 1763241200000,
"receivedAt": 1763241200451
}
}
}
node.location.get
Return the stored last-known location for the currently registered node connection.
node.status.set
Publish companion/node runtime status metadata (for example macOS menu-bar heartbeat state).
Request:
{
"id": 12,
"method": "node.status.set",
"params": {
"platform": "macos",
"appVersion": "0.3.1",
"deviceName": "MacBook Pro",
"statusText": "Idle",
"batteryPct": 64,
"powerSource": "battery"
}
}
node.push_token.set
Register a node push token (APNs or FCM) for companion delivery routing.
Requires server.nodes.push.enabled: true.
Request:
{
"id": 13,
"method": "node.push_token.set",
"params": {
"provider": "fcm",
"token": "fcm_abcdefghijklmnopqrstuvwxyz123456"
}
}
system.capabilities
Return gateway protocol version, node policy status, and feature-gate snapshot.
Request:
{
"id": 11,
"method": "system.capabilities"
}
Response:
{
"id": 11,
"result": {
"protocol": { "version": 1 },
"nodes": {
"enabled": true,
"locationEnabled": true,
"pushEnabled": true,
"allowedRoles": ["companion"],
"registered": true,
"role": "companion",
"nodeId": "companion-desktop"
},
"featureGates": {
"ui.canvas": true
}
}
}
system.location
Return the operator-facing snapshot of registered node locations.
system.nodes
Return the operator-facing snapshot of registered node connections (identity, role, capabilities, location/status).
Push tokens are returned as masked previews (tokenPreview) and never exposed in full.
Canvas Methods
Canvas artifacts are stored per session and persisted to the gateway data directory so they survive daemon restarts.
canvas.put
Upsert a session-scoped canvas artifact.
Request:
{
"id": 12,
"method": "canvas.put",
"params": {
"sessionId": "ws:abc123",
"artifactId": "summary-card",
"type": "note",
"title": "Draft Summary",
"content": { "markdown": "## Notes" },
"metadata": { "lane": "analysis" }
}
}
canvas.get
Fetch a single artifact by id.
canvas.list
List artifacts for a session (newest first).
canvas.delete
Delete a single artifact.
canvas.clear
Delete all artifacts for a session.
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.
When browser automation is enabled, tools.list may include workflow-reliability helpers such as:
browser.wait_for, browser.assert, browser.extract, browser.checkpoint.save, and browser.checkpoint.resume
in addition to baseline navigation/click/type/content/eval 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).
Browser workflow tools enforce runtime guardrails configured in browser.*:
domain allowlists, high-risk-domain confirmation (confirm_high_risk=true), retry bounds, and step-budget limits.
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"
}
}
Audio attachments (TTS responses) are best-effort with provider fallback: the daemon can try an ordered TTS provider chain and tracks provider health/cooldown between attempts. If synthesis still fails, the gateway returns the text reply without any audio attachment.
context_warning
Proactive context pressure signal emitted by agent.send before done.
{
"id": 1,
"event": "context_warning",
"data": {
"level": "warning",
"message": "Context usage is 76.0% (152000/200000 estimated tokens).",
"budget": {
"estimatedTokens": 152000,
"contextWindow": 200000,
"remainingTokens": 48000,
"usagePct": 76,
"thresholdPct": 80,
"thresholdTokens": 160000,
"shouldCompact": false
},
"actions": {
"checkpointSaved": false,
"autoCompacted": false
}
}
}
run_state
Run lifecycle transition emitted during agent.send processing (text and voice/talk turns use the same lifecycle states and cancel semantics).
{
"id": 1,
"event": "run_state",
"data": {
"state": "start",
"timestamp": 1730140800000
}
}
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: ...",
"queue": {
"code": "overflow",
"laneId": "ws:abc123",
"mode": "followup",
"overflow": "drop_new",
"droppedCount": 1
}
}
}
data.queue is optional and only present for queue policy rejections/superseded requests.
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 'context_warning':
console.warn('Context warning:', data.level, data.message);
break;
case 'run_state':
console.log('Run state:', data.state, data.timestamp);
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 contextUsage() {
return this.sendRequest('system.contextUsage');
}
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 - Companion runtime client helper:
src/companion/runtimeClient.ts(node + system +canvas.*typed RPC wrappers, optionalautoConnect/autoReconnect, optional reconnect state replay,sendAgentMessagehandoff helper, connection event subscriptions) - Platform companion wrappers:
src/companion/platformClients.ts - Companion bootstrap manifest helper:
src/companion/bootstrapManifest.ts(typed packaging manifest contract used byflynn companion --export-bootstrap, including optional initial status/location/push payloads) - Companion release bundle helper:
src/companion/releaseBundle.ts(writes bootstrap JSON + launcher script + README +CHECKSUMS.sha256; optionalCHECKSUMS.sha256.sigwhen a signing key is provided) - Companion release bundle verifier:
src/companion/releaseVerify.ts(validatesCHECKSUMS.sha256and optional signature metadata against a provided public key) - Companion shell template helper:
src/companion/shellTemplate.ts(writes platform-native starter template files formacos,ios, andandroidshell scaffolding)