Codex · 고유 특별기능: Code Mode (도구를 코드로 실행하는 런타임)
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이 전부 없다.
핵심 정리
모델이 직접 호출하는 공개 도구는 두 개뿐이다.
| 도구 | 입력 | 역할 |
|---|---|---|
exec | raw JS 텍스트(Custom) | JS 셀을 새 V8 격리공간에서 실행 |
wait | JSON 인자 | yield된 셀을 재개/대기/종료 |
[!note]- 펼쳐보기:
exec첫 줄 옵션과wait인자
exec첫 줄 프래그마:// @exec: {"yield_time_ms": 10000, "max_output_tokens": 1000}(허용 키는 이 둘뿐).wait의 JSON 인자(ExecWaitArgs):
cell_id(string, 필수) — 재개할 셀 ID.exec가 “Script running with cell ID …”를 반환한 뒤에만 사용yield_time_ms(u64, 기본 10000) — 더 기다릴 시간max_tokens(usize?, 기본 10000) — 이번 wait가 반환할 새 출력 토큰 한도terminate(bool, 기본 false) — true면 셀 강제 종료, false면 출력 대기
도구를 “코드모드용”으로 노출하는 형식(ToolDefinition)의 핵심:
| 필드 | 의미 |
|---|---|
name | 코드모드 식별자(JS 변수명으로 정규화) |
kind | Function(객체 인자) / Freeform(문자열 인자) |
input/output_schema | JSON Schema → TS 타입으로 렌더 |
[!note]- 펼쳐보기: 내부에서 오가는 요청·응답 형식 전체
ExecuteRequest(핸들러→런타임):tool_call_id(필수),enabled_tools(셀에 노출할 도구, 필수),source(옵션 제거된 순수 JS, 필수),yield_time_ms(기본DEFAULT_EXEC_YIELD_TIME_MS = 10_000),max_output_tokens(기본DEFAULT_MAX_OUTPUT_TOKENS_PER_EXEC_CALL = 10_000).CodeModeNestedToolCall(셀 안tools.X()의 변환 메시지):cell_id,runtime_tool_call_id(예tool-1),tool_name,tool_kind,input(객체/문자열).RuntimeResponse(셀 단위 결과, enum):
Yielded— 아직 실행 중, 중간 출력 후 양보 → 모델은wait필요Terminated— 강제 종료됨Result— 끝남(성공error_text=None, 실패면 메시지)content_items원소는FunctionCallOutputContentItem—InputText { text }또는InputImage { image_url, detail? }.
언제 켜지나 — 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+ 출력.
[!note]- 펼쳐보기: (보조) unified_exec — 영속 셸 세션
core/src/unified_exec는 별개 도구지만 같은 “왕복 줄이기” 철학이다. PTY 기반 대화형/영속 프로세스를 만들어 재사용:{command, cwd}→ 승인/샌드박스 선택 → PTY spawn, 거부 시SandboxType::None으로 재시도. 출력은 head/tail 버퍼로 캡(UNIFIED_EXEC_OUTPUT_MAX_BYTES = 1 MiB), yieldMIN 250ms ~ MAX 30_000ms, 최대 64개 프로세스. 셀이tools.exec_command(...)로 부르는 대상이 이 계열이다.
실제 예시
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"]
[!note]- 펼쳐보기: 위 경로의 실제 Rust 코드 (callback / dispatch)
// code-mode/src/runtime/callbacks.rs — JS 함수콜이 Rust 이벤트가 되는 곳 pub(super) fn tool_callback(scope, args, mut retval) { // ... tool_index, input 파싱, V8 PromiseResolver 생성 ... let id = format!("tool-{}", state.next_tool_call_id); state.next_tool_call_id = state.next_tool_call_id.saturating_add(1); state.pending_tool_calls.insert(id.clone(), resolver); let _ = event_tx.send(RuntimeEvent::ToolCall { id, name: tool_name, kind: tool_kind, input }); retval.set(promise.into()); // JS에는 즉시 Promise 반환 (await 가능) }// core/src/tools/code_mode/mod.rs — nested 호출을 실제 도구 라우터로 디스패치 let call = ToolCall { tool_name, call_id: format!("{PUBLIC_TOOL_NAME}-{}", uuid::Uuid::new_v4()), payload, // Function{arguments} 또는 Custom{input} }; let result = tool_runtime .handle_tool_call_with_source(call, ToolCallSource::CodeMode { cell_id: cell_id.to_string(), runtime_tool_call_id }, cancellation_token).await?; Ok(result.code_mode_result()) // 결과 JSON을 셀의 Promise로 resolve
[!note]- 펼쳐보기: 모델이 읽는
exec설명 템플릿 + 도구 TS 시그니처// code-mode-protocol/src/description.rs const EXEC_DESCRIPTION_TEMPLATE: &str = r#"Run JavaScript code to orchestrate/compose tool calls - Evaluates the provided JavaScript code in a fresh V8 isolate as an async module. - All nested tools are available on the global `tools` object, e.g. `await tools.exec_command(...)`. - Nested tool methods take either a string or an object as their input argument. - Runs raw JavaScript -- no Node, no file system, no network access, no console. - Accepts raw JavaScript source text, not JSON, quoted strings, or markdown code fences. - Optional first-line pragma: `// @exec: {"yield_time_ms": 10000, "max_output_tokens": 1000}`. - `store(key, value)` / `load(key)`: 같은 세션의 다음 exec에서 값 공유. - `yield_control()`: 누적 출력을 모델에 즉시 양보하고 스크립트는 계속. - `exit()`: 스크립트를 즉시 성공 종료."#;// description.rs::render_code_mode_sample 가 생성하는 형태 (테스트 검증) declare const tools: { weather_tool(args: { // look up weather for a given list of locations weather: Array<{ location: string; }>; }): Promise<{ // human readable weather forecast forecast: string; }>; };
[!note]- 펼쳐보기: 직접 구현 시 최소 골격 (Rust)
// 1) 도구를 코드모드용으로 노출 let defs = codex_tools::collect_code_mode_tool_definitions(&nested_tool_specs); // 2) exec 요청 구성 let req = codex_code_mode::ExecuteRequest { tool_call_id: call_id.clone(), enabled_tools: defs, source: parsed.code, // parse_exec_source로 프래그마 분리한 순수 JS yield_time_ms: parsed.yield_time_ms, max_output_tokens: parsed.max_output_tokens, }; // 3) 세션 실행 → 초기 응답 대기 → 후처리 let started = service.execute(req).await?; // StartedCell service.mark_cell_ready_for_dispatch(&started.cell_id); let resp = started.initial_response().await?; // RuntimeResponse let output = handle_runtime_response(&exec, resp, max_tokens, started_at).await?;
직접 만들 때 체크리스트
-
exec는 freeform/Custom으로 등록(JSON 아님, raw JS). 잘못 보내면 “expects raw JavaScript source text”. - 첫 줄 프래그마는
parse_exec_source로 분리. 허용 키yield_time_ms,max_output_tokens뿐. - nested 도구는
Function(객체) vsFreeform(문자열) 구분해 payload 빌드. - 도구 이름은
normalize_code_mode_identifier로 JS 식별자화, namespace는code_mode_name_for_tool_name. -
CodeModeSessionDelegate3개 구현: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는 같은 세션 안에서만 값을 공유한다.
스스로 답해보기
- 일반 도구 호출 대비 Code Mode가 토큰·속도를 아끼는 이유를 “왕복” 개념으로 설명할 수 있는가?
exec와wait는 각각 언제 쓰이며,Yielded응답이 오면 모델은 무엇을 해야 하는가?Function과Freeform도구는 인자를 어떻게 다르게 넘기는가?
연결
근거 파일
[!note]- 펼쳐보기: 전체 근거 파일 목록
codex-rs/code-mode-protocol/src/lib.rs— 공개 심볼,PUBLIC_TOOL_NAME="exec",WAIT_TOOL_NAME="wait"codex-rs/code-mode-protocol/src/session.rs—CellId,CodeModeSession/Delegate/Provider,StartedCellcodex-rs/code-mode-protocol/src/runtime.rs—ExecuteRequest,WaitRequest,RuntimeResponse,CodeModeNestedToolCall, 기본 상수codex-rs/code-mode-protocol/src/response.rs—FunctionCallOutputContentItem,ImageDetailcodex-rs/code-mode-protocol/src/description.rs—EXEC_DESCRIPTION_TEMPLATE,ToolDefinition,parse_exec_source,build_exec_tool_description,render_json_schema_to_typescript,normalize_code_mode_identifiercodex-rs/code-mode/src/service.rs—CodeModeService(execute/wait/terminate/shutdown),run_cell_control, store/load 세션 격리 테스트codex-rs/code-mode/src/lib.rs— 크레이트 재노출codex-rs/code-mode/src/runtime/mod.rs—RuntimeCommand/RuntimeEvent,spawn_runtimecodex-rs/code-mode/src/runtime/callbacks.rs—tool_callback(JS→ToolCall 이벤트),text_callback,image_callbackcodex-rs/code-mode/src/runtime/globals.rs—install_globals(tools/ALL_TOOLS/store/load/yield_control/exit)codex-rs/code-mode-host/src/main.rs— (현재fn main(){}스텁; 원격 호스트 바이너리 자리)codex-rs/core/src/tools/code_mode/mod.rs—CodeModeService래퍼,call_nested_tool, payload 빌드,handle_runtime_responsecodex-rs/core/src/tools/code_mode/delegate.rs—CodeModeDispatchBroker/Worker,CoreTurnHost, notify 주입codex-rs/core/src/tools/code_mode/execute_handler.rs—CodeModeExecuteHandler(exec 처리, code cell trace)codex-rs/core/src/tools/code_mode/wait_handler.rs—CodeModeWaitHandler,ExecWaitArgscodex-rs/tools/src/code_mode.rs—augment_tool_spec_for_code_mode,collect_code_mode_tool_definitions,code_mode_name_for_tool_namecodex-rs/protocol/src/openai_models.rs—ToolMode { Direct, CodeMode, CodeModeOnly }codex-rs/core/src/unified_exec/mod.rs— 영속 PTY 셸 세션(보조)- 공식문서 아카이브(
/mnt/d/6study/10_프레임워크분석/_원문아카이브/codex)에 code-mode 언급 없음(미문서화 내부 기능, grep 확인) (경로 공통 접두사:/home/seunghyeong/harness-work/codex/)
[!tip] Codex 교차검증 메모 (원본 분석 보존)
- 주입 위치: 턴 시작 시
CodeModeService::start_turn_worker가tool_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-tool→hidden_dynamic_tool).CodeModeOnly에서는exec설명에 모든 nested 도구의 TS 선언과 네임스페이스 가이드가 통째로 인라인된다(build_exec_tool_description(code_mode_only=true)).