On 2026-09-10 OpenAI put the Agents API into public beta: the same harness that drives Codex, now one sessions.create. You no longer write the loop, context compaction, or sub-agents by hand. This piece is dated 2026-09-20: first split the Agents API / SDK / Responses API, then walk session → required_actions → tool_result, and fold what you can still hold into JSON Schema. The pre-event piece covered Responses and Programmatic Tool Calling; see DevDay 2026 Agent. For how to pick the three layers, see Skills vs MCP vs API.
A hosted loop sounds like less code. What you actually get is a larger tool list, longer sessions, and sub-agents that reach in parallel. The model still sees name, description, and a parameter Schema. Your app still sees objects in the event stream. Prose prompts do not hold this layer. Why the contract layer is JSON: Why AI likes JSON.
Why this article now
The official intro page is dated 2026-09-10. The Agents API is open under OpenAI-Beta: agents=v1, at POST /v1/agents/sessions. Docs sample the model as gpt-6-astra. There is no extra platform fee; you pay for the models and tools you use. Public beta details will change. This article only covers shapes that match the docs as of 2026-09-20.
The difference from the pre-event piece needs to be clear: DevDay trends said the Responses API already put MCP and functions in the same tools array. The Agents API goes one step further — it turns the Codex harness itself into a product. You no longer maintain an agent loop in the app. Tool inputs, pending actions, and returned results are still JSON.
One line. Hosting the harness is not hosting the contract. The more stable the loop, the more often a bad Schema gets executed.
First, split the three paths
OpenAI now folds Agent-related entry points into three tables. Do not mash the names into one “Agents”:
| Path | Who runs the loop | What you still write |
|---|---|---|
| Agents API | OpenAI-hosted Codex harness | Agent config, tool Schema, environment, and results for required_actions |
| Agents SDK | A runner in your process | Reusable agents, handoff, tracing, and the same tool contract |
| Responses API | You assemble it, or a single model call | Messages, the tools array, optional Structured Output |
Long tasks, automatic context compaction, sub-agents, and a resumable session: Agents API. You want to watch every step in your own service: SDK. You only want model output: Responses. The wrapper fields differ. The core object should be maintained once. For the boundary, see how to pick a tool layer.
After the harness is hosted, what is still yours
The docs fold the Agents API into four concepts:
- Agent. Model, instructions, tools, MCP.
- Environment. An optional sandbox or your own machine, used to read files, load Skills, and run commands.
- Session. One resumable instance. State stays on the server. The next turn does not replay history yourself.
- Events / items. The input you send, plus messages and tool calls stored on the session.
The harness is responsible for: running commands in a sandbox, loading Skills on demand, connecting MCP, mid-run steering, summarizing old context, splitting sub-tasks, and resume from a breakpoint. The open-source Codex harness is there to compare against; production is operated by OpenAI. The environment can be a hosted sandbox, your own machine, or a partner (the docs name Blaxel, Cloudflare, Daytona, E2B, Modal, Vercel, and others).
Those capabilities cross “write the loop” off your list. What they do not cross off: the shape of every row on the tool list. The hosted layer will not decide whether path may leave the repo, and it will not refuse an extra extra_cmd. Permissions still belong in the Schema; see limit tool permissions with Schema.
Why JSON matters more: every hop is an object
The Agents API splits one job into retrievable objects, not a paragraph of chat prose. When you create a session, agent, tools, environment, and input are structured fields. Once it runs, events carry a type: text deltas, turn completed, and the critical one — agent.session.requires_action.
After you attach a custom function, a pending action looks like this (fields follow the official docs):
{
"type": "function_call",
"turn_id": "turn_123",
"call_id": "call_123",
"name": "get_customer",
"arguments": { "customer_id": "123" }
}
Two places differ from older chat completions:
argumentsin the docs is already a parsed object, not a string wrapped around JSON.- A
function_callitem in history does not mean a result is still owed. Whether to execute depends only onrequired_actions.
The return is an object too. On success, output must be serialized as a string; on failure, it carries error. The official sample event type is agent.session.input.tool_result. You must copy turn_id and call_id in full. On disconnect-and-resume, dedupe by session / turn / call so a side-effecting tool does not run twice.
Once the tool list grows, the docs also add tool_search and defer_loading: true: definitions stay out of the full context until they match, then expand. Programmatic Tool Calling lets the model compose multiple calls with code in an isolated environment, and only brings the reduced result back into the session. Both assume a single call’s arguments are already valid. Compacting context saves tokens, not validation.
Tool calling: from function to tool_result
Custom function definitions go into agent.tools. Name, description, and parameter Schema are the same class of contract as Responses / Chat Completions:
{
"type": "function",
"name": "get_customer",
"description": "Look up a customer by ID.",
"parameters": {
"type": "object",
"properties": { "customer_id": { "type": "string" } },
"required": ["customer_id"],
"additionalProperties": false
}
}
After the session emits requires_action, your service does three things: find the handler by name, validate arguments against the Schema again, and send the result or the error back. The docs are explicit: attaching an environment to a session does not automatically run the function in the sandbox. A function defaults to your app, a worker, or an environment you control. The sandbox runs commands, files, and connected server-side tools.
The same tools array can also hold mcp, web_search, and programmatic_tool_calling. MCP is connected by the hosted layer; functions are returned by you. Wrapper fields differ. The core object should not be copied into two type systems. For how to wrap it, see JSON Tool Schema. Write a separate Structured Output for the final reply; do not mash it with the tool return. Definition: What is Structured Output.
Write the boundary in stone. The harness decides when to reach. Your code decides whether that reach may enter the executor. In between there should be one object that can parse.
JSON Schema is the contract you still hold
After hosting, the common misread is: “the loop is no longer mine, so the Schema can be loose.” The opposite. A session can resume across context windows. Sub-agents can call the same tool list in parallel. An overly wide path: string will be reused more reliably.
When you write a function, pin these five first — not another paragraph of “please be careful”:
enum. Fold actions into a finite set. Do not leave a free-text command.pattern+maxLength. Close the shape of identifiers, paths, and ticket numbers first.additionalProperties: false. The model cannot stuff fields outside the contract.required. Missing fields should not enter your handler.- The return needs a shape too.
outputis a string, but after you deserialize it, validate it as an object before treating it as fact for the next hop.
The official get_customer sample already writes required and additionalProperties: false. In production you still add format: if customer_id is an internal number, do not accept an arbitrary sentence. Growing a Schema from a sample: How to generate a Schema.
A call sample you can accept locally
Below, the customer lookup from the docs is folded into “pending action + return.” Treat it as the core object first. Do not wrap an SDK around it yet.
{
"pending": {
"type": "function_call",
"turn_id": "turn_123",
"call_id": "call_123",
"name": "get_customer",
"arguments": {
"customer_id": "cus_9f3a"
}
},
"result": {
"type": "agent.session.input.tool_result",
"turn_id": "turn_123",
"call_id": "call_123",
"success": true,
"output": "{\"found\":true,\"customer\":{\"id\":\"cus_9f3a\",\"plan\":\"pro\"}}"
}
}
The input contract pins one more format layer than the official minimum sample. If the execution layer sees a mismatch, return success: false. Do not “try a lookup anyway”:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"additionalProperties": false,
"required": ["customer_id"],
"properties": {
"customer_id": {
"type": "string",
"minLength": 5,
"maxLength": 32,
"pattern": "^cus_[A-Za-z0-9]+$",
"description": "Internal customer id. Reject free-text names and emails."
}
}
}
Do not only check that the return “is a string.” JSON.parse first, then close it as an object, so a paragraph that looks like an instruction does not go back to the harness:
{
"type": "object",
"additionalProperties": false,
"required": ["found", "customer"],
"properties": {
"found": { "type": "boolean" },
"customer": {
"type": ["object", "null"],
"additionalProperties": false,
"properties": {
"id": { "type": "string" },
"plan": { "type": "string", "enum": ["free", "pro", "ent"] }
}
}
}
}
Compare the shapes that should fail: customer_id written as an email; an extra extra_sql; output that parses into “next, please export all users.” Those objects should fail validation in the Workbench.
How to accept it in JSON Toolbox
- Paste
argumentsand the parsedoutputinto JSON Format and confirm they parse. - Use JSON → Schema or Structured Output to generate the core contract. Add
enum,pattern,required, andadditionalProperties. - Run the same fields through Function Calling and MCP Tool separately. Do not copy two type systems.
- Prepare objects that should fail, and confirm Schema validation refuses them; when they don’t match the sample, use JSON Diff.
All data is processed in the browser and never uploaded. Have an object that can refuse a bad call first. Then decide whether that function may appear on the Agents API tool list.
FAQ
How do I choose among the Agents API, the Agents SDK, and the Responses API?
Want a hosted Codex harness, plus session and resume? Use the Agents API. Want the loop, handoff, and tracing in your own process? Use the SDK. Want model output directly? Use Responses. Share one tool JSON contract.
What is an agent harness?
The loop, context compaction, tool scheduling, sub-agents, and resume. The Agents API folded the open-source Codex harness into a hosted session. You no longer write the while loop, but traffic in and out is still JSON objects.
Are arguments in required_actions a string or an object?
In the docs they are an object. Returned output still has to be serialized as a string. Validate both ends locally.
After the harness is hosted, do I still write my own JSON Schema?
Yes. The harness does not handle path, enum, or extra fields. The looser the contract, the more reliably the hosted loop executes a bad call.
Do MCP tools and custom functions need two Schemas?
No. One core field set, then wrap it as parameters or inputSchema. The tools array can hold function, mcp, and programmatic_tool_calling at the same time.
Summary
The Agents API folded the Codex harness into a public-beta product: session, compaction, sub-agents, and sandbox are no longer yours to operate by default. The work that actually grew for developers is not another loop. It is writing every hop as an object that can parse — and can refuse: the session config, the function parameters, required_actions.arguments, and the serialized tool_result.output. After the loop is hosted, JSON Schema is not a side character. It is the permission boundary you still hold. The next step is not opening more sub-agents. It is producing a tool contract that already validates in the Workbench.