Skip to content

Commit 512216f

Browse files
committed
fix: add minimum budget guard to prevent wasted LLM calls near timeout
When earlier pipeline stages consume most of the timeout budget, later stages and ReAct loop steps start with insufficient time, virtually guaranteeing a mid-call timeout that still bills a full LLM request. Changes: - Runner: skip step when remaining budget < 8s (step > 0 only) - Orchestrator: skip stage when remaining budget < 15s (index > 0 only) - First step/stage always runs regardless of budget size, so small overall timeouts still work for simple pipelines and tests
1 parent 706879f commit 512216f

2 files changed

Lines changed: 43 additions & 4 deletions

File tree

src/agent/orchestrator.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -307,11 +307,28 @@ def _execute_pipeline(
307307
specialist_agents_inserted = False
308308
index = 0
309309

310+
# Minimum seconds required for a stage to do useful work. Starting
311+
# a stage with less budget virtually guarantees a timeout that wastes
312+
# an LLM billing cycle. Only enforced after at least one stage has
313+
# completed so that the first stage always gets a chance to run
314+
# even when the total budget is small.
315+
_MIN_STAGE_BUDGET_S = 15
316+
310317
while index < len(agents):
311318
agent = agents[index]
312319
elapsed_s = time.time() - t0
313-
if timeout_s and elapsed_s >= timeout_s:
314-
logger.error("[Orchestrator] pipeline timed out before stage '%s'", agent.agent_name)
320+
remaining_budget = timeout_s - elapsed_s if timeout_s else None
321+
budget_exhausted = (
322+
timeout_s
323+
and remaining_budget is not None
324+
and (
325+
remaining_budget <= 0
326+
or (index > 0 and remaining_budget < _MIN_STAGE_BUDGET_S)
327+
)
328+
)
329+
if budget_exhausted:
330+
reason = "timed out" if remaining_budget <= 0 else f"insufficient budget ({remaining_budget:.1f}s < {_MIN_STAGE_BUDGET_S}s)"
331+
logger.error("[Orchestrator] pipeline %s before stage '%s'", reason, agent.agent_name)
315332
if progress_callback:
316333
progress_callback({
317334
"type": "pipeline_timeout",

src/agent/runner.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -370,10 +370,32 @@ def run_agent_loop(
370370
provider_used = ""
371371
models_used: List[str] = []
372372

373+
# Minimum seconds needed for a meaningful LLM round-trip. If the
374+
# remaining budget is positive but below this threshold, the step will
375+
# almost certainly timeout mid-call, wasting a billed request. Only
376+
# enforced from step 2 onwards so the first step always gets a chance
377+
# even when the total budget is small.
378+
_MIN_STEP_BUDGET_S = 8.0
379+
373380
for step in range(max_steps):
374381
remaining_timeout = _remaining_timeout_seconds(start_time, max_wall_clock_seconds)
375-
if remaining_timeout is not None and remaining_timeout <= 0:
376-
logger.warning("Agent timed out before step %d", step + 1)
382+
budget_exhausted = (
383+
remaining_timeout is not None
384+
and (
385+
remaining_timeout <= 0
386+
or (step > 0 and remaining_timeout <= _MIN_STEP_BUDGET_S)
387+
)
388+
)
389+
if budget_exhausted:
390+
if remaining_timeout <= 0:
391+
logger.warning("Agent timed out before step %d", step + 1)
392+
else:
393+
logger.warning(
394+
"Agent budget too low for step %d (%.1fs remaining, min %.1fs) — treating as timeout",
395+
step + 1,
396+
remaining_timeout,
397+
_MIN_STEP_BUDGET_S,
398+
)
377399
return _build_timeout_result(
378400
start_time=start_time,
379401
max_wall_clock_seconds=float(max_wall_clock_seconds),

0 commit comments

Comments
 (0)