Hermes 플러그인 구축
anchor alias
anchor alias
anchor alias
anchor alias
Hermes 플러그인 구축
이 가이드는 처음부터 완전한 Hermes 플러그인을 구축하는 과정을 안내합니다. 결국에는 여러 도구, 수명 주기 후크, 제공된 데이터 파일, 번들 스킬 등 플러그인 시스템이 지원하는 모든 기능을 갖춘 작동하는 플러그인을 갖게 됩니다.
Hermes에는 몇 가지 고유한 플러그형 인터페이스가 있습니다. 일부는 Python register_* API를 사용하고 다른 일부는 구성 기반 또는 드롭인 디렉터리입니다. 먼저 이 지도를 사용하세요.
| 추가하려는 경우… | 읽기 |
|---|---|
| 사용자 정의 도구, 후크, 슬래시 명령, 기술 또는 CLI 하위 명령 | 이 가이드(일반 플러그인 표면) |
| LLM/추론 백엔드(새 제공자) | 모델 제공자 플러그인 |
| 게이트웨이 채널 (Discord/Telegram/IRC/Teams/etc.) | 플랫폼 어댑터 추가 |
| 메모리 백엔드(Honcho/Mem0/Supermemory/등) | 메모리 제공자 플러그인 |
| 컨텍스트 압축 엔진 | 컨텍스트 엔진 플러그인 |
| 이미지 생성 백엔드 | 이미지 생성 제공자 플러그인 |
| 비디오 생성 백엔드 | 비디오 생성 제공자 플러그인 |
| A TTS 백엔드(모든 CLI — Piper, VoxCPM, Kokoro, 음성 복제 등) | TTS 사용자 정의 명령 제공자 — 구성 기반, Python이 필요하지 않음 |
| STT 백엔드(사용자 정의 속삭임/ASR CLI) | 음성 메시지 기록 — HERMES_LOCAL_STT_COMMAND을 셸 템플릿으로 설정 |
| MCP를 통한 외부 도구(파일 시스템, GitHub, Linear, 모든 MCP 서버) | MCP — config.yaml에서 mcp_servers.<name>을 선언합니다. |
| 게이트웨이 이벤트 후크(시작 시 실행, 세션 이벤트, 명령) | 이벤트 후크 — HOOK.yaml + handler.py을 ~/.hermes/hooks/<name>/에 드롭합니다. |
| 셸 후크(이벤트에서 셸 명령 실행) | 셸 후크 — config.yaml의 hooks:에서 선언합니다. |
| 추가 기술 소스(맞춤형 GitHub 저장소, 비공개 기술 인덱스) | 기술 — hermes skills tap add <repo> · 탭 게시 |
| 일류 핵심 추론 제공자(플러그인 아님) | 제공자 추가 |
구성 기반(TTS, STT, MCP, 셸 후크) 및 드롭인 디렉터리(게이트웨이 후크) 스타일을 포함한 모든 확장 표면의 통합 보기는 전체 플러그 가능한 인터페이스 표를 참조하세요.
만들게 될 것
두 가지 도구가 포함된 계산기 플러그인:
calculate— 수학 표현식 평가(2**16,sqrt(144),pi * 5**2)unit_convert— 단위 간 변환(100 F → 37.78 C,5 km → 3.11 mi)
또한 모든 도구 호출을 기록하는 후크와 번들 스킬 파일도 있습니다.
1단계: 플러그인 디렉터리 생성
mkdir -p ~/.hermes/plugins/calculator
cd ~/.hermes/plugins/calculator
2단계: 매니페스트 작성
plugin.yaml 생성:
name: calculator
version: 1.0.0
description: Math calculator — evaluate expressions and convert units
provides_tools:
- calculate
- unit_convert
provides_hooks:
- post_tool_call
이는 Hermes에게 다음과 같이 말합니다. "저는 계산기라는 플러그인입니다. 도구와 후크를 제공합니다." provides_tools 및 provides_hooks 필드는 플러그인이 등록하는 항목의 목록입니다.
추가할 수 있는 선택 필드:
author: Your Name
requires_env: # gate loading on env vars; prompted during install
- SOME_API_KEY # simple format — plugin disabled if missing
- name: OTHER_KEY # rich format — shows description/url during install
description: "Key for the Other service"
url: "https://other.com/keys"
secret: true
3단계: 도구 스키마 작성
schemas.py 생성 - LLM이 도구 호출 시기를 결정하기 위해 읽는 내용입니다.
"""Tool schemas — what the LLM sees."""
CALCULATE = {
"name": "calculate",
"description": (
"Evaluate a mathematical expression and return the result. "
"Supports arithmetic (+, -, *, /, **), functions (sqrt, sin, cos, "
"log, abs, round, floor, ceil), and constants (pi, e). "
"Use this for any math the user asks about."
),
"parameters": {
"type": "object",
"properties": {
"expression": {
"type": "string",
"description": "Math expression to evaluate (e.g., '2**10', 'sqrt(144)')",
},
},
"required": ["expression"],
},
}
UNIT_CONVERT = {
"name": "unit_convert",
"description": (
"Convert a value between units. Supports length (m, km, mi, ft, in), "
"weight (kg, lb, oz, g), temperature (C, F, K), data (B, KB, MB, GB, TB), "
"and time (s, min, hr, day)."
),
"parameters": {
"type": "object",
"properties": {
"value": {
"type": "number",
"description": "The numeric value to convert",
},
"from_unit": {
"type": "string",
"description": "Source unit (e.g., 'km', 'lb', 'F', 'GB')",
},
"to_unit": {
"type": "string",
"description": "Target unit (e.g., 'mi', 'kg', 'C', 'MB')",
},
},
"required": ["value", "from_unit", "to_unit"],
},
}
스키마가 중요한 이유: description 필드는 LLM이 도구 사용 시기를 결정하는 방법입니다. 기능과 사용 시기를 구체적으로 설명하세요. parameters은 LLM이 전달하는 인수를 정의합니다.
4단계: 도구 핸들러 작성
tools.py 생성 — 이는 LLM이 도구를 호출할 때 실제로 실행되는 코드입니다.
"""Tool handlers — the code that runs when the LLM calls each tool."""
import json
import math
# Safe globals for expression evaluation — no file/network access
_SAFE_MATH = {
"abs": abs, "round": round, "min": min, "max": max,
"pow": pow, "sqrt": math.sqrt, "sin": math.sin, "cos": math.cos,
"tan": math.tan, "log": math.log, "log2": math.log2, "log10": math.log10,
"floor": math.floor, "ceil": math.ceil,
"pi": math.pi, "e": math.e,
"factorial": math.factorial,
}
def calculate(args: dict, **kwargs) -> str:
"""Evaluate a math expression safely.
Rules for handlers:
1. Receive args (dict) — the parameters the LLM passed
2. Do the work
3. Return a JSON string — ALWAYS, even on error
4. Accept **kwargs for forward compatibility
"""
expression = args.get("expression", "").strip()
if not expression:
return json.dumps({"error": "No expression provided"})
try:
result = eval(expression, {"__builtins__": {}}, _SAFE_MATH)
return json.dumps({"expression": expression, "result": result})
except ZeroDivisionError:
return json.dumps({"expression": expression, "error": "Division by zero"})
except Exception as e:
return json.dumps({"expression": expression, "error": f"Invalid: {e}"})
# Conversion tables — values are in base units
_LENGTH = {"m": 1, "km": 1000, "mi": 1609.34, "ft": 0.3048, "in": 0.0254, "cm": 0.01}
_WEIGHT = {"kg": 1, "g": 0.001, "lb": 0.453592, "oz": 0.0283495}
_DATA = {"B": 1, "KB": 1024, "MB": 1024**2, "GB": 1024**3, "TB": 1024**4}
_TIME = {"s": 1, "ms": 0.001, "min": 60, "hr": 3600, "day": 86400}
def _convert_temp(value, from_u, to_u):
# Normalize to Celsius
c = {"F": (value - 32) * 5/9, "K": value - 273.15}.get(from_u, value)
# Convert to target
return {"F": c * 9/5 + 32, "K": c + 273.15}.get(to_u, c)
def unit_convert(args: dict, **kwargs) -> str:
"""Convert between units."""
value = args.get("value")
from_unit = args.get("from_unit", "").strip()
to_unit = args.get("to_unit", "").strip()
if value is None or not from_unit or not to_unit:
return json.dumps({"error": "Need value, from_unit, and to_unit"})
try:
# Temperature
if from_unit.upper() in {"C","F","K"} and to_unit.upper() in {"C","F","K"}:
result = _convert_temp(float(value), from_unit.upper(), to_unit.upper())
return json.dumps({"input": f"{value} {from_unit}", "result": round(result, 4),
"output": f"{round(result, 4)} {to_unit}"})
# Ratio-based conversions
for table in (_LENGTH, _WEIGHT, _DATA, _TIME):
lc = {k.lower(): v for k, v in table.items()}
if from_unit.lower() in lc and to_unit.lower() in lc:
result = float(value) * lc[from_unit.lower()] / lc[to_unit.lower()]
return json.dumps({"input": f"{value} {from_unit}",
"result": round(result, 6),
"output": f"{round(result, 6)} {to_unit}"})
return json.dumps({"error": f"Cannot convert {from_unit} → {to_unit}"})
except Exception as e:
return json.dumps({"error": f"Conversion failed: {e}"})
핸들러의 주요 규칙:
- 서명:
def my_handler(args: dict,**kwargs) -> str - 반환: 항상 JSON 문자열입니다. 성공과 오류 모두 마찬가지입니다.
- 발생하지 않음: 모든 예외를 포착하고 대신 오류 JSON을 반환합니다.
**kwargs수락: Hermes는 향후 추가 컨텍스트를 전달할 수 있습니다.
5단계: 등록 작성
__init__.py 생성 - 스키마를 핸들러에 연결합니다.
"""Calculator plugin — registration."""
import logging
from. import schemas, tools
logger = logging.getLogger(__name__)
# Track tool usage via hooks
_call_log =
def _on_post_tool_call(tool_name, args, result, task_id, **kwargs):
"""Hook: runs after every tool call (not just ours)."""
_call_log.append({"tool": tool_name, "session": task_id})
if len(_call_log) > 100:
_call_log.pop(0)
logger.debug("Tool called: %s (session %s)", tool_name, task_id)
def register(ctx):
"""Wire schemas to handlers and register hooks."""
ctx.register_tool(name="calculate", toolset="calculator",
schema=schemas.CALCULATE, handler=tools.calculate)
ctx.register_tool(name="unit_convert", toolset="calculator",
schema=schemas.UNIT_CONVERT, handler=tools.unit_convert)
# This hook fires for ALL tool calls, not just ours
ctx.register_hook("post_tool_call", _on_post_tool_call)
register()이 하는 일:
- 시작 시 정확히 한 번 호출됨
ctx.register_tool()은 도구를 레지스트리에 넣습니다. 모델은 이를 즉시 확인합니다.ctx.register_hook()은 수명 주기 이벤트를 구독합니다.ctx.register_cli_command()은 CLI 하위 명령을 등록합니다(예:hermes my-plugin <subcommand>)ctx.register_command()은 세션 내 슬래시 명령을 등록합니다(예: CLI/게이트웨이 채팅 내부의/myplugin <args>) — 아래 슬래시 명령 등록을 참조하세요.ctx.dispatch_tool(name, arguments)— 상위 에이전트의 context(승인, 자격 증명, task_id)가 자동으로 연결된 다른 도구(내장 또는 다른 플러그인에서)를 호출합니다. 모델이 직접 호출한 것처럼terminal,read_file또는 기타 도구를 호출해야 하는 슬래시 명령 처리기에서 유용합니다.- 이 기능이 충돌하면 플러그인이 비활성화되지만 Hermes는 계속 잘 작동합니다.
dispatch_tool 예 — 도구를 실행하는 슬래시 명령:
def handle_scan(ctx, argstr):
"""Implement /scan by invoking the terminal tool through the registry."""
result = ctx.dispatch_tool("terminal", {"command": f"find. -name '{argstr}'"})
return result # returned to the caller's chat UI
def register(ctx):
ctx.register_command("scan", handle_scan, help="Find files matching a glob")
파견된 도구는 일반적인 승인, 수정 및 예산 파이프라인을 거칩니다. 이는 주변의 지름길이 아니라 실제 도구 호출입니다.
6단계: 테스트
Hermes 시작:
hermes
배너의 도구 목록에 calculator: calculate, unit_convert이 표시되어야 합니다.
다음 메시지를 시도해 보세요.
What's 2 to the power of 16?
Convert 100 fahrenheit to celsius
What's the square root of 2 times pi?
How many gigabytes is 1.5 terabytes?
플러그인 상태를 확인하세요:
/plugins
출력:
Plugins (1):
✓ calculator v1.0.0 (2 tools, 1 hooks)
디버깅 플러그인 검색
플러그인이 표시되지 않거나 표시되지만 로드되지 않는 경우 HERMES_PLUGINS_DEBUG=1을 설정하여 stderr에서 자세한 검색 로그를 얻습니다.
HERMES_PLUGINS_DEBUG=1 hermes plugins list
모든 플러그인 소스(번들, 사용자, 프로젝트, 진입점)에 대해 다음을 확인할 수 있습니다.
- 스캔된 디렉터리와 각각 생성된 매니페스트 수
- 매니페스트별: 확인된 키, 이름, 종류, 소스, 디스크 경로
- 건너뛰기 이유:
disabled via config,not enabled in config,exclusive plugin,no plugin.yaml, depth cap reached - 로드 시: 가져오는 플러그인과
register(ctx)등록된 내용(도구, 후크, 슬래시 명령, CLI 명령)에 대한 한 줄 요약 - 구문 분석 실패 시: 예외(YAML 스캐너 오류 등)에 대한 전체 역추적
register()실패:__init__.py에서 발생한 줄을 가리키는 전체 추적
env var가 설정되면 동일한 로그가 항상 WARNING 수준(실패만) 및 DEBUG 수준(모든 것)에서 ~/.hermes/logs/agent.log에 기록됩니다. 따라서 env var로 실행할 수 없는 경우(예: 게이트웨이 내부에서) 대신 로그 파일을 추적하세요.
hermes logs --level WARNING | grep -i plugin
플러그인이 표시되지 않는 일반적인 이유:
- 구성에서는 활성화되지 않음 — 플러그인이 선택되어 있습니다.
hermes plugins enable <name>을 실행합니다(이름은plugins list출력에서 나오며 중첩 레이아웃의 경우<category>/<plugin>일 수 있음). - 잘못된 디렉토리 레이아웃 —
~/.hermes/plugins/<plugin-name>/plugin.yaml(플랫) 또는~/.hermes/plugins/<category>/<plugin-name>/plugin.yaml(한 단계의 카테고리 중첩, 최대)이어야 합니다. 더 깊은 내용은 무시됩니다. - 누락된
__init__.py— 플러그인 디렉토리에는register(ctx)기능이 있는plugin.yaml및__init__.py이 모두 필요합니다. - 틀림
kind- 게이트웨이 어댑터의 매니페스트에kind: platform이 필요합니다. 메모리 제공자는kind: exclusive로 자동 감지되고plugins.enabled대신memory.provider구성을 통해 라우팅됩니다.
플러그인의 최종 구조
~/.hermes/plugins/calculator/
├── plugin.yaml # "I'm calculator, I provide tools and hooks"
├── __init__.py # Wiring: schemas → handlers, register hooks
├── schemas.py # What the LLM reads (descriptions + parameter specs)
└── tools.py # What runs (calculate, unit_convert functions)
4개의 파일, 명확한 구분:
- 매니페스트는 플러그인이 무엇인지 선언합니다.
- 스키마는 LLM용 도구를 설명합니다.
- 핸들러는 실제 로직을 구현합니다.
- 등록으로 모든 것이 연결됩니다
플러그인으로 또 무엇을 할 수 있나요?
배송 데이터 파일
플러그인 디렉터리에 파일을 넣고 가져올 때 읽어보세요.
# In tools.py or __init__.py
from pathlib import Path
_PLUGIN_DIR = Path(__file__).parent
_DATA_FILE = _PLUGIN_DIR / "data" / "languages.yaml"
with open(_DATA_FILE) as f:
_DATA = yaml.safe_load(f)
번들 스킬
플러그인은 에이전트가 skill_view("plugin:skill")을 통해 로드하는 스킬 파일을 제공할 수 있습니다. __init__.py에 등록하세요.
~/.hermes/plugins/my-plugin/
├── __init__.py
├── plugin.yaml
└── skills/
├── my-workflow/
│ └── SKILL.md
└── my-checklist/
└── SKILL.md
````python
from pathlib import Path
def register(ctx):
skills_dir = Path(__file__).parent / "skills"
for child in sorted(skills_dir.iterdir()):
skill_md = child / "SKILL.md"
if child.is_dir() and skill_md.exists():
ctx.register_skill(child.name, skill_md)
이제 에이전트는 네임스페이스 이름으로 기술을 로드할 수 있습니다.
skill_view("my-plugin:my-workflow") # → plugin's version
skill_view("my-workflow") # → built-in version (unchanged)
주요 속성:
- 플러그인 스킬은 읽기 전용입니다.
~/.hermes/skills/을 입력하지 않으며skill_manage을 통해 편집할 수 없습니다. - 플러그인 기술은 시스템 프롬프트의
<available_skills>색인에 나열되지 않습니다 — 선택적으로 명시적으로 로드됩니다. - 기본 스킬 이름은 영향을 받지 않습니다. 네임스페이스는 내장 스킬과의 충돌을 방지합니다.
- 에이전트가 플러그인 기술을 로드하면 동일한 플러그인의 형제 기술을 나열하는 번들 컨텍스트 배너가 앞에 추가됩니다.
이전 shutil.copy2 패턴(기술을 ~/.hermes/skills/에 복사)은 여전히 작동하지만 내장된 기술과 이름 충돌 위험이 발생합니다. 새 플러그인의 경우 ctx.register_skill()을 선호하세요.
환경 변수에 대한 게이트
플러그인에 API 키가 필요한 경우:
# plugin.yaml — simple format (backwards-compatible)
requires_env:
- WEATHER_API_KEY
WEATHER_API_KEY이 설정되지 않은 경우 명확한 메시지와 함께 플러그인이 비활성화됩니다. 충돌도 없고 에이전트에 오류도 없습니다. 단지 "플러그인 날씨가 비활성화되었습니다(누락: WEATHER_API_KEY)"입니다.
사용자가 hermes plugins install을 실행하면 누락된 requires_env 변수에 대해 대화형으로 프롬프트가 표시됩니다. 값은 .env에 자동으로 저장됩니다.
더 나은 설치 환경을 위해 설명 및 가입 URL이 포함된 서식 있는 형식을 사용하세요.
# plugin.yaml — rich format
requires_env:
- name: WEATHER_API_KEY
description: "API key for OpenWeather"
url: "https://openweathermap.org/api"
secret: true
| 필드 | 필수 | 설명 |
|---|---|---|
name | 예 | 환경 변수 이름 |
description | No | 설치 프롬프트 중에 사용자에게 표시됨 |
url | No | 자격증을 받을 수 있는 곳 |
secret | No | true인 경우 입력이 숨겨집니다(예: 비밀번호 필드). |
두 형식 모두 동일한 목록에 혼합될 수 있습니다. 이미 설정된 변수는 자동으로 건너뜁니다.
조건부 도구 가용성
선택적 라이브러리에 의존하는 도구의 경우:
ctx.register_tool(
name="my_tool",
schema={...},
handler=my_handler,
check_fn=lambda: _has_optional_lib(), # False = tool hidden from model
)
여러 Hook 등록
def register(ctx):
ctx.register_hook("pre_tool_call", before_any_tool)
ctx.register_hook("post_tool_call", after_any_tool)
ctx.register_hook("pre_llm_call", inject_memory)
ctx.register_hook("on_session_start", on_new_session)
ctx.register_hook("on_session_end", on_session_end)
후크 참조
각 후크는 **이벤트 후크 참조**에 전체 문서화되어 있습니다. 콜백 서명, 매개변수 테이블, 각각이 실행되는 정확한 시점 및 예시입니다. 요약은 다음과 같습니다.
| 후크 | 다음과 같은 경우에 발생합니다. | 콜백 서명 | 반품 |
|---|---|---|---|
pre_tool_call | 도구가 실행되기 전 | tool_name: str, args: dict, task_id: str | 무시됨 |
post_tool_call | 도구가 반환된 후 | tool_name: str, args: dict, result: str, task_id: str, duration_ms: int | 무시됨 |
pre_llm_call | 턴당 한 번, 도구 호출 루프 이전 | session_id: str, user_message: str, conversation_history: list, is_first_turn: bool, model: str, platform: str | 컨텍스트 주입 |
post_llm_call | 턴당 한 번, 도구 호출 루프 이후(성공적인 턴에만) | session_id: str, user_message: str, assistant_response: str, conversation_history: list, model: str, platform: str | 무시됨 |
on_session_start | 새 세션이 생성되었습니다(첫 번째 턴에만 해당). | session_id: str, model: str, platform: str | 무시됨 |
on_session_end | 모든 run_conversation 호출 종료 + CLI 종료 | session_id: str, completed: bool, interrupted: bool, model: str, platform: str | 무시됨 |
on_session_finalize | CLI/게이트웨이가 활성 세션을 종료합니다. | session_id: str | 없음, 플랫폼: str | 무시됨 |
on_session_reset | 새 세션 키(/new, /reset)로 게이트웨이를 교체합니다. | session_id: str, platform: str | 무시됨 |
Most hooks are fire-and-forget observers — their return values are ignored. 예외는 대화에 컨텍스트를 삽입할 수 있는 pre_llm_call입니다.
모든 콜백은 향후 호환성을 위해 **kwargs을 허용해야 합니다. 후크 콜백이 충돌하면 기록되고 건너뜁니다. 다른 후크와 에이전트는 정상적으로 계속됩니다.
pre_llm_call 컨텍스트 삽입
이 훅은 반환 값이 실제로 사용되는 유일한 훅입니다. pre_llm_call 콜백이 "context" 키(또는 일반 문자열)가 포함된 딕셔너리를 반환하면 Hermes는 해당 텍스트를 현재 턴의 사용자 메시지에 삽입합니다. 메모리 플러그인, RAG 통합, 가드레일처럼 모델에 추가 컨텍스트를 제공해야 하는 플러그인이 이 메커니즘을 사용합니다.
반환 형식
# Dict with context key
return {"context": "Recalled memories:\n- User prefers dark mode\n- Last project: hermes-agent"}
# Plain string (equivalent to the dict form above)
return "Recalled memories:\n- User prefers dark mode"
# Return None or don't return → no injection (observer-only)
return None
"context" 키(또는 비어 있지 않은 일반 문자열)를 사용하여 None이 아니고 비어 있지 않은 반환이 수집되어 현재 차례에 대한 사용자 메시지에 추가됩니다.
주입 작동 방식
주입된 컨텍스트는 시스템 프롬프트가 아닌 사용자 메시지에 추가됩니다. 이는 의도적인 디자인 선택입니다.
- 프롬프트 캐시 보존 — 시스템 프롬프트는 여러 차례에 걸쳐 동일하게 유지됩니다. Anthropic 및 OpenRouter는 시스템 프롬프트 접두사를 캐시하므로 이를 안정적으로 유지하면 다중 회전 대화에서 입력 토큰이 75% 이상 절약됩니다. 플러그인이 시스템 프롬프트를 수정한 경우 매 턴마다 캐시 누락이 발생합니다.
- 임시 — 주입은 API 호출 시에만 발생합니다. 대화 기록의 원래 사용자 메시지는 변경되지 않으며 세션 데이터베이스에 아무것도 유지되지 않습니다.
- 시스템 프롬프트는 Hermes의 영역입니다. 여기에는 모델별 지침, 도구 적용 규칙, 성격 지침 및 캐시된 기술 콘텐츠가 포함되어 있습니다. 플러그인은 에이전트의 핵심 지침을 변경하지 않고 사용자 입력과 함께 컨텍스트를 제공합니다.
예: 메모리 불러오기 플러그인
"""Memory plugin — recalls relevant 컨텍스트 from a vector store."""
import httpx
MEMORY_API = "https://your-memory-api.example.com"
def recall_컨텍스트(session_id, user_message, is_first_turn, **kwargs):
"""Called before each LLM turn. Returns recalled memories."""
try:
resp = httpx.post(f"{MEMORY_API}/recall", json={
"session_id": session_id,
"query": user_message,
}, timeout=3)
memories = resp.json().get("results", )
if not memories:
return None # nothing to inject
text = "Recalled 컨텍스트 from previous sessions:\n"
text += "\n".join(f"- {m['text']}" for m in memories)
return {"컨텍스트": text}
except Exception:
return None # fail silently, don't break the agent
def register(ctx):
ctx.register_hook("pre_llm_call", recall_컨텍스트)
예: 가드레일 플러그인
"""Guardrails plugin — enforces content policies."""
POLICY = """You MUST follow these content policies for this session:
- Never generate code that accesses the filesystem outside the working directory
- Always warn before executing destructive operations
- Refuse requests involving personal data extraction"""
def inject_guardrails(**kwargs):
"""Injects policy text into every turn."""
return {"컨텍스트": POLICY}
def register(ctx):
ctx.register_hook("pre_llm_call", inject_guardrails)
예: 관찰자 전용 후크(삽입 없음)
"""Analytics plugin — tracks turn metadata without injecting 컨텍스트."""
import logging
logger = logging.getLogger(__name__)
def log_turn(session_id, user_message, model, is_first_turn, **kwargs):
"""Fires before each LLM call. Returns None — no 컨텍스트 injected."""
logger.info("Turn: session=%s model=%s first=%s msg_len=%d",
session_id, model, is_first_turn, len(user_message or ""))
# No return → no injection
def register(ctx):
ctx.register_hook("pre_llm_call", log_turn)
컨텍스트를 반환하는 여러 플러그인
여러 플러그인이 pre_llm_call에서 컨텍스트를 반환하면 해당 출력은 이중 줄바꿈으로 결합되어 사용자 메시지에 함께 추가됩니다. 순서는 플러그인 검색 순서를 따릅니다(플러그인 디렉터리 이름의 알파벳순).
CLI 명령 등록
플러그인은 자체 hermes <plugin> 하위 명령 트리를 추가할 수 있습니다.
def _my_command(args):
"""Handler for hermes my-plugin <subcommand>."""
sub = getattr(args, "my_command", None)
if sub == "status":
print("All good!")
elif sub == "config":
print("Current config:...")
else:
print("Usage: hermes my-plugin <status|config>")
def _setup_argparse(subparser):
"""Build the argparse tree for hermes my-plugin."""
subs = subparser.add_subparsers(dest="my_command")
subs.add_parser("status", help="Show plugin status")
subs.add_parser("config", help="Show plugin config")
subparser.set_defaults(func=_my_command)
def register(ctx):
ctx.register_tool(...)
ctx.register_cli_command(
name="my-plugin",
help="Manage my plugin",
setup_fn=_setup_argparse,
handler_fn=_my_command,
)
등록 후 사용자는 hermes my-plugin status, hermes my-plugin config 등을 실행할 수 있습니다.
메모리 제공자 플러그인은 대신 규칙 기반 접근 방식을 사용합니다. 즉, 플러그인의 cli.py 파일에 register_cli(subparser) 함수를 추가합니다. 메모리 플러그인 검색 시스템이 자동으로 이를 찾습니다. ctx.register_cli_command() 호출이 필요하지 않습니다. 자세한 내용은 메모리 제공자 플러그인 가이드를 참조하세요.
활성 제공자 게이팅: 메모리 플러그인 CLI 명령은 해당 제공자가 구성에서 활성 memory.provider인 경우에만 나타납니다. 사용자가 제공자를 설정하지 않은 경우 CLI 명령으로 인해 도움말 출력이 복잡해지지 않습니다.
슬래시 명령어 등록
플러그인은 세션 내 슬래시 명령(사용자가 대화 중에 입력하는 명령(예: /lcm status 또는 /ping))을 등록할 수 있습니다. 이는 CLI와 게이트웨이(Telegram, Discord 등) 모두에서 작동합니다.
def _handle_status(raw_args: str) -> str:
"""Handler for /mystatus — called with everything after the command name."""
if raw_args.strip() == "help":
return "Usage: /mystatus [help|check]"
return "Plugin status: all systems nominal"
def register(ctx):
ctx.register_command(
"mystatus",
handler=_handle_status,
description="Show plugin status",
)
등록 후 사용자는 모든 세션에서 /mystatus을 입력할 수 있습니다. 명령은 자동 완성, /help 출력 및 Telegram 봇 메뉴에 나타납니다.
서명: ctx.register_command(name: str, handler: Callable, description: str = "")
| 매개변수 | 유형 | 설명 |
|---|---|---|
name | str | 앞에 슬래시가 없는 명령 이름(예: "lcm", "mystatus") |
handler | 호출 가능[[str], str | 없음] | 원시 인수 문자열로 호출됩니다. async일 수도 있습니다. |
description | str | /help, 자동 완성 및 텔레그램 봇 메뉴에 표시됨 |
register_cli_command()과의 주요 차이점:
register_command() | register_cli_command() | |
|---|---|---|
| 다음으로 호출됨 | 세션의 /name | 터미널의 hermes name |
| 그것이 작동하는 곳 | CLI 세션, 텔레그램, 디스코드 등 | 터미널 전용 |
| 핸들러가 수신함 | 원시 인수 문자열 | 인수 분석 Namespace |
| 사용 사례 | 진단, 상태, 빠른 작업 | 복잡한 하위 명령 트리, 설정 마법사 |
충돌 방지: 플러그인이 내장 명령(help, model, new 등)과 충돌하는 이름을 등록하려고 하면 로그 경고와 함께 등록이 자동으로 거부됩니다. 내장 명령이 항상 우선 적용됩니다.
비동기 처리기: 게이트웨이 디스패치는 비동기 처리기를 자동으로 감지하고 기다리므로 동기화 또는 비동기 기능을 사용할 수 있습니다.
async def _handle_check(raw_args: str) -> str:
result = await some_async_operation()
return f"Check result: {result}"
def register(ctx):
ctx.register_command("check", handler=_handle_check, description="Run async check")
슬래시 명령으로 도구 전달
도구를 조정해야 하는 슬래시 명령 처리기(delegate_task을 통해 하위 에이전트 생성, file_edit 호출 등)는 프레임워크 내부에 연결하는 대신 ctx.dispatch_tool()을 사용해야 합니다. 상위 에이전트 컨텍스트(작업 공간 힌트, 스피너, 모델 상속)는 자동으로 연결됩니다.
def register(ctx):
def _handle_deliver(raw_args: str):
result = ctx.dispatch_tool(
"delegate_task",
{
"goal": raw_args,
"toolsets": ["terminal", "file", "web"],
},
)
return result
ctx.register_command(
"deliver",
handler=_handle_deliver,
description="Delegate a goal to a subagent",
)
서명: ctx.dispatch_tool(name: str, args: dict, *, parent_agent=None) -> str
| 매개변수 | 유형 | 설명 |
|---|---|---|
name | str | 도구 레지스트리에 등록된 도구 이름(예: "delegate_task", "file_edit") |
args | dict | 도구 인수, 모델이 보내는 것과 동일한 모양 |
parent_agent | 에이전트 | 없음 | 선택적 재정의. 생략하면 현재 CLI 에이전트에서 확인됩니다(또는 게이트웨이 모드에서 단계적으로 성능 저하). |
런타임 동작:
- CLI 모드:
parent_agent은 활성 CLI 에이전트에서 확인되므로 작업공간 힌트, 스피너 및 모델 선택이 예상대로 상속됩니다. - 게이트웨이 모드: CLI 에이전트가 없으므로 도구 성능이 정상적으로 저하됩니다. 작업 공간은
TERMINAL_CWD에서 읽혀지며 스피너가 표시되지 않습니다. - 명시적 재정의: 호출자가
parent_agent=을 명시적으로 전달하는 경우 이를 존중하고 덮어쓰지 않습니다.
이는 플러그인 명령에서 도구를 발송하기 위한 안정적인 공용 인터페이스입니다. 플러그인은 ctx._cli_ref.agent 또는 이와 유사한 비공개 상태에 도달해서는 안 됩니다.
이 가이드에서는 일반 플러그인(도구, 후크, 슬래시 명령, CLI 명령)을 다룹니다. 아래 섹션에서는 각 특수 플러그인 유형에 대한 작성 패턴을 개략적으로 설명합니다. 각 링크는 현장 참조 및 예시에 대한 전체 가이드로 연결됩니다.
전문 플러그인 유형
Hermes에는 일반 플러그인 표면을 넘어서 다섯 가지 특수 플러그인 유형이 있습니다. 각각은 plugins/<category>/<name>/(번들) 또는 ~/.hermes/plugins/<category>/<name>/(사용자) 아래의 디렉터리로 제공됩니다. 계약은 카테고리별로 다릅니다. 필요한 유형을 선택한 다음 전체 가이드를 읽어보세요.
모델 제공자 플러그인 — LLM 백엔드 추가
plugins/model-providers/<name>/에 프로필을 추가하세요.
# plugins/model-providers/acme/__init__.py
from providers import register_provider
from providers.base import ProviderProfile
register_provider(ProviderProfile(
name="acme",
aliases=("acme-inference",),
display_name="Acme Inference",
env_vars=("ACME_API_KEY", "ACME_BASE_URL"),
base_url="https://api.acme.example.com/v1",
auth_type="api_key",
default_aux_model="acme-small-fast",
fallback_models=("acme-large-v3", "acme-medium-v3"),
))
````yaml
# plugins/model-providers/acme/plugin.yaml
name: acme-provider
kind: model-provider
version: 1.0.0
description: Acme Inference — OpenAI-compatible direct API
get_provider_profile() 또는 list_providers()을 처음 호출할 때 게으른 발견 — auth.py, config.py, doctor.py, models.py, runtime_provider.py 및 chat_completions가 자동 연결을 전송합니다. 사용자 플러그인은 번들로 제공되는 플러그인을 이름으로 재정의합니다.
전체 가이드: 모델 제공자 플러그인 — 필드 참조, 재정의 가능한 후크(prepare_messages, build_extra_body, build_api_kwargs_extras, fetch_models), api_mode 선택, 인증 유형, 테스트.
플랫폼 플러그인 — 게이트웨이 채널 추가
어댑터를 plugins/platforms/<name>/에 놓습니다.
# plugins/platforms/myplatform/adapter.py
from gateway.platforms.base import BasePlatformAdapter
class MyPlatformAdapter(BasePlatformAdapter):
async def connect(self):...
async def send(self, chat_id, text):...
async def disconnect(self):...
def check_requirements():
import os
return bool(os.environ.get("MYPLATFORM_TOKEN"))
def _env_enablement():
import os
tok = os.getenv("MYPLATFORM_TOKEN", "").strip()
if not tok:
return None
return {"token": tok}
def register(ctx):
ctx.register_platform(
name="myplatform",
label="MyPlatform",
adapter_factory=lambda cfg: MyPlatformAdapter(cfg),
check_fn=check_requirements,
required_env=["MYPLATFORM_TOKEN"],
# Auto-populate PlatformConfig.extra from env so env-only setups
# show up in `hermes gateway status` without SDK instantiation.
env_enablement_fn=_env_enablement,
# Opt in to cron delivery: `deliver=myplatform` routes to this var.
cron_deliver_env_var="MYPLATFORM_HOME_CHANNEL",
emoji="💬",
platform_hint="You are chatting via MyPlatform. Keep responses concise.",
)
````yaml
# plugins/platforms/myplatform/plugin.yaml
name: myplatform-platform
label: MyPlatform
kind: platform
version: 1.0.0
description: MyPlatform gateway adapter
requires_env:
- name: MYPLATFORM_TOKEN
description: "Bot token from the MyPlatform console"
password: true
optional_env:
- name: MYPLATFORM_HOME_CHANNEL
description: "Default channel for cron delivery"
password: false
전체 가이드: 플랫폼 어댑터 추가 — BasePlatformAdapter 계약, 메시지 라우팅, 인증 게이팅, 설정 마법사 통합을 완료합니다. stdlib 전용 작업 예제는 plugins/platforms/irc/을 참조하세요.
메모리 제공자 플러그인 - 세션 간 지식 백엔드 추가
MemoryProvider 구현을 plugins/memory/<name>/에 삭제합니다.
# plugins/memory/my-memory/__init__.py
from agent.memory_provider import MemoryProvider
class MyMemoryProvider(MemoryProvider):
@property
def name(self) -> str:
return "my-memory"
def is_available(self) -> bool:
import os
return bool(os.environ.get("MY_MEMORY_API_KEY"))
def initialize(self, session_id: str, **kwargs) -> None:
self._session_id = session_id
def sync_turn(self, user_message, assistant_response, **kwargs) -> None:...
def prefetch(self, query: str, **kwargs) -> str | None:...
def register(ctx):
ctx.register_memory_provider(MyMemoryProvider())
메모리 제공자는 단일 선택입니다. 한 번에 하나만 활성화되며 config.yaml의 memory.provider을 통해 선택됩니다.
전체 가이드: 메모리 제공자 플러그인 — 전체 MemoryProvider ABC, 스레딩 계약, 프로필 격리, cli.py을 통한 CLI 명령 등록.
컨텍스트 엔진 플러그인 — 컨텍스트 압축기 교체
# plugins/context_engine/my-engine/__init__.py
from agent.context_engine import ContextEngine
class MyContextEngine(ContextEngine):
@property
def name(self) -> str:
return "my-engine"
def should_compress(self, messages, model) -> bool:...
def compress(self, messages, model) -> list[dict]:...
def register(ctx):
ctx.register_context_engine(MyContextEngine())
컨텍스트 엔진은 config.yaml의 컨텍스트.engine을 통해 선택되는 단일 선택입니다.
전체 가이드: 컨텍스트 엔진 플러그인.
이미지 생성 백엔드
제공자를 plugins/image_gen/<name>/에 추가합니다.
# plugins/image_gen/my-imggen/__init__.py
from agent.image_gen_provider import ImageGenProvider
class MyImageGenProvider(ImageGenProvider):
@property
def name(self) -> str:
return "my-imggen"
def is_available(self) -> bool:...
def generate(self, prompt: str, **kwargs) -> str:... # returns image path
def register(ctx):
ctx.register_image_gen_provider(MyImageGenProvider())
````yaml
# plugins/image_gen/my-imggen/plugin.yaml
name: my-imggen
kind: backend
version: 1.0.0
description: Custom image generation backend
전체 가이드: 이미지 생성 제공자 플러그인 — 전체 ImageGenProvider ABC, list_models() / get_setup_schema() 메타데이터, success_response()/error_response() 도우미, base64 대 URL 출력, 사용자 재정의, pip 배포.
참조 예: plugins/image_gen/openai/(OpenAI SDK를 통한 DALL-E/GPT-이미지), plugins/image_gen/openai-codex/, plugins/image_gen/xai/(Grok 이미지 생성).
Python이 아닌 확장 표면
Hermes는 Python 플러그인이 아닌 확장도 허용합니다. 이는 플러그 가능한 인터페이스 표에 표시되어 있습니다. 아래 섹션에서는 각 작성 스타일을 간략하게 설명합니다.
MCP 서버 - 외부 도구 등록
MCP(Model Context Protocol) 서버는 Python 플러그인 없이 Hermes에 자체 도구를 등록합니다. ~/.hermes/config.yaml에서 선언하세요.
mcp_servers:
filesystem:
command: "npx"
args: ["-y", "@model컨텍스트protocol/server-filesystem", "/home/user/projects"]
timeout: 120
linear:
url: "https://mcp.linear.app/sse"
auth:
type: "oauth"
Hermes는 시작 시 각 서버에 연결하여 해당 도구를 나열하고 내장된 기능과 함께 등록합니다. LLM은 다른 도구와 똑같이 이를 봅니다. 전체 가이드: MCP.
게이트웨이 이벤트 후크 — 수명 주기 이벤트 발생
매니페스트 + 핸들러를 ~/.hermes/hooks/<name>/에 놓습니다.
# ~/.hermes/hooks/long-task-alert/HOOK.yaml
name: long-task-alert
description: Send a push notification when a long task finishes
events:
- agent:end
````python
# ~/.hermes/hooks/long-task-alert/handler.py
async def handle(event_type: str, context: dict) -> None:
if context.get("duration_seconds", 0) > 120:
# send notification …
pass
이벤트에는 gateway:startup, session:start, session:end, session:reset, agent:start, agent:step, agent:end 및 와일드카드 command:*. 후크의 오류는 포착되어 기록되며 주 파이프라인을 차단하지 않습니다.
전체 가이드: 게이트웨이 이벤트 후크.
셸 후크 — 도구 호출 시 셸 명령 실행
도구(알림, 감사 로그, 데스크톱 경고, 자동 포맷터)가 실행될 때 스크립트를 실행하려면 config.yaml에서 셸 후크를 사용하세요. Python이 필요하지 않습니다.
hooks:
- event: post_tool_call
command: "notify-send 'Tool ran: {tool_name}'"
when:
tools: [terminal, patch, write_file]
Python 플러그인 후크와 동일한 이벤트를 모두 지원합니다. pre_gateway_dispatch) 및 pre_tool_call 차단 결정에 대한 구조화된 JSON 출력입니다.
전체 가이드: 셸 후크.
스킬 소스 - 맞춤형 스킬 레지스트리 추가
GitHub 기술 저장소를 유지 관리하는 경우(또는 내장 소스 이외의 커뮤니티 색인에서 가져오려는 경우) 탭으로 추가하세요.
hermes skills tap add myorg/skills-repo
hermes skills search my-workflow --source myorg/skills-repo
hermes skills install myorg/skills-repo/my-workflow
자신만의 탭을 게시하는 것은 skills/<skill-name>/SKILL.md 디렉터리가 있는 GitHub 저장소에 불과합니다. 서버나 레지스트리 등록이 필요하지 않습니다.
전체 가이드: 스킬 허브 · 사용자 정의 탭 게시(저장소 레이아웃, 최소 예시, 기본이 아닌 경로, 신뢰 수준).
명령 템플릿을 통한 TTS/STT
오디오 또는 텍스트를 읽고 쓰는 모든 CLI는 config.yaml을 통해 연결할 수 있습니다. Python 코드는 없습니다.
tts:
provider: voxcpm
providers:
voxcpm:
type: command
command: "voxcpm --ref ~/voice.wav --text-file {input_path} --out {output_path}"
output_format: mp3
voice_compatible: true
STT의 경우 쉘 템플릿에서 HERMES_LOCAL_STT_COMMAND을 가리킵니다. 지원되는 자리 표시자: {input_path}, {output_path}, {format}, {voice}, {model}, {speed}(TTS); {input_path}, {output_dir}, {language}, {model}(STT). 모든 경로 상호작용 CLI는 자동으로 플러그인입니다.
전체 가이드: TTS 사용자 정의 명령 제공자 · STT.
pip를 통해 배포
플러그인을 공개적으로 공유하려면 Python 패키지에 진입점을 추가하세요.
# pyproject.toml
[project.entry-points."hermes_agent.plugins"]
my-plugin = "my_plugin_package"
````bash
pip install hermes-plugin-calculator
# Plugin auto-discovered on next hermes startup
NixOS용 배포
NixOS 사용자는 진입점과 함께 pyproject.toml을 제공하는 경우 플러그인을 선언적으로 설치할 수 있습니다.
진입점 플러그인(배포용으로 권장):
# User's configuration.nix
services.hermes-agent.extraPythonPackages = [
(pkgs.python312Packages.buildPythonPackage {
pname = "my-plugin";
version = "1.0.0";
src = pkgs.fetchFromGitHub {
owner = "you";
repo = "hermes-my-plugin";
rev = "v1.0.0";
hash = "sha256-..."; # nix-prefetch-url --unpack
};
format = "pyproject";
build-system = [ pkgs.python312Packages.setuptools ];
})
];
디렉터리 플러그인(pyproject.toml 필요 없음):
services.hermes-agent.extraPlugins = [
(pkgs.fetchFromGitHub {
owner = "you";
repo = "hermes-my-plugin";
rev = "v1.0.0";
hash = "sha256-...";
})
];
오버레이 사용 및 충돌 검사를 포함한 전체 문서는 Nix 설정 가이드를 참조하세요.
일반적인 실수
핸들러는 JSON 문자열을 반환하지 않습니다:
# Wrong — returns a dict
def handler(args, **kwargs):
return {"result": 42}
# Right — returns a JSON string
def handler(args, **kwargs):
return json.dumps({"result": 42})
핸들러 서명에 **kwargs 누락:
# Wrong — will break if Hermes passes extra 컨텍스트
def handler(args):...
# Right
def handler(args, **kwargs):...
핸들러에서 예외가 발생합니다.
# Wrong — exception propagates, tool call fails
def handler(args, **kwargs):
result = 1 / int(args["value"]) # ZeroDivisionError!
return json.dumps({"result": result})
# Right — catch and return error JSON
def handler(args, **kwargs):
try:
result = 1 / int(args.get("value", 0))
return json.dumps({"result": result})
except Exception as e:
return json.dumps({"error": str(e)})
스키마 설명이 너무 모호함:
# Bad — model doesn't know when to use it
"description": "Does stuff"
# Good — model knows exactly when and how
"description": "Evaluate a mathematical expression. Use for arithmetic, trig, logarithms. Supports: +, -, *, /, **, sqrt, sin, cos, log, pi, e."