深入 Open Agent SDK(番外篇):实战验证——把 SDK 塞进一个 macOS 原生 Agent 应用
gent 在后台跑 Agent Loop调工具、读文件、执行命令→ 流式输出结果到 UI。在集成 SDK 之前Motive 的 Agent 后端长这样Motive App (SwiftUI) └── OpenCodeBridge (actor) ├── OpenCodeServer — 启动外部 opencode 二进制进程 (opencode serve) ├── SSEClient — 通过 Server-Sent Events 接收流式事件 └── OpenCodeAPIClient — 通过 REST API 发送 prompt、回复权限请求每次用户发 promptMotive 要启动一个外部opencode serve进程如果没在跑的话通过 REST APIPOST /sessions创建会话通过 REST APIPOST /sessions/{id}/prompt发送 prompt通过 SSE 连接接收事件流文本片段、工具调用、完成信号等这套架构能用但有几个问题依赖外部二进制用户要自己安装opencodeCLIMotive 还要处理二进制签名、路径查找进程间通信开销REST API SSE 意味着事件要经过 HTTP 序列化/反序列化启动延迟外部进程冷启动需要时间调试困难跨进程的问题很难定位SDK 的出现正好给了另一种可能——把 Agent Loop 直接跑在应用进程内。目标SDKBridge我想做的替换不启动外部进程不经过 HTTP直接在 Motive 进程内用 SDK 的Agent.stream()跑 Agent Loop。目标架构Motive App (SwiftUI) └── BackendBridge (enum wrapper) ├── .opencode → OpenCodeBridge (原有架构保留) └── .sdk → SDKBridge (新增用 OpenAgentSDK) └── Agent.stream() → 直接在进程内跑 Agent Loop保留原有的OpenCodeBridge作为备选让用户可以在设置中切换后端类型。这是一个务实的决定——万一 SDK 后端有问题用户还能切回去。第一步BackendBridge 抽象层原有的OpenCodeBridge是一个 actorMotive 的AppState直接跟它交互。现在要加一个平行的SDKBridge需要一个分派层。我用了一个enum而不是 protocolenum BackendBridge { case opencode(OpenCodeBridge) case sdk(SDKBridge) func submitIntent(text: String, cwd: String, ...) async { ... } func interrupt() async { ... } func stop() async { ... } // ... }为什么不用 protocol因为OpenCodeBridge和SDKBridge的能力不完全一样。OpenCodeBridge有权限请求permission、问题回复question等 SDK 后端不需要的概念。用 enum 可以在共享接口上做统一分派同时保留各自特有的方法// OpenCode-only 方法SDK 后端直接 no-op func replyToQuestion(requestID: String, answers: [[String]], ...) async { guard case .opencode(let bridge) self else { return } await bridge.replyToQuestion(requestID: requestID, answers: answers, ...) }对于 AppState 来说大部分代码不需要改——它调bridge.submitIntent()至于底层是 HTTP 还是 SDK它不关心。第二步SDKBridge 核心——361 行的 ActorSDKBridge是整个替换的核心。它是一个 actor负责接收ConfigurationAPI key、model、MCP servers 等用 SDK 的createAgent()创建 Agent调用Agent.stream()获取流式响应把 SDK 的SDKMessage映射成 Motive 已有的OpenCodeEvent配置actor SDKBridge { struct Configuration: Sendable { let apiKey: String let model: String let provider: String // anthropic, openai, etc. let baseURL: String? let debugMode: Bool let projectDirectory: String let mcpEntries: [String: MCPEntry]? let env: [String: String]? let skillDirectories: [String]? } struct MCPEntry: Sendable { let command: String let args: [String]? let env: [String: String]? } }MCPEntry是中间类型——Motive 的配置系统有自己的 MCP 描述格式在传入 SDK 之前转成McpServerConfig.stdio。创建 Agentprivate func createAgent(from config: Configuration, sessionId: String? nil) - Agent { let provider: LLMProvider Self.anthropicProviders.contains(config.provider) ? .anthropic : .openai let mcpServers config.mcpEntries?.mapValues { entry in McpServerConfig.stdio(McpStdioConfig( command: entry.command, args: entry.args, env: entry.env )) } // 始终包含 core specialist 工具确保基本能力 let coreTools getAllBaseTools(tier: .core) getAllBaseTools(tier: .specialist) return OpenAgentSDK.createAgent(options: AgentOptions( apiKey: config.apiKey, model: config.model, baseURL: config.baseURL, provider: provider, permissionMode: .bypassPermissions, cwd: config.projectDirectory, tools: coreTools, mcpServers: mcpServers, sessionStore: sessionStore, sessionId: sessionId, skillDirectories: config.skillDirectories, logLevel: config.debugMode ? .debug : .none, env: config.env )) }注意几个细节provider 映射Motive 用字符串anthropic、openaiSDK 用LLMProvider枚举这里做了转换core specialist 工具始终包含基础工具即使 MCP 服务器连接失败Agent 也有读写文件、执行命令的能力sessionStore sessionId传入 SessionStore 让 SDK 自动持久化对话历史传入 sessionId 实现会话恢复流式响应submitIntent这是最核心的方法。用户每次发 prompt 都走这里func submitIntent( text: String, cwd: String, agent: String? nil, forceNewSession: Bool false, correlationId: String? nil ) async { guard let config configuration else { eventContinuation.yield(OpenCodeEvent(kind: .error, rawJson: , text: SDK bridge not configured)) return } let sessionId forceNewSession ? UUID().uuidString : (currentSessionId ?? UUID().uuidString) currentSessionId sessionId // 创建 Agent let sdkAgent createAgent(from: config, sessionId: sessionId) self.agent sdkAgent // 取消之前的流 streamTask?.cancel() // 在后台 Task 中消费 stream streamTask _Task { [weak self] in guard let self else { return } for await message in sdkAgent.stream(text) { guard !_Task.isCancelled else { return } await self.handleSDKMessage(message, sessionId: sessionId) } } }用 Swift 的Task包裹stream()的for await循环这样用户中断时可以 cancel 掉这个 Task。注意_Task是_Concurrency.Task的别名——因为 OpenAgentSDK 里也有个Task类型直接用Task会冲突。SDKMessage → OpenCodeEvent 映射Motive 的 UI 已经有一套基于OpenCodeEvent的事件处理系统。与其重写 UI 层不如在 bridge 层做映射private func handleSDKMessage(_ message: SDKMessage, sessionId: String) { switch message { case .partialMessage(let data): eventContinuation.yield(OpenCodeEvent(kind: .assistant, rawJson: , text: data.text)) case .toolUse(let data): eventContinuation.yield(OpenCodeEvent(kind: .tool, rawJson: , text: data.input, toolName: data.toolName, toolCallId: data.toolUseId)) case .toolResult(let data): let output data.isError ? Error: \(data.content) : data.content eventContinuation.yield(OpenCodeEvent(kind: .tool, rawJson: , text: , toolName: Result, toolOutput: output, toolCallId: data.toolUseId)) case .result(let data): // 映射 usage // 映射 finish / error ... default: break } }eventContinuation是一个AsyncStreamOpenCodeEvent.Continuation在初始化时传入。AppState 在 MainActor 上消费这个流驱动 UI 更新。这个设计让 SDKBridge 和 OpenCodeBridge 共用同一套 UI 处理逻辑——AppState 不知道也不关心事件来自哪个后端。第三步踩过的坑这不是一次顺利的替换。以下是我遇到的真实问题。坑 1macOS GUI 应用没有 shell PATH这是最头疼的问题。macOS 的 GUI 应用不继承用户的 shell 环境。SDK 的MCPStdioTransport用Process启动 MCP 子进程时PATH里没有nvm、homebrew等路径——MCP 服务器找不到node、python。解决方案在buildSDKMcpServers()里手动构建扩展 PATHlet extendedPath configManager.buildExtendedPath(base: ProcessInfo.processInfo.environment[PATH]) for entry in mcpEntries { var mergedEnv spec.environment // ... mergedEnv[PATH] extendedPath // 注入扩展 PATH }这样 MCP 子进程能找到正确的node/python可执行文件。OpenCode 后端没这个问题因为opencodeCLI 是从终端启动的自带完整 shell 环境。坑 2核心工具在无 MCP 时不加载SDK 的assembleFullToolPool()在没有 MCP 服务器时走了一条短路径——只返回baseTools用户自定义工具不包含内置的 Core 和 Specialist 工具。这意味着如果不配 MCPAgent 连Read、Write、Bash都没有。修复在createAgent()里始终传入 core specialist 工具let coreTools getAllBaseTools(tier: .core) getAllBaseTools(tier: .specialist) return OpenAgentSDK.createAgent(options: AgentOptions( // ... tools: coreTools, // 始终包含 // ... ))坑 3时序问题——配置还没完成就发 promptAppState.start()里异步配置 bridge但用户可能在配置完成之前就发了 prompt。这导致 SDK bridge not configured 错误。修复在每次submitIntent和resumeSession之前都调用configureBridge()确保配置是最新的func submitIntent(...) async { await configureBridge() // 先确保配置完成 // 然后检查配置是否成功 guard configuration ! nil else { ... } // ... }坑 4Swift Task 命名冲突OpenAgentSDK 的类型命名跟 Swift 标准库有冲突——SDK 里有个Task类型用于任务追踪跟 Swift 并发的Task撞了。直接写Task { }编译器会找错类型。用 typealias 解决private typealias _Task _Concurrency.Task然后所有地方用_Task { }代替Task { }。坑 5API Key 可选问题不是所有 LLM 提供商都需要 API key。本地运行的 Ollama、LM Studio 就不需要。但 SDK 默认要求 API key 不为空。修复在配置时检查 provider 是否允许空 API keyif apiKey.isEmpty, !configManager.provider.allowsOptionalAPIKey { lastErrorMessage API key required for SDK backend. Check Settings. return }SDK 本身也支持空 API key——传入空字符串就行它会跳过认证 header。第四步MCP 服务器配置 UI为了让 SDK 后端能连接外部 MCP 工具我在 Advanced Settings 里加了一个 MCP 服务器配置界面。用户可以添加自定义的 MCP stdio 服务器配置命令、参数、环境变量保存到 UserDefaults然后在创建 Agent 时注入。struct CustomMcpServerConfig: Codable, Identifiable { let id: UUID var name: String var command: String var args: [String] var env: [String: String] var enabled: Bool }这些自定义服务器在buildSDKMcpServers()里跟 Skill 系统注册的 MCP 服务器合并一起传给 SDK。架构对比替换前后的关键差异方面OpenCode 后端SDK 后端Agent 运行位置外部opencode进程应用进程内通信方式REST API SSE直接函数调用启动延迟进程冷启动 ~2-5s毫秒级额外依赖需要安装 opencode CLISPM 依赖无需额外安装调试跨进程需要看外部日志进程内Xcode 断点直接打事件映射SSE JSON → OpenCodeEventSDKMessage → OpenCodeEventMCP 服务器opencode 内部管理应用层配置通过 SDK 传入替换后代码量对比SDKBridge.swift361 行新增BackendBridge.swift134 行新增AppStateBridge.swift123/-16 行修改AdvancedSettingsView.swift309/-44 行MCP UI其他测试和配置文件60/-8 行总共净增约 600 行换来的是去掉了对外部二进制的依赖。验证结论这次集成验证了 SDK 在以下方面的工程表现能用的部分Agent.stream()的AsyncStreamSDKMessage接口简洁可以直接用在 SwiftUI 的响应式流程里SessionStore的会话持久化开箱即用不需要自己管理 JSON 文件MCP stdio 连接在注入正确的 PATH 后稳定工作多 provider 支持Anthropic/OpenAI 兼容覆盖了 Motive 已有的 provider 列表permissionMode: .bypassPermissions适合桌面应用的自动执行场景需要注意的部分macOS GUI 应用的环境变量PATH问题需要额外处理这不是 SDK 的 bug而是 macOS 的安全机制Swift 并发的Task命名冲突需要手动解决assembleFullToolPool()在无 MCP 时的短路径行为需要了解清楚整体评价SDK 的 API 设计对 GUI 应用集成是友好的。核心的createAgentstream两个调用就替代了原来启动外部进程 HTTP 服务 SSE 客户端 REST API 客户端四个组件。对于一个 361 行的 actor 来

相关新闻