docs: complete generic draft step authoring

This commit is contained in:
lda
2026-07-21 08:26:44 +07:00 Verified
parent f5bac90818
commit bc4c4e0257
21 changed files with 288 additions and 155 deletions
+3 -6
View File
@@ -2,9 +2,6 @@
## Draft authoring parity
- [ ] No dedicated draft CLI subcommands for adding non-capability step kinds.
- [ ] `DraftInterruptPayload` cannot preserve `request_schema` or
`resume_schema`, so typed interrupt contracts cannot be authored through the
draft model.
- [ ] `DraftStep` and its adapter have no subgraph representation even though
`SubgraphNode` is a canonical core step.
- [x] Dedicated draft CLI subcommands cover every draft step kind.
- [x] Draft interrupts preserve request and resume schemas.
- [x] Draft subgraphs preserve workflow references and boundary contracts.
+7 -7
View File
@@ -550,12 +550,12 @@ clear operator feedback before adding more architecture.
output-to-state helper and reducing manual draft patch repairs in agent
challenge runs.
- Completed: draft CLI vocabulary now uses `wf draft create --capability` and
`wf draft add-step --capability`, replacing the longer
`*-from-capability` commands that agents repeatedly guessed around.
- Completed: `wf draft add-step` inserts one explicit
capability-backed step with route, input, and output-to-state schema/binding
wiring in a single revision, reducing brittle JSON Patch authoring for
multi-step workflows. Accepts `--route OUTCOME=TARGET` for multi-outcome steps.
the typed `wf draft add <kind>` command tree. All nine draft step kinds have
dedicated commands; capability insertion retains schema projection, while
interrupt and subgraph commands preserve their explicit boundary contracts.
Generic insertion is exposed through Python and JSON-RPC with atomic incoming
and outgoing route wiring. Implementation:
[`generic draft step authoring`](historical/superpowers/plans/2026-07-20-generic-draft-add-step.md).
- Completed: `wf draft branch` and `wf draft handle` provide atomic route
editing for existing draft steps without rewriting the full routes object.
- Completed: `wf draft compile` returns the compiled raw plan plus required
@@ -786,7 +786,7 @@ stable.
[`required-only wrapper inputs`](historical/superpowers/plans/2026-06-29-required-only-wrapper-inputs.md).
- Completed: `wf draft set-input` rejects `local.x` targets before RPC and
shows the equivalent bare-target mapping.
- Completed: `wf draft add-step --route` errors include declared outcomes and
- Completed: `wf draft add capability --route` errors include declared outcomes and
direct add/remove repair guidance.
- Completed: repeated idempotent `wf draft bind input/state -> local` behavior
is covered by regression tests.
@@ -54,7 +54,7 @@
- Produces: `DraftSubgraphPayload`, `DraftSubgraphStep`, expanded `DraftInterruptPayload`, and updated `DraftStep`.
- Produces: adapter lowering to `InterruptNode` and `SubgraphNode` with contracts intact.
- [ ] **Step 1: Write failing model tests for typed interrupt contracts**
- [x] **Step 1: Write failing model tests for typed interrupt contracts**
Add a draft containing:
@@ -79,7 +79,7 @@ Add a draft containing:
Assert the parsed fields and `model_dump(mode="json", by_alias=True)` preserve both schemas.
- [ ] **Step 2: Write failing model tests for subgraph boundaries**
- [x] **Step 2: Write failing model tests for subgraph boundaries**
Cover both workflow reference forms:
@@ -97,13 +97,13 @@ Cover both workflow reference forms:
Assert `DraftSubgraphStep` is selected and aliases round-trip.
- [ ] **Step 3: Run model tests and confirm red**
- [x] **Step 3: Run model tests and confirm red**
Run: `uv run pytest tests/artifacts/test_draft_models.py -q`
Expected: failures for forbidden interrupt schema fields and unknown `subgraph` kind.
- [ ] **Step 4: Implement the draft model fields and union member**
- [x] **Step 4: Implement the draft model fields and union member**
Import `SchemaRef` and `WorkflowRef`, add `subgraph` to `STEP_KIND_KEYS`, add the payload/step classes from the approved design, and append `DraftSubgraphStep` to `DraftStep`. Add to `DraftInterruptPayload`:
@@ -116,7 +116,7 @@ Add a field validator that accepts `None` and rejects a supplied schema unless
`schema.type == "object"`. This keeps untyped interrupts untyped while
validating explicit contracts at draft parse time.
- [ ] **Step 5: Write failing adapter tests**
- [x] **Step 5: Write failing adapter tests**
Assert `build_workflow_from_draft` produces:
@@ -130,7 +130,7 @@ assert child.output_schema == output_schema
assert child.outcomes == ["ok", "error"]
```
- [ ] **Step 6: Implement adapter lowering**
- [x] **Step 6: Implement adapter lowering**
Build interrupt keyword arguments so `request_schema`/`resume_schema` are
omitted when `None`; passing object defaults would incorrectly set
@@ -149,7 +149,7 @@ return node
Use an explicit `isinstance(step, DraftSubgraphStep)` branch before the final `TypeError`.
- [ ] **Step 7: Verify and commit**
- [x] **Step 7: Verify and commit**
Run:
@@ -182,7 +182,7 @@ git commit -m "feat: complete draft step model parity"
- Consumes: `DraftStep` including `DraftSubgraphStep` from Task 1.
- Produces: `RouteSource` and `WorkflowApiSurface.add_step(*, workspace_id, revision, step_id, step, incoming, routes)`.
- [ ] **Step 1: Rename the internal route value object**
- [x] **Step 1: Rename the internal route value object**
Replace `DraftOutcomeRef` with:
@@ -197,7 +197,7 @@ class RouteSource:
Update `handle_draft`, `WorkflowApi.handle_draft`, imports, and existing tests. Do not retain an alias because all callers are repository-owned.
- [ ] **Step 2: Write failing parameterized insertion tests**
- [x] **Step 2: Write failing parameterized insertion tests**
Parameterize the nine payloads (`use`, `foreach`, `interrupt`, `join`, `end`, `when`, `choose`, `match`, `subgraph`). For each, call:
@@ -213,7 +213,7 @@ result = await api.add_step(
Use a Pydantic `TypeAdapter(DraftStep)` in the test and assert revision `2` plus the canonical dumped payload under `draft.steps.new_step`.
- [ ] **Step 3: Write failing atomic routing/error tests**
- [x] **Step 3: Write failing atomic routing/error tests**
Cover:
@@ -225,7 +225,7 @@ Cover:
- incomplete but valid route subsets accepted;
- each failure leaves revision and draft bytes unchanged.
- [ ] **Step 4: Implement declared-outcome validation**
- [x] **Step 4: Implement declared-outcome validation**
Add a private helper with exhaustive `isinstance` branches:
@@ -251,7 +251,7 @@ def _draft_step_route_outcomes(self, step: DraftStep) -> set[str] | None:
`None` means top-level routes are forbidden, not unknown.
- [ ] **Step 5: Implement `WorkflowDraftAuthoringApi.add_step`**
- [x] **Step 5: Implement `WorkflowDraftAuthoringApi.add_step`**
Build a patch only after all checks pass:
@@ -294,11 +294,11 @@ return await self.drafts.patch_draft_workspace(
Check `steps`, `routes`, duplicate id, incoming source existence, forbidden routes, and unknown route keys before this call.
- [ ] **Step 6: Expose the method through service and surface**
- [x] **Step 6: Expose the method through service and surface**
Use the exact signature from the design in both `WorkflowApi` and `WorkflowApiSurface`. The service method delegates to `self.draft_authoring.add_step` without converting the typed step back to a raw dict.
- [ ] **Step 7: Verify and commit**
- [x] **Step 7: Verify and commit**
Run:
@@ -329,7 +329,7 @@ git commit -m "feat: add atomic generic draft step insertion"
- Consumes: `WorkflowApiSurface.add_step`, `DraftStep`, and `RouteSource` from Task 2.
- Produces: method `workflow.draft_workspaces.add_step` and remote client parity.
- [ ] **Step 1: Write failing RPC parameter tests**
- [x] **Step 1: Write failing RPC parameter tests**
Add:
@@ -350,7 +350,7 @@ class AddDraftStepParams(RpcParamsModel):
Before implementation, tests should attempt to import the models and validate a foreach alias (`as`), a when alias (`if`), typed interrupt schemas, and a subgraph artifact reference. Add malformed tests for unknown/multiple kind keys and blank route-source fields.
- [ ] **Step 2: Implement parameter models and canonical serialization tests**
- [x] **Step 2: Implement parameter models and canonical serialization tests**
Import `DraftStep` from `wf_artifacts.drafts`. Assert:
@@ -360,11 +360,11 @@ assert dumped["step"]["foreach"]["as"] == "item"
assert "as_" not in dumped["step"]["foreach"]
```
- [ ] **Step 3: Write a failing server round-trip test**
- [x] **Step 3: Write a failing server round-trip test**
Call `workflow.draft_workspaces.add_step` against a temporary store with a typed interrupt step, incoming source, and routes. Assert one revision increment and preserved request/resume schemas. Add a malformed RPC request and assert the workspace is unchanged.
- [ ] **Step 4: Register the method**
- [x] **Step 4: Register the method**
Add to `methods/drafts.py`:
@@ -397,7 +397,7 @@ async def workflow_draft_workspaces_add_step(
raise_workflow_rpc_error(exc)
```
- [ ] **Step 5: Write failing client request-shape tests**
- [x] **Step 5: Write failing client request-shape tests**
Use the existing recording transport fixture. Assert exact method name and payload:
@@ -409,7 +409,7 @@ assert request["params"]["incoming"] == {"step_id": "lookup", "outcome": "ok"}
Parameterize all nine variants so alias/schema/reference fields cannot be dropped.
- [ ] **Step 6: Implement the client method**
- [x] **Step 6: Implement the client method**
The client accepts typed values and dumps aliases explicitly:
@@ -441,7 +441,7 @@ async def add_step(
)
```
- [ ] **Step 7: Verify and commit**
- [x] **Step 7: Verify and commit**
Run:
@@ -472,7 +472,7 @@ git commit -m "feat: expose generic draft steps over rpc"
- Consumes: `WorkflowApiSurface.add_step` and existing `add_step_from_capability`.
- Produces: `draft_add.app` registered as `wf draft add` and migrated `capability` command.
- [ ] **Step 1: Write failing command-tree tests**
- [x] **Step 1: Write failing command-tree tests**
Assert:
@@ -486,7 +486,7 @@ removed = runner.invoke(app, ["draft", "add-step", "--help"])
assert removed.exit_code != 0
```
- [ ] **Step 2: Extract only shared parser helpers**
- [x] **Step 2: Extract only shared parser helpers**
Move `_parse_assignment_flags`, `_parse_map_flags`, `_parse_output_map_flags`, `_parse_step_input_map_flags`, and `_parse_route_flags` from `drafts.py` into `draft_options.py`. Add:
@@ -511,7 +511,7 @@ def route_source(from_step: str | None, from_outcome: str | None) -> RouteSource
Keep imports updated so existing draft commands retain identical parsing.
- [ ] **Step 3: Create and register the subgroup**
- [x] **Step 3: Create and register the subgroup**
In `draft_add.py`:
@@ -525,7 +525,7 @@ app = typer.Typer(
In `drafts.py`, import `draft_add` and register `app.add_typer(draft_add.app, name="add")` after constructing the draft app.
- [ ] **Step 4: Move the capability command without changing behavior**
- [x] **Step 4: Move the capability command without changing behavior**
Register the existing body as `@app.command("capability")`. Keep all current
capability options and call `context.handlers.add_step_from_capability` with
@@ -534,7 +534,7 @@ fields, parsed routes, input map, and output bindings exactly as the removed
command does. Its docstring must state that it also projects schemas/bindings
and recommend `wf draft validate`.
- [ ] **Step 5: Verify local and remote capability behavior**
- [x] **Step 5: Verify local and remote capability behavior**
Update old CLI tests from:
@@ -550,7 +550,7 @@ wf draft add capability WORKSPACE --revision REVISION --step STEP --capability Q
Keep assertions on request payload, projected schemas, route errors, and revision unchanged. Add a remote test proving it still calls `workflow.draft_workspaces.add_step_from_capability`, not generic insertion.
- [ ] **Step 6: Verify and commit**
- [x] **Step 6: Verify and commit**
Run:
@@ -579,7 +579,7 @@ git commit -m "feat: group draft add commands"
- Consumes: generic `add_step`, parsing helpers, and concrete draft models.
- Produces: four type-specific commands with local/remote parity.
- [ ] **Step 1: Add a private command dispatcher and failing delegation tests**
- [x] **Step 1: Add a private command dispatcher and failing delegation tests**
Use one helper so every command has identical transport behavior:
@@ -611,7 +611,7 @@ def _submit_step(
Tests must invoke both local fake handlers and `--url` RPC targets and assert the concrete model received by `add_step`.
- [ ] **Step 2: Implement `interrupt` with schema and binding validation**
- [x] **Step 2: Implement `interrupt` with schema and binding validation**
Construct:
@@ -640,7 +640,7 @@ DraftInterruptStep(interrupt=DraftInterruptPayload(
Use the repository's existing binding payload/model helpers rather than duplicating path conversion. Tests cover two outcomes, both schemas, aliases, duplicate flags, malformed files, and no API call after parse failure.
- [ ] **Step 3: Implement `foreach` and validate policy relationships**
- [x] **Step 3: Implement `foreach` and validate policy relationships**
Construct `DraftForeachPayload` from `--over`, `--as`, `--mode`, and:
@@ -658,11 +658,11 @@ concurrent = (
Reject concurrent limits in serial mode with `typer.BadParameter`; rely on Pydantic to require `collect_to` for collect behavior. Route tests cover `loop`, `done`, and `completed_with_errors`.
- [ ] **Step 4: Implement `join` and `end`**
- [x] **Step 4: Implement `join` and `end`**
`join` constructs `DraftJoinStep(join={})` and accepts routes. `end` constructs `DraftEndStep(end=DraftEndPayload(outcome=outcome))`, exposes no `--route`, and passes `routes=None`.
- [ ] **Step 5: Pin per-command help and error surfaces**
- [x] **Step 5: Pin per-command help and error surfaces**
For each command assert `--help` lists its own fields and does not list unrelated fields. Specifically:
@@ -671,7 +671,7 @@ For each command assert `--help` lists its own fields and does not list unrelate
- join has only common routing flags;
- end has `--outcome` but no `--route`.
- [ ] **Step 6: Verify and commit**
- [x] **Step 6: Verify and commit**
Run:
@@ -701,7 +701,7 @@ git commit -m "feat: add draft control step commands"
- Consumes: `_submit_step`, JSON-file parsing, and draft models from prior tasks.
- Produces: `when`, `choose`, `match`, and `subgraph` commands.
- [ ] **Step 1: Write failing `when` tests and implement the command**
- [x] **Step 1: Write failing `when` tests and implement the command**
Given `condition.json`:
@@ -721,7 +721,7 @@ DraftWhenStep(when=DraftWhenPayload(
The command must not expose `--route` because targets are embedded.
- [ ] **Step 2: Write failing `choose` tests and implement the command**
- [x] **Step 2: Write failing `choose` tests and implement the command**
`--clauses-file` contains a JSON array. Validate with
`TypeAdapter(list[DraftChooseClause]).validate_python(value)`, then construct
@@ -729,7 +729,7 @@ The command must not expose `--route` because targets are embedded.
Tests cover ordered clauses, canonical `if` alias output, an empty array, a
non-array document, and no generic routes.
- [ ] **Step 3: Write failing `match` tests and implement the command**
- [x] **Step 3: Write failing `match` tests and implement the command**
`--cases-file` contains a JSON array. Validate with
`TypeAdapter(list[DraftMatchCase])`, then construct:
@@ -744,7 +744,7 @@ DraftMatchStep(match=DraftMatchPayload(
Tests preserve scalar `equals` values (`str`, `int`, `bool`, `None`) and ordered targets.
- [ ] **Step 4: Write failing subgraph reference tests**
- [x] **Step 4: Write failing subgraph reference tests**
Cover:
@@ -756,7 +756,7 @@ Cover:
All invalid combinations must fail before `add_step` is called.
- [ ] **Step 5: Implement subgraph construction**
- [x] **Step 5: Implement subgraph construction**
Build the reference explicitly:
@@ -777,7 +777,7 @@ else:
Then construct `DraftSubgraphPayload` with optional schema files, canonical input/output bindings, outcomes defaulting to `['ok']`, and description. Pass repeatable routes through `_submit_step`.
- [ ] **Step 6: Verify all nine commands and remote parity**
- [x] **Step 6: Verify all nine commands and remote parity**
Add a parameterized remote test that invokes every generic command and asserts method `workflow.draft_workspaces.add_step`, canonical step payload aliases, incoming route source, and routes. Keep capability in a separate assertion because it intentionally calls the composed method.
@@ -789,7 +789,7 @@ uv run ruff check src/wf_cli/commands tests/wf_cli
uv run basedpyright src/wf_cli/commands tests/wf_cli --level error
```
- [ ] **Step 7: Commit**
- [x] **Step 7: Commit**
```bash
git add src/wf_cli/commands/draft_add.py tests/wf_cli
@@ -814,7 +814,7 @@ git commit -m "feat: add draft decision and subgraph commands"
- Consumes: all implemented commands and method names.
- Produces: accurate live docs and a clean, archived implementation record.
- [ ] **Step 1: Search live references before editing**
- [x] **Step 1: Search live references before editing**
Run:
@@ -825,7 +825,7 @@ rg -n -F 'add_step_from_capability' docs skills --glob '!docs/historical/**'
Classify each reference: migrate command examples; retain API references when they describe the composed capability helper; do not rewrite thesis/history solely for naming.
- [ ] **Step 2: Update user-facing CLI and skill documentation**
- [x] **Step 2: Update user-facing CLI and skill documentation**
Document the command tree and at least these complete examples:
@@ -846,11 +846,11 @@ wf draft add when report_ws --revision 3 --step decide \
Explain that `when`/`choose`/`match` embed targets and do not accept `--route`, while invalid intermediate drafts remain saveable and should be checked with `wf draft validate`.
- [ ] **Step 3: Update API architecture and roadmap**
- [x] **Step 3: Update API architecture and roadmap**
Document `workflow.draft_workspaces.add_step`, the `DraftStep` boundary, separate map-key `step_id`, atomic route wiring, and the continued role of `add_step_from_capability`. Mark the roadmap slice complete only after verification.
- [ ] **Step 4: Resolve tracked issues honestly**
- [x] **Step 4: Resolve tracked issues honestly**
Change the three items in `ISSUES.md` to checked entries only if tests prove:
@@ -862,7 +862,7 @@ Change the three items in `ISSUES.md` to checked entries only if tests prove:
Add any newly discovered out-of-scope defects as unchecked, reproducible statements.
- [ ] **Step 5: Run focused regression suites**
- [x] **Step 5: Run focused regression suites**
```bash
uv run pytest tests/artifacts/test_draft_models.py tests/artifacts/test_draft_adapter.py tests/wf_api/test_drafts_service.py tests/wf_transport_rpc_http/test_app.py tests/wf_transport_rpc_http/test_client.py tests/wf_cli/test_app.py tests/wf_cli/test_remote_target.py -q
@@ -870,7 +870,7 @@ uv run pytest tests/artifacts/test_draft_models.py tests/artifacts/test_draft_ad
Expected: all pass.
- [ ] **Step 6: Run the repository quality gate**
- [x] **Step 6: Run the repository quality gate**
```bash
uv run ruff check
@@ -881,11 +881,11 @@ git diff --check
If formatting fails, run `uv run ruff format`, inspect the diff, and rerun all four checks. Do not claim the full `uv run pytest -q` suite unless it is actually run; the focused matrix above is the required test gate for this slice.
- [ ] **Step 7: Review and archive**
- [x] **Step 7: Review and archive**
Run the `requesting-code-review` skill against the design/spec and this plan. Fix Critical/Important findings, rerun affected checks, tick completed plan checkboxes, then move the plan to the matching historical path and update live links.
- [ ] **Step 8: Commit documentation and issue closure**
- [x] **Step 8: Commit documentation and issue closure**
```bash
git add docs skills ISSUES.md
@@ -149,7 +149,7 @@ authoring intent.
### Add Step From Capability
`add-step` atomically adds:
`add capability` atomically adds:
- one capability-backed `use` step;
- explicit input bindings;
@@ -160,7 +160,7 @@ authoring intent.
The outgoing CLI option is repeatable:
```powershell
wf draft add-step WORKSPACE `
wf draft add capability WORKSPACE `
--revision 4 `
--step second_echo `
--capability everything.default.echo `
@@ -11,7 +11,7 @@ helpers behave too much like final validation gates. In challenge runs, agents
tried:
```powershell
wf draft add-step browser_click `
wf draft add capability browser_click `
--revision 1 `
--step wait `
--capability local.browser_click.wait_for_click `
@@ -66,7 +66,7 @@ Expected behavior:
## Acceptance Criteria
- A focused test proves `wf draft add-step --route ok=collect` persists an
- A focused test proves `wf draft add capability --route ok=collect` persists an
invalid workspace when `collect` does not exist yet.
- A follow-up edit that adds `collect` can make the same workspace valid.
- `wf draft save` and `wf draft compile` continue to reject the invalid
+11 -2
View File
@@ -169,8 +169,17 @@ frontends. Internally, they should prefer typed models from `wf_core`,
`WorkflowDraftApi` owns draft workspace lifecycle, validation, compilation,
JSON Patch application, and focused low-level map edits. `WorkflowDraftAuthoringApi`
is the semantic authoring layer above it: capability-aware bootstrap, bind,
add-step, branch, handle, and remove helpers lower intent into ordinary draft
workspace patches while preserving revision checks.
typed step insertion, branch, handle, and remove helpers lower intent into
ordinary draft workspace patches while preserving revision checks.
`workflow.draft_workspaces.add_step` accepts a discriminated `DraftStep` value,
while `step_id` remains the separate map key chosen by the caller. The operation
atomically inserts that step plus an optional incoming `RouteSource` and any
top-level outcome routes, so a failed structural check does not partially wire
the graph. Decision targets for `when`, `choose`, and `match` remain embedded in
their typed payloads. The composed `add_step_from_capability` operation remains
separate because it also resolves capability metadata, projects schemas, and
requires complete declared-outcome coverage.
## Relationship To wf_core
+44 -5
View File
@@ -389,21 +389,27 @@ The command combines two common edits:
Use `set-route` separately for outcome routing.
### Add A Capability Step To A Draft
### Add Typed Steps To A Draft
Use `wf draft add-step` when adding a new capability-backed step
to an existing draft. The command is explicit: it does not guess missing maps.
Use `wf draft add` to add one typed step to an existing draft:
```text
capability interrupt foreach join end
when choose match subgraph
```
The capability command is explicit: it does not guess missing maps.
Explicit top-level `--input input.x=x` and `--input state.x=x` mappings project
the corresponding workflow input/state schema fields from the capability input
schema.
When the capability declares multiple outcomes, provide exactly one
`--route OUTCOME=TARGET` for each declared outcome. Missing or unknown outcomes
are rejected before the draft is mutated. When `add-step --route` rejects an
are rejected before the draft is mutated. When `add capability --route` rejects an
outcome, use the declared outcomes and repair text from the error. Remove
unknown route entries and add one route for each missing declared outcome.
```bash
wf draft add-step report_ws \
wf draft add capability report_ws \
--revision 3 \
--step render \
--capability local.report.render_markdown_report \
@@ -417,6 +423,39 @@ wf draft add-step report_ws \
--bind-output title=state.title
```
Interrupts preserve explicit request and resume contracts:
```bash
wf draft add interrupt report_ws \
--revision 4 \
--step review \
--kind issue_review \
--request-schema-file request.schema.json \
--resume-schema-file resume.schema.json \
--outcome submitted \
--outcome cancelled \
--from-step draft_issues \
--from-outcome ok \
--route submitted=create_issues \
--route cancelled=revision_requested
```
Decision steps embed their targets and therefore do not accept `--route`:
```bash
wf draft add when report_ws \
--revision 5 \
--step decide \
--condition-file has-report.json \
--then publish \
--otherwise revise
```
`choose` reads an ordered clause array from `--clauses-file`; `match` reads an
ordered scalar case array from `--cases-file`. `subgraph` accepts either
`--workflow-name` or an immutable `--artifact-id` plus `--artifact-version`,
along with explicit boundary schemas and bindings.
Repeat `--input` and `--bind-output` once per mapping. Do not put multiple
mappings after a single flag; `--bind-output title=state.title
summary=state.summary` is parsed as an unexpected extra argument.
+12 -4
View File
@@ -50,7 +50,9 @@ wf draft handle <workspace_id> --revision <n> --to fail --branch lookup:error --
wf draft compile <workspace_id>
wf draft bind <workspace_id> --revision <n> --step <step_id> --from local.<field> --to state.<field>
wf draft bind <workspace_id> --revision <n> --step <step_id> --from input.<field> --to local.<field>
wf draft add-step <workspace_id> --revision <n> --step <step_id> --capability <qualified_name> --from-step <prev> --from-outcome ok --route ok=__end__ --route error=fail --input input.text=text --bind-output result=state.result
wf draft add capability <workspace_id> --revision <n> --step <step_id> --capability <qualified_name> --from-step <prev> --from-outcome ok --route ok=__end__ --route error=fail --input input.text=text --bind-output result=state.result
wf draft add interrupt <workspace_id> --revision <n> --step review --kind issue_review --outcome submitted --outcome cancelled --route submitted=next --route cancelled=revise
wf draft add when <workspace_id> --revision <n> --step decide --condition-file condition.json --then next --otherwise revise
wf draft validate <workspace_id>
wf draft save <workspace_id> --artifact <artifact_id> --version <n> --title <title>
@@ -105,7 +107,7 @@ projection. Use `input/state -> local` for step inputs and `local ->
state/output` for step outputs. It requires a capability-backed step with
`use`; use JSON Patch for non-capability/control draft steps.
To add a capability step, prefer `wf draft add-step` over raw
To add a capability step, prefer `wf draft add capability` over raw
JSON Patch when the route, input bindings, and output-to-state bindings are
known. It is explicit and does not guess missing maps.
If a capability has multiple outcomes, pass one `--route OUTCOME=TARGET` for
@@ -114,9 +116,15 @@ Repeat `--input` and `--bind-output` once per mapping. Do not put multiple
mappings after one flag.
```bash
wf draft add-step <workspace_id> --revision <n> --step <step_id> --capability <qualified_name> --input state.title=title --input state.summary=summary --bind-output markdown=state.markdown --bind-output title=state.title
wf draft add capability <workspace_id> --revision <n> --step <step_id> --capability <qualified_name> --input state.title=title --input state.summary=summary --bind-output markdown=state.markdown --bind-output title=state.title
```
Use the matching `wf draft add <kind>` command for control steps. `when`,
`choose`, and `match` embed their targets and do not accept `--route`.
Interrupt and subgraph commands preserve their explicit schema contracts.
Intermediate drafts may remain `status: invalid`; run `wf draft validate`
after the intended steps and routes are present.
`wf draft compile` prints the raw plan JSON directly on success. Do not expect a
top-level `compiled_plan` key from the CLI output.
@@ -145,7 +153,7 @@ top-level `compiled_plan` key from the CLI output.
- Do not use planning-session specs or implementation plans as user-facing runtime guidance.
- `set-input --map` is `GRAPH_SOURCE=BARE_LOCAL_FIELD`; never prefix the target
with `local.`.
- For `add-step --route`, route only outcomes reported by `wf cap inspect` or
- For `wf draft add capability --route`, route only outcomes reported by `wf cap inspect` or
the command error's `declared_outcomes` field.
- Do not confuse draft shape with raw plan shape: drafts use `steps/routes/use`;
raw plans use `nodes/edges/node`.
@@ -74,7 +74,7 @@ If a patch returns `revision_conflict`, fetch the workspace again and retry
against the latest revision.
Forward routes in drafts are allowed as invalid intermediate state. If
`wf draft add-step --route ok=collect` returns `status: invalid`, add the
`wf draft add capability --route ok=collect` returns `status: invalid`, add the
missing `collect` step next, then run `wf draft validate`. Do not save or
compile until validation is valid.
@@ -88,6 +88,7 @@ Prefer focused helpers over JSON Patch for common edits:
- `set_step_output_map`
- `set_workflow_output_map`
- `bind_draft`
- `add_step`
- `add_step_from_capability`
- `branch_draft`
- `handle_draft`
@@ -109,7 +110,9 @@ wf draft handle <workspace_id> --revision <n> --to fail --branch lookup:error --
wf draft compile <workspace_id>
wf draft bind <workspace_id> --revision <n> --step <step_id> --from local.<field> --to state.<field>
wf draft bind <workspace_id> --revision <n> --step <step_id> --from input.<field> --to local.<field>
wf draft add-step <workspace_id> --revision <n> --step <step_id> --capability <qualified_name> --from-step <prev> --from-outcome ok --route ok=__end__ --route error=fail --input input.text=text --bind-output result=state.result
wf draft add capability <workspace_id> --revision <n> --step <step_id> --capability <qualified_name> --from-step <prev> --from-outcome ok --route ok=__end__ --route error=fail --input input.text=text --bind-output result=state.result
wf draft add interrupt <workspace_id> --revision <n> --step review --kind issue_review --request-schema-file request.schema.json --resume-schema-file resume.schema.json --outcome submitted --outcome cancelled --route submitted=next --route cancelled=revise
wf draft add when <workspace_id> --revision <n> --step decide --condition-file condition.json --then next --otherwise revise
```
`set-workflow-output` maps a graph source path (`input.*`, `state.*`, or
@@ -163,7 +166,8 @@ wf draft validate <workspace_id>
`--route OUTCOME=TARGET` for each outcome; when omitted and the capability
declares a single outcome, that outcome routes to `__end__`. Multi-outcome
capabilities require exact route coverage; missing or unknown outcomes are
rejected before mutation. When `add-step --route` rejects an outcome, the
rejected before mutation. When `wf draft add capability --route` rejects an
outcome, the
error reports declared outcomes and direct add/remove repair guidance. Remove
unknown route entries and add one route for each missing declared outcome. It
still requires explicit choices; if you do not
@@ -173,7 +177,7 @@ wf draft validate <workspace_id>
capability input schema.
```bash
wf draft add-step <workspace_id> --revision <n> --step <step_id> --capability <qualified_name> --from-step <prev> --from-outcome ok --route ok=__end__ --route error=fail --input input.text=text --input input.other=other --bind-output result=state.result --bind-output title=state.title
wf draft add capability <workspace_id> --revision <n> --step <step_id> --capability <qualified_name> --from-step <prev> --from-outcome ok --route ok=__end__ --route error=fail --input input.text=text --input input.other=other --bind-output result=state.result --bind-output title=state.title
wf draft validate <workspace_id>
```
@@ -181,6 +185,16 @@ Repeat `--input` and `--bind-output` once per mapping. Do not write
`--bind-output title=state.title summary=state.summary`; the second mapping is
an unexpected extra argument because it is not attached to its own flag.
- `add_step`
Adds any typed `DraftStep` with optional incoming and outgoing route wiring in
one revision. The CLI exposes one command per kind under `wf draft add`:
`interrupt`, `foreach`, `join`, `end`, `when`, `choose`, `match`, and
`subgraph`. Decision targets are embedded and reject `--route`. Interrupts
and subgraphs preserve JSON Schema boundary contracts. Invalid intermediate
drafts remain saveable in the workspace but must pass `wf draft validate`
before compile or artifact save.
- `branch_draft`
Updates routes for an existing step in one revision without rewriting the
@@ -23,12 +23,15 @@ validated, runnable deployment.
local input/output property. It declares the matching schema and merges
the binding in one revision-checked edit.
- When adding a new capability-backed step, prefer:
```bash
wf draft add-step ...
wf draft add capability ...
wf draft validate <workspace_id>
```
Use raw `wf draft patch` only when changing structure that no focused helper
covers.
For control flow, use the corresponding typed `wf draft add <kind>`
command. Use raw `wf draft patch` only when changing structure that no
focused helper covers.
- Use JSON Patch only for general structural edits.
6. Save an artifact.
- Draft artifact:
+7 -1
View File
@@ -166,7 +166,13 @@ class WorkflowDraftAuthoringApi:
routes: dict[str, str] | None = None,
) -> dict[str, Any]:
"""Add one typed draft step and optional route edits in one revision."""
workspace = self.drafts._draft_store().get_workspace(workspace_id)
checked = self._workspace_if_revision_matches(
workspace_id=workspace_id,
revision=revision,
)
if isinstance(checked, dict):
return checked
workspace = checked
steps = workspace.draft.get("steps")
if not isinstance(steps, dict):
raise ValueError("draft steps must be an object")
+2 -2
View File
@@ -74,8 +74,8 @@ def _add_step(builder: WorkflowBuilder, step_id: str, step: DraftStep):
"outcomes": step.interrupt.outcomes,
}
if step.interrupt.request_schema is not None:
interrupt_kwargs["request_schema"] = step.interrupt.request_schema.model_dump(
mode="json", exclude_none=True
interrupt_kwargs["request_schema"] = (
step.interrupt.request_schema.model_dump(mode="json", exclude_none=True)
)
if step.interrupt.resume_schema is not None:
interrupt_kwargs["resume_schema"] = step.interrupt.resume_schema.model_dump(
+1 -3
View File
@@ -205,9 +205,7 @@ class DraftSubgraphPayload(BaseModel):
workflow: WorkflowRef
desc: str | None = None
input_schema: SchemaRef = Field(default_factory=lambda: SchemaRef(type="object"))
output_schema: SchemaRef = Field(
default_factory=lambda: SchemaRef(type="object")
)
output_schema: SchemaRef = Field(default_factory=lambda: SchemaRef(type="object"))
input: list[InputBinding] = Field(default_factory=list)
output: list[OutputBinding] = Field(default_factory=list)
outcomes: list[str] = Field(default_factory=lambda: ["ok"], min_length=1)
+45 -11
View File
@@ -145,6 +145,10 @@ def add_step_from_capability(
This command does not guess missing maps. Pass the route and bindings you
want, then run `wf draft validate <workspace_id>`.
Example:
`wf draft add capability report_ws --revision 1 --step render
--capability local.report.render --route ok=__end__`
Repeat the flag for multiple bindings:
`--input state.title=title --input state.summary=summary`
`--bind-output title=state.title --bind-output summary=state.summary`
@@ -222,7 +226,11 @@ def add_interrupt_step(
typer.Option("--route", help="Route mapping OUTCOME=TARGET. Repeat as needed."),
] = None,
) -> None:
"""Add a typed interrupt and its request/resume contract."""
"""Add a typed interrupt and its request/resume contract.
Example: `wf draft add interrupt WS --revision 1 --step review --kind review`.
Run `wf draft validate WS` after editing.
"""
request_map = _parse_step_input_map_flags(request, option_name="--request")
resume_map = _parse_output_map_flags(resume, option_name="--resume")
routes = _parse_route_flags(route)
@@ -287,9 +295,7 @@ def add_foreach_step(
over: Annotated[
str, typer.Option("--over", help="Graph path containing the item list.")
],
as_: Annotated[
str, typer.Option("--as", help="Context key for the current item.")
],
as_: Annotated[str, typer.Option("--as", help="Context key for the current item.")],
mode: Annotated[
Literal["serial", "concurrent"],
typer.Option("--mode", help="Item admission mode."),
@@ -324,7 +330,11 @@ def add_foreach_step(
typer.Option("--route", help="Route mapping OUTCOME=TARGET. Repeat as needed."),
] = None,
) -> None:
"""Add a foreach loop with explicit item and concurrency policies."""
"""Add a foreach loop with explicit item and concurrency policies.
Example: `wf draft add foreach WS --revision 1 --step each --over state.items --as item`.
Run `wf draft validate WS` after editing.
"""
if mode == "serial" and (max_active is not None or max_outstanding is not None):
raise typer.BadParameter(
"--max-active and --max-outstanding require --mode concurrent"
@@ -389,7 +399,11 @@ def add_join_step(
typer.Option("--route", help="Route mapping OUTCOME=TARGET. Repeat as needed."),
] = None,
) -> None:
"""Add a join step."""
"""Add a join step.
Example: `wf draft add join WS --revision 1 --step joined --route done=__end__`.
Run `wf draft validate WS` after editing.
"""
_submit_step(
ctx,
workspace_id=workspace_id,
@@ -421,7 +435,11 @@ def add_end_step(
typer.Option("--from-outcome", help="Outcome on --from-step (default: ok)."),
] = None,
) -> None:
"""Add an explicit terminal outcome step."""
"""Add an explicit terminal outcome step.
Example: `wf draft add end WS --revision 1 --step finish --outcome ok`.
Run `wf draft validate WS` after editing.
"""
try:
step = DraftEndStep(end=DraftEndPayload(outcome=outcome))
except ValidationError as exc:
@@ -461,7 +479,11 @@ def add_when_step(
typer.Option("--from-outcome", help="Outcome on --from-step (default: ok)."),
] = None,
) -> None:
"""Add a boolean decision whose targets are embedded in the step."""
"""Add a boolean decision whose targets are embedded in the step.
Example: `wf draft add when WS --revision 1 --step decide --condition-file condition.json --then next`.
Run `wf draft validate WS` after editing.
"""
try:
condition = _condition_adapter.validate_python(
parse_json_file(condition_file, option_name="--condition-file")
@@ -508,7 +530,11 @@ def add_choose_step(
typer.Option("--from-outcome", help="Outcome on --from-step (default: ok)."),
] = None,
) -> None:
"""Add an ordered first-true decision with embedded targets."""
"""Add an ordered first-true decision with embedded targets.
Example: `wf draft add choose WS --revision 1 --step decide --clauses-file clauses.json`.
Run `wf draft validate WS` after editing.
"""
try:
clauses = _choose_clauses_adapter.validate_python(
parse_json_file(clauses_file, option_name="--clauses-file")
@@ -554,7 +580,11 @@ def add_match_step(
typer.Option("--from-outcome", help="Outcome on --from-step (default: ok)."),
] = None,
) -> None:
"""Add an ordered scalar match decision with embedded targets."""
"""Add an ordered scalar match decision with embedded targets.
Example: `wf draft add match WS --revision 1 --step decide --value state.status --cases-file cases.json`.
Run `wf draft validate WS` after editing.
"""
try:
cases = _match_cases_adapter.validate_python(
parse_json_file(cases_file, option_name="--cases-file")
@@ -637,7 +667,11 @@ def add_subgraph_step(
typer.Option("--route", help="Route mapping OUTCOME=TARGET. Repeat as needed."),
] = None,
) -> None:
"""Add a child-workflow boundary with an explicit reference and contract."""
"""Add a child-workflow boundary with an explicit reference and contract.
Example: `wf draft add subgraph WS --revision 1 --step child --workflow-name child`.
Run `wf draft validate WS` after editing.
"""
if workflow_name is not None:
if artifact_id is not None or artifact_version is not None:
raise typer.BadParameter(
+1 -3
View File
@@ -553,9 +553,7 @@ def test_adapter_lowers_subgraph_step_without_resolving_artifact() -> None:
"input_schema": input_schema,
"output_schema": output_schema,
"input": [{"target": "topic", "path": "state.topic"}],
"output": [
{"source": "report", "target": "state.report"}
],
"output": [{"source": "report", "target": "state.report"}],
"outcomes": ["ok", "error"],
}
}
+28
View File
@@ -932,6 +932,34 @@ async def test_add_step_routes_incoming_and_outgoing_edges_atomically(
assert workspace["draft"]["routes"]["new_step"] == {"ok": "__end__"}
@pytest.mark.asyncio
async def test_add_step_stale_revision_wins_over_content_preflight(
tmp_path: Path,
) -> None:
artifact_store = FileWorkflowArtifactStore(tmp_path / "draft_add_stale")
draft_api, _service, authoring = _draft_api(artifact_store, register_echo=True)
api = WorkflowApi(authoring.context)
await draft_api.create_draft_workspace(workspace_id="draft_ws", draft=_echo_draft())
before = await draft_api.get_draft_workspace(
workspace_id="draft_ws", include_draft=True
)
step = TypeAdapter(DraftStep).validate_python({"join": {}})
result = await api.add_step(
workspace_id="draft_ws",
revision=2,
step_id="echo",
step=step,
)
after = await draft_api.get_draft_workspace(
workspace_id="draft_ws", include_draft=True
)
assert result["status"] == "conflict"
assert result["diagnostics"][0]["code"] == "revision_conflict"
assert after == before
@pytest.mark.asyncio
async def test_add_step_adds_missing_incoming_route_parent_atomically(
tmp_path: Path,
+32 -26
View File
@@ -233,6 +233,7 @@ def test_wf_draft_add_capability_help_explains_explicit_wiring() -> None:
assert "does not guess" in output
assert "projects its schemas and bindings" in output
assert "draft validate" in output
assert "wf draft add capability report_ws" in output
assert "Repeat the flag" in output
assert "--input state.title=title --input state.summary=summary" in output
assert (
@@ -521,9 +522,7 @@ def test_wf_draft_add_join_and_end_build_concrete_steps(monkeypatch) -> None:
assert end_result.exit_code == 0, end_result.output
assert calls[0]["step"].model_dump(mode="json") == {"join": {}}
assert calls[0]["routes"] == {"done": "finish"}
assert calls[1]["step"].model_dump(mode="json") == {
"end": {"outcome": "completed"}
}
assert calls[1]["step"].model_dump(mode="json") == {"end": {"outcome": "completed"}}
assert calls[1]["routes"] is None
@@ -667,7 +666,9 @@ def test_wf_draft_add_control_commands_reject_invalid_input_before_api_call(
assert "--bind-output" not in duplicate_resume.output
assert "Traceback" not in duplicate_resume.output
assert missing_collect_target.exit_code == 2
assert "collect item error policy requires collect_to" in missing_collect_target.output
assert (
"collect item error policy requires collect_to" in missing_collect_target.output
)
assert "Traceback" not in missing_collect_target.output
assert end_route.exit_code == 2
assert "No such option" in end_route.output
@@ -681,7 +682,9 @@ def test_wf_draft_add_control_command_help_is_type_specific() -> None:
join = runner.invoke(app, ["draft", "add", "join", "--help"])
end = runner.invoke(app, ["draft", "add", "end", "--help"])
assert interrupt.exit_code == foreach.exit_code == join.exit_code == end.exit_code == 0
assert (
interrupt.exit_code == foreach.exit_code == join.exit_code == end.exit_code == 0
)
assert "--request-schema-file" in interrupt.output
assert "--resume-schema-file" in interrupt.output
assert "--request" in interrupt.output
@@ -701,7 +704,9 @@ def test_wf_draft_add_control_command_help_is_type_specific() -> None:
assert "--route" not in end.output
def test_wf_draft_add_decisions_build_ordered_typed_steps(monkeypatch, tmp_path) -> None:
def test_wf_draft_add_decisions_build_ordered_typed_steps(
monkeypatch, tmp_path
) -> None:
calls: list[dict[str, Any]] = []
class FakeHandlers:
@@ -714,9 +719,7 @@ def test_wf_draft_add_decisions_build_ordered_typed_steps(monkeypatch, tmp_path)
"wf_cli.commands.draft_add.load_cli_context", lambda _ctx: context
)
condition_file = tmp_path / "condition.json"
condition_file.write_text(
'{"op":"exists","path":"state.report"}', encoding="utf-8"
)
condition_file.write_text('{"op":"exists","path":"state.report"}', encoding="utf-8")
clauses_file = tmp_path / "clauses.json"
clauses_file.write_text(
'[{"if":{"op":"exists","path":"state.report"},"then":"publish"},'
@@ -835,9 +838,7 @@ def test_wf_draft_add_decisions_reject_bad_files_and_generic_routes(
object_cases = tmp_path / "object.json"
object_cases.write_text("{}", encoding="utf-8")
condition_file = tmp_path / "condition.json"
condition_file.write_text(
'{"op":"exists","path":"state.report"}', encoding="utf-8"
)
condition_file.write_text('{"op":"exists","path":"state.report"}', encoding="utf-8")
empty = runner.invoke(
app,
@@ -978,23 +979,15 @@ def test_wf_draft_add_subgraph_builds_name_and_artifact_contracts(
assert named.exit_code == 0, named.output
assert artifact.exit_code == 0, artifact.output
named_payload = calls[0]["step"].model_dump(mode="json", by_alias=True)[
"subgraph"
]
named_payload = calls[0]["step"].model_dump(mode="json", by_alias=True)["subgraph"]
assert named_payload["workflow"] == {"name": "child_workflow"}
assert named_payload["desc"] == "Generate the child report."
assert named_payload["input_schema"]["properties"] == {
"topic": {"type": "string"}
}
assert named_payload["input_schema"]["properties"] == {"topic": {"type": "string"}}
assert named_payload["output_schema"]["properties"] == {
"report": {"type": "string"}
}
assert named_payload["input"] == [
{"target": "topic", "path": "state.topic"}
]
assert named_payload["output"] == [
{"source": "report", "target": "state.report"}
]
assert named_payload["input"] == [{"target": "topic", "path": "state.topic"}]
assert named_payload["output"] == [{"source": "report", "target": "state.report"}]
assert named_payload["outcomes"] == ["ok", "error"]
assert calls[0]["routes"] == {"ok": "publish", "error": "revise"}
artifact_payload = calls[1]["step"].model_dump(mode="json", by_alias=True)[
@@ -1024,7 +1017,14 @@ def test_wf_draft_add_subgraph_rejects_invalid_reference_combinations(
invalid_refs = [
[],
["--workflow-name", " "],
["--workflow-name", "child", "--artifact-id", "saved", "--artifact-version", "1"],
[
"--workflow-name",
"child",
"--artifact-id",
"saved",
"--artifact-version",
"1",
],
["--artifact-id", "saved"],
["--artifact-version", "1"],
]
@@ -1056,7 +1056,9 @@ def test_wf_draft_add_decision_and_subgraph_help_is_type_specific() -> None:
match = runner.invoke(app, ["draft", "add", "match", "--help"])
subgraph = runner.invoke(app, ["draft", "add", "subgraph", "--help"])
assert when.exit_code == choose.exit_code == match.exit_code == subgraph.exit_code == 0
assert (
when.exit_code == choose.exit_code == match.exit_code == subgraph.exit_code == 0
)
assert "--condition-file" in when.output
assert "--then" in when.output
assert "--route" not in when.output
@@ -1073,6 +1075,10 @@ def test_wf_draft_add_decision_and_subgraph_help_is_type_specific() -> None:
assert "--output-schema-file" in subgraph.output
assert "--route" in subgraph.output
for result in (when, choose, match, subgraph):
assert "Example:" in result.output
assert "draft validate WS" in result.output
def test_wf_draft_help_does_not_list_old_add_step_from_capability() -> None:
result = runner.invoke(app, ["draft", "--help"])
+5 -7
View File
@@ -1393,7 +1393,9 @@ def test_wf_draft_add_capability_uses_rpc_target(monkeypatch, tmp_path) -> None:
assert "workflow.draft_workspaces.add_step" not in rpc_methods
def test_wf_draft_add_control_steps_use_generic_rpc_target(monkeypatch, tmp_path) -> None:
def test_wf_draft_add_control_steps_use_generic_rpc_target(
monkeypatch, tmp_path
) -> None:
server = build_local_static_workflow_server(tmp_path / "store")
_patch_rpc_client_to_server(monkeypatch, server)
rpc_methods: list[str] = []
@@ -1421,9 +1423,7 @@ def test_wf_draft_add_control_steps_use_generic_rpc_target(monkeypatch, tmp_path
encoding="utf-8",
)
condition_file = tmp_path / "condition.json"
condition_file.write_text(
'{"op":"exists","path":"state.value"}', encoding="utf-8"
)
condition_file.write_text('{"op":"exists","path":"state.value"}', encoding="utf-8")
clauses_file = tmp_path / "clauses.json"
clauses_file.write_text(
'[{"if":{"op":"exists","path":"state.value"},"then":"call"}]',
@@ -1586,9 +1586,7 @@ def test_wf_draft_add_control_steps_use_generic_rpc_target(monkeypatch, tmp_path
{
"workflow": {"name": "child"},
"input": [{"target": "value", "path": "input.value"}],
"output": [
{"source": "value", "target": "state.child_value"}
],
"output": [{"source": "value", "target": "state.child_value"}],
"outcomes": ["ok"],
},
{"ok": "__end__"},
+7 -10
View File
@@ -952,9 +952,7 @@ def test_add_draft_step_params_preserve_typed_step_json() -> None:
"workspace_id": "ws",
"revision": 1,
"step_id": "child",
"step": {
"subgraph": {"workflow": {"artifact_id": "child", "version": 2}}
},
"step": {"subgraph": {"workflow": {"artifact_id": "child", "version": 2}}},
}
)
assert subgraph.model_dump(mode="json", by_alias=True)["step"]["subgraph"][
@@ -983,7 +981,10 @@ def test_add_draft_step_params_reject_invalid_kind_and_route_source() -> None:
}
)
for incoming in ({"step_id": "", "outcome": "ok"}, {"step_id": "call", "outcome": ""}):
for incoming in (
{"step_id": "", "outcome": "ok"},
{"step_id": "call", "outcome": ""},
):
with pytest.raises(ValidationError):
AddDraftStepParams.model_validate(
{
@@ -1058,9 +1059,7 @@ async def test_rpc_draft_workspace_add_typed_step_round_trip(tmp_path) -> None:
assert "bad" not in fetched["result"]["draft"]["steps"]
review = fetched["result"]["draft"]["steps"]["review"]["interrupt"]
assert review["request_schema"]["type"] == "object"
assert review["resume_schema"]["properties"]["selected"] == {
"type": "array"
}
assert review["resume_schema"]["properties"]["selected"] == {"type": "array"}
async def test_rpc_draft_workspace_add_untyped_interrupt_preserves_null_schemas(
@@ -1105,9 +1104,7 @@ async def test_rpc_draft_workspace_add_untyped_interrupt_preserves_null_schemas(
interrupt = fetched["result"]["draft"]["steps"]["pause"]["interrupt"]
assert interrupt["request_schema"] is None
assert interrupt["resume_schema"] is None
assert fetched["result"]["draft"]["steps"]["pause"] == {
"interrupt": interrupt
}
assert fetched["result"]["draft"]["steps"]["pause"] == {"interrupt": interrupt}
async def test_rpc_diagnoses_source(tmp_path) -> None:
+7 -9
View File
@@ -721,9 +721,7 @@ async def test_rpc_client_draft_workspace_add_step_from_capability(tmp_path) ->
(
"subgraph",
TypeAdapter(DraftStep).validate_python(
{
"subgraph": {"workflow": {"artifact_id": "child", "version": 2}}
}
{"subgraph": {"workflow": {"artifact_id": "child", "version": 2}}}
),
{"workflow": {"artifact_id": "child", "version": 2}},
),
@@ -769,12 +767,12 @@ async def test_rpc_client_add_step_preserves_all_typed_variants(
assert request["params"]["step"]["foreach"]["as"] == "item"
assert "as_" not in request["params"]["step"]["foreach"]
if step_id == "interrupt":
assert request["params"]["step"]["interrupt"]["request_schema"][
"type"
] == "object"
assert request["params"]["step"]["interrupt"]["resume_schema"][
"type"
] == "object"
assert (
request["params"]["step"]["interrupt"]["request_schema"]["type"] == "object"
)
assert (
request["params"]["step"]["interrupt"]["resume_schema"]["type"] == "object"
)
if step_id == "subgraph":
assert request["params"]["step"]["subgraph"]["workflow"] == {
"artifact_id": "child",