@agentunion/kite 1.0.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 (69) hide show
  1. package/__init__.py +1 -0
  2. package/__main__.py +15 -0
  3. package/cli.js +70 -0
  4. package/core/__init__.py +0 -0
  5. package/core/__pycache__/__init__.cpython-313.pyc +0 -0
  6. package/core/event_hub/BENCHMARK.md +94 -0
  7. package/core/event_hub/__init__.py +0 -0
  8. package/core/event_hub/__pycache__/__init__.cpython-313.pyc +0 -0
  9. package/core/event_hub/__pycache__/bench.cpython-313.pyc +0 -0
  10. package/core/event_hub/__pycache__/bench_perf.cpython-313.pyc +0 -0
  11. package/core/event_hub/__pycache__/dedup.cpython-313.pyc +0 -0
  12. package/core/event_hub/__pycache__/entry.cpython-313.pyc +0 -0
  13. package/core/event_hub/__pycache__/hub.cpython-313.pyc +0 -0
  14. package/core/event_hub/__pycache__/router.cpython-313.pyc +0 -0
  15. package/core/event_hub/__pycache__/server.cpython-313.pyc +0 -0
  16. package/core/event_hub/bench.py +459 -0
  17. package/core/event_hub/bench_extreme.py +308 -0
  18. package/core/event_hub/bench_perf.py +350 -0
  19. package/core/event_hub/bench_results/.gitkeep +0 -0
  20. package/core/event_hub/bench_results/2026-02-28_13-26-48.json +51 -0
  21. package/core/event_hub/bench_results/2026-02-28_13-44-45.json +51 -0
  22. package/core/event_hub/bench_results/2026-02-28_13-45-39.json +51 -0
  23. package/core/event_hub/dedup.py +31 -0
  24. package/core/event_hub/entry.py +113 -0
  25. package/core/event_hub/hub.py +263 -0
  26. package/core/event_hub/module.md +21 -0
  27. package/core/event_hub/router.py +21 -0
  28. package/core/event_hub/server.py +138 -0
  29. package/core/event_hub_bench/entry.py +371 -0
  30. package/core/event_hub_bench/module.md +25 -0
  31. package/core/launcher/__init__.py +0 -0
  32. package/core/launcher/__pycache__/__init__.cpython-313.pyc +0 -0
  33. package/core/launcher/__pycache__/entry.cpython-313.pyc +0 -0
  34. package/core/launcher/__pycache__/module_scanner.cpython-313.pyc +0 -0
  35. package/core/launcher/__pycache__/process_manager.cpython-313.pyc +0 -0
  36. package/core/launcher/data/log/lifecycle.jsonl +1045 -0
  37. package/core/launcher/data/processes_14752.json +32 -0
  38. package/core/launcher/data/token.txt +1 -0
  39. package/core/launcher/entry.py +965 -0
  40. package/core/launcher/module.md +37 -0
  41. package/core/launcher/module_scanner.py +253 -0
  42. package/core/launcher/process_manager.py +435 -0
  43. package/core/registry/__init__.py +0 -0
  44. package/core/registry/__pycache__/__init__.cpython-313.pyc +0 -0
  45. package/core/registry/__pycache__/entry.cpython-313.pyc +0 -0
  46. package/core/registry/__pycache__/server.cpython-313.pyc +0 -0
  47. package/core/registry/__pycache__/store.cpython-313.pyc +0 -0
  48. package/core/registry/data/port.txt +1 -0
  49. package/core/registry/data/port_14752.txt +1 -0
  50. package/core/registry/data/port_484.txt +1 -0
  51. package/core/registry/entry.py +73 -0
  52. package/core/registry/module.md +30 -0
  53. package/core/registry/server.py +256 -0
  54. package/core/registry/store.py +232 -0
  55. package/extensions/__init__.py +0 -0
  56. package/extensions/__pycache__/__init__.cpython-313.pyc +0 -0
  57. package/extensions/services/__init__.py +0 -0
  58. package/extensions/services/__pycache__/__init__.cpython-313.pyc +0 -0
  59. package/extensions/services/watchdog/__init__.py +0 -0
  60. package/extensions/services/watchdog/__pycache__/__init__.cpython-313.pyc +0 -0
  61. package/extensions/services/watchdog/__pycache__/entry.cpython-313.pyc +0 -0
  62. package/extensions/services/watchdog/__pycache__/monitor.cpython-313.pyc +0 -0
  63. package/extensions/services/watchdog/__pycache__/server.cpython-313.pyc +0 -0
  64. package/extensions/services/watchdog/entry.py +143 -0
  65. package/extensions/services/watchdog/module.md +25 -0
  66. package/extensions/services/watchdog/monitor.py +420 -0
  67. package/extensions/services/watchdog/server.py +167 -0
  68. package/main.py +17 -0
  69. package/package.json +27 -0
@@ -0,0 +1,371 @@
1
+ """
2
+ Event Hub Benchmark module entry point.
3
+ Discovers hub via Registry, runs benchmarks, saves results, exits.
4
+ """
5
+
6
+ import asyncio
7
+ import json
8
+ import os
9
+ import platform
10
+ import sys
11
+ import threading
12
+ import time
13
+ from datetime import datetime, timezone
14
+
15
+ import statistics
16
+ import uuid
17
+
18
+ import httpx
19
+ import websockets
20
+
21
+ _this_dir = os.path.dirname(os.path.abspath(__file__))
22
+ _project_root = os.path.dirname(os.path.dirname(_this_dir))
23
+ if _project_root not in sys.path:
24
+ sys.path.insert(0, _project_root)
25
+
26
+ RESULTS_DIR = os.path.join(_project_root, "core", "event_hub", "bench_results")
27
+ TAG = "[event_hub_bench]"
28
+
29
+
30
+ def _discover_hub(registry_url: str, token: str, timeout: float = 30) -> str | None:
31
+ """Poll registry until event_hub is found. Returns ws:// URL or None."""
32
+ headers = {"Authorization": f"Bearer {token}"}
33
+ deadline = time.time() + timeout
34
+ while time.time() < deadline:
35
+ try:
36
+ resp = httpx.get(
37
+ f"{registry_url}/lookup/event_hub",
38
+ headers=headers, timeout=5,
39
+ )
40
+ if resp.status_code == 200:
41
+ info = resp.json()
42
+ ws_url = (info.get("metadata") or {}).get("ws_endpoint")
43
+ if ws_url:
44
+ return ws_url
45
+ except Exception:
46
+ pass
47
+ time.sleep(1)
48
+ return None
49
+
50
+
51
+ def _get_hub_stats(ws_url: str) -> dict:
52
+ """Fetch hub /stats via HTTP (derive from ws:// URL)."""
53
+ http_url = ws_url.replace("ws://", "http://").rsplit("/ws", 1)[0]
54
+ try:
55
+ resp = httpx.get(f"{http_url}/stats", timeout=5)
56
+ if resp.status_code == 200:
57
+ return resp.json()
58
+ except Exception:
59
+ pass
60
+ return {}
61
+
62
+
63
+ def _sample_hub_resources(ws_url: str) -> dict:
64
+ """Sample hub process CPU/memory via psutil. Returns {} if unavailable."""
65
+ try:
66
+ import psutil
67
+ except ImportError:
68
+ return {"error": "psutil not installed"}
69
+ http_url = ws_url.replace("ws://", "http://").rsplit("/ws", 1)[0]
70
+ port = int(http_url.rsplit(":", 1)[1].split("/")[0])
71
+ for conn in psutil.net_connections(kind="tcp"):
72
+ if conn.laddr.port == port and conn.status == "LISTEN":
73
+ try:
74
+ p = psutil.Process(conn.pid)
75
+ mem = p.memory_info()
76
+ return {
77
+ "pid": conn.pid,
78
+ "cpu_percent": p.cpu_percent(interval=1),
79
+ "rss_mb": round(mem.rss / 1048576, 1),
80
+ "vms_mb": round(mem.vms / 1048576, 1),
81
+ "threads": p.num_threads(),
82
+ }
83
+ except Exception:
84
+ pass
85
+ return {"error": "hub process not found"}
86
+
87
+
88
+ # ── WebSocket client ──
89
+
90
+ class WsClient:
91
+ """Lightweight async WebSocket client for hub benchmarks."""
92
+
93
+ def __init__(self, ws_url: str, token: str, client_id: str):
94
+ self.url = f"{ws_url}?token={token}&id={client_id}"
95
+ self.id = client_id
96
+ self.ws = None
97
+
98
+ async def connect(self):
99
+ self.ws = await websockets.connect(self.url, max_size=None)
100
+
101
+ async def subscribe(self, patterns: list[str]):
102
+ await self.ws.send(json.dumps({"type": "subscribe", "events": patterns}))
103
+
104
+ async def publish(self, event_type: str, data: dict) -> str:
105
+ eid = str(uuid.uuid4())
106
+ await self.ws.send(json.dumps({
107
+ "type": "event", "event_id": eid, "event": event_type,
108
+ "source": self.id,
109
+ "timestamp": datetime.now(timezone.utc).isoformat(),
110
+ "data": data,
111
+ }))
112
+ return eid
113
+
114
+ async def recv(self, timeout=5.0):
115
+ try:
116
+ return json.loads(await asyncio.wait_for(self.ws.recv(), timeout=timeout))
117
+ except Exception:
118
+ return None
119
+
120
+ async def close(self):
121
+ if self.ws:
122
+ await self.ws.close()
123
+
124
+
125
+ # ── Benchmark runner ──
126
+
127
+ class BenchRunner:
128
+ def __init__(self, ws_url: str, token: str):
129
+ self.ws_url = ws_url
130
+ self.token = token
131
+ self.results = {}
132
+
133
+ async def _make_client(self, name: str) -> WsClient:
134
+ c = WsClient(self.ws_url, self.token, name)
135
+ await c.connect()
136
+ return c
137
+
138
+ # ── Throughput: burst N events, measure hub processing rate ──
139
+
140
+ async def bench_throughput(self, n: int = 10000):
141
+ print(f"{TAG} Throughput test: {n} events...")
142
+ pub = await self._make_client("bench_tp_pub")
143
+ sub = await self._make_client("bench_tp_sub")
144
+ await sub.subscribe(["bench.tp.*"])
145
+ await asyncio.sleep(0.2)
146
+
147
+ recvd = 0
148
+ stop = asyncio.Event()
149
+
150
+ async def _recv():
151
+ nonlocal recvd
152
+ while not stop.is_set():
153
+ try:
154
+ raw = await asyncio.wait_for(sub.ws.recv(), timeout=0.5)
155
+ if '"event"' in raw and '"ack"' not in raw:
156
+ recvd += 1
157
+ except Exception:
158
+ pass
159
+
160
+ async def _drain_acks():
161
+ while not stop.is_set():
162
+ try:
163
+ await asyncio.wait_for(pub.ws.recv(), timeout=0.5)
164
+ except Exception:
165
+ pass
166
+
167
+ recv_task = asyncio.create_task(_recv())
168
+ ack_task = asyncio.create_task(_drain_acks())
169
+
170
+ hub_before = (_get_hub_stats(self.ws_url).get("counters") or {})
171
+
172
+ t0 = time.time()
173
+ for i in range(n):
174
+ await pub.publish("bench.tp.test", {"i": i})
175
+ send_time = time.time() - t0
176
+
177
+ deadline = time.time() + max(15, n / 300)
178
+ while recvd < n and time.time() < deadline:
179
+ await asyncio.sleep(0.1)
180
+ total_time = time.time() - t0
181
+
182
+ stop.set()
183
+ await recv_task
184
+ ack_task.cancel()
185
+
186
+ hub_after = (_get_hub_stats(self.ws_url).get("counters") or {})
187
+
188
+ await pub.close()
189
+ await sub.close()
190
+
191
+ self.results["throughput"] = {
192
+ "events": n,
193
+ "send_rate": round(n / send_time),
194
+ "recv_rate": round(recvd / total_time) if total_time > 0 else 0,
195
+ "client_recv": recvd,
196
+ "hub_queued": hub_after.get("events_queued", 0) - hub_before.get("events_queued", 0),
197
+ "hub_routed": hub_after.get("events_routed", 0) - hub_before.get("events_routed", 0),
198
+ "send_time_s": round(send_time, 2),
199
+ "total_time_s": round(total_time, 2),
200
+ }
201
+ print(f"{TAG} send_rate={self.results['throughput']['send_rate']} evt/s, "
202
+ f"recv={recvd}/{n}, time={round(total_time, 1)}s")
203
+
204
+ # ── Latency: sequential pub→recv round-trip ──
205
+
206
+ async def bench_latency(self, n: int = 200):
207
+ print(f"{TAG} Latency test: {n} samples...")
208
+ pub = await self._make_client("bench_lat_pub")
209
+ sub = await self._make_client("bench_lat_sub")
210
+ await sub.subscribe(["bench.lat.*"])
211
+ await asyncio.sleep(0.2)
212
+
213
+ latencies = []
214
+ for i in range(n):
215
+ t0 = time.time()
216
+ await pub.publish("bench.lat.test", {"i": i})
217
+ await pub.recv(timeout=2) # ack
218
+ msg = await sub.recv(timeout=2)
219
+ if msg and msg.get("type") == "event":
220
+ latencies.append((time.time() - t0) * 1000)
221
+
222
+ await pub.close()
223
+ await sub.close()
224
+
225
+ if latencies:
226
+ latencies.sort()
227
+ self.results["latency"] = {
228
+ "samples": len(latencies),
229
+ "avg_ms": round(statistics.mean(latencies), 2),
230
+ "p50_ms": round(latencies[len(latencies) // 2], 2),
231
+ "p95_ms": round(latencies[int(len(latencies) * 0.95)], 2),
232
+ "p99_ms": round(latencies[int(len(latencies) * 0.99)], 2),
233
+ }
234
+ print(f"{TAG} avg={self.results['latency']['avg_ms']}ms, "
235
+ f"p50={self.results['latency']['p50_ms']}ms, "
236
+ f"p99={self.results['latency']['p99_ms']}ms")
237
+
238
+ # ── Fan-out: 1 pub, N subs ──
239
+
240
+ async def bench_fanout(self, n_events: int = 2000):
241
+ for n_subs in [1, 5, 10, 50]:
242
+ print(f"{TAG} Fan-out x{n_subs}: {n_events} events...")
243
+ pub = await self._make_client("bench_fo_pub")
244
+ subs = []
245
+ for i in range(n_subs):
246
+ s = await self._make_client(f"bench_fo_sub_{i}")
247
+ await s.subscribe(["bench.fo.*"])
248
+ subs.append(s)
249
+ await asyncio.sleep(0.3)
250
+
251
+ counts = [0] * n_subs
252
+ stop = asyncio.Event()
253
+
254
+ async def _recv(idx, client):
255
+ while not stop.is_set():
256
+ try:
257
+ raw = await asyncio.wait_for(client.ws.recv(), timeout=0.5)
258
+ if '"event"' in raw and '"ack"' not in raw:
259
+ counts[idx] += 1
260
+ except Exception:
261
+ pass
262
+
263
+ tasks = [asyncio.create_task(_recv(i, s)) for i, s in enumerate(subs)]
264
+ ack_stop = asyncio.Event()
265
+
266
+ async def _drain():
267
+ while not ack_stop.is_set():
268
+ try:
269
+ await asyncio.wait_for(pub.ws.recv(), timeout=0.5)
270
+ except Exception:
271
+ pass
272
+
273
+ ack_task = asyncio.create_task(_drain())
274
+
275
+ t0 = time.time()
276
+ for i in range(n_events):
277
+ await pub.publish("bench.fo.test", {"i": i})
278
+ send_time = time.time() - t0
279
+
280
+ await asyncio.sleep(max(3, n_events * n_subs / 3000))
281
+ stop.set()
282
+ ack_stop.set()
283
+ for t in tasks:
284
+ await t
285
+ ack_task.cancel()
286
+
287
+ await pub.close()
288
+ for s in subs:
289
+ await s.close()
290
+
291
+ avg_recv = sum(counts) / n_subs if n_subs else 0
292
+ self.results[f"fanout_{n_subs}"] = {
293
+ "subs": n_subs,
294
+ "events": n_events,
295
+ "send_rate": round(n_events / send_time) if send_time > 0 else 0,
296
+ "avg_recv": round(avg_recv),
297
+ "min_recv": min(counts),
298
+ }
299
+ print(f"{TAG} subs={n_subs}, avg_recv={round(avg_recv)}/{n_events}, "
300
+ f"min={min(counts)}")
301
+
302
+ # ── Save results ──
303
+
304
+ def save(self, ws_url: str):
305
+ os.makedirs(RESULTS_DIR, exist_ok=True)
306
+ now = datetime.now()
307
+ filepath = os.path.join(RESULTS_DIR, now.strftime("%Y-%m-%d_%H-%M-%S") + ".json")
308
+
309
+ hub_stats = _get_hub_stats(ws_url)
310
+ resources = _sample_hub_resources(ws_url)
311
+
312
+ data = {
313
+ "timestamp": now.isoformat(),
314
+ "env": {
315
+ "platform": sys.platform,
316
+ "python": platform.python_version(),
317
+ },
318
+ "hub_resources": resources,
319
+ "hub_counters": hub_stats.get("counters", {}),
320
+ **self.results,
321
+ }
322
+ with open(filepath, "w", encoding="utf-8") as f:
323
+ json.dump(data, f, indent=2, ensure_ascii=False)
324
+ print(f"{TAG} Results saved: {filepath}")
325
+
326
+
327
+ # ── Entry point ──
328
+
329
+ async def _run(ws_url: str, token: str):
330
+ bench = BenchRunner(ws_url, token)
331
+ await bench.bench_throughput()
332
+ await bench.bench_latency()
333
+ await bench.bench_fanout()
334
+ bench.save(ws_url)
335
+
336
+
337
+ def main():
338
+ # Read boot_info from stdin
339
+ token = ""
340
+ registry_port = 0
341
+ try:
342
+ line = sys.stdin.readline().strip()
343
+ if line:
344
+ boot = json.loads(line)
345
+ token = boot.get("token", "")
346
+ registry_port = boot.get("registry_port", 0)
347
+ except Exception:
348
+ pass
349
+
350
+ if not token or not registry_port:
351
+ print(f"{TAG} ERROR: Missing token or registry_port")
352
+ sys.exit(1)
353
+
354
+ registry_url = f"http://127.0.0.1:{registry_port}"
355
+ print(f"{TAG} Discovering event_hub via registry...")
356
+
357
+ ws_url = _discover_hub(registry_url, token)
358
+ if not ws_url:
359
+ print(f"{TAG} ERROR: event_hub not found")
360
+ sys.exit(1)
361
+
362
+ print(f"{TAG} Hub found: {ws_url}")
363
+ print(f"{TAG} Starting benchmarks...")
364
+
365
+ asyncio.run(_run(ws_url, token))
366
+
367
+ print(f"{TAG} Benchmarks complete, exiting.")
368
+
369
+
370
+ if __name__ == "__main__":
371
+ main()
@@ -0,0 +1,25 @@
1
+ ---
2
+ name: event_hub_bench
3
+ display_name: Event Hub Benchmark
4
+ version: "1.0"
5
+ type: tool
6
+ state: enabled
7
+ runtime: python
8
+ entry: entry.py
9
+ events: []
10
+ subscriptions: []
11
+ ---
12
+
13
+ # Event Hub Benchmark
14
+
15
+ 事件中心性能基准测试模块。
16
+
17
+ - 启用后随 Kite 启动自动运行基准测试
18
+ - 通过 Registry 发现 Event Hub,以真实模块身份连接
19
+ - 多线程模拟多个 publisher/subscriber 压测 hub
20
+ - 监控 hub 进程 CPU/内存占用
21
+ - 结果保存到 `core/event_hub/bench_results/`
22
+ - 测试完成后自动退出
23
+
24
+ 启用:将 `state` 改为 `enabled`
25
+ 停用:将 `state` 改为 `disabled`
File without changes