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.
- package/README.md +219 -87
- 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 +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 +4 -4
- package/adapters/tokenless/common/hooks/compress_response_hook.py +250 -307
- package/adapters/tokenless/common/hooks/compress_schema_hook.py +34 -42
- package/adapters/tokenless/common/hooks/hook_utils.py +281 -44
- 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 -355
- package/adapters/tokenless/hermes/plugin.yaml +2 -2
- package/adapters/tokenless/manifest.json +18 -3
- 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/codex/scripts/compress-response +0 -445
- 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.
|
|
5
|
-
"description": "
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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.
|
|
4
|
-
"description": "
|
|
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": {
|
|
@@ -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.
|
|
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":
|
|
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"
|