@agentunion/kite 1.2.0 → 1.3.1

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 (53) hide show
  1. package/CHANGELOG.md +208 -0
  2. package/README.md +48 -0
  3. package/cli.js +1 -1
  4. package/extensions/agents/assistant/entry.py +30 -81
  5. package/extensions/agents/assistant/module.md +1 -1
  6. package/extensions/agents/assistant/server.py +83 -122
  7. package/extensions/channels/acp_channel/entry.py +30 -81
  8. package/extensions/channels/acp_channel/module.md +1 -1
  9. package/extensions/channels/acp_channel/server.py +83 -122
  10. package/extensions/event_hub_bench/entry.py +81 -121
  11. package/extensions/services/backup/entry.py +213 -85
  12. package/extensions/services/model_service/entry.py +213 -85
  13. package/extensions/services/watchdog/entry.py +513 -460
  14. package/extensions/services/watchdog/monitor.py +55 -69
  15. package/extensions/services/web/entry.py +11 -108
  16. package/extensions/services/web/server.py +120 -77
  17. package/{core/registry → kernel}/entry.py +65 -37
  18. package/{core/event_hub/hub.py → kernel/event_hub.py} +61 -81
  19. package/kernel/module.md +33 -0
  20. package/{core/registry/store.py → kernel/registry_store.py} +13 -4
  21. package/kernel/rpc_router.py +388 -0
  22. package/kernel/server.py +267 -0
  23. package/launcher/__init__.py +10 -0
  24. package/launcher/__main__.py +6 -0
  25. package/launcher/count_lines.py +258 -0
  26. package/{core/launcher → launcher}/entry.py +693 -767
  27. package/launcher/logging_setup.py +289 -0
  28. package/{core/launcher → launcher}/module_scanner.py +11 -6
  29. package/main.py +11 -350
  30. package/package.json +6 -9
  31. package/__init__.py +0 -1
  32. package/__main__.py +0 -15
  33. package/core/event_hub/BENCHMARK.md +0 -94
  34. package/core/event_hub/__init__.py +0 -0
  35. package/core/event_hub/bench.py +0 -459
  36. package/core/event_hub/bench_extreme.py +0 -308
  37. package/core/event_hub/bench_perf.py +0 -350
  38. package/core/event_hub/entry.py +0 -436
  39. package/core/event_hub/module.md +0 -20
  40. package/core/event_hub/server.py +0 -269
  41. package/core/kite_log.py +0 -241
  42. package/core/launcher/__init__.py +0 -0
  43. package/core/registry/__init__.py +0 -0
  44. package/core/registry/module.md +0 -30
  45. package/core/registry/server.py +0 -339
  46. package/extensions/services/backup/server.py +0 -244
  47. package/extensions/services/model_service/server.py +0 -236
  48. package/extensions/services/watchdog/server.py +0 -229
  49. /package/{core → kernel}/__init__.py +0 -0
  50. /package/{core/event_hub → kernel}/dedup.py +0 -0
  51. /package/{core/event_hub → kernel}/router.py +0 -0
  52. /package/{core/launcher → launcher}/module.md +0 -0
  53. /package/{core/launcher → launcher}/process_manager.py +0 -0
@@ -1,8 +1,6 @@
1
1
  """
2
- ACP Channel HTTP server.
3
- Exposes /health and /status endpoints.
4
- Connects to Event Hub via WebSocket for event publishing and subscription.
5
- Sends periodic heartbeat to Registry and test events to Event Hub.
2
+ ACP Channel WebSocket client.
3
+ Connects to Kernel via WebSocket JSON-RPC 2.0 for event publishing and subscription.
6
4
  """
7
5
 
8
6
  import asyncio
@@ -11,127 +9,101 @@ import time
11
9
  import uuid
12
10
  from datetime import datetime, timezone
13
11
 
14
- import httpx
15
12
  import websockets
