| """AURA daemon entry point.""" |
|
|
| from __future__ import annotations |
|
|
| import argparse |
| import asyncio |
| import json |
| import os |
| import signal |
| import sys |
| from contextlib import suppress |
| from dataclasses import dataclass |
| from pathlib import Path |
| from typing import Any |
|
|
| if __package__ in {None, ""}: |
| sys.path.insert(0, str(Path(__file__).resolve().parents[1])) |
|
|
| try: |
| from .core.agent_loop import ReActAgentLoop |
| from .core.config import AppConfig, load_config |
| from .core.event_bus import EventBus |
| from .core.hotkey import GlobalHotkeyManager |
| from .core.ipc import UnixSocketServer |
| from .core.llm_router import OllamaRouter |
| from .core.logging import configure_logging, get_logger |
| from .core.tools import ToolRegistry, ToolSpec, get_tool_registry |
| from .core.tray import TrayController |
| from .core.router.quota_tracker import QuotaTracker |
| from .core.router.smart_router import SmartRouter |
| from .core.multiagent.orchestrator import NexusOrchestrator |
| from .core.multiagent.dispatcher import A2ADispatcher |
| from .core.multiagent.registry import AgentRegistry |
| from .agents.atlas.tools import register_atlas_tools, set_config as set_atlas_config, set_event_bus as set_atlas_event_bus |
| from .agents.logos.tools import register_logos_tools, set_router as set_logos_router |
| from .agents.echo.tools import register_echo_tools, set_config as set_echo_config |
| from .memory import set_config as set_mneme_config, set_router as set_mneme_router |
| from .agents.aegis.tools import register_aegis_tools, set_config as set_aegis_config, set_event_bus as set_aegis_event_bus |
| from .agents.director.tools import register_director_tools, set_config as set_director_config, set_event_bus as set_director_event_bus, set_router as set_director_router, resume_interrupted_workflows |
| from .agents.phantom.tools import register_phantom_tools, set_config as set_phantom_config, set_event_bus as set_phantom_event_bus, phantom_loop |
| from .agents.lyra.tools import register_lyra_tools, set_config as set_lyra_config, set_event_bus as set_lyra_event_bus, start_wake_word_listener, stop_wake_word_listener |
| from .agents.stream.tools import register_stream_tools, set_config as set_stream_config, set_router as set_stream_router |
| from .agents.mosaic.tools import register_mosaic_tools, set_config as set_mosaic_config, set_router as set_mosaic_router |
| from .ui.server import configure_runtime as set_ui_runtime, start_server_task as start_ui_server_task |
| except ImportError: |
| from aura.core.agent_loop import ReActAgentLoop |
| from aura.core.config import AppConfig, load_config |
| from aura.core.event_bus import EventBus |
| from aura.core.hotkey import GlobalHotkeyManager |
| from aura.core.ipc import UnixSocketServer |
| from aura.core.llm_router import OllamaRouter |
| from aura.core.logging import configure_logging, get_logger |
| from aura.core.tools import ToolRegistry, ToolSpec, get_tool_registry |
| from aura.core.tray import TrayController |
| from aura.core.router.quota_tracker import QuotaTracker |
| from aura.core.router.smart_router import SmartRouter |
| from aura.core.multiagent.orchestrator import NexusOrchestrator |
| from aura.core.multiagent.dispatcher import A2ADispatcher |
| from aura.core.multiagent.registry import AgentRegistry |
| from aura.agents.atlas.tools import register_atlas_tools, set_config as set_atlas_config, set_event_bus as set_atlas_event_bus |
| from aura.agents.logos.tools import register_logos_tools, set_router as set_logos_router |
| from aura.agents.echo.tools import register_echo_tools, set_config as set_echo_config |
| from aura.memory import set_config as set_mneme_config, set_router as set_mneme_router |
| from aura.agents.aegis.tools import register_aegis_tools, set_config as set_aegis_config, set_event_bus as set_aegis_event_bus |
| from aura.agents.director.tools import register_director_tools, set_config as set_director_config, set_event_bus as set_director_event_bus, set_router as set_director_router, resume_interrupted_workflows |
| from aura.agents.phantom.tools import register_phantom_tools, set_config as set_phantom_config, set_event_bus as set_phantom_event_bus, phantom_loop |
| from aura.agents.lyra.tools import register_lyra_tools, set_config as set_lyra_config, set_event_bus as set_lyra_event_bus, start_wake_word_listener, stop_wake_word_listener |
| from aura.agents.stream.tools import register_stream_tools, set_config as set_stream_config, set_router as set_stream_router |
| from aura.agents.mosaic.tools import register_mosaic_tools, set_config as set_mosaic_config, set_router as set_mosaic_router |
| from aura.ui.server import configure_runtime as set_ui_runtime, start_server_task as start_ui_server_task |
|
|
|
|
| @dataclass(slots=True) |
| class DaemonState: |
| """Container for the running daemon components.""" |
|
|
| config: AppConfig |
| event_bus: EventBus |
| tools: ToolRegistry |
| router: Any |
| agent_loop: ReActAgentLoop |
| ipc_server: UnixSocketServer | None = None |
| hotkey: GlobalHotkeyManager | None = None |
| tray: TrayController | None = None |
| phantom_task: asyncio.Task[None] | None = None |
| ui_task: asyncio.Task[None] | None = None |
|
|
|
|
| def _default_registry() -> ToolRegistry: |
| registry = ToolRegistry() |
| registry.register( |
| ToolSpec( |
| name="system_status", |
| description="Return a basic daemon health snapshot.", |
| tier=1, |
| arguments_schema={"type": "object", "properties": {}, "additionalProperties": False}, |
| return_schema={"type": "object"}, |
| handler=lambda _args: {"status": "ok"}, |
| ) |
| ) |
| return registry |
|
|
|
|
| async def bootstrap(config_path: str | Path | None = None) -> DaemonState: |
| """Load configuration and initialize daemon subsystems.""" |
|
|
| config = load_config(config_path) |
| configure_logging(config.log_level) |
| logger = get_logger(__name__, component="daemon") |
| logger.info("bootstrapping", extra={"event": "bootstrap", "config": str(config.source_path)}) |
| event_bus = EventBus() |
| registry = get_tool_registry() |
| try: |
| registry.register( |
| ToolSpec( |
| name="system_status", |
| description="Return a basic daemon health snapshot.", |
| tier=1, |
| arguments_schema={"type": "object", "properties": {}, "additionalProperties": False}, |
| return_schema={"type": "object"}, |
| handler=lambda _args: {"status": "ok"}, |
| ) |
| ) |
| except ValueError: |
| logger.debug("system-status-tool-already-registered") |
| orchestrator = None |
| router: Any |
| if getattr(config, "router", None) is not None: |
| quota_db = Path(str(config.router.quota_db).replace("{data_path}", str(config.paths.data_dir))) |
| smart_router = SmartRouter(QuotaTracker(quota_db), event_bus=event_bus) |
| router = smart_router |
| orchestrator = NexusOrchestrator(smart_router, A2ADispatcher(AgentRegistry()), AgentRegistry()) |
| else: |
| router = OllamaRouter(model=config.primary_model.name, host=config.primary_model.host) |
| agent_loop = ReActAgentLoop(router=router, registry=registry, event_bus=event_bus, orchestrator=orchestrator) |
| set_atlas_config(config) |
| set_atlas_event_bus(event_bus) |
| set_logos_router(router) |
| set_echo_config(config) |
| set_mneme_config(config) |
| set_mneme_router(router) |
| set_aegis_config(config) |
| set_aegis_event_bus(event_bus) |
| set_director_config(config) |
| set_director_event_bus(event_bus) |
| set_director_router(router) |
| set_phantom_config(config) |
| set_phantom_event_bus(event_bus) |
| set_lyra_config(config) |
| set_lyra_event_bus(event_bus) |
| set_stream_config(config) |
| set_stream_router(router) |
| set_mosaic_config(config) |
| set_mosaic_router(router) |
| register_atlas_tools() |
| register_logos_tools() |
| register_echo_tools() |
| register_aegis_tools() |
| register_director_tools() |
| register_phantom_tools() |
| register_lyra_tools() |
| register_stream_tools() |
| register_mosaic_tools() |
| resume_interrupted_workflows() |
| set_ui_runtime(config, event_bus, agent_loop, orchestrator=orchestrator) |
| hf_space = bool(os.environ.get("HF_SPACE")) |
| ipc_server = UnixSocketServer(config.paths.ipc_socket) if config.features.ipc else None |
| hotkey = GlobalHotkeyManager() if config.features.hotkey and not hf_space else None |
| tray = TrayController() if config.features.tray and not hf_space else None |
| return DaemonState( |
| config=config, |
| event_bus=event_bus, |
| tools=registry, |
| router=router, |
| agent_loop=agent_loop, |
| ipc_server=ipc_server, |
| hotkey=hotkey, |
| tray=tray, |
| ) |
|
|
|
|
| async def run_once(config_path: str | Path | None = None, prompt: str = "Bootstrap verification.") -> dict[str, Any]: |
| """Run a single diagnostic agent loop turn.""" |
|
|
| state = await bootstrap(config_path) |
| result = await state.agent_loop.run(prompt) |
| return {"config": str(state.config.source_path), "result": {"ok": result.ok, "answer": result.answer, "error": result.error, "steps": result.steps}} |
|
|
|
|
| async def run_forever(config_path: str | Path | None = None) -> None: |
| """Run the daemon until cancelled.""" |
|
|
| state = await bootstrap(config_path) |
| logger = get_logger(__name__, component="daemon") |
| router = getattr(state, "router", None) |
| if isinstance(router, SmartRouter): |
| tracker = router.quota_tracker |
| provider_status = [ |
| { |
| "name": status.name, |
| "available": status.available, |
| "requests_remaining": status.requests_remaining, |
| "tokens_remaining": status.tokens_remaining, |
| } |
| for status in tracker.get_all_status() |
| ] |
| logger.info("active-provider-status", extra={"providers": provider_status}) |
| else: |
| logger.info("active-router", extra={"router": type(router).__name__ if router is not None else "none"}) |
| phantom_task = asyncio.create_task(phantom_loop()) if not os.environ.get("HF_SPACE") else None |
| ui_task: asyncio.Task[None] | None = None |
| config = getattr(state, "config", None) |
| ui_config = getattr(config, "ui", None) if config is not None else None |
| if ui_config is not None and ui_config.enabled: |
| ui_task = start_ui_server_task() |
| state.ui_task = ui_task |
| lyra_token: str | None = None |
| lyra_config = getattr(config, "lyra", None) if config is not None else None |
| if lyra_config is not None and lyra_config.enabled and not os.environ.get("HF_SPACE"): |
| async def on_wake_word(_topic: str, payload: Any) -> None: |
| transcription = payload.get("transcription", {}) if isinstance(payload, dict) else {} |
| text = str(transcription.get("text", "")) |
| if text: |
| await state.agent_loop.run(text) |
|
|
| lyra_token = await state.event_bus.subscribe("lyra.wake_word_detected", on_wake_word) |
| start_wake_word_listener() |
| if state.ipc_server is not None: |
| await state.ipc_server.start() |
| if state.hotkey is not None: |
| state.hotkey.start() |
| if state.tray is not None: |
| state.tray.start() |
| stop_event = asyncio.Event() |
| loop = asyncio.get_running_loop() |
| for sig in (signal.SIGTERM, signal.SIGINT): |
| try: |
| loop.add_signal_handler(sig, stop_event.set) |
| except Exception: |
| continue |
| try: |
| while not stop_event.is_set(): |
| await asyncio.sleep(1) |
| except asyncio.CancelledError: |
| logger.debug("daemon-cancelled") |
| finally: |
| if phantom_task is not None: |
| phantom_task.cancel() |
| with suppress(asyncio.CancelledError): |
| await phantom_task |
| ui_task = getattr(state, "ui_task", None) |
| if ui_task is not None: |
| ui_task.cancel() |
| with suppress(asyncio.CancelledError): |
| await ui_task |
| if lyra_config is not None and lyra_config.enabled: |
| stop_wake_word_listener() |
| if lyra_token is not None: |
| await state.event_bus.unsubscribe("lyra.wake_word_detected", lyra_token) |
| if state.hotkey is not None: |
| state.hotkey.stop() |
| if state.tray is not None: |
| state.tray.stop() |
| if state.ipc_server is not None: |
| await state.ipc_server.stop() |
|
|
|
|
| def main() -> None: |
| """CLI entry point.""" |
|
|
| parser = argparse.ArgumentParser(description="Start the AURA daemon") |
| parser.add_argument("--config", type=str, default=None, help="Path to config/config.yaml") |
| parser.add_argument("--once", action="store_true", help="Run a single diagnostic cycle and exit") |
| args = parser.parse_args() |
| if args.once: |
| sys.stdout.write(json.dumps(asyncio.run(run_once(args.config)), ensure_ascii=True) + "\n") |
| return |
| asyncio.run(run_forever(args.config)) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|