Skip to content

Unify orchestrator architecture across Interactive, Cron, and Inbox modes #37

@dittops

Description

@dittops

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:

  1. Emit tool:start
  2. Persist to DB
  3. Check approval (auto-approve for CRON/INBOX)
  4. Dispatch to appropriate executor
  5. Emit tool:delta with result
  6. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions