Skip to content

symfonic.agent.cutover.continuation

continuation

The public redemption door, and what decides which body opens it (TA8.45).

TA8.36 walked the public surface structurally and found a fourth route: SymfonicAgent.resume and SymfonicAgent.resume_interrupt each start a turn — a continuation of a paused one — through AgentRuntime.stream_events_workflow.astream_events, and neither consulted a capability decision. Three switches had moved and the door a human-in-the-loop consumer actually knocks on still opened onto the graph runtime.

HK2 (TA8.35) had already built the whole inside: the checkpointer role, the rehydration, the seven-step redemption and three entry points that share one preparation. What nothing under src/ did was reach it. This module is that wiring, and it is deliberately three small functions rather than a second resume implementation — :mod:symfonic.agent.cutover.kernel_resume stays the only place a token is redeemed.

Admission is a recognition question, not an execution one. Three facts decide whether the kernel can serve a redemption, and all three are known before anything is redeemed:

  • a composition root wired a :class:HumanInteractionCapability at all;
  • it wired :attr:~HumanInteractionCapability.decode_token, so the opaque string a consumer holds can be turned back into the envelope that was minted;
  • that transport recognises this string — decode_token returning None means "not one of mine", which is what an HMAC token minted by the pre-cutover engine looks like to it.

Anything that goes wrong after those three hold is a failure, not a fallback: an expired token, a mismatched axis, a missing checkpoint or a kernel error all propagate to the caller. That rule is ST3's, and it is here for ST3's reason — a route that quietly degraded to the legacy body on failure would make every later "zero legacy reaches" meaningless, because the probe would find the body unreached on a healthy run and reached on a broken one, and no certificate can tell those two apart.

Recognition

Recognition(*, envelope: Any = None, refusal: str | None = None)

What the door found: an envelope to continue with, or why it cannot.

Two fields and never both: envelope is the thing HK2's prepare takes, refusal is the counted fallback reason. Keeping them on one value is what stops a caller reading "no envelope" as "no reason" and dispatching to the kernel with None.

Source code in src/symfonic/agent/cutover/continuation.py
def __init__(self, *, envelope: Any = None, refusal: str | None = None) -> None:
    self.envelope = envelope
    self.refusal = refusal

recognise

recognise(capability: Any, pause_token: str) -> Recognition

Can the kernel serve this redemption, and if not, why not — by name.

Called before the switch is even consulted is not what happens: the switch is read first, and this is asked only of a flipped one, so a rolled-back capability never touches the transport. Both orders give the same answer; this one keeps the rollback lever total.

decode_token is allowed to raise, and a raise is not converted into a refusal here. A transport that cannot parse a string it did issue has a real fault, and turning it into a silent legacy continuation is the degrade this module's docstring refuses.

Source code in src/symfonic/agent/cutover/continuation.py
def recognise(capability: Any, pause_token: str) -> Recognition:
    """Can the kernel serve this redemption, and if not, why not — by name.

    Called before the switch is even consulted is *not* what happens: the
    switch is read first, and this is asked only of a flipped one, so a
    rolled-back capability never touches the transport. Both orders give the
    same answer; this one keeps the rollback lever total.

    ``decode_token`` is allowed to raise, and a raise is not converted into a
    refusal here. A transport that cannot parse a string it *did* issue has a
    real fault, and turning it into a silent legacy continuation is the degrade
    this module's docstring refuses.
    """
    if capability is None:
        return Recognition(refusal=NO_PAUSE_CAPABILITY)
    decode = getattr(capability, "decode_token", None)
    if decode is None:
        return Recognition(refusal=NO_DECODE_TRANSPORT)
    envelope = decode(pause_token)
    if envelope is None:
        return Recognition(refusal=NOT_A_CAPABILITY_TOKEN)
    return Recognition(envelope=envelope)