@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.
- package/CHANGELOG.md +208 -0
- package/README.md +48 -0
- package/cli.js +1 -1
- package/extensions/agents/assistant/entry.py +30 -81
- package/extensions/agents/assistant/module.md +1 -1
- package/extensions/agents/assistant/server.py +83 -122
- package/extensions/channels/acp_channel/entry.py +30 -81
- package/extensions/channels/acp_channel/module.md +1 -1
- package/extensions/channels/acp_channel/server.py +83 -122
- package/extensions/event_hub_bench/entry.py +81 -121
- package/extensions/services/backup/entry.py +213 -85
- package/extensions/services/model_service/entry.py +213 -85
- package/extensions/services/watchdog/entry.py +513 -460
- package/extensions/services/watchdog/monitor.py +55 -69
- package/extensions/services/web/entry.py +11 -108
- package/extensions/services/web/server.py +120 -77
- package/{core/registry → kernel}/entry.py +65 -37
- package/{core/event_hub/hub.py → kernel/event_hub.py} +61 -81
- package/kernel/module.md +33 -0
- package/{core/registry/store.py → kernel/registry_store.py} +13 -4
- package/kernel/rpc_router.py +388 -0
- package/kernel/server.py +267 -0
- package/launcher/__init__.py +10 -0
- package/launcher/__main__.py +6 -0
- package/launcher/count_lines.py +258 -0
- package/{core/launcher → launcher}/entry.py +693 -767
- package/launcher/logging_setup.py +289 -0
- package/{core/launcher → launcher}/module_scanner.py +11 -6
- package/main.py +11 -350
- package/package.json +6 -9
- package/__init__.py +0 -1
- package/__main__.py +0 -15
- package/core/event_hub/BENCHMARK.md +0 -94
- package/core/event_hub/__init__.py +0 -0
- package/core/event_hub/bench.py +0 -459
- package/core/event_hub/bench_extreme.py +0 -308
- package/core/event_hub/bench_perf.py +0 -350
- package/core/event_hub/entry.py +0 -436
- package/core/event_hub/module.md +0 -20
- package/core/event_hub/server.py +0 -269
- package/core/kite_log.py +0 -241
- package/core/launcher/__init__.py +0 -0
- package/core/registry/__init__.py +0 -0
- package/core/registry/module.md +0 -30
- package/core/registry/server.py +0 -339
- package/extensions/services/backup/server.py +0 -244
- package/extensions/services/model_service/server.py +0 -236
- package/extensions/services/watchdog/server.py +0 -229
- /package/{core → kernel}/__init__.py +0 -0
- /package/{core/event_hub → kernel}/dedup.py +0 -0
- /package/{core/event_hub → kernel}/router.py +0 -0
- /package/{core/launcher → launcher}/module.md +0 -0
- /package/{core/launcher → launcher}/process_manager.py +0 -0
|
@@ -6,15 +6,17 @@ Reads boot_info from stdin, registers to Registry, starts model_service service.
|
|
|
6
6
|
import builtins
|
|
7
7
|
import json
|
|
8
8
|
import os
|
|
9
|
-
import socket
|
|
10
9
|
import sys
|
|
11
10
|
import threading
|
|
12
11
|
import re
|
|
13
12
|
import time
|
|
14
13
|
from datetime import datetime, timezone
|
|
15
14
|
|
|
16
|
-
import
|
|
17
|
-
import
|
|
15
|
+
import asyncio
|
|
16
|
+
import traceback
|
|
17
|
+
import uuid
|
|
18
|
+
|
|
19
|
+
import websockets
|
|
18
20
|
|
|
19
21
|
|
|
20
22
|
# ── Safe stdout/stderr: ignore BrokenPipeError after Launcher closes stdio ──
|
|
@@ -228,7 +230,6 @@ _project_root = os.environ.get("KITE_PROJECT") or os.path.dirname(os.path.dirnam
|
|
|
228
230
|
if _project_root not in sys.path:
|
|
229
231
|
sys.path.insert(0, _project_root)
|
|
230
232
|
|
|
231
|
-
from extensions.services.model_service.server import ModelServiceServer
|
|
232
233
|
|
|
233
234
|
|
|
234
235
|
def _fmt_elapsed(t0: float) -> str:
|
|
@@ -240,62 +241,32 @@ def _fmt_elapsed(t0: float) -> str:
|
|
|
240
241
|
return f"{d:.0f}s"
|
|
241
242
|
|
|
242
243
|
|
|
243
|
-
def
|
|
244
|
-
|
|
245
|
-
|
|
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": "model_service",
|
|
253
|
-
"module_type": "service",
|
|
254
|
-
"name": "Model Service",
|
|
255
|
-
"api_endpoint": f"http://127.0.0.1:{port}",
|
|
256
|
-
"health_endpoint": "/health",
|
|
257
|
-
"events_publish": {
|
|
258
|
-
"model_service.test": {"description": "Test event from model_service 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"[model_service] WARNING: Registry returned {resp.status_code}")
|
|
273
|
-
except Exception as e:
|
|
274
|
-
print(f"[model_service] WARNING: Registry registration failed: {e}")
|
|
275
|
-
|
|
244
|
+
def _read_stdin_kite_message(expected_type: str, timeout: float = 10) -> dict | None:
|
|
245
|
+
"""Read a single kite message of expected type from stdin with timeout."""
|
|
246
|
+
result = [None]
|
|
276
247
|
|
|
277
|
-
def
|
|
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:
|
|
248
|
+
def _read():
|
|
283
249
|
try:
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
val = resp.json()
|
|
290
|
-
if val:
|
|
291
|
-
return val
|
|
250
|
+
line = sys.stdin.readline().strip()
|
|
251
|
+
if line:
|
|
252
|
+
msg = json.loads(line)
|
|
253
|
+
if isinstance(msg, dict) and msg.get("kite") == expected_type:
|
|
254
|
+
result[0] = msg
|
|
292
255
|
except Exception:
|
|
293
256
|
pass
|
|
294
|
-
|
|
295
|
-
|
|
257
|
+
|
|
258
|
+
t = threading.Thread(target=_read, daemon=True)
|
|
259
|
+
t.start()
|
|
260
|
+
t.join(timeout=timeout)
|
|
261
|
+
return result[0]
|
|
296
262
|
|
|
297
263
|
|
|
298
|
-
|
|
264
|
+
# Global WS reference for publish_event callback
|
|
265
|
+
_ws_global = None
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
async def main():
|
|
269
|
+
global _ws_global
|
|
299
270
|
# Initialize log file paths
|
|
300
271
|
global _log_dir, _log_latest_path, _crash_log_path
|
|
301
272
|
module_data = os.environ.get("KITE_MODULE_DATA")
|
|
@@ -334,47 +305,204 @@ def main():
|
|
|
334
305
|
except Exception:
|
|
335
306
|
pass
|
|
336
307
|
|
|
337
|
-
# Read
|
|
338
|
-
|
|
308
|
+
# Read kernel_port: env first (fast path), stdin fallback (parallel start)
|
|
309
|
+
kernel_port = int(os.environ.get("KITE_KERNEL_PORT", "0"))
|
|
310
|
+
if not kernel_port:
|
|
311
|
+
msg = _read_stdin_kite_message("kernel_port", timeout=10)
|
|
312
|
+
if msg:
|
|
313
|
+
kernel_port = int(msg.get("kernel_port", 0))
|
|
339
314
|
|
|
340
|
-
if not token or not
|
|
341
|
-
print("[model_service] ERROR: Missing token or
|
|
315
|
+
if not token or not kernel_port:
|
|
316
|
+
print("[model_service] ERROR: Missing token or kernel_port")
|
|
342
317
|
sys.exit(1)
|
|
343
318
|
|
|
344
|
-
print(f"[model_service] Token received ({len(token)} chars),
|
|
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"[model_service] 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("[model_service] WARNING: Could not discover Event Hub WS, events disabled")
|
|
356
|
-
else:
|
|
357
|
-
print(f"[model_service] Discovered Event Hub: {event_hub_ws}")
|
|
358
|
-
client.close()
|
|
319
|
+
print(f"[model_service] Token received ({len(token)} chars), kernel port: {kernel_port} ({_fmt_elapsed(_t0)})")
|
|
359
320
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
event_hub_ws=event_hub_ws,
|
|
364
|
-
boot_t0=_t0,
|
|
365
|
-
)
|
|
321
|
+
# Connect to Kernel WebSocket
|
|
322
|
+
ws_url = f"ws://127.0.0.1:{kernel_port}/ws?token={token}&id=model_service"
|
|
323
|
+
print(f"[model_service] Connecting to Kernel: {ws_url}")
|
|
366
324
|
|
|
367
|
-
print(f"[model_service] Starting on port {port} ({_fmt_elapsed(_t0)})")
|
|
368
325
|
try:
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
326
|
+
async with websockets.connect(ws_url, open_timeout=5, ping_interval=None, ping_timeout=None, close_timeout=10) as ws:
|
|
327
|
+
_ws_global = ws
|
|
328
|
+
print(f"[model_service] Connected to Kernel ({_fmt_elapsed(_t0)})")
|
|
329
|
+
|
|
330
|
+
# Subscribe to events
|
|
331
|
+
await _rpc_call(ws, "event.subscribe", {
|
|
332
|
+
"events": [
|
|
333
|
+
"module.started",
|
|
334
|
+
"module.stopped",
|
|
335
|
+
"module.shutdown",
|
|
336
|
+
],
|
|
337
|
+
})
|
|
338
|
+
print(f"[model_service] Subscribed to events ({_fmt_elapsed(_t0)})")
|
|
339
|
+
|
|
340
|
+
# Register to Kernel Registry via RPC
|
|
341
|
+
await _rpc_call(ws, "registry.register", {
|
|
342
|
+
"module_id": "model_service",
|
|
343
|
+
"module_type": "service",
|
|
344
|
+
"events_publish": {
|
|
345
|
+
"model_service.test": {"description": "Test event from model_service module"},
|
|
346
|
+
},
|
|
347
|
+
"events_subscribe": [
|
|
348
|
+
"module.started",
|
|
349
|
+
"module.stopped",
|
|
350
|
+
"module.shutdown",
|
|
351
|
+
],
|
|
352
|
+
})
|
|
353
|
+
print(f"[model_service] Registered to Kernel ({_fmt_elapsed(_t0)})")
|
|
354
|
+
|
|
355
|
+
# Publish module.ready
|
|
356
|
+
await _rpc_call(ws, "event.publish", {
|
|
357
|
+
"event_id": str(uuid.uuid4()),
|
|
358
|
+
"event": "module.ready",
|
|
359
|
+
"data": {
|
|
360
|
+
"module_id": "model_service",
|
|
361
|
+
"graceful_shutdown": True,
|
|
362
|
+
},
|
|
363
|
+
})
|
|
364
|
+
print(f"[model_service] module.ready published ({_fmt_elapsed(_t0)})")
|
|
365
|
+
|
|
366
|
+
# Start test event loop in background
|
|
367
|
+
test_task = asyncio.create_task(_test_event_loop(ws))
|
|
368
|
+
|
|
369
|
+
# Message loop: handle incoming RPC + events
|
|
370
|
+
async for raw in ws:
|
|
371
|
+
try:
|
|
372
|
+
msg = json.loads(raw)
|
|
373
|
+
except (json.JSONDecodeError, TypeError):
|
|
374
|
+
continue
|
|
375
|
+
|
|
376
|
+
try:
|
|
377
|
+
has_method = "method" in msg
|
|
378
|
+
has_id = "id" in msg
|
|
379
|
+
|
|
380
|
+
if has_method and not has_id:
|
|
381
|
+
# Event Notification
|
|
382
|
+
await _handle_event_notification(msg)
|
|
383
|
+
elif has_method and has_id:
|
|
384
|
+
# Incoming RPC request
|
|
385
|
+
await _handle_rpc_request(ws, msg)
|
|
386
|
+
# Ignore RPC responses (we don't await them in this simple impl)
|
|
387
|
+
except Exception as e:
|
|
388
|
+
print(f"[model_service] 消息处理异常(已忽略): {e}")
|
|
389
|
+
|
|
373
390
|
except Exception as e:
|
|
374
391
|
_write_crash(type(e), e, e.__traceback__, severity="critical", handled=True)
|
|
375
392
|
_print_crash_summary(type(e), e.__traceback__)
|
|
376
393
|
sys.exit(1)
|
|
377
394
|
|
|
378
395
|
|
|
396
|
+
async def _rpc_call(ws, method: str, params: dict = None):
|
|
397
|
+
"""Send a JSON-RPC 2.0 request (fire-and-forget, no response awaited)."""
|
|
398
|
+
msg = {"jsonrpc": "2.0", "id": str(uuid.uuid4()), "method": method}
|
|
399
|
+
if params:
|
|
400
|
+
msg["params"] = params
|
|
401
|
+
await ws.send(json.dumps(msg))
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
async def _publish_event(ws, event: dict):
|
|
405
|
+
"""Publish an event via RPC event.publish."""
|
|
406
|
+
await _rpc_call(ws, "event.publish", {
|
|
407
|
+
"event_id": str(uuid.uuid4()),
|
|
408
|
+
"event": event.get("event", ""),
|
|
409
|
+
"data": event.get("data", {}),
|
|
410
|
+
})
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
async def _handle_event_notification(msg: dict):
|
|
414
|
+
"""Handle an event notification (JSON-RPC 2.0 Notification with method='event')."""
|
|
415
|
+
params = msg.get("params", {})
|
|
416
|
+
event_type = params.get("event", "")
|
|
417
|
+
data = params.get("data", {})
|
|
418
|
+
|
|
419
|
+
# Special handling for module.shutdown targeting model_service
|
|
420
|
+
if event_type == "module.shutdown" and data.get("module_id") == "model_service":
|
|
421
|
+
await _handle_shutdown()
|
|
422
|
+
return
|
|
423
|
+
|
|
424
|
+
# Log other events
|
|
425
|
+
print(f"[model_service] Event received: {event_type}")
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
async def _handle_rpc_request(ws, msg: dict):
|
|
429
|
+
"""Handle an incoming RPC request (model_service.* methods)."""
|
|
430
|
+
rpc_id = msg.get("id", "")
|
|
431
|
+
method = msg.get("method", "")
|
|
432
|
+
params = msg.get("params", {})
|
|
433
|
+
|
|
434
|
+
handlers = {
|
|
435
|
+
"health": lambda p: _rpc_health(),
|
|
436
|
+
"status": lambda p: _rpc_status(),
|
|
437
|
+
}
|
|
438
|
+
handler = handlers.get(method)
|
|
439
|
+
if handler:
|
|
440
|
+
try:
|
|
441
|
+
result = await handler(params)
|
|
442
|
+
await ws.send(json.dumps({"jsonrpc": "2.0", "id": rpc_id, "result": result}))
|
|
443
|
+
except Exception as e:
|
|
444
|
+
await ws.send(json.dumps({
|
|
445
|
+
"jsonrpc": "2.0", "id": rpc_id,
|
|
446
|
+
"error": {"code": -32603, "message": str(e)},
|
|
447
|
+
}))
|
|
448
|
+
else:
|
|
449
|
+
await ws.send(json.dumps({
|
|
450
|
+
"jsonrpc": "2.0", "id": rpc_id,
|
|
451
|
+
"error": {"code": -32601, "message": f"Method not found: {method}"},
|
|
452
|
+
}))
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
async def _rpc_health() -> dict:
|
|
456
|
+
"""RPC handler for model_service.health."""
|
|
457
|
+
return {
|
|
458
|
+
"status": "healthy",
|
|
459
|
+
"details": {
|
|
460
|
+
"uptime_seconds": round(time.time() - _start_ts),
|
|
461
|
+
},
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
async def _rpc_status() -> dict:
|
|
466
|
+
"""RPC handler for model_service.status."""
|
|
467
|
+
return {
|
|
468
|
+
"module": "model_service",
|
|
469
|
+
"status": "running",
|
|
470
|
+
"uptime_seconds": round(time.time() - _start_ts),
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
async def _handle_shutdown():
|
|
475
|
+
"""Handle module.shutdown event — ack, cleanup, ready, exit."""
|
|
476
|
+
print("[model_service] Received shutdown request")
|
|
477
|
+
# Step 1: Send ack
|
|
478
|
+
await _publish_event(_ws_global, {
|
|
479
|
+
"event": "module.shutdown.ack",
|
|
480
|
+
"data": {"module_id": "model_service", "estimated_cleanup": 2},
|
|
481
|
+
})
|
|
482
|
+
# Step 2: Cleanup (nothing to clean up for model_service)
|
|
483
|
+
# Step 3: Send ready
|
|
484
|
+
await _publish_event(_ws_global, {
|
|
485
|
+
"event": "module.shutdown.ready",
|
|
486
|
+
"data": {"module_id": "model_service"},
|
|
487
|
+
})
|
|
488
|
+
print("[model_service] Shutdown ready, exiting")
|
|
489
|
+
# Step 4: Exit
|
|
490
|
+
sys.exit(0)
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
async def _test_event_loop(ws):
|
|
494
|
+
"""Publish a test event every 10 seconds."""
|
|
495
|
+
while True:
|
|
496
|
+
await asyncio.sleep(10)
|
|
497
|
+
await _publish_event(ws, {
|
|
498
|
+
"event": "model_service.test",
|
|
499
|
+
"data": {
|
|
500
|
+
"message": "test event from model_service",
|
|
501
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
502
|
+
},
|
|
503
|
+
})
|
|
504
|
+
print("[model_service] test event published")
|
|
505
|
+
|
|
506
|
+
|
|
379
507
|
if __name__ == "__main__":
|
|
380
|
-
main()
|
|
508
|
+
asyncio.run(main())
|