feat(tools): add WaitTool for pausing on long-running jobs - #6690
Conversation
Agents that kick off out-of-band work (a sandbox build, a deployment, an async API job) have no way to let clock time pass: they either poll in a tight loop or give up before the work finishes. WaitTool pauses for a given number of seconds, with an optional reason echoed back for traces. A single call waits at most max_seconds (default 300, configurable). Longer requests are clamped to the cap and the result says so, so the model calls again rather than failing. Sync and async execution are both implemented; stdlib only, no new dependencies. The tool description spells out when to reach for it (builds, deploys, batch jobs, async polling, backoff) and when not to, so models pick it up for the right reason. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds ChangesWaitTool feature
Sequence Diagram(s)sequenceDiagram
participant Agent
participant WaitTool
participant Sleep as time.sleep_or_asyncio.sleep
Agent->>WaitTool: invoke with seconds and optional reason
WaitTool->>WaitTool: validate and clamp seconds
WaitTool->>Sleep: wait for effective duration
Sleep-->>WaitTool: wait completed
WaitTool-->>Agent: return formatted result
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
lib/crewai-tools/tests/tools/wait_tool_test.py (1)
85-89: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAlso test the
crewai_tools.toolsexport.This test verifies only
from crewai_tools import WaitTool; add the correspondingcrewai_tools.toolsimport assertion so that both public export paths are protected.Suggested test extension
def test_exported_from_package(): from crewai_tools import WaitTool as ExportedWaitTool + from crewai_tools.tools import WaitTool as ToolsExportedWaitTool assert ExportedWaitTool is WaitTool + assert ToolsExportedWaitTool is WaitToolBased on the PR objective, both package exports are part of the public contract.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/crewai-tools/tests/tools/wait_tool_test.py` around lines 85 - 89, Extend test_exported_from_package to also import WaitTool from crewai_tools.tools and assert it is the same object as WaitTool, preserving the existing top-level export assertion.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/edge/en/tools/automation/waittool.mdx`:
- Around line 43-53: Update the example around the Agent tools list to define or
import check_build_status_tool before it is passed to Agent, or explicitly mark
it as an application-provided placeholder so the snippet is copyable without an
undefined symbol.
- Around line 100-107: Update the Async Support example around wait_tool.arun to
show await inside an async context: wrap the call in an async function and
invoke it with an async runner, or explicitly state that the snippet requires an
async notebook/runtime.
In `@lib/crewai-tools/src/crewai_tools/tools/wait_tool/README.md`:
- Around line 30-35: Update the Agent usage example to eliminate the undefined
check_build_status_tool reference: either add its valid definition/import within
the example or remove it from the tools list and use only the defined wait_tool,
keeping the snippet directly runnable.
---
Nitpick comments:
In `@lib/crewai-tools/tests/tools/wait_tool_test.py`:
- Around line 85-89: Extend test_exported_from_package to also import WaitTool
from crewai_tools.tools and assert it is the same object as WaitTool, preserving
the existing top-level export assertion.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 8882d403-1006-450a-bf47-d3f3e1b24959
📒 Files selected for processing (10)
docs/docs.jsondocs/edge/en/tools/automation/overview.mdxdocs/edge/en/tools/automation/waittool.mdxlib/crewai-tools/src/crewai_tools/__init__.pylib/crewai-tools/src/crewai_tools/tools/__init__.pylib/crewai-tools/src/crewai_tools/tools/wait_tool/README.mdlib/crewai-tools/src/crewai_tools/tools/wait_tool/__init__.pylib/crewai-tools/src/crewai_tools/tools/wait_tool/wait_tool.pylib/crewai-tools/tests/tools/wait_tool_test.pylib/crewai-tools/tool.specs.json
There was a problem hiding this comment.
Pull request overview
Adds a new WaitTool to crewai-tools to let agents pause for real clock time between checks of long-running out-of-band work (builds, deploys, async jobs, backoff), with a per-call cap and both sync/async implementations.
Changes:
- Introduces
WaitTool(synctime.sleep/ asyncasyncio.sleep) withseconds+ optionalreason, clamping waits tomax_seconds. - Exposes the tool via package exports and registers it in
tool.specs.json. - Adds unit tests and documentation (tool page, automation overview card, docs nav entry).
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| lib/crewai-tools/tool.specs.json | Registers WaitTool in generated tool specs for platform sync. |
| lib/crewai-tools/tests/tools/wait_tool_test.py | Adds coverage for clamping, reason echo, async behavior, and exports. |
| lib/crewai-tools/src/crewai_tools/tools/wait_tool/wait_tool.py | Implements WaitTool, schemas, clamping, and result formatting. |
| lib/crewai-tools/src/crewai_tools/tools/wait_tool/README.md | Documents WaitTool usage and parameters in tool README. |
| lib/crewai-tools/src/crewai_tools/tools/wait_tool/init.py | Exports WaitTool / WaitToolSchema from the tool package. |
| lib/crewai-tools/src/crewai_tools/tools/init.py | Adds WaitTool import and export in tools module. |
| lib/crewai-tools/src/crewai_tools/init.py | Re-exports WaitTool at package top-level. |
| docs/edge/en/tools/automation/waittool.mdx | Adds end-user documentation page for WaitTool. |
| docs/edge/en/tools/automation/overview.mdx | Adds WaitTool card to Automation overview page. |
| docs/docs.json | Adds WaitTool to docs navigation under Automation. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Workflows to automatically generate PRs for you. |
…ippets BaseTool.run() skips args_schema validation when called with positional arguments, so tool.run(-5) reached time.sleep(-5) and failed with an unrelated error. _resolve_duration now enforces the seconds >= 0 contract itself, covered for both run() and arun(). Docs and README examples are now self-contained: check_build_status_tool is defined with the @tool decorator instead of referenced out of nowhere, and the async example awaits inside asyncio.run() rather than at top level. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Review comments addressed in 438a89c. Copilot — negative CodeRabbit — undefined CodeRabbit — top-level CodeRabbit nitpick — also assert the CodeRabbit pre-merge — docstring coverage 29.41%: not addressed deliberately. The shortfall is test functions; neighbouring suites in Suite is 12 passing; |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (2)
lib/crewai-tools/src/crewai_tools/tools/wait_tool/wait_tool.py:139
BaseTool.run()skipsargs_schemavalidation when called with positional args, so_run()can be invoked with non-float values (e.g.tool.run("5")ortool.run("abc")). Right now that can lead toTypeErrorinside_resolve_duration/_format_resultrather than a consistentValueError, and it also bypasses the coercion you get with keyword args.
Coerce seconds to float inside _run() and pass the coerced value through to _resolve_duration and _format_result so positional calls behave consistently with keyword calls.
def _run(self, seconds: float, reason: str | None = None) -> str:
"""Block for ``seconds``, capped at ``max_seconds``.
Args:
seconds: How many seconds to wait.
reason: Optional note on what is being waited for.
Returns:
A summary of how long was waited and whether the request was capped.
"""
waited, _ = self._resolve_duration(seconds)
time.sleep(waited)
return self._format_result(waited, seconds, reason)
lib/crewai-tools/src/crewai_tools/tools/wait_tool/wait_tool.py:153
- Same positional-args issue exists for async execution:
BaseTool.arun()skipsargs_schemavalidation when positional args are used, so_arun()should coerce/validatesecondsbefore calling_resolve_durationand formatting the result. Otherwise positional calls likeawait tool.arun("5")can raiseTypeErrorunexpectedly.
async def _arun(self, seconds: float, reason: str | None = None) -> str:
"""Await for ``seconds``, capped at ``max_seconds``, without blocking the loop.
Args:
seconds: How many seconds to wait.
reason: Optional note on what is being waited for.
Returns:
A summary of how long was waited and whether the request was capped.
"""
waited, _ = self._resolve_duration(seconds)
await asyncio.sleep(waited)
return self._format_result(waited, seconds, reason)
Unprefixed links resolve against the default docs version (v1.15.7), where the wait tool page does not exist, so the card 404'd in the broken link check. Prefixing with /edge matches how other edge pages link. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
There are 2 total unresolved issues (including 1 from previous review).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 6205516. Configure here.
Two issues from review, both confirmed against the code. Waits inherited the default cache_function, which always allows caching. With crew cache enabled, a repeat call with the same arguments returned "Waited N seconds." straight from the cache without sleeping, turning a poll-wait-check loop into a busy loop. WaitTool now declares a cache_function that always refuses. The description advertising the cap was only rebuilt when max_seconds reached __init__ without an explicit description. Passing both (as a platform building from tool.specs.json init params would), calling model_validate, or assigning max_seconds left the text claiming 300 seconds while clamping to something else. A model_validator now derives the description from max_seconds on construction, validation, and assignment, and leaves a caller-supplied description untouched. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
🧹 Nitpick comments (1)
lib/crewai-tools/tests/tools/wait_tool_test.py (1)
4-7: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueKeep the test on the public contract.
DEFAULT_MAX_SECONDSand_build_descriptionare implementation details. Obtain the baseline description throughWaitTool().descriptioninstead. As per coding guidelines, “Write unit tests for new functionality that focus on behavior rather than implementation details.”Proposed change
-from crewai_tools.tools.wait_tool.wait_tool import ( - DEFAULT_MAX_SECONDS, - _build_description, -) - ... - tool = build(_build_description(DEFAULT_MAX_SECONDS)) + tool = build(WaitTool().description)Also applies to: 117-117
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/crewai-tools/tests/tools/wait_tool_test.py` around lines 4 - 7, Update the wait tool test imports and setup to stop importing the implementation details DEFAULT_MAX_SECONDS and _build_description; obtain the baseline description from WaitTool().description instead, while keeping assertions focused on the public behavior of WaitTool.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@lib/crewai-tools/tests/tools/wait_tool_test.py`:
- Around line 4-7: Update the wait tool test imports and setup to stop importing
the implementation details DEFAULT_MAX_SECONDS and _build_description; obtain
the baseline description from WaitTool().description instead, while keeping
assertions focused on the public behavior of WaitTool.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 4bc4a60b-c68a-479a-b640-daaac52e877b
📒 Files selected for processing (3)
lib/crewai-tools/src/crewai_tools/tools/wait_tool/wait_tool.pylib/crewai-tools/tests/tools/wait_tool_test.pylib/crewai-tools/tool.specs.json
🚧 Files skipped from review as they are similar to previous changes (2)
- lib/crewai-tools/tool.specs.json
- lib/crewai-tools/src/crewai_tools/tools/wait_tool/wait_tool.py
_resolve_duration now rejects NaN with its own message instead of letting time.sleep raise "Invalid value NaN (not a number)" from a positional call. Infinity keeps clamping to the cap like any other oversized wait. Result and description text no longer says "1 seconds". Tests use the public WaitTool().description as the baseline rather than reaching for module-private helpers. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Latest round addressed in ff94734. Copilot — NaN reaching Copilot — "Waited 1 seconds": valid, fixed. A CodeRabbit — tests importing Copilot — 23 tests passing; ruff, mypy, and |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (1)
lib/crewai-tools/src/crewai_tools/tools/wait_tool/wait_tool.py:232
BaseTool.run()/arun()skipargs_schemavalidation whenever positional args are used (seelib/crewai/src/crewai/tools/base_tool.py:318-320). Ifsecondsis passed positionally as a non-numeric value (e.g. a string),_resolve_duration()(viamath.isnan) and_format_result()comparisons can raiseTypeErrorinstead of a clearValueError. Coercingsecondstofloatin_run/_arunkeeps positional calls aligned with Pydantic’s coercion behavior for kwargs and ensures consistent error messages.
waited, _ = self._resolve_duration(seconds)
time.sleep(waited)
return self._format_result(waited, seconds, reason)
async def _arun(self, seconds: float, reason: str | None = None) -> str:
"""Await for ``seconds``, capped at ``max_seconds``, without blocking the loop.
Args:
seconds: How many seconds to wait.
reason: Optional note on what is being waited for.
Returns:
A summary of how long was waited and whether the request was capped.
"""
waited, _ = self._resolve_duration(seconds)
await asyncio.sleep(waited)
return self._format_result(waited, seconds, reason)
alex-clawd
left a comment
There was a problem hiding this comment.
Approved.
Genuinely useful gap to close — an agent that starts out-of-band work currently has no way to let clock time pass, so it either busy-polls or gives up early.
Three things I'd have worried about, all handled:
cache_function returns False. This is the one that would have quietly ruined the tool. A cached hit hands back "Waited 300 seconds." without waiting, turning the poll-wait-check loop it exists to support into a busy loop that looks like it's working. Easy to miss and miserable to debug.
_resolve_duration doesn't trust the schema. BaseTool.run skips args_schema validation for positional args, so bounds are enforced in the method too — with tests that specifically call positionally. NaN and infinity are handled explicitly rather than left to time.sleep to reject in its own way.
The advertised cap can't drift from the enforced one. _sync_description_with_cap runs on construction and assignment, and _is_generated_description means a caller-authored description is left alone rather than clobbered. Telling the model a cap that isn't the real one is the sort of thing that surfaces as a confusing loop much later.
On clamping rather than erroring: agree with the call. A model asking for an hour and getting a hard failure learns to avoid the tool; getting 5 minutes plus "call again if more waiting is needed" teaches the right loop. The result string carries that instruction, which is what makes it work.
Also good that reason is echoed back — a trace showing "Reason: sandbox build running" beats a bare sleep when someone is working out where 5 minutes went.
Ran the tests locally: 23 passed. CI 27 pass / 1 skipping.

