Skip to content

feat: make AForge exec the default runtime - #137

Open
AbirAbbas wants to merge 10 commits into
mainfrom
agent/aforge-exec-default
Open

feat: make AForge exec the default runtime#137
AbirAbbas wants to merge 10 commits into
mainfrom
agent/aforge-exec-default

Conversation

@AbirAbbas

@AbirAbbas AbirAbbas commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator

Status update — agentfield v0.1.130 is released; this PR is ready

Everything below the line was written against the 0.1.129 pin and is kept for history. What is true on the current head:

  • Pins bumped: Python agentfield>=0.1.130 (pyproject, requirements.txt, requirements-docker.txt); Go sdk/go v0.1.130 (go/go.mod/go.sum), go/Dockerfile + .github/workflows/ci.yml + go/Makefile AGENTFIELD_SDK_REF → the v0.1.130 commit. This bump was required, not optional: the Go node's new aforge default was unusable on the July SDK pin (unknown harness provider: "aforge"). The ~25-release SDK jump needed zero source changes.
  • "Known gaps" resolved: 0.1.130 reads AGENTFIELD_AFORGE_COMMAND (exec default), honours AFORGE_BIN, and passes --timeout/--turns. README/.env.example/go README wording updated. sdk/go/vX.Y.Z tags now exist (release workflow publishes them).
  • Live verification (real OpenRouter key, ANTHROPIC_API_KEY unset, no runtime override): both images built with default args from agentfield.ai + PyPI (aforge v0.1.0, agentfield 0.1.130; go version -m on the Go binary shows sdk/go v0.1.130); all four nodes registered on a fresh agentfield/control-plane:latest. Go swe-planner-go.implement_issue on a tiny repo → success: true, correct add() + passing pytest, 2 commits, 60 s; Python swe-fast.buildsuccess: true, verification 3/3, 333 s. .aforge/trace/1.trace.log present in both worktrees; live argv aforge exec --json … --timeout 1795 --turns … --context-fill 60 --completion-reserve 65536. SWE_DEFAULT_RUNTIME=claude_code flips runtime/harness provider (Py + Go tests). CI (make check 1211 passed; Go build/vet/test -race) green on the pushed head.

What & why

Makes AForge the default SWE runtime, and — new in this revision — makes the PR
actually installable and buildable.

Three things in the original revision could not work:

  1. The image did not exist. The Dockerfile did
    FROM ghcr.io/agent-field/aforge-v2:chat-v2-exec. The aforge-v2 source repo
    is private and no such image is published, so the build could never succeed
    for anyone.
  2. The dependency pin was not installable. All three dependency files
    pointed at agentfield @ git+...@bfd34426#subdirectory=sdk/python — a commit
    that exists only on the draft agentfield#905 branch, on no published index.
  3. Only half the product moved. go/ was untouched, so the Go
    implementation of swe-planner still resolved OpenRouter-only installs to
    open_code while the shared README and manifests said aforge.

Distribution: download + verify, not a container image

The Dockerfile now has an aforge fetch stage that downloads the released
binary and checksum-verifies it before it enters the image:

ARG AFORGE_BASE_URL=https://agentfield.ai/downloads/aforge
ARG AFORGE_VERSION=v0.1.0
  1. GET ${AFORGE_BASE_URL}/${AFORGE_VERSION}/aforge-linux-${TARGETARCH}.gz
  2. gunzip to /out/aforge
  3. GET .../checksums.txt — its hashes are of the decompressed binaries —
    rewrite the matching "<sha256> aforge-linux-<arch>" line to name the local
    file, sha256sum -c
  4. chmod +x + aforge --help smoke, so a corrupt or wrong-arch download fails
    the build, not the deploy

COPY --from=aforge /out/aforge /usr/local/bin/aforge as before. Both ARGs stay
overridable for mirrors/air-gapped registries, and AFORGE_VERSION is part of
the layer cache key — bumping it is what pulls a newer AForge; a floating URL
alone would keep restoring the cached binary. Applied to go/Dockerfile too,
and ca-certificates is now explicit in the Python runtime stage (aforge speaks
HTTPS to openrouter.ai).

