feat: 新增 ResourcePort——host 声明资源目录 + TTS 合成 (issue #152) #161

Merged
Yushu merged 1 commit from feat/152-resource-port-tts into main 2026-08-18 07:32:29 +00:00
Member

Closes #152

实现内容

按 ADR-0001(issue #108 grill Q2 更新节)+ ADR-0011 §6 落地 ResourcePort

  • ports.py:新增 ResourcePort Protocol(list_resources(kind) + synthesize(text, voice_id)),ArisePorts.resource: ResourcePort | None = None——可选,靠 port 是否为 None 当完整信号,不新开 CapabilitySet 布尔位(AC 明确排除)。
  • tools.py:新增 list_resources 工具,resource_available 门控(同 delegate_enabled/capabilities.sticker 一样收在 build_tool_schemas 内部,因为它同时影响 send_message 的字段描述,两处必须用同一个值);content_type=sticker/voice 的字段描述按 resource_available 分流,无 port 时字面不变(ADR 原文"退回现状")。
  • runtime_loop.py
    • _handle_list_resourceskind 显式白名单校验(不直接透传)——两轴 design-verify 抓到:Literal 类型只是静态标注,模型填出非法值时若不校验,异常会一路冒泡到没有任何兜底的 fire-and-forget 触发任务,整轮回复静默消失。
    • content_type=voice 语义调整:有 resource port 时 content 是待合成文本、新增 voice_id 参数(从 list_resources('voice') 选);没有 port 时维持现状(裸引用直通 send_voice)。
    • _handle_send_message 重构:content_type 能力降级判定拆成独立的 _resolve_content_type,连同 voice_id 校验一起挪到 MAX_SENDS_PER_TURN 消费之前——同 _handle_edit 已经踩过的教训(两轴 review 抓到的 bug),避免被拒绝的语音调用白吃预算。
    • _send_pendingsynthesize() 调用放在 flush 阶段而不是工具调用阶段——避免阻塞模型的工具循环,且复用 _flush 已有的、覆盖全部 egress 调用的 send-failure 处理路径。

设计判断(已过对抗式验证)

content_type=voice 该怎么把"待合成文本"和"voice_id"两个值都传给 core,ADR 原文没有逐字规定到 schema 参数这一层——判断结论:content 复用为自由文本、新增 voice_id 独立参数,是 synthesize(text, voice_id) 这个双参签名下唯一可行的实现路径。这个判断经过两轮 design-verify(12+9 个对抗式核实 agent)逐条核实,另发现并修正了 3 处 HIGH 级问题:内容字段描述未按 sticker/voice 分叉(会让模型把 voice_id 当 content 填、合成出字面 resource_id)、kind 参数未校验的静默失败风险、voice_id 校验闸序不对导致的预算误消费。

Not in scope(按票面)

  • search_stickers(习得贴纸)不受影响,与资源 port 静态目录并存不合并(ADR-0024 既有边界)。
  • char_count 不接经济 hook 消费——synthesize 只产出,不管谁消费(issue #157 范围)。

测试

  • 新增 tests/test_list_resources_tools.py:工具可见性(有/无 port 对照)、kind 校验、run_limit 计入。
  • tests/test_runtime_loop.py::TestRichMediaSend 新增 3 条:resource 存在时先合成再发送、voice_id 缺失时闸序正确(不吃预算)、能力降级优先于 voice_id 校验。
  • tests/test_tools.pybuild_tool_schemas 全集穷尽性守卫补上 resource_available 维度(两轴 review 抓到的覆盖缺口)。
  • 全部 1898 个测试通过;对 3 处关键路径(闸序、kind 校验、synthesize 接线)做过变异测试,确认新增测试真实覆盖。
  • ruff checklint-importsuvx ty check 均通过(ty 报的 47 条既有 diagnostics 与本次改动无关,逐条核实过)。
Closes #152 ## 实现内容 按 ADR-0001(issue #108 grill Q2 更新节)+ ADR-0011 §6 落地 `ResourcePort`: - `ports.py`:新增 `ResourcePort` Protocol(`list_resources(kind)` + `synthesize(text, voice_id)`),`ArisePorts.resource: ResourcePort | None = None`——可选,靠 port 是否为 `None` 当完整信号,不新开 `CapabilitySet` 布尔位(AC 明确排除)。 - `tools.py`:新增 `list_resources` 工具,`resource_available` 门控(同 `delegate_enabled`/`capabilities.sticker` 一样收在 `build_tool_schemas` 内部,因为它同时影响 `send_message` 的字段描述,两处必须用同一个值);`content_type=sticker/voice` 的字段描述按 `resource_available` 分流,无 port 时字面不变(ADR 原文"退回现状")。 - `runtime_loop.py`: - `_handle_list_resources`:`kind` 显式白名单校验(不直接透传)——两轴 design-verify 抓到:`Literal` 类型只是静态标注,模型填出非法值时若不校验,异常会一路冒泡到没有任何兜底的 fire-and-forget 触发任务,整轮回复静默消失。 - `content_type=voice` 语义调整:有 `resource` port 时 `content` 是待合成文本、新增 `voice_id` 参数(从 `list_resources('voice')` 选);没有 port 时维持现状(裸引用直通 `send_voice`)。 - `_handle_send_message` 重构:`content_type` 能力降级判定拆成独立的 `_resolve_content_type`,连同 voice_id 校验一起挪到 `MAX_SENDS_PER_TURN` 消费之前——同 `_handle_edit` 已经踩过的教训(两轴 review 抓到的 bug),避免被拒绝的语音调用白吃预算。 - `_send_pending`:`synthesize()` 调用放在 flush 阶段而不是工具调用阶段——避免阻塞模型的工具循环,且复用 `_flush` 已有的、覆盖全部 egress 调用的 send-failure 处理路径。 ## 设计判断(已过对抗式验证) `content_type=voice` 该怎么把"待合成文本"和"voice_id"两个值都传给 core,ADR 原文没有逐字规定到 schema 参数这一层——判断结论:`content` 复用为自由文本、新增 `voice_id` 独立参数,是 `synthesize(text, voice_id)` 这个双参签名下唯一可行的实现路径。这个判断经过两轮 design-verify(12+9 个对抗式核实 agent)逐条核实,另发现并修正了 3 处 HIGH 级问题:内容字段描述未按 sticker/voice 分叉(会让模型把 voice_id 当 content 填、合成出字面 resource_id)、`kind` 参数未校验的静默失败风险、voice_id 校验闸序不对导致的预算误消费。 ## Not in scope(按票面) - `search_stickers`(习得贴纸)不受影响,与资源 port 静态目录并存不合并(ADR-0024 既有边界)。 - `char_count` 不接经济 hook 消费——`synthesize` 只产出,不管谁消费(issue #157 范围)。 ## 测试 - 新增 `tests/test_list_resources_tools.py`:工具可见性(有/无 port 对照)、`kind` 校验、run_limit 计入。 - `tests/test_runtime_loop.py::TestRichMediaSend` 新增 3 条:resource 存在时先合成再发送、voice_id 缺失时闸序正确(不吃预算)、能力降级优先于 voice_id 校验。 - `tests/test_tools.py` 的 `build_tool_schemas` 全集穷尽性守卫补上 `resource_available` 维度(两轴 review 抓到的覆盖缺口)。 - 全部 1898 个测试通过;对 3 处关键路径(闸序、`kind` 校验、synthesize 接线)做过变异测试,确认新增测试真实覆盖。 - `ruff check`、`lint-imports`、`uvx ty check` 均通过(`ty` 报的 47 条既有 diagnostics 与本次改动无关,逐条核实过)。
按 ADR-0001 issue #108 grill Q2 更新节落地 ResourcePort(list_resources +
synthesize),content_type=voice 语义从裸引用改为待合成文本、新增 voice_id
参数(两轴 design-verify 确认这是唯一可行的实现路径),list_resources 工具
可见性靠 port 是否为 None 当信号,不新开 CapabilitySet 布尔位。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Yushu merged commit f8d1ae6edc into main 2026-08-18 07:32:29 +00:00
Yushu deleted branch feat/152-resource-port-tts 2026-08-18 07:32:30 +00:00
Sign in to join this conversation.
No description provided.