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

Emitted when the janitor sweeps an expired generic-interrupt token.

Analogous to AskUserExpiredEvent.

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.