Codex · 고유 특별기능: Code Mode (도구를 코드로 실행하는 런타임)

한 줄 요약

Code Mode는 도구를 하나씩 호출하는 대신 exec 도구 하나에 짧은 자바스크립트(JS)를 써서 여러 도구를 한 번에 엮어 실행하는 Codex 고유 기능이다. 모델↔서버 왕복 횟수를 줄여 속도와 토큰(비용)을 아끼는 차별화 설계다.

그림

flowchart TD
  M["모델: exec 도구에 JS 셀 작성"] --> H[CodeModeExecuteHandler 접수]
  H --> P["parse_exec_source: 첫 줄 옵션 분리"]
  P --> S["CodeModeService.execute: 새 V8 격리공간 생성"]
  S --> JS["V8 안에 tools/store/text 등 도구 설치"]
  JS -->|await tools.X 호출| CB["tool_callback → 도구호출 이벤트 발생"]
  CB --> D[CodeModeSessionDelegate.invoke_tool]
  D --> BR["DispatchBroker → 실제 도구 라우터로 전달"]
  BR --> R["(실제 도구 / MCP / exec_command 실행)"]
  R -->|결과 JSON| RC[JS의 Promise를 결과로 채움]
  JS -->|끝 또는 시간초과| RR["RuntimeResponse: Result / Yielded / Terminated"]
  RR --> HR["handle_runtime_response: 헤더+절단+이미지 정리"]
  HR --> M2[모델에게 최종 결과만 반환]
  RR -.시간초과(Yielded).-> W[모델이 wait 도구로 이어받기]
  W --> S

쉽게 풀기

보통 방식(느린 이유) — 식당에서 “물 주세요”→받고→“메뉴 주세요”→받고처럼 하나씩 부탁하고 매번 답을 기다린다. 도구 5개면 모델↔서버를 5번 오간다. 왕복이 많을수록 느리고 토큰도 많이 쓴다.

Code Mode(빠른 이유) — 주문서 한 장에 “물·메뉴·주문을 한 번에”라고 적어 건넨다. 모델에게는 도구 목록 대신 exec 단 하나만 준다. exec는 “JS 코드를 써라”는 도구다.

flowchart LR
  subgraph 일반["일반 방식: 5왕복"]
    A1[도구1] --> A2[도구2] --> A3[도구3] --> A4[도구4] --> A5[도구5]
  end
  subgraph CM["Code Mode: 1왕복"]
    B[exec 셀 한 장에 도구 5개 엮기] --> B2[최종 결과만 반환]
  end

용어 3개만 잡으면 된다.

  • 셀(Cell) = 모델이 한 번에 쓰는 짧은 JS 한 덩어리. 곧 “주문서 한 장”.
  • V8 엔진 = Codex 안의 작은 JS 실행기(브라우저용 그 엔진). 주문서를 처리하는 주방.
  • 코드 안에서 도구는 await tools.exec_command(...)처럼 평범한 함수로 불린다. 모델은 도구 10개를 엮는 작은 프로그램을 한 번에 짜고, Codex는 돌려서 최종 결과만 돌려준다.

오래 걸리면 — yield/wait — 오래 걸리는 요리면 exec가 “지금까지 나온 것”을 먼저 내놓고(yield) 셀 번호(cell_id)를 준다. 모델은 나중에 wait로 이어받는다. 진동벨을 받아 두고 기다리는 것과 같다.

안전 울타리 — 이 JS는 격리된 작은 상자(V8 isolate) 안에서만 돈다. Node·파일시스템·네트워크·console이 전부 없다.

핵심 정리

모델이 직접 호출하는 공개 도구는 두 개뿐이다.

도구입력역할
execraw JS 텍스트(Custom)JS 셀을 새 V8 격리공간에서 실행
waitJSON 인자yield된 셀을 재개/대기/종료

도구를 “코드모드용”으로 노출하는 형식(ToolDefinition)의 핵심:

필드의미
name코드모드 식별자(JS 변수명으로 정규화)
kindFunction(객체 인자) / Freeform(문자열 인자)
input/output_schemaJSON Schema → TS 타입으로 렌더

언제 켜지나 — ToolMode 세 가지 (protocol/src/openai_models.rs)

flowchart LR
  D["Direct: 개별 function-call, 코드모드 OFF"]
  C["CodeMode: exec + 기존 직접 도구 병행"]
  O["CodeModeOnly: exec 단일 진입점, 모든 nested 도구 TS 선언을 exec 설명에 인라인"]

핵심 동시성 사실(소스 확인)

