@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,8 +1,6 @@
1
1
  """
2
- Assistant 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.
2
+ Assistant WebSocket client.
3
+ Connects to Kernel via WebSocket JSON-RPC 2.0 for event publishing and subscription.
6
4
  """
7
5
 
8
6
  import asyncio
@@ -11,127 +9,101 @@ import time
11
9
  import uuid
12
10
  from datetime import datetime, timezone
13
11
 
14
- import httpx
15
12
  import websockets
16
- from fastapi import FastAPI
17
13
 
18
14
 
19
15
  class AssistantServer:
20
16
 
21
- def __init__(self, token: str = "", registry_url: str = "",
22
- event_hub_ws: str = "", boot_t0: float = 0):
17
+ def __init__(self, token: str = "", kernel_port: int = 0, boot_t0: float = 0):
23
18
  self.token = token
24
- self.registry_url = registry_url
25
- self.event_hub_ws = event_hub_ws
19
+ self.kernel_port = kernel_port
26
20
  self.boot_t0 = boot_t0
27
21
  self._ws_task: asyncio.Task | None = None
28
- self._heartbeat_task: asyncio.Task | None = None
29
22
  self._test_task: asyncio.Task | None = None
30
23
  self._ws: object | None = None
31
24
  self._ready_sent = False
32
25
  self._shutting_down = False
33
- self._uvicorn_server = None # set by entry.py for graceful shutdown
34
26
  self._start_time = time.time()
35
- self.app = self._create_app()
36
-
37
- def _create_app(self) -> FastAPI:
38
- app = FastAPI(title="Kite Assistant", 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("[assistant] 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
27
 
70
- @app.get("/status")
71
- async def status():
72
- return {
73
- "module": "assistant",
74
- "status": "running",
75
- "event_hub_connected": server._ws is not None,
76
- "uptime_seconds": round(time.time() - server._start_time),
77
- }
28
+ async def run(self):
29
+ """Main entry point: start WebSocket loop and test event loop."""
30
+ if self.kernel_port:
31
+ self._ws_task = asyncio.create_task(self._ws_loop())
32
+ self._test_task = asyncio.create_task(self._test_event_loop())
33
+
34
+ # Wait for tasks to complete
35
+ tasks = [t for t in [self._ws_task, self._test_task] if t]
36
+ if tasks:
37
+ await asyncio.gather(*tasks, return_exceptions=True)
78
38
 
79
- return app
39
+ print("[assistant] Shutdown complete")
80
40
 
81
- # ── Event Hub WebSocket client ──
41
+ # ── Kernel WebSocket client ──
82
42
 
83
43
  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
44
+ """Connect to Kernel, subscribe, register, and listen. Reconnect on failure."""
45
+ retry_delay = 0.5
46
+ max_delay = 30
87
47
  while not self._shutting_down:
88
48
  try:
89
49
  await self._ws_connect()
90
50
  except asyncio.CancelledError:
91
51
  return
92
- retry_delay = 0.5 # reset on successful connection
93
52
  except Exception as e:
94
- print(f"[assistant] Event Hub connection error: {e}, retrying in {retry_delay:.1f}s")
53
+ print(f"[assistant] Kernel connection error: {e}, retrying in {retry_delay:.1f}s")
95
54
  self._ws = None
96
55
  if self._shutting_down:
97
56
  return
98
57
  await asyncio.sleep(retry_delay)
99
- retry_delay = min(retry_delay * 2, max_delay) # exponential backoff
58
+ retry_delay = min(retry_delay * 2, max_delay)
100
59
 
101
60
  async def _ws_connect(self):
102
- """Single WebSocket session: connect, subscribe, receive loop."""
103
- url = f"{self.event_hub_ws}?token={self.token}&id=assistant"
104
- print(f"[assistant] WS connecting to {self.event_hub_ws}")
61
+ """Single WebSocket session: connect, subscribe, register, ready, receive loop."""
62
+ url = f"ws://127.0.0.1:{self.kernel_port}/ws?token={self.token}&id=assistant"
63
+ print(f"[assistant] Connecting to Kernel (port {self.kernel_port})")
105
64
  async with websockets.connect(url, open_timeout=3, ping_interval=None, ping_timeout=None, close_timeout=10) as ws:
106
65
  self._ws = ws
107
66
  elapsed = time.monotonic() - self.boot_t0 if self.boot_t0 else 0
108
67
  elapsed_str = f" ({elapsed:.1f}s)" if elapsed else ""
109
- print(f"[assistant] Connected to Event Hub{elapsed_str}")
68
+ print(f"[assistant] Connected to Kernel{elapsed_str}")
110
69
 
111
- # Subscribe to module lifecycle events + shutdown
112
- await ws.send(json.dumps({
113
- "type": "subscribe",
70
+ # Step 1: Subscribe to events (先订阅)
71
+ await self._rpc_call(ws, "event.subscribe", {
114
72
  "events": ["module.started", "module.stopped", "module.shutdown"],
115
- }))
73
+ })
74
+
75
+ # Step 2: Register to Kernel (再注册)
76
+ await self._rpc_call(ws, "registry.register", {
77
+ "module_id": "assistant",
78
+ "module_type": "agent",
79
+ "name": "Assistant",
80
+ "events_publish": {
81
+ "assistant.test": {},
82
+ },
83
+ "events_subscribe": [
84
+ "module.started",
85
+ "module.stopped",
86
+ "module.shutdown",
87
+ ],
88
+ })
116
89
 
117
- # Send module.ready (once) so Launcher knows we're up
90
+ # Step 3: Publish module.ready (once)
118
91
  if not self._ready_sent:
119
- ready_msg = {
120
- "type": "event",
92
+ await self._rpc_call(ws, "event.publish", {
121
93
  "event_id": str(uuid.uuid4()),
122
94
  "event": "module.ready",
123
- "source": "assistant",
124
- "timestamp": datetime.now(timezone.utc).isoformat(),
125
95
  "data": {
126
96
  "module_id": "assistant",
127
97
  "graceful_shutdown": True,
128
98
  },
129
- }
130
- await ws.send(json.dumps(ready_msg))
99
+ })
131
100
  self._ready_sent = True
132
101
  elapsed = time.monotonic() - self.boot_t0 if self.boot_t0 else 0
133
102
  elapsed_str = f" ({elapsed:.1f}s)" if elapsed else ""
134
- print(f"[assistant] module.ready sent{elapsed_str}")
103
+ print(f"[assistant] module.ready published{elapsed_str}")
104
+
105
+ # Reset retry delay on successful connection
106
+ retry_delay = 0.5
135
107
 
136
108
  # Receive loop
137
109
  async for raw in ws:
@@ -141,18 +113,22 @@ class AssistantServer:
141
113
  continue
142
114
 
143
115
  try:
144
- msg_type = msg.get("type", "")
145
- if msg_type == "event":
146
- event_name = msg.get("event", "")
116
+ has_method = "method" in msg
117
+ has_id = "id" in msg
118
+
119
+ if has_method and not has_id:
120
+ # JSON-RPC Notification (event delivery)
121
+ params = msg.get("params", {})
122
+ event_name = params.get("event", "")
147
123
  if event_name == "module.shutdown":
148
- target = (msg.get("data") if isinstance(msg.get("data"), dict) else {}).get("module_id", "")
124
+ data = params.get("data", {})
125
+ target = data.get("module_id", "")
149
126
  if target == "assistant":
150
127
  await self._handle_shutdown(ws)
151
128
  return
152
- elif msg_type == "ack":
153
- pass # publish confirmed
154
- elif msg_type == "error":
155
- print(f"[assistant] Event Hub error: {msg.get('message')}")
129
+ elif not has_method and has_id:
130
+ # JSON-RPC Response (to our RPC calls)
131
+ pass
156
132
  except Exception as e:
157
133
  print(f"[assistant] 事件处理异常(已忽略): {e}")
158
134
 
@@ -162,64 +138,49 @@ class AssistantServer:
162
138
  self._shutting_down = True
163
139
 
164
140
  # Step 1: Send ack
165
- await self._publish_event({
141
+ await self._rpc_call(ws, "event.publish", {
142
+ "event_id": str(uuid.uuid4()),
166
143
  "event": "module.shutdown.ack",
167
144
  "data": {"module_id": "assistant", "estimated_cleanup": 2},
168
145
  })
169
146
  print("[assistant] shutdown ack sent")
170
147
 
171
148
  # Step 2: Cleanup (cancel background tasks)
172
- if self._heartbeat_task:
173
- self._heartbeat_task.cancel()
174
149
  if self._test_task:
175
150
  self._test_task.cancel()
176
151
 
177
152
  # Step 3: Send ready (before closing WS!)
178
- await self._publish_event({
153
+ await self._rpc_call(ws, "event.publish", {
154
+ "event_id": str(uuid.uuid4()),
179
155
  "event": "module.shutdown.ready",
180
156
  "data": {"module_id": "assistant"},
181
157
  })
182
- print("[assistant] Shutdown complete")
158
+ print("[assistant] Shutdown ready sent")
183
159
 
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
160
+ # Step 4: Exit process
161
+ import sys
162
+ sys.exit(0)
163
+
164
+ async def _rpc_call(self, ws, method: str, params: dict = None):
165
+ """Send a JSON-RPC 2.0 request."""
166
+ msg = {"jsonrpc": "2.0", "id": str(uuid.uuid4()), "method": method}
167
+ if params:
168
+ msg["params"] = params
169
+ await ws.send(json.dumps(msg))
187
170
 
188
171
  async def _publish_event(self, event: dict):
189
- """Publish an event to Event Hub via WebSocket."""
172
+ """Publish an event via JSON-RPC event.publish."""
190
173
  if not self._ws:
191
174
  return
192
- msg = {
193
- "type": "event",
194
- "event_id": str(uuid.uuid4()),
195
- "event": event.get("event", ""),
196
- "source": "assistant",
197
- "timestamp": datetime.now(timezone.utc).isoformat(),
198
- "data": event.get("data", {}),
199
- }
200
175
  try:
201
- await self._ws.send(json.dumps(msg))
176
+ await self._rpc_call(self._ws, "event.publish", {
177
+ "event_id": str(uuid.uuid4()),
178
+ "event": event.get("event", ""),
179
+ "data": event.get("data", {}),
180
+ })
202
181
  except Exception as e:
203
182
  print(f"[assistant] Failed to publish event: {e}")
204
183
 
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": "assistant"},
216
- headers={"Authorization": f"Bearer {self.token}"},
217
- timeout=5,
218
- )
219
- print("[assistant] heartbeat sent")
220
- except Exception:
221
- pass
222
-
223
184
  # ── Test event loop ──
224
185
 
225
186
  async def _test_event_loop(self):
@@ -1,21 +1,19 @@
1
1
  """
2
2
  ACP Channel entry point.
3
- Reads boot_info from stdin, registers to Registry, starts acp_channel service.
3
+ Reads boot_info from stdin, connects to Kernel via WebSocket JSON-RPC 2.0.
4
4
  """
5
5
 
6
+ import asyncio
6
7
  import builtins
7
8
  import json
8
9
  import os
9
- import socket
10
+ import re
10
11
  import sys
11
12
  import threading
12
- import re
13
13
  import time
14
+ import traceback
14
15
  from datetime import datetime, timezone
15
16
 
16
- import httpx
17
- import uvicorn
18
-
19
17
 
20
18
  # ── Safe stdout/stderr: ignore BrokenPipeError after Launcher closes stdio ──
21
19
 
@@ -240,59 +238,24 @@ def _fmt_elapsed(t0: float) -> str:
240
238
  return f"{d:.0f}s"
241
239
 
242
240
 
243
- def _get_free_port() -> int:
244
- with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
245
- s.bind(("127.0.0.1", 0))
246
- return s.getsockname()[1]
247
-
248
-
249
- def _register_to_registry(client: httpx.Client, token: str, registry_url: str, port: int):
250
- payload = {
251
- "action": "register",
252
- "module_id": "acp_channel",
253
- "module_type": "channel",
254
- "name": "ACP Channel",
255
- "api_endpoint": f"http://127.0.0.1:{port}",
256
- "health_endpoint": "/health",
257
- "events_publish": {
258
- "acp_channel.test": {"description": "Test event from acp_channel module"},
259
- },
260
- "events_subscribe": [
261
- "module.started",
262
- "module.stopped",
263
- "module.shutdown",
264
- ],
265
- }
266
- headers = {"Authorization": f"Bearer {token}"}
267
- try:
268
- resp = client.post(f"{registry_url}/modules", json=payload, headers=headers)
269
- if resp.status_code == 200:
270
- pass # timing printed in main()
271
- else:
272
- print(f"[acp_channel] WARNING: Registry returned {resp.status_code}")
273
- except Exception as e:
274
- print(f"[acp_channel] WARNING: Registry registration failed: {e}")
275
-
241
+ def _read_stdin_kite_message(expected_type: str, timeout: float = 10) -> dict | None:
242
+ """Read a single kite message of expected type from stdin with timeout."""
243
+ result = [None]
276
244
 
277
- def _get_event_hub_ws(client: httpx.Client, token: str, registry_url: str) -> str:
278
- """Discover Event Hub WebSocket endpoint from Registry, with retry."""
279
- import time
280
- headers = {"Authorization": f"Bearer {token}"}
281
- deadline = time.time() + 10
282
- while time.time() < deadline:
245
+ def _read():
283
246
  try:
284
- resp = client.get(
285
- f"{registry_url}/get/event_hub.metadata.ws_endpoint",
286
- headers=headers,
287
- )
288
- if resp.status_code == 200:
289
- val = resp.json()
290
- if val:
291
- return val
247
+ line = sys.stdin.readline().strip()
248
+ if line:
249
+ msg = json.loads(line)
250
+ if isinstance(msg, dict) and msg.get("kite") == expected_type:
251
+ result[0] = msg
292
252
  except Exception:
293
253
  pass
294
- time.sleep(0.2)
295
- return ""
254
+
255
+ t = threading.Thread(target=_read, daemon=True)
256
+ t.start()
257
+ t.join(timeout=timeout)
258
+ return result[0]
296
259
 
297
260
 
298
261
  def main():
@@ -334,42 +297,28 @@ def main():
334
297
  except Exception:
335
298
  pass
336
299
 
337
- # Read registry_port from environment variable
338
- registry_port = int(os.environ.get("KITE_REGISTRY_PORT", "0"))
300
+ # Read kernel_port: env first (fast path), stdin fallback (parallel start)
301
+ kernel_port = int(os.environ.get("KITE_KERNEL_PORT", "0"))
302
+ if not kernel_port:
303
+ msg = _read_stdin_kite_message("kernel_port", timeout=10)
304
+ if msg:
305
+ kernel_port = int(msg.get("kernel_port", 0))
339
306
 
340
- if not token or not registry_port:
341
- print("[acp_channel] ERROR: Missing token or KITE_REGISTRY_PORT")
307
+ if not token or not kernel_port:
308
+ print("[acp_channel] ERROR: Missing token or kernel_port")
342
309
  sys.exit(1)
343
310
 
344
- print(f"[acp_channel] Token received ({len(token)} chars), registry port: {registry_port} ({_fmt_elapsed(_t0)})")
345
-
346
- registry_url = f"http://127.0.0.1:{registry_port}"
347
- port = _get_free_port()
348
-
349
- # Register and discover Event Hub synchronously before starting uvicorn
350
- client = httpx.Client(timeout=5)
351
- _register_to_registry(client, token, registry_url, port)
352
- print(f"[acp_channel] Registered to Registry ({_fmt_elapsed(_t0)})")
353
- event_hub_ws = _get_event_hub_ws(client, token, registry_url)
354
- if not event_hub_ws:
355
- print("[acp_channel] WARNING: Could not discover Event Hub WS, events disabled")
356
- else:
357
- print(f"[acp_channel] Discovered Event Hub: {event_hub_ws}")
358
- client.close()
311
+ print(f"[acp_channel] Token received ({len(token)} chars), kernel port: {kernel_port} ({_fmt_elapsed(_t0)})")
359
312
 
360
313
  server = AcpChannelServer(
361
314
  token=token,
362
- registry_url=registry_url,
363
- event_hub_ws=event_hub_ws,
315
+ kernel_port=kernel_port,
364
316
  boot_t0=_t0,
365
317
  )
366
318
 
367
- print(f"[acp_channel] Starting on port {port} ({_fmt_elapsed(_t0)})")
319
+ print(f"[acp_channel] Starting ({_fmt_elapsed(_t0)})")
368
320
  try:
369
- config = uvicorn.Config(server.app, host="127.0.0.1", port=port, log_level="warning")
370
- uvi_server = uvicorn.Server(config)
371
- server._uvicorn_server = uvi_server
372
- uvi_server.run()
321
+ asyncio.run(server.run())
373
322
  except Exception as e:
374
323
  _write_crash(type(e), e, e.__traceback__, severity="critical", handled=True)
375
324
  _print_crash_summary(type(e), e.__traceback__)
@@ -19,4 +19,4 @@ subscriptions:
19
19
  ACP 协议通道模块,负责接入外部消息通道。
20
20
 
21
21
  - 消息接入 — 通过 ACP 协议接收和发送消息
22
- - 事件通知 — 通过 Event Hub 发布通道状态事件
22
+ - 事件通知 — 通过 Kernel 发布通道状态事件