Function bodies 185 total
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 = FalseCommandRouter 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()] = handlerexact 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()] = handlerprefix 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()] = handlerRepobility — 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 = Noneparse_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_textdream_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 NoneWant 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
osend_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_textcheck_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=ft 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 textget_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_pathRepobility (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.parentload_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 agentsFleetRuntime 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_tasksis_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}
# ── Orchesmain 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 pathlog_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_pathWant 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("\nsave_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