HookSystem(core/hooks/system.py)은 런타임의 모든 의미 있는 경계에서 이벤트를 발화하는 단일 버스입니다. HookEvent enum은 정확히 56개의 라이프사이클 이벤트를 정의합니다. 새 동작은 루프 코드를 고치는 대신 이 이벤트들 위에 핸들러로 쌓입니다.
하나의 이벤트, 네 가지 쌓기
같은 이벤트라도 핸들러가 무엇을 하느냐에 따라 네 층위로 쌓입니다.
| 층위 | 호출 방식 | 의미 |
|---|---|---|
| Observe | trigger() | fire-and-forget 관찰. 핸들러 오류는 로깅 후 격리됩니다 |
| React | trigger() 구독 핸들러 | 이벤트를 계기로 부수 작업을 실행 (알림, 저널, 메트릭) |
| Decide | trigger_with_result() | 핸들러 반환값을 호출자가 수집해 전략을 결정 (예: CONTEXT_OVERFLOW_ACTION) |
| Act | trigger_interceptor() | 핸들러가 실행 자체를 차단하거나 수정 (인터셉터 패턴) |
비동기 변형으로 trigger_async가 있습니다.
이벤트 카테고리
56개 이벤트는 enum 안의 섹션 주석으로 분류됩니다. 대표만 추리면 이렇습니다.
| 카테고리 | 대표 이벤트 |
|---|---|
| agentic 턴 / 세션 | TURN_COMPLETED, SESSION_STARTED, SESSION_ENDED |
| LLM 호출 | LLM_CALL_STARTED/ENDED/FAILED/RETRIED, ADAPTER_DISPATCH_ATTEMPT, MODEL_SWITCHED |
| 도구 실행 / 승인 | TOOL_EXEC_STARTED/ENDED/FAILED, TOOL_RESULT_TRANSFORM, TOOL_APPROVAL_REQUESTED, APPROVAL_TRANSITION, TOOL_RECOVERY_ATTEMPTED/SUCCEEDED/FAILED |
| 컨텍스트 / 오프로드 | CONTEXT_CRITICAL, CONTEXT_OVERFLOW_ACTION, TOOL_RESULT_OFFLOADED |
| 프롬프트 / 메모리 | PROMPT_ASSEMBLED, PROGRAM_MD_UNREADABLE, MEMORY_SAVED, RULE_CHANGED |
| 비용 / 인터셉터 | USER_INPUT_RECEIVED, COST_WARNING, COST_LIMIT_EXCEEDED, EXECUTION_CANCELLED |
| 서브에이전트 / 핸드오프 | SUBAGENT_STARTED/COMPLETED/FAILED, HANDOFF_TRIGGERED/COMPLETED/FAILED |
| 자기개선 루프 | MUTATION_PROPOSED/APPLIED/REJECTED/REVERTED, BASELINE_PROMOTED, SELF_IMPROVING_AUTO_TRIGGER |
| 인프라 | TRIGGER_FIRED, CONFIG_RELOADED, SHUTDOWN_STARTED, MCP_SERVER_CONNECTED/FAILED |
등록
from core.hooks.system import HookSystem, HookEvent
hooks.register(
HookEvent.TURN_COMPLETED,
my_handler,
name="my-plugin",
priority=100, # 낮을수록 먼저 실행, 안정 정렬
)
hooks.register_prefix("*", mirror_everything) # 와일드카드 구독부트스트랩 등록이 필수
핸들러가 존재한다는 사실과 핸들러가 발화한다는 사실은 다릅니다. 프로덕션 핸들러는 전부 core/wiring/bootstrap.py에서 등록됩니다. 거기 없는 핸들러는 코드로 존재해도 영원히 호출되지 않습니다. 이 규칙은 저장소의 wiring 검증 인바리언트로 핀 고정되어 있습니다.
부트스트랩에는 priority 50의 "*" prefix 핸들러가 하나 있어, 모든 트리거를 활성 RunTranscript의 activity log 행으로 미러링합니다. 훅 버스 자체가 관측 파이프라인의 입구입니다.
실패 모드
| 증상 | 원인 | 해법 |
|---|---|---|
| 핸들러를 만들었는데 한 번도 안 불림 | 부트스트랩 미등록 | core/wiring/bootstrap.py에 등록을 추가합니다 |
| 핸들러 실행 순서가 뒤섞임 | priority 미지정 (기본 100) | 순서가 중요하면 명시적 priority를 부여합니다. 낮은 값이 먼저입니다 |
| observe 핸들러의 예외가 보이지 않음 | trigger()는 오류를 격리하고 로깅만 합니다 | 로그를 확인합니다. 흐름을 막아야 하는 로직이면 인터셉터로 옮깁니다 |
Activity row 스키마와 에러 정책
모든 trigger*() 호출은 활성 RunTranscript에 타입이 지정된 Activity row 한 줄로 미러링됩니다. 56개 이벤트 전부 구체 타입의 row를 가집니다 (19 lifecycle + 37 K-group). action필드가 discriminator이고, GenericActivityRow는 정상 경로 목적지가 아니라 fail-soft 폴백 전용입니다.
| 정책 | 내용 |
|---|---|
| 커버리지 | 56/56 타입 지정. 37 K-group은 선언적 spec 테이블 + 단일 빌더로 구성 |
| details 스키마 | 23 공유 모델, frozen + extra=forbid. 모든 row에 schema_version |
| silent fallback 금지 | 강제 generic 폴백은 _fallback_reason을 동봉해 timeline에서 구분 가능. 미러링 / dispatch / 학습 저장 실패는 이벤트당 한 번만 WARNING |
| 프라이버시 드롭 | raw user_input, cognitive-state 스냅샷, 전체 tool result는 적재 금지. input_len 같은 파생 스칼라만 |
다음
- 훅 핸들러 등록 가이드. 손으로 따라가는 절차.
- 안쪽 agentic 루프. 이벤트가 발화되는 본진.
- 하네스 라이프사이클. serve 데몬에서의 이벤트 흐름.