7 min read
AI assisted

Codex App Server Python SDK — JSON-RPC v2 stdio 호출

codex app-server를 호출하는 Python SDK — 설치, 첫 호출, thread 모델, 주요 메서드

OpenAI Codex CLI는 GPT-5 기반 코딩 에이전트를 터미널에서 대화형으로 구동하는 오픈소스 도구입니다(github.com/openai/codex). 평소에는 TUI 환경에서 직접 사용하지만, codex app-server 명령을 실행하면 JSON-RPC v2 서버 모드로 진입해 외부 프로그램이 stdio를 통해 에이전트를 제어할 수 있습니다.

이 Python SDK는 그 stdio 인터페이스를 Python 객체 모델로 래핑합니다. 공식 SDK 소스는 github.com/openai/codex/tree/main/sdk/python에 있습니다. snake_case ↔ camelCase 자동 변환, Pydantic wire 모델, 동기/비동기 호출 모드를 모두 제공합니다.


왜 의미가 있는가

그동안 OpenAI Codex의 GPT-5 기반 코딩 에이전트는 CLI/TUI 형태로만 쓸 수 있었습니다. 외부 워크플로에 끼워 넣으려면 subprocess를 띄우고 표준 출력을 파싱하거나 CLI를 직접 fork해야 했습니다.

이 SDK는 그 우회를 없앱니다. Anthropic의 Claude Agent SDK가 Claude Code 런타임을 Python·TypeScript에 임베드할 수 있게 만든 것과 같은 자리에 있는, OpenAI 진영의 대응입니다.

결과적으로 GPT-5 기반 코딩 에이전트를 평가·자동화·내부 도구에 통합하는 표준 경로가 생깁니다. CLI를 reverse-engineer하지 않아도 됩니다.

한 줄 정리: Claude Agent SDK ↔ Codex App Server SDK — 두 진영의 코딩 에이전트가 공식 SDK로 임베드 가능해진 시점입니다.


codex app-server SDK가 하는 일

Codex CLI는 app-server 서브커맨드를 통해 JSON-RPC v2 프로토콜을 stdio로 노출합니다. 이 채널을 통해 thread 생성, turn 실행, 이벤트 수신을 요청할 수 있습니다. SDK는 그 프로토콜 위에서 세 가지 작업을 담당합니다.

wire 포맷과 Python 코드 사이의 명명 규칙 변환이 자동으로 처리됩니다. 서버가 사용하는 camelCase 필드(approvalPolicy, baseInstructions, developerInstructions 등)는 SDK 공개 API에서 모두 snake_case(approval_policy, base_instructions, developer_instructions)로 노출됩니다. 직렬화 과정에서 원래 wire 형식으로 되돌아가는 구조입니다.

Pydantic 모델이 wire 계층 전반에 사용됩니다. 생성된 모델은 codex_app_server.generated.v2_all에 있으며, 편의 래퍼는 패키지 루트에서 직접 임포트할 수 있습니다.

런타임 바이너리는 openai-codex-cli-bin이라는 별도 패키지에 묶입니다. SDK 패키지(openai-codex-app-server-sdk)와 런타임 패키지는 버전이 반드시 일치해야 합니다. 배포된 SDK 빌드는 런타임 버전을 exact pin으로 고정합니다.


설치

Python 3.10 이상과 uv가 필요합니다. 저장소 루트에서 가상환경을 만들고 의존성을 설치합니다.

uv venv
uv pip install -r requirements.txt -e .
source .venv/bin/activate

설치 전에 로컬 Codex 인증과 세션이 구성되어 있어야 합니다. 배포된 SDK 빌드는 openai-codex-cli-bin 런타임 패키지를 exact 버전으로 고정합니다. 로컬 저장소 개발 환경에서는 AppServerConfig(codex_bin=...) 옵션으로 바이너리 경로를 직접 지정할 수 있습니다.

from codex_app_server import Codex
from codex_app_server.config import AppServerConfig

config = AppServerConfig(codex_bin="/path/to/local/codex")
with Codex(config=config) as codex:
    ...

첫 호출

컨텍스트 매니저로 Codex() 인스턴스를 열고, thread_start()로 새 thread를 만든 뒤, thread.run()으로 turn을 실행합니다.

from codex_app_server import Codex

with Codex() as codex:
    thread = codex.thread_start(model="gpt-5.4", config={"model_reasoning_effort": "high"})
    result = thread.run("Say hello in one sentence.")

    print("Thread:", thread.id)
    print("Text:", result.final_response)
    print("Items:", len(result.items))

