diff --git a/CHANGELOG.md b/CHANGELOG.md index 44d65fd3f3..aee605f2a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1397,6 +1397,13 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. ### Fixed +- **channels:** IM channel runs now use the top-level `recursion_limit`, as + Gateway API runs do, instead of a fixed `100` that could end a multi-step + task after a few tool-calling turns without an answer. A `recursion_limit` + in an IM channel's `session.config` still takes precedence; earlier README + examples set it to `100`, so delete that line from `config.yaml` to follow + the top-level value. GitHub channel runs are unchanged. ([#7521]) + - **sandbox:** The AIO provider's async acquire no longer checks the thread's skill directory on the event loop. Picking the sandbox ID ran a filesystem `stat` and path resolution inline on every thread acquire, which stalls the @@ -13091,3 +13098,4 @@ with **180 merged pull requests** since the first 2.0 milestone tag. [#7445]: https://github.com/bytedance/deer-flow/pull/7445 [#7449]: https://github.com/bytedance/deer-flow/pull/7449 [#7507]: https://github.com/bytedance/deer-flow/pull/7507 +[#7521]: https://github.com/bytedance/deer-flow/pull/7521 diff --git a/CHANGELOG_zh.md b/CHANGELOG_zh.md index 315bbb7d06..be7ffe58d8 100644 --- a/CHANGELOG_zh.md +++ b/CHANGELOG_zh.md @@ -1112,6 +1112,11 @@ ### 修复 +- **渠道:** IM 渠道的运行现在与 Gateway API 运行一样使用顶层 `recursion_limit`,不再固定 + 发送 `100`;此前多步任务可能在几轮工具调用后就中止,用户收不到回答。IM 渠道 + `session.config` 中的 `recursion_limit` 仍然优先;早先的 README 示例把它设为 `100`, + 如需跟随顶层设置,请从 `config.yaml` 删除这一行。GitHub 渠道的运行不变。([#7521]) + - **沙箱:** AIO 提供方的异步 acquire 不再在事件循环上检查线程的技能目录。此前每次 线程 acquire 选择沙箱 ID 时,都会在事件循环上直接执行文件系统 `stat` 和路径解析; `DEER_FLOW_HOME` 位于较慢或网络存储上时会阻塞事件循环。现在这一步与 acquire 的 @@ -10451,3 +10456,4 @@ DeerFlow 2.0 是围绕"超级智能体"框架的彻底重写,核心包含子 [#7445]: https://github.com/bytedance/deer-flow/pull/7445 [#7449]: https://github.com/bytedance/deer-flow/pull/7449 [#7507]: https://github.com/bytedance/deer-flow/pull/7507 +[#7521]: https://github.com/bytedance/deer-flow/pull/7521 diff --git a/README.md b/README.md index 963e8c0021..6737ec36cc 100644 --- a/README.md +++ b/README.md @@ -537,8 +537,10 @@ Gateway runs use the top-level `recursion_limit` in `config.yaml` when an API request does not provide one. The default is `100`; valid per-request values take precedence, and `max_recursion_limit` (default `1000`) caps both. Changes apply to the next run without restarting the Gateway. This top-level setting -applies to Gateway API runs; IM channel and embedded `DeerFlowClient` runs -retain their own defaults and per-call override paths. +applies to Gateway API and IM channel runs. A `recursion_limit` in an IM +channel's `session.config` takes precedence, and GitHub channel runs keep +their own floor. Embedded `DeerFlowClient` runs retain their own defaults and +per-call override paths. A custom frontend can add an optional `context.client` to a Gateway run request: a client name, capabilities such as `artifacts` or `charts`, and display @@ -1192,8 +1194,6 @@ channels: # Optional: global session defaults for all mobile channels session: assistant_id: lead_agent # or a custom agent name; custom agents are routed via lead_agent + agent_name - config: - recursion_limit: 100 context: thinking_enabled: true is_plan_mode: false diff --git a/README_fr.md b/README_fr.md index 1ec607817f..8c4bb58abb 100644 --- a/README_fr.md +++ b/README_fr.md @@ -328,8 +328,6 @@ channels: # Optional: global session defaults for all mobile channels session: assistant_id: lead_agent - config: - recursion_limit: 100 context: thinking_enabled: true is_plan_mode: false diff --git a/README_ja.md b/README_ja.md index 4cac812612..f9610b7abb 100644 --- a/README_ja.md +++ b/README_ja.md @@ -315,8 +315,6 @@ channels: # オプション: 全モバイルチャネルのグローバルセッションデフォルト session: assistant_id: lead_agent - config: - recursion_limit: 100 context: thinking_enabled: true is_plan_mode: false diff --git a/README_ko.md b/README_ko.md index e7d8a586e5..06acbbb342 100644 --- a/README_ko.md +++ b/README_ko.md @@ -378,7 +378,7 @@ Docker 빌드는 기본적으로 업스트림 `uv` 레지스트리를 사용합 백엔드 프로세스는 다음 설정 접근 시 `config.yaml` 변경 사항을 자동으로 반영하므로, 개발 중 모델 메타데이터를 업데이트할 때 수동으로 재시작할 필요가 없습니다. -Gateway 실행(run)은 API 요청이 값을 지정하지 않은 경우 최상위 `recursion_limit` 값(`config.yaml`에 설정)을 사용합니다. 기본값은 `100`이며, 유효한 요청별 값이 우선하고 `max_recursion_limit`(기본값 `1000`)이 두 값 모두의 상한이 됩니다. 변경 사항은 Gateway를 재시작하지 않아도 다음 실행부터 적용됩니다. 이 최상위 설정은 Gateway API 실행에 적용되며, IM 채널 실행과 임베디드 `DeerFlowClient` 실행은 각자의 기본값과 호출별 재정의 경로를 그대로 유지합니다. +Gateway 실행(run)은 API 요청이 값을 지정하지 않은 경우 최상위 `recursion_limit` 값(`config.yaml`에 설정)을 사용합니다. 기본값은 `100`이며, 유효한 요청별 값이 우선하고 `max_recursion_limit`(기본값 `1000`)이 두 값 모두의 상한이 됩니다. 변경 사항은 Gateway를 재시작하지 않아도 다음 실행부터 적용됩니다. 이 최상위 설정은 Gateway API 실행과 IM 채널 실행에 적용됩니다. IM 채널의 `session.config`에 지정한 `recursion_limit`이 우선하며, GitHub 채널 실행은 자체 하한을 유지합니다. 임베디드 `DeerFlowClient` 실행은 각자의 기본값과 호출별 재정의 경로를 그대로 유지합니다. 체크포인트 저장소 설정인 `database.checkpoint_channel_mode`와 `database.checkpoint_delta.snapshot_frequency`(기본값 `10`)는 예외입니다. 두 설정 모두 프로세스가 처음 에이전트를 빌드할 때(`DeerFlowClient`를 통한 경우 포함) 고정되므로, 안전하게 변경하려면 프로세스를 재시작해야 합니다. 선택 사항인 `database.checkpoint_cache` 섹션(delta 채널 모드 전용)은 구체화된 체크포인트 이력을 캐시합니다. `type`은 `memory`(기본값) 또는 `redis`이며, `max_entries: 0`으로 설정하면 캐시가 비활성화됩니다. `redis` 백엔드는 Gateway/비동기 경로 전용이며, 동기 TUI/임베디드 경로는 `memory`만 지원합니다. 이 캐시는 성능만을 위한 것으로 비활성화해도 결과가 동일합니다. 따라서 고정되는 일이 없으며, 하나의 체크포인트 데이터베이스를 공유하는 워커들이 서로 다른 캐시 설정으로 실행되어도 안전합니다. Redis 스레드 정리는 설정된 `key_prefix`를 glob 메타문자까지 포함해 리터럴 텍스트로 취급하므로, 삭제는 해당 네임스페이스와 스레드 안에서만 이루어집니다. @@ -706,8 +706,6 @@ channels: # Optional: global session defaults for all mobile channels session: assistant_id: lead_agent # or a custom agent name; custom agents are routed via lead_agent + agent_name - config: - recursion_limit: 100 context: thinking_enabled: true is_plan_mode: false diff --git a/README_pt.md b/README_pt.md index eee3d2ba2f..e00cd8f561 100644 --- a/README_pt.md +++ b/README_pt.md @@ -335,7 +335,7 @@ O tráfego de controle do sandbox AIO local é sempre direto: endereços de loop Os processos do backend pegam as mudanças do `config.yaml` automaticamente no próximo acesso à configuração, então atualizar metadados de modelo não exige reinício manual durante o desenvolvimento. -Os runs do Gateway usam o `recursion_limit` de nível superior do `config.yaml` quando a requisição de API não informa um. O padrão é `100`; valores válidos por requisição têm precedência, e `max_recursion_limit` (padrão `1000`) limita os dois. As mudanças valem para o próximo run sem reiniciar o Gateway. Essa configuração de nível superior se aplica aos runs da API do Gateway; os runs de canais de IM e do `DeerFlowClient` embutido mantêm os próprios padrões e caminhos de override por chamada. As configurações de armazenamento de checkpoint `database.checkpoint_channel_mode` e `database.checkpoint_delta.snapshot_frequency` (padrão `10`) são exceções: as duas ficam congeladas quando o processo constrói um agente pela primeira vez (inclusive via `DeerFlowClient`) e exigem reinício do processo para serem alteradas com segurança. +Os runs do Gateway usam o `recursion_limit` de nível superior do `config.yaml` quando a requisição de API não informa um. O padrão é `100`; valores válidos por requisição têm precedência, e `max_recursion_limit` (padrão `1000`) limita os dois. As mudanças valem para o próximo run sem reiniciar o Gateway. Essa configuração de nível superior se aplica aos runs da API do Gateway e dos canais de IM. Um `recursion_limit` no `session.config` de um canal de IM tem precedência, e os runs do canal GitHub mantêm o próprio piso. Os runs do `DeerFlowClient` embutido mantêm os próprios padrões e caminhos de override por chamada. As configurações de armazenamento de checkpoint `database.checkpoint_channel_mode` e `database.checkpoint_delta.snapshot_frequency` (padrão `10`) são exceções: as duas ficam congeladas quando o processo constrói um agente pela primeira vez (inclusive via `DeerFlowClient`) e exigem reinício do processo para serem alteradas com segurança. A seção opcional `database.checkpoint_cache` (apenas no modo de canal delta) faz cache dos históricos de checkpoint materializados: `type` é `memory` (padrão) ou `redis`, e `max_entries: 0` desativa o cache. O backend `redis` funciona só no Gateway/async; o caminho síncrono da TUI e do cliente embutido aceita apenas `memory`. O cache serve só para desempenho, os resultados são idênticos com ele desativado. Por isso ele nunca é congelado, e workers que compartilham o mesmo banco de checkpoints podem usar configurações de cache diferentes sem problema. @@ -606,8 +606,6 @@ channels: # Optional: global session defaults for all mobile channels session: assistant_id: lead_agent # or a custom agent name; custom agents are routed via lead_agent + agent_name - config: - recursion_limit: 100 context: thinking_enabled: true is_plan_mode: false diff --git a/README_zh.md b/README_zh.md index a55eded469..9f492b5632 100644 --- a/README_zh.md +++ b/README_zh.md @@ -462,8 +462,6 @@ channels: # 可选:所有移动端渠道共用的全局 session 默认值 session: assistant_id: lead_agent # 也可以填自定义 agent 名;渠道层会自动转换为 lead_agent + agent_name - config: - recursion_limit: 100 context: thinking_enabled: true is_plan_mode: false diff --git a/backend/app/channels/manager.py b/backend/app/channels/manager.py index 620dc99a30..9d5331e34a 100644 --- a/backend/app/channels/manager.py +++ b/backend/app/channels/manager.py @@ -87,12 +87,13 @@ IN_PROCESS_CACHE_MAX_ENTRIES = 4096 MAX_CHANNEL_AGENT_LIST_ITEMS = 50 MAX_CHANNEL_AGENT_DESCRIPTION_CHARS = 120 -# Lead-agent recursion budget (LangGraph super-steps for the lead graph only). -# This is independent of subagent depth: a `task()` dispatch runs the whole -# subagent inside ONE lead tools-node step, and subagents enforce their own -# limit via `subagents.max_turns` (see SubagentExecutor). Do not conflate this -# 100 with the general-purpose subagent's max_turns. -DEFAULT_RUN_CONFIG: dict[str, Any] = {"recursion_limit": 100} +# No lead-agent recursion budget here: like Gateway API runs, IM runs get +# the Gateway's configured top-level ``recursion_limit`` (clamped by +# ``max_recursion_limit``) unless a session ``config.recursion_limit`` sets +# one. The budget counts lead-graph super-steps only: a `task()` dispatch runs +# the whole subagent inside ONE lead tools-node step, and subagents enforce +# their own limit via `subagents.max_turns` (see SubagentExecutor). +DEFAULT_RUN_CONFIG: dict[str, Any] = {} DEFAULT_RUN_CONTEXT: dict[str, Any] = { "thinking_enabled": True, "is_plan_mode": False, @@ -2190,7 +2191,7 @@ class ChannelManager: if isinstance(override, int) and override > 0: run_config["recursion_limit"] = override else: - run_config["recursion_limit"] = max(run_config.get("recursion_limit", 100), policy.default_recursion_limit) + run_config["recursion_limit"] = max(run_config.get("recursion_limit", 0), policy.default_recursion_limit) return assistant_id, run_config, run_context diff --git a/backend/app/channels/run_policy.py b/backend/app/channels/run_policy.py index 3ab3de1740..79d8176e00 100644 --- a/backend/app/channels/run_policy.py +++ b/backend/app/channels/run_policy.py @@ -53,8 +53,9 @@ class ChannelRunPolicy: for an IM channel). default_recursion_limit: When set, the manager raises ``run_config["recursion_limit"]`` to ``max(existing, - limit)``. None leaves the global default (100) untouched — - interactive chat turns don't need 250 super-steps. + limit)``. None keeps the session config's value, if + any; without one the run gets the Gateway's configured + ``recursion_limit``. credentials_provider: Optional async hook that mutates ``run_context`` with platform-specific credentials. Called after ``_resolve_run_params``. Exceptions are caught and diff --git a/backend/app/gateway/github/run_policy.py b/backend/app/gateway/github/run_policy.py index f67685f677..0498b41c4d 100644 --- a/backend/app/gateway/github/run_policy.py +++ b/backend/app/gateway/github/run_policy.py @@ -106,7 +106,7 @@ def register_policy() -> None: is_interactive=False, interaction_mode="webhook", # Autonomous coder runs (clone -> edit -> test -> push -> PR) - # routinely need more than the 100 super-step interactive ceiling. + # routinely need more than 100 super-steps. # Per-agent overrides via GitHubAgentConfig.recursion_limit still # win (read in ChannelManager._resolve_run_params from msg.metadata). default_recursion_limit=250, diff --git a/backend/app/gateway/services.py b/backend/app/gateway/services.py index 8824e272d3..c783b65384 100644 --- a/backend/app/gateway/services.py +++ b/backend/app/gateway/services.py @@ -1331,9 +1331,9 @@ def build_run_config( load the matching ``agents//SOUL.md`` and per-agent config — without it the agent silently runs as the default lead agent. - This mirrors the channel manager's ``_resolve_run_params`` logic except for - the recursion default: Gateway API runs use the configured top-level - ``recursion_limit``, while IM channel runs retain their own default. + This mirrors the channel manager's ``_resolve_run_params`` logic. IM + channel runs send no ``recursion_limit`` unless their session config or + channel policy sets one, so they get the same configured default here. """ # Lead-agent recursion budget (LangGraph super-steps for the lead graph # only). Independent of subagent depth: a `task()` dispatch runs the whole diff --git a/backend/docs/API.md b/backend/docs/API.md index c0e4ce8010..8296143a18 100644 --- a/backend/docs/API.md +++ b/backend/docs/API.md @@ -380,9 +380,11 @@ or configured value to a server ceiling (`max_recursion_limit` in `config.yaml`, default `1000`) so a single run cannot execute unbounded graph steps (runaway LLM cost / DoS); invalid or non-positive request values fall back to the configured default. Both top-level fields are read per run, so edits apply to -the next request without restarting the Gateway. This top-level setting applies -to Gateway API runs only; IM channel and embedded `DeerFlowClient` runs retain -their own defaults and override paths. +the next request without restarting the Gateway. IM channel runs get the same +default unless a channel `session.config` sets `recursion_limit` (GitHub channel +runs have their own floor; see +[CONFIGURATION.md](CONFIGURATION.md#recursion-limits)); embedded +`DeerFlowClient` runs retain their own defaults and override paths. **Configurable Options:** - `model_name` (string): Override the default model diff --git a/backend/docs/CONFIGURATION.md b/backend/docs/CONFIGURATION.md index 1c0cddd89b..f7f06ea223 100644 --- a/backend/docs/CONFIGURATION.md +++ b/backend/docs/CONFIGURATION.md @@ -145,9 +145,11 @@ configured default. `max_recursion_limit` (default `1000`) caps both sources to limit runaway LLM cost. Both settings are read per run, so changes apply to the next request without a Gateway restart. -These settings apply to Gateway API runs. IM channel runs and embedded -`DeerFlowClient` runs retain their own defaults and can be overridden through -their channel/client-specific configuration or per-call options. +These settings also apply to IM channel runs. A `recursion_limit` in an IM +channel's `session.config` (for all channels, one channel or one user) takes +precedence. The GitHub channel instead uses `250`, or a higher +`session.config` value, unless the agent sets `github.recursion_limit`. +Embedded `DeerFlowClient` runs retain their own defaults and per-call options. ### Models diff --git a/backend/tests/test_channels.py b/backend/tests/test_channels.py index 873ae7f5be..273c477d64 100644 --- a/backend/tests/test_channels.py +++ b/backend/tests/test_channels.py @@ -5261,19 +5261,33 @@ class TestResolveRunParamsUserId: assert run_context["channel_user_id"] == "U-platform" def test_github_channel_gets_raised_recursion_limit(self): - """Autonomous GitHub coding runs (clone → edit → test → push → PR) need - more super-steps than an interactive chat turn. The default - ``recursion_limit`` of 100 is raised for the github channel only.""" + """Autonomous GitHub coding runs (clone → edit → test → push → PR) get + a channel floor of 250 super-steps; other channels send no + ``recursion_limit`` and leave the budget to the Gateway.""" manager = self._manager() gh_msg = InboundMessage(channel_name="github", chat_id="zhfeng/llm-gateway", user_id="zhfeng", text="hi") _, gh_config, _ = manager._resolve_run_params(gh_msg, "thread-1") assert gh_config["recursion_limit"] >= 250 - # Interactive channels keep the default ceiling. slack_msg = InboundMessage(channel_name="slack", chat_id="C1", user_id="u", text="hi") _, slack_config, _ = manager._resolve_run_params(slack_msg, "thread-1") - assert slack_config["recursion_limit"] == 100 + assert "recursion_limit" not in slack_config + + def test_im_run_uses_gateway_configured_recursion_limit(self): + """An IM run without a session value gets the operator's top-level + ``recursion_limit``, like Gateway API runs, instead of a fixed 100.""" + from app.gateway.services import build_run_config + from deerflow.config.app_config import AppConfig, reset_app_config, set_app_config + + set_app_config(AppConfig.model_validate({"sandbox": {"use": "deerflow.sandbox.local:LocalSandboxProvider"}, "recursion_limit": 700})) + try: + manager = self._manager() + msg = InboundMessage(channel_name="telegram", chat_id="c", user_id="u", text="hi") + _, run_config, _ = manager._resolve_run_params(msg, "thread-1") + assert build_run_config("thread-1", run_config, None)["recursion_limit"] == 700 + finally: + reset_app_config() def test_github_channel_recursion_limit_respects_higher_override(self): """An explicit higher recursion_limit in channel/user config must not be diff --git a/config.example.yaml b/config.example.yaml index 628c8aa3e3..88b3b324c0 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -3119,8 +3119,6 @@ run_ownership: # # Optional: default mobile/session settings for all IM channels # session: # assistant_id: lead_agent # or a custom agent name; custom agents route via lead_agent + agent_name -# config: -# recursion_limit: 100 # context: # thinking_enabled: true # is_plan_mode: false