Pular para o conteúdo principal

Documentação oficial desta página · v0.20.6

Runtime de Ferramentas

As ferramentas do Hermes são funções que se registram sozinhas, agrupadas em conjuntos de ferramentas e executadas por um sistema central de registro e despacho.

Arquivos principais:

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

Modelo de registro de ferramentas​

Cada módulo de ferramenta chama registry.register(...) no momento da importação.

O model_tools.py é responsável por importar/descobrir os módulos de ferramenta e montar a lista de esquemas usada pelo modelo.

Como o registry.register() funciona​

Todo arquivo de ferramenta em tools/ chama registry.register() no nível do módulo para se declarar. A assinatura da função é:

registry.register(
name="terminal", # Nome único da ferramenta (usado nos esquemas da API)
toolset="terminal", # Conjunto de ferramentas ao qual ela pertence
schema={...}, # Esquema visível ao modelo (descrição, parâmetros)
handler=handle_terminal, # A função executada quando a ferramenta é chamada
check_fn=check_terminal, # Opcional: devolve True/False de disponibilidade
requires_env=["SOME_VAR"], # Opcional: variáveis de ambiente necessárias (para exibir na interface)
is_async=False, # Se o handler é uma corrotina assíncrona
description="Run commands", # Opcional: metadado de registro do ToolEntry
emoji="💻", # Emoji para o spinner/indicador de progresso
)

Cada chamada cria um ToolEntry guardado no dicionário singleton ToolRegistry._tools, indexado pelo nome da ferramenta. Um registro que sobrescreveria uma ferramenta existente de um conjunto diferente é rejeitado (com log de erro), a menos que quem chamou passe override=True; sobrescritas de ferramentas embutidas feitas por plugin exigem, além disso, que o operador opte por elas com plugins.entries.<plugin_id>.allow_tool_override: true no config.yaml.

O schema["description"] é a descrição oficial que o modelo enxerga. O argumento separado description= preenche o ToolEntry.description; quando ele é omitido, o metadado do registro recai na descrição do esquema. O get_definitions() monta a definição de função no formato OpenAI a partir de entry.schema e não copia o entry.description para um esquema que não tenha description. Portanto, o description= sozinho não descreve a ferramenta para o modelo, e quando os dois valores divergem o modelo vê o valor do esquema. Prefira definir a descrição uma vez só, no esquema, a menos que algum consumidor do registro precise de metadados diferentes de propósito.

Descoberta: discover_builtin_tools()​

