@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.
- package/__init__.py +1 -0
- package/__main__.py +15 -0
- package/cli.js +70 -0
- package/core/__init__.py +0 -0
- package/core/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/event_hub/BENCHMARK.md +94 -0
- package/core/event_hub/__init__.py +0 -0
- package/core/event_hub/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/event_hub/__pycache__/bench.cpython-313.pyc +0 -0
- package/core/event_hub/__pycache__/bench_perf.cpython-313.pyc +0 -0
- package/core/event_hub/__pycache__/dedup.cpython-313.pyc +0 -0
- package/core/event_hub/__pycache__/entry.cpython-313.pyc +0 -0
- package/core/event_hub/__pycache__/hub.cpython-313.pyc +0 -0
- package/core/event_hub/__pycache__/router.cpython-313.pyc +0 -0
- package/core/event_hub/__pycache__/server.cpython-313.pyc +0 -0
- package/core/event_hub/bench.py +459 -0
- package/core/event_hub/bench_extreme.py +308 -0
- package/core/event_hub/bench_perf.py +350 -0
- package/core/event_hub/bench_results/.gitkeep +0 -0
- package/core/event_hub/bench_results/2026-02-28_13-26-48.json +51 -0
- package/core/event_hub/bench_results/2026-02-28_13-44-45.json +51 -0
- package/core/event_hub/bench_results/2026-02-28_13-45-39.json +51 -0
- package/core/event_hub/dedup.py +31 -0
- package/core/event_hub/entry.py +113 -0
- package/core/event_hub/hub.py +263 -0
- package/core/event_hub/module.md +21 -0
- package/core/event_hub/router.py +21 -0
- package/core/event_hub/server.py +138 -0
- package/core/event_hub_bench/entry.py +371 -0
- package/core/event_hub_bench/module.md +25 -0
- package/core/launcher/__init__.py +0 -0
- package/core/launcher/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/launcher/__pycache__/entry.cpython-313.pyc +0 -0
- package/core/launcher/__pycache__/module_scanner.cpython-313.pyc +0 -0
- package/core/launcher/__pycache__/process_manager.cpython-313.pyc +0 -0
- package/core/launcher/data/log/lifecycle.jsonl +1045 -0
- package/core/launcher/data/processes_14752.json +32 -0
- package/core/launcher/data/token.txt +1 -0
- package/core/launcher/entry.py +965 -0
- package/core/launcher/module.md +37 -0
- package/core/launcher/module_scanner.py +253 -0
- package/core/launcher/process_manager.py +435 -0
- package/core/registry/__init__.py +0 -0
- package/core/registry/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/registry/__pycache__/entry.cpython-313.pyc +0 -0
- package/core/registry/__pycache__/server.cpython-313.pyc +0 -0
- package/core/registry/__pycache__/store.cpython-313.pyc +0 -0
- package/core/registry/data/port.txt +1 -0
- package/core/registry/data/port_14752.txt +1 -0
- package/core/registry/data/port_484.txt +1 -0
- package/core/registry/entry.py +73 -0
- package/core/registry/module.md +30 -0
- package/core/registry/server.py +256 -0
- package/core/registry/store.py +232 -0
- package/extensions/__init__.py +0 -0
- package/extensions/__pycache__/__init__.cpython-313.pyc +0 -0
- package/extensions/services/__init__.py +0 -0
- package/extensions/services/__pycache__/__init__.cpython-313.pyc +0 -0
- package/extensions/services/watchdog/__init__.py +0 -0
- package/extensions/services/watchdog/__pycache__/__init__.cpython-313.pyc +0 -0
- package/extensions/services/watchdog/__pycache__/entry.cpython-313.pyc +0 -0
- package/extensions/services/watchdog/__pycache__/monitor.cpython-313.pyc +0 -0
- package/extensions/services/watchdog/__pycache__/server.cpython-313.pyc +0 -0
- package/extensions/services/watchdog/entry.py +143 -0
- package/extensions/services/watchdog/module.md +25 -0
- package/extensions/services/watchdog/monitor.py +420 -0
- package/extensions/services/watchdog/server.py +167 -0
- package/main.py +17 -0
- 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
|
+
)
|