From 583723b7678022082af74165cb2d995d38dcd2b4 Mon Sep 17 00:00:00 2001 From: DABH Date: Thu, 10 Sep 2026 00:53:55 -0500 Subject: [PATCH 1/6] deepagents: route string summarizer models through the durable seam MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SummarizationMiddleware and create_summarization_tool_middleware resolve a model NAME STRING through seams the plugin did not patch: LangChain's summarization middleware resolves via init_chat_model (bound at the top of langchain.agents.middleware.summarization), and the tool-middleware helper via a call-time import of deepagents._models.resolve_model. Constructed in-workflow with a string — the documented way to customize the summarizer's trigger, keep policy, or prompt — either path built a real provider client inside the workflow and would run compaction LLM calls there: nondeterministic, replay-unsafe, and invisible until a conversation grew past its trigger (demonstrated live: the middleware resolved a ChatAnthropic inside a sandboxed workflow). install_model_patch now covers both additional bindings, gated on workflow.in_workflow() like the existing graph seam, and uninstall_model_patch restores them. The DEFAULT stack was never affected: create_deep_agent resolves the agent model through the patched graph seam before handing the middleware an already-durable instance. Regression tests pin the resolved type for both explicit string-model paths. --- temporalio/contrib/deepagents/_model.py | 73 ++++++++++-- .../contrib/deepagents/test_summarization.py | 107 ++++++++++++++++++ 2 files changed, 170 insertions(+), 10 deletions(-) create mode 100644 tests/contrib/deepagents/test_summarization.py diff --git a/temporalio/contrib/deepagents/_model.py b/temporalio/contrib/deepagents/_model.py index 2d97f992d..0d7641ca2 100644 --- a/temporalio/contrib/deepagents/_model.py +++ b/temporalio/contrib/deepagents/_model.py @@ -266,9 +266,21 @@ def _stream( # The durability seam is ``deepagents._models.resolve_model``, which # ``create_deep_agent`` calls to turn a ``model=`` string (or instance) into a # ``BaseChatModel`` — for both the top-level agent and every ``SubAgent`` -# (``graph.py`` lines 592 and 634). We patch it on the ``deepagents.graph`` -# module, where ``create_deep_agent``'s body resolves the ``resolve_model`` name -# at call time. +# (``graph.py`` lines 592 and 634). We patch BOTH bindings of that function: +# on ``deepagents.graph`` (whose module-top ``from deepagents._models import +# resolve_model`` froze its own reference, read afresh from graph's globals on +# each ``create_deep_agent`` call) and on ``deepagents._models`` itself — the +# definition site — which covers every call-time importer. The middleware +# layer is why the extra bindings matter. Two middleware seams exist: +# ``create_summarization_tool_middleware`` resolves its model via a +# function-level ``from deepagents._models import resolve_model`` (covered by +# the ``_models`` binding), while ``SummarizationMiddleware`` itself delegates +# to LangChain's summarization middleware, whose ``__init__`` resolves a name +# string through ``init_chat_model`` — bound at the top of +# ``langchain.agents.middleware.summarization`` — so that module's binding is +# patched too. With either seam unpatched, a summarizer configured as a name +# string and constructed in-workflow builds a real provider client and runs +# compaction LLM calls inside the workflow (nondeterministic, replay-unsafe). # # Patching *this* seam (not ``deepagents.create_deep_agent``) is what makes the # rewrite survive the user's import style. A user who writes the idiomatic @@ -288,6 +300,7 @@ def _stream( _original_create_deep_agent: Any = None _original_resolve_model: Any = None +_original_lc_init_chat_model: Any = None def _wrap_model_arg(model: Any) -> Any: @@ -317,21 +330,29 @@ def _wrap_model_arg(model: Any) -> Any: def install_model_patch() -> None: """Route Deep Agents' model resolution through :class:`TemporalModel`. - Patches ``deepagents.graph.resolve_model`` (the seam ``create_deep_agent`` - uses for the main agent *and* every sub-agent) so a bare ``model="..."`` - string becomes a durable :class:`TemporalModel`, and additionally wraps - ``deepagents.create_deep_agent`` to fire the advisory tool / checkpointer - warnings. Both only act when called inside a workflow, so importing - deepagents on a plain client / activity worker is unaffected. Idempotent. + Patches every binding that turns a ``model="..."`` name string into a + live model, so each becomes a durable :class:`TemporalModel` in-workflow: + ``deepagents.graph.resolve_model`` (the ``create_deep_agent`` seam for the + main agent and every sub-agent), ``deepagents._models.resolve_model`` (the + definition site, read call-time by ``create_summarization_tool_middleware``), + and ``init_chat_model`` as bound in LangChain's summarization middleware + module (the seam ``SummarizationMiddleware`` resolves its summarizer + through). Additionally wraps ``deepagents.create_deep_agent`` to fire the + advisory tool / checkpointer warnings. All of it only acts when called + inside a workflow, so importing deepagents on a plain client / activity + worker is unaffected. Idempotent. """ global _original_create_deep_agent, _original_resolve_model # importlib: `deepagents` is absent on Python 3.10 environments (its floor # is 3.11), so static imports here fail type-checking there. deepagents = importlib.import_module("deepagents") _graph = importlib.import_module("deepagents.graph") + _models = importlib.import_module("deepagents._models") if _original_resolve_model is None: - _original_resolve_model = _graph.resolve_model + # graph's module-top import bound the same function object the + # definition site holds; one stored original restores both bindings. + _original_resolve_model = _models.resolve_model def patched_resolve_model(model: Any) -> Any: if workflow.in_workflow(): @@ -339,6 +360,30 @@ def patched_resolve_model(model: Any) -> Any: return _original_resolve_model(model) setattr(_graph, "resolve_model", patched_resolve_model) + setattr(_models, "resolve_model", patched_resolve_model) + + global _original_lc_init_chat_model + if _original_lc_init_chat_model is None: + # Best-effort: the module path is LangChain-internal and may move. + # If it does, string summarizer models fall back to a live client + # in-workflow — the regression test pins this so an upstream move + # fails loudly in CI instead of silently shipping. + try: + _lc_sum = importlib.import_module( + "langchain.agents.middleware.summarization" + ) + original_init_chat_model = _lc_sum.init_chat_model + except (ImportError, AttributeError): + pass + else: + _original_lc_init_chat_model = original_init_chat_model + + def patched_init_chat_model(model: Any, *args: Any, **kwargs: Any) -> Any: + if workflow.in_workflow() and isinstance(model, str): + return _wrap_model_arg(model) + return original_init_chat_model(model, *args, **kwargs) + + setattr(_lc_sum, "init_chat_model", patched_init_chat_model) if _original_create_deep_agent is None: _original_create_deep_agent = deepagents.create_deep_agent @@ -362,9 +407,17 @@ def uninstall_model_patch() -> None: global _original_create_deep_agent, _original_resolve_model if _original_resolve_model is not None: _graph = importlib.import_module("deepagents.graph") + _models = importlib.import_module("deepagents._models") setattr(_graph, "resolve_model", _original_resolve_model) + setattr(_models, "resolve_model", _original_resolve_model) _original_resolve_model = None + global _original_lc_init_chat_model + if _original_lc_init_chat_model is not None: + _lc_sum = importlib.import_module("langchain.agents.middleware.summarization") + + setattr(_lc_sum, "init_chat_model", _original_lc_init_chat_model) + _original_lc_init_chat_model = None if _original_create_deep_agent is not None: deepagents = importlib.import_module("deepagents") diff --git a/tests/contrib/deepagents/test_summarization.py b/tests/contrib/deepagents/test_summarization.py new file mode 100644 index 000000000..909f77492 --- /dev/null +++ b/tests/contrib/deepagents/test_summarization.py @@ -0,0 +1,107 @@ +"""Summarization middleware's model resolves through the durable seam. + +String summarizer models used to bypass the plugin entirely: +``SummarizationMiddleware`` delegates to LangChain's summarization middleware, +whose ``__init__`` resolves a name string via ``init_chat_model`` (bound at +the top of ``langchain.agents.middleware.summarization``), and +``create_summarization_tool_middleware`` resolves via a call-time ``from +deepagents._models import resolve_model``. Neither reads the +``deepagents.graph`` binding the plugin patched, so a middleware constructed +in-workflow with a name string built a real provider client and ran +compaction LLM calls inside the workflow — nondeterministic, replay-unsafe, +and invisible until a conversation grew past its trigger. (The DEFAULT +stack's summarizer is unaffected: ``create_deep_agent`` resolves the agent +model through the patched graph seam first and hands the middleware the +already-durable instance.) The plugin now patches all three bindings; these +tests pin the resolved type for both explicit string-model paths. +""" + +from __future__ import annotations + +import sys +import uuid + +import pytest + +from temporalio.testing import WorkflowEnvironment + +pytestmark = pytest.mark.skipif( + sys.version_info < (3, 11), reason="deepagents requires Python >= 3.11" +) +pytest.importorskip("deepagents") +pytest.importorskip("langchain_core") + +from temporalio import workflow +from temporalio.contrib.deepagents import DeepAgentsPlugin, TemporalModel +from temporalio.worker import Worker + +with workflow.unsafe.imports_passed_through(): + from deepagents.backends import StateBackend + from deepagents.middleware import SummarizationMiddleware + from deepagents.middleware.summarization import create_summarization_tool_middleware + + +@workflow.defn +class SummarizerResolutionWorkflow: + @workflow.run + async def run(self) -> str: + # The seam itself, pinned directly: in-workflow, the middleware's + # resolved summarizer must be a TemporalModel, not a provider client. + middleware = SummarizationMiddleware( + "anthropic:claude-sonnet-4-5", + backend=StateBackend(), + ) + return type(middleware.model).__name__ + + +@pytest.mark.asyncio +async def test_summarizer_model_resolves_durable(env: WorkflowEnvironment) -> None: + plugin = DeepAgentsPlugin() + async with Worker( + env.client, + task_queue="da-summarizer-resolve", + workflows=[SummarizerResolutionWorkflow], + plugins=[plugin], + ): + out = await env.client.execute_workflow( + SummarizerResolutionWorkflow.run, + id=f"da-summarizer-resolve-{uuid.uuid4()}", + task_queue="da-summarizer-resolve", + ) + assert out == TemporalModel.__name__, out + + +@workflow.defn +class ToolMiddlewareResolutionWorkflow: + @workflow.run + async def run(self) -> str: + # The OTHER seam: create_summarization_tool_middleware resolves via a + # call-time `from deepagents._models import resolve_model` — the + # definition-site binding, which only this patch covers. Its own + # docstring example passes a name string, so this is a documented + # user path. The composed middleware keeps the resolved summarizer at + # `_summarization.model`. + middleware = create_summarization_tool_middleware( + "anthropic:claude-sonnet-4-5", + StateBackend(), + ) + return type(middleware._summarization.model).__name__ + + +@pytest.mark.asyncio +async def test_tool_middleware_summarizer_resolves_durable( + env: WorkflowEnvironment, +) -> None: + plugin = DeepAgentsPlugin() + async with Worker( + env.client, + task_queue="da-summarizer-tool-resolve", + workflows=[ToolMiddlewareResolutionWorkflow], + plugins=[plugin], + ): + out = await env.client.execute_workflow( + ToolMiddlewareResolutionWorkflow.run, + id=f"da-summarizer-tool-resolve-{uuid.uuid4()}", + task_queue="da-summarizer-tool-resolve", + ) + assert out == TemporalModel.__name__, out From 700c5835fee5b9f6e872741bb489a349faa3a338 Mon Sep 17 00:00:00 2001 From: DABH Date: Thu, 10 Sep 2026 01:32:56 -0500 Subject: [PATCH 2/6] Condense the seam comments and test docstring --- temporalio/contrib/deepagents/_model.py | 70 ++++++------------- .../contrib/deepagents/test_summarization.py | 32 +++------ 2 files changed, 29 insertions(+), 73 deletions(-) diff --git a/temporalio/contrib/deepagents/_model.py b/temporalio/contrib/deepagents/_model.py index 0d7641ca2..fcac52f79 100644 --- a/temporalio/contrib/deepagents/_model.py +++ b/temporalio/contrib/deepagents/_model.py @@ -263,40 +263,22 @@ def _stream( # create_deep_agent model patch (implicit wrapping) # --------------------------------------------------------------------------- # -# The durability seam is ``deepagents._models.resolve_model``, which -# ``create_deep_agent`` calls to turn a ``model=`` string (or instance) into a -# ``BaseChatModel`` — for both the top-level agent and every ``SubAgent`` -# (``graph.py`` lines 592 and 634). We patch BOTH bindings of that function: -# on ``deepagents.graph`` (whose module-top ``from deepagents._models import -# resolve_model`` froze its own reference, read afresh from graph's globals on -# each ``create_deep_agent`` call) and on ``deepagents._models`` itself — the -# definition site — which covers every call-time importer. The middleware -# layer is why the extra bindings matter. Two middleware seams exist: -# ``create_summarization_tool_middleware`` resolves its model via a -# function-level ``from deepagents._models import resolve_model`` (covered by -# the ``_models`` binding), while ``SummarizationMiddleware`` itself delegates -# to LangChain's summarization middleware, whose ``__init__`` resolves a name -# string through ``init_chat_model`` — bound at the top of -# ``langchain.agents.middleware.summarization`` — so that module's binding is -# patched too. With either seam unpatched, a summarizer configured as a name -# string and constructed in-workflow builds a real provider client and runs -# compaction LLM calls inside the workflow (nondeterministic, replay-unsafe). +# The durability seam is ``resolve_model``, which turns a ``model=`` name +# string into a live ``BaseChatModel``. We patch every binding a string can +# reach in-workflow: # -# Patching *this* seam (not ``deepagents.create_deep_agent``) is what makes the -# rewrite survive the user's import style. A user who writes the idiomatic -# ``from deepagents import create_deep_agent`` binds the *original* function -# object into their module; rebinding the ``deepagents.create_deep_agent`` -# attribute would never be seen by that already-bound reference, so string -# models would reach the real provider inside the workflow (a hang / non- -# determinism). ``create_deep_agent``'s body, by contrast, always looks up -# ``resolve_model`` in the ``deepagents.graph`` globals afresh on each call, so -# rebinding it there is observed no matter how the caller imported the factory. -# It also preserves ``_model_spec`` (the original string), which the factory -# reads *before* calling ``resolve_model`` for harness-profile lookup. +# - ``deepagents.graph`` — read afresh by ``create_deep_agent`` for the main +# agent and every sub-agent (its module-top import froze its own binding). +# - ``deepagents._models`` — the definition site; covers call-time importers +# such as ``create_summarization_tool_middleware``. +# - ``init_chat_model`` as bound in ``langchain.agents.middleware.summarization`` +# — the seam ``SummarizationMiddleware`` resolves a string summarizer through. # -# ``create_deep_agent`` is still wrapped separately, best-effort, purely to fire -# the construction-time warnings that need the ``tools`` / ``checkpointer`` -# kwargs (those warnings are advisory and carry no durability weight). +# An unpatched binding means a real provider client constructed (and called) +# inside the workflow: nondeterministic and replay-unsafe. We do NOT rebind +# ``deepagents.create_deep_agent`` for durability — callers who already did +# ``from deepagents import create_deep_agent`` hold the original object — only +# a best-effort wrap to fire the advisory construction-time warnings. _original_create_deep_agent: Any = None _original_resolve_model: Any = None @@ -330,17 +312,10 @@ def _wrap_model_arg(model: Any) -> Any: def install_model_patch() -> None: """Route Deep Agents' model resolution through :class:`TemporalModel`. - Patches every binding that turns a ``model="..."`` name string into a - live model, so each becomes a durable :class:`TemporalModel` in-workflow: - ``deepagents.graph.resolve_model`` (the ``create_deep_agent`` seam for the - main agent and every sub-agent), ``deepagents._models.resolve_model`` (the - definition site, read call-time by ``create_summarization_tool_middleware``), - and ``init_chat_model`` as bound in LangChain's summarization middleware - module (the seam ``SummarizationMiddleware`` resolves its summarizer - through). Additionally wraps ``deepagents.create_deep_agent`` to fire the - advisory tool / checkpointer warnings. All of it only acts when called - inside a workflow, so importing deepagents on a plain client / activity - worker is unaffected. Idempotent. + Patches the model-resolution bindings listed above so a name string + becomes a durable :class:`TemporalModel` in-workflow, and wraps + ``deepagents.create_deep_agent`` for the advisory warnings. No effect + outside workflows. Idempotent. """ global _original_create_deep_agent, _original_resolve_model # importlib: `deepagents` is absent on Python 3.10 environments (its floor @@ -350,8 +325,7 @@ def install_model_patch() -> None: _models = importlib.import_module("deepagents._models") if _original_resolve_model is None: - # graph's module-top import bound the same function object the - # definition site holds; one stored original restores both bindings. + # One original serves both bindings (same function object). _original_resolve_model = _models.resolve_model def patched_resolve_model(model: Any) -> Any: @@ -364,10 +338,8 @@ def patched_resolve_model(model: Any) -> Any: global _original_lc_init_chat_model if _original_lc_init_chat_model is None: - # Best-effort: the module path is LangChain-internal and may move. - # If it does, string summarizer models fall back to a live client - # in-workflow — the regression test pins this so an upstream move - # fails loudly in CI instead of silently shipping. + # Best-effort: LangChain-internal path; if it moves, the regression + # test fails loudly rather than this crashing worker start. try: _lc_sum = importlib.import_module( "langchain.agents.middleware.summarization" diff --git a/tests/contrib/deepagents/test_summarization.py b/tests/contrib/deepagents/test_summarization.py index 909f77492..56390d064 100644 --- a/tests/contrib/deepagents/test_summarization.py +++ b/tests/contrib/deepagents/test_summarization.py @@ -1,19 +1,11 @@ -"""Summarization middleware's model resolves through the durable seam. - -String summarizer models used to bypass the plugin entirely: -``SummarizationMiddleware`` delegates to LangChain's summarization middleware, -whose ``__init__`` resolves a name string via ``init_chat_model`` (bound at -the top of ``langchain.agents.middleware.summarization``), and -``create_summarization_tool_middleware`` resolves via a call-time ``from -deepagents._models import resolve_model``. Neither reads the -``deepagents.graph`` binding the plugin patched, so a middleware constructed -in-workflow with a name string built a real provider client and ran -compaction LLM calls inside the workflow — nondeterministic, replay-unsafe, -and invisible until a conversation grew past its trigger. (The DEFAULT -stack's summarizer is unaffected: ``create_deep_agent`` resolves the agent -model through the patched graph seam first and hands the middleware the -already-durable instance.) The plugin now patches all three bindings; these -tests pin the resolved type for both explicit string-model paths. +"""String summarizer models must resolve to a durable ``TemporalModel``. + +``SummarizationMiddleware`` resolves a name string via LangChain's +``init_chat_model`` binding, and ``create_summarization_tool_middleware`` via +a call-time import of ``deepagents._models.resolve_model`` — neither reads +the patched ``deepagents.graph`` seam, so in-workflow both built a real +provider client (the default stack is unaffected: it receives the +already-resolved agent model). One pin per seam. """ from __future__ import annotations @@ -45,8 +37,6 @@ class SummarizerResolutionWorkflow: @workflow.run async def run(self) -> str: - # The seam itself, pinned directly: in-workflow, the middleware's - # resolved summarizer must be a TemporalModel, not a provider client. middleware = SummarizationMiddleware( "anthropic:claude-sonnet-4-5", backend=StateBackend(), @@ -75,12 +65,6 @@ async def test_summarizer_model_resolves_durable(env: WorkflowEnvironment) -> No class ToolMiddlewareResolutionWorkflow: @workflow.run async def run(self) -> str: - # The OTHER seam: create_summarization_tool_middleware resolves via a - # call-time `from deepagents._models import resolve_model` — the - # definition-site binding, which only this patch covers. Its own - # docstring example passes a name string, so this is a documented - # user path. The composed middleware keeps the resolved summarizer at - # `_summarization.model`. middleware = create_summarization_tool_middleware( "anthropic:claude-sonnet-4-5", StateBackend(), From c5f226053816aaa1d6a9ebeb160e9faa41053b5e Mon Sep 17 00:00:00 2001 From: DABH Date: Thu, 10 Sep 2026 02:04:45 -0500 Subject: [PATCH 3/6] Bind deepagents test symbols via importorskip for Python 3.10 lint --- tests/contrib/deepagents/test_summarization.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/tests/contrib/deepagents/test_summarization.py b/tests/contrib/deepagents/test_summarization.py index 56390d064..a6c942952 100644 --- a/tests/contrib/deepagents/test_summarization.py +++ b/tests/contrib/deepagents/test_summarization.py @@ -27,10 +27,16 @@ from temporalio.contrib.deepagents import DeepAgentsPlugin, TemporalModel from temporalio.worker import Worker -with workflow.unsafe.imports_passed_through(): - from deepagents.backends import StateBackend - from deepagents.middleware import SummarizationMiddleware - from deepagents.middleware.summarization import create_summarization_tool_middleware +# Bind deepagents symbols off importorskip modules: static imports cannot +# resolve on Python 3.10 environments, where deepagents is absent. +_backends_mod = pytest.importorskip("deepagents.backends") +_middleware_mod = pytest.importorskip("deepagents.middleware") +_summarization_mod = pytest.importorskip("deepagents.middleware.summarization") +StateBackend = _backends_mod.StateBackend +SummarizationMiddleware = _middleware_mod.SummarizationMiddleware +create_summarization_tool_middleware = ( + _summarization_mod.create_summarization_tool_middleware +) @workflow.defn From 677d107055b01a1ee6fa8ddf559cc5fc3e61b029 Mon Sep 17 00:00:00 2001 From: DABH Date: Thu, 10 Sep 2026 02:05:50 -0500 Subject: [PATCH 4/6] Add changelog entry --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 40e806916..22177ece3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ to include examples, links to docs, or any other relevant information. ### Deprecated — soon-to-be-removed features ### :boom: Breaking Changes — removed or backwards-incompatible features ### Fixed — notable bug fixes + +- `contrib.deepagents`: summarization middleware configured with a model name string now routes its LLM calls through Activities instead of running them in the Workflow. ### Security — notable security fixes --> From 443e29c2c97894fe74479dd49d78e611834a6f07 Mon Sep 17 00:00:00 2001 From: DABH Date: Thu, 10 Sep 2026 02:06:09 -0500 Subject: [PATCH 5/6] Move changelog entry under the Unreleased Fixed heading --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 22177ece3..d636f2f84 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,8 +11,6 @@ to include examples, links to docs, or any other relevant information. ### Deprecated — soon-to-be-removed features ### :boom: Breaking Changes — removed or backwards-incompatible features ### Fixed — notable bug fixes - -- `contrib.deepagents`: summarization middleware configured with a model name string now routes its LLM calls through Activities instead of running them in the Workflow. ### Security — notable security fixes --> @@ -54,6 +52,8 @@ to include examples, links to docs, or any other relevant information. ### Fixed +- `contrib.deepagents`: summarization middleware configured with a model name string now routes its LLM calls through Activities instead of running them in the Workflow. + - **Experimental**: External storage metrics now report the wall-clock time storage was in flight. Previously each batch's duration was summed, over-reporting the time whenever storage operations ran concurrently. From dfa794d2074a98df2d59d302d4dc2c4f41b39b4f Mon Sep 17 00:00:00 2001 From: DABH Date: Thu, 10 Sep 2026 11:25:44 -0500 Subject: [PATCH 6/6] Fail closed: patch the summarizer seam on deepagents' own class MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The LangChain-internal init_chat_model binding was guarded best-effort, so an upstream module move silently restored the in-workflow provider client this patch exists to prevent. Patch SummarizationMiddleware.__init__ on deepagents' class instead — pre-resolving a string model before the middleware delegates to LangChain — which sits inside this package's deepagents version pin and therefore needs no guard: a missing seam is a broken install and fails the worker at startup. --- temporalio/contrib/deepagents/_model.py | 52 ++++++++++++------------- 1 file changed, 25 insertions(+), 27 deletions(-) diff --git a/temporalio/contrib/deepagents/_model.py b/temporalio/contrib/deepagents/_model.py index fcac52f79..c9f2b9a09 100644 --- a/temporalio/contrib/deepagents/_model.py +++ b/temporalio/contrib/deepagents/_model.py @@ -271,18 +271,21 @@ def _stream( # agent and every sub-agent (its module-top import froze its own binding). # - ``deepagents._models`` — the definition site; covers call-time importers # such as ``create_summarization_tool_middleware``. -# - ``init_chat_model`` as bound in ``langchain.agents.middleware.summarization`` -# — the seam ``SummarizationMiddleware`` resolves a string summarizer through. +# - ``SummarizationMiddleware.__init__`` — pre-resolves a string summarizer +# before the middleware delegates to LangChain's ``init_chat_model``. # # An unpatched binding means a real provider client constructed (and called) -# inside the workflow: nondeterministic and replay-unsafe. We do NOT rebind +# inside the workflow: nondeterministic and replay-unsafe. Every patched seam +# is deepagents-internal and covered by this package's deepagents version +# pin, so none is guarded: a missing seam is a broken install and fails the +# worker at startup rather than silently reverting. We do NOT rebind # ``deepagents.create_deep_agent`` for durability — callers who already did # ``from deepagents import create_deep_agent`` hold the original object — only # a best-effort wrap to fire the advisory construction-time warnings. _original_create_deep_agent: Any = None _original_resolve_model: Any = None -_original_lc_init_chat_model: Any = None +_original_summarization_init: Any = None def _wrap_model_arg(model: Any) -> Any: @@ -336,26 +339,21 @@ def patched_resolve_model(model: Any) -> Any: setattr(_graph, "resolve_model", patched_resolve_model) setattr(_models, "resolve_model", patched_resolve_model) - global _original_lc_init_chat_model - if _original_lc_init_chat_model is None: - # Best-effort: LangChain-internal path; if it moves, the regression - # test fails loudly rather than this crashing worker start. - try: - _lc_sum = importlib.import_module( - "langchain.agents.middleware.summarization" - ) - original_init_chat_model = _lc_sum.init_chat_model - except (ImportError, AttributeError): - pass - else: - _original_lc_init_chat_model = original_init_chat_model + global _original_summarization_init + if _original_summarization_init is None: + _da_sum = importlib.import_module("deepagents.middleware.summarization") + summarization_cls = _da_sum.SummarizationMiddleware + original_init = summarization_cls.__init__ + _original_summarization_init = original_init - def patched_init_chat_model(model: Any, *args: Any, **kwargs: Any) -> Any: - if workflow.in_workflow() and isinstance(model, str): - return _wrap_model_arg(model) - return original_init_chat_model(model, *args, **kwargs) + def patched_summarization_init( + self: Any, model: Any, *args: Any, **kwargs: Any + ) -> None: + if workflow.in_workflow() and isinstance(model, str): + model = _wrap_model_arg(model) + original_init(self, model, *args, **kwargs) - setattr(_lc_sum, "init_chat_model", patched_init_chat_model) + summarization_cls.__init__ = patched_summarization_init if _original_create_deep_agent is None: _original_create_deep_agent = deepagents.create_deep_agent @@ -384,12 +382,12 @@ def uninstall_model_patch() -> None: setattr(_graph, "resolve_model", _original_resolve_model) setattr(_models, "resolve_model", _original_resolve_model) _original_resolve_model = None - global _original_lc_init_chat_model - if _original_lc_init_chat_model is not None: - _lc_sum = importlib.import_module("langchain.agents.middleware.summarization") + global _original_summarization_init + if _original_summarization_init is not None: + _da_sum = importlib.import_module("deepagents.middleware.summarization") - setattr(_lc_sum, "init_chat_model", _original_lc_init_chat_model) - _original_lc_init_chat_model = None + _da_sum.SummarizationMiddleware.__init__ = _original_summarization_init + _original_summarization_init = None if _original_create_deep_agent is not None: deepagents = importlib.import_module("deepagents")