orchestrator/
July 22, 2026 ยท View on GitHub
Central routing layer between ingress transports (Telegram/Matrix/API) and CLI execution.
Files
orchestrator/core.py:Orchestratorclass, command dispatch, integrations, thin delegationsorchestrator/lifecycle.py: async factory/startup/shutdown helpersorchestrator/observers.py:ObserverManagerfor all observer lifecycle wiringorchestrator/providers.py:ProviderManager(provider auth/model resolution/metadata)orchestrator/injection.py: shared session injection for inter-agent and task flowsorchestrator/registry.py:CommandRegistry,OrchestratorResultorchestrator/commands.py: slash-command handlersorchestrator/flows.py: normal/streaming/named-session/heartbeat flowsorchestrator/directives.py: leading@...parserorchestrator/hooks.py: message hooks (MAINMEMORY_REMINDER, delegation hints)orchestrator/memory_flush.py: silent pre-compaction memory flush + optionalMAINMEMORY.mdcompactionorchestrator/selectors/*: model/cron/session/task selector modules + selector types
Why it was split
Recent refactors moved startup/provider/observer concerns out of core.py:
- lifecycle logic ->
lifecycle.py - provider/model state ->
providers.py - observer orchestration ->
observers.py - selector UI modules ->
orchestrator/selectors/(transport-agnostic)
Result: smaller core.py with clearer responsibilities.
Startup (Orchestrator.create -> lifecycle.create_orchestrator)
High-level steps:
- resolve paths + set main-agent
DUCTOR_HOME - optional Docker setup + Docker skill resync
- runtime environment injection into workspace rule files
- instantiate
Orchestrator(sessions, CLI service, hook registry, optional memory flusher) - provider auth detection + available-provider update
- initialize Gemini/Antigravity/Codex cache observers
- initialize/start task observers (
Background,Cron,Webhook) +Heartbeat+Cleanup - start rule/skill watcher tasks
- optional API server startup
- start config reloader
Routing entry points
handle_message(key, text)handle_message_streaming(key, text, callbacks...)
Common path:
- clear abort marker for chat
- suspicious input detection (log warning only)
- command registry dispatch
- directive parsing
- normal or streaming flow
is_chat_busy(chat_id, topic_id=None) checks whether a CLI process is running. When topic_id is provided, only processes for that specific topic are considered busy; otherwise any process for the chat qualifies.
OrchestratorResult metadata
OrchestratorResult carries optional metadata fields populated after CLI execution:
model_name: resolved model identifier used for the turntotal_tokens: total token count (input + output)input_tokens: input token countcost_usd: estimated cost in USDduration_ms: wall-clock execution time in milliseconds
Command registry
Registered command handlers:
/new,/reset,/status,/model,/memory,/cron,/diagnose,/upgrade,/sessions,/tasks
/new resets only the currently configured provider bucket for the active SessionKey.
Other provider buckets for the same chat/topic remain intact.
/reset resets the currently active provider bucket for the active SessionKey.
/model never blocks: it always executes immediately (bypasses the sequential queue) and shows current model info if a CLI process is active in the chat.
Runtime main-agent registration:
/agents,/agent_start,/agent_stop,/agent_restart
Not orchestrator-owned:
/stop,/stop_all(middleware/bot layer)
Provider/model management (ProviderManager)
Responsibilities:
- authenticated provider set
- runtime model->provider resolution
- known-model IDs for directive parsing
- API provider metadata for auth responses
Directive resolution supports:
- provider directives (
@codex,@gemini,@claude,@antigravity,@grok) - model directives (
@opus,@flash, cache-backed IDs)
Selector subsystem (orchestrator/selectors/)
Selectors are transport-agnostic and return SelectorResponse with abstract button types.
Modules:
model_selector.py(ms:*)cron_selector.py(crn:*)session_selector.py(nsc:*)task_selector.py(tsc:*)- shared models/utilities in
selectors/models.py,selectors/utils.py
Important model-selector behavior:
- topic-scoped
/modelchanges only the active topic session - non-topic
/modelupdates config defaults (and sub-agentagents.jsonwhen relevant)
Flow behavior (flows.py)
Normal/streaming
- session resolution via
SessionKey - provider-isolated session buckets
- optional new-session system prompt append (
MAINMEMORY.md) - in-flight foreground turn tracking
CompactBoundaryEventmarks a pre-compaction boundary; after a successful user turn the optionalMemoryFlushermay run a silent follow-up flush turn and, when configured, a compaction turn- single automatic recovery retry on SIGKILL/invalid resumed session
- stale-session detection currently covers CLI errors such as
invalid session,session not found, andno conversation found - session update on success
- successful tool-only / empty turns are converted into a neutral visible status line instead of disappearing
Named session flow
- named session registry lookup/resume
- foreground follow-up support (
@name <msg>)
Heartbeat flow
- read-only session lookup
- provider match + cooldown checks
- ACK-token suppression
Injection paths (injection.py)
Shared helper _inject_prompt(...) is used by:
handle_async_interagent_result(...)handle_task_result(...)handle_task_question(...)- public
inject_prompt(...)on orchestrator
All injection paths respect topic_id when provided.
Inter-agent ingress also lives in injection.py:
- deterministic named sessions use scoped
ia.<sender-slug>.t<topic>.x<hash>names, or legacyia-<sender>without source context - if the provider CLI rejects a stored inter-agent session after an update/cache clear, the orchestrator ends that named session, retries once with a fresh session, and surfaces a visible recovery notice back to the caller
Observer and bus wiring
Orchestrator.wire_observers_to_bus(bus, wake_handler=...):
- delegates to
ObserverManager.wire_to_bus(...) - sets bus injector to orchestrator
- replaces old per-observer setter scatter
Observer manager owns lifecycle for:
- background, cron, webhook, heartbeat, cleanup
- Gemini/Antigravity/Codex cache observers
- config reloader
- rule sync watcher
- skill sync watcher
Config hot-reload impact
_on_config_hot_reload(...) updates orchestrator-owned runtime services for hot fields:
- CLI defaults (
model,provider,max_turns,max_budget_usd,permission_mode,reasoning_effort, provider CLI args) - provider known-model cache refresh when
modelchanges - i18n re-init when
languagechanges - heartbeat observer start/stop when
heartbeat.enabledflips - external bot callback for transport-owned hot fields such as auth/group updates
ConfigReloader classifies additional top-level fields as hot globally, but fields such as notifications, transcription, and memory-maintenance settings still require restart today because they are not emitted as orchestrator hot updates.
Restart-required fields are surfaced via reloader callback logging.
API integration
lifecycle.start_api_server(...):
- generates API token when missing
- computes default chat fallback from
api.chat_idor first allowed user - wires streaming message handler, abort handler, file context, provider metadata, active-state getter
ApiServer supports auth payload channel_id -> topic-aware SessionKey.
Shutdown
lifecycle.shutdown(...) performs:
- kill active CLI processes
- stop API server (if running)
- cleanup managed skill links
- stop observers/reloader/cache/watchers
- Docker teardown (if enabled)