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
7 changes: 5 additions & 2 deletions strix/interface/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -433,10 +433,13 @@ def main() -> None:

from strix.llm.warmup import start_import_warmup

start_import_warmup()

# parse_arguments() first so --version/--help/argparse errors exit before
# the (heavier) import warm-up runs. Warm-up then overlaps with the I/O
# startup below (update check, Docker checks, image pull).
args = parse_arguments()

start_import_warmup()

start_background_check()
if not args.non_interactive and prompt_update_if_available(Console()):
if is_binary_install() and sys.platform != "win32":
Expand Down
19 changes: 19 additions & 0 deletions strix/llm/warmup.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,24 @@ def _warm(modules: tuple[str, ...]) -> None:
logger.debug("Import warm-up for %r failed", name, exc_info=True)


def _preimport_thread_unsafe_sdk() -> None:
"""Import the ``agents`` SDK once, on the caller's thread, before the daemon
warm-up starts.

``openai-agents`` has an internal import cycle (``agents.agent_output`` <->
``agents.agent``) that is not thread-safe: when two threads import the
``agents`` package concurrently, CPython's deadlock-avoidance can hand one
of them a partially initialized module, raising ``ImportError: cannot
import name 'AgentOutputSchemaBase' ... (circular import)``. Fully importing
the package single-threaded here closes that race window before the daemon
warm-up thread (and the main thread's later report import) touch it.
"""
try:
importlib.import_module("agents")
except Exception: # noqa: BLE001 - a failed warm-up must never fail the run.
logger.debug("Pre-import of agents SDK failed", exc_info=True)


def start_import_warmup(modules: tuple[str, ...] = WARMUP_MODULES) -> threading.Thread:
"""Start importing the heavy scan dependencies in the background, once.

Expand All @@ -48,6 +66,7 @@ def start_import_warmup(modules: tuple[str, ...] = WARMUP_MODULES) -> threading.
with _lock:
if _thread is not None:
return _thread
_preimport_thread_unsafe_sdk()
_thread = threading.Thread(
target=_warm, args=(modules,), name="strix-import-warmup", daemon=True
)
Expand Down
49 changes: 49 additions & 0 deletions tests/test_warmup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
from __future__ import annotations

import subprocess
import sys
import textwrap
from pathlib import Path


PROJECT_ROOT = Path(__file__).resolve().parents[1]


def test_start_import_warmup_preimports_agents_sdk_synchronously() -> None:
"""``start_import_warmup`` must import the thread-unsafe ``agents`` SDK on
the caller's thread before spawning the daemon warm-up thread.

``openai-agents`` has an internal import cycle that is not thread-safe: if
the warm-up thread and the main thread import the ``agents`` package
concurrently, CPython can hand one of them a partially initialized module
(``ImportError: cannot import name 'AgentOutputSchemaBase' ... circular
import``). Warming ``agents`` synchronously closes that race window.

Run in a fresh interpreter with an empty ``modules`` set so the daemon
thread warms nothing: ``agents`` can then only be in ``sys.modules`` because
the synchronous pre-import ran.
"""
child = textwrap.dedent(
"""
import sys

assert "agents" not in sys.modules
from strix.llm.warmup import start_import_warmup

# Importing the warm-up module alone must not pull the agents SDK.
assert "agents" not in sys.modules
start_import_warmup(modules=())
assert "agents" in sys.modules, "agents SDK was not pre-imported synchronously"
print("OK")
"""
)
result = subprocess.run( # noqa: S603
[sys.executable, "-c", child],
cwd=PROJECT_ROOT,
check=False,
capture_output=True,
text=True,
)

assert result.returncode == 0, result.stdout + result.stderr
assert "OK" in result.stdout