OpenAI

23 בינואר 2026

הנדסה

Unrolling the Codex agent loop

By Michael Bolin, Member of Technical Staff

בטעינה…

Codex CLI⁠(נפתח בחלון חדש) is our cross-platform local software agent, designed to produce high-quality, reliable software changes while operating safely and efficiently on your machine. We’ve learned a tremendous amount about how to build a world-class software agent since we first launched the CLI in April⁠. To unpack those insights, this is the first post in an ongoing series where we’ll explore various aspects of how Codex works, as well as hard-earned lessons. (For an even more granular view on how the Codex CLI is built, check out our open source repository at https://github.com/openai/codex⁠(נפתח בחלון חדש). Many of the finer details of our design decisions are memorialized in GitHub issues and pull requests if you’d like to learn more.)

To kick off, we’ll focus on the agent loop, which is the core logic in Codex CLI that is responsible for orchestrating the interaction between the user, the model, and the tools the model invokes to perform meaningful software work. We hope this post gives you a good view into the role our agent (or “harness”) plays in making use of an LLM.

Before we dive in, a quick note on terminology: at OpenAI, “Codex” encompasses a suite of software agent offerings, including Codex CLI, Codex Cloud, and the Codex VS Code extension. This post focuses on the Codex harness, which provides the core agent loop and execution logic that underlies all Codex experiences and is surfaced through the Codex CLI. For ease here, we’ll use the terms “Codex” and “Codex CLI” interchangeably.

The agent loop

At the heart of every AI agent is something called “the agent loop.” A simplified illustration of the agent loop looks like this:

תרשים שכותרתו ״לולאת הסוכן״, הממחיש כיצד מערכת בינה מלאכותית מעבדת בקשת משתמש, קוראת לכלים, בוחנת תוצאות, מעדכנת את התוכנית שלה ומחזירה פלטים. חיצים מחברים בין שלבים כגון קלט משתמש, reasoning של המודל, פעולות כלים ותגובה סופית.

To start, the agent takes input from the user to include in the set of textual instructions it prepares for the model known as a prompt.

The next step is to query the model by sending it our instructions and asking it to generate a response, a process known as inference. During inference, the textual prompt is first translated into a sequence of input tokens⁠(נפתח בחלון חדש)—integers that index into the model’s vocabulary. These tokens are then used to sample the model, producing a new sequence of output tokens.

The output tokens are translated back into text, which becomes the model’s response. Because tokens are produced incrementally, this translation can happen as the model runs, which is why many LLM-based applications display streaming output. In practice, inference is usually encapsulated behind an API that operates on text, abstracting away the details of tokenization.

As the result of the inference step, the model either (1) produces a final response to the user’s original input, or (2) requests a tool call that the agent is expected to perform (e.g., “run ls and report the output”). In the case of (2), the agent executes the tool call and appends its output to the original prompt. This output is used to generate a new input that’s used to re-query the model; the agent can then take this new information into account and try again.

This process repeats until the model stops emitting tool calls and instead produces a message for the user (referred to as an assistant message in OpenAI models). In many cases, this message directly answers the user’s original request, but it may also be a follow-up question for the user.

Because the agent can execute tool calls that modify the local environment, its “output” is not limited to the assistant message. In many cases, the primary output of a software agent is the code it writes or edits on your machine. Nevertheless, each turn always ends with an assistant message—such as “I added the architecture.md you asked for”—which signals a termination state in the agent loop. From the agent’s perspective, its work is complete and control returns to the user.

The journey from user input to agent response shown in the diagram is referred to as one turn of a conversation (a thread in Codex). Though this conversation turn can include many iterations between the model inference and tool calls. Every time you send a new message to an existing conversation, the conversation history is included as part of the prompt for the new turn, which includes the messages and tool calls from previous turns:

תרשים שכותרתו ״לולאת סוכן מרובת תורים״, המציג כיצד סוכן בינה מלאכותית מקבל קלט מהמשתמש, מפיק פעולות, נעזר בכלים, מעדכן מצב ומחזיר תוצאות באופן מחזורי. כולל שלבים מסומנים, חיצים ודוגמאות לפלטי כלים, הממחישים את מחזור ה-reasoning של הסוכן.

This means that as the conversation grows, so does the length of the prompt used to sample the model. This length matters because every model has a context window, which is the maximum number of tokens it can use for one inference call. Note this window includes both input and output tokens. As you might imagine, an agent could decide to make hundreds of tool calls in a single turn, potentially exhausting the context window. For this reason, context window management is one of the agent’s many responsibilities. Now, let’s dive in to see how Codex runs the agent loop.

Model inference

The Codex CLI sends HTTP requests to the Responses API⁠(נפתח בחלון חדש) to run model inference. We’ll examine how information flows through Codex, which uses the Responses API to drive the agent loop.

The Responses API endpoint that the Codex CLI uses is configurable⁠(נפתח בחלון חדש), so it can be used with any endpoint that implements the Responses API⁠(נפתח בחלון חדש):

Let’s explore how Codex creates the prompt for the first inference call in a conversation.

Building the initial prompt

As an end user, you don’t specify the prompt used to sample the model verbatim when you query the Responses API. Instead, you specify various input types as part of your query, and the Responses API server decides how to structure this information into a prompt that the model is designed to consume. You can think of the prompt as a “list of items”; this section will explain how your query gets transformed into that list.

In the initial prompt, every item in the list is associated with a role. The role indicates how much weight the associated content should have and is one of the following values (in decreasing order of priority): system, developer, user, assistant.

The Responses API⁠(נפתח בחלון חדש) takes a JSON payload with many parameters. We’ll focus on these three:

In Codex, the instructions field is read from the model_instructions_file⁠(נפתח בחלון חדש) in ~/.codex/config.toml, if specified; otherwise, the base_instructions associated with a model⁠(נפתח בחלון חדש) are used. Model-specific instructions live in the Codex repo and are bundled into the CLI (e.g., gpt-5.2-codex_prompt.md⁠(נפתח בחלון חדש)).

The tools field is a list of tool definitions that conform to a schema defined by the Responses API. For Codex, this includes tools that are provided by the Codex CLI, tools that are provided by the Responses API that should be made available to Codex, as well as tools provided by the user, usually 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
]

Finally, the input field of the JSON payload is a list of items. Codex inserts the following items⁠(נפתח בחלון חדש) into the input before adding the user message:

1. A message with role=developer that describes the sandbox that applies only to the Codex-provided shell tool defined in the tools section. That is, other tools, such as those provided from MCP servers, are not sandboxed by Codex and are responsible for enforcing their own guardrails.

The message is built from a template where the key pieces of content come from snippets of Markdown bundled into the Codex CLI, such as workspace_write.md⁠(נפתח בחלון חדש) and on_request.md⁠(נפתח בחלון חדש):

טקסט פשוט

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. (Optional) A message with role=developer whose contents are the developer_instructions value read from the user’s config.toml file.

3. (Optional) A message with role=user whose contents are the “user instructions,” which are not sourced from a single file but are aggregated across multiple sources⁠(נפתח בחלון חדש). In general, more specific instructions appear later:

4. A message with role=user that describes the local environment in which the agent is currently operating. This specifies the current working directory and the user’s shell⁠(נפתח בחלון חדש):

טקסט פשוט

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

לאחר ש-Codex מסיים את כל החישובים שלעיל לאתחול input, הוא מצרף את הודעת המשתמש כדי להתחיל את השיחה.

הדוגמאות הקודמות התמקדו בתוכן של כל הודעה, אך שימו לב שכל איבר ב-input הוא אובייקט JSON עם type, ‏role⁠(נפתח בחלון חדש) ו-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
}

לאחר בניית מטען ה-JSON המלא לשליחה ל-Responses API, ‏Codex שולח בקשת HTTP POST עם כותרת Authorization בהתאם להגדרת ה-endpoint של Responses API ב-~/.codex/config.toml (כותרות HTTP ופרמטרים נוספים לשאילתה מתווספים אם הוגדרו).

כששרת Responses API של OpenAI מקבל את הבקשה, הוא משתמש ב-JSON כדי ליצור את הפרומפט למודל באופן הבא (כמובן, מימוש מותאם של Responses API עשוי לפעול אחרת):

תרשים תמונת מצב המציג שלב אחד בלולאה של סוכן בינה מלאכותית. בקשת משתמש נכנסת למודל, והוא מפיק מחשבה, פעולה עם שם של כלי וקלט לכלי. התרשים מדגיש את שלב הביניים הזה של ה-reasoning לפני הקריאה לכלי.

כפי שאפשר לראות, סדר שלושת הפריטים הראשונים בפרומפט נקבע על ידי השרת, ולא על ידי הלקוח. עם זאת, מבין שלושתם, רק התוכן של הודעת המערכת נשלט גם הוא על ידי השרת, שכן tools ו-instructions נקבעים על ידי הלקוח. אחריהם מגיע input ממטען ה-JSON, שמשלים את הפרומפט.

עכשיו, כשיש לנו פרומפט, אנחנו מוכנים לדגום מהמודל.

התור הראשון

בקשת ה-HTTP הזאת ל-Responses API מתחילה את ה״תור״ הראשון בשיחה ב-Codex. השרת משיב בזרם אירועים הנשלחים מהשרת (SSE⁠(נפתח בחלון חדש)). השדה data של כל אירוע הוא מטען JSON עם "type" שמתחיל ב-"response", ועשוי להיראות כך (רשימת האירועים המלאה נמצאת בתיעוד ה-API⁠(נפתח בחלון חדש) שלנו):

טקסט פשוט

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 קולט את זרם האירועים⁠(נפתח בחלון חדש) ומפרסם אותם מחדש כאובייקטי אירועים פנימיים שלקוח יכול להשתמש בהם. אירועים כמו response.output_text.delta משמשים לתמיכה בהזרמה בממשק המשתמש, ואילו אירועים אחרים, כמו response.output_item.added, מומרים לאובייקטים שמצורפים ל-input בקריאות הבאות ל-Responses API.

נניח שהבקשה הראשונה ל-Responses API כוללת שני אירועי response.output_item.done: אחד עם type=reasoning ואחד עם type=function_call. האירועים האלה חייבים להיכלל בשדה input של ה-JSON כשפונים שוב למודל עם התגובה לקריאה לכלי: 

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
]

הפרומפט שיתקבל וישמש לדגימה מהמודל בבקשה הבאה ייראה כך:

תרשים שכותרתו ״תמונת מצב 2״, המציג סוכן בינה מלאכותית לאחר קריאה לכלי. המודל מקבל תצפית מכלי ומפיק מחשבה ופעולה חדשות. חיצים מחברים בין קלטים, תצפיות ופלטים וממחישים כיצד הסוכן חוזר על לולאת ה-reasoning שלו.

שימו לב במיוחד לכך שהפרומפט הישן הוא תחילית זהה לחלוטין של הפרומפט החדש. זה מכוון: כך הבקשות הבאות יעילות הרבה יותר, משום שאפשר לנצל מטמון פרומפטים (שעליו נדון בחלק הבא, העוסק בביצועים).

אם נחזור לתרשים הראשון של לולאת הסוכן, נראה שייתכנו חזרות רבות בין הסקה לבין קריאה לכלים. הפרומפט עשוי להמשיך לגדול עד שלבסוף נקבל הודעת עוזר שמציינת את סוף התור:

טקסט פשוט

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

ב-Codex CLI, אנחנו מציגים למשתמש את הודעת העוזר ומעבירים את המיקוד לשדה כתיבת ההודעה, כדי לסמן שהגיע ״תורו״ להמשיך את השיחה. אם המשתמש משיב, יש לצרף גם את הודעת העוזר מהתור הקודם וגם את הודעת המשתמש החדשה ל-input בבקשת Responses API כדי להתחיל את התור החדש:

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
]

שוב, מכיוון שאנחנו ממשיכים שיחה קיימת, ה-input שאנחנו שולחים ל-Responses API הולך ומתארך:

תרשים שכותרתו ״תמונת מצב 3״, המציג את השלב האחרון בלולאה של סוכן בינה מלאכותית. לאחר קבלת תוצאות הכלי, המודל מפיק מחשבה מסכמת ותשובה סופית המוחזרת למשתמש. החיצים ממחישים את המעבר מפלט הכלי לתגובה המוגמרת.

נבחן כיצד הפרומפט ההולך וגדל משפיע על הביצועים.

שיקולי ביצועים

אולי אתם שואלים את עצמכם: ״רגע, האם כמות ה-JSON שנשלחת ל-Responses API לאורך השיחה בלולאת הסוכן לא גדלה באופן ריבועי?״ ואתם צודקים. אומנם Responses API תומך בפרמטר האופציונלי previous_response_id⁠(נפתח בחלון חדש) לצמצום הבעיה, אך Codex אינו משתמש בו כיום, בעיקר כדי שהבקשות לא יהיו תלויות כלל במצב שמור וכדי לתמוך בתצורות אפס שמירת נתונים (ZDR).

הימנעות משימוש ב-previous_response_id מפשטת את העבודה עבור ספק ה-Responses API, משום שהיא מבטיחה שכל בקשה אינה תלויה במצב שמור. כך גם קל יותר לתמוך בלקוחות שבחרו ב-אפס שמירת נתונים (ZDR)⁠(נפתח בחלון חדש), שכן שמירת הנתונים הנדרשים לתמיכה ב-previous_response_id הייתה מנוגדת ל-ZDR. שימו לב שלקוחות ZDR אינם מוותרים על האפשרות להפיק תועלת מהודעות reasoning קנייניות מתורים קודמים, שכן אפשר לפענח בשרת את ה-encrypted_content המשויך אליהן. (OpenAI שומרת את מפתח הפענוח של לקוח ZDR, אך לא את הנתונים שלו.) לשינויים הרלוונטיים ב-Codex לצורך תמיכה ב-ZDR, ראו את ה-pull requests ‏#642⁠(נפתח בחלון חדש) ו-#1641⁠(נפתח בחלון חדש).

ככלל, עלות הדגימה מהמודל גבוהה בהרבה מעלות תעבורת הרשת, ולכן מאמצי הייעול שלנו מתמקדים בדגימה. לכן מטמון פרומפטים חשוב כל כך: הוא מאפשר לנו לעשות שימוש חוזר בחישובים מקריאת הסקה קודמת. כשנמצאת התאמה במטמון, סיבוכיות הדגימה מהמודל היא ליניארית ולא ריבועית. בתיעוד שלנו בנושא מטמון פרומפטים ⁠(נפתח בחלון חדש)מופיע הסבר מפורט יותר:

התאמות במטמון אפשריות רק כשיש התאמה מדויקת של תחילית בפרומפט. כדי ליהנות מיתרונות המטמון, הציבו תוכן קבוע, כמו הוראות ודוגמאות, בתחילת הפרומפט, ותוכן משתנה, כמו מידע ייחודי למשתמש, בסופו. זה נכון גם לתמונות ולכלים, שחייבים להיות זהים בין הבקשות.

בהתאם לכך, נבחן אילו פעולות עלולות לגרום ל״החטאת מטמון״ ב-Codex:

  • שינוי ה-tools הזמינים למודל באמצע השיחה.

  • שינוי ה-model שאליו מופנית בקשת Responses API (בפועל, זה משנה את הפריט השלישי בפרומפט המקורי, כי הוא מכיל הוראות ייעודיות למודל).

  • שינוי תצורת ארגז החול, מצב האישור או תיקיית העבודה הנוכחית.

צוות Codex חייב להקפיד במיוחד בעת הוספת תכונות ל-Codex CLI שעלולות לפגוע במטמון פרומפטים. למשל, התמיכה הראשונית שלנו בכלי MCP הכניסה באג שבגללו לא פירטנו את הכלים בסדר עקבי⁠(נפתח בחלון חדש), מה שגרם להחטאות מטמון. שימו לב שכלי MCP עלולים להיות מורכבים במיוחד בהקשר הזה, כי שרתי MCP יכולים לשנות את רשימת הכלים שלהם בזמן אמת באמצעות הודעת notifications/tools/list_changed⁠(נפתח בחלון חדש). יישום העדכון שבנוטיפיקציה הזאת באמצע שיחה ארוכה עלול לגרום להחטאת מטמון יקרה.

ככל האפשר, אנחנו מטפלים בשינויי תצורה באמצע השיחה באמצעות הוספת הודעה חדשה ל-input שמשקפת את השינוי, במקום לשנות הודעה קודמת:

  • אם תצורת ארגז החול או מצב האישור משתנים, אנחנו מוסיפים⁠(נפתח בחלון חדש) הודעת role=developer חדשה באותו פורמט של הפריט המקורי <permissions instructions>.

  • אם תיקיית העבודה הנוכחית משתנה, אנחנו מוסיפים⁠(נפתח בחלון חדש) הודעת role=user חדשה באותו פורמט של <environment_context> המקורי.

אנחנו משקיעים מאמצים רבים כדי להבטיח התאמות במטמון לטובת הביצועים. יש משאב מרכזי נוסף שעלינו לנהל: חלון ההקשר.

האסטרטגיה הכללית שלנו למניעת מילוי חלון ההקשר היא לצמצם את השיחה ברגע שמספר הטוקנים חוצה סף מסוים. ליתר דיוק, אנחנו מחליפים את input ברשימת פריטים חדשה וקטנה יותר שמייצגת את השיחה, כדי שהסוכן יוכל להמשיך מתוך הבנה של מה שקרה עד כה. מימוש מוקדם של compaction⁠(נפתח בחלון חדש) דרש מהמשתמש להפעיל ידנית את הפקודה /compact, שפנתה ל-Responses API עם השיחה הקיימת והוראות מותאמות לסיכום⁠(נפתח בחלון חדש). Codex השתמש בהודעת העוזר שהתקבלה והכילה את הסיכום בתור הקלט החדש — input⁠(נפתח בחלון חדש) — לתורים הבאים בשיחה.

מאז, Responses API התפתח וכעת תומך ב-endpoint /responses/compact⁠(נפתח בחלון חדש) ייעודי שמבצע compaction ביעילות רבה יותר. הוא מחזיר רשימת פריטים⁠(נפתח בחלון חדש) שאפשר להשתמש בה במקום ה-input הקודם כדי להמשיך את השיחה ולפנות מקום בחלון ההקשר. הרשימה כוללת פריט מיוחד מסוג type=compaction עם פריט encrypted_content אטום, ששומר את ההבנה הסמויה של המודל לגבי השיחה המקורית. כיום, Codex משתמש אוטומטית ב-endpoint הזה לצמצום השיחה כשחורגים מ-auto_compact_limit⁠(נפתח בחלון חדש).

בהמשך הסדרה

הצגנו את לולאת הסוכן של Codex וסקרנו כיצד Codex בונה ומנהל את ההקשר שלו בעת פנייה למודל. לאורך הדרך הדגשנו שיקולים מעשיים ושיטות עבודה מומלצות שרלוונטיים לכל מי שבונה לולאת סוכן על גבי Responses API.

לולאת הסוכן אומנם מספקת את הבסיס ל-Codex, אך זו רק ההתחלה. בפוסטים הבאים נעמיק בארכיטקטורת ה-CLI, נבחן כיצד ממומש השימוש בכלים ונתבונן מקרוב במודל הבידוד בארגז חול של Codex.

מחבר/ת

Michael Bolin

תודות

תודה מיוחדת לכל הצוות שבנה את Codex CLI.