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:
@@ -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
@@ -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
@@ -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()
|
||||
|
||||
@@ -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]]
|
||||
|
||||
Reference in New Issue
Block a user