Skip to content
Closed
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
3 changes: 2 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 23 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 |
Expand Down
2 changes: 1 addition & 1 deletion agentfield-package.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion requirements-docker.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 9 additions & 6 deletions swe_af/execution/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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]


Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions swe_af/fast/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"


Expand Down
3 changes: 2 additions & 1 deletion swe_af/fast/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -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().
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion swe_af/issue/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 7 additions & 1 deletion swe_af/runtime/providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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}")
Expand All @@ -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"


Expand All @@ -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"
13 changes: 13 additions & 0 deletions tests/test_model_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions tests/test_runtime_provider_routing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading