@agentunion/kite 1.3.2 → 1.5.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 (293) hide show
  1. package/CHANGELOG.md +302 -0
  2. package/cli.js +119 -4
  3. package/core/dependency_checker.py +250 -0
  4. package/core/env_checker.py +490 -0
  5. package/dependencies_lock.json +128 -0
  6. package/extensions/agents/assistant/entry.py +111 -1
  7. package/extensions/agents/assistant/server.py +279 -215
  8. package/extensions/channels/acp_channel/entry.py +111 -1
  9. package/extensions/channels/acp_channel/module.md +23 -22
  10. package/extensions/channels/acp_channel/server.py +279 -215
  11. package/extensions/event_hub_bench/entry.py +107 -1
  12. package/extensions/services/backup/entry.py +306 -21
  13. package/extensions/services/backup/module.md +24 -22
  14. package/extensions/services/evol/auth_manager.py +443 -0
  15. package/extensions/services/evol/config.yaml +149 -0
  16. package/extensions/services/evol/config_loader.py +117 -0
  17. package/extensions/services/evol/entry.py +406 -0
  18. package/extensions/services/evol/evol_api.py +173 -0
  19. package/extensions/services/evol/evol_config.json5 +29 -0
  20. package/extensions/services/evol/migrate_tokens.py +122 -0
  21. package/extensions/services/evol/module.md +32 -0
  22. package/extensions/services/evol/pairing.py +250 -0
  23. package/extensions/services/evol/pairing_codes.jsonl +1 -0
  24. package/extensions/services/evol/relay.py +682 -0
  25. package/extensions/services/evol/relay_config.json5 +67 -0
  26. package/extensions/services/evol/routes/__init__.py +1 -0
  27. package/extensions/services/evol/routes/routes_management_ws.py +127 -0
  28. package/extensions/services/evol/routes/routes_rpc.py +89 -0
  29. package/extensions/services/evol/routes/routes_test.py +61 -0
  30. package/extensions/services/evol/server.py +875 -0
  31. package/extensions/services/evol/static/css/style.css +1200 -0
  32. package/extensions/services/evol/static/index.html +781 -0
  33. package/extensions/services/evol/static/index_evol.html +14 -0
  34. package/extensions/services/evol/static/js/app.js +6304 -0
  35. package/extensions/services/evol/static/js/auth.js +326 -0
  36. package/extensions/services/evol/static/js/dialog.js +285 -0
  37. package/extensions/services/evol/static/js/evol-app-fixed.js +50 -0
  38. package/extensions/services/evol/static/js/evol-app.js +1949 -0
  39. package/extensions/services/evol/static/js/evol-app.js.bak +1800 -0
  40. package/extensions/services/evol/static/js/kernel-client-example.js +228 -0
  41. package/extensions/services/evol/static/js/kernel-client.js +396 -0
  42. package/extensions/services/evol/static/js/main.js +141 -0
  43. package/extensions/services/evol/static/js/registry-tests.js +585 -0
  44. package/extensions/services/evol/static/js/stats.js +217 -0
  45. package/extensions/services/evol/static/js/token-manager.js +175 -0
  46. package/extensions/services/evol/static/pairing.html +248 -0
  47. package/extensions/services/evol/static/test_registry.html +262 -0
  48. package/extensions/services/evol/static/test_relay.html +462 -0
  49. package/extensions/services/evol/stats_manager.py +240 -0
  50. package/extensions/services/model_service/entry.py +167 -19
  51. package/extensions/services/model_service/module.md +21 -22
  52. package/extensions/services/proxy/.claude/settings.local.json +13 -0
  53. package/extensions/services/proxy/CHANGELOG_20260308.md +258 -0
  54. package/extensions/services/proxy/_fix_prints.py +133 -0
  55. package/extensions/services/proxy/_fix_prints2.py +87 -0
  56. package/extensions/services/proxy/agentcp/LICENCE +178 -0
  57. package/extensions/services/proxy/agentcp/README copy.md +85 -0
  58. package/extensions/services/proxy/agentcp/README.md +260 -0
  59. package/extensions/services/proxy/agentcp/__init__.py +16 -0
  60. package/extensions/services/proxy/agentcp/agent.py +4 -0
  61. package/extensions/services/proxy/agentcp/agentcp.py +2494 -0
  62. package/extensions/services/proxy/agentcp/agentprofile.json +89 -0
  63. package/extensions/services/proxy/agentcp/ap/__init__.py +16 -0
  64. package/extensions/services/proxy/agentcp/ap/ap_client.py +316 -0
  65. package/extensions/services/proxy/agentcp/assets/images/wechat_qr.png +0 -0
  66. package/extensions/services/proxy/agentcp/backup/metrics.json +31 -0
  67. package/extensions/services/proxy/agentcp/base/__init__.py +20 -0
  68. package/extensions/services/proxy/agentcp/base/auth_client.py +257 -0
  69. package/extensions/services/proxy/agentcp/base/client.py +112 -0
  70. package/extensions/services/proxy/agentcp/base/env.py +34 -0
  71. package/extensions/services/proxy/agentcp/base/html_util.py +336 -0
  72. package/extensions/services/proxy/agentcp/base/log.py +98 -0
  73. package/extensions/services/proxy/agentcp/ca/__init__.py +17 -0
  74. package/extensions/services/proxy/agentcp/ca/ca_client.py +414 -0
  75. package/extensions/services/proxy/agentcp/ca/ca_root.py +74 -0
  76. package/extensions/services/proxy/agentcp/context/__init__.py +20 -0
  77. package/extensions/services/proxy/agentcp/context/context.py +73 -0
  78. package/extensions/services/proxy/agentcp/context/exceptions.py +114 -0
  79. package/extensions/services/proxy/agentcp/create_profile.py +125 -0
  80. package/extensions/services/proxy/agentcp/create_profile_weather.py +125 -0
  81. package/extensions/services/proxy/agentcp/db/__init__.py +15 -0
  82. package/extensions/services/proxy/agentcp/db/db_mananger.py +550 -0
  83. package/extensions/services/proxy/agentcp/docs/UDP_HEARTBEAT_FIX_REPORT.md +265 -0
  84. package/extensions/services/proxy/agentcp/docs/heartbeat_issue_analysis.md +291 -0
  85. package/extensions/services/proxy/agentcp/file/__init__.py +16 -0
  86. package/extensions/services/proxy/agentcp/file/file_client.py +141 -0
  87. package/extensions/services/proxy/agentcp/file/wss_binary_message.py +137 -0
  88. package/extensions/services/proxy/agentcp/hcp.py +299 -0
  89. package/extensions/services/proxy/agentcp/heartbeat/__init__.py +16 -0
  90. package/extensions/services/proxy/agentcp/heartbeat/heartbeat_client.py +360 -0
  91. package/extensions/services/proxy/agentcp/improved_scheduler.py +498 -0
  92. package/extensions/services/proxy/agentcp/llm_agent_utils.py +249 -0
  93. package/extensions/services/proxy/agentcp/llm_server.py +172 -0
  94. package/extensions/services/proxy/agentcp/mermaid.py +210 -0
  95. package/extensions/services/proxy/agentcp/message.py +149 -0
  96. package/extensions/services/proxy/agentcp/metrics.py +256 -0
  97. package/extensions/services/proxy/agentcp/monitoring/__init__.py +20 -0
  98. package/extensions/services/proxy/agentcp/monitoring/global_monitor.py +27 -0
  99. package/extensions/services/proxy/agentcp/monitoring/metrics_store.py +325 -0
  100. package/extensions/services/proxy/agentcp/monitoring/monitoring_service.py +269 -0
  101. package/extensions/services/proxy/agentcp/monitoring/sliding_window.py +222 -0
  102. package/extensions/services/proxy/agentcp/monitoring/standalone_reader.py +224 -0
  103. package/extensions/services/proxy/agentcp/msg/__init__.py +21 -0
  104. package/extensions/services/proxy/agentcp/msg/connection_manager.py +456 -0
  105. package/extensions/services/proxy/agentcp/msg/message_client.py +2058 -0
  106. package/extensions/services/proxy/agentcp/msg/message_serialize.py +263 -0
  107. package/extensions/services/proxy/agentcp/msg/open_ai_message.py +88 -0
  108. package/extensions/services/proxy/agentcp/msg/session_manager.py +1062 -0
  109. package/extensions/services/proxy/agentcp/msg/stream_client.py +267 -0
  110. package/extensions/services/proxy/agentcp/msg/websocket_file_receiver.py +89 -0
  111. package/extensions/services/proxy/agentcp/msg/ws_logger.py +685 -0
  112. package/extensions/services/proxy/agentcp/msg/wss_binary_message.py +137 -0
  113. package/extensions/services/proxy/agentcp/requirements.txt +7 -0
  114. package/extensions/services/proxy/agentcp/samples/agent_graph/README.md +37 -0
  115. package/extensions/services/proxy/agentcp/samples/agent_graph/agentprofile.json +89 -0
  116. package/extensions/services/proxy/agentcp/samples/agent_graph/create_profile.py +138 -0
  117. package/extensions/services/proxy/agentcp/samples/agent_graph/main.py +164 -0
  118. package/extensions/services/proxy/agentcp/samples/agent_use/create_profile.py +123 -0
  119. package/extensions/services/proxy/agentcp/samples/agent_use/llm/create_profile.py +129 -0
  120. package/extensions/services/proxy/agentcp/samples/agent_use/llm/env.json +5 -0
  121. package/extensions/services/proxy/agentcp/samples/agent_use/llm/main.py +146 -0
  122. package/extensions/services/proxy/agentcp/samples/agent_use/main.py +123 -0
  123. package/extensions/services/proxy/agentcp/samples/agent_use/readme.md +379 -0
  124. package/extensions/services/proxy/agentcp/samples/agent_use/search/create_profile.py +129 -0
  125. package/extensions/services/proxy/agentcp/samples/agent_use/search/main.py +28 -0
  126. package/extensions/services/proxy/agentcp/samples/agent_use/tool/create_profile.py +129 -0
  127. package/extensions/services/proxy/agentcp/samples/agent_use/tool/main.py +20 -0
  128. package/extensions/services/proxy/agentcp/samples/ali_amap/README.md +97 -0
  129. package/extensions/services/proxy/agentcp/samples/ali_amap/amap_agent.py +88 -0
  130. package/extensions/services/proxy/agentcp/samples/ali_amap/create_profile.py +125 -0
  131. package/extensions/services/proxy/agentcp/samples/compute_agent/agent/powershell.py +228 -0
  132. package/extensions/services/proxy/agentcp/samples/compute_agent/agent/software.py +63 -0
  133. package/extensions/services/proxy/agentcp/samples/compute_agent/agent/tools.py +36 -0
  134. package/extensions/services/proxy/agentcp/samples/compute_agent/browser_user.py +41 -0
  135. package/extensions/services/proxy/agentcp/samples/deepseek/README.md +79 -0
  136. package/extensions/services/proxy/agentcp/samples/deepseek/create_profile.py +126 -0
  137. package/extensions/services/proxy/agentcp/samples/deepseek/deepseek.py +42 -0
  138. package/extensions/services/proxy/agentcp/samples/dify_chat/README.md +78 -0
  139. package/extensions/services/proxy/agentcp/samples/dify_chat/create_profile.py +126 -0
  140. package/extensions/services/proxy/agentcp/samples/dify_chat/dify_chat.py +47 -0
  141. package/extensions/services/proxy/agentcp/samples/dify_workflow/README.md +78 -0
  142. package/extensions/services/proxy/agentcp/samples/dify_workflow/create_profile.py +126 -0
  143. package/extensions/services/proxy/agentcp/samples/dify_workflow/dify_workflow.py +46 -0
  144. package/extensions/services/proxy/agentcp/samples/executor/README.md +44 -0
  145. package/extensions/services/proxy/agentcp/samples/executor/agentprofile.json +89 -0
  146. package/extensions/services/proxy/agentcp/samples/executor/create_profile.py +139 -0
  147. package/extensions/services/proxy/agentcp/samples/executor/main.py +160 -0
  148. package/extensions/services/proxy/agentcp/samples/filereader/README.md +45 -0
  149. package/extensions/services/proxy/agentcp/samples/filereader/agentprofile.json +90 -0
  150. package/extensions/services/proxy/agentcp/samples/filereader/create_profile.py +137 -0
  151. package/extensions/services/proxy/agentcp/samples/filereader/main.py +253 -0
  152. package/extensions/services/proxy/agentcp/samples/filewriter/README.md +38 -0
  153. package/extensions/services/proxy/agentcp/samples/filewriter/agentprofile.json +91 -0
  154. package/extensions/services/proxy/agentcp/samples/filewriter/create_profile.py +138 -0
  155. package/extensions/services/proxy/agentcp/samples/filewriter/main.py +289 -0
  156. package/extensions/services/proxy/agentcp/samples/hcp/README.md +85 -0
  157. package/extensions/services/proxy/agentcp/samples/hcp/acp_weather_agent.zip +0 -0
  158. package/extensions/services/proxy/agentcp/samples/hcp/create_profile.py +125 -0
  159. package/extensions/services/proxy/agentcp/samples/hcp/hcp.py +237 -0
  160. package/extensions/services/proxy/agentcp/samples/helloworld/README.md +68 -0
  161. package/extensions/services/proxy/agentcp/samples/helloworld/hello_world.py +40 -0
  162. package/extensions/services/proxy/agentcp/samples/llm_agent/MEADME.md +117 -0
  163. package/extensions/services/proxy/agentcp/samples/llm_agent/create_profile.py +125 -0
  164. package/extensions/services/proxy/agentcp/samples/llm_agent/qwen_agent.py +136 -0
  165. package/extensions/services/proxy/agentcp/samples/local_llm_agent/README.md +90 -0
  166. package/extensions/services/proxy/agentcp/samples/local_llm_agent/create_profile.py +125 -0
  167. package/extensions/services/proxy/agentcp/samples/local_llm_agent/main.py +49 -0
  168. package/extensions/services/proxy/agentcp/samples/query_llm_from_agent/README.md +55 -0
  169. package/extensions/services/proxy/agentcp/samples/query_llm_from_agent/create_profile.py +125 -0
  170. package/extensions/services/proxy/agentcp/samples/query_llm_from_agent/main.py +23 -0
  171. package/extensions/services/proxy/agentcp/samples/query_weather_api_agent/README.md +103 -0
  172. package/extensions/services/proxy/agentcp/samples/query_weather_api_agent/create_profile.py +125 -0
  173. package/extensions/services/proxy/agentcp/samples/query_weather_api_agent/main.py +69 -0
  174. package/extensions/services/proxy/agentcp/samples/query_weather_from_agent/README.md +58 -0
  175. package/extensions/services/proxy/agentcp/samples/query_weather_from_agent/create_profile.py +125 -0
  176. package/extensions/services/proxy/agentcp/samples/query_weather_from_agent/main.py +25 -0
  177. package/extensions/services/proxy/agentcp/samples/qwen3/README.md +71 -0
  178. package/extensions/services/proxy/agentcp/samples/qwen3/create_profile.py +126 -0
  179. package/extensions/services/proxy/agentcp/samples/qwen3/qwen3.py +37 -0
  180. package/extensions/services/proxy/agentcp/samples/qwen3_tools/README.md +133 -0
  181. package/extensions/services/proxy/agentcp/samples/qwen3_tools/create_profile.py +126 -0
  182. package/extensions/services/proxy/agentcp/samples/qwen3_tools/qwen3_tools.py +98 -0
  183. package/extensions/services/proxy/agentcp/samples/search/create_profile_qwen.py +125 -0
  184. package/extensions/services/proxy/agentcp/samples/search/create_profile_search.py +125 -0
  185. package/extensions/services/proxy/agentcp/samples/search/qwen_agent.py +136 -0
  186. package/extensions/services/proxy/agentcp/samples/search/search_agent.py +170 -0
  187. package/extensions/services/proxy/agentcp/samples/wrapper_agently_to_agent/README.md +89 -0
  188. package/extensions/services/proxy/agentcp/samples/wrapper_agently_to_agent/create_profile.py +125 -0
  189. package/extensions/services/proxy/agentcp/samples/wrapper_agently_to_agent/main.py +44 -0
  190. package/extensions/services/proxy/agentcp/utils/__init__.py +15 -0
  191. package/extensions/services/proxy/agentcp/utils/file_util.py +117 -0
  192. package/extensions/services/proxy/agentcp/utils/proxy_bypass.py +99 -0
  193. package/extensions/services/proxy/agentcp/workflow.py +203 -0
  194. package/extensions/services/proxy/console_auth.py +109 -0
  195. package/extensions/services/proxy/evol/__init__.py +1 -0
  196. package/extensions/services/proxy/evol/config.py +37 -0
  197. package/extensions/services/proxy/evol/http/__init__.py +1 -0
  198. package/extensions/services/proxy/evol/http/async_http.py +551 -0
  199. package/extensions/services/proxy/evol/log.py +28 -0
  200. package/extensions/services/proxy/evol/presenter/__init__.py +2 -0
  201. package/extensions/services/proxy/evol/presenter/agentIdPresenter.py +1031 -0
  202. package/extensions/services/proxy/evol/presenter/apikeyPresenter.py +106 -0
  203. package/extensions/services/proxy/evol/presenter/configPresenter.py +1281 -0
  204. package/extensions/services/proxy/evol/presenter/userPresenter.py +477 -0
  205. package/extensions/services/proxy/evol/server/__init__.py +1 -0
  206. package/extensions/services/proxy/evol/server/claude_proxy_async.py +3430 -0
  207. package/extensions/services/proxy/evol/server/openclaw_proxy.py +1861 -0
  208. package/extensions/services/proxy/evol/server/proxy_config.py +15 -0
  209. package/extensions/services/proxy/evol/server/proxy_engine.py +501 -0
  210. package/extensions/services/proxy/evol/version.py +24 -0
  211. package/extensions/services/proxy/logs/websocket.log +260 -0
  212. package/extensions/services/proxy/main.py +240 -0
  213. package/extensions/services/proxy/requirements.txt +13 -0
  214. package/extensions/services/proxy/server.py +271 -0
  215. package/extensions/services/watchdog/entry.py +215 -26
  216. package/extensions/services/watchdog/module.md +1 -0
  217. package/extensions/services/watchdog/monitor.py +178 -38
  218. package/extensions/services/web/WEBSOCKET_STATUS.md +143 -0
  219. package/extensions/services/web/config_example.py +35 -0
  220. package/extensions/services/web/config_loader.py +110 -0
  221. package/extensions/services/web/entry.py +114 -26
  222. package/extensions/services/web/module.md +35 -24
  223. package/extensions/services/web/pairing.py +250 -0
  224. package/extensions/services/web/pairing_codes.jsonl +16 -0
  225. package/extensions/services/web/relay.py +643 -0
  226. package/extensions/services/web/relay_config.json5 +67 -0
  227. package/extensions/services/web/routes/routes_management_ws.py +127 -0
  228. package/extensions/services/web/routes/routes_rpc.py +89 -0
  229. package/extensions/services/web/routes/routes_test.py +61 -0
  230. package/extensions/services/web/routes/schemas.py +0 -22
  231. package/extensions/services/web/server.py +434 -99
  232. package/extensions/services/web/static/css/style.css +67 -28
  233. package/extensions/services/web/static/index.html +234 -44
  234. package/extensions/services/web/static/js/app.js +1335 -48
  235. package/extensions/services/web/static/js/kernel-client-example.js +161 -0
  236. package/extensions/services/web/static/js/kernel-client.js +383 -0
  237. package/extensions/services/web/static/js/registry-tests.js +558 -0
  238. package/extensions/services/web/static/js/token-manager.js +175 -0
  239. package/extensions/services/web/static/pairing.html +248 -0
  240. package/extensions/services/web/static/test_registry.html +262 -0
  241. package/extensions/services/web/web_config.json5 +29 -0
  242. package/kernel/entry.py +120 -32
  243. package/kernel/event_hub.py +141 -16
  244. package/kernel/module.md +60 -33
  245. package/kernel/registry_store.py +45 -36
  246. package/kernel/rpc_router.py +152 -59
  247. package/kernel/server.py +322 -26
  248. package/kite_cli/__init__.py +3 -0
  249. package/kite_cli/__main__.py +5 -0
  250. package/kite_cli/commands/__init__.py +1 -0
  251. package/kite_cli/commands/clean.py +101 -0
  252. package/kite_cli/commands/deps_install.py +67 -0
  253. package/kite_cli/commands/doctor.py +35 -0
  254. package/kite_cli/commands/env_check.py +45 -0
  255. package/kite_cli/commands/history.py +111 -0
  256. package/kite_cli/commands/info.py +96 -0
  257. package/kite_cli/commands/install.py +313 -0
  258. package/kite_cli/commands/list.py +143 -0
  259. package/kite_cli/commands/log.py +81 -0
  260. package/kite_cli/commands/prepare.py +49 -0
  261. package/kite_cli/commands/rollback.py +88 -0
  262. package/kite_cli/commands/search.py +73 -0
  263. package/kite_cli/commands/uninstall.py +85 -0
  264. package/kite_cli/commands/update.py +118 -0
  265. package/kite_cli/commands/venv_setup.py +56 -0
  266. package/kite_cli/core/__init__.py +1 -0
  267. package/kite_cli/core/checker.py +142 -0
  268. package/kite_cli/core/dependency.py +229 -0
  269. package/kite_cli/core/downloader.py +209 -0
  270. package/kite_cli/core/install_info.py +40 -0
  271. package/kite_cli/core/tool_installer.py +397 -0
  272. package/kite_cli/core/validator.py +78 -0
  273. package/kite_cli/main.py +317 -0
  274. package/kite_cli/utils/__init__.py +1 -0
  275. package/kite_cli/utils/i18n.py +252 -0
  276. package/kite_cli/utils/interactive.py +63 -0
  277. package/kite_cli/utils/operation_log.py +77 -0
  278. package/kite_cli/utils/paths.py +34 -0
  279. package/kite_cli/utils/version.py +308 -0
  280. package/launcher/entry.py +1124 -178
  281. package/launcher/logging_setup.py +104 -0
  282. package/launcher/module.md +46 -37
  283. package/launcher/module_scanner.py +11 -1
  284. package/main.py +4 -1
  285. package/package.json +9 -1
  286. package/python_version.json +4 -0
  287. package/requirements.txt +38 -0
  288. package/scripts/env-manager.js +328 -0
  289. package/scripts/plan_manager.py +315 -0
  290. package/scripts/python-env.js +79 -0
  291. package/scripts/scan_dependencies.py +461 -0
  292. package/scripts/setup-python-env.js +191 -0
  293. package/extensions/services/web/routes/routes_modules.py +0 -249
@@ -1,215 +1,279 @@
1
- """
2
- ACP Channel WebSocket client.
3
- Connects to Kernel via WebSocket JSON-RPC 2.0 for event publishing and subscription.
4
- """
5
-
6
- import asyncio
7
- import json
8
- import time
9
- import uuid
10
- from datetime import datetime, timezone
11
-
12
- import websockets
13
-
14
-
15
- class AcpChannelServer:
16
-
17
- def __init__(self, token: str = "", kernel_port: int = 0, boot_t0: float = 0):
18
- self.token = token
19
- self.kernel_port = kernel_port
20
- self.boot_t0 = boot_t0
21
- self._ws_task: asyncio.Task | None = None
22
- self._test_task: asyncio.Task | None = None
23
- self._ws: object | None = None
24
- self._shutting_down = False
25
- self._start_time = time.time()
26
-
27
- async def run(self):
28
- """Main entry point: start WebSocket loop and test event loop."""
29
- if self.kernel_port:
30
- self._ws_task = asyncio.create_task(self._ws_loop())
31
- self._test_task = asyncio.create_task(self._test_event_loop())
32
-
33
- # Wait for tasks to complete
34
- tasks = [t for t in [self._ws_task, self._test_task] if t]
35
- if tasks:
36
- await asyncio.gather(*tasks, return_exceptions=True)
37
-
38
- print("[acp_channel] Shutdown complete")
39
-
40
- # ── Kernel WebSocket client ──
41
-
42
- async def _ws_loop(self):
43
- """Connect to Kernel, subscribe, register, and listen. Reconnect on failure."""
44
- retry_delay = 0.3
45
- max_delay = 5.0
46
- max_retries = 10
47
- attempt = 0
48
- while not self._shutting_down:
49
- try:
50
- await self._ws_connect()
51
- retry_delay = 0.3
52
- attempt = 0
53
- except asyncio.CancelledError:
54
- return
55
- except Exception as e:
56
- attempt += 1
57
- # Auth failure — don't retry
58
- if hasattr(e, 'rcvd') and e.rcvd is not None:
59
- code = e.rcvd.code if hasattr(e.rcvd, 'code') else 0
60
- if code in (4001, 4003):
61
- print(f"[acp_channel] Kernel 认证失败 (code {code}),退出")
62
- import sys; sys.exit(1)
63
- if attempt >= max_retries:
64
- print(f"[acp_channel] Kernel 重连失败 {max_retries} 次,退出")
65
- import sys; sys.exit(1)
66
- print(f"[acp_channel] Kernel connection error: {e}, retrying in {retry_delay:.1f}s ({attempt}/{max_retries})")
67
- self._ws = None
68
- if self._shutting_down:
69
- return
70
- await asyncio.sleep(retry_delay)
71
- retry_delay = min(retry_delay * 2, max_delay)
72
-
73
- async def _ws_connect(self):
74
- """Single WebSocket session: connect, subscribe, register, ready, receive loop."""
75
- url = f"ws://127.0.0.1:{self.kernel_port}/ws?token={self.token}&id=acp_channel"
76
- print(f"[acp_channel] Connecting to Kernel (port {self.kernel_port})")
77
- async with websockets.connect(url, open_timeout=3, ping_interval=None, ping_timeout=None, close_timeout=10) as ws:
78
- self._ws = ws
79
- elapsed = time.monotonic() - self.boot_t0 if self.boot_t0 else 0
80
- elapsed_str = f" ({elapsed:.1f}s)" if elapsed else ""
81
- print(f"[acp_channel] Connected to Kernel{elapsed_str}")
82
-
83
- # Step 1: Subscribe to events (先订阅)
84
- await self._rpc_call(ws, "event.subscribe", {
85
- "events": ["module.started", "module.stopped", "module.shutdown"],
86
- })
87
-
88
- # Step 2: Register to Kernel (再注册)
89
- await self._rpc_call(ws, "registry.register", {
90
- "module_id": "acp_channel",
91
- "module_type": "channel",
92
- "name": "ACP Channel",
93
- "events_publish": {
94
- "acp_channel.test": {},
95
- },
96
- "events_subscribe": [
97
- "module.started",
98
- "module.stopped",
99
- "module.shutdown",
100
- ],
101
- })
102
-
103
- # Step 3: Publish module.ready (every reconnect)
104
- if not self._shutting_down:
105
- await self._rpc_call(ws, "event.publish", {
106
- "event_id": str(uuid.uuid4()),
107
- "event": "module.ready",
108
- "data": {
109
- "module_id": "acp_channel",
110
- "graceful_shutdown": True,
111
- },
112
- })
113
- elapsed = time.monotonic() - self.boot_t0 if self.boot_t0 else 0
114
- elapsed_str = f" ({elapsed:.1f}s)" if elapsed else ""
115
- print(f"[acp_channel] module.ready published{elapsed_str}")
116
-
117
- # Receive loop
118
- async for raw in ws:
119
- try:
120
- msg = json.loads(raw)
121
- except (json.JSONDecodeError, TypeError):
122
- continue
123
-
124
- try:
125
- has_method = "method" in msg
126
- has_id = "id" in msg
127
-
128
- if has_method and not has_id:
129
- # JSON-RPC Notification (event delivery)
130
- params = msg.get("params", {})
131
- event_name = params.get("event", "")
132
- if event_name == "module.shutdown":
133
- data = params.get("data", {})
134
- target = data.get("module_id", "")
135
- reason = data.get("reason", "")
136
- # Handle both targeted shutdown (module_id == "acp_channel") and broadcast shutdown (no module_id or launcher_lost)
137
- if target == "acp_channel" or not target or reason == "launcher_lost":
138
- await self._handle_shutdown(ws)
139
- return
140
- elif not has_method and has_id:
141
- # JSON-RPC Response (to our RPC calls)
142
- pass
143
- except Exception as e:
144
- print(f"[acp_channel] 事件处理异常(已忽略): {e}")
145
-
146
- async def _handle_shutdown(self, ws):
147
- """Handle module.shutdown: exiting → ack → cleanup → ready → exit."""
148
- print("[acp_channel] Received module.shutdown")
149
- self._shutting_down = True
150
-
151
- # Step 0: Send module.exiting
152
- await self._rpc_call(ws, "event.publish", {
153
- "event_id": str(uuid.uuid4()),
154
- "event": "module.exiting",
155
- "data": {"module_id": "acp_channel", "action": "none"},
156
- })
157
-
158
- # Step 1: Send ack
159
- await self._rpc_call(ws, "event.publish", {
160
- "event_id": str(uuid.uuid4()),
161
- "event": "module.shutdown.ack",
162
- "data": {"module_id": "acp_channel", "estimated_cleanup": 2},
163
- })
164
- print("[acp_channel] shutdown ack sent")
165
-
166
- # Step 2: Cleanup (cancel background tasks)
167
- if self._test_task:
168
- self._test_task.cancel()
169
-
170
- # Step 3: Send ready (before closing WS!)
171
- await self._rpc_call(ws, "event.publish", {
172
- "event_id": str(uuid.uuid4()),
173
- "event": "module.shutdown.ready",
174
- "data": {"module_id": "acp_channel"},
175
- })
176
- print("[acp_channel] Shutdown ready sent")
177
-
178
- # Step 4: Exit process
179
- import sys
180
- sys.exit(0)
181
-
182
- async def _rpc_call(self, ws, method: str, params: dict = None):
183
- """Send a JSON-RPC 2.0 request."""
184
- msg = {"jsonrpc": "2.0", "id": str(uuid.uuid4()), "method": method}
185
- if params:
186
- msg["params"] = params
187
- await ws.send(json.dumps(msg))
188
-
189
- async def _publish_event(self, event: dict):
190
- """Publish an event via JSON-RPC event.publish."""
191
- if not self._ws:
192
- return
193
- try:
194
- await self._rpc_call(self._ws, "event.publish", {
195
- "event_id": str(uuid.uuid4()),
196
- "event": event.get("event", ""),
197
- "data": event.get("data", {}),
198
- })
199
- except Exception as e:
200
- print(f"[acp_channel] Failed to publish event: {e}")
201
-
202
- # ── Test event loop ──
203
-
204
- async def _test_event_loop(self):
205
- """Publish a test event every 10 seconds."""
206
- while True:
207
- await asyncio.sleep(10)
208
- await self._publish_event({
209
- "event": "acp_channel.test",
210
- "data": {
211
- "message": "test event from acp_channel",
212
- "timestamp": datetime.now(timezone.utc).isoformat(),
213
- },
214
- })
215
- print("[acp_channel] test event published")
1
+ """
2
+ ACP Channel WebSocket client.
3
+ Connects to Kernel via WebSocket JSON-RPC 2.0 for event publishing and subscription.
4
+ """
5
+
6
+ import asyncio
7
+ import json
8
+ import os
9
+ import time
10
+ import uuid
11
+ from datetime import datetime, timezone
12
+
13
+ import websockets
14
+
15
+
16
+ # System broadcast events (received by all modules, may not need handling)
17
+ SYSTEM_BROADCAST_EVENTS = {
18
+ "module.ready", "module.registered", "module.started", "module.stopped",
19
+ "module.crashed", "module.exiting", "module.offline",
20
+ "module.shutdown.ack", "module.shutdown.ready",
21
+ "system.ready", "registry.updated",
22
+ }
23
+
24
+
25
+ class AcpChannelServer:
26
+
27
+ def __init__(self, token: str = "", kernel_port: int = 0, boot_t0: float = 0):
28
+ self.token = token
29
+ self.kernel_port = kernel_port
30
+ self.boot_t0 = boot_t0
31
+ self._ws_task: asyncio.Task | None = None
32
+ self._test_task: asyncio.Task | None = None
33
+ self._ws: object | None = None
34
+ self._shutting_down = False
35
+ self._exit_code = 0 # Exit code for main() to use
36
+ self._start_time = time.time()
37
+
38
+ async def run(self):
39
+ """Main entry point: start WebSocket loop and test event loop."""
40
+ if self.kernel_port:
41
+ self._ws_task = asyncio.create_task(self._ws_loop())
42
+ self._test_task = asyncio.create_task(self._test_event_loop())
43
+
44
+ # Wait for tasks to complete
45
+ tasks = [t for t in [self._ws_task, self._test_task] if t]
46
+ if tasks:
47
+ await asyncio.gather(*tasks, return_exceptions=True)
48
+
49
+ print("[acp_channel] Shutdown complete")
50
+
51
+ # ── Kernel WebSocket client ──
52
+
53
+ async def _ws_loop(self):
54
+ """Connect to Kernel, subscribe, register, and listen. Reconnect on failure."""
55
+ retry_delay = 0.3
56
+ max_delay = 5.0
57
+ max_retries = 10
58
+ attempt = 0
59
+ while not self._shutting_down:
60
+ try:
61
+ await self._ws_connect()
62
+ retry_delay = 0.3
63
+ attempt = 0
64
+ except asyncio.CancelledError:
65
+ return
66
+ except Exception as e:
67
+ attempt += 1
68
+ # Auth failure — don't retry
69
+ if hasattr(e, 'rcvd') and e.rcvd is not None:
70
+ code = e.rcvd.code if hasattr(e.rcvd, 'code') else 0
71
+ if code in (4001, 4003):
72
+ print(f"[acp_channel] Kernel 认证失败 (code {code}),退出")
73
+ self._exit_code = 1
74
+ self._shutting_down = True
75
+ return
76
+ if attempt >= max_retries:
77
+ print(f"[acp_channel] Kernel 重连失败 {max_retries} 次,退出")
78
+ self._exit_code = 1
79
+ self._shutting_down = True
80
+ return
81
+ print(f"[acp_channel] Kernel connection error: {e}, retrying in {retry_delay:.1f}s ({attempt}/{max_retries})")
82
+ self._ws = None
83
+ if self._shutting_down:
84
+ return
85
+ await asyncio.sleep(retry_delay)
86
+ retry_delay = min(retry_delay * 2, max_delay)
87
+
88
+ async def _ws_connect(self):
89
+ """Single WebSocket session: connect, subscribe, register, ready, receive loop."""
90
+ url = f"ws://127.0.0.1:{self.kernel_port}/ws?token={self.token}&id=acp_channel"
91
+ print(f"[acp_channel] Connecting to Kernel (port {self.kernel_port})")
92
+ async with websockets.connect(url, open_timeout=3, ping_interval=None, close_timeout=10) as ws:
93
+ self._ws = ws
94
+ elapsed = time.monotonic() - self.boot_t0 if self.boot_t0 else 0
95
+ elapsed_str = f" ({elapsed:.1f}s)" if elapsed else ""
96
+ print(f"[acp_channel] Connected to Kernel{elapsed_str}")
97
+
98
+ # Step 1: Subscribe to events (先订阅)
99
+ await self._rpc_call(ws, "event.subscribe", {
100
+ "events": ["module.started", "module.stopped", "module.shutdown"],
101
+ })
102
+
103
+ # Step 2: Register to Kernel (再注册)
104
+ await self._rpc_call(ws, "registry.register", {
105
+ "module_id": "acp_channel",
106
+ "module_type": "channel",
107
+ "name": "ACP Channel",
108
+ "events_publish": {
109
+ "acp_channel": {
110
+ "test": {},
111
+ }
112
+ },
113
+ "events_subscribe": [
114
+ "module.started",
115
+ "module.stopped",
116
+ "module.shutdown",
117
+ ],
118
+ })
119
+
120
+ # Step 3: Publish module.ready (every reconnect)
121
+ if not self._shutting_down:
122
+ startup_time = time.monotonic() - self.boot_t0 if self.boot_t0 else 0
123
+ await self._rpc_call(ws, "event.publish", {
124
+ "event_id": str(uuid.uuid4()),
125
+ "event": "module.ready",
126
+ "data": {
127
+ "module_id": "acp_channel",
128
+ "graceful_shutdown": True,
129
+ "startup_time": startup_time,
130
+ },
131
+ })
132
+ elapsed_str = self._fmt_elapsed(self.boot_t0)
133
+ print(f"[acp_channel] module.ready published ({elapsed_str})")
134
+
135
+ # Receive loop
136
+ # CRITICAL: RPC 死锁防范
137
+ # - 入站 RPC 请求必须用 create_task() 异步执行,不可 await
138
+ # - 原因:如果 handler 内部调用 rpc_call() 发出站请求,出站响应需要本接收循环来分发
139
+ # - 如果接收循环被 await handler 阻塞,出站响应永远收不到 → 超时死锁
140
+ # - 事件通知和 RPC 响应可以同步处理(它们不会反向调用 rpc_call)
141
+
142
+ async for raw in ws:
143
+ try:
144
+ msg = json.loads(raw)
145
+ except (json.JSONDecodeError, TypeError):
146
+ continue
147
+
148
+ try:
149
+ has_method = "method" in msg
150
+ has_id = "id" in msg
151
+
152
+ if has_method and not has_id:
153
+ # JSON-RPC Notification (event delivery)
154
+ params = msg.get("params", {})
155
+ event_type = params.get("event", "")
156
+ data = params.get("data", {})
157
+
158
+ # Handle system.ping event
159
+ if event_type == "system.ping":
160
+ await self._handle_ping_event(data)
161
+ continue
162
+
163
+ # Layer 1: 处理订阅的事件
164
+ if event_type == "module.shutdown":
165
+ target = data.get("module_id", "")
166
+ reason = data.get("reason", "")
167
+ # Handle both targeted shutdown (module_id == "acp_channel") and broadcast shutdown (no module_id or launcher_lost)
168
+ if target == "acp_channel" or not target or reason == "launcher_lost":
169
+ await self._handle_shutdown(ws)
170
+ return
171
+
172
+ # Layer 2: 忽略系统广播事件
173
+ if event_type in SYSTEM_BROADCAST_EVENTS:
174
+ continue
175
+
176
+ # Layer 3: 警告未知事件(仅开发环境)
177
+ if os.environ.get("KITE_ENV") == "development":
178
+ print(f"[acp_channel] Debug: Unhandled event: {event_type}")
179
+
180
+ elif not has_method and has_id:
181
+ # JSON-RPC Response (to our RPC calls)
182
+ pass
183
+ except Exception as e:
184
+ print(f"[acp_channel] 事件处理异常(已忽略): {e}")
185
+
186
+ async def _handle_shutdown(self, ws):
187
+ """Handle module.shutdown: ack → exiting → cleanup → ready → exit."""
188
+ print("[acp_channel] Received module.shutdown")
189
+ self._shutting_down = True
190
+
191
+ # Step 1: Send ack (立即确认收到)
192
+ await self._rpc_call(ws, "event.publish", {
193
+ "event_id": str(uuid.uuid4()),
194
+ "event": "module.shutdown.ack",
195
+ "data": {"module_id": "acp_channel", "estimated_cleanup": 2},
196
+ })
197
+ print("[acp_channel] shutdown ack sent")
198
+
199
+ # Step 2: Send module.exiting (开始清理)
200
+ await self._rpc_call(ws, "event.publish", {
201
+ "event_id": str(uuid.uuid4()),
202
+ "event": "module.exiting",
203
+ "data": {"module_id": "acp_channel", "action": "none"},
204
+ })
205
+
206
+ # Step 3: Cleanup (cancel background tasks)
207
+ if self._test_task:
208
+ self._test_task.cancel()
209
+
210
+ # Step 4: Send ready (清理完成)
211
+ await self._rpc_call(ws, "event.publish", {
212
+ "event_id": str(uuid.uuid4()),
213
+ "event": "module.shutdown.ready",
214
+ "data": {"module_id": "acp_channel"},
215
+ })
216
+ print("[acp_channel] Shutdown ready sent")
217
+
218
+ # Step 4: Exit process
219
+ import sys
220
+ sys.exit(0)
221
+
222
+ async def _rpc_call(self, ws, method: str, params: dict = None):
223
+ """Send a JSON-RPC 2.0 request."""
224
+ msg = {"jsonrpc": "2.0", "id": str(uuid.uuid4()), "method": method}
225
+ if params:
226
+ msg["params"] = params
227
+ await ws.send(json.dumps(msg))
228
+
229
+ async def _handle_ping_event(self, data: dict):
230
+ """Handle system.ping event and reply with system.pong."""
231
+ t1 = data.get("ping_time")
232
+ t2 = time.time()
233
+
234
+ await self._rpc_call(self._ws, "event.publish", {
235
+ "event_id": str(uuid.uuid4()),
236
+ "event": "system.pong",
237
+ "data": {
238
+ "module_id": "acp_channel",
239
+ "ping_time": t1,
240
+ "pong_time": t2,
241
+ },
242
+ })
243
+
244
+ def _fmt_elapsed(self, t0: float) -> str:
245
+ """Format elapsed time since t0."""
246
+ d = time.monotonic() - t0 if t0 else 0
247
+ if d < 1:
248
+ return f"{d * 1000:.0f}ms"
249
+ if d < 10:
250
+ return f"{d:.1f}s"
251
+ return f"{d:.0f}s"
252
+
253
+ async def _publish_event(self, event: dict):
254
+ """Publish an event via JSON-RPC event.publish."""
255
+ if not self._ws:
256
+ return
257
+ try:
258
+ await self._rpc_call(self._ws, "event.publish", {
259
+ "event_id": str(uuid.uuid4()),
260
+ "event": event.get("event", ""),
261
+ "data": event.get("data", {}),
262
+ })
263
+ except Exception as e:
264
+ print(f"[acp_channel] Failed to publish event: {e}")
265
+
266
+ # ── Test event loop ──
267
+
268
+ async def _test_event_loop(self):
269
+ """Publish a test event every 10 seconds."""
270
+ while True:
271
+ await asyncio.sleep(10)
272
+ await self._publish_event({
273
+ "event": "acp_channel.test",
274
+ "data": {
275
+ "message": "test event from acp_channel",
276
+ "timestamp": datetime.now(timezone.utc).isoformat(),
277
+ },
278
+ })
279
+ print("[acp_channel] test event published")
@@ -35,7 +35,113 @@ TAG = "[event_hub_bench]"
35
35
 