Quando o model_tools.py é importado, ele chama discover_builtin_tools() de tools/registry.py. Essa função varre todo arquivo tools/*.py com análise de AST para achar os módulos que contêm chamadas de registry.register() no nível superior e então os importa:

# tools/registry.py (simplificado)
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): # checagem de AST por registry.register() no nível superior
importlib.import_module(f"tools.{path.stem}")

Essa descoberta automática significa que arquivos novos de ferramenta entram sozinhos — não existe lista manual para manter. A checagem de AST só casa com chamadas de registry.register() no nível superior (não com chamadas dentro de funções), então módulos auxiliares em tools/ não são importados.

Cada importação dispara as chamadas de registry.register() do módulo. Erros em ferramentas opcionais (por exemplo, falta do fal_client para geração de imagem) são capturados e registrados em log — eles não impedem o carregamento das outras ferramentas.

Depois da descoberta das ferramentas centrais, as ferramentas de MCP e de plugin também são descobertas:

  1. Ferramentas MCP — o tools.mcp_tool.discover_mcp_tools() lê a configuração dos servidores MCP e registra as ferramentas de servidores externos.
  2. Ferramentas de plugin — o hermes_cli.plugins.discover_plugins() carrega plugins de usuário, de projeto e do pip que podem registrar ferramentas adicionais.

Checagem de disponibilidade da ferramenta (check_fn)​

Cada ferramenta pode, opcionalmente, fornecer um check_fn — um chamável que devolve True quando a ferramenta está disponível e False caso contrário. Checagens típicas incluem:

  • Chave de API presente — por exemplo, lambda: bool(os.environ.get("SERP_API_KEY")) para a busca na web
  • Serviço em execução — por exemplo, verificar se o servidor Honcho está configurado
  • Binário instalado — por exemplo, confirmar que o playwright está disponível para as ferramentas de navegador

Quando o registry.get_definitions() monta a lista de esquemas para o modelo, ele roda o check_fn() de cada ferramenta:

# Simplificado de registry.py
if entry.check_fn:
try:
available = bool(entry.check_fn())
except Exception:
available = False # Exceções = indisponível
if not available:
continue # Pula esta ferramenta por completo

Comportamentos importantes:

  • Os resultados da checagem são cacheados por chamada — se várias ferramentas compartilham o mesmo check_fn, ele roda uma vez só.
  • Exceções no check_fn() são tratadas como "indisponível" (à prova de falha).
  • O método is_toolset_available() verifica se o check_fn de um conjunto de ferramentas passa; ele é usado na exibição da interface e na resolução de conjuntos.

Resolução de conjuntos de ferramentas​

Conjuntos de ferramentas são pacotes nomeados de ferramentas. O Hermes os resolve por:

  • listas explícitas de conjuntos habilitados/desabilitados
  • presets de plataforma (hermes-cli, hermes-telegram etc.)
  • conjuntos dinâmicos de MCP
  • conjuntos curados de propósito específico, como o hermes-acp

Como o get_tool_definitions() filtra as ferramentas​

O ponto de entrada principal é o model_tools.get_tool_definitions(enabled_toolsets, disabled_toolsets, quiet_mode):

  1. Se enabled_toolsets for informado — só entram as ferramentas desses conjuntos. Cada nome de conjunto é resolvido pelo resolve_toolset(), que expande conjuntos compostos em nomes individuais de ferramenta.

  2. Se disabled_toolsets for informado — comece com TODOS os conjuntos e subtraia os desabilitados.

  3. Se nenhum dos dois — inclua todos os conjuntos conhecidos.

  4. Filtragem no registro — o conjunto resolvido de nomes de ferramenta é passado para o registry.get_definitions(), que aplica a filtragem por check_fn e devolve os esquemas no formato OpenAI.

  5. Ajuste dinâmico de esquema — depois da filtragem, os esquemas de execute_code e browser_navigate são ajustados dinamicamente para só referenciar ferramentas que de fato passaram pelo filtro (evita que o modelo alucine ferramentas indisponíveis).

Nomes antigos de conjuntos​

Nomes antigos de conjuntos, com sufixo _tools (por exemplo, web_tools, terminal_tools), são mapeados para os nomes modernos pelo _LEGACY_TOOLSET_MAP, por compatibilidade retroativa.

Despacho​

Em tempo de execução, as ferramentas são despachadas pelo registro central, com exceções no laço do agente para algumas ferramentas de nível de agente, como o tratamento de memória, de todo e de busca em sessões.

Fluxo de despacho: tool_call do modelo → execução do handler​

Quando o modelo devolve um tool_call, o fluxo é:

Resposta do modelo com tool_call
↓
laço do agente em run_agent.py
↓
model_tools.handle_function_call(name, args, task_id, user_task)
↓
[Ferramenta do laço do agente?] → tratada direto pelo laço do agente (todo, memory, session_search, delegate_task)
↓
[Pré-hook de plugin] → invoke_hook("pre_tool_call", ...)
↓
registry.dispatch(name, args, **kwargs)
↓
Busca o ToolEntry pelo nome
↓
[Handler assíncrono?] → ponte via _run_async()
[Handler síncrono?] → chamada direta
↓
Devolve a string de resultado (ou um erro em JSON)
↓
[Pós-hook de plugin] → invoke_hook("post_tool_call", ...)

Encapsulamento de erros​

Toda execução de ferramenta é envolvida em tratamento de erro em dois níveis:

  1. registry.dispatch() — captura qualquer exceção do handler e devolve {"error": "Tool execution failed: ExceptionType: message"} em JSON.

  2. handle_function_call() — envolve o despacho inteiro num segundo try/except que devolve {"error": "Error executing tool_name: message"}.

Isso garante que o modelo sempre receba uma string JSON bem formada, nunca uma exceção não tratada.

Ferramentas do laço do agente​

Quatro ferramentas são interceptadas antes do despacho pelo registro, porque precisam de estado de nível de agente (TodoStore, MemoryStore etc.):

  • todo — planejamento e acompanhamento de tarefas
  • memory — escritas em memória persistente
  • session_search — recuperação entre sessões
  • delegate_task — abre sessões de subagente

Os esquemas dessas ferramentas continuam registrados no registro (para o get_tool_definitions), mas os handlers devolvem um erro de esqueleto se o despacho de alguma forma chegar até eles diretamente.

Ponte assíncrona​

Quando o handler de uma ferramenta é assíncrono, o _run_async() faz a ponte com o caminho síncrono de despacho:

  • Caminho da CLI (sem laço em execução) — usa um laço de eventos persistente para manter vivos os clientes assíncronos em cache
  • Caminho do gateway (laço em execução) — sobe uma thread descartável com asyncio.run()
  • Threads de trabalho (ferramentas em paralelo) — usa laços persistentes por thread, guardados em armazenamento thread-local

O fluxo de aprovação DANGEROUS_PATTERNS​

A ferramenta de terminal integra um sistema de aprovação de comandos perigosos definido em tools/approval.py:

  1. Detecção por padrão — o DANGEROUS_PATTERNS é uma lista de tuplas (regex, descrição) que cobrem operações destrutivas:

    • Remoções recursivas (rm -rf)
    • Formatação de sistema de arquivos (mkfs, dd)
    • Operações SQL destrutivas (DROP TABLE, DELETE FROM sem WHERE)
    • Sobrescrita de configuração do sistema (> /etc/)
    • Manipulação de serviços (systemctl stop)
    • Execução remota de código (curl | sh)
    • Fork bombs, encerramento de processos etc.
  2. Detecção — antes de executar qualquer comando de terminal, o detect_dangerous_command(command) confere todos os padrões.

  3. Pedido de aprovação — se houver correspondência:

    • Modo CLI — um prompt interativo pede que você aprove, negue ou permita de forma permanente
    • Modo gateway — um callback assíncrono de aprovação manda o pedido para a plataforma de mensagens
    • Aprovação inteligente — opcionalmente, um LLM auxiliar pode aprovar automaticamente comandos de baixo risco que casam com os padrões (por exemplo, rm -rf node_modules/ é seguro, mas casa com "remoção recursiva")
  4. Estado da sessão — as aprovações são rastreadas por sessão. Depois que você aprova "remoção recursiva" numa sessão, os comandos rm -rf seguintes não pedem de novo.

  5. Lista de permissão permanente — a opção "permitir permanentemente" escreve o padrão no command_allowlist do config.yaml, o que faz a permissão sobreviver entre sessões.

Ambientes de terminal/execução​

O sistema de terminal aceita vários backends:

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

Ele também aceita:

  • sobrescrita de cwd por tarefa
  • gerenciamento de processos em segundo plano
  • modo PTY
  • callbacks de aprovação para comandos perigosos

Concorrência​

As chamadas de ferramenta podem rodar em sequência ou ao mesmo tempo, dependendo da mistura de ferramentas e das exigências de interação.