Skip to content
Back to skills

Hermes Agent Inline Logging

ASecurity

Logging automático 100% inline no Hermes Agent via sitecustomize.py — monkey-patch de model_tools.handle_function_call e AIAgent.run_conversation. Loga tool calls + mensagens user/agent no SQLite automaticamente. Sobrevive a pip install -U.

  • 4 stars
  • 0 votes
  • 0 copies
  • 4 views
  • Added September 12, 2026
toolspythonbashsqlgit

Works with

  • cli

Security analysis

A92/100
  • mediumInstalls packages at runtime which could introduce malicious dependencies

Pro scans all 2 files and shows the line behind each finding

Scanned September 12, 2026

npx -y skills add aibot88/sec_skill_store --skill hermes-agent-inline-logging --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Hermes Agent Inline Logging?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Hermes Agent Inline Logging
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/aibot88-hermes-agent-inline-logging/badge)](https://www.skillsdirectory.com/skills/aibot88-hermes-agent-inline-logging)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: hermes-agent-inline-logging
description: Logging automático 100% inline no Hermes Agent via sitecustomize.py — monkey-patch de model_tools.handle_function_call e AIAgent.run_conversation. Loga tool calls + mensagens user/agent no SQLite automaticamente. Sobrevive a pip install -U.
---

# Hermes Agent — Inline Logging via sitecustomize.py

## Problema Original

O `hermes-session-logger` é procedural/manual — o agente precisa chamar `log()` explicitamente. Cron jobs não funcionam porque não conseguem ver o contexto em memória da sessão ativa. Precisávamos de logging **automático e inline**.

## Arquitetura Implementada

### Arquivo: `~/.hermes/hermes-agent/venv/lib/python3.11/site-packages/sitecustomize.py`

Python auto-importa `sitecustomize.py` de qualquer package/site-packages ao iniciar. O plugin:

1. **Hook de import** (`sys.meta_path`) — detecta quando `model_tools` ou `run_agent` forem carregados
2. **Aplica monkey-patch** — substitui `handle_function_call` e `AIAgent.run_conversation` por wrappers que logam antes/depois
3. **Loga no SQLite** — via `hermes_logger.py` em `~/.hermes/`

### O que é logado

| Evento | Como | Tabela |
|--------|------|--------|
| `user_message` | Patch em `AIAgent.run_conversation` | `events` |
| `agent_response` | Patch em `AIAgent.run_conversation` | `events` |
| `tool_call_start` | Patch em `handle_function_call` | `events` |
| `tool_call_result` | Patch em `handle_function_call` | `events` |

## Armadilha Crítica: "multiple values for tool_call_id"

### O Problema

`run_agent.py` chama `handle_function_call` de **duas formas diferentes**:

```python
# Forma A — tool_call_id como KEYWORD
handle_function_call(fn, args, task_id, tool_call_id=tc.id, session_id=s, ...)

# Forma B — tool_call_id como 5o POSICIONAL
handle_function_call(fn, args, task_id, "tc_id_val", session_id=s, ...)
```

O wrapper precisa aceitar **ambos os padrões** sem duplicar o argumento.

### Solução Anti-Regressão (Definitiva)

```python
def _logged_handle_function_call(
    function_name,
    function_args,
    task_id=None,
    *args_extra,      # capta tool_call_id SE vier como 5o positional
    **kwargs          # capta tool_call_id SE vier como keyword
):
    # 1. Extrai tool_call_id de kwargs (se existir)
    tool_call_id = kwargs.pop('tool_call_id', None)

    # 2. Se não veio em kwargs, verifica se veio como positional (5a posição)
    if tool_call_id is None and args_extra:
        tool_call_id = args_extra[0]

    # 3. Chama original — tool_call_id vai OU como positional OU como keyword (nunca ambos)
    if tool_call_id is not None:
        result = _original(
            function_name, function_args, task_id,
            tool_call_id,           # 4o posicional — NUNCA como keyword
            **kwargs
        )
    else:
        result = _original(
            function_name, function_args, task_id,
            **kwargs
        )
```

**Regra de ouro:** `tool_call_id` NUNCA é parâmetro nomeado do wrapper. Sempre extraído via `pop()` de kwargs ou `args_extra`, e passado ao original ou como positional ou como keyword — nunca os dois.

## Arquivo Completo (sitecustomize.py)

```python
#!/usr/bin/env python3
"""
Hermes Auto-Logging Plugin — sitecustomize.py
===============================================
Faz monkey-patch de:
  1. model_tools.handle_function_call — logging de toda tool call
  2. run_agent.AIAgent.run_conversation — logging de mensagens user/agent

Sobrevive a pip install -U hermes-agent porque vive no
venv/site-packages/ (pip não sobrescreve sitecustomize.py).
"""
import sys
import os

HERMES_LOGGER_PATH = os.path.expanduser("~/.hermes")
_hermes_logger = None
_logger_available = False

if HERMES_LOGGER_PATH not in sys.path:
    sys.path.insert(0, HERMES_LOGGER_PATH)

try:
    import hermes_logger as _hermes_logger
    _logger_available = True
except Exception:
    _logger_available = False
    _hermes_logger = None


# ════════════════════════════════════════════════════════════
# PATCH 1 — handle_function_call (tool calls)
# ════════════════════════════════════════════════════════════
_model_tools_patched = False


def _apply_model_tools_patch():
    global _model_tools_patched

    if not _logger_available or _hermes_logger is None:
        return
    if _model_tools_patched:
        return

    try:
        import model_tools as mt
    except ImportError:
        return

    if hasattr(mt, '_hermes_logging_patched'):
        _model_tools_patched = True
        return

    _original = mt.handle_function_call

    def _logged_handle_function_call(
        function_name,
        function_args,
        task_id=None,
        *args_extra,      # tool_call_id se vier como 5o positional
        **kwargs
    ):
        # Log do início
        try:
            _hermes_logger.log_tool_auto(function_name, function_args, started=True)
        except Exception:
            pass

        # Extrai tool_call_id SEM duplicação
        tool_call_id = kwargs.pop('tool_call_id', None)
        if tool_call_id is None and args_extra:
            tool_call_id = args_extra[0]

        # Chama original — tool_call_id vai posicional OU keyword, nunca ambos
        if tool_call_id is not None:
            result = _original(
                function_name, function_args, task_id,
                tool_call_id,
                **kwargs
            )
        else:
            result = _original(
                function_name, function_args, task_id,
                **kwargs
            )

        # Log do resultado
        try:
            _hermes_logger.log_tool_auto(function_name, function_args, result, started=False)
        except Exception:
            pass

        return result

    mt.handle_function_call = _logged_handle_function_call
    mt._hermes_logging_patched = True
    _model_tools_patched = True


class _ModelToolsPatcher:
    def find_module(self, fullname, path=None):
        if fullname == 'model_tools':
            return self
        return None

    def load_module(self, fullname):
        sys.meta_path = [x for x in sys.meta_path if x is not self]
        import importlib
        mod = importlib.import_module(fullname)
        _apply_model_tools_patch()
        return mod


# ════════════════════════════════════════════════════════════
# PATCH 2 — AIAgent.run_conversation (mensagens user/agent)
# ════════════════════════════════════════════════════════════
_run_agent_patched = False


def _apply_run_agent_patch():
    global _run_agent_patched

    if not _logger_available or _hermes_logger is None:
        return
    if _run_agent_patched:
        return

    try:
        from run_agent import AIAgent
    except ImportError:
        return

    if hasattr(AIAgent, '_hermes_msg_logging_patched'):
        _run_agent_patched = True
        return

    _original_run_conv = AIAgent.run_conversation

    def _logged_run_conversation(
        self,
        user_message,
        system_message=None,
        conversation_history=None,
        task_id=None,
        stream_callback=None,
        persist_user_message=None,
        **kwargs
    ):
        sess_id = task_id or getattr(self, 'session_id', None) or 'cli'
        if not _hermes_logger.get_active_session():
            _hermes_logger.set_active_session(sess_id)

        # Loga mensagem do usuário (entrada)
        msg_to_log = persist_user_message if persist_user_message else user_message
        if msg_to_log:
            try:
                _hermes_logger.log_user(sess_id, str(msg_to_log)[:10000])
            except Exception:
                pass

        # Chama original
        result = _original_run_conv(
            self, user_message,
            system_message=system_message,
            conversation_history=conversation_history,
            task_id=task_id,
            stream_callback=stream_callback,
            persist_user_message=persist_user_message,
            **kwargs
        )

        # Loga resposta do agent (saída)
        if result and isinstance(result, dict):
            final = result.get('final_response', '')
            if final:
                try:
                    _hermes_logger.log_agent(sess_id, str(final)[:10000])
                except Exception:
                    pass

        return result

    AIAgent.run_conversation = _logged_run_conversation
    AIAgent._hermes_msg_logging_patched = True
    _run_agent_patched = True


class _RunAgentPatcher:
    def find_module(self, fullname, path=None):
        if fullname == 'run_agent':
            return self
        return None

    def load_module(self, fullname):
        sys.meta_path = [x for x in sys.meta_path if x is not self]
        import importlib
        mod = importlib.import_module(fullname)
        _apply_run_agent_patch()
        return mod


# ─── Instala hooks ──────────────────────────────────────────
if _logger_available:
    sys.meta_path.insert(0, _ModelToolsPatcher())
    sys.meta_path.insert(0, _RunAgentPatcher())
```

## Pré-requisitos: hermes_logger.py

O `hermes_logger.py` precisa existir em `~/.hermes/` com estas funções:

```python
def log_user(session_id, message): ...
def log_agent(session_id, response): ...
def log_tool_auto(function_name, function_args, result=None, started=True): ...
def set_active_session(session_id): ...
def get_active_session(): ...
```

## Verificação Pós-Instalação

```bash
cd ~/.hermes/hermes-agent && source venv/bin/activate
python -c "from model_tools import handle_function_call; print('model_tools OK')"
python -c "from run_agent import AIAgent; print('AIAgent patched:', hasattr(AIAgent, '_hermes_msg_logging_patched'))"
hermes tools list  # não pode crashar

# Verificar logs
sqlite3 ~/.hermes/hermes_sessions.db "SELECT event_type, COUNT(*) FROM events GROUP BY event_type;"
```

Resultado esperado: `user_message`, `agent_response`, `tool_call_start`, `tool_call_result`.

## Garantias

- **100% automático** — não requer chamada explícita de `log()` em nenhum lugar
- **Todas as plataformas** — CLI, Telegram, gateway (o hook funciona globalmente)
- **Resiste a updates** — vive em `venv/site-packages/`, pip não sobrescreve
- **Thread-safe** — hermes_logger usa `threading.Lock`
- **Anti-regressão** — `tool_call_id` nunca duplicado

## NÃO Confundir Com

- **Callback nativo do AIAgent** — era a abordagem anterior (skill antiga); não foi o que funcionou
- **Cron job de logging** — inútil para contexto inline
- **Git hooks** — não relacionado

## Histórico

- 15/04/2026 — Primeira versão com monkey-patch de `handle_function_call`; bug "multiple values for tool_call_id" identificado e correção inicial
- 16/04/2026 — Versão definitiva com `*args_extra` + `**kwargs`; adicionada captura de `user_message` e `agent_response` via patch em `AIAgent.run_conversation`; todas as ferramentas testadas e funcionando

Files in this skill

  • SKILL.md11.2 KB
  • source.json805 B

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.