@agentunion/kite 1.3.1 → 1.4.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 (78) hide show
  1. package/CHANGELOG.md +287 -1
  2. package/cli.js +76 -0
  3. package/extensions/agents/assistant/entry.py +111 -1
  4. package/extensions/agents/assistant/server.py +263 -197
  5. package/extensions/channels/acp_channel/entry.py +111 -1
  6. package/extensions/channels/acp_channel/module.md +23 -22
  7. package/extensions/channels/acp_channel/server.py +263 -197
  8. package/extensions/event_hub_bench/entry.py +107 -1
  9. package/extensions/services/backup/entry.py +408 -72
  10. package/extensions/services/backup/module.md +24 -22
  11. package/extensions/services/model_service/entry.py +255 -71
  12. package/extensions/services/model_service/module.md +21 -22
  13. package/extensions/services/watchdog/entry.py +344 -90
  14. package/extensions/services/watchdog/monitor.py +237 -21
  15. package/extensions/services/web/WEBSOCKET_STATUS.md +143 -0
  16. package/extensions/services/web/config_example.py +35 -0
  17. package/extensions/services/web/config_loader.py +110 -0
  18. package/extensions/services/web/entry.py +114 -26
  19. package/extensions/services/web/module.md +35 -24
  20. package/extensions/services/web/pairing.py +250 -0
  21. package/extensions/services/web/pairing_codes.jsonl +16 -0
  22. package/extensions/services/web/relay.py +643 -0
  23. package/extensions/services/web/relay_config.json5 +67 -0
  24. package/extensions/services/web/routes/routes_management_ws.py +127 -0
  25. package/extensions/services/web/routes/routes_rpc.py +89 -0
  26. package/extensions/services/web/routes/routes_test.py +61 -0
  27. package/extensions/services/web/server.py +445 -99
  28. package/extensions/services/web/static/css/style.css +138 -2
  29. package/extensions/services/web/static/index.html +295 -2
  30. package/extensions/services/web/static/js/app.js +1579 -5
  31. package/extensions/services/web/static/js/kernel-client-example.js +161 -0
  32. package/extensions/services/web/static/js/kernel-client.js +383 -0
  33. package/extensions/services/web/static/js/registry-tests.js +558 -0
  34. package/extensions/services/web/static/js/token-manager.js +175 -0
  35. package/extensions/services/web/static/pairing.html +248 -0
  36. package/extensions/services/web/static/test_registry.html +262 -0
  37. package/extensions/services/web/web_config.json5 +29 -0
  38. package/kernel/entry.py +120 -32
  39. package/kernel/event_hub.py +159 -16
  40. package/kernel/module.md +36 -33
  41. package/kernel/registry_store.py +70 -20
  42. package/kernel/rpc_router.py +134 -57
  43. package/kernel/server.py +292 -15
  44. package/kite_cli/__init__.py +3 -0
  45. package/kite_cli/__main__.py +5 -0
  46. package/kite_cli/commands/__init__.py +1 -0
  47. package/kite_cli/commands/clean.py +101 -0
  48. package/kite_cli/commands/doctor.py +35 -0
  49. package/kite_cli/commands/history.py +111 -0
  50. package/kite_cli/commands/info.py +96 -0
  51. package/kite_cli/commands/install.py +313 -0
  52. package/kite_cli/commands/list.py +143 -0
  53. package/kite_cli/commands/log.py +81 -0
  54. package/kite_cli/commands/rollback.py +88 -0
  55. package/kite_cli/commands/search.py +73 -0
  56. package/kite_cli/commands/uninstall.py +85 -0
  57. package/kite_cli/commands/update.py +118 -0
  58. package/kite_cli/core/__init__.py +1 -0
  59. package/kite_cli/core/checker.py +142 -0
  60. package/kite_cli/core/dependency.py +229 -0
  61. package/kite_cli/core/downloader.py +209 -0
  62. package/kite_cli/core/install_info.py +40 -0
  63. package/kite_cli/core/tool_installer.py +397 -0
  64. package/kite_cli/core/validator.py +78 -0
  65. package/kite_cli/main.py +289 -0
  66. package/kite_cli/utils/__init__.py +1 -0
  67. package/kite_cli/utils/i18n.py +252 -0
  68. package/kite_cli/utils/interactive.py +63 -0
  69. package/kite_cli/utils/operation_log.py +77 -0
  70. package/kite_cli/utils/paths.py +34 -0
  71. package/kite_cli/utils/version.py +308 -0
  72. package/launcher/count_lines.py +34 -0
  73. package/launcher/entry.py +905 -166
  74. package/launcher/logging_setup.py +104 -0
  75. package/launcher/module.md +37 -37
  76. package/launcher/process_manager.py +12 -1
  77. package/package.json +2 -1
  78. package/scripts/plan_manager.py +315 -0