Commits

Commit Scope
feat(runtime): add aforge as a runtime and default it on OpenRouter Python: runtime vocabulary, mappings, model table, default flip
feat(go): mirror the aforge runtime and default on the Go node go/internal/{runtimex,config,fast,orch,roles}
build(docker): fetch and checksum-verify the released aforge binary Dockerfile, go/Dockerfile
build(deps): re-pin agentfield to the published PyPI release pyproject.toml, requirements*.txt
feat(runtime): prefer OpenRouter over Anthropic when both keys are set isolated behaviour change — see below
docs(config): document the aforge default, the download pinning, and the gaps READMEs, .env.example, manifests, compose
ci: build the aforge fetch stage when the download host is reachable .github/workflows/ci.yml

The precedence change is its own commit

feat(runtime): prefer OpenRouter over Anthropic when both keys are set is
deliberately isolated so it can be reviewed — or reverted — on its own.

Before, _openrouter_only_env() returned False whenever ANTHROPIC_API_KEY was
non-empty, so a deployment holding both keys resolved to claude_code.
After, only an explicit SWE_DEFAULT_RUNTIME vetoes the OpenRouter choice.

Anyone with both keys set and no SWE_DEFAULT_RUNTIME silently switches harness
and model family (sonnet/haikuopenrouter/deepseek/deepseek-v4-flash-0731)
and starts spending on OpenRouter instead of Anthropic. Opt-out:
SWE_DEFAULT_RUNTIME=claude_code. Reverting that one commit restores the old
precedence and leaves the rest of the aforge work intact.

Validation contract

Observable behaviours this change must exhibit:

  • With OPENROUTER_API_KEY set and no SWE_DEFAULT_RUNTIME, both nodes resolve
    the default runtime to aforge on the main, fast, issue and planning paths.
  • With only ANTHROPIC_API_KEY, the default stays claude_code.
  • With both keys and no SWE_DEFAULT_RUNTIME, the default is aforge
    (the isolated precedence commit; reverting it returns claude_code).
  • SWE_DEFAULT_RUNTIME=open_code still resolves to open_code — the rollback
    is configuration-only and OpenCode stays installed in the image.
  • The aforge runtime maps to a harness provider the pinned SDK actually
    registers, and resolves to openrouter/deepseek/deepseek-v4-flash-0731.
  • A stock docker build . produces an image with a working aforge on PATH
    whose sha256 equals the release checksums.txt entry for aforge-linux-amd64.
  • A tampered/absent download fails the build rather than shipping.
  • pip install -e . resolves from PyPI, with no VCS dependency.
  • Python and Go implementations of swe-planner agree on every resolution above.

How this was verified

Every gate the CI workflow runs, plus real container runs.

CI gates (workflow steps run literally, Python 3.12 / Go 1.23 semantics):

  • pip install -e ".[dev]" → resolves agentfield-0.1.129 from PyPI.
  • make check1211 passed, 1 skipped + compileall clean.
  • gofmt -l . clean · go build ./... ok · go vet ./... ok ·
    go test -race -count=1 ./... all packages ok.
  • ruff check . — 789 findings, byte-identical to the count on origin/main
    (pre-existing; CI does not run ruff).

Fetch stage, against a local mirror of the published layout:

#7 [aforge 4/4] RUN set -eu; arch="amd64"; curl -fsSL .../aforge-linux-${arch}.gz ...
#7 1.548 aforge: OK          <- sha256sum -c against the release checksums

Python image (docker build ., then run):

$ docker run --rm --entrypoint sh <img> -c 'command -v aforge; sha256sum /usr/local/bin/aforge'
/usr/local/bin/aforge
61217a18135f392d50c337d971fa5b228f436f8df144800b37fbb340020c79f5  /usr/local/bin/aforge

That hash is exactly the release checksums.txt line for aforge-linux-amd64.
aforge --help prints the CLI usage; pip show agentfield0.1.129.

Real end-to-end exec inside the image (OPENROUTER_API_KEY supplied at run
time, tiny budget):

$ docker run --rm -e OPENROUTER_API_KEY --entrypoint sh <img> \
    -c 'mkdir -p /tmp/w && echo "Reply with exactly OK" | aforge exec --json -w /tmp/w --timeout 120 --budget 20000 --turns 3'
{"text":"OK","stop":"done","usage":{"calls":1,"prompt_tokens":3548,"completion_tokens":2,"cost":0.000279172992},"artifacts":[],"turns":1,"elapsed_ms":14789}
EXIT=0

Default resolution inside the container (no test tree in the image, so the
config surface is exercised directly):

OPENROUTER_API_KEY only:
  _default_runtime()        : aforge
  BuildConfig().runtime     : aforge -> ai_provider aforge
  ExecutionConfig().runtime : aforge
  FastBuildConfig().runtime : aforge
  harness adapter           : aforge
  SDK SUPPORTED_PROVIDERS   : ['aforge', 'claude-code', 'codex', 'gemini', 'grok', 'opencode']
  built provider            : AforgeProvider  bin=aforge
both keys                   : aforge
ANTHROPIC_API_KEY only      : claude_code
SWE_DEFAULT_RUNTIME=open_code: open_code

Go image (docker build -f go/Dockerfile .): same verified aforge binary at
/usr/local/bin/aforge (identical sha256), AGENTFIELD_AFORGE_COMMAND=exec,
swe-planner / swe-fast / swe-pro all present.

Go default resolution vs. the pinned SDK (throwaway probe, not committed):

DefaultRuntime()      = "aforge"
DefaultFastRuntime()  = "aforge"
harness adapter       = "aforge"
BuildProvider("aforge") err = unknown harness provider: "aforge" (supported: claude-code, codex, gemini, opencode)

That last line is the Go-side gap, called out below.

CI job bash was executed under bash --noprofile --norc -eo pipefail before
committing: the probe correctly reports available=false for the not-yet-live
public host and available=true against a reachable one.

Test images and containers were removed afterwards.

Known gaps — these are what keep the PR in draft

1. The download host does not serve this version yet. The Dockerfile
default AFORGE_BASE_URL / AFORGE_VERSION point at
https://agentfield.ai/downloads/aforge/v0.1.0/ — aforge-v2's first semver
release. The host is live and already serves the older build-<sha>
coordinate, but v0.1.0/checksums.txt is still a 404 there. Everything above
was verified against a local mirror of exactly that layout via
--build-arg AFORGE_BASE_URL=.... Nothing in the tree references anything but
the public URL.

2. AFORGE_BIN and AGENTFIELD_AFORGE_COMMAND are no-ops today. On
agentfield 0.1.129 the aforge provider always runs
aforge exec --json -w <root> and always resolves the binary as aforge from
PATH; neither env var is read. They are accepted by the deployment surface and
documented as no-ops. It also passes no --timeout, so aforge's own
15-minute wall fires before the SDK-side AGENTFIELD_HARNESS_TIMEOUT_SECONDS
(default 1800s). Both start working with the release carrying agentfield#905.

Exact lines to bump when that release lands:

pyproject.toml:13            "agentfield>=0.1.129",
requirements.txt:5           agentfield>=0.1.129
requirements-docker.txt:5    agentfield>=0.1.129

3. The Go node's aforge default cannot run yet. The Go SDK pinned by
AGENTFIELD_SDK_REF has no aforge harness provider —
harness.BuildProvider("aforge") returns
unknown harness provider: "aforge" (supported: claude-code, codex, gemini, opencode).
The default is flipped for parity and the gap is documented at DefaultRuntime
and in go/README.md; until the pin moves, the Go node must be pointed at
another runtime with SWE_DEFAULT_RUNTIME. Three lines move together:

go/go.mod:8                          github.com/Agent-Field/agentfield/sdk/go v0.0.0-20260723130821-20955b2637b4
go/Dockerfile (ARG AGENTFIELD_SDK_REF=20955b2637b4708758c328a4f64fe460c7d4b772)
.github/workflows/ci.yml (env AGENTFIELD_SDK_REF: 20955b2637b4708758c328a4f64fe460c7d4b772)

4. CI cannot build the full image yet. The new aforge-fetch-stage job
probes the download host and, while it is unreachable, emits a notice and
passes. It reads the coordinates out of the Dockerfile so it cannot drift from
what the image builds. Once the host is live, swap --target aforge for a full
docker build . and this becomes the image-build gate.

5. Not touched, on purpose. sec-af/… is not applicable to this change.
AForge already embeds swe-pro (AFORGE_SWEPRO=1), so SWE-AF's own pro-engine
vendoring (go/bin/swe-pro-*, SWE_PRO_ENGINE) is deliberately left exactly as
it is — collapsing the two is a separate decision.

Un-drafting checklist

  • https://agentfield.ai/downloads/aforge/v0.1.0/ serves the gzipped
    binaries + checksums.txt (the release assets exist; publishing them to
    the download host is the remaining step)
  • agentfield#905 is merged and released; bump the three agentfield>=…
    lines above
  • bump the three AGENTFIELD_SDK_REF / go.mod locations to that release,
    so the Go node's aforge default resolves to a real provider
  • flip the CI job from --target aforge to a full image build

Follow-ups (out of scope here)

  • The image bakes ENV HARNESS_MODEL=openrouter/moonshotai/kimi-k2.6 for
    OpenCode's small_model interpolation, and HARNESS_MODEL sits in the
    SWE_DEFAULT_MODEL → AI_MODEL → HARNESS_MODEL cascade. So inside the image
    every role resolves to kimi-k2.6, not the
    openrouter/deepseek/deepseek-v4-flash-0731 the docs advertise. Pre-existing
    on main, unrelated to this change, but worth a dedicated fix.
  • Multi-arch: the fetch stage already keys on TARGETARCH and the release ships
    linux/arm64, but nothing here builds or publishes an arm64 image.

🤖 Generated with Claude Code

