anolisa-tokenless 0.7.14 → 0.8.1

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 (32) hide show
  1. package/README.md +226 -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/claude-code/scripts/detect.sh +50 -7
  5. package/adapters/tokenless/codex/.codex-plugin/plugin.json +1 -1
  6. package/adapters/tokenless/common/cosh-extension.json +4 -4
  7. package/adapters/tokenless/common/hooks/compress_response_hook.py +180 -96
  8. package/adapters/tokenless/common/hooks/compress_schema_hook.py +19 -31
  9. package/adapters/tokenless/common/hooks/hook_utils.py +240 -70
  10. package/adapters/tokenless/common/hooks/rewrite_hook.py +53 -169
  11. package/adapters/tokenless/dsh/dist/index.js +353 -264
  12. package/adapters/tokenless/dsh/package.json +2 -2
  13. package/adapters/tokenless/hermes/__init__.py +338 -363
  14. package/adapters/tokenless/hermes/plugin.yaml +2 -2
  15. package/adapters/tokenless/manifest.json +17 -1
  16. package/adapters/tokenless/openclaw/dist/index.d.ts +4 -16
  17. package/adapters/tokenless/openclaw/dist/index.js +291 -507
  18. package/adapters/tokenless/openclaw/index.ts +408 -628
  19. package/adapters/tokenless/openclaw/openclaw.plugin.json +4 -20
  20. package/adapters/tokenless/openclaw/package.json +6 -4
  21. package/adapters/tokenless/openclaw/scripts/install.sh +51 -12
  22. package/adapters/tokenless/qoder/.qoder-plugin/plugin.json +1 -1
  23. package/adapters/tokenless/qwencode/hooks/run-hook.sh +62 -0
  24. package/adapters/tokenless/qwencode/qwen-extension.json +4 -4
  25. package/adapters/tokenless/qwenpaw/plugin.json +17 -0
  26. package/adapters/tokenless/qwenpaw/plugin.py +390 -0
  27. package/adapters/tokenless/qwenpaw/requirements.txt +6 -0
  28. package/adapters/tokenless/qwenpaw/scripts/detect.sh +131 -0
  29. package/adapters/tokenless/qwenpaw/scripts/install.sh +98 -0
  30. package/adapters/tokenless/qwenpaw/scripts/uninstall.sh +61 -0
  31. package/package.json +5 -5
  32. package/adapters/tokenless/common/hooks/compress_toon_hook.py +0 -174
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "id": "tokenless",
3
3
  "name": "Tokenless",
4
- "version": "0.7.14",
5
- "description": "Unified RTK command rewriting + response/TOON compression + registered but hard-disabled Tool Ready. Wraps tokenless and rtk system binaries via child_process — this is expected and not malicious.",
4
+ "version": "0.8.1",
5
+ "description": "Protocol v2 PreTool RTK rewriting and PostTool optimization. Wraps the tokenless system binary via child_process — this is expected and not malicious.",
6
6
  "activation": {
7
7
  "onCapabilities": ["hook"]
8
8
  },
@@ -11,30 +11,14 @@
11
11
  "properties": {
12
12
  "rtk_enabled": { "type": "boolean", "default": true },
13
13
  "tool_ready_enabled": { "type": "boolean", "default": true },
14
- "response_compression_enabled": { "type": "boolean", "default": true },
15
- "skip_tools": {
16
- "type": "array",
17
- "items": { "type": "string" },
18
- "default": [],
19
- "description": "Layer 1: Tools that skip all compression (preserve integrity). Loaded from tool_categories.json if not specified."
20
- },
21
- "shell_tools": {
22
- "type": "array",
23
- "items": { "type": "string" },
24
- "default": [],
25
- "description": "Layer 2: Shell/exec tools with moderate truncation. Loaded from tool_categories.json if not specified."
26
- },
27
- "toon_compression_enabled": { "type": "boolean", "default": false },
14
+ "post_tool_enabled": { "type": "boolean", "default": true },
28
15
  "verbose": { "type": "boolean", "default": false }
29
16
  }
30
17
  },
