anolisa-tokenless 0.7.13 → 0.7.14
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.
- package/README.md +13 -8
- package/adapters/tokenless/claude-code/.claude-plugin/plugin.json +1 -1
- package/adapters/tokenless/codex/.codex-plugin/plugin.json +5 -4
- package/adapters/tokenless/codex/README.md +22 -32
- package/adapters/tokenless/codex/hooks/hooks.json +3 -3
- package/adapters/tokenless/codex/scripts/response-diagnostics +170 -0
- package/adapters/tokenless/common/cosh-extension.json +1 -1
- package/adapters/tokenless/common/hooks/compress_response_hook.py +123 -264
- package/adapters/tokenless/common/hooks/compress_schema_hook.py +35 -31
- package/adapters/tokenless/common/hooks/compress_toon_hook.py +3 -0
- package/adapters/tokenless/common/hooks/hook_utils.py +70 -3
- package/adapters/tokenless/dsh/package.json +1 -1
- package/adapters/tokenless/hermes/__init__.py +2 -0
- package/adapters/tokenless/hermes/plugin.yaml +1 -1
- package/adapters/tokenless/manifest.json +2 -3
- package/adapters/tokenless/openclaw/openclaw.plugin.json +1 -1
- package/adapters/tokenless/openclaw/package.json +1 -1
- package/adapters/tokenless/qoder/.qoder-plugin/plugin.json +1 -1
- package/adapters/tokenless/qwencode/qwen-extension.json +1 -1
- package/package.json +5 -5
- package/adapters/tokenless/codex/scripts/compress-response +0 -445
|
@@ -1,20 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""Tokenless response compression hook for Cosh-NG, Claude Code, Qoder, and OpenCode.
|
|
3
3
|
|
|
4
|
-
Reads a PostToolUse JSON from stdin,
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
- Other tools -> zero-truncation compress-response + TOON
|
|
15
|
-
3. Strip debug fields, nulls, empty values (no truncation risk)
|
|
16
|
-
4. If the compressed result is still valid JSON, encode to TOON format
|
|
17
|
-
5. Stats are recorded automatically by tokenless CLI commands.
|
|
4
|
+
Reads a PostToolUse JSON from stdin, forwards the model-visible tool
|
|
5
|
+
response to the unified ``tokenless compress`` entry point (protocol v1,
|
|
6
|
+
roadmap §5.4), and translates the CompressionResponse into the host's
|
|
7
|
+
envelope. JSON detection, tool threshold selection, TOON selection, and
|
|
8
|
+
final acceptance all live behind the entry point; this hook only parses the
|
|
9
|
+
host object, declares capabilities, and builds envelopes (§4.5).
|
|
10
|
+
|
|
11
|
+
One Tokenless subprocess per invocation (§5.6). Environment-error
|
|
12
|
+
attribution stays hook-side: it is genuinely additive diagnostics, not a
|
|
13
|
+
compression decision.
|
|
18
14
|
|
|
19
15
|
Hook point: **PostToolUse**
|
|
20
16
|
|
|
@@ -29,18 +25,17 @@ Output contract per agent:
|
|
|
29
25
|
- qoder-cli: the compressed payload replaces the response via the string
|
|
30
26
|
field ``hookSpecificOutput.updatedToolOutput``. Structured responses are
|
|
31
27
|
serialized as compact JSON because Qoder rejects object and array values.
|
|
32
|
-
Qoder supports replacement for every tool, so compressed data is never
|
|
33
|
-
appended beside the original.
|
|
34
28
|
- opencode: the adapter translates ``updatedToolOutput`` to OpenCode's
|
|
35
|
-
mutable ``tool.execute.after`` output.
|
|
36
|
-
reserved for additive readiness and environment diagnostics.
|
|
29
|
+
mutable ``tool.execute.after`` output.
|
|
37
30
|
- cosh-ng: the compressed payload replaces the response via
|
|
38
31
|
``hookSpecificOutput.updatedToolResponse``. Extract only ``llmContent``
|
|
39
|
-
from wrapped responses; never include ``returnDisplay``.
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
32
|
+
from wrapped responses; never include ``returnDisplay``. Unsupported
|
|
33
|
+
Cosh-NG versions fail open with compression disabled.
|
|
34
|
+
- other agents (additionalContext-only hosts): passthrough. Additive
|
|
35
|
+
injection would append the compressed copy beside the still-visible
|
|
36
|
+
original — a net token increase — so hosts without true output
|
|
37
|
+
replacement remain passthrough (roadmap §7). Environment attribution is
|
|
38
|
+
still injected: it is additive by design.
|
|
44
39
|
|
|
45
40
|
The agent ID is read from the TOKENLESS_AGENT_ID environment variable
|
|
46
41
|
(set by the install action script). When running under Cosh-NG, the
|
|
@@ -62,30 +57,33 @@ from hook_utils import (
|
|
|
62
57
|
_TOKENLESS_LOCAL_LIB,
|
|
63
58
|
_TOKENLESS_LOCAL_SHARE,
|
|
64
59
|
SKIP_TOOLS,
|
|
60
|
+
build_compression_request,
|
|
65
61
|
classify_env_error,
|
|
66
62
|
detect_cosh_ng_runtime,
|
|
67
|
-
get_thresholds,
|
|
68
63
|
is_skill_file,
|
|
69
64
|
parse_version,
|
|
70
65
|
resolve_agent_id,
|
|
71
66
|
resolve_binary,
|
|
72
67
|
resolve_tool_call_id,
|
|
68
|
+
run_compress,
|
|
73
69
|
secure_write_text,
|
|
74
70
|
skip,
|
|
75
71
|
try_parse_json,
|
|
76
|
-
unwrap_string_json,
|
|
77
72
|
warn,
|
|
78
73
|
)
|
|
79
74
|
|
|
80
75
|
# -- constants ---------------------------------------------------------------
|
|
81
76
|
|
|
77
|
+
# Spawn-avoidance mirror of the entry point's 200-char gate. The authority
|
|
78
|
+
# lives in Rust; skipping here only saves the subprocess for content the
|
|
79
|
+
# entry would pass through anyway (normalization never grows the char
|
|
80
|
+
# count, so raw < 200 implies normalized < 200).
|
|
82
81
|
_MIN_RESPONSE_CHARS = 200
|
|
83
82
|
|
|
84
|
-
#
|
|
85
|
-
#
|
|
86
|
-
#
|
|
87
|
-
|
|
88
|
-
_MIN_TOON_CHARS = 500
|
|
83
|
+
# Below the qwen/cosh extension manifests' 10 s host wrapper so a
|
|
84
|
+
# pathological input is killed here (fail-open skip) before the host kills
|
|
85
|
+
# the whole hook.
|
|
86
|
+
_COMPRESS_TIMEOUT = 8
|
|
89
87
|
|
|
90
88
|
# Claude Code added hookSpecificOutput.updatedToolOutput (normal-path tool
|
|
91
89
|
# output replacement for all tools) in v2.1.121. Older versions only support
|
|
@@ -106,17 +104,6 @@ _CLAUDE_VERSION_CACHE = os.path.join(
|
|
|
106
104
|
# -- helpers -------------------------------------------------------------------
|
|
107
105
|
|
|
108
106
|
|
|
109
|
-
def _build_additional_context(
|
|
110
|
-
content: str,
|
|
111
|
-
env_attribution: str = "",
|
|
112
|
-
) -> str:
|
|
113
|
-
parts = []
|
|
114
|
-
if env_attribution:
|
|
115
|
-
parts.append(env_attribution)
|
|
116
|
-
parts.append(content)
|
|
117
|
-
return "\n".join(parts)
|
|
118
|
-
|
|
119
|
-
|
|
120
107
|
def _emit(output: dict) -> None:
|
|
121
108
|
print(json.dumps(output, ensure_ascii=False))
|
|
122
109
|
|
|
@@ -181,8 +168,8 @@ def _cached_claude_version(claude_bin: str) -> tuple | None:
|
|
|
181
168
|
def _claude_supports_replacement() -> bool:
|
|
182
169
|
"""Whether the running Claude Code supports updatedToolOutput (>= 2.1.121).
|
|
183
170
|
|
|
184
|
-
Returns False when the version cannot be determined; the
|
|
185
|
-
|
|
171
|
+
Returns False when the version cannot be determined; the hook then
|
|
172
|
+
declares no replacement capability, so unknown versions never receive a
|
|
186
173
|
duplicate compressed payload through additionalContext.
|
|
187
174
|
"""
|
|
188
175
|
claude_bin = resolve_binary("claude")
|
|
@@ -192,74 +179,9 @@ def _claude_supports_replacement() -> bool:
|
|
|
192
179
|
return ver is not None and ver >= _CLAUDE_MIN_REPLACE_VERSION
|
|
193
180
|
|
|
194
181
|
|
|
195
|
-
def _restore_dropped_schema_fields(original: dict, compressed: dict) -> dict:
|
|
196
|
-
"""Restore top-level keys dropped by compression when originally empty.
|
|
197
|
-
|
|
198
|
-
compress-response drops nulls, empty values ("" / {} / []) and configured
|
|
199
|
-
debug fields. Built-in Claude Code tools expect a stable output schema
|
|
200
|
-
(e.g. Bash: stdout/stderr/interrupted/isImage), so cheap empty fields are
|
|
201
|
-
restored for updatedToolOutput; intentionally dropped non-empty debug
|
|
202
|
-
payloads stay dropped.
|
|
203
|
-
"""
|
|
204
|
-
restored = dict(compressed)
|
|
205
|
-
for key, value in original.items():
|
|
206
|
-
if key in restored:
|
|
207
|
-
continue
|
|
208
|
-
if value is None or value == "" or value == {} or value == []:
|
|
209
|
-
restored[key] = value
|
|
210
|
-
return restored
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
def _build_replacement_output(
|
|
214
|
-
tool_response_raw: object,
|
|
215
|
-
tool_response: str,
|
|
216
|
-
compressed: str,
|
|
217
|
-
final_output: str,
|
|
218
|
-
used_resp_compression: bool,
|
|
219
|
-
) -> tuple[bool, object]:
|
|
220
|
-
"""Build a schema-safe replacement for runtimes that support one."""
|
|
221
|
-
if not isinstance(tool_response_raw, (dict, list)):
|
|
222
|
-
return True, final_output
|
|
223
|
-
|
|
224
|
-
# TOON text cannot replace a structured response without changing the
|
|
225
|
-
# host tool schema, so this path requires a real JSON compression win.
|
|
226
|
-
if not used_resp_compression:
|
|
227
|
-
return False, None
|
|
228
|
-
|
|
229
|
-
compressed_parsed = try_parse_json(compressed)
|
|
230
|
-
if isinstance(tool_response_raw, dict) and isinstance(compressed_parsed, dict):
|
|
231
|
-
updated_output = _restore_dropped_schema_fields(
|
|
232
|
-
tool_response_raw, compressed_parsed
|
|
233
|
-
)
|
|
234
|
-
elif compressed_parsed is not None:
|
|
235
|
-
updated_output = compressed_parsed
|
|
236
|
-
else:
|
|
237
|
-
return False, None
|
|
238
|
-
|
|
239
|
-
# Restoring empty schema fields can cancel out a marginal win.
|
|
240
|
-
# ensure_ascii=False keeps the size comparison in Unicode characters,
|
|
241
|
-
# consistent with the non-escaped normalization below.
|
|
242
|
-
serialized = json.dumps(
|
|
243
|
-
updated_output, separators=(",", ":"), ensure_ascii=False
|
|
244
|
-
)
|
|
245
|
-
if len(serialized) >= len(tool_response):
|
|
246
|
-
return False, None
|
|
247
|
-
return True, updated_output
|
|
248
|
-
|
|
249
|
-
|
|
250
182
|
# -- main --------------------------------------------------------------------
|
|
251
183
|
|
|
252
184
|
|
|
253
|
-
def _warn_subprocess(label: str, proc: subprocess.CompletedProcess) -> None:
|
|
254
|
-
"""Log a non-zero subprocess exit with truncated stderr."""
|
|
255
|
-
detail = (proc.stderr or "").strip()[:200]
|
|
256
|
-
warn(
|
|
257
|
-
f"{label} exited {proc.returncode}: {detail}"
|
|
258
|
-
if detail
|
|
259
|
-
else f"{label} exited {proc.returncode} with empty stderr"
|
|
260
|
-
)
|
|
261
|
-
|
|
262
|
-
|
|
263
185
|
def main() -> None:
|
|
264
186
|
# 1. Detect runtime (Cosh-NG vs copilot-shell)
|
|
265
187
|
cosh_ng_version = detect_cosh_ng_runtime()
|
|
@@ -288,15 +210,12 @@ def main() -> None:
|
|
|
288
210
|
warn("failed to read PostToolUse payload. Passing through unchanged.")
|
|
289
211
|
skip()
|
|
290
212
|
|
|
291
|
-
# 5. Extract tool_name (skip-tools handled after attribution)
|
|
292
213
|
tool_name = input_data.get("tool_name", "unknown")
|
|
293
|
-
|
|
294
|
-
# 6. Extract tool_response
|
|
295
214
|
tool_response_raw = input_data.get("tool_response", "")
|
|
296
215
|
if not tool_response_raw or tool_response_raw == "{}":
|
|
297
216
|
skip()
|
|
298
217
|
|
|
299
|
-
#
|
|
218
|
+
# 5. For Cosh-NG, extract only llmContent from the wrapped response.
|
|
300
219
|
# Never include returnDisplay in the provider-visible replacement.
|
|
301
220
|
llm_content = None
|
|
302
221
|
if isinstance(tool_response_raw, dict):
|
|
@@ -304,201 +223,141 @@ def main() -> None:
|
|
|
304
223
|
if llm_content is None:
|
|
305
224
|
llm_content = tool_response_raw.get("returnDisplay")
|
|
306
225
|
elif isinstance(tool_response_raw, str):
|
|
307
|
-
# Try to parse as the {llmContent, returnDisplay} wrapper
|
|
308
226
|
parsed_wrapper = try_parse_json(tool_response_raw)
|
|
309
227
|
if isinstance(parsed_wrapper, dict) and "llmContent" in parsed_wrapper:
|
|
310
228
|
llm_content = parsed_wrapper["llmContent"]
|
|
311
229
|
|
|
312
|
-
# The model-visible content we will
|
|
230
|
+
# The model-visible content we will send for compression
|
|
313
231
|
model_visible_before = llm_content if llm_content is not None else tool_response_raw
|
|
314
232
|
|
|
315
|
-
#
|
|
233
|
+
# 6. Skip skill files (YAML frontmatter). Spawn avoidance only: they are
|
|
234
|
+
# never JSON, so the entry point would pass them through anyway.
|
|
316
235
|
if isinstance(model_visible_before, str) and is_skill_file(model_visible_before):
|
|
317
236
|
skip()
|
|
318
237
|
|
|
319
|
-
#
|
|
238
|
+
# 7. Copy the model-visible value into the request content (§4.5).
|
|
239
|
+
# ensure_ascii=False matches the entry point's normalization, so size
|
|
240
|
+
# gates measure Unicode characters on both sides.
|
|
320
241
|
if isinstance(model_visible_before, str):
|
|
321
|
-
|
|
322
|
-
if not unwrapped:
|
|
323
|
-
skip() # Plain text, not JSON
|
|
324
|
-
tool_response = unwrapped
|
|
242
|
+
content = model_visible_before
|
|
325
243
|
elif isinstance(model_visible_before, (dict, list)):
|
|
326
|
-
|
|
327
|
-
# characters (code points), not \uXXXX escape sequences, so
|
|
328
|
-
# structured payloads are measured the same way as JSON string
|
|
329
|
-
# inputs and the OpenClaw adapter.
|
|
330
|
-
tool_response = json.dumps(
|
|
244
|
+
content = json.dumps(
|
|
331
245
|
model_visible_before, separators=(",", ":"), ensure_ascii=False
|
|
332
246
|
)
|
|
333
247
|
else:
|
|
334
248
|
skip()
|
|
335
249
|
|
|
336
|
-
#
|
|
337
|
-
parsed = try_parse_json(tool_response)
|
|
338
|
-
if parsed is None:
|
|
339
|
-
skip()
|
|
340
|
-
|
|
341
|
-
# 11. Extract caller context
|
|
250
|
+
# 8. Extract caller context
|
|
342
251
|
session_id = input_data.get("session_id", "")
|
|
343
252
|
tool_use_id = resolve_tool_call_id(agent_id, input_data)
|
|
344
253
|
|
|
345
|
-
#
|
|
254
|
+
# 9. Environment attribution analysis — additive diagnostics, computed
|
|
255
|
+
# hook-side. Only structured payloads are classified (with the same
|
|
256
|
+
# string-unwrap the entry point applies): plain text never reached
|
|
257
|
+
# attribution in the two-subprocess hook and still does not.
|
|
258
|
+
if isinstance(model_visible_before, dict):
|
|
259
|
+
attr_subject = model_visible_before
|
|
260
|
+
else:
|
|
261
|
+
parsed = try_parse_json(content)
|
|
262
|
+
if isinstance(parsed, str):
|
|
263
|
+
parsed = try_parse_json(parsed)
|
|
264
|
+
attr_subject = parsed if isinstance(parsed, (dict, list)) else None
|
|
346
265
|
env_attribution = ""
|
|
347
|
-
attr_category, attr_fix_hint = classify_env_error(
|
|
266
|
+
attr_category, attr_fix_hint = classify_env_error(attr_subject)
|
|
348
267
|
if attr_category:
|
|
349
268
|
env_attribution = (
|
|
350
269
|
f"[tokenless:env] {tool_name} failed: "
|
|
351
270
|
f"{attr_category} ({attr_fix_hint}). Skip retry."
|
|
352
271
|
)
|
|
353
272
|
|
|
354
|
-
#
|
|
355
|
-
if
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
compressed = tool_response
|
|
366
|
-
used_resp_compression = False
|
|
367
|
-
|
|
368
|
-
if isinstance(parsed, (dict, list)):
|
|
369
|
-
thresholds = get_thresholds(tool_name)
|
|
370
|
-
cmd = [
|
|
371
|
-
tokenless_bin, "compress-response",
|
|
372
|
-
"--agent-id", agent_id,
|
|
373
|
-
"--truncate-strings-at", str(thresholds[0]),
|
|
374
|
-
"--truncate-arrays-at", str(thresholds[1]),
|
|
375
|
-
"--max-depth", str(thresholds[2]),
|
|
376
|
-
]
|
|
377
|
-
if session_id:
|
|
378
|
-
cmd.extend(["--session-id", session_id])
|
|
379
|
-
if tool_use_id:
|
|
380
|
-
cmd.extend(["--tool-use-id", tool_use_id])
|
|
381
|
-
|
|
382
|
-
try:
|
|
383
|
-
proc = subprocess.run(
|
|
384
|
-
cmd,
|
|
385
|
-
input=tool_response,
|
|
386
|
-
capture_output=True, text=True, timeout=3,
|
|
387
|
-
)
|
|
388
|
-
if proc.returncode == 0 and proc.stdout.strip():
|
|
389
|
-
candidate = proc.stdout.strip()
|
|
390
|
-
# Compare against actual model-visible before size
|
|
391
|
-
if len(candidate) < len(tool_response):
|
|
392
|
-
compressed = candidate
|
|
393
|
-
used_resp_compression = True
|
|
394
|
-
elif proc.returncode != 0:
|
|
395
|
-
_warn_subprocess("compress-response", proc)
|
|
396
|
-
except Exception as e:
|
|
397
|
-
warn(f"Response compression error: {e}")
|
|
398
|
-
|
|
399
|
-
# 16. Step 2: TOON encoding — only for payloads at or above the
|
|
400
|
-
# minimum threshold; small JSON gains near-zero chars from TOON but
|
|
401
|
-
# would still pay the full encode cost on every PostToolUse event.
|
|
402
|
-
toon_output = ""
|
|
403
|
-
|
|
404
|
-
if tokenless_bin and len(compressed) >= _MIN_TOON_CHARS:
|
|
405
|
-
toon_parsed = try_parse_json(compressed)
|
|
406
|
-
if toon_parsed is not None:
|
|
407
|
-
toon_cmd = [tokenless_bin, "compress-toon", "--agent-id", agent_id]
|
|
408
|
-
if session_id:
|
|
409
|
-
toon_cmd.extend(["--session-id", session_id])
|
|
410
|
-
if tool_use_id:
|
|
411
|
-
toon_cmd.extend(["--tool-use-id", tool_use_id])
|
|
412
|
-
try:
|
|
413
|
-
proc = subprocess.run(
|
|
414
|
-
toon_cmd,
|
|
415
|
-
input=compressed,
|
|
416
|
-
capture_output=True, text=True, timeout=1,
|
|
417
|
-
)
|
|
418
|
-
if proc.returncode == 0 and proc.stdout.strip():
|
|
419
|
-
candidate = proc.stdout.strip()
|
|
420
|
-
if len(candidate) < len(compressed):
|
|
421
|
-
toon_output = candidate
|
|
422
|
-
elif proc.returncode != 0:
|
|
423
|
-
_warn_subprocess("compress-toon", proc)
|
|
424
|
-
except Exception as e:
|
|
425
|
-
warn(f"TOON encoding error: {e}")
|
|
426
|
-
|
|
427
|
-
# Determine final output
|
|
428
|
-
final_output = toon_output if toon_output else compressed
|
|
429
|
-
|
|
430
|
-
# Nothing shrank — pass the original through untouched instead of
|
|
431
|
-
# emitting a same-size duplicate of the response (applies to all agents).
|
|
432
|
-
if not used_resp_compression and not toon_output:
|
|
433
|
-
_emit_attribution_or_skip(env_attribution)
|
|
434
|
-
|
|
435
|
-
# 17. Build response — dispatch by agent runtime.
|
|
436
|
-
#
|
|
437
|
-
# Claude Code, Qoder, and OpenCode support real tool-output replacement. Keep
|
|
438
|
-
# additionalContext for additive diagnostics only; using it for compressed
|
|
439
|
-
# data would leave the original result in context and increase token use.
|
|
440
|
-
if agent_id in {_CLAUDE_AGENT_ID, _QODER_AGENT_ID, _OPENCODE_AGENT_ID}:
|
|
441
|
-
if agent_id == _CLAUDE_AGENT_ID and not _claude_supports_replacement():
|
|
273
|
+
# 10. Capability declaration (§4.5): what can this host actually do?
|
|
274
|
+
if cosh_ng_detected:
|
|
275
|
+
can_replace = True
|
|
276
|
+
replace_with_text = True # updatedToolResponse accepts any text
|
|
277
|
+
elif agent_id in {_QODER_AGENT_ID, _OPENCODE_AGENT_ID}:
|
|
278
|
+
can_replace = True
|
|
279
|
+
replace_with_text = not isinstance(tool_response_raw, (dict, list))
|
|
280
|
+
elif agent_id == _CLAUDE_AGENT_ID:
|
|
281
|
+
can_replace = _claude_supports_replacement()
|
|
282
|
+
replace_with_text = not isinstance(tool_response_raw, (dict, list))
|
|
283
|
+
if not can_replace:
|
|
442
284
|
warn(
|
|
443
285
|
"Claude Code < 2.1.121 (or version unknown): "
|
|
444
286
|
"updatedToolOutput unsupported, response compression disabled."
|
|
445
287
|
)
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
288
|
+
else:
|
|
289
|
+
# additionalContext-only hosts have no true replacement: passthrough
|
|
290
|
+
# (additive injection would duplicate the original — see module doc).
|
|
291
|
+
can_replace = False
|
|
292
|
+
replace_with_text = True
|
|
293
|
+
|
|
294
|
+
# 11. Spawn-avoidance prefilters. The entry point re-checks all three
|
|
295
|
+
# authoritatively; skipping here just saves the exec. SKIP_TOOLS reads
|
|
296
|
+
# the same tool_categories.json the entry point embeds, so content
|
|
297
|
+
# retrieval — the hottest PostToolUse traffic — never pays a spawn.
|
|
298
|
+
if not can_replace:
|
|
299
|
+
_emit_attribution_or_skip(env_attribution)
|
|
300
|
+
if tool_name in SKIP_TOOLS:
|
|
301
|
+
_emit_attribution_or_skip(env_attribution)
|
|
302
|
+
if len(content) < _MIN_RESPONSE_CHARS:
|
|
303
|
+
_emit_attribution_or_skip(env_attribution)
|
|
457
304
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
305
|
+
# 12. The one Tokenless subprocess: the unified entry point decides.
|
|
306
|
+
request = build_compression_request(
|
|
307
|
+
content,
|
|
308
|
+
agent_id,
|
|
309
|
+
"post_tool",
|
|
310
|
+
session_id=session_id,
|
|
311
|
+
tool_use_id=tool_use_id,
|
|
312
|
+
tool_name=tool_name,
|
|
313
|
+
replace_output=True,
|
|
314
|
+
publish_retrieve_tool=True,
|
|
315
|
+
replace_with_text=replace_with_text,
|
|
316
|
+
)
|
|
317
|
+
response = run_compress(tokenless_bin, request, _COMPRESS_TIMEOUT)
|
|
318
|
+
if response is None or response.get("disposition") != "applied":
|
|
319
|
+
_emit_attribution_or_skip(env_attribution)
|
|
464
320
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
if env_attribution:
|
|
470
|
-
hook_output["additionalContext"] = env_attribution
|
|
471
|
-
_emit({"suppressOutput": True, "hookSpecificOutput": hook_output})
|
|
472
|
-
return
|
|
321
|
+
output_text = response.get("output")
|
|
322
|
+
if not isinstance(output_text, str) or not output_text:
|
|
323
|
+
warn("tokenless compress returned no output. Passing through unchanged.")
|
|
324
|
+
_emit_attribution_or_skip(env_attribution)
|
|
473
325
|
|
|
474
|
-
#
|
|
475
|
-
# Skip compression if it doesn't reduce model-visible size.
|
|
326
|
+
# 13. Envelope construction — dispatch by agent runtime.
|
|
476
327
|
if cosh_ng_detected:
|
|
477
|
-
if len(final_output) >= len(tool_response):
|
|
478
|
-
_emit_attribution_or_skip(env_attribution)
|
|
479
|
-
|
|
480
328
|
hook_specific = {
|
|
481
329
|
"hookEventName": "PostToolUse",
|
|
482
|
-
"updatedToolResponse":
|
|
330
|
+
"updatedToolResponse": output_text,
|
|
483
331
|
}
|
|
484
332
|
if env_attribution:
|
|
485
333
|
hook_specific["additionalContext"] = env_attribution
|
|
486
334
|
_emit({"suppressOutput": True, "hookSpecificOutput": hook_specific})
|
|
487
335
|
return
|
|
488
336
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
337
|
+
if replace_with_text:
|
|
338
|
+
updated_output = output_text
|
|
339
|
+
else:
|
|
340
|
+
# Structured slot: the entry point guarantees schema-stable JSON for
|
|
341
|
+
# an applied response. A parse failure means the subprocess boundary
|
|
342
|
+
# was violated — fail open.
|
|
343
|
+
updated_output = try_parse_json(output_text)
|
|
344
|
+
if updated_output is None:
|
|
345
|
+
warn("tokenless compress returned non-JSON for a structured slot.")
|
|
346
|
+
_emit_attribution_or_skip(env_attribution)
|
|
494
347
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
348
|
+
# Qoder validates updatedToolOutput as a string even when the original
|
|
349
|
+
# tool response is structured. The entry point's compact serialization
|
|
350
|
+
# is exactly that string.
|
|
351
|
+
if agent_id == _QODER_AGENT_ID and not isinstance(updated_output, str):
|
|
352
|
+
updated_output = output_text
|
|
353
|
+
|
|
354
|
+
hook_output = {
|
|
355
|
+
"hookEventName": "PostToolUse",
|
|
356
|
+
"updatedToolOutput": updated_output,
|
|
357
|
+
}
|
|
358
|
+
if env_attribution:
|
|
359
|
+
hook_output["additionalContext"] = env_attribution
|
|
360
|
+
_emit({"suppressOutput": True, "hookSpecificOutput": hook_output})
|
|
502
361
|
|
|
503
362
|
|
|
504
363
|
if __name__ == "__main__":
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""Tokenless schema compression hook.
|
|
3
3
|
|
|
4
|
-
Reads a BeforeModel JSON from stdin, extracts the tools array,
|
|
5
|
-
|
|
6
|
-
writes a HookOutput JSON to stdout.
|
|
4
|
+
Reads a BeforeModel JSON from stdin, extracts the tools array, forwards it
|
|
5
|
+
to the unified ``tokenless compress`` entry point (protocol v1, seam
|
|
6
|
+
``before_model``, roadmap §5.4), and writes a HookOutput JSON to stdout.
|
|
7
|
+
The entry point returns the original array on no-savings, which this hook
|
|
8
|
+
wraps exactly like a compressed one — the historical behavior of the
|
|
9
|
+
``compress-schema`` flow it replaces.
|
|
7
10
|
|
|
8
11
|
Hook point: **BeforeModel**
|
|
9
12
|
|
|
@@ -16,7 +19,6 @@ from __future__ import annotations
|
|
|
16
19
|
import contextlib
|
|
17
20
|
import json
|
|
18
21
|
import os
|
|
19
|
-
import subprocess
|
|
20
22
|
import sys
|
|
21
23
|
|
|
22
24
|
try: # POSIX hosts (cosh / Cosh-NG) — the platforms these hooks target.
|
|
@@ -30,9 +32,11 @@ from hook_utils import (
|
|
|
30
32
|
_TOKENLESS_FALLBACK,
|
|
31
33
|
_TOKENLESS_LOCAL_LIB,
|
|
32
34
|
_TOKENLESS_LOCAL_SHARE,
|
|
35
|
+
build_compression_request,
|
|
33
36
|
resolve_agent_id,
|
|
34
37
|
resolve_binary,
|
|
35
38
|
resolve_tool_call_id,
|
|
39
|
+
run_compress,
|
|
36
40
|
secure_write_text,
|
|
37
41
|
skip,
|
|
38
42
|
warn,
|
|
@@ -42,6 +46,11 @@ from hook_utils import (
|
|
|
42
46
|
|
|
43
47
|
_AGENT_ID = resolve_agent_id()
|
|
44
48
|
|
|
49
|
+
# Below the extension manifests' 10 s host wrapper, so a pathological batch
|
|
50
|
+
# is killed here (fail-open skip) instead of racing the host's kill of the
|
|
51
|
+
# whole hook (the old subprocess timeout was 10 s against a 10 s wrapper).
|
|
52
|
+
_COMPRESS_TIMEOUT = 8
|
|
53
|
+
|
|
45
54
|
# One marker file holds the session keys that already emitted the "no tool
|
|
46
55
|
# declarations" warning — one key per line, most recent last — so the warning
|
|
47
56
|
# repeats at most once per session even though BeforeModel fires on every
|
|
@@ -272,36 +281,31 @@ def main() -> None:
|
|
|
272
281
|
session_id = input_data.get("session_id", "")
|
|
273
282
|
tool_use_id = resolve_tool_call_id(_AGENT_ID, input_data)
|
|
274
283
|
|
|
275
|
-
# 5. Compress schemas via
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
text=True,
|
|
288
|
-
timeout=10,
|
|
289
|
-
)
|
|
290
|
-
except Exception:
|
|
284
|
+
# 5. Compress schemas via the unified entry point (one subprocess).
|
|
285
|
+
request = build_compression_request(
|
|
286
|
+
tools_json,
|
|
287
|
+
_AGENT_ID,
|
|
288
|
+
"before_model",
|
|
289
|
+
session_id=session_id,
|
|
290
|
+
tool_use_id=tool_use_id,
|
|
291
|
+
replace_output=True,
|
|
292
|
+
publish_retrieve_tool=True,
|
|
293
|
+
)
|
|
294
|
+
response = run_compress(tokenless_bin, request, _COMPRESS_TIMEOUT)
|
|
295
|
+
if response is None:
|
|
291
296
|
warn("Schema compression subprocess failed. Passing through unchanged.")
|
|
292
297
|
skip()
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
detail = (proc.stderr or "").strip()[:200]
|
|
296
|
-
warn(
|
|
297
|
-
f"Schema compression failed with exit code {proc.returncode}: {detail}"
|
|
298
|
-
if detail
|
|
299
|
-
else f"Schema compression failed with exit code {proc.returncode}. Passing through unchanged."
|
|
300
|
-
)
|
|
298
|
+
if response.get("disposition") in {"error", "timeout"}:
|
|
299
|
+
warn("Schema compression failed. Passing through unchanged.")
|
|
301
300
|
skip()
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
301
|
+
if response.get("disposition") == "reversibility_unavailable":
|
|
302
|
+
# Savings existed but the stash could not record the originals;
|
|
303
|
+
# the entry point returned the uncompressed schemas. Surface the
|
|
304
|
+
# distinction from "nothing to compress" (envelope unchanged).
|
|
305
|
+
warn("Schema stash unavailable; truncated descriptions would be lossy.")
|
|
306
|
+
|
|
307
|
+
compressed = response.get("output")
|
|
308
|
+
if not isinstance(compressed, str) or not _is_json_array(compressed):
|
|
305
309
|
warn(
|
|
306
310
|
"Schema compression returned invalid JSON. Passing through unchanged."
|
|
307
311
|
)
|
|
@@ -43,6 +43,9 @@ _AGENT_ID = resolve_agent_id()
|
|
|
43
43
|
# Minimum payload size for TOON encoding. TOON on small JSON saves only a
|
|
44
44
|
# few characters (observed ~0.3% below ~500 chars) while the per-event
|
|
45
45
|
# encode cost stays the same, so smaller responses pass through untouched.
|
|
46
|
+
# This early check avoids the subprocess spawn; the compress-toon CLI
|
|
47
|
+
# enforces the same threshold by default (tokenless-runtime MIN_TOON_CHARS),
|
|
48
|
+
# so keep the two values in sync.
|
|
46
49
|
_MIN_TOON_CHARS = 500
|
|
47
50
|
|
|
48
51
|
|