From 71316b3090f9d59f62458ea887743f4e2c7b6ed3 Mon Sep 17 00:00:00 2001 From: Hans Heinemann Date: Mon, 16 Mar 2026 00:30:28 -0400 Subject: [PATCH] refactor(team_runner): replace static adapter imports with dynamic importlib loading MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Concrete adapter classes (AnthropicAdapter, GitHubAdapter, etc.) are no longer imported at the top of team_runner.py. Instead, each registry maps short names to 'module.path:ClassName' strings resolved lazily via importlib.import_module at instantiation time. This means: - Adding a new adapter requires only an entry in the registry string dict (or a full dotted path directly in team.yaml) — no changes to TeamRunner. - Third-party / custom adapters work out of the box: set e.g. adapters.llm: mypackage.llm.openai:OpenAIAdapter in team.yaml. - The runner no longer hard-wires knowledge of which concrete classes exist. Addresses tandrewng review comment on PR #1. --- core/team_runner.py | 87 +++++++++++++++++++++++++++++---------------- 1 file changed, 57 insertions(+), 30 deletions(-) diff --git a/core/team_runner.py b/core/team_runner.py index 5109f0a..fdded84 100644 --- a/core/team_runner.py +++ b/core/team_runner.py @@ -25,15 +25,12 @@ from core.blackboard import Blackboard from core.escalation import EscalationHandler from core.task_brief import TaskBrief +import importlib + from adapters.base.llm import LLMAdapter from adapters.base.notify import NotifyAdapter from adapters.base.runtime import RuntimeAdapter from adapters.base.vcs import VCSAdapter -from adapters.llm.anthropic import AnthropicAdapter -from adapters.notify.openclaw import OpenClawNotifyAdapter -from adapters.runtime.claude_code import ClaudeCodeRuntimeAdapter -from adapters.runtime.openclaw import OpenClawRuntimeAdapter -from adapters.vcs.github import GitHubAdapter logger = logging.getLogger(__name__) @@ -59,22 +56,60 @@ _TIER_CAPABILITIES: dict[int, str] = { 5: "fast-cheap", } -# Adapter registry: config key → concrete class. -_LLM_ADAPTERS: dict[str, type[LLMAdapter]] = { - "anthropic": AnthropicAdapter, +# --------------------------------------------------------------------------- +# Adapter registries +# +# Values are "module.path:ClassName" strings resolved lazily via importlib. +# To add a new adapter, append an entry here — no changes to TeamRunner needed. +# team.yaml may also supply a full "module.path:ClassName" value directly, +# enabling third-party adapters without touching this file. +# --------------------------------------------------------------------------- + +_LLM_ADAPTERS: dict[str, str] = { + "anthropic": "adapters.llm.anthropic:AnthropicAdapter", } -_VCS_ADAPTERS: dict[str, type[VCSAdapter]] = { - "github": GitHubAdapter, +_VCS_ADAPTERS: dict[str, str] = { + "github": "adapters.vcs.github:GitHubAdapter", } -_NOTIFY_ADAPTERS: dict[str, type[NotifyAdapter]] = { - "openclaw": OpenClawNotifyAdapter, +_NOTIFY_ADAPTERS: dict[str, str] = { + "openclaw": "adapters.notify.openclaw:OpenClawNotifyAdapter", } -_RUNTIME_ADAPTERS: dict[str, type[RuntimeAdapter]] = { - "openclaw": OpenClawRuntimeAdapter, - "claude_code": ClaudeCodeRuntimeAdapter, +_RUNTIME_ADAPTERS: dict[str, str] = { + "openclaw": "adapters.runtime.openclaw:OpenClawRuntimeAdapter", + "claude_code": "adapters.runtime.claude_code:ClaudeCodeRuntimeAdapter", } +def _load_adapter_class(key: str, registry: dict[str, str], label: str) -> type: + """ + Resolve a short name or dotted "module:ClassName" path to an adapter class. + + Resolution order: + 1. If *key* is in *registry*, use the mapped dotted path. + 2. Otherwise, treat *key* itself as a dotted path (custom / third-party). + """ + dotted = registry.get(key, key) + if ":" not in dotted: + raise ValueError( + f"Unknown {label} adapter {key!r}. " + f"Built-in choices: {list(registry)}. " + f"Or supply a full 'module.path:ClassName' value in team.yaml." + ) + module_path, class_name = dotted.rsplit(":", 1) + try: + module = importlib.import_module(module_path) + except ModuleNotFoundError as exc: + raise ImportError( + f"Cannot import {label} adapter module {module_path!r}: {exc}" + ) from exc + try: + return getattr(module, class_name) + except AttributeError: + raise ImportError( + f"Module {module_path!r} has no class {class_name!r}" + ) + + # --------------------------------------------------------------------------- # Exceptions # --------------------------------------------------------------------------- @@ -163,28 +198,24 @@ class TeamRunner: return fh.read() def _build_llm(self, key: str) -> LLMAdapter: - cls = _LLM_ADAPTERS.get(key) - if cls is None: - raise ValueError( - f"Unknown LLM adapter {key!r}. Known: {list(_LLM_ADAPTERS)}" - ) + cls = _load_adapter_class(key, _LLM_ADAPTERS, "LLM") return cls(self._config) def _build_optional( self, - registry: dict, + registry: dict[str, str], key: Optional[str], label: str, ) -> Optional[object]: """Build an optional adapter, returning None on any init error.""" if not key: return None - cls = registry.get(key) - if cls is None: - logger.warning("Unknown %s adapter %r — skipping.", label, key) - return None try: + cls = _load_adapter_class(key, registry, label) return cls(self._config) + except (ImportError, ValueError) as exc: + logger.warning("Unknown %s adapter %r — skipping. (%s)", label, key, exc) + return None except Exception as exc: logger.warning( "%s adapter %r could not be initialised (%s) — skipping.", @@ -195,11 +226,7 @@ class TeamRunner: return None def _build_runtime(self, key: str) -> RuntimeAdapter: - cls = _RUNTIME_ADAPTERS.get(key) - if cls is None: - raise ValueError( - f"Unknown runtime adapter {key!r}. Known: {list(_RUNTIME_ADAPTERS)}" - ) + cls = _load_adapter_class(key, _RUNTIME_ADAPTERS, "runtime") return cls(self._config) # ------------------------------------------------------------------