feat: add focused draft remove commands
This commit is contained in:
@@ -482,6 +482,143 @@ class WorkflowDraftAuthoringApi:
|
||||
patch=patch,
|
||||
)
|
||||
|
||||
async def remove_draft_route(
|
||||
self,
|
||||
*,
|
||||
workspace_id: str,
|
||||
revision: int,
|
||||
step_id: str,
|
||||
outcome: str,
|
||||
) -> dict[str, Any]:
|
||||
"""Remove one route; missing routes are revision-checked no-ops."""
|
||||
workspace = self.drafts._draft_store().get_workspace(workspace_id)
|
||||
draft_routes = workspace.draft.get("routes", {})
|
||||
if not isinstance(draft_routes, dict):
|
||||
raise ValueError("draft routes must be an object")
|
||||
step_routes = draft_routes.get(step_id, {})
|
||||
if not isinstance(step_routes, dict):
|
||||
raise ValueError(f"routes for step {step_id!r} must be an object")
|
||||
if outcome not in step_routes:
|
||||
checked = self._workspace_if_revision_matches(
|
||||
workspace_id=workspace_id,
|
||||
revision=revision,
|
||||
)
|
||||
if isinstance(checked, dict):
|
||||
return checked
|
||||
return summarize_draft_workspace(checked)
|
||||
return await self.drafts.patch_draft_workspace(
|
||||
workspace_id=workspace_id,
|
||||
revision=revision,
|
||||
patch=[
|
||||
{
|
||||
"op": "remove",
|
||||
"path": (
|
||||
f"/routes/{escape_json_pointer(step_id)}/"
|
||||
f"{escape_json_pointer(outcome)}"
|
||||
),
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
async def remove_draft_step(
|
||||
self,
|
||||
*,
|
||||
workspace_id: str,
|
||||
revision: int,
|
||||
step_id: str,
|
||||
) -> dict[str, Any]:
|
||||
"""Remove a step and its own route map; inbound routes are left explicit."""
|
||||
workspace = self.drafts._draft_store().get_workspace(workspace_id)
|
||||
steps = workspace.draft.get("steps", {})
|
||||
if not isinstance(steps, dict):
|
||||
raise ValueError("draft steps must be an object")
|
||||
if step_id not in steps:
|
||||
checked = self._workspace_if_revision_matches(
|
||||
workspace_id=workspace_id,
|
||||
revision=revision,
|
||||
)
|
||||
if isinstance(checked, dict):
|
||||
return checked
|
||||
return summarize_draft_workspace(checked)
|
||||
patch = [
|
||||
{
|
||||
"op": "remove",
|
||||
"path": f"/steps/{escape_json_pointer(step_id)}",
|
||||
}
|
||||
]
|
||||
routes = workspace.draft.get("routes", {})
|
||||
if isinstance(routes, dict) and step_id in routes:
|
||||
patch.append(
|
||||
{
|
||||
"op": "remove",
|
||||
"path": f"/routes/{escape_json_pointer(step_id)}",
|
||||
}
|
||||
)
|
||||
return await self.drafts.patch_draft_workspace(
|
||||
workspace_id=workspace_id,
|
||||
revision=revision,
|
||||
patch=patch,
|
||||
)
|
||||
|
||||
async def remove_draft_binding(
|
||||
self,
|
||||
*,
|
||||
workspace_id: str,
|
||||
revision: int,
|
||||
step_id: str,
|
||||
inputs: Sequence[str] = (),
|
||||
outputs: Sequence[str] = (),
|
||||
) -> dict[str, Any]:
|
||||
"""Remove selected local input/output bindings from one draft step."""
|
||||
if not inputs and not outputs:
|
||||
raise ValueError("pass at least one input or output binding to remove")
|
||||
workspace = self.drafts._draft_store().get_workspace(workspace_id)
|
||||
step = draft_step(workspace.draft, step_id)
|
||||
current_inputs = step.get("input", [])
|
||||
current_outputs = step.get("output", [])
|
||||
if not isinstance(current_inputs, list):
|
||||
raise ValueError(f"input bindings for step {step_id!r} must be a list")
|
||||
if not isinstance(current_outputs, list):
|
||||
raise ValueError(f"output bindings for step {step_id!r} must be a list")
|
||||
input_targets = set(inputs)
|
||||
output_sources = set(outputs)
|
||||
next_inputs = [
|
||||
item for item in current_inputs if item.get("target") not in input_targets
|
||||
]
|
||||
next_outputs = [
|
||||
item for item in current_outputs if item.get("source") not in output_sources
|
||||
]
|
||||
if next_inputs == current_inputs and next_outputs == current_outputs:
|
||||
checked = self._workspace_if_revision_matches(
|
||||
workspace_id=workspace_id,
|
||||
revision=revision,
|
||||
)
|
||||
if isinstance(checked, dict):
|
||||
return checked
|
||||
return summarize_draft_workspace(checked)
|
||||
patch: list[dict[str, Any]] = []
|
||||
if next_inputs != current_inputs:
|
||||
patch.append(
|
||||
{
|
||||
"op": "replace",
|
||||
"path": f"/steps/{escape_json_pointer(step_id)}/input",
|
||||
"value": next_inputs,
|
||||
}
|
||||
)
|
||||
if next_outputs != current_outputs:
|
||||
patch.append(
|
||||
{
|
||||
"op": "replace",
|
||||
"path": f"/steps/{escape_json_pointer(step_id)}/output",
|
||||
"value": next_outputs,
|
||||
}
|
||||
)
|
||||
return await self.drafts.patch_draft_workspace(
|
||||
workspace_id=workspace_id,
|
||||
revision=revision,
|
||||
patch=patch,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DraftOutcomeRef:
|
||||
|
||||
@@ -478,6 +478,51 @@ class WorkflowApi:
|
||||
title=title,
|
||||
)
|
||||
|
||||
async def remove_draft_route(
|
||||
self,
|
||||
*,
|
||||
workspace_id: str,
|
||||
revision: int,
|
||||
step_id: str,
|
||||
outcome: str,
|
||||
) -> dict[str, Any]:
|
||||
return await self.draft_authoring.remove_draft_route(
|
||||
workspace_id=workspace_id,
|
||||
revision=revision,
|
||||
step_id=step_id,
|
||||
outcome=outcome,
|
||||
)
|
||||
|
||||
async def remove_draft_step(
|
||||
self,
|
||||
*,
|
||||
workspace_id: str,
|
||||
revision: int,
|
||||
step_id: str,
|
||||
) -> dict[str, Any]:
|
||||
return await self.draft_authoring.remove_draft_step(
|
||||
workspace_id=workspace_id,
|
||||
revision=revision,
|
||||
step_id=step_id,
|
||||
)
|
||||
|
||||
async def remove_draft_binding(
|
||||
self,
|
||||
*,
|
||||
workspace_id: str,
|
||||
revision: int,
|
||||
step_id: str,
|
||||
inputs: Sequence[str] = (),
|
||||
outputs: Sequence[str] = (),
|
||||
) -> dict[str, Any]:
|
||||
return await self.draft_authoring.remove_draft_binding(
|
||||
workspace_id=workspace_id,
|
||||
revision=revision,
|
||||
step_id=step_id,
|
||||
inputs=inputs,
|
||||
outputs=outputs,
|
||||
)
|
||||
|
||||
async def create_draft_workspace_from_capability(
|
||||
self,
|
||||
*,
|
||||
|
||||
@@ -156,6 +156,33 @@ class WorkflowDraftSurface(Protocol):
|
||||
target: str,
|
||||
) -> dict[str, Any]: ...
|
||||
|
||||
async def remove_draft_route(
|
||||
self,
|
||||
*,
|
||||
workspace_id: str,
|
||||
revision: int,
|
||||
step_id: str,
|
||||
outcome: str,
|
||||
) -> dict[str, Any]: ...
|
||||
|
||||
async def remove_draft_step(
|
||||
self,
|
||||
*,
|
||||
workspace_id: str,
|
||||
revision: int,
|
||||
step_id: str,
|
||||
) -> dict[str, Any]: ...
|
||||
|
||||
async def remove_draft_binding(
|
||||
self,
|
||||
*,
|
||||
workspace_id: str,
|
||||
revision: int,
|
||||
step_id: str,
|
||||
inputs: Sequence[str] = (),
|
||||
outputs: Sequence[str] = (),
|
||||
) -> dict[str, Any]: ...
|
||||
|
||||
async def validate_draft_workspace(
|
||||
self,
|
||||
*,
|
||||
|
||||
@@ -489,6 +489,92 @@ def handle_draft(
|
||||
)
|
||||
|
||||
|
||||
@app.command("remove-route")
|
||||
def remove_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: Annotated[str, typer.Option("--step", help="Draft step id.")],
|
||||
outcome: Annotated[str, typer.Option("--outcome", help="Outcome route to remove.")],
|
||||
) -> None:
|
||||
"""Remove one route from a draft step."""
|
||||
context = load_cli_context(ctx)
|
||||
emit_json(
|
||||
run_cli_operation(
|
||||
context,
|
||||
context.handlers.remove_draft_route(
|
||||
workspace_id=workspace_id,
|
||||
revision=revision,
|
||||
step_id=step,
|
||||
outcome=outcome,
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@app.command("remove-step")
|
||||
def remove_draft_step(
|
||||
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: Annotated[str, typer.Option("--step", help="Draft step id.")],
|
||||
) -> None:
|
||||
"""Remove one step and its outgoing draft route map."""
|
||||
context = load_cli_context(ctx)
|
||||
emit_json(
|
||||
run_cli_operation(
|
||||
context,
|
||||
context.handlers.remove_draft_step(
|
||||
workspace_id=workspace_id,
|
||||
revision=revision,
|
||||
step_id=step,
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@app.command("remove-binding")
|
||||
def remove_draft_binding(
|
||||
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: Annotated[str, typer.Option("--step", help="Draft step id.")],
|
||||
input_name: Annotated[
|
||||
list[str] | None,
|
||||
typer.Option("--input", help="Local input target to remove. Repeatable."),
|
||||
] = None,
|
||||
output_name: Annotated[
|
||||
list[str] | None,
|
||||
typer.Option("--output", help="Local output source to remove. Repeatable."),
|
||||
] = None,
|
||||
) -> None:
|
||||
"""Remove selected input/output bindings from one draft step.
|
||||
|
||||
Removal may return status: invalid. Run `wf draft validate` after cleanup.
|
||||
"""
|
||||
if not input_name and not output_name:
|
||||
raise typer.BadParameter("pass at least one --input or --output")
|
||||
context = load_cli_context(ctx)
|
||||
emit_json(
|
||||
run_cli_operation(
|
||||
context,
|
||||
context.handlers.remove_draft_binding(
|
||||
workspace_id=workspace_id,
|
||||
revision=revision,
|
||||
step_id=step,
|
||||
inputs=input_name or [],
|
||||
outputs=output_name or [],
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@app.command("validate")
|
||||
def validate_draft(
|
||||
ctx: typer.Context,
|
||||
|
||||
@@ -286,6 +286,39 @@ class AddStepFromCapabilityRequest(BaseModel):
|
||||
)
|
||||
|
||||
|
||||
class RemoveDraftRouteRequest(BaseModel):
|
||||
"""Typed MCP request for removing one route from a draft step."""
|
||||
|
||||
workspace_id: WorkspaceId
|
||||
revision: int = Field(ge=1, description="Expected current workspace revision.")
|
||||
step_id: str = Field(description="Draft step id whose route should be removed.")
|
||||
outcome: str = Field(description="Outcome label to remove from the step route map.")
|
||||
|
||||
|
||||
class RemoveDraftStepRequest(BaseModel):
|
||||
"""Typed MCP request for removing a draft step and its outgoing routes."""
|
||||
|
||||
workspace_id: WorkspaceId
|
||||
revision: int = Field(ge=1, description="Expected current workspace revision.")
|
||||
step_id: str = Field(description="Draft step id to remove.")
|
||||
|
||||
|
||||
class RemoveDraftBindingRequest(BaseModel):
|
||||
"""Typed MCP request for removing input/output bindings from a draft step."""
|
||||
|
||||
workspace_id: WorkspaceId
|
||||
revision: int = Field(ge=1, description="Expected current workspace revision.")
|
||||
step_id: str = Field(description="Draft step id whose bindings should be removed.")
|
||||
inputs: list[str] = Field(
|
||||
default_factory=list,
|
||||
description="Local input target names to remove.",
|
||||
)
|
||||
outputs: list[str] = Field(
|
||||
default_factory=list,
|
||||
description="Local output source names to remove.",
|
||||
)
|
||||
|
||||
|
||||
class BranchDraftRequest(BaseModel):
|
||||
"""Typed MCP request for branching routes on a draft step."""
|
||||
|
||||
|
||||
@@ -29,6 +29,9 @@ from .models import (
|
||||
DraftWorkspaceResult,
|
||||
HandleDraftRequest,
|
||||
PatchDraftWorkspaceRequest,
|
||||
RemoveDraftBindingRequest,
|
||||
RemoveDraftRouteRequest,
|
||||
RemoveDraftStepRequest,
|
||||
RunDeploymentResult,
|
||||
SetDraftNameRequest,
|
||||
SetDraftRouteRequest,
|
||||
@@ -543,6 +546,67 @@ def register_workflow_tools(server: FastMCP[Any], service: WfMcpService) -> None
|
||||
)
|
||||
)
|
||||
|
||||
@server.tool(
|
||||
name="wf.workflow.remove_draft_route",
|
||||
title="Remove Draft Route",
|
||||
description=(
|
||||
"Remove one route from a draft step. Missing routes are safe no-ops. "
|
||||
"Removal may return status: invalid; validate after cleanup."
|
||||
),
|
||||
)
|
||||
async def remove_draft_route(
|
||||
request: RemoveDraftRouteRequest,
|
||||
) -> DraftWorkspaceResult:
|
||||
return DraftWorkspaceResult.model_validate(
|
||||
await handlers.remove_draft_route(
|
||||
workspace_id=request.workspace_id,
|
||||
revision=request.revision,
|
||||
step_id=request.step_id,
|
||||
outcome=request.outcome,
|
||||
)
|
||||
)
|
||||
|
||||
@server.tool(
|
||||
name="wf.workflow.remove_draft_step",
|
||||
title="Remove Draft Step",
|
||||
description=(
|
||||
"Remove a step and its outgoing route map from a draft workspace. "
|
||||
"Inbound routes remain explicit. Use remove_draft_route to clean them up."
|
||||
),
|
||||
)
|
||||
async def remove_draft_step(
|
||||
request: RemoveDraftStepRequest,
|
||||
) -> DraftWorkspaceResult:
|
||||
return DraftWorkspaceResult.model_validate(
|
||||
await handlers.remove_draft_step(
|
||||
workspace_id=request.workspace_id,
|
||||
revision=request.revision,
|
||||
step_id=request.step_id,
|
||||
)
|
||||
)
|
||||
|
||||
@server.tool(
|
||||
name="wf.workflow.remove_draft_binding",
|
||||
title="Remove Draft Binding",
|
||||
description=(
|
||||
"Remove selected input/output bindings from one draft step. "
|
||||
"Pass inputs and/or outputs lists to identify bindings by target/source name. "
|
||||
"Removal may return status: invalid; validate after cleanup."
|
||||
),
|
||||
)
|
||||
async def remove_draft_binding(
|
||||
request: RemoveDraftBindingRequest,
|
||||
) -> DraftWorkspaceResult:
|
||||
return DraftWorkspaceResult.model_validate(
|
||||
await handlers.remove_draft_binding(
|
||||
workspace_id=request.workspace_id,
|
||||
revision=request.revision,
|
||||
step_id=request.step_id,
|
||||
inputs=request.inputs,
|
||||
outputs=request.outputs,
|
||||
)
|
||||
)
|
||||
|
||||
@server.tool(
|
||||
name="wf.workflow.create_minimal_draft_workspace",
|
||||
title="Create Minimal Draft Workspace",
|
||||
|
||||
@@ -32,6 +32,9 @@ from .models import (
|
||||
PatchDraftParams,
|
||||
PatchDraftWorkspaceParams,
|
||||
ReadRunTraceParams,
|
||||
RemoveDraftBindingParams,
|
||||
RemoveDraftRouteParams,
|
||||
RemoveDraftStepParams,
|
||||
ResumeRunParams,
|
||||
SaveArtifactParams,
|
||||
SaveDeploymentParams,
|
||||
@@ -75,6 +78,9 @@ __all__ = [
|
||||
"PatchDraftParams",
|
||||
"PatchDraftWorkspaceParams",
|
||||
"ReadRunTraceParams",
|
||||
"RemoveDraftBindingParams",
|
||||
"RemoveDraftRouteParams",
|
||||
"RemoveDraftStepParams",
|
||||
"ResumeRunParams",
|
||||
"SaveArtifactParams",
|
||||
"SaveDeploymentParams",
|
||||
|
||||
@@ -225,6 +225,60 @@ class RpcDraftClientMixin:
|
||||
},
|
||||
)
|
||||
|
||||
async def remove_draft_route(
|
||||
self: RpcCaller,
|
||||
*,
|
||||
workspace_id: str,
|
||||
revision: int,
|
||||
step_id: str,
|
||||
outcome: str,
|
||||
) -> dict[str, Any]:
|
||||
return await self._call(
|
||||
"workflow.draft_workspaces.remove_route",
|
||||
{
|
||||
"workspace_id": workspace_id,
|
||||
"revision": revision,
|
||||
"step_id": step_id,
|
||||
"outcome": outcome,
|
||||
},
|
||||
)
|
||||
|
||||
async def remove_draft_step(
|
||||
self: RpcCaller,
|
||||
*,
|
||||
workspace_id: str,
|
||||
revision: int,
|
||||
step_id: str,
|
||||
) -> dict[str, Any]:
|
||||
return await self._call(
|
||||
"workflow.draft_workspaces.remove_step",
|
||||
{
|
||||
"workspace_id": workspace_id,
|
||||
"revision": revision,
|
||||
"step_id": step_id,
|
||||
},
|
||||
)
|
||||
|
||||
async def remove_draft_binding(
|
||||
self: RpcCaller,
|
||||
*,
|
||||
workspace_id: str,
|
||||
revision: int,
|
||||
step_id: str,
|
||||
inputs: Sequence[str] = (),
|
||||
outputs: Sequence[str] = (),
|
||||
) -> dict[str, Any]:
|
||||
return await self._call(
|
||||
"workflow.draft_workspaces.remove_binding",
|
||||
{
|
||||
"workspace_id": workspace_id,
|
||||
"revision": revision,
|
||||
"step_id": step_id,
|
||||
"inputs": list(inputs),
|
||||
"outputs": list(outputs),
|
||||
},
|
||||
)
|
||||
|
||||
async def validate_draft_workspace(
|
||||
self: RpcCaller,
|
||||
*,
|
||||
|
||||
@@ -21,6 +21,9 @@ from ..models import (
|
||||
ListDraftWorkspacesParams,
|
||||
PatchDraftParams,
|
||||
PatchDraftWorkspaceParams,
|
||||
RemoveDraftBindingParams,
|
||||
RemoveDraftRouteParams,
|
||||
RemoveDraftStepParams,
|
||||
SetDraftNameParams,
|
||||
SetDraftRouteParams,
|
||||
SetStepInputMapParams,
|
||||
@@ -296,6 +299,54 @@ def register_methods(
|
||||
except (ValueError, KeyError, LookupError, FileNotFoundError) as exc:
|
||||
raise_workflow_rpc_error(exc)
|
||||
|
||||
@entrypoint.method(
|
||||
name="workflow.draft_workspaces.remove_route", errors=[WorkflowRpcError]
|
||||
)
|
||||
async def workflow_draft_workspaces_remove_route(
|
||||
params: RemoveDraftRouteParams = RpcParams(),
|
||||
) -> dict[str, Any]:
|
||||
try:
|
||||
return await server.api.remove_draft_route(
|
||||
workspace_id=params.workspace_id,
|
||||
revision=params.revision,
|
||||
step_id=params.step_id,
|
||||
outcome=params.outcome,
|
||||
)
|
||||
except (ValueError, KeyError, LookupError, FileNotFoundError) as exc:
|
||||
raise_workflow_rpc_error(exc)
|
||||
|
||||
@entrypoint.method(
|
||||
name="workflow.draft_workspaces.remove_step", errors=[WorkflowRpcError]
|
||||
)
|
||||
async def workflow_draft_workspaces_remove_step(
|
||||
params: RemoveDraftStepParams = RpcParams(),
|
||||
) -> dict[str, Any]:
|
||||
try:
|
||||
return await server.api.remove_draft_step(
|
||||
workspace_id=params.workspace_id,
|
||||
revision=params.revision,
|
||||
step_id=params.step_id,
|
||||
)
|
||||
except (ValueError, KeyError, LookupError, FileNotFoundError) as exc:
|
||||
raise_workflow_rpc_error(exc)
|
||||
|
||||
@entrypoint.method(
|
||||
name="workflow.draft_workspaces.remove_binding", errors=[WorkflowRpcError]
|
||||
)
|
||||
async def workflow_draft_workspaces_remove_binding(
|
||||
params: RemoveDraftBindingParams = RpcParams(),
|
||||
) -> dict[str, Any]:
|
||||
try:
|
||||
return await server.api.remove_draft_binding(
|
||||
workspace_id=params.workspace_id,
|
||||
revision=params.revision,
|
||||
step_id=params.step_id,
|
||||
inputs=params.inputs,
|
||||
outputs=params.outputs,
|
||||
)
|
||||
except (ValueError, KeyError, LookupError, FileNotFoundError) as exc:
|
||||
raise_workflow_rpc_error(exc)
|
||||
|
||||
@entrypoint.method(
|
||||
name="workflow.draft_workspaces.create_artifact", errors=[WorkflowRpcError]
|
||||
)
|
||||
|
||||
@@ -180,6 +180,27 @@ class HandleDraftParams(RpcParamsModel):
|
||||
target: str = Field(min_length=1)
|
||||
|
||||
|
||||
class RemoveDraftRouteParams(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)
|
||||
|
||||
|
||||
class RemoveDraftStepParams(RpcParamsModel):
|
||||
workspace_id: str = Field(min_length=1)
|
||||
revision: int = Field(ge=1)
|
||||
step_id: str = Field(min_length=1)
|
||||
|
||||
|
||||
class RemoveDraftBindingParams(RpcParamsModel):
|
||||
workspace_id: str = Field(min_length=1)
|
||||
revision: int = Field(ge=1)
|
||||
step_id: str = Field(min_length=1)
|
||||
inputs: list[str] = Field(default_factory=list)
|
||||
outputs: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class ValidateDraftWorkspaceParams(RpcParamsModel):
|
||||
workspace_id: str = Field(min_length=1)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user