@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,436 +0,0 @@
1
- """
2
- Event Hub entry point.
3
- Reads token from stdin boot_info, then reads launcher_ws_token + registry_port from
4
- stdin kite messages (in any order), starts FastAPI server, outputs ws_endpoint via
5
- stdout, waits for Launcher to connect, then registers to Registry after stdio disconnect.
6
- """
7
-
8
- import builtins
9
- import json
10
- import os
11
- import re
12
- import socket
13
- import sys
14
- import threading
15
- import time
16
- import traceback
17
- from datetime import datetime, timezone
18
-
19
- import uvicorn
20
-
21
-
22
- # ── Module configuration ──
23
- MODULE_NAME = "event_hub"
24
-
25
-
26
- def _fmt_elapsed(t0: float) -> str:
27
- """Format elapsed time since t0: <1s → 'NNNms', >=1s → 'N.Ns', >=10s → 'NNs'."""
28
- d = time.monotonic() - t0
29
- if d < 1:
30
- return f"{d * 1000:.0f}ms"
31
- if d < 10:
32
- return f"{d:.1f}s"
33
- return f"{d:.0f}s"
34
-
35
-
36
- # ── Safe stdout/stderr: ignore BrokenPipeError after Launcher closes stdio ──
37
-
38
- class _SafeWriter:
39
- """Wraps a stream to silently swallow BrokenPipeError on write/flush."""
40
- def __init__(self, stream):
41
- self._stream = stream
42
-
43
- def write(self, s):
44
- try:
45
- self._stream.write(s)
46
- except (BrokenPipeError, OSError):
47
- pass
48
-
49
- def flush(self):
50
- try:
51
- self._stream.flush()
52
- except (BrokenPipeError, OSError):
53
- pass
54
-
55
- def __getattr__(self, name):
56
- return getattr(self._stream, name)
57
-
58
- sys.stdout = _SafeWriter(sys.stdout)
59
- sys.stderr = _SafeWriter(sys.stderr)
60
-
61
-
62
- # ── Timestamped print + log file writer ──
63
- # Independent implementation per module (no shared code dependency)
64
-
65
- _builtin_print = builtins.print
66
- _start_ts = time.monotonic()
67
- _last_ts = time.monotonic()
68
- _ANSI_RE = re.compile(r"\033\[[0-9;]*m")
69
- _log_lock = threading.Lock()
70
- _log_latest_path = None
71
- _log_daily_path = None
72
- _log_daily_date = ""
73
- _log_dir = None
74
- _crash_log_path = None
75
-
76
- def _strip_ansi(s: str) -> str:
77
- return _ANSI_RE.sub("", s)
78
-
79
- def _resolve_daily_log_path():
80
- """Resolve daily log path based on current date."""
81
- global _log_daily_path, _log_daily_date
82
- if not _log_dir:
83
- return
84
- today = datetime.now().strftime("%Y-%m-%d")
85
- if today == _log_daily_date and _log_daily_path:
86
- return
87
- month_dir = os.path.join(_log_dir, today[:7])
88
- os.makedirs(month_dir, exist_ok=True)
89
- _log_daily_path = os.path.join(month_dir, f"{today}.log")
90
- _log_daily_date = today
91
-
92
- def _write_log(plain_line: str):
93
- """Write a plain-text line to both latest.log and daily log."""
94
- with _log_lock:
95
- if _log_latest_path:
96
- try:
97
- with open(_log_latest_path, "a", encoding="utf-8") as f:
98
- f.write(plain_line)
99
- except Exception:
100
- pass
101
- _resolve_daily_log_path()
102
- if _log_daily_path:
103
- try:
104
- with open(_log_daily_path, "a", encoding="utf-8") as f:
105
- f.write(plain_line)
106
- except Exception:
107
- pass
108
-
109
- def _write_crash(exc_type, exc_value, exc_tb, thread_name=None, severity="critical", handled=False):
110
- """Write crash record to crashes.jsonl + daily crash archive."""
111
- record = {
112
- "timestamp": datetime.now(timezone.utc).isoformat(),
113
- "module": MODULE_NAME,
114
- "thread": thread_name or threading.current_thread().name,
115
- "exception_type": exc_type.__name__ if exc_type else "Unknown",
116
- "exception_message": str(exc_value),
117
- "traceback": "".join(traceback.format_exception(exc_type, exc_value, exc_tb)),
118
- "severity": severity,
119
- "handled": handled,
120
- "process_id": os.getpid(),
121
- "platform": sys.platform,
122
- "runtime_version": f"Python {sys.version.split()[0]}",
123
- }
124
-
125
- if exc_tb:
126
- tb_entries = traceback.extract_tb(exc_tb)
127
- if tb_entries:
128
- last = tb_entries[-1]
129
- record["context"] = {
130
- "function": last.name,
131
- "file": os.path.basename(last.filename),
132
- "line": last.lineno,
133
- }
134
-
135
- line = json.dumps(record, ensure_ascii=False) + "\n"
136
-
137
- if _crash_log_path:
138
- try:
139
- with open(_crash_log_path, "a", encoding="utf-8") as f:
140
- f.write(line)
141
- except Exception:
142
- pass
143
-
144
- if _log_dir:
145
- try:
146
- today = datetime.now().strftime("%Y-%m-%d")
147
- archive_dir = os.path.join(_log_dir, "crashes", today[:7])
148
- os.makedirs(archive_dir, exist_ok=True)
149
- archive_path = os.path.join(archive_dir, f"{today}.jsonl")
150
- with open(archive_path, "a", encoding="utf-8") as f:
151
- f.write(line)
152
- except Exception:
153
- pass
154
-
155
- def _print_crash_summary(exc_type, exc_tb, thread_name=None):
156
- """Print crash summary to console (red highlight)."""
157
- RED = "\033[91m"
158
- RESET = "\033[0m"
159
-
160
- if exc_tb:
161
- tb_entries = traceback.extract_tb(exc_tb)
162
- if tb_entries:
163
- last = tb_entries[-1]
164
- location = f"{os.path.basename(last.filename)}:{last.lineno}"
165
- else:
166
- location = "unknown"
167
- else:
168
- location = "unknown"
169
-
170
- prefix = f"[{MODULE_NAME}]"
171
- if thread_name:
172
- _builtin_print(f"{prefix} {RED}线程 {thread_name} 崩溃: "
173
- f"{exc_type.__name__} in {location}{RESET}")
174
- else:
175
- _builtin_print(f"{prefix} {RED}崩溃: {exc_type.__name__} in {location}{RESET}")
176
- if _crash_log_path:
177
- _builtin_print(f"{prefix} 崩溃日志: {_crash_log_path}")
178
-
179
- def _setup_exception_hooks():
180
- """Set up global exception hooks."""
181
- _orig_excepthook = sys.excepthook
182
-
183
- def _excepthook(exc_type, exc_value, exc_tb):
184
- _write_crash(exc_type, exc_value, exc_tb, severity="critical", handled=False)
185
- _print_crash_summary(exc_type, exc_tb)
186
- _orig_excepthook(exc_type, exc_value, exc_tb)
187
-
188
- sys.excepthook = _excepthook
189
-
190
- if hasattr(threading, "excepthook"):
191
- def _thread_excepthook(args):
192
- _write_crash(args.exc_type, args.exc_value, args.exc_traceback,
193
- thread_name=args.thread.name if args.thread else "unknown",
194
- severity="error", handled=False)
195
- _print_crash_summary(args.exc_type, args.exc_traceback,
196
- thread_name=args.thread.name if args.thread else None)
197
-
198
- threading.excepthook = _thread_excepthook
199
-
200
- def _tprint(*args, **kwargs):
201
- """Timestamped print that adds [timestamp] HH:MM:SS.mmm +delta prefix."""
202
- global _last_ts
203
- now = time.monotonic()
204
- elapsed = now - _start_ts
205
- delta = now - _last_ts
206
- _last_ts = now
207
-
208
- if elapsed < 1:
209
- elapsed_str = f"{elapsed * 1000:.0f}ms"
210
- elif elapsed < 100:
211
- elapsed_str = f"{elapsed:.1f}s"
212
- else:
213
- elapsed_str = f"{elapsed:.0f}s"
214
-
215
- if delta < 0.001:
216
- delta_str = ""
217
- elif delta < 1:
218
- delta_str = f"+{delta * 1000:.0f}ms"
219
- elif delta < 100:
220
- delta_str = f"+{delta:.1f}s"
221
- else:
222
- delta_str = f"+{delta:.0f}s"
223
-
224
- ts = datetime.now().strftime("%H:%M:%S.%f")[:-3]
225
-
226
- _builtin_print(*args, **kwargs)
227
-
228
- if _log_latest_path or _log_daily_path:
229
- sep = kwargs.get("sep", " ")
230
- end = kwargs.get("end", "\n")
231
- text = sep.join(str(a) for a in args)
232
- prefix = f"[{elapsed_str:>6}] {ts} {delta_str:>8} "
233
- _write_log(prefix + _strip_ansi(text) + end)
234
-
235
- builtins.print = _tprint
236
-
237
- # Ensure project root is on sys.path
238
- _this_dir = os.path.dirname(os.path.abspath(__file__))
239
- _project_root = os.environ.get("KITE_PROJECT") or os.path.dirname(os.path.dirname(_this_dir))
240
- if _project_root not in sys.path:
241
- sys.path.insert(0, _project_root)
242
-
243
- from core.event_hub.hub import EventHub
244
- from core.event_hub.server import EventHubServer
245
-
246
-
247
- def _read_module_md() -> dict:
248
- """Read preferred_port, advertise_ip from own module.md."""
249
- md_path = os.path.join(_this_dir, "module.md")
250
- result = {"preferred_port": 0, "advertise_ip": "127.0.0.1"}
251
- try:
252
- with open(md_path, encoding="utf-8") as f:
253
- text = f.read()
254
- m = re.match(r'^---\s*\n(.*?)\n---', text, re.DOTALL)
255
- if m:
256
- try:
257
- import yaml
258
- fm = yaml.safe_load(m.group(1)) or {}
259
- except ImportError:
260
- fm = {}
261
- result["preferred_port"] = int(fm.get("preferred_port", 0))
262
- result["advertise_ip"] = fm.get("advertise_ip", "127.0.0.1")
263
- except Exception:
264
- pass
265
- return result
266
-
267
-
268
- def _bind_port(preferred: int, host: str) -> int:
269
- """Try preferred port first, fall back to OS-assigned."""
270
- if preferred:
271
- try:
272
- with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
273
- s.bind((host, preferred))
274
- return preferred
275
- except OSError:
276
- pass
277
- with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
278
- s.bind((host, 0))
279
- return s.getsockname()[1]
280
-
281
-
282
- def _read_stdin_kite_messages(expected: set[str], timeout: float = 10) -> dict[str, dict]:
283
- """Read multiple structured kite messages from stdin until all expected types received.
284
-
285
- Args:
286
- expected: set of kite message types to wait for (e.g. {"launcher_ws_token", "registry_port"})
287
- timeout: total timeout in seconds
288
-
289
- Returns:
290
- dict mapping kite type -> message dict. Missing types are absent from the result.
291
- """
292
- collected: dict[str, dict] = {}
293
- remaining = set(expected)
294
-
295
- def _read():
296
- while remaining:
297
- try:
298
- line = sys.stdin.readline().strip()
299
- if not line:
300
- return # stdin closed
301
- msg = json.loads(line)
302
- if isinstance(msg, dict) and "kite" in msg:
303
- kite_type = msg["kite"]
304
- collected[kite_type] = msg
305
- remaining.discard(kite_type)
306
- except Exception:
307
- return # parse error or stdin closed
308
-
309
- t = threading.Thread(target=_read, daemon=True)
310
- t.start()
311
- t.join(timeout=timeout)
312
- return collected
313
-
314
-
315
- def main():
316
- # Initialize log file paths
317
- global _log_dir, _log_latest_path, _crash_log_path
318
- module_data = os.environ.get("KITE_MODULE_DATA")
319
- if module_data:
320
- _log_dir = os.path.join(module_data, "log")
321
- os.makedirs(_log_dir, exist_ok=True)
322
- suffix = os.environ.get("KITE_INSTANCE_SUFFIX", "")
323
-
324
- _log_latest_path = os.path.join(_log_dir, f"latest{suffix}.log")
325
- try:
326
- with open(_log_latest_path, "w", encoding="utf-8") as f:
327
- pass
328
- except Exception:
329
- _log_latest_path = None
330
-
331
- _crash_log_path = os.path.join(_log_dir, f"crashes{suffix}.jsonl")
332
- try:
333
- with open(_crash_log_path, "w", encoding="utf-8") as f:
334
- pass
335
- except Exception:
336
- _crash_log_path = None
337
-
338
- _resolve_daily_log_path()
339
-
340
- _setup_exception_hooks()
341
-
342
- _t0 = time.monotonic()
343
-
344
- # Kite environment
345
- kite_instance = os.environ.get("KITE_INSTANCE", "")
346
- is_debug = os.environ.get("KITE_DEBUG") == "1"
347
-
348
- # Step 1: Read token from stdin boot_info
349
- token = ""
350
- try:
351
- line = sys.stdin.readline().strip()
352
- if line:
353
- boot_info = json.loads(line)
354
- token = boot_info.get("token", "")
355
- except Exception:
356
- pass
357
-
358
- if not token:
359
- print("[event_hub] 错误: boot_info 中缺少令牌")
360
- sys.exit(1)
361
-
362
- # Step 2: Check env for registry_port first (fast path for restart / Phase 3.5+ scenarios)
363
- registry_port = int(os.environ.get("KITE_REGISTRY_PORT", "0"))
364
-
365
- # Determine which stdin messages to wait for
366
- stdin_expected = {"launcher_ws_token"}
367
- if not registry_port:
368
- stdin_expected.add("registry_port")
369
-
370
- kite_msgs = _read_stdin_kite_messages(stdin_expected, timeout=10)
371
-
372
- launcher_ws_token = ""
373
- ws_msg = kite_msgs.get("launcher_ws_token")
374
- if ws_msg:
375
- launcher_ws_token = ws_msg.get("launcher_ws_token", "")
376
-
377
- if launcher_ws_token:
378
- print(f"[event_hub] 已收到启动器 WS 令牌 ({len(launcher_ws_token)} 字符)")
379
- else:
380
- print("[event_hub] 警告: 未收到 launcher_ws_token,启动器引导认证已禁用")
381
-
382
- # Step 3: registry_port — env already had it, or read from stdin
383
- if not registry_port:
384
- port_msg = kite_msgs.get("registry_port")
385
- if port_msg:
386
- registry_port = int(port_msg.get("registry_port", 0))
387
- if not registry_port:
388
- print("[event_hub] 错误: 未收到 registry_port(stdin 和环境变量均无)")
389
- sys.exit(1)
390
-
391
- print(f"[event_hub] 已收到令牌 ({len(token)} 字符),Registry 端口: {registry_port} ({_fmt_elapsed(_t0)})")
392
-
393
- # Step 4: Read config from own module.md
394
- md_config = _read_module_md()
395
- advertise_ip = md_config["advertise_ip"]
396
- preferred_port = md_config["preferred_port"]
397
-
398
- # Step 5: Bind port and create server
399
- bind_host = advertise_ip
400
- port = _bind_port(preferred_port, bind_host)
401
- registry_url = f"http://127.0.0.1:{registry_port}"
402
-
403
- if is_debug:
404
- print("[event_hub] 调试模式已启用 (KITE_DEBUG=1),接受所有令牌")
405
-
406
- hub = EventHub()
407
- server = EventHubServer(
408
- hub,
409
- own_token=token,
410
- registry_url=registry_url,
411
- launcher_ws_token=launcher_ws_token,
412
- advertise_ip=advertise_ip,
413
- port=port,
414
- )
415
-
416
- # Step 6: Output ws_endpoint via stdout (Launcher reads this)
417
- ws_endpoint = f"ws://{advertise_ip}:{port}/ws"
418
- print(json.dumps({"kite": "ws_endpoint", "ws_endpoint": ws_endpoint}), flush=True)
419
-
420
- # Step 7: Start HTTP + WS server
421
- # Launcher will connect with launcher_ws_token → Event Hub sends module.ready → stdio disconnect
422
- # After stdio disconnect, Event Hub registers to Registry (done by server on_launcher_connected callback)
423
- print(f"[event_hub] 启动中 {bind_host}:{port} ({_fmt_elapsed(_t0)})")
424
- try:
425
- config = uvicorn.Config(server.app, host=bind_host, port=port, log_level="warning")
426
- uvi_server = uvicorn.Server(config)
427
- server._uvicorn_server = uvi_server
428
- uvi_server.run()
429
- except Exception as e:
430
- _write_crash(type(e), e, e.__traceback__, severity="critical", handled=True)
431
- _print_crash_summary(type(e), e.__traceback__)
432
- sys.exit(1)
433
-
434
-
435
- if __name__ == "__main__":
436
- main()
@@ -1,20 +0,0 @@
1
- ---
2
- name: event_hub
3
- display_name: Event Hub
4
- version: "1.0"
5
- type: infrastructure
6
- state: enabled
7
- runtime: python
8
- entry: entry.py
9
- events: []
10
- subscriptions: []
11
- ---
12
-
13
- # Event Hub
14
-
15
- Kite 系统的实时事件路由器。
16
-
17
- - 接收模块通过 WebSocket 发送的事件
18
- - 根据订阅关系(支持 NATS 风格通配符)转发给匹配的模块
19
- - 事件去重(1h 滑动窗口)防止 outbox 重放导致重复转发
20
- - ACK 确认机制,模块据此清理本地 outbox