Files
lda-wf/docs/superpowers/specs/2026-07-01-self-describing-interrupt-contracts.md
T

303 lines
10 KiB
Markdown

# 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 <run_id>` includes the interrupt schemas in JSON output.
- Compact/text output summarizes `kind`, valid outcomes, and required resume
fields.
- `wf run resume <run_id> --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.