Codex가 요청 하나를 “모델에 묻기 → 도구 실행 → 결과를 다시 모델에 묻기” 의 반복으로 처리하는 심장부가 run_turn 루프다.
왜 배우나 — 모든 AI 에이전트의 본질인 “관찰→행동의 반복”이 코드 레벨에서 어떻게 도는지가 여기 다 들어 있다.
그림
명령을 친 순간부터 답이 돌아올 때까지의 생명주기다.
flowchart TD
A["사용자 입력<br/>codex exec '프롬프트'"] --> B["하나의 작업(Task)으로 포장<br/>UserTurn으로 변환"]
B --> C["백그라운드 스레드<br/>spawn_task → RegularTask"]
C --> D["run_turn 루프 시작"]
D --> E["1) 대화 전체를<br/>모델에 보냄 (샘플링)"]
E --> F{"모델의 응답은?"}
F -->|"'이 도구 실행해줘'"| G["2) 도구 실제 실행<br/>(파일·셸·웹검색)"]
G --> H["3) 도구 결과를<br/>대화 기록에 붙임"]
H --> I{"토큰 한계?"}
I -->|"닿음"| J["대화 자동 요약<br/>auto-compact"]
J --> E
I -->|"여유"| E
F -->|"답만"| K["턴 종료<br/>마지막 답 반환"]
flowchart LR
subgraph 껍데기["Task = 생명주기 껍데기"]
T1["RegularTask<br/>일반 대화"]
T2["ReviewTask<br/>코드 리뷰"]
T3["CompactTask<br/>수동 요약"]
T4["UserShellCommandTask<br/>!cmd 직접 실행"]
end
T1 --> RT["run_turn<br/>(샘플링 반복 엔진)"]
T2 --> RT
T4 -.모드에 따라.-> RT
T3 -.루프 아님.-> CMP["run_compact_task"]
쉽게 풀기
비유: 비서에게 일을 시키는 것
“이번 분기 매출 보고서 정리해줘”라는 쪽지 한 장을 비서에게 건넸다고 하자.
Task(작업) = 그 쪽지 한 장. “이 일을 처리하라”는 단위다. Codex는 요청 하나를 Task로 감싸 별도 책상(백그라운드 스레드)에서 처리한다.
run_turn(턴 루프) = 비서가 일하는 방식. 한 번에 끝내지 않고 ① 묻고(모델 호출=샘플링) → ② 행동하고(도구 실행) → ③ 결과를 들고 다시 묻기를 반복한다.
이 ①②③을 모델이 “이제 됐어, 끝”이라고 할 때까지 반복하는 것이 “한 턴”이다.
핵심은 모델이 직접 손을 쓰지 못한다는 점이다. 모델은 “이 도구를 써달라”고 말만 하고, 실제 실행은 Codex가 한다. 그 결과를 꼬박꼬박 모델에게 다시 보여주는 “보여주고 다시 묻기”가 에이전트의 비결이다.
sequenceDiagram
participant 모델
participant Codex as Codex(비서)
participant 도구 as 파일/셸/웹
모델->>Codex: "이 도구 써줘" (말만)
Codex->>도구: 실제 실행
도구-->>Codex: 결과
Codex->>모델: 결과를 히스토리에 붙여 재질문
모델->>Codex: "끝" 또는 다음 도구 요청
끼어드는 두 안전장치
대화가 너무 길어지면 (auto-compact) — 토큰 한계에 닿으면 루프 한가운데서 지난 대화를 자동 요약해 자리를 비우고 계속 일한다.
무한 자기복제 방지 (depth 제한) — 모델이 또 다른 에이전트를 끝없이 부르지 못하도록 “몇 단계까지만”이라는 깊이 제한을 둔다.
Task의 네 껍데기
같은 run_turn 엔진을 쓰되 포장지가 네 종류다. 대부분은 RegularTask(일반 대화)이고, 나머지는 특수 상황용이다.
핵심 정리
Task 종류
무엇을 하나
run_turn
RegularTask
일반 대화 턴 (가장 흔함)
직접 호출(loop)
ReviewTask
코드 리뷰, 자식 스레드 처리
간접
CompactTask
수동 /compact 요약 전용
UserShellCommandTask
!cmd 직접 셸 실행
모드에 따라
펼쳐보기: Task별 근거 파일
RegularTask → tasks/regular.rs:72-87
ReviewTask → tasks/review.rs:51-93,124-136 (자식 codex 스레드를 one-shot으로 띄워 그 안에서 run_turn이 돌고 이벤트를 부모로 중계)
CompactTask → tasks/compact.rs:24-63 (run_compact_task, 일반 샘플링 루프 아님)
UserShellCommandTask → tasks/user_shell.rs
네 Task 모두 SessionTask 트레잇 구현 → tasks/mod.rs:209-247
모델의 응답 스트림은 ResponseEvent 토막들로 들어온다. 루프가 실제로 신경 쓰는 것은 OutputItemDone(도구 호출이면 큐잉, 아니면 히스토리 기록)과 Completed(루프 1회 종료, end_turn==Some(false)면 한 번 더 돈다) 둘이다.
flowchart LR
S["모델 스트림"] --> E1["Created<br/>무시"]
S --> E2["OutputItemDone"]
S --> E3["Completed"]
E2 --> D{"도구 호출?"}
D -->|"예"| Q["도구 future 큐잉"]
D -->|"아니오"| H["메시지·추론<br/>히스토리 기록"]
E3 --> C{"end_turn?"}
C -->|"Some(false)"| F["needs_follow_up=true<br/>한 번 더"]
C -->|"true / None"| K["턴 종료"]
펼쳐보기: 전체 ResponseEvent 처리 + auto-compact 끼어드는 지점
이벤트 처리:
Created — 스트림 시작, 무시
OutputItemAdded — 아이템 시작, 스트리밍 파서 준비
OutputItemDone — 핵심. 도구 호출이면 future 큐잉, 아니면 메시지/추론 히스토리 기록