Codex()는 eager 초기화 방식으로, 생성자에서 즉시 프로세스를 시작하고 initialize를 호출합니다. 컨텍스트 매니저(with Codex() as codex:)를 사용하면 종료 시 close()가 자동으로 호출됩니다.

같은 thread에서 후속 turn을 실행하면 대화 맥락이 이어집니다.

with Codex() as codex:
    thread = codex.thread_start(model="gpt-5.4", config={"model_reasoning_effort": "high"})

    first = thread.run("Summarize Rust ownership in 2 bullets.")
    second = thread.run("Now explain it to a Python developer.")

    print(first.final_response)
    print(second.final_response)

비동기 환경에서는 AsyncCodex를 사용합니다. AsyncCodex는 lazy 초기화 방식으로, 컨텍스트 진입 시점에 시작됩니다.

import asyncio
from codex_app_server import AsyncCodex

async def main() -> None:
    async with AsyncCodex() as codex:
        thread = await codex.thread_start(model="gpt-5.4", config={"model_reasoning_effort": "high"})
        result = await thread.run("Continue where we left off.")
        print(result.final_response)

asyncio.run(main())

thread 모델

SDK의 실행 단위는 thread와 turn 두 가지입니다.

Thread는 대화 상태를 담는 컨테이너입니다. thread_start()로 새로 만들거나, thread_resume(thread_id)로 기존 thread를 이어받습니다. 하나의 Codex 클라이언트에서 여러 thread를 가질 수 있습니다.

Turn은 thread 안에서 일어나는 단일 모델 실행입니다. thread.run()을 호출할 때마다 새 turn이 시작됩니다.

thread.run()이 반환하는 RunResult는 세 필드를 포함합니다.

필드 타입 설명
final_response str | None turn의 최종 응답 텍스트
items list[ThreadItem] turn 중 수집된 항목 목록
usage ThreadTokenUsage | None 토큰 사용량

final_responseNone인 경우가 있습니다. turn이 final-answer 항목이나 phase 없는 assistant message 항목 없이 완료되었을 때입니다. 이벤트 수준의 제어가 필요할 때는 thread.turn()으로 TurnHandle을 받아 stream(), steer(), interrupt()를 사용합니다.

기존 thread ID를 알고 있다면 resume으로 이어받을 수 있습니다.

with Codex() as codex:
    thread = codex.thread_resume("thr_123")
    result = thread.run("Continue where we left off.")
    print(result.final_response)

주요 메서드

Codex / AsyncCodex

thread_start(...) -> Thread 새 thread를 만듭니다. model, config, base_instructions, developer_instructions, approval_policy, sandbox, cwd 등을 키워드 인자로 받습니다.

thread_resume(thread_id, ...) -> Thread 기존 thread를 이어받습니다. 인자 구성은 thread_start와 동일합니다.

thread_fork(thread_id, ...) -> Thread 기존 thread를 분기합니다. 같은 대화 상태에서 다른 방향으로 탐색할 때 사용합니다.

thread_list(...) -> ThreadListResponse thread 목록을 조회합니다. cursor, limit, archived, sort_key, source_kinds 등의 필터를 지원합니다.

thread_archive(thread_id) / thread_unarchive(thread_id) thread를 보관하거나 복원합니다.

models(*, include_hidden=False) -> ModelListResponse 사용 가능한 모델 목록을 반환합니다.

Thread / AsyncThread

run(input, ...) -> RunResult turn을 시작하고 완료까지 이벤트를 소비한 뒤 결과를 반환합니다. input은 문자열 또는 Input 타입을 받습니다. effort, model, output_schema, service_tier 등 turn 단위 파라미터를 재정의할 수 있습니다.

turn(input, ...) -> TurnHandle low-level turn 제어가 필요할 때 사용합니다. 반환된 TurnHandle에서 stream(), steer(), interrupt()를 호출합니다.

read(*, include_turns=False) -> ThreadReadResponse thread 상태와 항목을 읽습니다.

compact() -> ThreadCompactStartResponse thread 컨텍스트를 압축합니다. 긴 대화 후 컨텍스트 길이를 줄일 때 사용합니다.

TurnHandle / AsyncTurnHandle

stream() -> Iterator[Notification] raw 알림을 이벤트 단위로 순회합니다. 진행 상황 표시, 커스텀 타임아웃, 세밀한 파싱이 필요할 때 선택합니다.

run() -> Turn 이벤트를 소비하고 완료 시 생성된 Turn 모델을 반환합니다. Thread.run()의 편의 래퍼와 달리 canonical generated Turn을 반환합니다.

steer(input) -> TurnSteerResponse 진행 중인 turn에 추가 입력을 보냅니다.

interrupt() -> TurnInterruptResponse 진행 중인 turn을 중단합니다.

