← back to dream77r__my-claude-bot

Function bodies 185 total

All specs Real LLM only Function bodies
main function · python · L141-L166 (26 LOC)
src/cli.py
def main() -> None:
    """Точка входа CLI."""
    parser = argparse.ArgumentParser(
        description="Agent Fleet — управление агентами"
    )
    subparsers = parser.add_subparsers(dest="command")

    subparsers.add_parser("create-agent", help="Создать нового агента")
    subparsers.add_parser("list-agents", help="Список всех агентов")
    subparsers.add_parser("validate", help="Проверить конфиги всех агентов")

    args = parser.parse_args()

    if not args.command:
        parser.print_help()
        sys.exit(1)

    root = find_root()
    manager = AgentManager(root)

    if args.command == "create-agent":
        cmd_create_agent(manager)
    elif args.command == "list-agents":
        cmd_list_agents(manager)
    elif args.command == "validate":
        cmd_validate(manager)
RouteResult class · python · L30-L35 (6 LOC)
src/command_router.py
class RouteResult:
    """Результат маршрутизации."""

    handler: CommandHandler_t
    args: str
    is_priority: bool = False
CommandRouter class · python · L38-L114 (77 LOC)
src/command_router.py
class CommandRouter:
    """
    4-уровневый роутер команд.

    Priority-команды (например /stop) выполняются немедленно,
    даже если агент сейчас обрабатывает запрос.
    """

    def __init__(self):
        self._priority: dict[str, CommandHandler_t] = {}
        self._exact: dict[str, CommandHandler_t] = {}
        self._prefix: dict[str, CommandHandler_t] = {}
        self._interceptors: list[
            tuple[Callable[[str], bool], CommandHandler_t]
        ] = []

    def priority(self, command: str, handler: CommandHandler_t) -> None:
        """Зарегистрировать priority-команду (работает даже при занятом агенте)."""
        self._priority[command.lower()] = handler

    def exact(self, command: str, handler: CommandHandler_t) -> None:
        """Зарегистрировать exact-match команду."""
        self._exact[command.lower()] = handler

    def prefix(self, command: str, handler: CommandHandler_t) -> None:
        """Зарегистрировать prefix-команду (Phase 2)."""
        self._p
__init__ method · python · L46-L52 (7 LOC)
src/command_router.py
    def __init__(self):
        self._priority: dict[str, CommandHandler_t] = {}
        self._exact: dict[str, CommandHandler_t] = {}
        self._prefix: dict[str, CommandHandler_t] = {}
        self._interceptors: list[
            tuple[Callable[[str], bool], CommandHandler_t]
        ] = []
priority method · python · L54-L56 (3 LOC)
src/command_router.py
    def priority(self, command: str, handler: CommandHandler_t) -> None:
        """Зарегистрировать priority-команду (работает даже при занятом агенте)."""
        self._priority[command.lower()] = handler
exact method · python · L58-L60 (3 LOC)
src/command_router.py
    def exact(self, command: str, handler: CommandHandler_t) -> None:
        """Зарегистрировать exact-match команду."""
        self._exact[command.lower()] = handler
prefix method · python · L62-L64 (3 LOC)
src/command_router.py
    def prefix(self, command: str, handler: CommandHandler_t) -> None:
        """Зарегистрировать prefix-команду (Phase 2)."""
        self._prefix[command.lower()] = handler
Repobility — the code-quality scanner for AI-generated software · https://repobility.com
interceptor method · python · L66-L70 (5 LOC)
src/command_router.py
    def interceptor(
        self, predicate: Callable[[str], bool], handler: CommandHandler_t
    ) -> None:
        """Зарегистрировать interceptor (Phase 2)."""
        self._interceptors.append((predicate, handler))
route method · python · L72-L114 (43 LOC)
src/command_router.py
    def route(self, text: str) -> RouteResult | None:
        """
        Найти хэндлер для текста команды.

        Returns:
            RouteResult с хэндлером и аргументами, или None если не найден.
        """
        if not text or not text.startswith("/"):
            return None

        # Убрать @botname из команды (/help@mybot → /help)
        parts = text.split(maxsplit=1)
        cmd = parts[0].split("@")[0].lower()
        args = parts[1] if len(parts) > 1 else ""

        # 1. Priority
        if cmd in self._priority:
            return RouteResult(
                handler=self._priority[cmd], args=args, is_priority=True
            )

        # 2. Exact
        if cmd in self._exact:
            return RouteResult(handler=self._exact[cmd], args=args)

        # 3. Prefix (longest match first)
        best_prefix = ""
        best_handler = None
        for pfx, handler in self._prefix.items():
            if cmd.startswith(pfx) and len(pfx) > len(best_prefix):
          
CronJob class · python · L36-L43 (8 LOC)
src/cron.py
class CronJob:
    """Одна cron-задача."""
    name: str
    schedule: str            # cron expression: "min hour day month weekday"
    prompt: str
    model: str = "sonnet"
    notify: bool = True
    allowed_tools: list[str] | None = None
parse_cron_field function · python · L46-L71 (26 LOC)
src/cron.py
def parse_cron_field(field: str, current: int, max_val: int) -> bool:
    """
    Проверить совпадает ли текущее значение с cron-полем.

    Поддерживает: *, N, */N, N-M, N,M,K
    """
    if field == "*":
        return True

    # */N — каждые N
    if field.startswith("*/"):
        step = int(field[2:])
        return current % step == 0

    # N-M — диапазон
    if "-" in field:
        start, end = field.split("-", 1)
        return int(start) <= current <= int(end)

    # N,M,K — список
    if "," in field:
        values = [int(v) for v in field.split(",")]
        return current in values

    # Точное значение
    return current == int(field)
should_run function · python · L74-L99 (26 LOC)
src/cron.py
def should_run(schedule: str, now: datetime | None = None) -> bool:
    """
    Проверить должна ли задача запуститься в текущую минуту.

    Args:
        schedule: cron expression "min hour day month weekday"
        now: текущее время (для тестов)
    """
    if now is None:
        now = datetime.now()

    parts = schedule.strip().split()
    if len(parts) != 5:
        logger.warning(f"Невалидный cron: '{schedule}' (нужно 5 полей)")
        return False

    minute, hour, day, month, weekday = parts

    return (
        parse_cron_field(minute, now.minute, 59)
        and parse_cron_field(hour, now.hour, 23)
        and parse_cron_field(day, now.day, 31)
        and parse_cron_field(month, now.month, 12)
        and parse_cron_field(weekday, now.isoweekday() % 7, 6)
        # isoweekday: Mon=1..Sun=7, cron: Sun=0..Sat=6
    )
load_cron_jobs function · python · L102-L118 (17 LOC)
src/cron.py
def load_cron_jobs(config: dict) -> list[CronJob]:
    """Загрузить cron-задачи из конфига агента."""
    jobs = []
    for item in config.get("cron", []):
        try:
            job = CronJob(
                name=item["name"],
                schedule=item["schedule"],
                prompt=item["prompt"],
                model=item.get("model", "sonnet"),
                notify=item.get("notify", True),
                allowed_tools=item.get("allowed_tools"),
            )
            jobs.append(job)
        except KeyError as e:
            logger.warning(f"Пропущена cron-задача: отсутствует поле {e}")
    return jobs
_execute_job function · python · L121-L179 (59 LOC)
src/cron.py
async def _execute_job(
    job: CronJob,
    agent_dir: str,
    agent_name: str,
    bus: FleetBus | None = None,
    chat_id: int = 0,
) -> None:
    """Выполнить одну cron-задачу."""
    from claude_agent_sdk import (
        AssistantMessage,
        ClaudeAgentOptions,
        ResultMessage,
        TextBlock,
        query,
    )
    from . import memory

    logger.info(f"Cron '{job.name}' запущен для '{agent_name}'")

    memory_path = memory.get_memory_path(agent_dir)

    options = ClaudeAgentOptions(
        model=job.model,
        permission_mode="bypassPermissions",
        cli_path=get_claude_cli_path(),
        cwd=str(memory_path),
    )
    if job.allowed_tools:
        options.allowed_tools = job.allowed_tools

    result_text = ""
    try:
        async for msg in query(prompt=job.prompt, options=options):
            if isinstance(msg, AssistantMessage):
                for block in msg.content:
                    if isinstance(block, TextBlock):
                
cron_loop function · python · L182-L227 (46 LOC)
src/cron.py
async def cron_loop(
    config: dict,
    agent_dir: str,
    agent_name: str,
    bus: FleetBus | None = None,
    chat_id: int = 0,
) -> None:
    """
    Бесконечный цикл проверки cron-задач каждую минуту.

    Args:
        config: полный конфиг агента (из agent.yaml)
        agent_dir: путь к директории агента
        agent_name: имя агента
        bus: шина сообщений
        chat_id: ID чата для уведомлений
    """
    jobs = load_cron_jobs(config)
    if not jobs:
        return

    logger.info(
        f"Cron loop запущен для '{agent_name}': "
        f"{len(jobs)} задач ({', '.join(j.name for j in jobs)})"
    )

    while True:
        try:
            # Спать до начала следующей минуты
            now = time.time()
            sleep_seconds = 60 - (now % 60)
            await asyncio.sleep(sleep_seconds)

            current = datetime.now()
            for job in jobs:
                if should_run(job.schedule, current):
                    # Запустить в отдельной задаче
