hermeskill-hermes 0.1.0a1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,26 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ *.pyo
5
+ *.egg-info/
6
+ build/
7
+ dist/
8
+ .pytest_cache/
9
+ .mypy_cache/
10
+ .ruff_cache/
11
+ .hypothesis/
12
+ .coverage
13
+ htmlcov/
14
+ *.db
15
+ *.sqlite
16
+ .env
17
+ .env.local
18
+ ~/.hermeskill/
19
+ .idea/
20
+ .vscode/
21
+ *.log
22
+ .maestro/
23
+ .claude/settings.local.json
24
+ learn/
25
+ TODO.md
26
+ PUBLISH_READINESS.md
@@ -0,0 +1,136 @@
1
+ Metadata-Version: 2.4
2
+ Name: hermeskill-hermes
3
+ Version: 0.1.0a1
4
+ Summary: Hermeskill apoptosis plugin for Hermes Agent — install once, your agent never runs away again
5
+ Project-URL: Homepage, https://github.com/theopitori/hermeskill
6
+ Project-URL: Documentation, https://github.com/theopitori/hermeskill#readme
7
+ License: MIT
8
+ Requires-Python: >=3.11
9
+ Requires-Dist: hermeskill>=0.1.0a1
10
+ Description-Content-Type: text/markdown
11
+
12
+ # hermeskill-hermes
13
+
14
+ [Hermeskill](https://github.com/theopitori/hermeskill) apoptosis supervision
15
+ for [Hermes Agent](https://github.com/NousResearch/hermes-agent). Drops in as
16
+ a plugin: Hermeskill watches every tool call and LLM turn in your Hermes session
17
+ and terminates the agent cleanly if it enters a runaway loop, exceeds its
18
+ cost/token cap, runs past a wall-clock deadline, or calls a tool outside the
19
+ policy allowlist.
20
+
21
+ ## Install & enable (zero config)
22
+
23
+ ```bash
24
+ # Install Hermes with the Hermeskill plugin in its environment:
25
+ uv tool install hermes-agent --with hermeskill-hermes
26
+
27
+ # Install the Hermeskill CLI (`--with hermes-agent` lets enable-hermes read
28
+ # Hermes' config), then put uv's tool dir on PATH and restart your shell:
29
+ uv tool install hermeskill --with hermes-agent
30
+ uv tool update-shell
31
+
32
+ # Enable it (one shot — flips plugins.enabled in your Hermes config):
33
+ hermeskill enable-hermes
34
+ ```
35
+
36
+ That's it. **No API key, no control plane, no env vars.** Hermes auto-discovers
37
+ the plugin via the `hermes_agent.plugins` entry-point group; `hermeskill
38
+ enable-hermes` adds `hermeskill` to `plugins.enabled`. Run `hermes` and every
39
+ session is supervised. When a runaway is killed, the death certificate prints to
40
+ your terminal and saves to `~/.hermeskill/kills/`.
41
+
42
+ > **Why `hermeskill enable-hermes` and not `hermes plugins enable hermeskill`?** The
43
+ > latter (and the interactive `hermes plugins` UI) only manage **git-installed**
44
+ > plugins under `~/.hermes/plugins/` — they don't see pip/entry-point plugins
45
+ > like this one. `hermeskill enable-hermes` writes the supported `plugins.enabled`
46
+ > config key for you. (To do it by hand: add `hermeskill` to `plugins.enabled` in
47
+ > `~/.hermes/config.yaml`, Windows `%LOCALAPPDATA%\hermes\config.yaml`.)
48
+
49
+ ## Configure (optional — for a control plane)
50
+
51
+ Everything above works with nothing set. These add control-plane archival, a
52
+ fleet view, manual kill, and grants:
53
+
54
+ ```bash
55
+ export HERMESKILL_API_KEY=sk-... # ⇒ enables the control plane; unset = local-only
56
+ export HERMESKILL_BASE_URL=https://your-control-plane.example.com # default localhost:8000
57
+ export HERMESKILL_AGENT_NAME=my-coding-agent # display name
58
+ export HERMESKILL_POLICY=coding-default # policy
59
+ export HERMESKILL_LOCAL_CERT=0 # disable the local cert print/save (default: on)
60
+ ```
61
+
62
+ Or add the same keys to `~/.hermes/.env`, or run `hermeskill init` once to persist
63
+ them to `~/.hermeskill/config.toml`. With a key set, every session is also
64
+ queryable via the operator CLI (`hermeskill fleet`).
65
+
66
+ ## What it does
67
+
68
+ | Condition | What happens |
69
+ |-----------|-------------|
70
+ | Agent calls the same tool 5× in a row with identical inputs | Kill (`loop`) |
71
+ | Cumulative LLM cost exceeds policy cap | Kill (`token_runaway`) |
72
+ | Session runs longer than policy wall-clock cap | Kill (`wall_clock`) |
73
+ | Agent calls a tool not in the policy allowlist | Kill (`tool_scope_violation`) |
74
+ | Operator issues `hermeskill kill <agent_id>` | Kill (`manual_kill`) |
75
+ | Operator issues a grant | Suppress one symptom type for up to 24 h |
76
+
77
+ ## How the kill works
78
+
79
+ Hermes hooks are non-blocking — they can't raise out of the agent loop.
80
+ Hermeskill uses Hermes' canonical interception path: when an apoptosis check
81
+ fires, the plugin's `pre_tool_call` callback returns
82
+
83
+ ```python
84
+ {"action": "block", "message": "hermeskill apoptosis: <reason>. End the session."}
85
+ ```
86
+
87
+ Hermes refuses to run the tool and surfaces that message as the tool error
88
+ to the LLM. The harm is halted **immediately** — no further tool execution,
89
+ no further cost — and every subsequent tool call also blocks until the
90
+ agent's loop ends naturally. At session end, `on_session_end` fires and the
91
+ plugin posts a death certificate (full symptom log, shutdown sequence,
92
+ feedback URL) to the control plane.
93
+
94
+ This is the same pattern Hermes' built-in `security-guidance` plugin uses
95
+ for its strict block mode, and it's documented in PR #26759 as the canonical
96
+ interception path for "rate limiting, security restrictions, approval
97
+ workflows."
98
+
99
+ ## Policies
100
+
101
+ Shipped defaults:
102
+
103
+ | Policy | Loop cap | Cost cap | Wall-clock cap |
104
+ |--------|----------|----------|----------------|
105
+ | `strict` | 3 repeats / 15 actions | $2.00 | 5 min |
106
+ | `coding-default` | 5 repeats / 20 actions | $25.00 | 30 min |
107
+ | `permissive` | 10 repeats / 40 actions | $100.00 | 2 h |
108
+
109
+ ## Operator CLI
110
+
111
+ ```bash
112
+ hermeskill fleet
113
+ hermeskill logs <agent_id>
114
+ hermeskill kill <agent_id> --reason "infinite loop in file search"
115
+ hermeskill grant <agent_id> --symptoms loop --duration 1h --reason "known flaky task"
116
+ hermeskill revoke <grant_id>
117
+ ```
118
+
119
+ See the [repo root README](https://github.com/theopitori/hermeskill#readme)
120
+ for the full operator workflow, security model, and deployment guide.
121
+
122
+ ## Hermes hooks used
123
+
124
+ The plugin attaches to five hooks (see `hermes_cli/plugins.py::VALID_HOOKS`):
125
+
126
+ | Hook | Why |
127
+ |---|---|
128
+ | `pre_tool_call` | The checkpoint — runs all symptom checks; returns the block directive if armed |
129
+ | `post_tool_call` | Records tool outcome; re-runs cost/wall-clock checks |
130
+ | `pre_llm_call` | Lifecycle marker (model name) |
131
+ | `post_api_request` | Token + cost accounting (this hook carries `usage` in v0.14, not `post_llm_call`) |
132
+ | `on_session_end` | Flush death cert, tear down background worker |
133
+
134
+ ## License
135
+
136
+ [MIT](https://github.com/theopitori/hermeskill/blob/main/LICENSE) © 2026 Hermeskill Contributors
@@ -0,0 +1,38 @@
1
+ [project]
2
+ name = "hermeskill-hermes"
3
+ version = "0.1.0a1"
4
+ description = "Hermeskill apoptosis plugin for Hermes Agent — install once, your agent never runs away again"
5
+ readme = "src/hermeskill_hermes/README.md"
6
+ requires-python = ">=3.11"
7
+ license = { text = "MIT" }
8
+ dependencies = [
9
+ "hermeskill>=0.1.0a1",
10
+ ]
11
+
12
+ [project.urls]
13
+ Homepage = "https://github.com/theopitori/hermeskill"
14
+ Documentation = "https://github.com/theopitori/hermeskill#readme"
15
+
16
+ # Hermes Agent plugin auto-discovery: when this package is installed,
17
+ # Hermes finds it via importlib.metadata. The loader does
18
+ # `module = ep.load(); getattr(module, "register")` — so the entry-point
19
+ # value must resolve to the MODULE (which exposes register(ctx)), NOT the
20
+ # function itself. Pointing at `hermeskill_hermes:register` would make
21
+ # ep.load() return the function, and getattr(function, "register") is
22
+ # None → "no register() function". See hermes_cli/plugins.py
23
+ # ::_load_entrypoint_module + _load_plugin.
24
+ [project.entry-points."hermes_agent.plugins"]
25
+ hermeskill = "hermeskill_hermes"
26
+
27
+ [build-system]
28
+ requires = ["hatchling"]
29
+ build-backend = "hatchling.build"
30
+
31
+ [tool.hatch.build.targets.wheel]
32
+ packages = ["src/hermeskill_hermes"]
33
+
34
+ # Ship plugin.yaml inside the wheel so the directory-install path
35
+ # (cp -r into ~/.hermes/plugins/hermeskill/) also has a valid manifest.
36
+ # Entry-point installs don't read this file but won't reject it either.
37
+ [tool.hatch.build.targets.wheel.force-include]
38
+ "src/hermeskill_hermes/plugin.yaml" = "hermeskill_hermes/plugin.yaml"
@@ -0,0 +1,125 @@
1
+ # hermeskill-hermes
2
+
3
+ [Hermeskill](https://github.com/theopitori/hermeskill) apoptosis supervision
4
+ for [Hermes Agent](https://github.com/NousResearch/hermes-agent). Drops in as
5
+ a plugin: Hermeskill watches every tool call and LLM turn in your Hermes session
6
+ and terminates the agent cleanly if it enters a runaway loop, exceeds its
7
+ cost/token cap, runs past a wall-clock deadline, or calls a tool outside the
8
+ policy allowlist.
9
+
10
+ ## Install & enable (zero config)
11
+
12
+ ```bash
13
+ # Install Hermes with the Hermeskill plugin in its environment:
14
+ uv tool install hermes-agent --with hermeskill-hermes
15
+
16
+ # Install the Hermeskill CLI (`--with hermes-agent` lets enable-hermes read
17
+ # Hermes' config), then put uv's tool dir on PATH and restart your shell:
18
+ uv tool install hermeskill --with hermes-agent
19
+ uv tool update-shell
20
+
21
+ # Enable it (one shot — flips plugins.enabled in your Hermes config):
22
+ hermeskill enable-hermes
23
+ ```
24
+
25
+ That's it. **No API key, no control plane, no env vars.** Hermes auto-discovers
26
+ the plugin via the `hermes_agent.plugins` entry-point group; `hermeskill
27
+ enable-hermes` adds `hermeskill` to `plugins.enabled`. Run `hermes` and every
28
+ session is supervised. When a runaway is killed, the death certificate prints to
29
+ your terminal and saves to `~/.hermeskill/kills/`.
30
+
31
+ > **Why `hermeskill enable-hermes` and not `hermes plugins enable hermeskill`?** The
32
+ > latter (and the interactive `hermes plugins` UI) only manage **git-installed**
33
+ > plugins under `~/.hermes/plugins/` — they don't see pip/entry-point plugins
34
+ > like this one. `hermeskill enable-hermes` writes the supported `plugins.enabled`
35
+ > config key for you. (To do it by hand: add `hermeskill` to `plugins.enabled` in
36
+ > `~/.hermes/config.yaml`, Windows `%LOCALAPPDATA%\hermes\config.yaml`.)
37
+
38
+ ## Configure (optional — for a control plane)
39
+
40
+ Everything above works with nothing set. These add control-plane archival, a
41
+ fleet view, manual kill, and grants:
42
+
43
+ ```bash
44
+ export HERMESKILL_API_KEY=sk-... # ⇒ enables the control plane; unset = local-only
45
+ export HERMESKILL_BASE_URL=https://your-control-plane.example.com # default localhost:8000
46
+ export HERMESKILL_AGENT_NAME=my-coding-agent # display name
47
+ export HERMESKILL_POLICY=coding-default # policy
48
+ export HERMESKILL_LOCAL_CERT=0 # disable the local cert print/save (default: on)
49
+ ```
50
+
51
+ Or add the same keys to `~/.hermes/.env`, or run `hermeskill init` once to persist
52
+ them to `~/.hermeskill/config.toml`. With a key set, every session is also
53
+ queryable via the operator CLI (`hermeskill fleet`).
54
+
55
+ ## What it does
56
+
57
+ | Condition | What happens |
58
+ |-----------|-------------|
59
+ | Agent calls the same tool 5× in a row with identical inputs | Kill (`loop`) |
60
+ | Cumulative LLM cost exceeds policy cap | Kill (`token_runaway`) |
61
+ | Session runs longer than policy wall-clock cap | Kill (`wall_clock`) |
62
+ | Agent calls a tool not in the policy allowlist | Kill (`tool_scope_violation`) |
63
+ | Operator issues `hermeskill kill <agent_id>` | Kill (`manual_kill`) |
64
+ | Operator issues a grant | Suppress one symptom type for up to 24 h |
65
+
66
+ ## How the kill works
67
+
68
+ Hermes hooks are non-blocking — they can't raise out of the agent loop.
69
+ Hermeskill uses Hermes' canonical interception path: when an apoptosis check
70
+ fires, the plugin's `pre_tool_call` callback returns
71
+
72
+ ```python
73
+ {"action": "block", "message": "hermeskill apoptosis: <reason>. End the session."}
74
+ ```
75
+
76
+ Hermes refuses to run the tool and surfaces that message as the tool error
77
+ to the LLM. The harm is halted **immediately** — no further tool execution,
78
+ no further cost — and every subsequent tool call also blocks until the
79
+ agent's loop ends naturally. At session end, `on_session_end` fires and the
80
+ plugin posts a death certificate (full symptom log, shutdown sequence,
81
+ feedback URL) to the control plane.
82
+
83
+ This is the same pattern Hermes' built-in `security-guidance` plugin uses
84
+ for its strict block mode, and it's documented in PR #26759 as the canonical
85
+ interception path for "rate limiting, security restrictions, approval
86
+ workflows."
87
+
88
+ ## Policies
89
+
90
+ Shipped defaults:
91
+
92
+ | Policy | Loop cap | Cost cap | Wall-clock cap |
93
+ |--------|----------|----------|----------------|
94
+ | `strict` | 3 repeats / 15 actions | $2.00 | 5 min |
95
+ | `coding-default` | 5 repeats / 20 actions | $25.00 | 30 min |
96
+ | `permissive` | 10 repeats / 40 actions | $100.00 | 2 h |
97
+
98
+ ## Operator CLI
99
+
100
+ ```bash
101
+ hermeskill fleet
102
+ hermeskill logs <agent_id>
103
+ hermeskill kill <agent_id> --reason "infinite loop in file search"
104
+ hermeskill grant <agent_id> --symptoms loop --duration 1h --reason "known flaky task"
105
+ hermeskill revoke <grant_id>
106
+ ```
107
+
108
+ See the [repo root README](https://github.com/theopitori/hermeskill#readme)
109
+ for the full operator workflow, security model, and deployment guide.
110
+
111
+ ## Hermes hooks used
112
+
113
+ The plugin attaches to five hooks (see `hermes_cli/plugins.py::VALID_HOOKS`):
114
+
115
+ | Hook | Why |
116
+ |---|---|
117
+ | `pre_tool_call` | The checkpoint — runs all symptom checks; returns the block directive if armed |
118
+ | `post_tool_call` | Records tool outcome; re-runs cost/wall-clock checks |
119
+ | `pre_llm_call` | Lifecycle marker (model name) |
120
+ | `post_api_request` | Token + cost accounting (this hook carries `usage` in v0.14, not `post_llm_call`) |
121
+ | `on_session_end` | Flush death cert, tear down background worker |
122
+
123
+ ## License
124
+
125
+ [MIT](https://github.com/theopitori/hermeskill/blob/main/LICENSE) © 2026 Hermeskill Contributors
@@ -0,0 +1,256 @@
1
+ """Hermeskill Hermes plugin — apoptosis supervision for Hermes Agent.
2
+
3
+ Installation
4
+ ------------
5
+
6
+ pip install hermeskill-hermes
7
+
8
+ Hermes auto-discovers the plugin via the ``hermes_agent.plugins`` entry-point
9
+ group declared in this package's ``pyproject.toml``. No directory copy is
10
+ required. Plugins are opt-in, so enable it by adding ``hermeskill`` to
11
+ ``plugins.enabled`` in ``~/.hermes/config.yaml`` (Windows:
12
+ ``%LOCALAPPDATA%\\hermes\\config.yaml``)::
13
+
14
+ plugins:
15
+ enabled:
16
+ - hermeskill
17
+
18
+ Note: ``hermes plugins enable hermeskill`` and the interactive ``hermes plugins``
19
+ UI only manage git-installed plugins under ``~/.hermes/plugins/`` — they do not
20
+ list pip-installed (entry-point) plugins, which are enabled via the config key
21
+ above. The runtime loader (PluginManager.discover_and_load) still honours
22
+ ``plugins.enabled`` for entry-point plugins.
23
+
24
+ For the legacy directory-install path, copy this package into
25
+ ``~/.hermes/plugins/hermeskill/`` (``plugin.yaml`` is shipped alongside the
26
+ sources for that case).
27
+
28
+ Configuration (env vars or ``~/.hermes/.env``)
29
+ ----------------------------------------------
30
+
31
+ HERMESKILL_API_KEY — control-plane API key (OPTIONAL). Without it, Hermeskill
32
+ runs local-only: in-process symptom checks still kill
33
+ runaways and the death cert prints/saves locally; only
34
+ control-plane archival, fleet visibility, manual kill,
35
+ and grants need a key + reachable control plane.
36
+ HERMESKILL_BASE_URL — control plane URL (default: http://localhost:8000)
37
+ HERMESKILL_AGENT_NAME — display name for this session (default: "hermes")
38
+ HERMESKILL_POLICY — policy name (default: "coding-default")
39
+ HERMESKILL_LOCAL_CERT — print/save the death cert locally on a kill
40
+ (default: on; set 0 to disable)
41
+
42
+ How it works
43
+ ------------
44
+
45
+ Hermes calls ``register(ctx)`` once per session. The plugin attaches five
46
+ keyword-only hook callbacks against the real Hermes v0.14 hook API
47
+ (see ``hermes_cli/plugins.py``):
48
+
49
+ pre_tool_call — checkpoint; may return {"action": "block", ...}
50
+ post_tool_call — record outcome
51
+ pre_llm_call — lifecycle marker
52
+ post_api_request — token + cost accounting (carries the usage dict)
53
+ on_session_end — flush death cert, tear down worker
54
+
55
+ If an apoptosis condition fires (loop, cost, wall-clock, scope, manual kill),
56
+ the plugin's ``pre_tool_call`` returns Hermes' standard block directive on
57
+ every subsequent call. The agent gets a tool error response and the harm
58
+ is halted immediately — no further tool execution, no further cost. The
59
+ session ends cooperatively at the next natural turn boundary, at which
60
+ point ``on_session_end`` posts the death certificate.
61
+
62
+ Public surface
63
+ --------------
64
+
65
+ register(ctx) — Hermes plugin entry point (sync, called by runtime)
66
+ async_register(ctx) — async variant for callers inside a running loop
67
+ """
68
+
69
+ from __future__ import annotations
70
+
71
+ import logging
72
+ from typing import Any
73
+
74
+ from hermeskill.client import HermeskillClient
75
+ from hermeskill.config import SDKConfig
76
+
77
+ from hermeskill_hermes.plugin import HermeskillPlugin
78
+
79
+ logger = logging.getLogger("hermeskill_hermes")
80
+
81
+ __version__ = "0.1.0a1"
82
+
83
+ _current_plugin: HermeskillPlugin | None = None
84
+
85
+
86
+ def register(ctx: Any) -> None:
87
+ """Hermes plugin entry point. Called once by the Hermes runtime at session start.
88
+
89
+ ``ctx`` is the Hermes :class:`PluginContext` (v0.14). We use:
90
+ ctx.register_hook(event_name, callback) — wire lifecycle hooks
91
+ No other ctx surface is required for the cooperative-kill design.
92
+ """
93
+ global _current_plugin
94
+
95
+ # Resolve via SDKConfig so agent name / policy can come from
96
+ # ~/.hermeskill/config.toml (written by `hermeskill init`) or env vars, not just
97
+ # env. The adapter owns the Hermes-specific defaults when unset.
98
+ config = SDKConfig.load()
99
+ name = config.agent_name or "hermes"
100
+ policy = config.policy or "coding-default"
101
+ # No API key → run in local-only mode: in-process symptom checks still
102
+ # kill runaways and the death cert prints/saves locally; only control-plane
103
+ # archival, fleet visibility, manual kill, and grants are unavailable.
104
+ keyless = not config.api_key
105
+ client = HermeskillClient.from_config(config, allow_keyless=True)
106
+
107
+ plugin = HermeskillPlugin(
108
+ name=name,
109
+ policy=policy,
110
+ client=client,
111
+ forced_offline=keyless,
112
+ local_cert=config.local_cert,
113
+ )
114
+
115
+ # Run async setup on the plugin's own session loop thread. Hermes calls
116
+ # register() synchronously; plugin.start() blocks only the calling thread
117
+ # (never an event loop) until registration completes. Callers already
118
+ # inside a running loop should use async_register() instead, which awaits
119
+ # the same setup without blocking their loop.
120
+ plugin.start()
121
+
122
+ _current_plugin = plugin
123
+
124
+ _register_hooks(ctx)
125
+ logger.info("hermeskill: plugin registered for session (agent=%r, policy=%r)", name, policy)
126
+
127
+
128
+ async def async_register(ctx: Any) -> None:
129
+ """Async variant of register() for callers inside a running event loop."""
130
+ global _current_plugin
131
+
132
+ # Resolve via SDKConfig so agent name / policy can come from
133
+ # ~/.hermeskill/config.toml (written by `hermeskill init`) or env vars, not just
134
+ # env. The adapter owns the Hermes-specific defaults when unset.
135
+ config = SDKConfig.load()
136
+ name = config.agent_name or "hermes"
137
+ policy = config.policy or "coding-default"
138
+ keyless = not config.api_key
139
+ client = HermeskillClient.from_config(config, allow_keyless=True)
140
+
141
+ plugin = HermeskillPlugin(
142
+ name=name,
143
+ policy=policy,
144
+ client=client,
145
+ forced_offline=keyless,
146
+ local_cert=config.local_cert,
147
+ )
148
+ await plugin.astart()
149
+
150
+ _current_plugin = plugin
151
+
152
+ _register_hooks(ctx)
153
+ logger.info("hermeskill: plugin async-registered for session (agent=%r, policy=%r)", name, policy)
154
+
155
+
156
+ def _register_hooks(ctx: Any) -> None:
157
+ """Wire all five hook callbacks. Names match Hermes' VALID_HOOKS set."""
158
+ ctx.register_hook("pre_tool_call", _on_pre_tool_call)
159
+ ctx.register_hook("post_tool_call", _on_post_tool_call)
160
+ ctx.register_hook("pre_llm_call", _on_pre_llm_call)
161
+ # post_api_request (NOT post_llm_call) carries the token-usage dict in
162
+ # Hermes v0.14. post_llm_call's canonical payload is just
163
+ # {session_id, model, platform} — no usage data — so it's useless for
164
+ # cost tracking. See hermes_cli/hooks.py::_DEFAULT_PAYLOADS.
165
+ ctx.register_hook("post_api_request", _on_post_api_request)
166
+ ctx.register_hook("on_session_end", _on_session_end)
167
+
168
+
169
+ # --- hook dispatch -----------------------------------------------------------
170
+ # Hermes invokes hooks via cb(**kwargs) (plugins.py::invoke_hook line ~1559).
171
+ # All wrappers MUST be keyword-only and tolerate unknown kwargs via **_extra,
172
+ # so newer Hermes versions adding payload fields don't break us.
173
+
174
+
175
+ def _on_pre_tool_call(
176
+ *,
177
+ tool_name: str = "",
178
+ args: Any = None,
179
+ session_id: str = "",
180
+ task_id: str = "",
181
+ tool_call_id: str = "",
182
+ **_extra: Any,
183
+ ) -> dict[str, str] | None:
184
+ """Hermes invokes this before every tool call.
185
+
186
+ Returns ``{"action": "block", "message": ...}`` if apoptosis has fired
187
+ (Hermes turns that into a tool error the agent sees); ``None`` otherwise.
188
+ """
189
+ if _current_plugin is None:
190
+ return None
191
+ return _current_plugin.pre_tool_call(tool_name=tool_name, args=args)
192
+
193
+
194
+ def _on_post_tool_call(
195
+ *,
196
+ tool_name: str = "",
197
+ args: Any = None,
198
+ result: Any = None,
199
+ duration_ms: float = 0,
200
+ session_id: str = "",
201
+ task_id: str = "",
202
+ tool_call_id: str = "",
203
+ **_extra: Any,
204
+ ) -> None:
205
+ if _current_plugin is None:
206
+ return
207
+ _current_plugin.post_tool_call(tool_name=tool_name, args=args, result=result)
208
+
209
+
210
+ def _on_pre_llm_call(
211
+ *,
212
+ session_id: str = "",
213
+ user_message: Any = None,
214
+ conversation_history: Any = None,
215
+ is_first_turn: bool = False,
216
+ model: str = "",
217
+ platform: str = "",
218
+ **_extra: Any,
219
+ ) -> None:
220
+ if _current_plugin is None:
221
+ return
222
+ _current_plugin.pre_llm_call(model=model)
223
+
224
+
225
+ def _on_post_api_request(
226
+ *,
227
+ session_id: str = "",
228
+ task_id: str = "",
229
+ platform: str = "",
230
+ model: str = "",
231
+ provider: str = "",
232
+ base_url: str = "",
233
+ api_mode: str = "",
234
+ api_call_count: int = 0,
235
+ api_duration: float = 0,
236
+ finish_reason: str = "",
237
+ message_count: int = 0,
238
+ response_model: str = "",
239
+ usage: dict[str, Any] | None = None,
240
+ assistant_content_chars: int = 0,
241
+ assistant_tool_call_count: int = 0,
242
+ **_extra: Any,
243
+ ) -> None:
244
+ if _current_plugin is None:
245
+ return
246
+ _current_plugin.post_api_request(
247
+ model=model,
248
+ usage=usage or {},
249
+ api_duration=api_duration,
250
+ )
251
+
252
+
253
+ def _on_session_end(*, session_id: str = "", **_extra: Any) -> None:
254
+ if _current_plugin is None:
255
+ return
256
+ _current_plugin.session_end()