RGP/1 Wire Protocol Specification¶
Version: 1 · Status: frozen · Cross-language contract between the Node Driver and
all SDK bindings (Python, JS). Source of truth for byte layout: this document plus
the shared fixture corpus in packages/protocol/test/framing.test.ts and
python/reactivegraph/tests/test_protocol.py.
1. Transport¶
- Local transport: a single long-lived child-process stdio connection.
- Frames: four-byte unsigned big-endian byte length followed by one MessagePack envelope.
- Maximum decoded frame: 32 MiB by default, configurable downward. Oversized frames close the session with a protocol error.
- Frame order is preserved on the wire, but requests may complete out of order;
every request and callback has a globally unique
idwithin its session. - Remote transport uses the exact same envelope schema over WebSocket or HTTP/2; no second JSON API with different lifecycle meanings.
- Secrets, raw prompts, tool arguments, and state values are absent from transport-level error messages unless explicit unsafe development mode is enabled.
2. Envelope schema¶
type Envelope = {
version: 1;
id: string;
kind: "request" | "response" | "event" | "cancel";
method: Method;
trace?: { runId?: string; transactionId?: string; parentSpanId?: string };
payload: unknown;
};
type Method =
| "DRIVER_HELLO"
| "COMPILE_GRAPH"
| "RELEASE_GRAPH"
| "RUN"
| "RESUME"
| "GET_STATE"
| "TASK_INVOKE"
| "TASK_RESULT"
| "STREAM_EVENT"
| "CHECKPOINT_OP"
| "STORE_OP"
| "EXPORT_DOT"
| "VECTOR_UPSERT"
| "VECTOR_SEARCH"
| "TRACE_QUERY"
| "CANCEL"
| "SHUTDOWN";
3. Canonical values (codec)¶
Cross-language canonical state values (see docs/research/msgpack-cross-language.md):
| Value | Encoding |
|---|---|
| null / boolean | msgpack nil / bool |
| integer | int; int64 range -2^63 ≤ n < 2^64. TS uses bigint + useBigInt64:true. |
| float | msgpack float |
| string | msgpack str |
| bytes | msgpack bin (Uint8Array / bytes) |
| list / array | msgpack array |
| map | msgpack map with string keys |
| timestamp | msgpack native ext -1 (32/64/96-bit layouts) |
| decimal | custom ext type 0, payload = UTF-8 decimal string |
| registered extensions | custom ext types ≥1 |
Non-canonical objects (functions, undefined, cyclic refs, arbitrary class instances,
Map/Set as bare values) are rejected by the codec with a CodecError/ProtocolError.
4. Validation rules¶
- Unknown
version(≠1) → reject. - Frame length >
maxFrameBytes→ProtocolError, close session (decoder resets). - Duplicate request
idin a session → reject second. - Malformed payload / undecodable envelope → reject that session only.
- A
responsewhosemethoddoes not match the pending request's method → reject.
5. Method payload contracts¶
| Method | Required fields | Negative cases |
|---|---|---|
DRIVER_HELLO |
sdkVersion, protocolVersions[], featureFlags | no common version; unknown required feature |
COMPILE_GRAPH |
graphSpec, callbackIds[], schemaHash | duplicate id; unregistered callback; cycle with no policy |
RUN |
graphId, input, config, threadId | missing thread for durable run; unserializable value |
RESUME |
runId, threadId, interruptResponse, expectedVersion | stale version; already-terminal run |
TASK_INVOKE |
invocationId, snapshotVersion, callbackId, allowedPaths | unknown callback; duplicate invocation; response may carry optional stream_chunks (transient messages/custom) |
TASK_RESULT |
invocationId, readSet, patches, result, receipt | invalid path; write denied; stale state |
STREAM_EVENT |
cursor, eventType, payload, terminal | cursor regression; event after terminal |
EXPORT_DOT |
graphId | unknown graph |
TRACE_QUERY |
runId | no durable log; unknown run |
VECTOR_UPSERT |
namespace, id, vector, metadata? | missing id/vector |
VECTOR_SEARCH |
namespace, query, limit?, minScore? | missing query |
CANCEL |
targetId, reason | unknown target; terminal run |
6. Callback result contract (Python)¶
TaskSuccess(reads, patches, return_value, external_receipts)
TaskFailure(error_type, message, traceback, retryable)
TaskInterrupted(value, checkpoint_hint)
TaskCancelled(reason)
The Driver validates each form against the invocation's transaction and permission context. A callback never decides whether its own state changes commit.
Streaming callbacks: a host may register a generator callback (sync or
async). Each yielded value becomes a transient stream_chunks entry in the
TASK_INVOKE response — a {type, payload} dict passes through, anything else
(e.g. a token string) is wrapped as a messages chunk. The Driver forwards
these as STREAM_EVENTs (messages/custom) before committing the generator's
return value (which must still be a TaskSuccess shape). Consumers see LLM
token churn in stream() before the task commits. The Python binding exposes
this as a plain generator task function:
def llm(model_state):
for token in model_tokens:
yield token # streamed as a messages chunk
return {"patches": [...], "writes": [...], "return_value": "done"}