Codex · 프롬프트·컨텍스트 조립 (base 프롬프트 + AGENTS.md + 환경/스킬/권한 fragment)

한 줄 요약

Codex가 모델에 보내는 한 번의 요청은 고정 base 프롬프트(취업규칙) 위에 “지금 이 순간의 사실” 조각(포스트잇)을 정해진 순서로 쌓은 한 덩어리다. 모델이 왜 특정 행동을 하는지(권한·위치·프로젝트 규칙)는 모두 이 조립 단계에서 결정된다. 여기를 알면 Codex의 판단 근거를 통제할 수 있다.

그림

flowchart TD
  A[턴 시작] --> B[build_initial_context 호출]
  B --> C1["base 프롬프트 선택<br/>설정 오버라이드 > 히스토리 > 모델 기본"]
  B --> D1["권한·스킬·플러그인·성격 조각<br/>→ developer 묶음"]
  B --> D2["AGENTS.md 찾아 연결 + 환경 정보<br/>→ user 묶음"]
  D1 --> E[developer 메시지 1개]
  D2 --> F[user 메시지 1개]
  C1 --> G[Prompt.base_instructions 칸]
  E --> H[Prompt.input 칸]
  F --> H
  G --> I[Responses API 요청 발송]
  H --> I
  I --> J[모델 응답]
  J --> K{"대화창이 한계를 넘었나?"}
  K -- 예 --> L[compact 가 요약으로 압축]
  K -- 아니오 --> A

쉽게 풀기

1단계 — base = “전 직원 취업규칙”. 요청의 맨 밑판은 base instructions(시스템 프롬프트)다. “너는 Codex다, 이렇게 행동하라”는 고정 헌법이며 모델마다 prompt.md 한 장으로 정해진다. 매번 새로 쓰지 않고 늘 같은 판이 깔린다.

2단계 — 그 위에 “오늘 책상 포스트잇”을 붙인다. 취업규칙만으로는 지금 상황을 모른다. 그래서 상황 조각(fragment)을 얹는다 — 어느 폴더(cwd)·셸·날짜·권한·쓸 수 있는 스킬/플러그인, 그리고 프로젝트 AGENTS.md 규칙. 각각이 포스트잇 한 장이다.

3단계 — 포스트잇마다 “이름표”와 “붙일 자리”. 각 조각은 고유 마커(예: <environment_context>…</environment_context>)와 역할(role) 을 갖는다. 정책·도구 안내는 developer, “지금의 사실”은 user다. 이름표가 있어야 대화가 길어졌을 때 Codex가 옛 조각을 찾아 갈아끼울 수 있다.

4단계 — 같은 역할끼리 한 봉투로. developer 조각은 모여 developer 메시지 1개, user 조각은 user 메시지 1개가 된다. base는 별도 칸(base_instructions)에 담긴다. 이 셋이 합쳐져 한 요청이 된다.

아래 그림이 이 4단계의 흐름이다.

flowchart LR
  P1["② 상황 조각들<br/>(포스트잇)"] --> R{③ role 분류}
  R -->|developer| DV["권한·스킬·플러그인·성격"]
  R -->|user| US["AGENTS.md·환경"]
  DV --> M1["④ developer 메시지 1개"]
  US --> M2["④ user 메시지 1개"]
  B1["① base 취업규칙"] --> BK["base_instructions 칸"]
  M1 --> REQ[한 번의 모델 요청]
  M2 --> REQ
  BK --> REQ

핵심 비유

base = 회사 취업규칙(전 직원 동일). fragment = 그날 책상 포스트잇(오늘 위치·열쇠 권한·쓸 도구). 마커 = 포스트잇 제목(떼고 새로 붙일 때 찾는 용도).

핵심 정리

조립에 쓰이는 세 가지 그릇과, 조각이 지켜야 할 공통 계약은 다음과 같다.

그릇담는 것요청의 어디로
base_instructions시스템 프롬프트 전문(고정 헌법)instructions 필드
developer 묶음권한·스킬·플러그인·성격 등 정책/능력input의 developer 메시지
user 묶음AGENTS.md·환경 등 “지금의 사실”input의 user 메시지

조각의 공통 계약(ContextualUserFragment 트레잇): role()=들어갈 역할(developer/user), markers()/type_markers()=시작·끝 이름표(식별·교체용), body()=이름표 사이 본문, render()=이름표+본문을 구분자 없이 이어붙임.

AGENTS.md는 cwd에서 프로젝트 루트까지 거슬러 올라가며 경로상의 파일을 순서대로 모아 연결한다. 그 발견 규칙을 그림으로 보면 다음과 같다.

