Overslaan naar hoofdinhoud
OpenAI

23 januari 2026

Engineering

De 'agent loop' van Codex ontleed

Door Michael Bolin, lid van de technische staf

Bezig met laden...

Codex CLI(opent in een nieuw venster) is onze platformonafhankelijke lokale software-agent. Hij is ontworpen om betrouwbare softwarewijzigingen van hoge kwaliteit door te voeren, terwijl hij veilig en efficiënt op je eigen machine draait. Sinds we de CLI in april lanceerden, hebben we enorm veel geleerd over het bouwen van een software-agent van wereldklasse. Om die inzichten te delen, is dit de eerste post in een doorlopende reeks waarin we de werking van Codex en onze harde lessen onder de loep nemen. (Wil je tot in detail weten hoe de Codex CLI is gebouwd? Bekijk dan onze open source repository op https://github.com/openai/codex(opent in een nieuw venster). Veel van onze ontwerpkeuzes zijn gedocumenteerd in GitHub-issues en pull requests.)

We beginnen met de agent loop: de kernlogica in Codex CLI die de interactie coördineert tussen de gebruiker, het model en de tools die het model inschakelt om nuttig werk te verrichten. We hopen dat dit artikel duidelijk maakt welke rol onze agent (of 'harness') speelt bij het gebruik van een LLM.

Voordat we beginnen, een korte opmerking over terminologie: bij OpenAI omvat 'Codex' verschillende software-agents, waaronder Codex CLI, Codex Cloud en de Codex VS Code-extensie. Dit artikel richt zich op de Codex harness: de basislaag die de agent loop en uitvoeringslogica bevat voor alle Codex-ervaringen, en die direct zichtbaar is in de Codex CLI. Voor het gemak gebruiken we hier de termen 'Codex' en 'Codex CLI' door elkaar.

De agent loop

De kern van elke AI-agent wordt gevormd door de zogenoemde 'agent loop'. Een vereenvoudigde weergave ziet er als volgt uit:

Diagram met de titel 'Agent loop' dat illustreert hoe een AI-systeem een gebruikersverzoek verwerkt, tools aanroept, resultaten waarneemt, zijn plan bijwerkt en output levert. Pijlen verbinden stappen zoals gebruikersinput, redenering van het model, acties van tools en de uiteindelijke reactie.

Allereerst neemt de agent de input van de gebruiker en verwerkt deze in een set tekstuele instructies voor het model: de prompt.

Vervolgens wordt het model aangeroepen door de instructies te versturen en om een antwoord te vragen. Dit proces heet inferentie. Tijdens inferentie wordt de tekstuele prompt eerst vertaald naar een reeks input-tokens(opent in een nieuw venster) (integers die verwijzen naar de vocabulaire van het model). Deze tokens worden gebruikt om het model te samplen, wat een nieuwe reeks output-tokens oplevert.

De output-tokens worden terugvertaald naar tekst, wat het antwoord van het model vormt. Omdat tokens stuk voor stuk worden gegenereerd, kan deze vertaling plaatsvinden terwijl het model nog draait. Daarom tonen veel LLM-applicaties de tekst als een stream. In de praktijk zit inferentie meestal achter een API die op tekst werkt en de tokenisatiedetails afschermt.

Na de inferentiestap produceert het model ofwel: (1) een definitieve reactie op de input van de gebruiker, of (2) een vraag om een tool call die de agent moet uitvoeren (bijv. 'voer ls uit en rapporteer de output'). In het tweede geval voert de agent de opdracht uit en voegt de output toe aan de oorspronkelijke prompt. Deze output wordt gebruikt om een nieuwe input te genereren waarmee het model opnieuw wordt aangeroepen. De agent kan deze nieuwe informatie vervolgens meenemen in een volgende poging.

Dit proces herhaalt zich totdat het model stopt met het genereren van tool calls, maar een bericht voor de gebruiker genereert (in OpenAI-modellen noemen we dit een assistant message). In veel gevallen geeft dit bericht direct antwoord op het oorspronkelijke verzoek van de gebruiker, maar het kan ook een vervolgvraag zijn.

Omdat de agent tool calls kan uitvoeren die de lokale omgeving aanpassen, blijft zijn 'output' niet beperkt tot het bericht van de assistent. Vaak is de belangrijkste output van een software-agent namelijk de code die hij op je machine schrijft of bewerkt. Toch eindigt elke turn altijd met een assistent-bericht, zoals 'Ik heb architecture.md toegevoegd, zoals gevraagd', waarmee de agent aangeeft dat de taak binnen de agent loop is afgerond. Vanuit het perspectief van de agent zit het werk erop en ligt de controle weer bij de gebruiker.

De route van gebruikersinput naar agent-respons, zoals weergegeven in het diagram, noemen we één turn in een conversatie (of een thread in Codex). Zo'n conversation turn kan echter vele iteraties tussen de model-inferentie en tool calls bevatten. Elke keer dat je een nieuw bericht naar een bestaande conversatie stuurt, wordt de volledige conversatiegeschiedenis meegenomen in de prompt voor de nieuwe turn. Dit omvat ook de berichten en toolaanroepen uit eerdere turns:

Diagram met de naam 'Multi-turn agent loop' dat toont hoe een AI-agent iteratief gebruikersinput verwerkt, acties genereert, tools raadpleegt, de status bijwerkt en resultaten retourneert. Het bevat gelabelde stappen, pijlen en voorbeelden van tool-outputs die de redeneercyclus van de agent illustreren.

Dit betekent dat naarmate de conversatie vordert, de prompt die naar het model wordt gestuurd steeds langer wordt. Die lengte is van belang omdat elk model een contextvenster heeft: het maximale aantal tokens dat het kan verwerken in één inferentie-aanroep. Let op: dit venster omvat zowel de input- als de output-tokens. Zoals je je kunt voorstellen, kan een agent besluiten om in één turn honderden tool calls te doen, waardoor het contextvenster vol kan lopen. Daarom is het beheren van dit contextvenster een van de vele verantwoordelijkheden van de agent. Laten we nu de diepte in gaan en kijken hoe Codex de agent loop precies uitvoert.

Modelinferentie

De Codex CLI stuurt HTTP-verzoeken naar de Responses API(opent in een nieuw venster) om model-inferentie uit te voeren. We zullen bekijken hoe de informatiestroom door Codex loopt, waarbij de Responses API wordt gebruikt om de agent loop aan te sturen.

Het Responses API-endpoint dat de Codex CLI gebruikt is configureerbaar(opent in een nieuw venster). Hierdoor werkt de CLI met elk endpoint dat de Responses API implementeert(opent in een nieuw venster):

Laten we onderzoeken hoe Codex de prompt opbouwt voor de allereerste inferentie-aanroep in een conversatie.

De initiële prompt bouwen

Als eindgebruiker geef je niet letterlijk de prompt op die wordt gebruikt om het model te samplen wanneer je de Responses API bevraagt. In plaats daarvan geef je verschillende soorten input mee als onderdeel van je query. De Responses API-server bepaalt vervolgens hoe deze informatie wordt omgezet in een prompt die het model kan verwerken. Je kunt de prompt zien als een lijst met items. In dit gedeelte leggen we uit hoe je query wordt omgezet in die lijst.

In de initiële prompt is elk item in de lijst gekoppeld aan een rol. De rol geeft aan hoeveel gewicht de bijbehorende inhoud moet hebben en bestaat uit één van de volgende waarden (in afnemende volgorde van prioriteit): system, developer, user, assistant.

De Responses API(opent in een nieuw venster) accepteert een JSON-payload met vele parameters. We richten ons hier op de volgende drie:

Binnen Codex wordt het veld instructions uitgelezen uit de model_instructions_file(opent in een nieuw venster) in ~/.codex/config.toml, indien opgegeven; anders worden de base_instructions die aan het model zijn gekoppeld(opent in een nieuw venster) gebruikt. Modelspecifieke instructies bevinden zich in de Codex-repository en zijn gebundeld in de CLI (bijvoorbeeld gpt-5.2-codex_prompt.md(opent in een nieuw venster)).

Het veld tools bevat een lijst met tooldefinities die voldoen aan een schema van de Responses API. Voor Codex omvat dit tools die worden geleverd door de Codex CLI, tools vanuit de Responses API die beschikbaar moeten zijn voor Codex, en tools die door de gebruiker worden aangedragen, meestal via MCP-servers:

JavaScript

1
[
2
// Codex's default shell tool for spawning new processes locally.
3
{
4
"type": "function",
5
"name": "shell",
6
"description": "Runs a shell command and returns its output...",
7
"strict": false,
8
"parameters": {
9
"type": "object",
10
"properties": {
11
"command": {"type": "array", "description": "The command to execute", ...},
12
"workdir": {"description": "The working directory...", ...},
13
"timeout_ms": {"description": "The timeout for the command...", ...},
14
...
15
},
16
"required": ["command"],
17
}
18
}
19

20
// Codex's built-in plan tool.
21
{
22
"type": "function",
23
"name": "update_plan",
24
"description": "Updates the task plan...",
25
"strict": false,
26
"parameters": {
27
"type": "object",
28
"properties": {"plan":..., "explanation":...},
29
"required": ["plan"]
30
}
31
},
32

33
// Web search tool provided by the Responses API.
34
{
35
"type": "web_search",
36
"external_web_access": false
37
},
38

39
// MCP server for getting weather as configured in the
40
// user's ~/.codex/config.toml.
41
{
42
"type": "function",
43
"name": "mcp__weather__get-forecast",
44
"description": "Get weather alerts for a US state",
45
"strict": false,
46
"parameters": {
47
"type": "object",
48
"properties": {"latitude": {...}, "longitude": {...}},
49
"required": ["latitude", "longitude"]
50
}
51
}
52
]

Tot slot is het veld input van de JSON-payload een lijst met items. Codex voegt de volgende items toe(opent in een nieuw venster) aan de input voordat het bericht van de gebruiker wordt toegevoegd:

1. Een bericht met role=developer dat de sandbox beschrijft die alleen van toepassing is op de shell-tool die door Codex zelf wordt geleverd (beschreven in het gedeelte tools). Dat betekent dat andere tools, zoals die van MCP-servers, niet door Codex in een sandbox worden geplaatst. Zij zijn zelf verantwoordelijk voor het handhaven van hun eigen veiligheidsmaatregelen.

Het bericht wordt opgebouwd vanuit een template, waarbij de belangrijkste inhoud afkomstig is uit Markdown-fragmenten die in de Codex CLI zijn gebundeld, zoals workspace_write.md(opent in een nieuw venster) en on_request.md(opent in een nieuw venster):

Platte tekst

1
<permissions instructions>
2
- description of the sandbox explaining file permissions and network access
3
- instructions for when to ask the user for permissions to run a shell command
4
- list of folders writable by Codex, if any
5
</permissions instructions>

2. (Optioneel) Een bericht met role=developer waarvan de inhoud bestaat uit de waarde van developer_instructions, uitgelezen uit het config.toml-bestand van de gebruiker.

3. (Optioneel) Een bericht met role=user dat de 'gebruikersinstructies' bevat. Deze zijn niet afkomstig uit één enkel bestand, maar worden samengevoegd uit meerdere bronnen(opent in een nieuw venster). Over het algemeen verschijnen specifiekere instructies later:

4. Een bericht met role=user dat de lokale omgeving beschrijft waarin de agent momenteel actief is. Dit specificeert de huidige werkmap en de shell van de gebruiker(opent in een nieuw venster):

Platte tekst

1
<environment_context>
2
<cwd>/Users/mbolin/code/codex5</cwd>
3
<shell>zsh</shell>
4
</environment_context>

Zodra Codex al deze verwerkingsstappen heeft uitgevoerd om de input te initialiseren, wordt het bericht van de gebruiker toegevoegd om de conversatie te starten.

De voorgaande voorbeelden richtten zich op de inhoud van elk bericht, maar elk element van de input is een JSON-object met een type, role(opent in een nieuw venster) en content:

JSON

1
{
2
"type": "message",
3
"role": "user",
4
"content": [
5
{
6
"type": "input_text",
7
"text": "Add an architecture diagram to the README.md"
8
}
9
]
10
}

Zodra Codex de volledige JSON-payload heeft opgebouwd, wordt het HTTP POST-verzoek verstuurd naar de Responses API. Hierbij wordt een Authorization-header gebruikt die afhankelijk is van de configuratie in ~/.codex/config.toml (indien gespecificeerd worden extra HTTP-headers en query-parameters toegevoegd).

Wanneer een OpenAI Responses API-server het verzoek ontvangt, gebruikt deze de JSON om de prompt voor het model af te leiden (let op: een aangepaste implementatie van de Responses API zou hier een andere keuze kunnen maken):

Snapshot-diagram dat één stap in een AI-agent loop toont. Een gebruikersverzoek komt binnen bij het model, dat vervolgens een gedachte, een actie met een tool-naam en een tool-input produceert. Het diagram benadrukt deze tussenliggende redeneerstap voordat de tool daadwerkelijk wordt aangeroepen.

Zoals je ziet, wordt de volgorde van de eerste drie items in de prompt bepaald door de server, niet door de client. Van die drie items wordt echter alleen de inhoud van het systeembericht ook daadwerkelijk door de server beheerd; de tools en instructions worden bepaald door de client. Deze worden gevolgd door de input uit de JSON-payload om de prompt compleet te maken.

Nu we onze prompt hebben, zijn we klaar om het model te samplen.

De eerste turn

Dit HTTP-verzoek naar de Responses API initieert de eerste turn van een conversatie in Codex. De server antwoordt met een Server-Sent Events (SSE(opent in een nieuw venster))-stream. De data van elk event is een JSON-payload met een type dat begint met response, wat er ongeveer zo uit kan zien (een volledige lijst met events is te vinden in onze API-documentatie(opent in een nieuw venster)):

Platte tekst

1
data: {"type":"response.reasoning_summary_text.delta","delta":"ah ", ...}
2
data: {"type":"response.reasoning_summary_text.delta","delta":"ha!", ...}
3
data: {"type":"response.reasoning_summary_text.done", "item_id":...}
4
data: {"type":"response.output_item.added", "item":{...}}
5
data: {"type":"response.output_text.delta", "delta":"forty-", ...}
6
data: {"type":"response.output_text.delta", "delta":"two!", ...}
7
data: {"type":"response.completed","response":{...}}

Codex verwerkt de stroom van events(opent in een nieuw venster) en publiceert ze opnieuw als interne event-objecten die door een client gebruikt kunnen worden. Events zoals response.output_text.delta worden gebruikt om streaming in de UI te ondersteunen, terwijl andere events zoals response.output_item.added worden omgezet in objecten die aan de input worden toegevoegd voor volgende aanroepen naar de Responses API.

Stel dat het eerste verzoek aan de Responses API twee response.output_item.done events bevat: één met type=reasoning en één met type=function_call. Deze events moeten in het input-veld van de JSON worden opgenomen wanneer we het model opnieuw bevragen met het resultaat van de tool call: 

JavaScript

1
[
2
/* ... original 5 items from the input array ... */
3
{
4
"type": "reasoning",
5
"summary": [
6
"type": "summary_text",
7
"text": "**Adding an architecture diagram for README.md**\n\nI need to..."
8
],
9
"encrypted_content": "gAAAAABpaDWNMxMeLw..."
10
},
11
{
12
"type": "function_call",
13
"name": "shell",
14
"arguments": "{\"command\":\"cat README.md\",\"workdir\":\"/Users/mbolin/code/codex5\"}",
15
"call_id": "call_8675309..."
16
},
17
{
18
"type": "function_call_output",
19
"call_id": "call_8675309...",
20
"output": "<p align=\"center\"><code>npm i -g @openai/codex</code>..."
21
}
22
]

De resulterende prompt die wordt gebruikt om het model te samplen tijdens deze vervolgvraag, ziet er dan als volgt uit:

Diagram met het label 'Snapshot 2' dat een AI-agent toont na een tool call. Het model ontvangt een observatie van de tool en produceert een nieuwe gedachte en actie. Pijlen verbinden inputs, observaties en outputs om te illustreren hoe de agent de redeneerlus doorloopt.

Let er vooral op dat de oude prompt een exacte prefix is van de nieuwe prompt. Dit is opzettelijk: het maakt vervolgverzoeken veel efficiënter omdat we gebruik kunnen maken van prompt caching (dit bespreken we in het volgende gedeelte over prestaties).

Als we terugkijken naar ons eerste schema van de agent loop, zien we dat er vele iteraties kunnen plaatsvinden tussen inferentie en het aanroepen van tools. De prompt kan blijven groeien totdat we uiteindelijk een assistentbericht ontvangen, wat het einde van de turn markeert:

Platte tekst

1
data: {"type":"response.output_text.done","text": "I added a diagram to explain...", ...}
2
data: {"type":"response.completed","response":{...}}

In de Codex CLI tonen we dit bericht aan de gebruiker en plaatsen we de focus in het invoerveld om aan te geven dat het nu de beurt is aan de gebruiker om de conversatie voort te zetten. Als de gebruiker reageert, moeten zowel het assistentbericht van de vorige turn als het nieuwe bericht van de gebruiker aan de input worden toegevoegd in het nieuwe verzoek aan de Responses API om de nieuwe turn te starten:

JavaScript

1
[
2
/* ... all items from the last Responses API request ... */
3
{
4
"type": "message",
5
"role": "assistant",
6
"content": [
7
{
8
"type": "output_text",
9
"text": "I added a diagram to explain the client/server architecture."
10
}
11
]
12
},
13
{
14
"type": "message",
15
"role": "user",
16
"content": [
17
{
18
"type": "input_text",
19
"text": "That's not bad, but the diagram is missing the bike shed."
20
}
21
]
22
}
23
]

Omdat we een conversatie voortzetten, neemt de lengte van de input die we naar de Responses API sturen steeds verder toe:

Diagram met het label 'Snapshot 3' dat de laatste fase van een AI-agent loop toont. Na ontvangst van de resultaten van de tool genereert het model een afsluitende gedachte en een definitief antwoord voor de gebruiker. Pijlen tonen de overgang van tool-output naar de voltooide respons.

Laten we eens kijken wat deze alsmaar groeiende prompt betekent voor de prestaties.

Prestatieoverwegingen

Misschien vraag je je af: "Wacht even, verloopt de groei van de hoeveelheid JSON die tijdens een conversatie naar de Responses API wordt gestuurd niet kwadratisch binnen de agent loop?" En daar heb je gelijk in. Hoewel de Responses API een optionele parameter previous_response_id(opent in een nieuw venster) ondersteunt om dit probleem te verhelpen, maakt Codex daar momenteel geen gebruik van. De belangrijkste redenen zijn om verzoeken volledig stateless te houden en om configuraties voor Zero Data Retention (ZDR) te ondersteunen.

Het vermijden van previous_response_id vereenvoudigt de zaken voor de aanbieder van de Responses API, omdat het garandeert dat elk verzoek stateless is. Dit maakt het ook eenvoudig om klanten te ondersteunen die hebben gekozen voor Zero Data Retention (ZDR)(opent in een nieuw venster), aangezien het opslaan van de data die nodig is voor previous_response_id strijdig zou zijn met ZDR. Let wel: ZDR-klanten hoeven niet in te leveren op de voordelen van propriëtaire redeneerberichten uit eerdere turns, omdat de bijbehorende encrypted_content op de server ontsleuteld kan worden. (OpenAI bewaart wel de decryptiesleutel van een ZDR-klant, maar niet hun data.) Zie PR's #642(opent in een nieuw venster) en #1641(opent in een nieuw venster) voor de gerelateerde wijzigingen in Codex ter ondersteuning van ZDR.

Over het algemeen wegen de kosten voor het samplen van het model zwaarder dan de kosten van het netwerkverkeer. Daarom richten we onze efficiëntie-inspanningen vooral op het samplen. Dit is waarom prompt caching zo belangrijk is: het stelt ons in staat om rekenwerk van een eerdere inferentie-aanroep te hergebruiken. Wanneer we cache hits krijgen, verloopt het samplen van het model lineair in plaats van kwadratisch. Onze documentatie over prompt caching(opent in een nieuw venster) legt dit in detail uit:

Cache hits zijn alleen mogelijk bij exacte prefix matches binnen een prompt. Om te profiteren van caching, moet je statische inhoud (zoals instructies en voorbeelden) aan het begin van je prompt plaatsen en variabele inhoud (zoals gebruikersspecifieke informatie) aan het einde. Dit geldt ook voor afbeeldingen en tools: deze moeten identiek zijn tussen de verschillende verzoeken.

Met dit in het achterhoofd kijken we naar welke bewerkingen in Codex een 'cache miss' kunnen veroorzaken:

  • Het wijzigen van de beschikbare tools voor het model midden in een conversatie.
  • Het wijzigen van het model waarop het verzoek aan de Responses API is gericht (in de praktijk verandert dit het derde item in de oorspronkelijke prompt, omdat daar modelspecifieke instructies staan).
  • Het wijzigen van de sandbox-configuratie, de goedkeuringsmodus of de huidige werkmap.

Het Codex-team moet zorgvuldig zijn bij het introduceren van nieuwe functies in de Codex CLI die prompt caching kunnen ondermijnen. Zo introduceerde onze initiële ondersteuning voor MCP-tools een bug waarbij de tools niet in een consistente volgorde werden aangeboden(opent in een nieuw venster), wat leidde tot cache misses. Let op: MCP-tools kunnen extra lastig zijn, omdat MCP-servers de lijst met aangeboden tools on the fly kunnen wijzigen via een notifications/tools/list_changed(opent in een nieuw venster)-melding. Het verwerken van zo'n wijziging midden in een lang gesprek kan een kostbare cache miss veroorzaken.

Waar mogelijk verwerken we configuratiewijzigingen halverwege een conversatie door een nieuw bericht aan de input toe te voegen dat de wijziging weerspiegelt, in plaats van een eerder bericht aan te passen:

  • Als de sandbox-configuratie of goedkeuringsmodus verandert, voegen(opent in een nieuw venster) we een nieuw bericht met role=developer toe in hetzelfde formaat als het oorspronkelijke <permissions instructions>-item.
  • Als de huidige werkmap verandert, voegen(opent in een nieuw venster) we een nieuw bericht met role=user toe in hetzelfde formaat als de oorspronkelijke <environment_context>.

We doen er alles aan om cache hits te garanderen voor optimale prestaties. Er is echter nog een belangrijke bron die we moeten beheren: het contextvenster.

Onze algemene strategie om te voorkomen dat het contextvenster vol raakt, is het comprimeren van de conversatie zodra het aantal tokens een bepaalde drempel overschrijdt. Concreet vervangen we de input door een nieuwe, kortere lijst met items die representatief is voor de conversatie. Hierdoor kan de agent verdergaan met behoud van inzicht in wat er tot nu toe is gebeurd. Een vroege implementatie van deze compressie(opent in een nieuw venster) vereiste dat de gebruiker handmatig het commando /compact uitvoerde. Dit stuurde een verzoek naar de Responses API met de bestaande conversatie plus instructies om samen te vatten(opent in een nieuw venster). Codex gebruikte vervolgens het antwoord met de samenvatting als de nieuwe input(opent in een nieuw venster) voor het vervolg van de conversatie.

Sindsdien is de Responses API doorontwikkeld en ondersteunt nu een speciaal /responses/compact-endpoint(opent in een nieuw venster) dat compaction efficiënter uitvoert. Het retourneert een lijst met items(opent in een nieuw venster) die de vorige input vervangt, zodat het gesprek kan worden voortgezet terwijl er ruimte in het contextvenster wordt vrijgemaakt. Deze lijst bevat een speciaal item met type=compaction en een onleesbaar encrypted_content -veld, waarin het impliciete begrip van het model over de oorspronkelijke conversatie bewaard blijft. Codex gebruikt dit endpoint nu automatisch om de conversatie te comprimeren wanneer de auto_compact_limit(opent in een nieuw venster) wordt overschreden.

Wat volgt

We hebben de Codex agent loop geïntroduceerd en laten zien hoe Codex zijn context opbouwt en beheert bij het bevragen van een model. Onderweg hebben we praktische overwegingen en best practices belicht die relevant zijn voor iedereen die een agent loop bouwt bovenop de Responses API.

Hoewel de agent loop de fundering vormt voor Codex, is dit slechts het begin. In komende artikelen duiken we dieper in de architectuur van de CLI, onderzoeken we hoe toolgebruik is geïmplementeerd en nemen we het sandboxing-model van Codex onder de loep.

Auteur

Michael Bolin

Dankbetuigingen

Speciale dank aan het hele team dat de Codex CLI heeft gebouwd.