Add "wait" tool

linuztx committed Oct 16, 2025 at 13:39 UTC 8b16272bd7acf3142b56fcf353d8c476cfdf76f1
4 files changed +192
prompts/agent.system.tool.wait.md new
+34
@@ -0,0 +1,34 @@
1 +### wait
2 +pause execution for a set time or until a timestamp
3 +use args "seconds" "minutes" "hours" "days" for duration
4 +use "until" with ISO timestamp for a specific time
5 +usage:
6 +
7 +1 wait duration
8 +~~~json
9 +{
10 + "thoughts": [
11 + "I need to wait..."
12 + ],
13 + "headline": "...",
14 + "tool_name": "wait",
15 + "tool_args": {
16 + "minutes": 1,
17 + "seconds": 30
18 + }
19 +}
20 +~~~
21 +
22 +2 wait timestamp
23 +~~~json
24 +{
25 + "thoughts": [
26 + "I will wait until..."
27 + ],
28 + "headline": "...",
29 + "tool_name": "wait",
30 + "tool_args": {
31 + "until": "2025-10-20T10:00:00Z"
32 + }
33 +}
34 +~~~
prompts/fw.wait_complete.md new
+1
@@ -0,0 +1 @@
1 +Wait complete. Reached {{target_time}}.
\ No newline at end of file
python/helpers/wait.py new
+68
@@ -0,0 +1,68 @@
1 +import asyncio
2 +from datetime import datetime, timezone
3 +
4 +from python.helpers.print_style import PrintStyle
5 +
6 +
7 +def format_remaining_time(total_seconds: float) -> str:
8 + if total_seconds < 0:
9 + total_seconds = 0
10 +
11 + days, remainder = divmod(total_seconds, 86400)
12 + hours, remainder = divmod(remainder, 3600)
13 + minutes, seconds = divmod(remainder, 60)
14 +
15 + days = int(days)
16 + hours = int(hours)
17 + minutes = int(minutes)
18 +
19 + parts = []
20 + if days > 0:
21 + parts.append(f"{days}d")
22 + if hours > 0:
23 + parts.append(f"{hours}h")
24 + if minutes > 0:
25 + parts.append(f"{minutes}m")
26 +
27 + if days > 0 or hours > 0:
28 + if seconds >= 1:
29 + parts.append(f"{int(seconds)}s")
30 + elif minutes > 0:
31 + if seconds >= 0.1:
32 + parts.append(f"{seconds:.1f}s")
33 + else:
34 + parts.append(f"{total_seconds:.1f}s")
35 +
36 + if not parts:
37 + return "0.0s remaining"
38 +
39 + return " ".join(parts) + " remaining"
40 +
41 +
42 +async def managed_wait(agent, target_time, is_duration_wait, log, get_heading_callback):
43 +
44 + while datetime.now(timezone.utc) < target_time:
45 + before_intervention = datetime.now(timezone.utc)
46 + await agent.handle_intervention()
47 + after_intervention = datetime.now(timezone.utc)
48 +
49 + if is_duration_wait:
50 + pause_duration = after_intervention - before_intervention
51 + if pause_duration.total_seconds() > 1.5: # Adjust for pauses longer than the sleep cycle
52 + target_time += pause_duration
53 + PrintStyle.info(
54 + f"Wait extended by {pause_duration.total_seconds():.1f}s to {target_time.isoformat()}...",
55 + )
56 +
57 + current_time = datetime.now(timezone.utc)
58 + if current_time >= target_time:
59 + break
60 +
61 + remaining_seconds = (target_time - current_time).total_seconds()
62 + if log:
63 + log.update(heading=get_heading_callback(format_remaining_time(remaining_seconds)))
64 + sleep_duration = min(1.0, remaining_seconds)
65 +
66 + await asyncio.sleep(sleep_duration)
67 +
68 + return target_time
python/tools/wait.py new
+89
@@ -0,0 +1,89 @@
1 +import asyncio
2 +from datetime import datetime, timedelta, timezone
3 +from python.helpers.tool import Tool, Response
4 +from python.helpers.print_style import PrintStyle
5 +from python.helpers.wait import managed_wait
6 +from python.helpers.localization import Localization
7 +
8 +class WaitTool(Tool):
9 +
10 + async def execute(self, **kwargs) -> Response:
11 + await self.agent.handle_intervention()
12 +
13 + seconds = self.args.get("seconds", 0)
14 + minutes = self.args.get("minutes", 0)
15 + hours = self.args.get("hours", 0)
16 + days = self.args.get("days", 0)
17 + until_timestamp_str = self.args.get("until")
18 +
19 + is_duration_wait = not bool(until_timestamp_str)
20 +
21 + now = datetime.now(timezone.utc)
22 + target_time = None
23 +
24 + if until_timestamp_str:
25 + try:
26 + target_time = Localization.get().localtime_str_to_utc_dt(until_timestamp_str)
27 + if not target_time:
28 + raise ValueError(f"Invalid timestamp format: {until_timestamp_str}")
29 + except ValueError as e:
30 + return Response(
31 + message=str(e),
32 + break_loop=False,
33 + )
34 + else:
35 + wait_duration = timedelta(
36 + days=int(days),
37 + hours=int(hours),
38 + minutes=int(minutes),
39 + seconds=int(seconds),
40 + )
41 + if wait_duration.total_seconds() <= 0:
42 + return Response(
43 + message="Wait duration must be positive.",
44 + break_loop=False,
45 + )
46 + target_time = now + wait_duration
47 +
48 + if target_time <= now:
49 + return Response(
50 + message=f"Target time {target_time.isoformat()} is in the past.",
51 + break_loop=False,
52 + )
53 +
54 + PrintStyle.info(f"Waiting until {target_time.isoformat()}...")
55 +
56 + target_time = await managed_wait(
57 + agent=self.agent,
58 + target_time=target_time,
59 + is_duration_wait=is_duration_wait,
60 + log=self.log,
61 + get_heading_callback=self.get_heading
62 + )
63 +
64 + if self.log:
65 + self.log.update(heading=self.get_heading("Done", done=True))
66 +
67 + message = self.agent.read_prompt(
68 + "fw.wait_complete.md",
69 + target_time=target_time.isoformat()
70 + )
71 +
72 + return Response(
73 + message=message,
74 + break_loop=False,
75 + )
76 +
77 + def get_log_object(self):
78 + return self.agent.context.log.log(
79 + type="progress",
80 + heading=self.get_heading(),
81 + content="",
82 + kvps=self.args,
83 + )
84 +
85 + def get_heading(self, text: str = "", done: bool = False):
86 + done_icon = " icon://done_all" if done else ""
87 + if not text:
88 + text = f"Waiting..."
89 + return f"icon://timer wait: {text}{done_icon}"