hermes-jev-compact 0.1.0__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.
Files changed (28) hide show
  1. hermes_jev_compact-0.1.0/.gitignore +43 -0
  2. hermes_jev_compact-0.1.0/CHANGELOG.md +55 -0
  3. hermes_jev_compact-0.1.0/LICENSE +21 -0
  4. hermes_jev_compact-0.1.0/PKG-INFO +148 -0
  5. hermes_jev_compact-0.1.0/README.md +120 -0
  6. hermes_jev_compact-0.1.0/SECURITY.md +38 -0
  7. hermes_jev_compact-0.1.0/THIRD_PARTY_NOTICES.md +32 -0
  8. hermes_jev_compact-0.1.0/llms.txt +39 -0
  9. hermes_jev_compact-0.1.0/pyproject.toml +72 -0
  10. hermes_jev_compact-0.1.0/src/hermes_jev_compact/__init__.py +88 -0
  11. hermes_jev_compact-0.1.0/src/hermes_jev_compact/adapter.py +203 -0
  12. hermes_jev_compact-0.1.0/src/hermes_jev_compact/asker.py +182 -0
  13. hermes_jev_compact-0.1.0/src/hermes_jev_compact/engine.py +427 -0
  14. hermes_jev_compact-0.1.0/src/hermes_jev_compact/plugin.yaml +10 -0
  15. hermes_jev_compact-0.1.0/src/hermes_jev_compact/protocol.py +63 -0
  16. hermes_jev_compact-0.1.0/src/hermes_jev_compact/pruner.py +481 -0
  17. hermes_jev_compact-0.1.0/src/hermes_jev_compact/py.typed +0 -0
  18. hermes_jev_compact-0.1.0/src/hermes_jev_compact/request.py +56 -0
  19. hermes_jev_compact-0.1.0/src/hermes_jev_compact/shaping.py +107 -0
  20. hermes_jev_compact-0.1.0/tests/__init__.py +0 -0
  21. hermes_jev_compact-0.1.0/tests/conftest.py +92 -0
  22. hermes_jev_compact-0.1.0/tests/test_adapter.py +189 -0
  23. hermes_jev_compact-0.1.0/tests/test_asker.py +158 -0
  24. hermes_jev_compact-0.1.0/tests/test_engine.py +576 -0
  25. hermes_jev_compact-0.1.0/tests/test_pruner.py +297 -0
  26. hermes_jev_compact-0.1.0/tests/test_register.py +60 -0
  27. hermes_jev_compact-0.1.0/tests/test_shaping.py +90 -0
  28. hermes_jev_compact-0.1.0/uv.lock +436 -0