sequenceDiagram
  participant JS as 셀(V8 isolate, 별도 OS 스레드)
  participant DG as delegate
  JS->>DG: RuntimeEvent::ToolCall (await tools.X)
  DG-->>JS: RuntimeCommand::ToolResponse (Promise 채움)
  • 셀은 별도 OS 스레드의 V8 isolate에서 돈다. nested 호출은 위 양방향 채널로 Promise를 채운다.
  • store/load 값은 같은 세션의 셀끼리만 공유, 세션 간 격리(테스트 stored_values_are_shared_between_cells_but_not_sessions).
  • yield_time_ms 안에 못 끝나면 Yielded + cell_id, 헤더 "Script running with cell ID {cell_id}".
  • 종료 헤더: Script completed / Script failed / Script terminated, 뒤에 Wall time {n} seconds + 출력.

실제 예시

1) 모델이 작성하는 exec 셀 (복붙 예시)

// @exec: {"yield_time_ms": 8000, "max_output_tokens": 2000}
const [profile, prefs] = await Promise.all([          // 도구 3개 병렬
  tools.mcp__ologs__get_profile({ user_id: "u_42" }),
  tools.read_file({ path: "/etc/config.toml" }),      // Function: 객체 인자
]);
const cached = load("last_run");                       // 이전 셀 값 재사용
store("last_run", Date.now());                         // 다음 셀로 값 전달
text(`profile=${JSON.stringify(profile)}`);           // 텍스트 출력 누적
if (!profile) exit();                                 // 즉시 성공 종료

tools.X() 한 줄이 Rust 이벤트로 변환되어 실제 도구 라우터까지 가는 경로:

flowchart LR
  A["await tools.X(input)"] --> B["tool_callback: Promise 생성 + RuntimeEvent::ToolCall 발신"]
  B --> C["delegate.invoke_tool → ToolCall{"tool_name, call_id, payload"}"]
  C --> D["handle_tool_call_with_source(ToolCallSource::CodeMode)"]
  D --> E["결과 JSON → code_mode_result() → 셀 Promise resolve"]

직접 만들 때 체크리스트

  • execfreeform/Custom으로 등록(JSON 아님, raw JS). 잘못 보내면 “expects raw JavaScript source text”.
  • 첫 줄 프래그마는 parse_exec_source로 분리. 허용 키 yield_time_ms, max_output_tokens 뿐.
  • nested 도구는 Function(객체) vs Freeform(문자열) 구분해 payload 빌드.
  • 도구 이름은 normalize_code_mode_identifier로 JS 식별자화, namespace는 code_mode_name_for_tool_name.
  • CodeModeSessionDelegate 3개 구현: invoke_tool, notify, cell_closed.
  • Yielded 응답이면 cell_id 노출 + wait 경로 제공.
  • 세션 단위 store/load 격리, 종료 시 shutdown()으로 셀 cancel + isolate terminate.
  • V8 샌드박스: no Node/fs/network/console(테스트 v8_console_is_not_exposed_on_global_this). 원격 이미지 URL 거부, base64 data URI만.

요약 & 셀프체크

3줄 요약

  • Code Mode는 도구를 하나씩 부르는 대신 exec 한 도구에 JS 셀을 써서 여러 도구를 한 번에 엮어 모델 왕복을 줄인다.
  • 셀은 격리된 V8 안에서 돌고, 코드 속 tools.X()는 실제 도구 라우터로 디스패치되어 결과가 Promise로 돌아온다.
  • 오래 걸리면 Yielded로 양보하고 모델이 wait로 이어받으며, store/load는 같은 세션 안에서만 값을 공유한다.

스스로 답해보기

  1. 일반 도구 호출 대비 Code Mode가 토큰·속도를 아끼는 이유를 “왕복” 개념으로 설명할 수 있는가?
  2. execwait는 각각 언제 쓰이며, Yielded 응답이 오면 모델은 무엇을 해야 하는가?
  3. FunctionFreeform 도구는 인자를 어떻게 다르게 넘기는가?

연결

CX_개요 · _분석축_루브릭

근거 파일

Codex 교차검증 메모 (원본 분석 보존)

  • 주입 위치: 턴 시작 시 CodeModeService::start_turn_workertool_mode가 CodeMode/CodeModeOnly일 때만 디스패치 워커를 띄운다. 도구 스펙은 augment_tool_spec_for_code_mode / collect_code_mode_tool_definitions로 변환되어 각 설명 끝에 declare const tools: { ... } TS 샘플이 붙고, JSON Schema는 render_json_schema_to_typescript로 TS 타입이 된다.
  • 모델에게는 exec(+wait)와 설명에 박힌 도구 카탈로그가 프롬프트로 들어간다. 도구 이름은 normalize_code_mode_identifier로 JS 식별자화(예 hidden-dynamic-toolhidden_dynamic_tool).
  • CodeModeOnly에서는 exec 설명에 모든 nested 도구의 TS 선언과 네임스페이스 가이드가 통째로 인라인된다(build_exec_tool_description(code_mode_only=true)).