Codex CLI의 핵심인 에이전트 루프가 사용자, 모델, 그리고 모델이 호출하는 도구 간 상호작용을 어떻게 오케스트레이션하는지 살펴보고, 프롬프트 구성·성능(프롬프트 캐싱)·컨텍스트 창 관리(컴팩션)까지 Codex가 이를 어떻게 다루는지 설명합니다.
URL: https://openai.com/index/unrolling-the-codex-agent-loop/
Title: Unrolling the Codex agent loop
Codex CLI(새 창에서 열기)는 크로스 플랫폼 로컬 소프트웨어 에이전트로, 사용자의 머신에서 안전하고 효율적으로 동작하면서도 고품질의 신뢰할 수 있는 소프트웨어 변경을 만들어내도록 설계되어 있습니다. 우리는 4월에 CLI를 처음 출시한 이후 세계적 수준의 소프트웨어 에이전트를 어떻게 구축해야 하는지에 대해 엄청난 것을 배웠습니다. 그 인사이트를 풀어내기 위해, 이 글은 Codex가 어떻게 동작하는지와 그 과정에서 얻은 값비싼 교훈들을 다양한 측면에서 탐구하는 연재 시리즈의 첫 번째 글입니다. (Codex CLI가 어떻게 만들어졌는지 더 세밀하게 보고 싶다면 오픈 소스 리포지토리 https://github.com/openai/codex(새 창에서 열기)를 확인해 보세요. 더 알고 싶다면 GitHub 이슈와 풀 리퀘스트에 설계 결정의 세부 사항이 많이 기록되어 있습니다.)
시작으로, 우리는 _에이전트 루프(agent loop)_에 집중할 것입니다. 이는 Codex CLI의 핵심 로직으로, 사용자, 모델, 그리고 모델이 의미 있는 소프트웨어 작업을 수행하기 위해 호출하는 도구들 간의 상호작용을 오케스트레이션하는 역할을 합니다. 이 글이 LLM을 활용하는 데 있어 우리의 에이전트(또는 “하네스(harness)”)가 수행하는 역할을 잘 보여주길 바랍니다.
본격적으로 들어가기 전에 용어에 대한 짧은 메모를 남깁니다. OpenAI에서 “Codex”는 Codex CLI, Codex Cloud, Codex VS Code 확장 등을 포함한 소프트웨어 에이전트 제품군 전반을 의미합니다. 이 글은 모든 Codex 경험의 기반이 되는 핵심 에이전트 루프와 실행 로직을 제공하며 Codex CLI를 통해 노출되는 Codex _하네스(harness)_에 초점을 맞춥니다. 편의를 위해 여기서는 “Codex”와 “Codex CLI”를 같은 의미로 사용하겠습니다.
모든 AI 에이전트의 중심에는 “에이전트 루프(agent loop)”라고 불리는 것이 있습니다. 에이전트 루프를 단순화해 그리면 다음과 같습니다.
먼저 에이전트는 사용자로부터 _입력(input)_을 받아, 모델에 제공할 텍스트 지시사항의 집합인 _프롬프트(prompt)_를 준비할 때 포함합니다.
다음 단계는 모델에 질의하는 것입니다. 즉, 지시사항을 보내고 응답을 생성해 달라고 요청하는 과정인데, 이를 _추론(inference)_이라고 합니다. 추론 과정에서 텍스트 프롬프트는 먼저 입력 토큰(새 창에서 열기) 시퀀스로 변환됩니다. 토큰은 모델 어휘를 인덱싱하는 정수입니다. 그런 다음 이 토큰을 사용해 모델을 샘플링하여 새로운 출력 토큰 시퀀스를 생성합니다.
출력 토큰은 다시 텍스트로 번역되어 모델의 응답이 됩니다. 토큰은 점진적으로 생성되기 때문에, 모델이 실행되는 동안에도 이 번역이 진행될 수 있으며, 이것이 많은 LLM 기반 애플리케이션이 스트리밍 출력을 보여주는 이유입니다. 실제로 추론은 보통 텍스트를 다루는 API 뒤에 캡슐화되어, 토크나이징의 세부 사항을 추상화합니다.
추론 단계의 결과로, 모델은 (1) 사용자의 원래 입력에 대한 최종 응답을 생성하거나, (2) 에이전트가 수행해야 하는 _도구 호출(tool call)_을 요청합니다(예: “ls를 실행하고 출력 결과를 보고하라”). (2)의 경우, 에이전트는 도구 호출을 실행하고 그 출력을 원래 프롬프트에 덧붙입니다. 이 출력은 모델에 다시 질의하기 위한 새로운 입력을 생성하는 데 사용되며, 에이전트는 이 새로운 정보를 고려해 다시 시도할 수 있습니다.
이 과정은 모델이 도구 호출을 더 이상 내보내지 않고 대신 사용자에게 보낼 메시지(OpenAI 모델에서는 _assistant message_라고 부름)를 생성할 때까지 반복됩니다. 많은 경우 이 메시지는 사용자의 원래 요청에 직접 답하지만, 사용자에게 추가 질문을 던지는 후속 질문일 수도 있습니다.
에이전트는 로컬 환경을 수정하는 도구 호출을 실행할 수 있으므로, 에이전트의 “출력”은 assistant message에만 국한되지 않습니다. 많은 경우 소프트웨어 에이전트의 주된 출력은 사용자의 머신에서 작성하거나 편집하는 코드입니다. 그럼에도 매 턴은 항상 assistant message로 끝납니다(예: “요청하신 architecture.md를 추가했습니다”). 이는 에이전트 루프의 종료 상태를 나타냅니다. 에이전트의 관점에서 작업이 완료되었고 제어가 사용자에게 돌아갑니다.
도식에서 보여준 _사용자 입력_에서 _에이전트 응답_까지의 여정을 대화의 한 턴(turn)(Codex에서는 스레드(thread))이라고 부릅니다. 이 _대화 턴_에는 모델 추론과 도구 호출 사이의 많은 반복이 포함될 수 있습니다. 기존 대화에 새 메시지를 보낼 때마다, 이전 턴의 메시지와 도구 호출을 포함한 대화 기록 전체가 새 턴의 프롬프트 일부로 포함됩니다.
이는 대화가 길어질수록 모델을 샘플링하는 데 사용되는 프롬프트도 함께 길어진다는 뜻입니다. 이 길이는 중요한데, 모든 모델에는 한 번의 추론 호출에 사용할 수 있는 최대 토큰 수인 _컨텍스트 창(context window)_이 있기 때문입니다. 이 창에는 입력 토큰과 출력 토큰이 모두 포함됩니다. 상상할 수 있듯이, 에이전트가 한 턴에서 수백 번의 도구 호출을 결정할 수도 있어 컨텍스트 창을 소진할 수 있습니다. 이러한 이유로 _컨텍스트 창 관리(context window management)_는 에이전트의 많은 책임 중 하나입니다. 이제 Codex가 에이전트 루프를 어떻게 실행하는지 살펴보겠습니다.
Codex CLI는 Responses API(새 창에서 열기)에 HTTP 요청을 보내 모델 추론을 수행합니다. Responses API를 사용해 에이전트 루프를 구동하는 Codex 내부에서 정보가 어떻게 흐르는지 살펴보겠습니다.
https://chatgpt.com/backend-api/codex/responses를 사용합니다.https://api.openai.com/v1/responses를 사용합니다.--oss로 실행해 gpt-oss를 ollama 0.13.4+(새 창에서 열기) 또는 LM Studio 0.3.39+(새 창에서 열기)와 함께 사용할 때, 기본값은 로컬 컴퓨터에서 실행되는 http://localhost:11434/v1/responses입니다.이제 Codex가 대화에서 첫 번째 추론 호출을 위한 프롬프트를 어떻게 만드는지 살펴보겠습니다.
최종 사용자 입장에서, Responses API를 질의할 때 모델을 샘플링하는 데 사용되는 프롬프트를 문자 그대로 지정하지는 않습니다. 대신 질의의 일부로 여러 입력 타입을 지정하고, Responses API 서버가 이를 모델이 소비하도록 설계된 프롬프트 구조로 어떻게 구성할지 결정합니다. 프롬프트를 “항목 리스트(list of items)”로 생각해도 좋습니다. 이 절에서는 질의가 어떻게 그 리스트로 변환되는지 설명합니다.
초기 프롬프트에서 리스트의 각 항목에는 역할(role)이 연결됩니다. role은 해당 콘텐츠에 얼마나 큰 가중치를 둘지 나타내며, 다음 값 중 하나입니다(우선순위가 높은 순): system, developer, user, assistant.
instructions(새 창에서 열기): 모델 컨텍스트에 삽입되는 system(또는 developer) 메시지tools(새 창에서 열기): 응답을 생성하는 동안 모델이 호출할 수 있는 도구 목록input(새 창에서 열기): 모델에 제공되는 텍스트/이미지/파일 입력 목록tools 필드는 Responses API가 정의한 스키마를 따르는 도구 정의 목록입니다. Codex의 경우, 여기에는 Codex CLI가 제공하는 도구, Codex가 사용할 수 있도록 Responses API가 제공하는 도구, 그리고 보통 MCP 서버를 통해 사용자가 제공하는 도구가 포함됩니다.
tools 섹션에 정의된 Codex 제공 shell _도구_에만 적용되는 샌드박스를 설명하는 role=developer 메시지. 즉 MCP 서버가 제공하는 것과 같은 다른 도구들은 Codex에 의해 샌드박싱되지 않으며, 자체 가드레일을 강제할 책임이 있습니다.
(선택) 사용자의 config.toml 파일에서 읽은 developer_instructions 값을 내용으로 하는 role=developer 메시지.
(선택) “사용자 지침(user instructions)”을 내용으로 하는 role=user 메시지. 이는 단일 파일에서 오지 않고, 여러 소스에 걸쳐 집계(새 창에서 열기)됩니다. 일반적으로 더 구체적인 지침일수록 뒤에 나타납니다.
$CODEX_HOME에 있는 AGENTS.override.md 및 AGENTS.md의 내용cwd의 Git/프로젝트 루트(존재한다면)부터 cwd 자체까지 각 폴더를 훑어 AGENTS.override.md, AGENTS.md, 또는 config.toml의 project_doc_fallback_filenames에 지정된 파일명 중 존재하는 것의 내용을 추가Codex가 위의 모든 계산을 수행해 input을 초기화한 뒤, 대화를 시작하기 위해 사용자 메시지를 덧붙입니다.
앞선 예시는 각 메시지의 내용에 초점을 맞췄지만, input의 각 요소는 type, role(새 창에서 열기), content를 갖는 JSON 객체라는 점에 유의하세요. 형태는 다음과 같습니다.
Codex가 Responses API에 보낼 전체 JSON 페이로드를 구성하면, ~/.codex/config.toml에 설정된 Responses API 엔드포인트 구성에 따라 Authorization 헤더를 포함해 HTTP POST 요청을 보냅니다(추가 HTTP 헤더 및 쿼리 파라미터가 지정되어 있으면 함께 추가됩니다).
OpenAI Responses API 서버가 요청을 받으면, JSON을 사용해 다음과 같이 모델용 프롬프트를 도출합니다(물론 Responses API의 커스텀 구현은 다른 선택을 할 수도 있습니다).
보시다시피, 프롬프트의 처음 세 항목의 순서는 클라이언트가 아니라 서버가 결정합니다. 다만 그 세 항목 중에서도 _system 메시지_의 내용만 서버가 제어하고, tools와 instructions는 클라이언트가 결정합니다. 그 뒤에 JSON 페이로드의 input이 이어지며 프롬프트가 완성됩니다.
이제 프롬프트가 준비되었으니, 모델을 샘플링할 준비가 되었습니다.
Responses API로 보내는 이 HTTP 요청은 Codex에서 대화의 첫 번째 “턴”을 시작합니다. 서버는 Server-Sent Events(SSE(새 창에서 열기)) 스트림으로 응답합니다. 각 이벤트의 data는 "type"이 "response"로 시작하는 JSON 페이로드이며, 예시는 다음과 같을 수 있습니다(이벤트의 전체 목록은 API 문서(새 창에서 열기)에서 확인할 수 있습니다).
Codex는 이 이벤트 스트림을 소비(새 창에서 열기)하고, 클라이언트가 사용할 수 있는 내부 이벤트 객체로 재발행합니다. response.output_text.delta 같은 이벤트는 UI에서의 스트리밍을 지원하는 데 사용되며, response.output_item.added 같은 다른 이벤트는 이후 Responses API 호출을 위한 input에 덧붙여질 객체로 변환됩니다.
첫 번째 Responses API 요청에 response.output_item.done 이벤트가 두 개 포함되어 있다고 가정해 봅시다. 하나는 type=reasoning이고 다른 하나는 type=function_call입니다. 이 이벤트들은 도구 호출의 응답을 포함해 모델에 다시 질의할 때, JSON의 input 필드에서 다음과 같이 표현되어야 합니다.
이후 질의에서 모델을 샘플링하는 데 사용되는 프롬프트는 다음과 같이 보일 것입니다.
특히, 기존 프롬프트가 새 프롬프트의 _정확한 접두사(exact prefix)_가 된다는 점에 주목하세요. 이는 의도된 것으로, 이후 요청을 훨씬 더 효율적으로 만들기 때문입니다. 즉 _프롬프트 캐싱(prompt caching)_을 활용할 수 있게 해주기 때문입니다(성능 섹션에서 다음으로 다룹니다).
에이전트 루프의 첫 번째 다이어그램을 다시 보면, 추론과 도구 호출 사이에는 많은 반복이 있을 수 있습니다. 프롬프트는 assistant message를 받아 턴이 끝날 때까지 계속 커질 수 있습니다.
Codex CLI에서는 assistant message를 사용자에게 보여주고, 사용자가 대화를 이어갈 수 있도록 컴포저에 포커스를 맞춰 사용자의 “턴”임을 표시합니다. 사용자가 응답하면, 새 턴을 시작하기 위한 Responses API 요청의 input에는 이전 턴의 assistant message와 사용자의 새 메시지가 모두 덧붙여져야 합니다.
다시 말해, 대화를 이어가는 동안 Responses API로 보내는 input의 길이는 계속 증가합니다.
이제 이렇게 계속 커지는 프롬프트가 성능에 어떤 의미가 있는지 살펴보겠습니다.
“잠깐, 그럼 에이전트 루프는 대화가 진행되는 동안 Responses API로 전송되는 JSON 양 기준으로 _제곱(quadratic)_이 되는 거 아닌가요?”라고 생각할 수도 있습니다. 맞습니다. Responses API는 이 문제를 완화하기 위한 선택적 파라미터 previous_response_id(새 창에서 열기)를 지원하지만, Codex는 오늘날 이를 사용하지 않습니다. 주된 이유는 요청을 완전히 무상태(stateless)로 유지하고 Zero Data Retention(ZDR) 구성을 지원하기 위함입니다.
previous_response_id를 피하면 Responses API 제공자 입장에서 모든 요청이 _무상태_임이 보장되어 단순해집니다. 또한 Zero Data Retention(ZDR)(새 창에서 열기)를 선택한 고객 지원도 쉬워지는데, previous_response_id를 지원하려면 저장해야 하는 데이터가 ZDR과 충돌하기 때문입니다. ZDR 고객이 이전 턴의 독점적 reasoning 메시지의 이점을 포기하는 것은 아니라는 점도 유의하세요. 관련 encrypted_content는 서버에서 복호화될 수 있기 때문입니다.(OpenAI는 ZDR 고객의 복호화 키는 저장하지만 데이터는 저장하지 않습니다.) ZDR 지원을 위해 Codex가 변경된 내용은 PR #642(새 창에서 열기)와 #1641(새 창에서 열기)을 참고하세요.
일반적으로 모델을 샘플링하는 비용이 네트워크 트래픽 비용을 압도하므로, 효율화의 1차 목표는 샘플링입니다. 프롬프트 캐싱이 중요한 이유가 여기에 있습니다. 캐싱을 통해 이전 추론 호출의 계산을 재사용할 수 있기 때문입니다. 캐시 히트가 나면 _모델 샘플링은 제곱이 아니라 선형(linear)_이 됩니다. 프롬프트 캐싱(새 창에서 열기) 문서에서 이를 더 자세히 설명합니다.
캐시 히트는 프롬프트 내에서 정확한 접두사 일치(exact prefix match)가 있을 때만 가능합니다. 캐싱 이점을 얻으려면 지시사항과 예시 같은 정적 콘텐츠를 프롬프트 앞부분에 배치하고, 사용자별 정보 같은 가변 콘텐츠는 끝부분에 두세요. 이는 이미지와 도구에도 적용되며, 요청 간에 동일해야 합니다.
이를 염두에 두고 Codex에서 어떤 작업이 “캐시 미스(cache miss)”를 유발할 수 있는지 생각해 봅시다.
tools를 변경하는 경우model을 변경하는 경우(실제로는 원래 프롬프트의 세 번째 항목이 바뀌는데, 그 항목이 모델별 지시사항을 포함하기 때문입니다)Codex 팀은 프롬프트 캐싱을 훼손할 수 있는 새로운 기능을 Codex CLI에 도입할 때 매우 주의해야 합니다. 예를 들어, MCP 도구에 대한 초기 지원은 도구를 일관된 순서로 열거하지 못하는 버그(새 창에서 열기)를 도입해 캐시 미스를 유발했습니다. MCP 도구는 특히 까다로울 수 있는데, MCP 서버가 notifications/tools/list_changed(새 창에서 열기) 알림을 통해 제공하는 도구 목록을 런타임에 동적으로 바꿀 수 있기 때문입니다. 긴 대화 도중에 이 알림을 반영하면 비용이 큰 캐시 미스가 발생할 수 있습니다.
가능한 경우, 우리는 대화 중간에 발생하는 구성 변경을 처리할 때 이전 메시지를 수정하기보다 변경을 반영하는 _새 메시지_를 input에 덧붙이는 방식으로 처리합니다.
<permissions instructions> 항목과 동일한 형식의 새로운 role=developer 메시지를 삽입(새 창에서 열기)합니다.<environment_context>와 동일한 형식의 새로운 role=user 메시지를 삽입(새 창에서 열기)합니다.우리는 성능을 위해 캐시 히트를 보장하는 데 많은 노력을 기울입니다. 그런데 우리가 관리해야 할 또 다른 핵심 자원이 있습니다. 바로 컨텍스트 창입니다.
컨텍스트 창이 부족해지는 것을 피하기 위한 우리의 일반 전략은 토큰 수가 어떤 임계값을 넘으면 대화를 _컴팩트(compact)_하는 것입니다. 구체적으로는 input을 더 작고 새로운 항목 리스트로 교체하는데, 이는 대화를 대표할 수 있도록 만들어져 에이전트가 지금까지 무슨 일이 있었는지 이해하면서도 계속 진행할 수 있게 해줍니다. 초기의 컴팩션 구현(새 창에서 열기)은 사용자가 /compact 명령을 수동으로 실행해야 했으며, 기존 대화에 요약(summarization)(새 창에서 열기)을 위한 커스텀 지시사항을 더해 Responses API를 질의하는 방식이었습니다. Codex는 결과로 나온 요약이 담긴 assistant message를 이후 대화 턴을 위한 새 input(새 창에서 열기)으로 사용했습니다.
그 이후 Responses API는 컴팩션을 더 효율적으로 수행하는 특별한 /responses/compact 엔드포인트(새 창에서 열기)를 지원하도록 발전했습니다. 이 엔드포인트는 컨텍스트 창을 확보하면서 대화를 이어가기 위해 이전 input을 대체할 수 있는 항목 리스트(새 창에서 열기)를 반환합니다. 이 리스트에는 원래 대화에 대한 모델의 잠재적 이해를 보존하는 불투명한 encrypted_content 항목을 포함한 특별한 type=compaction 항목이 들어 있습니다. 이제 Codex는 auto_compact_limit(새 창에서 열기)을 초과하면 이 엔드포인트를 자동으로 사용해 대화를 컴팩션합니다.
이번 글에서는 Codex 에이전트 루프를 소개하고, 모델을 질의할 때 Codex가 컨텍스트를 구성하고 관리하는 방식을 살펴보았습니다. 그 과정에서 Responses API 위에 에이전트 루프를 구축하는 누구에게나 적용될 실용적 고려사항과 모범 사례도 함께 강조했습니다.
에이전트 루프는 Codex의 기반을 제공하지만, 그것은 시작에 불과합니다. 다음 글들에서는 CLI의 아키텍처를 더 깊이 파고들고, 도구 사용(tool use)이 어떻게 구현되는지 탐구하며, Codex의 샌드박싱 모델을 더 자세히 살펴볼 예정입니다.