Why
Agents that kick off out-of-band work — a sandbox build, a deployment, an async API job — have no way to let clock time pass. Today they either poll in a tight loop or give up before the work finishes. LLMs are bad at waiting because nothing in the toolset lets them.
What
WaitToolincrewai-tools. Stdlib only, no API key, no new dependencies.seconds(float,>= 0) and an optionalreason, echoed back so traces show what was being waited on instead of a bare sleep.max_seconds, default300._run(time.sleep) and async_arun(asyncio.sleep).Design notes
The cap clamps instead of erroring. A model asking for an hour gets 5 minutes plus an instruction to call again. A hard failure seemed likely to train worse behavior. Trade-off: an agent stuck polling burns a call every 5 minutes rather than getting a clear stop signal. There is no cumulative wait budget across calls — that would need per-instance state and felt out of scope here.
The description is written for tool selection. It lists when to reach for the tool (builds, deploys, batch jobs, async polling, rate-limit backoff) and explicitly when not to (pacing a conversation, faking progress, when the answer is already in hand).
Also included
crewai_toolsandcrewai_tools.toolstool.specs.json(+88 lines, WaitTool entry only) so the platform sync picks it uplib/crewai-tools/tests/tools/wait_tool_test.pydocs/edge/en/tools/automation/waittool.mdx, plus nav entry and overview cardNot included
edgeonly. Thear,ko, andpt-BRlocales and versioned snapshots are untouched, assuming translations sync separately. Happy to add them if this PR should carry all four.Testing
10 passedfor the new suite;test_generate_tool_specsandtest_import_without_warningsalso pass.ruff check,ruff format, andmypyclean on the new files.🤖 Generated with Claude Code
Note
Low Risk
Additive tool with no auth or data changes; main runtime effect is blocking sleep up to the configured cap per invocation.
Overview
Adds
WaitTooltocrewai-toolsso agents can pause between status checks on out-of-band work (builds, deploys, async jobs) instead of tight polling or giving up early.The tool accepts
seconds(≥ 0) and an optionalreasonechoed in the result, usesmax_seconds(default 300) to clamp longer waits and tell the model to call again, implements synctime.sleepand asyncasyncio.sleep, disables tool result caching so waits cannot be skipped, and keeps the LLM-facing description in sync with the cap. It is exported from the package, covered by tests, documented under edge automation, and registered intool.specs.json.Reviewed by Cursor Bugbot for commit ff94734. Bugbot is set up for automated code reviews on this repo. Configure here.