diff --git a/.env.example b/.env.example index 0f5ac5d1..3a33cc96 100644 --- a/.env.example +++ b/.env.example @@ -128,7 +128,8 @@ # only provider credential, else claude_code. An invalid value is logged as # a warning and ignored. Leave this UNSET on an OpenRouter-only deployment — # auto-select already picks open_code and the deepseek-v4-flash-0731 default. -# SWE_DEFAULT_RUNTIME=claude_code # or: open_code, codex +# SWE_DEFAULT_RUNTIME=claude_code # or: aforge, open_code, codex +# AFORGE_BIN=/absolute/path/to/aforge # optional; otherwise aforge must be on PATH # Default model when callers don't pass `models` in the request config. # Applies to all 16 agent roles for whichever runtime is active. Caller diff --git a/README.md b/README.md index 8579a70a..60691cef 100644 --- a/README.md +++ b/README.md @@ -279,6 +279,28 @@ python -m pip install -e ".[dev]" ### 3. Run +#### Benchmark with unreleased aforge-v2 + +Build aforge-v2 once, point AgentField at it, and select the runtime. No SWE-AF +code or prompt changes are required: + +```bash +cd /path/to/aforge-v2 +go build -o /absolute/path/to/bin/aforge ./cmd/aforge + +export OPENROUTER_API_KEY=sk-or-v1-... +export AFORGE_BIN=/absolute/path/to/bin/aforge +export SWE_DEFAULT_RUNTIME=aforge +export SWE_DEFAULT_MODEL=openrouter/deepseek/deepseek-v4-flash-0731 +python -m swe_af +``` + +The draft pins the exact AgentField harness commit used for these benchmarks. +Replace that source pin with the released SDK version before merging. The +existing Docker image does not bundle the unreleased aforge-v2 binary; add it +to the container or run the node on the host until Aforge publishes an +installable artifact. + ```bash af # starts AgentField control plane on :8080 python -m swe_af # registers node id "swe-planner" @@ -820,7 +842,7 @@ Pass `config` to `build` or `execute`. Full schema: [`swe_af/execution/schemas.p | Key | Default | Description | | ------------------------- | --------------- | ----------------------------------------------------- | -| `runtime` | `"claude_code"` | Model runtime: `"claude_code"`, `"open_code"`, or `"codex"`. The default also honors the `SWE_DEFAULT_RUNTIME` env var when no `runtime` is passed in `config` — set it on the deployment so callers don't need to plumb a config through. | +| `runtime` | `"claude_code"` | Model runtime: `"aforge"`, `"claude_code"`, `"open_code"`, or `"codex"`. The default also honors the `SWE_DEFAULT_RUNTIME` env var when no `runtime` is passed in `config` — set it on the deployment so callers don't need to plumb a config through. | | `models` | `null` | Flat role-model map (`default` + role keys below). Without a caller-supplied value, the `SWE_DEFAULT_MODEL` env var is used as the default for all roles — set it on the deployment to pin a model without code changes. Caller `models.default` or per-role keys still win. | | `max_coding_iterations` | `5` | Inner-loop retry budget | | `max_advisor_invocations` | `2` | Middle-loop advisor budget | diff --git a/agentfield-package.yaml b/agentfield-package.yaml index 4e0c239e..288ee310 100644 --- a/agentfield-package.yaml +++ b/agentfield-package.yaml @@ -53,7 +53,7 @@ user_environment: type: secret scope: global - name: SWE_DEFAULT_RUNTIME - description: Coding runtime for every role (claude_code | open_code | codex) + description: Coding runtime for every role (aforge | claude_code | open_code | codex) - name: SWE_DEFAULT_MODEL description: Override the model id for every role (e.g. openrouter/deepseek/deepseek-v4-flash-0731) - name: AGENTFIELD_SERVER diff --git a/pyproject.toml b/pyproject.toml index 24046524..122b08e9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ requires-python = ">=3.12" dependencies = [ # >=0.1.96 ships ReasonerFailed, which build() raises so an empty build # reports `failed` (not `succeeded`) with its result preserved (#82 Gap 2). - "agentfield>=0.1.113", + "agentfield @ git+https://github.com/Agent-Field/agentfield.git@60b0aa76df604575429af60fb0ccffc9aaca7745#subdirectory=sdk/python", "pydantic>=2.0", # Compatibility pin: newer SDK builds have surfaced # "Unknown message type: rate_limit_event" during streaming. diff --git a/requirements-docker.txt b/requirements-docker.txt index 1c5ff7db..000f33cb 100644 --- a/requirements-docker.txt +++ b/requirements-docker.txt @@ -2,7 +2,7 @@ # # Same runtime dependencies as requirements.txt. -agentfield>=0.1.111 +agentfield @ git+https://github.com/Agent-Field/agentfield.git@60b0aa76df604575429af60fb0ccffc9aaca7745#subdirectory=sdk/python pydantic>=2.0 claude-agent-sdk==0.1.20 hax-sdk>=0.2.4 diff --git a/requirements.txt b/requirements.txt index 53da52c2..e69bd93e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ # # Install: python -m pip install -r requirements.txt -agentfield>=0.1.113 +agentfield @ git+https://github.com/Agent-Field/agentfield.git@60b0aa76df604575429af60fb0ccffc9aaca7745#subdirectory=sdk/python pydantic>=2.0 claude-agent-sdk==0.1.20 hax-sdk>=0.2.4 diff --git a/swe_af/execution/schemas.py b/swe_af/execution/schemas.py index 532c056b..60ed0666 100644 --- a/swe_af/execution/schemas.py +++ b/swe_af/execution/schemas.py @@ -616,6 +616,9 @@ class QASynthesisResult(BaseModel): _OPENROUTER_AUTO_DEFAULT_MODEL = "openrouter/deepseek/deepseek-v4-flash-0731" _RUNTIME_BASE_MODELS: dict[str, dict[str, str]] = { + "aforge": { + **{field: _OPENROUTER_AUTO_DEFAULT_MODEL for field in ALL_MODEL_FIELDS}, + }, "claude_code": { **{field: "sonnet" for field in ALL_MODEL_FIELDS}, "qa_synthesizer_model": "haiku", @@ -656,7 +659,7 @@ def _codex_default_model() -> str: return _CODEX_CHATGPT_MODEL if _codex_uses_chatgpt_auth() else _CODEX_API_KEY_MODEL -def _runtime_to_provider(runtime: str) -> Literal["claude", "opencode", "codex"]: +def _runtime_to_provider(runtime: str) -> Literal["aforge", "claude", "opencode", "codex"]: return runtime_to_harness_provider(runtime) # type: ignore[return-value] @@ -677,7 +680,7 @@ def _openrouter_only_env() -> bool: return bool(os.getenv("OPENROUTER_API_KEY", "").strip()) -def _default_runtime() -> Literal["claude_code", "open_code", "codex"]: +def _default_runtime() -> Literal["aforge", "claude_code", "open_code", "codex"]: """Default runtime, honoring the ``SWE_DEFAULT_RUNTIME`` env var. Lets the deployer pick the runtime without every caller having to pass @@ -930,7 +933,7 @@ class BuildConfig(BaseModel): model_config = ConfigDict(extra="forbid") - runtime: Literal["claude_code", "open_code", "codex"] = Field(default_factory=_default_runtime) + runtime: Literal["aforge", "claude_code", "open_code", "codex"] = Field(default_factory=_default_runtime) models: dict[str, str] | None = None max_review_iterations: int = 2 @@ -1049,7 +1052,7 @@ def model_post_init(self, __context: Any) -> None: _validate_flat_models(self.models) @property - def ai_provider(self) -> Literal["claude", "opencode", "codex"]: + def ai_provider(self) -> Literal["aforge", "claude", "opencode", "codex"]: return _runtime_to_provider(self.runtime) @property @@ -1239,7 +1242,7 @@ class ExecutionConfig(BaseModel): model_config = ConfigDict(extra="forbid") - runtime: Literal["claude_code", "open_code", "codex"] = Field(default_factory=_default_runtime) + runtime: Literal["aforge", "claude_code", "open_code", "codex"] = Field(default_factory=_default_runtime) models: dict[str, str] | None = None _resolved_models: dict[str, str] = PrivateAttr(default_factory=dict) @@ -1291,7 +1294,7 @@ def _model_for(self, field_name: str) -> str: return self._resolved_models[field_name] @property - def ai_provider(self) -> Literal["claude", "opencode", "codex"]: + def ai_provider(self) -> Literal["aforge", "claude", "opencode", "codex"]: return _runtime_to_provider(self.runtime) @property diff --git a/swe_af/fast/app.py b/swe_af/fast/app.py index c91adc97..e4823113 100644 --- a/swe_af/fast/app.py +++ b/swe_af/fast/app.py @@ -58,6 +58,8 @@ def _runtime_to_provider(runtime: str) -> str: return "claude" if runtime == "codex": return "codex" + if runtime == "aforge": + return "aforge" return "opencode" diff --git a/swe_af/fast/schemas.py b/swe_af/fast/schemas.py index 8f0f1b6c..ad392baf 100644 --- a/swe_af/fast/schemas.py +++ b/swe_af/fast/schemas.py @@ -20,6 +20,7 @@ _OPEN_CODE_DEFAULT = "openrouter/deepseek/deepseek-v4-flash-0731" _RUNTIME_DEFAULTS: dict[str, str] = { + "aforge": _OPEN_CODE_DEFAULT, "claude_code": _CLAUDE_CODE_DEFAULT, "open_code": _OPEN_CODE_DEFAULT, # codex is resolved dynamically (auth-mode dependent); see _runtime_default(). @@ -126,7 +127,7 @@ class FastBuildConfig(BaseModel): model_config = ConfigDict(extra="forbid") - runtime: Literal["claude_code", "open_code", "codex"] = Field(default_factory=_default_fast_runtime) + runtime: Literal["aforge", "claude_code", "open_code", "codex"] = Field(default_factory=_default_fast_runtime) models: dict[str, str] | None = None max_tasks: int = 10 task_timeout_seconds: int = 300 diff --git a/swe_af/issue/schemas.py b/swe_af/issue/schemas.py index c93f5ac9..2f5831ff 100644 --- a/swe_af/issue/schemas.py +++ b/swe_af/issue/schemas.py @@ -106,7 +106,7 @@ class IssueBuildConfig(BaseModel): model_config = ConfigDict(extra="forbid") - runtime: Literal["claude_code", "open_code", "codex"] = Field( + runtime: Literal["aforge", "claude_code", "open_code", "codex"] = Field( default_factory=_default_runtime ) models: dict[str, str] | None = None diff --git a/swe_af/runtime/providers.py b/swe_af/runtime/providers.py index 8784253e..f6009bb5 100644 --- a/swe_af/runtime/providers.py +++ b/swe_af/runtime/providers.py @@ -2,7 +2,7 @@ from __future__ import annotations -RUNTIME_VALUES = ("claude_code", "open_code", "codex") +RUNTIME_VALUES = ("aforge", "claude_code", "open_code", "codex") def normalize_runtime_provider(runtime: str) -> str: @@ -12,6 +12,8 @@ def normalize_runtime_provider(runtime: str) -> str: return "claude_code" if value in {"open_code", "opencode"}: return "open_code" + if value in {"aforge", "aforge_v2", "aforge-v2"}: + return "aforge" if value == "codex": return "codex" raise ValueError(f"Unsupported runtime provider: {runtime}") @@ -24,6 +26,8 @@ def runtime_to_harness_provider(runtime: str) -> str: return "claude" if normalized == "open_code": return "opencode" + if normalized == "aforge": + return "aforge" return "codex" @@ -34,4 +38,6 @@ def runtime_to_harness_adapter(runtime: str) -> str: return "claude-code" if normalized == "open_code": return "opencode" + if normalized == "aforge": + return "aforge" return "codex" diff --git a/tests/test_model_config.py b/tests/test_model_config.py index a7a38967..8a539a51 100644 --- a/tests/test_model_config.py +++ b/tests/test_model_config.py @@ -89,6 +89,11 @@ def test_open_code_defaults(self) -> None: for field in ALL_MODEL_FIELDS: self.assertEqual(resolved[field], "openrouter/deepseek/deepseek-v4-flash-0731") + def test_aforge_defaults(self) -> None: + resolved = resolve_runtime_models(runtime="aforge", models=None) + for field in ALL_MODEL_FIELDS: + self.assertEqual(resolved[field], "openrouter/deepseek/deepseek-v4-flash-0731") + def test_models_default_applies_to_all(self) -> None: resolved = resolve_runtime_models( runtime="claude_code", @@ -127,6 +132,14 @@ def test_open_code_runtime_provider(self) -> None: resolved = cfg.resolved_models() self.assertEqual(resolved["coder_model"], "openrouter/deepseek/deepseek-v4-flash-0731") + def test_aforge_runtime_provider(self) -> None: + cfg = BuildConfig(runtime="aforge") + self.assertEqual(cfg.ai_provider, "aforge") + self.assertEqual( + cfg.resolved_models()["coder_model"], + "openrouter/deepseek/deepseek-v4-flash-0731", + ) + class TestOpenRouterAutoSelection(unittest.TestCase): """When only an OpenRouter key is present (no explicit runtime), SWE-AF diff --git a/tests/test_runtime_provider_routing.py b/tests/test_runtime_provider_routing.py index a6185096..82eebaf2 100644 --- a/tests/test_runtime_provider_routing.py +++ b/tests/test_runtime_provider_routing.py @@ -7,6 +7,11 @@ def test_runtime_to_harness_adapter_supports_codex() -> None: assert runtime_to_harness_adapter("codex") == "codex" +def test_runtime_to_harness_adapter_supports_aforge_aliases() -> None: + assert runtime_to_harness_adapter("aforge") == "aforge" + assert runtime_to_harness_adapter("aforge-v2") == "aforge" + + def test_execution_agents_source_uses_shared_runtime_adapter() -> None: import inspect import swe_af.reasoners.execution_agents as execution_agents