diff --git a/src/ai_client.py b/src/ai_client.py index 0e3e7a7a..e2e08039 100644 --- a/src/ai_client.py +++ b/src/ai_client.py @@ -648,7 +648,13 @@ def _gemini_tool_declaration() -> Optional[types.Tool]: """ [C: tests/test_tool_access_exclusion.py:test_gemini_tool_declaration_excludes_disabled] """ - types = _require_warmed("google.genai.types") + # Note: We look up the PARENT package `google.genai` and access `.types` + # as an attribute, not `_require_warmed("google.genai.types")` directly. + # The latter triggers a latent circular-import bug in google-genai's + # __init__.py chain in fresh pytest processes. Using the parent + # completes the chain once, then `.types` is just an attribute access. + genai = _require_warmed("google.genai") + types = genai.types raw_tools: list[dict[str, Any]] = [] for spec in mcp_client.get_tool_schemas(): if _agent_tools.get(spec["name"], True): @@ -1167,7 +1173,8 @@ def _send_anthropic(md_content: str, user_message: str, base_dir: str, file_item [C: src/ai_server.py:_handle_send] """ anthropic = _require_warmed("anthropic") - types = _require_warmed("google.genai.types") + genai = _require_warmed("google.genai") + types = genai.types monitor = performance_monitor.get_monitor() if monitor.enabled: monitor.start_component("ai_client._send_anthropic") try: @@ -1419,7 +1426,8 @@ def _send_gemini(md_content: str, user_message: str, base_dir: str, [C: src/ai_server.py:_handle_send, tests/test_tier4_interceptor.py:test_gemini_provider_passes_qa_callback_to_run_script] """ global _gemini_chat, _gemini_cache, _gemini_cache_md_hash, _gemini_cache_created_at, _gemini_cached_file_paths - types = _require_warmed("google.genai.types") + genai = _require_warmed("google.genai") + types = genai.types monitor = performance_monitor.get_monitor() if monitor.enabled: monitor.start_component("ai_client._send_gemini") try: @@ -2357,7 +2365,8 @@ def _send_minimax(md_content: str, user_message: str, base_dir: str, def run_tier4_analysis(stderr: str) -> str: """ """ - types = _require_warmed("google.genai.types") + genai = _require_warmed("google.genai") + types = genai.types if not stderr or not stderr.strip(): return "" try: @@ -2407,7 +2416,8 @@ def run_tier4_patch_generation(error: str, file_context: str) -> str: """ [C: src/gui_2.py:App.request_patch_from_tier4, tests/test_tier4_patch_generation.py:test_run_tier4_patch_generation_calls_ai, tests/test_tier4_patch_generation.py:test_run_tier4_patch_generation_empty_error, tests/test_tier4_patch_generation.py:test_run_tier4_patch_generation_returns_diff] """ - types = _require_warmed("google.genai.types") + genai = _require_warmed("google.genai") + types = genai.types if not error or not error.strip(): return "" try: @@ -2565,7 +2575,8 @@ def run_subagent_summarization(file_path: str, content: str, is_code: bool, outl [C: src/summarize.py:summarise_file, tests/test_subagent_summarization.py:test_run_subagent_summarization_anthropic, tests/test_subagent_summarization.py:test_run_subagent_summarization_gemini] """ requests = _require_warmed("requests") - types = _require_warmed("google.genai.types") + genai = _require_warmed("google.genai") + types = genai.types prompt_tmpl = mma_prompts.TIER4_SUMMARIZE_CODE_PROMPT if is_code else mma_prompts.TIER4_SUMMARIZE_TEXT_PROMPT prompt = prompt_tmpl.format(file_path=file_path, outline=outline, content=content) if _provider == "gemini": @@ -2613,7 +2624,8 @@ def run_subagent_summarization(file_path: str, content: str, is_code: bool, outl return "ERROR: Unsupported provider for sub-agent summarization" def run_discussion_compression(discussion_text: str) -> str: - types = _require_warmed("google.genai.types") + genai = _require_warmed("google.genai") + types = genai.types requests = _require_warmed("requests") # Robustly identify the provider string (handles case and whitespace) p = str(get_provider()).lower().strip() diff --git a/src/module_loader.py b/src/module_loader.py index 5094111e..f8839031 100644 --- a/src/module_loader.py +++ b/src/module_loader.py @@ -47,22 +47,13 @@ def _require_warmed(name: str) -> Any: import cost is paid during startup on a bg thread, NOT on the first user-triggered AI call. - For dotted names (e.g. 'google.genai.types'), the parent packages - are pre-imported first. This avoids circular-import errors in libraries - like google-genai where importing a sub-module triggers a chain that - re-enters the same sub-module (e.g. google.genai._api_client does - 'from .types import HttpOptions' during its own initialization). - Without parent pre-import, that relative import would see a - 'partially initialized' google.genai.types in sys.modules and raise - ImportError. + Callers should pass the TOP-LEVEL package name (e.g. 'google.genai'), + not a leaf sub-module (e.g. 'google.genai.types'). The library's + __init__.py chain has a circular-import bug when a sub-module is + loaded before its parent is fully initialized. The ai_client code + follows this convention: `_require_warmed("google.genai").types`. """ mod = sys.modules.get(name) if mod is not None: return mod - if "." in name: - parts = name.split(".") - for i in range(1, len(parts)): - parent = ".".join(parts[:i]) - if parent not in sys.modules: - importlib.import_module(parent) return importlib.import_module(name)