Skip to content

HERMES_TOOL_ONLY_MODE applies to cron wakes: chat-handle policy makes zero-tool scheduled turns structurally impossible #362

Description

@mostlydev

Summary

internal/driver/hermes/driver.go:200 sets HERMES_TOOL_ONLY_MODE=1 unconditionally whenever a pod declares a Discord or Slack handle. Hermes then forces tool_choice: "required" on the first LLM call of every turn (agent/chat_completion_helpers.py:817-836) — including scheduled cron invocations that have nothing to do with chat.

For an agent whose scheduled-wake contract is "if nothing is actionable, make zero tool calls and stop," this makes the contract structurally unsatisfiable: the model cannot terminate a wake without calling at least one tool, so every idle wake burns a mediated tool round (and its tokens) by construction. We spent a full day of live debugging (timeout layers, reasoning-effort propagation, provider windows) before the payload diff exposed that the caller policy itself forbade silence.

Why it's misapplied

The env is a chat-surface quality policy — "runner-native tool progress stays silent so only real replies post." That rationale is about what gets posted to a channel in response to a human message. A cron wake posts nothing; forcing a tool call there converts "no action is a good turn" into "at least one billable managed call per wake, forever."

Asks

  1. Scope tool-only mode per surface: apply it to chat-handle turns, not to INVOKE/cron-originated turns. The runner already distinguishes the two (cron wakes arrive via hermes cron run).
  2. Failing that, make it declarable: a Clawfile/pod-level knob (x-claw.tool-only-mode: chat|always|never) so an operator can express "this pod's handle is a coordination surface, not a product chat."
  3. Document that a service-level environment: HERMES_TOOL_ONLY_MODE: "" override is (or is not) honored against the driver env — merge order is currently unspecified, and Python's falsy-empty-string semantics make the empty override a natural escape hatch if it wins.

Evidence

Pod emmett-support (HANDLE slack, INVOKE cron): every scheduled wake's stored request carries tool_choice: "required" with the 16-tool manifest; direct calls to the same cllama path without tool_choice complete in ~3s and can return zero tool calls. Session history and the request payload diff are retained.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions