Skip to content
Open
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
28 changes: 16 additions & 12 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
# --- Required: exactly one LLM provider key ---
#
# Uncomment ONE of the options below. Leave the others commented out: any
# non-empty ANTHROPIC_API_KEY — including a leftover placeholder — makes
# SWE-AF pick the claude_code runtime, which is exactly what you don't want
# on an OpenRouter-only deployment.
# Uncomment one provider option. When OpenRouter is present, SWE-AF defaults to
# AForge; with only Anthropic credentials it defaults to Claude Code.

# Option A (recommended): OpenRouter — 200+ open and proprietary models
# (DeepSeek, Qwen, Llama, MiniMax, GLM, Kimi, …). This is the only secret
# needed to get started; GH_TOKEN below is optional.
# With ONLY an OpenRouter key set (no ANTHROPIC_API_KEY, no SWE_DEFAULT_RUNTIME),
# SWE-AF auto-selects the open_code runtime and defaults every role to
# With an OpenRouter key set and no SWE_DEFAULT_RUNTIME, SWE-AF auto-selects
# the aforge runtime and defaults every role to
# openrouter/deepseek/deepseek-v4-flash-0731. Override with SWE_DEFAULT_MODEL.
# OPENROUTER_API_KEY=sk-or-v1-...

Expand Down Expand Up @@ -147,18 +145,24 @@

# Default runtime when callers don't pass a `runtime` in the request config.
# Lets the deployer pick the runtime once instead of every caller threading
# a config through. Unset = auto: open_code when an OpenRouter key is the
# 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
# a config through. Unset = auto: aforge when an OpenRouter key is available,
# else claude_code. An invalid value is logged as a warning and ignored.
# SWE_DEFAULT_RUNTIME=claude_code # or: aforge, open_code, codex
#
# AFORGE_BIN / AGENTFIELD_AFORGE_COMMAND are honored on the pinned
# agentfield>=0.1.130 SDK (the release carrying agentfield#905): AFORGE_BIN
# overrides the binary otherwise resolved as `aforge` from PATH, and
# AGENTFIELD_AFORGE_COMMAND picks the headless command (`exec`, the default,
# or `do`).
# AFORGE_BIN=/absolute/path/to/aforge
# AGENTFIELD_AFORGE_COMMAND=exec

# Default model when callers don't pass `models` in the request config.
# Applies to all 16 agent roles for whichever runtime is active. Caller
# config (`models.default` or per-role keys) overrides this. Set this on
# the deployment to pin a model without code changes — e.g. swap from
# deepseek-v4-flash-0731 to a newer release. Empty / unset → use the runtime's
# baked-in defaults (openrouter/deepseek/deepseek-v4-flash-0731 on open_code).
# baked-in defaults (openrouter/deepseek/deepseek-v4-flash-0731 on aforge/open_code).
# This is the variable to use for role model selection; AI_MODEL below is
# part of the same cascade but is also the direct-LLM fallback, so prefer
# this one.
Expand Down
50 changes: 49 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ jobs:
runs-on: ubuntu-latest
env:
# Reproduce go/Dockerfile's sparse SDK clone; keep in sync with its AGENTFIELD_SDK_REF.
AGENTFIELD_SDK_REF: 20955b2637b4708758c328a4f64fe460c7d4b772
AGENTFIELD_SDK_REF: aba20a9b248d9ee6c74f4e7e688ef0740c542dc9
AGENTFIELD_REPO: https://github.com/Agent-Field/agentfield.git
GOWORK: off
steps:
Expand Down Expand Up @@ -72,3 +72,51 @@ jobs:
- name: Test (race)
working-directory: SWE-AF/go
run: go test -race -count=1 ./...

aforge-fetch-stage:
# SWE-AF had no docker job at all. This one builds JUST the `aforge` fetch
# stage of both Dockerfiles — the part that downloads the released AForge
# CLI and checksum-verifies it — so a bad version/URL/checksum fails here
# instead of on a deploy. It deliberately skips the rest of the image
# (apt, npm, the Go build), which is minutes of work for no extra signal.
#
# The download host is probed first: until agentfield.ai serves
# /downloads/aforge/<version>/, the job reports a notice and passes. Flip
# `--target aforge` to a full `docker build .` once the host is live and
# this becomes the image-build gate.
name: AForge fetch stage
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Read the AForge coordinates from the Dockerfile
id: coords
run: |
base="$(awk -F= '/^ARG AFORGE_BASE_URL=/{print $2; exit}' Dockerfile)"
version="$(awk -F= '/^ARG AFORGE_VERSION=/{print $2; exit}' Dockerfile)"
test -n "$base" && test -n "$version"
echo "base=$base" >> "$GITHUB_OUTPUT"
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "AForge $version from $base"

- name: Probe the download host
id: probe
env:
BASE: ${{ steps.coords.outputs.base }}
VERSION: ${{ steps.coords.outputs.version }}
run: |
if curl -fsS --head --max-time 30 "${BASE}/${VERSION}/checksums.txt" >/dev/null 2>&1; then
echo "available=true" >> "$GITHUB_OUTPUT"
else
echo "available=false" >> "$GITHUB_OUTPUT"
echo "::notice::${BASE}/${VERSION}/checksums.txt is not reachable yet — skipping the AForge fetch-stage build."
fi

- name: Build the fetch stage (Python image)
if: steps.probe.outputs.available == 'true'
run: docker build --target aforge -t swe-af-aforge-stage .

- name: Build the fetch stage (Go image)
if: steps.probe.outputs.available == 'true'
run: docker build --target aforge -f go/Dockerfile -t swe-af-go-aforge-stage .
49 changes: 47 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,3 +1,45 @@
# ---------------------------------------------------------------------------
# Stage 1 — aforge: fetch the released AForge CLI from the public download host
# and verify it against the release checksums before it ever enters the image.
#
# The release publishes GZIPPED binaries plus a checksums.txt whose hashes are
# of the DECOMPRESSED binaries, so this stage gunzips first and then rewrites
# the matching checksum line to the local file name before `sha256sum -c`.
#
# Both ARGs are overridable so a mirror / air-gapped registry can be used:
# docker build --build-arg AFORGE_BASE_URL=... --build-arg AFORGE_VERSION=...
# Changing AFORGE_VERSION also busts this layer's cache (per the docker cache
# rule) — a floating URL alone would keep restoring a stale binary.
# ---------------------------------------------------------------------------
ARG AFORGE_BASE_URL=https://agentfield.ai/downloads/aforge
ARG AFORGE_VERSION=v0.1.0

FROM debian:bookworm-slim AS aforge
ARG AFORGE_BASE_URL
ARG AFORGE_VERSION
# TARGETARCH is populated by BuildKit; the fallback keeps the classic builder
# working on the only architecture this image is published for.
ARG TARGETARCH
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates curl && \
rm -rf /var/lib/apt/lists/*
WORKDIR /out
RUN set -eu; \
arch="${TARGETARCH:-amd64}"; \
curl -fsSL "${AFORGE_BASE_URL}/${AFORGE_VERSION}/aforge-linux-${arch}.gz" -o aforge.gz; \
gunzip -c aforge.gz > aforge; \
curl -fsSL "${AFORGE_BASE_URL}/${AFORGE_VERSION}/checksums.txt" -o checksums.txt; \
grep " aforge-linux-${arch}$" checksums.txt | sed 's/ aforge-linux-.*/ aforge/' > aforge.sha256; \
test -s aforge.sha256; \
sha256sum -c aforge.sha256; \
chmod +x aforge; \
./aforge --help > /dev/null; \
rm -f aforge.gz checksums.txt aforge.sha256


# ---------------------------------------------------------------------------
# Stage 2 — runtime: the SWE-AF Python node.
# ---------------------------------------------------------------------------
FROM python:3.12-slim

ENV PYTHONDONTWRITEBYTECODE=1 \
Expand All @@ -8,7 +50,7 @@ WORKDIR /app
# System deps: git (worktrees, branches), curl (healthcheck), jq (agent bash),
# openssh-client (optional SSH git), gh CLI (draft PRs)
RUN apt-get update && apt-get install -y --no-install-recommends \
git curl openssh-client jq nodejs npm && \
git curl ca-certificates openssh-client jq nodejs npm && \
# Install GitHub CLI
curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \
| dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg && \
Expand Down Expand Up @@ -52,6 +94,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \

# Add OpenCode to PATH for non-interactive shells
ENV PATH="/root/.opencode/bin:${PATH}"
# AForge CLI (aforge runtime), fetched + checksum-verified in stage 1.
COPY --from=aforge /out/aforge /usr/local/bin/aforge

# Tell OpenCode to read its model AND small_model from the deployer's
# HARNESS_MODEL env var via {env:...} interpolation. Without this config,
Expand Down Expand Up @@ -99,7 +143,8 @@ EXPOSE 8003

ENV PORT=8003 \
AGENTFIELD_SERVER=http://control-plane:8080 \
NODE_ID=swe-planner
NODE_ID=swe-planner \
AGENTFIELD_AFORGE_COMMAND=exec

HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD curl -f http://localhost:${PORT}/health || exit 1
Expand Down
41 changes: 39 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ New to AgentField? Install the control plane first with `curl -fsSL https://agen

One click deploys SWE-AF + AgentField control plane + PostgreSQL. Exactly **one** environment variable is required in Railway — an LLM provider key:

- `OPENROUTER_API_KEY` — **recommended, simplest**. One key, 200+ open and proprietary models. With only this set (no `ANTHROPIC_API_KEY`, no `SWE_DEFAULT_RUNTIME`), SWE-AF auto-selects the `open_code` runtime and defaults every role to `openrouter/deepseek/deepseek-v4-flash-0731` — no further configuration needed.
- `OPENROUTER_API_KEY` — **recommended, simplest**. One key, 200+ open and proprietary models. When present and no runtime is explicitly selected, SWE-AF uses AForge `exec` and defaults every role to `openrouter/deepseek/deepseek-v4-flash-0731`.
- *Alternative:* `ANTHROPIC_API_KEY`, or `CLAUDE_CODE_OAUTH_TOKEN` from `claude setup-token` in [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code) (uses Pro/Max subscription credits), to run the `claude_code` runtime instead.

Optional:
Expand Down Expand Up @@ -279,6 +279,43 @@ python -m pip install -e ".[dev]"

### 3. Run

#### Harness selection

The Docker image ships AForge: a dedicated build stage downloads the released
binary from `https://agentfield.ai/downloads/aforge/<version>/` and verifies it
against the release `checksums.txt` before it enters the image. Both
coordinates are build args, so a mirror or a different release can be
substituted without editing the Dockerfile:

```bash
docker build \
--build-arg AFORGE_BASE_URL=https://agentfield.ai/downloads/aforge \
--build-arg AFORGE_VERSION=v0.1.0 \
-t swe-af .
```

`AFORGE_VERSION` is part of that layer's cache key — bumping it is what pulls a
newer AForge; a floating URL alone would keep restoring the cached binary.

A host installation needs `aforge` on `PATH` instead:

```bash
export OPENROUTER_API_KEY=sk-or-v1-...
export SWE_DEFAULT_RUNTIME=aforge
export SWE_DEFAULT_MODEL=openrouter/deepseek/deepseek-v4-flash-0731
python -m swe_af
```

Set `SWE_DEFAULT_RUNTIME=open_code` for an OpenCode rollback (OpenCode stays
installed in the image), or `claude_code` for Claude.

> `AFORGE_BIN` and `AGENTFIELD_AFORGE_COMMAND` are honored on the pinned
> `agentfield>=0.1.130` SDK, which carries
> [agentfield#905](https://github.com/Agent-Field/agentfield/pull/905):
> `AFORGE_BIN` overrides the binary otherwise resolved as `aforge` from `PATH`,
> and `AGENTFIELD_AFORGE_COMMAND` picks the headless command (`exec`, the
> default, or `do`) — `aforge exec --json -w <root> --timeout <seconds>`.

```bash
af # starts AgentField control plane on :8080
python -m swe_af # registers node id "swe-planner"
Expand Down Expand Up @@ -844,7 +881,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` | auto | Model runtime: `"aforge"`, `"claude_code"`, `"open_code"`, or `"codex"`. With OpenRouter available the default is `"aforge"`; otherwise it is `"claude_code"`. `SWE_DEFAULT_RUNTIME` overrides it deployment-wide. |
| `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
9 changes: 6 additions & 3 deletions agentfield-package.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ agent_node:

user_environment:
require_one_of:
# SWE-AF runs on Claude-compatible APIs or open models via OpenCode.
# Provide one. With only an OpenRouter key it auto-selects the open_code
# SWE-AF runs on Claude-compatible APIs or open models via AForge.
# Provide one. With an OpenRouter key it auto-selects the aforge
# runtime and defaults to openrouter/deepseek/deepseek-v4-flash-0731.
- id: llm_provider
description: an LLM provider key
Expand Down Expand Up @@ -61,7 +61,10 @@ 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: AGENTFIELD_AFORGE_COMMAND
description: AForge headless command
default: exec
- name: SWE_DEFAULT_MODEL
description: Override the model id for every role (e.g. openrouter/deepseek/deepseek-v4-flash-0731)
- name: ANTHROPIC_BASE_URL
Expand Down
2 changes: 2 additions & 0 deletions docker-compose.go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ services:
# else claude_code. A baked claude_code fallback here would break
# OpenRouter-only deployments.
- SWE_DEFAULT_RUNTIME=${SWE_DEFAULT_RUNTIME:-}
- AGENTFIELD_AFORGE_COMMAND=${AGENTFIELD_AFORGE_COMMAND:-exec}
- SWE_DEFAULT_MODEL=${SWE_DEFAULT_MODEL:-}
- SWE_CODEX_AUTH_MODE=${SWE_CODEX_AUTH_MODE:-auto}
- OPENAI_API_KEY=${OPENAI_API_KEY:-}
Expand Down Expand Up @@ -111,6 +112,7 @@ services:
- OPENCODE_MODEL=${OPENCODE_MODEL:-}
# Empty = auto (see swe-agent-go note).
- SWE_DEFAULT_RUNTIME=${SWE_DEFAULT_RUNTIME:-}
- AGENTFIELD_AFORGE_COMMAND=${AGENTFIELD_AFORGE_COMMAND:-exec}
- SWE_DEFAULT_MODEL=${SWE_DEFAULT_MODEL:-}
- SWE_CODEX_AUTH_MODE=${SWE_CODEX_AUTH_MODE:-auto}
# build-db lives in the Python stack; reachable over the shared network.
Expand Down
9 changes: 4 additions & 5 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,9 @@ services:
- NODE_ID=swe-planner
- PORT=8003
- AGENT_CALLBACK_URL=http://swe-agent:8003
# Empty = auto: open_code when only an OpenRouter key is present,
# else claude_code. A baked claude_code fallback here would break
# OpenRouter-only deployments.
# Empty = auto: aforge when OpenRouter is available, else claude_code.
- SWE_DEFAULT_RUNTIME=${SWE_DEFAULT_RUNTIME:-}
- AGENTFIELD_AFORGE_COMMAND=${AGENTFIELD_AFORGE_COMMAND:-exec}
- SWE_DEFAULT_MODEL=${SWE_DEFAULT_MODEL:-}
- SWE_CODEX_AUTH_MODE=${SWE_CODEX_AUTH_MODE:-auto}
# Provider keys and the GitHub token, so exporting them in the shell
Expand Down Expand Up @@ -85,9 +84,9 @@ services:
- OPENAI_API_KEY=${OPENAI_API_KEY:-}
- GOOGLE_API_KEY=${GOOGLE_API_KEY:-}
- OPENCODE_MODEL=${OPENCODE_MODEL:-}
# Empty = auto: open_code when only an OpenRouter key is present,
# else claude_code (see swe-agent note).
# Empty = auto: aforge when OpenRouter is available, else claude_code.
- SWE_DEFAULT_RUNTIME=${SWE_DEFAULT_RUNTIME:-}
- AGENTFIELD_AFORGE_COMMAND=${AGENTFIELD_AFORGE_COMMAND:-exec}
- SWE_DEFAULT_MODEL=${SWE_DEFAULT_MODEL:-}
- SWE_CODEX_AUTH_MODE=${SWE_CODEX_AUTH_MODE:-auto}
- DATABASE_URL_TEST=${DATABASE_URL_TEST:-postgres://builder:builder@build-db:5432/buildtest}
Expand Down
Loading
Loading