Files
EvoScientist-Multi/EvoScientist/middleware/subagent_timeout.py
T
m4 c683f6e739
Docker / build (push) Has been cancelled
Build / build (push) Has been cancelled
Lint / ruff (push) Has been cancelled
Test / pytest (ubuntu-latest, 3.11) (push) Has been cancelled
Test / pytest (ubuntu-latest, 3.12) (push) Has been cancelled
Test / pytest (windows-latest, 3.11) (push) Has been cancelled
Test / pytest (windows-latest, 3.12) (push) Has been cancelled
feat: prepare EvoScientist 0.3.0
Add bounded document ingestion, controlled web search, recoverable session support, subagent timeouts, and the native sandbox runtime contract. Unify package versioning and add release-focused regression coverage.
2026-09-03 06:55:56 +08:00

85 lines
2.8 KiB
Python

"""Bound synchronous sub-agent calls in hosted Web runs."""
from __future__ import annotations
import asyncio
import json
from collections.abc import Awaitable, Callable
from typing import Any
from langchain.agents.middleware.types import AgentMiddleware, ToolCallRequest
from langchain_core.messages import ToolMessage
from langgraph.types import Command
class SubagentTimeoutMiddleware(AgentMiddleware):
"""Cancel a synchronous ``task`` call that exceeds the Web time budget."""
@property
def name(self) -> str:
return "subagent_timeout"
def __init__(self, timeout_seconds: float = 180.0) -> None:
super().__init__()
if timeout_seconds <= 0:
raise ValueError("timeout_seconds must be positive")
self.timeout_seconds = float(timeout_seconds)
@staticmethod
async def _cancel_task(task: asyncio.Future[Any]) -> None:
task.cancel()
try:
await asyncio.shield(task)
except asyncio.CancelledError:
if not task.done():
task.add_done_callback(SubagentTimeoutMiddleware._consume_task_result)
raise
except Exception:
pass
@staticmethod
def _consume_task_result(task: asyncio.Future[Any]) -> None:
try:
task.result()
except (asyncio.CancelledError, Exception):
pass
async def awrap_tool_call(
self,
request: ToolCallRequest,
handler: Callable[[ToolCallRequest], Awaitable[ToolMessage | Command[Any]]],
) -> ToolMessage | Command[Any]:
if str(request.tool_call.get("name") or "") != "task":
return await handler(request)
task = asyncio.ensure_future(handler(request))
try:
done, _pending = await asyncio.wait(
{task}, timeout=self.timeout_seconds
)
except asyncio.CancelledError:
await self._cancel_task(task)
raise
if done:
return task.result()
await self._cancel_task(task)
current = asyncio.current_task()
if current is not None and current.cancelling():
raise asyncio.CancelledError
payload = {
"code": "SUBAGENT_TIMEOUT",
"message": (
"The delegated sub-agent exceeded the hosted Web time limit. "
"Continue with available evidence or use the controlled web search tool directly."
),
"retryable": True,
"timeout_seconds": self.timeout_seconds,
}
return ToolMessage(
content=json.dumps(payload, ensure_ascii=False),
tool_call_id=str(request.tool_call.get("id") or "subagent_timeout"),
name="task",
status="error",
additional_kwargs={"error_code": "SUBAGENT_TIMEOUT"},
)