langgraph 1.2.7__tar.gz → 1.2.8__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. {langgraph-1.2.7 → langgraph-1.2.8}/PKG-INFO +1 -1
  2. {langgraph-1.2.7 → langgraph-1.2.8}/langgraph/pregel/_checkpoint.py +5 -0
  3. {langgraph-1.2.7 → langgraph-1.2.8}/langgraph/pregel/main.py +32 -47
  4. {langgraph-1.2.7 → langgraph-1.2.8}/pyproject.toml +1 -1
  5. {langgraph-1.2.7 → langgraph-1.2.8}/tests/test_delta_channel_update_state.py +20 -30
  6. {langgraph-1.2.7 → langgraph-1.2.8}/uv.lock +281 -272
  7. {langgraph-1.2.7 → langgraph-1.2.8}/.gitignore +0 -0
  8. {langgraph-1.2.7 → langgraph-1.2.8}/LICENSE +0 -0
  9. {langgraph-1.2.7 → langgraph-1.2.8}/Makefile +0 -0
  10. {langgraph-1.2.7 → langgraph-1.2.8}/README.md +0 -0
  11. {langgraph-1.2.7 → langgraph-1.2.8}/bench/__init__.py +0 -0
  12. {langgraph-1.2.7 → langgraph-1.2.8}/bench/__main__.py +0 -0
  13. {langgraph-1.2.7 → langgraph-1.2.8}/bench/fanout_to_subgraph.py +0 -0
  14. {langgraph-1.2.7 → langgraph-1.2.8}/bench/pydantic_state.py +0 -0
  15. {langgraph-1.2.7 → langgraph-1.2.8}/bench/react_agent.py +0 -0
  16. {langgraph-1.2.7 → langgraph-1.2.8}/bench/sequential.py +0 -0
  17. {langgraph-1.2.7 → langgraph-1.2.8}/bench/serde_allowlist.py +0 -0
  18. {langgraph-1.2.7 → langgraph-1.2.8}/bench/wide_dict.py +0 -0
  19. {langgraph-1.2.7 → langgraph-1.2.8}/bench/wide_state.py +0 -0
  20. {langgraph-1.2.7 → langgraph-1.2.8}/langgraph/_internal/__init__.py +0 -0
  21. {langgraph-1.2.7 → langgraph-1.2.8}/langgraph/_internal/_cache.py +0 -0
  22. {langgraph-1.2.7 → langgraph-1.2.8}/langgraph/_internal/_config.py +0 -0
  23. {langgraph-1.2.7 → langgraph-1.2.8}/langgraph/_internal/_constants.py +0 -0
  24. {langgraph-1.2.7 → langgraph-1.2.8}/langgraph/_internal/_fields.py +0 -0
  25. {langgraph-1.2.7 → langgraph-1.2.8}/langgraph/_internal/_future.py +0 -0
  26. {langgraph-1.2.7 → langgraph-1.2.8}/langgraph/_internal/_pydantic.py +0 -0
  27. {langgraph-1.2.7 → langgraph-1.2.8}/langgraph/_internal/_queue.py +0 -0
  28. {langgraph-1.2.7 → langgraph-1.2.8}/langgraph/_internal/_replay.py +0 -0
  29. {langgraph-1.2.7 → langgraph-1.2.8}/langgraph/_internal/_retry.py +0 -0
  30. {langgraph-1.2.7 → langgraph-1.2.8}/langgraph/_internal/_runnable.py +0 -0
  31. {langgraph-1.2.7 → langgraph-1.2.8}/langgraph/_internal/_scratchpad.py +0 -0
  32. {langgraph-1.2.7 → langgraph-1.2.8}/langgraph/_internal/_serde.py +0 -0
  33. {langgraph-1.2.7 → langgraph-1.2.8}/langgraph/_internal/_timeout.py +0 -0
  34. {langgraph-1.2.7 → langgraph-1.2.8}/langgraph/_internal/_typing.py +0 -0
  35. {langgraph-1.2.7 → langgraph-1.2.8}/langgraph/callbacks.py +0 -0
  36. {langgraph-1.2.7 → langgraph-1.2.8}/langgraph/channels/__init__.py +0 -0
  37. {langgraph-1.2.7 → langgraph-1.2.8}/langgraph/channels/any_value.py +0 -0
  38. {langgraph-1.2.7 → langgraph-1.2.8}/langgraph/channels/base.py +0 -0
  39. {langgraph-1.2.7 → langgraph-1.2.8}/langgraph/channels/binop.py +0 -0
  40. {langgraph-1.2.7 → langgraph-1.2.8}/langgraph/channels/delta.py +0 -0
  41. {langgraph-1.2.7 → langgraph-1.2.8}/langgraph/channels/ephemeral_value.py +0 -0
  42. {langgraph-1.2.7 → langgraph-1.2.8}/langgraph/channels/last_value.py +0 -0
  43. {langgraph-1.2.7 → langgraph-1.2.8}/langgraph/channels/named_barrier_value.py +0 -0
  44. {langgraph-1.2.7 → langgraph-1.2.8}/langgraph/channels/topic.py +0 -0
  45. {langgraph-1.2.7 → langgraph-1.2.8}/langgraph/channels/untracked_value.py +0 -0
  46. {langgraph-1.2.7 → langgraph-1.2.8}/langgraph/config.py +0 -0
  47. {langgraph-1.2.7 → langgraph-1.2.8}/langgraph/constants.py +0 -0
  48. {langgraph-1.2.7 → langgraph-1.2.8}/langgraph/errors.py +0 -0
  49. {langgraph-1.2.7 → langgraph-1.2.8}/langgraph/func/__init__.py +0 -0
  50. {langgraph-1.2.7 → langgraph-1.2.8}/langgraph/graph/__init__.py +0 -0
  51. {langgraph-1.2.7 → langgraph-1.2.8}/langgraph/graph/_branch.py +0 -0
  52. {langgraph-1.2.7 → langgraph-1.2.8}/langgraph/graph/_node.py +0 -0
  53. {langgraph-1.2.7 → langgraph-1.2.8}/langgraph/graph/message.py +0 -0
  54. {langgraph-1.2.7 → langgraph-1.2.8}/langgraph/graph/state.py +0 -0
  55. {langgraph-1.2.7 → langgraph-1.2.8}/langgraph/graph/ui.py +0 -0
  56. {langgraph-1.2.7 → langgraph-1.2.8}/langgraph/managed/__init__.py +0 -0
  57. {langgraph-1.2.7 → langgraph-1.2.8}/langgraph/managed/base.py +0 -0
  58. {langgraph-1.2.7 → langgraph-1.2.8}/langgraph/managed/is_last_step.py +0 -0
  59. {langgraph-1.2.7 → langgraph-1.2.8}/langgraph/pregel/__init__.py +0 -0
  60. {langgraph-1.2.7 → langgraph-1.2.8}/langgraph/pregel/_algo.py +0 -0
  61. {langgraph-1.2.7 → langgraph-1.2.8}/langgraph/pregel/_call.py +0 -0
  62. {langgraph-1.2.7 → langgraph-1.2.8}/langgraph/pregel/_config.py +0 -0
  63. {langgraph-1.2.7 → langgraph-1.2.8}/langgraph/pregel/_draw.py +0 -0
  64. {langgraph-1.2.7 → langgraph-1.2.8}/langgraph/pregel/_executor.py +0 -0
  65. {langgraph-1.2.7 → langgraph-1.2.8}/langgraph/pregel/_io.py +0 -0
  66. {langgraph-1.2.7 → langgraph-1.2.8}/langgraph/pregel/_log.py +0 -0
  67. {langgraph-1.2.7 → langgraph-1.2.8}/langgraph/pregel/_loop.py +0 -0
  68. {langgraph-1.2.7 → langgraph-1.2.8}/langgraph/pregel/_messages.py +0 -0
  69. {langgraph-1.2.7 → langgraph-1.2.8}/langgraph/pregel/_read.py +0 -0
  70. {langgraph-1.2.7 → langgraph-1.2.8}/langgraph/pregel/_remote_run_stream.py +0 -0
  71. {langgraph-1.2.7 → langgraph-1.2.8}/langgraph/pregel/_retry.py +0 -0
  72. {langgraph-1.2.7 → langgraph-1.2.8}/langgraph/pregel/_runner.py +0 -0
  73. {langgraph-1.2.7 → langgraph-1.2.8}/langgraph/pregel/_tools.py +0 -0
  74. {langgraph-1.2.7 → langgraph-1.2.8}/langgraph/pregel/_utils.py +0 -0
  75. {langgraph-1.2.7 → langgraph-1.2.8}/langgraph/pregel/_validate.py +0 -0
  76. {langgraph-1.2.7 → langgraph-1.2.8}/langgraph/pregel/_write.py +0 -0
  77. {langgraph-1.2.7 → langgraph-1.2.8}/langgraph/pregel/debug.py +0 -0
  78. {langgraph-1.2.7 → langgraph-1.2.8}/langgraph/pregel/protocol.py +0 -0
  79. {langgraph-1.2.7 → langgraph-1.2.8}/langgraph/pregel/remote.py +0 -0
  80. {langgraph-1.2.7 → langgraph-1.2.8}/langgraph/pregel/types.py +0 -0
  81. {langgraph-1.2.7 → langgraph-1.2.8}/langgraph/py.typed +0 -0
  82. {langgraph-1.2.7 → langgraph-1.2.8}/langgraph/runtime.py +0 -0
  83. {langgraph-1.2.7 → langgraph-1.2.8}/langgraph/stream/__init__.py +0 -0
  84. {langgraph-1.2.7 → langgraph-1.2.8}/langgraph/stream/_convert.py +0 -0
  85. {langgraph-1.2.7 → langgraph-1.2.8}/langgraph/stream/_mux.py +0 -0
  86. {langgraph-1.2.7 → langgraph-1.2.8}/langgraph/stream/_types.py +0 -0
  87. {langgraph-1.2.7 → langgraph-1.2.8}/langgraph/stream/run_stream.py +0 -0
  88. {langgraph-1.2.7 → langgraph-1.2.8}/langgraph/stream/stream_channel.py +0 -0
  89. {langgraph-1.2.7 → langgraph-1.2.8}/langgraph/stream/transformers.py +0 -0
  90. {langgraph-1.2.7 → langgraph-1.2.8}/langgraph/types.py +0 -0
  91. {langgraph-1.2.7 → langgraph-1.2.8}/langgraph/typing.py +0 -0
  92. {langgraph-1.2.7 → langgraph-1.2.8}/langgraph/utils/__init__.py +0 -0
  93. {langgraph-1.2.7 → langgraph-1.2.8}/langgraph/utils/config.py +0 -0
  94. {langgraph-1.2.7 → langgraph-1.2.8}/langgraph/utils/runnable.py +0 -0
  95. {langgraph-1.2.7 → langgraph-1.2.8}/langgraph/version.py +0 -0
  96. {langgraph-1.2.7 → langgraph-1.2.8}/langgraph/warnings.py +0 -0
  97. {langgraph-1.2.7 → langgraph-1.2.8}/tests/__init__.py +0 -0
  98. {langgraph-1.2.7 → langgraph-1.2.8}/tests/__snapshots__/test_large_cases.ambr +0 -0
  99. {langgraph-1.2.7 → langgraph-1.2.8}/tests/__snapshots__/test_pregel.ambr +0 -0
  100. {langgraph-1.2.7 → langgraph-1.2.8}/tests/__snapshots__/test_pregel_async.ambr +0 -0
  101. {langgraph-1.2.7 → langgraph-1.2.8}/tests/agents.py +0 -0
  102. {langgraph-1.2.7 → langgraph-1.2.8}/tests/any_int.py +0 -0
  103. {langgraph-1.2.7 → langgraph-1.2.8}/tests/any_str.py +0 -0
  104. {langgraph-1.2.7 → langgraph-1.2.8}/tests/compose-postgres.yml +0 -0
  105. {langgraph-1.2.7 → langgraph-1.2.8}/tests/compose-redis.yml +0 -0
  106. {langgraph-1.2.7 → langgraph-1.2.8}/tests/conftest.py +0 -0
  107. {langgraph-1.2.7 → langgraph-1.2.8}/tests/conftest_checkpointer.py +0 -0
  108. {langgraph-1.2.7 → langgraph-1.2.8}/tests/conftest_store.py +0 -0
  109. {langgraph-1.2.7 → langgraph-1.2.8}/tests/example_app/example_graph.py +0 -0
  110. {langgraph-1.2.7 → langgraph-1.2.8}/tests/example_app/langgraph.json +0 -0
  111. {langgraph-1.2.7 → langgraph-1.2.8}/tests/example_app/requirements.txt +0 -0
  112. {langgraph-1.2.7 → langgraph-1.2.8}/tests/fake_chat.py +0 -0
  113. {langgraph-1.2.7 → langgraph-1.2.8}/tests/fake_tracer.py +0 -0
  114. {langgraph-1.2.7 → langgraph-1.2.8}/tests/memory_assert.py +0 -0
  115. {langgraph-1.2.7 → langgraph-1.2.8}/tests/messages.py +0 -0
  116. {langgraph-1.2.7 → langgraph-1.2.8}/tests/test_algo.py +0 -0
  117. {langgraph-1.2.7 → langgraph-1.2.8}/tests/test_channels.py +0 -0
  118. {langgraph-1.2.7 → langgraph-1.2.8}/tests/test_checkpoint_migration.py +0 -0
  119. {langgraph-1.2.7 → langgraph-1.2.8}/tests/test_config_async.py +0 -0
  120. {langgraph-1.2.7 → langgraph-1.2.8}/tests/test_delta_channel_benchmark.py +0 -0
  121. {langgraph-1.2.7 → langgraph-1.2.8}/tests/test_delta_channel_exit_mode.py +0 -0
  122. {langgraph-1.2.7 → langgraph-1.2.8}/tests/test_delta_channel_id_stability.py +0 -0
  123. {langgraph-1.2.7 → langgraph-1.2.8}/tests/test_delta_channel_migration.py +0 -0
  124. {langgraph-1.2.7 → langgraph-1.2.8}/tests/test_delta_channel_supersteps_bound.py +0 -0
  125. {langgraph-1.2.7 → langgraph-1.2.8}/tests/test_deprecation.py +0 -0
  126. {langgraph-1.2.7 → langgraph-1.2.8}/tests/test_graph_callbacks.py +0 -0
  127. {langgraph-1.2.7 → langgraph-1.2.8}/tests/test_interleave_arrival_order.py +0 -0
  128. {langgraph-1.2.7 → langgraph-1.2.8}/tests/test_interrupt_migration.py +0 -0
  129. {langgraph-1.2.7 → langgraph-1.2.8}/tests/test_interruption.py +0 -0
  130. {langgraph-1.2.7 → langgraph-1.2.8}/tests/test_large_cases.py +0 -0
  131. {langgraph-1.2.7 → langgraph-1.2.8}/tests/test_large_cases_async.py +0 -0
  132. {langgraph-1.2.7 → langgraph-1.2.8}/tests/test_managed_values.py +0 -0
  133. {langgraph-1.2.7 → langgraph-1.2.8}/tests/test_messages_state.py +0 -0
  134. {langgraph-1.2.7 → langgraph-1.2.8}/tests/test_parent_command.py +0 -0
  135. {langgraph-1.2.7 → langgraph-1.2.8}/tests/test_parent_command_async.py +0 -0
  136. {langgraph-1.2.7 → langgraph-1.2.8}/tests/test_pregel.py +0 -0
  137. {langgraph-1.2.7 → langgraph-1.2.8}/tests/test_pregel_async.py +0 -0
  138. {langgraph-1.2.7 → langgraph-1.2.8}/tests/test_pregel_debug.py +0 -0
  139. {langgraph-1.2.7 → langgraph-1.2.8}/tests/test_pregel_stream_events_v3.py +0 -0
  140. {langgraph-1.2.7 → langgraph-1.2.8}/tests/test_pydantic.py +0 -0
  141. {langgraph-1.2.7 → langgraph-1.2.8}/tests/test_remote_graph.py +0 -0
  142. {langgraph-1.2.7 → langgraph-1.2.8}/tests/test_remote_graph_v3.py +0 -0
  143. {langgraph-1.2.7 → langgraph-1.2.8}/tests/test_retry.py +0 -0
  144. {langgraph-1.2.7 → langgraph-1.2.8}/tests/test_runnable.py +0 -0
  145. {langgraph-1.2.7 → langgraph-1.2.8}/tests/test_runtime.py +0 -0
  146. {langgraph-1.2.7 → langgraph-1.2.8}/tests/test_serde_allowlist.py +0 -0
  147. {langgraph-1.2.7 → langgraph-1.2.8}/tests/test_state.py +0 -0
  148. {langgraph-1.2.7 → langgraph-1.2.8}/tests/test_stream_before_builtins.py +0 -0
  149. {langgraph-1.2.7 → langgraph-1.2.8}/tests/test_stream_data_transformers.py +0 -0
  150. {langgraph-1.2.7 → langgraph-1.2.8}/tests/test_stream_events_v3.py +0 -0
  151. {langgraph-1.2.7 → langgraph-1.2.8}/tests/test_stream_events_v3_e2e.py +0 -0
  152. {langgraph-1.2.7 → langgraph-1.2.8}/tests/test_stream_events_v3_kwarg_forwarding.py +0 -0
  153. {langgraph-1.2.7 → langgraph-1.2.8}/tests/test_stream_lifecycle_transformer.py +0 -0
  154. {langgraph-1.2.7 → langgraph-1.2.8}/tests/test_stream_messages_transformer.py +0 -0
  155. {langgraph-1.2.7 → langgraph-1.2.8}/tests/test_stream_subgraph_transformer.py +0 -0
  156. {langgraph-1.2.7 → langgraph-1.2.8}/tests/test_subgraph_persistence.py +0 -0
  157. {langgraph-1.2.7 → langgraph-1.2.8}/tests/test_subgraph_persistence_async.py +0 -0
  158. {langgraph-1.2.7 → langgraph-1.2.8}/tests/test_time_travel.py +0 -0
  159. {langgraph-1.2.7 → langgraph-1.2.8}/tests/test_time_travel_async.py +0 -0
  160. {langgraph-1.2.7 → langgraph-1.2.8}/tests/test_tool_stream_handler.py +0 -0
  161. {langgraph-1.2.7 → langgraph-1.2.8}/tests/test_tracing_interops.py +0 -0
  162. {langgraph-1.2.7 → langgraph-1.2.8}/tests/test_type_checking.py +0 -0
  163. {langgraph-1.2.7 → langgraph-1.2.8}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: langgraph
3
- Version: 1.2.7
3
+ Version: 1.2.8
4
4
  Summary: Building stateful, multi-actor applications with LLMs
5
5
  Project-URL: Homepage, https://docs.langchain.com/oss/python/langgraph/overview
6
6
  Project-URL: Documentation, https://reference.langchain.com/python/langgraph/
@@ -102,6 +102,11 @@ def create_checkpoint(
102
102
  continue
103
103
  ch = channels[k]
104
104
  if k in channels_to_snapshot:
105
+ # Callers force a full snapshot blob here: exit mode when a
106
+ # delta channel reaches its snapshot cadence, and update_state
107
+ # on a fresh thread (no ancestor to replay writes from). The
108
+ # manual version-bump below only applies to the exit-mode case.
109
+ #
105
110
  # In exit mode, the snapshot decision is deferred to exit
106
111
  # time (intermediate steps have do_checkpoint=False). The
107
112
  # channel's count may have reached snapshot_frequency over
@@ -1997,33 +1997,11 @@ class Pregel(
1997
1997
  ),
1998
1998
  )
1999
1999
  # save task writes
2000
- has_delta_writes = any(
2001
- isinstance(channels.get(c), DeltaChannel)
2002
- for task in run_tasks
2003
- for c, _ in task.writes
2004
- )
2005
- should_put_writes = saved is not None or has_delta_writes
2006
-
2007
- if saved is None and has_delta_writes:
2008
- # If there is no previous checkpoint, we need to create a stub checkpoint
2009
- # so the first delta writes has a parent to anchor under.
2010
- # This is the model of DeltaChannel.
2011
- stub = empty_checkpoint()
2012
- checkpoint_config = checkpointer.put(
2013
- patch_configurable(
2014
- checkpoint_config, {CONFIG_KEY_CHECKPOINT_ID: None}
2015
- ),
2016
- stub,
2017
- {"source": "update", "step": -1, "parents": {}},
2018
- {},
2019
- )
2020
-
2021
2000
  for task_id, task in zip(run_task_ids, run_tasks):
2022
2001
  # channel writes are saved to current checkpoint
2023
2002
  channel_writes = [w for w in task.writes if w[0] != PUSH]
2024
- if should_put_writes and channel_writes:
2003
+ if saved and channel_writes:
2025
2004
  checkpointer.put_writes(checkpoint_config, channel_writes, task_id)
2026
-
2027
2005
  # apply to checkpoint and save
2028
2006
  apply_writes(
2029
2007
  checkpoint,
@@ -2032,7 +2010,21 @@ class Pregel(
2032
2010
  checkpointer.get_next_version,
2033
2011
  self.trigger_to_nodes,
2034
2012
  )
2035
- checkpoint = create_checkpoint(checkpoint, channels, step + 1)
2013
+ # On a fresh thread there is no ancestor to replay DeltaChannel
2014
+ # writes from, so force a self-contained snapshot in the first
2015
+ # checkpoint instead of relying on ancestor write-replay.
2016
+ delta_snapshot = (
2017
+ {
2018
+ k
2019
+ for k, ch in channels.items()
2020
+ if isinstance(ch, DeltaChannel) and ch.is_available()
2021
+ }
2022
+ if saved is None
2023
+ else None
2024
+ )
2025
+ checkpoint = create_checkpoint(
2026
+ checkpoint, channels, step + 1, channels_to_snapshot=delta_snapshot
2027
+ )
2036
2028
  next_config = checkpointer.put(
2037
2029
  checkpoint_config,
2038
2030
  checkpoint,
@@ -2464,31 +2456,10 @@ class Pregel(
2464
2456
  ),
2465
2457
  )
2466
2458
  # save task writes
2467
- has_delta_writes = any(
2468
- isinstance(channels.get(c), DeltaChannel)
2469
- for task in run_tasks
2470
- for c, _ in task.writes
2471
- )
2472
- should_put_writes = saved is not None or has_delta_writes
2473
-
2474
- if saved is None and has_delta_writes:
2475
- # If there is no previous checkpoint, we need to create a stub checkpoint
2476
- # so the first delta writes has a parent to anchor under.
2477
- # This is the model of DeltaChannel.
2478
- stub = empty_checkpoint()
2479
- checkpoint_config = await checkpointer.aput(
2480
- patch_configurable(
2481
- checkpoint_config, {CONFIG_KEY_CHECKPOINT_ID: None}
2482
- ),
2483
- stub,
2484
- {"source": "update", "step": -1, "parents": {}},
2485
- {},
2486
- )
2487
-
2488
2459
  for task_id, task in zip(run_task_ids, run_tasks):
2489
2460
  # channel writes are saved to current checkpoint
2490
2461
  channel_writes = [w for w in task.writes if w[0] != PUSH]
2491
- if should_put_writes and channel_writes:
2462
+ if saved and channel_writes:
2492
2463
  await checkpointer.aput_writes(
2493
2464
  checkpoint_config, channel_writes, task_id
2494
2465
  )
@@ -2500,7 +2471,21 @@ class Pregel(
2500
2471
  checkpointer.get_next_version,
2501
2472
  self.trigger_to_nodes,
2502
2473
  )
2503
- checkpoint = create_checkpoint(checkpoint, channels, step + 1)
2474
+ # On a fresh thread there is no ancestor to replay DeltaChannel
2475
+ # writes from, so force a self-contained snapshot in the first
2476
+ # checkpoint instead of relying on ancestor write-replay.
2477
+ delta_snapshot = (
2478
+ {
2479
+ k
2480
+ for k, ch in channels.items()
2481
+ if isinstance(ch, DeltaChannel) and ch.is_available()
2482
+ }
2483
+ if saved is None
2484
+ else None
2485
+ )
2486
+ checkpoint = create_checkpoint(
2487
+ checkpoint, channels, step + 1, channels_to_snapshot=delta_snapshot
2488
+ )
2504
2489
  # save checkpoint, after applying writes
2505
2490
  next_config = await checkpointer.aput(
2506
2491
  checkpoint_config,
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "langgraph"
7
- version = "1.2.7"
7
+ version = "1.2.8"
8
8
  description = "Building stateful, multi-actor applications with LLMs"
9
9
  authors = []
10
10
  requires-python = ">=3.10"
@@ -2,10 +2,13 @@
2
2
 
3
3
  Originally a regression suite for deepagents#3774 — `update_state` on a *fresh*
4
4
  thread silently dropped the first write to a `DeltaChannel`-backed channel
5
- because channel writes were only persisted when a previous checkpoint existed.
6
- Fixed by lazily persisting an empty stub checkpoint on a fresh thread so the
7
- first write has a parent to anchor under (mirrors the exit-mode lazy-stub
8
- pattern in `_loop._put_exit_delta_writes`).
5
+ because channel writes were only persisted when a previous checkpoint existed
6
+ and no snapshot was written either, so the checkpoint reconstructed to empty.
7
+
8
+ Fixed by forcing a self-contained `_DeltaSnapshot` blob into the first
9
+ checkpoint on a fresh thread (`saved is None`), so the value is stored inline
10
+ and no ancestor write-replay is required. This keeps the read/replay path
11
+ untouched.
9
12
 
10
13
  Coverage:
11
14
 
@@ -13,8 +16,8 @@ Coverage:
13
16
  * non-fresh thread: `update_state` after `invoke`, after another `update_state`,
14
17
  and `bulk_update_state` with multiple per-superstep updates
15
18
  * update-by-id end-to-end via `update_state` (DeltaChannel reducer semantics)
16
- * state-history chain shape on a fresh thread (lazy stub + update checkpoint
17
- with correct parent linking)
19
+ * state-history chain shape on a fresh thread (single self-contained update
20
+ checkpoint with the snapshot inline and no parent)
18
21
  """
19
22
 
20
23
  from typing import Annotated, Any
@@ -94,7 +97,7 @@ async def test_aupdate_state_fresh_thread_delta_channel() -> None:
94
97
 
95
98
  def test_update_state_after_invoke_delta_channel() -> None:
96
99
  """The non-fresh-thread path was already working before the fix; pin it
97
- down so the lazy-stub change for fresh threads doesn't regress it."""
100
+ down so the forced-snapshot change for fresh threads doesn't regress it."""
98
101
  saver = InMemorySaver()
99
102
  graph = _build_graph(saver)
100
103
  config = {"configurable": {"thread_id": "after-invoke-sync"}}
@@ -133,9 +136,9 @@ async def test_aupdate_state_after_invoke_delta_channel() -> None:
133
136
 
134
137
 
135
138
  def test_consecutive_update_states_delta_channel() -> None:
136
- """First update_state lazily persists a stub; the second sees a real
137
- parent (`saved is not None`) and takes the original write path. Both
138
- messages must round-trip in chronological order."""
139
+ """First update_state forces a self-contained snapshot seed; the second
140
+ sees a real parent (`saved is not None`) and anchors its writes under that
141
+ seed. Both messages must round-trip in chronological order."""
139
142
  saver = InMemorySaver()
140
143
  graph = _build_graph(saver)
141
144
  config = {"configurable": {"thread_id": "consecutive-sync"}}
@@ -252,14 +255,14 @@ def test_bulk_update_state_multi_task_per_superstep_delta_channel() -> None:
252
255
 
253
256
 
254
257
  # ---------------------------------------------------------------------------
255
- # Public-API observation of the lazy-stub mechanism
258
+ # Public-API observation of the forced-snapshot mechanism
256
259
  # ---------------------------------------------------------------------------
257
260
 
258
261
 
259
262
  def test_state_history_chain_after_fresh_update_state_delta_channel() -> None:
260
- """A fresh-thread `update_state` should produce two checkpoints visible
261
- via `get_state_history`: a stub (step=-1, no parent) and the update
262
- (step=0, parent=stub). Both attributed `source='update'`."""
263
+ """A fresh-thread `update_state` should produce a single self-contained
264
+ checkpoint visible via `get_state_history`: step=0, `source='update'`,
265
+ no parent, with the DeltaChannel value snapshotted inline."""
263
266
  saver = InMemorySaver()
264
267
  graph = _build_graph(saver)
265
268
  config = {"configurable": {"thread_id": "history-chain"}}
@@ -270,25 +273,12 @@ def test_state_history_chain_after_fresh_update_state_delta_channel() -> None:
270
273
  as_node="model",
271
274
  )
272
275
 
273
- # Newest first per `get_state_history` ordering.
274
276
  history = list(graph.get_state_history(config))
275
- assert len(history) == 2
276
-
277
- update_snapshot, stub_snapshot = history
277
+ assert len(history) == 1
278
278
 
279
+ (update_snapshot,) = history
279
280
  assert update_snapshot.metadata is not None
280
281
  assert update_snapshot.metadata["source"] == "update"
281
282
  assert update_snapshot.metadata["step"] == 0
283
+ assert update_snapshot.parent_config is None
282
284
  assert [m.content for m in update_snapshot.values["messages"]] == ["hello"]
283
-
284
- assert stub_snapshot.metadata is not None
285
- assert stub_snapshot.metadata["source"] == "update"
286
- assert stub_snapshot.metadata["step"] == -1
287
- assert stub_snapshot.parent_config is None
288
-
289
- # The update checkpoint's parent is the stub.
290
- assert update_snapshot.parent_config is not None
291
- assert (
292
- update_snapshot.parent_config["configurable"]["checkpoint_id"]
293
- == stub_snapshot.config["configurable"]["checkpoint_id"]
294
- )