Ultra 模式的核心是 subagent 调度系统,整个调用链分为六个环节:


1. 中间件层:并发控制

build_middlewares()subagent_enabled=True 时加入 SubagentLimitMiddleware(默认 max_concurrent=3): 1

该中间件在 after_model 钩子中检查 LLM 输出的 task 工具调用数量,超过限制的调用直接从 AIMessage.tool_calls 中截断(同步更新 additional_kwargs["tool_calls"] 保持一致性): 2


2. 系统提示:编排策略注入

apply_prompt_template()subagent_enabled=True 时将 <subagent_system> 章节注入系统提示,告知 LLM:

  • 最多同时发起 N 个 task 调用(硬限制)
    让我查看 SubagentLimitMiddleware 的完整实现。

硬限制完全体现在 SubagentLimitMiddleware._truncate_task_calls() 中,是代码层面的强制截断,不依赖 LLM 是否遵守 prompt 指令。

核心逻辑在这里: 1

执行步骤:

  1. 找出 LLM 输出的最后一条 AIMessage 中所有 name == "task" 的工具调用索引
  2. 如果数量 ≤ max_concurrent,直接返回 None(不修改)
  3. 超出部分的索引放入 indices_to_drop,过滤掉它们
  4. clone_ai_message_with_tool_calls() 替换原 AIMessage,保持消息 ID 不变(触发 LangGraph 的 state 替换语义)

这个截断发生在 after_model 钩子,即 LLM 生成响应之后、工具实际执行之前2

max_concurrent 的值被 clamp 到 [2, 4],默认值 MAX_CONCURRENT_SUBAGENTS=3,即使用户在 config 中配置了超出范围的值也会被强制修正: 3

系统提示中的 <subagent_system> 章节(“⛔ HARD CONCURRENCY LIMIT”)是软提示,目的是让 LLM 主动分批规划,避免超额调用被静默丢弃。而 SubagentLimitMiddleware 才是真正的硬保障——即使 LLM 无视 prompt,超出的 task 调用也会在到达执行层之前被截断。

Citations

File: backend/packages/harness/deerflow/agents/middlewares/subagent_limit_middleware.py (L16-22)

MIN_SUBAGENT_LIMIT = 2
MAX_SUBAGENT_LIMIT = 4


def _clamp_subagent_limit(value: int) -> int:
    """Clamp subagent limit to valid range [2, 4]."""
    return max(MIN_SUBAGENT_LIMIT, min(MAX_SUBAGENT_LIMIT, value))

File: backend/packages/harness/deerflow/agents/middlewares/subagent_limit_middleware.py (L54-68)

        # Count task tool calls
        task_indices = [i for i, tc in enumerate(tool_calls) if tc.get("name") == "task"]
        if len(task_indices) <= self.max_concurrent:
            return None

        # Build set of indices to drop (excess task calls beyond the limit)
        indices_to_drop = set(task_indices[self.max_concurrent :])
        truncated_tool_calls = [tc for i, tc in enumerate(tool_calls) if i not in indices_to_drop]

        dropped_count = len(indices_to_drop)
        logger.warning(f"Truncated {dropped_count} excess task tool call(s) from model response (limit: {self.max_concurrent})")

        # Replace the AIMessage with truncated tool_calls (same id triggers replacement)
        updated_msg = clone_ai_message_with_tool_calls(last_msg, truncated_tool_calls)
        return {"messages": [updated_msg]}

File: backend/packages/harness/deerflow/agents/middlewares/subagent_limit_middleware.py (L70-76)

    @override
    def after_model(self, state: AgentState, runtime: Runtime) -> dict | None:
        return self._truncate_task_calls(state)

    @override
    async def aafter_model(self, state: AgentState, runtime: Runtime) -> dict | None:
        return self._truncate_task_calls(state)
  • 如何分批执行超过 N 个子任务
  • 何时应该直接执行 vs 委托给子 agent 3

3. task_tool:启动子 agent

LLM 调用 task 工具时,task_tool() 执行以下步骤:

① 从父 agent 的 runtime 提取上下文(sandbox_state、thread_id、parent_model、trace_id、user_id、user_role 等),确保子 agent 继承父 agent 的身份和沙箱状态: 4

② 获取子 agent 工具,强制 subagent_enabled=False 防止递归嵌套,继承父 agent 的 tool_groups 限制: 5

③ 创建 SubagentExecutor 并启动后台执行,以 tool_call_id 作为 task_id 便于追踪: 6


4. SubagentExecutor.execute_async():双线程池架构

execute_async()
  ├── 创建 SubagentResult(status=PENDING) 写入 _background_tasks 字典
  ├── copy_context() 捕获父 agent 的 contextvars(用户上下文等)
  └── _scheduler_pool.submit(run_task)   ← 3 workers
        └── run_task()
              └── _submit_to_isolated_loop_in_context()
                    └── 持久化独立 asyncio 事件循环(daemon 线程)
                          └── _aexecute(task, result_holder)
                                ├── _build_initial_state()
                                ├── _create_agent()  ← checkpointer=False
                                └── agent.astream()  ← 流式执行

子 agent 运行在持久化的独立 asyncio 事件循环中(避免嵌套事件循环问题),超时由 future.result(timeout=config.timeout_seconds) 强制执行(默认 1800s): 7


5. task_tool 轮询:SSE 事件推送

task_tool 在后台任务运行期间每 5 秒轮询一次,通过 get_stream_writer() 向前端推送 SSE 事件:

事件类型 触发时机
task_started 任务启动时立即发送
task_running 子 agent 产生新 AI 消息时(每条消息一个事件)
task_completed 子 agent 成功完成
task_failed 子 agent 执行失败
task_cancelled 用户取消
task_timed_out 超时

6. 前端:子任务卡片渲染

hooks.tsonCustomEvent 处理 task_running 事件,调用 updateSubtask() 更新子任务状态: 9

SubtaskCard 组件根据 task.status 展示进度(in_progress 显示最新工具调用,completed 显示结果,failed 显示错误): 10


完整调用链

LLM 输出 task 工具调用
(最多 3 个并发)

SubagentLimitMiddleware
after_model 截断超额调用

task_tool() 被调用
提取父 agent 上下文

SubagentExecutor.execute_async()
写入 _background_tasks

_scheduler_pool
(3 workers)

持久化独立 asyncio 事件循环
_aexecute()

子 agent 流式执行
checkpointer=False

task_tool 每 5s 轮询
get_background_task_result()

SSE 事件推送
task_running / task_completed

前端 SubtaskCard
实时展示进度

结果字符串返回给 LLM
继续主对话

Citations

File: backend/packages/harness/deerflow/agents/lead_agent/agent.py (L358-362)

    # Add SubagentLimitMiddleware to truncate excess parallel task calls
    subagent_enabled = cfg.get("subagent_enabled", False)
    if subagent_enabled:
        max_concurrent_subagents = cfg.get("max_concurrent_subagents", 3)
        middlewares.append(SubagentLimitMiddleware(max_concurrent=max_concurrent_subagents))

File: backend/tests/test_subagent_limit_middleware.py (L93-105)

    def test_task_calls_exceeding_limit_truncated(self):
        mw = SubagentLimitMiddleware(max_concurrent=2)
        msg = AIMessage(
            content="",
            tool_calls=[_task_call("t1"), _task_call("t2"), _task_call("t3"), _task_call("t4")],
        )
        result = mw._truncate_task_calls({"messages": [msg]})
        assert result is not None
        updated_msg = result["messages"][0]
        task_calls = [tc for tc in updated_msg.tool_calls if tc["name"] == "task"]
        assert len(task_calls) == 2
        assert task_calls[0]["id"] == "t1"
        assert task_calls[1]["id"] == "t2"

File: backend/packages/harness/deerflow/agents/lead_agent/prompt.py (L236-256)

    return f"""<subagent_system>
**🚀 SUBAGENT MODE ACTIVE - DECOMPOSE, DELEGATE, SYNTHESIZE**

You are running with subagent capabilities enabled. Your role is to be a **task orchestrator**:
1. **DECOMPOSE**: Break complex tasks into parallel sub-tasks
2. **DELEGATE**: Launch multiple subagents simultaneously using parallel `task` calls
3. **SYNTHESIZE**: Collect and integrate results into a coherent answer

**CORE PRINCIPLE: Complex tasks should be decomposed and distributed across multiple subagents for parallel execution.**

**⛔ HARD CONCURRENCY LIMIT: MAXIMUM {n} `task` CALLS PER RESPONSE. THIS IS NOT OPTIONAL.**
- Each response, you may include **at most {n}** `task` tool calls. Any excess calls are **silently discarded** by the system — you will lose that work.
- **Before launching subagents, you MUST count your sub-tasks in your thinking:**
  - If count ≤ {n}: Launch all in this response.
  - If count > {n}: **Pick the {n} most important/foundational sub-tasks for this turn.** Save the rest for the next turn.
- **Multi-batch execution** (for >{n} sub-tasks):
  - Turn 1: Launch sub-tasks 1-{n} in parallel → wait for results
  - Turn 2: Launch next batch in parallel → wait for results
  - ... continue until all sub-tasks are complete
  - Final turn: Synthesize ALL results into a coherent answer
- **Example thinking pattern**: "I identified 6 sub-tasks. Since the limit is {n} per turn, I will launch the first {n} now, and the rest in the next turn."

File: backend/packages/harness/deerflow/tools/builtins/task_tool.py (L260-289)

    if runtime is not None:
        sandbox_state = runtime.state.get("sandbox")
        thread_data = runtime.state.get("thread_data")
        thread_id = runtime.context.get("thread_id") if runtime.context else None
        if thread_id is None:
            thread_id = runtime.config.get("configurable", {}).get("thread_id")

        # Try to get parent model from configurable
        metadata = runtime.config.get("metadata", {})
        parent_model = metadata.get("model_name")

        # Get or generate trace_id for distributed tracing
        trace_id = metadata.get("trace_id") or str(uuid.uuid4())[:8]

    # Get user_id for tracing (uses standard resolution order)
    user_id = resolve_runtime_user_id(runtime)

    # Propagate the authenticated runtime context so delegated tool calls are
    # evaluated by GuardrailMiddleware with the same identity/attribution as
    # the lead agent. Sourced from the server-side context written by
    # inject_authenticated_user_context (and run_id by the run worker); stays
    # None when absent (e.g. internal-auth runs) so guardrail behavior is
    # unchanged. Without this, role-aware policy silently mis-attributes any
    # tool call delegated to a subagent (user_role=None).
    parent_context = runtime.context if runtime is not None else None
    parent_context = parent_context if isinstance(parent_context, dict) else {}
    user_role = parent_context.get("user_role")
    oauth_provider = parent_context.get("oauth_provider")
    oauth_id = parent_context.get("oauth_id")
    run_id = parent_context.get("run_id")

File: backend/packages/harness/deerflow/tools/builtins/task_tool.py (L309-317)

    # Subagents should not have subagent tools enabled (prevent recursive nesting)
    available_tools_kwargs = {
        "model_name": effective_model,
        "groups": parent_tool_groups,
        "subagent_enabled": False,
    }
    if resolved_app_config is not None:
        available_tools_kwargs["app_config"] = resolved_app_config
    tools = get_available_tools(**available_tools_kwargs)

File: backend/packages/harness/deerflow/tools/builtins/task_tool.py (L336-353)

    executor = SubagentExecutor(**executor_kwargs)

    # Start background execution (always async to prevent blocking)
    # Use tool_call_id as task_id for better traceability
    task_id = executor.execute_async(prompt, task_id=tool_call_id)

    # Poll for task completion in backend (removes need for LLM to poll)
    poll_count = 0
    last_status = None
    last_message_count = 0  # Track how many AI messages we've already sent
    # Polling timeout: execution timeout + 60s buffer, checked every 5s
    max_poll_count = (config.timeout_seconds + 60) // 5

    logger.info(f"[trace={trace_id}] Started background task {task_id} (subagent={subagent_type}, timeout={config.timeout_seconds}s, polling_limit={max_poll_count} polls)")

    writer = get_stream_writer()
    # Send Task Started message'
    writer({"type": "task_started", "task_id": task_id, "description": description})

File: backend/packages/harness/deerflow/tools/builtins/task_tool.py (L355-421)

    try:
        while True:
            result = get_background_task_result(task_id)

            if result is None:
                logger.error(f"[trace={trace_id}] Task {task_id} not found in background tasks")
                writer({"type": "task_failed", "task_id": task_id, "error": "Task disappeared from background tasks"})
                cleanup_background_task(task_id)
                return f"Error: Task {task_id} disappeared from background tasks"

            # Log status changes for debugging
            if result.status != last_status:
                logger.info(f"[trace={trace_id}] Task {task_id} status: {result.status.value}")
                last_status = result.status

            # Check for new AI messages and send task_running events
            ai_messages = result.ai_messages or []
            current_message_count = len(ai_messages)
            if current_message_count > last_message_count:
                # Send task_running event for each new message
                for i in range(last_message_count, current_message_count):
                    message = ai_messages[i]
                    writer(
                        {
                            "type": "task_running",
                            "task_id": task_id,
                            "message": message,
                            "message_index": i + 1,  # 1-based index for display
                            "total_messages": current_message_count,
                        }
                    )
                    logger.info(f"[trace={trace_id}] Task {task_id} sent message #{i + 1}/{current_message_count}")
                last_message_count = current_message_count

            # Check if task completed, failed, or timed out
            usage = _summarize_usage(getattr(result, "token_usage_records", None))
            if result.status == SubagentStatus.COMPLETED:
                _cache_subagent_usage(tool_call_id, usage, enabled=cache_token_usage)
                _report_subagent_usage(runtime, result)
                writer({"type": "task_completed", "task_id": task_id, "result": result.result, "usage": usage})
                logger.info(f"[trace={trace_id}] Task {task_id} completed after {poll_count} polls")
                cleanup_background_task(task_id)
                return f"Task Succeeded. Result: {result.result}"
            elif result.status == SubagentStatus.FAILED:
                _cache_subagent_usage(tool_call_id, usage, enabled=cache_token_usage)
                _report_subagent_usage(runtime, result)
                writer({"type": "task_failed", "task_id": task_id, "error": result.error, "usage": usage})
                logger.error(f"[trace={trace_id}] Task {task_id} failed: {result.error}")
                cleanup_background_task(task_id)
                return f"Task failed. Error: {result.error}"
            elif result.status == SubagentStatus.CANCELLED:
                _cache_subagent_usage(tool_call_id, usage, enabled=cache_token_usage)
                _report_subagent_usage(runtime, result)
                writer({"type": "task_cancelled", "task_id": task_id, "error": result.error, "usage": usage})
                logger.info(f"[trace={trace_id}] Task {task_id} cancelled: {result.error}")
                cleanup_background_task(task_id)
                return "Task cancelled by user."
            elif result.status == SubagentStatus.TIMED_OUT:
                _cache_subagent_usage(tool_call_id, usage, enabled=cache_token_usage)
                _report_subagent_usage(runtime, result)
                writer({"type": "task_timed_out", "task_id": task_id, "error": result.error, "usage": usage})
                logger.warning(f"[trace={trace_id}] Task {task_id} timed out: {result.error}")
                cleanup_background_task(task_id)
                return f"Task timed out. Error: {result.error}"

            # Still running, wait before next poll
            await asyncio.sleep(5)

File: backend/packages/harness/deerflow/subagents/executor.py (L827-888)

    def execute_async(self, task: str, task_id: str | None = None) -> str:
        """Start a task execution in the background.

        Args:
            task: The task description for the subagent.
            task_id: Optional task ID to use. If not provided, a random UUID will be generated.

        Returns:
            Task ID that can be used to check status later.
        """
        # Use provided task_id or generate a new one
        if task_id is None:
            task_id = str(uuid.uuid4())[:8]

        # Create initial pending result
        result = SubagentResult(
            task_id=task_id,
            trace_id=self.trace_id,
            status=SubagentStatus.PENDING,
        )

        logger.info(f"[trace={self.trace_id}] Subagent {self.config.name} starting async execution, task_id={task_id}, timeout={self.config.timeout_seconds}s")

        with _background_tasks_lock:
            _background_tasks[task_id] = result

        parent_context = copy_context()

        # Submit to scheduler pool
        def run_task():
            with _background_tasks_lock:
                _background_tasks[task_id].status = SubagentStatus.RUNNING
                _background_tasks[task_id].started_at = datetime.now()
                result_holder = _background_tasks[task_id]

            try:
                # Submit execution directly to the persistent isolated loop so the
                # background path does not create a temporary loop via execute().
                execution_future = _submit_to_isolated_loop_in_context(
                    parent_context,
                    lambda: self._aexecute(task, result_holder),
                )
                try:
                    # Wait for execution with timeout
                    execution_future.result(timeout=self.config.timeout_seconds)
                except FuturesTimeoutError:
                    logger.error(f"[trace={self.trace_id}] Subagent {self.config.name} execution timed out after {self.config.timeout_seconds}s")
                    # Signal cooperative cancellation and cancel the future
                    result_holder.cancel_event.set()
                    result_holder.try_set_terminal(
                        SubagentStatus.TIMED_OUT,
                        error=f"Execution timed out after {self.config.timeout_seconds} seconds",
                    )
                    execution_future.cancel()
            except Exception as e:
                logger.exception(f"[trace={self.trace_id}] Subagent {self.config.name} async execution failed")
                with _background_tasks_lock:
                    task_result = _background_tasks[task_id]
                task_result.try_set_terminal(SubagentStatus.FAILED, error=str(e))

        _scheduler_pool.submit(run_task)
        return task_id

File: frontend/src/core/threads/hooks.ts (L808-822)

    onCustomEvent(event: unknown) {
      if (
        typeof event === "object" &&
        event !== null &&
        "type" in event &&
        event.type === "task_running"
      ) {
        const e = event as {
          type: "task_running";
          task_id: string;
          message: AIMessage;
        };
        updateSubtask({ id: e.task_id, latestMessage: e.message });
        return;
      }

File: frontend/src/components/workspace/messages/subtask-card.tsx (L138-175)

          {task.status === "in_progress" &&
            task.latestMessage &&
            hasToolCalls(task.latestMessage) && (
              <ChainOfThoughtStep
                label={t.subtasks.in_progress}
                icon={<Loader2Icon className="size-4 animate-spin" />}
              >
                {explainLastToolCall(task.latestMessage, t)}
              </ChainOfThoughtStep>
            )}
          {task.status === "completed" && (
            <>
              <ChainOfThoughtStep
                label={t.subtasks.completed}
                icon={<CheckCircleIcon className="size-4" />}
              ></ChainOfThoughtStep>
              <ChainOfThoughtStep
                label={
                  task.result ? (
                    <MarkdownContent
                      content={task.result}
                      isLoading={false}
                      rehypePlugins={rehypePlugins}
                    />
                  ) : null
                }
              ></ChainOfThoughtStep>
            </>
          )}
          {task.status === "failed" && (
            <ChainOfThoughtStep
              label={<div className="text-red-500">{task.error}</div>}
              icon={<XCircleIcon className="size-4 text-red-500" />}
            ></ChainOfThoughtStep>
          )}
        </ChainOfThoughtContent>
      </div>
    </ChainOfThought>
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