> ## Documentation Index
> Fetch the complete documentation index at: https://docs.scomp.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Wire format

> JSON-RPC 2.0 framing, transport requirements, and what the bytes actually look like.

Scomp messages are JSON-RPC 2.0. That's the whole framing story — there is no scomp-specific envelope, no length prefix, no custom header. If you can parse JSON-RPC, you can parse scomp.

## JSON-RPC 2.0

Every message is a JSON object carrying `"jsonrpc": "2.0"` plus a request, response, or error shape.

**Request** (every method call from either peer):

```json theme={"theme":{"light":"vitesse-light","dark":"vesper"}}
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "eval",
  "params": { "code": "1 + 1" }
}
```

**Response — success:**

```json theme={"theme":{"light":"vitesse-light","dark":"vesper"}}
{ "jsonrpc": "2.0", "id": 1, "result": { "value": 2 } }
```

**Response — error:**

```json theme={"theme":{"light":"vitesse-light","dark":"vesper"}}
{ "jsonrpc": "2.0", "id": 1,
  "error": { "code": -32000, "message": "ReferenceError: foo is not defined",
             "data": { "kind": "EvalError", "stack": "..." } } }
```

The `id` echoes the request. The response carries `result` *or* `error`, never both.

## Bidirectional by design

Both peers can originate requests. The client opens with `handshake`, but after that either side can send `invoke` at any time. This is required for reverse-invoke — when the server's runtime needs to call a client-declared binding mid-eval — and it's why the transport needs to be bidirectional.

`id` is **scoped per direction**. The client and server each generate ids for the requests *they* originate. A client request with `id: 5` and a server request with `id: 5` are unrelated; the response correlation is by direction.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vesper"}}
sequenceDiagram
    participant C as Client
    participant S as Server
    C->>S: { "id": 1, "method": "eval", "params": {/* ... */} }
    S->>C: { "id": 7, "method": "invoke", "params": {/* ... */} }
    C->>S: { "id": 7, "result": {/* ... */} }
    S->>C: { "id": 1, "result": {/* ... */} }
```

## What we don't use

A few JSON-RPC 2.0 features are deliberately excluded in v0.1:

<CardGroup cols={2}>
  <Card title="No notifications" icon="bell-off">
    Requests without an `id` aren't used. The one exception is `release_ref`, which is structurally a notification — sent without expecting a response. Everything else carries an `id` and gets a response.
  </Card>

  <Card title="No batching" icon="layers-2">
    Implementations MUST NOT send batched requests and SHOULD reject incoming batches with a JSON-RPC error. Evals serialize on the server anyway; batches gain nothing.
  </Card>
</CardGroup>

The rationale: with bidirectional requests already in scope, notifications add ambiguity without solving a real problem (cancellation and telemetry are deferred to future revisions). And batches interact badly with bidirectional `invoke` — if a batch contains two evals and the first triggers reverse-invokes, the resolve order is undefined. Rejecting both keeps semantics unambiguous.

## Schemas

All schemas referenced by the protocol — binding `input` / `output`, handshake `metadata`, function-reference `input` / `output` — MUST conform to **JSON Schema Draft 2020-12**.

If your validator only supports older drafts, you're responsible for the conversion. Modern drafts are well-supported in TypeScript, Rust, Python, and most JVM languages.

## Transports

A conformant transport provides four things:

<Steps>
  <Step title="Bidirectional, message-oriented delivery">
    Each side can send framed messages independently of the other. No request/response correlation at the transport layer.
  </Step>

  <Step title="Reliable, in-order delivery within a connection">
    Messages sent are received exactly once, in send order. Lossy or out-of-order transports don't conform.
  </Step>

  <Step title="Clear connection boundaries">
    A well-defined moment when the transport opens, and when it closes. The protocol uses these as anchors for function-reference lifetime.
  </Step>

  <Step title="Optional transport-level identity">
    A way to convey peer credentials (bearer token, mTLS cert, etc.) from the transport into the server's authorization layer. Used at handshake time.
  </Step>
</Steps>

### WebSocket (the blessed transport)

WebSocket \[RFC 6455] is the v0.1 blessed transport.

* Each text frame contains **exactly one** JSON-RPC message. No batching of messages into a single frame.
* Binary frames MUST NOT be used.
* The subprotocol identifier `scomp.v0` MAY be advertised on the upgrade; servers SHOULD accept connections without subprotocol negotiation.
* Authentication piggybacks on the WebSocket upgrade (an `Authorization` header is typical). The protocol does not validate auth — it relays whatever the transport supplies as opaque handshake metadata.

### Other transports

Other transports conform as long as they meet the abstract contract above. An in-process pair (for tests), stdio (for subprocess-hosted runtimes), or a custom TCP framing all work. Such transports SHOULD document their framing rules explicitly and SHOULD reuse the JSON-RPC 2.0 wire format unchanged.

A future revision may bless additional transports (stdio is the most likely next addition).

## Worked example: a complete handshake

The first round-trip on every connection. The client declares one binding it wants the server to be able to invoke back; the server returns its own bindings and a session id.

```json theme={"theme":{"light":"vitesse-light","dark":"vesper"}}
// → client to server
{
  "jsonrpc": "2.0", "id": 1, "method": "handshake",
  "params": {
    "protocol": "0.1",
    "bindings": [
      {
        "name": "notify",
        "description": "Push a notification to the user.",
        "input": { "type": "object",
                   "properties": { "message": { "type": "string" } },
                   "required": ["message"] },
        "output": { "type": "null" }
      }
    ],
    "metadata": {
      "client": { "name": "scomp-agent-harness", "version": "0.1.0" }
    }
  }
}
```

```json theme={"theme":{"light":"vitesse-light","dark":"vesper"}}
// ← server to client
{
  "jsonrpc": "2.0", "id": 1,
  "result": {
    "protocol": "0.1",
    "sessionId": "sess_8f2a3c",
    "bindings": [
      {
        "name": "getOrder",
        "description": "Fetch an order by ID. Returns null if not found.",
        "input": { "type": "object",
                   "properties": { "id": { "type": "string" } },
                   "required": ["id"] },
        "output": { "oneOf": [{ "$ref": "#/$defs/Order" }, { "type": "null" }] },
        "effects": ["read"]
      }
    ],
    "metadata": {
      "server":  { "name": "orders-scomp", "version": "0.1.0" },
      "runtime": { "language": "javascript", "engine": "quickjs" }
    }
  }
}
```

After this exchange the session is live; the client can submit evals and either side can invoke.

## Worked example: an eval with a reverse-invoke

A common shape: the client submits code that calls a server-declared binding (`getOrder`); the server's runtime evaluates it, the binding's return value becomes the eval's result.

```json theme={"theme":{"light":"vitesse-light","dark":"vesper"}}
// 1. → eval
{ "jsonrpc": "2.0", "id": 2, "method": "eval",
  "params": { "code": "const o = await getOrder({ id: 'o_001' }); o.total" } }
```

```json theme={"theme":{"light":"vitesse-light","dark":"vesper"}}
// 2. ← eval response
{ "jsonrpc": "2.0", "id": 2, "result": { "value": 142.50 } }
```

If the server's `getOrder` handler needed to call back into the client mid-eval — say, asking the client to display a confirmation — you'd see an `invoke` request interleaved between the two messages above, with its own `id` independent of the eval's:

```json theme={"theme":{"light":"vitesse-light","dark":"vesper"}}
// 1.  → eval (id 2)
// 1a. ← invoke (id 100) — server asks client's "notify" binding
// 1b. → invoke response (id 100)
// 2.  ← eval response (id 2)
```

The bidirectional independence is what makes this work: the eval's response doesn't gate on the invoke's, and the invoke's response doesn't gate on the eval's. Each side just routes by id, in its own direction.

<Card title="Read the lifecycle" icon="arrow-right" href="https://github.com/scomp-protocol/scomp/blob/master/PROTOCOL.md#4-lifecycle" horizontal>
  Lifecycle — the full state machine, eval serialization, and how nested invokes stay deadlock-free.
</Card>
