soothe-deepagents 0.7.16__py3-none-any.whl

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 (57) hide show
  1. soothe_deepagents/__init__.py +50 -0
  2. soothe_deepagents/_api/__init__.py +5 -0
  3. soothe_deepagents/_api/deprecation.py +131 -0
  4. soothe_deepagents/_excluded_middleware.py +225 -0
  5. soothe_deepagents/_messages_reducer.py +90 -0
  6. soothe_deepagents/_models.py +211 -0
  7. soothe_deepagents/_tools.py +65 -0
  8. soothe_deepagents/_version.py +7 -0
  9. soothe_deepagents/backends/__init__.py +28 -0
  10. soothe_deepagents/backends/composite.py +861 -0
  11. soothe_deepagents/backends/context_hub.py +374 -0
  12. soothe_deepagents/backends/filesystem.py +1209 -0
  13. soothe_deepagents/backends/langsmith.py +279 -0
  14. soothe_deepagents/backends/local_shell.py +387 -0
  15. soothe_deepagents/backends/protocol.py +992 -0
  16. soothe_deepagents/backends/sandbox.py +1363 -0
  17. soothe_deepagents/backends/state.py +408 -0
  18. soothe_deepagents/backends/store.py +882 -0
  19. soothe_deepagents/backends/utils.py +917 -0
  20. soothe_deepagents/graph.py +1033 -0
  21. soothe_deepagents/middleware/__init__.py +104 -0
  22. soothe_deepagents/middleware/_fs_interrupt.py +183 -0
  23. soothe_deepagents/middleware/_message_eviction.py +162 -0
  24. soothe_deepagents/middleware/_overflow_clip.py +206 -0
  25. soothe_deepagents/middleware/_state.py +30 -0
  26. soothe_deepagents/middleware/_tool_exclusion.py +65 -0
  27. soothe_deepagents/middleware/_utils.py +23 -0
  28. soothe_deepagents/middleware/_video.py +404 -0
  29. soothe_deepagents/middleware/async_subagents.py +956 -0
  30. soothe_deepagents/middleware/filesystem.py +3102 -0
  31. soothe_deepagents/middleware/memory.py +441 -0
  32. soothe_deepagents/middleware/patch_tool_calls.py +46 -0
  33. soothe_deepagents/middleware/permissions.py +5 -0
  34. soothe_deepagents/middleware/rubric.py +805 -0
  35. soothe_deepagents/middleware/skills.py +1085 -0
  36. soothe_deepagents/middleware/subagents.py +871 -0
  37. soothe_deepagents/middleware/summarization.py +2190 -0
  38. soothe_deepagents/profiles/__init__.py +61 -0
  39. soothe_deepagents/profiles/_builtin_profiles.py +236 -0
  40. soothe_deepagents/profiles/_keys.py +42 -0
  41. soothe_deepagents/profiles/harness/__init__.py +22 -0
  42. soothe_deepagents/profiles/harness/_anthropic_haiku_4_5.py +52 -0
  43. soothe_deepagents/profiles/harness/_anthropic_opus_4_7.py +56 -0
  44. soothe_deepagents/profiles/harness/_anthropic_sonnet_4_6.py +52 -0
  45. soothe_deepagents/profiles/harness/_nvidia_nemotron_3_ultra.py +1811 -0
  46. soothe_deepagents/profiles/harness/_openai_codex.py +68 -0
  47. soothe_deepagents/profiles/harness/harness_profiles.py +1325 -0
  48. soothe_deepagents/profiles/provider/__init__.py +15 -0
  49. soothe_deepagents/profiles/provider/_nvidia.py +50 -0
  50. soothe_deepagents/profiles/provider/_openai.py +24 -0
  51. soothe_deepagents/profiles/provider/_openrouter.py +130 -0
  52. soothe_deepagents/profiles/provider/provider_profiles.py +455 -0
  53. soothe_deepagents/py.typed +0 -0
  54. soothe_deepagents-0.7.16.dist-info/METADATA +140 -0
  55. soothe_deepagents-0.7.16.dist-info/RECORD +57 -0
  56. soothe_deepagents-0.7.16.dist-info/WHEEL +5 -0
  57. soothe_deepagents-0.7.16.dist-info/top_level.txt +1 -0
@@ -0,0 +1,50 @@
1
+ """Deep Agents package."""
2
+
3
+ from soothe_deepagents._version import __version__
4
+ from soothe_deepagents.graph import (
5
+ DeepAgentState,
6
+ SystemPromptConfig,
7
+ create_deep_agent,
8
+ )
9
+ from soothe_deepagents.middleware.async_subagents import AsyncSubAgent, AsyncSubAgentMiddleware
10
+ from soothe_deepagents.middleware.filesystem import FilesystemMiddleware, FilesystemPermission, FsToolName
11
+ from soothe_deepagents.middleware.memory import MemoryMiddleware
12
+ from soothe_deepagents.middleware.rubric import RubricMiddleware
13
+ from soothe_deepagents.middleware.subagents import (
14
+ CompiledSubAgent,
15
+ SubAgent,
16
+ SubAgentMiddleware,
17
+ )
18
+ from soothe_deepagents.profiles.harness.harness_profiles import (
19
+ GeneralPurposeSubagentProfile,
20
+ HarnessProfile,
21
+ HarnessProfileConfig,
22
+ register_harness_profile,
23
+ )
24
+ from soothe_deepagents.profiles.provider.provider_profiles import (
25
+ ProviderProfile,
26
+ register_provider_profile,
27
+ )
28
+
29
+ __all__ = [
30
+ "AsyncSubAgent",
31
+ "AsyncSubAgentMiddleware",
32
+ "CompiledSubAgent",
33
+ "DeepAgentState",
34
+ "FilesystemMiddleware",
35
+ "FilesystemPermission",
36
+ "FsToolName",
37
+ "GeneralPurposeSubagentProfile",
38
+ "HarnessProfile",
39
+ "HarnessProfileConfig",
40
+ "MemoryMiddleware",
41
+ "ProviderProfile",
42
+ "RubricMiddleware",
43
+ "SubAgent",
44
+ "SubAgentMiddleware",
45
+ "SystemPromptConfig",
46
+ "__version__",
47
+ "create_deep_agent",
48
+ "register_harness_profile",
49
+ "register_provider_profile",
50
+ ]
@@ -0,0 +1,5 @@
1
+ """Internal helpers for `soothe_deepagents`.
2
+
3
+ Modules under this package are private. Their API is allowed to change between
4
+ minor releases without deprecation.
5
+ """
@@ -0,0 +1,131 @@
1
+ """Adapter for `langchain_core`'s private deprecation helpers.
2
+
3
+ Centralizes the import surface so an upstream rename or move is a one-file
4
+ change.
5
+
6
+ Re-exports:
7
+ - `deprecated`: decorator for callables, classes, and properties.
8
+ - `warn_deprecated`: helper for parameter/value-level deprecations where the
9
+ callable itself isn't being deprecated. Wraps the upstream helper to
10
+ accept a `stacklevel` argument (the upstream version hardcodes
11
+ `stacklevel=4`, which mis-attributes warnings emitted directly from a
12
+ deprecated method body).
13
+ - `suppress_langchain_deprecation_warning`: context manager that silences
14
+ emissions from this module's helpers (use sparingly — it is type-wide).
15
+ - `LangChainDeprecationWarning`: warning class emitted by the helpers above
16
+ (subclass of `DeprecationWarning`).
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import warnings
22
+
23
+ from langchain_core._api.deprecation import (
24
+ LangChainDeprecationWarning,
25
+ deprecated,
26
+ suppress_langchain_deprecation_warning,
27
+ warn_deprecated as _lc_warn_deprecated,
28
+ )
29
+
30
+ __all__ = [
31
+ "LangChainDeprecationWarning",
32
+ "deprecated",
33
+ "reset_deprecation_dedupe",
34
+ "suppress_langchain_deprecation_warning",
35
+ "warn_deprecated",
36
+ ]
37
+
38
+
39
+ def warn_deprecated(
40
+ since: str,
41
+ *,
42
+ message: str = "",
43
+ name: str = "",
44
+ alternative: str = "",
45
+ alternative_import: str = "",
46
+ pending: bool = False,
47
+ obj_type: str = "",
48
+ addendum: str = "",
49
+ removal: str = "",
50
+ package: str = "",
51
+ stacklevel: int = 2,
52
+ ) -> None:
53
+ """Emit a deprecation warning with caller-controlled stack attribution.
54
+
55
+ `langchain_core.warn_deprecated` formats a standard message but hardcodes
56
+ `stacklevel=4` in its internal `warnings.warn` call. That value targets a
57
+ decorator-wrapped frame layout; when called directly from a deprecated
58
+ method's body the warning is attributed one frame too high (above the
59
+ user's call site). This wrapper captures the formatted upstream warning
60
+ and re-emits it with an explicit `stacklevel`, so the warning points at
61
+ the user's call site.
62
+
63
+ Args:
64
+ since: Release at which this API became deprecated.
65
+ message: Override the default deprecation message. See upstream
66
+ `langchain_core.warn_deprecated` for supported format specifiers.
67
+ name: Name of the deprecated object.
68
+ alternative: Alternative API the user may use instead.
69
+ alternative_import: Alternative import path the user may use instead.
70
+ pending: If `True`, uses a `PendingDeprecationWarning` instead of a
71
+ `DeprecationWarning`. Cannot be combined with `removal`.
72
+ obj_type: Object type label (e.g., `"function"`, `"class"`).
73
+ addendum: Additional text appended to the final message.
74
+ removal: Expected removal version. Cannot be combined with `pending`.
75
+ package: Package name attribution for the deprecation message.
76
+ stacklevel: Frames above this call to attribute the warning to,
77
+ using the same convention as `warnings.warn` (`1` = this call,
78
+ `2` = the caller of the method body that invoked us, etc.).
79
+ """
80
+ with warnings.catch_warnings(record=True) as captured:
81
+ warnings.simplefilter("always")
82
+ _lc_warn_deprecated(
83
+ since,
84
+ message=message,
85
+ name=name,
86
+ alternative=alternative,
87
+ alternative_import=alternative_import,
88
+ pending=pending,
89
+ obj_type=obj_type,
90
+ addendum=addendum,
91
+ removal=removal,
92
+ package=package,
93
+ )
94
+ if not captured:
95
+ return
96
+ record = captured[0]
97
+ warnings.warn(record.message, category=record.category, stacklevel=stacklevel + 1)
98
+
99
+
100
+ def reset_deprecation_dedupe(*targets: object) -> None:
101
+ """Reset the `@deprecated` decorator's dedupe flag for testing.
102
+
103
+ The langchain_core `@deprecated` decorator emits each warning at most once
104
+ per process via a closure-bound `warned` flag. Tests that assert per-call
105
+ emission must reset that flag between cases — otherwise the assertions
106
+ become reorder-sensitive (notably under `pytest -n auto`).
107
+
108
+ Accepts decorated functions, methods, and `property` objects (in which
109
+ case the `fget` closure is reset). Targets without the expected `warned`
110
+ freevar are silently skipped, so passing non-decorated callables is safe.
111
+
112
+ Args:
113
+ *targets: Decorated callables (or properties wrapping them) to reset.
114
+ """
115
+ for target in targets:
116
+ fn = target.fget if isinstance(target, property) else target
117
+ code = getattr(fn, "__code__", None)
118
+ closure = getattr(fn, "__closure__", None)
119
+ if code is None or closure is None:
120
+ continue
121
+ try:
122
+ index = code.co_freevars.index("warned")
123
+ except ValueError:
124
+ continue
125
+ cell = closure[index]
126
+ try:
127
+ current = cell.cell_contents
128
+ except ValueError: # empty cell
129
+ continue
130
+ if isinstance(current, bool):
131
+ cell.cell_contents = False
@@ -0,0 +1,225 @@
1
+ """Filtering helpers for `HarnessProfile.excluded_middleware`.
2
+
3
+ These functions validate, apply, and audit exclusions against assembled
4
+ middleware stacks. The set of *required scaffolding* — classes/names that
5
+ must remain in the stack for the agent to function — is owned by
6
+ `soothe_deepagents.graph` and threaded through as parameters so that policy stays
7
+ next to `create_deep_agent`.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import logging
13
+ from typing import TYPE_CHECKING, Any
14
+
15
+ if TYPE_CHECKING:
16
+ from langchain.agents.middleware.types import AgentMiddleware
17
+
18
+ from soothe_deepagents.profiles import HarnessProfile
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ def _validate_excluded_middleware_config(
24
+ profile: HarnessProfile,
25
+ *,
26
+ required_classes: frozenset[type[AgentMiddleware[Any, Any, Any]]],
27
+ required_names: frozenset[str],
28
+ ) -> None:
29
+ """Validate stack-independent guards on `profile.excluded_middleware`.
30
+
31
+ Rejects required-scaffolding entries (class or the equivalent `.name`
32
+ string). Grammar-level checks (empty strings, multi-colon, underscore
33
+ prefix on plain names) already fire at `HarnessProfile` construction;
34
+ this function focuses on the assembly-time invariant that scaffolding
35
+ middleware must remain present for the agent to function.
36
+
37
+ Args:
38
+ profile: Profile whose `excluded_middleware` is validated.
39
+ required_classes: Scaffolding classes that must not be excluded.
40
+ required_names: Scaffolding `.name` values that must not be excluded.
41
+
42
+ Raises:
43
+ ValueError: If any entry is required scaffolding.
44
+ """
45
+ excluded = profile.excluded_middleware
46
+ if not excluded:
47
+ return
48
+
49
+ excluded_classes: set[type[AgentMiddleware[Any, Any, Any]]] = set()
50
+ excluded_names: set[str] = set()
51
+ for entry in excluded:
52
+ if isinstance(entry, type):
53
+ excluded_classes.add(entry)
54
+ else:
55
+ excluded_names.add(entry)
56
+
57
+ forbidden_classes = excluded_classes & required_classes
58
+ forbidden_names = excluded_names & required_names
59
+ if forbidden_classes or forbidden_names:
60
+ # Lazy import: harness_profiles owns the per-class guidance text.
61
+ from soothe_deepagents.profiles.harness.harness_profiles import _format_scaffolding_rejection # noqa: PLC0415
62
+
63
+ labels = [cls.__name__ for cls in forbidden_classes] + [f"{name!r} (string)" for name in forbidden_names]
64
+ raise ValueError(_format_scaffolding_rejection(labels))
65
+
66
+
67
+ def _raise_on_name_collisions(
68
+ name_matched_types: dict[str, set[type[AgentMiddleware[Any, Any, Any]]]],
69
+ ) -> None:
70
+ """Raise `ValueError` if any string exclusion matched multiple distinct classes.
71
+
72
+ A string entry that drops instances of more than one concrete class is
73
+ almost always a surprise — e.g. a user middleware whose `.name`
74
+ accidentally collides with a built-in alias. Force the caller to use a
75
+ class-form exclusion via the runtime `HarnessProfile` to disambiguate.
76
+ """
77
+ collisions = {name: classes for name, classes in name_matched_types.items() if len(classes) > 1}
78
+ if not collisions:
79
+ return
80
+ labels = sorted(f"{name!r} matched {sorted(cls.__name__ for cls in classes)}" for name, classes in collisions.items())
81
+ msg = (
82
+ "HarnessProfile.excluded_middleware name entry matched multiple "
83
+ "distinct middleware classes within a single stack: "
84
+ f"{'; '.join(labels)}. Use a class-form exclusion via the runtime "
85
+ "`HarnessProfile` to disambiguate."
86
+ )
87
+ raise ValueError(msg)
88
+
89
+
90
+ def _apply_excluded_middleware(
91
+ stack: list[AgentMiddleware[Any, Any, Any]],
92
+ profile: HarnessProfile,
93
+ *,
94
+ matched_classes: set[type[AgentMiddleware[Any, Any, Any]]] | None = None,
95
+ matched_names: set[str] | None = None,
96
+ ) -> list[AgentMiddleware[Any, Any, Any]]:
97
+ """Drop middleware in the stack matched by `profile.excluded_middleware`.
98
+
99
+ Class entries match on exact type (not `isinstance`), mirroring the
100
+ slot-identity semantics of `_merge_middleware` so a subclass introduced
101
+ by the caller is preserved when the profile excludes the base class.
102
+ String entries match `AgentMiddleware.name` exactly — defaults to the
103
+ class's `__name__` but is overridable when the public alias differs from
104
+ the impl class (e.g. `SummarizationMiddleware` for
105
+ `_DeepAgentsSummarizationMiddleware`).
106
+
107
+ When `matched_classes` / `matched_names` are supplied, matches are
108
+ recorded there so `_verify_excluded_middleware_coverage` can confirm
109
+ every entry matched *somewhere* across the stacks the profile applies to
110
+ (main agent + GP subagent). Per-stack checking would be too strict —
111
+ a profile legitimately targets middleware only one stack carries. Omit
112
+ the sets for single-stack filters where aggregation isn't meaningful.
113
+
114
+ Args:
115
+ stack: Fully assembled middleware list for a single agent/subagent.
116
+ profile: Profile whose `excluded_middleware` drives the filter.
117
+ matched_classes: Optional mutable set recording class matches across
118
+ calls for the same profile.
119
+ matched_names: Optional mutable set recording name matches, same
120
+ lifetime semantics as `matched_classes`.
121
+
122
+ Returns:
123
+ A new list with excluded entries removed. Always a fresh list, even
124
+ when no exclusions apply, so callers can mutate the result freely.
125
+ """
126
+ excluded = profile.excluded_middleware
127
+ if not excluded:
128
+ return list(stack)
129
+
130
+ excluded_classes: set[type[AgentMiddleware[Any, Any, Any]]] = set()
131
+ excluded_names: set[str] = set()
132
+ for entry in excluded:
133
+ if isinstance(entry, type):
134
+ excluded_classes.add(entry)
135
+ else:
136
+ excluded_names.add(entry)
137
+
138
+ filtered: list[AgentMiddleware[Any, Any, Any]] = []
139
+ name_matched_types: dict[str, set[type[AgentMiddleware[Any, Any, Any]]]] = {}
140
+ for mw in stack:
141
+ mw_type = type(mw)
142
+ mw_name = mw.name
143
+ if mw_type in excluded_classes:
144
+ if matched_classes is not None:
145
+ matched_classes.add(mw_type)
146
+ continue
147
+ if mw_name in excluded_names:
148
+ name_matched_types.setdefault(mw_name, set()).add(mw_type)
149
+ if matched_names is not None:
150
+ matched_names.add(mw_name)
151
+ continue
152
+ filtered.append(mw)
153
+
154
+ _raise_on_name_collisions(name_matched_types)
155
+
156
+ removed_count = len(stack) - len(filtered)
157
+ if removed_count:
158
+ logger.debug(
159
+ "Dropped %d middleware instance(s) from stack per profile.excluded_middleware=%r (matched classes=%s, names=%s)",
160
+ removed_count,
161
+ sorted(repr(entry) for entry in profile.excluded_middleware),
162
+ sorted(cls.__name__ for cls in excluded_classes),
163
+ sorted(excluded_names),
164
+ )
165
+ return filtered
166
+
167
+
168
+ def _verify_excluded_middleware_coverage(
169
+ profile: HarnessProfile,
170
+ matched_classes: set[type[AgentMiddleware[Any, Any, Any]]],
171
+ matched_names: set[str],
172
+ *,
173
+ required_classes: frozenset[type[AgentMiddleware[Any, Any, Any]]],
174
+ required_names: frozenset[str],
175
+ ) -> None:
176
+ """Raise `ValueError` if any `profile.excluded_middleware` entry matched nothing.
177
+
178
+ Run after every stack has been filtered so the accumulated `matched_*`
179
+ sets reflect matches anywhere. An entry that matched nothing is almost
180
+ always a typo or stale profile. Required-scaffolding and `_`-prefixed
181
+ entries are skipped — rejected earlier by
182
+ `_validate_excluded_middleware_config`.
183
+
184
+ Args:
185
+ profile: Profile whose `excluded_middleware` is being audited.
186
+ matched_classes: Accumulated class matches across filter calls.
187
+ matched_names: Accumulated name matches across filter calls.
188
+ required_classes: Scaffolding classes; subtracted from unmatched so
189
+ scaffolding exclusions (already rejected upstream) don't surface
190
+ here as defense-in-depth.
191
+ required_names: Scaffolding `.name` values; same purpose as
192
+ `required_classes`.
193
+
194
+ Raises:
195
+ ValueError: If any entry is missing from the corresponding `matched_*` set.
196
+ """
197
+ excluded = profile.excluded_middleware
198
+ if not excluded:
199
+ return
200
+
201
+ excluded_classes: set[type[AgentMiddleware[Any, Any, Any]]] = set()
202
+ excluded_names: set[str] = set()
203
+ for entry in excluded:
204
+ if isinstance(entry, type):
205
+ excluded_classes.add(entry)
206
+ else:
207
+ excluded_names.add(entry)
208
+
209
+ unmatched_classes = excluded_classes - matched_classes - required_classes
210
+ unmatched_names = excluded_names - matched_names - required_names
211
+ # Private-prefix names are rejected by the config guard; skip them here
212
+ # so the coverage error stays focused on legitimate "didn't match" cases.
213
+ unmatched_names = {name for name in unmatched_names if not name.startswith("_")}
214
+ if not unmatched_classes and not unmatched_names:
215
+ return
216
+
217
+ labels = sorted({cls.__name__ for cls in unmatched_classes} | {f"{name!r} (string)" for name in unmatched_names})
218
+ msg = (
219
+ f"HarnessProfile.excluded_middleware entries matched no middleware "
220
+ f"across any assembled stack: {', '.join(labels)}. Typo or stale "
221
+ f"profile — every exclusion must correspond to a middleware actually "
222
+ f"present at runtime. (Tip: use class-form exclusion when the class "
223
+ f"is available to catch typos at import time.)"
224
+ )
225
+ raise ValueError(msg)
@@ -0,0 +1,90 @@
1
+ """Local `DeltaChannel` reducer for the messages key.
2
+
3
+ Adapted from langgraph's `_messages_delta_reducer` (PR #7729). The upstream
4
+ version coerces `BaseMessageChunk` writes to full messages for parity with
5
+ `add_messages`. Deepagents never writes chunks to the messages channel —
6
+ `langchain.agents.create_agent` appends full `AIMessage` objects, and
7
+ streaming via `astream_events` operates on the output side, not the state
8
+ side — so we skip the per-message coercion.
9
+
10
+ ID assignment is intentionally absent here. LangGraph's `ensure_message_ids`
11
+ stamps stable UUIDs onto all `BaseMessage` writes before they are serialised
12
+ to the checkpoint, so by the time the reducer sees a message it already has a
13
+ stable ID. Assigning IDs in the reducer would be both redundant and fragile
14
+ (a reducer runs on replay too, where a randomly-assigned ID would differ from
15
+ the one stored in the checkpoint).
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ from typing import Any, cast
21
+
22
+ from langchain_core.messages import (
23
+ AnyMessage,
24
+ BaseMessage,
25
+ RemoveMessage,
26
+ convert_to_messages,
27
+ )
28
+ from langgraph.graph.message import REMOVE_ALL_MESSAGES
29
+
30
+
31
+ def _messages_delta_reducer( # noqa: C901, PLR0912
32
+ state: list[AnyMessage] | None, writes: list[list[AnyMessage]]
33
+ ) -> list[AnyMessage]:
34
+ """Batch reducer for use with `DeltaChannel` on the messages key.
35
+
36
+ Dedups by ID, tombstones via `RemoveMessage`, resets on
37
+ `REMOVE_ALL_MESSAGES`. IDs are expected to be pre-assigned by LangGraph's
38
+ `ensure_message_ids` hook; id=None messages are appended as-is.
39
+
40
+ Raw dict / string / tuple inputs are coerced to typed `BaseMessage` so
41
+ HTTP-driven graphs work without a separate coercion step.
42
+ """
43
+ # Each write is either a list of message-likes or a single message-like
44
+ # (BaseMessage / dict / str / tuple). Only lists flatten; everything
45
+ # else is one message.
46
+ flat: list[Any] = []
47
+ for w in writes:
48
+ if isinstance(w, list):
49
+ flat.extend(w)
50
+ else:
51
+ flat.append(w)
52
+ # Steady state: the reducer's own output is already typed BaseMessages,
53
+ # so skip convert_to_messages on the fast path. Only raw input (initial
54
+ # dicts, deserialized blobs) hits the slow path. `state` is `None` on
55
+ # `DeltaChannel.replay_writes` for threads whose earliest checkpoint did
56
+ # not seed `messages: []`; treat that as the empty list so the slow path
57
+ # doesn't pass `None` into `convert_to_messages`.
58
+ state_msgs = state if state and isinstance(state[0], BaseMessage) else cast("list[AnyMessage]", convert_to_messages(state or []))
59
+ msgs = cast("list[AnyMessage]", convert_to_messages(flat))
60
+
61
+ # REMOVE_ALL_MESSAGES resets everything; find the last sentinel and
62
+ # discard all state plus all writes before it.
63
+ remove_all_idx = None
64
+ for idx, m in enumerate(msgs):
65
+ if isinstance(m, RemoveMessage) and m.id == REMOVE_ALL_MESSAGES:
66
+ remove_all_idx = idx
67
+ if remove_all_idx is not None:
68
+ state_msgs = []
69
+ msgs = msgs[remove_all_idx + 1 :]
70
+
71
+ result: list[AnyMessage | None] = []
72
+ index: dict[str, int] = {}
73
+ for m in state_msgs:
74
+ if m.id is not None:
75
+ index[m.id] = len(result)
76
+ result.append(m)
77
+ for msg in msgs:
78
+ mid = msg.id
79
+ if mid is None:
80
+ result.append(msg)
81
+ elif isinstance(msg, RemoveMessage):
82
+ if mid in index:
83
+ result[index[mid]] = None
84
+ del index[mid]
85
+ elif mid in index:
86
+ result[index[mid]] = msg
87
+ else:
88
+ index[mid] = len(result)
89
+ result.append(msg)
90
+ return [m for m in result if m is not None]