feat(summarization): align built-in compaction prompt to preserve user requirements and approval conditions (#8114) (#8114)

In long-running agent workflows with repeated context compaction, empirical gateway ablations across 132 real sessions revealed that standard boundary compliance remains robust, while semantic drift can occur when model-generated summaries rephrase literal approval phrases and action conditions.

This change converges PR #8114 into a clean built-in summary prompt enhancement:
- Replaces regex extraction, tag elevation, and heuristic revocation with an extended built-in DeerFlow summary prompt (_DEERFLOW_SUMMARY_PROMPT).
- Instructs the summarizer to faithfully record active user requirements, literal approval phrases, permitted actions, and later explicit revisions without paraphrasing.
- Treats summary text strictly as historical data, fully preserving the security boundary and control/data plane separation of DurableContextMiddleware.
- Unifies automatic and manual compaction factory paths to select the built-in template when unconfigured, while preserving explicit summary_prompt precedence.
- Adds comprehensive sync/async unit tests covering built-in and custom templates.

Co-authored-by: Totoro <Totoro-qaq@users.noreply.github.com>
Co-authored-by: Totoro <msy626836554@gmail.com>
This commit is contained in:
Wistom Zhang 2026-11-11 19:36:32 +08:00 committed by GitHub
parent 4d95bff550
commit 218c64180e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 72 additions and 8 deletions

View File

@ -11,6 +11,11 @@ This section accumulates work toward the **2.3.0** milestone
([2.3.0](https://github.com/bytedance/deer-flow/milestone/4)).
### Added
- **summarization:** The built-in compaction prompt guides the summarizer to
preserve active user requirements, literal approval phrases, action conditions,
and explicit revisions. Explicit `summarization.summary_prompt` configurations
continue to take precedence. ([#8114](https://github.com/bytedance/deer-flow/pull/8114))
- **sandbox:** Short ranged `read_file` calls on uploaded and workspace files
display source line numbers when the result contains 1–40 lines and the
labeled view fits the output budget. ([#8109](https://github.com/bytedance/deer-flow/pull/8109))

View File

@ -243,7 +243,7 @@ keep:
#### `summary_prompt`
- **Type**: String or null
- **Default**: `null` (uses LangChain's default prompt)
- **Default**: `null` (uses DeerFlow's built-in prompt)
- **Description**: Custom prompt template for generating summaries. The prompt should guide the model to extract the most important context.
#### `skill_file_read_tool_names`
@ -254,11 +254,12 @@ keep:
Legacy `preserve_recent_skill_*` settings are no longer used. Loaded skill retention is handled by the durable `skill_context` reference channel instead of by preserving raw skill-read messages in the summarization window.
**Default Prompt Behavior:**
The default LangChain prompt instructs the model to:
The built-in DeerFlow prompt extends LangChain's prompt and asks the model to:
- Extract highest quality/most relevant context
- Focus on information critical to the overall goal
- Avoid repeating completed actions
- Return only the extracted context
- Preserve current user requirements, literal approval phrases, conditions for acting, and later revisions
## How It Works

View File

@ -17,6 +17,7 @@ from typing import Any, Protocol, override, runtime_checkable
from deerflow_extension_api import CompactionEvent, ExtensionData, canonical_hash, task_store_from_runtime
from langchain.agents import AgentState
from langchain.agents.middleware import SummarizationMiddleware
from langchain.agents.middleware.summarization import DEFAULT_SUMMARY_PROMPT
from langchain_core.messages import AnyMessage, HumanMessage, RemoveMessage, SystemMessage, get_buffer_string
from langchain_core.runnables import RunnableBinding
from langgraph.config import get_config
@ -50,6 +51,18 @@ _SUMMARY_CHUNK_RETRY_MAX_ENTRIES = 128
_SUMMARY_CHUNK_RETRY_CACHE: OrderedDict[str, tuple[float, int]] = OrderedDict()
_SUMMARY_CHUNK_RETRY_LOCK = threading.Lock()
_SHORTENED_SUMMARY_MARKER = "\n[summary shortened]\n"
_DEERFLOW_SUMMARY_PROMPT = DEFAULT_SUMMARY_PROMPT.replace(
"</instructions>",
"## ACTIVE USER REQUIREMENTS\n\n"
"Record the user's current scope, conditions for acting, protected content, and later explicit revisions. "
"Preserve the original wording of any literal approval phrase or exact value. "
"Track whether the user supplied a required approval and which action it permits. "
"Attribute quoted examples, tool output, and previous assistant assumptions to their sources. "
"Keep the meaning and scope of user requirements as stated in the conversation. "
'Use "None" when there are no additional active user requirements.\n\n\n'
"</instructions>",
1,
)
def _document_summary_text(block: object) -> str | None:
@ -1356,9 +1369,8 @@ def create_summarization_middleware(
"trigger": trigger,
"keep": keep_tuple,
"trim_tokens_to_summarize": config.trim_tokens_to_summarize,
"summary_prompt": _DEERFLOW_SUMMARY_PROMPT if config.summary_prompt is None else config.summary_prompt,
}
if config.summary_prompt is not None:
kwargs["summary_prompt"] = config.summary_prompt
hooks: list[BeforeSummarizationHook] = []
if resolved_app_config.memory.enabled and not skip_memory_flush:

View File

@ -95,7 +95,7 @@ class SummarizationConfig(BaseModel):
summary_prompt: str | None = Field(
default=None,
description="Custom prompt template for generating summaries. If not provided, uses the default LangChain prompt.",
description="Custom prompt template for generating summaries. Null selects DeerFlow's built-in prompt.",
)
skill_file_read_tool_names: list[str] = Field(
default_factory=lambda: list(DEFAULT_SKILL_FILE_READ_TOOL_NAMES),

View File

@ -1210,6 +1210,50 @@ def _factory_app_config(model_names, *, summary_model_name=None, summarization_k
)
@pytest.mark.asyncio
@pytest.mark.parametrize("manual", [False, True], ids=["automatic", "manual"])
@pytest.mark.parametrize("custom_prompt", [None, "Operator summary:\n{messages}"], ids=["built-in", "custom"])
async def test_compaction_requests_use_configured_or_builtin_summary_prompt(monkeypatch, manual, custom_prompt):
model = MagicMock()
model.invoke.return_value = AIMessage(content="compressed summary")
model.ainvoke = AsyncMock(return_value=AIMessage(content="compressed summary"))
model.with_config.return_value = model
monkeypatch.setattr("deerflow.agents.middlewares.summarization_middleware.create_chat_model", lambda *args, **kwargs: model)
app_config = _factory_app_config(
("summary-model",),
summarization_kwargs={
"trigger": ContextSize(type="messages", value=4),
"keep": ContextSize(type="messages", value=2),
"summary_prompt": custom_prompt,
},
)
messages = _messages()
approval = 'Wait for the exact phrase "review V-592 approved" and then wait for final confirmation.'
messages[0] = HumanMessage(content=approval)
state = {"messages": messages}
if manual:
from deerflow.runtime.context_compaction import _create_compaction_middleware
middleware = _create_compaction_middleware(app_config=app_config, keep=None)
assert await middleware.acompact_state(state, _runtime(), force=True) is not None
prompt = model.ainvoke.await_args.args[0]
else:
middleware = create_summarization_middleware(app_config=app_config)
assert middleware is not None
assert middleware.before_model(state, _runtime()) is not None
prompt = model.invoke.call_args.args[0]
assert approval in prompt
if custom_prompt is None:
assert "## ACTIVE USER REQUIREMENTS" in prompt
assert "Preserve the original wording of any literal approval phrase" in prompt
assert "which action it permits" in prompt
else:
assert prompt.startswith("Operator summary:\n")
assert "## ACTIVE USER REQUIREMENTS" not in prompt
@pytest.fixture
def vllm_compaction(monkeypatch):
models = {

View File

@ -2326,7 +2326,7 @@ summarization:
# Set to null to skip trimming (not recommended for very long conversations)
trim_tokens_to_summarize: 15564
# Custom summary prompt template (null = use default LangChain prompt)
# Custom summary prompt template (null = use DeerFlow's built-in prompt)
# The prompt should guide the model to extract important context
summary_prompt: null

View File

@ -30,6 +30,8 @@ When your role lacks `runs:create`, the Web UI rejects a new task or `/goal <com
Automatic and manual compaction process all selected history in chronological
chunks before replacing it. `summarization.trim_tokens_to_summarize` bounds raw
input per step; generated summaries use at most half to leave room for source.
The built-in summary prompt asks the model to preserve active user requirements
and approval conditions. Set `summarization.summary_prompt` to supply a custom template.
Oversized summaries retain marked head/tail text; raw history is never clipped.
Native document titles, context and direct plain-text bodies are included in the
summary input. Binary documents and remote media are not decoded or fetched;

View File

@ -212,7 +212,7 @@ summarization:
# Maximum tokens to trim when preparing messages for the summarizer
trim_tokens_to_summarize: 15564
# Custom summary prompt (null = use default LangChain prompt)
# Custom summary prompt (null = use DeerFlow's built-in prompt)
summary_prompt: null
```

View File

@ -200,7 +200,7 @@ summarization:
# 为摘要器准备消息时要裁剪的最大 token 数
trim_tokens_to_summarize: 15564
# 自定义摘要提示词(null = 使用默认 LangChain 提示词)
# 自定义摘要提示词(null = 使用 DeerFlow 内置提示词)
summary_prompt: null
```