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.
- soothe_deepagents/__init__.py +50 -0
- soothe_deepagents/_api/__init__.py +5 -0
- soothe_deepagents/_api/deprecation.py +131 -0
- soothe_deepagents/_excluded_middleware.py +225 -0
- soothe_deepagents/_messages_reducer.py +90 -0
- soothe_deepagents/_models.py +211 -0
- soothe_deepagents/_tools.py +65 -0
- soothe_deepagents/_version.py +7 -0
- soothe_deepagents/backends/__init__.py +28 -0
- soothe_deepagents/backends/composite.py +861 -0
- soothe_deepagents/backends/context_hub.py +374 -0
- soothe_deepagents/backends/filesystem.py +1209 -0
- soothe_deepagents/backends/langsmith.py +279 -0
- soothe_deepagents/backends/local_shell.py +387 -0
- soothe_deepagents/backends/protocol.py +992 -0
- soothe_deepagents/backends/sandbox.py +1363 -0
- soothe_deepagents/backends/state.py +408 -0
- soothe_deepagents/backends/store.py +882 -0
- soothe_deepagents/backends/utils.py +917 -0
- soothe_deepagents/graph.py +1033 -0
- soothe_deepagents/middleware/__init__.py +104 -0
- soothe_deepagents/middleware/_fs_interrupt.py +183 -0
- soothe_deepagents/middleware/_message_eviction.py +162 -0
- soothe_deepagents/middleware/_overflow_clip.py +206 -0
- soothe_deepagents/middleware/_state.py +30 -0
- soothe_deepagents/middleware/_tool_exclusion.py +65 -0
- soothe_deepagents/middleware/_utils.py +23 -0
- soothe_deepagents/middleware/_video.py +404 -0
- soothe_deepagents/middleware/async_subagents.py +956 -0
- soothe_deepagents/middleware/filesystem.py +3102 -0
- soothe_deepagents/middleware/memory.py +441 -0
- soothe_deepagents/middleware/patch_tool_calls.py +46 -0
- soothe_deepagents/middleware/permissions.py +5 -0
- soothe_deepagents/middleware/rubric.py +805 -0
- soothe_deepagents/middleware/skills.py +1085 -0
- soothe_deepagents/middleware/subagents.py +871 -0
- soothe_deepagents/middleware/summarization.py +2190 -0
- soothe_deepagents/profiles/__init__.py +61 -0
- soothe_deepagents/profiles/_builtin_profiles.py +236 -0
- soothe_deepagents/profiles/_keys.py +42 -0
- soothe_deepagents/profiles/harness/__init__.py +22 -0
- soothe_deepagents/profiles/harness/_anthropic_haiku_4_5.py +52 -0
- soothe_deepagents/profiles/harness/_anthropic_opus_4_7.py +56 -0
- soothe_deepagents/profiles/harness/_anthropic_sonnet_4_6.py +52 -0
- soothe_deepagents/profiles/harness/_nvidia_nemotron_3_ultra.py +1811 -0
- soothe_deepagents/profiles/harness/_openai_codex.py +68 -0
- soothe_deepagents/profiles/harness/harness_profiles.py +1325 -0
- soothe_deepagents/profiles/provider/__init__.py +15 -0
- soothe_deepagents/profiles/provider/_nvidia.py +50 -0
- soothe_deepagents/profiles/provider/_openai.py +24 -0
- soothe_deepagents/profiles/provider/_openrouter.py +130 -0
- soothe_deepagents/profiles/provider/provider_profiles.py +455 -0
- soothe_deepagents/py.typed +0 -0
- soothe_deepagents-0.7.16.dist-info/METADATA +140 -0
- soothe_deepagents-0.7.16.dist-info/RECORD +57 -0
- soothe_deepagents-0.7.16.dist-info/WHEEL +5 -0
- 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,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]
|