31
18
  "uiHints": {
32
19
  "rtk_enabled": { "label": "Enable RTK command rewriting" },
33
20
  "tool_ready_enabled": { "label": "Register Tool Ready hook (hard-disabled)" },
34
- "response_compression_enabled": { "label": "Enable response compression" },
35
- "skip_tools": { "label": "Tool names whose responses should not be compressed" },
36
- "shell_tools": { "label": "Tool names treated as shell/exec (moderate truncation)" },
37
- "toon_compression_enabled": { "label": "Enable TOON format compression" },
21
+ "post_tool_enabled": { "label": "Enable PostTool optimization" },
38
22
  "verbose": { "label": "Verbose logging" }
39
23
  }
40
24
  }
@@ -1,18 +1,20 @@
1
1
  {
2
2
  "name": "@tokenless/openclaw-plugin",
3
- "version": "0.7.14",
4
- "description": "Unified OpenClaw plugin — RTK command rewriting + tokenless schema/response compression for 60-90% LLM token savings",
3
+ "version": "0.8.1",
4
+ "description": "OpenClaw lifecycle adapter for Tokenless Protocol v2",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "openclaw": {
8
- "extensions": ["./dist/index.js"]
8
+ "extensions": ["./dist/index.js"],
9
+ "compat": {
10
+ "pluginApi": ">=2026.4.22"
11
+ }
9
12
  },
10
13
  "scripts": {
11
14
  "clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
12
15
  "build": "npm run clean && tsc --project tsconfig.json && cp ../common/hooks/tool_categories.json dist/"
13
16
  },
14
17
  "peerDependencies": {
15
- "rtk": ">=0.35.0",
16
18
  "tokenless": ">=0.1.0"
17
19
  },
18
20
  "devDependencies": {
@@ -45,7 +45,9 @@ if [ ! -f "$PLUGIN_SRC/dist/index.js" ]; then
45
45
  fi
46
46
 
47
47
  if [ "$DRY_RUN" = "1" ]; then
48
- echo "DRY-RUN: env -u OPENCLAW_HOME OPENCLAW_STATE_DIR=$OPENCLAW_STATE_DIR $OPENCLAW_BIN plugins install $PLUGIN_SRC --force --dangerously-force-unsafe-install"
48
+ echo "DRY-RUN: env -u OPENCLAW_HOME OPENCLAW_STATE_DIR=$OPENCLAW_STATE_DIR $OPENCLAW_BIN plugins install $PLUGIN_SRC --force"
49
+ echo "DRY-RUN: add --dangerously-force-unsafe-install only while the advertised option still has effect (legacy hosts)"
50
+ echo "DRY-RUN: add --accept-capabilities only if advertised by plugins install --help"
49
51
  exit 0
50
52
  fi
51
53
 
@@ -55,19 +57,56 @@ if ! command -v "$OPENCLAW_BIN" &>/dev/null; then
55
57
  exit 0
56
58
  fi
57
59
 
58
- # OpenClaw's built-in security scanner flags child_process imports as "dangerous
59
- # code patterns" and requires --dangerously-force-unsafe-install to proceed.
60
- # This is expected for the tokenless plugin: it delegates to tokenless and rtk
61
- # system binaries via execFileSync/spawnSync with fixed paths and timeouts.
62
- # No shell injection vector exists — all subprocess arguments are hardcoded or
63
- # come from resolveBinaryPath(), never from user input.
64
- echo "[${COMPONENT}] Note: --dangerously-force-unsafe-install is required because"
65
- echo "[${COMPONENT}] this plugin wraps tokenless/rtk system binaries via child_process."
66
- echo "[${COMPONENT}] See https://github.com/alibaba/anolisa for source."
60
+ INSTALL_ARGS=(plugins install "$PLUGIN_SRC" --force)
61
+ if ! INSTALL_HELP="$(env -u OPENCLAW_HOME OPENCLAW_STATE_DIR="$OPENCLAW_STATE_DIR" "$OPENCLAW_BIN" plugins install --help 2>&1)"; then
62
+ printf '[%s] Cannot inspect OpenClaw installer options: %s\n' "$COMPONENT" "$INSTALL_HELP" >&2
63
+ exit 1
64
+ fi
65
+ # Invoking this installer accepts declared capabilities. Older hosts must
66
+ # never receive an unsupported flag or a similarly named option.
67
+ if [[ "$INSTALL_HELP" =~ (^|[^[:alnum:]_.-])--accept-capabilities([^[:alnum:]_.-]|$) ]]; then
68
+ INSTALL_ARGS+=(--accept-capabilities)
69
+ fi
70
+
71
+ # Legacy hosts gate installs on a safety scan of child_process usage whose only
72
+ # non-interactive bypass is --dangerously-force-unsafe-install. OpenClaw 2026.9.2
73
+ # keeps the token as a deprecated no-op: passing it there accomplishes nothing
74
+ # and breaks again once the token is removed outright, so keep it only while the
75
+ # advertised option still has effect (the scanner moved to
76
+ # security.installPolicy on no-op hosts). The npm package ships this script to
77
+ # macOS, where /bin/bash stays at 3.2 — no ${var,,} here.
78
+ UNSAFE_SUPPORT=absent
79
+ while IFS= read -r help_line; do
80
+ if [[ "$help_line" =~ (^|[^[:alnum:]_.-])--dangerously-force-unsafe-install([^[:alnum:]_.-]|$) ]]; then
81
+ help_line_lc="$(printf '%s' "$help_line" | tr '[:upper:]' '[:lower:]')"
82
+ case "$help_line_lc" in
83
+ *"no op"*|*"no-op"*) UNSAFE_SUPPORT=noop ;;
84
+ *) UNSAFE_SUPPORT=effective; INSTALL_ARGS+=(--dangerously-force-unsafe-install) ;;
85
+ esac
86
+ break
87
+ fi
88
+ done <<< "$INSTALL_HELP"
89
+
90
+ # Why the bypass is legitimate on the hosts that still need it: their scanner
91
+ # reads child_process imports as "dangerous code patterns", and the tokenless
92
+ # plugin does delegate to the tokenless and rtk system binaries via
93
+ # execFileSync/spawnSync — with fixed paths and timeouts. No shell injection
94
+ # vector exists: all subprocess arguments are hardcoded or come from
95
+ # resolveBinaryPath(), never from user input. Hosts that dropped install-time
96
+ # scanning get no note, because nothing was bypassed.
97
+ if [ "$UNSAFE_SUPPORT" = "effective" ]; then
98
+ echo "[${COMPONENT}] Note: --dangerously-force-unsafe-install is required because"
99
+ echo "[${COMPONENT}] this plugin wraps tokenless/rtk system binaries via child_process."
100
+ echo "[${COMPONENT}] See https://github.com/alibaba/anolisa for source."
101
+ fi
67
102
 
68
- env -u OPENCLAW_HOME OPENCLAW_STATE_DIR="$OPENCLAW_STATE_DIR" "$OPENCLAW_BIN" plugins install "$PLUGIN_SRC" \
69
- --force --dangerously-force-unsafe-install || {
103
+ env -u OPENCLAW_HOME OPENCLAW_STATE_DIR="$OPENCLAW_STATE_DIR" "$OPENCLAW_BIN" "${INSTALL_ARGS[@]}" || {
70
104
  echo "[${COMPONENT}] openclaw CLI install failed — check OpenClaw version >= 5.0.0" >&2
105
+ if [ "$UNSAFE_SUPPORT" = "noop" ]; then
106
+ echo "[${COMPONENT}] this OpenClaw advertises --dangerously-force-unsafe-install as a" >&2
107
+ echo "[${COMPONENT}] deprecated no-op; the safety scan follows the operator-owned" >&2
108
+ echo "[${COMPONENT}] security.installPolicy instead." >&2
109
+ fi
71
110
  exit 1
72
111
  }
73
112
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokenless",
3
- "version": "0.7.14",
3
+ "version": "0.8.1",
4
4
  "description": "Token-Less LLM Token Optimization Toolkit for Qoder CLI",
5
5
  "author": {
6
6
  "name": "Shile Zhang",
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env bash
2
+ # run-hook.sh — Locate and exec a shared tokenless hook script.
3
+ #
4
+ # Prefer hooks from the wrapper's own adapter tree so concurrent RPM, Makefile,
5
+ # and raw installations cannot cross-load another version. Hosts that copy the
6
+ # wrapper into a detached cache still use the historical FHS fallbacks.
7
+ #
8
+ # Usage: run-hook.sh <hook-script-basename> [args...]
9
+ # Examples: run-hook.sh rewrite_hook.py
10
+ # run-hook.sh compress_response_hook.py
11
+ # run-hook.sh tool_ready_hook.sh
12
+ #
13
+ # Fail-open contract: any not-found / missing-interpreter condition emits
14
+ # an empty JSON object on stdout and exits 0, so the host never blocks
15
+ # on us.
16
+ #
17
+ # PreToolUse matcher overlap is by design: hooks.json registers both a
18
+ # Bash-specific entry (rewrite) and a catch-all entry (tool-ready). Bash
19
+ # tool calls therefore fire both — the host evaluates each matching
20
+ # matcher independently, so this is the documented way to attach a
21
+ # tool-specific hook alongside a global one. The timeout values are
22
+ # upper bounds; observed runtimes are well under them.
23
+ set -euo pipefail
24
+
25
+ SCRIPT_DIR="$(CDPATH='' cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
26
+ SCRIPT="${1:?usage: run-hook.sh <hook-script-basename> [args...]}"
27
+ shift
28
+
29
+ fail_open() { echo "{}"; exit 0; }
30
+
31
+ # Reject anything but a bare basename. Practical risk is low — hooks.json is
32
+ # RPM-installed root:root 0644 and unwritable at runtime — but a defense-in-
33
+ # depth check costs one line and stops any future caller from smuggling a
34
+ # traversal segment through this argument.
35
+ case "$SCRIPT" in
36
+ */*|"") fail_open ;;
37
+ esac
38
+
39
+ CANDIDATES=(
40
+ "${SCRIPT_DIR}/../../common/hooks/${SCRIPT}"
41
+ "/usr/local/share/anolisa/adapters/tokenless/common/hooks/${SCRIPT}"
42
+ "/usr/share/anolisa/adapters/tokenless/common/hooks/${SCRIPT}"
43
+ "${HOME}/.local/share/anolisa/adapters/tokenless/common/hooks/${SCRIPT}"
44
+ )
45
+
46
+ for candidate in "${CANDIDATES[@]}"; do
47
+ [ -f "$candidate" ] || continue
48
+ case "$candidate" in
49
+ *.py)
50
+ command -v python3 >/dev/null 2>&1 || fail_open
51
+ exec python3 "$candidate" "$@"
52
+ ;;
53
+ *.sh)
54
+ exec bash "$candidate" "$@"
55
+ ;;
56
+ *)
57
+ fail_open
58
+ ;;
59
+ esac
60
+ done
61
+
62
+ fail_open
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokenless",
3
- "version": "0.7.14",
3
+ "version": "0.8.1",
4
4
  "description": "Token-Less context compression for Qwen Code — RTK command rewriting, response/TOON/schema compression, and registered but hard-disabled Tool Ready",
5
5
  "author": { "name": "ANOLISA" },
6
6
  "license": "Apache-2.0",
@@ -29,8 +29,8 @@
29
29
  "type": "command",
30
30
  "name": "tokenless-rewrite",
31
31
  "description": "Rewrites shell commands via rtk for token savings",
32
- "command": "bash \"${extensionPath}/hooks/run-hook.sh\" rewrite_hook.py",
33
- "timeout": 5000,
32
+ "command": "bash \"${extensionPath}/hooks/run-hook.sh\" rewrite_hook.py --agent-id qwencode",
33
+ "timeout": 10000,
34
34
  "env": { "TOKENLESS_AGENT_ID": "qwencode" }
35
35
  }
36
36
  ]
@@ -44,7 +44,7 @@
44
44
  "type": "command",
45
45
  "name": "tokenless-compress-response",
46
46
  "description": "Compresses tool responses and encodes to TOON format",
47
- "command": "bash \"${extensionPath}/hooks/run-hook.sh\" compress_response_hook.py",
47
+ "command": "bash \"${extensionPath}/hooks/run-hook.sh\" compress_response_hook.py --agent-id qwencode",
48
48
  "timeout": 10000,
49
49
  "env": { "TOKENLESS_AGENT_ID": "qwencode" }
50
50
  }
@@ -0,0 +1,17 @@
1
+ {
2
+ "id": "tokenless",
3
+ "name": "Tokenless",
4
+ "version": "0.8.1",
5
+ "type": "general",
6
+ "description": "ANOLISA Tokenless: compress tool schemas and tool results before they reach the model",
7
+ "author": "ANOLISA",
8
+ "entry": {
9
+ "backend": "plugin.py"
10
+ },
11
+ "dependencies": [],
12
+ "qwenpaw_version": {
13
+ "min": "2.0.0",
14
+ "max": "3.0.0"
15
+ },
16
+ "meta": {}
17
+ }
@@ -0,0 +1,390 @@
1
+ """Tokenless plugin for QwenPaw.
2
+
3
+ Registers one AgentScope middleware that runs the complete Tokenless lifecycle
4
+ in-process through ``anolisa_tokenless.TokenlessSdk``: tool schema compression
5
+ before each model call, RTK command rewriting before ``execute_shell_command``,
6
+ tool result compression after every tool call, and a marker-authorized
7
+ ``tokenless_retrieve`` tool.
8
+
9
+ QwenPaw imports this file once at install time before the plugin's
10
+ ``requirements.txt`` is installed, so ``anolisa_tokenless`` is imported inside
11
+ ``register`` rather than at module level. Tools outside the built-in contract
12
+ table are passed through untouched.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ import logging
19
+ from collections.abc import AsyncGenerator, Callable
20
+ from pathlib import Path
21
+ from typing import Any
22
+
23
+ from agentscope.message import TextBlock, ToolResultState
24
+ from agentscope.middleware import MiddlewareBase
25
+ from agentscope.tool import ToolChunk, ToolResponse
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+ AGENT_ID = "qwenpaw"
30
+ STATE_KEY = "anolisa_tokenless"
31
+ RETRIEVE_TOOL = "tokenless_retrieve"
32
+ RETRIEVE_DECLARATION = {
33
+ "type": "function",
34
+ "function": {
35
+ "name": RETRIEVE_TOOL,
36
+ "description": (
37
+ "Restore omitted content when needed. Pass only the 24-character hash "
38
+ "from a visible Tokenless recovery instruction, not the whole instruction."
39
+ ),
40
+ "parameters": {
41
+ "type": "object",
42
+ "properties": {
43
+ "hash_or_marker": {
44
+ "type": "string",
45
+ "description": (
46
+ "The 24-character hash from a visible recovery instruction; "
47
+ "historical Tokenless markers are also accepted"
48
+ ),
49
+ }
50
+ },
51
+ "required": ["hash_or_marker"],
52
+ "additionalProperties": False,
53
+ },
54
+ },
55
+ }
56
+
57
+ # tool name -> (content_origin wire value, RTK command field) for QwenPaw
58
+ # 2.2.0's built-in tools. Core passes `file_content` through untouched, so a
59
+ # tool absent from this table (skills, MCP servers, newer built-ins) falls
60
+ # back to it and is never compressed or rewritten.
61
+ _CONTRACTS: dict[str, tuple[str, str | None]] = {
62
+ "execute_shell_command": ("command_output", "command"),
63
+ **{
64
+ name: ("file_content", None)
65
+ for name in ("read_file", "recall_history", "view_image", "view_video")
66
+ },
67
+ **{
68
+ name: ("api_response", None)
69
+ for name in (
70
+ "glob_search",
71
+ "grep_search",
72
+ "ast_search",
73
+ "web_fetch",
74
+ "web_search",
75
+ "memory_search",
76
+ "get_current_time",
77
+ "get_token_usage",
78
+ "list_agents",
79
+ "check_agent_task",
80
+ "chat_with_agent",
81
+ "submit_to_agent",
82
+ "spawn_subagent",
83
+ "delegate_external_agent",
84
+ "browser",
85
+ "desktop_screenshot",
86
+ "run_tool_batch",
87
+ "send_file_to_user",
88
+ "set_user_timezone",
89
+ "activate_f1_exploration_mode",
90
+ "edit_file",
91
+ "write_file",
92
+ "append_file",
93
+ "materialize_skill",
94
+ )
95
+ },
96
+ }
97
+ _DEFAULT_CONTRACT: tuple[str, str | None] = ("file_content", None)
98
+
99
+ # Every name the middleware and the retrieve tool import from
100
+ # anolisa_tokenless. A released wheel that predates one of them imports fine
101
+ # at registration and would fail at the first model call instead, so the
102
+ # plugin refuses to register against such a wheel.
103
+ _REQUIRED_SDK_NAMES = (
104
+ "Attribution",
105
+ "BeforeModelCapabilities",
106
+ "BeforeModelRequest",
107
+ "ContentOrigin",
108
+ "OutputOptimization",
109
+ "PostToolCapabilities",
110
+ "PostToolRequest",
111
+ "PreToolAction",
112
+ "PreToolCapabilities",
113
+ "PreToolRequest",
114
+ "RecoveryMethod",
115
+ "ResultKind",
116
+ "RetrieveRequest",
117
+ "TokenlessConfig",
118
+ "TokenlessError",
119
+ "TokenlessSdk",
120
+ "ToolResultStatus",
121
+ )
122
+
123
+ _SDKS: dict[str, Any] = {}
124
+
125
+
126
+ def _sdk_for(data_dir: str) -> Any:
127
+ sdk = _SDKS.get(data_dir)
128
+ if sdk is None:
129
+ from anolisa_tokenless import TokenlessConfig, TokenlessSdk
130
+
131
+ sdk = TokenlessSdk(
132
+ TokenlessConfig(
133
+ data_dir=data_dir, rtk_enabled=True, retrieve_tool_name=RETRIEVE_TOOL
134
+ )
135
+ )
136
+ _SDKS[data_dir] = sdk
137
+ return sdk
138
+
139
+
140
+ class TokenlessMiddleware(MiddlewareBase):
141
+ """Apply the Tokenless lifecycle to one QwenPaw agent request."""
142
+
143
+ def __init__(self, sdk: Any, data_dir: str) -> None:
144
+ self.sdk = sdk
145
+ self.data_dir = data_dir
146
+
147
+ async def on_model_call(
148
+ self,
149
+ agent: Any,
150
+ input_kwargs: dict[str, Any],
151
+ next_handler: Callable[..., Any],
152
+ ) -> Any:
153
+ from anolisa_tokenless import (
154
+ Attribution,
155
+ BeforeModelCapabilities,
156
+ BeforeModelRequest,
157
+ RecoveryMethod,
158
+ )
159
+
160
+ tools = [
161
+ tool
162
+ for tool in input_kwargs["tools"]
163
+ if tool.get("function", {}).get("name") != RETRIEVE_TOOL
164
+ ]
165
+ transformed = await self.sdk.before_model(
166
+ BeforeModelRequest(
167
+ tools=tuple(tools),
168
+ visible_context=json.dumps(
169
+ input_kwargs["messages"], ensure_ascii=False, default=str
170
+ ),
171
+ capabilities=BeforeModelCapabilities(
172
+ replace_tools=True, recovery=RecoveryMethod.tool(RETRIEVE_TOOL)
173
+ ),
174
+ attribution=Attribution(AGENT_ID, agent.state.session_id),
175
+ )
176
+ )
177
+ agent.state.middle_context[STATE_KEY] = {
178
+ "visible_markers": sorted(transformed.visible_markers),
179
+ "agent_id": AGENT_ID,
180
+ "data_dir": self.data_dir,
181
+ }
182
+ tools = list(transformed.tools)
183
+ tools.append(RETRIEVE_DECLARATION)
184
+ return await next_handler(**{**input_kwargs, "tools": tools})
185
+
186
+ async def on_acting(
187
+ self,
188
+ agent: Any,
189
+ input_kwargs: dict[str, Any],
190
+ next_handler: Callable[..., AsyncGenerator[Any, None]],
191
+ ) -> AsyncGenerator[Any, None]:
192
+ from anolisa_tokenless import (
193
+ Attribution,
194
+ OutputOptimization,
195
+ PreToolAction,
196
+ PreToolCapabilities,
197
+ PreToolRequest,
198
+ )
199
+
200
+ source = input_kwargs["tool_call"]
201
+ if source.name == RETRIEVE_TOOL:
202
+ async for item in next_handler(**input_kwargs):
203
+ yield item
204
+ return
205
+
206
+ origin, command_field = _CONTRACTS.get(source.name, _DEFAULT_CONTRACT)
207
+ attribution = Attribution(AGENT_ID, agent.state.session_id, source.id)
208
+ optimization = OutputOptimization.NONE
209
+ forwarded = source
210
+ if command_field is not None:
211
+ arguments = json.loads(source.input)
212
+ if not isinstance(arguments, dict):
213
+ raise TypeError(f"{source.name} arguments must be a JSON object")
214
+ transformed = await self.sdk.pre_tool(
215
+ PreToolRequest(
216
+ tool_name=source.name,
217
+ arguments=arguments,
218
+ command_field=command_field,
219
+ capabilities=PreToolCapabilities(
220
+ replace_arguments=True, block_and_suggest=False
221
+ ),
222
+ attribution=attribution,
223
+ )
224
+ )
225
+ if transformed.action is PreToolAction.BLOCK_AND_SUGGEST:
226
+ raise RuntimeError(
227
+ "Core returned block_and_suggest without host capability"
228
+ )
229
+ optimization = transformed.output_optimization
230
+ forwarded = source.model_copy(
231
+ update={
232
+ "input": json.dumps(
233
+ transformed.arguments,
234
+ ensure_ascii=False,
235
+ separators=(",", ":"),
236
+ )
237
+ }
238
+ )
239
+ async for item in next_handler(**{**input_kwargs, "tool_call": forwarded}):
240
+ if isinstance(item, ToolResponse):
241
+ yield await self._after_response(
242
+ item, source.name, origin, optimization, attribution
243
+ )
244
+ else:
245
+ yield item
246
+
247
+ async def _after_response(
248
+ self,
249
+ response: ToolResponse,
250
+ tool_name: str,
251
+ origin: str,
252
+ optimization: Any,
253
+ attribution: Any,
254
+ ) -> ToolResponse:
255
+ from anolisa_tokenless import (
256
+ ContentOrigin,
257
+ OutputOptimization,
258
+ PostToolCapabilities,
259
+ PostToolRequest,
260
+ RecoveryMethod,
261
+ ResultKind,
262
+ ToolResultStatus,
263
+ )
264
+
265
+ status = {
266
+ ToolResultState.SUCCESS: ToolResultStatus.SUCCESS,
267
+ ToolResultState.ERROR: ToolResultStatus.ERROR,
268
+ ToolResultState.INTERRUPTED: ToolResultStatus.INTERRUPTED,
269
+ ToolResultState.DENIED: ToolResultStatus.DENIED,
270
+ }[response.state]
271
+ # Recoverable compression needs the static retrieve tool; RTK output
272
+ # and failed results stay lossless-only, as in tokenless_agentscope.
273
+ recovery = (
274
+ RecoveryMethod.tool(RETRIEVE_TOOL)
275
+ if status is ToolResultStatus.SUCCESS
276
+ and optimization == OutputOptimization.NONE
277
+ else RecoveryMethod()
278
+ )
279
+ content = list(response.content)
280
+ extra_context: str | None = None
281
+ for index, block in enumerate(content):
282
+ if not isinstance(block, TextBlock):
283
+ continue
284
+ transformed = await self.sdk.post_tool(
285
+ PostToolRequest(
286
+ result_kind=ResultKind.TOOL,
287
+ tool_name=tool_name,
288
+ content=block.text,
289
+ status=status,
290
+ content_origin=ContentOrigin(origin),
291
+ output_optimization=optimization,
292
+ capabilities=PostToolCapabilities(
293
+ replace_output=True,
294
+ recovery=recovery,
295
+ replace_with_text=True,
296
+ ),
297
+ attribution=attribution,
298
+ )
299
+ )
300
+ extra_context = extra_context or transformed.additional_context
301
+ if transformed.output != block.text:
302
+ content[index] = block.model_copy(update={"text": transformed.output})
303
+ if extra_context is not None:
304
+ content.append(TextBlock(text=extra_context))
305
+ if content == response.content:
306
+ return response
307
+ return response.model_copy(update={"content": content})
308
+
309
+
310
+ def _factory(ctx: Any, agent_config: Any) -> TokenlessMiddleware:
311
+ del agent_config
312
+ # TokenlessConfig rejects relative data directories.
313
+ data_dir = str(Path(ctx.workspace.workspace_dir).expanduser().resolve() / ".tokenless")
314
+ return TokenlessMiddleware(_sdk_for(data_dir), data_dir)
315
+
316
+
317
+ async def tokenless_retrieve(hash_or_marker: str) -> ToolChunk:
318
+ """Restore omitted content behind a visible Tokenless recovery instruction.
319
+
320
+ Args:
321
+ hash_or_marker: The 24-character hash from a visible recovery
322
+ instruction; historical <<tokenless:HASH>> markers are accepted.
323
+ """
324
+ from anolisa_tokenless import Attribution, RetrieveRequest, TokenlessError
325
+ from qwenpaw.config.context import get_current_agent_state
326
+
327
+ state = get_current_agent_state()
328
+ saved = getattr(state, "middle_context", {}).get(STATE_KEY) if state else None
329
+ if not saved:
330
+ return ToolChunk(
331
+ content=[TextBlock(text="Tokenless has no marker state for this session")],
332
+ state=ToolResultState.ERROR,
333
+ )
334
+ try:
335
+ response = await _sdk_for(saved["data_dir"]).retrieve(
336
+ RetrieveRequest(
337
+ hash_or_marker,
338
+ frozenset(saved["visible_markers"]),
339
+ Attribution(saved["agent_id"], state.session_id),
340
+ )
341
+ )
342
+ except TokenlessError as error:
343
+ return ToolChunk(
344
+ content=[TextBlock(text=str(error))], state=ToolResultState.ERROR
345
+ )
346
+ return ToolChunk(
347
+ content=[TextBlock(text=response.payload)], state=ToolResultState.SUCCESS
348
+ )
349
+
350
+
351
+ class TokenlessPlugin:
352
+ """QwenPaw plugin entry point."""
353
+
354
+ def register(self, api: Any) -> None:
355
+ import anolisa_tokenless
356
+
357
+ manifest = json.loads(
358
+ (Path(__file__).parent / "plugin.json").read_text(encoding="utf-8")
359
+ )
360
+ missing = [
361
+ name for name in _REQUIRED_SDK_NAMES if not hasattr(anolisa_tokenless, name)
362
+ ]
363
+ if missing:
364
+ logger.error(
365
+ "tokenless: anolisa_tokenless %s lacks %s required by plugin %s; "
366
+ "install the anolisa_tokenless wheel from the tokenless/v%s release "
367
+ "into QwenPaw's Python environment. Tokenless stays disabled.",
368
+ anolisa_tokenless.__version__,
369
+ ", ".join(missing),
370
+ manifest["version"],
371
+ manifest["version"],
372
+ )
373
+ return
374
+ if anolisa_tokenless.__version__ != manifest["version"]:
375
+ logger.warning(
376
+ "tokenless: plugin %s runs against anolisa_tokenless %s",
377
+ manifest["version"],
378
+ anolisa_tokenless.__version__,
379
+ )
380
+ api.register_middleware(_factory, priority=100)
381
+ api.register_tool(
382
+ tool_name=RETRIEVE_TOOL,
383
+ tool_func=tokenless_retrieve,
384
+ description="Restore content behind a Tokenless marker",
385
+ enabled=True,
386
+ tool_type="internal",
387
+ )
388
+
389
+
390
+ plugin = TokenlessPlugin()
@@ -0,0 +1,6 @@
1
+ # Installed by QwenPaw's plugin loader into QwenPaw's own Python environment.
2
+ # The native wheel is published only as a Tokenless GitHub Release asset; pip
3
+ # picks the single line whose environment marker matches this host.
4
+ anolisa-tokenless @ https://github.com/alibaba/anolisa/releases/download/tokenless/v0.8.1/anolisa_tokenless-0.8.1-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl ; sys_platform == "linux" and platform_machine == "x86_64"
5
+ anolisa-tokenless @ https://github.com/alibaba/anolisa/releases/download/tokenless/v0.8.1/anolisa_tokenless-0.8.1-cp311-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl ; sys_platform == "linux" and platform_machine == "aarch64"
6
+ anolisa-tokenless @ https://github.com/alibaba/anolisa/releases/download/tokenless/v0.8.1/anolisa_tokenless-0.8.1-cp311-abi3-macosx_11_0_arm64.whl ; sys_platform == "darwin" and platform_machine == "arm64"