@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
@@ -6,15 +6,17 @@ Reads boot_info from stdin, registers to Registry, starts model_service service.
6
6
  import builtins
7
7
  import json
8
8
  import os
9
- import socket
10
9
  import sys
11
10
  import threading
12
11
  import re
13
12
  import time
14
13
  from datetime import datetime, timezone
15
14
 
16
- import httpx
17
- import uvicorn
15
+ import asyncio
16
+ import traceback
17
+ import uuid
18
+
19
+ import websockets
18
20
 
19
21
 
20
22
  # ── Safe stdout/stderr: ignore BrokenPipeError after Launcher closes stdio ──
@@ -228,7 +230,6 @@ _project_root = os.environ.get("KITE_PROJECT") or os.path.dirname(os.path.dirnam
228
230
  if _project_root not in sys.path:
229
231
  sys.path.insert(0, _project_root)
230
232
 
231
- from extensions.services.model_service.server import ModelServiceServer
232
233
 
233
234
 
234
235
  def _fmt_elapsed(t0: float) -> str:
@@ -240,62 +241,32 @@ def _fmt_elapsed(t0: float) -> str:
240
241
  return f"{d:.0f}s"
241
242
 
242
243
 
243
- def _get_free_port() -> int:
244
- with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
245
- s.bind(("127.0.0.1", 0))
246
- return s.getsockname()[1]
247
-
248
-
249
- def _register_to_registry(client: httpx.Client, token: str, registry_url: str, port: int):
250
- payload = {
251
- "action": "register",
252
- "module_id": "model_service",
253
- "module_type": "service",
254
- "name": "Model Service",
255
- "api_endpoint": f"http://127.0.0.1:{port}",
256
- "health_endpoint": "/health",
257
- "events_publish": {
258
- "model_service.test": {"description": "Test event from model_service module"},
259
- },
260
- "events_subscribe": [
261
- "module.started",
262
- "module.stopped",
263
- "module.shutdown",
264
- ],
265
- }
266
- headers = {"Authorization": f"Bearer {token}"}
267
- try:
268
- resp = client.post(f"{registry_url}/modules", json=payload, headers=headers)
269
- if resp.status_code == 200:
270
- pass # timing printed in main()
271
- else:
272
- print(f"[model_service] WARNING: Registry returned {resp.status_code}")
273
- except Exception as e:
274
- print(f"[model_service] WARNING: Registry registration failed: {e}")
275
-
244
+ def _read_stdin_kite_message(expected_type: str, timeout: float = 10) -> dict | None:
245
+ """Read a single kite message of expected type from stdin with timeout."""
246
+ result = [None]
276
247
 
277
- def _get_event_hub_ws(client: httpx.Client, token: str, registry_url: str) -> str:
278
- """Discover Event Hub WebSocket endpoint from Registry, with retry."""
279
- import time
280
- headers = {"Authorization": f"Bearer {token}"}
281
- deadline = time.time() + 10
282
- while time.time() < deadline:
248
+ def _read():
283
249
  try:
284
- resp = client.get(
285
- f"{registry_url}/get/event_hub.metadata.ws_endpoint",
286
- headers=headers,
287
- )
288
- if resp.status_code == 200:
289
- val = resp.json()
290
- if val:
291
- return val
250
+ line = sys.stdin.readline().strip()
251
+ if line:
252
+ msg = json.loads(line)
253
+ if isinstance(msg, dict) and msg.get("kite") == expected_type:
254
+ result[0] = msg
292
255
  except Exception:
293
256
  pass
294
- time.sleep(0.2)
295
- return ""
257
+
258
+ t = threading.Thread(target=_read, daemon=True)
259
+ t.start()
260
+ t.join(timeout=timeout)
261
+ return result[0]
296
262
 
297
263
 
298
- def main():
264
+ # Global WS reference for publish_event callback
265
+ _ws_global = None
266
+
267
+
268
+ async def main():
269
+ global _ws_global
299
270
  # Initialize log file paths
300
271
  global _log_dir, _log_latest_path, _crash_log_path
301
272
  module_data = os.environ.get("KITE_MODULE_DATA")
@@ -334,47 +305,204 @@ def main():
334
305
  except Exception:
335
306
  pass
336
307
 
337
- # Read registry_port from environment variable
338
- registry_port = int(os.environ.get("KITE_REGISTRY_PORT", "0"))
308
+ # Read kernel_port: env first (fast path), stdin fallback (parallel start)
309
+ kernel_port = int(os.environ.get("KITE_KERNEL_PORT", "0"))
310
+ if not kernel_port:
311
+ msg = _read_stdin_kite_message("kernel_port", timeout=10)
312
+ if msg:
313
+ kernel_port = int(msg.get("kernel_port", 0))
339
314
 
340
- if not token or not registry_port:
341
- print("[model_service] ERROR: Missing token or KITE_REGISTRY_PORT")
315
+ if not token or not kernel_port:
316
+ print("[model_service] ERROR: Missing token or kernel_port")
342
317
  sys.exit(1)
343
318
 
344
- print(f"[model_service] Token received ({len(token)} chars), registry port: {registry_port} ({_fmt_elapsed(_t0)})")
345
-
346
- registry_url = f"http://127.0.0.1:{registry_port}"
347
- port = _get_free_port()
348
-
349
- # Register and discover Event Hub synchronously before starting uvicorn
350
- client = httpx.Client(timeout=5)
351
- _register_to_registry(client, token, registry_url, port)
352
- print(f"[model_service] Registered to Registry ({_fmt_elapsed(_t0)})")
353
- event_hub_ws = _get_event_hub_ws(client, token, registry_url)
354
- if not event_hub_ws:
355
- print("[model_service] WARNING: Could not discover Event Hub WS, events disabled")
356
- else:
357
- print(f"[model_service] Discovered Event Hub: {event_hub_ws}")
358
- client.close()
319
+ print(f"[model_service] Token received ({len(token)} chars), kernel port: {kernel_port} ({_fmt_elapsed(_t0)})")
359
320
 
360
- server = ModelServiceServer(
361
- token=token,
362
- registry_url=registry_url,
363
- event_hub_ws=event_hub_ws,
364
- boot_t0=_t0,
365
- )
321
+ # Connect to Kernel WebSocket
322
+ ws_url = f"ws://127.0.0.1:{kernel_port}/ws?token={token}&id=model_service"
323
+ print(f"[model_service] Connecting to Kernel: {ws_url}")
366
324
 
367
- print(f"[model_service] Starting on port {port} ({_fmt_elapsed(_t0)})")
368
325
  try:
369
- config = uvicorn.Config(server.app, host="127.0.0.1", port=port, log_level="warning")
370
- uvi_server = uvicorn.Server(config)
371
- server._uvicorn_server = uvi_server
372
- uvi_server.run()
326
+ async with websockets.connect(ws_url, open_timeout=5, ping_interval=None, ping_timeout=None, close_timeout=10) as ws:
327
+ _ws_global = ws
328
+ print(f"[model_service] Connected to Kernel ({_fmt_elapsed(_t0)})")
329
+
330
+ # Subscribe to events
331
+ await _rpc_call(ws, "event.subscribe", {
332
+ "events": [
333
+ "module.started",
334
+ "module.stopped",
335
+ "module.shutdown",
336
+ ],
337
+ })
338
+ print(f"[model_service] Subscribed to events ({_fmt_elapsed(_t0)})")
339
+
340
+ # Register to Kernel Registry via RPC
341
+ await _rpc_call(ws, "registry.register", {
342
+ "module_id": "model_service",
343
+ "module_type": "service",
344
+ "events_publish": {
345
+ "model_service.test": {"description": "Test event from model_service module"},
346
+ },
347
+ "events_subscribe": [
348
+ "module.started",
349
+ "module.stopped",
350
+ "module.shutdown",
351
+ ],
352
+ })
353
+ print(f"[model_service] Registered to Kernel ({_fmt_elapsed(_t0)})")
354
+
355
+ # Publish module.ready
356
+ await _rpc_call(ws, "event.publish", {
357
+ "event_id": str(uuid.uuid4()),
358
+ "event": "module.ready",
359
+ "data": {
360
+ "module_id": "model_service",
361
+ "graceful_shutdown": True,
362
+ },
363
+ })
364
+ print(f"[model_service] module.ready published ({_fmt_elapsed(_t0)})")
365
+
366
+ # Start test event loop in background
367
+ test_task = asyncio.create_task(_test_event_loop(ws))
368
+
369
+ # Message loop: handle incoming RPC + events
370
+ async for raw in ws:
371
+ try:
372
+ msg = json.loads(raw)
373
+ except (json.JSONDecodeError, TypeError):
374
+ continue
375
+
376
+ try:
377
+ has_method = "method" in msg
378
+ has_id = "id" in msg
379
+
380
+ if has_method and not has_id:
381
+ # Event Notification
382
+ await _handle_event_notification(msg)
383
+ elif has_method and has_id:
384
+ # Incoming RPC request
385
+ await _handle_rpc_request(ws, msg)
386
+ # Ignore RPC responses (we don't await them in this simple impl)
387
+ except Exception as e:
388
+ print(f"[model_service] 消息处理异常(已忽略): {e}")
389
+
373
390
  except Exception as e:
374
391
  _write_crash(type(e), e, e.__traceback__, severity="critical", handled=True)
375
392
  _print_crash_summary(type(e), e.__traceback__)
376
393
  sys.exit(1)
377
394
 
378
395
 
396
+ async def _rpc_call(ws, method: str, params: dict = None):
397
+ """Send a JSON-RPC 2.0 request (fire-and-forget, no response awaited)."""
398
+ msg = {"jsonrpc": "2.0", "id": str(uuid.uuid4()), "method": method}
399
+ if params:
400
+ msg["params"] = params
401
+ await ws.send(json.dumps(msg))
402
+
403
+
404
+ async def _publish_event(ws, event: dict):
405
+ """Publish an event via RPC event.publish."""
406
+ await _rpc_call(ws, "event.publish", {
407
+ "event_id": str(uuid.uuid4()),
408
+ "event": event.get("event", ""),
409
+ "data": event.get("data", {}),
410
+ })
411
+
412
+
413
+ async def _handle_event_notification(msg: dict):
414
+ """Handle an event notification (JSON-RPC 2.0 Notification with method='event')."""
415
+ params = msg.get("params", {})
416
+ event_type = params.get("event", "")
417
+ data = params.get("data", {})
418
+
419
+ # Special handling for module.shutdown targeting model_service
420
+ if event_type == "module.shutdown" and data.get("module_id") == "model_service":
421
+ await _handle_shutdown()
422
+ return
423
+
424
+ # Log other events
425
+ print(f"[model_service] Event received: {event_type}")
426
+
427
+
428
+ async def _handle_rpc_request(ws, msg: dict):
429
+ """Handle an incoming RPC request (model_service.* methods)."""
430
+ rpc_id = msg.get("id", "")
431
+ method = msg.get("method", "")
432
+ params = msg.get("params", {})
433
+
434
+ handlers = {
435
+ "health": lambda p: _rpc_health(),
436
+ "status": lambda p: _rpc_status(),
437
+ }
438
+ handler = handlers.get(method)
439
+ if handler:
440
+ try:
441
+ result = await handler(params)
442
+ await ws.send(json.dumps({"jsonrpc": "2.0", "id": rpc_id, "result": result}))
443
+ except Exception as e:
444
+ await ws.send(json.dumps({
445
+ "jsonrpc": "2.0", "id": rpc_id,
446
+ "error": {"code": -32603, "message": str(e)},
447
+ }))
448
+ else:
449
+ await ws.send(json.dumps({
450
+ "jsonrpc": "2.0", "id": rpc_id,
451
+ "error": {"code": -32601, "message": f"Method not found: {method}"},
452
+ }))
453
+
454
+
455
+ async def _rpc_health() -> dict:
456
+ """RPC handler for model_service.health."""
457
+ return {
458
+ "status": "healthy",
459
+ "details": {
460
+ "uptime_seconds": round(time.time() - _start_ts),
461
+ },
462
+ }
463
+
464
+
465
+ async def _rpc_status() -> dict:
466
+ """RPC handler for model_service.status."""
467
+ return {
468
+ "module": "model_service",
469
+ "status": "running",
470
+ "uptime_seconds": round(time.time() - _start_ts),
471
+ }
472
+
473
+
474
+ async def _handle_shutdown():
475
+ """Handle module.shutdown event — ack, cleanup, ready, exit."""
476
+ print("[model_service] Received shutdown request")
477
+ # Step 1: Send ack
478
+ await _publish_event(_ws_global, {
479
+ "event": "module.shutdown.ack",
480
+ "data": {"module_id": "model_service", "estimated_cleanup": 2},
481
+ })
482
+ # Step 2: Cleanup (nothing to clean up for model_service)
483
+ # Step 3: Send ready
484
+ await _publish_event(_ws_global, {
485
+ "event": "module.shutdown.ready",
486
+ "data": {"module_id": "model_service"},
487
+ })
488
+ print("[model_service] Shutdown ready, exiting")
489
+ # Step 4: Exit
490
+ sys.exit(0)
491
+
492
+
493
+ async def _test_event_loop(ws):
494
+ """Publish a test event every 10 seconds."""
495
+ while True:
496
+ await asyncio.sleep(10)
497
+ await _publish_event(ws, {
498
+ "event": "model_service.test",
499
+ "data": {
500
+ "message": "test event from model_service",
501
+ "timestamp": datetime.now(timezone.utc).isoformat(),
502
+ },
503
+ })
504
+ print("[model_service] test event published")
505
+
506
+
379
507
  if __name__ == "__main__":
380
- main()
508
+ asyncio.run(main())