Want this analysis on your repo? https://repobility.com/scan/
_get_cursor function · python · L33-L39 (7 LOC)
src/dream.py
def _get_cursor(agent_dir: str) -> str | None:
    """Прочитать курсор последнего Dream-цикла (ISO timestamp)."""
    cursor_path = memory.get_memory_path(agent_dir) / CURSOR_FILE
    if cursor_path.exists():
        val = cursor_path.read_text(encoding="utf-8").strip()
        return val if val else None
    return None
_save_cursor function · python · L42-L46 (5 LOC)
src/dream.py
def _save_cursor(agent_dir: str, timestamp: str) -> None:
    """Сохранить курсор."""
    cursor_path = memory.get_memory_path(agent_dir) / CURSOR_FILE
    cursor_path.parent.mkdir(parents=True, exist_ok=True)
    cursor_path.write_text(timestamp, encoding="utf-8")
get_unprocessed_messages function · python · L49-L62 (14 LOC)
src/dream.py
def get_unprocessed_messages(agent_dir: str) -> list[dict]:
    """Получить сообщения после последнего Dream-курсора."""
    cursor = _get_cursor(agent_dir)
    all_msgs = memory.get_recent_messages(agent_dir, limit=500)

    if not cursor:
        return all_msgs

    result = []
    for msg in all_msgs:
        ts = msg.get("timestamp", "")
        if ts > cursor:
            result.append(msg)
    return result
_load_template function · python · L65-L77 (13 LOC)
src/dream.py
def _load_template(agent_dir: str, template_name: str) -> str:
    """Загрузить промпт-шаблон из templates/."""
    path = Path(agent_dir) / "templates" / template_name
    if path.exists():
        return path.read_text(encoding="utf-8")
    # Fallback: простой шаблон
    logger.warning(f"Шаблон {template_name} не найден, используется fallback")
    if "phase1" in template_name:
        return (
            "Извлеки ключевые факты из этих сообщений:\n\n{conversations}\n\n"
            "Ответь в JSON: {{\"facts\": [...], \"summary\": \"...\"}}"
        )
    return "Обнови wiki на основе этих фактов:\n\n{facts_json}"
_call_claude_simple function · python · L80-L104 (25 LOC)
src/dream.py
async def _call_claude_simple(
    prompt: str,
    model: str = "haiku",
    cwd: str | None = None,
) -> str:
    """Простой (не-агентный) вызов Claude для Phase 1."""
    options = ClaudeAgentOptions(
        model=model,
        permission_mode="bypassPermissions",
        cli_path=get_claude_cli_path(),
    )
    if cwd:
        options.cwd = cwd

    result_text = ""
    async for msg in query(prompt=prompt, options=options):
        if isinstance(msg, AssistantMessage):
            for block in msg.content:
                if isinstance(block, TextBlock):
                    result_text += block.text
        elif isinstance(msg, ResultMessage):
            if msg.result and not result_text:
                result_text = msg.result

    return result_text
_call_claude_agent function · python · L107-L134 (28 LOC)
src/dream.py
async def _call_claude_agent(
    prompt: str,
    model: str = "sonnet",
    cwd: str | None = None,
    allowed_tools: list[str] | None = None,
) -> str:
    """Агентный вызов Claude для Phase 2 (с tools)."""
    options = ClaudeAgentOptions(
        model=model,
        permission_mode="bypassPermissions",
        cli_path=get_claude_cli_path(),
    )
    if cwd:
        options.cwd = cwd
    if allowed_tools:
        options.allowed_tools = allowed_tools

    result_text = ""
    async for msg in query(prompt=prompt, options=options):
        if isinstance(msg, AssistantMessage):
            for block in msg.content:
                if isinstance(block, TextBlock):
                    result_text += block.text
        elif isinstance(msg, ResultMessage):
            if msg.result and not result_text:
                result_text = msg.result

    return result_text