@@ -23,7 +23,113 @@ import websockets
23
23
 
24
24
 
25
25
  # ── Module configuration ──
26
- MODULE_NAME = "model_service"
26
+
27
+ def _load_module_config() -> dict:
28
+ """Load module configuration from module.md frontmatter.
29
+
30
+ Returns:
31
+ Dict with keys: name, preferred_port, advertise_ip
32
+
33
+ Raises:
34
+ SystemExit: If module.md is invalid or name is non-compliant
35
+ """
36
+ _this_dir = os.path.dirname(os.path.abspath(__file__))
37
+ module_md = os.path.join(_this_dir, "module.md")
38
+
39
+ # Calculate relative path for error messages
40
+ project_root = os.environ.get("KITE_PROJECT", "")
41
+ if project_root and _this_dir.startswith(project_root):
42
+ rel_path = os.path.relpath(_this_dir, project_root)
43
+ else:
44
+ rel_path = _this_dir
45
+
46
+ # Default values (will be overridden if valid config exists)
47
+ result = {
48
+ "name": "",
49
+ "preferred_port": 0,
50
+ "advertise_ip": "0.0.0.0"
51
+ }
52
+
53
+ # Check if module.md exists
54
+ if not os.path.exists(module_md):
55
+ print(f"[{rel_path}] ERROR: Invalid module configuration in module.md")
56
+ print(f" Path: {rel_path}/module.md")
57
+ print(f" Reason: File not found")
58
+ sys.exit(1)
59
+
60
+ try:
61
+ with open(module_md, encoding="utf-8") as f:
62
+ text = f.read()
63
+
64
+ # Extract YAML frontmatter (between --- markers)
65
+ import re
66
+ m = re.match(r'^---\s*\n(.*?)\n---', text, re.DOTALL)
67
+ if not m:
68
+ print(f"[{rel_path}] ERROR: Invalid module configuration in module.md")
69
+ print(f" Path: {rel_path}/module.md")
70
+ print(f" Reason: Missing YAML frontmatter")
71
+ sys.exit(1)
72
+
73
+ # Parse YAML frontmatter
74
+ try:
75
+ import yaml
76
+ fm = yaml.safe_load(m.group(1)) or {}
77
+ except ImportError:
78
+ print(f"[{rel_path}] ERROR: PyYAML not installed, cannot parse module.md")
79
+ sys.exit(1)
80
+ except Exception as e:
81
+ print(f"[{rel_path}] ERROR: Invalid module configuration in module.md")
82
+ print(f" Path: {rel_path}/module.md")
83
+ print(f" Reason: YAML parse error: {e}")
84
+ sys.exit(1)
85
+
86
+ # Validate 'name' field (required)
87
+ if "name" not in fm:
88
+ print(f"[{rel_path}] ERROR: Invalid module configuration in module.md")
89
+ print(f" Path: {rel_path}/module.md")
90
+ print(f" Reason: Missing 'name' field")
91
+ sys.exit(1)
92
+
93
+ raw_name = str(fm["name"]).strip()
94
+
95
+ if not raw_name:
96
+ print(f"[{rel_path}] ERROR: Invalid module configuration in module.md")
97
+ print(f" Path: {rel_path}/module.md")
98
+ print(f" Reason: Empty module name")
99
+ sys.exit(1)
100
+
101
+ # Validate name characters
102
+ sanitized = re.sub(r'[^a-zA-Z0-9_\-]', '', raw_name)
103
+
104
+ if sanitized != raw_name:
105
+ invalid_chars = ''.join(sorted(set(c for c in raw_name if c not in sanitized)))
106
+ print(f"[{rel_path}] ERROR: Invalid module configuration in module.md")
107
+ print(f" Path: {rel_path}/module.md")
108
+ print(f" Reason: Invalid characters in name '{raw_name}': {repr(invalid_chars)}")
109
+ sys.exit(1)
110
+
111
+ result["name"] = sanitized
112
+
113
+ # Extract optional fields
114
+ if "preferred_port" in fm:
115
+ try:
116
+ result["preferred_port"] = int(fm["preferred_port"])
117
+ except (ValueError, TypeError):
118
+ pass
119
+
120
+ if "advertise_ip" in fm:
121
+ result["advertise_ip"] = str(fm["advertise_ip"])
122
+
123
+ except SystemExit:
124
+ raise # Re-raise exit to prevent catching by outer except
125
+ except Exception as e:
126
+ print(f"[{rel_path}] ERROR: Failed to read module.md: {e}")
127
+ sys.exit(1)
128
+
129
+ return result
130
+
131
+ _module_config = _load_module_config()
132
+ MODULE_NAME = _module_config["name"]
27
133
 
28
134
 
29
135
  class _SafeWriter:
@@ -263,10 +369,20 @@ def _read_stdin_kite_message(expected_type: str, timeout: float = 10) -> dict |
263
369
 
264
370
  # Global WS reference for publish_event callback
265
371
  _ws_global = None
372
+ _shutting_down = False
373
+ _exit_code = 0 # Exit code for main() to use
374
+
375
+
376
+ def _is_auth_failure(e: Exception) -> bool:
377
+ """Check if a WebSocket exception indicates authentication failure."""
378
+ if hasattr(e, 'rcvd') and e.rcvd is not None:
379
+ code = e.rcvd.code if hasattr(e.rcvd, 'code') else 0
380
+ return code in (4001, 4003)
381
+ return False
266
382
 
267
383
 
268
384
  async def main():
269
- global _ws_global
385
+ global _ws_global, _shutting_down
270
386
  # Initialize log file paths
271
387
  global _log_dir, _log_latest_path, _crash_log_path
272
388
  module_data = os.environ.get("KITE_MODULE_DATA")
@@ -318,41 +434,90 @@ async def main():
318
434
 
319
435
  print(f"[model_service] Token received ({len(token)} chars), kernel port: {kernel_port} ({_fmt_elapsed(_t0)})")
320
436
 
321
- # Connect to Kernel WebSocket
437
+ # Start reconnect loop
438
+ await _ws_loop(token, kernel_port, _t0)
439
+
440
+
441
+ async def _ws_loop(token: str, kernel_port: int, _t0: float):
442
+ """Connect to Kernel with exponential backoff reconnection."""
443
+ global _shutting_down, _exit_code
444
+ retry_delay = 0.3
445
+ max_delay = 5.0
446
+ max_retries = 10
447
+ attempt = 0
448
+ while not _shutting_down:
449
+ try:
450
+ await _ws_connect(token, kernel_port, _t0)
451
+ retry_delay = 0.3
452
+ attempt = 0
453
+ except asyncio.CancelledError:
454
+ return
455
+ except Exception as e:
456
+ attempt += 1
457
+ if _is_auth_failure(e):
458
+ print(f"[model_service] Kernel 认证失败,退出")
459
+ _exit_code = 1
460
+ _shutting_down = True
461
+ return
462
+ if attempt >= max_retries:
463
+ print(f"[model_service] 重连失败 {max_retries} 次,退出")
464
+ _exit_code = 1
465
+ _shutting_down = True
466
+ return
467
+ _write_crash(type(e), e, e.__traceback__, severity="error", handled=True)
468
+ print(f"[model_service] 连接错误: {e}, {retry_delay:.1f}s 后重试 ({attempt}/{max_retries})")
469
+ _ws_global_clear()
470
+ if _shutting_down:
471
+ return
472
+ await asyncio.sleep(retry_delay)
473
+ retry_delay = min(retry_delay * 2, max_delay)
474
+
475
+
476
+ def _ws_global_clear():
477
+ global _ws_global
478
+ _ws_global = None
479
+
480
+
481
+ async def _ws_connect(token: str, kernel_port: int, _t0: float):
482
+ """Single WebSocket session: connect → subscribe → register → ready → receive loop."""
483
+ global _ws_global
484
+
322
485
  ws_url = f"ws://127.0.0.1:{kernel_port}/ws?token={token}&id=model_service"
323
486
  print(f"[model_service] Connecting to Kernel: {ws_url}")
324
487
 
325
- try:
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)})")
488
+ async with websockets.connect(ws_url, open_timeout=5, ping_interval=20, ping_timeout=20, close_timeout=10) as ws:
489
+ _ws_global = ws
490
+ print(f"[model_service] Connected to Kernel ({_fmt_elapsed(_t0)})")
491
+
492
+ # Subscribe to events
493
+ await _rpc_call(ws, "event.subscribe", {
494
+ "events": [
495
+ "module.started",
496
+ "module.stopped",
497
+ "module.shutdown",
498
+ ],
499
+ })
500
+ print(f"[model_service] Subscribed to events ({_fmt_elapsed(_t0)})")
501
+
502
+ # Register to Kernel Registry via RPC
503
+ await _rpc_call(ws, "registry.register", {
504
+ "module_id": "model_service",
505
+ "module_type": "service",
506
+ "events_publish": {
507
+ "model_service": {
508
+ "test": {"description": "Test event from model_service module"},
509
+ }
510
+ },
511
+ "events_subscribe": [
512
+ "module.started",
513
+ "module.stopped",
514
+ "module.shutdown",
515
+ ],
516
+ })
517
+ print(f"[model_service] Registered to Kernel ({_fmt_elapsed(_t0)})")
354
518
 
355
- # Publish module.ready
519
+ # Publish module.ready (every reconnect)
520
+ if not _shutting_down:
356
521
  await _rpc_call(ws, "event.publish", {
357
522
  "event_id": str(uuid.uuid4()),
358
523
  "event": "module.ready",
@@ -363,34 +528,34 @@ async def main():
363
528
  })
364
529
  print(f"[model_service] module.ready published ({_fmt_elapsed(_t0)})")
365
530
 
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}")
531
+ # Start test event loop in background
532
+ test_task = asyncio.create_task(_test_event_loop(ws))
389
533
 
390
- except Exception as e:
391
- _write_crash(type(e), e, e.__traceback__, severity="critical", handled=True)
392
- _print_crash_summary(type(e), e.__traceback__)
393
- sys.exit(1)
534
+ # Message loop: handle incoming RPC + events
535
+ # CRITICAL: RPC 死锁防范
536
+ # - 入站 RPC 请求必须用 create_task() 异步执行,不可 await
537
+ # - 原因:如果 handler 内部调用 rpc_call() 发出站请求,出站响应需要本接收循环来分发
538
+ # - 如果接收循环被 await handler 阻塞,出站响应永远收不到 → 超时死锁
539
+ # - 事件通知和 RPC 响应可以同步处理(它们不会反向调用 rpc_call)
540
+ async for raw in ws:
541
+ try:
542
+ msg = json.loads(raw)
543
+ except (json.JSONDecodeError, TypeError):
544
+ continue
545
+
546
+ try:
547
+ has_method = "method" in msg
548
+ has_id = "id" in msg
549
+
550
+ if has_method and not has_id:
551
+ # Event Notification
552
+ await _handle_event_notification(msg)
553
+ elif has_method and has_id:
554
+ # Incoming RPC request — run in background to prevent deadlock
555
+ asyncio.create_task(_handle_rpc_request(ws, msg))
556
+ # Ignore RPC responses (we don't await them in this simple impl)
557
+ except Exception as e:
558
+ print(f"[model_service] 消息处理异常(已忽略): {e}")
394
559
 