16
- from fastapi import FastAPI
17
13
 
18
14
 
19
15
  class AcpChannelServer:
20
16
 
21
- def __init__(self, token: str = "", registry_url: str = "",
22
- event_hub_ws: str = "", boot_t0: float = 0):
17
+ def __init__(self, token: str = "", kernel_port: int = 0, boot_t0: float = 0):
23
18
  self.token = token
24
- self.registry_url = registry_url
25
- self.event_hub_ws = event_hub_ws
19
+ self.kernel_port = kernel_port
26
20
  self.boot_t0 = boot_t0
27
21
  self._ws_task: asyncio.Task | None = None
28
- self._heartbeat_task: asyncio.Task | None = None
29
22
  self._test_task: asyncio.Task | None = None
30
23
  self._ws: object | None = None
31
24
  self._ready_sent = False
32
25
  self._shutting_down = False
33
- self._uvicorn_server = None # set by entry.py for graceful shutdown
34
26
  self._start_time = time.time()
35
- self.app = self._create_app()
36
-
37
- def _create_app(self) -> FastAPI:
38
- app = FastAPI(title="Kite ACP Channel", docs_url=None, redoc_url=None)
39
- server = self
40
-
41
- @app.on_event("startup")
42
- async def _startup():
43
- server._heartbeat_task = asyncio.create_task(server._heartbeat_loop())
44
- if server.event_hub_ws:
45
- server._ws_task = asyncio.create_task(server._ws_loop())
46
- server._test_task = asyncio.create_task(server._test_event_loop())
47
-
48
- @app.on_event("shutdown")
49
- async def _shutdown():
50
- if server._heartbeat_task:
51
- server._heartbeat_task.cancel()
52
- if server._ws_task:
53
- server._ws_task.cancel()
54
- if server._test_task:
55
- server._test_task.cancel()
56
- if server._ws:
57
- await server._ws.close()
58
- print("[acp_channel] Shutdown complete")
59
-
60
- @app.get("/health")
61
- async def health():
62
- return {
63
- "status": "healthy",
64
- "details": {
65
- "event_hub_connected": server._ws is not None,
66
- "uptime_seconds": round(time.time() - server._start_time),
67
- },
68
- }
69
27
 
70
- @app.get("/status")
71
- async def status():
72
- return {
73
- "module": "acp_channel",
74
- "status": "running",
75
- "event_hub_connected": server._ws is not None,
76
- "uptime_seconds": round(time.time() - server._start_time),
77
- }
28
+ async def run(self):
29
+ """Main entry point: start WebSocket loop and test event loop."""
30
+ if self.kernel_port:
31
+ self._ws_task = asyncio.create_task(self._ws_loop())
32
+ self._test_task = asyncio.create_task(self._test_event_loop())
33
+
34
+ # Wait for tasks to complete
35
+ tasks = [t for t in [self._ws_task, self._test_task] if t]
36
+ if tasks:
37
+ await asyncio.gather(*tasks, return_exceptions=True)
78
38
 
79
- return app
39
+ print("[acp_channel] Shutdown complete")
80
40
 
81
- # ── Event Hub WebSocket client ──
41
+ # ── Kernel WebSocket client ──
82
42
 
83
43
  async def _ws_loop(self):
84
- """Connect to Event Hub, subscribe, and listen. Reconnect on failure."""
85
- retry_delay = 0.5 # start with 0.5s
86
- max_delay = 30 # cap at 30s
44
+ """Connect to Kernel, subscribe, register, and listen. Reconnect on failure."""
45
+ retry_delay = 0.5
46
+ max_delay = 30
87
47
  while not self._shutting_down:
88
48
  try:
89
49
  await self._ws_connect()
90
50
  except asyncio.CancelledError:
91
51
  return
92
- retry_delay = 0.5 # reset on successful connection
93
52
  except Exception as e:
