Files
xerrors--yuxi/backend/test/unit/middlewares/test_subagent_task_middleware.py
wehub-resource-sync 1443d3fdf9
Ruff Format Check / Ruff Format & Lint (push) Failing after 7m39s
Deploy VitePress site to Pages / build (push) Failing after 9m11s
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:32:26 +08:00

793 lines
30 KiB
Python

from __future__ import annotations
import json
from types import SimpleNamespace
import pytest
import yuxi.agents.middlewares.subagent_task as subagent_task_middleware
import yuxi.services.agent_run_service as agent_run_service
import yuxi.services.run_queue_service as run_queue_service
import yuxi.services.subagent_run_service as subagent_run_service
from langchain.agents._subagent_transformer import SubagentTransformer as LangChainSubagentTransformer
from langgraph.prebuilt.tool_node import ToolRuntime
from langgraph.stream._mux import StreamMux
from langgraph.types import Command
from yuxi.agents.buildin.chatbot.state import merge_subagent_runs
from yuxi.agents.middlewares.subagent_task import YUXI_SUBAGENTS_STREAM_KEY, YuxiSubAgentMiddleware
from yuxi.repositories.agent_repository import SUB_AGENT_BACKEND_ID
from yuxi.services.input_message_service import AgentRunInputMessage
from yuxi.utils.hash_utils import subagent_child_thread_id
def make_child_thread_id(parent_thread_id: str, agent_slug: str, tool_call_id: str) -> str:
return subagent_child_thread_id(parent_thread_id, agent_slug, tool_call_id)
class _ChildContext:
def __init__(self):
self.model = None
def update_from_dict(self, values: dict):
for key, value in values.items():
if hasattr(self, key):
setattr(self, key, value)
class _SessionContext:
async def __aenter__(self):
return object()
async def __aexit__(self, exc_type, exc, tb):
return None
def _patch_session(monkeypatch):
monkeypatch.setattr(
subagent_task_middleware,
"pg_manager",
SimpleNamespace(get_async_session_context=lambda: _SessionContext()),
)
def _patch_subagent_run_service(monkeypatch, service_class) -> None:
monkeypatch.setattr(
subagent_task_middleware,
"_subagent_run_service_module",
lambda: SimpleNamespace(
SubagentRunService=service_class,
SubagentRunBusy=subagent_run_service.SubagentRunBusy,
serialize_subagent_run_state=subagent_run_service.serialize_subagent_run_state,
subagent_run_urls=subagent_run_service.subagent_run_urls,
),
)
def _async_tool_middleware(*, model: str | None = None) -> YuxiSubAgentMiddleware:
parent_context = SimpleNamespace(thread_id="parent-thread", uid="user-1", run_id="parent-run")
if model:
parent_context.model = model
return YuxiSubAgentMiddleware(
parent_context=parent_context,
subagents=[
SimpleNamespace(
slug="worker",
name="Worker",
description="work on scoped tasks",
backend_id=SUB_AGENT_BACKEND_ID,
config_json={},
)
],
)
def _subagent_run(
*,
status: str = "running",
thread_id: str = "child-thread",
tool_call_id: str = "tool-async",
subagent_slug: str = "worker",
subagent_name: str = "Worker",
description: str = "run in background",
error_message: str | None = None,
):
return SimpleNamespace(
id="child-run",
conversation_thread_id=thread_id,
agent_slug=subagent_slug,
status=status,
created_by_run_id="parent-run",
subagent_thread_relation_id=77,
input_payload={
"runtime": {
"tool_call_id": tool_call_id,
"subagent_name": subagent_name,
"description": description,
},
},
created_at=None,
finished_at=None,
error_message=error_message,
)
def _patch_task_start_and_await(
monkeypatch,
captured: dict,
*,
status: str = "completed",
output: str = "child done",
thread_id: str = "child-thread",
error_message: str | None = None,
wait_timeout: bool = False,
):
class _SubagentRunService:
def __init__(self, db):
captured["db"] = db
async def start(self, **kwargs):
captured["start"] = kwargs
return SimpleNamespace(
run=_subagent_run(
status="pending",
thread_id=thread_id,
tool_call_id=kwargs["tool_call_id"],
subagent_slug=kwargs["agent_item"].slug,
subagent_name=kwargs["agent_item"].name,
description=kwargs["input_message"].content,
),
created=True,
continuing=bool(kwargs.get("requested_thread_id")),
relation=SimpleNamespace(id=77, child_thread_id=thread_id),
)
async def get_run_for_creator(self, **kwargs):
captured["get_run_for_creator"] = kwargs
started = captured["start"]
return _subagent_run(
status=status,
thread_id=thread_id,
tool_call_id=started["tool_call_id"],
subagent_slug=started["agent_item"].slug,
subagent_name=started["agent_item"].name,
description=started["input_message"].content,
error_message=error_message,
)
async def fake_await_agent_run_result(*, run_id: str, current_uid: str):
captured["await"] = {"run_id": run_id, "current_uid": current_uid}
result = {
"status": status,
"output": output,
"agent_run_id": run_id,
"thread_id": thread_id,
}
if error_message:
result["error"] = {"type": "RuntimeError", "message": error_message}
if wait_timeout:
raise agent_run_service.AgentRunWaitTimeout(result)
return result
_patch_session(monkeypatch)
_patch_subagent_run_service(monkeypatch, _SubagentRunService)
monkeypatch.setattr(agent_run_service, "await_agent_run_result", fake_await_agent_run_result)
@pytest.mark.asyncio
async def test_create_task_middleware_loads_all_visible_subagents_when_empty(monkeypatch) -> None:
class _UserRepository:
async def get_by_uid_with_db(self, _db, uid):
assert uid == "user-1"
return SimpleNamespace(uid="user-1", role="user")
class _AgentRepository:
def __init__(self, _db):
pass
async def list_visible_subagents(self, *, user):
assert user.uid == "user-1"
return [
SimpleNamespace(
slug="worker",
name="Worker",
description="work on scoped tasks",
backend_id=SUB_AGENT_BACKEND_ID,
config_json={},
),
]
async def get_visible_by_slug(self, *, slug, user, kind="main"):
del slug
del user
del kind
raise AssertionError("empty subagents should load all visible subagents")
_patch_session(monkeypatch)
monkeypatch.setattr(subagent_task_middleware, "UserRepository", _UserRepository)
monkeypatch.setattr(subagent_task_middleware, "AgentRepository", _AgentRepository)
middleware = await subagent_task_middleware.create_subagent_task_middleware(
SimpleNamespace(thread_id="parent-thread", uid="user-1", subagents=[]),
)
assert isinstance(middleware, YuxiSubAgentMiddleware)
assert middleware.subagent_names == frozenset({"worker"})
assert {tool.name for tool in middleware.tools} >= {"task", "subagent_start"}
def test_yuxi_subagent_transformer_does_not_conflict_with_langchain_default() -> None:
middleware = YuxiSubAgentMiddleware(
parent_context=SimpleNamespace(thread_id="parent-thread", uid="user-1"),
subagents=[
SimpleNamespace(
slug="worker",
name="Worker",
description="work on scoped tasks",
backend_id=SUB_AGENT_BACKEND_ID,
config_json={},
)
],
)
mux = StreamMux(
factories=[LangChainSubagentTransformer, *middleware.transformers],
is_async=True,
)
assert "subagents" in mux.extensions
assert YUXI_SUBAGENTS_STREAM_KEY in mux.extensions
@pytest.mark.asyncio
async def test_task_tool_rejects_unconfigured_subagent() -> None:
middleware = YuxiSubAgentMiddleware(
parent_context=SimpleNamespace(thread_id="parent-thread", uid="user-1", model=""),
subagents=[
SimpleNamespace(
slug="worker",
name="Worker",
description="work on scoped tasks",
backend_id=SUB_AGENT_BACKEND_ID,
config_json={},
)
],
)
runtime = ToolRuntime(
state={},
context=None,
tool_call_id="tool-1",
store=None,
stream_writer=lambda _: None,
config={},
)
result = await middleware.tools[0].ainvoke(
{"description": "do work", "subagent_slug": "missing", "runtime": runtime}
)
assert result == "无法调用子智能体 missing,可用子智能体只有:`worker`"
@pytest.mark.asyncio
async def test_task_tool_invokes_subagent_with_child_scope(monkeypatch) -> None:
captured = {}
_patch_task_start_and_await(monkeypatch, captured, thread_id="child-thread")
middleware = YuxiSubAgentMiddleware(
parent_context=SimpleNamespace(
thread_id="child-runtime-thread",
parent_thread_id="parent-thread",
file_thread_id="parent-file-thread",
uid="user-1",
run_id="parent-run",
),
subagents=[
SimpleNamespace(
slug="worker.agent",
name="Worker",
description="work on scoped tasks",
backend_id=SUB_AGENT_BACKEND_ID,
config_json={"context": {"model": "provider:model", "subagents": ["nested"]}},
)
],
)
runtime = SimpleNamespace(tool_call_id="tool-1", state={}, config={})
result = await middleware.tools[0].coroutine(
description="write a report",
subagent_slug="worker.agent",
runtime=runtime,
)
assert isinstance(result, Command)
assert result.update["messages"][0].content == "> 子智能体线程 ID: child-thread\n\n---\n\nchild done"
assert result.update["messages"][0].tool_call_id == "tool-1"
assert captured["start"]["uid"] == "user-1"
assert captured["start"]["created_by_run_id"] == "parent-run"
assert captured["start"]["requested_thread_id"] is None
assert captured["start"]["file_thread_id"] == "parent-file-thread"
assert captured["start"]["model_spec"] is None
assert captured["await"] == {"run_id": "child-run", "current_uid": "user-1"}
assert result.update["subagent_runs"][0]["run_id"] == "child-run"
assert result.update["subagent_runs"][0]["child_thread_id"] == "child-thread"
assert result.update["subagent_runs"][0]["status"] == "completed"
@pytest.mark.asyncio
async def test_task_tool_inherits_parent_model_when_subagent_model_empty(monkeypatch) -> None:
captured = {}
_patch_task_start_and_await(monkeypatch, captured)
middleware = YuxiSubAgentMiddleware(
parent_context=SimpleNamespace(
thread_id="parent-thread",
uid="user-1",
run_id="parent-run",
model="parent:model",
),
subagents=[
SimpleNamespace(
slug="worker",
name="Worker",
description="work on scoped tasks",
backend_id=SUB_AGENT_BACKEND_ID,
config_json={"context": {"model": ""}},
)
],
)
await middleware.tools[0].coroutine(
description="write a report",
subagent_slug="worker",
runtime=SimpleNamespace(tool_call_id="tool-1", state={}, config={}),
)
assert captured["start"]["model_spec"] == "parent:model"
@pytest.mark.asyncio
async def test_task_tool_records_failed_subagent_run(monkeypatch) -> None:
captured = {}
_patch_task_start_and_await(monkeypatch, captured, status="failed", output="", error_message="child boom")
middleware = YuxiSubAgentMiddleware(
parent_context=SimpleNamespace(thread_id="parent-thread", uid="user-1", run_id="parent-run", model=""),
subagents=[
SimpleNamespace(
slug="worker",
name="Worker",
description="work on scoped tasks",
backend_id=SUB_AGENT_BACKEND_ID,
config_json={},
)
],
)
result = await middleware.tools[0].coroutine(
description="write a report",
subagent_slug="worker",
runtime=SimpleNamespace(tool_call_id="tool-1", state={}, config={}),
)
assert isinstance(result, Command)
assert result.update["messages"][0].content == "> 子智能体线程 ID: child-thread\n\n---\n\nchild boom"
assert result.update["subagent_runs"][0]["status"] == "failed"
assert result.update["subagent_runs"][0]["error"] == "child boom"
@pytest.mark.asyncio
async def test_task_tool_reports_running_subagent_after_wait_timeout(monkeypatch) -> None:
captured = {}
_patch_task_start_and_await(monkeypatch, captured, status="running", output="", wait_timeout=True)
middleware = YuxiSubAgentMiddleware(
parent_context=SimpleNamespace(thread_id="parent-thread", uid="user-1", run_id="parent-run", model=""),
subagents=[
SimpleNamespace(
slug="worker",
name="Worker",
description="work on scoped tasks",
backend_id=SUB_AGENT_BACKEND_ID,
config_json={},
)
],
)
result = await middleware.tools[0].coroutine(
description="write a long report",
subagent_slug="worker",
runtime=SimpleNamespace(tool_call_id="tool-1", state={}, config={}),
)
assert isinstance(result, Command)
content = result.update["messages"][0].content
assert "子智能体仍在运行" in content
assert "run_id: child-run" in content
assert "不要把当前结果视为任务已完成" in content
assert "子智能体已完成任务" not in content
assert result.update["subagent_runs"][0]["status"] == "running"
@pytest.mark.asyncio
async def test_task_tool_continues_existing_subagent_thread(monkeypatch) -> None:
captured = {}
_patch_task_start_and_await(monkeypatch, captured, output="continued done", thread_id="child-thread")
middleware = YuxiSubAgentMiddleware(
parent_context=SimpleNamespace(thread_id="parent-thread", uid="user-1", run_id="parent-run", model=""),
subagents=[
SimpleNamespace(
slug="worker.agent",
name="Worker",
description="work on scoped tasks",
backend_id=SUB_AGENT_BACKEND_ID,
config_json={},
)
],
)
result = await middleware.tools[0].coroutine(
description="continue the report",
subagent_slug="worker.agent",
runtime=SimpleNamespace(tool_call_id="tool-2", state={}, config={}),
thread_id="child-thread",
)
assert isinstance(result, Command)
assert result.update["messages"][0].content == "> 子智能体线程 ID: child-thread\n\n---\n\ncontinued done"
assert captured["start"]["requested_thread_id"] == "child-thread"
assert result.update["subagent_runs"][0]["child_thread_id"] == "child-thread"
@pytest.mark.asyncio
async def test_task_tool_rejects_invalid_continuation_thread(monkeypatch) -> None:
class _SubagentRunService:
def __init__(self, db):
del db
async def start(self, **kwargs):
raise ValueError(
f"无法继续子智能体线程 {kwargs['requested_thread_id']}:当前对话中没有找到对应的运行记录"
)
_patch_session(monkeypatch)
_patch_subagent_run_service(monkeypatch, _SubagentRunService)
middleware = YuxiSubAgentMiddleware(
parent_context=SimpleNamespace(thread_id="parent-thread", uid="user-1", run_id="parent-run"),
subagents=[
SimpleNamespace(
slug="worker",
name="Worker",
description="work on scoped tasks",
backend_id=SUB_AGENT_BACKEND_ID,
config_json={},
)
],
)
unknown_thread_id = "opaque-child-thread"
runtime = SimpleNamespace(tool_call_id="tool-2", state={}, config={})
result = await middleware.tools[0].coroutine(
description="continue",
subagent_slug="worker",
runtime=runtime,
thread_id=unknown_thread_id,
)
assert result == f"无法继续子智能体线程 {unknown_thread_id}:当前对话中没有找到对应的运行记录"
@pytest.mark.asyncio
async def test_subagent_start_creates_child_run_and_enqueues(monkeypatch) -> None:
captured: dict[str, object] = {}
class _SubagentRunService:
def __init__(self, db):
captured["db"] = db
async def start(self, **kwargs):
captured["start"] = kwargs
child_thread_id = make_child_thread_id("parent-thread", "worker", "tool-async")
return SimpleNamespace(
run=_subagent_run(status="pending", thread_id=child_thread_id),
created=True,
continuing=False,
relation=SimpleNamespace(id=77, child_thread_id=child_thread_id),
)
_patch_session(monkeypatch)
_patch_subagent_run_service(monkeypatch, _SubagentRunService)
middleware = _async_tool_middleware(model="provider:parent-model")
runtime = SimpleNamespace(tool_call_id="tool-async", state={}, config={})
tool = next(item for item in middleware.tools if item.name == "subagent_start")
result = await tool.coroutine(
description="run in background",
subagent_slug="worker",
runtime=runtime,
)
child_thread_id = make_child_thread_id("parent-thread", "worker", "tool-async")
assert isinstance(result, Command)
payload = json.loads(result.update["messages"][0].content)
assert payload["status"] == "started"
assert payload["run_id"] == "child-run"
assert payload["thread_id"] == child_thread_id
assert payload["events_url"] == "/api/agent/runs/child-run/events"
assert payload["subagent_thread_relation_id"] == 77
assert captured["start"]["uid"] == "user-1"
assert captured["start"]["created_by_run_id"] == "parent-run"
assert isinstance(captured["start"]["input_message"], AgentRunInputMessage)
assert captured["start"]["input_message"].content == "run in background"
assert captured["start"]["input_message"].raw_message()["type"] == "human"
assert captured["start"]["input_message"].raw_message()["content"] == "run in background"
assert "description" not in captured["start"]
assert captured["start"]["requested_thread_id"] is None
assert captured["start"]["model_spec"] == "provider:parent-model"
assert result.update["subagent_runs"][0]["child_thread_id"] == child_thread_id
assert result.update["subagent_runs"][0]["run_id"] == "child-run"
@pytest.mark.asyncio
async def test_subagent_status_returns_terminal_result(monkeypatch) -> None:
captured: dict[str, object] = {}
class _SubagentRunService:
def __init__(self, db):
captured["db"] = db
async def get_run_for_creator(self, **kwargs):
captured["get_run_for_creator"] = kwargs
return _subagent_run(status="completed")
async def fake_get_agent_run_result(*, run_id: str, current_uid: str, db):
captured["get_agent_run_result"] = {"run_id": run_id, "current_uid": current_uid, "db": db}
return {"status": "completed", "output": "final result"}
async def fake_get_agent_run_progress(run_id: str):
captured["get_agent_run_progress"] = run_id
return {
"last_seq": "3-0",
"messages": [{"seq": "3-0", "kind": "assistant_message", "message_id": "msg-1", "content": "working"}],
}
_patch_session(monkeypatch)
_patch_subagent_run_service(monkeypatch, _SubagentRunService)
monkeypatch.setattr(agent_run_service, "get_agent_run_result", fake_get_agent_run_result)
monkeypatch.setattr(agent_run_service, "get_agent_run_progress", fake_get_agent_run_progress)
tool = next(item for item in _async_tool_middleware().tools if item.name == "subagent_status")
result = await tool.coroutine(run_id="child-run", runtime=SimpleNamespace(tool_call_id="status-call"))
assert isinstance(result, Command)
payload = json.loads(result.update["messages"][0].content)
assert payload["status"] == "completed"
assert payload["progress"]["messages"][0]["content"] == "working"
assert payload["result"]["output"] == "final result"
assert captured["get_agent_run_progress"] == "child-run"
assert captured["get_run_for_creator"] == {
"uid": "user-1",
"created_by_run_id": "parent-run",
"run_id": "child-run",
}
assert captured["get_agent_run_result"]["current_uid"] == "user-1"
assert result.update["subagent_runs"] == [
{
"id": "tool-async",
"run_id": "child-run",
"subagent_slug": "worker",
"subagent_name": "Worker",
"child_thread_id": "child-thread",
"status": "completed",
"events_url": "/api/agent/runs/child-run/events",
"result_url": "/api/agent/runs/child-run/result",
}
]
@pytest.mark.asyncio
async def test_subagent_status_returns_progress_for_running_run(monkeypatch) -> None:
captured: dict[str, object] = {}
class _SubagentRunService:
def __init__(self, db):
captured["db"] = db
async def get_run_for_creator(self, **kwargs):
captured["get_run_for_creator"] = kwargs
return _subagent_run(status="running")
async def fake_get_agent_run_result(**kwargs):
raise AssertionError(f"running status should not load terminal result: {kwargs}")
async def fake_get_agent_run_progress(run_id: str):
captured["get_agent_run_progress"] = run_id
return {
"last_seq": "9-0",
"messages": [
{"seq": "8-0", "kind": "tool_call", "tool_call_id": "call-1", "content": "调用工具 read_file"},
{"seq": "9-0", "kind": "assistant_message", "message_id": "msg-2", "content": "正在整理结果"},
],
}
_patch_session(monkeypatch)
_patch_subagent_run_service(monkeypatch, _SubagentRunService)
monkeypatch.setattr(agent_run_service, "get_agent_run_result", fake_get_agent_run_result)
monkeypatch.setattr(agent_run_service, "get_agent_run_progress", fake_get_agent_run_progress)
tool = next(item for item in _async_tool_middleware().tools if item.name == "subagent_status")
result = await tool.coroutine(run_id="child-run", runtime=SimpleNamespace(tool_call_id="status-call"))
payload = json.loads(result.update["messages"][0].content)
assert payload["status"] == "running"
assert "result" not in payload
assert payload["progress"]["last_seq"] == "9-0"
assert [item["content"] for item in payload["progress"]["messages"]] == ["调用工具 read_file", "正在整理结果"]
assert captured["get_run_for_creator"] == {
"uid": "user-1",
"created_by_run_id": "parent-run",
"run_id": "child-run",
}
@pytest.mark.asyncio
async def test_subagent_events_cancel_and_await_use_parent_run_scope(monkeypatch) -> None:
captured: dict[str, object] = {"loads": []}
async def fake_get_verified_subagent_run(self, *, run_id: str, uid: str, created_by_run_id: str):
del self
captured["loads"].append(
{
"run_id": run_id,
"uid": uid,
"created_by_run_id": created_by_run_id,
}
)
return _subagent_run(status="completed")
async def fake_list_run_stream_events(run_id: str, *, after_seq: str, limit: int):
captured["events"] = {"run_id": run_id, "after_seq": after_seq, "limit": limit}
return [{"seq": "2-0", "event_type": "messages", "payload": {"chunk": {"status": "loading"}}}]
async def fake_request_cancel_agent_run(*, run_id: str, current_uid: str, db):
captured["cancel"] = {"run_id": run_id, "current_uid": current_uid, "db": db}
return _subagent_run(status="cancelling")
async def fake_await_agent_run_result(*, run_id: str, current_uid: str):
captured["await"] = {"run_id": run_id, "current_uid": current_uid}
return {"status": "completed", "output": "awaited result"}
_patch_session(monkeypatch)
monkeypatch.setattr(YuxiSubAgentMiddleware, "_get_verified_subagent_run", fake_get_verified_subagent_run)
monkeypatch.setattr(run_queue_service, "list_run_stream_events", fake_list_run_stream_events)
monkeypatch.setattr(agent_run_service, "request_cancel_agent_run", fake_request_cancel_agent_run)
monkeypatch.setattr(agent_run_service, "await_agent_run_result", fake_await_agent_run_result)
tools = {item.name: item for item in _async_tool_middleware().tools}
events_result = await tools["subagent_events"].coroutine(
run_id="child-run",
after_seq="1-0",
limit=99,
runtime=SimpleNamespace(tool_call_id="events-call"),
)
events_payload = json.loads(events_result.update["messages"][0].content)
assert events_payload["last_seq"] == "2-0"
assert captured["events"] == {"run_id": "child-run", "after_seq": "1-0", "limit": 50}
cancel_result = await tools["subagent_cancel"].coroutine(
run_id="child-run",
runtime=SimpleNamespace(tool_call_id="cancel-call"),
)
cancel_payload = json.loads(cancel_result.update["messages"][0].content)
assert cancel_payload["status"] == "cancelling"
assert captured["cancel"]["current_uid"] == "user-1"
await_result = await tools["subagent_await"].coroutine(
run_id="child-run",
runtime=SimpleNamespace(tool_call_id="await-call"),
)
await_payload = json.loads(await_result.update["messages"][0].content)
assert await_payload["result"]["output"] == "awaited result"
assert captured["await"] == {"run_id": "child-run", "current_uid": "user-1"}
assert captured["loads"]
assert all(
load == {"run_id": "child-run", "uid": "user-1", "created_by_run_id": "parent-run"}
for load in captured["loads"]
)
@pytest.mark.asyncio
async def test_subagent_await_reports_timeout_when_run_is_still_active(monkeypatch) -> None:
captured: dict[str, object] = {"loads": []}
async def fake_get_verified_subagent_run(self, *, run_id: str, uid: str, created_by_run_id: str):
del self
captured["loads"].append(
{
"run_id": run_id,
"uid": uid,
"created_by_run_id": created_by_run_id,
}
)
return _subagent_run(status="running")
async def fake_await_agent_run_result(*, run_id: str, current_uid: str):
captured["await"] = {"run_id": run_id, "current_uid": current_uid}
raise agent_run_service.AgentRunWaitTimeout(
{"status": "running", "agent_run_id": run_id, "thread_id": "child-thread", "output": ""}
)
_patch_session(monkeypatch)
monkeypatch.setattr(YuxiSubAgentMiddleware, "_get_verified_subagent_run", fake_get_verified_subagent_run)
monkeypatch.setattr(agent_run_service, "await_agent_run_result", fake_await_agent_run_result)
result = await {item.name: item for item in _async_tool_middleware().tools}["subagent_await"].coroutine(
run_id="child-run",
runtime=SimpleNamespace(tool_call_id="await-call"),
)
payload = json.loads(result.update["messages"][0].content)
assert payload["status"] == "running"
assert payload["wait_timed_out"] is True
assert payload["message"] == "子智能体仍在运行,等待最终结果超时;请稍后继续查询。"
assert payload["result"]["status"] == "running"
assert captured["await"] == {"run_id": "child-run", "current_uid": "user-1"}
assert len(captured["loads"]) == 2
def test_merge_subagent_runs_keeps_new_run_on_same_child_thread() -> None:
child_thread_id = make_child_thread_id("parent-thread", "worker", "tool-old")
merged = merge_subagent_runs(
[
{
"id": "tool-old",
"run_id": "run-old",
"subagent_slug": "worker",
"subagent_name": "Worker",
"child_thread_id": child_thread_id,
"description": "first task",
"status": "completed",
"created_at": "2026-05-31T01:00:00Z",
"completed_at": "2026-05-31T01:01:00Z",
}
],
[
{
"id": "tool-new",
"run_id": "run-new",
"subagent_slug": "worker",
"subagent_name": "Worker",
"child_thread_id": child_thread_id,
"description": "continue task",
"status": "pending",
"created_at": "2026-05-31T02:00:00Z",
}
],
)
assert merged == [
{
"id": "tool-old",
"run_id": "run-old",
"subagent_slug": "worker",
"subagent_name": "Worker",
"child_thread_id": child_thread_id,
"description": "first task",
"status": "completed",
"created_at": "2026-05-31T01:00:00Z",
"completed_at": "2026-05-31T01:01:00Z",
},
{
"id": "tool-new",
"run_id": "run-new",
"subagent_slug": "worker",
"subagent_name": "Worker",
"child_thread_id": child_thread_id,
"description": "continue task",
"status": "pending",
"created_at": "2026-05-31T02:00:00Z",
},
]