hermes - 💡(How to fix) Fix [i18n] Thai Translation: Developer Guide Part d - provider-runtime, session-storage, tools-runtime, trajectory-format [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#15129Fetched 2026-04-25 06:24:24
View on GitHub
Comments
0
Participants
1
Timeline
2
Reactions
0
Author
Participants
Timeline (top)
labeled ×2

Error Message

Simplified from registry.py

if entry.check_fn: try: available = bool(entry.check_fn()) except Exception: available = False # Exceptions = unavailable if not available: continue # Skip this tool entirely

Fix Action

Fix / Workaround


sidebar_position: 9 title: "Tools Runtime" description: "Runtime behavior of the tool registry, toolsets, dispatch, and terminal environments"

Hermes tools คือฟังก์ชันที่ลงทะเบียนตัวเอง (self-registering functions) ซึ่งถูกจัดกลุ่มเป็น toolsets และถูกเรียกใช้งานผ่านระบบ central registry/dispatch

  1. Dynamic schema patching — หลังจากการกรอง execute_code และ browser_navigate schemas จะถูกปรับเปลี่ยนแบบ dynamic เพื่ออ้างอิงเฉพาะเครื่องมือที่ผ่านการกรองจริงเท่านั้น (ป้องกันไม่ให้ model hallucinate เครื่องมือที่ไม่มีอยู่)

Code Example

~/.hermes/state.db (SQLite, WAL mode)
├── sessions          — Session metadata, token counts, billing
├── messages          — Full message history per session
├── messages_fts      — FTS5 virtual table for full-text search
└── schema_version    — Single-row table tracking migration state

---

CREATE TABLE IF NOT EXISTS sessions (
    id TEXT PRIMARY KEY,
    source TEXT NOT NULL,
    user_id TEXT,
    model TEXT,
    model_config TEXT,
    system_prompt TEXT,
    parent_session_id TEXT,
    started_at REAL NOT NULL,
    ended_at REAL,
    end_reason TEXT,
    message_count INTEGER DEFAULT 0,
    tool_call_count INTEGER DEFAULT 0,
    input_tokens INTEGER DEFAULT 0,
    output_tokens INTEGER DEFAULT 0,
    cache_read_tokens INTEGER DEFAULT 0,
    cache_write_tokens INTEGER DEFAULT 0,
    reasoning_tokens INTEGER DEFAULT 0,
    billing_provider TEXT,
    billing_base_url TEXT,
    billing_mode TEXT,
    estimated_cost_usd REAL,
    actual_cost_usd REAL,
    cost_status TEXT,
    cost_source TEXT,
    pricing_version TEXT,
    title TEXT,
    FOREIGN KEY (parent_session_id) REFERENCES sessions(id)
);

CREATE INDEX IF NOT EXISTS idx_sessions_source ON sessions(source);
CREATE INDEX IF NOT EXISTS idx_sessions_parent ON sessions(parent_session_id);
CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at DESC);
CREATE UNIQUE INDEX IF NOT EXISTS idx_sessions_title_unique
    ON sessions(title) WHERE title IS NOT NULL;

---

CREATE TABLE IF NOT EXISTS messages (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    session_id TEXT NOT NULL REFERENCES sessions(id),
    role TEXT NOT NULL,
    content TEXT,
    tool_call_id TEXT,
    tool_calls TEXT,
    tool_name TEXT,
    timestamp REAL NOT NULL,
    token_count INTEGER,
    finish_reason TEXT,
    reasoning TEXT,
    reasoning_details TEXT,
    codex_reasoning_items TEXT
);

CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id, timestamp);

---

CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(
    content,
    content=messages,
    content_rowid=id
);

---

CREATE TRIGGER IF NOT EXISTS messages_fts_insert AFTER INSERT ON messages BEGIN
    INSERT INTO messages_fts(rowid, content) VALUES (new.id, new.content);
END;

CREATE TRIGGER IF NOT EXISTS messages_fts_delete AFTER DELETE ON messages BEGIN
    INSERT INTO messages_fts(messages_fts, rowid, content)
        VALUES('delete', old.id, old.content);
END;

CREATE TRIGGER IF NOT EXISTS messages_fts_update AFTER UPDATE ON messages BEGIN
    INSERT INTO messages_fts(messages_fts, rowid, content)
        VALUES('delete', old.id, old.content);
    INSERT INTO messages_fts(rowid, content) VALUES (new.id, new.content);
END;

---

_WRITE_MAX_RETRIES = 15
_WRITE_RETRY_MIN_S = 0.020   # 20ms
_WRITE_RETRY_MAX_S = 0.150   # 150ms
_CHECKPOINT_EVERY_N_WRITES = 50

---

from hermes_state import SessionDB

db = SessionDB()                           # Default: ~/.hermes/state.db
db = SessionDB(db_path=Path("/tmp/test.db"))  # Custom path

---

# Create a new session
db.create_session(
    session_id="sess_abc123",
    source="cli",
    model="anthropic/claude-sonnet-4.6",
    user_id="user_1",
    parent_session_id=None,  # or previous session ID for lineage
)

# End a session
db.end_session("sess_abc123", end_reason="user_exit")

# Reopen a session (clear ended_at/end_reason)
db.reopen_session("sess_abc123")

---

msg_id = db.append_message(
    session_id="sess_abc123",
    role="assistant",
    content="Here's the answer...",
    tool_calls=[{"id": "call_1", "function": {"name": "terminal", "arguments": "{}"}}],
    token_count=150,
    finish_reason="stop",
    reasoning="Let me think about this...",
)

---

# Raw messages with all metadata
messages = db.get_messages("sess_abc123")

# OpenAI conversation format (for API replay)
conversation = db.get_messages_as_conversation("sess_abc123")
# Returns: [{"role": "user", "content": "..."}, {"role": "assistant", ...}]

---

# Set a title (must be unique among non-NULL titles)
db.set_session_title("sess_abc123", "Fix Docker Build")

# Resolve by title (returns most recent in lineage)
session_id = db.resolve_session_by_title("Fix Docker Build")

# Auto-generate next title in lineage
next_title = db.get_next_title_in_lineage("Fix Docker Build")
# Returns: "Fix Docker Build #2"

---

results = db.search_messages("docker deployment")

---

# Search only CLI sessions
results = db.search_messages("error", source_filter=["cli"])

# Exclude gateway sessions
results = db.search_messages("bug", exclude_sources=["telegram", "discord"])

# Search only user messages
results = db.search_messages("help", role_filter=["user"])

---

-- Find all ancestors of a session
WITH RECURSIVE lineage AS (
    SELECT * FROM sessions WHERE id = ?
    UNION ALL
    SELECT s.* FROM sessions s
    JOIN lineage l ON s.id = l.parent_session_id
)
SELECT id, title, started_at, parent_session_id FROM lineage;

-- Find all descendants of a session
WITH RECURSIVE descendants AS (
    SELECT * FROM sessions WHERE id = ?
    UNION ALL
    SELECT s.* FROM sessions s
    JOIN descendants d ON s.parent_session_id = d.id
)
SELECT id, title, started_at FROM descendants;

---

SELECT s.*,
    COALESCE(
        (SELECT SUBSTR(m.content, 1, 63)
         FROM messages m
         WHERE m.session_id = s.id AND m.role = 'user' AND m.content IS NOT NULL
         ORDER BY m.timestamp, m.id LIMIT 1),
        ''
    ) AS preview,
    COALESCE(
        (SELECT MAX(m2.timestamp) FROM messages m2 WHERE m2.session_id = s.id),
        s.started_at
    ) AS last_active
FROM sessions s
ORDER BY s.started_at DESC
LIMIT 20;

---

-- Total tokens by model
SELECT model,
       COUNT(*) as session_count,
       SUM(input_tokens) as total_input,
       SUM(output_tokens) as total_output,
       SUM(estimated_cost_usd) as total_cost
FROM sessions
WHERE model IS NOT NULL
GROUP BY model
ORDER BY total_cost DESC;

-- Sessions with highest token usage
SELECT id, title, model, input_tokens + output_tokens AS total_tokens,
       estimated_cost_usd
FROM sessions
ORDER BY total_tokens DESC
LIMIT 10;

---

# Export a single session with messages
data = db.export_session("sess_abc123")

# Export all sessions (with messages) as list of dicts
all_data = db.export_all(source="cli")

# Delete old sessions (only ended sessions)
deleted_count = db.prune_sessions(older_than_days=90)
deleted_count = db.prune_sessions(older_than_days=30, source="telegram")

# Clear messages but keep the session record
db.clear_messages("sess_abc123")

# Delete session and all messages
db.delete_session("sess_abc123")

---

registry.register(
    name="terminal",               # Unique tool name (used in API schemas)
    toolset="terminal",            # Toolset this tool belongs to
    schema={...},                  # OpenAI function-calling schema (description, parameters)
    handler=handle_terminal,       # The function that executes when the tool is called
    check_fn=check_terminal,       # Optional: returns True/False for availability
    requires_env=["SOME_VAR"],     # Optional: env vars needed (for UI display)
    is_async=False,                # Whether the handler is an async coroutine
    description="Run commands",    # Human-readable description
    emoji="💻",                    # Emoji for spinner/progress display
)

---

# tools/registry.py (simplified)
def discover_builtin_tools(tools_dir=None):
    tools_path = Path(tools_dir) if tools_dir else Path(__file__).parent
    for path in sorted(tools_path.glob("*.py")):
        if path.name in {"__init__.py", "registry.py", "mcp_tool.py"}:
            continue
        if _module_registers_tools(path):  # AST check for top-level registry.register()
            importlib.import_module(f"tools.{path.stem}")

---

# Simplified from registry.py
if entry.check_fn:
    try:
        available = bool(entry.check_fn())
    except Exception:
        available = False   # Exceptions = unavailable
    if not available:
        continue            # Skip this tool entirely

---

Model response with tool_call
run_agent.py agent loop
model_tools.handle_function_call(name, args, task_id, user_task)
[Agent-loop tools?] → handled directly by agent loop (todo, memory, session_search, delegate_task)
[Plugin pre-hook]invoke_hook("pre_tool_call", ...)
registry.dispatch(name, args, **kwargs)
Look up ToolEntry by name
[Async handler?] → bridge via _run_async()
[Sync handler?]  → call directly
Return result string (or JSON error)
[Plugin post-hook]invoke_hook("post_tool_call", ...)

---

{
  "conversations": [ ... ],
  "timestamp": "2026-03-30T14:22:31.456789",
  "model": "anthropic/claude-sonnet-4.6",
  "completed": true
}

---

{
  "prompt_index": 42,
  "conversations": [ ... ],
  "metadata": { "prompt_source": "gsm8k", "difficulty": "hard" },
  "completed": true,
  "partial": false,
  "api_calls": 7,
  "toolsets_used": ["code_tools", "file_tools"],
  "tool_stats": {
    "terminal": {"count": 3, "success": 3, "failure": 0},
    "read_file": {"count": 2, "success": 2, "failure": 0},
    "write_file": {"count": 0, "success": 0, "failure": 0}
  },
  "tool_error_counts": {
    "terminal": 0,
    "read_file": 0,
    "write_file": 0
  }
}

---

{
  "conversations": [
    {
      "from": "system",
      "value": "You are a function calling AI model. You are provided with function signatures within <tools> </tools> XML tags. You may call one or more functions to assist with the user query. If available tools are not relevant in assisting with user query, just respond in natural conversational language. Don't make assumptions about what values to plug into functions. After calling & executing the functions, you will be provided with function results within <tool_response> </tool_response> XML tags. Here are the available tools:\n<tools>\n[{\"name\": \"terminal\", \"description\": \"Execute shell commands\", \"parameters\": {\"type\": \"object\", \"properties\": {\"command\": {\"type\": \"string\"}}}, \"required\": null}]\n</tools>\nFor each function call return a JSON object, with the following pydantic model json schema for each:\n{'title': 'FunctionCall', 'type': 'object', 'properties': {'name': {'title': 'Name', 'type': 'string'}, 'arguments': {'title': 'Arguments', 'type': 'object'}}, 'required': ['name', 'arguments']}\nEach function call should be enclosed within <tool_call> </tool_call> XML tags.\nExample:\n<tool_call>\n{'name': <function-name>,'arguments': <args-dict>}\n</tool_call>"
    },
    {
      "from": "human",
      "value": "What Python version is installed?"
    },
    {
      "from": "gpt",
      "value": "<think>\nThe user wants to know the Python version. I should run python3 --version.\n</think>\n<tool_call>\n{\"name\": \"terminal\", \"arguments\": {\"command\": \"python3 --version\"}}\n</tool_call>"
    },
    {
      "from": "tool",
      "value": "<tool_response>\n{\"tool_call_id\": \"call_abc123\", \"name\": \"terminal\", \"content\": \"Python 3.11.6\"}\n</tool_response>"
    },
    {
      "from": "gpt",
      "value": "<think>\nGot the version. I can now answer the user.\n</think>\nPython 3.11.6 is installed on this system."
    }
  ],
  "timestamp": "2026-03-30T14:22:31.456789",
  "model": "anthropic/claude-sonnet-4.6",
  "completed": true
}

---

<tool_call>
{"name": "terminal", "arguments": {"command": "ls -la"}}
</tool_call>

---

<tool_response>
{"tool_call_id": "call_abc123", "name": "terminal", "content": "output here"}
</tool_response>

---

import json

def load_trajectories(path: str):
    """Load trajectory entries from a JSONL file."""
    entries = []
    with open(path, "r", encoding="utf-8") as f:
        for line in f:
            line = line.strip()
            if line:
                entries.append(json.loads(line))
    return entries

# Filter to successful completions only
successful = [e for e in load_trajectories("trajectory_samples.jsonl")
              if e.get("completed")]

# Extract just the conversations for training
training_data = [e["conversations"] for e in successful]

---

from datasets import load_dataset

ds = load_dataset("json", data_files="trajectory_samples.jsonl")

---

# config.yaml
agent:
  save_trajectories: true  # default: false
RAW_BUFFERClick to expand / collapse

📄 developer-guide/provider-runtime.md


sidebar_position: 4 title: "Provider Runtime Resolution" description: "How Hermes resolves providers, credentials, API modes, and auxiliary models at runtime"

การแก้ไข Provider แบบ Runtime

Hermes ใช้ตัวแก้ไข provider runtime ร่วมกันสำหรับ:

  • CLI
  • gateway
  • cron jobs
  • ACP
  • auxiliary model calls

การใช้งานหลัก:

  • hermes_cli/runtime_provider.py - การแก้ไข credential, _resolve_custom_runtime()
  • hermes_cli/auth.py - provider registry, resolve_provider()
  • hermes_cli/model_switch.py - shared /model switch pipeline (CLI + gateway)
  • agent/auxiliary_client.py - auxiliary model routing

หากคุณต้องการเพิ่ม provider inference แบบ first-class ใหม่ โปรดอ่าน Adding Providers ควบคู่ไปกับหน้านี้

ลำดับความสำคัญของการแก้ไข

ในภาพรวม การแก้ไข provider จะใช้:

  1. explicit CLI/runtime request
  2. config.yaml model/provider config
  3. environment variables
  4. provider-specific defaults or auto resolution

ลำดับนี้มีความสำคัญ เพราะ Hermes ถือว่าการเลือก model/provider ที่บันทึกไว้เป็นแหล่งข้อมูลที่ถูกต้อง (source of truth) สำหรับการทำงานปกติ สิ่งนี้ช่วยป้องกันไม่ให้การ export shell ที่ล้าสมัยไปเขียนทับ endpoint ที่ผู้ใช้เลือกครั้งล่าสุดในคำสั่ง hermes model อย่างเงียบๆ

Providers