현재 실험적 빌드에서는 하나의 Codex 클라이언트 인스턴스에 동시에 하나의 active turn consumer만 허용됩니다. Thread.run(), TurnHandle.stream(), TurnHandle.run() 중 두 개를 동시에 시작하면 RuntimeError가 발생합니다.


실전 패턴

examples/ 디렉토리에는 14개 예제가 있습니다. 주요 항목을 소개합니다.

03_turn_stream_eventsTurnHandle.stream()으로 raw notification을 이벤트 단위로 처리하는 패턴입니다. 진행 상황을 실시간으로 표시하거나 특정 이벤트 타입만 선별할 때 참고합니다.

06_thread_lifecycle_and_controls — thread archive, unarchive, fork, compact를 포함한 전체 생명주기 흐름입니다.

10_error_handling_and_retryretry_on_overloadis_retryable_error를 조합한 에러 처리 패턴입니다.

11_cli_mini_app — 터미널 REPL 형태의 완성된 미니 앱입니다. 입력 루프, 다중 turn, thread 재개를 하나의 스크립트에서 확인할 수 있습니다.

이미지 입력이 필요하면 07_image_and_text(URL 기반)와 08_local_image_and_text(로컬 파일 기반)를 참고합니다. SDK는 ImageInput(url=...), LocalImageInput(path=...) 두 입력 클래스를 제공합니다.

from codex_app_server import Codex, LocalImageInput

with Codex() as codex:
    thread = codex.thread_start(model="gpt-5.4")
    result = thread.run([LocalImageInput(path="/tmp/screenshot.png"),
                         "이 코드에서 버그를 찾아줘."])
    print(result.final_response)

운영상 주의

런타임 바이너리 버전 고정 SDK 패키지와 openai-codex-cli-bin 런타임 패키지의 버전은 반드시 일치해야 합니다. 배포된 SDK 빌드는 exact pin을 사용하므로 직접 버전을 조정하지 않는 것이 안전합니다. 로컬 개발 환경에서 런타임 패키지를 별도로 설치하지 않았다면 AppServerConfig(codex_bin=...) 오버라이드가 필수입니다.

Codex() 생성자 실패 원인 Codex()는 eager 초기화 방식이라 생성자에서 곧바로 프로세스 시작과 initialize 호출이 일어납니다. 실패하는 주요 원인은 openai-codex-cli-bin 미설치, codex_bin이 가리키는 파일 부재, 로컬 Codex 인증 누락, 그리고 app-server 버전 불호환입니다. 오류 메시지에서 이 네 가지를 먼저 확인합니다.

stdio 버퍼링 JSON-RPC v2 over stdio 구조상 버퍼링 문제가 발생할 수 있습니다. 장시간 실행 프로세스나 대용량 응답에서 turn이 완료 신호(turn/completed) 없이 멈추는 것처럼 보이면 stdio 버퍼 설정을 점검합니다. run()은 완료 이벤트가 도착할 때까지 자동으로 대기하지만, stream()으로 직접 소비할 때는 완료까지 반드시 루프를 유지해야 합니다.

에러 유형 구분 retry_on_overload()ServerBusyError 같은 일시적 과부하 에러에만 재시도를 적용합니다. InvalidParamsErrorMethodNotFoundError는 입력 수정이나 버전 호환성 확인이 필요한 에러이므로 재시도 대상이 아닙니다. is_retryable_error(exc)로 예외 유형을 확인한 뒤 처리 방식을 분기합니다.

모델 선택과 추론 노력 thread_start(model="gpt-5.4", config={"model_reasoning_effort": "high"})와 같이 thread 생성 시 모델과 추론 노력을 지정합니다. thread.run(..., model=..., effort=...)로 turn 단위에서 재정의할 수도 있습니다. 사용 가능한 모델 전체 목록은 codex.models()로 조회합니다.

하나의 클라이언트, 하나의 active turn 현재 실험적 빌드에서는 Codex 인스턴스 하나에 동시에 하나의 turn consumer만 허용됩니다. 병렬 실행이 필요하면 Codex 인스턴스를 별도로 생성해야 합니다.


마무리

codex app-server Python SDK는 JSON-RPC v2 over stdio라는 낮은 수준의 프로토콜을 Python 관용 인터페이스로 감쌉니다. thread 기반 멀티턴 대화, 이벤트 스트리밍, 기존 thread 재개, 이미지 입력이 하나의 API 안에 들어옵니다. 로컬 Codex 프로세스를 Python 스크립트에서 직접 제어하거나 멀티턴 에이전트 흐름을 구성해야 할 때 적합합니다. Claude Code SDK와의 설계 차이는 별도 글에서 다룰 예정입니다.


참고