지식위키

진입점·에이전트 루프 — 횡단분석

진입점·에이전트 루프 — 횡단분석

한 줄 요약

에이전트 루프란 “사용자 한 마디 → 모델이 도구를 부르고, 그 결과를 다시 모델에 먹이고, 또 부르고… → 더 안 부르면 종료”의 반복이며, 7개 하네스가 이 한 골격을 누가 소유하고 언제 멈추는지로 갈린다. 왜 배우나 — 모든 하네스 동작의 심장이라, 이 루프 한 장을 이해하면 나머지 횡단 주제(컨텍스트·훅·검증)가 전부 이 루프의 “어디에 끼어드는가”로 설명되기 때문이다.

그림

10_agent-loop-diagram.svg

flowchart TD
    U(["사용자 한 마디 / 스크립트 -p"]) --> M[모델 추론]
    M -->|도구 호출 있음| T[도구 실행]
    T --> R[결과를 히스토리에 재주입]
    R --> M
    M -->|도구 호출 없음| G{"Stop 게이트<br/>끝내도 되나?"}
    G -->|"block: 한 바퀴 더"| M
    G -->|통과| E([턴 종료])
    G -.무한루프 가드.-> E
flowchart LR
    subgraph 소유형["루프 소유형 — while을 직접 돌림"]
      CC[Claude Code]
      CX[Codex]
      GJ[gajae-code]
      OB["ouroboros · AC그래프 병렬"]
    end
    subgraph 기생형["기생형 — CC 루프에 훅으로 얹힘"]
      OMC[OMC]
      FB[fable-ish]
      MINE["내 패턴 · 플러그인 로딩"]
    end
    CC -- "lifecycle 이벤트" --> OMC
    CC -- "lifecycle 이벤트" --> FB
    CC -- "부팅 시 능력 등록" --> MINE

쉽게 풀기

에이전트 루프를 “요리사와 주방 보조” 로 비유해 보자.

  1. 주문이 들어온다 — 사용자가 “이거 고쳐줘”라고 말한다. 대화창에서 직접(claude) 말할 수도 있고, 스크립트가 자동으로 주문서를 넣을 수도 있다(claude -p 헤드리스).
  2. 요리사(모델)가 생각한다 — 모델은 혼자서는 글만 쓸 수 있다. 실제로 칼질(파일 수정)이나 불 켜기(명령 실행)를 하려면 “보조야, 이 도구 좀 써줘”라고 도구 호출(tool_use) 을 내야 한다.
  3. 보조(하네스)가 도구를 실제로 돌린다 — 그리고 결과(잘 됐다/에러 났다)를 요리사 귀에 다시 속삭여 준다. 이 “결과 재주입”이 루프의 핵심이다. 결과가 다음 생각의 재료가 되어야 “관찰 → 행동”이 생긴다.
  4. 요리사가 더 시킬 게 없으면 “끝”이라고 말한다 — 도구를 더 안 부르면 한 턴이 끝난다.
  5. 그런데 문 앞에 감독관(Stop 게이트)이 있다 — “테스트도 안 돌리고 끝내려고? 한 번 더 해.”라고 되돌릴 수 있다. 단, 무한히 못 막게 가드(몇 번까지만 막기)가 항상 같이 붙는다.

여기서 가장 큰 갈림길은 “누가 주방을 소유하느냐” 다.

  • 루프 소유형(CC·Codex·gajae·ouroboros): 자기 주방을 직접 운영한다. 멈춤·압축·병렬·취소를 다 제어하지만 만들 게 많다.
  • 기생형(OMC·fable-ish·내 패턴): 남의 주방(CC)에 직원으로 들어가 “주문 들어올 때/나갈 때만 한마디 거든다.” 가볍지만, 요리사를 다시 부르는 실권은 주방 주인(CC)에게 있다. fable-ish의 표현이 정확하다 — “감독관이지 운전자가 아니다.”

핵심 정리

세 가지 질문으로 7개 하네스를 본다: (1) 루프를 누가 소유하는가, (2) 무엇이 종료/연장을 결정하는가, (3) AI에 무엇을 언제 주입하는가.

프레임워크루프 소유한 줄 특이점
Claude Code소유(본체)3-cadence(세션/턴/도구), Stop 훅 decision:block으로 종료 거부·연장(8연속 강제종료 가드)
Codex소유(Rust)루프 한가운데 auto-compact, end_turn==None fallback, sampling은 순차+tool은 동시
OMC기생자체 루프 없음, stdin/stdout JSON 계약으로 컨텍스트 주입·도구 차단
gajae-code소유(TS)단일 번역경계 convertToLlm, 두 겹 루프, 영수증(coverage)
ouroboros소유(Python)단일 턴이 아닌 AC 의존성 그래프 레벨 병렬 루프
fable-ish기생훅 3종 + ledger, 변경됐는데 검증 없으면 Stop 차단(최대 2회)
내 패턴기생(부팅)루프 아님 — settings→plugin.json 부팅으로 능력 일괄 등록

