Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions agent/oneshot.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
import logging
from typing import Any, Callable, Dict, Optional, Tuple

from agent.auxiliary_client import call_llm, extract_content_or_reasoning
from agent.auxiliary_client import call_llm, extract_content_or_reasoning, _get_task_timeout

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -112,7 +112,7 @@ def run_oneshot(
task: str = "title_generation",
max_tokens: int = 1024,
temperature: Optional[float] = 0.3,
timeout: float = 60.0,
timeout: Optional[float] = None,
main_runtime: Optional[Dict[str, Any]] = None,
) -> str:
"""Run a single stateless LLM request and return its text.
Expand All @@ -135,12 +135,22 @@ def run_oneshot(
messages.append({"role": "system", "content": instructions})
messages.append({"role": "user", "content": user_input or ""})

# Resolve the per-task timeout the same way call_llm does, but keep this
# helper's historical 60s default for callers that neither pass a timeout nor
# configure ``auxiliary.<task>.timeout``. Previously the hard-coded 60.0 default
# was always forwarded, so call_llm never saw ``None`` and silently ignored a
# configured ``auxiliary.<task>.timeout`` on the live llm.oneshot path (the exact
# class of #32729 that #56322 fixed for generate_title).
effective_timeout = (
timeout if timeout is not None else _get_task_timeout(task, default=60.0)
)

response = call_llm(
task=task,
messages=messages,
max_tokens=max_tokens,
temperature=temperature,
timeout=timeout,
timeout=effective_timeout,
main_runtime=main_runtime,
)

Expand Down
40 changes: 40 additions & 0 deletions tests/agent/test_oneshot.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,46 @@ def test_strips_wrapping_code_fence(self):
):
assert run_oneshot(instructions="x", user_input="y") == "fix: bug"

def test_no_timeout_honors_configured_task_timeout(self):
# Regression for the residual of #32729/#56322 on the oneshot path: when the
# caller (e.g. the llm.oneshot RPC) passes no timeout, the configured
# auxiliary.<task>.timeout must reach call_llm instead of a hard-coded default.
with patch(
"agent.oneshot.call_llm",
return_value=self._mock_response("ok"),
) as llm, patch(
"agent.oneshot._get_task_timeout", return_value=90.0
) as get_timeout:
run_oneshot(instructions="x", user_input="y", task="my_task")

get_timeout.assert_called_once_with("my_task", default=60.0)
assert llm.call_args.kwargs["timeout"] == 90.0

def test_no_timeout_falls_back_to_60_when_unconfigured(self):
# With no explicit timeout and no auxiliary.<task>.timeout configured, the
# historical 60s oneshot default is preserved (no behavior change for
# unconfigured callers) -- _get_task_timeout returns its supplied default.
with patch(
"agent.oneshot.call_llm",
return_value=self._mock_response("ok"),
) as llm, patch(
"agent.oneshot._get_task_timeout",
side_effect=lambda task, default: default,
):
run_oneshot(instructions="x", user_input="y")

assert llm.call_args.kwargs["timeout"] == 60.0

def test_explicit_timeout_is_forwarded_and_skips_resolution(self):
with patch(
"agent.oneshot.call_llm",
return_value=self._mock_response("ok"),
) as llm, patch("agent.oneshot._get_task_timeout") as get_timeout:
run_oneshot(instructions="x", user_input="y", timeout=5.0)

get_timeout.assert_not_called()
assert llm.call_args.kwargs["timeout"] == 5.0


class TestHelpers:
def test_truncate_under_limit_unchanged(self):
Expand Down
Loading