Command Layer (Level-3 System Interface)

November 12, 2025 · View on GitHub

The Command Layer provides atomic, deterministic system operations that bridge device agents with underlying platform capabilities. Each command encapsulates a tool and its parameters, mapping directly to MCP tools on the device client. This layer ensures reliable, auditable, and extensible execution across heterogeneous devices.

Overview

The Command Layer implements Level-3 of the three-layer device agent architecture. It provides:

  • Atomic Commands: Self-contained execution units with tool + parameters
  • MCP Integration: Commands map to Model Context Protocol tools on device client
  • CommandDispatcher: Routes commands from agent server to device client via AIP
  • Deterministic Execution: Same inputs → same outputs, fully auditable
  • Dynamic Discovery: LLM queries available tools and selects appropriate commands
graph TB
    subgraph "Command Layer Architecture"
        Strategy[ProcessingStrategy<br/>Level-2] -->|creates| Commands[List of Commands<br/>tool_name + parameters]
        Commands -->|executes via| Dispatcher[CommandDispatcher]
        
        Dispatcher -->|routes| AIP[AIP Protocol<br/>WebSocket]
        AIP -->|sends| Client[Device Client]
        
        Client -->|dispatches to| MCP[MCP Server Manager]
        MCP -->|invokes| Tool1[MCP Tool 1<br/>click_element]
        MCP -->|invokes| Tool2[MCP Tool 2<br/>type_text]
        MCP -->|invokes| Tool3[MCP Tool 3<br/>run_command]
        
        Tool1 -->|result| MCP
        Tool2 -->|result| MCP
        Tool3 -->|result| MCP
        
        MCP -->|aggregates| Client
        Client -->|returns| AIP
        AIP -->|results| Dispatcher
        Dispatcher -->|List of Results| Strategy
    end

Design Philosophy

The Command Layer follows the Command Pattern:

Command Structure

Design Philosophy

The Command Layer follows the Command Pattern:

  • Encapsulation: Each command encapsulates request as object
  • Decoupling: Invoker (strategy) decoupled from executor (MCP tool)
  • Extensibility: New commands added without changing invoker code
  • Auditability: Command history provides complete execution trace

Command Structure

Each command is represented by the Command Pydantic model:

class Command(BaseModel):
    """
    Represents a command to be executed by an agent.
    Commands are atomic units of work dispatched by the orchestrator.
    """
    
    tool_name: str = Field(..., description="Name of the tool to execute")
    parameters: Optional[Dict[str, Any]] = Field(
        default=None, description="Parameters for the tool"
    )
    tool_type: Literal["data_collection", "action"] = Field(
        ..., description="Type of tool: data_collection or action"
    )
    call_id: Optional[str] = Field(
        default=None, description="Unique identifier for this command call"
    )

Command Properties

PropertyTypePurposeExample
tool_namestrMCP tool name to invoke"click_element", "type_text", "shell_execute"
parametersOptional[Dict[str, Any]]Tool parameters{"control_id": "Button_123"}, {"text": "Hello"}
tool_typeLiteral["data_collection", "action"]Tool category"data_collection" (observation), "action" (modification)
call_idOptional[str]Unique execution identifier"uuid-1234-5678" (auto-generated)

Command Examples

Windows UI Automation Command:

Command(
    tool_name="click_element",
    parameters={
        "control_id": "Button_InsertChart",
        "process_name": "EXCEL.EXE",
        "app_root_name": "Microsoft Excel"
    },
    tool_type="action"
)

Linux Shell Command:

Command(
    tool_name="shell_execute",
    parameters={
        "command": "ls -la /home/user/documents",
        "timeout": 30
    },
    tool_type="action"
)

File Operation Command:

Command(
    tool_name="read_file",
    parameters={
        "file_path": "/home/user/data.csv",
        "encoding": "utf-8"
    },
    tool_type="data_collection"
)

Result Structure

Each command execution returns a Result object:

class Result(BaseModel):
    """
    Represents the result of a command execution.
    Contains status, error information, and the actual result payload.
    """
    
    status: ResultStatus = Field(..., description="Execution status")
    error: Optional[str] = Field(default=None, description="Error message if failed")
    result: Any = Field(default=None, description="Result payload")
    namespace: Optional[str] = Field(
        default=None, description="Namespace of the executed tool"
    )
    call_id: Optional[str] = Field(
        default=None, description="ID matching the Command.call_id"
    )

ResultStatus Enum

class ResultStatus(str, Enum):
    """Represents the status of a command execution result."""
    SUCCESS = "success"      # Command executed successfully
    FAILURE = "failure"      # Command failed with error
    SKIPPED = "skipped"      # Command was skipped
    NONE = "none"            # No result available

Result Examples

Successful Click:

Result(
    status=ResultStatus.SUCCESS,
    result={"clicked": True, "control_name": "Insert Chart"},
    call_id="uuid-1234-5678"
)

Failed Command:

Result(
    status=ResultStatus.FAILURE,
    result=None,
    error="Control 'Button_123' not found in UI tree",
    call_id="uuid-1234-5678"
)

Shell Execution:

Result(
    status=ResultStatus.SUCCESS,
    result={
        "stdout": "total 24\ndrwxr-xr-x  5 user user 4096...",
        "stderr": "",
        "exit_code": 0
    },
    call_id="uuid-1234-5678"
)

CommandDispatcher Interface

The BasicCommandDispatcher provides the abstract interface for command execution:

class BasicCommandDispatcher(ABC):
    """
    Abstract base class for command dispatcher.
    
    Responsibilities:
    - Send commands to device client
    - Wait for execution results
    - Handle errors and timeouts
    """
    
    @abstractmethod
    async def execute_commands(
        self, commands: List[Command], timeout: float = 6000
    ) -> Optional[List[Result]]:
        """
        Execute commands and return results.
        
        :param commands: List of commands to execute
        :param timeout: Execution timeout in seconds
        :return: List of results, or None if timeout
        """
        pass
    
    def generate_error_results(
        self, commands: List[Command], error: Exception
    ) -> List[Result]:
        """
        Generate error results for failed commands.
        
        :param commands: Commands that failed
        :param error: The error that occurred
        :return: List of error results
        """
        result_list = []
        for command in commands:
            error_msg = f"Error executing {command}: {error}"
            result = Result(
                status=ResultStatus.FAILURE,
                error=error_msg,
                result=error_msg,
                call_id=command.call_id
            )
            result_list.append(result)
        return result_list

Command Execution Flow

The following sequence diagram shows the complete command execution flow:

sequenceDiagram
    participant Strategy as ProcessingStrategy<br/>(ACTION_EXECUTION)
    participant Dispatcher as CommandDispatcher
    participant AIP as AIP Protocol
    participant Client as Device Client
    participant MCP as MCP Server Manager
    participant Tool as MCP Tool

    Note over Strategy: Step 1: Create Commands
    Strategy->>Strategy: Build Command objects<br/>from LLM response
    
    Note over Strategy: Step 2: Execute via Dispatcher
    Strategy->>Dispatcher: execute_commands([cmd1, cmd2])
    
    Note over Dispatcher: Step 3: Add Call IDs
    Dispatcher->>Dispatcher: Assign unique call_id<br/>to each command
    
    Note over Dispatcher: Step 4: Send via AIP
    Dispatcher->>AIP: Send ServerMessage<br/>(COMMAND type)
    AIP->>Client: WebSocket message<br/>(serialized commands)
    
    Note over Client: Step 5: Route to MCP
    Client->>MCP: Route commands to<br/>appropriate MCP server
    
    Note over MCP: Step 6: Execute Tools
    loop For each command
        MCP->>Tool: Invoke tool function<br/>with arguments
        Tool->>Tool: Execute operation<br/>(click, type, shell, etc.)
        Tool->>MCP: Return result
    end
    
    Note over Client: Step 7: Aggregate Results
    MCP->>Client: List[Result]
    Client->>AIP: Send ClientMessage<br/>(RESULT type)
    AIP->>Dispatcher: WebSocket message<br/>(serialized results)
    
    Note over Dispatcher: Step 8: Return Results
    Dispatcher->>Strategy: List[Result]
    
    Note over Strategy: Step 9: Process Results
    Strategy->>Strategy: Update context<br/>with execution results

Execution Phases

  1. Command Creation: Strategy builds Command objects from LLM response or predefined logic
  2. Dispatcher Invocation: Strategy calls dispatcher.execute_commands()
  3. Call ID Assignment: Dispatcher assigns unique call_id to each command
  4. AIP Transmission: Commands serialized and sent via WebSocket to device client
  5. MCP Routing: Client routes commands to appropriate MCP server
  6. Tool Execution: MCP server invokes tool functions with arguments
  7. Result Aggregation: Results collected and sent back via AIP
  8. Result Handling: Dispatcher returns results to strategy
  9. Context Update: Strategy updates processing context with results

!!! warning "Timeout Handling" If execution exceeds timeout:

1. Dispatcher raises `asyncio.TimeoutError`
2. Error results generated via `generate_error_results()`
3. Strategy receives error results (status = FAILURE)
4. Processor can retry or fail based on `fail_fast` setting

Dispatcher Implementations

UFO provides two dispatcher implementations for different deployment scenarios:

1. LocalCommandDispatcher

Purpose: Direct local execution (server and client on same machine)

Use Case: Development, testing, single-device deployments

class LocalCommandDispatcher(BasicCommandDispatcher):
    """
    Local command dispatcher - executes commands directly.
    
    No network communication - calls MCP tools locally.
    """
    
    def __init__(self, session: BaseSession, mcp_server_manager: MCPServerManager):
        self.session = session
        self.mcp_server_manager = mcp_server_manager
        
        # Direct local execution
        self.computer_manager = ComputerManager(configs, mcp_server_manager)
        self.command_router = CommandRouter(self.computer_manager)
    
    async def execute_commands(
        self, commands: List[Command], timeout=6000
    ) -> Optional[List[Result]]:
        """Execute commands locally via CommandRouter"""
        try:
            # Direct invocation (no network)
            action_results = await asyncio.wait_for(
                self.command_router.execute(
                    agent_name=self.session.current_agent_class,
                    root_name=self.session.context.get(ContextNames.APPLICATION_ROOT_NAME),
                    process_name=self.session.context.get(ContextNames.APPLICATION_PROCESS_NAME),
                    commands=commands
                ),
                timeout=timeout
            )
            return action_results
        except Exception as e:
            return self.generate_error_results(commands, e)

2. WebSocketCommandDispatcher

Purpose: Remote execution via AIP (server and client on different machines)

Use Case: Production, multi-device deployments, distributed systems

class WebSocketCommandDispatcher(BasicCommandDispatcher):
    """
    WebSocket command dispatcher - executes commands remotely via AIP.
    
    Uses AIP's TaskExecutionProtocol for structured messaging.
    """
    
    def __init__(self, session: BaseSession, protocol: TaskExecutionProtocol):
        self.session = session
        self.protocol = protocol  # AIP protocol instance
        self.pending: Dict[str, asyncio.Future] = {}
        self.logger = logging.getLogger(__name__)
    
    async def execute_commands(
        self, commands: List[Command], timeout=6000
    ) -> Optional[List[Result]]:
        """Execute commands remotely via AIP WebSocket"""
        try:
            # Build ServerMessage
            server_msg = self.make_server_response(commands)
            
            # Send via AIP
            await self.protocol.send_command(server_msg)
            
            # Wait for results
            results = await asyncio.wait_for(
                self._wait_for_results(server_msg.response_id),
                timeout=timeout
            )
            
            return results
        except Exception as e:
            return self.generate_error_results(commands, e)
    
    def make_server_response(self, commands: List[Command]) -> ServerMessage:
        """Create ServerMessage for commands"""
        # Assign call_ids
        for command in commands:
            command.call_id = str(uuid.uuid4())
        
        return ServerMessage(
            type=ServerMessageType.COMMAND,
            status=TaskStatus.CONTINUE,
            agent_name=self.session.current_agent_class,
            process_name=self.session.context.get(ContextNames.APPLICATION_PROCESS_NAME),
            root_name=self.session.context.get(ContextNames.APPLICATION_ROOT_NAME),
            actions=commands,
            session_id=self.session.id,
            task_name=self.session.task,
            response_id=str(uuid.uuid4()),
            timestamp=datetime.datetime.now(datetime.timezone.utc).isoformat()
        )

Dispatcher Selection

The dispatcher is selected at session initialization:

  • Local Mode: LocalCommandDispatcher (no AIP client connection)
  • Remote Mode: WebSocketCommandDispatcher (AIP client connected)

This is transparent to strategies - they call dispatcher.execute_commands() regardless.

MCP Integration

Commands map to Model Context Protocol (MCP) tools on the device client:

graph TB
    subgraph "Device Client"
        Router[Command Router]
        Manager[MCP Server Manager]
        
        Router -->|routes by agent/app| Manager
    end
    
    subgraph "MCP Servers"
        Win[Windows MCP Server<br/>ufo_windows]
        Linux[Linux MCP Server<br/>ufo_linux]
        Custom[Custom MCP Server<br/>user_defined]
    end
    
    Manager -->|manages| Win
    Manager -->|manages| Linux
    Manager -->|manages| Custom
    
    subgraph "MCP Tools (Windows)"
        Win -->|provides| T1[click_element]
        Win -->|provides| T2[type_text]
        Win -->|provides| T3[get_ui_tree]
        Win -->|provides| T4[screenshot]
    end
    
    subgraph "MCP Tools (Linux)"
        Linux -->|provides| T5[shell_execute]
        Linux -->|provides| T6[read_file]
        Linux -->|provides| T7[write_file]
    end
    
    Command[Command<br/>function + arguments] -->|routed to| Router

MCP Tool Registration

MCP tools are registered on the device client at initialization:

# Windows MCP Server registration
mcp_server_manager.register_server(
    server_name="ufo_windows",
    tools=[
        MCPToolInfo(
            name="click_element",
            description="Click UI element by control ID",
            arguments_schema={
                "control_id": {"type": "string", "required": True},
                "process_name": {"type": "string", "required": True}
            }
        ),
        MCPToolInfo(
            name="type_text",
            description="Type text into focused element",
            arguments_schema={
                "text": {"type": "string", "required": True}
            }
        ),
        # ... more tools
    ]
)

Tool Discovery

The LLM can query available tools via the get_mcp_tools command:

# Strategy requests available tools
tools_cmd = Command(
    function="get_mcp_tools",
    arguments={"server_name": "ufo_windows"}
)

results = await dispatcher.execute_commands([tools_cmd])

# LLM receives tool registry
available_tools = results[0].result  # List[MCPToolInfo]

Dynamic Tool Selection

The LLM dynamically selects appropriate tools based on:

  1. Tool Descriptions: Natural language descriptions of tool capabilities
  2. Input Schemas: Required/optional parameters with types
  3. Context: Current task requirements and device state

This enables adaptive behavior without hardcoded command sequences.

See MCP Documentation for complete MCP integration details.

Command Categories

Commands can be categorized by their purpose:

1. Observation Commands (DATA_COLLECTION)

Purpose: Gather information from device without modifying state

CommandPlatformDescriptionArgumentsResult
screenshotAllCapture screen image{"region": "fullscreen"}Base64 image
get_ui_treeWindowsExtract UI Automation tree{"process_name": "..."}XML/JSON tree
shell_execute_readLinuxExecute shell command (read-only){"command": "ls -la"}stdout/stderr
get_accessibility_treemacOSExtract Accessibility tree{"app_name": "..."}Tree structure

2. Action Commands (ACTION_EXECUTION)

Purpose: Modify device state through interactions

CommandPlatformDescriptionArgumentsResult
click_elementWindowsClick UI element{"control_id": "..."}Click success
type_textWindowsType text into element{"text": "..."}Typing success
scrollWindowsScroll element{"direction": "down", "amount": 3}Scroll success
shell_executeLinuxExecute shell command{"command": "...", "timeout": 30}stdout/stderr/exit_code
press_keyAllPress keyboard key{"key": "Enter"}Key press success

3. System Commands

Purpose: Interact with OS or hardware

CommandPlatformDescriptionArgumentsResult
launch_applicationAllStart application{"app_name": "...", "args": [...]}PID
close_applicationAllTerminate application{"process_name": "..."}Close success
read_fileAllRead file contents{"file_path": "...", "encoding": "utf-8"}File contents
write_fileAllWrite file contents{"file_path": "...", "content": "..."}Write success
get_system_infoAllQuery system status{"info_type": "cpu"}CPU/memory/disk stats

Command Naming Convention

  • Use snake_case for function names
  • Use verb_noun pattern (e.g., click_element, read_file)
  • Keep names concise but descriptive
  • Prefix with platform if platform-specific (e.g., windows_get_registry)

Command Validation

Commands are validated at multiple stages:

1. Client-Side Validation

The device client validates commands before execution:

class CommandRouter:
    """Routes and validates commands on device client"""
    
    async def execute(
        self,
        agent_name: str,
        root_name: str,
        process_name: str,
        commands: List[Command]
    ) -> List[Result]:
        """Execute commands with validation"""
        results = []
        
        for command in commands:
            # Validate command
            validation_error = self._validate_command(command)
            if validation_error:
                results.append(Result(
                    status=ResultStatus.FAILURE,
                    error=validation_error,
                    call_id=command.call_id
                ))
                continue
            
            # Execute command
            try:
                result = await self._execute_single_command(command)
                results.append(result)
            except Exception as e:
                results.append(Result(
                    status=ResultStatus.FAILURE,
                    error=str(e),
                    call_id=command.call_id
                ))
        
        return results
    
    def _validate_command(self, command: Command) -> Optional[str]:
        """Validate command structure and arguments"""
        # Check function exists
        tool_info = self.mcp_server_manager.get_tool(command.function)
        if not tool_info:
            return f"Unknown command: {command.function}"
        
        # Check required arguments
        schema = tool_info.arguments_schema
        for arg_name, arg_spec in schema.items():
            if arg_spec.get("required") and arg_name not in command.arguments:
                return f"Missing required argument: {arg_name}"
        
        # Check argument types
        for arg_name, arg_value in command.arguments.items():
            expected_type = schema.get(arg_name, {}).get("type")
            if expected_type and not self._check_type(arg_value, expected_type):
                return f"Argument '{arg_name}' has wrong type"
        
        return None  # Valid

2. MCP Schema Validation

MCP tools define argument schemas that are enforced:

{
    "name": "click_element",
    "description": "Click UI element by control ID",
    "arguments_schema": {
        "control_id": {
            "type": "string",
            "required": True,
            "description": "Unique identifier of control to click"
        },
        "process_name": {
            "type": "string",
            "required": True,
            "description": "Process name of application"
        },
        "double_click": {
            "type": "boolean",
            "required": False,
            "default": False,
            "description": "Whether to double-click"
        }
    }
}

Validation Benefits

  • Early Error Detection: Invalid commands caught before execution
  • Clear Error Messages: Specific validation failures reported
  • Type Safety: Argument types validated against schema
  • Security: Prevents injection attacks and malformed requests

Command Execution Patterns

Common patterns for command execution in strategies:

1. Single Command Execution

# Execute single command
async def execute(self, agent, context):
    command = Command(
        function="screenshot",
        arguments={"region": "fullscreen"}
    )
    
    results = await context.command_dispatcher.execute_commands([command])
    
    if results[0].status == ResultStatus.SUCCESS:
        screenshot = results[0].result
        context.update_local({"screenshot": screenshot})
        return ProcessingResult(success=True, data={"screenshot": screenshot})
    else:
        return ProcessingResult(success=False, error=results[0].error)

2. Batch Command Execution

# Execute multiple commands in parallel
async def execute(self, agent, context):
    commands = [
        Command(function="screenshot", arguments={}),
        Command(function="get_ui_tree", arguments={"process_name": "EXCEL.EXE"}),
        Command(function="get_system_info", arguments={"info_type": "memory"})
    ]
    
    results = await context.command_dispatcher.execute_commands(commands)
    
    # Process results
    data = {
        "screenshot": results[0].result if results[0].status == ResultStatus.SUCCESS else None,
        "ui_tree": results[1].result if results[1].status == ResultStatus.SUCCESS else None,
        "memory_info": results[2].result if results[2].status == ResultStatus.SUCCESS else None
    }
    
    return ProcessingResult(success=True, data=data)

3. Conditional Command Execution

# Execute command based on LLM decision
async def execute(self, agent, context):
    parsed_response = context.require_local("parsed_response")
    action = parsed_response.get("ControlText")
    
    if action == "click":
        command = Command(
            function="click_element",
            arguments={"control_id": parsed_response.get("ControlID")}
        )
    elif action == "type":
        command = Command(
            function="type_text",
            arguments={"text": parsed_response.get("InputText")}
        )
    else:
        return ProcessingResult(success=False, error=f"Unknown action: {action}")
    
    results = await context.command_dispatcher.execute_commands([command])
    return ProcessingResult(success=results[0].status == ResultStatus.SUCCESS)

4. Retry Pattern

# Retry command on failure
async def execute(self, agent, context):
    command = Command(
        function="click_element",
        arguments={"control_id": "Button_123"}
    )
    
    max_retries = 3
    for attempt in range(max_retries):
        results = await context.command_dispatcher.execute_commands([command])
        
        if results[0].status == ResultStatus.SUCCESS:
            return ProcessingResult(success=True, data=results[0].result)
        
        # Retry with exponential backoff
        await asyncio.sleep(2 ** attempt)
    
    return ProcessingResult(success=False, error="Max retries exceeded")

Best Practices

Command Design Guidelines

1. Atomic Operations: Each command should perform one well-defined operation

  • ✅ Good: click_element(control_id="Button_123")
  • ❌ Bad: click_and_wait_and_validate(...) (too many responsibilities)

2. Idempotency: Commands should be safe to retry

  • ✅ Good: read_file(path="/data.csv") (idempotent)
  • ⚠️ Caution: append_to_file(path="/log.txt", text="...") (not idempotent)

3. Clear Arguments: Use descriptive argument names

  • ✅ Good: {"file_path": "...", "encoding": "utf-8"}
  • ❌ Bad: {"p": "...", "e": "utf-8"} (unclear)

4. Structured Results: Return structured data, not just strings

  • ✅ Good: {"stdout": "...", "stderr": "...", "exit_code": 0}
  • ❌ Bad: "output: ... error: ... code: 0" (unstructured)

Security Considerations

!!! warning "Security Best Practices" Validate All Inputs: Never trust command arguments from LLM without validation

**Limit Command Scope**: Restrict commands to necessary operations only

- Use MCP tool permissions to limit file access
- Sandbox shell command execution
- Validate file paths against allowed directories

