Serialization
For harness run-event payloads in a custom transport, see Run Event Serialization. This page describes runtime message codecs instead.
Most AgentLane code does not need to think about serialization at all. If you send dataclasses, Pydantic models, protobuf messages, or plain JSON-compatible values, the runtime usually has enough information to do the right thing.
Transport becomes important when work crosses a process boundary, when you need stable schema identifiers, or when you want to take control of how values are encoded and decoded.
When that boundary matters, the runtime leans on
SerializerRegistry to choose a
MessageSerializer, and the
encoded value travels as a
WirePayload.
The Default Path
Every runtime owns a default
SerializerRegistry. That
registry can infer a serializer from the Python value you send.
In the common case, the runtime figures out:
- a schema id for the value’s type (
infer_schema_id_for_value/infer_schema_id_for_type) - the content type for the payload (
infer_content_type_for_value/infer_content_type_for_type) - which serializer should handle that combination
That means most application code can simply send a value and let the transport
layer do the rest. The runtime’s default registry is produced by
create_default_serializer_registry,
which constructs a SerializerRegistry(auto_register_defaults=True).
SerializerRegistry exposes two operating modes through its
auto_register_defaults flag:
- default mode (
auto_register_defaults=True): the registry infers and caches a serializer for a common payload type the first time it is used - strict mode (
auto_register_defaults=False): the serializer key must already be registered or encode/decode raisesUnknownSerializerError
Beyond register_type, the registry surface includes register,
register_many, unregister, has, encode, decode, and the read-only
serializers snapshot.
from pydantic import BaseModel
from agentlane.messaging import AgentId
from agentlane.runtime import SingleThreadedRuntimeEngine
class TaskModel(BaseModel):
name: str
runtime = SingleThreadedRuntimeEngine()
recipient = AgentId.from_values("planner", "default")
result = await runtime.send_message(TaskModel(name="compile"), recipient)
When Explicit Registration Helps
Explicit registration is useful when you need stricter control than the default inference path gives you.
Typical reasons include:
- remote decode should reconstruct a typed value instead of falling back to a plain dict or list
- a custom content type or serializer is part of your contract
- the payload type is uncommon enough that you do not want to rely on inference
from pydantic import BaseModel
from agentlane.transport import SerializerRegistry
class TaskModel(BaseModel):
name: str
registry = SerializerRegistry()
registry.register_type(TaskModel)
If you need full control, implement the
MessageSerializer protocol
directly and register that serializer yourself.
Built-in Serializers
The registry ships four concrete serializers, all keyword-only. Default
inference (and register_type) picks one of them based on the Python type:
PydanticJsonSerializer(*, schema_id, model_type, content_type=JSON_CONTENT_TYPE) forpydantic.BaseModelsubclasses.DataclassJsonSerializer(*, schema_id, model_type, content_type=JSON_CONTENT_TYPE) for dataclass types.ProtobufSerializer(*, schema_id, message_type, content_type=PROTOBUF_CONTENT_TYPE) forgoogle.protobuf.message.Messagesubclasses.JsonValueSerializer(*, schema_id, content_type=JSON_CONTENT_TYPE) for any other JSON-compatible value. This is also the conservative decode-only fallback: a JSON payload with no registered typed serializer is recovered as plaindict/list/scalars, while typed protobuf, Pydantic, and dataclass decoding always requires explicit registration.
Wire Payloads
At the transport boundary, runtime payloads become
WirePayload values. Most
code never needs to construct them directly, but the runtime exposes helpers for
cases where you do want to convert a payload manually.
wire_payload = runtime.payload_to_wire_payload(payload)
restored_payload = runtime.wire_payload_to_payload(wire_payload)
A WirePayload carries four
fields: schema_id (SchemaId),
content_type (ContentType),
encoding (WireEncoding: JSON,
PROTOBUF, or BYTES), and body (the serialized bytes).
These transport identity types validate eagerly:
SchemaIdmust be non-empty, use only letters, numbers, and_.:/-separators, and be globally namespaced — it must contain at least one of.,:, or/, or construction raisesValueError.ContentTypemust be MIME-like (for exampleapplication/json), or construction raisesValueError.
The canonical content-type constants are JSON_CONTENT_TYPE
(application/json), PROTOBUF_CONTENT_TYPE (application/x-protobuf), and
OCTET_STREAM_CONTENT_TYPE (application/octet-stream).
When converting between the transport and messaging layers, WireEncoding maps
to the messaging PayloadFormat through the
wire_encoding_for_payload_format
and
payload_format_for_wire_encoding
helpers.
Errors
All transport serializer errors subclass
SerializationError:
SerializerConflictError— raised byregister(and thereforeregister_many/register_type) when a serializer already exists for the(schema_id, content_type)key andreplace=False.UnknownSerializerError— raised byunregisterwhen the key is absent, and byencode/decodewhen no serializer resolves for the key.SerializerEncodeError— raised byencodewhen the underlying codec fails during object-to-bytes conversion.SerializerDecodeError— raised bydecodewhen the underlying codec fails during bytes-to-object conversion.
A Useful Rule Of Thumb
If both sender and receiver live inside one normal runtime and you are sending ordinary Python models, do not over-configure serialization. Start with the default path and only reach for explicit registry setup when you have a clear transport requirement.