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.
- hermes_jev_compact-0.1.0/.gitignore +43 -0
- hermes_jev_compact-0.1.0/CHANGELOG.md +55 -0
- hermes_jev_compact-0.1.0/LICENSE +21 -0
- hermes_jev_compact-0.1.0/PKG-INFO +148 -0
- hermes_jev_compact-0.1.0/README.md +120 -0
- hermes_jev_compact-0.1.0/SECURITY.md +38 -0
- hermes_jev_compact-0.1.0/THIRD_PARTY_NOTICES.md +32 -0
- hermes_jev_compact-0.1.0/llms.txt +39 -0
- hermes_jev_compact-0.1.0/pyproject.toml +72 -0
- hermes_jev_compact-0.1.0/src/hermes_jev_compact/__init__.py +88 -0
- hermes_jev_compact-0.1.0/src/hermes_jev_compact/adapter.py +203 -0
- hermes_jev_compact-0.1.0/src/hermes_jev_compact/asker.py +182 -0
- hermes_jev_compact-0.1.0/src/hermes_jev_compact/engine.py +427 -0
- hermes_jev_compact-0.1.0/src/hermes_jev_compact/plugin.yaml +10 -0
- hermes_jev_compact-0.1.0/src/hermes_jev_compact/protocol.py +63 -0
- hermes_jev_compact-0.1.0/src/hermes_jev_compact/pruner.py +481 -0
- hermes_jev_compact-0.1.0/src/hermes_jev_compact/py.typed +0 -0
- hermes_jev_compact-0.1.0/src/hermes_jev_compact/request.py +56 -0
- hermes_jev_compact-0.1.0/src/hermes_jev_compact/shaping.py +107 -0
- hermes_jev_compact-0.1.0/tests/__init__.py +0 -0
- hermes_jev_compact-0.1.0/tests/conftest.py +92 -0
- hermes_jev_compact-0.1.0/tests/test_adapter.py +189 -0
- hermes_jev_compact-0.1.0/tests/test_asker.py +158 -0
- hermes_jev_compact-0.1.0/tests/test_engine.py +576 -0
- hermes_jev_compact-0.1.0/tests/test_pruner.py +297 -0
- hermes_jev_compact-0.1.0/tests/test_register.py +60 -0
- hermes_jev_compact-0.1.0/tests/test_shaping.py +90 -0
- 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)")
|