Skip to content

feat: add voice transcript bridge and plugin dispatch infrastructure#1612

Open
MomiJiSan wants to merge 4 commits into
Project-N-E-K-O:mainfrom
MomiJiSan:feat/study-companion-neko-core
Open

feat: add voice transcript bridge and plugin dispatch infrastructure#1612
MomiJiSan wants to merge 4 commits into
Project-N-E-K-O:mainfrom
MomiJiSan:feat/study-companion-neko-core

Conversation

@MomiJiSan
Copy link
Copy Markdown
Contributor

@MomiJiSan MomiJiSan commented Jun 2, 2026

概述 (Part 1/2 — Neko 本体)

从原 PR #1582 拆分。伴学插件变更见 Part 2 #1613

变更范围(11 文件)

app/agent_server.py                                    — voice bridge action 处理
app/main_server.py                                     — voice bridge 注册
main_logic/agent_event_bus.py                          — 语音转录事件总线(新)
main_logic/core.py                                     — 核心 voice bridge 分发
frontend/plugin-manager/…/check-hosted-tsx.mjs         — 前端检查更新
plugin/server/…/dispatch_service.py                    — 通用插件调度(含 _validate_timeout)
plugin/server/…/voice_contracts.py                     — 共享 voice contract 定义(新)
plugin/tests/unit/server/test_plugin_dispatch_service.py — dispatch 测试
tests/unit/test_agent_server_voice_bridge.py           — voice bridge 测试
tests/unit/test_core_game_route_memory_contract.py     — 契约测试
tests/unit/test_voice_bridge_event_bus.py              — 事件总线测试

设计边界

host 只做通道:转发 voice transcript、等待插件结果、执行通用 session 操作。所有伴学业务逻辑在 Part 2 插件内。

审查修复记录

  • ✅ CRITICAL: 边界违规已修复 — arbitrate_voice_transcript_resultsplugin/plugins/study_companion/ 提升到 plugin/server/application/plugins/voice_contracts.py
  • ✅ agent_server.py/core.py/dispatch_service.py 添加 asyncio.CancelledError re-raise
  • ✅ dispatch_service.py 添加 asyncio.TimeoutError 专项处理 + _validate_timeout helper
  • ✅ core.py 移除冗余 _session_changed() 调用

Summary by CodeRabbit

  • 新功能

    • 增加语音转录插件桥接:支持插件订阅、并发分发、仲裁(cancel_response / prime_context / noop)并回传最终结果。
    • 新增可靠的语音桥请求发布与两阶段等待(seen + result)机制。
  • 优化

    • 强化自定义事件分发,增加超时/错误分项与并发隔离处理。
    • 调整路由装配顺序与临时路径解析以改善模块挂载行为。
  • 测试

    • 补充大量单元测试覆盖分发、仲裁、重试、等待/取消与生命周期场景。

Add Neko core infrastructure for companion learning plugins:

- Voice transcript event bridge (main_logic/agent_event_bus.py):
  routes real-time voice transcripts to plugins via custom events
- Plugin dispatch service (plugin/server/.../dispatch_service.py):
  supports cancel_response / prime_context event contracts
- Voice bridge action handling in agent_server.py
- Voice bridge registration wiring in main_server.py
- Hosted TSX check update for study_companion surfaces

The boundary is intentionally narrow: the host only forwards events
and executes generic session actions. All business logic — filtering
rules, action definitions, learning scenarios — stays in the plugin
layer (see companion PR for study_companion plugin).

This is Part 1 of the original PR Project-N-E-K-O#1582 split.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 2, 2026

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7f0ee504-7cc2-4f0c-85ad-fdc99209a024

📥 Commits

Reviewing files that changed from the base of the PR and between fc770d7 and 2c0c978.

📒 Files selected for processing (4)
  • main_logic/core.py
  • tests/unit/test_agent_server_voice_bridge.py
  • tests/unit/test_core_game_route_memory_contract.py
  • tests/unit/test_voice_bridge_event_bus.py
🚧 Files skipped from review as they are similar to previous changes (4)
  • tests/unit/test_core_game_route_memory_contract.py
  • main_logic/core.py
  • tests/unit/test_voice_bridge_event_bus.py
  • tests/unit/test_agent_server_voice_bridge.py

Walkthrough

