@agentunion/kite 1.2.0 → 1.3.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 (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,236 +0,0 @@
1
- """
2
- Model Service HTTP server.
3
- Exposes /health and /status endpoints.
4
- Connects to Event Hub via WebSocket for event publishing and subscription.
5
- Sends periodic heartbeat to Registry and test events to Event Hub.
6
- """
7
-
8
- import asyncio
9
- import json
10
- import time
11
- import uuid
12
- from datetime import datetime, timezone
13
-
14
- import httpx
15
- import websockets
16
- from fastapi import FastAPI
17
-
18
-
19
- class ModelServiceServer:
20
-
21
- def __init__(self, token: str = "", registry_url: str = "",
22
- event_hub_ws: str = "", boot_t0: float = 0):
23
- self.token = token
24
- self.registry_url = registry_url
25
- self.event_hub_ws = event_hub_ws
26
- self.boot_t0 = boot_t0
27
- self._ws_task: asyncio.Task | None = None
28
- self._heartbeat_task: asyncio.Task | None = None
29
- self._test_task: asyncio.Task | None = None
30
- self._ws: object | None = None
31
- self._ready_sent = False
32
- self._shutting_down = False
33
- self._uvicorn_server = None # set by entry.py for graceful shutdown
34
- self._start_time = time.time()
35
- self.app = self._create_app()
36
-
37
- def _create_app(self) -> FastAPI:
38
- app = FastAPI(title="Kite Model Service", docs_url=None, redoc_url=None)
39
- server = self
40
-
41
- @app.on_event("startup")
42
- async def _startup():
43
- server._heartbeat_task = asyncio.create_task(server._heartbeat_loop())
44
- if server.event_hub_ws:
45
- server._ws_task = asyncio.create_task(server._ws_loop())
46
- server._test_task = asyncio.create_task(server._test_event_loop())
47
-
48
- @app.on_event("shutdown")
49
- async def _shutdown():
50
- if server._heartbeat_task:
51
- server._heartbeat_task.cancel()
52
- if server._ws_task:
53
- server._ws_task.cancel()
54
- if server._test_task:
55
- server._test_task.cancel()
56
- if server._ws:
57
- await server._ws.close()
58
- print("[model_service] Shutdown complete")
59
-
60
- @app.get("/health")
61
- async def health():
62
- return {
63
- "status": "healthy",
64
- "details": {
65
- "event_hub_connected": server._ws is not None,
66
- "uptime_seconds": round(time.time() - server._start_time),
67
- },
68
- }
69
-
70
- @app.get("/status")
71
- async def status():
72
- return {
73
- "module": "model_service",
74
- "status": "running",
75
- "event_hub_connected": server._ws is not None,
76
- "uptime_seconds": round(time.time() - server._start_time),
77
- }
78
-
79
- return app
80
-
81
- # ── Event Hub WebSocket client ──
82
-
83
- async def _ws_loop(self):
84
- """Connect to Event Hub, subscribe, and listen. Reconnect on failure."""
85
- retry_delay = 0.5 # start with 0.5s
86
- max_delay = 30 # cap at 30s
87
- while not self._shutting_down:
88
- try:
89
- await self._ws_connect()
90
- except asyncio.CancelledError:
91
- return
92
- retry_delay = 0.5 # reset on successful connection
93
- except Exception as e:
94
- print(f"[model_service] Event Hub connection error: {e}, retrying in {retry_delay:.1f}s")
95
- self._ws = None
96
- if self._shutting_down:
97
- return
98
- await asyncio.sleep(retry_delay)
99
- retry_delay = min(retry_delay * 2, max_delay) # exponential backoff
100
-
101
- async def _ws_connect(self):
102
- """Single WebSocket session: connect, subscribe, receive loop."""
103
- url = f"{self.event_hub_ws}?token={self.token}&id=model_service"
104
- print(f"[model_service] WS connecting to {self.event_hub_ws}")
105
- async with websockets.connect(url, open_timeout=3, ping_interval=None, ping_timeout=None, close_timeout=10) as ws:
106
- self._ws = ws
107
- elapsed = time.monotonic() - self.boot_t0 if self.boot_t0 else 0
108
- elapsed_str = f" ({elapsed:.1f}s)" if elapsed else ""
109
- print(f"[model_service] Connected to Event Hub{elapsed_str}")
110
-
111
- # Subscribe to module lifecycle events + shutdown
112
- await ws.send(json.dumps({
113
- "type": "subscribe",
114
- "events": ["module.started", "module.stopped", "module.shutdown"],
115
- }))
116
-
117
- # Send module.ready (once) so Launcher knows we're up
118
- if not self._ready_sent:
119
- ready_msg = {
120
- "type": "event",
121
- "event_id": str(uuid.uuid4()),
122
- "event": "module.ready",
123
- "source": "model_service",
124
- "timestamp": datetime.now(timezone.utc).isoformat(),
125
- "data": {
126
- "module_id": "model_service",
127
- "graceful_shutdown": True,
128
- },
129
- }
130
- await ws.send(json.dumps(ready_msg))
131
- self._ready_sent = True
132
- elapsed = time.monotonic() - self.boot_t0 if self.boot_t0 else 0
133
- elapsed_str = f" ({elapsed:.1f}s)" if elapsed else ""
134
- print(f"[model_service] module.ready sent{elapsed_str}")
135
-
136
- # Receive loop
137
- async for raw in ws:
138
- try:
139
- msg = json.loads(raw)
140
- except (json.JSONDecodeError, TypeError):
141
- continue
142
-
143
- try:
144
- msg_type = msg.get("type", "")
145
- if msg_type == "event":
146
- event_name = msg.get("event", "")
147
- if event_name == "module.shutdown":
148
- target = (msg.get("data") if isinstance(msg.get("data"), dict) else {}).get("module_id", "")
149
- if target == "model_service":
150
- await self._handle_shutdown(ws)
151
- return
152
- elif msg_type == "ack":
153
- pass # publish confirmed
154
- elif msg_type == "error":
155
- print(f"[model_service] Event Hub error: {msg.get('message')}")
156
- except Exception as e:
157
- print(f"[model_service] 事件处理异常(已忽略): {e}")
158
-
159
- async def _handle_shutdown(self, ws):
160
- """Handle module.shutdown: ack → cleanup → ready → exit."""
161
- print("[model_service] Received module.shutdown")
162
- self._shutting_down = True
163
-
164
- # Step 1: Send ack
165
- await self._publish_event({
166
- "event": "module.shutdown.ack",
167
- "data": {"module_id": "model_service", "estimated_cleanup": 2},
168
- })
169
- print("[model_service] shutdown ack sent")
170
-
171
- # Step 2: Cleanup (cancel background tasks)
172
- if self._heartbeat_task:
173
- self._heartbeat_task.cancel()
174
- if self._test_task:
175
- self._test_task.cancel()
176
-
177
- # Step 3: Send ready (before closing WS!)
178
- await self._publish_event({
179
- "event": "module.shutdown.ready",
180
- "data": {"module_id": "model_service"},
181
- })
182
- print("[model_service] Shutdown complete")
183
-
184
- # Step 4: Trigger uvicorn exit (WS will close when uvicorn shuts down)
185
- if self._uvicorn_server:
186
- self._uvicorn_server.should_exit = True
187
-
188
- async def _publish_event(self, event: dict):
189
- """Publish an event to Event Hub via WebSocket."""
190
- if not self._ws:
191
- return
192
- msg = {
193
- "type": "event",
194
- "event_id": str(uuid.uuid4()),
195
- "event": event.get("event", ""),
196
- "source": "model_service",
197
- "timestamp": datetime.now(timezone.utc).isoformat(),
198
- "data": event.get("data", {}),
199
- }
200
- try:
201
- await self._ws.send(json.dumps(msg))
202
- except Exception as e:
203
- print(f"[model_service] Failed to publish event: {e}")
204
-
205
- # ── Heartbeat to Registry ──
206
-
207
- async def _heartbeat_loop(self):
208
- """Send heartbeat to Registry every 30 seconds."""
209
- while True:
210
- await asyncio.sleep(30)
211
- try:
212
- async with httpx.AsyncClient() as client:
213
- await client.post(
214
- f"{self.registry_url}/modules",
215
- json={"action": "heartbeat", "module_id": "model_service"},
216
- headers={"Authorization": f"Bearer {self.token}"},
217
- timeout=5,
218
- )
219
- print("[model_service] heartbeat sent")
220
- except Exception:
221
- pass
222
-
223
- # ── Test event loop ──
224
-
225
- async def _test_event_loop(self):
226
- """Publish a test event every 10 seconds."""
227
- while True:
228
- await asyncio.sleep(10)
229
- await self._publish_event({
230
- "event": "model_service.test",
231
- "data": {
232
- "message": "test event from model_service",
233
- "timestamp": datetime.now(timezone.utc).isoformat(),
234
- },
235
- })
236
- print("[model_service] test event published")
@@ -1,229 +0,0 @@
1
- """
2
- Watchdog HTTP server.
3
- Exposes /health and /status endpoints. Runs the monitor loop on startup.
4
- Connects to Event Hub via WebSocket for event publishing and subscription.
5
- """
6
-
7
- import asyncio
8
- import json
9
- import time
10
- import uuid
11
-
12
- import httpx
13
- import websockets
14
- from fastapi import FastAPI
15
-
16
- from .monitor import HealthMonitor
17
-
18
-
19
- class WatchdogServer:
20
-
21
- def __init__(self, monitor: HealthMonitor, token: str = "",
22
- event_hub_ws: str = ""):
23
- self.monitor = monitor
24
- self.token = token
25
- self.event_hub_ws = event_hub_ws
26
- self._monitor_task: asyncio.Task | None = None
27
- self._ws_task: asyncio.Task | None = None
28
- self._heartbeat_task: asyncio.Task | None = None
29
- self._ws: object | None = None
30
- self._ready_sent = False
31
- self._start_time = time.time()
32
- self._uvicorn_server = None # set by entry.py for graceful shutdown
33
- self._shutting_down = False
34
- self.app = self._create_app()
35
-
36
- # Wire up publish callback on monitor
37
- self.monitor.publish_event = self._publish_event
38
-
39
- def _create_app(self) -> FastAPI:
40
- app = FastAPI(title="Kite Watchdog", docs_url=None, redoc_url=None)
41
- server = self
42
-
43
- @app.on_event("startup")
44
- async def _startup():
45
- server._monitor_task = asyncio.create_task(server.monitor.run())
46
- server._heartbeat_task = asyncio.create_task(server._heartbeat_loop())
47
- if server.event_hub_ws:
48
- server._ws_task = asyncio.create_task(server._ws_loop())
49
- server._test_task = asyncio.create_task(server._test_event_loop())
50
-
51
- @app.on_event("shutdown")
52
- async def _shutdown():
53
- server.monitor.stop()
54
- if server._monitor_task:
55
- server._monitor_task.cancel()
56
- if server._heartbeat_task:
57
- server._heartbeat_task.cancel()
58
- if server._ws_task:
59
- server._ws_task.cancel()
60
- if hasattr(server, '_test_task') and server._test_task:
61
- server._test_task.cancel()
62
- if server._ws:
63
- await server._ws.close()
64
-
65
- @app.get("/health")
66
- async def health():
67
- return {
68
- "status": "healthy",
69
- "details": {
70
- "monitored_modules": len(server.monitor.modules),
71
- "event_hub_connected": server._ws is not None,
72
- "uptime_seconds": round(time.time() - server._start_time),
73
- },
74
- }
75
-
76
- @app.get("/status")
77
- async def status():
78
- return server.monitor.get_status()
79
-
80
- return app
81
-
82
- # ── Event Hub WebSocket client ──
83
-
84
- async def _ws_loop(self):
85
- """Connect to Event Hub, subscribe, and listen. Reconnect on failure."""
86
- retry_delay = 0.5 # start with 0.5s
87
- max_delay = 30 # cap at 30s
88
- while not self._shutting_down:
89
- try:
90
- await self._ws_connect()
91
- retry_delay = 0.5 # reset on successful connection
92
- except asyncio.CancelledError:
93
- return
94
- except Exception as e:
95
- print(f"[watchdog] Event Hub connection error: {e}, retrying in {retry_delay:.1f}s")
96
- self._ws = None
97
- if self._shutting_down:
98
- return
99
- await asyncio.sleep(retry_delay)
100
- retry_delay = min(retry_delay * 2, max_delay) # exponential backoff
101
-
102
- async def _ws_connect(self):
103
- """Single WebSocket session: connect, subscribe, receive loop."""
104
- url = f"{self.event_hub_ws}?token={self.token}&id=watchdog"
105
- print(f"[watchdog] WS connecting to {self.event_hub_ws}")
106
- async with websockets.connect(url, open_timeout=3, ping_interval=None, ping_timeout=None, close_timeout=10) as ws:
107
- self._ws = ws
108
- print("[watchdog] Connected to Event Hub")
109
-
110
- # Subscribe to module lifecycle events
111
- await ws.send(json.dumps({
112
- "type": "subscribe",
113
- "events": ["system.ready", "module.started", "module.stopped", "module.exiting", "module.ready", "module.shutdown"],
114
- }))
115
-
116
- # Send module.ready (once) so Launcher knows we're up
117
- if not self._ready_sent:
118
- from datetime import datetime, timezone
119
- ready_msg = {
120
- "type": "event",
121
- "event_id": str(uuid.uuid4()),
122
- "event": "module.ready",
123
- "source": "watchdog",
124
- "timestamp": datetime.now(timezone.utc).isoformat(),
125
- "data": {
126
- "module_id": "watchdog",
127
- "graceful_shutdown": True,
128
- },
129
- }
130
- await ws.send(json.dumps(ready_msg))
131
- self._ready_sent = True
132
-
133
- # Receive loop
134
- async for raw in ws:
135
- try:
136
- msg = json.loads(raw)
137
- except (json.JSONDecodeError, TypeError):
138
- continue
139
-
140
- try:
141
- msg_type = msg.get("type", "")
142
- if msg_type == "event":
143
- event = msg.get("event", "")
144
- data = msg.get("data") if isinstance(msg.get("data"), dict) else {}
145
- if event == "module.shutdown" and data.get("module_id") == "watchdog":
146
- await self._handle_shutdown(data)
147
- return
148
- await self.monitor.handle_event(msg)
149
- elif msg_type == "ack":
150
- pass # publish confirmed
151
- elif msg_type == "error":
152
- print(f"[watchdog] Event Hub error: {msg.get('message')}")
153
- except Exception as e:
154
- print(f"[watchdog] 事件处理异常(已忽略): {e}")
155
-
156
- async def _handle_shutdown(self, data: dict):
157
- """Handle module.shutdown event — ack, cleanup, ready, exit."""
158
- print("[watchdog] Received shutdown request")
159
- self._shutting_down = True
160
- # Step 1: Send ack
161
- await self._publish_event({
162
- "event": "module.shutdown.ack",
163
- "data": {"module_id": "watchdog", "estimated_cleanup": 2},
164
- })
165
- # Step 2: Cleanup
166
- self.monitor.stop()
167
- if self._monitor_task:
168
- self._monitor_task.cancel()
169
- if self._heartbeat_task:
170
- self._heartbeat_task.cancel()
171
- if hasattr(self, '_test_task') and self._test_task:
172
- self._test_task.cancel()
173
- # Step 3: Send ready (before closing WS!)
174
- await self._publish_event({
175
- "event": "module.shutdown.ready",
176
- "data": {"module_id": "watchdog"},
177
- })
178
- print("[watchdog] Shutdown ready, exiting")
179
- # Step 4: Trigger uvicorn exit (WS will close when uvicorn shuts down)
180
- if self._uvicorn_server:
181
- self._uvicorn_server.should_exit = True
182
-
183
- async def _publish_event(self, event: dict):
184
- """Publish an event to Event Hub via WebSocket."""
185
- if not self._ws:
186
- return
187
- from datetime import datetime, timezone
188
- msg = {
189
- "type": "event",
190
- "event_id": str(uuid.uuid4()),
191
- "event": event.get("event", ""),
192
- "source": "watchdog",
193
- "timestamp": datetime.now(timezone.utc).isoformat(),
194
- "data": event.get("data", {}),
195
- }
196
- try:
197
- await self._ws.send(json.dumps(msg))
198
- except Exception as e:
199
- print(f"[watchdog] Failed to publish event: {e}")
200
-
201
- # ── Heartbeat to Registry ──
202
-
203
- async def _heartbeat_loop(self):
204
- """Send heartbeat to Registry every 30 seconds."""
205
- while True:
206
- await asyncio.sleep(30)
207
- try:
208
- async with httpx.AsyncClient() as client:
209
- await client.post(
210
- f"{self.monitor.registry_url}/modules",
211
- json={"action": "heartbeat", "module_id": "watchdog"},
212
- headers={"Authorization": f"Bearer {self.monitor.own_token}"},
213
- timeout=5,
214
- )
215
- except Exception:
216
- pass
217
-
218
- async def _test_event_loop(self):
219
- """Publish a test event every 5 seconds."""
220
- from datetime import datetime, timezone
221
- while True:
222
- await asyncio.sleep(5)
223
- await self._publish_event({
224
- "event": "watchdog.test",
225
- "data": {
226
- "message": "heartbeat from watchdog",
227
- "timestamp": datetime.now(timezone.utc).isoformat(),
228
- },
229
- })
File without changes
File without changes
File without changes
File without changes
File without changes