AbirAbbas and others added 7 commits August 17, 2026 11:30
Adds `aforge` to RUNTIME_VALUES and to every place a runtime is accepted or
mapped: the alias normaliser (`aforge` / `aforge_v2` / `aforge-v2`), both
harness mappings (provider and adapter both resolve to `aforge`, which is what
the AgentField SDK's provider factory registers), the Build/Execution/Issue/Fast
config `runtime` literals, and the per-runtime model table — where aforge shares
open_code's OpenRouter default (openrouter/deepseek/deepseek-v4-flash-0731).

Flips the OpenRouter auto-selection target from `open_code` to `aforge`, on both
the main and the fast path. The precondition is unchanged in this commit: an
OpenRouter key, no Anthropic key, no explicit SWE_DEFAULT_RUNTIME.
`SWE_DEFAULT_RUNTIME=open_code` remains the configuration-only rollback and
OpenCode stays installed in the image.

Also fixes an inverted comment in the fast-path test ("an OpenRouter key with no
OpenRouter key" -> "no Anthropic key").

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PR #137 changed only the Python node, so the two implementations of
`swe-planner` disagreed about what an OpenRouter-only install resolves to —
Python said `aforge`, Go still said `open_code` — while the shared README and
manifests describe one product. This ports the same change into go/:

- runtimex: `aforge` joins RuntimeValues (first, matching Python's tuple order,
  which is joined verbatim into the "Valid runtimes: ..." error), the alias set
  (`aforge` / `aforge_v2` / `aforge-v2`), and both harness mappings.
- config: an `aforge` base-model row sharing open_code's OpenRouter default,
  and DefaultRuntime / DefaultFastRuntime auto-select `aforge`.
- fast: the fast-path runtime→provider map gains the `aforge` case, matching
  fast/app.py::_runtime_to_provider.

Known gap, documented at DefaultRuntime and in go/README.md: the Go SDK pinned
by AGENTFIELD_SDK_REF has no aforge harness provider —
`harness.BuildProvider("aforge")` returns
`unknown harness provider: "aforge" (supported: claude-code, codex, gemini,
opencode)`. The Go node therefore needs AGENTFIELD_SDK_REF bumped to a release
carrying agentfield#905 before the new default is usable; until then
SWE_DEFAULT_RUNTIME must point it at another runtime. This is one of the gates
keeping the PR in draft.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replaces `FROM ghcr.io/agent-field/aforge-v2:chat-v2-exec AS aforge` +
`COPY --from=aforge /aforge` with a fetch stage. The aforge-v2 source repo is
private and no such image is published, so the image contract could never be
satisfied; the release is distributed as gzipped binaries on the public
download host instead.

The new `aforge` stage (debian:bookworm-slim + curl/ca-certificates):

  1. downloads ${AFORGE_BASE_URL}/${AFORGE_VERSION}/aforge-linux-${TARGETARCH}.gz
  2. gunzips it to /out/aforge
  3. downloads checksums.txt — whose hashes are of the DECOMPRESSED binaries —
     rewrites the matching "<sha256>  aforge-linux-<arch>" line to name the
     local file, and runs `sha256sum -c`
  4. chmod +x and smoke-runs `aforge --help` so a corrupt or wrong-arch
     download fails the build rather than the deploy

`COPY --from=aforge /out/aforge /usr/local/bin/aforge` is unchanged in spirit.
Both ARGs stay overridable (`--build-arg AFORGE_BASE_URL=... AFORGE_VERSION=...`)
so CI, a mirror, or an air-gapped registry can point elsewhere; AFORGE_VERSION
is part of the layer's cache key, so bumping it is what pulls a newer AForge.

Applied to go/Dockerfile too — the Go node ships the same CLI surface — and
ca-certificates is now explicit in the Python runtime stage, since aforge
speaks HTTPS to openrouter.ai.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PR #137 pinned all three dependency files to a git SHA on the agentfield#905
branch (`agentfield @ git+...@bfd34426#subdirectory=sdk/python`). That commit
is unreleased and lives only on a draft PR branch, so the pin is not
installable from any published index and would break `pip install swe-af`.

The aforge harness provider itself has shipped since agentfield 0.1.127, so the
floor moves to the published `agentfield>=0.1.129` instead. Verified: a clean
`pip install -e ".[dev]"` resolves agentfield 0.1.129, whose
harness.providers._factory registers "aforge" in SUPPORTED_PROVIDERS and builds
an AforgeProvider.

What is NOT yet in a release, and what to bump when it is (all three lines):

  pyproject.toml:13          "agentfield>=0.1.129",
  requirements.txt:5         agentfield>=0.1.129
  requirements-docker.txt:5  agentfield>=0.1.129

0.1.129's provider always runs `aforge exec --json -w <root>` with the binary
resolved as `aforge` from PATH, so AFORGE_BIN and AGENTFIELD_AFORGE_COMMAND are
accepted by the deployment surface but are no-ops. It also passes no
`--timeout`, so aforge's own 15-minute wall applies before the SDK-side
AGENTFIELD_HARNESS_TIMEOUT_SECONDS (default 1800s) can fire.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
BEHAVIOUR CHANGE, isolated here so it can be reviewed and reverted on its own.

Before: `_openrouter_only_env()` returned False whenever ANTHROPIC_API_KEY was
non-empty, so a deployment holding both keys resolved to `claude_code`.
After: only an explicit SWE_DEFAULT_RUNTIME vetoes the OpenRouter choice, so
both-keys deployments now resolve to `aforge` with the OpenRouter model
defaults.

Who this moves: anyone who has both ANTHROPIC_API_KEY and OPENROUTER_API_KEY
set and has never set SWE_DEFAULT_RUNTIME. They silently switch harness and
model family (sonnet/haiku -> openrouter/deepseek/deepseek-v4-flash-0731) and
start spending on OpenRouter instead of Anthropic. The opt-out is
`SWE_DEFAULT_RUNTIME=claude_code`.

Reverting this commit alone restores the old precedence and leaves the rest of
the aforge work intact — the only coupling is the docstrings and the two test
cases changed here.

Python and Go are changed together so the two swe-planner implementations stay
in agreement.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…the gaps

- README / .env.example / both agentfield-package.yaml manifests describe the
  aforge default, list `aforge` in SWE_DEFAULT_RUNTIME's value set, and declare
  AGENTFIELD_AFORGE_COMMAND.
- README documents how the image gets AForge (download + checksum verification)
  and the two build args, including that AFORGE_VERSION is the cache key that
  actually pulls a newer binary.
- Both README files carry the honest caveats rather than promising behaviour
  the pinned dependencies do not have: AFORGE_BIN / AGENTFIELD_AFORGE_COMMAND
  are no-ops on agentfield 0.1.129, and the Go node's aforge runtime needs
  AGENTFIELD_SDK_REF bumped past agentfield#905 before it can run at all.
- docker-compose.go.yml passes AGENTFIELD_AFORGE_COMMAND through for both Go
  services, matching docker-compose.yml.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
SWE-AF's CI had no docker job at all, so nothing caught the previous
Dockerfile's reference to an image that is never published.

The new job builds only `--target aforge` from both Dockerfiles — the download
plus checksum verification — which is seconds of work and is the part that can
actually rot (wrong version string, moved URL, changed checksums). The rest of
the image (apt, npm, the Go build) is deliberately skipped.

The coordinates are read out of the Dockerfile itself rather than duplicated
here, so the job can never drift from what the image builds. The host is probed
first: while agentfield.ai does not yet serve /downloads/aforge/<version>/, the
job emits a notice and passes. Once the host is live, swap `--target aforge`
for a full `docker build .` to make this the image-build gate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@AbirAbbas
AbirAbbas force-pushed the agent/aforge-exec-default branch from bbd5e11 to 572329a Compare August 17, 2026 15:37
AbirAbbas and others added 3 commits August 17, 2026 13:07
aforge-v2 cut its first semver release (v0.1.0), so the AFORGE_VERSION
default moves off the build-<sha> coordinate onto the tag. Bumping the
string is what busts the fetch layer's cache, so this is what actually
pulls the released binary instead of restoring the stale one.

Touches the ARG default in both the Python and Go Dockerfiles plus the
matching docker build examples in the two READMEs. The AgentField SDK
pins are deliberately left alone — they bump on their own release.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
v0.1.130 is the published release that carries agentfield#905, so the
aforge harness provider honors AFORGE_BIN and AGENTFIELD_AFORGE_COMMAND
and passes an explicit --timeout. Docker layer caching keys off the
constraint string, so the floor itself has to move for an image rebuild
to pick the new release up.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Go node defaults to the aforge runtime, but harness.BuildProvider in
the previously pinned July SDK snapshot knew only claude-code, codex,
gemini and opencode — it returned `unknown harness provider: "aforge"`,
so the default was unusable on this node.

sdk/go/v0.1.130 is the first published submodule tag carrying
agentfield#905, so go.mod now requires a real version instead of a
pseudo-version, and go/Dockerfile, go/Makefile and the CI workflow pin
the same release by its tag commit. The docs that described the old
`replace`-directive layout and the "no submodule tags" workaround are
updated to match: resolution goes through the module proxy, and the
sparse SDK clone in the builder only records the pinned commit.

No source changes were needed for the SDK jump — build, vet, gofmt and
go test -race are clean on v0.1.130.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@AbirAbbas
AbirAbbas marked this pull request as ready for review August 18, 2026 02:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant