Share on X

Writing / AI Agent

N° 03

Inside the Codex Agent Loop: Separating Model Reasoning from Tool Execution

This article decomposes the mechanism that lets Codex CLI inspect, edit, test, and revise software into four responsibilities: model, harness, tools, and environment. It follows the tool-call boundary through execution feedback, permissions, MCP, termination, and context management.

Introduction: Where Does Codex “Think,” and Where Does It Execute?

Give Codex CLI a bug-fixing task and you may see a sequence like this:

plaintext
Inspect files
↓
Change code
↓
Run tests
↓
Observe a failure
↓
Revise the change
↓
Run tests again

It is easy to perceive this as one intelligence reading the repository, executing commands, and interpreting the results. Saying “Codex runs a command” is convenient, but it hides an important architectural boundary.

The model at the center generates its next output from the context it receives. It does not itself reach into the local filesystem or start an operating-system process. The surrounding harness interprets the model’s tool call, executes it in a real environment, and returns the result as new model input. The model then decides what to do next.

Codex can modify software not because the model directly executes commands, but because the model emits a structured description of the next action, the harness performs that action, and the result is fed back to the model. This repetition is the Agent Loop.

This article focuses on the execution model of the shared Codex harness as it can primarily be observed through Codex CLI, rather than attempting to describe every Codex product surface. It assumes familiarity with LLMs, JSON, command-line programs, exit codes, Git, tests, and function calling. Details of the Responses API and streaming are introduced only where they help explain the loop.

The Four Layers: Model, Harness, Tools, and Environment

The Agent Loop becomes easier to reason about when “Codex” is not treated as a single box. At minimum, it helps to separate four layers: the model, the harness, the tools, and the environment.

The model

The model is responsible for decisions. It reads the request and available evidence, forms hypotheses, chooses which file to inspect or which tool to use, and generates tool arguments. After receiving a result, it can revise its plan. When it judges the task complete, it generates a final response for the user.

The model itself is not responsible for starting OS processes, directly writing files, opening network connections, enforcing permissions, or terminating timed-out commands. Emitting a desire to produce a side effect and actually producing that side effect are separate operations.

The harness

The harness is the runtime that turns a model into an agent. It assembles model input, registers tools and their schemas, calls the API, and consumes streamed events. When the model returns a tool call, the harness dispatches it to an implementation, applies permissions, approval rules, and sandboxing, and formats the result for the next model invocation.

Conversation history, context compaction, thread persistence, configuration, authentication, cancellation, and progress events for the UI are also harness concerns. Two agents using the same model can behave differently because their harnesses differ.

Tools

A tool is the interface connecting a model decision to an external capability. Examples include a shell, a file editor, web search, planning updates, an MCP server, or an internal service API.

The model usually receives more than a tool name. It sees a schema containing a description, arguments, types, required fields, and constraints. It treats that schema as a capability catalog and emits which tool should be called with which arguments.

The environment

The environment is what tools inspect or modify: the repository, filesystem, Git working tree, installed commands, test runner, network, environment variables, and credentials.

The relationship can be summarized in one sentence:

The model makes decisions, the harness provides capabilities and constraints, and tools observe or modify the environment.

plaintext
┌─────────────┐
│    User     │
└──────┬──────┘
       │ request
       ▼
┌─────────────┐
│   Harness   │
│ build input │
└──────┬──────┘
       │ instructions / tools / input
       ▼
┌─────────────┐
│    Model    │
│  decision   │
└──────┬──────┘
       │ function call or final response
       ▼
┌─────────────┐
│   Harness   │
│ check policy│
│ execute tool│
└──────┬──────┘
       ▼
┌─────────────┐
│ Environment │
└──────┬──────┘
       │ stdout / error / diff
       └───────────────→ next model inference

The Agent Loop in Minimal Pseudocode

With implementation details removed, the Agent Loop looks like this:

python
history = build_initial_context(
    instructions=instructions,
    tools=available_tools,
    user_message=user_message,
)

while True:
    response = model.infer(history)

    if response.has_final_message():
        return response.final_message

    for tool_call in response.tool_calls:
        result = harness.execute(tool_call)
        history.append(tool_call)
        history.append(result)

    history = compact_if_needed(history)

The important distinction is that model.infer and harness.execute are different operations. The first generates an answer or action request from context. The second creates a side effect in a real environment and returns an observation.

A production harness adds streaming, multiple response items, approval waits, timeouts, output truncation, tool failures, cancellation, context compaction, and UI events. The core remains the same: infer, execute when necessary, append the result, and infer again.

Before the First Inference: What the Harness Sends to the Model

The sentence typed by the user is not the entire prompt received by the model. Conceptually, the Codex harness builds the initial input from information such as:

  • Base instructions for the model
  • Available tools and their schemas
  • Sandbox, approval, and permission guidance
  • Project instructions such as AGENTS.md
  • Information about Skills
  • The working directory and active shell
  • Earlier messages, tool calls, and tool results
  • The current user request

OpenAI’s description of the Codex Agent Loop explains that input is primarily organized around the Responses API’s instructions, tools, and input, with Codex also supplying information such as permissions, project instructions, Skills, the working directory, and the shell. The same user sentence can therefore lead to a different first action when the repository, configuration, or available toolset changes.

A tool definition is both a capability and a prompt

A shell tool might be described to the model with a contract conceptually similar to this:

json
{
  "name": "shell",
  "description": "Run a command in the project environment",
  "parameters": {
    "type": "object",
    "properties": {
      "command": {
        "type": "array",
        "items": { "type": "string" }
      },
      "workdir": { "type": "string" }
    },
    "required": ["command", "workdir"]
  }
}

If the description is vague, the arguments are unnecessarily complex, or the meaning of the result is unclear, the model is less likely to use the tool well. A focused tool with an explicit purpose and constraints makes the action space easier to navigate. Tool design is not merely API design; it is also prompt design that teaches the model how a capability should be used.

The same model does not guarantee the same agent performance

Behavior changes with the set of tools, their descriptions, environment metadata, project instructions, retained history, and output truncation rules. A harness that cannot reach the test command cannot verify a fix. One that feeds an enormous unfiltered log back to the model may bury the most relevant evidence.

This is why agent quality cannot be reduced to model quality. The model is the central decision-maker, but the harness determines what the model can observe and which actions it can take.

The Reasoning–Execution Boundary: Tool Calls as a Contract

A model response can be viewed as taking one of two broad forms: a final message for the user or a tool call.

If it is a final message, the harness normally returns control to the user for that turn.

plaintext
The fix is complete, and the relevant tests pass.

If it is a tool call, the model returns a structured execution request.

json
{
  "type": "function_call",
  "name": "shell",
  "arguments": {
    "command": ["rg", "UserService", "src"],
    "workdir": "/workspace/project"
  },
  "call_id": "call_123"
}

At this point, rg has not run. The model has only requested that a tool named shell be called with those arguments. Specific field names may change as APIs and tool implementations evolve, but the durable principle is the separation of inference and execution through a structured request.

How the harness turns a request into execution

When the harness receives a tool call, it conceptually performs the following work:

  1. Resolve the tool name to a registered implementation.
  2. Parse the arguments and validate them against the schema.
  3. Check policies for the command, path, network, or other capability.
  4. If required, ask for user approval and pause the turn until a response arrives.
  5. Run the tool in the permitted environment or sandbox.
  6. Collect stdout, stderr, exit status, diffs, exceptions, or other results.
  7. Format the outcome as a tool result suitable for model input.

This split has practical value. Permissions can be checked before a side effect, dangerous actions can be denied, and audit information can be retained. Tool implementations can be replaced, and the same model can be attached to different environments.

Do not confuse a model’s claim with a policy decision

A model saying “this command is safe” does not grant permission. The model is subject to policy; it should not be the final enforcement mechanism. Allowed paths, network access, write scopes, and approval requirements should be enforced outside the model by the harness or execution layer.

Otherwise, a natural-language restriction is being entrusted to the same natural-language system constrained by it. A security boundary should be verifiable and difficult to bypass.

A shell and an MCP tool do not necessarily share a security boundary

The sandbox around a Codex-provided shell tool should not be assumed to govern an external MCP server. OpenAI’s Agent Loop explanation notes that the Codex permission discussion directly applies to the Codex-provided shell tool, while external tools such as those exposed by MCP need their own guardrails.

If an MCP tool can write to a production database, a filesystem sandbox around the shell does not control that side effect. The MCP server needs its own authentication, authorization, input validation, operation scoping, auditing, and approval mechanisms where appropriate.

Waiting for approval pauses the loop; it does not complete it

When an action needs user approval, the Agent Loop is not finished. The harness reports an approval request to the UI, waits for an answer, and uses the approval or denial when handling the tool call. OpenAI’s description of the Codex App Server explains a bidirectional mechanism in which the server can request client approval and pause the turn until the user responds.

Tool Results Are “Observations” for the Next Inference

After executing a tool, the harness appends a result associated with the original call. Conceptually, it may look like this:

json
{
  "type": "function_call_output",
  "call_id": "call_123",
  "output": "src/service/UserService.java:42: public User createUser(...)"
}

The call_id associates the observation with the request that produced it. The model is then invoked again with the updated context.

The key is to treat a tool result as an observation. The model does not begin with the current repository contents, latest Git diff, or actual test result embedded inside it. External state becomes visible through tools.

plaintext
Hypothesis: UserService lacks validation
  ↓
Run a search
  ↓
No matching validation is found
  ↓
Revise the hypothesis
  ↓
Inspect Controller and DTO conversion

Failure is also information

A tool does not need to exit successfully to help the loop. Exit status 1, a compiler error, a failed test, a missing file, a permission denial, a timeout, or an unavailable command can all inform the next decision.

A failed test is not necessarily a failed Agent Loop. It may be a high-quality observation that falsifies an implementation hypothesis. If the harness preserves the error, exit status, and relevant output, the model can reconsider the change or the test assumptions.

An observation is not absolute truth

A tool result may be truncated, based on too narrow a search, produced by an incorrect command, affected by a cache, limited to a subset of tests, or generated in an environment unlike production.

“Command succeeded” and “goal achieved” are therefore not equivalent. The model must consider the scope and reliability of the observation and, when necessary, verify it with another tool or broader test. The harness should make truncation, exit status, and execution scope visible rather than ambiguous.

Worked Example: Following a Bug Fix One Loop at a Time

Consider this request:

plaintext
Fix the bug that allows users to register with an empty username,
and add the relevant tests.

Step 1: Explore

At the first inference, the model does not yet know where the change belongs. It may choose a search.

json
{
  "name": "shell",
  "arguments": {
    "command": ["rg", "createUser|UserService", "src"],
    "workdir": "/workspace/project"
  }
}

The harness checks policy, starts rg, and returns stdout and the exit status. File paths and line numbers become evidence for the next inference.

Step 2: Read the candidate code

If the search identifies UserService and UserController, the model may request a bounded read.

bash
sed -n '1,240p' src/main/java/example/UserService.java

Only after that output enters the context does the model know what this version of the file contains. The contents of unread files remain hypotheses.

Step 3: Edit

From the observed code and the request, the model decides whether whitespace-only names should also be rejected, whether an existing exception contract must be preserved, and whether validation belongs in the service or DTO layer. It then emits a call to an editing tool.

The editing tool, not the model, produces the side effect. The harness checks the target path and permissions, applies the change, and returns a result or diff to the history.

Step 4: Test

The model next requests the relevant test command.

bash
./gradlew test --tests UserServiceTest

Suppose the result is:

plaintext
Expected IllegalArgumentException but no exception was thrown

This is not necessarily the end of the task. It is a new observation.

Step 5: Revise after falsification

The model can compare several hypotheses:

  • The changed method is not on the actual registration path.
  • A controller or mapper transforms the input.
  • The test constructs a different service implementation.
  • The validation condition or test expectation is wrong.

It requests more searches or file reads to gather evidence. The essential behavior is not “execute the original plan to completion,” but “update the plan when the external state contradicts it.”

Step 6: Edit again and verify again

If the actual path uses another method, the model requests an updated edit and reruns the focused test. It may then run a broader related test set, static analysis, a build, or a Git diff review.

plaintext
User      Harness       Model        Tool        Repository
 |           |             |            |              |
 | request   |             |            |              |
 |---------->| inference   |            |              |
 |           |------------>|            |              |
 |           | tool call   |            |              |
 |           |<------------|            |              |
 |           | execute ---------------->|              |
 |           | result <-----------------|              |
 |           | inference + result       |              |
 |           |------------>|            |              |
 |           | edit call   |            |              |
 |           |<------------| apply ------------------->|
 |           | test ------------------->|              |
 |           | fail <-------------------|              |
 |           | inference   |            |              |
 |           |------------>|            |              |
 |           |       ...repeat...       |              |

Step 7: Return a final response

When the model judges the evidence sufficient, it emits a user-facing response instead of another tool request.

plaintext
Usernames that are empty or contain only whitespace are now rejected.
I added the related cases to UserServiceTest, and the focused tests pass.

That output normally ends the turn. The test-success event itself did not mechanically terminate the loop.

When Does the Agent Loop End?

The basic termination condition is that the model produces a final assistant message instead of another tool call. The Agent Loop is therefore not “a loop guaranteed to run until the answer is correct.” It runs while the model continues to request actions and the harness can continue handling them.

In practice, a turn can also stop or be interrupted because:

  • The model judges the work complete.
  • A question or confirmation from the user is required.
  • The user cancels the task.
  • A required permission or approval is denied.
  • A required tool is missing or unavailable.
  • A timeout, limit, or unrecoverable error is reached.

A passing test does not automatically prove completion, and a failing test does not guarantee that the loop will continue. A model may declare completion too early without sufficient verification. If the completion condition is vague, it may also continue low-value exploration.

Harness implementers therefore need limits, timeouts, cancellation, and error classification. Users should provide observable completion criteria.

Context Management for Long Loops

As the loop continues, history accumulates user messages, model outputs, tool calls, file contents, logs, errors, and diffs. The next inference needs prior context, but the context window is finite.

OpenAI’s Agent Loop article explains that subsequent requests preserve earlier input as a prefix while appending tool calls and their results. Keeping an exact prefix also supports prompt caching. Continuing to add large logs or files, however, affects input size, latency, and the model’s ability to find the important evidence.

More output is not the same as better observation

Instead of returning an entire large log, it is often more useful to narrow likely errors first.

bash
rg "ERROR|Exception" app.log | tail -n 100

Over-filtering can hide the cause, so the goal is an observation that is sufficient to evaluate a hypothesis, contains little irrelevant material, and clearly states its scope. Harness design should cover not only a maximum output size, but also head-and-tail preservation, explicit truncation notices, structured results, and ways to retrieve more detail later.

Compaction and its trade-offs

As history approaches a threshold, a harness needs compaction: replacing detailed history with a smaller representation. OpenAI’s explanation states that Codex uses the Responses API compact endpoint when a configured limit is exceeded.

Compaction creates room for longer work, but it can weaken fine-grained evidence, unresolved hypotheses, or early constraints. Important invariants should therefore not live only in ephemeral conversation. Keeping build procedures, prohibitions, required tests, and architectural constraints in retrievable project instructions such as AGENTS.md helps reduce information loss across a long Agent Loop.

How Understanding the Agent Loop Changes the Way You Use Codex

This architecture is not merely implementation trivia. It provides concrete ways to improve instructions and diagnose failures.

Make completion criteria observable

A request such as:

plaintext
Fix it appropriately.

provides little guidance about what the model should observe before declaring completion. A better request is:

plaintext
Run UserServiceTest after the change.
Do not change the existing API response format.
In the final response, report the changed files and test results.

Tests, builds, type checks, and diff inspection give the loop external feedback. Because tests do not prove the entire specification, important invariants should also be stated explicitly.

Specify the boundary between investigation and implementation

plaintext
First investigate the cause and explain the candidate change and impact.
Wait for my confirmation before implementing it.

This does not change the model’s intelligence. It changes the allowed side effects and the termination condition of the turn. The investigation phase can remain read-only, while implementation waits for another user decision.

Ask for targeted observations

Searching and reading bounded ranges is generally more context-efficient than loading enormous files or logs. You can instruct the agent to identify relevant locations first, then read only the necessary regions.

To compensate for the risk of over-narrowing, ask for broader searches or tests before completion. Efficient exploration and comprehensive final verification are separate design concerns.

Look beyond “change the model”

When an agent behaves poorly, do not examine only the model. Ask:

  • Are the tool description and schema clear?
  • Can the agent reach the required commands, files, and logs?
  • Are permissions too narrow or too broad?
  • Do tool results expose exit status and truncation?
  • Are project instructions contradictory?
  • Is there a verifiable completion condition?
  • Does the MCP boundary have its own authorization and guardrails?

Debugging an agent means examining the chain from input construction through the tool contract, execution policy, observation, and history update—not just the text emitted by the model.

Common Misconceptions

Misconception 1: The model directly operates the shell

The model generates a tool call. The harness or tool implementation starts the process.

Misconception 2: The UI shows all of the model’s internal reasoning

Visible progress, summaries, and tool events are observable interfaces, not a complete representation of the model’s internal computation. The reliable way to analyze the Agent Loop is to follow observable boundaries: inputs, tool calls, results, and the final message.

Misconception 3: A passing test guarantees correctness

The test scope may be narrow, the expectation may be wrong, or the environment may differ from production. A successful Agent Loop and a correct specification are different claims.

Misconception 4: A sandbox makes every tool safe

Restrictions applied to a Codex shell do not necessarily govern the side effects of an external MCP tool. Safety must be designed at each boundary.

Misconception 5: A capable model makes the harness trivial

In production, context construction, tool design, permissions, approvals, output handling, error recovery, and observability strongly affect quality. Even a capable model cannot accurately observe hidden state or use capabilities that do not exist.

Conclusion: The Agent Loop Connects Inference to Reality

In the Agent Loop, the model chooses the next action, the harness executes the request under policy, and tools observe or modify the environment. The result returns as new evidence, and the cycle repeats until the model emits a final message.

Once this boundary is clear, it becomes easier to see where permissions must be enforced, what guardrails an MCP server needs, where failures should be observed, and why context management affects quality.

Understanding Codex requires more than asking how capable the model is. The decisive questions are how a model decision becomes a real operation and how the outcome is represented when it returns to the model. The Agent Loop is the runtime that connects inference to the software-development environment.