docs: design focused capability step updates

This commit is contained in:
lda
2026-07-26 22:18:01 +07:00 Verified
parent 30f353c10f
commit 05afc2e1d1
@@ -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.