274 lines
10 KiB
Markdown
274 lines
10 KiB
Markdown
# Atomic Step Output Bindings Design
|
|
|
|
## Status
|
|
|
|
Approved for implementation planning on 2026-07-23.
|
|
|
|
## Problem
|
|
|
|
Canonical step outputs are ordered `OutputBinding` records that map one
|
|
node-local output path into one workflow state path. The focused authoring
|
|
surface currently reduces those records to a `dict[str, str]`. That map cannot
|
|
represent one local source feeding several state targets, so reading and later
|
|
merging a valid canonical list can silently discard fan-out.
|
|
|
|
The existing focused operation also patches only the binding list. Callers must
|
|
separately declare matching state schema paths even when the capability output
|
|
schema already describes the source values. This makes a common authoring edit
|
|
multi-step and exposes an avoidable intermediate invalid revision.
|
|
|
|
## Goals
|
|
|
|
- Replace one capability step's complete ordered output-binding list in one
|
|
revision-checked operation.
|
|
- Preserve source fan-out and nested local/state paths without lowering through
|
|
a dictionary.
|
|
- Project missing state schema paths from the capability output schema in the
|
|
same atomic mutation.
|
|
- Expose the canonical operation through Python, JSON-RPC, MCP, and CLI.
|
|
- Keep the existing map operation for real compatibility callers while making
|
|
its lossy behavior explicit.
|
|
- Reuse the existing schema path and projection helpers instead of adding a
|
|
second JSON Schema implementation.
|
|
|
|
## Non-Goals
|
|
|
|
- Changing the persisted `OutputBinding` model or runtime output application.
|
|
- Supporting literal step outputs. Step outputs read values produced by a node;
|
|
literal bindings belong to workflow output projection, not this operation.
|
|
- Solving top-level workflow output literals or nested workflow output schema
|
|
projection.
|
|
- Replacing every legacy output-map reader in this slice.
|
|
- Adding a generic input/output binding replacement framework.
|
|
- Generating TypeScript RPC contracts.
|
|
|
|
## Canonical Operation
|
|
|
|
Add this operation to the public workflow API:
|
|
|
|
```python
|
|
async def set_step_output_bindings(
|
|
*,
|
|
workspace_id: str,
|
|
revision: int,
|
|
step_id: str,
|
|
bindings: Sequence[OutputBinding],
|
|
) -> dict[str, Any]:
|
|
"""Replace one capability step's canonical output bindings atomically."""
|
|
```
|
|
|
|
The operation performs these steps in order:
|
|
|
|
1. Validate the request envelope before entering semantic authoring logic.
|
|
2. Check the expected draft revision before reading the step or capability.
|
|
3. Require the selected draft step to declare a capability through `use`.
|
|
4. Resolve that capability's output schema contract.
|
|
5. Validate every local `source` against the capability output schema.
|
|
6. Reject duplicate or ancestor/descendant state `target` paths.
|
|
7. Project each missing state target from its selected capability output schema
|
|
fragment, preserving local `$defs` and `definitions` references.
|
|
8. Accept an existing state target only when its schema is exactly equal to the
|
|
selected capability output fragment.
|
|
9. Replace the step output list and any changed `state_schema` in one patch.
|
|
|
|
An identical binding list with an identical projected state schema is an exact
|
|
no-op: return the current workspace summary without incrementing the revision.
|
|
|
|
## Binding Semantics
|
|
|
|
The operation accepts the existing core model unchanged:
|
|
|
|
```json
|
|
[
|
|
{"source": "report.title", "target": "state.report.title"},
|
|
{"source": "report.title", "target": "state.audit.title"},
|
|
{"source": ".", "target": "state.raw_result"}
|
|
]
|
|
```
|
|
|
|
- `source` is a rootless `LocalPath`. `.` selects the complete capability
|
|
output payload.
|
|
- `target` is a non-root `StatePath`, serialized with the `state.` prefix.
|
|
- Binding order is preserved exactly.
|
|
- Repeating a source is valid and represents fan-out.
|
|
- Repeating a target is invalid.
|
|
- Ancestor/descendant targets such as `state.report` and
|
|
`state.report.title` are invalid because applying both is order-dependent.
|
|
- Empty bindings are valid and clear the step output list; they do not remove
|
|
previously declared state schema fields.
|
|
|
|
## Schema Projection
|
|
|
|
`wf_api.schema_projection` remains the only implementation of JSON Schema path
|
|
selection and projection. Output authoring uses
|
|
`schema_fragment_at_path(...)`, `schema_path_exists(...)`, and
|
|
`project_schema_path_to_schema_path(...)` rather than introducing output-only
|
|
schema traversal.
|
|
|
|
For each binding:
|
|
|
|
- select `source.parts` from the capability output schema;
|
|
- if `target.parts` is absent from the draft state schema, copy the selected
|
|
fragment into that target path;
|
|
- if the target exists with the exact same schema, leave it unchanged;
|
|
- if the target exists with a different schema, reject the operation rather
|
|
than guessing JSON Schema compatibility;
|
|
- reject unresolved, remote, cyclic, or conflicting schema references through
|
|
the existing projection errors.
|
|
|
|
Projection is monotonic. Replacing or clearing bindings never deletes state
|
|
schema declarations because other steps or reducers may still depend on them.
|
|
|
|
## Compatibility Boundary
|
|
|
|
Keep `set_step_output_map(...)` and
|
|
`workflow.draft_workspaces.set_step_output_map` unchanged for existing callers.
|
|
Its `merge=True` mode remains a compatibility adapter and remains inherently
|
|
lossy when canonical fan-out already exists.
|
|
|
|
New code and documentation prefer canonical replacement. The issue describing
|
|
lossy compatibility maps remains open until all map readers and writers are
|
|
removed or made incapable of rewriting canonical fan-out.
|
|
|
|
## Transport Design
|
|
|
|
Add these surfaces:
|
|
|
|
- Python API: `set_step_output_bindings`;
|
|
- JSON-RPC: `workflow.draft_workspaces.set_step_output_bindings`;
|
|
- remote RPC client method: `set_step_output_bindings`;
|
|
- MCP tool: `wf.workflow.set_step_output_bindings`.
|
|
|
|
RPC and MCP request models use `list[OutputBinding]` directly. They must preserve
|
|
the canonical list order and must not translate through `DraftPathMap`.
|
|
Malformed objects fail Pydantic/request-envelope validation before semantic
|
|
authoring logic runs.
|
|
|
|
## CLI Design
|
|
|
|
Keep the existing command name:
|
|
|
|
```text
|
|
wf draft set-output WORKSPACE --revision N --step STEP ...
|
|
```
|
|
|
|
Preferred canonical forms are:
|
|
|
|
```bash
|
|
wf draft set-output WS \
|
|
--revision 4 \
|
|
--step analyze \
|
|
--map report.title=state.report.title \
|
|
--map report.title=state.audit.title
|
|
```
|
|
|
|
and:
|
|
|
|
```bash
|
|
wf draft set-output WS \
|
|
--revision 4 \
|
|
--step analyze \
|
|
--bindings-file output-bindings.json
|
|
```
|
|
|
|
CLI behavior:
|
|
|
|
- repeatable `--map LOCAL_SOURCE=STATE_TARGET` creates canonical bindings in
|
|
flag order;
|
|
- `--bindings-file` accepts the exact canonical JSON array and preserves order;
|
|
- `--clear` explicitly replaces the list with `[]`;
|
|
- no bindings and no `--clear` is an input error;
|
|
- `--bindings-file` and `--clear` are mutually exclusive with `--map`;
|
|
- canonical replacement does not support `--merge`;
|
|
- `--merge --map ...` delegates to the legacy map operation and is documented
|
|
as compatibility-only and potentially lossy.
|
|
|
|
Current bindings remain exportable without another endpoint:
|
|
|
|
```bash
|
|
wf draft inspect WS --include-draft |
|
|
jq '.draft.steps.analyze.output' > output-bindings.json
|
|
```
|
|
|
|
## Error Contract
|
|
|
|
Semantic failures should identify the binding index and canonical path where
|
|
possible. Representative errors are:
|
|
|
|
- `bindings[1].source 'missing' is not declared by capability 'report.analyze'`;
|
|
- `bindings[0].target 'state.report' overlaps bindings[1].target 'state.report.title'`;
|
|
- `bindings[0].target 'state.report.title' already has an incompatible schema`;
|
|
- `bindings[2].source '.' cannot be projected to 'state.raw_result': ...`.
|
|
|
|
After request-envelope validation, stale revision wins over missing steps,
|
|
unknown capabilities, schema lookup, overlap, and projection errors. Every
|
|
failure leaves the draft and revision unchanged.
|
|
|
|
## Testing
|
|
|
|
### Python Authoring
|
|
|
|
Add focused tests for:
|
|
|
|
- ordered replacement with nested local and state paths;
|
|
- one local source fanning out to several state targets;
|
|
- whole-payload `.` projection;
|
|
- missing nested state schema projection;
|
|
- exact-equivalent existing target schemas;
|
|
- incompatible existing target schemas;
|
|
- missing local sources;
|
|
- duplicate and ancestor/descendant state targets;
|
|
- stable binding order in the stored draft;
|
|
- exact no-op behavior;
|
|
- explicit clearing without state schema deletion;
|
|
- stale revision precedence and no mutation for every failure class;
|
|
- compatibility of the existing map-only merge operation.
|
|
|
|
Compile and execute one representative draft to prove runtime output fan-out
|
|
writes the expected values to both state paths.
|
|
|
|
### Transport And CLI
|
|
|
|
Add tests proving:
|
|
|
|
- JSON-RPC, remote client, and MCP preserve canonical output-binding order and
|
|
duplicate sources;
|
|
- malformed binding objects fail request validation;
|
|
- CLI repeated `--map` flags preserve order and fan-out;
|
|
- `--bindings-file` round-trips an exported inspected list;
|
|
- `--clear` is explicit and an empty accidental invocation fails;
|
|
- canonical replacement rejects `--merge` combinations;
|
|
- compatibility `--merge --map` remains operational;
|
|
- local and remote CLI targets use the same canonical operation.
|
|
|
|
## Documentation And Issue State
|
|
|
|
After verification:
|
|
|
|
- keep the combined input/output map-loss issue open, but update it to state
|
|
that both canonical replacements preserve fan-out while map compatibility
|
|
operations remain lossy;
|
|
- leave workflow-output literals, nested workflow-output projection, focused
|
|
step updates, and TypeScript parity open;
|
|
- update CLI help, user docs, and agent skills with canonical replacement,
|
|
fan-out, clear, compatibility, and export examples;
|
|
- add a completed roadmap entry linking to the archived implementation plan;
|
|
- archive the implementation plan under
|
|
`docs/historical/superpowers/plans/`.
|
|
|
|
## Success Criteria
|
|
|
|
- One focused operation replaces a step's complete canonical output-binding
|
|
list in one revision.
|
|
- Nested paths, source fan-out, and whole-payload output use the existing core
|
|
binding model unchanged.
|
|
- Every local source is validated against the capability output schema before
|
|
mutation.
|
|
- Missing state target schemas are projected atomically with the binding list.
|
|
- Existing map-only callers retain their current behavior through a clearly
|
|
documented compatibility adapter.
|
|
- No new persisted binding type, template language, or JSON Schema traversal is
|
|
introduced.
|
|
- Focused API, runtime, RPC, MCP, CLI, Ruff, formatting, and basedpyright checks
|
|
pass.
|