multiagent/
April 21, 2026 ยท View on GitHub
Multi-agent runtime: run multiple independent ductor agents in one process.
Files
multiagent/supervisor.py:AgentSupervisorlifecycle, health, crash recovery, agents watchermultiagent/stack.py:AgentStackcontainer (config + bot + orchestrator)multiagent/bus.py:InterAgentBus(sync + async inter-agent messaging)multiagent/internal_api.py: localhost HTTP bridge for tool scripts (/interagent/*,/tasks/*)multiagent/models.py:SubAgentConfig, merge helpersmultiagent/registry.py:agents.jsonread/writemultiagent/health.py: per-agent health modelmultiagent/shared_knowledge.py: shared knowledge sync (SHAREDMEMORY.md)multiagent/commands.py: Telegram commands (/agents,/agent_start,/agent_stop,/agent_restart)
Runtime model
AgentSupervisor
+-- AgentStack "main"
+-- AgentStack "sub-*" (0..n)
+-- InterAgentBus
+-- InternalAgentAPI (localhost bridge)
+-- optional TaskHub (shared)
+-- SharedKnowledgeSync
+-- FileWatcher(agents.json)
Each stack is isolated (token/workspace/sessions), but shares process/event-loop infrastructure.
Startup sequence (AgentSupervisor.start)
- start inter-agent bus
- start internal API
- optional shared task hub
- create/start main stack
- wait for main readiness
- start sub-agents from
agents.json - start shared knowledge sync
- start
agents.jsonwatcher
Dynamic agent changes
Watcher polls agents.json every 5s.
- added entry -> start sub-agent
- removed entry -> stop sub-agent
- restart triggers for running sub-agents:
transportchanged- Telegram identity changed (
telegram_token) - Matrix identity changed (
matrix.homeserverormatrix.user_id)
- other field changes do not auto-restart running agent
Crash/restart policy
Per agent _supervised_run behavior:
- clean exit -> stop
- exit code
42:- main -> propagate full process/service restart
- sub-agent -> in-process hot-reload
- crash -> exponential backoff retries (5 attempts max), then mark
crashed
Sub-agent config (agents.json)
Minimal entry:
{
"name": "coder",
"telegram_token": "123456:ABC...",
"allowed_user_ids": [12345678],
"allowed_group_ids": [],
"provider": "codex",
"model": "o3"
}
Merge behavior:
- base: main
AgentConfig - override: non-null
SubAgentConfigfields - always forced:
ductor_home=~/.ductor/agents/<name>/- sub-agent
telegram_token,allowed_user_ids,allowed_group_ids api.enabled=falseunless explicitly provided for sub-agent
Shared vs isolated
Isolated per sub-agent:
- transport credentials and auth (Telegram token or Matrix account)
- workspace and files under
~/.ductor/agents/<name>/ sessions.json,named_sessions.json, cron/webhook state
Shared across process:
InterAgentBusInternalAgentAPI- optional shared
TaskHub - central log file (
~/.ductor/logs/agent.log) - shared knowledge source (
~/.ductor/SHAREDMEMORY.md)
Inter-agent communication
In-memory bus
- sync: waits for target response (
send) - async: returns task ID immediately (
send_async)
Recipient processing uses deterministic named session ia-<sender>.
Async send metadata currently supports:
new_sessionsummarychat_id/topic_idfor result routing back to the correct Telegram topicreply_toto deliver the final async result to a specific agent instead of the sender fieldsilentto suppress the recipient-side "async task received" notification
Provider-switch safeguard:
- if recipient provider changed since prior
ia-<sender>session, old session is ended and recreated - provider-switch notice is surfaced back to sender side
- when
reply_tois set, async result handler lookup uses that agent name instead ofsender - recipient notifications are skipped when
silent=trueorreply_tois set
Local HTTP bridge for tool scripts
InternalAgentAPI runs on config.interagent_port (default 8799):
- host mode:
127.0.0.1:<port> - Docker mode:
0.0.0.0:<port>
Inter-agent endpoints:
POST /interagent/sendPOST /interagent/send_asyncGET /interagent/agentsGET /interagent/health
POST /interagent/send_async accepts the same routing fields as AsyncSendOptions, including reply_to and silent.
Task endpoints (shared hub):
POST /tasks/createPOST /tasks/resumePOST /tasks/ask_parentGET /tasks/listPOST /tasks/cancelPOST /tasks/delete
Ownership checks apply for resume/cancel/delete when from=<agent> is present.
POST /tasks/create also accepts priority=interactive|background|batch.
TaskHub integration
When enabled, supervisor wires each stack into shared TaskHub:
- per-agent CLI service
- per-agent paths (
tasks_dir) - task result callback
- task question callback
- agent primary chat ID mapping
This enables task submission from any agent while preserving owner routing.
Priority behavior is shared across agents:
interactivebypasses the per-chat concurrency capbackgroundis the defaultbatchkeeps the same cap semantics asbackgroundbut documents low-urgency work explicitly
Shared knowledge sync
SharedKnowledgeSync watches ~/.ductor/SHAREDMEMORY.md and mirrors content into each agent's MAINMEMORY.md block.
Legacy HTML marker format is migrated to current block markers when rewritten.
Chat and CLI commands
Main-agent chat commands:
- Telegram:
/agents,/agent_start <name>,/agent_stop <name>,/agent_restart <name> - Matrix:
!agents,!agent_start <name>,!agent_stop <name>,!agent_restart <name>(/prefix also works)
CLI:
ductor agentsductor agents listductor agents add <name>ductor agents remove <name>
ductor agents list fetches live health from internal API when main bot is running.
Important CLI nuance:
ductor agents add <name>currently prompts for Telegram token/user/group data only.- Matrix sub-agents are supported by the runtime and merge logic, but are created via manual
agents.jsonediting or the bundledcreate_agent.py --transport matrixtool script.