Summary
Refactor the three agent orchestrators (Interactive, Cron, Inbox) to share a common stateless, event-driven architecture with a unified ToolRouter abstraction.
Context
Currently there are 3 separate orchestrators with duplicated patterns:
| Orchestrator |
File |
Mode |
Tool Execution |
BudAgentOrchestrator |
orchestrator.py |
INTERACTIVE |
Background thread + Redis BLPOP |
CronAgentOrchestrator |
cron_orchestrator.py |
CRON |
Celery task + suspend/resume to DB |
InboxAgentOrchestrator |
inbox_orchestrator.py |
INBOX |
Celery task, no local tools |
Each has its own:
- Tool creation and handler logic (duplicated emit → approve → execute → persist pattern)
- LLM call orchestration
- Message history construction
- Error handling
Proposed Changes
1. Shared ToolRouter + ToolExecutor abstraction
tool_router = ToolRouter([
LocalToolExecutor(mode), # INTERACTIVE: Socket.IO, CRON: suspend, INBOX: disabled
RemoteToolExecutor(), # Same for all modes
ConnectorToolExecutor(), # Same for all modes
InboxToolExecutor(), # INBOX only
])
The ToolRouter handles the common pattern that's currently duplicated:
- Emit
tool:start
- Persist to DB
- Check approval (auto-approve for CRON/INBOX)
- Dispatch to appropriate executor
- Emit
tool:delta with result
- Update DB
2. Shared stateless _run_llm_turn
All three modes should use the same core LLM turn logic:
- Load messages from DB
- Call LLM (streaming for INTERACTIVE, non-streaming for CRON/INBOX)
- Handle tool calls via
ToolRouter
- Persist results
3. Mode-specific behavior via configuration, not separate classes
@dataclass
class OrchestratorConfig:
mode: AgentExecutionMode
streaming: bool # True for INTERACTIVE
auto_approve: bool # True for CRON/INBOX
local_tools_enabled: bool # True for INTERACTIVE/CRON
local_tool_strategy: str # "socket_io" | "suspend" | "disabled"
available_tool_sets: list[str] # ["local", "remote", "connector", "inbox", ...]
Depends On
- Socket.IO migration for Interactive mode (see
plans/agent-websocket-protocol.md and plans/agent-websocket-implementation.md)
Key Files
backend/onyx/agents/bud_agent/orchestrator.py
backend/onyx/agents/bud_agent/cron_orchestrator.py
backend/onyx/agents/bud_agent/inbox_orchestrator.py
backend/onyx/agents/bud_agent/agent_context.py
backend/onyx/agents/bud_agent/local_tool_bridge.py
backend/onyx/agents/bud_agent/tool_definitions.py
Benefits
- Less code duplication — shared tool lifecycle, LLM turn logic, error handling
- Easier to add new execution modes — just configure, don't write a new orchestrator
- Consistent behavior — tool persistence, approval, error handling works the same everywhere
- Simpler testing — test ToolRouter once, not per-orchestrator
Summary
Refactor the three agent orchestrators (Interactive, Cron, Inbox) to share a common stateless, event-driven architecture with a unified
ToolRouterabstraction.Context
Currently there are 3 separate orchestrators with duplicated patterns:
BudAgentOrchestratororchestrator.pyCronAgentOrchestratorcron_orchestrator.pyInboxAgentOrchestratorinbox_orchestrator.pyEach has its own:
Proposed Changes
1. Shared
ToolRouter+ToolExecutorabstractionThe
ToolRouterhandles the common pattern that's currently duplicated:tool:starttool:deltawith result2. Shared stateless
_run_llm_turnAll three modes should use the same core LLM turn logic:
ToolRouter3. Mode-specific behavior via configuration, not separate classes
Depends On
plans/agent-websocket-protocol.mdandplans/agent-websocket-implementation.md)Key Files
backend/onyx/agents/bud_agent/orchestrator.pybackend/onyx/agents/bud_agent/cron_orchestrator.pybackend/onyx/agents/bud_agent/inbox_orchestrator.pybackend/onyx/agents/bud_agent/agent_context.pybackend/onyx/agents/bud_agent/local_tool_bridge.pybackend/onyx/agents/bud_agent/tool_definitions.pyBenefits