94
- print(f"[acp_channel] Event Hub connection error: {e}, retrying in {retry_delay:.1f}s")
53
+ print(f"[acp_channel] Kernel connection error: {e}, retrying in {retry_delay:.1f}s")
95
54
  self._ws = None
96
55
  if self._shutting_down:
97
56
  return
98
57
  await asyncio.sleep(retry_delay)
99
- retry_delay = min(retry_delay * 2, max_delay) # exponential backoff
58
+ retry_delay = min(retry_delay * 2, max_delay)
100
59
 
101
60
  async def _ws_connect(self):
102
- """Single WebSocket session: connect, subscribe, receive loop."""
103
- url = f"{self.event_hub_ws}?token={self.token}&id=acp_channel"
104
- print(f"[acp_channel] WS connecting to {self.event_hub_ws}")
61
+ """Single WebSocket session: connect, subscribe, register, ready, receive loop."""
62
+ url = f"ws://127.0.0.1:{self.kernel_port}/ws?token={self.token}&id=acp_channel"
63
+ print(f"[acp_channel] Connecting to Kernel (port {self.kernel_port})")
105
64
  async with websockets.connect(url, open_timeout=3, ping_interval=None, ping_timeout=None, close_timeout=10) as ws:
106
65
  self._ws = ws
107
66
  elapsed = time.monotonic() - self.boot_t0 if self.boot_t0 else 0
108
67
  elapsed_str = f" ({elapsed:.1f}s)" if elapsed else ""
109
- print(f"[acp_channel] Connected to Event Hub{elapsed_str}")
68
+ print(f"[acp_channel] Connected to Kernel{elapsed_str}")
110
69
 
111
- # Subscribe to module lifecycle events + shutdown
112
- await ws.send(json.dumps({
113
- "type": "subscribe",
70
+ # Step 1: Subscribe to events (先订阅)
71
+ await self._rpc_call(ws, "event.subscribe", {
114
72
  "events": ["module.started", "module.stopped", "module.shutdown"],
115
- }))
73
+ })
74
+
75
+ # Step 2: Register to Kernel (再注册)
76
+ await self._rpc_call(ws, "registry.register", {
77
+ "module_id": "acp_channel",
78
+ "module_type": "channel",
79
+ "name": "ACP Channel",
80
+ "events_publish": {
81
+ "acp_channel.test": {},
82
+ },
83
+ "events_subscribe": [
84
+ "module.started",
85
+ "module.stopped",
86
+ "module.shutdown",
87
+ ],
88
+ })
116
89
 
117
- # Send module.ready (once) so Launcher knows we're up
90
+ # Step 3: Publish module.ready (once)
118
91
  if not self._ready_sent:
119
- ready_msg = {
120
- "type": "event",
92
+ await self._rpc_call(ws, "event.publish", {
121
93
  "event_id": str(uuid.uuid4()),
122
94
  "event": "module.ready",
123
- "source": "acp_channel",
124
- "timestamp": datetime.now(timezone.utc).isoformat(),
125
95
  "data": {
126
96
  "module_id": "acp_channel",
127
97
  "graceful_shutdown": True,
128
98
  },
129
- }
130
- await ws.send(json.dumps(ready_msg))
99
+ })
131
100
  self._ready_sent = True
132
101
  elapsed = time.monotonic() - self.boot_t0 if self.boot_t0 else 0
133
102
  elapsed_str = f" ({elapsed:.1f}s)" if elapsed else ""
134
- print(f"[acp_channel] module.ready sent{elapsed_str}")
103
+ print(f"[acp_channel] module.ready published{elapsed_str}")
104
+
105
+ # Reset retry delay on successful connection
106
+ retry_delay = 0.5
135
107
 
136
108
  # Receive loop
137
109
  async for raw in ws:
@@ -141,18 +113,22 @@ class AcpChannelServer:
141
113
  continue
142
114
 
143
115
  try:
