symfonic.kernel.contracts.interrupts¶
interrupts ¶
A run that stopped to ask a person, and the vocabulary it stops in (HK1).
Until TA8.34 the kernel had no way to say "this run is waiting for a human".
Its event vocabulary held seven kinds -- thinking, text_delta,
tool_call, tool_result, done, error, cancelled -- and none
of them is a pause: done says the run answered, error says it broke,
cancelled says somebody stopped it. A run that is waiting is none of
those, so the information never reached the typed loop at all and there was
nothing there to translate. That is the gap ST2 recorded as a blocker on ST3's
flip, and this module is the half of the fix that lives in the kernel.
Two things live here and nothing else.
:class:PendingInterrupt is the fact: which registered interaction the run
stopped at, the payload the caller has to answer, and the token they answer
with. It is a kernel-contracts value -- plain stdlib, no capability imported --
so the capability that mints one and the projection that publishes one can both
name it without either naming the other.
:class:InvocationPaused is the signal. It derives from BaseException
on purpose, for the reason asyncio.CancelledError does: a pause is not a
failure, and every except Exception between the pause point and the runner
would otherwise convert it into one. Two of them are directly in the path and
both are correct as they stand --
symfonic.agent.backend.tools.execute_tool_call turns a raising tool into a
ToolCall.error the model reads as an observation, and
StageDispatcher._invoke turns a raising stage into a FAILED trace. A
pause caught by either would be reported to the model as a broken tool or to
the turn as a crashed stage, and the run would carry on without the answer it
stopped for. Deriving from BaseException is what makes that impossible to
get wrong by writing ordinary error handling.
InvocationPaused ¶
Bases: BaseException
The run stopped at a registered pause point. Not an error (HK1).
Raised by whatever reaches the pause point -- a capability's pre-tool
stage today -- and caught in exactly one place:
symfonic.kernel.runner.InvocationRunner.events, which turns it into the
run's terminal event. It is never converted into an error terminal and
never re-raised at the consumer, because a paused run did not fail.
Source code in src/symfonic/kernel/contracts/interrupts.py
PendingInterrupt
dataclass
¶
PendingInterrupt(name: str, kind: str = 'interrupt', payload: Any = None, token: str = '', tool_call_id: str = '', interrupt_id: str = '', run_id: str = '', session_id: str = '', expires_at: float | None = None, resumable: bool = False)
One pause: what was asked, who may answer, and with which token.
payload is carried, never interpreted. The kernel does not know what an
ask_user request looks like and must not learn: the capability that
registered the interaction validated the payload against its own schema
before minting, and the projection that publishes it is the layer that
knows which public event shape it becomes.
resumable is a declaration, not a wish. TA8.34 mints a pause and
publishes it; nothing in this build redeems one on the kernel route --
resume, rehydration and the checkpointer a resume needs are TA8.35. A pause
that cannot be resumed has to say so, because the alternative is a consumer
holding a token that looks live and answering into nothing.