{isSkillsLoading ? (
技能加载中...
) : enabledSkills.length === 0 ? (
diff --git a/offline-backend-20260512/backend/packages/harness/deerflow/agents/middlewares/setup_agent_approval_middleware.py b/offline-backend-20260512/backend/packages/harness/deerflow/agents/middlewares/setup_agent_approval_middleware.py
index 51a90017..9f80b5c7 100644
--- a/offline-backend-20260512/backend/packages/harness/deerflow/agents/middlewares/setup_agent_approval_middleware.py
+++ b/offline-backend-20260512/backend/packages/harness/deerflow/agents/middlewares/setup_agent_approval_middleware.py
@@ -3,16 +3,13 @@
from __future__ import annotations
import logging
-from collections.abc import Callable
from hashlib import sha256
from typing import Any, override
from langchain.agents import AgentState
-from langchain.agents.middleware import AgentMiddleware
-from langchain_core.messages import ToolMessage
-from langgraph.graph import END
-from langgraph.prebuilt.tool_node import ToolCallRequest
-from langgraph.types import Command
+from langchain.agents.middleware import AgentMiddleware, hook_config
+from langchain_core.messages import AIMessage, ToolMessage
+from langgraph.runtime import Runtime
logger = logging.getLogger(__name__)
@@ -26,15 +23,22 @@ class SetupAgentApprovalMiddlewareState(AgentState):
class SetupAgentApprovalMiddleware(AgentMiddleware[SetupAgentApprovalMiddlewareState]):
"""Stop before ``setup_agent`` writes files and ask the user to approve.
- The first ``setup_agent`` call is converted into a structured tool message
- and the run ends. A subsequent request can set ``setup_agent_approved`` in
- runtime context to allow the tool to execute.
+ The interception runs in ``after_model``: as soon as the model emits an
+ AIMessage that contains a ``setup_agent`` tool call (and the run has not
+ been pre-approved via ``setup_agent_approved=True`` in runtime context),
+ we append a structured approval ``ToolMessage`` and return
+ ``jump_to="end"`` to terminate the LangGraph run *before* the tool node
+ executes. This is the LangChain-recommended way to short-circuit after
+ the model; using ``wrap_tool_call`` + ``Command(goto=END)`` proved
+ unreliable when the AIMessage contained multiple parallel tool calls
+ (the ``Command`` got mixed with normal tool outputs and the graph
+ looped back to the model instead of terminating).
"""
state_schema = SetupAgentApprovalMiddlewareState
- def _is_approved(self, request: ToolCallRequest) -> bool:
- context = getattr(request.runtime, "context", None) or {}
+ def _is_approved(self, runtime: Runtime) -> bool:
+ context = getattr(runtime, "context", None) or {}
if not isinstance(context, dict):
return False
return context.get("setup_agent_approved") is True
@@ -54,37 +58,57 @@ class SetupAgentApprovalMiddleware(AgentMiddleware[SetupAgentApprovalMiddlewareS
"skills": skills if isinstance(skills, list) else [],
}
- def _handle_setup_agent(self, request: ToolCallRequest) -> Command:
- args = request.tool_call.get("args", {}) or {}
- tool_call_id = request.tool_call.get("id", "")
+ def _intercept(self, last: AIMessage) -> dict[str, Any] | None:
+ """Return a state update if setup_agent must be gated; None to passthrough."""
+ tool_calls = list(last.tool_calls or [])
+ setup_call = next((tc for tc in tool_calls if tc.get("name") == "setup_agent"), None)
+ if setup_call is None:
+ return None
+
+ args = setup_call.get("args") or {}
+ tool_call_id = setup_call.get("id") or ""
payload = self._build_payload(args)
logger.info("Setup agent approval requested before writing agent files")
- message = ToolMessage(
+ approval_msg = ToolMessage(
id=self._stable_message_id(tool_call_id, args),
content="请确认是否创建/更新这个专家配置。",
tool_call_id=tool_call_id,
name="setup_agent",
additional_kwargs={"setup_agent_approval": payload},
)
- return Command(update={"messages": [message]}, goto=END)
- @override
- def wrap_tool_call(
- self,
- request: ToolCallRequest,
- handler: Callable[[ToolCallRequest], ToolMessage | Command],
- ) -> ToolMessage | Command:
- if request.tool_call.get("name") != "setup_agent" or self._is_approved(request):
- return handler(request)
- return self._handle_setup_agent(request)
+ # Any other tool_calls on the same AIMessage are left dangling; the
+ # existing DanglingToolCallMiddleware will inject placeholder
+ # ToolMessages on the next model invocation, which matches the
+ # desired semantics: every action pauses until the user approves.
+ return {
+ "messages": [approval_msg],
+ "jump_to": "end",
+ }
+ @hook_config(can_jump_to=["end"])
@override
- async def awrap_tool_call(
+ def after_model(
self,
- request: ToolCallRequest,
- handler: Callable[[ToolCallRequest], ToolMessage | Command],
- ) -> ToolMessage | Command:
- if request.tool_call.get("name") != "setup_agent" or self._is_approved(request):
- return await handler(request)
- return self._handle_setup_agent(request)
+ state: SetupAgentApprovalMiddlewareState,
+ runtime: Runtime,
+ ) -> dict[str, Any] | None:
+ if self._is_approved(runtime):
+ return None
+ messages = state.get("messages") or []
+ if not messages:
+ return None
+ last = messages[-1]
+ if not isinstance(last, AIMessage):
+ return None
+ return self._intercept(last)
+
+ @hook_config(can_jump_to=["end"])
+ @override
+ async def aafter_model(
+ self,
+ state: SetupAgentApprovalMiddlewareState,
+ runtime: Runtime,
+ ) -> dict[str, Any] | None:
+ return self.after_model(state, runtime)