@agentunion/kite 1.0.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 (69) hide show
  1. package/__init__.py +1 -0
  2. package/__main__.py +15 -0
  3. package/cli.js +70 -0
  4. package/core/__init__.py +0 -0
  5. package/core/__pycache__/__init__.cpython-313.pyc +0 -0
  6. package/core/event_hub/BENCHMARK.md +94 -0
  7. package/core/event_hub/__init__.py +0 -0
  8. package/core/event_hub/__pycache__/__init__.cpython-313.pyc +0 -0
  9. package/core/event_hub/__pycache__/bench.cpython-313.pyc +0 -0
  10. package/core/event_hub/__pycache__/bench_perf.cpython-313.pyc +0 -0
  11. package/core/event_hub/__pycache__/dedup.cpython-313.pyc +0 -0
  12. package/core/event_hub/__pycache__/entry.cpython-313.pyc +0 -0
  13. package/core/event_hub/__pycache__/hub.cpython-313.pyc +0 -0
  14. package/core/event_hub/__pycache__/router.cpython-313.pyc +0 -0
  15. package/core/event_hub/__pycache__/server.cpython-313.pyc +0 -0
  16. package/core/event_hub/bench.py +459 -0
  17. package/core/event_hub/bench_extreme.py +308 -0
  18. package/core/event_hub/bench_perf.py +350 -0
  19. package/core/event_hub/bench_results/.gitkeep +0 -0
  20. package/core/event_hub/bench_results/2026-02-28_13-26-48.json +51 -0
  21. package/core/event_hub/bench_results/2026-02-28_13-44-45.json +51 -0
  22. package/core/event_hub/bench_results/2026-02-28_13-45-39.json +51 -0
  23. package/core/event_hub/dedup.py +31 -0
  24. package/core/event_hub/entry.py +113 -0
  25. package/core/event_hub/hub.py +263 -0
  26. package/core/event_hub/module.md +21 -0
  27. package/core/event_hub/router.py +21 -0
  28. package/core/event_hub/server.py +138 -0
  29. package/core/event_hub_bench/entry.py +371 -0
  30. package/core/event_hub_bench/module.md +25 -0
  31. package/core/launcher/__init__.py +0 -0
  32. package/core/launcher/__pycache__/__init__.cpython-313.pyc +0 -0
  33. package/core/launcher/__pycache__/entry.cpython-313.pyc +0 -0
  34. package/core/launcher/__pycache__/module_scanner.cpython-313.pyc +0 -0
  35. package/core/launcher/__pycache__/process_manager.cpython-313.pyc +0 -0
  36. package/core/launcher/data/log/lifecycle.jsonl +1045 -0
  37. package/core/launcher/data/processes_14752.json +32 -0
  38. package/core/launcher/data/token.txt +1 -0
  39. package/core/launcher/entry.py +965 -0
  40. package/core/launcher/module.md +37 -0
  41. package/core/launcher/module_scanner.py +253 -0
  42. package/core/launcher/process_manager.py +435 -0
  43. package/core/registry/__init__.py +0 -0
  44. package/core/registry/__pycache__/__init__.cpython-313.pyc +0 -0
  45. package/core/registry/__pycache__/entry.cpython-313.pyc +0 -0
  46. package/core/registry/__pycache__/server.cpython-313.pyc +0 -0
  47. package/core/registry/__pycache__/store.cpython-313.pyc +0 -0
  48. package/core/registry/data/port.txt +1 -0
  49. package/core/registry/data/port_14752.txt +1 -0
  50. package/core/registry/data/port_484.txt +1 -0
  51. package/core/registry/entry.py +73 -0
  52. package/core/registry/module.md +30 -0
  53. package/core/registry/server.py +256 -0
  54. package/core/registry/store.py +232 -0
  55. package/extensions/__init__.py +0 -0
  56. package/extensions/__pycache__/__init__.cpython-313.pyc +0 -0
  57. package/extensions/services/__init__.py +0 -0
  58. package/extensions/services/__pycache__/__init__.cpython-313.pyc +0 -0
  59. package/extensions/services/watchdog/__init__.py +0 -0
  60. package/extensions/services/watchdog/__pycache__/__init__.cpython-313.pyc +0 -0
  61. package/extensions/services/watchdog/__pycache__/entry.cpython-313.pyc +0 -0
  62. package/extensions/services/watchdog/__pycache__/monitor.cpython-313.pyc +0 -0
  63. package/extensions/services/watchdog/__pycache__/server.cpython-313.pyc +0 -0
  64. package/extensions/services/watchdog/entry.py +143 -0
  65. package/extensions/services/watchdog/module.md +25 -0
  66. package/extensions/services/watchdog/monitor.py +420 -0
  67. package/extensions/services/watchdog/server.py +167 -0
  68. package/main.py +17 -0
  69. package/package.json +27 -0
@@ -0,0 +1,37 @@
1
+ ---
2
+ name: launcher
3
+ display_name: "Launcher"
4
+ version: "1.0"
5
+ type: infrastructure
6
+ state: enabled
7
+ events:
8
+ - module.started
9
+ - module.stopped
10
+ - module.state_changed
11
+ subscriptions: []
12
+ discovery:
13
+ test_modules:
14
+ type: scan_dir
15
+ path: test_modules
16
+ enabled: true
17
+ ---
18
+
19
+ # Launcher
20
+
21
+ Kite 系统的启动器和进程管理器。与 main.py 在同一进程中运行,是整个系统的入口和控制台。
22
+
23
+ ## 职责
24
+
25
+ - 启动 Registry,等待就绪
26
+ - 扫描 core/ 和 extensions/ 下的模块
27
+ - 并行启动 state: enabled 的模块
28
+ - 监控子进程状态,core 模块崩溃触发全量重启
29
+ - 提供 Launcher API 供其他模块启停管理
30
+ - 优雅退出时清理所有子进程
31
+
32
+ ## API
33
+
34
+ - `GET /launcher/modules` — 列出所有模块及状态
35
+ - `POST /launcher/modules/{name}/start` — 启动模块
36
+ - `POST /launcher/modules/{name}/stop` — 停止模块
37
+ - `PUT /launcher/modules/{name}/state` — 修改模块 state
@@ -0,0 +1,253 @@
1
+ """
2
+ Discover modules via configurable sources (scan_dir / module_list).
3
+ Parse module.md YAML frontmatter to build ModuleInfo objects.
4
+ """
5
+
6
+ import os
7
+ import re
8
+ from dataclasses import dataclass, field
9
+
10
+
11
+ @dataclass
12
+ class LaunchConfig:
13
+ cmd: list = field(default_factory=list) # custom command, overrides runtime+entry
14
+ env: dict = field(default_factory=dict) # extra env vars, merged into process env
15
+ cwd: str = "" # working dir, relative to module_dir
16
+ boot_stdin: bool = True # whether to pass boot_info via stdin
17
+ timeout: int = 30 # seconds to wait for module.ready
18
+
19
+
20
+ @dataclass
21
+ class ModuleInfo:
22
+ name: str
23
+ display_name: str = ""
24
+ version: str = "1.0"
25
+ type: str = "infrastructure" # infrastructure | channel | agent | service | kernel
26
+ state: str = "enabled" # enabled | manual | disabled
27
+ runtime: str = "python" # python | node | binary
28
+ entry: str = "entry.py"
29
+ events: list = field(default_factory=list)
30
+ subscriptions: list = field(default_factory=list)
31
+ preferred_port: int = 0 # 0 = OS assigns; non-zero = try this port first
32
+ depends_on: list = field(default_factory=list) # module names this module depends on
33
+ monitor: bool = True # whether Watchdog should monitor this module
34
+ module_dir: str = "" # absolute path to the module directory
35
+ launch: LaunchConfig = field(default_factory=LaunchConfig)
36
+
37
+ def is_core(self, project_root: str) -> bool:
38
+ """Core modules live directly under {project_root}/core/."""
39
+ core_dir = os.path.join(project_root, "core")
40
+ return os.path.normcase(self.module_dir).startswith(os.path.normcase(core_dir + os.sep))
41
+
42
+
43
+ def _parse_frontmatter(text: str) -> dict:
44
+ """Parse YAML frontmatter from markdown text. Uses PyYAML if available."""
45
+ m = re.match(r'^---\s*\n(.*?)\n---', text, re.DOTALL)
46
+ if not m:
47
+ return {}
48
+ raw = m.group(1)
49
+ try:
50
+ import yaml
51
+ result = yaml.safe_load(raw)
52
+ return result if isinstance(result, dict) else {}
53
+ except ImportError:
54
+ pass
55
+ # Fallback: minimal parser (flat key-value + lists only)
56
+ result = {}
57
+ current_key = None
58
+ current_list = None
59
+ for line in raw.split('\n'):
60
+ stripped = line.strip()
61
+ if not stripped or stripped.startswith('#'):
62
+ continue
63
+ if stripped.startswith('- ') and current_key is not None and current_list is not None:
64
+ current_list.append(stripped[2:].strip().strip('"').strip("'"))
65
+ continue
66
+ if ':' in stripped:
67
+ key, _, val = stripped.partition(':')
68
+ key = key.strip()
69
+ val = val.strip().strip('"').strip("'")
70
+ if val:
71
+ if val == '[]':
72
+ result[key] = []
73
+ current_key = key
74
+ current_list = None
75
+ else:
76
+ result[key] = val
77
+ current_key = key
78
+ current_list = None
79
+ else:
80
+ current_key = key
81
+ current_list = []
82
+ result[key] = current_list
83
+ else:
84
+ current_key = None
85
+ current_list = None
86
+ return result
87
+
88
+
89
+ class ModuleScanner:
90
+ """Discover modules via configurable sources."""
91
+
92
+ def __init__(self, project_root: str, discovery: dict = None, launcher_dir: str = ""):
93
+ self.project_root = project_root
94
+ self.discovery = discovery
95
+ self.launcher_dir = launcher_dir or os.path.join(project_root, "core", "launcher")
96
+
97
+ def scan(self) -> dict[str, ModuleInfo]:
98
+ """Return dict of {module_name: ModuleInfo}. Duplicate names are skipped."""
99
+ modules = {}
100
+
101
+ # Built-in: always scan core/ (depth 1) and extensions/ (depth 2)
102
+ self._scan_dir(os.path.join(self.project_root, "core"), 1, modules)
103
+ self._scan_dir(os.path.join(self.project_root, "extensions"), 2, modules)
104
+
105
+ # Extra sources from discovery config
106
+ if self.discovery:
107
+ for key, src in self.discovery.items():
108
+ if not isinstance(src, dict):
109
+ continue
110
+ enabled = src.get("enabled", True)
111
+ if isinstance(enabled, str):
112
+ enabled = enabled.lower() not in ("false", "0", "no")
113
+ if not enabled:
114
+ continue
115
+
116
+ src_type = src.get("type", "")
117
+ path = src.get("path", "")
118
+ if not path:
119
+ continue
120
+ if not os.path.isabs(path):
121
+ path = os.path.join(self.project_root, path)
122
+
123
+ if src_type == "scan_dir":
124
+ max_depth = int(src.get("max_depth", 2))
125
+ self._scan_dir(path, max_depth, modules)
126
+ elif src_type == "module_list":
127
+ self._load_list(path, modules)
128
+
129
+ return modules
130
+
131
+ def _scan_dir(self, base: str, max_depth: int, modules: dict, depth: int = 0):
132
+ """Recursively scan for module.md up to max_depth levels."""
133
+ if not os.path.isdir(base) or depth > max_depth:
134
+ return
135
+ for name in os.listdir(base):
136
+ sub = os.path.join(base, name)
137
+ if not os.path.isdir(sub):
138
+ continue
139
+ # Skip launcher itself
140
+ if os.path.normcase(os.path.abspath(sub)) == os.path.normcase(os.path.abspath(self.launcher_dir)):
141
+ continue
142
+ if os.path.isfile(os.path.join(sub, "module.md")):
143
+ self._add_module(sub, modules)
144
+ elif depth + 1 <= max_depth:
145
+ self._scan_dir(sub, max_depth, depth=depth + 1, modules=modules)
146
+
147
+ def _load_list(self, list_path: str, modules: dict):
148
+ """Load modules from a list file (one directory path per line)."""
149
+ if not os.path.isfile(list_path):
150
+ print(f"[launcher] WARNING: module list not found: {list_path}")
151
+ return
152
+ try:
153
+ with open(list_path, "r", encoding="utf-8") as f:
154
+ for line in f:
155
+ line = line.strip()
156
+ if not line or line.startswith("#"):
157
+ continue
158
+ if not os.path.isabs(line):
159
+ line = os.path.join(self.project_root, line)
160
+ self._add_module(line, modules)
161
+ except Exception as e:
162
+ print(f"[launcher] WARNING: failed to read module list {list_path}: {e}")
163
+
164
+ def _add_module(self, mod_dir: str, modules: dict):
165
+ """Load a module and add it if name is unique."""
166
+ info = self._try_load(mod_dir)
167
+ if not info:
168
+ return
169
+ if info.name in modules:
170
+ print(f"[launcher] WARNING: duplicate module '{info.name}' at {mod_dir}, skipped")
171
+ return
172
+ modules[info.name] = info
173
+
174
+ def _try_load(self, mod_dir: str) -> ModuleInfo | None:
175
+ """Try to load a ModuleInfo from a directory containing module.md."""
176
+ if not os.path.isdir(mod_dir):
177
+ return None
178
+ md_path = os.path.join(mod_dir, "module.md")
179
+ if not os.path.isfile(md_path):
180
+ return None
181
+
182
+ try:
183
+ with open(md_path, "r", encoding="utf-8") as f:
184
+ text = f.read()
185
+ except Exception as e:
186
+ print(f"[launcher] WARNING: failed to read {md_path}: {e}")
187
+ return None
188
+
189
+ fm = _parse_frontmatter(text)
190
+ if not fm.get("name"):
191
+ print(f"[launcher] WARNING: module.md missing 'name' in {mod_dir}")
192
+ return None
193
+
194
+ events = fm.get("events", [])
195
+ subs = fm.get("subscriptions", [])
196
+ if isinstance(events, str):
197
+ events = [events]
198
+ if isinstance(subs, str):
199
+ subs = [subs]
200
+
201
+ try:
202
+ preferred_port = int(fm.get("preferred_port", 0))
203
+ except (ValueError, TypeError):
204
+ preferred_port = 0
205
+
206
+ depends_on = fm.get("depends_on", [])
207
+ if isinstance(depends_on, str):
208
+ depends_on = [depends_on]
209
+
210
+ monitor_val = fm.get("monitor", "true")
211
+ monitor = str(monitor_val).lower() not in ("false", "0", "no")
212
+
213
+ # Parse optional launch config
214
+ launch_raw = fm.get("launch", {})
215
+ if not isinstance(launch_raw, dict):
216
+ launch_raw = {}
217
+ launch_cmd = launch_raw.get("cmd", [])
218
+ if isinstance(launch_cmd, str):
219
+ launch_cmd = [launch_cmd]
220
+ launch_env = launch_raw.get("env", {})
221
+ if not isinstance(launch_env, dict):
222
+ launch_env = {}
223
+ launch_cwd = str(launch_raw.get("cwd", ""))
224
+ boot_stdin_val = launch_raw.get("boot_stdin", True)
225
+ launch_boot_stdin = str(boot_stdin_val).lower() not in ("false", "0", "no")
226
+ try:
227
+ launch_timeout = max(1, int(launch_raw.get("timeout", 30)))
228
+ except (ValueError, TypeError):
229
+ launch_timeout = 30
230
+ launch = LaunchConfig(
231
+ cmd=launch_cmd,
232
+ env=launch_env,
233
+ cwd=launch_cwd,
234
+ boot_stdin=launch_boot_stdin,
235
+ timeout=launch_timeout,
236
+ )
237
+
238
+ return ModuleInfo(
239
+ name=fm["name"],
240
+ display_name=fm.get("display_name", fm["name"]),
241
+ version=fm.get("version", "1.0"),
242
+ type=fm.get("type", "infrastructure"),
243
+ state=fm.get("state", "enabled"),
244
+ runtime=fm.get("runtime", "python"),
245
+ entry=fm.get("entry", "entry.py"),
246
+ events=events,
247
+ subscriptions=subs,
248
+ preferred_port=preferred_port,
249
+ depends_on=depends_on,
250
+ monitor=monitor,
251
+ module_dir=mod_dir,
252
+ launch=launch,
253
+ )