Skip to main content
Version: v2.0.0

Frame Protocol Conversion

Audience: contributors adding a protocol or understanding the internal wire representation — the EventMeshFrame format, the FrameAdaptor SPI, and end-to-end conversion chains per protocol. Part of the offset / load-balancing / frame design trio; see also Offset management.


Architectural layering

┌─ external protocols (FrameAdaptor SPI) ─────────────────────────────┐
│ │
│ CloudEvents (HTTP/SSE/WS) → CloudEventsFrameAdaptor → EventMeshFrame │
│ MeshMessage (legacy TCP) → MeshMessageFrameAdaptor → EventMeshFrame │
│ A2A (JSON-RPC 2.0) → A2AFrameAdaptor → EventMeshFrame │
│ future protocol → new FrameAdaptor → EventMeshFrame │
│ │
│ The three protocols are peers; each converts directly to Frame — │
│ none of them passes through CloudEvent. │
└──────────────────────────────┬───────────────────────────────────────┘
│ external protocol → Frame, directly
┌──────────────────────────────▼───────────────────────────────────────┐
│ internal (runtime + storage): EventMeshFrame end to end │
│ │
│ ingress publish → Frame → storage.send(Frame) → MQ bytes │
│ MQ bytes → storage.poll() → Frame → dispatch/filter/TTL → Frame │
└──────────────────────────────┬───────────────────────────────────────┘
│ egress: Frame → the matching FrameAdaptor
┌──────────────────────────────▼───────────────────────────────────────┐
│ egress (FrameAdaptor SPI, per client connection protocol) │
│ │
│ Frame → CloudEvents-JSON (SSE / WS / HTTP poll) │
│ Frame → MeshMessage Package (legacy TCP) │
│ Frame → A2A JSON-RPC bytes (A2A callbacks) │
└────────────────────────────────────────────────────────────────────────┘

CloudEvent is not the internal representation — it is one of the external ingress formats for CloudEvents clients, converted to Frame by CloudEventsFrameAdaptor on entry. MeshMessage and A2A likewise each have their own independent adaptor; they never pass through CloudEvent.

EventMeshFrame wire format

fixed header, 14 bytes:
[magic:1=0xEF][ver:1=1][msgType:1][flags:1][seq:4][keyCount:2][dataLen:4]

KV attribute section (keyCount ×):
[nameLen:2][name:UTF-8][valLen:4][value:UTF-8]

data:
raw bytes (streaming chunk/prompt / event business payload)

msgType = STREAM_REQ(1) | STREAM_CHUNK(2) | EVENT(3)
flags = bit0 done | bit1 hasError | bit2 hasMeta (streaming use)

Field mapping per msgType

msgTypeFixed-header fieldsKV attributesdata
STREAM_REQsid=sessionId, replyTo=reply address, model?, conv?prompt text
STREAM_CHUNKseq=in-stream sequence, flags.done=termination marksid, etype?, err?, meta?(JSON)chunk text
EVENTid/type/source/subject/time/emttl/emcorrelationid/empopck/user extensionsevent payload

The FrameAdaptor SPI

Bidirectional conversion between external protocols and EventMeshFrame is defined by the FrameAdaptor SPI (eventmesh-protocol-plugin/eventmesh-protocol-api/.../FrameAdaptor.java):

@EventMeshSPI(eventMeshExtensionType = EventMeshExtensionType.PROTOCOL)
public interface FrameAdaptor {
EventMeshFrame toFrame(ProtocolTransportObject proto); // ingress
ProtocolTransportObject fromFrame(EventMeshFrame frame); // egress
String getProtocolType();
}

The runtime never calls EventMeshFrame.fromCloudEvent() / .toCloudEvent() / MeshMessageFrameCodec directly — everything goes through FrameAdaptors.get(<protocol name>). Adding a new protocol is just implementing FrameAdaptor + registering the SPI — no runtime code changes.

Protocol plugin modules

ModuleFrameAdaptorExternal protocolConversion
protocol-api— (SPI interface + FrameAdaptors loader only)zero implementations
protocol-cloudeventsCloudEventsFrameAdaptorCloudEvents-JSON (HTTP/SSE/WS)CE-JSON bytes ↔ Frame (field mapping via the CE object)
protocol-meshmessageMeshMessageFrameAdaptorMeshMessage Package (TCP)Package ↔ Frame (direct field mapping, zero CE intermediary)
protocol-a2aA2AFrameAdaptorA2A JSON-RPC 2.0JSON-RPC bytes ↔ Frame (direct field mapping, zero CE intermediary)
protocol-grpcdeleted (empty shell, no sources)

The WireCodec SPI (internal MQ wire encoding)

Byte-level encode/decode for the internal MQ wire is defined by the WireCodec SPI (eventmesh-common/.../wire/):

public interface WireCodec {
byte[] encode(StreamRequest request);
StreamRequest decodeRequest(byte[] bytes);
byte[] encode(StreamChunk chunk);
StreamChunk decodeChunk(byte[] bytes);
byte[] encode(CloudEvent event);
CloudEvent decodeEvent(byte[] bytes);
}
  • Default implementation: EventMeshFrameCodec (EventMeshFrame ↔ byte[]).
  • Replaceable via -Deventmesh.wire.codec=<fqcn>.

Full conversion chains per protocol

CloudEvents (HTTP/SSE/WS clients)

Ingress (client → runtime):

SDK sends CloudEvents-JSON bytes
→ UniHttpServer.publish: body = CE-JSON bytes
→ UniIngressService.publish(topic, CloudEvent): CE → EventMeshFrame.fromCloudEvent(event)
→ storage.send(topic, frame): frame.encode() → MQ bytes

Egress (runtime → client):

storage.poll() → frame (EventMeshFrame)
→ SseConnection.send / WsConnection.send / UniHttpServer.poll
→ FrameAdaptors.toCloudEventsJson(frame): frame.toCloudEvent() → CE-JSON serialize
→ written as SSE data: / WS TextFrame / HTTP JSON

MeshMessage (legacy TCP clients)

Ingress:

legacy TCP SDK sends Package (EventMeshMessage)
→ MeshMessagePackageRouter.route(pkg)
→ FrameAdaptors.get("meshmessage").toFrameSilent(pkg)
→ MeshMessageFrameAdaptor.toFrame:
topic → attributes["subject"]
body → data
header.seq → attributes["id"]
properties → attributes KV
→ TcpRequest.publish(topic, frame)
→ UniIngressService.publish(topic, frame) (Frame overload — skips CE conversion)
→ storage.send(topic, frame)

Egress:

ReliableDispatcher.deliver → NettyTcpPushChannel.deliver(deliveryId, frame, callback)
→ FrameAdaptors.get("meshmessage").fromFrameSilent(frame)
→ MeshMessageFrameAdaptor.fromFrame:
attributes["subject"] → topic
data → body
attributes KV → properties
→ Package(header=ASYNC_MESSAGE_TO_CLIENT, body=EventMeshMessage)
→ channel.writeAndFlush(pkg)

A2A (JSON-RPC clients)

Ingress:

A2A client sends JSON-RPC 2.0 bytes
→ FrameAdaptors.get("a2a").toFrameSilent(new ByteTransport(jsonBytes))
→ A2AFrameAdaptor.toFrame:
parse JSON-RPC → extract method/params/_topic
method → attributes["ema2amethod"]
params._topic → attributes["subject"]
raw JSON → data
→ EventMeshFrame

Egress:

EventMeshA2ATransport receives frame
→ frame.data() = the original A2A JSON-RPC bytes (stored verbatim at ingress)
→ ByteTransport(jsonBytes) → A2A client

Frame conversion in the storage SPI

Storage plugins (MeshStoragePlugin + LiteTopicCapable) carry EventMeshFrame directly on send/poll:

send(topic, EventMeshFrame frame):
→ rocketmq5: frame.encode() → MQ message body bytes
→ kafka: frame.encode() → ProducerRecord value
→ rocketmq4: frame.encode() → MQ message body bytes

poll(topic, partition, ...):
→ MQ bytes → EventMeshFrame.decode(bytes)
→ with legacy CE-JSON fallback (old messages → EventMeshFrame.fromCloudEvent(deserialize))

Where it landed

LayerChangeStatus
storage SPIMeshStoragePlugin.send/poll + LiteTopicCapable.sendLite/pullLite all EventMeshFrame
runtime dispatchDelivery/BufferedEvent/PushChannel/Connection/DeadLetterSink/ReliableDispatcher/PushService + CloudEventFilter/SubscriptionManager fully Frame-typed
streamingMode-1 (runtime↔agent cross-process) + Mode-2 (in-runtime pub/sub) all EventMeshFrame
legacy connectorsProducer/Consumer SPI untouched (separate subsystem, speaks CE, boundary conversion)✅ kept
WireCodec SPIdefault EventMeshFrameCodec, replaceable
FrameAdaptor SPICloudEvents as an independent plugin + MeshMessage + A2A each with an independent adaptor