Skip to content
Open
14 changes: 14 additions & 0 deletions Lightweight.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,20 @@ ENV UV_HTTP_TIMEOUT=300
RUN uv pip install --system torch==2.2.0 --index-url https://download.pytorch.org/whl/cpu
RUN uv pip install --system -r requirements.txt

# Aider has a sprawling dep graph (litellm, prompt-toolkit, gitpython, …)
# that pins different versions of packages we already use. Install it in
# a dedicated venv so its deps don't fight ours, then symlink the binary
# onto PATH so `subprocess.run(["aider", ...])` in src/seer/automation/harness/
# Just Works. Only loaded when AUTOFIX_HARNESS=aider; the default
# AUTOFIX_HARNESS=builtin path never touches this venv.
RUN python -m venv /opt/aider-venv \
&& /opt/aider-venv/bin/pip install --no-cache-dir \
aider-chat==0.65.0 \
"google-auth>=2.29" \
"google-cloud-aiplatform>=1.60" \
&& ln -s /opt/aider-venv/bin/aider /usr/local/bin/aider \
&& aider --version

# Copy model files (assuming they are in the 'models' directory)
COPY models/ models/
# Copy scripts
Expand Down
11 changes: 9 additions & 2 deletions src/seer/automation/autofix/components/coding/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@
from seer.automation.agent.agent import AgentConfig, RunConfig
from seer.automation.agent.client import GeminiProvider, LlmClient
from seer.automation.agent.models import Message, ToolCall
from seer.automation.autofix.autofix_agent import AutofixAgent
from seer.automation.autofix.autofix_agent import AutofixAgent # noqa: F401
from seer.automation.autofix.autofix_context import AutofixContext
from seer.automation.autofix.components.coding.models import CodingOutput, CodingRequest
from seer.automation.autofix.components.coding.prompts import CodingPrompts
from seer.automation.autofix.components.root_cause.models import RootCauseAnalysisItem
from seer.automation.autofix.prompts import format_repo_prompt
from seer.automation.autofix.tools.tools import BaseTools
from seer.automation.component import BaseComponent
from seer.automation.harness import select_orchestrator
from seer.configuration import AppConfig
from seer.dependency_injection import inject, injected

Expand Down Expand Up @@ -141,7 +142,13 @@ def invoke(self, request: CodingRequest) -> None:
if not memory:
memory = self._prefill_initial_memory(request=request)