flowchart TD
  S[cwd에서 시작] --> U{"루트 마커<br/>.git 도달?"}
  U -- 아니오 --> UP[상위 폴더로 이동] --> U
  U -- 예 --> ROOT[프로젝트 루트 확정]
  ROOT --> COL["루트→cwd 경로의<br/>모든 AGENTS.md 순서대로 수집"]
  COL --> JOIN["첫 doc 앞에 --- project-doc --- 삽입"]
  JOIN --> BUD{"project_doc_max_bytes<br/>예산 초과?"}
  BUD -- 예 --> TR[초과분 truncate]
  BUD -- 아니오 --> OK[연결 완료]
  TR --> OK

실제 예시

Prompt 구조체가 한 턴 요청의 페이로드다. baseBaseInstructions { text: String }(protocol/src/models.rs)로 감싸지며 기본값은 BASE_INSTRUCTIONS_DEFAULT = include_str!("prompts/base_instructions/default.md").

조립 순서 — build_initial_context

build_initial_context는 3개 버킷에 각 fragment를 render() 해서 push한 뒤 합친다. 버킷에서 메시지가 되는 흐름은 다음과 같다.

flowchart TD
  subgraph DEV[developer_sections 순서]
    d1[model-switch] --> d2[Permissions] --> d3[developer_instructions]
    d3 --> d4[Collaboration] --> d5[realtime] --> d6[Personality]
    d6 --> d7[Apps] --> d8[Skills] --> d9[Plugins]
    d9 --> d10[확장 contributor] --> d11[TokenBudget]
  end
  subgraph USR[contextual_user_sections 순서]
    u1[확장 contributor] --> u2["UserInstructions=AGENTS.md"] --> u3["EnvironmentContext+subagents"]
  end
  DEV --> BD["build_developer_update_item<br/>role=developer 메시지 1개"]
  USR --> BU["build_contextual_user_message<br/>role=user 메시지 1개"]
  BD --> NOTE["섹션은 구분자 join이 아니라<br/>섹션마다 별도 ContentItem::InputText"]
  BU --> NOTE

separate_developer_sections는 별도 developer 메시지로 분리되는 항목들이다. (session/mod.rs:2871, context_manager/updates.rs:191)

새 fragment 직접 추가하기

// my_crate/src/context/my_context.rs
use codex_context_fragments::ContextualUserFragment;
 
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MyContext { pub note: String }
 
impl ContextualUserFragment for MyContext {
    fn role(&self) -> &'static str { "developer" }            // 1) 역할
    fn markers(&self) -> (&'static str, &'static str) { Self::type_markers() }
    fn type_markers() -> (&'static str, &'static str) {
        ("<my_context>", "</my_context>")                     // 2) 고유 마커
    }
    fn body(&self) -> String {                                 // 3) 본문(개행 직접 포함)
        format!("\n  {}\n", self.note)
    }
}
// build_initial_context 에서 등록
if turn_context.config.include_my_context {
    developer_sections.push(MyContext { note: "...".into() }.render());
}

추가할 때 체크리스트

  • roledeveloper(정책/능력) 또는 user(AGENTS.md·환경 같은 사실)로 맞게 골랐는가
  • type_markers()가 다른 조각과 충돌하지 않는 고유 태그인가
  • body()가 마커와 붙어도 안 깨지게 자기 줄바꿈을 직접 넣었는가 (render는 구분자 안 넣음)
  • base 변경은 config.base_instructions 오버라이드나 models.json 모델 항목 수정(둘 다 instructions 필드로 감)
  • AGENTS.md를 키우면 project_doc_max_bytes 한도 확인(초과 시 truncate)

요약 & 셀프체크

3줄 요약:

  1. 한 번의 모델 요청 = 고정 base + 상황 조각(fragment)을 정해진 순서로 쌓은 것.
  2. 각 조각은 고유 마커와 역할(developer/user)을 갖고, 같은 역할끼리 한 메시지로 묶여 Prompt.input에, base는 base_instructions 칸에 따로 들어간다.
  3. 마커가 붙는 이유는 대화가 길어졌을 때 옛 조각을 찾아 교체·압축(compact)하기 위해서다.

스스로 답해보기:

  • AGENTS.md 조각과 환경 정보 조각은 왜 둘 다 user이고, 권한·스킬 조각은 왜 developer인가?
  • 같은 모델에서 base 프롬프트를 바꾸려면 어디를 건드려야 하나? (우선순위 3단계 중 어느 것?)
  • 조각마다 마커가 없다면 대화가 길어졌을 때 무엇을 못 하게 되는가?

연결

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