By Sagar Shankaran, Founder of CallSphere
Deep dive into Microsoft UFO's dual-agent system where HostAgent orchestrates application selection and AppAgent executes in-app UI actions, with detailed coordination flow and plan execution examples.
Key takeaways
Windows automation is fundamentally different from web automation. A browser has one viewport, one DOM, and a uniform API. A Windows desktop has dozens of applications running simultaneously, each with its own window, menu system, and control hierarchy. A single agent trying to manage both "which app should I use?" and "which button should I click?" would face an overwhelming observation space.
UFO solves this by splitting responsibilities. The HostAgent operates at the desktop level — it sees all open windows, understands which applications are available, and decides where to route each sub-task. The AppAgent operates within a single application — it sees the controls, menus, and content of one window and executes precise UI actions.
This separation of concerns mirrors how humans work. You first decide "I need Excel for this" (HostAgent thinking), then you interact with Excel's ribbons, cells, and menus (AppAgent thinking).
The HostAgent is responsible for:
Hear it before you finish reading
Talk to a live CallSphere AI voice agent in your browser — 60 seconds, no signup.
flowchart LR
INPUT(["User intent"])
PARSE["Parse plus<br/>classify"]
PLAN["Plan and tool<br/>selection"]
AGENT["Agent loop<br/>LLM plus tools"]
GUARD{"Guardrails<br/>and policy"}
EXEC["Execute and<br/>verify result"]
OBS[("Trace and metrics")]
OUT(["Outcome plus<br/>next action"])
INPUT --> PARSE --> PLAN --> AGENT --> GUARD
GUARD -->|Pass| EXEC --> OUT
GUARD -->|Fail| AGENT
AGENT --> OBS
style AGENT fill:#4f46e5,stroke:#4338ca,color:#fff
style GUARD fill:#f59e0b,stroke:#d97706,color:#1f2937
style OBS fill:#ede9fe,stroke:#7c3aed,color:#1e1b4b
style OUT fill:#059669,stroke:#047857,color:#fff
# Simplified HostAgent logic
class HostAgent:
def __init__(self, config: dict):
self.model = config["HOST_AGENT"]["API_MODEL"]
self.active_apps = self.detect_open_applications()
def detect_open_applications(self) -> list[dict]:
"""Use Windows API to enumerate all visible windows."""
import pywinauto
desktop = pywinauto.Desktop(backend="uia")
windows = desktop.windows()
return [
{
"title": w.window_text(),
"process": w.process_id(),
"rect": w.rectangle(),
}
for w in windows if w.is_visible()
]
def plan_task(self, user_request: str) -> list[dict]:
"""Ask GPT-4V to decompose the task into sub-tasks."""
screenshot = self.capture_desktop_screenshot()
prompt = f"""You are a Windows desktop automation planner.
User request: {user_request}
Open applications: {self.active_apps}
Break this into ordered sub-tasks. For each sub-task, specify:
1. The target application
2. The action to perform within that application
3. Any data to transfer between applications"""
response = call_vision_model(
model=self.model,
prompt=prompt,
image=screenshot
)
return parse_subtasks(response)
Once the HostAgent selects an application and brings it to the foreground, the AppAgent takes over. It operates in a tight observe-plan-act loop:
class AppAgent:
def __init__(self, app_window, config: dict):
self.window = app_window
self.model = config["APP_AGENT"]["API_MODEL"]
self.action_history = []
self.max_steps = config.get("MAX_STEP", 50)
def execute_task(self, instruction: str) -> bool:
"""Run the observation-action loop until task completes."""
for step in range(self.max_steps):
# Observe: capture and annotate current state
screenshot = self.capture_app_screenshot()
controls = self.enumerate_controls()
annotated = self.annotate_screenshot(screenshot, controls)
# Plan: ask the model what to do next
action = self.get_next_action(
annotated_screenshot=annotated,
instruction=instruction,
history=self.action_history,
available_controls=controls
)
# Check for completion
if action["status"] == "FINISH":
return True
# Act: execute the planned action
self.execute_action(action, controls)
self.action_history.append(action)
return False # Max steps exceeded
def enumerate_controls(self) -> list[dict]:
"""List all interactive UI elements in the window."""
controls = []
for element in self.window.descendants():
if element.is_enabled():
controls.append({
"id": len(controls),
"type": element.control_type(),
"name": element.window_text(),
"rect": element.rectangle(),
"automationId": element.automation_id(),
})
return controls
A complete task flows through these stages:
UFO internally represents plans as structured action sequences. Each action has a type, target control, and parameters:
{
"plan": [
{
"step": 1,
"application": "Microsoft Excel",
"action": "click",
"target": "Cell A1",
"description": "Click on cell A1 to start selection"
},
{
"step": 2,
"application": "Microsoft Excel",
"action": "keyboard",
"keys": "Ctrl+Shift+End",
"description": "Select all data from A1 to the last used cell"
}
]
}
When the AppAgent encounters an error — for example, a dialog box appears unexpectedly — it reports the failure back to the HostAgent. The HostAgent can then decide to retry the sub-task, modify the plan, or skip to an alternative approach.
Still reading? Stop comparing — try CallSphere live.
CallSphere ships complete AI voice agents per industry — 14 tools for healthcare, 10 agents for real estate, 4 specialists for salons. See how it actually handles a call before you book a demo.
This error recovery is one of the key advantages of the dual-agent design. A monolithic agent would need to handle both application-level and desktop-level recovery in a single decision space. By separating them, each agent can focus on errors within its domain.
UFO's architecture is designed around the two-agent pattern. However, you can extend the AppAgent with custom action handlers or wrap UFO in a higher-level orchestration framework that manages multiple UFO instances for truly complex multi-desktop workflows.
The AppAgent will fail to find the expected UI elements and report a failure. The HostAgent can then re-evaluate the desktop screenshot and try a different application. In practice, GPT-4o is quite accurate at application identification from window titles and visual appearance.
UFO primarily uses the Windows clipboard for cross-application data transfer — the same mechanism humans use (Ctrl+C, Ctrl+V). For structured data, the AppAgent can also read values from UI elements and pass them as text context to the next sub-task.
#MicrosoftUFO #DualAgent #HostAgent #AppAgent #AgenticArchitecture #WindowsAutomation #MultiAgent #Orchestration

Written by
Sagar Shankaran· Founder, CallSphere
LinkedInSagar Shankaran is the founder of CallSphere, where he builds production AI voice and chat agents deployed across healthcare, hospitality, real estate, and home services. He writes about agentic AI, LLM engineering, and shipping voice agents that handle real calls in production.
See how AI voice agents work for your industry. Live demo available -- no signup required.
How to design a multi-agent system using MCP for tools and A2A for cross-vendor coordination, with a CallSphere voice agent as a participating node.
Five proven multi-agent architecture patterns built on A2A — orchestrator, peer mesh, hub-and-spoke, marketplace, and tiered specialist.
A2A is the open standard for agent-to-agent coordination. Here is how the Agent Card JSON works, how discovery happens, and what to publish.
A2A unlocks cross-vendor agent coordination, but most enterprise voice/chat workloads still ship faster on a single-vendor stack. Here is how to choose.
Every 100ms of latency costs you. So does every cent per minute. Here is the decision matrix we use across 6 verticals to pick where to spend and where to save on voice AI infrastructure.
When to use Pinecone vs pgvector vs Qdrant vs Weaviate. A decision framework that maps team size and workload to the right pick without endless evaluation loops.
© 2026 CallSphere Inc. All rights reserved.
Made within San Francisco
Watch how CallSphere handles real customer calls, schedules appointments, and processes payments — live.