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
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.
85 lines
2.8 KiB
Python
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"},
|
|
)
|