395
560
 
396
561
  async def _rpc_call(ws, method: str, params: dict = None):
@@ -416,10 +581,14 @@ async def _handle_event_notification(msg: dict):
416
581
  event_type = params.get("event", "")
417
582
  data = params.get("data", {})
418
583
 
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
584
+ # Special handling for module.shutdown
585
+ if event_type == "module.shutdown":
586
+ target = data.get("module_id", "")
587
+ reason = data.get("reason", "")
588
+ # Handle both targeted shutdown (module_id == "model_service") and broadcast shutdown (no module_id or launcher_lost)
589
+ if target == "model_service" or not target or reason == "launcher_lost":
590
+ await _handle_shutdown()
591
+ return
423
592
 
424
593
  # Log other events
425
594
  print(f"[model_service] Event received: {event_type}")
@@ -472,22 +641,37 @@ async def _rpc_status() -> dict:
472
641
 
473
642
 
474
643
  async def _handle_shutdown():
475
- """Handle module.shutdown event — ack, cleanup, ready, exit."""
644
+ """Handle module.shutdown event — ack → exiting → cleanup ready exit."""
645
+ global _shutting_down
476
646
  print("[model_service] Received shutdown request")
477
- # Step 1: Send ack
647
+ _shutting_down = True
648
+ # Step 1: Send ack (立即确认收到)
478
649
  await _publish_event(_ws_global, {
479
650
  "event": "module.shutdown.ack",
480
- "data": {"module_id": "model_service", "estimated_cleanup": 2},
651
+ "data": {"module_id": "model_service"},
652
+ })
653
+ # Step 2: Send module.exiting (开始清理)
654
+ await _publish_event(_ws_global, {
655
+ "event": "module.exiting",
656
+ "data": {
657
+ "module_id": "model_service",
658
+ "type": "passive",
659
+ "reason": "shutdown_requested",
660
+ "restart": "auto",
661
+ "action": "none",
662
+ "timeout": 2.0,
663
+ "restart_delay": 0.0,
664
+ },
481
665
  })
482
- # Step 2: Cleanup (nothing to clean up for model_service)
483
- # Step 3: Send ready
666
+ # Step 3: Cleanup (nothing to clean up for model_service)
667
+ # Step 4: Send ready (清理完成)
484
668
  await _publish_event(_ws_global, {
485
669
  "event": "module.shutdown.ready",
486
670
  "data": {"module_id": "model_service"},
487
671
  })
488
672
  print("[model_service] Shutdown ready, exiting")
489
- # Step 4: Exit
490
- sys.exit(0)
673
+ # Step 5: Exit
674
+ sys.exit(_exit_code)
491
675
 
492
676
 
493
677
  async def _test_event_loop(ws):
@@ -1,22 +1,21 @@
1
- ---
2
- name: model_service
3
- display_name: Model Service
4
- version: "1.0"
5
- type: service
6
- state: enabled
7
- runtime: python
8
- entry: entry.py
9
- events:
10
- - model_service.test
11
- subscriptions:
12
- - module.started
13
- - module.stopped
14
- - module.shutdown
15
- ---
16
-
17
- # Model Service(大模型服务)
18
-
19
- 大模型服务模块,提供统一的 LLM 调用接口。
20
-
21
- - 模型调用封装多种大模型 API 的统一调用接口
22
- - 事件通知 — 通过 Event Hub 发布模型服务状态事件
1
+ ---
2
+ name: model_service
3
+ display_name: Model Service
4
+ version: '1.0'
5
+ type: service
6
+ state: manual
7
+ runtime: python
8
+ entry: entry.py
9
+ events:
10
+ - model_service.test
11
+ subscriptions:
12
+ - module.started
13
+ - module.stopped
14
+ - module.shutdown
15
+ ---
16
+ # Model Service(大模型服务)
17
+
18
+ 大模型服务模块,提供统一的 LLM 调用接口。
19
+
20
+ - 模型调用 — 封装多种大模型 API 的统一调用接口
21
+ - 事件通知通过 Event Hub 发布模型服务状态事件