Skip to content

feat(deadline): bound LLM calls and retries by the run's execution deadline (PC-4871) - #124

Draft
vldcmp-uipath wants to merge 1 commit into
mainfrom
feat/pc-4871-execution-deadline
Draft

feat(deadline): bound LLM calls and retries by the run's execution deadline (PC-4871)#124
vldcmp-uipath wants to merge 1 commit into
mainfrom
feat/pc-4871-execution-deadline

Conversation

@vldcmp-uipath

@vldcmp-uipath vldcmp-uipath commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Problem — PC-4871

Serverless agent runs are force-killed (SIGTERM) by the control plane after a 15-minute execution window. A single LLM call plus its automatic retries can outlast that window:

  • the request timeout is a fixed 895s (client-side httpx timeout + the static X-UiPath-LLMGateway-TimeoutSeconds: 895 header), leaving no room inside the window;
  • the retry loop stops after N attempts (stop_after_attempt), not after total elapsed time — 5 attempts × up to 895s each plus backoff waits;
  • LLM Gateway additionally retries against other regions, extending the total further.

The process dies mid-call: no logs are flushed and LLM spans never record a timeout, making these failures nearly impossible to diagnose.

What this PR does

Adds an execution-deadline mechanism to the shared transports (uipath.llm_client.utils.deadline). The host runtime declares the run's hard deadline once at startup:

from uipath.llm_client import set_execution_deadline

set_execution_deadline(15 * 60 - 10)  # window minus graceful-shutdown buffer

When a deadline is set, every request through RetryableHTTPTransport / RetryableAsyncHTTPTransport is clamped per attempt:

  1. Client-side timeout of each attempt (initial + retries) is capped to the remaining budget — this is the enforcement nothing upstream can override, and what guarantees the run fails cleanly inside its window.
  2. X-UiPath-LLMGateway-TimeoutSeconds is lowered to the remaining budget (never raised above its configured value) so the gateway can give up when the client does.
  3. The retry loop stops at the deadline: stop_when_deadline_exhausted is OR-ed with the existing attempt-count stop, and backoff sleeps (including Retry-After hints) are shortened so a retry never sleeps through the deadline.
  4. Fail fast: with under 5s of budget left, the call raises the new UiPathExecutionDeadlineError (EXECUTION_DEADLINE_EXCEEDED) instead of starting a doomed attempt — so the run ends with a clear, traceable error.

When no deadline is set (the default), behaviour is byte-for-byte unchanged — verified by the existing retry suite.

What this PR deliberately does NOT do (follow-ups)

  • Wire the deadline in the agents runtime (uipath-agents-python): set set_execution_deadline() at run start from the serverless execution window. The window is not currently discoverable at runtime (no env var / context field), so this needs an env var contract with the serverless control plane (e.g. UIPATH_EXECUTION_TIME_LIMIT_SECONDS) — opt-in, so robot/long-running/local runs are unaffected.
  • AgentHub pass-through (AgentHubService): the AgentHub LLM proxy currently ignores the caller's timeout header and forwards its own static config (900s) to LLM Gateway (LlmGatewayClient.CreateRequestAsync). Until AgentHub forwards min(caller value, configured value), item (2) above only reaches LLM Gateway on the direct-LLMGW path; on the AgentHub path the server-side hint stays 900s and enforcement is purely client-side (which still fixes the SIGTERM symptom).
  • LLM Gateway semantics verification: confirm with the LLM GW team whether X-UiPath-LlmGateway-TimeoutSeconds caps the total processing time including cross-region retries, or per-attempt only.

Known limitation

For streaming responses the httpx read timeout applies per chunk, so a slowly-trickling stream can still outlast the deadline. Streaming callers need their own total-elapsed cutoff (documented in the module docstring).

Test plan

  • tests/core/features/test_deadline.py (new, 19 tests): ContextVar helpers, fail-fast without touching the transport (sync/async/retries-disabled), header + timeout clamping (including "never raise the hint"), retry loop stopping at the deadline, wait capping (backoff and Retry-After), and no-deadline behaviour unchanged.
  • Full tests/core unit suite: 493 passed (the 42 errors are pre-existing integration tests requiring live LLMGW_* credentials).
  • ruff check / ruff format / pyright clean.

🤖 Generated with Claude Code

…adline (PC-4871)

Serverless agent runs are force-killed by the control plane after a fixed
execution window (15 minutes today). A single LLM call plus its automatic
retries could outlast that window: the request timeout was a fixed 895s, the
retry loop was bounded by attempt count rather than elapsed time, and the
process died mid-call with no logs flushed and no timeout recorded on spans.

The host runtime can now declare the run's hard deadline once at startup via
set_execution_deadline(seconds_from_now). When set, the shared retryable
transports enforce it per attempt:

- cap the client-side httpx timeout of every attempt to the remaining budget
- lower the X-UiPath-LLMGateway-TimeoutSeconds header to the remaining budget
  (never raising it above its configured value)
- stop the retry loop at the deadline (stop_when_deadline_exhausted, OR-ed
  with the attempt-count stop) and shorten backoff sleeps so retries never
  sleep through the deadline
- fail fast with UiPathExecutionDeadlineError when under 5s of budget remains

When no deadline is set, behaviour is unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant