Skip to content

Expose Codex per-turn outputSchema in the built-in ACP transport - #64

Merged
geeen-jp merged 3 commits into
mainfrom
issue-35
Oct 3, 2026
Merged

geeen-jp merged 3 commits into
mainfrom
issue-35

Conversation

@geeen-jp

@geeen-jp geeen-jp commented Oct 3, 2026 •

Copy link
Copy Markdown
Owner

AGENT:
End-to-end evidence: live qualification (box, ChatGPT subscription OAuth, no API key, read-only, isolated CODEX_HOME, non-fast)
The user asked for these runs to use gpt-6-luna / max instead of gpt-6.1-sol, since structured output is a transport / App Server feature. The transport does not depend on the model. gpt-6.1-sol was not qualified separately.

  • P2: subscription/OAuth経路でCodex App Serverの outputSchema を検証する(GPT-6.1 Sol) #34 (direct App Server, codex 0.159.3). turn/start.outputSchema appeared on the wire. account/read reported type=chatgpt. The final answer was exactly one conformant JSON object. A follow-up turn that baited for markdown plus fenced JSON still returned one conformant object. The workspace was unchanged.
  • This PR, full ACPAgent → codex-acp 2.1.0 → bridge → app-server 0.159.3, at f20c2c2. Both main-thread turn/start requests carried outputSchema equal to the caller's schema (model gpt-6-luna, effort max, sandbox readOnly, serviceTier null). Both turns returned a single conformant JSON object, the workspace was unchanged, and the caller schema was not mutated. codex-acp's own title turn kept its own schema.
  • Subset probes. Strict subset accepted: type (including arrays with null), properties, required (all), additionalProperties false, items, enum, typed const, nested typed anyOf, minLength, pattern, minItems, maxItems, minimum. Rejected by the provider with 400 invalid_json_schema: objects that do not list every property in required, and subschemas without type.

Why

The built-in Codex ACP path could not use the provider-neutral StructuredOutputConfig, so Codex roles returned prompt-only JSON. The workflow saw wrapper/prose format retries. Codex App Server supports turn/start.outputSchema, but no released codex-acp forwards a caller schema.

Summary

Built-in Codex ACP now accepts the provider-neutral StructuredOutputConfig(mode="json_schema", schema=...). The SDK attaches the schema to every Codex App Server turn/start.outputSchema, including later turns on the same conversation such as a format retry.

  • Why a bridge. No released @agentclientprotocol/codex-acp forwards a caller schema; I checked 2.1.0, 2.1.1 and 2.1.2-preview.1. App Server takes outputSchema only on turn/start. So this adds a small repository-owned JSON-RPC proxy (acp_codex_output_schema.py), set via codex-acp's existing CODEX_PATH seam. It adds the schema to each turn/start that lacks one. This follows step 5 of the issue; codex-acp is not forked, vendored or patched.
  • Final response. Codex constrains only the final agent message of a turn (the final_answer item). When Codex structured output is set, the response is the last agent message, picked by message_id. Nothing is stripped, extracted or parsed.
  • Schema boundary. Codex applies OpenAI strict rules. The validator accepts only the live-qualified subset and rejects known provider rejections and unqualified constructs before any provider run (fail closed). It also rejects integers above 2^53-1. The caller schema is never rewritten.
  • Settings. ACPAgentSettings accepts acp_server="codex". The Docker wrapper always uses the pinned Codex runtime.
  • Docs. README and AGENTS.md are updated. There is no change to StructuredOutputConfig, persisted settings, REST shape, or version pins.

Issue Number

Closes #35

How to Test

  • Unit/integration: With CLAUDE_CODE_OAUTH_TOKEN unset, pytest on tests/sdk/settings, tests/sdk/agent, tests/agent_server and tests/sdk/test_settings.py gave 3220 passed. pyright reported 0 errors. pre-commit passed on all changed files.
  • Live: build ACPAgent(acp_server="codex", acp_model="gpt-6-luna", acp_permission_policy="read_only", acp_config_options={"reasoning_effort": "max"}, acp_isolate_data_dir=True, structured_output=StructuredOutputConfig(mode="json_schema", schema=<strict schema>)), provide CODEX_AUTH_JSON from a ChatGPT auth.json, and set CODEX_PATH to codex 0.159.3. Run two turns and check that every main turn/start carries outputSchema and that each final response is one conformant JSON object.

Type

  • Bug fix
  • Feature
  • Refactor
  • Breaking change
  • Docs / chore

Notes

There is no codex-acp fork. The proxy depends only on codex-acp's CODEX_PATH / $CODEX_PATH app-server JSONL stdio and on App Server turn/start.outputSchema.

@github-actions

github-actions Bot commented Oct 3, 2026

Copy link
Copy Markdown

Endpoint audit

⚠️ 18 actionable Agent Server contract divergence(s) · report-only

Contract: pinned release artifact

Category Count
Actionable client-only calls 0
Actionable server-only operations 18
Documented non-divergences 13
Agent Server contract operations 129
Audited handwritten client endpoints 119

Actionable client-only calls (0)

none

Actionable server-only operations (18)

  • DELETE /api/canvas-extensions/installed/{}
  • DELETE /api/llm/provider-connections/{}
  • GET /api/canvas-extensions/installed
  • GET /api/canvas-extensions/installed/{}
  • GET /api/canvas-extensions/installed/{}/bundle
  • GET /api/conversations/{}/events
  • GET /api/file/archive
  • GET /api/git/commits
  • GET /api/git/commits/{}/changes
  • GET /api/init
  • GET /api/llm/provider-connections
  • PATCH /api/canvas-extensions/installed/{}
  • PATCH /api/llm/provider-connections/{}
  • POST /api/canvas-extensions/install
  • POST /api/conversations/{}/load_plugin
  • POST /api/file/create_directory
  • POST /api/init
  • POST /api/llm/provider-connections
Documented non-divergences (13)

Client calls intentionally absent from the filtered contract (11)

  • GET /
  • GET /alive
  • GET /health
  • GET /ready
  • GET /server_info

Reason: Operational Agent Server endpoints intentionally excluded from the filtered public release artifact.
Owner: OpenHands runtime maintainers

  • DELETE /api/meta-profiles/{}
  • GET /api/meta-profiles
  • GET /api/meta-profiles/{}
  • POST /api/meta-profiles/{}
  • POST /api/meta-profiles/{}/activate

Reason: Client-ahead API stacked on the pending Agent Server meta-profiles implementation.
Owner: OpenHands SDK maintainers
Tracking: OpenHands#3744

  • POST /api/profiles/{}/validate

Reason: Client-ahead API stacked on the pending Agent Server pre-flight LLM validation endpoint.
Owner: OpenHands TypeScript client maintainers
Tracking: OpenHands#4422

Server operations covered by an exposed browser URL (2)

  • GET /api/conversations/{}/workspace
  • GET /api/conversations/{}/workspace/{}

Reason: RemoteWorkspace.startWorkspaceSession exposes these authenticated URLs for browser iframe and file requests; they are not HttpClient method calls.
Owner: OpenHands TypeScript client maintainers

@github-actions

github-actions Bot commented Oct 3, 2026

Copy link
Copy Markdown

📁 PR Artifacts Notice

This PR contains a .pr/ directory with temporary PR-specific documents. The directory will be automatically removed when the PR is approved.

@github-actions

github-actions Bot commented Oct 3, 2026

Copy link
Copy Markdown

Python API breakage checks — ✅ PASSED

Result: ✅ PASSED

Action log

@github-actions

github-actions Bot commented Oct 3, 2026

Copy link
Copy Markdown

REST API breakage checks (OpenAPI) — ✅ PASSED

Result: ✅ PASSED

Action log

@github-actions

github-actions Bot commented Oct 3, 2026

Copy link
Copy Markdown

Coverage

Coverage Report •
FileStmtsMissCoverMissing
openhands-sdk/openhands/sdk/agent
   acp_agent.py253834087%260, 330–333, 374, 406, 535, 542, 555, 580, 597, 600, 685, 692, 695–696, 794, 801, 821, 863, 866, 937, 993, 1074–1075, 1079, 1089–1092, 1098–1101, 1104, 1111, 1123–1125, 1131, 1137, 1144, 1146, 1187, 1275, 1389, 1437, 1557, 1561, 1563, 1565, 1594, 1597, 1599, 1603, 1606, 1615, 1698, 1704, 1727, 1785, 1795–1801, 1827–1829, 1840–1841, 1878, 1881, 1898–1902, 2119, 2123, 2159, 2165, 2173–2174, 2177, 2193–2194, 2210, 2213, 2216–2221, 2226, 2236, 2285–2295, 2304, 2344–2345, 2396, 2398, 2402, 2406, 2422, 2564, 2711, 2859, 2862–2863, 2880–2881, 2897, 2914, 2919, 2943, 2974, 2987, 3001, 3022, 3051, 3067, 3080, 3092, 3141, 3149, 3152, 3156–3159, 3170–3172, 3194, 3202–3203, 3206–3209, 3218–3220, 3226–3228, 3236–3237, 3249–3254, 3257–3259, 3266–3269, 3274–3278, 3290, 3316–3317, 3321–3322, 3330–3333, 3340, 3392, 3397, 3490–3491, 3493, 3514–3515, 3615, 3622–3623, 3714, 3970–3971, 3974, 3978–3980, 3983–3987, 4222, 4323, 4437, 4495, 4501, 4516–4517, 4528–4531, 4557, 4565, 4738–4740, 4752–4754, 4781, 4796–4797, 4800–4802, 4809, 4813, 4824–4825, 4833, 4838, 4850–4851, 4860–4865, 4872, 4877–4880, 4882–4885, 4887–4890, 4912, 4924–4926, 4961–4962, 4965, 4973–4975, 4979, 4981, 4985, 4988, 4997–4999, 5003, 5037–5039, 5041, 5046–5048, 5057–5061, 5064, 5069–5071, 5074–5075, 5090–5092, 5147–5149, 5387–5389, 5428, 5432, 5443, 5453, 5461, 5473, 5498, 5519–5521, 5535, 5546–5549, 5628–5629, 5670–5671, 5686–5687, 5695, 5698–5699, 5704, 5706, 5740–5741, 5757–5758
openhands-sdk/openhands/sdk/settings
   acp_providers.py2911695%151, 158, 166, 187, 726, 748, 761, 904, 947, 950, 964, 981–982, 986, 1007, 1010
   model.py8226992%316, 334, 543, 560, 570–573, 576, 589, 593, 599, 609, 615, 620, 715, 718–719, 725–728, 733, 737, 752, 755–757, 805, 810–811, 816, 837, 849, 898, 907, 954, 1141, 1166, 1168, 1170, 1172, 1174, 1176, 1178, 1180, 1182, 1184, 1448, 1450, 1816, 1836, 1978, 2107, 2146, 2301–2303, 2305, 2391, 2401, 2403, 2408, 2426, 2439, 2441, 2443, 2445, 2452
TOTAL43452789382% 

@geeen-jp
geeen-jp merged commit 7566620 into main Oct 3, 2026
39 of 43 checks passed
@geeen-jp
geeen-jp deleted the issue-35 branch October 3, 2026 03:02
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.

P2: built-in ACP transportでCodexのturn単位 outputSchema を公開する

1 participant