@@ -0,0 +1,43 @@
1
+ # Python
2
+ __pycache__/
3
+ *.pyc
4
+ *.pyo
5
+ *.egg-info/
6
+ dist/
7
+ build/
8
+ .eggs/
9
+
10
+ # Virtual environments
11
+ .venv/
12
+ venv/
13
+
14
+ # Testing
15
+ .pytest_cache/
16
+ .mypy_cache/
17
+ .ruff_cache/
18
+
19
+ # IDE
20
+ .vscode/
21
+ .idea/
22
+
23
+ # OS
24
+ .DS_Store
25
+ Thumbs.db
26
+
27
+ # Runtime data (secrets, credentials, logs — NEVER commit)
28
+ **/data/
29
+ **/sockets/
30
+ *.tmp
31
+ *.lock
32
+ !**/uv.lock
33
+
34
+ # Environment
35
+ .env
36
+ .env.*
37
+
38
+ # Hermes runtime (if symlinked from install)
39
+ *.log
40
+ node_modules
41
+
42
+ # Local Hermes session artifacts (never commit)
43
+ .hermes/
@@ -0,0 +1,55 @@
1
+ # Changelog
2
+
3
+ ## [Unreleased]
4
+
5
+ ### Fixed
6
+
7
+ - Bug-hunt wave (5 review agents, all findings verified before fixing):
8
+ - UTF-16-unit parity in the estimator/truncate/abridge — emoji-heavy text
9
+ was under-counted ~2x vs the TS reference.
10
+ - TS `_compact_call` port: `key=value` per entry with raw strings (was
11
+ whole-object JSON).
12
+ - 25% minimum-reduction rule enforced in-engine (TS `reductionRatio`):
13
+ low-value Jev passes fall back to the deterministic prune.
14
+ - Duplicate tool-result ids skip scoring (was: last-row-wins collapse
15
+ while pruning every row sharing the id).
16
+ - Out-of-order pairs (result before its call) are never candidates; the
17
+ validity gate rejects them plus malformed top-level rows.
18
+ - Multimodal text parts are flattened into state (host `_part_text`
19
+ parity); image/file parts contribute nothing instead of voiding the row.
20
+ - Multimodal assistant rows survive call-stripping (were dropped as
21
+ "payload-empty").
22
+ - `apply_decisions_openai` fails closed on duplicate/conflicting decision
23
+ input instead of last-wins.
24
+ - Transport: base URLs with query strings join correctly, fragments
25
+ refused, empty userinfo refused, malformed URLs raise JevError,
26
+ timeout must be finite + positive, NaN/Infinity bodies rejected,
27
+ `noul` answers range-checked to [0,1], status code is the authority
28
+ over the legacy `ok` flag.
29
+ - Engine inherits host `quiet_mode` / `protect_last_n`; `keep_threshold`
30
+ clamped to [0,1]; question-token cache bounded (4096 entries, tuple
31
+ keys); `register()` latches only after success.
32
+
33
+ ### Changed
34
+
35
+ - Generic System One endpoint: settings are now `base_url` / `api_key_env`
36
+ (defaults `https://api.typesafe.ai/v1` / `TYPESAFE_API_KEY`) and `jev_model`
37
+ defaults to `jev-latest`. Works with TypeSafe's API or any router relaying
38
+ the same `{model, state, questions}` shape. The old `conduit_*` keys and
39
+ `CONDUIT_NEXUS_API_KEY` default are gone (pre-release rename — no
40
+ migration).
41
+ - Transport hardening: redirects refused, response bodies capped at 1 MiB,
42
+ upstream error bodies no longer echoed into logs.
43
+
44
+ ## [0.1.0] - 2026-09-18
45
+
46
+ ### Added
47
+
48
+ - Initial release: `JevContextCompressor(ContextCompressor)` engine `jev`.
49
+ - Single-seam override of `_prune_old_tool_results`: Jev keep/drop scoring
50
+ via `/v1/systemone`, validity-checked, with built-in fallback on every
51
+ failure mode (transport, validation, timeout, cancel, missing key).
52
+ - Port of fast-jev-compaction state shaping (estimator, fit stages,
53
+ questions, decisions, batching) with parity tests.
54
+ - Deterministic demote still runs after a Jev pass
55
+ (dedup/args/images/stubs).
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 TheEpTic
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,148 @@
1
+ Metadata-Version: 2.5
2
+ Name: hermes-jev-compact
3
+ Version: 0.1.0
4
+ Summary: Jev-powered smart tool-prune context engine for Hermes Agent (any OpenAI-style System One endpoint)
5
+ Project-URL: Homepage, https://github.com/TheEpTic/hermes-plugins
6
+ Project-URL: Repository, https://github.com/TheEpTic/hermes-plugins
7
+ Project-URL: Issues, https://github.com/TheEpTic/hermes-plugins/issues
8
+ Project-URL: Changelog, https://github.com/TheEpTic/hermes-plugins/blob/main/hermes-jev-compact/CHANGELOG.md
9
+ Author-email: TheEpTic <nexus@eptic.me>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: agent,compaction,context,hermes,jev,plugin,systemone
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: System :: Systems Administration
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: >=3.11
23
+ Provides-Extra: dev
24
+ Requires-Dist: black==26.5.1; extra == 'dev'
25
+ Requires-Dist: mypy==2.3.0; extra == 'dev'
26
+ Requires-Dist: pytest==9.0.3; extra == 'dev'
27
+ Description-Content-Type: text/markdown
28
+
29
+ # hermes-jev-compact
30
+
31
+ Smarter context compression for Hermes Agent: stale tool calls are scored by
32
+ [TypeSafe Jev](https://docs.typesafe.ai/) — a fast decision model built for
33
+ exactly this kind of keep-or-drop judgment — instead of being pruned by age
34
+ alone. What Jev says still matters stays; the dead weight goes.
35
+
36
+ Opt-in per profile (`context.engine: jev`). Worst case is exactly the built-in
37
+ behavior: any Jev failure — transport, validation, timeout, cancel, missing
38
+ key — falls back to the inherited deterministic prune.
39
+
40
+ ## why
41
+
42
+ The built-in compressor prunes old tool results blindly: beyond the protected
43
+ tail, everything is truncated by position. That is safe, but it throws away
44
+ results the conversation still depends on (a test failure three turns back, the
45
+ file listing that motivated the current edit) while keeping verbose output
46
+ nobody will ever reference again.
47
+
48
+ Jev fixes the targeting. For every stale tool call/result unit it answers two
49
+ calibrated questions — *does the call still matter? does its full output still
50
+ matter?* — against the whole conversation as state. The result:
51
+
52
+ - **fewer broken continuations** — results the next step actually needs survive
53
+ compression instead of being truncated by age.
54
+ - **smaller contexts** — high-confidence dead weight (passing test logs,
55
+ superseded listings, retried commands) is dropped entirely, not kept as
56
+ stubs.
57
+ - **cheap judgments, not LLM summaries** — Jev returns probabilities in
58
+ ~70–500 ms at a fraction of a cent per prune; no generative model is
59
+ consulted during the prune path.
60
+
61
+ ## how it works
62
+
63
+ `JevContextCompressor` subclasses the built-in `ContextCompressor` and
64
+ overrides exactly one seam — `_prune_old_tool_results` (full-compression
65
+ phase 1). The hot proactive path (`prune_tool_results_only`, documented
66
+ deterministic/no-LLM) is untouched: it bypasses Jev entirely.
67
+
68
+ One prune, end to end:
69
+
70
+ 1. **Candidates.** Paired tool call + result before the prune boundary, above
71
+ the char floor. Never candidates: system rows, index 0, the protected
72
+ tail, unpaired calls, unusable result shapes (bytes/numbers), duplicate or
73
+ out-of-order pairs (ambiguous address — fail closed).
74
+ 2. **State.** The whole transcript with result bodies replaced by short notes
75
+ (`ok, 9000 chars (omitted)`) is fitted into `max_state_tokens` through a
76
+ shrink ladder: cap call inputs → abridge long texts → collapse old texts →
77
+ compact old calls → drop text-only rows → merge call runs. Pinned rows
78
+ (index 0 + recent tail) shrink last.
79
+ 3. **Questions.** Two `noul` (yes/no probability) questions per call — *keep
80
+ the call? keep its full result?* — batched into `max_request_tokens` and
81
+ asked sequentially (so cancellation stops between asks).
82
+ 4. **Decisions.** `keep` / `drop_result` (truncate to a head + marker) /
83
+ `drop_call` (remove result rows, strip the call). Pinned calls always keep.
84
+ 5. **Commit gates.** Output is validity-checked (no orphans either way, no
85
+ duplicates, no out-of-order pairs, clean row shapes) and must shrink the
86
+ transcript by ≥25% (TS `reductionRatio` rule) — otherwise the deterministic
87
+ prune runs instead. The deterministic demote passes (dedup, arg truncation,
88
+ image retire, stubs) still run after a Jev pass, so nothing the built-in
89
+ compressor did is lost.
90
+
91
+ State shaping is a port of
92
+ [tamara/fast-jev-compaction](https://github.com/tamara/fast-jev-compaction)
93
+ (MIT) — see THIRD_PARTY_NOTICES.md. Deliberate divergences from upstream:
94
+ sequential batches (cancellation), OpenAI row adaptation, and the 25% rule
95
+ enforced in-engine rather than by the caller.
96
+
97
+ ## install
98
+
99
+ Works with any System One-compatible endpoint. TypeSafe's own API is the
100
+ reference: get a key at
101
+ [console.typesafe.ai](https://console.typesafe.ai/settings/keys), put it in
102
+ `~/.hermes/.env` as `TYPESAFE_API_KEY`. Self-hosted routers relaying the same
103
+ `{model, state, questions}` shape work too — just point `base_url` at them.
104
+
105
+ ```bash
106
+ /path/to/hermes-python -m pip install hermes-jev-compact
107
+ hermes plugins enable hermes-jev-compact --no-allow-tool-override
108
+ ```
109
+
110
+ Then opt in per profile and `/reset`:
111
+
112
+ ```yaml
113
+ context:
114
+ engine: jev
115
+ plugins:
116
+ entries:
117
+ hermes-jev-compact:
118
+ settings:
119
+ base_url: https://api.typesafe.ai/v1 # any POST {base}/systemone endpoint
120
+ api_key_env: TYPESAFE_API_KEY # env var holding the key
121
+ jev_model: jev-latest
122
+ keep_threshold: 0.5 # noul >= this keeps the unit
123
+ max_state_tokens: 25000 # transcript budget per request
124
+ max_request_tokens: 30000 # state + questions budget
125
+ truncate_head_chars: 300 # kept head of a dropped result
126
+ request_timeout_s: 30
127
+ min_result_chars: 8000 # results below this never become candidates
128
+ ```
129
+
130
+ `compressor` (default) bypasses plugins entirely; `jev` only activates when
131
+ named. To disable: `hermes config set context.engine compressor` (and
132
+ optionally `hermes plugins disable hermes-jev-compact`), then `/reset`.
133
+
134
+ ## observability
135
+
136
+ Per-agent counters live on the compressor: `jev_calls` (requests made),
137
+ `jev_pruned_units` (units dropped/truncated), `jev_fallbacks` (times the
138
+ built-in prune ran instead). A successful pass logs its score/drop counts and
139
+ fit stage; every fallback logs its reason.
140
+
141
+ ## development
142
+
143
+ ```bash
144
+ uv sync --extra dev --locked
145
+ uv run pytest
146
+ uv run black --check src tests
147
+ uv run mypy src
148
+ ```
@@ -0,0 +1,120 @@
1
+ # hermes-jev-compact
2
+
3
+ Smarter context compression for Hermes Agent: stale tool calls are scored by
4
+ [TypeSafe Jev](https://docs.typesafe.ai/) — a fast decision model built for
5
+ exactly this kind of keep-or-drop judgment — instead of being pruned by age
6
+ alone. What Jev says still matters stays; the dead weight goes.
7
+
8
+ Opt-in per profile (`context.engine: jev`). Worst case is exactly the built-in
9
+ behavior: any Jev failure — transport, validation, timeout, cancel, missing
10
+ key — falls back to the inherited deterministic prune.
11
+
12
+ ## why
13
+
14
+ The built-in compressor prunes old tool results blindly: beyond the protected
15
+ tail, everything is truncated by position. That is safe, but it throws away
16
+ results the conversation still depends on (a test failure three turns back, the
17
+ file listing that motivated the current edit) while keeping verbose output
18
+ nobody will ever reference again.
19
+
20
+ Jev fixes the targeting. For every stale tool call/result unit it answers two
21
+ calibrated questions — *does the call still matter? does its full output still
22
+ matter?* — against the whole conversation as state. The result:
23
+
24
+ - **fewer broken continuations** — results the next step actually needs survive
25
+ compression instead of being truncated by age.
26
+ - **smaller contexts** — high-confidence dead weight (passing test logs,
27
+ superseded listings, retried commands) is dropped entirely, not kept as
28
+ stubs.
29
+ - **cheap judgments, not LLM summaries** — Jev returns probabilities in
30
+ ~70–500 ms at a fraction of a cent per prune; no generative model is
31
+ consulted during the prune path.
32
+
33
+ ## how it works
34
+
35
+ `JevContextCompressor` subclasses the built-in `ContextCompressor` and
36
+ overrides exactly one seam — `_prune_old_tool_results` (full-compression
37
+ phase 1). The hot proactive path (`prune_tool_results_only`, documented
38
+ deterministic/no-LLM) is untouched: it bypasses Jev entirely.
39
+
40
+ One prune, end to end:
41
+
42
+ 1. **Candidates.** Paired tool call + result before the prune boundary, above
43
+ the char floor. Never candidates: system rows, index 0, the protected
44
+ tail, unpaired calls, unusable result shapes (bytes/numbers), duplicate or
45
+ out-of-order pairs (ambiguous address — fail closed).
46
+ 2. **State.** The whole transcript with result bodies replaced by short notes
47
+ (`ok, 9000 chars (omitted)`) is fitted into `max_state_tokens` through a
48
+ shrink ladder: cap call inputs → abridge long texts → collapse old texts →
49
+ compact old calls → drop text-only rows → merge call runs. Pinned rows
50
+ (index 0 + recent tail) shrink last.
51
+ 3. **Questions.** Two `noul` (yes/no probability) questions per call — *keep
52
+ the call? keep its full result?* — batched into `max_request_tokens` and
53
+ asked sequentially (so cancellation stops between asks).
54
+ 4. **Decisions.** `keep` / `drop_result` (truncate to a head + marker) /
55
+ `drop_call` (remove result rows, strip the call). Pinned calls always keep.
56
+ 5. **Commit gates.** Output is validity-checked (no orphans either way, no
57
+ duplicates, no out-of-order pairs, clean row shapes) and must shrink the
58
+ transcript by ≥25% (TS `reductionRatio` rule) — otherwise the deterministic
59
+ prune runs instead. The deterministic demote passes (dedup, arg truncation,
60
+ image retire, stubs) still run after a Jev pass, so nothing the built-in
61
+ compressor did is lost.
62
+
63
+ State shaping is a port of
64
+ [tamara/fast-jev-compaction](https://github.com/tamara/fast-jev-compaction)
65
+ (MIT) — see THIRD_PARTY_NOTICES.md. Deliberate divergences from upstream:
66
+ sequential batches (cancellation), OpenAI row adaptation, and the 25% rule
67
+ enforced in-engine rather than by the caller.
68
+
69
+ ## install
70
+
71
+ Works with any System One-compatible endpoint. TypeSafe's own API is the
72
+ reference: get a key at
73
+ [console.typesafe.ai](https://console.typesafe.ai/settings/keys), put it in
74
+ `~/.hermes/.env` as `TYPESAFE_API_KEY`. Self-hosted routers relaying the same
75
+ `{model, state, questions}` shape work too — just point `base_url` at them.
76
+
77
+ ```bash
78
+ /path/to/hermes-python -m pip install hermes-jev-compact
79
+ hermes plugins enable hermes-jev-compact --no-allow-tool-override
80
+ ```
81
+
82
+ Then opt in per profile and `/reset`:
83
+
84
+ ```yaml
85
+ context:
86
+ engine: jev
87
+ plugins:
88
+ entries:
89
+ hermes-jev-compact:
90
+ settings:
91
+ base_url: https://api.typesafe.ai/v1 # any POST {base}/systemone endpoint
92
+ api_key_env: TYPESAFE_API_KEY # env var holding the key
93
+ jev_model: jev-latest
94
+ keep_threshold: 0.5 # noul >= this keeps the unit
95
+ max_state_tokens: 25000 # transcript budget per request
96
+ max_request_tokens: 30000 # state + questions budget
97
+ truncate_head_chars: 300 # kept head of a dropped result
98
+ request_timeout_s: 30
99
+ min_result_chars: 8000 # results below this never become candidates
100
+ ```
101
+
102
+ `compressor` (default) bypasses plugins entirely; `jev` only activates when
103
+ named. To disable: `hermes config set context.engine compressor` (and
104
+ optionally `hermes plugins disable hermes-jev-compact`), then `/reset`.
105
+
106
+ ## observability
107
+
108
+ Per-agent counters live on the compressor: `jev_calls` (requests made),
109
+ `jev_pruned_units` (units dropped/truncated), `jev_fallbacks` (times the
110
+ built-in prune ran instead). A successful pass logs its score/drop counts and
111
+ fit stage; every fallback logs its reason.
112
+
113
+ ## development
114
+
115
+ ```bash
116
+ uv sync --extra dev --locked
117
+ uv run pytest
118
+ uv run black --check src tests
119
+ uv run mypy src
120
+ ```
@@ -0,0 +1,38 @@
1
+ # Security
2
+
3
+ ## trust boundary
4
+
5
+ Jev requests carry the conversation history with tool result bodies replaced
6
+ by short notes (`ok, N chars (omitted)`); message text is abridged to fit the
7
+ state budget, and call arguments are included (truncated per the fit stage).
8
+ Prompt the operator before enabling on sessions carrying secrets outside the
9
+ trust boundary of the Jev endpoint.
10
+
11
+ Any OpenAI-style `POST {base_url}/systemone` endpoint works; TypeSafe's own
12
+ API (https://api.typesafe.ai/v1) is the reference. Self-hosted routers keep
13
+ the data in-house — point `base_url` at them.
14
+
15
+ ## key handling
16
+
17
+ - The Jev API key is read at prune-call time via
18
+ `agent.secret_scope.get_secret()` (profile-scope aware, multiplex
19
+ fail-closed) and never cached on the engine instance or written to config.
20
+ The shared plugin singleton never retains it (stripped in `__init__`,
21
+ excluded from `__deepcopy__`); only per-agent copies hold the host chat key
22
+ the summary path needs, exactly like the built-in compressor.
23
+ - The transport refuses redirects, embedded credentials, non-HTTP schemes,
24
+ and cleartext HTTP off loopback — a misconfigured `base_url` fails closed
25
+ to the built-in prune instead of sending the bearer key somewhere
26
+ surprising.
27
+ - Upstream error bodies and raw transport exceptions are never echoed into
28
+ logs (status category only); response bodies are capped at 1 MiB.
29
+
30
+ ## lossiness
31
+
32
+ Pruning is lossy by design (same as the built-in compressor). Dropped tool
33
+ results are unrecoverable; dropped calls can be re-run by the agent.
34
+
35
+ ## reporting
36
+
37
+ Report vulnerabilities to the repository owner; do not open public issues
38
+ with sensitive details.
@@ -0,0 +1,32 @@
1
+ # Third-party notices
2
+
3
+ ## fast-jev-compaction
4
+
5
+ State shaping, estimator, batching, questions, and decision logic in
6
+ `hermes_jev_compact/shaping.py` and `hermes_jev_compact/pruner.py` are ported
7
+ from [tamaratran/fast-jev-compaction](https://github.com/tamaratran/fast-jev-compaction)
8
+ (© 2025), used under the MIT License:
9
+
10
+ ```
11
+ MIT License
12
+
13
+ Copyright (c) 2025
14
+
15
+ Permission is hereby granted, free of charge, to any person obtaining a copy
16
+ of this software and associated documentation files (the "Software"), to deal
17
+ in the Software without restriction, including without limitation the rights
18
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
19
+ copies of the Software, and to permit persons to whom the Software is
20
+ furnished to do so, subject to the following conditions:
21
+
22
+ The above copyright notice and this permission notice shall be included in all
23
+ copies or substantial portions of the Software.
24
+
25
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
26
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
27
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
28
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
29
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
30
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
31
+ SOFTWARE.
32
+ ```
@@ -0,0 +1,39 @@
1
+ # hermes-jev-compact
2
+
3
+ Smarter context compression for Hermes Agent (opt-in `context.engine: jev`):
4
+ stale tool call/result units are scored by TypeSafe Jev (keep / truncate /
5
+ drop by calibrated probability) instead of being pruned by age alone.
6
+ Any failure falls back to the built-in deterministic prune.
7
+
8
+ ## install
9
+
10
+ ```bash
11
+ /path/to/hermes-python -m pip install hermes-jev-compact
12
+ hermes plugins enable hermes-jev-compact --no-allow-tool-override
13
+ ```
14
+
15
+ Then set `context.engine: jev` and `/reset`. `compressor` bypasses plugins;
16
+ `jev` only activates when named. Disable with
17
+ `hermes config set context.engine compressor` + `/reset`.
18
+
19
+ ## requires
20
+
21
+ - a System One endpoint at `base_url` (default `https://api.typesafe.ai/v1`;
22
+ key from https://console.typesafe.ai/settings/keys)
23
+ - client key in `~/.hermes/.env` under `api_key_env` (default `TYPESAFE_API_KEY`)
24
+
25
+ ## settings
26
+
27
+ `plugins.entries.hermes-jev-compact.settings`: `base_url`, `api_key_env`,
28
+ `jev_model` (`jev-latest`), `keep_threshold` (0.5), `max_state_tokens` (25000),
29
+ `max_request_tokens` (30000), `truncate_head_chars` (300), `request_timeout_s`
30
+ (30), `min_result_chars` (8000).
31
+
32
+ ## behavior
33
+
34
+ Overrides only `_prune_old_tool_results` on the built-in compressor (the hot
35
+ proactive path bypasses Jev — deterministic/no-LLM contract kept). Candidates
36
+ are paired call+result units before the prune boundary; Jev scores two `noul`
37
+ questions per unit against the fitted transcript; output must pass structural
38
+ validity AND shrink ≥25% or the built-in prune runs instead. Counters
39
+ `jev_calls` / `jev_pruned_units` / `jev_fallbacks` on the compressor.
@@ -0,0 +1,72 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "hermes-jev-compact"
7
+ version = "0.1.0"
8
+ description = "Jev-powered smart tool-prune context engine for Hermes Agent (any OpenAI-style System One endpoint)"
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = "MIT"
12
+ authors = [
13
+ { name = "TheEpTic", email = "nexus@eptic.me" },
14
+ ]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Intended Audience :: Developers",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.11",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Programming Language :: Python :: 3.13",
22
+ "Topic :: System :: Systems Administration",
23
+ "Typing :: Typed",
24
+ "Operating System :: OS Independent",
25
+ ]
26
+
27
+ keywords = ["hermes", "agent", "plugin", "context", "compaction", "jev", "systemone"]
28
+
29
+ [project.urls]
30
+ Homepage = "https://github.com/TheEpTic/hermes-plugins"
31
+ Repository = "https://github.com/TheEpTic/hermes-plugins"
32
+ Issues = "https://github.com/TheEpTic/hermes-plugins/issues"
33
+ Changelog = "https://github.com/TheEpTic/hermes-plugins/blob/main/hermes-jev-compact/CHANGELOG.md"
34
+
35
+ [project.optional-dependencies]
36
+ dev = [
37
+ "black==26.5.1",
38
+ "mypy==2.3.0",
39
+ "pytest==9.0.3",
40
+ ]
41
+
42
+ [tool.hatch.build.targets.wheel]
43
+ packages = ["src/hermes_jev_compact"]
44
+
45
+ [project.entry-points."hermes_agent.plugins"]
46
+ hermes-jev-compact = "hermes_jev_compact"
47
+
48
+ [tool.black]
49
+ line-length = 100
50
+ target-version = ["py311"]
51
+
52
+ [tool.mypy]
53
+ python_version = "3.11"
54
+ strict = true
55
+ warn_return_any = true
56
+ warn_unused_configs = true
57
+ disallow_untyped_defs = true
58
+
59
+ [[tool.mypy.overrides]]
60
+ module = ["agent.*"]
61
+ ignore_missing_imports = true
62
+
63
+ [[tool.mypy.overrides]]
64
+ module = ["tests.*"]
65
+ disallow_untyped_defs = false
66
+
67
+ [tool.pytest.ini_options]
68
+ testpaths = ["tests"]
69
+ python_files = ["test_*.py"]
70
+ python_functions = ["test_*"]
71
+ addopts = "-v --tb=short --strict-markers"
72
+ pythonpath = ["src"]
@@ -0,0 +1,88 @@
1
+ """hermes-jev-compact — Jev-powered smart tool-prune context engine for Hermes.
2
+
3
+ A ContextCompressor subclass named ``jev`` that scores stale tool call/result
4
+ units with TypeSafe Jev (any OpenAI-style ``POST {base_url}/systemone``
5
+ endpoint — TypeSafe's own API at https://api.typesafe.ai/v1 is the reference)
6
+ and keeps/drops them by probability, falling back to the built-in
7
+ deterministic prune on any failure.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import logging
13
+ import math
14
+ from importlib.metadata import PackageNotFoundError
15
+ from importlib.metadata import version as distribution_version
16
+ from typing import Any
17
+
18
+ try:
19
+ __version__ = distribution_version("hermes-jev-compact")
20
+ except PackageNotFoundError:
21
+ __version__ = "0.0.0+local"
22
+ __all__ = ["__version__", "register"]
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+ _registered: bool = False
27
+
28
+
29
+ def _setting(ctx: Any, key: str, default: Any) -> Any:
30
+ """One typed coercion for ctx settings; garbage in → default out."""
31
+ try:
32
+ value = ctx.get_config(key, default)
33
+ except Exception:
34
+ return default
35
+ if isinstance(value, bool):
36
+ # bool coerces to everything (int(True) == 1); a bool setting is
37
+ # never what a str/int/float knob wants, so reject it outright.
38
+ return default
39
+ if isinstance(default, str):
40
+ return value.strip() if isinstance(value, str) and value.strip() else default
41
+ try:
42
+ out = type(default)(value)
43
+ except (TypeError, ValueError):
44
+ return default
45
+ if isinstance(out, float) and not math.isfinite(out):
46
+ return default
47
+ return out
48
+
49
+
50
+ def _build_engine(ctx: Any) -> Any:
51
+ from .engine import _JEV_KNOBS, JevContextCompressor
52
+
53
+ knobs = {attr: _setting(ctx, key, default) for attr, key, default in _JEV_KNOBS}
54
+ # Host policy the built-in constructor would receive (agent_init.py:1856+):
55
+ # the plugin singleton is built at register() time, so read the same
56
+ # config roots directly. Without this the jev engine logs in quiet mode
57
+ # and guards the wrong tail size on the fallback/proactive paths.
58
+ base: dict[str, Any] = {}
59
+ try:
60
+ from hermes_cli.config import load_config_readonly # type: ignore[import-not-found]
61
+
62
+ cfg = load_config_readonly() or {}
63
+ agent_cfg = cfg.get("agent", {}) if isinstance(cfg, dict) else {}
64
+ comp_cfg = cfg.get("compression", {}) if isinstance(cfg, dict) else {}
65
+ if isinstance(agent_cfg, dict) and isinstance(agent_cfg.get("quiet_mode"), bool):
66
+ base["quiet_mode"] = agent_cfg["quiet_mode"]
67
+ if isinstance(comp_cfg, dict) and comp_cfg.get("protect_last_n") is not None:
68
+ try:
69
+ base["protect_last_n"] = max(0, int(comp_cfg["protect_last_n"]))
70
+ except (TypeError, ValueError):
71
+ pass
72
+ except Exception:
73
+ pass
74
+ return JevContextCompressor(model="jev-latest", **base, **knobs)
75
+
76
+
77
+ def register(ctx: Any) -> None:
78
+ """Register the ``jev`` context engine (config-only singleton, no secrets)."""
79
+ global _registered
80
+ if _registered:
81
+ logger.debug("hermes-jev-compact: already registered, skipping")
82
+ return
83
+ ctx.register_context_engine(_build_engine(ctx))
84
+ # Only latch AFTER success: a failed registration must not permanently
85
+ # disable retries (a second plugin wins the single-engine slot instead —
86
+ # the host rejects it with a warning, same as any double-register).
87
+ _registered = True
88
+ logger.info("hermes-jev-compact loaded (engine: jev)")