-
Notifications
You must be signed in to change notification settings - Fork 162
feat(shell): adaptive timeouts and a wait-for-condition primitive #3382
Description
Waiting is where the agent most often fails. The shell tool defaults to a 30-second timeout, far below what a test suite, a build or an install needs, so long commands get killed rather than finished.
The default is the defect, not the ceiling: timeout is already a declared parameter on the tool, so the model can raise it — it rarely does, because nothing tells it how long the command it is about to run should take. A default that is wrong for the common case, with no signal to correct it, behaves like a hard limit.
There is also no way to wait for a condition. An agent that needs to wait for a server to come up, or a CI run to finish, has only one option: sleep in a loop and re-check. That is what a user sees as the agent spinning and doing nothing, and it burns steps against the loop's cap for no work.
Acceptance criteria
- Timeouts adapt to a named, enumerated set of command classes with a stated default per class — e.g. test runners, builds/installs, VCS and network calls, everything else. A contributor must be able to check the table off.
- A wait-for-condition primitive exists: a predicate plus a deadline, with a stated poll interval and a bounded maximum wait, so sleeping in a loop is unnecessary.
- Tests cover each timeout class and the wait primitive's deadline expiry, and assert the three already-true behaviours below have not regressed.
Already true — must not regress
Three things already work and a contributor should not rebuild them: the applied timeout is already returned in the result, a timed-out command is already distinguishable via a dedicated flag, and partial stdout/stderr is already captured and returned on timeout.
🔍 Technical details
run_shell_command in src/gaia/agents/tools/shell_tools.py takes timeout: int = 30, passed straight to subprocess.run. The agent-level tool timeout is separate and much larger, which means a shell command can be killed at 30s inside a step that had minutes of budget.
The TimeoutExpired handler already captures exc.stdout / exc.stderr and returns them with timed_out: True — that behaviour is good and is the thing to build on, not replace.
The wait primitive should not be built out of sleep internally either; a deadline plus a predicate check is what is wanted, so that the step cost is one call rather than one call per poll.