2026-01-19 16:17:31 +08:00
# Conversation Summarization
DeerFlow includes automatic conversation summarization to handle long conversations that approach model token limits. When enabled, the system automatically condenses older messages while preserving recent context.
2026-07-04 11:27:19 +08:00
New checkpoints no longer use raw task-result or skill-read transcript content to derive durable context. The capture path consumes bounded structured metadata stamped on the corresponding `ToolMessage.additional_kwargs` ; transcript text remains display/model content, not the state-capture protocol.
2026-01-19 16:17:31 +08:00
## Overview
The summarization feature uses LangChain's `SummarizationMiddleware` to monitor conversation history and trigger summarization based on configurable thresholds. When activated, it:
1. Monitors message token counts in real-time
2. Triggers summarization when thresholds are met
3. Keeps recent messages intact while summarizing older exchanges
4. Maintains AI/Tool message pairs together for context continuity
2026-07-01 22:49:17 +08:00
5. Stores the summary in `ThreadState.summary_text` and projects it ephemerally through durable context data
2026-01-19 16:17:31 +08:00
## Configuration
Summarization is configured in `config.yaml` under the `summarization` key:
``` yaml
summarization :
enabled : true
fix(summarization): summarize with the run model, fall back on summary-provider failure (#4361)
* 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.
2026-07-26 07:39:39 +08:00
model_name : null # null = summarize with the run's own model (see below); or name a lightweight model
2026-01-19 16:17:31 +08:00
# Trigger conditions (OR logic - any condition triggers summarization)
trigger :
- type : tokens
value : 4000
# Additional triggers (optional)
# - type: messages
# value: 50
# - type: fraction
# value: 0.8 # 80% of model's max input tokens
# Context retention policy
keep :
type : messages
value : 20
# Token trimming for summarization call
trim_tokens_to_summarize : 4000
# Custom summary prompt (optional)
summary_prompt : null
2026-04-24 15:19:46 +02:00
2026-07-01 22:49:17 +08:00
# Tool names treated as skill file reads for the durable skill_context channel
2026-04-24 15:19:46 +02:00
skill_file_read_tool_names :
- read_file
- read
- view
- cat
2026-01-19 16:17:31 +08:00
```
### Configuration Options
#### `enabled`
- **Type**: Boolean
- **Default**: `false`
- **Description**: Enable or disable automatic summarization
#### `model_name`
- **Type**: String or null
fix(summarization): summarize with the run model, fall back on summary-provider failure (#4361)
* 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.
2026-07-26 07:39:39 +08:00
- **Default**: `null`
- **Description**: Model to use for generating summaries.
- **`null` (model ownership)**: summarize with the model the run actually executes with — the lead run's resolved model, a subagent's own model, or a thread's custom-agent model — **not ** `config.models[0]` . This keeps compaction working on a run whose model is healthy even when `models[0]` 's provider is broken (expired key, quota, outage).
- **Set to a model name**: that model generates summaries. If its provider fails, compaction **falls back to the run's own model ** so a broken summary provider cannot disable compaction while a working model is available. Recommended to use a lightweight, cost-effective model like `gpt-4o-mini` or equivalent.
- Ownership applies to all three paths — automatic lead compaction, subagent compaction, and manual `/compact` . Manual `/compact` resolves the run model with the same precedence as a normal run: the model selected for the request (`POST /api/threads/{id}/compact` body `model_name` , sent by the frontend from the composer's current model) → the thread's custom-agent model → the default. A whitespace-only summary response is treated as a generation failure (it is never committed as a valid empty summary).
2026-01-19 16:17:31 +08:00
#### `trigger`
- **Type**: Single `ContextSize` or list of `ContextSize` objects
- **Required**: At least one trigger must be specified when enabled
- **Description**: Thresholds that trigger summarization. Uses OR logic - summarization runs when ANY threshold is met.
**ContextSize Types: **
1. **Token-based trigger ** : Activates when token count reaches the specified value
```yaml
trigger:
type: tokens
value: 4000
` ``
2. **Message-based trigger**: Activates when message count reaches the specified value
` ``yaml
trigger:
type: messages
value: 50
` ``
3. **Fraction-based trigger**: Activates when token usage reaches a percentage of the model's maximum input tokens
` ``yaml
trigger:
type: fraction
value: 0.8 # 80% of max input tokens
` ``
fix(summarization): stop fraction triggers from crashing the agent build (#4901)
* fix(summarization): resolve fraction triggers from declared context_window, degrade instead of crashing the agent build
A fraction trigger/keep clause requires profile["max_input_tokens"], which any
third-party OpenAI-compatible model lacks, so SummarizationMiddleware
construction raised ValueError out of create_summarization_middleware and failed
the whole agent build (#3103).
- factory: translate a declared model context_window into the langchain
profile (metadata-only, never reaches the provider payload); explicit
caller/override profiles win
- summarization factory: drop unusable fraction trigger clauses (absolute
clauses survive), fall a fraction keep back to the messages default, and
disable compaction with an actionable warning only when no usable trigger
clause remains — the agent build never dies from summarization config
- docs: config.example.yaml, ModelConfig.context_window, summarization.md
* refactor(summarization): share the default keep constant with the fraction fallback
The fraction-keep degradation fallback hardcoded ("messages", 20),
duplicating SummarizationConfig.keep's default_factory literal. Move the
value to a shared DEFAULT_KEEP constant so the two cannot drift apart.
* fix(summarization): keep trigger-null + fraction-keep constructing after degradation
A trigger of None with a fraction keep hit the all-clauses-dropped branch
(has_usable_trigger=False) and disabled compaction, and the accompanying
warning claimed configured triggers were all fraction-based when none were
configured. Only report nothing-usable when trigger clauses actually
existed; trigger:null keeps constructing the never-firing middleware with
the degraded keep, matching its behavior outside the degradation path.
* fix(summarization): address review — keep manual compaction, validate ContextSize, pin wiring
Review follow-ups on #4901:
- When every configured trigger is a dropped fraction clause, keep
constructing the never-firing middleware (trigger=None) instead of
returning None: manual /compact runs with force=True and never consults
trigger clauses, so it must keep working for a profile-less model
rather than reporting 'compaction is disabled'. The warning now says
auto-compaction will not fire while manual compaction remains.
- ContextSize gains a config-load validator: fraction values must be in
(0,1] (a percent-style 80 instead of 0.8 previously produced a threshold
the context could never reach — a silently inert trigger), absolute
values must be positive.
- New un-monkeypatched integration test pins the shipped wiring
(context_window declared -> real factory attaches profile -> fraction
clause survives -> middleware constructs), which the stubbed
middleware-side tests and kwarg-capturing factory-side tests each
stopped short of.
- Docs (summarization.md + config.example.yaml) clarify that the fraction
resolves against the summary/anchor model's context_window
(summarization.model_name when set, else the run model), including the
mismatch caveat for a larger-window summary model.
* fix(summarization): reject non-finite ContextSize values at config load
YAML .nan / .inf pass pydantic's float parsing, and nan <= 0 is False,
so the positivity check alone let them through as dead thresholds
(count >= nan is always False) — the same silent-inert-trigger class the
range validator was added to close. Guard with math.isfinite first,
consistent with the existing non-finite guards on mem0 timeout_seconds
and poll_after_seconds.
* fix(summarization): merge context_window into inferred profile, require whole message counts
- construct the model first, then merge max_input_tokens into the
provider-inferred langchain profile: passing profile= to the
constructor replaced the whole inferred metadata (tool_calling,
structured_output, io capabilities, output limits) with the single
key. An explicitly configured profile is still never clobbered.
- reject non-integral ContextSize values for type=messages at config
load: langchain slices the message list with them, so a float index
raised TypeError mid-compaction.
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-09-03 17:05:09 +08:00
The percentage resolves from the **summary model's** declared ` context_window`
— the anchor that generates summaries: ` summarization.model_name` when set,
otherwise the run's own model. Declare ` context_window` on that models entry
in ` config.yaml`. Third-party OpenAI-compatible models carry no built-in
capacity profile, so without a declared ` context_window` the fraction clause
is dropped with a warning at agent build — any remaining absolute clauses
(` tokens` / ` messages`) keep working. Caveat: when a separate summary model
is configured, its window sizes the threshold — a 64k run model paired with
a 128k-window summary model resolves ` fraction: 0.8` to ~102k tokens and
auto-summarization cannot fire before the run model overflows; in that setup
prefer absolute ` tokens` thresholds sized for the run model.
2026-01-19 16:17:31 +08:00
**Multiple Triggers:**
` ``yaml
trigger:
- type: tokens
value: 4000
- type: messages
value: 50
` ``
#### ` keep`
- **Type**: ` ContextSize` object
- **Default**: ` {type: messages, value: 20}`
- **Description**: Specifies how much recent conversation history to preserve after summarization.
**Examples:**
` ``yaml
# Keep most recent 20 messages
keep:
type: messages
value: 20
# Keep most recent 3000 tokens
keep:
type: tokens
value: 3000
# Keep most recent 30% of model's max input tokens
keep:
type: fraction
value: 0.3
` ``
#### ` trim_tokens_to_summarize`
- **Type**: Integer or null
- **Default**: ` 4000`
2026-09-08 01:00:45 -07:00
- **Description**: Token budget used to trim the raw input sections for the summarization call. Escaping, wrapper tags, and the summary prompt add overhead beyond this budget; it is not a hard limit on the final model request. When preserving the current user request leaves an assistant/tool-only summary window, trimming favors the most recent content in that window. If a mixed window still contains a human message but the human-anchored trim is empty, the existing final-message fallback is preserved. Set to ` null` to skip trimming (not recommended for very long conversations).
2026-01-19 16:17:31 +08:00
#### ` summary_prompt`
- **Type**: String or null
- **Default**: ` null` (uses LangChain's default prompt)
- **Description**: Custom prompt template for generating summaries. The prompt should guide the model to extract the most important context.
2026-04-24 15:19:46 +02:00
#### ` skill_file_read_tool_names`
- **Type**: List of strings
- **Default**: ` ["read_file", "read", "view", "cat"]`
2026-07-01 22:49:17 +08:00
- **Description**: Tool names treated as skill file reads when ` DurableContextMiddleware` captures loaded skills into the checkpointed ` skill_context` channel. A tool call is captured only when its name appears in this list and its target path is under ` skills.container_path`. Set this list to ` []` to disable durable skill-reference capture.
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.
2026-04-24 15:19:46 +02:00
2026-01-19 16:17:31 +08:00
**Default Prompt Behavior:**
The default LangChain prompt instructs 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
## How It Works
### Summarization Flow
2026-07-01 22:49:17 +08:00
1. **Monitoring**: Before each model call, the middleware counts tokens in the message history plus the existing ` summary_text`, because both are projected into the next model request
2026-01-19 16:17:31 +08:00
2. **Trigger Check**: If any configured threshold is met, summarization is triggered
3. **Message Partitioning**: Messages are split into:
- Messages to summarize (older messages beyond the ` keep` threshold)
- Messages to preserve (recent messages within the ` keep` threshold)
4. **Summary Generation**: The model generates a concise summary of the older messages
5. **Context Replacement**: The message history is updated:
- All old messages are removed
- Recent messages are preserved
2026-07-01 22:49:17 +08:00
- The generated prose summary is stored in ` summary_text`
2026-01-19 16:17:31 +08:00
6. **AI/Tool Pair Protection**: The system ensures AI messages and their corresponding tool messages stay together
2026-07-04 11:27:19 +08:00
7. **Skill context channel**: Skill files read during the conversation (tool calls whose name is in ` skill_file_read_tool_names` and whose path is under ` skills.container_path`, narrowed to ` .../SKILL.md`) are stamped with ` skill_context_entry` metadata at the read-tool boundary, then captured by ` DurableContextMiddleware` into the checkpointed ` skill_context` channel as references: ` name`, ` path`, a one-line ` description` parsed in-memory from the file's frontmatter, and ` loaded_at`, deduped by path. On every model call they are rendered into a hidden durable-context data message as a compact "active skills" reminder that points at each ` SKILL.md` for on-demand re-read, so which skills are active survives summarization without persisting or re-injecting the verbatim body. The channel keeps the most recently read skills (cap ` _SKILL_CONTEXT_MAX_ENTRIES`; re-reading an existing skill refreshes its recency); sessions typically load only 1-3.
2026-01-19 16:17:31 +08:00
### Token Counting
- Uses approximate token counting based on character count
- For Anthropic models: ~3.3 characters per token
- For other models: Uses LangChain's default estimation
- Can be customized with a custom ` token_counter` function
### Message Preservation
The middleware intelligently preserves message context:
- **Recent Messages**: Always kept intact based on ` keep` configuration
- **AI/Tool Pairs**: Never split - if a cutoff point falls within tool messages, the system adjusts to keep the entire AI + Tool message sequence together
2026-07-01 22:49:17 +08:00
- **Summary Format**: Summary prose is stored in ` summary_text` and rendered into an ephemeral hidden durable-context data message. Static handling rules live in a separate system message; summary text and other user/tool/model-derived values stay in the lower-authority data message.
2026-01-19 16:17:31 +08:00
` ``
2026-07-01 22:49:17 +08:00
<durable_context_data>
## Conversation summary so far
2026-01-19 16:17:31 +08:00
[Generated summary text]
2026-07-01 22:49:17 +08:00
</durable_context_data>
2026-01-19 16:17:31 +08:00
` ``
## Best Practices
### Choosing Trigger Thresholds
1. **Token-based triggers**: Recommended for most use cases
- Set to 60-80% of your model's context window
- Example: For 8K context, use 4000-6000 tokens
2. **Message-based triggers**: Useful for controlling conversation length
- Good for applications with many short messages
- Example: 50-100 messages depending on average message length
3. **Fraction-based triggers**: Ideal when using multiple models
- Automatically adapts to each model's capacity
- Example: 0.8 (80% of model's max input tokens)
### Choosing Retention Policy (` keep`)
1. **Message-based retention**: Best for most scenarios
- Preserves natural conversation flow
- Recommended: 15-25 messages
2. **Token-based retention**: Use when precise control is needed
- Good for managing exact token budgets
- Recommended: 2000-4000 tokens
3. **Fraction-based retention**: For multi-model setups
- Automatically scales with model capacity
- Recommended: 0.2-0.4 (20-40% of max input)
### Model Selection
- **Recommended**: Use a lightweight, cost-effective model for summaries
- Examples: ` gpt-4o-mini`, ` claude-haiku`, or equivalent
- Summaries don't require the most powerful models
- Significant cost savings on high-volume applications
fix(summarization): summarize with the run model, fall back on summary-provider failure (#4361)
* 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.
2026-07-26 07:39:39 +08:00
- **Default**: If ` model_name` is ` null`, summarizes with the run's own model (not ` models[0]`)
- Keeps compaction working when ` models[0]`'s provider is broken but the run's model is healthy
- Good for simple setups; no separate summary provider to keep credentialed
2026-01-19 16:17:31 +08:00
### Optimization Tips
1. **Balance triggers**: Combine token and message triggers for robust handling
` ``yaml
trigger:
- type: tokens
value: 4000
- type: messages
value: 50
` ``
2. **Conservative retention**: Keep more messages initially, adjust based on performance
` ``yaml
keep:
type: messages
value: 25 # Start higher, reduce if needed
` ``
3. **Trim strategically**: Limit tokens sent to summarization model
` ``yaml
trim_tokens_to_summarize: 4000 # Prevents expensive summarization calls
` ``
4. **Monitor and iterate**: Track summary quality and adjust configuration
## Troubleshooting
### Summary Quality Issues
**Problem**: Summaries losing important context
**Solutions**:
1. Increase ` keep` value to preserve more messages
2. Decrease trigger thresholds to summarize earlier
3. Customize ` summary_prompt` to emphasize key information
4. Use a more capable model for summarization
### Performance Issues
**Problem**: Summarization calls taking too long
**Solutions**:
1. Use a faster model for summaries (e.g., ` gpt-4o-mini`)
2. Reduce ` trim_tokens_to_summarize` to send less context
3. Increase trigger thresholds to summarize less frequently
### Token Limit Errors
**Problem**: Still hitting token limits despite summarization
**Solutions**:
1. Lower trigger thresholds to summarize earlier
2. Reduce ` keep` value to preserve fewer messages
3. Check if individual messages are very large
4. Consider using fraction-based triggers
## Implementation Details
### Code Structure
refactor: split backend into harness (deerflow.*) and app (app.*) (#1131)
* refactor: extract shared utils to break harness→app cross-layer imports
Move _validate_skill_frontmatter to src/skills/validation.py and
CONVERTIBLE_EXTENSIONS + convert_file_to_markdown to src/utils/file_conversion.py.
This eliminates the two reverse dependencies from client.py (harness layer)
into gateway/routers/ (app layer), preparing for the harness/app package split.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: split backend/src into harness (deerflow.*) and app (app.*)
Physically split the monolithic backend/src/ package into two layers:
- **Harness** (`packages/harness/deerflow/`): publishable agent framework
package with import prefix `deerflow.*`. Contains agents, sandbox, tools,
models, MCP, skills, config, and all core infrastructure.
- **App** (`app/`): unpublished application code with import prefix `app.*`.
Contains gateway (FastAPI REST API) and channels (IM integrations).
Key changes:
- Move 13 harness modules to packages/harness/deerflow/ via git mv
- Move gateway + channels to app/ via git mv
- Rename all imports: src.* → deerflow.* (harness) / app.* (app layer)
- Set up uv workspace with deerflow-harness as workspace member
- Update langgraph.json, config.example.yaml, all scripts, Docker files
- Add build-system (hatchling) to harness pyproject.toml
- Add PYTHONPATH=. to gateway startup commands for app.* resolution
- Update ruff.toml with known-first-party for import sorting
- Update all documentation to reflect new directory structure
Boundary rule enforced: harness code never imports from app.
All 429 tests pass. Lint clean.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* chore: add harness→app boundary check test and update docs
Add test_harness_boundary.py that scans all Python files in
packages/harness/deerflow/ and fails if any `from app.*` or
`import app.*` statement is found. This enforces the architectural
rule that the harness layer never depends on the app layer.
Update CLAUDE.md to document the harness/app split architecture,
import conventions, and the boundary enforcement test.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add config versioning with auto-upgrade on startup
When config.example.yaml schema changes, developers' local config.yaml
files can silently become outdated. This adds a config_version field and
auto-upgrade mechanism so breaking changes (like src.* → deerflow.*
renames) are applied automatically before services start.
- Add config_version: 1 to config.example.yaml
- Add startup version check warning in AppConfig.from_file()
- Add scripts/config-upgrade.sh with migration registry for value replacements
- Add `make config-upgrade` target
- Auto-run config-upgrade in serve.sh and start-daemon.sh before starting services
- Add config error hints in service failure messages
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix comments
* fix: update src.* import in test_sandbox_tools_security to deerflow.*
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: handle empty config and search parent dirs for config.example.yaml
Address Copilot review comments on PR #1131:
- Guard against yaml.safe_load() returning None for empty config files
- Search parent directories for config.example.yaml instead of only
looking next to config.yaml, fixing detection in common setups
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: correct skills root path depth and config_version type coercion
- loader.py: fix get_skills_root_path() to use 5 parent levels (was 3)
after harness split, file lives at packages/harness/deerflow/skills/
so parent×3 resolved to backend/packages/harness/ instead of backend/
- app_config.py: coerce config_version to int() before comparison in
_check_config_version() to prevent TypeError when YAML stores value
as string (e.g. config_version: "1")
- tests: add regression tests for both fixes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: update test imports from src.* to deerflow.*/app.* after harness refactor
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 22:55:52 +08:00
- **Configuration**: ` packages/harness/deerflow/config/summarization_config.py`
- **Integration**: ` packages/harness/deerflow/agents/lead_agent/agent.py`
2026-01-19 16:17:31 +08:00
- **Middleware**: Uses ` langchain.agents.middleware.SummarizationMiddleware`
### Middleware Order
2026-07-01 22:49:17 +08:00
Durable context capture runs before summarization so task delegations and
loaded skill references are recorded before their raw tool messages can be
compacted. It records in-progress dispatches as well as terminal result
summaries. Summarization then reduces message history before downstream
middlewares such as title generation, memory queuing, and clarification:
2026-01-19 16:17:31 +08:00
2026-07-01 22:49:17 +08:00
1. Runtime middlewares, including ThreadData and Sandbox initialization
2. DynamicContextMiddleware
3. SkillActivationMiddleware
4. DurableContextMiddleware
5. **SummarizationMiddleware** ← Runs here
6. Downstream lead middlewares such as Title, Memory, and Clarification
2026-01-19 16:17:31 +08:00
### State Management
2026-07-01 22:49:17 +08:00
- Summarization configuration is loaded from ` config.yaml`
- Generated summaries are stored in ` ThreadState.summary_text`, not as regular ` messages`
- The message reducer removes compacted raw messages while the checkpointer persists ` summary_text`
- DurableContextMiddleware projects ` summary_text` back into later model calls as hidden durable context data
2026-01-19 16:17:31 +08:00
## Example Configurations
### Minimal Configuration
` ``yaml
summarization:
enabled: true
trigger:
type: tokens
value: 4000
keep:
type: messages
value: 20
` ``
### Production Configuration
` ``yaml
summarization:
enabled: true
model_name: gpt-4o-mini # Lightweight model for cost efficiency
trigger:
- type: tokens
value: 6000
- type: messages
value: 75
keep:
type: messages
value: 25
trim_tokens_to_summarize: 5000
` ``
### Multi-Model Configuration
` ``yaml
summarization:
enabled: true
model_name: gpt-4o-mini
trigger:
type: fraction
value: 0.7 # 70% of model's max input
keep:
type: fraction
value: 0.3 # Keep 30% of max input
trim_tokens_to_summarize: 4000
` ``
### Conservative Configuration (High Quality)
` ``yaml
summarization:
enabled: true
model_name: gpt-4 # Use full model for high-quality summaries
trigger:
type: tokens
value: 8000
keep:
type: messages
value: 40 # Keep more context
trim_tokens_to_summarize: null # No trimming
` ``
## References
- [LangChain Summarization Middleware Documentation ](https://docs.langchain.com/oss/python/langchain/middleware/built-in#summarization )
- [LangChain Source Code ](https://github.com/langchain-ai/langchain )