OpenAI Codex · Code Mode

모델 컨텍스트에 넣기 전에, 먼저 타입을 만드세요

승인 접두사, 작업 공간, AGENTS.md, 중단된 turn은 모두 모델이 스스로 볼 수 없는 사실을 알려 줍니다. Codex는 주입마다 먼저 타입을 만들고, 타입은 자신의 role, marker, body를 알아요.

강의 목표읽고 나면 세 가지를 말할 수 있어요. 첫째, 모델 컨텍스트에 넣는 글마다 왜 먼저 타입이 되어야 하는지. 둘째, 표시가 있는 것과 없는 것의 차이, 압축과 복원이 무엇으로 다시 알아보는지. 셋째, 모델로 보내는 메시지 한 줄이 어떤 필드로 분류되고 순서는 어디서 오는지요.
먼저 해보기 · 어떤 스위치를 켜느냐에 따라 봉투 안의 블록이 달라집니다
같은 라운드 입력: fragment 스위치 다섯 개, 메시지가 어떻게 조립되는지 보세요
켜기
스위치를 바꾸면 왼쪽 봉투가 바로 다시 정렬돼요. 재생을 눌러, 각 블록이 role과 marker를 어떻게 답하고 메시지 세 줄로 나뉘는지 보세요.
모델로 가는 봉투0개 메시지 · 0
출처 카드타입부터 묻기
논리 궤적 · 애니메이션 한 걸음이 소스의 어느 구간에 대응하는지
  1. role을 읽어 user인지 developer인지 정합니다fragment.rs L15
  2. 따로 한 줄이 필요한지 묻습니다fragment.rs L18
  3. 인스턴스가 marker를 원하고, 그걸로 render합니다fragment.rs L22
  4. 빈 marker는 body만 내고 절대 매칭되지 않습니다fragment.rs L41
  5. 창 정체는 루프 전에 단독 칸으로 밀어 넣습니다session/mod.rs L3661
  6. 나머지 fragment는 role과 marker로 분류합니다session/mod.rs L3677
  7. user 쪽은 레지스트리 .any()로 다시 알아봅니다contextual_user_message.rs L18
  8. developer 쪽은 접두사 표로 다시 알아봅니다event_mapping.rs L40
스위치를 켜거나 끄고, 모델에 최종으로 들어가는 내용이 어떤 블록으로 이뤄지는지 보세요. 재생을 누르면 분류가 보입니다.
구성은 어디서 오나합칠 수 있는 developer 구간을 먼저 한 메시지로 모으고, 따로 서야 하는 것은 각자 한 줄, user 구간은 다시 한 줄입니다. 순서는 타입 필드에 적혀 있어요. 문자열에 어떤 단어가 있는지는 도움이 안 됩니다.
나중에 알아볼 수 있나표시가 있는 것은 앞뒤 marker로 다시 알아봅니다. 없는 것은 기본으로 알아보지 못하고, developer 쪽이 접두사 표로 일부를 메워요.
왜 타입을 먼저 만드나조립 입구는 trait를 구현한 fragment만 받아요. 아무렇게나 format한 태그는 못 들어가고, matcher 목록도 등록되지 않은 태그를 알아보지 못합니다.
수업용 스케치: 스위치 조합과 본문 표현은 수업용 설정이고, 분류 순서는 build_initial_context_with_world_state에 맞췄어요. 논리 궤적 오른쪽 행 번호는 openai/codex 저장소 commit 4f39251a01에 대응합니다.
아이디어 1 · 주입은 먼저 타입이 됩니다
어떤 문제를 푸는가

에이전트에 이런 기능을 넣어 본 적 있을 거예요. 사용자가 방금 npm *를 승인했는데 다음 라운드 모델은 아직 npm test를 돌려도 되냐고 묻습니다. 아니면 압축이 막 끝났는데 모델이 작업 공간이 어디인지, 샌드박스가 읽기 전용인지 갑자기 잊어요. 아니면 세션을 fork했는데 옛 AGENTS.md가 그대로 있고 새 디렉터리 힌트가 겹쳐, 모델이 서로 싸우는 규칙 두 벌을 동시에 봅니다.

세 일의 공통 출처는 같은 종류의 연산입니다. 런타임에 모델 컨텍스트에 글 한 토막을 넣는 것이죠. 승인 접두사, 작업 공간, 권한 프로필, 중단된 turn, 현재 UTC 시간은 모두 모델이 스스로 못 보는 사실을 알려 줍니다. format! 한 줄이면 쓸 수 있어요.

문제는 그다음입니다. 그 글이 히스토리에 들어간 뒤, 압축이 남길지, 세션 복원이 알아볼지, UI가 숨길지, world state가 모델이 이미 아는 증거로 쓸지 — 전부 한 가지에 달렸어요. 순수 텍스트에서 그걸 다시 알아낼 수 있는가. 곳곳에 startsWith를 또 쓰면 몇 주 뒤 사방이 어긋납니다. 모르는 태그는 사용자 메시지 파싱으로도 새어 들어가요.

아이디어는 무엇인가

Codex는 주입마다 ContextualUserFragment를 구현한 타입으로 거둡니다. trait는 codex-core가 아니라 독립 crate codex-context-fragments에 있어요. 구현마다 한꺼번에 답해야 합니다. 이 토막이 Responses API에 user로 들어가나 developer로 들어가나, 앞뒤 marker는 무엇인가, 가운데 body는 어떻게 쓰나, 따로 한 메시지가 필요한가요.

render()는 앞뒤 marker와 body를 구분자 없이 이어 붙입니다. 공백과 줄바꿈은 body의 것이에요. 빈 marker는 body만 냅니다. into()는 렌더 결과를 content가 InputText 하나인 ResponseItem::Message로 거둡니다. 주입의 종점은 프로토콜 객체예요.

codex-rs/context-fragments/src/fragment.rs38–46행
    fn render(&self) -> String {
        let (start_marker, end_marker) = self.markers();
        let body = self.body();
        if start_marker.is_empty() && end_marker.is_empty() {
            return body;
        }

        format!("{start_marker}{body}{end_marker}")
    }
소스 스냅샷 안내: 로컬 저장소 openai/codex를 기준으로 codex-rs/context-fragments/src/fragment.rs를 대조했어요. commit 4f39251a01, 대조일 2026-08-22. 코드 블록은 소스 원문을 유지합니다. 타입이 스스로를 주입 가능한 텍스트로 접는 규칙의 전부가 이 구간이에요.

markers()self를 받아서 렌더는 인스턴스를 탑니다. type_markers()self가 없어 식별은 타입을 탑니다. 손에 히스토리 텍스트만 있고 원본 객체가 없어도, 이 토막이 어떤 fragment처럼 보이는지 물을 수 있어요. dyn ContextualUserFragmentmatches_text를 호출하지 못하고, 역방향 식별은 구체 타입을 짚어야 합니다.

주입을 하나 더하려면 파일을 만들고, trait를 구현하고, core/src/context/mod.rs에 걸어야 해요. 그 디렉터리에 모듈이 39개입니다. 이 마찰 자체가 규율이에요. 임시 format!는 통과하지 못합니다. 조립 함수가 받는 것이 Box<dyn ContextualUserFragment>이기 때문이죠.출처: codex-rs/core/src/context/mod.rs 3–41행; codex-rs/core/src/session/mod.rs 3677–3707행

런타임 사실 작업 공간, 지시, 중단 fragment 타입 role · markers · body render 이어 붙임 Message 나중에는 InputText 한 토막만 남음 type_markers로 구체 타입을 묻고, matches_text가 앞뒤를 보며, 인스턴스 없이도 다시 알아봅니다
수업용 구조도: 렌더는 인스턴스를 타고, 식별은 타입을 타며, 두 호출이 같은 marker 쌍을 읽어요.
왜 오래가는가

렌더와 식별이 정의 하나를 공유합니다. 다른 언어로 다시 써도 최소 형태는 인터페이스 하나, 레지스트리 하나, 같은 marker 쌍이에요. 조립 함수 인자를 fragment 타입으로 쓰면, 업무 쪽은 XML을 아무렇게나 붙이는 호출점을 잃습니다. 식별 함수는 레지스트리에서만 맞추고, text.startsWith를 또 쓰면 안 돼요.

아이디어 2 · 표시 있음과 없음을 분명히 적으세요
어떤 문제를 푸는가

주입마다 표시를 달면, 일회성 알림도 압축 뒤에 다시 알아보고 한 번 더 먹입니다. 표시를 전부 빼면, 압축된 히스토리에서 사용자 원문과 런타임에 넣은 설명을 가릴 수 없어요. fork는 만료된 <environment_context>를 사용자 입력으로 다시 제출합니다.

