Codex · 진입점과 에이전트 실행 루프 (run_turn / Task)

한 줄 요약

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 직접 셸 실행모드에 따라

모델의 응답 스트림은 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["턴 종료"]

실제 예시

1) 모든 Task의 계약 — SessionTask 트레잇

// codex-rs/core/src/tasks/mod.rs
pub(crate) trait SessionTask: Send + Sync + 'static {
    fn kind(&self) -> TaskKind;
    fn span_name(&self) -> &'static str;
    fn run(
        self: Arc<Self>,
        session: Arc<SessionTaskContext>,
        ctx: Arc<TurnContext>,
        input: Vec<TurnInput>,
        cancellation_token: CancellationToken,
    ) -> impl std::future::Future<Output = Option<String>> + Send;
    // 기본 no-op. abort 시 정리용.
    fn abort(&self, ...) -> impl std::future::Future<Output = ()> + Send { /* ... */ }
}

2) 루프 본체 시그니처 — run_turn

// codex-rs/core/src/session/turn.rs
pub(crate) async fn run_turn(
    sess: Arc<Session>,
    turn_context: Arc<TurnContext>,
    turn_extension_data: Arc<codex_extension_api::ExtensionData>,
    input: Vec<TurnInput>,
    prewarmed_client_session: Option<ModelClientSession>,
    cancellation_token: CancellationToken,
) -> Option<String> // 반환: 마지막 어시스턴트 메시지

3) 무한 자기복제를 막는 depth 가드

// codex-rs/core/src/tools/handlers/multi_agents/spawn.rs
let child_depth = next_thread_spawn_depth(&session_source);
let max_depth = turn.config.agent_max_depth;
if exceeds_thread_spawn_depth_limit(child_depth, max_depth) {
    return Err(FunctionCallError::RespondToModel(
        "Agent depth limit reached. Solve the task yourself.".to_string(),
    ));
}

깊이는 세션 출처에서 읽어 +1로 올리고(agent/registry.rs:63-73), 초과 시 도구 호출을 모델에 에러로 되돌려 차단한다. agent_max_depth는 config agents.max_depth(최소 1, config/mod.rs:842,3160-3165).

직접 만들 때 체크리스트

  • Task 껍데기와 turn 루프를 분리 (Task = 생명주기/이벤트, run_turn = 샘플링 반복)
  • OutputItemDone에서 도구 호출/메시지 분기, 도구 호출은 호출 자체를 먼저 기록
  • 도구 결과(FunctionCallOutput)를 히스토리에 넣고 다음 샘플링에 포함
  • Completedend_turn == None 케이스 fallback 마련
  • 매 샘플링 후 토큰 한계 검사 → 루프 내부에서 compact 후 continue
  • spawn에 depth + 1 > max_depth 가드, 초과 시 모델에 에러를 되돌려 무한재귀 차단
  • cancellation_token을 stream/도구 future에 전파해 abort 시 빠르게 빠짐

요약 & 셀프체크

3줄 요약:

  1. 요청 하나는 Task로 포장돼 백그라운드에서 돌고, 그 안에서 run_turn이 “모델 호출 → 도구 실행 → 재호출”을 반복한다.
  2. 모델은 손을 쓰지 못하고 “이 도구 써달라”고 말만 하며, Codex가 실행한 결과를 히스토리에 붙여 다시 보여주는 것이 핵심 메커니즘이다.
  3. 대화가 길어지면 루프 도중 auto-compact로 요약하고, 자기복제는 depth 제한으로 막는다.

스스로 답해보기:

  • 한 “턴”이 끝나는 조건은? (힌트: 도구를 더 안 부르고 답만 할 때 = needs_follow_up false)
  • 모델이 직접 파일을 읽거나 명령을 실행하는가, 누가 대신 하는가?
  • 토큰 한계에 닿으면 루프가 멈추는가, 어떻게 계속 도는가?

연결

CX_개요 · _분석축_루브릭 · CX_20_prompt-and-context-assembly (히스토리→프롬프트 조립) · CX_30_tool-system (도구 실행) · CX_60_subagents-multi-agent (depth 제한·서브에이전트) · CX_90_persistence-and-memory (auto-compact·히스토리)