Skip to content
Merged
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
2 changes: 1 addition & 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: dfb5c8a37f93f510f3e390bd515afd9154194066
AGENTFIELD_SDK_REF: 054a7d18b4bfbd6b48f8582a337894c3c5975d36
AGENTFIELD_REPO: https://github.com/Agent-Field/agentfield.git
GOWORK: off
steps:
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -641,6 +641,12 @@ Rule of thumb for the two prompt shapes:
| "Implement X feature" (needs decomposition) | `swe-planner.build` / `swe-fast.build` |
| "Change this code in this file, like this" (fully scoped, context supplied) | `swe-planner.implement_issue` / `swe-fast.implement_issue` |

A harness does not need this table hardcoded: both entry points register with
the control plane carrying an `entrypoint` tag and a routing description, so
`af ls --entrypoints` (or `GET /api/v1/discovery/capabilities`) lists them —
with when-to-use guidance — on any AgentField control plane the node joins
(agentfield ≥ 0.1.113).

Each call creates its own git worktree and an `issue/<build_id>-<slug>` branch
off `base_branch` (default: the current branch), implements the issue with the
coder → reviewer loop (a QA + synthesizer path when `needs_deeper_qa` is set),
Expand Down
2 changes: 1 addition & 1 deletion go/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ FROM golang:1.23-bookworm AS builder

# Pinned AgentField SDK ref. Default = agentfield origin/main HEAD at port time
# (v0.1.107-rc.1). Changing this string invalidates the clone layer below.
ARG AGENTFIELD_SDK_REF=dfb5c8a37f93f510f3e390bd515afd9154194066
ARG AGENTFIELD_SDK_REF=054a7d18b4bfbd6b48f8582a337894c3c5975d36
ARG AGENTFIELD_REPO=https://github.com/Agent-Field/agentfield.git

WORKDIR /src
Expand Down
2 changes: 1 addition & 1 deletion go/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ module github.com/Agent-Field/SWE-AF/go
go 1.21

require (
github.com/Agent-Field/agentfield/sdk/go v0.0.0-20260720184209-dfb5c8a37f93
github.com/Agent-Field/agentfield/sdk/go v0.0.0-20260721154150-054a7d18b4bf
github.com/invopop/jsonschema v0.13.0
golang.org/x/sync v0.11.0
)
Expand Down
6 changes: 2 additions & 4 deletions go/go.sum
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
github.com/Agent-Field/agentfield/sdk/go v0.0.0-20260713163335-795bdd57b4de h1:GYwOpQQheZ53FgmPta3PJ2CKQjATnPIMwAvNsBA7vF4=
github.com/Agent-Field/agentfield/sdk/go v0.0.0-20260713163335-795bdd57b4de/go.mod h1:08VZk14uw4GJH6a34psHkuLu+DcRr197Zi0IGmLlfrM=
github.com/Agent-Field/agentfield/sdk/go v0.0.0-20260720184209-dfb5c8a37f93 h1:J0MecY4o7QpRWdl5CVcIw75EZP5FGn01taJKJ9MmhnM=
github.com/Agent-Field/agentfield/sdk/go v0.0.0-20260720184209-dfb5c8a37f93/go.mod h1:08VZk14uw4GJH6a34psHkuLu+DcRr197Zi0IGmLlfrM=
github.com/Agent-Field/agentfield/sdk/go v0.0.0-20260721154150-054a7d18b4bf h1:wTYUlaz81NirvBpFC7tdXeCpHCWcmaKQZFAqamNZTJw=
github.com/Agent-Field/agentfield/sdk/go v0.0.0-20260721154150-054a7d18b4bf/go.mod h1:08VZk14uw4GJH6a34psHkuLu+DcRr197Zi0IGmLlfrM=
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
Expand Down
51 changes: 44 additions & 7 deletions go/internal/node/register.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,18 +137,40 @@ func (n *Node) registerOrchestrators() {
handlers["resolve"] = orch.ResolveHandler
handlers["resume_build"] = orch.ResumeBuildHandler

// Python registers the orchestrators via @app.reasoner() with NO tags
// (only router-registered roles carry tags) — keep the registration
// payload identical.
// Python registers the orchestrators via @app.reasoner(): only `build`
// carries tags (["entrypoint"]) plus an explicit routing description; the
// others get their docstring summaries as descriptions — keep the
// registration payload identical.
for name, h := range handlers {
var opts []agent.ReasonerOption
if name == "build" {
opts = append(opts, agent.WithReasonerTags("entrypoint"))
}
if d, ok := orchestratorDescriptions[name]; ok {
opts = append(opts, agent.WithDescription(d))
}
if s, ok := orchestratorSchemas[name]; ok {
opts = append(opts, agent.WithInputSchema(s))
}
regHandler(n, name, deps, h, opts...)
}
}

// orchestratorDescriptions mirrors the Python side: build's explicit
// description= kwarg, and the docstring first paragraphs the Python SDK
// auto-registers for the other orchestrators (swe_af/app.py).
var orchestratorDescriptions = map[string]string{
"build": "Feature-level build: plans a PRD → architecture → issue DAG, then codes, " +
"reviews, merges and verifies end-to-end. Give it a goal plus repo_path or " +
"repo_url; returns a verified feature branch (optionally a draft PR). " +
"Typical wall-clock 25-60 min. For one well-scoped change with known files, " +
"prefer implement_issue.",
"plan": "Run the full planning pipeline.",
"execute": "Execute a planned DAG with self-healing replanning.",
"resolve": "Update an existing PR: merge base, fix CI, address review comments, push.",
"resume_build": "Resume a crashed build from the last checkpoint.",
}

// ---------------------------------------------------------------------------
// Fast reasoners (swe-fast only)
// ---------------------------------------------------------------------------
Expand All @@ -164,11 +186,20 @@ func (n *Node) registerFastReasoners() {

// Python tags: fast_plan_tasks/fast_execute_tasks/fast_verify come from
// fast_router (tags=["swe-fast"]); the fast `build` is @app.reasoner()
// with NO tags. Mirror that exactly.
// tagged ["entrypoint"] with a routing description. Mirror that exactly.
tag := agent.WithReasonerTags(tagFast)
for name, h := range fast.Handlers() {
var opts []agent.ReasonerOption
if name != "build" {
if name == "build" {
opts = append(opts,
agent.WithReasonerTags("entrypoint"),
agent.WithDescription(
"Fast-mode build: one planning pass into a small task list, then code and "+
"verify with tight timeouts. Same goal/repo_path interface as "+
"swe-planner.build, but lighter and cheaper — suited to small features "+
"where full DAG planning is overkill."),
)
} else {
opts = append(opts, tag)
}
if s, ok := fastSchemas[name]; ok {
Expand All @@ -192,9 +223,15 @@ func (n *Node) registerIssueReasoner() {
Note: n.App,
NodeID: n.NodeID,
}
tag := agent.WithReasonerTags("swe-issue-go")
tag := agent.WithReasonerTags("swe-issue-go", "entrypoint")
for name, h := range issue.Handlers() {
opts := []agent.ReasonerOption{tag}
opts := []agent.ReasonerOption{tag, agent.WithDescription(
"Issue-level build (sub-harness entry): implements ONE fully-scoped issue " +
"on an isolated branch of a local repo — no planning agents, ~4-8 LLM " +
"calls, minutes not hours. Give it issue{title, description, " +
"acceptance_criteria, files_to_*} plus repo_path; returns the deliverable " +
"branch. Prefer this over build when you already know exactly what to change."),
}
if s, ok := issueSchemas[name]; ok {
opts = append(opts, agent.WithInputSchema(s))
}
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.111",
"agentfield>=0.1.113",
"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.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.111
agentfield>=0.1.113
pydantic>=2.0
claude-agent-sdk==0.1.20
hax-sdk>=0.2.4
Expand Down
11 changes: 10 additions & 1 deletion swe_af/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -489,7 +489,16 @@ def _is_empty_build(success: bool, ever_completed: int, ever_merged: int) -> boo
return not success and ever_completed == 0 and ever_merged == 0


@app.reasoner()
@app.reasoner(
tags=["entrypoint"],
description=(
"Feature-level build: plans a PRD → architecture → issue DAG, then codes, "
"reviews, merges and verifies end-to-end. Give it a goal plus repo_path or "
"repo_url; returns a verified feature branch (optionally a draft PR). "
"Typical wall-clock 25-60 min. For one well-scoped change with known files, "
"prefer implement_issue."
),
)
async def build(
goal: str,
repo_path: str = "",
Expand Down
10 changes: 9 additions & 1 deletion swe_af/fast/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,15 @@ def _runtime_to_provider(runtime: str) -> str:
return "opencode"


@app.reasoner()
@app.reasoner(
tags=["entrypoint"],
description=(
"Fast-mode build: one planning pass into a small task list, then code and "
"verify with tight timeouts. Same goal/repo_path interface as "
"swe-planner.build, but lighter and cheaper — suited to small features "
"where full DAG planning is overkill."
),
)
async def build(
goal: str,
repo_path: str = "",
Expand Down
11 changes: 10 additions & 1 deletion swe_af/issue/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,16 @@ def note(message: str, tags: list[str] | None = None) -> None:
).model_dump()


@issue_router.reasoner()
@issue_router.reasoner(
tags=["entrypoint"],
description=(
"Issue-level build (sub-harness entry): implements ONE fully-scoped issue "
"on an isolated branch of a local repo — no planning agents, ~4-8 LLM "
"calls, minutes not hours. Give it issue{title, description, "
"acceptance_criteria, files_to_*} plus repo_path; returns the deliverable "
"branch. Prefer this over build when you already know exactly what to change."
),
)
async def implement_issue(
issue: dict,
repo_path: str,
Expand Down
Loading