feat: add focused draft cli edits

This commit is contained in:
lda
2026-06-15 18:27:27 +07:00 Verified
parent 577fb4707a
commit 97076479f1
20 changed files with 1832 additions and 6 deletions
+3
View File
@@ -54,6 +54,9 @@ clear operator feedback before adding more architecture.
- Completed: the opencode browser-click challenge harness is local-first via - Completed: the opencode browser-click challenge harness is local-first via
`wf --config examples/browser_click_workflow/wf.config.json --local`, with `wf --config examples/browser_click_workflow/wf.config.json --local`, with
optional `--start-server` / `--server-url` modes for JSON-RPC-path trials. optional `--start-server` / `--server-url` modes for JSON-RPC-path trials.
- Completed: focused draft edit helpers are exposed through RPC/CLI, and
`wf deploy create` is accepted as an alias for `wf deploy save`. Docs now
distinguish draft shape from raw plan shape for agent authoring.
- Keep status read-only; do not mutate registry, auth, config, or stores. - Keep status read-only; do not mutate registry, auth, config, or stores.
## Priority 2: Durable Run/Resume Hardening ## Priority 2: Durable Run/Resume Hardening
File diff suppressed because it is too large Load Diff
+23 -1
View File
@@ -286,6 +286,22 @@ wf draft patch concat_ws \
--input-file draft-patch.json --input-file draft-patch.json
``` ```
Focused draft edit commands cover common graph edits without writing RFC 6902
patches directly:
```bash
wf draft set-name concat_ws --revision 1 --name concat_ws_v2
wf draft set-route concat_ws --revision 2 --step call --outcome ok --to __end__
wf draft set-input concat_ws --revision 3 --step call --map input.items=items --map input.separator=separator
wf draft set-output concat_ws --revision 4 --step call --map value=state.value
```
`set-input` maps graph source paths to node-local input fields:
`input.text=text` means `input.text -> local.text`.
`set-output` maps node-local output fields to workflow state paths:
`text=state.text` means `local.text -> state.text`.
Validate: Validate:
```bash ```bash
@@ -346,6 +362,10 @@ wf artifact create-from-plan workflow.plan.json \
--binding local.ops=local.ops --binding local.ops=local.ops
``` ```
`artifact create-from-plan` expects the raw workflow plan shape (`nodes`,
`edges`, `node`). It does not accept draft workspace shape (`steps`, `routes`,
`use`).
Prefer draft workspaces for iterative authoring. Use `create-from-plan` when a Prefer draft workspaces for iterative authoring. Use `create-from-plan` when a
compiler, fixture, or advanced client already has a complete raw workflow plan. compiler, fixture, or advanced client already has a complete raw workflow plan.
@@ -368,6 +388,9 @@ wf deploy save concat_ws.default \
--version 1 --version 1
``` ```
`wf deploy create` is accepted as an alias for `wf deploy save`; docs use
`save` as the canonical verb because deployments are mutable records.
Save a deployment from JSON: Save a deployment from JSON:
```bash ```bash
@@ -527,5 +550,4 @@ wf explain --input-file validation-output.json
- The CLI reuses `wf_mcp` service/config/store wiring in v1. - The CLI reuses `wf_mcp` service/config/store wiring in v1.
- Config loading registers stores and connections, but not arbitrary in-memory - Config loading registers stores and connections, but not arbitrary in-memory
test `NodeSpec` functions. test `NodeSpec` functions.
- Targeted draft editing helpers such as `wf draft step add` are not in v1.
- `wf` does not replace MCP resources/prompts or interactive MCP clients. - `wf` does not replace MCP resources/prompts or interactive MCP clients.
+20
View File
@@ -718,6 +718,26 @@ workflow can wrap a raw MCP call and map result content to outcomes such as
`found`, `not_found`, `unauthorized`, or `rate_limited` when those meanings are `found`, `not_found`, `unauthorized`, or `rate_limited` when those meanings are
known for that tool. known for that tool.
## Raw Plan Import
`wf artifact create-from-plan` imports a complete raw workflow plan. This is an
advanced/compiler path, not the interactive draft authoring path.
Raw plans use execution-model fields such as:
- `nodes`
- `edges`
- node field `node`
Draft workspaces use authoring fields such as:
- `steps`
- `routes`
- step field `use`
Do not pass draft JSON to `artifact create-from-plan`; save drafts with
`wf draft save`.
## Open Questions ## Open Questions
- How should child workflow interrupts compose with parent workflow execution - How should child workflow interrupts compose with parent workflow execution
+16
View File
@@ -667,6 +667,22 @@ the authoring envelope while raw JSON Schema fields remain plain JSON objects.
path. It validates and compiles the same draft workspace, but fixes the saved path. It validates and compiles the same draft workspace, but fixes the saved
artifact kind to `wrapper` so clients do not need to pass `kind` manually. artifact kind to `wrapper` so clients do not need to pass `kind` manually.
## Focused Edit Commands
For routine edits, prefer focused commands over hand-written JSON Patch:
```bash
wf draft set-name <workspace_id> --revision <n> --name <name>
wf draft set-route <workspace_id> --revision <n> --step <step_id> --outcome ok --to <target_step_or___end__>
wf draft set-input <workspace_id> --revision <n> --step <step_id> --map input.text=text
wf draft set-output <workspace_id> --revision <n> --step <step_id> --map text=state.text
```
Use `draft patch` when these focused commands do not cover the structural edit.
Drafts are not raw workflow plans. Drafts use `steps`, `routes`, and step field
`use`. Raw plans use `nodes`, `edges`, and node field `node`.
## Patching Drafts ## Patching Drafts
`patch_draft` accepts JSON Patch operations. `patch_draft` accepts JSON Patch operations.
@@ -32,11 +32,18 @@ workflow construction inside a Python script.
Two product-facing authoring paths are acceptable: Two product-facing authoring paths are acceptable:
- create a draft from one capability, patch it with RFC 6902 JSON Patch, then - draft path: create a draft from one capability, use focused draft edit
validate/save/deploy/run it; commands or your own RFC 6902 JSON Patch, then validate/save/deploy/run it;
- or, if you already have a complete raw workflow plan file, use - raw-plan path: write your own complete raw workflow plan file and use
`wf artifact create-from-plan` before deploy/run. `wf artifact create-from-plan` before deploy/run.
Do not mix the formats. Drafts use `steps`, `routes`, and step field `use`.
Raw plans use `nodes`, `edges`, and node field `node`. Do not pass draft JSON to
`wf artifact create-from-plan`.
The deployment command is `wf deploy save`; `wf deploy create` is accepted as an
alias.
Do not use a pre-existing generated patch or raw-plan answer file. If you find Do not use a pre-existing generated patch or raw-plan answer file. If you find
one, ignore it and author your own workflow definition. one, ignore it and author your own workflow definition.
+7
View File
@@ -33,11 +33,16 @@ wf cap call <capability> --input '{"field":"value"}'
wf draft create-from-capability <workspace_id> <capability> wf draft create-from-capability <workspace_id> <capability>
wf draft inspect <workspace_id> --include-draft wf draft inspect <workspace_id> --include-draft
wf draft patch <workspace_id> --revision <n> --input-file patch.json wf draft patch <workspace_id> --revision <n> --input-file patch.json
wf draft set-name <workspace_id> --revision <n> --name <name>
wf draft set-route <workspace_id> --revision <n> --step <step_id> --outcome <outcome> --to <target>
wf draft set-input <workspace_id> --revision <n> --step <step_id> --map input.text=text
wf draft set-output <workspace_id> --revision <n> --step <step_id> --map text=state.text
wf draft validate <workspace_id> wf draft validate <workspace_id>
wf draft save <workspace_id> --artifact <artifact_id> --version <n> --title <title> wf draft save <workspace_id> --artifact <artifact_id> --version <n> --title <title>
wf artifact create-from-plan workflow.plan.json --artifact <artifact_id> --version <n> --title <title> wf artifact create-from-plan workflow.plan.json --artifact <artifact_id> --version <n> --title <title>
wf deploy save <deployment_id> --artifact <artifact_id> --version <n> --binding <logical>=<concrete> wf deploy save <deployment_id> --artifact <artifact_id> --version <n> --binding <logical>=<concrete>
wf deploy create <deployment_id> --artifact <artifact_id> --version <n>
wf deploy validate <deployment_id> wf deploy validate <deployment_id>
wf run start <deployment_id> --input-file input.json wf run start <deployment_id> --input-file input.json
wf run trace <run_id> --from 0 --limit 25 wf run trace <run_id> --from 0 --limit 25
@@ -54,3 +59,5 @@ wf run trace <run_id> --from 0 --limit 25
- Do not treat wrapper hints as semantic guarantees. - Do not treat wrapper hints as semantic guarantees.
- If validation fails, run `wf explain <code>` or `wf explain --input-file <validation-output.json>`. - If validation fails, run `wf explain <code>` or `wf explain --input-file <validation-output.json>`.
- Do not use planning-session specs or implementation plans as user-facing runtime guidance. - Do not use planning-session specs or implementation plans as user-facing runtime guidance.
- Do not confuse draft shape with raw plan shape: drafts use `steps/routes/use`;
raw plans use `nodes/edges/node`.
+3
View File
@@ -41,9 +41,12 @@ low-level escape hatch or you already have a complete compiler/generated plan.
lifecycle behavior. lifecycle behavior.
- Treat wrapper hints as scaffolding, not semantic truth. - Treat wrapper hints as scaffolding, not semantic truth.
- Use draft workspaces for iterative authoring; avoid rewriting full drafts. - Use draft workspaces for iterative authoring; avoid rewriting full drafts.
- Prefer focused draft edit commands before hand-writing JSON Patch.
- If a complete raw JSON/YAML plan already exists, the CLI escape hatch is - If a complete raw JSON/YAML plan already exists, the CLI escape hatch is
`wf artifact create-from-plan`; do not write helper scripts that call `wf artifact create-from-plan`; do not write helper scripts that call
`WorkflowApi.create_artifact_from_plan` directly. `WorkflowApi.create_artifact_from_plan` directly.
- Use `artifact create-from-plan` only for complete raw plans; do not pass draft
JSON to it.
- Use explicit source bindings at deployment time. - Use explicit source bindings at deployment time.
- Keep traces bounded with `trace_range`. - Keep traces bounded with `trace_range`.
- Do not expect a newly saved workflow to become a new MCP tool mid-session. - Do not expect a newly saved workflow to become a new MCP tool mid-session.
@@ -69,6 +69,21 @@ Prefer focused helpers over JSON Patch for common edits:
- `set_step_input_map` - `set_step_input_map`
- `set_step_output_map` - `set_step_output_map`
CLI equivalents:
```bash
wf draft set-name <workspace_id> --revision <n> --name <name>
wf draft set-route <workspace_id> --revision <n> --step <step_id> --outcome ok --to <target>
wf draft set-input <workspace_id> --revision <n> --step <step_id> --map input.text=text
wf draft set-output <workspace_id> --revision <n> --step <step_id> --map text=state.text
```
`set-input` direction: `input.text=text` means graph source `input.text` maps to
node-local target `local.text`.
`set-output` direction: `text=state.text` means node-local source `local.text`
maps to graph target `state.text`.
Use JSON Patch for structural edits the helpers do not cover. Use JSON Patch for structural edits the helpers do not cover.
For larger patches, write a JSON Patch array to a file and pass it with For larger patches, write a JSON Patch array to a file and pass it with
@@ -18,12 +18,14 @@ validated, runnable deployment.
- MCP: `wf.workflow.create_draft_workspace_from_capability` - MCP: `wf.workflow.create_draft_workspace_from_capability`
- CLI: `wf draft create-from-capability <workspace_id> <capability>` - CLI: `wf draft create-from-capability <workspace_id> <capability>`
5. Inspect/patch/validate the workspace until valid. 5. Inspect/patch/validate the workspace until valid.
- Prefer focused helpers when available. - Use focused CLI commands (`set-name`, `set-route`, `set-input`, `set-output`)
- Use JSON Patch only for general edits. for common edits.
- Use JSON Patch only for general structural edits.
6. Save an artifact. 6. Save an artifact.
- Full workflow: `create_artifact_from_workspace` - Full workflow: `create_artifact_from_workspace`
- Reusable wrapper: `create_wrapper_from_workspace` - Reusable wrapper: `create_wrapper_from_workspace`
7. Save and validate a deployment. 7. Save and validate a deployment.
- `wf deploy save` (or `wf deploy create` alias)
8. Run the deployment. 8. Run the deployment.
9. Inspect the run summary first; read bounded traces only when needed. 9. Inspect the run summary first; read bounded traces only when needed.
@@ -43,6 +45,10 @@ wf artifact create-from-plan workflow.plan.json \
--binding <logical_source>=<concrete_source> --binding <logical_source>=<concrete_source>
``` ```
Do not pass draft JSON to `artifact create-from-plan`. Drafts use `steps`,
`routes`, and step field `use`. Raw plans use `nodes`, `edges`, and node field
`node`.
Then continue with the normal deployment and run steps: Then continue with the normal deployment and run steps:
```bash ```bash
+36
View File
@@ -76,6 +76,42 @@ class WorkflowDraftSurface(Protocol):
patch: list[dict[str, Any]], patch: list[dict[str, Any]],
) -> dict[str, Any]: ... ) -> dict[str, Any]: ...
async def set_draft_name(
self,
*,
workspace_id: str,
revision: int,
name: str,
) -> dict[str, Any]: ...
async def set_draft_route(
self,
*,
workspace_id: str,
revision: int,
step_id: str,
outcome: str,
target: str,
) -> dict[str, Any]: ...
async def set_step_input_map(
self,
*,
workspace_id: str,
revision: int,
step_id: str,
input_map: dict[str, str],
) -> dict[str, Any]: ...
async def set_step_output_map(
self,
*,
workspace_id: str,
revision: int,
step_id: str,
output_map: dict[str, str],
) -> dict[str, Any]: ...
async def validate_draft_workspace( async def validate_draft_workspace(
self, self,
*, *,
+55
View File
@@ -98,6 +98,61 @@ def save_deployment(
] = None, ] = None,
) -> None: ) -> None:
"""Save a workflow deployment from flags or a JSON object.""" """Save a workflow deployment from flags or a JSON object."""
_save_deployment_command(
ctx,
deployment_id=deployment_id,
artifact_id=artifact_id,
version=version,
binding=binding,
input_json=input_json,
input_file=input_file,
)
@app.command("create")
def create_deployment(
ctx: typer.Context,
deployment_id: Annotated[str | None, typer.Argument(help="Deployment id.")] = None,
artifact_id: Annotated[
str | None, typer.Option("--artifact", help="Artifact id.")
] = None,
version: Annotated[
int | None, typer.Option("--version", min=1, help="Artifact version.")
] = None,
binding: Annotated[
list[str] | None,
typer.Option("--binding", help="Logical=concrete source binding. Repeatable."),
] = None,
input_json: Annotated[
str | None, typer.Option("--input", help="Full deployment JSON object.")
] = None,
input_file: Annotated[
Path | None,
typer.Option("--input-file", help="Path to full deployment JSON object."),
] = None,
) -> None:
"""Alias for `deploy save`; creates or updates a deployment record."""
_save_deployment_command(
ctx,
deployment_id=deployment_id,
artifact_id=artifact_id,
version=version,
binding=binding,
input_json=input_json,
input_file=input_file,
)
def _save_deployment_command(
ctx: typer.Context,
*,
deployment_id: str | None,
artifact_id: str | None,
version: int | None,
binding: list[str] | None,
input_json: str | None,
input_file: Path | None,
) -> None:
try: try:
if input_json is not None or input_file is not None: if input_json is not None or input_file is not None:
payload = parse_json_input(input_json=input_json, input_file=input_file) payload = parse_json_input(input_json=input_json, input_file=input_file)
+128
View File
@@ -10,6 +10,18 @@ from wf_cli.formats import ListOutputFormat, emit_list_payload
from wf_cli.io import CliInputError, emit_json, parse_bindings, parse_json_value from wf_cli.io import CliInputError, emit_json, parse_bindings, parse_json_value
from wf_cli.remote_errors import run_cli_operation from wf_cli.remote_errors import run_cli_operation
def _parse_map_flags(values: list[str] | None) -> dict[str, str]:
parsed: dict[str, str] = {}
for item in values or []:
source, separator, target = item.partition("=")
if separator != "=" or not source or not target:
raise typer.BadParameter("--map must use source=target")
if source in parsed:
raise typer.BadParameter(f"duplicate --map for {source!r}")
parsed[source] = target
return parsed
app = typer.Typer( app = typer.Typer(
name="draft", name="draft",
help="Create, inspect, patch, validate, and save draft workflows.", help="Create, inspect, patch, validate, and save draft workflows.",
@@ -118,6 +130,122 @@ def patch_draft(
) )
@app.command("set-name")
def set_draft_name(
ctx: typer.Context,
workspace_id: Annotated[str, typer.Argument(help="Draft workspace id.")],
revision: Annotated[
int, typer.Option("--revision", min=1, help="Expected workspace revision.")
],
name: Annotated[str, typer.Option("--name", help="New draft workflow name.")],
) -> None:
"""Set the draft workflow name without writing JSON Patch manually."""
context = load_cli_context(ctx)
emit_json(
run_cli_operation(
context,
context.handlers.set_draft_name(
workspace_id=workspace_id,
revision=revision,
name=name,
),
)
)
@app.command("set-route")
def set_draft_route(
ctx: typer.Context,
workspace_id: Annotated[str, typer.Argument(help="Draft workspace id.")],
revision: Annotated[
int, typer.Option("--revision", min=1, help="Expected workspace revision.")
],
step_id: Annotated[str, typer.Option("--step", help="Draft step id.")],
outcome: Annotated[str, typer.Option("--outcome", help="Step outcome.")],
target: Annotated[
str, typer.Option("--to", help="Target step id or __end__.")
],
) -> None:
"""Set one route: steps.<step> outcome -> target."""
context = load_cli_context(ctx)
emit_json(
run_cli_operation(
context,
context.handlers.set_draft_route(
workspace_id=workspace_id,
revision=revision,
step_id=step_id,
outcome=outcome,
target=target,
),
)
)
@app.command("set-input")
def set_step_input_map(
ctx: typer.Context,
workspace_id: Annotated[str, typer.Argument(help="Draft workspace id.")],
revision: Annotated[
int, typer.Option("--revision", min=1, help="Expected workspace revision.")
],
step_id: Annotated[str, typer.Option("--step", help="Draft step id.")],
mapping: Annotated[
list[str] | None,
typer.Option(
"--map",
help="Input binding SOURCE=LOCAL_TARGET. Repeatable. Example: input.text=text",
),
] = None,
) -> None:
"""Replace one step's input map without writing JSON Patch manually."""
input_map = _parse_map_flags(mapping)
context = load_cli_context(ctx)
emit_json(
run_cli_operation(
context,
context.handlers.set_step_input_map(
workspace_id=workspace_id,
revision=revision,
step_id=step_id,
input_map=input_map,
),
)
)
@app.command("set-output")
def set_step_output_map(
ctx: typer.Context,
workspace_id: Annotated[str, typer.Argument(help="Draft workspace id.")],
revision: Annotated[
int, typer.Option("--revision", min=1, help="Expected workspace revision.")
],
step_id: Annotated[str, typer.Option("--step", help="Draft step id.")],
mapping: Annotated[
list[str] | None,
typer.Option(
"--map",
help="Output binding LOCAL_SOURCE=STATE_TARGET. Repeatable. Example: text=state.text",
),
] = None,
) -> None:
"""Replace one step's output map without writing JSON Patch manually."""
output_map = _parse_map_flags(mapping)
context = load_cli_context(ctx)
emit_json(
run_cli_operation(
context,
context.handlers.set_step_output_map(
workspace_id=workspace_id,
revision=revision,
step_id=step_id,
output_map=output_map,
),
)
)
@app.command("validate") @app.command("validate")
def validate_draft( def validate_draft(
ctx: typer.Context, ctx: typer.Context,
+8
View File
@@ -29,6 +29,10 @@ from .models import (
ResumeRunParams, ResumeRunParams,
SaveArtifactParams, SaveArtifactParams,
SaveDeploymentParams, SaveDeploymentParams,
SetDraftNameParams,
SetDraftRouteParams,
SetStepInputMapParams,
SetStepOutputMapParams,
StartRunParams, StartRunParams,
TraceRangeParams, TraceRangeParams,
ValidateDeploymentParams, ValidateDeploymentParams,
@@ -62,6 +66,10 @@ __all__ = [
"ResumeRunParams", "ResumeRunParams",
"SaveArtifactParams", "SaveArtifactParams",
"SaveDeploymentParams", "SaveDeploymentParams",
"SetDraftNameParams",
"SetDraftRouteParams",
"SetStepInputMapParams",
"SetStepOutputMapParams",
"StartRunParams", "StartRunParams",
"TraceRangeParams", "TraceRangeParams",
"ValidateDeploymentParams", "ValidateDeploymentParams",
@@ -69,6 +69,74 @@ class RpcDraftClientMixin:
{"workspace_id": workspace_id, "revision": revision, "patch": patch}, {"workspace_id": workspace_id, "revision": revision, "patch": patch},
) )
async def set_draft_name(
self: RpcCaller,
*,
workspace_id: str,
revision: int,
name: str,
) -> dict[str, Any]:
return await self._call(
"workflow.draft_workspaces.set_name",
{"workspace_id": workspace_id, "revision": revision, "name": name},
)
async def set_draft_route(
self: RpcCaller,
*,
workspace_id: str,
revision: int,
step_id: str,
outcome: str,
target: str,
) -> dict[str, Any]:
return await self._call(
"workflow.draft_workspaces.set_route",
{
"workspace_id": workspace_id,
"revision": revision,
"step_id": step_id,
"outcome": outcome,
"target": target,
},
)
async def set_step_input_map(
self: RpcCaller,
*,
workspace_id: str,
revision: int,
step_id: str,
input_map: dict[str, str],
) -> dict[str, Any]:
return await self._call(
"workflow.draft_workspaces.set_step_input_map",
{
"workspace_id": workspace_id,
"revision": revision,
"step_id": step_id,
"input_map": input_map,
},
)
async def set_step_output_map(
self: RpcCaller,
*,
workspace_id: str,
revision: int,
step_id: str,
output_map: dict[str, str],
) -> dict[str, Any]:
return await self._call(
"workflow.draft_workspaces.set_step_output_map",
{
"workspace_id": workspace_id,
"revision": revision,
"step_id": step_id,
"output_map": output_map,
},
)
async def validate_draft_workspace( async def validate_draft_workspace(
self: RpcCaller, self: RpcCaller,
*, *,
@@ -16,6 +16,10 @@ from ..models import (
ListDraftWorkspacesParams, ListDraftWorkspacesParams,
PatchDraftParams, PatchDraftParams,
PatchDraftWorkspaceParams, PatchDraftWorkspaceParams,
SetDraftNameParams,
SetDraftRouteParams,
SetStepInputMapParams,
SetStepOutputMapParams,
ValidateDraftParams, ValidateDraftParams,
ValidateDraftWorkspaceParams, ValidateDraftWorkspaceParams,
) )
@@ -105,6 +109,72 @@ def register_methods(
except (ValueError, KeyError, LookupError, FileNotFoundError) as exc: except (ValueError, KeyError, LookupError, FileNotFoundError) as exc:
raise_workflow_rpc_error(exc) raise_workflow_rpc_error(exc)
@entrypoint.method(
name="workflow.draft_workspaces.set_name", errors=[WorkflowRpcError]
)
async def workflow_draft_workspaces_set_name(
params: SetDraftNameParams = RpcParams(),
) -> dict[str, Any]:
try:
return await server.api.set_draft_name(
workspace_id=params.workspace_id,
revision=params.revision,
name=params.name,
)
except (ValueError, KeyError, LookupError, FileNotFoundError) as exc:
raise_workflow_rpc_error(exc)
@entrypoint.method(
name="workflow.draft_workspaces.set_route", errors=[WorkflowRpcError]
)
async def workflow_draft_workspaces_set_route(
params: SetDraftRouteParams = RpcParams(),
) -> dict[str, Any]:
try:
return await server.api.set_draft_route(
workspace_id=params.workspace_id,
revision=params.revision,
step_id=params.step_id,
outcome=params.outcome,
target=params.target,
)
except (ValueError, KeyError, LookupError, FileNotFoundError) as exc:
raise_workflow_rpc_error(exc)
@entrypoint.method(
name="workflow.draft_workspaces.set_step_input_map",
errors=[WorkflowRpcError],
)
async def workflow_draft_workspaces_set_step_input_map(
params: SetStepInputMapParams = RpcParams(),
) -> dict[str, Any]:
try:
return await server.api.set_step_input_map(
workspace_id=params.workspace_id,
revision=params.revision,
step_id=params.step_id,
input_map=params.input_map,
)
except (ValueError, KeyError, LookupError, FileNotFoundError) as exc:
raise_workflow_rpc_error(exc)
@entrypoint.method(
name="workflow.draft_workspaces.set_step_output_map",
errors=[WorkflowRpcError],
)
async def workflow_draft_workspaces_set_step_output_map(
params: SetStepOutputMapParams = RpcParams(),
) -> dict[str, Any]:
try:
return await server.api.set_step_output_map(
workspace_id=params.workspace_id,
revision=params.revision,
step_id=params.step_id,
output_map=params.output_map,
)
except (ValueError, KeyError, LookupError, FileNotFoundError) as exc:
raise_workflow_rpc_error(exc)
@entrypoint.method( @entrypoint.method(
name="workflow.draft_workspaces.validate", errors=[WorkflowRpcError] name="workflow.draft_workspaces.validate", errors=[WorkflowRpcError]
) )
+28
View File
@@ -111,6 +111,34 @@ class PatchDraftWorkspaceParams(RpcParamsModel):
patch: list[dict[str, Any]] patch: list[dict[str, Any]]
class SetDraftNameParams(RpcParamsModel):
workspace_id: str = Field(min_length=1)
revision: int = Field(ge=1)
name: str = Field(min_length=1)
class SetDraftRouteParams(RpcParamsModel):
workspace_id: str = Field(min_length=1)
revision: int = Field(ge=1)
step_id: str = Field(min_length=1)
outcome: str = Field(min_length=1)
target: str = Field(min_length=1)
class SetStepInputMapParams(RpcParamsModel):
workspace_id: str = Field(min_length=1)
revision: int = Field(ge=1)
step_id: str = Field(min_length=1)
input_map: dict[str, str]
class SetStepOutputMapParams(RpcParamsModel):
workspace_id: str = Field(min_length=1)
revision: int = Field(ge=1)
step_id: str = Field(min_length=1)
output_map: dict[str, str]
class ValidateDraftWorkspaceParams(RpcParamsModel): class ValidateDraftWorkspaceParams(RpcParamsModel):
workspace_id: str = Field(min_length=1) workspace_id: str = Field(min_length=1)
+148
View File
@@ -943,3 +943,151 @@ def test_wf_local_uses_selected_config_sources(tmp_path: Path) -> None:
assert {capability["name"] for capability in payload["capabilities"]} == { assert {capability["name"] for capability in payload["capabilities"]} == {
"local.ops.echo" "local.ops.echo"
} }
def test_wf_draft_focused_edit_commands_use_rpc_target(monkeypatch, tmp_path) -> None:
server = build_local_static_workflow_server(tmp_path / "store")
_patch_rpc_client_to_server(monkeypatch, server)
config_path = tmp_path / "wf.json"
config_path.write_text('{"version": 1}', encoding="utf-8")
runner = CliRunner()
base_args = ["--config", str(config_path), "--url", "http://test/rpc"]
created = runner.invoke(
app,
[
*base_args,
"draft",
"create-from-capability",
"focused_ws",
"wf.std.constant",
"--name",
"focused_initial",
],
)
assert created.exit_code == 0, created.output
named = runner.invoke(
app,
[
*base_args,
"draft",
"set-name",
"focused_ws",
"--revision",
"1",
"--name",
"focused_renamed",
],
)
routed = runner.invoke(
app,
[
*base_args,
"draft",
"set-route",
"focused_ws",
"--revision",
"2",
"--step",
"call",
"--outcome",
"ok",
"--to",
"__end__",
],
)
input_mapped = runner.invoke(
app,
[
*base_args,
"draft",
"set-input",
"focused_ws",
"--revision",
"3",
"--step",
"call",
"--map",
"input.value=value",
],
)
output_mapped = runner.invoke(
app,
[
*base_args,
"draft",
"set-output",
"focused_ws",
"--revision",
"4",
"--step",
"call",
"--map",
"value=state.value",
],
)
inspected = runner.invoke(
app,
[*base_args, "draft", "inspect", "focused_ws", "--include-draft"],
)
assert named.exit_code == 0, named.output
assert routed.exit_code == 0, routed.output
assert input_mapped.exit_code == 0, input_mapped.output
assert output_mapped.exit_code == 0, output_mapped.output
assert inspected.exit_code == 0, inspected.output
payload = json.loads(inspected.output)
draft = payload["draft"]
assert draft["name"] == "focused_renamed"
assert draft["routes"]["call"]["ok"] == "__end__"
assert draft["steps"]["call"]["input"] == [
{
"target": {"root": "local", "parts": ["value"]},
"path": {"root": "input", "parts": ["value"]},
}
]
assert draft["steps"]["call"]["output"] == [
{
"source": {"root": "local", "parts": ["value"]},
"target": {"root": "state", "parts": ["value"]},
}
]
def test_wf_deploy_create_alias_saves_deployment(monkeypatch, tmp_path) -> None:
server = build_local_static_workflow_server(tmp_path / "store")
asyncio.run(
server.api.create_artifact_from_plan(
artifact_id="alias_artifact",
version=1,
title="Alias Artifact",
plan=_constant_plan(),
outcomes=("ok",),
)
)
_patch_rpc_client_to_server(monkeypatch, server)
config_path = tmp_path / "wf.json"
config_path.write_text('{"version": 1}', encoding="utf-8")
runner = CliRunner()
created = runner.invoke(
app,
[
"--config",
str(config_path),
"--url",
"http://test/rpc",
"deploy",
"create",
"alias_artifact.default",
"--artifact",
"alias_artifact",
"--version",
"1",
],
)
assert created.exit_code == 0, created.output
payload = json.loads(created.output)
assert payload["deployment_id"] == "alias_artifact.default"
+83
View File
@@ -644,6 +644,89 @@ async def test_rpc_create_artifact_from_plan(tmp_path) -> None:
assert inspected["result"]["plan"]["name"] == "rpc_constant" assert inspected["result"]["plan"]["name"] == "rpc_constant"
async def test_rpc_draft_workspace_focused_edit_methods(tmp_path) -> None:
server = build_local_static_workflow_server(tmp_path / "store")
app = create_rpc_app(server)
transport = httpx.ASGITransport(app=app)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
created = await _rpc(
client,
"workflow.draft_workspaces.create_from_capability",
{
"workspace_id": "focused_ws",
"capability_name": "wf.std.constant",
"name": "focused_initial",
},
)
assert created["result"]["workspace_id"] == "focused_ws"
named = await _rpc(
client,
"workflow.draft_workspaces.set_name",
{
"workspace_id": "focused_ws",
"revision": 1,
"name": "focused_renamed",
},
)
routed = await _rpc(
client,
"workflow.draft_workspaces.set_route",
{
"workspace_id": "focused_ws",
"revision": 2,
"step_id": "call",
"outcome": "ok",
"target": "__end__",
},
)
input_mapped = await _rpc(
client,
"workflow.draft_workspaces.set_step_input_map",
{
"workspace_id": "focused_ws",
"revision": 3,
"step_id": "call",
"input_map": {"input.value": "value"},
},
)
output_mapped = await _rpc(
client,
"workflow.draft_workspaces.set_step_output_map",
{
"workspace_id": "focused_ws",
"revision": 4,
"step_id": "call",
"output_map": {"value": "state.value"},
},
)
fetched = await _rpc(
client,
"workflow.draft_workspaces.get",
{"workspace_id": "focused_ws", "include_draft": True},
)
assert named["result"]["revision"] == 2
assert routed["result"]["revision"] == 3
assert input_mapped["result"]["revision"] == 4
assert output_mapped["result"]["revision"] == 5
draft = fetched["result"]["draft"]
assert draft["name"] == "focused_renamed"
assert draft["routes"]["call"]["ok"] == "__end__"
assert draft["steps"]["call"]["input"] == [
{
"target": {"root": "local", "parts": ["value"]},
"path": {"root": "input", "parts": ["value"]},
}
]
assert draft["steps"]["call"]["output"] == [
{
"source": {"root": "local", "parts": ["value"]},
"target": {"root": "state", "parts": ["value"]},
}
]
async def test_rpc_diagnoses_source(tmp_path) -> None: async def test_rpc_diagnoses_source(tmp_path) -> None:
server = build_local_static_workflow_server(tmp_path / "store") server = build_local_static_workflow_server(tmp_path / "store")
app = create_rpc_app(server) app = create_rpc_app(server)
@@ -445,6 +445,53 @@ async def test_rpc_client_creates_artifact_from_plan(tmp_path) -> None:
assert inspected["id"] == "client_plan" assert inspected["id"] == "client_plan"
async def test_rpc_client_draft_workspace_focused_edit_methods(tmp_path) -> None:
server = build_local_static_workflow_server(tmp_path / "store")
app = create_rpc_app(server)
transport = httpx.ASGITransport(app=app)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as http_client:
client = RpcWorkflowApiClient(
url="http://test/rpc",
timeout_seconds=5,
http_client=http_client,
)
await client.create_draft_workspace_from_capability(
workspace_id="client_focused_ws",
capability_name="wf.std.constant",
name="client_initial",
)
named = await client.set_draft_name(
workspace_id="client_focused_ws",
revision=1,
name="client_renamed",
)
routed = await client.set_draft_route(
workspace_id="client_focused_ws",
revision=2,
step_id="call",
outcome="ok",
target="__end__",
)
input_mapped = await client.set_step_input_map(
workspace_id="client_focused_ws",
revision=3,
step_id="call",
input_map={"input.value": "value"},
)
output_mapped = await client.set_step_output_map(
workspace_id="client_focused_ws",
revision=4,
step_id="call",
output_map={"value": "state.value"},
)
assert named["revision"] == 2
assert routed["revision"] == 3
assert input_mapped["revision"] == 4
assert output_mapped["revision"] == 5
async def test_rpc_client_diagnoses_source(tmp_path) -> None: async def test_rpc_client_diagnoses_source(tmp_path) -> None:
calls: list[tuple[str, dict[str, object]]] = [] calls: list[tuple[str, dict[str, object]]] = []