mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-31 07:30:43 +00:00
feat(extensions): scope configured middlewares to lead or subagent (#7661)
* feat(extensions): scope configured middlewares to lead or subagent * fix(helm): synchronize configured middleware config schema version * fix(extensions): require explicit configured middleware scope
This commit is contained in:
parent
178a9ab458
commit
51f0e2de1c
@ -2225,7 +2225,7 @@ Follow-up suggestions also check `model:use` before calling the selected model,
|
||||
For vision-capable agents, `view_image` and the subsequent model-context image read also require `sandbox:execute`. Allowing the tool name alone does not grant image-file access; a role denied sandbox execution cannot reread previously recorded image metadata after its permissions change. External run input and thread-state updates cannot set `viewed_images` or `thread_data`; when a saved image is read from a host copy, its path must resolve to the current user's thread and the recorded virtual image path.
|
||||
Each model request includes at most four recently viewed images and 28 MiB of base64 image data. Images excluded by these limits are reported as omitted; the agent can call `view_image` again to bring one back into context.
|
||||
|
||||
Advanced deployments can also extend the agent runtime itself by declaring `AgentMiddleware` classes under `extensions.middlewares` in `config.yaml` or `extensions_config.json`. Each entry is a `module.path:ClassName` string (zero-argument constructor) or an object `{class, kwargs}` whose `kwargs` are passed to the constructor. `kwargs` values must be JSON types (object, array, string, number, boolean, or null); YAML dates and timestamps are coerced to ISO strings so they match JSON. DeerFlow loads the same configured list into the lead-agent and subagent pipelines after their built-in runtime middlewares and loop/token guards, but before the terminal-response/safety/clarification tail, so enterprise forks can add domain guardrails, tool-call governance, or observability hooks without patching the built-in middleware builders. Missing packages, invalid classes, broken modules, and constructor errors fail loudly at agent creation. Treat `config.yaml` and `extensions_config.json` as trusted operator-controlled files: middleware paths are code execution, just like custom tool, model, sandbox, guardrail, MCP server, and MCP interceptor declarations. Gateway skill/MCP toggle endpoints preserve this field but do not expose an API write path for `extensions.middlewares`. Separate lead-only/subagent-only middleware lists are not supported yet.
|
||||
Advanced deployments can also extend the agent runtime itself by declaring `AgentMiddleware` classes under `extensions.middlewares` in `config.yaml` or `extensions_config.json`. Each entry is a `module.path:ClassName` string (zero-argument constructor) or an object `{class, kwargs, apply_to}` whose `kwargs` are passed to the constructor. Optional `apply_to` selects `both` (default), `lead`, or `subagent`; legacy strings still apply to both chains. `kwargs` values must be JSON types (object, array, string, number, boolean, or null); YAML dates and timestamps are coerced to ISO strings so they match JSON. DeerFlow loads the selected configured entries into the lead-agent and subagent pipelines after their built-in runtime middlewares and loop/token guards, but before the terminal-response/safety/clarification tail, so enterprise forks can add domain guardrails, tool-call governance, or observability hooks without patching the built-in middleware builders. Missing packages, invalid classes, broken modules, and constructor errors fail loudly at agent creation. Treat `config.yaml` and `extensions_config.json` as trusted operator-controlled files: middleware paths are code execution, just like custom tool, model, sandbox, guardrail, MCP server, and MCP interceptor declarations. Gateway skill/MCP toggle endpoints preserve this field but do not expose an API write path for `extensions.middlewares`. Entries excluded from a chain are not imported or constructed there. Invalid scope values fail configuration validation, and selected entries retain their relative order. Packaged extensions continue to use their existing `AgentScope` contract.
|
||||
|
||||
For packaged and configurable runtime integrations, use DeerFlow's extension manager.
|
||||
It accepts a Python package requirement, a public HTTPS Git URL, or a local directory, installs the
|
||||
|
||||
@ -1195,7 +1195,7 @@ Agent settings에서 **Tool access**는 설정된 모든 도구 그룹을 상속
|
||||
비전을 지원하는 에이전트의 경우 `view_image`와 그 뒤에 이어지는 모델 컨텍스트 이미지 읽기에도 `sandbox:execute`가 필요합니다. 도구 이름만 허용해서는 이미지 파일 접근 권한이 부여되지 않으며, 샌드박스 실행이 거부된 역할은 권한이 변경된 후 이전에 기록된 이미지 메타데이터를 다시 읽을 수 없습니다. 외부 실행 입력과 스레드 상태 업데이트로는 `viewed_images`나 `thread_data`를 설정할 수 없습니다. 저장된 이미지를 호스트 사본에서 읽을 때는 해당 경로가 현재 사용자의 스레드와 기록된 가상 이미지 경로로 해석되어야 합니다.
|
||||
각 모델 요청에는 최근에 본 이미지가 최대 4개, base64 이미지 데이터가 최대 28 MiB까지 포함됩니다. 이 제한으로 제외된 이미지는 생략된 것으로 보고되며, 에이전트는 `view_image`를 다시 호출해 이미지를 컨텍스트로 다시 가져올 수 있습니다.
|
||||
|
||||
고급 배포에서는 `config.yaml` 또는 `extensions_config.json`의 `extensions.middlewares` 아래에 `AgentMiddleware` 클래스를 선언하여 에이전트 런타임 자체를 확장할 수도 있습니다. 각 항목은 `module.path:ClassName` 문자열(인자 없는 생성자) 또는 객체 `{class, kwargs}`이며, 객체의 `kwargs`는 생성자에 전달됩니다. `kwargs` 값은 JSON 타입(객체, 배열, 문자열, 숫자, 불리언, null)이어야 하며, YAML 날짜와 타임스탬프는 JSON과 일치하도록 ISO 문자열로 변환됩니다. DeerFlow는 설정된 같은 목록을 리드 에이전트와 서브에이전트 파이프라인에 불러오며, 그 위치는 기본 제공 런타임 미들웨어와 루프/토큰 가드 뒤, 그리고 terminal-response/safety/clarification 꼬리 앞입니다. 따라서 엔터프라이즈 포크는 기본 제공 미들웨어 빌더를 패치하지 않고도 도메인 가드레일, 도구 호출 거버넌스, 관측성 훅을 추가할 수 있습니다. 패키지 누락, 잘못된 클래스, 손상된 모듈, 생성자 오류는 에이전트 생성 시점에 명확한 오류로 실패합니다. `config.yaml`과 `extensions_config.json`은 운영자가 제어하는 신뢰된 파일로 취급하세요. 커스텀 도구, 모델, 샌드박스, 가드레일, MCP 서버, MCP 인터셉터 선언과 마찬가지로 미들웨어 경로는 곧 코드 실행입니다. Gateway의 스킬/MCP 토글 엔드포인트는 이 필드를 보존하지만 `extensions.middlewares`에 대한 API 쓰기 경로는 제공하지 않습니다. 리드 에이전트 전용/서브에이전트 전용 미들웨어 목록을 따로 지정하는 기능은 아직 지원되지 않습니다.
|
||||
고급 배포에서는 `config.yaml` 또는 `extensions_config.json`의 `extensions.middlewares` 아래에 `AgentMiddleware` 클래스를 선언하여 에이전트 런타임 자체를 확장할 수도 있습니다. 각 항목은 `module.path:ClassName` 문자열(인자 없는 생성자) 또는 객체 `{class, kwargs, apply_to}`이며, 객체의 `kwargs`는 생성자에 전달됩니다. `kwargs` 값은 JSON 타입(객체, 배열, 문자열, 숫자, 불리언, null)이어야 하며, YAML 날짜와 타임스탬프는 JSON과 일치하도록 ISO 문자열로 변환됩니다. DeerFlow는 각 체인에 대해 선택된 설정 항목을 리드 에이전트와 서브에이전트 파이프라인에 불러오며, 그 위치는 기본 제공 런타임 미들웨어와 루프/토큰 가드 뒤, 그리고 terminal-response/safety/clarification 꼬리 앞입니다. 따라서 엔터프라이즈 포크는 기본 제공 미들웨어 빌더를 패치하지 않고도 도메인 가드레일, 도구 호출 거버넌스, 관측성 훅을 추가할 수 있습니다. 패키지 누락, 잘못된 클래스, 손상된 모듈, 생성자 오류는 에이전트 생성 시점에 명확한 오류로 실패합니다. `config.yaml`과 `extensions_config.json`은 운영자가 제어하는 신뢰된 파일로 취급하세요. 커스텀 도구, 모델, 샌드박스, 가드레일, MCP 서버, MCP 인터셉터 선언과 마찬가지로 미들웨어 경로는 곧 코드 실행입니다. Gateway의 스킬/MCP 토글 엔드포인트는 이 필드를 보존하지만 `extensions.middlewares`에 대한 API 쓰기 경로는 제공하지 않습니다. 객체의 선택적 `apply_to`는 `both`(기본값), `lead`, `subagent`를 지원하며, 문자열 항목과 이 필드를 생략한 객체는 계속 두 파이프라인 모두에 적용됩니다. 해당 체인에서 제외된 항목은 임포트하거나 인스턴스화하지 않습니다.
|
||||
|
||||
패키지화되고 설정 가능한 런타임 통합에는 DeerFlow의 확장 관리자를 사용하세요. 확장 관리자는 Python 패키지 요구 사항, 공개 HTTPS Git URL, 또는 로컬 디렉터리를 받아 백엔드 전용 `extensions` 의존성 그룹에 패키지를 설치하고, `backend/uv.lock`을 업데이트하며, 시작 시에만 읽는 `config.yaml`의 최상위 `plugins:` 목록에 활성화된 항목을 추가합니다.
|
||||
|
||||
|
||||
@ -750,7 +750,7 @@ def build_middlewares(
|
||||
if custom_middlewares:
|
||||
middlewares.extend(custom_middlewares)
|
||||
|
||||
configured_middlewares = load_configured_extension_middlewares(resolved_app_config)
|
||||
configured_middlewares = load_configured_extension_middlewares(resolved_app_config, scope="lead")
|
||||
if configured_middlewares:
|
||||
middlewares.extend(configured_middlewares)
|
||||
|
||||
|
||||
@ -38,6 +38,13 @@ stay `in_progress`. Never infer status from reply text.
|
||||
|
||||
Assembly order: `tool_error_handling_middleware.py::_build_runtime_middlewares` (exposed as `build_lead_runtime_middlewares`), then `../lead_agent/agent.py::build_middlewares`. Optional entries require their config/runtime condition.
|
||||
|
||||
Config-declared middleware uses `configured_extensions.py`; the lead and subagent
|
||||
builders pass their own required keyword-only `scope` (no implicit lead default).
|
||||
Optional `apply_to: both | lead | subagent`
|
||||
defaults to both, including legacy strings. Filter before reflection/construction;
|
||||
preserve selected entry order and create fresh instances per build. This fixed-slot
|
||||
selector does not alter packaged extensions' `AgentScope` or placement contract.
|
||||
|
||||
**Message provenance.** Injected/rewritten messages use
|
||||
`deerflow_extension_api.provenance.provenance_kwargs()`: server-owned
|
||||
`deerflow_content_kind`, `deerflow_producer_kind`, optional entity ID, even
|
||||
@ -177,7 +184,7 @@ Before changing a later authorization phase, read the [authorization RFC](../../
|
||||
32. **TokenBudgetMiddleware** - `token_budget.enabled`: run-ID budgets survive continuations; missing/invalid IDs reset invocation state. With token usage tracking, place CompletedSubagentUsageMiddleware after the guard: reversed `after_model` hooks backfill terminal child usage before enforcement. Keep TokenUsageMiddleware earlier so current-step attribution reflects calls retained by all guards.
|
||||
33. **ModelContentCompatibilityMiddleware** - Read-time sanitizer (`wrap_model_call`/`awrap_model_call`, request view only; never writes state): rewrites URL-sourced `file` blocks (any role, gated on the `url` key; an http(s) URL is preserved when the selected model's serializer accepts it — OpenAI Responses API or Anthropic) and non-fetchable-scheme (non-`http`/`https`/`data`) `image` blocks into text placeholders whose MIME/location metadata passes the shared `neutralize_untrusted_tags` guard, so pre-MCP-fix checkpoints heal on the next turn with no migration. Uses the conversion layer's `resource_placeholder_text`/`model_visible_location` for model-visible text; artifact capture scans state messages, so healing adds neither `resource_links` nor handles. See the MCP guide for conversion and the module docstring for healing; malformed URLs degrade to location-less placeholders instead of raising. Registered after DurableContext/summarization, before the date/coalescer tail.
|
||||
34. **Custom middlewares** - *(optional)* Any `custom_middlewares` passed to `build_middlewares` are injected here, before config-declared extensions and the terminal-response/safety/clarification tail
|
||||
35. **Configured extension middlewares** - `extensions.middlewares` in `config.yaml` or `extensions_config.json` optionally accepts `module.path:ClassName` strings or `{class, kwargs}` objects. `deerflow.reflection.resolve_class` loads `AgentMiddleware` classes; import, class, and constructor errors fail agent creation. `kwargs` must be JSON-compatible; YAML dates/timestamps become ISO strings. Order: built-ins/custom and loop/token guards → extensions → terminal-response/safety/clarification tail. Subagents share the list before their safety tail; separate lead/subagent lists are unsupported. Trusted operator config only: paths instantiate arbitrary code. Gateway skill/MCP toggles preserve it in raw JSON; adding an API write path requires explicit trust-boundary review.
|
||||
35. **Configured extension middlewares** - `extensions.middlewares` in `config.yaml` or `extensions_config.json` optionally accepts `module.path:ClassName` strings or `{class, kwargs, apply_to}` objects. `deerflow.reflection.resolve_class` loads `AgentMiddleware` classes; import, class, and constructor errors fail agent creation. `kwargs` must be JSON-compatible; YAML dates/timestamps become ISO strings. Order: built-ins/custom and loop/token guards → extensions → terminal-response/safety/clarification tail. Optional `apply_to` selects both chains (default), lead, or subagent before import/construction. Trusted operator config only: paths instantiate arbitrary code. Gateway skill/MCP toggles preserve it in raw JSON; adding an API write path requires explicit trust-boundary review.
|
||||
36. **TerminalResponseMiddleware** - When a provider returns an empty terminal `AIMessage` after tool execution, injects a hidden recovery prompt and retries the model once; a second empty response is replaced in checkpoint state by a visible error fallback marked for the run worker, so the run finishes as an error instead of a silent success
|
||||
37. **ModelLengthFinishReasonMiddleware** - Responses: same-container `status=incomplete` + `incomplete_details.reason=max_output_tokens`. Suppress calls via shared cloning after clearing invalid calls; keep non-call blocks/metadata. Stamp cap markers; add notice. Also registered on subagent chains (before Safety), where `consume_stop_reason` surfaces `model_length_capped` to the executor.
|
||||
38. **SafetyFinishReasonMiddleware** - `safety_finish_reason.enabled`: suppresses valid/invalid safety-stopped calls. Responses: `status=incomplete` + `incomplete_details.reason=content_filter`. Clear invalid calls before shared cloning. Place after terminal/custom/configured hooks; `after_model` runs in reverse.
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
"""Config-declared agent middleware loading."""
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from typing import TYPE_CHECKING, Any, Literal
|
||||
|
||||
from langchain.agents.middleware import AgentMiddleware
|
||||
|
||||
@ -14,34 +14,39 @@ if TYPE_CHECKING:
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _middleware_constructor_args(entry: str | ConfiguredMiddlewareSpec | dict[str, Any]) -> tuple[str, dict[str, Any]]:
|
||||
"""Return ``(class_path, kwargs)`` for one config entry."""
|
||||
def _middleware_spec(entry: str | ConfiguredMiddlewareSpec | dict[str, Any]) -> ConfiguredMiddlewareSpec:
|
||||
"""Normalize and validate a config entry before scope selection."""
|
||||
if isinstance(entry, str):
|
||||
return entry, {}
|
||||
return ConfiguredMiddlewareSpec(class_path=entry)
|
||||
if isinstance(entry, ConfiguredMiddlewareSpec):
|
||||
return entry.class_path, dict(entry.kwargs)
|
||||
parsed = ConfiguredMiddlewareSpec.model_validate(entry)
|
||||
return parsed.class_path, dict(parsed.kwargs)
|
||||
return entry
|
||||
return ConfiguredMiddlewareSpec.model_validate(entry)
|
||||
|
||||
|
||||
def load_configured_extension_middlewares(app_config: "AppConfig") -> list[AgentMiddleware]:
|
||||
def load_configured_extension_middlewares(app_config: "AppConfig", *, scope: Literal["lead", "subagent"]) -> list[AgentMiddleware]:
|
||||
"""Instantiate config-declared agent middlewares.
|
||||
|
||||
Each entry is a ``module.path:ClassName`` string or a
|
||||
``ConfiguredMiddlewareSpec`` (``class`` plus optional ``kwargs``).
|
||||
``ConfiguredMiddlewareSpec`` (``class`` plus optional ``kwargs`` and
|
||||
``apply_to``). The host must explicitly supply the concrete chain scope.
|
||||
Excluded entries are not imported or constructed.
|
||||
Import, attribute, and subclass validation intentionally go through the
|
||||
shared reflection resolver so failures carry the same actionable
|
||||
dependency hints as models, tools, sandbox providers, and guardrail
|
||||
providers. Constructor errors fail loudly at agent creation.
|
||||
"""
|
||||
if scope not in ("lead", "subagent"):
|
||||
raise ValueError("middleware scope must be 'lead' or 'subagent'")
|
||||
middlewares: list[AgentMiddleware] = []
|
||||
for entry in list(app_config.extensions.middlewares or []):
|
||||
class_path, kwargs = _middleware_constructor_args(entry)
|
||||
middleware_cls = resolve_class(class_path, AgentMiddleware)
|
||||
spec = _middleware_spec(entry)
|
||||
if spec.apply_to not in ("both", scope):
|
||||
continue
|
||||
middleware_cls = resolve_class(spec.class_path, AgentMiddleware)
|
||||
try:
|
||||
middleware = middleware_cls(**kwargs)
|
||||
middleware = middleware_cls(**dict(spec.kwargs))
|
||||
except Exception:
|
||||
logger.exception("Failed to instantiate configured extension middleware %s", class_path)
|
||||
logger.exception("Failed to instantiate configured extension middleware %s", spec.class_path)
|
||||
raise
|
||||
middlewares.append(middleware)
|
||||
return middlewares
|
||||
|
||||
@ -699,7 +699,7 @@ def build_subagent_runtime_middlewares(
|
||||
|
||||
from deerflow.agents.middlewares.configured_extensions import load_configured_extension_middlewares
|
||||
|
||||
middlewares.extend(load_configured_extension_middlewares(app_config))
|
||||
middlewares.extend(load_configured_extension_middlewares(app_config, scope="subagent"))
|
||||
|
||||
# Same provider output-length guard the lead agent uses. A subagent answer
|
||||
# cut off at the model's output limit would otherwise reach the lead as a
|
||||
|
||||
@ -191,7 +191,7 @@ Tests: `tests/test_secret_file.py`, `tests/test_credentials_key.py`.
|
||||
- `mcpServers` - Map of server name → config (enabled, type, command, args, env, url, headers, oauth, description, `routing`, `tools`, `tool_call_timeout`, `session_init_timeout`). `routing.mode="prefer"` emits `<mcp_routing_hints>` prompt guidance; if `tool_search` defers the hinted tool, `McpRoutingMiddleware` can also auto-promote matching deferred schemas before the model call. It does not hard-disable other tools. `session_init_timeout` (default `DEFAULT_MCP_SESSION_INIT_TIMEOUT` = 60s, `null` to disable) bounds server bring-up: tool discovery and persistent stdio session initialization, so a hung server cannot block agent construction indefinitely; durable HTTP/SSE task calls use it for their ephemeral session initialization too. `tool_call_timeout` bounds individual stdio calls and durable-task calls on every transport; other HTTP/SSE tools use transport-level timeouts.
|
||||
- `tool_search.auto_promote_top_k` - Global MCP routing auto-promote breadth. Default `3`, clamped to `1..5`; applies only when `tool_search.enabled=true` and only to deferred MCP tools with `routing.mode="prefer"` and non-empty keywords. For lead agents the deferred catalog is built from the full configured MCP set; auto-promotion never grants authority — it only raises visibility for tools already admitted to the agent's catalog, while assembly and execution authorization still apply independently.
|
||||
- `skills` - Map of skill name → state (enabled)
|
||||
- `middlewares` - `AgentMiddleware` entries for lead and subagent runtime extension: class-path strings or `{class, kwargs}` objects. `kwargs` values must be JSON types; YAML dates and timestamps are coerced to ISO strings so they match JSON. `config.yaml -> extensions` can override these fields after validation; overrides are replace-per-field, not list concatenation.
|
||||
- `middlewares` - `AgentMiddleware` entries for lead and subagent runtime extension: class-path strings or `{class, kwargs, apply_to}` objects. Optional `apply_to` is `both` (default), `lead`, or `subagent`; strings always apply to both chains. Validate all entries before building agents; excluded entries are not imported or constructed. `kwargs` values must be JSON types; YAML dates and timestamps are coerced to ISO strings so they match JSON. `config.yaml -> extensions` can override these fields after validation; overrides are replace-per-field, not list concatenation.
|
||||
|
||||
Gateway API endpoints and `DeerFlowClient` methods can modify MCP servers and skill state at runtime; their `extensions_config.json` writes use the shared atomic replacement helper, while `middlewares` remains an operator-controlled config-file extension point.
|
||||
|
||||
|
||||
@ -369,6 +369,10 @@ class ConfiguredMiddlewareSpec(BaseModel):
|
||||
default_factory=dict,
|
||||
description=("Keyword arguments passed to the middleware constructor. Values must be JSON types (object, array, string, number, boolean, or null); YAML dates and timestamps are coerced to ISO strings so they match JSON."),
|
||||
)
|
||||
apply_to: Literal["both", "lead", "subagent"] = Field(
|
||||
default="both",
|
||||
description="Agent chain that loads this middleware. Omitted scope preserves both chains.",
|
||||
)
|
||||
model_config = ConfigDict(extra="forbid", populate_by_name=True)
|
||||
|
||||
@field_validator("class_path")
|
||||
@ -400,7 +404,7 @@ class ExtensionsConfig(BaseModel):
|
||||
description=(
|
||||
"AgentMiddleware entries loaded into the lead-agent and subagent middleware chains. "
|
||||
"Each entry is a 'module.path:ClassName' string or an object with 'class' and optional "
|
||||
"'kwargs'. kwargs values must be JSON types; YAML dates and timestamps are coerced to "
|
||||
"'kwargs' and 'apply_to' (both, lead, or subagent; default both). kwargs values must be JSON types; YAML dates and timestamps are coerced to "
|
||||
"ISO strings."
|
||||
),
|
||||
)
|
||||
|
||||
@ -1107,3 +1107,27 @@ def test_get_app_config_keeps_config_yaml_extension_override_after_extensions_re
|
||||
assert get_app_config().extensions.middlewares == ["pkg.from_yaml:YamlMiddleware"]
|
||||
finally:
|
||||
_reset_config_singletons()
|
||||
|
||||
|
||||
def test_middleware_scope_reloads_from_extensions_and_yaml_override(tmp_path, monkeypatch):
|
||||
entry = {"class": "example:Observer", "kwargs": {"label": "observer"}, "apply_to": "lead"}
|
||||
extensions_path = _stage_singleton_configs(tmp_path, monkeypatch, middlewares=[entry])
|
||||
try:
|
||||
assert get_app_config().extensions.middlewares[0].apply_to == "lead"
|
||||
_peer_writes_extensions(extensions_path, middlewares=[{**entry, "apply_to": "subagent"}])
|
||||
assert get_app_config().extensions.middlewares[0].apply_to == "subagent"
|
||||
|
||||
config_path = tmp_path / "config.yaml"
|
||||
raw = yaml.safe_load(config_path.read_text(encoding="utf-8"))
|
||||
raw["extensions"] = {"middlewares": [{**entry, "apply_to": "both"}]}
|
||||
config_path.write_text(yaml.safe_dump(raw), encoding="utf-8")
|
||||
assert get_app_config().extensions.middlewares[0].apply_to == "both"
|
||||
|
||||
_peer_writes_extensions(extensions_path, middlewares=[entry])
|
||||
assert get_app_config().extensions.middlewares[0].apply_to == "both"
|
||||
raw["extensions"]["middlewares"][0]["apply_to"] = "invalid"
|
||||
config_path.write_text(yaml.safe_dump(raw), encoding="utf-8")
|
||||
with pytest.raises(ValidationError, match="apply_to"):
|
||||
AppConfig.from_file(str(config_path))
|
||||
finally:
|
||||
_reset_config_singletons()
|
||||
|
||||
@ -37,7 +37,7 @@ def _config(*entries) -> SimpleNamespace:
|
||||
|
||||
|
||||
def test_string_entry_still_zero_arg():
|
||||
loaded = load_configured_extension_middlewares(_config(f"{__name__}:ZeroArgMiddleware"))
|
||||
loaded = load_configured_extension_middlewares(_config(f"{__name__}:ZeroArgMiddleware"), scope="lead")
|
||||
|
||||
assert len(loaded) == 1
|
||||
assert isinstance(loaded[0], ZeroArgMiddleware)
|
||||
@ -46,7 +46,7 @@ def test_string_entry_still_zero_arg():
|
||||
def test_dict_entry_passes_constructor_kwargs():
|
||||
entry = ConfiguredMiddlewareSpec.model_validate({"class": f"{__name__}:RecordingMiddleware", "kwargs": {"max_tool_calls": 3}})
|
||||
|
||||
loaded = load_configured_extension_middlewares(_config(entry))
|
||||
loaded = load_configured_extension_middlewares(_config(entry), scope="lead")
|
||||
|
||||
assert len(loaded) == 1
|
||||
assert isinstance(loaded[0], RecordingMiddleware)
|
||||
@ -54,7 +54,7 @@ def test_dict_entry_passes_constructor_kwargs():
|
||||
|
||||
|
||||
def test_raw_dict_entry_passes_constructor_kwargs():
|
||||
loaded = load_configured_extension_middlewares(_config({"class": f"{__name__}:RecordingMiddleware", "kwargs": {"max_tool_calls": 2}}))
|
||||
loaded = load_configured_extension_middlewares(_config({"class": f"{__name__}:RecordingMiddleware", "kwargs": {"max_tool_calls": 2}}), scope="lead")
|
||||
|
||||
assert len(loaded) == 1
|
||||
assert isinstance(loaded[0], RecordingMiddleware)
|
||||
@ -63,13 +63,13 @@ def test_raw_dict_entry_passes_constructor_kwargs():
|
||||
|
||||
def test_malformed_raw_dict_fails_at_load():
|
||||
with pytest.raises(ValidationError):
|
||||
load_configured_extension_middlewares(_config({"class": f"{__name__}:RecordingMiddleware", "apply_to": "lead"}))
|
||||
load_configured_extension_middlewares(_config({"class": f"{__name__}:RecordingMiddleware", "unexpected": "lead"}), scope="lead")
|
||||
|
||||
|
||||
def test_empty_kwargs_matches_zero_arg_constructor():
|
||||
entry = ConfiguredMiddlewareSpec.model_validate({"class": f"{__name__}:RecordingMiddleware"})
|
||||
|
||||
loaded = load_configured_extension_middlewares(_config(entry))
|
||||
loaded = load_configured_extension_middlewares(_config(entry), scope="lead")
|
||||
|
||||
assert loaded[0].max_tool_calls == 10
|
||||
|
||||
@ -78,7 +78,7 @@ def test_unknown_constructor_kwarg_fails_loudly():
|
||||
entry = ConfiguredMiddlewareSpec.model_validate({"class": f"{__name__}:ZeroArgMiddleware", "kwargs": {"not_a_param": 1}})
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
load_configured_extension_middlewares(_config(entry))
|
||||
load_configured_extension_middlewares(_config(entry), scope="lead")
|
||||
|
||||
|
||||
def test_extensions_config_keeps_string_entries():
|
||||
@ -106,7 +106,7 @@ def test_extensions_config_parses_class_and_kwargs():
|
||||
|
||||
def test_extensions_config_rejects_unknown_entry_fields():
|
||||
with pytest.raises(ValidationError):
|
||||
ExtensionsConfig.model_validate({"middlewares": [{"class": "pkg:Middleware", "apply_to": "lead"}]})
|
||||
ExtensionsConfig.model_validate({"middlewares": [{"class": "pkg:Middleware", "unexpected": "lead"}]})
|
||||
|
||||
|
||||
def test_extensions_config_rejects_blank_class_path():
|
||||
@ -154,7 +154,7 @@ def test_raw_file_round_trips_kwargs_entries(tmp_path, monkeypatch):
|
||||
raw = {
|
||||
"middlewares": [
|
||||
"pkg:Plain",
|
||||
{"class": "pkg:WithArgs", "kwargs": {"max_tool_calls": 5, "token": "$DEERFLOW_TEST_MIDDLEWARE_TOKEN"}},
|
||||
{"class": "pkg:WithArgs", "kwargs": {"max_tool_calls": 5, "token": "$DEERFLOW_TEST_MIDDLEWARE_TOKEN"}, "apply_to": "lead"},
|
||||
]
|
||||
}
|
||||
config_path.write_text(json.dumps(raw), encoding="utf-8")
|
||||
@ -172,4 +172,5 @@ def test_raw_file_round_trips_kwargs_entries(tmp_path, monkeypatch):
|
||||
assert dumped["middlewares"][1]["class"] == "pkg:WithArgs"
|
||||
assert dumped["middlewares"][1]["kwargs"] == {"max_tool_calls": 5, "token": "$DEERFLOW_TEST_MIDDLEWARE_TOKEN"}
|
||||
assert restored.middlewares[1].class_path == "pkg:WithArgs"
|
||||
assert restored.middlewares[1].apply_to == "lead"
|
||||
assert restored.middlewares[1].kwargs == {"max_tool_calls": 5, "token": "test-secret"}
|
||||
|
||||
136
backend/tests/test_configured_middleware_scope.py
Normal file
136
backend/tests/test_configured_middleware_scope.py
Normal file
@ -0,0 +1,136 @@
|
||||
"""Scope selection for operator-configured middleware, through real builders."""
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
from langchain.agents.middleware import AgentMiddleware
|
||||
from pydantic import ValidationError
|
||||
|
||||
from deerflow.agents.middlewares.configured_extensions import load_configured_extension_middlewares
|
||||
from deerflow.config.app_config import AppConfig
|
||||
from deerflow.config.extensions_config import ConfiguredMiddlewareSpec, ExtensionsConfig
|
||||
|
||||
|
||||
class ScopedMiddleware(AgentMiddleware):
|
||||
def __init__(self, label="legacy"):
|
||||
self.label = label
|
||||
|
||||
|
||||
class BrokenMiddleware(AgentMiddleware):
|
||||
def __init__(self):
|
||||
raise RuntimeError("constructor executed")
|
||||
|
||||
|
||||
def _entry(label, scope):
|
||||
return {"class": f"{__name__}:ScopedMiddleware", "kwargs": {"label": label}, "apply_to": scope}
|
||||
|
||||
|
||||
def _config(entries):
|
||||
return SimpleNamespace(extensions=ExtensionsConfig.model_validate({"middlewares": entries}))
|
||||
|
||||
|
||||
@pytest.mark.parametrize("scope", ["both", "lead", "subagent"])
|
||||
def test_scope_survives_config_serialization(scope):
|
||||
config = ExtensionsConfig.model_validate({"middlewares": [_entry("configured", scope)]})
|
||||
restored = ExtensionsConfig.model_validate_json(config.model_dump_json(by_alias=True))
|
||||
assert restored.middlewares[0].apply_to == scope
|
||||
assert restored.middlewares[0].kwargs == {"label": "configured"}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("invalid", [None, "", "worker", "LEAD", ["lead"], {}, True, 1])
|
||||
def test_invalid_scope_is_rejected_even_for_an_unavailable_class(invalid):
|
||||
with pytest.raises(ValidationError, match="apply_to"):
|
||||
ExtensionsConfig.model_validate({"middlewares": [{"class": "unavailable_module:Middleware", "apply_to": invalid}]})
|
||||
|
||||
|
||||
@pytest.mark.parametrize("scope", ["lead", "subagent"])
|
||||
def test_legacy_entries_and_order_are_preserved(scope):
|
||||
config = _config(
|
||||
[
|
||||
f"{__name__}:ScopedMiddleware",
|
||||
{"class": f"{__name__}:ScopedMiddleware", "kwargs": {"label": "object-default"}},
|
||||
_entry("lead", "lead"),
|
||||
_entry("both", "both"),
|
||||
_entry("subagent", "subagent"),
|
||||
]
|
||||
)
|
||||
loaded = load_configured_extension_middlewares(config, scope=scope)
|
||||
expected = ["legacy", "object-default", "lead", "both"] if scope == "lead" else ["legacy", "object-default", "both", "subagent"]
|
||||
assert [mw.label for mw in loaded] == expected
|
||||
again = load_configured_extension_middlewares(config, scope=scope)
|
||||
assert all(first is not second for first, second in zip(loaded, again, strict=True))
|
||||
assert config.extensions.middlewares[2].kwargs == {"label": "lead"}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("scope,excluded", [("lead", "subagent"), ("subagent", "lead")])
|
||||
@pytest.mark.parametrize("class_path", ["unavailable_module:Middleware", f"{__name__}:BrokenMiddleware"])
|
||||
def test_excluded_entries_are_not_imported_or_constructed(scope, excluded, class_path):
|
||||
config = _config([{"class": class_path, "apply_to": excluded}, _entry("selected", scope)])
|
||||
loaded = load_configured_extension_middlewares(config, scope=scope)
|
||||
assert [mw.label for mw in loaded] == ["selected"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("scope", ["lead", "subagent"])
|
||||
def test_selected_constructor_failure_still_propagates(scope):
|
||||
config = _config([{"class": f"{__name__}:BrokenMiddleware", "apply_to": scope}])
|
||||
with pytest.raises(RuntimeError, match="constructor executed"):
|
||||
load_configured_extension_middlewares(config, scope=scope)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("scope", ["lead", "subagent"])
|
||||
@pytest.mark.parametrize("class_path", ["unavailable_module:Middleware", "types:SimpleNamespace"])
|
||||
def test_selected_import_and_subclass_failures_still_propagate(scope, class_path):
|
||||
config = _config([{"class": class_path, "apply_to": scope}])
|
||||
with pytest.raises((ImportError, ValueError)):
|
||||
load_configured_extension_middlewares(config, scope=scope)
|
||||
|
||||
|
||||
def test_raw_dict_scope_is_validated_before_skipping():
|
||||
raw = {"class": "unavailable_module:Middleware", "apply_to": "subagent", "unexpected": True}
|
||||
config = SimpleNamespace(extensions=SimpleNamespace(middlewares=[raw]))
|
||||
with pytest.raises(ValidationError, match="unexpected"):
|
||||
load_configured_extension_middlewares(config, scope="lead")
|
||||
|
||||
|
||||
def test_raw_dict_and_model_entries_are_filtered():
|
||||
config = SimpleNamespace(extensions=SimpleNamespace(middlewares=[_entry("lead", "lead"), ConfiguredMiddlewareSpec.model_validate(_entry("subagent", "subagent"))]))
|
||||
assert [mw.label for mw in load_configured_extension_middlewares(config, scope="lead")] == ["lead"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("scope", ["both", "invalid", None])
|
||||
def test_loader_requires_a_concrete_host_scope(scope):
|
||||
with pytest.raises(ValueError, match="scope"):
|
||||
load_configured_extension_middlewares(_config([]), scope=scope)
|
||||
|
||||
|
||||
def test_loader_requires_explicit_scope():
|
||||
config = _config([_entry("lead", "lead"), _entry("subagent", "subagent")])
|
||||
with pytest.raises(TypeError, match="required keyword-only argument: 'scope'"):
|
||||
load_configured_extension_middlewares(config)
|
||||
|
||||
|
||||
def test_real_lead_and_subagent_builders_select_their_own_scope(tmp_path, monkeypatch):
|
||||
from deerflow.agents.lead_agent.agent import build_middlewares
|
||||
from deerflow.agents.middlewares.tool_error_handling_middleware import build_subagent_runtime_middlewares
|
||||
from deerflow.extensions import get_agent_build_extensions
|
||||
|
||||
monkeypatch.setenv("DEER_FLOW_HOME", str(tmp_path / "runtime"))
|
||||
app_config = AppConfig.model_validate(
|
||||
{
|
||||
"sandbox": {"use": "deerflow.sandbox.local:LocalSandboxProvider"},
|
||||
"models": [],
|
||||
"memory": {"enabled": False},
|
||||
"summarization": {"enabled": False},
|
||||
"extensions": {"middlewares": [_entry("lead", "lead"), _entry("shared", "both"), _entry("subagent", "subagent")]},
|
||||
}
|
||||
)
|
||||
extensions = get_agent_build_extensions()
|
||||
lead = build_middlewares({}, None, app_config=app_config, memory_enabled=False, available_skills=set(), extensions=extensions)
|
||||
subagent = build_subagent_runtime_middlewares(app_config=app_config, available_skills=set(), extensions=extensions)
|
||||
assert [mw.label for mw in lead if isinstance(mw, ScopedMiddleware)] == ["lead", "shared"]
|
||||
assert [mw.label for mw in subagent if isinstance(mw, ScopedMiddleware)] == ["shared", "subagent"]
|
||||
assert next(mw for mw in lead if isinstance(mw, ScopedMiddleware) and mw.label == "shared") is not next(mw for mw in subagent if isinstance(mw, ScopedMiddleware) and mw.label == "shared")
|
||||
for chain in (lead, subagent):
|
||||
configured_end = max(i for i, mw in enumerate(chain) if isinstance(mw, ScopedMiddleware))
|
||||
length_guard = next(i for i, mw in enumerate(chain) if type(mw).__name__ == "ModelLengthFinishReasonMiddleware")
|
||||
assert configured_end < length_guard
|
||||
@ -75,6 +75,7 @@ logging:
|
||||
# middlewares:
|
||||
# - my_company.deerflow_middlewares:DomainGuardMiddleware
|
||||
# - class: my_company.deerflow_middlewares:LatencyStampingMiddleware
|
||||
# apply_to: lead # both (default), lead, or subagent
|
||||
# kwargs:
|
||||
# header: X-DeerFlow-Latency
|
||||
|
||||
|
||||
@ -51,13 +51,16 @@ extensions:
|
||||
middlewares:
|
||||
- my_company.deerflow_middlewares:AuditMiddleware
|
||||
- class: my_company.deerflow_middlewares:AuditMiddleware
|
||||
apply_to: lead
|
||||
kwargs:
|
||||
max_tool_calls: 5
|
||||
```
|
||||
|
||||
Optional `apply_to` on object entries accepts `both` (default), `lead`, or `subagent`. String entries and objects without this field retain both chains. Invalid values fail configuration validation. The host selects the chain before importing or constructing a middleware, so an excluded entry has no import/constructor side effects in that chain. Selected entries keep their relative order. This selects the fixed-slot configuration path only; packaged extensions retain their existing scope and placement APIs.
|
||||
|
||||
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 or clarification stage, so configured middleware is followed by the model-length guard, 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()])`.
|
||||
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 by default. 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.
|
||||
|
||||
|
||||
@ -263,10 +263,13 @@ extensions:
|
||||
middlewares:
|
||||
- my_company.deerflow_middlewares:MyMiddleware
|
||||
- class: my_company.deerflow_middlewares:MyMiddleware
|
||||
apply_to: lead
|
||||
kwargs:
|
||||
max_tool_calls: 5
|
||||
```
|
||||
|
||||
Optional `apply_to` on object entries accepts `both` (default), `lead`, or `subagent`. String entries and objects without this field retain both chains. Invalid values fail configuration validation. The host selects the chain before importing or constructing a middleware, so an excluded entry has no import/constructor side effects in that chain. Selected entries keep their relative order. This selects the fixed-slot configuration path only; packaged extensions retain their existing scope and placement APIs.
|
||||
|
||||
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 or clarification stage, so configured middleware is followed by the model-length guard, 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()])`.
|
||||
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 by default. 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()])`.
|
||||
|
||||
@ -50,13 +50,16 @@ extensions:
|
||||
middlewares:
|
||||
- my_company.deerflow_middlewares:AuditMiddleware
|
||||
- class: my_company.deerflow_middlewares:AuditMiddleware
|
||||
apply_to: lead
|
||||
kwargs:
|
||||
max_tool_calls: 5
|
||||
```
|
||||
|
||||
对象条目可选 `apply_to`,取值为 `both`(默认)、`lead` 或 `subagent`。字符串条目和省略该字段的对象继续作用于两条链。非法取值在配置校验时失败。宿主先选择作用对象,再导入和构造中间件,因此被排除的条目不会在该链产生导入或构造副作用;入选条目保持原有相对顺序。这只选择固定槽位配置路径;打包扩展继续使用已有的作用范围和放置接口。
|
||||
|
||||
配置的中间件会在内置中间件及可选的循环/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()])`。
|
||||
应根据配置的所有者和插入位置选择注册方式。固定槽位(并未弃用)`extensions.middlewares` 可写在 `config.yaml` 或 `extensions_config.json` 中(前者优先),默认同时作用于主 Agent 和子 Agent。通过顶层 `plugins:` 注册的打包扩展可在语义化扩展点贡献中间件。若贡献代码需要可提交、可编程且仅作用于主 Agent 的接线,请使用 `build_middlewares(..., custom_middlewares=[AuditMiddleware()])`。
|
||||
|
||||
对于 `create_deerflow_agent`,`@Next` 或 `@Prev` 锚点必须指向这个较小链中实际存在的中间件;否则使用完整中间件列表中的锚点会解析失败。
|
||||
|
||||
|
||||
@ -246,10 +246,13 @@ extensions:
|
||||
middlewares:
|
||||
- my_company.deerflow_middlewares:MyMiddleware
|
||||
- class: my_company.deerflow_middlewares:MyMiddleware
|
||||
apply_to: lead
|
||||
kwargs:
|
||||
max_tool_calls: 5
|
||||
```
|
||||
|
||||
对象条目可选 `apply_to`,取值为 `both`(默认)、`lead` 或 `subagent`。字符串条目和省略该字段的对象继续作用于两条链。非法取值在配置校验时失败。宿主先选择作用对象,再导入和构造中间件,因此被排除的条目不会在该链产生导入或构造副作用;入选条目保持原有相对顺序。这只选择固定槽位配置路径;打包扩展继续使用已有的作用范围和放置接口。
|
||||
|
||||
配置的中间件会在内置中间件及可选的循环/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()])`。
|
||||
应根据配置的所有者和插入位置选择注册方式。固定槽位(并未弃用)`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