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}"