mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-09-09 13:39:26 +00:00
docs: update middleware contribution examples (#4945)
* docs: update middleware contribution examples * docs: clarify middleware registration paths * docs: clarify middleware injection scope * docs: clarify middleware state updates * docs: clarify middleware state updates * docs: clarify middleware pipeline placement * docs: complete middleware order guidance * docs: align middleware guard conditions * docs(middleware): name runtime sanitization order * docs: pin middleware runtime order * docs: clarify middleware assembly paths * docs: clarify middleware anchor scope
This commit is contained in:
parent
383263bd34
commit
364dad06aa
@ -165,8 +165,8 @@ feat: add support for Claude 3.5 model
|
||||
- Update model factory to handle Claude-specific settings
|
||||
- Add tests for new model
|
||||
```
|
||||
|
||||
Prefix types:
|
||||
|
||||
- `feat:` - New feature
|
||||
- `fix:` - Bug fix
|
||||
- `docs:` - Documentation
|
||||
@ -284,22 +284,57 @@ class MyMiddleware(AgentMiddleware[AgentState]):
|
||||
"""Middleware description."""
|
||||
|
||||
def before_model(self, state: AgentState, runtime: Runtime) -> dict | None:
|
||||
"""Runs before each model call. Return a dict of state updates, or None."""
|
||||
"""Run before each model call."""
|
||||
print(f"Model input contains {len(state.get('messages', []))} messages")
|
||||
return None
|
||||
|
||||
def after_model(self, state: AgentState, runtime: Runtime) -> dict | None:
|
||||
"""Runs after each model call. Inspect or modify the result."""
|
||||
"""Run after each model call."""
|
||||
messages = state.get("messages", [])
|
||||
last_message = messages[-1] if messages else None
|
||||
print(f"Last message type: {type(last_message).__name__ if last_message else 'none'}")
|
||||
return None
|
||||
```
|
||||
|
||||
2. Register via `custom_middlewares` when building the agent:
|
||||
Lifecycle hooks can return a dictionary of state updates, which LangChain merges
|
||||
into the agent state, or `None` when they only observe state.
|
||||
|
||||
```python
|
||||
middlewares = build_middlewares(
|
||||
config, model_name, custom_middlewares=[MyMiddleware()], ...
|
||||
)
|
||||
2. Register the zero-argument middleware class in `config.yaml`:
|
||||
|
||||
```yaml
|
||||
extensions:
|
||||
middlewares:
|
||||
- deerflow.agents.middlewares.my_middleware:MyMiddleware
|
||||
```
|
||||
|
||||
Configured middleware runs after the built-in middleware and optional loop/token
|
||||
guards. On the lead-agent pipeline, it runs before the terminal-response,
|
||||
model-length, safety, and clarification tail; subagents have no
|
||||
terminal-response, model-length, or clarification stage, so configured middleware is
|
||||
followed by the optional safety guard, `DurableContextMiddleware`, optional
|
||||
`SummarizationMiddleware`, then `SubagentDateContextMiddleware` and
|
||||
`SystemMessageCoalescingMiddleware`. Treat middleware class paths as trusted
|
||||
operator configuration because loading one executes Python code.
|
||||
Embedded callers can instead use `DeerFlowClient(middlewares=[...])`, which
|
||||
builds the full lead-agent chain and places middleware before its
|
||||
terminal-response, model-length, safety, and clarification tail.
|
||||
`create_deerflow_agent(extra_middleware=[...])` instead builds a smaller
|
||||
feature-based lead-agent chain; unanchored extras are placed immediately before
|
||||
`ClarificationMiddleware` (anchored extras follow their `@Next`/`@Prev`
|
||||
placement, but the anchor must be present in this smaller chain). Neither API
|
||||
forwards middleware to subagents.
|
||||
|
||||
Choose the registration path by ownership and placement. The fixed-slot (not deprecated)
|
||||
`extensions.middlewares` list is accepted in `config.yaml` and
|
||||
`extensions_config.json` (`config.yaml` wins) and applies to both lead and
|
||||
subagent pipelines. Packaged extensions registered through the top-level
|
||||
`plugins:` list contribute middleware at semantic extension points. Contributor
|
||||
code that needs committed, programmatic lead-only wiring can use
|
||||
`build_middlewares(..., custom_middlewares=[MyMiddleware()])` at the
|
||||
`build_middlewares` call in
|
||||
`packages/harness/deerflow/agents/lead_agent/agent.py` (reached through
|
||||
`make_lead_agent`).
|
||||
|
||||
### Adding New API Endpoints
|
||||
|
||||
1. Create router in `app/gateway/routers/`:
|
||||
|
||||
@ -312,7 +312,7 @@ class ExtensionsConfig(BaseModel):
|
||||
|
||||
middlewares: list[str] = Field(
|
||||
default_factory=list,
|
||||
description="AgentMiddleware class paths loaded into the lead-agent middleware chain. Each entry uses 'module.path:ClassName'.",
|
||||
description="AgentMiddleware class paths loaded into the lead-agent and subagent middleware chains. Each entry uses 'module.path:ClassName'.",
|
||||
)
|
||||
mcp_servers: dict[str, McpServerConfig] = Field(
|
||||
default_factory=dict,
|
||||
|
||||
194
backend/tests/test_middleware_documentation.py
Normal file
194
backend/tests/test_middleware_documentation.py
Normal file
@ -0,0 +1,194 @@
|
||||
"""Keep documented middleware examples aligned with the locked LangChain API."""
|
||||
|
||||
import inspect
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from langchain.agents.middleware import AgentMiddleware
|
||||
|
||||
from deerflow.agents import create_deerflow_agent
|
||||
from deerflow.client import DeerFlowClient
|
||||
from deerflow.config.extensions_config import ExtensionsConfig
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
MIDDLEWARE_GUIDES = (
|
||||
Path("backend/CONTRIBUTING.md"),
|
||||
Path("frontend/src/content/en/harness/customization.mdx"),
|
||||
Path("frontend/src/content/en/harness/middlewares.mdx"),
|
||||
Path("frontend/src/content/zh/harness/customization.mdx"),
|
||||
Path("frontend/src/content/zh/harness/middlewares.mdx"),
|
||||
)
|
||||
|
||||
|
||||
def _middleware_examples(path: Path) -> list[str]:
|
||||
content = (REPO_ROOT / path).read_text(encoding="utf-8")
|
||||
examples = [block for block in re.findall(r"```python\n(.*?)\n```", content, flags=re.DOTALL) if "AgentMiddleware" in block and ("class MyMiddleware" in block or "class AuditMiddleware" in block)]
|
||||
assert examples, f"no custom middleware example in {path}"
|
||||
return examples
|
||||
|
||||
|
||||
@pytest.mark.parametrize("path", MIDDLEWARE_GUIDES, ids=str)
|
||||
def test_custom_middleware_example_uses_current_lifecycle_hooks(path: Path) -> None:
|
||||
for example in _middleware_examples(path):
|
||||
namespace: dict[str, object] = {}
|
||||
exec(compile(example, str(path), "exec"), namespace) # noqa: S102 - executes a controlled in-repo documentation example
|
||||
|
||||
middleware_types = [value for value in namespace.values() if isinstance(value, type) and value is not AgentMiddleware and issubclass(value, AgentMiddleware)]
|
||||
assert len(middleware_types) == 1
|
||||
|
||||
middleware_type = middleware_types[0]
|
||||
assert middleware_type.before_model is not AgentMiddleware.before_model
|
||||
assert middleware_type.after_model is not AgentMiddleware.after_model
|
||||
|
||||
middleware = middleware_type()
|
||||
assert middleware.before_model({"messages": []}, None) is None
|
||||
assert middleware.after_model({"messages": []}, None) is None
|
||||
|
||||
|
||||
def test_documented_registration_apis_exist() -> None:
|
||||
ExtensionsConfig.model_validate({"middlewares": ["pkg.mod:MyMiddleware"]})
|
||||
assert "middlewares" in inspect.signature(DeerFlowClient.__init__).parameters
|
||||
assert "extra_middleware" in inspect.signature(create_deerflow_agent).parameters
|
||||
|
||||
|
||||
@pytest.mark.parametrize("path", MIDDLEWARE_GUIDES, ids=str)
|
||||
def test_embedded_middleware_scope_is_explicit(path: Path) -> None:
|
||||
content = (REPO_ROOT / path).read_text(encoding="utf-8")
|
||||
markers = (
|
||||
(
|
||||
"DeerFlowClient(middlewares=[",
|
||||
"builds the full lead-agent chain",
|
||||
"create_deerflow_agent(extra_middleware=[",
|
||||
"builds a smaller feature-based lead-agent chain",
|
||||
"Neither API forwards middleware to subagents.",
|
||||
)
|
||||
if "/zh/" not in path.as_posix()
|
||||
else (
|
||||
"DeerFlowClient(middlewares=[",
|
||||
"构建完整的主 Agent 链",
|
||||
"create_deerflow_agent(extra_middleware=[",
|
||||
"构建较小的按功能组装的主 Agent 链",
|
||||
"两个 API 均不会将中间件转发给子 Agent。",
|
||||
)
|
||||
)
|
||||
normalized = " ".join(content.split())
|
||||
positions = [normalized.index(marker) for marker in markers]
|
||||
assert positions == sorted(positions)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("path", MIDDLEWARE_GUIDES, ids=str)
|
||||
def test_lifecycle_return_contract_is_explicit(path: Path) -> None:
|
||||
content = (REPO_ROOT / path).read_text(encoding="utf-8")
|
||||
marker = "生命周期钩子可以返回状态更新字典" if "/zh/" in path.as_posix() else "Lifecycle hooks can return a dictionary of state updates"
|
||||
assert marker in " ".join(content.split())
|
||||
|
||||
|
||||
@pytest.mark.parametrize("path", MIDDLEWARE_GUIDES, ids=str)
|
||||
def test_middleware_placement_scope_is_explicit(path: Path) -> None:
|
||||
content = (REPO_ROOT / path).read_text(encoding="utf-8")
|
||||
marker = (
|
||||
"对于主 Agent 链,它位于终态响应、模型长度、安全和澄清尾部之前;子 Agent 链没有终态响应、模型长度或澄清阶段"
|
||||
if "/zh/" in path.as_posix()
|
||||
else "On the lead-agent pipeline, it runs before the terminal-response, model-length, safety, and clarification tail; subagents have no terminal-response, model-length, or clarification stage"
|
||||
)
|
||||
assert marker in " ".join(content.split())
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
(
|
||||
Path("frontend/src/content/en/harness/middlewares.mdx"),
|
||||
Path("frontend/src/content/zh/harness/middlewares.mdx"),
|
||||
),
|
||||
ids=str,
|
||||
)
|
||||
def test_middleware_order_includes_configured_extension_tail(path: Path) -> None:
|
||||
content = " ".join((REPO_ROOT / path).read_text(encoding="utf-8").split())
|
||||
markers = (
|
||||
(
|
||||
"`SkillToolPolicyMiddleware`",
|
||||
"Configured extension middlewares (if any)",
|
||||
"`TerminalResponseMiddleware`",
|
||||
"`ModelLengthFinishReasonMiddleware`",
|
||||
)
|
||||
if "/en/" in path.as_posix()
|
||||
else (
|
||||
"`SkillToolPolicyMiddleware`",
|
||||
"配置的扩展中间件(如有)",
|
||||
"`TerminalResponseMiddleware`",
|
||||
"`ModelLengthFinishReasonMiddleware`",
|
||||
)
|
||||
)
|
||||
positions = [content.index(marker) for marker in markers]
|
||||
assert positions == sorted(positions)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("path", MIDDLEWARE_GUIDES, ids=str)
|
||||
def test_subagent_summarization_optionality_is_explicit(path: Path) -> None:
|
||||
content = " ".join((REPO_ROOT / path).read_text(encoding="utf-8").split())
|
||||
marker = (
|
||||
"因此配置中间件之后会继续执行可选的安全防护、`DurableContextMiddleware`、可选的 `SummarizationMiddleware`,随后是 `SubagentDateContextMiddleware` 和 `SystemMessageCoalescingMiddleware`。"
|
||||
if "/zh/" in path.as_posix()
|
||||
else "so configured middleware is followed by the optional safety guard, `DurableContextMiddleware`, optional `SummarizationMiddleware`, then `SubagentDateContextMiddleware` and `SystemMessageCoalescingMiddleware`."
|
||||
)
|
||||
assert marker in content
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
(
|
||||
Path("frontend/src/content/en/harness/middlewares.mdx"),
|
||||
Path("frontend/src/content/zh/harness/middlewares.mdx"),
|
||||
),
|
||||
ids=str,
|
||||
)
|
||||
def test_runtime_middleware_summary_includes_current_guards(path: Path) -> None:
|
||||
content = " ".join((REPO_ROOT / path).read_text(encoding="utf-8").split())
|
||||
marker = (
|
||||
"运行时中间件(`InputSanitizationMiddleware` 输入清理 → `ToolOutputBudgetMiddleware` 输出预算截断 → "
|
||||
"`ToolResultSanitizationMiddleware` 工具结果清理,随后是线程数据、上传、沙箱、悬空工具调用修补和 LLM 错误处理;"
|
||||
"工具回执(如启用)、授权/guardrail(如启用)、沙箱审计、读前写后(如启用)、工具进度(如启用)和工具错误处理随后执行)"
|
||||
if "/zh/" in path.as_posix()
|
||||
else (
|
||||
"Runtime middlewares (`InputSanitizationMiddleware` for input sanitization → `ToolOutputBudgetMiddleware` "
|
||||
"for output-budget truncation → `ToolResultSanitizationMiddleware` for tool-result sanitization, then thread data, "
|
||||
"uploads, sandbox, dangling tool-call patching, and LLM error handling; tool receipts (if enabled), "
|
||||
"authorization/guardrail (if enabled), sandbox audit, read-before-write (if enabled), tool progress (if enabled), "
|
||||
"and tool error handling follow)"
|
||||
)
|
||||
)
|
||||
assert marker in content
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
(
|
||||
Path("frontend/src/content/en/harness/middlewares.mdx"),
|
||||
Path("frontend/src/content/zh/harness/middlewares.mdx"),
|
||||
),
|
||||
ids=str,
|
||||
)
|
||||
def test_runtime_sanitization_and_budget_order_is_explicit(path: Path) -> None:
|
||||
content = " ".join((REPO_ROOT / path).read_text(encoding="utf-8").split())
|
||||
markers = (
|
||||
"`InputSanitizationMiddleware`",
|
||||
"`ToolOutputBudgetMiddleware`",
|
||||
"`ToolResultSanitizationMiddleware`",
|
||||
)
|
||||
positions = [content.index(marker) for marker in markers]
|
||||
assert positions == sorted(positions)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
(
|
||||
Path("frontend/src/content/en/harness/middlewares.mdx"),
|
||||
Path("frontend/src/content/zh/harness/middlewares.mdx"),
|
||||
),
|
||||
ids=str,
|
||||
)
|
||||
def test_subagent_callout_does_not_overstate_lead_only_scope(path: Path) -> None:
|
||||
content = " ".join((REPO_ROOT / path).read_text(encoding="utf-8").split())
|
||||
marker = "记忆、标题生成和澄清等其他 Lead Agent 专属中间件不会在子 Agent 链中运行。" if "/zh/" in path.as_posix() else "other Lead-Agent-specific middlewares such as memory, title generation, and clarification do not run there."
|
||||
assert marker in content
|
||||
@ -398,6 +398,7 @@ def test_build_lead_runtime_middlewares_chain_order_matches_agents_md():
|
||||
from deerflow.agents.middlewares.sandbox_audit_middleware import SandboxAuditMiddleware
|
||||
from deerflow.agents.middlewares.thread_data_middleware import ThreadDataMiddleware
|
||||
from deerflow.agents.middlewares.tool_output_budget_middleware import ToolOutputBudgetMiddleware
|
||||
from deerflow.agents.middlewares.tool_result_sanitization_middleware import ToolResultSanitizationMiddleware
|
||||
from deerflow.agents.middlewares.uploads_middleware import UploadsMiddleware
|
||||
from deerflow.sandbox.middleware import SandboxMiddleware
|
||||
|
||||
@ -414,6 +415,7 @@ def test_build_lead_runtime_middlewares_chain_order_matches_agents_md():
|
||||
expected_order: list[tuple[str, type]] = [
|
||||
("InputSanitizationMiddleware", InputSanitizationMiddleware),
|
||||
("ToolOutputBudgetMiddleware", ToolOutputBudgetMiddleware),
|
||||
("ToolResultSanitizationMiddleware", ToolResultSanitizationMiddleware),
|
||||
("ThreadDataMiddleware", ThreadDataMiddleware),
|
||||
("UploadsMiddleware", UploadsMiddleware),
|
||||
("SandboxMiddleware", SandboxMiddleware),
|
||||
|
||||
@ -48,8 +48,8 @@ logging:
|
||||
# lead-only vs subagent-only configuration is not expressible yet. Treat these
|
||||
# files as trusted operator config because middleware classes execute code.
|
||||
# Uncomment this block to define middlewares in config.yaml. Leaving it commented
|
||||
# lets extensions_config.json remain the source of truth for this legacy
|
||||
# config-declared middleware list. Packaged plugins use the `plugins:` block below.
|
||||
# lets extensions_config.json remain the source of truth for this fixed-slot
|
||||
# (not deprecated) middleware list. Packaged plugins use the `plugins:` block below.
|
||||
# extensions:
|
||||
# middlewares:
|
||||
# - my_company.deerflow_middlewares:DomainGuardMiddleware
|
||||
|
||||
@ -22,27 +22,41 @@ Middlewares are the primary extension point for adding behavior to the Lead Agen
|
||||
To add a custom middleware:
|
||||
|
||||
1. Implement the `AgentMiddleware` interface from `langchain.agents.middleware`.
|
||||
2. Pass your middleware to the `custom_middlewares` parameter when building the agent.
|
||||
2. Register its import path under `extensions.middlewares`, or pass an instance through an embedded SDK API.
|
||||
|
||||
```python
|
||||
from langchain.agents import AgentState
|
||||
from langchain.agents.middleware import AgentMiddleware
|
||||
from langgraph.runtime import Runtime
|
||||
|
||||
|
||||
class AuditMiddleware(AgentMiddleware[AgentState]):
|
||||
def before_model(self, state: AgentState, runtime: Runtime) -> dict | None:
|
||||
"""Runs before each model call."""
|
||||
print(f"[audit] turn starts: {len(state.messages)} messages in context")
|
||||
print(f"[audit] model input has {len(state.get('messages', []))} messages")
|
||||
return None
|
||||
|
||||
def after_model(self, state: AgentState, runtime: Runtime) -> dict | None:
|
||||
"""Runs after each model call."""
|
||||
print(f"[audit] turn ends: last message type = {state.messages[-1].type}")
|
||||
messages = state.get("messages", [])
|
||||
last_message = messages[-1] if messages else None
|
||||
print(f"[audit] last message type: {type(last_message).__name__ if last_message else 'none'}")
|
||||
return None
|
||||
```
|
||||
|
||||
Custom middlewares are injected into the chain immediately before `ClarificationMiddleware`, which always runs last.
|
||||
Lifecycle hooks can return a dictionary of state updates, which LangChain merges
|
||||
into the agent state; return `None` when observing only.
|
||||
|
||||
For an operator-managed deployment, the class must have a zero-argument constructor and be importable by the Gateway process:
|
||||
|
||||
```yaml
|
||||
extensions:
|
||||
middlewares:
|
||||
- my_company.deerflow_middlewares:AuditMiddleware
|
||||
```
|
||||
|
||||
Configured middleware is loaded after the built-in middleware and optional loop/token guards. On the lead-agent pipeline, it runs before the terminal-response, model-length, safety, and clarification tail; subagents have no terminal-response, model-length, or clarification stage, so configured middleware is followed by the optional safety guard, `DurableContextMiddleware`, optional `SummarizationMiddleware`, then `SubagentDateContextMiddleware` and `SystemMessageCoalescingMiddleware`. Treat these class paths as trusted configuration because loading one executes Python code. Embedded callers can instead use `DeerFlowClient(middlewares=[AuditMiddleware()])`, which builds the full lead-agent chain and places middleware before its terminal-response, model-length, safety, and clarification tail. `create_deerflow_agent(extra_middleware=[AuditMiddleware()])` instead builds a smaller feature-based lead-agent chain; unanchored extras are placed immediately before `ClarificationMiddleware` (anchored extras follow their `@Next`/`@Prev` placement). Neither API forwards middleware to subagents.
|
||||
|
||||
Choose the registration path by ownership and placement. The fixed-slot (not deprecated) `extensions.middlewares` list is accepted in `config.yaml` and `extensions_config.json` (`config.yaml` wins) and applies to both lead and subagent pipelines. Packaged extensions registered through the top-level `plugins:` list contribute middleware at semantic extension points. Contributor code that needs committed, programmatic lead-only wiring can use `build_middlewares(..., custom_middlewares=[AuditMiddleware()])`.
|
||||
|
||||
For `create_deerflow_agent`, an `@Next` or `@Prev` anchor must name a middleware that this smaller chain actually contains; anchors from the full middleware list otherwise fail to resolve.
|
||||
|
||||
## Custom tools
|
||||
|
||||
|
||||
@ -20,8 +20,8 @@ This design keeps the agent core simple and stable while allowing rich, composab
|
||||
<Callout type="info">
|
||||
Each subagent runs its own agent loop and gets its own middleware chain. The
|
||||
loop-detection, token-budget, and summarization guards below are mirrored on
|
||||
the subagent chain (#3875); the other middlewares are Lead-Agent-specific
|
||||
(e.g. memory, title generation, clarification). See
|
||||
the subagent chain (#3875); other Lead-Agent-specific middlewares such as
|
||||
memory, title generation, and clarification do not run there. See
|
||||
[Subagents → Runaway guards](/docs/harness/subagents#runaway-guards).
|
||||
</Callout>
|
||||
|
||||
@ -29,24 +29,28 @@ This design keeps the agent core simple and stable while allowing rich, composab
|
||||
|
||||
The middleware chain is built once per agent invocation, based on the current configuration and request parameters. The middlewares run in a defined order:
|
||||
|
||||
1. Runtime middlewares (error handling, thread data, uploads, dangling tool call patching)
|
||||
1. Runtime middlewares (`InputSanitizationMiddleware` for input sanitization → `ToolOutputBudgetMiddleware` for output-budget truncation → `ToolResultSanitizationMiddleware` for tool-result sanitization, then thread data, uploads, sandbox, dangling tool-call patching, and LLM error handling; tool receipts (if enabled), authorization/guardrail (if enabled), sandbox audit, read-before-write (if enabled), tool progress (if enabled), and tool error handling follow)
|
||||
2. `DynamicContextMiddleware` — current date and optional memory context
|
||||
3. `SkillActivationMiddleware` — slash-skill activation
|
||||
4. `DurableContextMiddleware` — captures durable summary, delegation, and skill-reference state
|
||||
5. `SummarizationMiddleware` — context compression (if enabled)
|
||||
6. `TodoMiddleware` — task list management (plan mode only)
|
||||
7. `TokenUsageMiddleware` — token tracking (if enabled)
|
||||
8. `TitleMiddleware` — automatic thread title generation
|
||||
9. `MemoryMiddleware` — cross-session memory injection and queuing
|
||||
10. `ViewImageMiddleware` — image details injection (if model supports vision)
|
||||
11. `DeferredToolFilterMiddleware` — hides deferred tool schemas (if tool search enabled)
|
||||
12. `SystemMessageCoalescingMiddleware` — coalesces provider-facing system messages
|
||||
13. `SubagentLimitMiddleware` — limits parallel subagent calls (if subagents enabled)
|
||||
14. `LoopDetectionMiddleware` — breaks repetitive tool call loops
|
||||
15. `TokenBudgetMiddleware` — per-run token budget enforcement (if enabled)
|
||||
16. Custom middlewares (if any)
|
||||
17. `SafetyFinishReasonMiddleware` — suppresses tool execution after safety-terminated responses (if enabled)
|
||||
18. `ClarificationMiddleware` — intercepts clarification requests (always last)
|
||||
4. `SkillToolPolicyMiddleware` — filters skill tools to the active skill
|
||||
5. `DurableContextMiddleware` — captures durable summary, delegation, and skill-reference state
|
||||
6. `SummarizationMiddleware` — context compression (if enabled)
|
||||
7. `TodoMiddleware` — task list management (plan mode only)
|
||||
8. `TokenUsageMiddleware` — token tracking (if enabled)
|
||||
9. `TitleMiddleware` — automatic thread title generation
|
||||
10. `MemoryMiddleware` — cross-session memory injection and queuing
|
||||
11. `ViewImageMiddleware` — image details injection (if model supports vision)
|
||||
12. `DeferredToolFilterMiddleware` — hides deferred tool schemas (if tool search enabled)
|
||||
13. `SystemMessageCoalescingMiddleware` — coalesces provider-facing system messages
|
||||
14. `SubagentLimitMiddleware` — limits parallel subagent calls (if subagents enabled)
|
||||
15. `LoopDetectionMiddleware` — breaks repetitive tool call loops
|
||||
16. `TokenBudgetMiddleware` — per-run token budget enforcement (if enabled)
|
||||
17. Custom middlewares (if any)
|
||||
18. Configured extension middlewares (if any)
|
||||
19. `TerminalResponseMiddleware` — retries an empty final response once
|
||||
20. `ModelLengthFinishReasonMiddleware` — records a length-capped completion
|
||||
21. `SafetyFinishReasonMiddleware` — suppresses tool execution after safety-terminated responses (if enabled)
|
||||
22. `ClarificationMiddleware` — intercepts clarification requests (always last)
|
||||
|
||||
The ordering is significant. Durable context capture runs before summarization so delegated task dispatches, terminal delegation results, and loaded skill references survive compaction. Clarification always runs last so it can intercept after all other middlewares have had their turn.
|
||||
|
||||
@ -175,6 +179,8 @@ When tool search is enabled, this middleware hides deferred tool schemas from th
|
||||
|
||||
**Configuration**: `tool_search.enabled: true` in `config.yaml`.
|
||||
|
||||
For `create_deerflow_agent`, an `@Next` or `@Prev` anchor must name a middleware that this smaller chain actually contains; anchors from the full middleware list otherwise fail to resolve.
|
||||
|
||||
## Summarization configuration
|
||||
|
||||
The `SummarizationMiddleware` is one of the most impactful middlewares for long-horizon tasks. Here is the full configuration reference:
|
||||
@ -232,16 +238,33 @@ Custom middlewares can be injected into the chain for specialized use cases. A m
|
||||
The basic structure is:
|
||||
|
||||
```python
|
||||
from langchain.agents import AgentState
|
||||
from langchain.agents.middleware import AgentMiddleware
|
||||
from langgraph.runtime import Runtime
|
||||
|
||||
class MyMiddleware(AgentMiddleware):
|
||||
def before_model(self, state, runtime) -> dict | None:
|
||||
"""Runs before each model call."""
|
||||
class MyMiddleware(AgentMiddleware[AgentState]):
|
||||
def before_model(self, state: AgentState, runtime: Runtime) -> dict | None:
|
||||
print(f"Model input contains {len(state.get('messages', []))} messages")
|
||||
return None
|
||||
|
||||
def after_model(self, state, runtime) -> dict | None:
|
||||
"""Runs after each model call."""
|
||||
def after_model(self, state: AgentState, runtime: Runtime) -> dict | None:
|
||||
messages = state.get("messages", [])
|
||||
last_message = messages[-1] if messages else None
|
||||
print(f"Last message type: {type(last_message).__name__ if last_message else 'none'}")
|
||||
return None
|
||||
```
|
||||
|
||||
Custom middlewares are passed to `make_lead_agent` via the `custom_middlewares` parameter in `build_middlewares`. They are injected immediately before `ClarificationMiddleware` at the end of the chain.
|
||||
Lifecycle hooks can return a dictionary of state updates, which LangChain merges
|
||||
into the agent state, or `None` when they only observe state.
|
||||
|
||||
For operator-managed deployments, register a zero-argument class by import path:
|
||||
|
||||
```yaml
|
||||
extensions:
|
||||
middlewares:
|
||||
- my_company.deerflow_middlewares:MyMiddleware
|
||||
```
|
||||
|
||||
Configured middleware runs after the built-in middleware and optional loop/token guards. On the lead-agent pipeline, it runs before the terminal-response, model-length, safety, and clarification tail; subagents have no terminal-response, model-length, or clarification stage, so configured middleware is followed by the optional safety guard, `DurableContextMiddleware`, optional `SummarizationMiddleware`, then `SubagentDateContextMiddleware` and `SystemMessageCoalescingMiddleware`. Treat middleware paths as trusted configuration because loading one executes Python code. Embedded callers can instead use `DeerFlowClient(middlewares=[...])`, which builds the full lead-agent chain and places middleware before its terminal-response, model-length, safety, and clarification tail. `create_deerflow_agent(extra_middleware=[...])` instead builds a smaller feature-based lead-agent chain; unanchored extras are placed immediately before `ClarificationMiddleware` (anchored extras follow their `@Next`/`@Prev` placement). Neither API forwards middleware to subagents.
|
||||
|
||||
Choose the registration path by ownership and placement. The fixed-slot (not deprecated) `extensions.middlewares` list is accepted in `config.yaml` and `extensions_config.json` (`config.yaml` wins) and applies to both lead and subagent pipelines. Packaged extensions registered through the top-level `plugins:` list contribute middleware at semantic extension points. Contributor code that needs committed, programmatic lead-only wiring can use `build_middlewares(..., custom_middlewares=[MyMiddleware()])`.
|
||||
|
||||
@ -22,27 +22,40 @@ DeerFlow 的可插拔架构意味着系统的大多数部分都可以在不 fork
|
||||
添加自定义中间件:
|
||||
|
||||
1. 实现 `langchain.agents.middleware` 中的 `AgentMiddleware` 接口。
|
||||
2. 在构建 Agent 时通过 `custom_middlewares` 参数传入你的中间件。
|
||||
2. 在 `extensions.middlewares` 下注册它的导入路径,或通过嵌入式 SDK API 传入实例。
|
||||
|
||||
```python
|
||||
from langchain.agents import AgentState
|
||||
from langchain.agents.middleware import AgentMiddleware
|
||||
from langgraph.runtime import Runtime
|
||||
|
||||
|
||||
class AuditMiddleware(AgentMiddleware[AgentState]):
|
||||
def before_model(self, state: AgentState, runtime: Runtime) -> dict | None:
|
||||
"""在每次模型调用前运行。"""
|
||||
print(f"[审计] 轮次开始:上下文中有 {len(state.messages)} 条消息")
|
||||
print(f"[审计] 模型输入包含 {len(state.get('messages', []))} 条消息")
|
||||
return None
|
||||
|
||||
def after_model(self, state: AgentState, runtime: Runtime) -> dict | None:
|
||||
"""在每次模型调用后运行。"""
|
||||
print(f"[审计] 轮次结束:最后一条消息类型 = {state.messages[-1].type}")
|
||||
messages = state.get("messages", [])
|
||||
last_message = messages[-1] if messages else None
|
||||
print(f"[审计] 最后一条消息类型:{type(last_message).__name__ if last_message else 'none'}")
|
||||
return None
|
||||
```
|
||||
|
||||
自定义中间件在链末尾 `ClarificationMiddleware` 之前注入,后者始终最后运行。
|
||||
生命周期钩子可以返回状态更新字典,LangChain 会将其合并到 Agent 状态中;仅观察时返回 `None`。
|
||||
|
||||
对于运维配置的部署,该类必须提供零参数构造函数,并且 Gateway 进程必须能够导入它:
|
||||
|
||||
```yaml
|
||||
extensions:
|
||||
middlewares:
|
||||
- my_company.deerflow_middlewares:AuditMiddleware
|
||||
```
|
||||
|
||||
配置的中间件会在内置中间件及可选的循环/token 保护之后加载。对于主 Agent 链,它位于终态响应、模型长度、安全和澄清尾部之前;子 Agent 链没有终态响应、模型长度或澄清阶段,因此配置中间件之后会继续执行可选的安全防护、`DurableContextMiddleware`、可选的 `SummarizationMiddleware`,随后是 `SubagentDateContextMiddleware` 和 `SystemMessageCoalescingMiddleware`。中间件路径会执行 Python 代码,因此应视为可信配置。嵌入式调用方也可以使用 `DeerFlowClient(middlewares=[AuditMiddleware()])`;它构建完整的主 Agent 链,并将中间件放在其终态响应、模型长度、安全和澄清尾部之前。`create_deerflow_agent(extra_middleware=[AuditMiddleware()])` 则构建较小的按功能组装的主 Agent 链;未锚定的额外中间件会紧接在 `ClarificationMiddleware` 之前放置(锚定中间件遵循其 `@Next`/`@Prev` 位置)。两个 API 均不会将中间件转发给子 Agent。
|
||||
|
||||
应根据配置的所有者和插入位置选择注册方式。固定槽位(并未弃用)`extensions.middlewares` 可写在 `config.yaml` 或 `extensions_config.json` 中(前者优先),并同时作用于主 Agent 和子 Agent。通过顶层 `plugins:` 注册的打包扩展可在语义化扩展点贡献中间件。若贡献代码需要可提交、可编程且仅作用于主 Agent 的接线,请使用 `build_middlewares(..., custom_middlewares=[AuditMiddleware()])`。
|
||||
|
||||
对于 `create_deerflow_agent`,`@Next` 或 `@Prev` 锚点必须指向这个较小链中实际存在的中间件;否则使用完整中间件列表中的锚点会解析失败。
|
||||
|
||||
## 自定义工具
|
||||
|
||||
|
||||
@ -18,31 +18,35 @@ import { Callout } from "nextra/components";
|
||||
这种设计使 Agent 核心保持简单稳定,同时允许丰富的可组合行为分层叠加。
|
||||
|
||||
<Callout type="info">
|
||||
每个子 Agent 运行各自的 Agent 循环,并拥有自己的中间件链。下方的循环检测、token 预算和摘要压缩防护已镜像到子 Agent 链(#3875);其余中间件为 Lead Agent 专属(如记忆、标题生成、澄清)。参见[子 Agent → 失控行为防护](/docs/harness/subagents#失控行为防护)。
|
||||
每个子 Agent 运行各自的 Agent 循环,并拥有自己的中间件链。下方的循环检测、token 预算和摘要压缩防护已镜像到子 Agent 链(#3875);记忆、标题生成和澄清等其他 Lead Agent 专属中间件不会在子 Agent 链中运行。参见[子 Agent → 失控行为防护](/docs/harness/subagents#失控行为防护)。
|
||||
</Callout>
|
||||
|
||||
## 链的工作方式
|
||||
|
||||
中间件链在每次 Agent 调用时根据当前配置和请求参数构建一次。中间件按定义的顺序运行:
|
||||
|
||||
1. 运行时中间件(错误处理、线程数据、上传、悬空工具调用修补)
|
||||
1. 运行时中间件(`InputSanitizationMiddleware` 输入清理 → `ToolOutputBudgetMiddleware` 输出预算截断 → `ToolResultSanitizationMiddleware` 工具结果清理,随后是线程数据、上传、沙箱、悬空工具调用修补和 LLM 错误处理;工具回执(如启用)、授权/guardrail(如启用)、沙箱审计、读前写后(如启用)、工具进度(如启用)和工具错误处理随后执行)
|
||||
2. `DynamicContextMiddleware` — 当前日期和可选记忆上下文
|
||||
3. `SkillActivationMiddleware` — slash skill 激活
|
||||
4. `DurableContextMiddleware` — 捕获持久摘要、委托和技能引用状态
|
||||
5. `SummarizationMiddleware` — 上下文压缩(如果启用)
|
||||
6. `TodoMiddleware` — 任务列表管理(仅计划模式)
|
||||
7. `TokenUsageMiddleware` — token 追踪(如果启用)
|
||||
8. `TitleMiddleware` — 自动生成线程标题
|
||||
9. `MemoryMiddleware` — 跨会话记忆注入和队列
|
||||
10. `ViewImageMiddleware` — 图像细节注入(如果模型支持视觉)
|
||||
11. `DeferredToolFilterMiddleware` — 隐藏延迟工具 schema(如果启用工具搜索)
|
||||
12. `SystemMessageCoalescingMiddleware` — 合并面向 provider 的 system messages
|
||||
13. `SubagentLimitMiddleware` — 限制并行子 Agent 调用(如果启用子 Agent)
|
||||
14. `LoopDetectionMiddleware` — 打破重复工具调用循环
|
||||
15. `TokenBudgetMiddleware` — 每次运行的 token 预算限制(如果启用)
|
||||
16. 自定义中间件(如有)
|
||||
17. `SafetyFinishReasonMiddleware` — provider 安全终止后抑制工具执行(如果启用)
|
||||
18. `ClarificationMiddleware` — 拦截澄清请求(始终最后)
|
||||
4. `SkillToolPolicyMiddleware` — 将技能工具限制为当前激活的技能
|
||||
5. `DurableContextMiddleware` — 捕获持久摘要、委托和技能引用状态
|
||||
6. `SummarizationMiddleware` — 上下文压缩(如果启用)
|
||||
7. `TodoMiddleware` — 任务列表管理(仅计划模式)
|
||||
8. `TokenUsageMiddleware` — token 追踪(如果启用)
|
||||
9. `TitleMiddleware` — 自动生成线程标题
|
||||
10. `MemoryMiddleware` — 跨会话记忆注入和队列
|
||||
11. `ViewImageMiddleware` — 图像细节注入(如果模型支持视觉)
|
||||
12. `DeferredToolFilterMiddleware` — 隐藏延迟工具 schema(如果启用工具搜索)
|
||||
13. `SystemMessageCoalescingMiddleware` — 合并面向 provider 的 system messages
|
||||
14. `SubagentLimitMiddleware` — 限制并行子 Agent 调用(如果启用子 Agent)
|
||||
15. `LoopDetectionMiddleware` — 打破重复工具调用循环
|
||||
16. `TokenBudgetMiddleware` — 每次运行的 token 预算限制(如果启用)
|
||||
17. 自定义中间件(如有)
|
||||
18. 配置的扩展中间件(如有)
|
||||
19. `TerminalResponseMiddleware` — 重试一次空的最终响应
|
||||
20. `ModelLengthFinishReasonMiddleware` — 记录受长度限制的完成
|
||||
21. `SafetyFinishReasonMiddleware` — provider 安全终止后抑制工具执行(如果启用)
|
||||
22. `ClarificationMiddleware` — 拦截澄清请求(始终最后)
|
||||
|
||||
顺序很重要。Durable context 会在摘要压缩之前捕获已委托任务的派发状态、终态结果和已加载的技能引用,确保它们不会随原始 transcript 压缩而丢失。澄清总是最后运行,这样它可以在所有其他中间件完成后拦截。
|
||||
|
||||
@ -163,6 +167,8 @@ token_usage:
|
||||
|
||||
**配置**:`config.yaml` 中的 `tool_search.enabled: true`。
|
||||
|
||||
对于 `create_deerflow_agent`,`@Next` 或 `@Prev` 锚点必须指向这个较小链中实际存在的中间件;否则使用完整中间件列表中的锚点会解析失败。
|
||||
|
||||
## 摘要压缩配置详解
|
||||
|
||||
`SummarizationMiddleware` 是长时序任务中影响最大的中间件之一。完整配置参考如下:
|
||||
@ -220,15 +226,28 @@ from langchain.agents import AgentState
|
||||
from langchain.agents.middleware import AgentMiddleware
|
||||
from langgraph.runtime import Runtime
|
||||
|
||||
|
||||
class MyMiddleware(AgentMiddleware[AgentState]):
|
||||
def before_model(self, state: AgentState, runtime: Runtime) -> dict | None:
|
||||
"""在模型调用前运行。"""
|
||||
print(f"模型输入包含 {len(state.get('messages', []))} 条消息")
|
||||
return None
|
||||
|
||||
def after_model(self, state: AgentState, runtime: Runtime) -> dict | None:
|
||||
"""在模型调用后运行。"""
|
||||
messages = state.get("messages", [])
|
||||
last_message = messages[-1] if messages else None
|
||||
print(f"最后一条消息类型:{type(last_message).__name__ if last_message else 'none'}")
|
||||
return None
|
||||
```
|
||||
|
||||
自定义中间件在链末尾 `ClarificationMiddleware` 之前注入。
|
||||
生命周期钩子可以返回状态更新字典,LangChain 会将其合并到 Agent 状态中;仅观察时返回 `None`。
|
||||
|
||||
对于运维配置的部署,请通过导入路径注册零参数构造的类:
|
||||
|
||||
```yaml
|
||||
extensions:
|
||||
middlewares:
|
||||
- my_company.deerflow_middlewares:MyMiddleware
|
||||
```
|
||||
|
||||
配置的中间件会在内置中间件及可选的循环/token 保护之后加载。对于主 Agent 链,它位于终态响应、模型长度、安全和澄清尾部之前;子 Agent 链没有终态响应、模型长度或澄清阶段,因此配置中间件之后会继续执行可选的安全防护、`DurableContextMiddleware`、可选的 `SummarizationMiddleware`,随后是 `SubagentDateContextMiddleware` 和 `SystemMessageCoalescingMiddleware`。中间件路径会执行 Python 代码,因此应视为可信配置。嵌入式调用方也可以使用 `DeerFlowClient(middlewares=[...])`;它构建完整的主 Agent 链,并将中间件放在其终态响应、模型长度、安全和澄清尾部之前。`create_deerflow_agent(extra_middleware=[...])` 则构建较小的按功能组装的主 Agent 链;未锚定的额外中间件会紧接在 `ClarificationMiddleware` 之前放置(锚定中间件遵循其 `@Next`/`@Prev` 位置)。两个 API 均不会将中间件转发给子 Agent。
|
||||
|
||||
应根据配置的所有者和插入位置选择注册方式。固定槽位(并未弃用)`extensions.middlewares` 可写在 `config.yaml` 或 `extensions_config.json` 中(前者优先),并同时作用于主 Agent 和子 Agent。通过顶层 `plugins:` 注册的打包扩展可在语义化扩展点贡献中间件。若贡献代码需要可提交、可编程且仅作用于主 Agent 的接线,请使用 `build_middlewares(..., custom_middlewares=[MyMiddleware()])`。
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user