กลุ่ม provider ปัจจุบันประกอบด้วย:

  • AI Gateway (Vercel)
  • OpenRouter
  • Nous Portal
  • OpenAI Codex
  • Copilot / Copilot ACP
  • Anthropic (native)
  • Google / Gemini
  • Alibaba / DashScope
  • DeepSeek
  • Z.AI
  • Kimi / Moonshot
  • MiniMax
  • MiniMax China
  • Kilo Code
  • Hugging Face
  • OpenCode Zen / OpenCode Go
  • Custom (provider: custom) - provider แบบ first-class สำหรับ endpoint ที่เข้ากันได้กับ OpenAI ทุกประเภท
  • Named custom providers (custom_providers list in config.yaml)

ผลลัพธ์ของการแก้ไขแบบ Runtime

ตัวแก้ไข runtime จะส่งคืนข้อมูลต่างๆ เช่น:

  • provider
  • api_mode
  • base_url
  • api_key
  • source
  • metadata เฉพาะ provider เช่น ข้อมูล expiry/refresh

เหตุใดสิ่งนี้จึงสำคัญ

ตัวแก้ไขนี้เป็นเหตุผลหลักที่ทำให้ Hermes สามารถแชร์ logic ด้าน auth/runtime ระหว่าง:

  • hermes chat
  • gateway message handling
  • cron jobs running in fresh sessions
  • ACP editor sessions
  • auxiliary model tasks

AI Gateway

ตั้งค่า AI_GATEWAY_API_KEY ใน ~/.hermes/.env และรันด้วย --provider ai-gateway Hermes จะดึง model ที่พร้อมใช้งานจาก endpoint /models ของ gateway โดยกรองเฉพาะ language models ที่รองรับ tool-use

OpenRouter, AI Gateway, และ base URLs ที่เข้ากันได้กับ OpenAI แบบกำหนดเอง

Hermes มี logic เพื่อป้องกันการส่ง API key ผิดไปยัง custom endpoint เมื่อมีหลาย provider keys อยู่ (เช่น OPENROUTER_API_KEY, AI_GATEWAY_API_KEY, และ OPENAI_API_KEY)

API key ของแต่ละ provider จะถูกจำกัดขอบเขต (scoped) ให้กับ base URL ของตัวเอง:

  • OPENROUTER_API_KEY จะถูกส่งไปยัง endpoint openrouter.ai เท่านั้น
  • AI_GATEWAY_API_KEY จะถูกส่งไปยัง endpoint ai-gateway.vercel.sh เท่านั้น
  • OPENAI_API_KEY ถูกใช้สำหรับ custom endpoints และเป็น fallback

นอกจากนี้ Hermes ยังแยกแยะระหว่าง:

  • custom endpoint จริงที่ผู้ใช้เลือก
  • OpenRouter fallback path ที่ใช้เมื่อไม่มีการกำหนดค่า custom endpoint

การแยกแยะนี้มีความสำคัญอย่างยิ่งสำหรับ:

  • local model servers
  • non-OpenRouter/non-AI Gateway OpenAI-compatible APIs
  • การสลับ provider โดยไม่ต้องรัน setup ใหม่
  • custom endpoints ที่บันทึกใน config ซึ่งควรยังคงใช้งานได้แม้ว่า OPENAI_BASE_URL จะไม่ได้ถูก export ใน shell ปัจจุบัน

เส้นทาง Anthropic แบบ Native

Anthropic ไม่ได้เป็นเพียงแค่ "ผ่าน OpenRouter" อีกต่อไปแล้ว

เมื่อการแก้ไข provider เลือก anthropic Hermes จะใช้:

  • api_mode = anthropic_messages
  • Anthropic Messages API แบบ Native
  • agent/anthropic_adapter.py สำหรับการแปล

การแก้ไข credential สำหรับ Anthropic แบบ Native ตอนนี้จะให้ความสำคัญกับ Claude Code credentials ที่สามารถ refresh ได้มากกว่า env tokens ที่คัดลอกมา เมื่อมีทั้งสองอย่างอยู่ ในทางปฏิบัติหมายความว่า:

  • ไฟล์ credential ของ Claude Code จะถูกถือเป็นแหล่งข้อมูลที่ต้องการเมื่อมีการรวม auth ที่สามารถ refresh ได้
  • ค่า ANTHROPIC_TOKEN / CLAUDE_CODE_OAUTH_TOKEN แบบ manual ยังคงใช้ได้ในฐานะการ override ที่ชัดเจน
  • Hermes จะ preflight การ refresh credential ของ Anthropic ก่อนการเรียก Native Messages API
  • Hermes ยังคงพยายามอีกครั้งเมื่อเจอ 401 หลังจากสร้าง Anthropic client ใหม่ ในฐานะ fallback path

เส้นทาง OpenAI Codex

Codex ใช้ Responses API path แยกต่างหาก:

  • api_mode = codex_responses
  • dedicated credential resolution และ auth store support

การกำหนดเส้นทางสำหรับ auxiliary model

งาน auxiliary เช่น:

  • vision
  • web extraction summarization
  • context compression summaries
  • session search summarization
  • skills hub operations
  • MCP helper operations
  • memory flushes

สามารถใช้ provider/model routing ของตัวเองได้ แทนที่จะใช้ model สำหรับการสนทนาหลัก

เมื่อ auxiliary task ถูกกำหนดค่าด้วย provider main Hermes จะแก้ไขผ่าน shared runtime path เดียวกันกับการแชทปกติ ในทางปฏิบัติหมายความว่า:

  • custom endpoints ที่ขับเคลื่อนด้วย env ยังคงใช้งานได้
  • custom endpoints ที่บันทึกผ่าน hermes model / config.yaml ก็ใช้งานได้เช่นกัน
  • auxiliary routing สามารถแยกแยะระหว่าง custom endpoint ที่บันทึกจริงกับ OpenRouter fallback

Fallback models

Hermes รองรับคู่ model/provider fallback ที่กำหนดค่าไว้ ทำให้สามารถ failover แบบ runtime ได้เมื่อ model หลักเกิดข้อผิดพลาด

การทำงานภายใน

  1. Storage: AIAgent.__init__ จะจัดเก็บ dict fallback_model และตั้งค่า _fallback_activated = False.

  2. Trigger points: _try_activate_fallback() ถูกเรียกจากสามจุดใน main retry loop ใน run_agent.py:

    • หลังจาก max retries เมื่อเจอ invalid API responses (None choices, missing content)
    • เมื่อเกิด client errors ที่ไม่สามารถ retry ได้ (HTTP 401, 403, 404)
    • หลังจาก max retries เมื่อเจอ transient errors (HTTP 429, 500, 502, 503)
  3. Activation flow (_try_activate_fallback):

    • คืนค่า False ทันทีหากถูกเปิดใช้งานแล้วหรือไม่ได้กำหนดค่า
    • เรียก resolve_provider_client() จาก auxiliary_client.py เพื่อสร้าง client ใหม่พร้อม auth ที่ถูกต้อง
    • กำหนด api_mode: codex_responses สำหรับ openai-codex, anthropic_messages สำหรับ anthropic, chat_completions สำหรับทุกอย่างที่เหลือ
    • สลับค่าแบบ in-place: self.model, self.provider, self.base_url, self.api_mode, self.client, self._client_kwargs
    • สำหรับ anthropic fallback: สร้าง native Anthropic client แทน OpenAI-compatible
    • ประเมิน prompt caching ใหม่ (เปิดใช้งานสำหรับ Claude models บน OpenRouter)
    • ตั้งค่า _fallback_activated = True - ป้องกันการทำงานซ้ำ
    • รีเซ็ตจำนวนครั้งที่ retry เป็น 0 และดำเนินการ loop ต่อไป
  4. Config flow:

    • CLI: cli.py อ่าน CLI_CONFIG["fallback_model"] -> ส่งไปยัง AIAgent(fallback_model=...)
    • Gateway: gateway/run.py._load_fallback_model() อ่าน config.yaml -> ส่งไปยัง AIAgent
    • Validation: ทั้งคีย์ provider และ model ต้องไม่ว่างเปล่า มิฉะนั้น fallback จะถูกปิดใช้งาน

สิ่งที่ไม่รองรับ fallback

  • Subagent delegation (tools/delegate_tool.py): subagents จะสืบทอด provider ของ parent แต่ไม่รวม config fallback
  • Cron jobs (cron/): รันด้วย provider ที่กำหนดไว้ตายตัว ไม่มีกลไก fallback
  • Auxiliary tasks: ใช้ chain การตรวจจับ provider อัตโนมัติที่เป็นอิสระของตัวเอง (ดู Auxiliary model routing ข้างต้น)

การครอบคลุมการทดสอบ

ดูที่ tests/test_fallback_model.py สำหรับการทดสอบที่ครอบคลุม provider ที่รองรับทั้งหมด, one-shot semantics, และ edge cases

เอกสารที่เกี่ยวข้อง


📄 developer-guide/session-storage.md

การจัดเก็บ Session

Hermes Agent ใช้ฐานข้อมูล SQLite (~/.hermes/state.db) เพื่อจัดเก็บ metadata ของ session, ประวัติข้อความทั้งหมด, และการตั้งค่า model สำหรับ session ทั้งแบบ CLI และ gateway ซึ่งมาแทนที่วิธีการใช้ไฟล์ JSONL แยกตาม session แบบเดิม

Source file: hermes_state.py

ภาพรวมสถาปัตยกรรม

~/.hermes/state.db (SQLite, WAL mode)
├── sessions          — Session metadata, token counts, billing
├── messages          — Full message history per session
├── messages_fts      — FTS5 virtual table for full-text search
└── schema_version    — Single-row table tracking migration state

การตัดสินใจด้านการออกแบบที่สำคัญ:

  • WAL mode สำหรับการอ่านพร้อมกัน (concurrent readers) + writer หนึ่งตัว (gateway multi-platform)
  • FTS5 virtual table สำหรับการค้นหาข้อความที่รวดเร็วทั่วทุก session
  • Session lineage ผ่าน chains ของ parent_session_id (การแยก session ที่เกิดจากการบีบอัด context)
  • Source tagging (cli, telegram, discord, etc.) สำหรับการกรองแพลตฟอร์ม
  • Batch runner และ RL trajectories จะไม่ถูกจัดเก็บที่นี่ (เป็นระบบแยกต่างหาก)

Schema ของ SQLite

Sessions Table

CREATE TABLE IF NOT EXISTS sessions (
    id TEXT PRIMARY KEY,
    source TEXT NOT NULL,
    user_id TEXT,
    model TEXT,
    model_config TEXT,
    system_prompt TEXT,
    parent_session_id TEXT,
    started_at REAL NOT NULL,
    ended_at REAL,
    end_reason TEXT,
    message_count INTEGER DEFAULT 0,
    tool_call_count INTEGER DEFAULT 0,
    input_tokens INTEGER DEFAULT 0,
    output_tokens INTEGER DEFAULT 0,
    cache_read_tokens INTEGER DEFAULT 0,
    cache_write_tokens INTEGER DEFAULT 0,
    reasoning_tokens INTEGER DEFAULT 0,
    billing_provider TEXT,
    billing_base_url TEXT,
    billing_mode TEXT,
    estimated_cost_usd REAL,
    actual_cost_usd REAL,
    cost_status TEXT,
    cost_source TEXT,
    pricing_version TEXT,
    title TEXT,
    FOREIGN KEY (parent_session_id) REFERENCES sessions(id)
);

CREATE INDEX IF NOT EXISTS idx_sessions_source ON sessions(source);
CREATE INDEX IF NOT EXISTS idx_sessions_parent ON sessions(parent_session_id);
CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at DESC);
CREATE UNIQUE INDEX IF NOT EXISTS idx_sessions_title_unique
    ON sessions(title) WHERE title IS NOT NULL;

Messages Table

CREATE TABLE IF NOT EXISTS messages (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    session_id TEXT NOT NULL REFERENCES sessions(id),
    role TEXT NOT NULL,
    content TEXT,
    tool_call_id TEXT,
    tool_calls TEXT,
    tool_name TEXT,
    timestamp REAL NOT NULL,
    token_count INTEGER,
    finish_reason TEXT,
    reasoning TEXT,
    reasoning_details TEXT,
    codex_reasoning_items TEXT
);

CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id, timestamp);

หมายเหตุ:

  • tool_calls ถูกจัดเก็บเป็น JSON string (รายการ tool call objects ที่ถูก serialize)
  • reasoning_details และ codex_reasoning_items ถูกจัดเก็บเป็น JSON strings
  • reasoning จัดเก็บข้อความ reasoning ดิบสำหรับ provider ที่เปิดเผยข้อมูลนี้
  • Timestamps คือ Unix epoch floats (time.time())

FTS5 Full-Text Search

CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(
    content,
    content=messages,
    content_rowid=id
);

ตาราง FTS5 จะถูกซิงค์ผ่าน trigger สามตัวที่ทำงานเมื่อมีการ INSERT, UPDATE, และ DELETE ในตาราง messages:

CREATE TRIGGER IF NOT EXISTS messages_fts_insert AFTER INSERT ON messages BEGIN
    INSERT INTO messages_fts(rowid, content) VALUES (new.id, new.content);
END;

CREATE TRIGGER IF NOT EXISTS messages_fts_delete AFTER DELETE ON messages BEGIN
    INSERT INTO messages_fts(messages_fts, rowid, content)
        VALUES('delete', old.id, old.content);
END;

CREATE TRIGGER IF NOT EXISTS messages_fts_update AFTER UPDATE ON messages BEGIN
    INSERT INTO messages_fts(messages_fts, rowid, content)
        VALUES('delete', old.id, old.content);
    INSERT INTO messages_fts(rowid, content) VALUES (new.id, new.content);
END;

เวอร์ชัน Schema และการย้ายข้อมูล (Migrations)

เวอร์ชัน schema ปัจจุบัน: 6

ตาราง schema_version จัดเก็บ integer เพียงตัวเดียว เมื่อเริ่มต้นระบบ ฟังก์ชัน _init_schema() จะตรวจสอบเวอร์ชันปัจจุบันและใช้ migrations ตามลำดับ:

VersionChange
1Initial schema (sessions, messages, FTS5)
2Add finish_reason column to messages
3Add title column to sessions
4Add unique index on title (NULLs allowed, non-NULL must be unique)
5Add billing columns: cache_read_tokens, cache_write_tokens, reasoning_tokens, billing_provider, billing_base_url, billing_mode, estimated_cost_usd, actual_cost_usd, cost_status, cost_source, pricing_version
6Add reasoning columns to messages: reasoning, reasoning_details, codex_reasoning_items

แต่ละ migration ใช้ ALTER TABLE ADD COLUMN ที่ห่อด้วย try/except เพื่อจัดการกรณีที่ column มีอยู่แล้ว (idempotent) และจะเพิ่มหมายเลขเวอร์ชันหลังจากบล็อก migration สำเร็จแต่ละครั้ง

การจัดการ Write Contention

หลาย process ของ hermes (gateway + CLI sessions + worktree agents) แชร์ state.db เดียวกัน คลาส SessionDB จัดการ write contention ด้วย:

  • Short SQLite timeout (1 วินาที) แทนค่าเริ่มต้น 30 วินาที
  • Application-level retry พร้อม random jitter (20-150ms, สูงสุด 15 ครั้ง)
  • BEGIN IMMEDIATE transactions เพื่อแสดง lock contention ตั้งแต่เริ่ม transaction
  • Periodic WAL checkpoints ทุก 50 successful writes (PASSIVE mode)

สิ่งนี้ช่วยหลีกเลี่ยง "convoy effect" ที่การ backoff ภายในแบบ deterministic ของ SQLite ทำให้ writer ที่แข่งขันกันทั้งหมดต้อง retry ในช่วงเวลาเดียวกัน

_WRITE_MAX_RETRIES = 15
_WRITE_RETRY_MIN_S = 0.020   # 20ms
_WRITE_RETRY_MAX_S = 0.150   # 150ms
_CHECKPOINT_EVERY_N_WRITES = 50

