Codex · 프롬프트·컨텍스트 조립 (base 프롬프트 + AGENTS.md + 환경/스킬/권한 fragment)
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
[!note] 핵심 비유 base = 회사 취업규칙(전 직원 동일). fragment = 그날 책상 포스트잇(오늘 위치·열쇠 권한·쓸 도구). 마커 = 포스트잇 제목(떼고 새로 붙일 때 찾는 용도).
핵심 정리
조립에 쓰이는 세 가지 그릇과, 조각이 지켜야 할 공통 계약은 다음과 같다.
| 그릇 | 담는 것 | 요청의 어디로 |
|---|---|---|
base_instructions | 시스템 프롬프트 전문(고정 헌법) | instructions 필드 |
| developer 묶음 | 권한·스킬·플러그인·성격 등 정책/능력 | input의 developer 메시지 |
| user 묶음 | AGENTS.md·환경 등 “지금의 사실” | input의 user 메시지 |
조각의 공통 계약(ContextualUserFragment 트레잇): role()=들어갈 역할(developer/user), markers()/type_markers()=시작·끝 이름표(식별·교체용), body()=이름표 사이 본문, render()=이름표+본문을 구분자 없이 이어붙임.
[!note]- 펼쳐보기: base 프롬프트 선택 우선순위 + 주요 fragment 목록 base 선택 우선순위 (
core/src/session/mod.rs:570~584)
- ①
config.base_instructions오버라이드가 있으면 그것- ② 세션 재개 시 히스토리의
session_meta.base_instructions- ③ 둘 다 없으면 현재 모델의 기본 base
모델별 base는
models.json의 각 모델base_instructions필드에 인라인 텍스트로 박혀 있다.core/gpt-5.2-codex_prompt.md등prompt.md는 사람용 소스 사본일 뿐, 어떤.rs/.toml도include_str!로 직접 가져가지 않는다(grep 확인).주요 fragment 한눈에 보기
- UserInstructions(=AGENTS.md) — role
user, 마커# AGENTS.md instructions … </INSTRUCTIONS>- EnvironmentContext — role
user,<environment_context>…</environment_context>(cwd·shell·date·timezone·network·filesystem·subagents)- AvailableSkillsInstructions — role
developer,<skills_instructions>…- PermissionsInstructions — role
developer,<permissions instructions>…(샌드박스·승인정책·writable roots·denied reads)- AvailablePluginsInstructions / AppsInstructions — role
developer, 플러그인·MCP 커넥터 목록- PersonalitySpec / CollaborationMode / TokenBudgetContext — role
developer, 성격·협업모드·컨텍스트 메타
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
[!note]- 펼쳐보기: AGENTS.md 발견 규칙 전문 (
core/src/agents_md.rs)
project_root_markers(기본.git)를 만날 때까지 cwd에서 위로 올라가 루트를 정한다- 루트→cwd 경로상의 모든
AGENTS.md(+AGENTS.override.md+fallback 파일명)를 순서대로 모아 연결한다. 루트 위로는 안 올라간다- 첫 project doc 앞에
\n\n--- project-doc ---\n\n구분자를 넣는다project_doc_max_bytes예산을 넘으면 잘라낸다(truncate)
실제 예시
Prompt 구조체가 한 턴 요청의 페이로드다. base는 BaseInstructions { text: String }(protocol/src/models.rs)로 감싸지며 기본값은 BASE_INSTRUCTIONS_DEFAULT = include_str!("prompts/base_instructions/default.md").
[!note]- 펼쳐보기: Prompt 구조체 필드 + fragment 렌더 코드 전문 Prompt 구조체 (
codex-rs/core/src/client_common.rs)// input: 대화 입력 아이템(base 위에 쌓인 fragment + 히스토리) — 조립된 컨텍스트가 여기로 // tools: 사용 가능한 툴(내장 + MCP 서버 툴) // parallel_tool_calls: 병렬 툴콜 허용 여부 // base_instructions: 시스템 프롬프트 본문 → Responses API의 instructions 필드로 전달 // personality(opt) / output_schema(opt) / output_schema_strict(기본 true)AGENTS.md 조각의 마커·body (
core/src/context/user_instructions.rs)impl ContextualUserFragment for UserInstructions { fn role(&self) -> &'static str { "user" } fn markers(&self) -> (&'static str, &'static str) { Self::type_markers() } fn type_markers() -> (&'static str, &'static str) { ("# AGENTS.md instructions", "</INSTRUCTIONS>") } fn body(&self) -> String { let directory = self.directory.as_ref() .map(|directory| format!(" for {directory}")) .unwrap_or_default(); format!("{directory}\n\n<INSTRUCTIONS>\n{}\n", self.text) } }렌더 결과(개념):
# AGENTS.md instructions for /repo <INSTRUCTIONS> (AGENTS.md 내용) </INSTRUCTIONS>EnvironmentContext body 렌더링 (
core/src/context/environment_context.rs)fn body(&self) -> String { let mut lines = Vec::new(); lines.push(format!(" <cwd>{}</cwd>", environment.cwd.to_string_lossy())); lines.push(format!(" <shell>{}</shell>", environment.shell)); if let Some(current_date) = &self.current_date { lines.push(format!(" <current_date>{current_date}</current_date>")); } if let Some(timezone) = &self.timezone { lines.push(format!(" <timezone>{timezone}</timezone>")); } match &self.network { Some(network) => lines.push(format!(" {}", network.render())), None => {} } if let Some(filesystem) = &self.filesystem { lines.push(format!(" {}", filesystem.render())); } if let Some(subagents) = &self.subagents { lines.push(" <subagents>".to_string()); lines.extend(subagents.lines().map(|line| format!(" {line}"))); lines.push(" </subagents>".to_string()); } format!("\n{}\n", lines.join("\n")) }렌더 결과(개념):
<environment_context> <cwd>/home/user/repo</cwd> <shell>bash</shell> <current_date>2026-06-15</current_date> <timezone>Asia/Seoul</timezone> <network enabled="true"><allowed>example.com</allowed></network> <filesystem><workspace_roots><root>/repo</root></workspace_roots> <permission_profile type="managed"><file_system type="restricted" /></permission_profile></filesystem> </environment_context>
조립 순서 — 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());
}
[!warning] 추가할 때 체크리스트
role을developer(정책/능력) 또는user(AGENTS.md·환경 같은 사실)로 맞게 골랐는가type_markers()가 다른 조각과 충돌하지 않는 고유 태그인가body()가 마커와 붙어도 안 깨지게 자기 줄바꿈을 직접 넣었는가 (render는 구분자 안 넣음)- base 변경은
config.base_instructions오버라이드나models.json모델 항목 수정(둘 다instructions필드로 감)- AGENTS.md를 키우면
project_doc_max_bytes한도 확인(초과 시 truncate)
요약 & 셀프체크
3줄 요약:
- 한 번의 모델 요청 = 고정 base + 상황 조각(fragment)을 정해진 순서로 쌓은 것.
- 각 조각은 고유 마커와 역할(developer/user)을 갖고, 같은 역할끼리 한 메시지로 묶여
Prompt.input에, base는base_instructions칸에 따로 들어간다. - 마커가 붙는 이유는 대화가 길어졌을 때 옛 조각을 찾아 교체·압축(compact)하기 위해서다.
스스로 답해보기:
- AGENTS.md 조각과 환경 정보 조각은 왜 둘 다
user이고, 권한·스킬 조각은 왜developer인가? - 같은 모델에서 base 프롬프트를 바꾸려면 어디를 건드려야 하나? (우선순위 3단계 중 어느 것?)
- 조각마다 마커가 없다면 대화가 길어졌을 때 무엇을 못 하게 되는가?
연결
[!tip]- 펼쳐보기: 분석 근거 (소스 교차확인) 본 노트의 사실은 아래 소스를 직접 확인해 작성했다. 특히 “모델별 base는
models.json인라인 텍스트이며prompt.md는 사람용 사본일 뿐, 어떤 코드도include_str!로 직접 가져가지 않는다”는 점은 grep으로 교차검증했다.
…/core/src/client_common.rs—Prompt구조체…/protocol/src/models.rs—BaseInstructions,BASE_INSTRUCTIONS_DEFAULT…/context-fragments/src/fragment.rs—ContextualUserFragment트레잇,render()/matches_text()…/core/src/context/mod.rs— fragment 모듈 등록부…/core/src/context/user_instructions.rs— AGENTS.md 마커…/core/src/context/environment_context.rs— cwd/shell/date/network/filesystem/subagents 렌더…/core/src/context/available_skills_instructions.rs— skills 조각(role=developer)…/core/src/context/permissions_instructions.rs+…/prompts/src/permissions_instructions.rs— permissions 조각…/core/src/agents_md.rs— AGENTS.md 발견·연결·예산…/core/src/session/mod.rs:2871—build_initial_context…/core/src/context_manager/updates.rs:183—build_developer_update_item/build_contextual_user_message…/core/src/context_manager/mod.rs— history/normalize 윈도우 관리…/core/src/compact.rs— 한계 초과 요약 압축(SUMMARIZATION_PROMPT)…/protocol/src/protocol.rs:94~105— 마커 상수(ENVIRONMENT_CONTEXT_OPEN_TAG등)…/models-manager/src/model_info.rs+models.json+prompt.md— 모델별 base 출처…/core/gpt-5.2-codex_prompt.md,gpt_5_codex_prompt.md— base 사람용 소스 사본