36
36
 
37
37
  # ── Module configuration ──
38
- MODULE_NAME = "event_hub_bench"
38
+
39
+ def _load_module_config() -> dict:
40
+ """Load module configuration from module.md frontmatter.
41
+
42
+ Returns:
43
+ Dict with keys: name, preferred_port, advertise_ip
44
+
45
+ Raises:
46
+ SystemExit: If module.md is invalid or name is non-compliant
47
+ """
48
+ _this_dir = os.path.dirname(os.path.abspath(__file__))
49
+ module_md = os.path.join(_this_dir, "module.md")
50
+
51
+ # Calculate relative path for error messages
52
+ project_root = os.environ.get("KITE_PROJECT", "")
53
+ if project_root and _this_dir.startswith(project_root):
54
+ rel_path = os.path.relpath(_this_dir, project_root)
55
+ else:
56
+ rel_path = _this_dir
57
+
58
+ # Default values (will be overridden if valid config exists)
59
+ result = {
60
+ "name": "",
61
+ "preferred_port": 0,
62
+ "advertise_ip": "0.0.0.0"
63
+ }
64
+
65
+ # Check if module.md exists
66
+ if not os.path.exists(module_md):
67
+ print(f"[{rel_path}] ERROR: Invalid module configuration in module.md")
68
+ print(f" Path: {rel_path}/module.md")
69
+ print(f" Reason: File not found")
70
+ sys.exit(1)
71
+
72
+ try:
73
+ with open(module_md, encoding="utf-8") as f:
74
+ text = f.read()
75
+
76
+ # Extract YAML frontmatter (between --- markers)
77
+ import re
78
+ m = re.match(r'^---\s*\n(.*?)\n---', text, re.DOTALL)
79
+ if not m:
80
+ print(f"[{rel_path}] ERROR: Invalid module configuration in module.md")
81
+ print(f" Path: {rel_path}/module.md")
82
+ print(f" Reason: Missing YAML frontmatter")
83
+ sys.exit(1)
84
+
85
+ # Parse YAML frontmatter
86
+ try:
87
+ import yaml
88
+ fm = yaml.safe_load(m.group(1)) or {}
89
+ except ImportError:
90
+ print(f"[{rel_path}] ERROR: PyYAML not installed, cannot parse module.md")
91
+ sys.exit(1)
92
+ except Exception as e:
93
+ print(f"[{rel_path}] ERROR: Invalid module configuration in module.md")
94
+ print(f" Path: {rel_path}/module.md")
95
+ print(f" Reason: YAML parse error: {e}")
96
+ sys.exit(1)
97
+
98
+ # Validate 'name' field (required)
99
+ if "name" not in fm:
100
+ print(f"[{rel_path}] ERROR: Invalid module configuration in module.md")
101
+ print(f" Path: {rel_path}/module.md")
102
+ print(f" Reason: Missing 'name' field")
103
+ sys.exit(1)
104
+
105
+ raw_name = str(fm["name"]).strip()
106
+
107
+ if not raw_name:
108
+ print(f"[{rel_path}] ERROR: Invalid module configuration in module.md")
109
+ print(f" Path: {rel_path}/module.md")
110
+ print(f" Reason: Empty module name")
111
+ sys.exit(1)
112
+
113
+ # Validate name characters
114
+ sanitized = re.sub(r'[^a-zA-Z0-9_\-]', '', raw_name)
115
+
116
+ if sanitized != raw_name:
117
+ invalid_chars = ''.join(sorted(set(c for c in raw_name if c not in sanitized)))
118
+ print(f"[{rel_path}] ERROR: Invalid module configuration in module.md")
119
+ print(f" Path: {rel_path}/module.md")
120
+ print(f" Reason: Invalid characters in name '{raw_name}': {repr(invalid_chars)}")
121
+ sys.exit(1)
122
+
123
+ result["name"] = sanitized
124
+
125
+ # Extract optional fields
126
+ if "preferred_port" in fm:
127
+ try:
128
+ result["preferred_port"] = int(fm["preferred_port"])
129
+ except (ValueError, TypeError):
130
+ pass
131
+
132
+ if "advertise_ip" in fm:
133
+ result["advertise_ip"] = str(fm["advertise_ip"])
134
+
135
+ except SystemExit:
136
+ raise # Re-raise exit to prevent catching by outer except
137
+ except Exception as e:
138
+ print(f"[{rel_path}] ERROR: Failed to read module.md: {e}")
139
+ sys.exit(1)
140
+
141
+ return result
142
+
143
+ _module_config = _load_module_config()
144
+ MODULE_NAME = _module_config["name"]
39
145
 
40
146
 
41
147
  # ── Timestamped print + log file writer ──