OpenSpace/openspace/tools/sleep_tool.py
2026-07-17 11:43:42 +08:00

168 lines
5.4 KiB
Python

"""SleepTool - safe wait primitive for OpenSpace."""
from __future__ import annotations
import asyncio
import os
import time
from typing import Any, Mapping
from openspace.grounding.core.permissions.types import PermissionAllow
from openspace.grounding.core.tool.base import BaseTool
from openspace.grounding.core.types import BackendType, ToolResult, ToolSchema, ToolStatus
SLEEP_TOOL_NAME = "sleep"
SLEEP_TOOL_ALIAS = "Sleep"
DEFAULT_SLEEP_MAX_MS = 30_000
HARD_SLEEP_MAX_MS = 5 * 60 * 1000
DESCRIPTION = "Wait for a specified duration"
PROMPT = """Wait for a specified duration. The user can interrupt the sleep at any time.
Use this when the user tells you to sleep or rest, when you have nothing to do, or when you're waiting for something.
Prefer this over `bash(sleep ...)` because it does not hold a shell process."""
def _configured_max_ms() -> int:
raw = os.environ.get("OPENSPACE_SLEEP_MAX_MS")
if raw is None:
return DEFAULT_SLEEP_MAX_MS
try:
value = int(float(raw))
except (TypeError, ValueError):
return DEFAULT_SLEEP_MAX_MS
return max(1, min(value, HARD_SLEEP_MAX_MS))
def normalize_sleep_duration_ms(input_data: Mapping[str, Any]) -> tuple[int | None, str | None]:
has_duration_ms = input_data.get("duration_ms") is not None
has_seconds = input_data.get("seconds") is not None
if has_duration_ms == has_seconds:
return None, "Provide exactly one of duration_ms or seconds."
raw = input_data.get("duration_ms") if has_duration_ms else input_data.get("seconds")
try:
value = float(raw)
except (TypeError, ValueError):
return None, "Sleep duration must be a positive number."
if value <= 0:
return None, "Sleep duration must be positive."
requested_ms = int(round(value if has_duration_ms else value * 1000))
max_ms = _configured_max_ms()
if requested_ms > max_ms:
return None, f"Sleep duration exceeds the configured limit of {max_ms} ms."
return requested_ms, None
def make_input_schema() -> dict[str, Any]:
return {
"type": "object",
"properties": {
"duration_ms": {
"type": "number",
"exclusiveMinimum": 0,
"description": "Duration to wait in milliseconds.",
},
"seconds": {
"type": "number",
"exclusiveMinimum": 0,
"description": "Duration to wait in seconds.",
},
"reason": {
"type": "string",
"description": "Optional short reason for waiting.",
},
},
"additionalProperties": False,
}
class SleepTool(BaseTool):
_name = SLEEP_TOOL_NAME
_description = DESCRIPTION
backend_type = BackendType.META
aliases = [SLEEP_TOOL_ALIAS, "SleepTool"]
_is_read_only = True
_is_concurrency_safe = True
should_defer = True
search_hint = "wait pause delay timer"
max_result_size_chars = 100_000
def __init__(self) -> None:
self._current_context: Any | None = None
super().__init__(
schema=ToolSchema(
name=self._name,
description=DESCRIPTION,
parameters=make_input_schema(),
backend_type=self.backend_type,
)
)
def set_context(self, context: Any) -> None:
self._current_context = context
def get_prompt(self, context: Any = None) -> str:
return PROMPT
async def validate_input(self, input: dict[str, Any], context: Any = None) -> str | None:
_, error = normalize_sleep_duration_ms(input)
return error
async def check_permissions(self, input: dict[str, Any], context: Any = None) -> PermissionAllow:
return PermissionAllow(updated_input=dict(input))
async def _arun(
self,
duration_ms: float | int | None = None,
seconds: float | int | None = None,
reason: str | None = None,
) -> ToolResult:
requested_ms, error = normalize_sleep_duration_ms(
{"duration_ms": duration_ms, "seconds": seconds}
)
if error or requested_ms is None:
return ToolResult(status=ToolStatus.ERROR, content=error or "Invalid sleep duration.", error=error)
started = time.monotonic()
abort_event = getattr(self._current_context, "abort_event", None)
try:
if isinstance(abort_event, asyncio.Event):
await asyncio.wait_for(abort_event.wait(), timeout=requested_ms / 1000)
aborted = True
else:
await asyncio.sleep(requested_ms / 1000)
aborted = False
except asyncio.TimeoutError:
aborted = False
waited_ms = int(round((time.monotonic() - started) * 1000))
data = {
"requested_ms": requested_ms,
"waited_ms": waited_ms,
"aborted": aborted,
"reason": reason or "",
"max_ms": _configured_max_ms(),
}
if aborted:
content = f"Sleep interrupted after {waited_ms} ms."
else:
content = f"Slept for {waited_ms} ms."
return ToolResult(
status=ToolStatus.SUCCESS,
content=content,
metadata={"tool": self.name, "data": data},
)
__all__ = [
"SLEEP_TOOL_ALIAS",
"DEFAULT_SLEEP_MAX_MS",
"HARD_SLEEP_MAX_MS",
"SLEEP_TOOL_NAME",
"SleepTool",
"normalize_sleep_duration_ms",
]