Make end call tool can be cancelled

This commit is contained in:
Xin Wang
2026-08-12 09:18:40 +08:00
parent fc026e765e
commit d0bb5b3868
15 changed files with 350 additions and 48 deletions
+6 -1
View File
@@ -58,7 +58,12 @@ class CallEndPort(Protocol):
@property
def ending(self) -> bool: ...
def begin(self, reason: str) -> None: ...
@property
def accepts_input(self) -> bool: ...
def begin(self, reason: str, *, interruptible: bool = False) -> None: ...
def cancel_pending_end(self) -> bool: ...
def begin_response(self) -> None: ...
+16 -9
View File
@@ -488,15 +488,12 @@ class PromptBrain(BaseBrain):
if content and not interrupted:
self._store.record("agent", content, completed_agent_turn=True)
self._refresh_prompt()
if (
self._waiting_for_generated_end_speech
and self._runtime is not None
and self._runtime.call_end.ending
):
if self._waiting_for_generated_end_speech:
self._waiting_for_generated_end_speech = False
await self._runtime.call_end.finish_after_current_speech(
has_text=bool(content.strip()) and not interrupted
)
if self._runtime is not None and self._runtime.call_end.ending:
await self._runtime.call_end.finish_after_current_speech(
has_text=bool(content.strip()) and not interrupted
)
def _refresh_prompt(self) -> None:
if self._dynamic_enabled and self._runtime is not None:
@@ -594,12 +591,18 @@ class PromptBrain(BaseBrain):
message_type = str(config.get("message_type") or "none")
custom_message = str(config.get("custom_message") or "").strip()
capture_reason = bool(config.get("capture_reason", True))
allow_end_interruption = bool(
config.get("allow_end_interruption", False)
)
async def end_call(params: FunctionCallParams) -> None:
reason = str(params.arguments.get("reason") or "end_call_tool").strip()
uses_custom_message = message_type == "custom" and bool(custom_message)
self._waiting_for_generated_end_speech = not uses_custom_message
runtime.call_end.begin(reason)
runtime.call_end.begin(
reason,
interruptible=allow_end_interruption,
)
await params.result_callback(
{"status": "success", "action": "ending_call"},
properties=FunctionCallResultProperties(run_llm=False),
@@ -610,6 +613,8 @@ class PromptBrain(BaseBrain):
# invoking this tool. Decide at assistant-text-end whether to
# wait for that TTS audio or finish immediately for tool-only calls.
return
if not runtime.call_end.ending:
return
turn_id = uuid4().hex
timestamp = time_now_iso8601()
@@ -634,6 +639,8 @@ class PromptBrain(BaseBrain):
await runtime.queue_frame(
OutputTransportMessageUrgentFrame(message=message)
)
if not runtime.call_end.ending:
return
runtime.call_end.arm_after_speech()
await runtime.queue_frame(
TTSSpeakFrame(custom_message, append_to_context=False)
+12 -9
View File
@@ -603,15 +603,12 @@ class WorkflowBrain(BaseBrain):
self._state.consume_user_turn()
if self._engine.node_type(self._state.current_node_id) == "agent":
self._state.status = WorkflowStatus.WAITING_USER
if (
self._waiting_for_generated_end_speech
and self._runtime is not None
and self._runtime.call_end.ending
):
if self._waiting_for_generated_end_speech:
self._waiting_for_generated_end_speech = False
await self._runtime.call_end.finish_after_current_speech(
has_text=bool(content.strip()) and not interrupted
)
if self._runtime is not None and self._runtime.call_end.ending:
await self._runtime.call_end.finish_after_current_speech(
has_text=bool(content.strip()) and not interrupted
)
async def on_realtime_user_speech_started(self) -> None:
if self._realtime_controller is not None:
@@ -1090,13 +1087,19 @@ class WorkflowBrain(BaseBrain):
message_type = str(config.get("message_type") or "none")
custom_message = str(config.get("custom_message") or "").strip()
capture_reason = bool(config.get("capture_reason", True))
allow_end_interruption = bool(
config.get("allow_end_interruption", False)
)
async def handler(args, _flow_manager):
reason = str((args or {}).get("reason") or "end_conversation").strip()
uses_custom_message = message_type == "custom" and bool(custom_message)
self._waiting_for_generated_end_speech = not uses_custom_message
runtime = self._require_runtime()
runtime.call_end.begin(reason)
runtime.call_end.begin(
reason,
interruptible=allow_end_interruption,
)
if uses_custom_message:
await self._queue_visible_speech(
custom_message,