The built-in one-question ask_user pause, carried on the typed wire.
Shared by recovered continuations and the selected root's guarded typed
stream, so a pause minted with the platform's default registration is framed
the same way on both: as the generic InterruptEvent with its real
one-field payload, never as an AskUserQuestionEvent it cannot satisfy.
native_pause_events
async
native_pause_events(source: AsyncIterator[AgentEvent]) -> AsyncIterator[AgentEvent]
Keep the native one-question ask_user schema honest on the typed wire.
The shared projector's AskUserQuestionEvent requires a different,
multi-question payload. The platform's built-in one-question registration
therefore uses the generic typed interrupt, retaining its real payload.
Source code in src/symfonic/agent/native_pause_events.py
| async def native_pause_events(source: AsyncIterator[AgentEvent]) -> AsyncIterator[AgentEvent]:
"""Keep the native one-question ask_user schema honest on the typed wire.
The shared projector's ``AskUserQuestionEvent`` requires a different,
multi-question payload. The platform's built-in one-question registration
therefore uses the generic typed interrupt, retaining its real payload.
"""
from hashlib import sha256
from symfonic.capabilities.human.factory import AskUserPayload
async with aclosing(source):
async for event in source:
pending = event.interrupt
if (
event.kind == "ask_user"
and pending is not None
and isinstance(pending.payload, AskUserPayload)
):
# The one-question registration has a tool call id, but no
# generic interrupt id. Bind a stable public correlation id
# to the already-opaque token without minting new authority.
identifier = f"i-{sha256(pending.token.encode()).hexdigest()[:12]}"
native = pending.model_copy(
update={"kind": "interrupt", "interrupt_id": identifier}
)
yield event.model_copy(update={"kind": "interrupt", "interrupt": native})
else:
yield event
|