diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 929ae68..5abd4c0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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: 054a7d18b4bfbd6b48f8582a337894c3c5975d36 + AGENTFIELD_SDK_REF: 20955b2637b4708758c328a4f64fe460c7d4b772 AGENTFIELD_REPO: https://github.com/Agent-Field/agentfield.git GOWORK: off steps: diff --git a/go/Dockerfile b/go/Dockerfile index 3fb79b7..af62556 100644 --- a/go/Dockerfile +++ b/go/Dockerfile @@ -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=054a7d18b4bfbd6b48f8582a337894c3c5975d36 +ARG AGENTFIELD_SDK_REF=20955b2637b4708758c328a4f64fe460c7d4b772 ARG AGENTFIELD_REPO=https://github.com/Agent-Field/agentfield.git WORKDIR /src diff --git a/go/go.mod b/go/go.mod index f897fb8..002908a 100644 --- a/go/go.mod +++ b/go/go.mod @@ -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-20260721154150-054a7d18b4bf + github.com/Agent-Field/agentfield/sdk/go v0.0.0-20260723130821-20955b2637b4 github.com/invopop/jsonschema v0.13.0 golang.org/x/sync v0.11.0 ) diff --git a/go/go.sum b/go/go.sum index 1ae02cc..43bf171 100644 --- a/go/go.sum +++ b/go/go.sum @@ -1,5 +1,5 @@ -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/Agent-Field/agentfield/sdk/go v0.0.0-20260723130821-20955b2637b4 h1:OwOEyxRfYD0n2LAmaJIJdfejWpIYgRAf9oq/YA4qfVk= +github.com/Agent-Field/agentfield/sdk/go v0.0.0-20260723130821-20955b2637b4/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= diff --git a/go/internal/roles/coding/coding.go b/go/internal/roles/coding/coding.go index 54af5f1..0218e7c 100644 --- a/go/internal/roles/coding/coding.go +++ b/go/internal/roles/coding/coding.go @@ -514,6 +514,10 @@ func parseSynthesis(resp *ai.Response) (*schemas.QASynthesisResult, bool) { if err := resp.JSON(&out); err != nil { return nil, false } + // The system prompt names the actions in uppercase (FIX/APPROVE/BLOCK) and + // the reflected request schema carries no enum, so models routinely answer + // in uppercase. Normalize before the enum check. + out.Action = schemas.QASynthesisAction(strings.ToLower(strings.TrimSpace(string(out.Action)))) switch out.Action { case schemas.QASynthesisActionFix, schemas.QASynthesisActionApprove, schemas.QASynthesisActionBlock: return &out, true diff --git a/go/internal/roles/coding/coding_test.go b/go/internal/roles/coding/coding_test.go index 475c59e..dfefc89 100644 --- a/go/internal/roles/coding/coding_test.go +++ b/go/internal/roles/coding/coding_test.go @@ -593,3 +593,33 @@ func TestInputDefaults(t *testing.T) { t.Fatalf("qa_synthesizer default model must be haiku, got %q", si.Model) } } + +// Contract: models routinely answer the action in uppercase (the system prompt +// names FIX/APPROVE/BLOCK and the reflected request schema carries no enum), so +// parseSynthesis must case-normalize instead of dropping the synthesis and +// triggering the deterministic fallback. +func TestRunQASynthesizerNormalizesUppercaseAction(t *testing.T) { + nr := ¬eRecorder{} + mai := &mockAI{resp: aiJSONResponse(`{"action":"APPROVE","summary":"env failures are pre-existing","stuck":false}`)} + mh := &mockHarness{fn: func(_ any) (*harness.Result, error) { + t.Fatal("run_qa_synthesizer must not call the harness") + return nil, nil + }} + + out, err := RunQASynthesizer(context.Background(), newDeps(mh, mai, nr), map[string]any{ + "qa_result": map[string]any{"passed": false}, + "review_result": map[string]any{"approved": true}, + "iteration_history": []any{}, + "iteration_id": "s2", + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + m := asMap(t, out) + if m["action"] != "approve" { + t.Fatalf("expected normalized action \"approve\", got %v", m["action"]) + } + if m["summary"] != "env failures are pre-existing" { + t.Fatalf("model synthesis was discarded for the fallback: %v", m) + } +}