0-turn 구현기: DriftGuardBuffer 만들고 난 뒤 '이미 있었어'라는 걸 발견했어
TL;DR
- 여러 에이전트가 동시에 돌 때, 한쪽의 메시지가 다른 쪽 추론 도중에 난입하면 작은 모델은 Reasoning Drift에 걸린다.
- 0-turn 패턴은 메시지를 런치지 않은 채 버퍼에 넣고, 툴 호출이 끝나는 경계에서만 context에 덧붙이는 것.
- 이걸 직접
agent-drift-guard패키지로 구현하고 검증(37개 테스트 통과)했는데 — Hermes 코어에선AIAgent.steer()+busy_input_mode: "steer"로 이미 1:1 동일 메커니즘이 존재하고 있었다. - 결론: Hermes 타깃에선 더 이상 짓는 게 없음. 라이브러리는 SHELVED.
들어가며
0-turn 패턴 글 마지막에 “스텝 경계 버퍼는 설계를 다듬는 중”이라고 했었는데, 그 설계를 실제로 구현해봤다. 이름은 DriftGuardBuffer.
근데 구현하고 난 뒤 찾아낸 게 — 나보다 먼저, 그리고 더 잘, 만들어 놓은 게 이미 팀 프로젝트 안에 있었다는 거다.
구현한 것
framework-neutral core, 40줄:
class DriftGuardBuffer:
def __init__(self):
self._pending = []
self._lock = threading.Lock()
def on_message(self, msg):
"""Record incoming. Does NOT invoke the model (0-turn)."""
with self._lock:
self._pending.append(msg)
def on_step_end(self):
"""Called at a safe boundary. Returns + clears pending."""
with self._lock:
flushed = self._pending
self._pending = []
return flushed
핵심은 두 마디: buffer never calls the model, drain at step boundary.
Hermes 어댉터 쪽은 on_radio_message() → buffer, on_tool_call_complete() → drain + format_injection() → tool result appendix. 검증 스크립트(verify_drift_guard_0turn.py)는 가짜 모델로 0-turn 계약(모델 호출 0회, appendix 정확히 붙음, buffer 비워짐)을 assertion으로 잡았다.
[verify] 0-turn scenario PASS
- model extra turns invoked : 0 (expect 0)
- tool result appendix : ['[drift-guard] - from agent-2: pm: status check - are you done?']
- buffer after drain : 0 pending (expect 0)
“이미 있었어”
패키지를 SHELVED로 표기하던 중 docs/findings-superseded.md를 쓰다 보니, Hermes 코어를 뒤져보게 됐다. 그런데… 완전 똑같았음.
| agent-drift-guard | Hermes 코어 |
|---|---|
DriftGuardBuffer._pending |
AIAgent._pending_steer (agent_init.py:576) |
on_message() (락 + 드레인) |
AIAgent.steer() |
on_step_end() (드레인 후 클리어) |
_drain_pending_steer() (agent_runtime_helpers.py:3163) |
format_injection → tool result appendix |
“appends to the LAST tool result’s content” |
| 0-turn (extra turn 0회) | “does NOT stop the current tool call” |
심지어 게이트웨이 busy 경로도 실제로 배선되어 있음:
gateway/run.py:2786 _busy_input_mode: str = "interrupt"
gateway/run.py:4360 if _busy_input_mode in ("steer", "queue"): ...
gateway/run.py:4898 def _load_busy_input_mode() -> str → "steer" 인식
busy_input_mode = "steer"로 하면, �세지가 올 때마다 interrupt 없이 투 올리고, 툴 결과 경계에서만 model이 보게 된다. 즉 0-turn 맞통이다.
왜 직접 만들었는지, 왜 깨달았는지
당시 나는 “Hermes agent의 background 메시지 처리 메커니즘”을 정확히 파악하지 못했고, findings-superseded.md에 인용된 Discord 로그(“steer = 이미 완성된 0-turn 메커니즘”)조차 머릿속에 떠오르지 않았다.
패키지를 만들고 실제 검증을 돌린 게 중요한 포인트: verify_drift_guard_0turn.py를 실행했을 때 assertion이 깨졌음. (결론은 스크립트 기대값이 on_tool_call_complete()의 시그니처 변화에 뒤쳐진 건데, 실제 동작은 str을 반환한다는 걸 확인함.) 검증 자체가 내 설계가 실제로 뒷받침이 되는지 가늠하는 데에 큰 도움이 됐고, 결국 코어에서 동일한 게 이미 돌고 있음을 확정지을 수 있었다.
교훈
- 스펙 위에 짓는 게 아니라, 코드부터 먼저 뒤져라. 글 두 줄이면 되는데, 직접 구현하고 검증하고 싶은 마음에 37개 테스트를 짜갖고…
- SHELVED != 쓸모없음. 코드는 참고용 보존. 다른 런타임(자작 루프 / LangGraph / CrewAI / AutoGen)이 구체적 타깃으로 올라오면
src/drift_guard/의 framework-neutral core가 출발점이 된다. - 0-turn은 “알림 끄기”가 아니라 “소통 시점 맞추기”다. 모델이 투를 때까지 기다렸다가, 투 결과 경계에서 메시지를 밀어 넣는 게 핵심.
한 줄
DriftGuardBuffer는 0-turn 패턴의 구현 증명이었고, 동시에 “Hermes 코어에 똑같은 게 이미 있었다”는 걸 확인하게 해준 실험이 됐다. 더 짓는 게 아니라, busy_input_mode: steer 켜는 법을 알려줄 걸 그때.
따라하기 (다른 런타임에 끼우고 싶다면)
cd /d/develop/project/agent-drift-guard python3 examples/verify_drift_guard_0turn.py # 0-turn 계약 검증wiring:
on_radio_message→ background watcher,on_tool_call_complete→ tool executor step boundary.
Comments