Files
vllm-omni/vllm_omni/engine/duplex/session/playback_ledger.py
T
chickeytonandClaude Opus 5 99ff4f3070 [Core][Frontend] Unified Full-duplex Framework (#7413)
Signed-off-by: chickeyton <ngton2014@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 15:35:36 +08:00

138 lines
6.3 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Applying one ``playback.ack`` to a duplex session.
The client reports how much of a response it has actually played; the session
uses that to commit or truncate the assistant item in conversation history, and
to release the response's playback state once it is fully played.
Extracted from ``DuplexSessionRunner`` because the only runner state it touched
was ``session``. It returns the events it produces rather than emitting them,
so the rules can be exercised without a runner.
"""
from __future__ import annotations
from vllm_omni.engine.duplex.events import DuplexEvent, PlaybackAcknowledged, error_event
from vllm_omni.engine.duplex.session.engine_session import DuplexEngineSession
from vllm_omni.engine.duplex.session.lease import DuplexLeaseActivity
def apply_playback_ack(session: DuplexEngineSession, event: dict[str, object]) -> list[DuplexEvent]:
"""Apply one ``playback.ack`` and return the events it produces."""
played_ms = event.get("played_ms", event.get("audio_ms", 0))
committed_ms = event.get("committed_ms")
if not isinstance(played_ms, int | float):
return [error_event("bad_event", "playback.ack requires played_ms")]
try:
session.touch_lease(DuplexLeaseActivity.PLAYBACK_ACK)
except Exception:
pass
committed_cursor = int(committed_ms) if isinstance(committed_ms, int | float) else int(played_ms)
item_id = event.get("item_id")
response_id = event.get("response_id")
response_id = response_id if isinstance(response_id, str) and response_id else None
if not isinstance(item_id, str) or not item_id:
item_id = f"item_{response_id}" if response_id is not None else None
elif response_id is None and item_id.startswith("item_"):
response_id = item_id.removeprefix("item_")
if response_id is None and item_id is None and len(session.pending_history_item_ids) == 1:
item_id = next(iter(session.pending_history_item_ids))
if item_id.startswith("item_"):
response_id = item_id.removeprefix("item_")
if response_id is None and item_id is None and session.active_response_id is not None:
response_id = session.active_response_id
item_id = f"item_{response_id}"
if response_id is not None:
expected_item_id = f"item_{response_id}"
if item_id is None:
item_id = expected_item_id
elif item_id != expected_item_id:
return [error_event("playback_item_mismatch", "playback.ack item_id must match item_<response_id>.")]
if not session.has_assistant_response_item(response_id, item_id):
return [
error_event("playback_item_not_found", f"No assistant response item is registered for {response_id}.")
]
if session.playback_ack_is_too_late(response_id, item_id):
return [
error_event("playback_ack_too_late", "playback.ack arrived after a later user input was committed.")
]
session.reserve_history_item(item_id)
elif item_id is not None:
return [error_event("playback_item_not_found", "playback.ack requires a response-owned assistant item.")]
hard_truncate = event.get("truncate") is True
if hard_truncate:
playback = session.acknowledge_playback(int(played_ms), committed_cursor, response_id=response_id)
playback = session.truncate_playback_commit(committed_cursor, response_id=response_id)
else:
playback = session.acknowledge_playback(int(played_ms), committed_cursor, response_id=response_id)
committed_history = False
if isinstance(item_id, str) and item_id:
expected_item_id = f"item_{response_id}" if response_id is not None else None
if (
expected_item_id == item_id
and item_id not in session.history_item_ids
and item_id not in session.pending_history_item_ids
):
session.register_history_item(item_id, None)
committed_history = session.truncate_history_item(
item_id,
audio_end_ms=committed_cursor,
playback=playback,
hard=hard_truncate,
)
elif session.pending_history_item_ids:
pending_ids = list(session.pending_history_item_ids)
if len(pending_ids) == 1:
item_id = pending_ids[0]
committed_history = session.truncate_history_item(
item_id,
audio_end_ms=committed_cursor,
playback=playback,
hard=hard_truncate,
)
elif session.active_response_id is not None:
item_id = f"item_{session.active_response_id}"
committed_history = session.truncate_history_item(
item_id,
audio_end_ms=committed_cursor,
playback=playback,
hard=hard_truncate,
)
elif session.last_assistant_full_message is not None:
if item_id is None and session.history_item_ids:
assistant_item_ids = [
known_item_id
for known_item_id, message in session.history_item_ids.items()
if message.get("role") == "assistant"
]
if len(assistant_item_ids) == 1:
item_id = assistant_item_ids[0]
if isinstance(item_id, str) and item_id:
committed_history = session.truncate_history_item(
item_id,
audio_end_ms=committed_cursor,
playback=playback,
hard=hard_truncate,
)
events: list[DuplexEvent] = [
PlaybackAcknowledged(
details={
"type": "playback.acknowledged",
"session_id": session.session_id,
"epoch": session.epoch,
"item_id": item_id,
"played_ms": int(played_ms),
"committed_ms": committed_cursor,
"truncate": event.get("truncate") is True,
"playback": playback.as_dict(),
"history_committed": committed_history,
}
)
]
if committed_history and committed_cursor >= max(playback.sent_ms, playback.generated_ms):
session.release_response_playback(response_id)
session.release_response_history_snapshot(response_id)
return events