Engineering Reference · orchestrator/dfgadapter

DFGAdapter — TUI Bridge

DFGAdapter is the current glue layer between the Bubble Tea shell and Zeus. It owns command dispatch, event streaming, and the slash command registry. This page explains what it does, how it is wired, and why it is a temporary structure that should be absorbed into Zeus (issue #62).

What DFGAdapter owns

flowchart LR subgraph TUI["Shell TUI"] Input["keyboard input\nslash commands\nfree text"] end subgraph DFG["DFGAdapter"] Submit["Submit(input)"] Dispatch["dispatchCommand()\nslash commands"] Prompt["dispatchPrompt()\nfree text → LLM"] Registry["CommandRegistry\n/fix /review /startup ..."] Bus["EventBus\nTokenEvent · StatusEvent"] State["StateStore\nTUI display state"] end Zeus["Zeus\nGenerate / Stream"] Input --> Submit Submit -->|/prefix| Dispatch Submit -->|free text| Prompt Dispatch --> Registry Registry --> Bus Prompt --> Zeus Zeus -->|tokens| Bus Bus -->|channel| TUI style TUI fill:#1e2430,color:#D8DEE9,stroke:#4C566A style DFG fill:#2e2535,color:#D8DEE9,stroke:#8b6fc7 style Zeus fill:#1a2535,color:#D8DEE9,stroke:#5E81AC

How input is dispatched

DFGAdapter.Submit(input) is the single entry point for all user input. It serialises concurrent submissions (only one invocation runs at a time) and routes to one of two paths:

func (a *DFGAdapter) Submit(input string) error {
    trimmed := strings.TrimSpace(input)
    if trimmed == "" { return ErrEmptyInput }

    a.mu.Lock()
    if a.status == StatusRunning { return ErrBusy }
    ctx, cancel := context.WithCancel(context.Background())
    a.status = StatusRunning
    a.mu.Unlock()

    go func() {
        if strings.HasPrefix(trimmed, "/") {
            a.dispatchCommand(ctx, trimmed)   // fast, synchronous
        } else {
            a.dispatchPrompt(ctx, trimmed)    // streaming, may take seconds
        }
    }()
    return nil
}

Cancel() signals the in-flight context. Subsequent calls to Submit() return ErrBusy while running — callers must cancel first or wait for the terminal TokenEvent{Done: true}.

ExecutionEvent — what the TUI receives

All output flows over a typed event channel. The TUI subscribes via DFGAdapter.Events() and type-switches on arrival:

Event typeFieldsMeaning
TokenEvent Token string, Done bool, Err error A fragment of the LLM stream. When Done == true, the stream is over. Err may be set on cancellation or provider error.
StatusEvent Message string, Phase string A human-readable notification (slash command result, lifecycle update). When Phase == "prompt" the TUI re-submits the message to the LLM.

Each call to Events() returns a new subscription channel (buffer size 256). Multiple TUI components can subscribe independently. If a subscriber's buffer fills, events are dropped for that subscriber rather than blocking the goroutine.

Slash command dispatch

CommandRegistry maps slash command names and aliases to handler functions. Every handler receives the command arguments and a *StateStore, and returns an ExecutionEvent.

Commands registered in shell/zeus_commands.go:

CommandAliasesWhat it does
/helpLists all registered commands
/providers/llmsShows provider status table
/statsToken usage, cost, local/cloud split
/contextHistory size and compression state
/authAuth status for all providers
/routeActive routing configuration
/llm-orderProvider waterfall in priority order
/fixPhase="prompt" → streams LLM fix for last error
/reviewPhase="prompt" → streams code review
/securityPhase="prompt" → streams security audit
/governPhase="prompt" → streams governance check
/startup/init, /triageGathers git/gh env, builds preflight prompt, streams triage via LLM

Commands that return StatusEvent{Phase: "prompt"} cause the TUI to immediately re-submit the message content as a free-text prompt. This is how slash commands like /fix chain into the LLM stream without requiring the user to type anything.

The case for absorbing DFGAdapter into Zeus

DFGAdapter exists because the TUI needed an async event model and Zeus was not wired for it. The result is two orchestrators: Zeus owns the LLM calls and history; DFGAdapter owns the command dispatch and event fan-out. Neither is a complete control plane.

ProblemConsequence
Zeus's slash command list in buildDynamicContext() is hardcoded Adding a command requires editing two files — the registry and the context builder
Intent classification happens by "/" prefix only Zeus can't distinguish "explain this code" (chat) from "refactor this file" (agentic) — both go straight to LLM
StateStore is separate from Zeus's history TUI display state and LLM conversation state are in different structs with no shared lifecycle
EventBus is owned by DFGAdapter, not Zeus The agentic loop (loopdriver) can't emit events to the TUI without routing through DFGAdapter

Issue #62 documents a 4-phase migration: add Submit/Events/Cancel to Zeus, port the CommandRegistry, port the EventBus, then delete DFGAdapter.