该 PR 实现语音转写桥接:从发布请求、agent 并发触发插件订阅并仲裁返回,到将结果回填主服并在 core 会话快照一致性下执行动作喵。

Changes

Voice Transcript Bridge System

Layer / File(s) Summary
主服务等待器与可靠发布
main_logic/agent_event_bus.py, tests/unit/test_voice_bridge_event_bus.py
新增跨线程 waiters、notify_voice_bridge_result 与 publish_voice_transcript_request_reliably,支持超时/重试、agent-seen 阶段与事件循环关闭的清理与测试喵。
Agent 端请求解析与回传
app/agent_server.py, tests/unit/test_agent_server_voice_bridge.py
agent_server 增加 voice_transcript_request 后台任务:参数解析、启用/生命周期检查、调用 PluginDispatchService 并仲裁 dispatch 结果,以 voice_bridge_result 回传喵。
回传结果合同与仲裁
plugin/server/application/plugins/voice_contracts.py
新增 voice_transcript 事件/动作常量、payload 工厂与 arbitrate_voice_transcript_results 仲裁逻辑(优先级 cancel>prime>noop,支持 priority 比较与失败/noop 统计)喵。
Core 会话接入
main_logic/core.py, tests/unit/test_core_game_route_memory_contract.py
handle_input_transcript 调用 publish_voice_transcript_request_reliably 并在返回 cancel_response 时中断后续处理,prime_context 注入上下文(含占位符替换与异常/会话替换保护)喵。
插件自定义事件分发
plugin/server/application/plugins/dispatch_service.py, plugin/tests/unit/server/test_plugin_dispatch_service.py
新增 trigger_custom_event_subscribers:查找匹配处理器并并发触发(每处理器深拷贝 args、独立超时/错误封装),并由测试覆盖并发、隔离、超时与错误场景喵。
Main Server 路由与事件接入
app/main_server.py, tests/test_agent_rewrite_regression.py
main_server 新增对 voice_bridge_result/voice_bridge_request_seen 的接收并调用相应 notify 函数,且调整若干 FastAPI 路由的导入与 include 顺序;增加回归测试断言 router 已挂载喵。

其他

Layer / File(s) Summary
服务端免费状态微调
app/agent_server.py
将 _check_agent_api_gate 中 is_free_version 的取值由 cm.is_agent_free() 调整为 cm.is_free_version()(字段名不变)喵。

Sequence Diagram(s)

sequenceDiagram
  participant Core as Core
  participant EventBus as EventBus
  participant Agent as AgentServer
  participant Dispatch as PluginDispatchService
  participant Main as MainServer
  Core->>EventBus: publish_voice_transcript_request_reliably(event_id, transcript, metadata)
  EventBus->>Agent: voice_transcript_request(event_id, transcript, lanlan_name, metadata)
  Agent->>Dispatch: trigger_custom_event_subscribers(event_type, args, timeout)
  Dispatch-->>Agent: per-plugin dispatch results[]
  Agent->>Main: emit voice_bridge_result(event_id, result)
  Main->>EventBus: notify_voice_bridge_result(event_id, result)
  EventBus-->>Core: resolve waiting Future -> return action/result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

喵♪ 语音桥接上线喵,
文本发插件并行跑喵,
等待器守护不慌张喵,
仲裁决定要取消或注入喵,
测试齐全安心睡觉喵 ✨

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 标题准确总结了 PR 的核心改动:添加语音转录桥接和插件分发基础设施,涵盖了 11 个文件中的主要功能实现。
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0bb0d65f72

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread app/main_server.py
Comment on lines 1781 to 1782
app.include_router(game_router)
app.include_router(card_assist_router)
app.include_router(capture_router)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Restore the card-assist router mount

When the main app is started, the four /api/card-assist/* endpoints defined in main_routers/card_assist_router.py are no longer included because the router import/include was removed from this router list. I checked the repo and these endpoints are still referenced by card-assist tests and the Character Card Manager feature, so users clicking the AI-assisted card authoring flow will now get 404s from the real app/main_server.py app rather than reaching the existing handlers.

Useful? React with 👍 / 👎.

Comment thread app/agent_server.py Outdated
Comment on lines +1344 to +1346
cm = get_config_manager()
ok, reasons = cm.is_agent_api_ready()
# 字段名保留 is_free_version(前端/下游 gate 消费者沿用),值取 agent 维度的
# is_agent_free():判 agent 是否走内置免费模型,而非 core/assist 的版本免费。
return {"ready": ok, "reasons": reasons, "is_free_version": cm.is_agent_free()}
return {"ready": ok, "reasons": reasons, "is_free_version": cm.is_free_version()}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep agent gating tied to the agent model

In configurations where the voice/core side is free but the Agent model has been switched to a paid/custom model, this now reports is_free_version: true even though the Agent path is not using the built-in free agent model. ConfigManager.is_agent_api_ready() documents that Agent free/quota UI state is supposed to come from is_agent_free(), and the main router still uses cfg.is_agent_free() for the same field, so this endpoint can re-enable the free-agent quota/warning behavior incorrectly for paid Agent setups.

Useful? React with 👍 / 👎.

Comment thread app/agent_server.py Outdated
Comment on lines +1683 to +1685
from plugin.plugins.study_companion.voice_contracts import (
arbitrate_voice_transcript_results,
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Add the missing voice contract module before importing it

When any voice_transcript subscriber returns a successful action, this import runs, but plugin/plugins/study_companion/voice_contracts.py is not present in the repo (searched for *voice_contracts.py and arbitrate_voice). The resulting ModuleNotFoundError is caught by _handle_voice_transcript_request as dispatch_failed, so valid plugin actions are discarded and the new voice bridge cannot actually arbitrate successful subscriber responses.

Useful? React with 👍 / 👎.

@MomiJiSan MomiJiSan marked this pull request as draft June 2, 2026 14:43
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/agent_server.py`:
- Around line 1683-1720: The host code in _handle_voice_transcript_request
currently imports arbitrate_voice_transcript_results from
plugin.plugins.study_companion.voice_contracts, which couples the host to a
specific plugin; remove that import and either 1) move the arbitration logic
into the host/shared contract layer and call a locally defined
arbitrate_voice_transcript_results implementation, or 2) change the dispatch
layer so dispatch_results already returns a host-consumable standard result
(i.e., include normalized success/result/plugin_id/event_id fields) so no
plugin-specific import is needed; update _handle_voice_transcript_request to use
the new local/shared arbitration function or the normalized dispatch output and
ensure arbitration_items is computed the same way but without importing from
plugin.plugins.study_companion.voice_contracts.

In `@main_logic/agent_event_bus.py`:
- Around line 576-600: The timeout race happens because the recv thread enqueues
voice_bridge_result via run_coroutine_threadsafe but the main loop may not yet
call notify_voice_bridge_result(), so the waiter can be removed before resolve;
fix by adding an intermediate "queued" marker and checking it in the
timeout-retry logic: introduce a new set (e.g. _voice_bridge_waiters_queued) and
have the recv thread mark event_id as queued before calling
run_coroutine_threadsafe (and clear it when notify_voice_bridge_result() runs);
update the timeout branch around waiter/_voice_bridge_waiters_resolving to also
treat queued events as candidates for the short extra wait so the second wait
covers the period between enqueue and actual notify; ensure all places that
remove waiters (_voice_bridge_waiters) also clear queued/resolving safely under
_voice_bridge_waiters_lock.

In `@main_logic/core.py`:
- Around line 2015-2027: The code injects plugin result "context" into
prime_context without running apply_role_placeholders, causing placeholders like
{MASTER_NAME}/{LANLAN_NAME} to be left unexpanded for this pathway; modify the
block handling result.get("context") so that before calling
session_snapshot.prime_context(context_text, ...), you first call
apply_role_placeholders(context_text, session_snapshot or current session) and
use its returned text; keep checks for _session_changed(), respect skipped flag,
and only call prime_context if prime_context is callable and the
placeholder-applied context_text is non-empty.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 47550716-33ea-4c96-bba4-01b6f7ff267e

📥 Commits

Reviewing files that changed from the base of the PR and between f8e28f3 and 0bb0d65.

📒 Files selected for processing (10)
  • app/agent_server.py
  • app/main_server.py
  • frontend/plugin-manager/scripts/check-hosted-tsx.mjs
  • main_logic/agent_event_bus.py
  • main_logic/core.py
  • plugin/server/application/plugins/dispatch_service.py
  • plugin/tests/unit/server/test_plugin_dispatch_service.py
  • tests/unit/test_agent_server_voice_bridge.py
  • tests/unit/test_core_game_route_memory_contract.py
  • tests/unit/test_voice_bridge_event_bus.py

Comment thread app/agent_server.py Outdated
Comment on lines +1683 to +1720
from plugin.plugins.study_companion.voice_contracts import (
arbitrate_voice_transcript_results,
)

arbitration_items: list[dict[str, object]] = []
failure_count = 0
for item in dispatch_results:
if not isinstance(item, Mapping):
continue
if not bool(item.get("success")):
failure_count += 1
continue
result = item.get("result")
if not isinstance(result, Mapping):
continue
action = str(result.get("action") or "").strip()
if not action:
continue
payload: Dict[str, Any] = dict(result)
payload["action"] = action
plugin_id = str(item.get("plugin_id") or "").strip()
if plugin_id:
payload.setdefault("source_plugin", plugin_id)
source_event_id = str(item.get("event_id") or "").strip()
if source_event_id:
payload.setdefault("source_event_id", source_event_id)
arbitration_items.append(
{
"plugin_id": payload.get("source_plugin") or plugin_id,
"event_id": payload.get("source_event_id") or source_event_id,
"success": True,
"result": payload,
}
)

if not arbitration_items:
return _voice_bridge_noop("no_handler_result", failures=failure_count)
payload = arbitrate_voice_transcript_results(arbitration_items)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

把仲裁逻辑从具体插件包里拆出来喵。

这里在 host 侧直接依赖 plugin.plugins.study_companion.voice_contracts,会把这条 Part 1 的通用桥接链路运行时绑死到 Part 2 的 study_companion 实现上喵。只要有任意 voice subscriber 返回结果、但这个插件模块没有随当前部署一起存在,_handle_voice_transcript_request() 就会在这里抛错并回落成 dispatch_failed,整个 voice bridge 就没法独立工作了喵;当前这个 PR 里的 happy-path 单测也会被同一个依赖一起绑住喵。这个仲裁逻辑应该放到 host/shared contract 层,或者让 dispatch 层直接返回 host 可消费的标准结果,而不是从具体插件包反向 import 喵。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/agent_server.py` around lines 1683 - 1720, The host code in
_handle_voice_transcript_request currently imports
arbitrate_voice_transcript_results from
plugin.plugins.study_companion.voice_contracts, which couples the host to a
specific plugin; remove that import and either 1) move the arbitration logic
into the host/shared contract layer and call a locally defined
arbitrate_voice_transcript_results implementation, or 2) change the dispatch
layer so dispatch_results already returns a host-consumable standard result
(i.e., include normalized success/result/plugin_id/event_id fields) so no
plugin-specific import is needed; update _handle_voice_transcript_request to use
the new local/shared arbitration function or the normalized dispatch output and
ensure arbitration_items is computed the same way but without importing from
plugin.plugins.study_companion.voice_contracts.

Comment thread main_logic/agent_event_bus.py
Comment thread main_logic/core.py Outdated
@
fix: resolve plugin boundary violation and improve voice bridge robustness

- CRITICAL: Move arbitrate_voice_transcript_results from
  plugin/plugins/study_companion/voice_contracts.py to
  plugin/server/application/plugins/voice_contracts.py (shared
  infrastructure). Update import in agent_server.py accordingly.
  This fixes the plugin boundary violation where app/ imported
  from a specific plugin (备忘录 §2).

- Add explicit asyncio.CancelledError re-raise before broadcast
  Exception handlers in agent_server.py, core.py, and
  dispatch_service.py (matches codebase conventions).

- Add asyncio.TimeoutError-specific handling in dispatch_service.py
  to distinguish handler timeouts from crashes.

- Extract _validate_timeout() helper in dispatch_service.py to
  eliminate duplicated timeout validation between trigger_custom_event
  and trigger_custom_event_subscribers.

- Remove redundant _session_changed() calls in
  _dispatch_voice_transcript_bridge (core.py) — the outer check at
  the top already guards both cancel_response and prime_context
  branches. Inner checks after await remain as necessary.
@
@MomiJiSan MomiJiSan marked this pull request as ready for review June 2, 2026 16:31
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a9d3b6ec8a

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread main_logic/core.py
self._activity_tracker.on_voice_rms()

if is_voice_source and transcript_text:
voice_bridge_action = await self._dispatch_voice_transcript_bridge(transcript_text)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid blocking voice input when agent is absent

When the main app has started its local PUB socket but agent_server is not connected/running, publish_session_event() still returns true because it only sends to the local ZeroMQ socket and cannot know there are no subscribers; this awaited call then waits the full publish_voice_transcript_request_reliably timeout (default 1.2s) before ordinary transcript handling continues. In that environment every realtime voice transcript is delayed by the bridge even though the plugin result can never arrive, so the fallback path should not synchronously block the voice hot path without a real agent-side readiness/ack signal.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
tests/unit/test_agent_server_voice_bridge.py (1)

227-257: ⚡ Quick win

这个用例名写了“before_dispatch”,但断言还没有验证先后顺序喵。

现在只检查 voice_bridge_request_seen 最终出现;就算 _handle_voice_transcript_request 先跑,这个测试也会通过,回归可能被漏掉喵。建议显式记录执行顺序并断言 seen 早于 handle 喵。

🔧 可参考的最小改法
@@
-    emitted: list[tuple[str, str | None, dict[str, Any]]] = []
+    emitted: list[tuple[str, str | None, dict[str, Any]]] = []
+    order: list[str] = []
@@
     async def _emit_main_event(event_type: str, lanlan_name: str | None, **payload: Any) -> None:
         emitted.append((event_type, lanlan_name, payload))
+        order.append(f"emit:{event_type}")
@@
     async def _handle_voice_transcript_request(_event: dict[str, Any]) -> None:
+        order.append("handle")
         handled.set()
@@
     assert ("voice_bridge_request_seen", "Yui", {"event_id": "voice-seen"}) in emitted
+    assert order.index("emit:voice_bridge_request_seen") < order.index("handle")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/test_agent_server_voice_bridge.py` around lines 227 - 257, The
test name implies verifying ordering but currently only checks presence; modify
the monkeypatched helpers to record execution order and assert that "seen"
occurs before "handle": update the _emit_main_event spy (patched onto
srv._emit_main_event) to append a tuple including a marker like ("seen",
event_type, lanlan_name, payload) into the emitted list when event_type ==
"voice_bridge_request_seen", and update the _handle_voice_transcript_request spy
(patched onto srv._handle_voice_transcript_request) to append a marker like
("handled", ...) before setting the handled event; after calling
srv._on_session_event, assert that the index of the "seen" entry in emitted is
less than the index of the "handled" entry to ensure seen was emitted before
handling.
tests/unit/test_voice_bridge_event_bus.py (1)

116-117: ⚡ Quick win

这里的清理断言建议覆盖全部 voice-bridge waiter 状态喵。

现在只断言 _voice_bridge_waiters == {},如果 _voice_bridge_request_seen_waiters_voice_bridge_waiters_queued_voice_bridge_waiters_resolving 泄漏,会漏检喵。建议抽一个统一 helper,在这些发布成功/超时用例末尾复用喵。

🔧 可参考的断言 helper
+def _assert_voice_bridge_waiters_cleared() -> None:
+    assert agent_event_bus._voice_bridge_waiters == {}
+    assert agent_event_bus._voice_bridge_request_seen_waiters == {}
+    assert agent_event_bus._voice_bridge_waiters_queued == set()
+    assert agent_event_bus._voice_bridge_waiters_resolving == set()
@@
-    assert agent_event_bus._voice_bridge_waiters == {}
+    _assert_voice_bridge_waiters_cleared()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/test_voice_bridge_event_bus.py` around lines 116 - 117, 当前测试仅断言
agent_event_bus._voice_bridge_waiters == {},会漏掉其他 waiter 状态泄露;请在
tests/unit/test_voice_bridge_event_bus.py 抽出一个统一 helper(例如
assert_no_voice_bridge_waiters(agent_event_bus))来同时检查
_voice_bridge_waiters、_voice_bridge_request_seen_waiters、_voice_bridge_waiters_queued
和 _voice_bridge_waiters_resolving 都为空或处于初始状态,然后在所有发布成功/超时用例末尾调用该 helper
来复用断言并覆盖全部 voice-bridge waiter 状态。
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@tests/unit/test_agent_server_voice_bridge.py`:
- Around line 227-257: The test name implies verifying ordering but currently
only checks presence; modify the monkeypatched helpers to record execution order
and assert that "seen" occurs before "handle": update the _emit_main_event spy
(patched onto srv._emit_main_event) to append a tuple including a marker like
("seen", event_type, lanlan_name, payload) into the emitted list when event_type
== "voice_bridge_request_seen", and update the _handle_voice_transcript_request
spy (patched onto srv._handle_voice_transcript_request) to append a marker like
("handled", ...) before setting the handled event; after calling
srv._on_session_event, assert that the index of the "seen" entry in emitted is
less than the index of the "handled" entry to ensure seen was emitted before
handling.

In `@tests/unit/test_voice_bridge_event_bus.py`:
- Around line 116-117: 当前测试仅断言 agent_event_bus._voice_bridge_waiters == {},会漏掉其他
waiter 状态泄露;请在 tests/unit/test_voice_bridge_event_bus.py 抽出一个统一 helper(例如
assert_no_voice_bridge_waiters(agent_event_bus))来同时检查
_voice_bridge_waiters、_voice_bridge_request_seen_waiters、_voice_bridge_waiters_queued
和 _voice_bridge_waiters_resolving 都为空或处于初始状态,然后在所有发布成功/超时用例末尾调用该 helper
来复用断言并覆盖全部 voice-bridge waiter 状态。

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c68d0250-44b0-4f38-a429-af107e2ac510

📥 Commits

Reviewing files that changed from the base of the PR and between a9d3b6e and fc770d7.

📒 Files selected for processing (8)
  • app/agent_server.py
  • app/main_server.py
  • main_logic/agent_event_bus.py
  • main_logic/core.py
  • tests/test_agent_rewrite_regression.py
  • tests/unit/test_agent_server_voice_bridge.py
  • tests/unit/test_core_game_route_memory_contract.py
  • tests/unit/test_voice_bridge_event_bus.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • main_logic/core.py
  • app/agent_server.py
  • main_logic/agent_event_bus.py

@MomiJiSan
Copy link
Copy Markdown
Contributor Author

这个 PR 做了什么

这个 PR 给 Neko 本体新增了通用的 语音转录桥接(voice transcript bridge)插件调度基础设施,11 个文件,不涉及任何具体插件内容。

核心流程:

  1. main_logic/agent_event_bus.py — 新增 publish_voice_transcript_request_reliably,通过 ZMQ 将语音转录文本从 main_server 发给 agent_server
  2. app/agent_server.py — 收到 voice_transcript_request 事件后,通过 PluginDispatchService 广播给所有注册了 voice_transcript 自定义事件的插件,等待插件返回 {action} 结果
  3. plugin/server/application/plugins/dispatch_service.py — 新增 trigger_custom_event_subscribers,并行分发给所有匹配的插件 handler,收集结果
  4. plugin/server/application/plugins/voice_contracts.py — 定义共享的 voice action 契约(noop / cancel_response / prime_context)和仲裁逻辑,同时被 host 层和插件层引用
  5. main_logic/core.py — 在 handle_input_transcript 中接入 voice bridge:插件返回 cancel_response 就丢弃转录(不触发 LLM 响应),返回 prime_context 就注入上下文

为什么要这样做

核心目标是把"伴学语音判断"的通道能力收敛到 Neko 本体,同时把业务决策留给插件。

原来的设计是宿主直接承载伴学业务语义(判断自言自语、要不要打断、注入什么上下文),这违反插件边界。正确边界应该像现在这样:

  • 宿主(本 PR):只在 handle_input_transcript 里挂一个 hook,转发 transcript → 等待插件结果 → 执行通用 session action(cancel_response / prime_context)。宿主不判断、不过滤、不分类。
  • 插件(Part 2 feat: add study companion notebook and voice interaction [Part 2/2] #1613:决定"这段语音是不是自言自语""要不要打断当前对话""要注入什么上下文"。所有业务逻辑在 voice_filter.py / voice_contracts.py 内。

这样后续其他插件(不只是伴学)也可以注册 voice_transcript 事件处理器,宿主不需要任何改动。

边界设计验证voice_contracts.py 放在 plugin/server/application/plugins/(共享基础设施层),app/agent_server.pyplugin/plugins/study_companion/ 都从这个共享位置导入,不存在 app → 具体插件的反向依赖。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant