Skip to content

symfonic.core.contracts.interrupt

interrupt

Generic interrupt contracts — Roadmap Item 9 (v7.2-bound).

Provides the public Pydantic shapes that surface a named graph pause to stream consumers. ask_user continues to use its own dedicated event classes (AskUserQuestionEvent / AskUserAnsweredEvent / AskUserExpiredEvent); the generic interrupt path is additive and emitted only when a caller registers a custom interrupt name and toggles FrameworkConfig.experimental_interrupt=True.

Event taxonomy (mirrors the ask_user trio so consumers can hand-roll identical UI treatments):

  • :class:InterruptEvent -- the pause itself; carries name, opaque token, serialized payload, expiry.
  • :class:InterruptResolvedEvent -- emitted on successful /resume of a generic interrupt (analogous to AskUserAnsweredEvent).
  • :class:InterruptExpiredEvent -- emitted when the janitor sweeps an expired checkpoint (analogous to AskUserExpiredEvent).

Replay semantics

Each interrupt is independent. Two registered interrupts "a" and "b" triggered in sequence within one run mint two separate JTIs and two separate pause tokens; interrupt_consumed_tokens.jti PRIMARY KEY enforces single-use per token, but b's token can be consumed before or after a's -- the rows do not depend on each other.

Scope semantics

By default a pause token is bound to the FrameworkTenantScope that minted it (via PauseToken.hash_scope). A registration may opt into cross_scope_allowed=True to permit redemption under a different scope (e.g. tenant-admin approving a sub-tenant action). When cross-scope is allowed, the resume path still logs the mismatch as an audit event so the security envelope's relaxation is observable in operations.

InterruptEvent

Bases: BaseModel

Generic pause event emitted when a registered interrupt fires.

Mirrors :class:AskUserQuestionEvent for non-ask_user interrupts.

Fields

name: Registered interrupt name (must match a prior :meth:SymfonicAgent.register_interrupt call). pause_token: Opaque JWT carrying the same security envelope as ask_user (HMAC-signed, scope-bound, request-hash-bound, expiry-bounded, single-use via the DB-backed interrupt_consumed_tokens table). payload: JSON-serialized payload matching the registered payload_schema. expires_at: Token expiry (UTC). Resume after this raises. interrupt_id: Client-side correlation id, format i-<12hex>.

InterruptExpiredEvent

Bases: BaseModel

Would be emitted when a janitor sweeps an expired interrupt token.

.. warning:: Nothing constructs this today. There is no janitor. A handler written for it will never be called.

Analogous to :class:~symfonic.core.contracts.types.AskUserExpiredEvent, including the reasoning for why no sweeper was built -- see that class's docstring for the decision and what replaced it.

InterruptRegistration dataclass

InterruptRegistration(name: str, payload_schema: type[BaseModel], response_schema: type[BaseModel], validate_response: Callable[[BaseModel, BaseModel], None] | None = None, cross_scope_allowed: bool = False, built_in: bool = False, metadata: dict[str, Any] = dict())

Boot-time registration record for a named interrupt point.

Held in a per-agent registry (SymfonicAgent._registered_interrupts) and consulted by the engine's mint path AND the resume router so the same metadata controls both ends of the pause/resume cycle.

Attributes:

Name Type Description
name str

Stable identifier (e.g. "approval_required"). Must be a valid Python identifier-like string -- the engine echoes it into stream events and the resume URL is name-aware.

payload_schema type[BaseModel]

Pydantic BaseModel subclass for the agent-side payload (i.e. what the LLM/graph hands to the user).

response_schema type[BaseModel]

Pydantic BaseModel subclass for the user-side response (i.e. what the user POSTs to /resume).

validate_response Callable[[BaseModel, BaseModel], None] | None

Optional cross-validator that receives (response, payload) and raises on mismatch. Used by ask_user to verify selections match the question's allowed labels; custom interrupts can skip this if the response schema's own validators are sufficient.

cross_scope_allowed bool

When True, the resume endpoint accepts a token issued under one scope to be redeemed under another. Logs an audit entry on each cross-scope redemption. When False (default) same-scope binding is enforced.

built_in bool

True for ask_user; False for caller-registered interrupts. The engine uses this to skip the experimental_interrupt flag check for built-ins.

InterruptResolvedEvent

Bases: BaseModel

Emitted when a generic interrupt is resumed successfully.

Companion to InterruptEvent; analogous to AskUserAnsweredEvent.

ResumeDispatch dataclass

ResumeDispatch(name: str, is_ask_user: bool, known: bool, response_schema: type[BaseModel] | None = None)

Which registration a pause token belongs to, and what validates its body.

The /resume handler used to answer this itself: decode the token with the agent's private config, look the name up in the agent's private registry, and reason about built_in. That is three engine internals and a policy decision inside a mapper (TRN-1/TRN-2), and it put token reading in the layer where none of the pause-token suites can see it.

The value is deliberately not authoritative. name comes from a preview decode that swallows every failure and defaults to ask_user, because the engine re-decodes and raises the typed error on the real path; a transport that pre-empted that decision would move token validation out of the layer that owns it.

Attributes:

Name Type Description
name str

The interrupt name claimed by the token, or "ask_user".

is_ask_user bool

Dispatch to the historical resume path.

known bool

A registration exists for name.

response_schema type[BaseModel] | None

The schema that validates the POSTed body, or None when the ask_user contract applies.