From d3bc378c21065e6bc03e6da1d5bbd85e55ed23e4 Mon Sep 17 00:00:00 2001 From: Hyungi Ahn Date: Sat, 16 May 2026 20:42:34 +0900 Subject: [PATCH] =?UTF-8?q?docs(hermes):=20PR-Hermes-ToolCall-Adapter-1=20?= =?UTF-8?q?closure=20=EB=B3=B4=EA=B3=A0=EC=84=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mlx-proxy _stream_mlx 에 SSE filter 추가 — Gemma 4 raw <|tool_call> 토큰 leak suppression + 구조화 tool_calls 시 finish_reason 'stop'→'tool_calls' override. Layer 1 fixture (5 case): 5/5 raw_leak suppressed + 3/3 finish_reason override. Hermes chat multi-turn agent loop unlocked (이전 hallucinated 종결 → tool 실행). 후속 = PR-Hermes-Sandbox-Env-Propagation-1 (execute_code 가 HERMES_DOCSRV_TOKEN inherit 못 함 — PR-1/2 user-facing E2E 마지막 조각). --- .../pr_hermes_toolcall_adapter_1_closure.md | 141 ++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 reports/pr_hermes_toolcall_adapter_1_closure.md diff --git a/reports/pr_hermes_toolcall_adapter_1_closure.md b/reports/pr_hermes_toolcall_adapter_1_closure.md new file mode 100644 index 0000000..0f41786 --- /dev/null +++ b/reports/pr_hermes_toolcall_adapter_1_closure.md @@ -0,0 +1,141 @@ +# PR-Hermes-ToolCall-Adapter-1 Closure Report + +**Date**: 2026-05-17 +**Plan**: `~/.claude/plans/hermes-polymorphic-rossum.md` (Phase 1.5 hand-off, executed) +**관련 PR**: PR-Hermes-Docsrv-Search-1 / PR-Hermes-WebSearch-1 (Layer 2/3 unlock 의존) +**파일**: `~/scripts/mlx-proxy.py` (Mac mini) + +## Summary + +Gemma 4 (gemma-4-26b-a4b-it-8bit) MLX backend 가 **`<|tool_call>...` 특수 토큰을 SSE delta.content 로 leak** 하는 패턴 확정. Hermes parser 가 content 를 final answer 로 오인 → tool 실행 skip → hallucinated 응답. mlx-proxy 의 `_stream_mlx` 에 SSE filter 추가 (4-fix chain 한 commit): + +1. delta.content 의 raw `<|tool_call>` 패턴 검출 → 해당 chunk content 비움 +2. 멀티 chunk span 처리 (token-by-token 스트림 → state-machine buffer) +3. 구조화된 `delta.tool_calls` 가 chunk 에 등장 시 누적 추적 +4. SSE [DONE] 직전 finish_reason 'stop' → 'tool_calls' override chunk inject + +**Layer 1 fixture 5/5 raw_leak suppressed, 3/3 tool_calls + finish_reason override PASS.** + +## Root cause (4-iteration discovery chain) + +| Iteration | 발견 | 검증 | +|---|---|---| +| 1 | `hermes chat` 응답이 hallucinated (실제 corpus 없는 출처) — DS API 호출 0건 | curl direct 로 MLX 호출 시 tool_calls 정상 → 문제는 Hermes-MLX 경로 | +| 2 | Hermes 가 `stream=True` + `tools=[31]` 전달 — 기존 proxy 는 raw byte passthrough | proxy 에 SSE line capture 추가, Hermes 재호출 | +| 3 | SSE 분석 결과: content 32 chunks 에 `<\|tool_call>call:execute_code{code:<\|"\|>...<\|"\|>}` 누적, tool_calls 1 chunk (끝), finish_reason='stop' | Hermes 는 content 의 stop 신호 보고 종결 | +| 4 | `delta.content` 의 raw 패턴 strip + 구조화 tool_calls 발견 시 finish_reason override | 5/5 raw_leak 0, 3/3 정상 routing | + +## Code change + +**파일**: `~/scripts/mlx-proxy.py` + +**Import 추가**: +```python +import re +TOOL_CALL_OPEN_TOKEN = "<|tool_call" +TOOL_CALL_CLOSE_TOKEN = "" +TOOL_CALL_BLOCK_RE = re.compile(r"<\|tool_call[\s\S]*?") +``` + +**`_stream_mlx` 변경** — 이전 raw byte passthrough → SSE line parser: +- `raw_content_buffer` (누적 raw content for leak detection) +- `in_tool_call_block` bool (`<|tool_call>` 진입 ~ `` 종료) +- `seen_structured_tcalls` bool (MLX 구조화 tool_calls 등장 추적) +- `last_chunk_meta` (id/object/created/model — DONE 직전 finish_reason override chunk 의 메타 재사용) +- 각 `data: {...}` line 파싱 + - 진입: content 에 OPEN_TOKEN 발견 시 in_block=True + - 진행: in_block 동안 delta.content = "" 으로 비우고 forward + - 종료: CLOSE_TOKEN 도착 시 raw_content_buffer 에서 TOOL_CALL_BLOCK_RE.sub("") 으로 정리, in_block=False +- `[DONE]` 도착 시: + - seen_structured_tcalls && last_chunk_meta 있으면 → finish_reason='tool_calls' chunk inject + - 그 다음 [DONE] forward + +## Layer 1 fixture 결과 (5건, proxy 직접 호출) + +``` +memo-search | 1.09s | 0 leak | tool_calls=1(docsrv_ask) | finish=[stop,tool_calls] ✅ +asme-search | 11.34s | 0 leak | tool_calls=0 | finish=[stop] (Gemma 직접 답변 선택) +today-tasks | 5.35s | 0 leak | tool_calls=0 | finish=[stop] (Gemma 직접 답변 선택) +multi-tool | 0.91s | 0 leak | tool_calls=1(docsrv_ask) | finish=[stop,tool_calls] ✅ +explicit-call | 0.74s | 0 leak | tool_calls=1(docsrv_ask) | finish=[stop,tool_calls] ✅ +``` + +### Hard metrics + +| Gate | 측정 | 판정 | +|---|---|---| +| raw_token leak suppressed in content | 5/5 | ✅ **PASS** | +| 구조화 tool_calls 받으면 finish_reason override | 3/3 (해당 case) | ✅ **PASS** | +| LLM-decision: tool 사용 여부 | 3/5 사용, 2/5 직접 답변 | (Adapter A 범위 외) | + +**2/5 tool_calls 미사용은 Adapter A 의 문제가 아님** — Gemma 가 ASME 일반 지식 / "오늘 한 일" 도메인은 tool 없이 답변 선택. fixture prompt 강도 결정. Adapter A 의 mandate (raw leak suppress + finish_reason override) 는 100% 달성. + +## Layer 2 검증 (Hermes chat end-to-end) + +`hermes chat -s docsrv_ask -q '내 자료에서 ASME 압력용기 찾아줘'` 실행 → **multi-turn agent loop 4 turns active**: + +| Turn | messages_n | tool 실행 결과 | +|---|---|---| +| 1 | 2 | tool_call: execute_code(Python wrapping curl) | +| 2 | 4 | Python SyntaxError (f-string quote 충돌) — Gemma 코드 생성 quality 이슈 | +| 3 | 6 | curl 직접 실행 → DS API hit → **HTTP 401 "유효하지 않은 토큰"** | +| 4 | 8 | (반복 시도) | + +**핵심 검증** — Adapter A unlock 효과: +- ✅ Multi-turn agent loop 활성 (이전: 1 turn 후 hallucinated 종결) +- ✅ tool 실제 실행 (sandbox/terminal 호출) +- ✅ DS API 실제 호출 (이전: 호출 0건) +- ⚠️ 401 = HERMES_DOCSRV_TOKEN env 가 `execute_code` 샌드박스에 propagate 되지 않음 — **별 트랙** + +## 발견된 후속 이슈 (Adapter A 와 분리) + +| 트랙 | 범위 | 우선순위 | +|---|---|---| +| **PR-Hermes-Sandbox-Env-Propagation-1** | `execute_code` / `terminal` tool 의 환경변수 inherit 정책. HERMES_DOCSRV_TOKEN 이 child process 에 전달되도록 (Hermes config or skill design). PR-1 Layer 2 진정한 unblock | **다음** (PR-1 user-facing 답변 produce 의 마지막 조각) | +| **PR-Hermes-Skill-Curl-Refine-1** | docsrv_* skill 본문이 Python `execute_code` 우회를 유도 — `terminal` tool 직접 사용 명시 (env propagation 다름) | env 트랙 검토 후 결정 | +| **PR-Hermes-MaxTokens-Followup** | Mac mini 26B 가 23320 input tokens (31 tools + 90 skills + persona) 처리에 30s+ 소요. tools/skills 선택적 로딩 또는 prompt compression | P3 | + +## File changes + +### Mac mini +- `~/scripts/mlx-proxy.py` — Adapter A 구현 (+75 줄), TEMP DEBUG capture 코드 모두 제거 후 cleanup +- `~/scripts/mlx-proxy.py.pre-adapter-a.20260517` — 7일 안전망 백업 +- `~/.hermes/fixtures/pr_adapter_a_fixture.py` 신규 (proxy direct fixture, 5 case) + +### 변경 없음 +- `/opt/mlx-proxy.py` (구버전 root-owned 잔재, 별 chore 로 정리 예정) +- `~/Library/LaunchAgents/com.user.mlx-proxy.plist` (proxy 가리키는 경로 변경 없음) +- Hermes config / DS code + +## 7일 안전망 (2026-05-24) + +- Mac mini `~/scripts/mlx-proxy.py.pre-adapter-a.20260517` (Adapter A 적용 전 백업, ~21KB) +- 복귀 시: `ssh macmini "cp ~/scripts/mlx-proxy.py.pre-adapter-a.20260517 ~/scripts/mlx-proxy.py && launchctl bootout/bootstrap"` + +## 검증 commands (재실행) + +```bash +# Layer 1 proxy-direct fixture +ssh macmini "python3 ~/.hermes/fixtures/pr_adapter_a_fixture.py" + +# Layer 2 Hermes chat (env propagation 트랙 진행 후 의미있는 응답 기대) +ssh macmini "HERMES_DOCSRV_TOKEN=\$(/usr/libexec/PlistBuddy ...) && ~/.hermes/hermes-agent/venv/bin/python -m hermes_cli.main chat -Q -s docsrv_ask -q '내 자료에서 X 찾아줘'" + +# Proxy 로그 +ssh macmini "tail -20 ~/Library/Logs/mlx-proxy.log" +``` + +## 결정 사항 + +1. **PR-Hermes-ToolCall-Adapter-1 = SHIPPED**: + - raw token leak suppressed 5/5 + - finish_reason override 3/3 + - Multi-turn agent loop unlocked (proven via Hermes chat) + - **본 PR 의 mandate 완전 충족** + +2. **Layer 2/3 user-facing E2E = 부분 unlock**: + - Adapter A 가 tool 실행을 unlock 함 (이전 hallucinated 종결 → 이제 multi-turn loop) + - HERMES_DOCSRV_TOKEN sandbox env propagation 이 마지막 조각 — 별 PR (Sandbox-Env-Propagation-1) + - 이게 풀리면 PR-1 / PR-2 의 진짜 user-facing E2E 완성 + +3. **D 트랙 (PR-Hermes-Discord-Prefix-Route, P2 강등)**: 본 Adapter A closure 후 자연어 호출 실패율 1주 측정 → 진입 재평가 (메모리 참조).