# Design: 完整全双工 Agent 语音助手架构 ## Context 当前 Owner 项目已经具备真实语音桌宠的基础能力:本地唤醒词、麦克风采集、VAD/端点、实时字幕、final STT、云端 LLM、本地 TTS、播报、连续追问和初步打断规划。现有实现仍以 turn-based 轮次为中心:用户唤醒后系统录一段、识别、生成、播放,再回到待机或短暂追问窗口。 完整 Agent 语音助手需要更强的架构边界: 1. 音频输入和输出必须同时运行,而不是录完再播。 2. 播放音频必须回注给 AEC reference,避免助手自己的声音污染麦克风输入。 3. VAD、STT、LLM、TTS、工具执行和播放都必须能被统一取消。 4. 长期记忆和工具执行必须纳入安全策略,而不是让 LLM 随意执行文本命令。 5. 终端、未来 GUI 桌宠和自动测试必须消费同一事件流。 本设计仅用于 OpenSpec 规划。本阶段不修改 `src/`、不安装依赖、不下载模型、不运行外部工具、不接入 Open Interpreter,也不提交本地未跟踪的 `openinterpreter/`。 ## Goals / Non-Goals **Goals:** 1. 定义全双工音频底座:麦克风 capture、扬声器 render reference、WebRTC APM AEC/NS/AGC、环形缓冲和时钟对齐。 2. 定义状态机:`idle/listening/thinking/speaking/interrupted/tool_running/recovering`。 3. 定义并发任务模型:音频输入、STT、LLM、TTS、播放、工具执行相互独立但可被统一取消。 4. 定义低延迟打断:用户在 `speaking` 中说话时,VAD 快速触发 interruption,目标小于 200 ms。 5. 定义 Streaming STT/TTS:partial/final transcript、LLM token stream、句子切分、TTS chunk 播放。 6. 定义长期记忆:SQLite 保存文本和元数据,FAISS 保存向量索引,敏感内容默认不自动保存。 7. 定义 Tool Router:结构化工具调用协议、安全分类、确认策略、执行预算、防循环、结果脱敏回注。 8. 定义 Open Interpreter 外部 adapter:只作为 CLI/子进程后端候选,不复制外部仓库。 9. 定义 Playwright browser adapter 和未来 `ComputerControlProvider` 预留边界。 10. 定义测试策略:fake APM/VAD/STT/TTS/memory/tool、端到端模拟、性能指标和安全回归。 **Non-Goals:** 1. 本阶段不实现运行代码。 2. 本阶段不安装 WebRTC APM、Silero、Faster Whisper、SenseVoice、CosyVoice、FAISS、Playwright 或 Open Interpreter。 3. 本阶段不下载模型。 4. 本阶段不改变 `.env`、`pyproject.toml`、`src/`、`tests/`、`scripts/`。 5. 第一版规划不做 GUI 点击、键盘、屏幕控制,只预留公共 provider。 6. 第一版不做长期主人声纹注册和多人身份鉴权。 7. 第一版不自动执行高风险电脑控制、上传、交易、删除、权限修改或账号操作。 ## Decisions ### Decision 1: WebRTC APM 作为默认音频底座 采用 `WebRtcAudioProcessingStage` 作为全双工默认音频预处理层,启用 AEC、NS、AGC。 **理由:** 1. AEC 需要播放 reference 才能可靠消除助手自己的声音,音色门控只能做后验抑制。 2. NS 能降低风扇、环境声和麦克风底噪对持续 VAD/STT 的影响。 3. AGC 能减少用户远近变化导致的阈值不稳定。 **替代方案:** 1. 继续使用 GTCRN 降噪:能改善噪音,但不能处理扬声器回声。 2. 继续使用音色门控:实现成本低,但对不同扬声器、房间回声和 TTS 音色变化不稳定。 3. 只靠 VAD/STT 置信度:误触发风险高,不适合作为全双工底座。 ### Decision 2: capture/render 双环形缓冲 设计 `CaptureRingBuffer` 和 `RenderReferenceRingBuffer`。麦克风帧和播放帧都按 monotonic timestamp 写入,APM 处理 capture frame 时读取相邻 render reference。 **理由:** 1. 全双工需要持续输入和持续输出,不能用同步读写阻塞。 2. AEC 对 reference 时序敏感,必须保留时间戳和 drift 监控。 3. Ring buffer 可为 STT、VAD、打断检测和测试提供一致帧来源。 **替代方案:** 1. 直接从 sounddevice callback 推到各 stage:耦合高,难测试,易阻塞。 2. 文件式临时 wav:延迟高,不适合全双工。 ### Decision 3: 统一状态机和事件总线 新增 `FullDuplexAgentStateMachine` 和 `PipelineEventBus`。状态机只负责合法转移,事件总线承载终端、GUI 和测试可观察行为。 **理由:** 1. 现有 turn-based 状态无法表达 `tool_running` 和 `interrupted`。 2. 终端和未来 GUI 必须共享事件,不应各自读取内部字段。 3. 测试可以断言事件顺序、耗时和错误恢复。 ### Decision 4: Cancellation Graph 管理所有可中断任务 每个用户输入 turn 创建 root cancellation token,LLM、TTS、playback、tool 子任务挂在 root 下。用户打断时 root token 广播取消。 **理由:** 1. 播放停止但 LLM 继续生成会浪费资源并污染上下文。 2. TTS 合成继续运行会造成卡顿和旧回复残留。 3. 工具执行必须区分可取消和不可安全取消。 ### Decision 5: Streaming STT 分层输出 `StreamingSttProvider` 输出 `partial`、`stable_partial` 和 `final`。终端可展示 stable partial;LLM 默认只接收 final。 **理由:** 1. 用户需要实时看到系统听到了什么。 2. partial 抖动不能直接写入上下文。 3. final 是唯一可靠的 LLM 用户输入。 **开发候选:** 1. `faster-whisper`:开发阶段优先,生态成熟。 2. `SenseVoice`:产品候选,中文和情绪能力更强,需要评估部署成本。 3. `sherpa-onnx`:保留当前兼容 adapter,降低迁移风险。 ### Decision 6: LLM token stream 按句进入 Streaming TTS LLM delta 进入 `SentenceSegmenter`。达到完整句子、语义停顿或最大等待阈值后,将净化后的文本交给 TTS。 **理由:** 1. 不等待完整回复能显著缩短首音延迟。 2. 句子级 TTS 比 token 级 TTS 更自然。 3. 现有 TTS sanitizer 可复用,避免表情包、emoji 被读出。 ### Decision 7: 长期记忆使用 FAISS + SQLite SQLite 保存记忆文本、类型、敏感度、来源、时间和 checksum;FAISS 保存 embedding 向量。 **理由:** 1. SQLite 适合可审计元数据和删除。 2. FAISS 适合向量相似检索。 3. 两者分离便于一致性检查和索引重建。 **替代方案:** 1. 只用 SQLite FTS:部署简单,但语义召回弱。 2. 直接把历史对话全文塞进 prompt:隐私和成本都不可控。 3. 使用外部向量数据库:当前单机桌宠不需要额外服务复杂度。 ### Decision 8: Tool Router 先安全工具后电脑控制 第一版只规划低风险工具:`memory.search`、`memory.save`、`shell.readonly`、`openinterpreter.run`、`browser.playwright`。GUI 电脑控制只预留。 **理由:** 1. 语音助手执行工具的风险高于文本聊天,必须先定义确认策略。 2. Open Interpreter 能节省电脑任务开发时间,但必须被当成外部受限后端。 3. GUI 点击/键盘/屏幕控制需要 Accessibility 权限和更复杂安全策略,不适合第一阶段自动执行。 ### Decision 9: Open Interpreter 作为外部 CLI adapter 本地 `openinterpreter/` 是未跟踪新 Rust 版 Open Interpreter。Owner 不复制、不 vendoring、不提交它,只在后续实现中通过命令路径调用外部 CLI。 **理由:** 1. 避免把外部大型仓库混入 Owner。 2. 保持 Owner 的工具边界清晰。 3. 可以对 CLI 调用做超时、目录限制、输出截断和确认策略。 ### Decision 10: Codex Computer Use 只参考安全策略 Codex App 的 Computer Use 能力可作为“需要确认、限制高风险操作、避免私自执行 GUI 动作”的安全策略参考,但不复制私有实现。 **理由:** 1. 用户希望从 Codex 控制电脑技能中复刻思路,而不是复制私有实现。 2. 公共落地路线应基于 macOS Accessibility、Playwright、trycua 等能力。 3. 第一版先不做 GUI 控制可降低安全和实现风险。 ## Concurrent Task Model ### Runtime task groups 1. `audio_capture_task`:从麦克风读取帧,写入 capture ring buffer。 2. `audio_render_task`:从 playback queue 取 PCM,送扬声器并写入 render reference buffer。 3. `apm_task`:处理 capture frames,输出 cleaned frames。 4. `vad_interrupt_task`:对 cleaned frames 做 VAD 和打断检测。 5. `stt_task`:消费 cleaned frames,输出 partial/stable/final transcript。 6. `llm_task`:消费 final transcript、记忆和工具结果,输出 token stream 或 tool call。 7. `tts_task`:消费句子片段,输出 PCM chunks。 8. `tool_task`:执行已批准工具,输出脱敏结果。 9. `event_task`:聚合事件、指标和 reporter 输出。 ### Backpressure 1. Ring buffer 有固定容量,超出容量丢弃最旧非关键帧并发出 `audio_buffer_overrun`。 2. TTS queue 超限时暂停 LLM sentence enqueue 或请求 LLM stream 暂停/取消。 3. Tool output 超限时截断并返回 `truncated=true`。 ### Cancellation 1. `interrupt_detected` 触发 root cancellation。 2. LLM stream 立即关闭连接或停止读取。 3. TTS session 停止合成并释放资源。 4. Playback queue 清空未播放 chunks,保留已播放文本边界。 5. 可取消工具收到 token 后停止;不可取消工具标记为 pending cleanup。 ## Audio Ring Buffer Design ### Frame format 1. 默认 16 kHz 或 48 kHz 内部采样率需在实现前确认。 2. 默认 mono capture;render reference 可 mono 或 stereo downmix。 3. 默认 frame size 为 20 ms。 4. 每帧包含 `frame_id`、`timestamp_monotonic_ms`、`sample_rate`、`channels`、`samples`。 ### Reference alignment 1. render frame 写入 reference buffer 时记录实际播放排队时间和预计播放时间。 2. capture frame 进入 APM 时按 timestamp 查找 reference window。 3. drift 超过阈值时发 `audio_reference_drift`,并降低打断置信度。 ### Fallback 1. APM 不可用时,若 `OWNER_AUDIO_APM_REQUIRED=1`,全双工模式启动失败。 2. 若允许 fallback,则使用现有 GTCRN denoiser、assistant playback gate 和 conservative VAD,但必须标记 `apm_fallback=true`。 ## Tool Router Protocol ### Tool call request ```text ToolCallRequest: id: str name: str arguments: dict requested_by_turn_id: str natural_language_intent: str timeout_ms: int ``` ### Tool decision ```text ToolDecision: action: execute | reject | require_confirmation risk_level: low | medium | high | forbidden reason: str sanitized_arguments: dict confirmation_prompt: str | null ``` ### Tool result ```text ToolResult: id: str status: success | failed | cancelled | rejected | confirmation_required output_text: str output_truncated: bool error_code: str | null duration_ms: int audit_summary: str ``` ### First-version tools 1. `memory.search`:只读,低风险。 2. `memory.save`:低到中风险,敏感内容需要确认。 3. `shell.readonly`:低到中风险,只允许 allowlist 只读命令。 4. `openinterpreter.run`:默认中风险,第一版默认关闭;执行前必须经过目录、意图和风险校验。 5. `browser.playwright`:默认中风险,第一版默认关闭;账号、支付、提交、购买流程必须确认或拒绝。 ## Memory Retrieval Chain 1. 用户 final transcript 进入 `MemoryQueryBuilder`。 2. Query builder 结合当前任务、会话摘要和用户文本生成检索 query。 3. Embedding provider 生成 query vector。 4. FAISS 返回候选 ids。 5. SQLite 读取 metadata,过滤 disabled、sensitive、expired、low-confidence records。 6. Re-ranker 按相似度、类型、最近使用、用户显式偏好排序。 7. Top-K 以独立 `memory_context` 注入 LLM。 8. LLM 回复完成后,`MemoryWriteCandidateExtractor` 生成候选记忆。 9. Safety classifier 决定保存、丢弃或请求确认。 ## Migration Plan 1. 保留现有 turn-based `run-live` 作为稳定路径。 2. `run-agent-live` 是真实全双工 Agent 入口;`--check-config` 只做配置检查,不带该参数时必须启动运行时。 3. 先落地 fake provider 和模拟端到端,再接真实 WebRTC APM。 4. STT/TTS provider 先用兼容 adapter 接现有能力,再替换为 streaming provider。 5. 长期记忆默认关闭或空库启动,完成删除/禁用/隐私文档后再默认开启。 6. Tool Router 默认只启用 `memory.search`;其他工具按风险逐步开放。 7. Open Interpreter 和 Playwright adapter 默认关闭,用户显式配置后才可用。 8. GUI 电脑控制另开 OpenSpec 变更,不混入第一版全双工音频和安全工具验收。 ## Live Runtime Revision 第一版真实 `run-agent-live` 采用软件 render-reference gate,不新增 WebRTC APM 重依赖。播放队列把已播放 PCM chunk 写入进程内 render reference;后台麦克风监听持续读取 capture frames,先用 VAD 判断有效人声,再与近期 render reference 做相似度/能量门控。候选音频不像助手回放且持续达到最短人声时长时,立即设置 playback stop event;STT 只用于打断后的用户文本识别,不再作为停止播放的前置条件。 `run-live` 保持旧 wake/turn-based 入口。`run-agent-live` 跳过唤醒词和 ACK,常驻 `listening -> thinking -> speaking -> interrupted/listening`,打断后把已确认的用户音频接到下一轮 capture,避免丢首字。长期记忆、Tool Router、Open Interpreter 仍默认关闭。 ## Rollback Strategy 1. 若全双工音频不稳定,可通过配置回退 turn-based `run-live`。 2. 若 APM provider 失败,可禁用 full-duplex mode 或启用 fallback 标记的旧链路。 3. 若长期记忆异常,可设置 `OWNER_MEMORY_ENABLED=0`,继续短期对话。 4. 若 Tool Router 风险过高,可设置 `OWNER_TOOL_ROUTER_ENABLED=0`,保留纯聊天。 5. 若 Open Interpreter 或 Playwright adapter 出错,只禁用对应工具,不影响语音主循环。 ## Risks / Trade-offs 1. WebRTC APM 依赖复杂 -> 通过 provider 抽象、fake APM 测试和 fallback 降低风险。 2. 全双工并发复杂 -> 通过 bounded queue、cancellation graph、虚拟时钟测试和事件指标控制。 3. 流式 STT/TTS 模型较重 -> 分阶段实现,先 provider interface 和 fake tests,再接真实模型。 4. 长期记忆有隐私风险 -> 默认敏感不保存、可禁用、可删除、可审计。 5. 工具执行有安全风险 -> 安全工具优先,高风险确认或拒绝,输出脱敏和审计日志。 6. Open Interpreter 能力强但风险大 -> 默认关闭、低风险受限执行、外部 CLI adapter、禁止复制仓库。 7. Playwright 登录态风险 -> 第一版不默认使用用户登录态,敏感流程确认或拒绝。 8. 第一阶段 scope 大 -> 按音频、状态机、STT/TTS、记忆、工具拆分里程碑,每个里程碑独立提交。 ## Open Questions 1. 全双工入口是否命名为 `run-agent-live`,还是通过 `run-live --mode full-duplex` 启用。 2. WebRTC APM 的具体 Python/macOS 绑定选择需要验证。 3. 内部采样率统一用 16 kHz 还是 48 kHz,需要结合 APM、STT、TTS provider 决定。 4. SenseVoice 和 CosyVoice 是否作为产品强依赖,还是只保留候选。 5. 长期记忆是否默认开启,需要用户确认隐私预期。 6. Open Interpreter 是否允许写操作;若允许,确认流程和 sandbox 需要单独设计。 7. Playwright 是否允许使用现有 Chrome 登录态。 8. 后续 ComputerControlProvider 是否采用 macOS Accessibility、trycua 或其他公共方案。