@agentunion/kite 1.0.7 → 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 (100) hide show
  1. package/CHANGELOG.md +208 -0
  2. package/README.md +48 -0
  3. package/cli.js +1 -1
  4. package/extensions/agents/__init__.py +1 -0
  5. package/extensions/agents/assistant/__init__.py +1 -0
  6. package/extensions/agents/assistant/entry.py +329 -0
  7. package/extensions/agents/assistant/module.md +22 -0
  8. package/extensions/agents/assistant/server.py +197 -0
  9. package/extensions/channels/__init__.py +1 -0
  10. package/extensions/channels/acp_channel/__init__.py +1 -0
  11. package/extensions/channels/acp_channel/entry.py +329 -0
  12. package/extensions/channels/acp_channel/module.md +22 -0
  13. package/extensions/channels/acp_channel/server.py +197 -0
  14. package/extensions/event_hub_bench/entry.py +624 -379
  15. package/extensions/event_hub_bench/module.md +2 -1
  16. package/extensions/services/backup/__init__.py +1 -0
  17. package/extensions/services/backup/entry.py +508 -0
  18. package/extensions/services/backup/module.md +22 -0
  19. package/extensions/services/model_service/__init__.py +1 -0
  20. package/extensions/services/model_service/entry.py +508 -0
  21. package/extensions/services/model_service/module.md +22 -0
  22. package/extensions/services/watchdog/entry.py +468 -102
  23. package/extensions/services/watchdog/module.md +3 -0
  24. package/extensions/services/watchdog/monitor.py +170 -69
  25. package/extensions/services/web/__init__.py +1 -0
  26. package/extensions/services/web/config.yaml +149 -0
  27. package/extensions/services/web/entry.py +390 -0
  28. package/extensions/services/web/module.md +24 -0
  29. package/extensions/services/web/routes/__init__.py +1 -0
  30. package/extensions/services/web/routes/routes_call.py +189 -0
  31. package/extensions/services/web/routes/routes_config.py +512 -0
  32. package/extensions/services/web/routes/routes_contacts.py +98 -0
  33. package/extensions/services/web/routes/routes_devlog.py +99 -0
  34. package/extensions/services/web/routes/routes_phone.py +81 -0
  35. package/extensions/services/web/routes/routes_sms.py +48 -0
  36. package/extensions/services/web/routes/routes_stats.py +17 -0
  37. package/extensions/services/web/routes/routes_voicechat.py +554 -0
  38. package/extensions/services/web/routes/schemas.py +216 -0
  39. package/extensions/services/web/server.py +375 -0
  40. package/extensions/services/web/static/css/style.css +1064 -0
  41. package/extensions/services/web/static/index.html +1445 -0
  42. package/extensions/services/web/static/js/app.js +4671 -0
  43. package/extensions/services/web/vendor/__init__.py +1 -0
  44. package/extensions/services/web/vendor/bluetooth/audio.py +348 -0
  45. package/extensions/services/web/vendor/bluetooth/contacts.py +251 -0
  46. package/extensions/services/web/vendor/bluetooth/manager.py +395 -0
  47. package/extensions/services/web/vendor/bluetooth/sms.py +290 -0
  48. package/extensions/services/web/vendor/bluetooth/telephony.py +274 -0
  49. package/extensions/services/web/vendor/config.py +139 -0
  50. package/extensions/services/web/vendor/conversation/asr.py +936 -0
  51. package/extensions/services/web/vendor/conversation/engine.py +548 -0
  52. package/extensions/services/web/vendor/conversation/llm.py +534 -0
  53. package/extensions/services/web/vendor/conversation/mcp_tools.py +190 -0
  54. package/extensions/services/web/vendor/conversation/tts.py +322 -0
  55. package/extensions/services/web/vendor/conversation/vad.py +138 -0
  56. package/extensions/services/web/vendor/storage/__init__.py +1 -0
  57. package/extensions/services/web/vendor/storage/identity.py +312 -0
  58. package/extensions/services/web/vendor/storage/store.py +507 -0
  59. package/extensions/services/web/vendor/task/manager.py +864 -0
  60. package/extensions/services/web/vendor/task/models.py +45 -0
  61. package/extensions/services/web/vendor/task/webhook.py +263 -0
  62. package/extensions/services/web/vendor/tools/registry.py +321 -0
  63. package/kernel/__init__.py +0 -0
  64. package/kernel/entry.py +407 -0
  65. package/{core/event_hub/hub.py → kernel/event_hub.py} +62 -74
  66. package/kernel/module.md +33 -0
  67. package/{core/registry/store.py → kernel/registry_store.py} +23 -8
  68. package/kernel/rpc_router.py +388 -0
  69. package/kernel/server.py +267 -0
  70. package/launcher/__init__.py +10 -0
  71. package/launcher/__main__.py +6 -0
  72. package/launcher/count_lines.py +258 -0
  73. package/launcher/entry.py +1778 -0
  74. package/launcher/logging_setup.py +289 -0
  75. package/{core/launcher → launcher}/module_scanner.py +11 -6
  76. package/launcher/process_manager.py +880 -0
  77. package/main.py +11 -210
  78. package/package.json +6 -9
  79. package/__init__.py +0 -1
  80. package/__main__.py +0 -15
  81. package/core/event_hub/BENCHMARK.md +0 -94
  82. package/core/event_hub/bench.py +0 -459
  83. package/core/event_hub/bench_extreme.py +0 -308
  84. package/core/event_hub/bench_perf.py +0 -350
  85. package/core/event_hub/entry.py +0 -157
  86. package/core/event_hub/module.md +0 -20
  87. package/core/event_hub/server.py +0 -206
  88. package/core/launcher/entry.py +0 -1158
  89. package/core/launcher/process_manager.py +0 -470
  90. package/core/registry/entry.py +0 -110
  91. package/core/registry/module.md +0 -30
  92. package/core/registry/server.py +0 -289
  93. package/extensions/services/watchdog/server.py +0 -167
  94. /package/{core → extensions/services/web/vendor/bluetooth}/__init__.py +0 -0
  95. /package/{core/event_hub → extensions/services/web/vendor/conversation}/__init__.py +0 -0
  96. /package/{core/launcher → extensions/services/web/vendor/task}/__init__.py +0 -0
  97. /package/{core/registry → extensions/services/web/vendor/tools}/__init__.py +0 -0
  98. /package/{core/event_hub → kernel}/dedup.py +0 -0
  99. /package/{core/event_hub → kernel}/router.py +0 -0
  100. /package/{core/launcher → launcher}/module.md +0 -0
@@ -1,289 +0,0 @@
1
- """
2
- Registry HTTP server.
3
- 7 endpoints: /modules, /lookup, /get/{path}, /tokens, /verify, /query, /health.
4
- All endpoints except /health require Bearer token auth.
5
-
6
- Delayed ready mechanism (mechanism 7):
7
- Registry does NOT send module.ready immediately after HTTP starts.
8
- When Event Hub registers (POST /modules with metadata.ws_endpoint),
9
- Registry connects to Event Hub WS and then sends module.ready.
10
- """
11
-
12
- import asyncio
13
- import json
14
- import uuid
15
- from typing import Any
16
-
17
- import websockets
18
- from fastapi import FastAPI, Request, HTTPException
19
- from fastapi.responses import JSONResponse
20
-
21
- from .store import RegistryStore
22
-
23
-
24
- class RegistryServer:
25
- """FastAPI-based Registry HTTP server."""
26
-
27
- def __init__(self, store: RegistryStore, launcher_token: str = "", advertise_ip: str = "127.0.0.1"):
28
- self.store = store
29
- self.launcher_token = launcher_token
30
- self.advertise_ip = advertise_ip
31
- self.port: int = 0 # set by entry.py before uvicorn.run
32
- self.app = self._create_app()
33
- self._ttl_task: asyncio.Task | None = None
34
- # Event Hub WebSocket
35
- self._event_hub_ws_url: str = ""
36
- self._ws: object | None = None
37
- self._ws_task: asyncio.Task | None = None
38
- self._event_hub_connected = False
39
- self._ready_sent = False
40
-
41
- def _extract_token(self, request: Request) -> str:
42
- """Extract Bearer token from Authorization header."""
43
- auth = request.headers.get("Authorization", "")
44
- if auth.startswith("Bearer "):
45
- return auth[7:].strip()
46
- return ""
47
-
48
- def _require_auth(self, request: Request) -> str:
49
- """Verify token, return module_id. Raise 401 on failure."""
50
- token = self._extract_token(request)
51
- module_id = self.store.verify_token(token)
52
- if module_id is None:
53
- raise HTTPException(status_code=401, detail="Invalid or missing token")
54
- return module_id
55
-
56
- def _require_launcher(self, request: Request):
57
- """Verify the caller is Launcher. Raise 403 if not."""
58
- token = self._extract_token(request)
59
- if not self.store.is_launcher(token):
60
- raise HTTPException(status_code=403, detail="Only Launcher may call this endpoint")
61
-
62
- # ── Event Hub connection + delayed ready ──
63
-
64
- async def _try_connect_event_hub(self):
65
- """Event Hub just registered — connect to it and send module.ready."""
66
- if self._event_hub_connected:
67
- return
68
- eh = self.store.modules.get("event_hub")
69
- if not eh:
70
- return
71
- ws_url = (eh.get("metadata") or {}).get("ws_endpoint", "")
72
- if not ws_url:
73
- return
74
- self._event_hub_ws_url = ws_url
75
- if not self._ws_task:
76
- self._ws_task = asyncio.create_task(self._ws_loop())
77
-
78
- async def _ws_loop(self):
79
- """Connect to Event Hub, reconnect on failure."""
80
- while True:
81
- try:
82
- await self._ws_connect()
83
- except asyncio.CancelledError:
84
- return
85
- except Exception as e:
86
- print(f"[registry] Event Hub connection error: {e}")
87
- self._ws = None
88
- self._event_hub_connected = False
89
- await asyncio.sleep(5)
90
-
91
- async def _ws_connect(self):
92
- """Single WebSocket session. On first connect, send module.ready."""
93
- # Use registry's own per-module token to avoid conflicting with Launcher's connection
94
- token = self.store.token_map.get("registry", "") or self.launcher_token
95
- ws_url = f"{self._event_hub_ws_url}?token={token}"
96
- async with websockets.connect(ws_url, ping_interval=None, ping_timeout=None, close_timeout=10) as ws:
97
- self._ws = ws
98
- self._event_hub_connected = True
99
- print("[registry] Connected to Event Hub")
100
-
101
- # Send module.ready on first successful connection (delayed ready mechanism)
102
- if not self._ready_sent:
103
- self._ready_sent = True
104
- await self._send_module_ready()
105
-
106
- async for raw in ws:
107
- try:
108
- msg = json.loads(raw)
109
- except (json.JSONDecodeError, TypeError):
110
- continue
111
- msg_type = msg.get("type", "")
112
- if msg_type == "error":
113
- print(f"[registry] Event Hub error: {msg.get('message')}")
114
-
115
- async def _send_module_ready(self):
116
- """Send module.ready event to Event Hub. Launcher is listening for this."""
117
- from datetime import datetime, timezone
118
- msg = {
119
- "type": "event",
120
- "event_id": str(uuid.uuid4()),
121
- "event": "module.ready",
122
- "source": "registry",
123
- "timestamp": datetime.now(timezone.utc).isoformat(),
124
- "data": {
125
- "module_id": "registry",
126
- "api_endpoint": f"http://{self.advertise_ip}:{self.port}",
127
- },
128
- }
129
- try:
130
- await self._ws.send(json.dumps(msg))
131
- print("[registry] Sent module.ready")
132
- except Exception as e:
133
- print(f"[registry] Failed to send module.ready: {e}")
134
-
135
- async def _publish_event(self, event_type: str, data: dict):
136
- """Publish event to Event Hub. Best-effort, no-op if not connected."""
137
- if not self._ws:
138
- return
139
- from datetime import datetime, timezone
140
- msg = {
141
- "type": "event",
142
- "event_id": str(uuid.uuid4()),
143
- "event": event_type,
144
- "source": "registry",
145
- "timestamp": datetime.now(timezone.utc).isoformat(),
146
- "data": data,
147
- }
148
- try:
149
- await self._ws.send(json.dumps(msg))
150
- except Exception:
151
- pass
152
-
153
- # ── App factory ──
154
-
155
- def _create_app(self) -> FastAPI:
156
- app = FastAPI(title="Kite Registry", docs_url=None, redoc_url=None)
157
- server = self
158
-
159
- @app.on_event("startup")
160
- async def _startup():
161
- server._ttl_task = asyncio.create_task(server._ttl_loop())
162
-
163
- @app.on_event("shutdown")
164
- async def _shutdown():
165
- if server._ttl_task:
166
- server._ttl_task.cancel()
167
- if server._ws_task:
168
- server._ws_task.cancel()
169
- if server._ws:
170
- await server._ws.close()
171
-
172
- # ── 1. POST /modules ──
173
-
174
- @app.post("/modules")
175
- async def modules(request: Request):
176
- caller = server._require_auth(request)
177
- body = await request.json()
178
- action = body.get("action", "")
179
-
180
- if action == "register":
181
- if "module_id" not in body:
182
- raise HTTPException(400, "module_id required")
183
- # Only Launcher or the module itself may register
184
- if caller != "launcher" and caller != body["module_id"]:
185
- raise HTTPException(403, f"Module '{caller}' cannot register as '{body['module_id']}'")
186
- result = server.store.register_module(body)
187
- if result.get("ok"):
188
- mid = body["module_id"]
189
- await server._publish_event("module.registered", {"module_id": mid})
190
- # If Event Hub just registered, connect and send module.ready
191
- ws_endpoint = (body.get("metadata") or {}).get("ws_endpoint")
192
- if ws_endpoint and mid == "event_hub":
193
- await server._try_connect_event_hub()
194
- return result
195
-
196
- elif action == "deregister":
197
- mid = body.get("module_id")
198
- if not mid:
199
- raise HTTPException(400, "module_id required")
200
- if caller != "launcher" and caller != mid:
201
- raise HTTPException(403, f"Module '{caller}' cannot deregister '{mid}'")
202
- result = server.store.deregister_module(mid)
203
- if result.get("ok"):
204
- await server._publish_event("module.unregistered", {"module_id": mid})
205
- return result
206
-
207
- elif action == "heartbeat":
208
- mid = body.get("module_id")
209
- if not mid:
210
- raise HTTPException(400, "module_id required")
211
- if caller != "launcher" and caller != mid:
212
- raise HTTPException(403, f"Module '{caller}' cannot heartbeat for '{mid}'")
213
- result = server.store.heartbeat(mid)
214
- if result.get("ok"):
215
- await server._publish_event("module.heartbeat", {"module_id": mid})
216
- return result
217
-
218
- else:
219
- raise HTTPException(400, f"Unknown action: {action}")
220
-
221
- # ── 2. GET /lookup ──
222
-
223
- @app.get("/lookup")
224
- async def lookup(request: Request, field: str = None, module: str = None, value: str = None):
225
- server._require_auth(request)
226
- return server.store.lookup(field=field, module=module, value=value)
227
-
228
- # ── 3. GET /get/{path} ──
229
-
230
- @app.get("/get/{path:path}")
231
- async def get_by_path(request: Request, path: str):
232
- server._require_auth(request)
233
- val, found = server.store.get_by_path(path)
234
- if not found:
235
- raise HTTPException(404, f"Path not found: {path}")
236
- return val
237
-
238
- # ── 4. POST /tokens ──
239
-
240
- @app.post("/tokens")
241
- async def register_tokens(request: Request):
242
- server._require_launcher(request)
243
- body = await request.json()
244
- server.store.register_tokens(body)
245
- return {"ok": True}
246
-
247
- # ── 5. POST /verify ──
248
-
249
- @app.post("/verify")
250
- async def verify_token(request: Request):
251
- server._require_auth(request)
252
- body = await request.json()
253
- target_token = body.get("token", "")
254
- module_id = server.store.verify_token(target_token)
255
- if module_id:
256
- return {"ok": True, "module_id": module_id}
257
- return {"ok": False}
258
-
259
- # ── 6. POST /query (stub) ──
260
-
261
- @app.post("/query")
262
- async def query(request: Request):
263
- server._require_auth(request)
264
- body = await request.json()
265
- question = body.get("question", "")
266
- return {"ok": False, "error": "LLM query not implemented yet", "question": question}
267
-
268
- # ── 7. GET /health ──
269
-
270
- @app.get("/health")
271
- async def health():
272
- return {
273
- "status": "healthy",
274
- "module_count": len(server.store.modules),
275
- "online_count": sum(
276
- 1 for m in server.store.modules.values()
277
- if m.get("status") == "online"
278
- ),
279
- }
280
-
281
- return app
282
-
283
- async def _ttl_loop(self):
284
- """Periodically check heartbeat TTL and publish offline events."""
285
- while True:
286
- await asyncio.sleep(10)
287
- expired = self.store.check_ttl()
288
- for mid in expired:
289
- await self._publish_event("module.offline", {"module_id": mid})
@@ -1,167 +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._start_time = time.time()
31
- self.app = self._create_app()
32
-
33
- # Wire up publish callback on monitor
34
- self.monitor.publish_event = self._publish_event
35
-
36
- def _create_app(self) -> FastAPI:
37
- app = FastAPI(title="Kite Watchdog", docs_url=None, redoc_url=None)
38
- server = self
39
-
40
- @app.on_event("startup")
41
- async def _startup():
42
- server._monitor_task = asyncio.create_task(server.monitor.run())
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
- server.monitor.stop()
51
- if server._monitor_task:
52
- server._monitor_task.cancel()
53
- if server._heartbeat_task:
54
- server._heartbeat_task.cancel()
55
- if server._ws_task:
56
- server._ws_task.cancel()
57
- if hasattr(server, '_test_task') and server._test_task:
58
- server._test_task.cancel()
59
- if server._ws:
60
- await server._ws.close()
61
-
62
- @app.get("/health")
63
- async def health():
64
- return {
65
- "status": "healthy",
66
- "details": {
67
- "monitored_modules": len(server.monitor.modules),
68
- "event_hub_connected": server._ws is not None,
69
- "uptime_seconds": round(time.time() - server._start_time),
70
- },
71
- }
72
-
73
- @app.get("/status")
74
- async def status():
75
- return server.monitor.get_status()
76
-
77
- return app
78
-
79
- # ── Event Hub WebSocket client ──
80
-
81
- async def _ws_loop(self):
82
- """Connect to Event Hub, subscribe, and listen. Reconnect on failure."""
83
- while True:
84
- try:
85
- await self._ws_connect()
86
- except asyncio.CancelledError:
87
- return
88
- except Exception as e:
89
- print(f"[watchdog] Event Hub connection error: {e}")
90
- self._ws = None
91
- await asyncio.sleep(5) # reconnect delay
92
-
93
- async def _ws_connect(self):
94
- """Single WebSocket session: connect, subscribe, receive loop."""
95
- url = f"{self.event_hub_ws}?token={self.token}"
96
- async with websockets.connect(url, ping_interval=None, ping_timeout=None, close_timeout=10) as ws:
97
- self._ws = ws
98
- print("[watchdog] Connected to Event Hub")
99
-
100
- # Subscribe to module lifecycle events
101
- await ws.send(json.dumps({
102
- "type": "subscribe",
103
- "events": ["module.started", "module.stopped"],
104
- }))
105
-
106
- # Receive loop
107
- async for raw in ws:
108
- try:
109
- msg = json.loads(raw)
110
- except (json.JSONDecodeError, TypeError):
111
- continue
112
-
113
- msg_type = msg.get("type", "")
114
- if msg_type == "event":
115
- await self.monitor.handle_event(msg)
116
- elif msg_type == "ack":
117
- pass # publish confirmed
118
- elif msg_type == "error":
119
- print(f"[watchdog] Event Hub error: {msg.get('message')}")
120
-
121
- async def _publish_event(self, event: dict):
122
- """Publish an event to Event Hub via WebSocket."""
123
- if not self._ws:
124
- return
125
- from datetime import datetime, timezone
126
- msg = {
127
- "type": "event",
128
- "event_id": str(uuid.uuid4()),
129
- "event": event.get("event", ""),
130
- "source": "watchdog",
131
- "timestamp": datetime.now(timezone.utc).isoformat(),
132
- "data": event.get("data", {}),
133
- }
134
- try:
135
- await self._ws.send(json.dumps(msg))
136
- except Exception as e:
137
- print(f"[watchdog] Failed to publish event: {e}")
138
-
139
- # ── Heartbeat to Registry ──
140
-
141
- async def _heartbeat_loop(self):
142
- """Send heartbeat to Registry every 30 seconds."""
143
- while True:
144
- await asyncio.sleep(30)
145
- try:
146
- async with httpx.AsyncClient() as client:
147
- await client.post(
148
- f"{self.monitor.registry_url}/modules",
149
- json={"action": "heartbeat", "module_id": "watchdog"},
150
- headers={"Authorization": f"Bearer {self.monitor.own_token}"},
151
- timeout=5,
152
- )
153
- except Exception:
154
- pass
155
-
156
- async def _test_event_loop(self):
157
- """Publish a test event every 5 seconds."""
158
- from datetime import datetime, timezone
159
- while True:
160
- await asyncio.sleep(5)
161
- await self._publish_event({
162
- "event": "watchdog.test",
163
- "data": {
164
- "message": "heartbeat from watchdog",
165
- "timestamp": datetime.now(timezone.utc).isoformat(),
166
- },
167
- })
File without changes
File without changes
File without changes