아이디어는 무엇인가

창 정체, 환경, 중단, 이미지 축소, 관리 측 개발자 지시는 나중에 알아보고, diff하고, 압축 뒤 다시 넣을지 정해야 해서 begin과 end를 답니다. 일회성 알림 — 승인 접두사, 네트워크 규칙 등록, 남은 token 한 줄 알림 — 은 type_markers()가 빈 문자열 둘을 돌려줍니다. 기본 matches_text는 언제나 false예요.

매칭 규칙은 텍스트 전체의 앞뒤만 봅니다. 앞을 trim해 접두사를 비교하고, 뒤를 trim해 접미사를 비교하며, ASCII 대소문자는 가리지 않아요. 둘 다 맞아야 히트입니다. 가운데가 무엇인지는 상관없고, 표시 없는 fragment는 가역성을 스스로 포기합니다.출처: codex-rs/context-fragments/src/fragment.rs 89–103행

developer 쪽에는 event_mapping.rs에 접두사 표가 하나 더 있어, 표시 없는 식별을 일부 메웁니다. 범위는 user 쪽 matcher 목록보다 좁아요. 접두사 표에는 옛 <token_budget> 태그가 아직 남아 있고, 주석은 옛 버전이 남겨 둔 포장을 알아보기 위해서라고 적습니다. marker가 rollout에 쓰이는 순간 복원 계약의 일부가 되고, 태그를 바꾸는 것은 프로토콜을 바꾸는 것과 같아요.

UserInstructions에는 눈을 놓치기 쉬운 곳이 있어요. begin marker는 마크다운 제목 # AGENTS.md instructions이고, end marker는 </INSTRUCTIONS>입니다. 프로토콜에는 <user_instructions> 쌍이 따로 있지만 이 struct는 쓰지 않아요. 프로토콜 상수로 matches_text를 쓰면, 저장소가 실제로 렌더하는 텍스트를 놓칩니다.출처: codex-rs/core/src/context/user_instructions.rs 18–19행; codex-rs/protocol/src/protocol.rs 112–113행

표시 있음 · 나중에 알아봄 환경 / 창 / 중단 begin + body + end matches_text가 알아봄 표시 없음 · 그때 한 번만 승인 접두사 / 네트워크 규칙 body만 출력 기본 식별 포기 developer는 접두사 표로 보완
수업용 대조: 압축 뒤 다시 알아봐야 하는 것은 프로토콜로 만들고, 일회성 알림은 가역성을 스스로 포기합니다.
왜 오래가는가

나중에 알아봐야 하는 것은 프로토콜로 만들고, 일회성은 스스로 포기하며 타입에 분명히 적어요. 압축 뒤 다시 알아봐야 하면 begin과 end를 강제하고, 일회성 알림은 표시가 없어도 되지만 주석에 가역성 포기를 써야 합니다.

아이디어 3 · 조립은 타입 필드로 분류합니다
어떤 문제를 푸는가

곳곳에서 문자열을 아무렇게나 push하고, 순서는 약속에, 단독 줄은 주석에 기대요. TokenBudgetContext는 권한 설명과 같은 developer 메시지에 끼어 듭니다. 압축 필터는 줄 단위로 처리하지 못하고, 섞인 뒤에는 롤백도 쪼개지 못해요. event_mapping.rs 주석도 인정합니다. build_initial_context가 contextual fragment와 지속 developer 텍스트를 묶을 수 있다고.출처: codex-rs/core/src/event_mapping.rs 69–71행

아이디어는 무엇인가

첫 조립은 Session::build_initial_context_with_world_state에서 일어나요. fragment를 role(), markers()의 앞, requires_separate_message()로 나눕니다. 합칠 수 있는 developer 구간, 따로 서야 하는 developer 구간, user 구간, 맨 위나 맨 아래에 둘 특별 구간이요.

창 정체 한 줄은 조금 특별해요. Feature::TokenBudget이 켜지고 모델에 context window가 있으면, world_state 루프 전에 단독 칸으로 밀어 넣습니다. 출력 순서는 합쳐진 developer 메시지 하나, 그다음 단독 developer 메시지들, 그다음 contextual user 메시지 하나예요. 분류 근거는 타입 필드입니다.출처: codex-rs/core/src/session/mod.rs 3630–3728행

