기본 단위
AgenticLoop는 core/agent/loop/agent_loop.py에 있습니다. 모든 작업 실행의 엔진이고, 형태는 의도적으로 단순합니다. 모델이 도구를 요청하는 동안 계속 돕니다.
while stop_reason == "tool_use":
round-entry guards # round / time / session / cost budget
context-overflow check # compact or prune if needed
response = call_llm(messages, tools)
run tool calls -> append assistant msg + tool_results루프 클래스 본체 옆에 책임별 모듈이 같은 패키지에 나뉘어 있습니다. 시스템 프롬프트와 컨텍스트 위임은 _context.py, 결과 모델과 컨텍스트 고갈 처리는 models.py, 모델 전환은 _model_switching.py, 서브에이전트 알림은 _sub_agent_announce.py입니다. 예전 단일 파일 core/agent/loop.py는 더 이상 존재하지 않습니다.
턴 사이클
매 라운드는 같은 순서를 밟습니다.
- 라운드 진입 가드. 라운드 수, 시간 예산, 세션 예산, 비용 예산을 확인합니다.
- 컨텍스트 오버플로 점검. 임계값을 넘으면 압축하거나 정리합니다. 자세한 동작은 컨텍스트 조립을 참고합니다.
- LLM 호출.
- 모델이 요청한 도구 실행.
- assistant 메시지와 tool_result를 히스토리에 붙이고 다음 라운드로 진입합니다.
라운드 진입 가드
| 가드 | 조건 | 동작 |
|---|---|---|
| 라운드 한도 | max_rounds > 0 (0은 무제한) | max_rounds로 종료 |
| 시간 예산 | time_budget_s > 0, wall-clock 기준 | time_budget_expired로 종료 |
| 세션 예산 | 기본 세션 상한 2시간 (core/agent/budget.py) | 임계 직전 HANDOFF_TRIGGERED 훅 1회, 만료 시 하드 스톱 |
| 비용 예산 | cost_budget > 0 | 80%에서 1회 경고, 도달 시 cost_budget_exceeded |
| overthinking 감지 | 도구 없이 고출력 텍스트 라운드가 연속될 때. 임계값은 컨텍스트 윈도 비례(윈도의 1%, 최소 1024 토큰) | user_clarification_needed로 멈추고 사용자에게 묻습니다 |
종료 경로
모든 실행은 AgenticResult.termination_reason 하나로 끝납니다. SoT는 core/agent/loop/models.py입니다.
| termination_reason | 의미 |
|---|---|
natural | 모델이 도구 호출 없이 답을 마침 |
forced_text | 마무리 단계에서 텍스트 응답을 강제함 (adaptive compute: max_tokens 축소, thinking off) |
max_rounds | 라운드 한도 도달 |
time_budget_expired | wall-clock 예산 소진 |
cost_budget_exceeded | 세션 비용이 예산에 도달 |
context_exhausted | 압축과 정리 후에도 컨텍스트가 임계 상태 |
llm_error | 복구 불가능한 LLM 호출 실패 |
model_action_required | 모델이 외부 조치를 요구하며 종료 신호를 보냄 |
user_clarification_needed | 모델이 확인을 요청하거나 overthinking 감지가 멈춤 |
model_refusal | 모델이 안전 거절로 응답 (아래 절) |
input_blocked | 입력이 인터셉터에서 차단됨 |
billing_error | 결제/쿼터 치명 오류 |
user_cancelled | 사용자 취소 |
convergence_detected | 진전 없는 반복 감지 |
기본값은 unknown이며, 정상 경로에서는 나타나지 않습니다.
model_refusal: 거절을 1급 종료로
Fable 5의 안전 분류기는 요청을 거절할 때 HTTP 오류가 아니라 HTTP 200에 stop_reason: "refusal"을 실어 보냅니다. 본문이 비어 있는 경우도 많습니다. 이를 일반 응답처럼 다루면 빈 답이 조용히 사용자에게 흘러갑니다.
GEODE는 두 지점에서 처리합니다. Anthropic 프로바이더의 normalize_anthropic이 응답의 stop_details를 보존하고, 루프가 이를 termination_reason="model_refusal"로 매핑하며 stop_details.category를 포함한 정직한 메시지를 만듭니다. 같은 경로가 Opus 4.7과 4.8에도 적용됩니다.
발화되는 훅
루프는 의미 있는 경계마다 라이프사이클 이벤트를 발화합니다. 라운드 종료의 TURN_COMPLETED, LLM 호출의 LLM_CALL_STARTED / LLM_CALL_ENDED / LLM_CALL_FAILED / LLM_CALL_RETRIED, 도구 실행의 TOOL_EXEC_STARTED / TOOL_EXEC_ENDED / TOOL_EXEC_FAILED, 승인 게이트의 TOOL_APPROVAL_REQUESTED / GRANTED / DENIED, 컨텍스트의 CONTEXT_CRITICAL과 CONTEXT_OVERFLOW_ACTION이 대표입니다. 전체 목록과 등록 방법은 훅과 관측성을 참고합니다.
왜 얇은 루프인가
루프는 의도적으로 얇습니다. 시스템에서 가장 많이 테스트되고 가장 적게 바뀌는 코드입니다. 새로운 동작은 루프 안이 아니라 도구, 훅, 가드로 들어갑니다. 그래야 핵심 실행 경로가 예측 가능하고 테스트 가능한 상태로 유지됩니다.
다음
- 컨텍스트 조립. 오버플로 처리와 토큰 예산의 자세한 동작.
- 서브에이전트 오케스트레이션. 루프가 작업을 위임하는 길.
- 두 개의 루프. 이 루프가 큰 그림에서 차지하는 자리.