144
- msg_type = msg.get("type", "")
145
- if msg_type == "event":
146
- event_name = msg.get("event", "")
116
+ has_method = "method" in msg
117
+ has_id = "id" in msg
118
+
119
+ if has_method and not has_id:
120
+ # JSON-RPC Notification (event delivery)
121
+ params = msg.get("params", {})
122
+ event_name = params.get("event", "")
147
123
  if event_name == "module.shutdown":
148
- target = (msg.get("data") if isinstance(msg.get("data"), dict) else {}).get("module_id", "")
124
+ data = params.get("data", {})
125
+ target = data.get("module_id", "")
149
126
  if target == "acp_channel":
150
127
  await self._handle_shutdown(ws)
151
128
  return
152
- elif msg_type == "ack":
153
- pass # publish confirmed
154
- elif msg_type == "error":
155
- print(f"[acp_channel] Event Hub error: {msg.get('message')}")
129
+ elif not has_method and has_id:
130
+ # JSON-RPC Response (to our RPC calls)
131
+ pass
156
132
  except Exception as e:
157
133
  print(f"[acp_channel] 事件处理异常(已忽略): {e}")
158
134
 
@@ -162,64 +138,49 @@ class AcpChannelServer:
162
138
  self._shutting_down = True
163
139
 
164
140
  # Step 1: Send ack
165
- await self._publish_event({
141
+ await self._rpc_call(ws, "event.publish", {
142
+ "event_id": str(uuid.uuid4()),
166
143
  "event": "module.shutdown.ack",
167
144
  "data": {"module_id": "acp_channel", "estimated_cleanup": 2},
168
145
  })
169
146
  print("[acp_channel] shutdown ack sent")
170
147
 
171
148
  # Step 2: Cleanup (cancel background tasks)
172
- if self._heartbeat_task:
173
- self._heartbeat_task.cancel()
174
149
  if self._test_task:
175
150
  self._test_task.cancel()
176
151
 
177
152
  # Step 3: Send ready (before closing WS!)
178
- await self._publish_event({
153
+ await self._rpc_call(ws, "event.publish", {
154
+ "event_id": str(uuid.uuid4()),
179
155
  "event": "module.shutdown.ready",
180
156
  "data": {"module_id": "acp_channel"},
181
157
  })
182
- print("[acp_channel] Shutdown complete")
158
+ print("[acp_channel] Shutdown ready sent")
183
159
 
184
- # Step 4: Trigger uvicorn exit (WS will close when uvicorn shuts down)
185
- if self._uvicorn_server:
186
- self._uvicorn_server.should_exit = True
160
+ # Step 4: Exit process
161
+ import sys
162
+ sys.exit(0)
163
+
164
+ async def _rpc_call(self, ws, method: str, params: dict = None):
165
+ """Send a JSON-RPC 2.0 request."""
166
+ msg = {"jsonrpc": "2.0", "id": str(uuid.uuid4()), "method": method}
167
+ if params:
168
+ msg["params"] = params
169
+ await ws.send(json.dumps(msg))
187
170
 
188
171
  async def _publish_event(self, event: dict):
189
- """Publish an event to Event Hub via WebSocket."""
172
+ """Publish an event via JSON-RPC event.publish."""
190
173
  if not self._ws:
191
174
  return
192
- msg = {
193
- "type": "event",
194
- "event_id": str(uuid.uuid4()),
195
- "event": event.get("event", ""),
196
- "source": "acp_channel",
197
- "timestamp": datetime.now(timezone.utc).isoformat(),
198
- "data": event.get("data", {}),
199
- }
200
175
  try:
201
- await self._ws.send(json.dumps(msg))
176
+ await self._rpc_call(self._ws, "event.publish", {
177
+ "event_id": str(uuid.uuid4()),
178
+ "event": event.get("event", ""),
179
+ "data": event.get("data", {}),
180
+ })
202
181
  except Exception as e:
203
182
  print(f"[acp_channel] Failed to publish event: {e}")
204
183
 
205
- # ── Heartbeat to Registry ──
206
-
207
- async def _heartbeat_loop(self):
208
- """Send heartbeat to Registry every 30 seconds."""
209
- while True:
210
- await asyncio.sleep(30)
211
- try:
212
- async with httpx.AsyncClient() as client:
213
- await client.post(
214
- f"{self.registry_url}/modules",
215
- json={"action": "heartbeat", "module_id": "acp_channel"},
216
- headers={"Authorization": f"Bearer {self.token}"},
217
- timeout=5,
218
- )
219
- print("[acp_channel] heartbeat sent")
220
- except Exception:
221
- pass
222
-
223
184
  # ── Test event loop ──
224
185
 
225
186
  async def _test_event_loop(self):
@@ -1,6 +1,6 @@
1
1
  """
2
2
  Event Hub Benchmark module entry point.
3
- Discovers hub via Registry, runs benchmarks, saves results, exits.
3
+ Connects to Kernel via WebSocket JSON-RPC 2.0, runs benchmarks, saves results, exits.
4
4
  """
5
5
 
6
6
  import asyncio
@@ -231,54 +231,10 @@ if module_data:
231
231
  _setup_exception_hooks()
232
232
 
233
233
 
234
- def _register_to_registry(client: httpx.Client, registry_url: str, token: str):
235
- """Register module identity to Registry (events, metadata — no API endpoint)."""
236
- payload = {
237
- "action": "register",
238
- "module_id": "event_hub_bench",
239
- "module_type": "tool",
240
- "name": "Event Hub Benchmark",
241
- }
242
- headers = {"Authorization": f"Bearer {token}"}
243
- try:
244
- resp = client.post(
245
- f"{registry_url}/modules",
246
- json=payload, headers=headers,
247
- )
248
- if resp.status_code == 200:
249
- print(f"{TAG} Registered to Registry")
250
- else:
251
- print(f"{TAG} WARNING: Registry returned {resp.status_code}")
252
- except Exception as e:
253
- print(f"{TAG} WARNING: Registry registration failed: {e}")
254
-
255
-
256
- def _discover_hub(client: httpx.Client, registry_url: str, token: str, timeout: float = 30) -> str | None:
257
- """Poll registry until event_hub is found. Returns ws:// URL or None."""
258
- headers = {"Authorization": f"Bearer {token}"}
259
- deadline = time.time() + timeout
260
- while time.time() < deadline:
261
- try:
262
- resp = client.get(
263
- f"{registry_url}/get/event_hub",
264
- headers=headers,
265
- )
266
- if resp.status_code == 200:
267
- info = resp.json()
268
- ws_url = (info.get("metadata") or {}).get("ws_endpoint")
269
- if ws_url:
270
- return ws_url
271
- except Exception:
272
- pass
273
- time.sleep(0.2)
274
- return None
275
-
276
-
277
- def _get_hub_stats(ws_url: str) -> dict:
278
- """Fetch hub /stats via HTTP (derive from ws:// URL)."""
279
- http_url = ws_url.replace("ws://", "http://").rsplit("/ws", 1)[0]
234
+ def _get_kernel_stats(kernel_port: int) -> dict:
235
+ """Fetch Kernel /stats via HTTP."""
280
236
  try:
281
- resp = httpx.get(f"{http_url}/stats", timeout=5)
237
+ resp = httpx.get(f"http://127.0.0.1:{kernel_port}/stats", timeout=5)
282
238
  if resp.status_code == 200:
283
239
  return resp.json()
284
240
  except Exception:
@@ -286,16 +242,14 @@ def _get_hub_stats(ws_url: str) -> dict:
286
242
  return {}
287
243
 
288
244
 
289
- def _sample_hub_resources(ws_url: str) -> dict:
290
- """Sample hub process CPU/memory via psutil. Returns {} if unavailable."""
245
+ def _sample_kernel_resources(kernel_port: int) -> dict:
246
+ """Sample Kernel process CPU/memory via psutil. Returns {} if unavailable."""
291
247
  try:
292
248
  import psutil
293
249
  except ImportError:
294
250
  return {"error": "psutil not installed"}
295
- http_url = ws_url.replace("ws://", "http://").rsplit("/ws", 1)[0]
296
- port = int(http_url.rsplit(":", 1)[1].split("/")[0])
297
251
  for conn in psutil.net_connections(kind="tcp"):
298
- if conn.laddr.port == port and conn.status == "LISTEN":
252
+ if conn.laddr.port == kernel_port and conn.status == "LISTEN":
299
253
  try:
300
254
  p = psutil.Process(conn.pid)
301
255
  mem = p.memory_info()
@@ -308,13 +262,13 @@ def _sample_hub_resources(ws_url: str) -> dict:
308
262
  }
309
263
  except Exception:
310
264
  pass
311
- return {"error": "hub process not found"}
265
+ return {"error": "kernel process not found"}
312
266
 
313
267
 
314
268
  # ── WebSocket client ──
315
269
 
316
270
  class WsClient:
317
- """Lightweight async WebSocket client for hub benchmarks."""
271
+ """Lightweight async WebSocket client for Kernel benchmarks (JSON-RPC 2.0)."""
318
272
 
319
273
  def __init__(self, ws_url: str, token: str, client_id: str):
320
274
  self.url = f"{ws_url}?token={token}&id={client_id}"
@@ -325,16 +279,18 @@ class WsClient:
325
279
  self.ws = await websockets.connect(self.url, max_size=None)
326
280
 
327
281
  async def subscribe(self, patterns: list[str]):
328
- await self.ws.send(json.dumps({"type": "subscribe", "events": patterns}))
282
+ msg = {"jsonrpc": "2.0", "id": str(uuid.uuid4()), "method": "event.subscribe", "params": {"events": patterns}}
283
+ await self.ws.send(json.dumps(msg))
329
284
 
330
285
  async def publish(self, event_type: str, data: dict) -> str:
331
286
  eid = str(uuid.uuid4())
332
- await self.ws.send(json.dumps({
333
- "type": "event", "event_id": eid, "event": event_type,
334
- "source": self.id,
335
- "timestamp": datetime.now(timezone.utc).isoformat(),
336
- "data": data,
337
- }))
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))
338
294
  return eid
339
295
 
340
296
  async def recv(self, timeout=5.0):
@@ -351,9 +307,10 @@ class WsClient:
351
307
  # ── Benchmark runner ──
352
308
 
353
309
  class BenchRunner:
354
- def __init__(self, ws_url: str, token: str):
310
+ def __init__(self, ws_url: str, token: str, kernel_port: int):
355
311
  self.ws_url = ws_url
356
312
  self.token = token
313
+ self.kernel_port = kernel_port
357
314
  self.results = {}
358
315
 
359
316
  async def _make_client(self, name: str) -> WsClient:
@@ -378,7 +335,9 @@ class BenchRunner:
378
335
  while not stop.is_set():
379
336
  try:
380
337
  raw = await asyncio.wait_for(sub.ws.recv(), timeout=0.5)
381
- if '"event"' in raw and '"ack"' not in raw:
338
+ msg = json.loads(raw)
339
+ # JSON-RPC Notification (event): has method, no id
340
+ if "method" in msg and "id" not in msg:
382
341
  recvd += 1
383
342
  except Exception:
384
343
  pass
@@ -393,7 +352,7 @@ class BenchRunner:
393
352
  recv_task = asyncio.create_task(_recv())
394
353
  ack_task = asyncio.create_task(_drain_acks())
395
354
 
396
- hub_before = (_get_hub_stats(self.ws_url).get("counters") or {})
355
+ hub_before = (_get_kernel_stats(self.kernel_port).get("counters") or {})
397
356
 
398
357
  t0 = time.time()
399
358
  for i in range(n):
@@ -409,7 +368,7 @@ class BenchRunner:
409
368
  await recv_task
410
369
  ack_task.cancel()
411
370
 
412
- hub_after = (_get_hub_stats(self.ws_url).get("counters") or {})
371
+ hub_after = (_get_kernel_stats(self.kernel_port).get("counters") or {})
413
372
 
414
373
  await pub.close()
415
374
  await sub.close()
@@ -440,9 +399,9 @@ class BenchRunner:
440
399
  for i in range(n):
441
400
  t0 = time.time()
442
401
  await pub.publish("bench.lat.test", {"i": i})
443
- await pub.recv(timeout=2) # ack
402
+ await pub.recv(timeout=2) # RPC response
444
403
  msg = await sub.recv(timeout=2)
445
- if msg and msg.get("type") == "event":
404
+ if msg and "method" in msg and "id" not in msg:
446
405
  latencies.append((time.time() - t0) * 1000)
447
406
 
448
407
  await pub.close()
@@ -481,7 +440,8 @@ class BenchRunner:
481
440
  while not stop.is_set():
482
441
  try:
483
442
  raw = await asyncio.wait_for(client.ws.recv(), timeout=0.5)
484
- if '"event"' in raw and '"ack"' not in raw:
443
+ msg = json.loads(raw)
444
+ if "method" in msg and "id" not in msg:
485
445
  counts[idx] += 1
486
446
  except Exception:
487
447
  pass
@@ -527,13 +487,13 @@ class BenchRunner:
527
487
 
528
488
  # ── Save results ──
529
489
 
530
- def save(self, ws_url: str):
490
+ def save(self):
531
491
  os.makedirs(RESULTS_DIR, exist_ok=True)
532
492
  now = datetime.now()
533
493
  filepath = os.path.join(RESULTS_DIR, now.strftime("%Y-%m-%d_%H-%M-%S") + ".json")
534
494
 
535
- hub_stats = _get_hub_stats(ws_url)
536
- resources = _sample_hub_resources(ws_url)
495
+ kernel_stats = _get_kernel_stats(self.kernel_port)
496
+ resources = _sample_kernel_resources(self.kernel_port)
537
497
 
538
498
  data = {
539
499
  "timestamp": now.isoformat(),
@@ -542,7 +502,7 @@ class BenchRunner:
542
502
  "python": platform.python_version(),
543
503
  },
544
504
  "hub_resources": resources,
545
- "hub_counters": hub_stats.get("counters", {}),
505
+ "hub_counters": kernel_stats.get("counters", {}),
546
506
  **self.results,
547
507
  }
