anolisa-tokenless 0.7.14 → 0.8.0

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 (30) hide show
  1. package/README.md +206 -79
  2. package/adapters/tokenless/claude-code/.claude-plugin/plugin.json +1 -1
  3. package/adapters/tokenless/claude-code/hooks/run-hook.sh +62 -0
  4. package/adapters/tokenless/codex/.codex-plugin/plugin.json +1 -1
  5. package/adapters/tokenless/common/cosh-extension.json +4 -4
  6. package/adapters/tokenless/common/hooks/compress_response_hook.py +180 -96
  7. package/adapters/tokenless/common/hooks/compress_schema_hook.py +19 -31
  8. package/adapters/tokenless/common/hooks/hook_utils.py +240 -70
  9. package/adapters/tokenless/common/hooks/rewrite_hook.py +53 -169
  10. package/adapters/tokenless/dsh/dist/index.js +353 -264
  11. package/adapters/tokenless/dsh/package.json +2 -2
  12. package/adapters/tokenless/hermes/__init__.py +191 -357
  13. package/adapters/tokenless/hermes/plugin.yaml +2 -2
  14. package/adapters/tokenless/manifest.json +17 -1
  15. package/adapters/tokenless/openclaw/dist/index.d.ts +4 -16
  16. package/adapters/tokenless/openclaw/dist/index.js +291 -507
  17. package/adapters/tokenless/openclaw/index.ts +408 -628
  18. package/adapters/tokenless/openclaw/openclaw.plugin.json +4 -20
  19. package/adapters/tokenless/openclaw/package.json +6 -4
  20. package/adapters/tokenless/qoder/.qoder-plugin/plugin.json +1 -1
  21. package/adapters/tokenless/qwencode/hooks/run-hook.sh +62 -0
  22. package/adapters/tokenless/qwencode/qwen-extension.json +4 -4
  23. package/adapters/tokenless/qwenpaw/plugin.json +17 -0
  24. package/adapters/tokenless/qwenpaw/plugin.py +390 -0
  25. package/adapters/tokenless/qwenpaw/requirements.txt +6 -0
  26. package/adapters/tokenless/qwenpaw/scripts/detect.sh +131 -0
  27. package/adapters/tokenless/qwenpaw/scripts/install.sh +98 -0
  28. package/adapters/tokenless/qwenpaw/scripts/uninstall.sh +61 -0
  29. package/package.json +5 -5
  30. package/adapters/tokenless/common/hooks/compress_toon_hook.py +0 -174
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@anolisa/dsh-tokenless",
3
- "version": "0.7.14",
4
- "description": "Native DeepSeek Harness response compression for Tokenless",
3
+ "version": "0.8.0",
4
+ "description": "Native DeepSeek Harness lifecycle integration for Tokenless",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "exports": {
@@ -1,30 +1,11 @@
1
- """Token-Less Plugin for Hermes Agent.
2
-
3
- Combines multiple context-compression strategies into a single plugin:
4
-
5
- 1. **Response compression** ``transform_tool_result`` : compresses tool
6
- results via ``tokenless compress-response`` with per-layer thresholds
7
- (moderate for shell, zero-truncation for API tools), stripping debug
8
- fields, nulls, empty values.
9
- 2. **TOON encoding** — ``transform_tool_result`` : pipeline step after
10
- response compression; re-encodes JSON results to TOON format via
11
- ``tokenless compress-toon`` for additional token savings (15-40%)
12
- with proper stats recording and size check.
13
- 3. **Tool Ready** — ``pre_tool_call`` : environment readiness pre-check
14
- with auto-fix and skip-retry feedback for missing dependencies.
15
- 4. **Command rewriting** — ``pre_tool_call`` : blocks shell commands
16
- and suggests RTK-rewritten equivalents. Hermes's hook cannot modify
17
- arguments, so the agent must re-execute with the suggested command
18
- (one extra round-trip). Safe: ``rtk rewrite`` only does text
19
- substitution, never executes the command.
20
- 5. **Session tracking** — ``on_session_start`` : propagates agent/session
21
- IDs to tokenless stats recording.
22
-
23
- Not available in Hermes: schema compression (Hermes hooks do not expose
24
- tool schemas).
25
-
26
- Every hook degrades gracefully: if ``tokenless`` is not installed, all
27
- hooks are silently skipped.
1
+ """Tokenless lifecycle adapter for Hermes Agent.
2
+
3
+ Hermes cannot replace tool arguments on older supported releases, so PreTool
4
+ blocks a shell call and suggests the Core-rewritten command. PostTool sends the
5
+ final model-bound result to Core and applies only the returned disposition.
6
+ Schema compression is not available from the Hermes hook surface. Marker-directed
7
+ recovery uses Hermes's existing shell tool and the trusted local Tokenless CLI.
8
+ Tool Ready remains product-wide hard-disabled.
28
9
 
29
10
  Activation is controlled by the Hermes plugin system — list ``tokenless`` in
30
11
  ``plugins.enabled`` in ``config.yaml``, or enable via
@@ -36,7 +17,7 @@ from __future__ import annotations
36
17
  import json
37
18
  import logging
38
19
  import os
39
- import subprocess
20
+ import shlex
40
21
  import sys
41
22
  from typing import Any
42
23
 
@@ -107,6 +88,7 @@ def _resolve_hook_utils() -> tuple[str, list[str]]:
107
88
  # (NOT $HOME — env-controllable).
108
89
  try:
109
90
  import pwd as _pwd
91
+
110
92
  real_home = _pwd.getpwuid(os.getuid()).pw_dir
111
93
  except (ImportError, KeyError):
112
94
  real_home = ""
@@ -114,9 +96,10 @@ def _resolve_hook_utils() -> tuple[str, list[str]]:
114
96
  real_home = ""
115
97
 
116
98
  candidates = [
117
- os.path.join(_HERE, "..", "common", "hooks"), # source-tree / symlink install
118
- "/usr/share/anolisa/adapters/tokenless/common/hooks", # RPM system
119
- "/usr/local/share/anolisa/adapters/tokenless/common/hooks", # manual system
99
+ # Source-tree / symlink install.
100
+ os.path.join(_HERE, "..", "common", "hooks"),
101
+ "/usr/share/anolisa/adapters/tokenless/common/hooks", # RPM system
102
+ "/usr/local/share/anolisa/adapters/tokenless/common/hooks", # Manual system
120
103
  ]
121
104
  # XDG user data dir first (anolisa FsLayout::user precedence), then the
122
105
  # passwd-home default. XDG_DATA_HOME is env-controllable, but candidates
@@ -124,11 +107,14 @@ def _resolve_hook_utils() -> tuple[str, list[str]]:
124
107
  xdg_data = os.environ.get("XDG_DATA_HOME", "")
125
108
  if xdg_data and os.path.isabs(xdg_data):
126
109
  candidates.append(
127
- os.path.join(xdg_data, "anolisa", "adapters", "tokenless", "common", "hooks"))
110
+ os.path.join(xdg_data, "anolisa", "adapters", "tokenless", "common", "hooks")
111
+ )
128
112
  if real_home:
