fix(sdk): pass through summarization factory prompt knobs (#3559)

Closes #2622

---

Adds keyword-only controls for summarization prompt, trim limit, token
counter, and compact-tool system prompt while preserving model-aware
trigger/keep defaults. Brings to parity with class init.

_Opened collaboratively by Mason Daugherty and open-swe._

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
Co-authored-by: Mason Daugherty <61371264+mdrxy@users.noreply.github.com>
Co-authored-by: Mason Daugherty <mason@langchain.dev>
This commit is contained in:
open-swe[bot]
2026-06-03 11:23:10 -04:00
committed by GitHub
parent 2c2cec8747
commit a663cad885
3 changed files with 64 additions and 3 deletions
@@ -1158,6 +1158,10 @@ This is the name external callers should import and reference.
def create_summarization_middleware(
model: BaseChatModel,
backend: BACKEND_TYPES,
*,
summary_prompt: str = DEFAULT_SUMMARY_PROMPT,
trim_tokens_to_summarize: int | None = None,
token_counter: TokenCounter = count_tokens_approximately,
) -> _DeepAgentsSummarizationMiddleware:
"""Create a Deep Agents `SummarizationMiddleware` with model-aware defaults.
@@ -1199,6 +1203,9 @@ def create_summarization_middleware(
Use `resolve_model()` first if needed for model strings.
backend: Backend instance or factory for persisting conversation history.
summary_prompt: Prompt template for generating summaries.
trim_tokens_to_summarize: Max tokens to include when generating summary.
token_counter: Function to count tokens in messages.
Returns:
Configured `SummarizationMiddleware` instance.
@@ -1218,7 +1225,9 @@ def create_summarization_middleware(
backend=backend,
trigger=defaults["trigger"],
keep=defaults["keep"],
trim_tokens_to_summarize=None,
token_counter=token_counter,
summary_prompt=summary_prompt,
trim_tokens_to_summarize=trim_tokens_to_summarize,
truncate_args_settings=defaults["truncate_args_settings"],
)
@@ -1226,6 +1235,8 @@ def create_summarization_middleware(
def create_summarization_tool_middleware(
model: str | BaseChatModel,
backend: BACKEND_TYPES,
*,
system_prompt: str | None = SUMMARIZATION_SYSTEM_PROMPT,
) -> SummarizationToolMiddleware:
"""Create a `SummarizationToolMiddleware` with model-aware defaults.
@@ -1257,6 +1268,8 @@ def create_summarization_tool_middleware(
model: Chat model instance, or a model string
(e.g. `"anthropic:claude-sonnet-4-6"`).
backend: Backend instance or factory for persisting conversation history.
system_prompt: System-prompt fragment nudging the model to call
`compact_conversation`. Pass `None` to skip appending the nudge.
Returns:
Configured `SummarizationToolMiddleware` instance.
@@ -1307,7 +1320,7 @@ def create_summarization_tool_middleware(
if isinstance(model, str):
model = resolve_model(model)
summarization = create_summarization_middleware(model, backend)
return SummarizationToolMiddleware(summarization)
return SummarizationToolMiddleware(summarization, system_prompt=system_prompt)
class SummarizationToolMiddleware(AgentMiddleware):
@@ -2,6 +2,7 @@
from __future__ import annotations
from inspect import Parameter, signature
from typing import Any
from unittest.mock import MagicMock, NonCallableMagicMock, patch
@@ -640,6 +641,22 @@ def test_create_summarization_tool_middleware_returns_instance() -> None:
assert mw.tools[0].name == "compact_conversation"
def test_create_summarization_tool_middleware_accepts_system_prompt() -> None:
"""Factory passes explicit `system_prompt` through to the tool middleware."""
model = GenericFakeChatModel(messages=iter([AIMessage(content="ok")]))
model.profile = {"max_input_tokens": 120_000}
mw = create_summarization_tool_middleware(model, MagicMock(), system_prompt="custom nudge")
assert mw.system_prompt == "custom nudge"
def test_create_summarization_tool_middleware_system_prompt_is_keyword_only() -> None:
"""Requires the optional tool nudge to be passed by name."""
params = signature(create_summarization_tool_middleware).parameters
assert params["system_prompt"].kind is Parameter.KEYWORD_ONLY
# --- system_prompt override / suppression --------------------------------
@@ -1,10 +1,12 @@
"""Unit tests for the summarization middleware factory."""
from collections.abc import Iterable
from inspect import Parameter, signature
from typing import Any, cast
from unittest.mock import MagicMock
import pytest
from langchain_core.messages import AIMessage
from langchain_core.messages import AIMessage, MessageLikeRepresentation
from deepagents.middleware.summarization import create_summarization_middleware
from tests.unit_tests.chat_model import GenericFakeChatModel
@@ -42,6 +44,35 @@ def test_factory_uses_fallback_defaults_without_profile() -> None:
assert middleware._truncate_args_keep == ("messages", 20)
def test_factory_surfaces_summarization_knobs() -> None:
"""Passes explicit summarization settings through to the middleware."""
model = _make_model(with_profile_limit=120_000)
def token_counter(messages: Iterable[MessageLikeRepresentation]) -> int:
return len(list(messages))
middleware = create_summarization_middleware(
model,
cast("Any", MagicMock()),
summary_prompt="custom summary prompt: {messages}",
trim_tokens_to_summarize=123,
token_counter=token_counter,
)
assert middleware._lc_helper.summary_prompt == "custom summary prompt: {messages}"
assert middleware._lc_helper.trim_tokens_to_summarize == 123
assert middleware._lc_helper.token_counter is token_counter
def test_factory_summarization_knobs_are_keyword_only() -> None:
"""Requires optional factory controls to be passed by name."""
params = signature(create_summarization_middleware).parameters
assert params["summary_prompt"].kind is Parameter.KEYWORD_ONLY
assert params["trim_tokens_to_summarize"].kind is Parameter.KEYWORD_ONLY
assert params["token_counter"].kind is Parameter.KEYWORD_ONLY
def test_factory_rejects_string_model() -> None:
"""Raises `TypeError` when called with a string model name."""
with pytest.raises(TypeError, match="BaseChatModel"):