fix: update deepagents and langchain versions (#251)

* fix: update deepagents and langchain versions; enhance _reduce_messages_delta handling for None state

* fix: update langchain version constraint to >=1.3 in pyproject.toml and uv.lock
This commit is contained in:
Xi Zhang
2026-05-31 22:12:13 +01:00
committed by GitHub
parent a13904185d
commit 3ce6523faf
4 changed files with 149 additions and 18 deletions
+8 -4
View File
@@ -24,7 +24,7 @@ from collections.abc import AsyncIterator, Awaitable, Callable
from contextlib import asynccontextmanager
from datetime import UTC, datetime
from pathlib import Path
from typing import Any
from typing import Any, cast
import aiosqlite
from langchain_core.messages import (
@@ -509,7 +509,7 @@ async def _table_exists(conn: aiosqlite.Connection, table: str) -> bool:
def _reduce_messages_delta(
state: list[AnyMessage], writes: list[Any]
state: list[AnyMessage] | None, writes: list[Any]
) -> list[AnyMessage]:
"""Inline copy of deepagents' ``_messages_delta_reducer``.
@@ -532,6 +532,10 @@ def _reduce_messages_delta(
so HTTP-driven graphs (and persisted blobs that round-tripped
through JSON) reconstruct correctly without a separate coercion
step.
``state`` may be None on ``DeltaChannel.replay_writes`` for threads
whose earliest checkpoint did not seed ``messages: []``, and is
treated as the empty list.
"""
flat: list[Any] = []
for w in writes:
@@ -544,9 +548,9 @@ def _reduce_messages_delta(
state_msgs: list[AnyMessage] = (
state
if state and isinstance(state[0], BaseMessage)
else convert_to_messages(state) # type: ignore[arg-type]
else cast("list[AnyMessage]", convert_to_messages(state or []))
)
msgs: list[AnyMessage] = convert_to_messages(flat) # type: ignore[assignment]
msgs: list[AnyMessage] = cast("list[AnyMessage]", convert_to_messages(flat))
# ``REMOVE_ALL_MESSAGES`` resets everything; honor the last sentinel
# in the batch — discard prior state plus every write before it.
+2 -2
View File
@@ -16,8 +16,8 @@ classifiers = [
"Programming Language :: Python :: 3",
]
dependencies = [
"deepagents[quickjs]~=0.6.2",
"langchain>=1.2",
"deepagents[quickjs]~=0.6.7",
"langchain>=1.3",
"langchain-anthropic>=1.4",
"langchain-openai>=1.2",
"langchain-nvidia-ai-endpoints>=1.2",
+128 -1
View File
@@ -8,12 +8,15 @@ import unittest
from datetime import UTC
from unittest.mock import patch
from langchain_core.messages import AIMessage, HumanMessage
import pytest
from langchain_core.messages import AIMessage, HumanMessage, RemoveMessage
from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer
from langgraph.graph.message import REMOVE_ALL_MESSAGES
from EvoScientist.sessions import (
AGENT_NAME,
_format_relative_time,
_reduce_messages_delta,
delete_thread,
find_similar_threads,
generate_thread_id,
@@ -1868,5 +1871,129 @@ class TestDbStats(unittest.TestCase):
assert stats["size_bytes"] == 0
class TestReduceMessagesDeltaNoneState(unittest.TestCase):
"""Direct unit tests for the inline ``_reduce_messages_delta`` reducer.
Regression for the None-state crash: ``DeltaChannel.replay_writes``
can hand the reducer ``state=None`` for threads whose earliest
checkpoint never seeded ``messages: []``. Before the fix, the slow
path passed ``None`` straight into ``convert_to_messages`` and raised;
now ``state or []`` is substituted.
"""
def test_none_state_simple_append(self):
result = _reduce_messages_delta(None, [[HumanMessage(content="hi", id="1")]])
assert len(result) == 1
assert result[0].content == "hi"
assert result[0].id == "1"
def test_none_state_empty_writes(self):
# No state and nothing to append → empty list, no crash.
assert _reduce_messages_delta(None, []) == []
def test_empty_state_still_appends(self):
# Regression guard: an explicit empty-list state must behave the
# same as None — append the single write.
result = _reduce_messages_delta([], [[AIMessage(content="yo", id="2")]])
assert len(result) == 1
assert result[0].content == "yo"
assert result[0].id == "2"
def _signature(messages):
"""Comparable shape for reducer-output equality assertions."""
return [(type(m).__name__, m.id, m.content) for m in messages]
def _import_upstream_reducer():
"""Import deepagents' private delta reducer, or fail loudly.
A ``pytest.fail`` (not ``skip``) is deliberate: this test is the
tripwire that fires when the upstream private symbol is renamed or
relocated. A silent skip would let semantic drift between EvoSci's
inline copy (``sessions.py``) and upstream go unnoticed.
"""
try:
from deepagents._messages_reducer import (
_messages_delta_reducer as upstream,
)
except ImportError as exc: # pragma: no cover - tripwire path
pytest.fail(
"deepagents._messages_reducer._messages_delta_reducer could not "
f"be imported ({exc}). The upstream private reducer that EvoSci's "
"inline copy in sessions.py (_reduce_messages_delta) mirrors has "
"moved or been renamed. Re-locate the upstream symbol and "
"re-evaluate the inline copy for semantic drift before adjusting "
"this test."
)
return upstream
class TestReduceMessagesDeltaUpstreamParity:
"""Behavioral parity vs deepagents' private ``_messages_delta_reducer``.
Drift detector: if upstream changes the reducer's semantics (dedup,
tombstone, reset, coercion) the EvoSci inline copy must be updated to
match. These cases pass equivalent batched writes to both functions
and assert identical output. (EvoSci's signature is ``writes: list[Any]``
and upstream's is ``list[list[AnyMessage]]``, but both flatten lists
vs single items the same way, so batched-list writes are equivalent.)
"""
@pytest.fixture(scope="class")
def upstream(self):
return _import_upstream_reducer()
def _assert_parity(self, upstream, state, writes):
evo_out = _reduce_messages_delta(state, writes)
up_out = upstream(state, writes)
assert _signature(evo_out) == _signature(up_out)
return evo_out
def test_parity_none_state_append(self, upstream):
out = self._assert_parity(
upstream, None, [[HumanMessage(content="hi", id="a1")]]
)
assert _signature(out) == [("HumanMessage", "a1", "hi")]
def test_parity_dedup_by_id(self, upstream):
state = [HumanMessage(content="orig", id="1")]
writes = [[HumanMessage(content="updated", id="1")]]
out = self._assert_parity(upstream, state, writes)
# In-place update, no duplicate appended.
assert _signature(out) == [("HumanMessage", "1", "updated")]
def test_parity_remove_message_tombstone(self, upstream):
state = [
HumanMessage(content="keep", id="1"),
AIMessage(content="drop", id="2"),
]
writes = [[RemoveMessage(id="2")]]
out = self._assert_parity(upstream, state, writes)
assert _signature(out) == [("HumanMessage", "1", "keep")]
def test_parity_remove_all_then_append(self, upstream):
state = [
HumanMessage(content="old1", id="1"),
AIMessage(content="old2", id="2"),
]
writes = [
[
RemoveMessage(id=REMOVE_ALL_MESSAGES),
HumanMessage(content="fresh", id="3"),
]
]
out = self._assert_parity(upstream, state, writes)
# Sentinel wipes prior state + earlier writes; only "fresh" remains.
assert _signature(out) == [("HumanMessage", "3", "fresh")]
def test_parity_dict_shorthand_coercion(self, upstream):
# Raw dict shorthand must coerce to a typed BaseMessage identically
# in both reducers.
writes = [[{"role": "user", "content": "x", "id": "d1"}]]
out = self._assert_parity(upstream, None, writes)
assert _signature(out) == [("HumanMessage", "d1", "x")]
if __name__ == "__main__":
unittest.main()
Generated
+11 -11
View File
@@ -794,7 +794,7 @@ wheels = [
[[package]]
name = "deepagents"
version = "0.6.2"
version = "0.6.7"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "langchain" },
@@ -804,9 +804,9 @@ dependencies = [
{ name = "langsmith" },
{ name = "wcmatch" },
]
sdist = { url = "https://files.pythonhosted.org/packages/98/3f/eade375f843b05abdae6c667f341300f13b810b3d69e7f1ef9a093d17bae/deepagents-0.6.2.tar.gz", hash = "sha256:81bc6f69c6f54bd0b54ce929e8299003a604747404e7f9d208a66842c5fbf1f3", size = 179651, upload-time = "2026-05-18T23:57:50.072Z" }
sdist = { url = "https://files.pythonhosted.org/packages/51/bb/bb837a2c51631fe9d7eedf6aca7629ddca6336831801e75efcd2f5fa9c27/deepagents-0.6.7.tar.gz", hash = "sha256:af7b5857b28e29a847a4ced4cc7aaa809d33d42107696b1ca5d978d17e96b831", size = 194236, upload-time = "2026-05-30T04:42:14.591Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8d/cf/a760686e1bb347287bcb9f741134a088980112a7633d3b2017fb13a8adb0/deepagents-0.6.2-py3-none-any.whl", hash = "sha256:202c42c06b5a7abb762dedba6a57073253096fa28effa6e156db280e10027322", size = 204754, upload-time = "2026-05-18T23:57:48.665Z" },
{ url = "https://files.pythonhosted.org/packages/97/25/3a7f23d04778ccb95542f1b1ed39826290916221cc8203d0fb8b26c3edc1/deepagents-0.6.7-py3-none-any.whl", hash = "sha256:3518c1e5f4b9f6588ba39912b668b42b5c864b99a638e94c166d1c7176c7388e", size = 218740, upload-time = "2026-05-30T04:42:13.225Z" },
]
[package.optional-dependencies]
@@ -981,13 +981,13 @@ requires-dist = [
{ name = "certifi", marker = "extra == 'wechat'", specifier = ">=2024.0" },
{ name = "cryptography", marker = "extra == 'all-channels'", specifier = ">=41.0" },
{ name = "cryptography", marker = "extra == 'qq'", specifier = ">=41.0" },
{ name = "deepagents", extras = ["quickjs"], specifier = "~=0.6.2" },
{ name = "deepagents", extras = ["quickjs"], specifier = "~=0.6.7" },
{ name = "discord-py", marker = "extra == 'all-channels'", specifier = ">=2.3" },
{ name = "discord-py", marker = "extra == 'discord'", specifier = ">=2.3" },
{ name = "faster-whisper", marker = "extra == 'stt'", specifier = ">=1.0" },
{ name = "filelock", specifier = ">=3.16" },
{ name = "httpx", specifier = ">=0.28" },
{ name = "langchain", specifier = ">=1.2" },
{ name = "langchain", specifier = ">=1.3" },
{ name = "langchain-anthropic", specifier = ">=1.4" },
{ name = "langchain-google-genai", specifier = ">=4.2" },
{ name = "langchain-mcp-adapters", specifier = ">=0.2" },
@@ -1944,16 +1944,16 @@ wheels = [
[[package]]
name = "langchain"
version = "1.3.1"
version = "1.3.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "langchain-core" },
{ name = "langgraph" },
{ name = "pydantic" },
]
sdist = { url = "https://files.pythonhosted.org/packages/11/e5/6350e77a9e2764eaafcb2d581cbf0b800f53c6bc98fdf5ebc85f3a931ded/langchain-1.3.1.tar.gz", hash = "sha256:bc283c220233230f48b8e50ab1fbf1b688bcb206d933fa448d40a9b143177f62", size = 581329, upload-time = "2026-05-15T18:14:55.368Z" }
sdist = { url = "https://files.pythonhosted.org/packages/d5/d0/c7f9d3d26c0e3f8bb146c6d707ee0fc1d30d8da65a59626e8a580085e929/langchain-1.3.2.tar.gz", hash = "sha256:ffd5f204a46b5fa1a38bf89ba3b45ca0902c02d18fa7d2a2eaeaeb1f5bf19d0a", size = 600598, upload-time = "2026-05-26T18:17:57.715Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/78/11/3d7ed10b535413a07ed5e15682abcb77f3c4204ac49586977a495f9b24e6/langchain-1.3.1-py3-none-any.whl", hash = "sha256:154e9c30c90b391eba4315296f6bf6b6fac6b058ddea4cc771a10470968fe36f", size = 114345, upload-time = "2026-05-15T18:14:53.984Z" },
{ url = "https://files.pythonhosted.org/packages/f8/82/a54edcd1c48163de5642eb10fa2cb58b13a8889c659964f63f0306b58b1e/langchain-1.3.2-py3-none-any.whl", hash = "sha256:900f6b3f4ee08b9ba3cdbe667dbf42525bd6f66a4a07a7f1db26262673e41ed6", size = 121225, upload-time = "2026-05-26T18:17:56.075Z" },
]
[[package]]
@@ -2104,7 +2104,7 @@ wheels = [
[[package]]
name = "langgraph"
version = "1.2.0"
version = "1.2.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "langchain-core" },
@@ -2114,9 +2114,9 @@ dependencies = [
{ name = "pydantic" },
{ name = "xxhash" },
]
sdist = { url = "https://files.pythonhosted.org/packages/58/61/d5d25e783035aa307d289b37e082258a6061c0fb4caa4a284f3bf1e87169/langgraph-1.2.0.tar.gz", hash = "sha256:4a9baaf62afc5d5f63144a50095140a34b9aa9b7cea695d25326d564775348e7", size = 690248, upload-time = "2026-05-12T03:46:39.164Z" }
sdist = { url = "https://files.pythonhosted.org/packages/e6/5a/ffc12434ee8aecab830d58b4d204ddea45073eae7639c963310f671a5bf5/langgraph-1.2.2.tar.gz", hash = "sha256:f54a98458976b3ff0774683867df125fb52d8dbedeb2441d0b0656a51331cee5", size = 695730, upload-time = "2026-05-26T18:07:28.49Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f6/e8/e3304ac0015c2bdb04ad9785e4ed65c788855ce7857ce6104dd2f5d322db/langgraph-1.2.0-py3-none-any.whl", hash = "sha256:03fd5895a8d4b70db1ff63ebc3bacead29dd20cd794a8b1a483e7ec9018f7a65", size = 234262, upload-time = "2026-05-12T03:46:37.971Z" },
{ url = "https://files.pythonhosted.org/packages/42/9b/b08d578bba73e25351152dfd3d6d21e81210a5fff1b6f26e56f33197c8f5/langgraph-1.2.2-py3-none-any.whl", hash = "sha256:0a851bf4ba5939c5474a2fd57e6b439b5315283e254e42943bd392c2d71a5e03", size = 236376, upload-time = "2026-05-26T18:07:26.577Z" },
]
[[package]]