548
508
  with open(filepath, "w", encoding="utf-8") as f:
@@ -551,45 +511,60 @@ class BenchRunner:
551
511
 
552
512
 
553
513
  def _send_ready_event(ws_url: str, token: str):
554
- """Send module.ready to Event Hub. Startup phase complete."""
514
+ """Send module.ready to Kernel via JSON-RPC 2.0. Startup phase complete."""
555
515
  try:
556
516
  import websockets.sync.client as ws_sync
557
517
  url = f"{ws_url}?token={token}&id=event_hub_bench"
558
518
  with ws_sync.connect(url, close_timeout=3) as ws:
559
- msg = {
560
- "type": "event",
561
- "event_id": str(uuid.uuid4()),
562
- "event": "module.ready",
563
- "source": "event_hub_bench",
564
- "timestamp": datetime.now(timezone.utc).isoformat(),
565
- "data": {"module_id": "event_hub_bench"},
566
- }
567
- ws.send(json.dumps(msg))
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
+ }))
568
544
  time.sleep(0.1)
569
545
  except Exception as e:
570
546
  print(f"{TAG} WARNING: Could not send module.ready: {e}")
571
547
 
572
548
 
573
549
  def _send_exiting_event(ws_url: str, token: str, reason: str):
574
- """Send module.exiting event to Event Hub before exit. Best-effort, non-blocking."""
550
+ """Send module.exiting event to Kernel via JSON-RPC 2.0 before exit."""
575
551
  try:
576
552
  import websockets.sync.client as ws_sync
577
553
  url = f"{ws_url}?token={token}&id=event_hub_bench"
578
554
  with ws_sync.connect(url, close_timeout=3) as ws:
579
- msg = {
580
- "type": "event",
581
- "event_id": str(uuid.uuid4()),
582
- "event": "module.exiting",
583
- "source": "event_hub_bench",
584
- "timestamp": datetime.now(timezone.utc).isoformat(),
585
- "data": {
586
- "module_id": "event_hub_bench",
587
- "reason": reason,
588
- "action": "none",
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
+ },
589
566
  },
590
- }
591
- ws.send(json.dumps(msg))
592
- # Brief wait for delivery
567
+ }))
593
568
  time.sleep(0.3)
594
569
  except Exception as e:
595
570
  print(f"{TAG} WARNING: Could not send module.exiting: {e}")
@@ -597,12 +572,12 @@ def _send_exiting_event(ws_url: str, token: str, reason: str):
597
572
 
598
573
  # ── Entry point ──
599
574
 
600
- async def _run(ws_url: str, token: str):
601
- bench = BenchRunner(ws_url, token)
575
+ async def _run(ws_url: str, token: str, kernel_port: int):
576
+ bench = BenchRunner(ws_url, token, kernel_port)
602
577
  await bench.bench_throughput()
