fix: relay confirmation interrupts past muted input
This commit is contained in:
@@ -3,6 +3,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections import deque
|
||||
from collections.abc import Awaitable, Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Literal, Protocol
|
||||
|
||||
@@ -30,6 +32,14 @@ class _PendingClientToolCall:
|
||||
interrupt_on_result: bool = False
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class _DeferredClientToolResult:
|
||||
"""A successful result waiting for the media interruption barrier."""
|
||||
|
||||
future: asyncio.Future[dict[str, Any]]
|
||||
response: dict[str, Any]
|
||||
|
||||
|
||||
class ClientToolPort(Protocol):
|
||||
async def call(
|
||||
self,
|
||||
@@ -49,8 +59,17 @@ class ClientToolBroker(FrameProcessor):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self._pending: dict[str, _PendingClientToolCall] = {}
|
||||
self._deferred_results: deque[_DeferredClientToolResult] = deque()
|
||||
self._interrupt_handler: Callable[[], Awaitable[None]] | None = None
|
||||
self._closed_message: str | None = None
|
||||
|
||||
def set_interrupt_handler(
|
||||
self,
|
||||
handler: Callable[[], Awaitable[None]],
|
||||
) -> None:
|
||||
"""Use a processor downstream of muted user input as interrupt source."""
|
||||
self._interrupt_handler = handler
|
||||
|
||||
async def call(
|
||||
self,
|
||||
function_name: str,
|
||||
@@ -142,21 +161,42 @@ class ClientToolBroker(FrameProcessor):
|
||||
logger.debug(f"忽略未知或过期的客户端工具结果: {tool_call_id}")
|
||||
return
|
||||
|
||||
status = str(message.get("status") or "error")
|
||||
# Keep interruption in the input-frame path, matching text input.
|
||||
# Resolve the tool future only after the interruption has been
|
||||
# broadcast, so the workflow cannot enqueue its next node first.
|
||||
if status == "ok" and pending.interrupt_on_result:
|
||||
await self.broadcast_interruption()
|
||||
|
||||
future = pending.future
|
||||
status = str(message.get("status") or "error")
|
||||
if status == "ok":
|
||||
future.set_result(
|
||||
{
|
||||
"status": "ok",
|
||||
"data": message.get("data"),
|
||||
}
|
||||
)
|
||||
response = {
|
||||
"status": "ok",
|
||||
"data": message.get("data"),
|
||||
}
|
||||
if pending.interrupt_on_result:
|
||||
# Match text input exactly: broadcasting only starts the
|
||||
# interruption. The assistant aggregator calls
|
||||
# on_interruption_processed() after the old response state has
|
||||
# actually been cleared; only then may the workflow continue.
|
||||
deferred = _DeferredClientToolResult(
|
||||
future=future,
|
||||
response=response,
|
||||
)
|
||||
self._deferred_results.append(deferred)
|
||||
try:
|
||||
if self._interrupt_handler is not None:
|
||||
await self._interrupt_handler()
|
||||
else:
|
||||
await self.broadcast_interruption()
|
||||
except Exception as exc:
|
||||
try:
|
||||
self._deferred_results.remove(deferred)
|
||||
except ValueError:
|
||||
# The downstream acknowledgement may have won the
|
||||
# race before an upstream broadcast failure arrived.
|
||||
pass
|
||||
if not future.done():
|
||||
future.set_exception(
|
||||
ClientToolError("客户端工具结果中断处理失败")
|
||||
)
|
||||
logger.exception(f"客户端工具结果中断失败: {exc}")
|
||||
return
|
||||
future.set_result(response)
|
||||
else:
|
||||
future.set_result(
|
||||
{
|
||||
@@ -166,7 +206,18 @@ class ClientToolBroker(FrameProcessor):
|
||||
}
|
||||
)
|
||||
|
||||
def on_interruption_processed(self) -> None:
|
||||
"""Release one result after the aggregator acknowledges interruption."""
|
||||
while self._deferred_results:
|
||||
deferred = self._deferred_results.popleft()
|
||||
if deferred.future.done():
|
||||
continue
|
||||
deferred.future.set_result(deferred.response)
|
||||
logger.debug("客户端工具结果已通过中断处理边界")
|
||||
return
|
||||
|
||||
def _fail_pending(self, message: str) -> None:
|
||||
self._deferred_results.clear()
|
||||
for pending in self._pending.values():
|
||||
future = pending.future
|
||||
if not future.done():
|
||||
|
||||
Reference in New Issue
Block a user