Skip to content

Hook kwargs Reference

These are the actual kwargs confirmed from the Hermes Agent source. Only the ones used by this plugin are listed; unknown kwargs are absorbed by **_kw.

Source: agent/conversation_loop.py:295-300

session_id: str # unique; cron format: "cron_{job_id}_{YYYYMMDD_HHMMSS}"
model: str # active model name
platform: str # "cli" | "cron" | "telegram" | "discord" | ...

Fired once at the start of a brand-new session (not on each turn of an interactive session, not on session continuation).

Source: agent/conversation_loop.py:702-711

session_id: str
user_message: str
conversation_history: list
is_first_turn: bool
model: str
platform: str
sender_id: str

Fires once per turn (before the API call loop). Return value used for context injection only. NO token data.

Source: agent/conversation_loop.py:1235-1253

task_id: str
session_id: str
api_call_count: int # 0-indexed within this turn
approx_input_tokens: int # APPROXIMATE (character-based estimate)
request_messages: list
model: str
provider: str
base_url: str
...

Fires before each individual API call. approx_input_tokens is used only as a fallback estimator when usage=None in post_api_request.

post_api_request ← PRIMARY hook for tokens/cost/latency

Section titled “post_api_request ← PRIMARY hook for tokens/cost/latency”

Source: agent/turn_response_intake.py::_fire_post_api_request_hook

task_id: str # v18: persisted
session_id: str
platform: str
model: str
provider: str # verbatim from gateway — NOT normalized
api_call_count: int
api_duration: float # seconds; multiply by 1000 for ms
turn_id: str # v18: persisted
api_request_id: str # v18: persisted
finish_reason: str
response_model: str # model name as reported by the provider response
usage: dict | None # CanonicalUsage (see below) or None when unavailable
assistant_content_chars: int
assistant_tool_call_count: int

usage dict (from agent/usage_pricing.py::CanonicalUsage):

{
"input_tokens": int, # non-cached input tokens
"output_tokens": int,
"cache_read_tokens": int,
"cache_write_tokens": int,
"reasoning_tokens": int,
"request_count": int,
"prompt_tokens": int, # = input + cache_read + cache_write
"total_tokens": int, # = prompt + output
}

usage is None when the provider returns no usage info (some streaming providers, ACP mode). The fallback estimator in __init__.py kicks in, tokens are estimated from approx_input_tokens + assistant_content_chars/4, and the row is flagged estimated=1.

IMPORTANT: prompt_tokens is intentionally ignored in cost calculation to avoid double-counting. prompt_tokens = input + cache_read + cache_write, so using it plus the individual components would count them twice.

Source: model_tools.py::_emit_post_tool_call_hook

tool_name: str
args: dict
result: str # JSON string (or plain text) returned by the tool
task_id: str
session_id: str
tool_call_id: str # v18: persisted (NULL for the subagent_stop proxy row)
turn_id: str # v18: persisted
api_request_id: str
duration_ms: int
status: str # "ok" / "error" / "blocked" / "cancelled" / "timeout"; ABSENT on Hermes builds before core 0d9b7132 (~2026-06-03)
error_type: str
error_message: str
middleware_trace: list[dict]

Success/failure detection (ok on the tool_calls row):

  1. status present (current Hermes): ok = (status == "ok"). Core emits "ok", "error", "blocked", "cancelled" and "timeout" (agent/tool_executor.py; model_tools.py::_emit_post_tool_call_hook). Anything other than "ok" (including an unknown future value) records ok=0 and keeps its row. "blocked" covers every pre-dispatch guard: plugin pre_tool_call blocks, scope/policy blocks, pruned-args rejection and ACP edit-approval denial.
  2. status missing/None (pre-0d9b7132 Hermes only): _is_tool_ok(result, tool_name) approximates core’s _tool_result_observer_fields / agent/display.py::_detect_tool_failure: non-str, non-JSON and non-dict JSON results are ok; a truthy error fails; terminal with exit_code not in (None, 0) fails; success is False fails. The row is never dropped for lack of status. Known gaps vs _detect_tool_failure (fallback only): user_summary is not treated as a failure; core’s "error" / "failed" / Error string heuristic is not replicated; and a bare {"success": false} fails here whereas core also needs an error/message key.

Core’s own status can still flag a non-terminal result carrying "error": null as error via that string heuristic in _detect_tool_failure; we trust core regardless.

Gotcha (#120): the presence of an error key is not a failure. The foreground terminal result is always {"output", "exit_code", "error": null} — tools/terminal_tool_result.py:258 — so a failed command has error: null and only exit_code signals it. The old key-presence check recorded every successful terminal call as failed. error_type/error_message are still not persisted on the tool row.

Source: agent/conversation_loop.py:4692-4700

session_id: str
completed: bool
interrupted: bool
model: str
platform: str

Fires at the end of every run_conversation() call — once per turn in interactive CLI sessions, once per cron job execution. Status derivation: interrupted → "interrupted", completed → "ok", else → "error".

Source: cli.py:955, gateway/run.py:9646

session_id: str | None
platform: str

Fires on true session teardown. Safety net: sets status to "ok" if not already set (the on_session_end → on_session_finalize sequence ensures at least one of them fires).

Source: tools/delegate_tool.py (_build_child_agent)

parent_session_id: str
parent_turn_id: str
parent_subagent_id: str
child_session_id: str
child_subagent_id: str # format: "sa-{i}-{uuid8}"
child_role: str
child_goal: str

Fires synchronously in _build_child_agent, BEFORE the child is dispatched asynchronously — so the parent→child edge is persisted before the child’s own first post_api_request can race it. Now consumed by the plugin: db.record_subagent_start inserts a subagent_edges row keyed on child_session_id (schema v11, issue #49).

Source: tools/delegate_tool.py — the _invoke_hook("subagent_stop", …) call (exact line drifts across upstream commits; match by symbol, not line number).

parent_session_id: str
parent_turn_id: str
child_session_id: str
child_role: str
child_summary: str
child_status: str # "completed" | "failed" | "error" | "interrupted" | "timeout"
duration_ms: int

Carries child_session_id — verified in tools/delegate_tool.py’s subagent_stop invocation (child_session_id=getattr(_child_agent, "session_id", None)). An earlier version of this doc claimed subagent_stop had no child session id; that was stale and is corrected here. It does not carry child_subagent_id, and still has no token or cost data. We record a synthetic tool_calls row with tool_name="delegate_task/subagent" for proxy count, and db.record_subagent_stop finalizes the matching subagent_edges row (stopped_at + child_status) — child_session_id is the correlation key that ties this hook back to subagent_start.

hermes-telemetry is an independent community plugin for Hermes Agent. It is not affiliated with or endorsed by Nous Research.