mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-07-29 01:15:59 +00:00
* fix(summarization): own the run model for compaction; bound failure With summarization.model_name: null the summary model resolved to config.models[0] while the executing model is selected per run; when they differ and models[0]'s provider is broken (expired key, quota, outage) compaction silently failed every triggered turn and context grew unbounded until the main provider 400s the run (#3103's shape), even though the run's own model was healthy. Model ownership is now sourced from the builders, not re-derived at runtime: - The lead, subagent, and manual /compact builders each pass the resolved run model into create_summarization_middleware(run_model_name=...). The middleware no longer reads runtime.context / get_config(), which do not carry a custom agent's or a subagent's resolved model, so a custom-agent lead run and a distinct-model subagent now summarize with their own model, not models[0] / the parent's. Runtime re-resolution and the per-name model cache are removed. - model_name: null summarizes with the run's own model; an explicitly configured summary model generates and falls back to the run model on failure. The fallback is built lazily after the primary fails and its construction is guarded, so a broken fallback cannot skip a healthy primary or escape the automatic failure boundary. Failure is bounded and side-effect-safe: - An empty or whitespace-only response is treated as a generation failure, not a valid summary, so compaction never removes all history for an empty replacement. - compact_state/acompact_state take raise_on_failure independent of force: the manual /compact path always surfaces a generation failure (even force=false) and routes it to the existing ContextCompactionFailed path (HTTP 500 -> frontend error toast) instead of an unconsumed response reason. The automatic path leaves compaction state unchanged. - before_summarization hooks fire only after a replacement summary exists. SummarizationConfig.model_name, config.example.yaml, and docs/summarization.md document the final lead/subagent/manual ownership rules. Part of RFC #4346 (section A). Evaluating fraction/triggers against the run model's profile (profile ownership) is a separate follow-up. * fix(summarization): manual /compact model ownership + fail-open construct/parse Manual /compact carried only agent_name, so it derived the run model from the custom-agent model or config.models[0] and missed the request-selected model the run path uses (request -> custom-agent -> default). Carry model_name through ThreadCompactRequest and the frontend compact call, resolve with the same precedence, and move the custom-agent config read off the event loop (asyncio .to_thread) with user_id so the strict blocking-IO gate is not bypassed by the broad except. Make one summary attempt own its full lifecycle so the fail-open boundary covers construction and response parsing, not just invocation: build each candidate model lazily and guarded (a raising constructor falls through to the healthy run model instead of breaking agent construction), build the model_name:null primary from the run model rather than config.models[0], and run response text extraction inside the invocation try so a failing .text accessor falls back instead of escaping compaction. Adds factory-level constructor-failure, response-extraction-failure (sync/async), and route-path model-ownership tests.
83 lines
3.5 KiB
Python
83 lines
3.5 KiB
Python
"""Configuration for conversation summarization."""
|
|
|
|
from typing import Literal
|
|
|
|
from pydantic import BaseModel, Field
|
|
|
|
ContextSizeType = Literal["fraction", "tokens", "messages"]
|
|
DEFAULT_SKILL_FILE_READ_TOOL_NAMES: tuple[str, ...] = ("read_file", "read", "view", "cat")
|
|
|
|
|
|
class ContextSize(BaseModel):
|
|
"""Context size specification for trigger or keep parameters."""
|
|
|
|
type: ContextSizeType = Field(description="Type of context size specification")
|
|
value: int | float = Field(description="Value for the context size specification")
|
|
|
|
def to_tuple(self) -> tuple[ContextSizeType, int | float]:
|
|
"""Convert to tuple format expected by SummarizationMiddleware."""
|
|
return (self.type, self.value)
|
|
|
|
|
|
class SummarizationConfig(BaseModel):
|
|
"""Configuration for automatic conversation summarization."""
|
|
|
|
enabled: bool = Field(
|
|
default=False,
|
|
description="Whether to enable automatic conversation summarization",
|
|
)
|
|
model_name: str | None = Field(
|
|
default=None,
|
|
description="Model name to use for summarization. None = summarize with the model the run "
|
|
"actually executes with (the lead run's model, a subagent's own model, or a thread's "
|
|
"custom-agent model), not config.models[0]. When set, that model generates and the run's "
|
|
"own model is used as a fallback if the configured summary provider fails.",
|
|
)
|
|
trigger: ContextSize | list[ContextSize] | None = Field(
|
|
default=None,
|
|
description="One or more thresholds that trigger summarization. When any threshold is met, summarization runs. "
|
|
"Examples: {'type': 'messages', 'value': 50} triggers at 50 messages, "
|
|
"{'type': 'tokens', 'value': 4000} triggers at 4000 tokens, "
|
|
"{'type': 'fraction', 'value': 0.8} triggers at 80% of model's max input tokens",
|
|
)
|
|
keep: ContextSize = Field(
|
|
default_factory=lambda: ContextSize(type="messages", value=20),
|
|
description="Context retention policy after summarization. Specifies how much history to preserve. "
|
|
"Examples: {'type': 'messages', 'value': 20} keeps 20 messages, "
|
|
"{'type': 'tokens', 'value': 3000} keeps 3000 tokens, "
|
|
"{'type': 'fraction', 'value': 0.3} keeps 30% of model's max input tokens",
|
|
)
|
|
trim_tokens_to_summarize: int | None = Field(
|
|
default=4000,
|
|
description="Maximum tokens to keep when preparing messages for summarization. Pass null to skip trimming.",
|
|
)
|
|
summary_prompt: str | None = Field(
|
|
default=None,
|
|
description="Custom prompt template for generating summaries. If not provided, uses the default LangChain prompt.",
|
|
)
|
|
skill_file_read_tool_names: list[str] = Field(
|
|
default_factory=lambda: list(DEFAULT_SKILL_FILE_READ_TOOL_NAMES),
|
|
description="Tool names treated as skill-file reads when capturing loaded skills into the durable skill_context channel.",
|
|
)
|
|
|
|
|
|
# Global configuration instance
|
|
_summarization_config: SummarizationConfig = SummarizationConfig()
|
|
|
|
|
|
def get_summarization_config() -> SummarizationConfig:
|
|
"""Get the current summarization configuration."""
|
|
return _summarization_config
|
|
|
|
|
|
def set_summarization_config(config: SummarizationConfig) -> None:
|
|
"""Set the summarization configuration."""
|
|
global _summarization_config
|
|
_summarization_config = config
|
|
|
|
|
|
def load_summarization_config_from_dict(config_dict: dict) -> None:
|
|
"""Load summarization configuration from a dictionary."""
|
|
global _summarization_config
|
|
_summarization_config = SummarizationConfig(**config_dict)
|