This file gives coding agents a current map of the repository.
ductor is a multi-transport chat orchestrator for the official provider CLIs (claude, codex, gemini).
It runs Telegram and/or Matrix, can expose an optional direct WebSocket API, keeps state under ~/.ductor, and supervises the main agent plus optional sub-agents in one asyncio process.
Stack:
- Python 3.11+
- aiogram 3.x (Telegram)
- matrix-nio (Matrix, optional extra)
- aiohttp (webhook server, internal API, optional direct API)
- Pydantic 2.x
- asyncio
# Setup
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
# Run
ductor
ductor -v
# Tests
pytest
pytest -k "pattern"
# Quality
ruff format .
ruff check .
mypy ductor_botTelegram:
Update -> AuthMiddleware -> SequentialMiddleware -> TelegramBot
-> Orchestrator -> CLIService -> provider subprocess -> Telegram delivery
Matrix:
sync event -> MatrixBot auth/room checks -> Orchestrator
-> CLIService -> provider subprocess -> Matrix delivery
API (optional):
/ws auth (token + e2e_pk + optional chat_id/channel_id)
-> encrypted frames -> Orchestrator streaming -> encrypted result events
Background and async delivery:
Observer / TaskHub / InterAgentBus callback
-> bus.adapters -> Envelope -> MessageBus
-> optional shared lock + optional session injection
-> registered transport adapters (TelegramTransport / MatrixTransport)
| Module | Purpose |
|---|---|
cli_commands/ |
CLI command implementations (service, docker, api, agents, lifecycle, install, status) |
messenger/ |
transport protocol, capabilities, notifications, registry, multi-transport adapter |
messenger/telegram/ |
Telegram transport: middleware, handlers, startup, callback routing, file/media UX |
messenger/matrix/ |
Matrix transport: sync loop, auth, segment streaming, reaction buttons, media |
orchestrator/ |
command routing, directives/hooks, flows, provider/session/task wiring, lifecycle split |
bus/ |
unified Envelope, MessageBus, shared LockPool, delivery adapters |
cli/ |
provider wrappers, stream parsing, auth detection, model caches, process registry |
session/ |
SessionKey(transport, chat_id, topic_id), provider-isolated session buckets, named sessions |
tasks/ |
delegated background task runtime (TaskHub) and persistent registry |
background/ |
named background session execution for /session |
multiagent/ |
supervisor, inter-agent bus, internal localhost API bridge, shared knowledge sync |
api/ |
optional direct WebSocket API and authenticated file endpoints |
cron/, webhook/, heartbeat/, cleanup/ |
in-process automation observers |
workspace/ |
~/.ductor path model, seeding, rule deployment/sync, skill sync |
infra/ |
PID lock, service backends, Docker manager, restart/update/recovery helpers |
files/ |
shared file/path safety, MIME detection, image processing (image_processor.py: resize/convert incoming images) |
security/, text/ |
prompt safety, formatting helpers |
DuctorPathsinworkspace/paths.pyis the single source of truth for runtime paths.- Session identity is
SessionKey(transport, chat_id, topic_id)across Telegram chats/topics, Matrix rooms (mapped int), and API channel isolation. /newresets only the active provider bucket for the active session key.MessageBusis the single async delivery path for observers, task callbacks, webhook wake results, and async inter-agent responses. Delivery is transport-aware: UNICAST envelopes route to the matching transport only, with cascading fallback when the target transport is unavailable.- Telegram ingress and
MessageBusshare oneLockPool;ApiServercurrently uses its own lock pool. - Workspace init is zone-based:
- Zone 2 overwrite:
CLAUDE.md,AGENTS.md,GEMINI.md, framework-managed tool scripts - Zone 3 seed-once: user-owned files
- Zone 2 overwrite:
- Rule sync is mtime-based for sibling
CLAUDE.md/AGENTS.md/GEMINI.md; cron task folders additionally get missing rule backfill. - Skill sync spans
~/.ductor/workspace/skills,~/.claude/skills,~/.codex/skills,~/.gemini/skills:- normal mode: links/junctions
- Docker mode: managed copies (
.ductor_managed)
ductor agents addis a Telegram-focused scaffold; Matrix sub-agents are supported throughagents.jsonor the bundled agent tool scripts.
All run as in-process asyncio tasks:
BackgroundObserver(named sessions)CronObserver(results route UNICAST when job haschat_id, BROADCAST otherwise)WebhookObserverHeartbeatObserver(supportsgroup_targetswith per-target overrides)CleanupObserverCodexCacheObserverGeminiCacheObserver- config reloader
- rule sync watcher
- skill sync watcher
- update observer (upgradeable installs)
Platform dispatch lives in infra/service.py:
- Linux: systemd user service (
infra/service_linux.py) - macOS: launchd Launch Agent (
infra/service_macos.py) - Windows: Task Scheduler (
infra/service_windows.py)
Operational notes:
- onboarding offers service install when a backend is available
stop_bot()stops the installed service first so it does not immediately respawn the processductor service logsbehavior:- Linux:
journalctl --user -u ductor -f - macOS/Windows: recent lines from
~/.ductor/logs/agent.log(fallback newest*.log)
- Linux:
Core:
ductor,ductor onboarding,ductor resetductor status,ductor stop,ductor restart,ductor upgrade,ductor uninstall
Groups:
ductor service <install|status|start|stop|logs|uninstall>ductor docker <rebuild|enable|disable|mount|unmount|mounts|extras|extras-add|extras-remove>ductor api <enable|disable>ductor agents <list|add|remove>ductor install <matrix|api>
Nuances:
ductor agents addinteractively scaffolds Telegram sub-agents only- Matrix sub-agents are still first-class at runtime; define them in
agents.jsonor via the bundled agent tools
config/config.json.envsessions.jsonnamed_sessions.jsontasks.jsoncron_jobs.jsonwebhooks.jsonagents.jsonstartup_state.jsoninflight_turns.jsonchat_activity.jsonSHAREDMEMORY.mdlogs/agent.logworkspace/
asyncio_mode = "auto"in tests- line length 100
- mypy strict mode
- ruff strict lint profile
- config deep-merge adds new defaults without dropping user keys
- supervisor restart code is
42