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.
- package/README.md +206 -79
- package/adapters/tokenless/claude-code/.claude-plugin/plugin.json +1 -1
- package/adapters/tokenless/claude-code/hooks/run-hook.sh +62 -0
- package/adapters/tokenless/codex/.codex-plugin/plugin.json +1 -1
- package/adapters/tokenless/common/cosh-extension.json +4 -4
- package/adapters/tokenless/common/hooks/compress_response_hook.py +180 -96
- package/adapters/tokenless/common/hooks/compress_schema_hook.py +19 -31
- package/adapters/tokenless/common/hooks/hook_utils.py +240 -70
- package/adapters/tokenless/common/hooks/rewrite_hook.py +53 -169
- package/adapters/tokenless/dsh/dist/index.js +353 -264
- package/adapters/tokenless/dsh/package.json +2 -2
- package/adapters/tokenless/hermes/__init__.py +191 -357
- package/adapters/tokenless/hermes/plugin.yaml +2 -2
- package/adapters/tokenless/manifest.json +17 -1
- package/adapters/tokenless/openclaw/dist/index.d.ts +4 -16
- package/adapters/tokenless/openclaw/dist/index.js +291 -507
- package/adapters/tokenless/openclaw/index.ts +408 -628
- package/adapters/tokenless/openclaw/openclaw.plugin.json +4 -20
- package/adapters/tokenless/openclaw/package.json +6 -4
- package/adapters/tokenless/qoder/.qoder-plugin/plugin.json +1 -1
- package/adapters/tokenless/qwencode/hooks/run-hook.sh +62 -0
- package/adapters/tokenless/qwencode/qwen-extension.json +4 -4
- package/adapters/tokenless/qwenpaw/plugin.json +17 -0
- package/adapters/tokenless/qwenpaw/plugin.py +390 -0
- package/adapters/tokenless/qwenpaw/requirements.txt +6 -0
- package/adapters/tokenless/qwenpaw/scripts/detect.sh +131 -0
- package/adapters/tokenless/qwenpaw/scripts/install.sh +98 -0
- package/adapters/tokenless/qwenpaw/scripts/uninstall.sh +61 -0
- package/package.json +5 -5
- package/adapters/tokenless/common/hooks/compress_toon_hook.py +0 -174
|
@@ -2,15 +2,14 @@
|
|
|
2
2
|
"""Tokenless response compression hook for Cosh-NG, Claude Code, Qoder, and OpenCode.
|
|
3
3
|
|
|
4
4
|
Reads a PostToolUse JSON from stdin, forwards the model-visible tool
|
|
5
|
-
response to the unified ``tokenless compress``
|
|
6
|
-
|
|
5
|
+
response to the unified ``tokenless compress`` Protocol v2 PostTool operation
|
|
6
|
+
and translates the result into the host's
|
|
7
7
|
envelope. JSON detection, tool threshold selection, TOON selection, and
|
|
8
8
|
final acceptance all live behind the entry point; this hook only parses the
|
|
9
9
|
host object, declares capabilities, and builds envelopes (§4.5).
|
|
10
10
|
|
|
11
|
-
One Tokenless subprocess per invocation
|
|
12
|
-
|
|
13
|
-
compression decision.
|
|
11
|
+
One Tokenless subprocess per invocation. Environment-error attribution is
|
|
12
|
+
owned by the Rust PostTool service.
|
|
14
13
|
|
|
15
14
|
Hook point: **PostToolUse**
|
|
16
15
|
|
|
@@ -37,10 +36,10 @@ Output contract per agent:
|
|
|
37
36
|
replacement remain passthrough (roadmap §7). Environment attribution is
|
|
38
37
|
still injected: it is additive by design.
|
|
39
38
|
|
|
40
|
-
The agent ID is
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
39
|
+
The agent ID is resolved from the host runtime, ``--agent-id`` argument, or
|
|
40
|
+
TOKENLESS_AGENT_ID environment variable. When running under Cosh-NG, runtime
|
|
41
|
+
detection overrides the declared ID for correct stats attribution. Fallback
|
|
42
|
+
paths follow the ANOLISA FHS spec: /usr/bin/tokenless.
|
|
44
43
|
"""
|
|
45
44
|
|
|
46
45
|
from __future__ import annotations
|
|
@@ -56,11 +55,13 @@ from hook_utils import (
|
|
|
56
55
|
_TOKENLESS_FALLBACK,
|
|
57
56
|
_TOKENLESS_LOCAL_LIB,
|
|
58
57
|
_TOKENLESS_LOCAL_SHARE,
|
|
58
|
+
SHELL_TOOLS,
|
|
59
59
|
SKIP_TOOLS,
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
build_post_tool_request,
|
|
61
|
+
consume_output_optimization,
|
|
62
62
|
detect_cosh_ng_runtime,
|
|
63
63
|
is_skill_file,
|
|
64
|
+
is_tokenless_retrieve_command,
|
|
64
65
|
parse_version,
|
|
65
66
|
resolve_agent_id,
|
|
66
67
|
resolve_binary,
|
|
@@ -68,17 +69,19 @@ from hook_utils import (
|
|
|
68
69
|
run_compress,
|
|
69
70
|
secure_write_text,
|
|
70
71
|
skip,
|
|
72
|
+
tokenless_retrieve_command_available,
|
|
71
73
|
try_parse_json,
|
|
72
74
|
warn,
|
|
73
75
|
)
|
|
74
76
|
|
|
75
77
|
# -- constants ---------------------------------------------------------------
|
|
76
78
|
|
|
77
|
-
#
|
|
78
|
-
#
|
|
79
|
-
#
|
|
80
|
-
#
|
|
81
|
-
|
|
79
|
+
# Shell tool envelopes carry the log in one dominant text field. Unwrapping
|
|
80
|
+
# is worth a rebuilt envelope only when that field is large enough for the
|
|
81
|
+
# build/log engine to bite (its own gates start at 30 lines / 200 chars;
|
|
82
|
+
# 2000 chars keeps the rewrap machinery out of trivial outputs).
|
|
83
|
+
_SHELL_TEXT_FIELDS = ("stdout", "stderr")
|
|
84
|
+
_SHELL_UNWRAP_MIN_CHARS = 2_000
|
|
82
85
|
|
|
83
86
|
# Below the qwen/cosh extension manifests' 10 s host wrapper so a
|
|
84
87
|
# pathological input is killed here (fail-open skip) before the host kills
|
|
@@ -96,9 +99,7 @@ _OPENCODE_AGENT_ID = "opencode"
|
|
|
96
99
|
# Cache for `claude --version`, keyed on binary path+mtime+size so upgrades
|
|
97
100
|
# invalidate it. Hooks run as a fresh process per tool call and spawning the
|
|
98
101
|
# node CLI every time would add noticeable latency.
|
|
99
|
-
_CLAUDE_VERSION_CACHE = os.path.join(
|
|
100
|
-
os.path.expanduser("~"), ".tokenless", ".claude-version"
|
|
101
|
-
)
|
|
102
|
+
_CLAUDE_VERSION_CACHE = os.path.join(os.path.expanduser("~"), ".tokenless", ".claude-version")
|
|
102
103
|
|
|
103
104
|
|
|
104
105
|
# -- helpers -------------------------------------------------------------------
|
|
@@ -115,17 +116,44 @@ def _emit_attribution_or_skip(env_attribution: str) -> None:
|
|
|
115
116
|
additive and safe on every agent), otherwise a plain skip. Never returns.
|
|
116
117
|
"""
|
|
117
118
|
if env_attribution:
|
|
118
|
-
_emit(
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
"
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
119
|
+
_emit(
|
|
120
|
+
{
|
|
121
|
+
"suppressOutput": True,
|
|
122
|
+
"hookSpecificOutput": {
|
|
123
|
+
"hookEventName": "PostToolUse",
|
|
124
|
+
"additionalContext": env_attribution,
|
|
125
|
+
},
|
|
126
|
+
}
|
|
127
|
+
)
|
|
125
128
|
sys.exit(0)
|
|
126
129
|
skip()
|
|
127
130
|
|
|
128
131
|
|
|
132
|
+
def _shell_text_field(tool_name: str, envelope) -> tuple | None:
|
|
133
|
+
"""The dominant text field of a shell tool's envelope, or ``None``.
|
|
134
|
+
|
|
135
|
+
Shell envelopes (``{"stdout": …, "stderr": …}``) are JSON to the entry
|
|
136
|
+
point, which would compress them log-blind. Unwrapping the largest text
|
|
137
|
+
field sends the log itself through the text slot; step 13 re-injects the
|
|
138
|
+
compressed text into a same-shaped envelope, so the host's tool protocol
|
|
139
|
+
is untouched (adapters own envelope knowledge, §4.5). Only the single
|
|
140
|
+
largest field is compressed — one Tokenless subprocess per invocation
|
|
141
|
+
(§5.6) — the other field stays byte-identical.
|
|
142
|
+
"""
|
|
143
|
+
if tool_name not in SHELL_TOOLS or not isinstance(envelope, dict):
|
|
144
|
+
return None
|
|
145
|
+
best = None
|
|
146
|
+
for name in _SHELL_TEXT_FIELDS:
|
|
147
|
+
value = envelope.get(name)
|
|
148
|
+
if (
|
|
149
|
+
isinstance(value, str)
|
|
150
|
+
and len(value) >= _SHELL_UNWRAP_MIN_CHARS
|
|
151
|
+
and (best is None or len(value) > len(best[1]))
|
|
152
|
+
):
|
|
153
|
+
best = (name, value)
|
|
154
|
+
return best
|
|
155
|
+
|
|
156
|
+
|
|
129
157
|
def _cached_claude_version(claude_bin: str) -> tuple | None:
|
|
130
158
|
"""Return the Claude Code version tuple, caching `claude --version`."""
|
|
131
159
|
try:
|
|
@@ -145,7 +173,9 @@ def _cached_claude_version(claude_bin: str) -> tuple | None:
|
|
|
145
173
|
try:
|
|
146
174
|
proc = subprocess.run(
|
|
147
175
|
[claude_bin, "--version"],
|
|
148
|
-
capture_output=True,
|
|
176
|
+
capture_output=True,
|
|
177
|
+
text=True,
|
|
178
|
+
timeout=5,
|
|
149
179
|
)
|
|
150
180
|
except Exception as e:
|
|
151
181
|
warn(f"claude --version failed: {e}")
|
|
@@ -157,9 +187,7 @@ def _cached_claude_version(claude_bin: str) -> tuple | None:
|
|
|
157
187
|
try:
|
|
158
188
|
# Same hardened write as other ~/.tokenless state files (0o600,
|
|
159
189
|
# symlink-safe) so the cache stays private on shared HOMEs.
|
|
160
|
-
secure_write_text(
|
|
161
|
-
_CLAUDE_VERSION_CACHE, f"{cache_key}\n{proc.stdout.strip()}"
|
|
162
|
-
)
|
|
190
|
+
secure_write_text(_CLAUDE_VERSION_CACHE, f"{cache_key}\n{proc.stdout.strip()}")
|
|
163
191
|
except OSError:
|
|
164
192
|
pass
|
|
165
193
|
return ver
|
|
@@ -187,15 +215,29 @@ def main() -> None:
|
|
|
187
215
|
cosh_ng_version = detect_cosh_ng_runtime()
|
|
188
216
|
cosh_ng_detected = cosh_ng_version is not None
|
|
189
217
|
|
|
190
|
-
#
|
|
218
|
+
# 2. Resolve agent ID based on runtime
|
|
219
|
+
agent_id = resolve_agent_id()
|
|
220
|
+
|
|
221
|
+
# 3. Read stdin JSON and consume any matching PreTool state.
|
|
222
|
+
try:
|
|
223
|
+
input_data = json.load(sys.stdin)
|
|
224
|
+
except (json.JSONDecodeError, EOFError, ValueError):
|
|
225
|
+
warn("failed to read PostToolUse payload. Passing through unchanged.")
|
|
226
|
+
skip()
|
|
227
|
+
|
|
228
|
+
session_id = input_data.get("session_id", "")
|
|
229
|
+
tool_use_id = resolve_tool_call_id(agent_id, input_data)
|
|
230
|
+
try:
|
|
231
|
+
output_optimization = consume_output_optimization(agent_id, session_id, tool_use_id)
|
|
232
|
+
except OSError as error:
|
|
233
|
+
warn(f"failed to consume PreTool optimization state: {error}")
|
|
234
|
+
output_optimization = "none"
|
|
235
|
+
|
|
191
236
|
if cosh_ng_detected and cosh_ng_version == (0, 0, 0):
|
|
192
237
|
warn("Unsupported Cosh-NG version. Response compression disabled (fail open).")
|
|
193
238
|
skip()
|
|
194
239
|
|
|
195
|
-
#
|
|
196
|
-
agent_id = resolve_agent_id()
|
|
197
|
-
|
|
198
|
-
# 3. Resolve binaries
|
|
240
|
+
# 4. Resolve the single Core entry point after consuming per-call state.
|
|
199
241
|
tokenless_bin = resolve_binary(
|
|
200
242
|
"tokenless", _TOKENLESS_FALLBACK, _TOKENLESS_LOCAL_SHARE, _TOKENLESS_LOCAL_LIB
|
|
201
243
|
)
|
|
@@ -203,13 +245,6 @@ def main() -> None:
|
|
|
203
245
|
warn("tokenless is not installed. Response compression hook disabled.")
|
|
204
246
|
skip()
|
|
205
247
|
|
|
206
|
-
# 4. Read stdin JSON
|
|
207
|
-
try:
|
|
208
|
-
input_data = json.load(sys.stdin)
|
|
209
|
-
except (json.JSONDecodeError, EOFError, ValueError):
|
|
210
|
-
warn("failed to read PostToolUse payload. Passing through unchanged.")
|
|
211
|
-
skip()
|
|
212
|
-
|
|
213
248
|
tool_name = input_data.get("tool_name", "unknown")
|
|
214
249
|
tool_response_raw = input_data.get("tool_response", "")
|
|
215
250
|
if not tool_response_raw or tool_response_raw == "{}":
|
|
@@ -235,51 +270,36 @@ def main() -> None:
|
|
|
235
270
|
if isinstance(model_visible_before, str) and is_skill_file(model_visible_before):
|
|
236
271
|
skip()
|
|
237
272
|
|
|
238
|
-
# 7. Copy the model-visible value into the request content (§4.5).
|
|
239
|
-
#
|
|
240
|
-
#
|
|
241
|
-
|
|
273
|
+
# 7. Copy the model-visible value into the request content (§4.5). A
|
|
274
|
+
# shell envelope's dominant text field goes through the text slot
|
|
275
|
+
# instead of log-blind JSON; ensure_ascii=False matches the entry
|
|
276
|
+
# point's normalization, so size gates measure Unicode characters on
|
|
277
|
+
# both sides.
|
|
278
|
+
shell_field = _shell_text_field(tool_name, model_visible_before)
|
|
279
|
+
if shell_field is not None:
|
|
280
|
+
content = shell_field[1]
|
|
281
|
+
elif isinstance(model_visible_before, str):
|
|
242
282
|
content = model_visible_before
|
|
243
283
|
elif isinstance(model_visible_before, (dict, list)):
|
|
244
|
-
content = json.dumps(
|
|
245
|
-
model_visible_before, separators=(",", ":"), ensure_ascii=False
|
|
246
|
-
)
|
|
284
|
+
content = json.dumps(model_visible_before, separators=(",", ":"), ensure_ascii=False)
|
|
247
285
|
else:
|
|
248
286
|
skip()
|
|
249
287
|
|
|
250
|
-
# 8.
|
|
251
|
-
session_id = input_data.get("session_id", "")
|
|
252
|
-
tool_use_id = resolve_tool_call_id(agent_id, input_data)
|
|
253
|
-
|
|
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
|
|
265
|
-
env_attribution = ""
|
|
266
|
-
attr_category, attr_fix_hint = classify_env_error(attr_subject)
|
|
267
|
-
if attr_category:
|
|
268
|
-
env_attribution = (
|
|
269
|
-
f"[tokenless:env] {tool_name} failed: "
|
|
270
|
-
f"{attr_category} ({attr_fix_hint}). Skip retry."
|
|
271
|
-
)
|
|
272
|
-
|
|
273
|
-
# 10. Capability declaration (§4.5): what can this host actually do?
|
|
288
|
+
# 8. Capability declaration: what can this host actually do?
|
|
274
289
|
if cosh_ng_detected:
|
|
275
290
|
can_replace = True
|
|
276
291
|
replace_with_text = True # updatedToolResponse accepts any text
|
|
277
292
|
elif agent_id in {_QODER_AGENT_ID, _OPENCODE_AGENT_ID}:
|
|
278
293
|
can_replace = True
|
|
279
|
-
|
|
294
|
+
# An unwrapped shell field is plain text regardless of its envelope.
|
|
295
|
+
replace_with_text = shell_field is not None or not isinstance(
|
|
296
|
+
tool_response_raw, (dict, list)
|
|
297
|
+
)
|
|
280
298
|
elif agent_id == _CLAUDE_AGENT_ID:
|
|
281
299
|
can_replace = _claude_supports_replacement()
|
|
282
|
-
replace_with_text = not isinstance(
|
|
300
|
+
replace_with_text = shell_field is not None or not isinstance(
|
|
301
|
+
tool_response_raw, (dict, list)
|
|
302
|
+
)
|
|
283
303
|
if not can_replace:
|
|
284
304
|
warn(
|
|
285
305
|
"Claude Code < 2.1.121 (or version unknown): "
|
|
@@ -291,30 +311,81 @@ def main() -> None:
|
|
|
291
311
|
can_replace = False
|
|
292
312
|
replace_with_text = True
|
|
293
313
|
|
|
294
|
-
#
|
|
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)
|
|
314
|
+
# 9. Map host facts into the required lifecycle fields.
|
|
300
315
|
if tool_name in SKIP_TOOLS:
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
316
|
+
content_origin = "file_content"
|
|
317
|
+
elif tool_name in SHELL_TOOLS:
|
|
318
|
+
content_origin = "command_output"
|
|
319
|
+
else:
|
|
320
|
+
content_origin = "api_response"
|
|
321
|
+
raw_status = str(input_data.get("status", "")).lower()
|
|
322
|
+
shell_process_result = model_visible_before if isinstance(model_visible_before, dict) else None
|
|
323
|
+
shell_process_error = (
|
|
324
|
+
tool_name in SHELL_TOOLS
|
|
325
|
+
and shell_process_result is not None
|
|
326
|
+
and (
|
|
327
|
+
shell_process_result.get("error") is not None
|
|
328
|
+
or (
|
|
329
|
+
shell_process_result.get("exit_code") is not None
|
|
330
|
+
and shell_process_result.get("exit_code") != 0
|
|
331
|
+
)
|
|
332
|
+
or (
|
|
333
|
+
shell_process_result.get("exitCode") is not None
|
|
334
|
+
and shell_process_result.get("exitCode") != 0
|
|
335
|
+
)
|
|
336
|
+
)
|
|
337
|
+
)
|
|
338
|
+
if raw_status in {"interrupted", "denied"}:
|
|
339
|
+
status = raw_status
|
|
340
|
+
elif input_data.get("is_error") is True or (
|
|
341
|
+
isinstance(tool_response_raw, dict) and tool_response_raw.get("isError") is True
|
|
342
|
+
):
|
|
343
|
+
status = "error"
|
|
344
|
+
elif shell_process_error:
|
|
345
|
+
status = "error"
|
|
346
|
+
else:
|
|
347
|
+
status = "success"
|
|
348
|
+
|
|
349
|
+
# Shell envelopes often carry a large stdout alongside the actual failure
|
|
350
|
+
# in a short stderr. Error results are never replaced, so send the error
|
|
351
|
+
# stream to Core for diagnosis while the host keeps the original envelope.
|
|
352
|
+
if status == "error" and tool_name in SHELL_TOOLS and isinstance(model_visible_before, dict):
|
|
353
|
+
error_parts = []
|
|
354
|
+
for field in ("stderr", "error"):
|
|
355
|
+
value = model_visible_before.get(field)
|
|
356
|
+
if isinstance(value, str) and value.strip():
|
|
357
|
+
error_parts.append(value)
|
|
358
|
+
if error_parts:
|
|
359
|
+
content = "\n".join(error_parts)
|
|
360
|
+
|
|
361
|
+
retrieve_result = status == "success" and is_tokenless_retrieve_command(
|
|
362
|
+
tool_name, input_data.get("tool_input")
|
|
363
|
+
)
|
|
364
|
+
retrieval_available = (
|
|
365
|
+
can_replace
|
|
366
|
+
and status == "success"
|
|
367
|
+
and output_optimization == "none"
|
|
368
|
+
and not retrieve_result
|
|
369
|
+
and tokenless_retrieve_command_available()
|
|
370
|
+
)
|
|
304
371
|
|
|
305
|
-
#
|
|
306
|
-
request =
|
|
372
|
+
# 10. The one Tokenless subprocess: Core owns all PostTool policy.
|
|
373
|
+
request = build_post_tool_request(
|
|
307
374
|
content,
|
|
308
375
|
agent_id,
|
|
309
|
-
|
|
376
|
+
tool_name,
|
|
377
|
+
status,
|
|
378
|
+
content_origin,
|
|
379
|
+
output_optimization,
|
|
380
|
+
result_kind="retrieve" if retrieve_result else "tool",
|
|
381
|
+
recovery={"kind": "shell" if retrieval_available else "none"},
|
|
310
382
|
session_id=session_id,
|
|
311
383
|
tool_use_id=tool_use_id,
|
|
312
|
-
|
|
313
|
-
replace_output=True,
|
|
314
|
-
publish_retrieve_tool=True,
|
|
384
|
+
replace_output=can_replace,
|
|
315
385
|
replace_with_text=replace_with_text,
|
|
316
386
|
)
|
|
317
|
-
response = run_compress(tokenless_bin, request, _COMPRESS_TIMEOUT)
|
|
387
|
+
response = run_compress(tokenless_bin, request, _COMPRESS_TIMEOUT, "post_tool")
|
|
388
|
+
env_attribution = response.get("additional_context", "") if response is not None else ""
|
|
318
389
|
if response is None or response.get("disposition") != "applied":
|
|
319
390
|
_emit_attribution_or_skip(env_attribution)
|
|
320
391
|
|
|
@@ -323,18 +394,28 @@ def main() -> None:
|
|
|
323
394
|
warn("tokenless compress returned no output. Passing through unchanged.")
|
|
324
395
|
_emit_attribution_or_skip(env_attribution)
|
|
325
396
|
|
|
326
|
-
#
|
|
397
|
+
# 11. Envelope construction — dispatch by agent runtime. An unwrapped
|
|
398
|
+
# shell field is re-injected into a same-shaped envelope: the compressed
|
|
399
|
+
# text replaces exactly the field that was sent, every other field stays
|
|
400
|
+
# byte-identical.
|
|
401
|
+
rewrapped = None
|
|
402
|
+
if shell_field is not None:
|
|
403
|
+
rewrapped = dict(model_visible_before)
|
|
404
|
+
rewrapped[shell_field[0]] = output_text
|
|
405
|
+
|
|
327
406
|
if cosh_ng_detected:
|
|
328
407
|
hook_specific = {
|
|
329
408
|
"hookEventName": "PostToolUse",
|
|
330
|
-
"updatedToolResponse": output_text,
|
|
409
|
+
"updatedToolResponse": rewrapped if rewrapped is not None else output_text,
|
|
331
410
|
}
|
|
332
411
|
if env_attribution:
|
|
333
412
|
hook_specific["additionalContext"] = env_attribution
|
|
334
413
|
_emit({"suppressOutput": True, "hookSpecificOutput": hook_specific})
|
|
335
414
|
return
|
|
336
415
|
|
|
337
|
-
if
|
|
416
|
+
if rewrapped is not None:
|
|
417
|
+
updated_output = rewrapped
|
|
418
|
+
elif replace_with_text:
|
|
338
419
|
updated_output = output_text
|
|
339
420
|
else:
|
|
340
421
|
# Structured slot: the entry point guarantees schema-stable JSON for
|
|
@@ -347,9 +428,12 @@ def main() -> None:
|
|
|
347
428
|
|
|
348
429
|
# Qoder validates updatedToolOutput as a string even when the original
|
|
349
430
|
# tool response is structured. The entry point's compact serialization
|
|
350
|
-
# is exactly that string.
|
|
431
|
+
# is exactly that string; a rewrapped shell envelope serializes here.
|
|
351
432
|
if agent_id == _QODER_AGENT_ID and not isinstance(updated_output, str):
|
|
352
|
-
|
|
433
|
+
if rewrapped is not None:
|
|
434
|
+
updated_output = json.dumps(rewrapped, separators=(",", ":"), ensure_ascii=False)
|
|
435
|
+
else:
|
|
436
|
+
updated_output = output_text
|
|
353
437
|
|
|
354
438
|
hook_output = {
|
|
355
439
|
"hookEventName": "PostToolUse",
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
"""Tokenless schema compression hook.
|
|
3
3
|
|
|
4
4
|
Reads a BeforeModel JSON from stdin, extracts the tools array, forwards it
|
|
5
|
-
to the unified ``tokenless compress``
|
|
6
|
-
|
|
5
|
+
to the unified ``tokenless compress`` Protocol v2 BeforeModel operation and
|
|
6
|
+
writes a HookOutput JSON to stdout.
|
|
7
7
|
The entry point returns the original array on no-savings, which this hook
|
|
8
8
|
wraps exactly like a compressed one — the historical behavior of the
|
|
9
9
|
``compress-schema`` flow it replaces.
|
|
@@ -32,7 +32,7 @@ from hook_utils import (
|
|
|
32
32
|
_TOKENLESS_FALLBACK,
|
|
33
33
|
_TOKENLESS_LOCAL_LIB,
|
|
34
34
|
_TOKENLESS_LOCAL_SHARE,
|
|
35
|
-
|
|
35
|
+
build_before_model_request,
|
|
36
36
|
resolve_agent_id,
|
|
37
37
|
resolve_binary,
|
|
38
38
|
resolve_tool_call_id,
|
|
@@ -112,14 +112,6 @@ def _marker_lock():
|
|
|
112
112
|
# -- helpers -----------------------------------------------------------------
|
|
113
113
|
|
|
114
114
|
|
|
115
|
-
def _is_json_array(data: str) -> bool:
|
|
116
|
-
try:
|
|
117
|
-
obj = json.loads(data)
|
|
118
|
-
return isinstance(obj, list)
|
|
119
|
-
except (json.JSONDecodeError, ValueError):
|
|
120
|
-
return False
|
|
121
|
-
|
|
122
|
-
|
|
123
115
|
def _session_warn_key(session_id: str) -> str:
|
|
124
116
|
"""Normalize the session ID into the dedup key for the no-tools warning.
|
|
125
117
|
|
|
@@ -275,37 +267,33 @@ def main() -> None:
|
|
|
275
267
|
)
|
|
276
268
|
skip()
|
|
277
269
|
|
|
278
|
-
tools_json = json.dumps(tools, separators=(",", ":"))
|
|
279
|
-
|
|
280
270
|
# 4. Extract caller context
|
|
281
271
|
session_id = input_data.get("session_id", "")
|
|
282
272
|
tool_use_id = resolve_tool_call_id(_AGENT_ID, input_data)
|
|
283
273
|
|
|
284
274
|
# 5. Compress schemas via the unified entry point (one subprocess).
|
|
285
|
-
|
|
286
|
-
|
|
275
|
+
llm_request = input_data.get("llm_request", {})
|
|
276
|
+
visible_context = dict(llm_request) if isinstance(llm_request, dict) else {}
|
|
277
|
+
visible_context.pop("tools", None)
|
|
278
|
+
config = visible_context.get("config")
|
|
279
|
+
if isinstance(config, dict):
|
|
280
|
+
visible_context["config"] = dict(config)
|
|
281
|
+
visible_context["config"].pop("tools", None)
|
|
282
|
+
request = build_before_model_request(
|
|
283
|
+
tools,
|
|
284
|
+
visible_context,
|
|
287
285
|
_AGENT_ID,
|
|
288
|
-
"before_model",
|
|
289
286
|
session_id=session_id,
|
|
290
287
|
tool_use_id=tool_use_id,
|
|
291
|
-
replace_output=True,
|
|
292
|
-
publish_retrieve_tool=True,
|
|
293
288
|
)
|
|
294
|
-
response = run_compress(
|
|
289
|
+
response = run_compress(
|
|
290
|
+
tokenless_bin, request, _COMPRESS_TIMEOUT, "before_model"
|
|
291
|
+
)
|
|
295
292
|
if response is None:
|
|
296
293
|
warn("Schema compression subprocess failed. Passing through unchanged.")
|
|
297
294
|
skip()
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
skip()
|
|
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):
|
|
295
|
+
compressed = response.get("tools")
|
|
296
|
+
if not isinstance(compressed, list):
|
|
309
297
|
warn(
|
|
310
298
|
"Schema compression returned invalid JSON. Passing through unchanged."
|
|
311
299
|
)
|
|
@@ -317,7 +305,7 @@ def main() -> None:
|
|
|
317
305
|
"hookEventName": "BeforeModel",
|
|
318
306
|
"llm_request": {
|
|
319
307
|
"config": {
|
|
320
|
-
"tools":
|
|
308
|
+
"tools": compressed,
|
|
321
309
|
},
|
|
322
310
|
},
|
|
323
311
|
},
|