hermes - 💡(How to fix) Fix [i18n] Thai Translation: Features Part 2a - Hooks, Honcho, Image Gen [1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
NousResearch/hermes-agent#15002Fetched 2026-04-25 06:25:19
View on GitHub
Comments
0
Participants
1
Timeline
2
Reactions
0
Author
Participants
Timeline (top)
labeled ×2

Error Message

import httpx

MEMORY_API = "https://your-memory-api.example.com"

def recall(session_id, user_message, is_first_turn, **kwargs): 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 text = "Recalled context:\n" + "\n".join(f"- {m['text']}" for m in memories) return {"context": text} except Exception: return None

def register(ctx): ctx.register_hook("pre_llm_call", recall)

Fix Action

Fix / Workaround

DANGEROUS = {"terminal", "write_file", "patch"}

- **บล็อกการเรียกใช้ tool** - ปฏิเสธคำสั่ง `terminal` ที่อันตราย บังคับใช้นโยบายต่อ directory แต่ละตัว หรือกำหนดให้ต้องมีการอนุมัติสำหรับการดำเนินการ `write_file` / `patch` ที่ทำลายข้อมูล
- **รันหลังจากเรียกใช้ tool** - จัดรูปแบบไฟล์ Python หรือ TypeScript ที่ agent เพิ่งเขียนให้โดยอัตโนมัติ บันทึกการเรียกใช้ API หรือกระตุ้น workflow ของ CI
- **ฉีด context เข้าไปในการเรียก LLM ครั้งถัดไป** - เพิ่มผลลัพธ์ของ `git status` วันในสัปดาห์ปัจจุบัน หรือเอกสารที่ดึงมา ไปยังข้อความของผู้ใช้ (ดูที่ [`pre_llm_call`](#pre_llm_call))
- **สังเกตการณ์ event ของ lifecycle** - เขียนบรรทัด log เมื่อ subagent ทำงานเสร็จสิ้น (`subagent_stop`) หรือเมื่อเซสชันเริ่มต้น (`on_session_start`)

Shell hooks ถูกลงทะเบียนโดยการเรียกใช้ `agent.shell_hooks.register_from_config(cfg)` ทั้งที่การเริ่มต้น CLI (`hermes_cli/main.py`) และที่การเริ่มต้น gateway (`gateway/run.py`) พวกมันทำงานร่วมกับ Python plugin hooks ได้อย่างเป็นธรรมชาติ - ทั้งคู่จะไหลผ่าน dispatcher ตัวเดียวกัน

Code Example

~/.hermes/hooks/
└── my-hook/
    ├── HOOK.yaml      # Declares which events to listen for
    └── handler.py     # Python handler function

---

name: my-hook
description: Log all agent activity to a file
events:
  - agent:start
  - agent:end
  - agent:step

---

import json
from datetime import datetime
from pathlib import Path

LOG_FILE = Path.home() / ".hermes" / "hooks" / "my-hook" / "activity.log"

async def handle(event_type: str, context: dict):
    """Called for each subscribed event. Must be named 'handle'."""
    entry = {
        "timestamp": datetime.now().isoformat(),
        "event": event_type,
        **context,
    }
    with open(LOG_FILE, "a") as f:
        f.write(json.dumps(entry) + "\n")

---

# Startup Checklist

1. Check if any cron jobs failed overnight - run `hermes cron list`
2. Send a message to Discord #general saying "Gateway restarted, all systems go"
3. Check if /opt/app/deploy.log has any errors from the last 24 hours

---

# ~/.hermes/hooks/long-task-alert/HOOK.yaml
name: long-task-alert
description: Alert when agent is taking many steps
events:
  - agent:step

---

# ~/.hermes/hooks/long-task-alert/handler.py
import os
import httpx

THRESHOLD = 10
BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
CHAT_ID = os.getenv("TELEGRAM_HOME_CHANNEL")

async def handle(event_type: str, context: dict):
    iteration = context.get("iteration", 0)
    if iteration == THRESHOLD and BOT_TOKEN and CHAT_ID:
        tools = ", ".join(context.get("tool_names", []))
        text = f"⚠️ Agent has been running for {iteration} steps. Last tools: {tools}"
        async with httpx.AsyncClient() as client:
            await client.post(
                f"https://api.telegram.org/bot{BOT_TOKEN}/sendMessage",
                json={"chat_id": CHAT_ID, "text": text},
            )

---

# ~/.hermes/hooks/command-logger/HOOK.yaml
name: command-logger
description: Log slash command usage
events:
  - command:*

---

# ~/.hermes/hooks/command-logger/handler.py
import json
from datetime import datetime
from pathlib import Path

LOG = Path.home() / ".hermes" / "logs" / "command_usage.jsonl"

def handle(event_type: str, context: dict):
    LOG.parent.mkdir(parents=True, exist_ok=True)
    entry = {
        "ts": datetime.now().isoformat(),
        "command": context.get("command"),
        "args": context.get("args"),
        "platform": context.get("platform"),
        "user": context.get("user_id"),
    }
    with open(LOG, "a") as f:
        f.write(json.dumps(entry) + "\n")

---

# ~/.hermes/hooks/session-webhook/HOOK.yaml
name: session-webhook
description: Notify external service on new sessions
events:
  - session:start
  - session:reset

---

# ~/.hermes/hooks/session-webhook/handler.py
import httpx

WEBHOOK_URL = "https://your-service.example.com/hermes-events"

async def handle(event_type: str, context: dict):
    async with httpx.AsyncClient() as client:
        await client.post(WEBHOOK_URL, json={
            "event": event_type,
            **context,
        }, timeout=5)

---

def register(ctx):
    ctx.register_hook("pre_tool_call", my_tool_observer)
    ctx.register_hook("post_tool_call", my_tool_logger)
    ctx.register_hook("pre_llm_call", my_memory_callback)
    ctx.register_hook("post_llm_call", my_sync_callback)
    ctx.register_hook("on_session_start", my_init_callback)
    ctx.register_hook("on_session_end", my_cleanup_callback)

---

def my_callback(tool_name: str, args: dict, task_id: str, **kwargs):

---

return {"action": "block", "message": "Reason the tool call was blocked"}

---

import json, logging
from datetime import datetime

logger = logging.getLogger(__name__)

def audit_tool_call(tool_name, args, task_id, **kwargs):
    logger.info("TOOL_CALL session=%s tool=%s args=%s",
                task_id, tool_name, json.dumps(args)[:200])

def register(ctx):
    ctx.register_hook("pre_tool_call", audit_tool_call)

---

DANGEROUS = {"terminal", "write_file", "patch"}

def warn_dangerous(tool_name, **kwargs):
    if tool_name in DANGEROUS:
        print(f"⚠ Executing potentially dangerous tool: {tool_name}")

def register(ctx):
    ctx.register_hook("pre_tool_call", warn_dangerous)

---

def my_callback(tool_name: str, args: dict, result: str, task_id: str, **kwargs):

---

from collections import Counter
import json

_tool_counts = Counter()
_error_counts = Counter()

def track_metrics(tool_name, result, **kwargs):
    _tool_counts[tool_name] += 1
    try:
        parsed = json.loads(result)
        if "error" in parsed:
            _error_counts[tool_name] += 1
    except (json.JSONDecodeError, TypeError):
        pass

def register(ctx):
    ctx.register_hook("post_tool_call", track_metrics)

---

def my_callback(session_id: str, user_message: str, conversation_history: list,
                is_first_turn: bool, model: str, platform: str, **kwargs):

---

# Inject context
return {"context": "Recalled memories:\n- User likes Python\n- Working on hermes-agent"}

# Plain string (equivalent)
return "Recalled memories:\n- User likes Python"

# No injection
return None

---

import httpx

MEMORY_API = "https://your-memory-api.example.com"

def recall(session_id, user_message, is_first_turn, **kwargs):
    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
        text = "Recalled context:\n" + "\n".join(f"- {m['text']}" for m in memories)
        return {"context": text}
    except Exception:
        return None

def register(ctx):
    ctx.register_hook("pre_llm_call", recall)

---

POLICY = "Never execute commands that delete files without explicit user confirmation."

def guardrails(**kwargs):
    return {"context": POLICY}

def register(ctx):
    ctx.register_hook("pre_llm_call", guardrails)

---

def my_callback(session_id: str, user_message: str, assistant_response: str,
                conversation_history: list, model: str, platform: str, **kwargs):

---

import httpx

MEMORY_API = "https://your-memory-api.example.com"

def sync_memory(session_id, user_message, assistant_response, **kwargs):
    try:
        httpx.post(f"{MEMORY_API}/store", json={
            "session_id": session_id,
            "user": user_message,
            "assistant": assistant_response,
        }, timeout=5)
    except Exception:
        pass  # best-effort

def register(ctx):
    ctx.register_hook("post_llm_call", sync_memory)

---

import logging
logger = logging.getLogger(__name__)

def log_response_length(session_id, assistant_response, model, **kwargs):
    logger.info("RESPONSE session=%s model=%s chars=%d",
                session_id, model, len(assistant_response or ""))

def register(ctx):
    ctx.register_hook("post_llm_call", log_response_length)

---

def my_callback(session_id: str, model: str, platform: str, **kwargs):

---

_session_caches = {}

def init_session(session_id, model, platform, **kwargs):
    _session_caches[session_id] = {
        "model": model,
        "platform": platform,
        "tool_calls": 0,
        "started": __import__("datetime").datetime.now().isoformat(),
    }

def register(ctx):
    ctx.register_hook("on_session_start", init_session)

---

def my_callback(session_id: str, completed: bool, interrupted: bool,
                model: str, platform: str, **kwargs):

---

_session_caches = {}

def cleanup_session(session_id, completed, interrupted, **kwargs):
    cache = _session_caches.pop(session_id, None)
    if cache:
        # Flush accumulated data to disk or external service
        status = "completed" if completed else ("interrupted" if interrupted else "failed")
        print(f"Session {session_id} ended: {status}, {cache['tool_calls']} tool calls")

def register(ctx):
    ctx.register_hook("on_session_end", cleanup_session)

---

import time, logging
logger = logging.getLogger(__name__)

_start_times = {}

def on_start(session_id, **kwargs):
    _start_times[session_id] = time.time()

def on_end(session_id, completed, interrupted, **kwargs):
    start = _start_times.pop(session_id, None)
    if start:
        duration = time.time() - start
        logger.info("SESSION_DURATION session=%s seconds=%.1f completed=%s interrupted=%s",
                     session_id, duration, completed, interrupted)

def register(ctx):
    ctx.register_hook("on_session_start", on_start)
    ctx.register_hook("on_session_end", on_end)

---

def my_callback(session_id: str | None, platform: str, **kwargs):

---

def my_callback(session_id: str, platform: str, **kwargs):

---

def my_callback(parent_session_id: str, child_role: str | None,
                child_summary: str | None, child_status: str,
                duration_ms: int, **kwargs):

---

import logging
logger = logging.getLogger(__name__)

def log_subagent(parent_session_id, child_role, child_status, duration_ms, **kwargs):
    logger.info(
        "SUBAGENT parent=%s role=%s status=%s duration_ms=%d",
        parent_session_id, child_role, child_status, duration_ms,
    )

def register(ctx):
    ctx.register_hook("subagent_stop", log_subagent)

---

hooks:
  <event_name>:                  # Must be in VALID_HOOKS
    - matcher: "<regex>"         # Optional; used for pre/post_tool_call only
      command: "<shell command>" # Required; runs via shlex.split, shell=False
      timeout: <seconds>         # Optional; default 60, capped at 300

hooks_auto_accept: false         # See "Consent model" below

---

{
  "hook_event_name": "pre_tool_call",
  "tool_name":       "terminal",
  "tool_input":      {"command": "rm -rf /"},
  "session_id":      "sess_abc123",
  "cwd":             "/home/user/project",
  "extra":           {"task_id": "...", "tool_call_id": "..."}
}

---

// Block a pre_tool_call (both shapes accepted; normalised internally):
{"decision": "block", "reason":  "Forbidden: rm -rf"}   // Claude-Code style
{"action":   "block", "message": "Forbidden: rm -rf"}   // Hermes-canonical

// Inject context for pre_llm_call:
{"context": "Today is Friday, 2026-04-17"}

// Silent no-op - output ว่างหรือไม่ตรงกับรูปแบบใดๆ ก็ใช้ได้:

---

# ~/.hermes/config.yaml
hooks:
  post_tool_call:
    - matcher: "write_file|patch"
      command: "~/.hermes/agent-hooks/auto-format.sh"

---

#!/usr/bin/env bash
# ~/.hermes/agent-hooks/auto-format.sh
payload="$(cat -)"
path=$(echo "$payload" | jq -r '.tool_input.path // empty')
[[ "$path" == *.py ]] && command -v black >/dev/null && black "$path" 2>/dev/null
printf '{}\n'

---

hooks:
  pre_tool_call:
    - matcher: "terminal"
      command: "~/.hermes/agent-hooks/block-rm-rf.sh"
      timeout: 5

---

#!/usr/bin/env bash
# ~/.hermes/agent-hooks/block-rm-rf.sh
payload="$(cat -)"
cmd=$(echo "$payload" | jq -r '.tool_input.command // empty')
if echo "$cmd" | grep -qE 'rm[[:space:]]+-rf?[[:space:]]+/'; then
  printf '{"decision": "block", "reason": "blocked: rm -rf / is not permitted"}\n'
else
  printf '{}\n'
fi

---

hooks:
  pre_llm_call:
    - command: "~/.hermes/agent-hooks/inject-cwd-context.sh"

---

#!/usr/bin/env bash
# ~/.hermes/agent-hooks/inject-cwd-context.sh
cat - >/dev/null   # discard stdin payload
if status=$(git status --porcelain 2>/dev/null) && [[ -n "$status" ]]; then
  jq --null-input --arg s "$status" \
     '{context: ("Uncommitted changes in cwd:\n" + $s)}'
else
  printf '{}\n'
fi

---

hooks:
  subagent_stop:
    - command: "~/.hermes/agent-hooks/log-orchestration.sh"

---

#!/usr/bin/env bash
# ~/.hermes/agent-hooks/log-orchestration.sh
log=~/.hermes/logs/orchestration.log
jq -c '{ts: now, parent: .session_id, extra: .extra}' < /dev/stdin >> "$log"
printf '{}\n'

---

hermes memory setup    # select "honcho" from the provider list

---

# ~/.hermes/config.yaml
memory:
  provider: honcho

---

echo "HONCHO_API_KEY=*** >> ~/.hermes/.env

---

"observation": {
  "user": { "observeMe": true,  "observeOthers": true },
  "ai":   { "observeMe": true,  "observeOthers": false }
}

---

hermes honcho status          # Connection status, config, and key settings
hermes honcho setup           # Interactive setup wizard
hermes honcho strategy        # Show or set session strategy
hermes honcho peer            # Update peer names for multi-agent setups
hermes honcho mode            # Show or set recall mode
hermes honcho tokens          # Show or set context token budget
hermes honcho identity        # Show Honcho peer identity
hermes honcho sync            # Sync host blocks for all profiles
hermes honcho enable          # Enable Honcho
hermes honcho disable         # Disable Honcho

---

hermes tools

---

Model                          Speed    Strengths                    Price
  fal-ai/flux-2/klein/9b         <1s      Fast, crisp text             $0.006/MP   ← currently in use
  fal-ai/flux-2-pro              ~6s      Studio photorealism          $0.03/MP
  fal-ai/z-image/turbo           ~2s      Bilingual EN/CN, 6B          $0.005/MP
  ...

---

image_gen:
  model: fal-ai/flux-2/klein/9b
  use_gateway: false            # true if using Nous Subscription

---

Generate an image of a serene mountain landscape with cherry blossoms

---

Create a square portrait of a wise old owl - use the typography model

---

Make me a futuristic cityscape, landscape orientation

---

export IMAGE_TOOLS_DEBUG=true
RAW_BUFFERClick to expand / collapse

📄 user-guide/features/hooks.md


sidebar_position: 6 title: "Event Hooks" description: "Run custom code at key lifecycle points - log activity, send alerts, post to webhooks"

Event Hooks

Hermes มีระบบ hook สามระบบที่สามารถรันโค้ดที่กำหนดเองได้ ณ จุดสำคัญในวงจรการทำงาน:

SystemRegistered viaRuns inUse case
Gateway hooksHOOK.yaml + handler.py in ~/.hermes/hooks/Gateway onlyLogging, alerts, webhooks
Plugin hooksctx.register_hook() in a pluginCLI + GatewayTool interception, metrics, guardrails
Shell hookshooks: block in ~/.hermes/config.yaml pointing at shell scriptsCLI + GatewayDrop-in scripts for blocking, auto-formatting, context injection

ทั้งสามระบบนี้เป็นแบบ non-blocking - หากมีข้อผิดพลาดใน hook ใดๆ ระบบจะจับข้อผิดพลาดและบันทึกไว้ โดยจะไม่ทำให้ agent ล่ม

Gateway Event Hooks

Gateway hooks จะทำงานโดยอัตโนมัติระหว่างการทำงานของ gateway (Telegram, Discord, Slack, WhatsApp) โดยไม่บล็อก pipeline หลักของ agent

การสร้าง Hook

hook แต่ละตัวคือ directory ภายใต้ ~/.hermes/hooks/ ซึ่งประกอบด้วยสองไฟล์:

~/.hermes/hooks/
└── my-hook/
    ├── HOOK.yaml      # Declares which events to listen for
    └── handler.py     # Python handler function

HOOK.yaml

name: my-hook
description: Log all agent activity to a file
events:
  - agent:start
  - agent:end
  - agent:step

รายการ events จะกำหนดว่าเหตุการณ์ใดบ้างที่จะกระตุ้นให้ handler ของคุณทำงาน คุณสามารถสมัครรับการแจ้งเตือนสำหรับเหตุการณ์ใดๆ ก็ได้ รวมถึง wildcard เช่น command:*

handler.py

import json
from datetime import datetime
from pathlib import Path

LOG_FILE = Path.home() / ".hermes" / "hooks" / "my-hook" / "activity.log"

async def handle(event_type: str, context: dict):
    """Called for each subscribed event. Must be named 'handle'."""
    entry = {
        "timestamp": datetime.now().isoformat(),
        "event": event_type,
        **context,
    }
    with open(LOG_FILE, "a") as f:
        f.write(json.dumps(entry) + "\n")

กฎของ Handler:

  • ต้องชื่อว่า handle
  • รับค่า event_type (string) และ context (dict)
  • สามารถเป็น async def หรือ def ธรรมดา - ทั้งสองแบบใช้ได้
  • ข้อผิดพลาดจะถูกจับและบันทึกไว้ โดยจะไม่ทำให้ agent ล่ม

Available Events

EventWhen it firesContext keys
gateway:startupGateway process startsplatforms (list of active platform names)
session:startNew messaging session createdplatform, user_id, session_id, session_key
session:endSession ended (before reset)platform, user_id, session_key
session:resetUser ran /new or /resetplatform, user_id, session_key
agent:startAgent begins processing a messageplatform, user_id, session_id, message
agent:stepEach iteration of the tool-calling loopplatform, user_id, session_id, iteration, tool_names
agent:endAgent finishes processingplatform, user_id, session_id, message, response
command:*Any slash command executedplatform, user_id, command, args

Wildcard Matching

Handlers ที่ลงทะเบียนสำหรับ command:* จะทำงานสำหรับทุกเหตุการณ์ command: (เช่น command:model, command:reset เป็นต้น) คุณสามารถตรวจสอบ slash command ทั้งหมดได้ด้วยการสมัครรับการแจ้งเตือนเพียงครั้งเดียว

Examples

Boot Checklist (BOOT.md) - Built-in

Gateway มาพร้อมกับ hook boot-md ที่ตรวจสอบไฟล์ ~/.hermes/BOOT.md ทุกครั้งที่เริ่มต้นทำงาน หากไฟล์มีอยู่ agent จะรันคำสั่งตามที่ระบุใน background session ไม่ต้องติดตั้งอะไรเพิ่มเติม - เพียงแค่สร้างไฟล์เท่านั้น

สร้าง ~/.hermes/BOOT.md:

# Startup Checklist

1. Check if any cron jobs failed overnight - run `hermes cron list`
2. Send a message to Discord #general saying "Gateway restarted, all systems go"
3. Check if /opt/app/deploy.log has any errors from the last 24 hours

agent จะรันคำสั่งเหล่านี้ใน background thread เพื่อไม่ให้บล็อกการเริ่มต้นทำงานของ gateway หากไม่มีอะไรต้องสนใจ agent จะตอบกลับด้วย [SILENT] และไม่มีการส่งข้อความใดๆ

:::tip ไม่มี BOOT.md? hook จะข้ามไปอย่างเงียบๆ - ไม่มี overhead สร้างไฟล์เมื่อใดก็ตามที่คุณต้องการระบบอัตโนมัติในการเริ่มต้นทำงาน และลบออกเมื่อไม่ต้องการ :::

Telegram Alert on Long Tasks

ส่งข้อความถึงตัวคุณเองเมื่อ agent ใช้ขั้นตอนเกิน 10 ขั้นตอน:

# ~/.hermes/hooks/long-task-alert/HOOK.yaml
name: long-task-alert
description: Alert when agent is taking many steps
events:
  - agent:step
# ~/.hermes/hooks/long-task-alert/handler.py
import os
import httpx

THRESHOLD = 10
BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
CHAT_ID = os.getenv("TELEGRAM_HOME_CHANNEL")

async def handle(event_type: str, context: dict):
    iteration = context.get("iteration", 0)
    if iteration == THRESHOLD and BOT_TOKEN and CHAT_ID:
        tools = ", ".join(context.get("tool_names", []))
        text = f"⚠️ Agent has been running for {iteration} steps. Last tools: {tools}"
        async with httpx.AsyncClient() as client:
            await client.post(
                f"https://api.telegram.org/bot{BOT_TOKEN}/sendMessage",
                json={"chat_id": CHAT_ID, "text": text},
            )

Command Usage Logger

ติดตามว่ามีการใช้ slash command ใดบ้าง:

# ~/.hermes/hooks/command-logger/HOOK.yaml
name: command-logger
description: Log slash command usage
events:
  - command:*
# ~/.hermes/hooks/command-logger/handler.py
import json
from datetime import datetime
from pathlib import Path

LOG = Path.home() / ".hermes" / "logs" / "command_usage.jsonl"

def handle(event_type: str, context: dict):
    LOG.parent.mkdir(parents=True, exist_ok=True)
    entry = {
        "ts": datetime.now().isoformat(),
        "command": context.get("command"),
        "args": context.get("args"),
        "platform": context.get("platform"),
        "user": context.get("user_id"),
    }
    with open(LOG, "a") as f:
        f.write(json.dumps(entry) + "\n")

Session Start Webhook

POST ไปยัง external service เมื่อมี session ใหม่:

# ~/.hermes/hooks/session-webhook/HOOK.yaml
name: session-webhook
description: Notify external service on new sessions
events:
  - session:start
  - session:reset
# ~/.hermes/hooks/session-webhook/handler.py
import httpx

WEBHOOK_URL = "https://your-service.example.com/hermes-events"

async def handle(event_type: str, context: dict):
    async with httpx.AsyncClient() as client:
        await client.post(WEBHOOK_URL, json={
            "event": event_type,
            **context,
        }, timeout=5)

How It Works

  1. เมื่อ gateway เริ่มทำงาน HookRegistry.discover_and_load() จะสแกน ~/.hermes/hooks/
  2. subdirectory แต่ละอันที่มี HOOK.yaml + handler.py จะถูกโหลดแบบ dynamic
  3. Handlers จะถูกลงทะเบียนสำหรับ events ที่ประกาศไว้
  4. ณ จุดวงจรชีวิตแต่ละจุด hooks.emit() จะเรียกใช้ handlers ทั้งหมดที่ตรงกัน
  5. ข้อผิดพลาดใน handler ใดๆ จะถูกจับและบันทึกไว้ - hook ที่เสียจะไม่ทำให้ agent ล่ม

:::info Gateway hooks จะทำงานเฉพาะใน gateway (Telegram, Discord, Slack, WhatsApp) เท่านั้น CLI จะไม่โหลด gateway hooks สำหรับ hook ที่ทำงานได้ทุกที่ ให้ใช้ plugin hooks :::

Plugin Hooks

Plugins สามารถลงทะเบียน hooks ที่ทำงานได้ทั้งใน session CLI และ gateway โดยการลงทะเบียนผ่าน ctx.register_hook() ในฟังก์ชัน register() ของ plugin ของคุณ

def register(ctx):
    ctx.register_hook("pre_tool_call", my_tool_observer)
    ctx.register_hook("post_tool_call", my_tool_logger)
    ctx.register_hook("pre_llm_call", my_memory_callback)
    ctx.register_hook("post_llm_call", my_sync_callback)
    ctx.register_hook("on_session_start", my_init_callback)
    ctx.register_hook("on_session_end", my_cleanup_callback)

กฎทั่วไปสำหรับทุก hooks:

  • Callbacks จะรับ keyword arguments เสมอ ควรรับค่า **kwargs เพื่อความเข้ากันได้ในอนาคต - พารามิเตอร์ใหม่ๆ อาจถูกเพิ่มเข้ามาในเวอร์ชันอนาคตโดยไม่ทำให้ plugin ของคุณพัง
  • หาก callback ล้มเหลว จะถูกบันทึกและข้ามไป hooks และ agent อื่นๆ จะทำงานตามปกติ plugin ที่ทำงานผิดพลาดจะไม่สามารถทำให้ agent พังได้
  • ค่าที่ส่งกลับของสอง hooks มีผลต่อพฤติกรรม: pre_tool_call สามารถ บล็อก tool ได้ และ pre_llm_call สามารถ แทรก context เข้าไปในการเรียก LLM ได้ hooks อื่นๆ ทั้งหมดเป็น observer แบบ fire-and-forget

Quick reference

HookFires whenReturns
pre_tool_callBefore any tool executes{"action": "block", "message": str} to veto the call
post_tool_callAfter any tool returnsignored
pre_llm_callOnce per turn, before the tool-calling loop{"context": str} to prepend context to the user message
post_llm_callOnce per turn, after the tool-calling loopignored
on_session_startNew session created (first turn only)ignored
on_session_endSession endsignored
on_session_finalizeCLI/gateway tears down an active session (flush, save, stats)ignored
on_session_resetGateway swaps in a fresh session key (e.g. /new, /reset)ignored
subagent_stopA delegate_task child has exitedignored

pre_tool_call

ทำงาน ทันทีก่อน การเรียกใช้ tool ทุกครั้ง - ไม่ว่าจะเป็น tool ที่ built-in หรือ tool ของ plugin

Callback signature:

def my_callback(tool_name: str, args: dict, task_id: str, **kwargs):
ParameterTypeDescription
tool_namestrชื่อของ tool ที่กำลังจะถูกเรียกใช้ (เช่น "terminal", "web_search", "read_file")
argsdictarguments ที่ model ส่งไปยัง tool
task_idstrตัวระบุ session/task. เป็น empty string หากไม่ได้ตั้งค่า

Fires: ใน model_tools.py, ภายใน handle_function_call(), ก่อนที่ handler ของ tool จะทำงาน ทำงานครั้งเดียวต่อการเรียก tool - หาก model เรียกใช้ 3 tools แบบขนาน hook นี้จะทำงาน 3 ครั้ง

Return value - veto the call:

return {"action": "block", "message": "Reason the tool call was blocked"}

agent จะหยุดการทำงานของ tool ด้วย message ที่ถูกส่งกลับเป็น error ไปยัง model การกำหนด block ที่ตรงกันครั้งแรกจะเป็นผู้ชนะ (plugin ของ Python ที่ลงทะเบียนก่อน, ตามด้วย shell hooks) ค่า return อื่นๆ จะถูกละเลย ดังนั้น observer-only callbacks ที่มีอยู่จะยังคงทำงานโดยไม่มีการเปลี่ยนแปลง

Use cases: Logging, audit trails, tool call counters, blocking dangerous operations, rate limiting, per-user policy enforcement.

Example - tool call audit log:

import json, logging
from datetime import datetime

logger = logging.getLogger(__name__)

def audit_tool_call(tool_name, args, task_id, **kwargs):
    logger.info("TOOL_CALL session=%s tool=%s args=%s",
                task_id, tool_name, json.dumps(args)[:200])

def register(ctx):
    ctx.register_hook("pre_tool_call", audit_tool_call)

Example - warn on dangerous tools:

DANGEROUS = {"terminal", "write_file", "patch"}

def warn_dangerous(tool_name, **kwargs):
    if tool_name in DANGEROUS:
        print(f"⚠ Executing potentially dangerous tool: {tool_name}")

def register(ctx):
    ctx.register_hook("pre_tool_call", warn_dangerous)

post_tool_call

ทำงาน ทันทีหลังจาก tool ทุกตัวทำงานเสร็จสิ้น

Callback signature:

def my_callback(tool_name: str, args: dict, result: str, task_id: str, **kwargs):
ParameterTypeDescription
tool_namestrชื่อของ tool ที่เพิ่งทำงานเสร็จ
argsdictarguments ที่ model ส่งไปยัง tool
resultstrค่าที่ return ของ tool (เป็น JSON string เสมอ)
task_idstrตัวระบุ session/task. เป็น empty string หากไม่ได้ตั้งค่า

Fires: ใน model_tools.py, ภายใน handle_function_call(), หลังจากที่ handler ของ tool ทำงานเสร็จ ทำงานครั้งเดียวต่อการเรียก tool จะไม่ทำงานหาก tool เกิด unhandled exception (ข้อผิดพลาดจะถูกจับและส่งกลับเป็น error JSON string แทน และ post_tool_call จะทำงานโดยใช้ error string นั้นเป็น result)

Return value: ถูกละเลย

Use cases: Logging tool results, metrics collection, tracking tool success/failure rates, sending notifications when specific tools complete.

Example - track tool usage metrics:

from collections import Counter
import json

_tool_counts = Counter()
_error_counts = Counter()

def track_metrics(tool_name, result, **kwargs):
    _tool_counts[tool_name] += 1
    try:
        parsed = json.loads(result)
        if "error" in parsed:
            _error_counts[tool_name] += 1
    except (json.JSONDecodeError, TypeError):
        pass

def register(ctx):
    ctx.register_hook("post_tool_call", track_metrics)

pre_llm_call

ทำงาน ครั้งเดียวต่อ turn ก่อนที่ tool-calling loop จะเริ่มต้น นี่คือ hook เดียวที่ค่า return ถูกนำไปใช้ - มันสามารถแทรก context เข้าไปใน user message ของ turn ปัจจุบันได้

Callback signature:

def my_callback(session_id: str, user_message: str, conversation_history: list,
                is_first_turn: bool, model: str, platform: str, **kwargs):
ParameterTypeDescription
session_idstrตัวระบุเฉพาะสำหรับ session ปัจจุบัน
user_messagestrข้อความต้นฉบับของผู้ใช้สำหรับ turn นี้ (ก่อนการแทรก skill ใดๆ)
conversation_historylistสำเนาของรายการข้อความทั้งหมด (รูปแบบ OpenAI: [{"role": "user", "content": "..."}])
is_first_turnboolTrue หากนี่คือ turn แรกของ session ใหม่, False ใน turn ถัดไป
modelstrตัวระบุ model (เช่น "anthropic/claude-sonnet-4.6")
platformstrสถานที่ที่ session กำลังทำงาน: "cli", "telegram", "discord", เป็นต้น

Fires: ใน run_agent.py, ภายใน run_conversation(), หลังจาก context compression แต่ก่อน main while loop ทำงาน ทำงานครั้งเดียวต่อการเรียก run_conversation() (นั่นคือครั้งเดียวต่อ user turn) ไม่ใช่ครั้งเดียวต่อ API call ภายใน tool loop

Return value: หาก callback ส่งคืน dict ที่มี key "context" หรือ string ธรรมดาที่ไม่ว่าง ข้อความนั้นจะถูกเพิ่มเข้าไปใน user message ของ turn ปัจจุบัน ให้ return None หากไม่มีการแทรก context

# Inject context
return {"context": "Recalled memories:\n- User likes Python\n- Working on hermes-agent"}

# Plain string (equivalent)
return "Recalled memories:\n- User likes Python"

# No injection
return None

Where context is injected: เสมอที่ user message ไม่ใช่ system prompt สิ่งนี้ช่วยรักษา prompt cache - system prompt จะคงที่ตลอด turn ดังนั้น token ที่ถูก cache จึงถูกนำกลับมาใช้ Plugins จะช่วย contribute context ควบคู่ไปกับ input ของผู้ใช้

Context ที่ถูกแทรกทั้งหมดเป็นแบบ ephemeral - เพิ่มเฉพาะเวลาเรียก API เท่านั้น user message ต้นฉบับใน conversation history จะไม่ถูกแก้ไข และไม่มีอะไรถูกบันทึกใน session database

เมื่อ multiple plugins ส่งคืน context ผลลัพธ์ของพวกมันจะถูกรวมด้วย double newlines ตามลำดับการค้นพบ plugin (ตามตัวอักษรของชื่อ directory)

Use cases: Memory recall, RAG context injection, guardrails, per-turn analytics.

Example - memory recall:

import httpx

MEMORY_API = "https://your-memory-api.example.com"

def recall(session_id, user_message, is_first_turn, **kwargs):
    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
        text = "Recalled context:\n" + "\n".join(f"- {m['text']}" for m in memories)
        return {"context": text}
    except Exception:
        return None

def register(ctx):
    ctx.register_hook("pre_llm_call", recall)

Example - guardrails:

POLICY = "Never execute commands that delete files without explicit user confirmation."

def guardrails(**kwargs):
    return {"context": POLICY}

def register(ctx):
    ctx.register_hook("pre_llm_call", guardrails)

post_llm_call

ทำงาน ครั้งเดียวต่อ turn หลังจากที่ tool-calling loop เสร็จสมบูรณ์ และ agent ได้สร้าง response สุดท้าย จะทำงานเฉพาะใน turn ที่ สำเร็จ เท่านั้น - จะไม่ทำงานหาก turn ถูกขัดจังหวะ

Callback signature:

def my_callback(session_id: str, user_message: str, assistant_response: str,
                conversation_history: list, model: str, platform: str, **kwargs):
ParameterTypeDescription
session_idstrตัวระบุเฉพาะสำหรับ session ปัจจุบัน
user_messagestrข้อความต้นฉบับของผู้ใช้สำหรับ turn นี้
assistant_responsestrข้อความตอบกลับสุดท้ายของ agent สำหรับ turn นี้
conversation_historylistสำเนาของรายการข้อความทั้งหมดหลัง turn เสร็จสมบูรณ์
modelstrตัวระบุ model
platformstrสถานที่ที่ session กำลังทำงาน

Fires: ใน run_agent.py, ภายใน run_conversation(), หลังจากที่ tool loop ออกด้วย response สุดท้าย ถูกป้องกันด้วย if final_response and not interrupted - ดังนั้นจะไม่ทำงานเมื่อผู้ใช้ขัดจังหวะกลาง turn หรือ agent ถึงขีดจำกัด iteration โดยที่ไม่ได้สร้าง response

Return value: ถูกละเลย

Use cases: Syncing conversation data to an external memory system, computing response quality metrics, logging turn summaries, triggering follow-up actions.

Example - sync to external memory:

import httpx

MEMORY_API = "https://your-memory-api.example.com"

def sync_memory(session_id, user_message, assistant_response, **kwargs):
    try:
        httpx.post(f"{MEMORY_API}/store", json={
            "session_id": session_id,
            "user": user_message,
            "assistant": assistant_response,
        }, timeout=5)
    except Exception:
        pass  # best-effort

def register(ctx):
    ctx.register_hook("post_llm_call", sync_memory)

Example - track response lengths:

import logging
logger = logging.getLogger(__name__)

def log_response_length(session_id, assistant_response, model, **kwargs):
    logger.info("RESPONSE session=%s model=%s chars=%d",
                session_id, model, len(assistant_response or ""))

def register(ctx):
    ctx.register_hook("post_llm_call", log_response_length)

on_session_start

ทำงาน ครั้งเดียว เมื่อมีการสร้าง session ใหม่เอี่ยม จะไม่ทำงานเมื่อ session ดำเนินการต่อ (เมื่อผู้ใช้ส่งข้อความที่สองใน session ที่มีอยู่)

Callback signature:

def my_callback(session_id: str, model: str, platform: str, **kwargs):
ParameterTypeDescription
session_idstrตัวระบุเฉพาะสำหรับ session ใหม่
modelstrตัวระบุ model
platformstrสถานที่ที่ session กำลังทำงาน

Fires: ใน run_agent.py, ภายใน run_conversation(), ในระหว่าง turn แรกของ session ใหม่ - โดยเฉพาะหลังจากที่ system prompt ถูกสร้างแล้วแต่ก่อนที่ tool loop จะเริ่ม การตรวจสอบคือ if not conversation_history (ไม่มีข้อความก่อนหน้า = session ใหม่)

Return value: ถูกละเลย

Use cases: Initializing session-scoped state, warming caches, registering the session with an external service, logging session starts.

Example - initialize a session cache:

_session_caches = {}

def init_session(session_id, model, platform, **kwargs):
    _session_caches[session_id] = {
        "model": model,
        "platform": platform,
        "tool_calls": 0,
        "started": __import__("datetime").datetime.now().isoformat(),
    }

def register(ctx):
    ctx.register_hook("on_session_start", init_session)

on_session_end

ทำงานที่ ท้ายสุด ของทุกการเรียก run_conversation(), ไม่ว่าผลลัพธ์จะเป็นอย่างไร นอกจากนี้ยังทำงานจาก exit handler ของ CLI หาก agent อยู่ระหว่าง turn เมื่อผู้ใช้ปิดโปรแกรม

Callback signature:

def my_callback(session_id: str, completed: bool, interrupted: bool,
                model: str, platform: str, **kwargs):
ParameterTypeDescription
session_idstrตัวระบุเฉพาะสำหรับ session
completedboolTrue หาก agent สร้าง response สุดท้ายได้, False หากไม่เช่นนั้น
interruptedboolTrue หาก turn ถูกขัดจังหวะ (ผู้ใช้ส่งข้อความใหม่, /stop, หรือปิดโปรแกรม)
modelstrตัวระบุ model
platformstrสถานที่ที่ session กำลังทำงาน

Fires: ในสองที่:

  1. run_agent.py - ที่ท้ายของทุกการเรียก run_conversation(), หลังจากทำ cleanup ทั้งหมด จะทำงานเสมอ แม้ว่า turn จะเกิดข้อผิดพลาดก็ตาม
  2. cli.py - ใน atexit handler ของ CLI, แต่ เฉพาะ เมื่อ agent อยู่ระหว่าง turn (_agent_running=True) เมื่อเกิดการ exit สิ่งนี้จะจับ Ctrl+C และ /exit ระหว่างการประมวลผล ในกรณีนี้ completed=False และ interrupted=True

Return value: ถูกละเลย

Use cases: Flushing buffers, closing connections, persisting session state, logging session duration, cleanup of resources initialized in on_session_start.

Example - flush and cleanup:

_session_caches = {}

def cleanup_session(session_id, completed, interrupted, **kwargs):
    cache = _session_caches.pop(session_id, None)
    if cache:
        # Flush accumulated data to disk or external service
        status = "completed" if completed else ("interrupted" if interrupted else "failed")
        print(f"Session {session_id} ended: {status}, {cache['tool_calls']} tool calls")

def register(ctx):
    ctx.register_hook("on_session_end", cleanup_session)

Example - session duration tracking:

import time, logging
logger = logging.getLogger(__name__)

_start_times = {}

def on_start(session_id, **kwargs):
    _start_times[session_id] = time.time()

def on_end(session_id, completed, interrupted, **kwargs):
    start = _start_times.pop(session_id, None)
    if start:
        duration = time.time() - start
        logger.info("SESSION_DURATION session=%s seconds=%.1f completed=%s interrupted=%s",
                     session_id, duration, completed, interrupted)

def register(ctx):
    ctx.register_hook("on_session_start", on_start)
    ctx.register_hook("on_session_end", on_end)

on_session_finalize

ทำงานเมื่อ CLI หรือ gateway tears down session ที่กำลังทำงานอยู่ - ตัวอย่างเช่น เมื่อผู้ใช้รัน /new, gateway GC'd session ที่ไม่ได้ใช้งาน, หรือ CLI ปิดตัวลงพร้อม agent ที่กำลังทำงาน นี่คือโอกาสสุดท้ายในการ flush state ที่ผูกกับ session ที่กำลังจะออกไปก่อนที่ identity ของมันจะหายไป

Callback signature:

def my_callback(session_id: str | None, platform: str, **kwargs):
ParameterTypeDescription
session_idstr or Nonesession ID ที่กำลังจะออกไป อาจเป็น None หากไม่มี session ที่ใช้งานอยู่
platformstr"cli" หรือชื่อ platform ของ messaging ("telegram", "discord", เป็นต้น)

Fires: ใน cli.py (เมื่อรัน /new / CLI exit) และ gateway/run.py (เมื่อ session ถูก reset หรือ GC'd) มักจะมาคู่กับ on_session_reset ในฝั่ง gateway

Return value: ถูกละเลย

Use cases: Persist final session metrics ก่อนที่ session ID จะถูกทิ้ง, close per-session resources, emit a final telemetry event, drain queued writes.


on_session_reset

ทำงานเมื่อ gateway สลับ key session ใหม่ สำหรับ chat ที่กำลังใช้งานอยู่ - ผู้ใช้เรียกใช้ /new, /reset, /clear, หรือ adapter เลือก session ใหม่หลังจากช่วงเวลาที่ไม่ได้ใช้งาน สิ่งนี้ช่วยให้ plugin สามารถตอบสนองต่อข้อเท็จจริงที่ว่าสถานะการสนทนาถูกล้างออกไปแล้ว โดยไม่ต้องรอ on_session_start ครั้งถัดไป

Callback signature:

def my_callback(session_id: str, platform: str, **kwargs):
ParameterTypeDescription
session_idstrID ของ session ใหม่ (ถูกหมุนเป็นค่าใหม่แล้ว)
platformstrชื่อ messaging platform

Fires: ใน gateway/run.py, ทันทีหลังจากที่ key session ใหม่ถูกจัดสรร แต่ก่อนที่ข้อความขาเข้าถัดไปจะถูกประมวลผล ในฝั่ง gateway ลำดับคือ: on_session_finalize(old_id) → swap → on_session_reset(new_id)on_session_start(new_id) ใน turn ขาเข้าแรก

Return value: ถูกละเลย

Use cases: Reset per-session caches ที่ key ด้วย session_id, emit "session rotated" analytics, prime a fresh state bucket.


ดู Build a Plugin guide สำหรับคำแนะนำฉบับเต็ม รวมถึง tool schemas, handlers, และ advanced hook patterns


subagent_stop

ทำงาน ครั้งเดียวต่อ child agent หลังจากที่ delegate_task เสร็จสิ้น ไม่ว่าคุณจะ delegate task เดียวหรือ batch สามตัว hook นี้จะทำงานครั้งเดียวสำหรับแต่ละ child โดยถูก serialised บน parent thread

Callback signature:

def my_callback(parent_session_id: str, child_role: str | None,
                child_summary: str | None, child_status: str,
                duration_ms: int, **kwargs):
ParameterTypeDescription
parent_session_idstrSession ID ของ parent agent ที่ทำการ delegate
child_rolestr | NoneOrchestrator role tag ที่ตั้งค่าบน child (None หากฟีเจอร์ไม่ได้เปิดใช้งาน)
child_summarystr | Noneresponse สุดท้ายที่ child ส่งกลับไปยัง parent
child_statusstr"completed", "failed", "interrupted", หรือ "error"
duration_msintเวลาที่ใช้ในการรัน child, เป็นหน่วยมิลลิวินาที

Fires: ใน tools/delegate_tool.py, หลังจากที่ ThreadPoolExecutor.as_completed() drain child futures ทั้งหมด การทำงานจะถูก marshal ไปยัง parent thread เพื่อให้ผู้เขียน hook ไม่ต้องคำนึงถึงการทำงานของ callback แบบ concurrent

Return value: ถูกละเลย

Use cases: Logging orchestration activity, accumulating child durations for billing, writing post-delegation audit records.

Example - log orchestrator activity:

import logging
logger = logging.getLogger(__name__)

def log_subagent(parent_session_id, child_role, child_status, duration_ms, **kwargs):
    logger.info(
        "SUBAGENT parent=%s role=%s status=%s duration_ms=%d",
        parent_session_id, child_role, child_status, duration_ms,
    )

def register(ctx):
    ctx.register_hook("subagent_stop", log_subagent)

:::info ด้วยการ delegate ที่หนักหน่วง (เช่น orchestrator roles × 5 leaves × nested depth), subagent_stop จะทำงานหลายครั้งต่อ turn ให้รักษา callback ของคุณให้รวดเร็ว และย้ายงานที่ใช้ทรัพยากรสูงไปที่ background queue :::

Shell Hooks

คุณสามารถประกาศ shell-script hooks ในไฟล์ cli-config.yaml ของคุณได้ และ Hermes จะรัน hooks เหล่านั้นเป็น subprocesses ทุกครั้งที่ event plugin-hook ที่เกี่ยวข้องถูกเรียกใช้ ไม่ว่าจะในเซสชัน CLI หรือ gateway ไม่จำเป็นต้องเขียน plugin ด้วย Python

ควรใช้ shell hooks เมื่อคุณต้องการ script แบบ single-file ที่สามารถใช้งานได้ทันที (Bash, Python, หรืออะไรก็ตามที่มี shebang) เพื่อ:

  • บล็อกการเรียกใช้ tool - ปฏิเสธคำสั่ง terminal ที่อันตราย บังคับใช้นโยบายต่อ directory แต่ละตัว หรือกำหนดให้ต้องมีการอนุมัติสำหรับการดำเนินการ write_file / patch ที่ทำลายข้อมูล
  • รันหลังจากเรียกใช้ tool - จัดรูปแบบไฟล์ Python หรือ TypeScript ที่ agent เพิ่งเขียนให้โดยอัตโนมัติ บันทึกการเรียกใช้ API หรือกระตุ้น workflow ของ CI
  • ฉีด context เข้าไปในการเรียก LLM ครั้งถัดไป - เพิ่มผลลัพธ์ของ git status วันในสัปดาห์ปัจจุบัน หรือเอกสารที่ดึงมา ไปยังข้อความของผู้ใช้ (ดูที่ pre_llm_call)
  • สังเกตการณ์ event ของ lifecycle - เขียนบรรทัด log เมื่อ subagent ทำงานเสร็จสิ้น (subagent_stop) หรือเมื่อเซสชันเริ่มต้น (on_session_start)

Shell hooks ถูกลงทะเบียนโดยการเรียกใช้ agent.shell_hooks.register_from_config(cfg) ทั้งที่การเริ่มต้น CLI (hermes_cli/main.py) และที่การเริ่มต้น gateway (gateway/run.py) พวกมันทำงานร่วมกับ Python plugin hooks ได้อย่างเป็นธรรมชาติ - ทั้งคู่จะไหลผ่าน dispatcher ตัวเดียวกัน

Comparison at a glance

DimensionShell hooksPlugin hooksGateway hooks
Declared inhooks: block in ~/.hermes/config.yamlregister() in a plugin.yaml pluginHOOK.yaml + handler.py directory
Lives under~/.hermes/agent-hooks/ (by convention)~/.hermes/plugins/<name>/~/.hermes/hooks/<name>/
LanguageAny (Bash, Python, Go binary, …)Python onlyPython only
Runs inCLI + GatewayCLI + GatewayGateway only
EventsVALID_HOOKS (incl. subagent_stop)VALID_HOOKSGateway lifecycle (gateway:startup, agent:*, command:*)
Can block a tool callYes (pre_tool_call)Yes (pre_tool_call)No
Can inject LLM contextYes (pre_llm_call)Yes (pre_llm_call)No
ConsentFirst-use prompt per (event, command) pairImplicit (Python plugin trust)Implicit (dir trust)
Inter-process isolationYes (subprocess)No (in-process)No (in-process)

Configuration schema

hooks:
  <event_name>:                  # Must be in VALID_HOOKS
    - matcher: "<regex>"         # Optional; used for pre/post_tool_call only
      command: "<shell command>" # Required; runs via shlex.split, shell=False
      timeout: <seconds>         # Optional; default 60, capped at 300

hooks_auto_accept: false         # See "Consent model" below

ชื่อ event ต้องเป็นหนึ่งใน plugin hook events; การพิมพ์ผิดจะแสดงคำเตือน "Did you mean X?" และจะถูกข้ามไป คีย์ที่ไม่รู้จักภายในรายการเดียวจะถูกละเลย; การขาด command จะถูกข้ามไปพร้อมคำเตือน timeout > 300 จะถูกจำกัดค่าพร้อมคำเตือน

JSON wire protocol

ทุกครั้งที่ event ถูกเรียกใช้ Hermes จะสร้าง subprocess สำหรับ hook ที่ตรงกันทุกตัว (หาก matcher อนุญาต) จะส่ง JSON payload ไปยัง stdin และอ่าน stdout กลับมาเป็น JSON

stdin - payload ที่ script ได้รับ:

{
  "hook_event_name": "pre_tool_call",
  "tool_name":       "terminal",
  "tool_input":      {"command": "rm -rf /"},
  "session_id":      "sess_abc123",
  "cwd":             "/home/user/project",
  "extra":           {"task_id": "...", "tool_call_id": "..."}
}

tool_name และ tool_input จะเป็น null สำหรับ event ที่ไม่ใช่ tool (pre_llm_call, subagent_stop, lifecycle ของ session) dict extra บรรจุ kwargs เฉพาะ event ทั้งหมด (user_message, conversation_history, child_role, duration_ms, …) ค่าที่ไม่สามารถ serialize ได้จะถูกแปลงเป็น string แทนที่จะถูกละเว้น

stdout - การตอบกลับทางเลือก:

// Block a pre_tool_call (both shapes accepted; normalised internally):
{"decision": "block", "reason":  "Forbidden: rm -rf"}   // Claude-Code style
{"action":   "block", "message": "Forbidden: rm -rf"}   // Hermes-canonical

// Inject context for pre_llm_call:
{"context": "Today is Friday, 2026-04-17"}

// Silent no-op - output ว่างหรือไม่ตรงกับรูปแบบใดๆ ก็ใช้ได้:

JSON ที่ผิดรูปแบบ, exit code ไม่เป็นศูนย์, และ timeout จะบันทึกคำเตือนแต่จะไม่ยกเลิก agent loop

Worked examples

1. Auto-format Python files after every write

# ~/.hermes/config.yaml
hooks:
  post_tool_call:
    - matcher: "write_file|patch"
      command: "~/.hermes/agent-hooks/auto-format.sh"
#!/usr/bin/env bash
# ~/.hermes/agent-hooks/auto-format.sh
payload="$(cat -)"
path=$(echo "$payload" | jq -r '.tool_input.path // empty')
[[ "$path" == *.py ]] && command -v black >/dev/null && black "$path" 2>/dev/null
printf '{}\n'

มุมมองใน context ของ agent ต่อไฟล์จะ ไม่ ถูกอ่านใหม่โดยอัตโนมัติ - การจัดรูปแบบใหม่จะส่งผลต่อไฟล์บน disk เท่านั้น การเรียกใช้ read_file ครั้งต่อไปจะดึงเวอร์ชันที่จัดรูปแบบแล้ว

2. Block destructive terminal commands

hooks:
  pre_tool_call:
    - matcher: "terminal"
      command: "~/.hermes/agent-hooks/block-rm-rf.sh"
      timeout: 5
#!/usr/bin/env bash
# ~/.hermes/agent-hooks/block-rm-rf.sh
payload="$(cat -)"
cmd=$(echo "$payload" | jq -r '.tool_input.command // empty')
if echo "$cmd" | grep -qE 'rm[[:space:]]+-rf?[[:space:]]+/'; then
  printf '{"decision": "block", "reason": "blocked: rm -rf / is not permitted"}\n'
else
  printf '{}\n'
fi

3. Inject git status into every turn (Claude-Code UserPromptSubmit equivalent)

hooks:
  pre_llm_call:
    - command: "~/.hermes/agent-hooks/inject-cwd-context.sh"
#!/usr/bin/env bash
# ~/.hermes/agent-hooks/inject-cwd-context.sh
cat - >/dev/null   # discard stdin payload
if status=$(git status --porcelain 2>/dev/null) && [[ -n "$status" ]]; then
  jq --null-input --arg s "$status" \
     '{context: ("Uncommitted changes in cwd:\n" + $s)}'
else
  printf '{}\n'
fi

Event UserPromptSubmit ของ Claude Code ถูกออกแบบมาให้ไม่ใช่ event ของ Hermes แยกต่างหาก - pre_llm_call จะทำงานในตำแหน่งเดียวกันและรองรับการฉีด context อยู่แล้ว ใช้มันที่นี่

4. Log every subagent completion

hooks:
  subagent_stop:
    - command: "~/.hermes/agent-hooks/log-orchestration.sh"
#!/usr/bin/env bash
# ~/.hermes/agent-hooks/log-orchestration.sh
log=~/.hermes/logs/orchestration.log
jq -c '{ts: now, parent: .session_id, extra: .extra}' < /dev/stdin >> "$log"
printf '{}\n'

Consent model

คู่ (event, command) ที่ไม่ซ้ำกันแต่ละคู่จะแจ้งให้ผู้ใช้ทราบเพื่อขออนุมัติในครั้งแรกที่ Hermes เห็น จากนั้นจะบันทึกการตัดสินใจไว้ที่ ~/.hermes/shell-hooks-allowlist.json การรันครั้งถัดไป (CLI หรือ gateway) จะข้ามการแจ้งเตือนนี้ไป

มีช่องทางหลีกเลี่ยงสามช่องทางที่ข้ามการแจ้งเตือนแบบ interactive ได้ - เพียงอย่างใดอย่างหนึ่งก็เพียงพอ:

  1. flag --accept-hooks บน CLI (เช่น hermes --accept-hooks chat)
  2. ตัวแปร environment HERMES_ACCEPT_HOOKS=1
  3. hooks_auto_accept: true ใน cli-config.yaml

การรันที่ไม่ใช่ TTY (gateway, cron, CI) ต้องมีอย่างใดอย่างหนึ่งในสามอย่างนี้ - มิฉะนั้น hook ที่เพิ่มเข้ามาใหม่จะยังคงไม่ถูกลงทะเบียนอย่างเงียบๆ และจะบันทึกคำเตือน

การแก้ไข script จะถูกเชื่อถืออย่างเงียบๆ คีย์ allowlist จะอ้างอิงจากสตริงคำสั่งที่แน่นอน ไม่ใช่ hash ของ script ดังนั้นการแก้ไข script บน disk จึงไม่ทำให้ consent เป็นโมฆะ การรัน hermes hooks doctor จะแจ้งเตือน mtime drift เพื่อให้คุณสามารถตรวจพบการแก้ไขและตัดสินใจว่าจะอนุมัติใหม่หรือไม่

The hermes hooks CLI

CommandWhat it does
hermes hooks listแสดง hooks ที่กำหนดค่าไว้พร้อม matcher, timeout, และสถานะ consent
hermes hooks test <event> [--for-tool X] [--payload-file F]เรียกใช้ hook ที่ตรงกันทุกตัวกับ payload จำลองและพิมพ์การตอบกลับที่ parse แล้ว
hermes hooks revoke <command>ลบรายการ allowlist ทุกรายการที่ตรงกับ <command> (มีผลในการรีสตาร์ทครั้งถัดไป)
hermes hooks doctorสำหรับ hook ที่กำหนดค่าไว้ทุกตัว: ตรวจสอบ exec bit, สถานะ allowlist, mtime drift, ความถูกต้องของ JSON output, และเวลาการทำงานโดยประมาณ

Security

Shell hooks ทำงานด้วย สิทธิ์ผู้ใช้ทั้งหมดของคุณ - มีขอบเขตความน่าเชื่อถือเดียวกับการรัน cron entry หรือ shell alias ถือว่าบล็อก hooks: ใน config.yaml เป็นการกำหนดค่าที่มีสิทธิ์:

  • อ้างอิงเฉพาะ script ที่คุณเขียนหรือตรวจสอบอย่างถี่ถ้วนเท่านั้น
  • เก็บ script ไว้ภายใน ~/.hermes/agent-hooks/ เพื่อให้ path ง่ายต่อการตรวจสอบ
  • รัน hermes hooks doctor ซ้ำหลังจากที่คุณดึง config ที่แชร์มา เพื่อตรวจหา hooks ที่เพิ่มเข้ามาใหม่ก่อนที่พวกมันจะลงทะเบียน
  • หาก config.yaml ของคุณถูก version-control ทั่วทีม ให้ตรวจสอบ PRs ที่เปลี่ยนแปลงส่วน hooks: เช่นเดียวกับที่คุณตรวจสอบ config ของ CI

Ordering and precedence

ทั้ง Python plugin hooks และ shell hooks จะไหลผ่าน dispatcher invoke_hook() ตัวเดียวกัน Python plugins จะถูกลงทะเบียนก่อน (discover_and_load()), shell hooks จะถูกลงทะเบียนทีหลัง (register_from_config()) ดังนั้นการตัดสินใจบล็อก pre_tool_call ของ Python จึงมีลำดับความสำคัญในกรณีที่เกิดข้อขัดแย้ง การบล็อกที่ถูกต้องตัวแรกที่ชนะ - aggregator จะส่งคืนทันทีที่ callback ใดๆ สร้าง {"action": "block", "message": str} ที่มีข้อความไม่ว่าง


📄 user-guide/features/honcho.md


sidebar_position: 99 title: "Honcho Memory" description: "AI-native persistent memory via Honcho - dialectic reasoning, multi-agent user modeling, and deep personalization"

Honcho Memory

Honcho คือ memory backend ที่เป็น AI-native ซึ่งเพิ่ม dialectic reasoning และ deep user modeling เข้าไปบนระบบ memory ที่มีอยู่แล้วของ Hermes แทนที่จะใช้การจัดเก็บแบบ key-value ทั่วไป Honcho จะรักษารูปแบบจำลอง (running model) ของผู้ใช้ — ไม่ว่าจะเป็นความชอบ, รูปแบบการสื่อสาร, เป้าหมาย, และรูปแบบพฤติกรรม — โดยการวิเคราะห์บทสนทนาหลังจากที่เกิดขึ้นแล้ว

:::info Honcho is a Memory Provider Plugin Honcho ถูกรวมเข้ากับระบบ Memory Providers คุณสมบัติทั้งหมดด้านล่างสามารถเข้าถึงได้ผ่าน unified memory provider interface :::

What Honcho Adds

CapabilityBuilt-in MemoryHoncho
Cross-session persistence✔ File-based MEMORY.md/USER.md✔ Server-side with API
User profile✔ Manual agent curation✔ Automatic dialectic reasoning
Session summary✔ Session-scoped context injection
Multi-agent isolation✔ Per-peer profile separation
Observation modes✔ Unified or directional observation
Conclusions (derived insights)✔ Server-side reasoning about patterns
Search across history✔ FTS5 session search✔ Semantic search over conclusions

Dialectic reasoning: หลังจากทุก turn ของบทสนทนา (ซึ่งถูกควบคุมโดย dialecticCadence) Honcho จะวิเคราะห์การแลกเปลี่ยนข้อมูลและดึงข้อมูลเชิงลึกเกี่ยวกับความชอบ, นิสัย, และเป้าหมายของผู้ใช้ ข้อมูลเหล่านี้จะสะสมเมื่อเวลาผ่านไป ทำให้ agent มีความเข้าใจที่ลึกซึ้งกว่าสิ่งที่ผู้ใช้ระบุอย่างชัดเจน โดย dialectic รองรับ multi-pass depth (1-3 passes) พร้อมการเลือก prompt อัตโนมัติแบบ cold/warm - cold start queries จะเน้นที่ข้อเท็จจริงทั่วไปของผู้ใช้ ขณะที่ warm queries จะให้ความสำคัญกับ context ใน session นั้นๆ

Session-scoped context: Base context ตอนนี้รวม session summary เข้ามาด้วย นอกเหนือจาก user representation และ peer card สิ่งนี้ช่วยให้ agent รับรู้ว่ามีการพูดคุยอะไรไปแล้วใน session ปัจจุบัน ซึ่งช่วยลดการพูดซ้ำและเพิ่มความต่อเนื่อง

Multi-agent profiles: เมื่อหลาย instance ของ Hermes พูดคุยกับผู้ใช้คนเดียวกัน (เช่น coding assistant และ personal assistant) Honcho จะรักษารูปแบบจำลอง "peer" ที่แยกจากกัน แต่ละ peer จะเห็นเฉพาะ observation และ conclusion ของตัวเองเท่านั้น ป้องกันการปนเปื้อนของ context

Setup

hermes memory setup    # select "honcho" from the provider list

หรือกำหนดค่าด้วยตนเอง:

# ~/.hermes/config.yaml
memory:
  provider: honcho
echo "HONCHO_API_KEY=*** >> ~/.hermes/.env

รับ API key ได้ที่ honcho.dev

Architecture

Two-Layer Context Injection

ทุก turn (ในโหมด hybrid หรือ context) Honcho จะประกอบ context สองชั้นที่ถูกฉีดเข้าไปใน system prompt:

  1. Base context - session summary, user representation, user peer card, AI self-representation, และ AI identity card. จะถูกรีเฟรชเมื่อถึง contextCadence นี่คือชั้น "ผู้ใช้คนนี้คือใคร"
  2. Dialectic supplement - การให้เหตุผลที่สังเคราะห์โดย LLM เกี่ยวกับสถานะและความต้องการปัจจุบันของผู้ใช้ จะถูกรีเฟรชเมื่อถึง dialecticCadence นี่คือชั้น "อะไรที่สำคัญในตอนนี้"

ทั้งสองชั้นจะถูกนำมาต่อกันและถูกตัดให้เหลือตามงบประมาณ contextTokens (หากมีการกำหนด)

Cold/Warm Prompt Selection

dialectic จะเลือกกลยุทธ์ prompt ระหว่างสองแบบโดยอัตโนมัติ:

  • Cold start (ยังไม่มี base context): General query - "ผู้คนคนนี้คือใคร? ความชอบ, เป้าหมาย, และรูปแบบการทำงานของพวกเขาคืออะไร?"
  • Warm session (มี base context): Session-scoped query - "เมื่อพิจารณาจากสิ่งที่ได้พูดคุยกันใน session นี้จนถึงตอนนี้ context ใดเกี่ยวกับผู้ใช้คนนี้ที่เกี่ยวข้องที่สุด?"

สิ่งนี้จะเกิดขึ้นโดยอัตโนมัติโดยอิงจากว่ามีการ populate base context หรือไม่

Three Orthogonal Config Knobs

ค่าใช้จ่ายและความลึกจะถูกควบคุมโดย knobs อิสระสามตัว:

KnobControlsDefault
contextCadenceTurns between context() API calls (base layer refresh)1
dialecticCadenceTurns between peer.chat() LLM calls (dialectic layer refresh)2 (recommended 1–5)
dialecticDepthNumber of .chat() passes per dialectic invocation (1–3)1

สิ่งเหล่านี้เป็น orthogonal - คุณสามารถมีการรีเฟรช context บ่อยครั้งด้วย dialectic ที่ไม่บ่อย หรือทำ multi-pass dialectic ที่ลึกในความถี่ต่ำ ตัวอย่าง: contextCadence: 1, dialecticCadence: 5, dialecticDepth: 2 จะรีเฟรช base context ทุก turn, รัน dialectic ทุก 5 turns, และแต่ละรอบ dialectic จะทำ 2 passes

Dialectic Depth (Multi-Pass)

เมื่อ dialecticDepth > 1 การเรียกใช้ dialectic แต่ละครั้งจะรันหลาย .chat() passes:

  • Pass 0: Cold หรือ warm prompt (ดูด้านบน)
  • Pass 1: Self-audit - ระบุช่องว่างในการประเมินครั้งแรกและสังเคราะห์หลักฐานจาก session ล่าสุด
  • Pass 2: Reconciliation - ตรวจสอบความขัดแย้งระหว่าง passes ก่อนหน้าและสร้างการสังเคราะห์ขั้นสุดท้าย

แต่ละ pass จะใช้ระดับการให้เหตุผลแบบ proportional (pass แรกๆ จะเบากว่า, pass หลักใช้ base level) สามารถ override per-pass levels ด้วย dialecticDepthLevels - เช่น ["minimal", "medium", "high"] สำหรับ depth-3 run

Passes จะยกเลิกก่อนกำหนดหาก pass ก่อนหน้าส่งสัญญาณที่แข็งแกร่ง (output ที่ยาวและมีโครงสร้าง) ดังนั้น depth 3 จึงไม่ได้หมายความว่าต้องมีการเรียกใช้ LLM 3 ครั้งเสมอไป

Session-Start Prewarm

เมื่อเริ่มต้น session, Honcho จะเรียกใช้ dialectic call ในพื้นหลังด้วย dialecticDepth ที่กำหนดค่าเต็ม และส่งผลลัพธ์โดยตรงไปยัง context assembly ของ turn 1 การ prewarm แบบ single-pass บน peer ที่เป็น cold มักจะให้ output ที่บาง - multi-pass depth จะรัน cycle ของ audit/reconcile ก่อนที่ผู้ใช้จะพูดด้วยซ้ำ หาก prewarm ไม่มาถึงภายใน turn 1, turn 1 จะ fallback ไปใช้ synchronous call พร้อมด้วย bounded timeout

Query-Adaptive Reasoning Level

dialectic อัตโนมัติจะปรับ dialecticReasoningLevel ตามความยาวของ query: +1 level ที่ ≥120 chars, +2 ที่ ≥400, ถูกจำกัดที่ reasoningLevelCap (default "high") สามารถปิดการใช้งานได้ด้วย reasoningHeuristic: false เพื่อกำหนดทุก auto call ให้ใช้ dialecticReasoningLevel ที่กำหนดไว้ ระดับที่มีให้ใช้: minimal, low, medium, high, max

Configuration Options

Honcho ถูกกำหนดค่าใน ~/.honcho/config.json (global) หรือ $HERMES_HOME/honcho.json (profile-local) setup wizard จะจัดการสิ่งนี้ให้คุณ

Full Config Reference

KeyDefaultDescription
contextTokensnull (uncapped)Token budget สำหรับ auto-injected context ต่อ turn กำหนดเป็น integer (เช่น 1200) เพื่อจำกัด จะถูกตัดที่ word boundaries
contextCadence1Minimum turns ระหว่าง context() API calls (base layer refresh)
dialecticCadence2Minimum turns ระหว่าง peer.chat() LLM calls (dialectic layer). แนะนำ 1–5. ใน tools mode ไม่เกี่ยวข้อง - model calls อย่างชัดเจน
dialecticDepth1Number of .chat() passes ต่อ dialectic invocation. ถูกจำกัดที่ 1–3
dialecticDepthLevelsnullOptional array of reasoning levels per pass, e.g. ["minimal", "low", "medium"]. Overrides proportional defaults
dialecticReasoningLevel'low'Base reasoning level: minimal, low, medium, high, max
dialecticDynamictrueเมื่อเป็น true, model สามารถ override reasoning level ต่อ-call ผ่าน tool param
dialecticMaxChars600Max chars ของ dialectic result ที่ถูกฉีดเข้าไปใน system prompt
recallMode'hybrid'hybrid (auto-inject + tools), context (inject only), tools (tools only)
writeFrequency'async'เมื่อไหร่ที่ควร flush messages: async (background thread), turn (sync), session (batch on end), หรือ integer N
saveMessagestrueว่าจะ persist messages ไปยัง Honcho API หรือไม่
observationMode'directional'directional (all on) หรือ unified (shared pool). Override ด้วย observation object สำหรับการควบคุมแบบ granular
messageMaxChars25000Max chars ต่อ message ที่ส่งผ่าน add_messages(). จะถูก chunk หากเกิน
dialecticMaxInputChars10000Max chars สำหรับ dialectic query input ไปยัง peer.chat()
sessionStrategy'per-directory'per-directory, per-repo, per-session, หรือ global

Session strategy ควบคุมว่า Honcho sessions จะแมปกับงานของคุณอย่างไร:

  • per-session - แต่ละการรัน hermes จะได้ session ใหม่ เริ่มต้นสะอาด memory ผ่าน tools แนะนำสำหรับผู้ใช้ใหม่
  • per-directory - หนึ่ง session ของ Honcho ต่อ working directory หนึ่ง context จะสะสมข้ามการรัน
  • per-repo - หนึ่ง session ต่อ git repository หนึ่ง
  • global - single session ข้ามทุก directory

Recall mode ควบคุมว่า memory ไหลเข้าสู่บทสนทนาอย่างไร:

  • hybrid - context ถูก auto-inject เข้า system prompt และมี tools ให้ใช้ (model ตัดสินใจว่าจะ query เมื่อไหร่)
  • context - auto-injection อย่างเดียว tools ถูกซ่อน
  • tools - tools อย่างเดียว ไม่มี auto-injection Agent ต้องเรียกใช้ honcho_reasoning, honcho_search, ฯลฯ อย่างชัดเจน

Settings per recall mode:

Settinghybridcontexttools
writeFrequencyflushes messagesflushes messagesflushes messages
contextCadencegates base context refreshgates base context refreshirrelevant - no injection
dialecticCadencegates auto LLM callsgates auto LLM callsirrelevant - model calls explicitly
dialecticDepthmulti-pass per invocationmulti-pass per invocationirrelevant - model calls explicitly
contextTokenscaps injectioncaps injectionirrelevant - no injection
dialecticDynamicgates model overrideN/A (no tools)gates model override

ในโหมด tools, model จะควบคุมได้อย่างสมบูรณ์ - มันจะเรียกใช้ honcho_reasoning เมื่อต้องการ ที่ระดับ reasoning_level ใดก็ได้ การตั้งค่า Cadence และ budget ใช้ได้เฉพาะกับโหมดที่มี auto-injection (hybrid และ context) เท่านั้น

Observation (Directional vs. Unified)

Honcho จำลองบทสนทนาเป็น peers ที่แลกเปลี่ยน messages แต่ละ peer มีสอง toggles observation ที่แมป 1:1 กับ SessionPeerConfig ของ Honcho:

ToggleEffect
observeMeHoncho สร้าง representation ของ peer นี้จาก messages ของตัวเอง
observeOtherspeer นี้สังเกต messages ของ peer อื่น (ป้อน cross-peer reasoning)

สอง peers × สอง toggles = สี่ flags. observationMode เป็น preset แบบย่อ:

PresetUser flagsAI flagsSemantics
"directional" (default)me: on, others: onme: on, others: onFull mutual observation. เปิดใช้งาน cross-peer dialectic - "สิ่งที่ AI รู้เกี่ยวกับผู้ใช้ โดยอิงจากสิ่งที่ผู้ใช้พูดและสิ่งที่ AI ตอบกลับ"
"unified"me: on, others: offme: off, others: onShared-pool semantics - AI สังเกต messages ของผู้ใช้เท่านั้น, user peer ทำ self-model อย่างเดียว. Single-observer pool.

สามารถ override preset ด้วยบล็อก observation ที่ชัดเจนเพื่อควบคุมต่อ peer:

"observation": {
  "user": { "observeMe": true,  "observeOthers": true },
  "ai":   { "observeMe": true,  "observeOthers": false }
}

รูปแบบทั่วไป:

IntentConfig
Full observation (most users)"observationMode": "directional"
AI shouldn't re-model the user from its own replies"ai": {"observeMe": true, "observeOthers": false}
Strong persona the AI peer shouldn't update from self-observation"ai": {"observeMe": false, "observeOthers": true}

Server-side toggles ที่ตั้งค่าผ่าน Honcho dashboard จะมีผลเหนือกว่าค่า default ในเครื่อง - Hermes จะซิงค์ค่าเหล่านี้กลับมาเมื่อเริ่มต้น session

Tools

เมื่อ Honcho ทำงานเป็น memory provider, tools ห้าตัวจะพร้อมใช้งาน:

ToolPurpose
honcho_profileอ่านหรืออัปเดต peer card - ส่ง card (list of facts) เพื่ออัปเดต, ละเว้นเพื่ออ่าน
honcho_searchSemantic search over context - raw excerpts, no LLM synthesis
honcho_contextFull session context - summary, representation, card, recent messages
honcho_reasoningSynthesized answer from Honcho's LLM - ส่ง reasoning_level (minimal/low/medium/high/max) เพื่อควบคุมความลึก
honcho_concludeสร้างหรือลบ conclusions - ส่ง conclusion เพื่อสร้าง, delete_id เพื่อลบ (PII only)

CLI Commands

hermes honcho status          # Connection status, config, and key settings
hermes honcho setup           # Interactive setup wizard
hermes honcho strategy        # Show or set session strategy
hermes honcho peer            # Update peer names for multi-agent setups
hermes honcho mode            # Show or set recall mode
hermes honcho tokens          # Show or set context token budget
hermes honcho identity        # Show Honcho peer identity
hermes honcho sync            # Sync host blocks for all profiles
hermes honcho enable          # Enable Honcho
hermes honcho disable         # Disable Honcho

Migrating from hermes honcho

หากคุณเคยใช้ hermes honcho setup แบบ standalone:

  1. การตั้งค่าเดิมของคุณ (honcho.json หรือ ~/.honcho/config.json) จะยังคงอยู่
  2. ข้อมูล server-side ของคุณ (memories, conclusions, user profiles) ยังคงสมบูรณ์
  3. ตั้งค่า memory.provider: honcho ใน config.yaml เพื่อเปิดใช้งานอีกครั้ง

ไม่จำเป็นต้อง re-login หรือ re-setup เพียงแค่รัน hermes memory setup และเลือก "honcho" - wizard จะตรวจพบ config ที่มีอยู่ของคุณ

Full Documentation

ดู Memory Providers - Honcho สำหรับข้อมูลอ้างอิงที่สมบูรณ์


📄 user-guide/features/image-generation.md


title: Image Generation description: Generate images via FAL.ai - 9 models including FLUX 2, GPT Image (1.5 & 2), Nano Banana Pro, Ideogram, Recraft V4 Pro, and more, selectable via hermes tools. sidebar_label: Image Generation sidebar_position: 6

Image Generation

Hermes Agent สร้างรูปภาพจากข้อความ (text prompts) ผ่าน FAL.ai โมเดลที่รองรับพร้อมใช้งานมีถึงเก้าโมเดล โดยแต่ละโมเดลมีการแลกเปลี่ยนระหว่างความเร็ว คุณภาพ และค่าใช้จ่ายที่แตกต่างกัน โมเดลที่ใช้งานอยู่สามารถตั้งค่าได้โดยผู้ใช้ผ่าน hermes tools และจะถูกบันทึกไว้ใน config.yaml

Supported Models

ModelSpeedStrengthsPrice
fal-ai/flux-2/klein/9b (default)<1sFast, crisp text$0.006/MP
fal-ai/flux-2-pro~6sStudio photorealism$0.03/MP
fal-ai/z-image/turbo~2sBilingual EN/CN, 6B params$0.005/MP
fal-ai/nano-banana-pro~8sGemini 3 Pro, reasoning depth, text rendering$0.15/image (1K)
fal-ai/gpt-image-1.5~15sPrompt adherence$0.034/image
fal-ai/gpt-image-2~20sSOTA text rendering + CJK, world-aware photorealism$0.04–0.06/image
fal-ai/ideogram/v3~5sBest typography$0.03–0.09/image
fal-ai/recraft/v4/pro/text-to-image~8sDesign, brand systems, production-ready$0.25/image
fal-ai/qwen-image~12sLLM-based, complex text$0.02/MP

ราคานี้เป็นราคาของ FAL ณ เวลาที่เขียนบทความนี้ โปรดตรวจสอบ fal.ai สำหรับตัวเลขปัจจุบัน

Setup

:::tip Nous Subscribers หากคุณมีการสมัครใช้งาน Nous Portal แบบเสียเงิน คุณสามารถใช้การสร้างรูปภาพผ่าน Tool Gateway ได้โดยไม่ต้องใช้ API key ของ FAL การเลือกโมเดลของคุณจะคงอยู่ทั้งสองเส้นทาง

หาก managed gateway ส่งคืน HTTP 4xx สำหรับโมเดลใดโมเดลหนึ่ง แสดงว่าโมเดลนั้นยังไม่ได้ถูก proxy ที่ฝั่ง portal - agent จะแจ้งให้คุณทราบ พร้อมขั้นตอนการแก้ไข (ตั้งค่า FAL_KEY สำหรับการเข้าถึงโดยตรง หรือเลือกโมเดลอื่น) :::

Get a FAL API Key

  1. Sign up at fal.ai
  2. Generate an API key from your dashboard

Configure and Pick a Model

รันคำสั่ง tools:

hermes tools

ไปที่ 🎨 Image Generation เลือก backend ของคุณ (Nous Subscription หรือ FAL.ai) จากนั้นตัวเลือก (picker) จะแสดงโมเดลที่รองรับทั้งหมดในรูปแบบตารางที่จัดเรียงคอลัมน์ - ใช้ปุ่มลูกศรเพื่อนำทาง และ Enter เพื่อเลือก:

  Model                          Speed    Strengths                    Price
  fal-ai/flux-2/klein/9b         <1s      Fast, crisp text             $0.006/MP   ← currently in use
  fal-ai/flux-2-pro              ~6s      Studio photorealism          $0.03/MP
  fal-ai/z-image/turbo           ~2s      Bilingual EN/CN, 6B          $0.005/MP
  ...

การเลือกของคุณจะถูกบันทึกใน config.yaml:

image_gen:
  model: fal-ai/flux-2/klein/9b
  use_gateway: false            # true if using Nous Subscription

GPT-Image Quality

คุณภาพของ fal-ai/gpt-image-1.5 และ fal-ai/gpt-image-2 ถูกกำหนดให้เป็น medium (ประมาณ $0.034–$0.06/image ที่ 1024×1024) เราไม่ได้เปิดเผยระดับ low / high เป็นตัวเลือกที่ผู้ใช้เห็น เพื่อให้การเรียกเก็บเงินของ Nous Portal มีความคาดเดาได้สำหรับผู้ใช้ทุกคน - ค่าใช้จ่ายที่กระจายระหว่างระดับต่างๆ คือ 3–22× หากคุณต้องการตัวเลือกที่ราคาถูกกว่า ให้เลือก Klein 9B หรือ Z-Image Turbo; หากคุณต้องการคุณภาพที่สูงขึ้น ให้ใช้ Nano Banana Pro หรือ Recraft V4 Pro

Usage

Schema ที่ใช้กับ agent ถูกออกแบบให้เรียบง่ายโดยเจตนา - โมเดลจะดึงค่าที่คุณกำหนดไว้:

Generate an image of a serene mountain landscape with cherry blossoms
Create a square portrait of a wise old owl - use the typography model
Make me a futuristic cityscape, landscape orientation

Aspect Ratios

ทุกโมเดลรับอัตราส่วนภาพ (aspect ratios) สามแบบจากมุมมองของ agent โดยภายในแล้ว ขนาดดั้งเดิม (native size spec) ของแต่ละโมเดลจะถูกเติมให้โดยอัตโนมัติ:

Agent inputimage_size (flux/z-image/qwen/recraft/ideogram)aspect_ratio (nano-banana-pro)image_size (gpt-image-1.5)image_size (gpt-image-2)
landscapelandscape_16_916:91536x1024landscape_4_3 (1024×768)
squaresquare_hd1:11024x1024square_hd (1024×1024)
portraitportrait_16_99:161024x1536portrait_4_3 (768×1024)

GPT Image 2 จะแมปไปยังค่า preset 4:3 แทนที่จะเป็น 16:9 เนื่องจากจำนวนพิกเซลขั้นต่ำของมันคือ 655,360 - preset landscape_16_9 (1024×576 = 589,824) จะถูกปฏิเสธ

การแปลนี้เกิดขึ้นใน _build_fal_payload() - โค้ดของ agent จึงไม่จำเป็นต้องรู้เกี่ยวกับความแตกต่างของ schema ของแต่ละโมเดล

Automatic Upscaling

การ Upscaling ผ่าน Clarity Upscaler ของ FAL มีการจำกัดตามโมเดล:

ModelUpscale?Why
fal-ai/flux-2-proBackward-compat (was the pre-picker default)
All othersFast models would lose their sub-second value prop; hi-res models don't need it

เมื่อมีการรัน Upscaling จะใช้การตั้งค่าเหล่านี้:

SettingValue
Upscale factor
Creativity0.35
Resemblance0.6
Guidance scale4
Inference steps18

หากการ Upscaling ล้มเหลว (ปัญหาเครือข่าย, rate limit) จะมีการส่งคืนรูปภาพต้นฉบับโดยอัตโนมัติ

How It Works Internally

  1. Model resolution - _resolve_fal_model() อ่าน image_gen.model จาก config.yaml, Fallback ไปยัง env var FAL_IMAGE_MODEL, จากนั้นไปยัง fal-ai/flux-2/klein/9b
  2. Payload building - _build_fal_payload() แปลง aspect_ratio ของคุณให้เป็นรูปแบบดั้งเดิมของโมเดล (preset enum, aspect-ratio enum, หรือ GPT literal), รวมพารามิเตอร์เริ่มต้นของโมเดล, ใช้การ override ใดๆ จากผู้เรียก, จากนั้นกรองให้ตรงกับ whitelist supports ของโมเดล เพื่อให้แน่ใจว่าไม่มีการส่งคีย์ที่ไม่รองรับ
  3. Submission - _submit_fal_request() ส่งผ่าน credentials ของ FAL โดยตรง หรือผ่าน Nous gateway ที่จัดการ
  4. Upscaling - รันเฉพาะเมื่อ metadata ของโมเดลมี upscale: True
  5. Delivery - ส่ง URL รูปภาพสุดท้ายกลับไปยัง agent ซึ่งจะปล่อยแท็ก MEDIA:<url> ที่ platform adapters แปลงเป็นสื่อดั้งเดิม

Debugging

เปิดใช้งาน debug logging:

export IMAGE_TOOLS_DEBUG=true

Debug logs จะถูกบันทึกที่ ./logs/image_tools_debug_<session_id>.json พร้อมรายละเอียดต่อการเรียกใช้ (model, parameters, timing, errors)

Platform Delivery

PlatformDelivery
CLIImage URL ถูกพิมพ์เป็น markdown ![](url) - คลิกเพื่อเปิด
TelegramPhoto message พร้อม prompt เป็น caption
Discordฝังอยู่ในข้อความ (Embedded)
SlackURL ถูก unfurled โดย Slack
WhatsAppMedia message
OthersURL ใน plain text

Limitations

  • Requires FAL credentials (direct FAL_KEY หรือ Nous Subscription)
  • Text-to-image only - ไม่รองรับ inpainting, img2img, หรือการแก้ไขผ่านเครื่องมือนี้
  • Temporary URLs - FAL ส่งคืน hosted URLs ที่หมดอายุภายในไม่กี่ชั่วโมง/วัน ควรบันทึกไว้ในเครื่องหากจำเป็น
  • Per-model constraints - โมเดลบางตัวไม่รองรับ seed, num_inference_steps ฯลฯ ตัวกรอง supports จะลบพารามิเตอร์ที่ไม่รองรับอย่างเงียบๆ; นี่คือพฤติกรรมที่คาดหวัง

extent analysis

TL;DR

The issue seems to be related to the Hermes Agent's image generation feature, which relies on FAL.ai models, and the problem might be due to incorrect model selection, insufficient FAL credentials, or other configuration issues.

Guidance

To troubleshoot the issue:

  1. Verify FAL credentials: Ensure that the FAL API key is correctly set up and valid.
  2. Check model selection: Confirm that the chosen model is supported and properly configured in the config.yaml file.
  3. Review debug logs: Enable debug logging by setting IMAGE_TOOLS_DEBUG=true and inspect the logs for any error messages or clues.
  4. Test with a different model: Try switching to a different FAL model to see if the issue persists.
  5. Consult the FAL.ai documentation: Check the FAL.ai documentation for any specific requirements or restrictions on model usage.

Example

To set up the FAL API key, run the following command:

export FAL_KEY="YOUR_FAL_API_KEY_HERE"

Replace YOUR_FAL_API_KEY_HERE with your actual FAL API key.

Notes

  • Make sure to handle FAL API keys securely and do not share them publicly.
  • If using a Nous Subscription, ensure that the subscription is active and properly linked to the Hermes Agent.

Recommendation

Apply the suggested troubleshooting steps to identify and resolve the issue. If the problem persists, consider seeking further assistance from the Hermes Agent community or FAL.ai support.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

hermes - 💡(How to fix) Fix [i18n] Thai Translation: Features Part 2a - Hooks, Honcho, Image Gen [1 participants]