LLM 호출 하나에 들어가는 컨텍스트는 세 재료로 만들어집니다. 메모리 계층의 요약, 레이어로 조립된 시스템 프롬프트, 그리고 대화 히스토리입니다. 이 페이지는 세 재료가 토큰 예산 안에서 어떻게 합쳐지고, 예산을 넘으면 무엇이 양보하는지 정리합니다.
재료 1: 메모리 계층
core/memory/context.py의 ContextAssembler가 5계층 메모리를 병합하고, LLM에 넣을 _llm_summary를 만듭니다. 병합은 Identity, User Profile, Organization, Project, Session 순서로 흐르고, 더 구체적인 계층이 앞 계층을 덮습니다.
| 계층 | 요약 예산 |
|---|---|
| Identity (SOUL.md) | 10% |
| User Profile | 있으면 앞부분 예산에 짧게 포함 |
| Organization | 25% |
| Project | 25% |
| Session | 나머지. 최신 항목부터 채웁니다 |
계층 병합 뒤에는 프로젝트 타입, 최근 실행 기록, 프로젝트 저널, Vault 요약 같은 보강 블록이 붙습니다. 계층 자체의 구조와 override 규칙은 메모리 계층에서 다룹니다.
재료 2: 시스템 프롬프트 레이어
core/agent/system_prompt.py의 build_system_prompt(model)이 캐시 가능한 정적 prefix와 턴마다 바뀌는 동적 섹션(<dynamic_context>)을 경계 마커로 나눠 조립합니다. 레이어 구성과 모드는 프롬프트 조립, 캐시 동작은 프롬프트 캐싱을 참고합니다.
오버플로 처리: 누가 양보하는가
루프는 매 라운드 진입 시 core/agent/context_manager.py의 ContextWindowManager에 오버플로 점검을 위임합니다. 임계값은 고정된 80/95가 아니라 core/orchestration/context_budget.py의 resolve_context_budget_policy가 모델의 컨텍스트 윈도에 맞춰 계산합니다. 반환된 ContextBudgetPolicy가 세 티어 중 하나를 고릅니다.
| 티어 | 윈도 범위 | 경고 임계 | 임계 |
|---|---|---|---|
| small | ≤ 256K | 50% | 90% |
| standard | ≤ 512K | 70% | 90% |
| large | > 512K | 80% | 90% |
퍼센트는 raw 윈도가 아니라 유효 프롬프트 예산(effective_prompt_budget_tokens = 윈도에서 출력 예비분 약 20K를 뺀 값) 기준입니다. 실제 대응은 프로바이더에 따라 갈립니다.
- Anthropic. 경고 수준 압력은 서버 측 context management가 처리하므로 클라이언트는 개입하지 않습니다. 임계 수준에서만 클라이언트가 비상 정리(prune)를 수행합니다.
- OpenAI / GLM. 서버 측 압축이 없어 클라이언트가 3단계 압력 대응을 순차 실행합니다. (1) 값싼 도구 압축 — 오래된 관측 마스킹(
mask_stale_observations)과 큰 도구 결과 요약(summarize_tool_results, LLM 호출 없음), (2) 구조화 LLM 압축(compact_conversation), (3) 압축으로 부족하거나 실패하면 적응형 정리(adaptive_prune).
- 컨텍스트 윈도가 200K를 넘는 모델에는 별도로 절대 200K 토큰 천장(
absolute_ceiling_tokens)이 걸립니다. 퍼센트 임계와 무관하게 rate-limit 풀 분리를 피하려는 조치로, 도구 결과 요약 후 필요하면 압축을 강제합니다. - 전략 선택은
CONTEXT_OVERFLOW_ACTION훅 핸들러에 위임되고, 등록된 핸들러가 없으면 해석된 policy가 폴백입니다. 임계 상태에서는CONTEXT_CRITICAL훅이 발화합니다. - 정리 후에도 임계 상태면 루프는
context_exhausted로 종료하고, 사용자 언어에 맞춘 안내문을 생성해 돌려줍니다 (core/agent/loop/models.py). - API가 400으로 컨텍스트 오버플로를 알리면 공격적 복구를 시도한 뒤 재시도하고, 실패하면 역시
context_exhausted입니다.
압축 장비는 core/orchestration/compaction.py와 core/orchestration/context_monitor.py에 있고, 티어 경계와 임계 상수는 core/orchestration/context_budget.py가 SoT입니다. 모델별 컨텍스트 윈도 값은 core/llm/token_tracker.py의 MODEL_CONTEXT_WINDOW가 SoT입니다 (core/llm/model_pricing.toml이 뒷받침).
대형 도구 결과: 오프로드
도구 결과가 5000 토큰 임계값을 넘으면 core/orchestration/tool_offload.py의 ToolResultOffloadStore가 결과를 디스크 (.geode/tool-offload/ 아래 세션 디렉터리)로 내리고, 컨텍스트에는 요약과 ref_id만 남깁니다. 모델은 필요할 때 recall_tool_result(ref_id) 경로로 원본을 다시 가져옵니다. 오프로드 시 TOOL_RESULT_OFFLOADED 훅이 발화합니다.
장기 컨텍스트 아티팩트: dreaming
메시지 트랜스크립트와 별개로, 프로젝트별 sessions.db(SQLite)에는 context_artifacts 행이 쌓입니다. 합성된 장기 컨텍스트 기록으로, 턴 경로 밖에서 만들어집니다. core/memory/dreaming.py의 DreamingService가 TURN_COMPLETED 훅에서 백그라운드로 동작합니다(best-effort — 포그라운드 턴을 절대 막지 않습니다). 트랜스크립트를 증거로 삼아 지속 사실, 결정, 미해결 작업, 낡은 리스크, 유용한 recall 질의, 인용을 정해진 헤딩으로 요약하고, dream 종류의 아티팩트로 되씁니다. source_end_seq 기준으로 멱등이라 새 메시지가 없으면 건너뛰고, LLM을 못 쓰면 LLM 없는 로컬 요약으로 폴백합니다.
주입은 경계가 있습니다. ContextAssembler의 _inject_long_context_artifacts가 최신 compaction_summary/dream 아티팩트 최대 3개를 각 500자로 잘라 _long_context_summary로 넣습니다. session_search 도구는 include_artifacts=true(선택적 artifact_kinds 필터)로 FTS5 메시지 검색과 함께 이 합성 아티팩트도 뒤집니다.
캐시를 깨지 않는 주입
현재 날짜와 라운드 번호 같은 턴별 정보는 core/agent/system_injection.py의 append_system_reminder가 요청별 복사본의 마지막 메시지로 덧붙입니다. 공유 히스토리는 변형되지 않으므로 메시지 prefix가 라운드 간 바이트 단위로 안정적이고, Anthropic과 OpenAI의 prefix 캐싱이 적중합니다.
실패 모드
| 증상 | 원인 | 해법 |
|---|---|---|
긴 세션에서 context_exhausted 종료 | 압축 후에도 히스토리가 임계 상태 | 새 세션을 열거나 /compact로 미리 압축합니다 |
| 도구 결과가 요약으로만 보임 | 5000 토큰 임계값을 넘어 오프로드됨 | 정상 동작입니다. recall_tool_result(ref_id)로 원본을 조회합니다 |
| 캐시 적중률이 갑자기 하락 | 히스토리 앞부분을 변형하는 커스텀 주입 | 주입은 append 방식만 사용합니다 (append_system_reminder 패턴) |