docs(hermes): PR-Hermes-ToolCall-Adapter-1 closure 보고서
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 마지막 조각).
This commit is contained in:
@@ -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>...<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_call\|>` 누적, 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|>"
|
||||||
|
TOOL_CALL_BLOCK_RE = re.compile(r"<\|tool_call[\s\S]*?<tool_call\|>")
|
||||||
|
```
|
||||||
|
|
||||||
|
**`_stream_mlx` 변경** — 이전 raw byte passthrough → SSE line parser:
|
||||||
|
- `raw_content_buffer` (누적 raw content for leak detection)
|
||||||
|
- `in_tool_call_block` bool (`<|tool_call>` 진입 ~ `<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주 측정 → 진입 재평가 (메모리 참조).
|
||||||
Reference in New Issue
Block a user