anolisa-tokenless 0.7.13 → 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 (34) hide show
  1. package/README.md +219 -87
  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 +5 -4
  5. package/adapters/tokenless/codex/README.md +22 -32
  6. package/adapters/tokenless/codex/hooks/hooks.json +3 -3
  7. package/adapters/tokenless/codex/scripts/response-diagnostics +170 -0
  8. package/adapters/tokenless/common/cosh-extension.json +4 -4
  9. package/adapters/tokenless/common/hooks/compress_response_hook.py +250 -307
  10. package/adapters/tokenless/common/hooks/compress_schema_hook.py +34 -42
  11. package/adapters/tokenless/common/hooks/hook_utils.py +281 -44
  12. package/adapters/tokenless/common/hooks/rewrite_hook.py +53 -169
  13. package/adapters/tokenless/dsh/dist/index.js +353 -264
  14. package/adapters/tokenless/dsh/package.json +2 -2
  15. package/adapters/tokenless/hermes/__init__.py +191 -355
  16. package/adapters/tokenless/hermes/plugin.yaml +2 -2
  17. package/adapters/tokenless/manifest.json +18 -3
  18. package/adapters/tokenless/openclaw/dist/index.d.ts +4 -16
  19. package/adapters/tokenless/openclaw/dist/index.js +291 -507
  20. package/adapters/tokenless/openclaw/index.ts +408 -628
  21. package/adapters/tokenless/openclaw/openclaw.plugin.json +4 -20
  22. package/adapters/tokenless/openclaw/package.json +6 -4
  23. package/adapters/tokenless/qoder/.qoder-plugin/plugin.json +1 -1
  24. package/adapters/tokenless/qwencode/hooks/run-hook.sh +62 -0
  25. package/adapters/tokenless/qwencode/qwen-extension.json +4 -4
  26. package/adapters/tokenless/qwenpaw/plugin.json +17 -0
  27. package/adapters/tokenless/qwenpaw/plugin.py +390 -0
  28. package/adapters/tokenless/qwenpaw/requirements.txt +6 -0
  29. package/adapters/tokenless/qwenpaw/scripts/detect.sh +131 -0
  30. package/adapters/tokenless/qwenpaw/scripts/install.sh +98 -0
  31. package/adapters/tokenless/qwenpaw/scripts/uninstall.sh +61 -0
  32. package/package.json +5 -5
  33. package/adapters/tokenless/codex/scripts/compress-response +0 -445
  34. package/adapters/tokenless/common/hooks/compress_toon_hook.py +0 -171
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "id": "tokenless",
3
3
  "name": "Tokenless",
4
- "version": "0.7.13",
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.0",
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.13",
4
- "description": "Unified OpenClaw plugin RTK command rewriting + tokenless schema/response compression for 60-90% LLM token savings",
3
+ "version": "0.8.0",
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": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokenless",
3
- "version": "0.7.13",
3
+ "version": "0.8.0",
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.13",
3
+ "version": "0.8.0",
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.0",
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.0/anolisa_tokenless-0.8.0-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.0/anolisa_tokenless-0.8.0-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.0/anolisa_tokenless-0.8.0-cp311-abi3-macosx_11_0_arm64.whl ; sys_platform == "darwin" and platform_machine == "arm64"