603
578
  await bench.bench_latency()
604
579
  await bench.bench_fanout()
605
- bench.save(ws_url)
580
+ bench.save()
606
581
 
607
582
 
608
583
  def main():
@@ -617,29 +592,14 @@ def main():
617
592
  pass
618
593
 
619
594
  # Read registry_port from environment variable
620
- registry_port = int(os.environ.get("KITE_REGISTRY_PORT", "0"))
621
-
622
- if not token or not registry_port:
623
- print(f"{TAG} ERROR: Missing token or KITE_REGISTRY_PORT")
624
- sys.exit(1)
625
-
626
- registry_url = f"http://127.0.0.1:{registry_port}"
627
-
628
- client = httpx.Client(timeout=5)
629
-
630
- # Register to Registry first (identity declaration)
631
- _register_to_registry(client, registry_url, token)
632
-
633
- print(f"{TAG} Discovering event_hub via registry...")
634
-
635
- ws_url = _discover_hub(client, registry_url, token)
595
+ kernel_port = int(os.environ.get("KITE_KERNEL_PORT", "0"))
636
596
 
637
- client.close()
638
- if not ws_url:
639
- print(f"{TAG} ERROR: event_hub not found")
597
+ if not token or not kernel_port:
598
+ print(f"{TAG} ERROR: Missing token or KITE_KERNEL_PORT")
640
599
  sys.exit(1)
641
600
 
642
- print(f"{TAG} Hub found: {ws_url}")
601
+ ws_url = f"ws://127.0.0.1:{kernel_port}/ws"
602
+ print(f"{TAG} Kernel at port {kernel_port}")
643
603
 
644
604
  # Debug mode required: multiple benchmark clients share one token
645
605
  if os.environ.get("KITE_DEBUG") != "1":
@@ -653,7 +613,7 @@ def main():
653
613
  # ── Business logic: run benchmarks ──
654
614
  print(f"{TAG} Starting benchmarks...")
655
615
 
656
- asyncio.run(_run(ws_url, token))
616
+ asyncio.run(_run(ws_url, token, kernel_port))
657
617
 
658
618
  print(f"{TAG} Benchmarks complete, sending exit intent...")
659
619
  _send_exiting_event(ws_url, token, "benchmarks complete")