**Audit Command History**: Log all commands for compliance

```python
self.logger.info(f"Executing command: {command.tool_name} with args: {command.parameters}")
```

**Timeout All Commands**: Prevent runaway execution

```python
results = await dispatcher.execute_commands(commands, timeout=30)
```

!!! danger "Dangerous Commands" Some commands require extra caution:

**Shell Execution**: Risk of command injection

- Use argument escaping/sanitization
- Whitelist allowed commands
- Never concatenate user input directly

**File Operations**: Risk of unauthorized access

- Validate paths against allowed directories
- Check file permissions before access
- Never allow arbitrary path traversal

**System Modification**: Risk of breaking system state

- Require explicit user confirmation
- Implement undo/rollback mechanisms
- Never allow destructive ops without safeguards

Integration with Other Layers

The Command Layer integrates with other components:

graph TB
    subgraph "Strategy Layer (Level-2)"
        AE[ACTION_EXECUTION<br/>Strategy]
        DC[DATA_COLLECTION<br/>Strategy]
    end
    
    subgraph "Command Layer (Level-3)"
        Dispatcher[CommandDispatcher]
        Commands[Commands]
        Results[Results]
    end
    
    subgraph "Communication"
        AIP[AIP Protocol]
    end
    
    subgraph "Device Client"
        MCP[MCP Servers]
        Tools[MCP Tools]
    end
    
    AE -->|creates| Commands
    DC -->|creates| Commands
    Commands -->|via| Dispatcher
    Dispatcher -->|via| AIP
    AIP -->|routes to| MCP
    MCP -->|invokes| Tools
    Tools -->|results via| MCP
    MCP -->|via| AIP
    AIP -->|via| Dispatcher
    Dispatcher -->|returns| Results
    Results -->|used by| AE
    Results -->|used by| DC
Integration PointLayer/ComponentRelationship
ProcessingStrategyLevel-2 StrategyStrategies create and execute commands via dispatcher
AIP ProtocolCommunicationDispatcher uses AIP to send commands to client
Device ClientExecutionClient receives commands, routes to MCP servers
MCP ServersTool RegistryMCP servers execute tool functions, return results
Global ContextModule SystemCommand dispatcher accessed via processing context

See Strategy Layer, AIP Protocol, and MCP Integration for integration details.

API Reference

Below is the complete API reference for the Command Layer:

BasicCommandDispatcher (Abstract Base Class)

# Location: ufo/module/dispatcher.py
class BasicCommandDispatcher(ABC):
    """Abstract base class for command dispatcher handling."""
    
    @abstractmethod
    async def execute_commands(
        self, commands: List[Command], timeout: float = 6000
    ) -> Optional[List[Result]]:
        """Execute commands and return results."""
        pass

LocalCommandDispatcher (Local Execution)

# Location: ufo/module/dispatcher.py
class LocalCommandDispatcher(BasicCommandDispatcher):
    """Command dispatcher for local execution (testing/development)."""
    pass

WebSocketCommandDispatcher (Server-Client Communication)

# Location: ufo/module/dispatcher.py
class WebSocketCommandDispatcher(BasicCommandDispatcher):
    """Command dispatcher using WebSocket/AIP protocol."""
    pass

Summary

Key Takeaways:

  • Atomic Execution: Commands are self-contained units with tool_name + parameters
  • MCP Integration: Commands map to Model Context Protocol tools on device client
  • CommandDispatcher: Routes commands from server to client via AIP
  • Deterministic: Same inputs → same outputs, fully auditable
  • Dynamic Discovery: LLM queries and selects appropriate tools at runtime
  • Validation: Multi-stage validation (client + MCP schema) ensures safety
  • Extensibility: New commands added via MCP tool registration without code changes

The Command Layer completes the three-layer device agent architecture, providing reliable, auditable, and extensible system interfaces that bridge high-level reasoning with low-level device operations across heterogeneous platforms.