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