draft redo
This commit is contained in:
@@ -0,0 +1,448 @@
|
||||
# Workflow Draft Surface Design
|
||||
|
||||
## Goal
|
||||
|
||||
Define the first real MCP-facing workflow draft surface.
|
||||
|
||||
The current draft prototype mostly relabels the raw graph model. It proved that
|
||||
draft validation, patching, and artifact creation are useful, but it is not yet a
|
||||
deep authoring interface. This design replaces that prototype before it becomes
|
||||
a compatibility burden.
|
||||
|
||||
The new draft surface should feel like `wf_authoring` over MCP:
|
||||
|
||||
- intent-level authoring
|
||||
- stable patch targets
|
||||
- explicit semantics
|
||||
- compilation into the existing core workflow graph
|
||||
|
||||
## Scope
|
||||
|
||||
This pass covers:
|
||||
|
||||
1. keyed steps
|
||||
2. compact outcome routes
|
||||
3. verb-keyed step shapes such as `use` and `route`
|
||||
4. saved capability/workflow references in `use`
|
||||
5. stable JSON Patch paths
|
||||
6. parity documentation against current `wf_authoring`
|
||||
|
||||
This pass does **not** cover:
|
||||
|
||||
- reverse-branch / shared outcome handlers
|
||||
- a new `wf_authoring` fluent API
|
||||
- true subgraph support
|
||||
- new core graph semantics
|
||||
- migration compatibility for the current draft prototype
|
||||
|
||||
The current prototype is not version `1`. It is disposable scaffolding.
|
||||
|
||||
## Design Principles
|
||||
|
||||
### Drafts Are Authoring Models
|
||||
|
||||
Drafts exist to make authoring easier. They are not another runtime model.
|
||||
|
||||
Compilation remains:
|
||||
|
||||
```text
|
||||
WorkflowDraft
|
||||
-> draft-to-authoring adapter
|
||||
-> WorkflowBuilder
|
||||
-> core Workflow graph
|
||||
-> WorkflowArtifact
|
||||
-> WorkflowDeployment
|
||||
```
|
||||
|
||||
The draft layer must not grow a second graph builder. Where semantics already
|
||||
exist in `wf_authoring`, the adapter should call `WorkflowBuilder` rather than
|
||||
reimplementing lowering directly against `wf_core`.
|
||||
|
||||
### JSON Should Optimize For LLM Edits
|
||||
|
||||
Array-index patch paths are weak:
|
||||
|
||||
```text
|
||||
/steps/3/out/foo
|
||||
```
|
||||
|
||||
Stable ids are better:
|
||||
|
||||
```text
|
||||
/steps/echo/out/foo
|
||||
```
|
||||
|
||||
The first real draft surface therefore uses keyed objects where identity matters.
|
||||
|
||||
### Reuse `wf_authoring` Semantics
|
||||
|
||||
The draft layer owns:
|
||||
|
||||
- JSON parsing
|
||||
- stable-id keyed presentation
|
||||
- patch-friendly document shape
|
||||
- translation from capability references into authoring inputs
|
||||
|
||||
It should not own:
|
||||
|
||||
- route expansion
|
||||
- graph construction rules
|
||||
- duplicate edge-building machinery
|
||||
- alternate workflow semantics
|
||||
|
||||
If the draft surface needs an authoring operation that `wf_authoring` does not
|
||||
yet expose, prefer adding the missing authoring primitive first. Temporary gaps
|
||||
must stay visibly thin and documented rather than becoming a parallel builder.
|
||||
|
||||
### Nice Syntax Must Still Be Explicit
|
||||
|
||||
Verb-keyed steps are allowed:
|
||||
|
||||
```json
|
||||
{
|
||||
"use": "demo.echo_tool"
|
||||
}
|
||||
```
|
||||
|
||||
but no step kind is inferred.
|
||||
|
||||
Exactly one step-kind key must be present. Allowed step-kind keys are:
|
||||
|
||||
- `use`
|
||||
- `route`
|
||||
- `foreach`
|
||||
- `interrupt`
|
||||
- `join`
|
||||
|
||||
Zero kind keys or multiple kind keys are validation errors.
|
||||
|
||||
## Draft Shape
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "echo_or_fail",
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"text": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["text"]
|
||||
},
|
||||
"state_schema": {
|
||||
"fields": {
|
||||
"echoed": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"output_schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"echoed": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"start": "has_text",
|
||||
"steps": {
|
||||
"has_text": {
|
||||
"route": [
|
||||
{
|
||||
"when": {
|
||||
"op": "exists",
|
||||
"path": "input.text"
|
||||
},
|
||||
"to": "echo"
|
||||
},
|
||||
{
|
||||
"otherwise": "missing_text"
|
||||
}
|
||||
]
|
||||
},
|
||||
"echo": {
|
||||
"use": "demo.personal.echo_tool",
|
||||
"in": {
|
||||
"input.text": "text"
|
||||
},
|
||||
"out": {
|
||||
"echoed": "state.echoed"
|
||||
}
|
||||
},
|
||||
"missing_text": {
|
||||
"use": "wf.std.runtime_error",
|
||||
"in": {
|
||||
"message": "message"
|
||||
}
|
||||
}
|
||||
},
|
||||
"routes": {
|
||||
"echo": {
|
||||
"ok": "__end__",
|
||||
"error": "missing_text"
|
||||
},
|
||||
"missing_text": {
|
||||
"error": "__end__"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Step Shapes
|
||||
|
||||
### `use`
|
||||
|
||||
Call one workflow capability.
|
||||
|
||||
```json
|
||||
{
|
||||
"use": "demo.echo_tool",
|
||||
"in": {
|
||||
"input.text": "text"
|
||||
},
|
||||
"out": {
|
||||
"echoed": "state.echoed"
|
||||
},
|
||||
"desc": "optional",
|
||||
"retry": 2,
|
||||
"timeout_seconds": 30
|
||||
}
|
||||
```
|
||||
|
||||
`use` accepts any workflow capability ref:
|
||||
|
||||
- generated wrapper around an upstream MCP tool
|
||||
- local source capability such as `wf.std.runtime_error`
|
||||
- saved wrapper capability
|
||||
- saved workflow capability once graph-as-node is available
|
||||
|
||||
The compiler lowers this to a core `NodeUse`.
|
||||
|
||||
### `route`
|
||||
|
||||
Route based on input/state conditions.
|
||||
|
||||
```json
|
||||
{
|
||||
"route": [
|
||||
{
|
||||
"when": {
|
||||
"op": "exists",
|
||||
"path": "state.hit"
|
||||
},
|
||||
"to": "found"
|
||||
},
|
||||
{
|
||||
"otherwise": "missing"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
The draft adapter asks `wf_authoring` to lower a route step using the same
|
||||
repeated condition-chain sugar that current Python authoring uses:
|
||||
|
||||
```text
|
||||
if cond_1 -> target_1 else
|
||||
if cond_2 -> target_2 else
|
||||
...
|
||||
otherwise -> fallback
|
||||
```
|
||||
|
||||
That means traces show the expanded condition steps. No new core runtime
|
||||
semantics are required.
|
||||
|
||||
Rules:
|
||||
|
||||
- clauses are ordered
|
||||
- zero or more `when` clauses may appear
|
||||
- at most one `otherwise` clause may appear
|
||||
- `otherwise` must be last
|
||||
- every clause must declare a target
|
||||
|
||||
### `foreach`
|
||||
|
||||
Keep the current concept, but key the step by id:
|
||||
|
||||
```json
|
||||
{
|
||||
"foreach": {
|
||||
"over": "state.items",
|
||||
"as": "item",
|
||||
"mode": "serial",
|
||||
"on_item_error": "fail"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This lowers to the current core `ForeachNode`.
|
||||
|
||||
### `interrupt`
|
||||
|
||||
```json
|
||||
{
|
||||
"interrupt": {
|
||||
"kind": "input",
|
||||
"request": {
|
||||
"state.question": "question"
|
||||
},
|
||||
"resume": {
|
||||
"answer": "state.answer"
|
||||
},
|
||||
"outcomes": ["submitted"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This lowers to the current core `InterruptNode`.
|
||||
|
||||
### `join`
|
||||
|
||||
```json
|
||||
{
|
||||
"join": {}
|
||||
}
|
||||
```
|
||||
|
||||
This lowers to the current core `JoinNode`.
|
||||
|
||||
`join` is not a reverse branch. It remains reserved for actual join/frame
|
||||
semantics.
|
||||
|
||||
## Routes
|
||||
|
||||
Most ordinary edges should be authored through `routes`:
|
||||
|
||||
```json
|
||||
"routes": {
|
||||
"echo": {
|
||||
"ok": "__end__",
|
||||
"error": "missing_text"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The adapter passes these through the same authoring route/connection path that
|
||||
produces normal core edges.
|
||||
|
||||
Outcome keys remain strings in JSON because they are wire values, but they
|
||||
should not be treated as arbitrary text. When the referenced capability is
|
||||
resolvable, draft validation should check route keys against that capability's
|
||||
declared outcomes.
|
||||
|
||||
`routes` is intended for:
|
||||
|
||||
- outcome routing from `use`
|
||||
- outgoing edges from lowered route/condition machinery
|
||||
- ordinary terminal edges
|
||||
|
||||
The raw edge list remains compiler output, not the normal authoring surface.
|
||||
|
||||
## Patching
|
||||
|
||||
Stable ids make targeted patches readable:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"op": "replace",
|
||||
"path": "/steps/echo/in/input.text",
|
||||
"value": "message"
|
||||
},
|
||||
{
|
||||
"op": "replace",
|
||||
"path": "/routes/echo/error",
|
||||
"value": "fallback"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
This is a core reason for keyed `steps`.
|
||||
|
||||
## Relationship To Current `wf_authoring`
|
||||
|
||||
| Python `wf_authoring` | MCP Draft Surface | Notes |
|
||||
| --- | --- | --- |
|
||||
| `g.use(spec, ...)` | `steps[id].use` | direct conceptual match |
|
||||
| `g.connect(step, outcome, target)` | `routes[id][outcome] = target` | same graph meaning, better JSON |
|
||||
| `g.branch(...)` | `routes[...]` | outcome routing is already compact in JSON |
|
||||
| `route(...)` node | `steps[id].route` | state/input routing |
|
||||
| explicit `start(...)` | `start` | direct match |
|
||||
| `END` | `"__end__"` | keep wire token explicit |
|
||||
| `NodeSpec` object | capability ref string | MCP cannot carry Python callable identity |
|
||||
| builder object refs | stable string ids | JSON needs durable names |
|
||||
| reducers | state schema reducer refs | same domain concept |
|
||||
| Python callable nodes | not representable inline | must already be capabilities |
|
||||
| fluent/cursor ergonomics | intentionally absent | good in Python, poor in patchable JSON |
|
||||
|
||||
## Later `wf_authoring` / Core Work
|
||||
|
||||
These are real ideas, but intentionally outside this pass.
|
||||
|
||||
### Shared Outcome Handlers
|
||||
|
||||
Useful reverse-branch sugar:
|
||||
|
||||
```text
|
||||
node_a.error
|
||||
node_b.error
|
||||
node_c.unreachable
|
||||
-> runtime_error
|
||||
```
|
||||
|
||||
This is not `join`. It is compressed declaration of several ordinary edges.
|
||||
|
||||
Possible later surfaces:
|
||||
|
||||
- Python: `g.on(...).to(...)`
|
||||
- JSON: a dedicated `handlers` / `on` section
|
||||
|
||||
### `wf_authoring` v2
|
||||
|
||||
The MCP draft surface exposes friction in current Python authoring too:
|
||||
|
||||
- no fluent shared-handler helper
|
||||
- no full parity with route sugar
|
||||
- likely room for better grouped declarations
|
||||
- outcome names still travel as bare strings even though `NodeSpec` already
|
||||
declares them
|
||||
|
||||
Python authoring should eventually expose outcome helpers derived from the
|
||||
`NodeSpec`, for example `spec.outcomes.ok` or equivalent. Those helpers should
|
||||
come from the declared contract rather than duplicated `ClassVar` constants that
|
||||
can drift from it.
|
||||
|
||||
That should be handled as its own pass, not smuggled into this MCP draft change.
|
||||
|
||||
### Core Follow-Ups
|
||||
|
||||
Potential later core work:
|
||||
|
||||
- true graph-as-node / subgraph support
|
||||
- meaningful join semantics
|
||||
- future START-edge support if `Workflow.start` changes
|
||||
|
||||
## Prototype Replacement
|
||||
|
||||
The current draft prototype should be deleted or replaced directly.
|
||||
|
||||
There is no compatibility guarantee because:
|
||||
|
||||
- it is not yet a released/versioned user contract
|
||||
- it mostly relabels raw graph fields
|
||||
- preserving it would make the first real authoring surface carry avoidable
|
||||
complexity from day one
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
Tests should cover:
|
||||
|
||||
1. verb-key validation: exactly one step-kind key
|
||||
2. keyed-step compilation into core nodes
|
||||
3. `routes` compilation into core edges
|
||||
4. `route` lowering into current condition nodes/edges
|
||||
5. saved capability refs passing through `use`
|
||||
6. patching by stable ids
|
||||
7. artifact creation from the new draft surface
|
||||
8. diagnostics with stable draft paths
|
||||
Reference in New Issue
Block a user