[!note] 세부 스키마·트리거·주입방식 (펼쳐 보기) Claude Code — 진입점 2종(claude 대화형 / claude -p 헤드리스), 루프 3-cadence(세션1회 / 턴1회 UserPromptSubmitStop / 도구마다 PreToolUsePostToolUse), --max-turns·--max-budget-usd. 한 턴 = 모델 추론 → tool_use면 실행 후 결과 재주입 → 안 부르면 Stop. 시스템프롬프트+CLAUDE.md+메모리+도구정의를 하네스가 조립해 주입. CodexSessionTask 트레잇 + 구현체(RegularTask/ReviewTask/CompactTask/UserShellCommandTask), ResponseEvent enum 스트림(OutputItemDone/Completed{end_turn}). codex execUserTurnspawn_tasktokio::spawn. build_prompt가 입력+model_visible_specs()+base_instructions 조립, FunctionCallOutput을 다음 히스토리에 포함. OMChooks/hooks.json 이벤트→.mjs 매핑표 + run.cjs 공통런처. CC lifecycle 이벤트 시 스크립트 spawn. hookSpecificOutput.additionalContext로 주입([MAGIC KEYWORD: RALPH] 등), permissionDecision:'deny'로 차단. gajae-codeagentLoop/agentLoopContinue(+ detailed wrapper 2종), AgentLoopConfig. 바깥(followUp 재진입)/안쪽(steering→stream→executeToolCalls→pause) 두 겹. 끝까지 AgentMessage로 들고 LLM 직전에만 Message[]로 변환, _i(intent) 필드 주입→실행 전 제거. ouroboros — 진입점 3통로(SKILL.md 매핑 / parse_ooo_command→registry / typer CLI). execute_seed→Seed→AC 의존성 LLM질의→StagedExecutionPlan 레벨그래프. 스테이지 직렬·스테이지내 AC 병렬. 각 AC는 자기+형제경계+직전레벨 컨텍스트만. fable-ishhooks.json 3종(UserPromptSubmit=분류 / PostToolUse=증거기록 / Stop=검증게이트), ledger 스키마, should_block_stop. additionalContext로 연성신호 주입. 내 패턴settings.jsoninstalled_plugins.jsonplugin.json 부팅. 켜진 플러그인의 skills/commands/agents/hooks/MCP를 도구카탈로그·시스템리마인더로 컨텍스트 맨앞 주입. CLAUDE.md=시스템프롬프트 흡수.

모두가 수렴하는 공통 패턴 4가지:

  • “추론→실행→결과 재주입→재추론” 반복이 루프의 본질 — 모델 단독은 텍스트만 생성(CC tools.md)하므로 결과 피드백이 다음 입력이 돼야 “관찰→행동”이 생긴다.
  • 종료 직전 “Stop 게이트” + 무한루프 가드는 항상 짝 — CC 8연속 block, fable-ish MAX_STOP_BLOCKS=2, Codex stop_hook_active, ouroboros watchdog.
  • stdin JSON → stdout JSON이 호스트↔확장 표준 계약additionalContext(주입)나 permissionDecision/decision(통제)을 stdout으로. 다수 훅이 개별적으로 fail-open.
  • 컨텍스트는 “필요한 것만, 늦게” — gajae 단일 번역경계, ouroboros on-demand SKILL.md, Codex auto-compact, 내 패턴 deferred tool+ToolSearch. (단 “내 패턴”은 startup registration 성격이라 late injection과 결이 다름 — 아래 교차검증 참고.)

[!note] 분기점 — 누가 왜 다르게 했나

  • 루프 소유 vs 기생(가장 큰 분기): 소유형은 종료·압축·병렬·취소를 완전 제어하나 구현 부담 큼. 기생형은 가볍지만 실권은 CC에 있고 확장은 주입/차단의 연성신호에 그침.
  • 단일 턴 vs 그래프 병렬(ouroboros의 급진성): ouroboros만 루프 자체를 AC 의존성 그래프로 재정의(의존성 질의→위상정렬→레벨 병렬→코디네이터 충돌중재). 충돌·오염을 구조적으로 줄이는 대신 막대한 복잡도. 단 Codex도 한 sampling turn 안에서 tool future를 동시 관리하므로, 비교축은 “작업 단위 병렬화가 loop의 1급 구조인가”가 정확.
  • 종료 신호 신뢰도: CC는 stop_reason 신뢰. Codex는 end_turn==None fallback까지(멀티프로바이더 중립성의 대가). gajae는 Harmony 누출(GPT-5 찌꺼기)도 종료/재시도 경로에 흡수.
  • 상태 공유 매개체: fable-ish ledger(<sha256>.json), ouroboros EventStore, gajae in-memory 영수증, Codex 히스토리 자체. 영속성 요구가 매개체를 가름.
  • 진입점 다중성: ouroboros만 3통로(백엔드 중립 야심), 나머지는 1~2통로.

실제 예시

1) Stop 게이트로 “끝내려는 순간”을 가로채 한 바퀴 더 돌리는 패턴 (기생형, OMC/fable-ish 계약)

// CC/OMC Stop 훅 출력 — stdout 한 줄. 'message' 필드는 무효(코드 주석에 명시)
{
  "decision": "block",                 // 종료 거부 → 루프 한 바퀴 더
  "reason": "변경 파일이 있는데 성공 검증이 없습니다. 테스트를 먼저 돌리세요."
}
// 무한루프 가드: ledger stop_blocks >= 2 이거나 CC 8연속 block이면 통과시켜야 함
# /home/seunghyeong/harness-work/fable-ish/hooks/stop_gate.py
# 핵심 규칙: 변경됐는데 성공검증 없음 → block (단 MAX_STOP_BLOCKS=2)
# "이제 하겠다"는 의지만 말하고 실제 작업 안 한 경우도 차단
# 모든 훅 fail-open: 예외 시 SystemExit(0) 으로 작업을 막지 않음

2) 루프를 자기 소유하는 형(Codex) — 한가운데서 압축 후 continue

// /home/seunghyeong/harness-work/codex/codex-rs/core/src/session/turn.rs
// run_turn: stream() 소비 → function_call이면 future를 FuturesOrdered로 큐잉
//           → 끝에서 drain_in_flight (sampling 순차 + tool 동시)
// Pre/Mid-turn 토큰한계 시 auto-compact 후 같은 루프를 continue
// end_turn == None 이면 fallback 처리 (프로바이더가 안 채우는 경우)

3) 내 스택(Claude+Codex+OMC)에 차용할 구체안 — 루프는 안 짜고 확장점만 무장

1. Stop 게이트 + 무한루프 가드를 OMC 훅으로 내장
   - 기존 context-guard-stop.mjs / persistent-mode.mjs 와 순서·우선순위 정의
   - 예외(context-limit / user abort / auth / scheduled wakeup)는 반드시 통과
   - fable-ish식 검증게이트(변경됨 && 검증없음 → 1회 block, ledger>=2면 통과) 이식
2. fable-ish ledger 패턴을 OMC 공유상태로
   - session_id+cwd sha256 키, 원자적 replace, redact, "이번 턴 증거"만 담는 경량 장부
   - Node 환경이므로 SystemExit(0) 대신 try/catch 후 {continue:true, suppressOutput:true} 또는 {}
3. Codex auto-compact 사고를 CC PreCompact 훅에 반영
   - "압축 직전 핵심상태를 wiki/notepad에 flush" 까지만 가능 (mid-turn continue 제어권은 plugin에 없음)
4. gajae coverage(toolsAvailable/Invoked/Unused) 집계
   - SubagentStop verify-deliverables.mjs는 의도적으로 additionalContext를 안 냄(항상 suppress)
   - 따라서 parent-visible ledger / SessionEnd report / 별도 state artifact로 모을 것
5. gajae 단일 번역경계 원칙을 Codex 워커 호출에 적용 (프롬프트 조립 한 곳에 모아 prefix 캐시 적중)
6. 차용 안 함: ouroboros AC-그래프 병렬 루프는 일상엔 과함
   - 대형 멀티파일 리팩터링에서만 "AC 분할→레벨 병렬→충돌중재"를 ultrawork/team 위에 선택적으로

요약 & 셀프체크

3줄 요약:

  1. 에이전트 루프의 본질은 “추론 → 도구 실행 → 결과 재주입 → 재추론”의 반복이고, 도구를 더 안 부르면 종료다.
  2. 7개 하네스는 루프를 직접 소유하는 4개(CC·Codex·gajae·ouroboros)CC 루프에 기생하는 3개(OMC·fable-ish·내 패턴) 로 갈리며, 후자는 “감독관이지 운전자가 아니다.”
  3. 종료 직전 Stop 게이트와 무한루프 가드는 늘 짝으로 오고, 내 스택에선 루프를 새로 짜는 대신 이 확장점을 OMC 훅으로 무장하는 게 관건이다.

셀프체크:

  • 모델이 도구를 부른 뒤 “결과 재주입”을 생략하면 루프가 왜 망가지는가?
  • 기생형(OMC)이 Stop에서 decision:block을 내도 “무한루프 가드”가 없으면 무슨 일이 벌어지나?
  • ouroboros의 그래프 병렬 루프를 내 일상 작업에 통째로 들이면 안 되는 이유와, 그럼에도 차용할 한 가지는?

연결

_분석축_루브릭 · HOME

근거 — 참조한 기능노트:

근거 — 소스/문서 경로:

  • /mnt/d/6study/10_프레임워크분석/_원문아카이브/claude-code/how-claude-code-works.md, headless.md, cli-reference.md, hooks.md(L22 cadence, L2293 Stop 제어)
  • /home/seunghyeong/harness-work/codex/codex-rs/core/src/session/turn.rs(run_turn, auto-compact), tasks/mod.rs(SessionTask), codex-api/src/common.rs(ResponseEvent)
  • /home/seunghyeong/harness-work/oh-my-claudecode/hooks/hooks.json, scripts/run.cjs, scripts/keyword-detector.mjs
  • /home/seunghyeong/harness-work/gajae-code/packages/agent/src/agent-loop.ts, types.ts, run-collector.ts
  • /home/seunghyeong/harness-work/ouroboros/src/ouroboros/orchestrator/{runner.py,parallel_executor.py,coordinator.py,dependency_analyzer.py}, router/command_parser.py
  • /home/seunghyeong/harness-work/fable-ish/hooks/{user_prompt_submit.py,post_tool_use.py,stop_gate.py}, scripts/{ledger.py,verify_state.py}
  • /home/seunghyeong/.claude/settings.json, plugins/installed_plugins.json, plugins/marketplaces/omc/.claude-plugin/plugin.json

[!tip] Codex 교차검증 (gpt-5.5가 harness-work 소스와 직접 대조) 총평: 큰 축(“루프 소유형 vs CC 기생형”, “종료 직전 게이트”, “컨텍스트 주입 시점”)은 맞다. 다만 아래 사실 보정이 필요하다. 사실/형식 오류 — ① OMC는 “13개”가 아니라 11개 이벤트(UserPromptSubmit/SessionStart/PreToolUse/PermissionRequest/PostToolUse/PostToolUseFailure/SubagentStart/SubagentStop/PreCompact/Stop/SessionEnd): hooks.json. 최신 Claude HookEvent는 19종이라 “13”은 어느 기준으로도 애매. ② OMC “fail-open 철칙”은 과장 — run.cjs는 스크립트 누락·timeout만 fail-open하고 정상 child의 exit code는 전파: run.cjs. “각 훅이 개별적으로 fail-open”이 정확. ③ Codex UserShellCommandTask.kind()UserShell이 아니라 TaskKind::Regular 반환: tasks/mod.rs. ④ Codex “spawn depth 제한”은 V1 한정 — V2는 MultiAgentVersion::V2 => true로 열림: spec_plan.rs. ⑤ gajae 진입점은 “3개”가 아니라 “2 핵심 + 2 detailed wrapper”. ⑥ “내 패턴”은 같은 층위의 agent loop가 아니라 plugin loading/능력 등록 패턴. 빠진 차이점 — CC row에 PostToolBatch/Setup/ConfigChange/WorktreeCreate·Remove/MessageDisplay 등 최신 HookEvent가 빠짐(“Claude hook surface”와 “OMC가 실제 매핑한 surface”는 분리해야 함). Codex는 “직렬 모델↔도구”가 아니라 “단일 turn loop + 내부 tool concurrency”(FuturesOrdered). ouroboros watchdog은 두 종류 혼재 — AC executor STALL_TIMEOUT=900s/heartbeat 30s vs ooo auto용 runtime watchdog(기본 4시간). 분기점 해석 수정 — “Stop 게이트 보편”은 과함: gajae getFollowUpMessages는 Stop hook이 아니라 yield 직전 follow-up queue, ouroboros stall/watchdog도 orchestration safety. “late injection” 묶음에 “내 패턴”을 넣으면 안 됨(startup registration 성격). 차용안 현실성 — Stop gate+ledger 이식은 현실적이나 기존 OMC 훅과 순서 정의·예외 통과 필수. ledger fail-open은 Node식({continue:true,suppressOutput:true})으로 변환. auto-compact는 “flush”까지만 가능. gajae coverage를 SubagentStop verify-deliverables.mjs에 붙이는 안은 비현실적 — 이 훅은 의도적으로 항상 suppress: verify-deliverables.mjs. coverage는 parent-visible ledger/SessionEnd report/별도 state artifact로 모을 것.