dream_cycle function · python · L137-L247 (111 LOC)
src/dream.py
async def dream_cycle(
    agent_dir: str,
    model_phase1: str = "haiku",
    model_phase2: str = "sonnet",
) -> dict:
    """
    Выполнить один Dream-цикл.

    Returns:
        dict с ключами: facts_count, summary, phase1_ok, phase2_ok
    """
    result = {
        "facts_count": 0,
        "summary": "",
        "phase1_ok": False,
        "phase2_ok": False,
    }

    # Получить необработанные сообщения
    messages = get_unprocessed_messages(agent_dir)
    if not messages:
        logger.info("Dream: нет новых сообщений для обработки")
        result["summary"] = "Нет новых сообщений"
        return result

    logger.info(f"Dream: обрабатываю {len(messages)} новых сообщений")

    memory_path = memory.get_memory_path(agent_dir)

    # Подготовить контекст для Phase 1
    conversations_text = "\n".join(
        f"[{m.get('timestamp', '?')}] {m.get('role', '?')}: {m.get('content', '')[:500]}"
        for m in messages
    )

    profile_text = ""
    profile_path = memory_path
_extract_json function · python · L250-L276 (27 LOC)
src/dream.py
def _extract_json(text: str) -> dict | None:
    """Извлечь JSON из текста (может быть в ```json блоке)."""
    import re

    # Попробовать найти JSON в ```json блоке
    match = re.search(r"```json\s*\n(.*?)\n\s*```", text, re.DOTALL)
    if match:
        try:
            return json.loads(match.group(1))
        except json.JSONDecodeError:
            pass

    # Попробовать парсить весь текст
    try:
        return json.loads(text)
    except json.JSONDecodeError:
        pass

    # Попробовать найти первый { ... }
    match = re.search(r"\{.*\}", text, re.DOTALL)
    if match:
        try:
            return json.loads(match.group(0))
        except json.JSONDecodeError:
            pass

    return None
Want fix-PRs on findings? Install Repobility's GitHub App · github.com/apps/repobility-bot
dream_loop function · python · L279-L305 (27 LOC)
src/dream.py
async def dream_loop(
    agent_dir: str,
    interval_hours: float = 2.0,
    model_phase1: str = "haiku",
    model_phase2: str = "sonnet",
) -> None:
    """
    Бесконечный цикл Dream-обработки.

    Запускается как asyncio.Task при старте агента.
    """
    interval_seconds = interval_hours * 3600
    logger.info(
        f"Dream loop запущен для {agent_dir}, "
        f"интервал: {interval_hours}ч"
    )

    while True:
        await asyncio.sleep(interval_seconds)
        try:
            result = await dream_cycle(agent_dir, model_phase1, model_phase2)
            logger.info(f"Dream result: {result}")
        except asyncio.CancelledError:
            logger.info("Dream loop остановлен")
            break
        except Exception as e:
            logger.error(f"Dream loop error: {e}")
download_file function · python · L21-L64 (44 LOC)
src/file_handler.py
async def download_file(bot: Bot, file_id: str, agent_dir: str) -> str:
    """
    Скачать файл из Telegram в raw/files/.

    Args:
        bot: Telegram Bot instance
        file_id: ID файла в Telegram
        agent_dir: путь к директории агента

    Returns:
        Локальный путь к скачанному файлу

    Raises:
        ValueError: если файл слишком большой
        RuntimeError: если не удалось скачать
    """
    files_dir = Path(agent_dir) / "memory" / "raw" / "files"
    files_dir.mkdir(parents=True, exist_ok=True)

    try:
        tg_file = await bot.get_file(file_id)
    except Exception as e:
        raise RuntimeError(f"Не удалось получить файл: {e}") from e

    # Проверка размера
    if tg_file.file_size and tg_file.file_size > MAX_FILE_SIZE:
        raise ValueError(
            f"Файл слишком большой: {tg_file.file_size / 1024 / 1024:.1f}MB "
            f"(макс. {MAX_FILE_SIZE / 1024 / 1024:.0f}MB)"
        )

    # Сформировать имя файла: timestamp_originalname
    o