requires_separate_message()는 다른 것과 같은 developer 메시지에 끼일 수 있는지도 타입에 넣어요. TokenBudgetContext, ImageResizeNotice, ManagedDeveloperInstructions는 따로 서기를 고릅니다. 대가는 메시지 한 줄과 role 전환 한 번이 더 드는 것이고, 이점은 압축 필터가 줄 단위로 처리할 수 있다는 점입니다.

켜진 타입 승인 접두사 창 정체 환경 정보 AGENTS.md 중단 알림 분류 role() markers().0 requires_separate 필드를 보고, 본문은 훑지 않음 developer · 한 줄로 합침 developer · 각자 한 줄 user · contextual 한 줄로
수업용 분류도: 출력 순서는 합친 developer, 단독 developer, contextual user예요.
모델 입력의 어떤 토막을 짚어도, 어떤 fragment 타입으로 돌아갈 수 있어요.
왜 오래가는가

조립 입구는 등록된 타입만 받아요. 등록되지 않은 주입을 문 밖에 두는 흔한 모양입니다. 타입은 미등록을 막지만, 등록되고도 라운드마다 40KB를 넣는 것은 막지 못해요. 그 층은 리뷰 규칙이고, 다음 장에서 펼칩니다.

가로 비교 · 같은 문제의 다른 답

게이트는 어디에: DeepSeek Harness는 요청을 보낼 때 대사합니다

DSH는 저장소 루트 AGENTS.md 107행에 적혀 있고, 중국어 원칙은 “모델에 보이면 이미 기록됨”으로 접혀요. 모델 요청에 닿는 모든 것은 세션 로그에서 다시 만들 수 있어야 하고, 모델이 볼 입력 하나를 더하면 세션 이벤트도 하나 더 필요합니다.

실행 면은 invariant.ts예요. llm/stream에 리스너를 걸고, 세션 로그에서 deriveMessages()로 기댓값을 얻은 뒤 나가려는 options.messagesJSON.stringify로 비교합니다. 안 맞으면 fail이에요. 게이트는 붕괴 지점에 있어, 조립 뒤에 messages를 고치는 식의 런타임에만 나타나는 드리프트를 잡을 수 있어요. invariants 서비스를 끄면 이 게이트도 사라집니다.

양쪽 소스 모두 확인 · 2026-08-22 · DSH · Model-visible ⟺ logged

참된 출처가 다름: 이벤트 로그인가, 닫힌 타입 집합인가

DSH는 fragment trait가 없어도 됩니다. 참된 출처가 이벤트 로그이고, messages는 투영이니까요. Codex는 컨텍스트에 나타날 수 있는 것을 먼저 닫힌 타입 집합으로 거둔 뒤, marker로 나중에 알아봅니다.

대가도 다릅니다. Codex의 타입은 trait 없이 render_full에 넣는 길을 막지만, impl 안에서 format!한 동적 문자열은 막지 못해요. matches_text가 알아보는 것은 텍스트 모양입니다. 양쪽 모두 추적 가능성에 값을 치르고, 한쪽은 요청마다 어서션으로, 다른 쪽은 주입을 더할 때마다 타입 마찰로 냅니다.

양쪽 소스 모두 확인 · 2026-08-22
수업 실습
01

놓치는 토막은 어느 쪽일까요

UserInstructions의 begin marker는 # AGENTS.md instructions인데, 프로토콜 상수는 <user_instructions>예요. 누군가 프로토콜 상수로 matches_text를 쓰면, 지금 저장소가 실제로 렌더하는 어떤 텍스트를 놓칠까요?

한 걸음 더: 위 데모에서 승인 접두사를 켜고, 압축 뒤 기본 matches_text가 그걸 다시 알아볼 수 있는지 보세요. developer 쪽은 어느 표로 이 한 칼을 메우며, 상수를 잘못 바꾸면 컴파일러가 울릴까요.

Takeaway:모델 컨텍스트에 넣는 토막마다 먼저 타입이 되고, 타입은 자신의 role, marker, body를 압니다. 나중에 알아봐야 하는 것은 begin과 end를 달고, 일회성 알림은 가역성을 스스로 포기해요. 조립은 타입 필드로 분류하고, 등록되지 않은 문자열은 봉투에 들어가지 못합니다.