docs: design focused capability step updates
This commit is contained in:
@@ -0,0 +1,425 @@
|
|||||||
|
# Capability Step Update Design
|
||||||
|
|
||||||
|
**Status:** Approved design
|
||||||
|
**Date:** 2026-07-26
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Add one revision-checked operation for updating an existing capability-backed
|
||||||
|
draft step without removing and recreating it or writing raw JSON Patch. The
|
||||||
|
operation updates step metadata and, when requested, replaces the step's
|
||||||
|
complete canonical input-binding list in the same atomic revision.
|
||||||
|
|
||||||
|
The same slice extends capability-step creation so callers can set metadata and
|
||||||
|
canonical literal inputs when the step is first added.
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
`DraftUseStep` already supports:
|
||||||
|
|
||||||
|
- `desc`;
|
||||||
|
- `retry`;
|
||||||
|
- `timeout_seconds`;
|
||||||
|
- ordered canonical path and literal input bindings.
|
||||||
|
|
||||||
|
The focused authoring surfaces do not expose those capabilities coherently.
|
||||||
|
`wf draft add capability` accepts path inputs through a compatibility map but
|
||||||
|
cannot set literals or execution metadata. After creation, changing metadata
|
||||||
|
requires raw JSON Patch or removing and recreating the step. Removing and
|
||||||
|
recreating is especially poor for agents because it also risks disturbing
|
||||||
|
routes and output bindings that are unrelated to the intended edit.
|
||||||
|
|
||||||
|
The existing `set_step_input_bindings` operation solves canonical input
|
||||||
|
replacement, but it cannot combine that replacement with metadata changes in
|
||||||
|
one revision. A focused capability-step update should provide that atomic edit
|
||||||
|
without becoming a generic step-replacement mechanism.
|
||||||
|
|
||||||
|
## Goals
|
||||||
|
|
||||||
|
- Update `desc`, `retry`, and `timeout_seconds` on an existing
|
||||||
|
`DraftUseStep`.
|
||||||
|
- Distinguish omitted metadata from explicit clearing.
|
||||||
|
- Optionally replace the complete ordered canonical input-binding list in the
|
||||||
|
same revision.
|
||||||
|
- Reuse existing capability-aware input validation and schema projection.
|
||||||
|
- Preserve `use`, output bindings, routes, and all unrelated draft content.
|
||||||
|
- Expose the operation through Python, JSON-RPC, MCP, and local/remote CLI.
|
||||||
|
- Let `wf draft add capability` set the same metadata and canonical input
|
||||||
|
shapes at creation.
|
||||||
|
- Preserve stale-revision precedence, exact no-op behavior, and atomic failure.
|
||||||
|
|
||||||
|
## Non-Goals
|
||||||
|
|
||||||
|
- Changing the step's `use` capability.
|
||||||
|
- Updating routes or step output bindings.
|
||||||
|
- Updating non-capability step kinds.
|
||||||
|
- Replacing the complete `DraftUseStep` document.
|
||||||
|
- Removing existing compatibility map operations.
|
||||||
|
- Adding TypeScript RPC coverage in this slice.
|
||||||
|
|
||||||
|
Changing `use` is deliberately structural. A different capability can change
|
||||||
|
input/output schemas and declared outcomes, so callers must remove and add the
|
||||||
|
step explicitly rather than hiding that migration inside metadata repair.
|
||||||
|
|
||||||
|
## Considered Interfaces
|
||||||
|
|
||||||
|
### Typed Patch Model
|
||||||
|
|
||||||
|
Use one presence-aware `CapabilityStepUpdate` model and pass it through every
|
||||||
|
transport.
|
||||||
|
|
||||||
|
Advantages:
|
||||||
|
|
||||||
|
- omission and explicit `null` remain distinct;
|
||||||
|
- one interface carries the same semantics across Python, RPC, MCP, and CLI;
|
||||||
|
- validation and mutation remain concentrated in the authoring module;
|
||||||
|
- future metadata fields can be added without another operation.
|
||||||
|
|
||||||
|
This is the selected approach.
|
||||||
|
|
||||||
|
### Loose Optional Parameters
|
||||||
|
|
||||||
|
Pass one optional parameter per field plus internal sentinel values.
|
||||||
|
|
||||||
|
This makes the Python call superficially smaller, but every transport must
|
||||||
|
reconstruct omission-versus-null semantics. It spreads one concept across
|
||||||
|
several adapters and is rejected.
|
||||||
|
|
||||||
|
### Complete Step Replacement
|
||||||
|
|
||||||
|
Require the caller to provide a complete `DraftUseStep`.
|
||||||
|
|
||||||
|
This simplifies mutation code but forces callers to preserve `use`, outputs,
|
||||||
|
and unrelated metadata. It creates avoidable lost-update risk and is rejected.
|
||||||
|
|
||||||
|
## Domain Model
|
||||||
|
|
||||||
|
The transport-safe update model is:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class CapabilityStepUpdate(BaseModel):
|
||||||
|
desc: str | None = Field(default=None, min_length=1)
|
||||||
|
retry: int | None = Field(default=None, ge=0)
|
||||||
|
timeout_seconds: int | None = Field(default=None, gt=0)
|
||||||
|
input: list[InputBinding] | None = None
|
||||||
|
```
|
||||||
|
|
||||||
|
The model's interface includes Pydantic field presence:
|
||||||
|
|
||||||
|
| Field state | Meaning |
|
||||||
|
| --- | --- |
|
||||||
|
| Field omitted | Preserve the stored value |
|
||||||
|
| `desc: null` | Remove the description |
|
||||||
|
| `retry: null` | Remove the retry override |
|
||||||
|
| `timeout_seconds: null` | Remove the timeout override |
|
||||||
|
| `input` omitted | Preserve current canonical input bindings |
|
||||||
|
| `input: []` | Clear canonical input bindings |
|
||||||
|
| `input: null` | Reject as ambiguous |
|
||||||
|
|
||||||
|
An update with no fields set is invalid. The model or operation must reject it
|
||||||
|
before capability metadata is loaded.
|
||||||
|
|
||||||
|
`model_fields_set` is semantic input, not an implementation detail to discard
|
||||||
|
at a transport seam. RPC and MCP request models therefore carry the update as
|
||||||
|
a nested object rather than flattening nullable fields into the request
|
||||||
|
envelope.
|
||||||
|
|
||||||
|
## Canonical Authoring Operation
|
||||||
|
|
||||||
|
Add:
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def update_capability_step(
|
||||||
|
*,
|
||||||
|
workspace_id: str,
|
||||||
|
revision: int,
|
||||||
|
step_id: str,
|
||||||
|
update: CapabilityStepUpdate,
|
||||||
|
) -> dict[str, Any]
|
||||||
|
```
|
||||||
|
|
||||||
|
to `WorkflowDraftAuthoringApi`, the public workflow surface, and `WorkflowApi`.
|
||||||
|
|
||||||
|
### Validation And Mutation Order
|
||||||
|
|
||||||
|
The operation must:
|
||||||
|
|
||||||
|
1. Validate the request envelope, including the non-empty update rule.
|
||||||
|
2. Load the workspace and check `revision`.
|
||||||
|
3. Require `steps` to be an object.
|
||||||
|
4. Require `step_id` to exist.
|
||||||
|
5. Parse the selected step and require `DraftUseStep`.
|
||||||
|
6. If `input` is omitted, skip capability resolution entirely.
|
||||||
|
7. If `input` is present, resolve the unchanged `use` capability and reuse the
|
||||||
|
same canonical input preflight used by `set_step_input_bindings`.
|
||||||
|
8. Build the complete changed step and any projected input/state schemas in
|
||||||
|
memory.
|
||||||
|
9. Return the current workspace summary without a revision increment when the
|
||||||
|
resulting step and schemas are exactly unchanged.
|
||||||
|
10. Apply one JSON Patch containing all changed schema and step fields.
|
||||||
|
|
||||||
|
Stale revision must win over missing-step, wrong-step-kind, unavailable
|
||||||
|
capability, path, overlap, literal, and schema errors. The final
|
||||||
|
`patch_draft_workspace` call remains the mutation-time race guard.
|
||||||
|
|
||||||
|
### Input Replacement
|
||||||
|
|
||||||
|
When `input` is present, it replaces the complete ordered input-binding list.
|
||||||
|
The operation must reuse or extract the semantic implementation behind
|
||||||
|
`set_step_input_bindings`; it must not maintain a second version of:
|
||||||
|
|
||||||
|
- graph source validation;
|
||||||
|
- local-target overlap detection;
|
||||||
|
- input/state schema projection;
|
||||||
|
- literal validation;
|
||||||
|
- root binding behavior;
|
||||||
|
- context target handling;
|
||||||
|
- exact ordering and fan-out preservation.
|
||||||
|
|
||||||
|
Clearing input bindings does not delete workflow input/state schema fields
|
||||||
|
projected by earlier edits. This matches existing canonical replacement
|
||||||
|
semantics and avoids destructive schema inference.
|
||||||
|
|
||||||
|
### Metadata Mutation
|
||||||
|
|
||||||
|
Only fields present in `update.model_fields_set` are changed. Explicit `null`
|
||||||
|
removes the corresponding optional field from the canonical step, which is
|
||||||
|
dumped with optional `None` fields excluded. Omitted fields retain their parsed
|
||||||
|
values.
|
||||||
|
|
||||||
|
`retry=0` is valid. `retry<0`, `timeout_seconds<=0`, and an explicitly supplied
|
||||||
|
empty description fail request-model validation.
|
||||||
|
|
||||||
|
## Capability-Step Creation Parity
|
||||||
|
|
||||||
|
Extend `add_step_from_capability` with:
|
||||||
|
|
||||||
|
- `desc`;
|
||||||
|
- `retry`;
|
||||||
|
- `timeout_seconds`;
|
||||||
|
- canonical `input_bindings`.
|
||||||
|
|
||||||
|
The existing `input_map` remains a compatibility adapter for real callers.
|
||||||
|
Supplying both `input_map` and `input_bindings` is invalid. Canonical callers
|
||||||
|
and new transports should use `input_bindings`.
|
||||||
|
|
||||||
|
Creation uses the same binding preflight and schema projection as update and
|
||||||
|
`set_step_input_bindings`. It persists the metadata directly on the new
|
||||||
|
`DraftUseStep`, so a caller that already knows these fields does not need a
|
||||||
|
second revision.
|
||||||
|
|
||||||
|
The operation's route and output-binding behavior remains unchanged.
|
||||||
|
|
||||||
|
## JSON-RPC
|
||||||
|
|
||||||
|
Add:
|
||||||
|
|
||||||
|
```text
|
||||||
|
workflow.draft_workspaces.update_capability_step
|
||||||
|
```
|
||||||
|
|
||||||
|
with parameters:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"workspace_id": "report",
|
||||||
|
"revision": 4,
|
||||||
|
"step_id": "publish",
|
||||||
|
"update": {
|
||||||
|
"desc": "Publish the report",
|
||||||
|
"retry": 2,
|
||||||
|
"timeout_seconds": 30,
|
||||||
|
"input": [
|
||||||
|
{
|
||||||
|
"path": "state.report.title",
|
||||||
|
"target": "request.title"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "markdown",
|
||||||
|
"target": "request.format"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The client must preserve omitted fields and exact input-binding order. It
|
||||||
|
serializes the nested update with `exclude_unset=True`; otherwise default
|
||||||
|
`None` values would become unintended clear operations. The existing
|
||||||
|
`add_step_from_capability` request gains optional metadata and canonical
|
||||||
|
`input_bindings`; its compatibility `input_map` remains accepted but cannot be
|
||||||
|
combined with the canonical field.
|
||||||
|
|
||||||
|
## MCP
|
||||||
|
|
||||||
|
Add:
|
||||||
|
|
||||||
|
```text
|
||||||
|
wf.workflow.update_capability_step
|
||||||
|
```
|
||||||
|
|
||||||
|
with a typed request carrying `workspace_id`, `revision`, `step_id`, and nested
|
||||||
|
`update`.
|
||||||
|
|
||||||
|
The tool delegates once to `WorkflowApi.update_capability_step`. Its
|
||||||
|
description must state that:
|
||||||
|
|
||||||
|
- `use`, routes, and outputs are preserved;
|
||||||
|
- omitted fields are preserved;
|
||||||
|
- explicit metadata `null` clears;
|
||||||
|
- supplied `input` replaces the complete canonical list.
|
||||||
|
|
||||||
|
The tool belongs in the stable workflow search allow-list. Extend the existing
|
||||||
|
capability-add request with the creation-parity fields.
|
||||||
|
|
||||||
|
## CLI
|
||||||
|
|
||||||
|
Add a dedicated update group in a focused module:
|
||||||
|
|
||||||
|
```text
|
||||||
|
wf draft update capability
|
||||||
|
```
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wf draft update capability report \
|
||||||
|
--revision 4 \
|
||||||
|
--step publish \
|
||||||
|
--description "Publish the report" \
|
||||||
|
--retry 2 \
|
||||||
|
--timeout-seconds 30 \
|
||||||
|
--input state.report.title=request.title \
|
||||||
|
--value request.format='"markdown"'
|
||||||
|
```
|
||||||
|
|
||||||
|
Clearing:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wf draft update capability report \
|
||||||
|
--revision 5 \
|
||||||
|
--step publish \
|
||||||
|
--clear-description \
|
||||||
|
--clear-retry \
|
||||||
|
--clear-timeout \
|
||||||
|
--clear-input
|
||||||
|
```
|
||||||
|
|
||||||
|
### CLI Rules
|
||||||
|
|
||||||
|
- `--description` and `--clear-description` are mutually exclusive.
|
||||||
|
- `--retry` and `--clear-retry` are mutually exclusive.
|
||||||
|
- `--timeout-seconds` and `--clear-timeout` are mutually exclusive.
|
||||||
|
- `--bindings-file` is mutually exclusive with `--input`, `--value`, and
|
||||||
|
`--clear-input`.
|
||||||
|
- `--clear-input` is mutually exclusive with `--input` and `--value`.
|
||||||
|
- At least one update field or input mode is required.
|
||||||
|
- All mode errors occur before CLI context loading.
|
||||||
|
- `--input` means `GRAPH_SOURCE=LOCAL_TARGET` and is repeatable.
|
||||||
|
- `--value` means `LOCAL_TARGET=JSON` and is repeatable.
|
||||||
|
- Convenience flags serialize path bindings first and literal bindings second,
|
||||||
|
matching `set-input`.
|
||||||
|
- `--bindings-file` is the lossless path/value interleaving form.
|
||||||
|
|
||||||
|
Extend `wf draft add capability` with:
|
||||||
|
|
||||||
|
- `--description`;
|
||||||
|
- `--retry`;
|
||||||
|
- `--timeout-seconds`;
|
||||||
|
- `--value`;
|
||||||
|
- `--bindings-file`.
|
||||||
|
|
||||||
|
Its existing `--input` flag remains the path-binding convenience form.
|
||||||
|
`--bindings-file` is mutually exclusive with `--input` and `--value`.
|
||||||
|
|
||||||
|
The update and add commands must reuse the existing canonical input flag/file
|
||||||
|
parsers. They must not add a third parser for the same binding union.
|
||||||
|
|
||||||
|
## Error Contract
|
||||||
|
|
||||||
|
After request-envelope validation, focused errors include:
|
||||||
|
|
||||||
|
- workspace or step not found;
|
||||||
|
- stale revision conflict;
|
||||||
|
- selected step is not capability-backed;
|
||||||
|
- empty update;
|
||||||
|
- explicit `input: null`;
|
||||||
|
- capability unavailable when input replacement needs its schema;
|
||||||
|
- undeclared graph source;
|
||||||
|
- overlapping local targets;
|
||||||
|
- literal target absent from the capability input schema;
|
||||||
|
- incompatible source/target schema;
|
||||||
|
- invalid metadata constraints.
|
||||||
|
|
||||||
|
All semantic errors leave the complete workspace unchanged.
|
||||||
|
|
||||||
|
Metadata-only updates succeed even when the capability source is currently
|
||||||
|
unavailable because they do not require capability schema inspection.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
### Authoring
|
||||||
|
|
||||||
|
- Preserve omitted fields.
|
||||||
|
- Set and explicitly clear each metadata field.
|
||||||
|
- Accept `retry=0`.
|
||||||
|
- Reject invalid metadata constraints.
|
||||||
|
- Replace, clear, and exactly no-op canonical input.
|
||||||
|
- Apply metadata and input replacement in one revision.
|
||||||
|
- Preserve `use`, output bindings, and routes.
|
||||||
|
- Skip capability resolution for metadata-only changes.
|
||||||
|
- Validate capability-aware paths, literals, roots, overlaps, and schemas by
|
||||||
|
the shared input-binding implementation.
|
||||||
|
- Prove stale revision wins over every semantic error.
|
||||||
|
- Prove all failures leave the workspace unchanged.
|
||||||
|
- Compile and run a draft after a combined path/literal update.
|
||||||
|
|
||||||
|
### Creation
|
||||||
|
|
||||||
|
- Persist metadata at creation.
|
||||||
|
- Preserve ordered path/literal bindings.
|
||||||
|
- Accept a lossless bindings file through CLI.
|
||||||
|
- Reject simultaneous compatibility and canonical input forms.
|
||||||
|
- Preserve existing route and output behavior.
|
||||||
|
|
||||||
|
### Transports
|
||||||
|
|
||||||
|
- JSON-RPC model preserves omitted fields versus explicit `null`.
|
||||||
|
- JSON-RPC client preserves exact canonical input order.
|
||||||
|
- MCP request validation rejects malformed updates.
|
||||||
|
- Real MCP tool invocation delegates typed update data once.
|
||||||
|
- Local and remote CLI produce equivalent operation payloads.
|
||||||
|
|
||||||
|
### CLI
|
||||||
|
|
||||||
|
- Pin every set/clear exclusivity rule.
|
||||||
|
- Pin bindings-file exclusivity.
|
||||||
|
- Reject empty update before loading context.
|
||||||
|
- Verify add and update help text.
|
||||||
|
- Verify explicit metadata clears serialize as present `null`.
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
Update:
|
||||||
|
|
||||||
|
- `ISSUES.md`;
|
||||||
|
- `docs/wf_cli.md`;
|
||||||
|
- `docs/workflow_drafts.md`;
|
||||||
|
- `docs/workflow_capabilities.md`;
|
||||||
|
- `docs/wf_mcp_operator_manual.md`;
|
||||||
|
- `skills/wf-cli/SKILL.md`;
|
||||||
|
- `skills/wf-workflow` draft/lifecycle references;
|
||||||
|
- `docs/current_roadmap.md`.
|
||||||
|
|
||||||
|
Document that capability update is a focused patch, not capability replacement,
|
||||||
|
and that output bindings/routes remain separate operations.
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
|
||||||
|
- One atomic operation updates capability-step metadata and optional canonical
|
||||||
|
inputs without changing `use`, outputs, or routes.
|
||||||
|
- Omission, explicit clearing, replacement, and no-op semantics are verified.
|
||||||
|
- Creation can set the same metadata and canonical literal inputs directly.
|
||||||
|
- Python, JSON-RPC, MCP, and local/remote CLI expose the same behavior.
|
||||||
|
- Existing compatibility callers remain functional.
|
||||||
|
- Focused tests, Ruff, formatting, and basedpyright pass.
|
||||||
Reference in New Issue
Block a user