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.
on_session_start
Section titled “on_session_start”Source: agent/conversation_loop.py:295-300
session_id: str # unique; cron format: "cron_{job_id}_{YYYYMMDD_HHMMSS}"model: str # active model nameplatform: 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).
pre_llm_call
Section titled “pre_llm_call”Source: agent/conversation_loop.py:702-711
session_id: struser_message: strconversation_history: listis_first_turn: boolmodel: strplatform: strsender_id: strFires once per turn (before the API call loop). Return value used for context injection only. NO token data.
pre_api_request
Section titled “pre_api_request”Source: agent/conversation_loop.py:1235-1253
task_id: strsession_id: strapi_call_count: int # 0-indexed within this turnapprox_input_tokens: int # APPROXIMATE (character-based estimate)request_messages: listmodel: strprovider: strbase_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: persistedsession_id: strplatform: strmodel: strprovider: str # verbatim from gateway — NOT normalizedapi_call_count: intapi_duration: float # seconds; multiply by 1000 for msturn_id: str # v18: persistedapi_request_id: str # v18: persistedfinish_reason: strresponse_model: str # model name as reported by the provider responseusage: dict | None # CanonicalUsage (see below) or None when unavailableassistant_content_chars: intassistant_tool_call_count: intusage 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.
post_tool_call
Section titled “post_tool_call”Source: model_tools.py::_emit_post_tool_call_hook
tool_name: strargs: dictresult: str # JSON string (or plain text) returned by the tooltask_id: strsession_id: strtool_call_id: str # v18: persisted (NULL for the subagent_stop proxy row)turn_id: str # v18: persistedapi_request_id: strduration_ms: intstatus: str # "ok" / "error" / "blocked" / "cancelled" / "timeout"; ABSENT on Hermes builds before core 0d9b7132 (~2026-06-03)error_type: strerror_message: strmiddleware_trace: list[dict]Success/failure detection (ok on the tool_calls row):
statuspresent (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) recordsok=0and keeps its row."blocked"covers every pre-dispatch guard: pluginpre_tool_callblocks, scope/policy blocks, pruned-args rejection and ACP edit-approval denial.statusmissing/None (pre-0d9b7132Hermes 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 truthyerrorfails;terminalwithexit_codenot in(None, 0)fails;success is Falsefails. The row is never dropped for lack ofstatus. Known gaps vs_detect_tool_failure(fallback only):user_summaryis not treated as a failure; core’s"error"/"failed"/Errorstring heuristic is not replicated; and a bare{"success": false}fails here whereas core also needs anerror/messagekey.
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.
on_session_end
Section titled “on_session_end”Source: agent/conversation_loop.py:4692-4700
session_id: strcompleted: boolinterrupted: boolmodel: strplatform: strFires 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".
on_session_finalize
Section titled “on_session_finalize”Source: cli.py:955, gateway/run.py:9646
session_id: str | Noneplatform: strFires 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).
subagent_start
Section titled “subagent_start”Source: tools/delegate_tool.py (_build_child_agent)
parent_session_id: strparent_turn_id: strparent_subagent_id: strchild_session_id: strchild_subagent_id: str # format: "sa-{i}-{uuid8}"child_role: strchild_goal: strFires 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).
subagent_stop
Section titled “subagent_stop”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: strparent_turn_id: strchild_session_id: strchild_role: strchild_summary: strchild_status: str # "completed" | "failed" | "error" | "interrupted" | "timeout"duration_ms: intCarries 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.