11"""Tool that pauses execution for a given amount of time."""
22
33import asyncio
4+ import math
45import time
56from typing import Any
67
2526 "Do not use this to pace a conversation, to pretend to work, or when the "
2627 "information needed is already available. Waiting only lets clock time pass, it "
2728 "does not advance or check the job.\n "
28- "A single call waits at most {max_seconds} seconds . If more time is needed, "
29+ "A single call waits at most {cap} . If more time is needed, "
2930 "call this tool again."
3031)
3132
3233# Everything before the cap figure; used to tell a generated description from one
3334# the caller wrote, so only generated text is kept in sync with ``max_seconds``.
34- _GENERATED_DESCRIPTION_PREFIX = _DESCRIPTION_TEMPLATE .split ("{max_seconds}" )[0 ]
35+ _GENERATED_DESCRIPTION_PREFIX = _DESCRIPTION_TEMPLATE .split ("{cap}" )[0 ]
36+
37+
38+ def _format_seconds (value : float ) -> str :
39+ """Render a duration with a correctly pluralized unit.
40+
41+ Args:
42+ value: The duration in seconds.
43+
44+ Returns:
45+ The duration and its unit, e.g. ``"1 second"`` or ``"300 seconds"``.
46+ """
47+ return f"{ value :g} second" if value == 1 else f"{ value :g} seconds"
3548
3649
3750def _build_description (max_seconds : float ) -> str :
@@ -43,7 +56,7 @@ def _build_description(max_seconds: float) -> str:
4356 Returns:
4457 The tool description shown to the model.
4558 """
46- return _DESCRIPTION_TEMPLATE .format (max_seconds = f" { max_seconds :g } " )
59+ return _DESCRIPTION_TEMPLATE .format (cap = _format_seconds ( max_seconds ) )
4760
4861
4962def _is_generated_description (description : str ) -> bool :
@@ -145,8 +158,9 @@ def _resolve_duration(self, seconds: float) -> tuple[float, bool]:
145158 """Validate and clamp the requested duration to ``max_seconds``.
146159
147160 ``BaseTool.run`` skips ``args_schema`` validation when called with
148- positional arguments, so the non-negative bound is enforced here too
149- rather than left to ``time.sleep`` to reject.
161+ positional arguments, so the bounds are enforced here too rather than
162+ left to ``time.sleep`` to reject. Infinity is a valid request: it clamps
163+ to the cap like any other oversized wait.
150164
151165 Args:
152166 seconds: The requested wait duration.
@@ -155,8 +169,10 @@ def _resolve_duration(self, seconds: float) -> tuple[float, bool]:
155169 A tuple of the duration to actually wait and whether it was capped.
156170
157171 Raises:
158- ValueError: If ``seconds`` is negative.
172+ ValueError: If ``seconds`` is negative or not a number .
159173 """
174+ if math .isnan (seconds ):
175+ raise ValueError ("seconds must be a number, got NaN." )
160176 if seconds < 0 :
161177 raise ValueError (f"seconds must be zero or greater, got { seconds :g} ." )
162178 if seconds > self .max_seconds :
@@ -176,10 +192,11 @@ def _format_result(
176192 Returns:
177193 A summary of how long was waited and whether the request was capped.
178194 """
179- parts = [f"Waited { waited :g } seconds ." ]
195+ parts = [f"Waited { _format_seconds ( waited ) } ." ]
180196 if waited < requested :
181197 parts .append (
182- f"Requested { requested :g} seconds, capped at { self .max_seconds :g} seconds per call - "
198+ f"Requested { _format_seconds (requested )} , capped at "
199+ f"{ _format_seconds (self .max_seconds )} per call - "
183200 "call this tool again if more waiting is needed."
184201 )
185202 if reason :
0 commit comments