129
113
  candidates.append(
130
- os.path.join(real_home, ".local", "share",
131
- "anolisa", "adapters", "tokenless", "common", "hooks"))
114
+ os.path.join(
115
+ real_home, ".local", "share", "anolisa", "adapters", "tokenless", "common", "hooks"
116
+ )
117
+ )
132
118
 
133
119
  rejections: list[str] = []
134
120
  for candidate in candidates:
@@ -153,22 +139,22 @@ def _resolve_hook_utils() -> tuple[str, list[str]]:
153
139
  _HOOK_UTILS_RESOLVED, _HOOK_UTILS_CANDIDATES = _resolve_hook_utils()
154
140
 
155
141
  from hook_utils import (
156
- _TOKENLESS_FALLBACK,
157
- _TOKENLESS_LOCAL_SHARE,
158
- _TOKENLESS_LOCAL_LIB,
159
142
  _RTK_FALLBACK,
160
- _RTK_LOCAL_SHARE,
161
143
  _RTK_LOCAL_LIB,
144
+ _RTK_LOCAL_SHARE,
145
+ _TOKENLESS_FALLBACK,
146
+ _TOKENLESS_LOCAL_LIB,
147
+ _TOKENLESS_LOCAL_SHARE,
148
+ )
149
+ from hook_utils import SHELL_TOOLS as _SHELL_TOOLS_SHARED
150
+ from hook_utils import SKIP_TOOLS as _SKIP_TOOLS_SHARED
151
+ from hook_utils import (
152
+ build_post_tool_request,
153
+ build_pre_tool_request,
154
+ is_tokenless_retrieve_command,
162
155
  resolve_binary,
163
- warn as _warn_shared,
164
- try_parse_json as _try_parse_json,
165
- is_skill_file as _is_skill_file,
166
- write_context as _write_context,
167
- run as _run,
168
- parse_version as _parse_version,
169
- SKIP_TOOLS as _SKIP_TOOLS_SHARED,
170
- SHELL_TOOLS as _SHELL_TOOLS_SHARED,
171
- get_thresholds,
156
+ run_compress,
157
+ tokenless_retrieve_command_available,
172
158
  )
173
159
 
174
160
  logger = logging.getLogger(__name__)
@@ -178,26 +164,17 @@ logger = logging.getLogger(__name__)
178
164
  # ---------------------------------------------------------------------------
179
165
 
180
166
  AGENT_ID = "hermes-agent"
181
- _MIN_RESPONSE_LEN = 200
182
-
183
- # Minimum payload size for the TOON encoding step. TOON on small JSON
184
- # saves only a few characters (observed ~0.3% below ~500 chars) while
185
- # the per-event encode cost stays the same, so payloads under this
186
- # threshold keep the response-compressed form and skip TOON entirely.
187
- # Mirrors tokenless-runtime's MIN_TOON_CHARS, the default the
188
- # compress-toon CLI applies; keep the two values in sync.
189
- _MIN_TOON_CHARS = 500
167
+ _COMPRESS_TIMEOUT_SECONDS = 8
190
168
 
191
169
  _SKIP_TOOLS: set[str] = _SKIP_TOOLS_SHARED | {
192
- "session_search", "list_sessions",
170
+ "session_search",
171
+ "list_sessions",
193
172
  }
194
173
 
195
174
  # Use shared SHELL_TOOLS directly - all tools (including "terminal") are now
196
175
  # defined in the unified tool_categories.json
197
176
  _SHELL_TOOLS: set[str] = _SHELL_TOOLS_SHARED
198
177
 
199
- _MIN_RTK_VERSION = (0, 35, 0)
200
-
201
178
  # ---------------------------------------------------------------------------
202
179
  # Binary resolution (thin wrapper over shared cached resolve_binary)
203
180
  # ---------------------------------------------------------------------------
@@ -210,7 +187,14 @@ def _resolve_binary(name: str, fallback: str) -> str | None:
210
187
  """Resolve binary with hermes-specific fallback paths (cached via shared)."""
211
188
  local_bin = os.path.join(os.path.expanduser("~"), ".local", "bin", name)
212
189
  if name == "rtk":
213
- return resolve_binary(name, fallback, _RTK_LIB_FALLBACK, local_bin, _RTK_LOCAL_LIB, _RTK_LOCAL_SHARE)
190
+ return resolve_binary(
191
+ name,
192
+ fallback,
193
+ _RTK_LIB_FALLBACK,
194
+ local_bin,
195
+ _RTK_LOCAL_LIB,
196
+ _RTK_LOCAL_SHARE,
197
+ )
214
198
  return resolve_binary(name, fallback, local_bin, _TOKENLESS_LOCAL_LIB, _TOKENLESS_LOCAL_SHARE)
215
199
 
216
200
 
@@ -218,211 +202,60 @@ def _have(name: str, fallback: str) -> bool:
218
202
  return _resolve_binary(name, fallback) is not None
219
203
 
220
204
 
221
- # ---------------------------------------------------------------------------
222
- # Helpers (shared via hook_utils)
223
- # ---------------------------------------------------------------------------
224
-
225
-
226
- # ---------------------------------------------------------------------------
227
- # 1. Response Compression (via tokenless compress-response)
228
- # ---------------------------------------------------------------------------
229
-
230
-
231
- def _compress_response(
232
- tool_name: str,
233
- result: str,
234
- session_id: str,
235
- tool_call_id: str,
236
- ) -> str | None:
237
- tokenless_bin = _resolve_binary("tokenless", _TOKENLESS_FALLBACK)
238
- if not tokenless_bin:
239
- return None
240
-
241
- parsed = _try_parse_json(result)
242
- if not isinstance(parsed, (dict, list)):
243
- return None
244
-
245
- # 3-layer dispatch: shell tools use moderate truncation, API tools use zero-truncation
246
- thresholds = get_thresholds(tool_name)
247
-
248
- cmd = [
249
- tokenless_bin, "compress-response",
250
- "--agent-id", AGENT_ID,
251
- "--truncate-strings-at", str(thresholds[0]),
252
- "--truncate-arrays-at", str(thresholds[1]),
253
- "--max-depth", str(thresholds[2]),
254
- ]
255
- if session_id:
256
- cmd.extend(["--session-id", session_id])
257
- if tool_call_id:
258
- cmd.extend(["--tool-use-id", tool_call_id])
259
-
260
- proc = _run(cmd, result)
261
- if not proc or proc.returncode != 0 or not proc.stdout.strip():
262
- return None
263
-
264
- compressed = proc.stdout.strip()
265
- if len(compressed) >= len(result):
205
+ def _protocol_status(status: Any, result: str) -> str | None:
206
+ """Map Hermes status, deriving it for hosts that omit the field."""
207
+ if isinstance(status, str) and status:
208
+ return {
209
+ "ok": "success",
210
+ "success": "success",
211
+ "error": "error",
212
+ "blocked": "denied",
213
+ "denied": "denied",
214
+ "interrupted": "interrupted",
215
+ }.get(status.lower())
216
+ if status not in (None, ""):
266
217
  return None
267
- return compressed
268
-
269
-
270
- # ---------------------------------------------------------------------------
271
- # 2. TOON Encoding (via tokenless compress-toon)
272
- # ---------------------------------------------------------------------------
273
-
274
-
275
- def _encode_toon(data: str, session_id: str = "", tool_call_id: str = "") -> tuple[str, int] | None:
276
- tokenless_bin = _resolve_binary("tokenless", _TOKENLESS_FALLBACK)
277
- if not tokenless_bin:
278
- return None
279
-
280
- parsed = _try_parse_json(data)
281
- if not isinstance(parsed, (dict, list)):
282
- return None
283
-
284
- cmd = [tokenless_bin, "compress-toon", "--agent-id", AGENT_ID]
285
- if session_id:
286
- cmd.extend(["--session-id", session_id])
287
- if tool_call_id:
288
- cmd.extend(["--tool-use-id", tool_call_id])
289
-
290
- proc = _run(cmd, data, timeout=1)
291
- if not proc or proc.returncode != 0 or not proc.stdout.strip():
292
- return None
293
-
294
- toon_text = proc.stdout.strip()
295
- if len(toon_text) >= len(data):
296
- return None
297
-
298
- savings_pct = 0
299
- if len(data) > 0:
300
- savings_pct = (len(data) - len(toon_text)) * 100 // len(data)
301
-
302
- return toon_text, savings_pct
303
-
304
-
305
- # ---------------------------------------------------------------------------
306
- # 3. Tool Ready (via tokenless env-check)
307
- # ---------------------------------------------------------------------------
308
-
309
-
310
- def _env_check(tool_name: str) -> str | None:
311
- """Run tool-ready env-check and return feedback if tool is not ready."""
312
- tokenless_bin = _resolve_binary("tokenless", _TOKENLESS_FALLBACK)
313
- if not tokenless_bin:
314
- return None
315
-
316
- proc = _run([tokenless_bin, "env-check", "--tool", tool_name, "--json"], "", timeout=5)
317
- if not proc or not proc.stdout.strip():
318
- return None
319
-
320
218
  try:
321
- parsed = json.loads(proc.stdout)
219
+ parsed = json.loads(result)
322
220
  except json.JSONDecodeError:
323
- return None
221
+ return "success"
222
+ return "error" if isinstance(parsed, dict) and parsed.get("error") else "success"
324
223
 
325
- status = parsed.get("status", "UNKNOWN")
326
- if status in ("UNKNOWN", "READY"):
327
- return None
328
-
329
- # Attempt auto-fix
330
- proc = _run([tokenless_bin, "env-check", "--tool", tool_name, "--fix", "--json"], "", timeout=10)
331
- if not proc or not proc.stdout.strip():
332
- return _not_ready_msg(tool_name)
333
-
334
- try:
335
- fix_parsed = json.loads(proc.stdout)
336
- except json.JSONDecodeError:
337
- return _not_ready_msg(tool_name)
338
224
 
339
- if fix_parsed.get("status") == "READY":
340
- return None
341
-
342
- diagnostic = fix_parsed.get("diagnostic", "")
343
- return diagnostic or _not_ready_msg(tool_name)
344
-
345
-
346
- def _not_ready_msg(tool_name: str) -> str:
347
- return f"[tokenless:ready] {tool_name}: NOT_READY. Skip retry."
348
-
349
-
350
- # ---------------------------------------------------------------------------
351
- # 4. Command Rewriting (via rtk rewrite)
352
- # ---------------------------------------------------------------------------
353
-
354
-
355
- def _try_rewrite(
356
- args: Any,
357
- session_id: str,
358
- tool_call_id: str,
359
- ) -> dict[str, str] | None:
360
- """Attempt RTK command rewrite for terminal tool calls.
225
+ def _content_origin(tool_name: str) -> str:
226
+ if tool_name in _SKIP_TOOLS:
227
+ return "file_content"
228
+ if tool_name in _SHELL_TOOLS:
229
+ return "command_output"
230
+ return "api_response"
361
231
 
362
- Calls ``rtk rewrite <command>`` — a pure text substitution that never
363
- executes the command. On success, returns a block directive suggesting
364
- the rewritten command so the agent re-executes with the optimized version.
365
- """
366
- rtk_bin = _resolve_binary("rtk", _RTK_FALLBACK)
367
- if not rtk_bin:
368
- return None
369
232
 
233
+ def _output_optimization(args: Any) -> str:
234
+ """Recognize the attributed RTK wrapper in the command Hermes executed."""
370
235
  if not isinstance(args, dict):
371
- return None
372
-
373
- command = args.get("command", "")
374
- if not command:
375
- return None
376
-
377
- # Version guard — non-fatal
236
+ return "none"
237
+ command = args.get("command")
238
+ if not isinstance(command, str):
239
+ return "none"
378
240
  try:
379
- ver_proc = subprocess.run(
380
- [rtk_bin, "--version"], capture_output=True, text=True, timeout=3,
381
- )
382
- ver = _parse_version(ver_proc.stdout)
383
- if ver and ver < _MIN_RTK_VERSION:
384
- logger.warning("tokenless: rtk %s too old (need >= 0.35.0), rewrite skipped", ver_proc.stdout.strip())
385
- return None
386
- except Exception:
387
- pass
388
-
389
- # Write context file so rtk (running as proxy later) can recover IDs
390
- _write_context(AGENT_ID, session_id, tool_call_id)
391
-
392
- # Set env vars for rtk stats context
393
- env = os.environ.copy()
394
- env["TOKENLESS_AGENT_ID"] = AGENT_ID
395
- if session_id:
396
- env["TOKENLESS_SESSION_ID"] = session_id
397
- if tool_call_id:
398
- env["TOKENLESS_TOOL_USE_ID"] = tool_call_id
399
-
400
- proc = subprocess.run(
401
- [rtk_bin, "rewrite", command],
402
- capture_output=True, text=True, timeout=5, env=env,
403
- )
404
-
405
- # Exit code protocol (from rtk rewrite_cmd.rs):
406
- # 0 = rewrite available, Allow verdict (auto-allow by permission rule)
407
- # 1 = no RTK equivalent (passthrough)
408
- # 2 = deny rule matched (let Hermes handle)
409
- # 3 = Ask/Default verdict (rewrite available but permission model requires
410
- # user confirmation; in non-interactive hook context, treat as valid
411
- # rewrite since the intent is token optimization, not permission gating)
412
- if proc.returncode == 1 or proc.returncode == 2:
413
- return None
414
- if proc.returncode != 0 and proc.returncode != 3:
415
- return None
416
-
417
- rewritten = proc.stdout.strip()
418
- if not rewritten or rewritten == command:
419
- return None
420
-
421
- logger.info("tokenless: rtk rewrite %s → %s", command, rewritten)
422
- return {
423
- "action": "block",
424
- "message": f"[tokenless:rewrite] Re-execute as: {rewritten}",
425
- }
241
+ lexer = shlex.shlex(command, posix=True, punctuation_chars=True)
242
+ lexer.whitespace_split = True
243
+ tokens = list(lexer)
244
+ except ValueError:
245
+ return "none"
246
+ for index, token in enumerate(tokens):
247
+ wrapper = tokens[index : index + 6]
248
+ if len(wrapper) < 6 or token != "env":
249
+ continue
250
+ if (
251
+ wrapper[1] == f"TOKENLESS_AGENT_ID={AGENT_ID}"
252
+ and wrapper[2].startswith("TOKENLESS_SESSION_ID=")
253
+ and wrapper[3].startswith("TOKENLESS_TOOL_USE_ID=")
254
+ and wrapper[4].startswith("TOKENLESS_DATA_DIR=")
255
+ and os.path.basename(wrapper[5]) == "rtk"
256
+ ):
257
+ return "rtk"
258
+ return "none"
426
259
 
427
260
 
428
261
  # ---------------------------------------------------------------------------
@@ -446,28 +279,39 @@ def on_pre_tool_call(
446
279
  tool_call_id: str = "",
447
280
  **kwargs: Any,
448
281
  ) -> dict[str, str] | None:
449
- """Tool Ready + RTK rewrite pre-check.
450
-
451
- Step 1: env-check blocks when the tool's environment is not ready.
452
- Step 2: for ``terminal`` calls, blocks and suggests RTK-rewritten
453
- command (one extra round-trip; safe — rtk rewrite never executes).
454
- """
455
- # Step 1: env-check (all tools, needs tokenless)
456
- if _have("tokenless", _TOKENLESS_FALLBACK):
457
- if session_id:
458
- os.environ["TOKENLESS_SESSION_ID"] = str(session_id)
459
- feedback = _env_check(tool_name)
460
- if feedback:
461
- logger.info("tokenless: tool-ready blocking %s — %s", tool_name, feedback)
462
- return {"action": "block", "message": feedback}
463
-
464
- # Step 2: RTK rewrite (terminal only, needs rtk)
465
- if tool_name in _SHELL_TOOLS and _have("rtk", _RTK_FALLBACK):
466
- result = _try_rewrite(args, str(session_id), str(tool_call_id))
467
- if result:
468
- return result
469
-
470
- return None
282
+ """Ask Core for an RTK rewrite and translate it to a Hermes block."""
283
+ if tool_name not in _SHELL_TOOLS or not isinstance(args, dict):
284
+ return None
285
+ tokenless_bin = _resolve_binary("tokenless", _TOKENLESS_FALLBACK)
286
+ if not tokenless_bin:
287
+ return None
288
+ request = build_pre_tool_request(
289
+ args,
290
+ AGENT_ID,
291
+ tool_name,
292
+ "command",
293
+ str(session_id),
294
+ str(tool_call_id),
295
+ replace_arguments=False,
296
+ block_and_suggest=True,
297
+ )
298
+ result = run_compress(tokenless_bin, request, _COMPRESS_TIMEOUT_SECONDS, "pre_tool")
299
+ if not isinstance(result, dict):
300
+ return None
301
+ rewritten_args = result.get("arguments")
302
+ rewritten = rewritten_args.get("command") if isinstance(rewritten_args, dict) else None
303
+ if (
304
+ result.get("action") != "block_and_suggest"
305
+ or result.get("output_optimization") != "rtk"
306
+ or not isinstance(rewritten, str)
307
+ or rewritten == args.get("command")
308
+ ):
309
+ return None
310
+ logger.info("tokenless: Core rewrote %s", tool_name)
311
+ return {
312
+ "action": "block",
313
+ "message": f"[tokenless:rewrite] Re-execute as: {rewritten}",
314
+ }
471
315
 
472
316
 
473
317
  def on_transform_tool_result(
@@ -478,83 +322,81 @@ def on_transform_tool_result(
478
322
  session_id: str = "",
479
323
  tool_call_id: str = "",
480
324
  duration_ms: int = 0,
325
+ status: str = "",
481
326
  **kwargs: Any,
482
327
  ) -> str | None:
483
- """Response compression + TOON encoding pipeline.
484
-
485
- Replaces the tool result string with a compressed/TOON-encoded version.
486
- Runs after post_tool_call; first valid string return wins.
487
-
488
- Content retrieval tools (Read/Glob/Grep) are skipped entirely.
489
- Shell/exec tools (Bash/Shell) use moderate truncation (64K/128/8).
490
- All other tools use zero-truncation compress-response + TOON.
491
- """
492
- if not _have("tokenless", _TOKENLESS_FALLBACK):
328
+ """Send one final Hermes result to Core and apply its disposition."""
329
+ if not isinstance(result, str):
493
330
  return None
494
-
495
- # Content retrieval — skip entirely (preserve integrity)
496
- if tool_name in _SKIP_TOOLS:
331
+ protocol_status = _protocol_status(status, result)
332
+ if protocol_status is None:
497
333
  return None
498
-
499
- if not result or result in ("{}", "[]"):
500
- return None
501
-
502
- # Skip skill files (YAML frontmatter)
503
- if _is_skill_file(result):
504
- return None
505
-
506
- # Skip small responses
507
- if len(result) < _MIN_RESPONSE_LEN:
334
+ tokenless_bin = _resolve_binary("tokenless", _TOKENLESS_FALLBACK)
335
+ if not tokenless_bin:
508
336
  return None
337
+ output_optimization = _output_optimization(args)
338
+ retrieve_result = protocol_status == "success" and is_tokenless_retrieve_command(
339
+ tool_name, args
340
+ )
509
341
 
510
- # Validate it's JSON
511
- parsed = _try_parse_json(result)
512
- if parsed is None:
342
+ # Hermes's terminal tool returns a JSON envelope whose `output` field is
343
+ # the model-visible command output. Compress that field so structured JSON
344
+ # produced by the command remains visible to JsonCompressor, then restore
345
+ # the host envelope below. Other tools already expose their model-bound
346
+ # result directly and must keep the existing path.
347
+ shell_envelope = None
348
+ content = result
349
+ if tool_name in _SHELL_TOOLS:
350
+ try:
351
+ parsed_result = json.loads(result)
352
+ except json.JSONDecodeError:
353
+ parsed_result = None
354
+ if isinstance(parsed_result, dict) and isinstance(parsed_result.get("output"), str):
355
+ shell_envelope = parsed_result
356
+ content = parsed_result["output"]
357
+
358
+ request = build_post_tool_request(
359
+ content,
360
+ AGENT_ID,
361
+ tool_name,
362
+ protocol_status,
363
+ _content_origin(tool_name),
364
+ output_optimization,
365
+ result_kind="retrieve" if retrieve_result else "tool",
366
+ recovery={
367
+ "kind": (
368
+ "shell"
369
+ if (
370
+ protocol_status == "success"
371
+ and output_optimization == "none"
372
+ and not retrieve_result
373
+ and tokenless_retrieve_command_available()
374
+ )
375
+ else "none"
376
+ )
377
+ },
378
+ session_id=str(session_id),
379
+ tool_use_id=str(tool_call_id),
380
+ replace_output=True,
381
+ replace_with_text=True,
382
+ )
383
+ response = run_compress(tokenless_bin, request, _COMPRESS_TIMEOUT_SECONDS, "post_tool")
384
+ if not isinstance(response, dict):
513
385
  return None
514
-
515
- # Normalize: result is already a JSON string (Hermes tool contract)
516
- original = result
517
- original_len = len(original)
518
-
519
- # Step 1: Response compression (per-layer thresholds via get_thresholds)
520
- compressed = _compress_response(tool_name, result,
521
- str(session_id), str(tool_call_id))
522
- current = compressed if compressed else result
523
-
524
- # Step 2: TOON encoding — only for payloads at or above the minimum
525
- # threshold; small JSON gains near-zero chars from TOON but would still
526
- # pay the full encode cost on every tool result.
527
- toon_result = None
528
- if len(current) >= _MIN_TOON_CHARS:
529
- toon_result = _encode_toon(current, str(session_id), str(tool_call_id))
530
- used_compression = compressed is not None
531
- used_toon = toon_result is not None
532
-
533
- if not used_compression and not used_toon:
386
+ if response.get("disposition") == "applied":
387
+ output = response.get("output")
388
+ if isinstance(output, str):
389
+ logger.info("tokenless: Core optimized %s", tool_name)
390
+ if shell_envelope is not None:
391
+ shell_envelope["output"] = output
392
+ return json.dumps(shell_envelope, ensure_ascii=False)
393
+ return output
534
394
  return None
535
-
536
- # Build final output
537
- if used_toon:
538
- toon_text, savings_pct = toon_result
539
- final = toon_text
540
- final_len = len(toon_text)
541
- savings_label = (
542
- "response compressed + TOON encoded"
543
- if used_compression
544
- else "TOON encoded"
545
- )
546
- else:
547
- final = current # type: ignore[assignment]
548
- final_len = len(final)
549
- savings_pct = (original_len - final_len) * 100 // original_len if original_len else 0
550
- savings_label = "response compressed"
551
-
552
- logger.info(
553
- "tokenless: %s %s: %d -> %d chars (%d%% reduction)",
554
- savings_label, tool_name, original_len, final_len, savings_pct,
555
- )
556
-
557
- return final
395
+ if response.get("disposition") == "tool_error":
396
+ additional_context = response.get("additional_context")
397
+ if isinstance(additional_context, str) and additional_context:
398
+ return f"{result}\n\n{additional_context}"
399
+ return None
558
400
 
559
401
 
560
402
  # ---------------------------------------------------------------------------
@@ -569,16 +411,8 @@ def register(ctx: Any) -> None:
569
411
  ctx.register_hook("pre_tool_call", on_pre_tool_call)
570
412
  ctx.register_hook("transform_tool_result", on_transform_tool_result)
571
413
 
572
- # Log what's active
573
- features: list[str] = []
574
- if _have("tokenless", _TOKENLESS_FALLBACK):
575
- features.append("response-compression")
576
- features.append("toon-encoding")
577
- features.append("tool-ready")
578
- if _have("rtk", _RTK_FALLBACK):
579
- features.append("rtk-rewrite")
580
-
414
+ features = ["pre-tool", "post-tool"] if _have("tokenless", _TOKENLESS_FALLBACK) else []
581
415
  logger.info(
582
416
  "tokenless: Hermes plugin registered — active features: %s",
583
- ", ".join(features) if features else "none (install tokenless/rtk binary)",
417
+ ", ".join(features) if features else "none (install tokenless binary)",
584
418
  )
@@ -1,6 +1,6 @@
1
1
  name: tokenless
2
- version: "0.7.14"
3
- description: "Token-Less context compression for Hermes Agent — response compression, TOON encoding, command rewriting, and registered but hard-disabled Tool Ready"
2
+ version: "0.8.0"
3
+ description: "Tokenless lifecycle integration for Hermes Agent — Core-owned command rewriting and result optimization"
4
4
  author: ANOLISA
5
5
  requires_env: []
6
6
  provides_hooks: