fix(agent): 收敛超时与取消后的副作用生命周期 (#6388)

This commit is contained in:
InfinityPacer
2026-08-22 12:11:15 +08:00
committed by GitHub
parent 7b6353337d
commit ed3c0541c7
16 changed files with 952 additions and 72 deletions
+35 -10
View File
@@ -1,16 +1,19 @@
"""LangChain 工具调用的 MoviePilot 宿主策略中间件。"""
import asyncio
from collections.abc import Awaitable, Callable
from typing import Any
from langchain.agents.middleware import AgentMiddleware, ToolCallRequest, hook_config
from langchain_core.messages import AIMessage, ToolMessage
from app.agent.policy import (
from app.agent.policy.contracts import (
ToolOrigin,
ToolPolicyContext,
)
from app.agent.policy.orchestrator import (
DEFAULT_TOOL_POLICY_ORCHESTRATOR,
AgentToolPolicyOrchestrator,
ToolPolicyContext,
ToolOrigin,
call_policy_hook,
)
from app.agent.tools.catalog import ToolCatalogSnapshot
@@ -19,6 +22,10 @@ from app.agent.tools.impl.query_system_settings import QuerySystemSettingsTool
POLICY_DENIED_MESSAGE = "当前宿主策略不允许执行该工具。"
POLICY_UNAVAILABLE_MESSAGE = "宿主策略暂时不可用,未执行该工具。"
TOOL_TIMEOUT_MESSAGE = (
"工具执行超时,已停止等待结果;"
"若工具包含外部写操作,操作可能仍在继续,请先确认实际状态再重试。"
)
class AgentPolicyMiddleware(AgentMiddleware):
@@ -107,13 +114,22 @@ class AgentPolicyMiddleware(AgentMiddleware):
arguments = tool_call.get("args") or {}
if not isinstance(arguments, dict):
arguments = {}
_, result = await self.execute_tool_call(
tool=request.tool,
arguments=arguments,
invocation_id=tool_call.get("id"),
handler=lambda: handler(request),
enforce_decision=False,
)
try:
_, result = await self.execute_tool_call(
tool=request.tool,
arguments=arguments,
invocation_id=tool_call.get("id"),
handler=lambda: handler(request),
enforce_decision=False,
)
except TimeoutError:
tool_name = str(getattr(request.tool, "name", None) or "unknown")
return ToolMessage(
content=TOOL_TIMEOUT_MESSAGE,
tool_call_id=str(tool_call.get("id") or ""),
name=tool_name,
status="error",
)
# 普通 ToolNode 保持 shadow 观测;已确认调用使用默认的强制决策语义。
return result
@@ -144,6 +160,15 @@ class AgentPolicyMiddleware(AgentMiddleware):
return False, POLICY_DENIED_MESSAGE
try:
result = await handler()
except asyncio.CancelledError as error:
if observation is not None:
call_policy_hook(
"cancel",
self.orchestrator.fail,
observation,
error,
)
raise
except Exception as error:
if observation is not None:
call_policy_hook(
+57 -26
View File
@@ -23,14 +23,16 @@ from langchain_core.messages import AIMessage, HumanMessage
from langchain_core.tools import BaseTool, StructuredTool
from pydantic import BaseModel, Field
from app.agent.llm import LLMHelper
from app.agent.llm.helper import LLMHelper
from app.agent.middleware.policy import AgentPolicyMiddleware
from app.agent.middleware.utils import append_to_system_message
from app.agent.policy import (
from app.agent.policy.contracts import (
AuthSource,
PrincipalType,
ToolOrigin,
ToolPolicyContext,
)
from app.agent.policy.sanitizer import (
sanitize_for_host,
summarize_error,
)
@@ -46,6 +48,7 @@ SUBAGENT_STREAM_MARKER_KEY = "ls_agent_type"
SUBAGENT_STREAM_MARKER_VALUE = "subagent"
SUBAGENT_DEFAULT_WAIT_TIMEOUT_MS = 60000
SUBAGENT_MAX_WAIT_TIMEOUT_MS = 300000
SUBAGENT_CANCEL_GRACE_SECONDS = 5.0
SUBAGENT_MAX_ACTIVE_TASKS = 8
SUBAGENT_MAX_CONCURRENT_TASKS = 4
SUBAGENT_RESULT_MAX_CHARS = 12000
@@ -890,8 +893,10 @@ class SubAgentTaskControlMiddleware(AgentMiddleware):
)
@staticmethod
async def _cancel_records(records: list[_SubAgentRuntimeTask]) -> None:
"""取消一组尚未完成的任务。"""
async def _cancel_records(
records: list[_SubAgentRuntimeTask],
) -> list[_SubAgentRuntimeTask]:
"""取消一组任务,并返回等待上限内仍未收敛的记录。"""
cancellable_tasks = [
record.task for record in records if not record.task.done()
]
@@ -899,9 +904,35 @@ class SubAgentTaskControlMiddleware(AgentMiddleware):
logger.info(f"开始取消子代理任务: tasks={len(cancellable_tasks)}")
for task in cancellable_tasks:
task.cancel()
if cancellable_tasks:
await asyncio.gather(*cancellable_tasks, return_exceptions=True)
if not cancellable_tasks:
return []
done, pending = await asyncio.wait(
cancellable_tasks,
timeout=SUBAGENT_CANCEL_GRACE_SECONDS,
)
if done:
await asyncio.gather(*done, return_exceptions=True)
if pending:
logger.warning(
f"子代理任务取消等待超时: pending={len(pending)}, "
f"timeout={SUBAGENT_CANCEL_GRACE_SECONDS}s"
)
else:
logger.info(f"子代理任务取消完成: tasks={len(cancellable_tasks)}")
return [record for record in records if record.task in pending]
async def close(self) -> None:
"""取消脱离当前 Agent 回合的子代理任务。"""
unfinished_records = [
record for record in self._tasks.values() if not record.task.done()
]
if unfinished_records:
logger.info(
f"关闭子代理任务控制器,取消未完成任务: tasks={len(unfinished_records)}"
)
await self._cancel_records(unfinished_records)
self._tasks.clear()
@staticmethod
def _pipeline_description(
@@ -1039,14 +1070,16 @@ class SubAgentTaskControlMiddleware(AgentMiddleware):
f"subagent_type={record.subagent_type}"
)
try:
result = await asyncio.wait_for(task, timeout=timeout)
except asyncio.TimeoutError:
done, pending = await asyncio.wait({task}, timeout=timeout)
if pending:
task.cancel()
error = f"{step_index} 个管道子代理任务等待超时。"
logger.info(
f"{error} task_id={record.task_id}, timeout_ms={normalized_timeout_ms}"
)
return records, error
try:
result = next(iter(done)).result()
except Exception as err:
error = (
f"{step_index} 个管道子代理任务执行失败: "
@@ -1125,6 +1158,7 @@ class SubAgentTaskControlMiddleware(AgentMiddleware):
active_only=action in {"wait", "cancel"} and not task_ids and not task_id,
)
cancellation_pending: list[_SubAgentRuntimeTask] = []
if action == "wait":
logger.info(
f"准备等待子代理任务: selected={len(records)}, missing={len(missing_ids)}"
@@ -1138,31 +1172,28 @@ class SubAgentTaskControlMiddleware(AgentMiddleware):
logger.info(
f"准备取消子代理任务: selected={len(records)}, missing={len(missing_ids)}"
)
await self._cancel_records(records)
cancellation_pending = await self._cancel_records(records)
elif action == "status":
logger.info(
f"查询子代理任务状态: selected={len(records)}, missing={len(missing_ids)}"
)
return self._json_response(
{
"success": True,
"action": action,
"wait_mode": wait_mode if action == "wait" else None,
"missing_task_ids": missing_ids,
"tasks": [self._task_output(record) for record in records],
}
)
response = {
"success": not cancellation_pending,
"action": action,
"wait_mode": wait_mode if action == "wait" else None,
"missing_task_ids": missing_ids,
"tasks": [self._task_output(record) for record in records],
}
if action == "cancel":
response["cancel_pending_task_ids"] = [
record.task_id for record in cancellation_pending
]
return self._json_response(response)
async def aafter_agent(self, state: Any, runtime: Any) -> None:
"""Agent 结束时取消未完成的子代理任务,避免后台泄漏。"""
unfinished_records = [
record for record in self._tasks.values() if not record.task.done()
]
if unfinished_records:
logger.info(f"Agent 结束,取消未完成子代理任务: tasks={len(unfinished_records)}")
await self._cancel_records(unfinished_records)
self._tasks.clear()
await self.close()
async def awrap_tool_call(
self,
+59 -6
View File
@@ -1,5 +1,6 @@
import asyncio
import hashlib
import inspect
import json
import re
import traceback
@@ -395,6 +396,7 @@ class MoviePilotAgent:
self._llm_provider_selection: Dict[str, Any] = {}
self._agent_started_at: Optional[datetime] = None
self._compiled_agent_bundle: Optional[_CompiledAgentBundle] = None
self._subagent_middlewares: tuple[Any, ...] = ()
self._last_agent_cache_hit = False
# 流式token管理
@@ -1693,7 +1695,23 @@ class MoviePilotAgent:
return bundle.agent
return None
def _cache_agent(
@staticmethod
async def _close_subagent_middleware_instances(
middlewares: tuple[Any, ...],
) -> None:
"""释放不再由 Agent 图持有的子代理控制器。"""
for middleware in middlewares:
close = getattr(middleware, "close", None)
if not callable(close):
continue
try:
result = close()
if inspect.isawaitable(result):
await result
except Exception as error:
logger.debug(f"关闭子代理中间件失败: {error}")
async def _cache_agent(
self,
*,
signature: tuple[Any, ...],
@@ -1702,8 +1720,18 @@ class MoviePilotAgent:
tool_catalog: ToolCatalogSnapshot,
subagent_catalog: ToolCatalogSnapshot,
mcp_config_signature: str,
subagent_middlewares: tuple[Any, ...] = (),
) -> Any:
"""保存当前会话可复用的 Agent 图。"""
previous_middlewares = tuple(
middleware
for middleware in self._subagent_middlewares
if not any(
middleware is replacement
for replacement in subagent_middlewares
)
)
await self._close_subagent_middleware_instances(previous_middlewares)
self._compiled_agent_bundle = _CompiledAgentBundle(
signature=signature,
agent=agent,
@@ -1715,8 +1743,16 @@ class MoviePilotAgent:
mcp_config_signature=mcp_config_signature,
catalog_checked_at=datetime.now(),
)
self._subagent_middlewares = subagent_middlewares
return agent
async def _invalidate_cached_agent(self) -> None:
"""使当前图失效,并释放只属于该图的子代理控制器。"""
subagent_middlewares = self._subagent_middlewares
self._subagent_middlewares = ()
self._compiled_agent_bundle = None
await self._close_subagent_middleware_instances(subagent_middlewares)
@staticmethod
def _latest_turn_messages(messages: List[BaseMessage]) -> List[BaseMessage]:
"""从完整历史中提取本轮新增用户消息。"""
@@ -1785,6 +1821,7 @@ class MoviePilotAgent:
创建 LangGraph Agent(使用 create_agent + SummarizationMiddleware
:param streaming: 是否启用流式输出
"""
temporary_subagent_middlewares: tuple[Any, ...] = ()
try:
runtime_config = await self._resolve_llm_runtime_config()
plugin_revision = _get_plugin_tools_revision()
@@ -1888,6 +1925,7 @@ class MoviePilotAgent:
policy_context=policy_context.for_subagent(),
catalog=subagent_catalog,
)
temporary_subagent_middlewares = tuple(subagent_middlewares)
# 严格目录必须覆盖 LangGraph ToolNode 可执行的全部 client-side 工具。
tool_catalog = ToolCatalogSnapshot.from_tools(
[
@@ -1910,6 +1948,10 @@ class MoviePilotAgent:
if cached_agent:
# 签名相同表示已编译图中的精确工具实例仍有效;新建快照仅用于复核。
cached_bundle.catalog_checked_at = datetime.now()
await self._close_subagent_middleware_instances(
temporary_subagent_middlewares
)
temporary_subagent_middlewares = ()
logger.debug(f"复用会话内 Agent 图: session_id={self.session_id}")
return cached_agent
max_tools = settings.LLM_MAX_TOOLS
@@ -2014,17 +2056,28 @@ class MoviePilotAgent:
middleware=middlewares,
checkpointer=InMemorySaver(),
)
return self._cache_agent(
cached_agent = await self._cache_agent(
signature=bundle_signature,
agent=agent,
streaming=streaming,
tool_catalog=tool_catalog,
subagent_catalog=subagent_catalog,
mcp_config_signature=mcp_config_signature,
subagent_middlewares=tuple(subagent_middlewares),
)
temporary_subagent_middlewares = ()
return cached_agent
except asyncio.CancelledError:
await self._close_subagent_middleware_instances(
temporary_subagent_middlewares
)
raise
except Exception as e:
await self._close_subagent_middleware_instances(
temporary_subagent_middlewares
)
logger.error(f"创建 Agent 失败: {e}")
raise e
raise
async def process(
self,
@@ -2365,11 +2418,11 @@ class MoviePilotAgent:
except asyncio.CancelledError:
logger.info(f"Agent执行被取消: session_id={self.session_id}")
self._compiled_agent_bundle = None
await self._invalidate_cached_agent()
execution_error = "任务已取消"
raise
except Exception as e:
self._compiled_agent_bundle = None
await self._invalidate_cached_agent()
execution_error = str(e)
if self._messages_have_image_input(messages) and self._is_unsupported_image_input_error(e):
logger.warning(
@@ -2422,9 +2475,9 @@ class MoviePilotAgent:
"""
清理智能体资源
"""
await self._invalidate_cached_agent()
self._pending_secret_confirmation = None
self.protected_output_callback = None
self._compiled_agent_bundle = None
logger.info(f"MoviePilot智能体已清理: session_id={self.session_id}")
+2
View File
@@ -184,6 +184,8 @@ class ExecutionReceipt:
result_summary: Optional[str] = None
error_summary: Optional[str] = None
duration_ms: int = 0
external_may_continue: bool = False # 中断后外部操作仍可能继续,不能视为已停止。
needs_reconcile: bool = False # 调用方需查询外部实际状态后再决定补偿或重试。
@dataclass(frozen=True)
+30 -1
View File
@@ -1,5 +1,6 @@
"""Agent 工具策略观测、脱敏回执与共享执行边界。"""
import asyncio
import time
import uuid
from collections.abc import Callable
@@ -9,12 +10,14 @@ from langchain_core.messages import ToolMessage
from pydantic import ValidationError
from app.agent.policy.contracts import (
ActionEffect,
ConfirmationMode,
ExecutionOutcome,
ExecutionReceipt,
MigrationState,
PolicyDecision,
PolicyObservation,
RecoveryMode,
ToolInvocation,
ToolPolicyContext,
)
@@ -165,10 +168,32 @@ class AgentToolPolicyOrchestrator:
)
return receipt
@staticmethod
def _uncertain_external_state(
observation: PolicyObservation,
error: BaseException,
) -> tuple[bool, bool]:
"""标记取消或超时后无法确认的写操作终态。"""
interrupted = isinstance(error, (asyncio.CancelledError, TimeoutError))
read_only = observation.policy.effect in {
ActionEffect.SAFE_READ,
ActionEffect.SENSITIVE_READ,
}
if not interrupted or read_only:
return False, False
needs_reconcile = observation.policy.recovery not in {
RecoveryMode.TRANSACTION,
RecoveryMode.IDEMPOTENT,
}
return True, needs_reconcile
@staticmethod
def fail(observation: PolicyObservation, error: BaseException) -> ExecutionReceipt:
"""生成失败回执 envelope,不把异常中的凭据写入日志。"""
error_summary = summarize_error(error)
external_may_continue, needs_reconcile = (
AgentToolPolicyOrchestrator._uncertain_external_state(observation, error)
)
receipt = ExecutionReceipt(
invocation_id=observation.invocation.invocation_id,
tool_name=observation.invocation.tool_name,
@@ -181,11 +206,15 @@ class AgentToolPolicyOrchestrator:
0,
int((time.monotonic() - observation.started_at) * 1000),
),
external_may_continue=external_may_continue,
needs_reconcile=needs_reconcile,
)
logger.error(
f"Agent工具执行失败: tool={receipt.tool_name}, "
f"origin={receipt.origin.value}, shadow={receipt.decision.shadow}, "
f"duration_ms={receipt.duration_ms}, error={error_summary}"
f"duration_ms={receipt.duration_ms}, error={error_summary}, "
f"external_may_continue={receipt.external_may_continue}, "
f"needs_reconcile={receipt.needs_reconcile}"
)
return receipt
+14 -1
View File
@@ -2,6 +2,7 @@
from __future__ import annotations
import sys
import threading
from typing import Any
@@ -132,6 +133,18 @@ def is_tool_factory_materialized() -> bool:
)
async def close_materialized_terminal_sessions() -> None:
"""关闭已物化的终端会话管理器,不触发新的 Agent 工具导入。"""
module = sys.modules.get("app.agent.tools.impl._terminal_session")
manager = getattr(module, "terminal_session_manager", None) if module else None
close = getattr(manager, "close", None)
if callable(close):
await close()
async def begin_agent_shutdown() -> None:
"""不可逆关闭首用闸门,并等待全部同步及异步能力释放。"""
await _ensure_runtime().shutdown_async(reason="application_shutdown")
try:
await _ensure_runtime().shutdown_async(reason="application_shutdown")
finally:
await close_materialized_terminal_sessions()
+2 -1
View File
@@ -377,7 +377,7 @@ class MoviePilotTool(BaseTool, metaclass=ABCMeta):
except ToolExecutionTimeoutError as e:
error_message = summarize_error(e)
logger.warning(error_message)
result = error_message
raise
except Exception as e:
error_message = f"工具执行异常: {summarize_error(e)}"
logger.error(f"Tool {self.name} execution failed: {summarize_error(e)}")
@@ -415,6 +415,7 @@ class MoviePilotTool(BaseTool, metaclass=ABCMeta):
except asyncio.TimeoutError as err:
raise ToolExecutionTimeoutError(
f"工具 {self.name} 执行超时(超过 {timeout:g} 秒),已停止等待结果。"
"若工具包含外部写操作,操作可能仍在继续,请先确认实际状态再重试。"
) from err
@staticmethod
+104 -8
View File
@@ -128,6 +128,11 @@ class _TerminalSessionManager:
"""初始化会话表和并发保护锁。"""
self._sessions: dict[str, _TerminalSession] = {}
self._lock = asyncio.Lock()
self._close_lock = asyncio.Lock()
self._closed = False
self._starting = 0
self._starts_idle = asyncio.Event()
self._starts_idle.set()
@staticmethod
def _normalize_bool(value: Any, default: bool = True) -> bool:
@@ -211,20 +216,55 @@ class _TerminalSessionManager:
should_use_pty = self._normalize_bool(use_pty, default=True) and os.name == "posix"
async with self._lock:
if self._closed:
raise RuntimeError("终端会话管理器已关闭")
self._cleanup_finished_sessions_locked()
if self._active_session_count_locked() >= TERMINAL_CONCURRENCY_LIMIT:
if (
self._active_session_count_locked() + self._starting
>= TERMINAL_CONCURRENCY_LIMIT
):
raise RuntimeError(
f"后台终端会话数已达到上限 {TERMINAL_CONCURRENCY_LIMIT}"
)
self._starting += 1
self._starts_idle.clear()
session = (
await self._start_pty_session(command, normalized_cwd, normalized_env)
if should_use_pty
else await self._start_pipe_session(command, normalized_cwd, normalized_env)
)
session: Optional[_TerminalSession] = None
reject_session = False
session_registered = False
session_released = False
try:
session = (
await self._start_pty_session(command, normalized_cwd, normalized_env)
if should_use_pty
else await self._start_pipe_session(
command, normalized_cwd, normalized_env
)
)
async with self._lock:
self._sessions[session.session_id] = session
async with self._lock:
reject_session = self._closed
if not reject_session:
self._sessions[session.session_id] = session
session_registered = True
if reject_session:
await self._terminate_session(session)
session_released = True
raise RuntimeError("终端会话管理器已关闭")
except BaseException:
if session is not None and not session_registered and not session_released:
cleanup_task = asyncio.create_task(self._terminate_session(session))
try:
await asyncio.shield(cleanup_task)
except asyncio.CancelledError:
await cleanup_task
raise
finally:
async with self._lock:
self._starting -= 1
if self._starting == 0:
self._starts_idle.set()
logger.info(
"启动后台终端会话: session_id=%s, pid=%s, use_pty=%s, command=%s",
@@ -473,6 +513,62 @@ class _TerminalSessionManager:
return self._session_payload(session, output="", output_truncated=False)
async def close(self) -> None:
"""停止所有后台终端会话并释放 PTY、读取任务和会话记录。"""
async with self._close_lock:
async with self._lock:
self._closed = True
await self._starts_idle.wait()
async with self._lock:
sessions = list(self._sessions.values())
await asyncio.gather(
*(self._terminate_session(session) for session in sessions),
return_exceptions=True,
)
async with self._lock:
for session in sessions:
session.close_pty()
self._sessions.clear()
async def _terminate_session(self, session: _TerminalSession) -> None:
"""以有限等待停止进程,并在必要时升级为 SIGKILL。"""
if session.status == "running":
session.kill_requested = True
self._send_signal(session, signal.SIGTERM)
wait_task = session.wait_task
if wait_task and not wait_task.done():
try:
await asyncio.wait_for(
asyncio.shield(wait_task),
timeout=TERMINAL_KILL_GRACE_SECONDS,
)
except asyncio.TimeoutError:
force_signal = getattr(signal, "SIGKILL", signal.SIGTERM)
self._send_signal(session, force_signal)
try:
await asyncio.wait_for(
asyncio.shield(wait_task),
timeout=TERMINAL_KILL_GRACE_SECONDS,
)
except asyncio.TimeoutError:
logger.error(
"终端会话关闭超时: session_id=%s, pid=%s",
session.session_id,
session.pid,
)
for task in session.reader_tasks:
if not task.done():
task.cancel()
if session.reader_tasks:
await asyncio.gather(*session.reader_tasks, return_exceptions=True)
session.close_pty()
def get_session(self, session_id: str) -> _TerminalSession:
"""按 ID 获取会话,不存在时抛出清晰错误。"""
session = self._sessions.get(session_id)
+11 -5
View File
@@ -1,5 +1,6 @@
from __future__ import annotations
import asyncio
import json
import threading
import uuid
@@ -8,7 +9,8 @@ from typing import TYPE_CHECKING, Any, Dict, List, Optional
from app.runtime.log import logger
if TYPE_CHECKING:
from app.agent.policy import AgentToolPolicyOrchestrator, ToolPolicyContext
from app.agent.policy.contracts import ToolPolicyContext
from app.agent.policy.orchestrator import AgentToolPolicyOrchestrator
from app.agent.tools.catalog import ToolCatalogSnapshot
@@ -57,7 +59,7 @@ class MoviePilotToolsManager:
@staticmethod
def _summarize_error(error: Exception) -> str:
"""仅在错误路径加载策略脱敏器,保持默认导入轻量。"""
from app.agent.policy import summarize_error
from app.agent.policy.sanitizer import summarize_error
return summarize_error(error)
@@ -143,13 +145,13 @@ class MoviePilotToolsManager:
if policy_orchestrator is not None and policy_context is not None:
return policy_orchestrator, policy_context
from app.agent.policy import (
DEFAULT_TOOL_POLICY_ORCHESTRATOR,
from app.agent.policy.contracts import (
AuthSource,
PrincipalType,
ToolOrigin,
ToolPolicyContext,
)
from app.agent.policy.orchestrator import DEFAULT_TOOL_POLICY_ORCHESTRATOR
if policy_orchestrator is None:
policy_orchestrator = DEFAULT_TOOL_POLICY_ORCHESTRATOR
@@ -381,7 +383,7 @@ class MoviePilotToolsManager:
)
return error_msg
from app.agent.policy import call_policy_hook
from app.agent.policy.orchestrator import call_policy_hook
from app.agent.tools.base import (
ToolExecutionTimeoutError,
format_tool_result_for_agent,
@@ -414,6 +416,10 @@ class MoviePilotToolsManager:
tool_name=tool_name,
max_chars=getattr(tool_instance, "result_max_chars", None),
)
except asyncio.CancelledError as e:
if observation is not None and policy_orchestrator is not None:
call_policy_hook("cancel", policy_orchestrator.fail, observation, e)
raise
except ToolExecutionTimeoutError as e:
if observation is not None and policy_orchestrator is not None:
call_policy_hook("fail", policy_orchestrator.fail, observation, e)
+6 -3
View File
@@ -3,6 +3,7 @@ from typing import Any
from app.agent.runtime_loader import (
activate_agent_service,
begin_agent_shutdown,
close_materialized_terminal_sessions,
get_agent_manager as get_runtime_agent_manager,
get_running_agent_manager as get_runtime_running_agent_manager,
is_tool_factory_materialized,
@@ -70,14 +71,14 @@ def _get_prompt_manager() -> Any:
def _get_capability_manager() -> Any:
"""首个多模态调用才导入 Agent 能力管理器。"""
from app.agent.llm import AgentCapabilityManager
from app.agent.llm.capability import AgentCapabilityManager
return AgentCapabilityManager
def _get_llm_helper() -> Any:
"""首个模型能力查询才导入 LLM helper。"""
from app.agent.llm import LLMHelper
from app.agent.llm.helper import LLMHelper
return LLMHelper
@@ -216,6 +217,8 @@ async def stop_agent():
if is_tool_factory_materialized():
from app.agent.tools.base import shutdown_blocking_executors
shutdown_blocking_executors(cancel_futures=True)
shutdown_blocking_executors(wait=False, cancel_futures=True)
except Exception as e:
logger.error(f"停止AI智能体时发生错误: {e}")
finally:
await close_materialized_terminal_sessions()