This article walks through the full integration of Open Agent SDK into Motive, a native macOS app. The team replaced an external opencode service with an in-process Agent Loop to reduce startup latency, cut inter-process communication overhead, and improve debugging efficiency. Keywords: Open Agent SDK, SwiftUI, MCP.
The technical specification snapshot captures the integration surface
| Item | Details |
|---|---|
| Language | Swift, SwiftUI |
| Runtime Platform | Native macOS desktop application |
| Agent Protocol / Communication | In-process calls, AsyncStream, MCP stdio |
| Previous Backend Protocol | REST API + SSE |
| Integration Target | Open Agent SDK, Motive |
| Core Dependencies | OpenAgentSDK, Swift Concurrency, MCP, SessionStore |
| Repository Popularity | Star count not provided in the original source |
This replacement validated that the SDK can directly serve as the desktop Agent runtime
Motive is a native macOS AI Agent application built with SwiftUI. After a user submits a prompt, the system runs an Agent Loop in the background, invokes tools, reads files, executes commands, and streams results back to the UI.
The original architecture depended on an external opencode serve process. The app had to create sessions over REST, send prompts through HTTP, and receive streamed events via SSE. This approach worked, but it introduced clear engineering friction on desktop: a CLI installation dependency, slow cold starts, extra HTTP serialization overhead, and complicated cross-process debugging.
The original backend path can be summarized as a four-layer bridge
Motive App (SwiftUI)
└── OpenCodeBridge
├── OpenCodeServer # Starts the external opencode process
├── OpenCodeAPIClient # Sends prompts over REST
└── SSEClient # Receives streaming events over SSE
At its core, this path meant that the UI did not run the Agent directly. It only consumed events from an external service.
The new architecture pulls the Agent Loop back into the app process
The core idea behind the replacement is straightforward: stop launching an external binary, stop routing through HTTP, and call the SDK’s Agent.stream() directly to run the Agent Loop inside the Motive process.
To reduce risk, the author did not remove the old implementation all at once. Instead, they introduced SDKBridge while preserving the legacy OpenCodeBridge behind BackendBridge. Users can switch backends in settings, which allows both implementations to be validated in parallel.
A unified dispatch layer lets AppState remain largely unchanged
enum BackendBridge {
case opencode(OpenCodeBridge)
case sdk(SDKBridge)
func submitIntent(text: String, cwd: String) async {
switch self {
case .opencode(let bridge):
await bridge.submitIntent(text: text, cwd: cwd) // Uses the original HTTP backend
case .sdk(let bridge):
await bridge.submitIntent(text: text, cwd: cwd) // Uses the in-process SDK
}
}
}
This code provides a single entry point that hides backend differences while still allowing each bridge to retain backend-specific capabilities.
SDKBridge is the real core of this refactor
SDKBridge is implemented as an actor responsible for configuration intake, Agent creation, message stream consumption, and event mapping. Its responsibility boundary is clear: upward to the application state layer, downward to Open Agent SDK.
Its configuration includes the API key, model, provider, project directory, MCP servers, environment variables, and skill directories. One especially important detail is that MCP configuration must be converted from Motive’s internal config model into the SDK-compatible McpServerConfig.stdio format.
Agent creation must explicitly complete the tool pool
private func createAgent(from config: Configuration, sessionId: String? = nil) -> Agent {
let provider: LLMProvider = config.provider == "anthropic" ? .anthropic : .openai
let coreTools = getAllBaseTools(tier: .core) +
getAllBaseTools(tier: .specialist) // Always include built-in tools
return OpenAgentSDK.createAgent(options: AgentOptions(
apiKey: config.apiKey,
model: config.model,
provider: provider,
cwd: config.projectDirectory,
tools: coreTools,
sessionStore: sessionStore,
sessionId: sessionId,
logLevel: config.debugMode ? .debug : .none
))
}
This code creates an Agent instance that supports persistent and resumable sessions and can directly execute the toolchain.
Streaming prompt submission depends on wiring AsyncStream into the app event bus
Each time a user submits a prompt, SDKBridge.submitIntent() generates or reuses a sessionId, creates an Agent, and consumes the message stream returned by stream() inside a background Task.
This pattern fits naturally with SwiftUI’s reactive UI model. The UI layer does not need to understand the Agent’s internal loop. It only needs to consume the event stream and update view state.
Wrapping the streaming loop in a concurrent Task enables interruption
private typealias _Task = _Concurrency.Task
func submitIntent(text: String, cwd: String, forceNewSession: Bool = false) async {
let sessionId = forceNewSession ? UUID().uuidString : (currentSessionId ?? UUID().uuidString)
currentSessionId = sessionId
let sdkAgent = createAgent(from: config, sessionId: sessionId)
self.agent = sdkAgent
streamTask?.cancel() // Cancel the previous session stream
streamTask = _Task {
for await message in sdkAgent.stream(text) {
guard !_Task.isCancelled else { return }
await self.handleSDKMessage(message, sessionId: sessionId) // Convert to UI events
}
}
}
This code connects the SDK’s streaming output to a cancellable asynchronous execution path.
The event mapping strategy maximized reuse of the existing UI
Motive originally drove its UI through OpenCodeEvent. The most economical migration path was not to rewrite the UI, but to map SDKMessage into the legacy event model inside the bridge layer.
That choice delivers substantial value: the UI, state management, and rendering logic can all stay in place, while the actual changes remain concentrated in the infrastructure layer. This is a textbook compatibility-focused refactor and a key way to reduce migration risk.
Mapping SDKMessage to OpenCodeEvent is the pivot point of the migration
private func handleSDKMessage(_ message: SDKMessage, sessionId: String) {
switch message {
case .partialMessage(let data):
eventContinuation.yield(.init(kind: .assistant, rawJson: "", text: data.text))
case .toolUse(let data):
eventContinuation.yield(.init(kind: .tool, rawJson: "", text: data.input, toolName: data.toolName))
case .toolResult(let data):
let output = data.isError ? "Error: \(data.content)" : data.content
eventContinuation.yield(.init(kind: .tool, rawJson: "", text: "", toolName: "Result", toolOutput: output))
default:
break
}
}
This code converts the SDK’s internal message protocol into Motive’s existing UI event protocol.
Real integration pitfalls reveal the boundary conditions of desktop Agent apps
The hardest problems were not in the SDK API itself, but in the macOS GUI process environment. A GUI application does not automatically inherit the user’s shell PATH, which means MCP child processes may fail to find node or python. This is not an SDK defect. It is a consequence of the platform’s security model.
Another subtle issue is that when MCP is absent, the SDK’s tool pool assembly path may omit the built-in Core and Specialist tools. If you do not explicitly add them, the Agent may not even have access to basic file operations or Bash.
PATH injection is a prerequisite for MCP inside a GUI app
let extendedPath = configManager.buildExtendedPath(
base: ProcessInfo.processInfo.environment["PATH"]
)
for entry in mcpEntries {
var mergedEnv = entry.env ?? [:]
mergedEnv["PATH"] = extendedPath // Inject the extended PATH so node/python can be found
}
This code restores missing executable paths for MCP stdio child processes running under the GUI environment.
Configuration timing and naming collisions are also common Swift integration details
Because AppState.start() configures asynchronously, a user may submit a prompt before configuration is complete, which can trigger a bridge not configured error. The fix is to proactively call configureBridge() before every submission.
There is also an internal Task type inside OpenAgentSDK, which conflicts with Swift Concurrency’s Task. The author resolved the ambiguity with typealias _Task = _Concurrency.Task, a very typical naming-avoidance pattern in Swift projects.
The final gains came from dependency reduction and a shorter debugging path
After the integration, Motive added roughly 600 lines of net new code, with a 361-line SDKBridge handling the core replacement. In exchange, the app no longer depends on the external opencode CLI, and it no longer needs a REST client, an SSE client, or additional process management.
The engineering benefits are concrete: faster startup, a shorter invocation path, more direct debugging, reusable session persistence through SessionStore, and retained support for MCP and multiple providers. This shows that Open Agent SDK adapts well to native GUI Agent applications.
This implementation shows that the SDK is well suited as an embedded runtime for native desktop Agents
From an engineering perspective, this validation was not about whether the system could run at all. It was about whether the replacement cost was reasonable. The result suggests that just two core entry points, createAgent() and stream(), can replace an external process, an HTTP service, and an SSE channel, with benefits that clearly outweigh migration cost.
For teams building Agent applications with SwiftUI, macOS, and MCP, this case provides a direct migration template.
AI Visual Insight: This animated image is a blog platform sharing prompt. It does not contain technical information about the Agent architecture, execution flow, UI state machine, or MCP topology, so it should not be interpreted as a technical diagram.
FAQ
1. Why replace external opencode serve with the SDK?
Because in-process execution removes the costs of CLI installation, HTTP serialization, SSE listening, and cold starts, which significantly reduces integration complexity and debugging overhead for desktop applications.
2. What is the biggest MCP challenge in a native macOS app?
The biggest issue is that the GUI process does not inherit the user’s shell PATH, so MCP stdio child processes cannot locate runtimes such as node and python. You must inject an extended environment manually.
3. Does migrating to Open Agent SDK require a major UI rewrite?
Usually not. If you map SDKMessage to the existing event model in the bridge layer, such as OpenCodeEvent, you can continue reusing the original state management and rendering logic.
AI Readability Summary
This article explains how to embed Open Agent SDK directly into Motive, a native macOS Agent app built with SwiftUI. It shows how to replace an external opencode process, REST, and SSE communication with an in-process Agent Loop, and it highlights the key integration details around BackendBridge, SDKBridge, MCP configuration, session persistence, and macOS PATH handling.