2026-06-01 22:28:45 +08:00
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
2026-06-02 18:47:28 +08:00
|
|
|
|
import asyncio
|
|
|
|
|
|
import hashlib
|
|
|
|
|
|
import uuid
|
2026-06-01 22:28:45 +08:00
|
|
|
|
from collections.abc import Awaitable, Callable
|
|
|
|
|
|
from typing import Annotated, Any
|
|
|
|
|
|
|
|
|
|
|
|
from deepagents import SubagentTransformer
|
|
|
|
|
|
from deepagents.middleware._utils import append_to_system_message
|
|
|
|
|
|
from langchain.agents.middleware.types import AgentMiddleware, ContextT, ModelRequest, ModelResponse, ResponseT
|
|
|
|
|
|
from langchain_core.messages import AIMessage, HumanMessage, ToolMessage
|
|
|
|
|
|
from langchain_core.tools import StructuredTool
|
|
|
|
|
|
from langgraph.prebuilt.tool_node import ToolRuntime
|
|
|
|
|
|
from langgraph.types import Command
|
|
|
|
|
|
|
2026-06-02 18:47:28 +08:00
|
|
|
|
from yuxi.agents.context import build_agent_input_context
|
2026-06-01 22:28:45 +08:00
|
|
|
|
from yuxi.repositories.agent_repository import SUB_AGENT_BACKEND_ID, AgentRepository
|
2026-06-02 18:47:28 +08:00
|
|
|
|
from yuxi.repositories.agent_run_repository import AgentRunRepository
|
2026-06-01 22:28:45 +08:00
|
|
|
|
from yuxi.repositories.user_repository import UserRepository
|
|
|
|
|
|
from yuxi.storage.postgres.manager import pg_manager
|
|
|
|
|
|
from yuxi.storage.postgres.models_business import Agent
|
|
|
|
|
|
from yuxi.utils.datetime_utils import utc_isoformat
|
2026-06-03 18:55:34 +08:00
|
|
|
|
from yuxi.utils.subagent_thread_utils import make_child_thread_id
|
2026-06-01 22:28:45 +08:00
|
|
|
|
|
2026-06-02 18:47:28 +08:00
|
|
|
|
_CHILD_STATE_INHERIT_KEYS: frozenset[str] = frozenset()
|
|
|
|
|
|
_TERMINAL_RUN_STATUSES = {"completed", "failed", "cancelled", "interrupted"}
|
2026-06-01 22:28:45 +08:00
|
|
|
|
|
|
|
|
|
|
TASK_SYSTEM_PROMPT = """## `task`(子智能体任务工具)
|
|
|
|
|
|
|
|
|
|
|
|
你可以使用 `task` 工具把复杂、独立的子任务交给已配置的子智能体处理。子智能体只返回最终结果,你看不到它的中间步骤。
|
2026-06-02 18:47:28 +08:00
|
|
|
|
工具结果会包含子智能体线程 ID,后续需要继续同一个子任务时,把该 ID 作为 `thread_id` 传回 `task`。
|
2026-06-01 22:28:45 +08:00
|
|
|
|
|
|
|
|
|
|
使用原则:
|
|
|
|
|
|
- 任务足够复杂、可以独立完成、或需要隔离上下文时使用。
|
|
|
|
|
|
- 多个互不依赖的子任务可以并行调用多个 `task`。
|
2026-06-02 18:47:28 +08:00
|
|
|
|
- 继续既有子智能体任务时传入之前结果中的 `thread_id`;新任务不要填写 `thread_id`。
|
|
|
|
|
|
- 不要并行调用同一个 `thread_id`,避免多个续跑请求同时写入同一子线程。
|
2026-06-01 22:28:45 +08:00
|
|
|
|
- 简单问题或少量直接工具调用不要委派。
|
|
|
|
|
|
- 调用时必须选择下方可用的 `subagent_type`,并在 `description` 中写清目标、上下文和期望输出。
|
|
|
|
|
|
- 不要通过 shell、curl、HTTP API 或命令行间接调用子智能体;需要子智能体时必须使用 `task` 工具。
|
|
|
|
|
|
|
|
|
|
|
|
Available subagent types:
|
|
|
|
|
|
|
|
|
|
|
|
{available_agents}"""
|
|
|
|
|
|
|
|
|
|
|
|
TASK_TOOL_DESCRIPTION = """Launch a configured Yuxi subagent to handle an isolated task.
|
|
|
|
|
|
|
|
|
|
|
|
Available subagent types:
|
|
|
|
|
|
{available_agents}
|
|
|
|
|
|
|
|
|
|
|
|
Use `subagent_type` to select one available subagent and put the full task brief in `description`.
|
2026-06-02 18:47:28 +08:00
|
|
|
|
Omit `thread_id` for a new task. To continue a previous subagent task, pass the child thread ID returned by
|
|
|
|
|
|
that prior task result as `thread_id`.
|
2026-06-01 22:28:45 +08:00
|
|
|
|
Do not call subagents through shell, curl, HTTP APIs, or command-line indirection."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
TASK_DESCRIPTION_ARG = "需要子智能体独立完成的任务描述,包含必要上下文和期望输出。"
|
|
|
|
|
|
SUBAGENT_TYPE_ARG = "要调用的子智能体标识,必须是工具描述中列出的可用类型之一。"
|
2026-06-02 18:47:28 +08:00
|
|
|
|
THREAD_ID_ARG = "可选。要继续的既有子智能体线程 ID,必须来自之前 task 工具结果;新任务不要填写。"
|
2026-06-01 22:28:45 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _get_agent_backend(backend_id: str):
|
|
|
|
|
|
from yuxi.agents.buildin import agent_manager
|
|
|
|
|
|
|
|
|
|
|
|
return agent_manager.get_agent(backend_id)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _final_assistant_text(messages: list[Any]) -> str:
|
|
|
|
|
|
for message in reversed(messages):
|
|
|
|
|
|
if isinstance(message, AIMessage):
|
|
|
|
|
|
text = message.text.rstrip() if message.text else ""
|
|
|
|
|
|
if text:
|
|
|
|
|
|
return text
|
|
|
|
|
|
return "子智能体已完成任务,但没有返回文本结果。"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _result_artifacts(result: dict[str, Any]) -> list[str]:
|
|
|
|
|
|
artifacts = result.get("artifacts")
|
|
|
|
|
|
return list(artifacts) if isinstance(artifacts, list) else []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _preview_text(text: str, limit: int = 500) -> str:
|
|
|
|
|
|
return text if len(text) <= limit else f"{text[:limit]}..."
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-06-02 18:47:28 +08:00
|
|
|
|
def _tool_result_with_thread_id(child_thread_id: str, content: str) -> str:
|
2026-06-03 00:08:39 +08:00
|
|
|
|
return f"> 子智能体线程 ID: {child_thread_id}\n\n---\n\n{content}"
|
2026-06-02 18:47:28 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _new_child_thread_id(
|
|
|
|
|
|
requested_thread_id: str | None,
|
|
|
|
|
|
*,
|
|
|
|
|
|
parent_thread_id: str,
|
|
|
|
|
|
agent_slug: str,
|
|
|
|
|
|
tool_call_id: str,
|
|
|
|
|
|
) -> tuple[str, bool]:
|
|
|
|
|
|
requested_thread_id = str(requested_thread_id or "").strip()
|
|
|
|
|
|
if requested_thread_id:
|
|
|
|
|
|
return requested_thread_id, True
|
|
|
|
|
|
return make_child_thread_id(parent_thread_id, agent_slug, tool_call_id), False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _subagent_request_id(parent_run_id: str, child_thread_id: str, tool_call_id: str, agent_slug: str) -> str:
|
|
|
|
|
|
digest = hashlib.sha256(f"{parent_run_id}:{child_thread_id}:{tool_call_id}:{agent_slug}".encode()).hexdigest()
|
|
|
|
|
|
return f"subagent:{digest[:48]}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _with_run_payload(subagent_run: dict[str, Any], run) -> dict[str, Any]:
|
|
|
|
|
|
if not run:
|
|
|
|
|
|
return subagent_run
|
|
|
|
|
|
return {**subagent_run, **_agent_run_state_payload(run)}
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-06-01 22:28:45 +08:00
|
|
|
|
def _completed_tool_response(result: dict[str, Any], tool_call_id: str, subagent_run: dict[str, Any]) -> Command:
|
|
|
|
|
|
final_text = _final_assistant_text(result.get("messages") or [])
|
|
|
|
|
|
artifacts = _result_artifacts(result)
|
|
|
|
|
|
subagent_run = {
|
|
|
|
|
|
**subagent_run,
|
|
|
|
|
|
"status": "completed",
|
|
|
|
|
|
"completed_at": utc_isoformat(),
|
|
|
|
|
|
"result_preview": _preview_text(final_text),
|
|
|
|
|
|
"error": None,
|
|
|
|
|
|
"artifacts": artifacts,
|
|
|
|
|
|
}
|
2026-06-02 18:47:28 +08:00
|
|
|
|
tool_result = _tool_result_with_thread_id(subagent_run["child_thread_id"], final_text)
|
|
|
|
|
|
update: dict[str, Any] = {"messages": [ToolMessage(tool_result, tool_call_id=tool_call_id)]}
|
2026-06-01 22:28:45 +08:00
|
|
|
|
if artifacts:
|
|
|
|
|
|
update["artifacts"] = artifacts
|
|
|
|
|
|
update["subagent_runs"] = [subagent_run]
|
|
|
|
|
|
return Command(update=update)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-06-02 18:47:28 +08:00
|
|
|
|
def _reused_run_response(run, tool_call_id: str, subagent_run: dict[str, Any]) -> Command:
|
|
|
|
|
|
status = str(getattr(run, "status", "") or "unknown")
|
|
|
|
|
|
if status == "completed":
|
|
|
|
|
|
message = "子智能体任务已完成,未重复执行。"
|
|
|
|
|
|
elif status in _TERMINAL_RUN_STATUSES:
|
|
|
|
|
|
error_message = str(getattr(run, "error_message", "") or "")
|
|
|
|
|
|
message = f"子智能体任务已结束,状态:{status}。{error_message}".strip()
|
|
|
|
|
|
else:
|
|
|
|
|
|
message = f"子智能体任务已存在,当前状态:{status},未重复提交。"
|
|
|
|
|
|
|
|
|
|
|
|
subagent_run = {
|
|
|
|
|
|
**subagent_run,
|
|
|
|
|
|
**_agent_run_state_payload(run),
|
|
|
|
|
|
"status": status,
|
|
|
|
|
|
"result_preview": _preview_text(message),
|
|
|
|
|
|
}
|
|
|
|
|
|
tool_message = ToolMessage(
|
|
|
|
|
|
_tool_result_with_thread_id(subagent_run["child_thread_id"], message),
|
|
|
|
|
|
tool_call_id=tool_call_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
return Command(update={"messages": [tool_message], "subagent_runs": [subagent_run]})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _agent_run_state_payload(run) -> dict[str, Any]:
|
|
|
|
|
|
payload = {
|
|
|
|
|
|
"run_id": run.id,
|
|
|
|
|
|
"status": run.status,
|
|
|
|
|
|
"parent_agent_run_id": run.parent_agent_run_id,
|
|
|
|
|
|
"created_at": utc_isoformat(run.created_at) if run.created_at else None,
|
|
|
|
|
|
"completed_at": utc_isoformat(run.finished_at) if run.finished_at else None,
|
|
|
|
|
|
"error": run.error_message,
|
|
|
|
|
|
}
|
|
|
|
|
|
return {key: value for key, value in payload.items() if value is not None}
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-06-01 22:28:45 +08:00
|
|
|
|
def _failed_tool_response(error: Exception, tool_call_id: str, subagent_run: dict[str, Any]) -> Command:
|
|
|
|
|
|
error_text = str(error)
|
|
|
|
|
|
message = f"子智能体 {subagent_run['subagent_type']} 调用失败:{error_text}"
|
2026-06-02 18:47:28 +08:00
|
|
|
|
tool_result = _tool_result_with_thread_id(subagent_run["child_thread_id"], message)
|
2026-06-01 22:28:45 +08:00
|
|
|
|
update = {
|
2026-06-02 18:47:28 +08:00
|
|
|
|
"messages": [ToolMessage(tool_result, tool_call_id=tool_call_id)],
|
2026-06-01 22:28:45 +08:00
|
|
|
|
"subagent_runs": [
|
|
|
|
|
|
{
|
|
|
|
|
|
**subagent_run,
|
|
|
|
|
|
"status": "failed",
|
|
|
|
|
|
"completed_at": utc_isoformat(),
|
|
|
|
|
|
"result_preview": _preview_text(message),
|
|
|
|
|
|
"error": error_text,
|
|
|
|
|
|
"artifacts": [],
|
|
|
|
|
|
}
|
|
|
|
|
|
],
|
|
|
|
|
|
}
|
|
|
|
|
|
return Command(update=update)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _state_for_child(
|
|
|
|
|
|
description: str,
|
|
|
|
|
|
runtime: ToolRuntime,
|
|
|
|
|
|
*,
|
|
|
|
|
|
parent_thread_id: str,
|
|
|
|
|
|
file_thread_id: str,
|
|
|
|
|
|
skills_thread_id: str,
|
2026-06-02 18:47:28 +08:00
|
|
|
|
continuing: bool = False,
|
2026-06-01 22:28:45 +08:00
|
|
|
|
) -> dict[str, Any]:
|
2026-06-03 18:55:34 +08:00
|
|
|
|
state = {} if continuing else {key: runtime.state[key] for key in _CHILD_STATE_INHERIT_KEYS if key in runtime.state}
|
2026-06-01 22:28:45 +08:00
|
|
|
|
state.update(
|
|
|
|
|
|
{
|
|
|
|
|
|
"parent_thread_id": parent_thread_id,
|
|
|
|
|
|
"file_thread_id": file_thread_id,
|
|
|
|
|
|
"skills_thread_id": skills_thread_id,
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
state["messages"] = [HumanMessage(content=description)]
|
|
|
|
|
|
return state
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _child_config(
|
|
|
|
|
|
runtime: ToolRuntime,
|
|
|
|
|
|
*,
|
|
|
|
|
|
child_thread_id: str,
|
|
|
|
|
|
uid: str,
|
|
|
|
|
|
parent_thread_id: str,
|
|
|
|
|
|
file_thread_id: str,
|
|
|
|
|
|
skills_thread_id: str,
|
2026-06-02 18:47:28 +08:00
|
|
|
|
subagent_type: str,
|
|
|
|
|
|
run_id: str | None = None,
|
|
|
|
|
|
request_id: str | None = None,
|
2026-06-01 22:28:45 +08:00
|
|
|
|
) -> dict:
|
|
|
|
|
|
parent_config = runtime.config or {}
|
|
|
|
|
|
config: dict[str, Any] = {}
|
|
|
|
|
|
if "callbacks" in parent_config:
|
|
|
|
|
|
config["callbacks"] = parent_config["callbacks"]
|
|
|
|
|
|
if "tags" in parent_config:
|
|
|
|
|
|
config["tags"] = parent_config["tags"]
|
|
|
|
|
|
parent_configurable = (
|
|
|
|
|
|
parent_config.get("configurable") if isinstance(parent_config.get("configurable"), dict) else {}
|
|
|
|
|
|
)
|
|
|
|
|
|
parent_configurable = {
|
|
|
|
|
|
key: value
|
|
|
|
|
|
for key, value in parent_configurable.items()
|
|
|
|
|
|
if not str(key).startswith(("checkpoint_", "__pregel_"))
|
|
|
|
|
|
}
|
|
|
|
|
|
config["configurable"] = {
|
|
|
|
|
|
**parent_configurable,
|
|
|
|
|
|
"thread_id": child_thread_id,
|
|
|
|
|
|
"uid": uid,
|
|
|
|
|
|
"parent_thread_id": parent_thread_id,
|
|
|
|
|
|
"file_thread_id": file_thread_id,
|
|
|
|
|
|
"skills_thread_id": skills_thread_id,
|
2026-06-02 18:47:28 +08:00
|
|
|
|
"subagent_type": subagent_type,
|
|
|
|
|
|
"subagent_thread_id": child_thread_id,
|
|
|
|
|
|
"subagent_tool_call_id": runtime.tool_call_id,
|
|
|
|
|
|
"run_id": run_id,
|
|
|
|
|
|
"request_id": request_id,
|
2026-06-01 22:28:45 +08:00
|
|
|
|
"ls_agent_type": "subagent",
|
|
|
|
|
|
}
|
|
|
|
|
|
config["recursion_limit"] = parent_config.get("recursion_limit", 300)
|
|
|
|
|
|
return config
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class YuxiSubAgentMiddleware(AgentMiddleware[Any, ContextT, ResponseT]):
|
|
|
|
|
|
def __init__(self, *, parent_context, subagents: list[Agent]) -> None:
|
|
|
|
|
|
super().__init__()
|
|
|
|
|
|
self.parent_context = parent_context
|
|
|
|
|
|
self.subagents = {agent.slug: agent for agent in subagents}
|
|
|
|
|
|
available_agents = "\n".join(f"- {agent.slug}: {agent.description or agent.name}" for agent in subagents)
|
|
|
|
|
|
self.system_prompt = TASK_SYSTEM_PROMPT.format(available_agents=available_agents)
|
|
|
|
|
|
self.tools = [self._build_task_tool(available_agents)]
|
|
|
|
|
|
self.subagent_names = frozenset(self.subagents)
|
2026-06-02 18:47:28 +08:00
|
|
|
|
self.transformers = [lambda scope: SubagentTransformer(scope, subagent_names=self.subagent_names)]
|
|
|
|
|
|
|
|
|
|
|
|
async def _create_subagent_run(
|
|
|
|
|
|
self,
|
|
|
|
|
|
*,
|
|
|
|
|
|
child_thread_id: str,
|
|
|
|
|
|
description: str,
|
|
|
|
|
|
subagent_type: str,
|
|
|
|
|
|
agent: Agent,
|
|
|
|
|
|
uid: str,
|
|
|
|
|
|
parent_thread_id: str,
|
|
|
|
|
|
tool_call_id: str,
|
|
|
|
|
|
continuing: bool,
|
|
|
|
|
|
):
|
|
|
|
|
|
parent_run_id = str(getattr(self.parent_context, "run_id", "") or "").strip()
|
|
|
|
|
|
if not parent_run_id:
|
|
|
|
|
|
raise ValueError("当前运行时缺少父运行 ID,无法记录子智能体运行")
|
|
|
|
|
|
|
|
|
|
|
|
async with pg_manager.get_async_session_context() as db:
|
|
|
|
|
|
repo = AgentRunRepository(db)
|
|
|
|
|
|
parent_run = await repo.get_run_for_user(parent_run_id, uid)
|
|
|
|
|
|
if not parent_run:
|
|
|
|
|
|
raise ValueError("父运行任务不存在")
|
|
|
|
|
|
|
|
|
|
|
|
if continuing:
|
|
|
|
|
|
previous = await repo.get_latest_subagent_run_by_thread_for_user(child_thread_id, uid)
|
|
|
|
|
|
if not previous or previous.conversation_id != parent_run.conversation_id:
|
|
|
|
|
|
raise ValueError(
|
|
|
|
|
|
f"无法继续子智能体线程 {child_thread_id}:当前对话中没有找到对应的子智能体运行记录"
|
|
|
|
|
|
)
|
|
|
|
|
|
if previous.agent_id != subagent_type:
|
|
|
|
|
|
raise ValueError(
|
|
|
|
|
|
f"无法继续子智能体线程 {child_thread_id}:该线程属于子智能体 {previous.agent_id or '未知'}"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
request_id = _subagent_request_id(parent_run_id, child_thread_id, tool_call_id, agent.slug)
|
|
|
|
|
|
existing = await repo.get_run_by_request_id(request_id)
|
|
|
|
|
|
if existing:
|
|
|
|
|
|
return existing, False
|
|
|
|
|
|
|
|
|
|
|
|
run = await repo.create_run(
|
|
|
|
|
|
run_id=str(uuid.uuid4()),
|
|
|
|
|
|
thread_id=child_thread_id,
|
|
|
|
|
|
agent_id=subagent_type,
|
|
|
|
|
|
uid=uid,
|
|
|
|
|
|
request_id=request_id,
|
|
|
|
|
|
conversation_id=parent_run.conversation_id,
|
|
|
|
|
|
parent_agent_run_id=parent_run.id,
|
|
|
|
|
|
run_type="subagent",
|
|
|
|
|
|
checkpoint_thread_id=child_thread_id,
|
|
|
|
|
|
input_payload={
|
|
|
|
|
|
"description": description,
|
|
|
|
|
|
"tool_call_id": tool_call_id,
|
|
|
|
|
|
"subagent_type": subagent_type,
|
|
|
|
|
|
"subagent_name": agent.name,
|
|
|
|
|
|
"parent_thread_id": parent_thread_id,
|
|
|
|
|
|
"child_thread_id": child_thread_id,
|
|
|
|
|
|
"continuing": continuing,
|
|
|
|
|
|
},
|
|
|
|
|
|
)
|
|
|
|
|
|
return await repo.mark_running(run.id), True
|
|
|
|
|
|
|
|
|
|
|
|
async def _set_subagent_run_status(
|
|
|
|
|
|
self,
|
|
|
|
|
|
run_id: str,
|
|
|
|
|
|
status: str,
|
|
|
|
|
|
*,
|
|
|
|
|
|
error_type: str | None = None,
|
|
|
|
|
|
error_message: str | None = None,
|
|
|
|
|
|
):
|
|
|
|
|
|
async with pg_manager.get_async_session_context() as db:
|
|
|
|
|
|
return await AgentRunRepository(db).set_terminal_status(
|
|
|
|
|
|
run_id,
|
|
|
|
|
|
status=status,
|
|
|
|
|
|
error_type=error_type,
|
|
|
|
|
|
error_message=error_message,
|
|
|
|
|
|
)
|
2026-06-01 22:28:45 +08:00
|
|
|
|
|
|
|
|
|
|
def _build_task_tool(self, available_agents: str) -> StructuredTool:
|
|
|
|
|
|
def task(
|
|
|
|
|
|
description: Annotated[str, TASK_DESCRIPTION_ARG],
|
|
|
|
|
|
subagent_type: Annotated[str, SUBAGENT_TYPE_ARG],
|
|
|
|
|
|
runtime: ToolRuntime,
|
2026-06-02 18:47:28 +08:00
|
|
|
|
thread_id: Annotated[str | None, THREAD_ID_ARG] = None,
|
2026-06-01 22:28:45 +08:00
|
|
|
|
) -> str:
|
|
|
|
|
|
return "task 工具仅支持异步调用"
|
|
|
|
|
|
|
|
|
|
|
|
async def atask(
|
|
|
|
|
|
description: Annotated[str, TASK_DESCRIPTION_ARG],
|
|
|
|
|
|
subagent_type: Annotated[str, SUBAGENT_TYPE_ARG],
|
|
|
|
|
|
runtime: ToolRuntime,
|
2026-06-02 18:47:28 +08:00
|
|
|
|
thread_id: Annotated[str | None, THREAD_ID_ARG] = None,
|
2026-06-01 22:28:45 +08:00
|
|
|
|
) -> str | Command:
|
|
|
|
|
|
if subagent_type not in self.subagents:
|
|
|
|
|
|
allowed = ", ".join(f"`{slug}`" for slug in self.subagents)
|
|
|
|
|
|
return f"无法调用子智能体 {subagent_type},可用子智能体只有:{allowed}"
|
|
|
|
|
|
if not runtime.tool_call_id:
|
|
|
|
|
|
raise ValueError("Tool call ID is required for subagent invocation")
|
|
|
|
|
|
|
|
|
|
|
|
parent_thread_id = str(
|
|
|
|
|
|
getattr(self.parent_context, "parent_thread_id", None) or self.parent_context.thread_id
|
|
|
|
|
|
)
|
|
|
|
|
|
file_thread_id = str(getattr(self.parent_context, "file_thread_id", None) or parent_thread_id)
|
|
|
|
|
|
uid = str(getattr(self.parent_context, "uid", "") or "").strip()
|
|
|
|
|
|
if not uid:
|
|
|
|
|
|
return "无法调用子智能体:当前运行时缺少 uid"
|
|
|
|
|
|
|
|
|
|
|
|
agent = self.subagents[subagent_type]
|
|
|
|
|
|
backend = _get_agent_backend(agent.backend_id)
|
|
|
|
|
|
if not backend or agent.backend_id != SUB_AGENT_BACKEND_ID:
|
|
|
|
|
|
return f"无法调用子智能体 {subagent_type}:后端配置无效"
|
|
|
|
|
|
|
2026-06-02 18:47:28 +08:00
|
|
|
|
child_thread_id, continuing = _new_child_thread_id(
|
|
|
|
|
|
thread_id,
|
|
|
|
|
|
parent_thread_id=parent_thread_id,
|
|
|
|
|
|
agent_slug=agent.slug,
|
|
|
|
|
|
tool_call_id=runtime.tool_call_id,
|
|
|
|
|
|
)
|
2026-06-01 22:28:45 +08:00
|
|
|
|
subagent_run = {
|
|
|
|
|
|
"id": runtime.tool_call_id,
|
|
|
|
|
|
"subagent_type": subagent_type,
|
|
|
|
|
|
"subagent_name": agent.name,
|
|
|
|
|
|
"child_thread_id": child_thread_id,
|
|
|
|
|
|
"description": description,
|
|
|
|
|
|
"created_at": utc_isoformat(),
|
|
|
|
|
|
}
|
2026-06-02 18:47:28 +08:00
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
run, is_new_run = await self._create_subagent_run(
|
|
|
|
|
|
child_thread_id=child_thread_id,
|
|
|
|
|
|
description=description,
|
|
|
|
|
|
subagent_type=subagent_type,
|
|
|
|
|
|
agent=agent,
|
|
|
|
|
|
uid=uid,
|
|
|
|
|
|
parent_thread_id=parent_thread_id,
|
|
|
|
|
|
tool_call_id=runtime.tool_call_id,
|
|
|
|
|
|
continuing=continuing,
|
|
|
|
|
|
)
|
|
|
|
|
|
except ValueError as exc:
|
|
|
|
|
|
return str(exc)
|
|
|
|
|
|
subagent_run = _with_run_payload(subagent_run, run)
|
|
|
|
|
|
if not is_new_run:
|
|
|
|
|
|
return _reused_run_response(run, runtime.tool_call_id, subagent_run)
|
|
|
|
|
|
|
2026-06-01 22:28:45 +08:00
|
|
|
|
child_context = backend.context_schema()
|
|
|
|
|
|
config_context = (agent.config_json or {}).get("context") if isinstance(agent.config_json, dict) else None
|
2026-06-02 18:47:28 +08:00
|
|
|
|
child_input_context = await build_agent_input_context(
|
|
|
|
|
|
config_context if isinstance(config_context, dict) else {},
|
|
|
|
|
|
thread_id=child_thread_id,
|
|
|
|
|
|
uid=uid,
|
|
|
|
|
|
run_id=run.id,
|
|
|
|
|
|
request_id=run.request_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
child_context.update_from_dict(child_input_context)
|
2026-06-01 22:28:45 +08:00
|
|
|
|
child_context.uid = uid
|
|
|
|
|
|
child_context.thread_id = child_thread_id
|
|
|
|
|
|
child_context.parent_thread_id = parent_thread_id
|
|
|
|
|
|
child_context.file_thread_id = file_thread_id
|
|
|
|
|
|
child_context.skills_thread_id = child_thread_id
|
2026-06-02 18:47:28 +08:00
|
|
|
|
child_context.run_id = run.id
|
|
|
|
|
|
child_context.request_id = run.request_id
|
2026-06-01 22:28:45 +08:00
|
|
|
|
child_context.is_subagent_runtime = True
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
graph = await backend.get_graph(context=child_context)
|
|
|
|
|
|
result = await graph.ainvoke(
|
|
|
|
|
|
_state_for_child(
|
|
|
|
|
|
description,
|
|
|
|
|
|
runtime,
|
|
|
|
|
|
parent_thread_id=parent_thread_id,
|
|
|
|
|
|
file_thread_id=file_thread_id,
|
|
|
|
|
|
skills_thread_id=child_thread_id,
|
2026-06-02 18:47:28 +08:00
|
|
|
|
continuing=continuing,
|
2026-06-01 22:28:45 +08:00
|
|
|
|
),
|
|
|
|
|
|
config=_child_config(
|
|
|
|
|
|
runtime,
|
|
|
|
|
|
child_thread_id=child_thread_id,
|
|
|
|
|
|
uid=uid,
|
|
|
|
|
|
parent_thread_id=parent_thread_id,
|
|
|
|
|
|
file_thread_id=file_thread_id,
|
|
|
|
|
|
skills_thread_id=child_thread_id,
|
2026-06-02 18:47:28 +08:00
|
|
|
|
subagent_type=subagent_type,
|
|
|
|
|
|
run_id=run.id,
|
|
|
|
|
|
request_id=run.request_id,
|
2026-06-01 22:28:45 +08:00
|
|
|
|
),
|
|
|
|
|
|
context=child_context,
|
|
|
|
|
|
)
|
2026-06-02 18:47:28 +08:00
|
|
|
|
except asyncio.CancelledError:
|
|
|
|
|
|
await self._set_subagent_run_status(run.id, "cancelled")
|
|
|
|
|
|
raise
|
2026-06-01 22:28:45 +08:00
|
|
|
|
except Exception as exc:
|
2026-06-02 18:47:28 +08:00
|
|
|
|
failed_run = await self._set_subagent_run_status(
|
|
|
|
|
|
run.id,
|
|
|
|
|
|
"failed",
|
|
|
|
|
|
error_type=type(exc).__name__,
|
|
|
|
|
|
error_message=str(exc),
|
|
|
|
|
|
)
|
|
|
|
|
|
return _failed_tool_response(exc, runtime.tool_call_id, _with_run_payload(subagent_run, failed_run))
|
|
|
|
|
|
completed_run = await self._set_subagent_run_status(run.id, "completed")
|
|
|
|
|
|
return _completed_tool_response(
|
|
|
|
|
|
result, runtime.tool_call_id, _with_run_payload(subagent_run, completed_run)
|
|
|
|
|
|
)
|
2026-06-01 22:28:45 +08:00
|
|
|
|
|
|
|
|
|
|
return StructuredTool.from_function(
|
|
|
|
|
|
name="task",
|
|
|
|
|
|
func=task,
|
|
|
|
|
|
coroutine=atask,
|
|
|
|
|
|
description=TASK_TOOL_DESCRIPTION.format(available_agents=available_agents),
|
|
|
|
|
|
infer_schema=True,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def wrap_model_call(
|
|
|
|
|
|
self,
|
|
|
|
|
|
request: ModelRequest[ContextT],
|
|
|
|
|
|
handler: Callable[[ModelRequest[ContextT]], ModelResponse[ResponseT]],
|
|
|
|
|
|
) -> ModelResponse[ResponseT]:
|
|
|
|
|
|
return handler(
|
|
|
|
|
|
request.override(system_message=append_to_system_message(request.system_message, self.system_prompt))
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
async def awrap_model_call(
|
|
|
|
|
|
self,
|
|
|
|
|
|
request: ModelRequest[ContextT],
|
|
|
|
|
|
handler: Callable[[ModelRequest[ContextT]], Awaitable[ModelResponse[ResponseT]]],
|
|
|
|
|
|
) -> ModelResponse[ResponseT]:
|
|
|
|
|
|
return await handler(
|
|
|
|
|
|
request.override(system_message=append_to_system_message(request.system_message, self.system_prompt))
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def create_subagent_task_middleware(parent_context) -> YuxiSubAgentMiddleware | None:
|
|
|
|
|
|
selected_slugs = [
|
|
|
|
|
|
str(slug).strip() for slug in (getattr(parent_context, "subagents", None) or []) if str(slug).strip()
|
|
|
|
|
|
]
|
|
|
|
|
|
uid = str(getattr(parent_context, "uid", "") or "").strip()
|
|
|
|
|
|
if not uid:
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
async with pg_manager.get_async_session_context() as db:
|
|
|
|
|
|
user = await UserRepository().get_by_uid_with_db(db, uid)
|
|
|
|
|
|
if user is None:
|
|
|
|
|
|
return None
|
|
|
|
|
|
repo = AgentRepository(db)
|
|
|
|
|
|
if selected_slugs:
|
|
|
|
|
|
subagents: list[Agent] = []
|
|
|
|
|
|
seen: set[str] = set()
|
|
|
|
|
|
for slug in selected_slugs:
|
|
|
|
|
|
if slug in seen:
|
|
|
|
|
|
continue
|
|
|
|
|
|
seen.add(slug)
|
|
|
|
|
|
agent = await repo.get_visible_subagent_by_slug(slug=slug, user=user)
|
|
|
|
|
|
if agent and agent.backend_id == SUB_AGENT_BACKEND_ID:
|
|
|
|
|
|
subagents.append(agent)
|
|
|
|
|
|
else:
|
|
|
|
|
|
subagents = [
|
|
|
|
|
|
agent
|
|
|
|
|
|
for agent in await repo.list_visible_subagents(user=user)
|
|
|
|
|
|
if agent.backend_id == SUB_AGENT_BACKEND_ID
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
if not subagents:
|
|
|
|
|
|
return None
|
|
|
|
|
|
return YuxiSubAgentMiddleware(parent_context=parent_context, subagents=subagents)
|