Skip to content

Commit e0a5817

Browse files
authored
Merge branch 'main' into feat/cli-golden-path
2 parents 81ff73e + 6fe002a commit e0a5817

10 files changed

Lines changed: 455 additions & 13 deletions

File tree

CHANGELOG.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,42 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
66

77
<!-- changelog:entries -->
88

9+
## [0.1.114-rc.1] - 2026-07-22
10+
11+
12+
### Fixed
13+
14+
- Fix(sdk-go): survive OpenAI's strict validator on codex --output-schema (#818)
15+
16+
The server behind codex exec now validates --output-schema against OpenAI
17+
strict-mode rules (probed live on codex-cli 0.144.1): every object node
18+
needs additionalProperties:false and a full required array, every node
19+
needs a type (or \$ref / anyOf), and free-form maps, typed maps, and
20+
boolean subschemas (invopop's output for `any` fields) are rejected with
21+
invalid_json_schema — killing every schema-enforced codex role
22+
(Agent-Field/SWE-AF#106).
23+
24+
Three coordinated changes:
25+
26+
- schema.go: codexSchemaStrictExpressible classifies a strict-rewritten
27+
schema against the probed validator rules, so the runner knows when
28+
--output-schema would be refused (map[string]any / any fields cannot be
29+
expressed without forcing an empty object).
30+
- runner.go: for inexpressible schemas the runner still writes the schema
31+
file and keeps the codex-native prompt, but hands the provider an empty
32+
schemaPath — codex runs with --output-last-message only and the
33+
existing local validation enforces the schema.
34+
- codex.go: --output-last-message is decoupled from --output-schema, and
35+
a rejected schema (invalid_json_schema in the CLI output) triggers one
36+
reactive rerun without the flag, so future validator tightening
37+
degrades to local validation instead of failing the role.
38+
39+
Live-verified: the real SWE-AF GitInitResult and Architecture strict
40+
schemas are ACCEPTED by the validator; PRD (boolean subschema via
41+
AskUserFormField.default_value) is correctly gated to the fallback path.
42+
43+
Co-authored-by: Claude Fable 5 <noreply@anthropic.com> (d0dec79)
44+
945
## [0.1.113] - 2026-07-21
1046

1147
## [0.1.113-rc.1] - 2026-07-21

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.1.113
1+
0.1.114-rc.1

control-plane/internal/templates/go/go.mod.tmpl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@ module {{.GoModule}}
22

33
go 1.23
44

5-
require github.com/Agent-Field/agentfield/sdk/go v0.1.113
5+
require github.com/Agent-Field/agentfield/sdk/go v0.1.114-rc.1

sdk/go/harness/codex.go

Lines changed: 56 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,41 @@ func NewCodexProvider(binPath string) *CodexProvider {
4343
// computation and file writing; the provider only needs the paths so it can
4444
// point codex's native --output-schema / --output-last-message flags at them.
4545
//
46+
// An empty schemaPath with a non-empty outputPath means "last-message only":
47+
// the runner determined the schema cannot be expressed in OpenAI strict mode
48+
// (codexSchemaStrictExpressible, schema.go), so --output-schema must not be
49+
// sent — the server would reject it with invalid_json_schema — but codex still
50+
// persists its final JSON answer to outputPath for local validation.
51+
//
4652
// This implements the schemaAware interface the runner detects (see runner.go).
4753
func (p *CodexProvider) SetSchema(schemaPath, outputPath string) {
4854
p.schemaPath = schemaPath
4955
p.outputPath = outputPath
5056
}
5157

58+
// isOutputSchemaRejection reports whether CLI output carries the server-side
59+
// strict-schema validator's 400 for --output-schema. The error event embeds
60+
// `"code": "invalid_json_schema"` and `Invalid schema for response_format
61+
// 'codex_output_schema'` (both observed live on codex-cli 0.144.1).
62+
func isOutputSchemaRejection(output string) bool {
63+
lower := strings.ToLower(output)
64+
return strings.Contains(lower, "invalid_json_schema") ||
65+
strings.Contains(lower, "invalid schema for response_format")
66+
}
67+
68+
// withoutFlagValue returns cmd with one `flag value` pair removed.
69+
func withoutFlagValue(cmd []string, flag string) []string {
70+
out := make([]string, 0, len(cmd))
71+
for i := 0; i < len(cmd); i++ {
72+
if cmd[i] == flag && i+1 < len(cmd) {
73+
i++
74+
continue
75+
}
76+
out = append(out, cmd[i])
77+
}
78+
return out
79+
}
80+
5281
func (p *CodexProvider) Execute(ctx context.Context, prompt string, options Options) (*RawResult, error) {
5382
// --skip-git-repo-check lets the harness run in arbitrary working dirs
5483
// (temp dirs, non-repo project roots); codex exec otherwise refuses to
@@ -90,14 +119,20 @@ func (p *CodexProvider) Execute(ctx context.Context, prompt string, options Opti
90119
}
91120

92121
// Native structured output: when the runner has set a schema, point codex at
93-
// the strict schema file and the last-message output file (patch lines
94-
// 176-178). codex writes its final message to the last-message file, which
95-
// the runner reads back.
122+
// the strict schema file (patch lines 176-178). Kept independent of the
123+
// last-message flag below: the runner passes an empty schemaPath when the
124+
// schema is not strict-expressible (see SetSchema), and the reactive
125+
// fallback after execution needs the answer file even when the server
126+
// rejects the schema flag.
127+
usedOutputSchema := false
96128
if p.schemaPath != "" && fileExists(p.schemaPath) {
97129
cmd = append(cmd, "--output-schema", p.schemaPath)
98-
if p.outputPath != "" {
99-
cmd = append(cmd, "--output-last-message", p.outputPath)
100-
}
130+
usedOutputSchema = true
131+
}
132+
// codex writes its final message to the last-message file, which the
133+
// runner reads back.
134+
if p.outputPath != "" {
135+
cmd = append(cmd, "--output-last-message", p.outputPath)
101136
}
102137

103138
env := make(map[string]string)
@@ -117,6 +152,21 @@ func (p *CodexProvider) Execute(ctx context.Context, prompt string, options Opti
117152
// prompt from stdin, and delivering it there keeps large prompts off the
118153
// argv and out of process listings.
119154
cliResult, err := runCLI(ctx, cmd, env, cwd, options.timeout(), []byte(prompt))
155+
156+
// Reactive fallback: if the server's strict-schema validator refused the
157+
// schema we sent (invalid_json_schema 400 — the validator's rules can
158+
// tighten upstream at any time), rerun once WITHOUT --output-schema. The
159+
// prompt suffix still pins the JSON contract and --output-last-message
160+
// still captures the final answer, so the runner's local validation takes
161+
// over exactly as in the not-strict-expressible path.
162+
if err == nil && usedOutputSchema && cliResult.ReturnCode != 0 &&
163+
isOutputSchemaRejection(cliResult.Stdout+cliResult.Stderr) {
164+
retryCmd := withoutFlagValue(cmd, "--output-schema")
165+
if retryResult, retryErr := runCLI(ctx, retryCmd, env, cwd, options.timeout(), []byte(prompt)); retryErr == nil {
166+
cliResult = retryResult
167+
}
168+
}
169+
120170
apiMS := int(time.Since(startAPI).Milliseconds())
121171

122172
if err != nil {

0 commit comments

Comments
 (0)