send_file function · python · L67-L98 (32 LOC)
src/file_handler.py
async def send_file(bot: Bot, chat_id: int, file_path: str) -> None:
    """
    Отправить файл в Telegram чат.

    Args:
        bot: Telegram Bot instance
        chat_id: ID чата
        file_path: путь к локальному файлу

    Raises:
        FileNotFoundError: если файл не найден
        RuntimeError: если не удалось отправить
    """
    path = Path(file_path)

    if not path.exists():
        raise FileNotFoundError(f"Файл не найден: {file_path}")

    file_size = path.stat().st_size
    if file_size > 50 * 1024 * 1024:  # Telegram лимит 50MB для ботов
        raise ValueError(f"Файл слишком большой для отправки: {file_size / 1024 / 1024:.1f}MB")

    try:
        with open(path, "rb") as f:
            await bot.send_document(
                chat_id=chat_id,
                document=f,
                filename=path.name,
            )
        logger.info(f"Файл отправлен: {path.name} → chat {chat_id}")
    except Exception as e:
        raise RuntimeError(f"Не удалось отправи
_call_claude function · python · L29-L56 (28 LOC)
src/heartbeat.py
async def _call_claude(
    prompt: str,
    model: str = "haiku",
    cwd: str | None = None,
    allowed_tools: list[str] | None = None,
) -> str:
    """Вызов Claude (простой или агентный)."""
    options = ClaudeAgentOptions(
        model=model,
        permission_mode="bypassPermissions",
        cli_path=get_claude_cli_path(),
    )
    if cwd:
        options.cwd = cwd
    if allowed_tools:
        options.allowed_tools = allowed_tools

    result_text = ""
    async for msg in query(prompt=prompt, options=options):
        if isinstance(msg, AssistantMessage):
            for block in msg.content:
                if isinstance(block, TextBlock):
                    result_text += block.text
        elif isinstance(msg, ResultMessage):
            if msg.result and not result_text:
                result_text = msg.result

    return result_text
check_heartbeat function · python · L59-L157 (99 LOC)
src/heartbeat.py
async def check_heartbeat(agent_dir: str) -> dict:
    """
    Проверить HEARTBEAT.md и выполнить задачи если есть.

    Returns:
        dict: has_tasks, task_result, should_notify, notification
    """
    result = {
        "has_tasks": False,
        "task_result": "",
        "should_notify": False,
        "notification": "",
    }

    memory_path = memory.get_memory_path(agent_dir)
    heartbeat_path = Path(agent_dir) / "HEARTBEAT.md"

    if not heartbeat_path.exists():
        return result

    content = heartbeat_path.read_text(encoding="utf-8").strip()
    if not content:
        return result

    # ── Шаг 1: Дешёвый вызов — есть ли задачи? ──
    check_prompt = (
        f"Вот содержимое файла HEARTBEAT.md:\n\n{content}\n\n"
        "Есть ли здесь задачи, которые нужно выполнить прямо сейчас?\n"
        "Ответь СТРОГО одним словом: YES или NO"
    )

    try:
        answer = await _call_claude(check_prompt, model="haiku")
    except Exception as e:
        logger.error(
heartbeat_loop function · python · L160-L203 (44 LOC)
src/heartbeat.py
async def heartbeat_loop(
    agent_dir: str,
    agent_name: str,
    bus: FleetBus | None = None,
    chat_id: int = 0,
    interval_minutes: float = 30.0,
) -> None:
    """
    Бесконечный цикл Heartbeat.

    Args:
        agent_dir: путь к директории агента
        agent_name: имя агента
        bus: шина сообщений для отправки уведомлений
        chat_id: ID чата для уведомлений (если нет bus — не используется)
        interval_minutes: интервал проверки
    """
    interval_seconds = interval_minutes * 60
    logger.info(
        f"Heartbeat loop запущен для '{agent_name}', "
        f"интервал: {interval_minutes} мин"
    )

    while True:
        await asyncio.sleep(interval_seconds)
        try:
            result = await check_heartbeat(agent_dir)

            if result["should_notify"] and bus and chat_id:
                msg = FleetMessage(
                    source=f"agent:{agent_name}",
                    target=f"telegram:{agent_name}",
                    content=f
t function · python · L285-L305 (21 LOC)
src/i18n.py
def t(key: str, lang: str | None = None, **kwargs) -> str:
    """
    Получить локализованную строку.

    Args:
        key: ключ строки
        lang: язык ("en" или "ru"), по умолчанию "ru"
        **kwargs: подстановки для format()

    Returns:
        Локализованная строка
    """
    lang = lang or DEFAULT_LANG
    strings = _STRINGS.get(key, {})
    text = strings.get(lang) or strings.get(DEFAULT_LANG, f"[{key}]")
    if kwargs:
        try:
            text = text.format(**kwargs)
        except (KeyError, IndexError):
            pass
    return text
get_claude_cli_path function · python · L10-L37 (28 LOC)
src/__init__.py
def get_claude_cli_path() -> str:
    """
    Найти путь к Claude CLI.

    Приоритет:
    1. Переменная окружения CLAUDE_CLI_PATH
    2. Поиск в PATH через shutil.which()
    3. Fallback: /usr/local/bin/claude
    """
    global _claude_cli_path
    if _claude_cli_path:
        return _claude_cli_path

    # 1. Env var
    env_path = os.environ.get("CLAUDE_CLI_PATH")
    if env_path:
        _claude_cli_path = env_path
        return _claude_cli_path

    # 2. Поиск в PATH
    found = shutil.which("claude")
    if found:
        _claude_cli_path = found
        return _claude_cli_path

    # 3. Fallback
    _claude_cli_path = "/usr/local/bin/claude"
    return _claude_cli_path
Repobility (the analyzer behind this table) · https://repobility.com
find_project_root function · python · L41-L50 (10 LOC)
src/main.py
def find_project_root() -> Path:
    """Найти корень проекта (где лежит agents/)."""
    # Пробуем от текущей директории вверх
    current = Path.cwd()
    for parent in [current] + list(current.parents):
        if (parent / "agents").is_dir():
            return parent
    # Fallback: директория рядом с src/
    src_dir = Path(__file__).parent
    return src_dir.parent
load_agents function · python · L53-L78 (26 LOC)
src/main.py
def load_agents(root: Path) -> list[Agent]:
    """Найти и загрузить всех агентов из agents/*/agent.yaml."""
    agents_dir = root / "agents"
    if not agents_dir.exists():
        logger.error(f"Директория agents/ не найдена в {root}")
        return []

    agents = []
    for agent_yaml in sorted(agents_dir.glob("*/agent.yaml")):
        try:
            agent = Agent(str(agent_yaml))

            # Пропустить агента если bot_token не задан в .env
            if not agent.bot_token or "${" in agent.bot_token:
                logger.warning(
                    f"Агент '{agent.name}' пропущен: "
                    f"bot_token не задан (добавь в .env)"
                )
                continue

            agents.append(agent)
            logger.info(f"Загружен агент: {agent.name} ({agent.display_name})")
        except Exception as e:
            logger.error(f"Ошибка загрузки {agent_yaml}: {e}")

    return agents
FleetRuntime class · python · L81-L252 (172 LOC)
src/main.py
class FleetRuntime:
    """
    Глобальный контекст для управления агентами на лету.

    Позволяет запускать и останавливать агентов без перезапуска платформы.
    """

    def __init__(
        self,
        root: Path,
        bus: FleetBus,
        semaphore: asyncio.Semaphore,
        orchestrator: Orchestrator,
    ):
        self.root = root
        self.bus = bus
        self.semaphore = semaphore
        self.orchestrator = orchestrator
        self.manager = AgentManager(root)

        # Состояние запущенных агентов
        self.agents: dict[str, Agent] = {}
        self.workers: dict[str, AgentWorker] = {}
        self.bridges: dict[str, TelegramBridge] = {}
        self.tasks: dict[str, list[asyncio.Task]] = {}

    def register_running(
        self,
        agent: Agent,
        worker: AgentWorker,
        bridge: TelegramBridge,
        agent_tasks: list[asyncio.Task],
    ) -> None:
        """Зарегистрировать уже запущенного агента."""
        self.agents[agent.name] 
__init__ method · python · L88-L105 (18 LOC)
src/main.py
    def __init__(
        self,
        root: Path,
        bus: FleetBus,
        semaphore: asyncio.Semaphore,
        orchestrator: Orchestrator,
    ):
        self.root = root
        self.bus = bus
        self.semaphore = semaphore
        self.orchestrator = orchestrator
        self.manager = AgentManager(root)

        # Состояние запущенных агентов
        self.agents: dict[str, Agent] = {}
        self.workers: dict[str, AgentWorker] = {}
        self.bridges: dict[str, TelegramBridge] = {}
        self.tasks: dict[str, list[asyncio.Task]] = {}
register_running method · python · L107-L118 (12 LOC)
src/main.py
    def register_running(
        self,
        agent: Agent,
        worker: AgentWorker,
        bridge: TelegramBridge,
        agent_tasks: list[asyncio.Task],
    ) -> None:
        """Зарегистрировать уже запущенного агента."""
        self.agents[agent.name] = agent
        self.workers[agent.name] = worker
        self.bridges[agent.name] = bridge
        self.tasks[agent.name] = agent_tasks
is_running method · python · L120-L124 (5 LOC)
src/main.py
    def is_running(self, name: str) -> bool:
        """Проверить, запущен ли агент."""
        return name in self.tasks and any(
            not t.done() for t in self.tasks[name]
        )
running_agents method · python · L126-L128 (3 LOC)
src/main.py
    def running_agents(self) -> list[str]:
        """Список имён запущенных агентов."""
        return [n for n in self.tasks if self.is_running(n)]
start_agent method · python · L130-L214 (85 LOC)
src/main.py
    async def start_agent(self, name: str) -> tuple[bool, str]:
        """
        Запустить агента по имени.

        Returns:
            (ok, message)
        """
        if self.is_running(name):
            return False, f"Агент '{name}' уже запущен"

        agent_yaml = self.root / "agents" / name / "agent.yaml"
        if not agent_yaml.exists():
            return False, f"Агент '{name}' не найден"

        # Перезагрузить .env чтобы подхватить новые токены
        env_file = self.root / ".env"
        if env_file.exists():
            load_dotenv(env_file, override=True)

        try:
            agent = Agent(str(agent_yaml))
        except Exception as e:
            return False, f"Ошибка загрузки агента: {e}"

        if not agent.bot_token or "${" in agent.bot_token:
            return False, f"Токен агента '{name}' не задан в .env"

        # Git memory
        if memory.git_init(agent.agent_dir):
            logger.info(f"Git memory initialized for '{name}'")

       
Repobility — the code-quality scanner for AI-generated software · https://repobility.com
stop_agent method · python · L216-L252 (37 LOC)
src/main.py
    async def stop_agent(self, name: str) -> tuple[bool, str]:
        """
        Остановить агента по имени.

        Returns:
            (ok, message)
        """
        if not self.is_running(name):
            return False, f"Агент '{name}' не запущен"

        # Отменить все задачи
        for task in self.tasks.get(name, []):
            if not task.done():
                task.cancel()

        # Подождать завершения
        for task in self.tasks.get(name, []):
            try:
                await asyncio.wait_for(asyncio.shield(task), timeout=5)
            except (asyncio.CancelledError, asyncio.TimeoutError):
                pass

        # Отписаться от bus
        self.bus.unsubscribe(f"agent:{name}")
        self.bus.unsubscribe(f"telegram:{name}")

        # Удалить из orchestrator
        self.orchestrator.agents.pop(name, None)

        # Очистить
        self.agents.pop(name, None)
        self.workers.pop(name, None)
        self.bridges.pop(name, None)
        
run_bot function · python · L255-L280 (26 LOC)
src/main.py
async def run_bot(bridge: TelegramBridge) -> None:
    """Запустить один Telegram-бот."""
    app = bridge.build_app()
    bus_listener_task = None
    try:
        await app.initialize()
        await app.start()
        await app.updater.start_polling(drop_pending_updates=True)
        logger.info(f"Бот '{bridge.agent.name}' запущен")

        # Запустить bus listener (если bus подключён)
        if bridge.bus:
            bus_listener_task = asyncio.create_task(
                bridge.start_bus_listener(app)
            )

        # Ждём бесконечно (до отмены)
        await asyncio.Event().wait()
    except asyncio.CancelledError:
        logger.info(f"Бот '{bridge.agent.name}' останавливается...")
        if bus_listener_task:
            bus_listener_task.cancel()
    finally:
        await app.updater.stop()
        await app.stop()
        await app.shutdown()
async_main function · python · L283-L415 (133 LOC)
src/main.py
async def async_main() -> None:
    """Главная async функция."""
    root = find_project_root()
    logger.info(f"Корень проекта: {root}")

    # Загрузить .env
    env_file = root / ".env"
    if env_file.exists():
        load_dotenv(env_file)
        logger.info("Загружен .env")
    else:
        logger.warning(
            f".env не найден в {root}. "
            "Скопируй .env.example → .env и заполни токены."
        )

    # Загрузить агентов
    agents = load_agents(root)
    if not agents:
        logger.error("Нет агентов для запуска. Проверь agents/*/agent.yaml")
        sys.exit(1)

    # Глобальный семафор для Claude CLI
    semaphore = asyncio.Semaphore(MAX_CONCURRENT_CLAUDE)

    # Инициализировать git для памяти каждого агента
    for agent in agents:
        if memory.git_init(agent.agent_dir):
            logger.info(f"Git memory initialized for '{agent.name}'")

    # ── MessageBus ──
    bus = FleetBus()
    agents_dict = {a.name: a for a in agents}

    # ── Orches
main function · python · L418-L423 (6 LOC)
src/main.py
def main() -> None:
    """Точка входа (синхронная)."""
    try:
        asyncio.run(async_main())
    except KeyboardInterrupt:
        logger.info("Остановка по Ctrl+C")
get_memory_path function · python · L27-L29 (3 LOC)
src/memory.py
def get_memory_path(agent_dir: str) -> Path:
    """Получить путь к memory/ для агента."""
    return Path(agent_dir) / "memory"
ensure_dirs function · python · L32-L44 (13 LOC)
src/memory.py
def ensure_dirs(agent_dir: str) -> None:
    """Создать все необходимые директории памяти."""
    memory = get_memory_path(agent_dir)
    for subdir in [
        "daily",
        "wiki/entities",
        "wiki/concepts",
        "wiki/synthesis",
        "raw/files",
        "raw/conversations",
        "sessions",
    ]:
        (memory / subdir).mkdir(parents=True, exist_ok=True)
ensure_daily_note function · python · L47-L62 (16 LOC)
src/memory.py
def ensure_daily_note(agent_dir: str, date: datetime | None = None) -> Path:
    """Создать daily note если не существует. Вернуть путь."""
    if date is None:
        date = datetime.now()
    memory = get_memory_path(agent_dir)
    daily_dir = memory / "daily"
    daily_dir.mkdir(parents=True, exist_ok=True)

    filename = date.strftime("%Y-%m-%d") + ".md"
    path = daily_dir / filename

    if not path.exists():
        header = date.strftime("# %Y-%m-%d %A\n\n")
        path.write_text(header, encoding="utf-8")

    return path
log_message function · python · L65-L120 (56 LOC)
src/memory.py
def log_message(
    agent_dir: str,
    role: str,
    content: str,
    files: list[str] | None = None,
    date: datetime | None = None,
) -> None:
    """
    Записать сообщение в daily note + conversations.jsonl.

    Args:
        agent_dir: путь к директории агента (agents/me/)
        role: "user" или "assistant"
        content: текст сообщения
        files: список путей к файлам (опционально)
        date: дата (по умолчанию — сейчас)
    """
    if date is None:
        date = datetime.now()

    ensure_dirs(agent_dir)

    # 1. Запись в daily note
    daily_path = ensure_daily_note(agent_dir, date)
    timestamp = date.strftime("%H:%M")
    prefix = "👤" if role == "user" else "🤖"
    entry = f"\n**{timestamp}** {prefix} {content[:500]}\n"
    if files:
        for f in files:
            entry += f"  📎 {os.path.basename(f)}\n"

    with open(daily_path, "a", encoding="utf-8") as fh:
        fh.write(entry)

    # 2. Запись в conversations.jsonl
    memory = get_memory_path
Want this analysis on your repo? https://repobility.com/scan/
read_context function · python · L123-L154 (32 LOC)
src/memory.py
def read_context(agent_dir: str) -> str:
    """
    Прочитать контекст агента: profile.md + index.md.
    Возвращает строку для system prompt.
    """
    memory = get_memory_path(agent_dir)
    parts = []

    # profile.md
    profile_path = memory / "profile.md"
    if profile_path.exists():
        parts.append("## Профиль пользователя\n")
        parts.append(profile_path.read_text(encoding="utf-8"))

    # index.md
    index_path = memory / "index.md"
    if index_path.exists():
        parts.append("\n## Каталог знаний\n")
        parts.append(index_path.read_text(encoding="utf-8"))

    # Сегодняшняя daily note
    today = datetime.now()
    daily_path = memory / "daily" / f"{today.strftime('%Y-%m-%d')}.md"
    if daily_path.exists():
        text = daily_path.read_text(encoding="utf-8")
        # Ограничить до последних ~8000 символов (хватает на ~100 сообщений)
        if len(text) > 8000:
            text = "...(начало дня обрезано)\n" + text[-8000:]
        parts.append("\n
save_session_id function · python · L157-L163 (7 LOC)
src/memory.py
def save_session_id(agent_dir: str, session_id: str) -> None:
    """Сохранить ID сессии Claude CLI для --resume."""
    memory = get_memory_path(agent_dir)
    session_dir = memory / "sessions"
    session_dir.mkdir(parents=True, exist_ok=True)
    session_file = session_dir / "current_session_id"
    session_file.write_text(session_id, encoding="utf-8")
get_session_id function · python · L166-L173 (8 LOC)
src/memory.py
def get_session_id(agent_dir: str) -> str | None:
    """Прочитать ID текущей сессии. None если нет."""
    memory = get_memory_path(agent_dir)
    session_file = memory / "sessions" / "current_session_id"
    if session_file.exists():
        sid = session_file.read_text(encoding="utf-8").strip()
        return sid if sid else None
    return None
‹ prevpage 2 / 4next ›