agent = AutofixAgent(
# AUTOFIX_HARNESS=builtin (default) keeps AutofixAgent; "aider"
# delegates to the external coding harness. See harness/aider.py.
app_config = self._get_app_config()
agent_cls = select_orchestrator(
app_config.AUTOFIX_HARNESS, strict=app_config.AUTOFIX_HARNESS_STRICT
)
agent = agent_cls(
tools=tools.get_tools(include_claude_tools=True),
config=AgentConfig(interactive=True),
memory=memory,
Expand Down
11 changes: 9 additions & 2 deletions src/seer/automation/autofix/components/root_cause/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from seer.automation.agent.agent import AgentConfig, RunConfig
from seer.automation.agent.client import GeminiProvider, LlmClient
from seer.automation.autofix.autofix_agent import AutofixAgent
from seer.automation.autofix.autofix_agent import AutofixAgent # noqa: F401
from seer.automation.autofix.autofix_context import AutofixContext
from seer.automation.autofix.components.root_cause.models import (
MultipleRootCauseAnalysisOutputPrompt,
Expand All @@ -16,6 +16,7 @@
from seer.automation.autofix.prompts import format_repo_prompt
from seer.automation.autofix.tools.tools import BaseTools
from seer.automation.component import BaseComponent
from seer.automation.harness import select_orchestrator
from seer.configuration import AppConfig
from seer.dependency_injection import inject, injected

Expand All @@ -42,7 +43,13 @@ def invoke(

sentry_sdk.set_tag("is_rethinking", len(request.initial_memory) > 0)

agent = AutofixAgent(
# AUTOFIX_HARNESS=builtin (default) keeps AutofixAgent; "aider"
# delegates to the external coding harness — see harness/aider.py
# and docs/coding-harnesses.md.
agent_cls = select_orchestrator(
config.AUTOFIX_HARNESS, strict=config.AUTOFIX_HARNESS_STRICT
)
agent = agent_cls(
tools=(tools.get_tools(can_access_repos=bool(readable_repos))),
config=AgentConfig(
interactive=True,
Expand Down
10 changes: 8 additions & 2 deletions src/seer/automation/autofix/components/solution/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@
from seer.automation.agent.agent import AgentConfig, RunConfig
from seer.automation.agent.client import GeminiProvider, LlmClient
from seer.automation.agent.models import Message, ToolCall
from seer.automation.autofix.autofix_agent import AutofixAgent
from seer.automation.autofix.autofix_agent import AutofixAgent # noqa: F401
from seer.automation.autofix.autofix_context import AutofixContext
from seer.automation.autofix.components.root_cause.models import RootCauseAnalysisItem
from seer.automation.autofix.components.solution.models import SolutionOutput, SolutionRequest
from seer.automation.autofix.components.solution.prompts import SolutionPrompts
from seer.automation.autofix.prompts import format_repo_prompt
from seer.automation.autofix.tools.tools import BaseTools
from seer.automation.component import BaseComponent
from seer.automation.harness import select_orchestrator
from seer.configuration import AppConfig
from seer.dependency_injection import inject, injected

Expand Down Expand Up @@ -123,7 +124,12 @@ def invoke(
has_tools = bool(readable_repos)
repos_str = format_repo_prompt(readable_repos, unreadable_repos)

agent = AutofixAgent(
# AUTOFIX_HARNESS=builtin (default) keeps AutofixAgent; "aider"
# delegates to the external coding harness. See harness/aider.py.
agent_cls = select_orchestrator(
config.AUTOFIX_HARNESS, strict=config.AUTOFIX_HARNESS_STRICT
)
agent = agent_cls(
tools=tools.get_tools() if has_tools else None,
config=AgentConfig(interactive=True),
memory=memory,
Expand Down
94 changes: 94 additions & 0 deletions src/seer/automation/harness/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
"""Pluggable autofix orchestrators.

The default orchestrator is the in-process ``AutofixAgent`` (a ReAct loop
calling Gemini via the existing ``LlmClient``). Alternate orchestrators
("harnesses") wrap external coding agents like ``aider`` as a subprocess
and let them propose a patch given the same prompt + repo context.

Selection happens at autofix-component construction time via
``select_orchestrator(name, strict=...)``. Harness modules register
themselves at import time with ``register_harness("name", Cls)``.

See ``docs/coding-harnesses.md`` for the rollout plan.
"""

import logging
from typing import Protocol, Type, runtime_checkable

from seer.automation.agent.agent import RunConfig
from seer.automation.autofix.autofix_agent import AutofixAgent

logger = logging.getLogger(__name__)


@runtime_checkable
class AutofixOrchestrator(Protocol):
"""Anything a component can invoke to drive an autofix step.

Must support ``.run(run_config=RunConfig(...))`` and return whatever
``AutofixAgent.run`` returns today (a response object with ``.message``,
``.usage``, ``.metadata`` — see ``automation.agent.models``).
"""

def run(self, run_config: RunConfig): ...


class HarnessNotAvailableError(RuntimeError):
"""Raised when ``AUTOFIX_HARNESS_STRICT=True`` and the requested
harness is unknown or unimportable. Operators using strict mode want
a hard failure rather than a silent fallback.
"""


_REGISTRY: dict[str, Type[AutofixOrchestrator]] = {
"builtin": AutofixAgent,
}


def register_harness(name: str, cls: Type[AutofixOrchestrator]) -> None:
"""Used by harness modules at import time to register themselves.

Re-registering the same ``cls`` under the same ``name`` is a no-op
(pytest / module reloads commonly re-import). Registering a *different*
class under an existing name raises ``ValueError`` so two modules
can't silently clobber each other.
"""
existing = _REGISTRY.get(name)
if existing is cls:
return
if existing is not None:
raise ValueError(
f"Harness '{name}' is already registered as "
f"{existing.__name__}; remove the duplicate registration."
)
_REGISTRY[name] = cls


def select_orchestrator(harness_name: str, strict: bool = False) -> Type[AutofixOrchestrator]:
"""Resolve ``AppConfig.AUTOFIX_HARNESS`` to an orchestrator class.

When the requested harness isn't registered:
* ``strict=False`` (default): log a warning and return the builtin.
* ``strict=True``: raise ``HarnessNotAvailableError``.

The fallback prevents a typo or a missing optional dep from disabling
autofix outright.
"""
if harness_name in _REGISTRY:
return _REGISTRY[harness_name]
msg = f"Unknown AUTOFIX_HARNESS={harness_name!r}; " f"known: {sorted(_REGISTRY)}"
if strict:
raise HarnessNotAvailableError(msg)
logger.warning("%s — falling back to 'builtin'.", msg)
return _REGISTRY["builtin"]


# Eagerly import optional harness modules so their import-time
# register_harness() side-effects run. Wrapped in a try/except so a
# missing optional dep (e.g. aider-chat not pip-installed) doesn't
# break `from seer.automation.harness import select_orchestrator`
# for the builtin path.
try:
from . import aider as _aider # noqa: F401, E402
except ImportError as exc: # pragma: no cover
logger.debug("Optional aider harness not importable: %s", exc)
Loading
Loading