Latest Results
fix(core): accept `Serializable` constructor-envelope wire shape in `_convert_to_message`
`Serializable.to_json()` (Python) and its JS twin emit a constructor
envelope of the form
`{"lc": 1, "type": "constructor", "id": [..., "<ClassName>"], "kwargs": {...}}`
when a `BaseMessage` instance round-trips through JSON. Python's
`_convert_to_message` (and therefore `convert_to_messages`, which
`add_messages` calls on every input) only recognized `BaseMessage`,
`(role, content)` tuples, `{role, content}` / `{type, content}` dicts,
and strings — so this shape failed at the reducer boundary with
`MESSAGE_COERCION_FAILURE`. Servers receiving an envelope-shaped
`HumanMessage` from any client that JSON-serializes a `BaseMessage`
instance directly hit this.
This change accepts the envelope structurally: pattern-match the
four-field signature, look up the class name in a hardcoded allowlist
of message-type strings, and recurse through the standard
dict-with-type dispatch. No `langchain_core.load.load()` call and no
dynamic class instantiation — only message-type *strings* are mapped,
and they come from a fixed table in this module.
The structural unpack adds zero new attack surface beyond what
`_convert_to_message` already accepts from canonical
`{type, content, ...}` dicts:
- No dynamic class resolution from a registry
- No `eval` / `exec` / dynamic import of attacker-controlled names
- Unknown class names fall through to the existing
`MESSAGE_COERCION_FAILURE` path
- Final construction goes through the same
`_create_message_from_message_type` branches every other dict input
uses
Covered by nine new unit tests in `test_utils.py` exercising each
supported subclass plus the unknown-class fall-through and partial-
shape non-match cases.nh/messages-accept-lc-constructor-envelope fix(core): accept langchain-js `Serializable.toJSON()` wire shape in `_convert_to_message`
Older `@langchain/langgraph-sdk` releases serialize `BaseMessage`
instances on the wire via `JSON.stringify`, which invokes
`Serializable.toJSON()` and emits
`{"lc": 1, "type": "constructor", "id": [..., "<ClassName>"],
"kwargs": {...}}`. Python's `_convert_to_message` only recognized
`BaseMessage` / `(role, content)` tuple / `{role, content}` dict /
`{type, content}` dict / string, so this shape failed at the reducer
boundary with `MESSAGE_COERCION_FAILURE` — every Python langgraph-api
server today rejects a JS client that builds `new HumanMessage(prompt)`
and submits it.
Accept the envelope structurally: pattern-match the 4-field signature,
look up the class name in a hardcoded allowlist of message-type
strings, and recurse through the standard dict-with-type dispatch.
No `langchain_core.load.load()` call and no dynamic class
instantiation — only the message-type *strings* are dynamic, and they
come from a fixed mapping in this module.
The structural unpack adds zero new attack surface beyond what the
function already accepts from canonical `{type, content, ...}` dicts:
- No dynamic class resolution from a registry
- No `eval` / `exec` / `import` of attacker-controlled names
- Unknown class names fall through to the existing
`MESSAGE_COERCION_FAILURE` path
- Final construction goes through the same
`_create_message_from_message_type` branches that every other dict
input uses
Covered by 9 new unit tests in `test_utils.py` exercising each
supported subclass plus the unknown-class fall-through and partial-
shape non-match cases.nh/messages-accept-lc-constructor-envelope Latest Branches
-1%
nh/messages-accept-lc-constructor-envelope 0%
dependabot/uv/libs/langchain_v1/langsmith-0.8.4 -1%
dependabot/uv/libs/core/langsmith-0.8.4 © 2026 CodSpeed Technology