การดำเนินการทั่วไป

Initialize

from hermes_state import SessionDB

db = SessionDB()                           # Default: ~/.hermes/state.db
db = SessionDB(db_path=Path("/tmp/test.db"))  # Custom path

Create and Manage Sessions

# Create a new session
db.create_session(
    session_id="sess_abc123",
    source="cli",
    model="anthropic/claude-sonnet-4.6",
    user_id="user_1",
    parent_session_id=None,  # or previous session ID for lineage
)

# End a session
db.end_session("sess_abc123", end_reason="user_exit")

# Reopen a session (clear ended_at/end_reason)
db.reopen_session("sess_abc123")

Store Messages

msg_id = db.append_message(
    session_id="sess_abc123",
    role="assistant",
    content="Here's the answer...",
    tool_calls=[{"id": "call_1", "function": {"name": "terminal", "arguments": "{}"}}],
    token_count=150,
    finish_reason="stop",
    reasoning="Let me think about this...",
)

Retrieve Messages

# Raw messages with all metadata
messages = db.get_messages("sess_abc123")

# OpenAI conversation format (for API replay)
conversation = db.get_messages_as_conversation("sess_abc123")
# Returns: [{"role": "user", "content": "..."}, {"role": "assistant", ...}]

Session Titles

# Set a title (must be unique among non-NULL titles)
db.set_session_title("sess_abc123", "Fix Docker Build")

# Resolve by title (returns most recent in lineage)
session_id = db.resolve_session_by_title("Fix Docker Build")

# Auto-generate next title in lineage
next_title = db.get_next_title_in_lineage("Fix Docker Build")
# Returns: "Fix Docker Build #2"

การค้นหาแบบ Full-Text

เมธอด search_messages() รองรับ FTS5 query syntax พร้อมการ sanitize user input โดยอัตโนมัติ

Basic Search

results = db.search_messages("docker deployment")

FTS5 Query Syntax

SyntaxExampleMeaning
Keywordsdocker deploymentทั้งสองคำ (implicit AND)
Quoted phrase"exact phrase"ตรงกับวลีที่ระบุเป๊ะๆ
Boolean ORdocker OR kubernetesคำใดคำหนึ่ง
Boolean NOTpython NOT javaยกเว้นคำ
Prefixdeploy*ตรงกับ prefix

Filtered Search

# Search only CLI sessions
results = db.search_messages("error", source_filter=["cli"])

# Exclude gateway sessions
results = db.search_messages("bug", exclude_sources=["telegram", "discord"])

# Search only user messages
results = db.search_messages("help", role_filter=["user"])

Search Results Format

แต่ละผลลัพธ์ประกอบด้วย:

  • id, session_id, role, timestamp
  • snippet — snippet ที่สร้างโดย FTS5 พร้อม marker >>>match<<<
  • context — ข้อความ 1 ข้อความก่อนและหลังที่ตรงกัน (content ถูกตัดเหลือ 200 ตัวอักษร)
  • source, model, session_started — จาก session แม่

เมธอด _sanitize_fts5_query() จัดการกรณีขอบ (edge cases):

  • ลบ quotes และ special characters ที่ไม่ตรงกัน
  • ห่อ terms ที่มี hyphen ด้วย quotes (chat-send"chat-send")
  • ลบ boolean operators ที่ค้างอยู่ (hello ANDhello)

Session Lineage

Session สามารถสร้างเป็น chains ผ่าน parent_session_id สิ่งนี้เกิดขึ้นเมื่อ context compression กระตุ้นให้เกิดการแยก session ใน gateway

Query: Find Session Lineage

-- Find all ancestors of a session
WITH RECURSIVE lineage AS (
    SELECT * FROM sessions WHERE id = ?
    UNION ALL
    SELECT s.* FROM sessions s
    JOIN lineage l ON s.id = l.parent_session_id
)
SELECT id, title, started_at, parent_session_id FROM lineage;

-- Find all descendants of a session
WITH RECURSIVE descendants AS (
    SELECT * FROM sessions WHERE id = ?
    UNION ALL
    SELECT s.* FROM sessions s
    JOIN descendants d ON s.parent_session_id = d.id
)
SELECT id, title, started_at FROM descendants;

Query: Recent Sessions with Preview

SELECT s.*,
    COALESCE(
        (SELECT SUBSTR(m.content, 1, 63)
         FROM messages m
         WHERE m.session_id = s.id AND m.role = 'user' AND m.content IS NOT NULL
         ORDER BY m.timestamp, m.id LIMIT 1),
        ''
    ) AS preview,
    COALESCE(
        (SELECT MAX(m2.timestamp) FROM messages m2 WHERE m2.session_id = s.id),
        s.started_at
    ) AS last_active
FROM sessions s
ORDER BY s.started_at DESC
LIMIT 20;

Query: Token Usage Statistics

-- Total tokens by model
SELECT model,
       COUNT(*) as session_count,
       SUM(input_tokens) as total_input,
       SUM(output_tokens) as total_output,
       SUM(estimated_cost_usd) as total_cost
FROM sessions
WHERE model IS NOT NULL
GROUP BY model
ORDER BY total_cost DESC;

-- Sessions with highest token usage
SELECT id, title, model, input_tokens + output_tokens AS total_tokens,
       estimated_cost_usd
FROM sessions
ORDER BY total_tokens DESC
LIMIT 10;

การส่งออกและการล้างข้อมูล

# Export a single session with messages
data = db.export_session("sess_abc123")

# Export all sessions (with messages) as list of dicts
all_data = db.export_all(source="cli")

# Delete old sessions (only ended sessions)
deleted_count = db.prune_sessions(older_than_days=90)
deleted_count = db.prune_sessions(older_than_days=30, source="telegram")

# Clear messages but keep the session record
db.clear_messages("sess_abc123")

# Delete session and all messages
db.delete_session("sess_abc123")

ตำแหน่งฐานข้อมูล

Default path: ~/.hermes/state.db

ค่านี้ได้มาจาก hermes_constants.get_hermes_home() ซึ่งโดยค่าเริ่มต้นจะ resolve ไปที่ ~/.hermes/ หรือค่าของ environment variable HERMES_HOME

ไฟล์ฐานข้อมูล, ไฟล์ WAL (state.db-wal), และไฟล์ shared-memory (state.db-shm) จะถูกสร้างทั้งหมดในไดเรกทอรีเดียวกัน


📄 developer-guide/tools-runtime.md


sidebar_position: 9 title: "Tools Runtime" description: "Runtime behavior of the tool registry, toolsets, dispatch, and terminal environments"

Tools Runtime

Hermes tools คือฟังก์ชันที่ลงทะเบียนตัวเอง (self-registering functions) ซึ่งถูกจัดกลุ่มเป็น toolsets และถูกเรียกใช้งานผ่านระบบ central registry/dispatch

Primary files:

  • tools/registry.py
  • model_tools.py
  • toolsets.py
  • tools/terminal_tool.py
  • tools/environments/*

Tool registration model

โมดูลเครื่องมือแต่ละตัวจะเรียกใช้ registry.register(...) ในขณะที่ทำการ import

model_tools.py มีหน้าที่รับผิดชอบในการ import/ค้นหา tool modules และสร้าง schema list ที่ใช้โดย model

How registry.register() works

ไฟล์เครื่องมือทุกไฟล์ใน tools/ จะเรียกใช้ registry.register() ที่ระดับ module เพื่อประกาศตัวตนของตัวเอง Signature ของฟังก์ชันคือ:

registry.register(
    name="terminal",               # Unique tool name (used in API schemas)
    toolset="terminal",            # Toolset this tool belongs to
    schema={...},                  # OpenAI function-calling schema (description, parameters)
    handler=handle_terminal,       # The function that executes when the tool is called
    check_fn=check_terminal,       # Optional: returns True/False for availability
    requires_env=["SOME_VAR"],     # Optional: env vars needed (for UI display)
    is_async=False,                # Whether the handler is an async coroutine
    description="Run commands",    # Human-readable description
    emoji="💻",                    # Emoji for spinner/progress display
)

การเรียกใช้แต่ละครั้งจะสร้าง ToolEntry ซึ่งถูกเก็บไว้ใน dict ToolRegistry._tools แบบ singleton โดยใช้ชื่อเครื่องมือเป็น key หากเกิด name collision ระหว่าง toolsets จะมีการบันทึก warning และการลงทะเบียนครั้งหลังจะเป็นผู้ชนะ

Discovery: discover_builtin_tools()

เมื่อ model_tools.py ถูก import มันจะเรียกใช้ discover_builtin_tools() จาก tools/registry.py ฟังก์ชันนี้จะสแกนไฟล์ tools/*.py ทุกไฟล์โดยใช้ AST parsing เพื่อค้นหาโมดูลที่มีการเรียกใช้ registry.register() ที่ระดับ top-level จากนั้นจึงทำการ import โมดูลเหล่านั้น:

# tools/registry.py (simplified)
def discover_builtin_tools(tools_dir=None):
    tools_path = Path(tools_dir) if tools_dir else Path(__file__).parent
    for path in sorted(tools_path.glob("*.py")):
        if path.name in {"__init__.py", "registry.py", "mcp_tool.py"}:
            continue
        if _module_registers_tools(path):  # AST check for top-level registry.register()
            importlib.import_module(f"tools.{path.stem}")

การ auto-discovery นี้หมายความว่าไฟล์เครื่องมือใหม่ๆ จะถูกดึงมาใช้งานโดยอัตโนมัติ โดยไม่จำเป็นต้องดูแลรายการด้วยตนเอง การตรวจสอบ AST จะจับคู่เฉพาะการเรียกใช้ registry.register() ที่ระดับ top-level เท่านั้น (ไม่รวมการเรียกใช้ภายในฟังก์ชัน) ดังนั้น helper modules ใน tools/ จึงไม่ถูก import

การ import แต่ละครั้งจะกระตุ้นการเรียกใช้ registry.register() ของโมดูลนั้นๆ ข้อผิดพลาดในเครื่องมือทางเลือก (เช่น การขาด fal_client สำหรับการสร้างภาพ) จะถูกจับและบันทึก — ซึ่งจะไม่ขัดขวางการโหลดเครื่องมืออื่น

หลังจากค้นพบเครื่องมือหลักแล้ว ยังมีการค้นพบ MCP tools และ plugin tools เพิ่มเติม:

  1. MCP toolstools.mcp_tool.discover_mcp_tools() จะอ่าน config ของ MCP server และลงทะเบียนเครื่องมือจาก external servers
  2. Plugin toolshermes_cli.plugins.discover_plugins() จะโหลด plugins ของผู้ใช้/project/pip ที่อาจลงทะเบียนเครื่องมือเพิ่มเติม

Tool availability checking (check_fn)

เครื่องมือแต่ละตัวสามารถให้ check_fn ได้โดยทางเลือก ซึ่งเป็น callable ที่จะคืนค่า True เมื่อเครื่องมือพร้อมใช้งาน และ False ในกรณีอื่น ๆ การตรวจสอบทั่วไปรวมถึง:

  • API key present — เช่น lambda: bool(os.environ.get("SERP_API_KEY")) สำหรับ web search
  • Service running — เช่น การตรวจสอบว่า Honcho server ถูกตั้งค่าไว้หรือไม่
  • Binary installed — เช่น การตรวจสอบว่า playwright พร้อมใช้งานสำหรับ browser tools

เมื่อ registry.get_definitions() สร้าง schema list สำหรับ model มันจะรัน check_fn() ของเครื่องมือแต่ละตัว:

# Simplified from registry.py
if entry.check_fn:
    try:
        available = bool(entry.check_fn())
    except Exception:
        available = False   # Exceptions = unavailable
    if not available:
        continue            # Skip this tool entirely

พฤติกรรมหลัก:

  • ผลการตรวจสอบจะถูก cache ต่อการเรียกใช้ — หากเครื่องมือหลายตัวใช้ check_fn เดียวกัน จะถูกรันเพียงครั้งเดียว
  • Exceptions ใน check_fn() จะถูกถือว่า "ไม่พร้อมใช้งาน" (fail-safe)
  • เมธอด is_toolset_available() จะตรวจสอบว่า check_fn ของ toolset ผ่านหรือไม่ ซึ่งใช้สำหรับการแสดงผลใน UI และการแก้ไข toolset

Toolset resolution

Toolsets คือกลุ่มเครื่องมือที่มีชื่อเรียก Hermes จะแก้ไข toolsets เหล่านี้ผ่าน:

  • รายการ toolset ที่เปิด/ปิดใช้งานอย่างชัดเจน
  • platform presets (hermes-cli, hermes-telegram, etc.)
  • dynamic MCP toolsets
  • ชุดวัตถุประสงค์พิเศษที่คัดสรรมา เช่น hermes-acp

How get_tool_definitions() filters tools

จุดเข้าหลักคือ model_tools.get_tool_definitions(enabled_toolsets, disabled_toolsets, quiet_mode):

  1. หากมีการระบุ enabled_toolsets — จะรวมเฉพาะเครื่องมือจาก toolsets เหล่านั้นเท่านั้น ชื่อ toolset แต่ละชื่อจะถูกแก้ไขผ่าน resolve_toolset() ซึ่งจะขยาย toolsets แบบ composite ให้เป็นชื่อ tool แยกกัน

  2. หากมีการระบุ disabled_toolsets — จะเริ่มต้นด้วย toolsets ทั้งหมด แล้วลบ toolsets ที่ถูกปิดใช้งานออก

  3. หากไม่ระบุทั้งคู่ — จะรวม toolsets ที่ทราบทั้งหมด

  4. Registry filtering — ชุดชื่อ tool ที่แก้ไขแล้วจะถูกส่งไปยัง registry.get_definitions() ซึ่งจะใช้การกรอง check_fn และส่งคืน schema ในรูปแบบ OpenAI

  5. Dynamic schema patching — หลังจากการกรอง execute_code และ browser_navigate schemas จะถูกปรับเปลี่ยนแบบ dynamic เพื่ออ้างอิงเฉพาะเครื่องมือที่ผ่านการกรองจริงเท่านั้น (ป้องกันไม่ให้ model hallucinate เครื่องมือที่ไม่มีอยู่)

Legacy toolset names

ชื่อ toolset เก่าที่มี suffix _tools (เช่น web_tools, terminal_tools) จะถูกแมปไปยังชื่อ tool สมัยใหม่ผ่าน _LEGACY_TOOLSET_MAP เพื่อความเข้ากันได้แบบย้อนหลัง

Dispatch

ในขณะรันไทม์ เครื่องมือจะถูก dispatch ผ่าน central registry โดยมีข้อยกเว้นสำหรับ agent-loop สำหรับเครื่องมือระดับ agent บางตัว เช่น การจัดการ memory/todo/session-search

Dispatch flow: model tool_call → handler execution

เมื่อ model คืนค่า tool_call flow จะเป็นดังนี้:

Model response with tool_call
run_agent.py agent loop
model_tools.handle_function_call(name, args, task_id, user_task)
[Agent-loop tools?] → handled directly by agent loop (todo, memory, session_search, delegate_task)
[Plugin pre-hook] → invoke_hook("pre_tool_call", ...)
registry.dispatch(name, args, **kwargs)
Look up ToolEntry by name
[Async handler?] → bridge via _run_async()
[Sync handler?]  → call directly
Return result string (or JSON error)
[Plugin post-hook] → invoke_hook("post_tool_call", ...)

Error wrapping

การดำเนินการเครื่องมือทั้งหมดจะถูกห่อหุ้มด้วยการจัดการข้อผิดพลาดในสองระดับ:

  1. registry.dispatch() — จะจับทุก exception จาก handler และส่งคืน {"error": "Tool execution failed: ExceptionType: message"} ในรูปแบบ JSON

  2. handle_function_call() — จะห่อหุ้มการ dispatch ทั้งหมดด้วย try/except รองที่ส่งคืน {"error": "Error executing tool_name: message"}

สิ่งนี้รับประกันว่า model จะได้รับ JSON string ที่มีรูปแบบถูกต้องเสมอ ไม่ใช่ unhandled exception

Agent-loop tools

มีเครื่องมือสี่ตัวที่ถูกดักจับก่อนการ dispatch ของ registry เนื่องจากพวกมันต้องการ state ระดับ agent (TodoStore, MemoryStore, etc.):

  • todo — การวางแผน/การติดตามงาน
  • memory — การเขียน memory แบบ persistent
  • session_search — การเรียกคืนข้อมูลข้าม session
  • delegate_task — การสร้าง subagent sessions

schema ของเครื่องมือเหล่านี้ยังคงลงทะเบียนอยู่ใน registry (สำหรับ get_tool_definitions) แต่ handler ของพวกมันจะส่งคืน stub error หาก dispatch ไปถึงพวกมันโดยตรง

Async bridging

เมื่อ tool handler เป็น async, _run_async() จะทำหน้าที่เป็นสะพานเชื่อมไปยัง sync dispatch path:

  • CLI path (no running loop) — ใช้ persistent event loop เพื่อรักษา async clients ที่ถูก cache ให้มีชีวิตอยู่
  • Gateway path (running loop) — สร้าง thread ที่สามารถทิ้งได้ด้วย asyncio.run()
  • Worker threads (parallel tools) — ใช้ persistent loops ต่อ thread ที่ถูกเก็บไว้ใน thread-local storage

The DANGEROUS_PATTERNS approval flow

เครื่องมือ terminal ได้รวมระบบอนุมัติคำสั่งอันตราย (dangerous-command approval system) ซึ่งถูกกำหนดไว้ใน tools/approval.py:

  1. Pattern detectionDANGEROUS_PATTERNS คือ list ของ tuple (regex, description) ที่ครอบคลุมการดำเนินการที่ทำลายล้าง:

    • การลบแบบ recursive (rm -rf)
    • การ format filesystem (mkfs, dd)
    • การดำเนินการ SQL ที่ทำลายล้าง (DROP TABLE, DELETE FROM โดยไม่มี WHERE)
    • การเขียนทับ config ระบบ (> /etc/)
    • การจัดการ service (systemctl stop)
    • การรันโค้ดระยะไกล (curl | sh)
    • Fork bombs, process kills, etc.
  2. Detection — ก่อนการรันคำสั่ง terminal ใดๆ จะมีการตรวจสอบ detect_dangerous_command(command) กับทุก pattern

  3. Approval prompt — หากพบการจับคู่:

    • CLI mode — prompt แบบ interactive จะขอให้ผู้ใช้อนุมัติ ปฏิเสธ หรืออนุญาตถาวร
    • Gateway mode — async approval callback จะส่งคำขอไปยัง messaging platform
    • Smart approval — ทางเลือกคือ LLM ตัวช่วยสามารถ auto-approve คำสั่งความเสี่ยงต่ำที่ตรงกับ patterns (เช่น rm -rf node_modules/ ปลอดภัยแต่ตรงกับ "recursive delete")
  4. Session state — การอนุมัติจะถูกติดตามต่อ session หนึ่งๆ เมื่อคุณอนุมัติ "recursive delete" สำหรับ session หนึ่ง คำสั่ง rm -rf ครั้งต่อไปจะไม่ขอ prompt ซ้ำ

  5. Permanent allowlist — ตัวเลือก "allow permanently" จะเขียน pattern นั้นลงใน config.yaml's command_allowlist ทำให้คงอยู่ข้าม session

Terminal/runtime environments

ระบบ terminal รองรับ backends หลายตัว:

  • local
  • docker
  • ssh
  • singularity
  • modal
  • daytona

นอกจากนี้ยังรองรับ:

  • per-task cwd overrides
  • background process management
  • PTY mode
  • approval callbacks สำหรับคำสั่งอันตราย

Concurrency

การเรียกใช้เครื่องมืออาจดำเนินการแบบ sequential หรือ concurrently ขึ้นอยู่กับ mix ของเครื่องมือและความต้องการในการโต้ตอบ

Related docs


📄 developer-guide/trajectory-format.md

Trajectory Format

Hermes Agent บันทึก trajectory ของการสนทนาในรูปแบบ JSONL ที่เข้ากันได้กับ ShareGPT เพื่อใช้เป็นข้อมูลสำหรับการฝึกอบรม (training data), ข้อมูลที่ใช้ในการดีบัก (debugging artifacts), และชุดข้อมูลสำหรับ reinforcement learning

Source files: agent/trajectory.py, run_agent.py (search for _save_trajectory), batch_runner.py

File Naming Convention

Trajectories จะถูกเขียนลงในไฟล์ใน current working directory:

FileWhen
trajectory_samples.jsonlการสนทนาที่เสร็จสมบูรณ์เรียบร้อยแล้ว (completed=True)
failed_trajectories.jsonlการสนทนาที่ล้มเหลวหรือถูกขัดจังหวะ (completed=False)

batch runner (batch_runner.py) จะเขียนไปยังไฟล์ output แบบกำหนดเองสำหรับแต่ละ batch (เช่น batch_001_output.jsonl) พร้อมด้วย metadata fields เพิ่มเติม

คุณสามารถกำหนดชื่อไฟล์ใหม่ได้ผ่านพารามิเตอร์ filename ใน save_trajectory().

JSONL Entry Format

แต่ละบรรทัดในไฟล์คือ JSON object ที่สมบูรณ์ในตัวเอง มีสองรูปแบบ:

CLI/Interactive Format (จาก _save_trajectory)

{
  "conversations": [ ... ],
  "timestamp": "2026-03-30T14:22:31.456789",
  "model": "anthropic/claude-sonnet-4.6",
  "completed": true
}

Batch Runner Format (จาก batch_runner.py)

{
  "prompt_index": 42,
  "conversations": [ ... ],
  "metadata": { "prompt_source": "gsm8k", "difficulty": "hard" },
  "completed": true,
  "partial": false,
  "api_calls": 7,
  "toolsets_used": ["code_tools", "file_tools"],
  "tool_stats": {
    "terminal": {"count": 3, "success": 3, "failure": 0},
    "read_file": {"count": 2, "success": 2, "failure": 0},
    "write_file": {"count": 0, "success": 0, "failure": 0}
  },
  "tool_error_counts": {
    "terminal": 0,
    "read_file": 0,
    "write_file": 0
  }
}

พจนานุกรม tool_stats และ tool_error_counts จะถูกปรับให้เป็นมาตรฐาน (normalized) เพื่อรวม ALL possible tools (จาก model_tools.TOOL_TO_TOOLSET_MAP) โดยมีค่าเริ่มต้นเป็นศูนย์ ซึ่งรับประกัน schema ที่สม่ำเสมอในทุก entry สำหรับการโหลด dataset ของ HuggingFace

Conversations Array (ShareGPT Format)

array conversations ใช้ convention ของบทบาท (role) ของ ShareGPT:

API RoleShareGPT from
system"system"
user"human"
assistant"gpt"
tool"tool"

Complete Example

{
  "conversations": [
    {
      "from": "system",
      "value": "You are a function calling AI model. You are provided with function signatures within <tools> </tools> XML tags. You may call one or more functions to assist with the user query. If available tools are not relevant in assisting with user query, just respond in natural conversational language. Don't make assumptions about what values to plug into functions. After calling & executing the functions, you will be provided with function results within <tool_response> </tool_response> XML tags. Here are the available tools:\n<tools>\n[{\"name\": \"terminal\", \"description\": \"Execute shell commands\", \"parameters\": {\"type\": \"object\", \"properties\": {\"command\": {\"type\": \"string\"}}}, \"required\": null}]\n</tools>\nFor each function call return a JSON object, with the following pydantic model json schema for each:\n{'title': 'FunctionCall', 'type': 'object', 'properties': {'name': {'title': 'Name', 'type': 'string'}, 'arguments': {'title': 'Arguments', 'type': 'object'}}, 'required': ['name', 'arguments']}\nEach function call should be enclosed within <tool_call> </tool_call> XML tags.\nExample:\n<tool_call>\n{'name': <function-name>,'arguments': <args-dict>}\n</tool_call>"
    },
    {
      "from": "human",
      "value": "What Python version is installed?"
    },
    {
      "from": "gpt",
      "value": "<think>\nThe user wants to know the Python version. I should run python3 --version.\n</think>\n<tool_call>\n{\"name\": \"terminal\", \"arguments\": {\"command\": \"python3 --version\"}}\n</tool_call>"
    },
    {
      "from": "tool",
      "value": "<tool_response>\n{\"tool_call_id\": \"call_abc123\", \"name\": \"terminal\", \"content\": \"Python 3.11.6\"}\n</tool_response>"
    },
    {
      "from": "gpt",
      "value": "<think>\nGot the version. I can now answer the user.\n</think>\nPython 3.11.6 is installed on this system."
    }
  ],
  "timestamp": "2026-03-30T14:22:31.456789",
  "model": "anthropic/claude-sonnet-4.6",
  "completed": true
}

Normalization Rules

Reasoning Content Markup

trajectory converter จะปรับให้ reasoning ทั้งหมดอยู่ในแท็ก <think> โดยไม่คำนึงถึงวิธีการที่ model สร้างขึ้นมาแต่เดิม:

  1. Native thinking tokens (msg["reasoning"] field จาก providers เช่น Anthropic, OpenAI o-series): จะถูกห่อเป็น <think>\n{reasoning}\n</think>\n และนำมาวางไว้ก่อนเนื้อหา

  2. REASONING_SCRATCHPAD XML (เมื่อการคิดแบบ native ถูกปิดใช้งาน และ model ใช้ reasoning ผ่าน XML ที่ถูกสั่งการด้วย system-prompt): แท็ก <REASONING_SCRATCHPAD> จะถูกแปลงเป็น <think> ผ่าน convert_scratchpad_to_think().

  3. Empty think blocks: ทุก turn ของ gpt จะรับประกันว่าจะมีบล็อก <think> หากไม่มีการสร้าง reasoning ใดๆ จะมีการแทรกบล็อกว่างเปล่า: <think>\n</think>\n - สิ่งนี้รับประกันรูปแบบที่สม่ำเสมอสำหรับ training data.

Tool Call Normalization

Tool calls จาก API format (ที่มี tool_call_id, function name, arguments เป็น JSON string) จะถูกแปลงเป็น JSON ที่ห่อด้วย XML:

<tool_call>
{"name": "terminal", "arguments": {"command": "ls -la"}}
</tool_call>
  • Arguments จะถูก parse จาก JSON strings กลับเป็น objects (ไม่ double-encoded)
  • หากการ parse JSON ล้มเหลว (ไม่ควรเกิดขึ้น - ตรวจสอบแล้วระหว่างการสนทนา), จะใช้ {} ว่างเปล่าพร้อมกับการบันทึก warning
  • การเรียกใช้ tool หลายครั้งใน turn เดียวของ assistant จะสร้างหลายบล็อก <tool_call> ในข้อความ gpt เดียว

Tool Response Normalization

ผลลัพธ์ tool ทั้งหมดที่ตามหลังข้อความของ assistant จะถูกจัดกลุ่มเป็น turn tool เดียวพร้อมกับ JSON responses ที่ห่อด้วย XML:

<tool_response>
{"tool_call_id": "call_abc123", "name": "terminal", "content": "output here"}
</tool_response>
  • หาก tool content มีลักษณะเป็น JSON (ขึ้นต้นด้วย { หรือ [), จะถูก parse เพื่อให้ field content บรรจุ JSON object/array แทนที่จะเป็น string
  • ผลลัพธ์ tool หลายรายการจะถูกรวมด้วย newlines ในข้อความเดียว
  • ชื่อ tool จะถูกจับคู่ตามตำแหน่งกับ array tool_calls ของ assistant ตัวแม่

System Message

system message จะถูกสร้างขึ้น ณ เวลาที่บันทึก (ไม่ได้มาจาก conversation). มันจะปฏิบัติตาม Hermes function-calling prompt template โดยมี:

  • Preamble อธิบาย protocol การเรียกใช้ function
  • บล็อก XML <tools> ที่มีคำจำกัดความของ tool ในรูปแบบ JSON
  • Schema reference สำหรับ object FunctionCall
  • ตัวอย่าง <tool_call>

คำจำกัดความของ tool จะรวม name, description, parameters, และ required (ตั้งค่าเป็น null เพื่อให้ตรงกับ canonical format).

Loading Trajectories

Trajectories เป็น JSONL มาตรฐาน - สามารถโหลดด้วย JSON-lines reader ใดก็ได้:

import json

def load_trajectories(path: str):
    """Load trajectory entries from a JSONL file."""
    entries = []
    with open(path, "r", encoding="utf-8") as f:
        for line in f:
            line = line.strip()
            if line:
                entries.append(json.loads(line))
    return entries

# Filter to successful completions only
successful = [e for e in load_trajectories("trajectory_samples.jsonl")
              if e.get("completed")]

# Extract just the conversations for training
training_data = [e["conversations"] for e in successful]

Loading for HuggingFace Datasets

from datasets import load_dataset

ds = load_dataset("json", data_files="trajectory_samples.jsonl")

Schema tool_stats ที่ถูกปรับให้เป็นมาตรฐานรับประกันว่าทุก entry จะมีคอลัมน์เดียวกัน ป้องกันข้อผิดพลาด Arrow schema mismatch ในระหว่างการโหลด dataset

Controlling Trajectory Saving

ใน CLI, การบันทึก trajectory ถูกควบคุมโดย:

# config.yaml
agent:
  save_trajectories: true  # default: false

หรือผ่าน flag --save-trajectories. เมื่อ agent ถูก initialize ด้วย save_trajectories=True, method _save_trajectory() จะถูกเรียกที่ท้าย ของการสนทนาแต่ละ turn.

batch runner จะบันทึก trajectories เสมอ (นั่นคือวัตถุประสงค์หลักของมัน).

ตัวอย่างที่มี reasoning เป็นศูนย์ตลอดทุก turn จะถูก batch runner ทิ้งโดยอัตโนมัติ เพื่อหลีกเลี่ยงการปนเปื้อน training data ด้วยตัวอย่างที่ไม่มี reasoning.


extent analysis

TL;DR

The issue seems to be related to the Hermes Agent's trajectory format and saving mechanism, but without a specific error or problem statement, it's hard to provide a direct fix; however, ensuring the save_trajectories option is correctly set in the config.yaml file or using the appropriate flag can help control trajectory saving.

Guidance

  1. Check Configuration: Verify that the save_trajectories option is set to true in your config.yaml file if you want to save trajectories.
  2. Use Command Line Flags: If running the agent from the command line, use the --save-trajectories flag to enable trajectory saving.
  3. Review Trajectory Format: Ensure that the trajectory format conforms to the expected JSONL format, including the conversations array and other necessary fields.
  4. Loading Trajectories: When loading trajectories for training or analysis, use a JSON-lines reader to handle the JSONL format correctly.

Example

To load trajectories from a file named trajectory_samples.jsonl, you can use the following Python code:

import json

def load_trajectories(path: str):
    entries = []
    with open(path, "r", encoding="utf-8") as f:
        for line in f:
            line = line.strip()
            if line:
                entries.append(json.loads(line))
    return entries

trajectories = load_trajectories("trajectory_samples.jsonl")

Notes

  • The Hermes Agent's trajectory saving and loading are designed to work with the JSONL format, which is a sequence of JSON objects, one per line.
  • The save_trajectories option and command line flags provide control over whether trajectories are saved during agent operation.
  • When working with trajectories, ensure that the data is handled correctly, considering the specific requirements of your application or analysis.

Recommendation

Apply the --save-trajectories flag when running the agent to ensure trajectories are saved

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: Developer Guide Part d - provider-runtime, session-storage, tools-runtime, trajectory-format [1 participants]