# 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주 측정 → 진입 재평가 (메모리 참조).
