docs: plan workflow console and interrupt contracts

This commit is contained in:
lda
2026-07-01 05:47:27 +07:00 Verified
parent a1007c49a9
commit cdffb1d620
7 changed files with 2043 additions and 32 deletions
@@ -0,0 +1,308 @@
# Self-Describing Interrupt Contracts
Date: 2026-07-01
Status: Approved direction. Needs an executable implementation plan before code
changes.
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.
## Current Gap
The current core interrupt model has useful mechanics but not a complete public
contract.
- `InterruptNode` stores `kind`, request bindings, resume bindings, and resume
outcomes.
- Runtime builds an `InterruptRequest` with id, frame id, node id, kind,
payload, route, and resumability.
- `workflow.runs.inspect` serializes the current runtime interrupt request.
- Resume validates the selected outcome against `InterruptNode.outcomes` and
applies resume bindings.
What is missing is a machine-readable schema for the request payload and resume
payload. A client can see data, but it cannot know whether the response should
be `{ "approved": true }`, `{ "selected_issue_ids": [...] }`, or something else
without reading workflow code or challenge-specific docs.
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
Add JSON Schema contracts to interrupt nodes and carry them 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
Extend `InterruptNode` with two optional 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.
@@ -0,0 +1,428 @@
# Workflow Console, Agent Demo, And Defense Presentation
Date: 2026-07-01
Status: Approved direction. Implementation is split into prerequisite slices.
Related:
- [Current roadmap](../../current_roadmap.md)
- [Workflow API architecture](../../wf_api_architecture.md)
- [Persisted run/resume contract](2026-06-03-persisted-run-resume-contract.md)
- [Self-describing interrupt contracts](2026-07-01-self-describing-interrupt-contracts.md)
- [Thesis system design](../../thesis/system-design-implementation.md)
## Purpose
Build one local-first web application that serves three related needs:
1. a reusable Workflow Console for inspecting a running `wf` JSON-RPC server;
2. a reliable defense demonstration of the complete workflow lifecycle;
3. a compact thesis presentation with live-demo and recorded-replay routes.
The application also demonstrates where an agent belongs in the product. A
constrained agent translates a natural-language request into typed parameters
for a prepared recipe. The workflow substrate remains responsible for drafts,
artifacts, deployments, runs, interrupts, traces, validation, and source
bindings.
The demo agent is an integration client, not new evidence for general agent
planning ability. The audited challenge campaign remains the evidence for how
generic external agents interact with the public product surface.
## Product Boundary
Create a top-level `web/` package rather than putting product code under
`docs/presentation/`.
```text
web/
package.json
astro.config.mjs
src/
pages/
connect.astro
console/
demo/
replay/
presentation/
appendix/
components/
agent/
lifecycle/
rpc/
trace/
workflow-graph/
presentation/
```
The first release is a local-development Workflow Console. It is not a
production admin panel and does not claim authentication, authorization,
multi-user administration, or safe access to arbitrary remote servers.
## System Architecture
```mermaid
flowchart TB
Browser[Astro and React UI]
Routes[Astro server routes]
Agent[DemoAgent service]
Recipe[Prepared ReportRecipe]
Job[LifecycleJob service]
Rpc[WorkflowRpc service]
Server[wf JSON-RPC server]
Events[Job event stream]
Browser --> Routes
Routes --> Agent
Routes --> Job
Agent --> Recipe
Recipe --> Job
Job --> Rpc
Rpc --> Server
Job --> Events
Events --> Browser
```
All workflow operations use the existing public JSON-RPC API. No Python-side
demo endpoint, direct store mutation, or privileged workflow path is added.
The web app should not shell out to `wf` for normal operation. The CLI is an
operator frontend: it resolves config, parses files and flags, formats output,
and sometimes aggregates several calls for human convenience. The console needs
the lower-level JSON-RPC request/response stream so it can show exact protocol
evidence, avoid shell quoting and encoding failures, and keep raw calls aligned
with interpreted UI state.
## TypeScript And Effect Boundary
Use Effect for server-side orchestration and protocol handling:
- `Schema` decodes JSON-RPC envelopes and selected result projections;
- services and `Layer`s provide connection configuration, RPC, recipes, jobs,
replay storage, and the model gateway;
- tagged errors distinguish connection, protocol, decoding, workflow,
timeout, and demo-state failures;
- `SubscriptionRef` or `Stream` publishes lifecycle events;
- scoped fibers run and cancel autoplay;
- timeouts and retries apply only where semantically safe.
Do not spread Effect through every React component. React components consume
ordinary view models and event streams. Astro route handlers are the
`Effect.runPromise` boundary.
Mutation calls such as artifact creation, deployment saving, run start, and
resume are never retried blindly. Health and idempotent read calls may use a
small bounded retry policy.
## Connection Model
The connection page accepts a JSON-RPC URL such as
`http://127.0.0.1:8765/rpc`.
1. The Astro server validates the URL.
2. The first slice accepts only loopback hosts.
3. `workflow.health` verifies the endpoint.
4. The connection is retained for the browser session.
5. Astro proxies JSON-RPC calls server-side to avoid browser CORS coupling.
6. Every call records its raw request, raw response, interpreted result,
duration, and equivalent CLI command.
Allowing arbitrary remote targets would turn the proxy into an SSRF surface and
requires a separate security design.
## JSON-RPC Method Registry
Represent mapped operations declaratively. Each entry contains:
- JSON-RPC method name;
- Effect schema for parameters and selected result fields;
- human explanation;
- equivalent CLI formatter;
- interpretation function;
- mutation/idempotency classification.
The initial read surface maps:
- `workflow.health`;
- source list, inspect, and diagnose;
- capability list and inspect;
- draft-workspace list, get, validate, and compile;
- artifact list and inspect;
- deployment list, inspect, and validate;
- run list, inspect, and trace.
The prepared demo additionally maps:
- draft-workspace create from capability and patch;
- draft validation and compilation;
- artifact creation from a workspace;
- deployment save and validation;
- run start, inspect, trace, and resume.
Mapping a method does not grant special authority. The Workflow Console and the
prepared demo are ordinary users of the same registry.
## Workflow Console
The first console is read/inspect-focused. It provides:
- source and capability inventory;
- draft workspace list, graph, revision, and diagnostics;
- artifact versions and required source bindings;
- deployment binding and readiness state;
- run status, output, interrupt, and bounded trace;
- raw JSON-RPC request/response drawers.
Generic visual workflow editing is not part of the first initiative. The
prepared demo performs controlled mutations through the same public RPC
registry. A future editor can reuse the graph and inspector components.
## Visual Interaction Model
The UI uses focus modes instead of showing every panel simultaneously:
- **Lifecycle:** Draft, Artifact, Deployment, Run, and Trace records.
- **Graph:** interactive workflow graph with semantic zoom.
- **Execution:** active nodes and trace progression over the same graph.
- **Output:** rendered readiness report and created issues.
- **Raw:** collapsible JSON-RPC request and response drawer.
Use an interactive graph component, expected to be `@xyflow/react`, inside an
Astro React island. Static presentation diagrams remain Mermaid.
A small graph embedded in a lifecycle card can expand into the primary canvas.
Selecting a node opens a side drawer with capability, source, bindings,
outcomes, and trace data. Selecting a lifecycle record zooms back out.
## Lifecycle Job And Autoplay
The agent calls one macro tool. The macro creates a lifecycle job and returns a
job id. The TypeScript job then executes public JSON-RPC operations.
```mermaid
sequenceDiagram
actor User
participant Agent
participant Macro as create_workspace_report
participant Job as LifecycleJob
participant RPC as Workflow JSON-RPC
participant UI
User->>Agent: Request readiness report
Agent->>Macro: Typed recipe parameters
Macro->>Job: Create operation queue
Macro-->>Agent: job id accepted
Job-->>UI: Switch to lifecycle view
loop Autoplay operations
Job->>RPC: Execute next public operation
RPC-->>Job: Raw result
Job-->>UI: Event, reason, raw data, interpreted data
end
Job-->>UI: Pause for typed approval
User->>Job: Approve selected issues
Job->>RPC: Resume run
RPC-->>Job: Final report and trace
Job-->>Agent: Structured completion result
Agent-->>User: Summarize result
```
Autoplay:
- advances one operation at a time;
- allows Pause and Next;
- pauses automatically on errors, interrupts, and completion;
- never approves a human interrupt automatically;
- supports no backward step in live mode;
- supports previous, next, and timeline scrubbing in replay mode.
The chat collapses while the lifecycle and graph views carry the operation. It
returns after completion, when the agent summarizes the structured result.
## Attribution And Honesty
The UI distinguishes agent activity from orchestration activity.
- Agent card: `create_workspace_report({...})`.
- Orchestrator card: concrete JSON-RPC method, reason, equivalent CLI, raw
response, and interpreted result.
Do not present orchestrated sub-operations as independent agent tool calls. Do
not present recorded text as a live model response. Replay mode is visibly
labeled.
## Demo Agent
The agent is intentionally constrained:
- one prepared report recipe;
- typed recipe parameters;
- one macro tool;
- no shell, repository reads, code search, or subagents;
- bounded output and timeout;
- replaceable model gateway;
- recorded replay fallback.
The first model gateway may use the OpenCode API directly, but the application
depends only on a `DemoAgent` service contract. Exact provider selection belongs
to the agent-integration slice.
The demo agent is not compared with the benchmark agents. Benchmark agents test
public-surface discovery and free-form operation; the demo agent illustrates a
product integration built on that surface.
## Prepared `lda.chat` Report Workflow
Create a separate example rather than expanding the existing minimal report
case study:
```text
examples/lda_report_workflow/
documents/
project-brief.md
architecture-notes.md
evaluation-findings.md
risk-register.md
roadmap.md
document_source.py
report_source.py
issue_board_source.py
workflow.plan.json
run-input.json
wf.config.json
```
Python sources:
- `local.lda_docs`: list and read deterministic project documents;
- `local.lda_report`: analyse documents, combine findings, classify risks,
render, and finalise the report;
- `local.issue_board`: create, list, and reset local demo issues.
The issue board is JSON-backed with atomic writes. It is a deterministic local
demo source, not a production tracker.
Workflow shape:
```mermaid
flowchart TB
List[List lda.chat documents]
Each{For each document}
Analyse[Analyse document]
Combine[Combine findings]
Risks{Material risks found?}
RiskSection[Build risk section]
NoRisks[Record no material risks]
Render[Render readiness report]
Review[Typed issue-review interrupt]
Selected{For each selected issue}
Create[Create issue]
Finalise[Finalise report]
Revision[Record revision request]
List --> Each
Each -->|loop| Analyse
Analyse --> Each
Each -->|done| Combine
Combine --> Risks
Risks -->|yes| RiskSection
Risks -->|no| NoRisks
RiskSection --> Render
NoRisks --> Render
Render --> Review
Review -->|submitted| Selected
Review -->|cancelled| Revision
Selected -->|loop| Create
Create --> Selected
Selected -->|done| Finalise
```
The report is titled **lda.chat Thesis And Project Readiness Report** and covers
achievements, architecture status, evaluation evidence, material risks, and
next actions.
The interrupt request includes the rendered report and proposed issues. The
resume payload contains approval, selected issue ids, and an optional comment.
The UI selects all issues by default and allows individual deselection.
## Self-Describing Interrupt Dependency
The current interrupt payload exposes `kind` and data but not a complete
machine-readable response contract. The Workflow Console requires explicit
request and resume schemas so it can render and validate arbitrary interrupts
without reading workflow code.
The prerequisite contract is defined in
[Self-describing interrupt contracts](2026-07-01-self-describing-interrupt-contracts.md).
## Replay And Failure Handling
Every lifecycle event envelope records:
- operation id and stage;
- JSON-RPC method and parameters;
- raw response or error;
- interpreted result;
- equivalent CLI;
- reason and duration;
- resulting lifecycle ids.
Recorded mode replays these same envelopes. Live failure offers a visible switch
to the matching recording. The presentation does not restart in a different UI
or hide the failure.
## Defense Presentation
The defense has 15 minutes of presentation and 15 minutes of questions. Target
9-10 slides and no more than three minutes of live demo.
Routes:
- `/presentation`: primary slides;
- `/demo`: live agent and lifecycle presenter;
- `/replay`: recorded fallback;
- `/appendix`: backup architecture, evaluation, and implementation slides.
The presentation transitions directly into the demo and back. The exact slide
library is selected in the presentation slice; the route and shared component
boundaries are fixed by this design.
## Implementation Order
1. Self-describing interrupt request/resume contracts.
2. Deterministic `lda.chat` report workflow and Python sources.
3. `web/` Astro and Effect foundation, connection flow, and RPC registry.
4. Workflow Console read/inspect views, graph, trace, and raw drawers.
5. Lifecycle job, autoplay, typed approval, issue board, and replay.
6. Constrained demo agent and replaceable model gateway.
7. Defense presentation and appendix routes.
Each slice gets its own executable implementation plan. Do not combine the
Python contract change, web foundation, agent integration, and presentation
into one implementation pass.
## Non-Goals
- Production authentication or authorization.
- Arbitrary remote RPC proxying.
- A general visual workflow editor.
- Scheduling or event triggers.
- Google Drive, email, or other external service dependencies.
- Free-form autonomous workflow synthesis in the live defense demo.
- Treating the demo agent as benchmark evidence.
## Success Criteria
The initiative is complete when:
1. a local user can connect the console to a loopback workflow RPC server;
2. lifecycle records and traces are readable without scrolling raw JSON;
3. raw request/response evidence remains available beside interpretation;
4. the prepared recipe runs through draft, artifact, deployment, run,
interrupt, resume, issue creation, output, and trace;
5. interrupt forms are generated from public contracts;
6. live and replay modes render the same event model;
7. the constrained agent invokes one macro tool and receives the final result;
8. the three-minute demo has a tested replay fallback;
9. the presentation explains the agent/substrate boundary without overstating
autonomous planning.