Skip to content

WebSocket Protocol

The chat interface communicates with the server over a WebSocket connection at /ws/{session_id}.

Connection

const ws = new WebSocket(`ws://${location.host}/ws/${sessionId}`);

Authentication is enforced on the HTTP upgrade request using the aide_user session cookie. Connections without a valid session are rejected.

On connect, the server immediately sends a models event.


Events: server → client

models

Sent immediately on connect. Contains all available models and the default selection.

{
  "type": "models",
  "models": [
    {
      "id": "anthropic:claude-sonnet-4-6",
      "name": "Claude Sonnet 4.6",
      "provider": "anthropic",
      "context_length": 200000,
      "pricing": {"prompt": 3.0, "completion": 15.0},
      "capabilities": {"vision": true, "tools": true, "online": false}
    }
  ],
  "default": "anthropic:claude-sonnet-4-6"
}

text

Streaming text from the model. Multiple text events may arrive for a single response.

{
  "type": "text",
  "content": "The weather in Oslo today is "
}

tool_start

The agent is about to execute a tool.

{
  "type": "tool_start",
  "call_id": "tc_abc123",
  "tool_name": "web",
  "arguments": {"operation": "search", "query": "Oslo weather"}
}

tool_done

A tool has finished executing.

{
  "type": "tool_done",
  "call_id": "tc_abc123",
  "tool_name": "web",
  "success": true,
  "result_summary": "{'query': 'Oslo weather', 'results': [...]...}",
  "confirmed": false
}

confirmation_required

The agent needs user approval before proceeding. The agent loop is paused until the client sends a confirm message.

{
  "type": "confirmation_required",
  "call_id": "tc_def456",
  "tool_name": "email",
  "arguments": {"operation": "send_email", "to": "alice@example.com", "subject": "..."},
  "description": "Send email to alice@example.com: 'Meeting tomorrow'"
}

images

Image(s) generated by an image-generation model.

{
  "type": "images",
  "data_urls": ["data:image/png;base64,..."]
}

done

The agent loop has finished. No more events will be sent for this turn.

{
  "type": "done",
  "text": "The weather in Oslo today is 12°C and cloudy.",
  "tool_calls_made": 1,
  "usage": {"input_tokens": 1234, "output_tokens": 56}
}

error

A fatal error occurred. The run has been aborted.

{
  "type": "error",
  "message": "Provider error: 429 Too Many Requests"
}

Messages: client → server

message

Send a user message to start an agent run.

{
  "type": "message",
  "text": "What's the weather in Oslo?",
  "model": "anthropic:claude-sonnet-4-6",
  "session_id": "550e8400-e29b-41d4-a716-446655440000",
  "attachments": [
    {
      "media_type": "image/jpeg",
      "data": "<base64-encoded image>"
    }
  ]
}
  • model — optional; overrides the default for this turn
  • session_id — optional; if omitted, a new UUID is generated
  • attachments — optional; list of image or PDF attachments

confirm

Approve or deny a tool call that requires confirmation.

{
  "type": "confirm",
  "call_id": "tc_def456",
  "response": true
}
  • response: true — approved, proceed with the tool call
  • response: false — denied, tool call is cancelled

Server architecture

The WebSocket handler in main.py runs two concurrent asyncio tasks per connection:

  • receiver(): Reads incoming messages from the client and handles them:
  • type: confirm → immediately resolves the pending confirmation_manager request (unblocks the agent loop without waiting for the sender)
  • type: message → puts the message in a queue

  • sender(): Processes the message queue through the agent loop, serialises events to JSON, and sends them to the client

The receiver() and sender() run as asyncio.gather(receiver(), sender()) — fully concurrent.

Why the split?
The confirmation modal pattern requires the browser to send a confirm message while the agent loop is paused. If receiver and sender were sequential, the confirmation message would never be read until after the timeout. By running them concurrently, the receiver can unblock the agent loop while the sender is awaiting the confirmation.