agent-webkit
Reference

Wire protocol

The complete event/message catalog. protocol_version = "1.0".

This is the single source of truth for the protocol between any L1 client (@agent-webkit/core) and any compliant server. The reference server in agent-webkit-server is the canonical implementation.

protocol_version = "1.0".

Transport

  • All control traffic is HTTP/JSON.
  • All inbound (server → client) streaming is SSE.
  • All outbound (client → server) is POST with a JSON body.

Auth

  • Bearer token in Authorization: Bearer <token> header.
  • Auth can be disabled per-server via AuthConfig(disabled=True). When disabled, the browser-native EventSource may be used; when enabled, a fetch-based SSE polyfill is required because EventSource cannot send custom headers. @agent-webkit/core ships the polyfill by default.

Endpoints

POST /sessions

Create a new session. Spawns the underlying ClaudeSDKClient subprocess.

Request body (all optional):

{ "model": "claude-opus-4-7", "permission_mode": "default", "cwd": "/path/to/project" }

Response:

{ "session_id": "uuid", "protocol_version": "1.0" }

GET /sessions/{session_id}/stream

Server-Sent Events stream. Multi-subscriber: any number of clients may subscribe to the same session simultaneously, each with an independent cursor.

Headers honored:

  • Last-Event-ID: <seq> — replay from after that sequence number. Must be a non-negative integer; malformed values return 400 Bad Request. Returns 412 Precondition Failed if the requested seq has been evicted from the ring buffer.

Events:

  • Every event has a numeric id (server-assigned monotonic seq), an event name, and JSON data.
  • A :keepalive SSE comment frame is emitted every 15s.

POST /sessions/{session_id}/input

Body is a single inbound message (see "Inbound message types" below).

Returns 204 No Content on success, 409 Conflict if a permission_response / question_response arrives after another subscriber already replied.

DELETE /sessions/{session_id}

Graceful teardown. Drains the SDK client and closes all subscribers with a final done event.

Inbound message types

All inbound messages are JSON objects with a type discriminator.

typeFields
user_messagecontent: string | ContentBlock[]
interrupt(none) — calls client.interrupt(). Does NOT push to queue.
permission_responsecorrelation_id, behavior: "allow"|"deny". With allow: updated_input?, updated_permissions?. With deny: message?, interrupt? (deny-only — aborts the in-flight turn). interrupt MUST NOT be sent with allow.
question_responsecorrelation_id, answers (shape per AskUserQuestion tool input)
set_permission_modemode: string
set_modelmodel: string | null
stop_tasktask_id: string

ContentBlock is {type:"text", text} or {type:"image", source:{type:"base64", media_type, data}}.

correlation_id for permissions is the SDK's tool_use_id. For AskUserQuestion it is the tool_use_id of the AskUserQuestion tool call.

Outbound SSE event types

eventdata shape
session_ready{session_id, protocol_version} — first event
message_delta{message_id, delta}
message_complete{message_id, message}
tool_use{message_id, tool_use_id, tool_name, input}
tool_result{tool_use_id, output, is_error}
permission_request{correlation_id, tool_name, input, context} — awaits permission_response
ask_user_question{correlation_id, questions} — awaits question_response
hook_decision_request{correlation_id, hook_event, hook_input} — optional v1
result{session_id, subtype, total_cost_usd, ...} — end of a turn
error{code, message}
mcp_status_change{server_name, status}
done{} — terminal event for the session

Race semantics

For permission_request / ask_user_question / hook_decision_request: first reply wins. Subsequent replies for the same correlation_id get HTTP 409.

Dual ID scheme

  • id (top-level SSE id) is a server-only monotonic seq. Used for Last-Event-ID resume.
  • message_id, tool_use_id, correlation_id are SDK-issued identifiers used for content identity (delta → complete reconciliation, permission routing, etc.). Both coexist.

Known limitations (v1)

  • Single-host live fan-out. Cross-host failover requires the Postgres adapter; cross-host simultaneous fan-out is not supported.
  • interrupt() does not drain the queue — the server must finish draining receive_messages() before accepting the next query().
  • hook_decision_request is emitted but the L2 React hook does not yet expose it as a first-class state slot. Use the L1 client if you need it today.

On this page