@agentunion/kite 1.0.7 → 1.3.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 (100) hide show
  1. package/CHANGELOG.md +208 -0
  2. package/README.md +48 -0
  3. package/cli.js +1 -1
  4. package/extensions/agents/__init__.py +1 -0
  5. package/extensions/agents/assistant/__init__.py +1 -0
  6. package/extensions/agents/assistant/entry.py +329 -0
  7. package/extensions/agents/assistant/module.md +22 -0
  8. package/extensions/agents/assistant/server.py +197 -0
  9. package/extensions/channels/__init__.py +1 -0
  10. package/extensions/channels/acp_channel/__init__.py +1 -0
  11. package/extensions/channels/acp_channel/entry.py +329 -0
  12. package/extensions/channels/acp_channel/module.md +22 -0
  13. package/extensions/channels/acp_channel/server.py +197 -0
  14. package/extensions/event_hub_bench/entry.py +624 -379
  15. package/extensions/event_hub_bench/module.md +2 -1
  16. package/extensions/services/backup/__init__.py +1 -0
  17. package/extensions/services/backup/entry.py +508 -0
  18. package/extensions/services/backup/module.md +22 -0
  19. package/extensions/services/model_service/__init__.py +1 -0
  20. package/extensions/services/model_service/entry.py +508 -0
  21. package/extensions/services/model_service/module.md +22 -0
  22. package/extensions/services/watchdog/entry.py +468 -102
  23. package/extensions/services/watchdog/module.md +3 -0
  24. package/extensions/services/watchdog/monitor.py +170 -69
  25. package/extensions/services/web/__init__.py +1 -0
  26. package/extensions/services/web/config.yaml +149 -0
  27. package/extensions/services/web/entry.py +390 -0
  28. package/extensions/services/web/module.md +24 -0
  29. package/extensions/services/web/routes/__init__.py +1 -0
  30. package/extensions/services/web/routes/routes_call.py +189 -0
  31. package/extensions/services/web/routes/routes_config.py +512 -0
  32. package/extensions/services/web/routes/routes_contacts.py +98 -0
  33. package/extensions/services/web/routes/routes_devlog.py +99 -0
  34. package/extensions/services/web/routes/routes_phone.py +81 -0
  35. package/extensions/services/web/routes/routes_sms.py +48 -0
  36. package/extensions/services/web/routes/routes_stats.py +17 -0
  37. package/extensions/services/web/routes/routes_voicechat.py +554 -0
  38. package/extensions/services/web/routes/schemas.py +216 -0
  39. package/extensions/services/web/server.py +375 -0
  40. package/extensions/services/web/static/css/style.css +1064 -0
  41. package/extensions/services/web/static/index.html +1445 -0
  42. package/extensions/services/web/static/js/app.js +4671 -0
  43. package/extensions/services/web/vendor/__init__.py +1 -0
  44. package/extensions/services/web/vendor/bluetooth/audio.py +348 -0
  45. package/extensions/services/web/vendor/bluetooth/contacts.py +251 -0
  46. package/extensions/services/web/vendor/bluetooth/manager.py +395 -0
  47. package/extensions/services/web/vendor/bluetooth/sms.py +290 -0
  48. package/extensions/services/web/vendor/bluetooth/telephony.py +274 -0
  49. package/extensions/services/web/vendor/config.py +139 -0
  50. package/extensions/services/web/vendor/conversation/asr.py +936 -0
  51. package/extensions/services/web/vendor/conversation/engine.py +548 -0
  52. package/extensions/services/web/vendor/conversation/llm.py +534 -0
  53. package/extensions/services/web/vendor/conversation/mcp_tools.py +190 -0
  54. package/extensions/services/web/vendor/conversation/tts.py +322 -0
  55. package/extensions/services/web/vendor/conversation/vad.py +138 -0
  56. package/extensions/services/web/vendor/storage/__init__.py +1 -0
  57. package/extensions/services/web/vendor/storage/identity.py +312 -0
  58. package/extensions/services/web/vendor/storage/store.py +507 -0
  59. package/extensions/services/web/vendor/task/manager.py +864 -0
  60. package/extensions/services/web/vendor/task/models.py +45 -0
  61. package/extensions/services/web/vendor/task/webhook.py +263 -0
  62. package/extensions/services/web/vendor/tools/registry.py +321 -0
  63. package/kernel/__init__.py +0 -0
  64. package/kernel/entry.py +407 -0
  65. package/{core/event_hub/hub.py → kernel/event_hub.py} +62 -74
  66. package/kernel/module.md +33 -0
  67. package/{core/registry/store.py → kernel/registry_store.py} +23 -8
  68. package/kernel/rpc_router.py +388 -0
  69. package/kernel/server.py +267 -0
  70. package/launcher/__init__.py +10 -0
  71. package/launcher/__main__.py +6 -0
  72. package/launcher/count_lines.py +258 -0
  73. package/launcher/entry.py +1778 -0
  74. package/launcher/logging_setup.py +289 -0
  75. package/{core/launcher → launcher}/module_scanner.py +11 -6
  76. package/launcher/process_manager.py +880 -0
  77. package/main.py +11 -210
  78. package/package.json +6 -9
  79. package/__init__.py +0 -1
  80. package/__main__.py +0 -15
  81. package/core/event_hub/BENCHMARK.md +0 -94
  82. package/core/event_hub/bench.py +0 -459
  83. package/core/event_hub/bench_extreme.py +0 -308
  84. package/core/event_hub/bench_perf.py +0 -350
  85. package/core/event_hub/entry.py +0 -157
  86. package/core/event_hub/module.md +0 -20
  87. package/core/event_hub/server.py +0 -206
  88. package/core/launcher/entry.py +0 -1158
  89. package/core/launcher/process_manager.py +0 -470
  90. package/core/registry/entry.py +0 -110
  91. package/core/registry/module.md +0 -30
  92. package/core/registry/server.py +0 -289
  93. package/extensions/services/watchdog/server.py +0 -167
  94. /package/{core → extensions/services/web/vendor/bluetooth}/__init__.py +0 -0
  95. /package/{core/event_hub → extensions/services/web/vendor/conversation}/__init__.py +0 -0
  96. /package/{core/launcher → extensions/services/web/vendor/task}/__init__.py +0 -0
  97. /package/{core/registry → extensions/services/web/vendor/tools}/__init__.py +0 -0
  98. /package/{core/event_hub → kernel}/dedup.py +0 -0
  99. /package/{core/event_hub → kernel}/router.py +0 -0
  100. /package/{core/launcher → launcher}/module.md +0 -0
@@ -1,379 +1,624 @@
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 time
12
- from datetime import datetime, timezone
13
-
14
- import statistics
15
- import uuid
16
-
17
- import httpx
18
- import websockets
19
-
20
- _project_root = os.environ.get("KITE_PROJECT") or os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
21
- if _project_root not in sys.path:
22
- sys.path.insert(0, _project_root)
23
-
24
- # Results go to KITE_MODULE_DATA (set by Launcher per module)
25
- # Fallback to source tree for standalone runs
26
- RESULTS_DIR = os.environ.get("KITE_MODULE_DATA") or os.path.join(
27
- os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),
28
- "core", "event_hub", "bench_results",
29
- )
30
- TAG = "[event_hub_bench]"
31
-
32
-
33
- def _discover_hub(registry_url: str, token: str, timeout: float = 30) -> str | None:
34
- """Poll registry until event_hub is found. Returns ws:// URL or None."""
35
- headers = {"Authorization": f"Bearer {token}"}
36
- deadline = time.time() + timeout
37
- while time.time() < deadline:
38
- try:
39
- resp = httpx.get(
40
- f"{registry_url}/get/event_hub",
41
- headers=headers, timeout=5,
42
- )
43
- if resp.status_code == 200:
44
- info = resp.json()
45
- ws_url = (info.get("metadata") or {}).get("ws_endpoint")
46
- if ws_url:
47
- return ws_url
48
- except Exception:
49
- pass
50
- time.sleep(1)
51
- return None
52
-
53
-
54
- def _get_hub_stats(ws_url: str) -> dict:
55
- """Fetch hub /stats via HTTP (derive from ws:// URL)."""
56
- http_url = ws_url.replace("ws://", "http://").rsplit("/ws", 1)[0]
57
- try:
58
- resp = httpx.get(f"{http_url}/stats", timeout=5)
59
- if resp.status_code == 200:
60
- return resp.json()
61
- except Exception:
62
- pass
63
- return {}
64
-
65
-
66
- def _sample_hub_resources(ws_url: str) -> dict:
67
- """Sample hub process CPU/memory via psutil. Returns {} if unavailable."""
68
- try:
69
- import psutil
70
- except ImportError:
71
- return {"error": "psutil not installed"}
72
- http_url = ws_url.replace("ws://", "http://").rsplit("/ws", 1)[0]
73
- port = int(http_url.rsplit(":", 1)[1].split("/")[0])
74
- for conn in psutil.net_connections(kind="tcp"):
75
- if conn.laddr.port == port and conn.status == "LISTEN":
76
- try:
77
- p = psutil.Process(conn.pid)
78
- mem = p.memory_info()
79
- return {
80
- "pid": conn.pid,
81
- "cpu_percent": p.cpu_percent(interval=1),
82
- "rss_mb": round(mem.rss / 1048576, 1),
83
- "vms_mb": round(mem.vms / 1048576, 1),
84
- "threads": p.num_threads(),
85
- }
86
- except Exception:
87
- pass
88
- return {"error": "hub process not found"}
89
-
90
-
91
- # ── WebSocket client ──
92
-
93
- class WsClient:
94
- """Lightweight async WebSocket client for hub benchmarks."""
95
-
96
- def __init__(self, ws_url: str, token: str, client_id: str):
97
- self.url = f"{ws_url}?token={token}&id={client_id}"
98
- self.id = client_id
99
- self.ws = None
100
-
101
- async def connect(self):
102
- self.ws = await websockets.connect(self.url, max_size=None)
103
-
104
- async def subscribe(self, patterns: list[str]):
105
- await self.ws.send(json.dumps({"type": "subscribe", "events": patterns}))
106
-
107
- async def publish(self, event_type: str, data: dict) -> str:
108
- eid = str(uuid.uuid4())
109
- await self.ws.send(json.dumps({
110
- "type": "event", "event_id": eid, "event": event_type,
111
- "source": self.id,
112
- "timestamp": datetime.now(timezone.utc).isoformat(),
113
- "data": data,
114
- }))
115
- return eid
116
-
117
- async def recv(self, timeout=5.0):
118
- try:
119
- return json.loads(await asyncio.wait_for(self.ws.recv(), timeout=timeout))
120
- except Exception:
121
- return None
122
-
123
- async def close(self):
124
- if self.ws:
125
- await self.ws.close()
126
-
127
-
128
- # ── Benchmark runner ──
129
-
130
- class BenchRunner:
131
- def __init__(self, ws_url: str, token: str):
132
- self.ws_url = ws_url
133
- self.token = token
134
- self.results = {}
135
-
136
- async def _make_client(self, name: str) -> WsClient:
137
- c = WsClient(self.ws_url, self.token, name)
138
- await c.connect()
139
- return c
140
-
141
- # ── Throughput: burst N events, measure hub processing rate ──
142
-
143
- async def bench_throughput(self, n: int = 10000):
144
- print(f"{TAG} Throughput test: {n} events...")
145
- pub = await self._make_client("bench_tp_pub")
146
- sub = await self._make_client("bench_tp_sub")
147
- await sub.subscribe(["bench.tp.*"])
148
- await asyncio.sleep(0.2)
149
-
150
- recvd = 0
151
- stop = asyncio.Event()
152
-
153
- async def _recv():
154
- nonlocal recvd
155
- while not stop.is_set():
156
- try:
157
- raw = await asyncio.wait_for(sub.ws.recv(), timeout=0.5)
158
- if '"event"' in raw and '"ack"' not in raw:
159
- recvd += 1
160
- except Exception:
161
- pass
162
-
163
- async def _drain_acks():
164
- while not stop.is_set():
165
- try:
166
- await asyncio.wait_for(pub.ws.recv(), timeout=0.5)
167
- except Exception:
168
- pass
169
-
170
- recv_task = asyncio.create_task(_recv())
171
- ack_task = asyncio.create_task(_drain_acks())
172
-
173
- hub_before = (_get_hub_stats(self.ws_url).get("counters") or {})
174
-
175
- t0 = time.time()
176
- for i in range(n):
177
- await pub.publish("bench.tp.test", {"i": i})
178
- send_time = time.time() - t0
179
-
180
- deadline = time.time() + max(15, n / 300)
181
- while recvd < n and time.time() < deadline:
182
- await asyncio.sleep(0.1)
183
- total_time = time.time() - t0
184
-
185
- stop.set()
186
- await recv_task
187
- ack_task.cancel()
188
-
189
- hub_after = (_get_hub_stats(self.ws_url).get("counters") or {})
190
-
191
- await pub.close()
192
- await sub.close()
193
-
194
- self.results["throughput"] = {
195
- "events": n,
196
- "send_rate": round(n / send_time),
197
- "recv_rate": round(recvd / total_time) if total_time > 0 else 0,
198
- "client_recv": recvd,
199
- "hub_queued": hub_after.get("events_queued", 0) - hub_before.get("events_queued", 0),
200
- "hub_routed": hub_after.get("events_routed", 0) - hub_before.get("events_routed", 0),
201
- "send_time_s": round(send_time, 2),
202
- "total_time_s": round(total_time, 2),
203
- }
204
- print(f"{TAG} send_rate={self.results['throughput']['send_rate']} evt/s, "
205
- f"recv={recvd}/{n}, time={round(total_time, 1)}s")
206
-
207
- # ── Latency: sequential pub→recv round-trip ──
208
-
209
- async def bench_latency(self, n: int = 200):
210
- print(f"{TAG} Latency test: {n} samples...")
211
- pub = await self._make_client("bench_lat_pub")
212
- sub = await self._make_client("bench_lat_sub")
213
- await sub.subscribe(["bench.lat.*"])
214
- await asyncio.sleep(0.2)
215
-
216
- latencies = []
217
- for i in range(n):
218
- t0 = time.time()
219
- await pub.publish("bench.lat.test", {"i": i})
220
- await pub.recv(timeout=2) # ack
221
- msg = await sub.recv(timeout=2)
222
- if msg and msg.get("type") == "event":
223
- latencies.append((time.time() - t0) * 1000)
224
-
225
- await pub.close()
226
- await sub.close()
227
-
228
- if latencies:
229
- latencies.sort()
230
- self.results["latency"] = {
231
- "samples": len(latencies),
232
- "avg_ms": round(statistics.mean(latencies), 2),
233
- "p50_ms": round(latencies[len(latencies) // 2], 2),
234
- "p95_ms": round(latencies[int(len(latencies) * 0.95)], 2),
235
- "p99_ms": round(latencies[int(len(latencies) * 0.99)], 2),
236
- }
237
- print(f"{TAG} avg={self.results['latency']['avg_ms']}ms, "
238
- f"p50={self.results['latency']['p50_ms']}ms, "
239
- f"p99={self.results['latency']['p99_ms']}ms")
240
-
241
- # ── Fan-out: 1 pub, N subs ──
242
-
243
- async def bench_fanout(self, n_events: int = 2000):
244
- for n_subs in [1, 5, 10, 50]:
245
- print(f"{TAG} Fan-out x{n_subs}: {n_events} events...")
246
- pub = await self._make_client("bench_fo_pub")
247
- subs = []
248
- for i in range(n_subs):
249
- s = await self._make_client(f"bench_fo_sub_{i}")
250
- await s.subscribe(["bench.fo.*"])
251
- subs.append(s)
252
- await asyncio.sleep(0.3)
253
-
254
- counts = [0] * n_subs
255
- stop = asyncio.Event()
256
-
257
- async def _recv(idx, client):
258
- while not stop.is_set():
259
- try:
260
- raw = await asyncio.wait_for(client.ws.recv(), timeout=0.5)
261
- if '"event"' in raw and '"ack"' not in raw:
262
- counts[idx] += 1
263
- except Exception:
264
- pass
265
-
266
- tasks = [asyncio.create_task(_recv(i, s)) for i, s in enumerate(subs)]
267
- ack_stop = asyncio.Event()
268
-
269
- async def _drain():
270
- while not ack_stop.is_set():
271
- try:
272
- await asyncio.wait_for(pub.ws.recv(), timeout=0.5)
273
- except Exception:
274
- pass
275
-
276
- ack_task = asyncio.create_task(_drain())
277
-
278
- t0 = time.time()
279
- for i in range(n_events):
280
- await pub.publish("bench.fo.test", {"i": i})
281
- send_time = time.time() - t0
282
-
283
- await asyncio.sleep(max(3, n_events * n_subs / 3000))
284
- stop.set()
285
- ack_stop.set()
286
- for t in tasks:
287
- await t
288
- ack_task.cancel()
289
-
290
- await pub.close()
291
- for s in subs:
292
- await s.close()
293
-
294
- avg_recv = sum(counts) / n_subs if n_subs else 0
295
- self.results[f"fanout_{n_subs}"] = {
296
- "subs": n_subs,
297
- "events": n_events,
298
- "send_rate": round(n_events / send_time) if send_time > 0 else 0,
299
- "avg_recv": round(avg_recv),
300
- "min_recv": min(counts),
301
- }
302
- print(f"{TAG} subs={n_subs}, avg_recv={round(avg_recv)}/{n_events}, "
303
- f"min={min(counts)}")
304
-
305
- # ── Save results ──
306
-
307
- def save(self, ws_url: str):
308
- os.makedirs(RESULTS_DIR, exist_ok=True)
309
- now = datetime.now()
310
- filepath = os.path.join(RESULTS_DIR, now.strftime("%Y-%m-%d_%H-%M-%S") + ".json")
311
-
312
- hub_stats = _get_hub_stats(ws_url)
313
- resources = _sample_hub_resources(ws_url)
314
-
315
- data = {
316
- "timestamp": now.isoformat(),
317
- "env": {
318
- "platform": sys.platform,
319
- "python": platform.python_version(),
320
- },
321
- "hub_resources": resources,
322
- "hub_counters": hub_stats.get("counters", {}),
323
- **self.results,
324
- }
325
- with open(filepath, "w", encoding="utf-8") as f:
326
- json.dump(data, f, indent=2, ensure_ascii=False)
327
- print(f"{TAG} Results saved: {filepath}")
328
-
329
-
330
- # ── Entry point ──
331
-
332
- async def _run(ws_url: str, token: str):
333
- bench = BenchRunner(ws_url, token)
334
- await bench.bench_throughput()
335
- await bench.bench_latency()
336
- await bench.bench_fanout()
337
- bench.save(ws_url)
338
-
339
-
340
- def main():
341
- # Read boot_info from stdin (only token)
342
- token = ""
343
- try:
344
- line = sys.stdin.readline().strip()
345
- if line:
346
- boot = json.loads(line)
347
- token = boot.get("token", "")
348
- except Exception:
349
- pass
350
-
351
- # Read registry_port from environment variable
352
- registry_port = int(os.environ.get("KITE_REGISTRY_PORT", "0"))
353
-
354
- if not token or not registry_port:
355
- print(f"{TAG} ERROR: Missing token or KITE_REGISTRY_PORT")
356
- sys.exit(1)
357
-
358
- if os.environ.get("KITE_DEBUG") != "1":
359
- print(f"{TAG} ERROR: Benchmark requires KITE_DEBUG=1 (multiple clients share one token)")
360
- sys.exit(1)
361
-
362
- registry_url = f"http://127.0.0.1:{registry_port}"
363
- print(f"{TAG} Discovering event_hub via registry...")
364
-
365
- ws_url = _discover_hub(registry_url, token)
366
- if not ws_url:
367
- print(f"{TAG} ERROR: event_hub not found")
368
- sys.exit(1)
369
-
370
- print(f"{TAG} Hub found: {ws_url}")
371
- print(f"{TAG} Starting benchmarks...")
372
-
373
- asyncio.run(_run(ws_url, token))
374
-
375
- print(f"{TAG} Benchmarks complete, exiting.")
376
-
377
-
378
- if __name__ == "__main__":
379
- main()
1
+ """
2
+ Event Hub Benchmark module entry point.
3
+ Connects to Kernel via WebSocket JSON-RPC 2.0, runs benchmarks, saves results, exits.
4
+ """
5
+
6
+ import asyncio
7
+ import builtins
8
+ import json
9
+ import os
10
+ import platform
11
+ import re
12
+ import sys
13
+ import threading
14
+ import time
15
+ import traceback
16
+ from datetime import datetime, timezone
17
+
18
+ import statistics
19
+ import uuid
20
+
21
+ import httpx
22
+ import websockets
23
+
24
+ _project_root = os.environ.get("KITE_PROJECT") or os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
25
+ if _project_root not in sys.path:
26
+ sys.path.insert(0, _project_root)
27
+
28
+ # Results go to KITE_MODULE_DATA (set by Launcher per module)
29
+ # Fallback to source tree for standalone runs
30
+ RESULTS_DIR = os.environ.get("KITE_MODULE_DATA") or os.path.join(
31
+ os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),
32
+ "core", "event_hub", "bench_results",
33
+ )
34
+ TAG = "[event_hub_bench]"
35
+
36
+
37
+ # ── Module configuration ──
38
+ MODULE_NAME = "event_hub_bench"
39
+
40
+
41
+ # ── Timestamped print + log file writer ──
42
+ _builtin_print = builtins.print
43
+ _start_ts = time.monotonic()
44
+ _last_ts = time.monotonic()
45
+ _ANSI_RE = re.compile(r"\033\[[0-9;]*m")
46
+ _log_lock = threading.Lock()
47
+ _log_latest_path = None
48
+ _log_daily_path = None
49
+ _log_daily_date = ""
50
+ _log_dir = None
51
+ _crash_log_path = None
52
+
53
+ def _strip_ansi(s: str) -> str:
54
+ return _ANSI_RE.sub("", s)
55
+
56
+ def _resolve_daily_log_path():
57
+ global _log_daily_path, _log_daily_date
58
+ if not _log_dir:
59
+ return
60
+ today = datetime.now().strftime("%Y-%m-%d")
61
+ if today == _log_daily_date and _log_daily_path:
62
+ return
63
+ month_dir = os.path.join(_log_dir, today[:7])
64
+ os.makedirs(month_dir, exist_ok=True)
65
+ _log_daily_path = os.path.join(month_dir, f"{today}.log")
66
+ _log_daily_date = today
67
+
68
+ def _write_log(plain_line: str):
69
+ with _log_lock:
70
+ if _log_latest_path:
71
+ try:
72
+ with open(_log_latest_path, "a", encoding="utf-8") as f:
73
+ f.write(plain_line)
74
+ except Exception:
75
+ pass
76
+ _resolve_daily_log_path()
77
+ if _log_daily_path:
78
+ try:
79
+ with open(_log_daily_path, "a", encoding="utf-8") as f:
80
+ f.write(plain_line)
81
+ except Exception:
82
+ pass
83
+
84
+ def _write_crash(exc_type, exc_value, exc_tb, thread_name=None, severity="critical", handled=False):
85
+ record = {
86
+ "timestamp": datetime.now(timezone.utc).isoformat(),
87
+ "module": MODULE_NAME,
88
+ "thread": thread_name or threading.current_thread().name,
89
+ "exception_type": exc_type.__name__ if exc_type else "Unknown",
90
+ "exception_message": str(exc_value),
91
+ "traceback": "".join(traceback.format_exception(exc_type, exc_value, exc_tb)),
92
+ "severity": severity,
93
+ "handled": handled,
94
+ "process_id": os.getpid(),
95
+ "platform": sys.platform,
96
+ "runtime_version": f"Python {sys.version.split()[0]}",
97
+ }
98
+
99
+ if exc_tb:
100
+ tb_entries = traceback.extract_tb(exc_tb)
101
+ if tb_entries:
102
+ last = tb_entries[-1]
103
+ record["context"] = {
104
+ "function": last.name,
105
+ "file": os.path.basename(last.filename),
106
+ "line": last.lineno,
107
+ }
108
+
109
+ line = json.dumps(record, ensure_ascii=False) + "\n"
110
+
111
+ if _crash_log_path:
112
+ try:
113
+ with open(_crash_log_path, "a", encoding="utf-8") as f:
114
+ f.write(line)
115
+ except Exception:
116
+ pass
117
+
118
+ if _log_dir:
119
+ try:
120
+ today = datetime.now().strftime("%Y-%m-%d")
121
+ archive_dir = os.path.join(_log_dir, "crashes", today[:7])
122
+ os.makedirs(archive_dir, exist_ok=True)
123
+ archive_path = os.path.join(archive_dir, f"{today}.jsonl")
124
+ with open(archive_path, "a", encoding="utf-8") as f:
125
+ f.write(line)
126
+ except Exception:
127
+ pass
128
+
129
+ def _print_crash_summary(exc_type, exc_tb, thread_name=None):
130
+ RED = "\033[91m"
131
+ RESET = "\033[0m"
132
+
133
+ if exc_tb:
134
+ tb_entries = traceback.extract_tb(exc_tb)
135
+ if tb_entries:
136
+ last = tb_entries[-1]
137
+ location = f"{os.path.basename(last.filename)}:{last.lineno}"
138
+ else:
139
+ location = "unknown"
140
+ else:
141
+ location = "unknown"
142
+
143
+ prefix = f"[{MODULE_NAME}]"
144
+ if thread_name:
145
+ _builtin_print(f"{prefix} {RED}线程 {thread_name} 崩溃: "
146
+ f"{exc_type.__name__} in {location}{RESET}")
147
+ else:
148
+ _builtin_print(f"{prefix} {RED}崩溃: {exc_type.__name__} in {location}{RESET}")
149
+ if _crash_log_path:
150
+ _builtin_print(f"{prefix} 崩溃日志: {_crash_log_path}")
151
+
152
+ def _setup_exception_hooks():
153
+ _orig_excepthook = sys.excepthook
154
+
155
+ def _excepthook(exc_type, exc_value, exc_tb):
156
+ _write_crash(exc_type, exc_value, exc_tb, severity="critical", handled=False)
157
+ _print_crash_summary(exc_type, exc_tb)
158
+ _orig_excepthook(exc_type, exc_value, exc_tb)
159
+
160
+ sys.excepthook = _excepthook
161
+
162
+ if hasattr(threading, "excepthook"):
163
+ def _thread_excepthook(args):
164
+ _write_crash(args.exc_type, args.exc_value, args.exc_traceback,
165
+ thread_name=args.thread.name if args.thread else "unknown",
166
+ severity="error", handled=False)
167
+ _print_crash_summary(args.exc_type, args.exc_traceback,
168
+ thread_name=args.thread.name if args.thread else None)
169
+
170
+ threading.excepthook = _thread_excepthook
171
+
172
+ def _tprint(*args, **kwargs):
173
+ global _last_ts
174
+ now = time.monotonic()
175
+ elapsed = now - _start_ts
176
+ delta = now - _last_ts
177
+ _last_ts = now
178
+
179
+ if elapsed < 1:
180
+ elapsed_str = f"{elapsed * 1000:.0f}ms"
181
+ elif elapsed < 100:
182
+ elapsed_str = f"{elapsed:.1f}s"
183
+ else:
184
+ elapsed_str = f"{elapsed:.0f}s"
185
+
186
+ if delta < 0.001:
187
+ delta_str = ""
188
+ elif delta < 1:
189
+ delta_str = f"+{delta * 1000:.0f}ms"
190
+ elif delta < 100:
191
+ delta_str = f"+{delta:.1f}s"
192
+ else:
193
+ delta_str = f"+{delta:.0f}s"
194
+
195
+ ts = datetime.now().strftime("%H:%M:%S.%f")[:-3]
196
+
197
+ _builtin_print(*args, **kwargs)
198
+
199
+ if _log_latest_path or _log_daily_path:
200
+ sep = kwargs.get("sep", " ")
201
+ end = kwargs.get("end", "\n")
202
+ text = sep.join(str(a) for a in args)
203
+ prefix = f"[{elapsed_str:>6}] {ts} {delta_str:>8} "
204
+ _write_log(prefix + _strip_ansi(text) + end)
205
+
206
+ builtins.print = _tprint
207
+
208
+ # Initialize log files
209
+ module_data = os.environ.get("KITE_MODULE_DATA")
210
+ if module_data:
211
+ _log_dir = os.path.join(module_data, "log")
212
+ os.makedirs(_log_dir, exist_ok=True)
213
+ suffix = os.environ.get("KITE_INSTANCE_SUFFIX", "")
214
+
215
+ _log_latest_path = os.path.join(_log_dir, f"latest{suffix}.log")
216
+ try:
217
+ with open(_log_latest_path, "w", encoding="utf-8") as f:
218
+ pass
219
+ except Exception:
220
+ _log_latest_path = None
221
+
222
+ _crash_log_path = os.path.join(_log_dir, f"crashes{suffix}.jsonl")
223
+ try:
224
+ with open(_crash_log_path, "w", encoding="utf-8") as f:
225
+ pass
226
+ except Exception:
227
+ _crash_log_path = None
228
+
229
+ _resolve_daily_log_path()
230
+
231
+ _setup_exception_hooks()
232
+
233
+
234
+ def _get_kernel_stats(kernel_port: int) -> dict:
235
+ """Fetch Kernel /stats via HTTP."""
236
+ try:
237
+ resp = httpx.get(f"http://127.0.0.1:{kernel_port}/stats", timeout=5)
238
+ if resp.status_code == 200:
239
+ return resp.json()
240
+ except Exception:
241
+ pass
242
+ return {}
243
+
244
+
245
+ def _sample_kernel_resources(kernel_port: int) -> dict:
246
+ """Sample Kernel process CPU/memory via psutil. Returns {} if unavailable."""
247
+ try:
248
+ import psutil
249
+ except ImportError:
250
+ return {"error": "psutil not installed"}
251
+ for conn in psutil.net_connections(kind="tcp"):
252
+ if conn.laddr.port == kernel_port and conn.status == "LISTEN":
253
+ try:
254
+ p = psutil.Process(conn.pid)
255
+ mem = p.memory_info()
256
+ return {
257
+ "pid": conn.pid,
258
+ "cpu_percent": p.cpu_percent(interval=1),
259
+ "rss_mb": round(mem.rss / 1048576, 1),
260
+ "vms_mb": round(mem.vms / 1048576, 1),
261
+ "threads": p.num_threads(),
262
+ }
263
+ except Exception:
264
+ pass
265
+ return {"error": "kernel process not found"}
266
+
267
+
268
+ # ── WebSocket client ──
269
+
270
+ class WsClient:
271
+ """Lightweight async WebSocket client for Kernel benchmarks (JSON-RPC 2.0)."""
272
+
273
+ def __init__(self, ws_url: str, token: str, client_id: str):
274
+ self.url = f"{ws_url}?token={token}&id={client_id}"
275
+ self.id = client_id
276
+ self.ws = None
277
+
278
+ async def connect(self):
279
+ self.ws = await websockets.connect(self.url, max_size=None)
280
+
281
+ async def subscribe(self, patterns: list[str]):
282
+ msg = {"jsonrpc": "2.0", "id": str(uuid.uuid4()), "method": "event.subscribe", "params": {"events": patterns}}
283
+ await self.ws.send(json.dumps(msg))
284
+
285
+ async def publish(self, event_type: str, data: dict) -> str:
286
+ eid = str(uuid.uuid4())
287
+ msg = {
288
+ "jsonrpc": "2.0", "id": str(uuid.uuid4()), "method": "event.publish",
289
+ "params": {
290
+ "event_id": eid, "event": event_type, "data": data,
291
+ },
292
+ }
293
+ await self.ws.send(json.dumps(msg))
294
+ return eid
295
+
296
+ async def recv(self, timeout=5.0):
297
+ try:
298
+ return json.loads(await asyncio.wait_for(self.ws.recv(), timeout=timeout))
299
+ except Exception:
300
+ return None
301
+
302
+ async def close(self):
303
+ if self.ws:
304
+ await self.ws.close()
305
+
306
+
307
+ # ── Benchmark runner ──
308
+
309
+ class BenchRunner:
310
+ def __init__(self, ws_url: str, token: str, kernel_port: int):
311
+ self.ws_url = ws_url
312
+ self.token = token
313
+ self.kernel_port = kernel_port
314
+ self.results = {}
315
+
316
+ async def _make_client(self, name: str) -> WsClient:
317
+ c = WsClient(self.ws_url, self.token, name)
318
+ await c.connect()
319
+ return c
320
+
321
+ # ── Throughput: burst N events, measure hub processing rate ──
322
+
323
+ async def bench_throughput(self, n: int = 10000):
324
+ print(f"{TAG} Throughput test: {n} events...")
325
+ pub = await self._make_client("bench_tp_pub")
326
+ sub = await self._make_client("bench_tp_sub")
327
+ await sub.subscribe(["bench.tp.*"])
328
+ await asyncio.sleep(0.2)
329
+
330
+ recvd = 0
331
+ stop = asyncio.Event()
332
+
333
+ async def _recv():
334
+ nonlocal recvd
335
+ while not stop.is_set():
336
+ try:
337
+ raw = await asyncio.wait_for(sub.ws.recv(), timeout=0.5)
338
+ msg = json.loads(raw)
339
+ # JSON-RPC Notification (event): has method, no id
340
+ if "method" in msg and "id" not in msg:
341
+ recvd += 1
342
+ except Exception:
343
+ pass
344
+
345
+ async def _drain_acks():
346
+ while not stop.is_set():
347
+ try:
348
+ await asyncio.wait_for(pub.ws.recv(), timeout=0.5)
349
+ except Exception:
350
+ pass
351
+
352
+ recv_task = asyncio.create_task(_recv())
353
+ ack_task = asyncio.create_task(_drain_acks())
354
+
355
+ hub_before = (_get_kernel_stats(self.kernel_port).get("counters") or {})
356
+
357
+ t0 = time.time()
358
+ for i in range(n):
359
+ await pub.publish("bench.tp.test", {"i": i})
360
+ send_time = time.time() - t0
361
+
362
+ deadline = time.time() + max(15, n / 300)
363
+ while recvd < n and time.time() < deadline:
364
+ await asyncio.sleep(0.1)
365
+ total_time = time.time() - t0
366
+
367
+ stop.set()
368
+ await recv_task
369
+ ack_task.cancel()
370
+
371
+ hub_after = (_get_kernel_stats(self.kernel_port).get("counters") or {})
372
+
373
+ await pub.close()
374
+ await sub.close()
375
+
376
+ self.results["throughput"] = {
377
+ "events": n,
378
+ "send_rate": round(n / send_time),
379
+ "recv_rate": round(recvd / total_time) if total_time > 0 else 0,
380
+ "client_recv": recvd,
381
+ "hub_queued": hub_after.get("events_queued", 0) - hub_before.get("events_queued", 0),
382
+ "hub_routed": hub_after.get("events_routed", 0) - hub_before.get("events_routed", 0),
383
+ "send_time_s": round(send_time, 2),
384
+ "total_time_s": round(total_time, 2),
385
+ }
386
+ print(f"{TAG} send_rate={self.results['throughput']['send_rate']} evt/s, "
387
+ f"recv={recvd}/{n}, time={round(total_time, 1)}s")
388
+
389
+ # ── Latency: sequential pub→recv round-trip ──
390
+
391
+ async def bench_latency(self, n: int = 200):
392
+ print(f"{TAG} Latency test: {n} samples...")
393
+ pub = await self._make_client("bench_lat_pub")
394
+ sub = await self._make_client("bench_lat_sub")
395
+ await sub.subscribe(["bench.lat.*"])
396
+ await asyncio.sleep(0.2)
397
+
398
+ latencies = []
399
+ for i in range(n):
400
+ t0 = time.time()
401
+ await pub.publish("bench.lat.test", {"i": i})
402
+ await pub.recv(timeout=2) # RPC response
403
+ msg = await sub.recv(timeout=2)
404
+ if msg and "method" in msg and "id" not in msg:
405
+ latencies.append((time.time() - t0) * 1000)
406
+
407
+ await pub.close()
408
+ await sub.close()
409
+
410
+ if latencies:
411
+ latencies.sort()
412
+ self.results["latency"] = {
413
+ "samples": len(latencies),
414
+ "avg_ms": round(statistics.mean(latencies), 2),
415
+ "p50_ms": round(latencies[len(latencies) // 2], 2),
416
+ "p95_ms": round(latencies[int(len(latencies) * 0.95)], 2),
417
+ "p99_ms": round(latencies[int(len(latencies) * 0.99)], 2),
418
+ }
419
+ print(f"{TAG} avg={self.results['latency']['avg_ms']}ms, "
420
+ f"p50={self.results['latency']['p50_ms']}ms, "
421
+ f"p99={self.results['latency']['p99_ms']}ms")
422
+
423
+ # ── Fan-out: 1 pub, N subs ──
424
+
425
+ async def bench_fanout(self, n_events: int = 2000):
426
+ for n_subs in [1, 5, 10, 50]:
427
+ print(f"{TAG} Fan-out x{n_subs}: {n_events} events...")
428
+ pub = await self._make_client("bench_fo_pub")
429
+ subs = []
430
+ for i in range(n_subs):
431
+ s = await self._make_client(f"bench_fo_sub_{i}")
432
+ await s.subscribe(["bench.fo.*"])
433
+ subs.append(s)
434
+ await asyncio.sleep(0.3)
435
+
436
+ counts = [0] * n_subs
437
+ stop = asyncio.Event()
438
+
439
+ async def _recv(idx, client):
440
+ while not stop.is_set():
441
+ try:
442
+ raw = await asyncio.wait_for(client.ws.recv(), timeout=0.5)
443
+ msg = json.loads(raw)
444
+ if "method" in msg and "id" not in msg:
445
+ counts[idx] += 1
446
+ except Exception:
447
+ pass
448
+
449
+ tasks = [asyncio.create_task(_recv(i, s)) for i, s in enumerate(subs)]
450
+ ack_stop = asyncio.Event()
451
+
452
+ async def _drain():
453
+ while not ack_stop.is_set():
454
+ try:
455
+ await asyncio.wait_for(pub.ws.recv(), timeout=0.5)
456
+ except Exception:
457
+ pass
458
+
459
+ ack_task = asyncio.create_task(_drain())
460
+
461
+ t0 = time.time()
462
+ for i in range(n_events):
463
+ await pub.publish("bench.fo.test", {"i": i})
464
+ send_time = time.time() - t0
465
+
466
+ await asyncio.sleep(max(3, n_events * n_subs / 3000))
467
+ stop.set()
468
+ ack_stop.set()
469
+ for t in tasks:
470
+ await t
471
+ ack_task.cancel()
472
+
473
+ await pub.close()
474
+ for s in subs:
475
+ await s.close()
476
+
477
+ avg_recv = sum(counts) / n_subs if n_subs else 0
478
+ self.results[f"fanout_{n_subs}"] = {
479
+ "subs": n_subs,
480
+ "events": n_events,
481
+ "send_rate": round(n_events / send_time) if send_time > 0 else 0,
482
+ "avg_recv": round(avg_recv),
483
+ "min_recv": min(counts),
484
+ }
485
+ print(f"{TAG} subs={n_subs}, avg_recv={round(avg_recv)}/{n_events}, "
486
+ f"min={min(counts)}")
487
+
488
+ # ── Save results ──
489
+
490
+ def save(self):
491
+ os.makedirs(RESULTS_DIR, exist_ok=True)
492
+ now = datetime.now()
493
+ filepath = os.path.join(RESULTS_DIR, now.strftime("%Y-%m-%d_%H-%M-%S") + ".json")
494
+
495
+ kernel_stats = _get_kernel_stats(self.kernel_port)
496
+ resources = _sample_kernel_resources(self.kernel_port)
497
+
498
+ data = {
499
+ "timestamp": now.isoformat(),
500
+ "env": {
501
+ "platform": sys.platform,
502
+ "python": platform.python_version(),
503
+ },
504
+ "hub_resources": resources,
505
+ "hub_counters": kernel_stats.get("counters", {}),
506
+ **self.results,
507
+ }
508
+ with open(filepath, "w", encoding="utf-8") as f:
509
+ json.dump(data, f, indent=2, ensure_ascii=False)
510
+ print(f"{TAG} Results saved: {filepath}")
511
+
512
+
513
+ def _send_ready_event(ws_url: str, token: str):
514
+ """Send module.ready to Kernel via JSON-RPC 2.0. Startup phase complete."""
515
+ try:
516
+ import websockets.sync.client as ws_sync
517
+ url = f"{ws_url}?token={token}&id=event_hub_bench"
518
+ with ws_sync.connect(url, close_timeout=3) as ws:
519
+ # Subscribe (先订阅)
520
+ ws.send(json.dumps({
521
+ "jsonrpc": "2.0", "id": str(uuid.uuid4()),
522
+ "method": "event.subscribe", "params": {"events": []},
523
+ }))
524
+ # Register (再注册)
525
+ ws.send(json.dumps({
526
+ "jsonrpc": "2.0", "id": str(uuid.uuid4()),
527
+ "method": "registry.register",
528
+ "params": {
529
+ "module_id": "event_hub_bench",
530
+ "module_type": "tool",
531
+ "name": "Event Hub Benchmark",
532
+ },
533
+ }))
534
+ # Publish ready
535
+ ws.send(json.dumps({
536
+ "jsonrpc": "2.0", "id": str(uuid.uuid4()),
537
+ "method": "event.publish",
538
+ "params": {
539
+ "event_id": str(uuid.uuid4()),
540
+ "event": "module.ready",
541
+ "data": {"module_id": "event_hub_bench"},
542
+ },
543
+ }))
544
+ time.sleep(0.1)
545
+ except Exception as e:
546
+ print(f"{TAG} WARNING: Could not send module.ready: {e}")
547
+
548
+
549
+ def _send_exiting_event(ws_url: str, token: str, reason: str):
550
+ """Send module.exiting event to Kernel via JSON-RPC 2.0 before exit."""
551
+ try:
552
+ import websockets.sync.client as ws_sync
553
+ url = f"{ws_url}?token={token}&id=event_hub_bench"
554
+ with ws_sync.connect(url, close_timeout=3) as ws:
555
+ ws.send(json.dumps({
556
+ "jsonrpc": "2.0", "id": str(uuid.uuid4()),
557
+ "method": "event.publish",
558
+ "params": {
559
+ "event_id": str(uuid.uuid4()),
560
+ "event": "module.exiting",
561
+ "data": {
562
+ "module_id": "event_hub_bench",
563
+ "reason": reason,
564
+ "action": "none",
565
+ },
566
+ },
567
+ }))
568
+ time.sleep(0.3)
569
+ except Exception as e:
570
+ print(f"{TAG} WARNING: Could not send module.exiting: {e}")
571
+
572
+
573
+ # ── Entry point ──
574
+
575
+ async def _run(ws_url: str, token: str, kernel_port: int):
576
+ bench = BenchRunner(ws_url, token, kernel_port)
577
+ await bench.bench_throughput()
578
+ await bench.bench_latency()
579
+ await bench.bench_fanout()
580
+ bench.save()
581
+
582
+
583
+ def main():
584
+ # Read boot_info from stdin (only token)
585
+ token = ""
586
+ try:
587
+ line = sys.stdin.readline().strip()
588
+ if line:
589
+ boot = json.loads(line)
590
+ token = boot.get("token", "")
591
+ except Exception:
592
+ pass
593
+
594
+ # Read registry_port from environment variable
595
+ kernel_port = int(os.environ.get("KITE_KERNEL_PORT", "0"))
596
+
597
+ if not token or not kernel_port:
598
+ print(f"{TAG} ERROR: Missing token or KITE_KERNEL_PORT")
599
+ sys.exit(1)
600
+
601
+ ws_url = f"ws://127.0.0.1:{kernel_port}/ws"
602
+ print(f"{TAG} Kernel at port {kernel_port}")
603
+
604
+ # Debug mode required: multiple benchmark clients share one token
605
+ if os.environ.get("KITE_DEBUG") != "1":
606
+ print(f"{TAG} 需要调试模式才能运行 (启动时加 --debug)")
607
+ _send_exiting_event(ws_url, token, "requires KITE_DEBUG=1")
608
+ sys.exit(0)
609
+
610
+ # ── Startup complete — notify Launcher ──
611
+ _send_ready_event(ws_url, token)
612
+
613
+ # ── Business logic: run benchmarks ──
614
+ print(f"{TAG} Starting benchmarks...")
615
+
616
+ asyncio.run(_run(ws_url, token, kernel_port))
617
+
618
+ print(f"{TAG} Benchmarks complete, sending exit intent...")
619
+ _send_exiting_event(ws_url, token, "benchmarks complete")
620
+ print(f"{TAG} Exiting.")
621
+
622
+
623
+ if __name__ == "__main__":
624
+ main()