# Self-Describing Interrupt Contracts Date: 2026-07-01 Status: Implemented. This document is the live interrupt contract. Related: - [Current roadmap](../../current_roadmap.md) - [Persisted run/resume contract](2026-06-03-persisted-run-resume-contract.md) - [Workflow console, agent demo, and defense presentation](2026-07-01-workflow-console-agent-demo.md) - [Thesis system design](../../thesis/system-design-implementation.md) ## Purpose Make human-in-the-loop workflow pauses self-describing. A client inspecting an interrupted run should know: - what kind of interrupt occurred; - what payload was sent to the user; - which resume outcomes are valid; - what JSON shape a resume payload must have; - how to validate the response before sending `resume_run`. This is required for the Workflow Console and useful for CLI, JSON-RPC, MCP, and agent clients. It prevents every client from needing workflow-specific code just to render and answer an approval step. ## Implemented Contract The core interrupt model now exposes a complete public pause/resume contract. - `InterruptNode` stores `kind`, request bindings, resume bindings, and resume outcomes, plus `request_schema` and `resume_schema`. - Runtime builds an `InterruptRequest` with id, frame id, node id, kind, payload, route, resumability, outcomes, request schema, resume schema, and a `typed` marker. - `workflow.runs.inspect` serializes that persisted interrupt request. - Runtime validates request payloads before pausing and validates resume payloads against the persisted pause-time schema before mutating state. This is close to the LangGraph-style `interrupt(value)` and `Command(resume=...)` pattern: flexible and simple, but the response contract is mostly app convention. `wf` should keep the flexibility while making the contract explicit. ## Design Summary JSON Schema contracts are carried from interrupt nodes through persisted run inspection and resume validation. ```mermaid sequenceDiagram participant Runtime participant Store participant Client Runtime->>Runtime: Build request payload from interrupt.request bindings Runtime->>Runtime: Validate payload against request_schema Runtime->>Store: Persist interrupted run checkpoint Client->>Store: inspect_run(run_id) Store-->>Client: payload, outcomes, request_schema, resume_schema Client->>Client: Render form from resume_schema Client->>Store: resume_run(run_id, payload, outcome) Store->>Runtime: Validate resume payload against resume_schema Runtime->>Runtime: Apply interrupt.resume bindings ``` ## Core Model `InterruptNode` carries two schema fields: ```python class InterruptNode(BaseModel): id: str type: Literal["interrupt"] kind: str request: list[InputBinding] = Field(default_factory=list) resume: list[OutputBinding] = Field(default_factory=list) outcomes: list[str] = Field(default_factory=lambda: ["submitted"]) request_schema: JsonSchemaObject = Field(default_factory=_object_schema) resume_schema: JsonSchemaObject = Field(default_factory=_object_schema) ``` The default schema is a permissive object schema: ```json { "type": "object", "additionalProperties": true } ``` This preserves existing persisted workflow documents and current examples. New authoring helpers should emit explicit schemas whenever they can. ## Runtime Request Contract When an interrupt node executes: 1. Build the request payload using existing `request` bindings. 2. Validate the built payload against `request_schema`. 3. If validation fails, fail the run with a structured execution error. This is a workflow contract bug, not a human pause. 4. Persist the interrupted run with an `InterruptRequest` that includes: - interrupt id; - frame id; - node id; - kind; - payload; - resumable flag; - child interrupt route when present; - outcomes; - request schema; - resume schema; - `typed=true` when the schema was explicitly declared. The runtime should not infer request schemas from payload values. Inference would make the public contract depend on one execution instance rather than the workflow definition. ## Resume Contract Before mutating run state, `resume_run` validates: 1. the run is currently interrupted; 2. the selected resume outcome is declared by the interrupt node; 3. the resume payload satisfies the interrupt node's `resume_schema`. Only after those checks pass should the runtime apply `resume` bindings and advance the graph. Invalid resume payloads must not consume the interruption or write partial state changes. Resume validation should use the same JSON Schema validation helper used by workflow validation where possible. Do not hand-roll schema validation. ## Public Inspect Shape `workflow.runs.inspect` should expose the interrupt contract directly: ```json { "status": "interrupted", "interrupt": { "id": "interrupt:review", "frame_id": "frame_1", "node_id": "review", "kind": "issue_review", "payload": { "report_markdown": "# lda.chat Thesis And Project Readiness Report", "proposed_issues": [] }, "outcomes": ["submitted", "cancelled"], "request_schema": { "type": "object", "properties": { "report_markdown": {"type": "string"}, "proposed_issues": {"type": "array"} }, "required": ["report_markdown", "proposed_issues"] }, "resume_schema": { "type": "object", "properties": { "approved": {"type": "boolean"}, "selected_issue_ids": { "type": "array", "items": {"type": "string"} }, "comment": {"type": "string"} }, "required": ["approved", "selected_issue_ids"], "additionalProperties": false }, "typed": true, "resumable": true } } ``` The schema snapshot is part of the interrupted run's public contract. A client should not need to reload the mutable artifact or inspect Python source code to answer the interrupt. ## Authoring Contract Raw workflow plans may specify `request_schema` and `resume_schema` directly on interrupt nodes. Authoring helpers may provide convenience wrappers: - direct JSON Schema dictionaries; - Pydantic models converted through `model_json_schema()`; - a small built-in helper for common approval forms. The stored workflow should still contain ordinary JSON Schema dictionaries. Python type objects must not leak into persisted artifacts. The first planned demo uses: - `kind="issue_review"`; - a request schema containing rendered report markdown and proposed issues; - a resume schema containing approval, selected issue ids, and optional comment; - outcomes `submitted` and `cancelled`. ## Validation Contract Workflow validation should catch static interrupt contract errors: - `request_schema` and `resume_schema` must be valid JSON Schema documents; - schemas must describe JSON objects for V1; - request binding targets must be valid local payload paths; - resume binding sources must be valid local payload paths; - resume binding destinations must write to declared state fields; - every declared resume outcome that is routed must be known by the node. V1 does not need full static proof that every request binding output satisfies `request_schema`. Runtime validation still protects execution. Static validation can become stricter later if it remains useful and maintainable. ## CLI And RPC Behavior CLI and RPC should surface schema failures as ordinary structured product errors, not Python tracebacks. Recommended behavior: - `wf run inspect ` includes the interrupt schemas in JSON output. - Compact/text output summarizes `kind`, valid outcomes, and required resume fields. - `wf run resume --input ...` validates before mutation and reports missing/invalid resume fields. - `wf explain` gains cards for interrupt request-schema and resume-schema validation failures if new diagnostic codes are added. ## Compatibility Existing workflows without explicit schemas remain valid. - Missing `request_schema` or `resume_schema` is treated as a permissive object schema. - Public inspect can include `typed=false` for legacy interrupts. - New authoring helpers and examples should emit explicit schemas. - No migration is required for stored artifacts. Compatibility here is justified because raw workflow plans and saved artifacts are a documented external/persisted contract. ## Workflow Console Usage The Workflow Console uses this contract to render generic human approval forms: 1. inspect the interrupted run; 2. read `interrupt.kind` and `interrupt.resume_schema`; 3. choose a kind-specific renderer when available; 4. fall back to a generic JSON Schema form; 5. validate locally for user feedback; 6. send the resume payload through JSON-RPC; 7. display server-side validation errors if the schema check still fails. The console may provide a custom renderer for `issue_review`, but the custom renderer must still emit a payload accepted by `resume_schema`. ## Non-Goals - New interrupt execution semantics. - Multi-user approval workflow. - Authentication, authorization, or audit identity. - Scheduling or event-triggered resumes. - A full JSON Schema UI standard. - Semantic compatibility analysis between schema revisions. - LangGraph API compatibility. ## Implementation Slices 1. Extend core models and persistence-safe serialization. 2. Validate request and resume schemas using the existing schema validation library path. 3. Carry interrupt schemas and outcomes into `InterruptRequest` and `workflow.runs.inspect`. 4. Validate resume payloads before state mutation. 5. Add authoring/builder helpers and schema discovery output. 6. Update CLI docs, skills, and examples. 7. Add the `issue_review` interrupt to the prepared `lda.chat` report workflow. ## Test Plan - Core model accepts explicit schemas and defaults legacy nodes. - Invalid interrupt schemas are rejected by workflow validation. - Runtime validates a request payload before persisting interruption. - Resume rejects invalid payloads without mutating run state. - Resume rejects unknown outcomes before mutating run state. - `workflow.runs.inspect` includes schemas, outcomes, typed flag, and payload. - JSON-RPC and CLI resume return structured errors for invalid payloads. - Draft/raw-plan compilation preserves interrupt schemas. - Builder/Pydantic convenience emits plain JSON Schema in saved workflows. ## Success Criteria The contract is complete when an external client can inspect an interrupted run, render a correct form, submit a valid resume payload, and explain invalid responses without reading workflow source code or hard-coding that workflow's interrupt shape.